Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qcolrpaintgraphrenderer.cpp
Go to the documentation of this file.
1// Copyright (C) 2025 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3
5
6
8
9QColrPaintGraphRenderer::~QColrPaintGraphRenderer()
10{
11 Q_ASSERT(m_oldPaths.isEmpty());
12 Q_ASSERT(m_oldTransforms.isEmpty());
13
14 delete m_painter;
15}
16
17void QColrPaintGraphRenderer::save()
18{
19 qCDebug(lcColrv1).noquote().nospace()
20 << QByteArray().fill(' ', m_oldPaths.size() * 2)
21 << "[enter scope]";
22
23 m_oldPaths.append(m_currentPath);
24 m_oldTransforms.append(m_currentTransform);
25
26 if (m_painter != nullptr)
27 m_painter->save();
28}
29
30void QColrPaintGraphRenderer::restore()
31{
32 m_currentPath = m_oldPaths.takeLast();
33 m_currentTransform = m_oldTransforms.takeLast();
34
35 qCDebug(lcColrv1).noquote().nospace()
36 << QByteArray().fill(' ', m_oldPaths.size() * 2)
37 << "[exit scope]";
38
39 if (m_painter != nullptr)
40 m_painter->restore();
41}
42
43void QColrPaintGraphRenderer::setClip(QRect rect)
44{
45 qCDebug(lcColrv1).noquote().nospace()
46 << QByteArray().fill(' ', m_oldPaths.size() * 2)
47 << "[set clip: " << rect
48 << "]";
49 if (!isRendering())
50 m_boundingRect = m_boundingRect.united(rect);
51 if (m_painter != nullptr)
52 m_painter->setClipRect(rect);
53}
54
55void QColrPaintGraphRenderer::prependTransform(const QTransform &transform)
56{
57 qCDebug(lcColrv1).noquote().nospace()
58 << QByteArray().fill(' ', m_oldPaths.size() * 2)
59 << "[prepend transform: " << transform
60 << "]";
61 m_currentTransform = transform * m_currentTransform;
62}
63
64void QColrPaintGraphRenderer::appendPath(const QPainterPath &path)
65{
66 qCDebug(lcColrv1).noquote().nospace()
67 << QByteArray().fill(' ', m_oldPaths.size() * 2)
68 << "[append path: " << path.controlPointRect()
69 << "]";
70
71 QPainterPath transformedPath(m_currentTransform.map(path));
72 m_currentPath = m_currentPath.united(transformedPath);
73 if (!isRendering())
74 m_boundingRect = m_boundingRect.united(transformedPath.boundingRect());
75}
76
77void QColrPaintGraphRenderer::setPath(const QPainterPath &path)
78{
79 qCDebug(lcColrv1).noquote().nospace()
80 << QByteArray().fill(' ', m_oldPaths.size() * 2)
81 << "[set path]";
82
83 m_currentPath.clear();
84 appendPath(path);
85}
86
87void QColrPaintGraphRenderer::setSolidColor(QColor color)
88{
89 qCDebug(lcColrv1).noquote().nospace()
90 << QByteArray().fill(' ', m_oldPaths.size() * 2)
91 << "[solid " << color
92 << "]";
93
94 if (m_painter != nullptr)
95 m_painter->setBrush(color);
96}
97
98void QColrPaintGraphRenderer::setLinearGradient(QPointF p0, QPointF p1, QPointF p2,
99 QGradient::Spread spread,
100 const QGradientStops &gradientStops)
101{
102 if (m_painter != nullptr) {
103 qCDebug(lcColrv1).noquote().nospace()
104 << QByteArray().fill(' ', m_oldPaths.size() * 2)
105 << "[linear gradient " << p0 << ", " << p1 << ", " << p2
106 << ", spread: " << spread
107 << ", stop count: " << gradientStops.size()
108 << "]";
109
110 // Calculate new start and end point for single vector gradient preferred by Qt
111 // Find vector perpendicular to p0p2 and project p0p1 onto this to find p3 (final
112 // stop)
113 // https://learn.microsoft.com/en-us/typography/opentype/spec/colr#linear-gradients
114 QVector2D p0p2(p2 - p0);
115 if (qFuzzyIsNull(p0p2.lengthSquared())) {
116 qCWarning(lcColrv1) << "Malformed linear gradient in COLRv1 graph. Points:"
117 << p0
118 << p1
119 << p2;
120 return;
121 }
122
123 // v is perpendicular to p0p2
124 QVector2D v(p0p2.y(), -p0p2.x());
125
126 // u is the vector from p0 to p1
127 QVector2D u(p1 - p0);
128 if (qFuzzyIsNull(u.lengthSquared())) {
129 qCWarning(lcColrv1) << "Malformed linear gradient in COLRv1 graph. Points:"
130 << p0
131 << p1
132 << p2;
133 return;
134 }
135
136 // We find the projected point p3
137 QPointF p3((QVector2D(p0) + v * QVector2D::dotProduct(u, v) / v.lengthSquared()).toPointF());
138
139 p0 = m_currentTransform.map(p0);
140 p3 = m_currentTransform.map(p1);
141
142 QLinearGradient linearGradient(p0, p3);
143 linearGradient.setSpread(spread);
144 linearGradient.setStops(gradientStops);
145 linearGradient.setCoordinateMode(QGradient::LogicalMode);
146
147 m_painter->setBrush(linearGradient);
148 }
149}
150
151void QColrPaintGraphRenderer::setConicalGradient(QPointF center,
152 qreal startAngle, qreal endAngle,
153 QGradient::Spread spread,
154 const QGradientStops &gradientStops)
155{
156 if (m_painter != nullptr) {
157 qCDebug(lcColrv1).noquote().nospace()
158 << QByteArray().fill(' ', m_oldPaths.size() * 2)
159 << "[conical gradient " << center
160 << ", startAngle=" << startAngle
161 << ", endAngle=" << endAngle
162 << ", spread: " << spread
163 << ", stop count: " << gradientStops.size()
164 << "]";
165
166 QConicalGradient conicalGradient(center, startAngle);
167 conicalGradient.setSpread(spread);
168
169 // Adapt stops to actual span since Qt always assumes end angle of 360
170 // Note: This does not give accurate results for the colors outside the angle span.
171 // To do this correctly, we would have to insert stops at 0°, 360° and if the spread
172 // is reflect/repeat, also throughout the uncovered area to get the correct
173 // rendering. It might however be easier to support this in QConicalGradient itself.
174 // For now, this is left only semi-supported, as sweep gradients are currently rare.
175 const qreal multiplier = qFuzzyCompare(endAngle, startAngle)
176 ? 1.0
177 : (endAngle - startAngle) / 360.0;
178
179 QGradientStops adaptedStops;
180 adaptedStops.reserve(gradientStops.size());
181
182 for (const QGradientStop &gradientStop : gradientStops)
183 adaptedStops.append(qMakePair(gradientStop.first * multiplier, gradientStop.second));
184
185 conicalGradient.setStops(adaptedStops);
186 conicalGradient.setCoordinateMode(QGradient::LogicalMode);
187
188 m_painter->setBrush(conicalGradient);
189 }
190}
191
192void QColrPaintGraphRenderer::setRadialGradient(QPointF c0, qreal r0,
193 QPointF c1, qreal r1,
194 QGradient::Spread spread,
195 const QGradientStops &gradientStops)
196{
197 if (m_painter != nullptr) {
198 qCDebug(lcColrv1).noquote().nospace()
199 << QByteArray().fill(' ', m_oldPaths.size() * 2)
200 << "[radial gradient " << c0
201 << ", rad=" << r0
202 << ", " << c1
203 << ", rad=" << r1
204 << ", spread: " << spread
205 << ", stop count: " << gradientStops.size()
206 << "]";
207
208 QPointF c0e(c0 + QPointF(r0, 0.0));
209 QPointF c1e(c1 + QPointF(r1, 0.0));
210
211 c0 = m_currentTransform.map(c0);
212 c0e = m_currentTransform.map(c0e);
213 c1 = m_currentTransform.map(c1);
214 c1e = m_currentTransform.map(c1e);
215
216 const QVector2D d0(c0e - c0);
217 const QVector2D d1(c1e - c1);
218
219 QRadialGradient gradient(c1, d1.length(), c0, d0.length());
220 gradient.setCoordinateMode(QGradient::LogicalMode);
221 gradient.setSpread(spread);
222 gradient.setStops(gradientStops);
223 m_painter->setBrush(gradient);
224 }
225}
226
227void QColrPaintGraphRenderer::drawCurrentPath()
228{
229 qCDebug(lcColrv1).noquote().nospace()
230 << QByteArray().fill(' ', m_oldPaths.size() * 2)
231 << "[draw path " << m_currentPath.controlPointRect() << m_currentTransform
232 << "]";
233
234 if (m_painter != nullptr)
235 m_painter->drawPath(m_currentPath);
236}
237
238void QColrPaintGraphRenderer::beginCalculateBoundingBox()
239{
240 qCDebug(lcColrv1).noquote().nospace()
241 << QByteArray().fill(' ', m_oldPaths.size() * 2)
242 << "[begin calculate bounding box]";
243
244 Q_ASSERT(m_painter == nullptr);
245 m_boundingRect = QRect{};
246}
247
248void QColrPaintGraphRenderer::drawImage(const QImage &image)
249{
250 qCDebug(lcColrv1).noquote().nospace()
251 << QByteArray().fill(' ', m_oldPaths.size() * 2)
252 << "[draw image: " << image.size() << "]";
253
254 if (m_painter != nullptr) {
255 m_painter->setWorldMatrixEnabled(false);
256 m_painter->drawImage(QPoint(0, 0), image);
257 m_painter->setWorldMatrixEnabled(true);
258 }
259}
260
261void QColrPaintGraphRenderer::setCompositionMode(QPainter::CompositionMode mode)
262{
263 qCDebug(lcColrv1).noquote().nospace()
264 << QByteArray().fill(' ', m_oldPaths.size() * 2)
265 << "[set composition mode: " << mode << "]";
266
267 if (m_painter != nullptr)
268 m_painter->setCompositionMode(mode);
269}
270
271void QColrPaintGraphRenderer::beginRender(qreal pixelSizeScale, const QTransform &transform)
272{
273 qCDebug(lcColrv1).noquote().nospace()
274 << QByteArray().fill(' ', m_oldPaths.size() * 2)
275 << "[begin render scale: " << pixelSizeScale
276 << ", transform: " << transform
277 << "]";
278
279 if (m_boundingRect.isEmpty())
280 return;
281
282 QRect alignedRect(m_boundingRect.toAlignedRect());
283
284 m_image = QImage(alignedRect.size(), QImage::Format_ARGB32_Premultiplied);
285 m_image.fill(Qt::transparent);
286
287 Q_ASSERT(m_painter == nullptr);
288 m_painter = new QPainter;
289 m_painter->begin(&m_image);
290 m_painter->setRenderHint(QPainter::Antialiasing);
291 m_painter->setPen(Qt::NoPen);
292 m_painter->setBrush(Qt::NoBrush);
293
294 m_painter->translate(-alignedRect.topLeft());
295
296 // Scale from normalized coordinates
297 m_painter->scale(pixelSizeScale, pixelSizeScale);
298 m_painter->setWorldTransform(transform, true);
299}
300
301QImage QColrPaintGraphRenderer::endRender()
302{
303 qCDebug(lcColrv1).noquote().nospace()
304 << QByteArray().fill(' ', m_oldPaths.size() * 2)
305 << "[end image size: " << m_image.size()
306 << "]";
307
308 Q_ASSERT(m_painter != nullptr);
309 m_painter->end();
310 delete m_painter;
311 m_painter = nullptr;
312
313 return m_image;
314}
315
316QT_END_NAMESPACE