Revision control

Copy as Markdown

// Copyright (C) 2016 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
#include "qsvgdocument_p.h"
#include "qsvghandler_p.h"
#include "qsvgfont_p.h"
#include "qpainter.h"
#include "qfile.h"
#include "qbuffer.h"
#include "qbytearray.h"
#include "qstack.h"
#include "qtransform.h"
#include "qdebug.h"
#ifndef QT_NO_COMPRESS
#include <zlib.h>
#endif
QT_BEGIN_NAMESPACE
using namespace Qt::StringLiterals;
QSvgDocument::QSvgDocument(QtSvg::Options options, QtSvg::AnimatorType type)
: QSvgStructureNode(0)
, m_states(this)
, m_options(options)
{
m_states.trustedSource = m_options.testFlag(QtSvg::AssumeTrustedSource);
bool animationEnabled = !m_options.testFlag(QtSvg::DisableAnimations);
switch (type) {
case QtSvg::AnimatorType::Automatic:
if (animationEnabled)
m_animator.reset(new QSvgAnimator);
break;
case QtSvg::AnimatorType::Controlled:
if (animationEnabled)
m_animator.reset(new QSvgAnimationController);
}
}
QSvgDocument::~QSvgDocument()
{
// Only do that when AssumeTrustedSource is set to false. Otherwise, all nodes
// will be deleted by recursive calls of destructors.
if (!m_states.trustedSource)
releaseDescendants();
}
static bool hasSvgHeader(const QByteArray &buf)
{
QTextStream s(buf); // Handle multi-byte encodings
QString h = s.readAll();
QStringView th = QStringView(h).trimmed();
bool matched = false;
if (th.startsWith("<svg"_L1) || th.startsWith("<!DOCTYPE svg"_L1))
matched = true;
else if (th.startsWith("<?xml"_L1) || th.startsWith("<!--"_L1))
matched = th.contains("<!DOCTYPE svg"_L1) || th.contains("<svg"_L1);
return matched;
}
#ifndef QT_NO_COMPRESS
static QByteArray qt_inflateSvgzDataFrom(QIODevice *device, bool doCheckContent = true);
# ifdef QT_BUILD_INTERNAL
Q_AUTOTEST_EXPORT QByteArray qt_inflateGZipDataFrom(QIODevice *device)
{
return qt_inflateSvgzDataFrom(device, false); // autotest wants unchecked result
}
# endif
static QByteArray qt_inflateSvgzDataFrom(QIODevice *device, bool doCheckContent)
{
if (!device)
return QByteArray();
if (!device->isOpen())
device->open(QIODevice::ReadOnly);
Q_ASSERT(device->isOpen() && device->isReadable());
static const int CHUNK_SIZE = 4096;
int zlibResult = Z_OK;
QByteArray source;
QByteArray destination;
// Initialize zlib stream struct
z_stream zlibStream;
zlibStream.next_in = Z_NULL;
zlibStream.avail_in = 0;
zlibStream.avail_out = 0;
zlibStream.zalloc = Z_NULL;
zlibStream.zfree = Z_NULL;
zlibStream.opaque = Z_NULL;
// Adding 16 to the window size gives us gzip decoding
if (inflateInit2(&zlibStream, MAX_WBITS + 16) != Z_OK) {
qCWarning(lcSvgHandler, "Cannot initialize zlib, because: %s",
(zlibStream.msg != NULL ? zlibStream.msg : "Unknown error"));
return QByteArray();
}
bool stillMoreWorkToDo = true;
while (stillMoreWorkToDo) {
if (!zlibStream.avail_in) {
source = device->read(CHUNK_SIZE);
if (source.isEmpty())
break;
zlibStream.avail_in = source.size();
zlibStream.next_in = reinterpret_cast<Bytef*>(source.data());
}
do {
// Prepare the destination buffer
int oldSize = destination.size();
if (oldSize > INT_MAX - CHUNK_SIZE) {
inflateEnd(&zlibStream);
qCWarning(lcSvgHandler, "Error while inflating gzip file: integer size overflow");
return QByteArray();
}
destination.resize(oldSize + CHUNK_SIZE);
zlibStream.next_out = reinterpret_cast<Bytef*>(
destination.data() + oldSize - zlibStream.avail_out);
zlibStream.avail_out += CHUNK_SIZE;
zlibResult = inflate(&zlibStream, Z_NO_FLUSH);
switch (zlibResult) {
case Z_NEED_DICT:
case Z_DATA_ERROR:
case Z_STREAM_ERROR:
case Z_MEM_ERROR: {
inflateEnd(&zlibStream);
qCWarning(lcSvgHandler, "Error while inflating gzip file: %s",
(zlibStream.msg != NULL ? zlibStream.msg : "Unknown error"));
return QByteArray();
}
}
// If the output buffer still has more room after calling inflate
// it means we have to provide more data, so exit the loop here
} while (!zlibStream.avail_out);
if (doCheckContent) {
// Quick format check, equivalent to QSvgIOHandler::canRead()
const qsizetype destinationContents = std::min(destination.size(), static_cast<qsizetype>(zlibStream.total_out));
Q_ASSERT(destinationContents == static_cast<qsizetype>(zlibStream.total_out));
if (!hasSvgHeader(QByteArray::fromRawData(destination.constData(), destinationContents))) {
inflateEnd(&zlibStream);
qCWarning(lcSvgHandler, "Error while inflating gzip file: SVG format check failed");
return QByteArray();
}
doCheckContent = false; // Run only once, on first chunk
}
if (zlibResult == Z_STREAM_END) {
// Make sure there are no more members to process before exiting
if (!(zlibStream.avail_in && inflateReset(&zlibStream) == Z_OK))
stillMoreWorkToDo = false;
}
}
// Chop off trailing space in the buffer
destination.chop(zlibStream.avail_out);
inflateEnd(&zlibStream);
return destination;
}
#else
static QByteArray qt_inflateSvgzDataFrom(QIODevice *)
{
return QByteArray();
}
#endif
std::unique_ptr<QSvgDocument> QSvgDocument::load(const QString &fileName, QtSvg::Options options,
QtSvg::AnimatorType type)
{
std::unique_ptr<QSvgDocument> doc;
QFile file(fileName);
if (!file.open(QFile::ReadOnly)) {
qCWarning(lcSvgHandler, "Cannot open file '%s', because: %s",
qPrintable(fileName), qPrintable(file.errorString()));
return doc;
}
if (fileName.endsWith(QLatin1String(".svgz"), Qt::CaseInsensitive)
|| fileName.endsWith(QLatin1String(".svg.gz"), Qt::CaseInsensitive)) {
return load(qt_inflateSvgzDataFrom(&file));
}
QSvgHandler handler(&file, options, type);
if (handler.ok()) {
doc = handler.takeDocument();
if (doc->m_animator)
doc->m_animator->setAnimationDuration(handler.animationDuration());
} else {
qCWarning(lcSvgHandler, "Cannot read file '%s', because: %s (line %d)",
qPrintable(fileName), qPrintable(handler.errorString()), handler.lineNumber());
}
return doc;
}
std::unique_ptr<QSvgDocument> QSvgDocument::load(const QByteArray &contents, QtSvg::Options options,
QtSvg::AnimatorType type)
{
std::unique_ptr<QSvgDocument> doc;
QByteArray svg;
// Check for gzip magic number and inflate if appropriate
if (contents.startsWith("\x1f\x8b")) {
QBuffer buffer;
buffer.setData(contents);
svg = qt_inflateSvgzDataFrom(&buffer);
} else {
svg = contents;
}
if (svg.isNull())
return doc;
QBuffer buffer;
buffer.setData(svg);
buffer.open(QIODevice::ReadOnly);
QSvgHandler handler(&buffer, options, type);
if (handler.ok()) {
doc = handler.takeDocument();
if (doc->m_animator)
doc->m_animator->setAnimationDuration(handler.animationDuration());
}
return doc;
}
std::unique_ptr<QSvgDocument> QSvgDocument::load(QXmlStreamReader *contents, QtSvg::Options options,
QtSvg::AnimatorType type)
{
QSvgHandler handler(contents, options, type);
std::unique_ptr<QSvgDocument> doc;
if (handler.ok()) {
doc = handler.takeDocument();
if (doc->m_animator)
doc->m_animator->setAnimationDuration(handler.animationDuration());
}
return doc;
}
void QSvgDocument::draw(QPainter *p, const QRectF &bounds)
{
if (displayMode() == QSvgNode::NoneMode)
return;
p->save();
//sets default style on the painter
//### not the most optimal way
mapSourceToTarget(p, bounds);
initPainter(p);
applyStyle(p, m_states);
for (const auto &node : renderers()) {
if ((node->isVisible()) && (node->displayMode() != QSvgNode::NoneMode))
node->draw(p, m_states);
}
revertStyle(p, m_states);
p->restore();
}
void QSvgDocument::draw(QPainter *p, const QString &id,
const QRectF &bounds)
{
QSvgNode *node = namedNode(id);
if (!node) {
qCDebug(lcSvgHandler, "Couldn't find node %s. Skipping rendering.", qPrintable(id));
return;
}
if (node->displayMode() == QSvgNode::NoneMode)
return;
p->save();
const QRectF elementBounds = node->bounds(this);
mapSourceToTarget(p, bounds, elementBounds);
QTransform originalTransform = p->worldTransform();
//XXX set default style on the painter
QPen pen(Qt::NoBrush, 1, Qt::SolidLine, Qt::FlatCap, Qt::SvgMiterJoin);
pen.setMiterLimit(4);
p->setPen(pen);
p->setBrush(Qt::black);
p->setRenderHint(QPainter::Antialiasing);
p->setRenderHint(QPainter::SmoothPixmapTransform);
QStack<QSvgNode*> parentApplyStack;
QSvgNode *parent = node->parent();
while (parent) {
parentApplyStack.push(parent);
parent = parent->parent();
}
for (int i = parentApplyStack.size() - 1; i >= 0; --i)
parentApplyStack[i]->applyStyle(p, m_states);
// Reset the world transform so that our parents don't affect
// the position
QTransform currentTransform = p->worldTransform();
p->setWorldTransform(originalTransform);
node->draw(p, m_states);
p->setWorldTransform(currentTransform);
for (int i = 0; i < parentApplyStack.size(); ++i)
parentApplyStack[i]->revertStyle(p, m_states);
//p->fillRect(bounds.adjusted(-5, -5, 5, 5), QColor(0, 0, 255, 100));
p->restore();
}
QSvgNode::Type QSvgDocument::type() const
{
return Doc;
}
void QSvgDocument::setWidth(int len, bool percent)
{
m_size.setWidth(len);
m_widthPercent = percent;
}
void QSvgDocument::setHeight(int len, bool percent)
{
m_size.setHeight(len);
m_heightPercent = percent;
}
void QSvgDocument::setPreserveAspectRatio(bool on)
{
m_preserveAspectRatio = on;
}
void QSvgDocument::setViewBox(const QRectF &rect)
{
m_viewBox = rect;
m_implicitViewBox = rect.isNull();
}
QtSvg::Options QSvgDocument::options() const
{
return m_options;
}
void QSvgDocument::addSvgFont(const QString &name, QSvgFontPtr font)
{
if (name.isEmpty())
return;
if (!m_fonts.insert({ name, std::move(font) }).second)
qCWarning(lcSvgHandler) << "Font Family already added: " << name;
}
QSvgFont *QSvgDocument::svgFont(const QString &family) const
{
auto it = m_fonts.find(family);
return it != m_fonts.end() ? it->second.get() : nullptr;
}
void QSvgDocument::addNamedNode(const QString &id, QSvgNode *node)
{
m_namedNodes.insert(id, node);
}
QSvgNode *QSvgDocument::namedNode(const QString &id) const
{
return m_namedNodes.value(id);
}
void QSvgDocument::addPaintServer(QSvgPaintServerSharedPtr paintServer, const QString &id)
{
if (id.isEmpty())
return;
if (!m_paintServers.insert({ id, paintServer }).second)
qCWarning(lcSvgHandler) << "Duplicate unique style id:" << id;
}
QSvgPaintServerSharedPtr QSvgDocument::paintServer(QStringView id) const
{
auto it = m_paintServers.find(id.toString());
return it != m_paintServers.end() ? it->second : nullptr;
}
void QSvgDocument::restartAnimation()
{
if (m_animator)
m_animator->restartAnimation();
}
bool QSvgDocument::animated() const
{
return m_animated;
}
void QSvgDocument::setAnimated(bool a)
{
m_animated = a;
}
void QSvgDocument::draw(QPainter *p)
{
draw(p, QRectF());
}
void QSvgDocument::drawCommand(QPainter *, QSvgExtraStates &)
{
qCDebug(lcSvgHandler) << "SVG Tiny does not support nested <svg> elements: ignored.";
return;
}
static bool isValidMatrix(const QTransform &transform)
{
qreal determinant = transform.determinant();
return qIsFinite(determinant);
}
void QSvgDocument::mapSourceToTarget(QPainter *p, const QRectF &targetRect, const QRectF &sourceRect)
{
QTransform oldTransform = p->worldTransform();
QRectF target = targetRect;
if (target.isEmpty()) {
QPaintDevice *dev = p->device();
QRectF deviceRect(0, 0, dev->width(), dev->height());
if (deviceRect.isEmpty()) {
if (sourceRect.isEmpty())
target = QRectF(QPointF(0, 0), size());
else
target = QRectF(QPointF(0, 0), sourceRect.size());
} else {
target = deviceRect;
}
}
QRectF source = sourceRect;
if (source.isEmpty())
source = viewBox();
if (source != target && !qFuzzyIsNull(source.width()) && !qFuzzyIsNull(source.height())) {
if (m_implicitViewBox || !preserveAspectRatio()) {
// Code path used when no view box is set, or IgnoreAspectRatio requested
QTransform transform;
transform.scale(target.width() / source.width(),
target.height() / source.height());
QRectF c2 = transform.mapRect(source);
p->translate(target.x() - c2.x(),
target.y() - c2.y());
p->scale(target.width() / source.width(),
target.height() / source.height());
} else {
// Code path used when KeepAspectRatio is requested. This attempts to emulate the default values
// of the <preserveAspectRatio tag that's implicitly defined when <viewbox> is used.
// Scale the view box into the view port (target) by preserve the aspect ratio.
QSizeF viewBoxSize = source.size();
viewBoxSize.scale(target.width(), target.height(), Qt::KeepAspectRatio);
// Center the view box in the view port
p->translate(target.x() + (target.width() - viewBoxSize.width()) / 2,
target.y() + (target.height() - viewBoxSize.height()) / 2);
p->scale(viewBoxSize.width() / source.width(),
viewBoxSize.height() / source.height());
// Apply the view box translation if specified.
p->translate(-source.x(), -source.y());
}
}
if (!isValidMatrix(p->worldTransform()))
p->setWorldTransform(oldTransform);
}
QRectF QSvgDocument::boundsOnElement(const QString &id) const
{
const QSvgNode *node = namedNode(id);
if (!node)
node = this;
return node->bounds(this);
}
bool QSvgDocument::elementExists(const QString &id) const
{
return bool(namedNode(id));
}
QTransform QSvgDocument::transformForElement(const QString &id) const
{
QSvgNode *node = namedNode(id);
if (!node) {
qCDebug(lcSvgHandler, "Couldn't find node %s. Skipping rendering.", qPrintable(id));
return QTransform();
}
QTransform t;
node = node->parent();
while (node) {
if (auto prop = node->m_style.property(QSvgStyleProperty::Transform))
t *= static_cast<QSvgTransformStyle*>(prop)->qtransform();
node = node->parent();
}
return t;
}
int QSvgDocument::currentFrame() const
{
const double runningPercentage = qMin(currentElapsed() / double(animationDuration()), 1.);
const int totalFrames = m_fps * animationDuration() / 1000;
return int(runningPercentage * totalFrames);
}
void QSvgDocument::setCurrentFrame(int frame)
{
if (!m_animator)
return;
const int totalFrames = m_fps * animationDuration() / 1000;
if (totalFrames == 0)
return;
const int timeForFrame = frame * animationDuration() / totalFrames; //in ms
const int timeToAdd = timeForFrame - currentElapsed();
m_animator->setAnimatorTime(timeToAdd);
}
void QSvgDocument::setFramesPerSecond(int num)
{
m_fps = num;
}
QSharedPointer<QSvgAbstractAnimator> QSvgDocument::animator() const
{
return m_animator;
}
bool QSvgDocument::isLikelySvg(QIODevice *device, bool *isCompressed)
{
constexpr int bufSize = 4096;
char buf[bufSize];
char inflateBuf[bufSize];
bool useInflateBuf = false;
int readLen = device->peek(buf, bufSize);
if (readLen < 8)
return false;
#ifndef QT_NO_COMPRESS
if (quint8(buf[0]) == 0x1f && quint8(buf[1]) == 0x8b) {
// Indicates gzip compressed content, i.e. svgz
z_stream zlibStream;
zlibStream.avail_in = readLen;
zlibStream.next_out = reinterpret_cast<Bytef *>(inflateBuf);
zlibStream.avail_out = bufSize;
zlibStream.next_in = reinterpret_cast<Bytef *>(buf);
zlibStream.zalloc = Z_NULL;
zlibStream.zfree = Z_NULL;
zlibStream.opaque = Z_NULL;
if (inflateInit2(&zlibStream, MAX_WBITS + 16) != Z_OK)
return false;
int zlibResult = inflate(&zlibStream, Z_NO_FLUSH);
inflateEnd(&zlibStream);
if ((zlibResult != Z_OK && zlibResult != Z_STREAM_END) || zlibStream.total_out < 8)
return false;
readLen = zlibStream.total_out;
if (isCompressed)
*isCompressed = true;
useInflateBuf = true;
}
#endif
return hasSvgHeader(QByteArray::fromRawData(useInflateBuf ? inflateBuf : buf, readLen));
}
QT_END_NAMESPACE