基于D2D/DWrite的文本光栅化:字形路径边界框计算异常问题
D2D/DWrite文本光栅化图片底部空白问题解决方案
问题背景
我正尝试将指定字体的文本光栅化并写入图片文件,用于字体测试,因此排版、换行等无需考虑,但OpenType替换等特性需生效。在Windows环境(Windows11 版本10.0.26100、Microsoft (R) C/C++优化编译器19.30.30709 x64、Microsoft (R)增量链接器14.30.30709.0)下使用D2D/DWrite实现时,遇到边界框计算问题:生成的图片宽度正确,但底部存在大量空白。
复现代码
#include <string> #include <iostream> #include <fstream> #include <filesystem> #include <limits> #include <cmath> #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; }
测试步骤
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
问题原因与修正方案
核心错误点
原代码中RectF bounding_box的构造逻辑完全错误:RectF的参数是(left, top, right, bottom),但你传入的是(overhang_metrics.left, overhang_metrics.top, metrics.width, metrics.height),其中metrics.width是文本自身宽度(不是right坐标),metrics.height是文本行高(不是bottom坐标),导致计算出的高度包含了不必要的空白区域。
修正后的关键代码
替换原代码中边界框、尺寸计算及绘制原点的部分:
auto metrics = DWRITE_TEXT_METRICS{}; dwrite_text_layout->GetMetrics(&metrics); auto overhang_metrics = DWRITE_OVERHANG_METRICS{}; dwrite_text_layout->GetOverhangMetrics(&overhang_metrics); // 计算完整的文本包围盒尺寸:包含所有overhang区域 const float total_width = metrics.width + overhang_metrics.left + overhang_metrics.right; const float total_height = metrics.height + overhang_metrics.top + overhang_metrics.bottom; // 向上取整避免浮点截断导致图片尺寸不足 const auto width = static_cast<unsigned int>(ceil(total_width)); const auto height = static_cast<unsigned int>(ceil(total_height)); // 绘制原点:将文本的最左上角对齐到图片左上角,抵消overhang的偏移 const auto origin = D2D1_POINT_2F{-overhang_metrics.left, -overhang_metrics.top};
逻辑说明
DWRITE_OVERHANG_METRICS的定义:left:文本向左超出布局原点的距离(通常为负值)top:文本向上超出文本顶部的距离(通常为负值)right:文本向右超出自身宽度的距离(正值)bottom:文本向下超出自身行高的距离(正值)
- 总宽度 = 文本自身宽度 + 左偏移(负数) + 右偏移(正数)
- 总高度 = 文本行高 + 上偏移(负数) + 下偏移(正数)
- 绘制原点设置为
(-overhang_metrics.left, -overhang_metrics.top),可以让文本的最左上角对齐到图片的(0,0)位置,彻底消除不必要的空白。
内容的提问来源于stack exchange,提问作者Ambarish
相关产品推荐
相关产品推荐

