0

I'm trying to rasterise text in a specified font into an image to be written to a file; this is part of typeface testing, so layout, line breaks, etc. do not matter, but OpenType features like substitution do matter.

On Windows, I'm trying to use D2D/DWrite. Versions:

  • Windows11 version 10.0.26100
  • Microsoft (R) C/C++ Optimizing Compiler Version 19.30.30709 for x64
  • Microsoft (R) Incremental Linker Version 14.30.30709.0

The problem I'm having is with the bounding box calculation. While the image was the correct width, it was too tall, with tons of blank space towards the bottom. Here's a "minimum" repro with error checking etc. elided:

#include <string>
#include <iostream>
#include <fstream>
#include <filesystem>
#define NOMINMAX
#include <comdef.h>
#include <wrl/client.h>
#include <d2d1_1.h>
#include <dwrite_3.h>
#include <wincodec.h>
using namespace std;
using std::string;
using std::unique_ptr;
using std::filesystem::path;
using Microsoft::WRL::ComPtr;
using D2D1::ColorF;
using D2D1::RectF;
using D2D1::PixelFormat;
using D2D1::Matrix3x2F;
int main() {
 CoInitializeEx(nullptr, COINIT_MULTITHREADED);
 ComPtr<IWICImagingFactory2> wic_factory;
 CoCreateInstance(CLSID_WICImagingFactory, nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&wic_factory));
 ComPtr<ID2D1Factory1> d2d_factory;
 const auto options = D2D1_FACTORY_OPTIONS{};
 D2D1CreateFactory(D2D1_FACTORY_TYPE_SINGLE_THREADED, __uuidof(d2d_factory), &options, &d2d_factory);
 ComPtr<IDWriteFactory5> dwrite_factory;
 DWriteCreateFactory(DWRITE_FACTORY_TYPE_SHARED, __uuidof(dwrite_factory), &dwrite_factory);
 auto font_set_builder = ComPtr<IDWriteFontSetBuilder1>{};
 dwrite_factory->CreateFontSetBuilder(&font_set_builder);
 const auto typeface_file_path = path{"./Verdana.ttf"s};
 auto font_file = ComPtr<IDWriteFontFile>{};
 dwrite_factory->CreateFontFileReference(absolute(typeface_file_path).c_str(), nullptr, &font_file);
 font_set_builder->AddFontFile(font_file.Get());
 auto font_set = ComPtr<IDWriteFontSet>{};
 font_set_builder->CreateFontSet(&font_set);
 auto font_collection = ComPtr<IDWriteFontCollection1>{};
 dwrite_factory->CreateFontCollectionFromFontSet(font_set.Get(), &font_collection);
 const auto typeface_name = L"Verdana";
 const auto typeface_size_pt = 48u;
 ComPtr<IDWriteTextFormat> text_format;
 dwrite_factory->CreateTextFormat(typeface_name, font_collection.Get(), DWRITE_FONT_WEIGHT_NORMAL, DWRITE_FONT_STYLE_NORMAL, DWRITE_FONT_STRETCH_NORMAL, typeface_size_pt, L"", &text_format);
 const auto utf16_text = L"f"s;
 auto dwrite_text_layout = ComPtr<IDWriteTextLayout>{};
 dwrite_factory->CreateTextLayout(utf16_text.data(), static_cast<uint32_t>(utf16_text.length()), text_format.Get(), numeric_limits<float>::max(), numeric_limits<float>::max(), &dwrite_text_layout);
 auto metrics = DWRITE_TEXT_METRICS{};
 dwrite_text_layout->GetMetrics(&metrics);
 auto overhang_metrics = DWRITE_OVERHANG_METRICS{};
 dwrite_text_layout->GetOverhangMetrics(&overhang_metrics);
 const auto bounding_box = RectF(overhang_metrics.left, overhang_metrics.top, metrics.width, metrics.height);
 const auto width = static_cast<unsigned int>(bounding_box.right - bounding_box.left);
 const auto height = static_cast<unsigned int>(bounding_box.bottom - bounding_box.top);
 auto wic_bitmap = ComPtr<IWICBitmap>{};
 wic_factory->CreateBitmap(width, height, GUID_WICPixelFormat32bppBGR, WICBitmapCacheOnDemand, &wic_bitmap);
 const auto pixel_format = PixelFormat(DXGI_FORMAT_UNKNOWN, D2D1_ALPHA_MODE_IGNORE);
 const auto render_props = D2D1_RENDER_TARGET_PROPERTIES{D2D1_RENDER_TARGET_TYPE_DEFAULT, pixel_format, 0, 0, D2D1_RENDER_TARGET_USAGE_NONE, D2D1_FEATURE_LEVEL_DEFAULT,};
 auto render_target = ComPtr<ID2D1RenderTarget>{};
 d2d_factory->CreateWicBitmapRenderTarget(wic_bitmap.Get(), &render_props, &render_target);
 render_target->SetTextAntialiasMode(D2D1_TEXT_ANTIALIAS_MODE::D2D1_TEXT_ANTIALIAS_MODE_ALIASED);
 render_target->BeginDraw();
 render_target->Clear(ColorF(ColorF::White));
 auto black_brush = ComPtr<ID2D1SolidColorBrush>{};
 render_target->CreateSolidColorBrush(ColorF(ColorF::Black), &black_brush);
 const auto origin = D2D1_POINT_2F{bounding_box.left, bounding_box.top};
 render_target->DrawTextLayout(origin, dwrite_text_layout.Get(), black_brush.Get(), D2D1_DRAW_TEXT_OPTIONS_NONE);
 render_target->EndDraw();
 auto stream = ComPtr<IWICStream>{};
 wic_factory->CreateStream(&stream);
 stream->InitializeFromFilename(L"./output.png", GENERIC_WRITE);
 auto wic_bitmap_encoder = ComPtr<IWICBitmapEncoder>{};
 wic_factory->CreateEncoder(GUID_ContainerFormatPng, nullptr, &wic_bitmap_encoder);
 wic_bitmap_encoder->Initialize(stream.Get(), WICBitmapEncoderNoCache);
 auto wic_frame_encode = ComPtr<IWICBitmapFrameEncode>{};
 wic_bitmap_encoder->CreateNewFrame(&wic_frame_encode, nullptr);
 wic_frame_encode->Initialize(nullptr);
 wic_frame_encode->WriteSource(wic_bitmap.Get(), nullptr);
 wic_frame_encode->Commit();
 wic_bitmap_encoder->Commit();
 CoUninitialize();
 return EXIT_SUCCESS;
}

