File indexing completed on 2024-05-12 16:06:50

0001 /*
0002     SPDX-FileCopyrightText: 2006, 2009 Brad Hards <bradh@frogmouth.net>
0003 
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 
0007 #include "generator_xps.h"
0008 
0009 #include <KAboutData>
0010 #include <KLocalizedString>
0011 #include <QBuffer>
0012 #include <QDateTime>
0013 #include <QFile>
0014 #include <QImageReader>
0015 #include <QList>
0016 #include <QMutex>
0017 #include <QPainter>
0018 #include <QPrinter>
0019 #include <QUrl>
0020 
0021 #include <core/area.h>
0022 #include <core/document.h>
0023 #include <core/fileprinter.h>
0024 #include <core/page.h>
0025 
0026 OKULAR_EXPORT_PLUGIN(XpsGenerator, "libokularGenerator_xps.json")
0027 
0028 Q_DECLARE_METATYPE(QGradient *)
0029 Q_DECLARE_METATYPE(XpsPathFigure *)
0030 Q_DECLARE_METATYPE(XpsPathGeometry *)
0031 
0032 // From Qt4
0033 static int hex2int(char hex)
0034 {
0035     QChar hexchar = QLatin1Char(hex);
0036     int v;
0037     if (hexchar.isDigit()) {
0038         v = hexchar.digitValue();
0039     } else if (hexchar >= QLatin1Char('A') && hexchar <= QLatin1Char('F')) {
0040         v = hexchar.cell() - 'A' + 10;
0041     } else if (hexchar >= QLatin1Char('a') && hexchar <= QLatin1Char('f')) {
0042         v = hexchar.cell() - 'a' + 10;
0043     } else {
0044         v = -1;
0045     }
0046     return v;
0047 }
0048 
0049 // Modified from Qt4
0050 static QColor hexToRgba(const QByteArray &name)
0051 {
0052     const int len = name.length();
0053     if (len == 0 || name[0] != '#') {
0054         return QColor();
0055     }
0056     int r, g, b;
0057     int a = 255;
0058     if (len == 7) {
0059         r = (hex2int(name[1]) << 4) + hex2int(name[2]);
0060         g = (hex2int(name[3]) << 4) + hex2int(name[4]);
0061         b = (hex2int(name[5]) << 4) + hex2int(name[6]);
0062     } else if (len == 9) {
0063         a = (hex2int(name[1]) << 4) + hex2int(name[2]);
0064         r = (hex2int(name[3]) << 4) + hex2int(name[4]);
0065         g = (hex2int(name[5]) << 4) + hex2int(name[6]);
0066         b = (hex2int(name[7]) << 4) + hex2int(name[8]);
0067     } else {
0068         r = g = b = -1;
0069     }
0070     if ((uint)r > 255 || (uint)g > 255 || (uint)b > 255) {
0071         return QColor();
0072     }
0073     return QColor(r, g, b, a);
0074 }
0075 
0076 static QRectF stringToRectF(const QString &data)
0077 {
0078     QStringList numbers = data.split(QLatin1Char(','));
0079     QPointF origin(numbers.at(0).toDouble(), numbers.at(1).toDouble());
0080     QSizeF size(numbers.at(2).toDouble(), numbers.at(3).toDouble());
0081     return QRectF(origin, size);
0082 }
0083 
0084 static bool parseGUID(const QString &guidString, unsigned short guid[16])
0085 {
0086     if (guidString.length() <= 35) {
0087         return false;
0088     }
0089 
0090     // Maps bytes to positions in guidString
0091     const static int indexes[] = {6, 4, 2, 0, 11, 9, 16, 14, 19, 21, 24, 26, 28, 30, 32, 34};
0092 
0093     for (int i = 0; i < 16; i++) {
0094         int hex1 = hex2int(guidString[indexes[i]].cell());
0095         int hex2 = hex2int(guidString[indexes[i] + 1].cell());
0096 
0097         if ((hex1 < 0) || (hex2 < 0)) {
0098             return false;
0099         }
0100 
0101         guid[i] = hex1 * 16 + hex2;
0102     }
0103 
0104     return true;
0105 }
0106 
0107 // Read next token of abbreviated path data
0108 static bool nextAbbPathToken(AbbPathToken *token)
0109 {
0110     int *curPos = &token->curPos;
0111     QString data = token->data;
0112 
0113     while ((*curPos < data.length()) && (data.at(*curPos).isSpace())) {
0114         (*curPos)++;
0115     }
0116 
0117     if (*curPos == data.length()) {
0118         token->type = abtEOF;
0119         return true;
0120     }
0121 
0122     QChar ch = data.at(*curPos);
0123 
0124     if (ch.isNumber() || (ch == QLatin1Char('+')) || (ch == QLatin1Char('-'))) {
0125         int start = *curPos;
0126         while ((*curPos < data.length()) && (!data.at(*curPos).isSpace()) && (data.at(*curPos) != QLatin1Char(',') && (!data.at(*curPos).isLetter() || data.at(*curPos) == QLatin1Char('e')))) {
0127             (*curPos)++;
0128         }
0129         token->number = QStringView {data}.mid(start, *curPos - start).toDouble();
0130         token->type = abtNumber;
0131 
0132     } else if (ch == QLatin1Char(',')) {
0133         token->type = abtComma;
0134         (*curPos)++;
0135     } else if (ch.isLetter()) {
0136         token->type = abtCommand;
0137         token->command = data.at(*curPos).cell();
0138         (*curPos)++;
0139     } else {
0140         (*curPos)++;
0141         return false;
0142     }
0143 
0144     return true;
0145 }
0146 
0147 /**
0148     Read point (two reals delimited by comma) from abbreviated path data
0149 */
0150 static QPointF getPointFromString(AbbPathToken *token, bool relative, const QPointF currentPosition)
0151 {
0152     // TODO Check grammar
0153 
0154     QPointF result;
0155     result.rx() = token->number;
0156     nextAbbPathToken(token);
0157     nextAbbPathToken(token); // ,
0158     result.ry() = token->number;
0159     nextAbbPathToken(token);
0160 
0161     if (relative) {
0162         result += currentPosition;
0163     }
0164 
0165     return result;
0166 }
0167 
0168 /**
0169     Read point (two reals delimited by comma) from string
0170 */
0171 static QPointF getPointFromString(QStringView string)
0172 {
0173     const int commaPos = string.indexOf(QLatin1Char(QLatin1Char(',')));
0174     if (commaPos == -1 || string.indexOf(QLatin1Char(QLatin1Char(',')), commaPos + 1) != -1) {
0175         return QPointF();
0176     }
0177 
0178     QPointF result;
0179     bool ok = false;
0180     QStringView ref = string.mid(0, commaPos);
0181     result.setX(ref.toDouble(&ok));
0182     if (!ok) {
0183         return QPointF();
0184     }
0185 
0186     ref = string.mid(commaPos + 1);
0187     result.setY(ref.toDouble(&ok));
0188     if (!ok) {
0189         return QPointF();
0190     }
0191 
0192     return result;
0193 }
0194 
0195 static Qt::FillRule fillRuleFromString(const QString &data, Qt::FillRule def = Qt::OddEvenFill)
0196 {
0197     if (data == QLatin1String("EvenOdd")) {
0198         return Qt::OddEvenFill;
0199     } else if (data == QLatin1String("NonZero")) {
0200         return Qt::WindingFill;
0201     }
0202     return def;
0203 }
0204 
0205 /**
0206     Parse an abbreviated path "Data" description
0207     \param data the string containing the whitespace separated values
0208 
0209     \see XPS specification 4.2.3 and Appendix G
0210 */
0211 static QPainterPath parseAbbreviatedPathData(const QString &data)
0212 {
0213     QPainterPath path;
0214 
0215     AbbPathToken token;
0216 
0217     token.data = data;
0218     token.curPos = 0;
0219 
0220     nextAbbPathToken(&token);
0221 
0222     // Used by Smooth cubic curve (command s)
0223     char lastCommand = ' ';
0224     QPointF lastSecondControlPoint;
0225 
0226     while (true) {
0227         if (token.type != abtCommand) {
0228             if (token.type != abtEOF) {
0229                 qCWarning(OkularXpsDebug).nospace() << "Error in parsing abbreviated path data (" << token.type << "@" << token.curPos << "): " << data;
0230             }
0231             return path;
0232         }
0233 
0234         char command = QChar::fromLatin1(token.command).toLower().cell();
0235         bool isRelative = QChar::fromLatin1(token.command).isLower();
0236         QPointF currPos = path.currentPosition();
0237         nextAbbPathToken(&token);
0238 
0239         switch (command) {
0240         case 'f':
0241             int rule;
0242             rule = (int)token.number;
0243             if (rule == 0) {
0244                 path.setFillRule(Qt::OddEvenFill);
0245             } else if (rule == 1) {
0246                 // In xps specs rule 1 means NonZero fill. I think it's equivalent to WindingFill but I'm not sure
0247                 path.setFillRule(Qt::WindingFill);
0248             }
0249             nextAbbPathToken(&token);
0250             break;
0251         case 'm': // Move
0252             while (token.type == abtNumber) {
0253                 QPointF point = getPointFromString(&token, isRelative, currPos);
0254                 path.moveTo(point);
0255             }
0256             break;
0257         case 'l': // Line
0258             while (token.type == abtNumber) {
0259                 QPointF point = getPointFromString(&token, isRelative, currPos);
0260                 path.lineTo(point);
0261             }
0262             break;
0263         case 'h': // Horizontal line
0264             while (token.type == abtNumber) {
0265                 double x = token.number;
0266                 if (isRelative) {
0267                     x += path.currentPosition().x();
0268                 }
0269                 path.lineTo(x, path.currentPosition().y());
0270                 nextAbbPathToken(&token);
0271             }
0272             break;
0273         case 'v': // Vertical line
0274             while (token.type == abtNumber) {
0275                 double y = token.number;
0276                 if (isRelative) {
0277                     y += path.currentPosition().y();
0278                 }
0279                 path.lineTo(path.currentPosition().x(), y);
0280                 nextAbbPathToken(&token);
0281             }
0282             break;
0283         case 'c': // Cubic bezier curve
0284             while (token.type == abtNumber) {
0285                 QPointF firstControl = getPointFromString(&token, isRelative, currPos);
0286                 QPointF secondControl = getPointFromString(&token, isRelative, currPos);
0287                 QPointF endPoint = getPointFromString(&token, isRelative, currPos);
0288                 path.cubicTo(firstControl, secondControl, endPoint);
0289 
0290                 lastSecondControlPoint = secondControl;
0291             }
0292             break;
0293         case 'q': // Quadratic bezier curve
0294             while (token.type == abtNumber) {
0295                 QPointF point1 = getPointFromString(&token, isRelative, currPos);
0296                 QPointF point2 = getPointFromString(&token, isRelative, currPos);
0297                 path.quadTo(point1, point2);
0298             }
0299             break;
0300         case 's': // Smooth cubic bezier curve
0301             while (token.type == abtNumber) {
0302                 QPointF firstControl;
0303                 if ((lastCommand == 's') || (lastCommand == 'c')) {
0304                     firstControl = lastSecondControlPoint + (lastSecondControlPoint + path.currentPosition());
0305                 } else {
0306                     firstControl = path.currentPosition();
0307                 }
0308                 QPointF secondControl = getPointFromString(&token, isRelative, currPos);
0309                 QPointF endPoint = getPointFromString(&token, isRelative, currPos);
0310                 path.cubicTo(firstControl, secondControl, endPoint);
0311             }
0312             break;
0313         case 'a': // Arc
0314             // TODO Implement Arc drawing
0315             while (token.type == abtNumber) {
0316                 /*QPointF rp =*/getPointFromString(&token, isRelative, currPos);
0317                 /*double r = token.number;*/
0318                 nextAbbPathToken(&token);
0319                 /*double fArc = token.number; */
0320                 nextAbbPathToken(&token);
0321                 /*double fSweep = token.number; */
0322                 nextAbbPathToken(&token);
0323                 /*QPointF point = */ getPointFromString(&token, isRelative, currPos);
0324             }
0325             break;
0326         case 'z': // Close path
0327             path.closeSubpath();
0328             break;
0329         }
0330 
0331         lastCommand = command;
0332     }
0333 
0334     return path;
0335 }
0336 
0337 /**
0338    Parse a "Matrix" attribute string
0339    \param csv the comma separated list of values
0340    \return the QTransform corresponding to the affine transform
0341    given in the attribute
0342 
0343    \see XPS specification 7.4.1
0344 */
0345 static QTransform attsToMatrix(const QString &csv)
0346 {
0347     QStringList values = csv.split(QLatin1Char(','));
0348     if (values.count() != 6) {
0349         return QTransform(); // that is an identity matrix - no effect
0350     }
0351     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());
0352 }
0353 
0354 /**
0355    \return Brush with given color or brush specified by reference to resource
0356 */
0357 static QBrush parseRscRefColorForBrush(const QString &data)
0358 {
0359     if (data[0] == QLatin1Char('{')) {
0360         // TODO
0361         qCWarning(OkularXpsDebug) << "Reference" << data;
0362         return QBrush();
0363     } else {
0364         return QBrush(hexToRgba(data.toLatin1()));
0365     }
0366 }
0367 
0368 /**
0369    \return Pen with given color or Pen specified by reference to resource
0370 */
0371 static QPen parseRscRefColorForPen(const QString &data)
0372 {
0373     if (data[0] == QLatin1Char('{')) {
0374         // TODO
0375         qCWarning(OkularXpsDebug) << "Reference" << data;
0376         return QPen();
0377     } else {
0378         return QPen(hexToRgba(data.toLatin1()));
0379     }
0380 }
0381 
0382 /**
0383    \return Matrix specified by given data or by referenced dictionary
0384 */
0385 static QTransform parseRscRefMatrix(const QString &data)
0386 {
0387     if (data[0] == QLatin1Char('{')) {
0388         // TODO
0389         qCWarning(OkularXpsDebug) << "Reference" << data;
0390         return QTransform();
0391     } else {
0392         return attsToMatrix(data);
0393     }
0394 }
0395 
0396 /**
0397    \return Path specified by given data or by referenced dictionary
0398 */
0399 static QPainterPath parseRscRefPath(const QString &data)
0400 {
0401     if (data[0] == QLatin1Char('{')) {
0402         // TODO
0403         qCWarning(OkularXpsDebug) << "Reference" << data;
0404         return QPainterPath();
0405     } else {
0406         return parseAbbreviatedPathData(data);
0407     }
0408 }
0409 
0410 /**
0411    \return The path of the entry
0412 */
0413 static QString entryPath(const QString &entry)
0414 {
0415     const QChar slash = QChar::fromLatin1('/');
0416     const int index = entry.lastIndexOf(slash);
0417     QString ret = entry.mid(0, index);
0418     if (index > 0) {
0419         ret.append(slash);
0420     }
0421     if (!ret.startsWith(slash)) {
0422         ret.prepend(slash);
0423     }
0424     return ret;
0425 }
0426 
0427 /**
0428    \return The path of the entry
0429 */
0430 static QString entryPath(const KZipFileEntry *entry)
0431 {
0432     return entryPath(entry->path());
0433 }
0434 
0435 /**
0436    \return The absolute path of the \p location, according to \p path if it's non-absolute
0437 */
0438 static QString absolutePath(const QString &path, const QString &location)
0439 {
0440     QString retPath;
0441     if (location.startsWith(QLatin1Char('/'))) {
0442         // already absolute
0443         retPath = location;
0444     } else {
0445         QUrl u = QUrl::fromLocalFile(path);
0446         QUrl u2 = QUrl(location);
0447         retPath = u.resolved(u2).toDisplayString(QUrl::PreferLocalFile);
0448     }
0449     // it seems paths & file names can also be percent-encoded
0450     // (XPS won't ever finish surprising me)
0451     if (retPath.contains(QLatin1Char('%'))) {
0452         retPath = QUrl::fromPercentEncoding(retPath.toUtf8());
0453     }
0454     return retPath;
0455 }
0456 
0457 /**
0458    Read the content of an archive entry in both the cases:
0459    a) single file
0460       + foobar
0461    b) directory
0462       + foobar/
0463         + [0].piece
0464         + [1].piece
0465         + ...
0466         + [x].last.piece
0467 
0468    \see XPS specification 10.1.2
0469 */
0470 static QByteArray readFileOrDirectoryParts(const KArchiveEntry *entry, QString *pathOfFile = nullptr)
0471 {
0472     QByteArray data;
0473     if (entry->isDirectory()) {
0474         const KArchiveDirectory *relDir = static_cast<const KArchiveDirectory *>(entry);
0475         QStringList entries = relDir->entries();
0476         std::sort(entries.begin(), entries.end());
0477         for (const QString &entry : std::as_const(entries)) {
0478             const KArchiveEntry *relSubEntry = relDir->entry(entry);
0479             if (!relSubEntry->isFile()) {
0480                 continue;
0481             }
0482 
0483             const KZipFileEntry *relSubFile = static_cast<const KZipFileEntry *>(relSubEntry);
0484             data.append(relSubFile->data());
0485         }
0486     } else {
0487         const KZipFileEntry *relFile = static_cast<const KZipFileEntry *>(entry);
0488         data.append(relFile->data());
0489         if (pathOfFile) {
0490             *pathOfFile = entryPath(relFile);
0491         }
0492     }
0493     return data;
0494 }
0495 
0496 /**
0497    Load the resource \p fileName from the specified \p archive using the case sensitivity \p cs
0498 */
0499 static const KArchiveEntry *loadEntry(KZip *archive, const QString &fileName, Qt::CaseSensitivity cs)
0500 {
0501     // first attempt: loading the entry straight as requested
0502     const KArchiveEntry *entry = archive->directory()->entry(fileName);
0503     // in case sensitive mode, or if we actually found something, return what we found
0504     if (cs == Qt::CaseSensitive || entry) {
0505         return entry;
0506     }
0507 
0508     QString path;
0509     QString entryName;
0510     const int index = fileName.lastIndexOf(QChar::fromLatin1('/'));
0511     if (index > 0) {
0512         path = fileName.left(index);
0513         entryName = fileName.mid(index + 1);
0514     } else {
0515         path = QLatin1Char('/');
0516         entryName = fileName;
0517     }
0518     const KArchiveEntry *newEntry = archive->directory()->entry(path);
0519     if (newEntry->isDirectory()) {
0520         const KArchiveDirectory *relDir = static_cast<const KArchiveDirectory *>(newEntry);
0521         QStringList relEntries = relDir->entries();
0522         std::sort(relEntries.begin(), relEntries.end());
0523         for (const QString &relEntry : std::as_const(relEntries)) {
0524             if (relEntry.compare(entryName, Qt::CaseInsensitive) == 0) {
0525                 return relDir->entry(relEntry);
0526             }
0527         }
0528     }
0529     return nullptr;
0530 }
0531 
0532 /**
0533    \return The name of a resource from the \p fileName
0534 */
0535 static QString resourceName(const QString &fileName)
0536 {
0537     QString resource = fileName;
0538     const int slashPos = fileName.lastIndexOf(QLatin1Char('/'));
0539     const int dotPos = fileName.lastIndexOf(QLatin1Char('.'));
0540     if (slashPos > -1) {
0541         if (dotPos > -1 && dotPos > slashPos) {
0542             resource = fileName.mid(slashPos + 1, dotPos - slashPos - 1);
0543         } else {
0544             resource = fileName.mid(slashPos + 1);
0545         }
0546     }
0547     return resource;
0548 }
0549 
0550 static QColor interpolatedColor(const QColor &c1, const QColor &c2)
0551 {
0552     QColor res;
0553     res.setAlpha((c1.alpha() + c2.alpha()) / 2);
0554     res.setRed((c1.red() + c2.red()) / 2);
0555     res.setGreen((c1.green() + c2.green()) / 2);
0556     res.setBlue((c1.blue() + c2.blue()) / 2);
0557     return res;
0558 }
0559 
0560 static bool xpsGradientLessThan(const XpsGradient &g1, const XpsGradient &g2)
0561 {
0562     return qFuzzyCompare(g1.offset, g2.offset) ? g1.color.name() < g2.color.name() : g1.offset < g2.offset;
0563 }
0564 
0565 static int xpsGradientWithOffset(const QList<XpsGradient> &gradients, double offset)
0566 {
0567     int i = 0;
0568     for (const XpsGradient &grad : gradients) {
0569         if (grad.offset == offset) {
0570             return i;
0571         }
0572         ++i;
0573     }
0574     return -1;
0575 }
0576 
0577 /**
0578    Preprocess a list of gradients.
0579 
0580    \see XPS specification 11.3.1.1
0581 */
0582 static void preprocessXpsGradients(QList<XpsGradient> &gradients)
0583 {
0584     if (gradients.isEmpty()) {
0585         return;
0586     }
0587 
0588     // sort the gradients (case 1.)
0589     std::stable_sort(gradients.begin(), gradients.end(), xpsGradientLessThan);
0590 
0591     // no gradient with stop 0.0 (case 2.)
0592     if (xpsGradientWithOffset(gradients, 0.0) == -1) {
0593         int firstGreaterThanZero = 0;
0594         while (firstGreaterThanZero < gradients.count() && gradients.at(firstGreaterThanZero).offset < 0.0) {
0595             ++firstGreaterThanZero;
0596         }
0597         // case 2.a: no gradients with stop less than 0.0
0598         if (firstGreaterThanZero == 0) {
0599             gradients.prepend(XpsGradient(0.0, gradients.first().color));
0600         }
0601         // case 2.b: some gradients with stop more than 0.0
0602         else if (firstGreaterThanZero != gradients.count()) {
0603             QColor col1 = gradients.at(firstGreaterThanZero - 1).color;
0604             QColor col2 = gradients.at(firstGreaterThanZero).color;
0605             for (int i = 0; i < firstGreaterThanZero; ++i) {
0606                 gradients.removeFirst();
0607             }
0608             gradients.prepend(XpsGradient(0.0, interpolatedColor(col1, col2)));
0609         }
0610         // case 2.c: no gradients with stop more than 0.0
0611         else {
0612             XpsGradient newGrad(0.0, gradients.last().color);
0613             gradients.clear();
0614             gradients.append(newGrad);
0615         }
0616     }
0617 
0618     if (gradients.isEmpty()) {
0619         return;
0620     }
0621 
0622     // no gradient with stop 1.0 (case 3.)
0623     if (xpsGradientWithOffset(gradients, 1.0) == -1) {
0624         int firstLessThanOne = gradients.count() - 1;
0625         while (firstLessThanOne >= 0 && gradients.at(firstLessThanOne).offset > 1.0) {
0626             --firstLessThanOne;
0627         }
0628         // case 2.a: no gradients with stop greater than 1.0
0629         if (firstLessThanOne == gradients.count() - 1) {
0630             gradients.append(XpsGradient(1.0, gradients.last().color));
0631         }
0632         // case 2.b: some gradients with stop more than 1.0
0633         else if (firstLessThanOne != -1) {
0634             QColor col1 = gradients.at(firstLessThanOne).color;
0635             QColor col2 = gradients.at(firstLessThanOne + 1).color;
0636             for (int i = firstLessThanOne + 1; i < gradients.count(); ++i) {
0637                 gradients.removeLast();
0638             }
0639             gradients.append(XpsGradient(1.0, interpolatedColor(col1, col2)));
0640         }
0641         // case 2.c: no gradients with stop less than 1.0
0642         else {
0643             XpsGradient newGrad(1.0, gradients.first().color);
0644             gradients.clear();
0645             gradients.append(newGrad);
0646         }
0647     }
0648 }
0649 
0650 static void addXpsGradientsToQGradient(const QList<XpsGradient> &gradients, QGradient *qgrad)
0651 {
0652     for (const XpsGradient &grad : gradients) {
0653         qgrad->setColorAt(grad.offset, grad.color);
0654     }
0655 }
0656 
0657 static void applySpreadStyleToQGradient(const QString &style, QGradient *qgrad)
0658 {
0659     if (style.isEmpty()) {
0660         return;
0661     }
0662 
0663     if (style == QLatin1String("Pad")) {
0664         qgrad->setSpread(QGradient::PadSpread);
0665     } else if (style == QLatin1String("Reflect")) {
0666         qgrad->setSpread(QGradient::ReflectSpread);
0667     } else if (style == QLatin1String("Repeat")) {
0668         qgrad->setSpread(QGradient::RepeatSpread);
0669     }
0670 }
0671 
0672 /**
0673     Read an UnicodeString
0674     \param raw the raw value of UnicodeString
0675 
0676     \see XPS specification 5.1.4
0677 */
0678 static QString unicodeString(const QString &raw)
0679 {
0680     QString ret;
0681     if (raw.startsWith(QLatin1String("{}"))) {
0682         ret = raw.mid(2);
0683     } else {
0684         ret = raw;
0685     }
0686     return ret;
0687 }
0688 
0689 void XpsPage::processGlyph(QPainter *painter, XpsRenderNode &node)
0690 {
0691     // TODO Currently ignored attributes: CaretStops, DeviceFontName, IsSideways, OpacityMask, Name, FixedPage.NavigateURI, xml:lang, x:key
0692     // TODO Indices is only partially implemented
0693     // TODO Currently ignored child elements: Clip, OpacityMask
0694     // Handled separately: RenderTransform
0695 
0696     QString att;
0697 
0698     painter->save();
0699 
0700     // Get font (doesn't work well because qt doesn't allow to load font from file)
0701     // 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.
0702     float fontSize = node.attributes.value(QStringLiteral("FontRenderingEmSize")).toFloat();
0703     // qCWarning(OkularXpsDebug) << "Font Rendering EmSize:" << fontSize;
0704     // a value of 0.0 means the text is not visible (see XPS specs, chapter 12, "Glyphs")
0705     if (fontSize < 0.1) {
0706         painter->restore();
0707         return;
0708     }
0709     const QString absoluteFileName = absolutePath(entryPath(fileName()), node.attributes.value(QStringLiteral("FontUri")).toString());
0710     QFont font = m_file->getFontByName(absoluteFileName, fontSize);
0711     att = node.attributes.value(QStringLiteral("StyleSimulations")).toString();
0712     if (!att.isEmpty()) {
0713         if (att == QLatin1String("ItalicSimulation")) {
0714             font.setItalic(true);
0715         } else if (att == QLatin1String("BoldSimulation")) {
0716             font.setBold(true);
0717         } else if (att == QLatin1String("BoldItalicSimulation")) {
0718             font.setItalic(true);
0719             font.setBold(true);
0720         }
0721     }
0722     painter->setFont(font);
0723 
0724     // Origin
0725     QPointF origin(node.attributes.value(QStringLiteral("OriginX")).toDouble(), node.attributes.value(QStringLiteral("OriginY")).toDouble());
0726 
0727     // Fill
0728     QBrush brush;
0729     att = node.attributes.value(QStringLiteral("Fill")).toString();
0730     if (att.isEmpty()) {
0731         QVariant data = node.getChildData(QStringLiteral("Glyphs.Fill"));
0732         if (data.canConvert<QBrush>()) {
0733             brush = data.value<QBrush>();
0734         } else {
0735             // no "Fill" attribute and no "Glyphs.Fill" child, so show nothing
0736             // (see XPS specs, 5.10)
0737             painter->restore();
0738             return;
0739         }
0740     } else {
0741         brush = parseRscRefColorForBrush(att);
0742         if (brush.style() > Qt::NoBrush && brush.style() < Qt::LinearGradientPattern && brush.color().alpha() == 0) {
0743             painter->restore();
0744             return;
0745         }
0746     }
0747     painter->setBrush(brush);
0748     painter->setPen(QPen(brush, 0));
0749 
0750     // Opacity
0751     att = node.attributes.value(QStringLiteral("Opacity")).toString();
0752     if (!att.isEmpty()) {
0753         bool ok = true;
0754         double value = att.toDouble(&ok);
0755         if (ok && value >= 0.1) {
0756             painter->setOpacity(value);
0757         } else {
0758             painter->restore();
0759             return;
0760         }
0761     }
0762 
0763     // RenderTransform
0764     att = node.attributes.value(QStringLiteral("RenderTransform")).toString();
0765     if (!att.isEmpty()) {
0766         painter->setWorldTransform(parseRscRefMatrix(att), true);
0767     }
0768 
0769     // Clip
0770     att = node.attributes.value(QStringLiteral("Clip")).toString();
0771     if (!att.isEmpty()) {
0772         QPainterPath clipPath = parseRscRefPath(att);
0773         if (!clipPath.isEmpty()) {
0774             painter->setClipPath(clipPath);
0775         }
0776     }
0777 
0778     // BiDiLevel - default Left-to-Right
0779     painter->setLayoutDirection(Qt::LeftToRight);
0780     att = node.attributes.value(QStringLiteral("BiDiLevel")).toString();
0781     if (!att.isEmpty()) {
0782         if ((att.toInt() % 2) == 1) {
0783             // odd BiDiLevel, so Right-to-Left
0784             painter->setLayoutDirection(Qt::RightToLeft);
0785         }
0786     }
0787 
0788     // Indices - partial handling only
0789     att = node.attributes.value(QStringLiteral("Indices")).toString();
0790     QList<qreal> advanceWidths;
0791     if (!att.isEmpty()) {
0792         const QStringList indicesElements = att.split(QLatin1Char(';'));
0793         for (const QString &indicesElement : indicesElements) {
0794             if (indicesElements.contains(QStringLiteral(","))) {
0795                 const QStringList parts = indicesElement.split(QLatin1Char(','));
0796                 if (parts.size() == 2) {
0797                     // regular advance case, no offsets
0798                     advanceWidths.append(parts.at(1).toDouble() * fontSize / 100.0);
0799                 } else if (parts.size() == 3) {
0800                     // regular advance case, with uOffset
0801                     qreal AdvanceWidth = parts.at(1).toDouble() * fontSize / 100.0;
0802                     qreal uOffset = parts.at(2).toDouble() / 100.0;
0803                     advanceWidths.append(AdvanceWidth + uOffset);
0804                 } else {
0805                     // has vertical offset, but don't know how to handle that yet
0806                     qCWarning(OkularXpsDebug) << "Unhandled Indices element: " << indicesElement;
0807                     advanceWidths.append(-1.0);
0808                 }
0809             } else {
0810                 // no special advance case
0811                 advanceWidths.append(-1.0);
0812             }
0813         }
0814     }
0815 
0816     // UnicodeString
0817     QString stringToDraw(unicodeString(node.attributes.value(QStringLiteral("UnicodeString")).toString()));
0818     QPointF originAdvance(0, 0);
0819     QFontMetrics metrics = painter->fontMetrics();
0820     for (int i = 0; i < stringToDraw.size(); ++i) {
0821         QChar thisChar = stringToDraw.at(i);
0822         painter->drawText(origin + originAdvance, QString(thisChar));
0823         const qreal advanceWidth = advanceWidths.value(i, qreal(-1.0));
0824         if (advanceWidth > 0.0) {
0825             originAdvance.rx() += advanceWidth;
0826         } else {
0827             originAdvance.rx() += metrics.horizontalAdvance(thisChar);
0828         }
0829     }
0830     // qCWarning(OkularXpsDebug) << "Glyphs: " << atts.value("Fill") << ", " << atts.value("FontUri");
0831     // qCWarning(OkularXpsDebug) << "    Origin: " << atts.value("OriginX") << "," << atts.value("OriginY");
0832     // qCWarning(OkularXpsDebug) << "    Unicode: " << atts.value("UnicodeString");
0833 
0834     painter->restore();
0835 }
0836 
0837 void XpsPage::processFill(XpsRenderNode &node)
0838 {
0839     // TODO Ignored child elements: VirtualBrush
0840 
0841     if (node.children.size() != 1) {
0842         qCWarning(OkularXpsDebug) << "Fill element should have exactly one child";
0843     } else {
0844         node.data = node.children[0].data;
0845     }
0846 }
0847 
0848 void XpsPage::processStroke(XpsRenderNode &node)
0849 {
0850     // TODO Ignored child elements: VirtualBrush
0851 
0852     if (node.children.size() != 1) {
0853         qCWarning(OkularXpsDebug) << "Stroke element should have exactly one child";
0854     } else {
0855         node.data = node.children[0].data;
0856     }
0857 }
0858 
0859 void XpsPage::processImageBrush(XpsRenderNode &node)
0860 {
0861     // TODO Ignored attributes: Opacity, x:key, TileMode, ViewBoxUnits, ViewPortUnits
0862     // 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]
0863 
0864     QString att;
0865     QBrush brush;
0866 
0867     QRectF viewport = stringToRectF(node.attributes.value(QStringLiteral("Viewport")).toString());
0868     QRectF viewbox = stringToRectF(node.attributes.value(QStringLiteral("Viewbox")).toString());
0869     QImage image = loadImageFromFile(node.attributes.value(QStringLiteral("ImageSource")).toString());
0870 
0871     // Matrix which can transform [0, 0, 1, 1] rectangle to given viewbox
0872     QTransform viewboxMatrix = QTransform(viewbox.width() * image.physicalDpiX() / 96, 0, 0, viewbox.height() * image.physicalDpiY() / 96, viewbox.x(), viewbox.y());
0873 
0874     // Matrix which can transform [0, 0, 1, 1] rectangle to given viewport
0875     // TODO Take ViewPort into account
0876     QTransform viewportMatrix;
0877     att = node.attributes.value(QStringLiteral("Transform")).toString();
0878     if (att.isEmpty()) {
0879         QVariant data = node.getChildData(QStringLiteral("ImageBrush.Transform"));
0880         if (data.canConvert<QTransform>()) {
0881             viewportMatrix = data.value<QTransform>();
0882         } else {
0883             viewportMatrix = QTransform();
0884         }
0885     } else {
0886         viewportMatrix = parseRscRefMatrix(att);
0887     }
0888     viewportMatrix = viewportMatrix * QTransform(viewport.width(), 0, 0, viewport.height(), viewport.x(), viewport.y());
0889 
0890     brush = QBrush(image);
0891     brush.setTransform(viewboxMatrix.inverted() * viewportMatrix);
0892 
0893     node.data = QVariant::fromValue(brush);
0894 }
0895 
0896 void XpsPage::processPath(QPainter *painter, XpsRenderNode &node)
0897 {
0898     // TODO Ignored attributes: Clip, OpacityMask, StrokeEndLineCap, StorkeStartLineCap, Name, FixedPage.NavigateURI, xml:lang, x:key, AutomationProperties.Name, AutomationProperties.HelpText, SnapsToDevicePixels
0899     // TODO Ignored child elements: RenderTransform, Clip, OpacityMask
0900     // Handled separately: RenderTransform
0901     painter->save();
0902 
0903     QString att;
0904     QVariant data;
0905 
0906     // Get path
0907     XpsPathGeometry *pathdata = node.getChildData(QStringLiteral("Path.Data")).value<XpsPathGeometry *>();
0908     att = node.attributes.value(QStringLiteral("Data")).toString();
0909     if (!att.isEmpty()) {
0910         QPainterPath path = parseRscRefPath(att);
0911         delete pathdata;
0912         pathdata = new XpsPathGeometry();
0913         pathdata->paths.append(new XpsPathFigure(path, true));
0914     }
0915     if (!pathdata) {
0916         // nothing to draw
0917         painter->restore();
0918         return;
0919     }
0920 
0921     // Set Fill
0922     att = node.attributes.value(QStringLiteral("Fill")).toString();
0923     QBrush brush;
0924     if (!att.isEmpty()) {
0925         brush = parseRscRefColorForBrush(att);
0926     } else {
0927         data = node.getChildData(QStringLiteral("Path.Fill"));
0928         if (data.canConvert<QBrush>()) {
0929             brush = data.value<QBrush>();
0930         }
0931     }
0932     painter->setBrush(brush);
0933 
0934     // Stroke (pen)
0935     att = node.attributes.value(QStringLiteral("Stroke")).toString();
0936     QPen pen(Qt::transparent);
0937     if (!att.isEmpty()) {
0938         pen = parseRscRefColorForPen(att);
0939     } else {
0940         data = node.getChildData(QStringLiteral("Path.Stroke"));
0941         if (data.canConvert<QBrush>()) {
0942             pen.setBrush(data.value<QBrush>());
0943         }
0944     }
0945     att = node.attributes.value(QStringLiteral("StrokeThickness")).toString();
0946     if (!att.isEmpty()) {
0947         bool ok = false;
0948         int thickness = att.toInt(&ok);
0949         if (ok) {
0950             pen.setWidth(thickness);
0951         }
0952     }
0953     att = node.attributes.value(QStringLiteral("StrokeDashArray")).toString();
0954     if (!att.isEmpty()) {
0955         const QStringList pieces = att.split(QLatin1Char(' '), Qt::SkipEmptyParts);
0956         QVector<qreal> dashPattern(pieces.count());
0957         bool ok = false;
0958         for (int i = 0; i < pieces.count(); ++i) {
0959             qreal value = pieces.at(i).toInt(&ok);
0960             if (ok) {
0961                 dashPattern[i] = value;
0962             } else {
0963                 break;
0964             }
0965         }
0966         if (ok) {
0967             pen.setDashPattern(dashPattern);
0968         }
0969     }
0970     att = node.attributes.value(QStringLiteral("StrokeDashOffset")).toString();
0971     if (!att.isEmpty()) {
0972         bool ok = false;
0973         int offset = att.toInt(&ok);
0974         if (ok) {
0975             pen.setDashOffset(offset);
0976         }
0977     }
0978     att = node.attributes.value(QStringLiteral("StrokeDashCap")).toString();
0979     if (!att.isEmpty()) {
0980         Qt::PenCapStyle cap = Qt::FlatCap;
0981         if (att == QLatin1String("Flat")) {
0982             cap = Qt::FlatCap;
0983         } else if (att == QLatin1String("Round")) {
0984             cap = Qt::RoundCap;
0985         } else if (att == QLatin1String("Square")) {
0986             cap = Qt::SquareCap;
0987         }
0988         // ### missing "Triangle"
0989         pen.setCapStyle(cap);
0990     }
0991     att = node.attributes.value(QStringLiteral("StrokeLineJoin")).toString();
0992     if (!att.isEmpty()) {
0993         Qt::PenJoinStyle joinStyle = Qt::MiterJoin;
0994         if (att == QLatin1String("Miter")) {
0995             joinStyle = Qt::MiterJoin;
0996         } else if (att == QLatin1String("Bevel")) {
0997             joinStyle = Qt::BevelJoin;
0998         } else if (att == QLatin1String("Round")) {
0999             joinStyle = Qt::RoundJoin;
1000         }
1001         pen.setJoinStyle(joinStyle);
1002     }
1003     att = node.attributes.value(QStringLiteral("StrokeMiterLimit")).toString();
1004     if (!att.isEmpty()) {
1005         bool ok = false;
1006         double limit = att.toDouble(&ok);
1007         if (ok) {
1008             // we have to divide it by two, as XPS consider half of the stroke width,
1009             // while Qt the whole of it
1010             pen.setMiterLimit(limit / 2);
1011         }
1012     }
1013     painter->setPen(pen);
1014 
1015     // Opacity
1016     att = node.attributes.value(QStringLiteral("Opacity")).toString();
1017     if (!att.isEmpty()) {
1018         painter->setOpacity(att.toDouble());
1019     }
1020 
1021     // RenderTransform
1022     att = node.attributes.value(QStringLiteral("RenderTransform")).toString();
1023     if (!att.isEmpty()) {
1024         painter->setWorldTransform(parseRscRefMatrix(att), true);
1025     }
1026     if (!pathdata->transform.isIdentity()) {
1027         painter->setWorldTransform(pathdata->transform, true);
1028     }
1029 
1030     for (const XpsPathFigure *figure : std::as_const(pathdata->paths)) {
1031         painter->setBrush(figure->isFilled ? brush : QBrush());
1032         painter->drawPath(figure->path);
1033     }
1034 
1035     delete pathdata;
1036 
1037     painter->restore();
1038 }
1039 
1040 void XpsPage::processPathData(XpsRenderNode &node)
1041 {
1042     if (node.children.size() != 1) {
1043         qCWarning(OkularXpsDebug) << "Path.Data element should have exactly one child";
1044     } else {
1045         node.data = node.children[0].data;
1046     }
1047 }
1048 
1049 void XpsPage::processPathGeometry(XpsRenderNode &node)
1050 {
1051     XpsPathGeometry *geom = new XpsPathGeometry();
1052 
1053     for (const XpsRenderNode &child : std::as_const(node.children)) {
1054         if (child.data.canConvert<XpsPathFigure *>()) {
1055             XpsPathFigure *figure = child.data.value<XpsPathFigure *>();
1056             geom->paths.append(figure);
1057         }
1058     }
1059 
1060     QString att;
1061 
1062     att = node.attributes.value(QStringLiteral("Figures")).toString();
1063     if (!att.isEmpty()) {
1064         QPainterPath path = parseRscRefPath(att);
1065         qDeleteAll(geom->paths);
1066         geom->paths.clear();
1067         geom->paths.append(new XpsPathFigure(path, true));
1068     }
1069 
1070     att = node.attributes.value(QStringLiteral("FillRule")).toString();
1071     if (!att.isEmpty()) {
1072         geom->fillRule = fillRuleFromString(att);
1073     }
1074 
1075     // Transform
1076     att = node.attributes.value(QStringLiteral("Transform")).toString();
1077     if (!att.isEmpty()) {
1078         geom->transform = parseRscRefMatrix(att);
1079     }
1080 
1081     if (!geom->paths.isEmpty()) {
1082         node.data = QVariant::fromValue(geom);
1083     } else {
1084         delete geom;
1085     }
1086 }
1087 
1088 void XpsPage::processPathFigure(XpsRenderNode &node)
1089 {
1090     // TODO Ignored child elements: ArcSegment
1091 
1092     QString att;
1093     QPainterPath path;
1094 
1095     att = node.attributes.value(QStringLiteral("StartPoint")).toString();
1096     if (!att.isEmpty()) {
1097         QPointF point = getPointFromString(att);
1098         path.moveTo(point);
1099     } else {
1100         return;
1101     }
1102 
1103     for (const XpsRenderNode &child : std::as_const(node.children)) {
1104         bool isStroked = true;
1105         att = node.attributes.value(QStringLiteral("IsStroked")).toString();
1106         if (!att.isEmpty()) {
1107             isStroked = att == QLatin1String("true");
1108         }
1109         if (!isStroked) {
1110             continue;
1111         }
1112 
1113         // PolyLineSegment
1114         if (child.name == QLatin1String("PolyLineSegment")) {
1115             att = child.attributes.value(QStringLiteral("Points")).toString();
1116             if (!att.isEmpty()) {
1117                 const QStringList points = att.split(QLatin1Char(' '), Qt::SkipEmptyParts);
1118                 for (const QString &p : points) {
1119                     QPointF point = getPointFromString(p);
1120                     path.lineTo(point);
1121                 }
1122             }
1123         }
1124         // PolyBezierSegment
1125         else if (child.name == QLatin1String("PolyBezierSegment")) {
1126             att = child.attributes.value(QStringLiteral("Points")).toString();
1127             if (!att.isEmpty()) {
1128                 const QStringList points = att.split(QLatin1Char(' '), Qt::SkipEmptyParts);
1129                 if (points.count() % 3 == 0) {
1130                     for (int i = 0; i < points.count();) {
1131                         QPointF firstControl = getPointFromString(points.at(i++));
1132                         QPointF secondControl = getPointFromString(points.at(i++));
1133                         QPointF endPoint = getPointFromString(points.at(i++));
1134                         path.cubicTo(firstControl, secondControl, endPoint);
1135                     }
1136                 }
1137             }
1138         }
1139         // PolyQuadraticBezierSegment
1140         else if (child.name == QLatin1String("PolyQuadraticBezierSegment")) {
1141             att = child.attributes.value(QStringLiteral("Points")).toString();
1142             if (!att.isEmpty()) {
1143                 const QStringList points = att.split(QLatin1Char(' '), Qt::SkipEmptyParts);
1144                 if (points.count() % 2 == 0) {
1145                     for (int i = 0; i < points.count();) {
1146                         QPointF point1 = getPointFromString(points.at(i++));
1147                         QPointF point2 = getPointFromString(points.at(i++));
1148                         path.quadTo(point1, point2);
1149                     }
1150                 }
1151             }
1152         }
1153     }
1154 
1155     bool closePath = false;
1156     att = node.attributes.value(QStringLiteral("IsClosed")).toString();
1157     if (!att.isEmpty()) {
1158         closePath = att == QLatin1String("true");
1159     }
1160     if (closePath) {
1161         path.closeSubpath();
1162     }
1163 
1164     bool isFilled = true;
1165     att = node.attributes.value(QStringLiteral("IsFilled")).toString();
1166     if (!att.isEmpty()) {
1167         isFilled = att == QLatin1String("true");
1168     }
1169 
1170     if (!path.isEmpty()) {
1171         node.data = QVariant::fromValue(new XpsPathFigure(path, isFilled));
1172     }
1173 }
1174 
1175 void XpsPage::processStartElement(QPainter *painter, XpsRenderNode &node)
1176 {
1177     if (node.name == QLatin1String("Canvas")) {
1178         painter->save();
1179         QString att = node.attributes.value(QStringLiteral("RenderTransform")).toString();
1180         if (!att.isEmpty()) {
1181             painter->setWorldTransform(parseRscRefMatrix(att), true);
1182         }
1183         att = node.attributes.value(QStringLiteral("Opacity")).toString();
1184         if (!att.isEmpty()) {
1185             double value = att.toDouble();
1186             if (value > 0.0 && value <= 1.0) {
1187                 painter->setOpacity(painter->opacity() * value);
1188             } else {
1189                 // setting manually to 0 is necessary to "disable"
1190                 // all the stuff inside
1191                 painter->setOpacity(0.0);
1192             }
1193         }
1194     }
1195 }
1196 
1197 void XpsPage::processEndElement(QPainter *painter, XpsRenderNode &node)
1198 {
1199     if (node.name == QLatin1String("Glyphs")) {
1200         processGlyph(painter, node);
1201     } else if (node.name == QLatin1String("Path")) {
1202         processPath(painter, node);
1203     } else if (node.name == QLatin1String("MatrixTransform")) {
1204         // TODO Ignoring x:key
1205         node.data = QVariant::fromValue(QTransform(attsToMatrix(node.attributes.value(QStringLiteral("Matrix")).toString())));
1206     } else if ((node.name == QLatin1String("Canvas.RenderTransform")) || (node.name == QLatin1String("Glyphs.RenderTransform")) || (node.name == QLatin1String("Path.RenderTransform"))) {
1207         QVariant data = node.getRequiredChildData(QStringLiteral("MatrixTransform"));
1208         if (data.canConvert<QTransform>()) {
1209             painter->setWorldTransform(data.value<QTransform>(), true);
1210         }
1211     } else if (node.name == QLatin1String("Canvas")) {
1212         painter->restore();
1213     } else if ((node.name == QLatin1String("Path.Fill")) || (node.name == QLatin1String("Glyphs.Fill"))) {
1214         processFill(node);
1215     } else if (node.name == QLatin1String("Path.Stroke")) {
1216         processStroke(node);
1217     } else if (node.name == QLatin1String("SolidColorBrush")) {
1218         // TODO Ignoring opacity, x:key
1219         node.data = QVariant::fromValue(QBrush(QColor(hexToRgba(node.attributes.value(QStringLiteral("Color")).toLatin1()))));
1220     } else if (node.name == QLatin1String("ImageBrush")) {
1221         processImageBrush(node);
1222     } else if (node.name == QLatin1String("ImageBrush.Transform")) {
1223         node.data = node.getRequiredChildData(QStringLiteral("MatrixTransform"));
1224     } else if (node.name == QLatin1String("LinearGradientBrush")) {
1225         const XpsRenderNode *gradients = node.findChild(QStringLiteral("LinearGradientBrush.GradientStops"));
1226         if (gradients && gradients->data.canConvert<QGradient *>()) {
1227             QPointF start = getPointFromString(node.attributes.value(QStringLiteral("StartPoint")).toString());
1228             QPointF end = getPointFromString(node.attributes.value(QStringLiteral("EndPoint")).toString());
1229             QLinearGradient *qgrad = static_cast<QLinearGradient *>(gradients->data.value<QGradient *>());
1230             qgrad->setStart(start);
1231             qgrad->setFinalStop(end);
1232             applySpreadStyleToQGradient(node.attributes.value(QStringLiteral("SpreadMethod")).toString(), qgrad);
1233             node.data = QVariant::fromValue(QBrush(*qgrad));
1234             delete qgrad;
1235         }
1236     } else if (node.name == QLatin1String("RadialGradientBrush")) {
1237         const XpsRenderNode *gradients = node.findChild(QStringLiteral("RadialGradientBrush.GradientStops"));
1238         if (gradients && gradients->data.canConvert<QGradient *>()) {
1239             QPointF center = getPointFromString(node.attributes.value(QStringLiteral("Center")).toString());
1240             QPointF origin = getPointFromString(node.attributes.value(QStringLiteral("GradientOrigin")).toString());
1241             double radiusX = node.attributes.value(QStringLiteral("RadiusX")).toDouble();
1242             double radiusY = node.attributes.value(QStringLiteral("RadiusY")).toDouble();
1243             QRadialGradient *qgrad = static_cast<QRadialGradient *>(gradients->data.value<QGradient *>());
1244             qgrad->setCenter(center);
1245             qgrad->setFocalPoint(origin);
1246             // TODO what in case of different radii?
1247             qgrad->setRadius(qMin(radiusX, radiusY));
1248             applySpreadStyleToQGradient(node.attributes.value(QStringLiteral("SpreadMethod")).toString(), qgrad);
1249             node.data = QVariant::fromValue(QBrush(*qgrad));
1250             delete qgrad;
1251         }
1252     } else if (node.name == QLatin1String("LinearGradientBrush.GradientStops")) {
1253         QList<XpsGradient> gradients;
1254         for (const XpsRenderNode &child : std::as_const(node.children)) {
1255             double offset = child.attributes.value(QStringLiteral("Offset")).toDouble();
1256             QColor color = hexToRgba(child.attributes.value(QStringLiteral("Color")).toLatin1());
1257             gradients.append(XpsGradient(offset, color));
1258         }
1259         preprocessXpsGradients(gradients);
1260         if (!gradients.isEmpty()) {
1261             QLinearGradient *qgrad = new QLinearGradient();
1262             addXpsGradientsToQGradient(gradients, qgrad);
1263             node.data = QVariant::fromValue<QGradient *>(qgrad);
1264         }
1265     } else if (node.name == QLatin1String("RadialGradientBrush.GradientStops")) {
1266         QList<XpsGradient> gradients;
1267         for (const XpsRenderNode &child : std::as_const(node.children)) {
1268             double offset = child.attributes.value(QStringLiteral("Offset")).toDouble();
1269             QColor color = hexToRgba(child.attributes.value(QStringLiteral("Color")).toLatin1());
1270             gradients.append(XpsGradient(offset, color));
1271         }
1272         preprocessXpsGradients(gradients);
1273         if (!gradients.isEmpty()) {
1274             QRadialGradient *qgrad = new QRadialGradient();
1275             addXpsGradientsToQGradient(gradients, qgrad);
1276             node.data = QVariant::fromValue<QGradient *>(qgrad);
1277         }
1278     } else if (node.name == QLatin1String("PathFigure")) {
1279         processPathFigure(node);
1280     } else if (node.name == QLatin1String("PathGeometry")) {
1281         processPathGeometry(node);
1282     } else if (node.name == QLatin1String("Path.Data")) {
1283         processPathData(node);
1284     } else {
1285         // qCWarning(OkularXpsDebug) << "Unknown element: " << node->name;
1286     }
1287 }
1288 
1289 XpsPage::XpsPage(XpsFile *file, const QString &fileName)
1290     : m_file(file)
1291     , m_fileName(fileName)
1292     , m_pageIsRendered(false)
1293 {
1294     m_pageImage = nullptr;
1295 
1296     // qCWarning(OkularXpsDebug) << "page file name: " << fileName;
1297 
1298     const KZipFileEntry *pageFile = static_cast<const KZipFileEntry *>(m_file->xpsArchive()->directory()->entry(fileName));
1299 
1300     QXmlStreamReader xml;
1301     xml.addData(readFileOrDirectoryParts(pageFile));
1302     while (!xml.atEnd()) {
1303         xml.readNext();
1304         if (xml.isStartElement() && (xml.name() == QStringLiteral("FixedPage"))) {
1305             QXmlStreamAttributes attributes = xml.attributes();
1306             m_pageSize.setWidth(attributes.value(QStringLiteral("Width")).toString().toDouble());
1307             m_pageSize.setHeight(attributes.value(QStringLiteral("Height")).toString().toDouble());
1308             break;
1309         }
1310     }
1311     if (xml.error()) {
1312         qCWarning(OkularXpsDebug) << "Could not parse XPS page:" << xml.errorString();
1313     }
1314 }
1315 
1316 XpsPage::~XpsPage()
1317 {
1318     delete m_pageImage;
1319 }
1320 
1321 bool XpsPage::renderToImage(QImage *p)
1322 {
1323     if ((m_pageImage == nullptr) || (m_pageImage->size() != p->size())) {
1324         delete m_pageImage;
1325         m_pageImage = new QImage(p->size(), QImage::Format_ARGB32);
1326         // Set one point = one drawing unit. Useful for fonts, because xps specifies font size using drawing units, not points as usual
1327         m_pageImage->setDotsPerMeterX(2835);
1328         m_pageImage->setDotsPerMeterY(2835);
1329 
1330         m_pageIsRendered = false;
1331     }
1332     if (!m_pageIsRendered) {
1333         m_pageImage->fill(qRgba(255, 255, 255, 255));
1334         QPainter painter(m_pageImage);
1335         renderToPainter(&painter);
1336         m_pageIsRendered = true;
1337     }
1338 
1339     *p = *m_pageImage;
1340 
1341     return true;
1342 }
1343 
1344 bool XpsPage::renderToPainter(QPainter *painter)
1345 {
1346     painter->setWorldTransform(QTransform().scale((qreal)painter->device()->width() / size().width(), (qreal)painter->device()->height() / size().height()));
1347     const KZipFileEntry *pageFile = static_cast<const KZipFileEntry *>(m_file->xpsArchive()->directory()->entry(m_fileName));
1348     QByteArray data = readFileOrDirectoryParts(pageFile);
1349     QXmlStreamReader reader(data);
1350 
1351     while (!reader.atEnd()) {
1352         reader.readNext();
1353         // parse data and paint it to painter
1354         if (reader.isStartDocument()) {
1355             XpsRenderNode node;
1356             node.name = QStringLiteral("document");
1357             m_nodes.push(node);
1358         } else if (reader.isStartElement()) {
1359             XpsRenderNode node;
1360             node.name = reader.name().toString();
1361             node.attributes = reader.attributes();
1362             processStartElement(painter, node);
1363             m_nodes.push(node);
1364         } else if (reader.isEndElement()) {
1365             XpsRenderNode node = m_nodes.pop();
1366             if (node.name != reader.name().toString()) {
1367                 qCWarning(OkularXpsDebug) << "Name doesn't match" << node.name << " and next from document: " << reader.name().toString();
1368             }
1369             processEndElement(painter, node);
1370             node.children.clear();
1371             m_nodes.top().children.append(node);
1372         }
1373     }
1374 
1375     bool ok = !reader.hasError();
1376     if (!ok) {
1377         // Error handling
1378     }
1379     qCWarning(OkularXpsDebug) << "Parse result: " << ok;
1380 
1381     return true;
1382 }
1383 
1384 QSizeF XpsPage::size() const
1385 {
1386     return m_pageSize;
1387 }
1388 
1389 QFont XpsFile::getFontByName(const QString &absoluteFileName, float size)
1390 {
1391     // qCWarning(OkularXpsDebug) << "trying to get font: " << fileName << ", size: " << size;
1392 
1393     int index = m_fontCache.value(absoluteFileName, -1);
1394     if (index == -1) {
1395         index = loadFontByName(absoluteFileName);
1396         m_fontCache[absoluteFileName] = index;
1397     }
1398     if (index == -1) {
1399         qCWarning(OkularXpsDebug) << "Requesting unknown font:" << absoluteFileName;
1400         return QFont();
1401     }
1402 
1403     const QStringList fontFamilies = m_fontDatabase.applicationFontFamilies(index);
1404     if (fontFamilies.isEmpty()) {
1405         qCWarning(OkularXpsDebug) << "The unexpected has happened. No font family for a known font:" << absoluteFileName << index;
1406         return QFont();
1407     }
1408     const QString &fontFamily = fontFamilies[0];
1409     const QStringList fontStyles = m_fontDatabase.styles(fontFamily);
1410     if (fontStyles.isEmpty()) {
1411         qCWarning(OkularXpsDebug) << "The unexpected has happened. No font style for a known font family:" << absoluteFileName << index << fontFamily;
1412         return QFont();
1413     }
1414     const QString &fontStyle = fontStyles[0];
1415     return m_fontDatabase.font(fontFamily, fontStyle, qRound(size));
1416 }
1417 
1418 int XpsFile::loadFontByName(const QString &absoluteFileName)
1419 {
1420     // qCWarning(OkularXpsDebug) << "font file name: " << absoluteFileName;
1421 
1422     const KArchiveEntry *fontFile = loadEntry(m_xpsArchive.get(), absoluteFileName, Qt::CaseInsensitive);
1423     if (!fontFile) {
1424         return -1;
1425     }
1426 
1427     QByteArray fontData = readFileOrDirectoryParts(fontFile); // once per file, according to the docs
1428 
1429     int result = m_fontDatabase.addApplicationFontFromData(fontData);
1430     if (-1 == result) {
1431         // Try to deobfuscate font
1432         // TODO Use deobfuscation depending on font content type, don't do it always when standard loading fails
1433 
1434         const QString baseName = resourceName(absoluteFileName);
1435 
1436         unsigned short guid[16];
1437         if (!parseGUID(baseName, guid)) {
1438             qCWarning(OkularXpsDebug) << "File to load font - file name isn't a GUID";
1439         } else {
1440             if (fontData.length() < 32) {
1441                 qCWarning(OkularXpsDebug) << "Font file is too small";
1442             } else {
1443                 // Obfuscation - xor bytes in font binary with bytes from guid (font's filename)
1444                 const static int mapping[] = {15, 14, 13, 12, 11, 10, 9, 8, 6, 7, 4, 5, 0, 1, 2, 3};
1445                 for (int i = 0; i < 16; i++) {
1446                     fontData[i] = fontData[i] ^ guid[mapping[i]];
1447                     fontData[i + 16] = fontData[i + 16] ^ guid[mapping[i]];
1448                 }
1449                 result = m_fontDatabase.addApplicationFontFromData(fontData);
1450             }
1451         }
1452     }
1453 
1454     // qCWarning(OkularXpsDebug) << "Loaded font: " << m_fontDatabase.applicationFontFamilies( result );
1455 
1456     return result; // a font ID
1457 }
1458 
1459 KZip *XpsFile::xpsArchive()
1460 {
1461     return m_xpsArchive.get();
1462 }
1463 
1464 QImage XpsPage::loadImageFromFile(const QString &fileName)
1465 {
1466     // qCWarning(OkularXpsDebug) << "image file name: " << fileName;
1467 
1468     if (fileName.at(0) == QLatin1Char('{')) {
1469         // for example: '{ColorConvertedBitmap /Resources/bla.wdp /Resources/foobar.icc}'
1470         // TODO: properly read a ColorConvertedBitmap
1471         return QImage();
1472     }
1473 
1474     QString absoluteFileName = absolutePath(entryPath(m_fileName), fileName);
1475     const KArchiveEntry *imageFile = loadEntry(m_file->xpsArchive(), absoluteFileName, Qt::CaseInsensitive);
1476     if (!imageFile) {
1477         // image not found
1478         return QImage();
1479     }
1480 
1481     /* WORKAROUND:
1482         XPS standard requires to use 96dpi for images which doesn't have dpi specified (in file). When Qt loads such an image,
1483         it sets its dpi to qt_defaultDpi and doesn't allow to find out that it happend.
1484 
1485         To workaround this I used this procedure: load image, set its dpi to 96, load image again. When dpi isn't set in file,
1486         dpi set by me stays unchanged.
1487 
1488         Trolltech task ID: 159527.
1489 
1490     */
1491 
1492     QImage image;
1493     QByteArray data = readFileOrDirectoryParts(imageFile);
1494 
1495     QBuffer buffer(&data);
1496     buffer.open(QBuffer::ReadOnly);
1497 
1498     QImageReader reader(&buffer);
1499     image = reader.read();
1500 
1501     image.setDotsPerMeterX(qRound(96 / 0.0254));
1502     image.setDotsPerMeterY(qRound(96 / 0.0254));
1503 
1504     buffer.seek(0);
1505     reader.setDevice(&buffer);
1506     reader.read(&image);
1507 
1508     return image;
1509 }
1510 
1511 Okular::TextPage *XpsPage::textPage()
1512 {
1513     // qCWarning(OkularXpsDebug) << "Parsing XpsPage, text extraction";
1514 
1515     Okular::TextPage *textPage = new Okular::TextPage();
1516 
1517     const KZipFileEntry *pageFile = static_cast<const KZipFileEntry *>(m_file->xpsArchive()->directory()->entry(m_fileName));
1518     QXmlStreamReader xml;
1519     xml.addData(readFileOrDirectoryParts(pageFile));
1520 
1521     QTransform matrix = QTransform();
1522     QStack<QTransform> matrices;
1523     matrices.push(QTransform());
1524     bool useMatrix = false;
1525     QXmlStreamAttributes glyphsAtts;
1526 
1527     while (!xml.atEnd()) {
1528         xml.readNext();
1529         if (xml.isStartElement()) {
1530             if (xml.name() == QStringLiteral("Canvas")) {
1531                 matrices.push(matrix);
1532 
1533                 QString att = xml.attributes().value(QStringLiteral("RenderTransform")).toString();
1534                 if (!att.isEmpty()) {
1535                     matrix = parseRscRefMatrix(att) * matrix;
1536                 }
1537             } else if ((xml.name() == QStringLiteral("Canvas.RenderTransform")) || (xml.name() == QStringLiteral("Glyphs.RenderTransform"))) {
1538                 useMatrix = true;
1539             } else if (xml.name() == QStringLiteral("MatrixTransform")) {
1540                 if (useMatrix) {
1541                     matrix = attsToMatrix(xml.attributes().value(QStringLiteral("Matrix")).toString()) * matrix;
1542                 }
1543             } else if (xml.name() == QStringLiteral("Glyphs")) {
1544                 matrices.push(matrix);
1545                 glyphsAtts = xml.attributes();
1546             } else if ((xml.name() == QStringLiteral("Path")) || (xml.name() == QStringLiteral("Path.Fill")) || (xml.name() == QStringLiteral("SolidColorBrush")) || (xml.name() == QStringLiteral("ImageBrush")) ||
1547                        (xml.name() == QStringLiteral("ImageBrush.Transform")) || (xml.name() == QStringLiteral("Path.OpacityMask")) || (xml.name() == QStringLiteral("Path.Data")) || (xml.name() == QStringLiteral("PathGeometry")) ||
1548                        (xml.name() == QStringLiteral("PathFigure")) || (xml.name() == QStringLiteral("PolyLineSegment"))) {
1549                 // those are only graphical - no use in text handling
1550             } else if ((xml.name() == QStringLiteral("FixedPage")) || (xml.name() == QStringLiteral("FixedPage.Resources"))) {
1551                 // not useful for text extraction
1552             } else {
1553                 qCWarning(OkularXpsDebug) << "Unhandled element in Text Extraction start: " << xml.name().toString();
1554             }
1555         } else if (xml.isEndElement()) {
1556             if (xml.name() == QStringLiteral("Canvas")) {
1557                 matrix = matrices.pop();
1558             } else if ((xml.name() == QStringLiteral("Canvas.RenderTransform")) || (xml.name() == QStringLiteral("Glyphs.RenderTransform"))) {
1559                 useMatrix = false;
1560             } else if (xml.name() == QStringLiteral("MatrixTransform")) {
1561                 // not clear if we need to do anything here yet.
1562             } else if (xml.name() == QStringLiteral("Glyphs")) {
1563                 QString att = glyphsAtts.value(QStringLiteral("RenderTransform")).toString();
1564                 if (!att.isEmpty()) {
1565                     matrix = parseRscRefMatrix(att) * matrix;
1566                 }
1567                 QString text = unicodeString(glyphsAtts.value(QStringLiteral("UnicodeString")).toString());
1568 
1569                 // Get font (doesn't work well because qt doesn't allow to load font from file)
1570                 const QString absoluteFileName = absolutePath(entryPath(m_fileName), glyphsAtts.value(QStringLiteral("FontUri")).toString());
1571                 QFont font = m_file->getFontByName(absoluteFileName, glyphsAtts.value(QStringLiteral("FontRenderingEmSize")).toString().toFloat() * 72 / 96);
1572                 QFontMetrics metrics = QFontMetrics(font);
1573                 // Origin
1574                 QPointF origin(glyphsAtts.value(QStringLiteral("OriginX")).toString().toDouble(), glyphsAtts.value(QStringLiteral("OriginY")).toString().toDouble());
1575 
1576                 int lastWidth = 0;
1577                 for (int i = 0; i < text.length(); i++) {
1578                     const int width = metrics.horizontalAdvance(text, i + 1);
1579 
1580                     Okular::NormalizedRect rect =
1581                         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());
1582                     rect.transform(matrix);
1583                     textPage->append(text.mid(i, 1), rect);
1584 
1585                     lastWidth = width;
1586                 }
1587 
1588                 matrix = matrices.pop();
1589             } else if ((xml.name() == QStringLiteral("Path")) || (xml.name() == QStringLiteral("Path.Fill")) || (xml.name() == QStringLiteral("SolidColorBrush")) || (xml.name() == QStringLiteral("ImageBrush")) ||
1590                        (xml.name() == QStringLiteral("ImageBrush.Transform")) || (xml.name() == QStringLiteral("Path.OpacityMask")) || (xml.name() == QStringLiteral("Path.Data")) || (xml.name() == QStringLiteral("PathGeometry")) ||
1591                        (xml.name() == QStringLiteral("PathFigure")) || (xml.name() == QStringLiteral("PolyLineSegment"))) {
1592                 // those are only graphical - no use in text handling
1593             } else if ((xml.name() == QStringLiteral("FixedPage")) || (xml.name() == QStringLiteral("FixedPage.Resources"))) {
1594                 // not useful for text extraction
1595             } else {
1596                 qCWarning(OkularXpsDebug) << "Unhandled element in Text Extraction end: " << xml.name().toString();
1597             }
1598         }
1599     }
1600     if (xml.error()) {
1601         qCWarning(OkularXpsDebug) << "Error parsing XpsPage text: " << xml.errorString();
1602     }
1603     return textPage;
1604 }
1605 
1606 void XpsDocument::parseDocumentStructure(const QString &documentStructureFileName)
1607 {
1608     qCWarning(OkularXpsDebug) << "document structure file name: " << documentStructureFileName;
1609     m_haveDocumentStructure = false;
1610 
1611     const KZipFileEntry *documentStructureFile = static_cast<const KZipFileEntry *>(m_file->xpsArchive()->directory()->entry(documentStructureFileName));
1612 
1613     QXmlStreamReader xml;
1614     xml.addData(documentStructureFile->data());
1615 
1616     while (!xml.atEnd()) {
1617         xml.readNext();
1618 
1619         if (xml.isStartElement()) {
1620             if (xml.name() == QStringLiteral("DocumentStructure")) {
1621                 // just a container for optional outline and story elements - nothing to do here
1622             } else if (xml.name() == QStringLiteral("DocumentStructure.Outline")) {
1623                 qCWarning(OkularXpsDebug) << "found DocumentStructure.Outline";
1624             } else if (xml.name() == QStringLiteral("DocumentOutline")) {
1625                 qCWarning(OkularXpsDebug) << "found DocumentOutline";
1626                 m_docStructure = std::make_unique<Okular::DocumentSynopsis>();
1627             } else if (xml.name() == QStringLiteral("OutlineEntry")) {
1628                 m_haveDocumentStructure = true;
1629                 QXmlStreamAttributes attributes = xml.attributes();
1630                 int outlineLevel = attributes.value(QStringLiteral("OutlineLevel")).toString().toInt();
1631                 QString description = attributes.value(QStringLiteral("Description")).toString();
1632                 QDomElement synopsisElement = m_docStructure->createElement(description);
1633                 synopsisElement.setAttribute(QStringLiteral("OutlineLevel"), outlineLevel);
1634                 QString target = attributes.value(QStringLiteral("OutlineTarget")).toString();
1635                 int hashPosition = target.lastIndexOf(QLatin1Char('#'));
1636                 target = target.mid(hashPosition + 1);
1637                 // qCWarning(OkularXpsDebug) << "target: " << target;
1638                 Okular::DocumentViewport viewport;
1639                 viewport.pageNumber = m_docStructurePageMap.value(target);
1640                 synopsisElement.setAttribute(QStringLiteral("Viewport"), viewport.toString());
1641                 if (outlineLevel == 1) {
1642                     // qCWarning(OkularXpsDebug) << "Description: "
1643                     // << outlineEntryElement.attribute( "Description" );
1644                     m_docStructure->appendChild(synopsisElement);
1645                 } else {
1646                     // find the last next highest element (so it this is level 3, we need
1647                     // to find the most recent level 2 node)
1648                     QDomNode maybeParentNode = m_docStructure->lastChild();
1649                     while (!maybeParentNode.isNull()) {
1650                         if (maybeParentNode.toElement().attribute(QStringLiteral("OutlineLevel")).toInt() == (outlineLevel - 1)) {
1651                             // we have the right parent
1652                             maybeParentNode.appendChild(synopsisElement);
1653                             break;
1654                         }
1655                         maybeParentNode = maybeParentNode.lastChild();
1656                     }
1657                 }
1658             } else if (xml.name() == QStringLiteral("Story")) {
1659                 // we need to handle Story here, but I have no idea what to do with it.
1660             } else if (xml.name() == QStringLiteral("StoryFragment")) {
1661                 // we need to handle StoryFragment here, but I have no idea what to do with it.
1662             } else if (xml.name() == QStringLiteral("StoryFragmentReference")) {
1663                 // we need to handle StoryFragmentReference here, but I have no idea what to do with it.
1664             } else {
1665                 qCWarning(OkularXpsDebug) << "Unhandled entry in DocumentStructure: " << xml.name().toString();
1666             }
1667         }
1668     }
1669 }
1670 
1671 const Okular::DocumentSynopsis *XpsDocument::documentStructure()
1672 {
1673     return m_docStructure.get();
1674 }
1675 
1676 bool XpsDocument::hasDocumentStructure()
1677 {
1678     return m_haveDocumentStructure;
1679 }
1680 
1681 XpsDocument::XpsDocument(XpsFile *file, const QString &fileName)
1682     : m_file(file)
1683     , m_haveDocumentStructure(false)
1684     , m_docStructure(nullptr)
1685 {
1686     qCWarning(OkularXpsDebug) << "document file name: " << fileName;
1687 
1688     const KArchiveEntry *documentEntry = file->xpsArchive()->directory()->entry(fileName);
1689     QString documentFilePath = fileName;
1690     const QString documentEntryPath = entryPath(fileName);
1691 
1692     QXmlStreamReader docXml;
1693     docXml.addData(readFileOrDirectoryParts(documentEntry, &documentFilePath));
1694     while (!docXml.atEnd()) {
1695         docXml.readNext();
1696         if (docXml.isStartElement()) {
1697             if (docXml.name() == QStringLiteral("PageContent")) {
1698                 QString pagePath = docXml.attributes().value(QStringLiteral("Source")).toString();
1699                 qCWarning(OkularXpsDebug) << "Page Path: " << pagePath;
1700                 auto page = std::make_unique<XpsPage>(file, absolutePath(documentFilePath, pagePath));
1701                 m_pages.push_back(std::move(page));
1702             } else if (docXml.name() == QStringLiteral("PageContent.LinkTargets")) {
1703                 // do nothing - wait for the real LinkTarget elements
1704             } else if (docXml.name() == QStringLiteral("LinkTarget")) {
1705                 QString targetName = docXml.attributes().value(QStringLiteral("Name")).toString();
1706                 if (!targetName.isEmpty()) {
1707                     m_docStructurePageMap[targetName] = m_pages.size() - 1;
1708                 }
1709             } else if (docXml.name() == QStringLiteral("FixedDocument")) {
1710                 // we just ignore this - it is just a container
1711             } else {
1712                 qCWarning(OkularXpsDebug) << "Unhandled entry in FixedDocument: " << docXml.name().toString();
1713             }
1714         }
1715     }
1716     if (docXml.error()) {
1717         qCWarning(OkularXpsDebug) << "Could not parse main XPS document file: " << docXml.errorString();
1718     }
1719 
1720     // There might be a relationships entry for this document - typically used to tell us where to find the
1721     // content structure description
1722 
1723     // We should be able to find this using a reference from some other part of the document, but I can't see it.
1724     const int slashPosition = fileName.lastIndexOf(QLatin1Char('/'));
1725     const QString documentRelationshipFile = absolutePath(documentEntryPath, QStringLiteral("_rels/") + fileName.mid(slashPosition + 1) + QStringLiteral(".rels"));
1726 
1727     const KZipFileEntry *relFile = static_cast<const KZipFileEntry *>(file->xpsArchive()->directory()->entry(documentRelationshipFile));
1728 
1729     QString documentStructureFile;
1730     if (relFile) {
1731         QXmlStreamReader xml;
1732         xml.addData(readFileOrDirectoryParts(relFile));
1733         while (!xml.atEnd()) {
1734             xml.readNext();
1735             if (xml.isStartElement() && (xml.name() == QStringLiteral("Relationship"))) {
1736                 QXmlStreamAttributes attributes = xml.attributes();
1737                 if (attributes.value(QStringLiteral("Type")).toString() == QLatin1String("http://schemas.microsoft.com/xps/2005/06/documentstructure")) {
1738                     documentStructureFile = attributes.value(QStringLiteral("Target")).toString();
1739                 } else {
1740                     qCWarning(OkularXpsDebug) << "Unknown document relationships element: " << attributes.value(QStringLiteral("Type")).toString() << " : " << attributes.value(QStringLiteral("Target")).toString();
1741                 }
1742             }
1743         }
1744         if (xml.error()) {
1745             qCWarning(OkularXpsDebug) << "Could not parse XPS page relationships file ( " << documentRelationshipFile << " ) - " << xml.errorString();
1746         }
1747     } else { // the page relationship file didn't exist in the zipfile
1748         // this isn't fatal
1749         qCWarning(OkularXpsDebug) << "Could not open Document relationship file from " << documentRelationshipFile;
1750     }
1751 
1752     if (!documentStructureFile.isEmpty()) {
1753         // qCWarning(OkularXpsDebug) << "Document structure filename: " << documentStructureFile;
1754         // make the document path absolute
1755         documentStructureFile = absolutePath(documentEntryPath, documentStructureFile);
1756         // qCWarning(OkularXpsDebug) << "Document structure absolute path: " << documentStructureFile;
1757         parseDocumentStructure(documentStructureFile);
1758     }
1759 }
1760 
1761 XpsDocument::~XpsDocument()
1762 {
1763     m_pages.clear();
1764 }
1765 
1766 int XpsDocument::numPages() const
1767 {
1768     return m_pages.size();
1769 }
1770 
1771 XpsPage *XpsDocument::page(int pageNum) const
1772 {
1773     return m_pages.at(pageNum).get();
1774 }
1775 
1776 XpsFile::XpsFile()
1777 {
1778 }
1779 
1780 XpsFile::~XpsFile()
1781 {
1782     for (int fontId : std::as_const(m_fontCache)) {
1783         m_fontDatabase.removeApplicationFont(fontId);
1784     }
1785 }
1786 
1787 bool XpsFile::loadDocument(const QString &filename)
1788 {
1789     m_xpsArchive = std::make_unique<KZip>(filename);
1790     if (m_xpsArchive->open(QIODevice::ReadOnly) == true) {
1791         qCWarning(OkularXpsDebug) << "Successful open of " << m_xpsArchive->fileName();
1792     } else {
1793         qCWarning(OkularXpsDebug) << "Could not open XPS archive: " << m_xpsArchive->fileName();
1794         m_xpsArchive.reset();
1795         return false;
1796     }
1797 
1798     // The only fixed entry in XPS is /_rels/.rels
1799     const KArchiveEntry *relEntry = m_xpsArchive->directory()->entry(QStringLiteral("_rels/.rels"));
1800     if (!relEntry) {
1801         // this might occur if we can't read the zip directory, or it doesn't have the relationships entry
1802         return false;
1803     }
1804 
1805     QXmlStreamReader relXml;
1806     relXml.addData(readFileOrDirectoryParts(relEntry));
1807 
1808     QString fixedRepresentationFileName;
1809     // We work through the relationships document and pull out each element.
1810     while (!relXml.atEnd()) {
1811         relXml.readNext();
1812         if (relXml.isStartElement()) {
1813             if (relXml.name() == QStringLiteral("Relationship")) {
1814                 QXmlStreamAttributes attributes = relXml.attributes();
1815                 QString type = attributes.value(QStringLiteral("Type")).toString();
1816                 QString target = attributes.value(QStringLiteral("Target")).toString();
1817                 if (type == QStringLiteral("http://schemas.openxmlformats.org/package/2006/relationships/metadata/thumbnail")) {
1818                     m_thumbnailFileName = target;
1819                 } else if (type == QStringLiteral("http://schemas.microsoft.com/xps/2005/06/fixedrepresentation")) {
1820                     fixedRepresentationFileName = target;
1821                 } else if (type == QStringLiteral("http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties")) {
1822                     m_corePropertiesFileName = target;
1823                 } else if (type == QStringLiteral("http://schemas.openxmlformats.org/package/2006/relationships/digital-signature/origin")) {
1824                     m_signatureOrigin = target;
1825                 } else {
1826                     qCWarning(OkularXpsDebug) << "Unknown relationships element: " << type << " : " << target;
1827                 }
1828             } else if (relXml.name() == QStringLiteral("Relationships")) {
1829                 // nothing to do here - this is just the container level
1830             } else {
1831                 qCWarning(OkularXpsDebug) << "unexpected element in _rels/.rels: " << relXml.name().toString();
1832             }
1833         }
1834     }
1835     if (relXml.error()) {
1836         qCWarning(OkularXpsDebug) << "Could not parse _rels/.rels: " << relXml.errorString();
1837         return false;
1838     }
1839 
1840     if (fixedRepresentationFileName.isEmpty()) {
1841         // FixedRepresentation is a required part of the XPS document
1842         return false;
1843     }
1844 
1845     const KArchiveEntry *fixedRepEntry = m_xpsArchive->directory()->entry(fixedRepresentationFileName);
1846     QString fixedRepresentationFilePath = fixedRepresentationFileName;
1847 
1848     QXmlStreamReader fixedRepXml;
1849     fixedRepXml.addData(readFileOrDirectoryParts(fixedRepEntry, &fixedRepresentationFileName));
1850 
1851     while (!fixedRepXml.atEnd()) {
1852         fixedRepXml.readNext();
1853         if (fixedRepXml.isStartElement()) {
1854             if (fixedRepXml.name() == QStringLiteral("DocumentReference")) {
1855                 const QString source = fixedRepXml.attributes().value(QStringLiteral("Source")).toString();
1856                 auto doc = std::make_unique<XpsDocument>(this, absolutePath(fixedRepresentationFilePath, source));
1857                 for (int lv = 0; lv < doc->numPages(); ++lv) {
1858                     // our own copy of the pages list
1859                     m_pages.append(doc->page(lv));
1860                 }
1861                 m_documents.push_back(std::move(doc));
1862             } else if (fixedRepXml.name() == QStringLiteral("FixedDocumentSequence")) {
1863                 // we don't do anything here - this is just a container for one or more DocumentReference elements
1864             } else {
1865                 qCWarning(OkularXpsDebug) << "Unhandled entry in FixedDocumentSequence: " << fixedRepXml.name().toString();
1866             }
1867         }
1868     }
1869     if (fixedRepXml.error()) {
1870         qCWarning(OkularXpsDebug) << "Could not parse FixedRepresentation file:" << fixedRepXml.errorString();
1871         return false;
1872     }
1873 
1874     return true;
1875 }
1876 
1877 Okular::DocumentInfo XpsFile::generateDocumentInfo() const
1878 {
1879     Okular::DocumentInfo docInfo;
1880 
1881     docInfo.set(Okular::DocumentInfo::MimeType, QStringLiteral("application/oxps"));
1882 
1883     if (!m_corePropertiesFileName.isEmpty()) {
1884         const KZipFileEntry *corepropsFile = static_cast<const KZipFileEntry *>(m_xpsArchive->directory()->entry(m_corePropertiesFileName));
1885 
1886         QXmlStreamReader xml;
1887         xml.addData(corepropsFile->data());
1888         while (!xml.atEnd()) {
1889             xml.readNext();
1890             if (xml.isEndElement()) {
1891                 break;
1892             }
1893             if (xml.isStartElement()) {
1894                 if (xml.name() == QStringLiteral("title")) {
1895                     docInfo.set(Okular::DocumentInfo::Title, xml.readElementText());
1896                 } else if (xml.name() == QStringLiteral("subject")) {
1897                     docInfo.set(Okular::DocumentInfo::Subject, xml.readElementText());
1898                 } else if (xml.name() == QStringLiteral("description")) {
1899                     docInfo.set(Okular::DocumentInfo::Description, xml.readElementText());
1900                 } else if (xml.name() == QStringLiteral("creator")) {
1901                     docInfo.set(Okular::DocumentInfo::Creator, xml.readElementText());
1902                 } else if (xml.name() == QStringLiteral("category")) {
1903                     docInfo.set(Okular::DocumentInfo::Category, xml.readElementText());
1904                 } else if (xml.name() == QStringLiteral("created")) {
1905                     QDateTime createdDate = QDateTime::fromString(xml.readElementText(), QStringLiteral("yyyy-MM-ddThh:mm:ssZ"));
1906                     docInfo.set(Okular::DocumentInfo::CreationDate, QLocale().toString(createdDate, QLocale::LongFormat));
1907                 } else if (xml.name() == QStringLiteral("modified")) {
1908                     QDateTime modifiedDate = QDateTime::fromString(xml.readElementText(), QStringLiteral("yyyy-MM-ddThh:mm:ssZ"));
1909                     docInfo.set(Okular::DocumentInfo::ModificationDate, QLocale().toString(modifiedDate, QLocale::LongFormat));
1910                 } else if (xml.name() == QStringLiteral("keywords")) {
1911                     docInfo.set(Okular::DocumentInfo::Keywords, xml.readElementText());
1912                 } else if (xml.name() == QStringLiteral("revision")) {
1913                     docInfo.set(QStringLiteral("revision"), xml.readElementText(), i18n("Revision"));
1914                 }
1915             }
1916         }
1917         if (xml.error()) {
1918             qCWarning(OkularXpsDebug) << "Could not parse XPS core properties:" << xml.errorString();
1919         }
1920     } else {
1921         qCWarning(OkularXpsDebug) << "No core properties filename";
1922     }
1923 
1924     docInfo.set(Okular::DocumentInfo::Pages, QString::number(numPages()));
1925 
1926     return docInfo;
1927 }
1928 
1929 bool XpsFile::closeDocument()
1930 {
1931     m_documents.clear();
1932 
1933     return true;
1934 }
1935 
1936 int XpsFile::numPages() const
1937 {
1938     return m_pages.size();
1939 }
1940 
1941 int XpsFile::numDocuments() const
1942 {
1943     return m_documents.size();
1944 }
1945 
1946 XpsDocument *XpsFile::document(int documentNum) const
1947 {
1948     return m_documents.at(documentNum).get();
1949 }
1950 
1951 XpsPage *XpsFile::page(int pageNum) const
1952 {
1953     return m_pages.at(pageNum);
1954 }
1955 
1956 XpsGenerator::XpsGenerator(QObject *parent, const QVariantList &args)
1957     : Okular::Generator(parent, args)
1958     , m_xpsFile(nullptr)
1959 {
1960     setFeature(TextExtraction);
1961     setFeature(PrintNative);
1962     setFeature(PrintToFile);
1963     setFeature(Threaded);
1964     userMutex();
1965 }
1966 
1967 XpsGenerator::~XpsGenerator()
1968 {
1969 }
1970 
1971 bool XpsGenerator::loadDocument(const QString &fileName, QVector<Okular::Page *> &pagesVector)
1972 {
1973     m_xpsFile = std::make_unique<XpsFile>();
1974 
1975     bool result = m_xpsFile->loadDocument(fileName);
1976     if (!result) {
1977         return false;
1978     }
1979 
1980     pagesVector.resize(m_xpsFile->numPages());
1981 
1982     int pagesVectorOffset = 0;
1983 
1984     for (int docNum = 0; docNum < m_xpsFile->numDocuments(); ++docNum) {
1985         XpsDocument *doc = m_xpsFile->document(docNum);
1986         for (int pageNum = 0; pageNum < doc->numPages(); ++pageNum) {
1987             QSizeF pageSize = doc->page(pageNum)->size();
1988             pagesVector[pagesVectorOffset] = new Okular::Page(pagesVectorOffset, pageSize.width(), pageSize.height(), Okular::Rotation0);
1989             ++pagesVectorOffset;
1990         }
1991     }
1992 
1993     return true;
1994 }
1995 
1996 bool XpsGenerator::doCloseDocument()
1997 {
1998     m_xpsFile->closeDocument();
1999     m_xpsFile.reset();
2000 
2001     return true;
2002 }
2003 
2004 QImage XpsGenerator::image(Okular::PixmapRequest *request)
2005 {
2006     QMutexLocker lock(userMutex());
2007     QSize size((int)request->width(), (int)request->height());
2008     QImage image(size, QImage::Format_RGB32);
2009     XpsPage *pageToRender = m_xpsFile->page(request->page()->number());
2010     pageToRender->renderToImage(&image);
2011     return image;
2012 }
2013 
2014 Okular::TextPage *XpsGenerator::textPage(Okular::TextRequest *request)
2015 {
2016     QMutexLocker lock(userMutex());
2017     XpsPage *xpsPage = m_xpsFile->page(request->page()->number());
2018     return xpsPage->textPage();
2019 }
2020 
2021 Okular::DocumentInfo XpsGenerator::generateDocumentInfo(const QSet<Okular::DocumentInfo::Key> &keys) const
2022 {
2023     Q_UNUSED(keys);
2024 
2025     qCWarning(OkularXpsDebug) << "generating document metadata";
2026 
2027     return m_xpsFile->generateDocumentInfo();
2028 }
2029 
2030 const Okular::DocumentSynopsis *XpsGenerator::generateDocumentSynopsis()
2031 {
2032     qCWarning(OkularXpsDebug) << "generating document synopsis";
2033 
2034     // we only generate the synopsis for the first file.
2035     if (!m_xpsFile || !m_xpsFile->document(0)) {
2036         return nullptr;
2037     }
2038 
2039     if (m_xpsFile->document(0)->hasDocumentStructure()) {
2040         return m_xpsFile->document(0)->documentStructure();
2041     }
2042 
2043     return nullptr;
2044 }
2045 
2046 Okular::ExportFormat::List XpsGenerator::exportFormats() const
2047 {
2048     static Okular::ExportFormat::List formats;
2049     if (formats.isEmpty()) {
2050         formats.append(Okular::ExportFormat::standardFormat(Okular::ExportFormat::PlainText));
2051     }
2052     return formats;
2053 }
2054 
2055 bool XpsGenerator::exportTo(const QString &fileName, const Okular::ExportFormat &format)
2056 {
2057     if (format.mimeType().inherits(QStringLiteral("text/plain"))) {
2058         QFile f(fileName);
2059         if (!f.open(QIODevice::WriteOnly)) {
2060             return false;
2061         }
2062 
2063         QTextStream ts(&f);
2064         for (int i = 0; i < m_xpsFile->numPages(); ++i) {
2065             Okular::TextPage *textPage = m_xpsFile->page(i)->textPage();
2066             QString text = textPage->text();
2067             ts << text;
2068             ts << QLatin1Char('\n');
2069             delete textPage;
2070         }
2071         f.close();
2072 
2073         return true;
2074     }
2075 
2076     return false;
2077 }
2078 
2079 Okular::Document::PrintError XpsGenerator::print(QPrinter &printer)
2080 {
2081     QList<int> pageList = Okular::FilePrinter::pageList(printer, document()->pages(), document()->currentPage() + 1, document()->bookmarkedPageList());
2082 
2083     QPainter painter(&printer);
2084 
2085     for (int i = 0; i < pageList.count(); ++i) {
2086         if (i != 0) {
2087             printer.newPage();
2088         }
2089 
2090         const int page = pageList.at(i) - 1;
2091         XpsPage *pageToRender = m_xpsFile->page(page);
2092         pageToRender->renderToPainter(&painter);
2093     }
2094 
2095     return Okular::Document::NoPrintError;
2096 }
2097 
2098 const XpsRenderNode *XpsRenderNode::findChild(const QString &name) const
2099 {
2100     for (const XpsRenderNode &child : children) {
2101         if (child.name == name) {
2102             return &child;
2103         }
2104     }
2105 
2106     return nullptr;
2107 }
2108 
2109 QVariant XpsRenderNode::getRequiredChildData(const QString &name) const
2110 {
2111     const XpsRenderNode *child = findChild(name);
2112     if (child == nullptr) {
2113         qCWarning(OkularXpsDebug) << "Required element " << name << " is missing in " << this->name;
2114         return QVariant();
2115     }
2116 
2117     return child->data;
2118 }
2119 
2120 QVariant XpsRenderNode::getChildData(const QString &name) const
2121 {
2122     const XpsRenderNode *child = findChild(name);
2123     if (child == nullptr) {
2124         return QVariant();
2125     } else {
2126         return child->data;
2127     }
2128 }
2129 
2130 Q_LOGGING_CATEGORY(OkularXpsDebug, "org.kde.okular.generators.xps", QtWarningMsg)
2131 
2132 #include "generator_xps.moc"