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

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