You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.12 12:14:51