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
qpdflinkmodel.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
4#include "qpdflink_p.h"
9
10#include "third_party/pdfium/public/fpdf_doc.h"
11#include "third_party/pdfium/public/fpdf_text.h"
12
13#include <QMetaEnum>
14
16
17Q_PDF_LOGGING_CATEGORY(qLcLink, "qt.pdf.links")
18
19/*!
20 \class QPdfLinkModel
21 \since 6.6
22 \inmodule QtPdf
23 \inherits QAbstractListModel
24
25 \brief The QPdfLinkModel class holds the geometry and the destination for
26 each link that the specified \l page contains.
27
28 This is used in PDF viewers to implement the hyperlink mechanism.
29*/
30
31/*!
32 \enum QPdfLinkModel::Role
33
34 \value Link A QPdfLink object.
35 \value Rectangle Bounding rectangle around the link.
36 \value Url If the link is a web link, the URL for that; otherwise an empty URL.
37 \value Page If the link is an internal link, the page number to which the link should jump; otherwise \c {-1}.
38 \value Location If the link is an internal link, the location on the page to which the link should jump.
39 \value Zoom If the link is an internal link, the suggested zoom level on the destination page.
40 \omitvalue NRoles
41*/
42
43/*!
44 Constructs a new link model with parent object \a parent.
45*/
46QPdfLinkModel::QPdfLinkModel(QObject *parent)
47 : QAbstractListModel(parent),
48 d_ptr{std::make_unique<QPdfLinkModelPrivate>(this)}
49{
50 Q_D(QPdfLinkModel);
51 QMetaEnum rolesMetaEnum = metaObject()->enumerator(metaObject()->indexOfEnumerator("Role"));
52 for (int r = Qt::UserRole; r < int(Role::NRoles); ++r)
53 d->roleNames.insert(r, QByteArray(rolesMetaEnum.valueToKey(r)).toLower());
54}
55
56/*!
57 Destroys the model.
58*/
59QPdfLinkModel::~QPdfLinkModel() {}
60
61QHash<int, QByteArray> QPdfLinkModel::roleNames() const
62{
63 Q_D(const QPdfLinkModel);
64 return d->roleNames;
65}
66
67/*!
68 \reimp
69*/
70int QPdfLinkModel::rowCount(const QModelIndex &parent) const
71{
72 Q_D(const QPdfLinkModel);
73 Q_UNUSED(parent);
74 return d->links.size();
75}
76
77/*!
78 \reimp
79*/
80QVariant QPdfLinkModel::data(const QModelIndex &index, int role) const
81{
82 Q_D(const QPdfLinkModel);
83 const auto &link = d->links.at(index.row());
84 switch (Role(role)) {
85 case Role::Link:
86 return QVariant::fromValue(link);
87 case Role::Rectangle:
88 return link.rectangles().empty() ? QVariant() : link.rectangles().constFirst();
89 case Role::Url:
90 return link.url();
91 case Role::Page:
92 return link.page();
93 case Role::Location:
94 return link.location();
95 case Role::Zoom:
96 return link.zoom();
97 case Role::NRoles:
98 break;
99 }
100 if (role == Qt::DisplayRole)
101 return link.toString();
102 return QVariant();
103}
104
105/*!
106 \property QPdfLinkModel::document
107 \brief The document to load links from.
108*/
109QPdfDocument *QPdfLinkModel::document() const
110{
111 Q_D(const QPdfLinkModel);
112 return d->document;
113}
114
115void QPdfLinkModel::setDocument(QPdfDocument *document)
116{
117 Q_D(QPdfLinkModel);
118 if (d->document == document)
119 return;
120 if (d->document)
121 disconnect(d->document, &QPdfDocument::statusChanged, this, &QPdfLinkModel::onStatusChanged);
122 connect(document, &QPdfDocument::statusChanged, this, &QPdfLinkModel::onStatusChanged);
123 d->document = document;
124 emit documentChanged();
125 if (page())
126 setPage(0);
127 else
128 d->update();
129}
130
131/*!
132 \property QPdfLinkModel::page
133 \brief The page to load links from.
134*/
135int QPdfLinkModel::page() const
136{
137 Q_D(const QPdfLinkModel);
138 return d->page;
139}
140
141void QPdfLinkModel::setPage(int page)
142{
143 Q_D(QPdfLinkModel);
144 if (d->page == page)
145 return;
146
147 d->page = page;
148 emit pageChanged(page);
149 d->update();
150}
151
152/*!
153 Returns a \l {QPdfLink::isValid()}{valid} link if found under the \a point
154 (given in units of points, 1/72 of an inch), or an invalid link if it is
155 not found. In other words, this function is useful for picking, to handle
156 mouse click or hover.
157*/
158QPdfLink QPdfLinkModel::linkAt(QPointF point) const
159{
160 Q_D(const QPdfLinkModel);
161 for (const auto &link : std::as_const(d->links)) {
162 for (const auto &rect : link.rectangles()) {
163 if (rect.contains(point))
164 return link;
165 }
166 }
167 return {};
168}
169
171{
172 Q_Q(QPdfLinkModel);
173 if (!document || !document->d->doc)
174 return;
175 auto doc = document->d->doc;
176 const QPdfMutexLocker lock;
177 FPDF_PAGE pdfPage = FPDF_LoadPage(doc, page);
178 if (!pdfPage) {
179 qCWarning(qLcLink) << "failed to load page" << page;
180 return;
181 }
182 q->beginResetModel();
183 links.clear();
184
185 // Iterate the ordinary links
186 int linkStart = 0;
187 bool hasNext = true;
188 while (hasNext) {
189 FPDF_LINK linkAnnot;
190 hasNext = FPDFLink_Enumerate(pdfPage, &linkStart, &linkAnnot);
191 if (!hasNext)
192 break;
193 FS_RECTF rect;
194 bool ok = FPDFLink_GetAnnotRect(linkAnnot, &rect);
195 if (!ok) {
196 qCWarning(qLcLink) << "skipping link with invalid bounding box";
197 continue; // while enumerating links
198 }
199 // In case horizontal/vertical coordinates are flipped, swap them.
200 if (rect.right < rect.left)
201 std::swap(rect.right, rect.left);
202 if (rect.bottom > rect.top)
203 std::swap(rect.bottom, rect.top);
204
205 QPdfLink linkData;
206 // Use quad points if present; otherwise use the rect.
207 if (int quadPointsCount = FPDFLink_CountQuadPoints(linkAnnot) > 0) {
208 for (int i = 0; i < quadPointsCount; ++i) {
209 FS_QUADPOINTSF point;
210 if (FPDFLink_GetQuadPoints(linkAnnot, i, &point)) {
211 // Quadpoints are counter clockwise from bottom left (x1, y1)
212 QPolygonF poly;
213 poly << QPointF(point.x1, point.y1);
214 poly << QPointF(point.x2, point.y2);
215 poly << QPointF(point.x3, point.y3);
216 poly << QPointF(point.x4, point.y4);
217 QRectF bounds = poly.boundingRect();
218 bounds = document->d->mapPageToView(pdfPage, bounds.left(), bounds.top(), bounds.right(), bounds.bottom());
219 qCDebug(qLcLink) << "quadpoints" << i << "of" << quadPointsCount << ":" << poly << "mapped bounds" << bounds;
220 linkData.d->rects << bounds;
221 // QPdfLink could store polygons rather than rects, to get the benefit of quadpoints;
222 // so far we didn't bother. It would be an API change, and we'd need to use Shapes in PdfLinkDelegate.qml
223 }
224 }
225 } else {
226 linkData.d->rects << document->d->mapPageToView(pdfPage, rect.left, rect.top, rect.right, rect.bottom);
227 }
228 FPDF_DEST dest = FPDFLink_GetDest(doc, linkAnnot);
229 FPDF_ACTION action = FPDFLink_GetAction(linkAnnot);
230 switch (FPDFAction_GetType(action)) {
231 case PDFACTION_UNSUPPORTED: // this happens with valid links in some PDFs
232 case PDFACTION_GOTO: {
233 linkData.d->page = FPDFDest_GetDestPageIndex(doc, dest);
234 if (linkData.d->page < 0) {
235 qCWarning(qLcLink) << "skipping link with invalid page number" << linkData.d->page;
236 continue; // while enumerating links
237 }
238 FPDF_BOOL hasX, hasY, hasZoom;
239 FS_FLOAT x, y, zoom;
240 ok = FPDFDest_GetLocationInPage(dest, &hasX, &hasY, &hasZoom, &x, &y, &zoom);
241 if (!ok) {
242 qCWarning(qLcLink) << "link with invalid location and/or zoom @" << linkData.d->rects;
243 break; // at least we got a page number, so the link will jump there
244 }
245 if (hasX && hasY)
246 linkData.d->location = document->d->mapPageToView(pdfPage, x, y);
247 if (hasZoom)
248 linkData.d->zoom = zoom;
249 break;
250 }
251 case PDFACTION_URI: {
252 unsigned long len = FPDFAction_GetURIPath(doc, action, nullptr, 0);
253 if (len < 1) {
254 qCWarning(qLcLink) << "skipping link with empty URI @" << linkData.d->rects;
255 continue; // while enumerating links
256 } else {
257 QByteArray buf(len, 0);
258 unsigned long got = FPDFAction_GetURIPath(doc, action, buf.data(), len);
259 Q_ASSERT(got == len);
260 linkData.d->url = QString::fromLatin1(buf.data(), got - 1);
261 }
262 break;
263 }
264 case PDFACTION_LAUNCH:
265 case PDFACTION_REMOTEGOTO: {
266 unsigned long len = FPDFAction_GetFilePath(action, nullptr, 0);
267 if (len < 1) {
268 qCWarning(qLcLink) << "skipping link with empty file path @" << linkData.d->rects;
269 continue; // while enumerating links
270 } else {
271 QByteArray buf(len, 0);
272 unsigned long got = FPDFAction_GetFilePath(action, buf.data(), len);
273 Q_ASSERT(got == len);
274 linkData.d->url = QUrl::fromLocalFile(QString::fromLatin1(buf.data(), got - 1)).toString();
275
276 // Unfortunately, according to comments in fpdf_doc.h, if it's PDFACTION_REMOTEGOTO,
277 // we can't get the page and location without first opening the linked document
278 // and then calling FPDFAction_GetDest() again.
279 }
280 break;
281 }
282 }
283 links << linkData;
284 }
285
286 // Iterate the web links
287 FPDF_TEXTPAGE textPage = FPDFText_LoadPage(pdfPage);
288 if (textPage) {
289 FPDF_PAGELINK webLinks = FPDFLink_LoadWebLinks(textPage);
290 if (webLinks) {
291 int count = FPDFLink_CountWebLinks(webLinks);
292 for (int i = 0; i < count; ++i) {
293 QPdfLink linkData;
294 int len = FPDFLink_GetURL(webLinks, i, nullptr, 0);
295 if (len < 1) {
296 qCWarning(qLcLink) << "skipping link" << i << "with empty URL";
297 } else {
298 QList<unsigned short> buf(len);
299 int got = FPDFLink_GetURL(webLinks, i, buf.data(), len);
300 Q_ASSERT(got == len);
301 linkData.d->url = QString::fromUtf16(
302 reinterpret_cast<const char16_t *>(buf.data()), got - 1);
303 }
304 len = FPDFLink_CountRects(webLinks, i);
305 for (int r = 0; r < len; ++r) {
306 double left, top, right, bottom;
307 bool success = FPDFLink_GetRect(webLinks, i, r, &left, &top, &right, &bottom);
308 if (success) {
309 linkData.d->rects << document->d->mapPageToView(pdfPage, left, top, right, bottom);
310 links << linkData;
311 }
312 }
313 }
314 FPDFLink_CloseWebLinks(webLinks);
315 }
316 FPDFText_ClosePage(textPage);
317 }
318
319 // All done
320 FPDF_ClosePage(pdfPage);
321 if (Q_UNLIKELY(qLcLink().isDebugEnabled())) {
322 for (const auto &l : links)
323 qCDebug(qLcLink) << l;
324 }
325 q->endResetModel();
326}
327
328void QPdfLinkModel::onStatusChanged(QPdfDocument::Status status)
329{
330 Q_D(QPdfLinkModel);
331 qCDebug(qLcLink) << "sees document statusChanged" << status;
332 if (status == QPdfDocument::Status::Ready)
333 d->update();
334}
335
336QT_END_NAMESPACE
337
338#include "moc_qpdflinkmodel.cpp"
#define Q_PDF_LOGGING_CATEGORY(name,...)