I tested it like so:

cl /utf-8 /EHsc /std:c++latest d2d1.lib dwrite.lib min_repro.cpp
copy C:\Windows\Fonts\verdana.ttf .
min_repro.exe
start output.png

Notice output.png has the right width but lots of whitespace at the bottom. Looks like my bounding_box calculation using GetMetrics/GetOverhangMetrics may be wrong?

asked Aug 17 at 0:38
3
  • IDWriteTextLayout::GetMetrics is the good way to determine a layout's bounds. You can try IDWriteTextLayout2::GetMetrics1 to get heightIncludingTrailingWhitespace (or IDWriteTextLayout3 for more info) but it should work fine. Or your issue lies elsewhere and you should provide a simple reproducing project. Commented Aug 17 at 7:27
  • Ok, it depends on what you want to do, if you need to draw text, you usually use GetMetrics. If you remove the call to GetOverhangMetrics in your code, you'll see the box is quite allright, but you will have empty space around. That's because you're drawing text (which needs to have some overall consistency, for example like a O with top & bottom overhands after an E), not glyphs. this is different. DirectWrite has some support for working with glyphs for a font (metrics & rendering) learn.microsoft.com/en-us/windows/win32/directwrite/… Commented Aug 18 at 7:19
  • If you use IDWriteBitmapRenderTarget::DrawGlyphRun it can return the bounding box rectangle for the drawn glyphs. You'd need to populate a DWRITE_GLYPH_RUN. To get glyph IDs / advances / offsets, you'd use IDWriteTextAnalyzer methods. Commented Aug 20 at 15:36

0

Know someone who can answer? Share a link to this question via email, Twitter, or Facebook.

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.