diff --git a/core/area.h b/core/area.h index 6b4cc5531..4299b003c 100644 --- a/core/area.h +++ b/core/area.h @@ -1,891 +1,903 @@ /*************************************************************************** * Copyright (C) 2004-05 by Enrico Ros * * Copyright (C) 2005 by Piotr Szymanski * * 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. * ***************************************************************************/ #ifndef _OKULAR_AREA_H_ #define _OKULAR_AREA_H_ #include #include #include #include #include #include #include "global.h" #include "okularcore_export.h" class QPolygonF; class QRect; namespace Okular { class Annotation; class Action; class NormalizedShape; /** * NormalizedPoint is a helper class which stores the coordinates * of a normalized point. Normalized means that the coordinates are * between 0 and 1 so that it is page size independent. * * Example: * The normalized point is (0.5, 0.3) * * If you want to draw it on a 800x600 page, just multiply the x coordinate (0.5) with * the page width (800) and the y coordinate (0.3) with the page height (600), so * the point will be drawn on the page at (400, 180). * * That allows you to zoom the page by just multiplying the normalized points with the * zoomed page size. */ class OKULARCORE_EXPORT NormalizedPoint { public: /** * Creates a new empty normalized point. */ NormalizedPoint(); /** * Creates a new normalized point with the normalized coordinates (@p x, @p y ). */ NormalizedPoint( double x, double y ); /** * Creates a new normalized point with the coordinates (@p x, @p y) which are normalized * by the scaling factors @p xScale and @p yScale. */ NormalizedPoint( int x, int y, int xScale, int yScale ); /** * @internal */ NormalizedPoint& operator=( const NormalizedPoint& ); /** * Transforms the normalized point with the operations defined by @p matrix. */ void transform( const QTransform &matrix ); /** * Returns squared distance to point @p x @p y @p xScale @p yScale * @since 0.17 (KDE 4.11) */ double distanceSqr( double x, double y, double xScale, double yScale ) const; /** * @brief Calculates distance of the point @p x @p y @p xScale @p yScale to the line segment from @p start to @p end * @since 0.17 (KDE 4.11) */ static double distanceSqr( double x, double y, double xScale, double yScale, const NormalizedPoint& start, const NormalizedPoint& end ); /** * The normalized x coordinate. */ double x; /** * The normalized y coordinate. */ double y; }; /** * NormalizedRect is a helper class which stores the coordinates * of a normalized rect, which is a rectangle of @see NormalizedPoints. */ class OKULARCORE_EXPORT NormalizedRect { public: /** * Creates a null normalized rectangle. * @see isNull() */ NormalizedRect(); /** * Creates a normalized rectangle with the normalized coordinates * @p left, @p top, @p right, @p bottom. * * If you need the x, y, width and height coordinates use the * following formulas: * * @li x = left * @li y = top * @li width = right - left * @li height = bottom - top */ NormalizedRect( double left, double top, double right, double bottom ); /** * Creates a normalized rectangle of the given @p rectangle which is normalized * by the scaling factors @p xScale and @p yScale. */ NormalizedRect( const QRect &rectangle, double xScale, double yScale ); /** * @internal */ NormalizedRect( const NormalizedRect& ); /** * @internal */ NormalizedRect& operator=( const NormalizedRect &other ); /** * Build a normalized rect from a QRectF. */ static NormalizedRect fromQRectF( const QRectF &rect ); /** * Returns whether this normalized rectangle is a null normalized rect. */ bool isNull() const; /** * Returns whether the normalized rectangle contains the normalized coordinates * @p x and @p y. */ bool contains( double x, double y ) const; /** * Returns whether the normalized rectangle intersects the @p other normalized * rectangle. */ bool intersects( const NormalizedRect &other ) const; /** * This is an overloaded member function, provided for convenience. It behaves essentially * like the above function. */ bool intersects( const NormalizedRect *other ) const; /** * Returns whether the normalized rectangle intersects an other normalized * rectangle, which is defined by @p left, @p top, @p right and @p bottom. */ bool intersects( double left, double top, double right, double bottom ) const; /** * Returns the rectangle that accrues when the normalized rectangle is multiplyed * with the scaling @p xScale and @p yScale. */ QRect geometry( int xScale, int yScale ) const; /** * Same functionality as geometry, but the output is now rounded before typecasting to int * @since 0.14 (KDE 4.8) */ QRect roundedGeometry( int xScale, int yScale ) const; /** * Returns the normalized bounding rectangle of the normalized rectangle * combined with the @p other normalized rectangle. */ NormalizedRect operator|( const NormalizedRect &other ) const; /** * Sets the normalized rectangle to the normalized bounding rectangle * of itself combined with the @p other normalized rectangle. */ NormalizedRect& operator|=( const NormalizedRect &other ); /** * Returns the intersection of this normalized rectangle with the specified * @p other. If the rects do not intersect then the result is null. * * @since 0.7 (KDE 4.1) */ NormalizedRect operator&( const NormalizedRect &other ) const; /** * Returns whether the normalized rectangle is equal to the @p other * normalized rectangle. */ bool operator==( const NormalizedRect &other ) const; /** * Returns the center of the rectangle * @since 0.10 (KDE 4.4) */ NormalizedPoint center() const; /** * Transforms the normalized rectangle with the operations defined by @p matrix. */ void transform( const QTransform &matrix ); /** * Returns true if the point pt is located to the bottom of the rectangle * @since 0.14 (KDE 4.8) */ bool isBottom(const NormalizedPoint& pt) const { return bottom < pt.y; } /** * Returns true if the point pt is located on the top of the rectangle * @since 0.14 (KDE 4.8) */ bool isTop(const NormalizedPoint& pt) const { return top > pt.y; } /** * Returns true if the point pt is located under the top of the rectangle * @since 0.14 (KDE 4.8) */ bool isBottomOrLevel(const NormalizedPoint& pt) const { return top < pt.y; } /** * Returns true if the point pt is located above the bottom of the rectangle * @since 0.14 (KDE 4.8) */ bool isTopOrLevel(const NormalizedPoint& pt) const { return bottom > pt.y; } /** * Returns true if the point pt is located to the right of the left arm of rectangle * @since 0.14 (KDE 4.8) */ bool isLeft(const NormalizedPoint& pt) const { return left < pt.x; } /** * Returns true if the point pt is located to the left of the right arm of rectangle * @since 0.14 (KDE 4.8) */ bool isRight(const NormalizedPoint& pt) const { return right > pt.x; } /** * Returns the distance of the point @p x @p y @p xScale @p yScale to the closest * edge or 0 if the point is within the rectangle * @since 0.17 (KDE 4.11) */ double distanceSqr(double x, double y, double xScale, double yScale) const { double distX = 0; if ( x < left ) distX = left - x; else if ( x > right ) distX = x - right; double distY = 0; if ( top > y ) distY = top - y; else if (bottom < y) distY = y - bottom; return pow( distX * xScale, 2 ) + pow( distY * yScale, 2 ); } + /// @since 1.4 + double width() const + { + return right - left; + } + + /// @since 1.4 + double height() const + { + return bottom - top; + } + /** * The normalized left coordinate. */ double left; /** * The normalized top coordinate. */ double top; /** * The normalized right coordinate. */ double right; /** * The normalized bottom coordinate. */ double bottom; }; //KDE_DUMMY_QHASH_FUNCTION(NormalizedRect) /** * @short NormalizedRect that contains a reference to an object. * * These rects contains a pointer to a okular object (such as an action or something * like that). The pointer is read and stored as 'void pointer' so cast is * performed by accessors based on the value returned by objectType(). Objects * are reparented to this class. * * Type / Class correspondency tab: * - Action : class Action: description of an action * - Image : class Image : description of an image (n/a) * - Annotation: class Annotation: description of an annotation */ class OKULARCORE_EXPORT ObjectRect { public: /** * Describes the type of storable object. */ enum ObjectType { Action, ///< An action Image, ///< An image OAnnotation, ///< An annotation SourceRef ///< A source reference }; /** * Creates a new object rectangle. * * @param left The left coordinate of the rectangle. * @param top The top coordinate of the rectangle. * @param right The right coordinate of the rectangle. * @param bottom The bottom coordinate of the rectangle. * @param ellipse If true the rectangle describes an ellipse. * @param type The type of the storable object @see ObjectType. * @param object The pointer to the storable object. */ ObjectRect( double left, double top, double right, double bottom, bool ellipse, ObjectType type, void *object ); /** * This is an overloaded member function, provided for convenience. */ ObjectRect( const NormalizedRect &rect, bool ellipse, ObjectType type, void *object ); /** * This is an overloaded member function, provided for convenience. */ ObjectRect( const QPolygonF &poly, ObjectType type, void *object ); /** * Destroys the object rectangle. */ virtual ~ObjectRect(); /** * Returns the object type of the object rectangle. * @see ObjectType */ ObjectType objectType() const; /** * Returns the storable object of the object rectangle. */ const void *object() const; /** * Returns the region that is covered by the object rectangle. */ const QPainterPath ®ion() const; /** * Returns the bounding rect of the object rectangle for the * scaling factor @p xScale and @p yScale. */ virtual QRect boundingRect( double xScale, double yScale ) const; /** * Returns whether the object rectangle contains the point @p x, @p y for the * scaling factor @p xScale and @p yScale. */ virtual bool contains( double x, double y, double xScale, double yScale ) const; /** * Transforms the object rectangle with the operations defined by @p matrix. */ virtual void transform( const QTransform &matrix ); /** * Returns the square of the distance between the object and the point @p x, @p y * for the scaling factor @p xScale and @p yScale. * * @since 0.8.2 (KDE 4.2.2) */ // FIXME this should most probably be a virtual method double distanceSqr( double x, double y, double xScale, double yScale ) const; protected: ObjectType m_objectType; void * m_object; QPainterPath m_path; QPainterPath m_transformedPath; }; /** * This class describes the object rectangle for an annotation. */ class OKULARCORE_EXPORT AnnotationObjectRect : public ObjectRect { public: /** * Creates a new annotation object rectangle with the * given @p annotation. */ AnnotationObjectRect( Annotation *annotation ); /** * Destroys the annotation object rectangle. */ virtual ~AnnotationObjectRect(); /** * Returns the annotation object of the annotation object rectangle. */ Annotation *annotation() const; /** * Returns the bounding rect of the annotation object rectangle for the * scaling factor @p xScale and @p yScale. */ QRect boundingRect( double xScale, double yScale ) const override; /** * Returns whether the annotation object rectangle contains the point @p x, @p y for the * scaling factor @p xScale and @p yScale. */ bool contains( double x, double y, double xScale, double yScale ) const override; /** * Transforms the annotation object rectangle with the operations defined by @p matrix. */ void transform( const QTransform &matrix ) override; private: Annotation * m_annotation; }; /** * This class describes the object rectangle for a source reference. */ class OKULARCORE_EXPORT SourceRefObjectRect : public ObjectRect { friend class ObjectRect; public: /** * Creates a new source reference object rectangle. * * @param point The point of the source reference. * @param reference The storable source reference object. */ SourceRefObjectRect( const NormalizedPoint& point, void *reference ); /** * Returns the bounding rect of the source reference object rectangle for the * scaling factor @p xScale and @p yScale. */ QRect boundingRect( double xScale, double yScale ) const override; /** * Returns whether the source reference object rectangle contains the point @p x, @p y for the * scaling factor @p xScale and @p yScale. */ bool contains( double x, double y, double xScale, double yScale ) const override; private: NormalizedPoint m_point; }; /// @cond PRIVATE /** @internal */ template void doDelete( T& t ) { (void)t; } /** @internal */ template T* givePtr( T& t ) { return &t; } /** @internal */ template T& deref( T& t ) { return t; } /** @internal */ template static void doDelete( T* t ) { delete t; } /** @internal */ template static T* givePtr( T* t ) { return t; } /** @internal */ template static T& deref( T* t ) { return *t; } /// @endcond /** * @short A regular area of NormalizedShape which normalizes a Shape * * Class NormalizedShape \b must have the following functions/operators defined: * - bool contains( double, double ) * - bool intersects( NormalizedShape ) * - bool isNull() * - Shape geometry( int, int ) * - operator|=( NormalizedShape ) which unite two NormalizedShape's */ template class RegularArea : public QList { public: /** * Destroys a regular area. */ ~RegularArea(); /** * Returns whether the regular area contains the * normalized point @p x, @p y. */ bool contains( double x, double y ) const; /** * Returns whether the regular area contains the * given @p shape. */ bool contains( const NormalizedShape& shape ) const; /** * Returns whether the regular area intersects with the given @p area. */ bool intersects( const RegularArea *area ) const; /** * Returns whether the regular area intersects with the given @p shape. */ bool intersects( const NormalizedShape& shape ) const; /** * Appends the given @p area to the regular area. */ void appendArea( const RegularArea *area ); /** * Appends the given @p shape to the regular area. */ void appendShape( const NormalizedShape& shape, MergeSide side = MergeAll ); /** * Simplifies the regular area by merging its intersecting subareas. */ void simplify(); /** * Returns whether the regular area is a null area. */ bool isNull() const; /** * Returns the subareas of the regular areas as shapes for the given scaling factor * @p xScale and @p yScale, translated by @p dx and @p dy. */ QList geometry( int xScale, int yScale, int dx = 0, int dy = 0 ) const; /** * Transforms the regular area with the operations defined by @p matrix. */ void transform( const QTransform &matrix ); }; template RegularArea::~RegularArea() { int size = this->count(); for ( int i = 0; i < size; ++i ) doDelete( (*this)[i] ); } template void RegularArea::simplify() { #ifdef DEBUG_REGULARAREA int prev_end = this->count(); #endif int end = this->count() - 1, x = 0; for ( int i = 0; i < end; ++i ) { if ( givePtr( (*this)[x] )->intersects( deref( (*this)[i+1] ) ) ) { deref((*this)[x]) |= deref((*this)[i+1]); NormalizedShape& tobedeleted = (*this)[i+1]; this->removeAt( i + 1 ); doDelete( tobedeleted ); --end; --i; } else { x=i+1; } } #ifdef DEBUG_REGULARAREA qCDebug(OkularCoreDebug) << "from" << prev_end << "to" << this->count(); #endif } template bool RegularArea::isNull() const { if ( this->isEmpty() ) return false; typename QList::const_iterator it = this->begin(), itEnd = this->end(); for ( ; it != itEnd; ++it ) if ( !givePtr( *it )->isNull() ) return false; return true; } template bool RegularArea::intersects( const NormalizedShape& rect ) const { if ( this->isEmpty() ) return false; typename QList::const_iterator it = this->begin(), itEnd = this->end(); for ( ; it != itEnd; ++it ) if ( !givePtr( *it )->isNull() && givePtr( *it )->intersects( rect ) ) return true; return false; } template bool RegularArea::intersects( const RegularArea *area ) const { if ( this->isEmpty() ) return false; typename QList::const_iterator it = this->begin(), itEnd = this->end(); for ( ; it != itEnd; ++it ) { typename QList::const_iterator areaIt = area->begin(), areaItEnd = area->end(); for ( ; areaIt != areaItEnd; ++areaIt ) { if ( !( *it ).isNull() && ( *it ).intersects( *areaIt ) ) return true; } } return false; } template void RegularArea::appendArea( const RegularArea *area ) { typename QList::const_iterator areaIt = area->begin(), areaItEnd = area->end(); for ( ; areaIt != areaItEnd; ++areaIt ) this->append( *areaIt ); } template void RegularArea::appendShape( const NormalizedShape& shape, MergeSide side ) { int size = this->count(); // if the list is empty, adds the shape normally if ( size == 0 ) { this->append( shape ); } else { bool intersection = false; NormalizedShape& last = (*this)[size - 1]; #define O_LAST givePtr( last ) # define O_LAST_R O_LAST->right # define O_LAST_L O_LAST->left # define O_LAST_T O_LAST->top # define O_LAST_B O_LAST->bottom #define O_NEW givePtr( shape ) # define O_NEW_R O_NEW->right # define O_NEW_L O_NEW->left # define O_NEW_T O_NEW->top # define O_NEW_B O_NEW->bottom switch ( side ) { case MergeRight: intersection = ( O_LAST_R >= O_NEW_L ) && ( O_LAST_L <= O_NEW_R ) && ( ( O_LAST_T <= O_NEW_T && O_LAST_B >= O_NEW_B ) || ( O_LAST_T >= O_NEW_T && O_LAST_B <= O_NEW_B ) ); break; case MergeBottom: intersection = ( O_LAST_B >= O_NEW_T ) && ( O_LAST_T <= O_NEW_B ) && ( ( O_LAST_R <= O_NEW_R && O_LAST_L >= O_NEW_L ) || ( O_LAST_R >= O_NEW_R && O_LAST_L <= O_NEW_L ) ); break; case MergeLeft: intersection = ( O_LAST_L <= O_NEW_R ) && ( O_LAST_R >= O_NEW_L ) && ( ( O_LAST_T <= O_NEW_T && O_LAST_B >= O_NEW_B ) || ( O_LAST_T >= O_NEW_T && O_LAST_B <= O_NEW_B ) ); break; case MergeTop: intersection = ( O_LAST_T <= O_NEW_B ) && ( O_LAST_B >= O_NEW_T ) && ( ( O_LAST_R <= O_NEW_R && O_LAST_L >= O_NEW_L ) || ( O_LAST_R >= O_NEW_R && O_LAST_L <= O_NEW_L ) ); break; case MergeAll: intersection = O_LAST->intersects( shape ); break; } #undef O_LAST # undef O_LAST_R # undef O_LAST_L # undef O_LAST_T # undef O_LAST_B #undef O_NEW # undef O_NEW_R # undef O_NEW_L # undef O_NEW_T # undef O_NEW_B // if the new shape intersects with the last shape in the list, then // merge it with that and delete the shape if ( intersection ) { deref((*this)[size - 1]) |= deref( shape ); doDelete( const_cast( shape ) ); } else this->append( shape ); } } template bool RegularArea::contains( double x, double y ) const { if ( this->isEmpty() ) return false; typename QList::const_iterator it = this->begin(), itEnd = this->end(); for ( ; it != itEnd; ++it ) if ( ( *it ).contains( x, y ) ) return true; return false; } template bool RegularArea::contains( const NormalizedShape& shape ) const { if ( this->isEmpty() ) return false; return QList::contains( shape ); } template QList RegularArea::geometry( int xScale, int yScale, int dx, int dy ) const { if ( this->isEmpty() ) return QList(); QList ret; Shape t; typename QList::const_iterator it = this->begin(), itEnd = this->end(); for ( ; it != itEnd; ++it ) { t = givePtr( *it )->geometry( xScale, yScale ); t.translate( dx, dy ); ret.append( t ); } return ret; } template void RegularArea::transform( const QTransform &matrix ) { if ( this->isEmpty() ) return; for ( int i = 0; i < this->count(); ++i ) givePtr( (*this)[i] )->transform( matrix ); } class OKULARCORE_EXPORT RegularAreaRect : public RegularArea< NormalizedRect, QRect > { public: RegularAreaRect(); RegularAreaRect( const RegularAreaRect& rar ); ~RegularAreaRect(); RegularAreaRect& operator=( const RegularAreaRect& rar ); private: class Private; Private * const d; }; /** * This class stores the coordinates of a highlighting area * together with the id of the highlight owner and the color. */ class HighlightAreaRect : public RegularAreaRect { public: /** * Creates a new highlight area rect with the coordinates of * the given @p area. */ HighlightAreaRect( const RegularAreaRect *area = nullptr ); /** * The search ID of the highlight owner. */ int s_id; /** * The color of the highlight. */ QColor color; }; } uint qHash(const Okular::NormalizedRect& r, uint seed = 0); #ifndef QT_NO_DEBUG_STREAM /** * Debug operator for normalized @p point. */ OKULARCORE_EXPORT QDebug operator<<( QDebug str, const Okular::NormalizedPoint &point ); /** * Debug operator for normalized @p rect. */ OKULARCORE_EXPORT QDebug operator<<( QDebug str, const Okular::NormalizedRect &rect ); #endif #endif diff --git a/core/document.cpp b/core/document.cpp index 00744c502..8db23b3e4 100644 --- a/core/document.cpp +++ b/core/document.cpp @@ -1,5394 +1,5540 @@ /*************************************************************************** * Copyright (C) 2004-2005 by Enrico Ros * * Copyright (C) 2004-2008 by Albert Astals Cid * * Copyright (C) 2017 Klarälvdalens Datakonsult AB, a KDAB Group * * company, info@kdab.com. Work sponsored by the * * LiMux project of the city of Munich * * * * 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. * ***************************************************************************/ #include "document.h" #include "document_p.h" #include "documentcommands_p.h" #include #ifdef Q_OS_WIN #define _WIN32_WINNT 0x0500 #include #elif defined(Q_OS_FREEBSD) #include #include #include #endif // qt/kde/system includes #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // local includes #include "action.h" #include "annotations.h" #include "annotations_p.h" #include "audioplayer.h" #include "audioplayer_p.h" #include "bookmarkmanager.h" #include "chooseenginedialog_p.h" #include "debug_p.h" #include "generator_p.h" #include "interfaces/configinterface.h" #include "interfaces/guiinterface.h" #include "interfaces/printinterface.h" #include "interfaces/saveinterface.h" #include "observer.h" #include "misc.h" #include "page.h" #include "page_p.h" #include "pagecontroller_p.h" #include "scripter.h" #include "settings_core.h" #include "sourcereference.h" #include "sourcereference_p.h" #include "texteditors_p.h" #include "tile.h" #include "tilesmanager_p.h" #include "utils_p.h" #include "view.h" #include "view_p.h" #include "form.h" #include "utils.h" #include #include using namespace Okular; struct AllocatedPixmap { // owner of the page DocumentObserver *observer; int page; qulonglong memory; // public constructor: initialize data AllocatedPixmap( DocumentObserver *o, int p, qulonglong m ) : observer( o ), page( p ), memory( m ) {} }; struct ArchiveData { ArchiveData() { } QString originalFileName; QTemporaryFile document; QTemporaryFile metadataFile; }; struct RunningSearch { // store search properties int continueOnPage; RegularAreaRect continueOnMatch; QSet< int > highlightedPages; // fields related to previous searches (used for 'continueSearch') QString cachedString; Document::SearchType cachedType; Qt::CaseSensitivity cachedCaseSensitivity; bool cachedViewportMove : 1; bool isCurrentlySearching : 1; QColor cachedColor; int pagesDone; }; #define foreachObserver( cmd ) {\ QSet< DocumentObserver * >::const_iterator it=d->m_observers.constBegin(), end=d->m_observers.constEnd();\ for ( ; it != end ; ++ it ) { (*it)-> cmd ; } } #define foreachObserverD( cmd ) {\ QSet< DocumentObserver * >::const_iterator it = m_observers.constBegin(), end = m_observers.constEnd();\ for ( ; it != end ; ++ it ) { (*it)-> cmd ; } } #define OKULAR_HISTORY_MAXSTEPS 100 #define OKULAR_HISTORY_SAVEDSTEPS 10 /***** Document ******/ QString DocumentPrivate::pagesSizeString() const { if (m_generator) { if (m_generator->pagesSizeMetric() != Generator::None) { QSizeF size = m_parent->allPagesSize(); if (size.isValid()) return localizedSize(size); else return QString(); } else return QString(); } else return QString(); } QString DocumentPrivate::namePaperSize(double inchesWidth, double inchesHeight) const { const QPrinter::Orientation orientation = inchesWidth > inchesHeight ? QPrinter::Landscape : QPrinter::Portrait; const QSize pointsSize(inchesWidth *72.0, inchesHeight*72.0); const QPageSize::PageSizeId paperSize = QPageSize::id(pointsSize, QPageSize::FuzzyOrientationMatch); const QString paperName = QPageSize::name(paperSize); if (orientation == QPrinter::Portrait) { return i18nc("paper type and orientation (eg: Portrait A4)", "Portrait %1", paperName); } else { return i18nc("paper type and orientation (eg: Portrait A4)", "Landscape %1", paperName); } } QString DocumentPrivate::localizedSize(const QSizeF &size) const { double inchesWidth = 0, inchesHeight = 0; switch (m_generator->pagesSizeMetric()) { case Generator::Points: inchesWidth = size.width() / 72.0; inchesHeight = size.height() / 72.0; break; case Generator::Pixels: { const QSizeF dpi = m_generator->dpi(); inchesWidth = size.width() / dpi.width(); inchesHeight = size.height() / dpi.height(); } break; case Generator::None: break; } if (QLocale::system().measurementSystem() == QLocale::ImperialSystem) { return i18nc("%1 is width, %2 is height, %3 is paper size name", "%1 x %2 in (%3)", inchesWidth, inchesHeight, namePaperSize(inchesWidth, inchesHeight)); } else { return i18nc("%1 is width, %2 is height, %3 is paper size name", "%1 x %2 mm (%3)", QString::number(inchesWidth * 25.4, 'd', 0), QString::number(inchesHeight * 25.4, 'd', 0), namePaperSize(inchesWidth, inchesHeight)); } } qulonglong DocumentPrivate::calculateMemoryToFree() { // [MEM] choose memory parameters based on configuration profile qulonglong clipValue = 0; qulonglong memoryToFree = 0; switch ( SettingsCore::memoryLevel() ) { case SettingsCore::EnumMemoryLevel::Low: memoryToFree = m_allocatedPixmapsTotalMemory; break; case SettingsCore::EnumMemoryLevel::Normal: { qulonglong thirdTotalMemory = getTotalMemory() / 3; qulonglong freeMemory = getFreeMemory(); if (m_allocatedPixmapsTotalMemory > thirdTotalMemory) memoryToFree = m_allocatedPixmapsTotalMemory - thirdTotalMemory; if (m_allocatedPixmapsTotalMemory > freeMemory) clipValue = (m_allocatedPixmapsTotalMemory - freeMemory) / 2; } break; case SettingsCore::EnumMemoryLevel::Aggressive: { qulonglong freeMemory = getFreeMemory(); if (m_allocatedPixmapsTotalMemory > freeMemory) clipValue = (m_allocatedPixmapsTotalMemory - freeMemory) / 2; } break; case SettingsCore::EnumMemoryLevel::Greedy: { qulonglong freeSwap; qulonglong freeMemory = getFreeMemory( &freeSwap ); const qulonglong memoryLimit = qMin( qMax( freeMemory, getTotalMemory()/2 ), freeMemory+freeSwap ); if (m_allocatedPixmapsTotalMemory > memoryLimit) clipValue = (m_allocatedPixmapsTotalMemory - memoryLimit) / 2; } break; } if ( clipValue > memoryToFree ) memoryToFree = clipValue; return memoryToFree; } void DocumentPrivate::cleanupPixmapMemory() { cleanupPixmapMemory( calculateMemoryToFree() ); } void DocumentPrivate::cleanupPixmapMemory( qulonglong memoryToFree ) { if ( memoryToFree < 1 ) return; const int currentViewportPage = (*m_viewportIterator).pageNumber; // Create a QMap of visible rects, indexed by page number QMap< int, VisiblePageRect * > visibleRects; QVector< Okular::VisiblePageRect * >::const_iterator vIt = m_pageRects.constBegin(), vEnd = m_pageRects.constEnd(); for ( ; vIt != vEnd; ++vIt ) visibleRects.insert( (*vIt)->pageNumber, (*vIt) ); // Free memory starting from pages that are farthest from the current one int pagesFreed = 0; while ( memoryToFree > 0 ) { AllocatedPixmap * p = searchLowestPriorityPixmap( true, true ); if ( !p ) // No pixmap to remove break; qCDebug(OkularCoreDebug).nospace() << "Evicting cache pixmap observer=" << p->observer << " page=" << p->page; // m_allocatedPixmapsTotalMemory can't underflow because we always add or remove // the memory used by the AllocatedPixmap so at most it can reach zero m_allocatedPixmapsTotalMemory -= p->memory; // Make sure memoryToFree does not underflow if ( p->memory > memoryToFree ) memoryToFree = 0; else memoryToFree -= p->memory; pagesFreed++; // delete pixmap m_pagesVector.at( p->page )->deletePixmap( p->observer ); // delete allocation descriptor delete p; } // If we're still on low memory, try to free individual tiles // Store pages that weren't completely removed QLinkedList< AllocatedPixmap * > pixmapsToKeep; while (memoryToFree > 0) { int clean_hits = 0; foreach (DocumentObserver *observer, m_observers) { AllocatedPixmap * p = searchLowestPriorityPixmap( false, true, observer ); if ( !p ) // No pixmap to remove continue; clean_hits++; TilesManager *tilesManager = m_pagesVector.at( p->page )->d->tilesManager( observer ); if ( tilesManager && tilesManager->totalMemory() > 0 ) { qulonglong memoryDiff = p->memory; NormalizedRect visibleRect; if ( visibleRects.contains( p->page ) ) visibleRect = visibleRects[ p->page ]->rect; // Free non visible tiles tilesManager->cleanupPixmapMemory( memoryToFree, visibleRect, currentViewportPage ); p->memory = tilesManager->totalMemory(); memoryDiff -= p->memory; memoryToFree = (memoryDiff < memoryToFree) ? (memoryToFree - memoryDiff) : 0; m_allocatedPixmapsTotalMemory -= memoryDiff; if ( p->memory > 0 ) pixmapsToKeep.append( p ); else delete p; } else pixmapsToKeep.append( p ); } if (clean_hits == 0) break; } m_allocatedPixmaps += pixmapsToKeep; //p--rintf("freeMemory A:[%d -%d = %d] \n", m_allocatedPixmaps.count() + pagesFreed, pagesFreed, m_allocatedPixmaps.count() ); } /* Returns the next pixmap to evict from cache, or NULL if no suitable pixmap * if found. If unloadableOnly is set, only unloadable pixmaps are returned. If * thenRemoveIt is set, the pixmap is removed from m_allocatedPixmaps before * returning it */ AllocatedPixmap * DocumentPrivate::searchLowestPriorityPixmap( bool unloadableOnly, bool thenRemoveIt, DocumentObserver *observer ) { QLinkedList< AllocatedPixmap * >::iterator pIt = m_allocatedPixmaps.begin(); QLinkedList< AllocatedPixmap * >::iterator pEnd = m_allocatedPixmaps.end(); QLinkedList< AllocatedPixmap * >::iterator farthestPixmap = pEnd; const int currentViewportPage = (*m_viewportIterator).pageNumber; /* Find the pixmap that is farthest from the current viewport */ int maxDistance = -1; while ( pIt != pEnd ) { const AllocatedPixmap * p = *pIt; // Filter by observer if ( observer == nullptr || p->observer == observer ) { const int distance = qAbs( p->page - currentViewportPage ); if ( maxDistance < distance && ( !unloadableOnly || p->observer->canUnloadPixmap( p->page ) ) ) { maxDistance = distance; farthestPixmap = pIt; } } ++pIt; } /* No pixmap to remove */ if ( farthestPixmap == pEnd ) return nullptr; AllocatedPixmap * selectedPixmap = *farthestPixmap; if ( thenRemoveIt ) m_allocatedPixmaps.erase( farthestPixmap ); return selectedPixmap; } qulonglong DocumentPrivate::getTotalMemory() { static qulonglong cachedValue = 0; if ( cachedValue ) return cachedValue; #if defined(Q_OS_LINUX) // if /proc/meminfo doesn't exist, return 128MB QFile memFile( QStringLiteral("/proc/meminfo") ); if ( !memFile.open( QIODevice::ReadOnly ) ) return (cachedValue = 134217728); QTextStream readStream( &memFile ); while ( true ) { QString entry = readStream.readLine(); if ( entry.isNull() ) break; if ( entry.startsWith( QLatin1String("MemTotal:") ) ) return (cachedValue = (Q_UINT64_C(1024) * entry.section( QLatin1Char ( ' ' ), -2, -2 ).toULongLong())); } #elif defined(Q_OS_FREEBSD) qulonglong physmem; int mib[] = {CTL_HW, HW_PHYSMEM}; size_t len = sizeof( physmem ); if ( sysctl( mib, 2, &physmem, &len, NULL, 0 ) == 0 ) return (cachedValue = physmem); #elif defined(Q_OS_WIN) MEMORYSTATUSEX stat; stat.dwLength = sizeof(stat); GlobalMemoryStatusEx (&stat); return ( cachedValue = stat.ullTotalPhys ); #endif return (cachedValue = 134217728); } qulonglong DocumentPrivate::getFreeMemory( qulonglong *freeSwap ) { static QTime lastUpdate = QTime::currentTime().addSecs(-3); static qulonglong cachedValue = 0; static qulonglong cachedFreeSwap = 0; if ( qAbs( lastUpdate.secsTo( QTime::currentTime() ) ) <= 2 ) { if (freeSwap) *freeSwap = cachedFreeSwap; return cachedValue; } /* Initialize the returned free swap value to 0. It is overwritten if the * actual value is available */ if (freeSwap) *freeSwap = 0; #if defined(Q_OS_LINUX) // if /proc/meminfo doesn't exist, return MEMORY FULL QFile memFile( QStringLiteral("/proc/meminfo") ); if ( !memFile.open( QIODevice::ReadOnly ) ) return 0; // read /proc/meminfo and sum up the contents of 'MemFree', 'Buffers' // and 'Cached' fields. consider swapped memory as used memory. qulonglong memoryFree = 0; QString entry; QTextStream readStream( &memFile ); static const int nElems = 5; QString names[nElems] = { QStringLiteral("MemFree:"), QStringLiteral("Buffers:"), QStringLiteral("Cached:"), QStringLiteral("SwapFree:"), QStringLiteral("SwapTotal:") }; qulonglong values[nElems] = { 0, 0, 0, 0, 0 }; bool foundValues[nElems] = { false, false, false, false, false }; while ( true ) { entry = readStream.readLine(); if ( entry.isNull() ) break; for ( int i = 0; i < nElems; ++i ) { if ( entry.startsWith( names[i] ) ) { values[i] = entry.section( QLatin1Char ( ' ' ), -2, -2 ).toULongLong( &foundValues[i] ); } } } memFile.close(); bool found = true; for ( int i = 0; found && i < nElems; ++i ) found = found && foundValues[i]; if ( found ) { /* MemFree + Buffers + Cached - SwapUsed = * = MemFree + Buffers + Cached - (SwapTotal - SwapFree) = * = MemFree + Buffers + Cached + SwapFree - SwapTotal */ memoryFree = values[0] + values[1] + values[2] + values[3]; if ( values[4] > memoryFree ) memoryFree = 0; else memoryFree -= values[4]; } else { return 0; } lastUpdate = QTime::currentTime(); if (freeSwap) *freeSwap = ( cachedFreeSwap = (Q_UINT64_C(1024) * values[3]) ); return ( cachedValue = (Q_UINT64_C(1024) * memoryFree) ); #elif defined(Q_OS_FREEBSD) qulonglong cache, inact, free, psize; size_t cachelen, inactlen, freelen, psizelen; cachelen = sizeof( cache ); inactlen = sizeof( inact ); freelen = sizeof( free ); psizelen = sizeof( psize ); // sum up inactive, cached and free memory if ( sysctlbyname( "vm.stats.vm.v_cache_count", &cache, &cachelen, NULL, 0 ) == 0 && sysctlbyname( "vm.stats.vm.v_inactive_count", &inact, &inactlen, NULL, 0 ) == 0 && sysctlbyname( "vm.stats.vm.v_free_count", &free, &freelen, NULL, 0 ) == 0 && sysctlbyname( "vm.stats.vm.v_page_size", &psize, &psizelen, NULL, 0 ) == 0 ) { lastUpdate = QTime::currentTime(); return (cachedValue = (cache + inact + free) * psize); } else { return 0; } #elif defined(Q_OS_WIN) MEMORYSTATUSEX stat; stat.dwLength = sizeof(stat); GlobalMemoryStatusEx (&stat); lastUpdate = QTime::currentTime(); if (freeSwap) *freeSwap = ( cachedFreeSwap = stat.ullAvailPageFile ); return ( cachedValue = stat.ullAvailPhys ); #else // tell the memory is full.. will act as in LOW profile return 0; #endif } bool DocumentPrivate::loadDocumentInfo( LoadDocumentInfoFlags loadWhat ) // note: load data and stores it internally (document or pages). observers // are still uninitialized at this point so don't access them { //qCDebug(OkularCoreDebug).nospace() << "Using '" << d->m_xmlFileName << "' as document info file."; if ( m_xmlFileName.isEmpty() ) return false; QFile infoFile( m_xmlFileName ); return loadDocumentInfo( infoFile, loadWhat ); } bool DocumentPrivate::loadDocumentInfo( QFile &infoFile, LoadDocumentInfoFlags loadWhat ) { if ( !infoFile.exists() || !infoFile.open( QIODevice::ReadOnly ) ) return false; // Load DOM from XML file QDomDocument doc( QStringLiteral("documentInfo") ); if ( !doc.setContent( &infoFile ) ) { qCDebug(OkularCoreDebug) << "Can't load XML pair! Check for broken xml."; infoFile.close(); return false; } infoFile.close(); QDomElement root = doc.documentElement(); if ( root.tagName() != QLatin1String("documentInfo") ) return false; QUrl documentUrl( root.attribute( "url" ) ); bool loadedAnything = false; // set if something gets actually loaded // Parse the DOM tree QDomNode topLevelNode = root.firstChild(); while ( topLevelNode.isElement() ) { QString catName = topLevelNode.toElement().tagName(); // Restore page attributes (bookmark, annotations, ...) from the DOM if ( catName == QLatin1String("pageList") && ( loadWhat & LoadPageInfo ) ) { QDomNode pageNode = topLevelNode.firstChild(); while ( pageNode.isElement() ) { QDomElement pageElement = pageNode.toElement(); if ( pageElement.hasAttribute( QStringLiteral("number") ) ) { // get page number (node's attribute) bool ok; int pageNumber = pageElement.attribute( QStringLiteral("number") ).toInt( &ok ); // pass the domElement to the right page, to read config data from if ( ok && pageNumber >= 0 && pageNumber < (int)m_pagesVector.count() ) { if ( m_pagesVector[ pageNumber ]->d->restoreLocalContents( pageElement ) ) loadedAnything = true; } } pageNode = pageNode.nextSibling(); } } // Restore 'general info' from the DOM else if ( catName == QLatin1String("generalInfo") && ( loadWhat & LoadGeneralInfo ) ) { QDomNode infoNode = topLevelNode.firstChild(); while ( infoNode.isElement() ) { QDomElement infoElement = infoNode.toElement(); // restore viewports history if ( infoElement.tagName() == QLatin1String("history") ) { // clear history m_viewportHistory.clear(); // append old viewports QDomNode historyNode = infoNode.firstChild(); while ( historyNode.isElement() ) { QDomElement historyElement = historyNode.toElement(); if ( historyElement.hasAttribute( QStringLiteral("viewport") ) ) { QString vpString = historyElement.attribute( QStringLiteral("viewport") ); m_viewportIterator = m_viewportHistory.insert( m_viewportHistory.end(), DocumentViewport( vpString ) ); loadedAnything = true; } historyNode = historyNode.nextSibling(); } // consistancy check if ( m_viewportHistory.isEmpty() ) m_viewportIterator = m_viewportHistory.insert( m_viewportHistory.end(), DocumentViewport() ); } else if ( infoElement.tagName() == QLatin1String("rotation") ) { QString str = infoElement.text(); bool ok = true; int newrotation = !str.isEmpty() ? ( str.toInt( &ok ) % 4 ) : 0; if ( ok && newrotation != 0 ) { setRotationInternal( newrotation, false ); loadedAnything = true; } } else if ( infoElement.tagName() == QLatin1String("views") ) { QDomNode viewNode = infoNode.firstChild(); while ( viewNode.isElement() ) { QDomElement viewElement = viewNode.toElement(); if ( viewElement.tagName() == QLatin1String("view") ) { const QString viewName = viewElement.attribute( QStringLiteral("name") ); Q_FOREACH ( View * view, m_views ) { if ( view->name() == viewName ) { loadViewsInfo( view, viewElement ); loadedAnything = true; break; } } } viewNode = viewNode.nextSibling(); } } infoNode = infoNode.nextSibling(); } } topLevelNode = topLevelNode.nextSibling(); } // return loadedAnything; } void DocumentPrivate::loadViewsInfo( View *view, const QDomElement &e ) { QDomNode viewNode = e.firstChild(); while ( viewNode.isElement() ) { QDomElement viewElement = viewNode.toElement(); if ( viewElement.tagName() == QLatin1String("zoom") ) { const QString valueString = viewElement.attribute( QStringLiteral("value") ); bool newzoom_ok = true; const double newzoom = !valueString.isEmpty() ? valueString.toDouble( &newzoom_ok ) : 1.0; if ( newzoom_ok && newzoom != 0 && view->supportsCapability( View::Zoom ) && ( view->capabilityFlags( View::Zoom ) & ( View::CapabilityRead | View::CapabilitySerializable ) ) ) { view->setCapability( View::Zoom, newzoom ); } const QString modeString = viewElement.attribute( QStringLiteral("mode") ); bool newmode_ok = true; const int newmode = !modeString.isEmpty() ? modeString.toInt( &newmode_ok ) : 2; if ( newmode_ok && view->supportsCapability( View::ZoomModality ) && ( view->capabilityFlags( View::ZoomModality ) & ( View::CapabilityRead | View::CapabilitySerializable ) ) ) { view->setCapability( View::ZoomModality, newmode ); } } viewNode = viewNode.nextSibling(); } } void DocumentPrivate::saveViewsInfo( View *view, QDomElement &e ) const { if ( view->supportsCapability( View::Zoom ) && ( view->capabilityFlags( View::Zoom ) & ( View::CapabilityRead | View::CapabilitySerializable ) ) && view->supportsCapability( View::ZoomModality ) && ( view->capabilityFlags( View::ZoomModality ) & ( View::CapabilityRead | View::CapabilitySerializable ) ) ) { QDomElement zoomEl = e.ownerDocument().createElement( QStringLiteral("zoom") ); e.appendChild( zoomEl ); bool ok = true; const double zoom = view->capability( View::Zoom ).toDouble( &ok ); if ( ok && zoom != 0 ) { zoomEl.setAttribute( QStringLiteral("value"), QString::number(zoom) ); } const int mode = view->capability( View::ZoomModality ).toInt( &ok ); if ( ok ) { zoomEl.setAttribute( QStringLiteral("mode"), mode ); } } } QUrl DocumentPrivate::giveAbsoluteUrl( const QString & fileName ) const { if ( !QDir::isRelativePath( fileName ) ) return QUrl::fromLocalFile(fileName); if ( !m_url.isValid() ) return QUrl(); return QUrl(KIO::upUrl(m_url).toString() + fileName); } bool DocumentPrivate::openRelativeFile( const QString & fileName ) { QUrl url = giveAbsoluteUrl( fileName ); if ( url.isEmpty() ) return false; qCDebug(OkularCoreDebug).nospace() << "openRelativeFile: '" << url << "'"; emit m_parent->openUrl( url ); return true; } Generator * DocumentPrivate::loadGeneratorLibrary( const KPluginMetaData &service ) { KPluginLoader loader( service.fileName() ); qCDebug(OkularCoreDebug) << service.fileName(); KPluginFactory *factory = loader.factory(); if ( !factory ) { qCWarning(OkularCoreDebug).nospace() << "Invalid plugin factory for " << service.fileName() << ":" << loader.errorString(); return nullptr; } Generator * plugin = factory->create(); GeneratorInfo info( plugin, service ); m_loadedGenerators.insert( service.pluginId(), info ); return plugin; } void DocumentPrivate::loadAllGeneratorLibraries() { if ( m_generatorsLoaded ) return; loadServiceList( availableGenerators() ); m_generatorsLoaded = true; } void DocumentPrivate::loadServiceList( const QVector& offers ) { int count = offers.count(); if ( count <= 0 ) return; for ( int i = 0; i < count; ++i ) { QString id = offers.at(i).pluginId(); // don't load already loaded generators QHash< QString, GeneratorInfo >::const_iterator genIt = m_loadedGenerators.constFind( id ); if ( !m_loadedGenerators.isEmpty() && genIt != m_loadedGenerators.constEnd() ) continue; Generator * g = loadGeneratorLibrary( offers.at(i) ); (void)g; } } void DocumentPrivate::unloadGenerator( const GeneratorInfo& info ) { delete info.generator; } void DocumentPrivate::cacheExportFormats() { if ( m_exportCached ) return; const ExportFormat::List formats = m_generator->exportFormats(); for ( int i = 0; i < formats.count(); ++i ) { if ( formats.at( i ).mimeType().name() == QLatin1String( "text/plain" ) ) m_exportToText = formats.at( i ); else m_exportFormats.append( formats.at( i ) ); } m_exportCached = true; } ConfigInterface* DocumentPrivate::generatorConfig( GeneratorInfo& info ) { if ( info.configChecked ) return info.config; info.config = qobject_cast< Okular::ConfigInterface * >( info.generator ); info.configChecked = true; return info.config; } SaveInterface* DocumentPrivate::generatorSave( GeneratorInfo& info ) { if ( info.saveChecked ) return info.save; info.save = qobject_cast< Okular::SaveInterface * >( info.generator ); info.saveChecked = true; return info.save; } Document::OpenResult DocumentPrivate::openDocumentInternal( const KPluginMetaData& offer, bool isstdin, const QString& docFile, const QByteArray& filedata, const QString& password ) { QString propName = offer.pluginId(); QHash< QString, GeneratorInfo >::const_iterator genIt = m_loadedGenerators.constFind( propName ); m_walletGenerator = nullptr; if ( genIt != m_loadedGenerators.constEnd() ) { m_generator = genIt.value().generator; } else { m_generator = loadGeneratorLibrary( offer ); if ( !m_generator ) return Document::OpenError; genIt = m_loadedGenerators.constFind( propName ); Q_ASSERT( genIt != m_loadedGenerators.constEnd() ); } Q_ASSERT_X( m_generator, "Document::load()", "null generator?!" ); m_generator->d_func()->m_document = this; // connect error reporting signals QObject::connect( m_generator, &Generator::error, m_parent, &Document::error ); QObject::connect( m_generator, &Generator::warning, m_parent, &Document::warning ); QObject::connect( m_generator, &Generator::notice, m_parent, &Document::notice ); QApplication::setOverrideCursor( Qt::WaitCursor ); const QSizeF dpi = Utils::realDpi(m_widget); qCDebug(OkularCoreDebug) << "Output DPI:" << dpi; m_generator->setDPI(dpi); Document::OpenResult openResult = Document::OpenError; if ( !isstdin ) { openResult = m_generator->loadDocumentWithPassword( docFile, m_pagesVector, password ); } else if ( !filedata.isEmpty() ) { if ( m_generator->hasFeature( Generator::ReadRawData ) ) { openResult = m_generator->loadDocumentFromDataWithPassword( filedata, m_pagesVector, password ); } else { m_tempFile = new QTemporaryFile(); if ( !m_tempFile->open() ) { delete m_tempFile; m_tempFile = nullptr; } else { m_tempFile->write( filedata ); QString tmpFileName = m_tempFile->fileName(); m_tempFile->close(); openResult = m_generator->loadDocumentWithPassword( tmpFileName, m_pagesVector, password ); } } } QApplication::restoreOverrideCursor(); if ( openResult != Document::OpenSuccess || m_pagesVector.size() <= 0 ) { m_generator->d_func()->m_document = nullptr; QObject::disconnect( m_generator, nullptr, m_parent, nullptr ); // TODO this is a bit of a hack, since basically means that // you can only call walletDataForFile after calling openDocument // but since in reality it's what happens I've decided not to refactor/break API // One solution is just kill walletDataForFile and make OpenResult be an object // where the wallet data is also returned when OpenNeedsPassword m_walletGenerator = m_generator; m_generator = nullptr; qDeleteAll( m_pagesVector ); m_pagesVector.clear(); delete m_tempFile; m_tempFile = nullptr; // TODO: emit a message telling the document is empty if ( openResult == Document::OpenSuccess ) openResult = Document::OpenError; } return openResult; } bool DocumentPrivate::savePageDocumentInfo( QTemporaryFile *infoFile, int what ) const { if ( infoFile->open() ) { // 1. Create DOM QDomDocument doc( QStringLiteral("documentInfo") ); QDomProcessingInstruction xmlPi = doc.createProcessingInstruction( QStringLiteral( "xml" ), QStringLiteral( "version=\"1.0\" encoding=\"utf-8\"" ) ); doc.appendChild( xmlPi ); QDomElement root = doc.createElement( QStringLiteral("documentInfo") ); doc.appendChild( root ); // 2.1. Save page attributes (bookmark state, annotations, ... ) to DOM QDomElement pageList = doc.createElement( QStringLiteral("pageList") ); root.appendChild( pageList ); // .... save pages that hold data QVector< Page * >::const_iterator pIt = m_pagesVector.constBegin(), pEnd = m_pagesVector.constEnd(); for ( ; pIt != pEnd; ++pIt ) (*pIt)->d->saveLocalContents( pageList, doc, PageItems( what ) ); // 3. Save DOM to XML file QString xml = doc.toString(); QTextStream os( infoFile ); os.setCodec( "UTF-8" ); os << xml; return true; } return false; } DocumentViewport DocumentPrivate::nextDocumentViewport() const { DocumentViewport ret = m_nextDocumentViewport; if ( !m_nextDocumentDestination.isEmpty() && m_generator ) { DocumentViewport vp( m_parent->metaData( QStringLiteral("NamedViewport"), m_nextDocumentDestination ).toString() ); if ( vp.isValid() ) { ret = vp; } } return ret; } void DocumentPrivate::performAddPageAnnotation( int page, Annotation * annotation ) { Okular::SaveInterface * iface = qobject_cast< Okular::SaveInterface * >( m_generator ); AnnotationProxy *proxy = iface ? iface->annotationProxy() : nullptr; // find out the page to attach annotation Page * kp = m_pagesVector[ page ]; if ( !m_generator || !kp ) return; // the annotation belongs already to a page if ( annotation->d_ptr->m_page ) return; // add annotation to the page kp->addAnnotation( annotation ); // tell the annotation proxy if ( proxy && proxy->supports(AnnotationProxy::Addition) ) proxy->notifyAddition( annotation, page ); // notify observers about the change notifyAnnotationChanges( page ); if ( annotation->flags() & Annotation::ExternallyDrawn ) { // Redraw everything, including ExternallyDrawn annotations refreshPixmaps( page ); } } void DocumentPrivate::performRemovePageAnnotation( int page, Annotation * annotation ) { Okular::SaveInterface * iface = qobject_cast< Okular::SaveInterface * >( m_generator ); AnnotationProxy *proxy = iface ? iface->annotationProxy() : nullptr; bool isExternallyDrawn; // find out the page Page * kp = m_pagesVector[ page ]; if ( !m_generator || !kp ) return; if ( annotation->flags() & Annotation::ExternallyDrawn ) isExternallyDrawn = true; else isExternallyDrawn = false; // try to remove the annotation if ( m_parent->canRemovePageAnnotation( annotation ) ) { // tell the annotation proxy if ( proxy && proxy->supports(AnnotationProxy::Removal) ) proxy->notifyRemoval( annotation, page ); kp->removeAnnotation( annotation ); // Also destroys the object // in case of success, notify observers about the change notifyAnnotationChanges( page ); if ( isExternallyDrawn ) { // Redraw everything, including ExternallyDrawn annotations refreshPixmaps( page ); } } } void DocumentPrivate::performModifyPageAnnotation( int page, Annotation * annotation, bool appearanceChanged ) { Okular::SaveInterface * iface = qobject_cast< Okular::SaveInterface * >( m_generator ); AnnotationProxy *proxy = iface ? iface->annotationProxy() : nullptr; // find out the page Page * kp = m_pagesVector[ page ]; if ( !m_generator || !kp ) return; // tell the annotation proxy if ( proxy && proxy->supports(AnnotationProxy::Modification) ) { proxy->notifyModification( annotation, page, appearanceChanged ); } // notify observers about the change notifyAnnotationChanges( page ); if ( appearanceChanged && (annotation->flags() & Annotation::ExternallyDrawn) ) { /* When an annotation is being moved, the generator will not render it. * Therefore there's no need to refresh pixmaps after the first time */ if ( annotation->flags() & (Annotation::BeingMoved | Annotation::BeingResized) ) { if ( m_annotationBeingModified ) return; else // First time: take note m_annotationBeingModified = true; } else { m_annotationBeingModified = false; } // Redraw everything, including ExternallyDrawn annotations qCDebug(OkularCoreDebug) << "Refreshing Pixmaps"; refreshPixmaps( page ); } } void DocumentPrivate::performSetAnnotationContents( const QString & newContents, Annotation *annot, int pageNumber ) { bool appearanceChanged = false; // Check if appearanceChanged should be true switch ( annot->subType() ) { // If it's an in-place TextAnnotation, set the inplace text case Okular::Annotation::AText: { Okular::TextAnnotation * txtann = static_cast< Okular::TextAnnotation * >( annot ); if ( txtann->textType() == Okular::TextAnnotation::InPlace ) { appearanceChanged = true; } break; } // If it's a LineAnnotation, check if caption text is visible case Okular::Annotation::ALine: { Okular::LineAnnotation * lineann = static_cast< Okular::LineAnnotation * >( annot ); if ( lineann->showCaption() ) appearanceChanged = true; break; } default: break; } // Set contents annot->setContents( newContents ); // Tell the document the annotation has been modified performModifyPageAnnotation( pageNumber, annot, appearanceChanged ); } void DocumentPrivate::recalculateForms() { const QVariant fco = m_parent->metaData(QLatin1String("FormCalculateOrder")); const QVector formCalculateOrder = fco.value>(); foreach(int formId, formCalculateOrder) { for ( uint pageIdx = 0; pageIdx < m_parent->pages(); pageIdx++ ) { const Page *p = m_parent->page( pageIdx ); if (p) { foreach( FormField *form, p->formFields() ) { if ( form->id() == formId ) { Action *action = form->additionalAction( FormField::CalculateField ); if (action) { m_parent->processAction( action ); } else { qWarning() << "Form that is part of calculate order doesn't have a calculate action"; } } } } } } } void DocumentPrivate::saveDocumentInfo() const { if ( m_xmlFileName.isEmpty() ) return; QFile infoFile( m_xmlFileName ); qCDebug(OkularCoreDebug) << "About to save document info to" << m_xmlFileName; if (!infoFile.open( QIODevice::WriteOnly | QIODevice::Truncate)) { qCWarning(OkularCoreDebug) << "Failed to open docdata file" << m_xmlFileName; return; } // 1. Create DOM QDomDocument doc( QStringLiteral("documentInfo") ); QDomProcessingInstruction xmlPi = doc.createProcessingInstruction( QStringLiteral( "xml" ), QStringLiteral( "version=\"1.0\" encoding=\"utf-8\"" ) ); doc.appendChild( xmlPi ); QDomElement root = doc.createElement( QStringLiteral("documentInfo") ); root.setAttribute( QStringLiteral("url"), m_url.toDisplayString(QUrl::PreferLocalFile) ); doc.appendChild( root ); // 2.1. Save page attributes (bookmark state, annotations, ... ) to DOM // -> do this if there are not-yet-migrated annots or forms in docdata/ if ( m_docdataMigrationNeeded ) { QDomElement pageList = doc.createElement( "pageList" ); root.appendChild( pageList ); // OriginalAnnotationPageItems and OriginalFormFieldPageItems tell to // store the same unmodified annotation list and form contents that we // read when we opened the file and ignore any change made by the user. // Since we don't store annotations and forms in docdata/ any more, this is // necessary to preserve annotations/forms that previous Okular version // had stored there. const PageItems saveWhat = AllPageItems | OriginalAnnotationPageItems | OriginalFormFieldPageItems; // .... save pages that hold data QVector< Page * >::const_iterator pIt = m_pagesVector.constBegin(), pEnd = m_pagesVector.constEnd(); for ( ; pIt != pEnd; ++pIt ) (*pIt)->d->saveLocalContents( pageList, doc, saveWhat ); } // 2.2. Save document info (current viewport, history, ... ) to DOM QDomElement generalInfo = doc.createElement( QStringLiteral("generalInfo") ); root.appendChild( generalInfo ); // create rotation node if ( m_rotation != Rotation0 ) { QDomElement rotationNode = doc.createElement( QStringLiteral("rotation") ); generalInfo.appendChild( rotationNode ); rotationNode.appendChild( doc.createTextNode( QString::number( (int)m_rotation ) ) ); } // ... save history up to OKULAR_HISTORY_SAVEDSTEPS viewports QLinkedList< DocumentViewport >::const_iterator backIterator = m_viewportIterator; if ( backIterator != m_viewportHistory.constEnd() ) { // go back up to OKULAR_HISTORY_SAVEDSTEPS steps from the current viewportIterator int backSteps = OKULAR_HISTORY_SAVEDSTEPS; while ( backSteps-- && backIterator != m_viewportHistory.constBegin() ) --backIterator; // create history root node QDomElement historyNode = doc.createElement( QStringLiteral("history") ); generalInfo.appendChild( historyNode ); // add old[backIterator] and present[viewportIterator] items QLinkedList< DocumentViewport >::const_iterator endIt = m_viewportIterator; ++endIt; while ( backIterator != endIt ) { QString name = (backIterator == m_viewportIterator) ? QStringLiteral ("current") : QStringLiteral ("oldPage"); QDomElement historyEntry = doc.createElement( name ); historyEntry.setAttribute( QStringLiteral("viewport"), (*backIterator).toString() ); historyNode.appendChild( historyEntry ); ++backIterator; } } // create views root node QDomElement viewsNode = doc.createElement( QStringLiteral("views") ); generalInfo.appendChild( viewsNode ); Q_FOREACH ( View * view, m_views ) { QDomElement viewEntry = doc.createElement( QStringLiteral("view") ); viewEntry.setAttribute( QStringLiteral("name"), view->name() ); viewsNode.appendChild( viewEntry ); saveViewsInfo( view, viewEntry ); } // 3. Save DOM to XML file QString xml = doc.toString(); QTextStream os( &infoFile ); os.setCodec( "UTF-8" ); os << xml; infoFile.close(); } void DocumentPrivate::slotTimedMemoryCheck() { // [MEM] clean memory (for 'free mem dependant' profiles only) if ( SettingsCore::memoryLevel() != SettingsCore::EnumMemoryLevel::Low && m_allocatedPixmapsTotalMemory > 1024*1024 ) cleanupPixmapMemory(); } void DocumentPrivate::sendGeneratorPixmapRequest() { /* If the pixmap cache will have to be cleaned in order to make room for the * next request, get the distance from the current viewport of the page * whose pixmap will be removed. We will ignore preload requests for pages * that are at the same distance or farther */ const qulonglong memoryToFree = calculateMemoryToFree(); const int currentViewportPage = (*m_viewportIterator).pageNumber; int maxDistance = INT_MAX; // Default: No maximum if ( memoryToFree ) { AllocatedPixmap *pixmapToReplace = searchLowestPriorityPixmap( true ); if ( pixmapToReplace ) maxDistance = qAbs( pixmapToReplace->page - currentViewportPage ); } // find a request PixmapRequest * request = nullptr; m_pixmapRequestsMutex.lock(); while ( !m_pixmapRequestsStack.isEmpty() && !request ) { PixmapRequest * r = m_pixmapRequestsStack.last(); if (!r) { m_pixmapRequestsStack.pop_back(); continue; } QRect requestRect = r->isTile() ? r->normalizedRect().geometry( r->width(), r->height() ) : QRect( 0, 0, r->width(), r->height() ); TilesManager *tilesManager = r->d->tilesManager(); // If it's a preload but the generator is not threaded no point in trying to preload if ( r->preload() && !m_generator->hasFeature( Generator::Threaded ) ) { m_pixmapRequestsStack.pop_back(); delete r; } // request only if page isn't already present and request has valid id // request only if page isn't already present and request has valid id else if ( ( !r->d->mForce && r->page()->hasPixmap( r->observer(), r->width(), r->height(), r->normalizedRect() ) ) || !m_observers.contains(r->observer()) ) { m_pixmapRequestsStack.pop_back(); delete r; } else if ( !r->d->mForce && r->preload() && qAbs( r->pageNumber() - currentViewportPage ) >= maxDistance ) { m_pixmapRequestsStack.pop_back(); //qCDebug(OkularCoreDebug) << "Ignoring request that doesn't fit in cache"; delete r; } // Ignore requests for pixmaps that are already being generated else if ( tilesManager && tilesManager->isRequesting( r->normalizedRect(), r->width(), r->height() ) ) { m_pixmapRequestsStack.pop_back(); delete r; } // If the requested area is above 8000000 pixels, switch on the tile manager else if ( !tilesManager && m_generator->hasFeature( Generator::TiledRendering ) && (long)r->width() * (long)r->height() > 8000000L ) { // if the image is too big. start using tiles qCDebug(OkularCoreDebug).nospace() << "Start using tiles on page " << r->pageNumber() << " (" << r->width() << "x" << r->height() << " px);"; // fill the tiles manager with the last rendered pixmap const QPixmap *pixmap = r->page()->_o_nearestPixmap( r->observer(), r->width(), r->height() ); if ( pixmap ) { tilesManager = new TilesManager( r->pageNumber(), pixmap->width(), pixmap->height(), r->page()->rotation() ); - tilesManager->setPixmap( pixmap, NormalizedRect( 0, 0, 1, 1 ) ); + tilesManager->setPixmap( pixmap, NormalizedRect( 0, 0, 1, 1 ), true /*isPartialPixmap*/ ); tilesManager->setSize( r->width(), r->height() ); } else { // create new tiles manager tilesManager = new TilesManager( r->pageNumber(), r->width(), r->height(), r->page()->rotation() ); } tilesManager->setRequest( r->normalizedRect(), r->width(), r->height() ); r->page()->deletePixmap( r->observer() ); r->page()->d->setTilesManager( r->observer(), tilesManager ); r->setTile( true ); // Change normalizedRect to the smallest rect that contains all // visible tiles. if ( !r->normalizedRect().isNull() ) { NormalizedRect tilesRect; const QList tiles = tilesManager->tilesAt( r->normalizedRect(), TilesManager::TerminalTile ); QList::const_iterator tIt = tiles.constBegin(), tEnd = tiles.constEnd(); while ( tIt != tEnd ) { Tile tile = *tIt; if ( tilesRect.isNull() ) tilesRect = tile.rect(); else tilesRect |= tile.rect(); ++tIt; } r->setNormalizedRect( tilesRect ); request = r; } else { // Discard request if normalizedRect is null. This happens in // preload requests issued by PageView if the requested page is // not visible and the user has just switched from a non-tiled // zoom level to a tiled one m_pixmapRequestsStack.pop_back(); delete r; } } // If the requested area is below 6000000 pixels, switch off the tile manager else if ( tilesManager && (long)r->width() * (long)r->height() < 6000000L ) { qCDebug(OkularCoreDebug).nospace() << "Stop using tiles on page " << r->pageNumber() << " (" << r->width() << "x" << r->height() << " px);"; // page is too small. stop using tiles. r->page()->deletePixmap( r->observer() ); r->setTile( false ); request = r; } else if ( (long)requestRect.width() * (long)requestRect.height() > 200000000L && (SettingsCore::memoryLevel() != SettingsCore::EnumMemoryLevel::Greedy ) ) { m_pixmapRequestsStack.pop_back(); if ( !m_warnedOutOfMemory ) { qCWarning(OkularCoreDebug).nospace() << "Running out of memory on page " << r->pageNumber() << " (" << r->width() << "x" << r->height() << " px);"; qCWarning(OkularCoreDebug) << "this message will be reported only once."; m_warnedOutOfMemory = true; } delete r; } else { request = r; } } // if no request found (or already generated), return if ( !request ) { m_pixmapRequestsMutex.unlock(); return; } // [MEM] preventive memory freeing qulonglong pixmapBytes = 0; TilesManager * tm = request->d->tilesManager(); if ( tm ) pixmapBytes = tm->totalMemory(); else pixmapBytes = 4 * request->width() * request->height(); if ( pixmapBytes > (1024 * 1024) ) cleanupPixmapMemory( memoryToFree /* previously calculated value */ ); // submit the request to the generator if ( m_generator->canGeneratePixmap() ) { QRect requestRect = !request->isTile() ? QRect(0, 0, request->width(), request->height() ) : request->normalizedRect().geometry( request->width(), request->height() ); qCDebug(OkularCoreDebug).nospace() << "sending request observer=" << request->observer() << " " <pageNumber() << " async == " << request->asynchronous() << " isTile == " << request->isTile(); m_pixmapRequestsStack.removeAll ( request ); if ( tm ) tm->setRequest( request->normalizedRect(), request->width(), request->height() ); if ( (int)m_rotation % 2 ) request->d->swap(); if ( m_rotation != Rotation0 && !request->normalizedRect().isNull() ) request->setNormalizedRect( TilesManager::fromRotatedRect( request->normalizedRect(), m_rotation ) ); - request->setPartialUpdatesWanted( request->asynchronous() && !request->page()->hasPixmap( request->observer() ) ); + // If set elsewhere we already know we want it to be partial + if ( !request->partialUpdatesWanted() ) + { + request->setPartialUpdatesWanted( request->asynchronous() && !request->page()->hasPixmap( request->observer() ) ); + } // we always have to unlock _before_ the generatePixmap() because // a sync generation would end with requestDone() -> deadlock, and // we can not really know if the generator can do async requests m_executingPixmapRequests.push_back( request ); m_pixmapRequestsMutex.unlock(); m_generator->generatePixmap( request ); } else { m_pixmapRequestsMutex.unlock(); // pino (7/4/2006): set the polling interval from 10 to 30 QTimer::singleShot( 30, m_parent, SLOT(sendGeneratorPixmapRequest()) ); } } void DocumentPrivate::rotationFinished( int page, Okular::Page *okularPage ) { Okular::Page *wantedPage = m_pagesVector.value( page, 0 ); if ( !wantedPage || wantedPage != okularPage ) return; foreach(DocumentObserver *o, m_observers) o->notifyPageChanged( page, DocumentObserver::Pixmap | DocumentObserver::Annotations ); } void DocumentPrivate::slotFontReadingProgress( int page ) { emit m_parent->fontReadingProgress( page ); if ( page >= (int)m_parent->pages() - 1 ) { emit m_parent->fontReadingEnded(); m_fontThread = nullptr; m_fontsCached = true; } } void DocumentPrivate::fontReadingGotFont( const Okular::FontInfo& font ) { // Try to avoid duplicate fonts if (m_fontsCache.indexOf(font) == -1) { m_fontsCache.append( font ); emit m_parent->gotFont( font ); } } void DocumentPrivate::slotGeneratorConfigChanged( const QString& ) { if ( !m_generator ) return; // reparse generator config and if something changed clear Pages bool configchanged = false; QHash< QString, GeneratorInfo >::iterator it = m_loadedGenerators.begin(), itEnd = m_loadedGenerators.end(); for ( ; it != itEnd; ++it ) { Okular::ConfigInterface * iface = generatorConfig( it.value() ); if ( iface ) { bool it_changed = iface->reparseConfig(); if ( it_changed && ( m_generator == it.value().generator ) ) configchanged = true; } } if ( configchanged ) { // invalidate pixmaps QVector::const_iterator it = m_pagesVector.constBegin(), end = m_pagesVector.constEnd(); for ( ; it != end; ++it ) { (*it)->deletePixmaps(); } // [MEM] remove allocation descriptors qDeleteAll( m_allocatedPixmaps ); m_allocatedPixmaps.clear(); m_allocatedPixmapsTotalMemory = 0; // send reload signals to observers foreachObserverD( notifyContentsCleared( DocumentObserver::Pixmap ) ); } // free memory if in 'low' profile if ( SettingsCore::memoryLevel() == SettingsCore::EnumMemoryLevel::Low && !m_allocatedPixmaps.isEmpty() && !m_pagesVector.isEmpty() ) cleanupPixmapMemory(); } void DocumentPrivate::refreshPixmaps( int pageNumber ) { Page* page = m_pagesVector.value( pageNumber, 0 ); if ( !page ) return; - QLinkedList< Okular::PixmapRequest * > requestedPixmaps; QMap< DocumentObserver*, PagePrivate::PixmapObject >::ConstIterator it = page->d->m_pixmaps.constBegin(), itEnd = page->d->m_pixmaps.constEnd(); + QVector< Okular::PixmapRequest * > pixmapsToRequest; for ( ; it != itEnd; ++it ) { - QSize size = (*it).m_pixmap->size(); + const QSize size = (*it).m_pixmap->size(); PixmapRequest * p = new PixmapRequest( it.key(), pageNumber, size.width() / qApp->devicePixelRatio(), size.height() / qApp->devicePixelRatio(), 1, PixmapRequest::Asynchronous ); p->d->mForce = true; - requestedPixmaps.push_back( p ); + pixmapsToRequest << p; + } + + // Need to do this ↑↓ in two steps since requestPixmaps can end up calling cancelRenderingBecauseOf + // which changes m_pixmaps and thus breaks the loop above + for ( PixmapRequest *pr : qAsConst( pixmapsToRequest ) ) + { + QLinkedList< Okular::PixmapRequest * > requestedPixmaps; + pixmapsToRequest.push_back( pr ); + m_parent->requestPixmaps( requestedPixmaps, Okular::Document::NoOption ); } foreach (DocumentObserver *observer, m_observers) { + QLinkedList< Okular::PixmapRequest * > requestedPixmaps; + TilesManager *tilesManager = page->d->tilesManager( observer ); if ( tilesManager ) { tilesManager->markDirty(); PixmapRequest * p = new PixmapRequest( observer, pageNumber, tilesManager->width() / qApp->devicePixelRatio(), tilesManager->height() / qApp->devicePixelRatio(), 1, PixmapRequest::Asynchronous ); NormalizedRect tilesRect; // Get the visible page rect NormalizedRect visibleRect; QVector< Okular::VisiblePageRect * >::const_iterator vIt = m_pageRects.constBegin(), vEnd = m_pageRects.constEnd(); for ( ; vIt != vEnd; ++vIt ) { if ( (*vIt)->pageNumber == pageNumber ) { visibleRect = (*vIt)->rect; break; } } if ( !visibleRect.isNull() ) { p->setNormalizedRect( visibleRect ); p->setTile( true ); p->d->mForce = true; requestedPixmaps.push_back( p ); } else { delete p; } } - } - if ( !requestedPixmaps.isEmpty() ) m_parent->requestPixmaps( requestedPixmaps, Okular::Document::NoOption ); + } + } void DocumentPrivate::_o_configChanged() { // free text pages if needed calculateMaxTextPages(); while (m_allocatedTextPagesFifo.count() > m_maxAllocatedTextPages) { int pageToKick = m_allocatedTextPagesFifo.takeFirst(); m_pagesVector.at(pageToKick)->setTextPage( nullptr ); // deletes the textpage } } void DocumentPrivate::doContinueDirectionMatchSearch(void *doContinueDirectionMatchSearchStruct) { DoContinueDirectionMatchSearchStruct *searchStruct = static_cast(doContinueDirectionMatchSearchStruct); RunningSearch *search = m_searches.value(searchStruct->searchID); if ((m_searchCancelled && !searchStruct->match) || !search) { // if the user cancelled but he just got a match, give him the match! QApplication::restoreOverrideCursor(); if (search) search->isCurrentlySearching = false; emit m_parent->searchFinished( searchStruct->searchID, Document::SearchCancelled ); delete searchStruct->pagesToNotify; delete searchStruct; return; } const bool forward = search->cachedType == Document::NextMatch; bool doContinue = false; // if no match found, loop through the whole doc, starting from currentPage if ( !searchStruct->match ) { const int pageCount = m_pagesVector.count(); if (search->pagesDone < pageCount) { doContinue = true; if ( searchStruct->currentPage >= pageCount ) { searchStruct->currentPage = 0; emit m_parent->notice(i18n("Continuing search from beginning"), 3000); } else if ( searchStruct->currentPage < 0 ) { searchStruct->currentPage = pageCount - 1; emit m_parent->notice(i18n("Continuing search from bottom"), 3000); } } } if (doContinue) { // get page Page * page = m_pagesVector[ searchStruct->currentPage ]; // request search page if needed if ( !page->hasTextPage() ) m_parent->requestTextPage( page->number() ); // if found a match on the current page, end the loop searchStruct->match = page->findText( searchStruct->searchID, search->cachedString, forward ? FromTop : FromBottom, search->cachedCaseSensitivity ); if ( !searchStruct->match ) { if (forward) searchStruct->currentPage++; else searchStruct->currentPage--; search->pagesDone++; } else { search->pagesDone = 1; } // Both of the previous if branches need to call doContinueDirectionMatchSearch QMetaObject::invokeMethod(m_parent, "doContinueDirectionMatchSearch", Qt::QueuedConnection, Q_ARG(void *, searchStruct)); } else { doProcessSearchMatch( searchStruct->match, search, searchStruct->pagesToNotify, searchStruct->currentPage, searchStruct->searchID, search->cachedViewportMove, search->cachedColor ); delete searchStruct; } } void DocumentPrivate::doProcessSearchMatch( RegularAreaRect *match, RunningSearch *search, QSet< int > *pagesToNotify, int currentPage, int searchID, bool moveViewport, const QColor & color ) { // reset cursor to previous shape QApplication::restoreOverrideCursor(); bool foundAMatch = false; search->isCurrentlySearching = false; // if a match has been found.. if ( match ) { // update the RunningSearch structure adding this match.. foundAMatch = true; search->continueOnPage = currentPage; search->continueOnMatch = *match; search->highlightedPages.insert( currentPage ); // ..add highlight to the page.. m_pagesVector[ currentPage ]->d->setHighlight( searchID, match, color ); // ..queue page for notifying changes.. pagesToNotify->insert( currentPage ); // Create a normalized rectangle around the search match that includes a 5% buffer on all sides. const Okular::NormalizedRect matchRectWithBuffer = Okular::NormalizedRect( match->first().left - 0.05, match->first().top - 0.05, match->first().right + 0.05, match->first().bottom + 0.05 ); const bool matchRectFullyVisible = isNormalizedRectangleFullyVisible( matchRectWithBuffer, currentPage ); // ..move the viewport to show the first of the searched word sequence centered if ( moveViewport && !matchRectFullyVisible ) { DocumentViewport searchViewport( currentPage ); searchViewport.rePos.enabled = true; searchViewport.rePos.normalizedX = (match->first().left + match->first().right) / 2.0; searchViewport.rePos.normalizedY = (match->first().top + match->first().bottom) / 2.0; m_parent->setViewport( searchViewport, nullptr, true ); } delete match; } // notify observers about highlights changes foreach(int pageNumber, *pagesToNotify) foreach(DocumentObserver *observer, m_observers) observer->notifyPageChanged( pageNumber, DocumentObserver::Highlights ); if (foundAMatch) emit m_parent->searchFinished( searchID, Document::MatchFound ); else emit m_parent->searchFinished( searchID, Document::NoMatchFound ); delete pagesToNotify; } void DocumentPrivate::doContinueAllDocumentSearch(void *pagesToNotifySet, void *pageMatchesMap, int currentPage, int searchID) { QMap< Page *, QVector > *pageMatches = static_cast< QMap< Page *, QVector > * >(pageMatchesMap); QSet< int > *pagesToNotify = static_cast< QSet< int > * >( pagesToNotifySet ); RunningSearch *search = m_searches.value(searchID); if (m_searchCancelled || !search) { typedef QVector MatchesVector; QApplication::restoreOverrideCursor(); if (search) search->isCurrentlySearching = false; emit m_parent->searchFinished( searchID, Document::SearchCancelled ); foreach(const MatchesVector &mv, *pageMatches) qDeleteAll(mv); delete pageMatches; delete pagesToNotify; return; } if (currentPage < m_pagesVector.count()) { // get page (from the first to the last) Page *page = m_pagesVector.at(currentPage); int pageNumber = page->number(); // redundant? is it == currentPage ? // request search page if needed if ( !page->hasTextPage() ) m_parent->requestTextPage( pageNumber ); // loop on a page adding highlights for all found items RegularAreaRect * lastMatch = nullptr; while ( 1 ) { if ( lastMatch ) lastMatch = page->findText( searchID, search->cachedString, NextResult, search->cachedCaseSensitivity, lastMatch ); else lastMatch = page->findText( searchID, search->cachedString, FromTop, search->cachedCaseSensitivity ); if ( !lastMatch ) break; // add highligh rect to the matches map (*pageMatches)[page].append(lastMatch); } delete lastMatch; QMetaObject::invokeMethod(m_parent, "doContinueAllDocumentSearch", Qt::QueuedConnection, Q_ARG(void *, pagesToNotifySet), Q_ARG(void *, pageMatches), Q_ARG(int, currentPage + 1), Q_ARG(int, searchID)); } else { // reset cursor to previous shape QApplication::restoreOverrideCursor(); search->isCurrentlySearching = false; bool foundAMatch = pageMatches->count() != 0; QMap< Page *, QVector >::const_iterator it, itEnd; it = pageMatches->constBegin(); itEnd = pageMatches->constEnd(); for ( ; it != itEnd; ++it) { foreach(RegularAreaRect *match, it.value()) { it.key()->d->setHighlight( searchID, match, search->cachedColor ); delete match; } search->highlightedPages.insert( it.key()->number() ); pagesToNotify->insert( it.key()->number() ); } foreach(DocumentObserver *observer, m_observers) observer->notifySetup( m_pagesVector, 0 ); // notify observers about highlights changes foreach(int pageNumber, *pagesToNotify) foreach(DocumentObserver *observer, m_observers) observer->notifyPageChanged( pageNumber, DocumentObserver::Highlights ); if (foundAMatch) emit m_parent->searchFinished(searchID, Document::MatchFound ); else emit m_parent->searchFinished( searchID, Document::NoMatchFound ); delete pageMatches; delete pagesToNotify; } } void DocumentPrivate::doContinueGooglesDocumentSearch(void *pagesToNotifySet, void *pageMatchesMap, int currentPage, int searchID, const QStringList & words) { typedef QPair MatchColor; QMap< Page *, QVector > *pageMatches = static_cast< QMap< Page *, QVector > * >(pageMatchesMap); QSet< int > *pagesToNotify = static_cast< QSet< int > * >( pagesToNotifySet ); RunningSearch *search = m_searches.value(searchID); if (m_searchCancelled || !search) { typedef QVector MatchesVector; QApplication::restoreOverrideCursor(); if (search) search->isCurrentlySearching = false; emit m_parent->searchFinished( searchID, Document::SearchCancelled ); foreach(const MatchesVector &mv, *pageMatches) { foreach(const MatchColor &mc, mv) delete mc.first; } delete pageMatches; delete pagesToNotify; return; } const int wordCount = words.count(); const int hueStep = (wordCount > 1) ? (60 / (wordCount - 1)) : 60; int baseHue, baseSat, baseVal; search->cachedColor.getHsv( &baseHue, &baseSat, &baseVal ); if (currentPage < m_pagesVector.count()) { // get page (from the first to the last) Page *page = m_pagesVector.at(currentPage); int pageNumber = page->number(); // redundant? is it == currentPage ? // request search page if needed if ( !page->hasTextPage() ) m_parent->requestTextPage( pageNumber ); // loop on a page adding highlights for all found items bool allMatched = wordCount > 0, anyMatched = false; for ( int w = 0; w < wordCount; w++ ) { const QString &word = words[ w ]; int newHue = baseHue - w * hueStep; if ( newHue < 0 ) newHue += 360; QColor wordColor = QColor::fromHsv( newHue, baseSat, baseVal ); RegularAreaRect * lastMatch = nullptr; // add all highlights for current word bool wordMatched = false; while ( 1 ) { if ( lastMatch ) lastMatch = page->findText( searchID, word, NextResult, search->cachedCaseSensitivity, lastMatch ); else lastMatch = page->findText( searchID, word, FromTop, search->cachedCaseSensitivity); if ( !lastMatch ) break; // add highligh rect to the matches map (*pageMatches)[page].append(MatchColor(lastMatch, wordColor)); wordMatched = true; } allMatched = allMatched && wordMatched; anyMatched = anyMatched || wordMatched; } // if not all words are present in page, remove partial highlights const bool matchAll = search->cachedType == Document::GoogleAll; if ( !allMatched && matchAll ) { QVector &matches = (*pageMatches)[page]; foreach(const MatchColor &mc, matches) delete mc.first; pageMatches->remove(page); } QMetaObject::invokeMethod(m_parent, "doContinueGooglesDocumentSearch", Qt::QueuedConnection, Q_ARG(void *, pagesToNotifySet), Q_ARG(void *, pageMatches), Q_ARG(int, currentPage + 1), Q_ARG(int, searchID), Q_ARG(QStringList, words)); } else { // reset cursor to previous shape QApplication::restoreOverrideCursor(); search->isCurrentlySearching = false; bool foundAMatch = pageMatches->count() != 0; QMap< Page *, QVector >::const_iterator it, itEnd; it = pageMatches->constBegin(); itEnd = pageMatches->constEnd(); for ( ; it != itEnd; ++it) { foreach(const MatchColor &mc, it.value()) { it.key()->d->setHighlight( searchID, mc.first, mc.second ); delete mc.first; } search->highlightedPages.insert( it.key()->number() ); pagesToNotify->insert( it.key()->number() ); } // send page lists to update observers (since some filter on bookmarks) foreach(DocumentObserver *observer, m_observers) observer->notifySetup( m_pagesVector, 0 ); // notify observers about highlights changes foreach(int pageNumber, *pagesToNotify) foreach(DocumentObserver *observer, m_observers) observer->notifyPageChanged( pageNumber, DocumentObserver::Highlights ); if (foundAMatch) emit m_parent->searchFinished( searchID, Document::MatchFound ); else emit m_parent->searchFinished( searchID, Document::NoMatchFound ); delete pageMatches; delete pagesToNotify; } } QVariant DocumentPrivate::documentMetaData( const Generator::DocumentMetaDataKey key, const QVariant &option ) const { switch ( key ) { case Generator::PaperColorMetaData: { bool giveDefault = option.toBool(); QColor color; if ( ( SettingsCore::renderMode() == SettingsCore::EnumRenderMode::Paper ) && SettingsCore::changeColors() ) { color = SettingsCore::paperColor(); } else if ( giveDefault ) { color = Qt::white; } return color; } break; case Generator::TextAntialiasMetaData: switch ( SettingsCore::textAntialias() ) { case SettingsCore::EnumTextAntialias::Enabled: return true; break; #if 0 case Settings::EnumTextAntialias::UseKDESettings: // TODO: read the KDE configuration return true; break; #endif case SettingsCore::EnumTextAntialias::Disabled: return false; break; } break; case Generator::GraphicsAntialiasMetaData: switch ( SettingsCore::graphicsAntialias() ) { case SettingsCore::EnumGraphicsAntialias::Enabled: return true; break; case SettingsCore::EnumGraphicsAntialias::Disabled: return false; break; } break; case Generator::TextHintingMetaData: switch ( SettingsCore::textHinting() ) { case SettingsCore::EnumTextHinting::Enabled: return true; break; case SettingsCore::EnumTextHinting::Disabled: return false; break; } break; } return QVariant(); } bool DocumentPrivate::isNormalizedRectangleFullyVisible( const Okular::NormalizedRect & rectOfInterest, int rectPage ) { bool rectFullyVisible = false; const QVector & visibleRects = m_parent->visiblePageRects(); QVector::const_iterator vEnd = visibleRects.end(); QVector::const_iterator vIt = visibleRects.begin(); for ( ; ( vIt != vEnd ) && !rectFullyVisible; ++vIt ) { if ( (*vIt)->pageNumber == rectPage && (*vIt)->rect.contains( rectOfInterest.left, rectOfInterest.top ) && (*vIt)->rect.contains( rectOfInterest.right, rectOfInterest.bottom ) ) { rectFullyVisible = true; } } return rectFullyVisible; } struct pdfsyncpoint { QString file; qlonglong x; qlonglong y; int row; int column; int page; }; void DocumentPrivate::loadSyncFile( const QString & filePath ) { QFile f( filePath + QLatin1String( "sync" ) ); if ( !f.open( QIODevice::ReadOnly ) ) return; QTextStream ts( &f ); // first row: core name of the pdf output const QString coreName = ts.readLine(); // second row: version string, in the form 'Version %u' QString versionstr = ts.readLine(); QRegExp versionre( QStringLiteral("Version (\\d+)") ); versionre.setCaseSensitivity( Qt::CaseInsensitive ); if ( !versionre.exactMatch( versionstr ) ) return; QHash points; QStack fileStack; int currentpage = -1; const QLatin1String texStr( ".tex" ); const QChar spaceChar = QChar::fromLatin1( ' ' ); fileStack.push( coreName + texStr ); const QSizeF dpi = m_generator->dpi(); QString line; while ( !ts.atEnd() ) { line = ts.readLine(); const QStringList tokens = line.split( spaceChar, QString::SkipEmptyParts ); const int tokenSize = tokens.count(); if ( tokenSize < 1 ) continue; if ( tokens.first() == QLatin1String( "l" ) && tokenSize >= 3 ) { int id = tokens.at( 1 ).toInt(); QHash::const_iterator it = points.constFind( id ); if ( it == points.constEnd() ) { pdfsyncpoint pt; pt.x = 0; pt.y = 0; pt.row = tokens.at( 2 ).toInt(); pt.column = 0; // TODO pt.page = -1; pt.file = fileStack.top(); points[ id ] = pt; } } else if ( tokens.first() == QLatin1String( "s" ) && tokenSize >= 2 ) { currentpage = tokens.at( 1 ).toInt() - 1; } else if ( tokens.first() == QLatin1String( "p*" ) && tokenSize >= 4 ) { // TODO qCDebug(OkularCoreDebug) << "PdfSync: 'p*' line ignored"; } else if ( tokens.first() == QLatin1String( "p" ) && tokenSize >= 4 ) { int id = tokens.at( 1 ).toInt(); QHash::iterator it = points.find( id ); if ( it != points.end() ) { it->x = tokens.at( 2 ).toInt(); it->y = tokens.at( 3 ).toInt(); it->page = currentpage; } } else if ( line.startsWith( QLatin1Char( '(' ) ) && tokenSize == 1 ) { QString newfile = line; // chop the leading '(' newfile.remove( 0, 1 ); if ( !newfile.endsWith( texStr ) ) { newfile += texStr; } fileStack.push( newfile ); } else if ( line == QLatin1String( ")" ) ) { if ( !fileStack.isEmpty() ) { fileStack.pop(); } else qCDebug(OkularCoreDebug) << "PdfSync: going one level down too much"; } else qCDebug(OkularCoreDebug).nospace() << "PdfSync: unknown line format: '" << line << "'"; } QVector< QLinkedList< Okular::SourceRefObjectRect * > > refRects( m_pagesVector.size() ); foreach ( const pdfsyncpoint& pt, points ) { // drop pdfsync points not completely valid if ( pt.page < 0 || pt.page >= m_pagesVector.size() ) continue; // magic numbers for TeX's RSU's (Ridiculously Small Units) conversion to pixels Okular::NormalizedPoint p( ( pt.x * dpi.width() ) / ( 72.27 * 65536.0 * m_pagesVector[pt.page]->width() ), ( pt.y * dpi.height() ) / ( 72.27 * 65536.0 * m_pagesVector[pt.page]->height() ) ); QString file = pt.file; Okular::SourceReference * sourceRef = new Okular::SourceReference( file, pt.row, pt.column ); refRects[ pt.page ].append( new Okular::SourceRefObjectRect( p, sourceRef ) ); } for ( int i = 0; i < refRects.size(); ++i ) if ( !refRects.at(i).isEmpty() ) m_pagesVector[i]->setSourceReferences( refRects.at(i) ); } Document::Document( QWidget *widget ) : QObject( nullptr ), d( new DocumentPrivate( this ) ) { d->m_widget = widget; d->m_bookmarkManager = new BookmarkManager( d ); d->m_viewportIterator = d->m_viewportHistory.insert( d->m_viewportHistory.end(), DocumentViewport() ); d->m_undoStack = new QUndoStack(this); connect( SettingsCore::self(), SIGNAL(configChanged()), this, SLOT(_o_configChanged()) ); connect(d->m_undoStack, &QUndoStack::canUndoChanged, this, &Document::canUndoChanged); connect(d->m_undoStack, &QUndoStack::canRedoChanged, this, &Document::canRedoChanged); connect(d->m_undoStack, &QUndoStack::cleanChanged, this, &Document::undoHistoryCleanChanged); qRegisterMetaType(); } Document::~Document() { // delete generator, pages, and related stuff closeDocument(); QSet< View * >::const_iterator viewIt = d->m_views.constBegin(), viewEnd = d->m_views.constEnd(); for ( ; viewIt != viewEnd; ++viewIt ) { View *v = *viewIt; v->d_func()->document = nullptr; } // delete the bookmark manager delete d->m_bookmarkManager; // delete the loaded generators QHash< QString, GeneratorInfo >::const_iterator it = d->m_loadedGenerators.constBegin(), itEnd = d->m_loadedGenerators.constEnd(); for ( ; it != itEnd; ++it ) d->unloadGenerator( it.value() ); d->m_loadedGenerators.clear(); // delete the private structure delete d; } QString DocumentPrivate::docDataFileName(const QUrl &url, qint64 document_size) { QString fn = url.fileName(); fn = QString::number( document_size ) + QLatin1Char('.') + fn + QStringLiteral(".xml"); QString docdataDir = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QStringLiteral("/okular/docdata"); // make sure that the okular/docdata/ directory exists (probably this used to be handled by KStandardDirs) if (!QFileInfo::exists(docdataDir)) { qCDebug(OkularCoreDebug) << "creating docdata folder" << docdataDir; QDir().mkpath(docdataDir); } QString newokularfile = docdataDir + QLatin1Char('/') + fn; // we don't want to accidentally migrate old files when running unit tests if (!QFile::exists( newokularfile ) && !QStandardPaths::isTestModeEnabled()) { // see if an KDE4 file still exists static Kdelibs4Migration k4migration; QString oldfile = k4migration.locateLocal("data", QStringLiteral("okular/docdata/") + fn); if (oldfile.isEmpty()) { oldfile = k4migration.locateLocal("data", QStringLiteral("kpdf/") + fn); } if ( !oldfile.isEmpty() && QFile::exists( oldfile ) ) { // ### copy or move? if ( !QFile::copy( oldfile, newokularfile ) ) return QString(); } } return newokularfile; } QVector DocumentPrivate::availableGenerators() { static QVector result; if (result.isEmpty()) { result = KPluginLoader::findPlugins( QLatin1String ( "okular/generators" ) ); } return result; } KPluginMetaData DocumentPrivate::generatorForMimeType(const QMimeType& type, QWidget* widget, const QVector &triedOffers) { // First try to find an exact match, and then look for more general ones (e. g. the plain text one) // Ideally we would rank these by "closeness", but that might be overdoing it const QVector available = availableGenerators(); QVector offers; QVector exactMatches; QMimeDatabase mimeDatabase; for (const KPluginMetaData& md : available) { if (triedOffers.contains(md)) continue; foreach (const QString& supported, md.mimeTypes()) { QMimeType mimeType = mimeDatabase.mimeTypeForName(supported); if (mimeType == type && !exactMatches.contains(md)) { exactMatches << md; } if (type.inherits(supported) && !offers.contains(md)) { offers << md; } } } if (!exactMatches.isEmpty()) { offers = exactMatches; } if (offers.isEmpty()) { return KPluginMetaData(); } int hRank=0; // best ranked offer search int offercount = offers.size(); if (offercount > 1) { // sort the offers: the offers with an higher priority come before auto cmp = [](const KPluginMetaData& s1, const KPluginMetaData& s2) { const QString property = QStringLiteral("X-KDE-Priority"); return s1.rawData()[property].toInt() > s2.rawData()[property].toInt(); }; std::stable_sort(offers.begin(), offers.end(), cmp); if (SettingsCore::chooseGenerators()) { QStringList list; for (int i = 0; i < offercount; ++i) { list << offers.at(i).pluginId(); } ChooseEngineDialog choose(list, type, widget); if (choose.exec() == QDialog::Rejected) return KPluginMetaData(); hRank = choose.selectedGenerator(); } } Q_ASSERT(hRank < offers.size()); return offers.at(hRank); } Document::OpenResult Document::openDocument(const QString & docFile, const QUrl &url, const QMimeType &_mime, const QString & password ) { QMimeDatabase db; QMimeType mime = _mime; QByteArray filedata; bool isstdin = url.fileName() == QLatin1String( "-" ); bool triedMimeFromFileContent = false; if ( !isstdin ) { if ( !mime.isValid() ) return OpenError; d->m_url = url; d->m_docFileName = docFile; if ( !d->updateMetadataXmlNameAndDocSize() ) return OpenError; } else { QFile qstdin; qstdin.open( stdin, QIODevice::ReadOnly ); filedata = qstdin.readAll(); mime = db.mimeTypeForData( filedata ); if ( !mime.isValid() || mime.isDefault() ) return OpenError; d->m_docSize = filedata.size(); triedMimeFromFileContent = true; } // 0. load Generator // request only valid non-disabled plugins suitable for the mimetype KPluginMetaData offer = DocumentPrivate::generatorForMimeType(mime, d->m_widget); if ( !offer.isValid() && !triedMimeFromFileContent ) { QMimeType newmime = db.mimeTypeForFile(docFile, QMimeDatabase::MatchExtension); triedMimeFromFileContent = true; if ( newmime != mime ) { mime = newmime; offer = DocumentPrivate::generatorForMimeType(mime, d->m_widget); } if ( !offer.isValid() ) { // There's still no offers, do a final mime search based on the filename // We need this because sometimes (e.g. when downloading from a webserver) the mimetype we // use is the one fed by the server, that may be wrong newmime = db.mimeTypeForUrl( url ); if ( !newmime.isDefault() && newmime != mime ) { mime = newmime; offer = DocumentPrivate::generatorForMimeType(mime, d->m_widget); } } } if (!offer.isValid()) { emit error( i18n( "Can not find a plugin which is able to handle the document being passed." ), -1 ); qCWarning(OkularCoreDebug).nospace() << "No plugin for mimetype '" << mime.name() << "'."; return OpenError; } // 1. load Document OpenResult openResult = d->openDocumentInternal( offer, isstdin, docFile, filedata, password ); if ( openResult == OpenError ) { QVector triedOffers; triedOffers << offer; offer = DocumentPrivate::generatorForMimeType(mime, d->m_widget, triedOffers); while ( offer.isValid() ) { openResult = d->openDocumentInternal( offer, isstdin, docFile, filedata, password ); if ( openResult == OpenError ) { triedOffers << offer; offer = DocumentPrivate::generatorForMimeType(mime, d->m_widget, triedOffers); } else break; } if (openResult == OpenError && !triedMimeFromFileContent ) { QMimeType newmime = db.mimeTypeForFile(docFile, QMimeDatabase::MatchExtension); triedMimeFromFileContent = true; if ( newmime != mime ) { mime = newmime; offer = DocumentPrivate::generatorForMimeType(mime, d->m_widget, triedOffers); while ( offer.isValid() ) { openResult = d->openDocumentInternal( offer, isstdin, docFile, filedata, password ); if ( openResult == OpenError ) { triedOffers << offer; offer = DocumentPrivate::generatorForMimeType(mime, d->m_widget, triedOffers); } else break; } } } if ( openResult == OpenSuccess ) { // Clear errors, since we're trying various generators, maybe one of them errored out // but we finally succeeded // TODO one can still see the error message animating out but since this is a very rare // condition we can leave this for future work emit error( QString(), -1 ); } } if ( openResult != OpenSuccess ) { return openResult; } // no need to check for the existence of a synctex file, no parser will be // created if none exists d->m_synctex_scanner = synctex_scanner_new_with_output_file( QFile::encodeName( docFile ).constData(), nullptr, 1); if ( !d->m_synctex_scanner && QFile::exists(docFile + QLatin1String( "sync" ) ) ) { d->loadSyncFile(docFile); } d->m_generatorName = offer.pluginId(); d->m_pageController = new PageController(); connect( d->m_pageController, SIGNAL(rotationFinished(int,Okular::Page*)), this, SLOT(rotationFinished(int,Okular::Page*)) ); foreach ( Page * p, d->m_pagesVector ) p->d->m_doc = d; d->m_metadataLoadingCompleted = false; d->m_docdataMigrationNeeded = false; // 2. load Additional Data (bookmarks, local annotations and metadata) about the document if ( d->m_archiveData ) { // QTemporaryFile is weird and will return false in exists if fileName wasn't called before d->m_archiveData->metadataFile.fileName(); d->loadDocumentInfo( d->m_archiveData->metadataFile, LoadPageInfo ); d->loadDocumentInfo( LoadGeneralInfo ); } else { if ( d->loadDocumentInfo( LoadPageInfo ) ) d->m_docdataMigrationNeeded = true; d->loadDocumentInfo( LoadGeneralInfo ); } d->m_metadataLoadingCompleted = true; d->m_bookmarkManager->setUrl( d->m_url ); // 3. setup observers inernal lists and data foreachObserver( notifySetup( d->m_pagesVector, DocumentObserver::DocumentChanged | DocumentObserver::UrlChanged ) ); // 4. set initial page (restoring the page saved in xml if loaded) DocumentViewport loadedViewport = (*d->m_viewportIterator); if ( loadedViewport.isValid() ) { (*d->m_viewportIterator) = DocumentViewport(); if ( loadedViewport.pageNumber >= (int)d->m_pagesVector.size() ) loadedViewport.pageNumber = d->m_pagesVector.size() - 1; } else loadedViewport.pageNumber = 0; setViewport( loadedViewport ); // start bookmark saver timer if ( !d->m_saveBookmarksTimer ) { d->m_saveBookmarksTimer = new QTimer( this ); connect( d->m_saveBookmarksTimer, SIGNAL(timeout()), this, SLOT(saveDocumentInfo()) ); } d->m_saveBookmarksTimer->start( 5 * 60 * 1000 ); // start memory check timer if ( !d->m_memCheckTimer ) { d->m_memCheckTimer = new QTimer( this ); connect( d->m_memCheckTimer, SIGNAL(timeout()), this, SLOT(slotTimedMemoryCheck()) ); } d->m_memCheckTimer->start( 2000 ); const DocumentViewport nextViewport = d->nextDocumentViewport(); if ( nextViewport.isValid() ) { setViewport( nextViewport ); d->m_nextDocumentViewport = DocumentViewport(); d->m_nextDocumentDestination = QString(); } AudioPlayer::instance()->d->m_currentDocument = isstdin ? QUrl() : d->m_url; const QStringList docScripts = d->m_generator->metaData( QStringLiteral("DocumentScripts"), QStringLiteral ( "JavaScript" ) ).toStringList(); if ( !docScripts.isEmpty() ) { d->m_scripter = new Scripter( d ); Q_FOREACH ( const QString &docscript, docScripts ) { d->m_scripter->execute( JavaScript, docscript ); } } return OpenSuccess; } bool DocumentPrivate::updateMetadataXmlNameAndDocSize() { // m_docFileName is always local so we can use QFileInfo on it QFileInfo fileReadTest( m_docFileName ); if ( !fileReadTest.isFile() && !fileReadTest.isReadable() ) return false; m_docSize = fileReadTest.size(); // determine the related "xml document-info" filename if ( m_url.isLocalFile() ) { const QString filePath = docDataFileName( m_url, m_docSize ); qCDebug(OkularCoreDebug) << "Metadata file is now:" << filePath; m_xmlFileName = filePath; } else { qCDebug(OkularCoreDebug) << "Metadata file: disabled"; m_xmlFileName = QString(); } return true; } KXMLGUIClient* Document::guiClient() { if ( d->m_generator ) { Okular::GuiInterface * iface = qobject_cast< Okular::GuiInterface * >( d->m_generator ); if ( iface ) return iface->guiClient(); } return nullptr; } void Document::closeDocument() { // check if there's anything to close... if ( !d->m_generator ) return; delete d->m_pageController; d->m_pageController = nullptr; delete d->m_scripter; d->m_scripter = nullptr; // remove requests left in queue d->m_pixmapRequestsMutex.lock(); QLinkedList< PixmapRequest * >::const_iterator sIt = d->m_pixmapRequestsStack.constBegin(); QLinkedList< PixmapRequest * >::const_iterator sEnd = d->m_pixmapRequestsStack.constEnd(); for ( ; sIt != sEnd; ++sIt ) delete *sIt; d->m_pixmapRequestsStack.clear(); d->m_pixmapRequestsMutex.unlock(); QEventLoop loop; bool startEventLoop = false; do { d->m_pixmapRequestsMutex.lock(); startEventLoop = !d->m_executingPixmapRequests.isEmpty(); + + if ( d->m_generator->hasFeature( Generator::SupportsCancelling ) ) + { + for ( PixmapRequest *executingRequest : qAsConst( d->m_executingPixmapRequests ) ) + executingRequest->d->mShouldAbortRender = 1; + + if ( d->m_generator->d_ptr->mTextPageGenerationThread ) + d->m_generator->d_ptr->mTextPageGenerationThread->abortExtraction(); + } + d->m_pixmapRequestsMutex.unlock(); if ( startEventLoop ) { d->m_closingLoop = &loop; loop.exec(); d->m_closingLoop = nullptr; } } while ( startEventLoop ); if ( d->m_fontThread ) { disconnect( d->m_fontThread, nullptr, this, nullptr ); d->m_fontThread->stopExtraction(); d->m_fontThread->wait(); d->m_fontThread = nullptr; } // stop any audio playback AudioPlayer::instance()->stopPlaybacks(); // close the current document and save document info if a document is still opened if ( d->m_generator && d->m_pagesVector.size() > 0 ) { d->saveDocumentInfo(); d->m_generator->closeDocument(); } if ( d->m_synctex_scanner ) { synctex_scanner_free( d->m_synctex_scanner ); d->m_synctex_scanner = nullptr; } // stop timers if ( d->m_memCheckTimer ) d->m_memCheckTimer->stop(); if ( d->m_saveBookmarksTimer ) d->m_saveBookmarksTimer->stop(); if ( d->m_generator ) { // disconnect the generator from this document ... d->m_generator->d_func()->m_document = nullptr; // .. and this document from the generator signals disconnect( d->m_generator, nullptr, this, nullptr ); QHash< QString, GeneratorInfo >::const_iterator genIt = d->m_loadedGenerators.constFind( d->m_generatorName ); Q_ASSERT( genIt != d->m_loadedGenerators.constEnd() ); } d->m_generator = nullptr; d->m_generatorName = QString(); d->m_url = QUrl(); d->m_walletGenerator = nullptr; d->m_docFileName = QString(); d->m_xmlFileName = QString(); delete d->m_tempFile; d->m_tempFile = nullptr; delete d->m_archiveData; d->m_archiveData = nullptr; d->m_docSize = -1; d->m_exportCached = false; d->m_exportFormats.clear(); d->m_exportToText = ExportFormat(); d->m_fontsCached = false; d->m_fontsCache.clear(); d->m_rotation = Rotation0; // send an empty list to observers (to free their data) foreachObserver( notifySetup( QVector< Page * >(), DocumentObserver::DocumentChanged | DocumentObserver::UrlChanged ) ); // delete pages and clear 'd->m_pagesVector' container QVector< Page * >::const_iterator pIt = d->m_pagesVector.constBegin(); QVector< Page * >::const_iterator pEnd = d->m_pagesVector.constEnd(); for ( ; pIt != pEnd; ++pIt ) delete *pIt; d->m_pagesVector.clear(); // clear 'memory allocation' descriptors qDeleteAll( d->m_allocatedPixmaps ); d->m_allocatedPixmaps.clear(); // clear 'running searches' descriptors QMap< int, RunningSearch * >::const_iterator rIt = d->m_searches.constBegin(); QMap< int, RunningSearch * >::const_iterator rEnd = d->m_searches.constEnd(); for ( ; rIt != rEnd; ++rIt ) delete *rIt; d->m_searches.clear(); // clear the visible areas and notify the observers QVector< VisiblePageRect * >::const_iterator vIt = d->m_pageRects.constBegin(); QVector< VisiblePageRect * >::const_iterator vEnd = d->m_pageRects.constEnd(); for ( ; vIt != vEnd; ++vIt ) delete *vIt; d->m_pageRects.clear(); foreachObserver( notifyVisibleRectsChanged() ); // reset internal variables d->m_viewportHistory.clear(); d->m_viewportHistory.append( DocumentViewport() ); d->m_viewportIterator = d->m_viewportHistory.begin(); d->m_allocatedPixmapsTotalMemory = 0; d->m_allocatedTextPagesFifo.clear(); d->m_pageSize = PageSize(); d->m_pageSizes.clear(); d->m_documentInfo = DocumentInfo(); d->m_documentInfoAskedKeys.clear(); AudioPlayer::instance()->d->m_currentDocument = QUrl(); d->m_undoStack->clear(); d->m_docdataMigrationNeeded = false; } void Document::addObserver( DocumentObserver * pObserver ) { Q_ASSERT( !d->m_observers.contains( pObserver ) ); d->m_observers << pObserver; // if the observer is added while a document is already opened, tell it if ( !d->m_pagesVector.isEmpty() ) { pObserver->notifySetup( d->m_pagesVector, DocumentObserver::DocumentChanged | DocumentObserver::UrlChanged ); pObserver->notifyViewportChanged( false /*disables smoothMove*/ ); } } void Document::removeObserver( DocumentObserver * pObserver ) { // remove observer from the map. it won't receive notifications anymore if ( d->m_observers.contains( pObserver ) ) { // free observer's pixmap data QVector::const_iterator it = d->m_pagesVector.constBegin(), end = d->m_pagesVector.constEnd(); for ( ; it != end; ++it ) (*it)->deletePixmap( pObserver ); // [MEM] free observer's allocation descriptors QLinkedList< AllocatedPixmap * >::iterator aIt = d->m_allocatedPixmaps.begin(); QLinkedList< AllocatedPixmap * >::iterator aEnd = d->m_allocatedPixmaps.end(); while ( aIt != aEnd ) { AllocatedPixmap * p = *aIt; if ( p->observer == pObserver ) { aIt = d->m_allocatedPixmaps.erase( aIt ); delete p; } else ++aIt; } // delete observer entry from the map d->m_observers.remove( pObserver ); } } void Document::reparseConfig() { // reparse generator config and if something changed clear Pages bool configchanged = false; if ( d->m_generator ) { Okular::ConfigInterface * iface = qobject_cast< Okular::ConfigInterface * >( d->m_generator ); if ( iface ) configchanged = iface->reparseConfig(); } if ( configchanged ) { // invalidate pixmaps QVector::const_iterator it = d->m_pagesVector.constBegin(), end = d->m_pagesVector.constEnd(); for ( ; it != end; ++it ) { (*it)->deletePixmaps(); } // [MEM] remove allocation descriptors qDeleteAll( d->m_allocatedPixmaps ); d->m_allocatedPixmaps.clear(); d->m_allocatedPixmapsTotalMemory = 0; // send reload signals to observers foreachObserver( notifyContentsCleared( DocumentObserver::Pixmap ) ); } // free memory if in 'low' profile if ( SettingsCore::memoryLevel() == SettingsCore::EnumMemoryLevel::Low && !d->m_allocatedPixmaps.isEmpty() && !d->m_pagesVector.isEmpty() ) d->cleanupPixmapMemory(); } bool Document::isOpened() const { return d->m_generator; } bool Document::canConfigurePrinter( ) const { if ( d->m_generator ) { Okular::PrintInterface * iface = qobject_cast< Okular::PrintInterface * >( d->m_generator ); return iface ? true : false; } else return 0; } DocumentInfo Document::documentInfo() const { QSet keys; for (Okular::DocumentInfo::Key ks = Okular::DocumentInfo::Title; ks < Okular::DocumentInfo::Invalid; ks = Okular::DocumentInfo::Key( ks+1 ) ) { keys << ks; } return documentInfo( keys ); } DocumentInfo Document::documentInfo( const QSet &keys ) const { DocumentInfo result = d->m_documentInfo; const QSet missingKeys = keys - d->m_documentInfoAskedKeys; if ( d->m_generator && !missingKeys.isEmpty() ) { DocumentInfo info = d->m_generator->generateDocumentInfo( missingKeys ); if ( missingKeys.contains( DocumentInfo::FilePath ) ) { info.set( DocumentInfo::FilePath, currentDocument().toDisplayString() ); } if ( d->m_docSize != -1 && missingKeys.contains( DocumentInfo::DocumentSize ) ) { const QString sizeString = KFormat().formatByteSize( d->m_docSize ); info.set( DocumentInfo::DocumentSize, sizeString ); } if ( missingKeys.contains( DocumentInfo::PagesSize ) ) { const QString pagesSize = d->pagesSizeString(); if ( !pagesSize.isEmpty() ) { info.set( DocumentInfo::PagesSize, pagesSize ); } } if ( missingKeys.contains( DocumentInfo::Pages ) && info.get( DocumentInfo::Pages ).isEmpty() ) { info.set( DocumentInfo::Pages, QString::number( this->pages() ) ); } d->m_documentInfo.d->values.unite(info.d->values); d->m_documentInfo.d->titles.unite(info.d->titles); result.d->values.unite(info.d->values); result.d->titles.unite(info.d->titles); } d->m_documentInfoAskedKeys += keys; return result; } const DocumentSynopsis * Document::documentSynopsis() const { return d->m_generator ? d->m_generator->generateDocumentSynopsis() : nullptr; } void Document::startFontReading() { if ( !d->m_generator || !d->m_generator->hasFeature( Generator::FontInfo ) || d->m_fontThread ) return; if ( d->m_fontsCached ) { // in case we have cached fonts, simulate a reading // this way the API is the same, and users no need to care about the // internal caching for ( int i = 0; i < d->m_fontsCache.count(); ++i ) { emit gotFont( d->m_fontsCache.at( i ) ); emit fontReadingProgress( i / pages() ); } emit fontReadingEnded(); return; } d->m_fontThread = new FontExtractionThread( d->m_generator, pages() ); connect( d->m_fontThread, SIGNAL(gotFont(Okular::FontInfo)), this, SLOT(fontReadingGotFont(Okular::FontInfo)) ); connect( d->m_fontThread.data(), SIGNAL(progress(int)), this, SLOT(slotFontReadingProgress(int)) ); d->m_fontThread->startExtraction( /*d->m_generator->hasFeature( Generator::Threaded )*/true ); } void Document::stopFontReading() { if ( !d->m_fontThread ) return; disconnect( d->m_fontThread, nullptr, this, nullptr ); d->m_fontThread->stopExtraction(); d->m_fontThread = nullptr; d->m_fontsCache.clear(); } bool Document::canProvideFontInformation() const { return d->m_generator ? d->m_generator->hasFeature( Generator::FontInfo ) : false; } const QList *Document::embeddedFiles() const { return d->m_generator ? d->m_generator->embeddedFiles() : nullptr; } const Page * Document::page( int n ) const { return ( n < d->m_pagesVector.count() ) ? d->m_pagesVector.at(n) : 0; } const DocumentViewport & Document::viewport() const { return (*d->m_viewportIterator); } const QVector< VisiblePageRect * > & Document::visiblePageRects() const { return d->m_pageRects; } void Document::setVisiblePageRects( const QVector< VisiblePageRect * > & visiblePageRects, DocumentObserver *excludeObserver ) { QVector< VisiblePageRect * >::const_iterator vIt = d->m_pageRects.constBegin(); QVector< VisiblePageRect * >::const_iterator vEnd = d->m_pageRects.constEnd(); for ( ; vIt != vEnd; ++vIt ) delete *vIt; d->m_pageRects = visiblePageRects; // notify change to all other (different from id) observers foreach(DocumentObserver *o, d->m_observers) if ( o != excludeObserver ) o->notifyVisibleRectsChanged(); } uint Document::currentPage() const { return (*d->m_viewportIterator).pageNumber; } uint Document::pages() const { return d->m_pagesVector.size(); } QUrl Document::currentDocument() const { return d->m_url; } bool Document::isAllowed( Permission action ) const { if ( action == Okular::AllowNotes && ( d->m_docdataMigrationNeeded || !d->m_annotationEditingEnabled ) ) return false; if ( action == Okular::AllowFillForms && d->m_docdataMigrationNeeded ) return false; #if !OKULAR_FORCE_DRM if ( KAuthorized::authorize( QStringLiteral("skip_drm") ) && !SettingsCore::obeyDRM() ) return true; #endif return d->m_generator ? d->m_generator->isAllowed( action ) : false; } bool Document::supportsSearching() const { return d->m_generator ? d->m_generator->hasFeature( Generator::TextExtraction ) : false; } bool Document::supportsPageSizes() const { return d->m_generator ? d->m_generator->hasFeature( Generator::PageSizes ) : false; } bool Document::supportsTiles() const { return d->m_generator ? d->m_generator->hasFeature( Generator::TiledRendering ) : false; } PageSize::List Document::pageSizes() const { if ( d->m_generator ) { if ( d->m_pageSizes.isEmpty() ) d->m_pageSizes = d->m_generator->pageSizes(); return d->m_pageSizes; } return PageSize::List(); } bool Document::canExportToText() const { if ( !d->m_generator ) return false; d->cacheExportFormats(); return !d->m_exportToText.isNull(); } bool Document::exportToText( const QString& fileName ) const { if ( !d->m_generator ) return false; d->cacheExportFormats(); if ( d->m_exportToText.isNull() ) return false; return d->m_generator->exportTo( fileName, d->m_exportToText ); } ExportFormat::List Document::exportFormats() const { if ( !d->m_generator ) return ExportFormat::List(); d->cacheExportFormats(); return d->m_exportFormats; } bool Document::exportTo( const QString& fileName, const ExportFormat& format ) const { return d->m_generator ? d->m_generator->exportTo( fileName, format ) : false; } bool Document::historyAtBegin() const { return d->m_viewportIterator == d->m_viewportHistory.begin(); } bool Document::historyAtEnd() const { return d->m_viewportIterator == --(d->m_viewportHistory.end()); } QVariant Document::metaData( const QString & key, const QVariant & option ) const { // if option starts with "src:" assume that we are handling a // source reference if ( key == QLatin1String("NamedViewport") && option.toString().startsWith( QLatin1String("src:"), Qt::CaseInsensitive ) && d->m_synctex_scanner) { const QString reference = option.toString(); // The reference is of form "src:1111Filename", where "1111" // points to line number 1111 in the file "Filename". // Extract the file name and the numeral part from the reference string. // This will fail if Filename starts with a digit. QString name, lineString; // Remove "src:". Presence of substring has been checked before this // function is called. name = reference.mid( 4 ); // split int nameLength = name.length(); int i = 0; for( i = 0; i < nameLength; ++i ) { if ( !name[i].isDigit() ) break; } lineString = name.left( i ); name = name.mid( i ); // Remove spaces. name = name.trimmed(); lineString = lineString.trimmed(); // Convert line to integer. bool ok; int line = lineString.toInt( &ok ); if (!ok) line = -1; // Use column == -1 for now. if( synctex_display_query( d->m_synctex_scanner, QFile::encodeName(name).constData(), line, -1, 0 ) > 0 ) { synctex_node_p node; // For now use the first hit. Could possibly be made smarter // in case there are multiple hits. while( ( node = synctex_scanner_next_result( d->m_synctex_scanner ) ) ) { Okular::DocumentViewport viewport; // TeX pages start at 1. viewport.pageNumber = synctex_node_page( node ) - 1; if ( viewport.pageNumber >= 0 ) { const QSizeF dpi = d->m_generator->dpi(); // TeX small points ... double px = (synctex_node_visible_h( node ) * dpi.width()) / 72.27; double py = (synctex_node_visible_v( node ) * dpi.height()) / 72.27; viewport.rePos.normalizedX = px / page(viewport.pageNumber)->width(); viewport.rePos.normalizedY = ( py + 0.5 ) / page(viewport.pageNumber)->height(); viewport.rePos.enabled = true; viewport.rePos.pos = Okular::DocumentViewport::Center; return viewport.toString(); } } } } return d->m_generator ? d->m_generator->metaData( key, option ) : QVariant(); } Rotation Document::rotation() const { return d->m_rotation; } QSizeF Document::allPagesSize() const { bool allPagesSameSize = true; QSizeF size; for (int i = 0; allPagesSameSize && i < d->m_pagesVector.count(); ++i) { const Page *p = d->m_pagesVector.at(i); if (i == 0) size = QSizeF(p->width(), p->height()); else { allPagesSameSize = (size == QSizeF(p->width(), p->height())); } } if (allPagesSameSize) return size; else return QSizeF(); } QString Document::pageSizeString(int page) const { if (d->m_generator) { if (d->m_generator->pagesSizeMetric() != Generator::None) { const Page *p = d->m_pagesVector.at( page ); return d->localizedSize(QSizeF(p->width(), p->height())); } } return QString(); } +static bool shouldCancelRenderingBecauseOf( const PixmapRequest & executingRequest, const PixmapRequest & otherRequest ) +{ + // New request has higher priority -> cancel + if ( executingRequest.priority() > otherRequest.priority() ) + return true; + + // New request has lower priority -> don't cancel + if ( executingRequest.priority() < otherRequest.priority() ) + return false; + + // New request has same priority and is from a different observer -> don't cancel + // AFAIK this never happens since all observers have different priorities + if ( executingRequest.observer() != otherRequest.observer() ) + return false; + + // Same priority and observer, different page number -> don't cancel + // may still end up cancelled later in the parent caller if none of the requests + // is of the executingRequest page and RemoveAllPrevious is specified + if ( executingRequest.pageNumber() != otherRequest.pageNumber() ) + return false; + + // Same priority, observer, page, different size -> cancel + if ( executingRequest.width() != otherRequest.width() ) + return true; + + // Same priority, observer, page, different size -> cancel + if ( executingRequest.height() != otherRequest.height() ) + return true; + + // Same priority, observer, page, different tiling -> cancel + if ( executingRequest.isTile() != otherRequest.isTile() ) + return true; + + // Same priority, observer, page, different tiling -> cancel + if ( executingRequest.isTile() ) + { + const NormalizedRect bothRequestsRect = executingRequest.normalizedRect() | otherRequest.normalizedRect(); + if ( !( bothRequestsRect == executingRequest.normalizedRect() ) ) + return true; + } + + return false; +} + +bool DocumentPrivate::cancelRenderingBecauseOf( PixmapRequest *executingRequest, PixmapRequest *newRequest ) +{ + // No point in aborting the rendering already finished, let it go through + if ( !executingRequest->d->mResultImage.isNull() ) + return false; + + if ( newRequest && executingRequest->partialUpdatesWanted() ) { + newRequest->setPartialUpdatesWanted( true ); + } + + TilesManager *tm = executingRequest->d->tilesManager(); + if ( tm ) + { + tm->setPixmap( nullptr, executingRequest->normalizedRect(), true /*isPartialPixmap*/ ); + tm->setRequest( NormalizedRect(), 0, 0 ); + } + PagePrivate::PixmapObject object = executingRequest->page()->d->m_pixmaps.take( executingRequest->observer() ); + delete object.m_pixmap; + + if ( executingRequest->d->mShouldAbortRender != 0) + return false; + + executingRequest->d->mShouldAbortRender = 1; + + if ( m_generator->d_ptr->mTextPageGenerationThread && m_generator->d_ptr->mTextPageGenerationThread->page() == executingRequest->page() ) + { + m_generator->d_ptr->mTextPageGenerationThread->abortExtraction(); + } + + return true; +} + void Document::requestPixmaps( const QLinkedList< PixmapRequest * > & requests ) { requestPixmaps( requests, RemoveAllPrevious ); } void Document::requestPixmaps( const QLinkedList< PixmapRequest * > & requests, PixmapRequestFlags reqOptions ) { if ( requests.isEmpty() ) return; if ( !d->m_pageController ) { // delete requests.. QLinkedList< PixmapRequest * >::const_iterator rIt = requests.constBegin(), rEnd = requests.constEnd(); for ( ; rIt != rEnd; ++rIt ) delete *rIt; // ..and return return; } + QSet< DocumentObserver * > observersPixmapCleared; + // 1. [CLEAN STACK] remove previous requests of requesterID - // FIXME This assumes all requests come from the same observer, that is true atm but not enforced anywhere DocumentObserver *requesterObserver = requests.first()->observer(); QSet< int > requestedPages; { QLinkedList< PixmapRequest * >::const_iterator rIt = requests.constBegin(), rEnd = requests.constEnd(); for ( ; rIt != rEnd; ++rIt ) + { + Q_ASSERT( (*rIt)->observer() == requesterObserver ); requestedPages.insert( (*rIt)->pageNumber() ); + } } const bool removeAllPrevious = reqOptions & RemoveAllPrevious; d->m_pixmapRequestsMutex.lock(); QLinkedList< PixmapRequest * >::iterator sIt = d->m_pixmapRequestsStack.begin(), sEnd = d->m_pixmapRequestsStack.end(); while ( sIt != sEnd ) { if ( (*sIt)->observer() == requesterObserver && ( removeAllPrevious || requestedPages.contains( (*sIt)->pageNumber() ) ) ) { // delete request and remove it from stack delete *sIt; sIt = d->m_pixmapRequestsStack.erase( sIt ); } else ++sIt; } - // 2. [ADD TO STACK] add requests to stack - QLinkedList< PixmapRequest * >::const_iterator rIt = requests.constBegin(), rEnd = requests.constEnd(); - for ( ; rIt != rEnd; ++rIt ) + // 1.B [PREPROCESS REQUESTS] tweak some values of the requests + for ( PixmapRequest *request : requests ) { // set the 'page field' (see PixmapRequest) and check if it is valid - PixmapRequest * request = *rIt; qCDebug(OkularCoreDebug).nospace() << "request observer=" << request->observer() << " " <width() << "x" << request->height() << "@" << request->pageNumber(); if ( d->m_pagesVector.value( request->pageNumber() ) == 0 ) { // skip requests referencing an invalid page (must not happen) delete request; continue; } request->d->mPage = d->m_pagesVector.value( request->pageNumber() ); if ( request->isTile() ) { // Change the current request rect so that only invalid tiles are // requested. Also make sure the rect is tile-aligned. NormalizedRect tilesRect; const QList tiles = request->d->tilesManager()->tilesAt( request->normalizedRect(), TilesManager::TerminalTile ); QList::const_iterator tIt = tiles.constBegin(), tEnd = tiles.constEnd(); while ( tIt != tEnd ) { const Tile &tile = *tIt; if ( !tile.isValid() ) { if ( tilesRect.isNull() ) tilesRect = tile.rect(); else tilesRect |= tile.rect(); } tIt++; } request->setNormalizedRect( tilesRect ); } if ( !request->asynchronous() ) request->d->mPriority = 0; + } + + // 1.C [CANCEL REQUESTS] cancel those requests that are running and should be cancelled because of the new requests coming in + if ( d->m_generator->hasFeature( Generator::SupportsCancelling ) ) + { + for ( PixmapRequest *executingRequest : qAsConst( d->m_executingPixmapRequests ) ) + { + bool newRequestsContainExecutingRequestPage = false; + bool requestCancelled = false; + for ( PixmapRequest *newRequest : requests ) + { + if ( newRequest->pageNumber() == executingRequest->pageNumber() && requesterObserver == executingRequest->observer()) + { + newRequestsContainExecutingRequestPage = true; + } + + if ( shouldCancelRenderingBecauseOf( *executingRequest, *newRequest ) ) + { + requestCancelled = d->cancelRenderingBecauseOf( executingRequest, newRequest ); + } + } + + // If we were told to remove all the previous requests and the executing request page is not part of the new requests, cancel it + if ( !requestCancelled && removeAllPrevious && requesterObserver == executingRequest->observer() && !newRequestsContainExecutingRequestPage ) + { + requestCancelled = d->cancelRenderingBecauseOf( executingRequest, nullptr ); + } + + if ( requestCancelled ) + { + observersPixmapCleared << executingRequest->observer(); + } + } + } + // 2. [ADD TO STACK] add requests to stack + for ( PixmapRequest *request : requests ) + { // add request to the 'stack' at the right place if ( !request->priority() ) // add priority zero requests to the top of the stack d->m_pixmapRequestsStack.append( request ); else { // insert in stack sorted by priority sIt = d->m_pixmapRequestsStack.begin(); sEnd = d->m_pixmapRequestsStack.end(); while ( sIt != sEnd && (*sIt)->priority() > request->priority() ) ++sIt; d->m_pixmapRequestsStack.insert( sIt, request ); } } d->m_pixmapRequestsMutex.unlock(); // 3. [START FIRST GENERATION] if generator is ready, start a new generation, // or else (if gen is running) it will be started when the new contents will //come from generator (in requestDone()) // all handling of requests put into sendGeneratorPixmapRequest // if ( generator->canRequestPixmap() ) d->sendGeneratorPixmapRequest(); + + for ( DocumentObserver *o : qAsConst( observersPixmapCleared ) ) + o->notifyContentsCleared( Okular::DocumentObserver::Pixmap ); } void Document::requestTextPage( uint page ) { Page * kp = d->m_pagesVector[ page ]; if ( !d->m_generator || !kp ) return; // Memory management for TextPages d->m_generator->generateTextPage( kp ); } void DocumentPrivate::notifyAnnotationChanges( int page ) { foreachObserverD( notifyPageChanged( page, DocumentObserver::Annotations ) ); } void DocumentPrivate::notifyFormChanges( int /*page*/ ) { } void Document::addPageAnnotation( int page, Annotation * annotation ) { // Transform annotation's base boundary rectangle into unrotated coordinates Page *p = d->m_pagesVector[page]; QTransform t = p->d->rotationMatrix(); annotation->d_ptr->baseTransform(t.inverted()); QUndoCommand *uc = new AddAnnotationCommand(this->d, annotation, page); d->m_undoStack->push(uc); } bool Document::canModifyPageAnnotation( const Annotation * annotation ) const { if ( !annotation || ( annotation->flags() & Annotation::DenyWrite ) ) return false; if ( !isAllowed(Okular::AllowNotes) ) return false; if ( ( annotation->flags() & Annotation::External ) && !d->canModifyExternalAnnotations() ) return false; switch ( annotation->subType() ) { case Annotation::AText: case Annotation::ALine: case Annotation::AGeom: case Annotation::AHighlight: case Annotation::AStamp: case Annotation::AInk: return true; default: return false; } } void Document::prepareToModifyAnnotationProperties( Annotation * annotation ) { Q_ASSERT(d->m_prevPropsOfAnnotBeingModified.isNull()); if (!d->m_prevPropsOfAnnotBeingModified.isNull()) { qCCritical(OkularCoreDebug) << "Error: Document::prepareToModifyAnnotationProperties has already been called since last call to Document::modifyPageAnnotationProperties"; return; } d->m_prevPropsOfAnnotBeingModified = annotation->getAnnotationPropertiesDomNode(); } void Document::modifyPageAnnotationProperties( int page, Annotation * annotation ) { Q_ASSERT(!d->m_prevPropsOfAnnotBeingModified.isNull()); if (d->m_prevPropsOfAnnotBeingModified.isNull()) { qCCritical(OkularCoreDebug) << "Error: Document::prepareToModifyAnnotationProperties must be called before Annotation is modified"; return; } QDomNode prevProps = d->m_prevPropsOfAnnotBeingModified; QUndoCommand *uc = new Okular::ModifyAnnotationPropertiesCommand( d, annotation, page, prevProps, annotation->getAnnotationPropertiesDomNode() ); d->m_undoStack->push( uc ); d->m_prevPropsOfAnnotBeingModified.clear(); } void Document::translatePageAnnotation(int page, Annotation* annotation, const NormalizedPoint & delta ) { int complete = (annotation->flags() & Okular::Annotation::BeingMoved) == 0; QUndoCommand *uc = new Okular::TranslateAnnotationCommand( d, annotation, page, delta, complete ); d->m_undoStack->push(uc); } void Document::adjustPageAnnotation( int page, Annotation *annotation, const Okular::NormalizedPoint & delta1, const Okular::NormalizedPoint & delta2 ) { const bool complete = (annotation->flags() & Okular::Annotation::BeingResized) == 0; QUndoCommand *uc = new Okular::AdjustAnnotationCommand( d, annotation, page, delta1, delta2, complete ); d->m_undoStack->push(uc); } void Document::editPageAnnotationContents( int page, Annotation* annotation, const QString & newContents, int newCursorPos, int prevCursorPos, int prevAnchorPos ) { QString prevContents = annotation->contents(); QUndoCommand *uc = new EditAnnotationContentsCommand( d, annotation, page, newContents, newCursorPos, prevContents, prevCursorPos, prevAnchorPos ); d->m_undoStack->push( uc ); } bool Document::canRemovePageAnnotation( const Annotation * annotation ) const { if ( !annotation || ( annotation->flags() & Annotation::DenyDelete ) ) return false; if ( ( annotation->flags() & Annotation::External ) && !d->canRemoveExternalAnnotations() ) return false; switch ( annotation->subType() ) { case Annotation::AText: case Annotation::ALine: case Annotation::AGeom: case Annotation::AHighlight: case Annotation::AStamp: case Annotation::AInk: case Annotation::ACaret: return true; default: return false; } } void Document::removePageAnnotation( int page, Annotation * annotation ) { QUndoCommand *uc = new RemoveAnnotationCommand(this->d, annotation, page); d->m_undoStack->push(uc); } void Document::removePageAnnotations( int page, const QList &annotations ) { d->m_undoStack->beginMacro(i18nc("remove a collection of annotations from the page", "remove annotations")); foreach(Annotation* annotation, annotations) { QUndoCommand *uc = new RemoveAnnotationCommand(this->d, annotation, page); d->m_undoStack->push(uc); } d->m_undoStack->endMacro(); } bool DocumentPrivate::canAddAnnotationsNatively() const { Okular::SaveInterface * iface = qobject_cast< Okular::SaveInterface * >( m_generator ); if ( iface && iface->supportsOption(Okular::SaveInterface::SaveChanges) && iface->annotationProxy() && iface->annotationProxy()->supports(AnnotationProxy::Addition) ) return true; return false; } bool DocumentPrivate::canModifyExternalAnnotations() const { Okular::SaveInterface * iface = qobject_cast< Okular::SaveInterface * >( m_generator ); if ( iface && iface->supportsOption(Okular::SaveInterface::SaveChanges) && iface->annotationProxy() && iface->annotationProxy()->supports(AnnotationProxy::Modification) ) return true; return false; } bool DocumentPrivate::canRemoveExternalAnnotations() const { Okular::SaveInterface * iface = qobject_cast< Okular::SaveInterface * >( m_generator ); if ( iface && iface->supportsOption(Okular::SaveInterface::SaveChanges) && iface->annotationProxy() && iface->annotationProxy()->supports(AnnotationProxy::Removal) ) return true; return false; } void Document::setPageTextSelection( int page, RegularAreaRect * rect, const QColor & color ) { Page * kp = d->m_pagesVector[ page ]; if ( !d->m_generator || !kp ) return; // add or remove the selection basing whether rect is null or not if ( rect ) kp->d->setTextSelections( rect, color ); else kp->d->deleteTextSelections(); // notify observers about the change foreachObserver( notifyPageChanged( page, DocumentObserver::TextSelection ) ); } bool Document::canUndo() const { return d->m_undoStack->canUndo(); } bool Document::canRedo() const { return d->m_undoStack->canRedo(); } /* REFERENCE IMPLEMENTATION: better calling setViewport from other code void Document::setNextPage() { // advance page and set viewport on observers if ( (*d->m_viewportIterator).pageNumber < (int)d->m_pagesVector.count() - 1 ) setViewport( DocumentViewport( (*d->m_viewportIterator).pageNumber + 1 ) ); } void Document::setPrevPage() { // go to previous page and set viewport on observers if ( (*d->m_viewportIterator).pageNumber > 0 ) setViewport( DocumentViewport( (*d->m_viewportIterator).pageNumber - 1 ) ); } */ void Document::setViewportPage( int page, DocumentObserver *excludeObserver, bool smoothMove ) { // clamp page in range [0 ... numPages-1] if ( page < 0 ) page = 0; else if ( page > (int)d->m_pagesVector.count() ) page = d->m_pagesVector.count() - 1; // make a viewport from the page and broadcast it setViewport( DocumentViewport( page ), excludeObserver, smoothMove ); } void Document::setViewport( const DocumentViewport & viewport, DocumentObserver *excludeObserver, bool smoothMove ) { if ( !viewport.isValid() ) { qCDebug(OkularCoreDebug) << "invalid viewport:" << viewport.toString(); return; } if ( viewport.pageNumber >= int(d->m_pagesVector.count()) ) { //qCDebug(OkularCoreDebug) << "viewport out of document:" << viewport.toString(); return; } // if already broadcasted, don't redo it DocumentViewport & oldViewport = *d->m_viewportIterator; // disabled by enrico on 2005-03-18 (less debug output) //if ( viewport == oldViewport ) // qCDebug(OkularCoreDebug) << "setViewport with the same viewport."; const int oldPageNumber = oldViewport.pageNumber; // set internal viewport taking care of history if ( oldViewport.pageNumber == viewport.pageNumber || !oldViewport.isValid() ) { // if page is unchanged save the viewport at current position in queue oldViewport = viewport; } else { // remove elements after viewportIterator in queue d->m_viewportHistory.erase( ++d->m_viewportIterator, d->m_viewportHistory.end() ); // keep the list to a reasonable size by removing head when needed if ( d->m_viewportHistory.count() >= OKULAR_HISTORY_MAXSTEPS ) d->m_viewportHistory.pop_front(); // add the item at the end of the queue d->m_viewportIterator = d->m_viewportHistory.insert( d->m_viewportHistory.end(), viewport ); } const int currentViewportPage = (*d->m_viewportIterator).pageNumber; const bool currentPageChanged = (oldPageNumber != currentViewportPage); // notify change to all other (different from id) observers foreach(DocumentObserver *o, d->m_observers) { if ( o != excludeObserver ) o->notifyViewportChanged( smoothMove ); if ( currentPageChanged ) o->notifyCurrentPageChanged( oldPageNumber, currentViewportPage ); } } void Document::setZoom(int factor, DocumentObserver *excludeObserver) { // notify change to all other (different from id) observers foreach(DocumentObserver *o, d->m_observers) if (o != excludeObserver) o->notifyZoom( factor ); } void Document::setPrevViewport() // restore viewport from the history { if ( d->m_viewportIterator != d->m_viewportHistory.begin() ) { const int oldViewportPage = (*d->m_viewportIterator).pageNumber; // restore previous viewport and notify it to observers --d->m_viewportIterator; foreachObserver( notifyViewportChanged( true ) ); const int currentViewportPage = (*d->m_viewportIterator).pageNumber; if (oldViewportPage != currentViewportPage) foreachObserver( notifyCurrentPageChanged( oldViewportPage, currentViewportPage ) ); } } void Document::setNextViewport() // restore next viewport from the history { QLinkedList< DocumentViewport >::const_iterator nextIterator = d->m_viewportIterator; ++nextIterator; if ( nextIterator != d->m_viewportHistory.end() ) { const int oldViewportPage = (*d->m_viewportIterator).pageNumber; // restore next viewport and notify it to observers ++d->m_viewportIterator; foreachObserver( notifyViewportChanged( true ) ); const int currentViewportPage = (*d->m_viewportIterator).pageNumber; if (oldViewportPage != currentViewportPage) foreachObserver( notifyCurrentPageChanged( oldViewportPage, currentViewportPage ) ); } } void Document::setNextDocumentViewport( const DocumentViewport & viewport ) { d->m_nextDocumentViewport = viewport; } void Document::setNextDocumentDestination( const QString &namedDestination ) { d->m_nextDocumentDestination = namedDestination; } void Document::searchText( int searchID, const QString & text, bool fromStart, Qt::CaseSensitivity caseSensitivity, SearchType type, bool moveViewport, const QColor & color ) { d->m_searchCancelled = false; // safety checks: don't perform searches on empty or unsearchable docs if ( !d->m_generator || !d->m_generator->hasFeature( Generator::TextExtraction ) || d->m_pagesVector.isEmpty() ) { emit searchFinished( searchID, NoMatchFound ); return; } // if searchID search not recorded, create new descriptor and init params QMap< int, RunningSearch * >::iterator searchIt = d->m_searches.find( searchID ); if ( searchIt == d->m_searches.end() ) { RunningSearch * search = new RunningSearch(); search->continueOnPage = -1; searchIt = d->m_searches.insert( searchID, search ); } RunningSearch * s = *searchIt; // update search structure bool newText = text != s->cachedString; s->cachedString = text; s->cachedType = type; s->cachedCaseSensitivity = caseSensitivity; s->cachedViewportMove = moveViewport; s->cachedColor = color; s->isCurrentlySearching = true; // global data for search QSet< int > *pagesToNotify = new QSet< int >; // remove highlights from pages and queue them for notifying changes *pagesToNotify += s->highlightedPages; foreach(int pageNumber, s->highlightedPages) d->m_pagesVector.at(pageNumber)->d->deleteHighlights( searchID ); s->highlightedPages.clear(); // set hourglass cursor QApplication::setOverrideCursor( Qt::WaitCursor ); // 1. ALLDOC - proces all document marking pages if ( type == AllDocument ) { QMap< Page *, QVector > *pageMatches = new QMap< Page *, QVector >; // search and highlight 'text' (as a solid phrase) on all pages QMetaObject::invokeMethod(this, "doContinueAllDocumentSearch", Qt::QueuedConnection, Q_ARG(void *, pagesToNotify), Q_ARG(void *, pageMatches), Q_ARG(int, 0), Q_ARG(int, searchID)); } // 2. NEXTMATCH - find next matching item (or start from top) // 3. PREVMATCH - find previous matching item (or start from bottom) else if ( type == NextMatch || type == PreviousMatch ) { // find out from where to start/resume search from const bool forward = type == NextMatch; const int viewportPage = (*d->m_viewportIterator).pageNumber; const int fromStartSearchPage = forward ? 0 : d->m_pagesVector.count() - 1; int currentPage = fromStart ? fromStartSearchPage : ((s->continueOnPage != -1) ? s->continueOnPage : viewportPage); Page * lastPage = fromStart ? 0 : d->m_pagesVector[ currentPage ]; int pagesDone = 0; // continue checking last TextPage first (if it is the current page) RegularAreaRect * match = nullptr; if ( lastPage && lastPage->number() == s->continueOnPage ) { if ( newText ) match = lastPage->findText( searchID, text, forward ? FromTop : FromBottom, caseSensitivity ); else match = lastPage->findText( searchID, text, forward ? NextResult : PreviousResult, caseSensitivity, &s->continueOnMatch ); if ( !match ) { if (forward) currentPage++; else currentPage--; pagesDone++; } } s->pagesDone = pagesDone; DoContinueDirectionMatchSearchStruct *searchStruct = new DoContinueDirectionMatchSearchStruct(); searchStruct->pagesToNotify = pagesToNotify; searchStruct->match = match; searchStruct->currentPage = currentPage; searchStruct->searchID = searchID; QMetaObject::invokeMethod(this, "doContinueDirectionMatchSearch", Qt::QueuedConnection, Q_ARG(void *, searchStruct)); } // 4. GOOGLE* - process all document marking pages else if ( type == GoogleAll || type == GoogleAny ) { QMap< Page *, QVector< QPair > > *pageMatches = new QMap< Page *, QVector > >; const QStringList words = text.split( QLatin1Char ( ' ' ), QString::SkipEmptyParts ); // search and highlight every word in 'text' on all pages QMetaObject::invokeMethod(this, "doContinueGooglesDocumentSearch", Qt::QueuedConnection, Q_ARG(void *, pagesToNotify), Q_ARG(void *, pageMatches), Q_ARG(int, 0), Q_ARG(int, searchID), Q_ARG(QStringList, words)); } } void Document::continueSearch( int searchID ) { // check if searchID is present in runningSearches QMap< int, RunningSearch * >::const_iterator it = d->m_searches.constFind( searchID ); if ( it == d->m_searches.constEnd() ) { emit searchFinished( searchID, NoMatchFound ); return; } // start search with cached parameters from last search by searchID RunningSearch * p = *it; if ( !p->isCurrentlySearching ) searchText( searchID, p->cachedString, false, p->cachedCaseSensitivity, p->cachedType, p->cachedViewportMove, p->cachedColor ); } void Document::continueSearch( int searchID, SearchType type ) { // check if searchID is present in runningSearches QMap< int, RunningSearch * >::const_iterator it = d->m_searches.constFind( searchID ); if ( it == d->m_searches.constEnd() ) { emit searchFinished( searchID, NoMatchFound ); return; } // start search with cached parameters from last search by searchID RunningSearch * p = *it; if ( !p->isCurrentlySearching ) searchText( searchID, p->cachedString, false, p->cachedCaseSensitivity, type, p->cachedViewportMove, p->cachedColor ); } void Document::resetSearch( int searchID ) { // if we are closing down, don't bother doing anything if ( !d->m_generator ) return; // check if searchID is present in runningSearches QMap< int, RunningSearch * >::iterator searchIt = d->m_searches.find( searchID ); if ( searchIt == d->m_searches.end() ) return; // get previous parameters for search RunningSearch * s = *searchIt; // unhighlight pages and inform observers about that foreach(int pageNumber, s->highlightedPages) { d->m_pagesVector.at(pageNumber)->d->deleteHighlights( searchID ); foreachObserver( notifyPageChanged( pageNumber, DocumentObserver::Highlights ) ); } // send the setup signal too (to update views that filter on matches) foreachObserver( notifySetup( d->m_pagesVector, 0 ) ); // remove serch from the runningSearches list and delete it d->m_searches.erase( searchIt ); delete s; } void Document::cancelSearch() { d->m_searchCancelled = true; } void Document::undo() { d->m_undoStack->undo(); } void Document::redo() { d->m_undoStack->redo(); } void Document::editFormText( int pageNumber, Okular::FormFieldText* form, const QString & newContents, int newCursorPos, int prevCursorPos, int prevAnchorPos ) { QUndoCommand *uc = new EditFormTextCommand( this->d, form, pageNumber, newContents, newCursorPos, form->text(), prevCursorPos, prevAnchorPos ); d->m_undoStack->push( uc ); d->recalculateForms(); } void Document::editFormList( int pageNumber, FormFieldChoice* form, const QList< int > & newChoices ) { const QList< int > prevChoices = form->currentChoices(); QUndoCommand *uc = new EditFormListCommand( this->d, form, pageNumber, newChoices, prevChoices ); d->m_undoStack->push( uc ); d->recalculateForms(); } void Document::editFormCombo( int pageNumber, FormFieldChoice* form, const QString & newText, int newCursorPos, int prevCursorPos, int prevAnchorPos ) { QString prevText; if ( form->currentChoices().isEmpty() ) { prevText = form->editChoice(); } else { prevText = form->choices()[form->currentChoices().constFirst()]; } QUndoCommand *uc = new EditFormComboCommand( this->d, form, pageNumber, newText, newCursorPos, prevText, prevCursorPos, prevAnchorPos ); d->m_undoStack->push( uc ); d->recalculateForms(); } void Document::editFormButtons( int pageNumber, const QList< FormFieldButton* >& formButtons, const QList< bool >& newButtonStates ) { QUndoCommand *uc = new EditFormButtonsCommand( this->d, pageNumber, formButtons, newButtonStates ); d->m_undoStack->push( uc ); } void Document::reloadDocument() const { const int numOfPages = pages(); for( int i = currentPage(); i >= 0; i -- ) d->refreshPixmaps( i ); for( int i = currentPage() + 1; i < numOfPages; i ++ ) d->refreshPixmaps( i ); } BookmarkManager * Document::bookmarkManager() const { return d->m_bookmarkManager; } QList Document::bookmarkedPageList() const { QList list; uint docPages = pages(); //pages are 0-indexed internally, but 1-indexed externally for ( uint i = 0; i < docPages; i++ ) { if ( bookmarkManager()->isBookmarked( i ) ) { list << i + 1; } } return list; } QString Document::bookmarkedPageRange() const { // Code formerly in Part::slotPrint() // range detecting QString range; uint docPages = pages(); int startId = -1; int endId = -1; for ( uint i = 0; i < docPages; ++i ) { if ( bookmarkManager()->isBookmarked( i ) ) { if ( startId < 0 ) startId = i; if ( endId < 0 ) endId = startId; else ++endId; } else if ( startId >= 0 && endId >= 0 ) { if ( !range.isEmpty() ) range += QLatin1Char ( ',' ); if ( endId - startId > 0 ) range += QStringLiteral( "%1-%2" ).arg( startId + 1 ).arg( endId + 1 ); else range += QString::number( startId + 1 ); startId = -1; endId = -1; } } if ( startId >= 0 && endId >= 0 ) { if ( !range.isEmpty() ) range += QLatin1Char ( ',' ); if ( endId - startId > 0 ) range += QStringLiteral( "%1-%2" ).arg( startId + 1 ).arg( endId + 1 ); else range += QString::number( startId + 1 ); } return range; } void Document::processAction( const Action * action ) { if ( !action ) return; switch( action->actionType() ) { case Action::Goto: { const GotoAction * go = static_cast< const GotoAction * >( action ); d->m_nextDocumentViewport = go->destViewport(); d->m_nextDocumentDestination = go->destinationName(); // Explanation of why d->m_nextDocumentViewport is needed: // all openRelativeFile does is launch a signal telling we // want to open another URL, the problem is that when the file is // non local, the loading is done assynchronously so you can't // do a setViewport after the if as it was because you are doing the setViewport // on the old file and when the new arrives there is no setViewport for it and // it does not show anything // first open filename if link is pointing outside this document if ( go->isExternal() && !d->openRelativeFile( go->fileName() ) ) { qCWarning(OkularCoreDebug).nospace() << "Action: Error opening '" << go->fileName() << "'."; return; } else { const DocumentViewport nextViewport = d->nextDocumentViewport(); // skip local links that point to nowhere (broken ones) if ( !nextViewport.isValid() ) return; setViewport( nextViewport, nullptr, true ); d->m_nextDocumentViewport = DocumentViewport(); d->m_nextDocumentDestination = QString(); } } break; case Action::Execute: { const ExecuteAction * exe = static_cast< const ExecuteAction * >( action ); const QString fileName = exe->fileName(); if ( fileName.endsWith( QLatin1String(".pdf"), Qt::CaseInsensitive ) ) { d->openRelativeFile( fileName ); return; } // Albert: the only pdf i have that has that kind of link don't define // an application and use the fileName as the file to open QUrl url = d->giveAbsoluteUrl( fileName ); QMimeDatabase db; QMimeType mime = db.mimeTypeForUrl( url ); // Check executables if ( KRun::isExecutableFile( url, mime.name() ) ) { // Don't have any pdf that uses this code path, just a guess on how it should work if ( !exe->parameters().isEmpty() ) { url = d->giveAbsoluteUrl( exe->parameters() ); mime = db.mimeTypeForUrl( url ); if ( KRun::isExecutableFile( url, mime.name() ) ) { // this case is a link pointing to an executable with a parameter // that also is an executable, possibly a hand-crafted pdf KMessageBox::information( d->m_widget, i18n("The document is trying to execute an external application and, for your safety, Okular does not allow that.") ); return; } } else { // this case is a link pointing to an executable with no parameters // core developers find unacceptable executing it even after asking the user KMessageBox::information( d->m_widget, i18n("The document is trying to execute an external application and, for your safety, Okular does not allow that.") ); return; } } KService::Ptr ptr = KMimeTypeTrader::self()->preferredService( mime.name(), QStringLiteral("Application") ); if ( ptr ) { QList lst; lst.append( url ); KRun::runService( *ptr, lst, nullptr ); } else KMessageBox::information( d->m_widget, i18n( "No application found for opening file of mimetype %1.", mime.name() ) ); } break; case Action::DocAction: { const DocumentAction * docaction = static_cast< const DocumentAction * >( action ); switch( docaction->documentActionType() ) { case DocumentAction::PageFirst: setViewportPage( 0 ); break; case DocumentAction::PagePrev: if ( (*d->m_viewportIterator).pageNumber > 0 ) setViewportPage( (*d->m_viewportIterator).pageNumber - 1 ); break; case DocumentAction::PageNext: if ( (*d->m_viewportIterator).pageNumber < (int)d->m_pagesVector.count() - 1 ) setViewportPage( (*d->m_viewportIterator).pageNumber + 1 ); break; case DocumentAction::PageLast: setViewportPage( d->m_pagesVector.count() - 1 ); break; case DocumentAction::HistoryBack: setPrevViewport(); break; case DocumentAction::HistoryForward: setNextViewport(); break; case DocumentAction::Quit: emit quit(); break; case DocumentAction::Presentation: emit linkPresentation(); break; case DocumentAction::EndPresentation: emit linkEndPresentation(); break; case DocumentAction::Find: emit linkFind(); break; case DocumentAction::GoToPage: emit linkGoToPage(); break; case DocumentAction::Close: emit close(); break; } } break; case Action::Browse: { const BrowseAction * browse = static_cast< const BrowseAction * >( action ); QString lilySource; int lilyRow = 0, lilyCol = 0; // if the url is a mailto one, invoke mailer if ( browse->url().scheme() == QLatin1String("mailto") ) { QDesktopServices::openUrl( browse->url() ); } else if ( extractLilyPondSourceReference( browse->url(), &lilySource, &lilyRow, &lilyCol ) ) { const SourceReference ref( lilySource, lilyRow, lilyCol ); processSourceReference( &ref ); } else { const QUrl url = browse->url(); // fix for #100366, documents with relative links that are the form of http:foo.pdf if ((url.scheme() == "http") && url.host().isEmpty() && url.fileName().endsWith("pdf")) { d->openRelativeFile(url.fileName()); return; } // handle documents with relative path if ( d->m_url.isValid() ) { const QUrl realUrl = KIO::upUrl(d->m_url).resolved(url); // KRun autodeletes new KRun( realUrl, d->m_widget ); } } } break; case Action::Sound: { const SoundAction * linksound = static_cast< const SoundAction * >( action ); AudioPlayer::instance()->playSound( linksound->sound(), linksound ); } break; case Action::Script: { const ScriptAction * linkscript = static_cast< const ScriptAction * >( action ); if ( !d->m_scripter ) d->m_scripter = new Scripter( d ); d->m_scripter->execute( linkscript->scriptType(), linkscript->script() ); } break; case Action::Movie: emit processMovieAction( static_cast< const MovieAction * >( action ) ); break; case Action::Rendition: { const RenditionAction * linkrendition = static_cast< const RenditionAction * >( action ); if ( !linkrendition->script().isEmpty() ) { if ( !d->m_scripter ) d->m_scripter = new Scripter( d ); d->m_scripter->execute( linkrendition->scriptType(), linkrendition->script() ); } emit processRenditionAction( static_cast< const RenditionAction * >( action ) ); } break; case Action::BackendOpaque: { d->m_generator->opaqueAction( static_cast< const BackendOpaqueAction * >( action ) ); } break; } } void Document::processSourceReference( const SourceReference * ref ) { if ( !ref ) return; const QUrl url = d->giveAbsoluteUrl( ref->fileName() ); if ( !url.isLocalFile() ) { qCDebug(OkularCoreDebug) << url.url() << "is not a local file."; return; } const QString absFileName = url.toLocalFile(); if ( !QFile::exists( absFileName ) ) { qCDebug(OkularCoreDebug) << "No such file:" << absFileName; return; } bool handled = false; emit sourceReferenceActivated(absFileName, ref->row(), ref->column(), &handled); if(handled) { return; } static QHash< int, QString > editors; // init the editors table if empty (on first run, usually) if ( editors.isEmpty() ) { editors = buildEditorsMap(); } QHash< int, QString >::const_iterator it = editors.constFind( SettingsCore::externalEditor() ); QString p; if ( it != editors.constEnd() ) p = *it; else p = SettingsCore::externalEditorCommand(); // custom editor not yet configured if ( p.isEmpty() ) return; // manually append the %f placeholder if not specified if ( p.indexOf( QLatin1String( "%f" ) ) == -1 ) p.append( QLatin1String( " %f" ) ); // replacing the placeholders QHash< QChar, QString > map; map.insert( QLatin1Char ( 'f' ), absFileName ); map.insert( QLatin1Char ( 'c' ), QString::number( ref->column() ) ); map.insert( QLatin1Char ( 'l' ), QString::number( ref->row() ) ); const QString cmd = KMacroExpander::expandMacrosShellQuote( p, map ); if ( cmd.isEmpty() ) return; const QStringList args = KShell::splitArgs( cmd ); if ( args.isEmpty() ) return; KProcess::startDetached( args ); } const SourceReference * Document::dynamicSourceReference( int pageNr, double absX, double absY ) { if ( !d->m_synctex_scanner ) return nullptr; const QSizeF dpi = d->m_generator->dpi(); if (synctex_edit_query(d->m_synctex_scanner, pageNr + 1, absX * 72. / dpi.width(), absY * 72. / dpi.height()) > 0) { synctex_node_p node; // TODO what should we do if there is really more than one node? while (( node = synctex_scanner_next_result( d->m_synctex_scanner ) )) { int line = synctex_node_line(node); int col = synctex_node_column(node); // column extraction does not seem to be implemented in synctex so far. set the SourceReference default value. if ( col == -1 ) { col = 0; } const char *name = synctex_scanner_get_name( d->m_synctex_scanner, synctex_node_tag( node ) ); return new Okular::SourceReference( QFile::decodeName( name ), line, col ); } } return nullptr; } Document::PrintingType Document::printingSupport() const { if ( d->m_generator ) { if ( d->m_generator->hasFeature( Generator::PrintNative ) ) { return NativePrinting; } #ifndef Q_OS_WIN if ( d->m_generator->hasFeature( Generator::PrintPostscript ) ) { return PostscriptPrinting; } #endif } return NoPrinting; } bool Document::supportsPrintToFile() const { return d->m_generator ? d->m_generator->hasFeature( Generator::PrintToFile ) : false; } bool Document::print( QPrinter &printer ) { return d->m_generator ? d->m_generator->print( printer ) : false; } QString Document::printError() const { Okular::Generator::PrintError err = Generator::UnknownPrintError; if ( d->m_generator ) { QMetaObject::invokeMethod( d->m_generator, "printError", Qt::DirectConnection, Q_RETURN_ARG(Okular::Generator::PrintError, err) ); } Q_ASSERT( err != Generator::NoPrintError ); switch ( err ) { case Generator::TemporaryFileOpenPrintError: return i18n( "Could not open a temporary file" ); case Generator::FileConversionPrintError: return i18n( "Print conversion failed" ); case Generator::PrintingProcessCrashPrintError: return i18n( "Printing process crashed" ); case Generator::PrintingProcessStartPrintError: return i18n( "Printing process could not start" ); case Generator::PrintToFilePrintError: return i18n( "Printing to file failed" ); case Generator::InvalidPrinterStatePrintError: return i18n( "Printer was in invalid state" ); case Generator::UnableToFindFilePrintError: return i18n( "Unable to find file to print" ); case Generator::NoFileToPrintError: return i18n( "There was no file to print" ); case Generator::NoBinaryToPrintError: return i18n( "Could not find a suitable binary for printing. Make sure CUPS lpr binary is available" ); case Generator::InvalidPageSizePrintError: return i18n( "The page print size is invalid" ); case Generator::NoPrintError: return QString(); case Generator::UnknownPrintError: return QString(); } return QString(); } QWidget* Document::printConfigurationWidget() const { if ( d->m_generator ) { PrintInterface * iface = qobject_cast< Okular::PrintInterface * >( d->m_generator ); return iface ? iface->printConfigurationWidget() : nullptr; } else return nullptr; } void Document::fillConfigDialog( KConfigDialog * dialog ) { if ( !dialog ) return; // ensure that we have all the generators with settings loaded QVector offers = DocumentPrivate::configurableGenerators(); d->loadServiceList( offers ); bool pagesAdded = false; QHash< QString, GeneratorInfo >::iterator it = d->m_loadedGenerators.begin(); QHash< QString, GeneratorInfo >::iterator itEnd = d->m_loadedGenerators.end(); for ( ; it != itEnd; ++it ) { Okular::ConfigInterface * iface = d->generatorConfig( it.value() ); if ( iface ) { iface->addPages( dialog ); pagesAdded = true; } } if ( pagesAdded ) { connect( dialog, SIGNAL(settingsChanged(QString)), this, SLOT(slotGeneratorConfigChanged(QString)) ); } } QVector DocumentPrivate::configurableGenerators() { const QVector available = availableGenerators(); QVector result; for (const KPluginMetaData& md : available) { if (md.rawData()[QStringLiteral("X-KDE-okularHasInternalSettings")].toBool()) { result << md; } } return result; } KPluginMetaData Document::generatorInfo() const { if (!d->m_generator) return KPluginMetaData(); auto genIt = d->m_loadedGenerators.constFind(d->m_generatorName); Q_ASSERT(genIt != d->m_loadedGenerators.constEnd()); return genIt.value().metadata; } int Document::configurableGenerators() const { return DocumentPrivate::configurableGenerators().size(); } QStringList Document::supportedMimeTypes() const { // TODO: make it a static member of DocumentPrivate? QStringList result = d->m_supportedMimeTypes; if (result.isEmpty()) { const QVector available = DocumentPrivate::availableGenerators(); for (const KPluginMetaData& md : available) { result << md.mimeTypes(); } // Remove duplicate mimetypes represented by different names QMimeDatabase mimeDatabase; QSet uniqueMimetypes; for (const QString &mimeName : result) { uniqueMimetypes.insert(mimeDatabase.mimeTypeForName(mimeName)); } result.clear(); for (const QMimeType &mimeType : uniqueMimetypes) { result.append(mimeType.name()); } // Add the Okular archive mimetype result << QStringLiteral("application/vnd.kde.okular-archive"); // Sorting by mimetype name doesn't make a ton of sense, // but ensures that the list is ordered the same way every time qSort(result); d->m_supportedMimeTypes = result; } return result; } bool Document::canSwapBackingFile() const { if ( !d->m_generator ) return false; return d->m_generator->hasFeature( Generator::SwapBackingFile ); } bool Document::swapBackingFile( const QString &newFileName, const QUrl &url ) { if ( !d->m_generator ) return false; if ( !d->m_generator->hasFeature( Generator::SwapBackingFile ) ) return false; // Save metadata about the file we're about to close d->saveDocumentInfo(); qCDebug(OkularCoreDebug) << "Swapping backing file to" << newFileName; QVector< Page * > newPagesVector; Generator::SwapBackingFileResult result = d->m_generator->swapBackingFile( newFileName, newPagesVector ); if (result != Generator::SwapBackingFileError) { QLinkedList< ObjectRect* > rectsToDelete; QLinkedList< Annotation* > annotationsToDelete; QSet< PagePrivate* > pagePrivatesToDelete; if (result == Generator::SwapBackingFileReloadInternalData) { // Here we need to replace everything that the old generator // had created with what the new one has without making it look like // we have actually closed and opened the file again // Simple sanity check if (newPagesVector.count() != d->m_pagesVector.count()) return false; // Update the undo stack contents for (int i = 0; i < d->m_undoStack->count(); ++i) { // Trust me on the const_cast ^_^ QUndoCommand *uc = const_cast( d->m_undoStack->command( i ) ); if (OkularUndoCommand *ouc = dynamic_cast( uc )) { const bool success = ouc->refreshInternalPageReferences( newPagesVector ); if ( !success ) { qWarning() << "Document::swapBackingFile: refreshInternalPageReferences failed" << ouc; return false; } } else { qWarning() << "Document::swapBackingFile: Unhandled undo command" << uc; return false; } } for (int i = 0; i < d->m_pagesVector.count(); ++i) { // switch the PagePrivate* from newPage to oldPage // this way everyone still holding Page* doesn't get // disturbed by it Page *oldPage = d->m_pagesVector[i]; Page *newPage = newPagesVector[i]; newPage->d->adoptGeneratedContents(oldPage->d); pagePrivatesToDelete << oldPage->d; oldPage->d = newPage->d; oldPage->d->m_page = oldPage; oldPage->d->m_doc = d; newPage->d = nullptr; annotationsToDelete << oldPage->m_annotations; rectsToDelete << oldPage->m_rects; oldPage->m_annotations = newPage->m_annotations; oldPage->m_rects = newPage->m_rects; } qDeleteAll( newPagesVector ); } d->m_url = url; d->m_docFileName = newFileName; d->updateMetadataXmlNameAndDocSize(); d->m_bookmarkManager->setUrl( d->m_url ); if ( d->m_synctex_scanner ) { synctex_scanner_free( d->m_synctex_scanner ); d->m_synctex_scanner = synctex_scanner_new_with_output_file( QFile::encodeName( newFileName ).constData(), nullptr, 1); if ( !d->m_synctex_scanner && QFile::exists(newFileName + QLatin1String( "sync" ) ) ) { d->loadSyncFile(newFileName); } } foreachObserver( notifySetup( d->m_pagesVector, DocumentObserver::UrlChanged ) ); qDeleteAll( annotationsToDelete ); qDeleteAll( rectsToDelete ); qDeleteAll( pagePrivatesToDelete ); return true; } else { return false; } } bool Document::swapBackingFileArchive( const QString &newFileName, const QUrl &url ) { qCDebug(OkularCoreDebug) << "Swapping backing archive to" << newFileName; ArchiveData *newArchive = DocumentPrivate::unpackDocumentArchive( newFileName ); if ( !newArchive ) return false; const QString tempFileName = newArchive->document.fileName(); const bool success = swapBackingFile( tempFileName, url ); if ( success ) { delete d->m_archiveData; d->m_archiveData = newArchive; } return success; } void Document::setHistoryClean( bool clean ) { if ( clean ) d->m_undoStack->setClean(); // Since we only use the resetClean // in some cases and we're past the dependency freeze // if you happen to compile with an old Qt you will miss // some extra nicety when saving an okular file with annotations to png file // it's quite corner case compared to how important the whole save feature // is so you'll have to live without it #if QT_VERSION > QT_VERSION_CHECK(5, 8, 0) else d->m_undoStack->resetClean(); #endif } bool Document::canSaveChanges() const { if ( !d->m_generator ) return false; Q_ASSERT( !d->m_generatorName.isEmpty() ); QHash< QString, GeneratorInfo >::iterator genIt = d->m_loadedGenerators.find( d->m_generatorName ); Q_ASSERT( genIt != d->m_loadedGenerators.end() ); SaveInterface* saveIface = d->generatorSave( genIt.value() ); if ( !saveIface ) return false; return saveIface->supportsOption( SaveInterface::SaveChanges ); } bool Document::canSaveChanges( SaveCapability cap ) const { switch ( cap ) { case SaveFormsCapability: /* Assume that if the generator supports saving, forms can be saved. * We have no means to actually query the generator at the moment * TODO: Add some method to query the generator in SaveInterface */ return canSaveChanges(); case SaveAnnotationsCapability: return d->canAddAnnotationsNatively(); } return false; } bool Document::saveChanges( const QString &fileName ) { QString errorText; return saveChanges( fileName, &errorText ); } bool Document::saveChanges( const QString &fileName, QString *errorText ) { if ( !d->m_generator || fileName.isEmpty() ) return false; Q_ASSERT( !d->m_generatorName.isEmpty() ); QHash< QString, GeneratorInfo >::iterator genIt = d->m_loadedGenerators.find( d->m_generatorName ); Q_ASSERT( genIt != d->m_loadedGenerators.end() ); SaveInterface* saveIface = d->generatorSave( genIt.value() ); if ( !saveIface || !saveIface->supportsOption( SaveInterface::SaveChanges ) ) return false; return saveIface->save( fileName, SaveInterface::SaveChanges, errorText ); } void Document::registerView( View *view ) { if ( !view ) return; Document *viewDoc = view->viewDocument(); if ( viewDoc ) { // check if already registered for this document if ( viewDoc == this ) return; viewDoc->unregisterView( view ); } d->m_views.insert( view ); view->d_func()->document = d; } void Document::unregisterView( View *view ) { if ( !view ) return; Document *viewDoc = view->viewDocument(); if ( !viewDoc || viewDoc != this ) return; view->d_func()->document = nullptr; d->m_views.remove( view ); } QByteArray Document::fontData(const FontInfo &font) const { QByteArray result; if (d->m_generator) { QMetaObject::invokeMethod(d->m_generator, "requestFontData", Qt::DirectConnection, Q_ARG(Okular::FontInfo, font), Q_ARG(QByteArray *, &result)); } return result; } ArchiveData *DocumentPrivate::unpackDocumentArchive( const QString &archivePath ) { QMimeDatabase db; const QMimeType mime = db.mimeTypeForFile( archivePath, QMimeDatabase::MatchExtension ); if ( !mime.inherits( QStringLiteral("application/vnd.kde.okular-archive") ) ) return nullptr; KZip okularArchive( archivePath ); if ( !okularArchive.open( QIODevice::ReadOnly ) ) return nullptr; const KArchiveDirectory * mainDir = okularArchive.directory(); const KArchiveEntry * mainEntry = mainDir->entry( QStringLiteral("content.xml") ); if ( !mainEntry || !mainEntry->isFile() ) return nullptr; std::unique_ptr< QIODevice > mainEntryDevice( static_cast< const KZipFileEntry * >( mainEntry )->createDevice() ); QDomDocument doc; if ( !doc.setContent( mainEntryDevice.get() ) ) return nullptr; mainEntryDevice.reset(); QDomElement root = doc.documentElement(); if ( root.tagName() != QLatin1String("OkularArchive") ) return nullptr; QString documentFileName; QString metadataFileName; QDomElement el = root.firstChild().toElement(); for ( ; !el.isNull(); el = el.nextSibling().toElement() ) { if ( el.tagName() == QLatin1String("Files") ) { QDomElement fileEl = el.firstChild().toElement(); for ( ; !fileEl.isNull(); fileEl = fileEl.nextSibling().toElement() ) { if ( fileEl.tagName() == QLatin1String("DocumentFileName") ) documentFileName = fileEl.text(); else if ( fileEl.tagName() == QLatin1String("MetadataFileName") ) metadataFileName = fileEl.text(); } } } if ( documentFileName.isEmpty() ) return nullptr; const KArchiveEntry * docEntry = mainDir->entry( documentFileName ); if ( !docEntry || !docEntry->isFile() ) return nullptr; std::unique_ptr< ArchiveData > archiveData( new ArchiveData() ); const int dotPos = documentFileName.indexOf( QLatin1Char('.') ); if ( dotPos != -1 ) archiveData->document.setFileTemplate(QDir::tempPath() + QLatin1String("/okular_XXXXXX") + documentFileName.mid(dotPos)); if ( !archiveData->document.open() ) return nullptr; archiveData->originalFileName = documentFileName; { std::unique_ptr< QIODevice > docEntryDevice( static_cast< const KZipFileEntry * >( docEntry )->createDevice() ); copyQIODevice( docEntryDevice.get(), &archiveData->document ); archiveData->document.close(); } const KArchiveEntry * metadataEntry = mainDir->entry( metadataFileName ); if ( metadataEntry && metadataEntry->isFile() ) { std::unique_ptr< QIODevice > metadataEntryDevice( static_cast< const KZipFileEntry * >( metadataEntry )->createDevice() ); archiveData->metadataFile.setFileTemplate(QDir::tempPath() + QLatin1String("/okular_XXXXXX.xml")); if ( archiveData->metadataFile.open() ) { copyQIODevice( metadataEntryDevice.get(), &archiveData->metadataFile ); archiveData->metadataFile.close(); } } return archiveData.release(); } Document::OpenResult Document::openDocumentArchive( const QString & docFile, const QUrl & url, const QString & password ) { d->m_archiveData = DocumentPrivate::unpackDocumentArchive( docFile ); if ( !d->m_archiveData ) return OpenError; const QString tempFileName = d->m_archiveData->document.fileName(); QMimeDatabase db; const QMimeType docMime = db.mimeTypeForFile( tempFileName, QMimeDatabase::MatchContent ); const OpenResult ret = openDocument( tempFileName, url, docMime, password ); if ( ret != OpenSuccess ) { delete d->m_archiveData; d->m_archiveData = nullptr; } return ret; } bool Document::saveDocumentArchive( const QString &fileName ) { if ( !d->m_generator ) return false; /* If we opened an archive, use the name of original file (eg foo.pdf) * instead of the archive's one (eg foo.okular) */ QString docFileName = d->m_archiveData ? d->m_archiveData->originalFileName : d->m_url.fileName(); if ( docFileName == QLatin1String( "-" ) ) return false; QString docPath = d->m_docFileName; const QFileInfo fi( docPath ); if ( fi.isSymLink() ) docPath = fi.symLinkTarget(); KZip okularArchive( fileName ); if ( !okularArchive.open( QIODevice::WriteOnly ) ) return false; const KUser user; #ifndef Q_OS_WIN const KUserGroup userGroup( user.groupId() ); #else const KUserGroup userGroup( QString( "" ) ); #endif QDomDocument contentDoc( QStringLiteral("OkularArchive") ); QDomProcessingInstruction xmlPi = contentDoc.createProcessingInstruction( QStringLiteral( "xml" ), QStringLiteral( "version=\"1.0\" encoding=\"utf-8\"" ) ); contentDoc.appendChild( xmlPi ); QDomElement root = contentDoc.createElement( QStringLiteral("OkularArchive") ); contentDoc.appendChild( root ); QDomElement filesNode = contentDoc.createElement( QStringLiteral("Files") ); root.appendChild( filesNode ); QDomElement fileNameNode = contentDoc.createElement( QStringLiteral("DocumentFileName") ); filesNode.appendChild( fileNameNode ); fileNameNode.appendChild( contentDoc.createTextNode( docFileName ) ); QDomElement metadataFileNameNode = contentDoc.createElement( QStringLiteral("MetadataFileName") ); filesNode.appendChild( metadataFileNameNode ); metadataFileNameNode.appendChild( contentDoc.createTextNode( QStringLiteral("metadata.xml") ) ); // If the generator can save annotations natively, do it QTemporaryFile modifiedFile; bool annotationsSavedNatively = false; bool formsSavedNatively = false; if ( d->canAddAnnotationsNatively() || canSaveChanges( SaveFormsCapability ) ) { if ( !modifiedFile.open() ) return false; const QString modifiedFileName = modifiedFile.fileName(); modifiedFile.close(); // We're only interested in the file name QString errorText; if ( saveChanges( modifiedFileName, &errorText ) ) { docPath = modifiedFileName; // Save this instead of the original file annotationsSavedNatively = d->canAddAnnotationsNatively(); formsSavedNatively = canSaveChanges( SaveFormsCapability ); } else { qCWarning(OkularCoreDebug) << "saveChanges failed: " << errorText; qCDebug(OkularCoreDebug) << "Falling back to saving a copy of the original file"; } } PageItems saveWhat = None; if ( !annotationsSavedNatively ) saveWhat |= AnnotationPageItems; if ( !formsSavedNatively ) saveWhat |= FormFieldPageItems; QTemporaryFile metadataFile; if ( !d->savePageDocumentInfo( &metadataFile, saveWhat ) ) return false; const QByteArray contentDocXml = contentDoc.toByteArray(); const mode_t perm = 0100644; okularArchive.writeFile( QStringLiteral("content.xml"), contentDocXml, perm, user.loginName(), userGroup.name() ); okularArchive.addLocalFile( docPath, docFileName ); okularArchive.addLocalFile( metadataFile.fileName(), QStringLiteral("metadata.xml") ); if ( !okularArchive.close() ) return false; return true; } bool Document::extractArchivedFile( const QString &destFileName ) { if ( !d->m_archiveData ) return false; // Remove existing file, if present (QFile::copy doesn't overwrite by itself) QFile::remove( destFileName ); return d->m_archiveData->document.copy( destFileName ); } QPrinter::Orientation Document::orientation() const { double width, height; int landscape, portrait; const Okular::Page *currentPage; // if some pages are landscape and others are not, the most common wins, as // QPrinter does not accept a per-page setting landscape = 0; portrait = 0; for (uint i = 0; i < pages(); i++) { currentPage = page(i); width = currentPage->width(); height = currentPage->height(); if (currentPage->orientation() == Okular::Rotation90 || currentPage->orientation() == Okular::Rotation270) qSwap(width, height); if (width > height) landscape++; else portrait++; } return (landscape > portrait) ? QPrinter::Landscape : QPrinter::Portrait; } void Document::setAnnotationEditingEnabled( bool enable ) { d->m_annotationEditingEnabled = enable; foreachObserver( notifySetup( d->m_pagesVector, 0 ) ); } void Document::walletDataForFile( const QString &fileName, QString *walletName, QString *walletFolder, QString *walletKey ) const { if (d->m_generator) { d->m_generator->walletDataForFile( fileName, walletName, walletFolder, walletKey ); } else if (d->m_walletGenerator) { d->m_walletGenerator->walletDataForFile( fileName, walletName, walletFolder, walletKey ); } } bool Document::isDocdataMigrationNeeded() const { return d->m_docdataMigrationNeeded; } void Document::docdataMigrationDone() { if (d->m_docdataMigrationNeeded) { d->m_docdataMigrationNeeded = false; foreachObserver( notifySetup( d->m_pagesVector, 0 ) ); } } QAbstractItemModel * Document::layersModel() const { return d->m_generator ? d->m_generator->layersModel() : nullptr; } void DocumentPrivate::requestDone( PixmapRequest * req ) { if ( !req ) return; if ( !m_generator || m_closingLoop ) { m_pixmapRequestsMutex.lock(); m_executingPixmapRequests.removeAll( req ); m_pixmapRequestsMutex.unlock(); delete req; if ( m_closingLoop ) m_closingLoop->exit(); return; } #ifndef NDEBUG if ( !m_generator->canGeneratePixmap() ) qCDebug(OkularCoreDebug) << "requestDone with generator not in READY state."; #endif - // [MEM] 1.1 find and remove a previous entry for the same page and id - QLinkedList< AllocatedPixmap * >::iterator aIt = m_allocatedPixmaps.begin(); - QLinkedList< AllocatedPixmap * >::iterator aEnd = m_allocatedPixmaps.end(); - for ( ; aIt != aEnd; ++aIt ) - if ( (*aIt)->page == req->pageNumber() && (*aIt)->observer == req->observer() ) - { - AllocatedPixmap * p = *aIt; - m_allocatedPixmaps.erase( aIt ); - m_allocatedPixmapsTotalMemory -= p->memory; - delete p; - break; - } - - DocumentObserver *observer = req->observer(); - if ( m_observers.contains(observer) ) + if ( !req->shouldAbortRender() ) { - // [MEM] 1.2 append memory allocation descriptor to the FIFO - qulonglong memoryBytes = 0; - const TilesManager *tm = req->d->tilesManager(); - if ( tm ) - memoryBytes = tm->totalMemory(); - else - memoryBytes = 4 * req->width() * req->height(); + // [MEM] 1.1 find and remove a previous entry for the same page and id + QLinkedList< AllocatedPixmap * >::iterator aIt = m_allocatedPixmaps.begin(); + QLinkedList< AllocatedPixmap * >::iterator aEnd = m_allocatedPixmaps.end(); + for ( ; aIt != aEnd; ++aIt ) + if ( (*aIt)->page == req->pageNumber() && (*aIt)->observer == req->observer() ) + { + AllocatedPixmap * p = *aIt; + m_allocatedPixmaps.erase( aIt ); + m_allocatedPixmapsTotalMemory -= p->memory; + delete p; + break; + } + + DocumentObserver *observer = req->observer(); + if ( m_observers.contains(observer) ) + { + // [MEM] 1.2 append memory allocation descriptor to the FIFO + qulonglong memoryBytes = 0; + const TilesManager *tm = req->d->tilesManager(); + if ( tm ) + memoryBytes = tm->totalMemory(); + else + memoryBytes = 4 * req->width() * req->height(); - AllocatedPixmap * memoryPage = new AllocatedPixmap( req->observer(), req->pageNumber(), memoryBytes ); - m_allocatedPixmaps.append( memoryPage ); - m_allocatedPixmapsTotalMemory += memoryBytes; + AllocatedPixmap * memoryPage = new AllocatedPixmap( req->observer(), req->pageNumber(), memoryBytes ); + m_allocatedPixmaps.append( memoryPage ); + m_allocatedPixmapsTotalMemory += memoryBytes; - // 2. notify an observer that its pixmap changed - observer->notifyPageChanged( req->pageNumber(), DocumentObserver::Pixmap ); - } + // 2. notify an observer that its pixmap changed + observer->notifyPageChanged( req->pageNumber(), DocumentObserver::Pixmap ); + } #ifndef NDEBUG - else - qCWarning(OkularCoreDebug) << "Receiving a done request for the defunct observer" << observer; + else + qCWarning(OkularCoreDebug) << "Receiving a done request for the defunct observer" << observer; #endif + } // 3. delete request m_pixmapRequestsMutex.lock(); m_executingPixmapRequests.removeAll( req ); m_pixmapRequestsMutex.unlock(); delete req; // 4. start a new generation if some is pending m_pixmapRequestsMutex.lock(); bool hasPixmaps = !m_pixmapRequestsStack.isEmpty(); m_pixmapRequestsMutex.unlock(); if ( hasPixmaps ) sendGeneratorPixmapRequest(); } void DocumentPrivate::setPageBoundingBox( int page, const NormalizedRect& boundingBox ) { Page * kp = m_pagesVector[ page ]; if ( !m_generator || !kp ) return; if ( kp->boundingBox() == boundingBox ) return; kp->setBoundingBox( boundingBox ); // notify observers about the change foreachObserverD( notifyPageChanged( page, DocumentObserver::BoundingBox ) ); // TODO: For generators that generate the bbox by pixmap scanning, if the first generated pixmap is very small, the bounding box will forever be inaccurate. // TODO: Crop computation should also consider annotations, actions, etc. to make sure they're not cropped away. // TODO: Help compute bounding box for generators that create a QPixmap without a QImage, like text and plucker. // TODO: Don't compute the bounding box if no one needs it (e.g., Trim Borders is off). } void DocumentPrivate::calculateMaxTextPages() { int multipliers = qMax(1, qRound(getTotalMemory() / 536870912.0)); // 512 MB switch (SettingsCore::memoryLevel()) { case SettingsCore::EnumMemoryLevel::Low: m_maxAllocatedTextPages = multipliers * 2; break; case SettingsCore::EnumMemoryLevel::Normal: m_maxAllocatedTextPages = multipliers * 50; break; case SettingsCore::EnumMemoryLevel::Aggressive: m_maxAllocatedTextPages = multipliers * 250; break; case SettingsCore::EnumMemoryLevel::Greedy: m_maxAllocatedTextPages = multipliers * 1250; break; } } void DocumentPrivate::textGenerationDone( Page *page ) { if ( !m_pageController ) return; // 1. If we reached the cache limit, delete the first text page from the fifo if (m_allocatedTextPagesFifo.size() == m_maxAllocatedTextPages) { int pageToKick = m_allocatedTextPagesFifo.takeFirst(); if (pageToKick != page->number()) // this should never happen but better be safe than sorry { m_pagesVector.at(pageToKick)->setTextPage( nullptr ); // deletes the textpage } } // 2. Add the page to the fifo of generated text pages m_allocatedTextPagesFifo.append( page->number() ); } void Document::setRotation( int r ) { d->setRotationInternal( r, true ); } void DocumentPrivate::setRotationInternal( int r, bool notify ) { Rotation rotation = (Rotation)r; if ( !m_generator || ( m_rotation == rotation ) ) return; // tell the pages to rotate QVector< Okular::Page * >::const_iterator pIt = m_pagesVector.constBegin(); QVector< Okular::Page * >::const_iterator pEnd = m_pagesVector.constEnd(); for ( ; pIt != pEnd; ++pIt ) (*pIt)->d->rotateAt( rotation ); if ( notify ) { // notify the generator that the current rotation has changed m_generator->rotationChanged( rotation, m_rotation ); } // set the new rotation m_rotation = rotation; if ( notify ) { foreachObserverD( notifySetup( m_pagesVector, DocumentObserver::NewLayoutForPages ) ); foreachObserverD( notifyContentsCleared( DocumentObserver::Pixmap | DocumentObserver::Highlights | DocumentObserver::Annotations ) ); } qCDebug(OkularCoreDebug) << "Rotated:" << r; } void Document::setPageSize( const PageSize &size ) { if ( !d->m_generator || !d->m_generator->hasFeature( Generator::PageSizes ) ) return; if ( d->m_pageSizes.isEmpty() ) d->m_pageSizes = d->m_generator->pageSizes(); int sizeid = d->m_pageSizes.indexOf( size ); if ( sizeid == -1 ) return; // tell the pages to change size QVector< Okular::Page * >::const_iterator pIt = d->m_pagesVector.constBegin(); QVector< Okular::Page * >::const_iterator pEnd = d->m_pagesVector.constEnd(); for ( ; pIt != pEnd; ++pIt ) (*pIt)->d->changeSize( size ); // clear 'memory allocation' descriptors qDeleteAll( d->m_allocatedPixmaps ); d->m_allocatedPixmaps.clear(); d->m_allocatedPixmapsTotalMemory = 0; // notify the generator that the current page size has changed d->m_generator->pageSizeChanged( size, d->m_pageSize ); // set the new page size d->m_pageSize = size; foreachObserver( notifySetup( d->m_pagesVector, DocumentObserver::NewLayoutForPages ) ); foreachObserver( notifyContentsCleared( DocumentObserver::Pixmap | DocumentObserver::Highlights ) ); qCDebug(OkularCoreDebug) << "New PageSize id:" << sizeid; } /** DocumentViewport **/ DocumentViewport::DocumentViewport( int n ) : pageNumber( n ) { // default settings rePos.enabled = false; rePos.normalizedX = 0.5; rePos.normalizedY = 0.0; rePos.pos = Center; autoFit.enabled = false; autoFit.width = false; autoFit.height = false; } DocumentViewport::DocumentViewport( const QString & xmlDesc ) : pageNumber( -1 ) { // default settings (maybe overridden below) rePos.enabled = false; rePos.normalizedX = 0.5; rePos.normalizedY = 0.0; rePos.pos = Center; autoFit.enabled = false; autoFit.width = false; autoFit.height = false; // check for string presence if ( xmlDesc.isEmpty() ) return; // decode the string bool ok; int field = 0; QString token = xmlDesc.section( QLatin1Char(';'), field, field ); while ( !token.isEmpty() ) { // decode the current token if ( field == 0 ) { pageNumber = token.toInt( &ok ); if ( !ok ) return; } else if ( token.startsWith( QLatin1String("C1") ) ) { rePos.enabled = true; rePos.normalizedX = token.section( QLatin1Char(':'), 1, 1 ).toDouble(); rePos.normalizedY = token.section( QLatin1Char(':'), 2, 2 ).toDouble(); rePos.pos = Center; } else if ( token.startsWith( QLatin1String("C2") ) ) { rePos.enabled = true; rePos.normalizedX = token.section( QLatin1Char(':'), 1, 1 ).toDouble(); rePos.normalizedY = token.section( QLatin1Char(':'), 2, 2 ).toDouble(); if (token.section( QLatin1Char(':'), 3, 3 ).toInt() == 1) rePos.pos = Center; else rePos.pos = TopLeft; } else if ( token.startsWith( QLatin1String("AF1") ) ) { autoFit.enabled = true; autoFit.width = token.section( QLatin1Char(':'), 1, 1 ) == QLatin1String("T"); autoFit.height = token.section( QLatin1Char(':'), 2, 2 ) == QLatin1String("T"); } // proceed tokenizing string field++; token = xmlDesc.section( QLatin1Char(';'), field, field ); } } QString DocumentViewport::toString() const { // start string with page number QString s = QString::number( pageNumber ); // if has center coordinates, save them on string if ( rePos.enabled ) s += QStringLiteral( ";C2:" ) + QString::number( rePos.normalizedX ) + QLatin1Char(':') + QString::number( rePos.normalizedY ) + QLatin1Char(':') + QString::number( rePos.pos ); // if has autofit enabled, save its state on string if ( autoFit.enabled ) s += QStringLiteral( ";AF1:" ) + (autoFit.width ? QLatin1Char('T') : QLatin1Char('F')) + QLatin1Char(':') + (autoFit.height ? QLatin1Char('T') : QLatin1Char('F')); return s; } bool DocumentViewport::isValid() const { return pageNumber >= 0; } bool DocumentViewport::operator==( const DocumentViewport & vp ) const { bool equal = ( pageNumber == vp.pageNumber ) && ( rePos.enabled == vp.rePos.enabled ) && ( autoFit.enabled == vp.autoFit.enabled ); if ( !equal ) return false; if ( rePos.enabled && (( rePos.normalizedX != vp.rePos.normalizedX) || ( rePos.normalizedY != vp.rePos.normalizedY ) || rePos.pos != vp.rePos.pos) ) return false; if ( autoFit.enabled && (( autoFit.width != vp.autoFit.width ) || ( autoFit.height != vp.autoFit.height )) ) return false; return true; } bool DocumentViewport::operator<( const DocumentViewport & vp ) const { // TODO: Check autoFit and Position if ( pageNumber != vp.pageNumber ) return pageNumber < vp.pageNumber; if ( !rePos.enabled && vp.rePos.enabled ) return true; if ( !vp.rePos.enabled ) return false; if ( rePos.normalizedY != vp.rePos.normalizedY ) return rePos.normalizedY < vp.rePos.normalizedY; return rePos.normalizedX < vp.rePos.normalizedX; } /** DocumentInfo **/ DocumentInfo::DocumentInfo() : d(new DocumentInfoPrivate()) { } DocumentInfo::DocumentInfo(const DocumentInfo &info) : d(new DocumentInfoPrivate()) { *this = info; } DocumentInfo& DocumentInfo::operator=(const DocumentInfo &info) { d->values = info.d->values; d->titles = info.d->titles; return *this; } DocumentInfo::~DocumentInfo() { delete d; } void DocumentInfo::set( const QString &key, const QString &value, const QString &title ) { d->values[ key ] = value; d->titles[ key ] = title; } void DocumentInfo::set( Key key, const QString &value ) { d->values[ getKeyString( key ) ] = value; } QStringList DocumentInfo::keys() const { return d->values.keys(); } QString DocumentInfo::get( Key key ) const { return get( getKeyString( key ) ); } QString DocumentInfo::get( const QString &key ) const { return d->values[ key ]; } QString DocumentInfo::getKeyString( Key key ) //const { switch ( key ) { case Title: return QStringLiteral("title"); break; case Subject: return QStringLiteral("subject"); break; case Description: return QStringLiteral("description"); break; case Author: return QStringLiteral("author"); break; case Creator: return QStringLiteral("creator"); break; case Producer: return QStringLiteral("producer"); break; case Copyright: return QStringLiteral("copyright"); break; case Pages: return QStringLiteral("pages"); break; case CreationDate: return QStringLiteral("creationDate"); break; case ModificationDate: return QStringLiteral("modificationDate"); break; case MimeType: return QStringLiteral("mimeType"); break; case Category: return QStringLiteral("category"); break; case Keywords: return QStringLiteral("keywords"); break; case FilePath: return QStringLiteral("filePath"); break; case DocumentSize: return QStringLiteral("documentSize"); break; case PagesSize: return QStringLiteral("pageSize"); break; default: qCWarning(OkularCoreDebug) << "Unknown" << key; return QString(); break; } } DocumentInfo::Key DocumentInfo::getKeyFromString( const QString &key ) //const { if (key == QLatin1String("title")) return Title; else if (key == QLatin1String("subject")) return Subject; else if (key == QLatin1String("description")) return Description; else if (key == QLatin1String("author")) return Author; else if (key == QLatin1String("creator")) return Creator; else if (key == QLatin1String("producer")) return Producer; else if (key == QLatin1String("copyright")) return Copyright; else if (key == QLatin1String("pages")) return Pages; else if (key == QLatin1String("creationDate")) return CreationDate; else if (key == QLatin1String("modificationDate")) return ModificationDate; else if (key == QLatin1String("mimeType")) return MimeType; else if (key == QLatin1String("category")) return Category; else if (key == QLatin1String("keywords")) return Keywords; else if (key == QLatin1String("filePath")) return FilePath; else if (key == QLatin1String("documentSize")) return DocumentSize; else if (key == QLatin1String("pageSize")) return PagesSize; else return Invalid; } QString DocumentInfo::getKeyTitle( Key key ) //const { switch ( key ) { case Title: return i18n( "Title" ); break; case Subject: return i18n( "Subject" ); break; case Description: return i18n( "Description" ); break; case Author: return i18n( "Author" ); break; case Creator: return i18n( "Creator" ); break; case Producer: return i18n( "Producer" ); break; case Copyright: return i18n( "Copyright" ); break; case Pages: return i18n( "Pages" ); break; case CreationDate: return i18n( "Created" ); break; case ModificationDate: return i18n( "Modified" ); break; case MimeType: return i18n( "Mime Type" ); break; case Category: return i18n( "Category" ); break; case Keywords: return i18n( "Keywords" ); break; case FilePath: return i18n( "File Path" ); break; case DocumentSize: return i18n( "File Size" ); break; case PagesSize: return i18n("Page Size"); break; default: return QString(); break; } } QString DocumentInfo::getKeyTitle( const QString &key ) const { QString title = getKeyTitle ( getKeyFromString( key ) ); if ( title.isEmpty() ) title = d->titles[ key ]; return title; } /** DocumentSynopsis **/ DocumentSynopsis::DocumentSynopsis() : QDomDocument( QStringLiteral("DocumentSynopsis") ) { // void implementation, only subclassed for naming } DocumentSynopsis::DocumentSynopsis( const QDomDocument &document ) : QDomDocument( document ) { } /** EmbeddedFile **/ EmbeddedFile::EmbeddedFile() { } EmbeddedFile::~EmbeddedFile() { } VisiblePageRect::VisiblePageRect( int page, const NormalizedRect &rectangle ) : pageNumber( page ), rect( rectangle ) { } #undef foreachObserver #undef foreachObserverD #include "moc_document.cpp" /* kate: replace-tabs on; indent-width 4; */ diff --git a/core/document_p.h b/core/document_p.h index 644a3c77b..66e1d617f 100644 --- a/core/document_p.h +++ b/core/document_p.h @@ -1,320 +1,321 @@ /*************************************************************************** * Copyright (C) 2004-2005 by Enrico Ros * * Copyright (C) 2004-2007 by Albert Astals Cid * * * * 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. * ***************************************************************************/ #ifndef _OKULAR_DOCUMENT_P_H_ #define _OKULAR_DOCUMENT_P_H_ #include "document.h" #include "synctex/synctex_parser.h" // qt/kde/system includes #include #include #include #include #include #include #include // local includes #include "fontinfo.h" #include "generator.h" class QUndoStack; class QEventLoop; class QFile; class QTimer; class QTemporaryFile; class KPluginMetaData; struct AllocatedPixmap; struct ArchiveData; struct RunningSearch; namespace Okular { class ConfigInterface; class PageController; class SaveInterface; class Scripter; class View; } struct GeneratorInfo { explicit GeneratorInfo( Okular::Generator *g, const KPluginMetaData &data) : generator( g ), metadata( data ), config( nullptr ), save( nullptr ), configChecked( false ), saveChecked( false ) {} Okular::Generator * generator; KPluginMetaData metadata; Okular::ConfigInterface * config; Okular::SaveInterface * save; bool configChecked : 1; bool saveChecked : 1; }; namespace Okular { class FontExtractionThread; struct DoContinueDirectionMatchSearchStruct { QSet< int > *pagesToNotify; RegularAreaRect *match; int currentPage; int searchID; }; enum LoadDocumentInfoFlag { LoadNone = 0, LoadPageInfo = 1, // Load annotations and forms LoadGeneralInfo = 2, // History, rotation, ... LoadAllInfo = 0xff }; Q_DECLARE_FLAGS(LoadDocumentInfoFlags, LoadDocumentInfoFlag) class DocumentPrivate { public: DocumentPrivate( Document *parent ) : m_parent( parent ), m_tempFile( nullptr ), m_docSize( -1 ), m_allocatedPixmapsTotalMemory( 0 ), m_maxAllocatedTextPages( 0 ), m_warnedOutOfMemory( false ), m_rotation( Rotation0 ), m_exportCached( false ), m_bookmarkManager( nullptr ), m_memCheckTimer( nullptr ), m_saveBookmarksTimer( nullptr ), m_generator( nullptr ), m_walletGenerator( nullptr ), m_generatorsLoaded( false ), m_pageController( nullptr ), m_closingLoop( nullptr ), m_scripter( nullptr ), m_archiveData( nullptr ), m_fontsCached( false ), m_annotationEditingEnabled ( true ), m_annotationBeingModified( false ), m_docdataMigrationNeeded( false ), m_synctex_scanner( nullptr ) { calculateMaxTextPages(); } // private methods bool updateMetadataXmlNameAndDocSize(); QString pagesSizeString() const; QString namePaperSize(double inchesWidth, double inchesHeight) const; QString localizedSize(const QSizeF &size) const; qulonglong calculateMemoryToFree(); void cleanupPixmapMemory(); void cleanupPixmapMemory( qulonglong memoryToFree ); AllocatedPixmap * searchLowestPriorityPixmap( bool unloadableOnly = false, bool thenRemoveIt = false, DocumentObserver *observer = nullptr /* any */ ); void calculateMaxTextPages(); qulonglong getTotalMemory(); qulonglong getFreeMemory( qulonglong *freeSwap = nullptr ); bool loadDocumentInfo( LoadDocumentInfoFlags loadWhat ); bool loadDocumentInfo( QFile &infoFile, LoadDocumentInfoFlags loadWhat ); void loadViewsInfo( View *view, const QDomElement &e ); void saveViewsInfo( View *view, QDomElement &e ) const; QUrl giveAbsoluteUrl( const QString & fileName ) const; bool openRelativeFile( const QString & fileName ); Generator * loadGeneratorLibrary( const KPluginMetaData& service ); void loadAllGeneratorLibraries(); void loadServiceList( const QVector& offers ); void unloadGenerator( const GeneratorInfo& info ); void cacheExportFormats(); void setRotationInternal( int r, bool notify ); ConfigInterface* generatorConfig( GeneratorInfo& info ); SaveInterface* generatorSave( GeneratorInfo& info ); Document::OpenResult openDocumentInternal( const KPluginMetaData& offer, bool isstdin, const QString& docFile, const QByteArray& filedata, const QString& password ); static ArchiveData *unpackDocumentArchive( const QString &archivePath ); bool savePageDocumentInfo( QTemporaryFile *infoFile, int what ) const; DocumentViewport nextDocumentViewport() const; void notifyAnnotationChanges( int page ); void notifyFormChanges( int page ); bool canAddAnnotationsNatively() const; bool canModifyExternalAnnotations() const; bool canRemoveExternalAnnotations() const; OKULARCORE_EXPORT static QString docDataFileName(const QUrl &url, qint64 document_size); + bool cancelRenderingBecauseOf( PixmapRequest *executingRequest, PixmapRequest *newRequest ); // Methods that implement functionality needed by undo commands void performAddPageAnnotation( int page, Annotation *annotation ); void performRemovePageAnnotation( int page, Annotation * annotation ); void performModifyPageAnnotation( int page, Annotation * annotation, bool appearanceChanged ); void performSetAnnotationContents( const QString & newContents, Annotation *annot, int pageNumber ); void recalculateForms(); // private slots void saveDocumentInfo() const; void slotTimedMemoryCheck(); void sendGeneratorPixmapRequest(); void rotationFinished( int page, Okular::Page *okularPage ); void slotFontReadingProgress( int page ); void fontReadingGotFont( const Okular::FontInfo& font ); void slotGeneratorConfigChanged( const QString& ); void refreshPixmaps( int ); void _o_configChanged(); void doContinueDirectionMatchSearch(void *doContinueDirectionMatchSearchStruct); void doContinueAllDocumentSearch(void *pagesToNotifySet, void *pageMatchesMap, int currentPage, int searchID); void doContinueGooglesDocumentSearch(void *pagesToNotifySet, void *pageMatchesMap, int currentPage, int searchID, const QStringList & words); void doProcessSearchMatch( RegularAreaRect *match, RunningSearch *search, QSet< int > *pagesToNotify, int currentPage, int searchID, bool moveViewport, const QColor & color ); // generators stuff /** * This method is used by the generators to signal the finish of * the pixmap generation @p request. */ void requestDone( PixmapRequest * request ); void textGenerationDone( Page *page ); /** * Sets the bounding box of the given @p page (in terms of upright orientation, i.e., Rotation0). */ void setPageBoundingBox( int page, const NormalizedRect& boundingBox ); /** * Request a particular metadata of the Document itself (ie, not something * depending on the document type/backend). */ QVariant documentMetaData( const Generator::DocumentMetaDataKey key, const QVariant &option ) const; /** * Return whether the normalized rectangle @p rectOfInterest on page number @p rectPage * is fully visible. */ bool isNormalizedRectangleFullyVisible( const Okular::NormalizedRect & rectOfInterest, int rectPage ); // For sync files void loadSyncFile( const QString & filePath ); // member variables Document *m_parent; QPointer m_widget; // find descriptors, mapped by ID (we handle multiple searches) QMap< int, RunningSearch * > m_searches; bool m_searchCancelled; // needed because for remote documents docFileName is a local file and // we want the remote url when the document refers to relativeNames QUrl m_url; // cached stuff QString m_docFileName; QString m_xmlFileName; QTemporaryFile *m_tempFile; qint64 m_docSize; // viewport stuff QLinkedList< DocumentViewport > m_viewportHistory; QLinkedList< DocumentViewport >::iterator m_viewportIterator; DocumentViewport m_nextDocumentViewport; // see Link::Goto for an explanation QString m_nextDocumentDestination; // observers / requests / allocator stuff QSet< DocumentObserver * > m_observers; QLinkedList< PixmapRequest * > m_pixmapRequestsStack; QLinkedList< PixmapRequest * > m_executingPixmapRequests; QMutex m_pixmapRequestsMutex; QLinkedList< AllocatedPixmap * > m_allocatedPixmaps; qulonglong m_allocatedPixmapsTotalMemory; QList< int > m_allocatedTextPagesFifo; int m_maxAllocatedTextPages; bool m_warnedOutOfMemory; // the rotation applied to the document Rotation m_rotation; // the current size of the pages (if available), and the cache of the // available page sizes PageSize m_pageSize; PageSize::List m_pageSizes; // cache of the export formats bool m_exportCached; ExportFormat::List m_exportFormats; ExportFormat m_exportToText; // our bookmark manager BookmarkManager *m_bookmarkManager; // timers (memory checking / info saver) QTimer *m_memCheckTimer; QTimer *m_saveBookmarksTimer; QHash m_loadedGenerators; Generator * m_generator; QString m_generatorName; Generator * m_walletGenerator; bool m_generatorsLoaded; QVector< Page * > m_pagesVector; QVector< VisiblePageRect * > m_pageRects; // cache of the mimetype we support QStringList m_supportedMimeTypes; PageController *m_pageController; QEventLoop *m_closingLoop; Scripter *m_scripter; ArchiveData *m_archiveData; QString m_archivedFileName; QPointer< FontExtractionThread > m_fontThread; bool m_fontsCached; QSet m_documentInfoAskedKeys; DocumentInfo m_documentInfo; FontInfo::List m_fontsCache; QSet< View * > m_views; bool m_annotationEditingEnabled; bool m_annotationBeingModified; // is an annotation currently being moved or resized? bool m_metadataLoadingCompleted; QUndoStack *m_undoStack; QDomNode m_prevPropsOfAnnotBeingModified; // Since 0.21, we no longer support saving annotations and form data in // the docdata/ directory and we ask the user to migrate them to an // external file as soon as possible, otherwise the document will be // shown in read-only mode. This flag is set if the docdata/ XML file // for the current document contains any annotation or form. bool m_docdataMigrationNeeded; synctex_scanner_p m_synctex_scanner; // generator selection static QVector availableGenerators(); static QVector configurableGenerators(); static KPluginMetaData generatorForMimeType(const QMimeType& type, QWidget* widget, const QVector &triedOffers = QVector()); }; class DocumentInfoPrivate { public: QMap values; // key -> value QMap titles; // key -> title For the custom keys }; } #endif /* kate: replace-tabs on; indent-width 4; */ diff --git a/core/generator.cpp b/core/generator.cpp index ba6013bec..91e34447a 100644 --- a/core/generator.cpp +++ b/core/generator.cpp @@ -1,729 +1,816 @@ /*************************************************************************** * Copyright (C) 2005 by Piotr Szymanski * * Copyright (C) 2008 by Albert Astals Cid * * Copyright (C) 2017 Klarälvdalens Datakonsult AB, a KDAB Group * * company, info@kdab.com. Work sponsored by the * * LiMux project of the city of Munich * * * * 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. * ***************************************************************************/ #include "generator.h" #include "generator_p.h" #include "observer.h" #include #include #include #include #include #include +#include #include #include #include "document.h" #include "document_p.h" #include "page.h" #include "page_p.h" #include "textpage.h" #include "utils.h" using namespace Okular; GeneratorPrivate::GeneratorPrivate() : m_document( nullptr ), mPixmapGenerationThread( nullptr ), mTextPageGenerationThread( nullptr ), m_mutex( nullptr ), m_threadsMutex( nullptr ), mPixmapReady( true ), mTextPageReady( true ), m_closing( false ), m_closingLoop( nullptr ), m_dpi(72.0, 72.0) { qRegisterMetaType(); } GeneratorPrivate::~GeneratorPrivate() { if ( mPixmapGenerationThread ) mPixmapGenerationThread->wait(); delete mPixmapGenerationThread; if ( mTextPageGenerationThread ) mTextPageGenerationThread->wait(); delete mTextPageGenerationThread; delete m_mutex; delete m_threadsMutex; } PixmapGenerationThread* GeneratorPrivate::pixmapGenerationThread() { if ( mPixmapGenerationThread ) return mPixmapGenerationThread; Q_Q( Generator ); mPixmapGenerationThread = new PixmapGenerationThread( q ); QObject::connect( mPixmapGenerationThread, SIGNAL(finished()), q, SLOT(pixmapGenerationFinished()), Qt::QueuedConnection ); return mPixmapGenerationThread; } TextPageGenerationThread* GeneratorPrivate::textPageGenerationThread() { if ( mTextPageGenerationThread ) return mTextPageGenerationThread; Q_Q( Generator ); mTextPageGenerationThread = new TextPageGenerationThread( q ); QObject::connect( mTextPageGenerationThread, SIGNAL(finished()), q, SLOT(textpageGenerationFinished()), Qt::QueuedConnection ); return mTextPageGenerationThread; } void GeneratorPrivate::pixmapGenerationFinished() { Q_Q( Generator ); PixmapRequest *request = mPixmapGenerationThread->request(); + const QImage& img = mPixmapGenerationThread->image(); mPixmapGenerationThread->endGeneration(); QMutexLocker locker( threadsLock() ); - mPixmapReady = true; if ( m_closing ) { + mPixmapReady = true; delete request; if ( mTextPageReady ) { locker.unlock(); m_closingLoop->quit(); } return; } - const QImage& img = mPixmapGenerationThread->image(); - request->page()->setPixmap( request->observer(), new QPixmap( QPixmap::fromImage( img ) ), request->normalizedRect() ); - const int pageNumber = request->page()->number(); - if ( mPixmapGenerationThread->calcBoundingBox() ) - q->updatePageBoundingBox( pageNumber, mPixmapGenerationThread->boundingBox() ); + if ( !request->shouldAbortRender() ) + { + request->page()->setPixmap( request->observer(), new QPixmap( QPixmap::fromImage( img ) ), request->normalizedRect() ); + const int pageNumber = request->page()->number(); + + if ( mPixmapGenerationThread->calcBoundingBox() ) + q->updatePageBoundingBox( pageNumber, mPixmapGenerationThread->boundingBox() ); + } + else + { + // Cancel the text page generation too if it's still running + if ( mTextPageGenerationThread && mTextPageGenerationThread->isRunning() ) { + mTextPageGenerationThread->abortExtraction(); + mTextPageGenerationThread->wait(); + } + } + + mPixmapReady = true; q->signalPixmapRequestDone( request ); } void GeneratorPrivate::textpageGenerationFinished() { Q_Q( Generator ); Page *page = mTextPageGenerationThread->page(); mTextPageGenerationThread->endGeneration(); QMutexLocker locker( threadsLock() ); mTextPageReady = true; if ( m_closing ) { delete mTextPageGenerationThread->textPage(); if ( mPixmapReady ) { locker.unlock(); m_closingLoop->quit(); } return; } if ( mTextPageGenerationThread->textPage() ) { TextPage *tp = mTextPageGenerationThread->textPage(); page->setTextPage( tp ); q->signalTextGenerationDone( page, tp ); } } QMutex* GeneratorPrivate::threadsLock() { if ( !m_threadsMutex ) m_threadsMutex = new QMutex(); return m_threadsMutex; } QVariant GeneratorPrivate::metaData( const QString &, const QVariant & ) const { return QVariant(); } QImage GeneratorPrivate::image( PixmapRequest * ) { return QImage(); } -Generator::Generator(QObject* parent, const QVariantList&) - : QObject(parent) - , d_ptr( new GeneratorPrivate() ) +Generator::Generator(QObject* parent, const QVariantList &args) + : Generator( *new GeneratorPrivate(), parent, args ) { - d_ptr->q_ptr = this; + // the delegated constructor does it all } Generator::Generator(GeneratorPrivate &dd, QObject *parent, const QVariantList &args) : QObject(parent), d_ptr(&dd) { d_ptr->q_ptr = this; Q_UNUSED(args) } Generator::~Generator() { delete d_ptr; } bool Generator::loadDocument( const QString & fileName, QVector< Page * > & pagesVector ) { Q_UNUSED(fileName); Q_UNUSED(pagesVector); return false; } bool Generator::loadDocumentFromData( const QByteArray &, QVector< Page * > & ) { return false; } Document::OpenResult Generator::loadDocumentWithPassword( const QString & fileName, QVector< Page * > & pagesVector, const QString & ) { return loadDocument( fileName, pagesVector ) ? Document::OpenSuccess : Document::OpenError; } Document::OpenResult Generator::loadDocumentFromDataWithPassword( const QByteArray & fileData, QVector< Page * > & pagesVector, const QString & ) { return loadDocumentFromData( fileData, pagesVector ) ? Document::OpenSuccess : Document::OpenError; } Generator::SwapBackingFileResult Generator::swapBackingFile( QString const &/*newFileName */, QVector & /*newPagesVector*/ ) { return SwapBackingFileError; } bool Generator::closeDocument() { Q_D( Generator ); d->m_closing = true; d->threadsLock()->lock(); if ( !( d->mPixmapReady && d->mTextPageReady ) ) { QEventLoop loop; d->m_closingLoop = &loop; d->threadsLock()->unlock(); loop.exec(); d->m_closingLoop = nullptr; } else { d->threadsLock()->unlock(); } bool ret = doCloseDocument(); d->m_closing = false; return ret; } bool Generator::canGeneratePixmap() const { Q_D( const Generator ); return d->mPixmapReady; } void Generator::generatePixmap( PixmapRequest *request ) { Q_D( Generator ); d->mPixmapReady = false; const bool calcBoundingBox = !request->isTile() && !request->page()->isBoundingBoxKnown(); if ( request->asynchronous() && hasFeature( Threaded ) ) { + if ( d->textPageGenerationThread()->isFinished() && !canGenerateTextPage() ) + { + // It can happen that the text generation has already finished but + // mTextPageReady is still false because textpageGenerationFinished + // didn't have time to run, if so queue ourselves + QTimer::singleShot(0, this, [this, request] { generatePixmap(request); }); + return; + } + d->pixmapGenerationThread()->startGeneration( request, calcBoundingBox ); /** * We create the text page for every page that is visible to the * user, so he can use the text extraction tools without a delay. */ if ( hasFeature( TextExtraction ) && !request->page()->hasTextPage() && canGenerateTextPage() && !d->m_closing ) { d->mTextPageReady = false; - // Queue the text generation request so that pixmap generation gets a chance to start before the text generation - QMetaObject::invokeMethod(d->textPageGenerationThread(), "startGeneration", Qt::QueuedConnection, Q_ARG(Okular::Page*, request->page())); + d->textPageGenerationThread()->setPage( request->page() ); + + // dummy is used as a way to make sure the lambda gets disconnected each time it is executed + // since not all the times the pixmap generation thread starts we want the text generation thread to also start + QObject *dummy = new QObject(); + connect(d_ptr->pixmapGenerationThread(), &QThread::started, dummy, [this, dummy] { + delete dummy; + d_ptr->textPageGenerationThread()->startGeneration(); + }); } return; } const QImage& img = image( request ); request->page()->setPixmap( request->observer(), new QPixmap( QPixmap::fromImage( img ) ), request->normalizedRect() ); const int pageNumber = request->page()->number(); d->mPixmapReady = true; signalPixmapRequestDone( request ); if ( calcBoundingBox ) updatePageBoundingBox( pageNumber, Utils::imageBoundingBox( &img ) ); } bool Generator::canGenerateTextPage() const { Q_D( const Generator ); return d->mTextPageReady; } void Generator::generateTextPage( Page *page ) { - TextPage *tp = textPage( page ); + TextRequest treq( page ); + TextPage *tp = textPage( &treq ); page->setTextPage( tp ); signalTextGenerationDone( page, tp ); } QImage Generator::image( PixmapRequest *request ) { Q_D( Generator ); return d->image( request ); } -TextPage* Generator::textPage( Page* ) +TextPage* Generator::textPage( TextRequest * ) { return nullptr; } DocumentInfo Generator::generateDocumentInfo(const QSet &keys) const { Q_UNUSED(keys); return DocumentInfo(); } const DocumentSynopsis * Generator::generateDocumentSynopsis() { return nullptr; } FontInfo::List Generator::fontsForPage( int ) { return FontInfo::List(); } const QList * Generator::embeddedFiles() const { return nullptr; } Generator::PageSizeMetric Generator::pagesSizeMetric() const { return None; } bool Generator::isAllowed( Permission ) const { return true; } void Generator::rotationChanged( Rotation, Rotation ) { } PageSize::List Generator::pageSizes() const { return PageSize::List(); } void Generator::pageSizeChanged( const PageSize &, const PageSize & ) { } bool Generator::print( QPrinter& ) { return false; } Generator::PrintError Generator::printError() const { return UnknownPrintError; } void Generator::opaqueAction( const BackendOpaqueAction * /*action*/ ) { } QVariant Generator::metaData( const QString &key, const QVariant &option ) const { Q_D( const Generator ); return d->metaData( key, option ); } ExportFormat::List Generator::exportFormats() const { return ExportFormat::List(); } bool Generator::exportTo( const QString&, const ExportFormat& ) { return false; } void Generator::walletDataForFile( const QString &fileName, QString *walletName, QString *walletFolder, QString *walletKey ) const { *walletKey = fileName.section( QLatin1Char('/'), -1, -1); *walletName = KWallet::Wallet::NetworkWallet(); *walletFolder = QStringLiteral("KPdf"); } bool Generator::hasFeature( GeneratorFeature feature ) const { Q_D( const Generator ); return d->m_features.contains( feature ); } void Generator::signalPixmapRequestDone( PixmapRequest * request ) { Q_D( Generator ); if ( d->m_document ) d->m_document->requestDone( request ); else { delete request; } } void Generator::signalTextGenerationDone( Page *page, TextPage *textPage ) { Q_D( Generator ); if ( d->m_document ) d->m_document->textGenerationDone( page ); else delete textPage; } void Generator::signalPartialPixmapRequest( PixmapRequest *request, const QImage &image ) { - request->page()->setPixmap( request->observer(), new QPixmap( QPixmap::fromImage( image ) ), request->normalizedRect() ); + if ( request->shouldAbortRender() ) + return; + + PagePrivate *pagePrivate = PagePrivate::get( request->page() ); + pagePrivate->setPixmap( request->observer(), new QPixmap( QPixmap::fromImage( image ) ), request->normalizedRect(), true /* isPartialPixmap */ ); const int pageNumber = request->page()->number(); request->observer()->notifyPageChanged( pageNumber, Okular::DocumentObserver::Pixmap ); } const Document * Generator::document() const { Q_D( const Generator ); if ( d->m_document ) { return d->m_document->m_parent; } return nullptr; } void Generator::setFeature( GeneratorFeature feature, bool on ) { Q_D( Generator ); if ( on ) d->m_features.insert( feature ); else d->m_features.remove( feature ); } QVariant Generator::documentMetaData( const QString &key, const QVariant &option ) const { Q_D( const Generator ); if ( !d->m_document ) return QVariant(); if (key == QLatin1String("PaperColor")) return documentMetaData(PaperColorMetaData, option); if (key == QLatin1String("GraphicsAntialias")) return documentMetaData(GraphicsAntialiasMetaData, option); if (key == QLatin1String("TextAntialias")) return documentMetaData(TextAntialiasMetaData, option); if (key == QLatin1String("TextHinting")) return documentMetaData(TextHintingMetaData, option); return QVariant(); } QVariant Generator::documentMetaData( const DocumentMetaDataKey key, const QVariant &option ) const { Q_D( const Generator ); if ( !d->m_document ) return QVariant(); return d->m_document->documentMetaData( key, option ); } QMutex* Generator::userMutex() const { Q_D( const Generator ); if ( !d->m_mutex ) { d->m_mutex = new QMutex(); } return d->m_mutex; } void Generator::updatePageBoundingBox( int page, const NormalizedRect & boundingBox ) { Q_D( Generator ); if ( d->m_document ) // still connected to document? d->m_document->setPageBoundingBox( page, boundingBox ); } void Generator::requestFontData(const Okular::FontInfo & /*font*/, QByteArray * /*data*/) { } void Generator::setDPI(const QSizeF & dpi) { Q_D( Generator ); d->m_dpi = dpi; } QSizeF Generator::dpi() const { Q_D( const Generator ); return d->m_dpi; } QAbstractItemModel * Generator::layersModel() const { return nullptr; } +TextRequest::TextRequest() +: d( new TextRequestPrivate ) +{ + d->mPage = nullptr; + d->mShouldAbortExtraction = 0; +} + +TextRequest::TextRequest( Page *page ) +: d( new TextRequestPrivate ) +{ + d->mPage = page; + d->mShouldAbortExtraction = 0; +} + +TextRequest::~TextRequest() +{ + delete d; +} + +Page *TextRequest::page() const +{ + return d->mPage; +} + +bool TextRequest::shouldAbortExtraction() const +{ + return d->mShouldAbortExtraction != 0; +} + +TextRequestPrivate *TextRequestPrivate::get(const TextRequest *req) +{ + return req->d; +} + PixmapRequest::PixmapRequest( DocumentObserver *observer, int pageNumber, int width, int height, int priority, PixmapRequestFeatures features ) : d( new PixmapRequestPrivate ) { d->mObserver = observer; d->mPageNumber = pageNumber; d->mWidth = ceil(width * qApp->devicePixelRatio()); d->mHeight = ceil(height * qApp->devicePixelRatio()); d->mPriority = priority; d->mFeatures = features; d->mForce = false; d->mTile = false; d->mNormalizedRect = NormalizedRect(); d->mPartialUpdatesWanted = false; + d->mShouldAbortRender = 0; } PixmapRequest::~PixmapRequest() { delete d; } DocumentObserver *PixmapRequest::observer() const { return d->mObserver; } int PixmapRequest::pageNumber() const { return d->mPageNumber; } int PixmapRequest::width() const { return d->mWidth; } int PixmapRequest::height() const { return d->mHeight; } int PixmapRequest::priority() const { return d->mPriority; } bool PixmapRequest::asynchronous() const { return d->mFeatures & Asynchronous; } bool PixmapRequest::preload() const { return d->mFeatures & Preload; } Page* PixmapRequest::page() const { return d->mPage; } void PixmapRequest::setTile( bool tile ) { d->mTile = tile; } bool PixmapRequest::isTile() const { return d->mTile; } void PixmapRequest::setNormalizedRect( const NormalizedRect &rect ) { if ( d->mNormalizedRect == rect ) return; d->mNormalizedRect = rect; } const NormalizedRect& PixmapRequest::normalizedRect() const { return d->mNormalizedRect; } void PixmapRequest::setPartialUpdatesWanted(bool partialUpdatesWanted) { d->mPartialUpdatesWanted = partialUpdatesWanted; } bool PixmapRequest::partialUpdatesWanted() const { return d->mPartialUpdatesWanted; } +bool PixmapRequest::shouldAbortRender() const +{ + return d->mShouldAbortRender != 0; +} + Okular::TilesManager* PixmapRequestPrivate::tilesManager() const { return mPage->d->tilesManager(mObserver); } +PixmapRequestPrivate *PixmapRequestPrivate::get(const PixmapRequest *req) +{ + return req->d; +} + void PixmapRequestPrivate::swap() { qSwap( mWidth, mHeight ); } class Okular::ExportFormatPrivate : public QSharedData { public: ExportFormatPrivate( const QString &description, const QMimeType &mimeType, const QIcon &icon = QIcon() ) : QSharedData(), mDescription( description ), mMimeType( mimeType ), mIcon( icon ) { } ~ExportFormatPrivate() { } QString mDescription; QMimeType mMimeType; QIcon mIcon; }; ExportFormat::ExportFormat() : d( new ExportFormatPrivate( QString(), QMimeType() ) ) { } ExportFormat::ExportFormat( const QString &description, const QMimeType &mimeType ) : d( new ExportFormatPrivate( description, mimeType ) ) { } ExportFormat::ExportFormat( const QIcon &icon, const QString &description, const QMimeType &mimeType ) : d( new ExportFormatPrivate( description, mimeType, icon ) ) { } ExportFormat::~ExportFormat() { } ExportFormat::ExportFormat( const ExportFormat &other ) : d( other.d ) { } ExportFormat& ExportFormat::operator=( const ExportFormat &other ) { if ( this == &other ) return *this; d = other.d; return *this; } QString ExportFormat::description() const { return d->mDescription; } QMimeType ExportFormat::mimeType() const { return d->mMimeType; } QIcon ExportFormat::icon() const { return d->mIcon; } bool ExportFormat::isNull() const { return !d->mMimeType.isValid() || d->mDescription.isNull(); } ExportFormat ExportFormat::standardFormat( StandardExportFormat type ) { QMimeDatabase db; switch ( type ) { case PlainText: return ExportFormat( QIcon::fromTheme( QStringLiteral("text-x-generic") ), i18n( "Plain &Text..." ), db.mimeTypeForName( QStringLiteral("text/plain") ) ); break; case PDF: return ExportFormat( QIcon::fromTheme( QStringLiteral("application-pdf") ), i18n( "PDF" ), db.mimeTypeForName( QStringLiteral("application/pdf") ) ); break; case OpenDocumentText: return ExportFormat( QIcon::fromTheme( QStringLiteral("application-vnd.oasis.opendocument.text") ), i18nc( "This is the document format", "OpenDocument Text" ), db.mimeTypeForName( QStringLiteral("application/vnd.oasis.opendocument.text") ) ); break; case HTML: return ExportFormat( QIcon::fromTheme( QStringLiteral("text-html") ), i18nc( "This is the document format", "HTML" ), db.mimeTypeForName( QStringLiteral("text/html") ) ); break; } return ExportFormat(); } bool ExportFormat::operator==( const ExportFormat &other ) const { return d == other.d; } bool ExportFormat::operator!=( const ExportFormat &other ) const { return d != other.d; } QDebug operator<<( QDebug str, const Okular::PixmapRequest &req ) { - QString s = QStringLiteral( "PixmapRequest(#%2, %1, %3x%4, page %6, prio %5)" ) - .arg( QString( req.asynchronous() ? QStringLiteral ( "async" ) : QStringLiteral ( "sync" ) ) ) - .arg( (qulonglong)req.observer() ) - .arg( req.width() ) - .arg( req.height() ) - .arg( req.priority() ) - .arg( req.pageNumber() ); - str << qPrintable( s ); + PixmapRequestPrivate *reqPriv = PixmapRequestPrivate::get(&req); + + str << "PixmapRequest:" << &req; + str << "- observer:" << (qulonglong)req.observer(); + str << "- page:" << req.pageNumber(); + str << "- width:" << req.width(); + str << "- height:" << req.height(); + str << "- priority:" << req.priority(); + str << "- async:" << ( req.asynchronous() ? "true" : "false" ); + str << "- tile:" << ( req.isTile() ? "true" : "false" ); + str << "- rect:" << req.normalizedRect(); + str << "- preload:" << ( req.preload() ? "true" : "false" ); + str << "- partialUpdates:" << ( req.partialUpdatesWanted() ? "true" : "false" ); + str << "- shouldAbort:" << ( req.shouldAbortRender() ? "true" : "false" ); + str << "- force:" << ( reqPriv->mForce ? "true" : "false" ); return str; } #include "moc_generator.cpp" /* kate: replace-tabs on; indent-width 4; */ diff --git a/core/generator.h b/core/generator.h index 24438548e..3146b6b07 100644 --- a/core/generator.h +++ b/core/generator.h @@ -1,773 +1,824 @@ /*************************************************************************** * Copyright (C) 2004-5 by Enrico Ros * * Copyright (C) 2005 by Piotr Szymanski * * Copyright (C) 2008 by Albert Astals Cid * * Copyright (C) 2017 Klarälvdalens Datakonsult AB, a KDAB Group * * company, info@kdab.com. Work sponsored by the * * LiMux project of the city of Munich * * * * 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. * ***************************************************************************/ #ifndef _OKULAR_GENERATOR_H_ #define _OKULAR_GENERATOR_H_ #include "okularcore_export.h" #include "document.h" #include "fontinfo.h" #include "global.h" #include "pagesize.h" #include #include #include #include #include #include #include #include #include #define OKULAR_EXPORT_PLUGIN(classname, json ) \ static_assert(json[0] != '\0', "arg2 must be a string literal"); \ K_PLUGIN_FACTORY_WITH_JSON(classname ## Factory, json, registerPlugin();) class QByteArray; class QMutex; class QPrinter; class QPrintDialog; class QIcon; namespace Okular { class BackendOpaqueAction; class DocumentFonts; class DocumentInfo; class DocumentObserver; class DocumentSynopsis; class EmbeddedFile; class ExportFormatPrivate; class FontInfo; class GeneratorPrivate; class Page; class PixmapRequest; class PixmapRequestPrivate; class TextPage; +class TextRequest; +class TextRequestPrivate; class NormalizedRect; class SourceReference; /* Note: on contents generation and asynchronous queries. * Many observers may want to request data syncronously or asynchronously. * - Sync requests. These should be done in-place. * - Async request must be done in real background. That usually means a * thread, such as QThread derived classes. * Once contents are available, they must be immediately stored in the * Page they refer to, and a signal is emitted as soon as storing * (even for sync or async queries) has been done. */ /** * @short Defines an entry for the export menu * * This class encapsulates information about an export format. * Every Generator can support 0 or more export formats which can be * queried with @ref Generator::exportFormats(). */ class OKULARCORE_EXPORT ExportFormat { public: typedef QList List; /** * Creates an empty export format. * * @see isNull() */ ExportFormat(); /** * Creates a new export format. * * @param description The i18n'ed description of the format. * @param mimeType The supported mime type of the format. */ ExportFormat( const QString &description, const QMimeType &mimeType ); /** * Creates a new export format. * * @param icon The icon used in the GUI for this format. * @param description The i18n'ed description of the format. * @param mimeType The supported mime type of the format. */ ExportFormat( const QIcon &icon, const QString &description, const QMimeType &mimeType ); /** * Destroys the export format. */ ~ExportFormat(); /** * @internal */ ExportFormat( const ExportFormat &other ); /** * @internal */ ExportFormat& operator=( const ExportFormat &other ); /** * Returns the description of the format. */ QString description() const; /** * Returns the mime type of the format. */ QMimeType mimeType() const; /** * Returns the icon for GUI representations of the format. */ QIcon icon() const; /** * Returns whether the export format is null/valid. * * An ExportFormat is null if the mimetype is not valid or the * description is empty, or both. */ bool isNull() const; /** * Type of standard export format. */ enum StandardExportFormat { PlainText, ///< Plain text PDF, ///< PDF, aka Portable Document Format OpenDocumentText, ///< OpenDocument Text format @since 0.8 (KDE 4.2) HTML ///< OpenDocument Text format @since 0.8 (KDE 4.2) }; /** * Builds a standard format for the specified @p type . */ static ExportFormat standardFormat( StandardExportFormat type ); bool operator==( const ExportFormat &other ) const; bool operator!=( const ExportFormat &other ) const; private: /// @cond PRIVATE friend class ExportFormatPrivate; /// @endcond QSharedDataPointer d; }; /** * @short [Abstract Class] The information generator. * * Most of class members are virtuals and some of them pure virtual. The pure * virtuals provide the minimal functionalities for a Generator, that is being * able to generate QPixmap for the Page 's of the Document. * * Implementing the other functions will make the Generator able to provide * more contents and/or functionalities (like text extraction). * * Generation/query is requested by the Document class only, and that * class stores the resulting data into Page s. The data will then be * displayed by the GUI components (PageView, ThumbnailList, etc..). * * @see PrintInterface, ConfigInterface, GuiInterface */ class OKULARCORE_EXPORT Generator : public QObject { /// @cond PRIVATE friend class PixmapGenerationThread; friend class TextPageGenerationThread; /// @endcond Q_OBJECT public: /** * Describe the possible optional features that a Generator can * provide. */ enum GeneratorFeature { Threaded, TextExtraction, ///< Whether the Generator can extract text from the document in the form of TextPage's ReadRawData, ///< Whether the Generator can read a document directly from its raw data. FontInfo, ///< Whether the Generator can provide information about the fonts used in the document PageSizes, ///< Whether the Generator can change the size of the document pages. PrintNative, ///< Whether the Generator supports native cross-platform printing (QPainter-based). PrintPostscript, ///< Whether the Generator supports postscript-based file printing. PrintToFile, ///< Whether the Generator supports export to PDF & PS through the Print Dialog TiledRendering, ///< Whether the Generator can render tiles @since 0.16 (KDE 4.10) - SwapBackingFile ///< Whether the Generator can hot-swap the file it's reading from @since 1.3 + SwapBackingFile, ///< Whether the Generator can hot-swap the file it's reading from @since 1.3 + SupportsCancelling ///< Whether the Generator can cancel requests @since 1.4 }; /** * Creates a new generator. */ Generator(QObject* parent = nullptr, const QVariantList& args = QVariantList()); /** * Destroys the generator. */ virtual ~Generator(); /** * Loads the document with the given @p fileName and fills the * @p pagesVector with the parsed pages. * * @note If you implement the WithPassword variants you don't need to implement this one * * @returns true on success, false otherwise. */ virtual bool loadDocument( const QString & fileName, QVector< Page * > & pagesVector ); /** * Loads the document from the raw data @p fileData and fills the * @p pagesVector with the parsed pages. * * @note If you implement the WithPassword variants you don't need to implement this one * * @note the Generator has to have the feature @ref ReadRawData enabled * * @returns true on success, false otherwise. */ virtual bool loadDocumentFromData( const QByteArray & fileData, QVector< Page * > & pagesVector ); /** * Loads the document with the given @p fileName and @p password and fills the * @p pagesVector with the parsed pages. * * @note Do not implement this if your format doesn't support passwords, it'll cleanly call loadDocument() * * @since 0.20 (KDE 4.14) * * @returns a LoadResult defining the result of the operation */ virtual Document::OpenResult loadDocumentWithPassword( const QString & fileName, QVector< Page * > & pagesVector, const QString &password ); /** * Loads the document from the raw data @p fileData and @p password and fills the * @p pagesVector with the parsed pages. * * @note Do not implement this if your format doesn't support passwords, it'll cleanly call loadDocumentFromData() * * @note the Generator has to have the feature @ref ReadRawData enabled * * @since 0.20 (KDE 4.14) * * @returns a LoadResult defining the result of the operation */ virtual Document::OpenResult loadDocumentFromDataWithPassword( const QByteArray & fileData, QVector< Page * > & pagesVector, const QString &password ); /** * Describes the result of an swap file operation. * * @since 1.3 */ enum SwapBackingFileResult { SwapBackingFileError, //< The document could not be swapped SwapBackingFileNoOp, //< The document was swapped and nothing needs to be done SwapBackingFileReloadInternalData //< The document was swapped and internal data (forms, annotations, etc) needs to be reloaded }; /** * Changes the path of the file we are reading from. The new path must * point to a copy of the same document. * * @note the Generator has to have the feature @ref SwapBackingFile enabled * * @since 1.3 */ virtual SwapBackingFileResult swapBackingFile( const QString & newFileName, QVector & newPagesVector ); /** * This method is called when the document is closed and not used * any longer. * * @returns true on success, false otherwise. */ bool closeDocument(); /** * This method returns whether the generator is ready to * handle a new pixmap request. */ virtual bool canGeneratePixmap() const; /** * This method can be called to trigger the generation of * a new pixmap as described by @p request. */ virtual void generatePixmap( PixmapRequest * request ); /** * This method returns whether the generator is ready to * handle a new text page request. */ virtual bool canGenerateTextPage() const; /** * This method can be called to trigger the generation of * a text page for the given @p page. * - * The generation is done synchronous or asynchronous, depending - * on the @p type parameter and the capabilities of the - * generator (e.g. multithreading). + * The generation is done in the calling thread. * * @see TextPage */ - virtual void generateTextPage( Page * page ); + void generateTextPage( Page * page ); /** * Returns the general information object of the document. * * Changed signature in okular version 0.21 */ virtual DocumentInfo generateDocumentInfo( const QSet &keys ) const; /** * Returns the 'table of content' object of the document or 0 if * no table of content is available. */ virtual const DocumentSynopsis * generateDocumentSynopsis(); /** * Returns the 'list of embedded fonts' object of the specified \page * of the document. * * \param page a page of the document, starting from 0 - -1 indicates all * the other fonts */ virtual FontInfo::List fontsForPage( int page ); /** * Returns the 'list of embedded files' object of the document or 0 if * no list of embedded files is available. */ virtual const QList * embeddedFiles() const; /** * This enum identifies the metric of the page size. */ enum PageSizeMetric { None, ///< The page size is not defined in a physical metric. Points, ///< The page size is given in 1/72 inches. Pixels ///< The page size is given in screen pixels @since 0.19 (KDE 4.13) }; /** * This method returns the metric of the page size. Default is @ref None. */ virtual PageSizeMetric pagesSizeMetric() const; /** * This method returns whether given @p action (@ref Permission) is * allowed in this document. */ virtual bool isAllowed( Permission action ) const; /** * This method is called when the orientation has been changed by the user. */ virtual void rotationChanged( Rotation orientation, Rotation oldOrientation ); /** * Returns the list of supported page sizes. */ virtual PageSize::List pageSizes() const; /** * This method is called when the page size has been changed by the user. */ virtual void pageSizeChanged( const PageSize &pageSize, const PageSize &oldPageSize ); /** * This method is called to print the document to the given @p printer. */ virtual bool print( QPrinter &printer ); /** * Possible print errors * @since 0.11 (KDE 4.5) */ enum PrintError { NoPrintError, ///< There was no print error UnknownPrintError, TemporaryFileOpenPrintError, FileConversionPrintError, PrintingProcessCrashPrintError, PrintingProcessStartPrintError, PrintToFilePrintError, InvalidPrinterStatePrintError, UnableToFindFilePrintError, NoFileToPrintError, NoBinaryToPrintError, InvalidPageSizePrintError ///< @since 0.18.2 (KDE 4.12.2) }; /** * This method returns the meta data of the given @p key with the given @p option * of the document. */ virtual QVariant metaData( const QString &key, const QVariant &option ) const; /** * Returns the list of additional supported export formats. */ virtual ExportFormat::List exportFormats() const; /** * This method is called to export the document in the given @p format and save it * under the given @p fileName. The format must be one of the supported export formats. */ virtual bool exportTo( const QString &fileName, const ExportFormat &format ); /** * This method is called to know which wallet data should be used for the given file name. * Unless you have very special requirements to where wallet data should be stored you * don't need to reimplement this method. */ virtual void walletDataForFile( const QString &fileName, QString *walletName, QString *walletFolder, QString *walletKey ) const; /** * Query for the specified @p feature. */ bool hasFeature( GeneratorFeature feature ) const; /** * Update DPI of the generator * * @since 0.19 (KDE 4.13) */ void setDPI(const QSizeF &dpi); /** * Returns the 'layers model' object of the document or NULL if * layers model is not available. * * @since 0.24 */ virtual QAbstractItemModel * layersModel() const; /** * Calls the backend to execute an BackendOpaqueAction */ virtual void opaqueAction( const BackendOpaqueAction *action ); Q_SIGNALS: /** * This signal should be emitted whenever an error occurred in the generator. * * @param message The message which should be shown to the user. * @param duration The time that the message should be shown to the user. */ void error( const QString &message, int duration ); /** * This signal should be emitted whenever the user should be warned. * * @param message The message which should be shown to the user. * @param duration The time that the message should be shown to the user. */ void warning( const QString &message, int duration ); /** * This signal should be emitted whenever the user should be noticed. * * @param message The message which should be shown to the user. * @param duration The time that the message should be shown to the user. */ void notice( const QString &message, int duration ); protected: /** * This method must be called when the pixmap request triggered by generatePixmap() * has been finished. */ void signalPixmapRequestDone( PixmapRequest * request ); /** * This method must be called when a text generation has been finished. */ void signalTextGenerationDone( Page *page, TextPage *textPage ); /** * This method is called when the document is closed and not used * any longer. * * @returns true on success, false otherwise. */ virtual bool doCloseDocument() = 0; /** * Returns the image of the page as specified in * the passed pixmap @p request. * + * Must return a null image if the request was cancelled and the generator supports cancelling + * * @warning this method may be executed in its own separated thread if the * @ref Threaded is enabled! */ virtual QImage image( PixmapRequest *page ); /** - * Returns the text page for the given @p page. + * Returns the text page for the given @p request. + * + * Must return a null pointer if the request was cancelled and the generator supports cancelling * * @warning this method may be executed in its own separated thread if the * @ref Threaded is enabled! + * + * @since 1.4 */ - virtual TextPage* textPage( Page *page ); + virtual TextPage* textPage( TextRequest *request ); /** * Returns a pointer to the document. */ const Document * document() const; /** * Toggle the @p feature . */ void setFeature( GeneratorFeature feature, bool on = true ); /** * Internal document setting */ enum DocumentMetaDataKey { PaperColorMetaData, ///< Returns (QColor) the paper color if set in Settings or the default color (white) if option is true (otherwise returns a non initialized QColor) TextAntialiasMetaData, ///< Returns (bool) text antialias from Settings (option is not used) GraphicsAntialiasMetaData, ///< Returns (bool)graphic antialias from Settings (option is not used) TextHintingMetaData ///< Returns (bool)text hinting from Settings (option is not used) }; /** * Request a meta data of the Document, if available, like an internal * setting. * * @since 1.1 */ QVariant documentMetaData( const DocumentMetaDataKey key, const QVariant &option = QVariant() ) const; /** * Request a meta data of the Document, if available, like an internal * setting. */ OKULARCORE_DEPRECATED QVariant documentMetaData( const QString &key, const QVariant &option = QVariant() ) const; /** * Return the pointer to a mutex the generator can use freely. */ QMutex* userMutex() const; /** * Set the bounding box of a page after the page has already been handed * to the Document. Call this instead of Page::setBoundingBox() to ensure * that all observers are notified. * * @since 0.7 (KDE 4.1) */ void updatePageBoundingBox( int page, const NormalizedRect & boundingBox ); /** * Returns DPI, previously set via setDPI() * @since 0.19 (KDE 4.13) */ QSizeF dpi() const; protected Q_SLOTS: /** * Gets the font data for the given font * * @since 0.8 (KDE 4.1) */ void requestFontData(const Okular::FontInfo &font, QByteArray *data); /** * Returns the last print error in case print() failed * @since 0.11 (KDE 4.5) */ Okular::Generator::PrintError printError() const; /** * This method can be called to trigger a partial pixmap update for the given request * Make sure you call it in a way it's executed in the main thread. * @since 1.3 */ void signalPartialPixmapRequest( Okular::PixmapRequest *request, const QImage &image ); protected: /// @cond PRIVATE Generator(GeneratorPrivate &dd, QObject *parent, const QVariantList &args); Q_DECLARE_PRIVATE( Generator ) GeneratorPrivate *d_ptr; friend class Document; friend class DocumentPrivate; /// @endcond PRIVATE private: Q_DISABLE_COPY( Generator ) Q_PRIVATE_SLOT( d_func(), void pixmapGenerationFinished() ) Q_PRIVATE_SLOT( d_func(), void textpageGenerationFinished() ) }; /** * @short Describes a pixmap type request. */ class OKULARCORE_EXPORT PixmapRequest { friend class Document; friend class DocumentPrivate; public: enum PixmapRequestFeature { NoFeature = 0, Asynchronous = 1, Preload = 2 }; Q_DECLARE_FLAGS( PixmapRequestFeatures, PixmapRequestFeature ) /** * Creates a new pixmap request. * * @param observer The observer. * @param pageNumber The page number. * @param width The width of the page. * @param height The height of the page. * @param priority The priority of the request. * @param features The features of generation. */ PixmapRequest( DocumentObserver *observer, int pageNumber, int width, int height, int priority, PixmapRequestFeatures features ); /** * Destroys the pixmap request. */ ~PixmapRequest(); /** * Returns the observer of the request. */ DocumentObserver *observer() const; /** * Returns the page number of the request. */ int pageNumber() const; /** * Returns the page width of the requested pixmap. */ int width() const; /** * Returns the page height of the requested pixmap. */ int height() const; /** * Returns the priority (less it better, 0 is maximum) of the * request. */ int priority() const; /** * Returns whether the generation should be done synchronous or * asynchronous. * * If asynchronous, the pixmap is created in a thread and the observer * is notified when the job is done. */ bool asynchronous() const; /** * Returns whether the generation request is for a page that is not important * i.e. it's just for speeding up future rendering */ bool preload() const; /** * Returns a pointer to the page where the pixmap shall be generated for. */ Page *page() const; /** * Sets whether the generator should render only the given normalized * rect or the entire page * * @since 0.16 (KDE 4.10) */ void setTile( bool tile ); /** * Returns whether the generator should render just the region given by * normalizedRect() or the entire page. * * @since 0.16 (KDE 4.10) */ bool isTile() const; /** * Sets the region of the page to request. * * @since 0.16 (KDE 4.10) */ void setNormalizedRect( const NormalizedRect &rect ); /** * Returns the normalized region of the page to request. * * @since 0.16 (KDE 4.10) */ const NormalizedRect& normalizedRect() const; /** * Sets whether the request should report back updates if possible * * @since 1.3 */ void setPartialUpdatesWanted(bool partialUpdatesWanted); /** * Should the request report back updates if possible? * * @since 1.3 */ bool partialUpdatesWanted() const; + /** + * Should the request be aborted if possible? + * + * @since 1.4 + */ + bool shouldAbortRender() const; + private: Q_DISABLE_COPY( PixmapRequest ) friend class PixmapRequestPrivate; PixmapRequestPrivate* const d; }; +/** + * @short Describes a text request. + * + * @since 1.4 + */ +class OKULARCORE_EXPORT TextRequest +{ + public: + /** + * Creates a new text request. + */ + TextRequest( Page *page ); + + TextRequest(); + + /** + * Destroys the pixmap request. + */ + ~TextRequest(); + + /** + * Returns a pointer to the page where the pixmap shall be generated for. + */ + Page *page() const; + + /** + * Should the request be aborted if possible? + */ + bool shouldAbortExtraction() const; + + private: + Q_DISABLE_COPY( TextRequest ) + + friend TextRequestPrivate; + TextRequestPrivate* const d; +}; + } Q_DECLARE_METATYPE(Okular::Generator::PrintError) Q_DECLARE_METATYPE(Okular::PixmapRequest*) #define OkularGeneratorInterface_iid "org.kde.okular.Generator" Q_DECLARE_INTERFACE(Okular::Generator, OkularGeneratorInterface_iid) #ifndef QT_NO_DEBUG_STREAM OKULARCORE_EXPORT QDebug operator<<( QDebug str, const Okular::PixmapRequest &req ); #endif #endif /* kate: replace-tabs on; indent-width 4; */ diff --git a/core/generator_p.cpp b/core/generator_p.cpp index 502e2b5bf..f884bf34e 100644 --- a/core/generator_p.cpp +++ b/core/generator_p.cpp @@ -1,144 +1,178 @@ /*************************************************************************** * Copyright (C) 2007 Tobias Koenig * * * * 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. * ***************************************************************************/ #include "generator_p.h" #include #include "fontinfo.h" #include "generator.h" #include "utils.h" using namespace Okular; PixmapGenerationThread::PixmapGenerationThread( Generator *generator ) : mGenerator( generator ), mRequest( nullptr ), mCalcBoundingBox( false ) { } void PixmapGenerationThread::startGeneration( PixmapRequest *request, bool calcBoundingBox ) { mRequest = request; mCalcBoundingBox = calcBoundingBox; start( QThread::InheritPriority ); } void PixmapGenerationThread::endGeneration() { mRequest = nullptr; } PixmapRequest *PixmapGenerationThread::request() const { return mRequest; } QImage PixmapGenerationThread::image() const { - return mImage; + return mRequest ? PixmapRequestPrivate::get(mRequest)->mResultImage : QImage(); } bool PixmapGenerationThread::calcBoundingBox() const { return mCalcBoundingBox; } NormalizedRect PixmapGenerationThread::boundingBox() const { return mBoundingBox; } void PixmapGenerationThread::run() { - mImage = QImage(); - if ( mRequest ) { - mImage = mGenerator->image( mRequest ); + PixmapRequestPrivate::get(mRequest)->mResultImage = mGenerator->image( mRequest ); + if ( mCalcBoundingBox ) - mBoundingBox = Utils::imageBoundingBox( &mImage ); + mBoundingBox = Utils::imageBoundingBox( &PixmapRequestPrivate::get(mRequest)->mResultImage ); } } TextPageGenerationThread::TextPageGenerationThread( Generator *generator ) - : mGenerator( generator ), mPage( nullptr ) + : mGenerator( generator ), mTextPage( nullptr ) { + TextRequestPrivate *treqPriv = TextRequestPrivate::get( &mTextRequest ); + treqPriv->mPage = nullptr; + treqPriv->mShouldAbortExtraction = 0; } -void TextPageGenerationThread::startGeneration( Page *page ) +void TextPageGenerationThread::startGeneration() { - mPage = page; - - start( QThread::InheritPriority ); + if ( page() ) + { + start( QThread::InheritPriority ); + } } void TextPageGenerationThread::endGeneration() { - mPage = nullptr; + TextRequestPrivate *treqPriv = TextRequestPrivate::get( &mTextRequest ); + treqPriv->mPage = nullptr; + treqPriv->mShouldAbortExtraction = 0; +} + +void TextPageGenerationThread::setPage( Page *page ) +{ + TextRequestPrivate *treqPriv = TextRequestPrivate::get( &mTextRequest ); + treqPriv->mPage = page; + treqPriv->mShouldAbortExtraction = 0; } Page *TextPageGenerationThread::page() const { - return mPage; + return mTextRequest.page(); } TextPage* TextPageGenerationThread::textPage() const { return mTextPage; } +void TextPageGenerationThread::abortExtraction() +{ + // If extraction already finished no point in aborting + if ( !mTextPage ) + { + TextRequestPrivate *treqPriv = TextRequestPrivate::get( &mTextRequest ); + treqPriv->mShouldAbortExtraction = 1; + } +} + +bool TextPageGenerationThread::shouldAbortExtraction() const +{ + return mTextRequest.shouldAbortExtraction(); +} + void TextPageGenerationThread::run() { mTextPage = nullptr; - if ( mPage ) - mTextPage = mGenerator->textPage( mPage ); + Q_ASSERT ( page() ); + + mTextPage = mGenerator->textPage( &mTextRequest ); + + if ( mTextRequest.shouldAbortExtraction() ) + { + delete mTextPage; + mTextPage = nullptr; + } } FontExtractionThread::FontExtractionThread( Generator *generator, int pages ) : mGenerator( generator ), mNumOfPages( pages ), mGoOn( true ) { } void FontExtractionThread::startExtraction( bool async ) { if ( async ) { connect(this, &FontExtractionThread::finished, this, &FontExtractionThread::deleteLater); start( QThread::InheritPriority ); } else { run(); deleteLater(); } } void FontExtractionThread::stopExtraction() { mGoOn = false; } void FontExtractionThread::run() { for ( int i = -1; i < mNumOfPages && mGoOn; ++i ) { FontInfo::List list = mGenerator->fontsForPage( i ); foreach ( const FontInfo& fi, list ) { emit gotFont( fi ); } emit progress( i ); } } #include "moc_generator_p.cpp" diff --git a/core/generator_p.h b/core/generator_p.h index 616e8fc53..8e1f05e47 100644 --- a/core/generator_p.h +++ b/core/generator_p.h @@ -1,178 +1,196 @@ /*************************************************************************** * Copyright (C) 2007 Tobias Koenig * * Copyright (C) 2017 Klarälvdalens Datakonsult AB, a KDAB Group * * company, info@kdab.com. Work sponsored by the * * LiMux project of the city of Munich * * * * 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. * ***************************************************************************/ #ifndef OKULAR_THREADEDGENERATOR_P_H #define OKULAR_THREADEDGENERATOR_P_H #include "area.h" #include #include #include class QEventLoop; class QMutex; +#include "generator.h" #include "page.h" namespace Okular { class DocumentObserver; class DocumentPrivate; class FontInfo; class Generator; class Page; class PixmapGenerationThread; class PixmapRequest; class TextPage; class TextPageGenerationThread; class TilesManager; class GeneratorPrivate { public: GeneratorPrivate(); virtual ~GeneratorPrivate(); Q_DECLARE_PUBLIC( Generator ) Generator *q_ptr; PixmapGenerationThread* pixmapGenerationThread(); TextPageGenerationThread* textPageGenerationThread(); void pixmapGenerationFinished(); void textpageGenerationFinished(); QMutex* threadsLock(); virtual QVariant metaData( const QString &key, const QVariant &option ) const; virtual QImage image( PixmapRequest * ); DocumentPrivate *m_document; // NOTE: the following should be a QSet< GeneratorFeature >, // but it is not to avoid #include'ing generator.h QSet< int > m_features; PixmapGenerationThread *mPixmapGenerationThread; TextPageGenerationThread *mTextPageGenerationThread; mutable QMutex *m_mutex; QMutex *m_threadsMutex; bool mPixmapReady : 1; bool mTextPageReady : 1; bool m_closing : 1; QEventLoop *m_closingLoop; QSizeF m_dpi; }; class PixmapRequestPrivate { public: void swap(); TilesManager *tilesManager() const; + static PixmapRequestPrivate *get(const PixmapRequest *req); + DocumentObserver *mObserver; int mPageNumber; int mWidth; int mHeight; int mPriority; int mFeatures; bool mForce : 1; bool mTile : 1; bool mPartialUpdatesWanted : 1; Page *mPage; NormalizedRect mNormalizedRect; + QAtomicInt mShouldAbortRender; + QImage mResultImage; +}; + + +class TextRequestPrivate +{ + public: + static TextRequestPrivate *get(const TextRequest *req); + + Page *mPage; + QAtomicInt mShouldAbortExtraction; }; class PixmapGenerationThread : public QThread { Q_OBJECT public: PixmapGenerationThread( Generator *generator ); void startGeneration( PixmapRequest *request, bool calcBoundingRect ); void endGeneration(); PixmapRequest *request() const; QImage image() const; bool calcBoundingBox() const; NormalizedRect boundingBox() const; protected: void run() override; private: Generator *mGenerator; PixmapRequest *mRequest; - QImage mImage; NormalizedRect mBoundingBox; bool mCalcBoundingBox : 1; }; class TextPageGenerationThread : public QThread { Q_OBJECT public: TextPageGenerationThread( Generator *generator ); void endGeneration(); + void setPage( Page *page ); Page *page() const; TextPage* textPage() const; + void abortExtraction(); + bool shouldAbortExtraction() const; + public slots: - void startGeneration( Okular::Page *page ); + void startGeneration(); protected: void run() override; private: Generator *mGenerator; - Page *mPage; TextPage *mTextPage; + TextRequest mTextRequest; }; class FontExtractionThread : public QThread { Q_OBJECT public: FontExtractionThread( Generator *generator, int pages ); void startExtraction( bool async ); void stopExtraction(); Q_SIGNALS: void gotFont( const Okular::FontInfo& ); void progress( int page ); protected: void run() override; private: Generator *mGenerator; int mNumOfPages; bool mGoOn; }; } Q_DECLARE_METATYPE(Okular::Page*) #endif diff --git a/core/page.cpp b/core/page.cpp index 33827984a..c5ca06b62 100644 --- a/core/page.cpp +++ b/core/page.cpp @@ -1,1123 +1,1129 @@ /*************************************************************************** * Copyright (C) 2004 by Enrico Ros * * Copyright (C) 2017 Klarälvdalens Datakonsult AB, a KDAB Group * * company, info@kdab.com. Work sponsored by the * * LiMux project of the city of Munich * * * * 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. * ***************************************************************************/ #include "page.h" #include "page_p.h" // qt/kde includes #include #include #include #include #include #include #include #include #include // local includes #include "action.h" #include "annotations.h" #include "annotations_p.h" #include "area.h" #include "debug_p.h" #include "document.h" #include "document_p.h" #include "form.h" #include "form_p.h" #include "observer.h" #include "pagecontroller_p.h" #include "pagesize.h" #include "pagetransition.h" #include "rotationjob_p.h" #include "textpage.h" #include "textpage_p.h" #include "tile.h" #include "tilesmanager_p.h" #include "utils_p.h" #include #ifdef PAGE_PROFILE #include #endif using namespace Okular; static const double distanceConsideredEqual = 25; // 5px static void deleteObjectRects( QLinkedList< ObjectRect * >& rects, const QSet& which ) { QLinkedList< ObjectRect * >::iterator it = rects.begin(), end = rects.end(); for ( ; it != end; ) if ( which.contains( (*it)->objectType() ) ) { delete *it; it = rects.erase( it ); } else ++it; } PagePrivate::PagePrivate( Page *page, uint n, double w, double h, Rotation o ) : m_page( page ), m_number( n ), m_orientation( o ), m_width( w ), m_height( h ), m_doc( nullptr ), m_boundingBox( 0, 0, 1, 1 ), m_rotation( Rotation0 ), m_text( nullptr ), m_transition( nullptr ), m_textSelections( nullptr ), m_openingAction( nullptr ), m_closingAction( nullptr ), m_duration( -1 ), m_isBoundingBoxKnown( false ) { // avoid Division-By-Zero problems in the program if ( m_width <= 0 ) m_width = 1; if ( m_height <= 0 ) m_height = 1; } PagePrivate::~PagePrivate() { qDeleteAll( formfields ); delete m_openingAction; delete m_closingAction; delete m_text; delete m_transition; } PagePrivate *PagePrivate::get( Page * page ) { return page ? page->d : nullptr; } void PagePrivate::imageRotationDone( RotationJob * job ) { TilesManager *tm = tilesManager( job->observer() ); if ( tm ) { QPixmap *pixmap = new QPixmap( QPixmap::fromImage( job->image() ) ); - tm->setPixmap( pixmap, job->rect() ); + tm->setPixmap( pixmap, job->rect(), job->isPartialUpdate() ); delete pixmap; return; } QMap< DocumentObserver*, PixmapObject >::iterator it = m_pixmaps.find( job->observer() ); if ( it != m_pixmaps.end() ) { PixmapObject &object = it.value(); (*object.m_pixmap) = QPixmap::fromImage( job->image() ); object.m_rotation = job->rotation(); } else { PixmapObject object; object.m_pixmap = new QPixmap( QPixmap::fromImage( job->image() ) ); object.m_rotation = job->rotation(); m_pixmaps.insert( job->observer(), object ); } } QTransform PagePrivate::rotationMatrix() const { return Okular::buildRotationMatrix( m_rotation ); } /** class Page **/ Page::Page( uint page, double w, double h, Rotation o ) : d( new PagePrivate( this, page, w, h, o ) ) { } Page::~Page() { if (d) { deletePixmaps(); deleteRects(); d->deleteHighlights(); deleteAnnotations(); d->deleteTextSelections(); deleteSourceReferences(); delete d; } } int Page::number() const { return d->m_number; } Rotation Page::orientation() const { return d->m_orientation; } Rotation Page::rotation() const { return d->m_rotation; } Rotation Page::totalOrientation() const { return (Rotation)( ( (int)d->m_orientation + (int)d->m_rotation ) % 4 ); } double Page::width() const { return d->m_width; } double Page::height() const { return d->m_height; } double Page::ratio() const { return d->m_height / d->m_width; } NormalizedRect Page::boundingBox() const { return d->m_boundingBox; } bool Page::isBoundingBoxKnown() const { return d->m_isBoundingBoxKnown; } void Page::setBoundingBox( const NormalizedRect& bbox ) { if ( d->m_isBoundingBoxKnown && d->m_boundingBox == bbox ) return; // Allow tiny rounding errors (happens during rotation) static const double epsilon = 0.00001; Q_ASSERT( bbox.left >= -epsilon && bbox.top >= -epsilon && bbox.right <= 1 + epsilon && bbox.bottom <= 1 + epsilon ); d->m_boundingBox = bbox & NormalizedRect( 0., 0., 1., 1. ); d->m_isBoundingBoxKnown = true; } bool Page::hasPixmap( DocumentObserver *observer, int width, int height, const NormalizedRect &rect ) const { TilesManager *tm = d->tilesManager( observer ); if ( tm ) { if ( width != tm->width() || height != tm->height() ) { // FIXME hasPixmap should not be calling setSize on the TilesManager this is not very "const" // as this function claims to be if ( width != -1 && height != -1 ) { tm->setSize( width, height ); } return false; } return tm->hasPixmap( rect ); } QMap< DocumentObserver*, PagePrivate::PixmapObject >::const_iterator it = d->m_pixmaps.constFind( observer ); if ( it == d->m_pixmaps.constEnd() ) return false; if ( width == -1 || height == -1 ) return true; const QPixmap *pixmap = it.value().m_pixmap; return (pixmap->width() == width && pixmap->height() == height); } bool Page::hasTextPage() const { return d->m_text != nullptr; } RegularAreaRect * Page::wordAt( const NormalizedPoint &p, QString *word ) const { if ( d->m_text ) return d->m_text->wordAt( p, word ); return nullptr; } RegularAreaRect * Page::textArea ( TextSelection * selection ) const { if ( d->m_text ) return d->m_text->textArea( selection ); return nullptr; } bool Page::hasObjectRect( double x, double y, double xScale, double yScale ) const { if ( m_rects.isEmpty() ) return false; QLinkedList< ObjectRect * >::const_iterator it = m_rects.begin(), end = m_rects.end(); for ( ; it != end; ++it ) if ( (*it)->distanceSqr( x, y, xScale, yScale ) < distanceConsideredEqual ) return true; return false; } bool Page::hasHighlights( int s_id ) const { // simple case: have no highlights if ( m_highlights.isEmpty() ) return false; // simple case: we have highlights and no id to match if ( s_id == -1 ) return true; // iterate on the highlights list to find an entry by id QLinkedList< HighlightAreaRect * >::const_iterator it = m_highlights.begin(), end = m_highlights.end(); for ( ; it != end; ++it ) if ( (*it)->s_id == s_id ) return true; return false; } bool Page::hasTransition() const { return d->m_transition != nullptr; } bool Page::hasAnnotations() const { return !m_annotations.isEmpty(); } RegularAreaRect * Page::findText( int id, const QString & text, SearchDirection direction, Qt::CaseSensitivity caseSensitivity, const RegularAreaRect *lastRect ) const { RegularAreaRect* rect = nullptr; if ( text.isEmpty() || !d->m_text ) return rect; rect = d->m_text->findText( id, text, direction, caseSensitivity, lastRect ); return rect; } QString Page::text( const RegularAreaRect * area ) const { return text( area, TextPage::AnyPixelTextAreaInclusionBehaviour ); } QString Page::text( const RegularAreaRect * area, TextPage::TextAreaInclusionBehaviour b ) const { QString ret; if ( !d->m_text ) return ret; if ( area ) { RegularAreaRect rotatedArea = *area; rotatedArea.transform( d->rotationMatrix().inverted() ); ret = d->m_text->text( &rotatedArea, b ); } else ret = d->m_text->text( nullptr, b ); return ret; } TextEntity::List Page::words( const RegularAreaRect * area, TextPage::TextAreaInclusionBehaviour b ) const { TextEntity::List ret; if ( !d->m_text ) return ret; if ( area ) { RegularAreaRect rotatedArea = *area; rotatedArea.transform( d->rotationMatrix().inverted() ); ret = d->m_text->words( &rotatedArea, b ); } else ret = d->m_text->words( nullptr, b ); for (int i = 0; i < ret.length(); ++i) { const TextEntity * orig = ret[i]; ret[i] = new TextEntity( orig->text(), new Okular::NormalizedRect(orig->transformedArea ( d->rotationMatrix() )) ); delete orig; } return ret; } void PagePrivate::rotateAt( Rotation orientation ) { if ( orientation == m_rotation ) return; deleteHighlights(); deleteTextSelections(); if ( ( (int)m_orientation + (int)m_rotation ) % 2 != ( (int)m_orientation + (int)orientation ) % 2 ) qSwap( m_width, m_height ); Rotation oldRotation = m_rotation; m_rotation = orientation; /** * Rotate the images of the page. */ QMapIterator< DocumentObserver*, PagePrivate::PixmapObject > it( m_pixmaps ); while ( it.hasNext() ) { it.next(); const PagePrivate::PixmapObject &object = it.value(); RotationJob *job = new RotationJob( object.m_pixmap->toImage(), object.m_rotation, m_rotation, it.key() ); job->setPage( this ); m_doc->m_pageController->addRotationJob(job); } /** * Rotate tiles manager */ QMapIterator i(m_tilesManagers); while (i.hasNext()) { i.next(); TilesManager *tm = i.value(); if ( tm ) tm->setRotation( m_rotation ); } /** * Rotate the object rects on the page. */ const QTransform matrix = rotationMatrix(); QLinkedList< ObjectRect * >::const_iterator objectIt = m_page->m_rects.begin(), end = m_page->m_rects.end(); for ( ; objectIt != end; ++objectIt ) (*objectIt)->transform( matrix ); QLinkedList< HighlightAreaRect* >::const_iterator hlIt = m_page->m_highlights.begin(), hlItEnd = m_page->m_highlights.end(); for ( ; hlIt != hlItEnd; ++hlIt ) { (*hlIt)->transform( RotationJob::rotationMatrix( oldRotation, m_rotation ) ); } } void PagePrivate::changeSize( const PageSize &size ) { if ( size.isNull() || ( size.width() == m_width && size.height() == m_height ) ) return; m_page->deletePixmaps(); // deleteHighlights(); // deleteTextSelections(); m_width = size.width(); m_height = size.height(); if ( m_rotation % 2 ) qSwap( m_width, m_height ); } const ObjectRect * Page::objectRect( ObjectRect::ObjectType type, double x, double y, double xScale, double yScale ) const { // Walk list in reverse order so that annotations in the foreground are preferred QLinkedListIterator< ObjectRect * > it( m_rects ); it.toBack(); while ( it.hasPrevious() ) { const ObjectRect *objrect = it.previous(); if ( ( objrect->objectType() == type ) && objrect->distanceSqr( x, y, xScale, yScale ) < distanceConsideredEqual ) return objrect; } return nullptr; } QLinkedList< const ObjectRect * > Page::objectRects( ObjectRect::ObjectType type, double x, double y, double xScale, double yScale ) const { QLinkedList< const ObjectRect * > result; QLinkedListIterator< ObjectRect * > it( m_rects ); it.toBack(); while ( it.hasPrevious() ) { const ObjectRect *objrect = it.previous(); if ( ( objrect->objectType() == type ) && objrect->distanceSqr( x, y, xScale, yScale ) < distanceConsideredEqual ) result.append( objrect ); } return result; } const ObjectRect* Page::nearestObjectRect( ObjectRect::ObjectType type, double x, double y, double xScale, double yScale, double * distance ) const { ObjectRect * res = nullptr; double minDistance = std::numeric_limits::max(); QLinkedList< ObjectRect * >::const_iterator it = m_rects.constBegin(), end = m_rects.constEnd(); for ( ; it != end; ++it ) { if ( (*it)->objectType() == type ) { double d = (*it)->distanceSqr( x, y, xScale, yScale ); if ( d < minDistance ) { res = (*it); minDistance = d; } } } if ( distance ) *distance = minDistance; return res; } const PageTransition * Page::transition() const { return d->m_transition; } QLinkedList< Annotation* > Page::annotations() const { return m_annotations; } Annotation * Page::annotation( const QString & uniqueName ) const { foreach(Annotation *a, m_annotations) { if ( a->uniqueName() == uniqueName ) return a; } return nullptr; } const Action * Page::pageAction( PageAction action ) const { switch ( action ) { case Page::Opening: return d->m_openingAction; break; case Page::Closing: return d->m_closingAction; break; } return nullptr; } QLinkedList< FormField * > Page::formFields() const { return d->formfields; } void Page::setPixmap( DocumentObserver *observer, QPixmap *pixmap, const NormalizedRect &rect ) { - if ( d->m_rotation == Rotation0 ) { - TilesManager *tm = d->tilesManager( observer ); + d->setPixmap( observer, pixmap, rect, false /*isPartialPixmap*/ ); +} + +void PagePrivate::setPixmap( DocumentObserver *observer, QPixmap *pixmap, const NormalizedRect &rect, bool isPartialPixmap ) +{ + if ( m_rotation == Rotation0 ) { + TilesManager *tm = tilesManager( observer ); if ( tm ) { - tm->setPixmap( pixmap, rect ); + tm->setPixmap( pixmap, rect, isPartialPixmap ); delete pixmap; return; } - QMap< DocumentObserver*, PagePrivate::PixmapObject >::iterator it = d->m_pixmaps.find( observer ); - if ( it != d->m_pixmaps.end() ) + QMap< DocumentObserver*, PagePrivate::PixmapObject >::iterator it = m_pixmaps.find( observer ); + if ( it != m_pixmaps.end() ) { delete it.value().m_pixmap; } else { - it = d->m_pixmaps.insert( observer, PagePrivate::PixmapObject() ); + it = m_pixmaps.insert( observer, PagePrivate::PixmapObject() ); } it.value().m_pixmap = pixmap; - it.value().m_rotation = d->m_rotation; + it.value().m_rotation = m_rotation; } else { // it can happen that we get a setPixmap while closing and thus the page controller is gone - if ( d->m_doc->m_pageController ) + if ( m_doc->m_pageController ) { - RotationJob *job = new RotationJob( pixmap->toImage(), Rotation0, d->m_rotation, observer ); - job->setPage( d ); - job->setRect( TilesManager::toRotatedRect( rect, d->m_rotation ) ); - d->m_doc->m_pageController->addRotationJob(job); + RotationJob *job = new RotationJob( pixmap->toImage(), Rotation0, m_rotation, observer ); + job->setPage( this ); + job->setRect( TilesManager::toRotatedRect( rect, m_rotation ) ); + job->setIsPartialUpdate( isPartialPixmap ); + m_doc->m_pageController->addRotationJob(job); } delete pixmap; } } void Page::setTextPage( TextPage * textPage ) { delete d->m_text; d->m_text = textPage; if ( d->m_text ) { d->m_text->d->m_page = this; /** * Correct text order for before text selection */ d->m_text->d->correctTextOrder(); } } void Page::setObjectRects( const QLinkedList< ObjectRect * > & rects ) { QSet which; which << ObjectRect::Action << ObjectRect::Image; deleteObjectRects( m_rects, which ); /** * Rotate the object rects of the page. */ const QTransform matrix = d->rotationMatrix(); QLinkedList< ObjectRect * >::const_iterator objectIt = rects.begin(), end = rects.end(); for ( ; objectIt != end; ++objectIt ) (*objectIt)->transform( matrix ); m_rects << rects; } void PagePrivate::setHighlight( int s_id, RegularAreaRect *rect, const QColor & color ) { HighlightAreaRect * hr = new HighlightAreaRect(rect); hr->s_id = s_id; hr->color = color; m_page->m_highlights.append( hr ); } void PagePrivate::setTextSelections( RegularAreaRect *r, const QColor & color ) { deleteTextSelections(); if ( r ) { HighlightAreaRect * hr = new HighlightAreaRect( r ); hr->s_id = -1; hr->color = color; m_textSelections = hr; delete r; } } void Page::setSourceReferences( const QLinkedList< SourceRefObjectRect * > & refRects ) { deleteSourceReferences(); foreach( SourceRefObjectRect * rect, refRects ) m_rects << rect; } void Page::setDuration( double seconds ) { d->m_duration = seconds; } double Page::duration() const { return d->m_duration; } void Page::setLabel( const QString& label ) { d->m_label = label; } QString Page::label() const { return d->m_label; } const RegularAreaRect * Page::textSelection() const { return d->m_textSelections; } QColor Page::textSelectionColor() const { return d->m_textSelections ? d->m_textSelections->color : QColor(); } void Page::addAnnotation( Annotation * annotation ) { // Generate uniqueName: okular-{UUID} if(annotation->uniqueName().isEmpty()) { QString uniqueName = QStringLiteral("okular-") + QUuid::createUuid().toString(); annotation->setUniqueName( uniqueName ); } annotation->d_ptr->m_page = d; m_annotations.append( annotation ); AnnotationObjectRect *rect = new AnnotationObjectRect( annotation ); // Rotate the annotation on the page. const QTransform matrix = d->rotationMatrix(); annotation->d_ptr->annotationTransform( matrix ); m_rects.append( rect ); } bool Page::removeAnnotation( Annotation * annotation ) { if ( !d->m_doc->m_parent->canRemovePageAnnotation(annotation) ) return false; QLinkedList< Annotation * >::iterator aIt = m_annotations.begin(), aEnd = m_annotations.end(); for ( ; aIt != aEnd; ++aIt ) { if((*aIt) && (*aIt)->uniqueName()==annotation->uniqueName()) { int rectfound = false; QLinkedList< ObjectRect * >::iterator it = m_rects.begin(), end = m_rects.end(); for ( ; it != end && !rectfound; ++it ) if ( ( (*it)->objectType() == ObjectRect::OAnnotation ) && ( (*it)->object() == (*aIt) ) ) { delete *it; it = m_rects.erase( it ); rectfound = true; } qCDebug(OkularCoreDebug) << "removed annotation:" << annotation->uniqueName(); annotation->d_ptr->m_page = nullptr; m_annotations.erase( aIt ); break; } } return true; } void Page::setTransition( PageTransition * transition ) { delete d->m_transition; d->m_transition = transition; } void Page::setPageAction( PageAction action, Action * link ) { switch ( action ) { case Page::Opening: delete d->m_openingAction; d->m_openingAction = link; break; case Page::Closing: delete d->m_closingAction; d->m_closingAction = link; break; } } void Page::setFormFields( const QLinkedList< FormField * >& fields ) { qDeleteAll( d->formfields ); d->formfields = fields; QLinkedList< FormField * >::const_iterator it = d->formfields.begin(), itEnd = d->formfields.end(); for ( ; it != itEnd; ++it ) { (*it)->d_ptr->setDefault(); } } void Page::deletePixmap( DocumentObserver *observer ) { TilesManager *tm = d->tilesManager( observer ); if ( tm ) { delete tm; d->m_tilesManagers.remove(observer); } else { PagePrivate::PixmapObject object = d->m_pixmaps.take( observer ); delete object.m_pixmap; } } void Page::deletePixmaps() { QMapIterator< DocumentObserver*, PagePrivate::PixmapObject > it( d->m_pixmaps ); while ( it.hasNext() ) { it.next(); delete it.value().m_pixmap; } d->m_pixmaps.clear(); qDeleteAll(d->m_tilesManagers); d->m_tilesManagers.clear(); } void Page::deleteRects() { // delete ObjectRects of type Link and Image QSet which; which << ObjectRect::Action << ObjectRect::Image; deleteObjectRects( m_rects, which ); } void PagePrivate::deleteHighlights( int s_id ) { // delete highlights by ID QLinkedList< HighlightAreaRect* >::iterator it = m_page->m_highlights.begin(), end = m_page->m_highlights.end(); while ( it != end ) { HighlightAreaRect* highlight = *it; if ( s_id == -1 || highlight->s_id == s_id ) { it = m_page->m_highlights.erase( it ); delete highlight; } else ++it; } } void PagePrivate::deleteTextSelections() { delete m_textSelections; m_textSelections = nullptr; } void Page::deleteSourceReferences() { deleteObjectRects( m_rects, QSet() << ObjectRect::SourceRef ); } void Page::deleteAnnotations() { // delete ObjectRects of type Annotation deleteObjectRects( m_rects, QSet() << ObjectRect::OAnnotation ); // delete all stored annotations QLinkedList< Annotation * >::const_iterator aIt = m_annotations.begin(), aEnd = m_annotations.end(); for ( ; aIt != aEnd; ++aIt ) delete *aIt; m_annotations.clear(); } bool PagePrivate::restoreLocalContents( const QDomNode & pageNode ) { bool loadedAnything = false; // set if something actually gets loaded // iterate over all chilren (annotationList, ...) QDomNode childNode = pageNode.firstChild(); while ( childNode.isElement() ) { QDomElement childElement = childNode.toElement(); childNode = childNode.nextSibling(); // parse annotationList child element if ( childElement.tagName() == QLatin1String("annotationList") ) { #ifdef PAGE_PROFILE QTime time; time.start(); #endif // Clone annotationList as root node in restoredLocalAnnotationList const QDomNode clonedNode = restoredLocalAnnotationList.importNode( childElement, true ); restoredLocalAnnotationList.appendChild( clonedNode ); // iterate over all annotations QDomNode annotationNode = childElement.firstChild(); while( annotationNode.isElement() ) { // get annotation element and advance to next annot QDomElement annotElement = annotationNode.toElement(); annotationNode = annotationNode.nextSibling(); // get annotation from the dom element Annotation * annotation = AnnotationUtils::createAnnotation( annotElement ); // append annotation to the list or show warning if ( annotation ) { m_doc->performAddPageAnnotation(m_number, annotation); qCDebug(OkularCoreDebug) << "restored annot:" << annotation->uniqueName(); loadedAnything = true; } else qCWarning(OkularCoreDebug).nospace() << "page (" << m_number << "): can't restore an annotation from XML."; } #ifdef PAGE_PROFILE qCDebug(OkularCoreDebug).nospace() << "annots: XML Load time: " << time.elapsed() << "ms"; #endif } // parse formList child element else if ( childElement.tagName() == QLatin1String("forms") ) { // Clone forms as root node in restoredFormFieldList const QDomNode clonedNode = restoredFormFieldList.importNode( childElement, true ); restoredFormFieldList.appendChild( clonedNode ); if ( formfields.isEmpty() ) continue; QHash hashedforms; QLinkedList< FormField * >::const_iterator fIt = formfields.begin(), fItEnd = formfields.end(); for ( ; fIt != fItEnd; ++fIt ) { hashedforms[(*fIt)->id()] = (*fIt); } // iterate over all forms QDomNode formsNode = childElement.firstChild(); while( formsNode.isElement() ) { // get annotation element and advance to next annot QDomElement formElement = formsNode.toElement(); formsNode = formsNode.nextSibling(); if ( formElement.tagName() != QLatin1String("form") ) continue; bool ok = true; int index = formElement.attribute( QStringLiteral("id") ).toInt( &ok ); if ( !ok ) continue; QHash::const_iterator wantedIt = hashedforms.constFind( index ); if ( wantedIt == hashedforms.constEnd() ) continue; QString value = formElement.attribute( QStringLiteral("value") ); (*wantedIt)->d_ptr->setValue( value ); loadedAnything = true; } } } return loadedAnything; } void PagePrivate::saveLocalContents( QDomNode & parentNode, QDomDocument & document, PageItems what ) const { // create the page node and set the 'number' attribute QDomElement pageElement = document.createElement( QStringLiteral("page") ); pageElement.setAttribute( QStringLiteral("number"), m_number ); #if 0 // add bookmark info if is bookmarked if ( d->m_bookmarked ) { // create the pageElement's 'bookmark' child QDomElement bookmarkElement = document.createElement( "bookmark" ); pageElement.appendChild( bookmarkElement ); // add attributes to the element //bookmarkElement.setAttribute( "name", bookmark name ); } #endif // add annotations info if has got any if ( ( what & AnnotationPageItems ) && ( what & OriginalAnnotationPageItems ) ) { const QDomElement savedDocRoot = restoredLocalAnnotationList.documentElement(); if ( !savedDocRoot.isNull() ) { // Import and append node in target document const QDomNode importedNode = document.importNode( savedDocRoot, true ); pageElement.appendChild( importedNode ); } } else if ( ( what & AnnotationPageItems ) && !m_page->m_annotations.isEmpty() ) { // create the annotationList QDomElement annotListElement = document.createElement( QStringLiteral("annotationList") ); // add every annotation to the annotationList QLinkedList< Annotation * >::const_iterator aIt = m_page->m_annotations.constBegin(), aEnd = m_page->m_annotations.constEnd(); for ( ; aIt != aEnd; ++aIt ) { // get annotation const Annotation * a = *aIt; // only save okular annotations (not the embedded in file ones) if ( !(a->flags() & Annotation::External) ) { // append an filled-up element called 'annotation' to the list QDomElement annElement = document.createElement( QStringLiteral("annotation") ); AnnotationUtils::storeAnnotation( a, annElement, document ); annotListElement.appendChild( annElement ); qCDebug(OkularCoreDebug) << "save annotation:" << a->uniqueName(); } } // append the annotationList element if annotations have been set if ( annotListElement.hasChildNodes() ) pageElement.appendChild( annotListElement ); } // add forms info if has got any if ( ( what & FormFieldPageItems ) && ( what & OriginalFormFieldPageItems ) ) { const QDomElement savedDocRoot = restoredFormFieldList.documentElement(); if ( !savedDocRoot.isNull() ) { // Import and append node in target document const QDomNode importedNode = document.importNode( savedDocRoot, true ); pageElement.appendChild( importedNode ); } } else if ( ( what & FormFieldPageItems ) && !formfields.isEmpty() ) { // create the formList QDomElement formListElement = document.createElement( QStringLiteral("forms") ); // add every form data to the formList QLinkedList< FormField * >::const_iterator fIt = formfields.constBegin(), fItEnd = formfields.constEnd(); for ( ; fIt != fItEnd; ++fIt ) { // get the form field const FormField * f = *fIt; QString newvalue = f->d_ptr->value(); if ( f->d_ptr->m_default == newvalue ) continue; // append an filled-up element called 'annotation' to the list QDomElement formElement = document.createElement( QStringLiteral("form") ); formElement.setAttribute( QStringLiteral("id"), f->id() ); formElement.setAttribute( QStringLiteral("value"), newvalue ); formListElement.appendChild( formElement ); } // append the annotationList element if annotations have been set if ( formListElement.hasChildNodes() ) pageElement.appendChild( formListElement ); } // append the page element only if has children if ( pageElement.hasChildNodes() ) parentNode.appendChild( pageElement ); } const QPixmap * Page::_o_nearestPixmap( DocumentObserver *observer, int w, int h ) const { Q_UNUSED( h ) const QPixmap * pixmap = nullptr; // if a pixmap is present for given id, use it QMap< DocumentObserver*, PagePrivate::PixmapObject >::const_iterator itPixmap = d->m_pixmaps.constFind( observer ); if ( itPixmap != d->m_pixmaps.constEnd() ) pixmap = itPixmap.value().m_pixmap; // else find the closest match using pixmaps of other IDs (great optim!) else if ( !d->m_pixmaps.isEmpty() ) { int minDistance = -1; QMap< DocumentObserver*, PagePrivate::PixmapObject >::const_iterator it = d->m_pixmaps.constBegin(), end = d->m_pixmaps.constEnd(); for ( ; it != end; ++it ) { int pixWidth = (*it).m_pixmap->width(), distance = pixWidth > w ? pixWidth - w : w - pixWidth; if ( minDistance == -1 || distance < minDistance ) { pixmap = (*it).m_pixmap; minDistance = distance; } } } return pixmap; } bool Page::hasTilesManager( const DocumentObserver *observer ) const { return d->tilesManager( observer ) != nullptr; } QList Page::tilesAt( const DocumentObserver *observer, const NormalizedRect &rect ) const { TilesManager *tm = d->m_tilesManagers.value( observer ); if ( tm ) return tm->tilesAt( rect, TilesManager::PixmapTile ); else return QList(); } TilesManager *PagePrivate::tilesManager( const DocumentObserver *observer ) const { return m_tilesManagers.value( observer ); } void PagePrivate::setTilesManager( const DocumentObserver *observer, TilesManager *tm ) { TilesManager *old = m_tilesManagers.value( observer ); delete old; m_tilesManagers.insert(observer, tm); } void PagePrivate::adoptGeneratedContents( PagePrivate *oldPage ) { rotateAt( oldPage->m_rotation ); m_pixmaps = oldPage->m_pixmaps; oldPage->m_pixmaps.clear(); m_tilesManagers = oldPage->m_tilesManagers; oldPage->m_tilesManagers.clear(); m_boundingBox = oldPage->m_boundingBox; m_isBoundingBoxKnown = oldPage->m_isBoundingBoxKnown; m_text = oldPage->m_text; oldPage->m_text = nullptr; m_textSelections = oldPage->m_textSelections; oldPage->m_textSelections = nullptr; restoredLocalAnnotationList = oldPage->restoredLocalAnnotationList; restoredFormFieldList = oldPage->restoredFormFieldList; } FormField *PagePrivate::findEquivalentForm( const Page *p, FormField *oldField ) { // given how id is not very good of id (at least for pdf) we do a few passes // same rect, type and id foreach(FormField *f, p->d->formfields) { if (f->rect() == oldField->rect() && f->type() == oldField->type() && f->id() == oldField->id()) return f; } // same rect and type foreach(FormField *f, p->d->formfields) { if (f->rect() == oldField->rect() && f->type() == oldField->type()) return f; } // fuzzy rect, same type and id foreach(FormField *f, p->d->formfields) { if (f->type() == oldField->type() && f->id() == oldField->id() && qFuzzyCompare(f->rect().left, oldField->rect().left) && qFuzzyCompare(f->rect().top, oldField->rect().top) && qFuzzyCompare(f->rect().right, oldField->rect().right) && qFuzzyCompare(f->rect().bottom, oldField->rect().bottom)) { return f; } } // fuzzy rect and same type foreach(FormField *f, p->d->formfields) { if (f->type() == oldField->type() && qFuzzyCompare(f->rect().left, oldField->rect().left) && qFuzzyCompare(f->rect().top, oldField->rect().top) && qFuzzyCompare(f->rect().right, oldField->rect().right) && qFuzzyCompare(f->rect().bottom, oldField->rect().bottom)) { return f; } } return nullptr; } diff --git a/core/page_p.h b/core/page_p.h index 4b7831c4c..c456f485d 100644 --- a/core/page_p.h +++ b/core/page_p.h @@ -1,172 +1,174 @@ /*************************************************************************** * Copyright (C) 2004 by Enrico Ros * * Copyright (C) 2007 by Pino Toscano * * Copyright (C) 2017 Klarälvdalens Datakonsult AB, a KDAB Group * * company, info@kdab.com. Work sponsored by the * * LiMux project of the city of Munich * * * * 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. * ***************************************************************************/ #ifndef _OKULAR_PAGE_PRIVATE_H_ #define _OKULAR_PAGE_PRIVATE_H_ // qt/kde includes #include #include #include #include #include // local includes #include "global.h" #include "area.h" class QColor; namespace Okular { class Action; class Annotation; class DocumentObserver; class DocumentPrivate; class FormField; class HighlightAreaRect; class Page; class PageSize; class PageTransition; class RotationJob; class TextPage; class TilesManager; enum PageItem { None = 0, AnnotationPageItems = 0x01, FormFieldPageItems = 0x02, AllPageItems = 0xff, /* If set along with AnnotationPageItems, tells saveLocalContents to save * the original annotations (if any) instead of the modified ones */ OriginalAnnotationPageItems = 0x100, /* If set along with FormFieldPageItems, tells saveLocalContents to save * the original form contents (if any) instead of the modified one */ OriginalFormFieldPageItems = 0x200 }; Q_DECLARE_FLAGS(PageItems, PageItem) class PagePrivate { public: PagePrivate( Page *page, uint n, double w, double h, Rotation o ); ~PagePrivate(); static PagePrivate *get( Page *page ); void imageRotationDone( RotationJob * job ); QTransform rotationMatrix() const; /** * Loads the local contents (e.g. annotations) of the page. */ bool restoreLocalContents( const QDomNode & pageNode ); /** * Saves the local contents (e.g. annotations) of the page. */ void saveLocalContents( QDomNode & parentNode, QDomDocument & document, PageItems what = AllPageItems ) const; /** * Rotates the image and object rects of the page to the given @p orientation. */ void rotateAt( Rotation orientation ); /** * Changes the size of the page to the given @p size. * * The @p size is meant to be referred to the page not rotated. */ void changeSize( const PageSize &size ); /** * Sets the @p color and @p areas of text selections. */ void setTextSelections( RegularAreaRect *areas, const QColor & color ); /** * Sets the @p color and @p area of the highlight for the observer with * the given @p id. */ void setHighlight( int id, RegularAreaRect *area, const QColor & color ); /** * Deletes all highlight objects for the observer with the given @p id. */ void deleteHighlights( int id = -1 ); /** * Deletes all text selection objects of the page. */ void deleteTextSelections(); /** * Get the tiles manager for the tiled @observer */ TilesManager *tilesManager( const DocumentObserver *observer ) const; /** * Set the tiles manager for the tiled @observer */ void setTilesManager( const DocumentObserver *observer, TilesManager *tm ); /** * Moves contents that are generated from oldPage to this. And clears them from page * so it can be deleted fine. */ void adoptGeneratedContents( PagePrivate *oldPage ); /* * Tries to find an equivalent form field to oldField by looking into the rect, type and name */ OKULARCORE_EXPORT static FormField *findEquivalentForm( const Page *p, FormField *oldField ); + void setPixmap( DocumentObserver *observer, QPixmap *pixmap, const NormalizedRect &rect, bool isPartialPixmap ); + class PixmapObject { public: - QPixmap *m_pixmap; + QPixmap *m_pixmap = nullptr; Rotation m_rotation; }; QMap< DocumentObserver*, PixmapObject > m_pixmaps; QMap< const DocumentObserver*, TilesManager *> m_tilesManagers; Page *m_page; int m_number; Rotation m_orientation; double m_width, m_height; DocumentPrivate *m_doc; NormalizedRect m_boundingBox; Rotation m_rotation; TextPage * m_text; PageTransition * m_transition; HighlightAreaRect *m_textSelections; QLinkedList< FormField * > formfields; Action * m_openingAction; Action * m_closingAction; double m_duration; QString m_label; bool m_isBoundingBoxKnown : 1; QDomDocument restoredLocalAnnotationList; // ... QDomDocument restoredFormFieldList; // ... }; } Q_DECLARE_OPERATORS_FOR_FLAGS(Okular::PageItems) #endif diff --git a/core/rotationjob.cpp b/core/rotationjob.cpp index 519ebbfa2..d34862446 100644 --- a/core/rotationjob.cpp +++ b/core/rotationjob.cpp @@ -1,115 +1,126 @@ /*************************************************************************** * Copyright (C) 2006 by Tobias Koenig * * * * 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. * ***************************************************************************/ #include "rotationjob_p.h" #include using namespace Okular; RotationJob::RotationJob( const QImage &image, Rotation oldRotation, Rotation newRotation, DocumentObserver *observer ) : ThreadWeaver::QObjectDecorator( new RotationJobInternal( image, oldRotation, newRotation ) ) , mObserver( observer ), m_pd( nullptr ) , mRect( NormalizedRect() ) + , mIsPartialUpdate( false ) { } void RotationJob::setPage( PagePrivate * pd ) { m_pd = pd; } void RotationJob::setRect( const NormalizedRect &rect ) { mRect = rect; } +void RotationJob::setIsPartialUpdate( bool partialUpdate ) +{ + mIsPartialUpdate = partialUpdate; +} + DocumentObserver * RotationJob::observer() const { return mObserver; } PagePrivate * RotationJob::page() const { return m_pd; } NormalizedRect RotationJob::rect() const { return mRect; } +bool RotationJob::isPartialUpdate() const +{ + return mIsPartialUpdate; +} + QTransform RotationJob::rotationMatrix( Rotation from, Rotation to ) { QTransform matrix; if ( from == Rotation0 ) { if ( to == Rotation90 ) matrix.rotate( 90 ); else if ( to == Rotation180 ) matrix.rotate( 180 ); else if ( to == Rotation270 ) matrix.rotate( 270 ); } else if ( from == Rotation90 ) { if ( to == Rotation180 ) matrix.rotate( 90 ); else if ( to == Rotation270 ) matrix.rotate( 180 ); else if ( to == Rotation0 ) matrix.rotate( 270 ); } else if ( from == Rotation180 ) { if ( to == Rotation270 ) matrix.rotate( 90 ); else if ( to == Rotation0 ) matrix.rotate( 180 ); else if ( to == Rotation90 ) matrix.rotate( 270 ); } else if ( from == Rotation270 ) { if ( to == Rotation0 ) matrix.rotate( 90 ); else if ( to == Rotation90 ) matrix.rotate( 180 ); else if ( to == Rotation180 ) matrix.rotate( 270 ); } return matrix; } RotationJobInternal::RotationJobInternal( const QImage &image, Rotation oldRotation, Rotation newRotation ) : mImage( image ), mOldRotation( oldRotation ), mNewRotation( newRotation ) { } QImage RotationJobInternal::image() const { return mRotatedImage; } Rotation RotationJobInternal::rotation() const { return mNewRotation; } void RotationJobInternal::run(ThreadWeaver::JobPointer self, ThreadWeaver::Thread *thread) { Q_UNUSED(self); Q_UNUSED(thread); if ( mOldRotation == mNewRotation ) { mRotatedImage = mImage; return; } const QTransform matrix = RotationJob::rotationMatrix( mOldRotation, mNewRotation ); mRotatedImage = mImage.transformed( matrix ); } #include "moc_rotationjob_p.cpp" diff --git a/core/rotationjob_p.h b/core/rotationjob_p.h index 51d096991..b0662f549 100644 --- a/core/rotationjob_p.h +++ b/core/rotationjob_p.h @@ -1,73 +1,76 @@ /*************************************************************************** * Copyright (C) 2006 by Tobias Koenig * * * * 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. * ***************************************************************************/ #ifndef _OKULAR_ROTATIONJOB_P_H_ #define _OKULAR_ROTATIONJOB_P_H_ #include #include #include #include #include "core/global.h" #include "core/area.h" namespace Okular { class DocumentObserver; class PagePrivate; class RotationJobInternal : public ThreadWeaver::Job { friend class RotationJob; public: QImage image() const; Rotation rotation() const; NormalizedRect rect() const; protected: void run(ThreadWeaver::JobPointer self, ThreadWeaver::Thread *thread) override; private: RotationJobInternal( const QImage &image, Rotation oldRotation, Rotation newRotation ); const QImage mImage; Rotation mOldRotation; Rotation mNewRotation; QImage mRotatedImage; }; class RotationJob : public ThreadWeaver::QObjectDecorator { Q_OBJECT public: RotationJob( const QImage &image, Rotation oldRotation, Rotation newRotation, DocumentObserver *observer ); void setPage( PagePrivate * pd ); void setRect( const NormalizedRect &rect ); + void setIsPartialUpdate( bool partialUpdate ); QImage image() const { return static_cast(job())->image(); } Rotation rotation() const { return static_cast(job())->rotation(); } DocumentObserver *observer() const; PagePrivate * page() const; NormalizedRect rect() const; + bool isPartialUpdate() const; static QTransform rotationMatrix( Rotation from, Rotation to ); private: DocumentObserver *mObserver; PagePrivate * m_pd; NormalizedRect mRect; + bool mIsPartialUpdate; }; } #endif diff --git a/core/textdocumentgenerator.cpp b/core/textdocumentgenerator.cpp index 5fa60d71e..78a571425 100644 --- a/core/textdocumentgenerator.cpp +++ b/core/textdocumentgenerator.cpp @@ -1,553 +1,553 @@ /*************************************************************************** * Copyright (C) 2007 by Tobias Koenig * * * * 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. * ***************************************************************************/ #include "textdocumentgenerator.h" #include "textdocumentgenerator_p.h" #include #include #include #include #include #include #include #include #include #include #include "action.h" #include "annotations.h" #include "page.h" #include "textpage.h" #include "document.h" using namespace Okular; /** * Generic Converter Implementation */ TextDocumentConverter::TextDocumentConverter() : QObject( nullptr ), d_ptr( new TextDocumentConverterPrivate ) { } TextDocumentConverter::~TextDocumentConverter() { delete d_ptr; } QTextDocument *TextDocumentConverter::convert( const QString & ) { return nullptr; } Document::OpenResult TextDocumentConverter::convertWithPassword( const QString &fileName, const QString & ) { QTextDocument *doc = convert( fileName ); setDocument( doc ); return doc != nullptr ? Document::OpenSuccess : Document::OpenError; } QTextDocument *TextDocumentConverter::document() { return d_ptr->mDocument; } void TextDocumentConverter::setDocument( QTextDocument *document ) { d_ptr->mDocument = document; } DocumentViewport TextDocumentConverter::calculateViewport( QTextDocument *document, const QTextBlock &block ) { return TextDocumentUtils::calculateViewport( document, block ); } TextDocumentGenerator* TextDocumentConverter::generator() const { return d_ptr->mParent ? d_ptr->mParent->q_func() : nullptr; } /** * Generic Generator Implementation */ Okular::TextPage* TextDocumentGeneratorPrivate::createTextPage( int pageNumber ) const { #ifdef OKULAR_TEXTDOCUMENT_THREADED_RENDERING Q_Q( const TextDocumentGenerator ); #endif Okular::TextPage *textPage = new Okular::TextPage; int start, end; #ifdef OKULAR_TEXTDOCUMENT_THREADED_RENDERING q->userMutex()->lock(); #endif TextDocumentUtils::calculatePositions( mDocument, pageNumber, start, end ); { QTextCursor cursor( mDocument ); for ( int i = start; i < end - 1; ++i ) { cursor.setPosition( i ); cursor.setPosition( i + 1, QTextCursor::KeepAnchor ); QString text = cursor.selectedText(); if ( text.length() == 1 ) { QRectF rect; TextDocumentUtils::calculateBoundingRect( mDocument, i, i + 1, rect, pageNumber ); if ( pageNumber == -1 ) text = QStringLiteral("\n"); textPage->append( text, new Okular::NormalizedRect( rect.left(), rect.top(), rect.right(), rect.bottom() ) ); } } } #ifdef OKULAR_TEXTDOCUMENT_THREADED_RENDERING q->userMutex()->unlock(); #endif return textPage; } void TextDocumentGeneratorPrivate::addAction( Action *action, int cursorBegin, int cursorEnd ) { if ( !action ) return; LinkPosition position; position.link = action; position.startPosition = cursorBegin; position.endPosition = cursorEnd; mLinkPositions.append( position ); } void TextDocumentGeneratorPrivate::addAnnotation( Annotation *annotation, int cursorBegin, int cursorEnd ) { if ( !annotation ) return; annotation->setFlags( annotation->flags() | Okular::Annotation::External ); AnnotationPosition position; position.annotation = annotation; position.startPosition = cursorBegin; position.endPosition = cursorEnd; mAnnotationPositions.append( position ); } void TextDocumentGeneratorPrivate::addTitle( int level, const QString &title, const QTextBlock &block ) { TitlePosition position; position.level = level; position.title = title; position.block = block; mTitlePositions.append( position ); } void TextDocumentGeneratorPrivate::addMetaData( const QString &key, const QString &value, const QString &title ) { mDocumentInfo.set( key, value, title ); } void TextDocumentGeneratorPrivate::addMetaData( DocumentInfo::Key key, const QString &value ) { mDocumentInfo.set( key, value ); } void TextDocumentGeneratorPrivate::generateLinkInfos() { for ( int i = 0; i < mLinkPositions.count(); ++i ) { const LinkPosition &linkPosition = mLinkPositions[ i ]; LinkInfo info; info.link = linkPosition.link; TextDocumentUtils::calculateBoundingRect( mDocument, linkPosition.startPosition, linkPosition.endPosition, info.boundingRect, info.page ); if ( info.page >= 0 ) mLinkInfos.append( info ); } } void TextDocumentGeneratorPrivate::generateAnnotationInfos() { for ( int i = 0; i < mAnnotationPositions.count(); ++i ) { const AnnotationPosition &annotationPosition = mAnnotationPositions[ i ]; AnnotationInfo info; info.annotation = annotationPosition.annotation; TextDocumentUtils::calculateBoundingRect( mDocument, annotationPosition.startPosition, annotationPosition.endPosition, info.boundingRect, info.page ); if ( info.page >= 0 ) mAnnotationInfos.append( info ); } } void TextDocumentGeneratorPrivate::generateTitleInfos() { QStack< QPair > parentNodeStack; QDomNode parentNode = mDocumentSynopsis; parentNodeStack.push( qMakePair( 0, parentNode ) ); for ( int i = 0; i < mTitlePositions.count(); ++i ) { const TitlePosition &position = mTitlePositions[ i ]; Okular::DocumentViewport viewport = TextDocumentUtils::calculateViewport( mDocument, position.block ); QDomElement item = mDocumentSynopsis.createElement( position.title ); item.setAttribute( QStringLiteral("Viewport"), viewport.toString() ); int headingLevel = position.level; // we need a parent, which has to be at a higher heading level than this heading level // so we just work through the stack while ( ! parentNodeStack.isEmpty() ) { int parentLevel = parentNodeStack.top().first; if ( parentLevel < headingLevel ) { // this is OK as a parent parentNode = parentNodeStack.top().second; break; } else { // we'll need to be further into the stack parentNodeStack.pop(); } } parentNode.appendChild( item ); parentNodeStack.push( qMakePair( headingLevel, QDomNode(item) ) ); } } void TextDocumentGeneratorPrivate::initializeGenerator() { Q_Q( TextDocumentGenerator ); mConverter->d_ptr->mParent = q->d_func(); if ( mGeneralSettings ) { mFont = mGeneralSettings->font(); } q->setFeature( Generator::TextExtraction ); q->setFeature( Generator::PrintNative ); q->setFeature( Generator::PrintToFile ); #ifdef OKULAR_TEXTDOCUMENT_THREADED_RENDERING if ( QFontDatabase::supportsThreadedFontRendering() ) q->setFeature( Generator::Threaded ); #endif QObject::connect( mConverter, SIGNAL(addAction(Action*,int,int)), q, SLOT(addAction(Action*,int,int)) ); QObject::connect( mConverter, SIGNAL(addAnnotation(Annotation*,int,int)), q, SLOT(addAnnotation(Annotation*,int,int)) ); QObject::connect( mConverter, SIGNAL(addTitle(int,QString,QTextBlock)), q, SLOT(addTitle(int,QString,QTextBlock)) ); QObject::connect( mConverter, SIGNAL(addMetaData(QString,QString,QString)), q, SLOT(addMetaData(QString,QString,QString)) ); QObject::connect( mConverter, SIGNAL(addMetaData(DocumentInfo::Key,QString)), q, SLOT(addMetaData(DocumentInfo::Key,QString)) ); QObject::connect( mConverter, &TextDocumentConverter::error, q, &Generator::error ); QObject::connect( mConverter, &TextDocumentConverter::warning, q, &Generator::warning ); QObject::connect( mConverter, &TextDocumentConverter::notice, q, &Generator::notice ); } TextDocumentGenerator::TextDocumentGenerator(TextDocumentConverter *converter, const QString& configName , QObject *parent, const QVariantList &args) : Okular::Generator( *new TextDocumentGeneratorPrivate( converter ), parent, args ) { Q_D( TextDocumentGenerator ); d->mGeneralSettings = new TextDocumentSettings( configName, this ); d->initializeGenerator(); } TextDocumentGenerator::~TextDocumentGenerator() { } Document::OpenResult TextDocumentGenerator::loadDocumentWithPassword( const QString & fileName, QVector & pagesVector, const QString &password ) { Q_D( TextDocumentGenerator ); const Document::OpenResult openResult = d->mConverter->convertWithPassword( fileName, password ); if ( openResult != Document::OpenSuccess ) { d->mDocument = nullptr; // loading failed, cleanup all the stuff eventually gathered from the converter d->mTitlePositions.clear(); Q_FOREACH ( const TextDocumentGeneratorPrivate::LinkPosition &linkPos, d->mLinkPositions ) { delete linkPos.link; } d->mLinkPositions.clear(); Q_FOREACH ( const TextDocumentGeneratorPrivate::AnnotationPosition &annPos, d->mAnnotationPositions ) { delete annPos.annotation; } d->mAnnotationPositions.clear(); return openResult; } d->mDocument = d->mConverter->document(); d->generateTitleInfos(); d->generateLinkInfos(); d->generateAnnotationInfos(); pagesVector.resize( d->mDocument->pageCount() ); const QSize size = d->mDocument->pageSize().toSize(); QVector< QLinkedList > objects( d->mDocument->pageCount() ); for ( int i = 0; i < d->mLinkInfos.count(); ++i ) { const TextDocumentGeneratorPrivate::LinkInfo &info = d->mLinkInfos.at( i ); // in case that the converter report bogus link info data, do not assert here if ( info.page >= objects.count() ) continue; const QRectF rect = info.boundingRect; objects[ info.page ].append( new Okular::ObjectRect( rect.left(), rect.top(), rect.right(), rect.bottom(), false, Okular::ObjectRect::Action, info.link ) ); } QVector< QLinkedList > annots( d->mDocument->pageCount() ); for ( int i = 0; i < d->mAnnotationInfos.count(); ++i ) { const TextDocumentGeneratorPrivate::AnnotationInfo &info = d->mAnnotationInfos[ i ]; annots[ info.page ].append( info.annotation ); } for ( int i = 0; i < d->mDocument->pageCount(); ++i ) { Okular::Page * page = new Okular::Page( i, size.width(), size.height(), Okular::Rotation0 ); pagesVector[ i ] = page; if ( !objects.at( i ).isEmpty() ) { page->setObjectRects( objects.at( i ) ); } QLinkedList::ConstIterator annIt = annots.at( i ).begin(), annEnd = annots.at( i ).end(); for ( ; annIt != annEnd; ++annIt ) { page->addAnnotation( *annIt ); } } return openResult; } bool TextDocumentGenerator::doCloseDocument() { Q_D( TextDocumentGenerator ); delete d->mDocument; d->mDocument = nullptr; d->mTitlePositions.clear(); d->mLinkPositions.clear(); d->mLinkInfos.clear(); d->mAnnotationPositions.clear(); d->mAnnotationInfos.clear(); // do not use clear() for the following two, otherwise they change type d->mDocumentInfo = Okular::DocumentInfo(); d->mDocumentSynopsis = Okular::DocumentSynopsis(); return true; } bool TextDocumentGenerator::canGeneratePixmap() const { return Generator::canGeneratePixmap(); } void TextDocumentGenerator::generatePixmap( Okular::PixmapRequest * request ) { Generator::generatePixmap( request ); } QImage TextDocumentGeneratorPrivate::image( PixmapRequest * request ) { if ( !mDocument ) return QImage(); #ifdef OKULAR_TEXTDOCUMENT_THREADED_RENDERING Q_Q( TextDocumentGenerator ); #endif QImage image( request->width(), request->height(), QImage::Format_ARGB32 ); image.fill( Qt::white ); QPainter p; p.begin( &image ); qreal width = request->width(); qreal height = request->height(); const QSize size = mDocument->pageSize().toSize(); p.scale( width / (qreal)size.width(), height / (qreal)size.height() ); QRect rect; rect = QRect( 0, request->pageNumber() * size.height(), size.width(), size.height() ); p.translate( QPoint( 0, request->pageNumber() * size.height() * -1 ) ); p.setClipRect( rect ); #ifdef OKULAR_TEXTDOCUMENT_THREADED_RENDERING q->userMutex()->lock(); #endif QAbstractTextDocumentLayout::PaintContext context; context.palette.setColor( QPalette::Text, Qt::black ); // FIXME Fix Qt, this doesn't work, we have horrible hacks // in the generators that return html, remove that code // if Qt ever gets fixed // context.palette.setColor( QPalette::Link, Qt::blue ); context.clip = rect; mDocument->setDefaultFont( mFont ); mDocument->documentLayout()->draw( &p, context ); #ifdef OKULAR_TEXTDOCUMENT_THREADED_RENDERING q->userMutex()->unlock(); #endif p.end(); return image; } -Okular::TextPage* TextDocumentGenerator::textPage( Okular::Page * page ) +Okular::TextPage* TextDocumentGenerator::textPage( Okular::TextRequest * request ) { Q_D( TextDocumentGenerator ); - return d->createTextPage( page->number() ); + return d->createTextPage( request->page()->number() ); } bool TextDocumentGenerator::print( QPrinter& printer ) { Q_D( TextDocumentGenerator ); if ( !d->mDocument ) return false; d->mDocument->print( &printer ); return true; } Okular::DocumentInfo TextDocumentGenerator::generateDocumentInfo( const QSet & /*keys*/ ) const { Q_D( const TextDocumentGenerator ); return d->mDocumentInfo; } const Okular::DocumentSynopsis* TextDocumentGenerator::generateDocumentSynopsis() { Q_D( TextDocumentGenerator ); if ( !d->mDocumentSynopsis.hasChildNodes() ) return nullptr; else return &d->mDocumentSynopsis; } QVariant TextDocumentGeneratorPrivate::metaData( const QString &key, const QVariant &option ) const { Q_UNUSED( option ) if ( key == QLatin1String("DocumentTitle") ) { return mDocumentInfo.get( DocumentInfo::Title ); } return QVariant(); } Okular::ExportFormat::List TextDocumentGenerator::exportFormats( ) const { static Okular::ExportFormat::List formats; if ( formats.isEmpty() ) { formats.append( Okular::ExportFormat::standardFormat( Okular::ExportFormat::PlainText ) ); formats.append( Okular::ExportFormat::standardFormat( Okular::ExportFormat::PDF ) ); if ( QTextDocumentWriter::supportedDocumentFormats().contains( "ODF" ) ) { formats.append( Okular::ExportFormat::standardFormat( Okular::ExportFormat::OpenDocumentText ) ); } if ( QTextDocumentWriter::supportedDocumentFormats().contains( "HTML" ) ) { formats.append( Okular::ExportFormat::standardFormat( Okular::ExportFormat::HTML ) ); } } return formats; } bool TextDocumentGenerator::exportTo( const QString &fileName, const Okular::ExportFormat &format ) { Q_D( TextDocumentGenerator ); if ( !d->mDocument ) return false; if ( format.mimeType().name() == QLatin1String( "application/pdf" ) ) { QFile file( fileName ); if ( !file.open( QIODevice::WriteOnly ) ) return false; QPrinter printer( QPrinter::HighResolution ); printer.setOutputFormat( QPrinter::PdfFormat ); printer.setOutputFileName( fileName ); d->mDocument->print( &printer ); return true; } else if ( format.mimeType().name() == QLatin1String( "text/plain" ) ) { QFile file( fileName ); if ( !file.open( QIODevice::WriteOnly ) ) return false; QTextStream out( &file ); out << d->mDocument->toPlainText(); return true; } else if ( format.mimeType().name() == QLatin1String( "application/vnd.oasis.opendocument.text" ) ) { QTextDocumentWriter odfWriter( fileName, "odf" ); return odfWriter.write( d->mDocument ); } else if ( format.mimeType().name() == QLatin1String( "text/html" ) ) { QTextDocumentWriter odfWriter( fileName, "html" ); return odfWriter.write( d->mDocument ); } return false; } bool TextDocumentGenerator::reparseConfig() { Q_D( TextDocumentGenerator ); const QFont newFont = d->mGeneralSettings->font(); if ( newFont != d->mFont ) { d->mFont = newFont; return true; } return false; } void TextDocumentGenerator::addPages( KConfigDialog* /*dlg*/ ) { qCWarning(OkularCoreDebug) << "You forgot to reimplement addPages in your TextDocumentGenerator"; return; } TextDocumentSettings* TextDocumentGenerator::generalSettings() { Q_D( TextDocumentGenerator ); return d->mGeneralSettings; } #include "moc_textdocumentgenerator.cpp" diff --git a/core/textdocumentgenerator.h b/core/textdocumentgenerator.h index 553b56278..efcfcb703 100644 --- a/core/textdocumentgenerator.h +++ b/core/textdocumentgenerator.h @@ -1,226 +1,226 @@ /*************************************************************************** * Copyright (C) 2007 by Tobias Koenig * * * * 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. * ***************************************************************************/ #ifndef _OKULAR_TEXTDOCUMENTGENERATOR_H_ #define _OKULAR_TEXTDOCUMENTGENERATOR_H_ #include "okularcore_export.h" #include "document.h" #include "generator.h" #include "textdocumentsettings.h" #include "../interfaces/configinterface.h" class QTextBlock; class QTextDocument; namespace Okular { class TextDocumentConverterPrivate; class TextDocumentGenerator; class TextDocumentGeneratorPrivate; class OKULARCORE_EXPORT TextDocumentConverter : public QObject { Q_OBJECT friend class TextDocumentGenerator; friend class TextDocumentGeneratorPrivate; public: /** * Creates a new generic converter. */ TextDocumentConverter(); /** * Destroys the generic converter. */ ~TextDocumentConverter(); /** * Returns the generated QTextDocument object. The caller takes ownership of the QTextDocument * * @note there is no need to implement this one if you implement convertWithPassword */ virtual QTextDocument *convert( const QString &fileName ); /** * Returns the generated QTextDocument object. */ virtual Document::OpenResult convertWithPassword( const QString &fileName, const QString &password ); /** * Returns the generated QTextDocument object. Will be null if convert didn't succeed */ QTextDocument *document(); Q_SIGNALS: /** * Adds a new link object which is located between cursorBegin and * cursorEnd to the generator. */ void addAction( Action *link, int cursorBegin, int cursorEnd ); /** * Adds a new annotation object which is located between cursorBegin and * cursorEnd to the generator. */ void addAnnotation( Annotation *annotation, int cursorBegin, int cursorEnd ); /** * Adds a new title at the given level which is located as position to the generator. */ void addTitle( int level, const QString &title, const QTextBlock &position ); /** * Adds a set of meta data to the generator. */ void addMetaData( const QString &key, const QString &value, const QString &title ); /** * Adds a set of meta data to the generator. * * @since 0.7 (KDE 4.1) */ void addMetaData( DocumentInfo::Key key, const QString &value ); /** * This signal should be emitted whenever an error occurred in the converter. * * @param message The message which should be shown to the user. * @param duration The time that the message should be shown to the user. */ void error( const QString &message, int duration ); /** * This signal should be emitted whenever the user should be warned. * * @param message The message which should be shown to the user. * @param duration The time that the message should be shown to the user. */ void warning( const QString &message, int duration ); /** * This signal should be emitted whenever the user should be noticed. * * @param message The message which should be shown to the user. * @param duration The time that the message should be shown to the user. */ void notice( const QString &message, int duration ); protected: /** * Sets the converted QTextDocument object. */ void setDocument( QTextDocument *document ); /** * This method can be used to calculate the viewport for a given text block. * * @note This method should be called at the end of the convertion, because it * triggers QTextDocument to do the layout calculation. */ DocumentViewport calculateViewport( QTextDocument *document, const QTextBlock &block ); /** * Returns the generator that owns this converter. * * @note May be null if the converter was not created for a generator. * * @since 0.7 (KDE 4.1) */ TextDocumentGenerator* generator() const; private: TextDocumentConverterPrivate *d_ptr; Q_DECLARE_PRIVATE( TextDocumentConverter ) Q_DISABLE_COPY( TextDocumentConverter ) }; /** * @brief QTextDocument-based Generator * * This generator provides a document in the form of a QTextDocument object, * parsed using a specialized TextDocumentConverter. */ class OKULARCORE_EXPORT TextDocumentGenerator : public Generator, public Okular::ConfigInterface { /// @cond PRIVATE friend class TextDocumentConverter; /// @endcond Q_OBJECT Q_INTERFACES( Okular::ConfigInterface ) public: /** * Creates a new generator that uses the specified @p converter. * * @param configName - see Okular::TextDocumentSettings * * @note the generator will take ownership of the converter, so you * don't have to delete it yourself * @since 0.17 (KDE 4.11) */ TextDocumentGenerator(TextDocumentConverter *converter, const QString& configName, QObject *parent, const QVariantList &args); virtual ~TextDocumentGenerator(); // [INHERITED] load a document and fill up the pagesVector Document::OpenResult loadDocumentWithPassword( const QString & fileName, QVector & pagesVector, const QString &password ) override; // [INHERITED] perform actions on document / pages bool canGeneratePixmap() const override; void generatePixmap( Okular::PixmapRequest * request ) override; // [INHERITED] print document using already configured QPrinter bool print( QPrinter& printer ) override; // [INHERITED] text exporting Okular::ExportFormat::List exportFormats() const override; bool exportTo( const QString &fileName, const Okular::ExportFormat &format ) override; // [INHERITED] config interface /// By default checks if the default font has changed or not bool reparseConfig() override; /// Does nothing by default. You need to reimplement it in your generator void addPages( KConfigDialog* dlg ) override; /** * Config skeleton for TextDocumentSettingsWidget * * You must use new construtor to initialize TextDocumentSettings, * that contain @param configName. * * @since 0.17 (KDE 4.11) */ TextDocumentSettings* generalSettings(); Okular::DocumentInfo generateDocumentInfo( const QSet &keys ) const override; const Okular::DocumentSynopsis* generateDocumentSynopsis() override; protected: bool doCloseDocument() override; - Okular::TextPage* textPage( Okular::Page *page ) override; + Okular::TextPage* textPage( Okular::TextRequest *request ) override; private: Q_DECLARE_PRIVATE( TextDocumentGenerator ) Q_DISABLE_COPY( TextDocumentGenerator ) Q_PRIVATE_SLOT( d_func(), void addAction( Action*, int, int ) ) Q_PRIVATE_SLOT( d_func(), void addAnnotation( Annotation*, int, int ) ) Q_PRIVATE_SLOT( d_func(), void addTitle( int, const QString&, const QTextBlock& ) ) Q_PRIVATE_SLOT( d_func(), void addMetaData( const QString&, const QString&, const QString& ) ) Q_PRIVATE_SLOT( d_func(), void addMetaData( DocumentInfo::Key, const QString& ) ) }; } #endif diff --git a/core/tilesmanager.cpp b/core/tilesmanager.cpp index e8282fa33..d20c1b1e0 100644 --- a/core/tilesmanager.cpp +++ b/core/tilesmanager.cpp @@ -1,709 +1,727 @@ /*************************************************************************** * Copyright (C) 2012 by Mailson Menezes * * 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. * ***************************************************************************/ #include "tilesmanager_p.h" #include #include #include #include #include "tile.h" #define TILES_MAXSIZE 2000000 using namespace Okular; static bool rankedTilesLessThan( TileNode *t1, TileNode *t2 ) { // Order tiles by its dirty state and then by distance from the viewport. if ( t1->dirty == t2->dirty ) return t1->distance < t2->distance; return !t1->dirty; } class TilesManager::Private { public: Private(); bool hasPixmap( const NormalizedRect &rect, const TileNode &tile ) const; void tilesAt( const NormalizedRect &rect, TileNode &tile, QList &result, TileLeaf tileLeaf ); - void setPixmap( const QPixmap *pixmap, const NormalizedRect &rect, TileNode &tile ); + void setPixmap( const QPixmap *pixmap, const NormalizedRect &rect, TileNode &tile, bool isPartialPixmap ); /** * Mark @p tile and all its children as dirty */ static void markDirty( TileNode &tile ); /** * Deletes all tiles, recursively */ void deleteTiles( const TileNode &tile ); void markParentDirty( const TileNode &tile ); void rankTiles( TileNode &tile, QList &rankedTiles, const NormalizedRect &visibleRect, int visiblePageNumber ); /** * Since the tile can be large enough to occupy a significant amount of * space, they may be split in more tiles. This operation is performed * when the tiles of a certain region is requested and they are bigger * than an arbitrary value. Only tiles intersecting the desired region * are split. There's no need to do this for the entire page. */ void split( TileNode &tile, const NormalizedRect &rect ); /** * Checks whether the tile's size is bigger than an arbitrary value and * performs the split operation returning true. * Otherwise it just returns false, without performing any operation. */ bool splitBigTiles( TileNode &tile, const NormalizedRect &rect ); // The page is split in a 4x4 grid of tiles TileNode tiles[16]; int width; int height; int pageNumber; qulonglong totalPixels; Rotation rotation; NormalizedRect visibleRect; NormalizedRect requestRect; int requestWidth; int requestHeight; }; TilesManager::Private::Private() : width( 0 ) , height( 0 ) , pageNumber( 0 ) , totalPixels( 0 ) , rotation( Rotation0 ) , requestRect( NormalizedRect() ) , requestWidth( 0 ) , requestHeight( 0 ) { } TilesManager::TilesManager( int pageNumber, int width, int height, Rotation rotation ) : d( new Private ) { d->pageNumber = pageNumber; d->width = width; d->height = height; d->rotation = rotation; // The page is split in a 4x4 grid of tiles const double dim = 0.25; for ( int i = 0; i < 16; ++i ) { int x = i % 4; int y = i / 4; d->tiles[ i ].rect = NormalizedRect( x*dim, y*dim, x*dim+dim, y*dim+dim ); } } TilesManager::~TilesManager() { for ( int i = 0; i < 16; ++i ) d->deleteTiles( d->tiles[ i ] ); delete d; } void TilesManager::Private::deleteTiles( const TileNode &tile ) { if ( tile.pixmap ) { totalPixels -= tile.pixmap->width()*tile.pixmap->height(); delete tile.pixmap; } if ( tile.nTiles > 0 ) { for ( int i = 0; i < tile.nTiles; ++i ) deleteTiles( tile.tiles[ i ] ); delete [] tile.tiles; } } void TilesManager::setSize( int width, int height ) { if ( width == d->width && height == d->height ) return; d->width = width; d->height = height; markDirty(); } int TilesManager::width() const { return d->width; } int TilesManager::height() const { return d->height; } void TilesManager::setRotation( Rotation rotation ) { if ( rotation == d->rotation ) return; d->rotation = rotation; } Rotation TilesManager::rotation() const { return d->rotation; } void TilesManager::markDirty() { for ( int i = 0; i < 16; ++i ) { TilesManager::Private::markDirty( d->tiles[ i ] ); } } void TilesManager::Private::markDirty( TileNode &tile ) { tile.dirty = true; for ( int i = 0; i < tile.nTiles; ++i ) { markDirty( tile.tiles[ i ] ); } } -void TilesManager::setPixmap( const QPixmap *pixmap, const NormalizedRect &rect ) +void TilesManager::setPixmap( const QPixmap *pixmap, const NormalizedRect &rect, bool isPartialPixmap ) { - NormalizedRect rotatedRect = TilesManager::fromRotatedRect( rect, d->rotation ); + const NormalizedRect rotatedRect = TilesManager::fromRotatedRect( rect, d->rotation ); if ( !d->requestRect.isNull() ) { if ( !(d->requestRect == rect) ) return; - // Check whether the pixmap has the same absolute size of the expected - // request. - // If the document is rotated, rotate requestRect back to the original - // rotation before comparing to pixmap's size. This is to avoid - // conversion issues. The pixmap request was made using an unrotated - // rect. - QSize pixmapSize = pixmap->size(); - int w = width(); - int h = height(); - if ( d->rotation % 2 ) + if ( pixmap ) { - qSwap(w, h); - pixmapSize.transpose(); - } + // Check whether the pixmap has the same absolute size of the expected + // request. + // If the document is rotated, rotate requestRect back to the original + // rotation before comparing to pixmap's size. This is to avoid + // conversion issues. The pixmap request was made using an unrotated + // rect. + QSize pixmapSize = pixmap->size(); + int w = width(); + int h = height(); + if ( d->rotation % 2 ) + { + qSwap(w, h); + pixmapSize.transpose(); + } - if ( rotatedRect.geometry( w, h ).size() != pixmapSize ) - return; + if ( rotatedRect.geometry( w, h ).size() != pixmapSize ) + return; + } d->requestRect = NormalizedRect(); } for ( int i = 0; i < 16; ++i ) { - d->setPixmap( pixmap, rotatedRect, d->tiles[ i ] ); + d->setPixmap( pixmap, rotatedRect, d->tiles[ i ], isPartialPixmap ); } } -void TilesManager::Private::setPixmap( const QPixmap *pixmap, const NormalizedRect &rect, TileNode &tile ) +void TilesManager::Private::setPixmap( const QPixmap *pixmap, const NormalizedRect &rect, TileNode &tile, bool isPartialPixmap ) { QRect pixmapRect = TilesManager::toRotatedRect( rect, rotation ).geometry( width, height ); // Exclude tiles outside the viewport if ( !tile.rect.intersects( rect ) ) return; // if the tile is not entirely within the viewport (the tile intersects an // edged of the viewport), attempt to set the pixmap in the children tiles if ( !((tile.rect & rect) == tile.rect) ) { // paint children tiles if ( tile.nTiles > 0 ) { for ( int i = 0; i < tile.nTiles; ++i ) - setPixmap( pixmap, rect, tile.tiles[ i ] ); + setPixmap( pixmap, rect, tile.tiles[ i ], isPartialPixmap ); delete tile.pixmap; tile.pixmap = nullptr; } return; } // the tile lies entirely within the viewport if ( tile.nTiles == 0 ) { - tile.dirty = false; + tile.dirty = isPartialPixmap; // check whether the tile size is big and split it if necessary if ( !splitBigTiles( tile, rect ) ) { if ( tile.pixmap ) { totalPixels -= tile.pixmap->width()*tile.pixmap->height(); delete tile.pixmap; } - NormalizedRect rotatedRect = TilesManager::toRotatedRect( tile.rect, rotation ); - tile.pixmap = new QPixmap( pixmap->copy( rotatedRect.geometry( width, height ).translated( -pixmapRect.topLeft() ) ) ); tile.rotation = rotation; - totalPixels += tile.pixmap->width()*tile.pixmap->height(); + if ( pixmap ) + { + const NormalizedRect rotatedRect = TilesManager::toRotatedRect( tile.rect, rotation ); + tile.pixmap = new QPixmap( pixmap->copy( rotatedRect.geometry( width, height ).translated( -pixmapRect.topLeft() ) ) ); + totalPixels += tile.pixmap->width()*tile.pixmap->height(); + } + else + { + tile.pixmap = nullptr; + } } else { if ( tile.pixmap ) { totalPixels -= tile.pixmap->width()*tile.pixmap->height(); delete tile.pixmap; tile.pixmap = nullptr; } for ( int i = 0; i < tile.nTiles; ++i ) - setPixmap( pixmap, rect, tile.tiles[ i ] ); + setPixmap( pixmap, rect, tile.tiles[ i ], isPartialPixmap ); } } else { QRect tileRect = tile.rect.geometry( width, height ); // sets the pixmap of the children tiles. if the tile's size is too // small, discards the children tiles and use the current one if ( tileRect.width()*tileRect.height() >= TILES_MAXSIZE ) { - tile.dirty = false; + tile.dirty = isPartialPixmap; if ( tile.pixmap ) { totalPixels -= tile.pixmap->width()*tile.pixmap->height(); delete tile.pixmap; tile.pixmap = nullptr; } for ( int i = 0; i < tile.nTiles; ++i ) - setPixmap( pixmap, rect, tile.tiles[ i ] ); + setPixmap( pixmap, rect, tile.tiles[ i ], isPartialPixmap ); } else { // remove children tiles for ( int i = 0; i < tile.nTiles; ++i ) { deleteTiles( tile.tiles[ i ] ); tile.tiles[ i ].pixmap = nullptr; } delete [] tile.tiles; tile.tiles = nullptr; tile.nTiles = 0; // paint tile if ( tile.pixmap ) { totalPixels -= tile.pixmap->width()*tile.pixmap->height(); delete tile.pixmap; } - NormalizedRect rotatedRect = TilesManager::toRotatedRect( tile.rect, rotation ); - tile.pixmap = new QPixmap( pixmap->copy( rotatedRect.geometry( width, height ).translated( -pixmapRect.topLeft() ) ) ); tile.rotation = rotation; - totalPixels += tile.pixmap->width()*tile.pixmap->height(); - tile.dirty = false; + if ( pixmap ) + { + const NormalizedRect rotatedRect = TilesManager::toRotatedRect( tile.rect, rotation ); + tile.pixmap = new QPixmap( pixmap->copy( rotatedRect.geometry( width, height ).translated( -pixmapRect.topLeft() ) ) ); + totalPixels += tile.pixmap->width()*tile.pixmap->height(); + } + else + { + tile.pixmap = nullptr; + } + tile.dirty = isPartialPixmap; } } } bool TilesManager::hasPixmap( const NormalizedRect &rect ) { NormalizedRect rotatedRect = fromRotatedRect( rect, d->rotation ); for ( int i = 0; i < 16; ++i ) { if ( !d->hasPixmap( rotatedRect, d->tiles[ i ] ) ) return false; } return true; } bool TilesManager::Private::hasPixmap( const NormalizedRect &rect, const TileNode &tile ) const { - if ( !tile.rect.intersects( rect ) ) + const NormalizedRect rectIntersection = tile.rect & rect; + if ( rectIntersection.width() <= 0 || rectIntersection.height() <= 0 ) return true; if ( tile.nTiles == 0 ) return tile.isValid(); // all children tiles are clean. doesn't need to go deeper if ( !tile.dirty ) return true; for ( int i = 0; i < tile.nTiles; ++i ) { if ( !hasPixmap( rect, tile.tiles[ i ] ) ) return false; } return true; } QList TilesManager::tilesAt( const NormalizedRect &rect, TileLeaf tileLeaf ) { QList result; NormalizedRect rotatedRect = fromRotatedRect( rect, d->rotation ); for ( int i = 0; i < 16; ++i ) { d->tilesAt( rotatedRect, d->tiles[ i ], result, tileLeaf ); } return result; } void TilesManager::Private::tilesAt( const NormalizedRect &rect, TileNode &tile, QList &result, TileLeaf tileLeaf ) { if ( !tile.rect.intersects( rect ) ) return; // split big tiles before the requests are made, otherwise we would end up // requesting huge areas unnecessarily splitBigTiles( tile, rect ); if ( ( tileLeaf == TerminalTile && tile.nTiles == 0 ) || ( tileLeaf == PixmapTile && tile.pixmap ) ) { NormalizedRect rotatedRect; if ( rotation != Rotation0 ) rotatedRect = TilesManager::toRotatedRect( tile.rect, rotation ); else rotatedRect = tile.rect; if ( tile.pixmap && tileLeaf == PixmapTile && tile.rotation != rotation ) { // Lazy tiles rotation int angleToRotate = (rotation - tile.rotation)*90; int xOffset = 0, yOffset = 0; int w = 0, h = 0; switch( angleToRotate ) { case 0: xOffset = 0; yOffset = 0; w = tile.pixmap->width(); h = tile.pixmap->height(); break; case 90: case -270: xOffset = 0; yOffset = -tile.pixmap->height(); w = tile.pixmap->height(); h = tile.pixmap->width(); break; case 180: case -180: xOffset = -tile.pixmap->width(); yOffset = -tile.pixmap->height(); w = tile.pixmap->width(); h = tile.pixmap->height(); break; case 270: case -90: xOffset = -tile.pixmap->width(); yOffset = 0; w = tile.pixmap->height(); h = tile.pixmap->width(); break; } QPixmap *rotatedPixmap = new QPixmap( w, h ); QPainter p( rotatedPixmap ); p.rotate( angleToRotate ); p.translate( xOffset, yOffset ); p.drawPixmap( 0, 0, *tile.pixmap ); p.end(); delete tile.pixmap; tile.pixmap = rotatedPixmap; tile.rotation = rotation; } result.append( Tile( rotatedRect, tile.pixmap, tile.isValid() ) ); } else { for ( int i = 0; i < tile.nTiles; ++i ) tilesAt( rect, tile.tiles[ i ], result, tileLeaf ); } } qulonglong TilesManager::totalMemory() const { return 4*d->totalPixels; } void TilesManager::cleanupPixmapMemory( qulonglong numberOfBytes, const NormalizedRect &visibleRect, int visiblePageNumber ) { QList rankedTiles; for ( int i = 0; i < 16; ++i ) { d->rankTiles( d->tiles[ i ], rankedTiles, visibleRect, visiblePageNumber ); } qSort( rankedTiles.begin(), rankedTiles.end(), rankedTilesLessThan ); while ( numberOfBytes > 0 && !rankedTiles.isEmpty() ) { TileNode *tile = rankedTiles.takeLast(); if ( !tile->pixmap ) continue; // do not evict visible pixmaps if ( tile->rect.intersects( visibleRect ) ) continue; qulonglong pixels = tile->pixmap->width()*tile->pixmap->height(); d->totalPixels -= pixels; if ( numberOfBytes < 4*pixels ) numberOfBytes = 0; else numberOfBytes -= 4*pixels; delete tile->pixmap; tile->pixmap = nullptr; d->markParentDirty( *tile ); } } void TilesManager::Private::markParentDirty( const TileNode &tile ) { if ( !tile.parent ) return; if ( !tile.parent->dirty ) { tile.parent->dirty = true; markParentDirty( *tile.parent ); } } void TilesManager::Private::rankTiles( TileNode &tile, QList &rankedTiles, const NormalizedRect &visibleRect, int visiblePageNumber ) { // If the page is visible, visibleRect is not null. // Otherwise we use the number of one of the visible pages to calculate the // distance. // Note that the current page may be visible and yet its pageNumber is // different from visiblePageNumber. Since we only use this value on hidden // pages, any visible page number will fit. if ( visibleRect.isNull() && visiblePageNumber < 0 ) return; if ( tile.pixmap ) { // Update distance if ( !visibleRect.isNull() ) { NormalizedPoint viewportCenter = visibleRect.center(); NormalizedPoint tileCenter = tile.rect.center(); // Manhattan distance. It's a good and fast approximation. tile.distance = qAbs(viewportCenter.x - tileCenter.x) + qAbs(viewportCenter.y - tileCenter.y); } else { // For non visible pages only the vertical distance is used if ( pageNumber < visiblePageNumber ) tile.distance = 1 - tile.rect.bottom; else tile.distance = tile.rect.top; } rankedTiles.append( &tile ); } else { for ( int i = 0; i < tile.nTiles; ++i ) { rankTiles( tile.tiles[ i ], rankedTiles, visibleRect, visiblePageNumber ); } } } bool TilesManager::isRequesting( const NormalizedRect &rect, int pageWidth, int pageHeight ) const { return rect == d->requestRect && pageWidth == d->requestWidth && pageHeight == d->requestHeight; } void TilesManager::setRequest( const NormalizedRect &rect, int pageWidth, int pageHeight ) { d->requestRect = rect; d->requestWidth = pageWidth; d->requestHeight = pageHeight; } bool TilesManager::Private::splitBigTiles( TileNode &tile, const NormalizedRect &rect ) { QRect tileRect = tile.rect.geometry( width, height ); if ( tileRect.width()*tileRect.height() < TILES_MAXSIZE ) return false; split( tile, rect ); return true; } void TilesManager::Private::split( TileNode &tile, const NormalizedRect &rect ) { if ( tile.nTiles != 0 ) return; if ( rect.isNull() || !tile.rect.intersects( rect ) ) return; tile.nTiles = 4; tile.tiles = new TileNode[4]; double hCenter = (tile.rect.left + tile.rect.right)/2; double vCenter = (tile.rect.top + tile.rect.bottom)/2; tile.tiles[0].rect = NormalizedRect( tile.rect.left, tile.rect.top, hCenter, vCenter ); tile.tiles[1].rect = NormalizedRect( hCenter, tile.rect.top, tile.rect.right, vCenter ); tile.tiles[2].rect = NormalizedRect( tile.rect.left, vCenter, hCenter, tile.rect.bottom ); tile.tiles[3].rect = NormalizedRect( hCenter, vCenter, tile.rect.right, tile.rect.bottom ); for ( int i = 0; i < tile.nTiles; ++i ) { tile.tiles[ i ].parent = &tile; splitBigTiles( tile.tiles[ i ], rect ); } } NormalizedRect TilesManager::fromRotatedRect( const NormalizedRect &rect, Rotation rotation ) { if ( rotation == Rotation0 ) return rect; NormalizedRect newRect; switch ( rotation ) { case Rotation90: newRect = NormalizedRect( rect.top, 1 - rect.right, rect.bottom, 1 - rect.left ); break; case Rotation180: newRect = NormalizedRect( 1 - rect.right, 1 - rect.bottom, 1 - rect.left, 1 - rect.top ); break; case Rotation270: newRect = NormalizedRect( 1 - rect.bottom, rect.left, 1 - rect.top, rect.right ); break; default: newRect = rect; break; } return newRect; } NormalizedRect TilesManager::toRotatedRect( const NormalizedRect &rect, Rotation rotation ) { if ( rotation == Rotation0 ) return rect; NormalizedRect newRect; switch ( rotation ) { case Rotation90: newRect = NormalizedRect( 1 - rect.bottom, rect.left, 1 - rect.top, rect.right ); break; case Rotation180: newRect = NormalizedRect( 1 - rect.right, 1 - rect.bottom, 1 - rect.left, 1 - rect.top ); break; case Rotation270: newRect = NormalizedRect( rect.top, 1 - rect.right, rect.bottom, 1 - rect.left ); break; default: newRect = rect; break; } return newRect; } TileNode::TileNode() : pixmap( nullptr ) , rotation( Rotation0 ) , dirty ( true ) , distance( -1 ) , tiles( nullptr ) , nTiles( 0 ) , parent( nullptr ) { } bool TileNode::isValid() const { return pixmap && !dirty; } class Tile::Private { public: Private(); NormalizedRect rect; QPixmap *pixmap; bool isValid; }; Tile::Private::Private() : pixmap( nullptr ) , isValid( false ) { } Tile::Tile( const NormalizedRect &rect, QPixmap *pixmap, bool isValid ) : d( new Tile::Private ) { d->rect = rect; d->pixmap = pixmap; d->isValid = isValid; } Tile::Tile( const Tile &t ) : d( new Tile::Private ) { d->rect = t.d->rect; d->pixmap = t.d->pixmap; d->isValid = t.d->isValid; } Tile& Tile::operator=( const Tile &other ) { if ( this == &other ) return *this; d->rect = other.d->rect; d->pixmap = other.d->pixmap; d->isValid = other.d->isValid; return *this; } Tile::~Tile() { delete d; } NormalizedRect Tile::rect() const { return d->rect; } QPixmap * Tile::pixmap() const { return d->pixmap; } bool Tile::isValid() const { return d->isValid; } diff --git a/core/tilesmanager_p.h b/core/tilesmanager_p.h index 44aa78ae9..a1acde647 100644 --- a/core/tilesmanager_p.h +++ b/core/tilesmanager_p.h @@ -1,207 +1,207 @@ /*************************************************************************** * Copyright (C) 2012 by Mailson Menezes * * 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. * ***************************************************************************/ #ifndef _OKULAR_TILES_MANAGER_P_H_ #define _OKULAR_TILES_MANAGER_P_H_ #include "okularcore_export.h" #include "area.h" class QPixmap; namespace Okular { class Tile; /** * Node in the quadtree structure used by the tiles manager to store tiles. * * Except for the first level, the tiles manager stores tiles in a quadtree * structure. * Each node stores the pixmap of a tile and its location on the page. * There's a limit on the size of the pixmaps (TILES_MAXSIZE, defined in * tilesmanager.cpp), and tiles that are bigger than that value are split into * four children tiles, which are stored as children of the original tile. * If children tiles are still too big, they are recursively split again. * If the zoom level changes and a big tile goes below the limit, it is merged * back into a leaf tile. */ class TileNode { public: TileNode(); bool isValid() const; /** * Location on the page in normalized coords */ NormalizedRect rect; /** * Associated pixmap or NULL if not present * * For each node, it is guaranteed that there's no more than one pixmap * along the path from the root to the node itself. * In fact, it is very frequent that a leaf node has no pixmap and one * of its ancestors has. Such a situation shows, for example, when the * parent tile still has a dirty tile from a previous lower zoom level. */ QPixmap *pixmap; /** * Rotation of this individual tile. * * A rotation to the page does not immediately rotates the pixmaps in * cache. This operation happens when pixmaps are going to be used. */ Rotation rotation; /** * Whether the tile needs to be repainted (after a zoom or rotation) * If a tile doesn't have a pixmap but all its children are updated * (dirty = false), the parent tile is also considered updated. */ bool dirty; /** * Distance between the tile and the viewport. * This is used by the evicting algorithm. */ double distance; /** * Children tiles * When a tile is split into multiple tiles, they're added as children. * nTiles can be either 0 (in leaf tiles) or 4 (in split tiles). */ TileNode *tiles; int nTiles; TileNode *parent; }; /** * @short Tiles management * * This class has direct access to all tiles and handles how they should be * stored, deleted and retrieved. Each tiles manager only handles one page. * * The tiles manager is a tree of tiles. At first the page is divided in a 4x4 * grid of 16 tiles. Then each of these tiles can be recursively split in 4 * subtiles so that we keep the size of each pixmap inside a safe interval. */ class TilesManager { public: enum TileLeaf { TerminalTile, ///< Return tiles without children PixmapTile ///< Return only tiles with pixmap }; TilesManager( int pageNumber, int width, int height, Rotation rotation = Rotation0 ); ~TilesManager(); /** * Sets the pixmap of the tiles covered by @p rect (which represents * the location of @p pixmap on the page). * @p pixmap may cover an area which contains multiple tiles. So each * tile we get a cropped part of the @p pixmap. * * Also it checks the dimensions of the given parameters against the * current request as to avoid setting pixmaps of late requests. */ - void setPixmap( const QPixmap *pixmap, const NormalizedRect &rect ); + void setPixmap( const QPixmap *pixmap, const NormalizedRect &rect, bool isPartialPixmap ); /** * Checks whether all tiles intersecting with @p rect are available. * Returns false if at least one tile needs to be repainted (the tile * is dirty). */ bool hasPixmap( const NormalizedRect &rect ); /** * Returns a list of all tiles intersecting with @p rect. * * As to avoid requests of big areas, each traversed tile is checked * for its size and split if necessary. * * @param tileLeaf Indicate the type of tile to return */ QList tilesAt( const NormalizedRect &rect, TileLeaf tileLeaf ); /** * The total memory consumed by the tiles manager */ qulonglong totalMemory() const; /** * Removes at least @p numberOfBytes bytes worth of tiles (least ranked * tiles are removed first). * Set @p visibleRect to the visible region of the page. Set a * @p visiblePageNumber if the current page is not visible. * Visible tiles are not discarded. */ void cleanupPixmapMemory( qulonglong numberOfBytes, const NormalizedRect &visibleRect, int visiblePageNumber ); /** * Checks whether a given region has already been requested */ bool isRequesting( const NormalizedRect &rect, int pageWidth, int pageHeight ) const; /** * Sets a region to be requested so the tiles manager knows which * pixmaps to expect and discard those not useful anymore (late pixmaps) */ void setRequest( const NormalizedRect &rect, int pageWidth, int pageHeight ); /** * Inform the new size of the page and mark all tiles to repaint */ void setSize( int width, int height ); /** * Gets the width of the page in tiles manager */ int width() const; /** * Gets the height of the page in tiles manager */ int height() const; /** * Inform the new rotation of the page */ void setRotation( Rotation rotation ); Rotation rotation() const; /** * Mark all tiles as dirty */ void markDirty(); /** * Returns a rotated NormalizedRect given a @p rotation */ static NormalizedRect toRotatedRect( const NormalizedRect &rect, Rotation rotation ); /** * Returns a non rotated version of @p rect, which is rotated by @p rotation */ static NormalizedRect fromRotatedRect( const NormalizedRect &rect, Rotation rotation ); private: class Private; Private * const d; friend class Private; }; } #endif // _OKULAR_TILES_MANAGER_P_H_ diff --git a/generators/chm/generator_chm.cpp b/generators/chm/generator_chm.cpp index ada9b3d53..3befb5177 100644 --- a/generators/chm/generator_chm.cpp +++ b/generators/chm/generator_chm.cpp @@ -1,448 +1,449 @@ /*************************************************************************** * Copyright (C) 2005 by Piotr SzymaĹ„ski * * Copyright (C) 2008 by Albert Astals Cid * * * * 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. * ***************************************************************************/ #include "generator_chm.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include OKULAR_EXPORT_PLUGIN(CHMGenerator, "libokularGenerator_chmlib.json") static QString absolutePath( const QString &baseUrl, const QString &path ) { QString absPath; if ( path.startsWith(QLatin1Char( '/' )) ) { // already absolute absPath = path; } else { QUrl url = QUrl::fromLocalFile( baseUrl ).adjusted(QUrl::RemoveFilename); url.setPath( url.path() + path ); absPath = url.toLocalFile(); } return absPath; } CHMGenerator::CHMGenerator( QObject *parent, const QVariantList &args ) : Okular::Generator( parent, args ) { setFeature( TextExtraction ); m_syncGen=0; m_file=0; m_request = 0; } CHMGenerator::~CHMGenerator() { delete m_syncGen; } bool CHMGenerator::loadDocument( const QString & fileName, QVector< Okular::Page * > & pagesVector ) { m_file = new EBook_CHM(); m_file = EBook::loadFile( fileName ); if (!m_file) { return false; } m_fileName=fileName; QList< EBookTocEntry > topics; m_file->getTableOfContents(topics); // fill m_docSyn QMap lastIndentElement; QMap tmpPageList; int pageNum = 0; foreach(const EBookTocEntry &e, topics) { QDomElement item = m_docSyn.createElement(e.name); if (!e.url.isEmpty()) { QString url = e.url.toString(); item.setAttribute(QStringLiteral("ViewportName"), url); if(!tmpPageList.contains(url)) {//add a page only once tmpPageList.insert(url, pageNum); pageNum++; } } item.setAttribute(QStringLiteral("Icon"), e.iconid); if (e.indent == 0) m_docSyn.appendChild(item); else lastIndentElement[e.indent - 1].appendChild(item); lastIndentElement[e.indent] = item; } // fill m_urlPage and m_pageUrl QList pageList; m_file->enumerateFiles(pageList); const QUrl home = m_file->homeUrl(); if (home.path() != QLatin1String("/")) pageList.prepend(home); m_pageUrl.resize(pageNum); foreach (const QUrl &qurl, pageList) { QString url = qurl.toString(); const QString urlLower = url.toLower(); if (!urlLower.endsWith(QLatin1String(".html")) && !urlLower.endsWith(QLatin1String(".htm"))) continue; int pos = url.indexOf (QLatin1Char(('#'))); // insert the url into the maps, but insert always the variant without the #ref part QString tmpUrl = pos == -1 ? url : url.left(pos); // url already there, abort insertion if (m_urlPage.contains(tmpUrl)) continue; int foundPage = tmpPageList.value(tmpUrl, -1); if (foundPage != -1 ) { m_urlPage.insert(tmpUrl, foundPage); m_pageUrl[foundPage] = tmpUrl; } else { //add pages not present in toc m_urlPage.insert(tmpUrl, pageNum); m_pageUrl.append(tmpUrl); pageNum++; } } pagesVector.resize(m_pageUrl.count()); m_textpageAddedList.fill(false, pagesVector.count()); m_rectsGenerated.fill(false, pagesVector.count()); if (!m_syncGen) { m_syncGen = new KHTMLPart(); } disconnect( m_syncGen, 0, this, 0 ); for (int i = 0; i < m_pageUrl.count(); ++i) { preparePageForSyncOperation(m_pageUrl.at(i)); pagesVector[ i ] = new Okular::Page (i, m_syncGen->view()->contentsWidth(), m_syncGen->view()->contentsHeight(), Okular::Rotation0 ); } connect( m_syncGen, SIGNAL(completed()), this, SLOT(slotCompleted()) ); connect( m_syncGen, &KParts::ReadOnlyPart::canceled, this, &CHMGenerator::slotCompleted ); return true; } bool CHMGenerator::doCloseDocument() { // delete the document information of the old document delete m_file; m_file=0; m_textpageAddedList.clear(); m_rectsGenerated.clear(); m_urlPage.clear(); m_pageUrl.clear(); m_docSyn.clear(); if (m_syncGen) { m_syncGen->closeUrl(); } return true; } void CHMGenerator::preparePageForSyncOperation(const QString & url) { QString pAddress = QStringLiteral("ms-its:") + m_fileName + QStringLiteral("::") + m_file->urlToPath(QUrl(url)); m_chmUrl = url; m_syncGen->openUrl(QUrl(pAddress)); m_syncGen->view()->layout(); QEventLoop loop; connect( m_syncGen, SIGNAL(completed()), &loop, SLOT(quit()) ); connect( m_syncGen, &KParts::ReadOnlyPart::canceled, &loop, &QEventLoop::quit ); // discard any user input, otherwise it breaks the "synchronicity" of this // function loop.exec( QEventLoop::ExcludeUserInputEvents ); } void CHMGenerator::slotCompleted() { if ( !m_request ) return; QImage image( m_request->width(), m_request->height(), QImage::Format_ARGB32 ); image.fill( Qt::white ); QPainter p( &image ); QRect r( 0, 0, m_request->width(), m_request->height() ); bool moreToPaint; m_syncGen->paint( &p, r, 0, &moreToPaint ); p.end(); if ( !m_textpageAddedList.at( m_request->pageNumber() ) ) { additionalRequestData(); m_textpageAddedList[ m_request->pageNumber() ] = true; } m_syncGen->closeUrl(); m_chmUrl = QString(); userMutex()->unlock(); Okular::PixmapRequest *req = m_request; m_request = 0; if ( !req->page()->isBoundingBoxKnown() ) updatePageBoundingBox( req->page()->number(), Okular::Utils::imageBoundingBox( &image ) ); req->page()->setPixmap( req->observer(), new QPixmap( QPixmap::fromImage( image ) ) ); signalPixmapRequestDone( req ); } Okular::DocumentInfo CHMGenerator::generateDocumentInfo( const QSet &keys ) const { Okular::DocumentInfo docInfo; if ( keys.contains( Okular::DocumentInfo::MimeType ) ) docInfo.set( Okular::DocumentInfo::MimeType, QStringLiteral("application/x-chm") ); if ( keys.contains( Okular::DocumentInfo::Title ) ) docInfo.set( Okular::DocumentInfo::Title, m_file->title() ); return docInfo; } const Okular::DocumentSynopsis * CHMGenerator::generateDocumentSynopsis() { return &m_docSyn; } bool CHMGenerator::canGeneratePixmap () const { bool isLocked = true; if ( userMutex()->tryLock() ) { userMutex()->unlock(); isLocked = false; } return !isLocked; } void CHMGenerator::generatePixmap( Okular::PixmapRequest * request ) { int requestWidth = request->width(); int requestHeight = request->height(); userMutex()->lock(); QString url= m_pageUrl[request->pageNumber()]; QString pAddress= QStringLiteral("ms-its:") + m_fileName + QStringLiteral("::") + m_file->urlToPath(QUrl(url)); m_chmUrl = url; m_syncGen->view()->resizeContents(requestWidth,requestHeight); m_request=request; // will emit openURL without problems m_syncGen->openUrl ( QUrl(pAddress) ); } void CHMGenerator::recursiveExploreNodes(DOM::Node node,Okular::TextPage *tp) { if (node.nodeType() == DOM::Node::TEXT_NODE && !node.getRect().isNull()) { QString nodeText=node.nodeValue().string(); QRect r=node.getRect(); int vWidth=m_syncGen->view()->width(); int vHeight=m_syncGen->view()->height(); Okular::NormalizedRect *nodeNormRect; #define NOEXP #ifndef NOEXP int x,y,height; int x_next,y_next,height_next; int nodeTextLength = nodeText.length(); if (nodeTextLength==1) { nodeNormRect=new Okular::NormalizedRect (r,vWidth,vHeight); tp->append(nodeText,nodeNormRect,nodeNormRect->bottom,0,(nodeText=="\n")); } else { for (int i=0;iappend(nodeText,nodeNormRect/*,0*/); #endif } DOM::Node child = node.firstChild(); while ( !child.isNull() ) { recursiveExploreNodes(child,tp); child = child.nextSibling(); } } void CHMGenerator::additionalRequestData() { Okular::Page * page=m_request->page(); const bool genObjectRects = !m_rectsGenerated.at( m_request->page()->number() ); const bool genTextPage = !m_request->page()->hasTextPage() && genObjectRects; if (genObjectRects || genTextPage ) { DOM::HTMLDocument domDoc=m_syncGen->htmlDocument(); // only generate object info when generating a full page not a thumbnail if ( genObjectRects ) { QLinkedList< Okular::ObjectRect * > objRects; int xScale=m_syncGen->view()->width(); int yScale=m_syncGen->view()->height(); // getting links DOM::HTMLCollection coll=domDoc.links(); DOM::Node n; QRect r; if (! coll.isNull() ) { int size=coll.length(); for(int i=0;ipage()->setObjectRects( objRects ); m_rectsGenerated[ m_request->page()->number() ] = true; } if ( genTextPage ) { Okular::TextPage *tp=new Okular::TextPage(); recursiveExploreNodes(domDoc,tp); page->setTextPage (tp); } } } -Okular::TextPage* CHMGenerator::textPage( Okular::Page * page ) +Okular::TextPage* CHMGenerator::textPage( Okular::TextRequest * request ) { userMutex()->lock(); + const Okular::Page *page = request->page(); m_syncGen->view()->resize(page->width(), page->height()); preparePageForSyncOperation(m_pageUrl[page->number()]); Okular::TextPage *tp=new Okular::TextPage(); recursiveExploreNodes( m_syncGen->htmlDocument(), tp); userMutex()->unlock(); return tp; } QVariant CHMGenerator::metaData( const QString &key, const QVariant &option ) const { if ( key == QLatin1String("NamedViewport") && !option.toString().isEmpty() ) { const int pos = option.toString().indexOf(QLatin1Char('#')); QString tmpUrl = pos == -1 ? option.toString() : option.toString().left(pos); Okular::DocumentViewport viewport; QMap::const_iterator it = m_urlPage.find(tmpUrl); if (it != m_urlPage.end()) { viewport.pageNumber = it.value(); return viewport.toString(); } } else if ( key == QLatin1String("DocumentTitle") ) { return m_file->title(); } return QVariant(); } /* kate: replace-tabs on; tab-width 4; */ #include "generator_chm.moc" diff --git a/generators/chm/generator_chm.h b/generators/chm/generator_chm.h index 25295bf90..13fa7108f 100644 --- a/generators/chm/generator_chm.h +++ b/generators/chm/generator_chm.h @@ -1,71 +1,71 @@ /*************************************************************************** * Copyright (C) 2005 by Piotr SzymaĹ„ski * * Copyright (C) 2008 by Albert Astals Cid * * * * 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. * ***************************************************************************/ #ifndef _OKULAR_CHMGENERATOR_H_ #define _OKULAR_CHMGENERATOR_H_ #include #include #include "lib/ebook_chm.h" #include class KHTMLPart; namespace Okular { class TextPage; } namespace DOM { class Node; } class CHMGenerator : public Okular::Generator { Q_OBJECT Q_INTERFACES( Okular::Generator ) public: CHMGenerator( QObject *parent, const QVariantList &args ); ~CHMGenerator(); bool loadDocument( const QString & fileName, QVector< Okular::Page * > & pagesVector ) override; Okular::DocumentInfo generateDocumentInfo( const QSet &keys ) const override; const Okular::DocumentSynopsis * generateDocumentSynopsis() override; bool canGeneratePixmap() const override; void generatePixmap( Okular::PixmapRequest * request ) override; QVariant metaData( const QString & key, const QVariant & option ) const override; public Q_SLOTS: void slotCompleted(); protected: bool doCloseDocument() override; - Okular::TextPage* textPage( Okular::Page *page ) override; + Okular::TextPage* textPage( Okular::TextRequest *request ) override; private: void additionalRequestData(); void recursiveExploreNodes( DOM::Node node, Okular::TextPage *tp ); void preparePageForSyncOperation( const QString &url ); QMap m_urlPage; QVector m_pageUrl; Okular::DocumentSynopsis m_docSyn; EBook* m_file; KHTMLPart *m_syncGen; QString m_fileName; QString m_chmUrl; Okular::PixmapRequest* m_request; QBitArray m_textpageAddedList; QBitArray m_rectsGenerated; }; #endif diff --git a/generators/djvu/generator_djvu.cpp b/generators/djvu/generator_djvu.cpp index 1ddacfd4d..644f746ca 100644 --- a/generators/djvu/generator_djvu.cpp +++ b/generators/djvu/generator_djvu.cpp @@ -1,441 +1,442 @@ /*************************************************************************** * Copyright (C) 2006 by Pino Toscano * * * * 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. * ***************************************************************************/ #include "generator_djvu.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include static void recurseCreateTOC( QDomDocument &maindoc, const QDomNode &parent, QDomNode &parentDestination, KDjVu *djvu ) { QDomNode n = parent.firstChild(); while( !n.isNull() ) { QDomElement el = n.toElement(); QDomElement newel = maindoc.createElement( el.attribute( QStringLiteral("title") ) ); parentDestination.appendChild( newel ); QString dest; if ( !( dest = el.attribute( QStringLiteral("PageNumber") ) ).isEmpty() ) { Okular::DocumentViewport vp; vp.pageNumber = dest.toInt() - 1; newel.setAttribute( QStringLiteral("Viewport"), vp.toString() ); } else if ( !( dest = el.attribute( QStringLiteral("PageName") ) ).isEmpty() ) { Okular::DocumentViewport vp; vp.pageNumber = djvu->pageNumber( dest ); newel.setAttribute( QStringLiteral("Viewport"), vp.toString() ); } else if ( !( dest = el.attribute( QStringLiteral("URL") ) ).isEmpty() ) { newel.setAttribute( QStringLiteral("URL"), dest ); } if ( el.hasChildNodes() ) { recurseCreateTOC( maindoc, n, newel, djvu ); } n = n.nextSibling(); } } OKULAR_EXPORT_PLUGIN(DjVuGenerator, "libokularGenerator_djvu.json") DjVuGenerator::DjVuGenerator( QObject *parent, const QVariantList &args ) : Okular::Generator( parent, args ), m_docSyn( nullptr ) { setFeature( TextExtraction ); setFeature( Threaded ); setFeature( PrintPostscript ); if ( Okular::FilePrinter::ps2pdfAvailable() ) setFeature( PrintToFile ); m_djvu = new KDjVu(); m_djvu->setCacheEnabled( false ); } DjVuGenerator::~DjVuGenerator() { delete m_djvu; } bool DjVuGenerator::loadDocument( const QString & fileName, QVector< Okular::Page * > & pagesVector ) { QMutexLocker locker( userMutex() ); if ( !m_djvu->openFile( fileName ) ) return false; locker.unlock(); loadPages( pagesVector, 0 ); return true; } bool DjVuGenerator::doCloseDocument() { userMutex()->lock(); m_djvu->closeFile(); userMutex()->unlock(); delete m_docSyn; m_docSyn = nullptr; return true; } QImage DjVuGenerator::image( Okular::PixmapRequest *request ) { userMutex()->lock(); QImage img = m_djvu->image( request->pageNumber(), request->width(), request->height(), request->page()->rotation() ); userMutex()->unlock(); return img; } Okular::DocumentInfo DjVuGenerator::generateDocumentInfo( const QSet &keys ) const { Okular::DocumentInfo docInfo; if ( keys.contains( Okular::DocumentInfo::MimeType ) ) docInfo.set( Okular::DocumentInfo::MimeType, QStringLiteral("image/vnd.djvu") ); if ( m_djvu ) { // compile internal structure reading properties from KDjVu if ( keys.contains( Okular::DocumentInfo::Author ) ) docInfo.set( Okular::DocumentInfo::Title, m_djvu->metaData( QStringLiteral("title") ).toString() ); if ( keys.contains( Okular::DocumentInfo::Author ) ) docInfo.set( Okular::DocumentInfo::Author, m_djvu->metaData( QStringLiteral("author") ).toString() ); if ( keys.contains( Okular::DocumentInfo::CreationDate ) ) docInfo.set( Okular::DocumentInfo::CreationDate, m_djvu->metaData( QStringLiteral("year") ).toString() ); if ( keys.contains( Okular::DocumentInfo::CustomKeys ) ) { docInfo.set( QStringLiteral("editor"), m_djvu->metaData( QStringLiteral("editor") ).toString(), i18n( "Editor" ) ); docInfo.set( QStringLiteral("publisher"), m_djvu->metaData( QStringLiteral("publisher") ).toString(), i18n( "Publisher" ) ); docInfo.set( QStringLiteral("volume"), m_djvu->metaData( QStringLiteral("volume") ).toString(), i18n( "Volume" ) ); docInfo.set( QStringLiteral("documentType"), m_djvu->metaData( QStringLiteral("documentType") ).toString(), i18n( "Type of document" ) ); QVariant numcomponents = m_djvu->metaData( QStringLiteral("componentFile") ); docInfo.set( QStringLiteral("componentFile"), numcomponents.type() != QVariant::Int ? i18nc( "Unknown number of component files", "Unknown" ) : numcomponents.toString(), i18n( "Component Files" ) ); } } return docInfo; } const Okular::DocumentSynopsis * DjVuGenerator::generateDocumentSynopsis() { QMutexLocker locker( userMutex() ); if ( m_docSyn ) return m_docSyn; const QDomDocument *doc = m_djvu->documentBookmarks(); if ( doc ) { m_docSyn = new Okular::DocumentSynopsis(); recurseCreateTOC( *m_docSyn, *doc, *m_docSyn, m_djvu ); } locker.unlock(); return m_docSyn; } bool DjVuGenerator::print( QPrinter& printer ) { bool result = false; // Create tempfile to write to QTemporaryFile tf(QDir::tempPath() + QLatin1String("/okular_XXXXXX.ps")); if ( !tf.open() ) return false; const QString fileName = tf.fileName(); QMutexLocker locker( userMutex() ); QList pageList = Okular::FilePrinter::pageList( printer, m_djvu->pages().count(), document()->currentPage() + 1, document()->bookmarkedPageList() ); if ( m_djvu->exportAsPostScript( &tf, pageList ) ) { tf.setAutoRemove( false ); tf.close(); int ret = Okular::FilePrinter::printFile( printer, fileName, document()->orientation(), Okular::FilePrinter::SystemDeletesFiles, Okular::FilePrinter::ApplicationSelectsPages, document()->bookmarkedPageRange() ); result = ( ret >=0 ); } return result; } QVariant DjVuGenerator::metaData( const QString &key, const QVariant &option ) const { Q_UNUSED( option ) if ( key == QLatin1String("DocumentTitle") ) { return m_djvu->metaData( QStringLiteral("title") ); } return QVariant(); } -Okular::TextPage* DjVuGenerator::textPage( Okular::Page *page ) +Okular::TextPage* DjVuGenerator::textPage( Okular::TextRequest *request ) { userMutex()->lock(); + const Okular::Page *page = request->page(); QList te; #if 0 m_djvu->textEntities( page->number(), "char" ); #endif if ( te.isEmpty() ) te = m_djvu->textEntities( page->number(), QStringLiteral("word") ); if ( te.isEmpty() ) te = m_djvu->textEntities( page->number(), QStringLiteral("line") ); userMutex()->unlock(); QList::ConstIterator it = te.constBegin(); QList::ConstIterator itEnd = te.constEnd(); QList words; const KDjVu::Page* djvupage = m_djvu->pages().at( page->number() ); for ( ; it != itEnd; ++it ) { const KDjVu::TextEntity& cur = *it; words.append( new Okular::TextEntity( cur.text(), new Okular::NormalizedRect( cur.rect(), djvupage->width(), djvupage->height() ) ) ); } Okular::TextPage *textpage = new Okular::TextPage( words ); return textpage; } void DjVuGenerator::loadPages( QVector & pagesVector, int rotation ) { const QVector &djvu_pages = m_djvu->pages(); int numofpages = djvu_pages.count(); pagesVector.resize( numofpages ); for ( int i = 0; i < numofpages; ++i ) { const KDjVu::Page *p = djvu_pages.at( i ); if (pagesVector[i]) delete pagesVector[i]; int w = p->width(); int h = p->height(); if ( rotation % 2 == 1 ) qSwap( w, h ); Okular::Page *page = new Okular::Page( i, w, h, (Okular::Rotation)( p->orientation() + rotation ) ); pagesVector[i] = page; QList annots; QList links; userMutex()->lock(); m_djvu->linksAndAnnotationsForPage( i, &links, &annots ); userMutex()->unlock(); if ( !links.isEmpty() ) { QLinkedList rects; QList::ConstIterator it = links.constBegin(); QList::ConstIterator itEnd = links.constEnd(); for ( ; it != itEnd; ++it ) { KDjVu::Link *curlink = (*it); Okular::ObjectRect *newrect = convertKDjVuLink( i, curlink ); if ( newrect ) rects.append( newrect ); // delete the links as soon as we process them delete curlink; } if ( rects.count() > 0 ) page->setObjectRects( rects ); } if ( !annots.isEmpty() ) { QList::ConstIterator it = annots.constBegin(); QList::ConstIterator itEnd = annots.constEnd(); for ( ; it != itEnd; ++it ) { KDjVu::Annotation *ann = (*it); Okular::Annotation *newann = convertKDjVuAnnotation( w, h, ann ); if ( newann ) page->addAnnotation( newann ); // delete the annotations as soon as we process them delete ann; } } } } Okular::ObjectRect* DjVuGenerator::convertKDjVuLink( int page, KDjVu::Link * link ) const { int newpage = -1; Okular::Action *newlink = nullptr; Okular::ObjectRect *newrect = nullptr; switch ( link->type() ) { case KDjVu::Link::PageLink: { KDjVu::PageLink* l = static_cast( link ); bool ok = true; QString target = l->page(); if ( ( target.length() > 0 ) && target.at(0) == QLatin1Char( '#' ) ) target.remove( 0, 1 ); int tmppage = target.toInt( &ok ); if ( ok || target.isEmpty() ) { Okular::DocumentViewport vp; if ( !target.isEmpty() ) { vp.pageNumber = ( target.at(0) == QLatin1Char( '+' ) || target.at(0) == QLatin1Char( '-' ) ) ? page + tmppage : tmppage - 1; newpage = vp.pageNumber; } newlink = new Okular::GotoAction( QString(), vp ); } break; } case KDjVu::Link::UrlLink: { KDjVu::UrlLink* l = static_cast( link ); QString url = l->url(); newlink = new Okular::BrowseAction( QUrl(url) ); break; } } if ( newlink ) { const KDjVu::Page* p = m_djvu->pages().value( newpage == -1 ? page : newpage ); if ( !p ) p = m_djvu->pages().at( page ); int width = p->width(); int height = p->height(); bool scape_orientation = false; // hack by tokoe, should always create default page if ( scape_orientation ) qSwap( width, height ); switch ( link->areaType() ) { case KDjVu::Link::RectArea: case KDjVu::Link::EllipseArea: { QRect r( QPoint( link->point().x(), p->height() - link->point().y() - link->size().height() ), link->size() ); bool ellipse = ( link->areaType() == KDjVu::Link::EllipseArea ); newrect = new Okular::ObjectRect( Okular::NormalizedRect( Okular::Utils::rotateRect( r, width, height, 0 ), width, height ), ellipse, Okular::ObjectRect::Action, newlink ); break; } case KDjVu::Link::PolygonArea: { QPolygon poly = link->polygon(); QPolygonF newpoly; for ( int i = 0; i < poly.count(); ++i ) { int x = poly.at(i).x(); int y = poly.at(i).y(); if ( scape_orientation ) qSwap( x, y ); else { y = height - y; } newpoly << QPointF( (double)(x)/width, (double)(y)/height ); } if ( !newpoly.isEmpty() ) { newpoly << newpoly.first(); newrect = new Okular::ObjectRect( newpoly, Okular::ObjectRect::Action, newlink ); } break; } default: ; } if ( !newrect ) { delete newlink; } } return newrect; } Okular::Annotation* DjVuGenerator::convertKDjVuAnnotation( int w, int h, KDjVu::Annotation * ann ) const { Okular::Annotation *newann = nullptr; switch ( ann->type() ) { case KDjVu::Annotation::TextAnnotation: { KDjVu::TextAnnotation* txtann = static_cast( ann ); Okular::TextAnnotation * newtxtann = new Okular::TextAnnotation(); // boundary QRect rect( QPoint( txtann->point().x(), h - txtann->point().y() - txtann->size().height() ), txtann->size() ); newtxtann->setBoundingRectangle( Okular::NormalizedRect( Okular::Utils::rotateRect( rect, w, h, 0 ), w, h ) ); // type newtxtann->setTextType( txtann->inlineText() ? Okular::TextAnnotation::InPlace : Okular::TextAnnotation::Linked ); newtxtann->style().setOpacity( txtann->color().alphaF() ); // FIXME remove once the annotation text handling is fixed newtxtann->setContents( ann->comment() ); newann = newtxtann; break; } case KDjVu::Annotation::LineAnnotation: { KDjVu::LineAnnotation* lineann = static_cast( ann ); Okular::LineAnnotation * newlineann = new Okular::LineAnnotation(); // boundary QPoint a( lineann->point().x(), h - lineann->point().y() ); QPoint b( lineann->point2().x(), h - lineann->point2().y() ); QRect rect = QRect( a, b ).normalized(); newlineann->setBoundingRectangle( Okular::NormalizedRect( Okular::Utils::rotateRect( rect, w, h, 0 ), w, h ) ); // line points QLinkedList points; points.append( Okular::NormalizedPoint( a.x(), a.y(), w, h ) ); points.append( Okular::NormalizedPoint( b.x(), b.y(), w, h ) ); newlineann->setLinePoints( points ); // arrow? if ( lineann->isArrow() ) newlineann->setLineEndStyle( Okular::LineAnnotation::OpenArrow ); // width newlineann->style().setWidth( lineann->width() ); newann = newlineann; break; } } if ( newann ) { // setting the common parameters newann->style().setColor( ann->color() ); newann->setContents( ann->comment() ); // creating an id as name for the annotation QString uid = QUuid::createUuid().toString(); uid.remove( 0, 1 ); uid.chop( 1 ); uid.remove( QLatin1Char( '-' ) ); newann->setUniqueName( uid ); // is external newann->setFlags( newann->flags() | Okular::Annotation::External ); } return newann; } #include "generator_djvu.moc" diff --git a/generators/djvu/generator_djvu.h b/generators/djvu/generator_djvu.h index c5d3970aa..4e11b5c0f 100644 --- a/generators/djvu/generator_djvu.h +++ b/generators/djvu/generator_djvu.h @@ -1,58 +1,58 @@ /*************************************************************************** * Copyright (C) 2006 by Pino Toscano * * * * 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. * ***************************************************************************/ #ifndef _GENERATOR_DJVU_H_ #define _GENERATOR_DJVU_H_ #include #include #include "kdjvu.h" namespace Okular { class Annotation; class ObjectRect; } class DjVuGenerator : public Okular::Generator { Q_OBJECT Q_INTERFACES( Okular::Generator ) public: DjVuGenerator( QObject *parent, const QVariantList &args ); ~DjVuGenerator(); bool loadDocument( const QString & fileName, QVector & pagesVector ) override; // document information Okular::DocumentInfo generateDocumentInfo( const QSet &keys ) const override; const Okular::DocumentSynopsis * generateDocumentSynopsis() override; // printing bool print( QPrinter& printer ) override; QVariant metaData( const QString & key, const QVariant & option ) const override; protected: bool doCloseDocument() override; // pixmap generation QImage image( Okular::PixmapRequest *request ) override; - Okular::TextPage* textPage( Okular::Page *page ) override; + Okular::TextPage* textPage( Okular::TextRequest *request ) override; private: void loadPages( QVector & pagesVector, int rotation ); Okular::ObjectRect* convertKDjVuLink( int page, KDjVu::Link * link ) const; Okular::Annotation* convertKDjVuAnnotation( int w, int h, KDjVu::Annotation * ann ) const; KDjVu *m_djvu; Okular::DocumentSynopsis *m_docSyn; }; #endif diff --git a/generators/dvi/generator_dvi.cpp b/generators/dvi/generator_dvi.cpp index 0815188ef..16a64f940 100644 --- a/generators/dvi/generator_dvi.cpp +++ b/generators/dvi/generator_dvi.cpp @@ -1,577 +1,579 @@ /*************************************************************************** * Copyright (C) 2006-2009 by Luigi Toscano * * * * 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. * ***************************************************************************/ #include #include #include #include #include #include #include "generator_dvi.h" #include "debug_dvi.h" #include "dviFile.h" #include "dviPageInfo.h" #include "dviRenderer.h" #include "pageSize.h" #include "dviexport.h" #include "TeXFont.h" #include #include #include #include #include #include #include #include #include #include #include #ifdef DVI_OPEN_BUSYLOOP #include #endif OKULAR_EXPORT_PLUGIN(DviGenerator, "libokularGenerator_dvi.json") DviGenerator::DviGenerator( QObject *parent, const QVariantList &args ) : Okular::Generator( parent, args ), m_fontExtracted( false ), m_docSynopsis( nullptr ), m_dviRenderer( nullptr ) { setFeature( Threaded ); setFeature( TextExtraction ); setFeature( FontInfo ); setFeature( PrintPostscript ); if ( Okular::FilePrinter::ps2pdfAvailable() ) setFeature( PrintToFile ); } bool DviGenerator::loadDocument( const QString & fileName, QVector< Okular::Page * > &pagesVector ) { //qCDebug(OkularDviDebug) << "file:" << fileName; QUrl base( QUrl::fromLocalFile(fileName) ); (void)userMutex(); m_dviRenderer = new dviRenderer(documentMetaData(TextHintingMetaData, QVariant()).toBool()); connect(m_dviRenderer, &dviRenderer::error, this, &DviGenerator::error); connect(m_dviRenderer, &dviRenderer::warning, this, &DviGenerator::warning); connect(m_dviRenderer, &dviRenderer::notice, this, &DviGenerator::notice); #ifdef DVI_OPEN_BUSYLOOP static const ushort s_waitTime = 800; // milliseconds static const int s_maxIterations = 10; int iter = 0; for ( ; !m_dviRenderer->isValidFile( fileName ) && iter < s_maxIterations; ++iter ) { qCDebug(OkularDviDebug).nospace() << "file not valid after iteration #" << iter << "/" << s_maxIterations << ", waiting for " << s_waitTime; QThread::msleep(s_waitTime); } if ( iter >= s_maxIterations && !m_dviRenderer->isValidFile( fileName ) ) { qCDebug(OkularDviDebug) << "file still not valid after" << s_maxIterations; delete m_dviRenderer; m_dviRenderer = 0; return false; } #else if ( !m_dviRenderer->isValidFile( fileName ) ) { delete m_dviRenderer; m_dviRenderer = nullptr; return false; } #endif if ( ! m_dviRenderer->setFile( fileName, base ) ) { delete m_dviRenderer; m_dviRenderer = nullptr; return false; } qCDebug(OkularDviDebug) << "# of pages:" << m_dviRenderer->dviFile->total_pages; m_resolution = dpi().height(); loadPages( pagesVector ); return true; } bool DviGenerator::doCloseDocument() { delete m_docSynopsis; m_docSynopsis = nullptr; delete m_dviRenderer; m_dviRenderer = nullptr; m_linkGenerated.clear(); m_fontExtracted = false; return true; } void DviGenerator::fillViewportFromAnchor( Okular::DocumentViewport &vp, const Anchor &anch, const Okular::Page *page ) const { fillViewportFromAnchor( vp, anch, page->width(), page->height() ); } void DviGenerator::fillViewportFromAnchor( Okular::DocumentViewport &vp, const Anchor &anch, int pW, int pH ) const { vp.pageNumber = anch.page - 1; SimplePageSize ps = m_dviRenderer->sizeOfPage( vp.pageNumber ); double resolution = 0; if (ps.isValid()) resolution = (double)(pW)/ps.width().getLength_in_inch(); else resolution = m_resolution; double py = (double)anch.distance_from_top.getLength_in_inch()*resolution + 0.5; vp.rePos.normalizedX = 0.5; vp.rePos.normalizedY = py/(double)pH; vp.rePos.enabled = true; vp.rePos.pos = Okular::DocumentViewport::Center; } QLinkedList DviGenerator::generateDviLinks( const dviPageInfo *pageInfo ) { QLinkedList dviLinks; int pageWidth = pageInfo->width, pageHeight = pageInfo->height; foreach( const Hyperlink &dviLink, pageInfo->hyperLinkList ) { QRect boxArea = dviLink.box; double nl = (double)boxArea.left() / pageWidth, nt = (double)boxArea.top() / pageHeight, nr = (double)boxArea.right() / pageWidth, nb = (double)boxArea.bottom() / pageHeight; QString linkText = dviLink.linkText; if ( linkText.startsWith(QLatin1String("#")) ) linkText = linkText.mid( 1 ); Anchor anch = m_dviRenderer->findAnchor( linkText ); Okular::Action *okuLink = nullptr; /* distinguish between local (-> anchor) and remote links */ if (anch.isValid()) { /* internal link */ Okular::DocumentViewport vp; fillViewportFromAnchor( vp, anch, pageWidth, pageHeight ); okuLink = new Okular::GotoAction( QLatin1String(""), vp ); } else { okuLink = new Okular::BrowseAction( QUrl::fromUserInput( dviLink.linkText ) ); } if ( okuLink ) { Okular::ObjectRect *orlink = new Okular::ObjectRect( nl, nt, nr, nb, false, Okular::ObjectRect::Action, okuLink ); dviLinks.push_front( orlink ); } } return dviLinks; } QImage DviGenerator::image( Okular::PixmapRequest *request ) { dviPageInfo *pageInfo = new dviPageInfo(); pageSize ps; QImage ret; pageInfo->width = request->width(); pageInfo->height = request->height(); pageInfo->pageNumber = request->pageNumber() + 1; // pageInfo->resolution = m_resolution; QMutexLocker lock( userMutex() ); if ( m_dviRenderer ) { SimplePageSize s = m_dviRenderer->sizeOfPage( pageInfo->pageNumber ); /* if ( s.width() != pageInfo->width) */ // if (!useDocumentSpecifiedSize) // s = userPreferredSize; if (s.isValid()) { ps = s; /* it should be the user specified size, if any, instead */ } pageInfo->resolution = (double)(pageInfo->width)/ps.width().getLength_in_inch(); #if 0 qCDebug(OkularDviDebug) << *request << ", res:" << pageInfo->resolution << " - (" << pageInfo->width << "," << ps.width().getLength_in_inch() << ")," << ps.width().getLength_in_mm() << endl; #endif m_dviRenderer->drawPage( pageInfo ); if ( ! pageInfo->img.isNull() ) { qCDebug(OkularDviDebug) << "Image OK"; ret = pageInfo->img; if ( !m_linkGenerated[ request->pageNumber() ] ) { request->page()->setObjectRects( generateDviLinks( pageInfo ) ); m_linkGenerated[ request->pageNumber() ] = true; } } } lock.unlock(); delete pageInfo; return ret; } -Okular::TextPage* DviGenerator::textPage( Okular::Page *page ) +Okular::TextPage* DviGenerator::textPage( Okular::TextRequest *request ) { + const Okular::Page *page = request->page(); + qCDebug(OkularDviDebug); dviPageInfo *pageInfo = new dviPageInfo(); pageSize ps; pageInfo->width=page->width(); pageInfo->height=page->height(); pageInfo->pageNumber = page->number() + 1; pageInfo->resolution = m_resolution; QMutexLocker lock( userMutex() ); // get page text from m_dviRenderer Okular::TextPage *ktp = nullptr; if ( m_dviRenderer ) { SimplePageSize s = m_dviRenderer->sizeOfPage( pageInfo->pageNumber ); pageInfo->resolution = (double)(pageInfo->width)/ps.width().getLength_in_inch(); m_dviRenderer->getText( pageInfo ); lock.unlock(); ktp = extractTextFromPage( pageInfo ); } delete pageInfo; return ktp; } Okular::TextPage *DviGenerator::extractTextFromPage( dviPageInfo *pageInfo ) { QList textOfThePage; QVector::ConstIterator it = pageInfo->textBoxList.constBegin(); QVector::ConstIterator itEnd = pageInfo->textBoxList.constEnd(); QRect tmpRect; int pageWidth = pageInfo->width, pageHeight = pageInfo->height; for ( ; it != itEnd ; ++it ) { TextBox curTB = *it; #if 0 qCDebug(OkularDviDebug) << "orientation: " << orientation << ", curTB.box: " << curTB.box << ", tmpRect: " << tmpRect << ", ( " << pageWidth << "," << pageHeight << " )" < &keys ) const { Okular::DocumentInfo docInfo; if ( keys.contains( Okular::DocumentInfo::MimeType ) ) docInfo.set( Okular::DocumentInfo::MimeType, QStringLiteral("application/x-dvi") ); QMutexLocker lock( userMutex() ); if ( m_dviRenderer && m_dviRenderer->dviFile ) { dvifile *dvif = m_dviRenderer->dviFile; // read properties from dvif //docInfo.set( "filename", dvif->filename, i18n("Filename") ); if ( keys.contains( Okular::DocumentInfo::CustomKeys ) ) docInfo.set( QStringLiteral("generatorDate"), dvif->generatorString, i18n("Generator/Date") ); if ( keys.contains( Okular::DocumentInfo::Pages ) ) docInfo.set( Okular::DocumentInfo::Pages, QString::number( dvif->total_pages ) ); } return docInfo; } const Okular::DocumentSynopsis *DviGenerator::generateDocumentSynopsis() { if ( m_docSynopsis ) return m_docSynopsis; m_docSynopsis = new Okular::DocumentSynopsis(); userMutex()->lock(); QVector prebookmarks = m_dviRenderer->getPrebookmarks(); userMutex()->unlock(); if ( prebookmarks.isEmpty() ) return m_docSynopsis; QStack stack; QVector::ConstIterator it = prebookmarks.constBegin(); QVector::ConstIterator itEnd = prebookmarks.constEnd(); for( ; it != itEnd; ++it ) { QDomElement domel = m_docSynopsis->createElement( (*it).title ); Anchor a = m_dviRenderer->findAnchor((*it).anchorName); if ( a.isValid() ) { Okular::DocumentViewport vp; const Okular::Page *p = document()->page( a.page - 1 ); fillViewportFromAnchor( vp, a, (int)p->width(), (int)p->height() ); domel.setAttribute( QStringLiteral("Viewport"), vp.toString() ); } if ( stack.isEmpty() ) m_docSynopsis->appendChild( domel ); else { stack.top().appendChild( domel ); stack.pop(); } for ( int i = 0; i < (*it).noOfChildren; ++i ) stack.push( domel ); } return m_docSynopsis; } Okular::FontInfo::List DviGenerator::fontsForPage( int page ) { Q_UNUSED( page ); Okular::FontInfo::List list; // the list of the fonts is extracted once if ( m_fontExtracted ) return list; if ( m_dviRenderer && m_dviRenderer->dviFile && m_dviRenderer->dviFile->font_pool ) { QList fonts = m_dviRenderer->dviFile->font_pool->fontList; foreach (const TeXFontDefinition* font, fonts) { Okular::FontInfo of; QString name; int zoom = (int)(font->enlargement*100 + 0.5); #ifdef HAVE_FREETYPE if ( font->getFullFontName().isEmpty() ) { name = QStringLiteral( "%1, %2%" ) .arg( font->fontname ) .arg( zoom ); } else { name = QStringLiteral( "%1 (%2), %3%" ) .arg( font->fontname, font->getFullFontName(), QString::number(zoom) ); } #else name = QString( "%1, %2%" ) .arg( font->fontname ) .arg( zoom ); #endif of.setName( name ); QString fontFileName; if (!(font->flags & TeXFontDefinition::FONT_VIRTUAL)) { if ( font->font != nullptr ) fontFileName = font->font->errorMessage; else fontFileName = i18n("Font file not found"); if ( fontFileName.isEmpty() ) fontFileName = font->filename; } of.setFile( fontFileName ); Okular::FontInfo::FontType ft; switch ( font->getFontType() ) { case TeXFontDefinition::TEX_PK: ft = Okular::FontInfo::TeXPK; break; case TeXFontDefinition::TEX_VIRTUAL: ft = Okular::FontInfo::TeXVirtual; break; case TeXFontDefinition::TEX_FONTMETRIC: ft = Okular::FontInfo::TeXFontMetric; break; case TeXFontDefinition::FREETYPE: ft = Okular::FontInfo::TeXFreeTypeHandled; break; } of.setType( ft ); // DVI has not the concept of "font embedding" of.setEmbedType( Okular::FontInfo::NotEmbedded ); of.setCanBeExtracted( false ); list.append( of ); } m_fontExtracted = true; } return list; } void DviGenerator::loadPages( QVector< Okular::Page * > &pagesVector ) { QSize pageRequiredSize; int numofpages = m_dviRenderer->dviFile->total_pages; pagesVector.resize( numofpages ); m_linkGenerated.fill( false, numofpages ); //qCDebug(OkularDviDebug) << "resolution:" << m_resolution << ", dviFile->preferred?"; /* get the suggested size */ if ( m_dviRenderer->dviFile->suggestedPageSize ) { pageRequiredSize = m_dviRenderer->dviFile->suggestedPageSize->sizeInPixel( m_resolution ); } else { pageSize ps; pageRequiredSize = ps.sizeInPixel( m_resolution ); } for ( int i = 0; i < numofpages; ++i ) { //qCDebug(OkularDviDebug) << "getting status of page" << i << ":"; if ( pagesVector[i] ) { delete pagesVector[i]; } Okular::Page * page = new Okular::Page( i, pageRequiredSize.width(), pageRequiredSize.height(), Okular::Rotation0 ); pagesVector[i] = page; } qCDebug(OkularDviDebug) << "pagesVector successfully inizialized!"; // filling the pages with the source references rects const QVector& sourceAnchors = m_dviRenderer->sourceAnchors(); QVector< QLinkedList< Okular::SourceRefObjectRect * > > refRects( numofpages ); foreach ( const DVI_SourceFileAnchor& sfa, sourceAnchors ) { if ( sfa.page < 1 || (int)sfa.page > numofpages ) continue; Okular::NormalizedPoint p( -1.0, (double)sfa.distance_from_top.getLength_in_pixel( dpi().height() ) / (double)pageRequiredSize.height() ); Okular::SourceReference * sourceRef = new Okular::SourceReference( sfa.fileName, sfa.line ); refRects[ sfa.page - 1 ].append( new Okular::SourceRefObjectRect( p, sourceRef ) ); } for ( int i = 0; i < refRects.size(); ++i ) if ( !refRects.at(i).isEmpty() ) pagesVector[i]->setSourceReferences( refRects.at(i) ); } bool DviGenerator::print( QPrinter& printer ) { // Create tempfile to write to QTemporaryFile tf(QDir::tempPath() + QLatin1String("/okular_XXXXXX.ps" )); if ( !tf.open() ) return false; QList pageList = Okular::FilePrinter::pageList( printer, m_dviRenderer->totalPages(), document()->currentPage() + 1, document()->bookmarkedPageList() ); QString pages; QStringList printOptions; // List of pages to print. foreach ( int p, pageList ) { pages += QStringLiteral(",%1").arg(p); } if ( !pages.isEmpty() ) printOptions << QStringLiteral("-pp") << pages.mid(1); QEventLoop el; m_dviRenderer->setEventLoop( &el ); m_dviRenderer->exportPS( tf.fileName(), printOptions, &printer, document()->orientation() ); tf.close(); // Error messages are handled by the generator - ugly, but it works. return true; } QVariant DviGenerator::metaData( const QString & key, const QVariant & option ) const { if ( key == QLatin1String("NamedViewport") && !option.toString().isEmpty() ) { const Anchor anchor = m_dviRenderer->parseReference( option.toString() ); if ( anchor.isValid() ) { const Okular::Page *page = document()->page( anchor.page - 1 ); Q_ASSERT_X( page, "DviGenerator::metaData()", "NULL page as result of valid Anchor" ); Okular::DocumentViewport viewport; fillViewportFromAnchor( viewport, anchor, page ); if ( viewport.isValid() ) { return viewport.toString(); } } } return QVariant(); } Q_LOGGING_CATEGORY(OkularDviDebug, "org.kde.okular.generators.dvi.core", QtWarningMsg) Q_LOGGING_CATEGORY(OkularDviShellDebug, "org.kde.okular.generators.dvi.shell", QtWarningMsg) #include "generator_dvi.moc" diff --git a/generators/dvi/generator_dvi.h b/generators/dvi/generator_dvi.h index fa4ebdfb4..3f7b4f8e7 100644 --- a/generators/dvi/generator_dvi.h +++ b/generators/dvi/generator_dvi.h @@ -1,70 +1,70 @@ /*************************************************************************** * Copyright (C) 2006 by Luigi Toscano * * * * 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. * ***************************************************************************/ #ifndef _DVI_GENERATOR_H_ #define _DVI_GENERATOR_H_ #include #include class dviRenderer; class dviPageInfo; class Anchor; namespace Okular { class DocumentViewport; class ObjectRect; } class DviGenerator : public Okular::Generator { Q_OBJECT Q_INTERFACES( Okular::Generator ) public: DviGenerator( QObject *parent, const QVariantList &args ); bool loadDocument( const QString & fileName, QVector< Okular::Page * > & pagesVector ) override; // document information Okular::DocumentInfo generateDocumentInfo( const QSet &keys ) const override; // table of contents const Okular::DocumentSynopsis *generateDocumentSynopsis() override; // list of fonts Okular::FontInfo::List fontsForPage( int page ) override; bool print( QPrinter &printer ) override; QVariant metaData( const QString & key, const QVariant & option ) const override; protected: bool doCloseDocument() override; QImage image( Okular::PixmapRequest * request ) override; - Okular::TextPage* textPage( Okular::Page *page ) override; + Okular::TextPage* textPage( Okular::TextRequest *request ) override; private: double m_resolution; bool m_fontExtracted; Okular::DocumentSynopsis *m_docSynopsis; dviRenderer *m_dviRenderer; QBitArray m_linkGenerated; void loadPages( QVector< Okular::Page * > & pagesVector ); Okular::TextPage *extractTextFromPage( dviPageInfo *pageInfo ); void fillViewportFromAnchor( Okular::DocumentViewport &vp, const Anchor &anch, int pW, int pH ) const; void fillViewportFromAnchor( Okular::DocumentViewport &vp, const Anchor &anch, const Okular::Page *page ) const; QLinkedList generateDviLinks( const dviPageInfo *pageInfo ); }; #endif diff --git a/generators/poppler/CMakeLists.txt b/generators/poppler/CMakeLists.txt index a5ad02743..a3dd38dc9 100644 --- a/generators/poppler/CMakeLists.txt +++ b/generators/poppler/CMakeLists.txt @@ -1,94 +1,105 @@ remove_definitions(-DTRANSLATION_DOMAIN="okular") add_definitions(-DTRANSLATION_DOMAIN="okular_poppler") add_subdirectory( conf ) if (Poppler_VERSION VERSION_GREATER "0.23.99") set (HAVE_POPPLER_0_24 1) endif() if (Poppler_VERSION VERSION_GREATER "0.27.99") set (HAVE_POPPLER_0_28 1) endif() if (Poppler_VERSION VERSION_GREATER "0.35.99") set (HAVE_POPPLER_0_36 1) endif() if (Poppler_VERSION VERSION_GREATER "0.36.99") set (HAVE_POPPLER_0_37 1) endif() set(CMAKE_REQUIRED_LIBRARIES Poppler::Qt5 Qt5::Core Qt5::Gui) check_cxx_source_compiles(" #include int main() { Poppler::LinkOCGState *l = 0; return 0; } " HAVE_POPPLER_0_50) check_cxx_source_compiles(" #include #include int main() { Poppler::FormFieldButton *ff = 0; Poppler::Link *l = ff->additionalAction(Poppler::FormField::CalculateField); return 0; } " HAVE_POPPLER_0_53) check_cxx_source_compiles(" #include int main() { Poppler::Document::RenderHint hint = Poppler::Document::HideAnnotations; return 0; } " HAVE_POPPLER_0_60) check_cxx_source_compiles(" #include #include int main() { Poppler::Page *p; p->renderToImage(0, 0, 0, 0, 0, 0, Poppler::Page::Rotate0, nullptr, nullptr, QVariant()); return 0; } " HAVE_POPPLER_0_62) +check_cxx_source_compiles(" +#include +#include +int main() +{ + Poppler::Page *p; + p->renderToImage(0, 0, 0, 0, 0, 0, Poppler::Page::Rotate0, nullptr, nullptr, nullptr, QVariant()); + return 0; +} +" HAVE_POPPLER_0_63) + configure_file( ${CMAKE_CURRENT_SOURCE_DIR}/config-okular-poppler.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config-okular-poppler.h ) include_directories( ${CMAKE_CURRENT_SOURCE_DIR}/../.. ) ########### next target ############### set(okularGenerator_poppler_PART_SRCS generator_pdf.cpp formfields.cpp annots.cpp ) ki18n_wrap_ui(okularGenerator_poppler_PART_SRCS conf/pdfsettingswidget.ui ) kconfig_add_kcfg_files(okularGenerator_poppler_PART_SRCS conf/pdfsettings.kcfgc ) okular_add_generator(okularGenerator_poppler ${okularGenerator_poppler_PART_SRCS}) target_link_libraries(okularGenerator_poppler okularcore KF5::I18n KF5::Completion Poppler::Qt5 Qt5::Xml) ########### install files ############### install( FILES okularPoppler.desktop DESTINATION ${KDE_INSTALL_KSERVICES5DIR} ) install( PROGRAMS okularApplication_pdf.desktop org.kde.mobile.okular_pdf.desktop DESTINATION ${KDE_INSTALL_APPDIR} ) install( FILES org.kde.okular-poppler.metainfo.xml DESTINATION ${KDE_INSTALL_METAINFODIR} ) diff --git a/generators/poppler/config-okular-poppler.h.cmake b/generators/poppler/config-okular-poppler.h.cmake index 9b8da5051..16831f749 100644 --- a/generators/poppler/config-okular-poppler.h.cmake +++ b/generators/poppler/config-okular-poppler.h.cmake @@ -1,23 +1,26 @@ /* Defined if we have the 0.24 version of the Poppler library */ #cmakedefine HAVE_POPPLER_0_24 1 /* Defined if we have the 0.28 version of the Poppler library */ #cmakedefine HAVE_POPPLER_0_28 1 /* Defined if we have the 0.36 version of the Poppler library */ #cmakedefine HAVE_POPPLER_0_36 1 /* Defined if we have the 0.37 version of the Poppler library */ #cmakedefine HAVE_POPPLER_0_37 1 /* Defined if we have the 0.50 version of the Poppler library */ #cmakedefine HAVE_POPPLER_0_50 1 /* Defined if we have the 0.53 version of the Poppler library */ #cmakedefine HAVE_POPPLER_0_53 1 /* Defined if we have the 0.60 version of the Poppler library */ #cmakedefine HAVE_POPPLER_0_60 1 /* Defined if we have the 0.62 version of the Poppler library */ #cmakedefine HAVE_POPPLER_0_62 1 + +/* Defined if we have the 0.63 version of the Poppler library */ +#cmakedefine HAVE_POPPLER_0_63 1 diff --git a/generators/poppler/generator_pdf.cpp b/generators/poppler/generator_pdf.cpp index 61a393f22..5421c7427 100644 --- a/generators/poppler/generator_pdf.cpp +++ b/generators/poppler/generator_pdf.cpp @@ -1,1828 +1,1915 @@ /*************************************************************************** * Copyright (C) 2004-2008 by Albert Astals Cid * * Copyright (C) 2004 by Enrico Ros * * Copyright (C) 2012 by Guillermo A. Amaral B. * * Copyright (C) 2017 Klarälvdalens Datakonsult AB, a KDAB Group * * company, info@kdab.com. Work sponsored by the * * LiMux project of the city of Munich * * * * 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. * ***************************************************************************/ #include "generator_pdf.h" // qt/kde includes #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "ui_pdfsettingswidget.h" #include "pdfsettings.h" #include #include #include "debug_pdf.h" #include "annots.h" #include "formfields.h" #include "popplerembeddedfile.h" Q_DECLARE_METATYPE(Poppler::Annotation*) Q_DECLARE_METATYPE(Poppler::FontInfo) Q_DECLARE_METATYPE(const Poppler::LinkMovie*) Q_DECLARE_METATYPE(const Poppler::LinkRendition*) #ifdef HAVE_POPPLER_0_50 Q_DECLARE_METATYPE(const Poppler::LinkOCGState*) #endif static const int defaultPageWidth = 595; static const int defaultPageHeight = 842; class PDFOptionsPage : public QWidget { Q_OBJECT public: PDFOptionsPage() { setWindowTitle( i18n( "PDF Options" ) ); QVBoxLayout *layout = new QVBoxLayout(this); m_printAnnots = new QCheckBox(i18n("Print annotations"), this); m_printAnnots->setToolTip(i18n("Include annotations in the printed document")); m_printAnnots->setWhatsThis(i18n("Includes annotations in the printed document. You can disable this if you want to print the original unannotated document.")); layout->addWidget(m_printAnnots); m_forceRaster = new QCheckBox(i18n("Force rasterization"), this); m_forceRaster->setToolTip(i18n("Rasterize into an image before printing")); m_forceRaster->setWhatsThis(i18n("Forces the rasterization of each page into an image before printing it. This usually gives somewhat worse results, but is useful when printing documents that appear to print incorrectly.")); layout->addWidget(m_forceRaster); layout->addStretch(1); #if defined(Q_OS_WIN) && !defined HAVE_POPPLER_0_60 m_printAnnots->setVisible( false ); #endif setPrintAnnots( true ); // Default value } bool printAnnots() { return m_printAnnots->isChecked(); } void setPrintAnnots( bool printAnnots ) { m_printAnnots->setChecked( printAnnots ); } bool printForceRaster() { return m_forceRaster->isChecked(); } void setPrintForceRaster( bool forceRaster ) { m_forceRaster->setChecked( forceRaster ); } private: QCheckBox *m_printAnnots; QCheckBox *m_forceRaster; }; static void fillViewportFromLinkDestination( Okular::DocumentViewport &viewport, const Poppler::LinkDestination &destination ) { viewport.pageNumber = destination.pageNumber() - 1; if (!viewport.isValid()) return; // get destination position // TODO add other attributes to the viewport (taken from link) // switch ( destination->getKind() ) // { // case destXYZ: if (destination.isChangeLeft() || destination.isChangeTop()) { // TODO remember to change this if we implement DPI and/or rotation double left, top; left = destination.left(); top = destination.top(); viewport.rePos.normalizedX = left; viewport.rePos.normalizedY = top; viewport.rePos.enabled = true; viewport.rePos.pos = Okular::DocumentViewport::TopLeft; } /* TODO if ( dest->getChangeZoom() ) make zoom change*/ /* break; default: // implement the others cases break;*/ // } } Okular::Sound* createSoundFromPopplerSound( const Poppler::SoundObject *popplerSound ) { Okular::Sound *sound = popplerSound->soundType() == Poppler::SoundObject::Embedded ? new Okular::Sound( popplerSound->data() ) : new Okular::Sound( popplerSound->url() ); sound->setSamplingRate( popplerSound->samplingRate() ); sound->setChannels( popplerSound->channels() ); sound->setBitsPerSample( popplerSound->bitsPerSample() ); switch ( popplerSound->soundEncoding() ) { case Poppler::SoundObject::Raw: sound->setSoundEncoding( Okular::Sound::Raw ); break; case Poppler::SoundObject::Signed: sound->setSoundEncoding( Okular::Sound::Signed ); break; case Poppler::SoundObject::muLaw: sound->setSoundEncoding( Okular::Sound::muLaw ); break; case Poppler::SoundObject::ALaw: sound->setSoundEncoding( Okular::Sound::ALaw ); break; } return sound; } Okular::Movie* createMovieFromPopplerMovie( const Poppler::MovieObject *popplerMovie ) { Okular::Movie *movie = new Okular::Movie( popplerMovie->url() ); movie->setSize( popplerMovie->size() ); movie->setRotation( (Okular::Rotation)( popplerMovie->rotation() / 90 ) ); movie->setShowControls( popplerMovie->showControls() ); movie->setPlayMode( (Okular::Movie::PlayMode)popplerMovie->playMode() ); movie->setAutoPlay( false ); // will be triggered by external MovieAnnotation movie->setShowPosterImage( popplerMovie->showPosterImage() ); movie->setPosterImage( popplerMovie->posterImage() ); return movie; } Okular::Movie* createMovieFromPopplerScreen( const Poppler::LinkRendition *popplerScreen ) { Poppler::MediaRendition *rendition = popplerScreen->rendition(); Okular::Movie *movie = 0; if ( rendition->isEmbedded() ) movie = new Okular::Movie( rendition->fileName(), rendition->data() ); else movie = new Okular::Movie( rendition->fileName() ); movie->setSize( rendition->size() ); movie->setShowControls( rendition->showControls() ); if ( rendition->repeatCount() == 0 ) { movie->setPlayMode( Okular::Movie::PlayRepeat ); } else { movie->setPlayMode( Okular::Movie::PlayLimited ); movie->setPlayRepetitions( rendition->repeatCount() ); } movie->setAutoPlay( rendition->autoPlay() ); return movie; } #ifdef HAVE_POPPLER_0_36 QPair createMovieFromPopplerRichMedia( const Poppler::RichMediaAnnotation *popplerRichMedia ) { const QPair emptyResult(0, 0); /** * To convert a Flash/Video based RichMedia annotation to a movie, we search for the first * Flash/Video richmedia instance and parse the flashVars parameter for the 'source' identifier. * That identifier is then used to find the associated embedded file through the assets * mapping. */ const Poppler::RichMediaAnnotation::Content *content = popplerRichMedia->content(); if ( !content ) return emptyResult; const QList configurations = content->configurations(); if ( configurations.isEmpty() ) return emptyResult; const Poppler::RichMediaAnnotation::Configuration *configuration = configurations[0]; const QList instances = configuration->instances(); if ( instances.isEmpty() ) return emptyResult; const Poppler::RichMediaAnnotation::Instance *instance = instances[0]; if ( ( instance->type() != Poppler::RichMediaAnnotation::Instance::TypeFlash ) && ( instance->type() != Poppler::RichMediaAnnotation::Instance::TypeVideo ) ) return emptyResult; const Poppler::RichMediaAnnotation::Params *params = instance->params(); if ( !params ) return emptyResult; QString sourceId; bool playbackLoops = false; const QStringList flashVars = params->flashVars().split( QLatin1Char( '&' ) ); foreach ( const QString & flashVar, flashVars ) { const int pos = flashVar.indexOf( QLatin1Char( '=' ) ); if ( pos == -1 ) continue; const QString key = flashVar.left( pos ); const QString value = flashVar.mid( pos + 1 ); if ( key == QLatin1String( "source" ) ) sourceId = value; else if ( key == QLatin1String( "loop" ) ) playbackLoops = ( value == QLatin1String( "true" ) ? true : false ); } if ( sourceId.isEmpty() ) return emptyResult; const QList assets = content->assets(); if ( assets.isEmpty() ) return emptyResult; Poppler::RichMediaAnnotation::Asset *matchingAsset = 0; foreach ( Poppler::RichMediaAnnotation::Asset *asset, assets ) { if ( asset->name() == sourceId ) { matchingAsset = asset; break; } } if ( !matchingAsset ) return emptyResult; Poppler::EmbeddedFile *embeddedFile = matchingAsset->embeddedFile(); if ( !embeddedFile ) return emptyResult; Okular::EmbeddedFile *pdfEmbeddedFile = new PDFEmbeddedFile( embeddedFile ); Okular::Movie *movie = new Okular::Movie( embeddedFile->name(), embeddedFile->data() ); movie->setPlayMode( playbackLoops ? Okular::Movie::PlayRepeat : Okular::Movie::PlayLimited ); if ( popplerRichMedia && popplerRichMedia->settings() && popplerRichMedia->settings()->activation() ) { if ( popplerRichMedia->settings()->activation()->condition() == Poppler::RichMediaAnnotation::Activation::PageOpened || popplerRichMedia->settings()->activation()->condition() == Poppler::RichMediaAnnotation::Activation::PageVisible ) { movie->setAutoPlay( true ); } else { movie->setAutoPlay( false ); } } else { movie->setAutoPlay( false ); } return qMakePair(movie, pdfEmbeddedFile); } #endif /** * Note: the function will take ownership of the popplerLink object. */ Okular::Action* createLinkFromPopplerLink(const Poppler::Link *popplerLink) { if (!popplerLink) return nullptr; Okular::Action *link = 0; const Poppler::LinkGoto *popplerLinkGoto; const Poppler::LinkExecute *popplerLinkExecute; const Poppler::LinkBrowse *popplerLinkBrowse; const Poppler::LinkAction *popplerLinkAction; const Poppler::LinkSound *popplerLinkSound; const Poppler::LinkJavaScript *popplerLinkJS; const Poppler::LinkMovie *popplerLinkMovie; const Poppler::LinkRendition *popplerLinkRendition; Okular::DocumentViewport viewport; bool deletePopplerLink = true; switch(popplerLink->linkType()) { case Poppler::Link::None: break; case Poppler::Link::Goto: { popplerLinkGoto = static_cast(popplerLink); const Poppler::LinkDestination dest = popplerLinkGoto->destination(); const QString destName = dest.destinationName(); if (destName.isEmpty()) { fillViewportFromLinkDestination( viewport, dest ); link = new Okular::GotoAction(popplerLinkGoto->fileName(), viewport); } else { link = new Okular::GotoAction(popplerLinkGoto->fileName(), destName); } } break; case Poppler::Link::Execute: popplerLinkExecute = static_cast(popplerLink); link = new Okular::ExecuteAction( popplerLinkExecute->fileName(), popplerLinkExecute->parameters() ); break; case Poppler::Link::Browse: popplerLinkBrowse = static_cast(popplerLink); link = new Okular::BrowseAction( QUrl(popplerLinkBrowse->url()) ); break; case Poppler::Link::Action: popplerLinkAction = static_cast(popplerLink); link = new Okular::DocumentAction( (Okular::DocumentAction::DocumentActionType)popplerLinkAction->actionType() ); break; case Poppler::Link::Sound: { popplerLinkSound = static_cast(popplerLink); Poppler::SoundObject *popplerSound = popplerLinkSound->sound(); Okular::Sound *sound = createSoundFromPopplerSound( popplerSound ); link = new Okular::SoundAction( popplerLinkSound->volume(), popplerLinkSound->synchronous(), popplerLinkSound->repeat(), popplerLinkSound->mix(), sound ); } break; case Poppler::Link::JavaScript: { popplerLinkJS = static_cast(popplerLink); link = new Okular::ScriptAction( Okular::JavaScript, popplerLinkJS->script() ); } break; case Poppler::Link::Rendition: { deletePopplerLink = false; // we'll delete it inside resolveMediaLinkReferences() after we have resolved all references popplerLinkRendition = static_cast( popplerLink ); Okular::RenditionAction::OperationType operation = Okular::RenditionAction::None; switch ( popplerLinkRendition->action() ) { case Poppler::LinkRendition::NoRendition: operation = Okular::RenditionAction::None; break; case Poppler::LinkRendition::PlayRendition: operation = Okular::RenditionAction::Play; break; case Poppler::LinkRendition::StopRendition: operation = Okular::RenditionAction::Stop; break; case Poppler::LinkRendition::PauseRendition: operation = Okular::RenditionAction::Pause; break; case Poppler::LinkRendition::ResumeRendition: operation = Okular::RenditionAction::Resume; break; }; Okular::Movie *movie = 0; if ( popplerLinkRendition->rendition() ) movie = createMovieFromPopplerScreen( popplerLinkRendition ); Okular::RenditionAction *renditionAction = new Okular::RenditionAction( operation, movie, Okular::JavaScript, popplerLinkRendition->script() ); renditionAction->setNativeId( QVariant::fromValue( popplerLinkRendition ) ); link = renditionAction; } break; case Poppler::Link::Movie: { deletePopplerLink = false; // we'll delete it inside resolveMediaLinkReferences() after we have resolved all references popplerLinkMovie = static_cast( popplerLink ); Okular::MovieAction::OperationType operation = Okular::MovieAction::Play; switch ( popplerLinkMovie->operation() ) { case Poppler::LinkMovie::Play: operation = Okular::MovieAction::Play; break; case Poppler::LinkMovie::Stop: operation = Okular::MovieAction::Stop; break; case Poppler::LinkMovie::Pause: operation = Okular::MovieAction::Pause; break; case Poppler::LinkMovie::Resume: operation = Okular::MovieAction::Resume; break; }; Okular::MovieAction *movieAction = new Okular::MovieAction( operation ); movieAction->setNativeId( QVariant::fromValue( popplerLinkMovie ) ); link = movieAction; } break; } if ( deletePopplerLink ) delete popplerLink; return link; } /** * Note: the function will take ownership of the popplerLink objects. */ static QLinkedList generateLinks( const QList &popplerLinks ) { QLinkedList links; foreach(const Poppler::Link *popplerLink, popplerLinks) { QRectF linkArea = popplerLink->linkArea(); double nl = linkArea.left(), nt = linkArea.top(), nr = linkArea.right(), nb = linkArea.bottom(); // create the rect using normalized coords and attach the Okular::Link to it Okular::ObjectRect * rect = new Okular::ObjectRect( nl, nt, nr, nb, false, Okular::ObjectRect::Action, createLinkFromPopplerLink(popplerLink) ); // add the ObjectRect to the container links.push_front( rect ); } return links; } /** NOTES on threading: * internal: thread race prevention is done via the 'docLock' mutex. the * mutex is needed only because we have the asynchronous thread; else * the operations are all within the 'gui' thread, scheduled by the * Qt scheduler and no mutex is needed. * external: dangerous operations are all locked via mutex internally, and the * only needed external thing is the 'canGeneratePixmap' method * that tells if the generator is free (since we don't want an * internal queue to store PixmapRequests). A generatedPixmap call * without the 'ready' flag set, results in undefined behavior. * So, as example, printing while generating a pixmap asynchronously is safe, * it might only block the gui thread by 1) waiting for the mutex to unlock * in async thread and 2) doing the 'heavy' print operation. */ OKULAR_EXPORT_PLUGIN(PDFGenerator, "libokularGenerator_poppler.json") static void PDFGeneratorPopplerDebugFunction(const QString &message, const QVariant &closure) { Q_UNUSED(closure); qCDebug(OkularPdfDebug) << "[Poppler]" << message; } PDFGenerator::PDFGenerator( QObject *parent, const QVariantList &args ) : Generator( parent, args ), pdfdoc( 0 ), docSynopsisDirty( true ), docEmbeddedFilesDirty( true ), nextFontPage( 0 ), annotProxy( 0 ) { setFeature( Threaded ); setFeature( TextExtraction ); setFeature( FontInfo ); #ifdef Q_OS_WIN32 setFeature( PrintNative ); #else setFeature( PrintPostscript ); #endif if ( Okular::FilePrinter::ps2pdfAvailable() ) setFeature( PrintToFile ); setFeature( ReadRawData ); setFeature( TiledRendering ); setFeature( SwapBackingFile ); +#ifdef HAVE_POPPLER_0_63 + setFeature( SupportsCancelling ); +#endif // You only need to do it once not for each of the documents but it is cheap enough // so doing it all the time won't hurt either Poppler::setDebugErrorFunction(PDFGeneratorPopplerDebugFunction, QVariant()); } PDFGenerator::~PDFGenerator() { delete pdfOptionsPage; } //BEGIN Generator inherited functions Okular::Document::OpenResult PDFGenerator::loadDocumentWithPassword( const QString & filePath, QVector & pagesVector, const QString &password ) { #ifndef NDEBUG if ( pdfdoc ) { qCDebug(OkularPdfDebug) << "PDFGenerator: multiple calls to loadDocument. Check it."; return Okular::Document::OpenError; } #endif // create PDFDoc for the given file pdfdoc = Poppler::Document::load( filePath, 0, 0 ); return init(pagesVector, password); } Okular::Document::OpenResult PDFGenerator::loadDocumentFromDataWithPassword( const QByteArray & fileData, QVector & pagesVector, const QString &password ) { #ifndef NDEBUG if ( pdfdoc ) { qCDebug(OkularPdfDebug) << "PDFGenerator: multiple calls to loadDocument. Check it."; return Okular::Document::OpenError; } #endif // create PDFDoc for the given file pdfdoc = Poppler::Document::loadFromData( fileData, 0, 0 ); return init(pagesVector, password); } Okular::Document::OpenResult PDFGenerator::init(QVector & pagesVector, const QString &password) { if ( !pdfdoc ) return Okular::Document::OpenError; if ( pdfdoc->isLocked() ) { pdfdoc->unlock( password.toLatin1(), password.toLatin1() ); if ( pdfdoc->isLocked() ) { delete pdfdoc; pdfdoc = 0; return Okular::Document::OpenNeedsPassword; } } // build Pages (currentPage was set -1 by deletePages) int pageCount = pdfdoc->numPages(); if (pageCount < 0) { delete pdfdoc; pdfdoc = 0; return Okular::Document::OpenError; } pagesVector.resize(pageCount); rectsGenerated.fill(false, pageCount); annotationsOnOpenHash.clear(); loadPages(pagesVector, 0, false); // update the configuration reparseConfig(); // create annotation proxy annotProxy = new PopplerAnnotationProxy( pdfdoc, userMutex(), &annotationsOnOpenHash ); // the file has been loaded correctly return Okular::Document::OpenSuccess; } PDFGenerator::SwapBackingFileResult PDFGenerator::swapBackingFile( QString const &newFileName, QVector & newPagesVector ) { doCloseDocument(); auto openResult = loadDocumentWithPassword(newFileName, newPagesVector, QString()); if (openResult != Okular::Document::OpenSuccess) return SwapBackingFileError; return SwapBackingFileReloadInternalData; } bool PDFGenerator::doCloseDocument() { // remove internal objects userMutex()->lock(); delete annotProxy; annotProxy = 0; delete pdfdoc; pdfdoc = 0; userMutex()->unlock(); docSynopsisDirty = true; docSyn.clear(); docEmbeddedFilesDirty = true; qDeleteAll(docEmbeddedFiles); docEmbeddedFiles.clear(); nextFontPage = 0; rectsGenerated.clear(); return true; } void PDFGenerator::loadPages(QVector &pagesVector, int rotation, bool clear) { // TODO XPDF 3.01 check const int count = pagesVector.count(); double w = 0, h = 0; for ( int i = 0; i < count ; i++ ) { // get xpdf page Poppler::Page * p = pdfdoc->page( i ); Okular::Page * page; if (p) { const QSizeF pSize = p->pageSizeF(); w = pSize.width() / 72.0 * dpi().width(); h = pSize.height() / 72.0 * dpi().height(); Okular::Rotation orientation = Okular::Rotation0; switch (p->orientation()) { case Poppler::Page::Landscape: orientation = Okular::Rotation90; break; case Poppler::Page::UpsideDown: orientation = Okular::Rotation180; break; case Poppler::Page::Seascape: orientation = Okular::Rotation270; break; case Poppler::Page::Portrait: orientation = Okular::Rotation0; break; } if (rotation % 2 == 1) qSwap(w,h); // init a Okular::page, add transition and annotation information page = new Okular::Page( i, w, h, orientation ); addTransition( p, page ); if ( true ) //TODO real check addAnnotations( p, page ); Poppler::Link * tmplink = p->action( Poppler::Page::Opening ); if ( tmplink ) { page->setPageAction( Okular::Page::Opening, createLinkFromPopplerLink( tmplink ) ); } tmplink = p->action( Poppler::Page::Closing ); if ( tmplink ) { page->setPageAction( Okular::Page::Closing, createLinkFromPopplerLink( tmplink ) ); } page->setDuration( p->duration() ); page->setLabel( p->label() ); addFormFields( p, page ); // kWarning(PDFDebug).nospace() << page->width() << "x" << page->height(); #ifdef PDFGENERATOR_DEBUG qCDebug(OkularPdfDebug) << "load page" << i << "with rotation" << rotation << "and orientation" << orientation; #endif delete p; if (clear && pagesVector[i]) delete pagesVector[i]; } else { page = new Okular::Page( i, defaultPageWidth, defaultPageHeight, Okular::Rotation0 ); } // set the Okular::page at the right position in document's pages vector pagesVector[i] = page; } } Okular::DocumentInfo PDFGenerator::generateDocumentInfo( const QSet &keys ) const { Okular::DocumentInfo docInfo; docInfo.set( Okular::DocumentInfo::MimeType, QStringLiteral("application/pdf") ); userMutex()->lock(); if ( pdfdoc ) { // compile internal structure reading properties from PDFDoc if ( keys.contains( Okular::DocumentInfo::Title ) ) docInfo.set( Okular::DocumentInfo::Title, pdfdoc->info(QStringLiteral("Title")) ); if ( keys.contains( Okular::DocumentInfo::Subject ) ) docInfo.set( Okular::DocumentInfo::Subject, pdfdoc->info(QStringLiteral("Subject")) ); if ( keys.contains( Okular::DocumentInfo::Author ) ) docInfo.set( Okular::DocumentInfo::Author, pdfdoc->info(QStringLiteral("Author")) ); if ( keys.contains( Okular::DocumentInfo::Keywords ) ) docInfo.set( Okular::DocumentInfo::Keywords, pdfdoc->info(QStringLiteral("Keywords")) ); if ( keys.contains( Okular::DocumentInfo::Creator ) ) docInfo.set( Okular::DocumentInfo::Creator, pdfdoc->info(QStringLiteral("Creator")) ); if ( keys.contains( Okular::DocumentInfo::Producer ) ) docInfo.set( Okular::DocumentInfo::Producer, pdfdoc->info(QStringLiteral("Producer")) ); if ( keys.contains( Okular::DocumentInfo::CreationDate ) ) docInfo.set( Okular::DocumentInfo::CreationDate, QLocale().toString( pdfdoc->date(QStringLiteral("CreationDate")), QLocale::LongFormat ) ); if ( keys.contains( Okular::DocumentInfo::ModificationDate ) ) docInfo.set( Okular::DocumentInfo::ModificationDate, QLocale().toString( pdfdoc->date(QStringLiteral("ModDate")), QLocale::LongFormat ) ); if ( keys.contains( Okular::DocumentInfo::CustomKeys ) ) { int major, minor; pdfdoc->getPdfVersion(&major, &minor); docInfo.set( QStringLiteral("format"), i18nc( "PDF v. ", "PDF v. %1.%2", major, minor ), i18n( "Format" ) ); docInfo.set( QStringLiteral("encryption"), pdfdoc->isEncrypted() ? i18n( "Encrypted" ) : i18n( "Unencrypted" ), i18n("Security") ); docInfo.set( QStringLiteral("optimization"), pdfdoc->isLinearized() ? i18n( "Yes" ) : i18n( "No" ), i18n("Optimized") ); } docInfo.set( Okular::DocumentInfo::Pages, QString::number( pdfdoc->numPages() ) ); } userMutex()->unlock(); return docInfo; } const Okular::DocumentSynopsis * PDFGenerator::generateDocumentSynopsis() { if ( !docSynopsisDirty ) return &docSyn; if ( !pdfdoc ) return NULL; userMutex()->lock(); QDomDocument *toc = pdfdoc->toc(); userMutex()->unlock(); if ( !toc ) return NULL; addSynopsisChildren(toc, &docSyn); delete toc; docSynopsisDirty = false; return &docSyn; } static Okular::FontInfo::FontType convertPopplerFontInfoTypeToOkularFontInfoType( Poppler::FontInfo::Type type ) { switch ( type ) { case Poppler::FontInfo::Type1: return Okular::FontInfo::Type1; break; case Poppler::FontInfo::Type1C: return Okular::FontInfo::Type1C; break; case Poppler::FontInfo::Type3: return Okular::FontInfo::Type3; break; case Poppler::FontInfo::TrueType: return Okular::FontInfo::TrueType; break; case Poppler::FontInfo::CIDType0: return Okular::FontInfo::CIDType0; break; case Poppler::FontInfo::CIDType0C: return Okular::FontInfo::CIDType0C; break; case Poppler::FontInfo::CIDTrueType: return Okular::FontInfo::CIDTrueType; break; case Poppler::FontInfo::Type1COT: return Okular::FontInfo::Type1COT; break; case Poppler::FontInfo::TrueTypeOT: return Okular::FontInfo::TrueTypeOT; break; case Poppler::FontInfo::CIDType0COT: return Okular::FontInfo::CIDType0COT; break; case Poppler::FontInfo::CIDTrueTypeOT: return Okular::FontInfo::CIDTrueTypeOT; break; case Poppler::FontInfo::unknown: default: ; } return Okular::FontInfo::Unknown; } static Okular::FontInfo::EmbedType embedTypeForPopplerFontInfo( const Poppler::FontInfo &fi ) { Okular::FontInfo::EmbedType ret = Okular::FontInfo::NotEmbedded; if ( fi.isEmbedded() ) { if ( fi.isSubset() ) { ret = Okular::FontInfo::EmbeddedSubset; } else { ret = Okular::FontInfo::FullyEmbedded; } } return ret; } Okular::FontInfo::List PDFGenerator::fontsForPage( int page ) { Okular::FontInfo::List list; if ( page != nextFontPage ) return list; QList fonts; userMutex()->lock(); Poppler::FontIterator* it = pdfdoc->newFontIterator(page); if (it->hasNext()) { fonts = it->next(); } userMutex()->unlock(); foreach (const Poppler::FontInfo &font, fonts) { Okular::FontInfo of; of.setName( font.name() ); of.setType( convertPopplerFontInfoTypeToOkularFontInfoType( font.type() ) ); of.setEmbedType( embedTypeForPopplerFontInfo( font) ); of.setFile( font.file() ); of.setCanBeExtracted( of.embedType() != Okular::FontInfo::NotEmbedded ); QVariant nativeId; nativeId.setValue( font ); of.setNativeId( nativeId ); list.append( of ); } ++nextFontPage; return list; } const QList *PDFGenerator::embeddedFiles() const { if (docEmbeddedFilesDirty) { userMutex()->lock(); const QList &popplerFiles = pdfdoc->embeddedFiles(); foreach(Poppler::EmbeddedFile* pef, popplerFiles) { docEmbeddedFiles.append(new PDFEmbeddedFile(pef)); } userMutex()->unlock(); docEmbeddedFilesDirty = false; } return &docEmbeddedFiles; } QAbstractItemModel* PDFGenerator::layersModel() const { return pdfdoc->hasOptionalContent() ? pdfdoc->optionalContentModel() : NULL; } void PDFGenerator::opaqueAction( const Okular::BackendOpaqueAction *action ) { #ifdef HAVE_POPPLER_0_50 const Poppler::LinkOCGState *popplerLink = action->nativeId().value(); pdfdoc->optionalContentModel()->applyLink( const_cast< Poppler::LinkOCGState* >( popplerLink ) ); #else (void)action; #endif } bool PDFGenerator::isAllowed( Okular::Permission permission ) const { bool b = true; switch ( permission ) { case Okular::AllowModify: b = pdfdoc->okToChange(); break; case Okular::AllowCopy: b = pdfdoc->okToCopy(); break; case Okular::AllowPrint: b = pdfdoc->okToPrint(); break; case Okular::AllowNotes: b = pdfdoc->okToAddNotes(); break; case Okular::AllowFillForms: b = pdfdoc->okToFillForm(); break; default: ; } return b; } #ifdef HAVE_POPPLER_0_62 -struct PartialUpdatePayload +struct RenderImagePayload { - PartialUpdatePayload(PDFGenerator *g, Okular::PixmapRequest *r) : + RenderImagePayload(PDFGenerator *g, Okular::PixmapRequest *r) : generator(g), request(r) { // Don't report partial updates for the first 500 ms timer.setInterval(500); timer.setSingleShot(true); timer.start(); } PDFGenerator *generator; Okular::PixmapRequest *request; QTimer timer; }; -Q_DECLARE_METATYPE(PartialUpdatePayload*) +Q_DECLARE_METATYPE(RenderImagePayload*) static bool shouldDoPartialUpdateCallback(const QVariant &vPayload) { - auto payload = vPayload.value(); + auto payload = vPayload.value(); // Since the timer lives in a thread without an event loop we need to stop it ourselves // when the remaining time has reached 0 if (payload->timer.isActive() && payload->timer.remainingTime() == 0) { payload->timer.stop(); } return !payload->timer.isActive(); } static void partialUpdateCallback(const QImage &image, const QVariant &vPayload) { - auto payload = vPayload.value(); + auto payload = vPayload.value(); QMetaObject::invokeMethod(payload->generator, "signalPartialPixmapRequest", Qt::QueuedConnection, Q_ARG(Okular::PixmapRequest*, payload->request), Q_ARG(QImage, image)); } #endif +#ifdef HAVE_POPPLER_0_63 +static bool shouldAbortRenderCallback(const QVariant &vPayload) +{ + auto payload = vPayload.value(); + return payload->request->shouldAbortRender(); +} +#endif + QImage PDFGenerator::image( Okular::PixmapRequest * request ) { // debug requests to this (xpdf) generator //qCDebug(OkularPdfDebug) << "id: " << request->id << " is requesting " << (request->async ? "ASYNC" : "sync") << " pixmap for page " << request->page->number() << " [" << request->width << " x " << request->height << "]."; // compute dpi used to get an image with desired width and height Okular::Page * page = request->page(); double pageWidth = page->width(), pageHeight = page->height(); if ( page->rotation() % 2 ) qSwap( pageWidth, pageHeight ); qreal fakeDpiX = request->width() / pageWidth * dpi().width(); qreal fakeDpiY = request->height() / pageHeight * dpi().height(); // generate links rects only the first time bool genObjectRects = !rectsGenerated.at( page->number() ); // 0. LOCK [waits for the thread end] userMutex()->lock(); + if ( request->shouldAbortRender() ) + { + userMutex()->unlock(); + return QImage(); + } + // 1. Set OutputDev parameters and Generate contents // note: thread safety is set on 'false' for the GUI (this) thread Poppler::Page *p = pdfdoc->page(page->number()); // 2. Take data from outputdev and attach it to the Page QImage img; if (p) { +#ifdef HAVE_POPPLER_0_63 if ( request->isTile() ) { - QRect rect = request->normalizedRect().geometry( request->width(), request->height() ); -#ifdef HAVE_POPPLER_0_62 + const QRect rect = request->normalizedRect().geometry( request->width(), request->height() ); if ( request->partialUpdatesWanted() ) { - PartialUpdatePayload payload( this, request ); + RenderImagePayload payload( this, request ); + img = p->renderToImage( fakeDpiX, fakeDpiY, rect.x(), rect.y(), rect.width(), rect.height(), Poppler::Page::Rotate0, + partialUpdateCallback, shouldDoPartialUpdateCallback, shouldAbortRenderCallback, QVariant::fromValue( &payload ) ); + } + else + { + RenderImagePayload payload( this, request ); + img = p->renderToImage( fakeDpiX, fakeDpiY, rect.x(), rect.y(), rect.width(), rect.height(), Poppler::Page::Rotate0, + nullptr, nullptr, shouldAbortRenderCallback, QVariant::fromValue( &payload ) ); + } + } + else + { + if ( request->partialUpdatesWanted() ) + { + RenderImagePayload payload( this, request ); + img = p->renderToImage( fakeDpiX, fakeDpiY, -1, -1, -1, -1, Poppler::Page::Rotate0, + partialUpdateCallback, shouldDoPartialUpdateCallback, shouldAbortRenderCallback, QVariant::fromValue( &payload ) ); + } + else + { + RenderImagePayload payload( this, request ); + img = p->renderToImage( fakeDpiX, fakeDpiY, -1, -1, -1, -1, Poppler::Page::Rotate0, + nullptr, nullptr, shouldAbortRenderCallback, QVariant::fromValue( &payload ) ); + } + + } +#elif defined(HAVE_POPPLER_0_62) + if ( request->isTile() ) + { + const QRect rect = request->normalizedRect().geometry( request->width(), request->height() ); + if ( request->partialUpdatesWanted() ) + { + RenderImagePayload payload( this, request ); img = p->renderToImage( fakeDpiX, fakeDpiY, rect.x(), rect.y(), rect.width(), rect.height(), Poppler::Page::Rotate0, partialUpdateCallback, shouldDoPartialUpdateCallback, QVariant::fromValue( &payload ) ); } else -#endif { img = p->renderToImage( fakeDpiX, fakeDpiY, rect.x(), rect.y(), rect.width(), rect.height(), Poppler::Page::Rotate0 ); } } else { -#ifdef HAVE_POPPLER_0_62 if ( request->partialUpdatesWanted() ) { - PartialUpdatePayload payload(this, request); + RenderImagePayload payload( this, request ); img = p->renderToImage( fakeDpiX, fakeDpiY, -1, -1, -1, -1, Poppler::Page::Rotate0, partialUpdateCallback, shouldDoPartialUpdateCallback, QVariant::fromValue( &payload ) ); } else -#endif { - img = p->renderToImage(fakeDpiX, fakeDpiY, -1, -1, -1, -1, Poppler::Page::Rotate0 ); + img = p->renderToImage( fakeDpiX, fakeDpiY, -1, -1, -1, -1, Poppler::Page::Rotate0 ); } + } +#else + if ( request->isTile() ) + { + const QRect rect = request->normalizedRect().geometry( request->width(), request->height() ); + img = p->renderToImage( fakeDpiX, fakeDpiY, rect.x(), rect.y(), rect.width(), rect.height(), Poppler::Page::Rotate0 ); + } + else + { + img = p->renderToImage( fakeDpiX, fakeDpiY, -1, -1, -1, -1, Poppler::Page::Rotate0 ); + } +#endif } else { img = QImage( request->width(), request->height(), QImage::Format_Mono ); img.fill( Qt::white ); } if ( p && genObjectRects ) { // TODO previously we extracted Image type rects too, but that needed porting to poppler // and as we are not doing anything with Image type rects i did not port it, have a look at // dead gp_outputdev.cpp on image extraction page->setObjectRects( generateLinks(p->links()) ); rectsGenerated[ request->page()->number() ] = true; resolveMediaLinkReferences( page ); } // 3. UNLOCK [re-enables shared access] userMutex()->unlock(); delete p; return img; } template void resolveMediaLinks( Okular::Action *action, enum Okular::Annotation::SubType subType, QHash &annotationsHash ) { OkularLinkType *okularAction = static_cast( action ); const PopplerLinkType *popplerLink = action->nativeId().value(); QHashIterator it( annotationsHash ); while ( it.hasNext() ) { it.next(); if ( it.key()->subType() == subType ) { const PopplerAnnotationType *popplerAnnotation = static_cast( it.value() ); if ( popplerLink->isReferencedAnnotation( popplerAnnotation ) ) { okularAction->setAnnotation( static_cast( it.key() ) ); okularAction->setNativeId( QVariant() ); delete popplerLink; // delete the associated Poppler::LinkMovie object, it's not needed anymore break; } } } } void PDFGenerator::resolveMediaLinkReference( Okular::Action *action ) { if ( !action ) return; if ( (action->actionType() != Okular::Action::Movie) && (action->actionType() != Okular::Action::Rendition) ) return; resolveMediaLinks( action, Okular::Annotation::AMovie, annotationsOnOpenHash ); resolveMediaLinks( action, Okular::Annotation::AScreen, annotationsOnOpenHash ); } void PDFGenerator::resolveMediaLinkReferences( Okular::Page *page ) { resolveMediaLinkReference( const_cast( page->pageAction( Okular::Page::Opening ) ) ); resolveMediaLinkReference( const_cast( page->pageAction( Okular::Page::Closing ) ) ); foreach ( Okular::Annotation *annotation, page->annotations() ) { if ( annotation->subType() == Okular::Annotation::AScreen ) { Okular::ScreenAnnotation *screenAnnotation = static_cast( annotation ); resolveMediaLinkReference( screenAnnotation->additionalAction( Okular::Annotation::PageOpening ) ); resolveMediaLinkReference( screenAnnotation->additionalAction( Okular::Annotation::PageClosing ) ); } if ( annotation->subType() == Okular::Annotation::AWidget ) { Okular::WidgetAnnotation *widgetAnnotation = static_cast( annotation ); resolveMediaLinkReference( widgetAnnotation->additionalAction( Okular::Annotation::PageOpening ) ); resolveMediaLinkReference( widgetAnnotation->additionalAction( Okular::Annotation::PageClosing ) ); } } foreach ( Okular::FormField *field, page->formFields() ) resolveMediaLinkReference( field->activationAction() ); } -Okular::TextPage* PDFGenerator::textPage( Okular::Page *page ) +#ifdef HAVE_POPPLER_0_63 +struct TextExtractionPayload { + TextExtractionPayload(Okular::TextRequest *r) : + request(r) + { + } + + Okular::TextRequest *request; +}; +Q_DECLARE_METATYPE(TextExtractionPayload*) + +static bool shouldAbortTextExtractionCallback(const QVariant &vPayload) +{ + auto payload = vPayload.value(); + return payload->request->shouldAbortExtraction(); +} +#endif + +Okular::TextPage* PDFGenerator::textPage( Okular::TextRequest *request ) +{ + const Okular::Page *page = request->page(); #ifdef PDFGENERATOR_DEBUG qCDebug(OkularPdfDebug) << "page" << page->number(); #endif // build a TextList... QList textList; double pageWidth, pageHeight; Poppler::Page *pp = pdfdoc->page( page->number() ); if (pp) { userMutex()->lock(); +#ifdef HAVE_POPPLER_0_63 + TextExtractionPayload payload(request); + textList = pp->textList( Poppler::Page::Rotate0, shouldAbortTextExtractionCallback, QVariant::fromValue( &payload ) ); +#else textList = pp->textList(); +#endif userMutex()->unlock(); QSizeF s = pp->pageSizeF(); pageWidth = s.width(); pageHeight = s.height(); delete pp; } else { pageWidth = defaultPageWidth; pageHeight = defaultPageHeight; } + if ( textList.isEmpty() && request->shouldAbortExtraction() ) + return nullptr; + Okular::TextPage *tp = abstractTextPage(textList, pageHeight, pageWidth, (Poppler::Page::Rotation)page->orientation()); qDeleteAll(textList); return tp; } void PDFGenerator::requestFontData(const Okular::FontInfo &font, QByteArray *data) { Poppler::FontInfo fi = font.nativeId().value(); *data = pdfdoc->fontData(fi); } #define DUMMY_QPRINTER_COPY bool PDFGenerator::print( QPrinter& printer ) { bool printAnnots = true; bool forceRasterize = false; if ( pdfOptionsPage ) { printAnnots = pdfOptionsPage->printAnnots(); forceRasterize = pdfOptionsPage->printForceRaster(); } #ifdef Q_OS_WIN // Windows can only print by rasterization, because that is // currently the only way Okular implements printing without using UNIX-specific // tools like 'lpr'. forceRasterize = true; #ifndef HAVE_POPPLER_0_60 // The Document::HideAnnotations flags was introduced in poppler 0.60 printAnnots = true; #endif #endif #ifdef HAVE_POPPLER_0_60 if ( forceRasterize ) { pdfdoc->setRenderHint(Poppler::Document::HideAnnotations, !printAnnots); #else if ( forceRasterize && printAnnots) { #endif QPainter painter; painter.begin(&printer); QList pageList = Okular::FilePrinter::pageList( printer, pdfdoc->numPages(), document()->currentPage() + 1, document()->bookmarkedPageList() ); for ( int i = 0; i < pageList.count(); ++i ) { if ( i != 0 ) printer.newPage(); const int page = pageList.at( i ) - 1; userMutex()->lock(); Poppler::Page *pp = pdfdoc->page( page ); if (pp) { #ifdef Q_OS_WIN QImage img = pp->renderToImage( printer.physicalDpiX(), printer.physicalDpiY() ); #else // UNIX: Same resolution as the postscript rasterizer; see discussion at https://git.reviewboard.kde.org/r/130218/ QImage img = pp->renderToImage( 300, 300 ); #endif painter.drawImage( painter.window(), img, QRectF(0, 0, img.width(), img.height()) ); delete pp; } userMutex()->unlock(); } painter.end(); return true; } #ifdef DUMMY_QPRINTER_COPY // Get the real page size to pass to the ps generator QPrinter dummy( QPrinter::PrinterResolution ); dummy.setFullPage( true ); dummy.setOrientation( printer.orientation() ); dummy.setPageSize( printer.pageSize() ); dummy.setPaperSize( printer.paperSize( QPrinter::Millimeter ), QPrinter::Millimeter ); int width = dummy.width(); int height = dummy.height(); #else int width = printer.width(); int height = printer.height(); #endif if (width <= 0 || height <= 0) { lastPrintError = InvalidPageSizePrintError; return false; } // Create the tempfile to send to FilePrinter, which will manage the deletion QTemporaryFile tf(QDir::tempPath() + QLatin1String("/okular_XXXXXX.ps")); if ( !tf.open() ) { lastPrintError = TemporaryFileOpenPrintError; return false; } QString tempfilename = tf.fileName(); // Generate the list of pages to be printed as selected in the print dialog QList pageList = Okular::FilePrinter::pageList( printer, pdfdoc->numPages(), document()->currentPage() + 1, document()->bookmarkedPageList() ); // TODO rotation tf.setAutoRemove(false); QString pstitle = metaData(QStringLiteral("Title"), QVariant()).toString(); if ( pstitle.trimmed().isEmpty() ) { pstitle = document()->currentDocument().fileName(); } Poppler::PSConverter *psConverter = pdfdoc->psConverter(); psConverter->setOutputDevice(&tf); psConverter->setPageList(pageList); psConverter->setPaperWidth(width); psConverter->setPaperHeight(height); psConverter->setRightMargin(0); psConverter->setBottomMargin(0); psConverter->setLeftMargin(0); psConverter->setTopMargin(0); psConverter->setStrictMargins(false); psConverter->setForceRasterize(forceRasterize); psConverter->setTitle(pstitle); if (!printAnnots) psConverter->setPSOptions(psConverter->psOptions() | Poppler::PSConverter::HideAnnotations ); userMutex()->lock(); if (psConverter->convert()) { userMutex()->unlock(); delete psConverter; tf.close(); int ret = Okular::FilePrinter::printFile( printer, tempfilename, document()->orientation(), Okular::FilePrinter::SystemDeletesFiles, Okular::FilePrinter::ApplicationSelectsPages, document()->bookmarkedPageRange() ); lastPrintError = Okular::FilePrinter::printError( ret ); return (lastPrintError == NoPrintError); } else { lastPrintError = FileConversionPrintError; delete psConverter; userMutex()->unlock(); } tf.close(); return false; } QVariant PDFGenerator::metaData( const QString & key, const QVariant & option ) const { if ( key == QLatin1String("StartFullScreen") ) { QMutexLocker ml(userMutex()); // asking for the 'start in fullscreen mode' (pdf property) if ( pdfdoc->pageMode() == Poppler::Document::FullScreen ) return true; } else if ( key == QLatin1String("NamedViewport") && !option.toString().isEmpty() ) { Okular::DocumentViewport viewport; QString optionString = option.toString(); // asking for the page related to a 'named link destination'. the // option is the link name. @see addSynopsisChildren. userMutex()->lock(); Poppler::LinkDestination *ld = pdfdoc->linkDestination( optionString ); userMutex()->unlock(); if ( ld ) { fillViewportFromLinkDestination( viewport, *ld ); } delete ld; if ( viewport.pageNumber >= 0 ) return viewport.toString(); } else if ( key == QLatin1String("DocumentTitle") ) { userMutex()->lock(); QString title = pdfdoc->info( QStringLiteral("Title") ); userMutex()->unlock(); return title; } else if ( key == QLatin1String("OpenTOC") ) { QMutexLocker ml(userMutex()); if ( pdfdoc->pageMode() == Poppler::Document::UseOutlines ) return true; } else if ( key == QLatin1String("DocumentScripts") && option.toString() == QLatin1String("JavaScript") ) { QMutexLocker ml(userMutex()); return pdfdoc->scripts(); } else if ( key == QLatin1String("HasUnsupportedXfaForm") ) { QMutexLocker ml(userMutex()); return pdfdoc->formType() == Poppler::Document::XfaForm; } else if ( key == QLatin1String("FormCalculateOrder") ) { #ifdef HAVE_POPPLER_0_53 QMutexLocker ml(userMutex()); return QVariant::fromValue>(pdfdoc->formCalculateOrder()); #endif } return QVariant(); } bool PDFGenerator::reparseConfig() { if ( !pdfdoc ) return false; bool somethingchanged = false; // load paper color QColor color = documentMetaData( PaperColorMetaData, true ).value< QColor >(); // if paper color is changed we have to rebuild every visible pixmap in addition // to the outputDevice. it's the 'heaviest' case, other effect are just recoloring // over the page rendered on 'standard' white background. if ( color != pdfdoc->paperColor() ) { userMutex()->lock(); pdfdoc->setPaperColor(color); userMutex()->unlock(); somethingchanged = true; } bool aaChanged = setDocumentRenderHints(); somethingchanged = somethingchanged || aaChanged; return somethingchanged; } void PDFGenerator::addPages( KConfigDialog *dlg ) { #ifdef HAVE_POPPLER_0_24 Ui_PDFSettingsWidget pdfsw; QWidget* w = new QWidget(dlg); pdfsw.setupUi(w); dlg->addPage(w, PDFSettings::self(), i18n("PDF"), QStringLiteral("application-pdf"), i18n("PDF Backend Configuration") ); #endif } bool PDFGenerator::setDocumentRenderHints() { bool changed = false; const Poppler::Document::RenderHints oldhints = pdfdoc->renderHints(); #define SET_HINT(hintname, hintdefvalue, hintflag) \ { \ bool newhint = documentMetaData(hintname, hintdefvalue).toBool(); \ if (newhint != oldhints.testFlag(hintflag)) \ { \ pdfdoc->setRenderHint(hintflag, newhint); \ changed = true; \ } \ } SET_HINT(GraphicsAntialiasMetaData, true, Poppler::Document::Antialiasing) SET_HINT(TextAntialiasMetaData, true, Poppler::Document::TextAntialiasing) SET_HINT(TextHintingMetaData, false, Poppler::Document::TextHinting) #undef SET_HINT #ifdef HAVE_POPPLER_0_24 // load thin line mode const int thinLineMode = PDFSettings::enhanceThinLines(); const bool enableThinLineSolid = thinLineMode == PDFSettings::EnumEnhanceThinLines::Solid; const bool enableShapeLineSolid = thinLineMode == PDFSettings::EnumEnhanceThinLines::Shape; const bool thinLineSolidWasEnabled = (oldhints & Poppler::Document::ThinLineSolid) == Poppler::Document::ThinLineSolid; const bool thinLineShapeWasEnabled = (oldhints & Poppler::Document::ThinLineShape) == Poppler::Document::ThinLineShape; if (enableThinLineSolid != thinLineSolidWasEnabled) { pdfdoc->setRenderHint(Poppler::Document::ThinLineSolid, enableThinLineSolid); changed = true; } if (enableShapeLineSolid != thinLineShapeWasEnabled) { pdfdoc->setRenderHint(Poppler::Document::ThinLineShape, enableShapeLineSolid); changed = true; } #endif return changed; } Okular::ExportFormat::List PDFGenerator::exportFormats() const { static Okular::ExportFormat::List formats; if ( formats.isEmpty() ) { formats.append( Okular::ExportFormat::standardFormat( Okular::ExportFormat::PlainText ) ); } return formats; } bool PDFGenerator::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 ); int num = document()->pages(); for ( int i = 0; i < num; ++i ) { QString text; userMutex()->lock(); Poppler::Page *pp = pdfdoc->page(i); if (pp) { text = pp->text(QRect()).normalized(QString::NormalizationForm_KC); } userMutex()->unlock(); ts << text; delete pp; } f.close(); return true; } return false; } //END Generator inherited functions inline void append (Okular::TextPage* ktp, const QString &s, double l, double b, double r, double t) { // kWarning(PDFDebug).nospace() << "text: " << s << " at (" << l << "," << t << ")x(" << r <<","<append(s, new Okular::NormalizedRect(l, t, r, b)); } Okular::TextPage * PDFGenerator::abstractTextPage(const QList &text, double height, double width,int rot) { Q_UNUSED(rot); Okular::TextPage* ktp=new Okular::TextPage; Poppler::TextBox *next; #ifdef PDFGENERATOR_DEBUG qCDebug(OkularPdfDebug) << "getting text page in generator pdf - rotation:" << rot; #endif QString s; bool addChar; foreach (Poppler::TextBox *word, text) { const int qstringCharCount = word->text().length(); next=word->nextWord(); int textBoxChar = 0; for (int j = 0; j < qstringCharCount; j++) { const QChar c = word->text().at(j); if (c.isHighSurrogate()) { s = c; addChar = false; } else if (c.isLowSurrogate()) { s += c; addChar = true; } else { s = c; addChar = true; } if (addChar) { QRectF charBBox = word->charBoundingBox(textBoxChar); append(ktp, (j==qstringCharCount-1 && !next) ? (s + QLatin1Char('\n')) : s, charBBox.left()/width, charBBox.bottom()/height, charBBox.right()/width, charBBox.top()/height); textBoxChar++; } } if ( word->hasSpaceAfter() && next ) { // TODO Check with a document with vertical text // probably won't work and we will need to do comparisons // between wordBBox and nextWordBBox to see if they are // vertically or horizontally aligned QRectF wordBBox = word->boundingBox(); QRectF nextWordBBox = next->boundingBox(); append(ktp, QStringLiteral(" "), wordBBox.right()/width, wordBBox.bottom()/height, nextWordBBox.left()/width, wordBBox.top()/height); } } return ktp; } void PDFGenerator::addSynopsisChildren( QDomNode * parent, QDomNode * parentDestination ) { // keep track of the current listViewItem QDomNode n = parent->firstChild(); while( !n.isNull() ) { // convert the node to an element (sure it is) QDomElement e = n.toElement(); // The name is the same QDomElement item = docSyn.createElement( e.tagName() ); parentDestination->appendChild(item); if (!e.attribute(QStringLiteral("ExternalFileName")).isNull()) item.setAttribute(QStringLiteral("ExternalFileName"), e.attribute(QStringLiteral("ExternalFileName"))); if (!e.attribute(QStringLiteral("DestinationName")).isNull()) item.setAttribute(QStringLiteral("ViewportName"), e.attribute(QStringLiteral("DestinationName"))); if (!e.attribute(QStringLiteral("Destination")).isNull()) { Okular::DocumentViewport vp; fillViewportFromLinkDestination( vp, Poppler::LinkDestination(e.attribute(QStringLiteral("Destination"))) ); item.setAttribute( QStringLiteral("Viewport"), vp.toString() ); } if (!e.attribute(QStringLiteral("Open")).isNull()) item.setAttribute(QStringLiteral("Open"), e.attribute(QStringLiteral("Open"))); if (!e.attribute(QStringLiteral("DestinationURI")).isNull()) item.setAttribute(QStringLiteral("URL"), e.attribute(QStringLiteral("DestinationURI"))); // descend recursively and advance to the next node if ( e.hasChildNodes() ) addSynopsisChildren( &n, & item ); n = n.nextSibling(); } } void PDFGenerator::addAnnotations( Poppler::Page * popplerPage, Okular::Page * page ) { #ifdef HAVE_POPPLER_0_28 QSet subtypes; subtypes << Poppler::Annotation::AFileAttachment << Poppler::Annotation::ASound << Poppler::Annotation::AMovie << Poppler::Annotation::AWidget << Poppler::Annotation::AScreen << Poppler::Annotation::AText << Poppler::Annotation::ALine << Poppler::Annotation::AGeom << Poppler::Annotation::AHighlight << Poppler::Annotation::AInk << Poppler::Annotation::AStamp << Poppler::Annotation::ACaret; QList popplerAnnotations = popplerPage->annotations( subtypes ); #else QList popplerAnnotations = popplerPage->annotations(); #endif foreach(Poppler::Annotation *a, popplerAnnotations) { bool doDelete = true; Okular::Annotation * newann = createAnnotationFromPopplerAnnotation( a, &doDelete ); if (newann) { page->addAnnotation(newann); if ( a->subType() == Poppler::Annotation::AScreen ) { Poppler::ScreenAnnotation *annotScreen = static_cast( a ); Okular::ScreenAnnotation *screenAnnotation = static_cast( newann ); // The activation action const Poppler::Link *actionLink = annotScreen->action(); if ( actionLink ) screenAnnotation->setAction( createLinkFromPopplerLink( actionLink ) ); // The additional actions const Poppler::Link *pageOpeningLink = annotScreen->additionalAction( Poppler::Annotation::PageOpeningAction ); if ( pageOpeningLink ) screenAnnotation->setAdditionalAction( Okular::Annotation::PageOpening, createLinkFromPopplerLink( pageOpeningLink ) ); const Poppler::Link *pageClosingLink = annotScreen->additionalAction( Poppler::Annotation::PageClosingAction ); if ( pageClosingLink ) screenAnnotation->setAdditionalAction( Okular::Annotation::PageClosing, createLinkFromPopplerLink( pageClosingLink ) ); } if ( a->subType() == Poppler::Annotation::AWidget ) { Poppler::WidgetAnnotation *annotWidget = static_cast( a ); Okular::WidgetAnnotation *widgetAnnotation = static_cast( newann ); // The additional actions const Poppler::Link *pageOpeningLink = annotWidget->additionalAction( Poppler::Annotation::PageOpeningAction ); if ( pageOpeningLink ) widgetAnnotation->setAdditionalAction( Okular::Annotation::PageOpening, createLinkFromPopplerLink( pageOpeningLink ) ); const Poppler::Link *pageClosingLink = annotWidget->additionalAction( Poppler::Annotation::PageClosingAction ); if ( pageClosingLink ) widgetAnnotation->setAdditionalAction( Okular::Annotation::PageClosing, createLinkFromPopplerLink( pageClosingLink ) ); } if ( !doDelete ) annotationsOnOpenHash.insert( newann, a ); } if ( doDelete ) delete a; } } void PDFGenerator::addTransition( Poppler::Page * pdfPage, Okular::Page * page ) // called on opening when MUTEX is not used { Poppler::PageTransition *pdfTransition = pdfPage->transition(); if ( !pdfTransition || pdfTransition->type() == Poppler::PageTransition::Replace ) return; Okular::PageTransition *transition = new Okular::PageTransition(); switch ( pdfTransition->type() ) { case Poppler::PageTransition::Replace: // won't get here, added to avoid warning break; case Poppler::PageTransition::Split: transition->setType( Okular::PageTransition::Split ); break; case Poppler::PageTransition::Blinds: transition->setType( Okular::PageTransition::Blinds ); break; case Poppler::PageTransition::Box: transition->setType( Okular::PageTransition::Box ); break; case Poppler::PageTransition::Wipe: transition->setType( Okular::PageTransition::Wipe ); break; case Poppler::PageTransition::Dissolve: transition->setType( Okular::PageTransition::Dissolve ); break; case Poppler::PageTransition::Glitter: transition->setType( Okular::PageTransition::Glitter ); break; case Poppler::PageTransition::Fly: transition->setType( Okular::PageTransition::Fly ); break; case Poppler::PageTransition::Push: transition->setType( Okular::PageTransition::Push ); break; case Poppler::PageTransition::Cover: transition->setType( Okular::PageTransition::Cover ); break; case Poppler::PageTransition::Uncover: transition->setType( Okular::PageTransition::Uncover ); break; case Poppler::PageTransition::Fade: transition->setType( Okular::PageTransition::Fade ); break; } #ifdef HAVE_POPPLER_0_37 transition->setDuration( pdfTransition->durationReal() ); #else transition->setDuration( pdfTransition->duration() ); #endif switch ( pdfTransition->alignment() ) { case Poppler::PageTransition::Horizontal: transition->setAlignment( Okular::PageTransition::Horizontal ); break; case Poppler::PageTransition::Vertical: transition->setAlignment( Okular::PageTransition::Vertical ); break; } switch ( pdfTransition->direction() ) { case Poppler::PageTransition::Inward: transition->setDirection( Okular::PageTransition::Inward ); break; case Poppler::PageTransition::Outward: transition->setDirection( Okular::PageTransition::Outward ); break; } transition->setAngle( pdfTransition->angle() ); transition->setScale( pdfTransition->scale() ); transition->setIsRectangular( pdfTransition->isRectangular() ); page->setTransition( transition ); } void PDFGenerator::addFormFields( Poppler::Page * popplerPage, Okular::Page * page ) { QList popplerFormFields = popplerPage->formFields(); QLinkedList okularFormFields; foreach( Poppler::FormField *f, popplerFormFields ) { Okular::FormField * of = 0; switch ( f->type() ) { case Poppler::FormField::FormButton: of = new PopplerFormFieldButton( static_cast( f ) ); break; case Poppler::FormField::FormText: of = new PopplerFormFieldText( static_cast( f ) ); break; case Poppler::FormField::FormChoice: of = new PopplerFormFieldChoice( static_cast( f ) ); break; default: ; } if ( of ) // form field created, good - it will take care of the Poppler::FormField okularFormFields.append( of ); else // no form field available - delete the Poppler::FormField delete f; } if ( !okularFormFields.isEmpty() ) page->setFormFields( okularFormFields ); } PDFGenerator::PrintError PDFGenerator::printError() const { return lastPrintError; } QWidget* PDFGenerator::printConfigurationWidget() const { if ( !pdfOptionsPage ) { const_cast(this)->pdfOptionsPage = new PDFOptionsPage(); } return pdfOptionsPage; } bool PDFGenerator::supportsOption( SaveOption option ) const { switch ( option ) { case SaveChanges: { return true; } default: ; } return false; } bool PDFGenerator::save( const QString &fileName, SaveOptions options, QString *errorText ) { Q_UNUSED(errorText); Poppler::PDFConverter *pdfConv = pdfdoc->pdfConverter(); pdfConv->setOutputFileName( fileName ); if ( options & SaveChanges ) pdfConv->setPDFOptions( pdfConv->pdfOptions() | Poppler::PDFConverter::WithChanges ); QMutexLocker locker( userMutex() ); QHashIterator it( annotationsOnOpenHash ); while ( it.hasNext() ) { it.next(); if ( it.value()->uniqueName().isEmpty() ) { it.value()->setUniqueName( it.key()->uniqueName() ); } } bool success = pdfConv->convert(); if (!success) { switch (pdfConv->lastError()) { case Poppler::BaseConverter::NotSupportedInputFileError: // This can only happen with Poppler before 0.22 which did not have qt5 version break; case Poppler::BaseConverter::NoError: case Poppler::BaseConverter::FileLockedError: // we can't get here break; case Poppler::BaseConverter::OpenOutputError: // the default text message is good for this case break; } } delete pdfConv; return success; } Okular::AnnotationProxy* PDFGenerator::annotationProxy() const { return annotProxy; } #include "generator_pdf.moc" Q_LOGGING_CATEGORY(OkularPdfDebug, "org.kde.okular.generators.pdf", QtWarningMsg) /* kate: replace-tabs on; indent-width 4; */ diff --git a/generators/poppler/generator_pdf.h b/generators/poppler/generator_pdf.h index d6ea065ba..4439a1144 100644 --- a/generators/poppler/generator_pdf.h +++ b/generators/poppler/generator_pdf.h @@ -1,156 +1,156 @@ /*************************************************************************** * Copyright (C) 2004-2008 by Albert Astals Cid * * Copyright (C) 2004 by Enrico Ros * * Copyright (C) 2017 Klarälvdalens Datakonsult AB, a KDAB Group * * company, info@kdab.com. Work sponsored by the * * LiMux project of the city of Munich * * * * 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. * ***************************************************************************/ #ifndef _OKULAR_GENERATOR_PDF_H_ #define _OKULAR_GENERATOR_PDF_H_ //#include "synctex/synctex_parser.h" #include #include #include #include #include #include #include #include namespace Okular { class ObjectRect; class SourceReference; } class PDFOptionsPage; class PopplerAnnotationProxy; /** * @short A generator that builds contents from a PDF document. * * All Generator features are supported and implented by this one. * Internally this holds a reference to xpdf's core objects and provides * contents generation using the PDFDoc object and a couple of OutputDevices * called Okular::OutputDev and Okular::TextDev (both defined in gp_outputdev.h). * * For generating page contents we tell PDFDoc to render a page and grab * contents from out OutputDevs when rendering finishes. * */ class PDFGenerator : public Okular::Generator, public Okular::ConfigInterface, public Okular::PrintInterface, public Okular::SaveInterface { Q_OBJECT Q_INTERFACES( Okular::Generator ) Q_INTERFACES( Okular::ConfigInterface ) Q_INTERFACES( Okular::PrintInterface ) Q_INTERFACES( Okular::SaveInterface ) public: PDFGenerator( QObject *parent, const QVariantList &args ); virtual ~PDFGenerator(); // [INHERITED] load a document and fill up the pagesVector Okular::Document::OpenResult loadDocumentWithPassword( const QString & fileName, QVector & pagesVector, const QString & password ) override; Okular::Document::OpenResult loadDocumentFromDataWithPassword( const QByteArray & fileData, QVector & pagesVector, const QString & password ) override; void loadPages(QVector &pagesVector, int rotation=-1, bool clear=false); // [INHERITED] document information Okular::DocumentInfo generateDocumentInfo( const QSet &keys ) const override; const Okular::DocumentSynopsis * generateDocumentSynopsis() override; Okular::FontInfo::List fontsForPage( int page ) override; const QList * embeddedFiles() const override; PageSizeMetric pagesSizeMetric() const override{ return Pixels; } QAbstractItemModel * layersModel() const override; void opaqueAction( const Okular::BackendOpaqueAction *action ) override; // [INHERITED] document information bool isAllowed( Okular::Permission permission ) const override; // [INHERITED] perform actions on document / pages QImage image( Okular::PixmapRequest *page ) override; // [INHERITED] print page using an already configured kprinter bool print( QPrinter& printer ) override; // [INHERITED] reply to some metadata requests QVariant metaData( const QString & key, const QVariant & option ) const override; // [INHERITED] reparse configuration bool reparseConfig() override; void addPages( KConfigDialog * ) override; // [INHERITED] text exporting Okular::ExportFormat::List exportFormats() const override; bool exportTo( const QString &fileName, const Okular::ExportFormat &format ) override; // [INHERITED] print interface QWidget* printConfigurationWidget() const override; // [INHERITED] save interface bool supportsOption( SaveOption ) const override; bool save( const QString &fileName, SaveOptions options, QString *errorText ) override; Okular::AnnotationProxy* annotationProxy() const override; protected: SwapBackingFileResult swapBackingFile( QString const &newFileName, QVector & newPagesVector ) override; bool doCloseDocument() override; - Okular::TextPage* textPage( Okular::Page *page ) override; + Okular::TextPage* textPage( Okular::TextRequest *request ) override; protected Q_SLOTS: void requestFontData(const Okular::FontInfo &font, QByteArray *data); Okular::Generator::PrintError printError() const; private: Okular::Document::OpenResult init(QVector & pagesVector, const QString &password); // create the document synopsis hieracy void addSynopsisChildren( QDomNode * parentSource, QDomNode * parentDestination ); // fetch annotations from the pdf file and add they to the page void addAnnotations( Poppler::Page * popplerPage, Okular::Page * page ); // fetch the transition information and add it to the page void addTransition( Poppler::Page * popplerPage, Okular::Page * page ); // fetch the form fields and add them to the page void addFormFields( Poppler::Page * popplerPage, Okular::Page * page ); Okular::TextPage * abstractTextPage(const QList &text, double height, double width, int rot); void resolveMediaLinkReferences( Okular::Page *page ); void resolveMediaLinkReference( Okular::Action *action ); bool setDocumentRenderHints(); // poppler dependant stuff Poppler::Document *pdfdoc; // misc variables for document info and synopsis caching bool docSynopsisDirty; Okular::DocumentSynopsis docSyn; mutable bool docEmbeddedFilesDirty; mutable QList docEmbeddedFiles; int nextFontPage; PopplerAnnotationProxy *annotProxy; // the hash below only contains annotations that were present on the file at open time // this is enough for what we use it for QHash annotationsOnOpenHash; QBitArray rectsGenerated; QPointer pdfOptionsPage; PrintError lastPrintError; }; #endif /* kate: replace-tabs on; indent-width 4; */ diff --git a/generators/xps/generator_xps.cpp b/generators/xps/generator_xps.cpp index 69b5079c5..d184b68da 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); 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 ) +Okular::TextPage* XpsGenerator::textPage( Okular::TextRequest * request ) { QMutexLocker lock( userMutex() ); - XpsPage * xpsPage = m_xpsFile->page( page->number() ); + XpsPage * xpsPage = m_xpsFile->page( request->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" diff --git a/generators/xps/generator_xps.h b/generators/xps/generator_xps.h index 93e80a59f..3535fb49a 100644 --- a/generators/xps/generator_xps.h +++ b/generators/xps/generator_xps.h @@ -1,330 +1,330 @@ /* Copyright (C) 2006 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. */ #ifndef _OKULAR_GENERATOR_XPS_H_ #define _OKULAR_GENERATOR_XPS_H_ #include #include #include #include #include #include #include #include #include #include #include #include typedef enum {abtCommand, abtNumber, abtComma, abtEOF} AbbPathTokenType; class AbbPathToken { public: QString data; int curPos; AbbPathTokenType type; char command; double number; }; /** Holds information about xml element during SAX parsing of page */ class XpsRenderNode { public: QString name; QVector children; QXmlAttributes attributes; QVariant data; XpsRenderNode * findChild( const QString &name ); QVariant getRequiredChildData( const QString &name ); QVariant getChildData( const QString &name ); }; struct XpsGradient { XpsGradient( double o, const QColor &c ) : offset( o ), color( c ) {} double offset; QColor color; }; /** Types of data in XpsRenderNode::data. Name of each type consist of Xps and name of xml element which data it holds */ typedef QTransform XpsMatrixTransform; typedef QTransform XpsRenderTransform; typedef QBrush XpsFill; struct XpsPathFigure { XpsPathFigure( const QPainterPath &_path, bool filled ) : path( _path ), isFilled( filled ) {} QPainterPath path; bool isFilled; }; struct XpsPathGeometry { XpsPathGeometry() : fillRule( Qt::OddEvenFill ) {} ~XpsPathGeometry() { qDeleteAll( paths ); } QList< XpsPathFigure* > paths; Qt::FillRule fillRule; XpsMatrixTransform transform; }; class XpsPage; class XpsFile; class XpsHandler: public QXmlDefaultHandler { public: XpsHandler( XpsPage *page ); ~XpsHandler(); bool startElement( const QString & nameSpace, const QString & localName, const QString & qname, const QXmlAttributes & atts ) override; bool endElement( const QString & nameSpace, const QString & localName, const QString & qname ) override; bool startDocument() override; protected: XpsPage *m_page; void processStartElement( XpsRenderNode &node ); void processEndElement( XpsRenderNode &node ); // Methods for processing of diferent xml elements void processGlyph( XpsRenderNode &node ); void processPath( XpsRenderNode &node ); void processPathData( XpsRenderNode &node ); void processFill( XpsRenderNode &node ); void processStroke( XpsRenderNode &node ); void processImageBrush (XpsRenderNode &node ); void processPathGeometry( XpsRenderNode &node ); void processPathFigure( XpsRenderNode &node ); QPainter *m_painter; QImage m_image; QStack m_nodes; friend class XpsPage; }; class XpsPage { public: XpsPage(XpsFile *file, const QString &fileName); ~XpsPage(); QSizeF size() const; bool renderToImage( QImage *p ); bool renderToPainter( QPainter *painter ); Okular::TextPage* textPage(); QImage loadImageFromFile( const QString &filename ); private: XpsFile *m_file; const QString m_fileName; QSizeF m_pageSize; QString m_thumbnailFileName; bool m_thumbnailMightBeAvailable; QImage m_thumbnail; bool m_thumbnailIsLoaded; QImage *m_pageImage; bool m_pageIsRendered; friend class XpsHandler; friend class XpsTextExtractionHandler; }; /** Represents one of the (perhaps the only) documents in an XpsFile */ class XpsDocument { public: XpsDocument(XpsFile *file, const QString &fileName); ~XpsDocument(); /** the total number of pages in this document */ int numPages() const; /** obtain a certain page from this document \param pageNum the number of the page to return \note page numbers are zero based - they run from 0 to numPages() - 1 */ XpsPage* page(int pageNum) const; /** whether this document has a Document Structure */ bool hasDocumentStructure(); /** the document structure for this document, if available */ const Okular::DocumentSynopsis * documentStructure(); private: void parseDocumentStructure( const QString &documentStructureFileName ); QList m_pages; XpsFile * m_file; bool m_haveDocumentStructure; Okular::DocumentSynopsis *m_docStructure; QMap m_docStructurePageMap; }; /** Represents the contents of a Microsoft XML Paper Specification format document. */ class XpsFile { public: XpsFile(); ~XpsFile(); bool loadDocument( const QString & fileName ); bool closeDocument(); Okular::DocumentInfo generateDocumentInfo() const; QImage thumbnail(); /** the total number of XpsDocuments with this file */ int numDocuments() const; /** the total number of pages in all the XpsDocuments within this file */ int numPages() const; /** a page from the file \param pageNum the page number of the page to return \note page numbers are zero based - they run from 0 to numPages() - 1 */ XpsPage* page(int pageNum) const; /** obtain a certain document from this file \param documentNum the number of the document to return \note document numbers are zero based - they run from 0 to numDocuments() - 1 */ XpsDocument* document(int documentNum) const; QFont getFontByName( const QString &fontName, float size ); KZip* xpsArchive(); private: int loadFontByName( const QString &fontName ); QList m_documents; QList m_pages; QString m_thumbnailFileName; bool m_thumbnailMightBeAvailable; QImage m_thumbnail; bool m_thumbnailIsLoaded; QString m_corePropertiesFileName; QString m_signatureOrigin; KZip * m_xpsArchive; QMap m_fontCache; QFontDatabase m_fontDatabase; }; class XpsGenerator : public Okular::Generator { Q_OBJECT Q_INTERFACES( Okular::Generator ) public: XpsGenerator( QObject *parent, const QVariantList &args ); virtual ~XpsGenerator(); bool loadDocument( const QString & fileName, QVector & pagesVector ) override; Okular::DocumentInfo generateDocumentInfo( const QSet &keys ) const override; const Okular::DocumentSynopsis * generateDocumentSynopsis() override; Okular::ExportFormat::List exportFormats() const override; bool exportTo( const QString &fileName, const Okular::ExportFormat &format ) override; bool print( QPrinter &printer ) override; protected: bool doCloseDocument() override; QImage image( Okular::PixmapRequest *page ) override; - Okular::TextPage* textPage( Okular::Page * page ) override; + Okular::TextPage* textPage( Okular::TextRequest * request ) override; private: XpsFile *m_xpsFile; }; Q_DECLARE_LOGGING_CATEGORY(OkularXpsDebug) #endif