2 Copyright (C) 2006, 2009 Brad Hards <bradh@frogmouth.net>
4 This program is free software; you can redistribute it and/or
5 modify it under the terms of the GNU General Public License
6 as published by the Free Software Foundation; either version 2
7 of the License, or (at your option) any later version
9 This program is distributed in the hope that it will be useful,
10 but WITHOUT ANY WARRANTY; without even the implied warranty of
11 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 GNU General Public License for more details.
14 You should have received a copy of the GNU General Public License
15 along with this program; if not, write to the Free Software
16 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
20 #include "generator_xps.h"
22 #include <qdatetime.h>
28 #include <kaboutdata.h>
32 #include <ktemporaryfile.h>
35 #include <QImageReader>
38 #include <core/document.h>
39 #include <core/page.h>
40 #include <core/area.h>
41 #include <core/utils.h>
45 const int XpsDebug = 4712;
47 static KAboutData createAboutData()
52 ki18n( "XPS Backend" ),
54 ki18n( "An XPS backend" ),
55 KAboutData::License_GPL,
56 ki18n( "© 2006-2007 Brad Hards\n"
57 "© 2007 Jiri Klement\n"
58 "© 2008 Pino Toscano" )
60 aboutData.addAuthor( ki18n( "Brad Hards" ), KLocalizedString(), "bradh@frogmouth.net" );
61 aboutData.addAuthor( ki18n( "Jiri Klement" ), KLocalizedString(), "jiri.klement@gmail.com" );
62 aboutData.addAuthor( ki18n( "Pino Toscano" ), KLocalizedString(), "pino@kde.org" );
66 OKULAR_EXPORT_PLUGIN( XpsGenerator, createAboutData() )
68 Q_DECLARE_METATYPE( QGradient* )
69 Q_DECLARE_METATYPE( XpsPathFigure* )
70 Q_DECLARE_METATYPE( XpsPathGeometry* )
73 static int hex2int(char hex)
75 QChar hexchar = QLatin1Char(hex);
77 if (hexchar.isDigit())
78 v = hexchar.digitValue();
79 else if (hexchar >= QLatin1Char('A') && hexchar <= QLatin1Char('F'))
80 v = hexchar.cell() - 'A' + 10;
81 else if (hexchar >= QLatin1Char('a') && hexchar <= QLatin1Char('f'))
82 v = hexchar.cell() - 'a' + 10;
89 static QColor hexToRgba(const char *name)
93 name++; // eat the leading '#'
94 int len = qstrlen(name);
98 r = (hex2int(name[0]) << 4) + hex2int(name[1]);
99 g = (hex2int(name[2]) << 4) + hex2int(name[3]);
100 b = (hex2int(name[4]) << 4) + hex2int(name[5]);
101 } else if (len == 8) {
102 a = (hex2int(name[0]) << 4) + hex2int(name[1]);
103 r = (hex2int(name[2]) << 4) + hex2int(name[3]);
104 g = (hex2int(name[4]) << 4) + hex2int(name[5]);
105 b = (hex2int(name[6]) << 4) + hex2int(name[7]);
109 if ((uint)r > 255 || (uint)g > 255 || (uint)b > 255) {
112 return QColor(r,g,b,a);
115 static QRectF stringToRectF( const QString &data )
117 QStringList numbers = data.split(',');
118 QPointF origin( numbers.at(0).toDouble(), numbers.at(1).toDouble() );
119 QSizeF size( numbers.at(2).toDouble(), numbers.at(3).toDouble() );
120 return QRectF( origin, size );
123 // Read next token of abbreviated path data
124 static bool nextAbbPathToken(AbbPathToken *token)
126 int *curPos = &token->curPos;
127 QString data = token->data;
129 while ((*curPos < data.length()) && (data.at(*curPos).isSpace()))
134 if (*curPos == data.length())
136 token->type = abtEOF;
140 QChar ch = data.at(*curPos);
142 if (ch.isNumber() || (ch == '+') || (ch == '-'))
145 while ((*curPos < data.length()) && (!data.at(*curPos).isSpace()) && (data.at(*curPos) != ',') && (!data.at(*curPos).isLetter() || data.at(*curPos) == 'e'))
149 token->number = data.mid(start, *curPos - start).toDouble();
150 token->type = abtNumber;
152 } else if (ch == ',')
154 token->type = abtComma;
156 } else if (ch.isLetter())
158 token->type = abtCommand;
159 token->command = data.at(*curPos).cell();
171 Read point (two reals delimited by comma) from abbreviated path data
173 static QPointF getPointFromString(AbbPathToken *token, bool relative, const QPointF ¤tPosition) {
177 result.rx() = token->number;
178 nextAbbPathToken(token);
179 nextAbbPathToken(token); // ,
180 result.ry() = token->number;
181 nextAbbPathToken(token);
185 result += currentPosition;
192 Read point (two reals delimited by comma) from string
194 static QPointF getPointFromString(const QString &string)
196 const int commaPos = string.indexOf(QLatin1Char(','));
197 if (commaPos == -1 || string.indexOf(QLatin1Char(','), commaPos + 1) != -1)
202 QStringRef ref = string.midRef(0, commaPos);
203 result.setX(QString::fromRawData(ref.constData(), ref.count()).toDouble(&ok));
207 ref = string.midRef(commaPos + 1);
208 result.setY(QString::fromRawData(ref.constData(), ref.count()).toDouble(&ok));
215 static Qt::FillRule fillRuleFromString( const QString &data, Qt::FillRule def = Qt::OddEvenFill )
217 if ( data == QLatin1String( "EvenOdd" ) ) {
218 return Qt::OddEvenFill;
219 } else if ( data == QLatin1String( "NonZero" ) ) {
220 return Qt::WindingFill;
226 Parse an abbreviated path "Data" description
227 \param data the string containing the whitespace separated values
229 \see XPS specification 4.2.3 and Appendix G
231 static QPainterPath parseAbbreviatedPathData( const QString &data)
240 nextAbbPathToken(&token);
242 // Used by Smooth cubic curve (command s)
243 char lastCommand = ' ';
244 QPointF lastSecondControlPoint;
248 if (token.type != abtCommand)
250 if (token.type != abtEOF)
252 kDebug(XpsDebug).nospace() << "Error in parsing abbreviated path data (" << token.type << "@" << token.curPos << "): " << data;
257 char command = QChar(token.command).toLower().cell();
258 bool isRelative = QChar(token.command).isLower();
259 QPointF currPos = path.currentPosition();
260 nextAbbPathToken(&token);
265 rule = (int)token.number;
268 path.setFillRule(Qt::OddEvenFill);
272 // In xps specs rule 1 means NonZero fill. I think it's equivalent to WindingFill but I'm not sure
273 path.setFillRule(Qt::WindingFill);
275 nextAbbPathToken(&token);
278 while (token.type == abtNumber)
280 QPointF point = getPointFromString(&token, isRelative, currPos);
285 while (token.type == abtNumber)
287 QPointF point = getPointFromString(&token, isRelative, currPos);
291 case 'h': // Horizontal line
292 while (token.type == abtNumber)
294 double x = token.number;
296 x += path.currentPosition().x();
297 path.lineTo(x, path.currentPosition().y());
298 nextAbbPathToken(&token);
301 case 'v': // Vertical line
302 while (token.type == abtNumber)
304 double y = token.number;
306 y += path.currentPosition().y();
307 path.lineTo(path.currentPosition().x(), y);
308 nextAbbPathToken(&token);
311 case 'c': // Cubic bezier curve
312 while (token.type == abtNumber)
314 QPointF firstControl = getPointFromString(&token, isRelative, currPos);
315 QPointF secondControl = getPointFromString(&token, isRelative, currPos);
316 QPointF endPoint = getPointFromString(&token, isRelative, currPos);
317 path.cubicTo(firstControl, secondControl, endPoint);
319 lastSecondControlPoint = secondControl;
322 case 'q': // Quadratic bezier curve
323 while (token.type == abtNumber)
325 QPointF point1 = getPointFromString(&token, isRelative, currPos);
326 QPointF point2 = getPointFromString(&token, isRelative, currPos);
327 path.quadTo(point1, point2);
330 case 's': // Smooth cubic bezier curve
331 while (token.type == abtNumber)
333 QPointF firstControl;
334 if ((lastCommand == 's') || (lastCommand == 'c'))
336 firstControl = lastSecondControlPoint + (lastSecondControlPoint + path.currentPosition());
340 firstControl = path.currentPosition();
342 QPointF secondControl = getPointFromString(&token, isRelative, currPos);
343 QPointF endPoint = getPointFromString(&token, isRelative, currPos);
344 path.cubicTo(firstControl, secondControl, endPoint);
348 //TODO Implement Arc drawing
349 while (token.type == abtNumber)
351 /*QPointF rp =*/ getPointFromString(&token, isRelative, currPos);
352 /*double r = token.number;*/
353 nextAbbPathToken(&token);
354 /*double fArc = token.number; */
355 nextAbbPathToken(&token);
356 /*double fSweep = token.number; */
357 nextAbbPathToken(&token);
358 /*QPointF point = */getPointFromString(&token, isRelative, currPos);
361 case 'z': // Close path
366 lastCommand = command;
373 Parse a "Matrix" attribute string
374 \param csv the comma separated list of values
375 \return the QTransform corresponding to the affine transform
376 given in the attribute
378 \see XPS specification 7.4.1
380 static QTransform attsToMatrix( const QString &csv )
382 QStringList values = csv.split( ',' );
383 if ( values.count() != 6 ) {
384 return QTransform(); // that is an identity matrix - no effect
386 return QTransform( values.at(0).toDouble(), values.at(1).toDouble(),
387 values.at(2).toDouble(), values.at(3).toDouble(),
388 values.at(4).toDouble(), values.at(5).toDouble() );
392 \return Brush with given color or brush specified by reference to resource
394 static QBrush parseRscRefColorForBrush( const QString &data )
396 if (data[0] == '{') {
398 kDebug(XpsDebug) << "Reference" << data;
401 return QBrush( hexToRgba( data.toLatin1() ) );
406 \return Pen with given color or Pen specified by reference to resource
408 static QPen parseRscRefColorForPen( const QString &data )
410 if (data[0] == '{') {
412 kDebug(XpsDebug) << "Reference" << data;
415 return QPen( hexToRgba( data.toLatin1() ) );
420 \return Matrix specified by given data or by referenced dictionary
422 static QTransform parseRscRefMatrix( const QString &data )
424 if (data[0] == '{') {
426 kDebug(XpsDebug) << "Reference" << data;
429 return attsToMatrix( data );
434 \return Path specified by given data or by referenced dictionary
436 static QPainterPath parseRscRefPath( const QString &data )
438 if (data[0] == '{') {
440 kDebug(XpsDebug) << "Reference" << data;
441 return QPainterPath();
443 return parseAbbreviatedPathData( data );
448 \return The filepath of the entry
450 static QString entryFilePath(const QString &entry )
453 if ( ret[ 0 ] == QLatin1Char( '/') ) {
454 ret = ret.mid( 1, ret.size() - 1);
460 \return The path of the entry
462 static QString entryPath(const QString &entry )
464 const int index = entry.lastIndexOf( QChar::fromLatin1( '/' ) );
465 QString ret = entry.mid( 0, index );
467 ret.append( QChar::fromLatin1( '/' ) );
473 \return The path of the entry
475 static QString entryPath( const KArchiveEntry* entry )
477 return QFile::decodeName(entry->pathname);
481 \return The absolute path of the \p location, according to \p path if it's non-absolute
482 \note ZIP paths are not actually absolute, they must not start with slash
484 static QString absolutePath( const QString &path, const QString &location )
487 if ( location.at( 0 ) == QLatin1Char( '/' ) ) {
489 retPath = location.mid(1, location.size() - 1);
491 KUrl url = KUrl::fromPath( path );
492 url.setFileName( location );
493 retPath = url.toLocalFile();
495 // it seems paths & file names can also be percent-encoded
496 // (XPS won't ever finish surprising me)
497 if ( retPath.contains( QLatin1Char( '%' ) ) ) {
498 retPath = QUrl::fromPercentEncoding( retPath.toUtf8() );
504 Read the content of an archive entry in both the cases:
514 \see XPS specification 10.1.2
516 static QByteArray readFileOrDirectoryParts(KArchive *archive, const KArchiveEntry *entry, QString *pathOfFile = 0 )
519 if ( S_ISDIR(entry->mode) ) {
520 const QList<KArchiveEntry> entries = archive->list( entry->pathname );
521 Q_FOREACH ( const KArchiveEntry &ee, entries ) {
522 if ( !S_ISREG(ee.mode) )
525 data.append( archive->data( ee.pathname ) );
528 data.append( archive->data( entry->pathname) );
530 *pathOfFile = entryPath( entry );
537 Load the resource \p fileName from the specified \p archive using the case sensitivity \p cs
539 static const KArchiveEntry loadEntry( KArchive *archive, const QString &fileName, Qt::CaseSensitivity cs )
541 // first attempt: loading the entry straight as requested
542 const KArchiveEntry entry = archive->entry( fileName );
543 // in case sensitive mode, or if we actually found something, return what we found
544 if ( cs == Qt::CaseSensitive || !entry.isNull() ) {
550 const int index = fileName.lastIndexOf( QChar::fromLatin1( '/' ) );
553 path = fileName.left( index );
554 entryName = fileName.mid( index + 1 );
557 entryName = fileName;
559 const KArchiveEntry newEntry = archive->entry( path );
560 if ( S_ISDIR(newEntry.mode) ) {
561 Q_FOREACH ( const KArchiveEntry &relEntry, archive->list( newEntry.pathname ) ) {
562 if ( QFile::decodeName(relEntry.pathname).compare( entryName, Qt::CaseInsensitive ) == 0 ) {
567 return KArchiveEntry();
570 static QColor interpolatedColor( const QColor &c1, const QColor &c2 )
573 res.setAlpha( ( c1.alpha() + c2.alpha() ) / 2 );
574 res.setRed( ( c1.red() + c2.red() ) / 2 );
575 res.setGreen( ( c1.green() + c2.green() ) / 2 );
576 res.setBlue( ( c1.blue() + c2.blue() ) / 2 );
580 static bool xpsGradientLessThan( const XpsGradient &g1, const XpsGradient &g2 )
582 return qFuzzyCompare( g1.offset, g2.offset )
583 ? g1.color.name() < g2.color.name()
584 : g1.offset < g2.offset;
587 static int xpsGradientWithOffset( const QList<XpsGradient> &gradients, double offset )
590 Q_FOREACH ( const XpsGradient &grad, gradients ) {
591 if ( grad.offset == offset ) {
600 Preprocess a list of gradients.
602 \see XPS specification 11.3.1.1
604 static void preprocessXpsGradients( QList<XpsGradient> &gradients )
606 if ( gradients.isEmpty() )
609 // sort the gradients (case 1.)
610 qStableSort( gradients.begin(), gradients.end(), xpsGradientLessThan );
612 // no gradient with stop 0.0 (case 2.)
613 if ( xpsGradientWithOffset( gradients, 0.0 ) == -1 ) {
614 int firstGreaterThanZero = 0;
615 while ( firstGreaterThanZero < gradients.count() && gradients.at( firstGreaterThanZero ).offset < 0.0 )
616 ++firstGreaterThanZero;
617 // case 2.a: no gradients with stop less than 0.0
618 if ( firstGreaterThanZero == 0 ) {
619 gradients.prepend( XpsGradient( 0.0, gradients.first().color ) );
621 // case 2.b: some gradients with stop more than 0.0
622 else if ( firstGreaterThanZero != gradients.count() ) {
623 QColor col1 = gradients.at( firstGreaterThanZero - 1 ).color;
624 QColor col2 = gradients.at( firstGreaterThanZero ).color;
625 for ( int i = 0; i < firstGreaterThanZero; ++i ) {
626 gradients.removeFirst();
628 gradients.prepend( XpsGradient( 0.0, interpolatedColor( col1, col2 ) ) );
630 // case 2.c: no gradients with stop more than 0.0
632 XpsGradient newGrad( 0.0, gradients.last().color );
634 gradients.append( newGrad );
638 if ( gradients.isEmpty() )
641 // no gradient with stop 1.0 (case 3.)
642 if ( xpsGradientWithOffset( gradients, 1.0 ) == -1 ) {
643 int firstLessThanOne = gradients.count() - 1;
644 while ( firstLessThanOne >= 0 && gradients.at( firstLessThanOne ).offset > 1.0 )
646 // case 2.a: no gradients with stop greater than 1.0
647 if ( firstLessThanOne == gradients.count() - 1 ) {
648 gradients.append( XpsGradient( 1.0, gradients.last().color ) );
650 // case 2.b: some gradients with stop more than 1.0
651 else if ( firstLessThanOne != -1 ) {
652 QColor col1 = gradients.at( firstLessThanOne ).color;
653 QColor col2 = gradients.at( firstLessThanOne + 1 ).color;
654 for ( int i = firstLessThanOne + 1; i < gradients.count(); ++i ) {
655 gradients.removeLast();
657 gradients.append( XpsGradient( 1.0, interpolatedColor( col1, col2 ) ) );
659 // case 2.c: no gradients with stop less than 1.0
661 XpsGradient newGrad( 1.0, gradients.first().color );
663 gradients.append( newGrad );
668 static void addXpsGradientsToQGradient( const QList<XpsGradient> &gradients, QGradient *qgrad )
670 Q_FOREACH ( const XpsGradient &grad, gradients ) {
671 qgrad->setColorAt( grad.offset, grad.color );
675 static void applySpreadStyleToQGradient( const QString &style, QGradient *qgrad )
677 if ( style.isEmpty() )
680 if ( style == QLatin1String( "Pad" ) ) {
681 qgrad->setSpread( QGradient::PadSpread );
682 } else if ( style == QLatin1String( "Reflect" ) ) {
683 qgrad->setSpread( QGradient::ReflectSpread );
684 } else if ( style == QLatin1String( "Repeat" ) ) {
685 qgrad->setSpread( QGradient::RepeatSpread );
690 Read an UnicodeString
691 \param string the raw value of UnicodeString
693 \see XPS specification 5.1.4
695 static QString unicodeString( const QString &raw )
698 if ( raw.startsWith( QLatin1String( "{}" ) ) ) {
707 XpsHandler::XpsHandler(XpsPage *page): m_page(page)
712 XpsHandler::~XpsHandler()
716 bool XpsHandler::startDocument()
718 kDebug(XpsDebug) << "start document" << m_page->m_fileName ;
721 node.name = "document";
727 bool XpsHandler::startElement( const QString &nameSpace,
728 const QString &localName,
729 const QString &qname,
730 const QXmlAttributes & atts )
732 Q_UNUSED( nameSpace )
736 node.name = localName;
737 node.attributes = atts;
738 processStartElement( node );
745 bool XpsHandler::endElement( const QString &nameSpace,
746 const QString &localName,
747 const QString &qname)
749 Q_UNUSED( nameSpace )
753 XpsRenderNode node = m_nodes.pop();
754 if (node.name != localName) {
755 kDebug(XpsDebug) << "Name doesn't match";
757 processEndElement( node );
758 node.children.clear();
759 m_nodes.top().children.append(node);
764 void XpsHandler::processGlyph( XpsRenderNode &node )
766 //TODO Currently ignored attributes: CaretStops, DeviceFontName, IsSideways, OpacityMask, Name, FixedPage.NavigateURI, xml:lang, x:key
767 //TODO Indices is only partially implemented
768 //TODO Currently ignored child elements: Clip, OpacityMask
769 //Handled separately: RenderTransform
775 // Get font (doesn't work well because qt doesn't allow to load font from file)
776 // 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.
777 float fontSize = node.attributes.value("FontRenderingEmSize").toFloat();
778 // kDebug(XpsDebug) << "Font Rendering EmSize:" << fontSize;
779 // a value of 0.0 means the text is not visible (see XPS specs, chapter 12, "Glyphs")
780 if ( fontSize < 0.1 ) {
781 m_painter->restore();
784 QFont font = m_page->m_file->getFontByName( node.attributes.value("FontUri"), fontSize );
785 att = node.attributes.value( "StyleSimulations" );
786 if ( !att.isEmpty() ) {
787 if ( att == QLatin1String( "ItalicSimulation" ) ) {
788 font.setItalic( true );
789 } else if ( att == QLatin1String( "BoldSimulation" ) ) {
790 font.setBold( true );
791 } else if ( att == QLatin1String( "BoldItalicSimulation" ) ) {
792 font.setItalic( true );
793 font.setBold( true );
796 m_painter->setFont(font);
799 QPointF origin( node.attributes.value("OriginX").toDouble(), node.attributes.value("OriginY").toDouble() );
803 att = node.attributes.value("Fill");
805 QVariant data = node.getChildData( "Glyphs.Fill" );
806 if (data.canConvert<QBrush>()) {
807 brush = data.value<QBrush>();
809 // no "Fill" attribute and no "Glyphs.Fill" child, so show nothing
810 // (see XPS specs, 5.10)
811 m_painter->restore();
815 brush = parseRscRefColorForBrush( att );
816 if ( brush.style() > Qt::NoBrush && brush.style() < Qt::LinearGradientPattern
817 && brush.color().alpha() == 0 ) {
818 m_painter->restore();
822 m_painter->setBrush( brush );
823 m_painter->setPen( QPen( brush, 0 ) );
826 att = node.attributes.value("Opacity");
827 if (! att.isEmpty()) {
829 double value = att.toDouble( &ok );
830 if ( ok && value >= 0.1 ) {
831 m_painter->setOpacity( value );
833 m_painter->restore();
839 att = node.attributes.value("RenderTransform");
840 if (!att.isEmpty()) {
841 m_painter->setWorldTransform( parseRscRefMatrix( att ), true);
845 att = node.attributes.value( "Clip" );
846 if ( !att.isEmpty() ) {
847 QPainterPath clipPath = parseRscRefPath( att );
848 if ( !clipPath.isEmpty() ) {
849 m_painter->setClipPath( clipPath );
853 // BiDiLevel - default Left-to-Right
854 m_painter->setLayoutDirection( Qt::LeftToRight );
855 att = node.attributes.value( "BiDiLevel" );
856 if ( !att.isEmpty() ) {
857 if ( (att.toInt() % 2) == 1 ) {
858 // odd BiDiLevel, so Right-to-Left
859 m_painter->setLayoutDirection( Qt::RightToLeft );
863 // Indices - partial handling only
864 att = node.attributes.value( "Indices" );
865 QList<qreal> advanceWidths;
866 if ( ! att.isEmpty() ) {
867 QStringList indicesElements = att.split( ';' );
868 for( int i = 0; i < indicesElements.size(); ++i ) {
869 if ( indicesElements.at(i).contains( "," ) ) {
870 QStringList parts = indicesElements.at(i).split( ',' );
871 if (parts.size() == 2 ) {
872 // regular advance case, no offsets
873 advanceWidths.append( parts.at(1).toDouble() * fontSize / 100.0 );
874 } else if (parts.size() == 3 ) {
875 // regular advance case, with uOffset
876 qreal AdvanceWidth = parts.at(1).toDouble() * fontSize / 100.0 ;
877 qreal uOffset = parts.at(2).toDouble() / 100.0;
878 advanceWidths.append( AdvanceWidth + uOffset );
880 // has vertical offset, but don't know how to handle that yet
881 kDebug(XpsDebug) << "Unhandled Indices element: " << indicesElements.at(i);
882 advanceWidths.append( -1.0 );
885 // no special advance case
886 advanceWidths.append( -1.0 );
892 QString stringToDraw( unicodeString( node.attributes.value( "UnicodeString" ) ) );
893 QPointF originAdvance(0, 0);
894 QFontMetrics metrics = m_painter->fontMetrics();
895 for ( int i = 0; i < stringToDraw.size(); ++i ) {
896 QChar thisChar = stringToDraw.at( i );
897 m_painter->drawText( origin + originAdvance, QString( thisChar ) );
898 const qreal advanceWidth = advanceWidths.value( i, qreal(-1.0) );
899 if ( advanceWidth > 0.0 ) {
900 originAdvance.rx() += advanceWidth;
902 originAdvance.rx() += metrics.width( thisChar );
905 // kDebug(XpsDebug) << "Glyphs: " << atts.value("Fill") << ", " << atts.value("FontUri");
906 // kDebug(XpsDebug) << " Origin: " << atts.value("OriginX") << "," << atts.value("OriginY");
907 // kDebug(XpsDebug) << " Unicode: " << atts.value("UnicodeString");
909 m_painter->restore();
912 void XpsHandler::processFill( XpsRenderNode &node )
914 //TODO Ignored child elements: VirtualBrush
916 if (node.children.size() != 1) {
917 kDebug(XpsDebug) << "Fill element should have exactly one child";
919 node.data = node.children[0].data;
923 void XpsHandler::processStroke( XpsRenderNode &node )
925 //TODO Ignored child elements: VirtualBrush
927 if (node.children.size() != 1) {
928 kDebug(XpsDebug) << "Stroke element should have exactly one child";
930 node.data = node.children[0].data;
934 void XpsHandler::processImageBrush( XpsRenderNode &node )
936 //TODO Ignored attributes: Opacity, x:key, TileMode, ViewBoxUnits, ViewPortUnits
937 //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]
942 QRectF viewport = stringToRectF( node.attributes.value( "Viewport" ) );
943 QRectF viewbox = stringToRectF( node.attributes.value( "Viewbox" ) );
944 QImage image = m_page->loadImageFromFile( node.attributes.value( "ImageSource" ) );
946 // Matrix which can transform [0, 0, 1, 1] rectangle to given viewbox
947 QTransform viewboxMatrix = QTransform( viewbox.width() * image.physicalDpiX() / 96, 0, 0, viewbox.height() * image.physicalDpiY() / 96, viewbox.x(), viewbox.y() );
949 // Matrix which can transform [0, 0, 1, 1] rectangle to given viewport
950 //TODO Take ViewPort into account
951 QTransform viewportMatrix;
952 att = node.attributes.value( "Transform" );
953 if ( att.isEmpty() ) {
954 QVariant data = node.getChildData( "ImageBrush.Transform" );
955 if (data.canConvert<QTransform>()) {
956 viewportMatrix = data.value<QTransform>();
958 viewportMatrix = QTransform();
961 viewportMatrix = parseRscRefMatrix( att );
963 viewportMatrix = viewportMatrix * QTransform( viewport.width(), 0, 0, viewport.height(), viewport.x(), viewport.y() );
966 brush = QBrush( image );
967 brush.setTransform( viewboxMatrix.inverted() * viewportMatrix );
969 node.data = qVariantFromValue( brush );
972 void XpsHandler::processPath( XpsRenderNode &node )
974 //TODO Ignored attributes: Clip, OpacityMask, StrokeEndLineCap, StorkeStartLineCap, Name, FixedPage.NavigateURI, xml:lang, x:key, AutomationProperties.Name, AutomationProperties.HelpText, SnapsToDevicePixels
975 //TODO Ignored child elements: RenderTransform, Clip, OpacityMask
976 // Handled separately: RenderTransform
983 XpsPathGeometry * pathdata = node.getChildData( "Path.Data" ).value< XpsPathGeometry * >();
984 att = node.attributes.value( "Data" );
985 if (! att.isEmpty() ) {
986 QPainterPath path = parseRscRefPath( att );
988 pathdata = new XpsPathGeometry();
989 pathdata->paths.append( new XpsPathFigure( path, true ) );
993 m_painter->restore();
998 att = node.attributes.value( "Fill" );
1000 if (! att.isEmpty() ) {
1001 brush = parseRscRefColorForBrush( att );
1003 data = node.getChildData( "Path.Fill" );
1004 if (data.canConvert<QBrush>()) {
1005 brush = data.value<QBrush>();
1008 m_painter->setBrush( brush );
1011 att = node.attributes.value( "Stroke" );
1012 QPen pen( Qt::transparent );
1013 if (! att.isEmpty() ) {
1014 pen = parseRscRefColorForPen( att );
1016 data = node.getChildData( "Path.Stroke" );
1017 if (data.canConvert<QBrush>()) {
1018 pen.setBrush( data.value<QBrush>() );
1021 att = node.attributes.value( "StrokeThickness" );
1022 if (! att.isEmpty() ) {
1024 int thickness = att.toInt( &ok );
1026 pen.setWidth( thickness );
1028 att = node.attributes.value( "StrokeDashArray" );
1029 if ( !att.isEmpty() ) {
1030 const QStringList pieces = att.split( QLatin1Char( ' ' ), QString::SkipEmptyParts );
1031 QVector<qreal> dashPattern( pieces.count() );
1033 for ( int i = 0; i < pieces.count(); ++i ) {
1034 qreal value = pieces.at( i ).toInt( &ok );
1036 dashPattern[i] = value;
1042 pen.setDashPattern( dashPattern );
1045 att = node.attributes.value( "StrokeDashOffset" );
1046 if ( !att.isEmpty() ) {
1048 int offset = att.toInt( &ok );
1050 pen.setDashOffset( offset );
1052 att = node.attributes.value( "StrokeDashCap" );
1053 if ( !att.isEmpty() ) {
1054 Qt::PenCapStyle cap = Qt::FlatCap;
1055 if ( att == QLatin1String( "Flat" ) ) {
1057 } else if ( att == QLatin1String( "Round" ) ) {
1059 } else if ( att == QLatin1String( "Square" ) ) {
1060 cap = Qt::SquareCap;
1062 // ### missing "Triangle"
1063 pen.setCapStyle( cap );
1065 att = node.attributes.value( "StrokeLineJoin" );
1066 if ( !att.isEmpty() ) {
1067 Qt::PenJoinStyle joinStyle = Qt::MiterJoin;
1068 if ( att == QLatin1String( "Miter" ) ) {
1069 joinStyle = Qt::MiterJoin;
1070 } else if ( att == QLatin1String( "Bevel" ) ) {
1071 joinStyle = Qt::BevelJoin;
1072 } else if ( att == QLatin1String( "Round" ) ) {
1073 joinStyle = Qt::RoundJoin;
1075 pen.setJoinStyle( joinStyle );
1077 att = node.attributes.value( "StrokeMiterLimit" );
1078 if ( !att.isEmpty() ) {
1080 double limit = att.toDouble( &ok );
1082 // we have to divide it by two, as XPS consider half of the stroke width,
1083 // while Qt the whole of it
1084 pen.setMiterLimit( limit / 2 );
1087 m_painter->setPen( pen );
1090 att = node.attributes.value("Opacity");
1091 if (! att.isEmpty()) {
1092 m_painter->setOpacity(att.toDouble());
1096 att = node.attributes.value( "RenderTransform" );
1097 if (! att.isEmpty() ) {
1098 m_painter->setWorldTransform( parseRscRefMatrix( att ), true );
1100 if ( !pathdata->transform.isIdentity() ) {
1101 m_painter->setWorldTransform( pathdata->transform, true );
1104 Q_FOREACH ( XpsPathFigure *figure, pathdata->paths ) {
1105 m_painter->setBrush( figure->isFilled ? brush : QBrush() );
1106 m_painter->drawPath( figure->path );
1111 m_painter->restore();
1114 void XpsHandler::processPathData( XpsRenderNode &node )
1116 if (node.children.size() != 1) {
1117 kDebug(XpsDebug) << "Path.Data element should have exactly one child";
1119 node.data = node.children[0].data;
1123 void XpsHandler::processPathGeometry( XpsRenderNode &node )
1125 XpsPathGeometry * geom = new XpsPathGeometry();
1127 Q_FOREACH ( const XpsRenderNode &child, node.children ) {
1128 if ( child.data.canConvert<XpsPathFigure *>() ) {
1129 XpsPathFigure *figure = child.data.value<XpsPathFigure *>();
1130 geom->paths.append( figure );
1136 att = node.attributes.value( "Figures" );
1137 if ( !att.isEmpty() ) {
1138 QPainterPath path = parseRscRefPath( att );
1139 qDeleteAll( geom->paths );
1140 geom->paths.clear();
1141 geom->paths.append( new XpsPathFigure( path, true ) );
1144 att = node.attributes.value( "FillRule" );
1145 if ( !att.isEmpty() ) {
1146 geom->fillRule = fillRuleFromString( att );
1150 att = node.attributes.value( "Transform" );
1151 if ( !att.isEmpty() ) {
1152 geom->transform = parseRscRefMatrix( att );
1155 if ( !geom->paths.isEmpty() ) {
1156 node.data = qVariantFromValue( geom );
1162 void XpsHandler::processPathFigure( XpsRenderNode &node )
1164 //TODO Ignored child elements: ArcSegment
1169 att = node.attributes.value( "StartPoint" );
1170 if ( !att.isEmpty() ) {
1171 QPointF point = getPointFromString( att );
1172 path.moveTo( point );
1177 Q_FOREACH ( const XpsRenderNode &child, node.children ) {
1178 bool isStroked = true;
1179 att = node.attributes.value( "IsStroked" );
1180 if ( !att.isEmpty() ) {
1181 isStroked = att == QLatin1String( "true" );
1188 if ( child.name == QLatin1String( "PolyLineSegment" ) ) {
1189 att = child.attributes.value( "Points" );
1190 if ( !att.isEmpty() ) {
1191 const QStringList points = att.split( QLatin1Char( ' ' ), QString::SkipEmptyParts );
1192 Q_FOREACH ( const QString &p, points ) {
1193 QPointF point = getPointFromString( p );
1194 path.lineTo( point );
1198 // PolyBezierSegment
1199 else if ( child.name == QLatin1String( "PolyBezierSegment" ) ) {
1200 att = child.attributes.value( "Points" );
1201 if ( !att.isEmpty() ) {
1202 const QStringList points = att.split( QLatin1Char( ' ' ), QString::SkipEmptyParts );
1203 if ( points.count() % 3 == 0 ) {
1204 for ( int i = 0; i < points.count(); ) {
1205 QPointF firstControl = getPointFromString( points.at( i++ ) );
1206 QPointF secondControl = getPointFromString( points.at( i++ ) );
1207 QPointF endPoint = getPointFromString( points.at( i++ ) );
1208 path.cubicTo(firstControl, secondControl, endPoint);
1213 // PolyQuadraticBezierSegment
1214 else if ( child.name == QLatin1String( "PolyQuadraticBezierSegment" ) ) {
1215 att = child.attributes.value( "Points" );
1216 if ( !att.isEmpty() ) {
1217 const QStringList points = att.split( QLatin1Char( ' ' ), QString::SkipEmptyParts );
1218 if ( points.count() % 2 == 0 ) {
1219 for ( int i = 0; i < points.count(); ) {
1220 QPointF point1 = getPointFromString( points.at( i++ ) );
1221 QPointF point2 = getPointFromString( points.at( i++ ) );
1222 path.quadTo( point1, point2 );
1229 bool closePath = false;
1230 att = node.attributes.value( "IsClosed" );
1231 if ( !att.isEmpty() ) {
1232 closePath = att == QLatin1String( "true" );
1235 path.closeSubpath();
1238 bool isFilled = true;
1239 att = node.attributes.value( "IsFilled" );
1240 if ( !att.isEmpty() ) {
1241 isFilled = att == QLatin1String( "true" );
1244 if ( !path.isEmpty() ) {
1245 node.data = qVariantFromValue( new XpsPathFigure( path, isFilled ) );
1249 void XpsHandler::processStartElement( XpsRenderNode &node )
1251 if (node.name == "Canvas") {
1253 QString att = node.attributes.value( "RenderTransform" );
1254 if ( !att.isEmpty() ) {
1255 m_painter->setWorldTransform( parseRscRefMatrix( att ), true );
1257 att = node.attributes.value( "Opacity" );
1258 if ( !att.isEmpty() ) {
1259 double value = att.toDouble();
1260 if ( value > 0.0 && value <= 1.0 ) {
1261 m_painter->setOpacity( m_painter->opacity() * value );
1263 // setting manually to 0 is necessary to "disable"
1264 // all the stuff inside
1265 m_painter->setOpacity( 0.0 );
1271 void XpsHandler::processEndElement( XpsRenderNode &node )
1273 if (node.name == "Glyphs") {
1274 processGlyph( node );
1275 } else if (node.name == "Path") {
1276 processPath( node );
1277 } else if (node.name == "MatrixTransform") {
1278 //TODO Ignoring x:key
1279 node.data = qVariantFromValue( QTransform( attsToMatrix( node.attributes.value( "Matrix" ) ) ) );
1280 } else if ((node.name == "Canvas.RenderTransform") || (node.name == "Glyphs.RenderTransform") || (node.name == "Path.RenderTransform")) {
1281 QVariant data = node.getRequiredChildData( "MatrixTransform" );
1282 if (data.canConvert<QTransform>()) {
1283 m_painter->setWorldTransform( data.value<QTransform>(), true );
1285 } else if (node.name == "Canvas") {
1286 m_painter->restore();
1287 } else if ((node.name == "Path.Fill") || (node.name == "Glyphs.Fill")) {
1288 processFill( node );
1289 } else if (node.name == "Path.Stroke") {
1290 processStroke( node );
1291 } else if (node.name == "SolidColorBrush") {
1292 //TODO Ignoring opacity, x:key
1293 node.data = qVariantFromValue( QBrush( QColor( hexToRgba( node.attributes.value( "Color" ).toLatin1() ) ) ) );
1294 } else if (node.name == "ImageBrush") {
1295 processImageBrush( node );
1296 } else if (node.name == "ImageBrush.Transform") {
1297 node.data = node.getRequiredChildData( "MatrixTransform" );
1298 } else if (node.name == "LinearGradientBrush") {
1299 XpsRenderNode * gradients = node.findChild( "LinearGradientBrush.GradientStops" );
1300 if ( gradients && gradients->data.canConvert< QGradient * >() ) {
1301 QPointF start = getPointFromString( node.attributes.value( "StartPoint" ) );
1302 QPointF end = getPointFromString( node.attributes.value( "EndPoint" ) );
1303 QLinearGradient * qgrad = static_cast< QLinearGradient * >( gradients->data.value< QGradient * >() );
1304 qgrad->setStart( start );
1305 qgrad->setFinalStop( end );
1306 applySpreadStyleToQGradient( node.attributes.value( "SpreadMethod" ), qgrad );
1307 node.data = qVariantFromValue( QBrush( *qgrad ) );
1310 } else if (node.name == "RadialGradientBrush") {
1311 XpsRenderNode * gradients = node.findChild( "RadialGradientBrush.GradientStops" );
1312 if ( gradients && gradients->data.canConvert< QGradient * >() ) {
1313 QPointF center = getPointFromString( node.attributes.value( "Center" ) );
1314 QPointF origin = getPointFromString( node.attributes.value( "GradientOrigin" ) );
1315 double radiusX = node.attributes.value( "RadiusX" ).toDouble();
1316 double radiusY = node.attributes.value( "RadiusY" ).toDouble();
1317 QRadialGradient * qgrad = static_cast< QRadialGradient * >( gradients->data.value< QGradient * >() );
1318 qgrad->setCenter( center );
1319 qgrad->setFocalPoint( origin );
1320 // TODO what in case of different radii?
1321 qgrad->setRadius( qMin( radiusX, radiusY ) );
1322 applySpreadStyleToQGradient( node.attributes.value( "SpreadMethod" ), qgrad );
1323 node.data = qVariantFromValue( QBrush( *qgrad ) );
1326 } else if (node.name == "LinearGradientBrush.GradientStops") {
1327 QList<XpsGradient> gradients;
1328 Q_FOREACH ( const XpsRenderNode &child, node.children ) {
1329 double offset = child.attributes.value( "Offset" ).toDouble();
1330 QColor color = hexToRgba( child.attributes.value( "Color" ).toLatin1() );
1331 gradients.append( XpsGradient( offset, color ) );
1333 preprocessXpsGradients( gradients );
1334 if ( !gradients.isEmpty() ) {
1335 QLinearGradient * qgrad = new QLinearGradient();
1336 addXpsGradientsToQGradient( gradients, qgrad );
1337 node.data = qVariantFromValue< QGradient * >( qgrad );
1339 } else if (node.name == "RadialGradientBrush.GradientStops") {
1340 QList<XpsGradient> gradients;
1341 Q_FOREACH ( const XpsRenderNode &child, node.children ) {
1342 double offset = child.attributes.value( "Offset" ).toDouble();
1343 QColor color = hexToRgba( child.attributes.value( "Color" ).toLatin1() );
1344 gradients.append( XpsGradient( offset, color ) );
1346 preprocessXpsGradients( gradients );
1347 if ( !gradients.isEmpty() ) {
1348 QRadialGradient * qgrad = new QRadialGradient();
1349 addXpsGradientsToQGradient( gradients, qgrad );
1350 node.data = qVariantFromValue< QGradient * >( qgrad );
1352 } else if (node.name == "PathFigure") {
1353 processPathFigure( node );
1354 } else if (node.name == "PathGeometry") {
1355 processPathGeometry( node );
1356 } else if (node.name == "Path.Data") {
1357 processPathData( node );
1359 //kDebug(XpsDebug) << "Unknown element: " << node->name;
1363 XpsPage::XpsPage(XpsFile *file, const QString &fileName): m_file( file ),
1364 m_fileName( fileName ), m_pageIsRendered(false)
1368 // kDebug(XpsDebug) << "page file name: " << fileName;
1370 const KArchiveEntry pageFile = m_file->xpsArchive()->entry( fileName );
1372 QXmlStreamReader xml;
1373 xml.addData( readFileOrDirectoryParts( m_file->xpsArchive(), &pageFile ) );
1374 while ( !xml.atEnd() )
1377 if ( xml.isStartElement() && ( xml.name() == "FixedPage" ) )
1379 QXmlStreamAttributes attributes = xml.attributes();
1380 m_pageSize.setWidth( attributes.value( "Width" ).toString().toDouble() );
1381 m_pageSize.setHeight( attributes.value( "Height" ).toString().toDouble() );
1387 kDebug(XpsDebug) << "Could not parse XPS page:" << xml.errorString();
1396 bool XpsPage::renderToImage( QImage *p )
1399 if ((m_pageImage == NULL) || (m_pageImage->size() != p->size())) {
1401 m_pageImage = new QImage( p->size(), QImage::Format_ARGB32 );
1402 // Set one point = one drawing unit. Useful for fonts, because xps specifies font size using drawing units, not points as usual
1403 m_pageImage->setDotsPerMeterX( 2835 );
1404 m_pageImage->setDotsPerMeterY( 2835 );
1406 m_pageIsRendered = false;
1408 if (! m_pageIsRendered) {
1409 m_pageImage->fill( qRgba( 255, 255, 255, 255 ) );
1410 QPainter painter( m_pageImage );
1411 renderToPainter( &painter );
1412 m_pageIsRendered = true;
1420 bool XpsPage::renderToPainter( QPainter *painter )
1422 XpsHandler handler( this );
1423 handler.m_painter = painter;
1424 handler.m_painter->setWorldTransform(QTransform().scale((qreal)painter->device()->width() / size().width(), (qreal)painter->device()->height() / size().height()));
1425 QXmlSimpleReader parser;
1426 parser.setContentHandler( &handler );
1427 parser.setErrorHandler( &handler );
1428 const KArchiveEntry pageFile = m_file->xpsArchive()->entry( m_fileName );
1429 QByteArray data = readFileOrDirectoryParts( m_file->xpsArchive(), &pageFile );
1430 QBuffer buffer( &data );
1431 QXmlInputSource source( &buffer );
1432 bool ok = parser.parse( source );
1433 kDebug(XpsDebug) << "Parse result: " << ok;
1438 QSizeF XpsPage::size() const
1443 QFont XpsFile::getFontByName( const QString &fileName, float size )
1445 // kDebug(XpsDebug) << "trying to get font: " << fileName << ", size: " << size;
1447 int index = m_fontCache.value(fileName, -1);
1450 index = loadFontByName(fileName);
1451 m_fontCache[fileName] = index;
1453 if ( index == -1 ) {
1454 kWarning(XpsDebug) << "Requesting uknown font:" << fileName;
1457 return QFont(m_fonts.at(index));
1460 int XpsFile::loadFontByName( const QString &fileName )
1462 // kDebug(XpsDebug) << "font file name: " << fileName;
1464 const KArchiveEntry fontFile = loadEntry( m_xpsArchive, entryFilePath( fileName ), Qt::CaseInsensitive );
1465 if ( fontFile.isNull() ) {
1469 QByteArray fontData = readFileOrDirectoryParts( m_xpsArchive, &fontFile ); // once per file, according to the docs
1472 KTemporaryFile tempfile;
1473 tempfile.setSuffix(QFileInfo(fileName).suffix());
1474 if (!tempfile.open()) {
1478 if (tempfile.write(fontData) != fontData.size()) {
1482 tempfile.setAutoRemove(false);
1483 m_fonts.append(tempfile.fileName());
1484 result = (m_fonts.size() - 1);
1486 // kDebug(XpsDebug) << "Saved font: " << tempfile.fileName();
1488 return result; // a font ID
1491 KArchive * XpsFile::xpsArchive() {
1492 return m_xpsArchive;
1495 QImage XpsPage::loadImageFromFile( const QString &fileName )
1497 // kDebug(XpsDebug) << "image file name: " << fileName;
1499 if ( fileName.at( 0 ) == QLatin1Char( '{' ) ) {
1500 // for example: '{ColorConvertedBitmap /Resources/bla.wdp /Resources/foobar.icc}'
1501 // TODO: properly read a ColorConvertedBitmap
1505 QString absoluteFileName = absolutePath( entryPath( m_fileName ), fileName );
1506 const KArchiveEntry imageFile = loadEntry( m_file->xpsArchive(), absoluteFileName, Qt::CaseInsensitive );
1507 if ( imageFile.isNull() ) {
1513 XPS standard requires to use 96dpi for images which doesn't have dpi specified (in file). When Qt loads such an image,
1514 it sets its dpi to qt_defaultDpi and doesn't allow to find out that it happend.
1516 To workaround this I used this procedure: load image, set its dpi to 96, load image again. When dpi isn't set in file,
1517 dpi set by me stays unchanged.
1519 Trolltech task ID: 159527.
1524 QByteArray data = m_file->xpsArchive()->data( imageFile.pathname );
1526 QBuffer buffer(&data);
1527 buffer.open(QBuffer::ReadOnly);
1529 QImageReader reader(&buffer);
1530 image = reader.read();
1532 image.setDotsPerMeterX(qRound(96 / 0.0254));
1533 image.setDotsPerMeterY(qRound(96 / 0.0254));
1536 reader.setDevice(&buffer);
1537 reader.read(&image);
1542 Okular::TextPage* XpsPage::textPage()
1544 // kDebug(XpsDebug) << "Parsing XpsPage, text extraction";
1546 Okular::TextPage* textPage = new Okular::TextPage();
1548 const KArchiveEntry pageFile = m_file->xpsArchive()->entry( m_fileName );
1549 QXmlStreamReader xml;
1550 xml.addData( readFileOrDirectoryParts( m_file->xpsArchive(), &pageFile ) );
1552 QTransform matrix = QTransform();
1553 QStack<QTransform> matrices;
1554 matrices.push( QTransform() );
1555 bool useMatrix = false;
1556 QXmlStreamAttributes glyphsAtts;
1558 while ( ! xml.atEnd() ) {
1560 if ( xml.isStartElement() ) {
1561 if ( xml.name() == "Canvas") {
1562 matrices.push(matrix);
1564 QString att = xml.attributes().value( "RenderTransform" ).toString();
1565 if (!att.isEmpty()) {
1566 matrix = parseRscRefMatrix( att ) * matrix;
1568 } else if ((xml.name() == "Canvas.RenderTransform") || (xml.name() == "Glyphs.RenderTransform")) {
1570 } else if (xml.name() == "MatrixTransform") {
1572 matrix = attsToMatrix( xml.attributes().value("Matrix").toString() ) * matrix;
1574 } else if (xml.name() == "Glyphs") {
1575 matrices.push( matrix );
1576 glyphsAtts = xml.attributes();
1577 } else if ( (xml.name() == "Path") || (xml.name() == "Path.Fill") || (xml.name() == "SolidColorBrush")
1578 || (xml.name() == "ImageBrush") || (xml.name() == "ImageBrush.Transform")
1579 || (xml.name() == "Path.OpacityMask") || (xml.name() == "Path.Data")
1580 || (xml.name() == "PathGeometry") || (xml.name() == "PathFigure")
1581 || (xml.name() == "PolyLineSegment") ) {
1582 // those are only graphical - no use in text handling
1583 } else if ( (xml.name() == "FixedPage") || (xml.name() == "FixedPage.Resources") ) {
1584 // not useful for text extraction
1586 kDebug(XpsDebug) << "Unhandled element in Text Extraction start: " << xml.name().toString();
1588 } else if (xml.isEndElement() ) {
1589 if (xml.name() == "Canvas") {
1590 matrix = matrices.pop();
1591 } else if ((xml.name() == "Canvas.RenderTransform") || (xml.name() == "Glyphs.RenderTransform")) {
1593 } else if (xml.name() == "MatrixTransform") {
1594 // not clear if we need to do anything here yet.
1595 } else if (xml.name() == "Glyphs") {
1596 QString att = glyphsAtts.value( "RenderTransform" ).toString();
1597 if (!att.isEmpty()) {
1598 matrix = parseRscRefMatrix( att ) * matrix;
1600 QString text = unicodeString( glyphsAtts.value( "UnicodeString" ).toString() );
1602 // Get font (doesn't work well because qt doesn't allow to load font from file)
1603 QFont font = m_file->getFontByName( glyphsAtts.value( "FontUri" ).toString(),
1604 glyphsAtts.value("FontRenderingEmSize").toString().toFloat() * 72 / 96 );
1605 QFontMetrics metrics = QFontMetrics( font );
1607 QPointF origin( glyphsAtts.value("OriginX").toString().toDouble(),
1608 glyphsAtts.value("OriginY").toString().toDouble() );
1612 for (int i = 0; i < text.length(); i++) {
1613 int width = metrics.width( text.mid(0, i + 1));
1615 Okular::NormalizedRect * rect = new Okular::NormalizedRect( (origin.x() + lastWidth) / m_pageSize.width(),
1616 (origin.y() - metrics.height()) / m_pageSize.height(),
1617 (origin.x() + width) / m_pageSize.width(),
1618 origin.y() / m_pageSize.height() );
1619 rect->transform( matrix );
1620 textPage->append( text.mid(i, 1), rect );
1625 matrix = matrices.pop();
1626 } else if ( (xml.name() == "Path") || (xml.name() == "Path.Fill") || (xml.name() == "SolidColorBrush")
1627 || (xml.name() == "ImageBrush") || (xml.name() == "ImageBrush.Transform")
1628 || (xml.name() == "Path.OpacityMask") || (xml.name() == "Path.Data")
1629 || (xml.name() == "PathGeometry") || (xml.name() == "PathFigure")
1630 || (xml.name() == "PolyLineSegment") ) {
1631 // those are only graphical - no use in text handling
1632 } else if ( (xml.name() == "FixedPage") || (xml.name() == "FixedPage.Resources") ) {
1633 // not useful for text extraction
1635 kDebug(XpsDebug) << "Unhandled element in Text Extraction end: " << xml.name().toString();
1639 if ( xml.error() ) {
1640 kDebug(XpsDebug) << "Error parsing XpsPage text: " << xml.errorString();
1645 void XpsDocument::parseDocumentStructure( const QString &documentStructureFileName )
1647 kDebug(XpsDebug) << "document structure file name: " << documentStructureFileName;
1648 m_haveDocumentStructure = false;
1650 QXmlStreamReader xml;
1651 xml.addData( m_file->xpsArchive()->data( documentStructureFileName ) );
1653 while ( !xml.atEnd() )
1657 if ( xml.isStartElement() ) {
1658 if ( xml.name() == "DocumentStructure" ) {
1659 // just a container for optional outline and story elements - nothing to do here
1660 } else if ( xml.name() == "DocumentStructure.Outline" ) {
1661 kDebug(XpsDebug) << "found DocumentStructure.Outline";
1662 } else if ( xml.name() == "DocumentOutline" ) {
1663 kDebug(XpsDebug) << "found DocumentOutline";
1664 m_docStructure = new Okular::DocumentSynopsis;
1665 } else if ( xml.name() == "OutlineEntry" ) {
1666 m_haveDocumentStructure = true;
1667 QXmlStreamAttributes attributes = xml.attributes();
1668 int outlineLevel = attributes.value( "OutlineLevel").toString().toInt();
1669 QString description = attributes.value("Description").toString();
1670 QDomElement synopsisElement = m_docStructure->createElement( description );
1671 synopsisElement.setAttribute( "OutlineLevel", outlineLevel );
1672 QString target = attributes.value("OutlineTarget").toString();
1673 int hashPosition = target.lastIndexOf( '#' );
1674 target = target.mid( hashPosition + 1 );
1675 // kDebug(XpsDebug) << "target: " << target;
1676 Okular::DocumentViewport viewport;
1677 viewport.pageNumber = m_docStructurePageMap.value( target );
1678 synopsisElement.setAttribute( "Viewport", viewport.toString() );
1679 if ( outlineLevel == 1 ) {
1680 // kDebug(XpsDebug) << "Description: "
1681 // << outlineEntryElement.attribute( "Description" );
1682 m_docStructure->appendChild( synopsisElement );
1684 // find the last next highest element (so it this is level 3, we need
1685 // to find the most recent level 2 node)
1686 QDomNode maybeParentNode = m_docStructure->lastChild();
1687 while ( !maybeParentNode.isNull() )
1689 if ( maybeParentNode.toElement().attribute( "OutlineLevel" ).toInt() == ( outlineLevel - 1 ) ) {
1690 // we have the right parent
1691 maybeParentNode.appendChild( synopsisElement );
1694 maybeParentNode = maybeParentNode.lastChild();
1697 } else if ( xml.name() == "Story" ) {
1698 // we need to handle Story here, but I have no idea what to do with it.
1699 } else if ( xml.name() == "StoryFragment" ) {
1700 // we need to handle StoryFragment here, but I have no idea what to do with it.
1701 } else if ( xml.name() == "StoryFragmentReference" ) {
1702 // we need to handle StoryFragmentReference here, but I have no idea what to do with it.
1704 kDebug(XpsDebug) << "Unhandled entry in DocumentStructure: " << xml.name().toString();
1710 const Okular::DocumentSynopsis * XpsDocument::documentStructure()
1712 return m_docStructure;
1715 bool XpsDocument::hasDocumentStructure()
1717 return m_haveDocumentStructure;
1720 XpsDocument::XpsDocument(XpsFile *file, const QString &fileName): m_file(file), m_haveDocumentStructure( false ), m_docStructure( 0 )
1722 kDebug(XpsDebug) << "document file name: " << fileName;
1724 const KArchiveEntry documentEntry = file->xpsArchive()->entry( fileName );
1725 QString documentFilePath = fileName;
1726 const QString documentEntryPath = entryPath( fileName );
1728 QXmlStreamReader docXml;
1729 docXml.addData( readFileOrDirectoryParts( file->xpsArchive(), &documentEntry, &documentFilePath ) );
1730 while( !docXml.atEnd() ) {
1732 if ( docXml.isStartElement() ) {
1733 if ( docXml.name() == "PageContent" ) {
1734 QString pagePath = docXml.attributes().value("Source").toString();
1735 kDebug(XpsDebug) << "Page Path: " << pagePath;
1736 XpsPage *page = new XpsPage( file, absolutePath( documentFilePath, pagePath ) );
1737 m_pages.append(page);
1738 } else if ( docXml.name() == "PageContent.LinkTargets" ) {
1739 // do nothing - wait for the real LinkTarget elements
1740 } else if ( docXml.name() == "LinkTarget" ) {
1741 QString targetName = docXml.attributes().value( "Name" ).toString();
1742 if ( ! targetName.isEmpty() ) {
1743 m_docStructurePageMap[ targetName ] = m_pages.count() - 1;
1745 } else if ( docXml.name() == "FixedDocument" ) {
1746 // we just ignore this - it is just a container
1748 kDebug(XpsDebug) << "Unhandled entry in FixedDocument: " << docXml.name().toString();
1752 if ( docXml.error() ) {
1753 kDebug(XpsDebug) << "Could not parse main XPS document file: " << docXml.errorString();
1756 // There might be a relationships entry for this document - typically used to tell us where to find the
1757 // content structure description
1759 // We should be able to find this using a reference from some other part of the document, but I can't see it.
1760 const int slashPosition = fileName.lastIndexOf( '/' );
1761 const QString documentRelationshipFile = absolutePath( documentEntryPath, "_rels/" + fileName.mid( slashPosition + 1 ) + ".rels" );
1763 const KArchiveEntry relFile = file->xpsArchive()->entry(documentRelationshipFile);
1765 QString documentStructureFile;
1766 if ( !relFile.isNull() ) {
1767 QXmlStreamReader xml;
1768 xml.addData( readFileOrDirectoryParts( file->xpsArchive(), &relFile ) );
1769 while ( !xml.atEnd() )
1772 if ( xml.isStartElement() && ( xml.name() == "Relationship" ) ) {
1773 QXmlStreamAttributes attributes = xml.attributes();
1774 if ( attributes.value( "Type" ).toString() == "http://schemas.microsoft.com/xps/2005/06/documentstructure" ) {
1775 documentStructureFile = attributes.value( "Target" ).toString();
1777 kDebug(XpsDebug) << "Unknown document relationships element: "
1778 << attributes.value( "Type" ).toString() << " : "
1779 << attributes.value( "Target" ).toString();
1783 if ( xml.error() ) {
1784 kDebug(XpsDebug) << "Could not parse XPS page relationships file ( "
1785 << documentRelationshipFile
1786 << " ) - " << xml.errorString();
1788 } else { // the page relationship file didn't exist in the zipfile
1790 kDebug(XpsDebug) << "Could not open Document relationship file from " << documentRelationshipFile;
1793 if ( ! documentStructureFile.isEmpty() )
1795 // kDebug(XpsDebug) << "Document structure filename: " << documentStructureFile;
1796 // make the document path absolute
1797 documentStructureFile = absolutePath( documentEntryPath, documentStructureFile );
1798 // kDebug(XpsDebug) << "Document structure absolute path: " << documentStructureFile;
1799 parseDocumentStructure( documentStructureFile );
1804 XpsDocument::~XpsDocument()
1806 for (int i = 0; i < m_pages.size(); i++) {
1807 delete m_pages.at( i );
1811 if ( m_docStructure )
1812 delete m_docStructure;
1815 int XpsDocument::numPages() const
1817 return m_pages.size();
1820 XpsPage* XpsDocument::page(int pageNum) const
1822 return m_pages.at(pageNum);
1825 XpsFile::XpsFile() : m_docInfo( 0 )
1832 m_fontCache.clear();
1833 foreach (const QString &fontfile, m_fonts) {
1834 QFile::remove(fontfile);
1839 bool XpsFile::loadDocument(const QString &filename)
1841 m_xpsArchive = new KArchive( filename );
1842 if ( m_xpsArchive->isReadable() == true ) {
1843 kDebug(XpsDebug) << "Successful open of " << filename;
1845 kDebug(XpsDebug) << "Could not open XPS archive: " << filename;
1846 delete m_xpsArchive;
1850 // The only fixed entry in XPS is /_rels/.rels
1851 const KArchiveEntry relEntry = m_xpsArchive->entry("_rels/.rels");
1852 if ( relEntry.isNull() ) {
1853 // this might occur if we can't read the zip directory, or it doesn't have the relationships entry
1854 kDebug(XpsDebug) << "No relationships found in archive: " << filename;
1858 QXmlStreamReader relXml;
1859 relXml.addData( readFileOrDirectoryParts( m_xpsArchive, &relEntry ) );
1861 QString fixedRepresentationFileName;
1862 // We work through the relationships document and pull out each element.
1863 while ( !relXml.atEnd() )
1866 if ( relXml.isStartElement() ) {
1867 if ( relXml.name() == "Relationship" ) {
1868 QXmlStreamAttributes attributes = relXml.attributes();
1869 QString type = attributes.value( "Type" ).toString();
1870 QString target = attributes.value( "Target" ).toString();
1871 if ( "http://schemas.openxmlformats.org/package/2006/relationships/metadata/thumbnail" == type ) {
1872 m_thumbnailFileName = target;
1873 } else if ( "http://schemas.microsoft.com/xps/2005/06/fixedrepresentation" == type ) {
1874 fixedRepresentationFileName = target;
1875 } else if ("http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties" == type) {
1876 m_corePropertiesFileName = target;
1877 } else if ("http://schemas.openxmlformats.org/package/2006/relationships/digital-signature/origin" == type) {
1878 m_signatureOrigin = target;
1880 kDebug(XpsDebug) << "Unknown relationships element: " << type << " : " << target;
1882 } else if ( relXml.name() == "Relationships" ) {
1883 // nothing to do here - this is just the container level
1885 kDebug(XpsDebug) << "unexpected element in _rels/.rels: " << relXml.name().toString();
1890 if ( relXml.error() ) {
1891 kDebug(XpsDebug) << "Could not parse _rels/.rels: " << relXml.errorString();
1895 if ( fixedRepresentationFileName.isEmpty() ) {
1896 // FixedRepresentation is a required part of the XPS document
1897 kDebug(XpsDebug) << "fixedrepresentation not found in document";
1901 const KArchiveEntry fixedRepEntry = m_xpsArchive->entry( fixedRepresentationFileName );
1902 QString fixedRepresentationFilePath = fixedRepresentationFileName;
1904 QXmlStreamReader fixedRepXml;
1905 fixedRepXml.addData( readFileOrDirectoryParts( m_xpsArchive, &fixedRepEntry, &fixedRepresentationFileName ) );
1907 while ( !fixedRepXml.atEnd() )
1909 fixedRepXml.readNext();
1910 if ( fixedRepXml.isStartElement() ) {
1911 if ( fixedRepXml.name() == "DocumentReference" ) {
1912 const QString source = fixedRepXml.attributes().value("Source").toString();
1913 XpsDocument *doc = new XpsDocument( this, absolutePath( fixedRepresentationFilePath, source ) );
1914 for (int lv = 0; lv < doc->numPages(); ++lv) {
1915 // our own copy of the pages list
1916 m_pages.append( doc->page( lv ) );
1918 m_documents.append(doc);
1919 } else if ( fixedRepXml.name() == "FixedDocumentSequence") {
1920 // we don't do anything here - this is just a container for one or more DocumentReference elements
1922 kDebug(XpsDebug) << "Unhandled entry in FixedDocumentSequence: " << fixedRepXml.name().toString();
1926 if ( fixedRepXml.error() ) {
1927 kDebug(XpsDebug) << "Could not parse FixedRepresentation file:" << fixedRepXml.errorString();
1934 const Okular::DocumentInfo * XpsFile::generateDocumentInfo()
1939 m_docInfo = new Okular::DocumentInfo();
1941 m_docInfo->set( Okular::DocumentInfo::MimeType, "application/oxps" );
1943 if ( ! m_corePropertiesFileName.isEmpty() ) {
1944 QXmlStreamReader xml;
1945 xml.addData( m_xpsArchive->data(m_corePropertiesFileName) );
1946 while ( !xml.atEnd() )
1949 if ( xml.isEndElement() )
1951 if ( xml.isStartElement() )
1953 if (xml.name() == "title") {
1954 m_docInfo->set( Okular::DocumentInfo::Title, xml.readElementText() );
1955 } else if (xml.name() == "subject") {
1956 m_docInfo->set( Okular::DocumentInfo::Subject, xml.readElementText() );
1957 } else if (xml.name() == "description") {
1958 m_docInfo->set( Okular::DocumentInfo::Description, xml.readElementText() );
1959 } else if (xml.name() == "creator") {
1960 m_docInfo->set( Okular::DocumentInfo::Creator, xml.readElementText() );
1961 } else if (xml.name() == "category") {
1962 m_docInfo->set( Okular::DocumentInfo::Category, xml.readElementText() );
1963 } else if (xml.name() == "created") {
1964 QDateTime createdDate = QDateTime::fromString( xml.readElementText(), "yyyy-MM-ddThh:mm:ssZ" );
1965 m_docInfo->set( Okular::DocumentInfo::CreationDate, KGlobal::locale()->formatDateTime( createdDate, KLocale::LongDate, true ) );
1966 } else if (xml.name() == "modified") {
1967 QDateTime modifiedDate = QDateTime::fromString( xml.readElementText(), "yyyy-MM-ddThh:mm:ssZ" );
1968 m_docInfo->set( Okular::DocumentInfo::ModificationDate, KGlobal::locale()->formatDateTime( modifiedDate, KLocale::LongDate, true ) );
1969 } else if (xml.name() == "keywords") {
1970 m_docInfo->set( Okular::DocumentInfo::Keywords, xml.readElementText() );
1971 } else if (xml.name() == "revision") {
1972 m_docInfo->set( "revision", xml.readElementText(), i18n( "Revision" ) );
1978 kDebug(XpsDebug) << "Could not parse XPS core properties:" << xml.errorString();
1981 kDebug(XpsDebug) << "No core properties filename";
1984 m_docInfo->set( Okular::DocumentInfo::Pages, QString::number(numPages()) );
1989 bool XpsFile::closeDocument()
1997 qDeleteAll( m_documents );
1998 m_documents.clear();
2000 delete m_xpsArchive;
2005 int XpsFile::numPages() const
2007 return m_pages.size();
2010 int XpsFile::numDocuments() const
2012 return m_documents.size();
2015 XpsDocument* XpsFile::document(int documentNum) const
2017 return m_documents.at( documentNum );
2020 XpsPage* XpsFile::page(int pageNum) const
2022 return m_pages.at( pageNum );
2025 XpsGenerator::XpsGenerator( QObject *parent, const QVariantList &args )
2026 : Okular::Generator( parent, args ), m_xpsFile( 0 )
2028 setFeature( TextExtraction );
2029 setFeature( PrintNative );
2032 XpsGenerator::~XpsGenerator()
2036 bool XpsGenerator::loadDocument( const QString & fileName, QVector<Okular::Page*> & pagesVector )
2038 m_xpsFile = new XpsFile();
2040 m_xpsFile->loadDocument( fileName );
2041 pagesVector.resize( m_xpsFile->numPages() );
2043 int pagesVectorOffset = 0;
2045 for (int docNum = 0; docNum < m_xpsFile->numDocuments(); ++docNum )
2047 XpsDocument *doc = m_xpsFile->document( docNum );
2048 for (int pageNum = 0; pageNum < doc->numPages(); ++pageNum )
2050 QSizeF pageSize = doc->page( pageNum )->size();
2051 pagesVector[pagesVectorOffset] = new Okular::Page( pagesVectorOffset, pageSize.width(), pageSize.height(), Okular::Rotation0 );
2052 ++pagesVectorOffset;
2059 bool XpsGenerator::doCloseDocument()
2061 m_xpsFile->closeDocument();
2068 QImage XpsGenerator::image( Okular::PixmapRequest * request )
2070 QMutexLocker lock( userMutex() );
2071 QSize size( (int)request->width(), (int)request->height() );
2072 QImage image( size, QImage::Format_RGB32 );
2073 XpsPage *pageToRender = m_xpsFile->page( request->page()->number() );
2074 pageToRender->renderToImage( &image );
2078 Okular::TextPage* XpsGenerator::textPage( Okular::Page * page )
2080 QMutexLocker lock( userMutex() );
2081 XpsPage * xpsPage = m_xpsFile->page( page->number() );
2082 return xpsPage->textPage();
2085 const Okular::DocumentInfo * XpsGenerator::generateDocumentInfo()
2087 kDebug(XpsDebug) << "generating document metadata";
2089 return m_xpsFile->generateDocumentInfo();
2092 const Okular::DocumentSynopsis * XpsGenerator::generateDocumentSynopsis()
2094 kDebug(XpsDebug) << "generating document synopsis";
2096 // we only generate the synopsis for the first file.
2097 if ( !m_xpsFile || !m_xpsFile->document( 0 ) )
2100 if ( m_xpsFile->document( 0 )->hasDocumentStructure() )
2101 return m_xpsFile->document( 0 )->documentStructure();
2107 Okular::ExportFormat::List XpsGenerator::exportFormats() const
2109 static Okular::ExportFormat::List formats;
2110 if ( formats.isEmpty() ) {
2111 formats.append( Okular::ExportFormat::standardFormat( Okular::ExportFormat::PlainText ) );
2116 bool XpsGenerator::exportTo( const QString &fileName, const Okular::ExportFormat &format )
2118 if ( format.mimeType()->name() == QLatin1String( "text/plain" ) ) {
2119 QFile f( fileName );
2120 if ( !f.open( QIODevice::WriteOnly ) )
2123 QTextStream ts( &f );
2124 for ( int i = 0; i < m_xpsFile->numPages(); ++i )
2126 Okular::TextPage* textPage = m_xpsFile->page(i)->textPage();
2127 QString text = textPage->text();
2140 bool XpsGenerator::print( QPrinter &printer )
2142 QList<int> pageList = Okular::Utils::pageList( printer, document()->pages(),
2143 document()->currentPage() + 1,
2144 document()->bookmarkedPageList() );
2146 QPainter painter( &printer );
2148 for ( int i = 0; i < pageList.count(); ++i )
2153 const int page = pageList.at( i ) - 1;
2154 XpsPage *pageToRender = m_xpsFile->page( page );
2155 QImage image( pageToRender->size().toSize(), QImage::Format_RGB32 );
2156 pageToRender->renderToImage( &image );
2157 painter.drawImage(0, 0, image);
2163 XpsRenderNode * XpsRenderNode::findChild( const QString &name )
2165 for (int i = 0; i < children.size(); i++) {
2166 if (children[i].name == name) {
2167 return &children[i];
2174 QVariant XpsRenderNode::getRequiredChildData( const QString &name )
2176 XpsRenderNode * child = findChild( name );
2177 if (child == NULL) {
2178 kDebug(XpsDebug) << "Required element " << name << " is missing in " << this->name;
2185 QVariant XpsRenderNode::getChildData( const QString &name )
2187 XpsRenderNode * child = findChild( name );
2188 if (child == NULL) {
2195 #include "moc_generator_xps.cpp"