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

Provided by KDAB

Privacy Policy
Learn to use CMake with our Intro Training
Find out more

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