PDF4QT/Pdf4QtLib/sources/pdftextlayout.h

583 lines
21 KiB
C++

// Copyright (C) 2019-2021 Jakub Melka
//
// This file is part of PDF4QT.
//
// PDF4QT is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// with the written consent of the copyright owner, any later version.
//
// PDF4QT 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 Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with PDF4QT. If not, see <https://www.gnu.org/licenses/>.
#ifndef PDFTEXTLAYOUT_H
#define PDFTEXTLAYOUT_H
#include "pdfglobal.h"
#include "pdfutils.h"
#include <QColor>
#include <QDataStream>
#include <QPainterPath>
#include <set>
#include <compare>
namespace pdf
{
class PDFTextLayout;
class PDFTextLayoutStorage;
struct PDFTextCharacterInfo
{
/// Character
QChar character;
/// Character path
QPainterPath outline;
/// Do we use a vertical writing system?
bool isVerticalWritingSystem = false;
/// Advance (in character space, it must be translated
/// into device space), for both vertical/horizontal modes.
PDFReal advance = 0.0;
/// Font size (in character space, it must be translated
/// into device space)
PDFReal fontSize = 0.0;
/// Transformation matrix from character space to device space
QTransform matrix;
};
struct PDFTextLayoutSettings
{
/// Number of samples for 'docstrum' algorithm, i.e. number of
/// nearest characters. By default, 5 characters should fit.
size_t samples = 5;
/// Distance sensitivity to determine, if characters are close enough.
/// Maximal distance is computed as current character advance multiplied
/// by this constant.
PDFReal distanceSensitivity = 4.0;
/// Maximal vertical distance, in portion of font size, of two characters
/// to be considered they lie on same line.
PDFReal charactersOnLineSensitivity = 0.25;
/// Maximal ratio between font size of characters to be considered
/// that they lie on same line.
PDFReal fontSensitivity = 2.0;
/// Maximal space ratio between two lines of block. Default coefficient
/// means, that height ratio limit is (height1 + height2)
PDFReal blockVerticalSensitivity = 1.5;
/// Minimal horizontal overlap for two lines considered to be in one block
PDFReal blockOverlapSensitivity = 0.3;
friend QDataStream& operator<<(QDataStream& stream, const PDFTextLayoutSettings& settings);
friend QDataStream& operator>>(QDataStream& stream, PDFTextLayoutSettings& settings);
};
/// Represents character in device space coordinates. All values (dimensions,
/// bounding box, etc. are in device space coordinates).
struct TextCharacter
{
QChar character;
QPointF position;
PDFReal angle = 0.0;
PDFReal fontSize = 0.0;
PDFReal advance = 0.0;
QPainterPath boundingBox;
size_t index = 0; // Just temporary index, it is not serialized, just for text layout algorithm
void applyTransform(const QTransform& matrix);
friend QDataStream& operator<<(QDataStream& stream, const TextCharacter& character);
friend QDataStream& operator>>(QDataStream& stream, TextCharacter& character);
};
using TextCharacters = std::vector<TextCharacter>;
/// Represents text line consisting of set of characters and line bounding box.
class PDFTextLine
{
public:
explicit inline PDFTextLine() = default;
/// Construct new line from characters. Characters are sorted in x-coordinate
/// and bounding box is computed.
/// \param characters
explicit PDFTextLine(TextCharacters characters);
const TextCharacters& getCharacters() const { return m_characters; }
const QPainterPath& getBoundingBox() const { return m_boundingBox; }
const QPointF& getTopLeft() const { return m_topLeft; }
/// Get angle inclination of block
PDFReal getAngle() const;
void applyTransform(const QTransform& matrix);
friend QDataStream& operator<<(QDataStream& stream, const PDFTextLine& line);
friend QDataStream& operator>>(QDataStream& stream, PDFTextLine& line);
private:
TextCharacters m_characters;
QPainterPath m_boundingBox;
QPointF m_topLeft;
};
using PDFTextLines = std::vector<PDFTextLine>;
/// Represents text block consisting of set of lines and block bounding box.
class PDFTextBlock
{
public:
explicit inline PDFTextBlock() = default;
explicit inline PDFTextBlock(PDFTextLines textLines);
const PDFTextLines& getLines() const { return m_lines; }
const QPainterPath& getBoundingBox() const { return m_boundingBox; }
const QPointF& getTopLeft() const { return m_topLeft; }
/// Get angle inclination of block
PDFReal getAngle() const;
void applyTransform(const QTransform& matrix);
friend QDataStream& operator<<(QDataStream& stream, const PDFTextBlock& block);
friend QDataStream& operator>>(QDataStream& stream, PDFTextBlock& block);
private:
PDFTextLines m_lines;
QPainterPath m_boundingBox;
QPointF m_topLeft;
};
using PDFTextBlocks = std::vector<PDFTextBlock>;
/// Character pointer points to some character in text layout.
/// It also has page index to decide, which page the pointer points to.
struct PDFCharacterPointer
{
auto operator<=>(const PDFCharacterPointer&) const = default;
/// Returns true, if character pointer is valid and points to the correct location
bool isValid() const { return pageIndex > -1; }
/// Returns true, if character belongs to same block
bool hasSameBlock(const PDFCharacterPointer& other) const;
/// Returns true, if character belongs to same line
bool hasSameLine(const PDFCharacterPointer& other) const;
PDFInteger pageIndex = -1;
size_t blockIndex = 0;
size_t lineIndex = 0;
size_t characterIndex = 0;
};
using PDFTextSelectionItem = std::pair<PDFCharacterPointer, PDFCharacterPointer>;
using PDFTextSelectionItems = std::vector<PDFTextSelectionItem>;
struct PDFTextSelectionColoredItem
{
explicit inline PDFTextSelectionColoredItem() = default;
explicit inline PDFTextSelectionColoredItem(PDFCharacterPointer start, PDFCharacterPointer end, QColor color) :
start(start),
end(end),
color(color)
{
}
bool operator==(const PDFTextSelectionColoredItem&) const = default;
bool operator!=(const PDFTextSelectionColoredItem&) const = default;
inline bool operator<(const PDFTextSelectionColoredItem& other) const { return std::tie(start, end) < std::tie(other.start, other.end); }
PDFCharacterPointer start;
PDFCharacterPointer end;
QColor color;
};
using PDFTextSelectionColoredItems = std::vector<PDFTextSelectionColoredItem>;
/// Text selection, can be used across multiple pages. Also defines color
/// for each text selection.
class PDF4QTLIBSHARED_EXPORT PDFTextSelection
{
public:
explicit PDFTextSelection() = default;
using iterator = PDFTextSelectionColoredItems::const_iterator;
bool operator==(const PDFTextSelection&) const = default;
bool operator!=(const PDFTextSelection&) const = default;
/// Adds text selection items to selection
/// \param items Items
/// \param color Color for items (must include alpha channel)
void addItems(const PDFTextSelectionItems& items, QColor color);
/// Builds text selection, so it is prepared for rendering. Text selection,
/// which is not build, can't be used for rendering.
void build();
/// Returns iterator to start of page range
iterator begin(PDFInteger pageIndex) const;
/// Returns iterator to end of page range
iterator end(PDFInteger pageIndex) const;
/// Returns iterator to next page range
iterator nextPageRange(iterator currentPageRange) const;
/// Returns true, if text selection is empty
bool isEmpty() const { return m_items.empty(); }
iterator begin() const { return m_items.cbegin(); }
iterator end() const { return m_items.cend(); }
private:
PDFTextSelectionColoredItems m_items;
};
struct PDFFindResult
{
bool operator<(const PDFFindResult& other) const;
/// Matched string during search
QString matched;
/// Context (text before and after match)
QString context;
/// Matched selection (can be multiple items, if selection
/// is spanned between multiple blocks)
PDFTextSelectionItems textSelectionItems;
};
using PDFFindResults = std::vector<PDFFindResult>;
class PDFTextFlow;
using PDFTextFlows = std::vector<PDFTextFlow>;
/// This class represents a portion of continuous text on the page. It can
/// consists of multiple blocks (which follow reading order).
class PDF4QTLIBSHARED_EXPORT PDFTextFlow
{
public:
enum FlowFlag
{
None = 0x0000,
SeparateBlocks = 0x0001, ///< Create flow for each block
RemoveSoftHyphen = 0x0002, ///< Removes 'soft hyphen' unicode character from end-of-line (character 0x00AD)
AddLineBreaks = 0x0004, ///< Add line break characters to the end of line
};
Q_DECLARE_FLAGS(FlowFlags, FlowFlag)
/// Finds simple text in current text flow. All text occurences are returned.
/// \param text Text to be found
/// \param caseSensitivity Case sensitivity
PDFFindResults find(const QString& text, Qt::CaseSensitivity caseSensitivity) const;
/// Finds regular expression matches in current text flow. All text occurences are returned.
/// \param expression Regular expression to be matched
PDFFindResults find(const QRegularExpression& expression) const;
/// Returns whole text for this text flow
QString getText() const { return m_text; }
/// Returns character bounding boxes
std::vector<QRectF> getBoundingBoxes() const { return m_characterBoundingBoxes; }
/// Returns text form character pointers
/// \param begin Begin character
/// \param end End character
QString getText(const PDFCharacterPointer& begin, const PDFCharacterPointer& end) const;
/// Merge data from \p next flow (i.e. connect two consecutive flows)
void merge(const PDFTextFlow& next);
/// Returns bounding box of a text flow on the page
QRectF getBoundingBox() const { return m_boundingBox; }
/// Creates text flows from text layout, according to creation flags.
/// \param layout Layout, from which is text flow created
/// \param flags Flow creation flags
/// \param pageIndex Page index
static PDFTextFlows createTextFlows(const PDFTextLayout& layout, FlowFlags flags, PDFInteger pageIndex);
private:
/// Returns text selection from index and length. Returned text selection can also
/// be empty (for example, if only single space character is selected, which has
/// no counterpart in real text)
/// \param index Index of text selection subrange
/// \param length Length of text selection
PDFTextSelectionItems getTextSelectionItems(size_t index, size_t length) const;
/// Returns context for text selection (or empty string, if text selection is empty)
/// \param index Index of text selection subrange
/// \param length Length of text selection
QString getContext(size_t index, size_t length) const;
QString m_text;
QRectF m_boundingBox;
std::vector<PDFCharacterPointer> m_characterPointers;
std::vector<QRectF> m_characterBoundingBoxes;
};
/// Text layout of single page. Can handle various fonts, various angles of lines
/// and vertically oriented text. It performs the "docstrum" algorithm.
class PDF4QTLIBSHARED_EXPORT PDFTextLayout
{
public:
explicit PDFTextLayout();
/// Adds character to the layout
void addCharacter(const PDFTextCharacterInfo& info);
/// Performs text layout algorithm
void perform();
/// Optimizes layout memory allocation to contain less space
void optimize();
/// Returns estimate of number of bytes, which this mesh occupies in memory
qint64 getMemoryConsumptionEstimate() const;
/// Returns recognized text blocks
const PDFTextBlocks& getTextBlocks() const { return m_blocks; }
/// 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
/// \param selectionColor Selection color
/// \param strictSelection If true, does not adjust horizontal range when above/below text block
PDFTextSelection createTextSelection(PDFInteger pageIndex,
const QPointF& point1,
const QPointF& point2,
QColor selectionColor = Qt::yellow,
bool strictSelection = false);
/// Returns string from text selection
/// \param itBegin Iterator (begin range)
/// \param itEnd Iterator (end range)
/// \param pageIndex Index of the page
QString getTextFromSelection(PDFTextSelection::iterator itBegin, PDFTextSelection::iterator itEnd, PDFInteger pageIndex) const;
/// Returns string from text selection
/// \param selection Text selection
/// \param pageIndex Index of the page
QString getTextFromSelection(const PDFTextSelection& selection, PDFInteger pageIndex) const;
/// Creates text selection for whole block
/// \param blockIndex Text block index
/// \param pageIndex pageIndex
/// \param color Selection color
PDFTextSelection selectBlock(const size_t blockIndex, PDFInteger pageIndex, QColor color) const;
/// Creates text selection for signle line of text block
/// \param blockIndex Text block index
/// \param lineIndex Line index
/// \param pageIndex pageIndex
/// \param color Selection color
PDFTextSelection selectLineInBlock(const size_t blockIndex, const size_t lineIndex, PDFInteger pageIndex, QColor color) const;
friend QDataStream& operator<<(QDataStream& stream, const PDFTextLayout& layout);
friend QDataStream& operator>>(QDataStream& stream, PDFTextLayout& layout);
private:
/// Makes layout for particular angle
void performDoLayout(PDFReal angle);
/// Returns a list of characters for particular angle. Exact match is used
/// for angle, even if angle is floating point number.
/// \param angle Angle
TextCharacters getCharactersForAngle(PDFReal angle) const;
/// Applies transform to text characters (positions and bounding boxes)
/// \param characters Characters
/// \param matrix Transform matrix
static void applyTransform(TextCharacters& characters, const QTransform& matrix);
TextCharacters m_characters;
std::set<PDFReal> m_angles;
PDFTextLayoutSettings m_settings;
PDFTextBlocks m_blocks;
};
/// Cache for storing single text layout
class PDF4QTLIBSHARED_EXPORT PDFTextLayoutCache
{
public:
explicit PDFTextLayoutCache(std::function<PDFTextLayout(PDFInteger)> textLayoutGetter);
/// Clears the cache
void clear();
/// Returns text layout. This function always succeeds. If compiler is not active,
/// then empty layout is returned.
/// \param compiler Text layout compiler
/// \param pageIndex Page index
const PDFTextLayout& getTextLayout(PDFInteger pageIndex);
private:
std::function<PDFTextLayout(PDFInteger)> m_textLayoutGetter;
PDFInteger m_pageIndex;
PDFTextLayout m_layout;
};
class PDF4QTLIBSHARED_EXPORT PDFTextLayoutGetter
{
public:
explicit inline PDFTextLayoutGetter(PDFTextLayoutCache* cache, PDFInteger pageIndex) :
m_cache(cache),
m_pageIndex(pageIndex)
{
}
/// Cast operator, casts to constant reference to PDFTextLayout
operator const PDFTextLayout&()
{
return m_cache->getTextLayout(m_pageIndex);
}
private:
PDFTextLayoutCache* m_cache;
PDFInteger m_pageIndex;
};
/// Lazy getter for text layouts from storage. This is used, when we do not want to
/// get text layout each time, because it is time expensive. If text layout is not needed,
/// then nothing happens. Text layout is returned only, if conversion operator is used.
class PDFTextLayoutStorageGetter
{
public:
explicit PDFTextLayoutStorageGetter(const PDFTextLayoutStorage* storage, PDFInteger pageIndex) :
m_storage(storage),
m_pageIndex(pageIndex)
{
}
/// Cast operator, casts to constant reference to PDFTextLayout
operator const PDFTextLayout&()
{
return m_textLayout.get(this, &PDFTextLayoutStorageGetter::getTextLayoutImpl);
}
private:
PDFTextLayout getTextLayoutImpl() const;
const PDFTextLayoutStorage* m_storage;
PDFInteger m_pageIndex;
PDFCachedItem<PDFTextLayout> m_textLayout;
};
/// Paints text selection on various pages using page to device point matrix
class PDF4QTLIBSHARED_EXPORT PDFTextSelectionPainter
{
public:
explicit inline PDFTextSelectionPainter(const PDFTextSelection* selection) :
m_selection(selection)
{
}
/// Draws text selection on the painter, using text layout and matrix. If current text selection
/// doesn't contain items from active page, then text layout is not accessed.
/// \param painter Painter
/// \param pageIndex Page index
/// \param textLayoutGetter Text layout getter
/// \param matrix Matrix which translates from page space to device space
void draw(QPainter* painter, PDFInteger pageIndex, PDFTextLayoutGetter& textLayoutGetter, const QTransform& matrix);
/// Prepares geometry for text selection drawing, using text layout and matrix. If current text selection
/// doesn't contain items from active page, then text layout is not accessed.
/// \param pageIndex Page index
/// \param textLayoutGetter Text layout getter
/// \param matrix Matrix which translates from page space to device space
QPainterPath prepareGeometry(PDFInteger pageIndex, PDFTextLayoutGetter& textLayoutGetter, const QTransform& matrix, QPolygonF* quadrilaterals);
private:
static constexpr const PDFReal HEIGHT_INCREASE_FACTOR = 0.40;
static constexpr const PDFReal SELECTION_ALPHA = 0.25;
const PDFTextSelection* m_selection;
};
/// Storage for text layouts. For reading and writing, this object is thread safe.
/// For writing, mutex is used to synchronize asynchronous writes, for reading
/// no mutex is used at all. For this reason, both reading/writing at the same time
/// is prohibited, it is not thread safe.
class PDF4QTLIBSHARED_EXPORT PDFTextLayoutStorage
{
public:
explicit inline PDFTextLayoutStorage() = default;
explicit inline PDFTextLayoutStorage(PDFInteger pageCount) :
m_offsets(pageCount, 0)
{
}
/// Returns text layout for particular page. If page index is invalid,
/// then empty text layout is returned. Function is not thread safe, if
/// function \p setTextLayout is called from another thread.
/// \param pageIndex Page index
PDFTextLayout getTextLayout(PDFInteger pageIndex) const;
/// Returns text layout for particular page. If page index is invalid,
/// then empty text layout is returned. Function is not thread safe, if
/// function \p setTextLayout is called from another thread.
/// \param pageIndex Page index
PDFTextLayoutStorageGetter getTextLayoutLazy(PDFInteger pageIndex) const { return PDFTextLayoutStorageGetter(this, pageIndex); }
/// Sets text layout to the particular index. Index must be valid and from
/// range 0 to \p pageCount - 1. Function is not thread safe.
/// \param pageIndex Page index
/// \param layout Text layout
/// \param mutex Mutex for locking (calls of setTextLayout from multiple threads)
void setTextLayout(PDFInteger pageIndex, const PDFTextLayout& layout, QMutex* mutex);
/// Finds simple text in all pages. All text occurences are returned.
/// \param text Text to be found
/// \param caseSensitivity Case sensitivity
/// \param flowFlags Text flow flags
PDFFindResults find(const QString& text, Qt::CaseSensitivity caseSensitivity, PDFTextFlow::FlowFlags flowFlags) const;
/// Finds regular expression matches in current text flow. All text occurences are returned.
/// \param expression Regular expression to be matched
/// \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<int> m_offsets;
QByteArray m_textLayouts;
};
} // namespace pdf
Q_DECLARE_OPERATORS_FOR_FLAGS(pdf::PDFTextFlow::FlowFlags)
#endif // PDFTEXTLAYOUT_H