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