diff --git a/generators/xps/generator_xps.cpp b/generators/xps/generator_xps.cpp index 69b5079c5..95acea323 100644 --- a/generators/xps/generator_xps.cpp +++ b/generators/xps/generator_xps.cpp @@ -1,2240 +1,2240 @@ /* Copyright (C) 2006, 2009 Brad Hards This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "generator_xps.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include OKULAR_EXPORT_PLUGIN(XpsGenerator, "libokularGenerator_xps.json") Q_DECLARE_METATYPE( QGradient* ) Q_DECLARE_METATYPE( XpsPathFigure* ) Q_DECLARE_METATYPE( XpsPathGeometry* ) // From Qt4 static int hex2int(char hex) { QChar hexchar = QLatin1Char(hex); int v; if (hexchar.isDigit()) v = hexchar.digitValue(); else if (hexchar >= QLatin1Char('A') && hexchar <= QLatin1Char('F')) v = hexchar.cell() - 'A' + 10; else if (hexchar >= QLatin1Char('a') && hexchar <= QLatin1Char('f')) v = hexchar.cell() - 'a' + 10; else v = -1; return v; } // Modified from Qt4 static QColor hexToRgba(const QByteArray &name) { const int len = name.length(); if (len == 0 || name[0] != '#') return QColor(); int r, g, b; int a = 255; if (len == 7) { r = (hex2int(name[1]) << 4) + hex2int(name[2]); g = (hex2int(name[3]) << 4) + hex2int(name[4]); b = (hex2int(name[5]) << 4) + hex2int(name[6]); } else if (len == 9) { a = (hex2int(name[1]) << 4) + hex2int(name[2]); r = (hex2int(name[3]) << 4) + hex2int(name[4]); g = (hex2int(name[5]) << 4) + hex2int(name[6]); b = (hex2int(name[7]) << 4) + hex2int(name[8]); } else { r = g = b = -1; } if ((uint)r > 255 || (uint)g > 255 || (uint)b > 255) { return QColor(); } return QColor(r,g,b,a); } static QRectF stringToRectF( const QString &data ) { QStringList numbers = data.split(QLatin1Char(',')); QPointF origin( numbers.at(0).toDouble(), numbers.at(1).toDouble() ); QSizeF size( numbers.at(2).toDouble(), numbers.at(3).toDouble() ); return QRectF( origin, size ); } static bool parseGUID( const QString &guidString, unsigned short guid[16]) { if (guidString.length() <= 35) { return false; } // Maps bytes to positions in guidString const static int indexes[] = {6, 4, 2, 0, 11, 9, 16, 14, 19, 21, 24, 26, 28, 30, 32, 34}; for (int i = 0; i < 16; i++) { int hex1 = hex2int(guidString[indexes[i]].cell()); int hex2 = hex2int(guidString[indexes[i]+1].cell()); if ((hex1 < 0) || (hex2 < 0)) { return false; } guid[i] = hex1 * 16 + hex2; } return true; } // Read next token of abbreviated path data static bool nextAbbPathToken(AbbPathToken *token) { int *curPos = &token->curPos; QString data = token->data; while ((*curPos < data.length()) && (data.at(*curPos).isSpace())) { (*curPos)++; } if (*curPos == data.length()) { token->type = abtEOF; return true; } QChar ch = data.at(*curPos); if (ch.isNumber() || (ch == QLatin1Char('+')) || (ch == QLatin1Char('-'))) { int start = *curPos; while ((*curPos < data.length()) && (!data.at(*curPos).isSpace()) && (data.at(*curPos) != QLatin1Char(',') && (!data.at(*curPos).isLetter() || data.at(*curPos) == QLatin1Char('e')))) { (*curPos)++; } token->number = data.midRef(start, *curPos - start).toDouble(); token->type = abtNumber; } else if (ch == QLatin1Char(',')) { token->type = abtComma; (*curPos)++; } else if (ch.isLetter()) { token->type = abtCommand; token->command = data.at(*curPos).cell(); (*curPos)++; } else { (*curPos)++; return false; } return true; } /** Read point (two reals delimited by comma) from abbreviated path data */ static QPointF getPointFromString(AbbPathToken *token, bool relative, const QPointF ¤tPosition) { //TODO Check grammar QPointF result; result.rx() = token->number; nextAbbPathToken(token); nextAbbPathToken(token); // , result.ry() = token->number; nextAbbPathToken(token); if (relative) { result += currentPosition; } return result; } /** Read point (two reals delimited by comma) from string */ static QPointF getPointFromString(const QString &string) { const int commaPos = string.indexOf(QLatin1Char(QLatin1Char(','))); if (commaPos == -1 || string.indexOf(QLatin1Char(QLatin1Char(',')), commaPos + 1) != -1) return QPointF(); QPointF result; bool ok = false; QStringRef ref = string.midRef(0, commaPos); result.setX(QString::fromRawData(ref.constData(), ref.count()).toDouble(&ok)); if (!ok) return QPointF(); ref = string.midRef(commaPos + 1); result.setY(QString::fromRawData(ref.constData(), ref.count()).toDouble(&ok)); if (!ok) return QPointF(); return result; } static Qt::FillRule fillRuleFromString( const QString &data, Qt::FillRule def = Qt::OddEvenFill ) { if ( data == QLatin1String( "EvenOdd" ) ) { return Qt::OddEvenFill; } else if ( data == QLatin1String( "NonZero" ) ) { return Qt::WindingFill; } return def; } /** Parse an abbreviated path "Data" description \param data the string containing the whitespace separated values \see XPS specification 4.2.3 and Appendix G */ static QPainterPath parseAbbreviatedPathData( const QString &data) { QPainterPath path; AbbPathToken token; token.data = data; token.curPos = 0; nextAbbPathToken(&token); // Used by Smooth cubic curve (command s) char lastCommand = ' '; QPointF lastSecondControlPoint; while (true) { if (token.type != abtCommand) { if (token.type != abtEOF) { qCWarning(OkularXpsDebug).nospace() << "Error in parsing abbreviated path data (" << token.type << "@" << token.curPos << "): " << data; } return path; } char command = QChar::fromLatin1(token.command).toLower().cell(); bool isRelative = QChar::fromLatin1(token.command).isLower(); QPointF currPos = path.currentPosition(); nextAbbPathToken(&token); switch (command) { case 'f': int rule; rule = (int)token.number; if (rule == 0) { path.setFillRule(Qt::OddEvenFill); } else if (rule == 1) { // In xps specs rule 1 means NonZero fill. I think it's equivalent to WindingFill but I'm not sure path.setFillRule(Qt::WindingFill); } nextAbbPathToken(&token); break; case 'm': // Move while (token.type == abtNumber) { QPointF point = getPointFromString(&token, isRelative, currPos); path.moveTo(point); } break; case 'l': // Line while (token.type == abtNumber) { QPointF point = getPointFromString(&token, isRelative, currPos); path.lineTo(point); } break; case 'h': // Horizontal line while (token.type == abtNumber) { double x = token.number; if ( isRelative ) x += path.currentPosition().x(); path.lineTo(x, path.currentPosition().y()); nextAbbPathToken(&token); } break; case 'v': // Vertical line while (token.type == abtNumber) { double y = token.number; if ( isRelative ) y += path.currentPosition().y(); path.lineTo(path.currentPosition().x(), y); nextAbbPathToken(&token); } break; case 'c': // Cubic bezier curve while (token.type == abtNumber) { QPointF firstControl = getPointFromString(&token, isRelative, currPos); QPointF secondControl = getPointFromString(&token, isRelative, currPos); QPointF endPoint = getPointFromString(&token, isRelative, currPos); path.cubicTo(firstControl, secondControl, endPoint); lastSecondControlPoint = secondControl; } break; case 'q': // Quadratic bezier curve while (token.type == abtNumber) { QPointF point1 = getPointFromString(&token, isRelative, currPos); QPointF point2 = getPointFromString(&token, isRelative, currPos); path.quadTo(point1, point2); } break; case 's': // Smooth cubic bezier curve while (token.type == abtNumber) { QPointF firstControl; if ((lastCommand == 's') || (lastCommand == 'c')) { firstControl = lastSecondControlPoint + (lastSecondControlPoint + path.currentPosition()); } else { firstControl = path.currentPosition(); } QPointF secondControl = getPointFromString(&token, isRelative, currPos); QPointF endPoint = getPointFromString(&token, isRelative, currPos); path.cubicTo(firstControl, secondControl, endPoint); } break; case 'a': // Arc //TODO Implement Arc drawing while (token.type == abtNumber) { /*QPointF rp =*/ getPointFromString(&token, isRelative, currPos); /*double r = token.number;*/ nextAbbPathToken(&token); /*double fArc = token.number; */ nextAbbPathToken(&token); /*double fSweep = token.number; */ nextAbbPathToken(&token); /*QPointF point = */getPointFromString(&token, isRelative, currPos); } break; case 'z': // Close path path.closeSubpath(); break; } lastCommand = command; } return path; } /** Parse a "Matrix" attribute string \param csv the comma separated list of values \return the QTransform corresponding to the affine transform given in the attribute \see XPS specification 7.4.1 */ static QTransform attsToMatrix( const QString &csv ) { QStringList values = csv.split( QLatin1Char(',') ); if ( values.count() != 6 ) { return QTransform(); // that is an identity matrix - no effect } 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() ); } /** \return Brush with given color or brush specified by reference to resource */ static QBrush parseRscRefColorForBrush( const QString &data ) { if (data[0] == QLatin1Char('{')) { //TODO qCWarning(OkularXpsDebug) << "Reference" << data; return QBrush(); } else { return QBrush( hexToRgba( data.toLatin1() ) ); } } /** \return Pen with given color or Pen specified by reference to resource */ static QPen parseRscRefColorForPen( const QString &data ) { if (data[0] == QLatin1Char('{')) { //TODO qCWarning(OkularXpsDebug) << "Reference" << data; return QPen(); } else { return QPen( hexToRgba( data.toLatin1() ) ); } } /** \return Matrix specified by given data or by referenced dictionary */ static QTransform parseRscRefMatrix( const QString &data ) { if (data[0] == QLatin1Char('{')) { //TODO qCWarning(OkularXpsDebug) << "Reference" << data; return QTransform(); } else { return attsToMatrix( data ); } } /** \return Path specified by given data or by referenced dictionary */ static QPainterPath parseRscRefPath( const QString &data ) { if (data[0] == QLatin1Char('{')) { //TODO qCWarning(OkularXpsDebug) << "Reference" << data; return QPainterPath(); } else { return parseAbbreviatedPathData( data ); } } /** \return The path of the entry */ static QString entryPath( const QString &entry ) { const int index = entry.lastIndexOf( QChar::fromLatin1( '/' ) ); QString ret = QLatin1String( "/" ) + entry.mid( 0, index ); if ( index > 0 ) { ret.append( QChar::fromLatin1( '/' ) ); } return ret; } /** \return The path of the entry */ static QString entryPath( const KZipFileEntry* entry ) { return entryPath( entry->path() ); } /** \return The absolute path of the \p location, according to \p path if it's non-absolute */ static QString absolutePath( const QString &path, const QString &location ) { QString retPath; if ( location.startsWith(QLatin1Char( '/' ) )) { // already absolute retPath = location; } else { QUrl u = QUrl::fromLocalFile(path); - QUrl u2 = QUrl::fromLocalFile(location); + QUrl u2 = QUrl(location); retPath = u.resolved(u2).toDisplayString(QUrl::PreferLocalFile); } // it seems paths & file names can also be percent-encoded // (XPS won't ever finish surprising me) if ( retPath.contains( QLatin1Char( '%' ) ) ) { retPath = QUrl::fromPercentEncoding( retPath.toUtf8() ); } return retPath; } /** Read the content of an archive entry in both the cases: a) single file + foobar b) directory + foobar/ + [0].piece + [1].piece + ... + [x].last.piece \see XPS specification 10.1.2 */ static QByteArray readFileOrDirectoryParts( const KArchiveEntry *entry, QString *pathOfFile = nullptr ) { QByteArray data; if ( entry->isDirectory() ) { const KArchiveDirectory* relDir = static_cast( entry ); QStringList entries = relDir->entries(); qSort( entries ); Q_FOREACH ( const QString &entry, entries ) { const KArchiveEntry* relSubEntry = relDir->entry( entry ); if ( !relSubEntry->isFile() ) continue; const KZipFileEntry* relSubFile = static_cast( relSubEntry ); data.append( relSubFile->data() ); } } else { const KZipFileEntry* relFile = static_cast( entry ); data.append( relFile->data() ); if ( pathOfFile ) { *pathOfFile = entryPath( relFile ); } } return data; } /** Load the resource \p fileName from the specified \p archive using the case sensitivity \p cs */ static const KArchiveEntry* loadEntry( KZip *archive, const QString &fileName, Qt::CaseSensitivity cs ) { // first attempt: loading the entry straight as requested const KArchiveEntry* entry = archive->directory()->entry( fileName ); // in case sensitive mode, or if we actually found something, return what we found if ( cs == Qt::CaseSensitive || entry ) { return entry; } QString path; QString entryName; const int index = fileName.lastIndexOf( QChar::fromLatin1( '/' ) ); if ( index > 0 ) { path = fileName.left( index ); entryName = fileName.mid( index + 1 ); } else { path = QLatin1Char('/'); entryName = fileName; } const KArchiveEntry * newEntry = archive->directory()->entry( path ); if ( newEntry->isDirectory() ) { const KArchiveDirectory* relDir = static_cast< const KArchiveDirectory * >( newEntry ); QStringList relEntries = relDir->entries(); qSort( relEntries ); Q_FOREACH ( const QString &relEntry, relEntries ) { if ( relEntry.compare( entryName, Qt::CaseInsensitive ) == 0 ) { return relDir->entry( relEntry ); } } } return nullptr; } static const KZipFileEntry* loadFile( KZip *archive, const QString &fileName, Qt::CaseSensitivity cs ) { const KArchiveEntry *entry = loadEntry( archive, fileName, cs ); return entry->isFile() ? static_cast< const KZipFileEntry * >( entry ) : nullptr; } /** \return The name of a resource from the \p fileName */ static QString resourceName( const QString &fileName ) { QString resource = fileName; const int slashPos = fileName.lastIndexOf( QLatin1Char( '/' ) ); const int dotPos = fileName.lastIndexOf( QLatin1Char( '.' ) ); if ( slashPos > -1 ) { if ( dotPos > -1 && dotPos > slashPos ) { resource = fileName.mid( slashPos + 1, dotPos - slashPos - 1 ); } else { resource = fileName.mid( slashPos + 1 ); } } return resource; } static QColor interpolatedColor( const QColor &c1, const QColor &c2 ) { QColor res; res.setAlpha( ( c1.alpha() + c2.alpha() ) / 2 ); res.setRed( ( c1.red() + c2.red() ) / 2 ); res.setGreen( ( c1.green() + c2.green() ) / 2 ); res.setBlue( ( c1.blue() + c2.blue() ) / 2 ); return res; } static bool xpsGradientLessThan( const XpsGradient &g1, const XpsGradient &g2 ) { return qFuzzyCompare( g1.offset, g2.offset ) ? g1.color.name() < g2.color.name() : g1.offset < g2.offset; } static int xpsGradientWithOffset( const QList &gradients, double offset ) { int i = 0; Q_FOREACH ( const XpsGradient &grad, gradients ) { if ( grad.offset == offset ) { return i; } ++i; } return -1; } /** Preprocess a list of gradients. \see XPS specification 11.3.1.1 */ static void preprocessXpsGradients( QList &gradients ) { if ( gradients.isEmpty() ) return; // sort the gradients (case 1.) qStableSort( gradients.begin(), gradients.end(), xpsGradientLessThan ); // no gradient with stop 0.0 (case 2.) if ( xpsGradientWithOffset( gradients, 0.0 ) == -1 ) { int firstGreaterThanZero = 0; while ( firstGreaterThanZero < gradients.count() && gradients.at( firstGreaterThanZero ).offset < 0.0 ) ++firstGreaterThanZero; // case 2.a: no gradients with stop less than 0.0 if ( firstGreaterThanZero == 0 ) { gradients.prepend( XpsGradient( 0.0, gradients.first().color ) ); } // case 2.b: some gradients with stop more than 0.0 else if ( firstGreaterThanZero != gradients.count() ) { QColor col1 = gradients.at( firstGreaterThanZero - 1 ).color; QColor col2 = gradients.at( firstGreaterThanZero ).color; for ( int i = 0; i < firstGreaterThanZero; ++i ) { gradients.removeFirst(); } gradients.prepend( XpsGradient( 0.0, interpolatedColor( col1, col2 ) ) ); } // case 2.c: no gradients with stop more than 0.0 else { XpsGradient newGrad( 0.0, gradients.last().color ); gradients.clear(); gradients.append( newGrad ); } } if ( gradients.isEmpty() ) return; // no gradient with stop 1.0 (case 3.) if ( xpsGradientWithOffset( gradients, 1.0 ) == -1 ) { int firstLessThanOne = gradients.count() - 1; while ( firstLessThanOne >= 0 && gradients.at( firstLessThanOne ).offset > 1.0 ) --firstLessThanOne; // case 2.a: no gradients with stop greater than 1.0 if ( firstLessThanOne == gradients.count() - 1 ) { gradients.append( XpsGradient( 1.0, gradients.last().color ) ); } // case 2.b: some gradients with stop more than 1.0 else if ( firstLessThanOne != -1 ) { QColor col1 = gradients.at( firstLessThanOne ).color; QColor col2 = gradients.at( firstLessThanOne + 1 ).color; for ( int i = firstLessThanOne + 1; i < gradients.count(); ++i ) { gradients.removeLast(); } gradients.append( XpsGradient( 1.0, interpolatedColor( col1, col2 ) ) ); } // case 2.c: no gradients with stop less than 1.0 else { XpsGradient newGrad( 1.0, gradients.first().color ); gradients.clear(); gradients.append( newGrad ); } } } static void addXpsGradientsToQGradient( const QList &gradients, QGradient *qgrad ) { Q_FOREACH ( const XpsGradient &grad, gradients ) { qgrad->setColorAt( grad.offset, grad.color ); } } static void applySpreadStyleToQGradient( const QString &style, QGradient *qgrad ) { if ( style.isEmpty() ) return; if ( style == QLatin1String( "Pad" ) ) { qgrad->setSpread( QGradient::PadSpread ); } else if ( style == QLatin1String( "Reflect" ) ) { qgrad->setSpread( QGradient::ReflectSpread ); } else if ( style == QLatin1String( "Repeat" ) ) { qgrad->setSpread( QGradient::RepeatSpread ); } } /** Read an UnicodeString \param string the raw value of UnicodeString \see XPS specification 5.1.4 */ static QString unicodeString( const QString &raw ) { QString ret; if ( raw.startsWith( QLatin1String( "{}" ) ) ) { ret = raw.mid( 2 ); } else { ret = raw; } return ret; } XpsHandler::XpsHandler(XpsPage *page): m_page(page) { m_painter = nullptr; } XpsHandler::~XpsHandler() { } bool XpsHandler::startDocument() { qCWarning(OkularXpsDebug) << "start document" << m_page->m_fileName ; XpsRenderNode node; node.name = QStringLiteral("document"); m_nodes.push(node); return true; } bool XpsHandler::startElement( const QString &nameSpace, const QString &localName, const QString &qname, const QXmlAttributes & atts ) { Q_UNUSED( nameSpace ) Q_UNUSED( qname ) XpsRenderNode node; node.name = localName; node.attributes = atts; processStartElement( node ); m_nodes.push(node); return true; } bool XpsHandler::endElement( const QString &nameSpace, const QString &localName, const QString &qname) { Q_UNUSED( nameSpace ) Q_UNUSED( qname ) XpsRenderNode node = m_nodes.pop(); if (node.name != localName) { qCWarning(OkularXpsDebug) << "Name doesn't match"; } processEndElement( node ); node.children.clear(); m_nodes.top().children.append(node); return true; } void XpsHandler::processGlyph( XpsRenderNode &node ) { //TODO Currently ignored attributes: CaretStops, DeviceFontName, IsSideways, OpacityMask, Name, FixedPage.NavigateURI, xml:lang, x:key //TODO Indices is only partially implemented //TODO Currently ignored child elements: Clip, OpacityMask //Handled separately: RenderTransform QString att; m_painter->save(); // Get font (doesn't work well because qt doesn't allow to load font from file) // 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. float fontSize = node.attributes.value(QStringLiteral("FontRenderingEmSize")).toFloat(); // qCWarning(OkularXpsDebug) << "Font Rendering EmSize:" << fontSize; // a value of 0.0 means the text is not visible (see XPS specs, chapter 12, "Glyphs") if ( fontSize < 0.1 ) { m_painter->restore(); return; } QFont font = m_page->m_file->getFontByName( node.attributes.value(QStringLiteral("FontUri")), fontSize ); att = node.attributes.value( QStringLiteral("StyleSimulations") ); if ( !att.isEmpty() ) { if ( att == QLatin1String( "ItalicSimulation" ) ) { font.setItalic( true ); } else if ( att == QLatin1String( "BoldSimulation" ) ) { font.setBold( true ); } else if ( att == QLatin1String( "BoldItalicSimulation" ) ) { font.setItalic( true ); font.setBold( true ); } } m_painter->setFont(font); //Origin QPointF origin( node.attributes.value(QStringLiteral("OriginX")).toDouble(), node.attributes.value(QStringLiteral("OriginY")).toDouble() ); //Fill QBrush brush; att = node.attributes.value(QStringLiteral("Fill")); if (att.isEmpty()) { QVariant data = node.getChildData( QStringLiteral("Glyphs.Fill") ); if (data.canConvert()) { brush = data.value(); } else { // no "Fill" attribute and no "Glyphs.Fill" child, so show nothing // (see XPS specs, 5.10) m_painter->restore(); return; } } else { brush = parseRscRefColorForBrush( att ); if ( brush.style() > Qt::NoBrush && brush.style() < Qt::LinearGradientPattern && brush.color().alpha() == 0 ) { m_painter->restore(); return; } } m_painter->setBrush( brush ); m_painter->setPen( QPen( brush, 0 ) ); // Opacity att = node.attributes.value(QStringLiteral("Opacity")); if (! att.isEmpty()) { bool ok = true; double value = att.toDouble( &ok ); if ( ok && value >= 0.1 ) { m_painter->setOpacity( value ); } else { m_painter->restore(); return; } } //RenderTransform att = node.attributes.value(QStringLiteral("RenderTransform")); if (!att.isEmpty()) { m_painter->setWorldTransform( parseRscRefMatrix( att ), true); } // Clip att = node.attributes.value( QStringLiteral("Clip") ); if ( !att.isEmpty() ) { QPainterPath clipPath = parseRscRefPath( att ); if ( !clipPath.isEmpty() ) { m_painter->setClipPath( clipPath ); } } // BiDiLevel - default Left-to-Right m_painter->setLayoutDirection( Qt::LeftToRight ); att = node.attributes.value( QStringLiteral("BiDiLevel") ); if ( !att.isEmpty() ) { if ( (att.toInt() % 2) == 1 ) { // odd BiDiLevel, so Right-to-Left m_painter->setLayoutDirection( Qt::RightToLeft ); } } // Indices - partial handling only att = node.attributes.value( QStringLiteral("Indices") ); QList advanceWidths; if ( ! att.isEmpty() ) { QStringList indicesElements = att.split( QLatin1Char(';') ); for( int i = 0; i < indicesElements.size(); ++i ) { if ( indicesElements.at(i).contains( QStringLiteral(",") ) ) { QStringList parts = indicesElements.at(i).split( QLatin1Char(',') ); if (parts.size() == 2 ) { // regular advance case, no offsets advanceWidths.append( parts.at(1).toDouble() * fontSize / 100.0 ); } else if (parts.size() == 3 ) { // regular advance case, with uOffset qreal AdvanceWidth = parts.at(1).toDouble() * fontSize / 100.0 ; qreal uOffset = parts.at(2).toDouble() / 100.0; advanceWidths.append( AdvanceWidth + uOffset ); } else { // has vertical offset, but don't know how to handle that yet qCWarning(OkularXpsDebug) << "Unhandled Indices element: " << indicesElements.at(i); advanceWidths.append( -1.0 ); } } else { // no special advance case advanceWidths.append( -1.0 ); } } } // UnicodeString QString stringToDraw( unicodeString( node.attributes.value( QStringLiteral("UnicodeString") ) ) ); QPointF originAdvance(0, 0); QFontMetrics metrics = m_painter->fontMetrics(); for ( int i = 0; i < stringToDraw.size(); ++i ) { QChar thisChar = stringToDraw.at( i ); m_painter->drawText( origin + originAdvance, QString( thisChar ) ); const qreal advanceWidth = advanceWidths.value( i, qreal(-1.0) ); if ( advanceWidth > 0.0 ) { originAdvance.rx() += advanceWidth; } else { originAdvance.rx() += metrics.width( thisChar ); } } // qCWarning(OkularXpsDebug) << "Glyphs: " << atts.value("Fill") << ", " << atts.value("FontUri"); // qCWarning(OkularXpsDebug) << " Origin: " << atts.value("OriginX") << "," << atts.value("OriginY"); // qCWarning(OkularXpsDebug) << " Unicode: " << atts.value("UnicodeString"); m_painter->restore(); } void XpsHandler::processFill( XpsRenderNode &node ) { //TODO Ignored child elements: VirtualBrush if (node.children.size() != 1) { qCWarning(OkularXpsDebug) << "Fill element should have exactly one child"; } else { node.data = node.children[0].data; } } void XpsHandler::processStroke( XpsRenderNode &node ) { //TODO Ignored child elements: VirtualBrush if (node.children.size() != 1) { qCWarning(OkularXpsDebug) << "Stroke element should have exactly one child"; } else { node.data = node.children[0].data; } } void XpsHandler::processImageBrush( XpsRenderNode &node ) { //TODO Ignored attributes: Opacity, x:key, TileMode, ViewBoxUnits, ViewPortUnits //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] QString att; QBrush brush; QRectF viewport = stringToRectF( node.attributes.value( QStringLiteral("Viewport") ) ); QRectF viewbox = stringToRectF( node.attributes.value( QStringLiteral("Viewbox") ) ); QImage image = m_page->loadImageFromFile( node.attributes.value( QStringLiteral("ImageSource") ) ); // Matrix which can transform [0, 0, 1, 1] rectangle to given viewbox QTransform viewboxMatrix = QTransform( viewbox.width() * image.physicalDpiX() / 96, 0, 0, viewbox.height() * image.physicalDpiY() / 96, viewbox.x(), viewbox.y() ); // Matrix which can transform [0, 0, 1, 1] rectangle to given viewport //TODO Take ViewPort into account QTransform viewportMatrix; att = node.attributes.value( QStringLiteral("Transform") ); if ( att.isEmpty() ) { QVariant data = node.getChildData( QStringLiteral("ImageBrush.Transform") ); if (data.canConvert()) { viewportMatrix = data.value(); } else { viewportMatrix = QTransform(); } } else { viewportMatrix = parseRscRefMatrix( att ); } viewportMatrix = viewportMatrix * QTransform( viewport.width(), 0, 0, viewport.height(), viewport.x(), viewport.y() ); brush = QBrush( image ); brush.setTransform( viewboxMatrix.inverted() * viewportMatrix ); node.data = qVariantFromValue( brush ); } void XpsHandler::processPath( XpsRenderNode &node ) { //TODO Ignored attributes: Clip, OpacityMask, StrokeEndLineCap, StorkeStartLineCap, Name, FixedPage.NavigateURI, xml:lang, x:key, AutomationProperties.Name, AutomationProperties.HelpText, SnapsToDevicePixels //TODO Ignored child elements: RenderTransform, Clip, OpacityMask // Handled separately: RenderTransform m_painter->save(); QString att; QVariant data; // Get path XpsPathGeometry * pathdata = node.getChildData( QStringLiteral("Path.Data") ).value< XpsPathGeometry * >(); att = node.attributes.value( QStringLiteral("Data") ); if (! att.isEmpty() ) { QPainterPath path = parseRscRefPath( att ); delete pathdata; pathdata = new XpsPathGeometry(); pathdata->paths.append( new XpsPathFigure( path, true ) ); } if ( !pathdata ) { // nothing to draw m_painter->restore(); return; } // Set Fill att = node.attributes.value( QStringLiteral("Fill") ); QBrush brush; if (! att.isEmpty() ) { brush = parseRscRefColorForBrush( att ); } else { data = node.getChildData( QStringLiteral("Path.Fill") ); if (data.canConvert()) { brush = data.value(); } } m_painter->setBrush( brush ); // Stroke (pen) att = node.attributes.value( QStringLiteral("Stroke") ); QPen pen( Qt::transparent ); if (! att.isEmpty() ) { pen = parseRscRefColorForPen( att ); } else { data = node.getChildData( QStringLiteral("Path.Stroke") ); if (data.canConvert()) { pen.setBrush( data.value() ); } } att = node.attributes.value( QStringLiteral("StrokeThickness") ); if (! att.isEmpty() ) { bool ok = false; int thickness = att.toInt( &ok ); if (ok) pen.setWidth( thickness ); } att = node.attributes.value( QStringLiteral("StrokeDashArray") ); if ( !att.isEmpty() ) { const QStringList pieces = att.split( QLatin1Char( ' ' ), QString::SkipEmptyParts ); QVector dashPattern( pieces.count() ); bool ok = false; for ( int i = 0; i < pieces.count(); ++i ) { qreal value = pieces.at( i ).toInt( &ok ); if ( ok ) { dashPattern[i] = value; } else { break; } } if ( ok ) { pen.setDashPattern( dashPattern ); } } att = node.attributes.value( QStringLiteral("StrokeDashOffset") ); if ( !att.isEmpty() ) { bool ok = false; int offset = att.toInt( &ok ); if ( ok ) pen.setDashOffset( offset ); } att = node.attributes.value( QStringLiteral("StrokeDashCap") ); if ( !att.isEmpty() ) { Qt::PenCapStyle cap = Qt::FlatCap; if ( att == QLatin1String( "Flat" ) ) { cap = Qt::FlatCap; } else if ( att == QLatin1String( "Round" ) ) { cap = Qt::RoundCap; } else if ( att == QLatin1String( "Square" ) ) { cap = Qt::SquareCap; } // ### missing "Triangle" pen.setCapStyle( cap ); } att = node.attributes.value( QStringLiteral("StrokeLineJoin") ); if ( !att.isEmpty() ) { Qt::PenJoinStyle joinStyle = Qt::MiterJoin; if ( att == QLatin1String( "Miter" ) ) { joinStyle = Qt::MiterJoin; } else if ( att == QLatin1String( "Bevel" ) ) { joinStyle = Qt::BevelJoin; } else if ( att == QLatin1String( "Round" ) ) { joinStyle = Qt::RoundJoin; } pen.setJoinStyle( joinStyle ); } att = node.attributes.value( QStringLiteral("StrokeMiterLimit") ); if ( !att.isEmpty() ) { bool ok = false; double limit = att.toDouble( &ok ); if ( ok ) { // we have to divide it by two, as XPS consider half of the stroke width, // while Qt the whole of it pen.setMiterLimit( limit / 2 ); } } m_painter->setPen( pen ); // Opacity att = node.attributes.value(QStringLiteral("Opacity")); if (! att.isEmpty()) { m_painter->setOpacity(att.toDouble()); } // RenderTransform att = node.attributes.value( QStringLiteral("RenderTransform") ); if (! att.isEmpty() ) { m_painter->setWorldTransform( parseRscRefMatrix( att ), true ); } if ( !pathdata->transform.isIdentity() ) { m_painter->setWorldTransform( pathdata->transform, true ); } Q_FOREACH ( XpsPathFigure *figure, pathdata->paths ) { m_painter->setBrush( figure->isFilled ? brush : QBrush() ); m_painter->drawPath( figure->path ); } delete pathdata; m_painter->restore(); } void XpsHandler::processPathData( XpsRenderNode &node ) { if (node.children.size() != 1) { qCWarning(OkularXpsDebug) << "Path.Data element should have exactly one child"; } else { node.data = node.children[0].data; } } void XpsHandler::processPathGeometry( XpsRenderNode &node ) { XpsPathGeometry * geom = new XpsPathGeometry(); Q_FOREACH ( const XpsRenderNode &child, node.children ) { if ( child.data.canConvert() ) { XpsPathFigure *figure = child.data.value(); geom->paths.append( figure ); } } QString att; att = node.attributes.value( QStringLiteral("Figures") ); if ( !att.isEmpty() ) { QPainterPath path = parseRscRefPath( att ); qDeleteAll( geom->paths ); geom->paths.clear(); geom->paths.append( new XpsPathFigure( path, true ) ); } att = node.attributes.value( QStringLiteral("FillRule") ); if ( !att.isEmpty() ) { geom->fillRule = fillRuleFromString( att ); } // Transform att = node.attributes.value( QStringLiteral("Transform") ); if ( !att.isEmpty() ) { geom->transform = parseRscRefMatrix( att ); } if ( !geom->paths.isEmpty() ) { node.data = qVariantFromValue( geom ); } else { delete geom; } } void XpsHandler::processPathFigure( XpsRenderNode &node ) { //TODO Ignored child elements: ArcSegment QString att; QPainterPath path; att = node.attributes.value( QStringLiteral("StartPoint") ); if ( !att.isEmpty() ) { QPointF point = getPointFromString( att ); path.moveTo( point ); } else { return; } Q_FOREACH ( const XpsRenderNode &child, node.children ) { bool isStroked = true; att = node.attributes.value( QStringLiteral("IsStroked") ); if ( !att.isEmpty() ) { isStroked = att == QLatin1String( "true" ); } if ( !isStroked ) { continue; } // PolyLineSegment if ( child.name == QLatin1String( "PolyLineSegment" ) ) { att = child.attributes.value( QStringLiteral("Points") ); if ( !att.isEmpty() ) { const QStringList points = att.split( QLatin1Char( ' ' ), QString::SkipEmptyParts ); Q_FOREACH ( const QString &p, points ) { QPointF point = getPointFromString( p ); path.lineTo( point ); } } } // PolyBezierSegment else if ( child.name == QLatin1String( "PolyBezierSegment" ) ) { att = child.attributes.value( QStringLiteral("Points") ); if ( !att.isEmpty() ) { const QStringList points = att.split( QLatin1Char( ' ' ), QString::SkipEmptyParts ); if ( points.count() % 3 == 0 ) { for ( int i = 0; i < points.count(); ) { QPointF firstControl = getPointFromString( points.at( i++ ) ); QPointF secondControl = getPointFromString( points.at( i++ ) ); QPointF endPoint = getPointFromString( points.at( i++ ) ); path.cubicTo(firstControl, secondControl, endPoint); } } } } // PolyQuadraticBezierSegment else if ( child.name == QLatin1String( "PolyQuadraticBezierSegment" ) ) { att = child.attributes.value( QStringLiteral("Points") ); if ( !att.isEmpty() ) { const QStringList points = att.split( QLatin1Char( ' ' ), QString::SkipEmptyParts ); if ( points.count() % 2 == 0 ) { for ( int i = 0; i < points.count(); ) { QPointF point1 = getPointFromString( points.at( i++ ) ); QPointF point2 = getPointFromString( points.at( i++ ) ); path.quadTo( point1, point2 ); } } } } } bool closePath = false; att = node.attributes.value( QStringLiteral("IsClosed") ); if ( !att.isEmpty() ) { closePath = att == QLatin1String( "true" ); } if ( closePath ) { path.closeSubpath(); } bool isFilled = true; att = node.attributes.value( QStringLiteral("IsFilled") ); if ( !att.isEmpty() ) { isFilled = att == QLatin1String( "true" ); } if ( !path.isEmpty() ) { node.data = qVariantFromValue( new XpsPathFigure( path, isFilled ) ); } } void XpsHandler::processStartElement( XpsRenderNode &node ) { if (node.name == QLatin1String("Canvas")) { m_painter->save(); QString att = node.attributes.value( QStringLiteral("RenderTransform") ); if ( !att.isEmpty() ) { m_painter->setWorldTransform( parseRscRefMatrix( att ), true ); } att = node.attributes.value( QStringLiteral("Opacity") ); if ( !att.isEmpty() ) { double value = att.toDouble(); if ( value > 0.0 && value <= 1.0 ) { m_painter->setOpacity( m_painter->opacity() * value ); } else { // setting manually to 0 is necessary to "disable" // all the stuff inside m_painter->setOpacity( 0.0 ); } } } } void XpsHandler::processEndElement( XpsRenderNode &node ) { if (node.name == QLatin1String("Glyphs")) { processGlyph( node ); } else if (node.name == QLatin1String("Path")) { processPath( node ); } else if (node.name == QLatin1String("MatrixTransform")) { //TODO Ignoring x:key node.data = qVariantFromValue( QTransform( attsToMatrix( node.attributes.value( QStringLiteral("Matrix") ) ) ) ); } else if ((node.name == QLatin1String("Canvas.RenderTransform")) || (node.name == QLatin1String("Glyphs.RenderTransform")) || (node.name == QLatin1String("Path.RenderTransform"))) { QVariant data = node.getRequiredChildData( QStringLiteral("MatrixTransform") ); if (data.canConvert()) { m_painter->setWorldTransform( data.value(), true ); } } else if (node.name == QLatin1String("Canvas")) { m_painter->restore(); } else if ((node.name == QLatin1String("Path.Fill")) || (node.name == QLatin1String("Glyphs.Fill"))) { processFill( node ); } else if (node.name == QLatin1String("Path.Stroke")) { processStroke( node ); } else if (node.name == QLatin1String("SolidColorBrush")) { //TODO Ignoring opacity, x:key node.data = qVariantFromValue( QBrush( QColor( hexToRgba( node.attributes.value( QStringLiteral("Color") ).toLatin1() ) ) ) ); } else if (node.name == QLatin1String("ImageBrush")) { processImageBrush( node ); } else if (node.name == QLatin1String("ImageBrush.Transform")) { node.data = node.getRequiredChildData( QStringLiteral("MatrixTransform") ); } else if (node.name == QLatin1String("LinearGradientBrush")) { XpsRenderNode * gradients = node.findChild( QStringLiteral("LinearGradientBrush.GradientStops") ); if ( gradients && gradients->data.canConvert< QGradient * >() ) { QPointF start = getPointFromString( node.attributes.value( QStringLiteral("StartPoint") ) ); QPointF end = getPointFromString( node.attributes.value( QStringLiteral("EndPoint") ) ); QLinearGradient * qgrad = static_cast< QLinearGradient * >( gradients->data.value< QGradient * >() ); qgrad->setStart( start ); qgrad->setFinalStop( end ); applySpreadStyleToQGradient( node.attributes.value( QStringLiteral("SpreadMethod") ), qgrad ); node.data = qVariantFromValue( QBrush( *qgrad ) ); delete qgrad; } } else if (node.name == QLatin1String("RadialGradientBrush")) { XpsRenderNode * gradients = node.findChild( QStringLiteral("RadialGradientBrush.GradientStops") ); if ( gradients && gradients->data.canConvert< QGradient * >() ) { QPointF center = getPointFromString( node.attributes.value( QStringLiteral("Center") ) ); QPointF origin = getPointFromString( node.attributes.value( QStringLiteral("GradientOrigin") ) ); double radiusX = node.attributes.value( QStringLiteral("RadiusX") ).toDouble(); double radiusY = node.attributes.value( QStringLiteral("RadiusY") ).toDouble(); QRadialGradient * qgrad = static_cast< QRadialGradient * >( gradients->data.value< QGradient * >() ); qgrad->setCenter( center ); qgrad->setFocalPoint( origin ); // TODO what in case of different radii? qgrad->setRadius( qMin( radiusX, radiusY ) ); applySpreadStyleToQGradient( node.attributes.value( QStringLiteral("SpreadMethod") ), qgrad ); node.data = qVariantFromValue( QBrush( *qgrad ) ); delete qgrad; } } else if (node.name == QLatin1String("LinearGradientBrush.GradientStops")) { QList gradients; Q_FOREACH ( const XpsRenderNode &child, node.children ) { double offset = child.attributes.value( QStringLiteral("Offset") ).toDouble(); QColor color = hexToRgba( child.attributes.value( QStringLiteral("Color") ).toLatin1() ); gradients.append( XpsGradient( offset, color ) ); } preprocessXpsGradients( gradients ); if ( !gradients.isEmpty() ) { QLinearGradient * qgrad = new QLinearGradient(); addXpsGradientsToQGradient( gradients, qgrad ); node.data = qVariantFromValue< QGradient * >( qgrad ); } } else if (node.name == QLatin1String("RadialGradientBrush.GradientStops")) { QList gradients; Q_FOREACH ( const XpsRenderNode &child, node.children ) { double offset = child.attributes.value( QStringLiteral("Offset") ).toDouble(); QColor color = hexToRgba( child.attributes.value( QStringLiteral("Color") ).toLatin1() ); gradients.append( XpsGradient( offset, color ) ); } preprocessXpsGradients( gradients ); if ( !gradients.isEmpty() ) { QRadialGradient * qgrad = new QRadialGradient(); addXpsGradientsToQGradient( gradients, qgrad ); node.data = qVariantFromValue< QGradient * >( qgrad ); } } else if (node.name == QLatin1String("PathFigure")) { processPathFigure( node ); } else if (node.name == QLatin1String("PathGeometry")) { processPathGeometry( node ); } else if (node.name == QLatin1String("Path.Data")) { processPathData( node ); } else { //qCWarning(OkularXpsDebug) << "Unknown element: " << node->name; } } XpsPage::XpsPage(XpsFile *file, const QString &fileName): m_file( file ), m_fileName( fileName ), m_pageIsRendered(false) { m_pageImage = nullptr; // qCWarning(OkularXpsDebug) << "page file name: " << fileName; const KZipFileEntry* pageFile = static_cast(m_file->xpsArchive()->directory()->entry( fileName )); QXmlStreamReader xml; xml.addData( readFileOrDirectoryParts( pageFile ) ); while ( !xml.atEnd() ) { xml.readNext(); if ( xml.isStartElement() && ( xml.name() == QStringLiteral("FixedPage") ) ) { QXmlStreamAttributes attributes = xml.attributes(); m_pageSize.setWidth( attributes.value( QStringLiteral("Width") ).toString().toDouble() ); m_pageSize.setHeight( attributes.value( QStringLiteral("Height") ).toString().toDouble() ); break; } } if ( xml.error() ) { qCWarning(OkularXpsDebug) << "Could not parse XPS page:" << xml.errorString(); } } XpsPage::~XpsPage() { delete m_pageImage; } bool XpsPage::renderToImage( QImage *p ) { if ((m_pageImage == nullptr) || (m_pageImage->size() != p->size())) { delete m_pageImage; m_pageImage = new QImage( p->size(), QImage::Format_ARGB32 ); // Set one point = one drawing unit. Useful for fonts, because xps specifies font size using drawing units, not points as usual m_pageImage->setDotsPerMeterX( 2835 ); m_pageImage->setDotsPerMeterY( 2835 ); m_pageIsRendered = false; } if (! m_pageIsRendered) { m_pageImage->fill( qRgba( 255, 255, 255, 255 ) ); QPainter painter( m_pageImage ); renderToPainter( &painter ); m_pageIsRendered = true; } *p = *m_pageImage; return true; } bool XpsPage::renderToPainter( QPainter *painter ) { XpsHandler handler( this ); handler.m_painter = painter; handler.m_painter->setWorldTransform(QTransform().scale((qreal)painter->device()->width() / size().width(), (qreal)painter->device()->height() / size().height())); QXmlSimpleReader parser; parser.setContentHandler( &handler ); parser.setErrorHandler( &handler ); const KZipFileEntry* pageFile = static_cast(m_file->xpsArchive()->directory()->entry( m_fileName )); QByteArray data = readFileOrDirectoryParts( pageFile ); QBuffer buffer( &data ); QXmlInputSource source( &buffer ); bool ok = parser.parse( source ); qCWarning(OkularXpsDebug) << "Parse result: " << ok; return true; } QSizeF XpsPage::size() const { return m_pageSize; } QFont XpsFile::getFontByName( const QString &fileName, float size ) { // qCWarning(OkularXpsDebug) << "trying to get font: " << fileName << ", size: " << size; int index = m_fontCache.value(fileName, -1); if (index == -1) { index = loadFontByName(fileName); m_fontCache[fileName] = index; } if ( index == -1 ) { qCWarning(OkularXpsDebug) << "Requesting uknown font:" << fileName; return QFont(); } const QStringList fontFamilies = m_fontDatabase.applicationFontFamilies( index ); if ( fontFamilies.isEmpty() ) { qCWarning(OkularXpsDebug) << "The unexpected has happened. No font family for a known font:" << fileName << index; return QFont(); } const QString fontFamily = fontFamilies[0]; const QStringList fontStyles = m_fontDatabase.styles( fontFamily ); if ( fontStyles.isEmpty() ) { qCWarning(OkularXpsDebug) << "The unexpected has happened. No font style for a known font family:" << fileName << index << fontFamily ; return QFont(); } const QString fontStyle = fontStyles[0]; return m_fontDatabase.font(fontFamily, fontStyle, qRound(size)); } int XpsFile::loadFontByName( const QString &fileName ) { // qCWarning(OkularXpsDebug) << "font file name: " << fileName; const KArchiveEntry* fontFile = loadEntry( m_xpsArchive, fileName, Qt::CaseInsensitive ); if ( !fontFile ) { return -1; } QByteArray fontData = readFileOrDirectoryParts( fontFile ); // once per file, according to the docs int result = m_fontDatabase.addApplicationFontFromData( fontData ); if (-1 == result) { // Try to deobfuscate font // TODO Use deobfuscation depending on font content type, don't do it always when standard loading fails const QString baseName = resourceName( fileName ); unsigned short guid[16]; if (!parseGUID(baseName, guid)) { qCWarning(OkularXpsDebug) << "File to load font - file name isn't a GUID"; } else { if (fontData.length() < 32) { qCWarning(OkularXpsDebug) << "Font file is too small"; } else { // Obfuscation - xor bytes in font binary with bytes from guid (font's filename) const static int mapping[] = {15, 14, 13, 12, 11, 10, 9, 8, 6, 7, 4, 5, 0, 1, 2, 3}; for (int i = 0; i < 16; i++) { fontData[i] = fontData[i] ^ guid[mapping[i]]; fontData[i+16] = fontData[i+16] ^ guid[mapping[i]]; } result = m_fontDatabase.addApplicationFontFromData( fontData ); } } } // qCWarning(OkularXpsDebug) << "Loaded font: " << m_fontDatabase.applicationFontFamilies( result ); return result; // a font ID } KZip * XpsFile::xpsArchive() { return m_xpsArchive; } QImage XpsPage::loadImageFromFile( const QString &fileName ) { // qCWarning(OkularXpsDebug) << "image file name: " << fileName; if ( fileName.at( 0 ) == QLatin1Char( '{' ) ) { // for example: '{ColorConvertedBitmap /Resources/bla.wdp /Resources/foobar.icc}' // TODO: properly read a ColorConvertedBitmap return QImage(); } QString absoluteFileName = absolutePath( entryPath( m_fileName ), fileName ); const KZipFileEntry* imageFile = loadFile( m_file->xpsArchive(), absoluteFileName, Qt::CaseInsensitive ); if ( !imageFile ) { // image not found return QImage(); } /* WORKAROUND: XPS standard requires to use 96dpi for images which doesn't have dpi specified (in file). When Qt loads such an image, it sets its dpi to qt_defaultDpi and doesn't allow to find out that it happend. To workaround this I used this procedure: load image, set its dpi to 96, load image again. When dpi isn't set in file, dpi set by me stays unchanged. Trolltech task ID: 159527. */ QImage image; QByteArray data = imageFile->data(); QBuffer buffer(&data); buffer.open(QBuffer::ReadOnly); QImageReader reader(&buffer); image = reader.read(); image.setDotsPerMeterX(qRound(96 / 0.0254)); image.setDotsPerMeterY(qRound(96 / 0.0254)); buffer.seek(0); reader.setDevice(&buffer); reader.read(&image); return image; } Okular::TextPage* XpsPage::textPage() { // qCWarning(OkularXpsDebug) << "Parsing XpsPage, text extraction"; Okular::TextPage* textPage = new Okular::TextPage(); const KZipFileEntry* pageFile = static_cast(m_file->xpsArchive()->directory()->entry( m_fileName )); QXmlStreamReader xml; xml.addData( readFileOrDirectoryParts( pageFile ) ); QTransform matrix = QTransform(); QStack matrices; matrices.push( QTransform() ); bool useMatrix = false; QXmlStreamAttributes glyphsAtts; while ( ! xml.atEnd() ) { xml.readNext(); if ( xml.isStartElement() ) { if ( xml.name() == QStringLiteral("Canvas")) { matrices.push(matrix); QString att = xml.attributes().value( QStringLiteral("RenderTransform") ).toString(); if (!att.isEmpty()) { matrix = parseRscRefMatrix( att ) * matrix; } } else if ((xml.name() == QStringLiteral("Canvas.RenderTransform")) || (xml.name() == QStringLiteral("Glyphs.RenderTransform"))) { useMatrix = true; } else if (xml.name() == QStringLiteral("MatrixTransform")) { if (useMatrix) { matrix = attsToMatrix( xml.attributes().value(QStringLiteral("Matrix")).toString() ) * matrix; } } else if (xml.name() == QStringLiteral("Glyphs")) { matrices.push( matrix ); glyphsAtts = xml.attributes(); } else if ( (xml.name() == QStringLiteral("Path")) || (xml.name() == QStringLiteral("Path.Fill")) || (xml.name() == QStringLiteral("SolidColorBrush")) || (xml.name() == QStringLiteral("ImageBrush")) || (xml.name() == QStringLiteral("ImageBrush.Transform")) || (xml.name() == QStringLiteral("Path.OpacityMask")) || (xml.name() == QStringLiteral("Path.Data")) || (xml.name() == QStringLiteral("PathGeometry")) || (xml.name() == QStringLiteral("PathFigure")) || (xml.name() == QStringLiteral("PolyLineSegment")) ) { // those are only graphical - no use in text handling } else if ( (xml.name() == QStringLiteral("FixedPage")) || (xml.name() == QStringLiteral("FixedPage.Resources")) ) { // not useful for text extraction } else { qCWarning(OkularXpsDebug) << "Unhandled element in Text Extraction start: " << xml.name().toString(); } } else if (xml.isEndElement() ) { if (xml.name() == QStringLiteral("Canvas")) { matrix = matrices.pop(); } else if ((xml.name() == QStringLiteral("Canvas.RenderTransform")) || (xml.name() == QStringLiteral("Glyphs.RenderTransform"))) { useMatrix = false; } else if (xml.name() == QStringLiteral("MatrixTransform")) { // not clear if we need to do anything here yet. } else if (xml.name() == QStringLiteral("Glyphs")) { QString att = glyphsAtts.value( QStringLiteral("RenderTransform") ).toString(); if (!att.isEmpty()) { matrix = parseRscRefMatrix( att ) * matrix; } QString text = unicodeString( glyphsAtts.value( QStringLiteral("UnicodeString") ).toString() ); // Get font (doesn't work well because qt doesn't allow to load font from file) QFont font = m_file->getFontByName( glyphsAtts.value( QStringLiteral("FontUri") ).toString(), glyphsAtts.value(QStringLiteral("FontRenderingEmSize")).toString().toFloat() * 72 / 96 ); QFontMetrics metrics = QFontMetrics( font ); // Origin QPointF origin( glyphsAtts.value(QStringLiteral("OriginX")).toString().toDouble(), glyphsAtts.value(QStringLiteral("OriginY")).toString().toDouble() ); int lastWidth = 0; for (int i = 0; i < text.length(); i++) { int width = metrics.width( text, i + 1 ); Okular::NormalizedRect * rect = new Okular::NormalizedRect( (origin.x() + lastWidth) / m_pageSize.width(), (origin.y() - metrics.height()) / m_pageSize.height(), (origin.x() + width) / m_pageSize.width(), origin.y() / m_pageSize.height() ); rect->transform( matrix ); textPage->append( text.mid(i, 1), rect ); lastWidth = width; } matrix = matrices.pop(); } else if ( (xml.name() == QStringLiteral("Path")) || (xml.name() == QStringLiteral("Path.Fill")) || (xml.name() == QStringLiteral("SolidColorBrush")) || (xml.name() == QStringLiteral("ImageBrush")) || (xml.name() == QStringLiteral("ImageBrush.Transform")) || (xml.name() == QStringLiteral("Path.OpacityMask")) || (xml.name() == QStringLiteral("Path.Data")) || (xml.name() == QStringLiteral("PathGeometry")) || (xml.name() == QStringLiteral("PathFigure")) || (xml.name() == QStringLiteral("PolyLineSegment")) ) { // those are only graphical - no use in text handling } else if ( (xml.name() == QStringLiteral("FixedPage")) || (xml.name() == QStringLiteral("FixedPage.Resources")) ) { // not useful for text extraction } else { qCWarning(OkularXpsDebug) << "Unhandled element in Text Extraction end: " << xml.name().toString(); } } } if ( xml.error() ) { qCWarning(OkularXpsDebug) << "Error parsing XpsPage text: " << xml.errorString(); } return textPage; } void XpsDocument::parseDocumentStructure( const QString &documentStructureFileName ) { qCWarning(OkularXpsDebug) << "document structure file name: " << documentStructureFileName; m_haveDocumentStructure = false; const KZipFileEntry* documentStructureFile = static_cast(m_file->xpsArchive()->directory()->entry( documentStructureFileName )); QXmlStreamReader xml; xml.addData( documentStructureFile->data() ); while ( !xml.atEnd() ) { xml.readNext(); if ( xml.isStartElement() ) { if ( xml.name() == QStringLiteral("DocumentStructure") ) { // just a container for optional outline and story elements - nothing to do here } else if ( xml.name() == QStringLiteral("DocumentStructure.Outline") ) { qCWarning(OkularXpsDebug) << "found DocumentStructure.Outline"; } else if ( xml.name() == QStringLiteral("DocumentOutline") ) { qCWarning(OkularXpsDebug) << "found DocumentOutline"; m_docStructure = new Okular::DocumentSynopsis; } else if ( xml.name() == QStringLiteral("OutlineEntry") ) { m_haveDocumentStructure = true; QXmlStreamAttributes attributes = xml.attributes(); int outlineLevel = attributes.value( QStringLiteral("OutlineLevel")).toString().toInt(); QString description = attributes.value(QStringLiteral("Description")).toString(); QDomElement synopsisElement = m_docStructure->createElement( description ); synopsisElement.setAttribute( QStringLiteral("OutlineLevel"), outlineLevel ); QString target = attributes.value(QStringLiteral("OutlineTarget")).toString(); int hashPosition = target.lastIndexOf( QLatin1Char('#') ); target = target.mid( hashPosition + 1 ); // qCWarning(OkularXpsDebug) << "target: " << target; Okular::DocumentViewport viewport; viewport.pageNumber = m_docStructurePageMap.value( target ); synopsisElement.setAttribute( QStringLiteral("Viewport"), viewport.toString() ); if ( outlineLevel == 1 ) { // qCWarning(OkularXpsDebug) << "Description: " // << outlineEntryElement.attribute( "Description" ) << endl; m_docStructure->appendChild( synopsisElement ); } else { // find the last next highest element (so it this is level 3, we need // to find the most recent level 2 node) QDomNode maybeParentNode = m_docStructure->lastChild(); while ( !maybeParentNode.isNull() ) { if ( maybeParentNode.toElement().attribute( QStringLiteral("OutlineLevel") ).toInt() == ( outlineLevel - 1 ) ) { // we have the right parent maybeParentNode.appendChild( synopsisElement ); break; } maybeParentNode = maybeParentNode.lastChild(); } } } else if ( xml.name() == QStringLiteral("Story") ) { // we need to handle Story here, but I have no idea what to do with it. } else if ( xml.name() == QStringLiteral("StoryFragment") ) { // we need to handle StoryFragment here, but I have no idea what to do with it. } else if ( xml.name() == QStringLiteral("StoryFragmentReference") ) { // we need to handle StoryFragmentReference here, but I have no idea what to do with it. } else { qCWarning(OkularXpsDebug) << "Unhandled entry in DocumentStructure: " << xml.name().toString(); } } } } const Okular::DocumentSynopsis * XpsDocument::documentStructure() { return m_docStructure; } bool XpsDocument::hasDocumentStructure() { return m_haveDocumentStructure; } XpsDocument::XpsDocument(XpsFile *file, const QString &fileName): m_file(file), m_haveDocumentStructure( false ), m_docStructure( nullptr ) { qCWarning(OkularXpsDebug) << "document file name: " << fileName; const KArchiveEntry* documentEntry = file->xpsArchive()->directory()->entry( fileName ); QString documentFilePath = fileName; const QString documentEntryPath = entryPath( fileName ); QXmlStreamReader docXml; docXml.addData( readFileOrDirectoryParts( documentEntry, &documentFilePath ) ); while( !docXml.atEnd() ) { docXml.readNext(); if ( docXml.isStartElement() ) { if ( docXml.name() == QStringLiteral("PageContent") ) { QString pagePath = docXml.attributes().value(QStringLiteral("Source")).toString(); qCWarning(OkularXpsDebug) << "Page Path: " << pagePath; XpsPage *page = new XpsPage( file, absolutePath( documentFilePath, pagePath ) ); m_pages.append(page); } else if ( docXml.name() == QStringLiteral("PageContent.LinkTargets") ) { // do nothing - wait for the real LinkTarget elements } else if ( docXml.name() == QStringLiteral("LinkTarget") ) { QString targetName = docXml.attributes().value( QStringLiteral("Name") ).toString(); if ( ! targetName.isEmpty() ) { m_docStructurePageMap[ targetName ] = m_pages.count() - 1; } } else if ( docXml.name() == QStringLiteral("FixedDocument") ) { // we just ignore this - it is just a container } else { qCWarning(OkularXpsDebug) << "Unhandled entry in FixedDocument: " << docXml.name().toString(); } } } if ( docXml.error() ) { qCWarning(OkularXpsDebug) << "Could not parse main XPS document file: " << docXml.errorString(); } // There might be a relationships entry for this document - typically used to tell us where to find the // content structure description // We should be able to find this using a reference from some other part of the document, but I can't see it. const int slashPosition = fileName.lastIndexOf( QLatin1Char('/') ); const QString documentRelationshipFile = absolutePath( documentEntryPath, QStringLiteral("_rels/") + fileName.mid( slashPosition + 1 ) + QStringLiteral(".rels") ); const KZipFileEntry* relFile = static_cast(file->xpsArchive()->directory()->entry(documentRelationshipFile)); QString documentStructureFile; if ( relFile ) { QXmlStreamReader xml; xml.addData( readFileOrDirectoryParts( relFile ) ); while ( !xml.atEnd() ) { xml.readNext(); if ( xml.isStartElement() && ( xml.name() == QStringLiteral("Relationship") ) ) { QXmlStreamAttributes attributes = xml.attributes(); if ( attributes.value( QStringLiteral("Type") ).toString() == QLatin1String("http://schemas.microsoft.com/xps/2005/06/documentstructure") ) { documentStructureFile = attributes.value( QStringLiteral("Target") ).toString(); } else { qCWarning(OkularXpsDebug) << "Unknown document relationships element: " << attributes.value( QStringLiteral("Type") ).toString() << " : " << attributes.value( QStringLiteral("Target") ).toString() << endl; } } } if ( xml.error() ) { qCWarning(OkularXpsDebug) << "Could not parse XPS page relationships file ( " << documentRelationshipFile << " ) - " << xml.errorString() << endl; } } else { // the page relationship file didn't exist in the zipfile // this isn't fatal qCWarning(OkularXpsDebug) << "Could not open Document relationship file from " << documentRelationshipFile; } if ( ! documentStructureFile.isEmpty() ) { // qCWarning(OkularXpsDebug) << "Document structure filename: " << documentStructureFile; // make the document path absolute documentStructureFile = absolutePath( documentEntryPath, documentStructureFile ); // qCWarning(OkularXpsDebug) << "Document structure absolute path: " << documentStructureFile; parseDocumentStructure( documentStructureFile ); } } XpsDocument::~XpsDocument() { for (int i = 0; i < m_pages.size(); i++) { delete m_pages.at( i ); } m_pages.clear(); if ( m_docStructure ) delete m_docStructure; } int XpsDocument::numPages() const { return m_pages.size(); } XpsPage* XpsDocument::page(int pageNum) const { return m_pages.at(pageNum); } XpsFile::XpsFile() { } XpsFile::~XpsFile() { m_fontCache.clear(); m_fontDatabase.removeAllApplicationFonts(); } bool XpsFile::loadDocument(const QString &filename) { m_xpsArchive = new KZip( filename ); if ( m_xpsArchive->open( QIODevice::ReadOnly ) == true ) { qCWarning(OkularXpsDebug) << "Successful open of " << m_xpsArchive->fileName(); } else { qCWarning(OkularXpsDebug) << "Could not open XPS archive: " << m_xpsArchive->fileName(); delete m_xpsArchive; return false; } // The only fixed entry in XPS is /_rels/.rels const KArchiveEntry* relEntry = m_xpsArchive->directory()->entry(QStringLiteral("_rels/.rels")); if ( !relEntry ) { // this might occur if we can't read the zip directory, or it doesn't have the relationships entry return false; } QXmlStreamReader relXml; relXml.addData( readFileOrDirectoryParts( relEntry ) ); QString fixedRepresentationFileName; // We work through the relationships document and pull out each element. while ( !relXml.atEnd() ) { relXml.readNext(); if ( relXml.isStartElement() ) { if ( relXml.name() == QStringLiteral("Relationship") ) { QXmlStreamAttributes attributes = relXml.attributes(); QString type = attributes.value( QStringLiteral("Type") ).toString(); QString target = attributes.value( QStringLiteral("Target") ).toString(); if ( QStringLiteral("http://schemas.openxmlformats.org/package/2006/relationships/metadata/thumbnail") == type ) { m_thumbnailFileName = target; } else if ( QStringLiteral("http://schemas.microsoft.com/xps/2005/06/fixedrepresentation") == type ) { fixedRepresentationFileName = target; } else if (QStringLiteral("http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties") == type) { m_corePropertiesFileName = target; } else if (QStringLiteral("http://schemas.openxmlformats.org/package/2006/relationships/digital-signature/origin") == type) { m_signatureOrigin = target; } else { qCWarning(OkularXpsDebug) << "Unknown relationships element: " << type << " : " << target; } } else if ( relXml.name() == QStringLiteral("Relationships") ) { // nothing to do here - this is just the container level } else { qCWarning(OkularXpsDebug) << "unexpected element in _rels/.rels: " << relXml.name().toString(); } } } if ( relXml.error() ) { qCWarning(OkularXpsDebug) << "Could not parse _rels/.rels: " << relXml.errorString(); return false; } if ( fixedRepresentationFileName.isEmpty() ) { // FixedRepresentation is a required part of the XPS document return false; } const KArchiveEntry* fixedRepEntry = m_xpsArchive->directory()->entry( fixedRepresentationFileName ); QString fixedRepresentationFilePath = fixedRepresentationFileName; QXmlStreamReader fixedRepXml; fixedRepXml.addData( readFileOrDirectoryParts( fixedRepEntry, &fixedRepresentationFileName ) ); while ( !fixedRepXml.atEnd() ) { fixedRepXml.readNext(); if ( fixedRepXml.isStartElement() ) { if ( fixedRepXml.name() == QStringLiteral("DocumentReference") ) { const QString source = fixedRepXml.attributes().value(QStringLiteral("Source")).toString(); XpsDocument *doc = new XpsDocument( this, absolutePath( fixedRepresentationFilePath, source ) ); for (int lv = 0; lv < doc->numPages(); ++lv) { // our own copy of the pages list m_pages.append( doc->page( lv ) ); } m_documents.append(doc); } else if ( fixedRepXml.name() == QStringLiteral("FixedDocumentSequence")) { // we don't do anything here - this is just a container for one or more DocumentReference elements } else { qCWarning(OkularXpsDebug) << "Unhandled entry in FixedDocumentSequence: " << fixedRepXml.name().toString(); } } } if ( fixedRepXml.error() ) { qCWarning(OkularXpsDebug) << "Could not parse FixedRepresentation file:" << fixedRepXml.errorString(); return false; } return true; } Okular::DocumentInfo XpsFile::generateDocumentInfo() const { Okular::DocumentInfo docInfo; docInfo.set( Okular::DocumentInfo::MimeType, QStringLiteral("application/oxps") ); if ( ! m_corePropertiesFileName.isEmpty() ) { const KZipFileEntry* corepropsFile = static_cast(m_xpsArchive->directory()->entry(m_corePropertiesFileName)); QXmlStreamReader xml; xml.addData( corepropsFile->data() ); while ( !xml.atEnd() ) { xml.readNext(); if ( xml.isEndElement() ) break; if ( xml.isStartElement() ) { if (xml.name() == QStringLiteral("title")) { docInfo.set( Okular::DocumentInfo::Title, xml.readElementText() ); } else if (xml.name() == QStringLiteral("subject")) { docInfo.set( Okular::DocumentInfo::Subject, xml.readElementText() ); } else if (xml.name() == QStringLiteral("description")) { docInfo.set( Okular::DocumentInfo::Description, xml.readElementText() ); } else if (xml.name() == QStringLiteral("creator")) { docInfo.set( Okular::DocumentInfo::Creator, xml.readElementText() ); } else if (xml.name() == QStringLiteral("category")) { docInfo.set( Okular::DocumentInfo::Category, xml.readElementText() ); } else if (xml.name() == QStringLiteral("created")) { QDateTime createdDate = QDateTime::fromString( xml.readElementText(), QStringLiteral("yyyy-MM-ddThh:mm:ssZ") ); docInfo.set( Okular::DocumentInfo::CreationDate, QLocale().toString( createdDate, QLocale::LongFormat ) ); } else if (xml.name() == QStringLiteral("modified")) { QDateTime modifiedDate = QDateTime::fromString( xml.readElementText(), QStringLiteral("yyyy-MM-ddThh:mm:ssZ") ); docInfo.set( Okular::DocumentInfo::ModificationDate, QLocale().toString( modifiedDate, QLocale::LongFormat ) ); } else if (xml.name() == QStringLiteral("keywords")) { docInfo.set( Okular::DocumentInfo::Keywords, xml.readElementText() ); } else if (xml.name() == QStringLiteral("revision")) { docInfo.set( QStringLiteral("revision"), xml.readElementText(), i18n( "Revision" ) ); } } } if ( xml.error() ) { qCWarning(OkularXpsDebug) << "Could not parse XPS core properties:" << xml.errorString(); } } else { qCWarning(OkularXpsDebug) << "No core properties filename"; } docInfo.set( Okular::DocumentInfo::Pages, QString::number(numPages()) ); return docInfo; } bool XpsFile::closeDocument() { qDeleteAll( m_documents ); m_documents.clear(); delete m_xpsArchive; return true; } int XpsFile::numPages() const { return m_pages.size(); } int XpsFile::numDocuments() const { return m_documents.size(); } XpsDocument* XpsFile::document(int documentNum) const { return m_documents.at( documentNum ); } XpsPage* XpsFile::page(int pageNum) const { return m_pages.at( pageNum ); } XpsGenerator::XpsGenerator( QObject *parent, const QVariantList &args ) : Okular::Generator( parent, args ), m_xpsFile( nullptr ) { setFeature( TextExtraction ); setFeature( PrintNative ); setFeature( PrintToFile ); // activate the threaded rendering iif: // 1) QFontDatabase says so // 2) Qt >= 4.4.0 (see Trolltech task ID: 169502) // 3) Qt >= 4.4.2 (see Trolltech task ID: 215090) if ( QFontDatabase::supportsThreadedFontRendering() ) setFeature( Threaded ); userMutex(); } XpsGenerator::~XpsGenerator() { } bool XpsGenerator::loadDocument( const QString & fileName, QVector & pagesVector ) { m_xpsFile = new XpsFile(); m_xpsFile->loadDocument( fileName ); pagesVector.resize( m_xpsFile->numPages() ); int pagesVectorOffset = 0; for (int docNum = 0; docNum < m_xpsFile->numDocuments(); ++docNum ) { XpsDocument *doc = m_xpsFile->document( docNum ); for (int pageNum = 0; pageNum < doc->numPages(); ++pageNum ) { QSizeF pageSize = doc->page( pageNum )->size(); pagesVector[pagesVectorOffset] = new Okular::Page( pagesVectorOffset, pageSize.width(), pageSize.height(), Okular::Rotation0 ); ++pagesVectorOffset; } } return true; } bool XpsGenerator::doCloseDocument() { m_xpsFile->closeDocument(); delete m_xpsFile; m_xpsFile = nullptr; return true; } QImage XpsGenerator::image( Okular::PixmapRequest * request ) { QMutexLocker lock( userMutex() ); QSize size( (int)request->width(), (int)request->height() ); QImage image( size, QImage::Format_RGB32 ); XpsPage *pageToRender = m_xpsFile->page( request->page()->number() ); pageToRender->renderToImage( &image ); return image; } Okular::TextPage* XpsGenerator::textPage( Okular::Page * page ) { QMutexLocker lock( userMutex() ); XpsPage * xpsPage = m_xpsFile->page( page->number() ); return xpsPage->textPage(); } Okular::DocumentInfo XpsGenerator::generateDocumentInfo( const QSet &keys ) const { Q_UNUSED(keys); qCWarning(OkularXpsDebug) << "generating document metadata"; return m_xpsFile->generateDocumentInfo(); } const Okular::DocumentSynopsis * XpsGenerator::generateDocumentSynopsis() { qCWarning(OkularXpsDebug) << "generating document synopsis"; // we only generate the synopsis for the first file. if ( !m_xpsFile || !m_xpsFile->document( 0 ) ) return nullptr; if ( m_xpsFile->document( 0 )->hasDocumentStructure() ) return m_xpsFile->document( 0 )->documentStructure(); return nullptr; } Okular::ExportFormat::List XpsGenerator::exportFormats() const { static Okular::ExportFormat::List formats; if ( formats.isEmpty() ) { formats.append( Okular::ExportFormat::standardFormat( Okular::ExportFormat::PlainText ) ); } return formats; } bool XpsGenerator::exportTo( const QString &fileName, const Okular::ExportFormat &format ) { if ( format.mimeType().inherits( QStringLiteral( "text/plain" ) ) ) { QFile f( fileName ); if ( !f.open( QIODevice::WriteOnly ) ) return false; QTextStream ts( &f ); for ( int i = 0; i < m_xpsFile->numPages(); ++i ) { Okular::TextPage* textPage = m_xpsFile->page(i)->textPage(); QString text = textPage->text(); ts << text; ts << QLatin1Char('\n'); delete textPage; } f.close(); return true; } return false; } bool XpsGenerator::print( QPrinter &printer ) { QList pageList = Okular::FilePrinter::pageList( printer, document()->pages(), document()->currentPage() + 1, document()->bookmarkedPageList() ); QPainter painter( &printer ); for ( int i = 0; i < pageList.count(); ++i ) { if ( i != 0 ) printer.newPage(); const int page = pageList.at( i ) - 1; XpsPage *pageToRender = m_xpsFile->page( page ); pageToRender->renderToPainter( &painter ); } return true; } XpsRenderNode * XpsRenderNode::findChild( const QString &name ) { for (int i = 0; i < children.size(); i++) { if (children[i].name == name) { return &children[i]; } } return nullptr; } QVariant XpsRenderNode::getRequiredChildData( const QString &name ) { XpsRenderNode * child = findChild( name ); if (child == nullptr) { qCWarning(OkularXpsDebug) << "Required element " << name << " is missing in " << this->name; return QVariant(); } return child->data; } QVariant XpsRenderNode::getChildData( const QString &name ) { XpsRenderNode * child = findChild( name ); if (child == nullptr) { return QVariant(); } else { return child->data; } } Q_LOGGING_CATEGORY(OkularXpsDebug, "org.kde.okular.generators.xps", QtWarningMsg) #include "generator_xps.moc"