1 | // Copyright (C) 2024 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 | |
4 | #include "qsvgvisitorimpl_p.h" |
5 | #include "qquickgenerator_p.h" |
6 | #include "qquicknodeinfo_p.h" |
7 | |
8 | #include <private/qsvgvisitor_p.h> |
9 | |
10 | #include <QString> |
11 | #include <QPainter> |
12 | #include <QTextDocument> |
13 | #include <QTextLayout> |
14 | #include <QMatrix4x4> |
15 | #include <QQuickItem> |
16 | |
17 | #include <private/qquickshape_p.h> |
18 | #include <private/qquicktext_p.h> |
19 | #include <private/qquicktranslate_p.h> |
20 | #include <private/qquickitem_p.h> |
21 | |
22 | #include <private/qquickimagebase_p_p.h> |
23 | #include <private/qquickimage_p.h> |
24 | #include <private/qsgcurveprocessor_p.h> |
25 | |
26 | #include <private/qquadpath_p.h> |
27 | |
28 | #include <QtCore/private/qstringiterator_p.h> |
29 | |
30 | #include "utils_p.h" |
31 | #include <QtCore/qloggingcategory.h> |
32 | |
33 | #include <QtSvg/private/qsvgstyle_p.h> |
34 | |
35 | QT_BEGIN_NAMESPACE |
36 | |
37 | using namespace Qt::StringLiterals; |
38 | |
39 | Q_DECLARE_LOGGING_CATEGORY(lcQuickVectorImage) |
40 | |
41 | class QSvgStyleResolver |
42 | { |
43 | public: |
44 | QSvgStyleResolver() |
45 | { |
46 | m_dummyImage = QImage(1, 1, QImage::Format_RGB32); |
47 | m_dummyPainter.begin(&m_dummyImage); |
48 | QPen defaultPen(Qt::NoBrush, 1, Qt::SolidLine, Qt::FlatCap, Qt::SvgMiterJoin); |
49 | defaultPen.setMiterLimit(4); |
50 | m_dummyPainter.setPen(defaultPen); |
51 | m_dummyPainter.setBrush(Qt::black); |
52 | } |
53 | |
54 | ~QSvgStyleResolver() |
55 | { |
56 | m_dummyPainter.end(); |
57 | } |
58 | |
59 | QPainter& painter() { return m_dummyPainter; } |
60 | QSvgExtraStates& states() { return m_svgState; } |
61 | |
62 | QColor currentFillColor() const |
63 | { |
64 | if (m_dummyPainter.brush().style() == Qt::NoBrush || |
65 | m_dummyPainter.brush().color() == QColorConstants::Transparent) { |
66 | return QColor(QColorConstants::Transparent); |
67 | } |
68 | |
69 | QColor fillColor; |
70 | fillColor = m_dummyPainter.brush().color(); |
71 | fillColor.setAlphaF(m_svgState.fillOpacity); |
72 | |
73 | return fillColor; |
74 | } |
75 | |
76 | qreal currentFillOpacity() const |
77 | { |
78 | return m_svgState.fillOpacity; |
79 | } |
80 | |
81 | const QGradient *currentStrokeGradient() const |
82 | { |
83 | QBrush brush = m_dummyPainter.pen().brush(); |
84 | if (brush.style() == Qt::LinearGradientPattern |
85 | || brush.style() == Qt::RadialGradientPattern |
86 | || brush.style() == Qt::ConicalGradientPattern) { |
87 | return brush.gradient(); |
88 | } |
89 | return nullptr; |
90 | } |
91 | |
92 | const QGradient *currentFillGradient() const |
93 | { |
94 | if (m_dummyPainter.brush().style() == Qt::LinearGradientPattern || m_dummyPainter.brush().style() == Qt::RadialGradientPattern || m_dummyPainter.brush().style() == Qt::ConicalGradientPattern ) |
95 | return m_dummyPainter.brush().gradient(); |
96 | return nullptr; |
97 | } |
98 | |
99 | QTransform currentFillTransform() const |
100 | { |
101 | return m_dummyPainter.brush().transform(); |
102 | } |
103 | |
104 | QColor currentStrokeColor() const |
105 | { |
106 | if (m_dummyPainter.pen().brush().style() == Qt::NoBrush || |
107 | m_dummyPainter.pen().brush().color() == QColorConstants::Transparent) { |
108 | return QColor(QColorConstants::Transparent); |
109 | } |
110 | |
111 | QColor strokeColor; |
112 | strokeColor = m_dummyPainter.pen().brush().color(); |
113 | strokeColor.setAlphaF(m_svgState.strokeOpacity); |
114 | |
115 | return strokeColor; |
116 | } |
117 | |
118 | static QGradient applyOpacityToGradient(const QGradient &gradient, float opacity) |
119 | { |
120 | QGradient grad = gradient; |
121 | QGradientStops stops; |
122 | for (auto &stop : grad.stops()) { |
123 | stop.second.setAlphaF(stop.second.alphaF() * opacity); |
124 | stops.append(t: stop); |
125 | } |
126 | |
127 | grad.setStops(stops); |
128 | |
129 | return grad; |
130 | } |
131 | |
132 | float currentStrokeWidth() const |
133 | { |
134 | float penWidth = m_dummyPainter.pen().widthF(); |
135 | return penWidth ? penWidth : 1; |
136 | } |
137 | |
138 | QPen currentStroke() const |
139 | { |
140 | return m_dummyPainter.pen(); |
141 | } |
142 | |
143 | protected: |
144 | QPainter m_dummyPainter; |
145 | QImage m_dummyImage; |
146 | QSvgExtraStates m_svgState; |
147 | }; |
148 | |
149 | Q_GLOBAL_STATIC(QSvgStyleResolver, styleResolver) |
150 | |
151 | namespace { |
152 | inline bool isPathContainer(const QSvgStructureNode *node) |
153 | { |
154 | bool foundPath = false; |
155 | for (const auto *child : node->renderers()) { |
156 | switch (child->type()) { |
157 | // nodes that shouldn't go inside Shape{} |
158 | case QSvgNode::Switch: |
159 | case QSvgNode::Doc: |
160 | case QSvgNode::Group: |
161 | case QSvgNode::Animation: |
162 | case QSvgNode::Use: |
163 | case QSvgNode::Video: |
164 | case QSvgNode::Image: |
165 | case QSvgNode::Textarea: |
166 | case QSvgNode::Text: |
167 | case QSvgNode::Tspan: |
168 | //qCDebug(lcQuickVectorGraphics) << "NOT path container because" << node->typeName() ; |
169 | return false; |
170 | |
171 | // nodes that could go inside Shape{} |
172 | case QSvgNode::Defs: |
173 | break; |
174 | |
175 | // nodes that are done as pure ShapePath{} |
176 | case QSvgNode::Rect: |
177 | case QSvgNode::Circle: |
178 | case QSvgNode::Ellipse: |
179 | case QSvgNode::Line: |
180 | case QSvgNode::Path: |
181 | case QSvgNode::Polygon: |
182 | case QSvgNode::Polyline: |
183 | if (!child->style().transform.isDefault()) { |
184 | //qCDebug(lcQuickVectorGraphics) << "NOT path container because local transform"; |
185 | return false; |
186 | } |
187 | foundPath = true; |
188 | break; |
189 | default: |
190 | qCDebug(lcQuickVectorImage) << "Unhandled type in switch" << child->type(); |
191 | break; |
192 | } |
193 | } |
194 | //qCDebug(lcQuickVectorGraphics) << "Container" << node->nodeId() << node->typeName() << "is" << foundPath; |
195 | return foundPath; |
196 | } |
197 | |
198 | static QString capStyleName(Qt::PenCapStyle style) |
199 | { |
200 | QString styleName; |
201 | |
202 | switch (style) { |
203 | case Qt::SquareCap: |
204 | styleName = QStringLiteral("squarecap" ); |
205 | break; |
206 | case Qt::FlatCap: |
207 | styleName = QStringLiteral("flatcap" ); |
208 | break; |
209 | case Qt::RoundCap: |
210 | styleName = QStringLiteral("roundcap" ); |
211 | break; |
212 | default: |
213 | break; |
214 | } |
215 | |
216 | return styleName; |
217 | } |
218 | |
219 | static QString joinStyleName(Qt::PenJoinStyle style) |
220 | { |
221 | QString styleName; |
222 | |
223 | switch (style) { |
224 | case Qt::MiterJoin: |
225 | styleName = QStringLiteral("miterjoin" ); |
226 | break; |
227 | case Qt::BevelJoin: |
228 | styleName = QStringLiteral("beveljoin" ); |
229 | break; |
230 | case Qt::RoundJoin: |
231 | styleName = QStringLiteral("roundjoin" ); |
232 | break; |
233 | case Qt::SvgMiterJoin: |
234 | styleName = QStringLiteral("svgmiterjoin" ); |
235 | break; |
236 | default: |
237 | break; |
238 | } |
239 | |
240 | return styleName; |
241 | } |
242 | |
243 | static QString dashArrayString(QList<qreal> dashArray) |
244 | { |
245 | if (dashArray.isEmpty()) |
246 | return QString(); |
247 | |
248 | QString dashArrayString; |
249 | QTextStream stream(&dashArrayString); |
250 | |
251 | for (int i = 0; i < dashArray.length() - 1; i++) { |
252 | qreal value = dashArray[i]; |
253 | stream << value << ", " ; |
254 | } |
255 | |
256 | stream << dashArray.last(); |
257 | |
258 | return dashArrayString; |
259 | } |
260 | }; |
261 | |
262 | QSvgVisitorImpl::QSvgVisitorImpl(const QString svgFileName, QQuickGenerator *generator) |
263 | : m_svgFileName(svgFileName) |
264 | , m_generator(generator) |
265 | { |
266 | } |
267 | |
268 | bool QSvgVisitorImpl::traverse() |
269 | { |
270 | if (!m_generator) { |
271 | qCDebug(lcQuickVectorImage) << "No valid QQuickGenerator is set. Genration will stop" ; |
272 | return false; |
273 | } |
274 | |
275 | auto *doc = QSvgTinyDocument::load(file: m_svgFileName); |
276 | if (!doc) { |
277 | qCDebug(lcQuickVectorImage) << "Not a valid Svg File : " << m_svgFileName; |
278 | return false; |
279 | } |
280 | |
281 | QSvgVisitor::traverse(node: doc); |
282 | return true; |
283 | } |
284 | |
285 | void QSvgVisitorImpl::visitNode(const QSvgNode *node) |
286 | { |
287 | handleBaseNodeSetup(node); |
288 | |
289 | NodeInfo info; |
290 | fillCommonNodeInfo(node, info); |
291 | |
292 | m_generator->generateNode(info); |
293 | |
294 | handleBaseNodeEnd(node); |
295 | } |
296 | |
297 | void QSvgVisitorImpl::visitImageNode(const QSvgImage *node) |
298 | { |
299 | // TODO: this requires proper asset management. |
300 | handleBaseNodeSetup(node); |
301 | |
302 | ImageNodeInfo info; |
303 | fillCommonNodeInfo(node, info); |
304 | info.image = node->image(); |
305 | info.rect = node->rect(); |
306 | info.externalFileReference = node->filename(); |
307 | |
308 | m_generator->generateImageNode(info); |
309 | |
310 | handleBaseNodeEnd(node); |
311 | } |
312 | |
313 | void QSvgVisitorImpl::visitRectNode(const QSvgRect *node) |
314 | { |
315 | QRectF rect = node->rect(); |
316 | QPointF rads = node->radius(); |
317 | // This is using Qt::RelativeSize semantics: percentage of half rect size |
318 | qreal x1 = rect.left(); |
319 | qreal x2 = rect.right(); |
320 | qreal y1 = rect.top(); |
321 | qreal y2 = rect.bottom(); |
322 | |
323 | qreal rx = rads.x() * rect.width() / 200; |
324 | qreal ry = rads.y() * rect.height() / 200; |
325 | QPainterPath p; |
326 | |
327 | p.moveTo(x: x1 + rx, y: y1); |
328 | p.lineTo(x: x2 - rx, y: y1); |
329 | // qCDebug(lcQuickVectorGraphics) << "Line1" << x2 - rx << y1; |
330 | p.arcTo(x: x2 - rx * 2, y: y1, w: rx * 2, h: ry * 2, startAngle: 90, arcLength: -90); // ARC to x2, y1 + ry |
331 | // qCDebug(lcQuickVectorGraphics) << "p1" << p; |
332 | |
333 | p.lineTo(x: x2, y: y2 - ry); |
334 | p.arcTo(x: x2 - rx * 2, y: y2 - ry * 2, w: rx * 2, h: ry * 2, startAngle: 0, arcLength: -90); // ARC to x2 - rx, y2 |
335 | |
336 | p.lineTo(x: x1 + rx, y: y2); |
337 | p.arcTo(x: x1, y: y2 - ry * 2, w: rx * 2, h: ry * 2, startAngle: 270, arcLength: -90); // ARC to x1, y2 - ry |
338 | |
339 | p.lineTo(x: x1, y: y1 + ry); |
340 | p.arcTo(x: x1, y: y1, w: rx * 2, h: ry * 2, startAngle: 180, arcLength: -90); // ARC to x1 + rx, y1 |
341 | |
342 | handlePathNode(node, path: p); |
343 | } |
344 | |
345 | void QSvgVisitorImpl::visitEllipseNode(const QSvgEllipse *node) |
346 | { |
347 | QRectF rect = node->rect(); |
348 | |
349 | QPainterPath p; |
350 | p.addEllipse(rect); |
351 | |
352 | handlePathNode(node, path: p); |
353 | } |
354 | |
355 | void QSvgVisitorImpl::visitPathNode(const QSvgPath *node) |
356 | { |
357 | handlePathNode(node, path: node->path()); |
358 | } |
359 | |
360 | void QSvgVisitorImpl::visitLineNode(const QSvgLine *node) |
361 | { |
362 | QPainterPath p; |
363 | p.moveTo(p: node->line().p1()); |
364 | p.lineTo(p: node->line().p2()); |
365 | handlePathNode(node, path: p); |
366 | } |
367 | |
368 | void QSvgVisitorImpl::visitPolygonNode(const QSvgPolygon *node) |
369 | { |
370 | QPainterPath p = QQuickVectorImageGenerator::Utils::polygonToPath(poly: node->polygon(), closed: true); |
371 | handlePathNode(node, path: p); |
372 | } |
373 | |
374 | void QSvgVisitorImpl::visitPolylineNode(const QSvgPolyline *node) |
375 | { |
376 | QPainterPath p = QQuickVectorImageGenerator::Utils::polygonToPath(poly: node->polygon(), closed: false); |
377 | handlePathNode(node, path: p); |
378 | } |
379 | |
380 | QString QSvgVisitorImpl::gradientCssDescription(const QGradient *gradient) |
381 | { |
382 | QString cssDescription; |
383 | if (gradient->type() == QGradient::LinearGradient) { |
384 | const QLinearGradient *linearGradient = static_cast<const QLinearGradient *>(gradient); |
385 | |
386 | cssDescription += " -qt-foreground: qlineargradient("_L1 ; |
387 | cssDescription += "x1:"_L1 + QString::number(linearGradient->start().x()) + u','; |
388 | cssDescription += "y1:"_L1 + QString::number(linearGradient->start().y()) + u','; |
389 | cssDescription += "x2:"_L1 + QString::number(linearGradient->finalStop().x()) + u','; |
390 | cssDescription += "y2:"_L1 + QString::number(linearGradient->finalStop().y()) + u','; |
391 | } else if (gradient->type() == QGradient::RadialGradient) { |
392 | const QRadialGradient *radialGradient = static_cast<const QRadialGradient *>(gradient); |
393 | |
394 | cssDescription += " -qt-foreground: qradialgradient("_L1 ; |
395 | cssDescription += "cx:"_L1 + QString::number(radialGradient->center().x()) + u','; |
396 | cssDescription += "cy:"_L1 + QString::number(radialGradient->center().y()) + u','; |
397 | cssDescription += "fx:"_L1 + QString::number(radialGradient->focalPoint().x()) + u','; |
398 | cssDescription += "fy:"_L1 + QString::number(radialGradient->focalPoint().y()) + u','; |
399 | cssDescription += "radius:"_L1 + QString::number(radialGradient->radius()) + u','; |
400 | } else { |
401 | const QConicalGradient *conicalGradient = static_cast<const QConicalGradient *>(gradient); |
402 | |
403 | cssDescription += " -qt-foreground: qconicalgradient("_L1 ; |
404 | cssDescription += "cx:"_L1 + QString::number(conicalGradient->center().x()) + u','; |
405 | cssDescription += "cy:"_L1 + QString::number(conicalGradient->center().y()) + u','; |
406 | cssDescription += "angle:"_L1 + QString::number(conicalGradient->angle()) + u','; |
407 | } |
408 | |
409 | const QStringList coordinateModes = { "logical"_L1 , "stretchtodevice"_L1 , "objectbounding"_L1 , "object"_L1 }; |
410 | cssDescription += "coordinatemode:"_L1 ; |
411 | cssDescription += coordinateModes.at(i: int(gradient->coordinateMode())); |
412 | cssDescription += u','; |
413 | |
414 | const QStringList spreads = { "pad"_L1 , "reflect"_L1 , "repeat"_L1 }; |
415 | cssDescription += "spread:"_L1 ; |
416 | cssDescription += spreads.at(i: int(gradient->spread())); |
417 | |
418 | for (const QGradientStop &stop : gradient->stops()) { |
419 | cssDescription += ",stop:"_L1 ; |
420 | cssDescription += QString::number(stop.first); |
421 | cssDescription += u' '; |
422 | cssDescription += stop.second.name(format: QColor::HexArgb); |
423 | } |
424 | |
425 | cssDescription += ");"_L1 ; |
426 | |
427 | return cssDescription; |
428 | } |
429 | |
430 | QString QSvgVisitorImpl::colorCssDescription(QColor color) |
431 | { |
432 | QString cssDescription; |
433 | cssDescription += QStringLiteral("rgba(" ); |
434 | cssDescription += QString::number(color.red()) + QStringLiteral("," ); |
435 | cssDescription += QString::number(color.green()) + QStringLiteral("," ); |
436 | cssDescription += QString::number(color.blue()) + QStringLiteral("," ); |
437 | cssDescription += QString::number(color.alphaF()) + QStringLiteral(")" ); |
438 | |
439 | return cssDescription; |
440 | } |
441 | |
442 | namespace { |
443 | |
444 | // Simple class for representing the SVG font as a font engine |
445 | // We use the Proxy font engine type, which is currently unused and does not map to |
446 | // any specific font engine |
447 | // (The QSvgFont object must outlive the engine.) |
448 | class QSvgFontEngine : public QFontEngine |
449 | { |
450 | public: |
451 | QSvgFontEngine(const QSvgFont *font, qreal size); |
452 | |
453 | QFontEngine *cloneWithSize(qreal size) const override; |
454 | |
455 | glyph_t glyphIndex(uint ucs4) const override; |
456 | int stringToCMap(const QChar *str, |
457 | int len, |
458 | QGlyphLayout *glyphs, |
459 | int *nglyphs, |
460 | ShaperFlags flags) const override; |
461 | |
462 | void addGlyphsToPath(glyph_t *glyphs, |
463 | QFixedPoint *positions, |
464 | int nGlyphs, |
465 | QPainterPath *path, |
466 | QTextItem::RenderFlags flags) override; |
467 | |
468 | glyph_metrics_t boundingBox(glyph_t glyph) override; |
469 | |
470 | void recalcAdvances(QGlyphLayout *, ShaperFlags) const override; |
471 | QFixed ascent() const override; |
472 | QFixed capHeight() const override; |
473 | QFixed descent() const override; |
474 | QFixed leading() const override; |
475 | qreal maxCharWidth() const override; |
476 | qreal minLeftBearing() const override; |
477 | qreal minRightBearing() const override; |
478 | |
479 | QFixed emSquareSize() const override; |
480 | |
481 | private: |
482 | const QSvgFont *m_font; |
483 | }; |
484 | |
485 | QSvgFontEngine::QSvgFontEngine(const QSvgFont *font, qreal size) |
486 | : QFontEngine(Proxy) |
487 | , m_font(font) |
488 | { |
489 | fontDef.pixelSize = size; |
490 | fontDef.families = QStringList(m_font->m_familyName); |
491 | } |
492 | |
493 | QFixed QSvgFontEngine::emSquareSize() const |
494 | { |
495 | return QFixed::fromReal(r: m_font->m_unitsPerEm); |
496 | } |
497 | |
498 | glyph_t QSvgFontEngine::glyphIndex(uint ucs4) const |
499 | { |
500 | if (ucs4 < USHRT_MAX && m_font->m_glyphs.contains(key: QChar(ushort(ucs4)))) |
501 | return glyph_t(ucs4); |
502 | |
503 | return 0; |
504 | } |
505 | |
506 | int QSvgFontEngine::stringToCMap(const QChar *str, |
507 | int len, |
508 | QGlyphLayout *glyphs, |
509 | int *nglyphs, |
510 | ShaperFlags flags) const |
511 | { |
512 | Q_ASSERT(glyphs->numGlyphs >= *nglyphs); |
513 | if (*nglyphs < len) { |
514 | *nglyphs = len; |
515 | return -1; |
516 | } |
517 | |
518 | int ucs4Length = 0; |
519 | QStringIterator it(str, str + len); |
520 | while (it.hasNext()) { |
521 | char32_t ucs4 = it.next(); |
522 | glyph_t index = glyphIndex(ucs4); |
523 | glyphs->glyphs[ucs4Length++] = index; |
524 | } |
525 | |
526 | *nglyphs = ucs4Length; |
527 | glyphs->numGlyphs = ucs4Length; |
528 | |
529 | if (!(flags & GlyphIndicesOnly)) |
530 | recalcAdvances(glyphs, flags); |
531 | |
532 | return *nglyphs; |
533 | } |
534 | |
535 | void QSvgFontEngine::addGlyphsToPath(glyph_t *glyphs, |
536 | QFixedPoint *positions, |
537 | int nGlyphs, |
538 | QPainterPath *path, |
539 | QTextItem::RenderFlags flags) |
540 | { |
541 | Q_UNUSED(flags); |
542 | const qreal scale = fontDef.pixelSize / m_font->m_unitsPerEm; |
543 | for (int i = 0; i < nGlyphs; ++i) { |
544 | glyph_t index = glyphs[i]; |
545 | if (index > 0) { |
546 | QPointF position = positions[i].toPointF(); |
547 | QPainterPath glyphPath = m_font->m_glyphs.value(key: QChar(ushort(index))).m_path; |
548 | |
549 | QTransform xform; |
550 | xform.translate(dx: position.x(), dy: position.y()); |
551 | xform.scale(sx: scale, sy: -scale); |
552 | glyphPath = xform.map(p: glyphPath); |
553 | path->addPath(path: glyphPath); |
554 | } |
555 | } |
556 | } |
557 | |
558 | glyph_metrics_t QSvgFontEngine::boundingBox(glyph_t glyph) |
559 | { |
560 | glyph_metrics_t ret; |
561 | ret.x = 0; // left bearing |
562 | ret.y = -ascent(); |
563 | const qreal scale = fontDef.pixelSize / m_font->m_unitsPerEm; |
564 | const QSvgGlyph &svgGlyph = m_font->m_glyphs.value(key: QChar(ushort(glyph))); |
565 | ret.width = QFixed::fromReal(r: svgGlyph.m_horizAdvX * scale); |
566 | ret.height = ascent() + descent(); |
567 | return ret; |
568 | } |
569 | |
570 | QFontEngine *QSvgFontEngine::cloneWithSize(qreal size) const |
571 | { |
572 | QSvgFontEngine *otherEngine = new QSvgFontEngine(m_font, size); |
573 | return otherEngine; |
574 | } |
575 | |
576 | void QSvgFontEngine::recalcAdvances(QGlyphLayout *glyphLayout, ShaperFlags) const |
577 | { |
578 | const qreal scale = fontDef.pixelSize / m_font->m_unitsPerEm; |
579 | for (int i = 0; i < glyphLayout->numGlyphs; i++) { |
580 | glyph_t glyph = glyphLayout->glyphs[i]; |
581 | const QSvgGlyph &svgGlyph = m_font->m_glyphs.value(key: QChar(ushort(glyph))); |
582 | glyphLayout->advances[i] = QFixed::fromReal(r: svgGlyph.m_horizAdvX * scale); |
583 | } |
584 | } |
585 | |
586 | QFixed QSvgFontEngine::ascent() const |
587 | { |
588 | return QFixed::fromReal(r: fontDef.pixelSize); |
589 | } |
590 | |
591 | QFixed QSvgFontEngine::capHeight() const |
592 | { |
593 | return ascent(); |
594 | } |
595 | QFixed QSvgFontEngine::descent() const |
596 | { |
597 | return QFixed{}; |
598 | } |
599 | |
600 | QFixed QSvgFontEngine::leading() const |
601 | { |
602 | return QFixed{}; |
603 | } |
604 | |
605 | qreal QSvgFontEngine::maxCharWidth() const |
606 | { |
607 | const qreal scale = fontDef.pixelSize / m_font->m_unitsPerEm; |
608 | return m_font->m_horizAdvX * scale; |
609 | } |
610 | |
611 | qreal QSvgFontEngine::minLeftBearing() const |
612 | { |
613 | return 0.0; |
614 | } |
615 | |
616 | qreal QSvgFontEngine::minRightBearing() const |
617 | { |
618 | return 0.0; |
619 | } |
620 | } |
621 | |
622 | void QSvgVisitorImpl::visitTextNode(const QSvgText *node) |
623 | { |
624 | handleBaseNodeSetup(node); |
625 | const bool isTextArea = node->type() == QSvgNode::Textarea; |
626 | |
627 | QString text; |
628 | const QSvgFont *svgFont = styleResolver->states().svgFont; |
629 | bool needsRichText = false; |
630 | bool preserveWhiteSpace = node->whitespaceMode() == QSvgText::Preserve; |
631 | const QGradient *mainGradient = styleResolver->currentFillGradient(); |
632 | |
633 | QFontEngine *fontEngine = nullptr; |
634 | if (svgFont != nullptr) { |
635 | fontEngine = new QSvgFontEngine(svgFont, styleResolver->painter().font().pointSize()); |
636 | fontEngine->ref.ref(); |
637 | } |
638 | |
639 | #if QT_CONFIG(texthtmlparser) |
640 | bool needsPathNode = mainGradient != nullptr |
641 | || svgFont != nullptr |
642 | || styleResolver->currentStrokeGradient() != nullptr; |
643 | #endif |
644 | for (const auto *tspan : node->tspans()) { |
645 | if (!tspan) { |
646 | text += QStringLiteral("<br>" ); |
647 | continue; |
648 | } |
649 | |
650 | // Note: We cannot get the font directly from the style, since this does |
651 | // not apply the weight, since this is relative and depends on current state. |
652 | handleBaseNodeSetup(node: tspan); |
653 | QFont font = styleResolver->painter().font(); |
654 | |
655 | QString styleTagContent; |
656 | |
657 | if ((font.resolveMask() & QFont::FamilyResolved) |
658 | || (font.resolveMask() & QFont::FamiliesResolved)) { |
659 | styleTagContent += QStringLiteral("font-family: %1;" ).arg(a: font.family()); |
660 | } |
661 | |
662 | if (font.resolveMask() & QFont::WeightResolved |
663 | && font.weight() != QFont::Normal |
664 | && font.weight() != QFont::Bold) { |
665 | styleTagContent += QStringLiteral("font-weight: %1;" ).arg(a: int(font.weight())); |
666 | } |
667 | |
668 | if (font.resolveMask() & QFont::SizeResolved) { |
669 | // Pixel size stored as point size in SVG parser |
670 | styleTagContent += QStringLiteral("font-size: %1px;" ).arg(a: int(font.pointSizeF())); |
671 | } |
672 | |
673 | if (font.resolveMask() & QFont::CapitalizationResolved |
674 | && font.capitalization() == QFont::SmallCaps) { |
675 | styleTagContent += QStringLiteral("font-variant: small-caps;" ); |
676 | } |
677 | |
678 | if (styleResolver->currentFillGradient() != nullptr |
679 | && styleResolver->currentFillGradient() != mainGradient) { |
680 | const QGradient grad = styleResolver->applyOpacityToGradient(gradient: *styleResolver->currentFillGradient(), opacity: styleResolver->currentFillOpacity()); |
681 | styleTagContent += gradientCssDescription(gradient: &grad) + u';'; |
682 | #if QT_CONFIG(texthtmlparser) |
683 | needsPathNode = true; |
684 | #endif |
685 | } |
686 | |
687 | const QColor currentStrokeColor = styleResolver->currentStrokeColor(); |
688 | if (currentStrokeColor.alpha() > 0) { |
689 | QString strokeColor = colorCssDescription(color: currentStrokeColor); |
690 | styleTagContent += QStringLiteral("-qt-stroke-color:%1;" ).arg(a: strokeColor); |
691 | styleTagContent += QStringLiteral("-qt-stroke-width:%1px;" ).arg(a: styleResolver->currentStrokeWidth()); |
692 | styleTagContent += QStringLiteral("-qt-stroke-dasharray:%1;" ).arg(a: dashArrayString(dashArray: styleResolver->currentStroke().dashPattern())); |
693 | styleTagContent += QStringLiteral("-qt-stroke-dashoffset:%1;" ).arg(a: styleResolver->currentStroke().dashOffset()); |
694 | styleTagContent += QStringLiteral("-qt-stroke-lineCap:%1;" ).arg(a: capStyleName(style: styleResolver->currentStroke().capStyle())); |
695 | styleTagContent += QStringLiteral("-qt-stroke-lineJoin:%1;" ).arg(a: joinStyleName(style: styleResolver->currentStroke().joinStyle())); |
696 | if (styleResolver->currentStroke().joinStyle() == Qt::MiterJoin || styleResolver->currentStroke().joinStyle() == Qt::SvgMiterJoin) |
697 | styleTagContent += QStringLiteral("-qt-stroke-miterlimit:%1;" ).arg(a: styleResolver->currentStroke().miterLimit()); |
698 | #if QT_CONFIG(texthtmlparser) |
699 | needsPathNode = true; |
700 | #endif |
701 | } |
702 | |
703 | if (tspan->whitespaceMode() == QSvgText::Preserve && !preserveWhiteSpace) |
704 | styleTagContent += QStringLiteral("white-space: pre-wrap;" ); |
705 | |
706 | QString content = tspan->text().toHtmlEscaped(); |
707 | content.replace(before: QLatin1Char('\t'), after: QLatin1Char(' ')); |
708 | content.replace(before: QLatin1Char('\n'), after: QLatin1Char(' ')); |
709 | |
710 | bool fontTag = false; |
711 | if (!tspan->style().fill.isDefault()) { |
712 | auto &b = tspan->style().fill->qbrush(); |
713 | qCDebug(lcQuickVectorImage) << "tspan FILL:" << b; |
714 | if (b.style() != Qt::NoBrush) |
715 | { |
716 | if (qFuzzyCompare(p1: b.color().alphaF() + 1.0, p2: 2.0)) |
717 | { |
718 | QString spanColor = b.color().name(); |
719 | fontTag = !spanColor.isEmpty(); |
720 | if (fontTag) |
721 | text += QStringLiteral("<font color=\"%1\">" ).arg(a: spanColor); |
722 | } else { |
723 | QString spanColor = colorCssDescription(color: b.color()); |
724 | styleTagContent += QStringLiteral("color:%1" ).arg(a: spanColor); |
725 | } |
726 | } |
727 | } |
728 | |
729 | needsRichText = needsRichText || !styleTagContent.isEmpty(); |
730 | if (!styleTagContent.isEmpty()) |
731 | text += QStringLiteral("<span style=\"%1\">" ).arg(a: styleTagContent); |
732 | |
733 | if (font.resolveMask() & QFont::WeightResolved && font.bold()) |
734 | text += QStringLiteral("<b>" ); |
735 | |
736 | if (font.resolveMask() & QFont::StyleResolved && font.italic()) |
737 | text += QStringLiteral("<i>" ); |
738 | |
739 | if (font.resolveMask() & QFont::CapitalizationResolved) { |
740 | switch (font.capitalization()) { |
741 | case QFont::AllLowercase: |
742 | content = content.toLower(); |
743 | break; |
744 | case QFont::AllUppercase: |
745 | content = content.toUpper(); |
746 | break; |
747 | case QFont::Capitalize: |
748 | // ### We need to iterate over the string and do the title case conversion, |
749 | // since this is not part of QString. |
750 | qCWarning(lcQuickVectorImage) << "Title case not implemented for tspan" ; |
751 | break; |
752 | default: |
753 | break; |
754 | } |
755 | } |
756 | text += content; |
757 | if (fontTag) |
758 | text += QStringLiteral("</font>" ); |
759 | |
760 | if (font.resolveMask() & QFont::StyleResolved && font.italic()) |
761 | text += QStringLiteral("</i>" ); |
762 | |
763 | if (font.resolveMask() & QFont::WeightResolved && font.bold()) |
764 | text += QStringLiteral("</b>" ); |
765 | |
766 | if (!styleTagContent.isEmpty()) |
767 | text += QStringLiteral("</span>" ); |
768 | |
769 | handleBaseNodeEnd(node: tspan); |
770 | } |
771 | |
772 | if (preserveWhiteSpace && (needsRichText || styleResolver->currentFillGradient() != nullptr)) |
773 | text = QStringLiteral("<span style=\"white-space: pre-wrap\">" ) + text + QStringLiteral("</span>" ); |
774 | |
775 | QFont font = styleResolver->painter().font(); |
776 | if (font.pixelSize() <= 0 && font.pointSize() > 0) |
777 | font.setPixelSize(font.pointSize()); // Pixel size stored as point size by SVG parser |
778 | |
779 | font.setHintingPreference(QFont::PreferNoHinting); |
780 | |
781 | #if QT_CONFIG(texthtmlparser) |
782 | if (needsPathNode) { |
783 | QTextDocument document; |
784 | document.setHtml(text); |
785 | if (isTextArea && node->size().width() > 0) |
786 | document.setTextWidth(node->size().width()); |
787 | document.setDefaultFont(font); |
788 | document.pageCount(); // Force layout |
789 | |
790 | QTextBlock block = document.firstBlock(); |
791 | while (block.isValid()) { |
792 | QTextLayout *lout = block.layout(); |
793 | |
794 | if (lout != nullptr) { |
795 | QRectF boundingRect = lout->boundingRect(); |
796 | |
797 | // If this block has requested the current SVG font, we override it |
798 | // (note that this limits the text to one svg font, but this is also the case |
799 | // in the QPainter at the moment, and needs a more centralized solution in Qt Svg |
800 | // first) |
801 | QFont blockFont = block.charFormat().font(); |
802 | if (svgFont != nullptr |
803 | && blockFont.family() == svgFont->m_familyName) { |
804 | QRawFont rawFont; |
805 | QRawFontPrivate *rawFontD = QRawFontPrivate::get(font: rawFont); |
806 | rawFontD->setFontEngine(fontEngine->cloneWithSize(blockFont.pixelSize())); |
807 | |
808 | lout->setRawFont(rawFont); |
809 | } |
810 | |
811 | auto addPathForFormat = [&](QPainterPath p, QTextCharFormat fmt) { |
812 | PathNodeInfo info; |
813 | fillCommonNodeInfo(node, info); |
814 | auto fillStyle = node->style().fill; |
815 | if (fillStyle) |
816 | info.fillRule = fillStyle->fillRule(); |
817 | |
818 | if (fmt.hasProperty(propertyId: QTextCharFormat::ForegroundBrush)) { |
819 | info.fillColor = fmt.foreground().color(); |
820 | if (fmt.foreground().gradient() != nullptr && fmt.foreground().gradient()->type() != QGradient::NoGradient) |
821 | info.grad = *fmt.foreground().gradient(); |
822 | } else { |
823 | info.fillColor = styleResolver->currentFillColor(); |
824 | } |
825 | |
826 | info.painterPath = p; |
827 | |
828 | const QGradient *strokeGradient = styleResolver->currentStrokeGradient(); |
829 | QPen pen; |
830 | if (fmt.hasProperty(propertyId: QTextCharFormat::TextOutline)) { |
831 | pen = fmt.textOutline(); |
832 | if (strokeGradient == nullptr) { |
833 | info.strokeStyle = StrokeStyle::fromPen(p: pen); |
834 | info.strokeStyle.color = pen.color(); |
835 | } |
836 | } else { |
837 | pen = styleResolver->currentStroke(); |
838 | if (strokeGradient == nullptr) { |
839 | info.strokeStyle = StrokeStyle::fromPen(p: pen); |
840 | info.strokeStyle.color = styleResolver->currentStrokeColor(); |
841 | } |
842 | } |
843 | |
844 | if (info.grad.type() == QGradient::NoGradient && styleResolver->currentFillGradient() != nullptr) |
845 | info.grad = styleResolver->applyOpacityToGradient(gradient: *styleResolver->currentFillGradient(), opacity: styleResolver->currentFillOpacity()); |
846 | |
847 | info.fillTransform = styleResolver->currentFillTransform(); |
848 | |
849 | m_generator->generatePath(info, overrideBoundingRect: boundingRect); |
850 | |
851 | if (strokeGradient != nullptr) { |
852 | PathNodeInfo strokeInfo; |
853 | fillCommonNodeInfo(node, info&: strokeInfo); |
854 | |
855 | strokeInfo.grad = *strokeGradient; |
856 | |
857 | QPainterPathStroker stroker(pen); |
858 | strokeInfo.painterPath = stroker.createStroke(path: p); |
859 | m_generator->generatePath(info: strokeInfo, overrideBoundingRect: boundingRect); |
860 | } |
861 | }; |
862 | |
863 | qreal baselineOffset = -QFontMetricsF(font).ascent(); |
864 | if (lout->lineCount() > 0 && lout->lineAt(i: 0).isValid()) |
865 | baselineOffset = -lout->lineAt(i: 0).ascent(); |
866 | |
867 | const QPointF baselineTranslation(0.0, baselineOffset); |
868 | auto glyphsToPath = [&](QList<QGlyphRun> glyphRuns, qreal width) { |
869 | QList<QPainterPath> paths; |
870 | for (const QGlyphRun &glyphRun : glyphRuns) { |
871 | QRawFont font = glyphRun.rawFont(); |
872 | QList<quint32> glyphIndexes = glyphRun.glyphIndexes(); |
873 | QList<QPointF> positions = glyphRun.positions(); |
874 | |
875 | for (qsizetype j = 0; j < glyphIndexes.size(); ++j) { |
876 | quint32 glyphIndex = glyphIndexes.at(i: j); |
877 | const QPointF &pos = positions.at(i: j); |
878 | |
879 | QPainterPath p = font.pathForGlyph(glyphIndex); |
880 | p.translate(offset: pos + node->position() + baselineTranslation); |
881 | if (styleResolver->states().textAnchor == Qt::AlignHCenter) |
882 | p.translate(offset: QPointF(-0.5 * width, 0)); |
883 | else if (styleResolver->states().textAnchor == Qt::AlignRight) |
884 | p.translate(offset: QPointF(-width, 0)); |
885 | paths.append(t: p); |
886 | } |
887 | } |
888 | |
889 | return paths; |
890 | }; |
891 | |
892 | QList<QTextLayout::FormatRange> formats = block.textFormats(); |
893 | for (int i = 0; i < formats.size(); ++i) { |
894 | QTextLayout::FormatRange range = formats.at(i); |
895 | |
896 | // If we hit a "multi" anchor, it means we have additional formats to apply |
897 | // for both this and the subsequent range, so we merge them. |
898 | if (!range.format.anchorNames().isEmpty() |
899 | && range.format.anchorNames().first().startsWith(QStringLiteral("multi" )) |
900 | && i < formats.size() - 1) { |
901 | QTextLayout::FormatRange = formats.at(i: ++i); |
902 | range.length += nextRange.length; |
903 | range.format.merge(other: nextRange.format); |
904 | } |
905 | QList<QGlyphRun> glyphRuns = lout->glyphRuns(from: range.start, length: range.length); |
906 | QList<QPainterPath> paths = glyphsToPath(glyphRuns, lout->minimumWidth()); |
907 | for (const QPainterPath &path : paths) |
908 | addPathForFormat(path, range.format); |
909 | } |
910 | } |
911 | |
912 | block = block.next(); |
913 | } |
914 | } else |
915 | #endif |
916 | { |
917 | TextNodeInfo info; |
918 | fillCommonNodeInfo(node, info); |
919 | |
920 | info.position = node->position(); |
921 | info.size = node->size(); |
922 | info.font = font; |
923 | info.text = text; |
924 | info.isTextArea = isTextArea; |
925 | info.needsRichText = needsRichText; |
926 | info.fillColor = styleResolver->currentFillColor(); |
927 | info.alignment = styleResolver->states().textAnchor; |
928 | info.strokeColor = styleResolver->currentStrokeColor(); |
929 | |
930 | m_generator->generateTextNode(info); |
931 | } |
932 | |
933 | handleBaseNodeEnd(node); |
934 | |
935 | if (fontEngine != nullptr) { |
936 | fontEngine->ref.deref(); |
937 | Q_ASSERT(fontEngine->ref.loadRelaxed() == 0); |
938 | delete fontEngine; |
939 | } |
940 | } |
941 | |
942 | void QSvgVisitorImpl::visitUseNode(const QSvgUse *node) |
943 | { |
944 | QSvgNode *link = node->link(); |
945 | if (!link) |
946 | return; |
947 | |
948 | handleBaseNodeSetup(node); |
949 | UseNodeInfo info; |
950 | fillCommonNodeInfo(node, info); |
951 | |
952 | info.stage = StructureNodeStage::Start; |
953 | info.startPos = node->start(); |
954 | |
955 | m_generator->generateUseNode(info); |
956 | |
957 | QSvgVisitor::traverse(node: link); |
958 | |
959 | info.stage = StructureNodeStage::End; |
960 | m_generator->generateUseNode(info); |
961 | handleBaseNodeEnd(node); |
962 | } |
963 | |
964 | bool QSvgVisitorImpl::visitSwitchNodeStart(const QSvgSwitch *node) |
965 | { |
966 | QSvgNode *link = node->childToRender(); |
967 | if (!link) |
968 | return false; |
969 | |
970 | QSvgVisitor::traverse(node: link); |
971 | |
972 | return false; |
973 | } |
974 | |
975 | void QSvgVisitorImpl::visitSwitchNodeEnd(const QSvgSwitch *node) |
976 | { |
977 | Q_UNUSED(node); |
978 | } |
979 | |
980 | bool QSvgVisitorImpl::visitDefsNodeStart(const QSvgDefs *node) |
981 | { |
982 | Q_UNUSED(node) |
983 | |
984 | return m_generator->generateDefsNode(info: NodeInfo{}); |
985 | } |
986 | |
987 | bool QSvgVisitorImpl::visitStructureNodeStart(const QSvgStructureNode *node) |
988 | { |
989 | constexpr bool forceSeparatePaths = false; |
990 | handleBaseNodeSetup(node); |
991 | |
992 | StructureNodeInfo info; |
993 | |
994 | fillCommonNodeInfo(node, info); |
995 | info.forceSeparatePaths = forceSeparatePaths; |
996 | info.isPathContainer = isPathContainer(node); |
997 | info.stage = StructureNodeStage::Start; |
998 | |
999 | return m_generator->generateStructureNode(info); |
1000 | } |
1001 | |
1002 | void QSvgVisitorImpl::visitStructureNodeEnd(const QSvgStructureNode *node) |
1003 | { |
1004 | handleBaseNodeEnd(node); |
1005 | // qCDebug(lcQuickVectorGraphics) << "REVERT" << node->nodeId() << node->type() << (m_styleResolver->painter().pen().style() != Qt::NoPen) << m_styleResolver->painter().pen().color().name() |
1006 | // << (m_styleResolver->painter().pen().brush().style() != Qt::NoBrush) << m_styleResolver->painter().pen().brush().color().name(); |
1007 | |
1008 | StructureNodeInfo info; |
1009 | fillCommonNodeInfo(node, info); |
1010 | info.stage = StructureNodeStage::End; |
1011 | |
1012 | m_generator->generateStructureNode(info); |
1013 | } |
1014 | |
1015 | bool QSvgVisitorImpl::visitDocumentNodeStart(const QSvgTinyDocument *node) |
1016 | { |
1017 | handleBaseNodeSetup(node); |
1018 | |
1019 | StructureNodeInfo info; |
1020 | fillCommonNodeInfo(node, info); |
1021 | |
1022 | const QSvgTinyDocument *doc = static_cast<const QSvgTinyDocument *>(node); |
1023 | info.size = doc->size(); |
1024 | info.viewBox = doc->viewBox(); |
1025 | info.isPathContainer = isPathContainer(node); |
1026 | info.forceSeparatePaths = false; |
1027 | info.stage = StructureNodeStage::Start; |
1028 | |
1029 | return m_generator->generateRootNode(info); |
1030 | } |
1031 | |
1032 | void QSvgVisitorImpl::visitDocumentNodeEnd(const QSvgTinyDocument *node) |
1033 | { |
1034 | handleBaseNodeEnd(node); |
1035 | qCDebug(lcQuickVectorImage) << "REVERT" << node->nodeId() << node->type() << (styleResolver->painter().pen().style() != Qt::NoPen) |
1036 | << styleResolver->painter().pen().color().name() << (styleResolver->painter().pen().brush().style() != Qt::NoBrush) |
1037 | << styleResolver->painter().pen().brush().color().name(); |
1038 | |
1039 | StructureNodeInfo info; |
1040 | fillCommonNodeInfo(node, info); |
1041 | info.stage = StructureNodeStage::End; |
1042 | |
1043 | m_generator->generateRootNode(info); |
1044 | } |
1045 | |
1046 | void QSvgVisitorImpl::fillCommonNodeInfo(const QSvgNode *node, NodeInfo &info) |
1047 | { |
1048 | info.nodeId = node->nodeId(); |
1049 | info.typeName = node->typeName(); |
1050 | info.isDefaultTransform = node->style().transform.isDefault(); |
1051 | info.transform = !info.isDefaultTransform ? node->style().transform->qtransform() : QTransform(); |
1052 | info.isDefaultOpacity = node->style().opacity.isDefault(); |
1053 | info.opacity = !info.isDefaultOpacity ? node->style().opacity->opacity() : 1.0; |
1054 | info.isVisible = node->isVisible(); |
1055 | info.isDisplayed = node->displayMode() != QSvgNode::DisplayMode::NoneMode; |
1056 | } |
1057 | |
1058 | void QSvgVisitorImpl::handleBaseNodeSetup(const QSvgNode *node) |
1059 | { |
1060 | qCDebug(lcQuickVectorImage) << "Before SETUP" << node << "fill" << styleResolver->currentFillColor() |
1061 | << "stroke" << styleResolver->currentStrokeColor() << styleResolver->currentStrokeWidth() |
1062 | << node->nodeId() << " type: " << node->typeName() << " " << node->type(); |
1063 | |
1064 | node->applyStyle(p: &styleResolver->painter(), states&: styleResolver->states()); |
1065 | |
1066 | qCDebug(lcQuickVectorImage) << "After SETUP" << node << "fill" << styleResolver->currentFillColor() |
1067 | << "stroke" << styleResolver->currentStrokeColor() |
1068 | << styleResolver->currentStrokeWidth() << node->nodeId(); |
1069 | } |
1070 | |
1071 | void QSvgVisitorImpl::handleBaseNode(const QSvgNode *node) |
1072 | { |
1073 | NodeInfo info; |
1074 | fillCommonNodeInfo(node, info); |
1075 | |
1076 | m_generator->generateNodeBase(info); |
1077 | } |
1078 | |
1079 | void QSvgVisitorImpl::handleBaseNodeEnd(const QSvgNode *node) |
1080 | { |
1081 | node->revertStyle(p: &styleResolver->painter(), states&: styleResolver->states()); |
1082 | |
1083 | qCDebug(lcQuickVectorImage) << "After END" << node << "fill" << styleResolver->currentFillColor() |
1084 | << "stroke" << styleResolver->currentStrokeColor() << styleResolver->currentStrokeWidth() |
1085 | << node->nodeId(); |
1086 | } |
1087 | |
1088 | void QSvgVisitorImpl::handlePathNode(const QSvgNode *node, const QPainterPath &path) |
1089 | { |
1090 | handleBaseNodeSetup(node); |
1091 | |
1092 | PathNodeInfo info; |
1093 | fillCommonNodeInfo(node, info); |
1094 | auto fillStyle = node->style().fill; |
1095 | if (fillStyle) |
1096 | info.fillRule = fillStyle->fillRule(); |
1097 | |
1098 | const QGradient *strokeGradient = styleResolver->currentStrokeGradient(); |
1099 | |
1100 | info.painterPath = path; |
1101 | info.fillColor = styleResolver->currentFillColor(); |
1102 | if (strokeGradient == nullptr) { |
1103 | info.strokeStyle = StrokeStyle::fromPen(p: styleResolver->currentStroke()); |
1104 | info.strokeStyle.color = styleResolver->currentStrokeColor(); |
1105 | } |
1106 | if (styleResolver->currentFillGradient() != nullptr) |
1107 | info.grad = styleResolver->applyOpacityToGradient(gradient: *styleResolver->currentFillGradient(), opacity: styleResolver->currentFillOpacity()); |
1108 | info.fillTransform = styleResolver->currentFillTransform(); |
1109 | |
1110 | m_generator->generatePath(info); |
1111 | |
1112 | if (strokeGradient != nullptr) { |
1113 | PathNodeInfo strokeInfo; |
1114 | fillCommonNodeInfo(node, info&: strokeInfo); |
1115 | |
1116 | strokeInfo.grad = *strokeGradient; |
1117 | |
1118 | QPainterPathStroker stroker(styleResolver->currentStroke()); |
1119 | strokeInfo.painterPath = stroker.createStroke(path); |
1120 | m_generator->generatePath(info: strokeInfo); |
1121 | } |
1122 | |
1123 | handleBaseNodeEnd(node); |
1124 | } |
1125 | |
1126 | QT_END_NAMESPACE |
1127 | |