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