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
qsvgdocument.cpp
Go to the documentation of this file.
1// Copyright (C) 2016 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
7#include "qsvgfont_p.h"
8
9#include "qpainter.h"
10#include "qfile.h"
11#include "qbuffer.h"
12#include "qbytearray.h"
13#include "qstack.h"
14#include "qtransform.h"
15#include "qdebug.h"
16
17#ifndef QT_NO_COMPRESS
18#include <zlib.h>
19#endif
20
21QT_BEGIN_NAMESPACE
22
23using namespace Qt::StringLiterals;
24
25QSvgDocument::QSvgDocument(QtSvg::Options options, QtSvg::AnimatorType type)
26 : QSvgStructureNode(0)
27 , m_widthPercent(false)
28 , m_heightPercent(false)
29 , m_animated(false)
30 , m_fps(30)
31 , m_states(this)
32 , m_options(options)
33{
34 m_states.trustedSource = m_options.testFlag(QtSvg::AssumeTrustedSource);
35 bool animationEnabled = !m_options.testFlag(QtSvg::DisableAnimations);
36 switch (type) {
37 case QtSvg::AnimatorType::Automatic:
38 if (animationEnabled)
39 m_animator.reset(new QSvgAnimator);
40 break;
41 case QtSvg::AnimatorType::Controlled:
42 if (animationEnabled)
43 m_animator.reset(new QSvgAnimationController);
44 }
45}
46
47QSvgDocument::~QSvgDocument()
48{
49 // Only do that when AssumeTrustedSource is set to false. Otherwise, all nodes
50 // will be deleted by recursive calls of destructors.
51 if (!m_states.trustedSource)
52 releaseDescendants();
53}
54
55static bool hasSvgHeader(const QByteArray &buf)
56{
57 QTextStream s(buf); // Handle multi-byte encodings
58 QString h = s.readAll();
59 QStringView th = QStringView(h).trimmed();
60 bool matched = false;
61 if (th.startsWith("<svg"_L1) || th.startsWith("<!DOCTYPE svg"_L1))
62 matched = true;
63 else if (th.startsWith("<?xml"_L1) || th.startsWith("<!--"_L1))
64 matched = th.contains("<!DOCTYPE svg"_L1) || th.contains("<svg"_L1);
65 return matched;
66}
67
68#ifndef QT_NO_COMPRESS
69static QByteArray qt_inflateSvgzDataFrom(QIODevice *device, bool doCheckContent = true);
70# ifdef QT_BUILD_INTERNAL
71Q_AUTOTEST_EXPORT QByteArray qt_inflateGZipDataFrom(QIODevice *device)
72{
73 return qt_inflateSvgzDataFrom(device, false); // autotest wants unchecked result
74}
75# endif
76
77static QByteArray qt_inflateSvgzDataFrom(QIODevice *device, bool doCheckContent)
78{
79 if (!device)
80 return QByteArray();
81
82 if (!device->isOpen())
83 device->open(QIODevice::ReadOnly);
84
85 Q_ASSERT(device->isOpen() && device->isReadable());
86
87 static const int CHUNK_SIZE = 4096;
88 int zlibResult = Z_OK;
89
90 QByteArray source;
91 QByteArray destination;
92
93 // Initialize zlib stream struct
94 z_stream zlibStream;
95 zlibStream.next_in = Z_NULL;
96 zlibStream.avail_in = 0;
97 zlibStream.avail_out = 0;
98 zlibStream.zalloc = Z_NULL;
99 zlibStream.zfree = Z_NULL;
100 zlibStream.opaque = Z_NULL;
101
102 // Adding 16 to the window size gives us gzip decoding
103 if (inflateInit2(&zlibStream, MAX_WBITS + 16) != Z_OK) {
104 qCWarning(lcSvgHandler, "Cannot initialize zlib, because: %s",
105 (zlibStream.msg != NULL ? zlibStream.msg : "Unknown error"));
106 return QByteArray();
107 }
108
109 bool stillMoreWorkToDo = true;
110 while (stillMoreWorkToDo) {
111
112 if (!zlibStream.avail_in) {
113 source = device->read(CHUNK_SIZE);
114
115 if (source.isEmpty())
116 break;
117
118 zlibStream.avail_in = source.size();
119 zlibStream.next_in = reinterpret_cast<Bytef*>(source.data());
120 }
121
122 do {
123 // Prepare the destination buffer
124 int oldSize = destination.size();
125 if (oldSize > INT_MAX - CHUNK_SIZE) {
126 inflateEnd(&zlibStream);
127 qCWarning(lcSvgHandler, "Error while inflating gzip file: integer size overflow");
128 return QByteArray();
129 }
130
131 destination.resize(oldSize + CHUNK_SIZE);
132 zlibStream.next_out = reinterpret_cast<Bytef*>(
133 destination.data() + oldSize - zlibStream.avail_out);
134 zlibStream.avail_out += CHUNK_SIZE;
135
136 zlibResult = inflate(&zlibStream, Z_NO_FLUSH);
137 switch (zlibResult) {
138 case Z_NEED_DICT:
139 case Z_DATA_ERROR:
140 case Z_STREAM_ERROR:
141 case Z_MEM_ERROR: {
142 inflateEnd(&zlibStream);
143 qCWarning(lcSvgHandler, "Error while inflating gzip file: %s",
144 (zlibStream.msg != NULL ? zlibStream.msg : "Unknown error"));
145 return QByteArray();
146 }
147 }
148
149 // If the output buffer still has more room after calling inflate
150 // it means we have to provide more data, so exit the loop here
151 } while (!zlibStream.avail_out);
152
153 if (doCheckContent) {
154 // Quick format check, equivalent to QSvgIOHandler::canRead()
155 const qsizetype destinationContents = std::min(destination.size(), static_cast<qsizetype>(zlibStream.total_out));
156 Q_ASSERT(destinationContents == static_cast<qsizetype>(zlibStream.total_out));
157 if (!hasSvgHeader(QByteArray::fromRawData(destination.constData(), destinationContents))) {
158 inflateEnd(&zlibStream);
159 qCWarning(lcSvgHandler, "Error while inflating gzip file: SVG format check failed");
160 return QByteArray();
161 }
162 doCheckContent = false; // Run only once, on first chunk
163 }
164
165 if (zlibResult == Z_STREAM_END) {
166 // Make sure there are no more members to process before exiting
167 if (!(zlibStream.avail_in && inflateReset(&zlibStream) == Z_OK))
168 stillMoreWorkToDo = false;
169 }
170 }
171
172 // Chop off trailing space in the buffer
173 destination.chop(zlibStream.avail_out);
174
175 inflateEnd(&zlibStream);
176 return destination;
177}
178#else
179static QByteArray qt_inflateSvgzDataFrom(QIODevice *)
180{
181 return QByteArray();
182}
183#endif
184
185std::unique_ptr<QSvgDocument> QSvgDocument::load(const QString &fileName, QtSvg::Options options,
186 QtSvg::AnimatorType type)
187{
188 std::unique_ptr<QSvgDocument> doc;
189 QFile file(fileName);
190 if (!file.open(QFile::ReadOnly)) {
191 qCWarning(lcSvgHandler, "Cannot open file '%s', because: %s",
192 qPrintable(fileName), qPrintable(file.errorString()));
193 return doc;
194 }
195
196 if (fileName.endsWith(QLatin1String(".svgz"), Qt::CaseInsensitive)
197 || fileName.endsWith(QLatin1String(".svg.gz"), Qt::CaseInsensitive)) {
198 return load(qt_inflateSvgzDataFrom(&file));
199 }
200
201 QSvgHandler handler(&file, options, type);
202 if (handler.ok()) {
203 doc = handler.takeDocument();
204 if (doc->m_animator)
205 doc->m_animator->setAnimationDuration(handler.animationDuration());
206 } else {
207 qCWarning(lcSvgHandler, "Cannot read file '%s', because: %s (line %d)",
208 qPrintable(fileName), qPrintable(handler.errorString()), handler.lineNumber());
209 }
210 return doc;
211}
212
213std::unique_ptr<QSvgDocument> QSvgDocument::load(const QByteArray &contents, QtSvg::Options options,
214 QtSvg::AnimatorType type)
215{
216 std::unique_ptr<QSvgDocument> doc;
217 QByteArray svg;
218 // Check for gzip magic number and inflate if appropriate
219 if (contents.startsWith("\x1f\x8b")) {
220 QBuffer buffer;
221 buffer.setData(contents);
222 svg = qt_inflateSvgzDataFrom(&buffer);
223 } else {
224 svg = contents;
225 }
226 if (svg.isNull())
227 return doc;
228
229 QBuffer buffer;
230 buffer.setData(svg);
231 buffer.open(QIODevice::ReadOnly);
232 QSvgHandler handler(&buffer, options, type);
233
234 if (handler.ok()) {
235 doc = handler.takeDocument();
236 if (doc->m_animator)
237 doc->m_animator->setAnimationDuration(handler.animationDuration());
238 }
239 return doc;
240}
241
242std::unique_ptr<QSvgDocument> QSvgDocument::load(QXmlStreamReader *contents, QtSvg::Options options,
243 QtSvg::AnimatorType type)
244{
245 QSvgHandler handler(contents, options, type);
246
247 std::unique_ptr<QSvgDocument> doc;
248 if (handler.ok()) {
249 doc = handler.takeDocument();
250 if (doc->m_animator)
251 doc->m_animator->setAnimationDuration(handler.animationDuration());
252 }
253 return doc;
254}
255
256void QSvgDocument::draw(QPainter *p, const QRectF &bounds)
257{
258 if (displayMode() == QSvgNode::NoneMode)
259 return;
260
261 p->save();
262 //sets default style on the painter
263 //### not the most optimal way
264 mapSourceToTarget(p, bounds);
265 initPainter(p);
266 applyStyle(p, m_states);
267 for (const auto &node : renderers()) {
268 if ((node->isVisible()) && (node->displayMode() != QSvgNode::NoneMode))
269 node->draw(p, m_states);
270 }
271 revertStyle(p, m_states);
272 p->restore();
273}
274
275
276void QSvgDocument::draw(QPainter *p, const QString &id,
277 const QRectF &bounds)
278{
279 QSvgNode *node = namedNode(id);
280
281 if (!node) {
282 qCDebug(lcSvgHandler, "Couldn't find node %s. Skipping rendering.", qPrintable(id));
283 return;
284 }
285
286 if (node->displayMode() == QSvgNode::NoneMode)
287 return;
288
289 p->save();
290
291 const QRectF elementBounds = node->bounds(this);
292
293 mapSourceToTarget(p, bounds, elementBounds);
294 QTransform originalTransform = p->worldTransform();
295
296 //XXX set default style on the painter
297 QPen pen(Qt::NoBrush, 1, Qt::SolidLine, Qt::FlatCap, Qt::SvgMiterJoin);
298 pen.setMiterLimit(4);
299 p->setPen(pen);
300 p->setBrush(Qt::black);
301 p->setRenderHint(QPainter::Antialiasing);
302 p->setRenderHint(QPainter::SmoothPixmapTransform);
303
304 QStack<QSvgNode*> parentApplyStack;
305 QSvgNode *parent = node->parent();
306 while (parent) {
307 parentApplyStack.push(parent);
308 parent = parent->parent();
309 }
310
311 for (int i = parentApplyStack.size() - 1; i >= 0; --i)
312 parentApplyStack[i]->applyStyle(p, m_states);
313
314 // Reset the world transform so that our parents don't affect
315 // the position
316 QTransform currentTransform = p->worldTransform();
317 p->setWorldTransform(originalTransform);
318
319 node->draw(p, m_states);
320
321 p->setWorldTransform(currentTransform);
322
323 for (int i = 0; i < parentApplyStack.size(); ++i)
324 parentApplyStack[i]->revertStyle(p, m_states);
325
326 //p->fillRect(bounds.adjusted(-5, -5, 5, 5), QColor(0, 0, 255, 100));
327
328 p->restore();
329}
330
331QSvgNode::Type QSvgDocument::type() const
332{
333 return Doc;
334}
335
336void QSvgDocument::setWidth(int len, bool percent)
337{
338 m_size.setWidth(len);
339 m_widthPercent = percent;
340}
341
342void QSvgDocument::setHeight(int len, bool percent)
343{
344 m_size.setHeight(len);
345 m_heightPercent = percent;
346}
347
348void QSvgDocument::setPreserveAspectRatio(bool on)
349{
350 m_preserveAspectRatio = on;
351}
352
353void QSvgDocument::setViewBox(const QRectF &rect)
354{
355 m_viewBox = rect;
356 m_implicitViewBox = rect.isNull();
357}
358
359QtSvg::Options QSvgDocument::options() const
360{
361 return m_options;
362}
363
364void QSvgDocument::addSvgFont(const QString &name, QSvgFontPtr font)
365{
366 if (name.isEmpty())
367 return;
368
369 if (!m_fonts.insert({ name, std::move(font) }).second)
370 qCWarning(lcSvgHandler) << "Font Family already added: " << name;
371}
372
373QSvgFont *QSvgDocument::svgFont(const QString &family) const
374{
375 auto it = m_fonts.find(family);
376 return it != m_fonts.end() ? it->second.get() : nullptr;
377}
378
379void QSvgDocument::addNamedNode(const QString &id, QSvgNode *node)
380{
381 m_namedNodes.insert(id, node);
382}
383
384QSvgNode *QSvgDocument::namedNode(const QString &id) const
385{
386 return m_namedNodes.value(id);
387}
388
389void QSvgDocument::addPaintServer(QSvgPaintServerSharedPtr paintServer, const QString &id)
390{
391 if (id.isEmpty())
392 return;
393
394 if (!m_paintServers.insert({ id, paintServer }).second)
395 qCWarning(lcSvgHandler) << "Duplicate unique style id:" << id;
396}
397
398QSvgPaintServerSharedPtr QSvgDocument::paintServer(QStringView id) const
399{
400 auto it = m_paintServers.find(id.toString());
401 return it != m_paintServers.end() ? it->second : nullptr;
402}
403
404void QSvgDocument::restartAnimation()
405{
406 if (m_animator)
407 m_animator->restartAnimation();
408}
409
410bool QSvgDocument::animated() const
411{
412 return m_animated;
413}
414
415void QSvgDocument::setAnimated(bool a)
416{
417 m_animated = a;
418}
419
420void QSvgDocument::draw(QPainter *p)
421{
422 draw(p, QRectF());
423}
424
425void QSvgDocument::drawCommand(QPainter *, QSvgExtraStates &)
426{
427 qCDebug(lcSvgHandler) << "SVG Tiny does not support nested <svg> elements: ignored.";
428 return;
429}
430
431static bool isValidMatrix(const QTransform &transform)
432{
433 qreal determinant = transform.determinant();
434 return qIsFinite(determinant);
435}
436
437void QSvgDocument::mapSourceToTarget(QPainter *p, const QRectF &targetRect, const QRectF &sourceRect)
438{
439 QTransform oldTransform = p->worldTransform();
440
441 QRectF target = targetRect;
442 if (target.isEmpty()) {
443 QPaintDevice *dev = p->device();
444 QRectF deviceRect(0, 0, dev->width(), dev->height());
445 if (deviceRect.isEmpty()) {
446 if (sourceRect.isEmpty())
447 target = QRectF(QPointF(0, 0), size());
448 else
449 target = QRectF(QPointF(0, 0), sourceRect.size());
450 } else {
451 target = deviceRect;
452 }
453 }
454
455 QRectF source = sourceRect;
456 if (source.isEmpty())
457 source = viewBox();
458
459 if (source != target && !qFuzzyIsNull(source.width()) && !qFuzzyIsNull(source.height())) {
460 if (m_implicitViewBox || !preserveAspectRatio()) {
461 // Code path used when no view box is set, or IgnoreAspectRatio requested
462 QTransform transform;
463 transform.scale(target.width() / source.width(),
464 target.height() / source.height());
465 QRectF c2 = transform.mapRect(source);
466 p->translate(target.x() - c2.x(),
467 target.y() - c2.y());
468 p->scale(target.width() / source.width(),
469 target.height() / source.height());
470 } else {
471 // Code path used when KeepAspectRatio is requested. This attempts to emulate the default values
472 // of the <preserveAspectRatio tag that's implicitly defined when <viewbox> is used.
473
474 // Scale the view box into the view port (target) by preserve the aspect ratio.
475 QSizeF viewBoxSize = source.size();
476 viewBoxSize.scale(target.width(), target.height(), Qt::KeepAspectRatio);
477
478 // Center the view box in the view port
479 p->translate(target.x() + (target.width() - viewBoxSize.width()) / 2,
480 target.y() + (target.height() - viewBoxSize.height()) / 2);
481
482 p->scale(viewBoxSize.width() / source.width(),
483 viewBoxSize.height() / source.height());
484
485 // Apply the view box translation if specified.
486 p->translate(-source.x(), -source.y());
487 }
488 }
489
490 if (!isValidMatrix(p->worldTransform()))
491 p->setWorldTransform(oldTransform);
492}
493
494QRectF QSvgDocument::boundsOnElement(const QString &id) const
495{
496 const QSvgNode *node = namedNode(id);
497 if (!node)
498 node = this;
499 return node->bounds(this);
500}
501
502bool QSvgDocument::elementExists(const QString &id) const
503{
504 return bool(namedNode(id));
505}
506
507QTransform QSvgDocument::transformForElement(const QString &id) const
508{
509 QSvgNode *node = namedNode(id);
510
511 if (!node) {
512 qCDebug(lcSvgHandler, "Couldn't find node %s. Skipping rendering.", qPrintable(id));
513 return QTransform();
514 }
515
516 QTransform t;
517
518 node = node->parent();
519 while (node) {
520 if (auto prop = node->m_style.property(QSvgStyleProperty::Transform))
521 t *= static_cast<QSvgTransformStyle*>(prop)->qtransform();
522 node = node->parent();
523 }
524
525 return t;
526}
527
528int QSvgDocument::currentFrame() const
529{
530 const double runningPercentage = qMin(currentElapsed() / double(animationDuration()), 1.);
531 const int totalFrames = m_fps * animationDuration() / 1000;
532 return int(runningPercentage * totalFrames);
533}
534
535void QSvgDocument::setCurrentFrame(int frame)
536{
537 if (!m_animator)
538 return;
539
540 const int totalFrames = m_fps * animationDuration() / 1000;
541 if (totalFrames == 0)
542 return;
543
544 const int timeForFrame = frame * animationDuration() / totalFrames; //in ms
545 const int timeToAdd = timeForFrame - currentElapsed();
546 m_animator->setAnimatorTime(timeToAdd);
547}
548
549void QSvgDocument::setFramesPerSecond(int num)
550{
551 m_fps = num;
552}
553
554QSharedPointer<QSvgAbstractAnimator> QSvgDocument::animator() const
555{
556 return m_animator;
557}
558
559bool QSvgDocument::isLikelySvg(QIODevice *device, bool *isCompressed)
560{
561 constexpr int bufSize = 4096;
562 char buf[bufSize];
563 char inflateBuf[bufSize];
564 bool useInflateBuf = false;
565 int readLen = device->peek(buf, bufSize);
566 if (readLen < 8)
567 return false;
568#ifndef QT_NO_COMPRESS
569 if (quint8(buf[0]) == 0x1f && quint8(buf[1]) == 0x8b) {
570 // Indicates gzip compressed content, i.e. svgz
571 z_stream zlibStream;
572 zlibStream.avail_in = readLen;
573 zlibStream.next_out = reinterpret_cast<Bytef *>(inflateBuf);
574 zlibStream.avail_out = bufSize;
575 zlibStream.next_in = reinterpret_cast<Bytef *>(buf);
576 zlibStream.zalloc = Z_NULL;
577 zlibStream.zfree = Z_NULL;
578 zlibStream.opaque = Z_NULL;
579 if (inflateInit2(&zlibStream, MAX_WBITS + 16) != Z_OK)
580 return false;
581 int zlibResult = inflate(&zlibStream, Z_NO_FLUSH);
582 inflateEnd(&zlibStream);
583 if ((zlibResult != Z_OK && zlibResult != Z_STREAM_END) || zlibStream.total_out < 8)
584 return false;
585 readLen = zlibStream.total_out;
586 if (isCompressed)
587 *isCompressed = true;
588 useInflateBuf = true;
589 }
590#endif
591 return hasSvgHeader(QByteArray::fromRawData(useInflateBuf ? inflateBuf : buf, readLen));
592}
593
594QT_END_NAMESPACE
#define qPrintable(string)
Definition qstring.h:1718
static QByteArray qt_inflateSvgzDataFrom(QIODevice *device, bool doCheckContent=true)
static bool isValidMatrix(const QTransform &transform)
static bool hasSvgHeader(const QByteArray &buf)