1 | /**************************************************************************** |
2 | ** |
3 | ** Copyright (C) 2014 Digia Plc and/or its subsidiary(-ies). |
4 | ** Contact: http://www.qt-project.org/legal |
5 | ** |
6 | ** This file is part of the QtSvg module of the Qt Toolkit. |
7 | ** |
8 | ** $QT_BEGIN_LICENSE:LGPL$ |
9 | ** Commercial License Usage |
10 | ** Licensees holding valid commercial Qt licenses may use this file in |
11 | ** accordance with the commercial license agreement provided with the |
12 | ** Software or, alternatively, in accordance with the terms contained in |
13 | ** a written agreement between you and Digia. For licensing terms and |
14 | ** conditions see http://qt.digia.com/licensing. For further information |
15 | ** use the contact form at http://qt.digia.com/contact-us. |
16 | ** |
17 | ** GNU Lesser General Public License Usage |
18 | ** Alternatively, this file may be used under the terms of the GNU Lesser |
19 | ** General Public License version 2.1 as published by the Free Software |
20 | ** Foundation and appearing in the file LICENSE.LGPL included in the |
21 | ** packaging of this file. Please review the following information to |
22 | ** ensure the GNU Lesser General Public License version 2.1 requirements |
23 | ** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. |
24 | ** |
25 | ** In addition, as a special exception, Digia gives you certain additional |
26 | ** rights. These rights are described in the Digia Qt LGPL Exception |
27 | ** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. |
28 | ** |
29 | ** GNU General Public License Usage |
30 | ** Alternatively, this file may be used under the terms of the GNU |
31 | ** General Public License version 3.0 as published by the Free Software |
32 | ** Foundation and appearing in the file LICENSE.GPL included in the |
33 | ** packaging of this file. Please review the following information to |
34 | ** ensure the GNU General Public License version 3.0 requirements will be |
35 | ** met: http://www.gnu.org/copyleft/gpl.html. |
36 | ** |
37 | ** |
38 | ** $QT_END_LICENSE$ |
39 | ** |
40 | ****************************************************************************/ |
41 | |
42 | #include "qsvgtinydocument_p.h" |
43 | |
44 | #ifndef QT_NO_SVG |
45 | |
46 | #include "qsvghandler_p.h" |
47 | #include "qsvgfont_p.h" |
48 | |
49 | #include "qpainter.h" |
50 | #include "qfile.h" |
51 | #include "qbuffer.h" |
52 | #include "qbytearray.h" |
53 | #include "qqueue.h" |
54 | #include "qstack.h" |
55 | #include "qdebug.h" |
56 | |
57 | #ifndef QT_NO_COMPRESS |
58 | #include <zlib.h> |
59 | #endif |
60 | |
61 | QT_BEGIN_NAMESPACE |
62 | |
63 | QSvgTinyDocument::QSvgTinyDocument() |
64 | : QSvgStructureNode(0) |
65 | , m_widthPercent(false) |
66 | , m_heightPercent(false) |
67 | , m_animated(false) |
68 | , m_animationDuration(0) |
69 | , m_fps(30) |
70 | { |
71 | } |
72 | |
73 | QSvgTinyDocument::~QSvgTinyDocument() |
74 | { |
75 | } |
76 | |
77 | #ifndef QT_NO_COMPRESS |
78 | # ifdef QT_BUILD_INTERNAL |
79 | Q_AUTOTEST_EXPORT QByteArray qt_inflateGZipDataFrom(QIODevice *device); |
80 | # else |
81 | static QByteArray qt_inflateGZipDataFrom(QIODevice *device); |
82 | # endif |
83 | |
84 | QByteArray qt_inflateGZipDataFrom(QIODevice *device) |
85 | { |
86 | if (!device) |
87 | return QByteArray(); |
88 | |
89 | if (!device->isOpen()) |
90 | device->open(QIODevice::ReadOnly); |
91 | |
92 | Q_ASSERT(device->isOpen() && device->isReadable()); |
93 | |
94 | static const int CHUNK_SIZE = 4096; |
95 | int zlibResult = Z_OK; |
96 | |
97 | QByteArray source; |
98 | QByteArray destination; |
99 | |
100 | // Initialize zlib stream struct |
101 | z_stream zlibStream; |
102 | zlibStream.next_in = Z_NULL; |
103 | zlibStream.avail_in = 0; |
104 | zlibStream.avail_out = 0; |
105 | zlibStream.zalloc = Z_NULL; |
106 | zlibStream.zfree = Z_NULL; |
107 | zlibStream.opaque = Z_NULL; |
108 | |
109 | // Adding 16 to the window size gives us gzip decoding |
110 | if (inflateInit2(&zlibStream, MAX_WBITS + 16) != Z_OK) { |
111 | qWarning("Cannot initialize zlib, because: %s" , |
112 | (zlibStream.msg != NULL ? zlibStream.msg : "Unknown error" )); |
113 | return QByteArray(); |
114 | } |
115 | |
116 | bool stillMoreWorkToDo = true; |
117 | while (stillMoreWorkToDo) { |
118 | |
119 | if (!zlibStream.avail_in) { |
120 | source = device->read(CHUNK_SIZE); |
121 | |
122 | if (source.isEmpty()) |
123 | break; |
124 | |
125 | zlibStream.avail_in = source.size(); |
126 | zlibStream.next_in = reinterpret_cast<Bytef*>(source.data()); |
127 | } |
128 | |
129 | do { |
130 | // Prepare the destination buffer |
131 | int oldSize = destination.size(); |
132 | destination.resize(oldSize + CHUNK_SIZE); |
133 | zlibStream.next_out = reinterpret_cast<Bytef*>( |
134 | destination.data() + oldSize - zlibStream.avail_out); |
135 | zlibStream.avail_out += CHUNK_SIZE; |
136 | |
137 | zlibResult = inflate(&zlibStream, Z_NO_FLUSH); |
138 | switch (zlibResult) { |
139 | case Z_NEED_DICT: |
140 | case Z_DATA_ERROR: |
141 | case Z_STREAM_ERROR: |
142 | case Z_MEM_ERROR: { |
143 | inflateEnd(&zlibStream); |
144 | qWarning("Error while inflating gzip file: %s" , |
145 | (zlibStream.msg != NULL ? zlibStream.msg : "Unknown error" )); |
146 | destination.chop(zlibStream.avail_out); |
147 | return destination; |
148 | } |
149 | } |
150 | |
151 | // If the output buffer still has more room after calling inflate |
152 | // it means we have to provide more data, so exit the loop here |
153 | } while (!zlibStream.avail_out); |
154 | |
155 | if (zlibResult == Z_STREAM_END) { |
156 | // Make sure there are no more members to process before exiting |
157 | if (!(zlibStream.avail_in && inflateReset(&zlibStream) == Z_OK)) |
158 | stillMoreWorkToDo = false; |
159 | } |
160 | } |
161 | |
162 | // Chop off trailing space in the buffer |
163 | destination.chop(zlibStream.avail_out); |
164 | |
165 | inflateEnd(&zlibStream); |
166 | return destination; |
167 | } |
168 | #endif |
169 | |
170 | QSvgTinyDocument * QSvgTinyDocument::load(const QString &fileName) |
171 | { |
172 | QFile file(fileName); |
173 | if (!file.open(QFile::ReadOnly)) { |
174 | qWarning("Cannot open file '%s', because: %s" , |
175 | qPrintable(fileName), qPrintable(file.errorString())); |
176 | return 0; |
177 | } |
178 | |
179 | #ifndef QT_NO_COMPRESS |
180 | if (fileName.endsWith(QLatin1String(".svgz" ), Qt::CaseInsensitive) |
181 | || fileName.endsWith(QLatin1String(".svg.gz" ), Qt::CaseInsensitive)) { |
182 | return load(qt_inflateGZipDataFrom(&file)); |
183 | } |
184 | #endif |
185 | |
186 | QSvgTinyDocument *doc = 0; |
187 | QSvgHandler handler(&file); |
188 | if (handler.ok()) { |
189 | doc = handler.document(); |
190 | doc->m_animationDuration = handler.animationDuration(); |
191 | } else { |
192 | qWarning("Cannot read file '%s', because: %s (line %d)" , |
193 | qPrintable(fileName), qPrintable(handler.errorString()), handler.lineNumber()); |
194 | } |
195 | return doc; |
196 | } |
197 | |
198 | QSvgTinyDocument * QSvgTinyDocument::load(const QByteArray &contents) |
199 | { |
200 | #ifndef QT_NO_COMPRESS |
201 | // Check for gzip magic number and inflate if appropriate |
202 | if (contents.startsWith("\x1f\x8b" )) { |
203 | QBuffer buffer(const_cast<QByteArray *>(&contents)); |
204 | return load(qt_inflateGZipDataFrom(&buffer)); |
205 | } |
206 | #endif |
207 | |
208 | QSvgHandler handler(contents); |
209 | |
210 | QSvgTinyDocument *doc = 0; |
211 | if (handler.ok()) { |
212 | doc = handler.document(); |
213 | doc->m_animationDuration = handler.animationDuration(); |
214 | } |
215 | return doc; |
216 | } |
217 | |
218 | QSvgTinyDocument * QSvgTinyDocument::load(QXmlStreamReader *contents) |
219 | { |
220 | QSvgHandler handler(contents); |
221 | |
222 | QSvgTinyDocument *doc = 0; |
223 | if (handler.ok()) { |
224 | doc = handler.document(); |
225 | doc->m_animationDuration = handler.animationDuration(); |
226 | } |
227 | return doc; |
228 | } |
229 | |
230 | void QSvgTinyDocument::draw(QPainter *p, const QRectF &bounds) |
231 | { |
232 | if (m_time.isNull()) { |
233 | m_time.start(); |
234 | } |
235 | |
236 | if (displayMode() == QSvgNode::NoneMode) |
237 | return; |
238 | |
239 | p->save(); |
240 | //sets default style on the painter |
241 | //### not the most optimal way |
242 | mapSourceToTarget(p, bounds); |
243 | QPen pen(Qt::NoBrush, 1, Qt::SolidLine, Qt::FlatCap, Qt::MiterJoin); |
244 | pen.setMiterLimit(4); |
245 | p->setPen(pen); |
246 | p->setBrush(Qt::black); |
247 | p->setRenderHint(QPainter::Antialiasing); |
248 | p->setRenderHint(QPainter::SmoothPixmapTransform); |
249 | QList<QSvgNode*>::iterator itr = m_renderers.begin(); |
250 | applyStyle(p, m_states); |
251 | while (itr != m_renderers.end()) { |
252 | QSvgNode *node = *itr; |
253 | if ((node->isVisible()) && (node->displayMode() != QSvgNode::NoneMode)) |
254 | node->draw(p, m_states); |
255 | ++itr; |
256 | } |
257 | revertStyle(p, m_states); |
258 | p->restore(); |
259 | } |
260 | |
261 | |
262 | void QSvgTinyDocument::draw(QPainter *p, const QString &id, |
263 | const QRectF &bounds) |
264 | { |
265 | QSvgNode *node = scopeNode(id); |
266 | |
267 | if (!node) { |
268 | qDebug("Couldn't find node %s. Skipping rendering." , qPrintable(id)); |
269 | return; |
270 | } |
271 | if (m_time.isNull()) { |
272 | m_time.start(); |
273 | } |
274 | |
275 | if (node->displayMode() == QSvgNode::NoneMode) |
276 | return; |
277 | |
278 | p->save(); |
279 | |
280 | const QRectF elementBounds = node->transformedBounds(); |
281 | |
282 | mapSourceToTarget(p, bounds, elementBounds); |
283 | QTransform originalTransform = p->worldTransform(); |
284 | |
285 | //XXX set default style on the painter |
286 | QPen pen(Qt::NoBrush, 1, Qt::SolidLine, Qt::FlatCap, Qt::MiterJoin); |
287 | pen.setMiterLimit(4); |
288 | p->setPen(pen); |
289 | p->setBrush(Qt::black); |
290 | p->setRenderHint(QPainter::Antialiasing); |
291 | p->setRenderHint(QPainter::SmoothPixmapTransform); |
292 | |
293 | QStack<QSvgNode*> parentApplyStack; |
294 | QSvgNode *parent = node->parent(); |
295 | while (parent) { |
296 | parentApplyStack.push(parent); |
297 | parent = parent->parent(); |
298 | } |
299 | |
300 | for (int i = parentApplyStack.size() - 1; i >= 0; --i) |
301 | parentApplyStack[i]->applyStyle(p, m_states); |
302 | |
303 | // Reset the world transform so that our parents don't affect |
304 | // the position |
305 | QTransform currentTransform = p->worldTransform(); |
306 | p->setWorldTransform(originalTransform); |
307 | |
308 | node->draw(p, m_states); |
309 | |
310 | p->setWorldTransform(currentTransform); |
311 | |
312 | for (int i = 0; i < parentApplyStack.size(); ++i) |
313 | parentApplyStack[i]->revertStyle(p, m_states); |
314 | |
315 | //p->fillRect(bounds.adjusted(-5, -5, 5, 5), QColor(0, 0, 255, 100)); |
316 | |
317 | p->restore(); |
318 | } |
319 | |
320 | |
321 | QSvgNode::Type QSvgTinyDocument::type() const |
322 | { |
323 | return DOC; |
324 | } |
325 | |
326 | void QSvgTinyDocument::setWidth(int len, bool percent) |
327 | { |
328 | m_size.setWidth(len); |
329 | m_widthPercent = percent; |
330 | } |
331 | |
332 | void QSvgTinyDocument::setHeight(int len, bool percent) |
333 | { |
334 | m_size.setHeight(len); |
335 | m_heightPercent = percent; |
336 | } |
337 | |
338 | void QSvgTinyDocument::setViewBox(const QRectF &rect) |
339 | { |
340 | m_viewBox = rect; |
341 | } |
342 | |
343 | void QSvgTinyDocument::addSvgFont(QSvgFont *font) |
344 | { |
345 | m_fonts.insert(font->familyName(), font); |
346 | } |
347 | |
348 | QSvgFont * QSvgTinyDocument::svgFont(const QString &family) const |
349 | { |
350 | return m_fonts[family]; |
351 | } |
352 | |
353 | void QSvgTinyDocument::addNamedNode(const QString &id, QSvgNode *node) |
354 | { |
355 | m_namedNodes.insert(id, node); |
356 | } |
357 | |
358 | QSvgNode *QSvgTinyDocument::namedNode(const QString &id) const |
359 | { |
360 | return m_namedNodes.value(id); |
361 | } |
362 | |
363 | void QSvgTinyDocument::addNamedStyle(const QString &id, QSvgFillStyleProperty *style) |
364 | { |
365 | m_namedStyles.insert(id, style); |
366 | } |
367 | |
368 | QSvgFillStyleProperty *QSvgTinyDocument::namedStyle(const QString &id) const |
369 | { |
370 | return m_namedStyles.value(id); |
371 | } |
372 | |
373 | void QSvgTinyDocument::restartAnimation() |
374 | { |
375 | m_time.restart(); |
376 | } |
377 | |
378 | bool QSvgTinyDocument::animated() const |
379 | { |
380 | return m_animated; |
381 | } |
382 | |
383 | void QSvgTinyDocument::setAnimated(bool a) |
384 | { |
385 | m_animated = a; |
386 | } |
387 | |
388 | void QSvgTinyDocument::draw(QPainter *p) |
389 | { |
390 | draw(p, QRectF()); |
391 | } |
392 | |
393 | void QSvgTinyDocument::(QPainter *p, QSvgExtraStates &) |
394 | { |
395 | draw(p); |
396 | } |
397 | |
398 | void QSvgTinyDocument::mapSourceToTarget(QPainter *p, const QRectF &targetRect, const QRectF &sourceRect) |
399 | { |
400 | QRectF target = targetRect; |
401 | if (target.isNull()) { |
402 | QPaintDevice *dev = p->device(); |
403 | QRectF deviceRect(0, 0, dev->width(), dev->height()); |
404 | if (deviceRect.isNull()) { |
405 | if (sourceRect.isNull()) |
406 | target = QRectF(QPointF(0, 0), size()); |
407 | else |
408 | target = QRectF(QPointF(0, 0), sourceRect.size()); |
409 | } else { |
410 | target = deviceRect; |
411 | } |
412 | } |
413 | |
414 | QRectF source = sourceRect; |
415 | if (source.isNull()) |
416 | source = viewBox(); |
417 | |
418 | if (source != target && !source.isNull()) { |
419 | QTransform transform; |
420 | transform.scale(target.width() / source.width(), |
421 | target.height() / source.height()); |
422 | QRectF c2 = transform.mapRect(source); |
423 | p->translate(target.x() - c2.x(), |
424 | target.y() - c2.y()); |
425 | p->scale(target.width() / source.width(), |
426 | target.height() / source.height()); |
427 | } |
428 | } |
429 | |
430 | QRectF QSvgTinyDocument::boundsOnElement(const QString &id) const |
431 | { |
432 | const QSvgNode *node = scopeNode(id); |
433 | if (!node) |
434 | node = this; |
435 | return node->transformedBounds(); |
436 | } |
437 | |
438 | bool QSvgTinyDocument::elementExists(const QString &id) const |
439 | { |
440 | QSvgNode *node = scopeNode(id); |
441 | |
442 | return (node!=0); |
443 | } |
444 | |
445 | QMatrix QSvgTinyDocument::matrixForElement(const QString &id) const |
446 | { |
447 | QSvgNode *node = scopeNode(id); |
448 | |
449 | if (!node) { |
450 | qDebug("Couldn't find node %s. Skipping rendering." , qPrintable(id)); |
451 | return QMatrix(); |
452 | } |
453 | |
454 | QTransform t; |
455 | |
456 | node = node->parent(); |
457 | while (node) { |
458 | if (node->m_style.transform) |
459 | t *= node->m_style.transform->qtransform(); |
460 | node = node->parent(); |
461 | } |
462 | |
463 | return t.toAffine(); |
464 | } |
465 | |
466 | int QSvgTinyDocument::currentFrame() const |
467 | { |
468 | double runningPercentage = qMin(m_time.elapsed()/double(m_animationDuration), 1.); |
469 | |
470 | int totalFrames = m_fps * m_animationDuration; |
471 | |
472 | return int(runningPercentage * totalFrames); |
473 | } |
474 | |
475 | void QSvgTinyDocument::setCurrentFrame(int frame) |
476 | { |
477 | int totalFrames = m_fps * m_animationDuration; |
478 | double framePercentage = frame/double(totalFrames); |
479 | double timeForFrame = m_animationDuration * framePercentage; //in S |
480 | timeForFrame *= 1000; //in ms |
481 | int timeToAdd = int(timeForFrame - m_time.elapsed()); |
482 | m_time = m_time.addMSecs(timeToAdd); |
483 | } |
484 | |
485 | void QSvgTinyDocument::setFramesPerSecond(int num) |
486 | { |
487 | m_fps = num; |
488 | } |
489 | |
490 | QT_END_NAMESPACE |
491 | |
492 | #endif // QT_NO_SVG |
493 | |