DirectWrite: Implement support for COLRv1

This traverses the paint node graph and renders
the emoji through a generic renderer. This renderer
is largely based on the renderer in the FreeType
backend and will be used there as well in a follow-up
patch.

Fixes: QTBUG-113458
Change-Id: I44d17b0be8d85e38589edeb68793bf4ee4491eca
Reviewed-by: Allan Sandfeld Jensen <allan.jensen@qt.io>
(cherry picked from commit 9f22a51987614ce51b3c12ced420bc4ca5b1ec61)
Reviewed-by: Qt Cherry-pick Bot <cherrypick_bot@qt-project.org>
This commit is contained in:
Eskil Abrahamsen Blomfeldt 2025-01-22 15:05:41 +01:00 committed by Qt Cherry-pick Bot
parent e50ae83c8c
commit 0c4b517e6e
9 changed files with 1086 additions and 114 deletions

View File

@ -234,6 +234,7 @@ qt_internal_add_module(Gui
rhi/qshader.cpp rhi/qshader.h rhi/qshader_p.h rhi/qshader.cpp rhi/qshader.h rhi/qshader_p.h
rhi/qshaderdescription.cpp rhi/qshaderdescription.h rhi/qshaderdescription_p.h rhi/qshaderdescription.cpp rhi/qshaderdescription.h rhi/qshaderdescription_p.h
text/qabstracttextdocumentlayout.cpp text/qabstracttextdocumentlayout.h text/qabstracttextdocumentlayout_p.h text/qabstracttextdocumentlayout.cpp text/qabstracttextdocumentlayout.h text/qabstracttextdocumentlayout_p.h
text/qcolrpaintgraphrenderer.cpp text/qcolrpaintgraphrenderer_p.h
text/qdistancefield.cpp text/qdistancefield_p.h text/qdistancefield.cpp text/qdistancefield_p.h
text/qfont.cpp text/qfont.h text/qfont_p.h text/qfont.cpp text/qfont.h text/qfont_p.h
text/qfontdatabase.cpp text/qfontdatabase.h text/qfontdatabase_p.h text/qfontdatabase.cpp text/qfontdatabase.h text/qfontdatabase_p.h

View File

@ -623,6 +623,22 @@ int main(int, char **)
} }
") ")
# directwritecolrv1
qt_config_compile_test(directwritecolrv1
LABEL "WINDOWS directwritecolrv1"
LIBRARIES
dwrite
CODE
"#include <dwrite_3.h>
int main(int, char **)
{
IUnknown *factory = nullptr;
// Just check that the API is available for the build
DWRITE_PAINT_ELEMENT paintElement;
return 0;
}
")
qt_config_compile_test(d2d1 qt_config_compile_test(d2d1
LABEL "WINDOWS Direct2D" LABEL "WINDOWS Direct2D"
LIBRARIES LIBRARIES
@ -691,6 +707,11 @@ qt_feature("directwrite3" PRIVATE
CONDITION QT_FEATURE_directwrite AND TEST_directwrite3 CONDITION QT_FEATURE_directwrite AND TEST_directwrite3
EMIT_IF WIN32 EMIT_IF WIN32
) )
qt_feature("directwritecolrv1" PRIVATE
LABEL "DirectWrite COLRv1 Support"
CONDITION QT_FEATURE_directwrite3 AND TEST_directwritecolrv1
EMIT_IF WIN32
)
qt_feature("direct2d" PRIVATE qt_feature("direct2d" PRIVATE
LABEL "Direct 2D" LABEL "Direct 2D"
CONDITION WIN32 AND NOT WINRT AND TEST_d2d1 CONDITION WIN32 AND NOT WINRT AND TEST_d2d1
@ -1413,6 +1434,7 @@ qt_configure_add_summary_entry(ARGS "direct2d")
qt_configure_add_summary_entry(ARGS "direct2d1_1") qt_configure_add_summary_entry(ARGS "direct2d1_1")
qt_configure_add_summary_entry(ARGS "directwrite") qt_configure_add_summary_entry(ARGS "directwrite")
qt_configure_add_summary_entry(ARGS "directwrite3") qt_configure_add_summary_entry(ARGS "directwrite3")
qt_configure_add_summary_entry(ARGS "directwritecolrv1")
qt_configure_end_summary_section() # end of "Windows" section qt_configure_end_summary_section() # end of "Windows" section
qt_configure_end_summary_section() # end of "QPA backends" section qt_configure_end_summary_section() # end of "QPA backends" section
qt_configure_add_report_entry( qt_configure_add_report_entry(

View File

@ -56,10 +56,6 @@
QT_BEGIN_NAMESPACE QT_BEGIN_NAMESPACE
#if defined(QFONTENGINE_FT_SUPPORT_COLRV1)
Q_STATIC_LOGGING_CATEGORY(lcColrv1, "qt.text.font.colrv1")
#endif
using namespace Qt::StringLiterals; using namespace Qt::StringLiterals;
#define FLOOR(x) ((x) & -64) #define FLOOR(x) ((x) & -64)

View File

@ -0,0 +1,316 @@
// Copyright (C) 2025 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
#include "qcolrpaintgraphrenderer_p.h"
QT_BEGIN_NAMESPACE
QColrPaintGraphRenderer::~QColrPaintGraphRenderer()
{
Q_ASSERT(m_oldPaths.isEmpty());
Q_ASSERT(m_oldTransforms.isEmpty());
delete m_painter;
}
void QColrPaintGraphRenderer::save()
{
qCDebug(lcColrv1).noquote().nospace()
<< QByteArray().fill(' ', m_oldPaths.size() * 2)
<< "[enter scope]";
m_oldPaths.append(m_currentPath);
m_oldTransforms.append(m_currentTransform);
if (m_painter != nullptr)
m_painter->save();
}
void QColrPaintGraphRenderer::restore()
{
m_currentPath = m_oldPaths.takeLast();
m_currentTransform = m_oldTransforms.takeLast();
qCDebug(lcColrv1).noquote().nospace()
<< QByteArray().fill(' ', m_oldPaths.size() * 2)
<< "[exit scope]";
if (m_painter != nullptr)
m_painter->restore();
}
void QColrPaintGraphRenderer::setClip(QRect rect)
{
qCDebug(lcColrv1).noquote().nospace()
<< QByteArray().fill(' ', m_oldPaths.size() * 2)
<< "[set clip: " << rect
<< "]";
if (!isRendering())
m_boundingRect = m_boundingRect.united(rect);
if (m_painter != nullptr)
m_painter->setClipRect(rect);
}
void QColrPaintGraphRenderer::prependTransform(const QTransform &transform)
{
qCDebug(lcColrv1).noquote().nospace()
<< QByteArray().fill(' ', m_oldPaths.size() * 2)
<< "[prepend transform: " << transform
<< "]";
m_currentTransform = transform * m_currentTransform;
}
void QColrPaintGraphRenderer::appendPath(const QPainterPath &path)
{
qCDebug(lcColrv1).noquote().nospace()
<< QByteArray().fill(' ', m_oldPaths.size() * 2)
<< "[append path: " << path.controlPointRect()
<< "]";
QPainterPath transformedPath(m_currentTransform.map(path));
m_currentPath = m_currentPath.united(transformedPath);
if (!isRendering())
m_boundingRect = m_boundingRect.united(transformedPath.boundingRect());
}
void QColrPaintGraphRenderer::setPath(const QPainterPath &path)
{
qCDebug(lcColrv1).noquote().nospace()
<< QByteArray().fill(' ', m_oldPaths.size() * 2)
<< "[set path]";
m_currentPath.clear();
appendPath(path);
}
void QColrPaintGraphRenderer::setSolidColor(QColor color)
{
qCDebug(lcColrv1).noquote().nospace()
<< QByteArray().fill(' ', m_oldPaths.size() * 2)
<< "[solid " << color
<< "]";
if (m_painter != nullptr)
m_painter->setBrush(color);
}
void QColrPaintGraphRenderer::setLinearGradient(QPointF p0, QPointF p1, QPointF p2,
QGradient::Spread spread,
const QGradientStops &gradientStops)
{
if (m_painter != nullptr) {
qCDebug(lcColrv1).noquote().nospace()
<< QByteArray().fill(' ', m_oldPaths.size() * 2)
<< "[linear gradient " << p0 << ", " << p1 << ", " << p2
<< ", spread: " << spread
<< ", stop count: " << gradientStops.size()
<< "]";
// Calculate new start and end point for single vector gradient preferred by Qt
// Find vector perpendicular to p0p2 and project p0p1 onto this to find p3 (final
// stop)
// https://learn.microsoft.com/en-us/typography/opentype/spec/colr#linear-gradients
QVector2D p0p2(p2 - p0);
if (qFuzzyIsNull(p0p2.lengthSquared())) {
qCWarning(lcColrv1) << "Malformed linear gradient in COLRv1 graph. Points:"
<< p0
<< p1
<< p2;
return;
}
// v is perpendicular to p0p2
QVector2D v(p0p2.y(), -p0p2.x());
// u is the vector from p0 to p1
QVector2D u(p1 - p0);
if (qFuzzyIsNull(u.lengthSquared())) {
qCWarning(lcColrv1) << "Malformed linear gradient in COLRv1 graph. Points:"
<< p0
<< p1
<< p2;
return;
}
// We find the projected point p3
QPointF p3((QVector2D(p0) + v * QVector2D::dotProduct(u, v) / v.lengthSquared()).toPointF());
p0 = m_currentTransform.map(p0);
p3 = m_currentTransform.map(p1);
QLinearGradient linearGradient(p0, p3);
linearGradient.setSpread(spread);
linearGradient.setStops(gradientStops);
linearGradient.setCoordinateMode(QGradient::LogicalMode);
m_painter->setBrush(linearGradient);
}
}
void QColrPaintGraphRenderer::setConicalGradient(QPointF center,
qreal startAngle, qreal endAngle,
QGradient::Spread spread,
const QGradientStops &gradientStops)
{
if (m_painter != nullptr) {
qCDebug(lcColrv1).noquote().nospace()
<< QByteArray().fill(' ', m_oldPaths.size() * 2)
<< "[conical gradient " << center
<< ", startAngle=" << startAngle
<< ", endAngle=" << endAngle
<< ", spread: " << spread
<< ", stop count: " << gradientStops.size()
<< "]";
QConicalGradient conicalGradient(center, startAngle);
conicalGradient.setSpread(spread);
// Adapt stops to actual span since Qt always assumes end angle of 360
// Note: This does not give accurate results for the colors outside the angle span.
// To do this correctly, we would have to insert stops at 0°, 360° and if the spread
// is reflect/repeat, also throughout the uncovered area to get the correct
// rendering. It might however be easier to support this in QConicalGradient itself.
// For now, this is left only semi-supported, as sweep gradients are currently rare.
const qreal multiplier = qFuzzyCompare(endAngle, startAngle)
? 1.0
: (endAngle - startAngle) / 360.0;
QGradientStops adaptedStops;
adaptedStops.reserve(gradientStops.size());
for (const QGradientStop &gradientStop : gradientStops)
adaptedStops.append(qMakePair(gradientStop.first * multiplier, gradientStop.second));
conicalGradient.setStops(adaptedStops);
conicalGradient.setCoordinateMode(QGradient::LogicalMode);
m_painter->setBrush(conicalGradient);
}
}
void QColrPaintGraphRenderer::setRadialGradient(QPointF c0, qreal r0,
QPointF c1, qreal r1,
QGradient::Spread spread,
const QGradientStops &gradientStops)
{
if (m_painter != nullptr) {
qCDebug(lcColrv1).noquote().nospace()
<< QByteArray().fill(' ', m_oldPaths.size() * 2)
<< "[radial gradient " << c0
<< ", rad=" << r0
<< ", " << c1
<< ", rad=" << r1
<< ", spread: " << spread
<< ", stop count: " << gradientStops.size()
<< "]";
QPointF c0e(c0 + QPointF(r0, 0.0));
QPointF c1e(c1 + QPointF(r1, 0.0));
c0 = m_currentTransform.map(c0);
c0e = m_currentTransform.map(c0e);
c1 = m_currentTransform.map(c1);
c1e = m_currentTransform.map(c1e);
const QVector2D d0(c0e - c0);
const QVector2D d1(c1e - c1);
QRadialGradient gradient(c1, d1.length(), c0, d0.length());
gradient.setCoordinateMode(QGradient::LogicalMode);
gradient.setSpread(spread);
gradient.setStops(gradientStops);
m_painter->setBrush(gradient);
}
}
void QColrPaintGraphRenderer::drawCurrentPath()
{
qCDebug(lcColrv1).noquote().nospace()
<< QByteArray().fill(' ', m_oldPaths.size() * 2)
<< "[draw path " << m_currentPath.controlPointRect() << m_currentTransform
<< "]";
if (m_painter != nullptr)
m_painter->drawPath(m_currentPath);
}
void QColrPaintGraphRenderer::beginCalculateBoundingBox()
{
qCDebug(lcColrv1).noquote().nospace()
<< QByteArray().fill(' ', m_oldPaths.size() * 2)
<< "[begin calculate bounding box]";
Q_ASSERT(m_painter == nullptr);
m_boundingRect = QRect{};
}
void QColrPaintGraphRenderer::drawImage(const QImage &image)
{
qCDebug(lcColrv1).noquote().nospace()
<< QByteArray().fill(' ', m_oldPaths.size() * 2)
<< "[draw image: " << image.size() << "]";
if (m_painter != nullptr) {
m_painter->setWorldMatrixEnabled(false);
m_painter->drawImage(QPoint(0, 0), image);
m_painter->setWorldMatrixEnabled(true);
}
}
void QColrPaintGraphRenderer::setCompositionMode(QPainter::CompositionMode mode)
{
qCDebug(lcColrv1).noquote().nospace()
<< QByteArray().fill(' ', m_oldPaths.size() * 2)
<< "[set composition mode: " << mode << "]";
if (m_painter != nullptr)
m_painter->setCompositionMode(mode);
}
void QColrPaintGraphRenderer::beginRender(qreal pixelSizeScale, const QTransform &transform)
{
qCDebug(lcColrv1).noquote().nospace()
<< QByteArray().fill(' ', m_oldPaths.size() * 2)
<< "[begin render scale: " << pixelSizeScale
<< ", transform: " << transform
<< "]";
if (m_boundingRect.isEmpty())
return;
QRect alignedRect(m_boundingRect.toAlignedRect());
m_image = QImage(alignedRect.size(), QImage::Format_ARGB32_Premultiplied);
m_image.fill(Qt::transparent);
Q_ASSERT(m_painter == nullptr);
m_painter = new QPainter;
m_painter->begin(&m_image);
m_painter->setRenderHint(QPainter::Antialiasing);
m_painter->setPen(Qt::NoPen);
m_painter->setBrush(Qt::NoBrush);
m_painter->translate(-alignedRect.topLeft());
// Scale from normalized coordinates
m_painter->scale(pixelSizeScale, pixelSizeScale);
m_painter->setWorldTransform(transform, true);
}
QImage QColrPaintGraphRenderer::endRender()
{
qCDebug(lcColrv1).noquote().nospace()
<< QByteArray().fill(' ', m_oldPaths.size() * 2)
<< "[end image size: " << m_image.size()
<< "]";
Q_ASSERT(m_painter != nullptr);
m_painter->end();
delete m_painter;
m_painter = nullptr;
return m_image;
}
QT_END_NAMESPACE

View File

@ -0,0 +1,81 @@
// Copyright (C) 2025 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
#ifndef QCOLRPAINTGRAPHRENDERER_P_H
#define QCOLRPAINTGRAPHRENDERER_P_H
//
// W A R N I N G
// -------------
//
// This file is not part of the Qt API. It exists purely as an
// implementation detail. This header file may change from version to
// version without notice, or even be removed.
//
// We mean it.
//
#include <QtGui/qpainter.h>
#include <QtGui/qpainterpath.h>
#include <private/qfontengine_p.h>
QT_BEGIN_NAMESPACE
class QColrPaintGraphRenderer
{
public:
~QColrPaintGraphRenderer();
void setBoundingRect(QRectF boundingRect) { m_boundingRect = boundingRect; }
QRectF boundingRect() const { return m_boundingRect; }
QTransform currentTransform() const { return m_currentTransform; }
QPainterPath currentPath() const { return m_currentPath; }
void save();
void restore();
void appendPath(const QPainterPath &path);
void setPath(const QPainterPath &path);
void prependTransform(const QTransform &transform);
void setSolidColor(QColor color);
void setRadialGradient(QPointF c0, qreal r0,
QPointF c1, qreal r1,
QGradient::Spread spread,
const QGradientStops &gradientStops);
void setLinearGradient(QPointF p0, QPointF p1, QPointF p2,
QGradient::Spread spread,
const QGradientStops &gradientStops);
void setConicalGradient(QPointF center,
qreal startAngle,
qreal endAngle,
QGradient::Spread spread,
const QGradientStops &gradientStops);
void setCompositionMode(QPainter::CompositionMode mode);
void drawCurrentPath();
void drawImage(const QImage &image);
void setClip(QRect rect);
void beginCalculateBoundingBox();
void beginRender(qreal pixelSizeScale, const QTransform &transform);
QImage endRender();
bool isRendering() const { return m_painter != nullptr; }
private:
QImage m_image;
QPainter *m_painter = nullptr;
QTransform m_currentTransform;
QRectF m_boundingRect;
QPainterPath m_currentPath;
QList<QPainterPath> m_oldPaths;
QList<QTransform> m_oldTransforms;
};
QT_END_NAMESPACE
#endif // QCOLRPAINTGRAPHRENDERER_P_H

View File

@ -28,6 +28,8 @@
QT_BEGIN_NAMESPACE QT_BEGIN_NAMESPACE
Q_LOGGING_CATEGORY(lcColrv1, "qt.text.font.colrv1")
using namespace Qt::StringLiterals; using namespace Qt::StringLiterals;
static inline bool qtransform_equals_no_translate(const QTransform &a, const QTransform &b) static inline bool qtransform_equals_no_translate(const QTransform &a, const QTransform &b)

View File

@ -42,6 +42,8 @@ enum HB_Compat_Error {
typedef void (*qt_destroy_func_t) (void *user_data); typedef void (*qt_destroy_func_t) (void *user_data);
typedef bool (*qt_get_font_table_func_t) (void *user_data, uint tag, uchar *buffer, uint *length); typedef bool (*qt_get_font_table_func_t) (void *user_data, uint tag, uchar *buffer, uint *length);
Q_DECLARE_LOGGING_CATEGORY(lcColrv1)
class Q_GUI_EXPORT QFontEngine class Q_GUI_EXPORT QFontEngine
{ {
public: public:

View File

@ -15,6 +15,8 @@
#include <qpa/qplatformintegration.h> #include <qpa/qplatformintegration.h>
#include <QtGui/qpainterpath.h> #include <QtGui/qpainterpath.h>
#include <private/qcolrpaintgraphrenderer_p.h>
#if QT_CONFIG(directwrite3) #if QT_CONFIG(directwrite3)
# include "qwindowsdirectwritefontdatabase_p.h" # include "qwindowsdirectwritefontdatabase_p.h"
# include <dwrite_3.h> # include <dwrite_3.h>
@ -582,16 +584,16 @@ void QWindowsFontEngineDirectWrite::recalcAdvances(QGlyphLayout *glyphs, QFontEn
} }
} }
void QWindowsFontEngineDirectWrite::getUnscaledGlyph(glyph_t glyph, QPainterPath QWindowsFontEngineDirectWrite::unscaledGlyph(glyph_t glyph) const
QPainterPath *path,
glyph_metrics_t *metric)
{ {
float advance = 0.0f; float advance = 0.0f;
UINT16 g = glyph; UINT16 g = glyph;
DWRITE_GLYPH_OFFSET offset; DWRITE_GLYPH_OFFSET offset;
offset.advanceOffset = 0; offset.advanceOffset = 0;
offset.ascenderOffset = 0; offset.ascenderOffset = 0;
GeometrySink geometrySink(path);
QPainterPath ret;
GeometrySink geometrySink(&ret);
HRESULT hr = m_directWriteFontFace->GetGlyphRunOutline(m_unitsPerEm, HRESULT hr = m_directWriteFontFace->GetGlyphRunOutline(m_unitsPerEm,
&g, &g,
&advance, &advance,
@ -600,13 +602,21 @@ void QWindowsFontEngineDirectWrite::getUnscaledGlyph(glyph_t glyph,
false, false,
false, false,
&geometrySink); &geometrySink);
if (FAILED(hr)) { if (FAILED(hr))
qErrnoWarning("%s: GetGlyphRunOutline failed", __FUNCTION__); qErrnoWarning("%s: GetGlyphRunOutline failed", __FUNCTION__);
return;
return ret;
} }
void QWindowsFontEngineDirectWrite::getUnscaledGlyph(glyph_t glyph,
QPainterPath *path,
glyph_metrics_t *metric)
{
*path = unscaledGlyph(glyph);
UINT16 g = glyph;
DWRITE_GLYPH_METRICS glyphMetrics; DWRITE_GLYPH_METRICS glyphMetrics;
hr = m_directWriteFontFace->GetDesignGlyphMetrics(&g, 1, &glyphMetrics); HRESULT hr = m_directWriteFontFace->GetDesignGlyphMetrics(&g, 1, &glyphMetrics);
if (FAILED(hr)) { if (FAILED(hr)) {
qErrnoWarning("%s: GetDesignGlyphMetrics failed", __FUNCTION__); qErrnoWarning("%s: GetDesignGlyphMetrics failed", __FUNCTION__);
return; return;
@ -838,6 +848,515 @@ bool QWindowsFontEngineDirectWrite::renderColr0GlyphRun(QImage *image,
return true; return true;
} }
#if QT_CONFIG(directwritecolrv1)
static inline QTransform matrixToTransform(const DWRITE_MATRIX &matrix,
int unitsPerEm = 1)
{
return QTransform(matrix.m11, matrix.m12,
matrix.m21, matrix.m22,
matrix.dx * unitsPerEm, matrix.dy * unitsPerEm);
}
static inline QColor colorToColor(const DWRITE_COLOR_F &color, float alphaMultiplier = 1.0)
{
return QColor::fromRgbF(color.r, color.g, color.b, color.a * alphaMultiplier);
}
static inline QGradient::Spread extendToSpread(UINT32 extendMode)
{
switch (extendMode) {
case D2D1_EXTEND_MODE_WRAP: return QGradient::RepeatSpread;
case D2D1_EXTEND_MODE_MIRROR: return QGradient::ReflectSpread;
default: return QGradient::PadSpread;
};
}
static inline QPainter::CompositionMode compositeToCompositionMode(DWRITE_COLOR_COMPOSITE_MODE mode)
{
switch (mode) {
case DWRITE_COLOR_COMPOSITE_CLEAR:
return QPainter::CompositionMode_Clear;
case DWRITE_COLOR_COMPOSITE_SRC:
return QPainter::CompositionMode_Source;
case DWRITE_COLOR_COMPOSITE_DEST:
return QPainter::CompositionMode_Destination;
case DWRITE_COLOR_COMPOSITE_SRC_OVER:
return QPainter::CompositionMode_SourceOver;
case DWRITE_COLOR_COMPOSITE_DEST_OVER:
return QPainter::CompositionMode_DestinationOver;
case DWRITE_COLOR_COMPOSITE_SRC_IN:
return QPainter::CompositionMode_SourceIn;
case DWRITE_COLOR_COMPOSITE_DEST_IN:
return QPainter::CompositionMode_DestinationIn;
case DWRITE_COLOR_COMPOSITE_SRC_OUT:
return QPainter::CompositionMode_SourceOut;
case DWRITE_COLOR_COMPOSITE_DEST_OUT:
return QPainter::CompositionMode_DestinationOut;
case DWRITE_COLOR_COMPOSITE_SRC_ATOP:
return QPainter::CompositionMode_SourceAtop;
case DWRITE_COLOR_COMPOSITE_DEST_ATOP:
return QPainter::CompositionMode_DestinationAtop;
case DWRITE_COLOR_COMPOSITE_XOR:
return QPainter::CompositionMode_Xor;
case DWRITE_COLOR_COMPOSITE_PLUS:
return QPainter::CompositionMode_Plus;
case DWRITE_COLOR_COMPOSITE_SCREEN:
return QPainter::CompositionMode_Screen;
case DWRITE_COLOR_COMPOSITE_OVERLAY:
return QPainter::CompositionMode_Overlay;
case DWRITE_COLOR_COMPOSITE_DARKEN:
return QPainter::CompositionMode_Darken;
case DWRITE_COLOR_COMPOSITE_LIGHTEN:
return QPainter::CompositionMode_Lighten;
case DWRITE_COLOR_COMPOSITE_COLOR_DODGE:
return QPainter::CompositionMode_ColorDodge;
case DWRITE_COLOR_COMPOSITE_COLOR_BURN:
return QPainter::CompositionMode_ColorBurn;
case DWRITE_COLOR_COMPOSITE_HARD_LIGHT:
return QPainter::CompositionMode_HardLight;
case DWRITE_COLOR_COMPOSITE_SOFT_LIGHT:
return QPainter::CompositionMode_SoftLight;
case DWRITE_COLOR_COMPOSITE_DIFFERENCE:
return QPainter::CompositionMode_Difference;
case DWRITE_COLOR_COMPOSITE_EXCLUSION:
return QPainter::CompositionMode_Exclusion;
case DWRITE_COLOR_COMPOSITE_MULTIPLY:
return QPainter::CompositionMode_Multiply;
default:
qCWarning(lcColrv1) << "Unhandled color composite mode:" << mode;
return QPainter::CompositionMode_SourceOver;
};
}
#endif // QT_CONFIG(directwritecolrv1)
QRect QWindowsFontEngineDirectWrite::paintGraphBounds(glyph_t glyph,
const DWRITE_MATRIX &matrix) const
{
#if QT_CONFIG(directwritecolrv1)
ComPtr<IDWriteFontFace7> face7;
HRESULT hr = m_directWriteFontFace->QueryInterface(__uuidof(IDWriteFontFace7),
&face7);
if (SUCCEEDED(hr)) {
DWRITE_PAINT_FEATURE_LEVEL featureLevel = face7->GetPaintFeatureLevel(DWRITE_GLYPH_IMAGE_FORMATS_COLR_PAINT_TREE);
if (featureLevel != DWRITE_PAINT_FEATURE_LEVEL_COLR_V1)
return QRect{};
ComPtr<IDWritePaintReader> paintReader;
hr = face7->CreatePaintReader(DWRITE_GLYPH_IMAGE_FORMATS_COLR_PAINT_TREE,
featureLevel,
&paintReader);
if (FAILED(hr)) {
qErrnoWarning(hr, "%s: CreatePaintReader failed", __FUNCTION__);
return QRect{};
}
DWRITE_PAINT_ELEMENT paintElement;
D2D_RECT_F clipBox;
hr = paintReader->SetCurrentGlyph(glyph,
&paintElement,
sizeof(paintElement),
&clipBox,
nullptr);
if (FAILED(hr)) {
qErrnoWarning(hr, "%s: SetCurrentGlyph failed", __FUNCTION__);
return QRect{};
}
if (paintElement.paintType == DWRITE_PAINT_TYPE_NONE)
return QRect{};
QRectF boundingRect = QRectF(QPointF(clipBox.left, clipBox.top),
QPointF(clipBox.right, clipBox.bottom));
if (boundingRect.isNull()) {
QColrPaintGraphRenderer boundingRectCalculator;
boundingRectCalculator.beginCalculateBoundingBox();
if (traverseColr1(paintReader.Get(), face7.Get(), &paintElement, &boundingRectCalculator))
boundingRect = boundingRectCalculator.boundingRect();
}
QTransform initialTransform;
initialTransform.scale(fontDef.pixelSize, fontDef.pixelSize);
boundingRect = initialTransform.mapRect(boundingRect);
QTransform originalXform = matrixToTransform(matrix);
boundingRect = originalXform.mapRect(boundingRect);
return boundingRect.toAlignedRect();
} else {
qCDebug(lcColrv1) << "Font face does not support IDWriteFontFace7 interface";
}
#else
Q_UNUSED(glyph);
Q_UNUSED(matrix);
#endif
return QRect{};
}
#if QT_CONFIG(directwritecolrv1)
bool QWindowsFontEngineDirectWrite::traverseColr1(IDWritePaintReader *paintReader,
IDWriteFontFace7 *face7,
const DWRITE_PAINT_ELEMENT *paintElement,
QColrPaintGraphRenderer *paintGraphRenderer) const
{
paintGraphRenderer->save();
auto cleanup = qScopeGuard([&paintGraphRenderer]() {
paintGraphRenderer->restore();
});
auto traverseChildren = [&](quint32 childCount) {
DWRITE_PAINT_ELEMENT childPaintElement;
if (FAILED(paintReader->MoveToFirstChild(&childPaintElement)))
return false;
while (childCount-- > 0) {
traverseColr1(paintReader, face7, &childPaintElement, paintGraphRenderer);
if (childCount > 0) {
if (FAILED(paintReader->MoveToNextSibling(&childPaintElement))) {
return false;
}
}
}
return SUCCEEDED(paintReader->MoveToParent());
};
auto collectStops = [&paintReader](int stopCount) {
QGradientStops ret;
QVarLengthArray<D2D1_GRADIENT_STOP> stops(stopCount);
HRESULT hr = paintReader->GetGradientStops(0, stopCount, stops.data());
if (FAILED(hr))
return ret;
for (int i = 0; i < stopCount; ++i) {
const D2D1_GRADIENT_STOP &stop = stops[i];
QColor color = QColor::fromRgbF(stop.color.r, stop.color.g, stop.color.b, stop.color.a);
ret.append(qMakePair(stop.position, color));
}
return ret;
};
switch (paintElement->paintType) {
case DWRITE_PAINT_TYPE_LAYERS:
{
if (!traverseChildren(paintElement->paint.layers.childCount))
return false;
break;
}
case DWRITE_PAINT_TYPE_SOLID_GLYPH:
{
QPainterPath glyphPath = unscaledGlyph(paintElement->paint.solidGlyph.glyphIndex);
QColor color = colorToColor(paintElement->paint.solidGlyph.color.value);
paintGraphRenderer->setPath(glyphPath);
paintGraphRenderer->setSolidColor(color);
paintGraphRenderer->drawCurrentPath();
break;
}
case DWRITE_PAINT_TYPE_SOLID:
{
QColor color = colorToColor(paintElement->paint.solid.value,
paintElement->paint.solid.alphaMultiplier);
paintGraphRenderer->setSolidColor(color);
paintGraphRenderer->drawCurrentPath();
break;
}
case DWRITE_PAINT_TYPE_COMPOSITE:
{
if (!paintGraphRenderer->isRendering()) {
traverseChildren(2);
} else {
DWRITE_PAINT_ELEMENT childElement;
HRESULT hr = paintReader->MoveToFirstChild(&childElement);
if (FAILED(hr)) {
qErrnoWarning(hr, "%s: Cannot move to first child of composite node",
__FUNCTION__);
return false;
}
// First draw back drop which is the second child
hr = paintReader->MoveToNextSibling(&childElement);
if (FAILED(hr)) {
qErrnoWarning(hr, "%s: Cannot move to second child of composite node",
__FUNCTION__);
return false;
}
DWRITE_COLOR_COMPOSITE_MODE compositeMode = paintElement->paint.composite.mode;
QColrPaintGraphRenderer compositeRenderer;
compositeRenderer.setBoundingRect(paintGraphRenderer->boundingRect());
compositeRenderer.beginRender(fontDef.pixelSize / m_unitsPerEm,
paintGraphRenderer->currentTransform());
if (!traverseColr1(paintReader, face7, &childElement, &compositeRenderer))
return false;
// Then draw source which is the first child
hr = paintReader->MoveToParent();
if (FAILED(hr)) {
qErrnoWarning(hr, "%s: Cannot move back to parent composite node",
__FUNCTION__);
return false;
}
hr = paintReader->MoveToFirstChild(&childElement);
if (FAILED(hr)) {
qErrnoWarning(hr, "%s: Cannot move to first child of composite node",
__FUNCTION__);
return false;
}
compositeRenderer.setCompositionMode(compositeToCompositionMode(compositeMode));
if (!traverseColr1(paintReader, face7, &childElement, &compositeRenderer))
return false;
hr = paintReader->MoveToParent();
if (FAILED(hr)) {
qErrnoWarning(hr, "%s: Cannot move back to parent composite node",
__FUNCTION__);
return false;
}
paintGraphRenderer->drawImage(compositeRenderer.endRender());
}
break;
}
case DWRITE_PAINT_TYPE_RADIAL_GRADIENT:
{
const QPointF c0(paintElement->paint.radialGradient.x0 * m_unitsPerEm,
paintElement->paint.radialGradient.y0 * m_unitsPerEm);
const QPointF c1(paintElement->paint.radialGradient.x1 * m_unitsPerEm,
paintElement->paint.radialGradient.y1 * m_unitsPerEm);
const qreal r0 = paintElement->paint.radialGradient.radius0 * m_unitsPerEm;
const qreal r1 = paintElement->paint.radialGradient.radius1 * m_unitsPerEm;
const QGradient::Spread spread
= extendToSpread(paintElement->paint.radialGradient.extendMode);
const QGradientStops gradientStops
= collectStops(paintElement->paint.radialGradient.gradientStopCount);
paintGraphRenderer->setRadialGradient(c0, r0, c1, r1, spread, gradientStops);
paintGraphRenderer->drawCurrentPath();
break;
}
case DWRITE_PAINT_TYPE_SWEEP_GRADIENT:
{
const QPointF center(paintElement->paint.sweepGradient.centerX * m_unitsPerEm,
paintElement->paint.sweepGradient.centerY * m_unitsPerEm);
const qreal startAngle = paintElement->paint.sweepGradient.startAngle;
const qreal endAngle = paintElement->paint.sweepGradient.endAngle;
const QGradient::Spread spread
= extendToSpread(paintElement->paint.radialGradient.extendMode);
const QGradientStops gradientStops
= collectStops(paintElement->paint.radialGradient.gradientStopCount);
paintGraphRenderer->setConicalGradient(center,
startAngle,
endAngle,
spread,
gradientStops);
paintGraphRenderer->drawCurrentPath();
break;
}
case DWRITE_PAINT_TYPE_LINEAR_GRADIENT:
{
const QPointF p0(paintElement->paint.linearGradient.x0 * m_unitsPerEm,
paintElement->paint.linearGradient.y0 * m_unitsPerEm);
const QPointF p1(paintElement->paint.linearGradient.x1 * m_unitsPerEm,
paintElement->paint.linearGradient.y1 * m_unitsPerEm);
const QPointF p2(paintElement->paint.linearGradient.x2 * m_unitsPerEm,
paintElement->paint.linearGradient.y2 * m_unitsPerEm);
const QGradient::Spread spread
= extendToSpread(paintElement->paint.linearGradient.extendMode);
const QGradientStops gradientStops
= collectStops(paintElement->paint.linearGradient.gradientStopCount);
paintGraphRenderer->setLinearGradient(p0, p1, p2, spread, gradientStops);
paintGraphRenderer->drawCurrentPath();
break;
}
case DWRITE_PAINT_TYPE_GLYPH:
{
QPainterPath glyphPath = unscaledGlyph(paintElement->paint.glyph.glyphIndex);
paintGraphRenderer->appendPath(glyphPath);
if (!traverseChildren(1))
return false;
break;
}
case DWRITE_PAINT_TYPE_COLOR_GLYPH:
{
D2D_RECT_F rect = paintElement->paint.colorGlyph.clipBox;
QRect clipBox = QRectF(QPointF(rect.left, rect.top),
QPointF(rect.right, rect.bottom)).toAlignedRect();
if (!clipBox.isEmpty()) {
QTransform coordinatesTransform;
coordinatesTransform.scale(m_unitsPerEm, m_unitsPerEm);
clipBox = coordinatesTransform.mapRect(clipBox);
paintGraphRenderer->setClip(clipBox);
}
DWRITE_PAINT_ELEMENT childElement;
if (FAILED(paintReader->MoveToFirstChild(&childElement)))
return false;
if (!traverseColr1(paintReader, face7, &childElement, paintGraphRenderer))
return false;
if (FAILED(paintReader->MoveToParent()))
return false;
break;
}
case DWRITE_PAINT_TYPE_TRANSFORM:
{
QTransform transform = matrixToTransform(paintElement->paint.transform, m_unitsPerEm);
paintGraphRenderer->prependTransform(transform);
if (!traverseChildren(1))
return false;
break;
}
default:
qCDebug(lcColrv1) << "Unhandled paint graph node type" << paintElement->paintType;
break;
};
return true;
}
bool QWindowsFontEngineDirectWrite::renderColr1GlyphRun(QImage *image,
const DWRITE_GLYPH_RUN *glyphRun,
const DWRITE_MATRIX &matrix,
QColor color) const
{
qCDebug(lcColrv1) << "renderColr1GlyphRun,"
<< "families:" << fontDef.families;
ComPtr<IDWriteFontFace7> face7;
HRESULT hr = m_directWriteFontFace->QueryInterface(__uuidof(IDWriteFontFace7),
&face7);
if (SUCCEEDED(hr)) {
DWRITE_PAINT_FEATURE_LEVEL featureLevel =
face7->GetPaintFeatureLevel(DWRITE_GLYPH_IMAGE_FORMATS_COLR_PAINT_TREE);
if (featureLevel != DWRITE_PAINT_FEATURE_LEVEL_COLR_V1) {
qCDebug(lcColrv1) << "Unsupported feature level:" << featureLevel;
return true;
}
ComPtr<IDWritePaintReader> paintReader;
hr = face7->CreatePaintReader(DWRITE_GLYPH_IMAGE_FORMATS_COLR_PAINT_TREE,
featureLevel,
&paintReader);
if (FAILED(hr)) {
qErrnoWarning(hr, "%s: CreatePaintReader failed", __FUNCTION__);
return false;
}
Q_ASSERT(glyphRun->glyphCount == 1);
DWRITE_PAINT_ELEMENT paintElement;
D2D_RECT_F clipBox;
hr = paintReader->SetCurrentGlyph(glyphRun->glyphIndices[0],
&paintElement,
&clipBox,
nullptr);
if (FAILED(hr)) {
qErrnoWarning(hr, "%s: SetCurrentGlyph failed", __FUNCTION__);
return false;
}
if (paintElement.paintType == DWRITE_PAINT_TYPE_NONE) {
qCDebug(lcColrv1) << "Glyph" << glyphRun->glyphIndices[0]
<< "does not have a paint graph";
return false;
}
DWRITE_COLOR_F dwColor;
dwColor.r = color.redF();
dwColor.g = color.greenF();
dwColor.b = color.blueF();
dwColor.a = color.alphaF();
paintReader->SetTextColor(dwColor);
QRectF boundingRect = QRectF(QPointF(clipBox.left, clipBox.top),
QPointF(clipBox.right, clipBox.bottom));
if (boundingRect.isNull()) {
QColrPaintGraphRenderer boundingRectCalculator;
boundingRectCalculator.beginCalculateBoundingBox();
if (traverseColr1(paintReader.Get(), face7.Get(), &paintElement, &boundingRectCalculator))
boundingRect = boundingRectCalculator.boundingRect();
}
QTransform initialTransform;
initialTransform.scale(fontDef.pixelSize, fontDef.pixelSize);
boundingRect = initialTransform.mapRect(boundingRect);
QTransform originalXform = matrixToTransform(matrix);
boundingRect = originalXform.mapRect(boundingRect);
qCDebug(lcColrv1).noquote() << "Bounds of"
<< glyphRun->glyphIndices[0]
<< " in device coordinates:"
<< boundingRect;
QColrPaintGraphRenderer graphRenderer;
graphRenderer.setBoundingRect(boundingRect);
graphRenderer.beginRender(fontDef.pixelSize / m_unitsPerEm, matrixToTransform(matrix));
if (!traverseColr1(paintReader.Get(), face7.Get(), &paintElement, &graphRenderer))
return false;
*image = graphRenderer.endRender();
} else {
qCDebug(lcColrv1) << "Font face does not support IDWriteFontFace7 interface";
}
return true;
}
#endif // QT_CONFIG(directwritecolrv1)
QImage QWindowsFontEngineDirectWrite::renderColorGlyph(DWRITE_GLYPH_RUN *glyphRun, QImage QWindowsFontEngineDirectWrite::renderColorGlyph(DWRITE_GLYPH_RUN *glyphRun,
const DWRITE_MATRIX &transform, const DWRITE_MATRIX &transform,
DWRITE_RENDERING_MODE renderMode, DWRITE_RENDERING_MODE renderMode,
@ -848,7 +1367,16 @@ QImage QWindowsFontEngineDirectWrite::renderColorGlyph(DWRITE_GLYPH_RUN *glyphRu
{ {
QImage ret; QImage ret;
#if QT_CONFIG(directwritecolrv1)
// Start by trying COLRv1 glyph, where we need to get the paint nodes ourselves and render
// them.
renderColr1GlyphRun(&ret, glyphRun, transform, color);
#endif // QT_CONFIG(directwritecolrv1)
#if QT_CONFIG(directwrite3) #if QT_CONFIG(directwrite3)
// If not successful, we try the modern API that supports both embedded pixmaps or COLRv0
// glyphs, or a combination.
if (ret.isNull()) {
ComPtr<IDWriteFactory4> factory4; ComPtr<IDWriteFactory4> factory4;
HRESULT hr = m_fontEngineData->directWriteFactory->QueryInterface(__uuidof(IDWriteFactory4), HRESULT hr = m_fontEngineData->directWriteFactory->QueryInterface(__uuidof(IDWriteFactory4),
&factory4); &factory4);
@ -873,11 +1401,6 @@ QImage QWindowsFontEngineDirectWrite::renderColorGlyph(DWRITE_GLYPH_RUN *glyphRu
NULL, NULL,
0, 0,
&enumerator); &enumerator);
if (FAILED(hr)) {
qErrnoWarning(hr, "%s: TranslateGlyphRun failed", __FUNCTION__);
return ret;
}
BOOL ok = true; BOOL ok = true;
while (SUCCEEDED(hr) && ok) { while (SUCCEEDED(hr) && ok) {
hr = enumerator->MoveNext(&ok); hr = enumerator->MoveNext(&ok);
@ -893,7 +1416,7 @@ QImage QWindowsFontEngineDirectWrite::renderColorGlyph(DWRITE_GLYPH_RUN *glyphRu
if (colorGlyphRun->glyphImageFormat == DWRITE_GLYPH_IMAGE_FORMATS_NONE) { if (colorGlyphRun->glyphImageFormat == DWRITE_GLYPH_IMAGE_FORMATS_NONE) {
break; break;
} else if (colorGlyphRun->glyphImageFormat & DWRITE_GLYPH_IMAGE_FORMATS_COLR) { } else if (colorGlyphRun->glyphImageFormat == DWRITE_GLYPH_IMAGE_FORMATS_COLR) {
if (ret.isNull()) { if (ret.isNull()) {
ret = QImage(boundingRect.width() - 1, ret = QImage(boundingRect.width() - 1,
boundingRect.height() - 1, boundingRect.height() - 1,
@ -964,8 +1487,10 @@ QImage QWindowsFontEngineDirectWrite::renderColorGlyph(DWRITE_GLYPH_RUN *glyphRu
} }
} }
} }
}
#endif // QT_CONFIG(directwrite3) #endif // QT_CONFIG(directwrite3)
// If all else fails, we go through the pre-dwrite3 version, which just supports COLRv0.
if (ret.isNull()) { if (ret.isNull()) {
ComPtr<IDWriteFactory2> factory2; ComPtr<IDWriteFactory2> factory2;
HRESULT hr = m_fontEngineData->directWriteFactory->QueryInterface(__uuidof(IDWriteFactory2), HRESULT hr = m_fontEngineData->directWriteFactory->QueryInterface(__uuidof(IDWriteFactory2),
@ -1092,7 +1617,9 @@ QImage QWindowsFontEngineDirectWrite::imageForGlyph(glyph_t t,
} }
if (SUCCEEDED(hr)) { if (SUCCEEDED(hr)) {
QRect rect = alphaTextureBounds(t, transform); QRect rect = paintGraphBounds(t, transform);
if (rect.isEmpty())
rect = alphaTextureBounds(t, transform);
if (rect.isEmpty()) if (rect.isEmpty())
rect = colorBitmapBounds(t, transform); rect = colorBitmapBounds(t, transform);
@ -1467,8 +1994,12 @@ glyph_metrics_t QWindowsFontEngineDirectWrite::alphaMapBoundingBox(glyph_t glyph
transform.m21 = matrix.m21(); transform.m21 = matrix.m21();
transform.m22 = matrix.m22(); transform.m22 = matrix.m22();
// Try general approach first (works with regular truetype glyphs as well as COLRv0) // Try COLRv1 approach first
QRect rect = alphaTextureBounds(glyph, transform); QRect rect = paintGraphBounds(glyph, transform);
// Then try general approach (works with regular truetype glyphs as well as COLRv0)
if (rect.isEmpty())
rect = alphaTextureBounds(glyph, transform);
// If this fails, we check if it is an embedded color bitmap // If this fails, we check if it is an embedded color bitmap
if (rect.isEmpty()) if (rect.isEmpty())

View File

@ -34,9 +34,16 @@ struct IDWriteGdiInterop;
struct IDWriteGlyphRunAnalysis; struct IDWriteGlyphRunAnalysis;
struct IDWriteColorGlyphRunEnumerator; struct IDWriteColorGlyphRunEnumerator;
#if QT_CONFIG(directwritecolrv1)
struct IDWriteFontFace7;
struct IDWritePaintReader;
struct DWRITE_PAINT_ELEMENT;
#endif
QT_BEGIN_NAMESPACE QT_BEGIN_NAMESPACE
class QWindowsFontEngineData; class QWindowsFontEngineData;
class QColrPaintGraphRenderer;
class Q_GUI_EXPORT QWindowsFontEngineDirectWrite : public QFontEngine class Q_GUI_EXPORT QWindowsFontEngineDirectWrite : public QFontEngine
{ {
@ -102,11 +109,25 @@ public:
void initializeHeightMetrics() const override; void initializeHeightMetrics() const override;
Properties properties() const override; Properties properties() const override;
QPainterPath unscaledGlyph(glyph_t glyph) const;
void getUnscaledGlyph(glyph_t glyph, QPainterPath *path, glyph_metrics_t *metrics) override; void getUnscaledGlyph(glyph_t glyph, QPainterPath *path, glyph_metrics_t *metrics) override;
QList<QFontVariableAxis> variableAxes() const override; QList<QFontVariableAxis> variableAxes() const override;
private: private:
#if QT_CONFIG(directwritecolrv1)
bool traverseColr1(IDWritePaintReader *paintReader,
IDWriteFontFace7 *face7,
const DWRITE_PAINT_ELEMENT *paintElement,
QColrPaintGraphRenderer *graphRenderer) const;
bool renderColr1GlyphRun(QImage *image,
const DWRITE_GLYPH_RUN *glyphRun,
const DWRITE_MATRIX &transform,
QColor color) const;
#endif
QRect paintGraphBounds(glyph_t glyph, const DWRITE_MATRIX &transform) const;
QRect alphaTextureBounds(glyph_t glyph, const DWRITE_MATRIX &transform); QRect alphaTextureBounds(glyph_t glyph, const DWRITE_MATRIX &transform);
QRect colorBitmapBounds(glyph_t glyph, const DWRITE_MATRIX &transform); QRect colorBitmapBounds(glyph_t glyph, const DWRITE_MATRIX &transform);
bool renderColr0GlyphRun(QImage *image, bool renderColr0GlyphRun(QImage *image,