From 12b2f446190e2d57362d8fe994e1075728ca9aee Mon Sep 17 00:00:00 2001 From: Jakub Melka Date: Sun, 26 Jan 2020 17:06:50 +0100 Subject: [PATCH] Text selection tool - finishing (without copying text) --- PdfForQtLib/sources/pdfcompiler.cpp | 52 +++++++++++ PdfForQtLib/sources/pdfcompiler.h | 4 + PdfForQtLib/sources/pdftextlayout.cpp | 121 +++++++++++++++++++++++++ PdfForQtLib/sources/pdftextlayout.h | 17 +++- PdfForQtLib/sources/pdfwidgettool.cpp | 78 ++++++++++++---- PdfForQtLib/sources/pdfwidgettool.h | 17 +++- PdfForQtViewer/pdfviewermainwindow.cpp | 10 ++ PdfForQtViewer/pdfviewermainwindow.h | 1 + PdfForQtViewer/pdfviewermainwindow.ui | 6 ++ 9 files changed, 286 insertions(+), 20 deletions(-) diff --git a/PdfForQtLib/sources/pdfcompiler.cpp b/PdfForQtLib/sources/pdfcompiler.cpp index 7e8afa8..5db7e6f 100644 --- a/PdfForQtLib/sources/pdfcompiler.cpp +++ b/PdfForQtLib/sources/pdfcompiler.cpp @@ -355,6 +355,58 @@ PDFTextLayoutGetter PDFAsynchronousTextLayoutCompiler::getTextLayoutLazy(PDFInte return PDFTextLayoutGetter(nullptr, pageIndex); } +PDFTextSelection PDFAsynchronousTextLayoutCompiler::getTextSelectionAll(QColor color) const +{ + PDFTextSelection result; + + if (m_textLayouts) + { + const PDFTextLayoutStorage& textLayouts = *m_textLayouts; + + QMutex mutex; + PDFIntegerRange pageRange(0, textLayouts.getCount()); + auto selectPageText = [this, &mutex, &textLayouts, &result, color](PDFInteger pageIndex) + { + PDFTextLayout textLayout = textLayouts.getTextLayout(pageIndex); + PDFTextSelectionItems items; + + const PDFTextBlocks& blocks = textLayout.getTextBlocks(); + for (size_t blockId = 0, blockCount = blocks.size(); blockId < blockCount; ++blockId) + { + const PDFTextBlock& block = blocks[blockId]; + const PDFTextLines& lines = block.getLines(); + + if (!lines.empty()) + { + const PDFTextLine& lastLine = lines.back(); + Q_ASSERT(!lastLine.getCharacters().empty()); + + PDFCharacterPointer ptrStart; + ptrStart.pageIndex = pageIndex; + ptrStart.blockIndex = blockId; + ptrStart.lineIndex = 0; + ptrStart.characterIndex = 0; + + PDFCharacterPointer ptrEnd; + ptrEnd.pageIndex = pageIndex; + ptrEnd.blockIndex = blockId; + ptrEnd.lineIndex = lines.size() - 1; + ptrEnd.characterIndex = lastLine.getCharacters().size() - 1; + + items.emplace_back(ptrStart, ptrEnd); + } + } + + QMutexLocker lock(&mutex); + result.addItems(qMove(items), color); + }; + PDFExecutionPolicy::execute(PDFExecutionPolicy::Scope::Page, pageRange.begin(), pageRange.end(), selectPageText); + } + + result.build(); + return result; +} + void PDFAsynchronousTextLayoutCompiler::makeTextLayout() { if (m_state != State::Active || !m_proxy->getDocument()) diff --git a/PdfForQtLib/sources/pdfcompiler.h b/PdfForQtLib/sources/pdfcompiler.h index 9eac145..0905ffb 100644 --- a/PdfForQtLib/sources/pdfcompiler.h +++ b/PdfForQtLib/sources/pdfcompiler.h @@ -130,6 +130,10 @@ public: /// \param pageIndex Page index PDFTextLayoutGetter getTextLayoutLazy(PDFInteger pageIndex); + /// Select all texts on all pages using \p color color. + /// \param color Color to be used for text selection + PDFTextSelection getTextSelectionAll(QColor color) const; + /// Create text layout for the document. Function is asynchronous, /// it returns immediately. After text layout is created, signal /// \p textLayoutChanged is emitted. diff --git a/PdfForQtLib/sources/pdftextlayout.cpp b/PdfForQtLib/sources/pdftextlayout.cpp index 1e22c2c..58263ac 100644 --- a/PdfForQtLib/sources/pdftextlayout.cpp +++ b/PdfForQtLib/sources/pdftextlayout.cpp @@ -362,6 +362,127 @@ bool PDFTextLayout::isHoveringOverTextBlock(const QPointF& point) const return false; } +PDFTextSelection PDFTextLayout::createTextSelection(PDFInteger pageIndex, const QPointF& point1, const QPointF& point2) +{ + PDFTextSelection selection; + + // Jakub Melka: We must treat each block in its own coordinate system. Because texts can + // have different angles, we will treat each block separately. + + size_t blockId = 0; + for (PDFTextBlock& block : m_blocks) + { + QMatrix angleMatrix; + angleMatrix.rotate(block.getAngle()); + block.applyTransform(angleMatrix); + + QPointF pointA = angleMatrix.map(point1); + QPointF pointB = angleMatrix.map(point2); + + const qreal xMin = qMin(pointA.x(), pointB.x()); + const qreal yMin = qMin(pointA.y(), pointB.y()); + const qreal xMax = qMax(pointA.x(), pointB.x()); + const qreal yMax = qMax(pointA.y(), pointB.y()); + + QRectF rect(xMin, yMin, xMax - xMin, yMax - yMin); + QPainterPath rectPath; + rectPath.addRect(rect); + QPainterPath intersectionPath = block.getBoundingBox().intersected(rectPath); + if (!intersectionPath.isEmpty()) + { + QRectF intersectionRect = intersectionPath.boundingRect(); + Q_ASSERT(intersectionRect.isValid()); + + const PDFTextLines& lines = block.getLines(); + auto itLineA = std::find_if(lines.cbegin(), lines.cend(), [pointA](const PDFTextLine& line) { return line.getBoundingBox().contains(pointA); }); + auto itLineB = std::find_if(lines.cbegin(), lines.cend(), [pointB](const PDFTextLine& line) { return line.getBoundingBox().contains(pointB); }); + if (itLineA == itLineB && itLineA != lines.cend()) + { + // Both points are in the same line. We consider point with lesser + // horizontal coordinate as start selection point, and point with greater + // horizontal coordinate as end selection point. + if (pointA.x() > pointB.x()) + { + std::swap(pointA, pointB); + } + } + else + { + // Otherwise points are not in the same line. Then start point will be + // point top of the second point. Bottom point will mark end of selection. + if (pointA.y() > pointB.y()) + { + std::swap(pointA, pointB); + } + } + + // Now, we have pointA as start point and pointB as end point. We must found + // nearest character to the right of point A, and nearest character to the + // left of point B (with respect to point A/B). + + qreal maxDistanceA = std::numeric_limits::infinity(); + qreal maxDistanceB = std::numeric_limits::infinity(); + + PDFCharacterPointer ptrA; + PDFCharacterPointer ptrB; + + for (size_t lineId = 0, linesCount = lines.size(); lineId < linesCount; ++lineId) + { + const PDFTextLine& line = lines[lineId]; + const TextCharacters& characters = line.getCharacters(); + for (size_t characterId = 0, characterCount = characters.size(); characterId < characterCount; ++characterId) + { + const TextCharacter& character = characters[characterId]; + QPointF characterCenter = character.boundingBox.boundingRect().center(); + + qreal distanceA = QLineF(pointA, characterCenter).length(); + qreal distanceB = QLineF(pointB, characterCenter).length(); + + if (distanceA < maxDistanceA && characterCenter.x() > pointA.x()) + { + maxDistanceA = distanceA; + ptrA.pageIndex = pageIndex; + ptrA.blockIndex = blockId; + ptrA.lineIndex = lineId; + ptrA.characterIndex = characterId; + } + + if (distanceB < maxDistanceB && characterCenter.x() < pointB.x()) + { + maxDistanceB = distanceB; + ptrB.pageIndex = pageIndex; + ptrB.blockIndex = blockId; + ptrB.lineIndex = lineId; + ptrB.characterIndex = characterId; + } + } + } + + // If we have filled the pointers, add them to the selection + if (ptrA.isValid() && ptrB.isValid()) + { + if (ptrA < ptrB) + { + selection.addItems({ PDFTextSelectionItem(ptrA, ptrB) }, Qt::yellow); + } + else + { + selection.addItems({ PDFTextSelectionItem(ptrB, ptrA) }, Qt::yellow); + } + } + } + + // Increment block index + ++blockId; + + // Apply backward transformation to restore original coordinate system + block.applyTransform(angleMatrix.inverted()); + } + + selection.build(); + return selection; +} + QDataStream& operator>>(QDataStream& stream, PDFTextLayout& layout) { stream >> layout.m_characters; diff --git a/PdfForQtLib/sources/pdftextlayout.h b/PdfForQtLib/sources/pdftextlayout.h index 3691417..1695ac5 100644 --- a/PdfForQtLib/sources/pdftextlayout.h +++ b/PdfForQtLib/sources/pdftextlayout.h @@ -180,7 +180,7 @@ struct PDFCharacterPointer /// Returns true, if character belongs to same line bool hasSameLine(const PDFCharacterPointer& other) const; - int pageIndex = -1; + PDFInteger pageIndex = -1; size_t blockIndex = 0; size_t lineIndex = 0; size_t characterIndex = 0; @@ -324,7 +324,7 @@ public: /// Adds character to the layout void addCharacter(const PDFTextCharacterInfo& info); - /// Perorms text layout algorithm + /// Performs text layout algorithm void perform(); /// Optimizes layout memory allocation to contain less space @@ -339,6 +339,14 @@ public: /// Returns true, if given point is pointing to some text block bool isHoveringOverTextBlock(const QPointF& point) const; + /// Creates text selection. This function needs to modify the layout contents, + /// so do not use this function from multiple threads (it is not thread-safe). + /// Text selection is created from rectangle using two points. + /// \param pageIndex Page index + /// \param point1 First point + /// \param point2 Second point + PDFTextSelection createTextSelection(PDFInteger pageIndex, const QPointF& point1, const QPointF& point2); + friend QDataStream& operator<<(QDataStream& stream, const PDFTextLayout& layout); friend QDataStream& operator>>(QDataStream& stream, PDFTextLayout& layout); @@ -354,7 +362,7 @@ private: /// Applies transform to text characters (positions and bounding boxes) /// \param characters Characters /// \param matrix Transform matrix - void applyTransform(TextCharacters& characters, const QMatrix& matrix); + static void applyTransform(TextCharacters& characters, const QMatrix& matrix); TextCharacters m_characters; std::set m_angles; @@ -458,6 +466,9 @@ public: /// \param flowFlags Text flow flags PDFFindResults find(const QRegularExpression& expression, PDFTextFlow::FlowFlags flowFlags) const; + /// Returns number of pages + size_t getCount() const { return m_offsets.size(); } + private: std::vector m_offsets; QByteArray m_textLayouts; diff --git a/PdfForQtLib/sources/pdfwidgettool.cpp b/PdfForQtLib/sources/pdfwidgettool.cpp index 0446add..737781a 100644 --- a/PdfForQtLib/sources/pdfwidgettool.cpp +++ b/PdfForQtLib/sources/pdfwidgettool.cpp @@ -446,15 +446,32 @@ PDFTextSelection PDFFindTextTool::getTextSelectionImpl() const return result; } -PDFSelectTextTool::PDFSelectTextTool(PDFDrawWidgetProxy* proxy, QAction* action, QAction* selectAllAction, QAction* deselectAction, QObject* parent) : +PDFSelectTextTool::PDFSelectTextTool(PDFDrawWidgetProxy* proxy, QAction* action, QAction* copyTextAction, QAction* selectAllAction, QAction* deselectAction, QObject* parent) : BaseClass(proxy, action, parent), + m_copyTextAction(copyTextAction), m_selectAllAction(selectAllAction), m_deselectAction(deselectAction), m_isCursorOverText(false) { + connect(copyTextAction, &QAction::triggered, this, &PDFSelectTextTool::onActionCopyText); + connect(selectAllAction, &QAction::triggered, this, &PDFSelectTextTool::onActionSelectAll); + connect(deselectAction, &QAction::triggered, this, &PDFSelectTextTool::onActionDeselect); + updateActions(); } +void PDFSelectTextTool::drawPage(QPainter* painter, + PDFInteger pageIndex, + const PDFPrecompiledPage* compiledPage, + PDFTextLayoutGetter& layoutGetter, + const QMatrix& pagePointToDevicePointMatrix) const +{ + Q_UNUSED(compiledPage); + + pdf::PDFTextSelectionPainter textSelectionPainter(&m_textSelection); + textSelectionPainter.draw(painter, pageIndex, layoutGetter, pagePointToDevicePointMatrix); +} + void PDFSelectTextTool::mousePressEvent(QWidget* widget, QMouseEvent* event) { Q_UNUSED(widget); @@ -493,6 +510,8 @@ void PDFSelectTextTool::mouseReleaseEvent(QWidget* widget, QMouseEvent* event) if (m_selectionInfo.pageIndex == pageIndex) { // Jakub Melka: handle the selection + PDFTextLayout textLayout = getProxy()->getTextLayoutCompiler()->getTextLayoutLazy(pageIndex); + setSelection(textLayout.createTextSelection(pageIndex, m_selectionInfo.selectionStartPoint, pagePoint)); } else { @@ -510,26 +529,29 @@ void PDFSelectTextTool::mouseMoveEvent(QWidget* widget, QMouseEvent* event) { Q_UNUSED(widget); + // We must make text layout. This is fast, because text layout is being + // created only, if it doesn't exist. This function is also called only, + // if tool is active. + getProxy()->getTextLayoutCompiler()->makeTextLayout(); + QPointF pagePoint; const PDFInteger pageIndex = getProxy()->getPageUnderPoint(event->pos(), &pagePoint); PDFTextLayout textLayout = getProxy()->getTextLayoutCompiler()->getTextLayoutLazy(pageIndex); m_isCursorOverText = textLayout.isHoveringOverTextBlock(pagePoint); - if (event->button() == Qt::LeftButton) + if (m_selectionInfo.pageIndex != -1) { - if (m_selectionInfo.pageIndex != -1) + if (m_selectionInfo.pageIndex == pageIndex) { - if (m_selectionInfo.pageIndex == pageIndex) - { - // Jakub Melka: handle the selection - } - else - { - setSelection(pdf::PDFTextSelection()); - } - - event->accept(); + // Jakub Melka: handle the selection + setSelection(textLayout.createTextSelection(pageIndex, m_selectionInfo.selectionStartPoint, pagePoint)); } + else + { + setSelection(pdf::PDFTextSelection()); + } + + event->accept(); } updateCursor(); @@ -556,8 +578,11 @@ void PDFSelectTextTool::updateActions() { BaseClass::updateActions(); - m_selectAllAction->setEnabled(isActive()); - m_deselectAction->setEnabled(isActive() && !m_textSelection.isEmpty()); + const bool isActive = this->isActive(); + const bool hasSelection = !m_textSelection.isEmpty(); + m_selectAllAction->setEnabled(isActive); + m_deselectAction->setEnabled(isActive && hasSelection); + m_copyTextAction->setEnabled(isActive && hasSelection); } void PDFSelectTextTool::updateCursor() @@ -575,6 +600,27 @@ void PDFSelectTextTool::updateCursor() } } +void PDFSelectTextTool::onActionCopyText() +{ + +} + +void PDFSelectTextTool::onActionSelectAll() +{ + if (isActive()) + { + setSelection(getProxy()->getTextLayoutCompiler()->getTextSelectionAll(Qt::yellow)); + } +} + +void PDFSelectTextTool::onActionDeselect() +{ + if (isActive()) + { + setSelection(pdf::PDFTextSelection()); + } +} + void PDFSelectTextTool::setSelection(PDFTextSelection&& textSelection) { if (m_textSelection != textSelection) @@ -590,7 +636,7 @@ PDFToolManager::PDFToolManager(PDFDrawWidgetProxy* proxy, Actions actions, QObje m_predefinedTools() { m_predefinedTools[FindTextTool] = new PDFFindTextTool(proxy, actions.findPrevAction, actions.findNextAction, this, parentDialog); - m_predefinedTools[SelectTextTool] = new PDFSelectTextTool(proxy, actions.selectTextToolAction, actions.selectAllAction, actions.deselectAction, this); + m_predefinedTools[SelectTextTool] = new PDFSelectTextTool(proxy, actions.selectTextToolAction, actions.copyTextAction, actions.selectAllAction, actions.deselectAction, this); for (PDFWidgetTool* tool : m_predefinedTools) { diff --git a/PdfForQtLib/sources/pdfwidgettool.h b/PdfForQtLib/sources/pdfwidgettool.h index b0822eb..4ec0860 100644 --- a/PdfForQtLib/sources/pdfwidgettool.h +++ b/PdfForQtLib/sources/pdfwidgettool.h @@ -193,8 +193,18 @@ private: public: /// Construct new text selection tool /// \param proxy Draw widget proxy + /// \param action Tool activation action + /// \param copyTextAction Copy text action + /// \param selectAllAction Select all text action + /// \param deselectAction Deselect text action /// \param parent Parent object - explicit PDFSelectTextTool(PDFDrawWidgetProxy* proxy, QAction* action, QAction* selectAllAction,QAction* deselectAction, QObject* parent); + explicit PDFSelectTextTool(PDFDrawWidgetProxy* proxy, QAction* action, QAction* copyTextAction, QAction* selectAllAction, QAction* deselectAction, QObject* parent); + + virtual void drawPage(QPainter* painter, + PDFInteger pageIndex, + const PDFPrecompiledPage* compiledPage, + PDFTextLayoutGetter& layoutGetter, + const QMatrix& pagePointToDevicePointMatrix) const override; virtual void mousePressEvent(QWidget* widget, QMouseEvent* event) override; virtual void mouseReleaseEvent(QWidget* widget, QMouseEvent* event) override; @@ -206,6 +216,9 @@ protected: private: void updateCursor(); + void onActionCopyText(); + void onActionSelectAll(); + void onActionDeselect(); void setSelection(pdf::PDFTextSelection&& textSelection); struct SelectionInfo @@ -214,6 +227,7 @@ private: QPointF selectionStartPoint; }; + QAction* m_copyTextAction; QAction* m_selectAllAction; QAction* m_deselectAction; pdf::PDFTextSelection m_textSelection; @@ -239,6 +253,7 @@ public: QAction* selectTextToolAction = nullptr; QAction* selectAllAction = nullptr; QAction* deselectAction = nullptr; + QAction* copyTextAction = nullptr; }; /// Construct new text search tool diff --git a/PdfForQtViewer/pdfviewermainwindow.cpp b/PdfForQtViewer/pdfviewermainwindow.cpp index 70971b0..faf1aa2 100644 --- a/PdfForQtViewer/pdfviewermainwindow.cpp +++ b/PdfForQtViewer/pdfviewermainwindow.cpp @@ -82,6 +82,7 @@ PDFViewerMainWindow::PDFViewerMainWindow(QWidget* parent) : m_progressTaskbarIndicator(nullptr), m_progressDialog(nullptr), m_isBusy(false), + m_isChangingProgressStep(false), m_toolManager(nullptr) { ui->setupUi(this); @@ -104,6 +105,7 @@ PDFViewerMainWindow::PDFViewerMainWindow(QWidget* parent) : ui->actionFindNext->setShortcut(QKeySequence::FindNext); ui->actionSelectTextAll->setShortcut(QKeySequence::SelectAll); ui->actionDeselectText->setShortcut(QKeySequence::Deselect); + ui->actionCopyText->setShortcut(QKeySequence::Copy); connect(ui->actionOpen, &QAction::triggered, this, &PDFViewerMainWindow::onActionOpenTriggered); connect(ui->actionClose, &QAction::triggered, this, &PDFViewerMainWindow::onActionCloseTriggered); @@ -229,6 +231,7 @@ PDFViewerMainWindow::PDFViewerMainWindow(QWidget* parent) : actions.selectTextToolAction = ui->actionSelectText; actions.selectAllAction = ui->actionSelectTextAll; actions.deselectAction = ui->actionDeselectText; + actions.copyTextAction = ui->actionCopyText; m_toolManager = new pdf::PDFToolManager(m_pdfWidget->getDrawWidgetProxy(), actions, this, this); m_pdfWidget->setToolManager(m_toolManager); @@ -575,6 +578,13 @@ void PDFViewerMainWindow::onProgressStarted(pdf::ProgressStartupInfo info) void PDFViewerMainWindow::onProgressStep(int percentage) { + if (m_isChangingProgressStep) + { + return; + } + + pdf::PDFTemporaryValueChange guard(&m_isChangingProgressStep, true); + if (m_progressDialog) { m_progressDialog->setValue(percentage); diff --git a/PdfForQtViewer/pdfviewermainwindow.h b/PdfForQtViewer/pdfviewermainwindow.h index 846eb30..a29f3f9 100644 --- a/PdfForQtViewer/pdfviewermainwindow.h +++ b/PdfForQtViewer/pdfviewermainwindow.h @@ -164,6 +164,7 @@ private: QProgressDialog* m_progressDialog; bool m_isBusy; + bool m_isChangingProgressStep; pdf::PDFToolManager* m_toolManager; }; diff --git a/PdfForQtViewer/pdfviewermainwindow.ui b/PdfForQtViewer/pdfviewermainwindow.ui index ebfb30c..c1a5034 100644 --- a/PdfForQtViewer/pdfviewermainwindow.ui +++ b/PdfForQtViewer/pdfviewermainwindow.ui @@ -105,6 +105,7 @@ + @@ -403,6 +404,11 @@ Deselect + + + Copy text + +