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"