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

source code of qtsvg/src/svg/qsvgtinydocument.cpp