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
qsvgtinydocument.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 "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(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
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("<svg"_L1) || th.startsWith("<!DOCTYPE svg"_L1))
57 matched = true;
58 else if (th.startsWith("<?xml"_L1) || th.startsWith("<!--"_L1))
59 matched = th.contains("<!DOCTYPE svg"_L1) || th.contains("<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, 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(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(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(&zlibStream);
122 qCWarning(lcSvgHandler, "Error while inflating gzip file: integer size overflow");
123 return QByteArray();
124 }
125
126 destination.resize(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(&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(&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 const qsizetype destinationContents = std::min(destination.size(), static_cast<qsizetype>(zlibStream.total_out));
151 Q_ASSERT(destinationContents == static_cast<qsizetype>(zlibStream.total_out));
152 if (!hasSvgHeader(QByteArray::fromRawData(destination.constData(), destinationContents))) {
153 inflateEnd(&zlibStream);
154 qCWarning(lcSvgHandler, "Error while inflating gzip file: SVG format check failed");
155 return QByteArray();
156 }
157 doCheckContent = false; // Run only once, on first chunk
158 }
159
160 if (zlibResult == Z_STREAM_END) {
161 // Make sure there are no more members to process before exiting
162 if (!(zlibStream.avail_in && inflateReset(&zlibStream) == Z_OK))
163 stillMoreWorkToDo = false;
164 }
165 }
166
167 // Chop off trailing space in the buffer
168 destination.chop(zlibStream.avail_out);
169
170 inflateEnd(&zlibStream);
171 return destination;
172}
173#else
174static QByteArray qt_inflateSvgzDataFrom(QIODevice *)
175{
176 return QByteArray();
177}
178#endif
179
180QSvgTinyDocument *QSvgTinyDocument::load(const QString &fileName, QtSvg::Options options,
181 QtSvg::AnimatorType type)
182{
183 QFile file(fileName);
184 if (!file.open(QFile::ReadOnly)) {
185 qCWarning(lcSvgHandler, "Cannot open file '%s', because: %s",
186 qPrintable(fileName), qPrintable(file.errorString()));
187 return 0;
188 }
189
190 if (fileName.endsWith(QLatin1String(".svgz"), Qt::CaseInsensitive)
191 || fileName.endsWith(QLatin1String(".svg.gz"), Qt::CaseInsensitive)) {
192 return load(qt_inflateSvgzDataFrom(&file));
193 }
194
195 QSvgTinyDocument *doc = nullptr;
196 QSvgHandler handler(&file, options, type);
197 if (handler.ok()) {
198 doc = handler.document();
199 doc->m_animator->setAnimationDuration(handler.animationDuration());
200 } else {
201 qCWarning(lcSvgHandler, "Cannot read file '%s', because: %s (line %d)",
202 qPrintable(fileName), qPrintable(handler.errorString()), handler.lineNumber());
203 delete handler.document();
204 }
205 return doc;
206}
207
208QSvgTinyDocument *QSvgTinyDocument::load(const QByteArray &contents, QtSvg::Options options,
209 QtSvg::AnimatorType type)
210{
211 QByteArray svg;
212 // Check for gzip magic number and inflate if appropriate
213 if (contents.startsWith("\x1f\x8b")) {
214 QBuffer buffer;
215 buffer.setData(contents);
216 svg = qt_inflateSvgzDataFrom(&buffer);
217 } else {
218 svg = contents;
219 }
220 if (svg.isNull())
221 return nullptr;
222
223 QBuffer buffer;
224 buffer.setData(svg);
225 buffer.open(QIODevice::ReadOnly);
226 QSvgHandler handler(&buffer, options, type);
227
228 QSvgTinyDocument *doc = nullptr;
229 if (handler.ok()) {
230 doc = handler.document();
231 doc->m_animator->setAnimationDuration(handler.animationDuration());
232 } else {
233 delete handler.document();
234 }
235 return doc;
236}
237
238QSvgTinyDocument *QSvgTinyDocument::load(QXmlStreamReader *contents, QtSvg::Options options,
239 QtSvg::AnimatorType type)
240{
241 QSvgHandler handler(contents, options, type);
242
243 QSvgTinyDocument *doc = nullptr;
244 if (handler.ok()) {
245 doc = handler.document();
246 doc->m_animator->setAnimationDuration(handler.animationDuration());
247 } else {
248 delete handler.document();
249 }
250 return doc;
251}
252
253void QSvgTinyDocument::draw(QPainter *p, const QRectF &bounds)
254{
255 if (displayMode() == QSvgNode::NoneMode)
256 return;
257
258 p->save();
259 //sets default style on the painter
260 //### not the most optimal way
261 mapSourceToTarget(p, bounds);
262 initPainter(p);
263 QList<QSvgNode*>::iterator itr = m_renderers.begin();
264 applyStyle(p, m_states);
265 while (itr != m_renderers.end()) {
266 QSvgNode *node = *itr;
267 if ((node->isVisible()) && (node->displayMode() != QSvgNode::NoneMode))
268 node->draw(p, m_states);
269 ++itr;
270 }
271 revertStyle(p, m_states);
272 p->restore();
273}
274
275
276void QSvgTinyDocument::draw(QPainter *p, const QString &id,
277 const QRectF &bounds)
278{
279 QSvgNode *node = scopeNode(id);
280
281 if (!node) {
282 qCDebug(lcSvgHandler, "Couldn't find node %s. Skipping rendering.", qPrintable(id));
283 return;
284 }
285
286 if (node->displayMode() == QSvgNode::NoneMode)
287 return;
288
289 p->save();
290
291 const QRectF elementBounds = node->bounds();
292
293 mapSourceToTarget(p, bounds, elementBounds);
294 QTransform originalTransform = p->worldTransform();
295
296 //XXX set default style on the painter
297 QPen pen(Qt::NoBrush, 1, Qt::SolidLine, Qt::FlatCap, Qt::SvgMiterJoin);
298 pen.setMiterLimit(4);
299 p->setPen(pen);
300 p->setBrush(Qt::black);
301 p->setRenderHint(QPainter::Antialiasing);
302 p->setRenderHint(QPainter::SmoothPixmapTransform);
303
304 QStack<QSvgNode*> parentApplyStack;
305 QSvgNode *parent = node->parent();
306 while (parent) {
307 parentApplyStack.push(parent);
308 parent = parent->parent();
309 }
310
311 for (int i = parentApplyStack.size() - 1; i >= 0; --i)
312 parentApplyStack[i]->applyStyle(p, m_states);
313
314 // Reset the world transform so that our parents don't affect
315 // the position
316 QTransform currentTransform = p->worldTransform();
317 p->setWorldTransform(originalTransform);
318
319 node->draw(p, m_states);
320
321 p->setWorldTransform(currentTransform);
322
323 for (int i = 0; i < parentApplyStack.size(); ++i)
324 parentApplyStack[i]->revertStyle(p, m_states);
325
326 //p->fillRect(bounds.adjusted(-5, -5, 5, 5), QColor(0, 0, 255, 100));
327
328 p->restore();
329}
330
331QSvgNode::Type QSvgTinyDocument::type() const
332{
333 return Doc;
334}
335
336void QSvgTinyDocument::setWidth(int len, bool percent)
337{
338 m_size.setWidth(len);
339 m_widthPercent = percent;
340}
341
342void QSvgTinyDocument::setHeight(int len, bool percent)
343{
344 m_size.setHeight(len);
345 m_heightPercent = percent;
346}
347
348void QSvgTinyDocument::setPreserveAspectRatio(bool on)
349{
350 m_preserveAspectRatio = on;
351}
352
353void QSvgTinyDocument::setViewBox(const QRectF &rect)
354{
355 m_viewBox = rect;
356 m_implicitViewBox = rect.isNull();
357}
358
359QtSvg::Options QSvgTinyDocument::options() const
360{
361 return m_options;
362}
363
364void QSvgTinyDocument::addSvgFont(QSvgFont *font)
365{
366 m_fonts.insert(font->familyName(), font);
367}
368
369QSvgFont * QSvgTinyDocument::svgFont(const QString &family) const
370{
371 return m_fonts[family];
372}
373
374void QSvgTinyDocument::addNamedNode(const QString &id, QSvgNode *node)
375{
376 m_namedNodes.insert(id, node);
377}
378
379QSvgNode *QSvgTinyDocument::namedNode(const QString &id) const
380{
381 return m_namedNodes.value(id);
382}
383
384void QSvgTinyDocument::addNamedStyle(const QString &id, QSvgPaintStyleProperty *style)
385{
386 if (!m_namedStyles.contains(id))
387 m_namedStyles.insert(id, style);
388 else
389 qCWarning(lcSvgHandler) << "Duplicate unique style id:" << id;
390}
391
392QSvgPaintStyleProperty *QSvgTinyDocument::namedStyle(const QString &id) const
393{
394 return m_namedStyles.value(id);
395}
396
397void QSvgTinyDocument::restartAnimation()
398{
399 m_animator->restartAnimation();
400}
401
402bool QSvgTinyDocument::animated() const
403{
404 return m_animated;
405}
406
407void QSvgTinyDocument::setAnimated(bool a)
408{
409 m_animated = a;
410}
411
412void QSvgTinyDocument::draw(QPainter *p)
413{
414 draw(p, QRectF());
415}
416
417void QSvgTinyDocument::drawCommand(QPainter *, QSvgExtraStates &)
418{
419 qCDebug(lcSvgHandler) << "SVG Tiny does not support nested <svg> elements: ignored.";
420 return;
421}
422
423static bool isValidMatrix(const QTransform &transform)
424{
425 qreal determinant = transform.determinant();
426 return qIsFinite(determinant);
427}
428
429void QSvgTinyDocument::mapSourceToTarget(QPainter *p, const QRectF &targetRect, const QRectF &sourceRect)
430{
431 QTransform oldTransform = p->worldTransform();
432
433 QRectF target = targetRect;
434 if (target.isEmpty()) {
435 QPaintDevice *dev = p->device();
436 QRectF deviceRect(0, 0, dev->width(), dev->height());
437 if (deviceRect.isEmpty()) {
438 if (sourceRect.isEmpty())
439 target = QRectF(QPointF(0, 0), size());
440 else
441 target = QRectF(QPointF(0, 0), sourceRect.size());
442 } else {
443 target = deviceRect;
444 }
445 }
446
447 QRectF source = sourceRect;
448 if (source.isEmpty())
449 source = viewBox();
450
451 if (source != target && !qFuzzyIsNull(source.width()) && !qFuzzyIsNull(source.height())) {
452 if (m_implicitViewBox || !preserveAspectRatio()) {
453 // Code path used when no view box is set, or IgnoreAspectRatio requested
454 QTransform transform;
455 transform.scale(target.width() / source.width(),
456 target.height() / source.height());
457 QRectF c2 = transform.mapRect(source);
458 p->translate(target.x() - c2.x(),
459 target.y() - c2.y());
460 p->scale(target.width() / source.width(),
461 target.height() / source.height());
462 } else {
463 // Code path used when KeepAspectRatio is requested. This attempts to emulate the default values
464 // of the <preserveAspectRatio tag that's implicitly defined when <viewbox> is used.
465
466 // Scale the view box into the view port (target) by preserve the aspect ratio.
467 QSizeF viewBoxSize = source.size();
468 viewBoxSize.scale(target.width(), target.height(), Qt::KeepAspectRatio);
469
470 // Center the view box in the view port
471 p->translate(target.x() + (target.width() - viewBoxSize.width()) / 2,
472 target.y() + (target.height() - viewBoxSize.height()) / 2);
473
474 p->scale(viewBoxSize.width() / source.width(),
475 viewBoxSize.height() / source.height());
476
477 // Apply the view box translation if specified.
478 p->translate(-source.x(), -source.y());
479 }
480 }
481
482 if (!isValidMatrix(p->worldTransform()))
483 p->setWorldTransform(oldTransform);
484}
485
486QRectF QSvgTinyDocument::boundsOnElement(const QString &id) const
487{
488 const QSvgNode *node = scopeNode(id);
489 if (!node)
490 node = this;
491 return node->bounds();
492}
493
494bool QSvgTinyDocument::elementExists(const QString &id) const
495{
496 QSvgNode *node = scopeNode(id);
497
498 return (node!=0);
499}
500
501QTransform QSvgTinyDocument::transformForElement(const QString &id) const
502{
503 QSvgNode *node = scopeNode(id);
504
505 if (!node) {
506 qCDebug(lcSvgHandler, "Couldn't find node %s. Skipping rendering.", qPrintable(id));
507 return QTransform();
508 }
509
510 QTransform t;
511
512 node = node->parent();
513 while (node) {
514 if (node->m_style.transform)
515 t *= node->m_style.transform->qtransform();
516 node = node->parent();
517 }
518
519 return t;
520}
521
522int QSvgTinyDocument::currentFrame() const
523{
524 const double runningPercentage = qMin(currentElapsed() / double(animationDuration()), 1.);
525 const int totalFrames = m_fps * animationDuration() / 1000;
526 return int(runningPercentage * totalFrames);
527}
528
529void QSvgTinyDocument::setCurrentFrame(int frame)
530{
531 const int totalFrames = m_fps * animationDuration() / 1000;
532 if (totalFrames == 0)
533 return;
534
535 const int timeForFrame = frame * animationDuration() / totalFrames; //in ms
536 const int timeToAdd = timeForFrame - currentElapsed();
537 m_animator->setAnimatorTime(timeToAdd);
538}
539
540void QSvgTinyDocument::setFramesPerSecond(int num)
541{
542 m_fps = num;
543}
544
545QSharedPointer<QSvgAbstractAnimator> QSvgTinyDocument::animator() const
546{
547 return m_animator;
548}
549
550bool QSvgTinyDocument::isLikelySvg(QIODevice *device, bool *isCompressed)
551{
552 constexpr int bufSize = 4096;
553 char buf[bufSize];
554 char inflateBuf[bufSize];
555 bool useInflateBuf = false;
556 int readLen = device->peek(buf, bufSize);
557 if (readLen < 8)
558 return false;
559#ifndef QT_NO_COMPRESS
560 if (quint8(buf[0]) == 0x1f && quint8(buf[1]) == 0x8b) {
561 // Indicates gzip compressed content, i.e. svgz
562 z_stream zlibStream;
563 zlibStream.avail_in = readLen;
564 zlibStream.next_out = reinterpret_cast<Bytef *>(inflateBuf);
565 zlibStream.avail_out = bufSize;
566 zlibStream.next_in = reinterpret_cast<Bytef *>(buf);
567 zlibStream.zalloc = Z_NULL;
568 zlibStream.zfree = Z_NULL;
569 zlibStream.opaque = Z_NULL;
570 if (inflateInit2(&zlibStream, MAX_WBITS + 16) != Z_OK)
571 return false;
572 int zlibResult = inflate(&zlibStream, Z_NO_FLUSH);
573 inflateEnd(&zlibStream);
574 if ((zlibResult != Z_OK && zlibResult != Z_STREAM_END) || zlibStream.total_out < 8)
575 return false;
576 readLen = zlibStream.total_out;
577 if (isCompressed)
578 *isCompressed = true;
579 useInflateBuf = true;
580 }
581#endif
582 return hasSvgHeader(QByteArray::fromRawData(useInflateBuf ? inflateBuf : buf, readLen));
583}
584
585QT_END_NAMESPACE
#define qPrintable(string)
Definition qstring.h:1685
static QByteArray qt_inflateSvgzDataFrom(QIODevice *device, bool doCheckContent=true)
static bool isValidMatrix(const QTransform &transform)
static bool hasSvgHeader(const QByteArray &buf)