Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qpdfsearchmodel.cpp
Go to the documentation of this file.
1// Copyright (C) 2020 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3
5#include "qpdflink.h"
9
10#include "third_party/pdfium/public/fpdf_text.h"
11#include "third_party/pdfium/public/fpdfview.h"
12
13#include <QtCore/qelapsedtimer.h>
14#include <QtCore/QMetaEnum>
15
17
18Q_PDF_LOGGING_CATEGORY(qLcS, "qt.pdf.search")
19
20static const int UpdateTimerInterval = 100;
21static const int ContextChars = 64;
22
23/*!
24 \class QPdfSearchModel
25 \since 5.15
26 \inmodule QtPdf
27 \inherits QAbstractListModel
28
29 \brief The QPdfSearchModel class searches for a string in a PDF document
30 and holds the results.
31
32 This is used in the \l {Model/View Programming} paradigm to display
33 a list of search results, to highlight them on the rendered PDF pages,
34 and to iterate through them using the "search forward" / "search backward"
35 buttons and shortcuts that would be found in a typical document-viewing UI:
36
37 \image search-results.png {PDF viewer with search results panel showing
38 entries for "sand" and highlighted result on page}
39*/
40
41/*!
42 \enum QPdfSearchModel::Role
43
44 \value Page The page number where the search result is found (int).
45 \value IndexOnPage The index of the search result on the page (int).
46 \value Location The position of the search result on the page (QPointF).
47 \value ContextBefore The adjacent text on the page, before the search string (QString).
48 \value ContextAfter The adjacent text on the page, after the search string (QString).
49 \omitvalue NRoles
50
51 \sa QPdfLink
52*/
53
54/*!
55 Constructs a new search model with parent object \a parent.
56*/
57QPdfSearchModel::QPdfSearchModel(QObject *parent)
58 : QAbstractListModel(*(new QPdfSearchModelPrivate()), parent)
59{
60 QMetaEnum rolesMetaEnum = metaObject()->enumerator(metaObject()->indexOfEnumerator("Role"));
61 for (int r = Qt::UserRole; r < int(Role::NRoles); ++r) {
62 QByteArray roleName = QByteArray(rolesMetaEnum.valueToKey(r));
63 if (roleName.isEmpty())
64 continue;
65 roleName[0] = QChar::toLower(roleName[0]);
66 m_roleNames.insert(r, roleName);
67 }
68 connect(this, &QAbstractListModel::dataChanged, this, &QPdfSearchModel::countChanged);
69 connect(this, &QAbstractListModel::modelReset, this, &QPdfSearchModel::countChanged);
70 connect(this, &QAbstractListModel::rowsRemoved, this, &QPdfSearchModel::countChanged);
71 connect(this, &QAbstractListModel::rowsInserted, this, &QPdfSearchModel::countChanged);
72}
73
74/*!
75 Destroys the model.
76*/
77QPdfSearchModel::~QPdfSearchModel() {}
78
79/*!
80 \reimp
81*/
82QHash<int, QByteArray> QPdfSearchModel::roleNames() const
83{
84 return m_roleNames;
85}
86
87/*!
88 \reimp
89
90 The number of rows in the model is equal to the number of search results found.
91*/
92int QPdfSearchModel::rowCount(const QModelIndex &parent) const
93{
94 Q_D(const QPdfSearchModel);
95 Q_UNUSED(parent);
96 return d->rowCountSoFar;
97}
98
99/*!
100 \reimp
101*/
102QVariant QPdfSearchModel::data(const QModelIndex &index, int role) const
103{
104 Q_D(const QPdfSearchModel);
105 const auto pi = const_cast<QPdfSearchModelPrivate*>(d)->pageAndIndexForResult(index.row());
106 if (pi.page < 0)
107 return QVariant();
108 switch (Role(role)) {
109 case Role::Page:
110 return pi.page;
111 case Role::IndexOnPage:
112 return pi.index;
113 case Role::Location:
114 return d->searchResults[pi.page][pi.index].location();
115 case Role::ContextBefore:
116 return d->searchResults[pi.page][pi.index].contextBefore();
117 case Role::ContextAfter:
118 return d->searchResults[pi.page][pi.index].contextAfter();
119 case Role::NRoles:
120 break;
121 }
122 if (role == Qt::DisplayRole) {
123 const QString ret = d->searchResults[pi.page][pi.index].contextBefore() +
124 QLatin1String("<b>") + d->searchString + QLatin1String("</b>") +
125 d->searchResults[pi.page][pi.index].contextAfter();
126 return ret;
127 }
128 return QVariant();
129}
130
131/*!
132 \since 6.8
133 \property QPdfSearchModel::count
134 \brief the number of search results found
135*/
136int QPdfSearchModel::count() const
137{
138 return rowCount(QModelIndex());
139}
140
141/*!
142 \since 6.12
143 \enum QPdfSearchModel::Status
144
145 This enum describes the current status of the search model.
146
147 \value Null The initial status before the first search has begun.
148 \value Searching The status while the search is being conducted.
149 \value Finished The status when all possible search results have been found on all pages.
150
151 \sa QPdfSearchModel::status()
152*/
153
154/*!
155 \since 6.12
156 \property QPdfSearchModel::status
157 \brief the search status
158
159 This property tells whether a search is in progress.
160
161 \sa count()
162*/
163QPdfSearchModel::Status QPdfSearchModel::status() const
164{
165 Q_D(const QPdfSearchModel);
166 return d->status;
167}
168
169void QPdfSearchModel::updatePage(int page)
170{
171 Q_D(QPdfSearchModel);
172 d->doSearch(page);
173}
174
175/*!
176 \property QPdfSearchModel::searchString
177 \brief the string to search for
178*/
179QString QPdfSearchModel::searchString() const
180{
181 Q_D(const QPdfSearchModel);
182 return d->searchString;
183}
184
185void QPdfSearchModel::setSearchString(const QString &searchString)
186{
187 Q_D(QPdfSearchModel);
188 if (d->searchString == searchString)
189 return;
190
191 d->searchString = searchString;
192 beginResetModel();
193 d->clearResults();
194 emit searchStringChanged();
195 endResetModel();
196}
197
198/*!
199 Returns the list of all results found on the given \a page.
200*/
201QList<QPdfLink> QPdfSearchModel::resultsOnPage(int page) const
202{
203 Q_D(const QPdfSearchModel);
204 const_cast<QPdfSearchModelPrivate *>(d)->doSearch(page);
205 if (d->searchResults.size() <= page)
206 return {};
207 return d->searchResults[page];
208}
209
210/*!
211 Returns a result found by \a index in the \l document, regardless of the
212 page on which it was found. \a index must be less than \l rowCount.
213*/
214QPdfLink QPdfSearchModel::resultAtIndex(int index) const
215{
216 Q_D(const QPdfSearchModel);
217 const auto pi = const_cast<QPdfSearchModelPrivate*>(d)->pageAndIndexForResult(index);
218 if (pi.page < 0 || index < 0)
219 return {};
220 return d->searchResults[pi.page][pi.index];
221}
222
223/*!
224 \property QPdfSearchModel::document
225 \brief the document to search
226*/
227QPdfDocument *QPdfSearchModel::document() const
228{
229 Q_D(const QPdfSearchModel);
230 return d->document;
231}
232
233void QPdfSearchModel::setDocument(QPdfDocument *document)
234{
235 Q_D(QPdfSearchModel);
236 if (d->document == document)
237 return;
238
239 disconnect(d->documentConnection);
240 d->documentConnection = connect(document, &QPdfDocument::pageCountChanged, this,
241 [this]() { d_func()->clearResults(); });
242
243 d->document = document;
244 d->clearResults();
245 emit documentChanged();
246}
247
248void QPdfSearchModel::timerEvent(QTimerEvent *event)
249{
250 Q_D(QPdfSearchModel);
251 if (event->timerId() != d->updateTimerId)
252 return;
253 if (!d->document || d->nextPageToUpdate >= d->document->pageCount()) {
254 if (d->document)
255 qCDebug(qLcS) << "done updating search results on" << d->searchResults.size() << "pages";
256 killTimer(d->updateTimerId);
257 d->updateTimerId = -1;
258 d->setStatus(QPdfSearchModel::Status::Finished);
259 } else if (!d->searchString.isEmpty()) {
260 d->setStatus(QPdfSearchModel::Status::Searching);
261 }
262 d->doSearch(d->nextPageToUpdate++);
263}
264
265QPdfSearchModelPrivate::QPdfSearchModelPrivate() : QAbstractItemModelPrivate()
266{
267}
268
270{
271 Q_Q(QPdfSearchModel);
272 rowCountSoFar = 0;
273 searchResults.clear();
274 pagesSearched.clear();
275 if (document) {
276 searchResults.resize(document->pageCount());
277 pagesSearched.resize(document->pageCount());
278 }
280 updateTimerId = q->startTimer(UpdateTimerInterval);
281}
282
284{
285 if (page < 0 || page >= pagesSearched.size() || searchString.isEmpty())
286 return false;
287 if (pagesSearched[page])
288 return true;
289 Q_Q(QPdfSearchModel);
290
291 const QPdfMutexLocker lock;
292 QElapsedTimer timer;
293 timer.start();
294 FPDF_PAGE pdfPage = FPDF_LoadPage(document->d->doc, page);
295 if (!pdfPage) {
296 qWarning() << "failed to load page" << page;
297 return false;
298 }
299 FPDF_TEXTPAGE textPage = FPDFText_LoadPage(pdfPage);
300 if (!textPage) {
301 qWarning() << "failed to load text of page" << page;
302 FPDF_ClosePage(pdfPage);
303 return false;
304 }
305 FPDF_SCHHANDLE sh = FPDFText_FindStart(textPage, searchString.utf16(), 0, 0);
306 QList<QPdfLink> newSearchResults;
307 constexpr double CharacterHitTolerance = 6.0;
308 while (FPDFText_FindNext(sh)) {
309 int idx = FPDFText_GetSchResultIndex(sh);
310 int count = FPDFText_GetSchCount(sh);
311 int rectCount = FPDFText_CountRects(textPage, idx, count);
312 QList<QRectF> rects;
313 int startIndex = -1;
314 int endIndex = -1;
315 for (int r = 0; r < rectCount; ++r) {
316 // get bounding box of search result in page coordinates
317 double left, top, right, bottom;
318 FPDFText_GetRect(textPage, r, &left, &top, &right, &bottom);
319 // deal with any internal PDF transforms and
320 // convert to the 1x (pixels = points) 4th-quadrant coordinate system
321 rects << document->d->mapPageToView(pdfPage, left, top, right, bottom);
322 if (r == 0) {
323 startIndex = FPDFText_GetCharIndexAtPos(textPage, left, top,
324 CharacterHitTolerance, CharacterHitTolerance);
325 }
326 if (r == rectCount - 1) {
327 endIndex = FPDFText_GetCharIndexAtPos(textPage, right, top,
328 CharacterHitTolerance, CharacterHitTolerance);
329 }
330 qCDebug(qLcS) << rects.last() << "char idx" << startIndex << "->" << endIndex
331 << "from page rect" << left << top << right << bottom;
332 }
333 QString contextBefore, contextAfter;
334 if (startIndex >= 0 || endIndex >= 0) {
335 startIndex = qMax(0, startIndex - ContextChars);
336 endIndex += ContextChars;
337 int count = endIndex - startIndex + 1;
338 if (count > 0) {
339 QList<ushort> buf(count + 1);
340 int len = FPDFText_GetText(textPage, startIndex, count, buf.data());
341 Q_ASSERT(len - 1 <= count); // len is number of characters written, including the terminator
342 QString context = QString::fromUtf16(
343 reinterpret_cast<const char16_t *>(buf.constData()), len - 1);
344 context = context.replace(QLatin1Char('\n'), QStringLiteral("\u23CE"));
345 context = context.remove(QLatin1Char('\r'));
346 // try to find the search string near the middle of the context if possible
347 int si = context.indexOf(searchString, ContextChars - 5, Qt::CaseInsensitive);
348 if (si < 0)
349 si = context.indexOf(searchString, Qt::CaseInsensitive);
350 if (si < 0)
351 qCDebug(qLcS) << "search string" << searchString << "not found in context" << context;
352 contextBefore = context.mid(0, si);
353 contextAfter = context.mid(si + searchString.size());
354 }
355 }
356 if (!rects.isEmpty())
357 newSearchResults << QPdfLink(page, rects, contextBefore, contextAfter);
358 }
359 FPDFText_FindClose(sh);
360 FPDFText_ClosePage(textPage);
361 FPDF_ClosePage(pdfPage);
362 qCDebug(qLcS) << searchString << "took" << timer.elapsed() << "ms to find"
363 << newSearchResults.size() << "results on page" << page;
364
365 pagesSearched[page] = true;
366 searchResults[page] = newSearchResults;
367 if (newSearchResults.size() > 0) {
368 int rowsBefore = rowsBeforePage(page);
369 qCDebug(qLcS) << "from row" << rowsBefore << "rowCount" << rowCountSoFar << "increasing by" << newSearchResults.size();
370 rowCountSoFar += newSearchResults.size();
371 q->beginInsertRows(QModelIndex(), rowsBefore, rowsBefore + newSearchResults.size() - 1);
372 q->endInsertRows();
373 }
374 return true;
375}
376
378{
379 if (pagesSearched.isEmpty())
380 return {-1, -1};
381 const int pageCount = document->pageCount();
382 int totalSoFar = 0;
383 int previousTotalSoFar = 0;
384 for (int page = 0; page < pageCount; ++page) {
385 if (!pagesSearched[page])
386 doSearch(page);
387 totalSoFar += searchResults[page].size();
388 if (totalSoFar > resultIndex)
389 return {page, resultIndex - previousTotalSoFar};
390 previousTotalSoFar = totalSoFar;
391 }
392 return {-1, -1};
393}
394
396{
397 int ret = 0;
398 for (int i = 0; i < page; ++i)
399 ret += searchResults[i].size();
400 return ret;
401}
402
403void QPdfSearchModelPrivate::setStatus(QPdfSearchModel::Status s)
404{
405 Q_Q(QPdfSearchModel);
406 if (status == s)
407 return;
408
409 status = s;
410 emit q->statusChanged(status);
411}
412
413QT_END_NAMESPACE
414
415#include "moc_qpdfsearchmodel.cpp"
void setStatus(QPdfSearchModel::Status s)
PageAndIndex pageAndIndexForResult(int resultIndex)
Combined button and popup list for selecting options.
static const int ContextChars
#define Q_PDF_LOGGING_CATEGORY(name,...)