diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index fb0242ca5..24fa16af6 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,54 +1,54 @@ 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_ubuntu_20_04: stage: build image: ubuntu:focal 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 script: - mkdir -p build && cd build - cmake -DOKULAR_UI=desktop -G Ninja .. - ninja - rm -rf * - cmake -DOKULAR_UI=mobile -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,modernize-loop-convert,-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,modernize-use-nullptr,-bugprone-macro-parentheses,-bugprone-narrowing-conversions,-bugprone-branch-clone,-bugprone-incorrect-roundings' -config=\"{WarningsAsErrors: '*'}\"" diff --git a/autotests/documenttest.cpp b/autotests/documenttest.cpp index 3ef82198f..df36961a1 100644 --- a/autotests/documenttest.cpp +++ b/autotests/documenttest.cpp @@ -1,133 +1,133 @@ /*************************************************************************** * Copyright (C) 2013 by Fabio D'Urso * * 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 #include #include "../core/annotations.h" #include "../core/document.h" #include "../core/document_p.h" #include "../core/generator.h" #include "../core/observer.h" #include "../core/page.h" #include "../core/rotationjob_p.h" #include "../settings_core.h" class DocumentTest : public QObject { Q_OBJECT private slots: void testCloseDuringRotationJob(); void testDocdataMigration(); }; // Test that we don't crash if the document is closed while a RotationJob // is enqueued/running void DocumentTest::testCloseDuringRotationJob() { Okular::SettingsCore::instance( QStringLiteral("documenttest") ); Okular::Document *m_document = new Okular::Document( nullptr ); const QString testFile = QStringLiteral(KDESRCDIR "data/file1.pdf"); QMimeDatabase db; const QMimeType mime = db.mimeTypeForFile( testFile ); Okular::DocumentObserver *dummyDocumentObserver = new Okular::DocumentObserver(); m_document->addObserver( dummyDocumentObserver ); m_document->openDocument( testFile, QUrl(), mime ); m_document->setRotation( 1 ); // Tell ThreadWeaver not to start any new job ThreadWeaver::Queue::instance()->suspend(); // Request a pixmap. A RotationJob will be enqueued but not started Okular::PixmapRequest *pixmapReq = new Okular::PixmapRequest( dummyDocumentObserver, 0, 100, 100, 1, Okular::PixmapRequest::NoFeature ); m_document->requestPixmaps( QLinkedList() << pixmapReq ); // Delete the document delete m_document; // Resume job processing and wait for the RotationJob to finish ThreadWeaver::Queue::instance()->resume(); ThreadWeaver::Queue::instance()->finish(); qApp->processEvents(); delete dummyDocumentObserver; } // Test that, if there's a XML file in docdata referring to a document, we // detect that it must be migrated, that it doesn't get wiped out if you close // the document without migrating and that it does get wiped out after migrating void DocumentTest::testDocdataMigration() { Okular::SettingsCore::instance( QStringLiteral("documenttest") ); const QUrl testFileUrl = QUrl::fromLocalFile(KDESRCDIR "data/file1.pdf"); const QString testFilePath = testFileUrl.toLocalFile(); const qint64 testFileSize = QFileInfo(testFilePath).size(); // Copy XML file to the docdata/ directory const QString docDataPath = Okular::DocumentPrivate::docDataFileName(testFileUrl, testFileSize); QFile::remove(docDataPath); QVERIFY( QFile::copy(KDESRCDIR "data/file1-docdata.xml", docDataPath) ); // Open our document - Okular::Document *m_document = new Okular::Document( 0 ); + Okular::Document *m_document = new Okular::Document( nullptr ); QMimeDatabase db; const QMimeType mime = db.mimeTypeForFile( testFilePath ); QCOMPARE( m_document->openDocument( testFilePath, testFileUrl, mime ), Okular::Document::OpenSuccess ); // Check that the annotation from file1-docdata.xml was loaded QCOMPARE( m_document->page( 0 )->annotations().size(), 1 ); QCOMPARE( m_document->page( 0 )->annotations().first()->uniqueName(), QString("testannot") ); // Check that we detect that it must be migrated QVERIFY( m_document->isDocdataMigrationNeeded() ); m_document->closeDocument(); // Reopen the document and check that the annotation is still present // (because we have not migrated) QCOMPARE( m_document->openDocument( testFilePath, testFileUrl, mime ), Okular::Document::OpenSuccess ); QCOMPARE( m_document->page( 0 )->annotations().size(), 1 ); QCOMPARE( m_document->page( 0 )->annotations().first()->uniqueName(), QString("testannot") ); QVERIFY( m_document->isDocdataMigrationNeeded() ); // Do the migration QTemporaryFile migratedSaveFile( QStringLiteral( "%1/okrXXXXXX.pdf" ).arg( QDir::tempPath() ) ); QVERIFY( migratedSaveFile.open() ); migratedSaveFile.close(); QVERIFY( m_document->saveChanges( migratedSaveFile.fileName() ) ); m_document->docdataMigrationDone(); QVERIFY( !m_document->isDocdataMigrationNeeded() ); m_document->closeDocument(); // Now the docdata file should have no annotations, let's check QCOMPARE( m_document->openDocument( testFilePath, testFileUrl, mime ), Okular::Document::OpenSuccess ); QCOMPARE( m_document->page( 0 )->annotations().size(), 0 ); QVERIFY( !m_document->isDocdataMigrationNeeded() ); m_document->closeDocument(); // And the new file should have 1 annotation, let's check QCOMPARE( m_document->openDocument( migratedSaveFile.fileName(), QUrl::fromLocalFile(migratedSaveFile.fileName()), mime ), Okular::Document::OpenSuccess ); QCOMPARE( m_document->page( 0 )->annotations().size(), 1 ); QVERIFY( !m_document->isDocdataMigrationNeeded() ); m_document->closeDocument(); delete m_document; } QTEST_MAIN( DocumentTest ) #include "documenttest.moc" diff --git a/autotests/parttest.cpp b/autotests/parttest.cpp index 26e489df0..84c3a16ec 100644 --- a/autotests/parttest.cpp +++ b/autotests/parttest.cpp @@ -1,1991 +1,1991 @@ /*************************************************************************** * Copyright (C) 2013 by Albert Astals Cid * * Copyright (C) 2017 Klarälvdalens Datakonsult AB, a KDAB Group * * company, info@kdab.com. Work sponsored by the * * LiMux project of the city of Munich * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * ***************************************************************************/ #include #include "../core/annotations.h" #include "../core/form.h" #include "../core/page.h" #include "../part.h" #include "../ui/toc.h" #include "../ui/sidebar.h" #include "../ui/pageview.h" #include "../ui/presentationwidget.h" #include "../settings.h" #include "closedialoghelper.h" #include "../generators/poppler/config-okular-poppler.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace Okular { class PartTest : public QObject { Q_OBJECT static bool openDocument(Okular::Part *part, const QString &filePath); signals: void urlHandler(const QUrl &url); // NOLINT(readability-inconsistent-declaration-parameter-name) private slots: void testReload(); void testCanceledReload(); void testTOCReload(); void testForwardPDF(); void testForwardPDF_data(); void testGeneratorPreferences(); void testSelectText(); void testClickInternalLink(); void testOpenUrlArguments(); void test388288(); void testSaveAs(); void testSaveAs_data(); void testSaveAsToNonExistingPath(); void testSaveAsToSymlink(); void testSaveIsSymlink(); void testSidebarItemAfterSaving(); void testViewModeSavingPerFile(); void testSaveAsUndoStackAnnotations(); void testSaveAsUndoStackAnnotations_data(); void testSaveAsUndoStackForms(); void testSaveAsUndoStackForms_data(); void testMouseMoveOverLinkWhileInSelectionMode(); void testClickUrlLinkWhileInSelectionMode(); void testeTextSelectionOverAndAcrossLinks_data(); void testeTextSelectionOverAndAcrossLinks(); void testClickUrlLinkWhileLinkTextIsSelected(); void testRClickWhileLinkTextIsSelected(); void testRClickOverLinkWhileLinkTextIsSelected(); void testRClickOnSelectionModeShoulShowFollowTheLinkMenu(); void testClickAnywhereAfterSelectionShouldUnselect(); void testeRectSelectionStartingOnLinks(); void testCheckBoxReadOnly(); void testCrashTextEditDestroy(); void testAnnotWindow(); void testAdditionalActionTriggers(); void testTypewriterAnnotTool(); void testJumpToPage(); void testForwardBackwardNavigation(); void testTabletProximityBehavior(); void testOpenPrintPreview(); private: void simulateMouseSelection(double startX, double startY, double endX, double endY, QWidget *target); }; class PartThatHijacksQueryClose : public Okular::Part { public: PartThatHijacksQueryClose(QWidget* parentWidget, QObject* parent, const QVariantList& args) : Okular::Part(parentWidget, parent, args), behavior(PassThru) {} enum Behavior { PassThru, ReturnTrue, ReturnFalse }; void setQueryCloseBehavior(Behavior new_behavior) { behavior = new_behavior; } bool queryClose() override { if (behavior == PassThru) return Okular::Part::queryClose(); else // ReturnTrue or ReturnFalse return (behavior == ReturnTrue); } private: Behavior behavior; }; bool PartTest::openDocument(Okular::Part *part, const QString &filePath) { part->openDocument( filePath ); return part->m_document->isOpened(); } // Test that Okular doesn't crash after a successful reload void PartTest::testReload() { QVariantList dummyArgs; Okular::Part part(nullptr, nullptr, dummyArgs); QVERIFY( openDocument(&part, QStringLiteral(KDESRCDIR "data/file1.pdf")) ); part.reload(); qApp->processEvents(); } // Test that Okular doesn't crash after a canceled reload void PartTest::testCanceledReload() { QVariantList dummyArgs; PartThatHijacksQueryClose part(nullptr, nullptr, dummyArgs); QVERIFY( openDocument(&part, QStringLiteral(KDESRCDIR "data/file1.pdf")) ); // When queryClose() returns false, the reload operation is canceled (as if // the user had chosen Cancel in the "Save changes?" message box) part.setQueryCloseBehavior(PartThatHijacksQueryClose::ReturnFalse); part.reload(); qApp->processEvents(); } void PartTest::testTOCReload() { QVariantList dummyArgs; Okular::Part part(nullptr, nullptr, dummyArgs); QVERIFY( openDocument(&part, QStringLiteral(KDESRCDIR "data/tocreload.pdf")) ); QCOMPARE(part.m_toc->expandedNodes().count(), 0); part.m_toc->m_treeView->expandAll(); QCOMPARE(part.m_toc->expandedNodes().count(), 3); part.reload(); qApp->processEvents(); QCOMPARE(part.m_toc->expandedNodes().count(), 3); } void PartTest::testForwardPDF() { QFETCH(QString, dir); QVariantList dummyArgs; Okular::Part part(nullptr, nullptr, dummyArgs); // Create temp dir named like this: ${system temp dir}/${random string}/${dir} const QTemporaryDir tempDir; const QDir workDir(QDir(tempDir.path()).filePath(dir)); workDir.mkpath(QStringLiteral(".")); QFile f(QStringLiteral(KDESRCDIR "data/synctextest.tex")); const QString texDestination = workDir.path() + QStringLiteral("/synctextest.tex"); QVERIFY(f.copy(texDestination)); QProcess process; process.setWorkingDirectory(workDir.path()); const QString pdflatexPath(QStandardPaths::findExecutable(QStringLiteral("pdflatex"))); if (pdflatexPath.isEmpty()) { QFAIL("pdflatex executable not found, but needed for the test. Try installing the respective TeXLive packages."); } process.start(pdflatexPath, QStringList() << QStringLiteral("-synctex=1") << QStringLiteral("-interaction=nonstopmode") << texDestination); bool started = process.waitForStarted(); if (!started) { qDebug() << "start error:" << process.error(); qDebug() << "start stdout:" << process.readAllStandardOutput(); qDebug() << "start stderr:" << process.readAllStandardError(); } QVERIFY(started); process.waitForFinished(); if (process.exitStatus() != QProcess::NormalExit || process.exitCode() != 0) { qDebug() << "exit error:" << process.error() << "status" << process.exitStatus() << "code" << process.exitCode(); qDebug() << "exit stdout:" << process.readAllStandardOutput(); qDebug() << "exit stderr:" << process.readAllStandardError(); } const QString pdfResult = workDir.path() + QStringLiteral("/synctextest.pdf"); QVERIFY(QFile::exists(pdfResult)); QVERIFY( openDocument(&part, pdfResult) ); part.m_document->setViewportPage(0); QCOMPARE(part.m_document->currentPage(), 0u); part.closeUrl(); QUrl u(QUrl::fromLocalFile(pdfResult)); u.setFragment(QStringLiteral("src:100") + texDestination); part.openUrl(u); QCOMPARE(part.m_document->currentPage(), 1u); } void PartTest::testForwardPDF_data() { QTest::addColumn("dir"); QTest::newRow("non-utf8") << QStringLiteral("synctextest"); //QStringliteral is broken on windows with non ascii chars so using QString::fromUtf8 QTest::newRow("utf8") << QString::fromUtf8("ßðđđŋßðđŋ"); } void PartTest::testGeneratorPreferences() { KConfigDialog * dialog; QVariantList dummyArgs; Okular::Part part(nullptr, nullptr, dummyArgs); // Test that we don't crash while opening the dialog dialog = part.slotGeneratorPreferences(); qApp->processEvents(); delete dialog; // closes the dialog and recursively destroys all widgets // Test that we don't crash while opening a new instance of the dialog // This catches attempts to reuse widgets that have been destroyed dialog = part.slotGeneratorPreferences(); qApp->processEvents(); delete dialog; } void PartTest::testSelectText() { QVariantList dummyArgs; Okular::Part part(nullptr, nullptr, dummyArgs); QVERIFY(openDocument(&part, QStringLiteral(KDESRCDIR "data/file2.pdf"))); part.widget()->show(); QVERIFY(QTest::qWaitForWindowExposed(part.widget())); part.m_document->setViewportPage(0); // wait for pixmap QTRY_VERIFY(part.m_document->page(0)->hasPixmap(part.m_pageView)); const int width = part.m_pageView->horizontalScrollBar()->maximum() + part.m_pageView->viewport()->width(); const int height = part.m_pageView->verticalScrollBar()->maximum() + part.m_pageView->viewport()->height(); QVERIFY(QMetaObject::invokeMethod(part.m_pageView, "slotSetMouseTextSelect")); const int mouseY = height * 0.052; const int mouseStartX = width * 0.12; const int mouseEndX = width * 0.7; simulateMouseSelection(mouseStartX, mouseY, mouseEndX, mouseY, part.m_pageView->viewport()); QApplication::clipboard()->clear(); QVERIFY(QMetaObject::invokeMethod(part.m_pageView, "copyTextSelection")); QCOMPARE(QApplication::clipboard()->text(), QStringLiteral("Hola que tal\n")); } void PartTest::testClickInternalLink() { QVariantList dummyArgs; Okular::Part part(nullptr, nullptr, dummyArgs); QVERIFY(openDocument(&part, QStringLiteral(KDESRCDIR "data/file2.pdf"))); part.widget()->show(); QVERIFY(QTest::qWaitForWindowExposed(part.widget())); part.m_document->setViewportPage(0); // wait for pixmap QTRY_VERIFY(part.m_document->page(0)->hasPixmap(part.m_pageView)); const int width = part.m_pageView->horizontalScrollBar()->maximum() + part.m_pageView->viewport()->width(); const int height = part.m_pageView->verticalScrollBar()->maximum() + part.m_pageView->viewport()->height(); QMetaObject::invokeMethod(part.m_pageView, "slotSetMouseNormal"); QCOMPARE(part.m_document->currentPage(), 0u); QTest::mouseMove(part.m_pageView->viewport(), QPoint(width * 0.17, height * 0.05)); QTest::mouseClick(part.m_pageView->viewport(), Qt::LeftButton, Qt::NoModifier, QPoint(width * 0.17, height * 0.05)); QTRY_COMPARE(part.m_document->currentPage(), 1u); } // cursor switches to Hand when hovering over link in TextSelect mode. void PartTest::testMouseMoveOverLinkWhileInSelectionMode() { QVariantList dummyArgs; Okular::Part part(nullptr, nullptr, dummyArgs); QVERIFY(openDocument(&part, QStringLiteral(KDESRCDIR "data/pdf_with_links.pdf"))); // resize window to avoid problem with selection areas part.widget()->resize(800, 600); part.widget()->show(); QVERIFY(QTest::qWaitForWindowExposed(part.widget())); part.m_document->setViewportPage(0); // wait for pixmap QTRY_VERIFY(part.m_document->page(0)->hasPixmap(part.m_pageView)); const int width = part.m_pageView->horizontalScrollBar()->maximum() + part.m_pageView->viewport()->width(); const int height = part.m_pageView->verticalScrollBar()->maximum() + part.m_pageView->viewport()->height(); // enter text-selection mode QVERIFY(QMetaObject::invokeMethod(part.m_pageView, "slotSetMouseTextSelect")); // move mouse over link QTest::mouseMove(part.m_pageView->viewport(), QPoint(width * 0.250, height * 0.127)); // check if mouse icon changed to proper icon QTRY_COMPARE(part.m_pageView->cursor().shape(), Qt::PointingHandCursor); } // clicking on hyperlink jumps to destination in TextSelect mode. void PartTest::testClickUrlLinkWhileInSelectionMode() { QVariantList dummyArgs; Okular::Part part(nullptr, nullptr, dummyArgs); QVERIFY(openDocument(&part, QStringLiteral(KDESRCDIR "data/pdf_with_links.pdf"))); // resize window to avoid problem with selection areas part.widget()->resize(800, 600); part.widget()->show(); QVERIFY(QTest::qWaitForWindowExposed(part.widget())); part.m_document->setViewportPage(0); // wait for pixmap QTRY_VERIFY(part.m_document->page(0)->hasPixmap(part.m_pageView)); const int width = part.m_pageView->horizontalScrollBar()->maximum() + part.m_pageView->viewport()->width(); const int height = part.m_pageView->verticalScrollBar()->maximum() + part.m_pageView->viewport()->height(); // enter text-selection mode QVERIFY(QMetaObject::invokeMethod(part.m_pageView, "slotSetMouseTextSelect")); // overwrite urlHandler for 'mailto' urls QDesktopServices::setUrlHandler(QStringLiteral("mailto"), this, "urlHandler"); QSignalSpy openUrlSignalSpy(this, &PartTest::urlHandler); // click on url QTest::mouseMove(part.m_pageView->viewport(), QPoint(width * 0.250, height * 0.127)); QTest::mouseClick(part.m_pageView->viewport(), Qt::LeftButton, Qt::NoModifier, QPoint(width * 0.250, height * 0.127)); // expect that the urlHandler signal was called QTRY_COMPARE(openUrlSignalSpy.count(), 1); QList arguments = openUrlSignalSpy.takeFirst(); QCOMPARE(arguments.at(0).value(), QUrl("mailto:foo@foo.bar")); } void PartTest::testeTextSelectionOverAndAcrossLinks_data() { QTest::addColumn("mouseStartX"); QTest::addColumn("mouseEndX"); QTest::addColumn("expectedResult"); // can text-select "over and across" hyperlink. QTest::newRow("start selection before link") << 0.1564 << 0.2943 << QStringLiteral(" a link: foo@foo.b"); // can text-select starting at text and ending selection in middle of hyperlink. QTest::newRow("start selection in the middle of the link") << 0.28 << 0.382 << QStringLiteral("o.bar\n"); // text selection works when selecting left to right or right to left QTest::newRow("start selection after link") << 0.40 << 0.05 << QStringLiteral("This is a link: foo@foo.bar\n"); } // can text-select "over and across" hyperlink. void PartTest::testeTextSelectionOverAndAcrossLinks() { QVariantList dummyArgs; Okular::Part part(nullptr, nullptr, dummyArgs); QVERIFY(openDocument(&part, QStringLiteral(KDESRCDIR "data/pdf_with_links.pdf"))); // resize window to avoid problem with selection areas part.widget()->resize(800, 600); part.widget()->show(); QVERIFY(QTest::qWaitForWindowExposed(part.widget())); part.m_document->setViewportPage(0); // wait for pixmap QTRY_VERIFY(part.m_document->page(0)->hasPixmap(part.m_pageView)); const int width = part.m_pageView->horizontalScrollBar()->maximum() + part.m_pageView->viewport()->width(); const int height = part.m_pageView->verticalScrollBar()->maximum() + part.m_pageView->viewport()->height(); // enter text-selection mode QVERIFY(QMetaObject::invokeMethod(part.m_pageView, "slotSetMouseTextSelect")); const double mouseY = height * 0.127; QFETCH(double, mouseStartX); QFETCH(double, mouseEndX); mouseStartX = width * mouseStartX; mouseEndX = width * mouseEndX; simulateMouseSelection(mouseStartX, mouseY, mouseEndX, mouseY, part.m_pageView->viewport()); QApplication::clipboard()->clear(); QVERIFY(QMetaObject::invokeMethod(part.m_pageView, "copyTextSelection")); QFETCH(QString, expectedResult); QCOMPARE(QApplication::clipboard()->text(), expectedResult); } // can jump to link while there's an active selection of text. void PartTest::testClickUrlLinkWhileLinkTextIsSelected() { QVariantList dummyArgs; Okular::Part part(nullptr, nullptr, dummyArgs); QVERIFY(openDocument(&part, QStringLiteral(KDESRCDIR "data/pdf_with_links.pdf"))); // resize window to avoid problem with selection areas part.widget()->resize(800, 600); part.widget()->show(); QVERIFY(QTest::qWaitForWindowExposed(part.widget())); part.m_document->setViewportPage(0); // wait for pixmap QTRY_VERIFY(part.m_document->page(0)->hasPixmap(part.m_pageView)); const int width = part.m_pageView->horizontalScrollBar()->maximum() + part.m_pageView->viewport()->width(); const int height = part.m_pageView->verticalScrollBar()->maximum() + part.m_pageView->viewport()->height(); // enter text-selection mode QVERIFY(QMetaObject::invokeMethod(part.m_pageView, "slotSetMouseTextSelect")); const double mouseY = height * 0.127; const double mouseStartX = width * 0.13; const double mouseEndX = width * 0.40; simulateMouseSelection(mouseStartX, mouseY, mouseEndX, mouseY, part.m_pageView->viewport()); // overwrite urlHandler for 'mailto' urls QDesktopServices::setUrlHandler(QStringLiteral("mailto"), this, "urlHandler"); QSignalSpy openUrlSignalSpy(this, &PartTest::urlHandler); // click on url const double mouseClickX = width * 0.2997; const double mouseClickY = height * 0.1293; QTest::mouseMove(part.m_pageView->viewport(), QPoint(mouseClickX, mouseClickY)); QTest::mouseClick(part.m_pageView->viewport(), Qt::LeftButton, Qt::NoModifier, QPoint(mouseClickX, mouseClickY), 1000); // expect that the urlHandler signal was called QTRY_COMPARE(openUrlSignalSpy.count(), 1); QList arguments = openUrlSignalSpy.takeFirst(); QCOMPARE(arguments.at(0).value(), QUrl("mailto:foo@foo.bar")); } // r-click on the selected text gives the "Go To:" content menu option void PartTest::testRClickWhileLinkTextIsSelected() { QVariantList dummyArgs; Okular::Part part(nullptr, nullptr, dummyArgs); QVERIFY(openDocument(&part, QStringLiteral(KDESRCDIR "data/pdf_with_links.pdf"))); // resize window to avoid problem with selection areas part.widget()->resize(800, 600); part.widget()->show(); QVERIFY(QTest::qWaitForWindowExposed(part.widget())); part.m_document->setViewportPage(0); // wait for pixmap QTRY_VERIFY(part.m_document->page(0)->hasPixmap(part.m_pageView)); const int width = part.m_pageView->horizontalScrollBar()->maximum() + part.m_pageView->viewport()->width(); const int height = part.m_pageView->verticalScrollBar()->maximum() + part.m_pageView->viewport()->height(); // enter text-selection mode QVERIFY(QMetaObject::invokeMethod(part.m_pageView, "slotSetMouseTextSelect")); const double mouseY = height * 0.162; const double mouseStartX = width * 0.42; const double mouseEndX = width * 0.60; simulateMouseSelection(mouseStartX, mouseY, mouseEndX, mouseY, part.m_pageView->viewport()); // Need to do this because the pop-menu will have his own mainloop and will block tests until // the menu disappear PageView *view = part.m_pageView; bool menuClosed = false; QTimer::singleShot(2000, [view, &menuClosed]() { // check if popup menu is active and visible QMenu *menu = qobject_cast(view->findChild(QStringLiteral("PopupMenu"))); QVERIFY(menu); QVERIFY(menu->isVisible()); // check if the menu contains go-to link action QAction *goToAction = qobject_cast(menu->findChild(QStringLiteral("GoToAction"))); QVERIFY(goToAction); // check if the "follow this link" action is not visible QAction *processLinkAction = qobject_cast(menu->findChild(QStringLiteral("ProcessLinkAction"))); QVERIFY(!processLinkAction); // check if the "copy link address" action is not visible QAction *copyLinkLocation = qobject_cast(menu->findChild(QStringLiteral("CopyLinkLocationAction"))); QVERIFY(!copyLinkLocation); // close menu to continue test menu->close(); menuClosed = true; }); // click on url const double mouseClickX = width * 0.425; const double mouseClickY = height * 0.162; QTest::mouseMove(part.m_pageView->viewport(), QPoint(mouseClickX, mouseClickY)); QTest::mouseClick(part.m_pageView->viewport(), Qt::RightButton, Qt::NoModifier, QPoint(mouseClickX, mouseClickY), 1000); // will continue after pop-menu get closed QTRY_VERIFY(menuClosed); } // r-click on the link gives the "follow this link" content menu option void PartTest::testRClickOverLinkWhileLinkTextIsSelected() { QVariantList dummyArgs; Okular::Part part(nullptr, nullptr, dummyArgs); QVERIFY(openDocument(&part, QStringLiteral(KDESRCDIR "data/pdf_with_links.pdf"))); // resize window to avoid problem with selection areas part.widget()->resize(800, 600); part.widget()->show(); QVERIFY(QTest::qWaitForWindowExposed(part.widget())); part.m_document->setViewportPage(0); // wait for pixmap QTRY_VERIFY(part.m_document->page(0)->hasPixmap(part.m_pageView)); const int width = part.m_pageView->horizontalScrollBar()->maximum() + part.m_pageView->viewport()->width(); const int height = part.m_pageView->verticalScrollBar()->maximum() + part.m_pageView->viewport()->height(); // enter text-selection mode QVERIFY(QMetaObject::invokeMethod(part.m_pageView, "slotSetMouseTextSelect")); const double mouseY = height * 0.162; const double mouseStartX = width * 0.42; const double mouseEndX = width * 0.60; simulateMouseSelection(mouseStartX, mouseY, mouseEndX, mouseY, part.m_pageView->viewport()); // Need to do this because the pop-menu will have his own mainloop and will block tests until // the menu disappear PageView *view = part.m_pageView; bool menuClosed = false; QTimer::singleShot(2000, [view, &menuClosed]() { // check if popup menu is active and visible QMenu *menu = qobject_cast(view->findChild(QStringLiteral("PopupMenu"))); QVERIFY(menu); QVERIFY(menu->isVisible()); // check if the menu contains "follow this link" action QAction *processLinkAction = qobject_cast(menu->findChild(QStringLiteral("ProcessLinkAction"))); QVERIFY(processLinkAction); // check if the menu contains "copy link address" action QAction *copyLinkLocation = qobject_cast(menu->findChild(QStringLiteral("CopyLinkLocationAction"))); QVERIFY(copyLinkLocation); // close menu to continue test menu->close(); menuClosed = true; }); // click on url const double mouseClickX = width * 0.593; const double mouseClickY = height * 0.162; QTest::mouseMove(part.m_pageView->viewport(), QPoint(mouseClickX, mouseClickY)); QTest::mouseClick(part.m_pageView->viewport(), Qt::RightButton, Qt::NoModifier, QPoint(mouseClickX, mouseClickY), 1000); // will continue after pop-menu get closed QTRY_VERIFY(menuClosed); } void PartTest::testRClickOnSelectionModeShoulShowFollowTheLinkMenu() { QVariantList dummyArgs; Okular::Part part(nullptr, nullptr, dummyArgs); QVERIFY(openDocument(&part, QStringLiteral(KDESRCDIR "data/pdf_with_links.pdf"))); // resize window to avoid problem with selection areas part.widget()->resize(800, 600); part.widget()->show(); QVERIFY(QTest::qWaitForWindowExposed(part.widget())); part.m_document->setViewportPage(0); // wait for pixmap QTRY_VERIFY(part.m_document->page(0)->hasPixmap(part.m_pageView)); const int width = part.m_pageView->horizontalScrollBar()->maximum() + part.m_pageView->viewport()->width(); const int height = part.m_pageView->verticalScrollBar()->maximum() + part.m_pageView->viewport()->height(); // enter text-selection mode QVERIFY(QMetaObject::invokeMethod(part.m_pageView, "slotSetMouseTextSelect")); // Need to do this because the pop-menu will have his own mainloop and will block tests until // the menu disappear PageView *view = part.m_pageView; bool menuClosed = false; QTimer::singleShot(2000, [view, &menuClosed]() { // check if popup menu is active and visible QMenu *menu = qobject_cast(view->findChild(QStringLiteral("PopupMenu"))); QVERIFY(menu); QVERIFY(menu->isVisible()); // check if the menu contains "Follow this link" action QAction *processLink = qobject_cast(menu->findChild(QStringLiteral("ProcessLinkAction"))); QVERIFY(processLink); // chek if the menu contains "Copy Link Address" action QAction *actCopyLinkLocation = qobject_cast(menu->findChild(QStringLiteral("CopyLinkLocationAction"))); QVERIFY(actCopyLinkLocation); // close menu to continue test menu->close(); menuClosed = true; }); // r-click on url const double mouseClickX = width * 0.604; const double mouseClickY = height * 0.162; QTest::mouseMove(part.m_pageView->viewport(), QPoint(mouseClickX, mouseClickY)); QTest::mouseClick(part.m_pageView->viewport(), Qt::RightButton, Qt::NoModifier, QPoint(mouseClickX, mouseClickY), 1000); // will continue after pop-menu get closed QTRY_VERIFY(menuClosed); } void PartTest::testClickAnywhereAfterSelectionShouldUnselect() { QVariantList dummyArgs; Okular::Part part(nullptr, nullptr, dummyArgs); QVERIFY(openDocument(&part, QStringLiteral(KDESRCDIR "data/pdf_with_links.pdf"))); // resize window to avoid problem with selection areas part.widget()->resize(800, 600); part.widget()->show(); QVERIFY(QTest::qWaitForWindowExposed(part.widget())); part.m_document->setViewportPage(0); // wait for pixmap QTRY_VERIFY(part.m_document->page(0)->hasPixmap(part.m_pageView)); const int width = part.m_pageView->horizontalScrollBar()->maximum() + part.m_pageView->viewport()->width(); const int height = part.m_pageView->verticalScrollBar()->maximum() + part.m_pageView->viewport()->height(); // enter text-selection mode QVERIFY(QMetaObject::invokeMethod(part.m_pageView, "slotSetMouseTextSelect")); const double mouseY = height * 0.162; const double mouseStartX = width * 0.42; const double mouseEndX = width * 0.60; simulateMouseSelection(mouseStartX, mouseY, mouseEndX, mouseY, part.m_pageView->viewport()); // click on url const double mouseClickX = width * 0.10; QTest::mouseMove(part.m_pageView->viewport(), QPoint(mouseClickX, mouseY)); QTest::mouseClick(part.m_pageView->viewport(), Qt::LeftButton, Qt::NoModifier, QPoint(mouseClickX, mouseY), 1000); QApplication::clipboard()->clear(); QVERIFY(QMetaObject::invokeMethod(part.m_pageView, "copyTextSelection")); // check if copied text is empty what means no text selected QVERIFY(QApplication::clipboard()->text().isEmpty()); } void PartTest::testeRectSelectionStartingOnLinks() { QVariantList dummyArgs; Okular::Part part(nullptr, nullptr, dummyArgs); QVERIFY(openDocument(&part, QStringLiteral(KDESRCDIR "data/pdf_with_links.pdf"))); // hide info messages as they interfere with selection area Okular::Settings::self()->setShowOSD(false);; part.widget()->show(); QVERIFY(QTest::qWaitForWindowExposed(part.widget())); part.m_document->setViewportPage(0); // wait for pixmap QTRY_VERIFY(part.m_document->page(0)->hasPixmap(part.m_pageView)); const int width = part.m_pageView->horizontalScrollBar()->maximum() + part.m_pageView->viewport()->width(); const int height = part.m_pageView->verticalScrollBar()->maximum() + part.m_pageView->viewport()->height(); // enter text-selection mode QVERIFY(QMetaObject::invokeMethod(part.m_pageView, "slotSetMouseSelect")); const double mouseStartY = height * 0.127; const double mouseEndY = height * 0.127; const double mouseStartX = width * 0.28; const double mouseEndX = width * 0.382; // Need to do this because the pop-menu will have his own mainloop and will block tests until // the menu disappear PageView *view = part.m_pageView; bool menuClosed = false; QTimer::singleShot(2000, [view, &menuClosed]() { QApplication::clipboard()->clear(); // check if popup menu is active and visible QMenu *menu = qobject_cast(view->findChild(QStringLiteral("PopupMenu"))); QVERIFY(menu); QVERIFY(menu->isVisible()); // check if the copy selected text to clipboard is present QAction *copyAct = qobject_cast(menu->findChild(QStringLiteral("CopyTextToClipboard"))); QVERIFY(copyAct); menu->close(); menuClosed = true; }); simulateMouseSelection(mouseStartX, mouseStartY, mouseEndX, mouseEndY, part.m_pageView->viewport()); // wait menu get closed QTRY_VERIFY(menuClosed); } void PartTest::simulateMouseSelection(double startX, double startY, double endX, double endY, QWidget *target) { const int steps = 5; const double diffX = endX - startX; const double diffY = endY - startY; const double diffXStep = diffX / steps; const double diffYStep = diffY / steps; QTestEventList events; events.addMouseMove(QPoint(startX, startY)); events.addMousePress(Qt::LeftButton, Qt::NoModifier, QPoint(startX, startY)); for (int i = 0; i < steps - 1; ++i) { events.addMouseMove(QPoint(startX + i * diffXStep, startY + i * diffYStep)); events.addDelay(100); } events.addMouseMove(QPoint(endX, endY)); events.addDelay(100); events.addMouseRelease(Qt::LeftButton, Qt::NoModifier, QPoint(endX, endY)); events.simulate(target); } void PartTest::testSaveAsToNonExistingPath() { Okular::Part part(nullptr, nullptr, QVariantList()); part.openDocument( KDESRCDIR "data/file1.pdf" ); QString saveFilePath; { QTemporaryFile saveFile( QStringLiteral( "%1/okrXXXXXX.pdf" ).arg( QDir::tempPath() ) ); saveFile.open(); saveFilePath = saveFile.fileName(); // QTemporaryFile is destroyed and the file it created is gone, this is a TOCTOU but who cares } QVERIFY( !QFileInfo::exists( saveFilePath ) ); QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFilePath ), Part::NoSaveAsFlags ) ); QFile::remove( saveFilePath ); } void PartTest::testSaveAsToSymlink() { #ifdef Q_OS_UNIX Okular::Part part(nullptr, nullptr, QVariantList()); part.openDocument( KDESRCDIR "data/file1.pdf" ); QTemporaryFile newFile( QStringLiteral( "%1/okrXXXXXX.pdf" ).arg( QDir::tempPath() ) ); newFile.open(); QString linkFilePath; { QTemporaryFile linkFile( QStringLiteral( "%1/okrXXXXXX.pdf" ).arg( QDir::tempPath() ) ); linkFile.open(); linkFilePath = linkFile.fileName(); // QTemporaryFile is destroyed and the file it created is gone, this is a TOCTOU but who cares } QFile::link( newFile.fileName(), linkFilePath ); QVERIFY( QFileInfo( linkFilePath ).isSymLink() ); QVERIFY( part.saveAs( QUrl::fromLocalFile( linkFilePath ), Part::NoSaveAsFlags ) ); QVERIFY( QFileInfo( linkFilePath ).isSymLink() ); QFile::remove( linkFilePath ); #endif } void PartTest::testSaveIsSymlink() { #ifdef Q_OS_UNIX Okular::Part part(nullptr, nullptr, QVariantList()); QString newFilePath; { QTemporaryFile newFile( QStringLiteral( "%1/okrXXXXXX.pdf" ).arg( QDir::tempPath() ) ); newFile.open(); newFilePath = newFile.fileName(); // QTemporaryFile is destroyed and the file it created is gone, this is a TOCTOU but who cares } QFile::copy( KDESRCDIR "data/file1.pdf", newFilePath ); QString linkFilePath; { QTemporaryFile linkFile( QStringLiteral( "%1/okrXXXXXX.pdf" ).arg( QDir::tempPath() ) ); linkFile.open(); linkFilePath = linkFile.fileName(); // QTemporaryFile is destroyed and the file it created is gone, this is a TOCTOU but who cares } QFile::link( newFilePath, linkFilePath ); QVERIFY( QFileInfo( linkFilePath ).isSymLink() ); part.openDocument( linkFilePath ); QVERIFY( part.saveAs( QUrl::fromLocalFile( linkFilePath ), Part::NoSaveAsFlags ) ); QVERIFY( QFileInfo( linkFilePath ).isSymLink() ); QFile::remove( newFilePath ); QFile::remove( linkFilePath ); #endif } void PartTest::testSaveAs() { QFETCH(QString, file); QFETCH(QString, extension); QFETCH(bool, nativelySupportsAnnotations); QFETCH(bool, canSwapBackingFile); QScopedPointer closeDialogHelper; QString annotName; QTemporaryFile archiveSave( QStringLiteral( "%1/okrXXXXXX.okular" ).arg( QDir::tempPath() ) ); QTemporaryFile nativeDirectSave( QStringLiteral( "%1/okrXXXXXX.%2" ).arg( QDir::tempPath() ).arg ( extension ) ); QTemporaryFile nativeFromArchiveFile( QStringLiteral( "%1/okrXXXXXX.%2" ).arg( QDir::tempPath() ).arg ( extension ) ); QVERIFY( archiveSave.open() ); archiveSave.close(); QVERIFY( nativeDirectSave.open() ); nativeDirectSave.close(); QVERIFY( nativeFromArchiveFile.open() ); nativeFromArchiveFile.close(); qDebug() << "Open file, add annotation and save both natively and to .okular"; { Okular::Part part(nullptr, nullptr, QVariantList()); part.openDocument( file ); part.m_document->documentInfo(); QCOMPARE(part.m_document->canSwapBackingFile(), canSwapBackingFile); Okular::Annotation *annot = new Okular::TextAnnotation(); annot->setBoundingRectangle( Okular::NormalizedRect( 0.1, 0.1, 0.15, 0.15 ) ); annot->setContents( QStringLiteral("annot contents") ); part.m_document->addPageAnnotation( 0, annot ); annotName = annot->uniqueName(); if ( canSwapBackingFile ) { if ( !nativelySupportsAnnotations ) { closeDialogHelper.reset(new TestingUtils::CloseDialogHelper( &part, QDialogButtonBox::No )); // this is the "you're going to lose the annotations" dialog } QVERIFY( part.saveAs( QUrl::fromLocalFile( nativeDirectSave.fileName() ), Part::NoSaveAsFlags ) ); // For backends that don't support annotations natively we mark the part as still modified // after a save because we keep the annotation around but it will get lost if the user closes the app // so we want to give her a last chance to save on close with the "you have changes dialog" QCOMPARE( part.isModified(), !nativelySupportsAnnotations ); QVERIFY( part.saveAs( QUrl::fromLocalFile( archiveSave.fileName() ), Part::SaveAsOkularArchive ) ); } else { // We need to save to archive first otherwise we lose the annotation closeDialogHelper.reset(new TestingUtils::CloseDialogHelper( &part, QDialogButtonBox::Yes )); // this is the "you're going to lose the undo/redo stack" dialog QVERIFY( part.saveAs( QUrl::fromLocalFile( archiveSave.fileName() ), Part::SaveAsOkularArchive ) ); if ( !nativelySupportsAnnotations ) { closeDialogHelper.reset(new TestingUtils::CloseDialogHelper( &part, QDialogButtonBox::No )); // this is the "you're going to lose the annotations" dialog } QVERIFY( part.saveAs( QUrl::fromLocalFile( nativeDirectSave.fileName() ), Part::NoSaveAsFlags ) ); } QCOMPARE( part.m_document->documentInfo().get( Okular::DocumentInfo::FilePath ), part.m_document->currentDocument().toDisplayString() ); part.closeUrl(); } qDebug() << "Open the .okular, check that the annotation is present and save to native"; { Okular::Part part(nullptr, nullptr, QVariantList()); part.openDocument( archiveSave.fileName() ); part.m_document->documentInfo(); QCOMPARE( part.m_document->page( 0 )->annotations().size(), 1 ); QCOMPARE( part.m_document->page( 0 )->annotations().first()->uniqueName(), annotName ); if ( !nativelySupportsAnnotations ) { closeDialogHelper.reset(new TestingUtils::CloseDialogHelper( &part, QDialogButtonBox::No )); // this is the "you're going to lose the annotations" dialog } QVERIFY( part.saveAs( QUrl::fromLocalFile( nativeFromArchiveFile.fileName() ), Part::NoSaveAsFlags ) ); if ( canSwapBackingFile && !nativelySupportsAnnotations ) { // For backends that don't support annotations natively we mark the part as still modified // after a save because we keep the annotation around but it will get lost if the user closes the app // so we want to give her a last chance to save on close with the "you have changes dialog" closeDialogHelper.reset(new TestingUtils::CloseDialogHelper( &part, QDialogButtonBox::No )); // this is the "do you want to save or discard" dialog } QCOMPARE( part.m_document->documentInfo().get( Okular::DocumentInfo::FilePath ), part.m_document->currentDocument().toDisplayString() ); part.closeUrl(); } qDebug() << "Open the native file saved directly, and check that the annot" << "is there iff we expect it"; { Okular::Part part(nullptr, nullptr, QVariantList()); part.openDocument( nativeDirectSave.fileName() ); QCOMPARE( part.m_document->page( 0 )->annotations().size(), nativelySupportsAnnotations ? 1 : 0 ); if ( nativelySupportsAnnotations ) QCOMPARE( part.m_document->page( 0 )->annotations().first()->uniqueName(), annotName ); part.closeUrl(); } qDebug() << "Open the native file saved from the .okular, and check that the annot" << "is there iff we expect it"; { Okular::Part part(nullptr, nullptr, QVariantList()); part.openDocument( nativeFromArchiveFile.fileName() ); QCOMPARE( part.m_document->page( 0 )->annotations().size(), nativelySupportsAnnotations ? 1 : 0 ); if ( nativelySupportsAnnotations ) QCOMPARE( part.m_document->page( 0 )->annotations().first()->uniqueName(), annotName ); part.closeUrl(); } } void PartTest::testSaveAs_data() { QTest::addColumn("file"); QTest::addColumn("extension"); QTest::addColumn("nativelySupportsAnnotations"); QTest::addColumn("canSwapBackingFile"); QTest::newRow("pdf") << KDESRCDIR "data/file1.pdf" << "pdf" << true << true; QTest::newRow("pdf.gz") << KDESRCDIR "data/file1.pdf.gz" << "pdf" << true << true; QTest::newRow("epub") << KDESRCDIR "data/contents.epub" << "epub" << false << false; QTest::newRow("jpg") << KDESRCDIR "data/potato.jpg" << "jpg" << false << true; } void PartTest::testSidebarItemAfterSaving() { QVariantList dummyArgs; Okular::Part part(nullptr, nullptr, dummyArgs); QWidget *currentSidebarItem = part.m_sidebar->currentItem(); // thumbnails openDocument(&part, QStringLiteral(KDESRCDIR "data/tocreload.pdf")); // since it has TOC it changes to TOC QVERIFY(currentSidebarItem != part.m_sidebar->currentItem()); // now change back to thumbnails part.m_sidebar->setCurrentItem(currentSidebarItem); part.saveAs(QUrl::fromLocalFile(QStringLiteral(KDESRCDIR "data/tocreload.pdf"))); // Check it is still thumbnails after saving QCOMPARE(currentSidebarItem, part.m_sidebar->currentItem()); } void PartTest::testViewModeSavingPerFile() { QVariantList dummyArgs; Okular::Part part( nullptr, nullptr, dummyArgs ); // Open some file QVERIFY( openDocument( &part, QStringLiteral( KDESRCDIR "data/file1.pdf" ) ) ); // Switch to 'continuous' view mode part.m_pageView->setCapability( Okular::View::ViewCapability::Continuous, QVariant( true ) ); // Close document part.closeUrl(); // Open another file QVERIFY( openDocument( &part, QStringLiteral( KDESRCDIR "data/file2.pdf" ) ) ); // Switch to 'non-continuous' mode part.m_pageView->setCapability( Okular::View::ViewCapability::Continuous, QVariant( false ) ); // Close that document, too part.closeUrl(); // Open first document again QVERIFY( openDocument( &part, QStringLiteral( KDESRCDIR "data/file1.pdf" ) ) ); // If per-file view mode saving works, the view mode should be 'continuous' again. QVERIFY( part.m_pageView->capability( Okular::View::ViewCapability::Continuous).toBool() ); } void PartTest::testSaveAsUndoStackAnnotations() { QFETCH(QString, file); QFETCH(QString, extension); QFETCH(bool, nativelySupportsAnnotations); QFETCH(bool, canSwapBackingFile); QFETCH(bool, saveToArchive); const Part::SaveAsFlag saveFlags = saveToArchive ? Part::SaveAsOkularArchive : Part::NoSaveAsFlags; QScopedPointer closeDialogHelper; // closeDialogHelper relies on the availability of the "Continue" button to drop changes // when saving to a file format not supporting those. However, this button is only sensible // and available for "Save As", but not for "Save". By alternately saving to saveFile1 and // saveFile2 we always force "Save As", so closeDialogHelper keeps working. QTemporaryFile saveFile1( QStringLiteral( "%1/okrXXXXXX_1.%2" ).arg( QDir::tempPath() ).arg ( extension ) ); QVERIFY( saveFile1.open() ); saveFile1.close(); QTemporaryFile saveFile2( QStringLiteral( "%1/okrXXXXXX_2.%2" ).arg( QDir::tempPath() ).arg ( extension ) ); QVERIFY( saveFile2.open() ); saveFile2.close(); Okular::Part part(nullptr, nullptr, QVariantList()); part.openDocument( file ); QCOMPARE(part.m_document->canSwapBackingFile(), canSwapBackingFile); Okular::Annotation *annot = new Okular::TextAnnotation(); annot->setBoundingRectangle( Okular::NormalizedRect( 0.1, 0.1, 0.15, 0.15 ) ); annot->setContents( QStringLiteral("annot contents") ); part.m_document->addPageAnnotation( 0, annot ); QString annotName = annot->uniqueName(); if ( !nativelySupportsAnnotations && !saveToArchive ) { closeDialogHelper.reset(new TestingUtils::CloseDialogHelper( &part, QDialogButtonBox::No )); // this is the "you're going to lose the annotations" dialog } QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile1.fileName() ), saveFlags ) ); if (!canSwapBackingFile) { // The undo/redo stack gets lost if you can not swap the backing file QVERIFY( !part.m_document->canUndo() ); QVERIFY( !part.m_document->canRedo() ); return; } // Check we can still undo the annot add after save QVERIFY( part.m_document->canUndo() ); part.m_document->undo(); QVERIFY( !part.m_document->canUndo() ); QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile1.fileName() ), saveFlags ) ); QVERIFY( part.m_document->page( 0 )->annotations().isEmpty() ); // Check we can redo the annot add after save QVERIFY( part.m_document->canRedo() ); part.m_document->redo(); QVERIFY( !part.m_document->canRedo() ); if ( nativelySupportsAnnotations ) { // If the annots are provided by the backend we need to refetch the pointer after save annot = part.m_document->page( 0 )->annotation( annotName ); QVERIFY( annot ); } // Remove the annotation, creates another undo command QVERIFY( part.m_document->canRemovePageAnnotation( annot ) ); part.m_document->removePageAnnotation( 0, annot ); QVERIFY( part.m_document->page( 0 )->annotations().isEmpty() ); // Check we can still undo the annot remove after save QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile1.fileName() ), saveFlags ) ); QVERIFY( part.m_document->canUndo() ); part.m_document->undo(); QVERIFY( part.m_document->canUndo() ); QCOMPARE( part.m_document->page( 0 )->annotations().count(), 1 ); // Check we can still undo the annot add after save if ( !nativelySupportsAnnotations && !saveToArchive ) { closeDialogHelper.reset(new TestingUtils::CloseDialogHelper( &part, QDialogButtonBox::No )); // this is the "you're going to lose the annotations" dialog } QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile2.fileName() ), saveFlags ) ); QVERIFY( part.m_document->canUndo() ); part.m_document->undo(); QVERIFY( !part.m_document->canUndo() ); QVERIFY( part.m_document->page( 0 )->annotations().isEmpty() ); // Redo the add annotation QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile1.fileName() ), saveFlags ) ); QVERIFY( part.m_document->canRedo() ); part.m_document->redo(); QVERIFY( part.m_document->canUndo() ); QVERIFY( part.m_document->canRedo() ); if ( nativelySupportsAnnotations ) { // If the annots are provided by the backend we need to refetch the pointer after save annot = part.m_document->page( 0 )->annotation( annotName ); QVERIFY( annot ); } // Add translate, adjust and modify commands part.m_document->translatePageAnnotation( 0, annot, Okular::NormalizedPoint( 0.1, 0.1 ) ); part.m_document->adjustPageAnnotation( 0, annot, Okular::NormalizedPoint( 0.1, 0.1 ), Okular::NormalizedPoint( 0.1, 0.1 ) ); part.m_document->prepareToModifyAnnotationProperties( annot ); part.m_document->modifyPageAnnotationProperties( 0, annot ); // Now check we can still undo/redo/save at all the intermediate states and things still work if ( !nativelySupportsAnnotations && !saveToArchive ) { closeDialogHelper.reset(new TestingUtils::CloseDialogHelper( &part, QDialogButtonBox::No )); // this is the "you're going to lose the annotations" dialog } QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile2.fileName() ), saveFlags ) ); QVERIFY( part.m_document->canUndo() ); part.m_document->undo(); QVERIFY( part.m_document->canUndo() ); if ( !nativelySupportsAnnotations && !saveToArchive ) { closeDialogHelper.reset(new TestingUtils::CloseDialogHelper( &part, QDialogButtonBox::No )); // this is the "you're going to lose the annotations" dialog } QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile1.fileName() ), saveFlags ) ); QVERIFY( part.m_document->canUndo() ); part.m_document->undo(); QVERIFY( part.m_document->canUndo() ); if ( !nativelySupportsAnnotations && !saveToArchive ) { closeDialogHelper.reset(new TestingUtils::CloseDialogHelper( &part, QDialogButtonBox::No )); // this is the "you're going to lose the annotations" dialog } QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile2.fileName() ), saveFlags ) ); QVERIFY( part.m_document->canUndo() ); part.m_document->undo(); QVERIFY( part.m_document->canUndo() ); if ( !nativelySupportsAnnotations && !saveToArchive ) { closeDialogHelper.reset(new TestingUtils::CloseDialogHelper( &part, QDialogButtonBox::No )); // this is the "you're going to lose the annotations" dialog } QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile1.fileName() ), saveFlags ) ); QVERIFY( part.m_document->canUndo() ); part.m_document->undo(); QVERIFY( !part.m_document->canUndo() ); QVERIFY( part.m_document->canRedo() ); QVERIFY( part.m_document->page( 0 )->annotations().isEmpty() ); QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile1.fileName() ), saveFlags ) ); QVERIFY( part.m_document->canRedo() ); part.m_document->redo(); QVERIFY( part.m_document->canRedo() ); if ( !nativelySupportsAnnotations && !saveToArchive ) { closeDialogHelper.reset(new TestingUtils::CloseDialogHelper( &part, QDialogButtonBox::No )); // this is the "you're going to lose the annotations" dialog } QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile2.fileName() ), saveFlags ) ); QVERIFY( part.m_document->canRedo() ); part.m_document->redo(); QVERIFY( part.m_document->canRedo() ); if ( !nativelySupportsAnnotations && !saveToArchive ) { closeDialogHelper.reset(new TestingUtils::CloseDialogHelper( &part, QDialogButtonBox::No )); // this is the "you're going to lose the annotations" dialog } QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile1.fileName() ), saveFlags ) ); QVERIFY( part.m_document->canRedo() ); part.m_document->redo(); QVERIFY( part.m_document->canRedo() ); if ( !nativelySupportsAnnotations && !saveToArchive ) { closeDialogHelper.reset(new TestingUtils::CloseDialogHelper( &part, QDialogButtonBox::No )); // this is the "you're going to lose the annotations" dialog } QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile2.fileName() ), saveFlags ) ); QVERIFY( part.m_document->canRedo() ); part.m_document->redo(); QVERIFY( !part.m_document->canRedo() ); closeDialogHelper.reset(new TestingUtils::CloseDialogHelper( &part, QDialogButtonBox::No )); // this is the "do you want to save or discard" dialog part.closeUrl(); } void PartTest::testSaveAsUndoStackAnnotations_data() { QTest::addColumn("file"); QTest::addColumn("extension"); QTest::addColumn("nativelySupportsAnnotations"); QTest::addColumn("canSwapBackingFile"); QTest::addColumn("saveToArchive"); QTest::newRow("pdf") << KDESRCDIR "data/file1.pdf" << "pdf" << true << true << false; QTest::newRow("epub") << KDESRCDIR "data/contents.epub" << "epub" << false << false << false; QTest::newRow("jpg") << KDESRCDIR "data/potato.jpg" << "jpg" << false << true << false; QTest::newRow("pdfarchive") << KDESRCDIR "data/file1.pdf" << "okular" << true << true << true; QTest::newRow("jpgarchive") << KDESRCDIR "data/potato.jpg" << "okular" << false << true << true; } void PartTest::testSaveAsUndoStackForms() { QFETCH(QString, file); QFETCH(QString, extension); QFETCH(bool, saveToArchive); const Part::SaveAsFlag saveFlags = saveToArchive ? Part::SaveAsOkularArchive : Part::NoSaveAsFlags; QTemporaryFile saveFile( QStringLiteral( "%1/okrXXXXXX.%2" ).arg( QDir::tempPath(), extension ) ); QVERIFY( saveFile.open() ); saveFile.close(); Okular::Part part(nullptr, nullptr, QVariantList()); part.openDocument( file ); for ( FormField *ff : part.m_document->page( 0 )->formFields() ) { if ( ff->id() == 65537 ) { QCOMPARE( ff->type(), FormField::FormText ); FormFieldText *fft = static_cast( ff ); part.m_document->editFormText( 0, fft, QStringLiteral("BlaBla"), 6, 0, 0 ); } else if ( ff->id() == 65538 ) { QCOMPARE( ff->type(), FormField::FormButton ); FormFieldButton *ffb = static_cast( ff ); QCOMPARE( ffb->buttonType(), FormFieldButton::Radio ); part.m_document->editFormButtons( 0, QList< FormFieldButton* >() << ffb, QList< bool >() << true ); } else if ( ff->id() == 65542 ) { QCOMPARE( ff->type(), FormField::FormChoice ); FormFieldChoice *ffc = static_cast( ff ); QCOMPARE( ffc->choiceType(), FormFieldChoice::ListBox ); part.m_document->editFormList( 0, ffc, QList< int >() << 1 ); } else if ( ff->id() == 65543 ) { QCOMPARE( ff->type(), FormField::FormChoice ); FormFieldChoice *ffc = static_cast( ff ); QCOMPARE( ffc->choiceType(), FormFieldChoice::ComboBox ); part.m_document->editFormCombo( 0, ffc, QStringLiteral("combo2"), 3, 0, 0); } } QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile.fileName() ), saveFlags ) ); QVERIFY( part.m_document->canUndo() ); part.m_document->undo(); QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile.fileName() ), saveFlags ) ); QVERIFY( part.m_document->canUndo() ); part.m_document->undo(); QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile.fileName() ), saveFlags ) ); QVERIFY( part.m_document->canUndo() ); part.m_document->undo(); QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile.fileName() ), saveFlags ) ); QVERIFY( part.m_document->canUndo() ); part.m_document->undo(); QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile.fileName() ), saveFlags ) ); QVERIFY( !part.m_document->canUndo() ); QVERIFY( part.m_document->canRedo() ); part.m_document->redo(); QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile.fileName() ), saveFlags ) ); QVERIFY( part.m_document->canRedo() ); part.m_document->redo(); QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile.fileName() ), saveFlags ) ); QVERIFY( part.m_document->canRedo() ); part.m_document->redo(); QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile.fileName() ), saveFlags ) ); QVERIFY( part.m_document->canRedo() ); part.m_document->redo(); QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile.fileName() ), saveFlags ) ); } void PartTest::testSaveAsUndoStackForms_data() { QTest::addColumn("file"); QTest::addColumn("extension"); QTest::addColumn("saveToArchive"); QTest::newRow("pdf") << KDESRCDIR "data/formSamples.pdf" << "pdf" << false; QTest::newRow("pdfarchive") << KDESRCDIR "data/formSamples.pdf" << "okular" << true; } void PartTest::testOpenUrlArguments() { QVariantList dummyArgs; - Okular::Part part(NULL, NULL, dummyArgs); + Okular::Part part(nullptr, nullptr, dummyArgs); KParts::OpenUrlArguments args; args.setMimeType(QStringLiteral("text/rtf")); part.setArguments(args); part.openUrl(QUrl::fromLocalFile(QStringLiteral(KDESRCDIR "data/file1.pdf"))); QCOMPARE( part.arguments().mimeType(), QStringLiteral("text/rtf") ); } void PartTest::test388288() { Okular::Part part(nullptr, nullptr, QVariantList()); part.openUrl(QUrl::fromLocalFile(QStringLiteral(KDESRCDIR "data/file1.pdf"))); part.widget()->show(); QVERIFY(QTest::qWaitForWindowExposed(part.widget())); QMetaObject::invokeMethod(part.m_pageView, "slotToggleAnnotator", Q_ARG( bool, true )); auto annot = new Okular::HighlightAnnotation(); annot->setHighlightType( Okular::HighlightAnnotation::Highlight ); const Okular::NormalizedRect r(0.36, 0.16, 0.51, 0.17); annot->setBoundingRectangle( r ); Okular::HighlightAnnotation::Quad q; q.setCapStart( false ); q.setCapEnd( false ); q.setFeather( 1.0 ); q.setPoint( Okular::NormalizedPoint( r.left, r.bottom ), 0 ); q.setPoint( Okular::NormalizedPoint( r.right, r.bottom ), 1 ); q.setPoint( Okular::NormalizedPoint( r.right, r.top ), 2 ); q.setPoint( Okular::NormalizedPoint( r.left, r.top ), 3 ); annot->highlightQuads().append( q ); part.m_document->addPageAnnotation( 0, annot ); const int width = part.m_pageView->horizontalScrollBar()->maximum() + part.m_pageView->viewport()->width(); const int height = part.m_pageView->verticalScrollBar()->maximum() + part.m_pageView->viewport()->height(); QTest::mouseMove(part.m_pageView->viewport(), QPoint(width * 0.5, height * 0.5)); QTRY_COMPARE(part.m_pageView->cursor().shape(), Qt::OpenHandCursor); QTest::mouseMove(part.m_pageView->viewport(), QPoint(width * 0.4, height * 0.165)); QTRY_COMPARE(part.m_pageView->cursor().shape(), Qt::ArrowCursor); QTest::mouseMove(part.m_pageView->viewport(), QPoint(width * 0.1, height * 0.165)); part.m_document->undo(); annot = new Okular::HighlightAnnotation(); annot->setHighlightType( Okular::HighlightAnnotation::Highlight ); annot->setBoundingRectangle( r ); annot->highlightQuads().append( q ); part.m_document->addPageAnnotation( 0, annot ); QTest::mouseMove(part.m_pageView->viewport(), QPoint(width * 0.5, height * 0.5)); QTRY_COMPARE(part.m_pageView->cursor().shape(), Qt::OpenHandCursor); } void PartTest::testCheckBoxReadOnly() { #ifndef HAVE_POPPLER_0_64 return; #endif const QString testFile = QStringLiteral( KDESRCDIR "data/checkbox_ro.pdf" ); Okular::Part part( nullptr, nullptr, QVariantList() ); part.openDocument( testFile ); // The test document uses the activation action of checkboxes // to update the read only state. For this we need the part so that // undo / redo activates the activation action. QVERIFY( part.m_document->isOpened() ); const Okular::Page* page = part.m_document->page( 0 ); QMap fields; // Field names in test document are: // CBMakeRW, CBMakeRO, TargetDefaultRO, TargetDefaultRW for ( Okular::FormField *ff: page->formFields() ) { fields.insert( ff->name(), static_cast< Okular::FormField* >( ff ) ); } // First grab all fields and check that the setup is as expected. auto cbMakeRW = dynamic_cast< Okular::FormFieldButton* > ( fields[QStringLiteral( "CBMakeRW" )] ); auto cbMakeRO = dynamic_cast< Okular::FormFieldButton* > ( fields[QStringLiteral( "CBMakeRO" )] ); auto targetDefaultRW = dynamic_cast< Okular::FormFieldText* > ( fields[QStringLiteral( "TargetDefaultRw" )] ); auto targetDefaultRO = dynamic_cast< Okular::FormFieldText* > ( fields[QStringLiteral( "TargetDefaultRo" )] ); QVERIFY( cbMakeRW ); QVERIFY( cbMakeRO ); QVERIFY( targetDefaultRW ); QVERIFY( targetDefaultRO ); QVERIFY( !cbMakeRW->state() ); QVERIFY( !cbMakeRO->state() ); QVERIFY( !targetDefaultRW->isReadOnly() ); QVERIFY( targetDefaultRO->isReadOnly() ); QList< Okular::FormFieldButton* > btns; btns << cbMakeRW << cbMakeRO; // Now check both boxes QList< bool > btnStates; btnStates << true << true; part.m_document->editFormButtons( 0, btns, btnStates ); // Read only should be inverted QVERIFY( targetDefaultRW->isReadOnly() ); QVERIFY( !targetDefaultRO->isReadOnly() ); // Test that undo / redo works QVERIFY( part.m_document->canUndo() ); part.m_document->undo(); QVERIFY( !targetDefaultRW->isReadOnly() ); QVERIFY( targetDefaultRO->isReadOnly() ); part.m_document->redo(); QVERIFY( targetDefaultRW->isReadOnly() ); QVERIFY( !targetDefaultRO->isReadOnly() ); btnStates.clear(); btnStates << false << true; part.m_document->editFormButtons( 0, btns, btnStates ); QVERIFY( targetDefaultRW->isReadOnly() ); QVERIFY( targetDefaultRO->isReadOnly() ); // Now set both to checked again and confirm that // save / load works. btnStates.clear(); btnStates << true << true; part.m_document->editFormButtons( 0, btns, btnStates ); QTemporaryFile saveFile( QStringLiteral( "%1/okrXXXXXX.pdf" ).arg( QDir::tempPath() ) ); QVERIFY( saveFile.open() ); saveFile.close(); // Save QVERIFY( part.saveAs( QUrl::fromLocalFile( saveFile.fileName() ), Part::NoSaveAsFlags ) ); part.closeUrl(); // Load part.openDocument( saveFile.fileName() ); QVERIFY( part.m_document->isOpened() ); page = part.m_document->page( 0 ); fields.clear(); for ( Okular::FormField *ff: page->formFields() ) { fields.insert( ff->name(), static_cast< Okular::FormField* >( ff ) ); } cbMakeRW = dynamic_cast< Okular::FormFieldButton* > ( fields[QStringLiteral( "CBMakeRW" )] ); cbMakeRO = dynamic_cast< Okular::FormFieldButton* > ( fields[QStringLiteral( "CBMakeRO" )] ); targetDefaultRW = dynamic_cast< Okular::FormFieldText* > ( fields[QStringLiteral( "TargetDefaultRw" )] ); targetDefaultRO = dynamic_cast< Okular::FormFieldText* > ( fields[QStringLiteral( "TargetDefaultRo" )] ); QVERIFY( cbMakeRW->state() ); QVERIFY( cbMakeRO->state() ); QVERIFY( targetDefaultRW->isReadOnly() ); QVERIFY( !targetDefaultRO->isReadOnly() ); } void PartTest::testCrashTextEditDestroy() { const QString testFile = QStringLiteral( KDESRCDIR "data/formSamples.pdf" ); Okular::Part part( nullptr, nullptr, QVariantList() ); part.openDocument( testFile ); part.widget()->show(); QVERIFY(QTest::qWaitForWindowExposed(part.widget())); part.widget()->findChild()->setText(QStringLiteral("HOLA")); part.actionCollection()->action(QStringLiteral("view_toggle_forms"))->trigger(); } void PartTest::testAnnotWindow() { QVariantList dummyArgs; Okular::Part part(nullptr, nullptr, dummyArgs); QVERIFY(openDocument(&part, QStringLiteral(KDESRCDIR "data/file1.pdf"))); part.widget()->show(); part.widget()->resize(800, 600); QVERIFY(QTest::qWaitForWindowExposed(part.widget())); part.m_document->setViewportPage(0); QMetaObject::invokeMethod(part.m_pageView, "slotSetMouseNormal"); QCOMPARE(part.m_document->currentPage(), 0u); // Create two distinct text annotations Okular::Annotation * annot1 = new Okular::TextAnnotation(); annot1->setBoundingRectangle( Okular::NormalizedRect( 0.8, 0.1, 0.85, 0.15 ) ); annot1->setContents( QStringLiteral("Annot contents 111111") ); Okular::Annotation *annot2 = new Okular::TextAnnotation(); annot2->setBoundingRectangle( Okular::NormalizedRect( 0.8, 0.3, 0.85, 0.35 ) ); annot2->setContents( QStringLiteral("Annot contents 222222") ); // Add annot1 and annot2 to document part.m_document->addPageAnnotation( 0, annot1 ); part.m_document->addPageAnnotation( 0, annot2 ); QVERIFY( part.m_document->page( 0 )->annotations().size() == 2 ); QTimer *delayResizeEventTimer = part.m_pageView->findChildren("delayResizeEventTimer").at(0); QVERIFY(delayResizeEventTimer->isActive()); QTest::qWait(delayResizeEventTimer->interval() * 2); // wait for pixmap QTRY_VERIFY(part.m_document->page(0)->hasPixmap(part.m_pageView)); const int width = part.m_pageView->horizontalScrollBar()->maximum() + part.m_pageView->viewport()->width(); const int height = part.m_pageView->verticalScrollBar()->maximum() + part.m_pageView->viewport()->height(); // Double click the first annotation to open its window (move mouse for visual feedback) const NormalizedPoint annot1pt = annot1->boundingRectangle().center(); QTest::mouseMove(part.m_pageView->viewport(), QPoint(width * annot1pt.x, height * annot1pt.y)); QTest::mouseDClick(part.m_pageView->viewport(), Qt::LeftButton, Qt::NoModifier, QPoint(width * annot1pt.x, height * annot1pt.y)); QTRY_COMPARE( part.m_pageView->findChildren("AnnotWindow").size(), 1 ); // Verify that the window is visible QFrame* win1 = part.m_pageView->findChild(QStringLiteral("AnnotWindow")); QVERIFY( !win1->visibleRegion().isEmpty() ); // Double click the second annotation to open its window (move mouse for visual feedback) const NormalizedPoint annot2pt = annot2->boundingRectangle().center(); QTest::mouseMove(part.m_pageView->viewport(), QPoint(width * annot2pt.x, height * annot2pt.y)); QTest::mouseDClick(part.m_pageView->viewport(), Qt::LeftButton, Qt::NoModifier, QPoint(width * annot2pt.x, height * annot2pt.y)); QTRY_COMPARE( part.m_pageView->findChildren("AnnotWindow").size(), 2 ); // Verify that the first window is hidden covered by the second, which is visible QList lstWin = part.m_pageView->findChildren(QStringLiteral("AnnotWindow")); QFrame * win2; if (lstWin[0] == win1) { win2 = lstWin[1]; } else { win2 = lstWin[0]; } QVERIFY( win1->visibleRegion().isEmpty() ); QVERIFY( !win2->visibleRegion().isEmpty() ); // Double click the first annotation to raise its window (move mouse for visual feedback) QTest::mouseMove(part.m_pageView->viewport(), QPoint(width * annot1pt.x, height * annot1pt.y)); QTest::mouseDClick(part.m_pageView->viewport(), Qt::LeftButton, Qt::NoModifier, QPoint(width * annot1pt.x, height * annot1pt.y)); // Verify that the second window is hidden covered by the first, which is visible QVERIFY( !win1->visibleRegion().isEmpty() ); QVERIFY( win2->visibleRegion().isEmpty() ); // Move annotation window 1 to partially show annotation window 2 win1->move(QPoint(win2->pos().x(), win2->pos().y() + 50)); // Verify that both windows are partially visible QVERIFY( !win1->visibleRegion().isEmpty() ); QVERIFY( !win2->visibleRegion().isEmpty() ); // Click the second annotation window to raise it (move mouse for visual feedback) auto widget = win2->window()->childAt(win2->mapTo(win2->window(), QPoint(10, 10))); QTest::mouseMove(win2->window(), win2->mapTo(win2->window(), QPoint(10, 10))); QTest::mouseClick(widget, Qt::LeftButton, Qt::NoModifier, widget->mapFrom(win2, QPoint(10, 10))); QVERIFY( win1->visibleRegion().rectCount() == 3); QVERIFY( win2->visibleRegion().rectCount() == 4); } // Helper for testAdditionalActionTriggers static void verifyTargetStates( const QString & triggerName, const QMap &fields, bool focusVisible, bool cursorVisible, bool mouseVisible, int line) { Okular::FormField *focusTarget = fields.value( triggerName + QStringLiteral ("_focus_target") ); Okular::FormField *cursorTarget = fields.value( triggerName + QStringLiteral ("_cursor_target") ); Okular::FormField *mouseTarget = fields.value( triggerName + QStringLiteral ("_mouse_target") ); QVERIFY( focusTarget ); QVERIFY( cursorTarget ); QVERIFY( mouseTarget ); QTRY_VERIFY2( focusTarget->isVisible() == focusVisible, QStringLiteral ("line: %1 focus for %2 not matched. Expected %3 Actual %4"). arg( line ).arg( triggerName ).arg( focusTarget->isVisible() ).arg( focusVisible ).toUtf8().constData() ); QTRY_VERIFY2( cursorTarget->isVisible() == cursorVisible, QStringLiteral ("line: %1 cursor for %2 not matched. Actual %3 Expected %4"). arg( line ).arg( triggerName ).arg( cursorTarget->isVisible() ).arg( cursorVisible ).toUtf8().constData() ); QTRY_VERIFY2( mouseTarget->isVisible() == mouseVisible, QStringLiteral ("line: %1 mouse for %2 not matched. Expected %3 Actual %4"). arg( line ).arg( triggerName ).arg( mouseTarget->isVisible() ).arg( mouseVisible ).toUtf8().constData() ); } void PartTest::testAdditionalActionTriggers() { #ifndef HAVE_POPPLER_0_65 return; #endif const QString testFile = QStringLiteral( KDESRCDIR "data/additionalFormActions.pdf" ); Okular::Part part( nullptr, nullptr, QVariantList() ); part.openDocument( testFile ); part.widget()->resize(800, 600); part.widget()->show(); QVERIFY( QTest::qWaitForWindowExposed( part.widget() ) ); QTimer *delayResizeEventTimer = part.m_pageView->findChildren("delayResizeEventTimer").at(0); QVERIFY(delayResizeEventTimer->isActive()); QTest::qWait(delayResizeEventTimer->interval() * 2); part.m_document->setViewportPage( 0 ); // wait for pixmap QTRY_VERIFY( part.m_document->page( 0 )->hasPixmap( part.m_pageView) ); QMap fields; // Field names in test document are: // For trigger fields: tf, cb, rb, dd, pb // For target fields: _focus_target, _cursor_target, // _mouse_target const Okular::Page* page = part.m_document->page( 0 ); for ( Okular::FormField *ff: page->formFields() ) { fields.insert( ff->name(), static_cast< Okular::FormField* >( ff ) ); } // Verify that everything is set up. verifyTargetStates( QStringLiteral( "tf" ), fields, true, true, true, __LINE__ ); verifyTargetStates( QStringLiteral( "cb" ), fields, true, true, true, __LINE__ ); verifyTargetStates( QStringLiteral( "rb" ), fields, true, true, true, __LINE__ ); verifyTargetStates( QStringLiteral( "dd" ), fields, true, true, true, __LINE__ ); verifyTargetStates( QStringLiteral( "pb" ), fields, true, true, true, __LINE__ ); const int width = part.m_pageView->horizontalScrollBar()->maximum() + part.m_pageView->viewport()->width(); const int height = part.m_pageView->verticalScrollBar()->maximum() + part.m_pageView->viewport()->height(); part.actionCollection()->action( QStringLiteral( "view_toggle_forms" ) )->trigger(); QPoint tfPos( width * 0.045, height * 0.05 ); QPoint cbPos( width * 0.045, height * 0.08 ); QPoint rbPos( width * 0.045, height * 0.12 ); QPoint ddPos( width * 0.045, height * 0.16 ); QPoint pbPos( width * 0.045, height * 0.26 ); // Test text field auto widget = part.m_pageView->viewport()->childAt( tfPos ); QVERIFY( widget ); QTest::mouseMove( part.m_pageView->viewport(), QPoint( tfPos )); verifyTargetStates( QStringLiteral( "tf" ), fields, true, false, true, __LINE__ ); QTest::mousePress( widget, Qt::LeftButton, Qt::NoModifier, QPoint( 5, 5 ) ); verifyTargetStates( QStringLiteral( "tf" ), fields, false, false, false, __LINE__ ); QTest::mouseRelease( widget, Qt::LeftButton, Qt::NoModifier, QPoint( 5, 5 ) ); verifyTargetStates( QStringLiteral( "tf" ), fields, false, false, true, __LINE__ ); // Checkbox widget = part.m_pageView->viewport()->childAt( cbPos ); QVERIFY( widget ); QTest::mouseMove( part.m_pageView->viewport(), QPoint( cbPos ) ); verifyTargetStates( QStringLiteral( "cb" ), fields, true, false, true, __LINE__ ); QTest::mousePress( widget, Qt::LeftButton, Qt::NoModifier, QPoint( 5, 5 ) ); verifyTargetStates( QStringLiteral( "cb" ), fields, false, false, false, __LINE__ ); // Confirm that the textfield no longer has any invisible verifyTargetStates( QStringLiteral( "tf" ), fields, true, true, true, __LINE__ ); QTest::mouseRelease( widget, Qt::LeftButton, Qt::NoModifier, QPoint( 5, 5 ) ); verifyTargetStates( QStringLiteral( "cb" ), fields, false, false, true, __LINE__ ); // Radio widget = part.m_pageView->viewport()->childAt( rbPos ); QVERIFY( widget ); QTest::mouseMove( part.m_pageView->viewport(), QPoint( rbPos ) ); verifyTargetStates( QStringLiteral( "rb" ), fields, true, false, true, __LINE__ ); QTest::mousePress( widget, Qt::LeftButton, Qt::NoModifier, QPoint( 5, 5 ) ); verifyTargetStates( QStringLiteral( "rb" ), fields, false, false, false, __LINE__ ); QTest::mouseRelease( widget, Qt::LeftButton, Qt::NoModifier, QPoint( 5, 5 ) ); verifyTargetStates( QStringLiteral( "rb" ), fields, false, false, true, __LINE__ ); // Dropdown widget = part.m_pageView->viewport()->childAt( ddPos ); QVERIFY( widget ); QTest::mouseMove( part.m_pageView->viewport(), QPoint( ddPos ) ); verifyTargetStates( QStringLiteral( "dd" ), fields, true, false, true, __LINE__ ); QTest::mousePress( widget, Qt::LeftButton, Qt::NoModifier, QPoint( 5, 5 ) ); verifyTargetStates( QStringLiteral( "dd" ), fields, false, false, false, __LINE__ ); QTest::mouseRelease( widget, Qt::LeftButton, Qt::NoModifier, QPoint( 5, 5 ) ); verifyTargetStates( QStringLiteral( "dd" ), fields, false, false, true, __LINE__ ); // Pushbutton widget = part.m_pageView->viewport()->childAt( pbPos ); QVERIFY( widget ); QTest::mouseMove( part.m_pageView->viewport(), QPoint( pbPos ) ); verifyTargetStates( QStringLiteral( "pb" ), fields, true, false, true, __LINE__ ); QTest::mousePress( widget, Qt::LeftButton, Qt::NoModifier, QPoint( 5, 5 ) ); verifyTargetStates( QStringLiteral( "pb" ), fields, false, false, false, __LINE__ ); QTest::mouseRelease( widget, Qt::LeftButton, Qt::NoModifier, QPoint( 5, 5 ) ); verifyTargetStates( QStringLiteral( "pb" ), fields, false, false, true, __LINE__ ); // Confirm that a mouse release outside does not trigger the show action. QTest::mousePress( widget, Qt::LeftButton, Qt::NoModifier, QPoint( 5, 5 ) ); verifyTargetStates( QStringLiteral( "pb" ), fields, false, false, false, __LINE__ ); QTest::mouseRelease( part.m_pageView->viewport(), Qt::LeftButton, Qt::NoModifier, tfPos ); verifyTargetStates( QStringLiteral( "pb" ), fields, false, false, false, __LINE__ ); } void PartTest::testTypewriterAnnotTool() { Okular::Part part(nullptr, nullptr, QVariantList()); part.openUrl(QUrl::fromLocalFile(QStringLiteral(KDESRCDIR "data/file1.pdf"))); part.widget()->show(); QVERIFY(QTest::qWaitForWindowExposed(part.widget())); const int width = part.m_pageView->horizontalScrollBar()->maximum() + part.m_pageView->viewport()->width(); const int height = part.m_pageView->verticalScrollBar()->maximum() + part.m_pageView->viewport()->height(); part.m_document->setViewportPage(0); QMetaObject::invokeMethod(part.m_pageView, "slotToggleAnnotator", Q_ARG( bool, true )); // Find the button for the TypeWriter annotation QList toolbuttonList = part.m_pageView->findChildren(); auto it = std::find_if( toolbuttonList.begin(), toolbuttonList.end(), [](const QToolButton * x) { return x->toolTip().contains("Typewriter"); } ); QVERIFY(it != toolbuttonList.end()); QToolButton* typewriterButton = *it; typewriterButton->click(); QTest::qWait(1000); // Wait for the "add new note" dialog to appear TestingUtils::CloseDialogHelper closeDialogHelper( QDialogButtonBox::Ok ); QTest::mouseClick(part.m_pageView->viewport(), Qt::LeftButton, Qt::NoModifier, QPoint(width * 0.5, height * 0.2)); Annotation* annot = part.m_document->page(0)->annotations().first(); TextAnnotation* ta = static_cast( annot ); QVERIFY( annot ); QVERIFY( ta ); QCOMPARE( annot->subType(), Okular::Annotation::AText ); QCOMPARE( annot->style().color(), QColor(255,255,255,0) ); QCOMPARE( ta->textType(), Okular::TextAnnotation::InPlace ); QCOMPARE( ta->inplaceIntent(), Okular::TextAnnotation::TypeWriter ); } void PartTest::testJumpToPage() { const QString testFile = QStringLiteral( KDESRCDIR "data/simple-multipage.pdf" ); const int targetPage = 25; Okular::Part part( nullptr, nullptr, QVariantList() ); part.openDocument( testFile ); part.widget()->resize(800, 600); part.widget()->show(); QVERIFY( QTest::qWaitForWindowExposed( part.widget() ) ); part.m_document->pages(); part.m_document->setViewportPage( targetPage ); /* Document::setViewportPage triggers pixmap rendering in another thread. * We want to test how things look AFTER finished signal arrives back, * because PageView::slotRelayoutPages may displace the viewport again. */ QTRY_VERIFY( part.m_document->page( targetPage )->hasPixmap( part.m_pageView ) ); const int contentAreaHeight = part.m_pageView->verticalScrollBar()->maximum() + part.m_pageView->viewport()->height(); const int pageWithSpaceTop = contentAreaHeight / part.m_document->pages() * targetPage; /* * This is a test for a "known by trial" displacement. * We'd need access to part.m_pageView->d->items[targetPage]->croppedGeometry().top(), * to determine the expected viewport position, but we don't have access. */ QCOMPARE(part.m_pageView->verticalScrollBar()->value(), pageWithSpaceTop - 4); } void PartTest::testForwardBackwardNavigation() { const QString testFile = QStringLiteral( KDESRCDIR "data/simple-multipage.pdf" ); Okular::Part part( nullptr, nullptr, QVariantList() ); part.openDocument( testFile ); part.widget()->resize(800, 600); part.widget()->show(); QVERIFY( QTest::qWaitForWindowExposed( part.widget() ) ); // Go to some page const int targetPageA = 15; part.m_document->setViewportPage( targetPageA ); QVERIFY( part.m_document->viewport() == targetPageA ); // Go to some other page const int targetPageB = 25; part.m_document->setViewportPage( targetPageB ); QVERIFY( part.m_document->viewport() == targetPageB ); // Go back to page A QVERIFY(QMetaObject::invokeMethod(&part, "slotHistoryBack")); QVERIFY( part.m_document->viewport().pageNumber == targetPageA ); // Go back to page B QVERIFY(QMetaObject::invokeMethod(&part, "slotHistoryNext")); QVERIFY( part.m_document->viewport().pageNumber == targetPageB ); } void PartTest::testTabletProximityBehavior() { QVariantList dummyArgs; Okular::Part part{ nullptr, nullptr, dummyArgs }; QVERIFY( openDocument( &part, QStringLiteral( KDESRCDIR "data/file1.pdf" ) ) ); part.slotShowPresentation(); PresentationWidget *w = part.m_presentationWidget; QVERIFY( w ); part.widget()->show(); // close the KMessageBox "There are two ways of exiting[...]" TestingUtils::CloseDialogHelper closeDialogHelper( w, QDialogButtonBox::Ok ); // confirm the "To leave, press ESC" QTabletEvent enterProximityEvent{ QEvent::TabletEnterProximity, QPoint( 10, 10 ), QPoint( 10, 10 ), QTabletEvent::Stylus, QTabletEvent::Pen, 1., 0, 0, 1., 1., 0, Qt::NoModifier, 0, Qt::NoButton, Qt::NoButton }; QTabletEvent leaveProximityEvent{ QEvent::TabletLeaveProximity, QPoint( 10, 10 ), QPoint( 10, 10 ), QTabletEvent::Stylus, QTabletEvent::Pen, 1., 0, 0, 1., 1., 0, Qt::NoModifier, 0, Qt::NoButton, Qt::NoButton }; // Test with the Okular::Settings::EnumSlidesCursor::Visible setting Okular::Settings::self()->setSlidesCursor( Okular::Settings::EnumSlidesCursor::Visible ); // Send an enterProximity event qApp->notify( qApp, &enterProximityEvent ); // The cursor should be a cross-hair QVERIFY( w->cursor().shape() == Qt::CursorShape( Qt::CrossCursor ) ); // Send a leaveProximity event qApp->notify( qApp, &leaveProximityEvent ); // After the leaveProximityEvent, the cursor should be an arrow again, because // we have set the slidesCursor mode to 'Visible' QVERIFY( w->cursor().shape() == Qt::CursorShape( Qt::ArrowCursor ) ); // Test with the Okular::Settings::EnumSlidesCursor::Hidden setting Okular::Settings::self()->setSlidesCursor( Okular::Settings::EnumSlidesCursor::Hidden ); qApp->notify( qApp, &enterProximityEvent ); QVERIFY( w->cursor().shape() == Qt::CursorShape( Qt::CrossCursor ) ); qApp->notify( qApp, &leaveProximityEvent ); QVERIFY( w->cursor().shape() == Qt::CursorShape( Qt::BlankCursor ) ); // Moving the mouse should not bring the cursor back QTest::mouseMove( w, QPoint( 100, 100 ) ); QVERIFY( w->cursor().shape() == Qt::CursorShape( Qt::BlankCursor ) ); // First test with the Okular::Settings::EnumSlidesCursor::HiddenDelay setting Okular::Settings::self()->setSlidesCursor( Okular::Settings::EnumSlidesCursor::HiddenDelay ); qApp->notify( qApp, &enterProximityEvent ); QVERIFY( w->cursor().shape() == Qt::CursorShape( Qt::CrossCursor ) ); qApp->notify( qApp, &leaveProximityEvent ); // After the leaveProximityEvent, the cursor should be blank, because // we have set the slidesCursor mode to 'HiddenDelay' QVERIFY( w->cursor().shape() == Qt::CursorShape( Qt::BlankCursor ) ); // Moving the mouse should bring the cursor back QTest::mouseMove(w, QPoint( 150, 150 )); QVERIFY( w->cursor().shape() == Qt::CursorShape( Qt::ArrowCursor ) ); } void PartTest::testOpenPrintPreview() { QVariantList dummyArgs; Okular::Part part{ nullptr, nullptr, dummyArgs }; QVERIFY( openDocument( &part, QStringLiteral( KDESRCDIR "data/file1.pdf" ) ) ); part.widget()->show(); QVERIFY(QTest::qWaitForWindowExposed(part.widget())); TestingUtils::CloseDialogHelper closeDialogHelper( QDialogButtonBox::Close ); part.slotPrintPreview(); } } // namespace Okular int main(int argc, char *argv[]) { // Force consistent locale QLocale locale(QStringLiteral("en_US.UTF-8")); if (locale == QLocale::c()) { // This is the way to check if the above worked locale = QLocale(QLocale::English, QLocale::UnitedStates); } QLocale::setDefault(locale); qputenv("LC_ALL", "en_US.UTF-8"); // For UNIX, third-party libraries // Ensure consistent configs/caches QTemporaryDir homeDir; // QTemporaryDir automatically cleans up when it goes out of scope Q_ASSERT(homeDir.isValid()); QByteArray homePath = QFile::encodeName(homeDir.path()); qDebug() << homePath; qputenv("USERPROFILE", homePath); qputenv("HOME", homePath); qputenv("XDG_DATA_HOME", homePath + "/.local"); qputenv("XDG_CONFIG_HOME", homePath + "/.kde-unit-test/xdg/config"); // Disable fancy debug output qunsetenv("QT_MESSAGE_PATTERN"); QApplication app( argc, argv ); app.setApplicationName(QStringLiteral("okularparttest")); app.setOrganizationDomain(QStringLiteral("kde.org")); app.setQuitOnLastWindowClosed(false); qRegisterMetaType(); /*as done by kapplication*/ qRegisterMetaType>(); Okular::PartTest test; return QTest::qExec( &test, argc, argv ); } #include "parttest.moc" diff --git a/generators/chm/autotests/chmgeneratortest.cpp b/generators/chm/autotests/chmgeneratortest.cpp index 6b0b8efc7..bb3e90ec4 100644 --- a/generators/chm/autotests/chmgeneratortest.cpp +++ b/generators/chm/autotests/chmgeneratortest.cpp @@ -1,104 +1,104 @@ /*************************************************************************** * Copyright (C) 2017 by Gilbert Assaf * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * ***************************************************************************/ #include #include "core/document.h" #include "core/page.h" #include "settings_core.h" #include "core/textpage.h" class ChmGeneratorTest : public QObject { Q_OBJECT private slots: void initTestCase(); void testDocumentStructure(); void testDocumentContent(); void cleanupTestCase(); private: Okular::Document *m_document; }; void ChmGeneratorTest::initTestCase() { Okular::SettingsCore::instance( QStringLiteral("ChmGeneratorTest") ); - m_document = new Okular::Document( 0 ); + m_document = new Okular::Document( nullptr ); const QString testFile = QStringLiteral(KDESRCDIR "autotests/data/test.chm"); QMimeDatabase db; const QMimeType mime = db.mimeTypeForFile( testFile ); QCOMPARE( m_document->openDocument(testFile, QUrl(), mime), Okular::Document::OpenSuccess ); } void ChmGeneratorTest::cleanupTestCase() { m_document->closeDocument(); delete m_document; } void ChmGeneratorTest::testDocumentStructure() { unsigned int expectedPageNr = 6; QCOMPARE( m_document->pages(), expectedPageNr); QCOMPARE( m_document->metaData(QLatin1String("DocumentTitle")).toString(), QStringLiteral("okular test chm") ); const Okular::DocumentSynopsis *docSyn = m_document->documentSynopsis(); QDomElement heading1 = docSyn->documentElement(); QCOMPARE( heading1.tagName(), QStringLiteral("Heading 1") ); QDomElement topic1 = heading1.firstChildElement(); QCOMPARE( topic1.tagName(), QStringLiteral("Topic 1") ); QDomElement heading1_1 = topic1.nextSiblingElement(); QCOMPARE( heading1_1.tagName(), QStringLiteral("Heading 1.1") ); QDomElement topic1_1 = heading1_1.firstChildElement(); QCOMPARE( topic1_1.tagName(), QStringLiteral("Topic 1.1") ); QDomElement heading2 = heading1.nextSiblingElement(); QCOMPARE( heading2.tagName(), QStringLiteral("Heading 2") ); } void ChmGeneratorTest::testDocumentContent() { const Okular::Page *page0 = m_document->page(0); QCOMPARE( page0->number(), 0); m_document->requestTextPage( page0->number() ); QVERIFY( page0->hasTextPage() ); QCOMPARE( page0->text(), QStringLiteral("Heading 1This is an example Text.") ); const Okular::Page *page1 = m_document->page(1); QCOMPARE( page1->number(), 1); m_document->requestTextPage( page1->number() ); QVERIFY( page1->hasTextPage() ); QCOMPARE( page1->text(), QStringLiteral("Topic 1This is an example Text.") ); const Okular::Page *page2 = m_document->page(2); QCOMPARE( page2->number(), 2); m_document->requestTextPage( page2->number() ); QVERIFY( page2->hasTextPage() ); QCOMPARE( page2->text(), QStringLiteral("Heading 1.1With html title.") ); //Test page, who doesn't have an TOC entry, but one in chm index const Okular::Page *indexPage1 = m_document->page(5); QCOMPARE( indexPage1->number(), 5); m_document->requestTextPage( indexPage1->number() ); QVERIFY( indexPage1->hasTextPage() ); QCOMPARE( indexPage1->text(), QStringLiteral("Index 1This is an example Text.") ); } QTEST_MAIN( ChmGeneratorTest ) #include "chmgeneratortest.moc" /* kate: replace-tabs on; tab-width 4; */ diff --git a/generators/chm/generator_chm.cpp b/generators/chm/generator_chm.cpp index 3b419ce71..9e432fc0c 100644 --- a/generators/chm/generator_chm.cpp +++ b/generators/chm/generator_chm.cpp @@ -1,449 +1,449 @@ /*************************************************************************** * Copyright (C) 2005 by Piotr Szymański * * Copyright (C) 2008 by Albert Astals Cid * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * ***************************************************************************/ #include "generator_chm.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include OKULAR_EXPORT_PLUGIN(CHMGenerator, "libokularGenerator_chmlib.json") static QString absolutePath( const QString &baseUrl, const QString &path ) { QString absPath; if ( path.startsWith(QLatin1Char( '/' )) ) { // already absolute absPath = path; } else { QUrl url = QUrl::fromLocalFile( baseUrl ).adjusted(QUrl::RemoveFilename); url.setPath( url.path() + path ); absPath = url.toLocalFile(); } return absPath; } CHMGenerator::CHMGenerator( QObject *parent, const QVariantList &args ) : Okular::Generator( parent, args ) { setFeature( TextExtraction ); - m_syncGen=0; - m_file=0; - m_request = 0; + m_syncGen=nullptr; + m_file=nullptr; + m_request = nullptr; } CHMGenerator::~CHMGenerator() { delete m_syncGen; } bool CHMGenerator::loadDocument( const QString & fileName, QVector< Okular::Page * > & pagesVector ) { m_file = new EBook_CHM(); m_file = EBook::loadFile( fileName ); if (!m_file) { return false; } m_fileName=fileName; QList< EBookTocEntry > topics; m_file->getTableOfContents(topics); // fill m_docSyn QMap lastIndentElement; QMap tmpPageList; int pageNum = 0; for (const EBookTocEntry &e : qAsConst(topics)) { QDomElement item = m_docSyn.createElement(e.name); if (!e.url.isEmpty()) { QString url = e.url.toString(); item.setAttribute(QStringLiteral("ViewportName"), url); if(!tmpPageList.contains(url)) {//add a page only once tmpPageList.insert(url, pageNum); pageNum++; } } item.setAttribute(QStringLiteral("Icon"), e.iconid); if (e.indent == 0) m_docSyn.appendChild(item); else lastIndentElement[e.indent - 1].appendChild(item); lastIndentElement[e.indent] = item; } // fill m_urlPage and m_pageUrl QList pageList; m_file->enumerateFiles(pageList); const QUrl home = m_file->homeUrl(); if (home.path() != QLatin1String("/")) pageList.prepend(home); m_pageUrl.resize(pageNum); for (const QUrl &qurl : qAsConst(pageList)) { QString url = qurl.toString(); const QString urlLower = url.toLower(); if (!urlLower.endsWith(QLatin1String(".html")) && !urlLower.endsWith(QLatin1String(".htm"))) continue; int pos = url.indexOf (QLatin1Char(('#'))); // insert the url into the maps, but insert always the variant without the #ref part QString tmpUrl = pos == -1 ? url : url.left(pos); // url already there, abort insertion if (m_urlPage.contains(tmpUrl)) continue; int foundPage = tmpPageList.value(tmpUrl, -1); if (foundPage != -1 ) { m_urlPage.insert(tmpUrl, foundPage); m_pageUrl[foundPage] = tmpUrl; } else { //add pages not present in toc m_urlPage.insert(tmpUrl, pageNum); m_pageUrl.append(tmpUrl); pageNum++; } } pagesVector.resize(m_pageUrl.count()); m_textpageAddedList.fill(false, pagesVector.count()); m_rectsGenerated.fill(false, pagesVector.count()); if (!m_syncGen) { m_syncGen = new KHTMLPart(); } - disconnect( m_syncGen, 0, this, 0 ); + disconnect( m_syncGen, nullptr, this, nullptr ); for (int i = 0; i < m_pageUrl.count(); ++i) { preparePageForSyncOperation(m_pageUrl.at(i)); pagesVector[ i ] = new Okular::Page (i, m_syncGen->view()->contentsWidth(), m_syncGen->view()->contentsHeight(), Okular::Rotation0 ); } connect( m_syncGen, SIGNAL(completed()), this, SLOT(slotCompleted()) ); connect( m_syncGen, &KParts::ReadOnlyPart::canceled, this, &CHMGenerator::slotCompleted ); return true; } bool CHMGenerator::doCloseDocument() { // delete the document information of the old document delete m_file; - m_file=0; + m_file=nullptr; m_textpageAddedList.clear(); m_rectsGenerated.clear(); m_urlPage.clear(); m_pageUrl.clear(); m_docSyn.clear(); if (m_syncGen) { m_syncGen->closeUrl(); } return true; } void CHMGenerator::preparePageForSyncOperation(const QString & url) { QString pAddress = QStringLiteral("ms-its:") + m_fileName + QStringLiteral("::") + m_file->urlToPath(QUrl(url)); m_chmUrl = url; m_syncGen->openUrl(QUrl(pAddress)); m_syncGen->view()->layout(); QEventLoop loop; connect( m_syncGen, SIGNAL(completed()), &loop, SLOT(quit()) ); connect( m_syncGen, &KParts::ReadOnlyPart::canceled, &loop, &QEventLoop::quit ); // discard any user input, otherwise it breaks the "synchronicity" of this // function loop.exec( QEventLoop::ExcludeUserInputEvents ); } void CHMGenerator::slotCompleted() { if ( !m_request ) return; QImage image( m_request->width(), m_request->height(), QImage::Format_ARGB32 ); image.fill( Qt::white ); QPainter p( &image ); QRect r( 0, 0, m_request->width(), m_request->height() ); bool moreToPaint; m_syncGen->paint( &p, r, 0, &moreToPaint ); p.end(); if ( !m_textpageAddedList.at( m_request->pageNumber() ) ) { additionalRequestData(); m_textpageAddedList[ m_request->pageNumber() ] = true; } m_syncGen->closeUrl(); m_chmUrl = QString(); userMutex()->unlock(); Okular::PixmapRequest *req = m_request; - m_request = 0; + m_request = nullptr; if ( !req->page()->isBoundingBoxKnown() ) updatePageBoundingBox( req->page()->number(), Okular::Utils::imageBoundingBox( &image ) ); req->page()->setPixmap( req->observer(), new QPixmap( QPixmap::fromImage( image ) ) ); signalPixmapRequestDone( req ); } Okular::DocumentInfo CHMGenerator::generateDocumentInfo( const QSet &keys ) const { Okular::DocumentInfo docInfo; if ( keys.contains( Okular::DocumentInfo::MimeType ) ) docInfo.set( Okular::DocumentInfo::MimeType, QStringLiteral("application/x-chm") ); if ( keys.contains( Okular::DocumentInfo::Title ) ) docInfo.set( Okular::DocumentInfo::Title, m_file->title() ); return docInfo; } const Okular::DocumentSynopsis * CHMGenerator::generateDocumentSynopsis() { return &m_docSyn; } bool CHMGenerator::canGeneratePixmap () const { bool isLocked = true; if ( userMutex()->tryLock() ) { userMutex()->unlock(); isLocked = false; } return !isLocked; } void CHMGenerator::generatePixmap( Okular::PixmapRequest * request ) { int requestWidth = request->width(); int requestHeight = request->height(); userMutex()->lock(); QString url= m_pageUrl[request->pageNumber()]; QString pAddress= QStringLiteral("ms-its:") + m_fileName + QStringLiteral("::") + m_file->urlToPath(QUrl(url)); m_chmUrl = url; m_syncGen->view()->resizeContents(requestWidth,requestHeight); m_request=request; // will emit openURL without problems m_syncGen->openUrl ( QUrl(pAddress) ); } void CHMGenerator::recursiveExploreNodes(DOM::Node node,Okular::TextPage *tp) { if (node.nodeType() == DOM::Node::TEXT_NODE && !node.getRect().isNull()) { QString nodeText=node.nodeValue().string(); QRect r=node.getRect(); int vWidth=m_syncGen->view()->width(); int vHeight=m_syncGen->view()->height(); Okular::NormalizedRect *nodeNormRect; #define NOEXP #ifndef NOEXP int x,y,height; int x_next,y_next,height_next; int nodeTextLength = nodeText.length(); if (nodeTextLength==1) { nodeNormRect=new Okular::NormalizedRect (r,vWidth,vHeight); tp->append(nodeText,nodeNormRect,nodeNormRect->bottom,0,(nodeText=="\n")); } else { for (int i=0;iappend(nodeText,nodeNormRect/*,0*/); #endif } DOM::Node child = node.firstChild(); while ( !child.isNull() ) { recursiveExploreNodes(child,tp); child = child.nextSibling(); } } void CHMGenerator::additionalRequestData() { Okular::Page * page=m_request->page(); const bool genObjectRects = !m_rectsGenerated.at( m_request->page()->number() ); const bool genTextPage = !m_request->page()->hasTextPage() && genObjectRects; if (genObjectRects || genTextPage ) { DOM::HTMLDocument domDoc=m_syncGen->htmlDocument(); // only generate object info when generating a full page not a thumbnail if ( genObjectRects ) { QLinkedList< Okular::ObjectRect * > objRects; int xScale=m_syncGen->view()->width(); int yScale=m_syncGen->view()->height(); // getting links DOM::HTMLCollection coll=domDoc.links(); DOM::Node n; QRect r; if (! coll.isNull() ) { int size=coll.length(); for(int i=0;ipage()->setObjectRects( objRects ); m_rectsGenerated[ m_request->page()->number() ] = true; } if ( genTextPage ) { Okular::TextPage *tp=new Okular::TextPage(); recursiveExploreNodes(domDoc,tp); page->setTextPage (tp); } } } Okular::TextPage* CHMGenerator::textPage( Okular::TextRequest * request ) { userMutex()->lock(); const Okular::Page *page = request->page(); m_syncGen->view()->resize(page->width(), page->height()); preparePageForSyncOperation(m_pageUrl[page->number()]); Okular::TextPage *tp=new Okular::TextPage(); recursiveExploreNodes( m_syncGen->htmlDocument(), tp); userMutex()->unlock(); return tp; } QVariant CHMGenerator::metaData( const QString &key, const QVariant &option ) const { if ( key == QLatin1String("NamedViewport") && !option.toString().isEmpty() ) { const int pos = option.toString().indexOf(QLatin1Char('#')); QString tmpUrl = pos == -1 ? option.toString() : option.toString().left(pos); Okular::DocumentViewport viewport; QMap::const_iterator it = m_urlPage.find(tmpUrl); if (it != m_urlPage.end()) { viewport.pageNumber = it.value(); return viewport.toString(); } } else if ( key == QLatin1String("DocumentTitle") ) { return m_file->title(); } return QVariant(); } /* kate: replace-tabs on; tab-width 4; */ #include "generator_chm.moc" diff --git a/generators/chm/kio-msits/msits.cpp b/generators/chm/kio-msits/msits.cpp index 1e9fea64c..4e0620ebd 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; + m_chmFile = nullptr; } ProtocolMSITS::~ProtocolMSITS() { if ( !m_chmFile ) return; chm_close (m_chmFile); - m_chmFile = 0; + m_chmFile = nullptr; } // 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 ) + if( (tmpchm = chm_open ( QFile::encodeName( QDir::toNativeSeparators( filename) ).constData() ) ) == nullptr ) { 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 ( const QString &iListing : qAsConst(listing) ) { // Strip the directory name 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/kio-msits/msits.h b/generators/chm/kio-msits/msits.h index febbd153b..9e02cf878 100644 --- a/generators/chm/kio-msits/msits.h +++ b/generators/chm/kio-msits/msits.h @@ -1,71 +1,71 @@ /*************************************************************************** * Copyright (C) 2005 by Georgy Yunaev * * tim@krasnogorsk.ru * * * * 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 MSITS_H #define MSITS_H #include #include #include #include #include "chm_lib.h" class ProtocolMSITS : public KIO::SlaveBase { public: ProtocolMSITS ( const QByteArray&, const QByteArray& ); ~ProtocolMSITS() override; void get (const QUrl & ) override; void listDir (const QUrl & url) override; void stat (const QUrl & url) override; private: // This function does next thing: // - parses the URL to get a file name and URL inside the file; // - loads a new CHM file, if needed; // - returns the parsed URL inside the file; bool parseLoadAndLookup (const QUrl &, QString& abspath ); // Resolve an object inside a CHM file inline bool ResolveObject (const QString& fileName, chmUnitInfo *ui) { - return m_chmFile != NULL && ::chm_resolve_object(m_chmFile, fileName.toUtf8().constData(), ui) == CHM_RESOLVE_SUCCESS; + return m_chmFile != nullptr && ::chm_resolve_object(m_chmFile, fileName.toUtf8().constData(), ui) == CHM_RESOLVE_SUCCESS; } // Retrieve an object from the CHM file inline size_t RetrieveObject (chmUnitInfo *ui, unsigned char *buffer, LONGUINT64 fileOffset, LONGINT64 bufferSize) { return ::chm_retrieve_object(m_chmFile, ui, buffer, fileOffset, bufferSize); } // An opened file name, if presend QString m_openedFile; // a CHM structure file pointer (from chmlib) chmFile * m_chmFile; }; #endif /* MSITS_H */ diff --git a/generators/chm/lib/ebook.cpp b/generators/chm/lib/ebook.cpp index c733bd272..d0044a8b5 100644 --- a/generators/chm/lib/ebook.cpp +++ b/generators/chm/lib/ebook.cpp @@ -1,50 +1,50 @@ /* * 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 "ebook.h" #include "ebook_chm.h" #include "ebook_epub.h" EBook::EBook() { } EBook::~EBook() { } EBook * EBook::loadFile( const QString &archiveName ) { EBook_CHM * cbook = new EBook_CHM(); if ( cbook->load( archiveName ) ) return cbook; delete cbook; EBook_EPUB * ebook = new EBook_EPUB(); if ( ebook->load( archiveName ) ) return ebook; delete ebook; - return 0; + return nullptr; } diff --git a/generators/chm/lib/ebook_chm.cpp b/generators/chm/lib/ebook_chm.cpp index 6c50cb1fd..948de2e26 100644 --- a/generators/chm/lib/ebook_chm.cpp +++ b/generators/chm/lib/ebook_chm.cpp @@ -1,1113 +1,1113 @@ /* * 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 #include "ebook_chm.h" #include "ebook_chm_encoding.h" #include "helper_entitydecoder.h" #include "bitfiddle.h" // Big-enough buffer size for use with various routines. #define BUF_SIZE 4096 #define COMMON_BUF_LEN 1025 #define TOPICS_ENTRY_LEN 16 #define URLTBL_ENTRY_LEN 12 //#define DEBUGPARSER(A) qDebug A #define DEBUGPARSER(A) static const char * URL_SCHEME_CHM = "ms-its"; EBook_CHM::EBook_CHM() : EBook() { m_envOptions = getenv("KCHMVIEWEROPTS"); - m_chmFile = NULL; + m_chmFile = nullptr; m_filename = m_font = QString(); - m_textCodec = 0; - m_textCodecForSpecialFiles = 0; + m_textCodec = nullptr; + m_textCodecForSpecialFiles = nullptr; m_detectedLCID = 0; m_currentEncoding = "UTF-8"; - m_htmlEntityDecoder = 0; + m_htmlEntityDecoder = nullptr; } EBook_CHM::~EBook_CHM() { close(); } void EBook_CHM::close() { - if ( m_chmFile == NULL ) + if ( m_chmFile == nullptr ) return; chm_close( m_chmFile ); - m_chmFile = NULL; + m_chmFile = nullptr; m_filename = m_font = QString(); m_home.clear(); m_topicsFile.clear(); m_indexFile.clear(); - m_textCodec = 0; - m_textCodecForSpecialFiles = 0; + m_textCodec = nullptr; + m_textCodecForSpecialFiles = nullptr; m_detectedLCID = 0; m_currentEncoding = "UTF-8"; } QString EBook_CHM::title() const { return encodeWithCurrentCodec( m_title ); } QUrl EBook_CHM::homeUrl() const { return pathToUrl( m_home ); } bool EBook_CHM::hasFeature(EBook::Feature code) const { switch ( code ) { case FEATURE_TOC: return m_tocAvailable; case FEATURE_INDEX: return m_indexAvailable; case FEATURE_ENCODING: return true; } return false; } bool EBook_CHM::getTableOfContents( QList &toc ) const { if ( parseBinaryTOC( toc ) ) return true; // Parse the plain text TOC QList< ParsedEntry > parsed; if ( !parseFileAndFillArray( m_topicsFile, parsed, false ) ) return false; // Find out the root offset, and reduce the indent level to it // so the toc starts from zero offset. int root_offset = -1; // Fill up the real toc toc.reserve( parsed.size() ); for ( const ParsedEntry &e : qAsConst(parsed) ) { if ( root_offset == -1 ) root_offset = e.indent; EBookTocEntry entry; entry.iconid = (EBookTocEntry::Icon) e.iconid; entry.indent = e.indent - root_offset; entry.name = e.name; if ( !e.urls.empty() ) entry.url = e.urls[0]; toc.append( entry ); } return true; } bool EBook_CHM::getIndex(QList &index) const { // Parse the plain text index QList< ParsedEntry > parsed; if ( !parseFileAndFillArray( m_indexFile, parsed, true ) ) return false; // Find out the root offset, and reduce the indent level to it // so the index starts from zero offset. int root_offset = 0; // Fill up the real index index.reserve( parsed.size() ); // Find the index root offset const QList< ParsedEntry > &parsedList = parsed; for ( const ParsedEntry &e : parsedList ) { if ( e.urls.empty() ) continue; root_offset = qMin( root_offset, e.indent ); } // And apply the index for( const ParsedEntry &e : parsedList ) { if ( e.urls.empty() ) continue; EBookIndexEntry entry; entry.name = e.name; entry.urls = e.urls; entry.seealso = e.seealso; // If the index array is empty, make sure the first entry is on root offset if ( index.isEmpty() ) entry.indent = root_offset; else entry.indent = e.indent - root_offset; index.append( entry ); printf("%d: %s\n", entry.indent, qPrintable(entry.name)); } return true; } bool EBook_CHM::getFileContentAsString( QString &str, const QUrl &url ) const { return getTextContent( str, urlToPath( url ) ); } bool EBook_CHM::getFileContentAsBinary( QByteArray &data, const QUrl &url ) const { return getBinaryContent( data, urlToPath(url) ); } bool EBook_CHM::getBinaryContent( QByteArray &data, const QString &url ) const { chmUnitInfo ui; if( !ResolveObject( url, &ui ) ) return false; data.resize( ui.length ); if ( RetrieveObject( &ui, (unsigned char*) data.data(), 0, ui.length ) ) return true; return false; } bool EBook_CHM::getTextContent( QString& str, const QString& url, bool internal_encoding ) const { QByteArray buf; if ( getBinaryContent( buf, url ) ) { unsigned int length = buf.size(); if ( length > 0 ) { buf.resize( length + 1 ); buf [length] = '\0'; str = internal_encoding ? (QString)( buf.constData() ) : encodeWithCurrentCodec( buf.constData() ); return true; } } return false; } int EBook_CHM::getContentSize(const QString &url) { chmUnitInfo ui; if( !ResolveObject( url, &ui ) ) return -1; return ui.length; } bool EBook_CHM::load(const QString &archiveName) { QString filename; // If the file has a file:// prefix, remove it if ( archiveName.startsWith( "file://" ) ) filename = archiveName.mid( 7 ); // strip it else filename = archiveName; if( m_chmFile ) close(); #if defined (WIN32) // chm_open on Windows OS uses the following prototype: // struct chmFile* chm_open(BSTR filename); // // however internally it simply passes the filename // directly to CreateFileW function without any conversion. // Thus we need to pass it as WCHAR * and not BSTR. m_chmFile = chm_open( (BSTR) filename.toStdWString().c_str() ); #else m_chmFile = chm_open( QFile::encodeName(filename) ); #endif - if ( m_chmFile == NULL ) + if ( m_chmFile == nullptr ) return false; m_filename = filename; // Reset encoding - m_textCodec = 0; - m_textCodecForSpecialFiles = 0; + m_textCodec = nullptr; + m_textCodecForSpecialFiles = nullptr; m_currentEncoding = "UTF-8"; // Get information from /#WINDOWS and /#SYSTEM files (encoding, title, context file and so) // and guess the encoding getInfoFromWindows(); getInfoFromSystem(); guessTextEncoding(); // Check whether the search tables are present if ( ResolveObject("/#TOPICS", &m_chmTOPICS) && ResolveObject("/#STRINGS", &m_chmSTRINGS) && ResolveObject("/#URLTBL", &m_chmURLTBL) && ResolveObject("/#URLSTR", &m_chmURLSTR) ) { m_lookupTablesValid = true; fillTopicsUrlMap(); } else m_lookupTablesValid = false; // Some CHM files have toc and index files, but do not set the name properly. // Some heuristics here. if ( m_topicsFile.isEmpty() && hasFile( "/toc.hhc" ) ) m_topicsFile = "/toc.hhc"; if ( m_indexFile.isEmpty() && hasFile( "/index.hhk" ) ) m_indexFile = "/index.hhk"; if ( !m_topicsFile.isEmpty() || ( m_lookupTablesValid && hasFile( "/#TOCIDX" ) ) ) m_tocAvailable = true; else m_tocAvailable = false; if ( !m_indexFile.isEmpty() || ( m_lookupTablesValid && hasFile( "/$WWKeywordLinks/BTree" ) ) ) m_indexAvailable = true; else m_indexAvailable = false; return true; } int EBook_CHM::findStringInQuotes (const QString& tag, int offset, QString& value, bool firstquote, bool decodeentities) const { int qbegin = tag.indexOf ('"', offset); if ( qbegin == -1 ) qFatal ("EBook_CHMImpl::findStringInQuotes: cannot find first quote in tag: '%s'", qPrintable( tag )); int qend = firstquote ? tag.indexOf ('"', qbegin + 1) : tag.lastIndexOf ('"'); if ( qend == -1 || qend <= qbegin ) qFatal ("EBook_CHMImpl::findStringInQuotes: cannot find last quote in tag: '%s'", qPrintable( tag )); // If we do not need to decode HTML entities, just return. if ( decodeentities ) { QString htmlentity = QString(); bool fill_entity = false; value.reserve (qend - qbegin); // to avoid multiple memory allocations for ( int i = qbegin + 1; i < qend; i++ ) { if ( !fill_entity ) { if ( tag[i] == '&' ) // HTML entity starts fill_entity = true; else value.append (tag[i]); } else { if ( tag[i] == ';' ) // HTML entity ends { // If entity is an ASCII code, just decode it QString decode = m_htmlEntityDecoder.decode( htmlentity ); if ( decode.isNull() ) break; value.append ( decode ); htmlentity = QString(); fill_entity = false; } else htmlentity.append (tag[i]); } } } else value = tag.mid (qbegin + 1, qend - qbegin - 1); return qend + 1; } bool EBook_CHM::parseFileAndFillArray( const QString& file, QList< ParsedEntry >& data, bool asIndex ) const { QString src; const int MAX_NEST_DEPTH = 256; if ( !getTextContent( src, file ) || src.isEmpty() ) return false; /* // Save the index for debugging purposes QFile outfile( "parsed.htm" ); if ( outfile.open( QIODevice::WriteOnly ) ) { QTextStream textstream( &outfile ); textstream << src; outfile.close(); } */ EBookTocEntry::Icon defaultimagenum = EBookTocEntry::IMAGE_AUTO; int pos = 0, indent = 0, root_indent_offset = 0; bool in_object = false, root_indent_offset_set = false; ParsedEntry entry; entry.iconid = defaultimagenum; // Split the HHC file by HTML tags int stringlen = src.length(); while ( pos < stringlen && (pos = src.indexOf ('<', pos)) != -1 ) { int i, word_end = 0; for ( i = ++pos; i < stringlen; i++ ) { // If a " or ' is found, skip to the next one. if ( (src[i] == '"' || src[i] == '\'') ) { // find where quote ends, either by another quote, or by '>' symbol (some people don't know HTML) int nextpos = src.indexOf (src[i], i+1); if ( nextpos == -1 && (nextpos = src.indexOf ('>', i+1)) == -1 ) { qWarning ("EBook_CHMImpl::ParseHhcAndFillTree: corrupted TOC: %s", qPrintable( src.mid(i) )); return false; } i = nextpos; } else if ( src[i] == '>' ) break; else if ( !src[i].isLetterOrNumber() && src[i] != '/' && !word_end ) word_end = i; } QString tagword, tag = src.mid (pos, i - pos); if ( word_end ) tagword = src.mid (pos, word_end - pos).toLower(); else tagword = tag.toLower(); //DEBUGPARSER(("tag: '%s', tagword: '%s'\n", qPrintable( tag ), qPrintable( tagword ) )); // - a topic entry if ( tagword == "object" && tag.indexOf ("text/sitemap", 0, Qt::CaseInsensitive ) != -1 ) in_object = true; else if ( tagword == "/object" && in_object ) { // a topic entry closed. Add a tree item if ( entry.name.isEmpty() && entry.urls.isEmpty() ) { qWarning ("EBook_CHMImpl::ParseAndFillTopicsTree: tag is parsed, but both name and url are empty."); } else { // If the name is empty, use the URL as name if ( entry.name.isEmpty() ) entry.name = entry.urls[0].toString(); if ( !root_indent_offset_set ) { root_indent_offset_set = true; root_indent_offset = indent; if ( root_indent_offset > 1 ) qWarning("CHM has improper index; root indent offset is %d", root_indent_offset); } // Trim the entry name entry.name = entry.name.trimmed(); int real_indent = indent - root_indent_offset; entry.indent = real_indent; data.push_back( entry ); } entry.name = QString(); entry.urls.clear(); entry.iconid = defaultimagenum; entry.seealso.clear(); in_object = false; } else if ( tagword == "param" && in_object ) { // int offset; // strlen("param ") QString name_pattern = "name=", value_pattern = "value="; QString pname, pvalue; if ( (offset = tag.indexOf (name_pattern, 0, Qt::CaseInsensitive )) == -1 ) qFatal ("EBook_CHMImpl::ParseAndFillTopicsTree: bad tag '%s': no name=\n", qPrintable( tag )); // offset+5 skips 'name=' offset = findStringInQuotes (tag, offset + name_pattern.length(), pname, true, false); pname = pname.toLower(); if ( (offset = tag.indexOf(value_pattern, offset, Qt::CaseInsensitive )) == -1 ) qFatal ("EBook_CHMImpl::ParseAndFillTopicsTree: bad tag '%s': no value=\n", qPrintable( tag )); // offset+6 skips 'value=' findStringInQuotes (tag, offset + value_pattern.length(), pvalue, false, true); //DEBUGPARSER((": name '%s', value '%s'", qPrintable( pname ), qPrintable( pvalue ))); if ( pname == "name" || pname == "keyword" ) { // Some help files contain duplicate names, where the second name is empty. Work it around by keeping the first one if ( !pvalue.isEmpty() ) entry.name = pvalue; } else if ( pname == "merge" ) { // MERGE implementation is experimental QUrl mergeurl = pathToUrl( pvalue ); QString mergecontent; if ( getFileContentAsString( mergecontent, mergeurl ) && !mergecontent.isEmpty() ) { qWarning( "MERGE is used in index; the implementation is experimental. Please let me know if it works" ); // Merge the read value into the current parsed file. // To save memory it is done in a kinda hacky way: src = mergecontent + src.mid( i ); pos = 0; stringlen = src.length(); } else qWarning( "MERGE is used in index but file %s was not found in CHM archive", qPrintable(pvalue) ); } else if ( pname == "local" ) { // Check for URL duplication QUrl url = pathToUrl( pvalue ); if ( !entry.urls.contains( url ) ) entry.urls.push_back( url ); } else if ( pname == "see also" && asIndex && entry.name != pvalue ) { entry.urls.push_back( QUrl("seealso") ); entry.seealso = pvalue; } else if ( pname == "imagenumber" ) { bool bok; int imgnum = pvalue.toInt (&bok); if ( bok && imgnum >= 0 && imgnum < EBookTocEntry::MAX_BUILTIN_ICONS ) entry.iconid = (EBookTocEntry::Icon) imgnum; } } else if ( tagword == "ul" ) // increase indent level { // Fix for buggy help files if ( ++indent >= MAX_NEST_DEPTH ) qFatal("EBook_CHMImpl::ParseAndFillTopicsTree: max nest depth (%d) is reached, error in help file", MAX_NEST_DEPTH); DEBUGPARSER(("
    : new intent is %d\n", indent - root_indent_offset)); } else if ( tagword == "/ul" ) // decrease indent level { if ( --indent < root_indent_offset ) indent = root_indent_offset; DEBUGPARSER(("
: new intent is %d\n", indent - root_indent_offset)); } pos = i; } // Dump our array // for ( int i = 0; i < data.size(); i++ ) // qDebug() << data[i].indent << data[i].name << data[i].urls; return true; } bool EBook_CHM::ResolveObject(const QString& fileName, chmUnitInfo *ui) const { - return m_chmFile != NULL + return m_chmFile != nullptr && ::chm_resolve_object(m_chmFile, qPrintable( fileName ), ui) == CHM_RESOLVE_SUCCESS; } bool EBook_CHM::hasFile(const QString & fileName) const { chmUnitInfo ui; - return m_chmFile != NULL + return m_chmFile != nullptr && ::chm_resolve_object(m_chmFile, qPrintable( fileName ), &ui) == CHM_RESOLVE_SUCCESS; } size_t EBook_CHM::RetrieveObject(const chmUnitInfo *ui, unsigned char *buffer, LONGUINT64 fileOffset, LONGINT64 bufferSize) const { return ::chm_retrieve_object(m_chmFile, const_cast(ui), buffer, fileOffset, bufferSize); } bool EBook_CHM::getInfoFromWindows() { #define WIN_HEADER_LEN 0x08 unsigned char buffer[BUF_SIZE]; unsigned int factor; chmUnitInfo ui; long size = 0; if ( ResolveObject("/#WINDOWS", &ui) ) { if ( !RetrieveObject(&ui, buffer, 0, WIN_HEADER_LEN) ) return false; unsigned int entries = get_int32_le( (unsigned int *)(buffer) ); unsigned int entry_size = get_int32_le( (unsigned int *)(buffer + 0x04) ); QVector uptr(entries * entry_size); unsigned char* raw = (unsigned char*) uptr.data(); if ( !RetrieveObject (&ui, raw, 8, entries * entry_size) ) return false; if( !ResolveObject ("/#STRINGS", &ui) ) return false; for ( unsigned int i = 0; i < entries; ++i ) { unsigned int offset = i * entry_size; unsigned int off_title = get_int32_le( (unsigned int *)(raw + offset + 0x14) ); unsigned int off_home = get_int32_le( (unsigned int *)(raw + offset + 0x68) ); unsigned int off_hhc = get_int32_le( (unsigned int *)(raw + offset + 0x60) ); unsigned int off_hhk = get_int32_le( (unsigned int *)(raw + offset + 0x64) ); factor = off_title / 4096; if ( size == 0 ) size = RetrieveObject(&ui, buffer, factor * 4096, BUF_SIZE); if ( size && off_title ) m_title = QByteArray( (const char*) (buffer + off_title % 4096) ); if ( factor != off_home / 4096) { factor = off_home / 4096; size = RetrieveObject (&ui, buffer, factor * 4096, BUF_SIZE); } if ( size && off_home ) m_home = QByteArray("/") + QByteArray( (const char*) buffer + off_home % 4096); if ( factor != off_hhc / 4096) { factor = off_hhc / 4096; size = RetrieveObject(&ui, buffer, factor * 4096, BUF_SIZE); } if ( size && off_hhc ) m_topicsFile = QByteArray("/") + QByteArray((const char*) buffer + off_hhc % 4096); if ( factor != off_hhk / 4096) { factor = off_hhk / 4096; size = RetrieveObject (&ui, buffer, factor * 4096, BUF_SIZE); } if ( size && off_hhk ) m_indexFile = QByteArray("/") + QByteArray((const char*) buffer + off_hhk % 4096); } } return true; } bool EBook_CHM::getInfoFromSystem() { unsigned char buffer[BUF_SIZE]; chmUnitInfo ui; int index = 0; - unsigned char* cursor = NULL, *p; + unsigned char* cursor = nullptr, *p; unsigned short value = 0; long size = 0; // Run the first loop to detect the encoding. We need this, because title could be // already encoded in user encoding. Same for file names if ( !ResolveObject ("/#SYSTEM", &ui) ) return false; // Can we pull BUFF_SIZE bytes of the #SYSTEM file? if ( (size = RetrieveObject (&ui, buffer, 4, BUF_SIZE)) == 0 ) return false; buffer[size - 1] = 0; // First loop to detect the encoding for ( index = 0; index < (size - 1 - (long)sizeof(unsigned short)) ;) { cursor = buffer + index; value = UINT16ARRAY(cursor); switch(value) { case 0: index += 2; cursor = buffer + index; if(m_topicsFile.isEmpty()) m_topicsFile = QByteArray("/") + QByteArray((const char*) buffer + index + 2); break; case 1: index += 2; cursor = buffer + index; if(m_indexFile.isEmpty()) m_indexFile = QByteArray("/") + QByteArray((const char*)buffer + index + 2); break; case 2: index += 2; cursor = buffer + index; if(m_home.isEmpty() || m_home == "/") m_home = QByteArray("/") + QByteArray((const char*) buffer + index + 2); break; case 3: index += 2; cursor = buffer + index; m_title = QByteArray( (const char*) (buffer + index + 2) ); break; case 4: index += 2; cursor = buffer + index; p = buffer + index + 2; m_detectedLCID = (short) (p[0] | (p[1]<<8)); break; case 6: index += 2; cursor = buffer + index; if ( m_topicsFile.isEmpty() ) { QString topicAttempt = "/", tmp; topicAttempt += QString ((const char*) buffer +index +2); tmp = topicAttempt + ".hhc"; if ( ResolveObject( tmp, &ui) ) m_topicsFile = qPrintable( tmp ); tmp = topicAttempt + ".hhk"; if ( ResolveObject( tmp, &ui) ) m_indexFile = qPrintable( tmp ); } break; case 16: index += 2; cursor = buffer + index; m_font = QString ((const char*) buffer + index + 2); break; default: index += 2; cursor = buffer + index; } value = UINT16ARRAY(cursor); index += value + 2; } return true; } QString EBook_CHM::getTopicByUrl( const QUrl& url ) { QMap< QUrl, QString >::const_iterator it = m_url2topics.constFind( url ); if ( it == m_url2topics.constEnd() ) return QString(); return it.value(); } static int chm_enumerator_callback( struct chmFile*, struct chmUnitInfo *ui, void *context ) { EBook_CHM tmp; ((QList *) context)->push_back( tmp.pathToUrl( ui->path ) ); return CHM_ENUMERATOR_CONTINUE; } bool EBook_CHM::enumerateFiles(QList &files ) { files.clear(); return chm_enumerate( m_chmFile, CHM_ENUMERATE_ALL, chm_enumerator_callback, &files ); } QString EBook_CHM::currentEncoding() const { return m_currentEncoding; } bool EBook_CHM::setCurrentEncoding( const char * encoding ) { m_currentEncoding = encoding; return changeFileEncoding( encoding ); } bool EBook_CHM::isSupportedUrl(const QUrl &url) { return url.scheme() == URL_SCHEME_CHM; } bool EBook_CHM::guessTextEncoding() { if ( !m_detectedLCID ) { qWarning ("Could not detect LCID"); return false; } QString enc = Ebook_CHM_Encoding::guessByLCID( m_detectedLCID ); if ( changeFileEncoding ( enc ) ) { m_currentEncoding = enc; return true; } return false; } bool EBook_CHM::changeFileEncoding( const QString& qtencoding ) { // Encoding could be either simple Qt codepage, or set like CP1251/KOI8, which allows to // set up encodings separately for text (first) and internal files (second) int p = qtencoding.indexOf( '/' ); if ( p != -1 ) { QString global = qtencoding.left( p ); QString special = qtencoding.mid( p + 1 ); m_textCodec = QTextCodec::codecForName( global.toUtf8() ); if ( !m_textCodec ) { qWarning( "Could not set up Text Codec for encoding '%s'", qPrintable( global ) ); return false; } m_textCodecForSpecialFiles = QTextCodec::codecForName( special.toUtf8() ); if ( !m_textCodecForSpecialFiles ) { qWarning( "Could not set up Text Codec for encoding '%s'", qPrintable( special ) ); return false; } } else { m_textCodecForSpecialFiles = m_textCodec = QTextCodec::codecForName( qtencoding.toUtf8() ); if ( !m_textCodec ) { qWarning( "Could not set up Text Codec for encoding '%s'", qPrintable( qtencoding ) ); return false; } } m_htmlEntityDecoder.changeEncoding( m_textCodec ); return true; } void EBook_CHM::fillTopicsUrlMap() { if ( !m_lookupTablesValid ) return; // Read those tables QVector topics( m_chmTOPICS.length ), urltbl( m_chmURLTBL.length ), urlstr( m_chmURLSTR.length ), strings( m_chmSTRINGS.length ); if ( !RetrieveObject( &m_chmTOPICS, (unsigned char*) topics.data(), 0, m_chmTOPICS.length ) || !RetrieveObject( &m_chmURLTBL, (unsigned char*) urltbl.data(), 0, m_chmURLTBL.length ) || !RetrieveObject( &m_chmURLSTR, (unsigned char*) urlstr.data(), 0, m_chmURLSTR.length ) || !RetrieveObject( &m_chmSTRINGS, (unsigned char*) strings.data(), 0, m_chmSTRINGS.length ) ) return; for ( LONGUINT64 i = 0; i < m_chmTOPICS.length; i += TOPICS_ENTRY_LEN ) { unsigned int off_title = get_int32_le( (unsigned int *)(topics.data() + i + 4) ); unsigned int off_url = get_int32_le( (unsigned int *)(topics.data() + i + 8) ); off_url = get_int32_le( (unsigned int *)( urltbl.data() + off_url + 8) ) + 8; QUrl url = pathToUrl( (const char*) urlstr.data() + off_url ); if ( off_title < (unsigned int)strings.size() ) m_url2topics[url] = encodeWithCurrentCodec ( (const char*) strings.data() + off_title ); else m_url2topics[url] = "Untitled"; } } bool EBook_CHM::parseBinaryTOC( QList< EBookTocEntry >& toc ) const { if ( !m_lookupTablesValid ) return false; QByteArray tocidx, topics, urltbl, urlstr, strings; // Read the index tables if ( !getBinaryContent( tocidx, "/#TOCIDX" ) || !getBinaryContent( topics, "/#TOPICS" ) || !getBinaryContent( urltbl, "/#URLTBL" ) || !getBinaryContent( urlstr, "/#URLSTR" ) || !getBinaryContent( strings, "/#STRINGS" ) ) return false; // Shamelessly stolen from xchm if ( !RecurseLoadBTOC( tocidx, topics, urltbl, urlstr, strings, UINT32ARRAY( tocidx.data() ), toc, 0 ) ) { qWarning("Failed to parse binary TOC, fallback to text-based TOC"); toc.clear(); return false; } return true; } // // This piece of code was based on the one in xchm written by Razvan Cojocaru // bool EBook_CHM::RecurseLoadBTOC( const QByteArray& tocidx, const QByteArray& topics, const QByteArray& urltbl, const QByteArray& urlstr, const QByteArray& strings, int offset, QList< EBookTocEntry >& entries, int level ) const { while ( offset ) { // If this is end of TOCIDX, return. if ( tocidx.size() < offset + 20 ) return true; unsigned int flags = UINT32ARRAY( tocidx.data() + offset + 4 ); int index = UINT32ARRAY( tocidx.data() + offset + 8 ); if ( (flags & 0x04) || (flags & 0x08)) { QString name, value; if ( (flags & 0x08) == 0 ) { if ( strings.size() < index + 1 ) { qWarning("EBook_CHM::RecurseLoadBTOC: invalid name index (%d) for book TOC entry!", index ); return false; } name = encodeWithCurrentCodec( strings.data() + index); } else { if ( topics.size() < (index * 16) + 12 ) { qWarning("EBook_CHM::RecurseLoadBTOC: invalid name index (%d) for local TOC entry!", index ); return false; } int tocoffset = (int) UINT32ARRAY(topics.data()+ (index * 16) + 4); if ( strings.size() < tocoffset + 1 ) { qWarning("EBook_CHM::RecurseLoadBTOC: invalid name tocoffset (%d) for TOC entry!", tocoffset ); return false; } if ( tocoffset < 0 ) name.clear(); else name = encodeWithCurrentCodec( strings.data() + tocoffset ); // #URLTBL index tocoffset = (int) UINT32ARRAY( topics.data() + (index * 16) + 8 ); if ( tocoffset < 0 || urltbl.size() < tocoffset + 12 ) { qWarning("EBook_CHM::RecurseLoadBTOC: invalid url index (%d) for TOC entry!", tocoffset ); return false; } tocoffset = (int) UINT32ARRAY(urltbl.data() + tocoffset + 8); if ( tocoffset < 0 || urlstr.size() < tocoffset ) { qWarning("EBook_CHM::RecurseLoadBTOC: invalid url offset (%d) for TOC entry!", tocoffset ); return false; } value = encodeWithCurrentCodec( urlstr.data() + tocoffset + 8 ); } EBookTocEntry entry; entry.name = name.trimmed(); if ( !entry.name.isEmpty() ) { if ( !value.isEmpty() ) entry.url = pathToUrl( value ); entry.iconid = EBookTocEntry::IMAGE_AUTO; entry.indent = level; entries.push_back( entry ); } } if ( flags & 0x04 ) { // book if ( tocidx.size() < offset + 24 ) { qWarning("EBook_CHM::RecurseLoadBTOC: invalid child entry offset (%d)", offset ); return false; } unsigned int childoffset = UINT32ARRAY( tocidx.data() + offset + 20 ); if ( childoffset ) { if ( !RecurseLoadBTOC( tocidx, topics, urltbl, urlstr, strings, childoffset, entries, level + 1 ) ) return false; } } offset = UINT32ARRAY( tocidx.data() + offset + 0x10 ); } return true; } bool EBook_CHM::hasOption(const QString & name) const { if ( !m_envOptions.isEmpty() && m_envOptions.contains( name ) ) return true; return false; } QUrl EBook_CHM::pathToUrl(const QString &link) const { if ( link.startsWith( "http://" ) || link.startsWith( "https://" ) ) return QUrl( link ); QUrl url; url.setScheme( URL_SCHEME_CHM ); url.setHost( URL_SCHEME_CHM ); // Does the link contain the fragment as well? int off = link.indexOf( '#' ); QString path; if ( off != -1 ) { path = link.left( off ); url.setFragment( link.mid( off + 1 ) ); } else path = link; if ( !path.startsWith( '/' ) ) path.prepend( '/' ); url.setPath( QUrl::fromPercentEncoding( path.toUtf8() ) ); return url; } QString EBook_CHM::urlToPath(const QUrl &link) const { if ( link.scheme() == URL_SCHEME_CHM ) { if ( link.path() == "/" || link.path().isEmpty() ) return m_home; return link.path(); } return ""; } EBook_CHM::ParsedEntry::ParsedEntry() { iconid = 0; indent = 0; } diff --git a/generators/chm/lib/ebook_chm_encoding.cpp b/generators/chm/lib/ebook_chm_encoding.cpp index c2ffdf67b..9ce6f054f 100644 --- a/generators/chm/lib/ebook_chm_encoding.cpp +++ b/generators/chm/lib/ebook_chm_encoding.cpp @@ -1,306 +1,306 @@ /* * 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 "ebook_chm_encoding.h" typedef struct { const char * qtcodec; const short * lcids; } EbookChmTextEncodingEntry; // Do not try to embed those in the text_encoding_table, it does not work - at least with gcc. static short lcid_arabic[] = { 0x1401, 0x3C01, 0x0C01, 0x0801, 0x2C01, 0x3401, 0x3001, 0x1001, 0x1801, 0x2001, 0x4001, 0x0401, 0x2801, 0x1C01, 0x3801, 0x2401, 0x0429, 0x0420, 0, }; static short lcid_baltic[] = { 0x0425, 0x0426, 0x0427, 0 }; static short lcid_centralEuropean[] = { 0x041C, 0x041A, 0x0405, 0x040E, 0x0415, 0x0418, 0x081A, 0x041B, 0x0424, 0 }; static short lcid_ChineseSimplifiedGB18030[] = { 0x0804, 0 }; static short lcid_ChineseSimplifiedGBK[] = { 0x0804, 0 }; static short lcid_ChineseSimplifiedGB2313[] = { 0x1004, 0 }; static short lcid_ChineseTraditionalBig5[] = { 0x0404, 0x1404, 0 }; static short lcid_ChineseTraditionalBigHKSCS[] = { 0x0C04, 0 }; static short lcid_CyrillicCP1251[] = { 0x082C, 0x0423, 0x0402, 0x042F, 0x0419, 0x0C1A, 0x0444, 0x0422, 0x0843, 0 }; static short lcid_CyrillicKOI8R[] = { 0x7001, // artifical LCID 0 }; static short lcid_Greek[] = { 0x0408, 0 }; static short lcid_Hebrew[] = { 0x040D, 0 }; static short lcid_Japanese_eucJP[] = { 0x0411, 0 }; static short lcid_Japanese_JIS7[] = { 0x0411, 0 }; static short lcid_Japanese_ShiftJIS[] = { 0x0411, 0 }; static short lcid_Korean_eucKR[] = { 0x0412, 0 }; static short lcid_TamilTSCII[] = { 0x0449, 0 }; static short lcid_ThaiTIS[] = { 0x041E, 0 }; static short lcid_UkrainianKOI[] = { 0x7006, 0 }; static short lcid_Turkish[] = { 0x042C, 0x041F, 0x0443, 0 }; static short lcid_Vietnamese[] = { 0x042A, 0 }; static short lcid_UnicodeUTF8[] = { 0x7004, // artifical LCID 0 }; static short lcid_UnicodeUTF16[] = { 0x7005, // artifical LCID 0 }; static short lcid_Western[] = { 0x0436, 0x042D, 0x0403, 0x0406, 0x0813, 0x0413, 0x0C09, 0x2809, 0x1009, 0x2409, 0x1809, 0x2009, 0x1409, 0x3409, 0x1C09, 0x2C09, 0x0809, 0x0409, 0x0438, 0x040B, 0x080C, 0x0C0C, 0x040C, 0x140C, 0x100C, 0x0C07, 0x0407, 0x1407, 0x1007, 0x0807, 0x040F, 0x0421, 0x0410, 0x0810, 0x083E, 0x043E, 0x0414, 0x0814, 0x0416, 0x0816, 0x0432, 0x2C0A, 0x400A, 0x340A, 0x240A, 0x140A, 0x1C0A, 0x300A, 0x440A, 0x100A, 0x480A, 0x080A, 0x4C0A, 0x180A, 0x3C0A, 0x280A, 0x500A, 0x0C0A, 0x380A, 0x200A, 0x0441, 0x081D, 0x041D, 0x0434, 0x0435, 0x042B, 0x042C, 0x0439, 0x043A, 0x044E, 0x044F, 0x081A, 0x0443, 0 }; static const EbookChmTextEncodingEntry text_encoding_table [] = { { "CP1256", lcid_arabic }, { "CP1257", lcid_baltic }, { "CP1250", lcid_centralEuropean }, { "GB18030", lcid_ChineseSimplifiedGB18030 }, { "GBK", lcid_ChineseSimplifiedGBK }, { "GB2313", lcid_ChineseSimplifiedGB2313 }, { "Big5", lcid_ChineseTraditionalBig5 }, { "Big5-HKSCS", lcid_ChineseTraditionalBigHKSCS }, { "CP1251", lcid_CyrillicCP1251 }, { "KOI8-R", lcid_CyrillicKOI8R }, { "CP1253", lcid_Greek }, { "CP1255", lcid_Hebrew }, { "Shift-JIS", lcid_Japanese_ShiftJIS }, { "eucJP", lcid_Japanese_eucJP }, { "JIS7", lcid_Japanese_JIS7 }, { "eucKR", lcid_Korean_eucKR }, { "TSCII", lcid_TamilTSCII }, { "TIS-620", lcid_ThaiTIS }, { "KOI8-U", lcid_UkrainianKOI }, { "CP1254", lcid_Turkish }, { "CP1258", lcid_Vietnamese }, { "UTF-8", lcid_UnicodeUTF8 }, { "UTF-16", lcid_UnicodeUTF16 }, { "CP1252", lcid_Western }, - { 0, 0 } + { nullptr, nullptr } }; QString Ebook_CHM_Encoding::guessByLCID(unsigned short lcid) { for ( const EbookChmTextEncodingEntry * t = text_encoding_table; t->qtcodec; ++t ) { for ( const short * lcids = t->lcids; *lcids; lcids++ ) if ( *lcids == lcid ) return t->qtcodec; } return "UTF-8"; } diff --git a/generators/chm/lib/ebook_epub.cpp b/generators/chm/lib/ebook_epub.cpp index 1fe48a109..1f51faff3 100644 --- a/generators/chm/lib/ebook_epub.cpp +++ b/generators/chm/lib/ebook_epub.cpp @@ -1,379 +1,379 @@ /* * 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 . */ #if defined (WIN32) #include // dup #else #include #endif #include #include #include "ebook_epub.h" #include "helperxmlhandler_epubcontainer.h" #include "helperxmlhandler_epubcontent.h" #include "helperxmlhandler_epubtoc.h" static const char * URL_SCHEME_EPUB = "epub"; EBook_EPUB::EBook_EPUB() : EBook() { - m_zipFile = 0; + m_zipFile = nullptr; } EBook_EPUB::~EBook_EPUB() { close(); } bool EBook_EPUB::load(const QString &archiveName) { close(); // We use QFile and zip_fdopen instead of zip_open because latter does not support Unicode file names m_epubFile.setFileName( archiveName ); if ( !m_epubFile.open( QIODevice::ReadOnly ) ) { qWarning("Could not open file %s: %s", qPrintable(archiveName), qPrintable( m_epubFile.errorString())); return false; } // Open the ZIP archive: http://www.nih.at/libzip/zip_fdopen.html // Note that zip_fdopen takes control over the passed descriptor, // so we need to pass a duplicate of it for this to work correctly int fdcopy = dup( m_epubFile.handle() ); if ( fdcopy < 0 ) { qWarning("Could not duplicate descriptor" ); return false; } int errcode; m_zipFile = zip_fdopen( fdcopy, 0, &errcode ); if ( !m_zipFile ) { qWarning("Could not open file %s: error %d", qPrintable(archiveName), errcode); return false; } // Parse the book descriptor file if ( !parseBookinfo() ) return false; return true; } void EBook_EPUB::close() { if ( m_zipFile ) { zip_close( m_zipFile ); - m_zipFile = 0; + m_zipFile = nullptr; } //if ( m_epubFile.isOpen() ) // m_epubFile.close(); } bool EBook_EPUB::getFileContentAsString(QString &str, const QUrl &url) const { return getFileAsString( str, urlToPath( url ) ); } bool EBook_EPUB::getFileContentAsBinary(QByteArray &data, const QUrl &url) const { return getFileAsBinary( data, urlToPath( url ) ); } bool EBook_EPUB::enumerateFiles(QList &files) { files = m_ebookManifest; return true; } QString EBook_EPUB::title() const { return m_title; } QUrl EBook_EPUB::homeUrl() const { return m_tocEntries[0].url; } bool EBook_EPUB::hasFeature(EBook::Feature code) const { switch ( code ) { case FEATURE_TOC: return true; case FEATURE_INDEX: return false; case FEATURE_ENCODING: return false; } return false; } bool EBook_EPUB::getTableOfContents( QList &toc ) const { toc = m_tocEntries; return true; } bool EBook_EPUB::getIndex(QList &) const { return false; } QString EBook_EPUB::getTopicByUrl(const QUrl& url) { if ( m_urlTitleMap.contains( url ) ) return m_urlTitleMap[ url ]; return ""; } QString EBook_EPUB::currentEncoding() const { return "UTF-8"; } bool EBook_EPUB::setCurrentEncoding(const char *) { abort(); } bool EBook_EPUB::isSupportedUrl(const QUrl &url) { return url.scheme() == URL_SCHEME_EPUB; } bool EBook_EPUB::parseXML(const QString &uri, QXmlDefaultHandler * parser) { QByteArray container; if ( !getFileAsBinary( container, uri ) ) { qDebug("Failed to retrieve XML file %s", qPrintable( uri ) ); return false; } // Use it as XML source QXmlInputSource source; source.setData( container ); // Init the reader QXmlSimpleReader reader; reader.setContentHandler( parser ); reader.setErrorHandler( parser ); return reader.parse( source ); } bool EBook_EPUB::parseBookinfo() { // Parse the container.xml to find the content descriptor HelperXmlHandler_EpubContainer container_parser; if ( !parseXML( "META-INF/container.xml", &container_parser ) || container_parser.contentPath.isEmpty() ) return false; // Parse the content.opf HelperXmlHandler_EpubContent content_parser; if ( !parseXML( container_parser.contentPath, &content_parser ) ) return false; // At least title and the TOC must be present if ( !content_parser.metadata.contains("title") || content_parser.tocname.isEmpty() ) return false; // All the files, including TOC, are relative to the container_parser.contentPath m_documentRoot.clear(); int sep = container_parser.contentPath.lastIndexOf( '/' ); if ( sep != -1 ) m_documentRoot = container_parser.contentPath.left( sep + 1 ); // Keep the trailing slash // Parse the TOC HelperXmlHandler_EpubTOC toc_parser( this ); if ( !parseXML( content_parser.tocname, &toc_parser ) ) return false; // Get the data m_title = content_parser.metadata[ "title" ]; // Move the manifest entries into the list for ( const QString &f : qAsConst(content_parser.manifest) ) m_ebookManifest.push_back( pathToUrl( f ) ); // Copy the manifest information and fill up the other maps if we have it if ( !toc_parser.entries.isEmpty() ) { for( const EBookTocEntry &e : qAsConst(toc_parser.entries) ) { // Add into url-title map m_urlTitleMap[ e.url ] = e.name; m_tocEntries.push_back( e ); } } else { // Copy them from spine for( QString url : qAsConst(content_parser.spine) ) { EBookTocEntry e; if ( content_parser.manifest.contains( url ) ) url = content_parser.manifest[ url ]; e.name = url; e.url= pathToUrl( url ); e.iconid = EBookTocEntry::IMAGE_NONE; e.indent = 0; // Add into url-title map m_urlTitleMap[ pathToUrl( url ) ] = url; m_tocEntries.push_back( e ); } } // EPub with an empty TOC is not valid if ( m_tocEntries.isEmpty() ) return false; return true; } QUrl EBook_EPUB::pathToUrl(const QString &link) const { QUrl url; url.setScheme( URL_SCHEME_EPUB ); url.setHost( URL_SCHEME_EPUB ); // Does the link contain the fragment as well? int off = link.indexOf( '#' ); QString path; if ( off != -1 ) { path = link.left( off ); url.setFragment( link.mid( off + 1 ) ); } else path = link; if ( !path.startsWith( '/' ) ) path.prepend( '/' ); url.setPath( QUrl::fromPercentEncoding( path.toUtf8() ) ); return url; } QString EBook_EPUB::urlToPath(const QUrl &link) const { if ( link.scheme() == URL_SCHEME_EPUB ) return link.path(); return ""; } bool EBook_EPUB::getFileAsString(QString &str, const QString &path) const { QByteArray data; if ( !getFileAsBinary( data, path ) ) return false; // I have never seen yet an UTF16 epub if ( data.startsWith("" ); int utf16 = data.indexOf("UTF-16"); if ( utf16 > 0 && utf16 < endxmltag ) { - QMessageBox::critical( 0, + QMessageBox::critical( nullptr, ("Unsupported encoding"), ("The encoding of this ebook is not supported yet. Please send it to gyunaev@ulduzsoft.com for support to be added") ); return false; } } str = QString::fromUtf8( data ); return true; } bool EBook_EPUB::getFileAsBinary(QByteArray &data, const QString &path) const { // Retrieve the file size struct zip_stat fileinfo; QString completeUrl; if ( !path.isEmpty() && path[0] == '/' ) completeUrl = m_documentRoot + path.mid( 1 ); else completeUrl = m_documentRoot + path; //qDebug("URL requested: %s (%s)", qPrintable(path), qPrintable(completeUrl)); // http://www.nih.at/libzip/zip_stat.html if ( zip_stat( m_zipFile, completeUrl.toUtf8().constData(), 0, &fileinfo) != 0 ) { qDebug("File %s is not found in the archive", qPrintable(completeUrl)); return false; } // Make sure the size field is valid if ( (fileinfo.valid & ZIP_STAT_SIZE) == 0 || (fileinfo.valid & ZIP_STAT_INDEX) == 0 ) return false; // Open the file struct zip_file * file = zip_fopen_index( m_zipFile, fileinfo.index, 0 ); if ( !file ) return false; // Allocate the memory and read the file data.resize( fileinfo.size ); // Could it return a positive number but not fileinfo.size??? int ret = zip_fread( file, data.data(), fileinfo.size ); if ( ret != (int) fileinfo.size ) { zip_fclose( file ); return false; } zip_fclose( file ); return true; } diff --git a/generators/chm/lib/ebook_search.cpp b/generators/chm/lib/ebook_search.cpp index 5960862ac..b72c0f3e1 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; + m_Index = nullptr; } 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 ( const QUrl &allDocumentsI : qAsConst( alldocuments ) ) { const QString docpath = allDocumentsI.path(); if ( docpath.endsWith( ".html", Qt::CaseInsensitive ) || docpath.endsWith( ".htm", Qt::CaseInsensitive ) || docpath.endsWith( ".xhtml", Qt::CaseInsensitive ) ) documents.push_back( allDocumentsI ); } if ( !m_Index->makeIndex( documents, ebookFile ) ) { delete m_Index; - m_Index = 0; + m_Index = nullptr; 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 ( const QChar &iChar : query ) { 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; + return m_Index != nullptr; } diff --git a/generators/chm/lib/helper_entitydecoder.h b/generators/chm/lib/helper_entitydecoder.h index bb2a12be5..54557e178 100644 --- a/generators/chm/lib/helper_entitydecoder.h +++ b/generators/chm/lib/helper_entitydecoder.h @@ -1,45 +1,45 @@ /* * 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 . */ #ifndef HELPER_ENTITYDECODER_H #define HELPER_ENTITYDECODER_H #include #include // // This helper class decodes the Unicode HTML entities into the Unicode characters // class HelperEntityDecoder { public: // Initialization with the specific decoder - HelperEntityDecoder( QTextCodec * encoder = 0 ); + HelperEntityDecoder( QTextCodec * encoder = nullptr ); // Used when the encoding changes - void changeEncoding( QTextCodec * encoder = 0 ); + void changeEncoding( QTextCodec * encoder = nullptr ); // The decoder function QString decode( const QString& entity ) const; private: // Map to decode HTML entitles like ´ based on current encoding, initialized upon the first use QMap m_entityDecodeMap; }; #endif // HELPER_ENTITYDECODER_H diff --git a/generators/chm/lib/helper_search_index.cpp b/generators/chm/lib/helper_search_index.cpp index acd6b1791..6f2b515ed 100644 --- a/generators/chm/lib/helper_search_index.cpp +++ b/generators/chm/lib/helper_search_index.cpp @@ -1,492 +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 ) + : QObject( nullptr ) { 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; + Entry *e = nullptr; 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; + Entry *e = nullptr; 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(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(const Document &doc : qAsConst(minDocs)) results << docList.at((int)doc.docNumber); return results; } QUrl fileName; 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 ( 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/epub/autotests/epubgeneratortest.cpp b/generators/epub/autotests/epubgeneratortest.cpp index c0344a02f..b9da3f5ff 100644 --- a/generators/epub/autotests/epubgeneratortest.cpp +++ b/generators/epub/autotests/epubgeneratortest.cpp @@ -1,84 +1,84 @@ /*************************************************************************** * Copyright (C) 2017 by Gilbert Assaf * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * ***************************************************************************/ #include #include "core/document.h" #include "core/page.h" #include "settings_core.h" #include "core/textpage.h" class EpubGeneratorTest : public QObject { Q_OBJECT private slots: void initTestCase(); void testDocumentStructure(); void testDocumentContent(); void cleanupTestCase(); private: Okular::Document *m_document; }; void EpubGeneratorTest::initTestCase() { Okular::SettingsCore::instance( QStringLiteral("EpubGeneratorTest") ); - m_document = new Okular::Document( 0 ); + m_document = new Okular::Document( nullptr ); const QString testFile = QStringLiteral(KDESRCDIR "autotests/data/test.epub"); QMimeDatabase db; const QMimeType mime = db.mimeTypeForFile( testFile ); QCOMPARE( m_document->openDocument(testFile, QUrl(), mime), Okular::Document::OpenSuccess ); } void EpubGeneratorTest::cleanupTestCase() { m_document->closeDocument(); delete m_document; } void EpubGeneratorTest::testDocumentStructure() { unsigned int expectedPageNr = 3; QCOMPARE( m_document->pages(), expectedPageNr); QCOMPARE( m_document->metaData(QLatin1String("DocumentTitle")).toString(), QStringLiteral("Okular Test") ); const Okular::DocumentSynopsis *docSyn = m_document->documentSynopsis(); QDomElement heading1 = docSyn->documentElement(); QCOMPARE( heading1.tagName(), QStringLiteral("Lorem ipsum Section 1") ); QDomElement heading2 = heading1.nextSiblingElement(); QCOMPARE( heading2.tagName(), QStringLiteral("Lorem ipsum Section 2") ); } void EpubGeneratorTest::testDocumentContent() { const Okular::Page *page0 = m_document->page(0); QCOMPARE( page0->number(), 0); m_document->requestTextPage( page0->number() ); QVERIFY( page0->hasTextPage() ); QCOMPARE( page0->text().trimmed(), QStringLiteral("Lorem ipsum Section 1\n\u2029This is an example Text.\n\uFFFC") ); const Okular::Page *page1 = m_document->page(1); QCOMPARE( page1->number(), 1); m_document->requestTextPage( page1->number() ); QVERIFY( page1->hasTextPage() ); QCOMPARE( page1->text().trimmed(), QStringLiteral("Lorem ipsum Section 2\n\u2029This is an example Text.") ); } QTEST_MAIN( EpubGeneratorTest ) #include "epubgeneratortest.moc" /* kate: replace-tabs on; tab-width 4; */ diff --git a/generators/epub/converter.cpp b/generators/epub/converter.cpp index adb1f5dcf..87aaa2350 100644 --- a/generators/epub/converter.cpp +++ b/generators/epub/converter.cpp @@ -1,458 +1,458 @@ /*************************************************************************** * Copyright (C) 2008 by Ely Levy * * * * 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 "converter.h" #include #include #include #include #include #include // Because of the HACK #include #include #include #include #include #include #include using namespace Epub; -Converter::Converter() : mTextDocument(NULL) +Converter::Converter() : mTextDocument(nullptr) { } Converter::~Converter() { } // join the char * array into one QString QString _strPack(char **str, int size) { QString res; res = QString::fromUtf8(str[0]); for (int i=1;igetEpub(), type, &size); if (data) { emit addMetaData(key, _strPack((char **)data, size)); for (int i=0;iend(); bit = bit.next()) { for (QTextBlock::iterator fit = bit.begin(); !(fit.atEnd()); ++fit) { QTextFragment frag = fit.fragment(); if (frag.isValid() && frag.charFormat().isAnchor()) { QString hrefString = frag.charFormat().anchorHref(); // remove ./ or ../ // making it easier to compare, with links while(!hrefString.isNull() && ( hrefString.at(0) == QLatin1Char('.') || hrefString.at(0) == QLatin1Char('/')) ){ hrefString.remove(0,1); } QUrl href(hrefString); if (href.isValid() && !href.isEmpty()) { if (href.isRelative()) { // Inside document link if(!hrefString.indexOf(QLatin1Char('#'))) hrefString = name + hrefString; else if(QFileInfo(hrefString).path() == QLatin1String(".") && curDir != QLatin1String(".")) hrefString = curDir + QLatin1Char('/') + hrefString; // QTextCharFormat sometimes splits a link in two // if there's no white space between words & the first one is an anchor // consider whole word to be an anchor ++fit; int fragLen = frag.length(); if(!fit.atEnd() && ((fit.fragment().position() - frag.position()) == 1)) fragLen += fit.fragment().length(); --fit; _insert_local_links(hrefString, QPair(frag.position(), frag.position()+fragLen)); } else { // Outside document link Okular::BrowseAction *action = new Okular::BrowseAction(QUrl(href.toString())); emit addAction(action, frag.position(), frag.position() + frag.length()); } } const QStringList &names = frag.charFormat().anchorNames(); if (!names.empty()) { for (QStringList::const_iterator lit = names.constBegin(); lit != names.constEnd(); ++lit) { mSectionMap.insert(name + QLatin1Char('#') + *lit, bit); } } } // end anchor case } } } void Converter::_insert_local_links(const QString &key, const QPair &value) { if(mLocalLinks.contains(key)){ mLocalLinks[key].append(value); } else { QVector< QPair > vec; vec.append(value); mLocalLinks.insert(key,vec); } } static QPoint calculateXYPosition( QTextDocument *document, int startPosition ) { const QTextBlock startBlock = document->findBlock( startPosition ); const QRectF startBoundingRect = document->documentLayout()->blockBoundingRect( startBlock ); QTextLayout *startLayout = startBlock.layout(); if (!startLayout) { qWarning() << "Start layout not found" << startLayout; return QPoint(); } int startPos = startPosition - startBlock.position(); const QTextLine startLine = startLayout->lineForTextPosition( startPos ); double x = startBoundingRect.x() ; double y = startBoundingRect.y() + startLine.y(); y = (int)y % 800; return QPoint(x,y); } QTextDocument* Converter::convert( const QString &fileName ) { EpubDocument *newDocument = new EpubDocument(fileName); if (!newDocument->isValid()) { emit error(i18n("Error while opening the EPub document."), -1); delete newDocument; - return NULL; + return nullptr; } mTextDocument = newDocument; QTextCursor *_cursor = new QTextCursor( mTextDocument ); mLocalLinks.clear(); mSectionMap.clear(); // Emit the document meta data _emitData(Okular::DocumentInfo::Title, EPUB_TITLE); _emitData(Okular::DocumentInfo::Author, EPUB_CREATOR); _emitData(Okular::DocumentInfo::Subject, EPUB_SUBJECT); _emitData(Okular::DocumentInfo::Creator, EPUB_PUBLISHER); _emitData(Okular::DocumentInfo::Description, EPUB_DESCRIPTION); _emitData(Okular::DocumentInfo::CreationDate, EPUB_DATE); _emitData(Okular::DocumentInfo::Category, EPUB_TYPE); _emitData(Okular::DocumentInfo::Copyright, EPUB_RIGHTS); emit addMetaData( Okular::DocumentInfo::MimeType, QStringLiteral("application/epub+zip")); struct eiterator *it; // iterate over the book it = epub_get_iterator(mTextDocument->getEpub(), EITERATOR_SPINE, 0); // if the background color of the document is non-white it will be handled by QTextDocument::setHtml() bool firstPage = true; QVector movieAnnots; QVector soundActions; // HACK BEGIN Get the links without CSS to be blue // Remove if Qt ever gets fixed and the code in textdocumentgenerator.cpp works const QPalette orig = qApp->palette(); QPalette p = orig; p.setColor(QPalette::Link, Qt::blue); // HACK END const QSize videoSize(320, 240); do{ if(!epub_it_get_curr(it)) { continue; } movieAnnots.clear(); soundActions.clear(); const QString link = QString::fromUtf8(epub_it_get_curr_url(it)); mTextDocument->setCurrentSubDocument(link); QString htmlContent = QString::fromUtf8(epub_it_get_curr(it)); // as QTextCharFormat::anchorNames() ignores sections, replace it with

htmlContent.replace(QRegExp(QStringLiteral("< *section")),QStringLiteral("maxContentHeight(); const int maxWidth = mTextDocument->maxContentWidth(); QDomDocument dom; if(dom.setContent(htmlContent)) { QDomNodeList svgs = dom.elementsByTagName(QStringLiteral("svg")); if(!svgs.isEmpty()) { QList< QDomNode > imgNodes; for (int i = 0; i < svgs.length(); ++i) { QDomNodeList images = svgs.at(i).toElement().elementsByTagName(QStringLiteral("image")); for (int j = 0; j < images.length(); ++j) { QString lnk = images.at(i).toElement().attribute(QStringLiteral("xlink:href")); int ht = images.at(i).toElement().attribute(QStringLiteral("height")).toInt(); int wd = images.at(i).toElement().attribute(QStringLiteral("width")).toInt(); QImage img = mTextDocument->loadResource(QTextDocument::ImageResource,QUrl(lnk)).value(); if(ht == 0) ht = img.height(); if(wd == 0) wd = img.width(); if(ht > maxHeight) ht = maxHeight; if(wd > maxWidth) wd = maxWidth; mTextDocument->addResource(QTextDocument::ImageResource,QUrl(lnk),img); QDomDocument newDoc; newDoc.setContent(QStringLiteral("").arg(lnk).arg(ht).arg(wd)); imgNodes.append(newDoc.documentElement()); } for (const QDomNode &nd : qAsConst(imgNodes)) { svgs.at(i).parentNode().replaceChild(nd,svgs.at(i)); } } } // handle embedded videos QDomNodeList videoTags = dom.elementsByTagName(QStringLiteral("video")); while(!videoTags.isEmpty()) { QDomNodeList sourceTags = videoTags.at(0).toElement().elementsByTagName(QStringLiteral("source")); if(!sourceTags.isEmpty()) { QString lnk = sourceTags.at(0).toElement().attribute(QStringLiteral("src")); Okular::Movie *movie = new Okular::Movie(mTextDocument->loadResource(EpubDocument::MovieResource,QUrl(lnk)).toString()); movie->setSize(videoSize); movie->setShowControls(true); Okular::MovieAnnotation *annot = new Okular::MovieAnnotation; annot->setMovie(movie); movieAnnots.push_back(annot); QDomDocument tempDoc; tempDoc.setContent(QStringLiteral("

<video></video>
")); videoTags.at(0).parentNode().replaceChild(tempDoc.documentElement(),videoTags.at(0)); } } //handle embedded audio QDomNodeList audioTags = dom.elementsByTagName(QStringLiteral("audio")); while(!audioTags.isEmpty()) { QDomElement element = audioTags.at(0).toElement(); bool repeat = element.hasAttribute(QStringLiteral("loop")); QString lnk = element.attribute(QStringLiteral("src")); Okular::Sound *sound = new Okular::Sound(mTextDocument->loadResource( EpubDocument::AudioResource, QUrl(lnk)).toByteArray()); Okular::SoundAction *soundAction = new Okular::SoundAction(1.0,true,repeat,false,sound); soundActions.push_back(soundAction); QDomDocument tempDoc; tempDoc.setContent(QStringLiteral("
<audio></audio>
")); audioTags.at(0).parentNode().replaceChild(tempDoc.documentElement(),audioTags.at(0)); } htmlContent = dom.toString(); } // HACK BEGIN qApp->setPalette(p); // HACK END QTextBlock before; if(firstPage) { mTextDocument->setHtml(htmlContent); firstPage = false; before = mTextDocument->begin(); } else { before = _cursor->block(); _cursor->insertHtml(htmlContent); } // HACK BEGIN qApp->setPalette(orig); // HACK END QTextCursor csr(before); // a temporary cursor pointing at the begin of the last inserted block int index = 0; while( !movieAnnots.isEmpty() && !(csr = mTextDocument->find(QStringLiteral(""),csr)).isNull() ) { const int posStart = csr.position(); const QPoint startPoint = calculateXYPosition(mTextDocument, posStart); QImage img(QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("okular/pics/okular-epub-movie.png"))); img = img.scaled(videoSize); csr.insertImage(img); const int posEnd = csr.position(); const QRect videoRect(startPoint,videoSize); movieAnnots[index]->setBoundingRectangle(Okular::NormalizedRect(videoRect,mTextDocument->pageSize().width(), mTextDocument->pageSize().height())); emit addAnnotation(movieAnnots[index++],posStart,posEnd); csr.movePosition(QTextCursor::NextWord); } csr = QTextCursor(before); index = 0; const QString keyToSearch(QStringLiteral("")); while( !soundActions.isEmpty() && !(csr = mTextDocument->find(keyToSearch, csr)).isNull() ) { const int posStart = csr.position() - keyToSearch.size(); const QImage img(QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("okular/pics/okular-epub-sound-icon.png"))); csr.insertImage(img); const int posEnd = csr.position(); qDebug() << posStart << posEnd;; emit addAction(soundActions[index++],posStart,posEnd); csr.movePosition(QTextCursor::NextWord); } mSectionMap.insert(link, before); _handle_anchors(before, link); const int page = mTextDocument->pageCount(); // it will clear the previous format // useful when the last line had a bullet _cursor->insertBlock(QTextBlockFormat()); while(mTextDocument->pageCount() == page) _cursor->insertText(QStringLiteral("\n")); } while (epub_it_get_next(it)); epub_free_iterator(it); // handle toc struct titerator *tit; // FIXME: support other method beside NAVMAP and GUIDE tit = epub_get_titerator(mTextDocument->getEpub(), TITERATOR_NAVMAP, 0); if (!tit) tit = epub_get_titerator(mTextDocument->getEpub(), TITERATOR_GUIDE, 0); if (tit) { do { if (epub_tit_curr_valid(tit)) { char *clink = epub_tit_get_curr_link(tit); QString link = QString::fromUtf8(clink); char *label = epub_tit_get_curr_label(tit); QTextBlock block = mTextDocument->begin(); // must point somewhere if (mSectionMap.contains(link)) { block = mSectionMap.value(link); } else { // load missing resource - char *data = 0; + char *data = nullptr; //epub_get_data can't handle whitespace url encodings QByteArray ba = link.replace(QLatin1String("%20"), QLatin1String(" ")).toLatin1(); const char *clinkClean = ba.data(); int size = epub_get_data(mTextDocument->getEpub(), clinkClean, &data); if (data) { _cursor->insertBlock(); // try to load as image and if not load as html block = _cursor->block(); QImage image; mSectionMap.insert(link, block); if (image.loadFromData((unsigned char *)data, size)) { mTextDocument->addResource(QTextDocument::ImageResource, QUrl(link), image); _cursor->insertImage(link); } else { _cursor->insertHtml(QString::fromUtf8(data)); // Add anchors to hashes _handle_anchors(block, link); } // Start new file in a new page int page = mTextDocument->pageCount(); while(mTextDocument->pageCount() == page) _cursor->insertText(QStringLiteral("\n")); } free(data); } if (block.isValid()) { // be sure we actually got a block emit addTitle(epub_tit_get_curr_depth(tit), QString::fromUtf8(label), block); } else { qDebug() << "Error: no block found for"<< link; } if (clink) free(clink); if (label) free(label); } } while (epub_tit_next(tit)); epub_free_titerator(tit); } else { qDebug() << "no toc found"; } // adding link actions QHashIterator > > hit(mLocalLinks); while (hit.hasNext()) { hit.next(); const QTextBlock block = mSectionMap.value(hit.key()); for (int i = 0; i < hit.value().size(); ++i) { if (block.isValid()) { // be sure we actually got a block Okular::DocumentViewport viewport = calculateViewport(mTextDocument, block); Okular::GotoAction *action = new Okular::GotoAction(QString(), viewport); emit addAction(action, hit.value()[i].first, hit.value()[i].second); } else { qDebug() << "Error: no block found for "<< hit.key(); } } } delete _cursor; return mTextDocument; } diff --git a/generators/markdown/converter.cpp b/generators/markdown/converter.cpp index 5c2da2fbc..042193a0d 100644 --- a/generators/markdown/converter.cpp +++ b/generators/markdown/converter.cpp @@ -1,191 +1,191 @@ /*************************************************************************** * Copyright (C) 2017 by Julian Wolff * * * * 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 "converter.h" #include "generator_md.h" #include #include #include #include #include #include #include #include "debug_md.h" extern "C" { #include } // older versions of discount might not have these flags. // defining them to 0 allows us to convert without them #ifndef MKD_FENCEDCODE #define MKD_FENCEDCODE 0 #endif #ifndef MKD_GITHUBTAGS #define MKD_GITHUBTAGS 0 #endif #ifndef MKD_AUTOLINK #define MKD_AUTOLINK 0 #endif using namespace Markdown; Converter::Converter() : m_markdownFile( nullptr ) { } Converter::~Converter() { if ( m_markdownFile ) { fclose( m_markdownFile ); } } QTextDocument* Converter::convert( const QString &fileName ) { m_markdownFile = fopen( fileName.toLocal8Bit(), "rb" ); if ( !m_markdownFile ) { emit error( i18n( "Failed to open the document" ), -1 ); return nullptr; } m_fileDir = QDir( fileName.left( fileName.lastIndexOf( '/' ) ) ); QTextDocument *doc = convertOpenFile(); extractLinks( doc->rootFrame() ); return doc; } void Converter::convertAgain() { setDocument( convertOpenFile() ); } QTextDocument *Converter::convertOpenFile() { rewind( m_markdownFile ); MMIOT *markdownHandle = mkd_in( m_markdownFile, 0 ); int flags = MKD_FENCEDCODE | MKD_GITHUBTAGS | MKD_AUTOLINK; if (!MarkdownGenerator::isFancyPantsEnabled()) flags |= MKD_NOPANTS; if ( !mkd_compile( markdownHandle, flags ) ) { emit error( i18n( "Failed to compile the Markdown document." ), -1 ); - return 0; + return nullptr; } char *htmlDocument; const int size = mkd_document( markdownHandle, &htmlDocument ); const QString html = QString::fromUtf8( htmlDocument, size ); QTextDocument *textDocument = new QTextDocument; textDocument->setPageSize( QSizeF( 980, 1307 ) ); textDocument->setHtml( html ); textDocument->setDefaultFont( generator()->generalSettings()->font() ); mkd_cleanup( markdownHandle ); QTextFrameFormat frameFormat; frameFormat.setMargin( 45 ); QTextFrame *rootFrame = textDocument->rootFrame(); rootFrame->setFrameFormat( frameFormat ); convertImages( rootFrame, m_fileDir, textDocument ); return textDocument; } void Converter::extractLinks(QTextFrame * parent) { for ( QTextFrame::iterator it = parent->begin(); !it.atEnd(); ++it ) { QTextFrame *textFrame = it.currentFrame(); const QTextBlock textBlock = it.currentBlock(); if ( textFrame ) { extractLinks(textFrame); } else if ( textBlock.isValid() ) { extractLinks(textBlock); } } } void Converter::extractLinks(const QTextBlock & parent) { for ( QTextBlock::iterator it = parent.begin(); !it.atEnd(); ++it ) { const QTextFragment textFragment = it.fragment(); if ( textFragment.isValid() ) { const QTextCharFormat textCharFormat = textFragment.charFormat(); if ( textCharFormat.isAnchor() ) { Okular::BrowseAction *action = new Okular::BrowseAction( QUrl( textCharFormat.anchorHref() ) ); emit addAction( action, textFragment.position(), textFragment.position()+textFragment.length() ); } } } } void Converter::convertImages(QTextFrame * parent, const QDir &dir, QTextDocument *textDocument) { for ( QTextFrame::iterator it = parent->begin(); !it.atEnd(); ++it ) { QTextFrame *textFrame = it.currentFrame(); const QTextBlock textBlock = it.currentBlock(); if ( textFrame ) { convertImages(textFrame, dir, textDocument); } else if ( textBlock.isValid() ) { convertImages(textBlock, dir, textDocument); } } } void Converter::convertImages(const QTextBlock & parent, const QDir &dir, QTextDocument *textDocument) { for ( QTextBlock::iterator it = parent.begin(); !it.atEnd(); ++it ) { const QTextFragment textFragment = it.fragment(); if ( textFragment.isValid() ) { const QTextCharFormat textCharFormat = textFragment.charFormat(); if( textCharFormat.isImageFormat() ) { //TODO: Show images from http URIs QTextImageFormat format; format.setName( QDir::cleanPath( dir.absoluteFilePath( textCharFormat.toImageFormat().name() ) ) ); const QImage img = QImage( format.name() ); if ( img.width() > 890 ) { format.setWidth( 890 ); format.setHeight( img.height() * 890. / img.width() ); } else { format.setWidth( img.width() ); format.setHeight( img.height() ); } QTextCursor cursor( textDocument ); cursor.setPosition( textFragment.position(), QTextCursor::MoveAnchor ); cursor.setPosition( textFragment.position() + textFragment.length(), QTextCursor::KeepAnchor ); cursor.removeSelectedText(); cursor.insertImage( format ); } } } } diff --git a/generators/mobipocket/converter.cpp b/generators/mobipocket/converter.cpp index bd9081ac6..efc26cc1b 100644 --- a/generators/mobipocket/converter.cpp +++ b/generators/mobipocket/converter.cpp @@ -1,107 +1,107 @@ /*************************************************************************** * Copyright (C) 2008 by Jakub Stachowski * * * * 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 "converter.h" #include #include #include #include #include #include #include #include #include using namespace Mobi; Converter::Converter() { } Converter::~Converter() { } void Converter::handleMetadata(const QMap &metadata) { QMapIterator it(metadata); while (it.hasNext()) { it.next(); switch (it.key()) { case Mobipocket::Document::Title: addMetaData(Okular::DocumentInfo::Title, it.value()); break; case Mobipocket::Document::Author: addMetaData(Okular::DocumentInfo::Author, it.value()); break; case Mobipocket::Document::Description: addMetaData(Okular::DocumentInfo::Description, it.value()); break; case Mobipocket::Document::Subject: addMetaData(Okular::DocumentInfo::Subject, it.value()); break; case Mobipocket::Document::Copyright: addMetaData(Okular::DocumentInfo::Copyright, it.value()); break; } } } QTextDocument* Converter::convert( const QString &fileName ) { MobiDocument* newDocument=new MobiDocument(fileName); if (!newDocument->mobi()->isValid()) { emit error(i18n("Error while opening the Mobipocket document."), -1); delete newDocument; - return NULL; + return nullptr; } if (newDocument->mobi()->hasDRM()) { emit error(i18n("This book is protected by DRM and can be displayed only on designated device"), -1); delete newDocument; - return NULL; + return nullptr; } handleMetadata(newDocument->mobi()->metadata()); newDocument->setPageSize(QSizeF(600, 800)); QTextFrameFormat frameFormat; frameFormat.setMargin( 20 ); QTextFrame *rootFrame = newDocument->rootFrame(); rootFrame->setFrameFormat( frameFormat ); QMap > links; QMap targets; // go over whole document and add all tags to links or targets map for (QTextBlock it = newDocument->begin(); it != newDocument->end(); it = it.next()) for (QTextBlock::iterator fit=it.begin(); !fit.atEnd(); ++fit) { QTextFragment frag=fit.fragment(); QTextCharFormat format=frag.charFormat(); if (!format.isAnchor()) continue; //link if (!format.anchorHref().isEmpty()) links[format.anchorHref()]= QPair(frag.position(), frag.position()+frag.length()); const QStringList anchors = format.anchorNames(); if (!anchors.isEmpty()) { // link targets for (const QString &name : anchors) targets[QLatin1Char('#')+name]=it; } } // create link actions QMapIterator > it(links); while (it.hasNext()) { it.next(); QUrl u(it.key()); // external or internal link if (!u.isRelative()) emit addAction(new Okular::BrowseAction(QUrl(it.key())), it.value().first, it.value().second); else { // is there valid target? if (!targets.contains( it.key() ) || !targets[it.key()].isValid()) continue; emit addAction(new Okular::GotoAction(QString(), calculateViewport( newDocument, targets[it.key()] )), it.value().first, it.value().second); } } return newDocument; } diff --git a/generators/plucker/unpluck/config.cpp b/generators/plucker/unpluck/config.cpp index 20562ece6..98611e0e9 100644 --- a/generators/plucker/unpluck/config.cpp +++ b/generators/plucker/unpluck/config.cpp @@ -1,404 +1,404 @@ /* -*- mode: c; indent-tabs-mode: nil; -*- * $Id: config.c,v 1.3 2003/12/28 20:59:21 chrish Exp $ * * config -- read and parse the Plucker config files * Copyright (c) 2002, Bill Janssen * * 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. * */ #if !defined(WIN32) #include /* for lseek, etc. */ #else #include #endif #include #include #include #include #include /* for fstat() */ #include /* for strndup() */ #include /* for errno */ #include /* for O_RDONLY */ #include /* for assert() */ #include "unpluck.h" #include "unpluckint.h" #define STRINGIFY(s) STRINGIFY2 (s) #define STRINGIFY2(s) #s #define MAX_LINE_SIZE 1023 #define COMMENT_CHARS "#;" #define SEGMENT_LEAD_CHAR '[' #define SEGMENT_END_CHAR ']' #define OPTION_SEPARATOR_CHARS "=:" -HashTable *SectionsTable = NULL; +HashTable *SectionsTable = nullptr; static HashTable* GetOrCreateSegment ( const char* name ) { HashTable* target; - if (SectionsTable == NULL) + if (SectionsTable == nullptr) SectionsTable = _plkr_NewHashTable (23); - if ((target = (HashTable*)_plkr_FindInTable (SectionsTable, name)) == NULL) { + if ((target = (HashTable*)_plkr_FindInTable (SectionsTable, name)) == nullptr) { target = _plkr_NewHashTable (53); _plkr_AddToTable (SectionsTable, name, target); } return target; } static int ReadConfigFile ( const char* filename ) { - HashTable* current_segment = NULL; + HashTable* current_segment = nullptr; FILE* fp = fopen (filename, "r"); char* ptr; char* str_end; char* str_begin; char* charptr; char* current_option; char* option_value; char linebuf[MAX_LINE_SIZE + 1]; int linelen; int len2; int buf_index; int status; int line_number; - if (fp == NULL) { + if (fp == nullptr) { _plkr_message ("Can't open config file %s", filename); return 0; } current_segment = GetOrCreateSegment ("default"); - current_option = NULL; + current_option = nullptr; status = 1; /* optimistic */ line_number = 0; while (true) { ptr = fgets (linebuf, sizeof (linebuf) - 1, fp); - if (ptr == NULL) + if (ptr == nullptr) break; line_number += 1; linebuf[strlen (linebuf) - 1] = 0; /* strip newline */ if (linebuf[strlen (linebuf) - 1] == '\r') linebuf[strlen (linebuf) - 1] = 0; /* strip carriage return */ /* fprintf (stderr, "%s:%d: line is '%s'\n", filename, line_number, linebuf); */ linelen = strlen (linebuf); for (buf_index = 0; linebuf[buf_index] != 0; buf_index++) if (!isspace (linebuf[buf_index])) break; if (linebuf[buf_index] == 0) /* blank line */ continue; - if ((strchr (COMMENT_CHARS, linebuf[0]) != NULL) || + if ((strchr (COMMENT_CHARS, linebuf[0]) != nullptr) || (strncmp (linebuf, "rem", 3) == 0) || (strncmp (linebuf, "REM", 3) == 0)) /* comment */ continue; /* At this point we have a valid thing */ if (linebuf[buf_index] == SEGMENT_LEAD_CHAR) { if ((str_end = strchr (linebuf + buf_index + 1, - SEGMENT_END_CHAR)) == NULL) { + SEGMENT_END_CHAR)) == nullptr) { /* invalid segment line */ _plkr_message ("%s:%d: Invalid segment line '%s'", filename, line_number, linebuf); goto error_exit; } str_begin = linebuf + buf_index + 1; for (charptr = str_begin; charptr < str_end; charptr++) *charptr = tolower (*charptr); *str_end = 0; current_segment = GetOrCreateSegment (str_begin); /* fprintf (stderr, "Current segment is now %p (%s)\n", current_segment, str_begin); */ if (current_option) free (current_option); - current_option = NULL; + current_option = nullptr; } else if ((linebuf[0] == ' ' || linebuf[0] == '\t') - && current_option != NULL) { + && current_option != nullptr) { /* continuation line */ str_begin = (char *) _plkr_RemoveFromTable (current_segment, current_option); for (str_end = linebuf + strlen (linebuf) - 1; str_end > linebuf && isspace (*str_end); str_end--); charptr = (char *) malloc (strlen (str_begin) + (str_end - (linebuf + buf_index)) + 2); strcpy (charptr, str_begin); len2 = strlen (charptr); charptr[len2] = '\n'; strncpy (charptr + len2 + 1, linebuf + buf_index, str_end - (linebuf + buf_index)); charptr[len2 + (str_end - (linebuf + buf_index)) + 1] = '\0'; _plkr_AddToTable (current_segment, current_option, charptr); free (str_begin); } else if ((int)strcspn (linebuf, OPTION_SEPARATOR_CHARS) < linelen) { /* possible option line */ for (str_begin = linebuf + buf_index, ptr = str_begin; isalnum (*ptr) || (*ptr == '.') || (*ptr == '_') || (*ptr == '-'); ptr++); if (ptr == str_begin) { _plkr_message ("%s:%d: Invalid option line '%s'", filename, line_number, linebuf); goto error_exit; } for (charptr = str_begin; charptr < ptr; charptr++) *charptr = tolower (*charptr); str_end = ptr; while (isspace (*ptr) && (*ptr != '\0')) ptr++; - if (strchr (OPTION_SEPARATOR_CHARS, *ptr) != NULL) + if (strchr (OPTION_SEPARATOR_CHARS, *ptr) != nullptr) ptr++; else { _plkr_message ("%s:%d: Invalid option line '%s'", filename, line_number, linebuf); goto error_exit; } while (isspace (*ptr) && (*ptr != '\0')) ptr++; if (*ptr == 0) { _plkr_message ("%s:%d: Invalid option line '%s'", filename, line_number, linebuf); goto error_exit; } if (current_option) free (current_option); current_option = _plkr_strndup (str_begin, str_end - str_begin); option_value = _plkr_strndup (ptr, strlen (ptr)); ptr = (char *) _plkr_RemoveFromTable (current_segment, current_option); if (ptr) free (ptr); _plkr_AddToTable (current_segment, current_option, option_value); /* fprintf (stderr, "Added value '%s' for option '%p:%s'\n", option_value, current_segment, current_option); */ } else { _plkr_message ("%s:%d: Bad line '%s'", filename, line_number, linebuf); goto error_exit; } } good_exit: if (current_option) free (current_option); fclose (fp); return status; error_exit: status = 0; goto good_exit; } static void TryReadConfigFile ( const char* dir, const char* name ) { char* filename; - if (dir == NULL || name == NULL) + if (dir == nullptr || name == nullptr) return; filename = (char *) malloc (strlen (dir) + strlen (name) + 2); strcpy (filename, dir); strcpy (filename + strlen (filename), STRINGIFY (FILE_SEPARATOR_CHAR_S)); strcpy (filename + strlen (filename), name); if (!ReadConfigFile (filename)) _plkr_message ("Error reading config file %s", filename); free (filename); } static void InitializeConfigInfo () { const char *config_dir = STRINGIFY (PLUCKER_CONFIG_DIR); const char *system_config_file_name = STRINGIFY (SYS_CONFIG_FILE_NAME); const char *user_config_filename = STRINGIFY (USER_CONFIG_FILE_NAME); char *home = getenv ("HOME"); TryReadConfigFile (config_dir, system_config_file_name); - if (home != NULL) + if (home != nullptr) TryReadConfigFile (home, user_config_filename); } char* plkr_GetConfigString ( const char* section_name, const char* option_name, char* default_value ) { - char* value = NULL; + char* value = nullptr; HashTable* section; - if (SectionsTable == NULL) + if (SectionsTable == nullptr) InitializeConfigInfo (); - if (SectionsTable == NULL) + if (SectionsTable == nullptr) return default_value; - if (section_name != NULL) { + if (section_name != nullptr) { if ((section = (HashTable *) _plkr_FindInTable (SectionsTable, - section_name)) != NULL) + section_name)) != nullptr) value = (char *) _plkr_FindInTable (section, option_name); } - if (value == NULL && ((section_name == NULL) + if (value == nullptr && ((section_name == nullptr) || (strcmp (section_name, "default") != 0))) { if ((section = (HashTable *) _plkr_FindInTable (SectionsTable, STRINGIFY (OS_SECTION_NAME))) - != NULL) + != nullptr) value = (char *) _plkr_FindInTable (section, option_name); } - if (value == NULL && ((section_name == NULL) + if (value == nullptr && ((section_name == nullptr) || (strcmp (section_name, "default") != 0))) { if ((section = (HashTable *) _plkr_FindInTable (SectionsTable, - "default")) != NULL) + "default")) != nullptr) value = (char *) _plkr_FindInTable (section, option_name); } - return ((value == NULL) ? default_value : value); + return ((value == nullptr) ? default_value : value); } long int plkr_GetConfigInt ( const char* section_name, const char* option_name, long int default_value ) { - char* svalue = plkr_GetConfigString (section_name, option_name, NULL); + char* svalue = plkr_GetConfigString (section_name, option_name, nullptr); char* endptr; long int value; - if (svalue == NULL) + if (svalue == nullptr) return default_value; value = strtol (svalue, &endptr, 0); if (*endptr != 0) { _plkr_message ("Bad int value string '%s' for option %s:%s", svalue, (section_name ? section_name : "default"), option_name); return default_value; } else { return value; } } double plkr_GetConfigFloat (const char *section_name, const char *option_name, double default_value ) { - char* svalue = plkr_GetConfigString (section_name, option_name, NULL); + char* svalue = plkr_GetConfigString (section_name, option_name, nullptr); char* endptr; double value; - if (svalue == NULL) + if (svalue == nullptr) return default_value; value = strtod (svalue, &endptr); if (*endptr != 0) { _plkr_message ("Bad float value string '%s' for option %s:%s", svalue, (section_name ? section_name : "default"), option_name); return default_value; } else { return value; } } int plkr_GetConfigBoolean (const char *section_name, const char *option_name, int default_value ) { - char* svalue = plkr_GetConfigString (section_name, option_name, NULL); + char* svalue = plkr_GetConfigString (section_name, option_name, nullptr); - if (svalue == NULL) + if (svalue == nullptr) return default_value; if ((strcmp (svalue, "1") == 0) || (strcmp (svalue, "true") == 0) || (strcmp (svalue, "TRUE") == 0) || (strcmp (svalue, "on") == 0) || (strcmp (svalue, "ON") == 0) || (strcmp (svalue, "t") == 0) || (strcmp (svalue, "T") == 0) || (strcmp (svalue, "True") == 0)) return 1; else if ((strcmp (svalue, "0") == 0) || (strcmp (svalue, "false") == 0) || (strcmp (svalue, "FALSE") == 0) || (strcmp (svalue, "off") == 0) || (strcmp (svalue, "OFF") == 0) || (strcmp (svalue, "F") == 0) || (strcmp (svalue, "f") == 0) || (strcmp (svalue, "False") == 0)) return 0; else { _plkr_message ("Bad boolean value string '%s' for option %s:%s", svalue, (section_name ? section_name : "default"), option_name); return default_value; } } diff --git a/generators/plucker/unpluck/image.cpp b/generators/plucker/unpluck/image.cpp index 5ab1c6e34..b368998ab 100644 --- a/generators/plucker/unpluck/image.cpp +++ b/generators/plucker/unpluck/image.cpp @@ -1,549 +1,549 @@ /*************************************************************************** * Copyright (C) 2007 by Tobias Koenig * * * * Based on code written by Bill Janssen 2002 * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * ***************************************************************************/ #include #include #include #include /* This code requires the Independent JPEG Group libjpeg library, version 6b or later */ extern "C" { #include "jpeglib.h" } #include "unpluck.h" #include "image.h" #define GET_FUNCTION_CODE_TYPE(x) (((x)>>3) & 0x1F) #define GET_FUNCTION_CODE_DATALEN(x) ((x) & 0x7) #define CELLS(row,col) cells[row*cols+col] /***********************************************************************/ /***********************************************************************/ /***** *****/ /***** Code to decode the Palm image format to JPEG *****/ /***** *****/ /***********************************************************************/ /***********************************************************************/ #define READ_BIGENDIAN_SHORT(p) (((p)[0] << 8)|((p)[1])) #define READ_BIGENDIAN_LONG(p) (((p)[0] << 24)|((p)[1] << 16)|((p)[2] << 8)|((p)[3])) #define PALM_IS_COMPRESSED_FLAG 0x8000 #define PALM_HAS_COLORMAP_FLAG 0x4000 #define PALM_HAS_TRANSPARENCY_FLAG 0x2000 #define PALM_DIRECT_COLOR_FLAG 0x0400 #define PALM_4_BYTE_FIELD_FLAG 0x0200 #define PALM_COMPRESSION_SCANLINE 0x00 #define PALM_COMPRESSION_RLE 0x01 #define PALM_COMPRESSION_PACKBITS 0x02 #define PALM_COMPRESSION_NONE 0xFF #define PALM_COLORMAP_SIZE 232 typedef struct { unsigned char red; unsigned char green; unsigned char blue; } ColorMapEntry; static ColorMapEntry Palm8BitColormap[] = { {255, 255, 255}, {255, 204, 255}, {255, 153, 255}, {255, 102, 255}, {255, 51, 255}, {255, 0, 255}, {255, 255, 204}, {255, 204, 204}, {255, 153, 204}, {255, 102, 204}, {255, 51, 204}, {255, 0, 204}, {255, 255, 153}, {255, 204, 153}, {255, 153, 153}, {255, 102, 153}, {255, 51, 153}, {255, 0, 153}, {204, 255, 255}, {204, 204, 255}, {204, 153, 255}, {204, 102, 255}, {204, 51, 255}, {204, 0, 255}, {204, 255, 204}, {204, 204, 204}, {204, 153, 204}, {204, 102, 204}, {204, 51, 204}, {204, 0, 204}, {204, 255, 153}, {204, 204, 153}, {204, 153, 153}, {204, 102, 153}, {204, 51, 153}, {204, 0, 153}, {153, 255, 255}, {153, 204, 255}, {153, 153, 255}, {153, 102, 255}, {153, 51, 255}, {153, 0, 255}, {153, 255, 204}, {153, 204, 204}, {153, 153, 204}, {153, 102, 204}, {153, 51, 204}, {153, 0, 204}, {153, 255, 153}, {153, 204, 153}, {153, 153, 153}, {153, 102, 153}, {153, 51, 153}, {153, 0, 153}, {102, 255, 255}, {102, 204, 255}, {102, 153, 255}, {102, 102, 255}, {102, 51, 255}, {102, 0, 255}, {102, 255, 204}, {102, 204, 204}, {102, 153, 204}, {102, 102, 204}, {102, 51, 204}, {102, 0, 204}, {102, 255, 153}, {102, 204, 153}, {102, 153, 153}, {102, 102, 153}, {102, 51, 153}, {102, 0, 153}, { 51, 255, 255}, { 51, 204, 255}, { 51, 153, 255}, { 51, 102, 255}, { 51, 51, 255}, { 51, 0, 255}, { 51, 255, 204}, { 51, 204, 204}, { 51, 153, 204}, { 51, 102, 204}, { 51, 51, 204}, { 51, 0, 204}, { 51, 255, 153}, { 51, 204, 153}, { 51, 153, 153}, { 51, 102, 153}, { 51, 51, 153}, { 51, 0, 153}, { 0, 255, 255}, { 0, 204, 255}, { 0, 153, 255}, { 0, 102, 255}, { 0, 51, 255}, { 0, 0, 255}, { 0, 255, 204}, { 0, 204, 204}, { 0, 153, 204}, { 0, 102, 204}, { 0, 51, 204}, { 0, 0, 204}, { 0, 255, 153}, { 0, 204, 153}, { 0, 153, 153}, { 0, 102, 153}, { 0, 51, 153}, { 0, 0, 153}, {255, 255, 102}, {255, 204, 102}, {255, 153, 102}, {255, 102, 102}, {255, 51, 102}, {255, 0, 102}, {255, 255, 51}, {255, 204, 51}, {255, 153, 51}, {255, 102, 51}, {255, 51, 51}, {255, 0, 51}, {255, 255, 0}, {255, 204, 0}, {255, 153, 0}, {255, 102, 0}, {255, 51, 0}, {255, 0, 0}, {204, 255, 102}, {204, 204, 102}, {204, 153, 102}, {204, 102, 102}, {204, 51, 102}, {204, 0, 102}, {204, 255, 51}, {204, 204, 51}, {204, 153, 51}, {204, 102, 51}, {204, 51, 51}, {204, 0, 51}, {204, 255, 0}, {204, 204, 0}, {204, 153, 0}, {204, 102, 0}, {204, 51, 0}, {204, 0, 0}, {153, 255, 102}, {153, 204, 102}, {153, 153, 102}, {153, 102, 102}, {153, 51, 102}, {153, 0, 102}, {153, 255, 51}, {153, 204, 51}, {153, 153, 51}, {153, 102, 51}, {153, 51, 51}, {153, 0, 51}, {153, 255, 0}, {153, 204, 0}, {153, 153, 0}, {153, 102, 0}, {153, 51, 0}, {153, 0, 0}, {102, 255, 102}, {102, 204, 102}, {102, 153, 102}, {102, 102, 102}, {102, 51, 102}, {102, 0, 102}, {102, 255, 51}, {102, 204, 51}, {102, 153, 51}, {102, 102, 51}, {102, 51, 51}, {102, 0, 51}, {102, 255, 0}, {102, 204, 0}, {102, 153, 0}, {102, 102, 0}, {102, 51, 0}, {102, 0, 0}, { 51, 255, 102}, { 51, 204, 102}, { 51, 153, 102}, { 51, 102, 102}, { 51, 51, 102}, { 51, 0, 102}, { 51, 255, 51}, { 51, 204, 51}, { 51, 153, 51}, { 51, 102, 51}, { 51, 51, 51}, { 51, 0, 51}, { 51, 255, 0}, { 51, 204, 0}, { 51, 153, 0}, { 51, 102, 0}, { 51, 51, 0}, { 51, 0, 0}, { 0, 255, 102}, { 0, 204, 102}, { 0, 153, 102}, { 0, 102, 102}, { 0, 51, 102}, { 0, 0, 102}, { 0, 255, 51}, { 0, 204, 51}, { 0, 153, 51}, { 0, 102, 51}, { 0, 51, 51}, { 0, 0, 51}, { 0, 255, 0}, { 0, 204, 0}, { 0, 153, 0}, { 0, 102, 0}, { 0, 51, 0}, { 17, 17, 17}, { 34, 34, 34}, { 68, 68, 68}, { 85, 85, 85}, {119, 119, 119}, {136, 136, 136}, {170, 170, 170}, {187, 187, 187}, {221, 221, 221}, {238, 238, 238}, {192, 192, 192}, {128, 0, 0}, {128, 0, 128}, { 0, 128, 0}, { 0, 128, 128}, { 0, 0, 0}, { 0, 0, 0}, { 0, 0, 0}, { 0, 0, 0}, { 0, 0, 0}, { 0, 0, 0}, { 0, 0, 0}, { 0, 0, 0}, { 0, 0, 0}, { 0, 0, 0}, { 0, 0, 0}, { 0, 0, 0}, { 0, 0, 0}, { 0, 0, 0}, { 0, 0, 0}, { 0, 0, 0}, { 0, 0, 0}, { 0, 0, 0}, { 0, 0, 0}, { 0, 0, 0}, { 0, 0, 0}, { 0, 0, 0}, { 0, 0, 0}, { 0, 0, 0}, { 0, 0, 0}, { 0, 0, 0} }; static ColorMapEntry Palm1BitColormap[] = { {255, 255, 255}, { 0, 0, 0} }; static ColorMapEntry Palm2BitColormap[] = { {255, 255, 255}, {192, 192, 192}, {128, 128, 128}, { 0, 0, 0} }; static ColorMapEntry Palm4BitColormap[] = { {255, 255, 255}, {238, 238, 238}, {221, 221, 221}, {204, 204, 204}, {187, 187, 187}, {170, 170, 170}, {153, 153, 153}, {136, 136, 136}, {119, 119, 119}, {102, 102, 102}, { 85, 85, 85}, { 68, 68, 68}, { 51, 51, 51}, { 34, 34, 34}, { 17, 17, 17}, { 0, 0, 0} }; bool TranscribePalmImageToJPEG ( unsigned char *image_bytes_in, QImage &image ) { unsigned int width; unsigned int height; unsigned int bytes_per_row; unsigned int flags; // unsigned int next_depth_offset; unsigned int bits_per_pixel; // unsigned int version; // unsigned int transparent_index; unsigned int compression_type; unsigned int i; unsigned int j; unsigned int inval; unsigned int inbit; unsigned int mask; unsigned int incount; unsigned int palm_red_bits = 0; unsigned int palm_green_bits = 0; unsigned int palm_blue_bits = 0; unsigned char* palm_ptr; // unsigned char* x_ptr; // unsigned char* imagedata = 0; unsigned char* inbyte; unsigned char* rowbuf; unsigned char* lastrow; unsigned char* imagedatastart; unsigned char* palmimage; ColorMapEntry *colormap; JSAMPLE* jpeg_row; struct jpeg_compress_struct cinfo; struct jpeg_error_mgr jerr; JSAMPROW row_pointer[1];/* pointer to JSAMPLE row[s] */ palmimage = image_bytes_in; width = READ_BIGENDIAN_SHORT (palmimage + 0); height = READ_BIGENDIAN_SHORT (palmimage + 2); bytes_per_row = READ_BIGENDIAN_SHORT (palmimage + 4); flags = READ_BIGENDIAN_SHORT (palmimage + 6); bits_per_pixel = palmimage[8]; // version = palmimage[9]; // next_depth_offset = READ_BIGENDIAN_SHORT (palmimage + 10); // transparent_index = palmimage[12]; compression_type = palmimage[13]; /* bytes 14 and 15 are reserved by Palm and always 0 */ if (compression_type == PALM_COMPRESSION_PACKBITS) { return false; } else if ((compression_type != PALM_COMPRESSION_NONE) && (compression_type != PALM_COMPRESSION_RLE) && (compression_type != PALM_COMPRESSION_SCANLINE)) { return false; } /* as of PalmOS 4.0, there are 6 different kinds of Palm pixmaps: 1, 2, or 4 bit grayscale 8-bit StaticColor using the Palm standard colormap 8-bit PseudoColor using a user-specified colormap 16-bit DirectColor using 5 bits for red, 6 for green, and 5 for blue Each of these can be compressed with one of four compression schemes, "RLE", "Scanline", "PackBits", or none. We begin by constructing the colormap. */ if (flags & PALM_HAS_COLORMAP_FLAG) { return false; } else if (bits_per_pixel == 1) { colormap = Palm1BitColormap; imagedatastart = palmimage + 16; } else if (bits_per_pixel == 2) { colormap = Palm2BitColormap; imagedatastart = palmimage + 16; } else if (bits_per_pixel == 4) { colormap = Palm4BitColormap; imagedatastart = palmimage + 16; } else if (bits_per_pixel == 8) { colormap = Palm8BitColormap; imagedatastart = palmimage + 16; } else if (bits_per_pixel == 16 && (flags & PALM_DIRECT_COLOR_FLAG)) { - colormap = NULL; + colormap = nullptr; palm_red_bits = palmimage[16]; palm_green_bits = palmimage[17]; palm_blue_bits = palmimage[18]; if (palm_blue_bits > 8 || palm_green_bits > 8 || palm_red_bits > 8) { return false; } if (bits_per_pixel > (8 * sizeof (unsigned long))) { return false; } imagedatastart = palmimage + 24; } else { return false; } QTemporaryFile tempFile; tempFile.open(); FILE *outfile = fopen( QFile::encodeName( tempFile.fileName() ).constData(), "w" ); if ( !outfile ) return false; /* now create the JPEG image row buffer */ jpeg_row = (JSAMPLE *) malloc (sizeof (JSAMPLE) * (width * 3)); /* Use standard JPEG error processing */ cinfo.err = jpeg_std_error (&jerr); /* Initialize the JPEG compression object. */ jpeg_create_compress (&cinfo); jpeg_stdio_dest (&cinfo, outfile); cinfo.image_width = width; /* image width and height, in pixels */ cinfo.image_height = height; cinfo.input_components = 3; /* # of color components per pixel */ cinfo.in_color_space = JCS_RGB; /* colorspace of input image */ jpeg_set_defaults (&cinfo); jpeg_set_quality (&cinfo, 100, true /* limit to baseline-JPEG values */ ); row_pointer[0] = &jpeg_row[0]; jpeg_start_compress (&cinfo, true); /* row by row, uncompress the Palm image and copy it to the JPEG buffer */ rowbuf = (unsigned char *) malloc (bytes_per_row * width); lastrow = (unsigned char *) malloc (bytes_per_row * width); for (i = 0, palm_ptr = imagedatastart/*, x_ptr = imagedata*/; i < height; ++i) { /* first, uncompress the Palm image */ if ((flags & PALM_IS_COMPRESSED_FLAG) && (compression_type == PALM_COMPRESSION_RLE)) { for (j = 0; j < bytes_per_row;) { incount = *palm_ptr++; inval = *palm_ptr++; if (incount + j <= bytes_per_row * width) { memset (rowbuf + j, inval, incount); j += incount; } else { free (rowbuf); free (lastrow); free (jpeg_row); jpeg_destroy_compress (&cinfo); fclose( outfile ); return false; } } } else if ((flags & PALM_IS_COMPRESSED_FLAG) && (compression_type == PALM_COMPRESSION_SCANLINE)) { for (j = 0; j < bytes_per_row; j += 8) { incount = *palm_ptr++; inval = ((bytes_per_row - j) < 8) ? (bytes_per_row - j) : 8; for (inbit = 0; inbit < inval; inbit += 1) { if (incount & (1 << (7 - inbit))) rowbuf[j + inbit] = *palm_ptr++; else rowbuf[j + inbit] = lastrow[j + inbit]; } } memcpy (lastrow, rowbuf, bytes_per_row); } else if (((flags & PALM_IS_COMPRESSED_FLAG) && (compression_type == PALM_COMPRESSION_NONE)) || (flags & PALM_IS_COMPRESSED_FLAG) == 0) { memcpy (rowbuf, palm_ptr, bytes_per_row); palm_ptr += bytes_per_row; } /* next, write it to the GDK bitmap */ if (colormap) { mask = (1 << bits_per_pixel) - 1; for (inbit = 8 - bits_per_pixel, inbyte = rowbuf, j = 0; j < width; ++j) { inval = ((*inbyte) & (mask << inbit)) >> inbit; /* correct for oddity of the 8-bit color Palm pixmap... */ if ((bits_per_pixel == 8) && (inval == 0xFF)) inval = 231; /* now lookup the correct color and set the pixel in the GTK bitmap */ jpeg_row[(j * 3) + 0] = colormap[inval].red; jpeg_row[(j * 3) + 1] = colormap[inval].green; jpeg_row[(j * 3) + 2] = colormap[inval].blue; if (!inbit) { ++inbyte; inbit = 8 - bits_per_pixel; } else { inbit -= bits_per_pixel; } } } else if (!colormap && bits_per_pixel == 16) { for (inbyte = rowbuf, j = 0; j < width; ++j) { inval = (inbyte[0] << 8) | inbyte[1]; jpeg_row[(j * 3) + 0] = (inval >> (bits_per_pixel - palm_red_bits)) & ((1 << palm_red_bits) - 1); jpeg_row[(j * 3) + 1] = (inval >> palm_blue_bits) & ((1 << palm_green_bits) - 1); jpeg_row[(j * 3) + 2] = (inval >> 0) & ((1 << palm_blue_bits) - 1); inbyte += 2; } } (void) jpeg_write_scanlines (&cinfo, row_pointer, 1); } free (rowbuf); free (lastrow); free (jpeg_row); jpeg_finish_compress (&cinfo); jpeg_destroy_compress (&cinfo); fclose( outfile ); return image.load( tempFile.fileName() ); } typedef struct { unsigned int width; unsigned int height; unsigned int bytes_per_row; unsigned int flags; unsigned int next_depth_offset; unsigned int bits_per_pixel; unsigned int version; unsigned int transparent_index; unsigned int compression_type; unsigned int palm_red_bits; unsigned int palm_green_bits; unsigned int palm_blue_bits; unsigned char* bytes; } PALMPIX; bool TranscribeMultiImageRecord ( plkr_Document* doc, QImage &image, unsigned char* bytes ) { - unsigned char* pbytes = 0; - unsigned char* outbytes = 0; - unsigned char* outptr = 0; + unsigned char* pbytes = nullptr; + unsigned char* outbytes = nullptr; + unsigned char* outptr = nullptr; unsigned char* ptr = &bytes[12]; plkr_DataRecordType ptype; - PALMPIX* cells = 0; - PALMPIX* acell = 0; + PALMPIX* cells = nullptr; + PALMPIX* acell = nullptr; unsigned int record_id = 0; int plen = 0; unsigned int x = 0; unsigned int y = 0; unsigned int cols = 0; unsigned int rows = 0; unsigned int width = 0; unsigned int height = 0; unsigned int bytes_per_row = 0; unsigned int flags = 0; unsigned int bits_per_pixel = 0; unsigned int version = 0; unsigned int transparent_index = 0; unsigned int compression_type = 0; unsigned int palm_red_bits = 0; unsigned int palm_green_bits = 0; unsigned int palm_blue_bits = 0; unsigned int outlen = 0; unsigned int offset = 0; bool status = true; cols = (bytes[8] << 8) + bytes[9]; rows = (bytes[10] << 8) + bytes[11]; cells = (PALMPIX *) calloc (cols * rows, sizeof (PALMPIX)); height = 0; for (y = 0; y < rows; y++) { width = 0; bytes_per_row = 0; for (x = 0; x < cols; x++) { acell = &CELLS (y, x); record_id = (ptr[0] << 8) + ptr[1]; ptr += 2; pbytes = plkr_GetRecordBytes (doc, record_id, &plen, &ptype); - if (pbytes == NULL) { + if (pbytes == nullptr) { free (cells); return false; } pbytes += 8; acell->width = READ_BIGENDIAN_SHORT (&pbytes[0]); width += acell->width; acell->height = READ_BIGENDIAN_SHORT (&pbytes[2]); acell->bytes_per_row = READ_BIGENDIAN_SHORT (&pbytes[4]); bytes_per_row += acell->bytes_per_row; acell->flags = READ_BIGENDIAN_SHORT (&pbytes[6]); flags = acell->flags; acell->bits_per_pixel = pbytes[8]; bits_per_pixel = acell->bits_per_pixel; acell->version = pbytes[9]; version = acell->version; acell->next_depth_offset = READ_BIGENDIAN_SHORT (&pbytes[10]); acell->transparent_index = pbytes[12]; transparent_index = acell->transparent_index; acell->compression_type = pbytes[13]; compression_type = acell->compression_type; if (acell->flags & PALM_HAS_COLORMAP_FLAG) { free (cells); return false; } acell->bytes = pbytes + 16; offset = 16; if (acell->bits_per_pixel == 16 && (acell->flags & PALM_DIRECT_COLOR_FLAG)) { acell->palm_red_bits = pbytes[16]; palm_red_bits = acell->palm_red_bits; acell->palm_green_bits = pbytes[17]; palm_green_bits = acell->palm_green_bits; acell->palm_blue_bits = pbytes[18]; palm_blue_bits = acell->palm_blue_bits; acell->bytes = pbytes + 24; offset = 24; } } height += acell->height; } outlen = bytes_per_row * height + offset; outbytes = (unsigned char *) malloc (outlen); outptr = outbytes; *outptr++ = width >> 8; *outptr++ = width; *outptr++ = height >> 8; *outptr++ = height; *outptr++ = bytes_per_row >> 8; *outptr++ = bytes_per_row; *outptr++ = flags >> 8; *outptr++ = flags; *outptr++ = bits_per_pixel; *outptr++ = version; *outptr++ = 0; /* next_depth_offset */ *outptr++ = 0; *outptr++ = transparent_index; *outptr++ = compression_type; *outptr++ = 0; *outptr++ = 0; if (acell->bits_per_pixel == 16 && (acell->flags & PALM_DIRECT_COLOR_FLAG)) { *outptr++ = palm_red_bits; *outptr++ = palm_green_bits; *outptr++ = palm_blue_bits; *outptr++ = 0; *outptr++ = 0; *outptr++ = 0; *outptr++ = 0; *outptr++ = 0; } for (y = 0; y < rows; y++) { int i, h; acell = &CELLS (y, 0); h = acell->height; for (i = 0; i < h; i++) { for (x = 0; x < cols; x++) { acell = &CELLS (y, x); memcpy (outptr, acell->bytes, acell->bytes_per_row); acell->bytes += acell->bytes_per_row; outptr += acell->bytes_per_row; } } } status = TranscribePalmImageToJPEG (outbytes, image); free (outbytes); free (cells); return status; } diff --git a/generators/plucker/unpluck/qunpluck.cpp b/generators/plucker/unpluck/qunpluck.cpp index f312841a4..dda824f11 100644 --- a/generators/plucker/unpluck/qunpluck.cpp +++ b/generators/plucker/unpluck/qunpluck.cpp @@ -1,1207 +1,1207 @@ /*************************************************************************** * Copyright (C) 2007 by Tobias Koenig * * * * Based on code written by Bill Janssen 2002 * * * * 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 "qunpluck.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "image.h" #define GET_FUNCTION_CODE_TYPE(x) (((x)>>3) & 0x1F) #define GET_FUNCTION_CODE_DATALEN(x) ((x) & 0x7) #define CELLS(row,col) cells[row*cols+col] #define READ_BIGENDIAN_SHORT(p) (((p)[0] << 8)|((p)[1])) #define READ_BIGENDIAN_LONG(p) (((p)[0] << 24)|((p)[1] << 16)|((p)[2] << 8)|((p)[3])) /* static void LinkRecords ( char* dir ) { RecordNode* ptr; char* realfilename; char* linkname; realfilename = (char*)malloc (strlen (dir) + 20); linkname = (char*)malloc (strlen (dir) + 20); for (ptr = records; ptr != NULL; ptr = ptr->next) { if (ptr->page_id != ptr->index) { sprintf (realfilename, "%s/r%d.html", dir, ptr->page_id); sprintf (linkname, "%s/r%d.html", dir, ptr->index); link (realfilename, linkname); } } free (realfilename); free (linkname); } */ class Context { public: int recordId; QTextDocument *document; QTextCursor *cursor; QStack stack; QList images; QString linkUrl; int linkStart; int linkPage; }; class RecordNode { public: int index; int page_id; bool done; }; static Okular::DocumentViewport calculateViewport( QTextDocument *document, const QTextBlock &block ) { if ( !block.isValid() ) return Okular::DocumentViewport(); const QRectF rect = document->documentLayout()->blockBoundingRect( block ); const QSizeF size = document->size(); int page = qRound( rect.y() ) / qRound( size.height() ); Okular::DocumentViewport viewport( page ); viewport.rePos.normalizedX = (double)rect.x() / (double)size.width(); viewport.rePos.normalizedY = (double)rect.y() / (double)size.height(); viewport.rePos.enabled = true; viewport.rePos.pos = Okular::DocumentViewport::Center; return viewport; } QUnpluck::QUnpluck() - : mDocument( 0 ) + : mDocument( nullptr ) { } QUnpluck::~QUnpluck() { mLinks.clear(); mNamedTargets.clear(); mPages.clear(); } bool QUnpluck::open( const QString &fileName ) { mLinks.clear(); mNamedTargets.clear(); mPages.clear(); mDocument = plkr_OpenDBFile( QFile::encodeName( fileName ).data() ); if ( !mDocument ) { mErrorString = QObject::tr( "Unable to open document" ); return false; } // bool status = true; mInfo.insert( QStringLiteral("name"), QString::fromLocal8Bit(plkr_GetName( mDocument ) )); mInfo.insert( QStringLiteral("title"), QString::fromLocal8Bit(plkr_GetTitle( mDocument ) )); mInfo.insert( QStringLiteral("author"), QString::fromLocal8Bit(plkr_GetAuthor( mDocument ) )); mInfo.insert( QStringLiteral("time"), QDateTime::fromSecsSinceEpoch( plkr_GetPublicationTime( mDocument ) ).toString() ); AddRecord( plkr_GetHomeRecordID( mDocument ) ); int number = GetNextRecordNumber(); while ( number > 0 ) { /*status = */TranscribeRecord( number ); number = GetNextRecordNumber (); } // Iterate over all records again to add those which aren't linked directly for ( int i = 1; i < plkr_GetRecordCount( mDocument ); ++i ) AddRecord( plkr_GetUidForIndex( mDocument, i ) ); number = GetNextRecordNumber(); while ( number > 0 ) { /*status = */TranscribeRecord( number ); number = GetNextRecordNumber (); } for ( int i = 0; i < mRecords.count(); ++i ) delete mRecords[ i ]; mRecords.clear(); plkr_CloseDoc( mDocument ); /** * Calculate hash map */ QHash pageHash; for ( int i = 0; i < mContext.count(); ++i ) pageHash.insert( mContext[ i ]->recordId, i ); /** * Convert ids */ for ( int i = 0; i < mContext.count(); ++i ) { Context *context = mContext[ i ]; for ( int j = 0; j < context->images.count(); ++j ) { int imgNumber = context->images[ j ]; context->document->addResource( QTextDocument::ImageResource, QUrl( QStringLiteral( "%1.jpg" ).arg( imgNumber ) ), mImages[ imgNumber ] ); } mPages.append( context->document ); } qDeleteAll( mContext ); mContext.clear(); // convert record_id into page for ( int i = 0; i < mLinks.count(); ++i ) { mLinks[ i ].page = pageHash[ mLinks[ i ].page ]; if ( mLinks[ i ].url.startsWith( QLatin1String("page:") ) ) { int page = mLinks[ i ].url.midRef( 5 ).toInt(); Okular::DocumentViewport viewport( pageHash[ page ] ); viewport.rePos.normalizedX = 0; viewport.rePos.normalizedY = 0; viewport.rePos.enabled = true; viewport.rePos.pos = Okular::DocumentViewport::TopLeft; mLinks[ i ].link = new Okular::GotoAction( QString(), viewport ); } else if ( mLinks[ i ].url.startsWith( QLatin1String("para:") ) ) { QPair data = mNamedTargets[ mLinks[ i ].url ]; QTextDocument *document = mPages[ mLinks[ i ].page ]; Okular::DocumentViewport viewport = calculateViewport( document, data.second ); mLinks[ i ].link = new Okular::GotoAction( QString(), viewport ); } else { mLinks[ i ].link = new Okular::BrowseAction( QUrl(mLinks[ i ].url) ); } } return true; } int QUnpluck::GetNextRecordNumber() { int index = 0; for ( int pos = 0; pos < mRecords.count(); ++pos ) { if ( !mRecords[ pos ]->done ) { index = mRecords[ pos ]->index; break; } } return index; } int QUnpluck::GetPageID( int index ) { for ( int pos = 0; pos < mRecords.count(); ++pos ) { if ( mRecords[ pos ]->index == index ) { return mRecords[ pos ]->page_id; } } return 0; } void QUnpluck::AddRecord( int index ) { for ( int pos = 0; pos < mRecords.count(); ++pos ) { if ( mRecords[ pos ]->index == index ) { return; } } RecordNode *node = new RecordNode; node->done = false; node->index = index; node->page_id = index; mRecords.append( node ); } void QUnpluck::MarkRecordDone( int index ) { for ( int pos = 0; pos < mRecords.count(); ++pos ) { if ( mRecords[ pos ]->index == index ) { mRecords[ pos ]->done = true; return; } } AddRecord( index ); MarkRecordDone( index ); } void QUnpluck::SetPageID( int index, int page_id ) { for ( int pos = 0; pos < mRecords.count(); ++pos ) { if ( mRecords[ pos ]->index == index ) { mRecords[ pos ]->page_id = page_id; return; } } AddRecord( index ); SetPageID( index, page_id ); } QString QUnpluck::MailtoURLFromBytes( unsigned char* record_data ) { unsigned char* bytes = record_data + 8; int to_offset = (bytes[0] << 8) + bytes[1]; int cc_offset = (bytes[2] << 8) + bytes[3]; int subject_offset = (bytes[4] << 8) + bytes[5]; int body_offset = (bytes[6] << 8) + bytes[7]; QString url( QStringLiteral("mailto:") ); if ( to_offset != 0 ) url += QString::fromLatin1( (char *)(bytes + to_offset) ); if ( (cc_offset != 0) || (subject_offset != 0) || (body_offset != 0) ) url += QLatin1String( "?" ); if ( cc_offset != 0 ) url += QLatin1String( "cc=" ) + QString::fromLatin1( (char *)(bytes + cc_offset) ); if ( subject_offset != 0 ) url += QLatin1String( "subject=" ) + QString::fromLatin1( (char *)(bytes + subject_offset) ); if ( body_offset != 0 ) url += QLatin1String( "body=" ) + QString::fromLatin1( (char *)(bytes + body_offset) ); return url; } QImage QUnpluck::TranscribeImageRecord( unsigned char* bytes ) { QImage image; TranscribePalmImageToJPEG( bytes + 8, image ); return image; } void QUnpluck::DoStyle( Context* context, int style, bool start ) { if ( start ) { QTextCharFormat format( context->cursor->charFormat() ); context->stack.push( format ); int pointSize = qRound( format.fontPointSize() ); switch (style) { case 1: format.setFontWeight( QFont::Bold ); pointSize += 3; break; case 2: format.setFontWeight( QFont::Bold ); pointSize += 2; break; case 3: format.setFontWeight( QFont::Bold ); pointSize += 1; break; case 4: format.setFontWeight( QFont::Bold ); break; case 5: format.setFontWeight( QFont::Bold ); pointSize += -1; break; case 6: format.setFontWeight( QFont::Bold ); pointSize += -2; break; case 7: format.setFontWeight( QFont::Bold ); break; case 8: format.setFontFamily( QStringLiteral( "Courier New,courier" ) ); break; } format.setFontPointSize( qMax( pointSize, 1 ) ); context->cursor->setCharFormat( format ); } else { if ( !context->stack.isEmpty() ) context->cursor->setCharFormat( context->stack.pop() ); } } void QUnpluck::ParseText ( plkr_Document* doc, unsigned char* ptr, int text_len, int* font, int* style, Context* context ) { unsigned char* end; int fctype; int fclen; end = ptr + text_len; while (ptr < end) { if (ptr[0]) { context->cursor->insertText( QString::fromLocal8Bit( (char*)ptr ) ); ptr += strlen ((char*)ptr); } else { fctype = GET_FUNCTION_CODE_TYPE (ptr[1]); fclen = 2 + GET_FUNCTION_CODE_DATALEN (ptr[1]); switch (fctype) { case PLKR_TFC_LINK: switch (fclen) { case 4: /* ANCHOR_BEGIN */ { int record_id = (ptr[2] << 8) + ptr[3]; /** TODO: plkr_DataRecordType type = (plkr_DataRecordType)plkr_GetRecordType (doc, record_id); if (type == PLKR_DRTYPE_IMAGE || type == PLKR_DRTYPE_IMAGE_COMPRESSED) output += QString( "" ).arg(record_id); else output += QString( "" ).arg(record_id); */ AddRecord (record_id); } break; case 2: /* ANCHOR_END */ //TODO: output += QString( "" ); break; } ptr += fclen; break; case PLKR_TFC_FONT: DoStyle (context, *style, false); *style = ptr[2]; DoStyle (context, *style, true); ptr += fclen; break; case PLKR_TFC_NEWLINE: { // TODO: remove the setCharFormat when Qt is fixed QTextCharFormat format( context->cursor->charFormat() ); context->cursor->insertText( QStringLiteral("\n") ); context->cursor->setCharFormat( format ); ptr += fclen; break; } case PLKR_TFC_BITALIC: { QTextCharFormat format( context->cursor->charFormat() ); format.setFontItalic( true ); context->cursor->setCharFormat( format ); ptr += fclen; break; } case PLKR_TFC_EITALIC: { QTextCharFormat format( context->cursor->charFormat() ); format.setFontItalic( false ); context->cursor->setCharFormat( format ); ptr += fclen; break; } case PLKR_TFC_COLOR: if (*font) { (*font)--; if ( !context->stack.isEmpty() ) context->cursor->setCharFormat( context->stack.pop() ); } { QTextCharFormat format( context->cursor->charFormat() ); context->stack.push( format ); format.setForeground( QColor((ptr[2] << 16), (ptr[3] << 8), ptr[4]) ); context->cursor->setCharFormat( format ); } (*font)++; ptr += fclen; break; case PLKR_TFC_BULINE: { QTextCharFormat format( context->cursor->charFormat() ); format.setFontUnderline( true ); context->cursor->setCharFormat( format ); ptr += fclen; } break; case PLKR_TFC_EULINE: { QTextCharFormat format( context->cursor->charFormat() ); format.setFontUnderline( false ); context->cursor->setCharFormat( format ); ptr += fclen; } break; case PLKR_TFC_BSTRIKE: { QTextCharFormat format( context->cursor->charFormat() ); format.setFontStrikeOut( true ); context->cursor->setCharFormat( format ); ptr += fclen; break; } case PLKR_TFC_ESTRIKE: { QTextCharFormat format( context->cursor->charFormat() ); format.setFontStrikeOut( false ); context->cursor->setCharFormat( format ); ptr += fclen; break; } case PLKR_TFC_TABLE: if (fclen == 4) { int record_id, datalen; plkr_DataRecordType type = (plkr_DataRecordType)0; - unsigned char *bytes = NULL; + unsigned char *bytes = nullptr; record_id = (ptr[2] << 8) + ptr[3]; bytes = plkr_GetRecordBytes (doc, record_id, &datalen, &type); TranscribeTableRecord (doc, context, bytes); } ptr += fclen; break; default: ptr += fclen; } } } } bool QUnpluck::TranscribeTableRecord ( plkr_Document* doc, Context* context, unsigned char* bytes ) { unsigned char* ptr = &bytes[24]; unsigned char* end; // char* align_names[] = { "left", "right", "center" }; // bool in_row = false; // int cols; int size; // int rows; // int border; int record_id; // int align; int text_len; // int colspan; // int rowspan; int font = 0; int style = 0; int fctype; int fclen; // long border_color; // long link_color; size = (bytes[8] << 8) + bytes[9]; // cols = (bytes[10] << 8) + bytes[11]; // rows = (bytes[12] << 8) + bytes[13]; // border = bytes[15]; // border_color = (bytes[17] << 16) + (bytes[18] << 8) + (bytes[19] << 8); // link_color = (bytes[21] << 16) + (bytes[22] << 8) + (bytes[23] << 8); end = ptr + size - 1; /** output += QString( "\n" ).arg(border, border_color, link_color); */ while (ptr < end) { if (ptr[0] == '\0') { fctype = GET_FUNCTION_CODE_TYPE (ptr[1]); fclen = 2 + GET_FUNCTION_CODE_DATALEN (ptr[1]); switch (fctype) { case PLKR_TFC_TABLE: switch (fclen) { case 2: /* NEW_ROW */ /* if (in_row) output += QString( "\n" ); output += QString( "\n" ); in_row = true; */ ptr += fclen; break; case 9: /* NEW_CELL */ // align = ptr[2]; // colspan = ptr[5]; // rowspan = ptr[6]; /** output += QString( "\n" ); break; default: ptr += fclen; } break; default: ptr += fclen; } } else { //output += QString( "
" ).arg( align_names[align], colspan, rowspan ); // border_color); */ if ( (record_id = READ_BIGENDIAN_SHORT (&ptr[3])) ) { QTextCharFormat format = context->cursor->charFormat(); context->cursor->insertImage( QStringLiteral( "%1.jpg" ).arg(record_id) ); context->cursor->setCharFormat( format ); context->images.append( record_id ); AddRecord (record_id); } DoStyle (context, style, true); text_len = READ_BIGENDIAN_SHORT (&ptr[7]); ptr += fclen; ParseText (doc, ptr, text_len, &font, &style, context); ptr += text_len; DoStyle (context, style, false); //output += QString( "
\n" ); return false; } } // output += QString( "\n" ); return true; } typedef struct { int size; int attributes; } ParagraphInfo; static ParagraphInfo *ParseParagraphInfo ( unsigned char* bytes, int* nparas ) { ParagraphInfo* paragraph_info; int j; int n; n = (bytes[2] << 8) + bytes[3]; paragraph_info = (ParagraphInfo *) malloc (sizeof (ParagraphInfo) * n); for (j = 0; j < n; j++) { paragraph_info[j].size = (bytes[8 + (j * 4) + 0] << 8) + bytes[8 + (j * 4) + 1]; paragraph_info[j].attributes = (bytes[8 + (j * 4) + 2] << 8) + bytes[8 + (j * 4) + 3]; } *nparas = n; return paragraph_info; } bool QUnpluck::TranscribeTextRecord ( plkr_Document* doc, int id, Context *context, unsigned char* bytes, plkr_DataRecordType type ) { unsigned char* ptr; unsigned char* run; unsigned char* para_start; unsigned char* data; unsigned char* start; ParagraphInfo* paragraphs; bool first_record_of_page = true; bool current_link; bool current_italic; bool current_struckthrough; bool current_underline; int fctype; int fclen; int para_index; int para_len; int textlen; int data_len; int current_font; int record_index; // int current_alignment; // int current_left_margin; // int current_right_margin; int nparagraphs; // long current_color; record_index = id; paragraphs = ParseParagraphInfo (bytes, &nparagraphs); start = bytes + 8 + ((bytes[2] << 8) + bytes[3]) * 4; for (para_index = 0, ptr = start, run = start; para_index < nparagraphs; para_index++) { para_len = paragraphs[para_index].size; /* If the paragraph is the last in the record, and it consists of a link to the next record in the logical page, we trim off the paragraph and instead insert the whole page */ if (((para_index + 1) == nparagraphs) && (para_len == (sizeof ("Click here for the next part") + 5)) && (*ptr == 0) && (ptr[1] == ((PLKR_TFC_LINK << 3) + 2)) && (strcmp ((char*)(ptr + 4), "Click here for the next part") == 0)) { record_index = (ptr[2] << 8) + ptr[3]; if ((data = plkr_GetRecordBytes (doc, record_index, &data_len, - &type)) == NULL) { + &type)) == nullptr) { // ShowWarning ("Can't open record %d!", record_index); free (paragraphs); return false; } else if (!(type == PLKR_DRTYPE_TEXT_COMPRESSED || type == PLKR_DRTYPE_TEXT)) { // ShowWarning ("Bad record type %d in record linked from end of record %d", type, id); free (paragraphs); return false; } first_record_of_page = false; para_index = 0; ptr = data + 8 + ((data[2] << 8) + data[3]) * 4; run = ptr; free (paragraphs); paragraphs = ParseParagraphInfo (data, &nparagraphs); para_len = paragraphs[para_index].size; MarkRecordDone (record_index); SetPageID (record_index, id); } if ((para_index == 0) && !first_record_of_page && (*ptr == 0) && (ptr[1] == ((PLKR_TFC_LINK << 3) + 2)) && (strcmp ((char*)(ptr + 4), "Click here for the previous part") == 0)) { /* throw away this inserted paragraph */ ptr += para_len; run = ptr; continue; } QTextCharFormat format( context->cursor->charFormat() ); QTextBlockFormat blockFormat( context->cursor->blockFormat() ); blockFormat.setAlignment( Qt::AlignLeft ); context->cursor->insertBlock( blockFormat ); context->cursor->setCharFormat( format ); mNamedTargets.insert( QStringLiteral( "para:%1-%2" ).arg( record_index ).arg( para_index ), QPair( GetPageID( record_index ), context->cursor->block() ) ); current_link = false; /* at the beginning of a paragraph, we start with a clean graphics context */ current_font = 0; // current_alignment = 0; // current_color = 0; current_italic = false; current_underline = false; current_struckthrough = false; // current_left_margin = 0; // current_right_margin = 0; for (para_start = ptr, textlen = 0; (ptr - para_start) < para_len;) { if (*ptr == 0) { /* function code */ if ((ptr - run) > 0) { /* write out any pending text */ context->cursor->insertText( QString::fromLatin1( (char*)run, ptr - run ) ); textlen += (ptr - run); } ptr++; fctype = GET_FUNCTION_CODE_TYPE (*ptr); fclen = GET_FUNCTION_CODE_DATALEN (*ptr); ptr++; if (fctype == PLKR_TFC_NEWLINE) { // TODO: remove the setCharFormat when Qt is fixed QTextCharFormat format( context->cursor->charFormat() ); context->cursor->insertText( QStringLiteral("\n") ); context->cursor->setCharFormat( format ); } else if (fctype == PLKR_TFC_LINK) { int record_id, real_record_id, datalen; plkr_DataRecordType type = (plkr_DataRecordType)0; - unsigned char *bytes = NULL; - char *url = NULL; + unsigned char *bytes = nullptr; + char *url = nullptr; if (fclen == 0) { if (current_link) { if ( !context->stack.isEmpty() ) context->cursor->setCharFormat( context->stack.pop() ); if ( !context->linkUrl.isEmpty() ) { Link link; link.url = context->linkUrl; link.start = context->linkStart; link.end = context->cursor->position(); link.page = GetPageID( id ); mLinks.append( link ); } } current_link = false; } else { record_id = (ptr[0] << 8) + ptr[1]; bytes = plkr_GetRecordBytes (doc, record_id, &datalen, &type); if (!bytes) { url = plkr_GetRecordURL (doc, record_id); } if (bytes && (type == PLKR_DRTYPE_MAILTO)) { context->linkUrl = MailtoURLFromBytes( bytes ); context->linkStart = context->cursor->position(); QTextCharFormat format( context->cursor->charFormat() ); context->stack.push( format ); format.setForeground( Qt::blue ); format.setUnderlineStyle( QTextCharFormat::SingleUnderline ); context->cursor->setCharFormat( format ); current_link = true; } else if (!bytes && url) { context->linkUrl = QString::fromLatin1( url ); context->linkStart = context->cursor->position(); QTextCharFormat format( context->cursor->charFormat() ); context->stack.push( format ); format.setForeground( Qt::blue ); format.setUnderlineStyle( QTextCharFormat::SingleUnderline ); context->cursor->setCharFormat( format ); current_link = true; } else if (bytes && (fclen == 2)) { AddRecord (record_id); real_record_id = GetPageID (record_id); if (type == PLKR_DRTYPE_IMAGE || type == PLKR_DRTYPE_IMAGE_COMPRESSED) { context->linkUrl = QStringLiteral( "%1.jpg" ).arg( record_id ); context->linkStart = context->cursor->position(); } else { context->linkUrl = QStringLiteral( "page:%1" ).arg( real_record_id ); context->linkStart = context->cursor->position(); } QTextCharFormat format( context->cursor->charFormat() ); context->stack.push( format ); format.setForeground( Qt::blue ); format.setUnderlineStyle( QTextCharFormat::SingleUnderline ); context->cursor->setCharFormat( format ); current_link = true; } else if (bytes && (fclen == 4)) { AddRecord (record_id); context->linkUrl = QStringLiteral( "para:%1-%2" ).arg( record_id ).arg( (ptr[2] << 8) + ptr[3] ); context->linkStart = context->cursor->position(); QTextCharFormat format( context->cursor->charFormat() ); context->stack.push( format ); format.setForeground( Qt::blue ); format.setUnderlineStyle( QTextCharFormat::SingleUnderline ); context->cursor->setCharFormat( format ); current_link = true; } else { // ShowWarning("odd link found: record_id=%d, bytes=0x%p, type=%d, url=%s", record_id, bytes, type, (url ? url : "0x0")); } } } else if (fctype == PLKR_TFC_FONT) { if (current_font != *ptr) { if ( !context->stack.isEmpty() ) context->cursor->setCharFormat( context->stack.pop() ); QTextCharFormat format( context->cursor->charFormat() ); context->stack.push( format ); int pointSize = qRound( format.fontPointSize() ); if (*ptr == 1) { format.setFontWeight( QFont::Bold ); pointSize += 3; } else if (*ptr == 2) { format.setFontWeight( QFont::Bold ); pointSize += 2; } else if (*ptr == 3) { format.setFontWeight( QFont::Bold ); pointSize += 1; } else if (*ptr == 4) { format.setFontWeight( QFont::Bold ); } else if (*ptr == 5) { format.setFontWeight( QFont::Bold ); pointSize += -1; } else if (*ptr == 6) { format.setFontWeight( QFont::Bold ); pointSize += -2; } else if (*ptr == 7) { format.setFontWeight( QFont::Bold ); } else if (*ptr == 8) { format.setFontFamily( QStringLiteral( "Courier New,courier" ) ); } else if (*ptr == 11) { format.setVerticalAlignment( QTextCharFormat::AlignSuperScript ); } format.setFontPointSize( qMax( pointSize, 1 ) ); context->cursor->setCharFormat( format ); current_font = *ptr; } } else if (fctype == PLKR_TFC_BITALIC) { QTextCharFormat format( context->cursor->charFormat() ); format.setFontItalic( true ); context->cursor->setCharFormat( format ); current_italic = true; } else if (fctype == PLKR_TFC_EITALIC) { if (current_italic) { QTextCharFormat format( context->cursor->charFormat() ); format.setFontItalic( false ); context->cursor->setCharFormat( format ); current_italic = false; } } else if (fctype == PLKR_TFC_BULINE) { QTextCharFormat format( context->cursor->charFormat() ); format.setFontUnderline( true ); context->cursor->setCharFormat( format ); current_underline = true; } else if (fctype == PLKR_TFC_EULINE) { if (current_underline) { QTextCharFormat format( context->cursor->charFormat() ); format.setFontUnderline( false ); context->cursor->setCharFormat( format ); current_underline = false; } } else if (fctype == PLKR_TFC_BSTRIKE) { QTextCharFormat format( context->cursor->charFormat() ); format.setFontStrikeOut( true ); context->cursor->setCharFormat( format ); current_struckthrough = true; } else if (fctype == PLKR_TFC_ESTRIKE) { if (current_struckthrough) { QTextCharFormat format( context->cursor->charFormat() ); format.setFontStrikeOut( false ); context->cursor->setCharFormat( format ); current_struckthrough = false; } } else if (fctype == PLKR_TFC_HRULE) { QTextCharFormat charFormat = context->cursor->charFormat(); QTextBlockFormat oldBlockFormat = context->cursor->blockFormat(); QTextBlockFormat blockFormat; blockFormat.setProperty( QTextFormat::BlockTrailingHorizontalRulerWidth, QStringLiteral("100%")); context->cursor->insertBlock( blockFormat ); context->cursor->insertBlock( oldBlockFormat ); context->cursor->setCharFormat( charFormat ); } else if (fctype == PLKR_TFC_ALIGN) { // current_alignment = 0; if (*ptr < 4) { QTextBlockFormat format( context->cursor->blockFormat() ); if (*ptr == 0) format.setAlignment( Qt::AlignLeft ); else if (*ptr == 1) format.setAlignment( Qt::AlignRight ); else if (*ptr == 2) format.setAlignment( Qt::AlignCenter ); else if (*ptr == 3) format.setAlignment( Qt::AlignJustify ); QTextCharFormat charFormat( context->cursor->charFormat() ); context->cursor->insertBlock( format ); context->cursor->setCharFormat( charFormat ); // current_alignment = (*ptr) + 1; } } else if (fctype == PLKR_TFC_MARGINS) { /* Not easy to set, in HTML */ #if 0 output += QString( "" ).arg(ptr[0], ptr[1]); if (current_left_margin != ptr[0] || current_right_margin != ptr[1]) { if (current_right_margin != 0) fprintf (fp, " ", current_right_margin); fprintf (fp, "\n"); } current_left_margin = ptr[0]; current_right_margin = ptr[1]; if (current_right_margin > 0 || current_left_margin > 0) { fprintf (fp, ""); if (current_left_margin != 0) { fprintf (fp, ""); } fprintf (fp, "
", current_left_margin); if ((ptr - run) > 2) { fwrite (run, 1, ((ptr - 2) - run), fp); textlen += ((ptr - 2) - run); } else { fprintf (fp, " "); } fprintf (fp, ""); if (current_left_margin == 0 && (ptr - run) > 2) { fwrite (run, 1, ((ptr - 2) - run), fp); textlen += ((ptr - 2) - run); } } else { if ((ptr - run) > 2) { fwrite (run, 1, ((ptr - 2) - run), fp); textlen += ((ptr - 2) - run); } } #endif // current_left_margin = ptr[0]; // current_right_margin = ptr[1]; } else if (fctype == PLKR_TFC_COLOR) { /* not sure what to do here yet */ /* fprintf (fp, "", ptr[0], ptr[1], ptr[2]);*/ // current_color = // (ptr[0] << 16) + (ptr[1] << 8) + ptr[2]; } else if (fctype == PLKR_TFC_IMAGE || fctype == PLKR_TFC_IMAGE2) { QTextCharFormat format = context->cursor->charFormat(); context->cursor->insertImage( QStringLiteral( "%1.jpg" ).arg( (ptr[0] << 8) + ptr[1] ) ); context->images.append( (ptr[0] << 8) + ptr[1] ); context->cursor->setCharFormat( format ); AddRecord ((ptr[0] << 8) + ptr[1]); } else if (fctype == PLKR_TFC_TABLE) { int record_id, datalen; plkr_DataRecordType type = (plkr_DataRecordType)0; - unsigned char *bytes = NULL; + unsigned char *bytes = nullptr; record_id = (ptr[0] << 8) + ptr[1]; bytes = plkr_GetRecordBytes (doc, record_id, &datalen, &type); TranscribeTableRecord (doc, context, bytes); } else if (fctype == PLKR_TFC_UCHAR) { if (fclen == 3) context->cursor->insertText( QChar( (ptr[1] << 8) + ptr[2] ) ); else if (fclen == 5) context->cursor->insertText( QChar( (ptr[3] << 8) + ptr[4] ) ); /* skip over alternate text */ ptr += ptr[0]; } else { /* ignore function */ //output += QString( "" ).arg(fctype); } ptr += fclen; run = ptr; } else { ptr++; } } if ((ptr - run) > 0) { /* output any pending text at the end of the paragraph */ context->cursor->insertText( QString::fromLatin1( (char *)run, ptr - run ) ); textlen += (ptr - run); run = ptr; } /* clear the graphics state again */ if (current_font > 0 && current_font < 9 ) { if ( !context->stack.isEmpty() ) context->cursor->setCharFormat( context->stack.pop() ); } if (current_italic) { QTextCharFormat format( context->cursor->charFormat() ); format.setFontItalic( false ); context->cursor->setCharFormat( format ); } if (current_underline) { QTextCharFormat format( context->cursor->charFormat() ); format.setFontUnderline( false ); context->cursor->setCharFormat( format ); } if (current_struckthrough) { QTextCharFormat format( context->cursor->charFormat() ); format.setFontStrikeOut( false ); context->cursor->setCharFormat( format ); } #if 0 if (current_alignment > 0) { context->cursor->insertBlock(); } if (current_right_margin > 0) fprintf (fp, " 
", current_right_margin); else if (current_left_margin > 0) fprintf (fp, ""); /* end the paragraph */ context->cursor->insertBlock(); #endif } free (paragraphs); return true; } bool QUnpluck::TranscribeRecord( int index ) { plkr_DataRecordType type; int data_len; bool status = true; unsigned char *data = plkr_GetRecordBytes( mDocument, index, &data_len, &type); if ( !data ) { MarkRecordDone( index ); return false; } if (type == PLKR_DRTYPE_TEXT_COMPRESSED || type == PLKR_DRTYPE_TEXT) { QTextDocument *document = new QTextDocument; QTextFrameFormat format( document->rootFrame()->frameFormat() ); format.setMargin( 20 ); document->rootFrame()->setFrameFormat( format ); Context *context = new Context; context->recordId = index; context->document = document; context->cursor = new QTextCursor( document ); QTextCharFormat charFormat; charFormat.setFontPointSize( 10 ); charFormat.setFontFamily( QStringLiteral("Helvetica") ); context->cursor->setCharFormat( charFormat ); status = TranscribeTextRecord( mDocument, index, context, data, type ); document->setTextWidth( 600 ); delete context->cursor; mContext.append( context ); } else if (type == PLKR_DRTYPE_IMAGE_COMPRESSED || type == PLKR_DRTYPE_IMAGE) { QImage image = TranscribeImageRecord( data ); mImages.insert( index, image ); } else if (type == PLKR_DRTYPE_MULTIIMAGE) { QImage image; if ( TranscribeMultiImageRecord( mDocument, image, data ) ) mImages.insert( index, image ); } else { status = false; } // plkr_GetHomeRecordID (doc))) MarkRecordDone( index ); return status; } diff --git a/generators/plucker/unpluck/qunpluck.h b/generators/plucker/unpluck/qunpluck.h index f34282d27..199d0bcc0 100644 --- a/generators/plucker/unpluck/qunpluck.h +++ b/generators/plucker/unpluck/qunpluck.h @@ -1,89 +1,89 @@ /*************************************************************************** * Copyright (C) 2007 by Tobias Koenig * * * * Based on code written by Bill Janssen 2002 * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * ***************************************************************************/ #ifndef QUNPLUCK_H #define QUNPLUCK_H #include #include #include #include #include "unpluck.h" class Context; class RecordNode; class QTextDocument; namespace Okular { class Action; } class Link { public: Link() - : link( 0 ) + : link( nullptr ) { } typedef QVector List; Okular::Action *link; QString url; int page; int start; int end; }; class QUnpluck { public: QUnpluck(); ~QUnpluck(); QUnpluck(const QUnpluck &) = delete; QUnpluck &operator=(const QUnpluck &) = delete; bool open( const QString &fileName ); QList pages() const { return mPages; } Link::List links() const { return mLinks; } QMap infos() const { return mInfo; } private: int GetNextRecordNumber(); int GetPageID( int index ); void AddRecord( int index ); void MarkRecordDone( int index ); void SetPageID( int index, int page_id ); QString MailtoURLFromBytes( unsigned char* record_data ); void DoStyle( Context* context, int style, bool start ); bool TranscribeRecord( int index ); QImage TranscribeImageRecord( unsigned char* bytes ); bool TranscribeTableRecord( plkr_Document* doc, Context* context, unsigned char* bytes ); bool TranscribeTextRecord( plkr_Document* doc, int id, Context* context, unsigned char* bytes, plkr_DataRecordType type ); void ParseText( plkr_Document* doc, unsigned char* ptr, int text_len, int* font, int* style, Context* context ); plkr_Document* mDocument; QList mRecords; QList mContext; QList mPages; QMap > mNamedTargets; QMap mImages; QMap mInfo; QString mErrorString; Link::List mLinks; }; #endif diff --git a/generators/plucker/unpluck/unpluck.cpp b/generators/plucker/unpluck/unpluck.cpp index 720958699..62ff3b1cc 100644 --- a/generators/plucker/unpluck/unpluck.cpp +++ b/generators/plucker/unpluck/unpluck.cpp @@ -1,1184 +1,1184 @@ /* -*- mode: c; indent-tabs-mode: nil; -*- * $Id: unpluck.c,v 1.12 2003/12/28 20:59:21 chrish Exp $ * * unpluck -- a library to read Plucker data files * Copyright (c) 2002, Bill Janssen * * 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. * */ #if !defined(WIN32) #include /* for lseek, etc. */ #else #include #endif #include #include #include /* for fstat() */ #include /* for strndup() */ #include /* for errno */ #include /* for O_RDONLY */ #include /* for assert() */ #include #include "unpluck.h" #include "unpluckint.h" /***********************************************************************/ /***********************************************************************/ /***** *****/ /***** Decompression code (taken from the Plucker PalmOS viewer *****/ /***** sources, Copyright (c) 1998-2002, by Mark Ian Lillywhite *****/ /***** and Michael Nordstr�m, also under the GPL) *****/ /***** *****/ /***********************************************************************/ /***********************************************************************/ /* uncompress DOC compressed document/image */ static unsigned int UncompressDOC ( unsigned char* src, /* in: compressed document */ unsigned int src_len, /* in: size of compressed document */ unsigned char* dest, /* out: buffer to put uncompressed document in */ unsigned int dest_len /* out: size of buffer to put uncompressed document in */ ) { // unsigned int offset; unsigned int src_index; unsigned int dest_index; - assert (src != NULL && src_len != 0 && dest != NULL && dest_len != 0); + assert (src != nullptr && src_len != 0 && dest != nullptr && dest_len != 0); // offset = 0; src_index = 0; dest_index = 0; memset (dest, 0, dest_len); while (src_index < src_len) { unsigned int token; token = (unsigned int) src[src_index++]; if (0 < token && token < 9) { while (token != 0) { dest[dest_index++] = src[src_index++]; token--; } } else if (token < 0x80) { dest[dest_index++] = token; } else if (0xc0 <= token) { dest[dest_index++] = ' '; dest[dest_index++] = token ^ 0x80; } else { int m; int n; token *= 256; token += src[src_index++]; m = (token & 0x3fff) / 8; n = token & 7; n += 3; while (n != 0) { dest[dest_index] = dest[dest_index - m]; dest_index++; n--; } } } assert (src_index == src_len && dest_index == dest_len); return 1; } /* uncompress ZLib compressed document/image */ static unsigned int UncompressZLib ( unsigned char* src, /* in: compressed document */ unsigned int src_len, /* in: size of compressed document */ unsigned char* dest, /* out: buffer to put uncompressed document in */ unsigned int dest_len, /* out: size of buffer to put uncompressed document in */ unsigned char* owner_id /* in: owner-id key */ ) { z_stream z; unsigned int err; unsigned int keylen; unsigned int i; unsigned char keybuf[OWNER_ID_HASH_LEN]; - assert (src != NULL && src_len != 0 && dest != NULL && dest_len != 0); + assert (src != nullptr && src_len != 0 && dest != nullptr && dest_len != 0); - keylen = (owner_id == NULL) ? 0 : MIN (src_len, OWNER_ID_HASH_LEN); + keylen = (owner_id == nullptr) ? 0 : MIN (src_len, OWNER_ID_HASH_LEN); memset (&z, 0, sizeof z); - if (owner_id != NULL) { + if (owner_id != nullptr) { for (i = 0; i < keylen; i++) keybuf[i] = src[i] ^ owner_id[i]; z.next_in = keybuf; z.avail_in = keylen; } else { z.next_in = src; z.avail_in = src_len; } z.next_out = dest; z.avail_out = dest_len; err = inflateInit (&z); if (err != Z_OK) { return err; } do { if (z.avail_in == 0 && keylen > 0) { z.next_in = src + keylen; z.avail_in = src_len - keylen; } err = inflate (&z, Z_SYNC_FLUSH); } while (err == Z_OK); if (err != Z_STREAM_END) return err; assert (z.total_out == dest_len); return inflateEnd (&z); } /***********************************************************************/ /***********************************************************************/ /***** *****/ /***** "Open" the DB (read the headers and parse the various *****/ /***** metadata, like URLs, default categories, charsets, etc.) *****/ /***** *****/ /***********************************************************************/ /***********************************************************************/ static void FreePluckerDoc ( plkr_Document* doc ) { - if (doc->name != NULL) + if (doc->name != nullptr) free (doc->name); - if (doc->title != NULL) + if (doc->title != nullptr) free (doc->title); - if (doc->author != NULL) + if (doc->author != nullptr) free (doc->author); - if (doc->records != NULL) { + if (doc->records != nullptr) { int i; for (i = 0; i < doc->nrecords; i++) { - if (doc->records[i].cache != NULL) + if (doc->records[i].cache != nullptr) free (doc->records[i].cache); } free (doc->records); } - if (doc->urls != NULL) + if (doc->urls != nullptr) free (doc->urls); - if (doc->handle != NULL) + if (doc->handle != nullptr) doc->handle->free (doc->handle); free (doc); } static plkr_DataRecord* FindRecordByIndex ( plkr_Document* doc, int record_index ) { int imin; int imax; int itest; for (imin = 0, imax = doc->nrecords; imin < imax;) { itest = imin + (imax - imin) / 2; /* _plkr_message("imin = %2d, imax = %2d, itest = %2d (%2d), record_index = %2d", imin, imax, itest, doc->records[itest].uid, record_index); */ if (doc->records[itest].uid == record_index) return &doc->records[itest]; else if (record_index > doc->records[itest].uid) imin = itest + 1; else if (record_index < doc->records[itest].uid) imax = itest; } - return NULL; + return nullptr; } static int GetUncompressedRecord ( plkr_Document* doc, plkr_DBHandle handle, int record_index, unsigned char* buffer, int buffer_size, plkr_DataRecordType expected_type, unsigned char** buffer_out, int* buffer_size_out, plkr_DataRecord** record_out ) { /* read whole data record, including header, into buffer. If some part of the record is compressed, uncompress it. If "buffer" is NULL, allocate enough bytes to fit. Returns TRUE if read is successful, and sets "buffer_out" and "buffer_size_out" and "record_out" on successful return. */ plkr_DataRecord* record; unsigned char* tbuffer = buffer; int size_needed; int blen = buffer_size; record = FindRecordByIndex (doc, record_index); - if (record == NULL) { + if (record == nullptr) { _plkr_message ("No record with index %d", record_index); return FALSE; }; if (expected_type != PLKR_DRTYPE_NONE && record->type != expected_type) { _plkr_message ("Record %d has unexpected type %d; expected %d", record_index, record->type, expected_type); return FALSE; } /* figure size needed */ size_needed = record->uncompressed_size + 8; if ((record->type == PLKR_DRTYPE_TEXT_COMPRESSED) || (record->type == PLKR_DRTYPE_TEXT)) size_needed += 4 * record->nparagraphs; if (!buffer) { - if (buffer_out == NULL) { + if (buffer_out == nullptr) { _plkr_message ("No output buffer"); return FALSE; } else if (record->cache) { tbuffer = record->cache; size_needed = record->cached_size; } else { tbuffer = (unsigned char *) malloc (size_needed); blen = size_needed; } } else { tbuffer = buffer; if (buffer_size < size_needed) { _plkr_message ("Buffer too small; needs %d", size_needed); return FALSE; } else if (record->cache) { memcpy (buffer, record->cache, record->cached_size); size_needed = record->cached_size; } } if (!record->cache) { if ((record->type == PLKR_DRTYPE_TEXT_COMPRESSED) || (record->type == PLKR_DRTYPE_IMAGE_COMPRESSED) || (record->type == PLKR_DRTYPE_TABLE_COMPRESSED) || (record->type == PLKR_DRTYPE_GLYPHPAGE) || (record->type == PLKR_DRTYPE_LINKS_COMPRESSED)) { unsigned char *start_of_data, *output_ptr; int len_of_data, buffer_remaining, buf_to_use; unsigned char *buf = (unsigned char*)malloc (record->size); if (!handle->seek (handle, record->offset) || (handle->read (handle, buf, record->size, record->size) != record->size)) { _plkr_message ("Bad read from DBHandle while reading record %d", record->uid); free (buf); if (tbuffer != buffer) free (tbuffer); return FALSE; } #if 0 _plkr_message ("data record %d (%d): uid is %d, # paras = %d, size = %d, type = %d", record_index, record->size, (buf[0] << 8) + buf[1], (buf[2] << 8) + buf[3], (buf[4] << 8) + buf[5], buf[6]); #endif memcpy (tbuffer, buf, 8); output_ptr = tbuffer + 8; buffer_remaining = blen - 8; start_of_data = buf + 8; len_of_data = record->size - 8; if (record->type == PLKR_DRTYPE_TEXT_COMPRESSED) { /* skip over the paragraph headers */ memcpy (output_ptr, start_of_data, 4 * record->nparagraphs); start_of_data += (4 * record->nparagraphs); len_of_data -= (4 * record->nparagraphs); output_ptr += (4 * record->nparagraphs); buffer_remaining -= (4 * record->nparagraphs); } buf_to_use = size_needed - (start_of_data - buf); if (doc->compression == PLKR_COMPRESSION_ZLIB) { if (UncompressZLib (start_of_data, len_of_data, output_ptr, buf_to_use, (doc->owner_id_required ? doc-> - owner_id_key : NULL)) != Z_OK) { + owner_id_key : nullptr)) != Z_OK) { _plkr_message ("Bad Zlib uncompress of record %d", record_index); free (buf); if (tbuffer != buffer) free (tbuffer); return FALSE; }; } else if (doc->compression == PLKR_COMPRESSION_DOC) { if (UncompressDOC (start_of_data, len_of_data, output_ptr, buf_to_use) != 1) { _plkr_message ("Bad DOC uncompress of record %d", record_index); free (buf); if (tbuffer != buffer) free (tbuffer); return FALSE; }; } free (buf); } else { /* all the record types which don't use compression */ if (!handle->seek (handle, record->offset) || (handle->read (handle, tbuffer, blen, size_needed) != size_needed)) { _plkr_message ("Bad read from DBHandle while reading record %d", record->uid); if (tbuffer != buffer) free (tbuffer); return FALSE; } } } if (record_out) *record_out = record; if (buffer_out) *buffer_out = tbuffer; if (buffer_size_out) *buffer_size_out = size_needed; return TRUE; } static int ParseCategories ( plkr_Document* newdoc, plkr_DBHandle handle ) { struct _plkr_CategoryName* categories; struct _plkr_CategoryName* newc; plkr_DataRecord *record; unsigned char* buf; unsigned char* ptr; int bufsize; if (GetUncompressedRecord - (newdoc, handle, newdoc->default_category_record_uid, NULL, 0, + (newdoc, handle, newdoc->default_category_record_uid, nullptr, 0, PLKR_DRTYPE_CATEGORY, &buf, &bufsize, &record)) { /* keep the record data, since the list of char * ptrs will point into it */ record->cache = buf; record->cached_size = bufsize; - categories = NULL; + categories = nullptr; for (ptr = buf + 8; (ptr - buf) < bufsize;) { newc = (struct _plkr_CategoryName *) malloc (sizeof (struct _plkr_CategoryName)); newc->next = categories; categories = newc; newc->name = (char*)ptr; ptr += (strlen ((char*)ptr) + 1); } newdoc->default_categories = categories; return TRUE; } else { return FALSE; } } static int ParseMetadata ( plkr_Document* newdoc, plkr_DBHandle handle ) { unsigned char* buf; unsigned char* ptr; int bufsize; int nsubrecords; int typecode; int subrecord_length; int i; if (!GetUncompressedRecord - (newdoc, handle, newdoc->metadata_record_uid, NULL, 0, - PLKR_DRTYPE_METADATA, &buf, &bufsize, NULL)) { + (newdoc, handle, newdoc->metadata_record_uid, nullptr, 0, + PLKR_DRTYPE_METADATA, &buf, &bufsize, nullptr)) { return FALSE; } else { nsubrecords = (buf[8] << 8) + buf[9]; for (i = 0, ptr = buf + 10; i < nsubrecords; i++) { typecode = (ptr[0] << 8) + ptr[1]; subrecord_length = ((ptr[2] << 8) + ptr[3]) * 2; if (typecode == PLKR_MDTYPE_DEFAULTCHARSET) { newdoc->default_charset_mibenum = (ptr[4] << 8) + ptr[5]; ptr += 6; } else if (typecode == PLKR_MDTYPE_EXCEPTCHARSETS) { int i, n, record_id, mibenum; plkr_DataRecord *record; ptr += 4; for (i = 0, n = subrecord_length / 4; i < n; i++, ptr += 4) { record_id = (ptr[0] << 8) + ptr[1]; mibenum = (ptr[2] << 8) + ptr[3]; record = FindRecordByIndex (newdoc, record_id); - if (record == NULL) { + if (record == nullptr) { _plkr_message ("Can't find record with id %d", record_id); free (buf); return FALSE; } record->charset_mibenum = mibenum; } } else if (typecode == PLKR_MDTYPE_OWNERIDCRC) { newdoc->owner_id_required = TRUE; ptr += 8; } else if (typecode == PLKR_MDTYPE_AUTHOR) { newdoc->author = _plkr_strndup ((char*)( ptr + 4 ), subrecord_length); ptr += (4 + subrecord_length); } else if (typecode == PLKR_MDTYPE_TITLE) { newdoc->title = _plkr_strndup ((char*)( ptr + 4 ), subrecord_length); ptr += (4 + subrecord_length); } else if (typecode == PLKR_MDTYPE_PUBLICATIONTIME) { newdoc->publication_time = READ_BIGENDIAN_LONG (ptr + 4) - PLKR_TIMEADJUST; ptr += 8; } else { _plkr_message ("Bad metadata typecode %d encountered in metadata record", typecode); free (buf); return FALSE; } } free (buf); return TRUE; } } static int ParseURLs ( plkr_Document* newdoc, plkr_DBHandle handle ) { plkr_DataRecord* record; unsigned char* buf; unsigned char* ptr; char** urls; int id; int i; int n; int count; int nurls; int bufsize; struct url_index_record { int last_url_index; int record_id; } *records; - buf = NULL; - urls = NULL; - records = NULL; + buf = nullptr; + urls = nullptr; + records = nullptr; if (!GetUncompressedRecord - (newdoc, handle, newdoc->urls_index_record_uid, NULL, 0, - PLKR_DRTYPE_LINKS_INDEX, &buf, &bufsize, NULL)) { + (newdoc, handle, newdoc->urls_index_record_uid, nullptr, 0, + PLKR_DRTYPE_LINKS_INDEX, &buf, &bufsize, nullptr)) { return FALSE; } else { n = ((buf[4] << 8) + buf[5]) / 4; records = (struct url_index_record *) malloc (n * sizeof (*records)); for (i = 0, nurls = 0; i < n; i++) { ptr = buf + 8 + (i * 4); records[i].last_url_index = (ptr[0] << 8) + ptr[1]; records[i].record_id = (ptr[2] << 8) + ptr[3]; #ifdef DEBUGURLS _plkr_message ("index %3d: last = %d, record_id = %d", i, records[i].last_url_index, records[i].record_id); #endif /* def DEBUGURLS */ nurls = MAX (nurls, records[i].last_url_index); } free (buf); - buf = NULL; + buf = nullptr; } urls = (char **) malloc (nurls * sizeof (char *)); memset (urls, 0, nurls * sizeof (char *)); for (count = 0, i = 0; i < n; i++) { id = records[i].record_id; if (!GetUncompressedRecord (newdoc, handle, id, - NULL, 0, PLKR_DRTYPE_NONE, &buf, + nullptr, 0, PLKR_DRTYPE_NONE, &buf, &bufsize, &record)) { goto errout4; } if (record->type != PLKR_DRTYPE_LINKS && record->type != PLKR_DRTYPE_LINKS_COMPRESSED) { _plkr_message ("Supposed URLs record has bad type %d", record->type); goto errout4; } record->cache = buf; record->cached_size = bufsize; - buf = NULL; + buf = nullptr; for (ptr = record->cache + 8; (ptr - record->cache) < record->cached_size; ptr += (strlen ((char*)ptr) + 1)) { #ifdef DEBUGURLS _plkr_message ("%3d: %s", count, ptr); #endif /* def DEBUGURLS */ assert (count < nurls); urls[count++] = (char*)ptr; } } free (records); newdoc->urls = urls; newdoc->nurls = nurls; return TRUE; errout4: - if (buf != NULL) + if (buf != nullptr) free (buf); free (urls); free (records); return FALSE; } plkr_Document* plkr_OpenDoc ( plkr_DBHandle handle ) { ReservedRecordEntry reserved[MAX_RESERVED]; plkr_DataRecord* record; plkr_Document* newdoc; unsigned char utilbuf[128]; static char id_stamp[9] = "DataPlkr"; int i; int nreserved; int records_size; int compression; if (!handle->seek (handle, 0) || (handle->read (handle, utilbuf, sizeof (utilbuf), 78) != 78)) { _plkr_message ("Bad read of DB header"); - return NULL; + return nullptr; } /* check for type stamp */ if (strncmp ((char *) (utilbuf + 60), id_stamp, 8) != 0) { _plkr_message ("Bad magic number"); - return NULL; + return nullptr; } /* check for version 1 */ i = (utilbuf[34] << 8) + utilbuf[35]; if (i != 1) { _plkr_message ("Not version 1 of Plucker format; version %d", i); - return NULL; + return nullptr; } /* get the title, creation time, and last modification time from header */ newdoc = (plkr_Document *) malloc (sizeof (plkr_Document)); memset (newdoc, 0, sizeof (plkr_Document)); newdoc->name = (char*)_plkr_strndup ((char*)utilbuf, MIN (strlen ((char*)utilbuf), 32)); newdoc->creation_time = (time_t) ((utilbuf[36] << 24) + (utilbuf[37] << 16) + (utilbuf[38] << 8) + utilbuf[39] - PLKR_TIMEADJUST); newdoc->modification_time = (time_t) ((utilbuf[40] << 24) + (utilbuf[41] << 16) + (utilbuf[42] << 8) + utilbuf[43] - PLKR_TIMEADJUST); newdoc->nrecords = (utilbuf[76] << 8) + utilbuf[77]; /* Now read the record-list to find out where the records are */ records_size = sizeof (plkr_DataRecord) * newdoc->nrecords; newdoc->records = (plkr_DataRecord *) malloc (records_size); memset (newdoc->records, 0, records_size); for (i = 0; i < newdoc->nrecords; i++) { if (handle->read (handle, utilbuf, sizeof (utilbuf), 8) != 8) { _plkr_message ("Bad read of record list"); FreePluckerDoc (newdoc); - return NULL; + return nullptr; } newdoc->records[i].offset = (utilbuf[0] << 24) + (utilbuf[1] << 16) + (utilbuf[2] << 8) + utilbuf[3]; } /* process the index record */ if (!handle->seek (handle, newdoc->records[0].offset) || (handle->read (handle, utilbuf, sizeof (utilbuf), 6) != 6)) { _plkr_message ("Bad read of index record"); FreePluckerDoc (newdoc); - return NULL; + return nullptr; } if ((utilbuf[0] << 8) + utilbuf[1] != 1) { _plkr_message ("index record has bad UID %d", (utilbuf[0] << 8) + utilbuf[1]); FreePluckerDoc (newdoc); - return NULL; + return nullptr; } newdoc->records[0].uid = 1; compression = (utilbuf[2] << 8) + utilbuf[3]; if (compression == PLKR_COMPRESSION_DOC) newdoc->compression = PLKR_COMPRESSION_DOC; else if (compression == PLKR_COMPRESSION_ZLIB) newdoc->compression = PLKR_COMPRESSION_ZLIB; else { _plkr_message ("Unknown compression type %d", compression); FreePluckerDoc (newdoc); - return NULL; + return nullptr; } nreserved = (utilbuf[4] << 8) + utilbuf[5]; if (nreserved > MAX_RESERVED) { _plkr_message ("Too many reserved records (%d) for software", nreserved); FreePluckerDoc (newdoc); - return NULL; + return nullptr; } for (i = 0; i < nreserved; i++) { if (handle->read (handle, utilbuf, sizeof (utilbuf), 4) != 4) { _plkr_message ("Bad read of reserved record list"); FreePluckerDoc (newdoc); - return NULL; + return nullptr; } reserved[i].name = (ReservedRecordName)( (utilbuf[0] << 8) + utilbuf[1] ); reserved[i].uid = (utilbuf[2] << 8) + utilbuf[3]; } /* OK, now process the data records */ newdoc->max_record_size = 0; for (i = 1; i < newdoc->nrecords; i++) { record = newdoc->records + i; if (!handle->seek (handle, record->offset) || (handle->read (handle, utilbuf, sizeof (utilbuf), 8) != 8)) { _plkr_message ("Can't read header of record %d", i); FreePluckerDoc (newdoc); - return NULL; + return nullptr; } newdoc->records[i - 1].size = record->offset - newdoc->records[i - 1].offset; record->uid = (utilbuf[0] << 8) + utilbuf[1]; record->nparagraphs = (utilbuf[2] << 8) + utilbuf[3]; record->uncompressed_size = (utilbuf[4] << 8) + utilbuf[5]; record->type = (plkr_DataRecordType)utilbuf[6]; newdoc->max_record_size = MAX (newdoc->max_record_size, record->uncompressed_size); } /* To get the size of the last record we subtract its offset from the total size of the DB. */ if ((i = handle->size (handle)) == 0) { _plkr_message ("Can't obtain size of DB"); FreePluckerDoc (newdoc); - return NULL; + return nullptr; }; record = newdoc->records + (newdoc->nrecords - 1); record->size = i - record->offset; /* make sure the uncompressed size is set, now that we know the record sizes */ for (i = 0; i < newdoc->nrecords; i++) { record = newdoc->records + i; if (record->uncompressed_size == 0) { if (record->type == PLKR_DRTYPE_LINKS_COMPRESSED || record->type == PLKR_DRTYPE_TEXT_COMPRESSED || record->type == PLKR_DRTYPE_TABLE_COMPRESSED || record->type == PLKR_DRTYPE_IMAGE_COMPRESSED) { _plkr_message ("Bad uncompressed size 0 in record uid %d", record->uid); FreePluckerDoc (newdoc); - return NULL; + return nullptr; } else { record->uncompressed_size = record->size - 8; } } #ifdef DEBUGOPEN { static char *types[] = { "TEXT", "TEXTC", "IMAGE", "IMAGEC", "MAILTO", "URLINDEX", "URLS", "URLSC", "BOOKMARKS", "CATEGORIES", "METADATA" }; _plkr_message ("%3d: type=%10s, offset=%07x, size=%5d, uncompressed_size=%5d", record->uid, types[MIN (record->type, sizeof (types) / sizeof (char *))], record->offset, record->size, record->uncompressed_size); } #endif } /* find the reserved records */ /* do metadata first, to find out whether we need an owner_id key */ for (i = 0; i < nreserved; i++) { if (reserved[i].name == PLKR_METADATA_NAME) { newdoc->metadata_record_uid = reserved[i].uid; if (!ParseMetadata (newdoc, handle)) { _plkr_message ("Error parsing metadata record"); FreePluckerDoc (newdoc); - return NULL; + return nullptr; } } } if (newdoc->owner_id_required) { /* we need to set up the owner-id key before uncompressing any records... */ - char *owner_id = plkr_GetConfigString (NULL, "owner_id", NULL); + char *owner_id = plkr_GetConfigString (nullptr, "owner_id", nullptr); - if (owner_id != NULL) { + if (owner_id != nullptr) { unsigned long crc; int owner_id_len = strlen (owner_id); - crc = crc32 (0L, NULL, 0); + crc = crc32 (0L, nullptr, 0); crc = crc32 (crc, (const Bytef*)owner_id, owner_id_len); for (i = 0; i < 10; i++) { crc = crc32 (crc, (const Bytef*)owner_id, owner_id_len); newdoc->owner_id_key[(i * 4) + 0] = (unsigned char)((crc >> 24) & 0xFF); newdoc->owner_id_key[(i * 4) + 1] = (unsigned char)((crc >> 16) & 0xFF); newdoc->owner_id_key[(i * 4) + 2] = (unsigned char)((crc >> 8) & 0xFF); newdoc->owner_id_key[(i * 4) + 3] = (unsigned char)(crc & 0xFF); } } else { _plkr_message ("Document requires owner-id to open"); FreePluckerDoc (newdoc); - return NULL; + return nullptr; } } /* now do the rest of the reserved records */ for (i = 0; i < nreserved; i++) { if (reserved[i].name == PLKR_HOME_NAME) newdoc->home_record_uid = reserved[i].uid; else if (reserved[i].name == PLKR_DEFAULT_CATEGORY_NAME) { newdoc->default_category_record_uid = reserved[i].uid; if (!ParseCategories (newdoc, handle)) { _plkr_message ("Error parsing default-categories record"); FreePluckerDoc (newdoc); - return NULL; + return nullptr; } } else if (reserved[i].name == PLKR_URLS_INDEX_NAME) { newdoc->urls_index_record_uid = reserved[i].uid; if (!ParseURLs (newdoc, handle)) { _plkr_message ("Error parsing URLs records"); FreePluckerDoc (newdoc); - return NULL; + return nullptr; } } } newdoc->handle = handle; #ifdef DEBUGOPEN /* test the record fetch by fetching them! */ for (i = 1; i < newdoc->nrecords; i++) { plkr_DataRecordType type; int n; printf ("==============================================\n" "record %3d (%d bytes)\n", newdoc->records[i].uid, newdoc->records[i].size); (void) plkr_GetRecordBytes (newdoc, newdoc->records[i].uid, &n, &type); } #endif return newdoc; } int plkr_GetUidForIndex ( plkr_Document *doc, int record_index ) { return doc->records[ record_index ].uid; } void plkr_CloseDoc ( plkr_Document * doc ) { - if (doc == NULL) { + if (doc == nullptr) { _plkr_message ("Attempt to free NULL doc"); } else { FreePluckerDoc (doc); } } /***********************************************************************/ /***********************************************************************/ /***** *****/ /***** An implementation of a file-based DBHandle *****/ /***** *****/ /***********************************************************************/ /***********************************************************************/ static int FpSeek ( plkr_DBHandle handle, long offset ) { long result; result = lseek (handle->dbprivate, offset, SEEK_SET); if (result != offset) { _plkr_message ("Unable to seek fp %d to offset %lu -- %lu instead\n", handle->dbprivate, offset, result); } return (result == offset); } static int FpRead ( plkr_DBHandle handle, unsigned char* buffer, int buffersize, int readsize ) { int result; result = read (handle->dbprivate, buffer, MIN (buffersize, readsize)); if (result != readsize) { _plkr_message ("Unable to read %d bytes from fp %d -- read %d instead\n", MIN (buffersize, readsize), handle->dbprivate, result); } return (result); } static void FpFree ( plkr_DBHandle handle ) { int fp = handle->dbprivate; if (fp > 0) close (fp); } static long FpSize ( plkr_DBHandle handle ) { int fp = handle->dbprivate; struct stat buf; if (fstat (fp, &buf) != 0) { _plkr_message ("Can't stat file; errno %d", errno); return 0; }; return buf.st_size; } plkr_Document* plkr_OpenDBFile ( const char* filename ) { plkr_DBHandle handle; plkr_Document* doc; int fp; #if !defined(WIN32) fp = open (filename, O_RDONLY); #else fp = open (filename, O_RDONLY | O_BINARY); #endif if (fp < 0) { _plkr_message ("Can't open file %s", filename); - return NULL; + return nullptr; } handle = (plkr_DBHandle) malloc (sizeof (*handle)); handle->dbprivate = fp; handle->seek = FpSeek; handle->read = FpRead; handle->free = FpFree; handle->size = FpSize; doc = plkr_OpenDoc (handle); - if (doc == NULL) + if (doc == nullptr) close (fp); return doc; } /***********************************************************************/ /***********************************************************************/ /***** *****/ /***** Routines to access individual uncompressed records *****/ /***** *****/ /***********************************************************************/ /***********************************************************************/ int plkr_CopyRecordBytes ( plkr_Document* doc, int record_index, unsigned char* output_buffer, int output_buffer_size, plkr_DataRecordType* type ) { plkr_DataRecord* record; int output_size; if (!FindRecordByIndex (doc, record_index)) return 0; if (!GetUncompressedRecord (doc, doc->handle, record_index, output_buffer, output_buffer_size, - PLKR_DRTYPE_NONE, NULL, &output_size, + PLKR_DRTYPE_NONE, nullptr, &output_size, &record)) return 0; else { *type = record->type; return output_size; } } unsigned char *plkr_GetRecordBytes ( plkr_Document* doc, int record_index, int* size, plkr_DataRecordType* type ) { plkr_DataRecord* record; unsigned char* buf; if (!FindRecordByIndex (doc, record_index)) - return NULL; + return nullptr; if (!GetUncompressedRecord (doc, doc->handle, record_index, - NULL, 0, PLKR_DRTYPE_NONE, + nullptr, 0, PLKR_DRTYPE_NONE, &buf, size, &record)) - return NULL; + return nullptr; else { if (!record->cache) { record->cache = buf; record->cached_size = *size; } *type = record->type; return buf; } } int plkr_GetHomeRecordID ( plkr_Document* doc ) { return doc->home_record_uid; } char* plkr_GetName ( plkr_Document* doc ) { return doc->name; } char* plkr_GetTitle ( plkr_Document* doc ) { return doc->title; } char* plkr_GetAuthor ( plkr_Document* doc ) { return doc->author; } int plkr_GetDefaultCharset ( plkr_Document* doc ) { return doc->default_charset_mibenum; } unsigned long plkr_GetPublicationTime ( plkr_Document* doc ) { if (doc->publication_time) return (unsigned long) doc->publication_time; else return (unsigned long) doc->creation_time; } plkr_CategoryList plkr_GetDefaultCategories ( plkr_Document* doc ) { return doc->default_categories; } int plkr_GetRecordCount ( plkr_Document* doc ) { return doc->nrecords; } int plkr_GetMaxRecordSize ( plkr_Document* doc ) { return doc->max_record_size; } char* plkr_GetRecordURL ( plkr_Document * doc, int record_index ) { if (record_index < 1 || record_index > doc->nurls) - return NULL; + return nullptr; else return (doc->urls[record_index - 1]); } int plkr_HasRecordWithID ( plkr_Document* doc, int record_index ) { - return (FindRecordByIndex (doc, record_index) != NULL); + return (FindRecordByIndex (doc, record_index) != nullptr); } int plkr_GetRecordType ( plkr_Document* doc, int record_index ) { plkr_DataRecord* r; r = FindRecordByIndex (doc, record_index); if (r) return r->type; else return PLKR_DRTYPE_NONE; } int plkr_GetRecordCharset ( plkr_Document* doc, int record_index ) { plkr_DataRecord* r; r = FindRecordByIndex (doc, record_index); if (r && ((r->type == PLKR_DRTYPE_TEXT_COMPRESSED) || (r->type == PLKR_DRTYPE_TEXT))) { if (r->charset_mibenum == 0) return doc->default_charset_mibenum; else return r->charset_mibenum; } else return 0; } diff --git a/generators/plucker/unpluck/util.cpp b/generators/plucker/unpluck/util.cpp index 54089677d..659610afb 100644 --- a/generators/plucker/unpluck/util.cpp +++ b/generators/plucker/unpluck/util.cpp @@ -1,262 +1,262 @@ /* -*- mode: c; indent-tabs-mode: nil; -*- * $Id: util.c,v 1.3 2003/12/28 20:59:21 chrish Exp $ * * util -- Some simple utility routines so we don't need GLib * Copyright (c) 2002, Bill Janssen * * 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. * */ #if !defined(WIN32) #include /* for lseek, etc. */ #else #include #endif #include #include #include /* for _plkr_message */ #include /* for stderr */ #include /* for fstat() */ #include /* for strndup() */ #include /* for errno */ #include /* for O_RDONLY */ #include /* for assert() */ #include #include "unpluck.h" #include "unpluckint.h" /***********************************************************************/ /***********************************************************************/ /***** *****/ /***** Messages *****/ /***** *****/ /***********************************************************************/ /***********************************************************************/ static int ShowMessages = 0; void _plkr_message ( const char* formatSpec, ... ) { va_list ap; va_start (ap, formatSpec); if (ShowMessages) { (void) vfprintf (stderr, formatSpec, ap); fprintf (stderr, "\n"); } va_end (ap); } int plkr_ShowMessages ( int val ) { int oldval = ShowMessages; ShowMessages = val; return oldval; } /***********************************************************************/ /***********************************************************************/ /***** *****/ /***** String Utilities *****/ /***** *****/ /***********************************************************************/ /***********************************************************************/ char* _plkr_strndup ( const char* str, int len ) { char* dup; dup = (char *) malloc (len + 1); strncpy (dup, str, len); dup[len] = 0; return dup; } /***********************************************************************/ /***********************************************************************/ /***** *****/ /***** Simple hash table maps string keys to void * values *****/ /***** *****/ /***********************************************************************/ /***********************************************************************/ typedef struct { char* he_key; void* he_data; } HashEntry; typedef struct { int hs_count; int hs_allocated; HashEntry* hs_entries; } HashTableSlot; struct HashTable { int ht_size; int ht_nPairs; HashTableSlot* ht_slots; }; #define HASH_INCREMENT_SIZE 5 #define hashtable_slot(ht,index) (&((ht)->ht_slots[index])) #define hashtable_hash_index(ht,key) (HashString((key), (ht)->ht_size)) #define hashtable_compare_keys(ht,key1,key2) (CompareStrings((key1),(key2))) static int CompareStrings ( const char* key1, const char* key2 ) { return (strcmp (key1, key2) == 0); } static int HashString ( const char* str, int size ) { unsigned long crc; - crc = crc32 (0L, NULL, 0); + crc = crc32 (0L, nullptr, 0); crc = crc32 (crc, (const Bytef*)str, strlen (str)); return (crc % size); } void* _plkr_FindInTable ( HashTable* ht, const char* key ) { HashTableSlot* slot; int count; - if (ht == NULL) - return (NULL); + if (ht == nullptr) + return (nullptr); slot = hashtable_slot (ht, hashtable_hash_index (ht, key)); for (count = slot->hs_count; count > 0; count -= 1) if (hashtable_compare_keys (ht, key, slot->hs_entries[count - 1].he_key)) return (slot->hs_entries[count - 1].he_data); - return (NULL); + return (nullptr); } void* _plkr_RemoveFromTable ( HashTable* ht, const char* key ) { HashTableSlot* slot; int count; - if (ht == NULL) - return (NULL); + if (ht == nullptr) + return (nullptr); slot = hashtable_slot (ht, hashtable_hash_index (ht, key)); for (count = 0; count < slot->hs_count; count += 1) if (hashtable_compare_keys (ht, slot->hs_entries[count].he_key, key)) { void *data = slot->hs_entries[count].he_data; free (slot->hs_entries[count].he_key); if ((1 + (unsigned) count) < (unsigned) slot->hs_count) slot->hs_entries[count] = slot->hs_entries[slot->hs_count - 1]; --ht->ht_nPairs; if (--slot->hs_count <= 0) { free (slot->hs_entries); - slot->hs_entries = NULL; + slot->hs_entries = nullptr; slot->hs_allocated = 0; slot->hs_count = 0; } return (data); } - return (NULL); + return (nullptr); } int _plkr_AddToTable ( HashTable* ht, const char* key, void* obj ) { HashTableSlot* slot; int count; - if (ht == NULL) + if (ht == nullptr) return (0); slot = hashtable_slot (ht, hashtable_hash_index (ht, key)); for (count = slot->hs_count; count > 0; count -= 1) if (hashtable_compare_keys (ht, key, slot->hs_entries[count - 1].he_key)) return (0); if (slot->hs_allocated == 0) { slot->hs_allocated = HASH_INCREMENT_SIZE; slot->hs_entries = (HashEntry *) malloc (sizeof (HashEntry) * slot->hs_allocated); slot->hs_count = 0; } else if (slot->hs_count >= slot->hs_allocated) slot->hs_entries = (HashEntry *) realloc (slot->hs_entries, (slot->hs_allocated += HASH_INCREMENT_SIZE) * sizeof (HashEntry)); slot->hs_entries[slot->hs_count].he_key = _plkr_strndup (key, strlen (key)); slot->hs_entries[slot->hs_count].he_data = obj; slot->hs_count += 1; ht->ht_nPairs += 1; return (1); } HashTable* _plkr_NewHashTable ( int size ) { HashTable *newHash = (HashTable *) malloc (sizeof (HashTable)); newHash->ht_size = size; newHash->ht_nPairs = 0; newHash->ht_slots = (HashTableSlot *) malloc (sizeof (HashTableSlot) * size); memset ((void *) (newHash->ht_slots), 0, sizeof (HashTableSlot) * size); return (newHash); } diff --git a/generators/poppler/annots.cpp b/generators/poppler/annots.cpp index 46a0eefd8..e3e4fd73b 100644 --- a/generators/poppler/annots.cpp +++ b/generators/poppler/annots.cpp @@ -1,431 +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; + Okular::Annotation *annotation = nullptr; *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 ); for (Okular::HighlightAnnotation::Quad &quad : hlann->highlightQuads()) { 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/poppler/generator_pdf.cpp b/generators/poppler/generator_pdf.cpp index 6c6493f1e..6e7631263 100644 --- a/generators/poppler/generator_pdf.cpp +++ b/generators/poppler/generator_pdf.cpp @@ -1,2037 +1,2037 @@ /*************************************************************************** * Copyright (C) 2004-2008 by Albert Astals Cid * * Copyright (C) 2004 by Enrico Ros * * Copyright (C) 2012 by Guillermo A. Amaral B. * * Copyright (C) 2017 Klarälvdalens Datakonsult AB, a KDAB Group * * company, info@kdab.com. Work sponsored by the * * LiMux project of the city of Munich * * Copyright (C) 2019 by Oliver Sander * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * ***************************************************************************/ #include #include "generator_pdf.h" // qt/kde includes #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "ui_pdfsettingswidget.h" #include "pdfsettings.h" #include #include #ifdef HAVE_POPPLER_0_73 #include #endif #include "debug_pdf.h" #include "annots.h" #include "formfields.h" #include "popplerembeddedfile.h" Q_DECLARE_METATYPE(Poppler::Annotation*) Q_DECLARE_METATYPE(Poppler::FontInfo) Q_DECLARE_METATYPE(const Poppler::LinkMovie*) Q_DECLARE_METATYPE(const Poppler::LinkRendition*) Q_DECLARE_METATYPE(const Poppler::LinkOCGState*) static const int defaultPageWidth = 595; static const int defaultPageHeight = 842; class PDFOptionsPage : public Okular::PrintOptionsWidget { Q_OBJECT public: enum ScaleMode { FitToPrintableArea, FitToPage, None }; Q_ENUM(ScaleMode) PDFOptionsPage() { setWindowTitle( i18n( "PDF Options" ) ); QVBoxLayout *layout = new QVBoxLayout(this); m_printAnnots = new QCheckBox(i18n("Print annotations"), this); m_printAnnots->setToolTip(i18n("Include annotations in the printed document")); m_printAnnots->setWhatsThis(i18n("Includes annotations in the printed document. You can disable this if you want to print the original unannotated document.")); layout->addWidget(m_printAnnots); m_forceRaster = new QCheckBox(i18n("Force rasterization"), this); m_forceRaster->setToolTip(i18n("Rasterize into an image before printing")); m_forceRaster->setWhatsThis(i18n("Forces the rasterization of each page into an image before printing it. This usually gives somewhat worse results, but is useful when printing documents that appear to print incorrectly.")); layout->addWidget(m_forceRaster); QWidget* formWidget = new QWidget(this); QFormLayout* printBackendLayout = new QFormLayout(formWidget); m_scaleMode = new QComboBox; m_scaleMode->insertItem(FitToPrintableArea, i18n("Fit to printable area"), FitToPrintableArea); m_scaleMode->insertItem(FitToPage, i18n("Fit to full page"), FitToPage); m_scaleMode->insertItem(None, i18n("None; print original size"), None); m_scaleMode->setToolTip(i18n( "Scaling mode for the printed pages" ) ); printBackendLayout->addRow(i18n("Scale mode:"), m_scaleMode); layout->addWidget(formWidget); layout->addStretch(1); setPrintAnnots( true ); // Default value } bool ignorePrintMargins() const override { return scaleMode() == FitToPage; } bool printAnnots() { return m_printAnnots->isChecked(); } void setPrintAnnots( bool printAnnots ) { m_printAnnots->setChecked( printAnnots ); } bool printForceRaster() { return m_forceRaster->isChecked(); } void setPrintForceRaster( bool forceRaster ) { m_forceRaster->setChecked( forceRaster ); } ScaleMode scaleMode() const { return m_scaleMode->currentData().value(); } private: QCheckBox *m_printAnnots; QCheckBox *m_forceRaster; QComboBox *m_scaleMode; }; static void fillViewportFromLinkDestination( Okular::DocumentViewport &viewport, const Poppler::LinkDestination &destination ) { viewport.pageNumber = destination.pageNumber() - 1; if (!viewport.isValid()) return; // get destination position // TODO add other attributes to the viewport (taken from link) // switch ( destination->getKind() ) // { // case destXYZ: if (destination.isChangeLeft() || destination.isChangeTop()) { // TODO remember to change this if we implement DPI and/or rotation double left, top; left = destination.left(); top = destination.top(); viewport.rePos.normalizedX = left; viewport.rePos.normalizedY = top; viewport.rePos.enabled = true; viewport.rePos.pos = Okular::DocumentViewport::TopLeft; } /* TODO if ( dest->getChangeZoom() ) make zoom change*/ /* break; default: // implement the others cases break;*/ // } } Okular::Sound* createSoundFromPopplerSound( const Poppler::SoundObject *popplerSound ) { Okular::Sound *sound = popplerSound->soundType() == Poppler::SoundObject::Embedded ? new Okular::Sound( popplerSound->data() ) : new Okular::Sound( popplerSound->url() ); sound->setSamplingRate( popplerSound->samplingRate() ); sound->setChannels( popplerSound->channels() ); sound->setBitsPerSample( popplerSound->bitsPerSample() ); switch ( popplerSound->soundEncoding() ) { case Poppler::SoundObject::Raw: sound->setSoundEncoding( Okular::Sound::Raw ); break; case Poppler::SoundObject::Signed: sound->setSoundEncoding( Okular::Sound::Signed ); break; case Poppler::SoundObject::muLaw: sound->setSoundEncoding( Okular::Sound::muLaw ); break; case Poppler::SoundObject::ALaw: sound->setSoundEncoding( Okular::Sound::ALaw ); break; } return sound; } Okular::Movie* createMovieFromPopplerMovie( const Poppler::MovieObject *popplerMovie ) { Okular::Movie *movie = new Okular::Movie( popplerMovie->url() ); movie->setSize( popplerMovie->size() ); movie->setRotation( (Okular::Rotation)( popplerMovie->rotation() / 90 ) ); movie->setShowControls( popplerMovie->showControls() ); movie->setPlayMode( (Okular::Movie::PlayMode)popplerMovie->playMode() ); movie->setAutoPlay( false ); // will be triggered by external MovieAnnotation movie->setShowPosterImage( popplerMovie->showPosterImage() ); movie->setPosterImage( popplerMovie->posterImage() ); return movie; } Okular::Movie* createMovieFromPopplerScreen( const Poppler::LinkRendition *popplerScreen ) { Poppler::MediaRendition *rendition = popplerScreen->rendition(); - Okular::Movie *movie = 0; + Okular::Movie *movie = nullptr; if ( rendition->isEmbedded() ) movie = new Okular::Movie( rendition->fileName(), rendition->data() ); else movie = new Okular::Movie( rendition->fileName() ); movie->setSize( rendition->size() ); movie->setShowControls( rendition->showControls() ); if ( rendition->repeatCount() == 0 ) { movie->setPlayMode( Okular::Movie::PlayRepeat ); } else { movie->setPlayMode( Okular::Movie::PlayLimited ); movie->setPlayRepetitions( rendition->repeatCount() ); } movie->setAutoPlay( rendition->autoPlay() ); return movie; } QPair createMovieFromPopplerRichMedia( const Poppler::RichMediaAnnotation *popplerRichMedia ) { const QPair emptyResult(0, 0); /** * To convert a Flash/Video based RichMedia annotation to a movie, we search for the first * Flash/Video richmedia instance and parse the flashVars parameter for the 'source' identifier. * That identifier is then used to find the associated embedded file through the assets * mapping. */ const Poppler::RichMediaAnnotation::Content *content = popplerRichMedia->content(); if ( !content ) return emptyResult; const QList configurations = content->configurations(); if ( configurations.isEmpty() ) return emptyResult; const Poppler::RichMediaAnnotation::Configuration *configuration = configurations[0]; const QList instances = configuration->instances(); if ( instances.isEmpty() ) return emptyResult; const Poppler::RichMediaAnnotation::Instance *instance = instances[0]; if ( ( instance->type() != Poppler::RichMediaAnnotation::Instance::TypeFlash ) && ( instance->type() != Poppler::RichMediaAnnotation::Instance::TypeVideo ) ) return emptyResult; const Poppler::RichMediaAnnotation::Params *params = instance->params(); if ( !params ) return emptyResult; QString sourceId; bool playbackLoops = false; const QStringList flashVars = params->flashVars().split( QLatin1Char( '&' ) ); for ( const QString &flashVar : flashVars ) { const int pos = flashVar.indexOf( QLatin1Char( '=' ) ); if ( pos == -1 ) continue; const QString key = flashVar.left( pos ); const QString value = flashVar.mid( pos + 1 ); if ( key == QLatin1String( "source" ) ) sourceId = value; else if ( key == QLatin1String( "loop" ) ) playbackLoops = ( value == QLatin1String( "true" ) ? true : false ); } if ( sourceId.isEmpty() ) return emptyResult; const QList assets = content->assets(); if ( assets.isEmpty() ) return emptyResult; - Poppler::RichMediaAnnotation::Asset *matchingAsset = 0; + Poppler::RichMediaAnnotation::Asset *matchingAsset = nullptr; for ( Poppler::RichMediaAnnotation::Asset *asset : assets ) { if ( asset->name() == sourceId ) { matchingAsset = asset; break; } } if ( !matchingAsset ) return emptyResult; Poppler::EmbeddedFile *embeddedFile = matchingAsset->embeddedFile(); if ( !embeddedFile ) return emptyResult; Okular::EmbeddedFile *pdfEmbeddedFile = new PDFEmbeddedFile( embeddedFile ); Okular::Movie *movie = new Okular::Movie( embeddedFile->name(), embeddedFile->data() ); movie->setPlayMode( playbackLoops ? Okular::Movie::PlayRepeat : Okular::Movie::PlayLimited ); if ( popplerRichMedia && popplerRichMedia->settings() && popplerRichMedia->settings()->activation() ) { if ( popplerRichMedia->settings()->activation()->condition() == Poppler::RichMediaAnnotation::Activation::PageOpened || popplerRichMedia->settings()->activation()->condition() == Poppler::RichMediaAnnotation::Activation::PageVisible ) { movie->setAutoPlay( true ); } else { movie->setAutoPlay( false ); } } else { movie->setAutoPlay( false ); } return qMakePair(movie, pdfEmbeddedFile); } /** * Note: the function will take ownership of the popplerLink object. */ Okular::Action* createLinkFromPopplerLink(const Poppler::Link *popplerLink, bool deletePopplerLink = true) { if (!popplerLink) return nullptr; - Okular::Action *link = 0; + Okular::Action *link = nullptr; const Poppler::LinkGoto *popplerLinkGoto; const Poppler::LinkExecute *popplerLinkExecute; const Poppler::LinkBrowse *popplerLinkBrowse; const Poppler::LinkAction *popplerLinkAction; const Poppler::LinkSound *popplerLinkSound; const Poppler::LinkJavaScript *popplerLinkJS; const Poppler::LinkMovie *popplerLinkMovie; const Poppler::LinkRendition *popplerLinkRendition; Okular::DocumentViewport viewport; switch(popplerLink->linkType()) { case Poppler::Link::None: break; case Poppler::Link::Goto: { popplerLinkGoto = static_cast(popplerLink); const Poppler::LinkDestination dest = popplerLinkGoto->destination(); const QString destName = dest.destinationName(); if (destName.isEmpty()) { fillViewportFromLinkDestination( viewport, dest ); link = new Okular::GotoAction(popplerLinkGoto->fileName(), viewport); } else { link = new Okular::GotoAction(popplerLinkGoto->fileName(), destName); } } break; case Poppler::Link::Execute: popplerLinkExecute = static_cast(popplerLink); link = new Okular::ExecuteAction( popplerLinkExecute->fileName(), popplerLinkExecute->parameters() ); break; case Poppler::Link::Browse: popplerLinkBrowse = static_cast(popplerLink); link = new Okular::BrowseAction( QUrl(popplerLinkBrowse->url()) ); break; case Poppler::Link::Action: popplerLinkAction = static_cast(popplerLink); link = new Okular::DocumentAction( (Okular::DocumentAction::DocumentActionType)popplerLinkAction->actionType() ); break; case Poppler::Link::Sound: { popplerLinkSound = static_cast(popplerLink); Poppler::SoundObject *popplerSound = popplerLinkSound->sound(); Okular::Sound *sound = createSoundFromPopplerSound( popplerSound ); link = new Okular::SoundAction( popplerLinkSound->volume(), popplerLinkSound->synchronous(), popplerLinkSound->repeat(), popplerLinkSound->mix(), sound ); } break; case Poppler::Link::JavaScript: { popplerLinkJS = static_cast(popplerLink); link = new Okular::ScriptAction( Okular::JavaScript, popplerLinkJS->script() ); } break; case Poppler::Link::Rendition: { if (!deletePopplerLink) { // If links should not be deleted it probably means that they // are part of a nextActions chain. There is no support // to resolveMediaLinkReferences on nextActions. It would also // be necessary to ensure that resolveMediaLinkReferences does // not delete the Links which are part of a nextActions list // to avoid a double deletion. qCDebug(OkularPdfDebug) << "parsing rendition link without deletion is not supported. Action chain might be broken."; break; } deletePopplerLink = false; // we'll delete it inside resolveMediaLinkReferences() after we have resolved all references popplerLinkRendition = static_cast( popplerLink ); Okular::RenditionAction::OperationType operation = Okular::RenditionAction::None; switch ( popplerLinkRendition->action() ) { case Poppler::LinkRendition::NoRendition: operation = Okular::RenditionAction::None; break; case Poppler::LinkRendition::PlayRendition: operation = Okular::RenditionAction::Play; break; case Poppler::LinkRendition::StopRendition: operation = Okular::RenditionAction::Stop; break; case Poppler::LinkRendition::PauseRendition: operation = Okular::RenditionAction::Pause; break; case Poppler::LinkRendition::ResumeRendition: operation = Okular::RenditionAction::Resume; break; }; - Okular::Movie *movie = 0; + Okular::Movie *movie = nullptr; if ( popplerLinkRendition->rendition() ) movie = createMovieFromPopplerScreen( popplerLinkRendition ); Okular::RenditionAction *renditionAction = new Okular::RenditionAction( operation, movie, Okular::JavaScript, popplerLinkRendition->script() ); renditionAction->setNativeId( QVariant::fromValue( popplerLinkRendition ) ); link = renditionAction; } break; case Poppler::Link::Movie: { if (!deletePopplerLink) { // See comment above in Link::Rendition qCDebug(OkularPdfDebug) << "parsing movie link without deletion is not supported. Action chain might be broken."; break; } deletePopplerLink = false; // we'll delete it inside resolveMediaLinkReferences() after we have resolved all references popplerLinkMovie = static_cast( popplerLink ); Okular::MovieAction::OperationType operation = Okular::MovieAction::Play; switch ( popplerLinkMovie->operation() ) { case Poppler::LinkMovie::Play: operation = Okular::MovieAction::Play; break; case Poppler::LinkMovie::Stop: operation = Okular::MovieAction::Stop; break; case Poppler::LinkMovie::Pause: operation = Okular::MovieAction::Pause; break; case Poppler::LinkMovie::Resume: operation = Okular::MovieAction::Resume; break; }; Okular::MovieAction *movieAction = new Okular::MovieAction( operation ); movieAction->setNativeId( QVariant::fromValue( popplerLinkMovie ) ); link = movieAction; } break; #ifdef HAVE_POPPLER_0_64 case Poppler::Link::Hide: { const Poppler::LinkHide * l = static_cast( popplerLink ); QStringList scripts; for ( const QString &target: l->targets() ) { scripts << QStringLiteral( "getField(\"%1\").hidden = %2;" ).arg( target ).arg( l->isShowAction() ? QLatin1String( "false" ) : QLatin1String( "true" ) ); } link = new Okular::ScriptAction( Okular::JavaScript, scripts.join( QLatin1Char( '\n' ) ) ); } break; #endif case Poppler::Link::OCGState: link = new Okular::BackendOpaqueAction(); link->setNativeId( QVariant::fromValue( static_cast( popplerLink ) ) ); deletePopplerLink = false; break; } #ifdef HAVE_POPPLER_0_64 if (link) { QVector< Okular::Action * > nextActions; for ( const Poppler::Link *nl : popplerLink->nextLinks() ) { nextActions << createLinkFromPopplerLink( nl, false ); } link->setNextActions( nextActions ); } #endif if ( deletePopplerLink ) delete popplerLink; return link; } /** * Note: the function will take ownership of the popplerLink objects. */ static QLinkedList generateLinks( const QList &popplerLinks ) { QLinkedList links; for (const Poppler::Link *popplerLink : popplerLinks) { QRectF linkArea = popplerLink->linkArea(); double nl = linkArea.left(), nt = linkArea.top(), nr = linkArea.right(), nb = linkArea.bottom(); // create the rect using normalized coords and attach the Okular::Link to it Okular::ObjectRect * rect = new Okular::ObjectRect( nl, nt, nr, nb, false, Okular::ObjectRect::Action, createLinkFromPopplerLink(popplerLink) ); // add the ObjectRect to the container links.push_front( rect ); } return links; } /** NOTES on threading: * internal: thread race prevention is done via the 'docLock' mutex. the * mutex is needed only because we have the asynchronous thread; else * the operations are all within the 'gui' thread, scheduled by the * Qt scheduler and no mutex is needed. * external: dangerous operations are all locked via mutex internally, and the * only needed external thing is the 'canGeneratePixmap' method * that tells if the generator is free (since we don't want an * internal queue to store PixmapRequests). A generatedPixmap call * without the 'ready' flag set, results in undefined behavior. * So, as example, printing while generating a pixmap asynchronously is safe, * it might only block the gui thread by 1) waiting for the mutex to unlock * in async thread and 2) doing the 'heavy' print operation. */ OKULAR_EXPORT_PLUGIN(PDFGenerator, "libokularGenerator_poppler.json") static void PDFGeneratorPopplerDebugFunction(const QString &message, const QVariant &closure) { Q_UNUSED(closure); qCDebug(OkularPdfDebug) << "[Poppler]" << message; } PDFGenerator::PDFGenerator( QObject *parent, const QVariantList &args ) - : Generator( parent, args ), pdfdoc( 0 ), + : Generator( parent, args ), pdfdoc( nullptr ), docSynopsisDirty( true ), docEmbeddedFilesDirty( true ), nextFontPage( 0 ), - annotProxy( 0 ) + annotProxy( nullptr ) { setFeature( Threaded ); setFeature( TextExtraction ); setFeature( FontInfo ); #ifdef Q_OS_WIN32 setFeature( PrintNative ); #else setFeature( PrintPostscript ); #endif if ( Okular::FilePrinter::ps2pdfAvailable() ) setFeature( PrintToFile ); setFeature( ReadRawData ); setFeature( TiledRendering ); setFeature( SwapBackingFile ); #ifdef HAVE_POPPLER_0_63 setFeature( SupportsCancelling ); #endif // You only need to do it once not for each of the documents but it is cheap enough // so doing it all the time won't hurt either Poppler::setDebugErrorFunction(PDFGeneratorPopplerDebugFunction, QVariant()); } PDFGenerator::~PDFGenerator() { delete pdfOptionsPage; } //BEGIN Generator inherited functions Okular::Document::OpenResult PDFGenerator::loadDocumentWithPassword( const QString & filePath, QVector & pagesVector, const QString &password ) { #ifndef NDEBUG if ( pdfdoc ) { qCDebug(OkularPdfDebug) << "PDFGenerator: multiple calls to loadDocument. Check it."; return Okular::Document::OpenError; } #endif // create PDFDoc for the given file - pdfdoc = Poppler::Document::load( filePath, 0, 0 ); + pdfdoc = Poppler::Document::load( filePath, nullptr, nullptr ); return init(pagesVector, password); } Okular::Document::OpenResult PDFGenerator::loadDocumentFromDataWithPassword( const QByteArray & fileData, QVector & pagesVector, const QString &password ) { #ifndef NDEBUG if ( pdfdoc ) { qCDebug(OkularPdfDebug) << "PDFGenerator: multiple calls to loadDocument. Check it."; return Okular::Document::OpenError; } #endif // create PDFDoc for the given file - pdfdoc = Poppler::Document::loadFromData( fileData, 0, 0 ); + pdfdoc = Poppler::Document::loadFromData( fileData, nullptr, nullptr ); return init(pagesVector, password); } Okular::Document::OpenResult PDFGenerator::init(QVector & pagesVector, const QString &password) { if ( !pdfdoc ) return Okular::Document::OpenError; if ( pdfdoc->isLocked() ) { pdfdoc->unlock( password.toLatin1(), password.toLatin1() ); if ( pdfdoc->isLocked() ) { delete pdfdoc; pdfdoc = nullptr; return Okular::Document::OpenNeedsPassword; } } // build Pages (currentPage was set -1 by deletePages) int pageCount = pdfdoc->numPages(); if (pageCount < 0) { delete pdfdoc; pdfdoc = nullptr; return Okular::Document::OpenError; } pagesVector.resize(pageCount); rectsGenerated.fill(false, pageCount); annotationsOnOpenHash.clear(); loadPages(pagesVector, 0, false); // update the configuration reparseConfig(); // create annotation proxy annotProxy = new PopplerAnnotationProxy( pdfdoc, userMutex(), &annotationsOnOpenHash ); // the file has been loaded correctly return Okular::Document::OpenSuccess; } PDFGenerator::SwapBackingFileResult PDFGenerator::swapBackingFile( QString const &newFileName, QVector & newPagesVector ) { const QBitArray oldRectsGenerated = rectsGenerated; doCloseDocument(); auto openResult = loadDocumentWithPassword(newFileName, newPagesVector, QString()); if (openResult != Okular::Document::OpenSuccess) return SwapBackingFileError; // Recreate links if needed since they are done on image() and image() is not called when swapping the file // since the page is already rendered if (oldRectsGenerated.count() == rectsGenerated.count()) { for (int i = 0; i < oldRectsGenerated.count(); ++i) { if (oldRectsGenerated[i]) { Okular::Page *page = newPagesVector[i]; Poppler::Page *pp = pdfdoc->page( i ); if (pp) { page->setObjectRects(generateLinks(pp->links())); rectsGenerated[i] = true; resolveMediaLinkReferences(page); delete pp; } } } } return SwapBackingFileReloadInternalData; } bool PDFGenerator::doCloseDocument() { // remove internal objects userMutex()->lock(); delete annotProxy; - annotProxy = 0; + annotProxy = nullptr; delete pdfdoc; pdfdoc = nullptr; userMutex()->unlock(); docSynopsisDirty = true; docSyn.clear(); docEmbeddedFilesDirty = true; qDeleteAll(docEmbeddedFiles); docEmbeddedFiles.clear(); nextFontPage = 0; rectsGenerated.clear(); return true; } void PDFGenerator::loadPages(QVector &pagesVector, int rotation, bool clear) { // TODO XPDF 3.01 check const int count = pagesVector.count(); double w = 0, h = 0; for ( int i = 0; i < count ; i++ ) { // get xpdf page Poppler::Page * p = pdfdoc->page( i ); Okular::Page * page; if (p) { const QSizeF pSize = p->pageSizeF(); w = pSize.width() / 72.0 * dpi().width(); h = pSize.height() / 72.0 * dpi().height(); Okular::Rotation orientation = Okular::Rotation0; switch (p->orientation()) { case Poppler::Page::Landscape: orientation = Okular::Rotation90; break; case Poppler::Page::UpsideDown: orientation = Okular::Rotation180; break; case Poppler::Page::Seascape: orientation = Okular::Rotation270; break; case Poppler::Page::Portrait: orientation = Okular::Rotation0; break; } if (rotation % 2 == 1) qSwap(w,h); // init a Okular::page, add transition and annotation information page = new Okular::Page( i, w, h, orientation ); addTransition( p, page ); if ( true ) //TODO real check addAnnotations( p, page ); Poppler::Link * tmplink = p->action( Poppler::Page::Opening ); if ( tmplink ) { page->setPageAction( Okular::Page::Opening, createLinkFromPopplerLink( tmplink ) ); } tmplink = p->action( Poppler::Page::Closing ); if ( tmplink ) { page->setPageAction( Okular::Page::Closing, createLinkFromPopplerLink( tmplink ) ); } page->setDuration( p->duration() ); page->setLabel( p->label() ); addFormFields( p, page ); // kWarning(PDFDebug).nospace() << page->width() << "x" << page->height(); #ifdef PDFGENERATOR_DEBUG qCDebug(OkularPdfDebug) << "load page" << i << "with rotation" << rotation << "and orientation" << orientation; #endif delete p; if (clear && pagesVector[i]) delete pagesVector[i]; } else { page = new Okular::Page( i, defaultPageWidth, defaultPageHeight, Okular::Rotation0 ); } // set the Okular::page at the right position in document's pages vector pagesVector[i] = page; } } Okular::DocumentInfo PDFGenerator::generateDocumentInfo( const QSet &keys ) const { Okular::DocumentInfo docInfo; docInfo.set( Okular::DocumentInfo::MimeType, QStringLiteral("application/pdf") ); userMutex()->lock(); if ( pdfdoc ) { // compile internal structure reading properties from PDFDoc if ( keys.contains( Okular::DocumentInfo::Title ) ) docInfo.set( Okular::DocumentInfo::Title, pdfdoc->info(QStringLiteral("Title")) ); if ( keys.contains( Okular::DocumentInfo::Subject ) ) docInfo.set( Okular::DocumentInfo::Subject, pdfdoc->info(QStringLiteral("Subject")) ); if ( keys.contains( Okular::DocumentInfo::Author ) ) docInfo.set( Okular::DocumentInfo::Author, pdfdoc->info(QStringLiteral("Author")) ); if ( keys.contains( Okular::DocumentInfo::Keywords ) ) docInfo.set( Okular::DocumentInfo::Keywords, pdfdoc->info(QStringLiteral("Keywords")) ); if ( keys.contains( Okular::DocumentInfo::Creator ) ) docInfo.set( Okular::DocumentInfo::Creator, pdfdoc->info(QStringLiteral("Creator")) ); if ( keys.contains( Okular::DocumentInfo::Producer ) ) docInfo.set( Okular::DocumentInfo::Producer, pdfdoc->info(QStringLiteral("Producer")) ); if ( keys.contains( Okular::DocumentInfo::CreationDate ) ) docInfo.set( Okular::DocumentInfo::CreationDate, QLocale().toString( pdfdoc->date(QStringLiteral("CreationDate")), QLocale::LongFormat ) ); if ( keys.contains( Okular::DocumentInfo::ModificationDate ) ) docInfo.set( Okular::DocumentInfo::ModificationDate, QLocale().toString( pdfdoc->date(QStringLiteral("ModDate")), QLocale::LongFormat ) ); if ( keys.contains( Okular::DocumentInfo::CustomKeys ) ) { int major, minor; pdfdoc->getPdfVersion(&major, &minor); docInfo.set( QStringLiteral("format"), i18nc( "PDF v. ", "PDF v. %1.%2", major, minor ), i18n( "Format" ) ); docInfo.set( QStringLiteral("encryption"), pdfdoc->isEncrypted() ? i18n( "Encrypted" ) : i18n( "Unencrypted" ), i18n("Security") ); docInfo.set( QStringLiteral("optimization"), pdfdoc->isLinearized() ? i18n( "Yes" ) : i18n( "No" ), i18n("Optimized") ); } docInfo.set( Okular::DocumentInfo::Pages, QString::number( pdfdoc->numPages() ) ); } userMutex()->unlock(); return docInfo; } const Okular::DocumentSynopsis * PDFGenerator::generateDocumentSynopsis() { if ( !docSynopsisDirty ) return &docSyn; if ( !pdfdoc ) - return NULL; + return nullptr; userMutex()->lock(); QDomDocument *toc = pdfdoc->toc(); userMutex()->unlock(); if ( !toc ) - return NULL; + return nullptr; addSynopsisChildren(toc, &docSyn); delete toc; docSynopsisDirty = false; return &docSyn; } static Okular::FontInfo::FontType convertPopplerFontInfoTypeToOkularFontInfoType( Poppler::FontInfo::Type type ) { switch ( type ) { case Poppler::FontInfo::Type1: return Okular::FontInfo::Type1; break; case Poppler::FontInfo::Type1C: return Okular::FontInfo::Type1C; break; case Poppler::FontInfo::Type3: return Okular::FontInfo::Type3; break; case Poppler::FontInfo::TrueType: return Okular::FontInfo::TrueType; break; case Poppler::FontInfo::CIDType0: return Okular::FontInfo::CIDType0; break; case Poppler::FontInfo::CIDType0C: return Okular::FontInfo::CIDType0C; break; case Poppler::FontInfo::CIDTrueType: return Okular::FontInfo::CIDTrueType; break; case Poppler::FontInfo::Type1COT: return Okular::FontInfo::Type1COT; break; case Poppler::FontInfo::TrueTypeOT: return Okular::FontInfo::TrueTypeOT; break; case Poppler::FontInfo::CIDType0COT: return Okular::FontInfo::CIDType0COT; break; case Poppler::FontInfo::CIDTrueTypeOT: return Okular::FontInfo::CIDTrueTypeOT; break; case Poppler::FontInfo::unknown: default: ; } return Okular::FontInfo::Unknown; } static Okular::FontInfo::EmbedType embedTypeForPopplerFontInfo( const Poppler::FontInfo &fi ) { Okular::FontInfo::EmbedType ret = Okular::FontInfo::NotEmbedded; if ( fi.isEmbedded() ) { if ( fi.isSubset() ) { ret = Okular::FontInfo::EmbeddedSubset; } else { ret = Okular::FontInfo::FullyEmbedded; } } return ret; } Okular::FontInfo::List PDFGenerator::fontsForPage( int page ) { Okular::FontInfo::List list; if ( page != nextFontPage ) return list; QList fonts; userMutex()->lock(); Poppler::FontIterator* it = pdfdoc->newFontIterator(page); if (it->hasNext()) { fonts = it->next(); } userMutex()->unlock(); for (const Poppler::FontInfo &font : qAsConst(fonts)) { Okular::FontInfo of; of.setName( font.name() ); #ifdef HAVE_POPPLER_0_80 of.setSubstituteName( font.substituteName() ); #endif of.setType( convertPopplerFontInfoTypeToOkularFontInfoType( font.type() ) ); of.setEmbedType( embedTypeForPopplerFontInfo( font) ); of.setFile( font.file() ); of.setCanBeExtracted( of.embedType() != Okular::FontInfo::NotEmbedded ); QVariant nativeId; nativeId.setValue( font ); of.setNativeId( nativeId ); list.append( of ); } ++nextFontPage; return list; } const QList *PDFGenerator::embeddedFiles() const { if (docEmbeddedFilesDirty) { userMutex()->lock(); const QList &popplerFiles = pdfdoc->embeddedFiles(); for (Poppler::EmbeddedFile *pef : popplerFiles) { docEmbeddedFiles.append(new PDFEmbeddedFile(pef)); } userMutex()->unlock(); docEmbeddedFilesDirty = false; } return &docEmbeddedFiles; } QAbstractItemModel* PDFGenerator::layersModel() const { - return pdfdoc->hasOptionalContent() ? pdfdoc->optionalContentModel() : NULL; + return pdfdoc->hasOptionalContent() ? pdfdoc->optionalContentModel() : nullptr; } void PDFGenerator::opaqueAction( const Okular::BackendOpaqueAction *action ) { const Poppler::LinkOCGState *popplerLink = action->nativeId().value(); pdfdoc->optionalContentModel()->applyLink( const_cast< Poppler::LinkOCGState* >( popplerLink ) ); } bool PDFGenerator::isAllowed( Okular::Permission permission ) const { bool b = true; switch ( permission ) { case Okular::AllowModify: b = pdfdoc->okToChange(); break; case Okular::AllowCopy: b = pdfdoc->okToCopy(); break; case Okular::AllowPrint: b = pdfdoc->okToPrint(); break; case Okular::AllowNotes: b = pdfdoc->okToAddNotes(); break; case Okular::AllowFillForms: b = pdfdoc->okToFillForm(); break; default: ; } return b; } struct RenderImagePayload { RenderImagePayload(PDFGenerator *g, Okular::PixmapRequest *r) : generator(g), request(r) { // Don't report partial updates for the first 500 ms timer.setInterval(500); timer.setSingleShot(true); timer.start(); } PDFGenerator *generator; Okular::PixmapRequest *request; QTimer timer; }; Q_DECLARE_METATYPE(RenderImagePayload*) static bool shouldDoPartialUpdateCallback(const QVariant &vPayload) { auto payload = vPayload.value(); // Since the timer lives in a thread without an event loop we need to stop it ourselves // when the remaining time has reached 0 if (payload->timer.isActive() && payload->timer.remainingTime() == 0) { payload->timer.stop(); } return !payload->timer.isActive(); } static void partialUpdateCallback(const QImage &image, const QVariant &vPayload) { auto payload = vPayload.value(); QMetaObject::invokeMethod(payload->generator, "signalPartialPixmapRequest", Qt::QueuedConnection, Q_ARG(Okular::PixmapRequest*, payload->request), Q_ARG(QImage, image)); } #ifdef HAVE_POPPLER_0_63 static bool shouldAbortRenderCallback(const QVariant &vPayload) { auto payload = vPayload.value(); return payload->request->shouldAbortRender(); } #endif QImage PDFGenerator::image( Okular::PixmapRequest * request ) { // debug requests to this (xpdf) generator //qCDebug(OkularPdfDebug) << "id: " << request->id << " is requesting " << (request->async ? "ASYNC" : "sync") << " pixmap for page " << request->page->number() << " [" << request->width << " x " << request->height << "]."; // compute dpi used to get an image with desired width and height Okular::Page * page = request->page(); double pageWidth = page->width(), pageHeight = page->height(); if ( page->rotation() % 2 ) qSwap( pageWidth, pageHeight ); qreal fakeDpiX = request->width() / pageWidth * dpi().width(); qreal fakeDpiY = request->height() / pageHeight * dpi().height(); // generate links rects only the first time bool genObjectRects = !rectsGenerated.at( page->number() ); // 0. LOCK [waits for the thread end] userMutex()->lock(); if ( request->shouldAbortRender() ) { userMutex()->unlock(); return QImage(); } // 1. Set OutputDev parameters and Generate contents // note: thread safety is set on 'false' for the GUI (this) thread Poppler::Page *p = pdfdoc->page(page->number()); // 2. Take data from outputdev and attach it to the Page QImage img; if (p) { #ifdef HAVE_POPPLER_0_63 if ( request->isTile() ) { const QRect rect = request->normalizedRect().geometry( request->width(), request->height() ); if ( request->partialUpdatesWanted() ) { RenderImagePayload payload( this, request ); img = p->renderToImage( fakeDpiX, fakeDpiY, rect.x(), rect.y(), rect.width(), rect.height(), Poppler::Page::Rotate0, partialUpdateCallback, shouldDoPartialUpdateCallback, shouldAbortRenderCallback, QVariant::fromValue( &payload ) ); } else { RenderImagePayload payload( this, request ); img = p->renderToImage( fakeDpiX, fakeDpiY, rect.x(), rect.y(), rect.width(), rect.height(), Poppler::Page::Rotate0, nullptr, nullptr, shouldAbortRenderCallback, QVariant::fromValue( &payload ) ); } } else { if ( request->partialUpdatesWanted() ) { RenderImagePayload payload( this, request ); img = p->renderToImage( fakeDpiX, fakeDpiY, -1, -1, -1, -1, Poppler::Page::Rotate0, partialUpdateCallback, shouldDoPartialUpdateCallback, shouldAbortRenderCallback, QVariant::fromValue( &payload ) ); } else { RenderImagePayload payload( this, request ); img = p->renderToImage( fakeDpiX, fakeDpiY, -1, -1, -1, -1, Poppler::Page::Rotate0, nullptr, nullptr, shouldAbortRenderCallback, QVariant::fromValue( &payload ) ); } } #else if ( request->isTile() ) { const QRect rect = request->normalizedRect().geometry( request->width(), request->height() ); if ( request->partialUpdatesWanted() ) { RenderImagePayload payload( this, request ); img = p->renderToImage( fakeDpiX, fakeDpiY, rect.x(), rect.y(), rect.width(), rect.height(), Poppler::Page::Rotate0, partialUpdateCallback, shouldDoPartialUpdateCallback, QVariant::fromValue( &payload ) ); } else { img = p->renderToImage( fakeDpiX, fakeDpiY, rect.x(), rect.y(), rect.width(), rect.height(), Poppler::Page::Rotate0 ); } } else { if ( request->partialUpdatesWanted() ) { RenderImagePayload payload( this, request ); img = p->renderToImage( fakeDpiX, fakeDpiY, -1, -1, -1, -1, Poppler::Page::Rotate0, partialUpdateCallback, shouldDoPartialUpdateCallback, QVariant::fromValue( &payload ) ); } else { img = p->renderToImage( fakeDpiX, fakeDpiY, -1, -1, -1, -1, Poppler::Page::Rotate0 ); } } #endif } else { img = QImage( request->width(), request->height(), QImage::Format_Mono ); img.fill( Qt::white ); } if ( p && genObjectRects ) { // TODO previously we extracted Image type rects too, but that needed porting to poppler // and as we are not doing anything with Image type rects i did not port it, have a look at // dead gp_outputdev.cpp on image extraction page->setObjectRects( generateLinks(p->links()) ); rectsGenerated[ request->page()->number() ] = true; resolveMediaLinkReferences( page ); } // 3. UNLOCK [re-enables shared access] userMutex()->unlock(); delete p; return img; } template void resolveMediaLinks( Okular::Action *action, enum Okular::Annotation::SubType subType, QHash &annotationsHash ) { OkularLinkType *okularAction = static_cast( action ); const PopplerLinkType *popplerLink = action->nativeId().value(); QHashIterator it( annotationsHash ); while ( it.hasNext() ) { it.next(); if ( it.key()->subType() == subType ) { const PopplerAnnotationType *popplerAnnotation = static_cast( it.value() ); if ( popplerLink->isReferencedAnnotation( popplerAnnotation ) ) { okularAction->setAnnotation( static_cast( it.key() ) ); okularAction->setNativeId( QVariant() ); delete popplerLink; // delete the associated Poppler::LinkMovie object, it's not needed anymore break; } } } } void PDFGenerator::resolveMediaLinkReference( Okular::Action *action ) { if ( !action ) return; if ( (action->actionType() != Okular::Action::Movie) && (action->actionType() != Okular::Action::Rendition) ) return; resolveMediaLinks( action, Okular::Annotation::AMovie, annotationsOnOpenHash ); resolveMediaLinks( action, Okular::Annotation::AScreen, annotationsOnOpenHash ); } void PDFGenerator::resolveMediaLinkReferences( Okular::Page *page ) { resolveMediaLinkReference( const_cast( page->pageAction( Okular::Page::Opening ) ) ); resolveMediaLinkReference( const_cast( page->pageAction( Okular::Page::Closing ) ) ); const QLinkedList< Okular::Annotation* > annotations = page->annotations(); for ( Okular::Annotation *annotation : annotations ) { if ( annotation->subType() == Okular::Annotation::AScreen ) { Okular::ScreenAnnotation *screenAnnotation = static_cast( annotation ); resolveMediaLinkReference( screenAnnotation->additionalAction( Okular::Annotation::PageOpening ) ); resolveMediaLinkReference( screenAnnotation->additionalAction( Okular::Annotation::PageClosing ) ); } if ( annotation->subType() == Okular::Annotation::AWidget ) { Okular::WidgetAnnotation *widgetAnnotation = static_cast( annotation ); resolveMediaLinkReference( widgetAnnotation->additionalAction( Okular::Annotation::PageOpening ) ); resolveMediaLinkReference( widgetAnnotation->additionalAction( Okular::Annotation::PageClosing ) ); } } const QLinkedList< Okular::FormField * > fields = page->formFields(); for ( Okular::FormField *field : fields ) { resolveMediaLinkReference( field->activationAction() ); } } #ifdef HAVE_POPPLER_0_63 struct TextExtractionPayload { TextExtractionPayload(Okular::TextRequest *r) : request(r) { } Okular::TextRequest *request; }; Q_DECLARE_METATYPE(TextExtractionPayload*) static bool shouldAbortTextExtractionCallback(const QVariant &vPayload) { auto payload = vPayload.value(); return payload->request->shouldAbortExtraction(); } #endif Okular::TextPage* PDFGenerator::textPage( Okular::TextRequest *request ) { const Okular::Page *page = request->page(); #ifdef PDFGENERATOR_DEBUG qCDebug(OkularPdfDebug) << "page" << page->number(); #endif // build a TextList... QList textList; double pageWidth, pageHeight; userMutex()->lock(); Poppler::Page *pp = pdfdoc->page( page->number() ); if (pp) { #ifdef HAVE_POPPLER_0_63 TextExtractionPayload payload(request); textList = pp->textList( Poppler::Page::Rotate0, shouldAbortTextExtractionCallback, QVariant::fromValue( &payload ) ); #else textList = pp->textList(); #endif const QSizeF s = pp->pageSizeF(); pageWidth = s.width(); pageHeight = s.height(); } else { pageWidth = defaultPageWidth; pageHeight = defaultPageHeight; } delete pp; userMutex()->unlock(); if ( textList.isEmpty() && request->shouldAbortExtraction() ) return nullptr; Okular::TextPage *tp = abstractTextPage(textList, pageHeight, pageWidth, (Poppler::Page::Rotation)page->orientation()); qDeleteAll(textList); return tp; } void PDFGenerator::requestFontData(const Okular::FontInfo &font, QByteArray *data) { Poppler::FontInfo fi = font.nativeId().value(); *data = pdfdoc->fontData(fi); } #define DUMMY_QPRINTER_COPY bool PDFGenerator::print( QPrinter& printer ) { bool printAnnots = true; bool forceRasterize = false; PDFOptionsPage::ScaleMode scaleMode = PDFOptionsPage::FitToPrintableArea; if ( pdfOptionsPage ) { printAnnots = pdfOptionsPage->printAnnots(); forceRasterize = pdfOptionsPage->printForceRaster(); scaleMode = pdfOptionsPage->scaleMode(); } #ifdef Q_OS_WIN // Windows can only print by rasterization, because that is // currently the only way Okular implements printing without using UNIX-specific // tools like 'lpr'. forceRasterize = true; #endif if ( forceRasterize ) { pdfdoc->setRenderHint(Poppler::Document::HideAnnotations, !printAnnots); if ( pdfOptionsPage ) { // If requested, scale to full page instead of the printable area printer.setFullPage( pdfOptionsPage->ignorePrintMargins() ); } QPainter painter; painter.begin(&printer); QList pageList = Okular::FilePrinter::pageList( printer, pdfdoc->numPages(), document()->currentPage() + 1, document()->bookmarkedPageList() ); for ( int i = 0; i < pageList.count(); ++i ) { if ( i != 0 ) printer.newPage(); const int page = pageList.at( i ) - 1; userMutex()->lock(); std::unique_ptr pp( pdfdoc->page( page ) ); if (pp) { QSizeF pageSize = pp->pageSizeF(); // Unit is 'points' (i.e., 1/72th of an inch) QRect painterWindow = painter.window(); // Unit is 'QPrinter::DevicePixel' // Default: no scaling at all, but we need to go from DevicePixel units to 'points' // Warning: We compute the horizontal scaling, and later assume that the vertical scaling will be the same. double scaling = printer.paperRect(QPrinter::DevicePixel).width() / printer.paperRect(QPrinter::Point).width(); if ( scaleMode != PDFOptionsPage::None ) { // Get the two scaling factors needed to fit the page onto paper horizontally or vertically auto horizontalScaling = painterWindow.width() / pageSize.width(); auto verticalScaling = painterWindow.height() / pageSize.height(); // We use the smaller of the two for both directions, to keep the aspect ratio scaling = std::min(horizontalScaling, verticalScaling); } #ifdef Q_OS_WIN QImage img = pp->renderToImage( printer.physicalDpiX(), printer.physicalDpiY() ); #else // UNIX: Same resolution as the postscript rasterizer; see discussion at https://git.reviewboard.kde.org/r/130218/ QImage img = pp->renderToImage( 300, 300 ); #endif painter.drawImage( QRectF( QPointF( 0, 0 ), scaling * pp->pageSizeF() ), img ); } userMutex()->unlock(); } painter.end(); return true; } #ifdef DUMMY_QPRINTER_COPY // Get the real page size to pass to the ps generator QPrinter dummy( QPrinter::PrinterResolution ); dummy.setFullPage( true ); dummy.setOrientation( printer.orientation() ); dummy.setPageSize( printer.pageSize() ); dummy.setPaperSize( printer.paperSize( QPrinter::Millimeter ), QPrinter::Millimeter ); int width = dummy.width(); int height = dummy.height(); #else int width = printer.width(); int height = printer.height(); #endif if (width <= 0 || height <= 0) { lastPrintError = InvalidPageSizePrintError; return false; } // Create the tempfile to send to FilePrinter, which will manage the deletion QTemporaryFile tf(QDir::tempPath() + QLatin1String("/okular_XXXXXX.ps")); if ( !tf.open() ) { lastPrintError = TemporaryFileOpenPrintError; return false; } QString tempfilename = tf.fileName(); // Generate the list of pages to be printed as selected in the print dialog QList pageList = Okular::FilePrinter::pageList( printer, pdfdoc->numPages(), document()->currentPage() + 1, document()->bookmarkedPageList() ); // TODO rotation tf.setAutoRemove(false); QString pstitle = metaData(QStringLiteral("Title"), QVariant()).toString(); if ( pstitle.trimmed().isEmpty() ) { pstitle = document()->currentDocument().fileName(); } Poppler::PSConverter *psConverter = pdfdoc->psConverter(); psConverter->setOutputDevice(&tf); psConverter->setPageList(pageList); psConverter->setPaperWidth(width); psConverter->setPaperHeight(height); psConverter->setRightMargin(0); psConverter->setBottomMargin(0); psConverter->setLeftMargin(0); psConverter->setTopMargin(0); psConverter->setStrictMargins(false); psConverter->setForceRasterize(forceRasterize); psConverter->setTitle(pstitle); if (!printAnnots) psConverter->setPSOptions(psConverter->psOptions() | Poppler::PSConverter::HideAnnotations ); userMutex()->lock(); if (psConverter->convert()) { userMutex()->unlock(); delete psConverter; tf.close(); const Okular::FilePrinter::ScaleMode filePrinterScaleMode = (scaleMode == PDFOptionsPage::None) ? Okular::FilePrinter::ScaleMode::NoScaling : Okular::FilePrinter::ScaleMode::FitToPrintArea; int ret = Okular::FilePrinter::printFile( printer, tempfilename, document()->orientation(), Okular::FilePrinter::SystemDeletesFiles, Okular::FilePrinter::ApplicationSelectsPages, document()->bookmarkedPageRange(), filePrinterScaleMode ); lastPrintError = Okular::FilePrinter::printError( ret ); return (lastPrintError == NoPrintError); } else { lastPrintError = FileConversionPrintError; delete psConverter; userMutex()->unlock(); } tf.close(); return false; } QVariant PDFGenerator::metaData( const QString & key, const QVariant & option ) const { if ( key == QLatin1String("StartFullScreen") ) { QMutexLocker ml(userMutex()); // asking for the 'start in fullscreen mode' (pdf property) if ( pdfdoc->pageMode() == Poppler::Document::FullScreen ) return true; } else if ( key == QLatin1String("NamedViewport") && !option.toString().isEmpty() ) { Okular::DocumentViewport viewport; QString optionString = option.toString(); // asking for the page related to a 'named link destination'. the // option is the link name. @see addSynopsisChildren. userMutex()->lock(); Poppler::LinkDestination *ld = pdfdoc->linkDestination( optionString ); userMutex()->unlock(); if ( ld ) { fillViewportFromLinkDestination( viewport, *ld ); } delete ld; if ( viewport.pageNumber >= 0 ) return viewport.toString(); } else if ( key == QLatin1String("DocumentTitle") ) { userMutex()->lock(); QString title = pdfdoc->info( QStringLiteral("Title") ); userMutex()->unlock(); return title; } else if ( key == QLatin1String("OpenTOC") ) { QMutexLocker ml(userMutex()); if ( pdfdoc->pageMode() == Poppler::Document::UseOutlines ) return true; } else if ( key == QLatin1String("DocumentScripts") && option.toString() == QLatin1String("JavaScript") ) { QMutexLocker ml(userMutex()); return pdfdoc->scripts(); } else if ( key == QLatin1String("HasUnsupportedXfaForm") ) { QMutexLocker ml(userMutex()); return pdfdoc->formType() == Poppler::Document::XfaForm; } else if ( key == QLatin1String("FormCalculateOrder") ) { QMutexLocker ml(userMutex()); return QVariant::fromValue>(pdfdoc->formCalculateOrder()); } else if ( key == QLatin1String("GeneratorExtraDescription") ) { #ifdef HAVE_POPPLER_0_73 if (Poppler::Version::string() == POPPLER_VERSION) { return i18n("Using Poppler %1", Poppler::Version::string()); } else { return i18n("Using Poppler %1\n\nBuilt against Poppler %2", Poppler::Version::string(), POPPLER_VERSION); } #endif } else if ( key == QLatin1String("IsDigitallySigned") ) { const Okular::Document *doc = document(); uint numPages = doc->pages(); for ( uint i = 0; i < numPages; i++ ) { const QLinkedList formFields = doc->page( i )->formFields(); for ( const Okular::FormField *f : formFields ) { if ( f->type() == Okular::FormField::FormSignature ) return true; } } return false; } return QVariant(); } bool PDFGenerator::reparseConfig() { if ( !pdfdoc ) return false; bool somethingchanged = false; // load paper color QColor color = documentMetaData( PaperColorMetaData, true ).value< QColor >(); // if paper color is changed we have to rebuild every visible pixmap in addition // to the outputDevice. it's the 'heaviest' case, other effect are just recoloring // over the page rendered on 'standard' white background. if ( color != pdfdoc->paperColor() ) { userMutex()->lock(); pdfdoc->setPaperColor(color); userMutex()->unlock(); somethingchanged = true; } bool aaChanged = setDocumentRenderHints(); somethingchanged = somethingchanged || aaChanged; return somethingchanged; } void PDFGenerator::addPages( KConfigDialog *dlg ) { Ui_PDFSettingsWidget pdfsw; QWidget* w = new QWidget(dlg); pdfsw.setupUi(w); dlg->addPage(w, PDFSettings::self(), i18n("PDF"), QStringLiteral("application-pdf"), i18n("PDF Backend Configuration") ); } bool PDFGenerator::setDocumentRenderHints() { bool changed = false; const Poppler::Document::RenderHints oldhints = pdfdoc->renderHints(); #define SET_HINT(hintname, hintdefvalue, hintflag) \ { \ bool newhint = documentMetaData(hintname, hintdefvalue).toBool(); \ if (newhint != oldhints.testFlag(hintflag)) \ { \ pdfdoc->setRenderHint(hintflag, newhint); \ changed = true; \ } \ } SET_HINT(GraphicsAntialiasMetaData, true, Poppler::Document::Antialiasing) SET_HINT(TextAntialiasMetaData, true, Poppler::Document::TextAntialiasing) SET_HINT(TextHintingMetaData, false, Poppler::Document::TextHinting) #undef SET_HINT // load thin line mode const int thinLineMode = PDFSettings::enhanceThinLines(); const bool enableThinLineSolid = thinLineMode == PDFSettings::EnumEnhanceThinLines::Solid; const bool enableShapeLineSolid = thinLineMode == PDFSettings::EnumEnhanceThinLines::Shape; const bool thinLineSolidWasEnabled = (oldhints & Poppler::Document::ThinLineSolid) == Poppler::Document::ThinLineSolid; const bool thinLineShapeWasEnabled = (oldhints & Poppler::Document::ThinLineShape) == Poppler::Document::ThinLineShape; if (enableThinLineSolid != thinLineSolidWasEnabled) { pdfdoc->setRenderHint(Poppler::Document::ThinLineSolid, enableThinLineSolid); changed = true; } if (enableShapeLineSolid != thinLineShapeWasEnabled) { pdfdoc->setRenderHint(Poppler::Document::ThinLineShape, enableShapeLineSolid); changed = true; } return changed; } Okular::ExportFormat::List PDFGenerator::exportFormats() const { static Okular::ExportFormat::List formats; if ( formats.isEmpty() ) { formats.append( Okular::ExportFormat::standardFormat( Okular::ExportFormat::PlainText ) ); } return formats; } bool PDFGenerator::exportTo( const QString &fileName, const Okular::ExportFormat &format ) { if ( format.mimeType().inherits( QStringLiteral( "text/plain" ) ) ) { QFile f( fileName ); if ( !f.open( QIODevice::WriteOnly ) ) return false; QTextStream ts( &f ); int num = document()->pages(); for ( int i = 0; i < num; ++i ) { QString text; userMutex()->lock(); Poppler::Page *pp = pdfdoc->page(i); if (pp) { text = pp->text(QRect()).normalized(QString::NormalizationForm_KC); } userMutex()->unlock(); ts << text; delete pp; } f.close(); return true; } return false; } //END Generator inherited functions inline void append (Okular::TextPage* ktp, const QString &s, double l, double b, double r, double t) { // kWarning(PDFDebug).nospace() << "text: " << s << " at (" << l << "," << t << ")x(" << r <<","<append(s, new Okular::NormalizedRect(l, t, r, b)); } Okular::TextPage * PDFGenerator::abstractTextPage(const QList &text, double height, double width,int rot) { Q_UNUSED(rot); Okular::TextPage* ktp=new Okular::TextPage; Poppler::TextBox *next; #ifdef PDFGENERATOR_DEBUG qCDebug(OkularPdfDebug) << "getting text page in generator pdf - rotation:" << rot; #endif QString s; bool addChar; for (const Poppler::TextBox *word : text) { const int qstringCharCount = word->text().length(); next=word->nextWord(); int textBoxChar = 0; for (int j = 0; j < qstringCharCount; j++) { const QChar c = word->text().at(j); if (c.isHighSurrogate()) { s = c; addChar = false; } else if (c.isLowSurrogate()) { s += c; addChar = true; } else { s = c; addChar = true; } if (addChar) { QRectF charBBox = word->charBoundingBox(textBoxChar); append(ktp, (j==qstringCharCount-1 && !next) ? (s + QLatin1Char('\n')) : s, charBBox.left()/width, charBBox.bottom()/height, charBBox.right()/width, charBBox.top()/height); textBoxChar++; } } if ( word->hasSpaceAfter() && next ) { // TODO Check with a document with vertical text // probably won't work and we will need to do comparisons // between wordBBox and nextWordBBox to see if they are // vertically or horizontally aligned QRectF wordBBox = word->boundingBox(); QRectF nextWordBBox = next->boundingBox(); append(ktp, QStringLiteral(" "), wordBBox.right()/width, wordBBox.bottom()/height, nextWordBBox.left()/width, wordBBox.top()/height); } } return ktp; } void PDFGenerator::addSynopsisChildren( QDomNode * parent, QDomNode * parentDestination ) { // keep track of the current listViewItem QDomNode n = parent->firstChild(); while( !n.isNull() ) { // convert the node to an element (sure it is) QDomElement e = n.toElement(); // The name is the same QDomElement item = docSyn.createElement( e.tagName() ); parentDestination->appendChild(item); if (!e.attribute(QStringLiteral("ExternalFileName")).isNull()) item.setAttribute(QStringLiteral("ExternalFileName"), e.attribute(QStringLiteral("ExternalFileName"))); if (!e.attribute(QStringLiteral("DestinationName")).isNull()) item.setAttribute(QStringLiteral("ViewportName"), e.attribute(QStringLiteral("DestinationName"))); if (!e.attribute(QStringLiteral("Destination")).isNull()) { Okular::DocumentViewport vp; fillViewportFromLinkDestination( vp, Poppler::LinkDestination(e.attribute(QStringLiteral("Destination"))) ); item.setAttribute( QStringLiteral("Viewport"), vp.toString() ); } if (!e.attribute(QStringLiteral("Open")).isNull()) item.setAttribute(QStringLiteral("Open"), e.attribute(QStringLiteral("Open"))); if (!e.attribute(QStringLiteral("DestinationURI")).isNull()) item.setAttribute(QStringLiteral("URL"), e.attribute(QStringLiteral("DestinationURI"))); // descend recursively and advance to the next node if ( e.hasChildNodes() ) addSynopsisChildren( &n, & item ); n = n.nextSibling(); } } void PDFGenerator::addAnnotations( Poppler::Page * popplerPage, Okular::Page * page ) { QSet subtypes; subtypes << Poppler::Annotation::AFileAttachment << Poppler::Annotation::ASound << Poppler::Annotation::AMovie << Poppler::Annotation::AWidget << Poppler::Annotation::AScreen << Poppler::Annotation::AText << Poppler::Annotation::ALine << Poppler::Annotation::AGeom << Poppler::Annotation::AHighlight << Poppler::Annotation::AInk << Poppler::Annotation::AStamp << Poppler::Annotation::ACaret; const QList popplerAnnotations = popplerPage->annotations( subtypes ); for (Poppler::Annotation *a : popplerAnnotations) { bool doDelete = true; Okular::Annotation * newann = createAnnotationFromPopplerAnnotation( a, &doDelete ); if (newann) { page->addAnnotation(newann); if ( a->subType() == Poppler::Annotation::AScreen ) { Poppler::ScreenAnnotation *annotScreen = static_cast( a ); Okular::ScreenAnnotation *screenAnnotation = static_cast( newann ); // The activation action const Poppler::Link *actionLink = annotScreen->action(); if ( actionLink ) screenAnnotation->setAction( createLinkFromPopplerLink( actionLink ) ); // The additional actions const Poppler::Link *pageOpeningLink = annotScreen->additionalAction( Poppler::Annotation::PageOpeningAction ); if ( pageOpeningLink ) screenAnnotation->setAdditionalAction( Okular::Annotation::PageOpening, createLinkFromPopplerLink( pageOpeningLink ) ); const Poppler::Link *pageClosingLink = annotScreen->additionalAction( Poppler::Annotation::PageClosingAction ); if ( pageClosingLink ) screenAnnotation->setAdditionalAction( Okular::Annotation::PageClosing, createLinkFromPopplerLink( pageClosingLink ) ); } if ( a->subType() == Poppler::Annotation::AWidget ) { Poppler::WidgetAnnotation *annotWidget = static_cast( a ); Okular::WidgetAnnotation *widgetAnnotation = static_cast( newann ); // The additional actions const Poppler::Link *pageOpeningLink = annotWidget->additionalAction( Poppler::Annotation::PageOpeningAction ); if ( pageOpeningLink ) widgetAnnotation->setAdditionalAction( Okular::Annotation::PageOpening, createLinkFromPopplerLink( pageOpeningLink ) ); const Poppler::Link *pageClosingLink = annotWidget->additionalAction( Poppler::Annotation::PageClosingAction ); if ( pageClosingLink ) widgetAnnotation->setAdditionalAction( Okular::Annotation::PageClosing, createLinkFromPopplerLink( pageClosingLink ) ); } if ( !doDelete ) annotationsOnOpenHash.insert( newann, a ); } if ( doDelete ) delete a; } } void PDFGenerator::addTransition( Poppler::Page * pdfPage, Okular::Page * page ) // called on opening when MUTEX is not used { Poppler::PageTransition *pdfTransition = pdfPage->transition(); if ( !pdfTransition || pdfTransition->type() == Poppler::PageTransition::Replace ) return; Okular::PageTransition *transition = new Okular::PageTransition(); switch ( pdfTransition->type() ) { case Poppler::PageTransition::Replace: // won't get here, added to avoid warning break; case Poppler::PageTransition::Split: transition->setType( Okular::PageTransition::Split ); break; case Poppler::PageTransition::Blinds: transition->setType( Okular::PageTransition::Blinds ); break; case Poppler::PageTransition::Box: transition->setType( Okular::PageTransition::Box ); break; case Poppler::PageTransition::Wipe: transition->setType( Okular::PageTransition::Wipe ); break; case Poppler::PageTransition::Dissolve: transition->setType( Okular::PageTransition::Dissolve ); break; case Poppler::PageTransition::Glitter: transition->setType( Okular::PageTransition::Glitter ); break; case Poppler::PageTransition::Fly: transition->setType( Okular::PageTransition::Fly ); break; case Poppler::PageTransition::Push: transition->setType( Okular::PageTransition::Push ); break; case Poppler::PageTransition::Cover: transition->setType( Okular::PageTransition::Cover ); break; case Poppler::PageTransition::Uncover: transition->setType( Okular::PageTransition::Uncover ); break; case Poppler::PageTransition::Fade: transition->setType( Okular::PageTransition::Fade ); break; } transition->setDuration( pdfTransition->durationReal() ); switch ( pdfTransition->alignment() ) { case Poppler::PageTransition::Horizontal: transition->setAlignment( Okular::PageTransition::Horizontal ); break; case Poppler::PageTransition::Vertical: transition->setAlignment( Okular::PageTransition::Vertical ); break; } switch ( pdfTransition->direction() ) { case Poppler::PageTransition::Inward: transition->setDirection( Okular::PageTransition::Inward ); break; case Poppler::PageTransition::Outward: transition->setDirection( Okular::PageTransition::Outward ); break; } transition->setAngle( pdfTransition->angle() ); transition->setScale( pdfTransition->scale() ); transition->setIsRectangular( pdfTransition->isRectangular() ); page->setTransition( transition ); } void PDFGenerator::addFormFields( Poppler::Page * popplerPage, Okular::Page * page ) { const QList popplerFormFields = popplerPage->formFields(); QLinkedList okularFormFields; for ( Poppler::FormField *f : popplerFormFields ) { Okular::FormField * of = nullptr; switch ( f->type() ) { case Poppler::FormField::FormButton: of = new PopplerFormFieldButton( std::unique_ptr( static_cast( f ) ) ); break; case Poppler::FormField::FormText: of = new PopplerFormFieldText( std::unique_ptr( static_cast( f ) ) ); break; case Poppler::FormField::FormChoice: of = new PopplerFormFieldChoice( std::unique_ptr( static_cast( f ) ) ); break; case Poppler::FormField::FormSignature: { of = new PopplerFormFieldSignature( std::unique_ptr( static_cast( f ) ) ); break; } default: ; } if ( of ) // form field created, good - it will take care of the Poppler::FormField okularFormFields.append( of ); else // no form field available - delete the Poppler::FormField delete f; } if ( !okularFormFields.isEmpty() ) page->setFormFields( okularFormFields ); } PDFGenerator::PrintError PDFGenerator::printError() const { return lastPrintError; } Okular::PrintOptionsWidget* PDFGenerator::printConfigurationWidget() const { if ( !pdfOptionsPage ) { const_cast(this)->pdfOptionsPage = new PDFOptionsPage(); } return pdfOptionsPage; } bool PDFGenerator::supportsOption( SaveOption option ) const { switch ( option ) { case SaveChanges: { return true; } default: ; } return false; } bool PDFGenerator::save( const QString &fileName, SaveOptions options, QString *errorText ) { Q_UNUSED(errorText); Poppler::PDFConverter *pdfConv = pdfdoc->pdfConverter(); pdfConv->setOutputFileName( fileName ); if ( options & SaveChanges ) pdfConv->setPDFOptions( pdfConv->pdfOptions() | Poppler::PDFConverter::WithChanges ); QMutexLocker locker( userMutex() ); QHashIterator it( annotationsOnOpenHash ); while ( it.hasNext() ) { it.next(); if ( it.value()->uniqueName().isEmpty() ) { it.value()->setUniqueName( it.key()->uniqueName() ); } } bool success = pdfConv->convert(); if (!success) { switch (pdfConv->lastError()) { case Poppler::BaseConverter::NotSupportedInputFileError: // This can only happen with Poppler before 0.22 which did not have qt5 version break; case Poppler::BaseConverter::NoError: case Poppler::BaseConverter::FileLockedError: // we can't get here break; case Poppler::BaseConverter::OpenOutputError: // the default text message is good for this case break; } } delete pdfConv; return success; } Okular::AnnotationProxy* PDFGenerator::annotationProxy() const { return annotProxy; } #include "generator_pdf.moc" Q_LOGGING_CATEGORY(OkularPdfDebug, "org.kde.okular.generators.pdf", QtWarningMsg) /* kate: replace-tabs on; indent-width 4; */ diff --git a/generators/spectre/generator_ghostview.cpp b/generators/spectre/generator_ghostview.cpp index 866c738d9..30a2d31b4 100644 --- a/generators/spectre/generator_ghostview.cpp +++ b/generators/spectre/generator_ghostview.cpp @@ -1,308 +1,308 @@ /*************************************************************************** * Copyright (C) 2007-2008 by Albert Astals Cid * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * ***************************************************************************/ #include "generator_ghostview.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "ui_gssettingswidget.h" #include "gssettings.h" #include "spectre_debug.h" #include "rendererthread.h" OKULAR_EXPORT_PLUGIN(GSGenerator, "libokularGenerator_ghostview.json") GSGenerator::GSGenerator( QObject *parent, const QVariantList &args ) : Okular::Generator( parent, args ), - m_internalDocument(0), - m_request(0) + m_internalDocument(nullptr), + m_request(nullptr) { setFeature( PrintPostscript ); setFeature( PrintToFile ); GSRendererThread *renderer = GSRendererThread::getCreateRenderer(); if (!renderer->isRunning()) renderer->start(); connect(renderer, &GSRendererThread::imageDone, this, &GSGenerator::slotImageGenerated, Qt::QueuedConnection); } GSGenerator::~GSGenerator() { } bool GSGenerator::reparseConfig() { bool changed = false; if (m_internalDocument) { #define SET_HINT(hintname, hintdefvalue, hintvar) \ { \ bool newhint = documentMetaData(hintname, hintdefvalue).toBool(); \ if (newhint != cache_##hintvar) \ { \ cache_##hintvar = newhint; \ changed = true; \ } \ } SET_HINT(GraphicsAntialiasMetaData, true, AAgfx) SET_HINT(TextAntialiasMetaData, true, AAtext) #undef SET_HINT } return changed; } void GSGenerator::addPages( KConfigDialog *dlg ) { Ui_GSSettingsWidget gsw; QWidget* w = new QWidget(dlg); gsw.setupUi(w); dlg->addPage(w, GSSettings::self(), i18n("Ghostscript"), QStringLiteral("okular-gv"), i18n("Ghostscript Backend Configuration") ); } bool GSGenerator::print( QPrinter& printer ) { bool result = false; // Create tempfile to write to QTemporaryFile tf(QDir::tempPath() + QLatin1String("/okular_XXXXXX.ps")); // Get list of pages to print QList pageList = Okular::FilePrinter::pageList( printer, spectre_document_get_n_pages( m_internalDocument ), document()->currentPage() + 1, document()->bookmarkedPageList() ); // Default to Postscript export, but if printing to PDF use that instead SpectreExporterFormat exportFormat = SPECTRE_EXPORTER_FORMAT_PS; if ( printer.outputFileName().right(3) == QLatin1String("pdf") ) { exportFormat = SPECTRE_EXPORTER_FORMAT_PDF; tf.setFileTemplate(QDir::tempPath() + QLatin1String("/okular_XXXXXX.pdf")); } if ( !tf.open() ) return false; SpectreExporter *exporter = spectre_exporter_new( m_internalDocument, exportFormat ); SpectreStatus exportStatus = spectre_exporter_begin( exporter, tf.fileName().toLatin1().constData() ); int i = 0; while ( i < pageList.count() && exportStatus == SPECTRE_STATUS_SUCCESS ) { exportStatus = spectre_exporter_do_page( exporter, pageList.at( i ) - 1 ); i++; } SpectreStatus endStatus = SPECTRE_STATUS_EXPORTER_ERROR; if (exportStatus == SPECTRE_STATUS_SUCCESS) endStatus = spectre_exporter_end( exporter ); spectre_exporter_free( exporter ); const QString fileName = tf.fileName(); tf.close(); if ( exportStatus == SPECTRE_STATUS_SUCCESS && endStatus == SPECTRE_STATUS_SUCCESS ) { tf.setAutoRemove( false ); int ret = Okular::FilePrinter::printFile( printer, fileName, document()->orientation(), Okular::FilePrinter::SystemDeletesFiles, Okular::FilePrinter::ApplicationSelectsPages, document()->bookmarkedPageRange() ); if ( ret >= 0 ) result = true; } return result; } bool GSGenerator::loadDocument( const QString & fileName, QVector< Okular::Page * > & pagesVector ) { cache_AAtext = documentMetaData(TextAntialiasMetaData, true).toBool(); cache_AAgfx = documentMetaData(GraphicsAntialiasMetaData, true).toBool(); m_internalDocument = spectre_document_new(); spectre_document_load(m_internalDocument, QFile::encodeName(fileName).constData()); const SpectreStatus loadStatus = spectre_document_status(m_internalDocument); if (loadStatus != SPECTRE_STATUS_SUCCESS) { qCDebug(OkularSpectreDebug) << "ERR:" << spectre_status_to_string(loadStatus); spectre_document_free(m_internalDocument); - m_internalDocument = 0; + m_internalDocument = nullptr; return false; } pagesVector.resize( spectre_document_get_n_pages(m_internalDocument) ); qCDebug(OkularSpectreDebug) << "Page count:" << pagesVector.count(); return loadPages(pagesVector); } bool GSGenerator::doCloseDocument() { spectre_document_free(m_internalDocument); - m_internalDocument = 0; + m_internalDocument = nullptr; return true; } void GSGenerator::slotImageGenerated(QImage *img, Okular::PixmapRequest *request) { // This can happen as GSInterpreterCMD is a singleton and on creation signals all the slots // of all the generators attached to it if (request != m_request) return; if ( !request->page()->isBoundingBoxKnown() ) updatePageBoundingBox( request->page()->number(), Okular::Utils::imageBoundingBox( img ) ); - m_request = 0; + m_request = nullptr; QPixmap *pix = new QPixmap(QPixmap::fromImage(*img)); delete img; request->page()->setPixmap( request->observer(), pix ); signalPixmapRequestDone( request ); } bool GSGenerator::loadPages( QVector< Okular::Page * > & pagesVector ) { for (uint i = 0; i < spectre_document_get_n_pages(m_internalDocument); i++) { SpectrePage *page; int width = 0, height = 0; SpectreOrientation pageOrientation = SPECTRE_ORIENTATION_PORTRAIT; page = spectre_document_get_page (m_internalDocument, i); if (spectre_document_status (m_internalDocument)) { qCDebug(OkularSpectreDebug) << "Error getting page" << i << spectre_status_to_string(spectre_document_status(m_internalDocument)); } else { spectre_page_get_size(page, &width, &height); pageOrientation = spectre_page_get_orientation(page); } spectre_page_free(page); if (pageOrientation % 2 == 1) qSwap(width, height); pagesVector[i] = new Okular::Page(i, width, height, orientation(pageOrientation)); } return pagesVector.count() > 0; } void GSGenerator::generatePixmap( Okular::PixmapRequest * req ) { qCDebug(OkularSpectreDebug) << "receiving" << *req; SpectrePage *page = spectre_document_get_page(m_internalDocument, req->pageNumber()); GSRendererThread *renderer = GSRendererThread::getCreateRenderer(); GSRendererThreadRequest gsreq(this); gsreq.spectrePage = page; gsreq.platformFonts = GSSettings::platformFonts(); int graphicsAA = 1; int textAA = 1; if (cache_AAgfx) graphicsAA = 4; if (cache_AAtext) textAA = 4; gsreq.textAAbits = textAA; gsreq.graphicsAAbits = graphicsAA; gsreq.orientation = req->page()->orientation(); if (req->page()->rotation() == Okular::Rotation90 || req->page()->rotation() == Okular::Rotation270) { gsreq.magnify = qMax( (double)req->height() / req->page()->width(), (double)req->width() / req->page()->height() ); } else { gsreq.magnify = qMax( (double)req->width() / req->page()->width(), (double)req->height() / req->page()->height() ); } gsreq.request = req; m_request = req; renderer->addRequest(gsreq); } bool GSGenerator::canGeneratePixmap() const { return !m_request; } Okular::DocumentInfo GSGenerator::generateDocumentInfo( const QSet &keys ) const { Okular::DocumentInfo docInfo; if ( keys.contains( Okular::DocumentInfo::Title ) ) docInfo.set( Okular::DocumentInfo::Title, spectre_document_get_title(m_internalDocument) ); if ( keys.contains( Okular::DocumentInfo::Author ) ) docInfo.set( Okular::DocumentInfo::Author, spectre_document_get_for(m_internalDocument) ); if ( keys.contains( Okular::DocumentInfo::Creator ) ) docInfo.set( Okular::DocumentInfo::Creator, spectre_document_get_creator(m_internalDocument) ); if ( keys.contains( Okular::DocumentInfo::CreationDate ) ) docInfo.set( Okular::DocumentInfo::CreationDate, spectre_document_get_creation_date(m_internalDocument) ); if ( keys.contains( Okular::DocumentInfo::CustomKeys ) ) docInfo.set( QStringLiteral("dscversion"), spectre_document_get_format(m_internalDocument), i18n("Document version") ); if ( keys.contains( Okular::DocumentInfo::MimeType ) ) { int languageLevel = spectre_document_get_language_level(m_internalDocument); if (languageLevel > 0) docInfo.set( QStringLiteral("langlevel"), QString::number(languageLevel), i18n("Language Level") ); if (spectre_document_is_eps(m_internalDocument)) docInfo.set( Okular::DocumentInfo::MimeType, QStringLiteral("image/x-eps") ); else docInfo.set( Okular::DocumentInfo::MimeType, QStringLiteral("application/postscript") ); } if ( keys.contains( Okular::DocumentInfo::Pages ) ) docInfo.set( Okular::DocumentInfo::Pages, QString::number(spectre_document_get_n_pages(m_internalDocument)) ); return docInfo; } Okular::Rotation GSGenerator::orientation(SpectreOrientation pageOrientation) const { switch (pageOrientation) { case SPECTRE_ORIENTATION_PORTRAIT: return Okular::Rotation0; case SPECTRE_ORIENTATION_LANDSCAPE: return Okular::Rotation90; case SPECTRE_ORIENTATION_REVERSE_PORTRAIT: return Okular::Rotation180; case SPECTRE_ORIENTATION_REVERSE_LANDSCAPE: return Okular::Rotation270; } // get rid of warnings, should never happen return Okular::Rotation0; } QVariant GSGenerator::metaData(const QString &key, const QVariant &option) const { Q_UNUSED(option) if (key == QLatin1String("DocumentTitle")) { const char *title = spectre_document_get_title(m_internalDocument); if (title) return QString::fromLatin1(title); } return QVariant(); } #include "generator_ghostview.moc" diff --git a/generators/spectre/generator_ghostview.h b/generators/spectre/generator_ghostview.h index 003ae70e6..d3720b0f0 100644 --- a/generators/spectre/generator_ghostview.h +++ b/generators/spectre/generator_ghostview.h @@ -1,70 +1,70 @@ /*************************************************************************** * Copyright (C) 2007 by Albert Astals Cid * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * ***************************************************************************/ #ifndef _OKULAR_GENERATOR_GHOSTVIEW_H_ #define _OKULAR_GENERATOR_GHOSTVIEW_H_ #include #include #include class GSGenerator : public Okular::Generator, public Okular::ConfigInterface { Q_OBJECT Q_INTERFACES( Okular::Generator ) Q_INTERFACES( Okular::ConfigInterface ) public: /** virtual methods to reimplement **/ // load a document and fill up the pagesVector bool loadDocument( const QString & fileName, QVector< Okular::Page * > & pagesVector ) override; // Document description and Table of contents Okular::DocumentInfo generateDocumentInfo( const QSet &keys ) const override; - const Okular::DocumentSynopsis * generateDocumentSynopsis() override { return 0L; } - const Okular::DocumentFonts * generateDocumentFonts() { return 0L; } + const Okular::DocumentSynopsis * generateDocumentSynopsis() override { return nullptr; } + const Okular::DocumentFonts * generateDocumentFonts() { return nullptr; } // page contents generation bool canGeneratePixmap() const override; void generatePixmap( Okular::PixmapRequest * request ) override; QVariant metaData(const QString &key, const QVariant &option) const override; // print document using already configured kprinter bool print( QPrinter& /*printer*/ ) override; QString fileName() const; bool reparseConfig() override; void addPages( KConfigDialog* dlg ) override; /** constructor **/ GSGenerator( QObject *parent, const QVariantList &args ); ~GSGenerator() override; public Q_SLOTS: void slotImageGenerated(QImage *img, Okular::PixmapRequest *request); protected: bool doCloseDocument() override; private: bool loadPages( QVector< Okular::Page * > & pagesVector ); Okular::Rotation orientation(SpectreOrientation orientation) const; // backendish stuff SpectreDocument *m_internalDocument; Okular::PixmapRequest *m_request; bool cache_AAtext; bool cache_AAgfx; }; #endif diff --git a/generators/spectre/rendererthread.cpp b/generators/spectre/rendererthread.cpp index ff9cd613c..b10b2e9ac 100644 --- a/generators/spectre/rendererthread.cpp +++ b/generators/spectre/rendererthread.cpp @@ -1,136 +1,136 @@ /*************************************************************************** * Copyright (C) 2007 by Albert Astals Cid * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * ***************************************************************************/ #include "rendererthread.h" #include #include "spectre_debug.h" #include "core/generator.h" #include "core/page.h" #include "core/utils.h" -GSRendererThread *GSRendererThread::theRenderer = 0; +GSRendererThread *GSRendererThread::theRenderer = nullptr; GSRendererThread *GSRendererThread::getCreateRenderer() { if (!theRenderer) theRenderer = new GSRendererThread(); return theRenderer; } GSRendererThread::GSRendererThread() { m_renderContext = spectre_render_context_new(); } GSRendererThread::~GSRendererThread() { spectre_render_context_free(m_renderContext); } void GSRendererThread::addRequest(const GSRendererThreadRequest &req) { m_queueMutex.lock(); m_queue.enqueue(req); m_queueMutex.unlock(); m_semaphore.release(); } void GSRendererThread::run() { while(true) { m_semaphore.acquire(); { m_queueMutex.lock(); GSRendererThreadRequest req = m_queue.dequeue(); m_queueMutex.unlock(); spectre_render_context_set_scale(m_renderContext, req.magnify, req.magnify); spectre_render_context_set_use_platform_fonts(m_renderContext, req.platformFonts); spectre_render_context_set_antialias_bits(m_renderContext, req.graphicsAAbits, req.textAAbits); // Do not use spectre_render_context_set_rotation makes some files not render correctly, e.g. bug210499.ps // so we basically do the rendering without any rotation and then rotate to the orientation as needed // spectre_render_context_set_rotation(m_renderContext, req.orientation); - unsigned char *data = NULL; + unsigned char *data = nullptr; int row_length = 0; int wantedWidth = req.request->width(); int wantedHeight = req.request->height(); if ( req.orientation % 2 ) qSwap( wantedWidth, wantedHeight ); spectre_page_render(req.spectrePage, m_renderContext, &data, &row_length); // Qt needs the missing alpha of QImage::Format_RGB32 to be 0xff if (data && data[3] != 0xff) { for (int i = 3; i < row_length * wantedHeight; i += 4) data[i] = 0xff; } QImage img; if (row_length == wantedWidth * 4) { img = QImage(data, wantedWidth, wantedHeight, QImage::Format_RGB32); } else { // In case this ends up beign very slow we can try with some memmove QImage aux(data, row_length / 4, wantedHeight, QImage::Format_RGB32); img = QImage(aux.copy(0, 0, wantedWidth, wantedHeight)); } switch (req.orientation) { case Okular::Rotation90: { QTransform m; m.rotate(90); img = img.transformed( m ); break; } case Okular::Rotation180: { QTransform m; m.rotate(180); img = img.transformed( m ); break; } case Okular::Rotation270: { QTransform m; m.rotate(270); img = img.transformed( m ); } } QImage *image = new QImage(img.copy()); free(data); if (image->width() != req.request->width() || image->height() != req.request->height()) { qCWarning(OkularSpectreDebug).nospace() << "Generated image does not match wanted size: " << "[" << image->width() << "x" << image->height() << "] vs requested " << "[" << req.request->width() << "x" << req.request->height() << "]"; QImage aux = image->scaled(wantedWidth, wantedHeight); delete image; image = new QImage(aux); } emit imageDone(image, req.request); spectre_page_free(req.spectrePage); } } } /* kate: replace-tabs on; indent-width 4; */ diff --git a/generators/spectre/rendererthread.h b/generators/spectre/rendererthread.h index 22e4dda2c..4f362cdd5 100644 --- a/generators/spectre/rendererthread.h +++ b/generators/spectre/rendererthread.h @@ -1,80 +1,80 @@ /*************************************************************************** * Copyright (C) 2007 by Albert Astals Cid * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * ***************************************************************************/ #ifndef _OKULAR_GSRENDERERTHREAD_H_ #define _OKULAR_GSRENDERERTHREAD_H_ #include #include #include #include #include #include class QImage; class GSGenerator; namespace Okular { class PixmapRequest; } struct GSRendererThreadRequest { GSRendererThreadRequest(GSGenerator *_owner) : owner(_owner) - , request(0) - , spectrePage(0) + , request(nullptr) + , spectrePage(nullptr) , textAAbits(1) , graphicsAAbits(1) , magnify(1.0) , orientation(0) , platformFonts(true) {} GSGenerator *owner; Okular::PixmapRequest *request; SpectrePage *spectrePage; int textAAbits; int graphicsAAbits; double magnify; int orientation; bool platformFonts; }; Q_DECLARE_TYPEINFO(GSRendererThreadRequest, Q_MOVABLE_TYPE); class GSRendererThread : public QThread { Q_OBJECT public: static GSRendererThread *getCreateRenderer(); ~GSRendererThread() override; void addRequest(const GSRendererThreadRequest &req); Q_SIGNALS: void imageDone(QImage *image, Okular::PixmapRequest *request); private: GSRendererThread(); QSemaphore m_semaphore; static GSRendererThread *theRenderer; void run() override; SpectreRenderContext *m_renderContext; QQueue m_queue; QMutex m_queueMutex; }; #endif diff --git a/ui/pagepainter.cpp b/ui/pagepainter.cpp index 123689403..d0dee1f2e 100644 --- a/ui/pagepainter.cpp +++ b/ui/pagepainter.cpp @@ -1,1229 +1,1229 @@ /*************************************************************************** * 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 "pagepainter.h" // qt / kde includes #include #include #include #include #include #include #include #include #include #include // system includes #include // local includes #include "core/area.h" #include "core/page.h" #include "core/page_p.h" #include "core/annotations.h" #include "core/utils.h" #include "guiutils.h" #include "settings.h" #include "core/observer.h" #include "core/tile.h" #include "settings_core.h" #include "ui/debug_ui.h" Q_GLOBAL_STATIC_WITH_ARGS( QPixmap, busyPixmap, ( KIconLoader::global()->loadIcon(QLatin1String("okular"), KIconLoader::NoGroup, IconSize(KIconLoader::Desktop), KIconLoader::DefaultState, QStringList(), 0, true) ) ) #define TEXTANNOTATION_ICONSIZE 24 inline QPen buildPen( const Okular::Annotation *ann, double width, const QColor &color ) { QPen p( QBrush( color ), width, ann->style().lineStyle() == Okular::Annotation::Dashed ? Qt::DashLine : Qt::SolidLine, Qt::SquareCap, Qt::MiterJoin ); return p; } void PagePainter::paintPageOnPainter( QPainter * destPainter, const Okular::Page * page, Okular::DocumentObserver *observer, int flags, int scaledWidth, int scaledHeight, const QRect &limits ) { paintCroppedPageOnPainter( destPainter, page, observer, flags, scaledWidth, scaledHeight, limits, Okular::NormalizedRect( 0, 0, 1, 1 ), nullptr ); } void PagePainter::paintCroppedPageOnPainter( QPainter * destPainter, const Okular::Page * page, Okular::DocumentObserver *observer, int flags, int scaledWidth, int scaledHeight, const QRect &limits, const Okular::NormalizedRect &crop, Okular::NormalizedPoint *viewPortPoint ) { qreal dpr = destPainter->device()->devicePixelRatioF(); /* Calculate the cropped geometry of the page */ QRect scaledCrop = crop.geometry( scaledWidth, scaledHeight ); /* variables prefixed with d are in the device pixels coordinate system, which translates to the rendered output - that means, * multiplied with the device pixel ratio of the target PaintDevice */ const QRect dScaledCrop(QRectF(scaledCrop.x() * dpr, scaledCrop.y() * dpr, scaledCrop.width() * dpr, scaledCrop.height() * dpr).toAlignedRect()); int croppedWidth = scaledCrop.width(); int croppedHeight = scaledCrop.height(); int dScaledWidth = ceil(scaledWidth * dpr); int dScaledHeight = ceil(scaledHeight * dpr); const QRect dLimits(QRectF(limits.x() * dpr, limits.y() * dpr, limits.width() * dpr, limits.height() * dpr).toAlignedRect()); QColor paperColor = Qt::white; QColor backgroundColor = paperColor; if ( Okular::SettingsCore::changeColors() ) { switch ( Okular::SettingsCore::renderMode() ) { case Okular::SettingsCore::EnumRenderMode::Inverted: backgroundColor = Qt::black; break; case Okular::SettingsCore::EnumRenderMode::Paper: paperColor = Okular::SettingsCore::paperColor(); backgroundColor = paperColor; break; case Okular::SettingsCore::EnumRenderMode::Recolor: backgroundColor = Okular::Settings::recolorBackground(); break; default: ; } } destPainter->fillRect( limits, backgroundColor ); const bool hasTilesManager = page->hasTilesManager( observer ); QPixmap pixmap; if ( !hasTilesManager ) { /** 1 - RETRIEVE THE 'PAGE+ID' PIXMAP OR A SIMILAR 'PAGE' ONE **/ const QPixmap *p = page->_o_nearestPixmap( observer, dScaledWidth, dScaledHeight ); - if (p != NULL) { + if (p != nullptr) { pixmap = *p; pixmap.setDevicePixelRatio( qApp->devicePixelRatio() ); } /** 1B - IF NO PIXMAP, DRAW EMPTY PAGE **/ double pixmapRescaleRatio = !pixmap.isNull() ? dScaledWidth / (double)pixmap.width() : -1; long pixmapPixels = !pixmap.isNull() ? (long)pixmap.width() * (long)pixmap.height() : 0; if ( pixmap.isNull() || pixmapRescaleRatio > 20.0 || pixmapRescaleRatio < 0.25 || (dScaledWidth > pixmap.width() && pixmapPixels > 60000000L) ) { // draw something on the blank page: the okular icon or a cross (as a fallback) if ( !busyPixmap()->isNull() ) { busyPixmap->setDevicePixelRatio(dpr); destPainter->drawPixmap( QPoint( 10, 10 ), *busyPixmap() ); } else { destPainter->setPen( Qt::gray ); destPainter->drawLine( 0, 0, croppedWidth-1, croppedHeight-1 ); destPainter->drawLine( 0, croppedHeight-1, croppedWidth-1, 0 ); } return; } } /** 2 - FIND OUT WHAT TO PAINT (Flags + Configuration + Presence) **/ bool canDrawHighlights = (flags & Highlights) && !page->m_highlights.isEmpty(); bool canDrawTextSelection = (flags & TextSelection) && page->textSelection(); bool canDrawAnnotations = (flags & Annotations) && !page->m_annotations.isEmpty(); bool enhanceLinks = (flags & EnhanceLinks) && Okular::Settings::highlightLinks(); bool enhanceImages = (flags & EnhanceImages) && Okular::Settings::highlightImages(); // vectors containing objects to draw // make this a qcolor, rect map, since we don't need // to know s_id here! we are only drawing this right? QList< QPair > * bufferedHighlights = nullptr; QList< Okular::Annotation * > * bufferedAnnotations = nullptr; QList< Okular::Annotation * > * unbufferedAnnotations = nullptr; Okular::Annotation *boundingRectOnlyAnn = nullptr; // Paint the bounding rect of this annotation // fill up lists with visible annotation/highlight objects/text selections if ( canDrawHighlights || canDrawTextSelection || canDrawAnnotations ) { // precalc normalized 'limits rect' for intersection double nXMin = ( (double)limits.left() / scaledWidth ) + crop.left, nXMax = ( (double)limits.right() / scaledWidth ) + crop.left, nYMin = ( (double)limits.top() / scaledHeight ) + crop.top, nYMax = ( (double)limits.bottom() / scaledHeight ) + crop.top; // append all highlights inside limits to their list if ( canDrawHighlights ) { if ( !bufferedHighlights ) bufferedHighlights = new QList< QPair >(); /* else {*/ Okular::NormalizedRect* limitRect = new Okular::NormalizedRect(nXMin, nYMin, nXMax, nYMax ); QLinkedList< Okular::HighlightAreaRect * >::const_iterator h2It = page->m_highlights.constBegin(), hEnd = page->m_highlights.constEnd(); Okular::HighlightAreaRect::const_iterator hIt; for ( ; h2It != hEnd; ++h2It ) for (hIt=(*h2It)->constBegin(); hIt!=(*h2It)->constEnd(); ++hIt) { if ((*hIt).intersects(limitRect)) bufferedHighlights->append( qMakePair((*h2It)->color,*hIt) ); } delete limitRect; //} } if ( canDrawTextSelection ) { if ( !bufferedHighlights ) bufferedHighlights = new QList< QPair >(); /* else {*/ Okular::NormalizedRect* limitRect = new Okular::NormalizedRect(nXMin, nYMin, nXMax, nYMax ); const Okular::RegularAreaRect *textSelection = page->textSelection(); Okular::HighlightAreaRect::const_iterator hIt = textSelection->constBegin(), hEnd = textSelection->constEnd(); for ( ; hIt != hEnd; ++hIt ) { if ( (*hIt).intersects( limitRect ) ) bufferedHighlights->append( qMakePair( page->textSelectionColor(), *hIt ) ); } delete limitRect; //} } // append annotations inside limits to the un/buffered list if ( canDrawAnnotations ) { QLinkedList< Okular::Annotation * >::const_iterator aIt = page->m_annotations.constBegin(), aEnd = page->m_annotations.constEnd(); for ( ; aIt != aEnd; ++aIt ) { Okular::Annotation * ann = *aIt; int flags = ann->flags(); if ( flags & Okular::Annotation::Hidden ) continue; if ( flags & Okular::Annotation::ExternallyDrawn ) { // ExternallyDrawn annots are never rendered by PagePainter. // Just paint the boundingRect if the annot is moved or resized. if ( flags & (Okular::Annotation::BeingMoved | Okular::Annotation::BeingResized) ) { boundingRectOnlyAnn = ann; } continue; } bool intersects = ann->transformedBoundingRectangle().intersects( nXMin, nYMin, nXMax, nYMax ); if ( ann->subType() == Okular::Annotation::AText ) { Okular::TextAnnotation * ta = static_cast< Okular::TextAnnotation * >( ann ); if ( ta->textType() == Okular::TextAnnotation::Linked ) { Okular::NormalizedRect iconrect( ann->transformedBoundingRectangle().left, ann->transformedBoundingRectangle().top, ann->transformedBoundingRectangle().left + TEXTANNOTATION_ICONSIZE / page->width(), ann->transformedBoundingRectangle().top + TEXTANNOTATION_ICONSIZE / page->height() ); intersects = iconrect.intersects( nXMin, nYMin, nXMax, nYMax ); } } if ( intersects ) { Okular::Annotation::SubType type = ann->subType(); if ( type == Okular::Annotation::ALine || type == Okular::Annotation::AHighlight || type == Okular::Annotation::AInk /*|| (type == Annotation::AGeom && ann->style().opacity() < 0.99)*/ ) { if ( !bufferedAnnotations ) bufferedAnnotations = new QList< Okular::Annotation * >(); bufferedAnnotations->append( ann ); } else { if ( !unbufferedAnnotations ) unbufferedAnnotations = new QList< Okular::Annotation * >(); unbufferedAnnotations->append( ann ); } } } } // end of intersections checking } /** 3 - ENABLE BACKBUFFERING IF DIRECT IMAGE MANIPULATION IS NEEDED **/ bool bufferAccessibility = (flags & Accessibility) && Okular::SettingsCore::changeColors() && (Okular::SettingsCore::renderMode() != Okular::SettingsCore::EnumRenderMode::Paper); bool useBackBuffer = bufferAccessibility || bufferedHighlights || bufferedAnnotations || viewPortPoint; QPixmap * backPixmap = nullptr; QPainter * mixedPainter = nullptr; QRect limitsInPixmap = limits.translated( scaledCrop.topLeft() ); QRect dLimitsInPixmap = dLimits.translated( dScaledCrop.topLeft() ); // limits within full (scaled but uncropped) pixmap /** 4A -- REGULAR FLOW. PAINT PIXMAP NORMAL OR RESCALED USING GIVEN QPAINTER **/ if ( !useBackBuffer ) { if ( hasTilesManager ) { const Okular::NormalizedRect normalizedLimits( limitsInPixmap, scaledWidth, scaledHeight ); const QList tiles = page->tilesAt( observer, normalizedLimits ); QList::const_iterator tIt = tiles.constBegin(), tEnd = tiles.constEnd(); while ( tIt != tEnd ) { const Okular::Tile &tile = *tIt; QRect tileRect = tile.rect().geometry( scaledWidth, scaledHeight ).translated( -scaledCrop.topLeft() ); QRect dTileRect = QRectF(tileRect.x() * dpr, tileRect.y() * dpr, tileRect.width() * dpr, tileRect.height() * dpr).toAlignedRect(); QRect limitsInTile = limits & tileRect; QRectF dLimitsInTile = dLimits & dTileRect; if ( !limitsInTile.isEmpty() ) { QPixmap* tilePixmap = tile.pixmap(); tilePixmap->setDevicePixelRatio( qApp->devicePixelRatio() ); if ( tilePixmap->width() == dTileRect.width() && tilePixmap->height() == dTileRect.height() ) { destPainter->drawPixmap( limitsInTile.topLeft(), *tilePixmap, dLimitsInTile.translated( -dTileRect.topLeft() ) ); } else { destPainter->drawPixmap( tileRect, *tilePixmap ); } } tIt++; } } else { QPixmap scaledCroppedPixmap = pixmap.scaled(dScaledWidth, dScaledHeight).copy(dLimitsInPixmap); scaledCroppedPixmap.setDevicePixelRatio(dpr); destPainter->drawPixmap( limits.topLeft(), scaledCroppedPixmap, QRectF(0, 0, dLimits.width(),dLimits.height())); } // 4A.2. active painter is the one passed to this method mixedPainter = destPainter; } /** 4B -- BUFFERED FLOW. IMAGE PAINTING + OPERATIONS. QPAINTER OVER PIXMAP **/ else { // the image over which we are going to draw QImage backImage = QImage( dLimits.width(), dLimits.height(), QImage::Format_ARGB32_Premultiplied ); backImage.setDevicePixelRatio(dpr); backImage.fill( paperColor ); QPainter p( &backImage ); if ( hasTilesManager ) { const Okular::NormalizedRect normalizedLimits( limitsInPixmap, scaledWidth, scaledHeight ); const QList tiles = page->tilesAt( observer, normalizedLimits ); QList::const_iterator tIt = tiles.constBegin(), tEnd = tiles.constEnd(); while ( tIt != tEnd ) { const Okular::Tile &tile = *tIt; QRect tileRect = tile.rect().geometry( scaledWidth, scaledHeight ).translated( -scaledCrop.topLeft() ); QRect dTileRect(QRectF(tileRect.x() * dpr, tileRect.y() * dpr, tileRect.width() * dpr, tileRect.height() * dpr).toAlignedRect()); QRect limitsInTile = limits & tileRect; QRect dLimitsInTile = dLimits & dTileRect; if ( !limitsInTile.isEmpty() ) { QPixmap* tilePixmap = tile.pixmap(); tilePixmap->setDevicePixelRatio( qApp->devicePixelRatio() ); if ( tilePixmap->width() == dTileRect.width() && tilePixmap->height() == dTileRect.height() ) { p.drawPixmap( limitsInTile.translated( -limits.topLeft() ).topLeft(), *tilePixmap, dLimitsInTile.translated( -dTileRect.topLeft() ) ); } else { double xScale = tilePixmap->width() / (double)dTileRect.width(); double yScale = tilePixmap->height() / (double)dTileRect.height(); QTransform transform( xScale, 0, 0, yScale, 0, 0 ); p.drawPixmap( limitsInTile.translated( -limits.topLeft() ), *tilePixmap, transform.mapRect( dLimitsInTile ).translated( -transform.mapRect( dTileRect ).topLeft() ) ); } } ++tIt; } } else { // 4B.1. draw the page pixmap: normal or scaled QPixmap scaledCroppedPixmap = pixmap.scaled(dScaledWidth, dScaledHeight).copy(dLimitsInPixmap); scaledCroppedPixmap.setDevicePixelRatio(dpr); p.drawPixmap( 0, 0, scaledCroppedPixmap ); } p.end(); // 4B.2. modify pixmap following accessibility settings if ( bufferAccessibility ) { switch ( Okular::SettingsCore::renderMode() ) { case Okular::SettingsCore::EnumRenderMode::Inverted: // Invert image pixels using QImage internal function backImage.invertPixels(QImage::InvertRgb); break; case Okular::SettingsCore::EnumRenderMode::Recolor: recolor(&backImage, Okular::Settings::recolorForeground(), Okular::Settings::recolorBackground()); break; case Okular::SettingsCore::EnumRenderMode::BlackWhite: // Manual Gray and Contrast unsigned int * data = (unsigned int *)backImage.bits(); int val, pixels = backImage.width() * backImage.height(), con = Okular::Settings::bWContrast(), thr = 255 - Okular::Settings::bWThreshold(); for( int i = 0; i < pixels; ++i ) { val = qGray( data[i] ); if ( val > thr ) val = 128 + (127 * (val - thr)) / (255 - thr); else if ( val < thr ) val = (128 * val) / thr; if ( con > 2 ) { val = con * ( val - thr ) / 2 + thr; if ( val > 255 ) val = 255; else if ( val < 0 ) val = 0; } data[i] = qRgba( val, val, val, 255 ); } break; } } // 4B.3. highlight rects in page if ( bufferedHighlights ) { // draw highlights that are inside the 'limits' paint region for (const auto& highlight : *bufferedHighlights) { const Okular::NormalizedRect & r = highlight.second; // find out the rect to highlight on pixmap QRect highlightRect = r.geometry( scaledWidth, scaledHeight ).translated( -scaledCrop.topLeft() ).intersected( limits ); highlightRect.translate( -limits.left(), -limits.top() ); const QColor highlightColor = highlight.first; QPainter painter(&backImage); painter.setCompositionMode(QPainter::CompositionMode_Multiply); painter.fillRect(highlightRect, highlightColor); auto frameColor = highlightColor.darker(150); const QRect frameRect = r.geometry( scaledWidth, scaledHeight ).translated( -scaledCrop.topLeft() ).translated( -limits.left(), -limits.top() ); painter.setPen(frameColor); painter.drawRect(frameRect); } } // 4B.4. paint annotations [COMPOSITED ONES] if ( bufferedAnnotations ) { // Albert: This is quite "heavy" but all the backImage that reach here are QImage::Format_ARGB32_Premultiplied // and have to be so that the QPainter::CompositionMode_Multiply works // we could also put a // backImage = backImage.convertToFormat(QImage::Format_ARGB32_Premultiplied) // that would be almost a noop, but we'll leave the assert for now Q_ASSERT(backImage.format() == QImage::Format_ARGB32_Premultiplied); // precalc constants for normalizing [0,1] page coordinates into normalized [0,1] limit rect coordinates double pageScale = (double)croppedWidth / page->width(); double xOffset = (double)limits.left() / (double)scaledWidth + crop.left, xScale = (double)scaledWidth / (double)limits.width(), yOffset = (double)limits.top() / (double)scaledHeight + crop.top, yScale = (double)scaledHeight / (double)limits.height(); // paint all buffered annotations in the page QList< Okular::Annotation * >::const_iterator aIt = bufferedAnnotations->constBegin(), aEnd = bufferedAnnotations->constEnd(); for ( ; aIt != aEnd; ++aIt ) { Okular::Annotation * a = *aIt; Okular::Annotation::SubType type = a->subType(); QColor acolor = a->style().color(); if ( !acolor.isValid() ) acolor = Qt::yellow; acolor.setAlphaF( a->style().opacity() ); // draw LineAnnotation MISSING: caption, dash pattern, endings for multipoint lines if ( type == Okular::Annotation::ALine ) { LineAnnotPainter linepainter { (Okular::LineAnnotation *) a, { page->width(), page->height() }, pageScale, { xScale, 0., 0., yScale, -xOffset * xScale, -yOffset * yScale } }; linepainter.draw( backImage ); } // draw HighlightAnnotation MISSING: under/strike width, feather, capping else if ( type == Okular::Annotation::AHighlight ) { // get the annotation Okular::HighlightAnnotation * ha = (Okular::HighlightAnnotation *) a; Okular::HighlightAnnotation::HighlightType type = ha->highlightType(); // draw each quad of the annotation int quads = ha->highlightQuads().size(); for ( int q = 0; q < quads; q++ ) { NormalizedPath path; const Okular::HighlightAnnotation::Quad & quad = ha->highlightQuads()[ q ]; // normalize page point to image for ( int i = 0; i < 4; i++ ) { Okular::NormalizedPoint point; point.x = (quad.transformedPoint( i ).x - xOffset) * xScale; point.y = (quad.transformedPoint( i ).y - yOffset) * yScale; path.append( point ); } // draw the normalized path into image switch ( type ) { // highlight the whole rect case Okular::HighlightAnnotation::Highlight: drawShapeOnImage( backImage, path, true, Qt::NoPen, acolor, pageScale, Multiply ); break; // highlight the bottom part of the rect case Okular::HighlightAnnotation::Squiggly: path[ 3 ].x = ( path[ 0 ].x + path[ 3 ].x ) / 2.0; path[ 3 ].y = ( path[ 0 ].y + path[ 3 ].y ) / 2.0; path[ 2 ].x = ( path[ 1 ].x + path[ 2 ].x ) / 2.0; path[ 2 ].y = ( path[ 1 ].y + path[ 2 ].y ) / 2.0; drawShapeOnImage( backImage, path, true, Qt::NoPen, acolor, pageScale, Multiply ); break; // make a line at 3/4 of the height case Okular::HighlightAnnotation::Underline: path[ 0 ].x = ( 3 * path[ 0 ].x + path[ 3 ].x ) / 4.0; path[ 0 ].y = ( 3 * path[ 0 ].y + path[ 3 ].y ) / 4.0; path[ 1 ].x = ( 3 * path[ 1 ].x + path[ 2 ].x ) / 4.0; path[ 1 ].y = ( 3 * path[ 1 ].y + path[ 2 ].y ) / 4.0; path.pop_back(); path.pop_back(); drawShapeOnImage( backImage, path, false, QPen( acolor, 2 ), QBrush(), pageScale ); break; // make a line at 1/2 of the height case Okular::HighlightAnnotation::StrikeOut: path[ 0 ].x = ( path[ 0 ].x + path[ 3 ].x ) / 2.0; path[ 0 ].y = ( path[ 0 ].y + path[ 3 ].y ) / 2.0; path[ 1 ].x = ( path[ 1 ].x + path[ 2 ].x ) / 2.0; path[ 1 ].y = ( path[ 1 ].y + path[ 2 ].y ) / 2.0; path.pop_back(); path.pop_back(); drawShapeOnImage( backImage, path, false, QPen( acolor, 2 ), QBrush(), pageScale ); break; } } } // draw InkAnnotation MISSING:invar width, PENTRACER else if ( type == Okular::Annotation::AInk ) { // get the annotation Okular::InkAnnotation * ia = (Okular::InkAnnotation *) a; // draw each ink path const QList< QLinkedList > transformedInkPaths = ia->transformedInkPaths(); const QPen inkPen = buildPen( a, a->style().width(), acolor ); int paths = transformedInkPaths.size(); for ( int p = 0; p < paths; p++ ) { NormalizedPath path; const QLinkedList & inkPath = transformedInkPaths[ p ]; // normalize page point to image QLinkedList::const_iterator pIt = inkPath.constBegin(), pEnd = inkPath.constEnd(); for ( ; pIt != pEnd; ++pIt ) { const Okular::NormalizedPoint & inkPoint = *pIt; Okular::NormalizedPoint point; point.x = (inkPoint.x - xOffset) * xScale; point.y = (inkPoint.y - yOffset) * yScale; path.append( point ); } // draw the normalized path into image drawShapeOnImage( backImage, path, false, inkPen, QBrush(), pageScale ); } } } // end current annotation drawing } if(viewPortPoint) { QPainter painter(&backImage); painter.translate( -limits.left(), -limits.top() ); painter.setPen( QApplication::palette().color( QPalette::Active, QPalette::Highlight ) ); painter.drawLine( 0, viewPortPoint->y * scaledHeight + 1, scaledWidth - 1, viewPortPoint->y * scaledHeight + 1 ); // ROTATION CURRENTLY NOT IMPLEMENTED /* if( page->rotation() == Okular::Rotation0) { } else if(page->rotation() == Okular::Rotation270) { painter.drawLine( viewPortPoint->y * scaledHeight + 1, 0, viewPortPoint->y * scaledHeight + 1, scaledWidth - 1); } else if(page->rotation() == Okular::Rotation180) { painter.drawLine( 0, (1.0 - viewPortPoint->y) * scaledHeight - 1, scaledWidth - 1, (1.0 - viewPortPoint->y) * scaledHeight - 1 ); } else if(page->rotation() == Okular::Rotation90) // not right, rotation clock-wise { painter.drawLine( scaledWidth - (viewPortPoint->y * scaledHeight + 1), 0, scaledWidth - (viewPortPoint->y * scaledHeight + 1), scaledWidth - 1); } */ } // 4B.5. create the back pixmap converting from the local image backPixmap = new QPixmap( QPixmap::fromImage( backImage ) ); backPixmap->setDevicePixelRatio(dpr); // 4B.6. create a painter over the pixmap and set it as the active one mixedPainter = new QPainter( backPixmap ); mixedPainter->translate( -limits.left(), -limits.top() ); } /** 5 -- MIXED FLOW. Draw ANNOTATIONS [OPAQUE ONES] on ACTIVE PAINTER **/ if ( unbufferedAnnotations ) { // iterate over annotations and paint AText, AGeom, AStamp QList< Okular::Annotation * >::const_iterator aIt = unbufferedAnnotations->constBegin(), aEnd = unbufferedAnnotations->constEnd(); for ( ; aIt != aEnd; ++aIt ) { Okular::Annotation * a = *aIt; // honor opacity settings on supported types unsigned int opacity = (unsigned int)( a->style().color().alpha() * a->style().opacity() ); // skip the annotation drawing if all the annotation is fully // transparent, but not with text annotations if ( opacity <= 0 && a->subType() != Okular::Annotation::AText ) continue; QColor acolor = a->style().color(); if ( !acolor.isValid() ) acolor = Qt::yellow; acolor.setAlpha( opacity ); // get annotation boundary and drawn rect QRect annotBoundary = a->transformedBoundingRectangle().geometry( scaledWidth, scaledHeight ).translated( -scaledCrop.topLeft() ); QRect annotRect = annotBoundary.intersected( limits ); QRect innerRect( annotRect.left() - annotBoundary.left(), annotRect.top() - annotBoundary.top(), annotRect.width(), annotRect.height() ); QRectF dInnerRect(innerRect.x() * dpr, innerRect.y() * dpr, innerRect.width() * dpr, innerRect.height() * dpr); Okular::Annotation::SubType type = a->subType(); // draw TextAnnotation if ( type == Okular::Annotation::AText ) { Okular::TextAnnotation * text = (Okular::TextAnnotation *)a; if ( text->textType() == Okular::TextAnnotation::InPlace ) { QImage image( annotBoundary.size(), QImage::Format_ARGB32 ); image.fill( acolor.rgba() ); QPainter painter( &image ); painter.setFont( text->textFont() ); painter.setPen( text->textColor() ); Qt::AlignmentFlag halign = ( text->inplaceAlignment() == 1 ? Qt::AlignHCenter : ( text->inplaceAlignment() == 2 ? Qt::AlignRight : Qt::AlignLeft ) ); const double invXScale = (double)page->width() / scaledWidth; const double invYScale = (double)page->height() / scaledHeight; const double borderWidth = text->style().width(); painter.scale( 1 / invXScale, 1 / invYScale ); painter.drawText( borderWidth * invXScale, borderWidth * invYScale, (image.width() - 2 * borderWidth) * invXScale, (image.height() - 2 * borderWidth) * invYScale, Qt::AlignTop | halign | Qt::TextWordWrap, text->contents() ); painter.resetTransform(); //Required as asking for a zero width pen results //in a default width pen (1.0) being created if ( borderWidth != 0 ) { QPen pen( Qt::black, borderWidth ); painter.setPen( pen ); painter.drawRect( 0, 0, image.width() - 1, image.height() - 1 ); } painter.end(); mixedPainter->drawImage( annotBoundary.topLeft(), image ); } else if ( text->textType() == Okular::TextAnnotation::Linked ) { // get pixmap, colorize and alpha-blend it QString path; QPixmap pixmap = GuiUtils::iconLoader()->loadIcon( text->textIcon().toLower(), KIconLoader::User, 32, KIconLoader::DefaultState, QStringList(), &path, true ); if ( path.isEmpty() ) pixmap = GuiUtils::iconLoader()->loadIcon( text->textIcon().toLower(), KIconLoader::NoGroup, 32 ); QRect annotBoundary2 = QRect( annotBoundary.topLeft(), QSize( TEXTANNOTATION_ICONSIZE * dpr, TEXTANNOTATION_ICONSIZE * dpr ) ); QRect annotRect2 = annotBoundary2.intersected( limits ); QRect innerRect2( annotRect2.left() - annotBoundary2.left(), annotRect2.top() - annotBoundary2.top(), annotRect2.width(), annotRect2.height() ); QPixmap scaledCroppedPixmap = pixmap.scaled(TEXTANNOTATION_ICONSIZE * dpr, TEXTANNOTATION_ICONSIZE * dpr).copy(dInnerRect.toAlignedRect()); scaledCroppedPixmap.setDevicePixelRatio(dpr); QImage scaledCroppedImage = scaledCroppedPixmap.toImage(); // if the annotation color is valid (ie it was set), then // use it to colorize the icon, otherwise the icon will be // "gray" if ( a->style().color().isValid() ) GuiUtils::colorizeImage( scaledCroppedImage, a->style().color(), opacity ); pixmap = QPixmap::fromImage( scaledCroppedImage ); // draw the mangled image to painter mixedPainter->drawPixmap( annotRect.topLeft(), pixmap); } } // draw StampAnnotation else if ( type == Okular::Annotation::AStamp ) { Okular::StampAnnotation * stamp = (Okular::StampAnnotation *)a; // get pixmap and alpha blend it if needed QPixmap pixmap = GuiUtils::loadStamp( stamp->stampIconName(), annotBoundary.width() ); if ( !pixmap.isNull() ) // should never happen but can happen on huge sizes { const QRect dInnerRect(QRectF(innerRect.x() * dpr, innerRect.y() * dpr, innerRect.width() * dpr, innerRect.height() * dpr).toAlignedRect()); QPixmap scaledCroppedPixmap = pixmap.scaled(annotBoundary.width() * dpr, annotBoundary.height() * dpr).copy(dInnerRect); scaledCroppedPixmap.setDevicePixelRatio(dpr); QImage scaledCroppedImage = scaledCroppedPixmap.toImage(); if ( opacity < 255 ) changeImageAlpha( scaledCroppedImage, opacity ); pixmap = QPixmap::fromImage( scaledCroppedImage ); // draw the scaled and al mixedPainter->drawPixmap( annotRect.topLeft(), pixmap ); } } // draw GeomAnnotation else if ( type == Okular::Annotation::AGeom ) { Okular::GeomAnnotation * geom = (Okular::GeomAnnotation *)a; // check whether there's anything to draw if ( geom->style().width() || geom->geometricalInnerColor().isValid() ) { mixedPainter->save(); const double width = geom->style().width() * Okular::Utils::realDpi(nullptr).width() / ( 72.0 * 2.0 ) * scaledWidth / page->width(); QRectF r( .0, .0, annotBoundary.width(), annotBoundary.height() ); r.adjust( width, width, -width, -width ); r.translate( annotBoundary.topLeft() ); if ( geom->geometricalInnerColor().isValid() ) { r.adjust( width, width, -width, -width ); const QColor color = geom->geometricalInnerColor(); mixedPainter->setPen( Qt::NoPen ); mixedPainter->setBrush( QColor( color.red(), color.green(), color.blue(), opacity ) ); if ( geom->geometricalType() == Okular::GeomAnnotation::InscribedSquare ) mixedPainter->drawRect( r ); else mixedPainter->drawEllipse( r ); r.adjust( -width, -width, width, width ); } if ( geom->style().width() ) // need to check the original size here.. { mixedPainter->setPen( buildPen( a, width * 2, acolor ) ); mixedPainter->setBrush( Qt::NoBrush ); if ( geom->geometricalType() == Okular::GeomAnnotation::InscribedSquare ) mixedPainter->drawRect( r ); else mixedPainter->drawEllipse( r ); } mixedPainter->restore(); } } // draw extents rectangle if ( Okular::Settings::debugDrawAnnotationRect() ) { mixedPainter->setPen( a->style().color() ); mixedPainter->drawRect( annotBoundary ); } } } if ( boundingRectOnlyAnn ) { QRect annotBoundary = boundingRectOnlyAnn->transformedBoundingRectangle().geometry( scaledWidth, scaledHeight ).translated( -scaledCrop.topLeft() ); mixedPainter->setPen( Qt::DashLine ); mixedPainter->drawRect( annotBoundary ); } /** 6 -- MIXED FLOW. Draw LINKS+IMAGES BORDER on ACTIVE PAINTER **/ if ( enhanceLinks || enhanceImages ) { mixedPainter->save(); mixedPainter->scale( scaledWidth, scaledHeight ); mixedPainter->translate( -crop.left, -crop.top ); QColor normalColor = QApplication::palette().color( QPalette::Active, QPalette::Highlight ); // enlarging limits for intersection is like growing the 'rectGeometry' below QRect limitsEnlarged = limits; limitsEnlarged.adjust( -2, -2, 2, 2 ); // draw rects that are inside the 'limits' paint region as opaque rects QLinkedList< Okular::ObjectRect * >::const_iterator lIt = page->m_rects.constBegin(), lEnd = page->m_rects.constEnd(); for ( ; lIt != lEnd; ++lIt ) { Okular::ObjectRect * rect = *lIt; if ( (enhanceLinks && rect->objectType() == Okular::ObjectRect::Action) || (enhanceImages && rect->objectType() == Okular::ObjectRect::Image) ) { if ( limitsEnlarged.intersects( rect->boundingRect( scaledWidth, scaledHeight ).translated( -scaledCrop.topLeft() ) ) ) { mixedPainter->strokePath( rect->region(), QPen( normalColor, 0 ) ); } } } mixedPainter->restore(); } /** 7 -- BUFFERED FLOW. Copy BACKPIXMAP on DESTINATION PAINTER **/ if ( useBackBuffer ) { delete mixedPainter; destPainter->drawPixmap( limits.left(), limits.top(), *backPixmap ); delete backPixmap; } // delete object containers delete bufferedHighlights; delete bufferedAnnotations; delete unbufferedAnnotations; } /** Private Helpers :: Pixmap conversion **/ void PagePainter::cropPixmapOnImage( QImage & dest, const QPixmap * src, const QRect & r ) { qreal dpr = src->devicePixelRatioF(); // handle quickly the case in which the whole pixmap has to be converted if ( r == QRect( 0, 0, src->width() / dpr, src->height() / dpr ) ) { dest = src->toImage(); dest = dest.convertToFormat(QImage::Format_ARGB32_Premultiplied); } // else copy a portion of the src to an internal pixmap (smaller) and convert it else { QImage croppedImage( r.width() * dpr, r.height() * dpr, QImage::Format_ARGB32_Premultiplied ); croppedImage.setDevicePixelRatio(dpr); QPainter p( &croppedImage ); p.drawPixmap( 0, 0, *src, r.left(), r.top(), r.width(), r.height() ); p.end(); dest = croppedImage; } } void PagePainter::recolor(QImage *image, const QColor &foreground, const QColor &background) { if (image->format() != QImage::Format_ARGB32_Premultiplied) { qCWarning(OkularUiDebug) << "Wrong image format! Converting..."; *image = image->convertToFormat(QImage::Format_ARGB32_Premultiplied); } Q_ASSERT(image->format() == QImage::Format_ARGB32_Premultiplied); const float scaleRed = background.redF() - foreground.redF(); const float scaleGreen = background.greenF() - foreground.greenF(); const float scaleBlue = background.blueF() - foreground.blueF(); for (int y=0; yheight(); y++) { QRgb *pixels = reinterpret_cast(image->scanLine(y)); for (int x=0; xwidth(); x++) { const int lightness = qGray(pixels[x]); pixels[x] = qRgba(scaleRed * lightness + foreground.red(), scaleGreen * lightness + foreground.green(), scaleBlue * lightness + foreground.blue(), qAlpha(pixels[x])); } } } /** Private Helpers :: Image Drawing **/ // from Arthur - qt4 static inline int qt_div_255(int x) { return (x + (x>>8) + 0x80) >> 8; } void PagePainter::changeImageAlpha( QImage & image, unsigned int destAlpha ) { // iterate over all pixels changing the alpha component value unsigned int * data = (unsigned int *)image.bits(); unsigned int pixels = image.width() * image.height(); int source, sourceAlpha; for( unsigned int i = 0; i < pixels; ++i ) { // optimize this loop keeping byte order into account source = data[i]; if ( (sourceAlpha = qAlpha( source )) == 255 ) { // use destAlpha data[i] = qRgba( qRed(source), qGreen(source), qBlue(source), destAlpha ); } else { // use destAlpha * sourceAlpha product sourceAlpha = qt_div_255( destAlpha * sourceAlpha ); data[i] = qRgba( qRed(source), qGreen(source), qBlue(source), sourceAlpha ); } } } void PagePainter::drawShapeOnImage( QImage & image, const NormalizedPath & normPath, bool closeShape, const QPen & pen, const QBrush & brush, double penWidthMultiplier, RasterOperation op //float antiAliasRadius ) { // safety checks int pointsNumber = normPath.size(); if ( pointsNumber < 2 ) return; int imageWidth = image.width(); int imageHeight = image.height(); double fImageWidth = (double)imageWidth; double fImageHeight = (double)imageHeight; // stroke outline double penWidth = (double)pen.width() * penWidthMultiplier; QPainter painter(&image); painter.setRenderHint(QPainter::Antialiasing); QPen pen2 = pen; pen2.setWidthF(penWidth); painter.setPen(pen2); painter.setBrush(brush); if (op == Multiply) { painter.setCompositionMode(QPainter::CompositionMode_Multiply); } if ( brush.style() == Qt::NoBrush ) { // create a polygon QPolygonF poly( closeShape ? pointsNumber + 1 : pointsNumber ); for ( int i = 0; i < pointsNumber; ++i ) { poly[ i ] = QPointF( normPath[ i ].x * fImageWidth, normPath[ i ].y * fImageHeight ); } if ( closeShape ) poly[ pointsNumber ] = poly[ 0 ]; painter.drawPolyline( poly ); } else { // create a 'path' QPainterPath path; path.setFillRule( Qt::WindingFill ); path.moveTo( normPath[ 0 ].x * fImageWidth, normPath[ 0 ].y * fImageHeight ); for ( int i = 1; i < pointsNumber; i++ ) { path.lineTo( normPath[ i ].x * fImageWidth, normPath[ i ].y * fImageHeight ); } if ( closeShape ) path.closeSubpath(); painter.drawPath( path ); } } void PagePainter::drawEllipseOnImage( QImage & image, const NormalizedPath & rect, const QPen & pen, const QBrush & brush, double penWidthMultiplier, RasterOperation op ) { const double fImageWidth = (double) image.width(); const double fImageHeight = (double) image.height(); // stroke outline const double penWidth = (double)pen.width() * penWidthMultiplier; QPainter painter(&image); painter.setRenderHint(QPainter::Antialiasing); QPen pen2 = pen; pen2.setWidthF(penWidth); painter.setPen(pen2); painter.setBrush(brush); if ( op == Multiply ) { painter.setCompositionMode(QPainter::CompositionMode_Multiply); } const QPointF &topLeft { rect[0].x * fImageWidth, rect[0].y * fImageHeight }; const QSizeF &size { (rect[1].x - rect[0].x) * fImageWidth, (rect[1].y - rect[0].y) * fImageHeight }; const QRectF imgRect { topLeft, size }; if ( brush.style() == Qt::NoBrush ) { painter.drawArc( imgRect, 0, 16*360 ); } else { painter.drawEllipse( imgRect ); } } LineAnnotPainter::LineAnnotPainter( const Okular::LineAnnotation * a, QSizeF pageSize, double pageScale, const QTransform &toNormalizedImage ) : la { a } , pageSize { pageSize } , pageScale { pageScale } , toNormalizedImage { toNormalizedImage } , aspectRatio { pageSize.height() / pageSize.width() } , linePen { buildPen( a, a->style().width(), a->style().color() ) } { if ( ( la->lineClosed() || la->transformedLinePoints().count() == 2 ) && la->lineInnerColor().isValid() ) { fillBrush = QBrush( la->lineInnerColor() ); } } void LineAnnotPainter::draw( QImage &image ) const { if ( la->transformedLinePoints().count() == 2 ) { const Okular::NormalizedPoint delta { la->transformedLinePoints().last().x - la->transformedLinePoints().first().x, la->transformedLinePoints().first().y - la->transformedLinePoints().last().y }; const double angle { atan2( delta.y * aspectRatio, delta.x ) }; const double cosA { cos( -angle ) }; const double sinA { sin( -angle ) }; const QTransform tmpMatrix = QTransform { cosA, sinA / aspectRatio, -sinA, cosA / aspectRatio, la->transformedLinePoints().first().x, la->transformedLinePoints().first().y }; const double deaspectedY { delta.y * aspectRatio }; const double mainSegmentLength { sqrt( delta.x * delta.x + deaspectedY * deaspectedY ) }; const double lineendSize { std::min( 6. * la->style().width() / pageSize.width(), mainSegmentLength / 2. ) }; drawShortenedLine( mainSegmentLength, lineendSize, image, tmpMatrix ); drawLineEnds( mainSegmentLength, lineendSize, image, tmpMatrix ); drawLeaderLine( 0., image, tmpMatrix ); drawLeaderLine( mainSegmentLength, image, tmpMatrix ); } else if ( la->transformedLinePoints().count() > 2 ) { drawMainLine( image ); } } void LineAnnotPainter::drawMainLine( QImage &image ) const { // draw the line as normalized path into image PagePainter::drawShapeOnImage( image, transformPath( la->transformedLinePoints(), toNormalizedImage ), la->lineClosed(), linePen, fillBrush, pageScale, PagePainter::Multiply ); } void LineAnnotPainter::drawShortenedLine( double mainSegmentLength, double size, QImage &image, const QTransform& toNormalizedPage ) const { const QTransform combinedTransform { toNormalizedPage * toNormalizedImage }; const QList path { { shortenForArrow(size, la->lineStartStyle()), 0 }, { mainSegmentLength - shortenForArrow(size, la->lineEndStyle()), 0 } }; PagePainter::drawShapeOnImage( image, transformPath(path, combinedTransform), la->lineClosed(), linePen, fillBrush, pageScale, PagePainter::Multiply ); } void LineAnnotPainter::drawLineEnds( double mainSegmentLength, double size, QImage &image, const QTransform& transform ) const { switch ( la->lineStartStyle() ) { case Okular::LineAnnotation::Square: drawLineEndSquare( 0, -size, transform, image ); break; case Okular::LineAnnotation::Circle: drawLineEndCircle( 0, -size, transform, image ); break; case Okular::LineAnnotation::Diamond: drawLineEndDiamond( 0, -size, transform, image ); break; case Okular::LineAnnotation::OpenArrow: drawLineEndArrow( 0, -size, 1., false, transform, image ); break; case Okular::LineAnnotation::ClosedArrow: drawLineEndArrow( 0, -size, 1., true, transform, image ); break; case Okular::LineAnnotation::None: break; case Okular::LineAnnotation::Butt: drawLineEndButt( 0, size, transform, image ); break; case Okular::LineAnnotation::ROpenArrow: drawLineEndArrow( 0, size, 1., false, transform, image ); break; case Okular::LineAnnotation::RClosedArrow: drawLineEndArrow( 0, size, 1., true, transform, image ); break; case Okular::LineAnnotation::Slash: drawLineEndSlash( 0, -size, transform, image ); break; } switch ( la->lineEndStyle() ) { case Okular::LineAnnotation::Square: drawLineEndSquare( mainSegmentLength, size, transform, image ); break; case Okular::LineAnnotation::Circle: drawLineEndCircle( mainSegmentLength, size, transform, image ); break; case Okular::LineAnnotation::Diamond: drawLineEndDiamond( mainSegmentLength, size, transform, image ); break; case Okular::LineAnnotation::OpenArrow: drawLineEndArrow( mainSegmentLength, size, 1., false, transform, image ); break; case Okular::LineAnnotation::ClosedArrow: drawLineEndArrow( mainSegmentLength, size, 1., true, transform, image ); break; case Okular::LineAnnotation::None: break; case Okular::LineAnnotation::Butt: drawLineEndButt( mainSegmentLength, size, transform, image ); break; case Okular::LineAnnotation::ROpenArrow: drawLineEndArrow( mainSegmentLength, size, -1., false, transform, image ); break; case Okular::LineAnnotation::RClosedArrow: drawLineEndArrow( mainSegmentLength, size, -1., true, transform, image ); break; case Okular::LineAnnotation::Slash: drawLineEndSlash( mainSegmentLength, size, transform, image ); break; } } void LineAnnotPainter::drawLineEndArrow( double xEndPos, double size, double flipX, bool close, const QTransform& toNormalizedPage, QImage &image ) const { const QTransform combinedTransform { toNormalizedPage * toNormalizedImage }; const QList path { { xEndPos - size * flipX, size / 2. }, { xEndPos, 0 }, { xEndPos - size * flipX, -size / 2. }, }; PagePainter::drawShapeOnImage( image, transformPath(path, combinedTransform), close, linePen, fillBrush, pageScale, PagePainter::Multiply); } void LineAnnotPainter::drawLineEndButt( double xEndPos, double size, const QTransform& toNormalizedPage, QImage &image ) const { const QTransform combinedTransform { toNormalizedPage * toNormalizedImage }; const double halfSize { size / 2. }; const QList path { { xEndPos, halfSize }, { xEndPos, -halfSize }, }; PagePainter::drawShapeOnImage( image, transformPath(path, combinedTransform), true, linePen, fillBrush, pageScale, PagePainter::Multiply); } void LineAnnotPainter::drawLineEndCircle( double xEndPos, double size, const QTransform& toNormalizedPage, QImage &image ) const { /* transform the circle midpoint to intermediate normalized coordinates * where it's easy to construct the bounding rect of the circle */ Okular::NormalizedPoint center; toNormalizedPage.map( xEndPos - size / 2., 0, ¢er.x, ¢er.y ); const double halfSize { size / 2. }; const QList path { { center.x - halfSize, center.y - halfSize / aspectRatio }, { center.x + halfSize, center.y + halfSize / aspectRatio }, }; /* then transform bounding rect with toNormalizedImage */ PagePainter::drawEllipseOnImage( image, transformPath(path, toNormalizedImage), linePen, fillBrush, pageScale, PagePainter::Multiply); } void LineAnnotPainter::drawLineEndSquare( double xEndPos, double size, const QTransform& toNormalizedPage, QImage &image ) const { const QTransform combinedTransform { toNormalizedPage * toNormalizedImage }; const QList path { { xEndPos, size / 2. }, { xEndPos - size, size / 2. }, { xEndPos - size, -size / 2. }, { xEndPos, -size / 2. } }; PagePainter::drawShapeOnImage( image, transformPath(path, combinedTransform), true, linePen, fillBrush, pageScale, PagePainter::Multiply); } void LineAnnotPainter::drawLineEndDiamond( double xEndPos, double size, const QTransform& toNormalizedPage, QImage &image ) const { const QTransform combinedTransform { toNormalizedPage * toNormalizedImage }; const QList path { { xEndPos, 0 }, { xEndPos - size / 2., size / 2. }, { xEndPos - size, 0 }, { xEndPos - size / 2., -size / 2. } }; PagePainter::drawShapeOnImage( image, transformPath(path, combinedTransform), true, linePen, fillBrush, pageScale, PagePainter::Multiply); } void LineAnnotPainter::drawLineEndSlash( double xEndPos, double size, const QTransform& toNormalizedPage, QImage &image ) const { const QTransform combinedTransform { toNormalizedPage * toNormalizedImage }; const double halfSize { size / 2. }; const double xOffset { cos(M_PI/3.) * halfSize }; const QList path { { xEndPos - xOffset, halfSize }, { xEndPos + xOffset, -halfSize }, }; PagePainter::drawShapeOnImage( image, transformPath(path, combinedTransform), true, linePen, fillBrush, pageScale, PagePainter::Multiply); } void LineAnnotPainter::drawLeaderLine( double xEndPos, QImage &image, const QTransform& toNormalizedPage ) const { const QTransform combinedTransform = toNormalizedPage * toNormalizedImage; const double ll = aspectRatio * la->lineLeadingForwardPoint() / pageSize.height(); const double lle = aspectRatio * la->lineLeadingBackwardPoint() / pageSize.height(); const int sign { ll > 0 ? -1 : 1 }; QList path; if ( fabs( ll ) > 0 ) { path.append( { xEndPos, ll } ); // do we have the extension on the "back"? if ( fabs( lle ) > 0 ) { path.append( { xEndPos, sign * lle } ); } else { path.append( { xEndPos, 0 } ); } } PagePainter::drawShapeOnImage( image, transformPath(path, combinedTransform), false, linePen, fillBrush, pageScale, PagePainter::Multiply); } double LineAnnotPainter::shortenForArrow( double size, Okular::LineAnnotation::TermStyle endStyle ) { double shortenBy { 0 }; if ( endStyle == Okular::LineAnnotation::Square || endStyle == Okular::LineAnnotation::Circle || endStyle == Okular::LineAnnotation::Diamond || endStyle == Okular::LineAnnotation::ClosedArrow ) { shortenBy = size; } return shortenBy; } /* kate: replace-tabs on; indent-width 4; */ diff --git a/ui/pageview.cpp b/ui/pageview.cpp index 4779d7982..2fc434f2c 100644 --- a/ui/pageview.cpp +++ b/ui/pageview.cpp @@ -1,5753 +1,5753 @@ /*************************************************************************** * Copyright (C) 2004-2005 by Enrico Ros * * Copyright (C) 2004-2006 by Albert Astals Cid * * Copyright (C) 2017 Klarälvdalens Datakonsult AB, a KDAB Group * * company, info@kdab.com. Work sponsored by the * * LiMux project of the city of Munich * * * * With portions of code from kpdf/kpdf_pagewidget.cc by: * * Copyright (C) 2002 by Wilco Greven * * Copyright (C) 2003 by Christophe Devriese * * * * Copyright (C) 2003 by Laurent Montel * * Copyright (C) 2003 by Dirk Mueller * * Copyright (C) 2004 by James Ots * * Copyright (C) 2011 by Jiri Baum - NICTA * * * * 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 "pageview.h" // qt/kde includes #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // system includes #include #include // local includes #include "debug_ui.h" #include "formwidgets.h" #include "pageviewutils.h" #include "pagepainter.h" #include "core/annotations.h" #include "annotwindow.h" #include "guiutils.h" #include "annotationpopup.h" #include "pageviewannotator.h" #include "pageviewmouseannotation.h" #include "priorities.h" #include "toggleactionmenu.h" #include "okmenutitle.h" #ifdef HAVE_SPEECH #include "tts.h" #endif #include "videowidget.h" #include "core/action.h" #include "core/area.h" #include "core/document_p.h" #include "core/form.h" #include "core/page.h" #include "core/page_p.h" #include "core/misc.h" #include "core/generator.h" #include "core/movie.h" #include "core/audioplayer.h" #include "core/sourcereference.h" #include "core/tile.h" #include "settings.h" #include "settings_core.h" #include "url_utils.h" #include "magnifierview.h" static const int pageflags = PagePainter::Accessibility | PagePainter::EnhanceLinks | PagePainter::EnhanceImages | PagePainter::Highlights | PagePainter::TextSelection | PagePainter::Annotations; static const float kZoomValues[] = { 0.12, 0.25, 0.33, 0.50, 0.66, 0.75, 1.00, 1.25, 1.50, 2.00, 4.00, 8.00, 16.00 }; // This is the length of the text that will be shown when the user is searching for a specific piece of text. static const int searchTextPreviewLength = 21; // When following a link, only a preview of this length will be used to set the text of the action. static const int linkTextPreviewLength = 30; static inline double normClamp( double value, double def ) { return ( value < 0.0 || value > 1.0 ) ? def : value; } struct TableSelectionPart { PageViewItem * item; Okular::NormalizedRect rectInItem; Okular::NormalizedRect rectInSelection; TableSelectionPart(PageViewItem * item_p, const Okular::NormalizedRect &rectInItem_p, const Okular::NormalizedRect &rectInSelection_p); }; TableSelectionPart::TableSelectionPart( PageViewItem * item_p, const Okular::NormalizedRect &rectInItem_p, const Okular::NormalizedRect &rectInSelection_p) : item ( item_p ), rectInItem (rectInItem_p), rectInSelection (rectInSelection_p) { } // structure used internally by PageView for data storage class PageViewPrivate { public: PageViewPrivate( PageView *qq ); FormWidgetsController* formWidgetsController(); #ifdef HAVE_SPEECH OkularTTS* tts(); #endif QString selectedText() const; // the document, pageviewItems and the 'visible cache' PageView *q; Okular::Document * document; QVector< PageViewItem * > items; QLinkedList< PageViewItem * > visibleItems; MagnifierView *magnifierView; // view layout (columns and continuous in Settings), zoom and mouse PageView::ZoomMode zoomMode; float zoomFactor; QPoint mouseGrabOffset; QPoint mousePressPos; QPoint mouseSelectPos; QPoint previousMouseMovePos; int mouseMidLastY; bool mouseSelecting; QRect mouseSelectionRect; QColor mouseSelectionColor; bool mouseTextSelecting; QSet< int > pagesWithTextSelection; bool mouseOnRect; int mouseMode; MouseAnnotation * mouseAnnotation; // table selection QList tableSelectionCols; QList tableSelectionRows; QList tableSelectionParts; bool tableDividersGuessed; int lastSourceLocationViewportPageNumber; double lastSourceLocationViewportNormalizedX; double lastSourceLocationViewportNormalizedY; int controlWheelAccumulatedDelta; // auto scroll int scrollIncrement; QTimer * autoScrollTimer; // annotations PageViewAnnotator * annotator; //text annotation dialogs list QSet< AnnotWindow * > m_annowindows; // other stuff QTimer * delayResizeEventTimer; bool dirtyLayout; bool blockViewport; // prevents changes to viewport bool blockPixmapsRequest; // prevent pixmap requests PageViewMessage * messageWindow; // in pageviewutils.h bool m_formsVisible; FormWidgetsController *formsWidgetController; #ifdef HAVE_SPEECH OkularTTS * m_tts; #endif QTimer * refreshTimer; QSet refreshPages; // bbox state for Trim to Selection mode Okular::NormalizedRect trimBoundingBox; // infinite resizing loop prevention bool verticalScrollBarVisible; bool horizontalScrollBarVisible; // drag scroll QPoint dragScrollVector; QTimer dragScrollTimer; // left click depress QTimer leftClickTimer; // actions QAction * aRotateClockwise; QAction * aRotateCounterClockwise; QAction * aRotateOriginal; KSelectAction * aPageSizes; KActionMenu * aTrimMode; KToggleAction * aTrimMargins; QAction * aMouseNormal; QAction * aMouseSelect; QAction * aMouseTextSelect; QAction * aMouseTableSelect; QAction * aMouseMagnifier; KToggleAction * aTrimToSelection; KToggleAction * aToggleAnnotator; KSelectAction * aZoom; QAction * aZoomIn; QAction * aZoomOut; QAction * aZoomActual; KToggleAction * aZoomFitWidth; KToggleAction * aZoomFitPage; KToggleAction * aZoomAutoFit; KActionMenu * aViewMode; KToggleAction * aViewContinuous; QAction * aPrevAction; QAction * aToggleForms; QAction * aSpeakDoc; QAction * aSpeakPage; QAction * aSpeakStop; QAction * aSpeakPauseResume; KActionCollection * actionCollection; QActionGroup * mouseModeActionGroup; ToggleActionMenu * aMouseModeMenu; QAction * aFitWindowToPage; int setting_viewCols; bool rtl_Mode; // Keep track of whether tablet pen is currently pressed down bool penDown; // Keep track of mouse over link object const Okular::ObjectRect * mouseOverLinkObject; QScroller * scroller; }; PageViewPrivate::PageViewPrivate( PageView *qq ) : q( qq ) #ifdef HAVE_SPEECH , m_tts( nullptr ) #endif { } FormWidgetsController* PageViewPrivate::formWidgetsController() { if ( !formsWidgetController ) { formsWidgetController = new FormWidgetsController( document ); QObject::connect( formsWidgetController, SIGNAL( changed( int ) ), q, SLOT( slotFormChanged( int ) ) ); QObject::connect( formsWidgetController, SIGNAL( action( Okular::Action* ) ), q, SLOT( slotAction( Okular::Action* ) ) ); QObject::connect( formsWidgetController, &FormWidgetsController::formatAction, q, [this] (const Okular::Action *action, Okular::FormFieldText *fft ) { document->processFormatAction( action, fft ); } ); QObject::connect( formsWidgetController, &FormWidgetsController::keystrokeAction, q, [this] (const Okular::Action *action, Okular::FormFieldText *fft, bool &ok ) { document->processKeystrokeAction( action, fft, ok ); } ); QObject::connect( formsWidgetController, &FormWidgetsController::focusAction, q, [this] (const Okular::Action *action, Okular::FormFieldText *fft ) { document->processFocusAction( action, fft ); } ); QObject::connect( formsWidgetController, &FormWidgetsController::validateAction, q, [this] (const Okular::Action *action, Okular::FormFieldText *fft, bool &ok ) { document->processValidateAction( action, fft, ok ); } ); } return formsWidgetController; } #ifdef HAVE_SPEECH OkularTTS* PageViewPrivate::tts() { if ( !m_tts ) { m_tts = new OkularTTS( q ); if ( aSpeakStop ) { QObject::connect( m_tts, &OkularTTS::canPauseOrResume, aSpeakStop, &QAction::setEnabled ); } if ( aSpeakPauseResume ) { QObject::connect( m_tts, &OkularTTS::canPauseOrResume, aSpeakPauseResume, &QAction::setEnabled ); } } return m_tts; } #endif /* PageView. What's in this file? -> quick overview. * Code weight (in rows) and meaning: * 160 - constructor and creating actions plus their connected slots (empty stuff) * 70 - DocumentObserver inherited methodes (important) * 550 - events: mouse, keyboard, drag * 170 - slotRelayoutPages: set contents of the scrollview on continuous/single modes * 100 - zoom: zooming pages in different ways, keeping update the toolbar actions, etc.. * other misc functions: only slotRequestVisiblePixmaps and pickItemOnPoint noticeable, * and many insignificant stuff like this comment :-) */ PageView::PageView( QWidget *parent, Okular::Document *document ) : QAbstractScrollArea( parent ) , Okular::View( QStringLiteral( "PageView" ) ) { // create and initialize private storage structure d = new PageViewPrivate( this ); d->document = document; d->aRotateClockwise = nullptr; d->aRotateCounterClockwise = nullptr; d->aRotateOriginal = nullptr; d->aViewMode = nullptr; d->zoomMode = PageView::ZoomFitWidth; d->zoomFactor = 1.0; d->mouseSelecting = false; d->mouseTextSelecting = false; d->mouseOnRect = false; d->mouseMode = Okular::Settings::mouseMode(); d->mouseAnnotation = new MouseAnnotation( this, document ); d->tableDividersGuessed = false; d->lastSourceLocationViewportPageNumber = -1; d->lastSourceLocationViewportNormalizedX = 0.0; d->lastSourceLocationViewportNormalizedY = 0.0; d->controlWheelAccumulatedDelta = 0; d->scrollIncrement = 0; d->autoScrollTimer = nullptr; d->annotator = nullptr; d->dirtyLayout = false; d->blockViewport = false; d->blockPixmapsRequest = false; d->messageWindow = new PageViewMessage(this); d->m_formsVisible = false; d->formsWidgetController = nullptr; #ifdef HAVE_SPEECH d->m_tts = nullptr; #endif d->refreshTimer = nullptr; d->aRotateClockwise = nullptr; d->aRotateCounterClockwise = nullptr; d->aRotateOriginal = nullptr; d->aPageSizes = nullptr; d->aTrimMode = nullptr; d->aTrimMargins = nullptr; d->aTrimToSelection = nullptr; d->aMouseNormal = nullptr; d->aMouseSelect = nullptr; d->aMouseTextSelect = nullptr; d->aToggleAnnotator = nullptr; d->aZoomFitWidth = nullptr; d->aZoomFitPage = nullptr; d->aZoomAutoFit = nullptr; d->aViewMode = nullptr; d->aViewContinuous = nullptr; d->aPrevAction = nullptr; d->aToggleForms = nullptr; d->aSpeakDoc = nullptr; d->aSpeakPage = nullptr; d->aSpeakStop = nullptr; d->aSpeakPauseResume = nullptr; d->actionCollection = nullptr; d->aPageSizes=nullptr; d->setting_viewCols = Okular::Settings::viewColumns(); d->rtl_Mode = Okular::Settings::rtlReadingDirection(); d->mouseModeActionGroup = nullptr; d->aMouseModeMenu = nullptr; d->penDown = false; d->aMouseMagnifier = nullptr; d->aFitWindowToPage = nullptr; d->trimBoundingBox = Okular::NormalizedRect(); // Null box switch( Okular::Settings::zoomMode() ) { case 0: { d->zoomFactor = 1; d->zoomMode = PageView::ZoomFixed; break; } case 1: { d->zoomMode = PageView::ZoomFitWidth; break; } case 2: { d->zoomMode = PageView::ZoomFitPage; break; } case 3: { d->zoomMode = PageView::ZoomFitAuto; break; } } d->delayResizeEventTimer = new QTimer( this ); d->delayResizeEventTimer->setSingleShot( true ); d->delayResizeEventTimer->setObjectName("delayResizeEventTimer"); connect( d->delayResizeEventTimer, &QTimer::timeout, this, &PageView::delayedResizeEvent ); setFrameStyle(QFrame::NoFrame); setAttribute( Qt::WA_StaticContents ); setObjectName( QStringLiteral( "okular::pageView" ) ); // viewport setup: setup focus, and track mouse viewport()->setFocusProxy( this ); viewport()->setFocusPolicy( Qt::StrongFocus ); viewport()->setAttribute( Qt::WA_OpaquePaintEvent ); viewport()->setAttribute( Qt::WA_NoSystemBackground ); viewport()->setMouseTracking( true ); viewport()->setAutoFillBackground( false ); d->scroller = QScroller::scroller(viewport()); QScrollerProperties prop; prop.setScrollMetric(QScrollerProperties::DecelerationFactor, 0.3); prop.setScrollMetric(QScrollerProperties::MaximumVelocity, 1); prop.setScrollMetric(QScrollerProperties::OvershootScrollDistanceFactor, 0.05); prop.setScrollMetric(QScrollerProperties::OvershootDragDistanceFactor, 0.05); d->scroller->setScrollerProperties(prop); connect(d->scroller, &QScroller::stateChanged, this, &PageView::slotRequestVisiblePixmaps); // the apparently "magic" value of 20 is the same used internally in QScrollArea verticalScrollBar()->setCursor( Qt::ArrowCursor ); verticalScrollBar()->setSingleStep( 20 ); horizontalScrollBar()->setCursor( Qt::ArrowCursor ); horizontalScrollBar()->setSingleStep( 20 ); // connect the padding of the viewport to pixmaps requests connect(horizontalScrollBar(), &QAbstractSlider::valueChanged, this, &PageView::slotRequestVisiblePixmaps); connect(verticalScrollBar(), &QAbstractSlider::valueChanged, this, &PageView::slotRequestVisiblePixmaps); auto update_scroller = [=](){ d->scroller->scrollTo(QPoint(horizontalScrollBar()->value(), verticalScrollBar()->value()), 0); //sync scroller with scrollbar }; connect(verticalScrollBar(), &QAbstractSlider::sliderReleased, this, update_scroller); connect(horizontalScrollBar(), &QAbstractSlider::sliderReleased, this, update_scroller); connect(verticalScrollBar(), &QAbstractSlider::sliderMoved, this, update_scroller); connect(horizontalScrollBar(), &QAbstractSlider::sliderMoved, this, update_scroller); connect( &d->dragScrollTimer, &QTimer::timeout, this, &PageView::slotDragScroll ); d->leftClickTimer.setSingleShot( true ); connect( &d->leftClickTimer, &QTimer::timeout, this, &PageView::slotShowSizeAllCursor ); // set a corner button to resize the view to the page size // QPushButton * resizeButton = new QPushButton( viewport() ); // resizeButton->setPixmap( SmallIcon("crop") ); // setCornerWidget( resizeButton ); // resizeButton->setEnabled( false ); // connect(...); setAttribute( Qt::WA_InputMethodEnabled, true ); // Grab pinch gestures to zoom and rotate the view grabGesture(Qt::PinchGesture); d->magnifierView = new MagnifierView(document, this); d->magnifierView->hide(); d->magnifierView->setGeometry(0, 0, 351, 201); // TODO: more dynamic? connect(document, &Okular::Document::processMovieAction, this, &PageView::slotProcessMovieAction); connect(document, &Okular::Document::processRenditionAction, this, &PageView::slotProcessRenditionAction); // schedule the welcome message QMetaObject::invokeMethod(this, "slotShowWelcome", Qt::QueuedConnection); } PageView::~PageView() { #ifdef HAVE_SPEECH if ( d->m_tts ) d->m_tts->stopAllSpeechs(); #endif delete d->mouseAnnotation; // delete the local storage structure // We need to assign it to a different list otherwise slotAnnotationWindowDestroyed // will bite us and clear d->m_annowindows QSet< AnnotWindow * > annowindows = d->m_annowindows; d->m_annowindows.clear(); qDeleteAll( annowindows ); // delete all widgets qDeleteAll( d->items ); delete d->formsWidgetController; d->document->removeObserver( this ); delete d; } void PageView::setupBaseActions( KActionCollection * ac ) { d->actionCollection = ac; // Zoom actions ( higher scales takes lots of memory! ) d->aZoom = new KSelectAction(QIcon::fromTheme( QStringLiteral("page-zoom") ), i18n("Zoom"), this); ac->addAction(QStringLiteral("zoom_to"), d->aZoom ); d->aZoom->setEditable( true ); d->aZoom->setMaxComboViewCount( 14 ); connect( d->aZoom, SIGNAL(triggered(QAction*)), this, SLOT(slotZoom()) ); updateZoomText(); d->aZoomIn = KStandardAction::zoomIn( this, SLOT(slotZoomIn()), ac ); d->aZoomOut = KStandardAction::zoomOut( this, SLOT(slotZoomOut()), ac ); d->aZoomActual = KStandardAction::actualSize( this, &PageView::slotZoomActual, ac ); d->aZoomActual->setText(i18n("Zoom to 100%")); } void PageView::setupViewerActions( KActionCollection * ac ) { d->actionCollection = ac; ac->setDefaultShortcut(d->aZoomIn, QKeySequence(Qt::CTRL + Qt::ALT + Qt::Key_Plus)); ac->setDefaultShortcut(d->aZoomOut, QKeySequence(Qt::CTRL + Qt::ALT + Qt::Key_Minus)); // orientation menu actions d->aRotateClockwise = new QAction( QIcon::fromTheme( QStringLiteral("object-rotate-right") ), i18n( "Rotate &Right" ), this ); d->aRotateClockwise->setIconText( i18nc( "Rotate right", "Right" ) ); ac->addAction( QStringLiteral("view_orientation_rotate_cw"), d->aRotateClockwise ); d->aRotateClockwise->setEnabled( false ); connect( d->aRotateClockwise, &QAction::triggered, this, &PageView::slotRotateClockwise ); d->aRotateCounterClockwise = new QAction( QIcon::fromTheme( QStringLiteral("object-rotate-left") ), i18n( "Rotate &Left" ), this ); d->aRotateCounterClockwise->setIconText( i18nc( "Rotate left", "Left" ) ); ac->addAction( QStringLiteral("view_orientation_rotate_ccw"), d->aRotateCounterClockwise ); d->aRotateCounterClockwise->setEnabled( false ); connect( d->aRotateCounterClockwise, &QAction::triggered, this, &PageView::slotRotateCounterClockwise ); d->aRotateOriginal = new QAction( i18n( "Original Orientation" ), this ); ac->addAction( QStringLiteral("view_orientation_original"), d->aRotateOriginal ); d->aRotateOriginal->setEnabled( false ); connect( d->aRotateOriginal, &QAction::triggered, this, &PageView::slotRotateOriginal ); d->aPageSizes = new KSelectAction(i18n("&Page Size"), this); ac->addAction(QStringLiteral("view_pagesizes"), d->aPageSizes); d->aPageSizes->setEnabled( false ); connect( d->aPageSizes , SIGNAL(triggered(int)), this, SLOT(slotPageSizes(int)) ); // Trim View actions d->aTrimMode = new KActionMenu(i18n( "&Trim View" ), this ); d->aTrimMode->setDelayed( false ); ac->addAction(QStringLiteral("view_trim_mode"), d->aTrimMode ); d->aTrimMargins = new KToggleAction(QIcon::fromTheme( QStringLiteral("trim-margins") ), i18n( "&Trim Margins" ), d->aTrimMode->menu() ); d->aTrimMode->addAction( d->aTrimMargins ); ac->addAction( QStringLiteral("view_trim_margins"), d->aTrimMargins ); d->aTrimMargins->setData( QVariant::fromValue( (int)Okular::Settings::EnumTrimMode::Margins ) ); connect( d->aTrimMargins, &QAction::toggled, this, &PageView::slotTrimMarginsToggled ); d->aTrimMargins->setChecked( Okular::Settings::trimMargins() ); d->aTrimToSelection = new KToggleAction(QIcon::fromTheme( QStringLiteral("trim-to-selection") ), i18n( "Trim To &Selection" ), d->aTrimMode->menu() ); d->aTrimMode->addAction( d->aTrimToSelection); ac->addAction( QStringLiteral("view_trim_selection"), d->aTrimToSelection); d->aTrimToSelection->setData( QVariant::fromValue( (int)Okular::Settings::EnumTrimMode::Selection ) ); connect( d->aTrimToSelection, &QAction::toggled, this, &PageView::slotTrimToSelectionToggled ); d->aZoomFitWidth = new KToggleAction(QIcon::fromTheme( QStringLiteral("zoom-fit-width") ), i18n("Fit &Width"), this); ac->addAction(QStringLiteral("view_fit_to_width"), d->aZoomFitWidth ); connect( d->aZoomFitWidth, &QAction::toggled, this, &PageView::slotFitToWidthToggled ); d->aZoomFitPage = new KToggleAction(QIcon::fromTheme( QStringLiteral("zoom-fit-best") ), i18n("Fit &Page"), this); ac->addAction(QStringLiteral("view_fit_to_page"), d->aZoomFitPage ); connect( d->aZoomFitPage, &QAction::toggled, this, &PageView::slotFitToPageToggled ); d->aZoomAutoFit = new KToggleAction(QIcon::fromTheme( QStringLiteral("zoom-fit-best") ), i18n("&Auto Fit"), this); ac->addAction(QStringLiteral("view_auto_fit"), d->aZoomAutoFit ); connect( d->aZoomAutoFit, &QAction::toggled, this, &PageView::slotAutoFitToggled ); d->aFitWindowToPage = new QAction(QIcon::fromTheme( QStringLiteral("zoom-fit-width") ), i18n("Fit Wi&ndow to Page"), this); d->aFitWindowToPage->setEnabled( Okular::Settings::viewMode() == (int)Okular::Settings::EnumViewMode::Single ); ac->setDefaultShortcut(d->aFitWindowToPage, QKeySequence(Qt::CTRL + Qt::Key_J) ); ac->addAction( QStringLiteral("fit_window_to_page"), d->aFitWindowToPage ); connect( d->aFitWindowToPage, &QAction::triggered, this, &PageView::slotFitWindowToPage ); // View-Layout actions d->aViewMode = new KActionMenu( QIcon::fromTheme( QStringLiteral("view-split-left-right") ), i18n( "&View Mode" ), this ); d->aViewMode->setDelayed( false ); #define ADD_VIEWMODE_ACTION( text, name, id ) \ do { \ QAction *vm = new QAction( text, this ); \ vm->setCheckable( true ); \ vm->setData( QVariant::fromValue( id ) ); \ d->aViewMode->addAction( vm ); \ ac->addAction( QStringLiteral(name), vm ); \ vmGroup->addAction( vm ); \ } while( 0 ) ac->addAction(QStringLiteral("view_render_mode"), d->aViewMode ); QActionGroup *vmGroup = new QActionGroup( this ); //d->aViewMode->menu() ); ADD_VIEWMODE_ACTION( i18n( "Single Page" ), "view_render_mode_single", (int)Okular::Settings::EnumViewMode::Single ); ADD_VIEWMODE_ACTION( i18n( "Facing Pages" ), "view_render_mode_facing", (int)Okular::Settings::EnumViewMode::Facing ); ADD_VIEWMODE_ACTION( i18n( "Facing Pages (Center First Page)" ), "view_render_mode_facing_center_first", (int)Okular::Settings::EnumViewMode::FacingFirstCentered ); ADD_VIEWMODE_ACTION( i18n( "Overview" ), "view_render_mode_overview", (int)Okular::Settings::EnumViewMode::Summary ); const QList viewModeActions = d->aViewMode->menu()->actions(); for (QAction *viewModeAction : viewModeActions) { if (viewModeAction->data().toInt() == Okular::Settings::viewMode()) { viewModeAction->setChecked( true ); } } connect( vmGroup, &QActionGroup::triggered, this, &PageView::slotViewMode ); #undef ADD_VIEWMODE_ACTION d->aViewContinuous = new KToggleAction(QIcon::fromTheme( QStringLiteral("view-list-text") ), i18n("&Continuous"), this); ac->addAction(QStringLiteral("view_continuous"), d->aViewContinuous ); connect( d->aViewContinuous, &QAction::toggled, this, &PageView::slotContinuousToggled ); d->aViewContinuous->setChecked( Okular::Settings::viewContinuous() ); // Mouse mode actions for viewer mode d->mouseModeActionGroup = new QActionGroup( this ); d->mouseModeActionGroup->setExclusive( true ); d->aMouseNormal = new QAction( QIcon::fromTheme( QStringLiteral("transform-browse") ), i18n( "&Browse" ), this ); ac->addAction(QStringLiteral("mouse_drag"), d->aMouseNormal ); connect( d->aMouseNormal, &QAction::triggered, this, &PageView::slotSetMouseNormal ); d->aMouseNormal->setCheckable( true ); ac->setDefaultShortcut(d->aMouseNormal, QKeySequence(Qt::CTRL + Qt::Key_1)); d->aMouseNormal->setActionGroup( d->mouseModeActionGroup ); d->aMouseNormal->setChecked( Okular::Settings::mouseMode() == Okular::Settings::EnumMouseMode::Browse ); QAction * mz = new QAction(QIcon::fromTheme( QStringLiteral("page-zoom") ), i18n("&Zoom"), this); ac->addAction(QStringLiteral("mouse_zoom"), mz ); connect( mz, &QAction::triggered, this, &PageView::slotSetMouseZoom ); mz->setCheckable( true ); ac->setDefaultShortcut(mz, QKeySequence(Qt::CTRL + Qt::Key_2)); mz->setActionGroup( d->mouseModeActionGroup ); mz->setChecked( Okular::Settings::mouseMode() == Okular::Settings::EnumMouseMode::Zoom ); QAction * aToggleChangeColors = new QAction(i18n("&Toggle Change Colors"), this); ac->addAction(QStringLiteral("toggle_change_colors"), aToggleChangeColors ); connect( aToggleChangeColors, &QAction::triggered, this, &PageView::slotToggleChangeColors ); } // WARNING: 'setupViewerActions' must have been called before this method void PageView::setupActions( KActionCollection * ac ) { d->actionCollection = ac; ac->setDefaultShortcuts(d->aZoomIn, KStandardShortcut::zoomIn()); ac->setDefaultShortcuts(d->aZoomOut, KStandardShortcut::zoomOut()); // Mouse-Mode actions d->aMouseSelect = new QAction(QIcon::fromTheme( QStringLiteral("select-rectangular") ), i18n("Area &Selection"), this); ac->addAction(QStringLiteral("mouse_select"), d->aMouseSelect ); connect( d->aMouseSelect, &QAction::triggered, this, &PageView::slotSetMouseSelect ); d->aMouseSelect->setCheckable( true ); ac->setDefaultShortcut(d->aMouseSelect, Qt::CTRL + Qt::Key_3); d->aMouseSelect->setActionGroup( d->mouseModeActionGroup ); d->aMouseSelect->setChecked( Okular::Settings::mouseMode() == Okular::Settings::EnumMouseMode::RectSelect ); d->aMouseTextSelect = new QAction(QIcon::fromTheme( QStringLiteral("edit-select-text") ), i18n("&Text Selection"), this); ac->addAction(QStringLiteral("mouse_textselect"), d->aMouseTextSelect ); connect( d->aMouseTextSelect, &QAction::triggered, this, &PageView::slotSetMouseTextSelect ); d->aMouseTextSelect->setCheckable( true ); ac->setDefaultShortcut(d->aMouseTextSelect, Qt::CTRL + Qt::Key_4); d->aMouseTextSelect->setActionGroup( d->mouseModeActionGroup ); d->aMouseTextSelect->setChecked( Okular::Settings::mouseMode() == Okular::Settings::EnumMouseMode::TextSelect ); d->aMouseTableSelect = new QAction(QIcon::fromTheme( QStringLiteral("table") ), i18n("T&able Selection"), this); ac->addAction(QStringLiteral("mouse_tableselect"), d->aMouseTableSelect ); connect( d->aMouseTableSelect, &QAction::triggered, this, &PageView::slotSetMouseTableSelect ); d->aMouseTableSelect->setCheckable( true ); ac->setDefaultShortcut(d->aMouseTableSelect, Qt::CTRL + Qt::Key_5); d->aMouseTableSelect->setActionGroup( d->mouseModeActionGroup ); d->aMouseTableSelect->setChecked( Okular::Settings::mouseMode() == Okular::Settings::EnumMouseMode::TableSelect ); d->aMouseMagnifier = new QAction(QIcon::fromTheme( QStringLiteral("document-preview") ), i18n("&Magnifier"), this); ac->addAction(QStringLiteral("mouse_magnifier"), d->aMouseMagnifier ); connect( d->aMouseMagnifier, &QAction::triggered, this, &PageView::slotSetMouseMagnifier ); d->aMouseMagnifier->setCheckable( true ); ac->setDefaultShortcut(d->aMouseMagnifier, Qt::CTRL + Qt::Key_6); d->aMouseMagnifier->setActionGroup( d->mouseModeActionGroup ); d->aMouseMagnifier->setChecked( Okular::Settings::mouseMode() == Okular::Settings::EnumMouseMode::Magnifier ); // Mouse-Mode action menu d->aMouseModeMenu = new ToggleActionMenu( QIcon(),QString(), this, ToggleActionMenu::MenuButtonPopup, ToggleActionMenu::ImplicitDefaultAction ); d->aMouseModeMenu->addAction( d->aMouseSelect ); d->aMouseModeMenu->addAction( d->aMouseTextSelect ); d->aMouseModeMenu->addAction( d->aMouseTableSelect ); d->aMouseModeMenu->suggestDefaultAction( d->aMouseTextSelect ); d->aMouseModeMenu->setText( i18nc( "@action", "Selection Tools" ) ); ac->addAction( QStringLiteral( "mouse_selecttools" ), d->aMouseModeMenu ); d->aToggleAnnotator = new KToggleAction(QIcon::fromTheme( QStringLiteral("draw-freehand") ), i18n("&Review"), this); ac->addAction(QStringLiteral("mouse_toggle_annotate"), d->aToggleAnnotator ); d->aToggleAnnotator->setCheckable( true ); connect( d->aToggleAnnotator, &QAction::toggled, this, &PageView::slotToggleAnnotator ); ac->setDefaultShortcut(d->aToggleAnnotator, Qt::Key_F6); // speak actions #ifdef HAVE_SPEECH d->aSpeakDoc = new QAction( QIcon::fromTheme( QStringLiteral("text-speak") ), i18n( "Speak Whole Document" ), this ); ac->addAction( QStringLiteral("speak_document"), d->aSpeakDoc ); d->aSpeakDoc->setEnabled( false ); connect( d->aSpeakDoc, &QAction::triggered, this, &PageView::slotSpeakDocument ); d->aSpeakPage = new QAction( QIcon::fromTheme( QStringLiteral("text-speak") ), i18n( "Speak Current Page" ), this ); ac->addAction( QStringLiteral("speak_current_page"), d->aSpeakPage ); d->aSpeakPage->setEnabled( false ); connect( d->aSpeakPage, &QAction::triggered, this, &PageView::slotSpeakCurrentPage ); d->aSpeakStop = new QAction( QIcon::fromTheme( QStringLiteral("media-playback-stop") ), i18n( "Stop Speaking" ), this ); ac->addAction( QStringLiteral("speak_stop_all"), d->aSpeakStop ); d->aSpeakStop->setEnabled( false ); connect( d->aSpeakStop, &QAction::triggered, this, &PageView::slotStopSpeaks ); d->aSpeakPauseResume = new QAction( QIcon::fromTheme( QStringLiteral("media-playback-pause") ), i18n( "Pause/Resume Speaking" ), this ); ac->addAction( QStringLiteral("speak_pause_resume"), d->aSpeakPauseResume ); d->aSpeakPauseResume->setEnabled( false ); connect( d->aSpeakPauseResume, &QAction::triggered, this, &PageView::slotPauseResumeSpeech ); #else - d->aSpeakDoc = 0; - d->aSpeakPage = 0; - d->aSpeakStop = 0; - d->aSpeakPauseResume = 0; + d->aSpeakDoc = nullptr; + d->aSpeakPage = nullptr; + d->aSpeakStop = nullptr; + d->aSpeakPauseResume = nullptr; #endif // Other actions QAction * su = new QAction(i18n("Scroll Up"), this); ac->addAction(QStringLiteral("view_scroll_up"), su ); connect( su, &QAction::triggered, this, &PageView::slotAutoScrollUp ); ac->setDefaultShortcut(su, QKeySequence(Qt::SHIFT + Qt::Key_Up)); addAction(su); QAction * sd = new QAction(i18n("Scroll Down"), this); ac->addAction(QStringLiteral("view_scroll_down"), sd ); connect( sd, &QAction::triggered, this, &PageView::slotAutoScrollDown ); ac->setDefaultShortcut(sd, QKeySequence(Qt::SHIFT + Qt::Key_Down)); addAction(sd); QAction * spu = new QAction(i18n("Scroll Page Up"), this); ac->addAction( QStringLiteral("view_scroll_page_up"), spu ); connect( spu, &QAction::triggered, this, &PageView::slotScrollUp ); ac->setDefaultShortcut(spu, QKeySequence(Qt::SHIFT + Qt::Key_Space)); addAction( spu ); QAction * spd = new QAction(i18n("Scroll Page Down"), this); ac->addAction( QStringLiteral("view_scroll_page_down"), spd ); connect( spd, &QAction::triggered, this, &PageView::slotScrollDown ); ac->setDefaultShortcut(spd, QKeySequence(Qt::Key_Space)); addAction( spd ); d->aToggleForms = new QAction( this ); ac->addAction( QStringLiteral("view_toggle_forms"), d->aToggleForms ); connect( d->aToggleForms, &QAction::triggered, this, &PageView::slotToggleForms ); d->aToggleForms->setEnabled( false ); toggleFormWidgets( false ); // Setup undo and redo actions QAction *kundo = KStandardAction::create( KStandardAction::Undo, d->document, SLOT(undo()), ac ); QAction *kredo = KStandardAction::create( KStandardAction::Redo, d->document, SLOT(redo()), ac ); connect(d->document, &Okular::Document::canUndoChanged, kundo, &QAction::setEnabled); connect(d->document, &Okular::Document::canRedoChanged, kredo, &QAction::setEnabled); kundo->setEnabled(false); kredo->setEnabled(false); } bool PageView::canFitPageWidth() const { return Okular::Settings::viewMode() != Okular::Settings::EnumViewMode::Single || d->zoomMode != ZoomFitWidth; } void PageView::fitPageWidth( int page ) { // zoom: Fit Width, columns: 1. setActions + relayout + setPage + update d->zoomMode = ZoomFitWidth; Okular::Settings::setViewMode( 0 ); d->aZoomFitWidth->setChecked( true ); d->aZoomFitPage->setChecked( false ); d->aZoomAutoFit->setChecked( false ); d->aViewMode->menu()->actions().at( 0 )->setChecked( true ); viewport()->setUpdatesEnabled( false ); slotRelayoutPages(); viewport()->setUpdatesEnabled( true ); d->document->setViewportPage( page ); updateZoomText(); setFocus(); } void PageView::openAnnotationWindow( Okular::Annotation * annotation, int pageNumber ) { if ( !annotation ) return; // find the annot window AnnotWindow* existWindow = nullptr; for (AnnotWindow *aw : qAsConst(d->m_annowindows)) { if ( aw->annotation() == annotation ) { existWindow = aw; break; } } if ( existWindow == nullptr ) { existWindow = new AnnotWindow( this, annotation, d->document, pageNumber ); connect(existWindow, &QObject::destroyed, this, &PageView::slotAnnotationWindowDestroyed); d->m_annowindows << existWindow; } else { existWindow->raise(); existWindow->findChild()->setFocus(); } existWindow->show(); } void PageView::slotAnnotationWindowDestroyed( QObject * window ) { d->m_annowindows.remove( static_cast( window ) ); } void PageView::displayMessage( const QString & message, const QString & details, PageViewMessage::Icon icon, int duration ) { if ( !Okular::Settings::showOSD() ) { if (icon == PageViewMessage::Error) { if ( !details.isEmpty() ) KMessageBox::detailedError( this, message, details ); else KMessageBox::error( this, message ); } return; } // hide messageWindow if string is empty if ( message.isEmpty() ) return d->messageWindow->hide(); // display message (duration is length dependent) if (duration==-1) { duration = 500 + 100 * message.length(); if ( !details.isEmpty() ) duration += 500 + 100 * details.length(); } d->messageWindow->display( message, details, icon, duration ); } void PageView::reparseConfig() { // set the scroll bars policies Qt::ScrollBarPolicy scrollBarMode = Okular::Settings::showScrollBars() ? Qt::ScrollBarAsNeeded : Qt::ScrollBarAlwaysOff; if ( horizontalScrollBarPolicy() != scrollBarMode ) { setHorizontalScrollBarPolicy( scrollBarMode ); setVerticalScrollBarPolicy( scrollBarMode ); } if ( Okular::Settings::viewMode() == Okular::Settings::EnumViewMode::Summary && ( (int)Okular::Settings::viewColumns() != d->setting_viewCols ) ) { d->setting_viewCols = Okular::Settings::viewColumns(); slotRelayoutPages(); } if (Okular::Settings::rtlReadingDirection() != d->rtl_Mode ) { d->rtl_Mode = Okular::Settings::rtlReadingDirection(); slotRelayoutPages(); } updatePageStep(); if ( d->annotator ) { d->annotator->setEnabled( false ); d->annotator->reparseConfig(); if ( d->aToggleAnnotator->isChecked() ) slotToggleAnnotator( true ); } // Something like invert colors may have changed // As we don't have a way to find out the old value // We just update the viewport, this shouldn't be that bad // since it's just a repaint of pixmaps we already have viewport()->update(); } KActionCollection *PageView::actionCollection() const { return d->actionCollection; } QAction *PageView::toggleFormsAction() const { return d->aToggleForms; } int PageView::contentAreaWidth() const { return horizontalScrollBar()->maximum() + viewport()->width(); } int PageView::contentAreaHeight() const { return verticalScrollBar()->maximum() + viewport()->height(); } QPoint PageView::contentAreaPosition() const { return QPoint( horizontalScrollBar()->value(), verticalScrollBar()->value() ); } QPoint PageView::contentAreaPoint( const QPoint & pos ) const { return pos + contentAreaPosition(); } QPointF PageView::contentAreaPoint( const QPointF & pos ) const { return pos + contentAreaPosition(); } QString PageViewPrivate::selectedText() const { if ( pagesWithTextSelection.isEmpty() ) return QString(); QString text; QList< int > selpages = pagesWithTextSelection.values(); std::sort(selpages.begin(), selpages.end()); const Okular::Page * pg = nullptr; if ( selpages.count() == 1 ) { pg = document->page( selpages.first() ); text.append( pg->text( pg->textSelection(), Okular::TextPage::CentralPixelTextAreaInclusionBehaviour ) ); } else { pg = document->page( selpages.first() ); text.append( pg->text( pg->textSelection(), Okular::TextPage::CentralPixelTextAreaInclusionBehaviour ) ); int end = selpages.count() - 1; for( int i = 1; i < end; ++i ) { pg = document->page( selpages.at( i ) ); text.append( pg->text( nullptr, Okular::TextPage::CentralPixelTextAreaInclusionBehaviour ) ); } pg = document->page( selpages.last() ); text.append( pg->text( pg->textSelection(), Okular::TextPage::CentralPixelTextAreaInclusionBehaviour ) ); } return text; } void PageView::copyTextSelection() const { const QString text = d->selectedText(); if ( !text.isEmpty() ) { QClipboard *cb = QApplication::clipboard(); cb->setText( text, QClipboard::Clipboard ); } } void PageView::selectAll() { for ( const PageViewItem * item : qAsConst( d->items ) ) { Okular::RegularAreaRect * area = textSelectionForItem( item ); d->pagesWithTextSelection.insert( item->pageNumber() ); d->document->setPageTextSelection( item->pageNumber(), area, palette().color( QPalette::Active, QPalette::Highlight ) ); } } void PageView::createAnnotationsVideoWidgets(PageViewItem *item, const QLinkedList< Okular::Annotation * > &annotations) { qDeleteAll( item->videoWidgets() ); item->videoWidgets().clear(); for ( Okular::Annotation * a : annotations ) { if ( a->subType() == Okular::Annotation::AMovie ) { Okular::MovieAnnotation * movieAnn = static_cast< Okular::MovieAnnotation * >( a ); VideoWidget * vw = new VideoWidget( movieAnn, movieAnn->movie(), d->document, viewport() ); item->videoWidgets().insert( movieAnn->movie(), vw ); vw->pageInitialized(); } else if ( a->subType() == Okular::Annotation::ARichMedia ) { Okular::RichMediaAnnotation * richMediaAnn = static_cast< Okular::RichMediaAnnotation * >( a ); VideoWidget * vw = new VideoWidget( richMediaAnn, richMediaAnn->movie(), d->document, viewport() ); item->videoWidgets().insert( richMediaAnn->movie(), vw ); vw->pageInitialized(); } else if ( a->subType() == Okular::Annotation::AScreen ) { const Okular::ScreenAnnotation * screenAnn = static_cast< Okular::ScreenAnnotation * >( a ); Okular::Movie *movie = GuiUtils::renditionMovieFromScreenAnnotation( screenAnn ); if ( movie ) { VideoWidget * vw = new VideoWidget( screenAnn, movie, d->document, viewport() ); item->videoWidgets().insert( movie, vw ); vw->pageInitialized(); } } } } //BEGIN DocumentObserver inherited methods void PageView::notifySetup( const QVector< Okular::Page * > & pageSet, int setupFlags ) { bool documentChanged = setupFlags & Okular::DocumentObserver::DocumentChanged; const bool allownotes = d->document->isAllowed( Okular::AllowNotes ); const bool allowfillforms = d->document->isAllowed( Okular::AllowFillForms ); // allownotes may have changed if ( d->aToggleAnnotator ) d->aToggleAnnotator->setEnabled( allownotes ); // reuse current pages if nothing new if ( ( pageSet.count() == d->items.count() ) && !documentChanged && !( setupFlags & Okular::DocumentObserver::NewLayoutForPages ) ) { int count = pageSet.count(); for ( int i = 0; (i < count) && !documentChanged; i++ ) { if ( (int)pageSet[i]->number() != d->items[i]->pageNumber() ) { documentChanged = true; } else { // even if the document has not changed, allowfillforms may have // changed, so update all fields' "canBeFilled" flag const QSet formWidgetsList = d->items[i]->formWidgets(); for ( FormWidgetIface *w : formWidgetsList ) w->setCanBeFilled( allowfillforms ); } } if ( !documentChanged ) { if ( setupFlags & Okular::DocumentObserver::UrlChanged ) { // Here with UrlChanged and no document changed it means we // need to update all the Annotation* and Form* otherwise // they still point to the old document ones, luckily the old ones are still // around so we can look for the new ones using unique ids, etc d->mouseAnnotation->updateAnnotationPointers(); for (AnnotWindow *aw : qAsConst(d->m_annowindows)) { Okular::Annotation *newA = d->document->page( aw->pageNumber() )->annotation( aw->annotation()->uniqueName() ); aw->updateAnnotation( newA ); } const QRect viewportRect( horizontalScrollBar()->value(), verticalScrollBar()->value(), viewport()->width(), viewport()->height() ); for ( int i = 0; i < count; i++ ) { PageViewItem *item = d->items[i]; const QSet fws = item->formWidgets(); for ( FormWidgetIface *w : fws ) { Okular::FormField *f = Okular::PagePrivate::findEquivalentForm( d->document->page( i ), w->formField() ); if (f) { w->setFormField( f ); } else { qWarning() << "Lost form field on document save, something is wrong"; item->formWidgets().remove(w); delete w; } } // For the video widgets we don't really care about reusing them since they don't contain much info so just // create them again createAnnotationsVideoWidgets( item, pageSet[i]->annotations() ); const QHash videoWidgets = item->videoWidgets(); for ( VideoWidget *vw : videoWidgets ) { const Okular::NormalizedRect r = vw->normGeometry(); vw->setGeometry( qRound( item->uncroppedGeometry().left() + item->uncroppedWidth() * r.left ) + 1 - viewportRect.left(), qRound( item->uncroppedGeometry().top() + item->uncroppedHeight() * r.top ) + 1 - viewportRect.top(), qRound( fabs( r.right - r.left ) * item->uncroppedGeometry().width() ), qRound( fabs( r.bottom - r.top ) * item->uncroppedGeometry().height() ) ); // Workaround, otherwise the size somehow gets lost vw->show(); vw->hide(); } } } return; } } // mouseAnnotation must not access our PageViewItem widgets any longer d->mouseAnnotation->reset(); // delete all widgets (one for each page in pageSet) qDeleteAll( d->items ); d->items.clear(); d->visibleItems.clear(); d->pagesWithTextSelection.clear(); toggleFormWidgets( false ); if ( d->formsWidgetController ) d->formsWidgetController->dropRadioButtons(); bool haspages = !pageSet.isEmpty(); bool hasformwidgets = false; // create children widgets for ( const Okular::Page * page : pageSet ) { PageViewItem * item = new PageViewItem( page ); d->items.push_back( item ); #ifdef PAGEVIEW_DEBUG qCDebug(OkularUiDebug).nospace() << "cropped geom for " << d->items.last()->pageNumber() << " is " << d->items.last()->croppedGeometry(); #endif const QLinkedList< Okular::FormField * > pageFields = page->formFields(); for ( Okular::FormField * ff : pageFields ) { FormWidgetIface * w = FormWidgetFactory::createWidget( ff, viewport() ); if ( w ) { w->setPageItem( item ); w->setFormWidgetsController( d->formWidgetsController() ); w->setVisibility( false ); w->setCanBeFilled( allowfillforms ); item->formWidgets().insert( w ); hasformwidgets = true; } } createAnnotationsVideoWidgets( item, page->annotations() ); } // invalidate layout so relayout/repaint will happen on next viewport change if ( haspages ) { // We do a delayed call to slotRelayoutPages but also set the dirtyLayout // because we might end up in notifyViewportChanged while slotRelayoutPages // has not been done and we don't want that to happen d->dirtyLayout = true; QMetaObject::invokeMethod(this, "slotRelayoutPages", Qt::QueuedConnection); } else { // update the mouse cursor when closing because we may have close through a link and // want the cursor to come back to the normal cursor updateCursor(); // then, make the message window and scrollbars disappear, and trigger a repaint d->messageWindow->hide(); resizeContentArea( QSize( 0,0 ) ); viewport()->update(); // when there is no change to the scrollbars, no repaint would // be done and the old document would still be shown } // OSD to display pages if ( documentChanged && pageSet.count() > 0 && Okular::Settings::showOSD() ) d->messageWindow->display( i18np(" Loaded a one-page document.", " Loaded a %1-page document.", pageSet.count() ), QString(), PageViewMessage::Info, 4000 ); updateActionState( haspages, documentChanged, hasformwidgets ); // We need to assign it to a different list otherwise slotAnnotationWindowDestroyed // will bite us and clear d->m_annowindows QSet< AnnotWindow * > annowindows = d->m_annowindows; d->m_annowindows.clear(); qDeleteAll( annowindows ); selectionClear(); } void PageView::updateActionState( bool haspages, bool documentChanged, bool hasformwidgets ) { if ( d->aPageSizes ) { // may be null if dummy mode is on bool pageSizes = d->document->supportsPageSizes(); d->aPageSizes->setEnabled( pageSizes ); // set the new page sizes: // - if the generator supports them // - if the document changed if ( pageSizes && documentChanged ) { QStringList items; const QList sizes = d->document->pageSizes(); for ( const Okular::PageSize &p : sizes ) items.append( p.name() ); d->aPageSizes->setItems( items ); } } if ( d->aTrimMargins ) d->aTrimMargins->setEnabled( haspages ); if ( d->aTrimToSelection ) d->aTrimToSelection->setEnabled( haspages ); if ( d->aViewMode ) d->aViewMode->setEnabled( haspages ); if ( d->aViewContinuous ) d->aViewContinuous->setEnabled( haspages ); if ( d->aZoomFitWidth ) d->aZoomFitWidth->setEnabled( haspages ); if ( d->aZoomFitPage ) d->aZoomFitPage->setEnabled( haspages ); if ( d->aZoomAutoFit ) d->aZoomAutoFit->setEnabled( haspages ); if ( d->aZoom ) { d->aZoom->selectableActionGroup()->setEnabled( haspages ); d->aZoom->setEnabled( haspages ); } if ( d->aZoomIn ) d->aZoomIn->setEnabled( haspages ); if ( d->aZoomOut ) d->aZoomOut->setEnabled( haspages ); if ( d->aZoomActual ) d->aZoomActual->setEnabled( haspages && d->zoomFactor != 1.0 ); if ( d->mouseModeActionGroup ) d->mouseModeActionGroup->setEnabled( haspages ); if ( d->aMouseModeMenu ) d->aMouseModeMenu->setEnabled( haspages ); if ( d->aRotateClockwise ) d->aRotateClockwise->setEnabled( haspages ); if ( d->aRotateCounterClockwise ) d->aRotateCounterClockwise->setEnabled( haspages ); if ( d->aRotateOriginal ) d->aRotateOriginal->setEnabled( haspages ); if ( d->aToggleForms ) { // may be null if dummy mode is on d->aToggleForms->setEnabled( haspages && hasformwidgets ); } bool allowAnnotations = d->document->isAllowed( Okular::AllowNotes ); if ( d->annotator ) { bool allowTools = haspages && allowAnnotations; d->annotator->setToolsEnabled( allowTools ); d->annotator->setTextToolsEnabled( allowTools && d->document->supportsSearching() ); } if ( d->aToggleAnnotator ) { if ( !allowAnnotations && d->aToggleAnnotator->isChecked() ) { d->aToggleAnnotator->trigger(); } d->aToggleAnnotator->setEnabled( allowAnnotations ); } #ifdef HAVE_SPEECH if ( d->aSpeakDoc ) { const bool enablettsactions = haspages ? Okular::Settings::useTTS() : false; d->aSpeakDoc->setEnabled( enablettsactions ); d->aSpeakPage->setEnabled( enablettsactions ); } #endif if (d->aMouseMagnifier) d->aMouseMagnifier->setEnabled(d->document->supportsTiles()); if ( d->aFitWindowToPage ) d->aFitWindowToPage->setEnabled( haspages && !Okular::Settings::viewContinuous() ); } bool PageView::areSourceLocationsShownGraphically() const { return Okular::Settings::showSourceLocationsGraphically(); } void PageView::setShowSourceLocationsGraphically(bool show) { if( show == Okular::Settings::showSourceLocationsGraphically() ) { return; } Okular::Settings::setShowSourceLocationsGraphically( show ); viewport()->update(); } void PageView::setLastSourceLocationViewport( const Okular::DocumentViewport& vp ) { if( vp.rePos.enabled ) { d->lastSourceLocationViewportNormalizedX = normClamp( vp.rePos.normalizedX, 0.5 ); d->lastSourceLocationViewportNormalizedY = normClamp( vp.rePos.normalizedY, 0.0 ); } else { d->lastSourceLocationViewportNormalizedX = 0.5; d->lastSourceLocationViewportNormalizedY = 0.0; } d->lastSourceLocationViewportPageNumber = vp.pageNumber; viewport()->update(); } void PageView::clearLastSourceLocationViewport() { d->lastSourceLocationViewportPageNumber = -1; d->lastSourceLocationViewportNormalizedX = 0.0; d->lastSourceLocationViewportNormalizedY = 0.0; viewport()->update(); } void PageView::notifyViewportChanged( bool smoothMove ) { QMetaObject::invokeMethod(this, "slotRealNotifyViewportChanged", Qt::QueuedConnection, Q_ARG( bool, smoothMove )); } void PageView::slotRealNotifyViewportChanged( bool smoothMove ) { // if we are the one changing viewport, skip this notify if ( d->blockViewport ) return; // block setViewport outgoing calls d->blockViewport = true; // find PageViewItem matching the viewport description const Okular::DocumentViewport & vp = d->document->viewport(); const PageViewItem * item = nullptr; for ( const PageViewItem * tmpItem : qAsConst( d->items ) ) if ( tmpItem->pageNumber() == vp.pageNumber ) { item = tmpItem; break; } if ( !item ) { qCWarning(OkularUiDebug) << "viewport for page" << vp.pageNumber << "has no matching item!"; d->blockViewport = false; return; } #ifdef PAGEVIEW_DEBUG qCDebug(OkularUiDebug) << "document viewport changed"; #endif // relayout in "Single Pages" mode or if a relayout is pending d->blockPixmapsRequest = true; if ( !Okular::Settings::viewContinuous() || d->dirtyLayout ) slotRelayoutPages(); // restore viewport center or use default {x-center,v-top} alignment const QPoint centerCoord = viewportToContentArea( vp ); // if smooth movement requested, setup parameters and start it center( centerCoord.x(), centerCoord.y(), smoothMove ); d->blockPixmapsRequest = false; // request visible pixmaps in the current viewport and recompute it slotRequestVisiblePixmaps(); // enable setViewport calls d->blockViewport = false; if( viewport() ) { viewport()->update(); } // since the page has moved below cursor, update it updateCursor(); } void PageView::notifyPageChanged( int pageNumber, int changedFlags ) { // only handle pixmap / highlight changes notifies if ( changedFlags & DocumentObserver::Bookmark ) return; if ( changedFlags & DocumentObserver::Annotations ) { const QLinkedList< Okular::Annotation * > annots = d->document->page( pageNumber )->annotations(); const QLinkedList< Okular::Annotation * >::ConstIterator annItEnd = annots.end(); QSet< AnnotWindow * >::Iterator it = d->m_annowindows.begin(); for ( ; it != d->m_annowindows.end(); ) { QLinkedList< Okular::Annotation * >::ConstIterator annIt = std::find( annots.begin(), annots.end(), (*it)->annotation() ); if ( annIt != annItEnd ) { (*it)->reloadInfo(); ++it; } else { AnnotWindow *w = *it; it = d->m_annowindows.erase( it ); // Need to delete after removing from the list // otherwise deleting will call slotAnnotationWindowDestroyed which will mess // the list and the iterators delete w; } } d->mouseAnnotation->notifyAnnotationChanged( pageNumber ); } if ( changedFlags & DocumentObserver::BoundingBox ) { #ifdef PAGEVIEW_DEBUG qCDebug(OkularUiDebug) << "BoundingBox change on page" << pageNumber; #endif slotRelayoutPages(); slotRequestVisiblePixmaps(); // TODO: slotRelayoutPages() may have done this already! // Repaint the whole widget since layout may have changed viewport()->update(); return; } // iterate over visible items: if page(pageNumber) is one of them, repaint it for ( const PageViewItem * visibleItem : qAsConst( d->visibleItems ) ) if ( visibleItem->pageNumber() == pageNumber && visibleItem->isVisible() ) { // update item's rectangle plus the little outline QRect expandedRect = visibleItem->croppedGeometry(); // a PageViewItem is placed in the global page layout, // while we need to map its position in the viewport coordinates // (to get the correct area to repaint) expandedRect.translate( -contentAreaPosition() ); expandedRect.adjust( -1, -1, 3, 3 ); viewport()->update( expandedRect ); // if we were "zoom-dragging" do not overwrite the "zoom-drag" cursor if ( cursor().shape() != Qt::SizeVerCursor ) { // since the page has been regenerated below cursor, update it updateCursor(); } break; } } void PageView::notifyContentsCleared( int changedFlags ) { // if pixmaps were cleared, re-ask them if ( changedFlags & DocumentObserver::Pixmap ) QMetaObject::invokeMethod(this, "slotRequestVisiblePixmaps", Qt::QueuedConnection); } void PageView::notifyZoom( int factor ) { if ( factor > 0 ) updateZoom( ZoomIn ); else updateZoom( ZoomOut ); } bool PageView::canUnloadPixmap( int pageNumber ) const { if ( Okular::SettingsCore::memoryLevel() == Okular::SettingsCore::EnumMemoryLevel::Low || Okular::SettingsCore::memoryLevel() == Okular::SettingsCore::EnumMemoryLevel::Normal ) { // if the item is visible, forbid unloading for ( const PageViewItem * visibleItem : qAsConst( d->visibleItems ) ) if ( visibleItem->pageNumber() == pageNumber ) return false; } else { // forbid unloading of the visible items, and of the previous and next for ( const PageViewItem * visibleItem : qAsConst( d->visibleItems ) ) if ( abs( visibleItem->pageNumber() - pageNumber ) <= 1 ) return false; } // if hidden premit unloading return true; } void PageView::notifyCurrentPageChanged( int previous, int current ) { if ( previous != -1 ) { PageViewItem * item = d->items.at( previous ); if ( item ) { const QHash videoWidgetsList = item->videoWidgets(); for ( VideoWidget *videoWidget : videoWidgetsList ) videoWidget->pageLeft(); } // On close, run the widget scripts, needed for running animated PDF const Okular::Page *page = d->document->page( previous ); const QLinkedList annotations = page->annotations(); for ( Okular::Annotation *annotation : annotations ) { if ( annotation->subType() == Okular::Annotation::AWidget ) { Okular::WidgetAnnotation *widgetAnnotation = static_cast( annotation ); d->document->processAction( widgetAnnotation->additionalAction( Okular::Annotation::PageClosing ) ); } } } if ( current != -1 ) { PageViewItem * item = d->items.at( current ); if ( item ) { const QHash videoWidgetsList = item->videoWidgets(); for ( VideoWidget *videoWidget : videoWidgetsList ) videoWidget->pageEntered(); } // update zoom text and factor if in a ZoomFit/* zoom mode if ( d->zoomMode != ZoomFixed ) updateZoomText(); // Opening any widget scripts, needed for running animated PDF const Okular::Page *page = d->document->page( current ); const QLinkedList annotations = page->annotations(); for ( Okular::Annotation *annotation : annotations ) { if ( annotation->subType() == Okular::Annotation::AWidget ) { Okular::WidgetAnnotation *widgetAnnotation = static_cast( annotation ); d->document->processAction( widgetAnnotation->additionalAction( Okular::Annotation::PageOpening ) ); } } } } //END DocumentObserver inherited methods //BEGIN View inherited methods bool PageView::supportsCapability( ViewCapability capability ) const { switch ( capability ) { case Zoom: case ZoomModality: case Continuous: case ViewModeModality: case TrimMargins: return true; } return false; } Okular::View::CapabilityFlags PageView::capabilityFlags( ViewCapability capability ) const { switch ( capability ) { case Zoom: case ZoomModality: case Continuous: case ViewModeModality: case TrimMargins: return CapabilityRead | CapabilityWrite | CapabilitySerializable; } return NoFlag; } QVariant PageView::capability( ViewCapability capability ) const { switch ( capability ) { case Zoom: return d->zoomFactor; case ZoomModality: return d->zoomMode; case Continuous: return d->aViewContinuous ? d->aViewContinuous->isChecked() : true; case ViewModeModality: { const int nActions = d->aViewMode ? d->aViewMode->menu()->actions().size() : 0; for (int i=0; i < nActions; ++i) { const QAction* action = d->aViewMode->menu()->actions().at(i); if ( action->isChecked() ) return action->data(); } return QVariant(); } case TrimMargins: return d->aTrimMargins ? d->aTrimMargins->isChecked() : false; } return QVariant(); } void PageView::setCapability( ViewCapability capability, const QVariant &option ) { switch ( capability ) { case Zoom: { bool ok = true; double factor = option.toDouble( &ok ); if ( ok && factor > 0.0 ) { d->zoomFactor = static_cast< float >( factor ); updateZoom( ZoomRefreshCurrent ); } break; } case ZoomModality: { bool ok = true; int mode = option.toInt( &ok ); if ( ok ) { if ( mode >= 0 && mode < 3 ) updateZoom( (ZoomMode)mode ); } break; } case ViewModeModality: { bool ok = true; int mode = option.toInt( &ok ); if ( ok ) { if ( mode >= 0 && mode < Okular::Settings::EnumViewMode::COUNT) updateViewMode(mode); } break; } case Continuous: { bool mode = option.toBool( ); d->aViewContinuous->setChecked(mode); slotContinuousToggled(mode); break; } case TrimMargins: { bool value = option.toBool( ); d->aTrimMargins->setChecked(value); slotTrimMarginsToggled(value); break; } } } //END View inherited methods //BEGIN widget events bool PageView::event( QEvent * event ) { if ( event->type() == QEvent::Gesture ) { return gestureEvent(static_cast( event )); } // do not stop the event return QAbstractScrollArea::event( event ); } bool PageView::gestureEvent( QGestureEvent * event ) { QPinchGesture *pinch = static_cast(event->gesture(Qt::PinchGesture)); if (pinch) { // Viewport zoom level at the moment where the pinch gesture starts. // The viewport zoom level _during_ the gesture will be this value // times the relative zoom reported by QGestureEvent. static qreal vanillaZoom = d->zoomFactor; if (pinch->state() == Qt::GestureStarted) { vanillaZoom = d->zoomFactor; } const QPinchGesture::ChangeFlags changeFlags = pinch->changeFlags(); // Zoom if (pinch->changeFlags() & QPinchGesture::ScaleFactorChanged) { d->zoomFactor = vanillaZoom * pinch->totalScaleFactor(); d->blockPixmapsRequest = true; updateZoom( ZoomRefreshCurrent ); d->blockPixmapsRequest = false; viewport()->update(); } // Count the number of 90-degree rotations we did since the start of the pinch gesture. // Otherwise a pinch turned to 90 degrees and held there will rotate the page again and again. static int rotations = 0; if (changeFlags & QPinchGesture::RotationAngleChanged) { // Rotation angle relative to the accumulated page rotations triggered by the current pinch // We actually turn at 80 degrees rather than at 90 degrees. That's less strain on the hands. const qreal relativeAngle = pinch->rotationAngle() - rotations*90; if (relativeAngle > 80) { slotRotateClockwise(); rotations++; } if (relativeAngle < -80) { slotRotateCounterClockwise(); rotations--; } } if (pinch->state() == Qt::GestureFinished) { rotations = 0; } return true; } return false; } void PageView::paintEvent(QPaintEvent *pe) { const QPoint areaPos = contentAreaPosition(); // create the rect into contents from the clipped screen rect QRect viewportRect = viewport()->rect(); viewportRect.translate( areaPos ); QRect contentsRect = pe->rect().translated( areaPos ).intersected( viewportRect ); if ( !contentsRect.isValid() ) return; #ifdef PAGEVIEW_DEBUG qCDebug(OkularUiDebug) << "paintevent" << contentsRect; #endif // create the screen painter. a pixel painted at contentsX,contentsY // appears to the top-left corner of the scrollview. QPainter screenPainter( viewport() ); // translate to simulate the scrolled content widget screenPainter.translate( -areaPos ); // selectionRect is the normalized mouse selection rect QRect selectionRect = d->mouseSelectionRect; if ( !selectionRect.isNull() ) selectionRect = selectionRect.normalized(); // selectionRectInternal without the border QRect selectionRectInternal = selectionRect; selectionRectInternal.adjust( 1, 1, -1, -1 ); // color for blending QColor selBlendColor = (selectionRect.width() > 8 || selectionRect.height() > 8) ? d->mouseSelectionColor : Qt::red; // subdivide region into rects QRegion rgn = pe->region(); // preprocess rects area to see if it worths or not using subdivision uint summedArea = 0; for ( const QRect & r : rgn ) { summedArea += r.width() * r.height(); } // very elementary check: SUMj(Region[j].area) is less than boundingRect.area const bool useSubdivision = summedArea < (0.6 * contentsRect.width() * contentsRect.height()); if ( !useSubdivision ) { rgn = contentsRect; } // iterate over the rects (only one loop if not using subdivision) for ( const QRect & r : rgn ) { if ( useSubdivision ) { // set 'contentsRect' to a part of the sub-divided region contentsRect = r.translated( areaPos ).intersected( viewportRect ); if ( !contentsRect.isValid() ) continue; } #ifdef PAGEVIEW_DEBUG qCDebug(OkularUiDebug) << contentsRect; #endif // note: this check will take care of all things requiring alpha blending (not only selection) bool wantCompositing = !selectionRect.isNull() && contentsRect.intersects( selectionRect ); // also alpha-blend when there is a table selection... wantCompositing |= !d->tableSelectionParts.isEmpty(); if ( wantCompositing && Okular::Settings::enableCompositing() ) { // create pixmap and open a painter over it (contents{left,top} becomes pixmap {0,0}) QPixmap doubleBuffer( contentsRect.size() * devicePixelRatioF() ); doubleBuffer.setDevicePixelRatio(devicePixelRatioF()); QPainter pixmapPainter( &doubleBuffer ); pixmapPainter.translate( -contentsRect.left(), -contentsRect.top() ); // 1) Layer 0: paint items and clear bg on unpainted rects drawDocumentOnPainter( contentsRect, &pixmapPainter ); // 2a) Layer 1a: paint (blend) transparent selection (rectangle) if ( !selectionRect.isNull() && selectionRect.intersects( contentsRect ) && !selectionRectInternal.contains( contentsRect ) ) { QRect blendRect = selectionRectInternal.intersected( contentsRect ); // skip rectangles covered by the selection's border if ( blendRect.isValid() ) { // grab current pixmap into a new one to colorize contents QPixmap blendedPixmap( blendRect.width() * devicePixelRatioF(), blendRect.height() * devicePixelRatioF() ); blendedPixmap.setDevicePixelRatio(devicePixelRatioF()); QPainter p( &blendedPixmap ); p.drawPixmap( 0, 0, doubleBuffer, (blendRect.left() - contentsRect.left()) * devicePixelRatioF(), (blendRect.top() - contentsRect.top()) * devicePixelRatioF(), blendRect.width() * devicePixelRatioF(), blendRect.height() * devicePixelRatioF() ); QColor blCol = selBlendColor.darker( 140 ); blCol.setAlphaF( 0.2 ); p.fillRect( blendedPixmap.rect(), blCol ); p.end(); // copy the blended pixmap back to its place pixmapPainter.drawPixmap( blendRect.left(), blendRect.top(), blendedPixmap ); } // draw border (red if the selection is too small) pixmapPainter.setPen( selBlendColor ); pixmapPainter.drawRect( selectionRect.adjusted( 0, 0, -1, -1 ) ); } // 2b) Layer 1b: paint (blend) transparent selection (table) for (const TableSelectionPart &tsp : qAsConst(d->tableSelectionParts)) { QRect selectionPartRect = tsp.rectInItem.geometry(tsp.item->uncroppedWidth(), tsp.item->uncroppedHeight()); selectionPartRect.translate( tsp.item->uncroppedGeometry().topLeft () ); QRect selectionPartRectInternal = selectionPartRect; selectionPartRectInternal.adjust( 1, 1, -1, -1 ); if ( !selectionPartRect.isNull() && selectionPartRect.intersects( contentsRect ) && !selectionPartRectInternal.contains( contentsRect ) ) { QRect blendRect = selectionPartRectInternal.intersected( contentsRect ); // skip rectangles covered by the selection's border if ( blendRect.isValid() ) { // grab current pixmap into a new one to colorize contents QPixmap blendedPixmap( blendRect.width() * devicePixelRatioF(), blendRect.height() * devicePixelRatioF() ); blendedPixmap.setDevicePixelRatio(devicePixelRatioF()); QPainter p( &blendedPixmap ); p.drawPixmap( 0, 0, doubleBuffer, (blendRect.left() - contentsRect.left()) * devicePixelRatioF(), (blendRect.top() - contentsRect.top()) * devicePixelRatioF(), blendRect.width() * devicePixelRatioF(), blendRect.height() * devicePixelRatioF() ); QColor blCol = d->mouseSelectionColor.darker( 140 ); blCol.setAlphaF( 0.2 ); p.fillRect( blendedPixmap.rect(), blCol ); p.end(); // copy the blended pixmap back to its place pixmapPainter.drawPixmap( blendRect.left(), blendRect.top(), blendedPixmap ); } // draw border (red if the selection is too small) pixmapPainter.setPen( d->mouseSelectionColor ); pixmapPainter.drawRect( selectionPartRect.adjusted( 0, 0, -1, -1 ) ); } } drawTableDividers( &pixmapPainter ); // 3a) Layer 1: give annotator painting control if ( d->annotator && d->annotator->routePaints( contentsRect ) ) d->annotator->routePaint( &pixmapPainter, contentsRect ); // 3b) Layer 1: give mouseAnnotation painting control d->mouseAnnotation->routePaint( &pixmapPainter, contentsRect ); // 4) Layer 2: overlays if ( Okular::Settings::debugDrawBoundaries() ) { pixmapPainter.setPen( Qt::blue ); pixmapPainter.drawRect( contentsRect ); } // finish painting and draw contents pixmapPainter.end(); screenPainter.drawPixmap( contentsRect.left(), contentsRect.top(), doubleBuffer ); } else { // 1) Layer 0: paint items and clear bg on unpainted rects drawDocumentOnPainter( contentsRect, &screenPainter ); // 2a) Layer 1a: paint opaque selection (rectangle) if ( !selectionRect.isNull() && selectionRect.intersects( contentsRect ) && !selectionRectInternal.contains( contentsRect ) ) { screenPainter.setPen( palette().color( QPalette::Active, QPalette::Highlight ).darker(110) ); screenPainter.drawRect( selectionRect ); } // 2b) Layer 1b: paint opaque selection (table) for (const TableSelectionPart &tsp : qAsConst(d->tableSelectionParts)) { QRect selectionPartRect = tsp.rectInItem.geometry(tsp.item->uncroppedWidth(), tsp.item->uncroppedHeight()); selectionPartRect.translate( tsp.item->uncroppedGeometry().topLeft () ); QRect selectionPartRectInternal = selectionPartRect; selectionPartRectInternal.adjust( 1, 1, -1, -1 ); if ( !selectionPartRect.isNull() && selectionPartRect.intersects( contentsRect ) && !selectionPartRectInternal.contains( contentsRect ) ) { screenPainter.setPen( palette().color( QPalette::Active, QPalette::Highlight ).darker(110) ); screenPainter.drawRect( selectionPartRect ); } } drawTableDividers( &screenPainter ); // 3a) Layer 1: give annotator painting control if ( d->annotator && d->annotator->routePaints( contentsRect ) ) d->annotator->routePaint( &screenPainter, contentsRect ); // 3b) Layer 1: give mouseAnnotation painting control d->mouseAnnotation->routePaint( &screenPainter, contentsRect ); // 4) Layer 2: overlays if ( Okular::Settings::debugDrawBoundaries() ) { screenPainter.setPen( Qt::red ); screenPainter.drawRect( contentsRect ); } } } } void PageView::drawTableDividers(QPainter * screenPainter) { if (!d->tableSelectionParts.isEmpty()) { screenPainter->setPen( d->mouseSelectionColor.darker() ); if (d->tableDividersGuessed) { QPen p = screenPainter->pen(); p.setStyle( Qt::DashLine ); screenPainter->setPen( p ); } for (const TableSelectionPart &tsp : qAsConst(d->tableSelectionParts)) { QRect selectionPartRect = tsp.rectInItem.geometry(tsp.item->uncroppedWidth(), tsp.item->uncroppedHeight()); selectionPartRect.translate( tsp.item->uncroppedGeometry().topLeft () ); QRect selectionPartRectInternal = selectionPartRect; selectionPartRectInternal.adjust( 1, 1, -1, -1 ); for (double col : qAsConst(d->tableSelectionCols)) { if (col >= tsp.rectInSelection.left && col <= tsp.rectInSelection.right) { col = (col - tsp.rectInSelection.left) / (tsp.rectInSelection.right - tsp.rectInSelection.left); const int x = selectionPartRect.left() + col * selectionPartRect.width() + 0.5; screenPainter->drawLine( x, selectionPartRectInternal.top(), x, selectionPartRectInternal.top() + selectionPartRectInternal.height() ); } } for (double row : qAsConst(d->tableSelectionRows)) { if (row >= tsp.rectInSelection.top && row <= tsp.rectInSelection.bottom) { row = (row - tsp.rectInSelection.top) / (tsp.rectInSelection.bottom - tsp.rectInSelection.top); const int y = selectionPartRect.top() + row * selectionPartRect.height() + 0.5; screenPainter->drawLine( selectionPartRectInternal.left(), y, selectionPartRectInternal.left() + selectionPartRectInternal.width(), y ); } } } } } void PageView::resizeEvent( QResizeEvent *e ) { if ( d->items.isEmpty() ) { resizeContentArea( e->size() ); return; } if ( ( d->zoomMode == ZoomFitWidth || d->zoomMode == ZoomFitAuto ) && !verticalScrollBar()->isVisible() && qAbs(e->oldSize().height() - e->size().height()) < verticalScrollBar()->width() && d->verticalScrollBarVisible ) { // this saves us from infinite resizing loop because of scrollbars appearing and disappearing // see bug 160628 for more info // TODO looks are still a bit ugly because things are left uncentered // but better a bit ugly than unusable d->verticalScrollBarVisible = false; resizeContentArea( e->size() ); return; } else if ( d->zoomMode == ZoomFitAuto && !horizontalScrollBar()->isVisible() && qAbs(e->oldSize().width() - e->size().width()) < horizontalScrollBar()->height() && d->horizontalScrollBarVisible ) { // this saves us from infinite resizing loop because of scrollbars appearing and disappearing // TODO looks are still a bit ugly because things are left uncentered // but better a bit ugly than unusable d->horizontalScrollBarVisible = false; resizeContentArea( e->size() ); return; } // start a timer that will refresh the pixmap after 0.2s d->delayResizeEventTimer->start( 200 ); d->verticalScrollBarVisible = verticalScrollBar()->isVisible(); d->horizontalScrollBarVisible = horizontalScrollBar()->isVisible(); } void PageView::keyPressEvent( QKeyEvent * e ) { e->accept(); // if performing a selection or dyn zooming, disable keys handling if ( ( d->mouseSelecting && e->key() != Qt::Key_Escape ) || ( QApplication::mouseButtons () & Qt::MidButton ) ) return; // move/scroll page by using keys switch ( e->key() ) { case Qt::Key_J: case Qt::Key_Down: slotScrollDown( 1 /* go down 1 step */ ); break; case Qt::Key_PageDown: slotScrollDown(); break; case Qt::Key_K: case Qt::Key_Up: slotScrollUp( 1 /* go up 1 step */ ); break; case Qt::Key_PageUp: case Qt::Key_Backspace: slotScrollUp(); break; case Qt::Key_Left: case Qt::Key_H: if ( horizontalScrollBar()->maximum() == 0 ) { //if we cannot scroll we go to the previous page vertically int next_page = d->document->currentPage() - viewColumns(); d->document->setViewportPage(next_page); } else{ d->scroller->scrollTo(d->scroller->finalPosition() + QPoint(-horizontalScrollBar()->singleStep(), 0), 100); } break; case Qt::Key_Right: case Qt::Key_L: if ( horizontalScrollBar()->maximum() == 0 ) { //if we cannot scroll we advance the page vertically int next_page = d->document->currentPage() + viewColumns(); d->document->setViewportPage(next_page); } else{ d->scroller->scrollTo(d->scroller->finalPosition() + QPoint(horizontalScrollBar()->singleStep(), 0), 100); } break; case Qt::Key_Escape: emit escPressed(); selectionClear( d->tableDividersGuessed ? ClearOnlyDividers : ClearAllSelection ); d->mousePressPos = QPoint(); if ( d->aPrevAction ) { d->aPrevAction->trigger(); d->aPrevAction = nullptr; } d->mouseAnnotation->routeKeyPressEvent( e ); break; case Qt::Key_Delete: d->mouseAnnotation->routeKeyPressEvent( e ); break; case Qt::Key_Shift: case Qt::Key_Control: if ( d->autoScrollTimer ) { if ( d->autoScrollTimer->isActive() ) d->autoScrollTimer->stop(); else slotAutoScroll(); return; } // fallthrough default: e->ignore(); return; } // if a known key has been pressed, stop scrolling the page if ( d->autoScrollTimer ) { d->scrollIncrement = 0; d->autoScrollTimer->stop(); } } void PageView::keyReleaseEvent( QKeyEvent * e ) { e->accept(); if ( d->annotator && d->annotator->active() ) { if ( d->annotator->routeKeyEvent( e ) ) return; } if ( e->key() == Qt::Key_Escape && d->autoScrollTimer ) { d->scrollIncrement = 0; d->autoScrollTimer->stop(); } } void PageView::inputMethodEvent( QInputMethodEvent * e ) { Q_UNUSED(e) } void PageView::tabletEvent( QTabletEvent * e ) { // Ignore tablet events that we don't care about if ( !( e->type() == QEvent::TabletPress || e->type() == QEvent::TabletRelease || e->type() == QEvent::TabletMove ) ) { e->ignore(); return; } // Determine pen state bool penReleased = false; if ( e->type() == QEvent::TabletPress ) { d->penDown = true; } if ( e->type() == QEvent::TabletRelease ) { d->penDown = false; penReleased = true; } // If we're editing an annotation and the tablet pen is either down or just released // then dispatch event to annotator if ( d->annotator && d->annotator->active() && ( d->penDown || penReleased ) ) { const QPoint eventPos = contentAreaPoint( e->pos() ); PageViewItem * pageItem = pickItemOnPoint( eventPos.x(), eventPos.y() ); const QPoint localOriginInGlobal = mapToGlobal( QPoint(0,0) ); // routeTabletEvent will accept or ignore event as appropriate d->annotator->routeTabletEvent( e, pageItem, localOriginInGlobal ); } else { e->ignore(); } } void PageView::mouseMoveEvent( QMouseEvent * e ) { // For some reason in Qt 5.11.2 (no idea when this started) all wheel // events are followed by mouse move events (without changing position), // so we only actually reset the controlWheelAccumulatedDelta if there is a mouse movement if ( e->globalPos() != d->previousMouseMovePos ) { d->controlWheelAccumulatedDelta = 0; } d->previousMouseMovePos = e->globalPos(); // don't perform any mouse action when no document is shown if ( d->items.isEmpty() ) return; // if holding mouse mid button, perform zoom if ( e->buttons() & Qt::MidButton ) { int mouseY = e->globalPos().y(); int deltaY = d->mouseMidLastY - mouseY; // wrap mouse from top to bottom const QRect mouseContainer = QApplication::desktop()->screenGeometry( this ); const int absDeltaY = abs(deltaY); if ( absDeltaY > mouseContainer.height() / 2 ) { deltaY = mouseContainer.height() - absDeltaY; } const float upperZoomLimit = d->document->supportsTiles() ? 15.99 : 3.99; if ( mouseY <= mouseContainer.top() + 4 && d->zoomFactor < upperZoomLimit ) { mouseY = mouseContainer.bottom() - 5; QCursor::setPos( e->globalPos().x(), mouseY ); } // wrap mouse from bottom to top else if ( mouseY >= mouseContainer.bottom() - 4 && d->zoomFactor > 0.101 ) { mouseY = mouseContainer.top() + 5; QCursor::setPos( e->globalPos().x(), mouseY ); } // remember last position d->mouseMidLastY = mouseY; // update zoom level, perform zoom and redraw if ( deltaY ) { d->zoomFactor *= ( 1.0 + ( (double)deltaY / 500.0 ) ); d->blockPixmapsRequest = true; updateZoom( ZoomRefreshCurrent ); d->blockPixmapsRequest = false; viewport()->update(); } return; } const QPoint eventPos = contentAreaPoint( e->pos() ); // if we're editing an annotation, dispatch event to it if ( d->annotator && d->annotator->active() ) { PageViewItem * pageItem = pickItemOnPoint( eventPos.x(), eventPos.y() ); updateCursor( eventPos ); d->annotator->routeMouseEvent( e, pageItem ); return; } bool leftButton = (e->buttons() == Qt::LeftButton); bool rightButton = (e->buttons() == Qt::RightButton); switch ( d->mouseMode ) { case Okular::Settings::EnumMouseMode::Browse: { PageViewItem * pageItem = pickItemOnPoint( eventPos.x(), eventPos.y() ); if ( leftButton ) { d->leftClickTimer.stop(); if ( pageItem && d->mouseAnnotation->isActive() ) { // if left button pressed and annotation is focused, forward move event d->mouseAnnotation->routeMouseMoveEvent( pageItem, eventPos, leftButton ); } // drag page else { if(d->scroller->state() == QScroller::Inactive || d->scroller->state() == QScroller::Scrolling) { d->mouseGrabOffset = QPoint(0,0); d->scroller->handleInput(QScroller::InputPress, e->pos(), e->timestamp()-1); } setCursor( Qt::ClosedHandCursor ); QPoint mousePos = e->globalPos(); const QRect mouseContainer = QApplication::desktop()->screenGeometry( this ); // wrap mouse from top to bottom if ( mousePos.y() <= mouseContainer.top() + 4 && verticalScrollBar()->value() < verticalScrollBar()->maximum() - 10 ) { mousePos.setY( mouseContainer.bottom() - 5 ); QCursor::setPos( mousePos ); d->mouseGrabOffset -= QPoint(0, mouseContainer.height()); } // wrap mouse from bottom to top else if ( mousePos.y() >= mouseContainer.bottom() - 4 && verticalScrollBar()->value() > 10 ) { mousePos.setY( mouseContainer.top() + 5 ); d->mouseGrabOffset += QPoint(0, mouseContainer.height()); QCursor::setPos( mousePos ); } d->scroller->handleInput(QScroller::InputMove, e->pos() + d->mouseGrabOffset, e->timestamp()); } } else if ( rightButton && !d->mousePressPos.isNull() && d->aMouseSelect ) { // if mouse moves 5 px away from the press point, switch to 'selection' int deltaX = d->mousePressPos.x() - e->globalPos().x(), deltaY = d->mousePressPos.y() - e->globalPos().y(); if ( deltaX > 5 || deltaX < -5 || deltaY > 5 || deltaY < -5 ) { d->aPrevAction = d->aMouseNormal; d->aMouseSelect->trigger(); QPoint newPos = eventPos + QPoint( deltaX, deltaY ); selectionStart( newPos, palette().color( QPalette::Active, QPalette::Highlight ).lighter( 120 ), false ); updateSelection( eventPos ); break; } } else { /* Forward move events which are still not yet consumed by "mouse grab" or aMouseSelect */ d->mouseAnnotation->routeMouseMoveEvent( pageItem, eventPos, leftButton ); updateCursor(); } } break; case Okular::Settings::EnumMouseMode::Zoom: case Okular::Settings::EnumMouseMode::RectSelect: case Okular::Settings::EnumMouseMode::TableSelect: case Okular::Settings::EnumMouseMode::TrimSelect: // set second corner of selection if ( d->mouseSelecting ) { updateSelection( eventPos ); d->mouseOverLinkObject = nullptr; } updateCursor(); break; case Okular::Settings::EnumMouseMode::Magnifier: if ( e->buttons() ) // if any button is pressed at all { moveMagnifier( e->pos() ); updateMagnifier( eventPos ); } break; case Okular::Settings::EnumMouseMode::TextSelect: // if mouse moves 5 px away from the press point and the document supports text extraction, do 'textselection' if ( !d->mouseTextSelecting && !d->mousePressPos.isNull() && d->document->supportsSearching() && ( ( eventPos - d->mouseSelectPos ).manhattanLength() > 5 ) ) { d->mouseTextSelecting = true; } updateSelection( eventPos ); updateCursor(); break; } } void PageView::mousePressEvent( QMouseEvent * e ) { d->controlWheelAccumulatedDelta = 0; // don't perform any mouse action when no document is shown if ( d->items.isEmpty() ) return; // if performing a selection or dyn zooming, disable mouse press if ( d->mouseSelecting || ( e->button() != Qt::MidButton && ( e->buttons() & Qt::MidButton) )) return; // if the page is scrolling, stop it if ( d->autoScrollTimer ) { d->scrollIncrement = 0; d->autoScrollTimer->stop(); } // if pressing mid mouse button while not doing other things, begin 'continuous zoom' mode if ( e->button() == Qt::MidButton ) { d->mouseMidLastY = e->globalPos().y(); setCursor( Qt::SizeVerCursor ); return; } const QPoint eventPos = contentAreaPoint( e->pos() ); // if we're editing an annotation, dispatch event to it if ( d->annotator && d->annotator->active() ) { d->scroller->stop(); PageViewItem * pageItem = pickItemOnPoint( eventPos.x(), eventPos.y() ); d->annotator->routeMouseEvent( e, pageItem ); return; } // trigger history navigation for additional mouse buttons if ( e->button() == Qt::XButton1 ) { emit mouseBackButtonClick(); return; } if ( e->button() == Qt::XButton2 ) { emit mouseForwardButtonClick(); return; } // update press / 'start drag' mouse position d->mousePressPos = e->globalPos(); // handle mode dependent mouse press actions bool leftButton = e->button() == Qt::LeftButton, rightButton = e->button() == Qt::RightButton; // Not sure we should erase the selection when clicking with left. if ( d->mouseMode != Okular::Settings::EnumMouseMode::TextSelect ) textSelectionClear(); switch ( d->mouseMode ) { case Okular::Settings::EnumMouseMode::Browse: // drag start / click / link following { PageViewItem * pageItem = pickItemOnPoint( eventPos.x(), eventPos.y() ); if ( leftButton ) { if ( pageItem ) { d->mouseAnnotation->routeMousePressEvent( pageItem, eventPos ); } if( !d->mouseOnRect ){ d->mouseGrabOffset = QPoint(0,0); d->scroller->handleInput(QScroller::InputPress, e->pos(), e->timestamp()); d->leftClickTimer.start( QApplication::doubleClickInterval() + 10 ); } } else if ( rightButton ) { if ( pageItem ) { // find out normalized mouse coords inside current item const QRect & itemRect = pageItem->uncroppedGeometry(); double nX = pageItem->absToPageX(eventPos.x()); double nY = pageItem->absToPageY(eventPos.y()); const QLinkedList< const Okular::ObjectRect *> orects = pageItem->page()->objectRects( Okular::ObjectRect::OAnnotation, nX, nY, itemRect.width(), itemRect.height() ); if ( !orects.isEmpty() ) { AnnotationPopup popup( d->document, AnnotationPopup::MultiAnnotationMode, this ); for ( const Okular::ObjectRect * orect : orects ) { Okular::Annotation * ann = ( (Okular::AnnotationObjectRect *)orect )->annotation(); if ( ann && (ann->subType() != Okular::Annotation::AWidget) ) popup.addAnnotation( ann, pageItem->pageNumber() ); } connect( &popup, &AnnotationPopup::openAnnotationWindow, this, &PageView::openAnnotationWindow ); popup.exec( e->globalPos() ); // Since ↑ spins its own event loop we won't get the mouse release event // so reset mousePressPos here d->mousePressPos = QPoint(); } } } } break; case Okular::Settings::EnumMouseMode::Zoom: // set first corner of the zoom rect if ( leftButton ) selectionStart( eventPos, palette().color( QPalette::Active, QPalette::Highlight ), false ); else if ( rightButton ) updateZoom( ZoomOut ); break; case Okular::Settings::EnumMouseMode::Magnifier: moveMagnifier( e->pos() ); d->magnifierView->show(); updateMagnifier( eventPos ); break; case Okular::Settings::EnumMouseMode::RectSelect: // set first corner of the selection rect case Okular::Settings::EnumMouseMode::TrimSelect: if ( leftButton ) { selectionStart( eventPos, palette().color( QPalette::Active, QPalette::Highlight ).lighter( 120 ), false ); } break; case Okular::Settings::EnumMouseMode::TableSelect: if ( leftButton ) { if (d->tableSelectionParts.isEmpty()) { selectionStart( eventPos, palette().color( QPalette::Active, QPalette::Highlight ).lighter( 120 ), false ); } else { QRect updatedRect; for (const TableSelectionPart &tsp : qAsConst(d->tableSelectionParts)) { QRect selectionPartRect = tsp.rectInItem.geometry(tsp.item->uncroppedWidth(), tsp.item->uncroppedHeight()); selectionPartRect.translate( tsp.item->uncroppedGeometry().topLeft () ); // This will update the whole table rather than just the added/removed divider // (which can span more than one part). updatedRect = updatedRect.united(selectionPartRect); if (!selectionPartRect.contains(eventPos)) continue; // At this point it's clear we're either adding or removing a divider manually, so obviously the user is happy with the guess (if any). d->tableDividersGuessed = false; // There's probably a neat trick to finding which edge it's closest to, // but this way has the advantage of simplicity. const int fromLeft = abs(selectionPartRect.left() - eventPos.x()); const int fromRight = abs(selectionPartRect.left() + selectionPartRect.width() - eventPos.x()); const int fromTop = abs(selectionPartRect.top() - eventPos.y()); const int fromBottom = abs(selectionPartRect.top() + selectionPartRect.height() - eventPos.y()); const int colScore = fromToptableSelectionCols.length(); i++) { const double col = (d->tableSelectionCols[i] - tsp.rectInSelection.left) / (tsp.rectInSelection.right - tsp.rectInSelection.left); const int colX = selectionPartRect.left() + col * selectionPartRect.width() + 0.5; if (abs(colX - eventPos.x())<=3) { d->tableSelectionCols.removeAt(i); deleted=true; break; } } if (!deleted) { double col = eventPos.x() - selectionPartRect.left(); col /= selectionPartRect.width(); // at this point, it's normalised within the part col *= (tsp.rectInSelection.right - tsp.rectInSelection.left); col += tsp.rectInSelection.left; // at this point, it's normalised within the whole table d->tableSelectionCols.append(col); std::sort(d->tableSelectionCols.begin(), d->tableSelectionCols.end()); } } else { bool deleted=false; for(int i=0; itableSelectionRows.length(); i++) { const double row = (d->tableSelectionRows[i] - tsp.rectInSelection.top) / (tsp.rectInSelection.bottom - tsp.rectInSelection.top); const int rowY = selectionPartRect.top() + row * selectionPartRect.height() + 0.5; if (abs(rowY - eventPos.y())<=3) { d->tableSelectionRows.removeAt(i); deleted=true; break; } } if (!deleted) { double row = eventPos.y() - selectionPartRect.top(); row /= selectionPartRect.height(); // at this point, it's normalised within the part row *= (tsp.rectInSelection.bottom - tsp.rectInSelection.top); row += tsp.rectInSelection.top; // at this point, it's normalised within the whole table d->tableSelectionRows.append(row); std::sort(d->tableSelectionRows.begin(), d->tableSelectionRows.end()); } } } updatedRect.translate( -contentAreaPosition() ); viewport()->update( updatedRect ); } } break; case Okular::Settings::EnumMouseMode::TextSelect: d->mouseSelectPos = eventPos; if ( !rightButton ) { textSelectionClear(); } break; } } void PageView::mouseReleaseEvent( QMouseEvent * e ) { d->controlWheelAccumulatedDelta = 0; // stop the drag scrolling d->dragScrollTimer.stop(); d->leftClickTimer.stop(); const bool leftButton = e->button() == Qt::LeftButton; const bool rightButton = e->button() == Qt::RightButton; if ( d->mouseAnnotation->isActive() && leftButton ) { // Just finished to move the annotation d->mouseAnnotation->routeMouseReleaseEvent(); } // don't perform any mouse action when no document is shown.. if ( d->items.isEmpty() ) { // ..except for right Clicks (emitted even it viewport is empty) if ( e->button() == Qt::RightButton ) emit rightClick( nullptr, e->globalPos() ); return; } const QPoint eventPos = contentAreaPoint( e->pos() ); // handle mode independent mid bottom zoom if ( e->button() == Qt::MidButton ) { // request pixmaps since it was disabled during drag slotRequestVisiblePixmaps(); // the cursor may now be over a link.. update it updateCursor( eventPos ); return; } // if we're editing an annotation, dispatch event to it if ( d->annotator && d->annotator->active() ) { PageViewItem * pageItem = pickItemOnPoint( eventPos.x(), eventPos.y() ); d->annotator->routeMouseEvent( e, pageItem ); return; } switch ( d->mouseMode ) { case Okular::Settings::EnumMouseMode::Browse:{ d->scroller->handleInput(QScroller::InputRelease, e->pos() + d->mouseGrabOffset, e->timestamp()); //disable flick if the cursor has wrapped around if(d->mouseGrabOffset != QPoint(0,0)) d->scroller->stop(); // return the cursor to its normal state after dragging if ( cursor().shape() == Qt::ClosedHandCursor ) updateCursor( eventPos ); PageViewItem * pageItem = pickItemOnPoint( eventPos.x(), eventPos.y() ); const QPoint pressPos = contentAreaPoint( mapFromGlobal( d->mousePressPos ) ); const PageViewItem * pageItemPressPos = pickItemOnPoint( pressPos.x(), pressPos.y() ); // if the mouse has not moved since the press, that's a -click- if ( leftButton && pageItem && pageItem == pageItemPressPos && ( (d->mousePressPos - e->globalPos()).manhattanLength() < QApplication::startDragDistance() ) ) { if ( !mouseReleaseOverLink( d->mouseOverLinkObject ) && ( e->modifiers() == Qt::ShiftModifier ) ) { const double nX = pageItem->absToPageX(eventPos.x()); const double nY = pageItem->absToPageY(eventPos.y()); const Okular::ObjectRect * rect; // TODO: find a better way to activate the source reference "links" // for the moment they are activated with Shift + left click // Search the nearest source reference. rect = pageItem->page()->objectRect( Okular::ObjectRect::SourceRef, nX, nY, pageItem->uncroppedWidth(), pageItem->uncroppedHeight() ); if ( !rect ) { static const double s_minDistance = 0.025; // FIXME?: empirical value? double distance = 0.0; rect = pageItem->page()->nearestObjectRect( Okular::ObjectRect::SourceRef, nX, nY, pageItem->uncroppedWidth(), pageItem->uncroppedHeight(), &distance ); // distance is distanceSqr, adapt it to a normalized value distance = distance / (pow( pageItem->uncroppedWidth(), 2 ) + pow( pageItem->uncroppedHeight(), 2 )); if ( rect && ( distance > s_minDistance ) ) rect = nullptr; } if ( rect ) { const Okular::SourceReference * ref = static_cast< const Okular::SourceReference * >( rect->object() ); d->document->processSourceReference( ref ); } else { const Okular::SourceReference * ref = d->document->dynamicSourceReference( pageItem-> pageNumber(), nX * pageItem->page()->width(), nY * pageItem->page()->height() ); if ( ref ) { d->document->processSourceReference( ref ); delete ref; } } } #if 0 else { // a link can move us to another page or even to another document, there's no point in trying to // process the click on the image once we have processes the click on the link rect = pageItem->page()->objectRect( Okular::ObjectRect::Image, nX, nY, pageItem->width(), pageItem->height() ); if ( rect ) { // handle click over a image } /* Enrico and me have decided this is not worth the trouble it generates else { // if not on a rect, the click selects the page // if ( pageItem->pageNumber() != (int)d->document->currentPage() ) d->document->setViewportPage( pageItem->pageNumber(), this ); }*/ } #endif } else if ( rightButton && !d->mouseAnnotation->isModified() ) { if ( pageItem && pageItem == pageItemPressPos && ( (d->mousePressPos - e->globalPos()).manhattanLength() < QApplication::startDragDistance() ) ) { QMenu * menu = createProcessLinkMenu(pageItem, eventPos ); if ( menu ) { menu->exec( e->globalPos() ); menu->deleteLater(); } else { const double nX = pageItem->absToPageX(eventPos.x()); const double nY = pageItem->absToPageY(eventPos.y()); // a link can move us to another page or even to another document, there's no point in trying to // process the click on the image once we have processes the click on the link const Okular::ObjectRect * rect = pageItem->page()->objectRect( Okular::ObjectRect::Image, nX, nY, pageItem->uncroppedWidth(), pageItem->uncroppedHeight() ); if ( rect ) { // handle right click over a image } else { // right click (if not within 5 px of the press point, the mode // had been already changed to 'Selection' instead of 'Normal') emit rightClick( pageItem->page(), e->globalPos() ); } } } else { // right click (if not within 5 px of the press point, the mode // had been already changed to 'Selection' instead of 'Normal') emit rightClick( pageItem ? pageItem->page() : nullptr, e->globalPos() ); } } }break; case Okular::Settings::EnumMouseMode::Zoom: // if a selection rect has been defined, zoom into it if ( leftButton && d->mouseSelecting ) { QRect selRect = d->mouseSelectionRect.normalized(); if ( selRect.width() <= 8 && selRect.height() <= 8 ) { selectionClear(); break; } // find out new zoom ratio and normalized view center (relative to the contentsRect) double zoom = qMin( (double)viewport()->width() / (double)selRect.width(), (double)viewport()->height() / (double)selRect.height() ); double nX = (double)(selRect.left() + selRect.right()) / (2.0 * (double)contentAreaWidth()); double nY = (double)(selRect.top() + selRect.bottom()) / (2.0 * (double)contentAreaHeight()); const float upperZoomLimit = d->document->supportsTiles() ? 16.0 : 4.0; if ( d->zoomFactor <= upperZoomLimit || zoom <= 1.0 ) { d->zoomFactor *= zoom; viewport()->setUpdatesEnabled( false ); updateZoom( ZoomRefreshCurrent ); viewport()->setUpdatesEnabled( true ); } // recenter view and update the viewport center( (int)(nX * contentAreaWidth()), (int)(nY * contentAreaHeight()) ); viewport()->update(); // hide message box and delete overlay window selectionClear(); } break; case Okular::Settings::EnumMouseMode::Magnifier: d->magnifierView->hide(); break; case Okular::Settings::EnumMouseMode::TrimSelect: { // if it is a left release checks if is over a previous link press if ( leftButton && mouseReleaseOverLink ( d->mouseOverLinkObject ) ) { selectionClear(); break; } // if mouse is released and selection is null this is a rightClick if ( rightButton && !d->mouseSelecting ) { break; } PageViewItem * pageItem = pickItemOnPoint(eventPos.x(), eventPos.y()); // ensure end point rests within a page, or ignore if (!pageItem) { break; } QRect selectionRect = d->mouseSelectionRect.normalized(); double nLeft = pageItem->absToPageX(selectionRect.left()); double nRight = pageItem->absToPageX(selectionRect.right()); double nTop = pageItem->absToPageY(selectionRect.top()); double nBottom = pageItem->absToPageY(selectionRect.bottom()); if ( nLeft < 0 ) nLeft = 0; if ( nTop < 0 ) nTop = 0; if ( nRight > 1 ) nRight = 1; if ( nBottom > 1 ) nBottom = 1; d->trimBoundingBox = Okular::NormalizedRect(nLeft, nTop, nRight, nBottom); // Trim Selection successfully done, hide prompt d->messageWindow->hide(); // clear widget selection and invalidate rect selectionClear(); // When Trim selection bbox interaction is over, we should switch to another mousemode. if ( d->aPrevAction ) { d->aPrevAction->trigger(); d->aPrevAction = nullptr; } else { d->aMouseNormal->trigger(); } // with d->trimBoundingBox defined, redraw for trim to take visual effect if ( d->document->pages() > 0 ) { slotRelayoutPages(); slotRequestVisiblePixmaps(); // TODO: slotRelayoutPages() may have done this already! } break; } case Okular::Settings::EnumMouseMode::RectSelect: { // if it is a left release checks if is over a previous link press if ( leftButton && mouseReleaseOverLink ( d->mouseOverLinkObject ) ) { selectionClear(); break; } // if mouse is released and selection is null this is a rightClick if ( rightButton && !d->mouseSelecting ) { PageViewItem * pageItem = pickItemOnPoint( eventPos.x(), eventPos.y() ); emit rightClick( pageItem ? pageItem->page() : nullptr, e->globalPos() ); break; } // if a selection is defined, display a popup if ( (!leftButton && !d->aPrevAction) || (!rightButton && d->aPrevAction) || !d->mouseSelecting ) break; QRect selectionRect = d->mouseSelectionRect.normalized(); if ( selectionRect.width() <= 8 && selectionRect.height() <= 8 ) { selectionClear(); if ( d->aPrevAction ) { d->aPrevAction->trigger(); d->aPrevAction = nullptr; } break; } // if we support text generation QString selectedText; if (d->document->supportsSearching()) { // grab text in selection by extracting it from all intersected pages const Okular::Page * okularPage=nullptr; for ( const PageViewItem * item : qAsConst( d->items ) ) { if ( !item->isVisible() ) continue; const QRect & itemRect = item->croppedGeometry(); if ( selectionRect.intersects( itemRect ) ) { // request the textpage if there isn't one okularPage= item->page(); qCDebug(OkularUiDebug) << "checking if page" << item->pageNumber() << "has text:" << okularPage->hasTextPage(); if ( !okularPage->hasTextPage() ) d->document->requestTextPage( okularPage->number() ); // grab text in the rect that intersects itemRect QRect relativeRect = selectionRect.intersected( itemRect ); relativeRect.translate( -item->uncroppedGeometry().topLeft() ); Okular::RegularAreaRect rects; rects.append( Okular::NormalizedRect( relativeRect, item->uncroppedWidth(), item->uncroppedHeight() ) ); selectedText += okularPage->text( &rects ); } } } // popup that ask to copy:text and copy/save:image QMenu menu( this ); menu.setObjectName(QStringLiteral("PopupMenu")); QAction *textToClipboard = nullptr; #ifdef HAVE_SPEECH QAction *speakText = nullptr; #endif QAction *imageToClipboard = nullptr; QAction *imageToFile = nullptr; if ( d->document->supportsSearching() && !selectedText.isEmpty() ) { menu.addAction( new OKMenuTitle( &menu, i18np( "Text (1 character)", "Text (%1 characters)", selectedText.length() ) ) ); textToClipboard = menu.addAction( QIcon::fromTheme(QStringLiteral("edit-copy")), i18n( "Copy to Clipboard" ) ); textToClipboard->setObjectName(QStringLiteral("CopyTextToClipboard")); bool copyAllowed = d->document->isAllowed( Okular::AllowCopy ); if ( !copyAllowed ) { textToClipboard->setEnabled( false ); textToClipboard->setText( i18n("Copy forbidden by DRM") ); } #ifdef HAVE_SPEECH if ( Okular::Settings::useTTS() ) speakText = menu.addAction( QIcon::fromTheme(QStringLiteral("text-speak")), i18n( "Speak Text" ) ); #endif if ( copyAllowed ) { addSearchWithinDocumentAction( &menu, selectedText ); addWebShortcutsMenu( &menu, selectedText ); } } menu.addAction( new OKMenuTitle( &menu, i18n( "Image (%1 by %2 pixels)", selectionRect.width(), selectionRect.height() ) ) ); imageToClipboard = menu.addAction( QIcon::fromTheme(QStringLiteral("image-x-generic")), i18n( "Copy to Clipboard" ) ); imageToFile = menu.addAction( QIcon::fromTheme(QStringLiteral("document-save")), i18n( "Save to File..." ) ); QAction *choice = menu.exec( e->globalPos() ); // check if the user really selected an action if ( choice ) { // IMAGE operation chosen if ( choice == imageToClipboard || choice == imageToFile ) { // renders page into a pixmap QPixmap copyPix( selectionRect.width(), selectionRect.height() ); QPainter copyPainter( ©Pix ); copyPainter.translate( -selectionRect.left(), -selectionRect.top() ); drawDocumentOnPainter( selectionRect, ©Painter ); copyPainter.end(); if ( choice == imageToClipboard ) { // [2] copy pixmap to clipboard QClipboard *cb = QApplication::clipboard(); cb->setPixmap( copyPix, QClipboard::Clipboard ); if ( cb->supportsSelection() ) cb->setPixmap( copyPix, QClipboard::Selection ); d->messageWindow->display( i18n( "Image [%1x%2] copied to clipboard.", copyPix.width(), copyPix.height() ) ); } else if ( choice == imageToFile ) { // [3] save pixmap to file QString fileName = QFileDialog::getSaveFileName(this, i18n("Save file"), QString(), i18n("Images (*.png *.jpeg)")); if ( fileName.isEmpty() ) d->messageWindow->display( i18n( "File not saved." ), QString(), PageViewMessage::Warning ); else { QMimeDatabase db; QMimeType mime = db.mimeTypeForUrl( QUrl::fromLocalFile(fileName) ); QString type; if ( !mime.isDefault() ) type = QStringLiteral("PNG"); else type = mime.name().section( QLatin1Char('/'), -1 ).toUpper(); copyPix.save( fileName, qPrintable( type ) ); d->messageWindow->display( i18n( "Image [%1x%2] saved to %3 file.", copyPix.width(), copyPix.height(), type ) ); } } } // TEXT operation chosen else { if ( choice == textToClipboard ) { // [1] copy text to clipboard QClipboard *cb = QApplication::clipboard(); cb->setText( selectedText, QClipboard::Clipboard ); if ( cb->supportsSelection() ) cb->setText( selectedText, QClipboard::Selection ); } #ifdef HAVE_SPEECH else if ( choice == speakText ) { // [2] speech selection using TTS d->tts()->say( selectedText ); } #endif } } // clear widget selection and invalidate rect selectionClear(); // restore previous action if came from it using right button if ( d->aPrevAction ) { d->aPrevAction->trigger(); d->aPrevAction = nullptr; } }break; case Okular::Settings::EnumMouseMode::TableSelect: { // if it is a left release checks if is over a previous link press if ( leftButton && mouseReleaseOverLink ( d->mouseOverLinkObject ) ) { selectionClear(); break; } // if mouse is released and selection is null this is a rightClick if ( rightButton && !d->mouseSelecting ) { PageViewItem * pageItem = pickItemOnPoint( eventPos.x(), eventPos.y() ); emit rightClick( pageItem ? pageItem->page() : nullptr, e->globalPos() ); break; } QRect selectionRect = d->mouseSelectionRect.normalized(); if ( selectionRect.width() <= 8 && selectionRect.height() <= 8 && d->tableSelectionParts.isEmpty() ) { selectionClear(); if ( d->aPrevAction ) { d->aPrevAction->trigger(); d->aPrevAction = nullptr; } break; } if (d->mouseSelecting) { // break up the selection into page-relative pieces d->tableSelectionParts.clear(); const Okular::Page * okularPage=nullptr; for ( PageViewItem * item : qAsConst( d->items ) ) { if ( !item->isVisible() ) continue; const QRect & itemRect = item->croppedGeometry(); if ( selectionRect.intersects( itemRect ) ) { // request the textpage if there isn't one okularPage= item->page(); qCDebug(OkularUiDebug) << "checking if page" << item->pageNumber() << "has text:" << okularPage->hasTextPage(); if ( !okularPage->hasTextPage() ) d->document->requestTextPage( okularPage->number() ); // grab text in the rect that intersects itemRect QRect rectInItem = selectionRect.intersected( itemRect ); rectInItem.translate( -item->uncroppedGeometry().topLeft() ); QRect rectInSelection = selectionRect.intersected( itemRect ); rectInSelection.translate( -selectionRect.topLeft() ); d->tableSelectionParts.append( TableSelectionPart( item, Okular::NormalizedRect( rectInItem, item->uncroppedWidth(), item->uncroppedHeight() ), Okular::NormalizedRect( rectInSelection, selectionRect.width(), selectionRect.height() ) ) ); } } QRect updatedRect = d->mouseSelectionRect.normalized().adjusted( 0, 0, 1, 1 ); updatedRect.translate( -contentAreaPosition() ); d->mouseSelecting = false; d->mouseSelectionRect.setCoords( 0, 0, 0, 0 ); d->tableSelectionCols.clear(); d->tableSelectionRows.clear(); guessTableDividers(); viewport()->update( updatedRect ); } if ( !d->document->isAllowed( Okular::AllowCopy ) ) { d->messageWindow->display( i18n("Copy forbidden by DRM"), QString(), PageViewMessage::Info, -1 ); break; } QString selText; QString selHtml; QList xs = d->tableSelectionCols; QList ys = d->tableSelectionRows; xs.prepend(0.0); xs.append(1.0); ys.prepend(0.0); ys.append(1.0); selHtml = QString::fromLatin1("" "" ""); for (int r=0; r+1"); for (int c=0; c+1tableSelectionParts)) { // first, crop the cell to this part if (!tsp.rectInSelection.intersects(cell)) continue; Okular::NormalizedRect cellPart = tsp.rectInSelection & cell; // intersection // second, convert it from table coordinates to part coordinates cellPart.left -= tsp.rectInSelection.left; cellPart.left /= (tsp.rectInSelection.right - tsp.rectInSelection.left); cellPart.right -= tsp.rectInSelection.left; cellPart.right /= (tsp.rectInSelection.right - tsp.rectInSelection.left); cellPart.top -= tsp.rectInSelection.top; cellPart.top /= (tsp.rectInSelection.bottom - tsp.rectInSelection.top); cellPart.bottom -= tsp.rectInSelection.top; cellPart.bottom /= (tsp.rectInSelection.bottom - tsp.rectInSelection.top); // third, convert from part coordinates to item coordinates cellPart.left *= (tsp.rectInItem.right - tsp.rectInItem.left); cellPart.left += tsp.rectInItem.left; cellPart.right *= (tsp.rectInItem.right - tsp.rectInItem.left); cellPart.right += tsp.rectInItem.left; cellPart.top *= (tsp.rectInItem.bottom - tsp.rectInItem.top); cellPart.top += tsp.rectInItem.top; cellPart.bottom *= (tsp.rectInItem.bottom - tsp.rectInItem.top); cellPart.bottom += tsp.rectInItem.top; // now get the text Okular::RegularAreaRect rects; rects.append( cellPart ); txt += tsp.item->page()->text( &rects, Okular::TextPage::CentralPixelTextAreaInclusionBehaviour ); } QString html = txt; selText += txt.replace(QLatin1Char('\n'), QLatin1Char(' ')); html.replace(QLatin1Char('&'), QLatin1String("&")).replace(QLatin1Char('<'), QLatin1String("<")).replace(QLatin1Char('>'), QLatin1String(">")); // Remove newlines, do not turn them into
, because // Excel interprets
within cell as new cell... html.replace(QLatin1Char('\n'), QLatin1String(" ")); selHtml += QStringLiteral("
"); } selText += QLatin1Char('\n'); selHtml += QLatin1String("\n"); } selHtml += QLatin1String("
") + html + QStringLiteral("
\n"); QClipboard *cb = QApplication::clipboard(); QMimeData *md = new QMimeData(); md->setText(selText); md->setHtml(selHtml); cb->setMimeData( md, QClipboard::Clipboard ); if ( cb->supportsSelection() ) cb->setMimeData( md, QClipboard::Selection ); }break; case Okular::Settings::EnumMouseMode::TextSelect: // if it is a left release checks if is over a previous link press if ( leftButton && mouseReleaseOverLink ( d->mouseOverLinkObject ) ) { selectionClear(); break; } if ( d->mouseTextSelecting ) { d->mouseTextSelecting = false; // textSelectionClear(); if ( d->document->isAllowed( Okular::AllowCopy ) ) { const QString text = d->selectedText(); if ( !text.isEmpty() ) { QClipboard *cb = QApplication::clipboard(); if ( cb->supportsSelection() ) cb->setText( text, QClipboard::Selection ); } } } else if ( !d->mousePressPos.isNull() && rightButton ) { PageViewItem* item = pickItemOnPoint(eventPos.x(),eventPos.y()); const Okular::Page *page; //if there is text selected in the page if (item) { QAction * httpLink = nullptr; QAction * textToClipboard = nullptr; QString url; QMenu * menu = createProcessLinkMenu( item, eventPos ); const bool mouseClickOverLink = (menu != nullptr); #ifdef HAVE_SPEECH QAction *speakText = nullptr; #endif if ( (page = item->page())->textSelection() ) { if ( !menu ) { menu = new QMenu(this); } textToClipboard = menu->addAction( QIcon::fromTheme( QStringLiteral("edit-copy") ), i18n( "Copy Text" ) ); #ifdef HAVE_SPEECH if ( Okular::Settings::useTTS() ) speakText = menu->addAction( QIcon::fromTheme( QStringLiteral("text-speak") ), i18n( "Speak Text" ) ); #endif if ( !d->document->isAllowed( Okular::AllowCopy ) ) { textToClipboard->setEnabled( false ); textToClipboard->setText( i18n("Copy forbidden by DRM") ); } else { addSearchWithinDocumentAction(menu, d->selectedText()); addWebShortcutsMenu( menu, d->selectedText() ); } // if the right-click was over a link add "Follow This link" instead of "Go to" if (!mouseClickOverLink) { url = UrlUtils::getUrl( d->selectedText() ); if ( !url.isEmpty() ) { const QString squeezedText = KStringHandler::rsqueeze( url, linkTextPreviewLength ); httpLink = menu->addAction( i18n( "Go to '%1'", squeezedText ) ); httpLink->setObjectName(QStringLiteral("GoToAction")); } } } if ( menu ) { menu->setObjectName(QStringLiteral("PopupMenu")); QAction *choice = menu->exec( e->globalPos() ); // check if the user really selected an action if ( choice ) { if ( choice == textToClipboard ) copyTextSelection(); #ifdef HAVE_SPEECH else if ( choice == speakText ) { const QString text = d->selectedText(); d->tts()->say( text ); } #endif else if ( choice == httpLink ) { new KRun( QUrl( url ), this ); } } menu->deleteLater(); } } } break; } // reset mouse press / 'drag start' position d->mousePressPos = QPoint(); } void PageView::guessTableDividers() { QList< QPair > colTicks, rowTicks, colSelectionTicks, rowSelectionTicks; for ( const TableSelectionPart& tsp : qAsConst(d->tableSelectionParts) ) { // add ticks for the edges of this area... colSelectionTicks.append( qMakePair( tsp.rectInSelection.left, +1 ) ); colSelectionTicks.append( qMakePair( tsp.rectInSelection.right, -1 ) ); rowSelectionTicks.append( qMakePair( tsp.rectInSelection.top, +1 ) ); rowSelectionTicks.append( qMakePair( tsp.rectInSelection.bottom, -1 ) ); // get the words in this part Okular::RegularAreaRect rects; rects.append( tsp.rectInItem ); const Okular::TextEntity::List words = tsp.item->page()->words( &rects, Okular::TextPage::CentralPixelTextAreaInclusionBehaviour ); for (const Okular::TextEntity *te : words) { if (te->text().isEmpty()) { delete te; continue; } Okular::NormalizedRect wordArea = *te->area(); // convert it from item coordinates to part coordinates wordArea.left -= tsp.rectInItem.left; wordArea.left /= (tsp.rectInItem.right - tsp.rectInItem.left); wordArea.right -= tsp.rectInItem.left; wordArea.right /= (tsp.rectInItem.right - tsp.rectInItem.left); wordArea.top -= tsp.rectInItem.top; wordArea.top /= (tsp.rectInItem.bottom - tsp.rectInItem.top); wordArea.bottom -= tsp.rectInItem.top; wordArea.bottom /= (tsp.rectInItem.bottom - tsp.rectInItem.top); // convert from part coordinates to table coordinates wordArea.left *= (tsp.rectInSelection.right - tsp.rectInSelection.left); wordArea.left += tsp.rectInSelection.left; wordArea.right *= (tsp.rectInSelection.right - tsp.rectInSelection.left); wordArea.right += tsp.rectInSelection.left; wordArea.top *= (tsp.rectInSelection.bottom - tsp.rectInSelection.top); wordArea.top += tsp.rectInSelection.top; wordArea.bottom *= (tsp.rectInSelection.bottom - tsp.rectInSelection.top); wordArea.bottom += tsp.rectInSelection.top; // add to the ticks arrays... colTicks.append( qMakePair( wordArea.left, +1) ); colTicks.append( qMakePair( wordArea.right, -1) ); rowTicks.append( qMakePair( wordArea.top, +1) ); rowTicks.append( qMakePair( wordArea.bottom, -1) ); delete te; } } int tally = 0; std::sort(colSelectionTicks.begin(), colSelectionTicks.end()); std::sort(rowSelectionTicks.begin(), rowSelectionTicks.end()); for (int i = 0; i < colSelectionTicks.length(); ++i) { tally += colSelectionTicks[i].second; if ( tally == 0 && i + 1 < colSelectionTicks.length() && colSelectionTicks[i+1].first != colSelectionTicks[i].first) { colTicks.append( qMakePair( colSelectionTicks[i].first, +1 ) ); colTicks.append( qMakePair( colSelectionTicks[i+1].first, -1 ) ); } } Q_ASSERT( tally == 0 ); for (int i = 0; i < rowSelectionTicks.length(); ++i) { tally += rowSelectionTicks[i].second; if ( tally == 0 && i + 1 < rowSelectionTicks.length() && rowSelectionTicks[i+1].first != rowSelectionTicks[i].first) { rowTicks.append( qMakePair( rowSelectionTicks[i].first, +1 ) ); rowTicks.append( qMakePair( rowSelectionTicks[i+1].first, -1 ) ); } } Q_ASSERT( tally == 0 ); std::sort(colTicks.begin(), colTicks.end()); std::sort(rowTicks.begin(), rowTicks.end()); for (int i = 0; i < colTicks.length(); ++i) { tally += colTicks[i].second; if ( tally == 0 && i + 1 < colTicks.length() && colTicks[i+1].first != colTicks[i].first) { d->tableSelectionCols.append( (colTicks[i].first+colTicks[i+1].first) / 2 ); d->tableDividersGuessed = true; } } Q_ASSERT( tally == 0 ); for (int i = 0; i < rowTicks.length(); ++i) { tally += rowTicks[i].second; if ( tally == 0 && i + 1 < rowTicks.length() && rowTicks[i+1].first != rowTicks[i].first) { d->tableSelectionRows.append( (rowTicks[i].first+rowTicks[i+1].first) / 2 ); d->tableDividersGuessed = true; } } Q_ASSERT( tally == 0 ); } void PageView::mouseDoubleClickEvent( QMouseEvent * e ) { d->controlWheelAccumulatedDelta = 0; if ( e->button() == Qt::LeftButton ) { const QPoint eventPos = contentAreaPoint( e->pos() ); PageViewItem * pageItem = pickItemOnPoint( eventPos.x(), eventPos.y() ); if ( pageItem ) { // find out normalized mouse coords inside current item double nX = pageItem->absToPageX(eventPos.x()); double nY = pageItem->absToPageY(eventPos.y()); if ( d->mouseMode == Okular::Settings::EnumMouseMode::TextSelect ) { textSelectionClear(); Okular::RegularAreaRect *wordRect = pageItem->page()->wordAt( Okular::NormalizedPoint( nX, nY ) ); if ( wordRect ) { // TODO words with hyphens across pages d->document->setPageTextSelection( pageItem->pageNumber(), wordRect, palette().color( QPalette::Active, QPalette::Highlight ) ); d->pagesWithTextSelection << pageItem->pageNumber(); if ( d->document->isAllowed( Okular::AllowCopy ) ) { const QString text = d->selectedText(); if ( !text.isEmpty() ) { QClipboard *cb = QApplication::clipboard(); if ( cb->supportsSelection() ) cb->setText( text, QClipboard::Selection ); } } return; } } const QRect & itemRect = pageItem->uncroppedGeometry(); Okular::Annotation * ann = nullptr; const Okular::ObjectRect * orect = pageItem->page()->objectRect( Okular::ObjectRect::OAnnotation, nX, nY, itemRect.width(), itemRect.height() ); if ( orect ) ann = ( (Okular::AnnotationObjectRect *)orect )->annotation(); if ( ann && ann->subType() != Okular::Annotation::AWidget ) { openAnnotationWindow( ann, pageItem->pageNumber() ); } } } } void PageView::wheelEvent( QWheelEvent *e ) { if ( !d->document->isOpened() ) { QAbstractScrollArea::wheelEvent( e ); return; } int delta = e->angleDelta().y(), vScroll = verticalScrollBar()->value(); e->accept(); if ( (e->modifiers() & Qt::ControlModifier) == Qt::ControlModifier ) { d->controlWheelAccumulatedDelta += delta; if ( d->controlWheelAccumulatedDelta <= -QWheelEvent::DefaultDeltasPerStep ) { slotZoomOut(); d->controlWheelAccumulatedDelta = 0; } else if ( d->controlWheelAccumulatedDelta >= QWheelEvent::DefaultDeltasPerStep ) { slotZoomIn(); d->controlWheelAccumulatedDelta = 0; } } else { d->controlWheelAccumulatedDelta = 0; if ( delta <= -QWheelEvent::DefaultDeltasPerStep && !Okular::Settings::viewContinuous() && vScroll == verticalScrollBar()->maximum() ) { // go to next page if ( (int)d->document->currentPage() < d->items.count() - 1 ) { // more optimized than document->setNextPage and then move view to top Okular::DocumentViewport newViewport = d->document->viewport(); newViewport.pageNumber += viewColumns(); if ( newViewport.pageNumber >= (int)d->items.count() ) newViewport.pageNumber = d->items.count() - 1; newViewport.rePos.enabled = true; newViewport.rePos.normalizedY = 0.0; d->document->setViewport( newViewport ); d->scroller->scrollTo(QPoint(horizontalScrollBar()->value(), verticalScrollBar()->value()), 0); //sync scroller with scrollbar } } else if ( delta >= QWheelEvent::DefaultDeltasPerStep && !Okular::Settings::viewContinuous() && vScroll == verticalScrollBar()->minimum() ) { // go to prev page if ( d->document->currentPage() > 0 ) { // more optimized than document->setPrevPage and then move view to bottom Okular::DocumentViewport newViewport = d->document->viewport(); newViewport.pageNumber -= viewColumns(); if ( newViewport.pageNumber < 0 ) newViewport.pageNumber = 0; newViewport.rePos.enabled = true; newViewport.rePos.normalizedY = 1.0; d->document->setViewport( newViewport ); d->scroller->scrollTo(QPoint(horizontalScrollBar()->value(), verticalScrollBar()->value()), 0); //sync scroller with scrollbar } } else{ if(delta != 0 && delta % QWheelEvent::DefaultDeltasPerStep == 0 ){ //number of scroll wheel steps Qt gives to us at the same time int count = abs(delta / QWheelEvent::DefaultDeltasPerStep); if(delta<0){ slotScrollDown(count); }else{ slotScrollUp(count); } } else{ d->scroller->scrollTo(d->scroller->finalPosition() - e->angleDelta()/4.0 , 0 ); } } } updateCursor(); } bool PageView::viewportEvent( QEvent * e ) { if ( e->type() == QEvent::ToolTip // Show tool tips only for those modes that change the cursor // to a hand when hovering over the link. && ( d->mouseMode == Okular::Settings::EnumMouseMode::Browse || d->mouseMode == Okular::Settings::EnumMouseMode::RectSelect || d->mouseMode == Okular::Settings::EnumMouseMode::TextSelect || d->mouseMode == Okular::Settings::EnumMouseMode::TrimSelect ) ) { QHelpEvent * he = static_cast< QHelpEvent* >( e ); if ( d->mouseAnnotation->isMouseOver() ) { d->mouseAnnotation->routeTooltipEvent( he ); } else { const QPoint eventPos = contentAreaPoint( he->pos() ); PageViewItem * pageItem = pickItemOnPoint( eventPos.x(), eventPos.y() ); const Okular::ObjectRect * rect = nullptr; const Okular::Action * link = nullptr; if ( pageItem ) { double nX = pageItem->absToPageX( eventPos.x() ); double nY = pageItem->absToPageY( eventPos.y() ); rect = pageItem->page()->objectRect( Okular::ObjectRect::Action, nX, nY, pageItem->uncroppedWidth(), pageItem->uncroppedHeight() ); if ( rect ) link = static_cast< const Okular::Action * >( rect->object() ); } if ( link ) { QRect r = rect->boundingRect( pageItem->uncroppedWidth(), pageItem->uncroppedHeight() ); r.translate( pageItem->uncroppedGeometry().topLeft() ); r.translate( -contentAreaPosition() ); QString tip = link->actionTip(); if ( !tip.isEmpty() ) QToolTip::showText( he->globalPos(), tip, viewport(), r ); } } e->accept(); return true; } else // do not stop the event return QAbstractScrollArea::viewportEvent( e ); } void PageView::scrollContentsBy( int dx, int dy ) { const QRect r = viewport()->rect(); viewport()->scroll( dx, dy, r ); // HACK manually repaint the damaged regions, as it seems some updates are missed // thus leaving artifacts around QRegion rgn( r ); rgn -= rgn & r.translated( dx, dy ); for ( const QRect &rect : rgn ) viewport()->update( rect ); } //END widget events QList< Okular::RegularAreaRect * > PageView::textSelections( const QPoint& start, const QPoint& end, int& firstpage ) { firstpage = -1; QList< Okular::RegularAreaRect * > ret; QSet< int > affectedItemsSet; QRect selectionRect = QRect( start, end ).normalized(); for ( const PageViewItem *item : qAsConst(d->items) ) { if ( item->isVisible() && selectionRect.intersects( item->croppedGeometry() ) ) affectedItemsSet.insert( item->pageNumber() ); } #ifdef PAGEVIEW_DEBUG qCDebug(OkularUiDebug) << ">>>> item selected by mouse:" << affectedItemsSet.count(); #endif if ( !affectedItemsSet.isEmpty() ) { // is the mouse drag line the ne-sw diagonal of the selection rect? bool direction_ne_sw = start == selectionRect.topRight() || start == selectionRect.bottomLeft(); int tmpmin = d->document->pages(); int tmpmax = 0; for ( const int p : qAsConst(affectedItemsSet) ) { if ( p < tmpmin ) tmpmin = p; if ( p > tmpmax ) tmpmax = p; } PageViewItem * a = pickItemOnPoint( (int)( direction_ne_sw ? selectionRect.right() : selectionRect.left() ), (int)selectionRect.top() ); int min = a && ( a->pageNumber() != tmpmax ) ? a->pageNumber() : tmpmin; PageViewItem * b = pickItemOnPoint( (int)( direction_ne_sw ? selectionRect.left() : selectionRect.right() ), (int)selectionRect.bottom() ); int max = b && ( b->pageNumber() != tmpmin ) ? b->pageNumber() : tmpmax; QList< int > affectedItemsIds; for ( int i = min; i <= max; ++i ) affectedItemsIds.append( i ); #ifdef PAGEVIEW_DEBUG qCDebug(OkularUiDebug) << ">>>> pages:" << affectedItemsIds; #endif firstpage = affectedItemsIds.first(); if ( affectedItemsIds.count() == 1 ) { PageViewItem * item = d->items[ affectedItemsIds.first() ]; selectionRect.translate( -item->uncroppedGeometry().topLeft() ); ret.append( textSelectionForItem( item, direction_ne_sw ? selectionRect.topRight() : selectionRect.topLeft(), direction_ne_sw ? selectionRect.bottomLeft() : selectionRect.bottomRight() ) ); } else if ( affectedItemsIds.count() > 1 ) { // first item PageViewItem * first = d->items[ affectedItemsIds.first() ]; QRect geom = first->croppedGeometry().intersected( selectionRect ).translated( -first->uncroppedGeometry().topLeft() ); ret.append( textSelectionForItem( first, selectionRect.bottom() > geom.height() ? ( direction_ne_sw ? geom.topRight() : geom.topLeft() ) : ( direction_ne_sw ? geom.bottomRight() : geom.bottomLeft() ), QPoint() ) ); // last item PageViewItem * last = d->items[ affectedItemsIds.last() ]; geom = last->croppedGeometry().intersected( selectionRect ).translated( -last->uncroppedGeometry().topLeft() ); // the last item needs to appended at last... Okular::RegularAreaRect * lastArea = textSelectionForItem( last, QPoint(), selectionRect.bottom() > geom.height() ? ( direction_ne_sw ? geom.bottomLeft() : geom.bottomRight() ) : ( direction_ne_sw ? geom.topLeft() : geom.topRight() ) ); affectedItemsIds.removeFirst(); affectedItemsIds.removeLast(); // item between the two above for ( const int page : qAsConst(affectedItemsIds) ) { ret.append( textSelectionForItem( d->items[ page ] ) ); } ret.append( lastArea ); } } return ret; } void PageView::drawDocumentOnPainter( const QRect & contentsRect, QPainter * p ) { QColor backColor; if ( Okular::Settings::useCustomBackgroundColor() ) backColor = Okular::Settings::backgroundColor(); else backColor = viewport()->palette().color( QPalette::Dark ); // create a region from which we'll subtract painted rects QRegion remainingArea( contentsRect ); // This loop draws the actual pages // iterate over all items painting the ones intersecting contentsRect for ( const PageViewItem * item : qAsConst( d->items ) ) { // check if a piece of the page intersects the contents rect if ( !item->isVisible() || !item->croppedGeometry().intersects( contentsRect ) ) continue; // get item and item's outline geometries QRect itemGeometry = item->croppedGeometry(); // move the painter to the top-left corner of the real page p->save(); p->translate( itemGeometry.left(), itemGeometry.top() ); // draw the page using the PagePainter with all flags active if ( contentsRect.intersects( itemGeometry ) ) { Okular::NormalizedPoint *viewPortPoint = nullptr; Okular::NormalizedPoint point( d->lastSourceLocationViewportNormalizedX, d->lastSourceLocationViewportNormalizedY ); if( Okular::Settings::showSourceLocationsGraphically() && item->pageNumber() == d->lastSourceLocationViewportPageNumber ) { viewPortPoint = &point; } QRect pixmapRect = contentsRect.intersected( itemGeometry ); pixmapRect.translate( -item->croppedGeometry().topLeft() ); PagePainter::paintCroppedPageOnPainter( p, item->page(), this, pageflags, item->uncroppedWidth(), item->uncroppedHeight(), pixmapRect, item->crop(), viewPortPoint ); } // remove painted area from 'remainingArea' and restore painter remainingArea -= itemGeometry; p->restore(); } // fill the visible area around the page with the background color for (const QRect& backRect : remainingArea ) p->fillRect( backRect, backColor ); // take outline and shadow into account when testing whether a repaint is necessary auto dpr = devicePixelRatioF(); QRect checkRect = contentsRect; checkRect.adjust( -3, -3, 1, 1 ); // Method to linearly interpolate between black (=(0,0,0), omitted) and the background color auto interpolateColor = [&backColor]( double t ) { return QColor( t*backColor.red(), t*backColor.green(), t*backColor.blue() ); }; // width of the shadow in device pixels static const int shadowWidth = 2*dpr; // iterate over all items painting a black outline and a simple bottom/right gradient for ( const PageViewItem * item : qAsConst( d->items ) ) { // check if a piece of the page intersects the contents rect if ( !item->isVisible() || !item->croppedGeometry().intersects( checkRect ) ) continue; // get item and item's outline geometries QRect itemGeometry = item->croppedGeometry(); // move the painter to the top-left corner of the real page p->save(); p->translate( itemGeometry.left(), itemGeometry.top() ); // draw the page outline (black border and bottom-right shadow) if ( !itemGeometry.contains( contentsRect ) ) { int itemWidth = itemGeometry.width(); int itemHeight = itemGeometry.height(); // draw simple outline QPen pen( Qt::black ); pen.setWidth(0); p->setPen( pen ); QRectF outline( -1.0/dpr, -1.0/dpr, itemWidth + 1.0/dpr, itemHeight + 1.0/dpr ); p->drawRect( outline ); // draw bottom/right gradient for ( int i = 1; i <= shadowWidth; i++ ) { pen.setColor( interpolateColor( double(i)/( shadowWidth+1 ) ) ); p->setPen( pen ); QPointF left( (i-1)/dpr, itemHeight + i/dpr ); QPointF up( itemWidth + i/dpr, (i-1)/dpr ); QPointF corner( itemWidth + i/dpr, itemHeight + i/dpr); p->drawLine( left, corner ); p->drawLine( up, corner ); } } p->restore(); } } void PageView::updateItemSize( PageViewItem * item, int colWidth, int rowHeight ) { const Okular::Page * okularPage = item->page(); double width = okularPage->width(), height = okularPage->height(), zoom = d->zoomFactor; Okular::NormalizedRect crop( 0., 0., 1., 1. ); // Handle cropping, due to either "Trim Margin" or "Trim to Selection" cases if (( Okular::Settings::trimMargins() && okularPage->isBoundingBoxKnown() && !okularPage->boundingBox().isNull() ) || ( d->aTrimToSelection && d->aTrimToSelection->isChecked() && !d->trimBoundingBox.isNull())) { crop = Okular::Settings::trimMargins() ? okularPage->boundingBox() : d->trimBoundingBox; // Rotate the bounding box for ( int i = okularPage->rotation(); i > 0; --i ) { Okular::NormalizedRect rot = crop; crop.left = 1 - rot.bottom; crop.top = rot.left; crop.right = 1 - rot.top; crop.bottom = rot.right; } // Expand the crop slightly beyond the bounding box (for Trim Margins only) if (Okular::Settings::trimMargins()) { static const double cropExpandRatio = 0.04; const double cropExpand = cropExpandRatio * ( (crop.right-crop.left) + (crop.bottom-crop.top) ) / 2; crop = Okular::NormalizedRect( crop.left - cropExpand, crop.top - cropExpand, crop.right + cropExpand, crop.bottom + cropExpand ) & Okular::NormalizedRect( 0, 0, 1, 1 ); } // We currently generate a larger image and then crop it, so if the // crop rect is very small the generated image is huge. Hence, we shouldn't // let the crop rect become too small. static double minCropRatio; if (Okular::Settings::trimMargins()) { // Make sure we crop by at most 50% in either dimension: minCropRatio = 0.5; } else { // Looser Constraint for "Trim Selection" minCropRatio = 0.20; } if ( ( crop.right - crop.left ) < minCropRatio ) { const double newLeft = ( crop.left + crop.right ) / 2 - minCropRatio/2; crop.left = qMax( 0.0, qMin( 1.0 - minCropRatio, newLeft ) ); crop.right = crop.left + minCropRatio; } if ( ( crop.bottom - crop.top ) < minCropRatio ) { const double newTop = ( crop.top + crop.bottom ) / 2 - minCropRatio/2; crop.top = qMax( 0.0, qMin( 1.0 - minCropRatio, newTop ) ); crop.bottom = crop.top + minCropRatio; } width *= ( crop.right - crop.left ); height *= ( crop.bottom - crop.top ); #ifdef PAGEVIEW_DEBUG qCDebug(OkularUiDebug) << "Cropped page" << okularPage->number() << "to" << crop << "width" << width << "height" << height << "by bbox" << okularPage->boundingBox(); #endif } if ( d->zoomMode == ZoomFixed ) { width *= zoom; height *= zoom; item->setWHZC( (int)width, (int)height, d->zoomFactor, crop ); } else if ( d->zoomMode == ZoomFitWidth ) { height = ( height / width ) * colWidth; zoom = (double)colWidth / width; item->setWHZC( colWidth, (int)height, zoom, crop ); if ((uint)item->pageNumber() == d->document->currentPage()) d->zoomFactor = zoom; } else if ( d->zoomMode == ZoomFitPage ) { const double scaleW = (double)colWidth / (double)width; const double scaleH = (double)rowHeight / (double)height; zoom = qMin( scaleW, scaleH ); item->setWHZC( (int)(zoom * width), (int)(zoom * height), zoom, crop ); if ((uint)item->pageNumber() == d->document->currentPage()) d->zoomFactor = zoom; } else if ( d->zoomMode == ZoomFitAuto ) { const double aspectRatioRelation = 1.25; // relation between aspect ratios for "auto fit" const double uiAspect = (double)rowHeight / (double)colWidth; const double pageAspect = (double)height / (double)width; const double rel = uiAspect / pageAspect; const bool isContinuous = Okular::Settings::viewContinuous(); if ( !isContinuous && rel > aspectRatioRelation ) { // UI space is relatively much higher than the page zoom = (double)rowHeight / (double)height; } else if ( rel < 1.0 / aspectRatioRelation ) { // UI space is relatively much wider than the page in relation zoom = (double)colWidth / (double)width; } else { // aspect ratios of page and UI space are very similar const double scaleW = (double)colWidth / (double)width; const double scaleH = (double)rowHeight / (double)height; zoom = qMin( scaleW, scaleH ); } item->setWHZC( (int)(zoom * width), (int)(zoom * height), zoom, crop ); if ((uint)item->pageNumber() == d->document->currentPage()) d->zoomFactor = zoom; } #ifndef NDEBUG else qCDebug(OkularUiDebug) << "calling updateItemSize with unrecognized d->zoomMode!"; #endif } PageViewItem * PageView::pickItemOnPoint( int x, int y ) { PageViewItem * item = nullptr; for ( PageViewItem * i : qAsConst( d->visibleItems ) ) { const QRect & r = i->croppedGeometry(); if ( x < r.right() && x > r.left() && y < r.bottom() ) { if ( y > r.top() ) item = i; break; } } return item; } void PageView::textSelectionClear() { // something to clear if ( !d->pagesWithTextSelection.isEmpty() ) { for ( const int page : qAsConst( d->pagesWithTextSelection ) ) d->document->setPageTextSelection( page, nullptr, QColor() ); d->pagesWithTextSelection.clear(); } } void PageView::selectionStart( const QPoint & pos, const QColor & color, bool /*aboveAll*/ ) { selectionClear(); d->mouseSelecting = true; d->mouseSelectionRect.setRect( pos.x(), pos.y(), 1, 1 ); d->mouseSelectionColor = color; // ensures page doesn't scroll if ( d->autoScrollTimer ) { d->scrollIncrement = 0; d->autoScrollTimer->stop(); } } void PageView::scrollPosIntoView( const QPoint & pos ) { // this number slows the speed of the page by its value, chosen not to be too fast or too slow, the actual speed is determined from the mouse position, not critical const int damping=6; if (pos.x() < horizontalScrollBar()->value()) d->dragScrollVector.setX((pos.x() - horizontalScrollBar()->value())/damping); else if (horizontalScrollBar()->value() + viewport()->width() < pos.x()) d->dragScrollVector.setX((pos.x() - horizontalScrollBar()->value() - viewport()->width())/damping); else d->dragScrollVector.setX(0); if (pos.y() < verticalScrollBar()->value()) d->dragScrollVector.setY((pos.y() - verticalScrollBar()->value())/damping); else if (verticalScrollBar()->value() + viewport()->height() < pos.y()) d->dragScrollVector.setY((pos.y() - verticalScrollBar()->value() - viewport()->height())/damping); else d->dragScrollVector.setY(0); if (d->dragScrollVector != QPoint(0, 0)) { if (!d->dragScrollTimer.isActive()) d->dragScrollTimer.start(1000/60); //60 fps } else d->dragScrollTimer.stop(); } QPoint PageView::viewportToContentArea( const Okular::DocumentViewport & vp ) const { Q_ASSERT( vp.pageNumber >= 0 ); const QRect & r = d->items[ vp.pageNumber ]->croppedGeometry(); QPoint c { r.left(), r.top() }; if ( vp.rePos.enabled ) { if ( vp.rePos.pos == Okular::DocumentViewport::Center ) { c.rx() += qRound( normClamp( vp.rePos.normalizedX, 0.5 ) * (double)r.width() ); c.ry() += qRound( normClamp( vp.rePos.normalizedY, 0.0 ) * (double)r.height() ); } else { // TopLeft c.rx() += qRound( normClamp( vp.rePos.normalizedX, 0.0 ) * (double)r.width() + viewport()->width() / 2.0 ); c.ry() += qRound( normClamp( vp.rePos.normalizedY, 0.0 ) * (double)r.height() + viewport()->height() / 2.0 ); } } else { // exact repositioning disabled, align page top margin with viewport top border by default c.rx() += r.width() / 2; c.ry() += viewport()->height() / 2 - 10; } return c; } void PageView::updateSelection( const QPoint & pos ) { if ( d->mouseSelecting ) { scrollPosIntoView( pos ); // update the selection rect QRect updateRect = d->mouseSelectionRect; d->mouseSelectionRect.setBottomLeft( pos ); updateRect |= d->mouseSelectionRect; updateRect.translate( -contentAreaPosition() ); viewport()->update( updateRect.adjusted( -1, -2, 2, 1 ) ); } else if ( d->mouseTextSelecting) { scrollPosIntoView( pos ); int first = -1; const QList< Okular::RegularAreaRect * > selections = textSelections( pos, d->mouseSelectPos, first ); QSet< int > pagesWithSelectionSet; for ( int i = 0; i < selections.count(); ++i ) pagesWithSelectionSet.insert( i + first ); const QSet< int > noMoreSelectedPages = d->pagesWithTextSelection - pagesWithSelectionSet; // clear the selection from pages not selected anymore for ( int p : noMoreSelectedPages ) { d->document->setPageTextSelection( p, nullptr, QColor() ); } // set the new selection for the selected pages for ( int p : qAsConst(pagesWithSelectionSet) ) { d->document->setPageTextSelection( p, selections[ p - first ], palette().color( QPalette::Active, QPalette::Highlight ) ); } d->pagesWithTextSelection = pagesWithSelectionSet; } } static Okular::NormalizedPoint rotateInNormRect( const QPoint &rotated, const QRect &rect, Okular::Rotation rotation ) { Okular::NormalizedPoint ret; switch ( rotation ) { case Okular::Rotation0: ret = Okular::NormalizedPoint( rotated.x(), rotated.y(), rect.width(), rect.height() ); break; case Okular::Rotation90: ret = Okular::NormalizedPoint( rotated.y(), rect.width() - rotated.x(), rect.height(), rect.width() ); break; case Okular::Rotation180: ret = Okular::NormalizedPoint( rect.width() - rotated.x(), rect.height() - rotated.y(), rect.width(), rect.height() ); break; case Okular::Rotation270: ret = Okular::NormalizedPoint( rect.height() - rotated.y(), rotated.x(), rect.height(), rect.width() ); break; } return ret; } Okular::RegularAreaRect * PageView::textSelectionForItem( const PageViewItem * item, const QPoint & startPoint, const QPoint & endPoint ) { const QRect & geometry = item->uncroppedGeometry(); Okular::NormalizedPoint startCursor( 0.0, 0.0 ); if ( !startPoint.isNull() ) { startCursor = rotateInNormRect( startPoint, geometry, item->page()->rotation() ); } Okular::NormalizedPoint endCursor( 1.0, 1.0 ); if ( !endPoint.isNull() ) { endCursor = rotateInNormRect( endPoint, geometry, item->page()->rotation() ); } Okular::TextSelection mouseTextSelectionInfo( startCursor, endCursor ); const Okular::Page * okularPage = item->page(); if ( !okularPage->hasTextPage() ) d->document->requestTextPage( okularPage->number() ); Okular::RegularAreaRect * selectionArea = okularPage->textArea( &mouseTextSelectionInfo ); #ifdef PAGEVIEW_DEBUG qCDebug(OkularUiDebug).nospace() << "text areas (" << okularPage->number() << "): " << ( selectionArea ? QString::number( selectionArea->count() ) : "(none)" ); #endif return selectionArea; } void PageView::selectionClear(const ClearMode mode) { QRect updatedRect = d->mouseSelectionRect.normalized().adjusted( -2, -2, 2, 2 ); d->mouseSelecting = false; d->mouseSelectionRect.setCoords( 0, 0, 0, 0 ); d->tableSelectionCols.clear(); d->tableSelectionRows.clear(); d->tableDividersGuessed = false; for (const TableSelectionPart &tsp : qAsConst(d->tableSelectionParts)) { QRect selectionPartRect = tsp.rectInItem.geometry(tsp.item->uncroppedWidth(), tsp.item->uncroppedHeight()); selectionPartRect.translate( tsp.item->uncroppedGeometry().topLeft () ); // should check whether this is on-screen here? updatedRect = updatedRect.united(selectionPartRect); } if ( mode != ClearOnlyDividers ) { d->tableSelectionParts.clear(); } d->tableSelectionParts.clear(); updatedRect.translate( -contentAreaPosition() ); viewport()->update( updatedRect ); } // const to be used for both zoomFactorFitMode function and slotRelayoutPages. static const int kcolWidthMargin = 6; static const int krowHeightMargin = 12; double PageView::zoomFactorFitMode( ZoomMode mode ) { const int pageCount = d->items.count(); if ( pageCount == 0 ) return 0; const bool facingCentered = Okular::Settings::viewMode() == Okular::Settings::EnumViewMode::FacingFirstCentered || (Okular::Settings::viewMode() == Okular::Settings::EnumViewMode::Facing && pageCount == 1); const bool overrideCentering = facingCentered && pageCount < 3; const int nCols = overrideCentering ? 1 : viewColumns(); const double colWidth = viewport()->width() / static_cast(nCols) - kcolWidthMargin; const double rowHeight = viewport()->height() - krowHeightMargin; const PageViewItem * currentItem = d->items[ qMax( 0, (int)d->document->currentPage()) ]; // prevent segmentation fault when opening a new document; if ( !currentItem ) return 0; const Okular::Page * okularPage = currentItem->page(); const double width = okularPage->width(), height = okularPage->height(); if ( mode == ZoomFitWidth ) return (double) colWidth / width; if ( mode == ZoomFitPage ) { const double scaleW = (double) colWidth / (double)width; const double scaleH = (double) rowHeight / (double)height; return qMin(scaleW, scaleH); } return 0; } void PageView::updateZoom( ZoomMode newZoomMode ) { if ( newZoomMode == ZoomFixed ) { if ( d->aZoom->currentItem() == 0 ) newZoomMode = ZoomFitWidth; else if ( d->aZoom->currentItem() == 1 ) newZoomMode = ZoomFitPage; else if ( d->aZoom->currentItem() == 2 ) newZoomMode = ZoomFitAuto; } float newFactor = d->zoomFactor; QAction * checkedZoomAction = nullptr; switch ( newZoomMode ) { case ZoomFixed:{ //ZoomFixed case QString z = d->aZoom->currentText(); // kdelibs4 sometimes adds accelerators to actions' text directly :( z.remove (QLatin1Char('&')); z.remove (QLatin1Char('%')); newFactor = QLocale().toDouble( z ) / 100.0; }break; case ZoomIn: case ZoomOut:{ const float zoomFactorFitWidth = zoomFactorFitMode(ZoomFitWidth); const float zoomFactorFitPage = zoomFactorFitMode(ZoomFitPage); QVector zoomValue(15); std::copy(kZoomValues, kZoomValues + 13, zoomValue.begin()); zoomValue[13] = zoomFactorFitWidth; zoomValue[14] = zoomFactorFitPage; std::sort(zoomValue.begin(), zoomValue.end()); QVector::iterator i; if ( newZoomMode == ZoomOut ) { if (newFactor <= zoomValue.first()) return; i = std::lower_bound(zoomValue.begin(), zoomValue.end(), newFactor) - 1; } else { if (newFactor >= zoomValue.last()) return; i = std::upper_bound(zoomValue.begin(), zoomValue.end(), newFactor); } const float tmpFactor = *i; if ( tmpFactor == zoomFactorFitWidth ) { newZoomMode = ZoomFitWidth; checkedZoomAction = d->aZoomFitWidth; } else if ( tmpFactor == zoomFactorFitPage ) { newZoomMode = ZoomFitPage; checkedZoomAction = d->aZoomFitPage; } else { newFactor = tmpFactor; newZoomMode = ZoomFixed; } } break; case ZoomActual: newZoomMode = ZoomFixed; newFactor = 1.0; break; case ZoomFitWidth: checkedZoomAction = d->aZoomFitWidth; break; case ZoomFitPage: checkedZoomAction = d->aZoomFitPage; break; case ZoomFitAuto: checkedZoomAction = d->aZoomAutoFit; break; case ZoomRefreshCurrent: newZoomMode = ZoomFixed; d->zoomFactor = -1; break; } const float upperZoomLimit = d->document->supportsTiles() ? 16.0 : 4.0; if ( newFactor > upperZoomLimit ) newFactor = upperZoomLimit; if ( newFactor < 0.1 ) newFactor = 0.1; if ( newZoomMode != d->zoomMode || (newZoomMode == ZoomFixed && newFactor != d->zoomFactor ) ) { // rebuild layout and update the whole viewport d->zoomMode = newZoomMode; d->zoomFactor = newFactor; // be sure to block updates to document's viewport bool prevState = d->blockViewport; d->blockViewport = true; slotRelayoutPages(); d->blockViewport = prevState; // request pixmaps slotRequestVisiblePixmaps(); // update zoom text updateZoomText(); // update actions checked state if ( d->aZoomFitWidth ) { d->aZoomFitWidth->setChecked( checkedZoomAction == d->aZoomFitWidth ); d->aZoomFitPage->setChecked( checkedZoomAction == d->aZoomFitPage ); d->aZoomAutoFit->setChecked( checkedZoomAction == d->aZoomAutoFit ); } } else if ( newZoomMode == ZoomFixed && newFactor == d->zoomFactor ) updateZoomText(); d->aZoomIn->setEnabled( d->zoomFactor < upperZoomLimit-0.001 ); d->aZoomOut->setEnabled( d->zoomFactor > 0.101 ); d->aZoomActual->setEnabled( d->zoomFactor != 1.0 ); } void PageView::updateZoomText() { // use current page zoom as zoomFactor if in ZoomFit/* mode if ( d->zoomMode != ZoomFixed && d->items.count() > 0 ) d->zoomFactor = d->items[ qMax( 0, (int)d->document->currentPage() ) ]->zoomFactor(); float newFactor = d->zoomFactor; d->aZoom->removeAllActions(); // add items that describe fit actions QStringList translated; translated << i18n("Fit Width") << i18n("Fit Page") << i18n("Auto Fit"); // add percent items int idx = 0, selIdx = 3; bool inserted = false; //use: "d->zoomMode != ZoomFixed" to hide Fit/* zoom ratio int zoomValueCount = 11; if ( d->document->supportsTiles() ) zoomValueCount = 13; while ( idx < zoomValueCount || !inserted ) { float value = idx < zoomValueCount ? kZoomValues[ idx ] : newFactor; if ( !inserted && newFactor < (value - 0.0001) ) value = newFactor; else idx ++; if ( value > (newFactor - 0.0001) && value < (newFactor + 0.0001) ) inserted = true; if ( !inserted ) selIdx++; // we do not need to display 2-digit precision QString localValue( QLocale().toString( value * 100.0, 'f', 1 ) ); localValue.remove( QLocale().decimalPoint() + QLatin1Char('0') ); // remove a trailing zero in numbers like 66.70 if ( localValue.right( 1 ) == QLatin1String( "0" ) && localValue.indexOf( QLocale().decimalPoint() ) > -1 ) localValue.chop( 1 ); translated << QStringLiteral( "%1%" ).arg( localValue ); } d->aZoom->setItems( translated ); // select current item in list if ( d->zoomMode == ZoomFitWidth ) selIdx = 0; else if ( d->zoomMode == ZoomFitPage ) selIdx = 1; else if ( d->zoomMode == ZoomFitAuto ) selIdx = 2; // we have to temporarily enable the actions as otherwise we can't set a new current item d->aZoom->setEnabled( true ); d->aZoom->selectableActionGroup()->setEnabled( true ); d->aZoom->setCurrentItem( selIdx ); d->aZoom->setEnabled( d->items.size() > 0 ); d->aZoom->selectableActionGroup()->setEnabled( d->items.size() > 0 ); } void PageView::updateViewMode(const int nr) { for ( QAction* action : d->aViewMode->menu()->actions() ) { QVariant mode_id = action->data(); if (mode_id.toInt() == nr) { action->trigger(); } } } void PageView::updateCursor() { const QPoint p = contentAreaPosition() + viewport()->mapFromGlobal( QCursor::pos() ); updateCursor( p ); } void PageView::updateCursor( const QPoint &p ) { // reset mouse over link it will be re-set if that still valid d->mouseOverLinkObject = nullptr; // detect the underlaying page (if present) PageViewItem * pageItem = pickItemOnPoint( p.x(), p.y() ); if ( d->annotator && d->annotator->active() ) { if ( pageItem || d->annotator->annotating() ) setCursor( d->annotator->cursor() ); else setCursor( Qt::ForbiddenCursor ); } else if ( pageItem ) { double nX = pageItem->absToPageX(p.x()); double nY = pageItem->absToPageY(p.y()); Qt::CursorShape cursorShapeFallback; // if over a ObjectRect (of type Link) change cursor to hand switch ( d->mouseMode ) { case Okular::Settings::EnumMouseMode::TextSelect: if (d->mouseTextSelecting) { setCursor( Qt::IBeamCursor ); return; } cursorShapeFallback = Qt::IBeamCursor; break; case Okular::Settings::EnumMouseMode::Magnifier: setCursor( Qt::CrossCursor ); return; case Okular::Settings::EnumMouseMode::RectSelect: case Okular::Settings::EnumMouseMode::TrimSelect: if (d->mouseSelecting) { setCursor( Qt::CrossCursor ); return; } cursorShapeFallback = Qt::CrossCursor; break; case Okular::Settings::EnumMouseMode::Browse: d->mouseOnRect = false; if ( d->mouseAnnotation->isMouseOver() ) { d->mouseOnRect = true; setCursor( d->mouseAnnotation->cursor() ); return; } else { cursorShapeFallback = Qt::OpenHandCursor; } break; default: setCursor( Qt::ArrowCursor ); return; } const Okular::ObjectRect * linkobj = pageItem->page()->objectRect( Okular::ObjectRect::Action, nX, nY, pageItem->uncroppedWidth(), pageItem->uncroppedHeight() ); if ( linkobj ) { d->mouseOverLinkObject = linkobj; d->mouseOnRect = true; setCursor( Qt::PointingHandCursor ); } else { setCursor(cursorShapeFallback); } } else { // if there's no page over the cursor and we were showing the pointingHandCursor // go back to the normal one d->mouseOnRect = false; setCursor( Qt::ArrowCursor ); } } void PageView::reloadForms() { if( d->m_formsVisible ) { for ( PageViewItem * item : qAsConst( d->visibleItems ) ) { item->reloadFormWidgetsState(); } } } void PageView::moveMagnifier( const QPoint& p ) // non scaled point { const int w = d->magnifierView->width() * 0.5; const int h = d->magnifierView->height() * 0.5; int x = p.x() - w; int y = p.y() - h; const int max_x = viewport()->width(); const int max_y = viewport()->height(); QPoint scroll(0,0); if (x < 0) { if (horizontalScrollBar()->value() > 0) scroll.setX(x - w); x = 0; } if (y < 0) { if (verticalScrollBar()->value() > 0) scroll.setY(y - h); y = 0; } if (p.x() + w > max_x) { if (horizontalScrollBar()->value() < horizontalScrollBar()->maximum()) scroll.setX(p.x() + 2 * w - max_x); x = max_x - d->magnifierView->width() - 1; } if (p.y() + h > max_y) { if (verticalScrollBar()->value() < verticalScrollBar()->maximum()) scroll.setY(p.y() + 2 * h - max_y); y = max_y - d->magnifierView->height() - 1; } if (!scroll.isNull()) scrollPosIntoView(contentAreaPoint(p + scroll)); d->magnifierView->move(x, y); } void PageView::updateMagnifier( const QPoint& p ) // scaled point { /* translate mouse coordinates to page coordinates and inform the magnifier of the situation */ PageViewItem *item = pickItemOnPoint(p.x(), p.y()); if (item) { Okular::NormalizedPoint np(item->absToPageX(p.x()), item->absToPageY(p.y())); d->magnifierView->updateView( np, item->page() ); } } int PageView::viewColumns() const { int vm = Okular::Settings::viewMode(); if (vm == Okular::Settings::EnumViewMode::Single) return 1; else if (vm == Okular::Settings::EnumViewMode::Facing || vm == Okular::Settings::EnumViewMode::FacingFirstCentered) return 2; else if (vm == Okular::Settings::EnumViewMode::Summary && d->document->pages() < Okular::Settings::viewColumns() ) return d->document->pages(); else return Okular::Settings::viewColumns(); } void PageView::center(int cx, int cy, bool smoothMove) { scrollTo( cx - viewport()->width() / 2, cy - viewport()->height() / 2, smoothMove ); } void PageView::scrollTo( int x, int y, bool smoothMove ) { bool prevState = d->blockPixmapsRequest; int newValue = -1; if ( x != horizontalScrollBar()->value() || y != verticalScrollBar()->value() ) newValue = 1; // Pretend this call is the result of a scrollbar event d->blockPixmapsRequest = true; if(smoothMove) d->scroller->scrollTo(QPoint(x, y)); else d->scroller->scrollTo(QPoint(x, y), 0); d->blockPixmapsRequest = prevState; slotRequestVisiblePixmaps( newValue ); } void PageView::toggleFormWidgets( bool on ) { bool somehadfocus = false; for ( PageViewItem * item : qAsConst( d->items ) ) { const bool hadfocus = item->setFormWidgetsVisible( on ); somehadfocus = somehadfocus || hadfocus; } if ( somehadfocus ) setFocus(); d->m_formsVisible = on; if ( d->aToggleForms ) // it may not exist if we are on dummy mode { if ( d->m_formsVisible ) { d->aToggleForms->setText( i18n( "Hide Forms" ) ); } else { d->aToggleForms->setText( i18n( "Show Forms" ) ); } } } void PageView::resizeContentArea( const QSize & newSize ) { const QSize vs = viewport()->size(); int hRange = newSize.width() - vs.width(); int vRange = newSize.height() - vs.height(); if ( horizontalScrollBar()->isVisible() && hRange == verticalScrollBar()->width() && verticalScrollBar()->isVisible() && vRange == horizontalScrollBar()->height() && Okular::Settings::showScrollBars() ) { hRange = 0; vRange = 0; } horizontalScrollBar()->setRange( 0, hRange ); verticalScrollBar()->setRange( 0, vRange ); updatePageStep(); } void PageView::updatePageStep() { const QSize vs = viewport()->size(); horizontalScrollBar()->setPageStep( vs.width() ); verticalScrollBar()->setPageStep( vs.height() * (100 - Okular::Settings::scrollOverlap()) / 100 ); } void PageView::addWebShortcutsMenu( QMenu * menu, const QString & text ) { if ( text.isEmpty() ) { return; } QString searchText = text; searchText = searchText.replace( QLatin1Char('\n'), QLatin1Char(' ') ).replace(QLatin1Char( '\r'), QLatin1Char(' ') ).simplified(); if ( searchText.isEmpty() ) { return; } KUriFilterData filterData( searchText ); filterData.setSearchFilteringOptions( KUriFilterData::RetrievePreferredSearchProvidersOnly ); if ( KUriFilter::self()->filterSearchUri( filterData, KUriFilter::NormalTextFilter ) ) { const QStringList searchProviders = filterData.preferredSearchProviders(); if ( !searchProviders.isEmpty() ) { QMenu *webShortcutsMenu = new QMenu( menu ); webShortcutsMenu->setIcon( QIcon::fromTheme( QStringLiteral("preferences-web-browser-shortcuts") ) ); const QString squeezedText = KStringHandler::rsqueeze( searchText, searchTextPreviewLength ); webShortcutsMenu->setTitle( i18n( "Search for '%1' with", squeezedText ) ); QAction *action = nullptr; for ( const QString &searchProvider : searchProviders ) { action = new QAction( searchProvider, webShortcutsMenu ); action->setIcon( QIcon::fromTheme( filterData.iconNameForPreferredSearchProvider( searchProvider ) ) ); action->setData( filterData.queryForPreferredSearchProvider( searchProvider ) ); connect( action, &QAction::triggered, this, &PageView::slotHandleWebShortcutAction ); webShortcutsMenu->addAction( action ); } webShortcutsMenu->addSeparator(); action = new QAction( i18n( "Configure Web Shortcuts..." ), webShortcutsMenu ); action->setIcon( QIcon::fromTheme( QStringLiteral("configure") ) ); connect( action, &QAction::triggered, this, &PageView::slotConfigureWebShortcuts ); webShortcutsMenu->addAction( action ); menu->addMenu(webShortcutsMenu); } } } QMenu* PageView::createProcessLinkMenu(PageViewItem *item, const QPoint &eventPos) { // check if the right-click was over a link const double nX = item->absToPageX(eventPos.x()); const double nY = item->absToPageY(eventPos.y()); const Okular::ObjectRect * rect = item->page()->objectRect( Okular::ObjectRect::Action, nX, nY, item->uncroppedWidth(), item->uncroppedHeight() ); if ( rect ) { const Okular::Action * link = static_cast< const Okular::Action * >( rect->object() ); if (!link) return nullptr; QMenu *menu = new QMenu(this); // creating the menu and its actions QAction * processLink = menu->addAction( i18n( "Follow This Link" ) ); processLink->setObjectName(QStringLiteral("ProcessLinkAction")); if ( link->actionType() == Okular::Action::Sound ) { processLink->setText( i18n( "Play this Sound" ) ); if ( Okular::AudioPlayer::instance()->state() == Okular::AudioPlayer::PlayingState ) { QAction * actStopSound = menu->addAction( i18n( "Stop Sound" ) ); connect( actStopSound, &QAction::triggered, []() { Okular::AudioPlayer::instance()->stopPlaybacks(); }); } } if ( dynamic_cast< const Okular::BrowseAction * >( link ) ) { QAction * actCopyLinkLocation = menu->addAction( QIcon::fromTheme( QStringLiteral("edit-copy") ), i18n( "Copy Link Address" ) ); actCopyLinkLocation->setObjectName(QStringLiteral("CopyLinkLocationAction")); connect( actCopyLinkLocation, &QAction::triggered, [ link ]() { const Okular::BrowseAction * browseLink = static_cast< const Okular::BrowseAction * >( link ); QClipboard *cb = QApplication::clipboard(); cb->setText( browseLink->url().toDisplayString(), QClipboard::Clipboard ); if ( cb->supportsSelection() ) cb->setText( browseLink->url().toDisplayString(), QClipboard::Selection ); } ); } connect( processLink, &QAction::triggered, [this, link]() { d->document->processAction( link ); }); return menu; } return nullptr; } void PageView::addSearchWithinDocumentAction(QMenu *menu, const QString &searchText) { const QString squeezedText = KStringHandler::rsqueeze( searchText, searchTextPreviewLength ); QAction *action = new QAction(i18n("Search for '%1' in this document", squeezedText), menu); action->setIcon( QIcon::fromTheme( QStringLiteral("document-preview") ) ); connect(action, &QAction::triggered, [this, searchText]{Q_EMIT triggerSearch(searchText);}); menu->addAction( action ); } //BEGIN private SLOTS void PageView::slotRelayoutPages() // called by: notifySetup, viewportResizeEvent, slotViewMode, slotContinuousToggled, updateZoom { // set an empty container if we have no pages const int pageCount = d->items.count(); if ( pageCount < 1 ) { return; } int viewportWidth = viewport()->width(), viewportHeight = viewport()->height(), fullWidth = 0, fullHeight = 0; // handle the 'center first page in row' stuff const bool facing = Okular::Settings::viewMode() == Okular::Settings::EnumViewMode::Facing && pageCount > 1; const bool facingCentered = Okular::Settings::viewMode() == Okular::Settings::EnumViewMode::FacingFirstCentered || (Okular::Settings::viewMode() == Okular::Settings::EnumViewMode::Facing && pageCount == 1); const bool overrideCentering = facingCentered && pageCount < 3; const bool centerFirstPage = facingCentered && !overrideCentering; const bool facingPages = facing || centerFirstPage; const bool centerLastPage = centerFirstPage && pageCount % 2 == 0; const bool continuousView = Okular::Settings::viewContinuous(); const int nCols = overrideCentering ? 1 : viewColumns(); const bool singlePageViewMode = Okular::Settings::viewMode() == Okular::Settings::EnumViewMode::Single; if ( d->aFitWindowToPage ) d->aFitWindowToPage->setEnabled( !continuousView && singlePageViewMode ); // set all items geometry and resize contents. handle 'continuous' and 'single' modes separately PageViewItem * currentItem = d->items[ qMax( 0, (int)d->document->currentPage() ) ]; // Here we find out column's width and row's height to compute a table // so we can place widgets 'centered in virtual cells'. const int nRows = (int)ceil( (float)(centerFirstPage ? (pageCount + nCols - 1) : pageCount) / (float)nCols ); int * colWidth = new int[ nCols ], * rowHeight = new int[ nRows ], cIdx = 0, rIdx = 0; for ( int i = 0; i < nCols; i++ ) colWidth[ i ] = viewportWidth / nCols; for ( int i = 0; i < nRows; i++ ) rowHeight[ i ] = 0; // handle the 'centering on first row' stuff if ( centerFirstPage ) cIdx += nCols - 1; // 1) find the maximum columns width and rows height for a grid in // which each page must well-fit inside a cell for ( PageViewItem * item : qAsConst( d->items ) ) { // update internal page size (leaving a little margin in case of Fit* modes) updateItemSize( item, colWidth[ cIdx ] - kcolWidthMargin, viewportHeight - krowHeightMargin ); // find row's maximum height and column's max width if ( item->croppedWidth() + kcolWidthMargin > colWidth[ cIdx ] ) colWidth[ cIdx ] = item->croppedWidth() + kcolWidthMargin; if ( item->croppedHeight() + krowHeightMargin > rowHeight[ rIdx ] ) rowHeight[ rIdx ] = item->croppedHeight() + krowHeightMargin; // handle the 'centering on first row' stuff // update col/row indices if ( ++cIdx == nCols ) { cIdx = 0; rIdx++; } } const int pageRowIdx = ( ( centerFirstPage ? nCols - 1 : 0 ) + currentItem->pageNumber() ) / nCols; // 2) compute full size for ( int i = 0; i < nCols; i++ ) fullWidth += colWidth[ i ]; if ( continuousView ) { for ( int i = 0; i < nRows; i++ ) fullHeight += rowHeight[ i ]; } else fullHeight = rowHeight[ pageRowIdx ]; // 3) arrange widgets inside cells (and refine fullHeight if needed) int insertX = 0, insertY = fullHeight < viewportHeight ? ( viewportHeight - fullHeight ) / 2 : 0; const int origInsertY = insertY; cIdx = 0; rIdx = 0; if ( centerFirstPage ) { cIdx += nCols - 1; for ( int i = 0; i < cIdx; ++i ) insertX += colWidth[ i ]; } for ( PageViewItem * item : qAsConst( d->items ) ) { int cWidth = colWidth[ cIdx ], rHeight = rowHeight[ rIdx ]; if ( continuousView || rIdx == pageRowIdx ) { const bool reallyDoCenterFirst = item->pageNumber() == 0 && centerFirstPage; const bool reallyDoCenterLast = item->pageNumber() == pageCount - 1 && centerLastPage; int actualX = 0; if ( reallyDoCenterFirst || reallyDoCenterLast ) { // page is centered across entire viewport actualX = (fullWidth - item->croppedWidth()) / 2; } else if ( facingPages ) { if (Okular::Settings::rtlReadingDirection()){ // RTL reading mode actualX = ( (centerFirstPage && item->pageNumber() % 2 == 0) || (!centerFirstPage && item->pageNumber() % 2 == 1) ) ? (fullWidth / 2) - item->croppedWidth() - 1 : (fullWidth / 2) + 1; } else { // page edges 'touch' the center of the viewport actualX = ( (centerFirstPage && item->pageNumber() % 2 == 1) || (!centerFirstPage && item->pageNumber() % 2 == 0) ) ? (fullWidth / 2) - item->croppedWidth() - 1 : (fullWidth / 2) + 1; } } else { // page is centered within its virtual column //actualX = insertX + (cWidth - item->croppedWidth()) / 2; if (Okular::Settings::rtlReadingDirection()){ actualX = fullWidth - insertX - cWidth +( (cWidth - item->croppedWidth()) / 2); } else { actualX = insertX + (cWidth - item->croppedWidth()) / 2; } } item->moveTo( actualX, (continuousView ? insertY : origInsertY) + (rHeight - item->croppedHeight()) / 2 ); item->setVisible( true ); } else { item->moveTo( 0, 0 ); item->setVisible( false ); } item->setFormWidgetsVisible( d->m_formsVisible ); // advance col/row index insertX += cWidth; if ( ++cIdx == nCols ) { cIdx = 0; rIdx++; insertX = 0; insertY += rHeight; } #ifdef PAGEVIEW_DEBUG kWarning() << "updating size for pageno" << item->pageNumber() << "cropped" << item->croppedGeometry() << "uncropped" << item->uncroppedGeometry(); #endif } delete [] colWidth; delete [] rowHeight; // 3) reset dirty state d->dirtyLayout = false; // 4) update scrollview's contents size and recenter view bool wasUpdatesEnabled = viewport()->updatesEnabled(); if ( fullWidth != contentAreaWidth() || fullHeight != contentAreaHeight() ) { const Okular::DocumentViewport vp = d->document->viewport(); // disable updates and resize the viewportContents if ( wasUpdatesEnabled ) viewport()->setUpdatesEnabled( false ); resizeContentArea( QSize( fullWidth, fullHeight ) ); // restore previous viewport if defined and updates enabled if ( wasUpdatesEnabled ) { if ( vp.pageNumber >= 0 ) { int prevX = horizontalScrollBar()->value(), prevY = verticalScrollBar()->value(); const QPoint centerPos = viewportToContentArea( vp ); center( centerPos.x(), centerPos.y() ); // center() usually moves the viewport, that requests pixmaps too. // if that doesn't happen we have to request them by hand if ( prevX == horizontalScrollBar()->value() && prevY == verticalScrollBar()->value() ) slotRequestVisiblePixmaps(); } // or else go to center page else center( fullWidth / 2, 0 ); viewport()->setUpdatesEnabled( true ); } } // 5) update the whole viewport if updated enabled if ( wasUpdatesEnabled ) viewport()->update(); } void PageView::delayedResizeEvent() { // If we already got here we don't need to execute the timer slot again d->delayResizeEventTimer->stop(); slotRelayoutPages(); slotRequestVisiblePixmaps(); } static void slotRequestPreloadPixmap( Okular::DocumentObserver * observer, const PageViewItem * i, const QRect &expandedViewportRect, QLinkedList< Okular::PixmapRequest * > *requestedPixmaps ) { Okular::NormalizedRect preRenderRegion; const QRect intersectionRect = expandedViewportRect.intersected( i->croppedGeometry() ); if ( !intersectionRect.isEmpty() ) preRenderRegion = Okular::NormalizedRect( intersectionRect.translated( -i->uncroppedGeometry().topLeft() ), i->uncroppedWidth(), i->uncroppedHeight() ); // request the pixmap if not already present if ( !i->page()->hasPixmap( observer, i->uncroppedWidth(), i->uncroppedHeight(), preRenderRegion ) && i->uncroppedWidth() > 0 ) { Okular::PixmapRequest::PixmapRequestFeatures requestFeatures = Okular::PixmapRequest::Preload; requestFeatures |= Okular::PixmapRequest::Asynchronous; const bool pageHasTilesManager = i->page()->hasTilesManager( observer ); if ( pageHasTilesManager && !preRenderRegion.isNull() ) { Okular::PixmapRequest * p = new Okular::PixmapRequest( observer, i->pageNumber(), i->uncroppedWidth(), i->uncroppedHeight(), PAGEVIEW_PRELOAD_PRIO, requestFeatures ); requestedPixmaps->push_back( p ); p->setNormalizedRect( preRenderRegion ); p->setTile( true ); } else if ( !pageHasTilesManager ) { Okular::PixmapRequest * p = new Okular::PixmapRequest( observer, i->pageNumber(), i->uncroppedWidth(), i->uncroppedHeight(), PAGEVIEW_PRELOAD_PRIO, requestFeatures ); requestedPixmaps->push_back( p ); p->setNormalizedRect( preRenderRegion ); } } } void PageView::slotRequestVisiblePixmaps( int newValue ) { // if requests are blocked (because raised by an unwanted event), exit if ( d->blockPixmapsRequest || d->scroller->state() == QScroller::Scrolling) return; // precalc view limits for intersecting with page coords inside the loop const bool isEvent = newValue != -1 && !d->blockViewport; const QRect viewportRect( horizontalScrollBar()->value(), verticalScrollBar()->value(), viewport()->width(), viewport()->height() ); const QRect viewportRectAtZeroZero( 0, 0, viewport()->width(), viewport()->height() ); // some variables used to determine the viewport int nearPageNumber = -1; const double viewportCenterX = (viewportRect.left() + viewportRect.right()) / 2.0; const double viewportCenterY = (viewportRect.top() + viewportRect.bottom()) / 2.0; double focusedX = 0.5, focusedY = 0.0, minDistance = -1.0; // Margin (in pixels) around the viewport to preload const int pixelsToExpand = 512; // iterate over all items d->visibleItems.clear(); QLinkedList< Okular::PixmapRequest * > requestedPixmaps; QVector< Okular::VisiblePageRect * > visibleRects; for ( PageViewItem * i : qAsConst( d->items ) ) { const QSet formWidgetsList = i->formWidgets(); for ( FormWidgetIface *fwi : formWidgetsList) { Okular::NormalizedRect r = fwi->rect(); fwi->moveTo( qRound( i->uncroppedGeometry().left() + i->uncroppedWidth() * r.left ) + 1 - viewportRect.left(), qRound( i->uncroppedGeometry().top() + i->uncroppedHeight() * r.top ) + 1 - viewportRect.top() ); } const QHash videoWidgets = i->videoWidgets(); for ( VideoWidget *vw : videoWidgets ) { const Okular::NormalizedRect r = vw->normGeometry(); vw->move( qRound( i->uncroppedGeometry().left() + i->uncroppedWidth() * r.left ) + 1 - viewportRect.left(), qRound( i->uncroppedGeometry().top() + i->uncroppedHeight() * r.top ) + 1 - viewportRect.top() ); if ( vw->isPlaying() && viewportRectAtZeroZero.intersected( vw->geometry() ).isEmpty() ) { vw->stop(); vw->pageLeft(); } } if ( !i->isVisible() ) continue; #ifdef PAGEVIEW_DEBUG kWarning() << "checking page" << i->pageNumber(); kWarning().nospace() << "viewportRect is " << viewportRect << ", page item is " << i->croppedGeometry() << " intersect : " << viewportRect.intersects( i->croppedGeometry() ); #endif // if the item doesn't intersect the viewport, skip it QRect intersectionRect = viewportRect.intersected( i->croppedGeometry() ); if ( intersectionRect.isEmpty() ) { continue; } // add the item to the 'visible list' d->visibleItems.push_back( i ); Okular::VisiblePageRect * vItem = new Okular::VisiblePageRect( i->pageNumber(), Okular::NormalizedRect( intersectionRect.translated( -i->uncroppedGeometry().topLeft() ), i->uncroppedWidth(), i->uncroppedHeight() ) ); visibleRects.push_back( vItem ); #ifdef PAGEVIEW_DEBUG kWarning() << "checking for pixmap for page" << i->pageNumber() << "=" << i->page()->hasPixmap( this, i->uncroppedWidth(), i->uncroppedHeight() ); kWarning() << "checking for text for page" << i->pageNumber() << "=" << i->page()->hasTextPage(); #endif Okular::NormalizedRect expandedVisibleRect = vItem->rect; if ( i->page()->hasTilesManager( this ) && Okular::Settings::memoryLevel() != Okular::Settings::EnumMemoryLevel::Low ) { double rectMargin = pixelsToExpand/(double)i->uncroppedHeight(); expandedVisibleRect.left = qMax( 0.0, vItem->rect.left - rectMargin ); expandedVisibleRect.top = qMax( 0.0, vItem->rect.top - rectMargin ); expandedVisibleRect.right = qMin( 1.0, vItem->rect.right + rectMargin ); expandedVisibleRect.bottom = qMin( 1.0, vItem->rect.bottom + rectMargin ); } // if the item has not the right pixmap, add a request for it if ( !i->page()->hasPixmap( this, i->uncroppedWidth(), i->uncroppedHeight(), expandedVisibleRect ) ) { #ifdef PAGEVIEW_DEBUG kWarning() << "rerequesting visible pixmaps for page" << i->pageNumber() << "!"; #endif Okular::PixmapRequest * p = new Okular::PixmapRequest( this, i->pageNumber(), i->uncroppedWidth(), i->uncroppedHeight(), PAGEVIEW_PRIO, Okular::PixmapRequest::Asynchronous ); requestedPixmaps.push_back( p ); if ( i->page()->hasTilesManager( this ) ) { p->setNormalizedRect( expandedVisibleRect ); p->setTile( true ); } else p->setNormalizedRect( vItem->rect ); } // look for the item closest to viewport center and the relative // position between the item and the viewport center if ( isEvent ) { const QRect & geometry = i->croppedGeometry(); // compute distance between item center and viewport center (slightly moved left) const double distance = hypot( (geometry.left() + geometry.right()) / 2.0 - (viewportCenterX - 4), (geometry.top() + geometry.bottom()) / 2.0 - viewportCenterY ); if ( distance >= minDistance && nearPageNumber != -1 ) continue; nearPageNumber = i->pageNumber(); minDistance = distance; if ( geometry.height() > 0 && geometry.width() > 0 ) { focusedX = ( viewportCenterX - (double)geometry.left() ) / (double)geometry.width(); focusedY = ( viewportCenterY - (double)geometry.top() ) / (double)geometry.height(); } } } // if preloading is enabled, add the pages before and after in preloading if ( !d->visibleItems.isEmpty() && Okular::SettingsCore::memoryLevel() != Okular::SettingsCore::EnumMemoryLevel::Low ) { // as the requests are done in the order as they appear in the list, // request first the next page and then the previous int pagesToPreload = viewColumns(); // if the greedy option is set, preload all pages if (Okular::SettingsCore::memoryLevel() == Okular::SettingsCore::EnumMemoryLevel::Greedy) pagesToPreload = d->items.count(); const QRect expandedViewportRect = viewportRect.adjusted( 0, -pixelsToExpand, 0, pixelsToExpand ); for( int j = 1; j <= pagesToPreload; j++ ) { // add the page after the 'visible series' in preload const int tailRequest = d->visibleItems.last()->pageNumber() + j; if ( tailRequest < (int)d->items.count() ) { slotRequestPreloadPixmap( this, d->items[ tailRequest ], expandedViewportRect, &requestedPixmaps ); } // add the page before the 'visible series' in preload const int headRequest = d->visibleItems.first()->pageNumber() - j; if ( headRequest >= 0 ) { slotRequestPreloadPixmap( this, d->items[ headRequest ], expandedViewportRect, &requestedPixmaps ); } // stop if we've already reached both ends of the document if ( headRequest < 0 && tailRequest >= (int)d->items.count() ) break; } } // send requests to the document if ( !requestedPixmaps.isEmpty() ) { d->document->requestPixmaps( requestedPixmaps ); } // if this functions was invoked by viewport events, send update to document if ( isEvent && nearPageNumber != -1 ) { // determine the document viewport Okular::DocumentViewport newViewport( nearPageNumber ); newViewport.rePos.enabled = true; newViewport.rePos.normalizedX = focusedX; newViewport.rePos.normalizedY = focusedY; // set the viewport to other observers d->document->setViewport( newViewport , this ); } d->document->setVisiblePageRects( visibleRects, this ); } void PageView::slotAutoScroll() { // the first time create the timer if ( !d->autoScrollTimer ) { d->autoScrollTimer = new QTimer( this ); d->autoScrollTimer->setSingleShot( true ); connect( d->autoScrollTimer, &QTimer::timeout, this, &PageView::slotAutoScroll ); } // if scrollIncrement is zero, stop the timer if ( !d->scrollIncrement ) { d->autoScrollTimer->stop(); return; } // compute delay between timer ticks and scroll amount per tick int index = abs( d->scrollIncrement ) - 1; // 0..9 const int scrollDelay[10] = { 200, 100, 50, 30, 20, 30, 25, 20, 30, 20 }; const int scrollOffset[10] = { 1, 1, 1, 1, 1, 2, 2, 2, 4, 4 }; d->autoScrollTimer->start( scrollDelay[ index ] ); int delta = d->scrollIncrement > 0 ? scrollOffset[ index ] : -scrollOffset[ index ]; d->scroller->scrollTo(d->scroller->finalPosition() + QPoint(0, delta), scrollDelay[ index ]); } void PageView::slotDragScroll() { scrollTo( horizontalScrollBar()->value() + d->dragScrollVector.x(), verticalScrollBar()->value() + d->dragScrollVector.y() ); QPoint p = contentAreaPosition() + viewport()->mapFromGlobal( QCursor::pos() ); updateSelection( p ); } void PageView::slotShowWelcome() { // show initial welcome text d->messageWindow->display( i18n( "Welcome" ), QString(), PageViewMessage::Info, 2000 ); } void PageView::slotShowSizeAllCursor() { setCursor( Qt::SizeAllCursor ); } void PageView::slotHandleWebShortcutAction() { QAction *action = qobject_cast( sender() ); if (action) { KUriFilterData filterData( action->data().toString() ); if ( KUriFilter::self()->filterSearchUri( filterData, KUriFilter::WebShortcutFilter ) ) { QDesktopServices::openUrl( filterData.uri() ); } } } void PageView::slotConfigureWebShortcuts() { KToolInvocation::kdeinitExec( QStringLiteral("kcmshell5"), QStringList() << QStringLiteral("webshortcuts") ); } void PageView::slotZoom() { if ( !d->aZoom->selectableActionGroup()->isEnabled() ) return; setFocus(); updateZoom( ZoomFixed ); } void PageView::slotZoomIn() { updateZoom( ZoomIn ); } void PageView::slotZoomOut() { updateZoom( ZoomOut ); } void PageView::slotZoomActual() { updateZoom( ZoomActual ); } void PageView::slotFitToWidthToggled( bool on ) { if ( on ) updateZoom( ZoomFitWidth ); } void PageView::slotFitToPageToggled( bool on ) { if ( on ) updateZoom( ZoomFitPage ); } void PageView::slotAutoFitToggled( bool on ) { if ( on ) updateZoom( ZoomFitAuto ); } void PageView::slotViewMode( QAction *action ) { const int nr = action->data().toInt(); if ( (int)Okular::Settings::viewMode() != nr ) { Okular::Settings::setViewMode( nr ); Okular::Settings::self()->save(); if ( d->document->pages() > 0 ) slotRelayoutPages(); } } void PageView::slotContinuousToggled( bool on ) { if ( Okular::Settings::viewContinuous() != on ) { Okular::Settings::setViewContinuous( on ); Okular::Settings::self()->save(); if ( d->document->pages() > 0 ) slotRelayoutPages(); } } void PageView::slotSetMouseNormal() { d->mouseMode = Okular::Settings::EnumMouseMode::Browse; Okular::Settings::setMouseMode( d->mouseMode ); // hide the messageWindow d->messageWindow->hide(); // reshow the annotator toolbar if hiding was forced (and if it is not already visible) if ( d->annotator && d->annotator->hidingWasForced() && d->aToggleAnnotator && !d->aToggleAnnotator->isChecked() ) d->aToggleAnnotator->trigger(); // force an update of the cursor updateCursor(); Okular::Settings::self()->save(); } void PageView::slotSetMouseZoom() { d->mouseMode = Okular::Settings::EnumMouseMode::Zoom; Okular::Settings::setMouseMode( d->mouseMode ); // change the text in messageWindow (and show it if hidden) d->messageWindow->display( i18n( "Select zooming area. Right-click to zoom out." ), QString(), PageViewMessage::Info, -1 ); // force hiding of annotator toolbar if ( d->aToggleAnnotator && d->aToggleAnnotator->isChecked() ) { d->aToggleAnnotator->trigger(); d->annotator->setHidingForced( true ); } // force an update of the cursor updateCursor(); Okular::Settings::self()->save(); } void PageView::slotSetMouseMagnifier() { d->mouseMode = Okular::Settings::EnumMouseMode::Magnifier; Okular::Settings::setMouseMode( d->mouseMode ); d->messageWindow->display( i18n( "Click to see the magnified view." ), QString() ); // force an update of the cursor updateCursor(); Okular::Settings::self()->save(); } void PageView::slotSetMouseSelect() { d->mouseMode = Okular::Settings::EnumMouseMode::RectSelect; Okular::Settings::setMouseMode( d->mouseMode ); // change the text in messageWindow (and show it if hidden) d->messageWindow->display( i18n( "Draw a rectangle around the text/graphics to copy." ), QString(), PageViewMessage::Info, -1 ); // force hiding of annotator toolbar if ( d->aToggleAnnotator && d->aToggleAnnotator->isChecked() ) { d->aToggleAnnotator->trigger(); d->annotator->setHidingForced( true ); } // force an update of the cursor updateCursor(); Okular::Settings::self()->save(); } void PageView::slotSetMouseTextSelect() { d->mouseMode = Okular::Settings::EnumMouseMode::TextSelect; Okular::Settings::setMouseMode( d->mouseMode ); // change the text in messageWindow (and show it if hidden) d->messageWindow->display( i18n( "Select text" ), QString(), PageViewMessage::Info, -1 ); // force hiding of annotator toolbar if ( d->aToggleAnnotator && d->aToggleAnnotator->isChecked() ) { d->aToggleAnnotator->trigger(); d->annotator->setHidingForced( true ); } // force an update of the cursor updateCursor(); Okular::Settings::self()->save(); } void PageView::slotSetMouseTableSelect() { d->mouseMode = Okular::Settings::EnumMouseMode::TableSelect; Okular::Settings::setMouseMode( d->mouseMode ); // change the text in messageWindow (and show it if hidden) d->messageWindow->display( i18n( "Draw a rectangle around the table, then click near edges to divide up; press Esc to clear." ), QString(), PageViewMessage::Info, -1 ); // force hiding of annotator toolbar if ( d->aToggleAnnotator && d->aToggleAnnotator->isChecked() ) { d->aToggleAnnotator->trigger(); d->annotator->setHidingForced( true ); } // force an update of the cursor updateCursor(); Okular::Settings::self()->save(); } void PageView::slotToggleAnnotator( bool on ) { // the 'inHere' trick is needed as the slotSetMouseZoom() calls this static bool inHere = false; if ( inHere ) return; inHere = true; // the annotator can be used in normal mouse mode only, so if asked for it, // switch to normal mode if ( on && d->mouseMode != Okular::Settings::EnumMouseMode::Browse ) d->aMouseNormal->trigger(); // ask for Author's name if not already set if ( Okular::Settings::identityAuthor().isEmpty() ) { // get default username from the kdelibs/kdecore/KUser KUser currentUser; QString userName = currentUser.property( KUser::FullName ).toString(); // ask the user for confirmation/change if ( userName.isEmpty() ) { bool ok = false; userName = QInputDialog::getText(nullptr, i18n( "Annotations author" ), i18n( "Please insert your name or initials:" ), QLineEdit::Normal, QString(), &ok ); if ( !ok ) { d->aToggleAnnotator->trigger(); inHere = false; return; } } // save the name Okular::Settings::setIdentityAuthor( userName ); Okular::Settings::self()->save(); } // create the annotator object if not present if ( !d->annotator ) { d->annotator = new PageViewAnnotator( this, d->document ); bool allowTools = d->document->pages() > 0 && d->document->isAllowed( Okular::AllowNotes ); d->annotator->setToolsEnabled( allowTools ); d->annotator->setTextToolsEnabled( allowTools && d->document->supportsSearching() ); } // initialize/reset annotator (and show/hide toolbar) d->annotator->setEnabled( on ); d->annotator->setHidingForced( false ); inHere = false; } void PageView::slotAutoScrollUp() { if ( d->scrollIncrement < -9 ) return; d->scrollIncrement--; slotAutoScroll(); setFocus(); } void PageView::slotAutoScrollDown() { if ( d->scrollIncrement > 9 ) return; d->scrollIncrement++; slotAutoScroll(); setFocus(); } void PageView::slotScrollUp( int nSteps ) { //if we are too far behind the animation, do nothing and let it catch up auto limit_value = nSteps ? 200 : verticalScrollBar()->rect().height(); if(d->scroller->state() == QScroller::Scrolling && abs(d->scroller->finalPosition().y() - verticalScrollBar()->value()) > limit_value){ return; } // if in single page mode and at the top of the screen, go to \ page if ( Okular::Settings::viewContinuous() || verticalScrollBar()->value() > verticalScrollBar()->minimum() ) { if ( nSteps ){ d->scroller->scrollTo(d->scroller->finalPosition() + QPoint(0,-100*nSteps), 100); }else{ if(d->scroller->finalPosition().y() > verticalScrollBar()->minimum()) d->scroller->scrollTo(d->scroller->finalPosition() + QPoint(0, -verticalScrollBar()->rect().height() )); } } else if ( d->document->currentPage() > 0 ) { // more optimized than document->setPrevPage and then move view to bottom Okular::DocumentViewport newViewport = d->document->viewport(); newViewport.pageNumber -= viewColumns(); if ( newViewport.pageNumber < 0 ) newViewport.pageNumber = 0; newViewport.rePos.enabled = true; newViewport.rePos.normalizedY = 1.0; d->document->setViewport( newViewport ); } } void PageView::slotScrollDown( int nSteps ) { //if we are too far behind the animation, do nothing and let it catch up auto limit_value = nSteps ? 200 : verticalScrollBar()->rect().height(); if(d->scroller->state() == QScroller::Scrolling && abs(d->scroller->finalPosition().y() - verticalScrollBar()->value()) > limit_value){ return; } // if in single page mode and at the bottom of the screen, go to next page if ( Okular::Settings::viewContinuous() || verticalScrollBar()->value() < verticalScrollBar()->maximum() ) { if ( nSteps ){ d->scroller->scrollTo(d->scroller->finalPosition() + QPoint(0,100*nSteps), 100); }else{ if(d->scroller->finalPosition().y() < verticalScrollBar()->maximum()) d->scroller->scrollTo(d->scroller->finalPosition() + QPoint(0, verticalScrollBar()->rect().height() )); } } else if ( (int)d->document->currentPage() < d->items.count() - 1 ) { // more optimized than document->setNextPage and then move view to top Okular::DocumentViewport newViewport = d->document->viewport(); newViewport.pageNumber += viewColumns(); if ( newViewport.pageNumber >= (int)d->items.count() ) newViewport.pageNumber = d->items.count() - 1; newViewport.rePos.enabled = true; newViewport.rePos.normalizedY = 0.0; d->document->setViewport( newViewport ); } } void PageView::slotRotateClockwise() { int id = ( (int)d->document->rotation() + 1 ) % 4; d->document->setRotation( id ); } void PageView::slotRotateCounterClockwise() { int id = ( (int)d->document->rotation() + 3 ) % 4; d->document->setRotation( id ); } void PageView::slotRotateOriginal() { d->document->setRotation( 0 ); } void PageView::slotPageSizes( int newsize ) { if ( newsize < 0 || newsize >= d->document->pageSizes().count() ) return; d->document->setPageSize( d->document->pageSizes().at( newsize ) ); } // Enforce mutual-exclusion between trim modes // Each mode is uniquely identified by a single value // From Okular::Settings::EnumTrimMode void PageView::updateTrimMode( int except_id ) { const QList trimModeActions = d->aTrimMode->menu()->actions(); for (QAction *trimModeAction : trimModeActions) { if (trimModeAction->data().toInt() != except_id) trimModeAction->setChecked( false ); } } bool PageView::mouseReleaseOverLink( const Okular::ObjectRect * rect ) const { if ( rect ) { // handle click over a link const Okular::Action * action = static_cast< const Okular::Action * >( rect->object() ); d->document->processAction( action ); return true; } return false; } void PageView::slotTrimMarginsToggled( bool on ) { if (on) { // Turn off any other Trim modes updateTrimMode(d->aTrimMargins->data().toInt()); } if ( Okular::Settings::trimMargins() != on ) { Okular::Settings::setTrimMargins( on ); Okular::Settings::self()->save(); if ( d->document->pages() > 0 ) { slotRelayoutPages(); slotRequestVisiblePixmaps(); // TODO: slotRelayoutPages() may have done this already! } } } void PageView::slotTrimToSelectionToggled( bool on ) { if ( on ) { // Turn off any other Trim modes updateTrimMode(d->aTrimToSelection->data().toInt()); d->mouseMode = Okular::Settings::EnumMouseMode::TrimSelect; // change the text in messageWindow (and show it if hidden) d->messageWindow->display( i18n( "Draw a rectangle around the page area you wish to keep visible" ), QString(), PageViewMessage::Info, -1 ); // force hiding of annotator toolbar if ( d->aToggleAnnotator && d->aToggleAnnotator->isChecked() ) { d->aToggleAnnotator->trigger(); d->annotator->setHidingForced( true ); } // force an update of the cursor updateCursor(); } else { // toggled off while making selection if ( Okular::Settings::EnumMouseMode::TrimSelect == d->mouseMode ) { // clear widget selection and invalidate rect selectionClear(); // When Trim selection bbox interaction is over, we should switch to another mousemode. if ( d->aPrevAction ) { d->aPrevAction->trigger(); d->aPrevAction = nullptr; } else { d->aMouseNormal->trigger(); } } d->trimBoundingBox = Okular::NormalizedRect(); // invalidate box if ( d->document->pages() > 0 ) { slotRelayoutPages(); slotRequestVisiblePixmaps(); // TODO: slotRelayoutPages() may have done this already! } } } void PageView::slotToggleForms() { toggleFormWidgets( !d->m_formsVisible ); } void PageView::slotFormChanged( int pageNumber ) { if ( !d->refreshTimer ) { d->refreshTimer = new QTimer( this ); d->refreshTimer->setSingleShot( true ); connect( d->refreshTimer, &QTimer::timeout, this, &PageView::slotRefreshPage ); } d->refreshPages << pageNumber; int delay = 0; if ( d->m_formsVisible ) { delay = 1000; } d->refreshTimer->start( delay ); } void PageView::slotRefreshPage() { for (int req : qAsConst(d->refreshPages)) { QTimer::singleShot(0, this, [this, req] { d->document->refreshPixmaps(req); }); } d->refreshPages.clear(); } #ifdef HAVE_SPEECH void PageView::slotSpeakDocument() { QString text; for ( const PageViewItem * item : qAsConst( d->items ) ) { Okular::RegularAreaRect * area = textSelectionForItem( item ); text.append( item->page()->text( area ) ); text.append( '\n' ); delete area; } d->tts()->say( text ); } void PageView::slotSpeakCurrentPage() { const int currentPage = d->document->viewport().pageNumber; PageViewItem *item = d->items.at( currentPage ); Okular::RegularAreaRect * area = textSelectionForItem( item ); const QString text = item->page()->text( area ); delete area; d->tts()->say( text ); } void PageView::slotStopSpeaks() { if ( !d->m_tts ) return; d->m_tts->stopAllSpeechs(); } void PageView::slotPauseResumeSpeech() { if ( !d->m_tts ) return; d->m_tts->pauseResumeSpeech(); } #endif void PageView::slotAction( Okular::Action *action ) { d->document->processAction( action ); } void PageView::externalKeyPressEvent( QKeyEvent *e ) { keyPressEvent( e ); } void PageView::slotProcessMovieAction( const Okular::MovieAction *action ) { const Okular::MovieAnnotation *movieAnnotation = action->annotation(); if ( !movieAnnotation ) return; Okular::Movie *movie = movieAnnotation->movie(); if ( !movie ) return; const int currentPage = d->document->viewport().pageNumber; PageViewItem *item = d->items.at( currentPage ); if ( !item ) return; VideoWidget *vw = item->videoWidgets().value( movie ); if ( !vw ) return; vw->show(); switch ( action->operation() ) { case Okular::MovieAction::Play: vw->stop(); vw->play(); break; case Okular::MovieAction::Stop: vw->stop(); break; case Okular::MovieAction::Pause: vw->pause(); break; case Okular::MovieAction::Resume: vw->play(); break; }; } void PageView::slotProcessRenditionAction( const Okular::RenditionAction *action ) { Okular::Movie *movie = action->movie(); if ( !movie ) return; const int currentPage = d->document->viewport().pageNumber; PageViewItem *item = d->items.at( currentPage ); if ( !item ) return; VideoWidget *vw = item->videoWidgets().value( movie ); if ( !vw ) return; if ( action->operation() == Okular::RenditionAction::None ) return; vw->show(); switch ( action->operation() ) { case Okular::RenditionAction::Play: vw->stop(); vw->play(); break; case Okular::RenditionAction::Stop: vw->stop(); break; case Okular::RenditionAction::Pause: vw->pause(); break; case Okular::RenditionAction::Resume: vw->play(); break; default: return; }; } void PageView::slotSetChangeColors(bool active) { Okular::SettingsCore::setChangeColors(active); Okular::Settings::self()->save(); viewport()->update(); } void PageView::slotToggleChangeColors() { slotSetChangeColors( !Okular::SettingsCore::changeColors() ); } void PageView::slotFitWindowToPage() { const PageViewItem *currentPageItem = nullptr; QSize viewportSize = viewport()->size(); for ( const PageViewItem *pageItem : qAsConst(d->items) ) { if ( pageItem->isVisible() ) { currentPageItem = pageItem; break; } } if ( !currentPageItem ) return; const QSize pageSize = QSize( currentPageItem->uncroppedWidth() + kcolWidthMargin, currentPageItem->uncroppedHeight() + krowHeightMargin ); if ( verticalScrollBar()->isVisible() ) viewportSize.setWidth( viewportSize.width() + verticalScrollBar()->width() ); if ( horizontalScrollBar()->isVisible() ) viewportSize.setHeight( viewportSize.height() + horizontalScrollBar()->height() ); emit fitWindowToPage( viewportSize, pageSize ); } void PageView::slotSelectPage() { textSelectionClear(); const int currentPage = d->document->viewport().pageNumber; PageViewItem *item = d->items.at( currentPage ); if ( item ) { Okular::RegularAreaRect * area = textSelectionForItem( item ); const QString text = item->page()->text( area ); d->pagesWithTextSelection.insert( currentPage ); d->document->setPageTextSelection( currentPage, area, palette().color( QPalette::Active, QPalette::Highlight ) ); } } void PageView::highlightSignatureFormWidget( const Okular::FormFieldSignature *form ) { QVector< PageViewItem * >::const_iterator dIt = d->items.constBegin(), dEnd = d->items.constEnd(); for ( ; dIt != dEnd; ++dIt ) { const QSet fwi = (*dIt)->formWidgets(); for ( FormWidgetIface *fw : fwi ) { if ( fw->formField() == form ) { SignatureEdit *widget = static_cast< SignatureEdit * >( fw ); widget->setDummyMode( true ); QTimer::singleShot( 250, this, [=]{ widget->setDummyMode( false ); }); return; } } } } //END private SLOTS /* kate: replace-tabs on; indent-width 4; */