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?
IDWriteTextLayout::GetMetricsis the good way to determine a layout's bounds. You can tryIDWriteTextLayout2::GetMetrics1to getheightIncludingTrailingWhitespace(orIDWriteTextLayout3for more info) but it should work fine. Or your issue lies elsewhere and you should provide a simple reproducing project.IDWriteBitmapRenderTarget::DrawGlyphRunit can return the bounding box rectangle for the drawn glyphs. You'd need to populate aDWRITE_GLYPH_RUN. To get glyph IDs / advances / offsets, you'd useIDWriteTextAnalyzermethods.