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