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