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 const QMetaEnum rolesMetaEnum = QMetaEnum::fromType<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->id() != 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 = Qt::TimerId::Invalid;
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 if (updateTimerId == Qt::TimerId::Invalid)
281 updateTimerId = Qt::TimerId(q->startTimer(UpdateTimerInterval));
282 // Go to Searching right away rather than in the first timer event: until the
283 // sweep begins, the status would otherwise still claim that the _previous_
284 // search is Finished, while the results of that search are already gone.
285 if (document && !searchString.isEmpty())
286 setStatus(QPdfSearchModel::Status::Searching);
287}
288
290{
291 if (page < 0 || page >= pagesSearched.size() || searchString.isEmpty())
292 return false;
293 if (pagesSearched[page])
294 return true;
295 Q_Q(QPdfSearchModel);
296
297 const QPdfMutexLocker lock;
298 QElapsedTimer timer;
299 timer.start();
300 FPDF_PAGE pdfPage = FPDF_LoadPage(document->d->doc, page);
301 if (!pdfPage) {
302 qWarning() << "failed to load page" << page;
303 return false;
304 }
305 FPDF_TEXTPAGE textPage = FPDFText_LoadPage(pdfPage);
306 if (!textPage) {
307 qWarning() << "failed to load text of page" << page;
308 FPDF_ClosePage(pdfPage);
309 return false;
310 }
311 FPDF_SCHHANDLE sh = FPDFText_FindStart(textPage, searchString.utf16(), 0, 0);
312 QList<QPdfLink> newSearchResults;
313 constexpr double CharacterHitTolerance = 6.0;
314 while (FPDFText_FindNext(sh)) {
315 int idx = FPDFText_GetSchResultIndex(sh);
316 int count = FPDFText_GetSchCount(sh);
317 int rectCount = FPDFText_CountRects(textPage, idx, count);
318 QList<QRectF> rects;
319 int startIndex = -1;
320 int endIndex = -1;
321 for (int r = 0; r < rectCount; ++r) {
322 // get bounding box of search result in page coordinates
323 double left, top, right, bottom;
324 FPDFText_GetRect(textPage, r, &left, &top, &right, &bottom);
325 // deal with any internal PDF transforms and
326 // convert to the 1x (pixels = points) 4th-quadrant coordinate system
327 rects << document->d->mapPageToView(pdfPage, left, top, right, bottom);
328 if (r == 0) {
329 startIndex = FPDFText_GetCharIndexAtPos(textPage, left, top,
330 CharacterHitTolerance, CharacterHitTolerance);
331 }
332 if (r == rectCount - 1) {
333 endIndex = FPDFText_GetCharIndexAtPos(textPage, right, top,
334 CharacterHitTolerance, CharacterHitTolerance);
335 }
336 qCDebug(qLcS) << rects.last() << "char idx" << startIndex << "->" << endIndex
337 << "from page rect" << left << top << right << bottom;
338 }
339 QString contextBefore, contextAfter;
340 if (startIndex >= 0 || endIndex >= 0) {
341 int contextStart = qMax(0, startIndex - ContextChars);
342 int contextEnd = endIndex + ContextChars;
343 int count = contextEnd - contextStart + 1;
344 if (count > 0) {
345 QList<ushort> buf(count + 1);
346 int len = FPDFText_GetText(textPage, contextStart, count, buf.data());
347 Q_ASSERT(len - 1 <= count); // len is number of characters written, including the terminator
348 QString context = QString::fromUtf16(
349 reinterpret_cast<const char16_t *>(buf.constData()), len - 1);
350
351 const auto replaceNewlines = [&](QString &s) {
352 s.replace(QLatin1Char('\n'), QStringLiteral("\u23CE"));
353 s.remove(QLatin1Char('\r'));
354 };
355
356 contextBefore = context.mid(0, startIndex - contextStart);
357 contextAfter = context.mid(startIndex - contextStart + searchString.size());
358 replaceNewlines(contextBefore);
359 replaceNewlines(contextAfter);
360 }
361 }
362 if (!rects.isEmpty())
363 newSearchResults << QPdfLink(page, rects, contextBefore, contextAfter);
364 }
365 FPDFText_FindClose(sh);
366 FPDFText_ClosePage(textPage);
367 FPDF_ClosePage(pdfPage);
368 qCDebug(qLcS) << searchString << "took" << timer.elapsed() << "ms to find"
369 << newSearchResults.size() << "results on page" << page;
370
371 pagesSearched[page] = true;
372 searchResults[page] = newSearchResults;
373 if (newSearchResults.size() > 0) {
374 int rowsBefore = rowsBeforePage(page);
375 qCDebug(qLcS) << "from row" << rowsBefore << "rowCount" << rowCountSoFar << "increasing by" << newSearchResults.size();
376 rowCountSoFar += newSearchResults.size();
377 q->beginInsertRows(QModelIndex(), rowsBefore, rowsBefore + newSearchResults.size() - 1);
378 q->endInsertRows();
379 }
380 return true;
381}
382
384{
385 if (pagesSearched.isEmpty())
386 return {-1, -1};
387 const int pageCount = document->pageCount();
388 int totalSoFar = 0;
389 int previousTotalSoFar = 0;
390 for (int page = 0; page < pageCount; ++page) {
391 if (!pagesSearched[page])
392 doSearch(page);
393 totalSoFar += searchResults[page].size();
394 if (totalSoFar > resultIndex)
395 return {page, resultIndex - previousTotalSoFar};
396 previousTotalSoFar = totalSoFar;
397 }
398 return {-1, -1};
399}
400
402{
403 int ret = 0;
404 for (int i = 0; i < page; ++i)
405 ret += searchResults[i].size();
406 return ret;
407}
408
409void QPdfSearchModelPrivate::setStatus(QPdfSearchModel::Status s)
410{
411 Q_Q(QPdfSearchModel);
412 if (status == s)
413 return;
414
415 status = s;
416 emit q->statusChanged(status);
417}
418
419QT_END_NAMESPACE
420
421#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,...)