2021-05-24 07:25:56 +00:00
|
|
|
/*
|
|
|
|
SPDX-FileCopyrightText: 2005 Piotr Szymanski <niedakh@gmail.com>
|
|
|
|
|
|
|
|
SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
*/
|
2006-09-21 08:45:36 +00:00
|
|
|
|
2007-04-19 18:30:20 +00:00
|
|
|
#include "textpage.h"
|
2007-09-03 19:52:46 +00:00
|
|
|
#include "textpage_p.h"
|
2006-11-27 18:31:20 +00:00
|
|
|
|
2018-08-31 09:23:45 +00:00
|
|
|
#include <QDebug>
|
2006-09-21 08:45:36 +00:00
|
|
|
|
2005-07-15 18:20:57 +00:00
|
|
|
#include "area.h"
|
2007-07-31 10:19:48 +00:00
|
|
|
#include "debug_p.h"
|
- GIGANTIC 2700 line diff with LOTS OF FEATURES!
- 1. editor-like text selection, and I do mean it, its not pseudo-editor
(like the ones acroread and kviewshell have) it doesnt intersect the
selection area with words under it, no, it does a lot more, including
work on cursors and searching for the text area closest to the given
cursor
- 2. rotation support, change the orientation of the documents if
you need too :)
- 3. the kfaxview backend works beautifully, porting kviewshell backends
is damn easy ! djvu and dvi will be next!
- 4. Hardware Blending of selection rectangles! We now use XRender
instead of KImageEffect, makes a damn faster blend!
- 5. Overview mode - as seen in Kviewshell, but quite a bit extended,
the kviewshell is only one state, while we support it in both
continous and non-continous form
- BTW. I coded all those features myself, (apart from kfaxview backend library)
it is an impressive bit right? but oKular cant be run by only one person,
join in on the fun! i can introduce you into the code just mail niedakh@gmail.com
svn path=/trunk/playground/graphics/oKular/kpdf/; revision=509871
2006-02-15 18:54:49 +00:00
|
|
|
#include "misc.h"
|
2007-09-05 14:57:15 +00:00
|
|
|
#include "page.h"
|
2007-09-04 22:37:39 +00:00
|
|
|
#include "page_p.h"
|
2006-09-21 08:45:36 +00:00
|
|
|
|
2008-05-01 19:03:02 +00:00
|
|
|
#include <cstring>
|
|
|
|
|
2011-08-23 14:02:08 +00:00
|
|
|
#include <QVarLengthArray>
|
2011-08-17 09:32:40 +00:00
|
|
|
#include <QtAlgorithms>
|
2011-06-24 16:53:44 +00:00
|
|
|
|
2006-09-21 08:45:36 +00:00
|
|
|
using namespace Okular;
|
2005-07-15 18:20:57 +00:00
|
|
|
|
2006-11-23 17:56:03 +00:00
|
|
|
class SearchPoint
|
2006-05-27 10:06:31 +00:00
|
|
|
{
|
2006-11-23 17:56:03 +00:00
|
|
|
public:
|
|
|
|
SearchPoint()
|
2008-03-14 18:15:03 +00:00
|
|
|
: offset_begin(-1)
|
|
|
|
, offset_end(-1)
|
2006-11-23 17:56:03 +00:00
|
|
|
{
|
|
|
|
}
|
|
|
|
|
2013-10-18 14:28:54 +00:00
|
|
|
/** The TinyTextEntity containing the first character of the match. */
|
2008-05-01 19:03:02 +00:00
|
|
|
TextList::ConstIterator it_begin;
|
2013-10-18 14:28:54 +00:00
|
|
|
|
|
|
|
/** The TinyTextEntity containing the last character of the match. */
|
2008-05-01 19:03:02 +00:00
|
|
|
TextList::ConstIterator it_end;
|
2013-10-18 14:28:54 +00:00
|
|
|
|
|
|
|
/** The index of the first character of the match in (*it_begin)->text().
|
|
|
|
* Satisfies 0 <= offset_begin < (*it_begin)->text().length().
|
|
|
|
*/
|
2006-11-23 17:56:03 +00:00
|
|
|
int offset_begin;
|
2013-10-18 14:28:54 +00:00
|
|
|
|
|
|
|
/** One plus the index of the last character of the match in (*it_end)->text().
|
|
|
|
* Satisfies 0 < offset_end <= (*it_end)->text().length().
|
|
|
|
*/
|
2006-11-23 17:56:03 +00:00
|
|
|
int offset_end;
|
|
|
|
};
|
|
|
|
|
2009-12-27 14:22:13 +00:00
|
|
|
/* text comparison functions */
|
|
|
|
|
2013-10-18 14:28:54 +00:00
|
|
|
static bool CaseInsensitiveCmpFn(const QStringRef &from, const QStringRef &to)
|
2009-12-27 14:22:13 +00:00
|
|
|
{
|
2020-03-14 10:41:58 +00:00
|
|
|
#ifdef DEBUG_TEXTPAGE
|
|
|
|
qDebug(OkularCoreDebug) << from << ":" << to << "(case insensitive)";
|
|
|
|
#endif
|
2009-12-27 14:22:13 +00:00
|
|
|
return from.compare(to, Qt::CaseInsensitive) == 0;
|
|
|
|
}
|
|
|
|
|
2013-10-18 14:28:54 +00:00
|
|
|
static bool CaseSensitiveCmpFn(const QStringRef &from, const QStringRef &to)
|
2009-12-27 14:22:13 +00:00
|
|
|
{
|
2020-03-14 10:41:58 +00:00
|
|
|
#ifdef DEBUG_TEXTPAGE
|
|
|
|
qDebug(OkularCoreDebug) << from << ":" << to << "(case sensitive)";
|
|
|
|
#endif
|
2009-12-27 14:22:13 +00:00
|
|
|
return from.compare(to, Qt::CaseSensitive) == 0;
|
|
|
|
}
|
|
|
|
|
2012-01-31 19:19:47 +00:00
|
|
|
/**
|
2014-02-25 22:57:06 +00:00
|
|
|
* Returns true iff segments [@p left1, @p right1] and [@p left2, @p right2] on the real line
|
|
|
|
* overlap within @p threshold percent, i. e. iff the ratio of the length of the
|
|
|
|
* intersection of the segments to the length of the shortest of the two input segments
|
|
|
|
* is not smaller than the threshold.
|
|
|
|
*/
|
|
|
|
static bool segmentsOverlap(double left1, double right1, double left2, double right2, int threshold)
|
2012-01-31 19:19:47 +00:00
|
|
|
{
|
2014-02-25 22:57:06 +00:00
|
|
|
// check if one consumes another fully (speed optimization)
|
|
|
|
|
|
|
|
if (left1 <= left2 && right1 >= right2) {
|
2012-01-31 19:19:47 +00:00
|
|
|
return true;
|
2022-03-08 10:10:43 +00:00
|
|
|
}
|
2012-01-31 19:19:47 +00:00
|
|
|
|
2014-02-25 22:57:06 +00:00
|
|
|
if (left1 >= left2 && right1 <= right2) {
|
2012-01-31 19:19:47 +00:00
|
|
|
return true;
|
2022-03-08 10:10:43 +00:00
|
|
|
}
|
2012-01-31 19:19:47 +00:00
|
|
|
|
2014-02-25 22:57:06 +00:00
|
|
|
// check if there is overlap above threshold
|
|
|
|
if (right2 >= left1 && right1 >= left2) {
|
|
|
|
double overlap = (right2 >= right1) ? right1 - left2 : right2 - left1;
|
|
|
|
|
|
|
|
double length1 = right1 - left1, length2 = right2 - left2;
|
2012-01-31 19:19:47 +00:00
|
|
|
|
2014-02-25 22:57:06 +00:00
|
|
|
return overlap * 100 >= threshold * qMin(length1, length2);
|
2012-01-31 19:19:47 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
2008-05-01 19:03:02 +00:00
|
|
|
|
2020-02-20 17:45:46 +00:00
|
|
|
static bool doesConsumeY(const QRect first, const QRect second, int threshold)
|
2014-02-25 22:57:06 +00:00
|
|
|
{
|
|
|
|
return segmentsOverlap(first.top(), first.bottom(), second.top(), second.bottom(), threshold);
|
|
|
|
}
|
|
|
|
|
|
|
|
static bool doesConsumeY(const NormalizedRect &first, const NormalizedRect &second, int threshold)
|
|
|
|
{
|
|
|
|
return segmentsOverlap(first.top, first.bottom, second.top, second.bottom, threshold);
|
|
|
|
}
|
|
|
|
|
2008-05-01 19:03:02 +00:00
|
|
|
/*
|
|
|
|
Rationale behind TinyTextEntity:
|
|
|
|
|
|
|
|
instead of storing directly a QString for the text of an entity,
|
|
|
|
we store the UTF-16 data and their length. This way, we save about
|
|
|
|
4 int's wrt a QString, and we can create a new string from that
|
|
|
|
raw data (that's the only penalty of that).
|
2009-12-28 11:48:25 +00:00
|
|
|
Even better, if the string we need to store has at most
|
|
|
|
MaxStaticChars characters, then we store those in place of the QChar*
|
|
|
|
that would be used (with new[] + free[]) for the data.
|
2008-05-01 19:03:02 +00:00
|
|
|
*/
|
|
|
|
class TinyTextEntity
|
|
|
|
{
|
2019-12-23 13:57:50 +00:00
|
|
|
static const int MaxStaticChars = sizeof(void *) / sizeof(QChar);
|
2009-12-28 11:48:25 +00:00
|
|
|
|
2008-05-01 19:03:02 +00:00
|
|
|
public:
|
|
|
|
TinyTextEntity(const QString &text, const NormalizedRect &rect)
|
|
|
|
: area(rect)
|
|
|
|
{
|
|
|
|
Q_ASSERT_X(!text.isEmpty(), "TinyTextEntity", "empty string");
|
2019-12-23 13:57:50 +00:00
|
|
|
Q_ASSERT_X(sizeof(d) == sizeof(void *), "TinyTextEntity", "internal storage is wider than QChar*, fix it!");
|
2008-05-01 19:03:02 +00:00
|
|
|
length = text.length();
|
2009-12-28 11:48:25 +00:00
|
|
|
switch (length) {
|
|
|
|
#if QT_POINTER_SIZE >= 8
|
|
|
|
case 4:
|
|
|
|
d.qc[3] = text.at(3).unicode();
|
|
|
|
// fall through
|
|
|
|
case 3:
|
|
|
|
d.qc[2] = text.at(2).unicode();
|
|
|
|
#endif
|
2017-07-27 14:57:40 +00:00
|
|
|
// fall through
|
2009-12-28 11:48:25 +00:00
|
|
|
case 2:
|
|
|
|
d.qc[1] = text.at(1).unicode();
|
|
|
|
// fall through
|
|
|
|
case 1:
|
|
|
|
d.qc[0] = text.at(0).unicode();
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
d.data = new QChar[length];
|
|
|
|
std::memcpy(d.data, text.constData(), length * sizeof(QChar));
|
2008-05-01 19:03:02 +00:00
|
|
|
}
|
2020-07-10 22:15:05 +00:00
|
|
|
}
|
2008-05-01 19:03:02 +00:00
|
|
|
|
|
|
|
~TinyTextEntity()
|
|
|
|
{
|
2009-12-28 11:48:25 +00:00
|
|
|
if (length > MaxStaticChars) {
|
|
|
|
delete[] d.data;
|
2008-05-01 19:03:02 +00:00
|
|
|
}
|
2020-07-10 22:15:05 +00:00
|
|
|
}
|
2008-05-01 19:03:02 +00:00
|
|
|
|
|
|
|
inline QString text() const
|
|
|
|
{
|
2009-12-28 11:48:25 +00:00
|
|
|
return length <= MaxStaticChars ? QString::fromRawData((const QChar *)&d.qc[0], length) : QString::fromRawData(d.data, length);
|
2008-05-01 19:03:02 +00:00
|
|
|
}
|
|
|
|
|
2012-09-08 15:06:02 +00:00
|
|
|
inline NormalizedRect transformedArea(const QTransform &matrix) const
|
2008-05-01 19:03:02 +00:00
|
|
|
{
|
|
|
|
NormalizedRect transformed_area = area;
|
|
|
|
transformed_area.transform(matrix);
|
|
|
|
return transformed_area;
|
|
|
|
}
|
|
|
|
|
|
|
|
NormalizedRect area;
|
|
|
|
|
|
|
|
private:
|
|
|
|
Q_DISABLE_COPY(TinyTextEntity)
|
|
|
|
|
2009-12-28 11:48:25 +00:00
|
|
|
union {
|
|
|
|
QChar *data;
|
|
|
|
ushort qc[MaxStaticChars];
|
|
|
|
} d;
|
2008-05-01 19:03:02 +00:00
|
|
|
int length;
|
|
|
|
};
|
|
|
|
|
2006-11-23 17:56:03 +00:00
|
|
|
TextEntity::TextEntity(const QString &text, NormalizedRect *area)
|
2017-09-05 21:27:18 +00:00
|
|
|
: m_text(text)
|
|
|
|
, m_area(area)
|
|
|
|
, d(nullptr)
|
2006-11-23 17:56:03 +00:00
|
|
|
{
|
|
|
|
}
|
|
|
|
|
|
|
|
TextEntity::~TextEntity()
|
|
|
|
{
|
|
|
|
delete m_area;
|
|
|
|
}
|
|
|
|
|
|
|
|
QString TextEntity::text() const
|
|
|
|
{
|
|
|
|
return m_text;
|
|
|
|
}
|
|
|
|
|
|
|
|
NormalizedRect *TextEntity::area() const
|
|
|
|
{
|
|
|
|
return m_area;
|
|
|
|
}
|
|
|
|
|
2012-09-08 15:06:02 +00:00
|
|
|
NormalizedRect TextEntity::transformedArea(const QTransform &matrix) const
|
2006-11-23 22:14:04 +00:00
|
|
|
{
|
2007-08-13 22:25:27 +00:00
|
|
|
NormalizedRect transformed_area = *m_area;
|
|
|
|
transformed_area.transform(matrix);
|
|
|
|
return transformed_area;
|
2006-11-23 22:14:04 +00:00
|
|
|
}
|
|
|
|
|
2008-04-18 09:57:55 +00:00
|
|
|
TextPagePrivate::TextPagePrivate()
|
2017-09-05 21:27:18 +00:00
|
|
|
: m_page(nullptr)
|
2008-04-18 09:57:55 +00:00
|
|
|
{
|
|
|
|
}
|
|
|
|
|
2007-09-03 19:52:46 +00:00
|
|
|
TextPagePrivate::~TextPagePrivate()
|
|
|
|
{
|
|
|
|
qDeleteAll(m_searchPoints);
|
|
|
|
qDeleteAll(m_words);
|
|
|
|
}
|
2006-05-27 10:06:31 +00:00
|
|
|
|
2006-11-23 17:56:03 +00:00
|
|
|
TextPage::TextPage()
|
2008-04-18 09:57:55 +00:00
|
|
|
: d(new TextPagePrivate())
|
2006-11-23 17:56:03 +00:00
|
|
|
{
|
|
|
|
}
|
|
|
|
|
|
|
|
TextPage::TextPage(const TextEntity::List &words)
|
2008-05-01 19:03:02 +00:00
|
|
|
: d(new TextPagePrivate())
|
2006-11-23 17:56:03 +00:00
|
|
|
{
|
2008-11-11 18:48:40 +00:00
|
|
|
TextEntity::List::ConstIterator it = words.constBegin(), itEnd = words.constEnd();
|
2008-05-01 19:03:02 +00:00
|
|
|
for (; it != itEnd; ++it) {
|
|
|
|
TextEntity *e = *it;
|
|
|
|
if (!e->text().isEmpty()) {
|
|
|
|
d->m_words.append(new TinyTextEntity(e->text(), *e->area()));
|
2022-03-08 10:10:43 +00:00
|
|
|
}
|
2008-05-01 19:03:02 +00:00
|
|
|
delete e;
|
|
|
|
}
|
2006-11-23 17:56:03 +00:00
|
|
|
}
|
|
|
|
|
2006-09-21 08:45:36 +00:00
|
|
|
TextPage::~TextPage()
|
2005-07-15 18:20:57 +00:00
|
|
|
{
|
2006-11-23 17:56:03 +00:00
|
|
|
delete d;
|
|
|
|
}
|
|
|
|
|
|
|
|
void TextPage::append(const QString &text, NormalizedRect *area)
|
|
|
|
{
|
2020-12-12 23:16:46 +00:00
|
|
|
if (!text.isEmpty()) {
|
|
|
|
if (!d->m_words.isEmpty()) {
|
|
|
|
TinyTextEntity *lastEntity = d->m_words.last();
|
|
|
|
const QString concatText = lastEntity->text() + text.normalized(QString::NormalizationForm_KC);
|
|
|
|
if (concatText != concatText.normalized(QString::NormalizationForm_KC)) {
|
|
|
|
// If this happens it means that the new text + old one have combined, for example A and ◌̊ form Å
|
|
|
|
NormalizedRect newArea = *area | lastEntity->area;
|
|
|
|
delete area;
|
|
|
|
delete lastEntity;
|
|
|
|
d->m_words.removeLast();
|
|
|
|
d->m_words.append(new TinyTextEntity(concatText.normalized(QString::NormalizationForm_KC), newArea));
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2011-03-25 20:07:45 +00:00
|
|
|
d->m_words.append(new TinyTextEntity(text.normalized(QString::NormalizationForm_KC), *area));
|
2020-12-12 23:16:46 +00:00
|
|
|
}
|
2008-05-01 19:03:02 +00:00
|
|
|
delete area;
|
2005-07-15 18:20:57 +00:00
|
|
|
}
|
|
|
|
|
2011-12-03 12:50:37 +00:00
|
|
|
struct WordWithCharacters {
|
|
|
|
WordWithCharacters(TinyTextEntity *w, const TextList &c)
|
|
|
|
: word(w)
|
|
|
|
, characters(c)
|
|
|
|
{
|
|
|
|
}
|
2020-07-10 22:15:05 +00:00
|
|
|
|
2011-12-03 12:50:37 +00:00
|
|
|
inline QString text() const
|
|
|
|
{
|
|
|
|
return word->text();
|
|
|
|
}
|
2020-07-10 22:15:05 +00:00
|
|
|
|
2011-12-03 12:50:37 +00:00
|
|
|
inline const NormalizedRect &area() const
|
|
|
|
{
|
|
|
|
return word->area;
|
|
|
|
}
|
2020-07-10 22:15:05 +00:00
|
|
|
|
2011-12-03 12:50:37 +00:00
|
|
|
TinyTextEntity *word;
|
|
|
|
TextList characters;
|
|
|
|
};
|
|
|
|
typedef QList<WordWithCharacters> WordsWithCharacters;
|
|
|
|
|
2011-08-18 01:03:46 +00:00
|
|
|
/**
|
|
|
|
* We will divide the whole page in some regions depending on the horizontal and
|
|
|
|
* vertical spacing among different regions. Each region will have an area and an
|
2011-12-03 12:50:37 +00:00
|
|
|
* associated WordsWithCharacters in sorted order.
|
2011-08-18 01:03:46 +00:00
|
|
|
*/
|
2011-08-21 04:44:42 +00:00
|
|
|
class RegionText
|
|
|
|
{
|
2011-08-17 09:32:40 +00:00
|
|
|
public:
|
2011-08-21 04:44:42 +00:00
|
|
|
RegionText() {};
|
2011-08-17 09:32:40 +00:00
|
|
|
|
2020-02-20 17:45:46 +00:00
|
|
|
RegionText(const WordsWithCharacters &wordsWithCharacters, const QRect area)
|
2011-12-03 12:50:37 +00:00
|
|
|
: m_region_wordWithCharacters(wordsWithCharacters)
|
|
|
|
, m_area(area)
|
2011-08-17 09:32:40 +00:00
|
|
|
{
|
|
|
|
}
|
2020-07-10 22:15:05 +00:00
|
|
|
|
2011-11-03 17:58:53 +00:00
|
|
|
inline QString string() const
|
|
|
|
{
|
|
|
|
QString res;
|
2019-12-09 13:16:55 +00:00
|
|
|
for (const WordWithCharacters &word : m_region_wordWithCharacters) {
|
2011-12-03 12:50:37 +00:00
|
|
|
res += word.text();
|
2019-12-09 13:16:55 +00:00
|
|
|
}
|
2011-11-03 17:58:53 +00:00
|
|
|
return res;
|
|
|
|
}
|
2011-08-17 09:32:40 +00:00
|
|
|
|
2011-12-03 12:50:37 +00:00
|
|
|
inline WordsWithCharacters text() const
|
2011-08-21 04:44:42 +00:00
|
|
|
{
|
2011-12-03 12:50:37 +00:00
|
|
|
return m_region_wordWithCharacters;
|
2011-08-17 09:32:40 +00:00
|
|
|
}
|
|
|
|
|
2011-08-21 04:44:42 +00:00
|
|
|
inline QRect area() const
|
|
|
|
{
|
2011-08-17 09:32:40 +00:00
|
|
|
return m_area;
|
|
|
|
}
|
|
|
|
|
2020-02-20 17:45:46 +00:00
|
|
|
inline void setArea(const QRect area)
|
2011-08-21 04:44:42 +00:00
|
|
|
{
|
2011-08-17 09:32:40 +00:00
|
|
|
m_area = area;
|
|
|
|
}
|
|
|
|
|
2011-12-03 12:50:37 +00:00
|
|
|
inline void setText(const WordsWithCharacters &wordsWithCharacters)
|
2011-08-21 04:44:42 +00:00
|
|
|
{
|
2011-12-03 12:50:37 +00:00
|
|
|
m_region_wordWithCharacters = wordsWithCharacters;
|
2011-08-17 09:32:40 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
private:
|
2011-12-03 12:50:37 +00:00
|
|
|
WordsWithCharacters m_region_wordWithCharacters;
|
2011-08-17 09:32:40 +00:00
|
|
|
QRect m_area;
|
|
|
|
};
|
|
|
|
|
2006-11-23 17:11:04 +00:00
|
|
|
RegularAreaRect *TextPage::textArea(TextSelection *sel) const
|
- GIGANTIC 2700 line diff with LOTS OF FEATURES!
- 1. editor-like text selection, and I do mean it, its not pseudo-editor
(like the ones acroread and kviewshell have) it doesnt intersect the
selection area with words under it, no, it does a lot more, including
work on cursors and searching for the text area closest to the given
cursor
- 2. rotation support, change the orientation of the documents if
you need too :)
- 3. the kfaxview backend works beautifully, porting kviewshell backends
is damn easy ! djvu and dvi will be next!
- 4. Hardware Blending of selection rectangles! We now use XRender
instead of KImageEffect, makes a damn faster blend!
- 5. Overview mode - as seen in Kviewshell, but quite a bit extended,
the kviewshell is only one state, while we support it in both
continous and non-continous form
- BTW. I coded all those features myself, (apart from kfaxview backend library)
it is an impressive bit right? but oKular cant be run by only one person,
join in on the fun! i can introduce you into the code just mail niedakh@gmail.com
svn path=/trunk/playground/graphics/oKular/kpdf/; revision=509871
2006-02-15 18:54:49 +00:00
|
|
|
{
|
2006-11-25 21:39:52 +00:00
|
|
|
if (d->m_words.isEmpty()) {
|
|
|
|
return new RegularAreaRect();
|
2022-03-08 10:10:43 +00:00
|
|
|
}
|
2006-11-25 21:39:52 +00:00
|
|
|
|
- GIGANTIC 2700 line diff with LOTS OF FEATURES!
- 1. editor-like text selection, and I do mean it, its not pseudo-editor
(like the ones acroread and kviewshell have) it doesnt intersect the
selection area with words under it, no, it does a lot more, including
work on cursors and searching for the text area closest to the given
cursor
- 2. rotation support, change the orientation of the documents if
you need too :)
- 3. the kfaxview backend works beautifully, porting kviewshell backends
is damn easy ! djvu and dvi will be next!
- 4. Hardware Blending of selection rectangles! We now use XRender
instead of KImageEffect, makes a damn faster blend!
- 5. Overview mode - as seen in Kviewshell, but quite a bit extended,
the kviewshell is only one state, while we support it in both
continous and non-continous form
- BTW. I coded all those features myself, (apart from kfaxview backend library)
it is an impressive bit right? but oKular cant be run by only one person,
join in on the fun! i can introduce you into the code just mail niedakh@gmail.com
svn path=/trunk/playground/graphics/oKular/kpdf/; revision=509871
2006-02-15 18:54:49 +00:00
|
|
|
/**
|
2007-01-05 14:40:38 +00:00
|
|
|
It works like this:
|
|
|
|
There are two cursors, we need to select all the text between them. The coordinates are normalised, leftTop is (0,0)
|
|
|
|
rightBottom is (1,1), so for cursors start (sx,sy) and end (ex,ey) we start with finding text rectangles under those
|
|
|
|
points, if not we search for the first that is to the right to it in the same baseline, if none found, then we search
|
|
|
|
for the first rectangle with a baseline under the cursor, having two points that are the best rectangles to both
|
|
|
|
of the cursors: (rx,ry)x(tx,ty) for start and (ux,uy)x(vx,vy) for end, we do a
|
|
|
|
1. (rx,ry)x(1,ty)
|
|
|
|
2. (0,ty)x(1,uy)
|
|
|
|
3. (0,uy)x(vx,vy)
|
2020-07-10 22:15:05 +00:00
|
|
|
|
2007-01-05 14:40:38 +00:00
|
|
|
To find the closest rectangle to cursor (cx,cy) we search for a rectangle that either contains the cursor
|
|
|
|
or that has a left border >= cx and bottom border >= cy.
|
- GIGANTIC 2700 line diff with LOTS OF FEATURES!
- 1. editor-like text selection, and I do mean it, its not pseudo-editor
(like the ones acroread and kviewshell have) it doesnt intersect the
selection area with words under it, no, it does a lot more, including
work on cursors and searching for the text area closest to the given
cursor
- 2. rotation support, change the orientation of the documents if
you need too :)
- 3. the kfaxview backend works beautifully, porting kviewshell backends
is damn easy ! djvu and dvi will be next!
- 4. Hardware Blending of selection rectangles! We now use XRender
instead of KImageEffect, makes a damn faster blend!
- 5. Overview mode - as seen in Kviewshell, but quite a bit extended,
the kviewshell is only one state, while we support it in both
continous and non-continous form
- BTW. I coded all those features myself, (apart from kfaxview backend library)
it is an impressive bit right? but oKular cant be run by only one person,
join in on the fun! i can introduce you into the code just mail niedakh@gmail.com
svn path=/trunk/playground/graphics/oKular/kpdf/; revision=509871
2006-02-15 18:54:49 +00:00
|
|
|
*/
|
2007-01-05 14:40:38 +00:00
|
|
|
RegularAreaRect *ret = new RegularAreaRect;
|
2007-09-04 22:37:39 +00:00
|
|
|
|
2017-12-05 22:07:10 +00:00
|
|
|
PagePrivate *pagePrivate = PagePrivate::get(d->m_page);
|
|
|
|
const QTransform matrix = pagePrivate ? pagePrivate->rotationMatrix() : QTransform();
|
|
|
|
const double scaleX = d->m_page->width();
|
|
|
|
const double scaleY = d->m_page->height();
|
2007-09-03 19:35:55 +00:00
|
|
|
|
2011-06-25 05:11:33 +00:00
|
|
|
NormalizedPoint startC = sel->start();
|
|
|
|
NormalizedPoint endC = sel->end();
|
2011-08-04 08:35:09 +00:00
|
|
|
NormalizedPoint temp;
|
2011-06-25 05:11:33 +00:00
|
|
|
|
2011-08-18 01:03:46 +00:00
|
|
|
// if startPoint is right to endPoint swap them
|
2011-08-21 04:44:42 +00:00
|
|
|
if (startC.x > endC.x) {
|
2011-06-25 05:11:33 +00:00
|
|
|
temp = startC;
|
|
|
|
startC = endC;
|
|
|
|
endC = temp;
|
|
|
|
}
|
|
|
|
|
2011-08-18 01:03:46 +00:00
|
|
|
// minX,maxX,minY,maxY gives the bounding rectangle coordinates of the document
|
2017-12-05 22:07:10 +00:00
|
|
|
const NormalizedRect boundingRect = d->m_page->boundingBox();
|
2011-08-22 18:19:23 +00:00
|
|
|
const QRect content = boundingRect.geometry(scaleX, scaleY);
|
|
|
|
const double minX = content.left();
|
|
|
|
const double maxX = content.right();
|
|
|
|
const double minY = content.top();
|
|
|
|
const double maxY = content.bottom();
|
2011-06-24 16:53:44 +00:00
|
|
|
|
2011-06-27 13:58:24 +00:00
|
|
|
/**
|
2011-08-21 04:44:42 +00:00
|
|
|
* We will now find out the TinyTextEntity for the startRectangle and TinyTextEntity for
|
|
|
|
* the endRectangle. We have four cases:
|
|
|
|
*
|
|
|
|
* Case 1(a): both startpoint and endpoint are out of the bounding Rectangle and at one side, so the rectangle made of start
|
|
|
|
* and endPoint are outof the bounding rect (do not intersect)
|
|
|
|
*
|
|
|
|
* Case 1(b): both startpoint and endpoint are out of bounding rect, but they are in different side, so is their rectangle
|
|
|
|
*
|
|
|
|
* Case 2(a): find the rectangle which contains start and endpoint and having some
|
|
|
|
* TextEntity
|
|
|
|
*
|
|
|
|
* Case 2(b): if 2(a) fails (if startPoint and endPoint both are unchanged), then we check whether there is any
|
|
|
|
* TextEntity within the rect made by startPoint and endPoint
|
|
|
|
*
|
|
|
|
* Case 3: Now, we may have two type of selection.
|
|
|
|
* 1. startpoint is left-top of start_end and endpoint is right-bottom
|
|
|
|
* 2. startpoint is left-bottom of start_end and endpoint is top-right
|
|
|
|
*
|
|
|
|
* Also, as 2(b) is passed, we might have it,itEnd or both unchanged, but the fact is that we have
|
|
|
|
* text within them. so, we need to search for the best suitable textposition for start and end.
|
|
|
|
*
|
|
|
|
* Case 3(a): We search the nearest rectangle consisting of some
|
|
|
|
* TinyTextEntity right to or bottom of the startPoint for selection 01.
|
|
|
|
* And, for selection 02, we have to search for right and top
|
|
|
|
*
|
|
|
|
* Case 3(b): For endpoint, we have to find the point top of or left to
|
|
|
|
* endpoint if we have selection 01.
|
|
|
|
* Otherwise, the search will be left and bottom
|
2011-08-18 01:03:46 +00:00
|
|
|
*/
|
2011-06-25 05:11:33 +00:00
|
|
|
|
2011-07-14 11:35:02 +00:00
|
|
|
// we know that startC.x > endC.x, we need to decide which is top and which is bottom
|
2011-08-22 18:19:23 +00:00
|
|
|
const NormalizedRect start_end = (startC.y < endC.y) ? NormalizedRect(startC.x, startC.y, endC.x, endC.y) : NormalizedRect(startC.x, endC.y, endC.x, startC.y);
|
2011-06-25 05:11:33 +00:00
|
|
|
|
2011-08-18 01:03:46 +00:00
|
|
|
// Case 1(a)
|
2011-07-14 11:35:02 +00:00
|
|
|
if (!boundingRect.intersects(start_end)) {
|
|
|
|
return ret;
|
2022-03-08 10:10:43 +00:00
|
|
|
} else {
|
2011-08-18 01:03:46 +00:00
|
|
|
// case 1(b)
|
2011-08-03 09:28:33 +00:00
|
|
|
/**
|
|
|
|
note that, after swapping of start and end, we know that,
|
|
|
|
start is always left to end. but, we cannot say start is
|
|
|
|
positioned upper than end.
|
|
|
|
**/
|
|
|
|
// if start is left to content rect take it to content rect boundary
|
2011-06-25 05:11:33 +00:00
|
|
|
if (startC.x * scaleX < minX) {
|
|
|
|
startC.x = minX / scaleX;
|
2022-03-08 10:10:43 +00:00
|
|
|
}
|
2011-06-25 05:11:33 +00:00
|
|
|
if (endC.x * scaleX > maxX) {
|
|
|
|
endC.x = maxX / scaleX;
|
2022-03-08 10:10:43 +00:00
|
|
|
}
|
2007-09-03 19:35:55 +00:00
|
|
|
|
2011-08-03 09:28:33 +00:00
|
|
|
// if start is top to end (selection type 01)
|
2011-06-25 05:11:33 +00:00
|
|
|
if (startC.y * scaleY < minY) {
|
|
|
|
startC.y = minY / scaleY;
|
2022-03-08 10:10:43 +00:00
|
|
|
}
|
2011-06-25 05:11:33 +00:00
|
|
|
if (endC.y * scaleY > maxY) {
|
|
|
|
endC.y = maxY / scaleY;
|
2022-03-08 10:10:43 +00:00
|
|
|
}
|
2007-09-03 19:35:55 +00:00
|
|
|
|
2011-08-03 09:28:33 +00:00
|
|
|
// if start is bottom to end (selection type 02)
|
|
|
|
if (startC.y * scaleY > maxY) {
|
|
|
|
startC.y = maxY / scaleY;
|
2022-03-08 10:10:43 +00:00
|
|
|
}
|
2011-08-03 09:28:33 +00:00
|
|
|
if (endC.y * scaleY < minY) {
|
|
|
|
endC.y = minY / scaleY;
|
2022-03-08 10:10:43 +00:00
|
|
|
}
|
2011-06-25 05:11:33 +00:00
|
|
|
}
|
2007-09-03 19:35:55 +00:00
|
|
|
|
2008-11-11 18:48:40 +00:00
|
|
|
TextList::ConstIterator it = d->m_words.constBegin(), itEnd = d->m_words.constEnd();
|
2011-08-04 08:35:09 +00:00
|
|
|
TextList::ConstIterator start = it, end = itEnd, tmpIt = it; //, tmpItEnd = itEnd;
|
2017-12-05 22:07:10 +00:00
|
|
|
const MergeSide side = d->m_page ? (MergeSide)d->m_page->totalOrientation() : MergeRight;
|
2011-06-24 16:53:44 +00:00
|
|
|
|
2011-08-04 04:34:11 +00:00
|
|
|
NormalizedRect tmp;
|
2011-08-18 01:03:46 +00:00
|
|
|
// case 2(a)
|
2007-09-03 19:35:55 +00:00
|
|
|
for (; it != itEnd; ++it) {
|
2008-05-01 19:03:02 +00:00
|
|
|
tmp = (*it)->area;
|
2011-08-04 04:34:11 +00:00
|
|
|
if (tmp.contains(startC.x, startC.y)) {
|
|
|
|
start = it;
|
|
|
|
}
|
|
|
|
if (tmp.contains(endC.x, endC.y)) {
|
|
|
|
end = it;
|
|
|
|
}
|
2011-07-14 11:35:02 +00:00
|
|
|
}
|
2011-06-25 05:11:33 +00:00
|
|
|
|
2011-08-18 01:03:46 +00:00
|
|
|
// case 2(b)
|
2011-07-14 11:35:02 +00:00
|
|
|
it = tmpIt;
|
2011-08-21 04:44:42 +00:00
|
|
|
if (start == it && end == itEnd) {
|
2011-07-14 11:35:02 +00:00
|
|
|
for (; it != itEnd; ++it) {
|
2018-11-14 19:12:15 +00:00
|
|
|
// is there any text rectangle within the start_end rect
|
2011-08-03 09:28:33 +00:00
|
|
|
tmp = (*it)->area;
|
2011-07-14 11:35:02 +00:00
|
|
|
if (start_end.intersects(tmp)) {
|
|
|
|
break;
|
2022-03-08 10:10:43 +00:00
|
|
|
}
|
2007-09-03 19:35:55 +00:00
|
|
|
}
|
2011-07-14 11:35:02 +00:00
|
|
|
|
2011-08-03 09:28:33 +00:00
|
|
|
// we have searched every text entities, but none is within the rectangle created by start and end
|
|
|
|
// so, no selection should be done
|
2011-08-21 04:44:42 +00:00
|
|
|
if (it == itEnd) {
|
2011-07-14 11:35:02 +00:00
|
|
|
return ret;
|
2007-09-03 19:35:55 +00:00
|
|
|
}
|
|
|
|
}
|
2011-08-04 04:34:11 +00:00
|
|
|
it = tmpIt;
|
|
|
|
bool selection_two_start = false;
|
2011-08-01 13:26:36 +00:00
|
|
|
|
2011-08-18 01:03:46 +00:00
|
|
|
// case 3.a
|
2011-08-21 04:44:42 +00:00
|
|
|
if (start == it) {
|
2011-08-03 09:28:33 +00:00
|
|
|
NormalizedRect rect;
|
2011-08-01 13:26:36 +00:00
|
|
|
|
2011-08-03 09:28:33 +00:00
|
|
|
// selection type 01
|
2011-08-21 04:44:42 +00:00
|
|
|
if (startC.y <= endC.y) {
|
|
|
|
for (; it != itEnd; ++it) {
|
2011-07-28 12:53:40 +00:00
|
|
|
rect = (*it)->area;
|
2023-08-10 22:47:57 +00:00
|
|
|
bool flagV = !rect.isBottom(startC);
|
2011-07-28 12:53:40 +00:00
|
|
|
|
2011-08-21 04:44:42 +00:00
|
|
|
if (flagV && rect.isRight(startC)) {
|
2011-07-28 12:53:40 +00:00
|
|
|
start = it;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2011-08-03 09:28:33 +00:00
|
|
|
// selection type 02
|
2011-08-21 04:44:42 +00:00
|
|
|
else {
|
2011-08-04 04:34:11 +00:00
|
|
|
selection_two_start = true;
|
2011-08-03 09:28:33 +00:00
|
|
|
int distance = scaleX + scaleY + 100;
|
2011-08-04 04:34:11 +00:00
|
|
|
int count = 0;
|
|
|
|
|
2011-08-21 04:44:42 +00:00
|
|
|
for (; it != itEnd; ++it) {
|
2011-08-03 09:28:33 +00:00
|
|
|
rect = (*it)->area;
|
2011-06-25 05:11:33 +00:00
|
|
|
|
2011-08-21 04:44:42 +00:00
|
|
|
if (rect.isBottomOrLevel(startC) && rect.isRight(startC)) {
|
2011-08-04 04:34:11 +00:00
|
|
|
count++;
|
2011-08-03 09:28:33 +00:00
|
|
|
QRect entRect = rect.geometry(scaleX, scaleY);
|
|
|
|
int xdist, ydist;
|
|
|
|
xdist = entRect.center().x() - startC.x * scaleX;
|
|
|
|
ydist = entRect.center().y() - startC.y * scaleY;
|
2011-07-28 12:53:40 +00:00
|
|
|
|
2011-08-03 09:28:33 +00:00
|
|
|
// make them positive
|
|
|
|
if (xdist < 0) {
|
|
|
|
xdist = -xdist;
|
2022-03-08 10:10:43 +00:00
|
|
|
}
|
2011-08-03 09:28:33 +00:00
|
|
|
if (ydist < 0) {
|
|
|
|
ydist = -ydist;
|
2022-03-08 10:10:43 +00:00
|
|
|
}
|
2011-07-28 12:53:40 +00:00
|
|
|
|
2011-08-21 04:44:42 +00:00
|
|
|
if ((xdist + ydist) < distance) {
|
2011-08-03 09:28:33 +00:00
|
|
|
distance = xdist + ydist;
|
|
|
|
start = it;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2011-07-29 12:18:41 +00:00
|
|
|
|
2011-08-18 01:03:46 +00:00
|
|
|
// case 3.b
|
2011-08-21 04:44:42 +00:00
|
|
|
if (end == itEnd) {
|
2011-08-18 01:03:46 +00:00
|
|
|
it = tmpIt;
|
2011-08-03 09:28:33 +00:00
|
|
|
itEnd = itEnd - 1;
|
2011-07-29 12:18:41 +00:00
|
|
|
|
2011-08-03 09:28:33 +00:00
|
|
|
NormalizedRect rect;
|
2011-07-29 12:18:41 +00:00
|
|
|
|
2011-08-21 04:44:42 +00:00
|
|
|
if (startC.y <= endC.y) {
|
|
|
|
for (; itEnd >= it; itEnd--) {
|
2011-08-03 09:28:33 +00:00
|
|
|
rect = (*itEnd)->area;
|
2023-08-10 22:47:57 +00:00
|
|
|
bool flagV = !rect.isTop(endC);
|
2011-07-29 12:18:41 +00:00
|
|
|
|
2011-08-21 04:44:42 +00:00
|
|
|
if (flagV && rect.isLeft(endC)) {
|
2011-08-03 09:28:33 +00:00
|
|
|
end = itEnd;
|
|
|
|
break;
|
2011-08-01 13:26:36 +00:00
|
|
|
}
|
2011-06-25 05:11:33 +00:00
|
|
|
}
|
|
|
|
}
|
2011-07-28 12:53:40 +00:00
|
|
|
|
2011-08-21 04:44:42 +00:00
|
|
|
else {
|
2011-08-01 13:26:36 +00:00
|
|
|
int distance = scaleX + scaleY + 100;
|
2011-08-21 04:44:42 +00:00
|
|
|
for (; itEnd >= it; itEnd--) {
|
2011-08-03 09:28:33 +00:00
|
|
|
rect = (*itEnd)->area;
|
2011-07-29 12:18:41 +00:00
|
|
|
|
2011-08-21 04:44:42 +00:00
|
|
|
if (rect.isTopOrLevel(endC) && rect.isLeft(endC)) {
|
2011-08-03 09:28:33 +00:00
|
|
|
QRect entRect = rect.geometry(scaleX, scaleY);
|
|
|
|
int xdist, ydist;
|
|
|
|
xdist = entRect.center().x() - endC.x * scaleX;
|
|
|
|
ydist = entRect.center().y() - endC.y * scaleY;
|
2011-07-28 12:53:40 +00:00
|
|
|
|
2011-08-03 09:28:33 +00:00
|
|
|
// make them positive
|
|
|
|
if (xdist < 0) {
|
|
|
|
xdist = -xdist;
|
2022-03-08 10:10:43 +00:00
|
|
|
}
|
2011-08-03 09:28:33 +00:00
|
|
|
if (ydist < 0) {
|
|
|
|
ydist = -ydist;
|
2022-03-08 10:10:43 +00:00
|
|
|
}
|
2011-08-03 09:28:33 +00:00
|
|
|
|
2011-08-21 04:44:42 +00:00
|
|
|
if ((xdist + ydist) < distance) {
|
2011-08-03 09:28:33 +00:00
|
|
|
distance = xdist + ydist;
|
|
|
|
end = itEnd;
|
|
|
|
}
|
|
|
|
}
|
2011-08-01 13:26:36 +00:00
|
|
|
}
|
|
|
|
}
|
2011-06-25 05:11:33 +00:00
|
|
|
}
|
|
|
|
|
2011-08-18 01:03:46 +00:00
|
|
|
/* if start and end in selection 02 are in the same column, and we
|
|
|
|
start at an empty space we have to remove the selection of last
|
|
|
|
character
|
|
|
|
*/
|
2011-08-21 04:44:42 +00:00
|
|
|
if (selection_two_start) {
|
|
|
|
if (start > end) {
|
2011-08-04 04:34:11 +00:00
|
|
|
start = start - 1;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2011-08-18 01:03:46 +00:00
|
|
|
// if start is less than end swap them
|
2011-08-21 04:44:42 +00:00
|
|
|
if (start > end) {
|
2011-06-24 16:53:44 +00:00
|
|
|
it = start;
|
|
|
|
start = end;
|
|
|
|
end = it;
|
|
|
|
}
|
|
|
|
|
2011-08-18 01:03:46 +00:00
|
|
|
// removes the possibility of crash, in case none of 1 to 3 is true
|
2011-07-14 11:35:02 +00:00
|
|
|
if (end == d->m_words.constEnd()) {
|
|
|
|
end--;
|
2022-03-08 10:10:43 +00:00
|
|
|
}
|
2011-06-27 13:58:24 +00:00
|
|
|
|
2011-08-21 04:44:42 +00:00
|
|
|
for (; start <= end; start++) {
|
2011-06-24 16:53:44 +00:00
|
|
|
ret->appendShape((*start)->transformedArea(matrix), side);
|
2011-07-14 11:35:02 +00:00
|
|
|
}
|
2011-06-24 16:53:44 +00:00
|
|
|
|
2007-01-05 14:40:38 +00:00
|
|
|
return ret;
|
- GIGANTIC 2700 line diff with LOTS OF FEATURES!
- 1. editor-like text selection, and I do mean it, its not pseudo-editor
(like the ones acroread and kviewshell have) it doesnt intersect the
selection area with words under it, no, it does a lot more, including
work on cursors and searching for the text area closest to the given
cursor
- 2. rotation support, change the orientation of the documents if
you need too :)
- 3. the kfaxview backend works beautifully, porting kviewshell backends
is damn easy ! djvu and dvi will be next!
- 4. Hardware Blending of selection rectangles! We now use XRender
instead of KImageEffect, makes a damn faster blend!
- 5. Overview mode - as seen in Kviewshell, but quite a bit extended,
the kviewshell is only one state, while we support it in both
continous and non-continous form
- BTW. I coded all those features myself, (apart from kfaxview backend library)
it is an impressive bit right? but oKular cant be run by only one person,
join in on the fun! i can introduce you into the code just mail niedakh@gmail.com
svn path=/trunk/playground/graphics/oKular/kpdf/; revision=509871
2006-02-15 18:54:49 +00:00
|
|
|
}
|
- Page/Link: tooltips for links backported
- Page: rotation does not switch height and width
- Document/Part/Generator:
1. Add API for attaching stuff to the interface: ActionCollection and the Navigation Panel
also add possibility to merge an XML .rc file with menu layout. Relevant functions are:
QString Generator::getXMLFile(), returns a QString with your .rc file name.
void Generator::setupGUI (KActionCollection* , QToolbox* ), add your components to the user interface
2. Supporting backend settings:
If during startup, backends which provide a configuration ([X-KDE-oKularHasInternalSettings]
set to true) are found, a menu item: configure backends is created, clicking on it results in
loading all the generators that have settings, but not those that dont. the Generator::addPages(KConfigDialog *dlg)
function should be overloaded by a generator and dlg->addPage should be used to add pages.
If a user opens a file that needs an already loaded generator, the already loaded one is used instead of loading another.
3. Error/Warning/Notice sending support, to send a notice/error/warning, add a relevant notice/error/warning(QString& txt ,int duration)
to the generator class, and sending a message to the user is as simple as emitting a signal!
4. Intercepting of events generated by the PageView is done by Generator::handleEvent(QEvent*), subclass it, do a switch on QEvent::type(), handle your
event and return true if pageview is to proceed with its handling or false if not.
5. Support configuring the KPrinter on the generator side, use Generator::canConfigurePrinter(), return true there, and you get a nonconfigured KPrinter in your
Generator::print()
6. PixmapRequest handling update:
a.) Generator::canGeneratePixmap is now Generator::canGeneratePixmap(bool async)
b.) Document::sendGeneratorRequests is a slot now
c.) Old way of sending pixmaps (Document::requestPixmaps(QValueList<PixmapRequest*> checking if we can generate pixmap if not, waiting for receiving)
is replaced with: requestPixmaps only queues the pixmap all checking if w can generate is done in sendGeneratorReqest, the sendGeneratorRequest is
run in three places:
1. in requestPixmaps when we receive a request
2. in requestDone if pixmapStack is not empty
3. sendGeneratorRequest, apart from removing invalid requests, takes the current request and if generator canGeratePixmap(request->async)
it removes the pixmap from stack and sends to generator if not, QTimer::singleshots to itself after 20ms, it ends when stack has no valid pixmap request
7. Added a commented out zoom field to PixmapGenerator, mightcome in handy sometime
- TextPage: add instructions that handle simplyfing the RegularAreaRect, no more double painted borders in selection rectangles, this rocks.
svn path=/trunk/playground/graphics/oKular/kpdf/; revision=445196
2005-08-10 16:14:39 +00:00
|
|
|
|
2007-01-05 15:00:12 +00:00
|
|
|
RegularAreaRect *TextPage::findText(int searchID, const QString &query, SearchDirection direct, Qt::CaseSensitivity caseSensitivity, const RegularAreaRect *area)
|
2005-07-15 18:20:57 +00:00
|
|
|
{
|
2006-11-23 16:54:28 +00:00
|
|
|
SearchDirection dir = direct;
|
2005-07-15 18:20:57 +00:00
|
|
|
// invalid search request
|
2008-03-14 13:45:33 +00:00
|
|
|
if (d->m_words.isEmpty() || query.isEmpty() || (area && area->isNull())) {
|
2017-09-05 21:27:18 +00:00
|
|
|
return nullptr;
|
2022-03-08 10:10:43 +00:00
|
|
|
}
|
2008-05-01 19:03:02 +00:00
|
|
|
TextList::ConstIterator start;
|
2013-10-18 14:28:54 +00:00
|
|
|
int start_offset = 0;
|
2008-05-01 19:03:02 +00:00
|
|
|
TextList::ConstIterator end;
|
2010-09-01 18:46:18 +00:00
|
|
|
const QMap<int, SearchPoint *>::const_iterator sIt = d->m_searchPoints.constFind(searchID);
|
2008-11-11 18:48:40 +00:00
|
|
|
if (sIt == d->m_searchPoints.constEnd()) {
|
2006-05-27 10:06:31 +00:00
|
|
|
// if no previous run of this search is found, then set it to start
|
|
|
|
// from the beginning (respecting the search direction)
|
2006-11-23 16:54:28 +00:00
|
|
|
if (dir == NextResult) {
|
2006-05-27 10:06:31 +00:00
|
|
|
dir = FromTop;
|
2006-11-23 16:54:28 +00:00
|
|
|
} else if (dir == PreviousResult) {
|
2006-05-27 10:06:31 +00:00
|
|
|
dir = FromBottom;
|
2022-03-08 10:10:43 +00:00
|
|
|
}
|
2005-07-15 18:20:57 +00:00
|
|
|
}
|
2006-05-27 10:06:31 +00:00
|
|
|
bool forward = true;
|
|
|
|
switch (dir) {
|
|
|
|
case FromTop:
|
2008-11-11 18:48:40 +00:00
|
|
|
start = d->m_words.constBegin();
|
2013-10-18 14:28:54 +00:00
|
|
|
start_offset = 0;
|
2008-11-11 18:48:40 +00:00
|
|
|
end = d->m_words.constEnd();
|
2006-05-27 10:06:31 +00:00
|
|
|
break;
|
|
|
|
case FromBottom:
|
2008-11-11 18:48:40 +00:00
|
|
|
start = d->m_words.constEnd();
|
2013-10-18 14:28:54 +00:00
|
|
|
start_offset = 0;
|
2008-11-11 18:48:40 +00:00
|
|
|
end = d->m_words.constBegin();
|
2006-05-27 10:06:31 +00:00
|
|
|
forward = false;
|
2005-07-15 18:20:57 +00:00
|
|
|
break;
|
2006-11-23 16:54:28 +00:00
|
|
|
case NextResult:
|
2008-03-14 18:15:03 +00:00
|
|
|
start = (*sIt)->it_end;
|
2013-10-18 14:28:54 +00:00
|
|
|
start_offset = (*sIt)->offset_end;
|
2008-11-11 18:48:40 +00:00
|
|
|
end = d->m_words.constEnd();
|
2006-05-27 10:06:31 +00:00
|
|
|
break;
|
2006-11-23 16:54:28 +00:00
|
|
|
case PreviousResult:
|
2008-03-14 18:15:03 +00:00
|
|
|
start = (*sIt)->it_begin;
|
2013-10-18 14:28:54 +00:00
|
|
|
start_offset = (*sIt)->offset_begin;
|
2008-11-11 18:48:40 +00:00
|
|
|
end = d->m_words.constBegin();
|
2006-05-27 10:06:31 +00:00
|
|
|
forward = false;
|
2005-07-15 18:20:57 +00:00
|
|
|
break;
|
2006-05-27 10:06:31 +00:00
|
|
|
};
|
2017-09-05 21:27:18 +00:00
|
|
|
RegularAreaRect *ret = nullptr;
|
2009-12-27 14:22:13 +00:00
|
|
|
const TextComparisonFunction cmpFn = caseSensitivity == Qt::CaseSensitive ? CaseSensitiveCmpFn : CaseInsensitiveCmpFn;
|
2006-05-27 10:06:31 +00:00
|
|
|
if (forward) {
|
2013-10-18 14:28:54 +00:00
|
|
|
ret = d->findTextInternalForward(searchID, query, cmpFn, start, start_offset, end);
|
2006-05-27 10:06:31 +00:00
|
|
|
} else {
|
2013-10-18 14:28:54 +00:00
|
|
|
ret = d->findTextInternalBackward(searchID, query, cmpFn, start, start_offset, end);
|
2006-05-27 10:06:31 +00:00
|
|
|
}
|
|
|
|
return ret;
|
2005-07-15 18:20:57 +00:00
|
|
|
}
|
|
|
|
|
2012-02-02 18:13:11 +00:00
|
|
|
// hyphenated '-' must be at the end of a word, so hyphenation means
|
|
|
|
// we have a '-' just followed by a '\n' character
|
|
|
|
// check if the string contains a '-' character
|
|
|
|
// if the '-' is the last entry
|
2014-02-25 22:57:06 +00:00
|
|
|
static int stringLengthAdaptedWithHyphen(const QString &str, const TextList::ConstIterator &it, const TextList::ConstIterator &textListEnd)
|
2012-02-02 18:13:11 +00:00
|
|
|
{
|
2020-03-14 10:41:58 +00:00
|
|
|
const int len = str.length();
|
2020-07-10 22:15:05 +00:00
|
|
|
|
2012-02-02 18:13:11 +00:00
|
|
|
// hyphenated '-' must be at the end of a word, so hyphenation means
|
|
|
|
// we have a '-' just followed by a '\n' character
|
|
|
|
// check if the string contains a '-' character
|
|
|
|
// if the '-' is the last entry
|
2016-07-11 20:07:57 +00:00
|
|
|
if (str.endsWith(QLatin1Char('-'))) {
|
2012-02-02 18:13:11 +00:00
|
|
|
// validity chek of it + 1
|
2013-10-18 14:28:54 +00:00
|
|
|
if ((it + 1) != textListEnd) {
|
2012-02-02 18:13:11 +00:00
|
|
|
// 1. if the next character is '\n'
|
|
|
|
const QString &lookahedStr = (*(it + 1))->text();
|
2016-07-11 20:07:57 +00:00
|
|
|
if (lookahedStr.startsWith(QLatin1Char('\n'))) {
|
2020-03-14 10:41:58 +00:00
|
|
|
return len - 1;
|
2012-02-02 18:13:11 +00:00
|
|
|
}
|
|
|
|
|
2020-03-14 10:41:58 +00:00
|
|
|
// 2. if the next word is in a different line or not
|
|
|
|
const NormalizedRect &hyphenArea = (*it)->area;
|
|
|
|
const NormalizedRect &lookaheadArea = (*(it + 1))->area;
|
|
|
|
|
|
|
|
// lookahead to check whether both the '-' rect and next character rect overlap
|
|
|
|
if (!doesConsumeY(hyphenArea, lookaheadArea, 70)) {
|
|
|
|
return len - 1;
|
2012-02-02 18:13:11 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// else if it is the second last entry - for example in pdf format
|
2015-10-29 12:37:11 +00:00
|
|
|
else if (str.endsWith(QLatin1String("-\n"))) {
|
2020-03-14 10:41:58 +00:00
|
|
|
return len - 2;
|
2012-02-02 18:13:11 +00:00
|
|
|
}
|
2020-07-10 22:15:05 +00:00
|
|
|
|
2012-02-02 18:13:11 +00:00
|
|
|
return len;
|
|
|
|
}
|
2005-07-15 18:20:57 +00:00
|
|
|
|
2013-10-18 14:28:54 +00:00
|
|
|
RegularAreaRect *TextPagePrivate::searchPointToArea(const SearchPoint *sp)
|
2005-07-15 18:20:57 +00:00
|
|
|
{
|
2017-12-05 22:07:10 +00:00
|
|
|
PagePrivate *pagePrivate = PagePrivate::get(m_page);
|
|
|
|
const QTransform matrix = pagePrivate ? pagePrivate->rotationMatrix() : QTransform();
|
- Page/Link: tooltips for links backported
- Page: rotation does not switch height and width
- Document/Part/Generator:
1. Add API for attaching stuff to the interface: ActionCollection and the Navigation Panel
also add possibility to merge an XML .rc file with menu layout. Relevant functions are:
QString Generator::getXMLFile(), returns a QString with your .rc file name.
void Generator::setupGUI (KActionCollection* , QToolbox* ), add your components to the user interface
2. Supporting backend settings:
If during startup, backends which provide a configuration ([X-KDE-oKularHasInternalSettings]
set to true) are found, a menu item: configure backends is created, clicking on it results in
loading all the generators that have settings, but not those that dont. the Generator::addPages(KConfigDialog *dlg)
function should be overloaded by a generator and dlg->addPage should be used to add pages.
If a user opens a file that needs an already loaded generator, the already loaded one is used instead of loading another.
3. Error/Warning/Notice sending support, to send a notice/error/warning, add a relevant notice/error/warning(QString& txt ,int duration)
to the generator class, and sending a message to the user is as simple as emitting a signal!
4. Intercepting of events generated by the PageView is done by Generator::handleEvent(QEvent*), subclass it, do a switch on QEvent::type(), handle your
event and return true if pageview is to proceed with its handling or false if not.
5. Support configuring the KPrinter on the generator side, use Generator::canConfigurePrinter(), return true there, and you get a nonconfigured KPrinter in your
Generator::print()
6. PixmapRequest handling update:
a.) Generator::canGeneratePixmap is now Generator::canGeneratePixmap(bool async)
b.) Document::sendGeneratorRequests is a slot now
c.) Old way of sending pixmaps (Document::requestPixmaps(QValueList<PixmapRequest*> checking if we can generate pixmap if not, waiting for receiving)
is replaced with: requestPixmaps only queues the pixmap all checking if w can generate is done in sendGeneratorReqest, the sendGeneratorRequest is
run in three places:
1. in requestPixmaps when we receive a request
2. in requestDone if pixmapStack is not empty
3. sendGeneratorRequest, apart from removing invalid requests, takes the current request and if generator canGeratePixmap(request->async)
it removes the pixmap from stack and sends to generator if not, QTimer::singleshots to itself after 20ms, it ends when stack has no valid pixmap request
7. Added a commented out zoom field to PixmapGenerator, mightcome in handy sometime
- TextPage: add instructions that handle simplyfing the RegularAreaRect, no more double painted borders in selection rectangles, this rocks.
svn path=/trunk/playground/graphics/oKular/kpdf/; revision=445196
2005-08-10 16:14:39 +00:00
|
|
|
RegularAreaRect *ret = new RegularAreaRect;
|
2011-03-25 20:07:45 +00:00
|
|
|
|
2013-10-18 14:28:54 +00:00
|
|
|
for (TextList::ConstIterator it = sp->it_begin;; it++) {
|
|
|
|
const TinyTextEntity *curEntity = *it;
|
|
|
|
ret->append(curEntity->transformedArea(matrix));
|
|
|
|
|
|
|
|
if (it == sp->it_end) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
ret->simplify();
|
|
|
|
return ret;
|
|
|
|
}
|
|
|
|
|
|
|
|
RegularAreaRect *TextPagePrivate::findTextInternalForward(int searchID, const QString &_query, TextComparisonFunction comparer, const TextList::ConstIterator &start, int start_offset, const TextList::ConstIterator &end)
|
|
|
|
{
|
2011-03-25 20:07:45 +00:00
|
|
|
// normalize query search all unicode (including glyphs)
|
2013-10-18 14:28:54 +00:00
|
|
|
const QString query = _query.normalized(QString::NormalizationForm_KC);
|
- Page/Link: tooltips for links backported
- Page: rotation does not switch height and width
- Document/Part/Generator:
1. Add API for attaching stuff to the interface: ActionCollection and the Navigation Panel
also add possibility to merge an XML .rc file with menu layout. Relevant functions are:
QString Generator::getXMLFile(), returns a QString with your .rc file name.
void Generator::setupGUI (KActionCollection* , QToolbox* ), add your components to the user interface
2. Supporting backend settings:
If during startup, backends which provide a configuration ([X-KDE-oKularHasInternalSettings]
set to true) are found, a menu item: configure backends is created, clicking on it results in
loading all the generators that have settings, but not those that dont. the Generator::addPages(KConfigDialog *dlg)
function should be overloaded by a generator and dlg->addPage should be used to add pages.
If a user opens a file that needs an already loaded generator, the already loaded one is used instead of loading another.
3. Error/Warning/Notice sending support, to send a notice/error/warning, add a relevant notice/error/warning(QString& txt ,int duration)
to the generator class, and sending a message to the user is as simple as emitting a signal!
4. Intercepting of events generated by the PageView is done by Generator::handleEvent(QEvent*), subclass it, do a switch on QEvent::type(), handle your
event and return true if pageview is to proceed with its handling or false if not.
5. Support configuring the KPrinter on the generator side, use Generator::canConfigurePrinter(), return true there, and you get a nonconfigured KPrinter in your
Generator::print()
6. PixmapRequest handling update:
a.) Generator::canGeneratePixmap is now Generator::canGeneratePixmap(bool async)
b.) Document::sendGeneratorRequests is a slot now
c.) Old way of sending pixmaps (Document::requestPixmaps(QValueList<PixmapRequest*> checking if we can generate pixmap if not, waiting for receiving)
is replaced with: requestPixmaps only queues the pixmap all checking if w can generate is done in sendGeneratorReqest, the sendGeneratorRequest is
run in three places:
1. in requestPixmaps when we receive a request
2. in requestDone if pixmapStack is not empty
3. sendGeneratorRequest, apart from removing invalid requests, takes the current request and if generator canGeratePixmap(request->async)
it removes the pixmap from stack and sends to generator if not, QTimer::singleshots to itself after 20ms, it ends when stack has no valid pixmap request
7. Added a commented out zoom field to PixmapGenerator, mightcome in handy sometime
- TextPage: add instructions that handle simplyfing the RegularAreaRect, no more double painted borders in selection rectangles, this rocks.
svn path=/trunk/playground/graphics/oKular/kpdf/; revision=445196
2005-08-10 16:14:39 +00:00
|
|
|
|
2005-07-15 18:20:57 +00:00
|
|
|
// j is the current position in our query
|
2020-03-14 10:41:58 +00:00
|
|
|
// queryLeft is the length of the query we have left to match
|
2013-10-18 14:28:54 +00:00
|
|
|
int j = 0, queryLeft = query.length();
|
|
|
|
|
2008-05-01 19:03:02 +00:00
|
|
|
TextList::ConstIterator it = start;
|
2013-10-18 14:28:54 +00:00
|
|
|
int offset = start_offset;
|
|
|
|
|
|
|
|
TextList::ConstIterator it_begin = TextList::ConstIterator();
|
|
|
|
int offset_begin = 0; // dummy initial value to suppress compiler warnings
|
|
|
|
|
|
|
|
while (it != end) {
|
|
|
|
const TinyTextEntity *curEntity = *it;
|
|
|
|
const QString &str = curEntity->text();
|
2020-03-14 10:41:58 +00:00
|
|
|
const int strLen = str.length();
|
|
|
|
const int adjustedLen = stringLengthAdaptedWithHyphen(str, it, m_words.constEnd());
|
|
|
|
// adjustedLen <= strLen
|
2013-10-18 14:28:54 +00:00
|
|
|
|
2020-03-14 10:41:58 +00:00
|
|
|
if (offset >= strLen) {
|
2013-10-18 14:28:54 +00:00
|
|
|
it++;
|
|
|
|
offset = 0;
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (it_begin == TextList::ConstIterator()) {
|
|
|
|
it_begin = it;
|
|
|
|
offset_begin = offset;
|
2006-05-27 10:06:31 +00:00
|
|
|
}
|
2013-10-18 14:28:54 +00:00
|
|
|
|
2020-03-14 10:41:58 +00:00
|
|
|
// Let the user write the hyphen or not when searching for text
|
|
|
|
int matchedLen = -1;
|
|
|
|
for (int matchingLen = strLen; matchingLen >= adjustedLen; matchingLen--) {
|
|
|
|
// we have equal (or less than) area of the query left as the length of the current
|
2005-07-15 18:20:57 +00:00
|
|
|
// entity
|
2020-03-14 10:41:58 +00:00
|
|
|
const int min = qMin(queryLeft, matchingLen - offset);
|
|
|
|
if (comparer(str.midRef(offset, min), query.midRef(j, min))) {
|
|
|
|
matchedLen = min;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (matchedLen == -1) {
|
|
|
|
// we have not matched
|
|
|
|
// this means we do not have a complete match
|
|
|
|
// we need to get back to query start
|
|
|
|
// and continue the search from this place
|
2005-11-18 20:48:01 +00:00
|
|
|
#ifdef DEBUG_TEXTPAGE
|
2014-09-11 17:36:01 +00:00
|
|
|
qCDebug(OkularCoreDebug) << "\tnot matched";
|
2005-11-18 20:48:01 +00:00
|
|
|
#endif
|
2020-03-14 10:41:58 +00:00
|
|
|
j = 0;
|
|
|
|
queryLeft = query.length();
|
|
|
|
it = it_begin;
|
|
|
|
offset = offset_begin + 1;
|
|
|
|
it_begin = TextList::ConstIterator();
|
|
|
|
} else {
|
|
|
|
// we have a match
|
|
|
|
// move the current position in the query
|
|
|
|
// to the position after the length of this string
|
|
|
|
// we matched
|
|
|
|
// subtract the length of the current entity from
|
|
|
|
// the left length of the query
|
2005-11-18 20:48:01 +00:00
|
|
|
#ifdef DEBUG_TEXTPAGE
|
2020-03-14 10:41:58 +00:00
|
|
|
qCDebug(OkularCoreDebug) << "\tmatched" << matchedLen;
|
2005-11-18 20:48:01 +00:00
|
|
|
#endif
|
2020-03-14 10:41:58 +00:00
|
|
|
j += matchedLen;
|
|
|
|
queryLeft -= matchedLen;
|
2013-10-18 14:28:54 +00:00
|
|
|
|
2020-03-14 10:41:58 +00:00
|
|
|
if (queryLeft == 0) {
|
|
|
|
// save or update the search point for the current searchID
|
|
|
|
QMap<int, SearchPoint *>::iterator sIt = m_searchPoints.find(searchID);
|
|
|
|
if (sIt == m_searchPoints.end()) {
|
|
|
|
sIt = m_searchPoints.insert(searchID, new SearchPoint);
|
|
|
|
}
|
|
|
|
SearchPoint *sp = *sIt;
|
|
|
|
sp->it_begin = it_begin;
|
|
|
|
sp->it_end = it;
|
|
|
|
sp->offset_begin = offset_begin;
|
|
|
|
sp->offset_end = offset + matchedLen;
|
|
|
|
return searchPointToArea(sp);
|
2006-05-27 10:06:31 +00:00
|
|
|
}
|
2020-03-14 10:41:58 +00:00
|
|
|
|
|
|
|
it++;
|
|
|
|
offset = 0;
|
2005-07-15 18:20:57 +00:00
|
|
|
}
|
2006-05-13 15:04:22 +00:00
|
|
|
}
|
2006-05-27 10:06:31 +00:00
|
|
|
// end of loop - it means that we've ended the textentities
|
2013-10-18 14:28:54 +00:00
|
|
|
|
2010-09-01 18:53:49 +00:00
|
|
|
const QMap<int, SearchPoint *>::iterator sIt = m_searchPoints.find(searchID);
|
|
|
|
if (sIt != m_searchPoints.end()) {
|
|
|
|
SearchPoint *sp = *sIt;
|
|
|
|
m_searchPoints.erase(sIt);
|
2006-05-27 10:06:31 +00:00
|
|
|
delete sp;
|
|
|
|
}
|
2017-09-05 21:27:18 +00:00
|
|
|
return nullptr;
|
2005-07-15 18:20:57 +00:00
|
|
|
}
|
|
|
|
|
2008-03-14 14:14:06 +00:00
|
|
|
RegularAreaRect *TextPagePrivate::findTextInternalBackward(int searchID, const QString &_query, TextComparisonFunction comparer, const TextList::ConstIterator &start, int start_offset, const TextList::ConstIterator &end)
|
|
|
|
{
|
2011-03-25 20:07:45 +00:00
|
|
|
// normalize query to search all unicode (including glyphs)
|
2013-10-18 14:28:54 +00:00
|
|
|
const QString query = _query.normalized(QString::NormalizationForm_KC);
|
2008-03-14 14:14:06 +00:00
|
|
|
|
|
|
|
// j is the current position in our query
|
|
|
|
// len is the length of the string in TextEntity
|
|
|
|
// queryLeft is the length of the query we have left
|
2013-10-18 14:28:54 +00:00
|
|
|
int j = query.length(), queryLeft = query.length();
|
|
|
|
|
2008-05-01 19:03:02 +00:00
|
|
|
TextList::ConstIterator it = start;
|
2013-10-18 14:28:54 +00:00
|
|
|
int offset = start_offset;
|
|
|
|
|
|
|
|
TextList::ConstIterator it_begin = TextList::ConstIterator();
|
|
|
|
int offset_begin = 0; // dummy initial value to suppress compiler warnings
|
|
|
|
|
2008-03-14 14:14:06 +00:00
|
|
|
while (true) {
|
2013-10-18 14:28:54 +00:00
|
|
|
if (offset <= 0) {
|
|
|
|
if (it == end) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
it--;
|
2008-03-14 14:14:06 +00:00
|
|
|
}
|
2013-10-18 14:28:54 +00:00
|
|
|
|
|
|
|
const TinyTextEntity *curEntity = *it;
|
|
|
|
const QString &str = curEntity->text();
|
2020-03-14 10:41:58 +00:00
|
|
|
const int strLen = str.length();
|
|
|
|
const int adjustedLen = stringLengthAdaptedWithHyphen(str, it, m_words.constEnd());
|
|
|
|
// adjustedLen <= strLen
|
2013-10-18 14:28:54 +00:00
|
|
|
|
|
|
|
if (offset <= 0) {
|
2020-03-14 10:41:58 +00:00
|
|
|
offset = strLen;
|
2013-10-18 14:28:54 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if (it_begin == TextList::ConstIterator()) {
|
|
|
|
it_begin = it;
|
|
|
|
offset_begin = offset;
|
|
|
|
}
|
|
|
|
|
2020-03-14 10:41:58 +00:00
|
|
|
// Let the user write the hyphen or not when searching for text
|
|
|
|
int matchedLen = -1;
|
|
|
|
// we have equal (or less than) area of the query left as the length of the current
|
|
|
|
// entity
|
|
|
|
for (int matchingLen = strLen; matchingLen >= adjustedLen; matchingLen--) {
|
|
|
|
const int hyphenOffset = (strLen - matchingLen);
|
|
|
|
const int min = qMin(queryLeft + hyphenOffset, offset);
|
|
|
|
if (comparer(str.midRef(offset - min, min - hyphenOffset), query.midRef(j - min + hyphenOffset, min - hyphenOffset))) {
|
|
|
|
matchedLen = min - hyphenOffset;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (matchedLen == -1) {
|
|
|
|
// we have not matched
|
|
|
|
// this means we do not have a complete match
|
|
|
|
// we need to get back to query start
|
|
|
|
// and continue the search from this place
|
2008-03-14 14:14:06 +00:00
|
|
|
#ifdef DEBUG_TEXTPAGE
|
2020-03-14 10:41:58 +00:00
|
|
|
qCDebug(OkularCoreDebug) << "\tnot matched";
|
2008-03-14 14:14:06 +00:00
|
|
|
#endif
|
2013-10-18 14:28:54 +00:00
|
|
|
|
2020-03-14 10:41:58 +00:00
|
|
|
j = query.length();
|
|
|
|
queryLeft = query.length();
|
|
|
|
it = it_begin;
|
|
|
|
offset = offset_begin - 1;
|
|
|
|
it_begin = TextList::ConstIterator();
|
|
|
|
} else {
|
|
|
|
// we have a match
|
|
|
|
// move the current position in the query
|
|
|
|
// to the position after the length of this string
|
|
|
|
// we matched
|
|
|
|
// subtract the length of the current entity from
|
|
|
|
// the left length of the query
|
2008-03-14 14:14:06 +00:00
|
|
|
#ifdef DEBUG_TEXTPAGE
|
2020-03-14 10:41:58 +00:00
|
|
|
qCDebug(OkularCoreDebug) << "\tmatched";
|
2008-03-14 14:14:06 +00:00
|
|
|
#endif
|
2020-03-14 10:41:58 +00:00
|
|
|
j -= matchedLen;
|
|
|
|
queryLeft -= matchedLen;
|
2008-03-14 14:14:06 +00:00
|
|
|
|
2020-03-14 10:41:58 +00:00
|
|
|
if (queryLeft == 0) {
|
|
|
|
// save or update the search point for the current searchID
|
|
|
|
QMap<int, SearchPoint *>::iterator sIt = m_searchPoints.find(searchID);
|
|
|
|
if (sIt == m_searchPoints.end()) {
|
|
|
|
sIt = m_searchPoints.insert(searchID, new SearchPoint);
|
|
|
|
}
|
|
|
|
SearchPoint *sp = *sIt;
|
|
|
|
sp->it_begin = it;
|
|
|
|
sp->it_end = it_begin;
|
|
|
|
sp->offset_begin = offset - matchedLen;
|
|
|
|
sp->offset_end = offset_begin;
|
|
|
|
return searchPointToArea(sp);
|
2008-03-14 14:14:06 +00:00
|
|
|
}
|
2013-10-18 14:28:54 +00:00
|
|
|
|
2020-03-14 10:41:58 +00:00
|
|
|
offset = 0;
|
2008-03-14 14:14:06 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
// end of loop - it means that we've ended the textentities
|
2013-10-18 14:28:54 +00:00
|
|
|
|
2010-09-01 18:46:18 +00:00
|
|
|
const QMap<int, SearchPoint *>::iterator sIt = m_searchPoints.find(searchID);
|
2008-03-14 14:14:06 +00:00
|
|
|
if (sIt != m_searchPoints.end()) {
|
|
|
|
SearchPoint *sp = *sIt;
|
|
|
|
m_searchPoints.erase(sIt);
|
|
|
|
delete sp;
|
|
|
|
}
|
2017-09-05 21:27:18 +00:00
|
|
|
return nullptr;
|
2008-03-14 14:14:06 +00:00
|
|
|
}
|
|
|
|
|
2006-11-23 17:11:04 +00:00
|
|
|
QString TextPage::text(const RegularAreaRect *area) const
|
2009-11-12 21:48:43 +00:00
|
|
|
{
|
|
|
|
return text(area, AnyPixelTextAreaInclusionBehaviour);
|
|
|
|
}
|
|
|
|
|
|
|
|
QString TextPage::text(const RegularAreaRect *area, TextAreaInclusionBehaviour b) const
|
2005-07-15 18:20:57 +00:00
|
|
|
{
|
2006-11-24 21:02:38 +00:00
|
|
|
if (area && area->isNull()) {
|
2006-05-26 14:32:35 +00:00
|
|
|
return QString();
|
2022-03-08 10:10:43 +00:00
|
|
|
}
|
2005-07-15 18:20:57 +00:00
|
|
|
|
2008-11-11 18:48:40 +00:00
|
|
|
TextList::ConstIterator it = d->m_words.constBegin(), itEnd = d->m_words.constEnd();
|
2006-11-24 21:02:38 +00:00
|
|
|
QString ret;
|
|
|
|
if (area) {
|
|
|
|
for (; it != itEnd; ++it) {
|
2009-11-12 21:48:43 +00:00
|
|
|
if (b == AnyPixelTextAreaInclusionBehaviour) {
|
|
|
|
if (area->intersects((*it)->area)) {
|
|
|
|
ret += (*it)->text();
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
NormalizedPoint center = (*it)->area.center();
|
|
|
|
if (area->contains(center.x, center.y)) {
|
|
|
|
ret += (*it)->text();
|
|
|
|
}
|
2006-11-24 21:02:38 +00:00
|
|
|
}
|
2005-07-15 18:20:57 +00:00
|
|
|
}
|
2006-11-24 21:02:38 +00:00
|
|
|
} else {
|
|
|
|
for (; it != itEnd; ++it) {
|
|
|
|
ret += (*it)->text();
|
2022-03-08 10:10:43 +00:00
|
|
|
}
|
2006-11-24 21:02:38 +00:00
|
|
|
}
|
2005-07-15 18:20:57 +00:00
|
|
|
return ret;
|
|
|
|
}
|
2011-06-24 16:53:44 +00:00
|
|
|
|
2011-12-03 12:50:37 +00:00
|
|
|
static bool compareTinyTextEntityX(const WordWithCharacters &first, const WordWithCharacters &second)
|
2011-08-21 04:44:42 +00:00
|
|
|
{
|
2011-12-03 12:50:37 +00:00
|
|
|
QRect firstArea = first.area().roundedGeometry(1000, 1000);
|
|
|
|
QRect secondArea = second.area().roundedGeometry(1000, 1000);
|
2011-06-29 12:17:15 +00:00
|
|
|
|
|
|
|
return firstArea.left() < secondArea.left();
|
|
|
|
}
|
|
|
|
|
2011-12-03 12:50:37 +00:00
|
|
|
static bool compareTinyTextEntityY(const WordWithCharacters &first, const WordWithCharacters &second)
|
2011-08-21 04:44:42 +00:00
|
|
|
{
|
2011-12-03 12:50:37 +00:00
|
|
|
const QRect firstArea = first.area().roundedGeometry(1000, 1000);
|
|
|
|
const QRect secondArea = second.area().roundedGeometry(1000, 1000);
|
2011-06-29 12:17:15 +00:00
|
|
|
|
2011-07-03 08:56:27 +00:00
|
|
|
return firstArea.top() < secondArea.top();
|
2011-06-29 12:17:15 +00:00
|
|
|
}
|
|
|
|
|
2011-08-18 01:03:46 +00:00
|
|
|
/**
|
2011-12-03 12:50:37 +00:00
|
|
|
* Sets a new world list. Deleting the contents of the old one
|
2011-08-18 01:03:46 +00:00
|
|
|
*/
|
2011-08-23 14:10:43 +00:00
|
|
|
void TextPagePrivate::setWordList(const TextList &list)
|
2011-08-21 04:44:42 +00:00
|
|
|
{
|
2011-08-22 17:17:47 +00:00
|
|
|
qDeleteAll(m_words);
|
2011-08-23 14:10:43 +00:00
|
|
|
m_words = list;
|
2011-07-10 14:54:38 +00:00
|
|
|
}
|
|
|
|
|
2011-08-18 01:03:46 +00:00
|
|
|
/**
|
2011-08-19 01:01:11 +00:00
|
|
|
* Remove all the spaces in between texts. It will make all the generators
|
|
|
|
* same, whether they save spaces(like pdf) or not(like djvu).
|
|
|
|
*/
|
2011-12-03 12:50:37 +00:00
|
|
|
static void removeSpace(TextList *words)
|
2011-08-21 04:44:42 +00:00
|
|
|
{
|
2012-02-22 22:20:44 +00:00
|
|
|
TextList::Iterator it = words->begin();
|
2016-07-11 20:07:57 +00:00
|
|
|
const QString str(QLatin1Char(' '));
|
2011-08-19 01:01:11 +00:00
|
|
|
|
2012-02-22 22:20:44 +00:00
|
|
|
while (it != words->end()) {
|
2011-08-21 04:44:42 +00:00
|
|
|
if ((*it)->text() == str) {
|
2011-12-03 12:50:37 +00:00
|
|
|
it = words->erase(it);
|
2011-08-22 18:29:36 +00:00
|
|
|
} else {
|
|
|
|
++it;
|
2011-08-19 01:01:11 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2011-12-03 12:50:37 +00:00
|
|
|
* We will read the TinyTextEntity from characters and try to create words from there.
|
|
|
|
* Note: characters might be already characters for some generators, but we will keep
|
|
|
|
* the nomenclature characters for the generator produced data. The resulting
|
|
|
|
* WordsWithCharacters memory has to be managed by the caller, both the
|
|
|
|
* WordWithCharacters::word and WordWithCharacters::characters contents
|
2011-08-18 01:03:46 +00:00
|
|
|
*/
|
2011-12-03 12:50:37 +00:00
|
|
|
static WordsWithCharacters makeWordFromCharacters(const TextList &characters, int pageWidth, int pageHeight)
|
2011-08-21 04:44:42 +00:00
|
|
|
{
|
2011-08-19 01:01:11 +00:00
|
|
|
/**
|
2011-12-03 12:50:37 +00:00
|
|
|
* We will traverse characters and try to create words from the TinyTextEntities in it.
|
2011-08-19 01:01:11 +00:00
|
|
|
* We will search TinyTextEntity blocks and merge them until we get a
|
|
|
|
* space between two consecutive TinyTextEntities. When we get a space
|
|
|
|
* we can take it as a end of word. Then we store the word as a TinyTextEntity
|
|
|
|
* and keep it in newList.
|
|
|
|
|
2011-12-03 12:50:37 +00:00
|
|
|
* We create a RegionText named regionWord that contains the word and the characters associated with it and
|
|
|
|
* a rectangle area of the element in newList.
|
2011-08-19 01:01:11 +00:00
|
|
|
|
|
|
|
*/
|
2011-12-03 12:50:37 +00:00
|
|
|
WordsWithCharacters wordsWithCharacters;
|
2011-07-01 19:40:37 +00:00
|
|
|
|
2011-12-03 12:50:37 +00:00
|
|
|
TextList::ConstIterator it = characters.begin(), itEnd = characters.end(), tmpIt;
|
2011-07-11 05:37:34 +00:00
|
|
|
int newLeft, newRight, newTop, newBottom;
|
2011-07-02 15:24:50 +00:00
|
|
|
int index = 0;
|
2011-07-12 14:24:17 +00:00
|
|
|
|
2011-08-21 04:44:42 +00:00
|
|
|
for (; it != itEnd; it++) {
|
2011-08-22 18:20:16 +00:00
|
|
|
QString textString = (*it)->text();
|
2011-07-01 19:40:37 +00:00
|
|
|
QString newString;
|
2011-07-12 14:24:17 +00:00
|
|
|
QRect lineArea = (*it)->area.roundedGeometry(pageWidth, pageHeight), elementArea;
|
2011-12-03 12:50:37 +00:00
|
|
|
TextList wordCharacters;
|
2011-07-12 14:24:17 +00:00
|
|
|
tmpIt = it;
|
2011-07-01 19:40:37 +00:00
|
|
|
int space = 0;
|
|
|
|
|
2011-11-30 21:54:19 +00:00
|
|
|
while (!space) {
|
2020-02-21 15:34:08 +00:00
|
|
|
if (!textString.isEmpty()) {
|
2011-07-12 14:24:17 +00:00
|
|
|
newString.append(textString);
|
|
|
|
|
2011-08-19 01:01:11 +00:00
|
|
|
// when textString is the start of the word
|
2011-11-30 21:54:19 +00:00
|
|
|
if (tmpIt == it) {
|
2011-07-12 14:24:17 +00:00
|
|
|
NormalizedRect newRect(lineArea, pageWidth, pageHeight);
|
2011-12-03 12:50:37 +00:00
|
|
|
wordCharacters.append(new TinyTextEntity(textString.normalized(QString::NormalizationForm_KC), newRect));
|
2011-08-21 04:44:42 +00:00
|
|
|
} else {
|
2011-07-12 14:24:17 +00:00
|
|
|
NormalizedRect newRect(elementArea, pageWidth, pageHeight);
|
2011-12-03 12:50:37 +00:00
|
|
|
wordCharacters.append(new TinyTextEntity(textString.normalized(QString::NormalizationForm_KC), newRect));
|
2011-07-12 14:24:17 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2011-11-30 21:54:19 +00:00
|
|
|
++it;
|
2011-07-03 08:56:27 +00:00
|
|
|
|
2011-08-19 01:01:11 +00:00
|
|
|
/*
|
|
|
|
we must have to put this line before the if condition of it==itEnd
|
|
|
|
otherwise the last character can be missed
|
|
|
|
*/
|
2011-11-30 21:54:19 +00:00
|
|
|
if (it == itEnd) {
|
|
|
|
break;
|
2022-03-08 10:10:43 +00:00
|
|
|
}
|
2011-07-12 14:24:17 +00:00
|
|
|
elementArea = (*it)->area.roundedGeometry(pageWidth, pageHeight);
|
2011-11-30 21:54:19 +00:00
|
|
|
if (!doesConsumeY(elementArea, lineArea, 60)) {
|
|
|
|
--it;
|
2011-07-02 15:24:50 +00:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
2011-08-22 18:19:23 +00:00
|
|
|
const int text_y1 = elementArea.top(), text_x1 = elementArea.left(), text_y2 = elementArea.y() + elementArea.height(), text_x2 = elementArea.x() + elementArea.width();
|
|
|
|
const int line_y1 = lineArea.top(), line_x1 = lineArea.left(), line_y2 = lineArea.y() + lineArea.height(), line_x2 = lineArea.x() + lineArea.width();
|
2011-07-01 19:40:37 +00:00
|
|
|
|
2011-07-08 18:12:39 +00:00
|
|
|
space = elementArea.left() - lineArea.right();
|
|
|
|
|
2011-11-30 22:01:36 +00:00
|
|
|
if (space != 0) {
|
2011-07-01 19:40:37 +00:00
|
|
|
it--;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
2011-07-08 18:12:39 +00:00
|
|
|
newLeft = text_x1 < line_x1 ? text_x1 : line_x1;
|
|
|
|
newRight = line_x2 > text_x2 ? line_x2 : text_x2;
|
2011-07-01 19:40:37 +00:00
|
|
|
newTop = text_y1 > line_y1 ? line_y1 : text_y1;
|
|
|
|
newBottom = text_y2 > line_y2 ? text_y2 : line_y2;
|
|
|
|
|
|
|
|
lineArea.setLeft(newLeft);
|
|
|
|
lineArea.setTop(newTop);
|
2011-07-08 18:12:39 +00:00
|
|
|
lineArea.setWidth(newRight - newLeft);
|
|
|
|
lineArea.setHeight(newBottom - newTop);
|
2011-07-02 15:24:50 +00:00
|
|
|
|
2011-08-22 18:20:16 +00:00
|
|
|
textString = (*it)->text();
|
2011-07-01 19:40:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// if newString is not empty, save it
|
2011-11-30 22:04:11 +00:00
|
|
|
if (!newString.isEmpty()) {
|
2011-11-30 21:54:19 +00:00
|
|
|
const NormalizedRect newRect(lineArea, pageWidth, pageHeight);
|
2011-12-03 12:50:37 +00:00
|
|
|
TinyTextEntity *word = new TinyTextEntity(newString.normalized(QString::NormalizationForm_KC), newRect);
|
|
|
|
wordsWithCharacters.append(WordWithCharacters(word, wordCharacters));
|
2011-07-13 11:16:03 +00:00
|
|
|
|
2011-07-02 15:24:50 +00:00
|
|
|
index++;
|
2011-07-01 19:40:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if (it == itEnd) {
|
|
|
|
break;
|
2022-03-08 10:10:43 +00:00
|
|
|
}
|
2011-07-01 19:40:37 +00:00
|
|
|
}
|
2020-07-10 22:15:05 +00:00
|
|
|
|
2011-12-03 12:50:37 +00:00
|
|
|
return wordsWithCharacters;
|
2011-07-01 19:40:37 +00:00
|
|
|
}
|
|
|
|
|
2011-08-18 01:03:46 +00:00
|
|
|
/**
|
|
|
|
* Create Lines from the words and sort them
|
|
|
|
*/
|
2011-12-03 12:50:37 +00:00
|
|
|
QList<QPair<WordsWithCharacters, QRect>> makeAndSortLines(const WordsWithCharacters &wordsTmp, int pageWidth, int pageHeight)
|
2011-08-21 04:44:42 +00:00
|
|
|
{
|
2011-07-03 08:56:27 +00:00
|
|
|
/**
|
2011-08-18 01:03:46 +00:00
|
|
|
* We cannot assume that the generator will give us texts in the right order.
|
|
|
|
* We can only assume that we will get texts in the page and their bounding
|
|
|
|
* rectangle. The texts can be character, word, half-word anything.
|
|
|
|
* So, we need to:
|
|
|
|
**
|
|
|
|
* 1. Sort rectangles/boxes containing texts by y0(top)
|
|
|
|
* 2. Create textline where there is y overlap between TinyTextEntity 's
|
|
|
|
* 3. Within each line sort the TinyTextEntity 's by x0(left)
|
|
|
|
*/
|
2020-07-10 22:15:05 +00:00
|
|
|
|
2011-12-03 12:50:37 +00:00
|
|
|
QList<QPair<WordsWithCharacters, QRect>> lines;
|
2011-07-01 10:41:55 +00:00
|
|
|
|
2011-08-19 01:01:11 +00:00
|
|
|
/*
|
|
|
|
Make a new copy of the TextList in the words, so that the wordsTmp and lines do
|
|
|
|
not contain same pointers for all the TinyTextEntity.
|
|
|
|
*/
|
2011-12-03 12:50:37 +00:00
|
|
|
QList<WordWithCharacters> words = wordsTmp;
|
2011-07-01 10:41:55 +00:00
|
|
|
|
2011-08-18 01:03:46 +00:00
|
|
|
// Step 1
|
2019-03-12 12:13:53 +00:00
|
|
|
std::sort(words.begin(), words.end(), compareTinyTextEntityY);
|
2011-07-01 10:41:55 +00:00
|
|
|
|
2011-08-18 01:03:46 +00:00
|
|
|
// Step 2
|
2011-12-03 12:50:37 +00:00
|
|
|
QList<WordWithCharacters>::Iterator it = words.begin(), itEnd = words.end();
|
2011-07-02 15:24:50 +00:00
|
|
|
|
2011-07-03 08:56:27 +00:00
|
|
|
// for every non-space texts(characters/words) in the textList
|
2011-08-21 04:44:42 +00:00
|
|
|
for (; it != itEnd; it++) {
|
2011-12-03 12:50:37 +00:00
|
|
|
const QRect elementArea = (*it).area().roundedGeometry(pageWidth, pageHeight);
|
2011-07-03 08:56:27 +00:00
|
|
|
bool found = false;
|
|
|
|
|
2020-01-02 23:16:36 +00:00
|
|
|
for (QPair<WordsWithCharacters, QRect> &linesI : lines) {
|
2011-08-18 01:03:46 +00:00
|
|
|
/* the line area which will be expanded
|
|
|
|
line_rects is only necessary to preserve the topmin and bottommax of all
|
|
|
|
the texts in the line, left and right is not necessary at all
|
|
|
|
*/
|
2020-01-02 23:16:36 +00:00
|
|
|
QRect &lineArea = linesI.second;
|
2011-08-22 18:19:23 +00:00
|
|
|
const int text_y1 = elementArea.top(), text_y2 = elementArea.top() + elementArea.height(), text_x1 = elementArea.left(), text_x2 = elementArea.left() + elementArea.width();
|
|
|
|
const int line_y1 = lineArea.top(), line_y2 = lineArea.top() + lineArea.height(), line_x1 = lineArea.left(), line_x2 = lineArea.left() + lineArea.width();
|
2011-07-03 08:56:27 +00:00
|
|
|
|
2011-08-18 01:03:46 +00:00
|
|
|
/*
|
|
|
|
if the new text and the line has y overlapping parts of more than 70%,
|
|
|
|
the text will be added to this line
|
|
|
|
*/
|
2011-08-21 04:44:42 +00:00
|
|
|
if (doesConsumeY(elementArea, lineArea, 70)) {
|
2020-01-02 23:16:36 +00:00
|
|
|
WordsWithCharacters &line = linesI.first;
|
2011-08-23 15:15:12 +00:00
|
|
|
line.append(*it);
|
2011-08-12 02:25:21 +00:00
|
|
|
|
2011-08-23 14:21:10 +00:00
|
|
|
const int newLeft = line_x1 < text_x1 ? line_x1 : text_x1;
|
|
|
|
const int newRight = line_x2 > text_x2 ? line_x2 : text_x2;
|
|
|
|
const int newTop = line_y1 < text_y1 ? line_y1 : text_y1;
|
|
|
|
const int newBottom = text_y2 > line_y2 ? text_y2 : line_y2;
|
2011-08-12 02:25:21 +00:00
|
|
|
|
2011-08-23 15:15:12 +00:00
|
|
|
lineArea = QRect(newLeft, newTop, newRight - newLeft, newBottom - newTop);
|
2011-08-12 02:25:21 +00:00
|
|
|
found = true;
|
|
|
|
}
|
2011-07-03 08:56:27 +00:00
|
|
|
|
2011-08-12 02:25:21 +00:00
|
|
|
if (found) {
|
|
|
|
break;
|
2022-03-08 10:10:43 +00:00
|
|
|
}
|
2011-07-01 10:41:55 +00:00
|
|
|
}
|
2011-07-03 08:56:27 +00:00
|
|
|
|
2011-08-18 01:03:46 +00:00
|
|
|
/* when we have found a new line create a new TextList containing
|
|
|
|
only one element and append it to the lines
|
|
|
|
*/
|
2011-08-21 04:44:42 +00:00
|
|
|
if (!found) {
|
2011-12-03 12:50:37 +00:00
|
|
|
WordsWithCharacters tmp;
|
2011-07-03 08:56:27 +00:00
|
|
|
tmp.append((*it));
|
2011-12-03 12:50:37 +00:00
|
|
|
lines.append(QPair<WordsWithCharacters, QRect>(tmp, elementArea));
|
2011-07-01 10:41:55 +00:00
|
|
|
}
|
2011-07-03 08:56:27 +00:00
|
|
|
}
|
2011-07-01 10:41:55 +00:00
|
|
|
|
2011-08-18 01:03:46 +00:00
|
|
|
// Step 3
|
2020-01-02 23:16:36 +00:00
|
|
|
for (QPair<WordsWithCharacters, QRect> &line : lines) {
|
|
|
|
WordsWithCharacters &list = line.first;
|
2019-03-12 12:13:53 +00:00
|
|
|
std::sort(list.begin(), list.end(), compareTinyTextEntityX);
|
2011-07-01 10:41:55 +00:00
|
|
|
}
|
2020-07-10 22:15:05 +00:00
|
|
|
|
2011-11-30 23:22:49 +00:00
|
|
|
return lines;
|
2011-07-08 18:12:39 +00:00
|
|
|
}
|
|
|
|
|
2011-08-19 01:01:11 +00:00
|
|
|
/**
|
|
|
|
* Calculate Statistical information from the lines we made previously
|
|
|
|
*/
|
2011-12-03 12:50:37 +00:00
|
|
|
static void calculateStatisticalInformation(const QList<WordWithCharacters> &words, int pageWidth, int pageHeight, int *word_spacing, int *line_spacing, int *col_spacing)
|
2011-08-21 04:44:42 +00:00
|
|
|
{
|
2011-08-19 01:01:11 +00:00
|
|
|
/**
|
|
|
|
* For the region, defined by line_rects and lines
|
|
|
|
* 1. Make line statistical analysis to find the line spacing
|
|
|
|
* 2. Make character statistical analysis to differentiate between
|
|
|
|
* word spacing and column spacing.
|
|
|
|
*/
|
2020-07-10 22:15:05 +00:00
|
|
|
|
2011-11-30 22:50:01 +00:00
|
|
|
/**
|
|
|
|
* Step 0
|
|
|
|
*/
|
2011-12-03 12:50:37 +00:00
|
|
|
const QList<QPair<WordsWithCharacters, QRect>> sortedLines = makeAndSortLines(words, pageWidth, pageHeight);
|
2011-08-19 01:01:11 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Step 1
|
|
|
|
*/
|
|
|
|
QMap<int, int> line_space_stat;
|
2011-11-30 23:22:49 +00:00
|
|
|
for (int i = 0; i < sortedLines.length(); i++) {
|
|
|
|
const QRect rectUpper = sortedLines.at(i).second;
|
2011-08-19 01:01:11 +00:00
|
|
|
|
2011-11-30 23:22:49 +00:00
|
|
|
if (i + 1 == sortedLines.length()) {
|
|
|
|
break;
|
2022-03-08 10:10:43 +00:00
|
|
|
}
|
2011-11-30 23:22:49 +00:00
|
|
|
const QRect rectLower = sortedLines.at(i + 1).second;
|
2011-08-19 01:01:11 +00:00
|
|
|
|
|
|
|
int linespace = rectLower.top() - (rectUpper.top() + rectUpper.height());
|
|
|
|
if (linespace < 0) {
|
|
|
|
linespace = -linespace;
|
2022-03-08 10:10:43 +00:00
|
|
|
}
|
2011-08-19 01:01:11 +00:00
|
|
|
|
|
|
|
if (line_space_stat.contains(linespace)) {
|
|
|
|
line_space_stat[linespace]++;
|
|
|
|
} else {
|
|
|
|
line_space_stat[linespace] = 1;
|
2022-03-08 10:10:43 +00:00
|
|
|
}
|
2011-08-19 01:01:11 +00:00
|
|
|
}
|
|
|
|
|
2011-08-23 15:15:12 +00:00
|
|
|
*line_spacing = 0;
|
2011-08-19 01:01:11 +00:00
|
|
|
int weighted_count = 0;
|
|
|
|
QMapIterator<int, int> iterate_linespace(line_space_stat);
|
|
|
|
|
2011-08-21 04:44:42 +00:00
|
|
|
while (iterate_linespace.hasNext()) {
|
2011-08-19 01:01:11 +00:00
|
|
|
iterate_linespace.next();
|
2011-08-23 15:15:12 +00:00
|
|
|
*line_spacing += iterate_linespace.value() * iterate_linespace.key();
|
2011-08-19 01:01:11 +00:00
|
|
|
weighted_count += iterate_linespace.value();
|
|
|
|
}
|
2011-08-23 15:15:12 +00:00
|
|
|
if (*line_spacing != 0) {
|
|
|
|
*line_spacing = (int)((double)*line_spacing / (double)weighted_count + 0.5);
|
2022-03-08 10:10:43 +00:00
|
|
|
}
|
2011-08-19 01:01:11 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Step 2
|
|
|
|
*/
|
2011-08-21 04:44:42 +00:00
|
|
|
// We would like to use QMap instead of QHash as it will keep the keys sorted
|
|
|
|
QMap<int, int> hor_space_stat;
|
|
|
|
QMap<int, int> col_space_stat;
|
|
|
|
QList<QList<QRect>> space_rects;
|
2020-02-19 16:48:17 +00:00
|
|
|
QVector<QRect> max_hor_space_rects;
|
2011-08-19 01:01:11 +00:00
|
|
|
|
2011-08-21 04:44:42 +00:00
|
|
|
// Space in every line
|
2020-01-02 23:16:36 +00:00
|
|
|
for (const QPair<WordsWithCharacters, QRect> &sortedLine : sortedLines) {
|
|
|
|
const WordsWithCharacters list = sortedLine.first;
|
2011-08-19 01:01:11 +00:00
|
|
|
QList<QRect> line_space_rects;
|
|
|
|
int maxSpace = 0, minSpace = pageWidth;
|
|
|
|
|
|
|
|
// for every TinyTextEntity element in the line
|
2011-12-03 12:50:37 +00:00
|
|
|
WordsWithCharacters::ConstIterator it = list.begin(), itEnd = list.end();
|
2011-08-19 01:01:11 +00:00
|
|
|
QRect max_area1, max_area2;
|
|
|
|
// for every line
|
2011-08-21 04:44:42 +00:00
|
|
|
for (; it != itEnd; it++) {
|
2011-12-03 12:50:37 +00:00
|
|
|
const QRect area1 = (*it).area().roundedGeometry(pageWidth, pageHeight);
|
2011-08-19 01:01:11 +00:00
|
|
|
if (it + 1 == itEnd) {
|
|
|
|
break;
|
2022-03-08 10:10:43 +00:00
|
|
|
}
|
2011-08-19 01:01:11 +00:00
|
|
|
|
2011-12-03 12:50:37 +00:00
|
|
|
const QRect area2 = (*(it + 1)).area().roundedGeometry(pageWidth, pageHeight);
|
2011-08-19 01:01:11 +00:00
|
|
|
int space = area2.left() - area1.right();
|
|
|
|
|
2011-08-21 04:44:42 +00:00
|
|
|
if (space > maxSpace) {
|
2011-08-19 01:01:11 +00:00
|
|
|
max_area1 = area1;
|
|
|
|
max_area2 = area2;
|
|
|
|
maxSpace = space;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (space < minSpace && space != 0) {
|
|
|
|
minSpace = space;
|
2022-03-08 10:10:43 +00:00
|
|
|
}
|
2011-08-19 01:01:11 +00:00
|
|
|
|
|
|
|
// if we found a real space, whose length is not zero and also less than the pageWidth
|
2011-08-21 04:44:42 +00:00
|
|
|
if (space != 0 && space != pageWidth) {
|
2011-08-19 01:01:11 +00:00
|
|
|
// increase the count of the space amount
|
2014-02-25 22:57:06 +00:00
|
|
|
if (hor_space_stat.contains(space)) {
|
|
|
|
hor_space_stat[space]++;
|
2011-08-19 01:01:11 +00:00
|
|
|
} else {
|
|
|
|
hor_space_stat[space] = 1;
|
2022-03-08 10:10:43 +00:00
|
|
|
}
|
2011-08-19 01:01:11 +00:00
|
|
|
|
|
|
|
int left, right, top, bottom;
|
|
|
|
|
|
|
|
left = area1.right();
|
|
|
|
right = area2.left();
|
|
|
|
|
|
|
|
top = area2.top() < area1.top() ? area2.top() : area1.top();
|
|
|
|
bottom = area2.bottom() > area1.bottom() ? area2.bottom() : area1.bottom();
|
|
|
|
|
|
|
|
QRect rect(left, top, right - left, bottom - top);
|
|
|
|
line_space_rects.append(rect);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
space_rects.append(line_space_rects);
|
|
|
|
|
2011-08-21 04:44:42 +00:00
|
|
|
if (hor_space_stat.contains(maxSpace)) {
|
2011-08-19 01:01:11 +00:00
|
|
|
if (hor_space_stat[maxSpace] != 1) {
|
2014-02-25 22:57:06 +00:00
|
|
|
hor_space_stat[maxSpace]--;
|
2011-08-19 01:01:11 +00:00
|
|
|
} else {
|
|
|
|
hor_space_stat.remove(maxSpace);
|
2022-03-08 10:10:43 +00:00
|
|
|
}
|
2011-08-19 01:01:11 +00:00
|
|
|
}
|
|
|
|
|
2011-08-21 04:44:42 +00:00
|
|
|
if (maxSpace != 0) {
|
2011-08-19 01:01:11 +00:00
|
|
|
if (col_space_stat.contains(maxSpace)) {
|
2014-02-25 22:57:06 +00:00
|
|
|
col_space_stat[maxSpace]++;
|
2011-08-19 01:01:11 +00:00
|
|
|
} else {
|
|
|
|
col_space_stat[maxSpace] = 1;
|
2022-03-08 10:10:43 +00:00
|
|
|
}
|
2011-08-19 01:01:11 +00:00
|
|
|
|
|
|
|
// store the max rect of each line
|
2011-08-22 18:19:23 +00:00
|
|
|
const int left = max_area1.right();
|
|
|
|
const int right = max_area2.left();
|
|
|
|
const int top = (max_area1.top() > max_area2.top()) ? max_area2.top() : max_area1.top();
|
|
|
|
const int bottom = (max_area1.bottom() < max_area2.bottom()) ? max_area2.bottom() : max_area1.bottom();
|
|
|
|
|
|
|
|
const QRect rect(left, top, right - left, bottom - top);
|
2011-08-19 01:01:11 +00:00
|
|
|
max_hor_space_rects.append(rect);
|
|
|
|
} else {
|
|
|
|
max_hor_space_rects.append(QRect(0, 0, 0, 0));
|
2022-03-08 10:10:43 +00:00
|
|
|
}
|
2011-08-19 01:01:11 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// All the between word space counts are in hor_space_stat
|
2011-08-23 15:15:12 +00:00
|
|
|
*word_spacing = 0;
|
2011-08-19 01:01:11 +00:00
|
|
|
weighted_count = 0;
|
|
|
|
QMapIterator<int, int> iterate(hor_space_stat);
|
|
|
|
|
2011-08-21 04:44:42 +00:00
|
|
|
while (iterate.hasNext()) {
|
2011-08-19 01:01:11 +00:00
|
|
|
iterate.next();
|
|
|
|
|
2011-08-21 04:44:42 +00:00
|
|
|
if (iterate.key() > 0) {
|
2011-08-23 15:15:12 +00:00
|
|
|
*word_spacing += iterate.value() * iterate.key();
|
2011-08-19 01:01:11 +00:00
|
|
|
weighted_count += iterate.value();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (weighted_count) {
|
2011-08-23 15:15:12 +00:00
|
|
|
*word_spacing = (int)((double)*word_spacing / (double)weighted_count + 0.5);
|
2022-03-08 10:10:43 +00:00
|
|
|
}
|
2011-08-19 01:01:11 +00:00
|
|
|
|
2011-08-23 15:15:12 +00:00
|
|
|
*col_spacing = 0;
|
2011-08-19 01:01:11 +00:00
|
|
|
QMapIterator<int, int> iterate_col(col_space_stat);
|
|
|
|
|
2011-08-21 04:44:42 +00:00
|
|
|
while (iterate_col.hasNext()) {
|
2011-08-19 01:01:11 +00:00
|
|
|
iterate_col.next();
|
2011-08-23 15:15:12 +00:00
|
|
|
if (iterate_col.value() > *col_spacing) {
|
|
|
|
*col_spacing = iterate_col.value();
|
2022-03-08 10:10:43 +00:00
|
|
|
}
|
2011-08-19 01:01:11 +00:00
|
|
|
}
|
2011-08-23 15:15:12 +00:00
|
|
|
*col_spacing = col_space_stat.key(*col_spacing);
|
2011-08-24 10:35:47 +00:00
|
|
|
|
|
|
|
// if there is just one line in a region, there is no point in dividing it
|
2011-11-30 23:22:49 +00:00
|
|
|
if (sortedLines.length() == 1) {
|
2011-08-24 10:42:50 +00:00
|
|
|
*word_spacing = *col_spacing;
|
2022-03-08 10:10:43 +00:00
|
|
|
}
|
2011-08-19 01:01:11 +00:00
|
|
|
}
|
|
|
|
|
2011-08-18 01:03:46 +00:00
|
|
|
/**
|
|
|
|
* Implements the XY Cut algorithm for textpage segmentation
|
2011-12-03 12:50:37 +00:00
|
|
|
* The resulting RegionTextList will contain RegionText whose WordsWithCharacters::word and
|
|
|
|
* WordsWithCharacters::characters are reused from wordsWithCharacters (i.e. no new nor delete happens in this function)
|
2011-08-18 01:03:46 +00:00
|
|
|
*/
|
2021-08-26 15:46:29 +00:00
|
|
|
static RegionTextList XYCutForBoundingBoxes(const QList<WordWithCharacters> &wordsWithCharacters, int pageWidth, int pageHeight)
|
2011-08-21 04:44:42 +00:00
|
|
|
{
|
2011-07-12 14:24:17 +00:00
|
|
|
RegionTextList tree;
|
2021-08-26 15:46:29 +00:00
|
|
|
QRect contentRect(0, 0, pageWidth, pageHeight);
|
2011-12-03 12:50:37 +00:00
|
|
|
const RegionText root(wordsWithCharacters, contentRect);
|
2011-07-08 18:12:39 +00:00
|
|
|
|
|
|
|
// start the tree with the root, it is our only region at the start
|
|
|
|
tree.push_back(root);
|
|
|
|
|
2011-11-30 22:43:53 +00:00
|
|
|
int i = 0;
|
2011-07-08 18:12:39 +00:00
|
|
|
|
|
|
|
// while traversing the tree has not been ended
|
2011-08-21 04:44:42 +00:00
|
|
|
while (i < tree.length()) {
|
2011-08-22 18:19:23 +00:00
|
|
|
const RegionText node = tree.at(i);
|
2011-07-08 18:12:39 +00:00
|
|
|
QRect regionRect = node.area();
|
|
|
|
|
2011-08-18 01:03:46 +00:00
|
|
|
/**
|
|
|
|
* 1. calculation of projection profiles
|
|
|
|
*/
|
2011-07-08 18:12:39 +00:00
|
|
|
// allocate the size of proj profiles and initialize with 0
|
2011-11-30 22:21:51 +00:00
|
|
|
int size_proj_y = node.area().height();
|
|
|
|
int size_proj_x = node.area().width();
|
2011-08-23 13:22:47 +00:00
|
|
|
// dynamic memory allocation
|
2011-08-23 14:02:08 +00:00
|
|
|
QVarLengthArray<int> proj_on_xaxis(size_proj_x);
|
|
|
|
QVarLengthArray<int> proj_on_yaxis(size_proj_y);
|
2011-07-08 18:12:39 +00:00
|
|
|
|
2011-11-30 22:43:53 +00:00
|
|
|
for (int j = 0; j < size_proj_y; ++j) {
|
|
|
|
proj_on_yaxis[j] = 0;
|
2022-03-08 10:10:43 +00:00
|
|
|
}
|
2011-11-30 22:43:53 +00:00
|
|
|
for (int j = 0; j < size_proj_x; ++j) {
|
|
|
|
proj_on_xaxis[j] = 0;
|
2022-03-08 10:10:43 +00:00
|
|
|
}
|
2011-07-08 18:12:39 +00:00
|
|
|
|
2011-12-03 12:50:37 +00:00
|
|
|
const QList<WordWithCharacters> list = node.text();
|
2011-07-15 11:29:59 +00:00
|
|
|
|
|
|
|
// Calculate tcx and tcy locally for each new region
|
2011-11-30 22:36:10 +00:00
|
|
|
int word_spacing, line_spacing, column_spacing;
|
2011-12-03 12:50:37 +00:00
|
|
|
calculateStatisticalInformation(list, pageWidth, pageHeight, &word_spacing, &line_spacing, &column_spacing);
|
2011-08-23 15:15:12 +00:00
|
|
|
|
2011-11-30 22:36:10 +00:00
|
|
|
const int tcx = word_spacing * 2;
|
|
|
|
const int tcy = line_spacing * 2;
|
2011-07-15 11:29:59 +00:00
|
|
|
|
2011-07-08 18:12:39 +00:00
|
|
|
int maxX = 0, maxY = 0;
|
2011-11-30 22:43:53 +00:00
|
|
|
int avgX = 0;
|
2011-07-15 11:29:59 +00:00
|
|
|
int count;
|
|
|
|
|
2011-07-08 18:12:39 +00:00
|
|
|
// for every text in the region
|
2020-01-02 23:16:36 +00:00
|
|
|
for (const WordWithCharacters &wwc : list) {
|
|
|
|
TinyTextEntity *ent = wwc.word;
|
2011-11-30 22:51:05 +00:00
|
|
|
const QRect entRect = ent->area.geometry(pageWidth, pageHeight);
|
2011-07-08 18:12:39 +00:00
|
|
|
|
2011-07-15 11:29:59 +00:00
|
|
|
// calculate vertical projection profile proj_on_xaxis1
|
2011-11-30 22:43:53 +00:00
|
|
|
for (int k = entRect.left(); k <= entRect.left() + entRect.width(); ++k) {
|
2011-08-23 13:22:47 +00:00
|
|
|
if ((k - regionRect.left()) < size_proj_x && (k - regionRect.left()) >= 0) {
|
|
|
|
proj_on_xaxis[k - regionRect.left()] += entRect.height();
|
2022-03-08 10:10:43 +00:00
|
|
|
}
|
2011-07-08 18:12:39 +00:00
|
|
|
}
|
|
|
|
|
2011-07-11 05:37:34 +00:00
|
|
|
// calculate horizontal projection profile in the same way
|
2011-11-30 22:43:53 +00:00
|
|
|
for (int k = entRect.top(); k <= entRect.top() + entRect.height(); ++k) {
|
2011-08-23 13:22:47 +00:00
|
|
|
if ((k - regionRect.top()) < size_proj_y && (k - regionRect.top()) >= 0) {
|
|
|
|
proj_on_yaxis[k - regionRect.top()] += entRect.width();
|
2022-03-08 10:10:43 +00:00
|
|
|
}
|
2011-07-08 18:12:39 +00:00
|
|
|
}
|
2011-07-03 08:56:27 +00:00
|
|
|
}
|
|
|
|
|
2011-11-30 22:43:53 +00:00
|
|
|
for (int j = 0; j < size_proj_y; ++j) {
|
2011-08-21 04:44:42 +00:00
|
|
|
if (proj_on_yaxis[j] > maxY) {
|
|
|
|
maxY = proj_on_yaxis[j];
|
2022-03-08 10:10:43 +00:00
|
|
|
}
|
2011-07-10 06:00:57 +00:00
|
|
|
}
|
|
|
|
|
2011-07-15 11:29:59 +00:00
|
|
|
avgX = count = 0;
|
2011-11-30 22:43:53 +00:00
|
|
|
for (int j = 0; j < size_proj_x; ++j) {
|
2011-07-10 06:00:57 +00:00
|
|
|
if (proj_on_xaxis[j] > maxX) {
|
|
|
|
maxX = proj_on_xaxis[j];
|
2022-03-08 10:10:43 +00:00
|
|
|
}
|
2011-08-21 04:44:42 +00:00
|
|
|
if (proj_on_xaxis[j]) {
|
2011-07-15 11:29:59 +00:00
|
|
|
count++;
|
|
|
|
avgX += proj_on_xaxis[j];
|
|
|
|
}
|
2011-07-08 18:12:39 +00:00
|
|
|
}
|
2011-08-21 04:44:42 +00:00
|
|
|
if (count) {
|
|
|
|
avgX /= count;
|
2022-03-08 10:10:43 +00:00
|
|
|
}
|
2011-07-10 06:00:57 +00:00
|
|
|
|
2011-08-18 01:03:46 +00:00
|
|
|
/**
|
|
|
|
* 2. Cleanup Boundary White Spaces and removal of noise
|
|
|
|
*/
|
2011-07-10 06:00:57 +00:00
|
|
|
int xbegin = 0, xend = size_proj_x - 1;
|
|
|
|
int ybegin = 0, yend = size_proj_y - 1;
|
2011-08-21 04:44:42 +00:00
|
|
|
while (xbegin < size_proj_x && proj_on_xaxis[xbegin] <= 0) {
|
2011-07-10 06:00:57 +00:00
|
|
|
xbegin++;
|
2022-03-08 10:10:43 +00:00
|
|
|
}
|
2011-08-21 04:44:42 +00:00
|
|
|
while (xend >= 0 && proj_on_xaxis[xend] <= 0) {
|
2011-07-10 06:00:57 +00:00
|
|
|
xend--;
|
2022-03-08 10:10:43 +00:00
|
|
|
}
|
2011-08-21 04:44:42 +00:00
|
|
|
while (ybegin < size_proj_y && proj_on_yaxis[ybegin] <= 0) {
|
2011-07-10 06:00:57 +00:00
|
|
|
ybegin++;
|
2022-03-08 10:10:43 +00:00
|
|
|
}
|
2011-08-21 04:44:42 +00:00
|
|
|
while (yend >= 0 && proj_on_yaxis[yend] <= 0) {
|
2011-07-10 06:00:57 +00:00
|
|
|
yend--;
|
2022-03-08 10:10:43 +00:00
|
|
|
}
|
2011-07-10 06:00:57 +00:00
|
|
|
|
|
|
|
// update the regionRect
|
|
|
|
int old_left = regionRect.left(), old_top = regionRect.top();
|
|
|
|
regionRect.setLeft(old_left + xbegin);
|
|
|
|
regionRect.setRight(old_left + xend);
|
|
|
|
regionRect.setTop(old_top + ybegin);
|
|
|
|
regionRect.setBottom(old_top + yend);
|
|
|
|
|
2011-07-15 11:29:59 +00:00
|
|
|
int tnx = (int)((double)avgX * 10.0 / 100.0 + 0.5), tny = 0;
|
2011-11-30 22:43:53 +00:00
|
|
|
for (int j = 0; j < size_proj_x; ++j) {
|
2011-07-11 05:37:34 +00:00
|
|
|
proj_on_xaxis[j] -= tnx;
|
2022-03-08 10:10:43 +00:00
|
|
|
}
|
2011-11-30 22:43:53 +00:00
|
|
|
for (int j = 0; j < size_proj_y; ++j) {
|
2011-07-11 05:37:34 +00:00
|
|
|
proj_on_yaxis[j] -= tny;
|
2022-03-08 10:10:43 +00:00
|
|
|
}
|
2011-07-10 06:00:57 +00:00
|
|
|
|
2011-08-21 04:44:42 +00:00
|
|
|
/**
|
|
|
|
* 3. Find the Widest gap
|
|
|
|
*/
|
2011-07-10 06:00:57 +00:00
|
|
|
int gap_hor = -1, pos_hor = -1;
|
|
|
|
int begin = -1, end = -1;
|
|
|
|
|
|
|
|
// find all hor_gaps and find the maximum between them
|
2011-11-30 22:43:53 +00:00
|
|
|
for (int j = 1; j < size_proj_y; ++j) {
|
2011-07-10 06:00:57 +00:00
|
|
|
// transition from white to black
|
|
|
|
if (begin >= 0 && proj_on_yaxis[j - 1] <= 0 && proj_on_yaxis[j] > 0) {
|
|
|
|
end = j;
|
2022-03-08 10:10:43 +00:00
|
|
|
}
|
2011-07-10 06:00:57 +00:00
|
|
|
|
|
|
|
// transition from black to white
|
|
|
|
if (proj_on_yaxis[j - 1] > 0 && proj_on_yaxis[j] <= 0) {
|
|
|
|
begin = j;
|
2022-03-08 10:10:43 +00:00
|
|
|
}
|
2011-07-10 06:00:57 +00:00
|
|
|
|
2011-08-21 04:44:42 +00:00
|
|
|
if (begin > 0 && end > 0 && end - begin > gap_hor) {
|
2011-07-10 06:00:57 +00:00
|
|
|
gap_hor = end - begin;
|
|
|
|
pos_hor = (end + begin) / 2;
|
|
|
|
begin = -1;
|
|
|
|
end = -1;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
begin = -1, end = -1;
|
|
|
|
int gap_ver = -1, pos_ver = -1;
|
|
|
|
|
|
|
|
// find all the ver_gaps and find the maximum between them
|
2011-11-30 22:43:53 +00:00
|
|
|
for (int j = 1; j < size_proj_x; ++j) {
|
2011-07-10 06:00:57 +00:00
|
|
|
// transition from white to black
|
|
|
|
if (begin >= 0 && proj_on_xaxis[j - 1] <= 0 && proj_on_xaxis[j] > 0) {
|
|
|
|
end = j;
|
|
|
|
}
|
|
|
|
|
|
|
|
// transition from black to white
|
|
|
|
if (proj_on_xaxis[j - 1] > 0 && proj_on_xaxis[j] <= 0) {
|
|
|
|
begin = j;
|
2022-03-08 10:10:43 +00:00
|
|
|
}
|
2011-07-10 06:00:57 +00:00
|
|
|
|
2011-08-21 04:44:42 +00:00
|
|
|
if (begin > 0 && end > 0 && end - begin > gap_ver) {
|
2011-07-10 06:00:57 +00:00
|
|
|
gap_ver = end - begin;
|
|
|
|
pos_ver = (end + begin) / 2;
|
|
|
|
begin = -1;
|
|
|
|
end = -1;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
int cut_pos_x = pos_ver, cut_pos_y = pos_hor;
|
|
|
|
int gap_x = gap_ver, gap_y = gap_hor;
|
|
|
|
|
2011-08-18 01:03:46 +00:00
|
|
|
/**
|
|
|
|
* 4. Cut the region and make nodes (left,right) or (up,down)
|
|
|
|
*/
|
2011-07-10 06:00:57 +00:00
|
|
|
bool cut_hor = false, cut_ver = false;
|
|
|
|
|
|
|
|
// For horizontal cut
|
2011-08-22 18:19:23 +00:00
|
|
|
const int topHeight = cut_pos_y - (regionRect.top() - old_top);
|
|
|
|
const QRect topRect(regionRect.left(), regionRect.top(), regionRect.width(), topHeight);
|
|
|
|
const QRect bottomRect(regionRect.left(), regionRect.top() + topHeight, regionRect.width(), regionRect.height() - topHeight);
|
2011-07-10 06:00:57 +00:00
|
|
|
|
|
|
|
// For vertical Cut
|
2011-08-22 18:19:23 +00:00
|
|
|
const int leftWidth = cut_pos_x - (regionRect.left() - old_left);
|
|
|
|
const QRect leftRect(regionRect.left(), regionRect.top(), leftWidth, regionRect.height());
|
|
|
|
const QRect rightRect(regionRect.left() + leftWidth, regionRect.top(), regionRect.width() - leftWidth, regionRect.height());
|
2020-07-10 22:15:05 +00:00
|
|
|
|
2011-08-21 04:44:42 +00:00
|
|
|
if (gap_y >= gap_x && gap_y >= tcy) {
|
2011-07-10 06:00:57 +00:00
|
|
|
cut_hor = true;
|
2011-08-21 04:44:42 +00:00
|
|
|
} else if (gap_y >= gap_x && gap_y <= tcy && gap_x >= tcx) {
|
2011-07-10 06:00:57 +00:00
|
|
|
cut_ver = true;
|
2011-08-21 04:44:42 +00:00
|
|
|
} else if (gap_x >= gap_y && gap_x >= tcx) {
|
2011-07-10 06:00:57 +00:00
|
|
|
cut_ver = true;
|
2011-08-21 04:44:42 +00:00
|
|
|
} else if (gap_x >= gap_y && gap_x <= tcx && gap_y >= tcy) {
|
2011-07-10 06:00:57 +00:00
|
|
|
cut_hor = true;
|
2022-03-08 10:10:43 +00:00
|
|
|
} else {
|
2011-08-18 01:03:46 +00:00
|
|
|
// no cut possible
|
2011-07-12 14:24:17 +00:00
|
|
|
// we can now update the node rectangle with the shrinked rectangle
|
|
|
|
RegionText tmpNode = tree.at(i);
|
|
|
|
tmpNode.setArea(regionRect);
|
|
|
|
tree.replace(i, tmpNode);
|
2011-07-10 06:00:57 +00:00
|
|
|
i++;
|
2011-07-11 05:37:34 +00:00
|
|
|
continue;
|
2011-07-10 06:00:57 +00:00
|
|
|
}
|
|
|
|
|
2011-12-03 12:50:37 +00:00
|
|
|
WordsWithCharacters list1, list2;
|
2011-07-10 14:54:38 +00:00
|
|
|
|
2011-08-21 04:44:42 +00:00
|
|
|
// horizontal cut, topRect and bottomRect
|
|
|
|
if (cut_hor) {
|
2020-01-02 23:16:36 +00:00
|
|
|
for (const WordWithCharacters &word : list) {
|
2011-12-03 12:50:37 +00:00
|
|
|
const QRect wordRect = word.area().geometry(pageWidth, pageHeight);
|
2011-07-10 06:00:57 +00:00
|
|
|
|
2011-12-03 12:50:37 +00:00
|
|
|
if (topRect.intersects(wordRect)) {
|
|
|
|
list1.append(word);
|
2011-08-21 04:44:42 +00:00
|
|
|
} else {
|
2011-12-03 12:50:37 +00:00
|
|
|
list2.append(word);
|
2022-03-08 10:10:43 +00:00
|
|
|
}
|
2011-07-10 06:00:57 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
RegionText node1(list1, topRect);
|
|
|
|
RegionText node2(list2, bottomRect);
|
|
|
|
|
|
|
|
tree.replace(i, node1);
|
|
|
|
tree.insert(i + 1, node2);
|
|
|
|
}
|
|
|
|
|
|
|
|
// vertical cut, leftRect and rightRect
|
2011-08-21 04:44:42 +00:00
|
|
|
else if (cut_ver) {
|
2020-01-02 23:16:36 +00:00
|
|
|
for (const WordWithCharacters &word : list) {
|
2011-12-03 12:50:37 +00:00
|
|
|
const QRect wordRect = word.area().geometry(pageWidth, pageHeight);
|
2011-07-10 06:00:57 +00:00
|
|
|
|
2011-12-03 12:50:37 +00:00
|
|
|
if (leftRect.intersects(wordRect)) {
|
|
|
|
list1.append(word);
|
|
|
|
} else {
|
|
|
|
list2.append(word);
|
2022-03-08 10:10:43 +00:00
|
|
|
}
|
2011-07-10 06:00:57 +00:00
|
|
|
}
|
2011-08-21 04:44:42 +00:00
|
|
|
|
2011-07-10 06:00:57 +00:00
|
|
|
RegionText node1(list1, leftRect);
|
|
|
|
RegionText node2(list2, rightRect);
|
|
|
|
|
|
|
|
tree.replace(i, node1);
|
|
|
|
tree.insert(i + 1, node2);
|
|
|
|
}
|
2011-07-11 05:37:34 +00:00
|
|
|
}
|
|
|
|
|
2011-08-23 15:15:12 +00:00
|
|
|
return tree;
|
2011-07-01 10:41:55 +00:00
|
|
|
}
|
|
|
|
|
2011-08-21 04:44:42 +00:00
|
|
|
/**
|
2011-12-03 12:50:37 +00:00
|
|
|
* Add spaces in between words in a line. It reuses the pointers passed in tree and might add new ones. You will need to take care of deleting them if needed
|
2011-08-21 04:44:42 +00:00
|
|
|
*/
|
2011-12-03 12:50:37 +00:00
|
|
|
WordsWithCharacters addNecessarySpace(RegionTextList tree, int pageWidth, int pageHeight)
|
2011-08-21 04:44:42 +00:00
|
|
|
{
|
2011-07-15 11:29:59 +00:00
|
|
|
/**
|
2011-08-21 04:44:42 +00:00
|
|
|
* 1. Call makeAndSortLines before adding spaces in between words in a line
|
|
|
|
* 2. Now add spaces between every two words in a line
|
2011-12-03 12:50:37 +00:00
|
|
|
* 3. Finally, extract all the space separated texts from each region and return it
|
2011-08-18 01:03:46 +00:00
|
|
|
*/
|
2011-07-15 11:29:59 +00:00
|
|
|
|
2011-08-21 04:44:42 +00:00
|
|
|
// Only change the texts under RegionTexts, not the area
|
2020-01-02 23:16:36 +00:00
|
|
|
for (RegionText &tmpRegion : tree) {
|
2011-08-21 04:44:42 +00:00
|
|
|
// Step 01
|
2011-12-03 12:50:37 +00:00
|
|
|
QList<QPair<WordsWithCharacters, QRect>> sortedLines = makeAndSortLines(tmpRegion.text(), pageWidth, pageHeight);
|
2011-07-15 11:29:59 +00:00
|
|
|
|
2011-08-21 04:44:42 +00:00
|
|
|
// Step 02
|
2020-01-02 23:16:36 +00:00
|
|
|
for (QPair<WordsWithCharacters, QRect> &sortedLine : sortedLines) {
|
|
|
|
WordsWithCharacters &list = sortedLine.first;
|
2011-08-23 13:58:46 +00:00
|
|
|
for (int k = 0; k < list.length(); k++) {
|
2011-12-03 12:50:37 +00:00
|
|
|
const QRect area1 = list.at(k).area().roundedGeometry(pageWidth, pageHeight);
|
2011-08-21 04:44:42 +00:00
|
|
|
if (k + 1 >= list.length()) {
|
|
|
|
break;
|
2022-03-08 10:10:43 +00:00
|
|
|
}
|
2011-07-15 11:29:59 +00:00
|
|
|
|
2011-12-03 12:50:37 +00:00
|
|
|
const QRect area2 = list.at(k + 1).area().roundedGeometry(pageWidth, pageHeight);
|
2011-08-22 18:19:23 +00:00
|
|
|
const int space = area2.left() - area1.right();
|
2011-07-15 11:29:59 +00:00
|
|
|
|
2011-08-21 04:44:42 +00:00
|
|
|
if (space != 0) {
|
|
|
|
// Make a TinyTextEntity of string space and push it between it and it+1
|
2011-08-22 18:19:23 +00:00
|
|
|
const int left = area1.right();
|
|
|
|
const int right = area2.left();
|
|
|
|
const int top = area2.top() < area1.top() ? area2.top() : area1.top();
|
|
|
|
const int bottom = area2.bottom() > area1.bottom() ? area2.bottom() : area1.bottom();
|
|
|
|
|
2015-10-29 12:37:11 +00:00
|
|
|
const QString spaceStr(QStringLiteral(" "));
|
2011-08-22 18:19:23 +00:00
|
|
|
const QRect rect(QPoint(left, top), QPoint(right, bottom));
|
|
|
|
const NormalizedRect entRect(rect, pageWidth, pageHeight);
|
2011-12-03 12:50:37 +00:00
|
|
|
TinyTextEntity *ent1 = new TinyTextEntity(spaceStr, entRect);
|
|
|
|
TinyTextEntity *ent2 = new TinyTextEntity(spaceStr, entRect);
|
|
|
|
WordWithCharacters word(ent1, QList<TinyTextEntity *>() << ent2);
|
2011-07-15 11:29:59 +00:00
|
|
|
|
2011-12-03 12:50:37 +00:00
|
|
|
list.insert(k + 1, word);
|
2011-07-15 11:29:59 +00:00
|
|
|
|
2011-08-21 04:44:42 +00:00
|
|
|
// Skip the space
|
|
|
|
k++;
|
2011-07-15 11:29:59 +00:00
|
|
|
}
|
|
|
|
}
|
2011-08-21 04:44:42 +00:00
|
|
|
}
|
2011-07-15 11:29:59 +00:00
|
|
|
|
2011-12-03 12:50:37 +00:00
|
|
|
WordsWithCharacters tmpList;
|
2020-01-02 23:16:36 +00:00
|
|
|
for (const QPair<WordsWithCharacters, QRect> &sortedLine : qAsConst(sortedLines)) {
|
|
|
|
tmpList += sortedLine.first;
|
2011-07-15 11:29:59 +00:00
|
|
|
}
|
2011-08-23 13:55:53 +00:00
|
|
|
tmpRegion.setText(tmpList);
|
2011-08-21 04:44:42 +00:00
|
|
|
}
|
2011-07-15 11:29:59 +00:00
|
|
|
|
2011-08-21 04:44:42 +00:00
|
|
|
// Step 03
|
2011-12-03 12:50:37 +00:00
|
|
|
WordsWithCharacters tmp;
|
2020-01-02 23:16:36 +00:00
|
|
|
for (const RegionText &tmpRegion : qAsConst(tree)) {
|
|
|
|
tmp += tmpRegion.text();
|
2011-08-21 04:44:42 +00:00
|
|
|
}
|
2011-12-03 12:50:37 +00:00
|
|
|
return tmp;
|
2011-07-13 13:27:04 +00:00
|
|
|
}
|
2011-07-01 10:41:55 +00:00
|
|
|
|
2011-08-18 01:03:46 +00:00
|
|
|
/**
|
2011-12-03 12:50:37 +00:00
|
|
|
* Correct the textOrder, all layout recognition works here
|
2011-08-18 01:03:46 +00:00
|
|
|
*/
|
2011-12-03 12:50:37 +00:00
|
|
|
void TextPagePrivate::correctTextOrder()
|
2011-08-21 04:44:42 +00:00
|
|
|
{
|
2019-07-21 08:49:24 +00:00
|
|
|
// m_page->width() and m_page->height() are in pixels at
|
|
|
|
// 100% zoom level, and thus depend on display DPI.
|
|
|
|
// To avoid Okular failing on lowDPI displays,
|
|
|
|
// we scale pageWidth and pageHeight so their sum equals 2000.
|
2017-12-05 22:07:10 +00:00
|
|
|
const double scalingFactor = 2000.0 / (m_page->width() + m_page->height());
|
|
|
|
const int pageWidth = (int)(scalingFactor * m_page->width());
|
|
|
|
const int pageHeight = (int)(scalingFactor * m_page->height());
|
2011-07-12 14:24:17 +00:00
|
|
|
|
2011-12-03 12:50:37 +00:00
|
|
|
TextList characters = m_words;
|
2011-07-15 11:29:59 +00:00
|
|
|
|
2011-08-17 09:32:40 +00:00
|
|
|
/**
|
|
|
|
* Remove spaces from the text
|
|
|
|
*/
|
2011-12-03 12:50:37 +00:00
|
|
|
removeSpace(&characters);
|
2011-07-13 11:16:03 +00:00
|
|
|
|
2011-08-17 09:32:40 +00:00
|
|
|
/**
|
|
|
|
* Construct words from characters
|
|
|
|
*/
|
2011-12-03 12:50:37 +00:00
|
|
|
const QList<WordWithCharacters> wordsWithCharacters = makeWordFromCharacters(characters, pageWidth, pageHeight);
|
2011-07-13 11:16:03 +00:00
|
|
|
|
2011-08-17 09:32:40 +00:00
|
|
|
/**
|
|
|
|
* Make a XY Cut tree for segmentation of the texts
|
|
|
|
*/
|
2021-08-26 15:46:29 +00:00
|
|
|
const RegionTextList tree = XYCutForBoundingBoxes(wordsWithCharacters, pageWidth, pageHeight);
|
2011-06-27 13:58:24 +00:00
|
|
|
|
2011-08-17 09:32:40 +00:00
|
|
|
/**
|
|
|
|
* Add spaces to the word
|
|
|
|
*/
|
2011-12-03 12:50:37 +00:00
|
|
|
const WordsWithCharacters listWithWordsAndSpaces = addNecessarySpace(tree, pageWidth, pageHeight);
|
2011-07-13 11:16:03 +00:00
|
|
|
|
2011-08-17 09:32:40 +00:00
|
|
|
/**
|
|
|
|
* Break the words into characters
|
|
|
|
*/
|
2011-12-03 12:50:37 +00:00
|
|
|
TextList listOfCharacters;
|
2019-12-09 13:16:55 +00:00
|
|
|
for (const WordWithCharacters &word : listWithWordsAndSpaces) {
|
2011-12-03 12:50:37 +00:00
|
|
|
delete word.word;
|
|
|
|
listOfCharacters.append(word.characters);
|
|
|
|
}
|
|
|
|
setWordList(listOfCharacters);
|
2011-06-27 13:58:24 +00:00
|
|
|
}
|
2011-10-31 15:33:03 +00:00
|
|
|
|
|
|
|
TextEntity::List TextPage::words(const RegularAreaRect *area, TextAreaInclusionBehaviour b) const
|
|
|
|
{
|
|
|
|
if (area && area->isNull()) {
|
|
|
|
return TextEntity::List();
|
2022-03-08 10:10:43 +00:00
|
|
|
}
|
2011-10-31 15:33:03 +00:00
|
|
|
|
|
|
|
TextEntity::List ret;
|
|
|
|
if (area) {
|
2019-12-09 13:16:55 +00:00
|
|
|
for (const TinyTextEntity *te : qAsConst(d->m_words)) {
|
2011-10-31 15:33:03 +00:00
|
|
|
if (b == AnyPixelTextAreaInclusionBehaviour) {
|
|
|
|
if (area->intersects(te->area)) {
|
|
|
|
ret.append(new TextEntity(te->text(), new Okular::NormalizedRect(te->area)));
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
const NormalizedPoint center = te->area.center();
|
|
|
|
if (area->contains(center.x, center.y)) {
|
|
|
|
ret.append(new TextEntity(te->text(), new Okular::NormalizedRect(te->area)));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
2019-12-09 13:16:55 +00:00
|
|
|
for (const TinyTextEntity *te : qAsConst(d->m_words)) {
|
2011-10-31 15:33:03 +00:00
|
|
|
ret.append(new TextEntity(te->text(), new Okular::NormalizedRect(te->area)));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return ret;
|
|
|
|
}
|
2012-03-25 21:48:07 +00:00
|
|
|
|
|
|
|
RegularAreaRect *TextPage::wordAt(const NormalizedPoint &p, QString *word) const
|
|
|
|
{
|
|
|
|
TextList::ConstIterator itBegin = d->m_words.constBegin(), itEnd = d->m_words.constEnd();
|
|
|
|
TextList::ConstIterator it = itBegin;
|
|
|
|
TextList::ConstIterator posIt = itEnd;
|
|
|
|
for (; it != itEnd; ++it) {
|
|
|
|
if ((*it)->area.contains(p.x, p.y)) {
|
|
|
|
posIt = it;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (posIt != itEnd) {
|
|
|
|
if ((*posIt)->text().simplified().isEmpty()) {
|
2017-09-05 21:27:18 +00:00
|
|
|
return nullptr;
|
2012-03-25 21:48:07 +00:00
|
|
|
}
|
|
|
|
// Find the first TinyTextEntity of the word
|
|
|
|
while (posIt != itBegin) {
|
|
|
|
--posIt;
|
|
|
|
const QString itText = (*posIt)->text();
|
|
|
|
if (itText.right(1).at(0).isSpace()) {
|
2015-10-29 12:37:11 +00:00
|
|
|
if (itText.endsWith(QLatin1String("-\n"))) {
|
2012-03-25 21:48:07 +00:00
|
|
|
// Is an hyphenated word
|
|
|
|
// continue searching the start of the word back
|
|
|
|
continue;
|
|
|
|
}
|
2020-07-10 22:15:05 +00:00
|
|
|
|
2015-10-29 12:37:11 +00:00
|
|
|
if (itText == QLatin1String("\n") && posIt != itBegin) {
|
2012-03-25 21:48:07 +00:00
|
|
|
--posIt;
|
2015-10-29 12:37:11 +00:00
|
|
|
if ((*posIt)->text().endsWith(QLatin1String("-"))) {
|
2012-03-25 21:48:07 +00:00
|
|
|
// Is an hyphenated word
|
|
|
|
// continue searching the start of the word back
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
++posIt;
|
|
|
|
}
|
|
|
|
|
|
|
|
++posIt;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
RegularAreaRect *ret = new RegularAreaRect();
|
2023-08-10 22:47:57 +00:00
|
|
|
QString foundWord;
|
2012-03-25 21:48:07 +00:00
|
|
|
for (; posIt != itEnd; ++posIt) {
|
|
|
|
const QString itText = (*posIt)->text();
|
|
|
|
if (itText.simplified().isEmpty()) {
|
|
|
|
break;
|
|
|
|
}
|
2020-07-10 22:15:05 +00:00
|
|
|
|
2012-03-25 21:48:07 +00:00
|
|
|
ret->appendShape((*posIt)->area);
|
2023-08-10 22:47:57 +00:00
|
|
|
foundWord += (*posIt)->text();
|
2012-03-25 21:48:07 +00:00
|
|
|
if (itText.right(1).at(0).isSpace()) {
|
2023-08-10 22:47:57 +00:00
|
|
|
if (!foundWord.endsWith(QLatin1String("-\n"))) {
|
2012-03-25 21:48:07 +00:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2020-07-10 22:15:05 +00:00
|
|
|
|
2012-03-25 21:48:07 +00:00
|
|
|
if (word) {
|
2023-08-10 22:47:57 +00:00
|
|
|
*word = foundWord;
|
2012-03-25 21:48:07 +00:00
|
|
|
}
|
|
|
|
return ret;
|
|
|
|
} else {
|
2017-09-05 21:27:18 +00:00
|
|
|
return nullptr;
|
2012-03-25 21:48:07 +00:00
|
|
|
}
|
|
|
|
}
|