diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 8de30a2ea..f6185787b 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,36 +1,36 @@ include: - https://invent.kde.org/sysadmin/ci-tooling/raw/master/invent/ci-before.yml - https://invent.kde.org/sysadmin/ci-tooling/raw/master/invent/ci-applications-linux.yml build_ubuntu_18_04: stage: build image: ubuntu:bionic only: - merge_requests before_script: - sed -i -e 's/# deb-src/deb-src/g' /etc/apt/sources.list - apt-get update - apt-get build-dep --yes --no-install-recommends okular - apt-get install --yes --no-install-recommends ninja-build libkf5crash-dev script: - mkdir -p build && cd build - cmake -G Ninja .. - ninja build_clang_tidy: stage: build image: debian:unstable only: - merge_requests before_script: - echo 'deb-src http://deb.debian.org/debian unstable main' >> /etc/apt/sources.list - apt-get update - apt-get build-dep --yes --no-install-recommends okular - apt-get install --yes --no-install-recommends ninja-build clang clang-tidy python python-yaml libkf5crash-dev libkf5purpose-dev libegl-dev jq script: - srcdir=`pwd` && mkdir -p /tmp/okular_build && cd /tmp/okular_build && CC=clang CXX=clang++ cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -G Ninja $srcdir && cat compile_commands.json | jq '[.[] | select(.file | contains("'"$srcdir"'"))]' > compile_commands.aux.json && cat compile_commands.aux.json | jq '[.[] | select(.file | contains("/synctex/")| not)]' > compile_commands.json - ninja # Fix the poppler header, remove when debian:unstable ships poppler 0.82 or later - sed -i "N;N;N;N; s#class MediaRendition\;\nclass MovieAnnotation\;\nclass ScreenAnnotation;#class MediaRendition\;#g" /usr/include/poppler/qt5/poppler-link.h - - "run-clang-tidy -header-filter='.*/okular/.*' -checks='-*,performance-*,bugprone-*,readability-inconsistent-declaration-parameter-name,readability-string-compare,modernize-redundant-void-arg,modernize-use-bool-literals,modernize-make-unique,modernize-make-shared,modernize-use-override,modernize-use-equals-delete,modernize-use-emplace,-bugprone-macro-parentheses,-bugprone-narrowing-conversions,-bugprone-branch-clone,-bugprone-incorrect-roundings' -config=\"{WarningsAsErrors: '*'}\"" + - "run-clang-tidy -header-filter='.*/okular/.*' -checks='-*,performance-*,bugprone-*,readability-inconsistent-declaration-parameter-name,readability-string-compare,modernize-redundant-void-arg,modernize-use-bool-literals,modernize-make-unique,modernize-make-shared,modernize-use-override,modernize-use-equals-delete,modernize-use-emplace,modernize-loop-convert,-bugprone-macro-parentheses,-bugprone-narrowing-conversions,-bugprone-branch-clone,-bugprone-incorrect-roundings' -config=\"{WarningsAsErrors: '*'}\"" diff --git a/core/annotations.cpp b/core/annotations.cpp index 308d328dc..f1d3b315d 100644 --- a/core/annotations.cpp +++ b/core/annotations.cpp @@ -1,3118 +1,3118 @@ /*************************************************************************** * Copyright (C) 2005 by Enrico Ros * * * * 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 "annotations.h" #include "annotations_p.h" // qt/kde includes #include #include // DBL_MAX #include // local includes #include "action.h" #include "document.h" #include "document_p.h" #include "movie.h" #include "page_p.h" #include "sound.h" using namespace Okular; /** * True, if point @p c lies to the left of the vector from @p a to @p b * @internal */ static bool isLeftOfVector( const NormalizedPoint& a, const NormalizedPoint& b, const NormalizedPoint& c ) { //cross product return ( (b.x - a.x) * ( c.y - a.y) - ( b.y - a.y ) * ( c.x - a.x ) ) > 0; } /** * @brief Calculates distance of the given point @p x @p y @p xScale @p yScale to the @p path * * Does piecewise comparison and selects the distance to the closest segment */ static double distanceSqr( double x, double y, double xScale, double yScale, const QLinkedList& path ) { double distance = DBL_MAX; double thisDistance; QLinkedList::const_iterator i = path.constBegin(); NormalizedPoint lastPoint = *i; for (++i; i != path.constEnd(); ++i) { thisDistance = NormalizedPoint::distanceSqr( x, y, xScale, yScale, lastPoint, (*i) ); if ( thisDistance < distance ) distance = thisDistance; lastPoint = *i; } return distance; } /** * Given the squared @p distance from the idealized 0-width line and a pen width @p penWidth, * (not squared!), returns the final distance * * @warning The returned distance is not exact: * We calculate an (exact) squared distance to the ideal (centered) line, and then subtract * the squared width of the pen: * a^2 - b^2 where a = "distance from idealized 0-width line" b = "pen width" * For an exact result, we would want to calculate "(a - b)^2" but that would require * a square root operation because we only know the squared distance a^2. * * However, the approximation is feasible, because: * error = (a-b)^2 - (a^2 - b^2) = -2ab + 2b^2 = 2b(b - a) * Therefore: * lim_{a->b} a^2 - b^2 - a^2 + 2ab - b^2 --> 0 * * In other words, this approximation will estimate the distance to be slightly more than it actually is * for as long as we are far "outside" the line, becoming more accurate the closer we get to the line * boundary. Trivially, it also fulfils (a1 < a2) => ((a1^2 - b^2) < (a2^2 - b^2)) making it monotonic. * "Inside" of the drawn line, the distance is 0 anyway. */ static double strokeDistance( double distance, double penWidth ) { return fmax(distance - pow( penWidth, 2 ), 0); } //BEGIN AnnotationUtils implementation Annotation * AnnotationUtils::createAnnotation( const QDomElement & annElement ) { // safety check on annotation element if ( !annElement.hasAttribute( QStringLiteral("type") ) ) return nullptr; // build annotation of given type Annotation * annotation = nullptr; int typeNumber = annElement.attribute( QStringLiteral("type") ).toInt(); switch ( typeNumber ) { case Annotation::AText: annotation = new TextAnnotation( annElement ); break; case Annotation::ALine: annotation = new LineAnnotation( annElement ); break; case Annotation::AGeom: annotation = new GeomAnnotation( annElement ); break; case Annotation::AHighlight: annotation = new HighlightAnnotation( annElement ); break; case Annotation::AStamp: annotation = new StampAnnotation( annElement ); break; case Annotation::AInk: annotation = new InkAnnotation( annElement ); break; case Annotation::ACaret: annotation = new CaretAnnotation( annElement ); break; } // return created annotation return annotation; } void AnnotationUtils::storeAnnotation( const Annotation * ann, QDomElement & annElement, QDomDocument & document ) { // save annotation's type as element's attribute annElement.setAttribute( QStringLiteral("type"), (uint)ann->subType() ); // append all annotation data as children of this node ann->store( annElement, document ); } QDomElement AnnotationUtils::findChildElement( const QDomNode & parentNode, const QString & name ) { // loop through the whole children and return a 'name' named element QDomNode subNode = parentNode.firstChild(); while( subNode.isElement() ) { QDomElement element = subNode.toElement(); if ( element.tagName() == name ) return element; subNode = subNode.nextSibling(); } // if the name can't be found, return a dummy null element return QDomElement(); } QRect AnnotationUtils::annotationGeometry( const Annotation * annotation, double scaleX, double scaleY ) { const QRect rect = annotation->transformedBoundingRectangle().geometry( (int)scaleX, (int)scaleY ); if ( annotation->subType() == Annotation::AText && ( ( (TextAnnotation*)annotation )->textType() == TextAnnotation::Linked ) ) { // To be honest i have no clue of why the 24,24 is here, maybe to make sure it's not too small? // But why only for linked text? const QRect rect24 = QRect( (int)( annotation->transformedBoundingRectangle().left * scaleX ), (int)( annotation->transformedBoundingRectangle().top * scaleY ), 24, 24 ); return rect24.united(rect); } return rect; } //END AnnotationUtils implementation AnnotationProxy::~AnnotationProxy() { } //BEGIN Annotation implementation class Annotation::Style::Private { public: Private() : m_opacity( 1.0 ), m_width( 1.0 ), m_style( Solid ), m_xCorners( 0.0 ), m_yCorners( 0.0 ), m_marks( 3 ), m_spaces( 0 ), m_effect( NoEffect ), m_effectIntensity( 1.0 ) { } QColor m_color; double m_opacity; double m_width; LineStyle m_style; double m_xCorners; double m_yCorners; int m_marks; int m_spaces; LineEffect m_effect; double m_effectIntensity; }; Annotation::Style::Style() : d( new Private ) { } Annotation::Style::~Style() { delete d; } Annotation::Style::Style( const Style &other ) : d( new Private ) { *d = *other.d; } Annotation::Style& Annotation::Style::operator=( const Style &other ) { if ( this != &other ) *d = *other.d; return *this; } void Annotation::Style::setColor( const QColor &color ) { d->m_color = color; } QColor Annotation::Style::color() const { return d->m_color; } void Annotation::Style::setOpacity( double opacity ) { d->m_opacity = opacity; } double Annotation::Style::opacity() const { return d->m_opacity; } void Annotation::Style::setWidth( double width ) { d->m_width = width; } double Annotation::Style::width() const { return d->m_width; } void Annotation::Style::setLineStyle( LineStyle style ) { d->m_style = style; } Annotation::LineStyle Annotation::Style::lineStyle() const { return d->m_style; } void Annotation::Style::setXCorners( double xCorners ) { d->m_xCorners = xCorners; } double Annotation::Style::xCorners() const { return d->m_xCorners; } void Annotation::Style::setYCorners( double yCorners ) { d->m_yCorners = yCorners; } double Annotation::Style::yCorners() const { return d->m_yCorners; } void Annotation::Style::setMarks( int marks ) { d->m_marks = marks; } int Annotation::Style::marks() const { return d->m_marks; } void Annotation::Style::setSpaces( int spaces ) { d->m_spaces = spaces; } int Annotation::Style::spaces() const { return d->m_spaces; } void Annotation::Style::setLineEffect( LineEffect effect ) { d->m_effect = effect; } Annotation::LineEffect Annotation::Style::lineEffect() const { return d->m_effect; } void Annotation::Style::setEffectIntensity( double intensity ) { d->m_effectIntensity = intensity; } double Annotation::Style::effectIntensity() const { return d->m_effectIntensity; } class Annotation::Window::Private { public: Private() : m_flags( -1 ), m_width( 0 ), m_height( 0 ) { } int m_flags; NormalizedPoint m_topLeft; int m_width; int m_height; QString m_title; QString m_summary; }; Annotation::Window::Window() : d( new Private ) { } Annotation::Window::~Window() { delete d; } Annotation::Window::Window( const Window &other ) : d( new Private ) { *d = *other.d; } Annotation::Window& Annotation::Window::operator=( const Window &other ) { if ( this != &other ) *d = *other.d; return *this; } void Annotation::Window::setFlags( int flags ) { d->m_flags = flags; } int Annotation::Window::flags() const { return d->m_flags; } void Annotation::Window::setTopLeft( const NormalizedPoint &point ) { d->m_topLeft = point; } NormalizedPoint Annotation::Window::topLeft() const { return d->m_topLeft; } void Annotation::Window::setWidth( int width ) { d->m_width = width; } int Annotation::Window::width() const { return d->m_width; } void Annotation::Window::setHeight( int height ) { d->m_height = height; } int Annotation::Window::height() const { return d->m_height; } void Annotation::Window::setTitle( const QString &title ) { d->m_title = title; } QString Annotation::Window::title() const { return d->m_title; } void Annotation::Window::setSummary( const QString &summary ) { d->m_summary = summary; } QString Annotation::Window::summary() const { return d->m_summary; } class Annotation::Revision::Private { public: Private() : m_annotation( nullptr ), m_scope( Reply ), m_type( None ) { } Annotation *m_annotation; RevisionScope m_scope; RevisionType m_type; }; Annotation::Revision::Revision() : d( new Private ) { } Annotation::Revision::~Revision() { delete d; } Annotation::Revision::Revision( const Revision &other ) : d( new Private ) { *d = *other.d; } Annotation::Revision& Annotation::Revision::operator=( const Revision &other ) { if ( this != &other ) *d = *other.d; return *this; } void Annotation::Revision::setAnnotation( Annotation *annotation ) { d->m_annotation = annotation; } Annotation *Annotation::Revision::annotation() const { return d->m_annotation; } void Annotation::Revision::setScope( RevisionScope scope ) { d->m_scope = scope; } Annotation::RevisionScope Annotation::Revision::scope() const { return d->m_scope; } void Annotation::Revision::setType( RevisionType type ) { d->m_type = type; } Annotation::RevisionType Annotation::Revision::type() const { return d->m_type; } AnnotationPrivate::AnnotationPrivate() : m_page( nullptr ), m_flags( 0 ), m_disposeFunc( nullptr ) { } AnnotationPrivate::~AnnotationPrivate() { // delete all children revisions if ( m_revisions.isEmpty() ) return; QLinkedList< Annotation::Revision >::iterator it = m_revisions.begin(), end = m_revisions.end(); for ( ; it != end; ++it ) delete (*it).annotation(); } Annotation::Annotation( AnnotationPrivate &dd ) : d_ptr( &dd ) { } Annotation::Annotation( AnnotationPrivate &dd, const QDomNode & description ) : d_ptr( &dd ) { d_ptr->setAnnotationProperties( description ); } Annotation::~Annotation() { if ( d_ptr->m_disposeFunc ) d_ptr->m_disposeFunc( this ); delete d_ptr; } void Annotation::setAuthor( const QString &author ) { Q_D( Annotation ); d->m_author = author; } QString Annotation::author() const { Q_D( const Annotation ); return d->m_author; } void Annotation::setContents( const QString &contents ) { Q_D( Annotation ); d->m_contents = contents; } QString Annotation::contents() const { Q_D( const Annotation ); return d->m_contents; } void Annotation::setUniqueName( const QString &name ) { Q_D( Annotation ); d->m_uniqueName = name; } QString Annotation::uniqueName() const { Q_D( const Annotation ); return d->m_uniqueName; } void Annotation::setModificationDate( const QDateTime &date ) { Q_D( Annotation ); d->m_modifyDate = date; } QDateTime Annotation::modificationDate() const { Q_D( const Annotation ); return d->m_modifyDate; } void Annotation::setCreationDate( const QDateTime &date ) { Q_D( Annotation ); d->m_creationDate = date; } QDateTime Annotation::creationDate() const { Q_D( const Annotation ); return d->m_creationDate; } void Annotation::setFlags( int flags ) { Q_D( Annotation ); d->m_flags = flags; } int Annotation::flags() const { Q_D( const Annotation ); return d->m_flags; } void Annotation::setBoundingRectangle( const NormalizedRect &rectangle ) { Q_D( Annotation ); d->m_boundary = rectangle; d->resetTransformation(); if ( d->m_page ) { d->transform( d->m_page->rotationMatrix() ); } } NormalizedRect Annotation::boundingRectangle() const { Q_D( const Annotation ); return d->m_boundary; } NormalizedRect Annotation::transformedBoundingRectangle() const { Q_D( const Annotation ); return d->m_transformedBoundary; } void Annotation::translate( const NormalizedPoint &coord ) { Q_D( Annotation ); d->translate( coord ); d->resetTransformation(); if ( d->m_page ) { d->transform( d->m_page->rotationMatrix() ); } } void Annotation::adjust( const NormalizedPoint & deltaCoord1, const NormalizedPoint & deltaCoord2 ) { Q_D( Annotation ); d->adjust( deltaCoord1, deltaCoord2 ); d->resetTransformation(); if ( d->m_page ) { d->transform( d->m_page->rotationMatrix() ); } } bool Annotation::openDialogAfterCreation() const { Q_D( const Annotation ); return d->openDialogAfterCreation(); } Annotation::Style & Annotation::style() { Q_D( Annotation ); return d->m_style; } const Annotation::Style & Annotation::style() const { Q_D( const Annotation ); return d->m_style; } Annotation::Window & Annotation::window() { Q_D( Annotation ); return d->m_window; } const Annotation::Window & Annotation::window() const { Q_D( const Annotation ); return d->m_window; } QLinkedList< Annotation::Revision > & Annotation::revisions() { Q_D( Annotation ); return d->m_revisions; } const QLinkedList< Annotation::Revision > & Annotation::revisions() const { Q_D( const Annotation ); return d->m_revisions; } void Annotation::setNativeId( const QVariant &id ) { Q_D( Annotation ); d->m_nativeId = id; } QVariant Annotation::nativeId() const { Q_D( const Annotation ); return d->m_nativeId; } void Annotation::setDisposeDataFunction( DisposeDataFunction func ) { Q_D( Annotation ); d->m_disposeFunc = func; } bool Annotation::canBeMoved() const { Q_D( const Annotation ); // Don't move annotations if they cannot be modified if ( !d->m_page || !d->m_page->m_doc->m_parent->canModifyPageAnnotation(this) ) return false; // highlight "requires" to be "bounded" to text, and that's tricky for now if ( subType() == AHighlight ) return false; return true; } bool Annotation::canBeResized() const { Q_D( const Annotation ); // Don't resize annotations if they cannot be modified if ( !d->m_page || !d->m_page->m_doc->m_parent->canModifyPageAnnotation(this) ) return false; return d->canBeResized(); } void Annotation::store( QDomNode & annNode, QDomDocument & document ) const { Q_D( const Annotation ); // create [base] element of the annotation node QDomElement e = document.createElement( QStringLiteral("base") ); annNode.appendChild( e ); // store -contents- attributes if ( !d->m_author.isEmpty() ) e.setAttribute( QStringLiteral("author"), d->m_author ); if ( !d->m_contents.isEmpty() ) e.setAttribute( QStringLiteral("contents"), d->m_contents ); if ( !d->m_uniqueName.isEmpty() ) e.setAttribute( QStringLiteral("uniqueName"), d->m_uniqueName ); if ( d->m_modifyDate.isValid() ) e.setAttribute( QStringLiteral("modifyDate"), d->m_modifyDate.toString(Qt::ISODate) ); if ( d->m_creationDate.isValid() ) e.setAttribute( QStringLiteral("creationDate"), d->m_creationDate.toString(Qt::ISODate) ); // store -other- attributes if ( d->m_flags ) // Strip internal flags e.setAttribute( QStringLiteral("flags"), d->m_flags & ~(External | ExternallyDrawn | BeingMoved | BeingResized ) ); if ( d->m_style.color().isValid() ) e.setAttribute( QStringLiteral("color"), d->m_style.color().name( QColor::HexArgb ) ); if ( d->m_style.opacity() != 1.0 ) e.setAttribute( QStringLiteral("opacity"), QString::number( d->m_style.opacity() ) ); // Sub-Node-1 - boundary QDomElement bE = document.createElement( QStringLiteral("boundary") ); e.appendChild( bE ); bE.setAttribute( QStringLiteral("l"), QString::number( d->m_boundary.left ) ); bE.setAttribute( QStringLiteral("t"), QString::number( d->m_boundary.top ) ); bE.setAttribute( QStringLiteral("r"), QString::number( d->m_boundary.right ) ); bE.setAttribute( QStringLiteral("b"), QString::number( d->m_boundary.bottom ) ); // Sub-Node-2 - penStyle if ( d->m_style.width() != 1 || d->m_style.lineStyle() != Solid || d->m_style.xCorners() != 0 || d->m_style.yCorners() != 0.0 || d->m_style.marks() != 3 || d->m_style.spaces() != 0 ) { QDomElement psE = document.createElement( QStringLiteral("penStyle") ); e.appendChild( psE ); psE.setAttribute( QStringLiteral("width"), QString::number( d->m_style.width() ) ); psE.setAttribute( QStringLiteral("style"), (int)d->m_style.lineStyle() ); psE.setAttribute( QStringLiteral("xcr"), QString::number( d->m_style.xCorners() ) ); psE.setAttribute( QStringLiteral("ycr"), QString::number( d->m_style.yCorners() ) ); psE.setAttribute( QStringLiteral("marks"), d->m_style.marks() ); psE.setAttribute( QStringLiteral("spaces"), d->m_style.spaces() ); } // Sub-Node-3 - penEffect if ( d->m_style.lineEffect() != NoEffect || d->m_style.effectIntensity() != 1.0 ) { QDomElement peE = document.createElement( QStringLiteral("penEffect") ); e.appendChild( peE ); peE.setAttribute( QStringLiteral("effect"), (int)d->m_style.lineEffect() ); peE.setAttribute( QStringLiteral("intensity"), QString::number( d->m_style.effectIntensity() ) ); } // Sub-Node-4 - window if ( d->m_window.flags() != -1 || !d->m_window.title().isEmpty() || !d->m_window.summary().isEmpty() ) { QDomElement wE = document.createElement( QStringLiteral("window") ); e.appendChild( wE ); wE.setAttribute( QStringLiteral("flags"), d->m_window.flags() ); wE.setAttribute( QStringLiteral("top"), QString::number( d->m_window.topLeft().x ) ); wE.setAttribute( QStringLiteral("left"), QString::number( d->m_window.topLeft().y ) ); wE.setAttribute( QStringLiteral("width"), d->m_window.width() ); wE.setAttribute( QStringLiteral("height"), d->m_window.height() ); wE.setAttribute( QStringLiteral("title"), d->m_window.title() ); wE.setAttribute( QStringLiteral("summary"), d->m_window.summary() ); } // create [revision] element of the annotation node (if any) if ( d->m_revisions.isEmpty() ) return; // add all revisions as children of revisions element QLinkedList< Revision >::const_iterator it = d->m_revisions.begin(), end = d->m_revisions.end(); for ( ; it != end; ++it ) { // create revision element const Revision & revision = *it; QDomElement r = document.createElement( QStringLiteral("revision") ); annNode.appendChild( r ); // set element attributes r.setAttribute( QStringLiteral("revScope"), (int)revision.scope() ); r.setAttribute( QStringLiteral("revType"), (int)revision.type() ); // use revision as the annotation element, so fill it up AnnotationUtils::storeAnnotation( revision.annotation(), r, document ); } } QDomNode Annotation::getAnnotationPropertiesDomNode() const { QDomDocument doc( QStringLiteral("documentInfo") ); QDomElement node = doc.createElement( QStringLiteral("annotation") ); store(node, doc); return node; } void Annotation::setAnnotationProperties( const QDomNode& node ) { // Save off internal properties that aren't contained in node Okular::PagePrivate *p = d_ptr->m_page; QVariant nativeID = d_ptr->m_nativeId; const int internalFlags = d_ptr->m_flags & (External | ExternallyDrawn | BeingMoved | BeingResized ); Annotation::DisposeDataFunction disposeFunc = d_ptr->m_disposeFunc; // Replace AnnotationPrivate object with a fresh copy AnnotationPrivate *new_d_ptr = d_ptr->getNewAnnotationPrivate(); delete( d_ptr ); d_ptr = new_d_ptr; // Set the annotations properties from node d_ptr->setAnnotationProperties(node); // Restore internal properties d_ptr->m_page = p; d_ptr->m_nativeId = nativeID; d_ptr->m_flags = d_ptr->m_flags | internalFlags; d_ptr->m_disposeFunc = disposeFunc; // Transform annotation to current page rotation d_ptr->transform( d_ptr->m_page->rotationMatrix() ); } double AnnotationPrivate::distanceSqr( double x, double y, double xScale, double yScale ) const { return m_transformedBoundary.distanceSqr( x, y, xScale, yScale ); } void AnnotationPrivate::annotationTransform( const QTransform &matrix ) { resetTransformation(); transform( matrix ); } void AnnotationPrivate::transform( const QTransform &matrix ) { m_transformedBoundary.transform( matrix ); } void AnnotationPrivate::baseTransform( const QTransform &matrix ) { m_boundary.transform( matrix ); } void AnnotationPrivate::resetTransformation() { m_transformedBoundary = m_boundary; } void AnnotationPrivate::translate( const NormalizedPoint &coord ) { m_boundary.left = m_boundary.left + coord.x; m_boundary.right = m_boundary.right + coord.x; m_boundary.top = m_boundary.top + coord.y; m_boundary.bottom = m_boundary.bottom + coord.y; } void AnnotationPrivate::adjust( const NormalizedPoint &deltaCoord1, const NormalizedPoint &deltaCoord2 ) { m_boundary.left = m_boundary.left + qBound( -m_boundary.left, deltaCoord1.x, m_boundary.right - m_boundary.left ); m_boundary.top = m_boundary.top + qBound( -m_boundary.top, deltaCoord1.y, m_boundary.bottom - m_boundary.top );; m_boundary.right = m_boundary.right + qBound( m_boundary.left - m_boundary.right, deltaCoord2.x, 1. - m_boundary.right ); m_boundary.bottom = m_boundary.bottom + qBound( m_boundary.top - m_boundary.bottom, deltaCoord2.y, 1. - m_boundary.bottom ); } bool AnnotationPrivate::openDialogAfterCreation() const { return false; } void AnnotationPrivate::setAnnotationProperties( const QDomNode& node ) { // get the [base] element of the annotation node QDomElement e = AnnotationUtils::findChildElement( node, QStringLiteral("base") ); if ( e.isNull() ) return; // parse -contents- attributes if ( e.hasAttribute( QStringLiteral("author") ) ) m_author = e.attribute( QStringLiteral("author") ); if ( e.hasAttribute( QStringLiteral("contents") ) ) m_contents = e.attribute( QStringLiteral("contents") ); if ( e.hasAttribute( QStringLiteral("uniqueName") ) ) m_uniqueName = e.attribute( QStringLiteral("uniqueName") ); if ( e.hasAttribute( QStringLiteral("modifyDate") ) ) m_modifyDate = QDateTime::fromString( e.attribute(QStringLiteral("modifyDate")), Qt::ISODate ); if ( e.hasAttribute( QStringLiteral("creationDate") ) ) m_creationDate = QDateTime::fromString( e.attribute(QStringLiteral("creationDate")), Qt::ISODate ); // parse -other- attributes if ( e.hasAttribute( QStringLiteral("flags") ) ) m_flags = e.attribute( QStringLiteral("flags") ).toInt(); if ( e.hasAttribute( QStringLiteral("color") ) ) m_style.setColor( QColor( e.attribute( QStringLiteral("color") ) ) ); if ( e.hasAttribute( QStringLiteral("opacity") ) ) m_style.setOpacity( e.attribute( QStringLiteral("opacity") ).toDouble() ); // parse -the-subnodes- (describing Style, Window, Revision(s) structures) // Note: all subnodes if present must be 'attributes complete' QDomNode eSubNode = e.firstChild(); while ( eSubNode.isElement() ) { QDomElement ee = eSubNode.toElement(); eSubNode = eSubNode.nextSibling(); // parse boundary if ( ee.tagName() == QLatin1String("boundary") ) { m_boundary=NormalizedRect(ee.attribute( QStringLiteral("l") ).toDouble(), ee.attribute( QStringLiteral("t") ).toDouble(), ee.attribute( QStringLiteral("r") ).toDouble(), ee.attribute( QStringLiteral("b") ).toDouble()); } // parse penStyle if not default else if ( ee.tagName() == QLatin1String("penStyle") ) { m_style.setWidth( ee.attribute( QStringLiteral("width") ).toDouble() ); m_style.setLineStyle( (Annotation::LineStyle)ee.attribute( QStringLiteral("style") ).toInt() ); m_style.setXCorners( ee.attribute( QStringLiteral("xcr") ).toDouble() ); m_style.setYCorners( ee.attribute( QStringLiteral("ycr") ).toDouble() ); m_style.setMarks( ee.attribute( QStringLiteral("marks") ).toInt() ); m_style.setSpaces( ee.attribute( QStringLiteral("spaces") ).toInt() ); } // parse effectStyle if not default else if ( ee.tagName() == QLatin1String("penEffect") ) { m_style.setLineEffect( (Annotation::LineEffect)ee.attribute( QStringLiteral("effect") ).toInt() ); m_style.setEffectIntensity( ee.attribute( QStringLiteral("intensity") ).toDouble() ); } // parse window if present else if ( ee.tagName() == QLatin1String("window") ) { m_window.setFlags( ee.attribute( QStringLiteral("flags") ).toInt() ); m_window.setTopLeft( NormalizedPoint( ee.attribute( QStringLiteral("top") ).toDouble(), ee.attribute( QStringLiteral("left") ).toDouble() ) ); m_window.setWidth( ee.attribute( QStringLiteral("width") ).toInt() ); m_window.setHeight( ee.attribute( QStringLiteral("height") ).toInt() ); m_window.setTitle( ee.attribute( QStringLiteral("title") ) ); m_window.setSummary( ee.attribute( QStringLiteral("summary") ) ); } } // get the [revisions] element of the annotation node QDomNode revNode = node.firstChild(); for ( ; revNode.isElement(); revNode = revNode.nextSibling() ) { QDomElement revElement = revNode.toElement(); if ( revElement.tagName() != QLatin1String("revision") ) continue; // compile the Revision structure crating annotation Annotation::Revision revision; revision.setScope( (Annotation::RevisionScope)revElement.attribute( QStringLiteral("revScope") ).toInt() ); revision.setType( (Annotation::RevisionType)revElement.attribute( QStringLiteral("revType") ).toInt() ); revision.setAnnotation( AnnotationUtils::createAnnotation( revElement ) ); // if annotation is valid, add revision to internal list if ( revision.annotation() ) m_revisions.append( revision ); } m_transformedBoundary = m_boundary; } bool AnnotationPrivate::canBeResized() const { return false; } //END Annotation implementation /** TextAnnotation [Annotation] */ class Okular::TextAnnotationPrivate : public Okular::AnnotationPrivate { public: TextAnnotationPrivate() : AnnotationPrivate(), m_textType( TextAnnotation::Linked ), m_textIcon( QStringLiteral("Comment") ), m_inplaceAlign( 0 ), m_inplaceIntent( TextAnnotation::Unknown ) { } void transform( const QTransform &matrix ) override; void baseTransform( const QTransform &matrix ) override; void resetTransformation() override; void translate( const NormalizedPoint &coord ) override; bool openDialogAfterCreation() const override; void setAnnotationProperties( const QDomNode& node ) override; bool canBeResized() const override; AnnotationPrivate* getNewAnnotationPrivate() override; TextAnnotation::TextType m_textType; QString m_textIcon; QFont m_textFont; QColor m_textColor; int m_inplaceAlign; NormalizedPoint m_inplaceCallout[3]; NormalizedPoint m_transformedInplaceCallout[3]; TextAnnotation::InplaceIntent m_inplaceIntent; }; /* The default textIcon for text annotation is Note as the PDF Reference says */ TextAnnotation::TextAnnotation() : Annotation( *new TextAnnotationPrivate() ) { } TextAnnotation::TextAnnotation( const QDomNode & description ) : Annotation( *new TextAnnotationPrivate(), description ) { } TextAnnotation::~TextAnnotation() { } void TextAnnotation::setTextType( TextType textType ) { Q_D( TextAnnotation ); d->m_textType = textType; } TextAnnotation::TextType TextAnnotation::textType() const { Q_D( const TextAnnotation ); return d->m_textType; } void TextAnnotation::setTextIcon( const QString &icon ) { Q_D( TextAnnotation ); d->m_textIcon = icon; } QString TextAnnotation::textIcon() const { Q_D( const TextAnnotation ); return d->m_textIcon; } void TextAnnotation::setTextFont( const QFont &font ) { Q_D( TextAnnotation ); d->m_textFont = font; } QFont TextAnnotation::textFont() const { Q_D( const TextAnnotation ); return d->m_textFont; } void TextAnnotation::setTextColor( const QColor &color ) { Q_D( TextAnnotation ); d->m_textColor = color; } QColor TextAnnotation::textColor() const { Q_D( const TextAnnotation ); return d->m_textColor; } void TextAnnotation::setInplaceAlignment( int alignment ) { Q_D( TextAnnotation ); d->m_inplaceAlign = alignment; } int TextAnnotation::inplaceAlignment() const { Q_D( const TextAnnotation ); return d->m_inplaceAlign; } void TextAnnotation::setInplaceCallout( const NormalizedPoint &point, int index ) { if ( index < 0 || index > 2 ) return; Q_D( TextAnnotation ); d->m_inplaceCallout[ index ] = point; } NormalizedPoint TextAnnotation::inplaceCallout( int index ) const { if ( index < 0 || index > 2 ) return NormalizedPoint(); Q_D( const TextAnnotation ); return d->m_inplaceCallout[ index ]; } NormalizedPoint TextAnnotation::transformedInplaceCallout( int index ) const { if ( index < 0 || index > 2 ) return NormalizedPoint(); Q_D( const TextAnnotation ); return d->m_transformedInplaceCallout[ index ]; } void TextAnnotation::setInplaceIntent( InplaceIntent intent ) { Q_D( TextAnnotation ); d->m_inplaceIntent = intent; } TextAnnotation::InplaceIntent TextAnnotation::inplaceIntent() const { Q_D( const TextAnnotation ); return d->m_inplaceIntent; } Annotation::SubType TextAnnotation::subType() const { return AText; } void TextAnnotation::store( QDomNode & node, QDomDocument & document ) const { Q_D( const TextAnnotation ); // recurse to parent objects storing properties Annotation::store( node, document ); // create [text] element QDomElement textElement = document.createElement( QStringLiteral("text") ); node.appendChild( textElement ); // store the optional attributes if ( d->m_textType != Linked ) textElement.setAttribute( QStringLiteral("type"), (int)d->m_textType ); if ( !d->m_textIcon.isEmpty() ) textElement.setAttribute( QStringLiteral("icon"), d->m_textIcon ); if ( d->m_textFont != QApplication::font() ) textElement.setAttribute( QStringLiteral("font"), d->m_textFont.toString() ); if ( d->m_textColor.isValid() ) textElement.setAttribute( QStringLiteral("fontColor"), d->m_textColor.name() ); if ( d->m_inplaceAlign ) textElement.setAttribute( QStringLiteral("align"), d->m_inplaceAlign ); if ( d->m_inplaceIntent != Unknown ) textElement.setAttribute( QStringLiteral("intent"), (int)d->m_inplaceIntent ); // Sub-Node - callout if ( d->m_inplaceCallout[0].x != 0.0 ) { QDomElement calloutElement = document.createElement( QStringLiteral("callout") ); textElement.appendChild( calloutElement ); calloutElement.setAttribute( QStringLiteral("ax"), QString::number( d->m_inplaceCallout[0].x ) ); calloutElement.setAttribute( QStringLiteral("ay"), QString::number( d->m_inplaceCallout[0].y ) ); calloutElement.setAttribute( QStringLiteral("bx"), QString::number( d->m_inplaceCallout[1].x ) ); calloutElement.setAttribute( QStringLiteral("by"), QString::number( d->m_inplaceCallout[1].y ) ); calloutElement.setAttribute( QStringLiteral("cx"), QString::number( d->m_inplaceCallout[2].x ) ); calloutElement.setAttribute( QStringLiteral("cy"), QString::number( d->m_inplaceCallout[2].y ) ); } } void TextAnnotationPrivate::transform( const QTransform &matrix ) { AnnotationPrivate::transform( matrix ); - for ( int i = 0; i < 3; ++i ) { - m_transformedInplaceCallout[i].transform( matrix ); + for ( NormalizedPoint &np : m_transformedInplaceCallout ) { + np.transform( matrix ); } } void TextAnnotationPrivate::baseTransform( const QTransform &matrix ) { AnnotationPrivate::baseTransform( matrix ); - for ( int i = 0; i < 3; ++i ) { - m_inplaceCallout[i].transform( matrix ); + for ( NormalizedPoint &np : m_inplaceCallout ) { + np.transform( matrix ); } } void TextAnnotationPrivate::resetTransformation() { AnnotationPrivate::resetTransformation(); for ( int i = 0; i < 3; ++i ) { m_transformedInplaceCallout[i] = m_inplaceCallout[i]; } } void TextAnnotationPrivate::translate( const NormalizedPoint &coord ) { AnnotationPrivate::translate( coord ); #define ADD_COORD( c1, c2 ) \ { \ c1.x = c1.x + c2.x; \ c1.y = c1.y + c2.y; \ } ADD_COORD( m_inplaceCallout[0], coord ) ADD_COORD( m_inplaceCallout[1], coord ) ADD_COORD( m_inplaceCallout[2], coord ) #undef ADD_COORD } bool TextAnnotationPrivate::openDialogAfterCreation() const { return ( m_textType == Okular::TextAnnotation::Linked ); } void TextAnnotationPrivate::setAnnotationProperties( const QDomNode& node ) { Okular::AnnotationPrivate::setAnnotationProperties(node); // loop through the whole children looking for a 'text' element QDomNode subNode = node.firstChild(); while( subNode.isElement() ) { QDomElement e = subNode.toElement(); subNode = subNode.nextSibling(); if ( e.tagName() != QLatin1String("text") ) continue; // parse the attributes if ( e.hasAttribute( QStringLiteral("type") ) ) m_textType = (TextAnnotation::TextType)e.attribute( QStringLiteral("type") ).toInt(); if ( e.hasAttribute( QStringLiteral("icon") ) ) m_textIcon = e.attribute( QStringLiteral("icon") ); if ( e.hasAttribute( QStringLiteral("font") ) ) m_textFont.fromString( e.attribute( QStringLiteral("font") ) ); if ( e.hasAttribute( QStringLiteral("fontColor") ) ) m_textColor = QColor( e.attribute( QStringLiteral("fontColor") ) ); if ( e.hasAttribute( QStringLiteral("align") ) ) m_inplaceAlign = e.attribute( QStringLiteral("align") ).toInt(); if ( e.hasAttribute( QStringLiteral("intent") ) ) m_inplaceIntent = (TextAnnotation::InplaceIntent)e.attribute( QStringLiteral("intent") ).toInt(); // parse the subnodes QDomNode eSubNode = e.firstChild(); while ( eSubNode.isElement() ) { QDomElement ee = eSubNode.toElement(); eSubNode = eSubNode.nextSibling(); if ( ee.tagName() == QLatin1String("escapedText") ) { m_contents = ee.firstChild().toCDATASection().data(); } else if ( ee.tagName() == QLatin1String("callout") ) { m_inplaceCallout[0].x = ee.attribute( QStringLiteral("ax") ).toDouble(); m_inplaceCallout[0].y = ee.attribute( QStringLiteral("ay") ).toDouble(); m_inplaceCallout[1].x = ee.attribute( QStringLiteral("bx") ).toDouble(); m_inplaceCallout[1].y = ee.attribute( QStringLiteral("by") ).toDouble(); m_inplaceCallout[2].x = ee.attribute( QStringLiteral("cx") ).toDouble(); m_inplaceCallout[2].y = ee.attribute( QStringLiteral("cy") ).toDouble(); } } // loading complete break; } for ( int i = 0; i < 3; ++i ) m_transformedInplaceCallout[i] = m_inplaceCallout[i]; } bool TextAnnotationPrivate::canBeResized() const { if ( m_textType != TextAnnotation::Linked ) { return true; } return false; } AnnotationPrivate* TextAnnotationPrivate::getNewAnnotationPrivate() { return new TextAnnotationPrivate(); } /** LineAnnotation [Annotation] */ class Okular::LineAnnotationPrivate : public Okular::AnnotationPrivate { public: LineAnnotationPrivate() : AnnotationPrivate(), m_lineStartStyle( LineAnnotation::None ), m_lineEndStyle( LineAnnotation::None ), m_lineClosed( false ), m_lineShowCaption( false ), m_lineLeadingFwdPt( 0 ), m_lineLeadingBackPt( 0 ), m_lineIntent( LineAnnotation::Unknown ) { } void transform( const QTransform &matrix ) override; void baseTransform( const QTransform &matrix ) override; void resetTransformation() override; void translate( const NormalizedPoint &coord ) override; double distanceSqr( double x, double y, double xScale, double yScale ) const override; void setAnnotationProperties( const QDomNode& node ) override; AnnotationPrivate* getNewAnnotationPrivate() override; QLinkedList m_linePoints; QLinkedList m_transformedLinePoints; LineAnnotation::TermStyle m_lineStartStyle; LineAnnotation::TermStyle m_lineEndStyle; bool m_lineClosed : 1; bool m_lineShowCaption : 1; QColor m_lineInnerColor; double m_lineLeadingFwdPt; double m_lineLeadingBackPt; LineAnnotation::LineIntent m_lineIntent; }; LineAnnotation::LineAnnotation() : Annotation( *new LineAnnotationPrivate() ) { } LineAnnotation::LineAnnotation( const QDomNode & description ) : Annotation( *new LineAnnotationPrivate(), description ) { } LineAnnotation::~LineAnnotation() { } void LineAnnotation::setLinePoints( const QLinkedList &points ) { Q_D( LineAnnotation ); d->m_linePoints = points; } QLinkedList LineAnnotation::linePoints() const { Q_D( const LineAnnotation ); return d->m_linePoints; } QLinkedList LineAnnotation::transformedLinePoints() const { Q_D( const LineAnnotation ); return d->m_transformedLinePoints; } void LineAnnotation::setLineStartStyle( TermStyle style ) { Q_D( LineAnnotation ); d->m_lineStartStyle = style; } LineAnnotation::TermStyle LineAnnotation::lineStartStyle() const { Q_D( const LineAnnotation ); return d->m_lineStartStyle; } void LineAnnotation::setLineEndStyle( TermStyle style ) { Q_D( LineAnnotation ); d->m_lineEndStyle = style; } LineAnnotation::TermStyle LineAnnotation::lineEndStyle() const { Q_D( const LineAnnotation ); return d->m_lineEndStyle; } void LineAnnotation::setLineClosed( bool closed ) { Q_D( LineAnnotation ); d->m_lineClosed = closed; } bool LineAnnotation::lineClosed() const { Q_D( const LineAnnotation ); return d->m_lineClosed; } void LineAnnotation::setLineInnerColor( const QColor &color ) { Q_D( LineAnnotation ); d->m_lineInnerColor = color; } QColor LineAnnotation::lineInnerColor() const { Q_D( const LineAnnotation ); return d->m_lineInnerColor; } void LineAnnotation::setLineLeadingForwardPoint( double point ) { Q_D( LineAnnotation ); d->m_lineLeadingFwdPt = point; } double LineAnnotation::lineLeadingForwardPoint() const { Q_D( const LineAnnotation ); return d->m_lineLeadingFwdPt; } void LineAnnotation::setLineLeadingBackwardPoint( double point ) { Q_D( LineAnnotation ); d->m_lineLeadingBackPt = point; } double LineAnnotation::lineLeadingBackwardPoint() const { Q_D( const LineAnnotation ); return d->m_lineLeadingBackPt; } void LineAnnotation::setShowCaption( bool show ) { Q_D( LineAnnotation ); d->m_lineShowCaption = show; } bool LineAnnotation::showCaption() const { Q_D( const LineAnnotation ); return d->m_lineShowCaption; } void LineAnnotation::setLineIntent( LineIntent intent ) { Q_D( LineAnnotation ); d->m_lineIntent = intent; } LineAnnotation::LineIntent LineAnnotation::lineIntent() const { Q_D( const LineAnnotation ); return d->m_lineIntent; } Annotation::SubType LineAnnotation::subType() const { return ALine; } void LineAnnotation::store( QDomNode & node, QDomDocument & document ) const { Q_D( const LineAnnotation ); // recurse to parent objects storing properties Annotation::store( node, document ); // create [line] element QDomElement lineElement = document.createElement( QStringLiteral("line") ); node.appendChild( lineElement ); // store the attributes if ( d->m_lineStartStyle != None ) lineElement.setAttribute( QStringLiteral("startStyle"), (int)d->m_lineStartStyle ); if ( d->m_lineEndStyle != None ) lineElement.setAttribute( QStringLiteral("endStyle"), (int)d->m_lineEndStyle ); if ( d->m_lineClosed ) lineElement.setAttribute( QStringLiteral("closed"), d->m_lineClosed ); if ( d->m_lineInnerColor.isValid() ) lineElement.setAttribute( QStringLiteral("innerColor"), d->m_lineInnerColor.name() ); if ( d->m_lineLeadingFwdPt != 0.0 ) lineElement.setAttribute( QStringLiteral("leadFwd"), QString::number( d->m_lineLeadingFwdPt ) ); if ( d->m_lineLeadingBackPt != 0.0 ) lineElement.setAttribute( QStringLiteral("leadBack"), QString::number( d->m_lineLeadingBackPt ) ); if ( d->m_lineShowCaption ) lineElement.setAttribute( QStringLiteral("showCaption"), d->m_lineShowCaption ); if ( d->m_lineIntent != Unknown ) lineElement.setAttribute( QStringLiteral("intent"), d->m_lineIntent ); // append the list of points int points = d->m_linePoints.count(); if ( points > 1 ) { QLinkedList::const_iterator it = d->m_linePoints.begin(), end = d->m_linePoints.end(); while ( it != end ) { const NormalizedPoint & p = *it; QDomElement pElement = document.createElement( QStringLiteral("point") ); lineElement.appendChild( pElement ); pElement.setAttribute( QStringLiteral("x"), QString::number( p.x ) ); pElement.setAttribute( QStringLiteral("y"), QString::number( p.y ) ); it++; //to avoid loop } } } void LineAnnotationPrivate::transform( const QTransform &matrix ) { AnnotationPrivate::transform( matrix ); QMutableLinkedListIterator it( m_transformedLinePoints ); while ( it.hasNext() ) it.next().transform( matrix ); } void LineAnnotationPrivate::baseTransform( const QTransform &matrix ) { AnnotationPrivate::baseTransform( matrix ); QMutableLinkedListIterator it( m_linePoints ); while ( it.hasNext() ) it.next().transform( matrix ); } void LineAnnotationPrivate::resetTransformation() { AnnotationPrivate::resetTransformation(); m_transformedLinePoints = m_linePoints; } void LineAnnotationPrivate::translate( const NormalizedPoint &coord ) { AnnotationPrivate::translate( coord ); QMutableLinkedListIterator it( m_linePoints ); while ( it.hasNext() ) { NormalizedPoint& p = it.next(); p.x = p.x + coord.x; p.y = p.y + coord.y; } } void LineAnnotationPrivate::setAnnotationProperties( const QDomNode& node ) { Okular::AnnotationPrivate::setAnnotationProperties(node); // loop through the whole children looking for a 'line' element QDomNode subNode = node.firstChild(); while( subNode.isElement() ) { QDomElement e = subNode.toElement(); subNode = subNode.nextSibling(); if ( e.tagName() != QLatin1String("line") ) continue; // parse the attributes if ( e.hasAttribute( QStringLiteral("startStyle") ) ) m_lineStartStyle = (LineAnnotation::TermStyle)e.attribute( QStringLiteral("startStyle") ).toInt(); if ( e.hasAttribute( QStringLiteral("endStyle") ) ) m_lineEndStyle = (LineAnnotation::TermStyle)e.attribute( QStringLiteral("endStyle") ).toInt(); if ( e.hasAttribute( QStringLiteral("closed") ) ) m_lineClosed = e.attribute( QStringLiteral("closed") ).toInt(); if ( e.hasAttribute( QStringLiteral("innerColor") ) ) m_lineInnerColor = QColor( e.attribute( QStringLiteral("innerColor") ) ); if ( e.hasAttribute( QStringLiteral("leadFwd") ) ) m_lineLeadingFwdPt = e.attribute( QStringLiteral("leadFwd") ).toDouble(); if ( e.hasAttribute( QStringLiteral("leadBack") ) ) m_lineLeadingBackPt = e.attribute( QStringLiteral("leadBack") ).toDouble(); if ( e.hasAttribute( QStringLiteral("showCaption") ) ) m_lineShowCaption = e.attribute( QStringLiteral("showCaption") ).toInt(); if ( e.hasAttribute( QStringLiteral("intent") ) ) m_lineIntent = (LineAnnotation::LineIntent)e.attribute( QStringLiteral("intent") ).toInt(); // parse all 'point' subnodes QDomNode pointNode = e.firstChild(); while ( pointNode.isElement() ) { QDomElement pe = pointNode.toElement(); pointNode = pointNode.nextSibling(); if ( pe.tagName() != QLatin1String("point") ) continue; NormalizedPoint p; p.x = pe.attribute( QStringLiteral("x"), QStringLiteral("0.0") ).toDouble(); p.y = pe.attribute( QStringLiteral("y"), QStringLiteral("0.0") ).toDouble(); m_linePoints.append( p ); } // loading complete break; } m_transformedLinePoints = m_linePoints; } AnnotationPrivate* LineAnnotationPrivate::getNewAnnotationPrivate() { return new LineAnnotationPrivate(); } double LineAnnotationPrivate::distanceSqr( double x, double y, double xScale, double yScale ) const { QLinkedList transformedLinePoints = m_transformedLinePoints; if ( m_lineClosed ) // Close the path transformedLinePoints.append( transformedLinePoints.first() ); if ( m_lineInnerColor.isValid() ) { QPolygonF polygon; for ( const NormalizedPoint &p : qAsConst(transformedLinePoints) ) polygon.append( QPointF( p.x, p.y ) ); if ( polygon.containsPoint( QPointF( x, y ), Qt::WindingFill ) ) return 0; } return strokeDistance( ::distanceSqr( x, y, xScale, yScale, transformedLinePoints ), m_style.width() * xScale / ( m_page->m_width * 2 ) ); } /** GeomAnnotation [Annotation] */ class Okular::GeomAnnotationPrivate : public Okular::AnnotationPrivate { public: GeomAnnotationPrivate() : AnnotationPrivate(), m_geomType( GeomAnnotation::InscribedSquare ) { } void setAnnotationProperties( const QDomNode& node ) override; bool canBeResized() const override; AnnotationPrivate* getNewAnnotationPrivate() override; double distanceSqr( double x, double y, double xScale, double yScale ) const override; GeomAnnotation::GeomType m_geomType; QColor m_geomInnerColor; }; GeomAnnotation::GeomAnnotation() : Annotation( *new GeomAnnotationPrivate() ) { } GeomAnnotation::GeomAnnotation( const QDomNode & description ) : Annotation( *new GeomAnnotationPrivate(), description ) { } GeomAnnotation::~GeomAnnotation() { } void GeomAnnotation::setGeometricalType( GeomType type ) { Q_D( GeomAnnotation ); d->m_geomType = type; } GeomAnnotation::GeomType GeomAnnotation::geometricalType() const { Q_D( const GeomAnnotation ); return d->m_geomType; } void GeomAnnotation::setGeometricalInnerColor( const QColor &color ) { Q_D( GeomAnnotation ); d->m_geomInnerColor = color; } QColor GeomAnnotation::geometricalInnerColor() const { Q_D( const GeomAnnotation ); return d->m_geomInnerColor; } Annotation::SubType GeomAnnotation::subType() const { return AGeom; } void GeomAnnotation::store( QDomNode & node, QDomDocument & document ) const { Q_D( const GeomAnnotation ); // recurse to parent objects storing properties Annotation::store( node, document ); // create [geom] element QDomElement geomElement = document.createElement( QStringLiteral("geom") ); node.appendChild( geomElement ); // append the optional attributes if ( d->m_geomType != InscribedSquare ) geomElement.setAttribute( QStringLiteral("type"), (int)d->m_geomType ); if ( d->m_geomInnerColor.isValid() ) geomElement.setAttribute( QStringLiteral("color"), d->m_geomInnerColor.name() ); } void GeomAnnotationPrivate::setAnnotationProperties( const QDomNode& node ) { Okular::AnnotationPrivate::setAnnotationProperties(node); // loop through the whole children looking for a 'geom' element QDomNode subNode = node.firstChild(); while( subNode.isElement() ) { QDomElement e = subNode.toElement(); subNode = subNode.nextSibling(); if ( e.tagName() != QLatin1String("geom") ) continue; // parse the attributes if ( e.hasAttribute( QStringLiteral("type") ) ) m_geomType = (GeomAnnotation::GeomType)e.attribute( QStringLiteral("type") ).toInt(); if ( e.hasAttribute( QStringLiteral("color") ) ) m_geomInnerColor = QColor( e.attribute( QStringLiteral("color") ) ); // compatibility if ( e.hasAttribute( QStringLiteral("width") ) ) m_style.setWidth( e.attribute( QStringLiteral("width") ).toInt() ); // loading complete break; } } bool GeomAnnotationPrivate::canBeResized() const { return true; } AnnotationPrivate* GeomAnnotationPrivate::getNewAnnotationPrivate() { return new GeomAnnotationPrivate(); } double GeomAnnotationPrivate::distanceSqr( double x, double y, double xScale, double yScale ) const { double distance = 0; //the line thickness is applied unevenly (only on the "inside") - account for this bool withinShape = false; switch (m_geomType) { case GeomAnnotation::InscribedCircle: { //calculate the center point and focus lengths of the ellipse const double centerX = ( m_transformedBoundary.left + m_transformedBoundary.right ) / 2.0; const double centerY = ( m_transformedBoundary.top + m_transformedBoundary.bottom ) / 2.0; const double focusX = ( m_transformedBoundary.right - centerX); const double focusY = ( m_transformedBoundary.bottom - centerY); const double focusXSqr = pow( focusX, 2 ); const double focusYSqr = pow( focusY, 2 ); // to calculate the distance from the ellipse, we will first find the point "projection" // that lies on the ellipse and is closest to the point (x,y) // This point can obviously be written as "center + lambda(inputPoint - center)". // Because the point lies on the ellipse, we know that: // 1 = ((center.x - projection.x)/focusX)^2 + ((center.y - projection.y)/focusY)^2 // After filling in projection.x = center.x + lambda * (inputPoint.x - center.x) // and its y-equivalent, we can solve for lambda: const double lambda = sqrt( focusXSqr * focusYSqr / ( focusYSqr * pow( x - centerX, 2 ) + focusXSqr * pow( y - centerY, 2 ) ) ); // if the ellipse is filled, we treat all points within as "on" it if ( lambda > 1 ) { if ( m_geomInnerColor.isValid() ) return 0; else withinShape = true; } //otherwise we calculate the squared distance from the projected point on the ellipse NormalizedPoint projection( centerX, centerY ); projection.x += lambda * ( x - centerX ); projection.y += lambda * ( y - centerY ); distance = projection.distanceSqr( x, y, xScale, yScale ); break; } case GeomAnnotation::InscribedSquare: //if the square is filled, only check the bounding box if ( m_geomInnerColor.isValid() ) return AnnotationPrivate::distanceSqr( x, y, xScale, yScale ); QLinkedList edges; edges << NormalizedPoint( m_transformedBoundary.left, m_transformedBoundary.top ); edges << NormalizedPoint( m_transformedBoundary.right, m_transformedBoundary.top ); edges << NormalizedPoint( m_transformedBoundary.right, m_transformedBoundary.bottom ); edges << NormalizedPoint( m_transformedBoundary.left, m_transformedBoundary.bottom ); edges << NormalizedPoint( m_transformedBoundary.left, m_transformedBoundary.top ); distance = ::distanceSqr( x, y, xScale, yScale, edges ); if ( m_transformedBoundary.contains( x, y ) ) withinShape = true; break; } if ( withinShape ) distance = strokeDistance( distance, m_style.width() * xScale / m_page->m_width ); return distance; } /** HighlightAnnotation [Annotation] */ class HighlightAnnotation::Quad::Private { public: Private() { } NormalizedPoint m_points[4]; NormalizedPoint m_transformedPoints[4]; bool m_capStart : 1; bool m_capEnd : 1; double m_feather; }; HighlightAnnotation::Quad::Quad() : d( new Private ) { } HighlightAnnotation::Quad::~Quad() { delete d; } HighlightAnnotation::Quad::Quad( const Quad &other ) : d( new Private ) { *d = *other.d; } HighlightAnnotation::Quad& HighlightAnnotation::Quad::operator=( const Quad &other ) { if ( this != &other ) *d = *other.d; return *this; } void HighlightAnnotation::Quad::setPoint( const NormalizedPoint &point, int index ) { if ( index < 0 || index > 3 ) return; d->m_points[ index ] = point; } NormalizedPoint HighlightAnnotation::Quad::point( int index ) const { if ( index < 0 || index > 3 ) return NormalizedPoint(); return d->m_points[ index ]; } NormalizedPoint HighlightAnnotation::Quad::transformedPoint( int index ) const { if ( index < 0 || index > 3 ) return NormalizedPoint(); return d->m_transformedPoints[ index ]; } void HighlightAnnotation::Quad::setCapStart( bool value ) { d->m_capStart = value; } bool HighlightAnnotation::Quad::capStart() const { return d->m_capStart; } void HighlightAnnotation::Quad::setCapEnd( bool value ) { d->m_capEnd = value; } bool HighlightAnnotation::Quad::capEnd() const { return d->m_capEnd; } void HighlightAnnotation::Quad::setFeather( double width ) { d->m_feather = width; } double HighlightAnnotation::Quad::feather() const { return d->m_feather; } void HighlightAnnotation::Quad::transform( const QTransform &matrix ) { for ( int i = 0; i < 4; ++i ) { d->m_transformedPoints[ i ] = d->m_points[ i ]; d->m_transformedPoints[ i ].transform( matrix ); } } class Okular::HighlightAnnotationPrivate : public Okular::AnnotationPrivate { public: HighlightAnnotationPrivate() : AnnotationPrivate(), m_highlightType( HighlightAnnotation::Highlight ) { } void transform( const QTransform &matrix ) override; void baseTransform( const QTransform &matrix ) override; double distanceSqr( double x, double y, double xScale, double yScale ) const override; void setAnnotationProperties( const QDomNode& node ) override; AnnotationPrivate* getNewAnnotationPrivate() override; HighlightAnnotation::HighlightType m_highlightType; QList< HighlightAnnotation::Quad > m_highlightQuads; }; HighlightAnnotation::HighlightAnnotation() : Annotation( *new HighlightAnnotationPrivate() ) { } HighlightAnnotation::HighlightAnnotation( const QDomNode & description ) : Annotation( *new HighlightAnnotationPrivate(), description ) { } HighlightAnnotation::~HighlightAnnotation() { } void HighlightAnnotation::setHighlightType( HighlightType type ) { Q_D( HighlightAnnotation ); d->m_highlightType = type; } HighlightAnnotation::HighlightType HighlightAnnotation::highlightType() const { Q_D( const HighlightAnnotation ); return d->m_highlightType; } QList< HighlightAnnotation::Quad > & HighlightAnnotation::highlightQuads() { Q_D( HighlightAnnotation ); return d->m_highlightQuads; } void HighlightAnnotation::store( QDomNode & node, QDomDocument & document ) const { Q_D( const HighlightAnnotation ); // recurse to parent objects storing properties Annotation::store( node, document ); // create [hl] element QDomElement hlElement = document.createElement( QStringLiteral("hl") ); node.appendChild( hlElement ); // append the optional attributes if ( d->m_highlightType != Highlight ) hlElement.setAttribute( QStringLiteral("type"), (int)d->m_highlightType ); if ( d->m_highlightQuads.count() < 1 ) return; // append highlight quads, all children describe quads QList< Quad >::const_iterator it = d->m_highlightQuads.begin(), end = d->m_highlightQuads.end(); for ( ; it != end; ++it ) { QDomElement quadElement = document.createElement( QStringLiteral("quad") ); hlElement.appendChild( quadElement ); const Quad & q = *it; quadElement.setAttribute( QStringLiteral("ax"), QString::number( q.point( 0 ).x ) ); quadElement.setAttribute( QStringLiteral("ay"), QString::number( q.point( 0 ).y ) ); quadElement.setAttribute( QStringLiteral("bx"), QString::number( q.point( 1 ).x ) ); quadElement.setAttribute( QStringLiteral("by"), QString::number( q.point( 1 ).y ) ); quadElement.setAttribute( QStringLiteral("cx"), QString::number( q.point( 2 ).x ) ); quadElement.setAttribute( QStringLiteral("cy"), QString::number( q.point( 2 ).y ) ); quadElement.setAttribute( QStringLiteral("dx"), QString::number( q.point( 3 ).x ) ); quadElement.setAttribute( QStringLiteral("dy"), QString::number( q.point( 3 ).y ) ); if ( q.capStart() ) quadElement.setAttribute( QStringLiteral("start"), 1 ); if ( q.capEnd() ) quadElement.setAttribute( QStringLiteral("end"), 1 ); quadElement.setAttribute( QStringLiteral("feather"), QString::number( q.feather() ) ); } } Annotation::SubType HighlightAnnotation::subType() const { return AHighlight; } void HighlightAnnotationPrivate::transform( const QTransform &matrix ) { AnnotationPrivate::transform( matrix ); QMutableListIterator it( m_highlightQuads ); while ( it.hasNext() ) it.next().transform( matrix ); } void HighlightAnnotationPrivate::baseTransform( const QTransform &matrix ) { AnnotationPrivate::baseTransform( matrix ); QMutableListIterator it( m_highlightQuads ); while ( it.hasNext() ) it.next().transform( matrix ); } void HighlightAnnotationPrivate::setAnnotationProperties( const QDomNode& node ) { Okular::AnnotationPrivate::setAnnotationProperties(node); m_highlightQuads.clear(); // loop through the whole children looking for a 'hl' element QDomNode subNode = node.firstChild(); while( subNode.isElement() ) { QDomElement e = subNode.toElement(); subNode = subNode.nextSibling(); if ( e.tagName() != QLatin1String("hl") ) continue; // parse the attributes if ( e.hasAttribute( QStringLiteral("type") ) ) m_highlightType = (HighlightAnnotation::HighlightType)e.attribute( QStringLiteral("type") ).toInt(); // parse all 'quad' subnodes QDomNode quadNode = e.firstChild(); for ( ; quadNode.isElement(); quadNode = quadNode.nextSibling() ) { QDomElement qe = quadNode.toElement(); if ( qe.tagName() != QLatin1String("quad") ) continue; HighlightAnnotation::Quad q; q.setPoint( NormalizedPoint( qe.attribute( QStringLiteral("ax"), QStringLiteral("0.0") ).toDouble(), qe.attribute( QStringLiteral("ay"), QStringLiteral("0.0") ).toDouble() ), 0 ); q.setPoint( NormalizedPoint( qe.attribute( QStringLiteral("bx"), QStringLiteral("0.0") ).toDouble(), qe.attribute( QStringLiteral("by"), QStringLiteral("0.0") ).toDouble() ), 1 ); q.setPoint( NormalizedPoint( qe.attribute( QStringLiteral("cx"), QStringLiteral("0.0") ).toDouble(), qe.attribute( QStringLiteral("cy"), QStringLiteral("0.0") ).toDouble() ), 2 ); q.setPoint( NormalizedPoint( qe.attribute( QStringLiteral("dx"), QStringLiteral("0.0") ).toDouble(), qe.attribute( QStringLiteral("dy"), QStringLiteral("0.0") ).toDouble() ), 3 ); q.setCapStart( qe.hasAttribute( QStringLiteral("start") ) ); q.setCapEnd( qe.hasAttribute( QStringLiteral("end") ) ); q.setFeather( qe.attribute( QStringLiteral("feather"), QStringLiteral("0.1") ).toDouble() ); q.transform( QTransform() ); m_highlightQuads.append( q ); } // loading complete break; } } AnnotationPrivate* HighlightAnnotationPrivate::getNewAnnotationPrivate() { return new HighlightAnnotationPrivate(); } double HighlightAnnotationPrivate::distanceSqr( double x, double y, double xScale, double yScale ) const { NormalizedPoint point( x, y ); double outsideDistance = DBL_MAX; for ( const HighlightAnnotation::Quad &quad : m_highlightQuads ) { QLinkedList pathPoints; //first, we check if the point is within the area described by the 4 quads //this is the case, if the point is always on one side of each segments delimiting the polygon: pathPoints << quad.transformedPoint( 0 ); int directionVote = 0; for ( int i = 1; i < 5; ++i ) { NormalizedPoint thisPoint = quad.transformedPoint( i % 4 ); directionVote += (isLeftOfVector( pathPoints.back(), thisPoint, point )) ? 1 : -1; pathPoints << thisPoint; } if ( abs( directionVote ) == 4 ) return 0; //if that's not the case, we treat the outline as path and simply determine //the distance from the path to the point const double thisOutsideDistance = ::distanceSqr( x, y, xScale, yScale, pathPoints ); if ( thisOutsideDistance < outsideDistance ) outsideDistance = thisOutsideDistance; } return outsideDistance; } /** StampAnnotation [Annotation] */ class Okular::StampAnnotationPrivate : public Okular::AnnotationPrivate { public: StampAnnotationPrivate() : AnnotationPrivate(), m_stampIconName( QStringLiteral("Draft") ) { } void setAnnotationProperties( const QDomNode& node ) override; bool canBeResized() const override; AnnotationPrivate* getNewAnnotationPrivate() override; QString m_stampIconName; }; StampAnnotation::StampAnnotation() : Annotation( *new StampAnnotationPrivate() ) { } StampAnnotation::StampAnnotation( const QDomNode & description ) : Annotation( *new StampAnnotationPrivate(), description ) { } StampAnnotation::~StampAnnotation() { } void StampAnnotation::setStampIconName( const QString &name ) { Q_D( StampAnnotation ); d->m_stampIconName = name; } QString StampAnnotation::stampIconName() const { Q_D( const StampAnnotation ); return d->m_stampIconName; } Annotation::SubType StampAnnotation::subType() const { return AStamp; } void StampAnnotation::store( QDomNode & node, QDomDocument & document ) const { Q_D( const StampAnnotation ); // recurse to parent objects storing properties Annotation::store( node, document ); // create [stamp] element QDomElement stampElement = document.createElement( QStringLiteral("stamp") ); node.appendChild( stampElement ); // append the optional attributes if ( d->m_stampIconName != QLatin1String("Draft") ) stampElement.setAttribute( QStringLiteral("icon"), d->m_stampIconName ); } void StampAnnotationPrivate::setAnnotationProperties( const QDomNode& node ) { Okular::AnnotationPrivate::setAnnotationProperties(node); // loop through the whole children looking for a 'stamp' element QDomNode subNode = node.firstChild(); while( subNode.isElement() ) { QDomElement e = subNode.toElement(); subNode = subNode.nextSibling(); if ( e.tagName() != QLatin1String("stamp") ) continue; // parse the attributes if ( e.hasAttribute( QStringLiteral("icon") ) ) m_stampIconName = e.attribute( QStringLiteral("icon") ); // loading complete break; } } bool StampAnnotationPrivate::canBeResized() const { return true; } AnnotationPrivate* StampAnnotationPrivate::getNewAnnotationPrivate() { return new StampAnnotationPrivate(); } /** InkAnnotation [Annotation] */ class Okular::InkAnnotationPrivate : public Okular::AnnotationPrivate { public: InkAnnotationPrivate() : AnnotationPrivate() { } void transform( const QTransform &matrix ) override; void baseTransform( const QTransform &matrix ) override; void resetTransformation() override; double distanceSqr( double x, double y, double xScale, double yScale ) const override; void translate( const NormalizedPoint &coord ) override; void setAnnotationProperties( const QDomNode& node ) override; AnnotationPrivate* getNewAnnotationPrivate() override; QList< QLinkedList > m_inkPaths; QList< QLinkedList > m_transformedInkPaths; }; InkAnnotation::InkAnnotation() : Annotation( *new InkAnnotationPrivate() ) { } InkAnnotation::InkAnnotation( const QDomNode & description ) : Annotation( *new InkAnnotationPrivate(), description ) { } InkAnnotation::~InkAnnotation() { } void InkAnnotation::setInkPaths( const QList< QLinkedList > &paths ) { Q_D( InkAnnotation ); d->m_inkPaths = paths; } QList< QLinkedList > InkAnnotation::inkPaths() const { Q_D( const InkAnnotation ); return d->m_inkPaths; } QList< QLinkedList > InkAnnotation::transformedInkPaths() const { Q_D( const InkAnnotation ); return d->m_transformedInkPaths; } Annotation::SubType InkAnnotation::subType() const { return AInk; } void InkAnnotation::store( QDomNode & node, QDomDocument & document ) const { Q_D( const InkAnnotation ); // recurse to parent objects storing properties Annotation::store( node, document ); // create [ink] element QDomElement inkElement = document.createElement( QStringLiteral("ink") ); node.appendChild( inkElement ); // append the optional attributes if ( d->m_inkPaths.count() < 1 ) return; QList< QLinkedList >::const_iterator pIt = d->m_inkPaths.begin(), pEnd = d->m_inkPaths.end(); for ( ; pIt != pEnd; ++pIt ) { QDomElement pathElement = document.createElement( QStringLiteral("path") ); inkElement.appendChild( pathElement ); const QLinkedList & path = *pIt; QLinkedList::const_iterator iIt = path.begin(), iEnd = path.end(); for ( ; iIt != iEnd; ++iIt ) { const NormalizedPoint & point = *iIt; QDomElement pointElement = document.createElement( QStringLiteral("point") ); pathElement.appendChild( pointElement ); pointElement.setAttribute( QStringLiteral("x"), QString::number( point.x ) ); pointElement.setAttribute( QStringLiteral("y"), QString::number( point.y ) ); } } } double InkAnnotationPrivate::distanceSqr( double x, double y, double xScale, double yScale ) const { double distance = DBL_MAX; for ( const QLinkedList &path : m_transformedInkPaths ) { const double thisDistance = ::distanceSqr( x, y, xScale, yScale, path ); if ( thisDistance < distance ) distance = thisDistance; } return strokeDistance( distance, m_style.width() * xScale / ( m_page->m_width * 2 ) ); } void InkAnnotationPrivate::transform( const QTransform &matrix ) { AnnotationPrivate::transform( matrix ); for ( int i = 0; i < m_transformedInkPaths.count(); ++i ) { QMutableLinkedListIterator it( m_transformedInkPaths[ i ] ); while ( it.hasNext() ) it.next().transform( matrix ); } } void InkAnnotationPrivate::baseTransform( const QTransform &matrix ) { AnnotationPrivate::baseTransform( matrix ); for ( int i = 0; i < m_inkPaths.count(); ++i ) { QMutableLinkedListIterator it( m_inkPaths[ i ] ); while ( it.hasNext() ) it.next().transform( matrix ); } } void InkAnnotationPrivate::resetTransformation() { AnnotationPrivate::resetTransformation(); m_transformedInkPaths = m_inkPaths; } void InkAnnotationPrivate::translate( const NormalizedPoint &coord ) { AnnotationPrivate::translate( coord ); for ( int i = 0; i < m_inkPaths.count(); ++i ) { QMutableLinkedListIterator it( m_inkPaths[ i ] ); while ( it.hasNext() ) { NormalizedPoint& p = it.next(); p.x = p.x + coord.x; p.y = p.y + coord.y; } } } void InkAnnotationPrivate::setAnnotationProperties( const QDomNode& node ) { Okular::AnnotationPrivate::setAnnotationProperties(node); m_inkPaths.clear(); // loop through the whole children looking for a 'ink' element QDomNode subNode = node.firstChild(); while( subNode.isElement() ) { QDomElement e = subNode.toElement(); subNode = subNode.nextSibling(); if ( e.tagName() != QLatin1String("ink") ) continue; // parse the 'path' subnodes QDomNode pathNode = e.firstChild(); while ( pathNode.isElement() ) { QDomElement pathElement = pathNode.toElement(); pathNode = pathNode.nextSibling(); if ( pathElement.tagName() != QLatin1String("path") ) continue; // build each path parsing 'point' subnodes QLinkedList path; QDomNode pointNode = pathElement.firstChild(); while ( pointNode.isElement() ) { QDomElement pointElement = pointNode.toElement(); pointNode = pointNode.nextSibling(); if ( pointElement.tagName() != QLatin1String("point") ) continue; NormalizedPoint p; p.x = pointElement.attribute( QStringLiteral("x"), QStringLiteral("0.0") ).toDouble(); p.y = pointElement.attribute( QStringLiteral("y"), QStringLiteral("0.0") ).toDouble(); path.append( p ); } // add the path to the path list if it contains at least 2 nodes if ( path.count() >= 2 ) m_inkPaths.append( path ); } // loading complete break; } m_transformedInkPaths = m_inkPaths; } AnnotationPrivate* InkAnnotationPrivate::getNewAnnotationPrivate() { return new InkAnnotationPrivate(); } /** CaretAnnotation [Annotation] */ class Okular::CaretAnnotationPrivate : public Okular::AnnotationPrivate { public: CaretAnnotationPrivate() : AnnotationPrivate(), m_symbol( CaretAnnotation::None ) { } void setAnnotationProperties( const QDomNode& node ) override; AnnotationPrivate* getNewAnnotationPrivate() override; CaretAnnotation::CaretSymbol m_symbol; }; static QString caretSymbolToString( CaretAnnotation::CaretSymbol symbol ) { switch ( symbol ) { case CaretAnnotation::None: return QStringLiteral( "None" ); case CaretAnnotation::P: return QStringLiteral( "P" ); } return QString(); } static CaretAnnotation::CaretSymbol caretSymbolFromString( const QString &symbol ) { if ( symbol == QLatin1String( "None" ) ) return CaretAnnotation::None; else if ( symbol == QLatin1String( "P" ) ) return CaretAnnotation::P; return CaretAnnotation::None; } void CaretAnnotationPrivate::setAnnotationProperties( const QDomNode& node ) { Okular::AnnotationPrivate::setAnnotationProperties(node); // loop through the whole children looking for a 'caret' element QDomNode subNode = node.firstChild(); while( subNode.isElement() ) { QDomElement e = subNode.toElement(); subNode = subNode.nextSibling(); if ( e.tagName() != QLatin1String("caret") ) continue; // parse the attributes if ( e.hasAttribute( QStringLiteral("symbol") ) ) m_symbol = caretSymbolFromString( e.attribute( QStringLiteral("symbol") ) ); // loading complete break; } } AnnotationPrivate* CaretAnnotationPrivate::getNewAnnotationPrivate() { return new CaretAnnotationPrivate(); } CaretAnnotation::CaretAnnotation() : Annotation( *new CaretAnnotationPrivate() ) { } CaretAnnotation::CaretAnnotation( const QDomNode & description ) : Annotation( *new CaretAnnotationPrivate(), description ) { } CaretAnnotation::~CaretAnnotation() { } void CaretAnnotation::setCaretSymbol( CaretAnnotation::CaretSymbol symbol ) { Q_D( CaretAnnotation ); d->m_symbol = symbol; } CaretAnnotation::CaretSymbol CaretAnnotation::caretSymbol() const { Q_D( const CaretAnnotation ); return d->m_symbol; } Annotation::SubType CaretAnnotation::subType() const { return ACaret; } void CaretAnnotation::store( QDomNode & node, QDomDocument & document ) const { Q_D( const CaretAnnotation ); // recurse to parent objects storing properties Annotation::store( node, document ); // create [caret] element QDomElement caretElement = document.createElement( QStringLiteral("caret") ); node.appendChild( caretElement ); // append the optional attributes if ( d->m_symbol != None ) caretElement.setAttribute( QStringLiteral("symbol"), caretSymbolToString( d->m_symbol ) ); } /** FileAttachmentAnnotation [Annotation] */ class Okular::FileAttachmentAnnotationPrivate : public Okular::AnnotationPrivate { public: FileAttachmentAnnotationPrivate() : AnnotationPrivate(), icon( QStringLiteral("PushPin") ), embfile( nullptr ) { } ~FileAttachmentAnnotationPrivate() override { delete embfile; } void setAnnotationProperties( const QDomNode& node ) override; AnnotationPrivate* getNewAnnotationPrivate() override; // data fields QString icon; EmbeddedFile *embfile; }; void FileAttachmentAnnotationPrivate::setAnnotationProperties( const QDomNode& node ) { Okular::AnnotationPrivate::setAnnotationProperties(node); // loop through the whole children looking for a 'fileattachment' element QDomNode subNode = node.firstChild(); while( subNode.isElement() ) { QDomElement e = subNode.toElement(); subNode = subNode.nextSibling(); if ( e.tagName() != QLatin1String("fileattachment") ) continue; // loading complete break; } } AnnotationPrivate* FileAttachmentAnnotationPrivate::getNewAnnotationPrivate() { return new FileAttachmentAnnotationPrivate(); } FileAttachmentAnnotation::FileAttachmentAnnotation() : Annotation( *new FileAttachmentAnnotationPrivate() ) { } FileAttachmentAnnotation::FileAttachmentAnnotation( const QDomNode & description ) : Annotation( *new FileAttachmentAnnotationPrivate(), description ) { } FileAttachmentAnnotation::~FileAttachmentAnnotation() { } void FileAttachmentAnnotation::store( QDomNode & node, QDomDocument & document ) const { // recurse to parent objects storing properties Annotation::store( node, document ); // create [fileattachment] element QDomElement fileAttachmentElement = document.createElement( QStringLiteral("fileattachment") ); node.appendChild( fileAttachmentElement ); } Annotation::SubType FileAttachmentAnnotation::subType() const { return AFileAttachment; } QString FileAttachmentAnnotation::fileIconName() const { Q_D( const FileAttachmentAnnotation ); return d->icon; } void FileAttachmentAnnotation::setFileIconName( const QString &iconName ) { Q_D( FileAttachmentAnnotation ); d->icon = iconName; } EmbeddedFile* FileAttachmentAnnotation::embeddedFile() const { Q_D( const FileAttachmentAnnotation ); return d->embfile; } void FileAttachmentAnnotation::setEmbeddedFile( EmbeddedFile *ef ) { Q_D( FileAttachmentAnnotation ); d->embfile = ef; } /** SoundAnnotation [Annotation] */ class Okular::SoundAnnotationPrivate : public Okular::AnnotationPrivate { public: SoundAnnotationPrivate() : AnnotationPrivate(), icon( QStringLiteral("Speaker") ), sound( nullptr ) { } ~SoundAnnotationPrivate() override { delete sound; } void setAnnotationProperties( const QDomNode& node ) override; AnnotationPrivate* getNewAnnotationPrivate() override; // data fields QString icon; Sound *sound; }; void SoundAnnotationPrivate::setAnnotationProperties( const QDomNode& node ) { Okular::AnnotationPrivate::setAnnotationProperties(node); // loop through the whole children looking for a 'sound' element QDomNode subNode = node.firstChild(); while( subNode.isElement() ) { QDomElement e = subNode.toElement(); subNode = subNode.nextSibling(); if ( e.tagName() != QLatin1String("sound") ) continue; // loading complete break; } } AnnotationPrivate* SoundAnnotationPrivate::getNewAnnotationPrivate() { return new SoundAnnotationPrivate(); } SoundAnnotation::SoundAnnotation() : Annotation( *new SoundAnnotationPrivate() ) { } SoundAnnotation::SoundAnnotation( const QDomNode & description ) : Annotation( *new SoundAnnotationPrivate(), description ) { } SoundAnnotation::~SoundAnnotation() { } void SoundAnnotation::store( QDomNode & node, QDomDocument & document ) const { // recurse to parent objects storing properties Annotation::store( node, document ); // create [sound] element QDomElement soundElement = document.createElement( QStringLiteral("sound") ); node.appendChild( soundElement ); } Annotation::SubType SoundAnnotation::subType() const { return ASound; } QString SoundAnnotation::soundIconName() const { Q_D( const SoundAnnotation ); return d->icon; } void SoundAnnotation::setSoundIconName( const QString &iconName ) { Q_D( SoundAnnotation ); d->icon = iconName; } Sound* SoundAnnotation::sound() const { Q_D( const SoundAnnotation ); return d->sound; } void SoundAnnotation::setSound( Sound *s ) { Q_D( SoundAnnotation ); d->sound = s; } /** MovieAnnotation [Annotation] */ class Okular::MovieAnnotationPrivate : public Okular::AnnotationPrivate { public: MovieAnnotationPrivate() : AnnotationPrivate(), movie( nullptr ) { } ~MovieAnnotationPrivate() override { delete movie; } void setAnnotationProperties( const QDomNode& node ) override; AnnotationPrivate* getNewAnnotationPrivate() override; // data fields Movie *movie; }; void MovieAnnotationPrivate::setAnnotationProperties( const QDomNode& node ) { Okular::AnnotationPrivate::setAnnotationProperties(node); // loop through the whole children looking for a 'movie' element QDomNode subNode = node.firstChild(); while( subNode.isElement() ) { QDomElement e = subNode.toElement(); subNode = subNode.nextSibling(); if ( e.tagName() != QLatin1String("movie") ) continue; // loading complete break; } } AnnotationPrivate* MovieAnnotationPrivate::getNewAnnotationPrivate() { return new MovieAnnotationPrivate(); } MovieAnnotation::MovieAnnotation() : Annotation( *new MovieAnnotationPrivate() ) { } MovieAnnotation::MovieAnnotation( const QDomNode & description ) : Annotation( *new MovieAnnotationPrivate(), description ) { } MovieAnnotation::~MovieAnnotation() { } void MovieAnnotation::store( QDomNode & node, QDomDocument & document ) const { // recurse to parent objects storing properties Annotation::store( node, document ); // create [movie] element QDomElement movieElement = document.createElement( QStringLiteral("movie") ); node.appendChild( movieElement ); } Annotation::SubType MovieAnnotation::subType() const { return AMovie; } Movie* MovieAnnotation::movie() const { Q_D( const MovieAnnotation ); return d->movie; } void MovieAnnotation::setMovie( Movie *movie ) { Q_D( MovieAnnotation ); d->movie = movie; } /** ScreenAnnotation [Annotation] */ class Okular::ScreenAnnotationPrivate : public Okular::AnnotationPrivate { public: ScreenAnnotationPrivate(); ~ScreenAnnotationPrivate() override; void setAnnotationProperties( const QDomNode& node ) override; AnnotationPrivate* getNewAnnotationPrivate() override; Okular::Action* m_action; QMap< Okular::Annotation::AdditionalActionType, Okular::Action* > m_additionalActions; }; ScreenAnnotationPrivate::ScreenAnnotationPrivate() : m_action( nullptr ) { } ScreenAnnotationPrivate::~ScreenAnnotationPrivate() { delete m_action; qDeleteAll( m_additionalActions ); } void ScreenAnnotationPrivate::setAnnotationProperties( const QDomNode& node ) { Okular::AnnotationPrivate::setAnnotationProperties(node); // loop through the whole children looking for a 'screen' element QDomNode subNode = node.firstChild(); while( subNode.isElement() ) { QDomElement e = subNode.toElement(); subNode = subNode.nextSibling(); if ( e.tagName() != QLatin1String("screen") ) continue; // loading complete break; } } AnnotationPrivate* ScreenAnnotationPrivate::getNewAnnotationPrivate() { return new ScreenAnnotationPrivate(); } ScreenAnnotation::ScreenAnnotation() : Annotation( *new ScreenAnnotationPrivate() ) { } ScreenAnnotation::ScreenAnnotation( const QDomNode & description ) : Annotation( *new ScreenAnnotationPrivate(), description ) { } ScreenAnnotation::~ScreenAnnotation() { } void ScreenAnnotation::store( QDomNode & node, QDomDocument & document ) const { // recurse to parent objects storing properties Annotation::store( node, document ); // create [screen] element QDomElement movieElement = document.createElement( QStringLiteral("screen") ); node.appendChild( movieElement ); } Annotation::SubType ScreenAnnotation::subType() const { return AScreen; } void ScreenAnnotation::setAdditionalAction( AdditionalActionType type, Action *action ) { Q_D( ScreenAnnotation ); if ( d->m_additionalActions.contains( type ) ) delete d->m_additionalActions.value( type ); d->m_additionalActions.insert( type, action ); } Action* ScreenAnnotation::additionalAction( AdditionalActionType type ) const { Q_D( const ScreenAnnotation ); if ( !d->m_additionalActions.contains( type ) ) return nullptr; else return d->m_additionalActions.value( type ); } void ScreenAnnotation::setAction( Action *action ) { Q_D( ScreenAnnotation ); delete d->m_action; d->m_action = action; } Action* ScreenAnnotation::action() const { Q_D( const ScreenAnnotation ); return d->m_action; } /** WidgetAnnotation [Annotation] */ class Okular::WidgetAnnotationPrivate : public Okular::AnnotationPrivate { public: ~WidgetAnnotationPrivate() override; void setAnnotationProperties( const QDomNode& node ) override; AnnotationPrivate* getNewAnnotationPrivate() override; QMap< Okular::Annotation::AdditionalActionType, Okular::Action* > m_additionalActions; }; WidgetAnnotationPrivate::~WidgetAnnotationPrivate() { qDeleteAll( m_additionalActions ); } void WidgetAnnotationPrivate::setAnnotationProperties( const QDomNode& node ) { Okular::AnnotationPrivate::setAnnotationProperties(node); // loop through the whole children looking for a 'widget' element QDomNode subNode = node.firstChild(); while( subNode.isElement() ) { QDomElement e = subNode.toElement(); subNode = subNode.nextSibling(); if ( e.tagName() != QLatin1String("widget") ) continue; // loading complete break; } } AnnotationPrivate* WidgetAnnotationPrivate::getNewAnnotationPrivate() { return new WidgetAnnotationPrivate(); } WidgetAnnotation::WidgetAnnotation() : Annotation( *new WidgetAnnotationPrivate() ) { } WidgetAnnotation::WidgetAnnotation( const QDomNode & description ) : Annotation( *new WidgetAnnotationPrivate, description ) { } WidgetAnnotation::~WidgetAnnotation() { } void WidgetAnnotation::store( QDomNode & node, QDomDocument & document ) const { // recurse to parent objects storing properties Annotation::store( node, document ); // create [widget] element QDomElement movieElement = document.createElement( QStringLiteral("widget") ); node.appendChild( movieElement ); } Annotation::SubType WidgetAnnotation::subType() const { return AWidget; } void WidgetAnnotation::setAdditionalAction( AdditionalActionType type, Action *action ) { Q_D( WidgetAnnotation ); if ( d->m_additionalActions.contains( type ) ) delete d->m_additionalActions.value( type ); d->m_additionalActions.insert( type, action ); } Action* WidgetAnnotation::additionalAction( AdditionalActionType type ) const { Q_D( const WidgetAnnotation ); if ( !d->m_additionalActions.contains( type ) ) return nullptr; else return d->m_additionalActions.value( type ); } /** RichMediaAnnotation [Annotation] */ class Okular::RichMediaAnnotationPrivate : public Okular::AnnotationPrivate { public: RichMediaAnnotationPrivate(); ~RichMediaAnnotationPrivate() override; void setAnnotationProperties( const QDomNode& node ) override; AnnotationPrivate* getNewAnnotationPrivate() override; // data fields Movie *movie; EmbeddedFile *embeddedFile; }; RichMediaAnnotationPrivate::RichMediaAnnotationPrivate() : movie( nullptr ), embeddedFile( nullptr ) { } RichMediaAnnotationPrivate::~RichMediaAnnotationPrivate() { delete movie; delete embeddedFile; } void RichMediaAnnotationPrivate::setAnnotationProperties( const QDomNode& node ) { Okular::AnnotationPrivate::setAnnotationProperties(node); // loop through the whole children looking for a 'richMedia' element QDomNode subNode = node.firstChild(); while( subNode.isElement() ) { QDomElement e = subNode.toElement(); subNode = subNode.nextSibling(); if ( e.tagName() != QLatin1String("richMedia") ) continue; // loading complete break; } } AnnotationPrivate* RichMediaAnnotationPrivate::getNewAnnotationPrivate() { return new RichMediaAnnotationPrivate(); } RichMediaAnnotation::RichMediaAnnotation() : Annotation( *new RichMediaAnnotationPrivate() ) { } RichMediaAnnotation::RichMediaAnnotation( const QDomNode & description ) : Annotation( *new RichMediaAnnotationPrivate, description ) { } RichMediaAnnotation::~RichMediaAnnotation() { } void RichMediaAnnotation::store( QDomNode & node, QDomDocument & document ) const { // recurse to parent objects storing properties Annotation::store( node, document ); // create [richMedia] element QDomElement movieElement = document.createElement( QStringLiteral("richMedia") ); node.appendChild( movieElement ); } Annotation::SubType RichMediaAnnotation::subType() const { return ARichMedia; } void RichMediaAnnotation::setMovie( Movie *movie ) { Q_D( RichMediaAnnotation ); delete d->movie; d->movie = movie; } Movie* RichMediaAnnotation::movie() const { Q_D( const RichMediaAnnotation ); return d->movie; } EmbeddedFile* RichMediaAnnotation::embeddedFile() const { Q_D( const RichMediaAnnotation ); return d->embeddedFile; } void RichMediaAnnotation::setEmbeddedFile( EmbeddedFile *embeddedFile ) { Q_D( RichMediaAnnotation ); delete d->embeddedFile; d->embeddedFile = embeddedFile; } diff --git a/core/page.cpp b/core/page.cpp index e004f5441..2336112b1 100644 --- a/core/page.cpp +++ b/core/page.cpp @@ -1,1128 +1,1128 @@ /*************************************************************************** * 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(), 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 pageNumber, double w, double h, Rotation o ) : d( new PagePrivate( this, pageNumber, 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) + for (auto &retI : ret) { - const TextEntity * orig = ret[i]; - ret[i] = new TextEntity( orig->text(), new Okular::NormalizedRect(orig->transformedArea ( d->rotationMatrix() )) ); + const TextEntity * orig = retI; + retI = 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; 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 ); const QTransform highlightRotationMatrix = Okular::buildRotationMatrix( (Rotation)(((int)m_rotation - (int)oldRotation + 4) % 4) ); QLinkedList< HighlightAreaRect* >::const_iterator hlIt = m_page->m_highlights.begin(), hlItEnd = m_page->m_highlights.end(); for ( ; hlIt != hlItEnd; ++hlIt ) { (*hlIt)->transform( highlightRotationMatrix ); } } 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 { for (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 ) { 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, isPartialPixmap ); delete pixmap; return; } QMap< DocumentObserver*, PagePrivate::PixmapObject >::iterator it = m_pixmaps.find( observer ); if ( it != m_pixmaps.end() ) { delete it.value().m_pixmap; } else { it = m_pixmaps.insert( observer, PagePrivate::PixmapObject() ); } it.value().m_pixmap = pixmap; 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 ( m_doc->m_pageController ) { 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/optimize text order for search and 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(); for ( 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 children (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 for (FormField *f : qAsConst(p->d->formfields)) { if (f->rect() == oldField->rect() && f->type() == oldField->type() && f->id() == oldField->id()) return f; } // same rect and type for (FormField *f : qAsConst(p->d->formfields)) { if (f->rect() == oldField->rect() && f->type() == oldField->type()) return f; } // fuzzy rect, same type and id for (FormField *f : qAsConst(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 for (FormField *f : qAsConst(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/script/kjs_util.cpp b/core/script/kjs_util.cpp index 061189296..2cf531245 100644 --- a/core/script/kjs_util.cpp +++ b/core/script/kjs_util.cpp @@ -1,132 +1,132 @@ /*************************************************************************** * Copyright (C) 2008 by Pino Toscano * * Copyright (C) 2008 by Harri Porten * * * * 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 "kjs_util_p.h" #include #include #include #include #include #include #include #include using namespace Okular; static KJSPrototype *g_utilProto; static KJSObject crackURL( KJSContext *context, void *, const KJSArguments &arguments ) { if ( arguments.count() < 1 ) { return context->throwException( QStringLiteral("Missing URL argument") ); } QString cURL = arguments.at( 0 ).toString( context ); QUrl url(QUrl::fromLocalFile(cURL) ); if ( !url.isValid() ) { return context->throwException( QStringLiteral("Invalid URL") ); } if ( url.scheme() != QLatin1String( "file" ) || url.scheme() != QLatin1String( "http" ) || url.scheme() != QLatin1String( "https" ) ) { return context->throwException( QStringLiteral("Protocol not valid: '") + url.scheme() + QLatin1Char('\'') ); } KJSObject obj; obj.setProperty( context, QStringLiteral("cScheme"), url.scheme() ); if ( !url.userName().isEmpty() ) obj.setProperty( context, QStringLiteral("cUser"), url.userName() ); if ( !url.password().isEmpty() ) obj.setProperty( context, QStringLiteral("cPassword"), url.password() ); obj.setProperty( context, QStringLiteral("cHost"), url.host() ); obj.setProperty( context, QStringLiteral("nPort"), url.port( 80 ) ); // TODO cPath (Optional) The path portion of the URL. // TODO cParameters (Optional) The parameter string portion of the URL. if ( url.hasFragment() ) obj.setProperty( context, QStringLiteral("cFragments"), url.fragment(QUrl::FullyDecoded) ); return obj; } static KJSObject printd( KJSContext *context, void *, const KJSArguments &arguments ) { if ( arguments.count() < 2 ) { return context->throwException( QStringLiteral("Invalid arguments") ); } KJSObject oFormat = arguments.at( 0 ); QString format; QLocale defaultLocale; if( oFormat.isNumber() ) { int formatType = oFormat.toInt32( context ); switch( formatType ) { case 0: format = QStringLiteral( "D:yyyyMMddHHmmss" ); break; case 1: format = QStringLiteral( "yyyy.MM.dd HH:mm:ss"); break; case 2: format = defaultLocale.dateTimeFormat( QLocale::ShortFormat ); if( !format.contains( QStringLiteral( "ss" ) ) ) format.insert( format.indexOf( QStringLiteral( "mm" ) ) + 2, QStringLiteral( ":ss" ) ); break; } } else { format = arguments.at( 0 ).toString( context ).replace( "tt", "ap" ); format.replace( "t", "a" ); - for( int i = 0 ; i < format.size() ; ++i ) + for( QChar &formatChar : format ) { - if( format[i] == 'M' ) - format[i] = 'm'; - else if( format[i] == 'm' ) - format[i] = 'M'; + if( formatChar == 'M' ) + formatChar = 'm'; + else if( formatChar == 'm' ) + formatChar = 'M'; } } QLocale locale( "en_US" ); QStringList str = arguments.at( 1 ).toString( context ).split( QRegularExpression( "\\W") ); QString myStr = QStringLiteral( "%1/%2/%3 %4:%5:%6" ).arg( str[1] ). arg( str[2] ).arg( str[3] ).arg( str[4] ).arg( str[5] ).arg( str[6] ); QDateTime date = locale.toDateTime( myStr, QStringLiteral( "MMM/d/yyyy H:m:s" ) ); return KJSString( defaultLocale.toString( date, format ) ); } void JSUtil::initType( KJSContext *ctx ) { static bool initialized = false; if ( initialized ) return; initialized = true; g_utilProto = new KJSPrototype(); g_utilProto->defineFunction( ctx, QStringLiteral("crackURL"), crackURL ); g_utilProto->defineFunction( ctx, QStringLiteral("printd"), printd ); } KJSObject JSUtil::object( KJSContext *ctx ) { return g_utilProto->constructObject( ctx, nullptr ); } diff --git a/core/textpage.cpp b/core/textpage.cpp index 23006d9e2..9d7ad6347 100644 --- a/core/textpage.cpp +++ b/core/textpage.cpp @@ -1,2039 +1,2035 @@ /*************************************************************************** * 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. * ***************************************************************************/ #include "textpage.h" #include "textpage_p.h" #include #include "area.h" #include "debug_p.h" #include "misc.h" #include "page.h" #include "page_p.h" #include #include #include using namespace Okular; class SearchPoint { public: SearchPoint() : offset_begin( -1 ), offset_end( -1 ) { } /** The TinyTextEntity containing the first character of the match. */ TextList::ConstIterator it_begin; /** The TinyTextEntity containing the last character of the match. */ TextList::ConstIterator it_end; /** The index of the first character of the match in (*it_begin)->text(). * Satisfies 0 <= offset_begin < (*it_begin)->text().length(). */ int offset_begin; /** One plus the index of the last character of the match in (*it_end)->text(). * Satisfies 0 < offset_end <= (*it_end)->text().length(). */ int offset_end; }; /* text comparison functions */ static bool CaseInsensitiveCmpFn( const QStringRef & from, const QStringRef & to ) { return from.compare( to, Qt::CaseInsensitive ) == 0; } static bool CaseSensitiveCmpFn( const QStringRef & from, const QStringRef & to ) { return from.compare( to, Qt::CaseSensitive ) == 0; } /** * Returns true iff segments [@p left1, @p right1] and [@p left2, @p right2] on the real line * overlap within @p threshold percent, i. e. iff the ratio of the length of the * intersection of the segments to the length of the shortest of the two input segments * is not smaller than the threshold. */ static bool segmentsOverlap(double left1, double right1, double left2, double right2, int threshold) { // check if one consumes another fully (speed optimization) if (left1 <= left2 && right1 >= right2) return true; if (left1 >= left2 && right1 <= right2) return true; // check if there is overlap above threshold if (right2 >= left1 && right1 >= left2) { double overlap = (right2 >= right1) ? right1 - left2 : right2 - left1; double length1 = right1 - left1, length2 = right2 - left2; return overlap * 100 >= threshold * qMin(length1, length2); } return false; } static bool doesConsumeY(const QRect& first, const QRect& second, int threshold) { return segmentsOverlap(first.top(), first.bottom(), second.top(), second.bottom(), threshold); } static bool doesConsumeY(const NormalizedRect& first, const NormalizedRect& second, int threshold) { return segmentsOverlap(first.top, first.bottom, second.top, second.bottom, threshold); } /* Rationale behind TinyTextEntity: instead of storing directly a QString for the text of an entity, we store the UTF-16 data and their length. This way, we save about 4 int's wrt a QString, and we can create a new string from that raw data (that's the only penalty of that). Even better, if the string we need to store has at most MaxStaticChars characters, then we store those in place of the QChar* that would be used (with new[] + free[]) for the data. */ class TinyTextEntity { static const int MaxStaticChars = sizeof( void* ) / sizeof( QChar ); public: TinyTextEntity( const QString &text, const NormalizedRect &rect ) : area( rect ) { Q_ASSERT_X( !text.isEmpty(), "TinyTextEntity", "empty string" ); Q_ASSERT_X( sizeof( d ) == sizeof( void * ), "TinyTextEntity", "internal storage is wider than QChar*, fix it!" ); length = text.length(); switch ( length ) { #if QT_POINTER_SIZE >= 8 case 4: d.qc[3] = text.at( 3 ).unicode(); // fall through case 3: d.qc[2] = text.at( 2 ).unicode(); #endif // fall through case 2: d.qc[1] = text.at( 1 ).unicode(); // fall through case 1: d.qc[0] = text.at( 0 ).unicode(); break; default: d.data = new QChar[ length ]; std::memcpy( d.data, text.constData(), length * sizeof( QChar ) ); } } ~TinyTextEntity() { if ( length > MaxStaticChars ) { delete [] d.data; } } inline QString text() const { return length <= MaxStaticChars ? QString::fromRawData( ( const QChar * )&d.qc[0], length ) : QString::fromRawData( d.data, length ); } inline NormalizedRect transformedArea( const QTransform &matrix ) const { NormalizedRect transformed_area = area; transformed_area.transform( matrix ); return transformed_area; } NormalizedRect area; private: Q_DISABLE_COPY( TinyTextEntity ) union { QChar *data; ushort qc[MaxStaticChars]; } d; int length; }; TextEntity::TextEntity( const QString &text, NormalizedRect *area ) : m_text( text ), m_area( area ), d( nullptr ) { } TextEntity::~TextEntity() { delete m_area; } QString TextEntity::text() const { return m_text; } NormalizedRect* TextEntity::area() const { return m_area; } NormalizedRect TextEntity::transformedArea(const QTransform &matrix) const { NormalizedRect transformed_area = *m_area; transformed_area.transform( matrix ); return transformed_area; } TextPagePrivate::TextPagePrivate() : m_page( nullptr ) { } TextPagePrivate::~TextPagePrivate() { qDeleteAll( m_searchPoints ); qDeleteAll( m_words ); } TextPage::TextPage() : d( new TextPagePrivate() ) { } TextPage::TextPage( const TextEntity::List &words ) : d( new TextPagePrivate() ) { TextEntity::List::ConstIterator it = words.constBegin(), itEnd = words.constEnd(); for ( ; it != itEnd; ++it ) { TextEntity *e = *it; if ( !e->text().isEmpty() ) d->m_words.append( new TinyTextEntity( e->text(), *e->area() ) ); delete e; } } TextPage::~TextPage() { delete d; } void TextPage::append( const QString &text, NormalizedRect *area ) { if ( !text.isEmpty() ) d->m_words.append( new TinyTextEntity( text.normalized(QString::NormalizationForm_KC), *area ) ); delete area; } struct WordWithCharacters { WordWithCharacters(TinyTextEntity *w, const TextList &c) : word(w), characters(c) { } inline QString text() const { return word->text(); } inline const NormalizedRect &area() const { return word->area; } TinyTextEntity *word; TextList characters; }; typedef QList WordsWithCharacters; /** * We will divide the whole page in some regions depending on the horizontal and * vertical spacing among different regions. Each region will have an area and an * associated WordsWithCharacters in sorted order. */ class RegionText { public: RegionText() { }; RegionText(const WordsWithCharacters &wordsWithCharacters, const QRect &area) : m_region_wordWithCharacters(wordsWithCharacters), m_area(area) { } inline QString string() const { QString res; for (const WordWithCharacters &word : m_region_wordWithCharacters) { res += word.text(); } return res; } inline WordsWithCharacters text() const { return m_region_wordWithCharacters; } inline QRect area() const { return m_area; } inline void setArea(const QRect &area) { m_area = area; } inline void setText(const WordsWithCharacters &wordsWithCharacters) { m_region_wordWithCharacters = wordsWithCharacters; } private: WordsWithCharacters m_region_wordWithCharacters; QRect m_area; }; RegularAreaRect * TextPage::textArea ( TextSelection * sel) const { if ( d->m_words.isEmpty() ) return new RegularAreaRect(); /** It works like this: There are two cursors, we need to select all the text between them. The coordinates are normalised, leftTop is (0,0) rightBottom is (1,1), so for cursors start (sx,sy) and end (ex,ey) we start with finding text rectangles under those points, if not we search for the first that is to the right to it in the same baseline, if none found, then we search for the first rectangle with a baseline under the cursor, having two points that are the best rectangles to both of the cursors: (rx,ry)x(tx,ty) for start and (ux,uy)x(vx,vy) for end, we do a 1. (rx,ry)x(1,ty) 2. (0,ty)x(1,uy) 3. (0,uy)x(vx,vy) To find the closest rectangle to cursor (cx,cy) we search for a rectangle that either contains the cursor or that has a left border >= cx and bottom border >= cy. */ RegularAreaRect * ret= new RegularAreaRect; PagePrivate *pagePrivate = PagePrivate::get(d->m_page); const QTransform matrix = pagePrivate ? pagePrivate->rotationMatrix() : QTransform(); #if 0 int it = -1; int itB = -1; int itE = -1; // ending cursor is higher than start cursor, we need to find positions in reverse NormalizedRect tmp; NormalizedRect start; NormalizedRect end; NormalizedPoint startC = sel->start(); double startCx = startC.x; double startCy = startC.y; NormalizedPoint endC = sel->end(); double endCx = endC.x; double endCy = endC.y; if ( sel->direction() == 1 || ( sel->itB() == -1 && sel->direction() == 0 ) ) { #ifdef DEBUG_TEXTPAGE qCWarning(OkularCoreDebug) << "running first loop"; #endif const int count = d->m_words.count(); for ( it = 0; it < count; it++ ) { tmp = *d->m_words[ it ]->area(); if ( tmp.contains( startCx, startCy ) || ( tmp.top <= startCy && tmp.bottom >= startCy && tmp.left >= startCx ) || ( tmp.top >= startCy)) { /// we have found the (rx,ry)x(tx,ty) itB = it; #ifdef DEBUG_TEXTPAGE qCWarning(OkularCoreDebug) << "start is" << itB << "count is" << d->m_words.count(); #endif break; } } sel->itB( itB ); } itB = sel->itB(); #ifdef DEBUG_TEXTPAGE qCWarning(OkularCoreDebug) << "direction is" << sel->direction(); qCWarning(OkularCoreDebug) << "reloaded start is" << itB << "against" << sel->itB(); #endif if ( sel->direction() == 0 || ( sel->itE() == -1 && sel->direction() == 1 ) ) { #ifdef DEBUG_TEXTPAGE qCWarning(OkularCoreDebug) << "running second loop"; #endif for ( it = d->m_words.count() - 1; it >= itB; it-- ) { tmp = *d->m_words[ it ]->area(); if ( tmp.contains( endCx, endCy ) || ( tmp.top <= endCy && tmp.bottom >= endCy && tmp.right <= endCx ) || ( tmp.bottom <= endCy ) ) { /// we have found the (ux,uy)x(vx,vy) itE = it; #ifdef DEBUG_TEXTPAGE qCWarning(OkularCoreDebug) << "ending is" << itE << "count is" << d->m_words.count(); qCWarning(OkularCoreDebug) << "conditions" << tmp.contains( endCx, endCy ) << " " << ( tmp.top <= endCy && tmp.bottom >= endCy && tmp.right <= endCx ) << " " << ( tmp.top >= endCy); #endif break; } } sel->itE( itE ); } #ifdef DEBUG_TEXTPAGE qCWarning(OkularCoreDebug) << "reloaded ending is" << itE << "against" << sel->itE(); #endif if ( sel->itB() != -1 && sel->itE() != -1 ) { start = *d->m_words[ sel->itB() ]->area(); end = *d->m_words[ sel->itE() ]->area(); NormalizedRect first, second, third; /// finding out if there is more than one baseline between them is a hard and discussable task /// we will create a rectangle (rx,0)x(tx,1) and will check how many times does it intersect the /// areas, if more than one -> we have a three or over line selection first = start; second.top = start.bottom; first.right = second.right = 1; third = end; third.left = second.left = 0; second.bottom = end.top; int selMax = qMax( sel->itB(), sel->itE() ); for ( it = qMin( sel->itB(), sel->itE() ); it <= selMax; ++it ) { tmp = *d->m_words[ it ]->area(); if ( tmp.intersects( &first ) || tmp.intersects( &second ) || tmp.intersects( &third ) ) ret->appendShape( d->m_words.at( it )->transformedArea( matrix ) ); } } #else const double scaleX = d->m_page->width(); const double scaleY = d->m_page->height(); NormalizedPoint startC = sel->start(); NormalizedPoint endC = sel->end(); NormalizedPoint temp; // if startPoint is right to endPoint swap them if(startC.x > endC.x) { temp = startC; startC = endC; endC = temp; } // minX,maxX,minY,maxY gives the bounding rectangle coordinates of the document const NormalizedRect boundingRect = d->m_page->boundingBox(); const QRect content = boundingRect.geometry(scaleX,scaleY); const double minX = content.left(); const double maxX = content.right(); const double minY = content.top(); const double maxY = content.bottom(); /** * We will now find out the TinyTextEntity for the startRectangle and TinyTextEntity for * the endRectangle. We have four cases: * * Case 1(a): both startpoint and endpoint are out of the bounding Rectangle and at one side, so the rectangle made of start * and endPoint are outof the bounding rect (do not intersect) * * Case 1(b): both startpoint and endpoint are out of bounding rect, but they are in different side, so is their rectangle * * Case 2(a): find the rectangle which contains start and endpoint and having some * TextEntity * * Case 2(b): if 2(a) fails (if startPoint and endPoint both are unchanged), then we check whether there is any * TextEntity within the rect made by startPoint and endPoint * * Case 3: Now, we may have two type of selection. * 1. startpoint is left-top of start_end and endpoint is right-bottom * 2. startpoint is left-bottom of start_end and endpoint is top-right * * Also, as 2(b) is passed, we might have it,itEnd or both unchanged, but the fact is that we have * text within them. so, we need to search for the best suitable textposition for start and end. * * Case 3(a): We search the nearest rectangle consisting of some * TinyTextEntity right to or bottom of the startPoint for selection 01. * And, for selection 02, we have to search for right and top * * Case 3(b): For endpoint, we have to find the point top of or left to * endpoint if we have selection 01. * Otherwise, the search will be left and bottom */ // we know that startC.x > endC.x, we need to decide which is top and which is bottom const NormalizedRect start_end = (startC.y < endC.y) ? NormalizedRect(startC.x, startC.y, endC.x, endC.y) : NormalizedRect(startC.x, endC.y, endC.x, startC.y); // Case 1(a) if(!boundingRect.intersects(start_end)) return ret; // case 1(b) /** note that, after swapping of start and end, we know that, start is always left to end. but, we cannot say start is positioned upper than end. **/ else { // if start is left to content rect take it to content rect boundary if(startC.x * scaleX < minX) startC.x = minX/scaleX; if(endC.x * scaleX > maxX) endC.x = maxX/scaleX; // if start is top to end (selection type 01) if(startC.y * scaleY < minY) startC.y = minY/scaleY; if(endC.y * scaleY > maxY) endC.y = maxY/scaleY; // if start is bottom to end (selection type 02) if(startC.y * scaleY > maxY) startC.y = maxY/scaleY; if(endC.y * scaleY < minY) endC.y = minY/scaleY; } TextList::ConstIterator it = d->m_words.constBegin(), itEnd = d->m_words.constEnd(); TextList::ConstIterator start = it, end = itEnd, tmpIt = it; //, tmpItEnd = itEnd; const MergeSide side = d->m_page ? (MergeSide)d->m_page->totalOrientation() : MergeRight; NormalizedRect tmp; //case 2(a) for ( ; it != itEnd; ++it ) { tmp = (*it)->area; if(tmp.contains(startC.x,startC.y)){ start = it; } if(tmp.contains(endC.x,endC.y)){ end = it; } } //case 2(b) it = tmpIt; if(start == it && end == itEnd) { for ( ; it != itEnd; ++it ) { // is there any text rectangle within the start_end rect tmp = (*it)->area; if(start_end.intersects(tmp)) break; } // we have searched every text entities, but none is within the rectangle created by start and end // so, no selection should be done if(it == itEnd) { return ret; } } it = tmpIt; bool selection_two_start = false; //case 3.a if(start == it) { bool flagV = false; NormalizedRect rect; // selection type 01 if(startC.y <= endC.y) { for ( ; it != itEnd; ++it ) { rect= (*it)->area; rect.isBottom(startC) ? flagV = false: flagV = true; if(flagV && rect.isRight(startC)) { start = it; break; } } } //selection type 02 else { selection_two_start = true; int distance = scaleX + scaleY + 100; int count = 0; for ( ; it != itEnd; ++it ) { rect= (*it)->area; if(rect.isBottomOrLevel(startC) && rect.isRight(startC)) { count++; QRect entRect = rect.geometry(scaleX,scaleY); int xdist, ydist; xdist = entRect.center().x() - startC.x * scaleX; ydist = entRect.center().y() - startC.y * scaleY; //make them positive if(xdist < 0) xdist = -xdist; if(ydist < 0) ydist = -ydist; if( (xdist + ydist) < distance) { distance = xdist+ ydist; start = it; } } } } } //case 3.b if(end == itEnd) { it = tmpIt; itEnd = itEnd-1; bool flagV = false; NormalizedRect rect; if(startC.y <= endC.y) { for ( ; itEnd >= it; itEnd-- ) { rect= (*itEnd)->area; rect.isTop(endC) ? flagV = false: flagV = true; if(flagV && rect.isLeft(endC)) { end = itEnd; break; } } } else { int distance = scaleX + scaleY + 100; for ( ; itEnd >= it; itEnd-- ) { rect= (*itEnd)->area; if(rect.isTopOrLevel(endC) && rect.isLeft(endC)) { QRect entRect = rect.geometry(scaleX,scaleY); int xdist, ydist; xdist = entRect.center().x() - endC.x * scaleX; ydist = entRect.center().y() - endC.y * scaleY; //make them positive if(xdist < 0) xdist = -xdist; if(ydist < 0) ydist = -ydist; if( (xdist + ydist) < distance) { distance = xdist+ ydist; end = itEnd; } } } } } /* if start and end in selection 02 are in the same column, and we start at an empty space we have to remove the selection of last character */ if(selection_two_start) { if(start > end) { start = start - 1; } } // if start is less than end swap them if(start > end) { it = start; start = end; end = it; } // removes the possibility of crash, in case none of 1 to 3 is true if(end == d->m_words.constEnd()) end--; for( ;start <= end ; start++) { ret->appendShape( (*start)->transformedArea( matrix ), side ); } #endif return ret; } RegularAreaRect* TextPage::findText( int searchID, const QString &query, SearchDirection direct, Qt::CaseSensitivity caseSensitivity, const RegularAreaRect *area ) { SearchDirection dir=direct; // invalid search request if ( d->m_words.isEmpty() || query.isEmpty() || ( area && area->isNull() ) ) return nullptr; TextList::ConstIterator start; int start_offset = 0; TextList::ConstIterator end; const QMap< int, SearchPoint* >::const_iterator sIt = d->m_searchPoints.constFind( searchID ); if ( sIt == d->m_searchPoints.constEnd() ) { // if no previous run of this search is found, then set it to start // from the beginning (respecting the search direction) if ( dir == NextResult ) dir = FromTop; else if ( dir == PreviousResult ) dir = FromBottom; } bool forward = true; switch ( dir ) { case FromTop: start = d->m_words.constBegin(); start_offset = 0; end = d->m_words.constEnd(); break; case FromBottom: start = d->m_words.constEnd(); start_offset = 0; end = d->m_words.constBegin(); forward = false; break; case NextResult: start = (*sIt)->it_end; start_offset = (*sIt)->offset_end; end = d->m_words.constEnd(); break; case PreviousResult: start = (*sIt)->it_begin; start_offset = (*sIt)->offset_begin; end = d->m_words.constBegin(); forward = false; break; }; RegularAreaRect* ret = nullptr; const TextComparisonFunction cmpFn = caseSensitivity == Qt::CaseSensitive ? CaseSensitiveCmpFn : CaseInsensitiveCmpFn; if ( forward ) { ret = d->findTextInternalForward( searchID, query, cmpFn, start, start_offset, end ); } else { ret = d->findTextInternalBackward( searchID, query, cmpFn, start, start_offset, end ); } return ret; } // hyphenated '-' must be at the end of a word, so hyphenation means // we have a '-' just followed by a '\n' character // check if the string contains a '-' character // if the '-' is the last entry static int stringLengthAdaptedWithHyphen(const QString &str, const TextList::ConstIterator &it, const TextList::ConstIterator &textListEnd) { int len = str.length(); // hyphenated '-' must be at the end of a word, so hyphenation means // we have a '-' just followed by a '\n' character // check if the string contains a '-' character // if the '-' is the last entry if ( str.endsWith( QLatin1Char('-') ) ) { // validity chek of it + 1 if ( ( it + 1 ) != textListEnd ) { // 1. if the next character is '\n' const QString &lookahedStr = (*(it+1))->text(); if (lookahedStr.startsWith(QLatin1Char('\n'))) { len -= 1; } else { // 2. if the next word is in a different line or not const NormalizedRect& hyphenArea = (*it)->area; const NormalizedRect& lookaheadArea = (*(it + 1))->area; // lookahead to check whether both the '-' rect and next character rect overlap if( !doesConsumeY( hyphenArea, lookaheadArea, 70 ) ) { len -= 1; } } } } // else if it is the second last entry - for example in pdf format else if (str.endsWith(QLatin1String("-\n"))) { len -= 2; } return len; } RegularAreaRect* TextPagePrivate::searchPointToArea(const SearchPoint* sp) { PagePrivate *pagePrivate = PagePrivate::get(m_page); const QTransform matrix = pagePrivate ? pagePrivate->rotationMatrix() : QTransform(); RegularAreaRect* ret=new RegularAreaRect; for (TextList::ConstIterator it = sp->it_begin; ; it++) { const TinyTextEntity* curEntity = *it; ret->append( curEntity->transformedArea( matrix ) ); if (it == sp->it_end) { break; } } ret->simplify(); return ret; } RegularAreaRect* TextPagePrivate::findTextInternalForward( int searchID, const QString &_query, TextComparisonFunction comparer, const TextList::ConstIterator &start, int start_offset, const TextList::ConstIterator &end) { // normalize query search all unicode (including glyphs) const QString query = _query.normalized(QString::NormalizationForm_KC); // j is the current position in our query // len is the length of the string in TextEntity // queryLeft is the length of the query we have left int j=0, queryLeft=query.length(); TextList::ConstIterator it = start; int offset = start_offset; TextList::ConstIterator it_begin = TextList::ConstIterator(); int offset_begin = 0; //dummy initial value to suppress compiler warnings while ( it != end ) { const TinyTextEntity* curEntity = *it; const QString& str = curEntity->text(); int len = stringLengthAdaptedWithHyphen(str, it, m_words.constEnd()); if (offset >= len) { it++; offset = 0; continue; } if ( it_begin == TextList::ConstIterator() ) { it_begin = it; offset_begin = offset; } int min=qMin(queryLeft,len-offset); { #ifdef DEBUG_TEXTPAGE qCDebug(OkularCoreDebug) << str.midRef(offset, min) << ":" << _query.midRef(j, min); #endif // we have equal (or less than) area of the query left as the length of the current // entity if ( !comparer( str.midRef( offset, min ), query.midRef( j, min ) ) ) { // we have not matched // this means we do not have a complete match // we need to get back to query start // and continue the search from this place #ifdef DEBUG_TEXTPAGE qCDebug(OkularCoreDebug) << "\tnot matched"; #endif j = 0; queryLeft=query.length(); it = it_begin; offset = offset_begin+1; it_begin = TextList::ConstIterator(); } else { // we have a match // move the current position in the query // to the position after the length of this string // we matched // subtract the length of the current entity from // the left length of the query #ifdef DEBUG_TEXTPAGE qCDebug(OkularCoreDebug) << "\tmatched"; #endif j += min; queryLeft -= min; if (queryLeft==0) { // save or update the search point for the current searchID QMap< int, SearchPoint* >::iterator sIt = m_searchPoints.find( searchID ); if ( sIt == m_searchPoints.end() ) { sIt = m_searchPoints.insert( searchID, new SearchPoint ); } SearchPoint* sp = *sIt; sp->it_begin = it_begin; sp->it_end = it; sp->offset_begin = offset_begin; sp->offset_end = offset + min; return searchPointToArea(sp); } it++; offset = 0; } } } // end of loop - it means that we've ended the textentities const QMap< int, SearchPoint* >::iterator sIt = m_searchPoints.find( searchID ); if ( sIt != m_searchPoints.end() ) { SearchPoint* sp = *sIt; m_searchPoints.erase( sIt ); delete sp; } return nullptr; } RegularAreaRect* TextPagePrivate::findTextInternalBackward( int searchID, const QString &_query, TextComparisonFunction comparer, const TextList::ConstIterator &start, int start_offset, const TextList::ConstIterator &end) { // normalize query to search all unicode (including glyphs) const QString query = _query.normalized(QString::NormalizationForm_KC); // j is the current position in our query // len is the length of the string in TextEntity // queryLeft is the length of the query we have left int j=query.length(), queryLeft=query.length(); TextList::ConstIterator it = start; int offset = start_offset; TextList::ConstIterator it_begin = TextList::ConstIterator(); int offset_begin = 0; //dummy initial value to suppress compiler warnings while ( true ) { if (offset <= 0) { if ( it == end ) { break; } it--; } const TinyTextEntity* curEntity = *it; const QString& str = curEntity->text(); int len = stringLengthAdaptedWithHyphen(str, it, m_words.constEnd()); if (offset <= 0) { offset = len; } if ( it_begin == TextList::ConstIterator() ) { it_begin = it; offset_begin = offset; } int min=qMin(queryLeft,offset); { #ifdef DEBUG_TEXTPAGE qCDebug(OkularCoreDebug) << str.midRef(offset-min, min) << " : " << _query.midRef(j-min, min); #endif // we have equal (or less than) area of the query left as the length of the current // entity // Note len is not str.length() so we can't use rightRef here if ( !comparer( str.midRef(offset-min, min ), query.midRef( j - min, min ) ) ) { // we have not matched // this means we do not have a complete match // we need to get back to query start // and continue the search from this place #ifdef DEBUG_TEXTPAGE qCDebug(OkularCoreDebug) << "\tnot matched"; #endif j = query.length(); queryLeft = query.length(); it = it_begin; offset = offset_begin-1; it_begin = TextList::ConstIterator(); } else { // we have a match // move the current position in the query // to the position after the length of this string // we matched // subtract the length of the current entity from // the left length of the query #ifdef DEBUG_TEXTPAGE qCDebug(OkularCoreDebug) << "\tmatched"; #endif j -= min; queryLeft -= min; if ( queryLeft == 0 ) { // save or update the search point for the current searchID QMap< int, SearchPoint* >::iterator sIt = m_searchPoints.find( searchID ); if ( sIt == m_searchPoints.end() ) { sIt = m_searchPoints.insert( searchID, new SearchPoint ); } SearchPoint* sp = *sIt; sp->it_begin = it; sp->it_end = it_begin; sp->offset_begin = offset - min; sp->offset_end = offset_begin; return searchPointToArea(sp); } offset = 0; } } } // end of loop - it means that we've ended the textentities const QMap< int, SearchPoint* >::iterator sIt = m_searchPoints.find( searchID ); if ( sIt != m_searchPoints.end() ) { SearchPoint* sp = *sIt; m_searchPoints.erase( sIt ); delete sp; } return nullptr; } QString TextPage::text(const RegularAreaRect *area) const { return text(area, AnyPixelTextAreaInclusionBehaviour); } QString TextPage::text(const RegularAreaRect *area, TextAreaInclusionBehaviour b) const { if ( area && area->isNull() ) return QString(); TextList::ConstIterator it = d->m_words.constBegin(), itEnd = d->m_words.constEnd(); QString ret; if ( area ) { for ( ; it != itEnd; ++it ) { if (b == AnyPixelTextAreaInclusionBehaviour) { if ( area->intersects( (*it)->area ) ) { ret += (*it)->text(); } } else { NormalizedPoint center = (*it)->area.center(); if ( area->contains( center.x, center.y ) ) { ret += (*it)->text(); } } } } else { for ( ; it != itEnd; ++it ) ret += (*it)->text(); } return ret; } static bool compareTinyTextEntityX(const WordWithCharacters &first, const WordWithCharacters &second) { QRect firstArea = first.area().roundedGeometry(1000,1000); QRect secondArea = second.area().roundedGeometry(1000,1000); return firstArea.left() < secondArea.left(); } static bool compareTinyTextEntityY(const WordWithCharacters &first, const WordWithCharacters &second) { const QRect firstArea = first.area().roundedGeometry(1000,1000); const QRect secondArea = second.area().roundedGeometry(1000,1000); return firstArea.top() < secondArea.top(); } /** * Sets a new world list. Deleting the contents of the old one */ void TextPagePrivate::setWordList(const TextList &list) { qDeleteAll(m_words); m_words = list; } /** * Remove all the spaces in between texts. It will make all the generators * same, whether they save spaces(like pdf) or not(like djvu). */ static void removeSpace(TextList *words) { TextList::Iterator it = words->begin(); const QString str(QLatin1Char(' ')); while ( it != words->end() ) { if((*it)->text() == str) { it = words->erase(it); } else { ++it; } } } /** * We will read the TinyTextEntity from characters and try to create words from there. * Note: characters might be already characters for some generators, but we will keep * the nomenclature characters for the generator produced data. The resulting * WordsWithCharacters memory has to be managed by the caller, both the * WordWithCharacters::word and WordWithCharacters::characters contents */ static WordsWithCharacters makeWordFromCharacters(const TextList &characters, int pageWidth, int pageHeight) { /** * We will traverse characters and try to create words from the TinyTextEntities in it. * We will search TinyTextEntity blocks and merge them until we get a * space between two consecutive TinyTextEntities. When we get a space * we can take it as a end of word. Then we store the word as a TinyTextEntity * and keep it in newList. * We create a RegionText named regionWord that contains the word and the characters associated with it and * a rectangle area of the element in newList. */ WordsWithCharacters wordsWithCharacters; TextList::ConstIterator it = characters.begin(), itEnd = characters.end(), tmpIt; int newLeft,newRight,newTop,newBottom; int index = 0; for( ; it != itEnd ; it++) { QString textString = (*it)->text(); QString newString; QRect lineArea = (*it)->area.roundedGeometry(pageWidth,pageHeight),elementArea; TextList wordCharacters; tmpIt = it; int space = 0; while (!space) { if (textString.length()) { newString.append(textString); // when textString is the start of the word if (tmpIt == it) { NormalizedRect newRect(lineArea,pageWidth,pageHeight); wordCharacters.append(new TinyTextEntity(textString.normalized (QString::NormalizationForm_KC), newRect)); } else { NormalizedRect newRect(elementArea,pageWidth,pageHeight); wordCharacters.append(new TinyTextEntity(textString.normalized (QString::NormalizationForm_KC), newRect)); } } ++it; /* we must have to put this line before the if condition of it==itEnd otherwise the last character can be missed */ if (it == itEnd) break; elementArea = (*it)->area.roundedGeometry(pageWidth,pageHeight); if (!doesConsumeY(elementArea, lineArea, 60)) { --it; break; } const int text_y1 = elementArea.top() , text_x1 = elementArea.left(), text_y2 = elementArea.y() + elementArea.height(), text_x2 = elementArea.x() + elementArea.width(); const int line_y1 = lineArea.top() ,line_x1 = lineArea.left(), line_y2 = lineArea.y() + lineArea.height(), line_x2 = lineArea.x() + lineArea.width(); space = elementArea.left() - lineArea.right(); if (space != 0) { it--; break; } newLeft = text_x1 < line_x1 ? text_x1 : line_x1; newRight = line_x2 > text_x2 ? line_x2 : text_x2; newTop = text_y1 > line_y1 ? line_y1 : text_y1; newBottom = text_y2 > line_y2 ? text_y2 : line_y2; lineArea.setLeft (newLeft); lineArea.setTop (newTop); lineArea.setWidth( newRight - newLeft ); lineArea.setHeight( newBottom - newTop ); textString = (*it)->text(); } // if newString is not empty, save it if (!newString.isEmpty()) { const NormalizedRect newRect(lineArea, pageWidth, pageHeight); TinyTextEntity *word = new TinyTextEntity(newString.normalized(QString::NormalizationForm_KC), newRect); wordsWithCharacters.append(WordWithCharacters(word, wordCharacters)); index++; } if(it == itEnd) break; } return wordsWithCharacters; } /** * Create Lines from the words and sort them */ QList< QPair > makeAndSortLines(const WordsWithCharacters &wordsTmp, int pageWidth, int pageHeight) { /** * We cannot assume that the generator will give us texts in the right order. * We can only assume that we will get texts in the page and their bounding * rectangle. The texts can be character, word, half-word anything. * So, we need to: ** * 1. Sort rectangles/boxes containing texts by y0(top) * 2. Create textline where there is y overlap between TinyTextEntity 's * 3. Within each line sort the TinyTextEntity 's by x0(left) */ QList< QPair > lines; /* Make a new copy of the TextList in the words, so that the wordsTmp and lines do not contain same pointers for all the TinyTextEntity. */ QList words = wordsTmp; // Step 1 std::sort(words.begin(),words.end(), compareTinyTextEntityY); // Step 2 QList::Iterator it = words.begin(), itEnd = words.end(); //for every non-space texts(characters/words) in the textList for( ; it != itEnd ; it++) { const QRect elementArea = (*it).area().roundedGeometry(pageWidth,pageHeight); bool found = false; - for( int i = 0 ; i < lines.length() ; i++) + for( QPair &linesI : lines) { /* the line area which will be expanded line_rects is only necessary to preserve the topmin and bottommax of all the texts in the line, left and right is not necessary at all */ - QRect &lineArea = lines[i].second; + QRect &lineArea = linesI.second; const int text_y1 = elementArea.top() , text_y2 = elementArea.top() + elementArea.height() , text_x1 = elementArea.left(), text_x2 = elementArea.left() + elementArea.width(); const int line_y1 = lineArea.top() , line_y2 = lineArea.top() + lineArea.height(), line_x1 = lineArea.left(), line_x2 = lineArea.left() + lineArea.width(); /* if the new text and the line has y overlapping parts of more than 70%, the text will be added to this line */ if(doesConsumeY(elementArea,lineArea,70)) { - WordsWithCharacters &line = lines[i].first; + WordsWithCharacters &line = linesI.first; line.append(*it); const int newLeft = line_x1 < text_x1 ? line_x1 : text_x1; const int newRight = line_x2 > text_x2 ? line_x2 : text_x2; const int newTop = line_y1 < text_y1 ? line_y1 : text_y1; const int newBottom = text_y2 > line_y2 ? text_y2 : line_y2; lineArea = QRect( newLeft,newTop, newRight - newLeft, newBottom - newTop ); found = true; } if(found) break; } /* when we have found a new line create a new TextList containing only one element and append it to the lines */ if(!found) { WordsWithCharacters tmp; tmp.append((*it)); lines.append(QPair(tmp, elementArea)); } } // Step 3 - for(int i = 0 ; i < lines.length() ; i++) + for(QPair &line : lines) { - WordsWithCharacters &list = lines[i].first; + WordsWithCharacters &list = line.first; std::sort(list.begin(), list.end(), compareTinyTextEntityX); } return lines; } /** * Calculate Statistical information from the lines we made previously */ static void calculateStatisticalInformation(const QList &words, int pageWidth, int pageHeight, int *word_spacing, int *line_spacing, int *col_spacing) { /** * For the region, defined by line_rects and lines * 1. Make line statistical analysis to find the line spacing * 2. Make character statistical analysis to differentiate between * word spacing and column spacing. */ /** * Step 0 */ const QList< QPair > sortedLines = makeAndSortLines(words, pageWidth, pageHeight); /** * Step 1 */ QMap line_space_stat; for(int i = 0 ; i < sortedLines.length(); i++) { const QRect rectUpper = sortedLines.at(i).second; if(i+1 == sortedLines.length()) break; const QRect rectLower = sortedLines.at(i+1).second; int linespace = rectLower.top() - (rectUpper.top() + rectUpper.height()); if(linespace < 0) linespace =-linespace; if(line_space_stat.contains(linespace)) line_space_stat[linespace]++; else line_space_stat[linespace] = 1; } *line_spacing = 0; int weighted_count = 0; QMapIterator iterate_linespace(line_space_stat); while(iterate_linespace.hasNext()) { iterate_linespace.next(); *line_spacing += iterate_linespace.value() * iterate_linespace.key(); weighted_count += iterate_linespace.value(); } if (*line_spacing != 0) *line_spacing = (int) ( (double)*line_spacing / (double) weighted_count + 0.5); /** * Step 2 */ // We would like to use QMap instead of QHash as it will keep the keys sorted QMap hor_space_stat; QMap col_space_stat; QList< QList > space_rects; QList max_hor_space_rects; // Space in every line - for(int i = 0 ; i < sortedLines.length() ; i++) + for(const QPair &sortedLine : sortedLines) { - const WordsWithCharacters list = sortedLines.at(i).first; + const WordsWithCharacters list = sortedLine.first; QList line_space_rects; int maxSpace = 0, minSpace = pageWidth; // for every TinyTextEntity element in the line WordsWithCharacters::ConstIterator it = list.begin(), itEnd = list.end(); QRect max_area1,max_area2; QString before_max, after_max; // for every line for( ; it != itEnd ; it++ ) { const QRect area1 = (*it).area().roundedGeometry(pageWidth,pageHeight); if( it+1 == itEnd ) break; const QRect area2 = (*(it+1)).area().roundedGeometry(pageWidth,pageHeight); int space = area2.left() - area1.right(); if(space > maxSpace) { max_area1 = area1; max_area2 = area2; maxSpace = space; before_max = (*it).text(); after_max = (*(it+1)).text(); } if(space < minSpace && space != 0) minSpace = space; //if we found a real space, whose length is not zero and also less than the pageWidth if(space != 0 && space != pageWidth) { // increase the count of the space amount if(hor_space_stat.contains(space)) hor_space_stat[space]++; else hor_space_stat[space] = 1; int left,right,top,bottom; left = area1.right(); right = area2.left(); top = area2.top() < area1.top() ? area2.top() : area1.top(); bottom = area2.bottom() > area1.bottom() ? area2.bottom() : area1.bottom(); QRect rect(left,top,right-left,bottom-top); line_space_rects.append(rect); } } space_rects.append(line_space_rects); if(hor_space_stat.contains(maxSpace)) { if(hor_space_stat[maxSpace] != 1) hor_space_stat[maxSpace]--; else hor_space_stat.remove(maxSpace); } if(maxSpace != 0) { if (col_space_stat.contains(maxSpace)) col_space_stat[maxSpace]++; else col_space_stat[maxSpace] = 1; //store the max rect of each line const int left = max_area1.right(); const int right = max_area2.left(); const int top = (max_area1.top() > max_area2.top()) ? max_area2.top() : max_area1.top(); const int bottom = (max_area1.bottom() < max_area2.bottom()) ? max_area2.bottom() : max_area1.bottom(); const QRect rect(left,top,right-left,bottom-top); max_hor_space_rects.append(rect); } else max_hor_space_rects.append(QRect(0,0,0,0)); } // All the between word space counts are in hor_space_stat *word_spacing = 0; weighted_count = 0; QMapIterator iterate(hor_space_stat); while (iterate.hasNext()) { iterate.next(); if(iterate.key() > 0) { *word_spacing += iterate.value() * iterate.key(); weighted_count += iterate.value(); } } if(weighted_count) *word_spacing = (int) ((double)*word_spacing / (double)weighted_count + 0.5); *col_spacing = 0; QMapIterator iterate_col(col_space_stat); while (iterate_col.hasNext()) { iterate_col.next(); if(iterate_col.value() > *col_spacing) *col_spacing = iterate_col.value(); } *col_spacing = col_space_stat.key(*col_spacing); // if there is just one line in a region, there is no point in dividing it if(sortedLines.length() == 1) *word_spacing = *col_spacing; } /** * Implements the XY Cut algorithm for textpage segmentation * The resulting RegionTextList will contain RegionText whose WordsWithCharacters::word and * WordsWithCharacters::characters are reused from wordsWithCharacters (i.e. no new nor delete happens in this function) */ static RegionTextList XYCutForBoundingBoxes(const QList &wordsWithCharacters, const NormalizedRect &boundingBox, int pageWidth, int pageHeight) { RegionTextList tree; QRect contentRect(boundingBox.geometry(pageWidth,pageHeight)); const RegionText root(wordsWithCharacters, contentRect); // start the tree with the root, it is our only region at the start tree.push_back(root); int i = 0; // while traversing the tree has not been ended while(i < tree.length()) { const RegionText node = tree.at(i); QRect regionRect = node.area(); /** * 1. calculation of projection profiles */ // allocate the size of proj profiles and initialize with 0 int size_proj_y = node.area().height(); int size_proj_x = node.area().width(); //dynamic memory allocation QVarLengthArray proj_on_xaxis(size_proj_x); QVarLengthArray proj_on_yaxis(size_proj_y); for( int j = 0 ; j < size_proj_y ; ++j ) proj_on_yaxis[j] = 0; for( int j = 0 ; j < size_proj_x ; ++j ) proj_on_xaxis[j] = 0; const QList list = node.text(); // Calculate tcx and tcy locally for each new region int word_spacing, line_spacing, column_spacing; calculateStatisticalInformation(list, pageWidth, pageHeight, &word_spacing, &line_spacing, &column_spacing); const int tcx = word_spacing * 2; const int tcy = line_spacing * 2; int maxX = 0 , maxY = 0; int avgX = 0; int count; // for every text in the region - for(int j = 0 ; j < list.length() ; ++j ) + for( const WordWithCharacters &wwc : list ) { - TinyTextEntity *ent = list.at(j).word; + TinyTextEntity *ent = wwc.word; const QRect entRect = ent->area.geometry(pageWidth, pageHeight); // calculate vertical projection profile proj_on_xaxis1 for(int k = entRect.left() ; k <= entRect.left() + entRect.width() ; ++k) { if( ( k-regionRect.left() ) < size_proj_x && ( k-regionRect.left() ) >= 0 ) proj_on_xaxis[k - regionRect.left()] += entRect.height(); } // calculate horizontal projection profile in the same way for(int k = entRect.top() ; k <= entRect.top() + entRect.height() ; ++k) { if( ( k-regionRect.top() ) < size_proj_y && ( k-regionRect.top() ) >= 0 ) proj_on_yaxis[k - regionRect.top()] += entRect.width(); } } for( int j = 0 ; j < size_proj_y ; ++j ) { if (proj_on_yaxis[j] > maxY) maxY = proj_on_yaxis[j]; } avgX = count = 0; for( int j = 0 ; j < size_proj_x ; ++j ) { if(proj_on_xaxis[j] > maxX) maxX = proj_on_xaxis[j]; if(proj_on_xaxis[j]) { count++; avgX+= proj_on_xaxis[j]; } } if(count) avgX /= count; /** * 2. Cleanup Boundary White Spaces and removal of noise */ int xbegin = 0, xend = size_proj_x - 1; int ybegin = 0, yend = size_proj_y - 1; while(xbegin < size_proj_x && proj_on_xaxis[xbegin] <= 0) xbegin++; while(xend >= 0 && proj_on_xaxis[xend] <= 0) xend--; while(ybegin < size_proj_y && proj_on_yaxis[ybegin] <= 0) ybegin++; while(yend >= 0 && proj_on_yaxis[yend] <= 0) yend--; //update the regionRect int old_left = regionRect.left(), old_top = regionRect.top(); regionRect.setLeft(old_left + xbegin); regionRect.setRight(old_left + xend); regionRect.setTop(old_top + ybegin); regionRect.setBottom(old_top + yend); int tnx = (int)((double)avgX * 10.0 / 100.0 + 0.5), tny = 0; for( int j = 0 ; j < size_proj_x ; ++j ) proj_on_xaxis[j] -= tnx; for( int j = 0 ; j < size_proj_y ; ++j ) proj_on_yaxis[j] -= tny; /** * 3. Find the Widest gap */ int gap_hor = -1, pos_hor = -1; int begin = -1, end = -1; // find all hor_gaps and find the maximum between them for(int j = 1 ; j < size_proj_y ; ++j) { //transition from white to black if(begin >= 0 && proj_on_yaxis[j-1] <= 0 && proj_on_yaxis[j] > 0) end = j; //transition from black to white if(proj_on_yaxis[j-1] > 0 && proj_on_yaxis[j] <= 0) begin = j; if(begin > 0 && end > 0 && end-begin > gap_hor) { gap_hor = end - begin; pos_hor = (end + begin) / 2; begin = -1; end = -1; } } begin = -1, end = -1; int gap_ver = -1, pos_ver = -1; //find all the ver_gaps and find the maximum between them for(int j = 1 ; j < size_proj_x ; ++j) { //transition from white to black if(begin >= 0 && proj_on_xaxis[j-1] <= 0 && proj_on_xaxis[j] > 0){ end = j; } //transition from black to white if(proj_on_xaxis[j-1] > 0 && proj_on_xaxis[j] <= 0) begin = j; if(begin > 0 && end > 0 && end-begin > gap_ver) { gap_ver = end - begin; pos_ver = (end + begin) / 2; begin = -1; end = -1; } } int cut_pos_x = pos_ver, cut_pos_y = pos_hor; int gap_x = gap_ver, gap_y = gap_hor; /** * 4. Cut the region and make nodes (left,right) or (up,down) */ bool cut_hor = false, cut_ver = false; // For horizontal cut const int topHeight = cut_pos_y - (regionRect.top() - old_top); const QRect topRect(regionRect.left(), regionRect.top(), regionRect.width(), topHeight); const QRect bottomRect(regionRect.left(), regionRect.top() + topHeight, regionRect.width(), regionRect.height() - topHeight ); // For vertical Cut const int leftWidth = cut_pos_x - (regionRect.left() - old_left); const QRect leftRect(regionRect.left(), regionRect.top(), leftWidth, regionRect.height()); const QRect rightRect(regionRect.left() + leftWidth, regionRect.top(), regionRect.width() - leftWidth, regionRect.height()); if(gap_y >= gap_x && gap_y >= tcy) cut_hor = true; else if(gap_y >= gap_x && gap_y <= tcy && gap_x >= tcx) cut_ver = true; else if(gap_x >= gap_y && gap_x >= tcx) cut_ver = true; else if(gap_x >= gap_y && gap_x <= tcx && gap_y >= tcy) cut_hor = true; // no cut possible else { // we can now update the node rectangle with the shrinked rectangle RegionText tmpNode = tree.at(i); tmpNode.setArea(regionRect); tree.replace(i,tmpNode); i++; continue; } WordsWithCharacters list1,list2; // horizontal cut, topRect and bottomRect if(cut_hor) { - for( int j = 0 ; j < list.length() ; ++j ) + for( const WordWithCharacters &word : list ) { - const WordWithCharacters &word = list.at(j); const QRect wordRect = word.area().geometry(pageWidth,pageHeight); if(topRect.intersects(wordRect)) list1.append(word); else list2.append(word); } RegionText node1(list1,topRect); RegionText node2(list2,bottomRect); tree.replace(i,node1); tree.insert(i+1,node2); } //vertical cut, leftRect and rightRect else if(cut_ver) { - for( int j = 0 ; j < list.length() ; ++j ) + for( const WordWithCharacters &word : list ) { - const WordWithCharacters &word = list.at(j); const QRect wordRect = word.area().geometry(pageWidth,pageHeight); if(leftRect.intersects(wordRect)) list1.append(word); else list2.append(word); } RegionText node1(list1,leftRect); RegionText node2(list2,rightRect); tree.replace(i,node1); tree.insert(i+1,node2); } } return tree; } /** * Add spaces in between words in a line. It reuses the pointers passed in tree and might add new ones. You will need to take care of deleting them if needed */ WordsWithCharacters addNecessarySpace(RegionTextList tree, int pageWidth, int pageHeight) { /** * 1. Call makeAndSortLines before adding spaces in between words in a line * 2. Now add spaces between every two words in a line * 3. Finally, extract all the space separated texts from each region and return it */ // Only change the texts under RegionTexts, not the area - for(int j = 0 ; j < tree.length() ; j++) + for(RegionText &tmpRegion : tree) { - RegionText &tmpRegion = tree[j]; - // Step 01 QList< QPair > sortedLines = makeAndSortLines(tmpRegion.text(), pageWidth, pageHeight); // Step 02 - for(int i = 0 ; i < sortedLines.length() ; i++) + for(QPair &sortedLine : sortedLines) { - WordsWithCharacters &list = sortedLines[i].first; + WordsWithCharacters &list = sortedLine.first; for(int k = 0 ; k < list.length() ; k++ ) { const QRect area1 = list.at(k).area().roundedGeometry(pageWidth,pageHeight); if( k+1 >= list.length() ) break; const QRect area2 = list.at(k+1).area().roundedGeometry(pageWidth,pageHeight); const int space = area2.left() - area1.right(); if(space != 0) { // Make a TinyTextEntity of string space and push it between it and it+1 const int left = area1.right(); const int right = area2.left(); const int top = area2.top() < area1.top() ? area2.top() : area1.top(); const int bottom = area2.bottom() > area1.bottom() ? area2.bottom() : area1.bottom(); const QString spaceStr(QStringLiteral(" ")); const QRect rect(QPoint(left,top),QPoint(right,bottom)); const NormalizedRect entRect(rect,pageWidth,pageHeight); TinyTextEntity *ent1 = new TinyTextEntity(spaceStr, entRect); TinyTextEntity *ent2 = new TinyTextEntity(spaceStr, entRect); WordWithCharacters word(ent1, QList() << ent2); list.insert(k+1, word); // Skip the space k++; } } } WordsWithCharacters tmpList; - for(int i = 0 ; i < sortedLines.length() ; i++) + for(const QPair &sortedLine : qAsConst(sortedLines)) { - tmpList += sortedLines.at(i).first; + tmpList += sortedLine.first; } tmpRegion.setText(tmpList); } // Step 03 WordsWithCharacters tmp; - for(int i = 0 ; i < tree.length() ; i++) + for(const RegionText &tmpRegion : qAsConst(tree)) { - tmp += tree.at(i).text(); + tmp += tmpRegion.text(); } return tmp; } /** * Correct the textOrder, all layout recognition works here */ void TextPagePrivate::correctTextOrder() { // m_page->width() and m_page->height() are in pixels at // 100% zoom level, and thus depend on display DPI. // To avoid Okular failing on lowDPI displays, // we scale pageWidth and pageHeight so their sum equals 2000. const double scalingFactor = 2000.0 / (m_page->width() + m_page->height()); const int pageWidth = (int) (scalingFactor * m_page->width() ); const int pageHeight = (int) (scalingFactor * m_page->height()); TextList characters = m_words; /** * Remove spaces from the text */ removeSpace(&characters); /** * Construct words from characters */ const QList wordsWithCharacters = makeWordFromCharacters(characters, pageWidth, pageHeight); /** * Make a XY Cut tree for segmentation of the texts */ const RegionTextList tree = XYCutForBoundingBoxes(wordsWithCharacters, m_page->boundingBox(), pageWidth, pageHeight); /** * Add spaces to the word */ const WordsWithCharacters listWithWordsAndSpaces = addNecessarySpace(tree, pageWidth, pageHeight); /** * Break the words into characters */ TextList listOfCharacters; for (const WordWithCharacters &word : listWithWordsAndSpaces) { delete word.word; listOfCharacters.append(word.characters); } setWordList(listOfCharacters); } TextEntity::List TextPage::words(const RegularAreaRect *area, TextAreaInclusionBehaviour b) const { if ( area && area->isNull() ) return TextEntity::List(); TextEntity::List ret; if ( area ) { for (const TinyTextEntity *te : qAsConst(d->m_words)) { if (b == AnyPixelTextAreaInclusionBehaviour) { if ( area->intersects( te->area ) ) { ret.append( new TextEntity( te->text(), new Okular::NormalizedRect( te->area) ) ); } } else { const NormalizedPoint center = te->area.center(); if ( area->contains( center.x, center.y ) ) { ret.append( new TextEntity( te->text(), new Okular::NormalizedRect( te->area) ) ); } } } } else { for (const TinyTextEntity *te : qAsConst(d->m_words)) { ret.append( new TextEntity( te->text(), new Okular::NormalizedRect( te->area) ) ); } } return ret; } RegularAreaRect * TextPage::wordAt( const NormalizedPoint &p, QString *word ) const { TextList::ConstIterator itBegin = d->m_words.constBegin(), itEnd = d->m_words.constEnd(); TextList::ConstIterator it = itBegin; TextList::ConstIterator posIt = itEnd; for ( ; it != itEnd; ++it ) { if ( (*it)->area.contains( p.x, p.y ) ) { posIt = it; break; } } QString text; if ( posIt != itEnd ) { if ( (*posIt)->text().simplified().isEmpty() ) { return nullptr; } // Find the first TinyTextEntity of the word while ( posIt != itBegin ) { --posIt; const QString itText = (*posIt)->text(); if ( itText.right(1).at(0).isSpace() ) { if (itText.endsWith(QLatin1String("-\n"))) { // Is an hyphenated word // continue searching the start of the word back continue; } if (itText == QLatin1String("\n") && posIt != itBegin ) { --posIt; if ((*posIt)->text().endsWith(QLatin1String("-"))) { // Is an hyphenated word // continue searching the start of the word back continue; } ++posIt; } ++posIt; break; } } RegularAreaRect *ret = new RegularAreaRect(); for ( ; posIt != itEnd; ++posIt ) { const QString itText = (*posIt)->text(); if ( itText.simplified().isEmpty() ) { break; } ret->appendShape( (*posIt)->area ); text += (*posIt)->text(); if (itText.right(1).at(0).isSpace()) { if (!text.endsWith(QLatin1String("-\n"))) { break; } } } if (word) { *word = text; } return ret; } else { return nullptr; } } diff --git a/core/tilesmanager.cpp b/core/tilesmanager.cpp index a2d798e47..8308bacc8 100644 --- a/core/tilesmanager.cpp +++ b/core/tilesmanager.cpp @@ -1,727 +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, 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 ] ); + for ( const TileNode &tile : d->tiles ) + d->deleteTiles( tile ); 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 ) + for ( TileNode &tile : d->tiles ) { - TilesManager::Private::markDirty( d->tiles[ i ] ); + TilesManager::Private::markDirty( tile ); } } 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, bool isPartialPixmap ) { const NormalizedRect rotatedRect = TilesManager::fromRotatedRect( rect, d->rotation ); if ( !d->requestRect.isNull() ) { if ( !(d->requestRect == rect) ) return; if ( pixmap ) { // 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; } d->requestRect = NormalizedRect(); } - for ( int i = 0; i < 16; ++i ) + for ( TileNode &tile : d->tiles ) { - d->setPixmap( pixmap, rotatedRect, d->tiles[ i ], isPartialPixmap ); + d->setPixmap( pixmap, rotatedRect, tile, isPartialPixmap ); } } 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 ], isPartialPixmap ); delete tile.pixmap; tile.pixmap = nullptr; } return; } // the tile lies entirely within the viewport if ( tile.nTiles == 0 ) { 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; } tile.rotation = rotation; 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 ], 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 = 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 ], 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; } tile.rotation = rotation; 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 ) + for ( const TileNode &tile : qAsConst( d->tiles ) ) { - if ( !d->hasPixmap( rotatedRect, d->tiles[ i ] ) ) + if ( !d->hasPixmap( rotatedRect, tile ) ) return false; } return true; } bool TilesManager::Private::hasPixmap( const NormalizedRect &rect, const TileNode &tile ) const { 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 ) + for ( TileNode &tile : d->tiles ) { - d->tilesAt( rotatedRect, d->tiles[ i ], result, tileLeaf ); + d->tilesAt( rotatedRect, tile, 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 ) + for ( TileNode &tile : d->tiles ) { - d->rankTiles( d->tiles[ i ], rankedTiles, visibleRect, visiblePageNumber ); + d->rankTiles( tile, rankedTiles, visibleRect, visiblePageNumber ); } std::sort(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/generators/chm/kio-msits/msits.cpp b/generators/chm/kio-msits/msits.cpp index 1f92490df..1e9fea64c 100644 --- a/generators/chm/kio-msits/msits.cpp +++ b/generators/chm/kio-msits/msits.cpp @@ -1,312 +1,312 @@ /* This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include #include #include #include #include "kio_mits_debug.h" #include #include #include #include #include #include #include #include "msits.h" #include "libchmurlfactory.h" using namespace KIO; extern "C" { int Q_DECL_EXPORT kdemain( int argc, char **argv ) { qCDebug(KIO_MITS_LOG) << "*** kio_msits Init"; QCoreApplication app(argc, argv); app.setApplicationName(QStringLiteral("kio_msits")); if ( argc != 4 ) { qCDebug(KIO_MITS_LOG) << "Usage: kio_msits protocol domain-socket1 domain-socket2"; exit (-1); } ProtocolMSITS slave ( argv[2], argv[3] ); slave.dispatchLoop(); qCDebug(KIO_MITS_LOG) << "*** kio_msits Done"; return 0; } } ProtocolMSITS::ProtocolMSITS (const QByteArray &pool_socket, const QByteArray &app_socket) : SlaveBase ("kio_msits", pool_socket, app_socket) { m_chmFile = 0; } ProtocolMSITS::~ProtocolMSITS() { if ( !m_chmFile ) return; chm_close (m_chmFile); m_chmFile = 0; } // A simple stat() wrapper static bool isDirectory ( const QString & filename ) { return filename.endsWith( QLatin1Char('/') ); } void ProtocolMSITS::get( const QUrl& url ) { QString htmdata, fileName; chmUnitInfo ui; QByteArray buf; qCDebug(KIO_MITS_LOG) << "kio_msits::get() " << url.path(); if ( !parseLoadAndLookup ( url, fileName ) ) return; // error() has been called by parseLoadAndLookup qCDebug(KIO_MITS_LOG) << "kio_msits::get: parseLoadAndLookup returned " << fileName; if ( LCHMUrlFactory::handleFileType( url.path(), htmdata ) ) { buf = htmdata.toUtf8(); qCDebug(KIO_MITS_LOG) << "Using special handling for image pages: " << htmdata; } else { if ( isDirectory (fileName) ) { error( KIO::ERR_IS_DIRECTORY, url.toString() ); return; } if ( !ResolveObject ( fileName, &ui) ) { qCDebug(KIO_MITS_LOG) << "kio_msits::get: could not resolve filename " << fileName; error( KIO::ERR_DOES_NOT_EXIST, url.toString() ); return; } buf.resize( ui.length ); if ( RetrieveObject (&ui, (unsigned char*) buf.data(), 0, ui.length) == 0 ) { qCDebug(KIO_MITS_LOG) << "kio_msits::get: could not retrieve filename " << fileName; error( KIO::ERR_NO_CONTENT, url.toString() ); return; } } totalSize( buf.size() ); QMimeDatabase db; QMimeType result = db.mimeTypeForFileNameAndData( fileName, buf ); qCDebug(KIO_MITS_LOG) << "Emitting mimetype " << result.name(); mimeType( result.name() ); data( buf ); processedSize( buf.size() ); finished(); } bool ProtocolMSITS::parseLoadAndLookup ( const QUrl& url, QString& abspath ) { qCDebug(KIO_MITS_LOG) << "ProtocolMSITS::parseLoadAndLookup (const KUrl&) " << url.path(); int pos = url.path().indexOf (QStringLiteral("::")); if ( pos == -1 ) { error( KIO::ERR_MALFORMED_URL, url.toString() ); return false; } QString filename = url.path().left (pos); abspath = url.path().mid (pos + 2); // skip :: // Some buggy apps add ms-its:/ to the path as well if ( abspath.startsWith( QLatin1String("ms-its:") ) ) abspath = abspath.mid( 7 ); qCDebug(KIO_MITS_LOG) << "ProtocolMSITS::parseLoadAndLookup: filename " << filename << ", path " << abspath; if ( filename.isEmpty() ) { error( KIO::ERR_MALFORMED_URL, url.toString() ); return false; } // If the file has been already loaded, nothing to do. if ( m_chmFile && filename == m_openedFile ) return true; qCDebug(KIO_MITS_LOG) << "Opening a new CHM file " << QFile::encodeName( QDir::toNativeSeparators( filename ) ); // First try to open a temporary file chmFile * tmpchm; if( (tmpchm = chm_open ( QFile::encodeName( QDir::toNativeSeparators( filename) ).constData() ) ) == 0 ) { error( KIO::ERR_COULD_NOT_READ, url.toString() ); return false; } // Replace an existing file by a new one if ( m_chmFile ) chm_close (m_chmFile); m_chmFile = tmpchm; m_openedFile = filename; qCDebug(KIO_MITS_LOG) << "A CHM file " << filename << " has beed opened successfully"; return true; } /* * Shamelessly stolen from a KDE KIO tutorial */ static void app_entry(UDSEntry& e, unsigned int uds, const QString& str) { e.insert(uds, str); } // appends an int with the UDS-ID uds static void app_entry(UDSEntry& e, unsigned int uds, long l) { e.insert(uds, l); } // internal function // fills a directory item with its name and size static void app_dir(UDSEntry& e, const QString & name) { e.clear(); app_entry(e, KIO::UDSEntry::UDS_NAME, name); app_entry(e, KIO::UDSEntry::UDS_FILE_TYPE, S_IFDIR); app_entry(e, KIO::UDSEntry::UDS_SIZE, 1); } // internal function // fills a file item with its name and size static void app_file(UDSEntry& e, const QString & name, size_t size) { e.clear(); app_entry(e, KIO::UDSEntry::UDS_NAME, name); app_entry(e, KIO::UDSEntry::UDS_FILE_TYPE, S_IFREG); app_entry(e, KIO::UDSEntry::UDS_SIZE, size); } void ProtocolMSITS::stat (const QUrl &url) { QString fileName; chmUnitInfo ui; qCDebug(KIO_MITS_LOG) << "kio_msits::stat (const KUrl& url) " << url.path(); if ( !parseLoadAndLookup ( url, fileName ) ) return; // error() has been called by parseLoadAndLookup if ( !ResolveObject ( fileName, &ui ) ) { error( KIO::ERR_DOES_NOT_EXIST, url.toString() ); return; } qCDebug(KIO_MITS_LOG) << "kio_msits::stat: adding an entry for " << fileName; UDSEntry entry; if ( isDirectory ( fileName ) ) app_dir(entry, fileName); else app_file(entry, fileName, ui.length); statEntry (entry); finished(); } // A local CHMLIB enumerator static int chmlib_enumerator (struct chmFile *, struct chmUnitInfo *ui, void *context) { ((QVector *) context)->push_back (QString::fromLocal8Bit (ui->path)); return CHM_ENUMERATOR_CONTINUE; } void ProtocolMSITS::listDir (const QUrl & url) { QString filepath; qCDebug(KIO_MITS_LOG) << "kio_msits::listDir (const KUrl& url) " << url.path(); if ( !parseLoadAndLookup ( url, filepath ) ) return; // error() has been called by parseLoadAndLookup filepath += QLatin1Char('/'); if ( !isDirectory (filepath) ) { error(KIO::ERR_CANNOT_ENTER_DIRECTORY, url.path()); return; } qCDebug(KIO_MITS_LOG) << "kio_msits::listDir: enumerating directory " << filepath; QVector listing; if ( chm_enumerate_dir ( m_chmFile, filepath.toLocal8Bit().constData(), CHM_ENUMERATE_NORMAL | CHM_ENUMERATE_FILES | CHM_ENUMERATE_DIRS, chmlib_enumerator, &listing ) != 1 ) { error(KIO::ERR_CANNOT_ENTER_DIRECTORY, url.path()); return; } UDSEntry entry; int striplength = filepath.length(); - for ( int i = 0; i < listing.size(); i++ ) + for ( const QString &iListing : qAsConst(listing) ) { // Strip the directory name - QString ename = listing[i].mid (striplength); + const QString ename = iListing.mid (striplength); if ( isDirectory ( ename ) ) app_dir(entry, ename); else app_file(entry, ename, 0); } finished(); } diff --git a/generators/chm/lib/ebook_search.cpp b/generators/chm/lib/ebook_search.cpp index 9eeec3f76..5960862ac 100644 --- a/generators/chm/lib/ebook_search.cpp +++ b/generators/chm/lib/ebook_search.cpp @@ -1,225 +1,225 @@ /* * Kchmviewer - a CHM and EPUB file viewer with broad language support * Copyright (C) 2004-2014 George Yunaev, gyunaev@ulduzsoft.com * * 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 3 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, see . */ #include #include "ebook.h" #include "ebook_search.h" // Helper class to simplicity state management and data keeping class SearchDataKeeper { public: SearchDataKeeper() { m_inPhrase = false; } void beginPhrase() { phrase_terms.clear(); m_inPhrase = true; } void endPhrase() { m_inPhrase = false; phrasewords += phrase_terms; phrases.push_back( phrase_terms.join(" ") ); } bool isInPhrase() const { return m_inPhrase; } void addTerm( const QString& term ) { if ( !term.isEmpty() ) { terms.push_back( term ); if ( m_inPhrase ) phrase_terms.push_back( term ); } } // Should contain all the search terms present in query, includind those from phrases. One element - one term . QStringList terms; // Should contain phrases present in query without quotes. One element - one phrase. QStringList phrases; // Should contain all the terms present in all the phrases (but not outside). QStringList phrasewords; private: bool m_inPhrase; QStringList phrase_terms; }; EBookSearch::EBookSearch() { m_Index = 0; } EBookSearch::~ EBookSearch() { delete m_Index; } bool EBookSearch::loadIndex( QDataStream & stream ) { delete m_Index; m_Index = new QtAs::Index(); return m_Index->readDict( stream ); } bool EBookSearch::generateIndex( EBook * ebookFile, QDataStream & stream ) { QList< QUrl > documents; QList< QUrl > alldocuments; emit progressStep( 0, "Generating the list of documents" ); processEvents(); // Enumerate the documents if ( !ebookFile->enumerateFiles( alldocuments ) ) return false; if ( m_Index ) delete m_Index; m_Index = new QtAs::Index(); connect( m_Index, SIGNAL( indexingProgress( int, const QString& ) ), this, SLOT( updateProgress( int, const QString& ) ) ); // Process the list of files in CHM archive and keep only HTML document files from there - for ( int i = 0; i < alldocuments.size(); i++ ) + for ( const QUrl &allDocumentsI : qAsConst( alldocuments ) ) { - QString docpath = alldocuments[i].path(); + const QString docpath = allDocumentsI.path(); if ( docpath.endsWith( ".html", Qt::CaseInsensitive ) || docpath.endsWith( ".htm", Qt::CaseInsensitive ) || docpath.endsWith( ".xhtml", Qt::CaseInsensitive ) ) - documents.push_back( alldocuments[i] ); + documents.push_back( allDocumentsI ); } if ( !m_Index->makeIndex( documents, ebookFile ) ) { delete m_Index; m_Index = 0; return false; } m_Index->writeDict( stream ); m_keywordDocuments.clear(); return true; } void EBookSearch::cancelIndexGeneration() { m_Index->setLastWinClosed(); } void EBookSearch::updateProgress(int value, const QString & stepName) { emit progressStep( value, stepName ); } void EBookSearch::processEvents() { // Do it up to ten times; some events generate other events for ( int i = 0; i < 10; i++ ) qApp->processEvents( QEventLoop::ExcludeUserInputEvents ); } bool EBookSearch::searchQuery(const QString & query, QList< QUrl > * results, EBook *ebookFile, unsigned int limit) { // We should have index if ( !m_Index ) return false; // Characters which split the words. We need to make them separate tokens QString splitChars = m_Index->getCharsSplit(); // Characters which are part of the word. We should keep them apart. QString partOfWordChars = m_Index->getCharsPartOfWord(); // Variables to store current state SearchDataKeeper keeper; QString term; - for ( int i = 0; i < query.length(); i++ ) + for ( const QChar &iChar : query ) { - QChar ch = query[i].toLower(); + const QChar ch = iChar.toLower(); // a quote either begins or ends the phrase if ( ch == '"' ) { keeper.addTerm( term ); if ( keeper.isInPhrase() ) keeper.endPhrase(); else keeper.beginPhrase(); continue; } // If new char does not stop the word, add ot and continue if ( ch.isLetterOrNumber() || partOfWordChars.indexOf( ch ) != -1 ) { term.append( ch ); continue; } // If it is a split char, add this term and split char as separate term if ( splitChars.indexOf( ch ) != -1 ) { // Add existing term if present keeper.addTerm( term ); // Change the term variable, so it will be added when we exit this block term = ch; } // Just add the word; it is most likely a space or terminated by tokenizer. keeper.addTerm( term ); term = QString(); } keeper.addTerm( term ); if ( keeper.isInPhrase() ) return false; QList< QUrl > foundDocs = m_Index->query( keeper.terms, keeper.phrases, keeper.phrasewords, ebookFile ); for ( QList< QUrl >::iterator it = foundDocs.begin(); it != foundDocs.end() && limit > 0; ++it, limit-- ) results->push_back( *it ); return true; } bool EBookSearch::hasIndex() const { return m_Index != 0; } diff --git a/generators/chm/lib/helper_search_index.cpp b/generators/chm/lib/helper_search_index.cpp index 9c88de867..acd6b1791 100644 --- a/generators/chm/lib/helper_search_index.cpp +++ b/generators/chm/lib/helper_search_index.cpp @@ -1,493 +1,492 @@ /* * Kchmviewer - a CHM and EPUB file viewer with broad language support * Copyright (C) 2004-2014 George Yunaev, gyunaev@ulduzsoft.com * * 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 3 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, see . */ #include #include #include "ebook.h" #include "ebook_search.h" #include "helper_search_index.h" static const int DICT_VERSION = 4; namespace QtAs { // Those characters are splitters (i.e. split the word), but added themselves into dictionary too. // This makes the dictionary MUCH larger, but ensure that for the piece of "window->print" both // search for "print" and "->print" will find it. static const char SPLIT_CHARACTERS[] = "!()*&^%#@[]{}':;,.?/|/?<>\\-+=~`"; // Those characters are parts of word - for example, '_' is here, and search for _debug will find only _debug. static const char WORD_CHARACTERS[] = "$_"; struct Term { Term() : frequency(-1) {} Term( const QString &t, int f, const QVector &l ) : term( t ), frequency( f ), documents( l ) {} QString term; int frequency; QVectordocuments; bool operator<( const Term &i2 ) const { return frequency < i2.frequency; } }; QDataStream &operator>>( QDataStream &s, Document &l ) { s >> l.docNumber; s >> l.frequency; return s; } QDataStream &operator<<( QDataStream &s, const Document &l ) { s << (short)l.docNumber; s << (short)l.frequency; return s; } Index::Index() : QObject( 0 ) { lastWindowClosed = false; connect( qApp, SIGNAL( lastWindowClosed() ), this, SLOT( setLastWinClosed() ) ); } void Index::setLastWinClosed() { lastWindowClosed = true; } bool Index::makeIndex(const QList< QUrl >& docs, EBook *chmFile ) { if ( docs.isEmpty() ) return false; docList = docs; if ( chmFile->hasFeature( EBook::FEATURE_ENCODING ) ) entityDecoder.changeEncoding( QTextCodec::codecForName( chmFile->currentEncoding().toUtf8() ) ); QList< QUrl >::ConstIterator it = docList.constBegin(); int steps = docList.count() / 100; if ( !steps ) steps++; int prog = 0; for ( int i = 0; it != docList.constEnd(); ++it, ++i ) { if ( lastWindowClosed ) return false; QUrl filename = *it; QStringList terms; if ( parseDocumentToStringlist( chmFile, filename, terms ) ) { for ( QStringList::ConstIterator tit = terms.constBegin(); tit != terms.constEnd(); ++tit ) insertInDict( *tit, i ); } if ( i%steps == 0 ) { prog++; prog = qMin( prog, 99 ); emit indexingProgress( prog, tr("Processing document %1") .arg( (*it).path() ) ); } } emit indexingProgress( 100, tr("Processing completed") ); return true; } void Index::insertInDict( const QString &str, int docNum ) { Entry *e = 0; if ( dict.count() ) e = dict[ str ]; if ( e ) { if ( e->documents.last().docNumber != docNum ) e->documents.append( Document(docNum, 1 ) ); else e->documents.last().frequency++; } else { dict.insert( str, new Entry( docNum ) ); } } bool Index::parseDocumentToStringlist(EBook *chmFile, const QUrl& filename, QStringList& tokenlist ) { QString parsedbuf, parseentity, text; if ( !chmFile->getFileContentAsString( text, filename ) || text.isEmpty() ) { qWarning( "Search index generator: could not retrieve the document content for %s", qPrintable( filename.toString() ) ); return false; } m_charssplit = SPLIT_CHARACTERS; m_charsword = WORD_CHARACTERS; tokenlist.clear(); // State machine states enum state_t { STATE_OUTSIDE_TAGS, // outside HTML tags; parse text STATE_IN_HTML_TAG, // inside HTML tags; wait for end tag STATE_IN_QUOTES, // inside HTML tags and inside quotes; wait for end quote (in var QuoteChar) STATE_IN_HTML_ENTITY // inside HTML entity; parse the entity }; state_t state = STATE_OUTSIDE_TAGS; QChar QuoteChar; // used in STATE_IN_QUOTES for ( int j = 0; j < text.length(); j++ ) { QChar ch = text[j]; if ( (j % 20000) == 0 ) qApp->processEvents( QEventLoop::ExcludeUserInputEvents ); if ( state == STATE_IN_HTML_TAG ) { // We are inside HTML tag. // Ignore everything until we see '>' (end of HTML tag) or quote char (quote start) if ( ch == '"' || ch == '\'' ) { state = STATE_IN_QUOTES; QuoteChar = ch; } else if ( ch == '>' ) state = STATE_OUTSIDE_TAGS; continue; } else if ( state == STATE_IN_QUOTES ) { // We are inside quoted text inside HTML tag. // Ignore everything until we see the quote character again if ( ch == QuoteChar ) state = STATE_IN_HTML_TAG; continue; } else if ( state == STATE_IN_HTML_ENTITY ) { // We are inside encoded HTML entity (like  ). // Collect to parsedbuf everything until we see ; if ( ch.isLetterOrNumber() ) { // get next character of this entity parseentity.append( ch ); continue; } // The entity ended state = STATE_OUTSIDE_TAGS; // Some shitty HTML does not terminate entities correctly. Screw it. if ( ch != ';' && ch != '<' ) { if ( parseentity.isEmpty() ) { // straight '&' symbol. Add and continue. parsedbuf += "&"; } else qWarning( "Index::parseDocument: incorrectly terminated HTML entity '&%s%c', ignoring", qPrintable( parseentity ), ch.toLatin1() ); j--; // parse this character again, but in different state continue; } // Don't we have a space? if ( parseentity.toLower() != "nbsp" ) { QString entity = entityDecoder.decode( parseentity ); if ( entity.isNull() ) { // decodeEntity() already printed error message //qWarning( "Index::parseDocument: failed to decode entity &%s;", parsedbuf.ascii() ); continue; } parsedbuf += entity; continue; } else ch = ' '; // We got a space, so treat it like it, and not add it to parsebuf } // // Now process STATE_OUTSIDE_TAGS // // Check for start of HTML tag, and switch to STATE_IN_HTML_TAG if it is if ( ch == '<' ) { state = STATE_IN_HTML_TAG; goto tokenize_buf; } // Check for start of HTML entity if ( ch == '&' ) { state = STATE_IN_HTML_ENTITY; parseentity = QString(); continue; } // Replace quote by ' - quotes are used in search window to set the phrase if ( ch == '"' ) ch = '\''; // Ok, we have a valid character outside HTML tags, and probably some in buffer already. // If it is char or letter, add it and continue if ( ch.isLetterOrNumber() || m_charsword.indexOf( ch ) != -1 ) { parsedbuf.append( ch ); continue; } // If it is a split char, add the word to the dictionary, and then add the char itself. if ( m_charssplit.indexOf( ch ) != -1 ) { if ( !parsedbuf.isEmpty() ) tokenlist.push_back( parsedbuf.toLower() ); tokenlist.push_back( ch.toLower() ); parsedbuf = QString(); continue; } tokenize_buf: // Just add the word; it is most likely a space or terminated by tokenizer. if ( !parsedbuf.isEmpty() ) { tokenlist.push_back( parsedbuf.toLower() ); parsedbuf = QString(); } } // Add the last word if still here - for broken htmls. if ( !parsedbuf.isEmpty() ) tokenlist.push_back( parsedbuf.toLower() ); return true; } void Index::writeDict( QDataStream& stream ) { stream << DICT_VERSION; stream << m_charssplit; stream << m_charsword; // Document list stream << docList; // Dictionary for( QHash::ConstIterator it = dict.constBegin(); it != dict.constEnd(); ++it ) { stream << it.key(); stream << (int) it.value()->documents.count(); stream << it.value()->documents; } } bool Index::readDict( QDataStream& stream ) { dict.clear(); docList.clear(); QString key; int version, numOfDocs; stream >> version; if ( version < 2 ) return false; stream >> m_charssplit; stream >> m_charsword; // Read the document list stream >> docList; while ( !stream.atEnd() ) { stream >> key; stream >> numOfDocs; QVector docs( numOfDocs ); stream >> docs; dict.insert( key, new Entry( docs ) ); } return dict.size() > 0; } QList< QUrl > Index::query(const QStringList &terms, const QStringList &termSeq, const QStringList &seqWords, EBook *chmFile ) { QList termList; QStringList::ConstIterator it = terms.begin(); for ( it = terms.begin(); it != terms.end(); ++it ) { Entry *e = 0; if ( dict[ *it ] ) { e = dict[ *it ]; termList.append( Term( *it, e->documents.count(), e->documents ) ); } else { return QList< QUrl >(); } } if ( !termList.count() ) return QList< QUrl >(); std::sort(termList.begin(), termList.end()); QVector minDocs = termList.takeFirst().documents; - for(QList::Iterator it = termList.begin(); it != termList.end(); ++it) { - Term *t = &(*it); - QVector docs = t->documents; + for(const Term &t : qAsConst(termList)) { + const QVector docs = t.documents; for(QVector::Iterator minDoc_it = minDocs.begin(); minDoc_it != minDocs.end(); ) { bool found = false; for (QVector::ConstIterator doc_it = docs.constBegin(); doc_it != docs.constEnd(); ++doc_it ) { if ( (*minDoc_it).docNumber == (*doc_it).docNumber ) { (*minDoc_it).frequency += (*doc_it).frequency; found = true; break; } } if ( !found ) minDoc_it = minDocs.erase( minDoc_it ); else ++minDoc_it; } } QList< QUrl > results; std::sort(minDocs.begin(), minDocs.end()); if ( termSeq.isEmpty() ) { - for(QVector::Iterator it = minDocs.begin(); it != minDocs.end(); ++it) - results << docList.at((int)(*it).docNumber); + for(const Document &doc : qAsConst(minDocs)) + results << docList.at((int)doc.docNumber); return results; } QUrl fileName; - for(QVector::Iterator it = minDocs.begin(); it != minDocs.end(); ++it) { - fileName = docList[ (int)(*it).docNumber ]; + for(const Document &doc : qAsConst(minDocs)) { + fileName = docList[ (int)doc.docNumber ]; if ( searchForPhrases( termSeq, seqWords, fileName, chmFile ) ) results << fileName; } return results; } bool Index::searchForPhrases( const QStringList &phrases, const QStringList &words, const QUrl &filename, EBook * chmFile ) { QStringList parsed_document; if ( !parseDocumentToStringlist( chmFile, filename, parsed_document ) ) return false; miniDict.clear(); // Initialize the dictionary with the words in phrase(s) - for ( QStringList::ConstIterator cIt = words.begin(); cIt != words.end(); ++cIt ) - miniDict.insert( *cIt, new PosEntry( 0 ) ); + for ( const QString &word : words ) + miniDict.insert( word, new PosEntry( 0 ) ); // Fill the dictionary with the words from the document unsigned int word_offset = 3; for ( QStringList::ConstIterator it = parsed_document.constBegin(); it != parsed_document.constEnd(); it++, word_offset++ ) { PosEntry * entry = miniDict[ *it ]; if ( entry ) entry->positions.append( word_offset ); } // Dump it /* QDictIterator it( miniDict ); for( ; it.current(); ++it ) { QString text( it.currentKey() ); QValueList pos = miniDict[text]->positions; for ( unsigned int i = 1; i < pos.size(); i++ ) text += " " + QString::number( pos[i] ); qDebug( "%s", text.ascii()); } */ QList first_word_positions; for ( QStringList::ConstIterator phrase_it = phrases.constBegin(); phrase_it != phrases.constEnd(); phrase_it++ ) { QStringList phrasewords = phrase_it->split( ' ' ); first_word_positions = miniDict[ phrasewords[0] ]->positions; for ( int j = 1; j < phrasewords.count(); ++j ) { QList next_word_it = miniDict[ phrasewords[j] ]->positions; QList::iterator dict_it = first_word_positions.begin(); while ( dict_it != first_word_positions.end() ) { if ( next_word_it.indexOf( *dict_it + 1 ) != -1 ) { (*dict_it)++; ++dict_it; } else dict_it = first_word_positions.erase( dict_it ); } } } if ( first_word_positions.count() ) return true; return false; } }; diff --git a/generators/dvi/TeXFont.h b/generators/dvi/TeXFont.h index 4b89e8665..d9478e280 100644 --- a/generators/dvi/TeXFont.h +++ b/generators/dvi/TeXFont.h @@ -1,48 +1,48 @@ // -*- Mode: C++; c-basic-offset: 2; indent-tabs-mode: nil; c-brace-offset: 0; -*- // TeXFont.h // // Part of KDVI - A DVI previewer for the KDE desktop environment // // (C) 2003 Stefan Kebekus // Distributed under the GPL #ifndef _TEXFONT_H #define _TEXFONT_H #include "glyph.h" #include "TeXFontDefinition.h" class TeXFont { public: TeXFont(TeXFontDefinition *_parent) { parent = _parent; errorMessage.clear(); } virtual ~TeXFont(); void setDisplayResolution() { - for(unsigned int i=0; i #include "TeXFont_PK.h" #include "fontpool.h" #include "debug_dvi.h" #include "xdvi.h" #include #include #include #include #include //#define DEBUG_PK #define PK_PRE 247 #define PK_ID 89 #define PK_MAGIC (PK_PRE << 8) + PK_ID extern void oops(const QString& message); TeXFont_PK::TeXFont_PK(TeXFontDefinition *parent) : TeXFont(parent) { #ifdef DEBUG_PK qCDebug(OkularDviDebug) << "TeXFont_PK::TeXFont_PK( parent=" << parent << ")"; #endif - for(unsigned int i=0; ifilename).constData(), "r"); if (file == nullptr) qCCritical(OkularDviDebug) << i18n("Cannot open font file %1.", parent->filename) << endl; #ifdef DEBUG_PK else qCDebug(OkularDviDebug) << "TeXFont_PK::TeXFont_PK(): file opened successfully"; #endif read_PK_index(); #ifdef DEBUG_PK qCDebug(OkularDviDebug) << "TeXFont_PK::TeXFont_PK() ended"; #endif } TeXFont_PK::~TeXFont_PK() { //@@@ Release bitmaps - for(unsigned int i=0; i= TeXFontDefinition::max_num_of_chars_in_font) { qCCritical(OkularDviDebug) << "TeXFont_PK::getGlyph(): Argument is too big." << endl; return glyphtable; } // This is the address of the glyph that will be returned. class glyph *g = glyphtable+ch; // Check if the glyph is loaded. If not, load it now. if (characterBitmaps[ch] == nullptr) { // If the character is not defined in the PK file, mark the // character as missing, and print an error message if (g->addr == 0) { qCCritical(OkularDviDebug) << i18n("TexFont_PK::operator[]: Character %1 not defined in font %2", ch, parent->filename) << endl; g->addr = -1; return g; } // If the character has already been marked as missing, just // return a pointer to the glyph (which will then be empty) if (g->addr == -1) return g; // Otherwise, try to load the character fseek(file, g->addr, 0); read_PK_char(ch); // Check if the character could be loaded. If not, mark the // character as 'missing', and return a pointer. if (characterBitmaps[ch]->bits == nullptr) { g->addr = -1; return g; } } // At this point, g points to a properly loaded character. Generate // a smoothly scaled QPixmap if the user asks for it. if ((generateCharacterPixmap == true) && ((g->shrunkenCharacter.isNull()) || (color != g->color)) && (characterBitmaps[ch]->w != 0)) { g->color = color; double shrinkFactor = 1200 / parent->displayResolution_in_dpi; // All is fine? Then we rescale the bitmap in order to produce the // required pixmap. Rescaling a character, however, is an art // that requires some explanation... // // If we would just divide the size of the character and the // coordinates by the shrink factor, then the result would look // quite ugly: due to the inevitable rounding errors in the // integer arithmetic, the characters would be displaced by up to // a pixel. That doesn't sound much, but on low-resolution // devices, such as a notebook screen, the effect would be a // "dancing line" of characters, which looks really bad. // Calculate the coordinates of the hot point in the shrunken // bitmap. For simplicity, let us consider the x-coordinate // first. In principle, the hot point should have an x-coordinate // of (g->x/shrinkFactor). That, however, will generally NOT be an // integral number. The cure is to translate the source image // somewhat, so that the x-coordinate of the hot point falls onto // the round-up of this number, i.e. g->x2 = (int)ceil(g->x/shrinkFactor); // Translating and scaling then means that the pixel in the scaled // image which covers the range [x,x+1) corresponds to the range // [x*shrinkFactor+srcXTrans, (x+1)*shrinkFactor+srcXTrans), where // srcXTrans is the following NEGATIVE number double srcXTrans = shrinkFactor * (g->x/shrinkFactor - ceil(g->x/shrinkFactor)); // How big will the shrunken bitmap then become? If shrunk_width // denotes that width of the scaled image, and // characterBitmaps[ch]->w the width of the original image, we // need to make sure that the following inequality holds: // // shrunk_width*shrinkFactor+srcXTrans >= characterBitmaps[ch]->w // // in other words, int shrunk_width = (int)ceil( (characterBitmaps[ch]->w - srcXTrans)/shrinkFactor ); // Now do the same for the y-coordinate g->y2 = (int)ceil(g->y/shrinkFactor); double srcYTrans = shrinkFactor * (g->y/shrinkFactor - ceil(g->y/shrinkFactor )); int shrunk_height = (int)ceil( (characterBitmaps[ch]->h - srcYTrans)/shrinkFactor ); // Turn the image into 8 bit QByteArray translated(characterBitmaps[ch]->w * characterBitmaps[ch]->h, '\0'); quint8 *data = (quint8 *)translated.data(); for(int x=0; xw; x++) for(int y=0; yh; y++) { quint8 bit = *(characterBitmaps[ch]->bits + characterBitmaps[ch]->bytes_wide*y + (x >> 3)); bit = bit >> (x & 7); bit = bit & 1; data[characterBitmaps[ch]->w*y + x] = bit; } // Now shrink the image. We shrink the X-direction first QByteArray xshrunk(shrunk_width*characterBitmaps[ch]->h, '\0'); quint8 *xdata = (quint8 *)xshrunk.data(); // Do the shrinking. The pixel (x,y) that we want to calculate // corresponds to the line segment from // // [shrinkFactor*x+srcXTrans, shrinkFactor*(x+1)+srcXTrans) // // The trouble is, these numbers are in general no integers. for(int y=0; yh; y++) for(int x=0; x= 0) && (srcX < characterBitmaps[ch]->w)) value += data[characterBitmaps[ch]->w*y + srcX] * 255; if (destStartX >= 0.0) value += (quint32) (255.0*(ceil(destStartX)-destStartX) * data[characterBitmaps[ch]->w*y + (int)floor(destStartX)]); if (floor(destEndX) < characterBitmaps[ch]->w) value += (quint32) (255.0*(destEndX-floor(destEndX)) * data[characterBitmaps[ch]->w*y + (int)floor(destEndX)]); xdata[shrunk_width*y + x] = (int)(value/shrinkFactor + 0.5); } // Now shrink the Y-direction QByteArray xyshrunk(shrunk_width*shrunk_height, '\0'); quint8 *xydata = (quint8 *)xyshrunk.data(); for(int x=0; x= 0) && (srcY < characterBitmaps[ch]->h)) value += xdata[shrunk_width*srcY + x]; if (destStartY >= 0.0) value += (quint32) ((ceil(destStartY)-destStartY) * xdata[shrunk_width*(int)floor(destStartY) + x]); if (floor(destEndY) < characterBitmaps[ch]->h) value += (quint32) ((destEndY-floor(destEndY)) * xdata[shrunk_width*(int)floor(destEndY) + x]); xydata[shrunk_width*y + x] = (int)(value/shrinkFactor); } QImage im32(shrunk_width, shrunk_height, QImage::Format_ARGB32); // Do QPixmaps fully support the alpha channel? If yes, we use // that. Otherwise, use other routines as a fallback if (parent->font_pool->QPixmapSupportsAlpha) { // If the alpha channel is properly supported, we set the // character glyph to a colored rectangle, and define the // character outline only using the alpha channel. That ensures // good quality rendering for overlapping characters. im32.fill(qRgb(color.red(), color.green(), color.blue())); for(int y=0; y white; data = 0xff -> use "color" *destScanLine = qRgba(0xFF - (rInv*data + 0x7F) / 0xFF, 0xFF - (gInv*data + 0x7F) / 0xFF, 0xFF - (bInv*data + 0x7F) / 0xFF, (data > 0x03) ? 0xff : 0x00); destScanLine++; srcScanLine++; } } } g->shrunkenCharacter = im32; } return g; } #define ADD(a, b) ((quint32 *) (((char *) a) + b)) #define SUB(a, b) ((quint32 *) (((char *) a) - b)) // This table is used for changing the bit order in a byte. The // expression bitflp[byte] takes a byte in big endian and gives the // little endian equivalent of that. static const uchar bitflip[256] = { 0, 128, 64, 192, 32, 160, 96, 224, 16, 144, 80, 208, 48, 176, 112, 240, 8, 136, 72, 200, 40, 168, 104, 232, 24, 152, 88, 216, 56, 184, 120, 248, 4, 132, 68, 196, 36, 164, 100, 228, 20, 148, 84, 212, 52, 180, 116, 244, 12, 140, 76, 204, 44, 172, 108, 236, 28, 156, 92, 220, 60, 188, 124, 252, 2, 130, 66, 194, 34, 162, 98, 226, 18, 146, 82, 210, 50, 178, 114, 242, 10, 138, 74, 202, 42, 170, 106, 234, 26, 154, 90, 218, 58, 186, 122, 250, 6, 134, 70, 198, 38, 166, 102, 230, 22, 150, 86, 214, 54, 182, 118, 246, 14, 142, 78, 206, 46, 174, 110, 238, 30, 158, 94, 222, 62, 190, 126, 254, 1, 129, 65, 193, 33, 161, 97, 225, 17, 145, 81, 209, 49, 177, 113, 241, 9, 137, 73, 201, 41, 169, 105, 233, 25, 153, 89, 217, 57, 185, 121, 249, 5, 133, 69, 197, 37, 165, 101, 229, 21, 149, 85, 213, 53, 181, 117, 245, 13, 141, 77, 205, 45, 173, 109, 237, 29, 157, 93, 221, 61, 189, 125, 253, 3, 131, 67, 195, 35, 163, 99, 227, 19, 147, 83, 211, 51, 179, 115, 243, 11, 139, 75, 203, 43, 171, 107, 235, 27, 155, 91, 219, 59, 187, 123, 251, 7, 135, 71, 199, 39, 167, 103, 231, 23, 151, 87, 215, 55, 183, 119, 247, 15, 143, 79, 207, 47, 175, 111, 239, 31, 159, 95, 223, 63, 191, 127, 255 }; static const quint32 bit_masks[33] = { 0x0, 0x1, 0x3, 0x7, 0xf, 0x1f, 0x3f, 0x7f, 0xff, 0x1ff, 0x3ff, 0x7ff, 0xfff, 0x1fff, 0x3fff, 0x7fff, 0xffff, 0x1ffff, 0x3ffff, 0x7ffff, 0xfffff, 0x1fffff, 0x3fffff, 0x7fffff, 0xffffff, 0x1ffffff, 0x3ffffff, 0x7ffffff, 0xfffffff, 0x1fffffff, 0x3fffffff, 0x7fffffff, 0xffffffff }; #define PK_ID 89 #define PK_CMD_START 240 #define PK_X1 240 #define PK_X2 241 #define PK_X3 242 #define PK_X4 243 #define PK_Y 244 #define PK_POST 245 #define PK_NOOP 246 #define PK_PRE 247 int TeXFont_PK::PK_get_nyb(FILE *fp) { #ifdef DEBUG_PK qCDebug(OkularDviDebug) << "PK_get_nyb"; #endif unsigned temp; if (PK_bitpos < 0) { PK_input_byte = one(fp); PK_bitpos = 4; } temp = PK_input_byte >> PK_bitpos; PK_bitpos -= 4; return (temp & 0xf); } int TeXFont_PK::PK_packed_num(FILE *fp) { #ifdef DEBUG_PK qCDebug(OkularDviDebug) << "PK_packed_num"; #endif int i, j; if ((i = PK_get_nyb(fp)) == 0) { do { j = PK_get_nyb(fp); ++i; } while (j == 0); while (i > 0) { j = (j << 4) | PK_get_nyb(fp); --i; } return (j - 15 + ((13 - PK_dyn_f) << 4) + PK_dyn_f); } else { if (i <= PK_dyn_f) return i; if (i < 14) return (((i - PK_dyn_f - 1) << 4) + PK_get_nyb(fp) + PK_dyn_f + 1); if (i == 14) PK_repeat_count = PK_packed_num(fp); else PK_repeat_count = 1; return PK_packed_num(fp); } } void TeXFont_PK::PK_skip_specials() { #ifdef DEBUG_PK qCDebug(OkularDviDebug) << "TeXFont_PK::PK_skip_specials() called"; #endif int i,j; FILE *fp = file; #ifdef DEBUG_PK if (fp == 0) qCDebug(OkularDviDebug) << "TeXFont_PK::PK_skip_specials(): file == 0"; #endif do { PK_flag_byte = one(fp); if (PK_flag_byte >= PK_CMD_START) { switch (PK_flag_byte) { case PK_X1 : case PK_X2 : case PK_X3 : case PK_X4 : i = 0; for (j = PK_CMD_START; j <= PK_flag_byte; ++j) i = (i << 8) | one(fp); while (i--) (void) one(fp); break; case PK_Y : (void) four(fp); case PK_POST : case PK_NOOP : break; default : oops(i18n("Unexpected %1 in PK file %2", PK_flag_byte, parent->filename) ); break; } } } while (PK_flag_byte != PK_POST && PK_flag_byte >= PK_CMD_START); #ifdef DEBUG_PK qCDebug(OkularDviDebug) << "TeXFont_PK::PK_skip_specials() ended"; #endif } void TeXFont_PK::read_PK_char(unsigned int ch) { #ifdef DEBUG_PK qCDebug(OkularDviDebug) << "read_PK_char"; #endif int i, j; int n; int row_bit_pos; bool paint_switch; quint32* cp; class glyph *g; FILE *fp = file; long fpwidth; quint32 word = 0; int word_weight, bytes_wide; int rows_left, h_bit, count; g = glyphtable + ch; PK_flag_byte = g->x2; PK_dyn_f = PK_flag_byte >> 4; paint_switch = ((PK_flag_byte & 8) != 0); PK_flag_byte &= 0x7; if (PK_flag_byte == 7) n = 4; else if (PK_flag_byte > 3) n = 2; else n = 1; #ifdef DEBUG_PK qCDebug(OkularDviDebug) << "loading pk char " << ch << ", char type " << n; #endif if (characterBitmaps[ch] == nullptr) characterBitmaps[ch] = new bitmap(); /* * now read rest of character preamble */ if (n != 4) fpwidth = num(fp, 3); else { fpwidth = sfour(fp); (void) four(fp); /* horizontal escapement */ } (void) num(fp, n); /* vertical escapement */ { unsigned long w, h; w = num(fp, n); h = num(fp, n); if (w > 0x7fff || h > 0x7fff) oops(i18n("The character %1 is too large in file %2", ch, parent->filename)); characterBitmaps[ch]->w = w; characterBitmaps[ch]->h = h; } g->x = snum(fp, n); g->y = snum(fp, n); g->dvi_advance_in_units_of_design_size_by_2e20 = fpwidth; { /* width must be multiple of 16 bits for raster_op */ characterBitmaps[ch]->bytes_wide = ROUNDUP((int) characterBitmaps[ch]->w, 32) * 4; unsigned int size = characterBitmaps[ch]->bytes_wide * characterBitmaps[ch]->h; characterBitmaps[ch]->bits = new char[size != 0 ? size : 1]; } cp = (quint32 *) characterBitmaps[ch]->bits; /* * read character data into *cp */ bytes_wide = ROUNDUP((int) characterBitmaps[ch]->w, 32) * 4; PK_bitpos = -1; // The routines which read the character depend on the bit // ordering. In principle, the bit order should be detected at // compile time and the proper routing chosen. For the moment, as // autoconf is somewhat complicated for the author, we prefer a // simpler -even if somewhat slower approach and detect the ordering // at runtime. That should of course be changed in the future. if (QSysInfo::ByteOrder == QSysInfo::BigEndian) { // Routine for big Endian machines. Applies e.g. to Motorola and // (Ultra-)Sparc processors. #ifdef DEBUG_PK qCDebug(OkularDviDebug) << "big Endian byte ordering"; #endif if (PK_dyn_f == 14) { /* get raster by bits */ memset(characterBitmaps[ch]->bits, 0, (int) characterBitmaps[ch]->h * bytes_wide); for (i = 0; i < (int) characterBitmaps[ch]->h; i++) { /* get all rows */ cp = ADD(characterBitmaps[ch]->bits, i * bytes_wide); row_bit_pos = 32; for (j = 0; j < (int) characterBitmaps[ch]->w; j++) { /* get one row */ if (--PK_bitpos < 0) { word = one(fp); PK_bitpos = 7; } if (--row_bit_pos < 0) { cp++; row_bit_pos = 32 - 1; } if (word & (1 << PK_bitpos)) *cp |= 1 << row_bit_pos; } } } else { /* get packed raster */ rows_left = characterBitmaps[ch]->h; h_bit = characterBitmaps[ch]->w; PK_repeat_count = 0; word_weight = 32; word = 0; while (rows_left > 0) { count = PK_packed_num(fp); while (count > 0) { if (count < word_weight && count < h_bit) { h_bit -= count; word_weight -= count; if (paint_switch) word |= bit_masks[count] << word_weight; count = 0; } else if (count >= h_bit && h_bit <= word_weight) { if (paint_switch) word |= bit_masks[h_bit] << (word_weight - h_bit); *cp++ = word; /* "output" row(s) */ for (i = PK_repeat_count * bytes_wide / 4; i > 0; --i) { *cp = *SUB(cp, bytes_wide); ++cp; } rows_left -= PK_repeat_count + 1; PK_repeat_count = 0; word = 0; word_weight = 32; count -= h_bit; h_bit = characterBitmaps[ch]->w; } else { if (paint_switch) word |= bit_masks[word_weight]; *cp++ = word; word = 0; count -= word_weight; h_bit -= word_weight; word_weight = 32; } } paint_switch = 1 - paint_switch; } if (cp != ((quint32 *) (characterBitmaps[ch]->bits + bytes_wide * characterBitmaps[ch]->h))) oops(i18n("Wrong number of bits stored: char. %1, font %2", ch, parent->filename)); if (rows_left != 0 || h_bit != characterBitmaps[ch]->w) oops(i18n("Bad pk file (%1), too many bits", parent->filename)); } // The data in the bitmap is now in the processor's bit order, // that is, big endian. Since XWindows needs little endian, we // need to change the bit order now. unsigned char* bitmapData = (unsigned char*) characterBitmaps[ch]->bits; unsigned char* endOfData = bitmapData + characterBitmaps[ch]->bytes_wide*characterBitmaps[ch]->h; while(bitmapData < endOfData) { *bitmapData = bitflip[*bitmapData]; bitmapData++; } } else { // Routines for small Endian start here. This applies e.g. to // Intel and Alpha processors. #ifdef DEBUG_PK qCDebug(OkularDviDebug) << "small Endian byte ordering"; #endif if (PK_dyn_f == 14) { /* get raster by bits */ memset(characterBitmaps[ch]->bits, 0, (int) characterBitmaps[ch]->h * bytes_wide); for (i = 0; i < (int) characterBitmaps[ch]->h; i++) { /* get all rows */ cp = ADD(characterBitmaps[ch]->bits, i * bytes_wide); row_bit_pos = -1; for (j = 0; j < (int) characterBitmaps[ch]->w; j++) { /* get one row */ if (--PK_bitpos < 0) { word = one(fp); PK_bitpos = 7; } if (++row_bit_pos >= 32) { cp++; row_bit_pos = 0; } if (word & (1 << PK_bitpos)) *cp |= 1 << row_bit_pos; } } } else { /* get packed raster */ rows_left = characterBitmaps[ch]->h; h_bit = characterBitmaps[ch]->w; PK_repeat_count = 0; word_weight = 32; word = 0; while (rows_left > 0) { count = PK_packed_num(fp); while (count > 0) { if (count < word_weight && count < h_bit) { if (paint_switch) word |= bit_masks[count] << (32 - word_weight); h_bit -= count; word_weight -= count; count = 0; } else if (count >= h_bit && h_bit <= word_weight) { if (paint_switch) word |= bit_masks[h_bit] << (32 - word_weight); *cp++ = word; /* "output" row(s) */ for (i = PK_repeat_count * bytes_wide / 4; i > 0; --i) { *cp = *SUB(cp, bytes_wide); ++cp; } rows_left -= PK_repeat_count + 1; PK_repeat_count = 0; word = 0; word_weight = 32; count -= h_bit; h_bit = characterBitmaps[ch]->w; } else { if (paint_switch) word |= bit_masks[word_weight] << (32 - word_weight); *cp++ = word; word = 0; count -= word_weight; h_bit -= word_weight; word_weight = 32; } } paint_switch = 1 - paint_switch; } if (cp != ((quint32 *) (characterBitmaps[ch]->bits + bytes_wide * characterBitmaps[ch]->h))) oops(i18n("Wrong number of bits stored: char. %1, font %2", ch, parent->filename)); if (rows_left != 0 || h_bit != characterBitmaps[ch]->w) oops(i18n("Bad pk file (%1), too many bits", parent->filename)); } } // endif: big or small Endian? } void TeXFont_PK::read_PK_index() { #ifdef DEBUG_PK qCDebug(OkularDviDebug) << "TeXFont_PK::read_PK_index() called"; #endif if (file == nullptr) { qCCritical(OkularDviDebug) << "TeXFont_PK::read_PK_index(): file == 0" << endl; return; } int magic = two(file); if (magic != PK_MAGIC) { qCCritical(OkularDviDebug) << "TeXFont_PK::read_PK_index(): file is not a PK file" << endl; return; } fseek(file, (long) one(file), SEEK_CUR); /* skip comment */ (void) four(file); /* skip design size */ checksum = four(file); int hppp = sfour(file); int vppp = sfour(file); if (hppp != vppp) qCWarning(OkularDviDebug) << i18n("Font has non-square aspect ratio ") << vppp << ":" << hppp ; // Read glyph directory (really a whole pass over the file). for (;;) { int bytes_left, flag_low_bits; unsigned int ch; PK_skip_specials(); if (PK_flag_byte == PK_POST) break; flag_low_bits = PK_flag_byte & 0x7; if (flag_low_bits == 7) { bytes_left = four(file); ch = four(file); } else if (flag_low_bits > 3) { bytes_left = ((flag_low_bits - 4) << 16) + two(file); ch = one(file); } else { bytes_left = (flag_low_bits << 8) + one(file); ch = one(file); } glyphtable[ch].addr = ftell(file); glyphtable[ch].x2 = PK_flag_byte; fseek(file, (long) bytes_left, SEEK_CUR); #ifdef DEBUG_PK qCDebug(OkularDviDebug) << "Scanning pk char " << ch << "at " << glyphtable[ch].addr; #endif } #ifdef DEBUG_PK qCDebug(OkularDviDebug) << "TeXFont_PK::read_PK_index() called"; #endif } diff --git a/generators/dvi/TeXFont_TFM.cpp b/generators/dvi/TeXFont_TFM.cpp index 051ab3387..6b169c8cf 100644 --- a/generators/dvi/TeXFont_TFM.cpp +++ b/generators/dvi/TeXFont_TFM.cpp @@ -1,161 +1,161 @@ // -*- Mode: C++; c-basic-offset: 2; indent-tabs-mode: nil; c-brace-offset: 0; -*- // TeXFont_TFM.cpp // // Part of KDVI - A DVI previewer for the KDE desktop environment // // (C) 2003 Stefan Kebekus // Distributed under the GPL #include #include "TeXFont_TFM.h" #include "debug_dvi.h" #include "debug_dvi.h" #include #include #include //#define DEBUG_TFM TeXFont_TFM::TeXFont_TFM(TeXFontDefinition *parent) : TeXFont(parent) { #ifdef DEBUG_TFM qCDebug(OkularDviDebug) << "TeXFont_TFM::TeXFont_TFM( parent=" << parent << " )"; #endif QFile file( parent->filename ); if ( !file.open( QIODevice::ReadOnly ) ) { qCCritical(OkularDviDebug) << "TeXFont_TFM::TeXFont_TFM(): Could not read TFM file" << endl; return; } QDataStream stream( &file ); // Data from the very beginning of the TFM file, as specified in // "The DVI Driver Standard, Level 0", section D.2.1 quint16 lf, lh, bc, ec, nw, nh, nd; stream >> lf >> lh >> bc >> ec >> nw >> nh >> nd; #ifdef DEBUG_TFM qCDebug(OkularDviDebug) << "lf= " << lf << "lh= " << lh << endl << "bc= " << bc << endl << "ec= " << ec << endl << "nw= " << nw << endl << "nh= " << nh << endl << "nd= " << nd << endl; #endif if ((bc > ec) || (ec >= TeXFontDefinition::max_num_of_chars_in_font)) { qCCritical(OkularDviDebug) << "TeXFont_TFM::TeXFont_TFM( filename=" << parent->filename << " ): The font has an invalid bc and ec entries." << endl; file.close(); return; } // Data from the HEADER section of the TFM data. file.seek(24); stream >> checksum >> design_size_in_TeX_points.value; #ifdef DEBUG_TFM qCDebug(OkularDviDebug) << "checksum = " << checksum << "design_size = " << design_size_in_TeX_points.toDouble() << " TeX Points" << endl << " = " << design_size_in_TeX_points.toDouble()*254.0/7227.0 << " cm" << endl; #endif // Width table fix_word widthTable_in_units_of_design_size[TeXFontDefinition::max_num_of_chars_in_font]; - for(unsigned int i=0; i> widthTable_in_units_of_design_size[i].value; // Some characters, which are used as parts of glyphs, have width // 0 --the real width is calculated in a lig_kern program and // depends on the preceding character. We cannot calculate the // real width here and take 0.4 times the design size as an // approximation. if (widthTable_in_units_of_design_size[i].value == 0) widthTable_in_units_of_design_size[i].fromDouble(0.4); } // Height table fix_word heightTable_in_units_of_design_size[16]; - for(unsigned int i=0; i<16; i++) - heightTable_in_units_of_design_size[i].value = 0; + for(fix_word &fw : heightTable_in_units_of_design_size) + fw.value = 0; for(unsigned int i=0; i> heightTable_in_units_of_design_size[i].value; } // Char-Info table file.seek( 24 + 4*lh ); for(unsigned int characterCode=bc; characterCode> byte; if (byte >= nw) qCCritical(OkularDviDebug) << "TeXFont_TFM::TeXFont_TFM( filename=" << parent->filename << " ): The font has an invalid Char-Info table." << endl; else { characterWidth_in_units_of_design_size[characterCode] = widthTable_in_units_of_design_size[byte]; g->dvi_advance_in_units_of_design_size_by_2e20 = widthTable_in_units_of_design_size[byte].value; } stream >> byte; byte = byte >> 4; if (byte >= nh) qCCritical(OkularDviDebug) << "TeXFont_TFM::TeXFont_TFM( filename=" << parent->filename << " ): The font has an invalid Char-Info table." << endl; else characterHeight_in_units_of_design_size[characterCode] = heightTable_in_units_of_design_size[byte]; stream >> byte; stream >> byte; } file.close(); } TeXFont_TFM::~TeXFont_TFM() { } glyph* TeXFont_TFM::getGlyph(quint16 characterCode, bool generateCharacterPixmap, const QColor& color) { #ifdef DEBUG_TFM qCDebug(OkularDviDebug) << "TeXFont_TFM::getGlyph( ch=" << ch << ", generateCharacterPixmap=" << generateCharacterPixmap << " )"; #endif // Paranoia checks if (characterCode >= TeXFontDefinition::max_num_of_chars_in_font) { qCCritical(OkularDviDebug) << "TeXFont_TFM::getGlyph(): Argument is too big." << endl; return glyphtable; } // This is the address of the glyph that will be returned. class glyph *g = glyphtable+characterCode; if ((generateCharacterPixmap == true) && ((g->shrunkenCharacter.isNull()) || (color != g->color)) ) { g->color = color; quint16 pixelWidth = (quint16)(parent->displayResolution_in_dpi * design_size_in_TeX_points.toDouble() * characterWidth_in_units_of_design_size[characterCode].toDouble() * 100.0/7227.0 + 0.5); quint16 pixelHeight = (quint16)(parent->displayResolution_in_dpi * design_size_in_TeX_points.toDouble() * characterHeight_in_units_of_design_size[characterCode].toDouble() * 100.0/7227.0 + 0.5); // Just make sure that weird TFM files never lead to giant // pixmaps that eat all system memory... if (pixelWidth > 50) pixelWidth = 50; if (pixelHeight > 50) pixelHeight = 50; g->shrunkenCharacter = QImage(pixelWidth, pixelHeight, QImage::Format_RGB32); g->shrunkenCharacter.fill(color.rgba()); g->x2 = 0; g->y2 = pixelHeight; } return g; } diff --git a/generators/poppler/annots.cpp b/generators/poppler/annots.cpp index a1d263d3d..46a0eefd8 100644 --- a/generators/poppler/annots.cpp +++ b/generators/poppler/annots.cpp @@ -1,433 +1,431 @@ /*************************************************************************** * Copyright (C) 2008 by Pino Toscano * * 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 "annots.h" #include // qt/kde includes #include #include #include #include #include "debug_pdf.h" #include "generator_pdf.h" #include "popplerembeddedfile.h" #include "config-okular-poppler.h" Q_DECLARE_METATYPE( Poppler::Annotation* ) extern Okular::Sound* createSoundFromPopplerSound( const Poppler::SoundObject *popplerSound ); extern Okular::Movie* createMovieFromPopplerMovie( const Poppler::MovieObject *popplerMovie ); extern Okular::Movie* createMovieFromPopplerScreen( const Poppler::LinkRendition *popplerScreen ); extern QPair createMovieFromPopplerRichMedia( const Poppler::RichMediaAnnotation *popplerRichMedia ); static void disposeAnnotation( const Okular::Annotation *ann ) { Poppler::Annotation *popplerAnn = qvariant_cast< Poppler::Annotation * >( ann->nativeId() ); delete popplerAnn; } static QPointF normPointToPointF( const Okular::NormalizedPoint& pt ) { return QPointF(pt.x, pt.y); } static QRectF normRectToRectF( const Okular::NormalizedRect& rect ) { return QRectF( QPointF(rect.left, rect.top), QPointF(rect.right, rect.bottom) ); } // Poppler and Okular share the same flag values, but we don't want to export internal flags static int maskExportedFlags(int flags) { return flags & ( Okular::Annotation::Hidden | Okular::Annotation::FixedSize | Okular::Annotation::FixedRotation | Okular::Annotation::DenyPrint | Okular::Annotation::DenyWrite | Okular::Annotation::DenyDelete | Okular::Annotation::ToggleHidingOnMouse ); } //BEGIN PopplerAnnotationProxy implementation PopplerAnnotationProxy::PopplerAnnotationProxy( Poppler::Document *doc, QMutex *userMutex, QHash *annotsOnOpenHash ) : ppl_doc ( doc ), mutex ( userMutex ), annotationsOnOpenHash( annotsOnOpenHash ) { } PopplerAnnotationProxy::~PopplerAnnotationProxy() { } bool PopplerAnnotationProxy::supports( Capability cap ) const { switch ( cap ) { case Addition: case Modification: case Removal: return true; default: return false; } } void PopplerAnnotationProxy::notifyAddition( Okular::Annotation *okl_ann, int page ) { // Export annotation to DOM QDomDocument doc; QDomElement dom_ann = doc.createElement( QStringLiteral("root") ); Okular::AnnotationUtils::storeAnnotation( okl_ann, dom_ann, doc ); QMutexLocker ml(mutex); // Create poppler annotation Poppler::Annotation *ppl_ann = Poppler::AnnotationUtils::createAnnotation( dom_ann ); // Poppler doesn't render StampAnnotations yet if ( ppl_ann->subType() != Poppler::Annotation::AStamp ) okl_ann->setFlags( okl_ann->flags() | Okular::Annotation::ExternallyDrawn ); // Poppler stores highlight points in swapped order if ( ppl_ann->subType() == Poppler::Annotation::AHighlight ) { Poppler::HighlightAnnotation * hlann = static_cast( ppl_ann ); QList quads = hlann->highlightQuads(); QMutableListIterator it( quads ); while ( it.hasNext() ) { Poppler::HighlightAnnotation::Quad &q = it.next(); QPointF t; t = q.points[3]; q.points[3] = q.points[0]; q.points[0] = t; t = q.points[2]; q.points[2] = q.points[1]; q.points[1] = t; } hlann->setHighlightQuads( quads ); } // Bind poppler object to page Poppler::Page *ppl_page = ppl_doc->page( page ); ppl_page->addAnnotation( ppl_ann ); delete ppl_page; // Set pointer to poppler annotation as native Id okl_ann->setNativeId( QVariant::fromValue( ppl_ann ) ); okl_ann->setDisposeDataFunction( disposeAnnotation ); qCDebug(OkularPdfDebug) << okl_ann->uniqueName(); } void PopplerAnnotationProxy::notifyModification( const Okular::Annotation *okl_ann, int page, bool appearanceChanged ) { Q_UNUSED( page ); Q_UNUSED( appearanceChanged ); Poppler::Annotation *ppl_ann = qvariant_cast( okl_ann->nativeId() ); if ( !ppl_ann ) // Ignore non-native annotations return; QMutexLocker ml(mutex); if ( okl_ann->flags() & (Okular::Annotation::BeingMoved | Okular::Annotation::BeingResized) ) { // Okular ui already renders the annotation on its own ppl_ann->setFlags( Poppler::Annotation::Hidden ); return; } // Set basic properties // Note: flags and boundary must be set first in order to correctly handle // FixedRotation annotations. ppl_ann->setFlags(maskExportedFlags( okl_ann->flags() )); ppl_ann->setBoundary(normRectToRectF( okl_ann->boundingRectangle() )); ppl_ann->setAuthor( okl_ann->author() ); ppl_ann->setContents( okl_ann->contents() ); // Set style Poppler::Annotation::Style s; s.setColor( okl_ann->style().color() ); s.setWidth( okl_ann->style().width() ); s.setOpacity( okl_ann->style().opacity() ); ppl_ann->setStyle( s ); // Set type-specific properties (if any) switch ( ppl_ann->subType() ) { case Poppler::Annotation::AText: { const Okular::TextAnnotation * okl_txtann = static_cast(okl_ann); Poppler::TextAnnotation * ppl_txtann = static_cast(ppl_ann); ppl_txtann->setTextIcon( okl_txtann->textIcon() ); ppl_txtann->setTextFont( okl_txtann->textFont() ); #ifdef HAVE_POPPLER_0_69 ppl_txtann->setTextColor( okl_txtann->textColor() ); #endif //HAVE_POPPLER_0_69 ppl_txtann->setInplaceAlign( okl_txtann->inplaceAlignment() ); ppl_txtann->setCalloutPoints( QVector() ); ppl_txtann->setInplaceIntent( (Poppler::TextAnnotation::InplaceIntent)okl_txtann->inplaceIntent() ); break; } case Poppler::Annotation::ALine: { const Okular::LineAnnotation * okl_lineann = static_cast(okl_ann); Poppler::LineAnnotation * ppl_lineann = static_cast(ppl_ann); QLinkedList points; const QLinkedList annotPoints = okl_lineann->linePoints(); for ( const Okular::NormalizedPoint &p : annotPoints ) { points.append(normPointToPointF( p )); } ppl_lineann->setLinePoints( points ); ppl_lineann->setLineStartStyle( (Poppler::LineAnnotation::TermStyle)okl_lineann->lineStartStyle() ); ppl_lineann->setLineEndStyle( (Poppler::LineAnnotation::TermStyle)okl_lineann->lineEndStyle() ); ppl_lineann->setLineClosed( okl_lineann->lineClosed() ); ppl_lineann->setLineInnerColor( okl_lineann->lineInnerColor() ); ppl_lineann->setLineLeadingForwardPoint( okl_lineann->lineLeadingForwardPoint() ); ppl_lineann->setLineLeadingBackPoint( okl_lineann->lineLeadingBackwardPoint() ); ppl_lineann->setLineShowCaption( okl_lineann->showCaption() ); ppl_lineann->setLineIntent( (Poppler::LineAnnotation::LineIntent)okl_lineann->lineIntent() ); break; } case Poppler::Annotation::AGeom: { const Okular::GeomAnnotation * okl_geomann = static_cast(okl_ann); Poppler::GeomAnnotation * ppl_geomann = static_cast(ppl_ann); ppl_geomann->setGeomType( (Poppler::GeomAnnotation::GeomType)okl_geomann->geometricalType() ); ppl_geomann->setGeomInnerColor( okl_geomann->geometricalInnerColor() ); break; } case Poppler::Annotation::AHighlight: { const Okular::HighlightAnnotation * okl_hlann = static_cast(okl_ann); Poppler::HighlightAnnotation * ppl_hlann = static_cast(ppl_ann); ppl_hlann->setHighlightType( (Poppler::HighlightAnnotation::HighlightType)okl_hlann->highlightType() ); break; } case Poppler::Annotation::AStamp: { const Okular::StampAnnotation * okl_stampann = static_cast(okl_ann); Poppler::StampAnnotation * ppl_stampann = static_cast(ppl_ann); ppl_stampann->setStampIconName( okl_stampann->stampIconName() ); break; } case Poppler::Annotation::AInk: { const Okular::InkAnnotation * okl_inkann = static_cast(okl_ann); Poppler::InkAnnotation * ppl_inkann = static_cast(ppl_ann); QList< QLinkedList > paths; const QList< QLinkedList > inkPathsList = okl_inkann->inkPaths(); for ( const QLinkedList &path : inkPathsList ) { QLinkedList points; for ( const Okular::NormalizedPoint &p : path ) { points.append(normPointToPointF( p )); } paths.append( points ); } ppl_inkann->setInkPaths( paths ); break; } default: qCDebug(OkularPdfDebug) << "Type-specific property modification is not implemented for this annotation type"; break; } qCDebug(OkularPdfDebug) << okl_ann->uniqueName(); } void PopplerAnnotationProxy::notifyRemoval( Okular::Annotation *okl_ann, int page ) { Poppler::Annotation *ppl_ann = qvariant_cast( okl_ann->nativeId() ); if ( !ppl_ann ) // Ignore non-native annotations return; QMutexLocker ml(mutex); Poppler::Page *ppl_page = ppl_doc->page( page ); annotationsOnOpenHash->remove( okl_ann ); ppl_page->removeAnnotation( ppl_ann ); // Also destroys ppl_ann delete ppl_page; okl_ann->setNativeId( QVariant::fromValue(0) ); // So that we don't double-free in disposeAnnotation qCDebug(OkularPdfDebug) << okl_ann->uniqueName(); } //END PopplerAnnotationProxy implementation Okular::Annotation* createAnnotationFromPopplerAnnotation( Poppler::Annotation *ann, bool *doDelete ) { Okular::Annotation *annotation = 0; *doDelete = true; bool tieToOkularAnn = false; bool externallyDrawn = false; switch ( ann->subType() ) { case Poppler::Annotation::AFileAttachment: { Poppler::FileAttachmentAnnotation * attachann = static_cast< Poppler::FileAttachmentAnnotation * >( ann ); Okular::FileAttachmentAnnotation * f = new Okular::FileAttachmentAnnotation(); annotation = f; tieToOkularAnn = true; *doDelete = false; f->setFileIconName( attachann->fileIconName() ); f->setEmbeddedFile( new PDFEmbeddedFile( attachann->embeddedFile() ) ); break; } case Poppler::Annotation::ASound: { Poppler::SoundAnnotation * soundann = static_cast< Poppler::SoundAnnotation * >( ann ); Okular::SoundAnnotation * s = new Okular::SoundAnnotation(); annotation = s; s->setSoundIconName( soundann->soundIconName() ); s->setSound( createSoundFromPopplerSound( soundann->sound() ) ); break; } case Poppler::Annotation::AMovie: { Poppler::MovieAnnotation * movieann = static_cast< Poppler::MovieAnnotation * >( ann ); Okular::MovieAnnotation * m = new Okular::MovieAnnotation(); annotation = m; tieToOkularAnn = true; *doDelete = false; m->setMovie( createMovieFromPopplerMovie( movieann->movie() ) ); break; } case Poppler::Annotation::AWidget: { annotation = new Okular::WidgetAnnotation(); break; } case Poppler::Annotation::AScreen: { Okular::ScreenAnnotation * m = new Okular::ScreenAnnotation(); annotation = m; tieToOkularAnn = true; *doDelete = false; break; } case Poppler::Annotation::ARichMedia: { Poppler::RichMediaAnnotation * richmediaann = static_cast< Poppler::RichMediaAnnotation * >( ann ); const QPair result = createMovieFromPopplerRichMedia( richmediaann ); if ( result.first ) { Okular::RichMediaAnnotation * r = new Okular::RichMediaAnnotation(); tieToOkularAnn = true; *doDelete = false; annotation = r; r->setMovie( result.first ); r->setEmbeddedFile( result.second ); } break; } case Poppler::Annotation::AText: case Poppler::Annotation::ALine: case Poppler::Annotation::AGeom: case Poppler::Annotation::AHighlight: case Poppler::Annotation::AInk: case Poppler::Annotation::ACaret: externallyDrawn = true; /* fallthrough */ case Poppler::Annotation::AStamp: tieToOkularAnn = true; *doDelete = false; /* fallthrough */ default: { // this is uber ugly but i don't know a better way to do it without introducing a poppler::annotation dependency on core QDomDocument doc; QDomElement root = doc.createElement( QStringLiteral("root") ); doc.appendChild( root ); Poppler::AnnotationUtils::storeAnnotation( ann, root, doc ); annotation = Okular::AnnotationUtils::createAnnotation( root ); break; } } if ( annotation ) { // the Contents field might have lines separated by \r QString contents = ann->contents(); contents.replace( QLatin1Char( '\r' ), QLatin1Char( '\n' ) ); annotation->setAuthor( ann->author() ); annotation->setContents( contents ); annotation->setUniqueName( ann->uniqueName() ); annotation->setModificationDate( ann->modificationDate() ); annotation->setCreationDate( ann->creationDate() ); annotation->setFlags( ann->flags() | Okular::Annotation::External ); annotation->setBoundingRectangle( Okular::NormalizedRect::fromQRectF( ann->boundary() ) ); if (externallyDrawn) annotation->setFlags( annotation->flags() | Okular::Annotation::ExternallyDrawn ); // Poppler stores highlight points in swapped order if ( annotation->subType() == Okular::Annotation::AHighlight ) { Okular::HighlightAnnotation * hlann = static_cast( annotation ); - QList &quads = hlann->highlightQuads(); - for (QList::iterator it = quads.begin(); it != quads.end(); ++it) + for (Okular::HighlightAnnotation::Quad &quad : hlann->highlightQuads()) { - Okular::NormalizedPoint t; - t = it->point( 3 ); - it->setPoint( it->point(0), 3 ); - it->setPoint( t, 0 ); - t = it->point( 2 ); - it->setPoint( it->point(1), 2 ); - it->setPoint( t, 1 ); + const Okular::NormalizedPoint p3 = quad.point( 3 ); + quad.setPoint( quad.point(0), 3 ); + quad.setPoint( p3, 0 ); + const Okular::NormalizedPoint p2 = quad.point( 2 ); + quad.setPoint( quad.point(1), 2 ); + quad.setPoint( p2, 1 ); } } if ( annotation->subType() == Okular::Annotation::AText ) { Okular::TextAnnotation * txtann = static_cast( annotation ); if ( txtann->textType() == Okular::TextAnnotation::Linked ) { Poppler::TextAnnotation * ppl_txtann = static_cast( ann ); // Poppler and Okular assume a different default icon name in XML // We re-read it via getter, which always tells the right one txtann->setTextIcon( ppl_txtann->textIcon() ); } } // TODO clone style // TODO clone window // TODO clone revisions if ( tieToOkularAnn ) { annotation->setNativeId( QVariant::fromValue( ann ) ); annotation->setDisposeDataFunction( disposeAnnotation ); } } return annotation; } diff --git a/generators/xps/generator_xps.cpp b/generators/xps/generator_xps.cpp index 76b01b642..48b9feeae 100644 --- a/generators/xps/generator_xps.cpp +++ b/generators/xps/generator_xps.cpp @@ -1,2241 +1,2239 @@ /* 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 QChar slash = QChar::fromLatin1( '/' ); const int index = entry.lastIndexOf( slash ); QString ret = entry.mid( 0, index ); if ( index > 0 ) { ret.append( slash ); } if ( !ret.startsWith( slash ) ) ret.prepend( slash ); 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(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(); std::sort(entries.begin(), entries.end()); for ( const QString &entry : qAsConst(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(); std::sort(relEntries.begin(), relEntries.end()); for ( const QString &relEntry : qAsConst(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; for ( 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.) std::stable_sort( 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 ) { for ( 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 raw 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; } const QString absoluteFileName = absolutePath( entryPath( m_page->fileName() ), node.attributes.value(QStringLiteral("FontUri")) ); QFont font = m_page->m_file->getFontByName( absoluteFileName, 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 = QVariant::fromValue( 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 ); } for ( const XpsPathFigure *figure : qAsConst(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(); for ( const XpsRenderNode &child : qAsConst(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 = QVariant::fromValue( 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; } for ( const XpsRenderNode &child : qAsConst(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 ); for ( 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 = QVariant::fromValue( 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 = QVariant::fromValue( 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 = QVariant::fromValue( 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") ); + const 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 = QVariant::fromValue( QBrush( *qgrad ) ); delete qgrad; } } else if (node.name == QLatin1String("RadialGradientBrush")) { - XpsRenderNode * gradients = node.findChild( QStringLiteral("RadialGradientBrush.GradientStops") ); + const 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 = QVariant::fromValue( QBrush( *qgrad ) ); delete qgrad; } } else if (node.name == QLatin1String("LinearGradientBrush.GradientStops")) { QList gradients; for ( const XpsRenderNode &child : qAsConst(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 = QVariant::fromValue< QGradient * >( qgrad ); } } else if (node.name == QLatin1String("RadialGradientBrush.GradientStops")) { QList gradients; for ( const XpsRenderNode &child : qAsConst(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 = QVariant::fromValue< 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 &absoluteFileName, float size ) { // qCWarning(OkularXpsDebug) << "trying to get font: " << fileName << ", size: " << size; int index = m_fontCache.value(absoluteFileName, -1); if (index == -1) { index = loadFontByName(absoluteFileName); m_fontCache[absoluteFileName] = index; } if ( index == -1 ) { qCWarning(OkularXpsDebug) << "Requesting unknown font:" << absoluteFileName; 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:" << absoluteFileName << 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:" << absoluteFileName << index << fontFamily ; return QFont(); } const QString fontStyle = fontStyles[0]; return m_fontDatabase.font(fontFamily, fontStyle, qRound(size)); } int XpsFile::loadFontByName( const QString &absoluteFileName ) { // qCWarning(OkularXpsDebug) << "font file name: " << absoluteFileName; const KArchiveEntry* fontFile = loadEntry( m_xpsArchive, absoluteFileName, 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( absoluteFileName ); 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) const QString absoluteFileName = absolutePath( entryPath( m_fileName ), glyphsAtts.value( QStringLiteral("FontUri") ).toString() ); QFont font = m_file->getFontByName( absoluteFileName, 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" ); 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(); } } } if ( xml.error() ) { qCWarning(OkularXpsDebug) << "Could not parse XPS page relationships file ( " << documentRelationshipFile << " ) - " << xml.errorString(); } } 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 ); - } + qDeleteAll(m_pages); 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() { for(int fontId : qAsConst(m_fontCache)) { m_fontDatabase.removeApplicationFont(fontId); } } 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 ); 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::TextRequest * request ) { QMutexLocker lock( userMutex() ); 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 ) +const XpsRenderNode * XpsRenderNode::findChild( const QString &name ) const { - for (int i = 0; i < children.size(); i++) { - if (children[i].name == name) { - return &children[i]; + for (const XpsRenderNode &child : children) { + if (child.name == name) { + return &child; } } return nullptr; } -QVariant XpsRenderNode::getRequiredChildData( const QString &name ) +QVariant XpsRenderNode::getRequiredChildData( const QString &name ) const { - XpsRenderNode * child = findChild( name ); + const 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 ) +QVariant XpsRenderNode::getChildData( const QString &name ) const { - XpsRenderNode * child = findChild( name ); + const 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 41686cd73..256f69eb3 100644 --- a/generators/xps/generator_xps.h +++ b/generators/xps/generator_xps.h @@ -1,343 +1,343 @@ /* 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 ); + const XpsRenderNode * findChild( const QString &name ) const; + QVariant getRequiredChildData( const QString &name ) const; + QVariant getChildData( const QString &name ) const; }; 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 ); } XpsPathGeometry(const XpsPathGeometry &) = delete; XpsPathGeometry &operator=(const XpsPathGeometry &) = delete; QList< XpsPathFigure* > paths; Qt::FillRule fillRule; XpsMatrixTransform transform; }; class XpsPage; class XpsFile; class XpsHandler: public QXmlDefaultHandler { public: explicit XpsHandler( XpsPage *page ); ~XpsHandler() override; 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 different 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(); XpsPage(const XpsPage &) = delete; XpsPage &operator=(const XpsPage &) = delete; QSizeF size() const; bool renderToImage( QImage *p ); bool renderToPainter( QPainter *painter ); Okular::TextPage* textPage(); QImage loadImageFromFile( const QString &filename ); QString fileName() const { return m_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(); XpsDocument(const XpsDocument &) = delete; XpsDocument &operator=(const XpsDocument &) = delete; /** 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(); XpsFile(const XpsFile &) = delete; XpsFile &operator=(const XpsFile &) = delete; 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 &absoluteFileName, float size ); KZip* xpsArchive(); private: int loadFontByName( const QString &absoluteFileName ); 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 ); ~XpsGenerator() override; 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 *request ) override; Okular::TextPage* textPage( Okular::TextRequest * request ) override; private: XpsFile *m_xpsFile; }; Q_DECLARE_LOGGING_CATEGORY(OkularXpsDebug) #endif diff --git a/shell/shell.cpp b/shell/shell.cpp index 80165687c..eaf5f2def 100644 --- a/shell/shell.cpp +++ b/shell/shell.cpp @@ -1,835 +1,835 @@ /*************************************************************************** * Copyright (C) 2002 by Wilco Greven * * Copyright (C) 2002 by Chris Cheney * * Copyright (C) 2003 by Benjamin Meyer * * Copyright (C) 2003-2004 by Christophe Devriese * * * * Copyright (C) 2003 by Laurent Montel * * Copyright (C) 2003-2004 by Albert Astals Cid * * Copyright (C) 2003 by Luboš Luňák * * Copyright (C) 2003 by Malcolm Hunter * * Copyright (C) 2004 by Dominique Devriese * * Copyright (C) 2004 by Dirk Mueller * * 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 "shell.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 #ifdef WITH_KACTIVITIES #include #endif // local includes #include "kdocumentviewer.h" #include "../interfaces/viewerinterface.h" #include "shellutils.h" static const char *shouldShowMenuBarComingFromFullScreen = "shouldShowMenuBarComingFromFullScreen"; static const char *shouldShowToolBarComingFromFullScreen = "shouldShowToolBarComingFromFullScreen"; static const char* const SESSION_URL_KEY = "Urls"; static const char* const SESSION_TAB_KEY = "ActiveTab"; Shell::Shell( const QString &serializedOptions ) : KParts::MainWindow(), m_menuBarWasShown(true), m_toolBarWasShown(true) #ifndef Q_OS_WIN , m_activityResource(nullptr) #endif , m_isValid(true) { setObjectName( QStringLiteral( "okular::Shell#" ) ); setContextMenuPolicy( Qt::NoContextMenu ); // otherwise .rc file won't be found by unit test setComponentName(QStringLiteral("okular"), QString()); // set the shell's ui resource file setXMLFile(QStringLiteral("shell.rc")); m_fileformatsscanned = false; m_showMenuBarAction = nullptr; // this routine will find and load our Part. it finds the Part by // name which is a bad idea usually.. but it's alright in this // case since our Part is made for this Shell KPluginLoader loader(QStringLiteral("okularpart")); m_partFactory = loader.factory(); if (!m_partFactory) { // if we couldn't find our Part, we exit since the Shell by // itself can't do anything useful m_isValid = false; KMessageBox::error(this, i18n("Unable to find the Okular component: %1", loader.errorString())); return; } // now that the Part plugin is loaded, create the part KParts::ReadWritePart* const firstPart = m_partFactory->create< KParts::ReadWritePart >( this ); if (firstPart) { // Setup tab bar m_tabWidget = new QTabWidget( this ); m_tabWidget->setTabsClosable( true ); m_tabWidget->setElideMode( Qt::ElideRight ); m_tabWidget->tabBar()->hide(); m_tabWidget->setDocumentMode( true ); m_tabWidget->setMovable( true ); m_tabWidget->setAcceptDrops(true); m_tabWidget->installEventFilter(this); connect( m_tabWidget, &QTabWidget::currentChanged, this, &Shell::setActiveTab ); connect( m_tabWidget, &QTabWidget::tabCloseRequested, this, &Shell::closeTab ); connect( m_tabWidget->tabBar(), &QTabBar::tabMoved, this, &Shell::moveTabData ); setCentralWidget( m_tabWidget ); // then, setup our actions setupActions(); connect( QCoreApplication::instance(), &QCoreApplication::aboutToQuit, this, &QObject::deleteLater ); // and integrate the part's GUI with the shell's setupGUI(Keys | ToolBar | Save); createGUI(firstPart); connectPart( firstPart ); m_tabs.append( firstPart ); m_tabWidget->addTab( firstPart->widget(), QString() ); readSettings(); m_unique = ShellUtils::unique(serializedOptions); if (m_unique) { m_unique = QDBusConnection::sessionBus().registerService(QStringLiteral("org.kde.okular")); if (!m_unique) KMessageBox::information(this, i18n("There is already a unique Okular instance running. This instance won't be the unique one.")); } else { QString serviceName = QStringLiteral("org.kde.okular-") + QString::number(qApp->applicationPid()); QDBusConnection::sessionBus().registerService(serviceName); } if (ShellUtils::noRaise(serializedOptions)) { setAttribute(Qt::WA_ShowWithoutActivating); } QDBusConnection::sessionBus().registerObject(QStringLiteral("/okularshell"), this, QDBusConnection::ExportScriptableSlots); } else { m_isValid = false; KMessageBox::error(this, i18n("Unable to find the Okular component.")); } } bool Shell::eventFilter(QObject *obj, QEvent *event) { Q_UNUSED(obj); QDragMoveEvent* dmEvent = dynamic_cast(event); if (dmEvent) { bool accept = dmEvent->mimeData()->hasUrls(); event->setAccepted(accept); return accept; } QDropEvent* dEvent = dynamic_cast(event); if (dEvent) { const QList list = KUrlMimeData::urlsFromMimeData(dEvent->mimeData()); handleDroppedUrls(list); dEvent->setAccepted(true); return true; } // Handle middle button click events on the tab bar if (obj == m_tabWidget && event->type() == QEvent::MouseButtonRelease) { QMouseEvent* mEvent = static_cast(event); if (mEvent->button() == Qt::MiddleButton) { int tabIndex = m_tabWidget->tabBar()->tabAt(mEvent->pos()); if (tabIndex != -1) { closeTab(tabIndex); return true; } } } return false; } bool Shell::isValid() const { return m_isValid; } void Shell::showOpenRecentMenu() { m_recent->menu()->popup(QCursor::pos()); } Shell::~Shell() { if( !m_tabs.empty() ) { writeSettings(); - for( QList::iterator it = m_tabs.begin(); it != m_tabs.end(); ++it ) + for( const TabState &tab : qAsConst( m_tabs ) ) { - it->part->closeUrl( false ); + tab.part->closeUrl( false ); } m_tabs.clear(); } if (m_unique) QDBusConnection::sessionBus().unregisterService(QStringLiteral("org.kde.okular")); delete m_tabWidget; } // Open a new document if we have space for it // This can hang if called on a unique instance and openUrl pops a messageBox bool Shell::openDocument( const QUrl& url, const QString &serializedOptions ) { if( m_tabs.size() <= 0 ) return false; KParts::ReadWritePart* const part = m_tabs[0].part; // Return false if we can't open new tabs and the only part is occupied if ( !dynamic_cast(part)->openNewFilesInTabs() && !part->url().isEmpty() && !ShellUtils::unique(serializedOptions)) { return false; } openUrl( url, serializedOptions ); return true; } bool Shell::openDocument( const QString& urlString, const QString &serializedOptions ) { return openDocument(QUrl(urlString), serializedOptions); } bool Shell::canOpenDocs( int numDocs, int desktop ) { if( m_tabs.size() <= 0 || numDocs <= 0 || m_unique ) return false; KParts::ReadWritePart* const part = m_tabs[0].part; const bool allowTabs = dynamic_cast(part)->openNewFilesInTabs(); if( !allowTabs && (numDocs > 1 || !part->url().isEmpty()) ) return false; const KWindowInfo winfo( window()->effectiveWinId(), KWindowSystem::WMDesktop ); if( winfo.desktop() != desktop ) return false; return true; } void Shell::openUrl( const QUrl & url, const QString &serializedOptions ) { const int activeTab = m_tabWidget->currentIndex(); if ( activeTab < m_tabs.size() ) { KParts::ReadWritePart* const activePart = m_tabs[activeTab].part; if( !activePart->url().isEmpty() ) { if( m_unique ) { applyOptionsToPart( activePart, serializedOptions ); activePart->openUrl( url ); } else { if( dynamic_cast(activePart)->openNewFilesInTabs() ) { openNewTab( url, serializedOptions ); } else { Shell* newShell = new Shell( serializedOptions ); newShell->show(); newShell->openUrl( url, serializedOptions ); } } } else { m_tabWidget->setTabText( activeTab, url.fileName() ); applyOptionsToPart( activePart, serializedOptions ); bool openOk = activePart->openUrl( url ); const bool isstdin = url.fileName() == QLatin1String( "-" ) || url.scheme() == QLatin1String( "fd" ); if ( !isstdin ) { if ( openOk ) { #ifdef WITH_KACTIVITIES if ( !m_activityResource ) m_activityResource = new KActivities::ResourceInstance( window()->winId(), this ); m_activityResource->setUri( url ); #endif m_recent->addUrl( url ); } else m_recent->removeUrl( url ); } } } } void Shell::closeUrl() { closeTab( m_tabWidget->currentIndex() ); } void Shell::readSettings() { m_recent->loadEntries( KSharedConfig::openConfig()->group( "Recent Files" ) ); m_recent->setEnabled( true ); // force enabling const KConfigGroup group = KSharedConfig::openConfig()->group( "Desktop Entry" ); bool fullScreen = group.readEntry( "FullScreen", false ); setFullScreen( fullScreen ); if (fullScreen) { m_menuBarWasShown = group.readEntry( shouldShowMenuBarComingFromFullScreen, true ); m_toolBarWasShown = group.readEntry( shouldShowToolBarComingFromFullScreen, true ); } } void Shell::writeSettings() { m_recent->saveEntries( KSharedConfig::openConfig()->group( "Recent Files" ) ); KConfigGroup group = KSharedConfig::openConfig()->group( "Desktop Entry" ); group.writeEntry( "FullScreen", m_fullScreenAction->isChecked() ); if (m_fullScreenAction->isChecked()) { group.writeEntry( shouldShowMenuBarComingFromFullScreen, m_menuBarWasShown ); group.writeEntry( shouldShowToolBarComingFromFullScreen, m_toolBarWasShown ); } KSharedConfig::openConfig()->sync(); } void Shell::setupActions() { KStandardAction::open(this, SLOT(fileOpen()), actionCollection()); m_recent = KStandardAction::openRecent( this, SLOT(openUrl(QUrl)), actionCollection() ); m_recent->setToolBarMode( KRecentFilesAction::MenuMode ); connect( m_recent, &QAction::triggered, this, &Shell::showOpenRecentMenu ); m_recent->setToolTip( i18n("Click to open a file\nClick and hold to open a recent file") ); m_recent->setWhatsThis( i18n( "Click to open a file or Click and hold to select a recent file" ) ); m_printAction = KStandardAction::print( this, SLOT(print()), actionCollection() ); m_printAction->setEnabled( false ); m_closeAction = KStandardAction::close( this, SLOT(closeUrl()), actionCollection() ); m_closeAction->setEnabled( false ); KStandardAction::quit(this, SLOT(close()), actionCollection()); setStandardToolBarMenuEnabled(true); m_showMenuBarAction = KStandardAction::showMenubar( this, SLOT(slotShowMenubar()), actionCollection()); m_fullScreenAction = KStandardAction::fullScreen( this, SLOT(slotUpdateFullScreen()), this,actionCollection() ); m_nextTabAction = actionCollection()->addAction(QStringLiteral("tab-next")); m_nextTabAction->setText( i18n("Next Tab") ); actionCollection()->setDefaultShortcuts(m_nextTabAction, KStandardShortcut::tabNext()); m_nextTabAction->setEnabled( false ); connect( m_nextTabAction, &QAction::triggered, this, &Shell::activateNextTab ); m_prevTabAction = actionCollection()->addAction(QStringLiteral("tab-previous")); m_prevTabAction->setText( i18n("Previous Tab") ); actionCollection()->setDefaultShortcuts(m_prevTabAction, KStandardShortcut::tabPrev()); m_prevTabAction->setEnabled( false ); connect( m_prevTabAction, &QAction::triggered, this, &Shell::activatePrevTab ); m_undoCloseTab = actionCollection()->addAction(QStringLiteral("undo-close-tab")); m_undoCloseTab->setText( i18n("Undo close tab") ); actionCollection()->setDefaultShortcut(m_undoCloseTab, QKeySequence(Qt::CTRL + Qt::SHIFT + Qt::Key_T)); m_undoCloseTab->setIcon(QIcon::fromTheme(QStringLiteral("edit-undo"))); m_undoCloseTab->setEnabled( false ); connect( m_undoCloseTab, &QAction::triggered, this, &Shell::undoCloseTab ); } void Shell::saveProperties(KConfigGroup &group) { if ( !m_isValid ) // part couldn't be loaded, nothing to save return; // Gather lists of settings to preserve QStringList urls; - for( int i = 0; i < m_tabs.size(); ++i ) + for( const TabState &tab : qAsConst( m_tabs ) ) { - urls.append( m_tabs[i].part->url().url() ); + urls.append( tab.part->url().url() ); } group.writePathEntry( SESSION_URL_KEY, urls ); group.writeEntry( SESSION_TAB_KEY, m_tabWidget->currentIndex() ); } void Shell::readProperties(const KConfigGroup &group) { // Reopen documents based on saved settings QStringList urls = group.readPathEntry( SESSION_URL_KEY, QStringList() ); int desiredTab = group.readEntry( SESSION_TAB_KEY, 0 ); while( !urls.isEmpty() ) { openUrl( QUrl(urls.takeFirst()) ); } if( desiredTab < m_tabs.size() ) { setActiveTab( desiredTab ); } } QStringList Shell::fileFormats() const { QStringList supportedPatterns; QString constraint( QStringLiteral("(Library == 'okularpart')") ); QLatin1String basePartService( "KParts/ReadOnlyPart" ); KService::List offers = KServiceTypeTrader::self()->query( basePartService, constraint ); KService::List::ConstIterator it = offers.constBegin(), itEnd = offers.constEnd(); for ( ; it != itEnd; ++it ) { KService::Ptr service = *it; QStringList mimeTypes = service->mimeTypes(); supportedPatterns += mimeTypes; } return supportedPatterns; } void Shell::fileOpen() { // this slot is called whenever the File->Open menu is selected, // the Open shortcut is pressed (usually CTRL+O) or the Open toolbar // button is clicked const int activeTab = m_tabWidget->currentIndex(); if ( !m_fileformatsscanned ) { const KDocumentViewer* const doc = qobject_cast(m_tabs[activeTab].part); if ( doc ) m_fileformats = doc->supportedMimeTypes(); if ( m_fileformats.isEmpty() ) m_fileformats = fileFormats(); m_fileformatsscanned = true; } QUrl startDir; const KParts::ReadWritePart* const curPart = m_tabs[activeTab].part; if ( curPart->url().isLocalFile() ) startDir = KIO::upUrl(curPart->url()); QPointer dlg( new QFileDialog( this )); dlg->setDirectoryUrl( startDir ); dlg->setAcceptMode( QFileDialog::AcceptOpen ); dlg->setOption( QFileDialog::HideNameFilterDetails, true ); dlg->setFileMode( QFileDialog::ExistingFiles ); // Allow selection of more than one file QMimeDatabase mimeDatabase; QSet globPatterns; QMap namedGlobs; for ( const QString &mimeName : qAsConst(m_fileformats) ) { QMimeType mimeType = mimeDatabase.mimeTypeForName( mimeName ); const QStringList globs( mimeType.globPatterns() ); if ( globs.isEmpty() ) { continue; } globPatterns.unite( globs.toSet() ) ; namedGlobs[ mimeType.comment() ].append( globs ); } QStringList namePatterns; for ( auto it = namedGlobs.cbegin(); it != namedGlobs.cend(); ++it ) { namePatterns.append( it.key() + QLatin1String(" (") + it.value().join( QLatin1Char(' ') ) + QLatin1Char(')') ); } namePatterns.prepend( i18n("All files (*)") ); namePatterns.prepend( i18n("All supported files (%1)", globPatterns.values().join( QLatin1Char(' ') ) ) ); dlg->setNameFilters( namePatterns ); dlg->setWindowTitle( i18n("Open Document") ); if ( dlg->exec() && dlg ) { const QList urlList = dlg->selectedUrls(); for (const QUrl &url : urlList) { openUrl( url ); } } if ( dlg ) { delete dlg.data(); } } void Shell::tryRaise() { KWindowSystem::forceActiveWindow( window()->effectiveWinId() ); } // only called when starting the program void Shell::setFullScreen( bool useFullScreen ) { if( useFullScreen ) setWindowState( windowState() | Qt::WindowFullScreen ); // set else setWindowState( windowState() & ~Qt::WindowFullScreen ); // reset } void Shell::setCaption( const QString &caption ) { bool modified = false; const int activeTab = m_tabWidget->currentIndex(); if ( activeTab >= 0 && activeTab < m_tabs.size() ) { KParts::ReadWritePart* const activePart = m_tabs[activeTab].part; QString tabCaption = activePart->url().fileName(); if ( activePart->isModified() ) { modified = true; if ( !tabCaption.isEmpty() ) { tabCaption.append( QStringLiteral( " *" ) ); } } m_tabWidget->setTabText( activeTab, tabCaption ); } setCaption( caption, modified ); } void Shell::showEvent(QShowEvent *e) { if (!menuBar()->isNativeMenuBar() && m_showMenuBarAction) m_showMenuBarAction->setChecked( menuBar()->isVisible() ); KParts::MainWindow::showEvent(e); } void Shell::slotUpdateFullScreen() { if(m_fullScreenAction->isChecked()) { m_menuBarWasShown = !menuBar()->isHidden(); menuBar()->hide(); m_toolBarWasShown = !toolBar()->isHidden(); toolBar()->hide(); KToggleFullScreenAction::setFullScreen(this, true); } else { if (m_menuBarWasShown) { menuBar()->show(); } if (m_toolBarWasShown) { toolBar()->show(); } KToggleFullScreenAction::setFullScreen(this, false); } } void Shell::slotShowMenubar() { if ( menuBar()->isHidden() ) menuBar()->show(); else menuBar()->hide(); } QSize Shell::sizeHint() const { return QApplication::desktop()->availableGeometry( this ).size() * 0.75; } bool Shell::queryClose() { if (m_tabs.count() > 1) { const QString dontAskAgainName = QStringLiteral("ShowTabWarning"); KMessageBox::ButtonCode dummy = KMessageBox::Yes; if (shouldBeShownYesNo(dontAskAgainName, dummy)) { QDialog *dialog = new QDialog(this); dialog->setWindowTitle(i18n("Confirm Close")); QDialogButtonBox *buttonBox = new QDialogButtonBox(dialog); buttonBox->setStandardButtons(QDialogButtonBox::Yes | QDialogButtonBox::No); KGuiItem::assign(buttonBox->button(QDialogButtonBox::Yes), KGuiItem(i18n("Close Tabs"), QStringLiteral("tab-close"))); KGuiItem::assign(buttonBox->button(QDialogButtonBox::No), KStandardGuiItem::cancel()); bool checkboxResult = true; const int result = KMessageBox::createKMessageBox(dialog, buttonBox, QMessageBox::Question, i18n("You are about to close %1 tabs. Are you sure you want to continue?", m_tabs.count()), QStringList(), i18n("Warn me when I attempt to close multiple tabs"), &checkboxResult, KMessageBox::Notify); if (!checkboxResult) { saveDontShowAgainYesNo(dontAskAgainName, dummy); } if (result != QDialogButtonBox::Yes) { return false; } } } for( int i = 0; i < m_tabs.size(); ++i ) { KParts::ReadWritePart* const part = m_tabs[i].part; // To resolve confusion about multiple modified docs, switch to relevant tab if( part->isModified() ) setActiveTab( i ); if( !part->queryClose() ) return false; } return true; } void Shell::setActiveTab( int tab ) { m_tabWidget->setCurrentIndex( tab ); createGUI( m_tabs[tab].part ); m_printAction->setEnabled( m_tabs[tab].printEnabled ); m_closeAction->setEnabled( m_tabs[tab].closeEnabled ); } void Shell::closeTab( int tab ) { KParts::ReadWritePart* const part = m_tabs[tab].part; QUrl url = part->url(); if( part->closeUrl() && m_tabs.count() > 1 ) { if( part->factory() ) part->factory()->removeClient( part ); part->disconnect(); part->deleteLater(); m_tabs.removeAt( tab ); m_tabWidget->removeTab( tab ); m_undoCloseTab->setEnabled( true ); m_closedTabUrls.append( url ); if( m_tabWidget->count() == 1 ) { m_tabWidget->tabBar()->hide(); m_nextTabAction->setEnabled( false ); m_prevTabAction->setEnabled( false ); } } } void Shell::openNewTab( const QUrl& url, const QString &serializedOptions ) { // Tabs are hidden when there's only one, so show it if( m_tabs.size() == 1 ) { m_tabWidget->tabBar()->show(); m_nextTabAction->setEnabled( true ); m_prevTabAction->setEnabled( true ); } const int newIndex = m_tabs.size(); // Make new part m_tabs.append( m_partFactory->create(this) ); connectPart( m_tabs[newIndex].part ); // Update GUI KParts::ReadWritePart* const part = m_tabs[newIndex].part; m_tabWidget->addTab( part->widget(), url.fileName() ); applyOptionsToPart(part, serializedOptions); int previousActiveTab = m_tabWidget->currentIndex(); setActiveTab( m_tabs.size() - 1 ); if( part->openUrl(url) ) m_recent->addUrl( url ); else setActiveTab( previousActiveTab ); } void Shell::applyOptionsToPart( QObject* part, const QString &serializedOptions ) { KDocumentViewer* const doc = qobject_cast(part); const QString find = ShellUtils::find(serializedOptions); if ( ShellUtils::startInPresentation(serializedOptions) ) doc->startPresentation(); if ( ShellUtils::showPrintDialog(serializedOptions) ) QMetaObject::invokeMethod( part, "enableStartWithPrint" ); if ( ShellUtils::showPrintDialogAndExit(serializedOptions) ) QMetaObject::invokeMethod( part, "enableExitAfterPrint" ); if(!find.isEmpty()) QMetaObject::invokeMethod( part, "enableStartWithFind", Q_ARG( const QString &, find )); } void Shell::connectPart( QObject* part ) { connect( this, SIGNAL(moveSplitter(int)), part, SLOT(moveSplitter(int)) ); connect( part, SIGNAL(enablePrintAction(bool)), this, SLOT(setPrintEnabled(bool))); connect( part, SIGNAL(enableCloseAction(bool)), this, SLOT(setCloseEnabled(bool))); connect( part, SIGNAL(mimeTypeChanged(QMimeType)), this, SLOT(setTabIcon(QMimeType))); connect( part, SIGNAL(urlsDropped(QList)), this, SLOT(handleDroppedUrls(QList)) ); connect( part, SIGNAL(fitWindowToPage(QSize,QSize)), this, SLOT(slotFitWindowToPage(QSize,QSize)) ); } void Shell::print() { QMetaObject::invokeMethod( m_tabs[m_tabWidget->currentIndex()].part, "slotPrint" ); } void Shell::setPrintEnabled( bool enabled ) { int i = findTabIndex( sender() ); if( i != -1 ) { m_tabs[i].printEnabled = enabled; if( i == m_tabWidget->currentIndex() ) m_printAction->setEnabled( enabled ); } } void Shell::setCloseEnabled( bool enabled ) { int i = findTabIndex( sender() ); if( i != -1 ) { m_tabs[i].closeEnabled = enabled; if( i == m_tabWidget->currentIndex() ) m_closeAction->setEnabled( enabled ); } } void Shell::activateNextTab() { if( m_tabs.size() < 2 ) return; const int activeTab = m_tabWidget->currentIndex(); const int nextTab = (activeTab == m_tabs.size()-1) ? 0 : activeTab+1; setActiveTab( nextTab ); } void Shell::activatePrevTab() { if( m_tabs.size() < 2 ) return; const int activeTab = m_tabWidget->currentIndex(); const int prevTab = (activeTab == 0) ? m_tabs.size()-1 : activeTab-1; setActiveTab( prevTab ); } void Shell::undoCloseTab() { if ( m_closedTabUrls.isEmpty() ) { return; } const QUrl lastTabUrl = m_closedTabUrls.takeLast(); if ( m_closedTabUrls.isEmpty() ) { m_undoCloseTab->setEnabled(false); } openUrl(lastTabUrl); } void Shell::setTabIcon( const QMimeType& mimeType ) { int i = findTabIndex( sender() ); if( i != -1 ) { m_tabWidget->setTabIcon( i, QIcon::fromTheme(mimeType.iconName()) ); } } int Shell::findTabIndex( QObject* sender ) { for( int i = 0; i < m_tabs.size(); ++i ) { if( m_tabs[i].part == sender ) { return i; } } return -1; } void Shell::handleDroppedUrls( const QList& urls ) { for ( const QUrl &url : urls ) { openUrl( url ); } } void Shell::moveTabData( int from, int to ) { m_tabs.move( from, to ); } void Shell::slotFitWindowToPage(const QSize& pageViewSize, const QSize& pageSize ) { const int xOffset = pageViewSize.width() - pageSize.width(); const int yOffset = pageViewSize.height() - pageSize.height(); showNormal(); resize( width() - xOffset, height() - yOffset); moveSplitter(pageSize.width()); } /* kate: replace-tabs on; indent-width 4; */ diff --git a/ui/annotationproxymodels.cpp b/ui/annotationproxymodels.cpp index ea8f43dd4..53f67734d 100644 --- a/ui/annotationproxymodels.cpp +++ b/ui/annotationproxymodels.cpp @@ -1,621 +1,622 @@ /*************************************************************************** * 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 "annotationproxymodels.h" #include #include #include #include "annotationmodel.h" #include "debug_ui.h" static quint32 mixIndex( int row, int column ) { return ( row << 4 ) | column; } PageFilterProxyModel::PageFilterProxyModel( QObject *parent ) : QSortFilterProxyModel( parent ), mGroupByCurrentPage( false ), mCurrentPage( -1 ) { setDynamicSortFilter( true ); } void PageFilterProxyModel::groupByCurrentPage( bool value ) { if ( mGroupByCurrentPage == value ) return; mGroupByCurrentPage = value; invalidateFilter(); } void PageFilterProxyModel::setCurrentPage( int page ) { if ( mCurrentPage == page ) return; mCurrentPage = page; // no need to invalidate when we're not showing the current page only if ( !mGroupByCurrentPage ) return; invalidateFilter(); } bool PageFilterProxyModel::filterAcceptsRow( int row, const QModelIndex &sourceParent ) const { if ( !mGroupByCurrentPage ) return true; const QModelIndex pageIndex = sourceModel()->index( row, 0, sourceParent ); int page = sourceModel()->data( pageIndex, AnnotationModel::PageRole ).toInt(); return (page == mCurrentPage); } PageGroupProxyModel::PageGroupProxyModel( QObject *parent ) : QAbstractProxyModel( parent ), mGroupByPage( false ) { } int PageGroupProxyModel::columnCount( const QModelIndex &parentIndex ) const { // For top-level and second level we have always only one column if ( mGroupByPage ) { if ( parentIndex.isValid() ) { if ( parentIndex.parent().isValid() ) return 0; else { return 1; // second-level } } else { return 1; // top-level } } else { if ( !parentIndex.isValid() ) // top-level return 1; else return 0; } return 1; } int PageGroupProxyModel::rowCount( const QModelIndex &parentIndex ) const { if ( mGroupByPage ) { if ( parentIndex.isValid() ) { if ( parentIndex.parent().isValid() ) return 0; else { return mTreeIndexes[ parentIndex.row() ].second.count(); // second-level } } else { return mTreeIndexes.count(); // top-level } } else { if ( !parentIndex.isValid() ) // top-level return mIndexes.count(); else return 0; } } QModelIndex PageGroupProxyModel::index( int row, int column, const QModelIndex &parentIndex ) const { if ( row < 0 || column != 0 ) return QModelIndex(); if ( mGroupByPage ) { if ( parentIndex.isValid() ) { if ( parentIndex.row() >= 0 && parentIndex.row() < mTreeIndexes.count() && row < mTreeIndexes[ parentIndex.row() ].second.count() ) return createIndex( row, column, qint32( parentIndex.row() + 1 ) ); else return QModelIndex(); } else { if ( row < mTreeIndexes.count() ) return createIndex( row, column ); else return QModelIndex(); } } else { if ( row < mIndexes.count() ) return createIndex( row, column, mixIndex( parentIndex.row(), parentIndex.column() ) ); else return QModelIndex(); } } QModelIndex PageGroupProxyModel::parent( const QModelIndex &idx ) const { if ( mGroupByPage ) { if ( idx.internalId() == 0 ) // top-level return QModelIndex(); else return index( idx.internalId() - 1, idx.column() ); } else { // We have only top-level items return QModelIndex(); } } QModelIndex PageGroupProxyModel::mapFromSource( const QModelIndex &sourceIndex ) const { if ( mGroupByPage ) { if ( sourceIndex.parent().isValid() ) { return index( sourceIndex.row(), sourceIndex.column(), sourceIndex.parent() ); } else { return index( sourceIndex.row(), sourceIndex.column() ); } } else { for ( int i = 0; i < mIndexes.count(); ++i ) { if ( mIndexes[ i ] == sourceIndex ) return index( i, 0 ); } return QModelIndex(); } } QModelIndex PageGroupProxyModel::mapToSource( const QModelIndex &proxyIndex ) const { if ( !proxyIndex.isValid() ) return QModelIndex(); if ( mGroupByPage ) { if ( proxyIndex.internalId() == 0 ) { if ( proxyIndex.row() >= mTreeIndexes.count() || proxyIndex.row() < 0 ) return QModelIndex(); return mTreeIndexes[ proxyIndex.row() ].first; } else { if ( qint32(proxyIndex.internalId()) - 1 >= mTreeIndexes.count() || proxyIndex.row() >= mTreeIndexes[ proxyIndex.internalId() - 1 ].second.count() ) return QModelIndex(); return mTreeIndexes[ proxyIndex.internalId() - 1 ].second[ proxyIndex.row() ]; } } else { if ( proxyIndex.column() > 0 || proxyIndex.row() >= mIndexes.count() ) return QModelIndex(); else { return mIndexes[ proxyIndex.row() ]; } } } void PageGroupProxyModel::setSourceModel( QAbstractItemModel *model ) { if ( sourceModel() ) { disconnect( sourceModel(), &QAbstractItemModel::layoutChanged, this, &PageGroupProxyModel::rebuildIndexes ); disconnect( sourceModel(), &QAbstractItemModel::modelReset, this, &PageGroupProxyModel::rebuildIndexes ); disconnect( sourceModel(), &QAbstractItemModel::rowsInserted, this, &PageGroupProxyModel::rebuildIndexes ); disconnect( sourceModel(), &QAbstractItemModel::rowsRemoved, this, &PageGroupProxyModel::rebuildIndexes ); disconnect( sourceModel(), &QAbstractItemModel::dataChanged, this, &PageGroupProxyModel::sourceDataChanged ); } QAbstractProxyModel::setSourceModel( model ); connect( sourceModel(), &QAbstractItemModel::layoutChanged, this, &PageGroupProxyModel::rebuildIndexes ); connect( sourceModel(), &QAbstractItemModel::modelReset, this, &PageGroupProxyModel::rebuildIndexes ); connect( sourceModel(), &QAbstractItemModel::rowsInserted, this, &PageGroupProxyModel::rebuildIndexes ); connect( sourceModel(), &QAbstractItemModel::rowsRemoved, this, &PageGroupProxyModel::rebuildIndexes ); connect( sourceModel(), &QAbstractItemModel::dataChanged, this, &PageGroupProxyModel::sourceDataChanged ); rebuildIndexes(); } void PageGroupProxyModel::rebuildIndexes() { beginResetModel(); if ( mGroupByPage ) { mTreeIndexes.clear(); for ( int row = 0; row < sourceModel()->rowCount(); ++row ) { const QModelIndex pageIndex = sourceModel()->index( row, 0 ); QList itemIndexes; for ( int subRow = 0; subRow < sourceModel()->rowCount( pageIndex ); ++subRow ) { itemIndexes.append( sourceModel()->index( subRow, 0, pageIndex ) ); } mTreeIndexes.append( QPair >( pageIndex, itemIndexes ) ); } } else { mIndexes.clear(); for ( int row = 0; row < sourceModel()->rowCount(); ++row ) { const QModelIndex pageIndex = sourceModel()->index( row, 0 ); for ( int subRow = 0; subRow < sourceModel()->rowCount( pageIndex ); ++subRow ) { mIndexes.append( sourceModel()->index( subRow, 0, pageIndex ) ); } } } endResetModel(); } void PageGroupProxyModel::sourceDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector &roles) { emit dataChanged(mapFromSource(topLeft), mapFromSource(bottomRight), roles); } void PageGroupProxyModel::groupByPage( bool value ) { if ( mGroupByPage == value ) return; mGroupByPage = value; rebuildIndexes(); } class AuthorGroupItem { public: enum Type { Page, Author, Annotation }; AuthorGroupItem( AuthorGroupItem *parent, Type type = Page, const QModelIndex &index = QModelIndex() ) : mParent( parent ), mType( type ), mIndex( index ) { } ~AuthorGroupItem() { qDeleteAll( mChilds ); } AuthorGroupItem(const AuthorGroupItem &) = delete; AuthorGroupItem &operator=(const AuthorGroupItem &) = delete; void appendChild( AuthorGroupItem *child ) { mChilds.append( child ); } AuthorGroupItem* parent() const { return mParent; } AuthorGroupItem* child( int row ) const { return mChilds.value( row ); } int childCount() const { return mChilds.count(); } void dump( int level = 0 ) { QString prefix; for ( int i = 0; i < level; ++i ) prefix += QLatin1Char(' '); qCDebug(OkularUiDebug, "%s%s", qPrintable( prefix ), ( mType == Page ? "Page" : (mType == Author ? "Author" : "Annotation") ) ); for ( int i = 0; i < mChilds.count(); ++i ) mChilds[ i ]->dump( level + 2 ); } const AuthorGroupItem* findIndex( const QModelIndex &index ) const { if ( index == mIndex ) return this; for ( int i = 0; i < mChilds.count(); ++i ) { const AuthorGroupItem *item = mChilds[ i ]->findIndex( index ); if ( item ) return item; } return nullptr; } int row() const { return ( mParent ? mParent->mChilds.indexOf( const_cast( this ) ) : 0 ); } Type type() const { return mType; } QModelIndex index() const { return mIndex; } void setAuthor( const QString &author ) { mAuthor = author; } QString author() const { return mAuthor; } private: AuthorGroupItem *mParent; Type mType; QModelIndex mIndex; QList mChilds; QString mAuthor; }; class AuthorGroupProxyModel::Private { public: Private( AuthorGroupProxyModel *parent ) : mParent( parent ), mRoot( nullptr ), mGroupByAuthor( false ) { } ~Private() { delete mRoot; } AuthorGroupProxyModel *mParent; AuthorGroupItem *mRoot; bool mGroupByAuthor; }; AuthorGroupProxyModel::AuthorGroupProxyModel( QObject *parent ) : QAbstractProxyModel( parent ), d( new Private( this ) ) { } AuthorGroupProxyModel::~AuthorGroupProxyModel() { delete d; } int AuthorGroupProxyModel::columnCount( const QModelIndex& ) const { return 1; } int AuthorGroupProxyModel::rowCount( const QModelIndex &parentIndex ) const { AuthorGroupItem *item = nullptr; if ( !parentIndex.isValid() ) item = d->mRoot; else item = static_cast( parentIndex.internalPointer() ); return item ? item->childCount() : 0; } QModelIndex AuthorGroupProxyModel::index( int row, int column, const QModelIndex &parentIndex ) const { if ( !hasIndex( row, column, parentIndex ) ) return QModelIndex(); AuthorGroupItem *parentItem = nullptr; if ( !parentIndex.isValid() ) parentItem = d->mRoot; else parentItem = static_cast( parentIndex.internalPointer() ); AuthorGroupItem *child = parentItem->child( row ); if ( child ) return createIndex( row, column, child ); else return QModelIndex(); } QModelIndex AuthorGroupProxyModel::parent( const QModelIndex &index ) const { if ( !index.isValid() ) return QModelIndex(); AuthorGroupItem *childItem = static_cast( index.internalPointer() ); AuthorGroupItem *parentItem = childItem->parent(); if ( parentItem == d->mRoot ) return QModelIndex(); else return createIndex( parentItem->row(), 0, parentItem ); } QModelIndex AuthorGroupProxyModel::mapFromSource( const QModelIndex &sourceIndex ) const { if ( !sourceIndex.isValid() ) return QModelIndex(); const AuthorGroupItem *item = d->mRoot->findIndex( sourceIndex ); if ( !item ) return QModelIndex(); return createIndex( item->row(), 0, const_cast( item ) ); } QModelIndex AuthorGroupProxyModel::mapToSource( const QModelIndex &proxyIndex ) const { if ( !proxyIndex.isValid() ) return QModelIndex(); AuthorGroupItem *item = static_cast( proxyIndex.internalPointer() ); return item->index(); } void AuthorGroupProxyModel::setSourceModel( QAbstractItemModel *model ) { if ( sourceModel() ) { disconnect( sourceModel(), &QAbstractItemModel::layoutChanged, this, &AuthorGroupProxyModel::rebuildIndexes ); disconnect( sourceModel(), &QAbstractItemModel::modelReset, this, &AuthorGroupProxyModel::rebuildIndexes ); disconnect( sourceModel(), &QAbstractItemModel::rowsInserted, this, &AuthorGroupProxyModel::rebuildIndexes ); disconnect( sourceModel(), &QAbstractItemModel::rowsRemoved, this, &AuthorGroupProxyModel::rebuildIndexes ); disconnect( sourceModel(), &QAbstractItemModel::dataChanged, this, &AuthorGroupProxyModel::sourceDataChanged ); } QAbstractProxyModel::setSourceModel( model ); connect( sourceModel(), &QAbstractItemModel::layoutChanged, this, &AuthorGroupProxyModel::rebuildIndexes ); connect( sourceModel(), &QAbstractItemModel::modelReset, this, &AuthorGroupProxyModel::rebuildIndexes ); connect( sourceModel(), &QAbstractItemModel::rowsInserted, this, &AuthorGroupProxyModel::rebuildIndexes ); connect( sourceModel(), &QAbstractItemModel::rowsRemoved, this, &AuthorGroupProxyModel::rebuildIndexes ); connect( sourceModel(), &QAbstractItemModel::dataChanged, this, &AuthorGroupProxyModel::sourceDataChanged ); rebuildIndexes(); } static bool isAuthorItem( const QModelIndex &index ) { if ( !index.isValid() ) { return false; } AuthorGroupItem *item = static_cast( index.internalPointer() ); return (item->type() == AuthorGroupItem::Author); } QItemSelection AuthorGroupProxyModel::mapSelectionToSource( const QItemSelection &selection ) const { - QModelIndexList proxyIndexes = selection.indexes(); + const QModelIndexList proxyIndexes = selection.indexes(); QItemSelection sourceSelection; - for ( int i = 0; i < proxyIndexes.size(); ++i ) { - if ( !isAuthorItem( proxyIndexes.at( i ) ) ) - sourceSelection << QItemSelectionRange( mapToSource( proxyIndexes.at( i ) ) ); + for ( const QModelIndex &proxyIndex : proxyIndexes ) { + if ( !isAuthorItem( proxyIndex ) ) { + sourceSelection << QItemSelectionRange( mapToSource( proxyIndex ) ); + } } return sourceSelection; } QItemSelection AuthorGroupProxyModel::mapSelectionFromSource( const QItemSelection &selection ) const { return QAbstractProxyModel::mapSelectionFromSource( selection ); } QVariant AuthorGroupProxyModel::data( const QModelIndex &proxyIndex, int role ) const { if ( isAuthorItem( proxyIndex ) ) { AuthorGroupItem *item = static_cast( proxyIndex.internalPointer() ); if ( role == Qt::DisplayRole ) return item->author(); else if ( role == Qt::DecorationRole ) return QIcon::fromTheme( item->author().isEmpty() ? QStringLiteral("user-away") : QStringLiteral("user-identity") ); else return QVariant(); } else { return QAbstractProxyModel::data( proxyIndex, role ); } } QMap AuthorGroupProxyModel::itemData( const QModelIndex &index ) const { if ( isAuthorItem( index ) ) { return QMap(); } else { return QAbstractProxyModel::itemData( index ); } } Qt::ItemFlags AuthorGroupProxyModel::flags( const QModelIndex &index ) const { if ( isAuthorItem( index ) ) { return Qt::ItemIsEnabled | Qt::ItemIsSelectable; } else { return QAbstractProxyModel::flags( index ); } } void AuthorGroupProxyModel::groupByAuthor( bool value ) { if ( d->mGroupByAuthor == value ) return; d->mGroupByAuthor = value; rebuildIndexes(); } void AuthorGroupProxyModel::rebuildIndexes() { beginResetModel(); delete d->mRoot; d->mRoot = new AuthorGroupItem( nullptr ); if ( d->mGroupByAuthor ) { QMap authorMap; for ( int row = 0; row < sourceModel()->rowCount(); ++row ) { const QModelIndex idx = sourceModel()->index( row, 0 ); const QString author = sourceModel()->data( idx, AnnotationModel::AuthorRole ).toString(); if ( !author.isEmpty() ) { // We have the annotations as top-level, so introduce authors as new // top-levels and append the annotations AuthorGroupItem *authorItem = authorMap.value( author, 0 ); if ( !authorItem ) { authorItem = new AuthorGroupItem( d->mRoot, AuthorGroupItem::Author ); authorItem->setAuthor( author ); // Add item to tree d->mRoot->appendChild( authorItem ); // Insert to lookup list authorMap.insert( author, authorItem ); } AuthorGroupItem *item = new AuthorGroupItem( authorItem, AuthorGroupItem::Annotation, idx ); authorItem->appendChild( item ); } else { // We have the pages as top-level, so we use them as top-level, append the // authors for all annotations of the page, and then the annotations themself AuthorGroupItem *pageItem = new AuthorGroupItem( d->mRoot, AuthorGroupItem::Page, idx ); d->mRoot->appendChild( pageItem ); // First collect all authors... QMap pageAuthorMap; for ( int subRow = 0; subRow < sourceModel()->rowCount( idx ); ++subRow ) { const QModelIndex annIdx = sourceModel()->index( subRow, 0, idx ); const QString author = sourceModel()->data( annIdx, AnnotationModel::AuthorRole ).toString(); AuthorGroupItem *authorItem = pageAuthorMap.value( author, 0 ); if ( !authorItem ) { authorItem = new AuthorGroupItem( pageItem, AuthorGroupItem::Author ); authorItem->setAuthor( author ); // Add item to tree pageItem->appendChild( authorItem ); // Insert to lookup list pageAuthorMap.insert( author, authorItem ); } AuthorGroupItem *item = new AuthorGroupItem( authorItem, AuthorGroupItem::Annotation, annIdx ); authorItem->appendChild( item ); } } } } else { for ( int row = 0; row < sourceModel()->rowCount(); ++row ) { const QModelIndex idx = sourceModel()->index( row, 0 ); const QString author = sourceModel()->data( idx, AnnotationModel::AuthorRole ).toString(); if ( !author.isEmpty() ) { // We have the annotations as top-level items AuthorGroupItem *item = new AuthorGroupItem( d->mRoot, AuthorGroupItem::Annotation, idx ); d->mRoot->appendChild( item ); } else { // We have the pages as top-level items AuthorGroupItem *pageItem = new AuthorGroupItem( d->mRoot, AuthorGroupItem::Page, idx ); d->mRoot->appendChild( pageItem ); // Append all annotations as second-level for ( int subRow = 0; subRow < sourceModel()->rowCount( idx ); ++subRow ) { const QModelIndex subIdx = sourceModel()->index( subRow, 0, idx ); AuthorGroupItem *item = new AuthorGroupItem( pageItem, AuthorGroupItem::Annotation, subIdx ); pageItem->appendChild( item ); } } } } endResetModel(); } void AuthorGroupProxyModel::sourceDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector &roles) { emit dataChanged(mapFromSource(topLeft), mapFromSource(bottomRight), roles); } #include "moc_annotationproxymodels.cpp" diff --git a/ui/propertiesdialog.cpp b/ui/propertiesdialog.cpp index d805b58f1..e1a028f0a 100644 --- a/ui/propertiesdialog.cpp +++ b/ui/propertiesdialog.cpp @@ -1,432 +1,429 @@ /*************************************************************************** * Copyright (C) 2004 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 "propertiesdialog.h" // qt/kde includes #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // local includes #include "core/document.h" #include "core/fontinfo.h" static const int IsExtractableRole = Qt::UserRole; static const int FontInfoRole = Qt::UserRole + 1; PropertiesDialog::PropertiesDialog(QWidget *parent, Okular::Document *doc) : KPageDialog( parent ), m_document( doc ), m_fontPage( nullptr ), m_fontModel( nullptr ), m_fontInfo( nullptr ), m_fontProgressBar( nullptr ), m_fontScanStarted( false ) { setFaceType( Tabbed ); setWindowTitle( i18n( "Unknown File" ) ); setStandardButtons( QDialogButtonBox::Ok ); // PROPERTIES QFrame *page = new QFrame(); KPageWidgetItem *item = addPage( page, i18n( "&Properties" ) ); item->setIcon( QIcon::fromTheme( QStringLiteral("document-properties") ) ); // get document info const Okular::DocumentInfo info = doc->documentInfo(); QFormLayout *layout = new QFormLayout( page ); // mime name based on mimetype id QString mimeName = info.get( Okular::DocumentInfo::MimeType ).section( QLatin1Char('/'), -1 ).toUpper(); setWindowTitle( i18n( "%1 Properties", mimeName ) ); /* obtains the properties list, conveniently ordered */ QStringList orderedProperties; orderedProperties << Okular::DocumentInfo::getKeyString( Okular::DocumentInfo::FilePath ) << Okular::DocumentInfo::getKeyString( Okular::DocumentInfo::PagesSize ) << Okular::DocumentInfo::getKeyString( Okular::DocumentInfo::DocumentSize ); for (Okular::DocumentInfo::Key ks = Okular::DocumentInfo::Title; ks <= Okular::DocumentInfo::Keywords; ks = Okular::DocumentInfo::Key( ks+1 ) ) { orderedProperties << Okular::DocumentInfo::getKeyString( ks ); } // keys() is a const method for (const QString &ks : info.keys()) { if ( !orderedProperties.contains( ks ) ) { orderedProperties << ks; } } - for ( QStringList::Iterator it = orderedProperties.begin(); - it != orderedProperties.end(); - ++it ) + for ( const QString &key : qAsConst(orderedProperties) ) { - const QString key = *it; const QString titleString = info.getKeyTitle( key ); const QString valueString = info.get( key ); if ( titleString.isNull() || valueString.isNull() ) continue; // create labels and layout them QWidget *value = nullptr; if ( key == Okular::DocumentInfo::getKeyString( Okular::DocumentInfo::MimeType ) ) { /// for mime type fields, show icon as well value = new QWidget( page ); /// place icon left of mime type's name QHBoxLayout *hboxLayout = new QHBoxLayout( value ); hboxLayout->setContentsMargins( 0, 0, 0, 0 ); /// retrieve icon and place it in a QLabel QMimeDatabase db; QMimeType mimeType = db.mimeTypeForName( valueString ); KSqueezedTextLabel *squeezed; if (mimeType.isValid()) { /// retrieve icon and place it in a QLabel QLabel *pixmapLabel = new QLabel( value ); hboxLayout->addWidget( pixmapLabel, 0 ); pixmapLabel->setPixmap( KIconLoader::global()->loadMimeTypeIcon( mimeType.iconName(), KIconLoader::Small ) ); /// mime type's name and label squeezed = new KSqueezedTextLabel( i18nc( "mimetype information, example: \"PDF Document (application/pdf)\"", "%1 (%2)", mimeType.comment(), valueString ), value ); } else { /// only mime type name squeezed = new KSqueezedTextLabel( valueString, value ); } squeezed->setTextInteractionFlags( Qt::TextSelectableByMouse ); hboxLayout->addWidget( squeezed, 1 ); } else { /// default for any other document information KSqueezedTextLabel *label = new KSqueezedTextLabel( valueString, page ); label->setTextInteractionFlags( Qt::TextSelectableByMouse ); value = label; } layout->addRow( new QLabel( i18n( "%1:", titleString ) ), value); } // FONTS if ( doc->canProvideFontInformation() ) { // create fonts tab and layout it QFrame *page2 = new QFrame(); m_fontPage = addPage(page2, i18n("&Fonts")); m_fontPage->setIcon( QIcon::fromTheme( QStringLiteral("preferences-desktop-font") ) ); QVBoxLayout *page2Layout = new QVBoxLayout(page2); // add a tree view QTreeView *view = new QTreeView(page2); view->setContextMenuPolicy(Qt::CustomContextMenu); connect(view, &QTreeView::customContextMenuRequested, this, &PropertiesDialog::showFontsMenu); page2Layout->addWidget(view); view->setRootIsDecorated(false); view->setAlternatingRowColors(true); view->setSortingEnabled( true ); // creating a proxy model so we can sort the data QSortFilterProxyModel *proxymodel = new QSortFilterProxyModel(view); proxymodel->setDynamicSortFilter( true ); proxymodel->setSortCaseSensitivity( Qt::CaseInsensitive ); m_fontModel = new FontsListModel(view); proxymodel->setSourceModel(m_fontModel); view->setModel(proxymodel); view->sortByColumn( 0, Qt::AscendingOrder ); m_fontInfo = new QLabel( this ); page2Layout->addWidget( m_fontInfo ); m_fontInfo->setText( i18n( "Reading font information..." ) ); m_fontInfo->hide(); m_fontProgressBar = new QProgressBar( this ); page2Layout->addWidget( m_fontProgressBar ); m_fontProgressBar->setRange( 0, 100 ); m_fontProgressBar->setValue( 0 ); m_fontProgressBar->hide(); } // KPageDialog is a bit buggy, it doesn't fix its own sizeHint, so we have to manually resize resize(layout->sizeHint()); connect( pageWidget(), SIGNAL(currentPageChanged(KPageWidgetItem*,KPageWidgetItem*)), this, SLOT(pageChanged(KPageWidgetItem*,KPageWidgetItem*)) ); } PropertiesDialog::~PropertiesDialog() { m_document->stopFontReading(); } void PropertiesDialog::pageChanged( KPageWidgetItem *current, KPageWidgetItem * ) { if ( current == m_fontPage && !m_fontScanStarted ) { connect(m_document, &Okular::Document::gotFont, m_fontModel, &FontsListModel::addFont); connect(m_document, &Okular::Document::fontReadingProgress, this, &PropertiesDialog::slotFontReadingProgress); connect(m_document, &Okular::Document::fontReadingEnded, this, &PropertiesDialog::slotFontReadingEnded); QTimer::singleShot( 0, this, &PropertiesDialog::reallyStartFontReading ); m_fontScanStarted = true; } } void PropertiesDialog::slotFontReadingProgress( int page ) { m_fontProgressBar->setValue( m_fontProgressBar->maximum() * ( page + 1 ) / m_document->pages() ); } void PropertiesDialog::slotFontReadingEnded() { m_fontInfo->hide(); m_fontProgressBar->hide(); } void PropertiesDialog::reallyStartFontReading() { m_fontInfo->show(); m_fontProgressBar->show(); m_document->startFontReading(); } void PropertiesDialog::showFontsMenu(const QPoint &pos) { QTreeView *view = static_cast(sender()); QModelIndex index = view->indexAt(pos); if (index.data(IsExtractableRole).toBool()) { QMenu *menu = new QMenu(this); menu->addAction( i18nc("@action:inmenu", "&Extract Font") ); QAction *result = menu->exec(view->viewport()->mapToGlobal(pos)); if (result) { Okular::FontInfo fi = index.data(FontInfoRole).value(); const QString caption = i18n( "Where do you want to save %1?", fi.name() ); const QString path = QFileDialog::getSaveFileName( this, caption, fi.name() ); if ( path.isEmpty() ) return; QFile f( path ); if ( f.open( QIODevice::WriteOnly ) ) { f.write( m_document->fontData(fi) ); f.close(); } else { KMessageBox::error( this, i18n( "Could not open \"%1\" for writing. File was not saved.", path ) ); } } } } FontsListModel::FontsListModel( QObject * parent ) : QAbstractTableModel( parent ) { } FontsListModel::~FontsListModel() { } void FontsListModel::addFont( const Okular::FontInfo &fi ) { beginInsertRows( QModelIndex(), m_fonts.size(), m_fonts.size() ); m_fonts << fi; endInsertRows(); } int FontsListModel::columnCount( const QModelIndex &parent ) const { return parent.isValid() ? 0 : 3; } static QString descriptionForFontType( Okular::FontInfo::FontType type ) { switch ( type ) { case Okular::FontInfo::Type1: return i18n("Type 1"); break; case Okular::FontInfo::Type1C: return i18n("Type 1C"); break; case Okular::FontInfo::Type1COT: return i18nc("OT means OpenType", "Type 1C (OT)"); break; case Okular::FontInfo::Type3: return i18n("Type 3"); break; case Okular::FontInfo::TrueType: return i18n("TrueType"); break; case Okular::FontInfo::TrueTypeOT: return i18nc("OT means OpenType", "TrueType (OT)"); break; case Okular::FontInfo::CIDType0: return i18n("CID Type 0"); break; case Okular::FontInfo::CIDType0C: return i18n("CID Type 0C"); break; case Okular::FontInfo::CIDType0COT: return i18nc("OT means OpenType", "CID Type 0C (OT)"); break; case Okular::FontInfo::CIDTrueType: return i18n("CID TrueType"); break; case Okular::FontInfo::CIDTrueTypeOT: return i18nc("OT means OpenType", "CID TrueType (OT)"); break; case Okular::FontInfo::TeXPK: return i18n("TeX PK"); break; case Okular::FontInfo::TeXVirtual: return i18n("TeX virtual"); break; case Okular::FontInfo::TeXFontMetric: return i18n("TeX Font Metric"); break; case Okular::FontInfo::TeXFreeTypeHandled: return i18n("TeX FreeType-handled"); break; case Okular::FontInfo::Unknown: return i18nc("Unknown font type", "Unknown"); break; } return QString(); } static QString pathOrDescription( const Okular::FontInfo &font ) { switch ( font.embedType() ) { case Okular::FontInfo::NotEmbedded: return font.file(); break; case Okular::FontInfo::EmbeddedSubset: return i18n("Embedded (subset)"); break; case Okular::FontInfo::FullyEmbedded: return i18n("Fully embedded"); break; } return QString(); } static QString descriptionForEmbedType( Okular::FontInfo::EmbedType type ) { switch ( type ) { case Okular::FontInfo::NotEmbedded: return i18n("No"); break; case Okular::FontInfo::EmbeddedSubset: return i18n("Yes (subset)"); break; case Okular::FontInfo::FullyEmbedded: return i18n("Yes"); break; } return QString(); } QVariant FontsListModel::data( const QModelIndex &index, int role ) const { if ( !index.isValid() || index.row() < 0 || index.row() >= m_fonts.count() ) return QVariant(); switch ( role ) { case Qt::DisplayRole: switch ( index.column() ) { case 0: { const Okular::FontInfo &fi = m_fonts.at( index.row() ); const QString fontname = fi.name(); const QString substituteName = fi.substituteName(); if ( fi.embedType() == Okular::FontInfo::NotEmbedded && !substituteName.isEmpty() && !fontname.isEmpty() && substituteName != fontname ) { return i18nc("Replacing missing font with another one", "%1 (substituting with %2)", fontname, substituteName); } return fontname.isEmpty() ? i18nc( "font name not available (empty)", "[n/a]" ) : fontname; break; } case 1: return descriptionForFontType( m_fonts.at( index.row() ).type() ); break; case 2: return pathOrDescription( m_fonts.at( index.row() ) ); break; } break; case Qt::ToolTipRole: { QString fontname = m_fonts.at( index.row() ).name(); if ( fontname.isEmpty() ) fontname = i18n( "Unknown font" ); QString tooltip = QLatin1String( "" ) + fontname + QLatin1String( "" ); if ( m_fonts.at( index.row() ).embedType() == Okular::FontInfo::NotEmbedded ) tooltip += QStringLiteral( " (%2)" ).arg( fontname, fontname ); tooltip += QLatin1String( "
" ) + i18n( "Embedded: %1", descriptionForEmbedType( m_fonts.at( index.row() ).embedType() ) ); tooltip += QLatin1String( "" ); return tooltip; break; } case IsExtractableRole: { return m_fonts.at( index.row() ).canBeExtracted(); } case FontInfoRole: { QVariant v; v.setValue( m_fonts.at( index.row() ) ); return v; } } return QVariant(); } QVariant FontsListModel::headerData( int section, Qt::Orientation orientation, int role ) const { if ( orientation != Qt::Horizontal ) return QVariant(); if ( role == Qt::TextAlignmentRole ) return QVariant( Qt::AlignLeft ); if ( role != Qt::DisplayRole ) return QVariant(); switch ( section ) { case 0: return i18n( "Name" ); break; case 1: return i18n( "Type" ); break; case 2: return i18n( "File" ); break; default: return QVariant(); } } int FontsListModel::rowCount( const QModelIndex &parent ) const { return parent.isValid() ? 0 : m_fonts.size(); } #include "moc_propertiesdialog.cpp" /* kate: replace-tabs on; indent-width 4; */