1 /*
2 SPDX-FileCopyrightText: 2006, 2009 Brad Hards <bradh@frogmouth.net>
3
4 SPDX-License-Identifier: GPL-2.0-or-later
5 */
6
7 #include "generator_xps.h"
8
9 #include <KAboutData>
10 #include <KLocalizedString>
11 #include <QBuffer>
12 #include <QDateTime>
13 #include <QFile>
14 #include <QImageReader>
15 #include <QList>
16 #include <QMutex>
17 #include <QPainter>
18 #include <QPrinter>
19 #include <QUrl>
20
21 #include <core/area.h>
22 #include <core/document.h>
23 #include <core/fileprinter.h>
24 #include <core/page.h>
25
26 OKULAR_EXPORT_PLUGIN(XpsGenerator, "libokularGenerator_xps.json")
27
Q_DECLARE_METATYPE(QGradient *)28 Q_DECLARE_METATYPE(QGradient *)
29 Q_DECLARE_METATYPE(XpsPathFigure *)
30 Q_DECLARE_METATYPE(XpsPathGeometry *)
31
32 // From Qt4
33 static int hex2int(char hex)
34 {
35 QChar hexchar = QLatin1Char(hex);
36 int v;
37 if (hexchar.isDigit())
38 v = hexchar.digitValue();
39 else if (hexchar >= QLatin1Char('A') && hexchar <= QLatin1Char('F'))
40 v = hexchar.cell() - 'A' + 10;
41 else if (hexchar >= QLatin1Char('a') && hexchar <= QLatin1Char('f'))
42 v = hexchar.cell() - 'a' + 10;
43 else
44 v = -1;
45 return v;
46 }
47
48 // Modified from Qt4
hexToRgba(const QByteArray & name)49 static QColor hexToRgba(const QByteArray &name)
50 {
51 const int len = name.length();
52 if (len == 0 || name[0] != '#')
53 return QColor();
54 int r, g, b;
55 int a = 255;
56 if (len == 7) {
57 r = (hex2int(name[1]) << 4) + hex2int(name[2]);
58 g = (hex2int(name[3]) << 4) + hex2int(name[4]);
59 b = (hex2int(name[5]) << 4) + hex2int(name[6]);
60 } else if (len == 9) {
61 a = (hex2int(name[1]) << 4) + hex2int(name[2]);
62 r = (hex2int(name[3]) << 4) + hex2int(name[4]);
63 g = (hex2int(name[5]) << 4) + hex2int(name[6]);
64 b = (hex2int(name[7]) << 4) + hex2int(name[8]);
65 } else {
66 r = g = b = -1;
67 }
68 if ((uint)r > 255 || (uint)g > 255 || (uint)b > 255) {
69 return QColor();
70 }
71 return QColor(r, g, b, a);
72 }
73
stringToRectF(const QString & data)74 static QRectF stringToRectF(const QString &data)
75 {
76 QStringList numbers = data.split(QLatin1Char(','));
77 QPointF origin(numbers.at(0).toDouble(), numbers.at(1).toDouble());
78 QSizeF size(numbers.at(2).toDouble(), numbers.at(3).toDouble());
79 return QRectF(origin, size);
80 }
81
parseGUID(const QString & guidString,unsigned short guid[16])82 static bool parseGUID(const QString &guidString, unsigned short guid[16])
83 {
84 if (guidString.length() <= 35) {
85 return false;
86 }
87
88 // Maps bytes to positions in guidString
89 const static int indexes[] = {6, 4, 2, 0, 11, 9, 16, 14, 19, 21, 24, 26, 28, 30, 32, 34};
90
91 for (int i = 0; i < 16; i++) {
92 int hex1 = hex2int(guidString[indexes[i]].cell());
93 int hex2 = hex2int(guidString[indexes[i] + 1].cell());
94
95 if ((hex1 < 0) || (hex2 < 0)) {
96 return false;
97 }
98
99 guid[i] = hex1 * 16 + hex2;
100 }
101
102 return true;
103 }
104
105 // Read next token of abbreviated path data
nextAbbPathToken(AbbPathToken * token)106 static bool nextAbbPathToken(AbbPathToken *token)
107 {
108 int *curPos = &token->curPos;
109 QString data = token->data;
110
111 while ((*curPos < data.length()) && (data.at(*curPos).isSpace())) {
112 (*curPos)++;
113 }
114
115 if (*curPos == data.length()) {
116 token->type = abtEOF;
117 return true;
118 }
119
120 QChar ch = data.at(*curPos);
121
122 if (ch.isNumber() || (ch == QLatin1Char('+')) || (ch == QLatin1Char('-'))) {
123 int start = *curPos;
124 while ((*curPos < data.length()) && (!data.at(*curPos).isSpace()) && (data.at(*curPos) != QLatin1Char(',') && (!data.at(*curPos).isLetter() || data.at(*curPos) == QLatin1Char('e')))) {
125 (*curPos)++;
126 }
127 token->number = data.midRef(start, *curPos - start).toDouble();
128 token->type = abtNumber;
129
130 } else if (ch == QLatin1Char(',')) {
131 token->type = abtComma;
132 (*curPos)++;
133 } else if (ch.isLetter()) {
134 token->type = abtCommand;
135 token->command = data.at(*curPos).cell();
136 (*curPos)++;
137 } else {
138 (*curPos)++;
139 return false;
140 }
141
142 return true;
143 }
144
145 /**
146 Read point (two reals delimited by comma) from abbreviated path data
147 */
getPointFromString(AbbPathToken * token,bool relative,const QPointF currentPosition)148 static QPointF getPointFromString(AbbPathToken *token, bool relative, const QPointF currentPosition)
149 {
150 // TODO Check grammar
151
152 QPointF result;
153 result.rx() = token->number;
154 nextAbbPathToken(token);
155 nextAbbPathToken(token); // ,
156 result.ry() = token->number;
157 nextAbbPathToken(token);
158
159 if (relative) {
160 result += currentPosition;
161 }
162
163 return result;
164 }
165
166 /**
167 Read point (two reals delimited by comma) from string
168 */
getPointFromString(const QString & string)169 static QPointF getPointFromString(const QString &string)
170 {
171 const int commaPos = string.indexOf(QLatin1Char(QLatin1Char(',')));
172 if (commaPos == -1 || string.indexOf(QLatin1Char(QLatin1Char(',')), commaPos + 1) != -1)
173 return QPointF();
174
175 QPointF result;
176 bool ok = false;
177 QStringRef ref = string.midRef(0, commaPos);
178 result.setX(QString::fromRawData(ref.constData(), ref.count()).toDouble(&ok));
179 if (!ok)
180 return QPointF();
181
182 ref = string.midRef(commaPos + 1);
183 result.setY(QString::fromRawData(ref.constData(), ref.count()).toDouble(&ok));
184 if (!ok)
185 return QPointF();
186
187 return result;
188 }
189
fillRuleFromString(const QString & data,Qt::FillRule def=Qt::OddEvenFill)190 static Qt::FillRule fillRuleFromString(const QString &data, Qt::FillRule def = Qt::OddEvenFill)
191 {
192 if (data == QLatin1String("EvenOdd")) {
193 return Qt::OddEvenFill;
194 } else if (data == QLatin1String("NonZero")) {
195 return Qt::WindingFill;
196 }
197 return def;
198 }
199
200 /**
201 Parse an abbreviated path "Data" description
202 \param data the string containing the whitespace separated values
203
204 \see XPS specification 4.2.3 and Appendix G
205 */
parseAbbreviatedPathData(const QString & data)206 static QPainterPath parseAbbreviatedPathData(const QString &data)
207 {
208 QPainterPath path;
209
210 AbbPathToken token;
211
212 token.data = data;
213 token.curPos = 0;
214
215 nextAbbPathToken(&token);
216
217 // Used by Smooth cubic curve (command s)
218 char lastCommand = ' ';
219 QPointF lastSecondControlPoint;
220
221 while (true) {
222 if (token.type != abtCommand) {
223 if (token.type != abtEOF) {
224 qCWarning(OkularXpsDebug).nospace() << "Error in parsing abbreviated path data (" << token.type << "@" << token.curPos << "): " << data;
225 }
226 return path;
227 }
228
229 char command = QChar::fromLatin1(token.command).toLower().cell();
230 bool isRelative = QChar::fromLatin1(token.command).isLower();
231 QPointF currPos = path.currentPosition();
232 nextAbbPathToken(&token);
233
234 switch (command) {
235 case 'f':
236 int rule;
237 rule = (int)token.number;
238 if (rule == 0) {
239 path.setFillRule(Qt::OddEvenFill);
240 } else if (rule == 1) {
241 // In xps specs rule 1 means NonZero fill. I think it's equivalent to WindingFill but I'm not sure
242 path.setFillRule(Qt::WindingFill);
243 }
244 nextAbbPathToken(&token);
245 break;
246 case 'm': // Move
247 while (token.type == abtNumber) {
248 QPointF point = getPointFromString(&token, isRelative, currPos);
249 path.moveTo(point);
250 }
251 break;
252 case 'l': // Line
253 while (token.type == abtNumber) {
254 QPointF point = getPointFromString(&token, isRelative, currPos);
255 path.lineTo(point);
256 }
257 break;
258 case 'h': // Horizontal line
259 while (token.type == abtNumber) {
260 double x = token.number;
261 if (isRelative)
262 x += path.currentPosition().x();
263 path.lineTo(x, path.currentPosition().y());
264 nextAbbPathToken(&token);
265 }
266 break;
267 case 'v': // Vertical line
268 while (token.type == abtNumber) {
269 double y = token.number;
270 if (isRelative)
271 y += path.currentPosition().y();
272 path.lineTo(path.currentPosition().x(), y);
273 nextAbbPathToken(&token);
274 }
275 break;
276 case 'c': // Cubic bezier curve
277 while (token.type == abtNumber) {
278 QPointF firstControl = getPointFromString(&token, isRelative, currPos);
279 QPointF secondControl = getPointFromString(&token, isRelative, currPos);
280 QPointF endPoint = getPointFromString(&token, isRelative, currPos);
281 path.cubicTo(firstControl, secondControl, endPoint);
282
283 lastSecondControlPoint = secondControl;
284 }
285 break;
286 case 'q': // Quadratic bezier curve
287 while (token.type == abtNumber) {
288 QPointF point1 = getPointFromString(&token, isRelative, currPos);
289 QPointF point2 = getPointFromString(&token, isRelative, currPos);
290 path.quadTo(point1, point2);
291 }
292 break;
293 case 's': // Smooth cubic bezier curve
294 while (token.type == abtNumber) {
295 QPointF firstControl;
296 if ((lastCommand == 's') || (lastCommand == 'c')) {
297 firstControl = lastSecondControlPoint + (lastSecondControlPoint + path.currentPosition());
298 } else {
299 firstControl = path.currentPosition();
300 }
301 QPointF secondControl = getPointFromString(&token, isRelative, currPos);
302 QPointF endPoint = getPointFromString(&token, isRelative, currPos);
303 path.cubicTo(firstControl, secondControl, endPoint);
304 }
305 break;
306 case 'a': // Arc
307 // TODO Implement Arc drawing
308 while (token.type == abtNumber) {
309 /*QPointF rp =*/getPointFromString(&token, isRelative, currPos);
310 /*double r = token.number;*/
311 nextAbbPathToken(&token);
312 /*double fArc = token.number; */
313 nextAbbPathToken(&token);
314 /*double fSweep = token.number; */
315 nextAbbPathToken(&token);
316 /*QPointF point = */ getPointFromString(&token, isRelative, currPos);
317 }
318 break;
319 case 'z': // Close path
320 path.closeSubpath();
321 break;
322 }
323
324 lastCommand = command;
325 }
326
327 return path;
328 }
329
330 /**
331 Parse a "Matrix" attribute string
332 \param csv the comma separated list of values
333 \return the QTransform corresponding to the affine transform
334 given in the attribute
335
336 \see XPS specification 7.4.1
337 */
attsToMatrix(const QString & csv)338 static QTransform attsToMatrix(const QString &csv)
339 {
340 QStringList values = csv.split(QLatin1Char(','));
341 if (values.count() != 6) {
342 return QTransform(); // that is an identity matrix - no effect
343 }
344 return QTransform(values.at(0).toDouble(), values.at(1).toDouble(), values.at(2).toDouble(), values.at(3).toDouble(), values.at(4).toDouble(), values.at(5).toDouble());
345 }
346
347 /**
348 \return Brush with given color or brush specified by reference to resource
349 */
parseRscRefColorForBrush(const QString & data)350 static QBrush parseRscRefColorForBrush(const QString &data)
351 {
352 if (data[0] == QLatin1Char('{')) {
353 // TODO
354 qCWarning(OkularXpsDebug) << "Reference" << data;
355 return QBrush();
356 } else {
357 return QBrush(hexToRgba(data.toLatin1()));
358 }
359 }
360
361 /**
362 \return Pen with given color or Pen specified by reference to resource
363 */
parseRscRefColorForPen(const QString & data)364 static QPen parseRscRefColorForPen(const QString &data)
365 {
366 if (data[0] == QLatin1Char('{')) {
367 // TODO
368 qCWarning(OkularXpsDebug) << "Reference" << data;
369 return QPen();
370 } else {
371 return QPen(hexToRgba(data.toLatin1()));
372 }
373 }
374
375 /**
376 \return Matrix specified by given data or by referenced dictionary
377 */
parseRscRefMatrix(const QString & data)378 static QTransform parseRscRefMatrix(const QString &data)
379 {
380 if (data[0] == QLatin1Char('{')) {
381 // TODO
382 qCWarning(OkularXpsDebug) << "Reference" << data;
383 return QTransform();
384 } else {
385 return attsToMatrix(data);
386 }
387 }
388
389 /**
390 \return Path specified by given data or by referenced dictionary
391 */
parseRscRefPath(const QString & data)392 static QPainterPath parseRscRefPath(const QString &data)
393 {
394 if (data[0] == QLatin1Char('{')) {
395 // TODO
396 qCWarning(OkularXpsDebug) << "Reference" << data;
397 return QPainterPath();
398 } else {
399 return parseAbbreviatedPathData(data);
400 }
401 }
402
403 /**
404 \return The path of the entry
405 */
entryPath(const QString & entry)406 static QString entryPath(const QString &entry)
407 {
408 const QChar slash = QChar::fromLatin1('/');
409 const int index = entry.lastIndexOf(slash);
410 QString ret = entry.mid(0, index);
411 if (index > 0) {
412 ret.append(slash);
413 }
414 if (!ret.startsWith(slash))
415 ret.prepend(slash);
416 return ret;
417 }
418
419 /**
420 \return The path of the entry
421 */
entryPath(const KZipFileEntry * entry)422 static QString entryPath(const KZipFileEntry *entry)
423 {
424 return entryPath(entry->path());
425 }
426
427 /**
428 \return The absolute path of the \p location, according to \p path if it's non-absolute
429 */
absolutePath(const QString & path,const QString & location)430 static QString absolutePath(const QString &path, const QString &location)
431 {
432 QString retPath;
433 if (location.startsWith(QLatin1Char('/'))) {
434 // already absolute
435 retPath = location;
436 } else {
437 QUrl u = QUrl::fromLocalFile(path);
438 QUrl u2 = QUrl(location);
439 retPath = u.resolved(u2).toDisplayString(QUrl::PreferLocalFile);
440 }
441 // it seems paths & file names can also be percent-encoded
442 // (XPS won't ever finish surprising me)
443 if (retPath.contains(QLatin1Char('%'))) {
444 retPath = QUrl::fromPercentEncoding(retPath.toUtf8());
445 }
446 return retPath;
447 }
448
449 /**
450 Read the content of an archive entry in both the cases:
451 a) single file
452 + foobar
453 b) directory
454 + foobar/
455 + [0].piece
456 + [1].piece
457 + ...
458 + [x].last.piece
459
460 \see XPS specification 10.1.2
461 */
readFileOrDirectoryParts(const KArchiveEntry * entry,QString * pathOfFile=nullptr)462 static QByteArray readFileOrDirectoryParts(const KArchiveEntry *entry, QString *pathOfFile = nullptr)
463 {
464 QByteArray data;
465 if (entry->isDirectory()) {
466 const KArchiveDirectory *relDir = static_cast<const KArchiveDirectory *>(entry);
467 QStringList entries = relDir->entries();
468 std::sort(entries.begin(), entries.end());
469 for (const QString &entry : qAsConst(entries)) {
470 const KArchiveEntry *relSubEntry = relDir->entry(entry);
471 if (!relSubEntry->isFile())
472 continue;
473
474 const KZipFileEntry *relSubFile = static_cast<const KZipFileEntry *>(relSubEntry);
475 data.append(relSubFile->data());
476 }
477 } else {
478 const KZipFileEntry *relFile = static_cast<const KZipFileEntry *>(entry);
479 data.append(relFile->data());
480 if (pathOfFile) {
481 *pathOfFile = entryPath(relFile);
482 }
483 }
484 return data;
485 }
486
487 /**
488 Load the resource \p fileName from the specified \p archive using the case sensitivity \p cs
489 */
loadEntry(KZip * archive,const QString & fileName,Qt::CaseSensitivity cs)490 static const KArchiveEntry *loadEntry(KZip *archive, const QString &fileName, Qt::CaseSensitivity cs)
491 {
492 // first attempt: loading the entry straight as requested
493 const KArchiveEntry *entry = archive->directory()->entry(fileName);
494 // in case sensitive mode, or if we actually found something, return what we found
495 if (cs == Qt::CaseSensitive || entry) {
496 return entry;
497 }
498
499 QString path;
500 QString entryName;
501 const int index = fileName.lastIndexOf(QChar::fromLatin1('/'));
502 if (index > 0) {
503 path = fileName.left(index);
504 entryName = fileName.mid(index + 1);
505 } else {
506 path = QLatin1Char('/');
507 entryName = fileName;
508 }
509 const KArchiveEntry *newEntry = archive->directory()->entry(path);
510 if (newEntry->isDirectory()) {
511 const KArchiveDirectory *relDir = static_cast<const KArchiveDirectory *>(newEntry);
512 QStringList relEntries = relDir->entries();
513 std::sort(relEntries.begin(), relEntries.end());
514 for (const QString &relEntry : qAsConst(relEntries)) {
515 if (relEntry.compare(entryName, Qt::CaseInsensitive) == 0) {
516 return relDir->entry(relEntry);
517 }
518 }
519 }
520 return nullptr;
521 }
522
523 /**
524 \return The name of a resource from the \p fileName
525 */
resourceName(const QString & fileName)526 static QString resourceName(const QString &fileName)
527 {
528 QString resource = fileName;
529 const int slashPos = fileName.lastIndexOf(QLatin1Char('/'));
530 const int dotPos = fileName.lastIndexOf(QLatin1Char('.'));
531 if (slashPos > -1) {
532 if (dotPos > -1 && dotPos > slashPos) {
533 resource = fileName.mid(slashPos + 1, dotPos - slashPos - 1);
534 } else {
535 resource = fileName.mid(slashPos + 1);
536 }
537 }
538 return resource;
539 }
540
interpolatedColor(const QColor & c1,const QColor & c2)541 static QColor interpolatedColor(const QColor &c1, const QColor &c2)
542 {
543 QColor res;
544 res.setAlpha((c1.alpha() + c2.alpha()) / 2);
545 res.setRed((c1.red() + c2.red()) / 2);
546 res.setGreen((c1.green() + c2.green()) / 2);
547 res.setBlue((c1.blue() + c2.blue()) / 2);
548 return res;
549 }
550
xpsGradientLessThan(const XpsGradient & g1,const XpsGradient & g2)551 static bool xpsGradientLessThan(const XpsGradient &g1, const XpsGradient &g2)
552 {
553 return qFuzzyCompare(g1.offset, g2.offset) ? g1.color.name() < g2.color.name() : g1.offset < g2.offset;
554 }
555
xpsGradientWithOffset(const QList<XpsGradient> & gradients,double offset)556 static int xpsGradientWithOffset(const QList<XpsGradient> &gradients, double offset)
557 {
558 int i = 0;
559 for (const XpsGradient &grad : gradients) {
560 if (grad.offset == offset) {
561 return i;
562 }
563 ++i;
564 }
565 return -1;
566 }
567
568 /**
569 Preprocess a list of gradients.
570
571 \see XPS specification 11.3.1.1
572 */
preprocessXpsGradients(QList<XpsGradient> & gradients)573 static void preprocessXpsGradients(QList<XpsGradient> &gradients)
574 {
575 if (gradients.isEmpty())
576 return;
577
578 // sort the gradients (case 1.)
579 std::stable_sort(gradients.begin(), gradients.end(), xpsGradientLessThan);
580
581 // no gradient with stop 0.0 (case 2.)
582 if (xpsGradientWithOffset(gradients, 0.0) == -1) {
583 int firstGreaterThanZero = 0;
584 while (firstGreaterThanZero < gradients.count() && gradients.at(firstGreaterThanZero).offset < 0.0)
585 ++firstGreaterThanZero;
586 // case 2.a: no gradients with stop less than 0.0
587 if (firstGreaterThanZero == 0) {
588 gradients.prepend(XpsGradient(0.0, gradients.first().color));
589 }
590 // case 2.b: some gradients with stop more than 0.0
591 else if (firstGreaterThanZero != gradients.count()) {
592 QColor col1 = gradients.at(firstGreaterThanZero - 1).color;
593 QColor col2 = gradients.at(firstGreaterThanZero).color;
594 for (int i = 0; i < firstGreaterThanZero; ++i) {
595 gradients.removeFirst();
596 }
597 gradients.prepend(XpsGradient(0.0, interpolatedColor(col1, col2)));
598 }
599 // case 2.c: no gradients with stop more than 0.0
600 else {
601 XpsGradient newGrad(0.0, gradients.last().color);
602 gradients.clear();
603 gradients.append(newGrad);
604 }
605 }
606
607 if (gradients.isEmpty())
608 return;
609
610 // no gradient with stop 1.0 (case 3.)
611 if (xpsGradientWithOffset(gradients, 1.0) == -1) {
612 int firstLessThanOne = gradients.count() - 1;
613 while (firstLessThanOne >= 0 && gradients.at(firstLessThanOne).offset > 1.0)
614 --firstLessThanOne;
615 // case 2.a: no gradients with stop greater than 1.0
616 if (firstLessThanOne == gradients.count() - 1) {
617 gradients.append(XpsGradient(1.0, gradients.last().color));
618 }
619 // case 2.b: some gradients with stop more than 1.0
620 else if (firstLessThanOne != -1) {
621 QColor col1 = gradients.at(firstLessThanOne).color;
622 QColor col2 = gradients.at(firstLessThanOne + 1).color;
623 for (int i = firstLessThanOne + 1; i < gradients.count(); ++i) {
624 gradients.removeLast();
625 }
626 gradients.append(XpsGradient(1.0, interpolatedColor(col1, col2)));
627 }
628 // case 2.c: no gradients with stop less than 1.0
629 else {
630 XpsGradient newGrad(1.0, gradients.first().color);
631 gradients.clear();
632 gradients.append(newGrad);
633 }
634 }
635 }
636
addXpsGradientsToQGradient(const QList<XpsGradient> & gradients,QGradient * qgrad)637 static void addXpsGradientsToQGradient(const QList<XpsGradient> &gradients, QGradient *qgrad)
638 {
639 for (const XpsGradient &grad : gradients) {
640 qgrad->setColorAt(grad.offset, grad.color);
641 }
642 }
643
applySpreadStyleToQGradient(const QString & style,QGradient * qgrad)644 static void applySpreadStyleToQGradient(const QString &style, QGradient *qgrad)
645 {
646 if (style.isEmpty())
647 return;
648
649 if (style == QLatin1String("Pad")) {
650 qgrad->setSpread(QGradient::PadSpread);
651 } else if (style == QLatin1String("Reflect")) {
652 qgrad->setSpread(QGradient::ReflectSpread);
653 } else if (style == QLatin1String("Repeat")) {
654 qgrad->setSpread(QGradient::RepeatSpread);
655 }
656 }
657
658 /**
659 Read an UnicodeString
660 \param raw the raw value of UnicodeString
661
662 \see XPS specification 5.1.4
663 */
unicodeString(const QString & raw)664 static QString unicodeString(const QString &raw)
665 {
666 QString ret;
667 if (raw.startsWith(QLatin1String("{}"))) {
668 ret = raw.mid(2);
669 } else {
670 ret = raw;
671 }
672 return ret;
673 }
674
XpsHandler(XpsPage * page)675 XpsHandler::XpsHandler(XpsPage *page)
676 : m_page(page)
677 {
678 m_painter = nullptr;
679 }
680
~XpsHandler()681 XpsHandler::~XpsHandler()
682 {
683 }
684
startDocument()685 bool XpsHandler::startDocument()
686 {
687 qCWarning(OkularXpsDebug) << "start document" << m_page->m_fileName;
688
689 XpsRenderNode node;
690 node.name = QStringLiteral("document");
691 m_nodes.push(node);
692
693 return true;
694 }
695
startElement(const QString & nameSpace,const QString & localName,const QString & qname,const QXmlAttributes & atts)696 bool XpsHandler::startElement(const QString &nameSpace, const QString &localName, const QString &qname, const QXmlAttributes &atts)
697 {
698 Q_UNUSED(nameSpace)
699 Q_UNUSED(qname)
700
701 XpsRenderNode node;
702 node.name = localName;
703 node.attributes = atts;
704 processStartElement(node);
705 m_nodes.push(node);
706
707 return true;
708 }
709
endElement(const QString & nameSpace,const QString & localName,const QString & qname)710 bool XpsHandler::endElement(const QString &nameSpace, const QString &localName, const QString &qname)
711 {
712 Q_UNUSED(nameSpace)
713 Q_UNUSED(qname)
714
715 XpsRenderNode node = m_nodes.pop();
716 if (node.name != localName) {
717 qCWarning(OkularXpsDebug) << "Name doesn't match";
718 }
719 processEndElement(node);
720 node.children.clear();
721 m_nodes.top().children.append(node);
722
723 return true;
724 }
725
processGlyph(XpsRenderNode & node)726 void XpsHandler::processGlyph(XpsRenderNode &node)
727 {
728 // TODO Currently ignored attributes: CaretStops, DeviceFontName, IsSideways, OpacityMask, Name, FixedPage.NavigateURI, xml:lang, x:key
729 // TODO Indices is only partially implemented
730 // TODO Currently ignored child elements: Clip, OpacityMask
731 // Handled separately: RenderTransform
732
733 QString att;
734
735 m_painter->save();
736
737 // Get font (doesn't work well because qt doesn't allow to load font from file)
738 // This works despite the fact that font size isn't specified in points as required by qt. It's because I set point size to be equal to drawing unit.
739 float fontSize = node.attributes.value(QStringLiteral("FontRenderingEmSize")).toFloat();
740 // qCWarning(OkularXpsDebug) << "Font Rendering EmSize:" << fontSize;
741 // a value of 0.0 means the text is not visible (see XPS specs, chapter 12, "Glyphs")
742 if (fontSize < 0.1) {
743 m_painter->restore();
744 return;
745 }
746 const QString absoluteFileName = absolutePath(entryPath(m_page->fileName()), node.attributes.value(QStringLiteral("FontUri")));
747 QFont font = m_page->m_file->getFontByName(absoluteFileName, fontSize);
748 att = node.attributes.value(QStringLiteral("StyleSimulations"));
749 if (!att.isEmpty()) {
750 if (att == QLatin1String("ItalicSimulation")) {
751 font.setItalic(true);
752 } else if (att == QLatin1String("BoldSimulation")) {
753 font.setBold(true);
754 } else if (att == QLatin1String("BoldItalicSimulation")) {
755 font.setItalic(true);
756 font.setBold(true);
757 }
758 }
759 m_painter->setFont(font);
760
761 // Origin
762 QPointF origin(node.attributes.value(QStringLiteral("OriginX")).toDouble(), node.attributes.value(QStringLiteral("OriginY")).toDouble());
763
764 // Fill
765 QBrush brush;
766 att = node.attributes.value(QStringLiteral("Fill"));
767 if (att.isEmpty()) {
768 QVariant data = node.getChildData(QStringLiteral("Glyphs.Fill"));
769 if (data.canConvert<QBrush>()) {
770 brush = data.value<QBrush>();
771 } else {
772 // no "Fill" attribute and no "Glyphs.Fill" child, so show nothing
773 // (see XPS specs, 5.10)
774 m_painter->restore();
775 return;
776 }
777 } else {
778 brush = parseRscRefColorForBrush(att);
779 if (brush.style() > Qt::NoBrush && brush.style() < Qt::LinearGradientPattern && brush.color().alpha() == 0) {
780 m_painter->restore();
781 return;
782 }
783 }
784 m_painter->setBrush(brush);
785 m_painter->setPen(QPen(brush, 0));
786
787 // Opacity
788 att = node.attributes.value(QStringLiteral("Opacity"));
789 if (!att.isEmpty()) {
790 bool ok = true;
791 double value = att.toDouble(&ok);
792 if (ok && value >= 0.1) {
793 m_painter->setOpacity(value);
794 } else {
795 m_painter->restore();
796 return;
797 }
798 }
799
800 // RenderTransform
801 att = node.attributes.value(QStringLiteral("RenderTransform"));
802 if (!att.isEmpty()) {
803 m_painter->setWorldTransform(parseRscRefMatrix(att), true);
804 }
805
806 // Clip
807 att = node.attributes.value(QStringLiteral("Clip"));
808 if (!att.isEmpty()) {
809 QPainterPath clipPath = parseRscRefPath(att);
810 if (!clipPath.isEmpty()) {
811 m_painter->setClipPath(clipPath);
812 }
813 }
814
815 // BiDiLevel - default Left-to-Right
816 m_painter->setLayoutDirection(Qt::LeftToRight);
817 att = node.attributes.value(QStringLiteral("BiDiLevel"));
818 if (!att.isEmpty()) {
819 if ((att.toInt() % 2) == 1) {
820 // odd BiDiLevel, so Right-to-Left
821 m_painter->setLayoutDirection(Qt::RightToLeft);
822 }
823 }
824
825 // Indices - partial handling only
826 att = node.attributes.value(QStringLiteral("Indices"));
827 QList<qreal> advanceWidths;
828 if (!att.isEmpty()) {
829 QStringList indicesElements = att.split(QLatin1Char(';'));
830 for (int i = 0; i < indicesElements.size(); ++i) {
831 if (indicesElements.at(i).contains(QStringLiteral(","))) {
832 QStringList parts = indicesElements.at(i).split(QLatin1Char(','));
833 if (parts.size() == 2) {
834 // regular advance case, no offsets
835 advanceWidths.append(parts.at(1).toDouble() * fontSize / 100.0);
836 } else if (parts.size() == 3) {
837 // regular advance case, with uOffset
838 qreal AdvanceWidth = parts.at(1).toDouble() * fontSize / 100.0;
839 qreal uOffset = parts.at(2).toDouble() / 100.0;
840 advanceWidths.append(AdvanceWidth + uOffset);
841 } else {
842 // has vertical offset, but don't know how to handle that yet
843 qCWarning(OkularXpsDebug) << "Unhandled Indices element: " << indicesElements.at(i);
844 advanceWidths.append(-1.0);
845 }
846 } else {
847 // no special advance case
848 advanceWidths.append(-1.0);
849 }
850 }
851 }
852
853 // UnicodeString
854 QString stringToDraw(unicodeString(node.attributes.value(QStringLiteral("UnicodeString"))));
855 QPointF originAdvance(0, 0);
856 QFontMetrics metrics = m_painter->fontMetrics();
857 for (int i = 0; i < stringToDraw.size(); ++i) {
858 QChar thisChar = stringToDraw.at(i);
859 m_painter->drawText(origin + originAdvance, QString(thisChar));
860 const qreal advanceWidth = advanceWidths.value(i, qreal(-1.0));
861 if (advanceWidth > 0.0) {
862 originAdvance.rx() += advanceWidth;
863 } else {
864 originAdvance.rx() += metrics.horizontalAdvance(thisChar);
865 }
866 }
867 // qCWarning(OkularXpsDebug) << "Glyphs: " << atts.value("Fill") << ", " << atts.value("FontUri");
868 // qCWarning(OkularXpsDebug) << " Origin: " << atts.value("OriginX") << "," << atts.value("OriginY");
869 // qCWarning(OkularXpsDebug) << " Unicode: " << atts.value("UnicodeString");
870
871 m_painter->restore();
872 }
873
processFill(XpsRenderNode & node)874 void XpsHandler::processFill(XpsRenderNode &node)
875 {
876 // TODO Ignored child elements: VirtualBrush
877
878 if (node.children.size() != 1) {
879 qCWarning(OkularXpsDebug) << "Fill element should have exactly one child";
880 } else {
881 node.data = node.children[0].data;
882 }
883 }
884
processStroke(XpsRenderNode & node)885 void XpsHandler::processStroke(XpsRenderNode &node)
886 {
887 // TODO Ignored child elements: VirtualBrush
888
889 if (node.children.size() != 1) {
890 qCWarning(OkularXpsDebug) << "Stroke element should have exactly one child";
891 } else {
892 node.data = node.children[0].data;
893 }
894 }
895
processImageBrush(XpsRenderNode & node)896 void XpsHandler::processImageBrush(XpsRenderNode &node)
897 {
898 // TODO Ignored attributes: Opacity, x:key, TileMode, ViewBoxUnits, ViewPortUnits
899 // TODO Check whether transformation works for non standard situations (viewbox different that whole image, Transform different that simple move & scale, Viewport different than [0, 0, 1, 1]
900
901 QString att;
902 QBrush brush;
903
904 QRectF viewport = stringToRectF(node.attributes.value(QStringLiteral("Viewport")));
905 QRectF viewbox = stringToRectF(node.attributes.value(QStringLiteral("Viewbox")));
906 QImage image = m_page->loadImageFromFile(node.attributes.value(QStringLiteral("ImageSource")));
907
908 // Matrix which can transform [0, 0, 1, 1] rectangle to given viewbox
909 QTransform viewboxMatrix = QTransform(viewbox.width() * image.physicalDpiX() / 96, 0, 0, viewbox.height() * image.physicalDpiY() / 96, viewbox.x(), viewbox.y());
910
911 // Matrix which can transform [0, 0, 1, 1] rectangle to given viewport
912 // TODO Take ViewPort into account
913 QTransform viewportMatrix;
914 att = node.attributes.value(QStringLiteral("Transform"));
915 if (att.isEmpty()) {
916 QVariant data = node.getChildData(QStringLiteral("ImageBrush.Transform"));
917 if (data.canConvert<QTransform>()) {
918 viewportMatrix = data.value<QTransform>();
919 } else {
920 viewportMatrix = QTransform();
921 }
922 } else {
923 viewportMatrix = parseRscRefMatrix(att);
924 }
925 viewportMatrix = viewportMatrix * QTransform(viewport.width(), 0, 0, viewport.height(), viewport.x(), viewport.y());
926
927 brush = QBrush(image);
928 brush.setTransform(viewboxMatrix.inverted() * viewportMatrix);
929
930 node.data = QVariant::fromValue(brush);
931 }
932
processPath(XpsRenderNode & node)933 void XpsHandler::processPath(XpsRenderNode &node)
934 {
935 // TODO Ignored attributes: Clip, OpacityMask, StrokeEndLineCap, StorkeStartLineCap, Name, FixedPage.NavigateURI, xml:lang, x:key, AutomationProperties.Name, AutomationProperties.HelpText, SnapsToDevicePixels
936 // TODO Ignored child elements: RenderTransform, Clip, OpacityMask
937 // Handled separately: RenderTransform
938 m_painter->save();
939
940 QString att;
941 QVariant data;
942
943 // Get path
944 XpsPathGeometry *pathdata = node.getChildData(QStringLiteral("Path.Data")).value<XpsPathGeometry *>();
945 att = node.attributes.value(QStringLiteral("Data"));
946 if (!att.isEmpty()) {
947 QPainterPath path = parseRscRefPath(att);
948 delete pathdata;
949 pathdata = new XpsPathGeometry();
950 pathdata->paths.append(new XpsPathFigure(path, true));
951 }
952 if (!pathdata) {
953 // nothing to draw
954 m_painter->restore();
955 return;
956 }
957
958 // Set Fill
959 att = node.attributes.value(QStringLiteral("Fill"));
960 QBrush brush;
961 if (!att.isEmpty()) {
962 brush = parseRscRefColorForBrush(att);
963 } else {
964 data = node.getChildData(QStringLiteral("Path.Fill"));
965 if (data.canConvert<QBrush>()) {
966 brush = data.value<QBrush>();
967 }
968 }
969 m_painter->setBrush(brush);
970
971 // Stroke (pen)
972 att = node.attributes.value(QStringLiteral("Stroke"));
973 QPen pen(Qt::transparent);
974 if (!att.isEmpty()) {
975 pen = parseRscRefColorForPen(att);
976 } else {
977 data = node.getChildData(QStringLiteral("Path.Stroke"));
978 if (data.canConvert<QBrush>()) {
979 pen.setBrush(data.value<QBrush>());
980 }
981 }
982 att = node.attributes.value(QStringLiteral("StrokeThickness"));
983 if (!att.isEmpty()) {
984 bool ok = false;
985 int thickness = att.toInt(&ok);
986 if (ok)
987 pen.setWidth(thickness);
988 }
989 att = node.attributes.value(QStringLiteral("StrokeDashArray"));
990 if (!att.isEmpty()) {
991 const QStringList pieces = att.split(QLatin1Char(' '), QString::SkipEmptyParts);
992 QVector<qreal> dashPattern(pieces.count());
993 bool ok = false;
994 for (int i = 0; i < pieces.count(); ++i) {
995 qreal value = pieces.at(i).toInt(&ok);
996 if (ok) {
997 dashPattern[i] = value;
998 } else {
999 break;
1000 }
1001 }
1002 if (ok) {
1003 pen.setDashPattern(dashPattern);
1004 }
1005 }
1006 att = node.attributes.value(QStringLiteral("StrokeDashOffset"));
1007 if (!att.isEmpty()) {
1008 bool ok = false;
1009 int offset = att.toInt(&ok);
1010 if (ok)
1011 pen.setDashOffset(offset);
1012 }
1013 att = node.attributes.value(QStringLiteral("StrokeDashCap"));
1014 if (!att.isEmpty()) {
1015 Qt::PenCapStyle cap = Qt::FlatCap;
1016 if (att == QLatin1String("Flat")) {
1017 cap = Qt::FlatCap;
1018 } else if (att == QLatin1String("Round")) {
1019 cap = Qt::RoundCap;
1020 } else if (att == QLatin1String("Square")) {
1021 cap = Qt::SquareCap;
1022 }
1023 // ### missing "Triangle"
1024 pen.setCapStyle(cap);
1025 }
1026 att = node.attributes.value(QStringLiteral("StrokeLineJoin"));
1027 if (!att.isEmpty()) {
1028 Qt::PenJoinStyle joinStyle = Qt::MiterJoin;
1029 if (att == QLatin1String("Miter")) {
1030 joinStyle = Qt::MiterJoin;
1031 } else if (att == QLatin1String("Bevel")) {
1032 joinStyle = Qt::BevelJoin;
1033 } else if (att == QLatin1String("Round")) {
1034 joinStyle = Qt::RoundJoin;
1035 }
1036 pen.setJoinStyle(joinStyle);
1037 }
1038 att = node.attributes.value(QStringLiteral("StrokeMiterLimit"));
1039 if (!att.isEmpty()) {
1040 bool ok = false;
1041 double limit = att.toDouble(&ok);
1042 if (ok) {
1043 // we have to divide it by two, as XPS consider half of the stroke width,
1044 // while Qt the whole of it
1045 pen.setMiterLimit(limit / 2);
1046 }
1047 }
1048 m_painter->setPen(pen);
1049
1050 // Opacity
1051 att = node.attributes.value(QStringLiteral("Opacity"));
1052 if (!att.isEmpty()) {
1053 m_painter->setOpacity(att.toDouble());
1054 }
1055
1056 // RenderTransform
1057 att = node.attributes.value(QStringLiteral("RenderTransform"));
1058 if (!att.isEmpty()) {
1059 m_painter->setWorldTransform(parseRscRefMatrix(att), true);
1060 }
1061 if (!pathdata->transform.isIdentity()) {
1062 m_painter->setWorldTransform(pathdata->transform, true);
1063 }
1064
1065 for (const XpsPathFigure *figure : qAsConst(pathdata->paths)) {
1066 m_painter->setBrush(figure->isFilled ? brush : QBrush());
1067 m_painter->drawPath(figure->path);
1068 }
1069
1070 delete pathdata;
1071
1072 m_painter->restore();
1073 }
1074
processPathData(XpsRenderNode & node)1075 void XpsHandler::processPathData(XpsRenderNode &node)
1076 {
1077 if (node.children.size() != 1) {
1078 qCWarning(OkularXpsDebug) << "Path.Data element should have exactly one child";
1079 } else {
1080 node.data = node.children[0].data;
1081 }
1082 }
1083
processPathGeometry(XpsRenderNode & node)1084 void XpsHandler::processPathGeometry(XpsRenderNode &node)
1085 {
1086 XpsPathGeometry *geom = new XpsPathGeometry();
1087
1088 for (const XpsRenderNode &child : qAsConst(node.children)) {
1089 if (child.data.canConvert<XpsPathFigure *>()) {
1090 XpsPathFigure *figure = child.data.value<XpsPathFigure *>();
1091 geom->paths.append(figure);
1092 }
1093 }
1094
1095 QString att;
1096
1097 att = node.attributes.value(QStringLiteral("Figures"));
1098 if (!att.isEmpty()) {
1099 QPainterPath path = parseRscRefPath(att);
1100 qDeleteAll(geom->paths);
1101 geom->paths.clear();
1102 geom->paths.append(new XpsPathFigure(path, true));
1103 }
1104
1105 att = node.attributes.value(QStringLiteral("FillRule"));
1106 if (!att.isEmpty()) {
1107 geom->fillRule = fillRuleFromString(att);
1108 }
1109
1110 // Transform
1111 att = node.attributes.value(QStringLiteral("Transform"));
1112 if (!att.isEmpty()) {
1113 geom->transform = parseRscRefMatrix(att);
1114 }
1115
1116 if (!geom->paths.isEmpty()) {
1117 node.data = QVariant::fromValue(geom);
1118 } else {
1119 delete geom;
1120 }
1121 }
1122
processPathFigure(XpsRenderNode & node)1123 void XpsHandler::processPathFigure(XpsRenderNode &node)
1124 {
1125 // TODO Ignored child elements: ArcSegment
1126
1127 QString att;
1128 QPainterPath path;
1129
1130 att = node.attributes.value(QStringLiteral("StartPoint"));
1131 if (!att.isEmpty()) {
1132 QPointF point = getPointFromString(att);
1133 path.moveTo(point);
1134 } else {
1135 return;
1136 }
1137
1138 for (const XpsRenderNode &child : qAsConst(node.children)) {
1139 bool isStroked = true;
1140 att = node.attributes.value(QStringLiteral("IsStroked"));
1141 if (!att.isEmpty()) {
1142 isStroked = att == QLatin1String("true");
1143 }
1144 if (!isStroked) {
1145 continue;
1146 }
1147
1148 // PolyLineSegment
1149 if (child.name == QLatin1String("PolyLineSegment")) {
1150 att = child.attributes.value(QStringLiteral("Points"));
1151 if (!att.isEmpty()) {
1152 const QStringList points = att.split(QLatin1Char(' '), QString::SkipEmptyParts);
1153 for (const QString &p : points) {
1154 QPointF point = getPointFromString(p);
1155 path.lineTo(point);
1156 }
1157 }
1158 }
1159 // PolyBezierSegment
1160 else if (child.name == QLatin1String("PolyBezierSegment")) {
1161 att = child.attributes.value(QStringLiteral("Points"));
1162 if (!att.isEmpty()) {
1163 const QStringList points = att.split(QLatin1Char(' '), QString::SkipEmptyParts);
1164 if (points.count() % 3 == 0) {
1165 for (int i = 0; i < points.count();) {
1166 QPointF firstControl = getPointFromString(points.at(i++));
1167 QPointF secondControl = getPointFromString(points.at(i++));
1168 QPointF endPoint = getPointFromString(points.at(i++));
1169 path.cubicTo(firstControl, secondControl, endPoint);
1170 }
1171 }
1172 }
1173 }
1174 // PolyQuadraticBezierSegment
1175 else if (child.name == QLatin1String("PolyQuadraticBezierSegment")) {
1176 att = child.attributes.value(QStringLiteral("Points"));
1177 if (!att.isEmpty()) {
1178 const QStringList points = att.split(QLatin1Char(' '), QString::SkipEmptyParts);
1179 if (points.count() % 2 == 0) {
1180 for (int i = 0; i < points.count();) {
1181 QPointF point1 = getPointFromString(points.at(i++));
1182 QPointF point2 = getPointFromString(points.at(i++));
1183 path.quadTo(point1, point2);
1184 }
1185 }
1186 }
1187 }
1188 }
1189
1190 bool closePath = false;
1191 att = node.attributes.value(QStringLiteral("IsClosed"));
1192 if (!att.isEmpty()) {
1193 closePath = att == QLatin1String("true");
1194 }
1195 if (closePath) {
1196 path.closeSubpath();
1197 }
1198
1199 bool isFilled = true;
1200 att = node.attributes.value(QStringLiteral("IsFilled"));
1201 if (!att.isEmpty()) {
1202 isFilled = att == QLatin1String("true");
1203 }
1204
1205 if (!path.isEmpty()) {
1206 node.data = QVariant::fromValue(new XpsPathFigure(path, isFilled));
1207 }
1208 }
1209
processStartElement(XpsRenderNode & node)1210 void XpsHandler::processStartElement(XpsRenderNode &node)
1211 {
1212 if (node.name == QLatin1String("Canvas")) {
1213 m_painter->save();
1214 QString att = node.attributes.value(QStringLiteral("RenderTransform"));
1215 if (!att.isEmpty()) {
1216 m_painter->setWorldTransform(parseRscRefMatrix(att), true);
1217 }
1218 att = node.attributes.value(QStringLiteral("Opacity"));
1219 if (!att.isEmpty()) {
1220 double value = att.toDouble();
1221 if (value > 0.0 && value <= 1.0) {
1222 m_painter->setOpacity(m_painter->opacity() * value);
1223 } else {
1224 // setting manually to 0 is necessary to "disable"
1225 // all the stuff inside
1226 m_painter->setOpacity(0.0);
1227 }
1228 }
1229 }
1230 }
1231
processEndElement(XpsRenderNode & node)1232 void XpsHandler::processEndElement(XpsRenderNode &node)
1233 {
1234 if (node.name == QLatin1String("Glyphs")) {
1235 processGlyph(node);
1236 } else if (node.name == QLatin1String("Path")) {
1237 processPath(node);
1238 } else if (node.name == QLatin1String("MatrixTransform")) {
1239 // TODO Ignoring x:key
1240 node.data = QVariant::fromValue(QTransform(attsToMatrix(node.attributes.value(QStringLiteral("Matrix")))));
1241 } else if ((node.name == QLatin1String("Canvas.RenderTransform")) || (node.name == QLatin1String("Glyphs.RenderTransform")) || (node.name == QLatin1String("Path.RenderTransform"))) {
1242 QVariant data = node.getRequiredChildData(QStringLiteral("MatrixTransform"));
1243 if (data.canConvert<QTransform>()) {
1244 m_painter->setWorldTransform(data.value<QTransform>(), true);
1245 }
1246 } else if (node.name == QLatin1String("Canvas")) {
1247 m_painter->restore();
1248 } else if ((node.name == QLatin1String("Path.Fill")) || (node.name == QLatin1String("Glyphs.Fill"))) {
1249 processFill(node);
1250 } else if (node.name == QLatin1String("Path.Stroke")) {
1251 processStroke(node);
1252 } else if (node.name == QLatin1String("SolidColorBrush")) {
1253 // TODO Ignoring opacity, x:key
1254 node.data = QVariant::fromValue(QBrush(QColor(hexToRgba(node.attributes.value(QStringLiteral("Color")).toLatin1()))));
1255 } else if (node.name == QLatin1String("ImageBrush")) {
1256 processImageBrush(node);
1257 } else if (node.name == QLatin1String("ImageBrush.Transform")) {
1258 node.data = node.getRequiredChildData(QStringLiteral("MatrixTransform"));
1259 } else if (node.name == QLatin1String("LinearGradientBrush")) {
1260 const XpsRenderNode *gradients = node.findChild(QStringLiteral("LinearGradientBrush.GradientStops"));
1261 if (gradients && gradients->data.canConvert<QGradient *>()) {
1262 QPointF start = getPointFromString(node.attributes.value(QStringLiteral("StartPoint")));
1263 QPointF end = getPointFromString(node.attributes.value(QStringLiteral("EndPoint")));
1264 QLinearGradient *qgrad = static_cast<QLinearGradient *>(gradients->data.value<QGradient *>());
1265 qgrad->setStart(start);
1266 qgrad->setFinalStop(end);
1267 applySpreadStyleToQGradient(node.attributes.value(QStringLiteral("SpreadMethod")), qgrad);
1268 node.data = QVariant::fromValue(QBrush(*qgrad));
1269 delete qgrad;
1270 }
1271 } else if (node.name == QLatin1String("RadialGradientBrush")) {
1272 const XpsRenderNode *gradients = node.findChild(QStringLiteral("RadialGradientBrush.GradientStops"));
1273 if (gradients && gradients->data.canConvert<QGradient *>()) {
1274 QPointF center = getPointFromString(node.attributes.value(QStringLiteral("Center")));
1275 QPointF origin = getPointFromString(node.attributes.value(QStringLiteral("GradientOrigin")));
1276 double radiusX = node.attributes.value(QStringLiteral("RadiusX")).toDouble();
1277 double radiusY = node.attributes.value(QStringLiteral("RadiusY")).toDouble();
1278 QRadialGradient *qgrad = static_cast<QRadialGradient *>(gradients->data.value<QGradient *>());
1279 qgrad->setCenter(center);
1280 qgrad->setFocalPoint(origin);
1281 // TODO what in case of different radii?
1282 qgrad->setRadius(qMin(radiusX, radiusY));
1283 applySpreadStyleToQGradient(node.attributes.value(QStringLiteral("SpreadMethod")), qgrad);
1284 node.data = QVariant::fromValue(QBrush(*qgrad));
1285 delete qgrad;
1286 }
1287 } else if (node.name == QLatin1String("LinearGradientBrush.GradientStops")) {
1288 QList<XpsGradient> gradients;
1289 for (const XpsRenderNode &child : qAsConst(node.children)) {
1290 double offset = child.attributes.value(QStringLiteral("Offset")).toDouble();
1291 QColor color = hexToRgba(child.attributes.value(QStringLiteral("Color")).toLatin1());
1292 gradients.append(XpsGradient(offset, color));
1293 }
1294 preprocessXpsGradients(gradients);
1295 if (!gradients.isEmpty()) {
1296 QLinearGradient *qgrad = new QLinearGradient();
1297 addXpsGradientsToQGradient(gradients, qgrad);
1298 node.data = QVariant::fromValue<QGradient *>(qgrad);
1299 }
1300 } else if (node.name == QLatin1String("RadialGradientBrush.GradientStops")) {
1301 QList<XpsGradient> gradients;
1302 for (const XpsRenderNode &child : qAsConst(node.children)) {
1303 double offset = child.attributes.value(QStringLiteral("Offset")).toDouble();
1304 QColor color = hexToRgba(child.attributes.value(QStringLiteral("Color")).toLatin1());
1305 gradients.append(XpsGradient(offset, color));
1306 }
1307 preprocessXpsGradients(gradients);
1308 if (!gradients.isEmpty()) {
1309 QRadialGradient *qgrad = new QRadialGradient();
1310 addXpsGradientsToQGradient(gradients, qgrad);
1311 node.data = QVariant::fromValue<QGradient *>(qgrad);
1312 }
1313 } else if (node.name == QLatin1String("PathFigure")) {
1314 processPathFigure(node);
1315 } else if (node.name == QLatin1String("PathGeometry")) {
1316 processPathGeometry(node);
1317 } else if (node.name == QLatin1String("Path.Data")) {
1318 processPathData(node);
1319 } else {
1320 // qCWarning(OkularXpsDebug) << "Unknown element: " << node->name;
1321 }
1322 }
1323
XpsPage(XpsFile * file,const QString & fileName)1324 XpsPage::XpsPage(XpsFile *file, const QString &fileName)
1325 : m_file(file)
1326 , m_fileName(fileName)
1327 , m_pageIsRendered(false)
1328 {
1329 m_pageImage = nullptr;
1330
1331 // qCWarning(OkularXpsDebug) << "page file name: " << fileName;
1332
1333 const KZipFileEntry *pageFile = static_cast<const KZipFileEntry *>(m_file->xpsArchive()->directory()->entry(fileName));
1334
1335 QXmlStreamReader xml;
1336 xml.addData(readFileOrDirectoryParts(pageFile));
1337 while (!xml.atEnd()) {
1338 xml.readNext();
1339 if (xml.isStartElement() && (xml.name() == QStringLiteral("FixedPage"))) {
1340 QXmlStreamAttributes attributes = xml.attributes();
1341 m_pageSize.setWidth(attributes.value(QStringLiteral("Width")).toString().toDouble());
1342 m_pageSize.setHeight(attributes.value(QStringLiteral("Height")).toString().toDouble());
1343 break;
1344 }
1345 }
1346 if (xml.error()) {
1347 qCWarning(OkularXpsDebug) << "Could not parse XPS page:" << xml.errorString();
1348 }
1349 }
1350
~XpsPage()1351 XpsPage::~XpsPage()
1352 {
1353 delete m_pageImage;
1354 }
1355
renderToImage(QImage * p)1356 bool XpsPage::renderToImage(QImage *p)
1357 {
1358 if ((m_pageImage == nullptr) || (m_pageImage->size() != p->size())) {
1359 delete m_pageImage;
1360 m_pageImage = new QImage(p->size(), QImage::Format_ARGB32);
1361 // Set one point = one drawing unit. Useful for fonts, because xps specifies font size using drawing units, not points as usual
1362 m_pageImage->setDotsPerMeterX(2835);
1363 m_pageImage->setDotsPerMeterY(2835);
1364
1365 m_pageIsRendered = false;
1366 }
1367 if (!m_pageIsRendered) {
1368 m_pageImage->fill(qRgba(255, 255, 255, 255));
1369 QPainter painter(m_pageImage);
1370 renderToPainter(&painter);
1371 m_pageIsRendered = true;
1372 }
1373
1374 *p = *m_pageImage;
1375
1376 return true;
1377 }
1378
renderToPainter(QPainter * painter)1379 bool XpsPage::renderToPainter(QPainter *painter)
1380 {
1381 XpsHandler handler(this);
1382 handler.m_painter = painter;
1383 handler.m_painter->setWorldTransform(QTransform().scale((qreal)painter->device()->width() / size().width(), (qreal)painter->device()->height() / size().height()));
1384 QXmlSimpleReader parser;
1385 parser.setContentHandler(&handler);
1386 parser.setErrorHandler(&handler);
1387 const KZipFileEntry *pageFile = static_cast<const KZipFileEntry *>(m_file->xpsArchive()->directory()->entry(m_fileName));
1388 QByteArray data = readFileOrDirectoryParts(pageFile);
1389 QBuffer buffer(&data);
1390 QXmlInputSource source(&buffer);
1391 bool ok = parser.parse(source);
1392 qCWarning(OkularXpsDebug) << "Parse result: " << ok;
1393
1394 return true;
1395 }
1396
size() const1397 QSizeF XpsPage::size() const
1398 {
1399 return m_pageSize;
1400 }
1401
getFontByName(const QString & absoluteFileName,float size)1402 QFont XpsFile::getFontByName(const QString &absoluteFileName, float size)
1403 {
1404 // qCWarning(OkularXpsDebug) << "trying to get font: " << fileName << ", size: " << size;
1405
1406 int index = m_fontCache.value(absoluteFileName, -1);
1407 if (index == -1) {
1408 index = loadFontByName(absoluteFileName);
1409 m_fontCache[absoluteFileName] = index;
1410 }
1411 if (index == -1) {
1412 qCWarning(OkularXpsDebug) << "Requesting unknown font:" << absoluteFileName;
1413 return QFont();
1414 }
1415
1416 const QStringList fontFamilies = m_fontDatabase.applicationFontFamilies(index);
1417 if (fontFamilies.isEmpty()) {
1418 qCWarning(OkularXpsDebug) << "The unexpected has happened. No font family for a known font:" << absoluteFileName << index;
1419 return QFont();
1420 }
1421 const QString fontFamily = fontFamilies[0];
1422 const QStringList fontStyles = m_fontDatabase.styles(fontFamily);
1423 if (fontStyles.isEmpty()) {
1424 qCWarning(OkularXpsDebug) << "The unexpected has happened. No font style for a known font family:" << absoluteFileName << index << fontFamily;
1425 return QFont();
1426 }
1427 const QString fontStyle = fontStyles[0];
1428 return m_fontDatabase.font(fontFamily, fontStyle, qRound(size));
1429 }
1430
loadFontByName(const QString & absoluteFileName)1431 int XpsFile::loadFontByName(const QString &absoluteFileName)
1432 {
1433 // qCWarning(OkularXpsDebug) << "font file name: " << absoluteFileName;
1434
1435 const KArchiveEntry *fontFile = loadEntry(m_xpsArchive, absoluteFileName, Qt::CaseInsensitive);
1436 if (!fontFile) {
1437 return -1;
1438 }
1439
1440 QByteArray fontData = readFileOrDirectoryParts(fontFile); // once per file, according to the docs
1441
1442 int result = m_fontDatabase.addApplicationFontFromData(fontData);
1443 if (-1 == result) {
1444 // Try to deobfuscate font
1445 // TODO Use deobfuscation depending on font content type, don't do it always when standard loading fails
1446
1447 const QString baseName = resourceName(absoluteFileName);
1448
1449 unsigned short guid[16];
1450 if (!parseGUID(baseName, guid)) {
1451 qCWarning(OkularXpsDebug) << "File to load font - file name isn't a GUID";
1452 } else {
1453 if (fontData.length() < 32) {
1454 qCWarning(OkularXpsDebug) << "Font file is too small";
1455 } else {
1456 // Obfuscation - xor bytes in font binary with bytes from guid (font's filename)
1457 const static int mapping[] = {15, 14, 13, 12, 11, 10, 9, 8, 6, 7, 4, 5, 0, 1, 2, 3};
1458 for (int i = 0; i < 16; i++) {
1459 fontData[i] = fontData[i] ^ guid[mapping[i]];
1460 fontData[i + 16] = fontData[i + 16] ^ guid[mapping[i]];
1461 }
1462 result = m_fontDatabase.addApplicationFontFromData(fontData);
1463 }
1464 }
1465 }
1466
1467 // qCWarning(OkularXpsDebug) << "Loaded font: " << m_fontDatabase.applicationFontFamilies( result );
1468
1469 return result; // a font ID
1470 }
1471
xpsArchive()1472 KZip *XpsFile::xpsArchive()
1473 {
1474 return m_xpsArchive;
1475 }
1476
loadImageFromFile(const QString & fileName)1477 QImage XpsPage::loadImageFromFile(const QString &fileName)
1478 {
1479 // qCWarning(OkularXpsDebug) << "image file name: " << fileName;
1480
1481 if (fileName.at(0) == QLatin1Char('{')) {
1482 // for example: '{ColorConvertedBitmap /Resources/bla.wdp /Resources/foobar.icc}'
1483 // TODO: properly read a ColorConvertedBitmap
1484 return QImage();
1485 }
1486
1487 QString absoluteFileName = absolutePath(entryPath(m_fileName), fileName);
1488 const KArchiveEntry *imageFile = loadEntry(m_file->xpsArchive(), absoluteFileName, Qt::CaseInsensitive);
1489 if (!imageFile) {
1490 // image not found
1491 return QImage();
1492 }
1493
1494 /* WORKAROUND:
1495 XPS standard requires to use 96dpi for images which doesn't have dpi specified (in file). When Qt loads such an image,
1496 it sets its dpi to qt_defaultDpi and doesn't allow to find out that it happend.
1497
1498 To workaround this I used this procedure: load image, set its dpi to 96, load image again. When dpi isn't set in file,
1499 dpi set by me stays unchanged.
1500
1501 Trolltech task ID: 159527.
1502
1503 */
1504
1505 QImage image;
1506 QByteArray data = readFileOrDirectoryParts(imageFile);
1507
1508 QBuffer buffer(&data);
1509 buffer.open(QBuffer::ReadOnly);
1510
1511 QImageReader reader(&buffer);
1512 image = reader.read();
1513
1514 image.setDotsPerMeterX(qRound(96 / 0.0254));
1515 image.setDotsPerMeterY(qRound(96 / 0.0254));
1516
1517 buffer.seek(0);
1518 reader.setDevice(&buffer);
1519 reader.read(&image);
1520
1521 return image;
1522 }
1523
textPage()1524 Okular::TextPage *XpsPage::textPage()
1525 {
1526 // qCWarning(OkularXpsDebug) << "Parsing XpsPage, text extraction";
1527
1528 Okular::TextPage *textPage = new Okular::TextPage();
1529
1530 const KZipFileEntry *pageFile = static_cast<const KZipFileEntry *>(m_file->xpsArchive()->directory()->entry(m_fileName));
1531 QXmlStreamReader xml;
1532 xml.addData(readFileOrDirectoryParts(pageFile));
1533
1534 QTransform matrix = QTransform();
1535 QStack<QTransform> matrices;
1536 matrices.push(QTransform());
1537 bool useMatrix = false;
1538 QXmlStreamAttributes glyphsAtts;
1539
1540 while (!xml.atEnd()) {
1541 xml.readNext();
1542 if (xml.isStartElement()) {
1543 if (xml.name() == QStringLiteral("Canvas")) {
1544 matrices.push(matrix);
1545
1546 QString att = xml.attributes().value(QStringLiteral("RenderTransform")).toString();
1547 if (!att.isEmpty()) {
1548 matrix = parseRscRefMatrix(att) * matrix;
1549 }
1550 } else if ((xml.name() == QStringLiteral("Canvas.RenderTransform")) || (xml.name() == QStringLiteral("Glyphs.RenderTransform"))) {
1551 useMatrix = true;
1552 } else if (xml.name() == QStringLiteral("MatrixTransform")) {
1553 if (useMatrix) {
1554 matrix = attsToMatrix(xml.attributes().value(QStringLiteral("Matrix")).toString()) * matrix;
1555 }
1556 } else if (xml.name() == QStringLiteral("Glyphs")) {
1557 matrices.push(matrix);
1558 glyphsAtts = xml.attributes();
1559 } else if ((xml.name() == QStringLiteral("Path")) || (xml.name() == QStringLiteral("Path.Fill")) || (xml.name() == QStringLiteral("SolidColorBrush")) || (xml.name() == QStringLiteral("ImageBrush")) ||
1560 (xml.name() == QStringLiteral("ImageBrush.Transform")) || (xml.name() == QStringLiteral("Path.OpacityMask")) || (xml.name() == QStringLiteral("Path.Data")) || (xml.name() == QStringLiteral("PathGeometry")) ||
1561 (xml.name() == QStringLiteral("PathFigure")) || (xml.name() == QStringLiteral("PolyLineSegment"))) {
1562 // those are only graphical - no use in text handling
1563 } else if ((xml.name() == QStringLiteral("FixedPage")) || (xml.name() == QStringLiteral("FixedPage.Resources"))) {
1564 // not useful for text extraction
1565 } else {
1566 qCWarning(OkularXpsDebug) << "Unhandled element in Text Extraction start: " << xml.name().toString();
1567 }
1568 } else if (xml.isEndElement()) {
1569 if (xml.name() == QStringLiteral("Canvas")) {
1570 matrix = matrices.pop();
1571 } else if ((xml.name() == QStringLiteral("Canvas.RenderTransform")) || (xml.name() == QStringLiteral("Glyphs.RenderTransform"))) {
1572 useMatrix = false;
1573 } else if (xml.name() == QStringLiteral("MatrixTransform")) {
1574 // not clear if we need to do anything here yet.
1575 } else if (xml.name() == QStringLiteral("Glyphs")) {
1576 QString att = glyphsAtts.value(QStringLiteral("RenderTransform")).toString();
1577 if (!att.isEmpty()) {
1578 matrix = parseRscRefMatrix(att) * matrix;
1579 }
1580 QString text = unicodeString(glyphsAtts.value(QStringLiteral("UnicodeString")).toString());
1581
1582 // Get font (doesn't work well because qt doesn't allow to load font from file)
1583 const QString absoluteFileName = absolutePath(entryPath(m_fileName), glyphsAtts.value(QStringLiteral("FontUri")).toString());
1584 QFont font = m_file->getFontByName(absoluteFileName, glyphsAtts.value(QStringLiteral("FontRenderingEmSize")).toString().toFloat() * 72 / 96);
1585 QFontMetrics metrics = QFontMetrics(font);
1586 // Origin
1587 QPointF origin(glyphsAtts.value(QStringLiteral("OriginX")).toString().toDouble(), glyphsAtts.value(QStringLiteral("OriginY")).toString().toDouble());
1588
1589 int lastWidth = 0;
1590 for (int i = 0; i < text.length(); i++) {
1591 const int width = metrics.horizontalAdvance(text, i + 1);
1592
1593 Okular::NormalizedRect *rect =
1594 new Okular::NormalizedRect((origin.x() + lastWidth) / m_pageSize.width(), (origin.y() - metrics.height()) / m_pageSize.height(), (origin.x() + width) / m_pageSize.width(), origin.y() / m_pageSize.height());
1595 rect->transform(matrix);
1596 textPage->append(text.mid(i, 1), rect);
1597
1598 lastWidth = width;
1599 }
1600
1601 matrix = matrices.pop();
1602 } else if ((xml.name() == QStringLiteral("Path")) || (xml.name() == QStringLiteral("Path.Fill")) || (xml.name() == QStringLiteral("SolidColorBrush")) || (xml.name() == QStringLiteral("ImageBrush")) ||
1603 (xml.name() == QStringLiteral("ImageBrush.Transform")) || (xml.name() == QStringLiteral("Path.OpacityMask")) || (xml.name() == QStringLiteral("Path.Data")) || (xml.name() == QStringLiteral("PathGeometry")) ||
1604 (xml.name() == QStringLiteral("PathFigure")) || (xml.name() == QStringLiteral("PolyLineSegment"))) {
1605 // those are only graphical - no use in text handling
1606 } else if ((xml.name() == QStringLiteral("FixedPage")) || (xml.name() == QStringLiteral("FixedPage.Resources"))) {
1607 // not useful for text extraction
1608 } else {
1609 qCWarning(OkularXpsDebug) << "Unhandled element in Text Extraction end: " << xml.name().toString();
1610 }
1611 }
1612 }
1613 if (xml.error()) {
1614 qCWarning(OkularXpsDebug) << "Error parsing XpsPage text: " << xml.errorString();
1615 }
1616 return textPage;
1617 }
1618
parseDocumentStructure(const QString & documentStructureFileName)1619 void XpsDocument::parseDocumentStructure(const QString &documentStructureFileName)
1620 {
1621 qCWarning(OkularXpsDebug) << "document structure file name: " << documentStructureFileName;
1622 m_haveDocumentStructure = false;
1623
1624 const KZipFileEntry *documentStructureFile = static_cast<const KZipFileEntry *>(m_file->xpsArchive()->directory()->entry(documentStructureFileName));
1625
1626 QXmlStreamReader xml;
1627 xml.addData(documentStructureFile->data());
1628
1629 while (!xml.atEnd()) {
1630 xml.readNext();
1631
1632 if (xml.isStartElement()) {
1633 if (xml.name() == QStringLiteral("DocumentStructure")) {
1634 // just a container for optional outline and story elements - nothing to do here
1635 } else if (xml.name() == QStringLiteral("DocumentStructure.Outline")) {
1636 qCWarning(OkularXpsDebug) << "found DocumentStructure.Outline";
1637 } else if (xml.name() == QStringLiteral("DocumentOutline")) {
1638 qCWarning(OkularXpsDebug) << "found DocumentOutline";
1639 m_docStructure = new Okular::DocumentSynopsis;
1640 } else if (xml.name() == QStringLiteral("OutlineEntry")) {
1641 m_haveDocumentStructure = true;
1642 QXmlStreamAttributes attributes = xml.attributes();
1643 int outlineLevel = attributes.value(QStringLiteral("OutlineLevel")).toString().toInt();
1644 QString description = attributes.value(QStringLiteral("Description")).toString();
1645 QDomElement synopsisElement = m_docStructure->createElement(description);
1646 synopsisElement.setAttribute(QStringLiteral("OutlineLevel"), outlineLevel);
1647 QString target = attributes.value(QStringLiteral("OutlineTarget")).toString();
1648 int hashPosition = target.lastIndexOf(QLatin1Char('#'));
1649 target = target.mid(hashPosition + 1);
1650 // qCWarning(OkularXpsDebug) << "target: " << target;
1651 Okular::DocumentViewport viewport;
1652 viewport.pageNumber = m_docStructurePageMap.value(target);
1653 synopsisElement.setAttribute(QStringLiteral("Viewport"), viewport.toString());
1654 if (outlineLevel == 1) {
1655 // qCWarning(OkularXpsDebug) << "Description: "
1656 // << outlineEntryElement.attribute( "Description" );
1657 m_docStructure->appendChild(synopsisElement);
1658 } else {
1659 // find the last next highest element (so it this is level 3, we need
1660 // to find the most recent level 2 node)
1661 QDomNode maybeParentNode = m_docStructure->lastChild();
1662 while (!maybeParentNode.isNull()) {
1663 if (maybeParentNode.toElement().attribute(QStringLiteral("OutlineLevel")).toInt() == (outlineLevel - 1)) {
1664 // we have the right parent
1665 maybeParentNode.appendChild(synopsisElement);
1666 break;
1667 }
1668 maybeParentNode = maybeParentNode.lastChild();
1669 }
1670 }
1671 } else if (xml.name() == QStringLiteral("Story")) {
1672 // we need to handle Story here, but I have no idea what to do with it.
1673 } else if (xml.name() == QStringLiteral("StoryFragment")) {
1674 // we need to handle StoryFragment here, but I have no idea what to do with it.
1675 } else if (xml.name() == QStringLiteral("StoryFragmentReference")) {
1676 // we need to handle StoryFragmentReference here, but I have no idea what to do with it.
1677 } else {
1678 qCWarning(OkularXpsDebug) << "Unhandled entry in DocumentStructure: " << xml.name().toString();
1679 }
1680 }
1681 }
1682 }
1683
documentStructure()1684 const Okular::DocumentSynopsis *XpsDocument::documentStructure()
1685 {
1686 return m_docStructure;
1687 }
1688
hasDocumentStructure()1689 bool XpsDocument::hasDocumentStructure()
1690 {
1691 return m_haveDocumentStructure;
1692 }
1693
XpsDocument(XpsFile * file,const QString & fileName)1694 XpsDocument::XpsDocument(XpsFile *file, const QString &fileName)
1695 : m_file(file)
1696 , m_haveDocumentStructure(false)
1697 , m_docStructure(nullptr)
1698 {
1699 qCWarning(OkularXpsDebug) << "document file name: " << fileName;
1700
1701 const KArchiveEntry *documentEntry = file->xpsArchive()->directory()->entry(fileName);
1702 QString documentFilePath = fileName;
1703 const QString documentEntryPath = entryPath(fileName);
1704
1705 QXmlStreamReader docXml;
1706 docXml.addData(readFileOrDirectoryParts(documentEntry, &documentFilePath));
1707 while (!docXml.atEnd()) {
1708 docXml.readNext();
1709 if (docXml.isStartElement()) {
1710 if (docXml.name() == QStringLiteral("PageContent")) {
1711 QString pagePath = docXml.attributes().value(QStringLiteral("Source")).toString();
1712 qCWarning(OkularXpsDebug) << "Page Path: " << pagePath;
1713 XpsPage *page = new XpsPage(file, absolutePath(documentFilePath, pagePath));
1714 m_pages.append(page);
1715 } else if (docXml.name() == QStringLiteral("PageContent.LinkTargets")) {
1716 // do nothing - wait for the real LinkTarget elements
1717 } else if (docXml.name() == QStringLiteral("LinkTarget")) {
1718 QString targetName = docXml.attributes().value(QStringLiteral("Name")).toString();
1719 if (!targetName.isEmpty()) {
1720 m_docStructurePageMap[targetName] = m_pages.count() - 1;
1721 }
1722 } else if (docXml.name() == QStringLiteral("FixedDocument")) {
1723 // we just ignore this - it is just a container
1724 } else {
1725 qCWarning(OkularXpsDebug) << "Unhandled entry in FixedDocument: " << docXml.name().toString();
1726 }
1727 }
1728 }
1729 if (docXml.error()) {
1730 qCWarning(OkularXpsDebug) << "Could not parse main XPS document file: " << docXml.errorString();
1731 }
1732
1733 // There might be a relationships entry for this document - typically used to tell us where to find the
1734 // content structure description
1735
1736 // We should be able to find this using a reference from some other part of the document, but I can't see it.
1737 const int slashPosition = fileName.lastIndexOf(QLatin1Char('/'));
1738 const QString documentRelationshipFile = absolutePath(documentEntryPath, QStringLiteral("_rels/") + fileName.mid(slashPosition + 1) + QStringLiteral(".rels"));
1739
1740 const KZipFileEntry *relFile = static_cast<const KZipFileEntry *>(file->xpsArchive()->directory()->entry(documentRelationshipFile));
1741
1742 QString documentStructureFile;
1743 if (relFile) {
1744 QXmlStreamReader xml;
1745 xml.addData(readFileOrDirectoryParts(relFile));
1746 while (!xml.atEnd()) {
1747 xml.readNext();
1748 if (xml.isStartElement() && (xml.name() == QStringLiteral("Relationship"))) {
1749 QXmlStreamAttributes attributes = xml.attributes();
1750 if (attributes.value(QStringLiteral("Type")).toString() == QLatin1String("http://schemas.microsoft.com/xps/2005/06/documentstructure")) {
1751 documentStructureFile = attributes.value(QStringLiteral("Target")).toString();
1752 } else {
1753 qCWarning(OkularXpsDebug) << "Unknown document relationships element: " << attributes.value(QStringLiteral("Type")).toString() << " : " << attributes.value(QStringLiteral("Target")).toString();
1754 }
1755 }
1756 }
1757 if (xml.error()) {
1758 qCWarning(OkularXpsDebug) << "Could not parse XPS page relationships file ( " << documentRelationshipFile << " ) - " << xml.errorString();
1759 }
1760 } else { // the page relationship file didn't exist in the zipfile
1761 // this isn't fatal
1762 qCWarning(OkularXpsDebug) << "Could not open Document relationship file from " << documentRelationshipFile;
1763 }
1764
1765 if (!documentStructureFile.isEmpty()) {
1766 // qCWarning(OkularXpsDebug) << "Document structure filename: " << documentStructureFile;
1767 // make the document path absolute
1768 documentStructureFile = absolutePath(documentEntryPath, documentStructureFile);
1769 // qCWarning(OkularXpsDebug) << "Document structure absolute path: " << documentStructureFile;
1770 parseDocumentStructure(documentStructureFile);
1771 }
1772 }
1773
~XpsDocument()1774 XpsDocument::~XpsDocument()
1775 {
1776 qDeleteAll(m_pages);
1777 m_pages.clear();
1778
1779 if (m_docStructure)
1780 delete m_docStructure;
1781 }
1782
numPages() const1783 int XpsDocument::numPages() const
1784 {
1785 return m_pages.size();
1786 }
1787
page(int pageNum) const1788 XpsPage *XpsDocument::page(int pageNum) const
1789 {
1790 return m_pages.at(pageNum);
1791 }
1792
XpsFile()1793 XpsFile::XpsFile()
1794 {
1795 }
1796
~XpsFile()1797 XpsFile::~XpsFile()
1798 {
1799 for (int fontId : qAsConst(m_fontCache)) {
1800 m_fontDatabase.removeApplicationFont(fontId);
1801 }
1802 }
1803
loadDocument(const QString & filename)1804 bool XpsFile::loadDocument(const QString &filename)
1805 {
1806 m_xpsArchive = new KZip(filename);
1807 if (m_xpsArchive->open(QIODevice::ReadOnly) == true) {
1808 qCWarning(OkularXpsDebug) << "Successful open of " << m_xpsArchive->fileName();
1809 } else {
1810 qCWarning(OkularXpsDebug) << "Could not open XPS archive: " << m_xpsArchive->fileName();
1811 delete m_xpsArchive;
1812 return false;
1813 }
1814
1815 // The only fixed entry in XPS is /_rels/.rels
1816 const KArchiveEntry *relEntry = m_xpsArchive->directory()->entry(QStringLiteral("_rels/.rels"));
1817 if (!relEntry) {
1818 // this might occur if we can't read the zip directory, or it doesn't have the relationships entry
1819 return false;
1820 }
1821
1822 QXmlStreamReader relXml;
1823 relXml.addData(readFileOrDirectoryParts(relEntry));
1824
1825 QString fixedRepresentationFileName;
1826 // We work through the relationships document and pull out each element.
1827 while (!relXml.atEnd()) {
1828 relXml.readNext();
1829 if (relXml.isStartElement()) {
1830 if (relXml.name() == QStringLiteral("Relationship")) {
1831 QXmlStreamAttributes attributes = relXml.attributes();
1832 QString type = attributes.value(QStringLiteral("Type")).toString();
1833 QString target = attributes.value(QStringLiteral("Target")).toString();
1834 if (QStringLiteral("http://schemas.openxmlformats.org/package/2006/relationships/metadata/thumbnail") == type) {
1835 m_thumbnailFileName = target;
1836 } else if (QStringLiteral("http://schemas.microsoft.com/xps/2005/06/fixedrepresentation") == type) {
1837 fixedRepresentationFileName = target;
1838 } else if (QStringLiteral("http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties") == type) {
1839 m_corePropertiesFileName = target;
1840 } else if (QStringLiteral("http://schemas.openxmlformats.org/package/2006/relationships/digital-signature/origin") == type) {
1841 m_signatureOrigin = target;
1842 } else {
1843 qCWarning(OkularXpsDebug) << "Unknown relationships element: " << type << " : " << target;
1844 }
1845 } else if (relXml.name() == QStringLiteral("Relationships")) {
1846 // nothing to do here - this is just the container level
1847 } else {
1848 qCWarning(OkularXpsDebug) << "unexpected element in _rels/.rels: " << relXml.name().toString();
1849 }
1850 }
1851 }
1852 if (relXml.error()) {
1853 qCWarning(OkularXpsDebug) << "Could not parse _rels/.rels: " << relXml.errorString();
1854 return false;
1855 }
1856
1857 if (fixedRepresentationFileName.isEmpty()) {
1858 // FixedRepresentation is a required part of the XPS document
1859 return false;
1860 }
1861
1862 const KArchiveEntry *fixedRepEntry = m_xpsArchive->directory()->entry(fixedRepresentationFileName);
1863 QString fixedRepresentationFilePath = fixedRepresentationFileName;
1864
1865 QXmlStreamReader fixedRepXml;
1866 fixedRepXml.addData(readFileOrDirectoryParts(fixedRepEntry, &fixedRepresentationFileName));
1867
1868 while (!fixedRepXml.atEnd()) {
1869 fixedRepXml.readNext();
1870 if (fixedRepXml.isStartElement()) {
1871 if (fixedRepXml.name() == QStringLiteral("DocumentReference")) {
1872 const QString source = fixedRepXml.attributes().value(QStringLiteral("Source")).toString();
1873 XpsDocument *doc = new XpsDocument(this, absolutePath(fixedRepresentationFilePath, source));
1874 for (int lv = 0; lv < doc->numPages(); ++lv) {
1875 // our own copy of the pages list
1876 m_pages.append(doc->page(lv));
1877 }
1878 m_documents.append(doc);
1879 } else if (fixedRepXml.name() == QStringLiteral("FixedDocumentSequence")) {
1880 // we don't do anything here - this is just a container for one or more DocumentReference elements
1881 } else {
1882 qCWarning(OkularXpsDebug) << "Unhandled entry in FixedDocumentSequence: " << fixedRepXml.name().toString();
1883 }
1884 }
1885 }
1886 if (fixedRepXml.error()) {
1887 qCWarning(OkularXpsDebug) << "Could not parse FixedRepresentation file:" << fixedRepXml.errorString();
1888 return false;
1889 }
1890
1891 return true;
1892 }
1893
generateDocumentInfo() const1894 Okular::DocumentInfo XpsFile::generateDocumentInfo() const
1895 {
1896 Okular::DocumentInfo docInfo;
1897
1898 docInfo.set(Okular::DocumentInfo::MimeType, QStringLiteral("application/oxps"));
1899
1900 if (!m_corePropertiesFileName.isEmpty()) {
1901 const KZipFileEntry *corepropsFile = static_cast<const KZipFileEntry *>(m_xpsArchive->directory()->entry(m_corePropertiesFileName));
1902
1903 QXmlStreamReader xml;
1904 xml.addData(corepropsFile->data());
1905 while (!xml.atEnd()) {
1906 xml.readNext();
1907 if (xml.isEndElement())
1908 break;
1909 if (xml.isStartElement()) {
1910 if (xml.name() == QStringLiteral("title")) {
1911 docInfo.set(Okular::DocumentInfo::Title, xml.readElementText());
1912 } else if (xml.name() == QStringLiteral("subject")) {
1913 docInfo.set(Okular::DocumentInfo::Subject, xml.readElementText());
1914 } else if (xml.name() == QStringLiteral("description")) {
1915 docInfo.set(Okular::DocumentInfo::Description, xml.readElementText());
1916 } else if (xml.name() == QStringLiteral("creator")) {
1917 docInfo.set(Okular::DocumentInfo::Creator, xml.readElementText());
1918 } else if (xml.name() == QStringLiteral("category")) {
1919 docInfo.set(Okular::DocumentInfo::Category, xml.readElementText());
1920 } else if (xml.name() == QStringLiteral("created")) {
1921 QDateTime createdDate = QDateTime::fromString(xml.readElementText(), QStringLiteral("yyyy-MM-ddThh:mm:ssZ"));
1922 docInfo.set(Okular::DocumentInfo::CreationDate, QLocale().toString(createdDate, QLocale::LongFormat));
1923 } else if (xml.name() == QStringLiteral("modified")) {
1924 QDateTime modifiedDate = QDateTime::fromString(xml.readElementText(), QStringLiteral("yyyy-MM-ddThh:mm:ssZ"));
1925 docInfo.set(Okular::DocumentInfo::ModificationDate, QLocale().toString(modifiedDate, QLocale::LongFormat));
1926 } else if (xml.name() == QStringLiteral("keywords")) {
1927 docInfo.set(Okular::DocumentInfo::Keywords, xml.readElementText());
1928 } else if (xml.name() == QStringLiteral("revision")) {
1929 docInfo.set(QStringLiteral("revision"), xml.readElementText(), i18n("Revision"));
1930 }
1931 }
1932 }
1933 if (xml.error()) {
1934 qCWarning(OkularXpsDebug) << "Could not parse XPS core properties:" << xml.errorString();
1935 }
1936 } else {
1937 qCWarning(OkularXpsDebug) << "No core properties filename";
1938 }
1939
1940 docInfo.set(Okular::DocumentInfo::Pages, QString::number(numPages()));
1941
1942 return docInfo;
1943 }
1944
closeDocument()1945 bool XpsFile::closeDocument()
1946 {
1947 qDeleteAll(m_documents);
1948 m_documents.clear();
1949
1950 delete m_xpsArchive;
1951
1952 return true;
1953 }
1954
numPages() const1955 int XpsFile::numPages() const
1956 {
1957 return m_pages.size();
1958 }
1959
numDocuments() const1960 int XpsFile::numDocuments() const
1961 {
1962 return m_documents.size();
1963 }
1964
document(int documentNum) const1965 XpsDocument *XpsFile::document(int documentNum) const
1966 {
1967 return m_documents.at(documentNum);
1968 }
1969
page(int pageNum) const1970 XpsPage *XpsFile::page(int pageNum) const
1971 {
1972 return m_pages.at(pageNum);
1973 }
1974
XpsGenerator(QObject * parent,const QVariantList & args)1975 XpsGenerator::XpsGenerator(QObject *parent, const QVariantList &args)
1976 : Okular::Generator(parent, args)
1977 , m_xpsFile(nullptr)
1978 {
1979 setFeature(TextExtraction);
1980 setFeature(PrintNative);
1981 setFeature(PrintToFile);
1982 setFeature(Threaded);
1983 userMutex();
1984 }
1985
~XpsGenerator()1986 XpsGenerator::~XpsGenerator()
1987 {
1988 }
1989
loadDocument(const QString & fileName,QVector<Okular::Page * > & pagesVector)1990 bool XpsGenerator::loadDocument(const QString &fileName, QVector<Okular::Page *> &pagesVector)
1991 {
1992 m_xpsFile = new XpsFile();
1993
1994 m_xpsFile->loadDocument(fileName);
1995 pagesVector.resize(m_xpsFile->numPages());
1996
1997 int pagesVectorOffset = 0;
1998
1999 for (int docNum = 0; docNum < m_xpsFile->numDocuments(); ++docNum) {
2000 XpsDocument *doc = m_xpsFile->document(docNum);
2001 for (int pageNum = 0; pageNum < doc->numPages(); ++pageNum) {
2002 QSizeF pageSize = doc->page(pageNum)->size();
2003 pagesVector[pagesVectorOffset] = new Okular::Page(pagesVectorOffset, pageSize.width(), pageSize.height(), Okular::Rotation0);
2004 ++pagesVectorOffset;
2005 }
2006 }
2007
2008 return true;
2009 }
2010
doCloseDocument()2011 bool XpsGenerator::doCloseDocument()
2012 {
2013 m_xpsFile->closeDocument();
2014 delete m_xpsFile;
2015 m_xpsFile = nullptr;
2016
2017 return true;
2018 }
2019
image(Okular::PixmapRequest * request)2020 QImage XpsGenerator::image(Okular::PixmapRequest *request)
2021 {
2022 QMutexLocker lock(userMutex());
2023 QSize size((int)request->width(), (int)request->height());
2024 QImage image(size, QImage::Format_RGB32);
2025 XpsPage *pageToRender = m_xpsFile->page(request->page()->number());
2026 pageToRender->renderToImage(&image);
2027 return image;
2028 }
2029
textPage(Okular::TextRequest * request)2030 Okular::TextPage *XpsGenerator::textPage(Okular::TextRequest *request)
2031 {
2032 QMutexLocker lock(userMutex());
2033 XpsPage *xpsPage = m_xpsFile->page(request->page()->number());
2034 return xpsPage->textPage();
2035 }
2036
generateDocumentInfo(const QSet<Okular::DocumentInfo::Key> & keys) const2037 Okular::DocumentInfo XpsGenerator::generateDocumentInfo(const QSet<Okular::DocumentInfo::Key> &keys) const
2038 {
2039 Q_UNUSED(keys);
2040
2041 qCWarning(OkularXpsDebug) << "generating document metadata";
2042
2043 return m_xpsFile->generateDocumentInfo();
2044 }
2045
generateDocumentSynopsis()2046 const Okular::DocumentSynopsis *XpsGenerator::generateDocumentSynopsis()
2047 {
2048 qCWarning(OkularXpsDebug) << "generating document synopsis";
2049
2050 // we only generate the synopsis for the first file.
2051 if (!m_xpsFile || !m_xpsFile->document(0))
2052 return nullptr;
2053
2054 if (m_xpsFile->document(0)->hasDocumentStructure())
2055 return m_xpsFile->document(0)->documentStructure();
2056
2057 return nullptr;
2058 }
2059
exportFormats() const2060 Okular::ExportFormat::List XpsGenerator::exportFormats() const
2061 {
2062 static Okular::ExportFormat::List formats;
2063 if (formats.isEmpty()) {
2064 formats.append(Okular::ExportFormat::standardFormat(Okular::ExportFormat::PlainText));
2065 }
2066 return formats;
2067 }
2068
exportTo(const QString & fileName,const Okular::ExportFormat & format)2069 bool XpsGenerator::exportTo(const QString &fileName, const Okular::ExportFormat &format)
2070 {
2071 if (format.mimeType().inherits(QStringLiteral("text/plain"))) {
2072 QFile f(fileName);
2073 if (!f.open(QIODevice::WriteOnly))
2074 return false;
2075
2076 QTextStream ts(&f);
2077 for (int i = 0; i < m_xpsFile->numPages(); ++i) {
2078 Okular::TextPage *textPage = m_xpsFile->page(i)->textPage();
2079 QString text = textPage->text();
2080 ts << text;
2081 ts << QLatin1Char('\n');
2082 delete textPage;
2083 }
2084 f.close();
2085
2086 return true;
2087 }
2088
2089 return false;
2090 }
2091
print(QPrinter & printer)2092 bool XpsGenerator::print(QPrinter &printer)
2093 {
2094 QList<int> pageList = Okular::FilePrinter::pageList(printer, document()->pages(), document()->currentPage() + 1, document()->bookmarkedPageList());
2095
2096 QPainter painter(&printer);
2097
2098 for (int i = 0; i < pageList.count(); ++i) {
2099 if (i != 0)
2100 printer.newPage();
2101
2102 const int page = pageList.at(i) - 1;
2103 XpsPage *pageToRender = m_xpsFile->page(page);
2104 pageToRender->renderToPainter(&painter);
2105 }
2106
2107 return true;
2108 }
2109
findChild(const QString & name) const2110 const XpsRenderNode *XpsRenderNode::findChild(const QString &name) const
2111 {
2112 for (const XpsRenderNode &child : children) {
2113 if (child.name == name) {
2114 return &child;
2115 }
2116 }
2117
2118 return nullptr;
2119 }
2120
getRequiredChildData(const QString & name) const2121 QVariant XpsRenderNode::getRequiredChildData(const QString &name) const
2122 {
2123 const XpsRenderNode *child = findChild(name);
2124 if (child == nullptr) {
2125 qCWarning(OkularXpsDebug) << "Required element " << name << " is missing in " << this->name;
2126 return QVariant();
2127 }
2128
2129 return child->data;
2130 }
2131
getChildData(const QString & name) const2132 QVariant XpsRenderNode::getChildData(const QString &name) const
2133 {
2134 const XpsRenderNode *child = findChild(name);
2135 if (child == nullptr) {
2136 return QVariant();
2137 } else {
2138 return child->data;
2139 }
2140 }
2141
2142 Q_LOGGING_CATEGORY(OkularXpsDebug, "org.kde.okular.generators.xps", QtWarningMsg)
2143
2144 #include "generator_xps.moc"
2145