diff options
author | Shawn Rutledge <shawn.rutledge@qt.io> | 2020-02-21 11:38:27 +0100 |
---|---|---|
committer | Shawn Rutledge <shawn.rutledge@qt.io> | 2020-02-21 11:40:49 +0100 |
commit | d9349a299f66fb154ad24f410451872a7ca253fb (patch) | |
tree | 2e8258ef3679707a2a9245c85bc8490251b3e256 /src/pdf/qpdfsearchmodel.cpp | |
parent | 50bc8b124705c33c5e27f035b1eab756e14247ba (diff) | |
parent | c0aa9d794378846e4cc0b6fe94f2765bc31cefdd (diff) |
Merge remote-tracking branch 'origin/wip/qtpdf' into 5.15v5.15.0-beta1
The feature set is mostly in place (except for some known shortcomings)
and we need the merge to build it on iOS.
Task-number: QTBUG-69519
Change-Id: Ib1ac82a9a7e0830d98d1c4327a1b15d4d7f4d4c1
Diffstat (limited to 'src/pdf/qpdfsearchmodel.cpp')
-rw-r--r-- | src/pdf/qpdfsearchmodel.cpp | 247 |
1 files changed, 221 insertions, 26 deletions
diff --git a/src/pdf/qpdfsearchmodel.cpp b/src/pdf/qpdfsearchmodel.cpp index 9010d76d3..4129c7cb7 100644 --- a/src/pdf/qpdfsearchmodel.cpp +++ b/src/pdf/qpdfsearchmodel.cpp @@ -34,80 +34,275 @@ ** ****************************************************************************/ +#include "qpdfdestination.h" +#include "qpdfdocument_p.h" #include "qpdfsearchmodel.h" #include "qpdfsearchmodel_p.h" -#include "qpdfdocument_p.h" +#include "qpdfsearchresult_p.h" #include "third_party/pdfium/public/fpdf_doc.h" #include "third_party/pdfium/public/fpdf_text.h" -#include <QLoggingCategory> +#include <QtCore/qelapsedtimer.h> +#include <QtCore/qloggingcategory.h> +#include <QtCore/QMetaEnum> QT_BEGIN_NAMESPACE Q_LOGGING_CATEGORY(qLcS, "qt.pdf.search") +static const int UpdateTimerInterval = 100; +static const int ContextChars = 20; +static const double CharacterHitTolerance = 6.0; + QPdfSearchModel::QPdfSearchModel(QObject *parent) - : QObject(parent), - d(new QPdfSearchModelPrivate()) + : QAbstractListModel(*(new QPdfSearchModelPrivate()), parent) { + QMetaEnum rolesMetaEnum = metaObject()->enumerator(metaObject()->indexOfEnumerator("Role")); + for (int r = Qt::UserRole; r < int(Role::_Count); ++r) { + QByteArray roleName = QByteArray(rolesMetaEnum.valueToKey(r)); + if (roleName.isEmpty()) + continue; + roleName[0] = QChar::toLower(roleName[0]); + m_roleNames.insert(r, roleName); + } } QPdfSearchModel::~QPdfSearchModel() {} -QVector<QRectF> QPdfSearchModel::matches(int page, const QString &searchString) +QHash<int, QByteArray> QPdfSearchModel::roleNames() const +{ + return m_roleNames; +} + +int QPdfSearchModel::rowCount(const QModelIndex &parent) const +{ + Q_D(const QPdfSearchModel); + Q_UNUSED(parent) + return d->rowCountSoFar; +} + +QVariant QPdfSearchModel::data(const QModelIndex &index, int role) const +{ + Q_D(const QPdfSearchModel); + const auto pi = const_cast<QPdfSearchModelPrivate*>(d)->pageAndIndexForResult(index.row()); + if (pi.page < 0) + return QVariant(); + switch (Role(role)) { + case Role::Page: + return pi.page; + case Role::IndexOnPage: + return pi.index; + case Role::Location: + return d->searchResults[pi.page][pi.index].location(); + case Role::Context: + return d->searchResults[pi.page][pi.index].context(); + case Role::_Count: + break; + } + if (role == Qt::DisplayRole) + return d->searchResults[pi.page][pi.index].context(); + return QVariant(); +} + +void QPdfSearchModel::updatePage(int page) +{ + Q_D(QPdfSearchModel); + d->doSearch(page); +} + +QString QPdfSearchModel::searchString() const +{ + Q_D(const QPdfSearchModel); + return d->searchString; +} + +void QPdfSearchModel::setSearchString(QString searchString) +{ + Q_D(QPdfSearchModel); + if (d->searchString == searchString) + return; + + d->searchString = searchString; + emit searchStringChanged(); + beginResetModel(); + d->clearResults(); + endResetModel(); +} + +QVector<QPdfSearchResult> QPdfSearchModel::resultsOnPage(int page) const +{ + Q_D(const QPdfSearchModel); + const_cast<QPdfSearchModelPrivate *>(d)->doSearch(page); + if (d->searchResults.count() <= page) + return {}; + return d->searchResults[page]; +} + +QPdfSearchResult QPdfSearchModel::resultAtIndex(int index) const +{ + Q_D(const QPdfSearchModel); + const auto pi = const_cast<QPdfSearchModelPrivate*>(d)->pageAndIndexForResult(index); + if (pi.page < 0) + return QPdfSearchResult(); + return d->searchResults[pi.page][pi.index]; +} + +QPdfDocument *QPdfSearchModel::document() const +{ + Q_D(const QPdfSearchModel); + return d->document; +} + +void QPdfSearchModel::setDocument(QPdfDocument *document) +{ + Q_D(QPdfSearchModel); + if (d->document == document) + return; + + d->document = document; + emit documentChanged(); + d->clearResults(); +} + +void QPdfSearchModel::timerEvent(QTimerEvent *event) +{ + Q_D(QPdfSearchModel); + if (event->timerId() != d->updateTimerId) + return; + if (!d->document || d->nextPageToUpdate >= d->document->pageCount()) { + if (d->document) + qCDebug(qLcS, "done updating search results on %d pages", d->searchResults.count()); + killTimer(d->updateTimerId); + d->updateTimerId = -1; + } + d->doSearch(d->nextPageToUpdate++); +} + +QPdfSearchModelPrivate::QPdfSearchModelPrivate() +{ +} + +void QPdfSearchModelPrivate::clearResults() +{ + Q_Q(QPdfSearchModel); + rowCountSoFar = 0; + searchResults.clear(); + pagesSearched.clear(); + if (document) { + searchResults.resize(document->pageCount()); + pagesSearched.resize(document->pageCount()); + } else { + searchResults.resize(0); + pagesSearched.resize(0); + } + nextPageToUpdate = 0; + updateTimerId = q->startTimer(UpdateTimerInterval); +} + +bool QPdfSearchModelPrivate::doSearch(int page) { + if (page < 0 || page >= pagesSearched.count() || searchString.isEmpty()) + return false; + if (pagesSearched[page]) + return true; + Q_Q(QPdfSearchModel); + const QPdfMutexLocker lock; - FPDF_PAGE pdfPage = FPDF_LoadPage(d->document->d->doc, page); + QElapsedTimer timer; + timer.start(); + FPDF_PAGE pdfPage = FPDF_LoadPage(document->d->doc, page); if (!pdfPage) { qWarning() << "failed to load page" << page; - return {}; + return false; } double pageHeight = FPDF_GetPageHeight(pdfPage); FPDF_TEXTPAGE textPage = FPDFText_LoadPage(pdfPage); if (!textPage) { qWarning() << "failed to load text of page" << page; FPDF_ClosePage(pdfPage); - return {}; + return false; } - QVector<QRectF> ret; - if (searchString.isEmpty()) - return ret; FPDF_SCHHANDLE sh = FPDFText_FindStart(textPage, searchString.utf16(), 0, 0); + QVector<QPdfSearchResult> newSearchResults; while (FPDFText_FindNext(sh)) { int idx = FPDFText_GetSchResultIndex(sh); int count = FPDFText_GetSchCount(sh); int rectCount = FPDFText_CountRects(textPage, idx, count); - qCDebug(qLcS) << searchString << ": matched" << count << "chars @" << idx << "across" << rectCount << "rects"; + QVector<QRectF> rects; + int startIndex = -1; + int endIndex = -1; for (int r = 0; r < rectCount; ++r) { double left, top, right, bottom; FPDFText_GetRect(textPage, r, &left, &top, &right, &bottom); - ret << QRectF(left, pageHeight - top, right - left, top - bottom); - qCDebug(qLcS) << ret.last(); + rects << QRectF(left, pageHeight - top, right - left, top - bottom); + if (r == 0) { + startIndex = FPDFText_GetCharIndexAtPos(textPage, left, top, + CharacterHitTolerance, CharacterHitTolerance); + } + if (r == rectCount - 1) { + endIndex = FPDFText_GetCharIndexAtPos(textPage, right, top, + CharacterHitTolerance, CharacterHitTolerance); + } + qCDebug(qLcS) << rects.last() << "char idx" << startIndex << "->" << endIndex; } + QString context; + if (startIndex >= 0 || endIndex >= 0) { + startIndex = qMax(0, startIndex - ContextChars); + endIndex += ContextChars; + int count = endIndex - startIndex + 1; + if (count > 0) { + QVector<ushort> buf(count + 1); + int len = FPDFText_GetText(textPage, startIndex, count, buf.data()); + Q_ASSERT(len - 1 <= count); // len is number of characters written, including the terminator + context = QString::fromUtf16(buf.constData(), len - 1); + context = context.replace(QLatin1Char('\n'), QLatin1Char(' ')); + context = context.replace(searchString, + QLatin1String("<b>") + searchString + QLatin1String("</b>")); + } + } + newSearchResults << QPdfSearchResult(page, rects, context); } FPDFText_FindClose(sh); FPDFText_ClosePage(textPage); FPDF_ClosePage(pdfPage); + qCDebug(qLcS) << searchString << "took" << timer.elapsed() << "ms to find" + << newSearchResults.count() << "results on page" << page; - return ret; -} - -QPdfDocument *QPdfSearchModel::document() const -{ - return d->document; + pagesSearched[page] = true; + searchResults[page] = newSearchResults; + if (newSearchResults.count() > 0) { + int rowsBefore = rowsBeforePage(page); + qCDebug(qLcS) << "from row" << rowsBefore << "rowCount" << rowCountSoFar << "increasing by" << newSearchResults.count(); + rowCountSoFar += newSearchResults.count(); + q->beginInsertRows(QModelIndex(), rowsBefore, rowsBefore + newSearchResults.count() - 1); + q->endInsertRows(); + } + return true; } -void QPdfSearchModel::setDocument(QPdfDocument *document) +QPdfSearchModelPrivate::PageAndIndex QPdfSearchModelPrivate::pageAndIndexForResult(int resultIndex) { - if (d->document == document) - return; - d->document = document; - emit documentChanged(); + const int pageCount = document->pageCount(); + int totalSoFar = 0; + int previousTotalSoFar = 0; + for (int page = 0; page < pageCount; ++page) { + if (!pagesSearched[page]) + doSearch(page); + totalSoFar += searchResults[page].count(); + if (totalSoFar > resultIndex) + return {page, resultIndex - previousTotalSoFar}; + previousTotalSoFar = totalSoFar; + } + return {-1, -1}; } -QPdfSearchModelPrivate::QPdfSearchModelPrivate() +int QPdfSearchModelPrivate::rowsBeforePage(int page) { + int ret = 0; + for (int i = 0; i < page; ++i) + ret += searchResults[i].count(); + return ret; } QT_END_NAMESPACE |