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