/src/poppler/cpp/poppler-page.cpp
Line | Count | Source |
1 | | /* |
2 | | * Copyright (C) 2009-2010, Pino Toscano <pino@kde.org> |
3 | | * Copyright (C) 2017-2020, 2025, 2026, Albert Astals Cid <aacid@kde.org> |
4 | | * Copyright (C) 2017, Jason Alan Palmer <jalanpalmer@gmail.com> |
5 | | * Copyright (C) 2018, 2020, Suzuki Toshiya <mpsuzuki@hiroshima-u.ac.jp> |
6 | | * Copyright (C) 2018, 2020, Adam Reichold <adam.reichold@t-online.de> |
7 | | * Copyright (C) 2018, Zsombor Hollay-Horvath <hollay.horvath@gmail.com> |
8 | | * Copyright (C) 2018, Aleksey Nikolaev <nae202@gmail.com> |
9 | | * Copyright (C) 2020, Jiri Jakes <freedesktop@jirijakes.eu> |
10 | | * Copyright (C) 2026 g10 Code GmbH, Author: Sune Stolborg Vuorela <sune@vuorela.dk> |
11 | | * |
12 | | * This program is free software; you can redistribute it and/or modify |
13 | | * it under the terms of the GNU General Public License as published by |
14 | | * the Free Software Foundation; either version 2, or (at your option) |
15 | | * any later version. |
16 | | * |
17 | | * This program is distributed in the hope that it will be useful, |
18 | | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
19 | | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
20 | | * GNU General Public License for more details. |
21 | | * |
22 | | * You should have received a copy of the GNU General Public License |
23 | | * along with this program; if not, write to the Free Software |
24 | | * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. |
25 | | */ |
26 | | |
27 | | /** |
28 | | \file poppler-page.h |
29 | | */ |
30 | | #include "poppler-page.h" |
31 | | #include "poppler-page-transition.h" |
32 | | |
33 | | #include "poppler-document-private.h" |
34 | | #include "poppler-page-private.h" |
35 | | #include "poppler-private.h" |
36 | | #include "poppler-font-private.h" |
37 | | #include "poppler-font.h" |
38 | | |
39 | | #include "TextOutputDev.h" |
40 | | |
41 | | #include <memory> |
42 | | #include <utility> |
43 | | |
44 | | using namespace poppler; |
45 | | |
46 | 243k | page_private::page_private(document_private *_doc, int _index) : doc(_doc), page(doc->doc->getCatalog()->getPage(_index + 1)), index(_index) { } |
47 | | |
48 | | page_private::~page_private() |
49 | 243k | { |
50 | 243k | delete transition; |
51 | 243k | } |
52 | | |
53 | | void page_private::init_font_info_cache() |
54 | 759k | { |
55 | 759k | if (font_info_cache_initialized) { |
56 | 752k | return; |
57 | 752k | } |
58 | | |
59 | 6.91k | poppler::font_iterator it(index, doc); |
60 | | |
61 | 6.91k | if (it.has_next()) { |
62 | 6.91k | font_info_cache = it.next(); |
63 | 6.91k | } |
64 | | |
65 | 6.91k | font_info_cache_initialized = true; |
66 | 6.91k | } |
67 | | |
68 | | /** |
69 | | \class poppler::page poppler-page.h "poppler/cpp/poppler-page.h" |
70 | | |
71 | | A page in a PDF %document. |
72 | | */ |
73 | | |
74 | | /** |
75 | | \enum poppler::page::orientation_enum |
76 | | |
77 | | The possible orientation of a page. |
78 | | */ |
79 | | |
80 | | /** |
81 | | \enum poppler::page::search_direction_enum |
82 | | |
83 | | The direction/action to follow when performing a text search. |
84 | | */ |
85 | | |
86 | | /** |
87 | | \enum poppler::page::text_layout_enum |
88 | | |
89 | | A layout of the text of a page. |
90 | | */ |
91 | | |
92 | 243k | page::page(document_private *doc, int index) : d(new page_private(doc, index)) { } |
93 | | |
94 | | /** |
95 | | Destructor. |
96 | | */ |
97 | | page::~page() |
98 | 243k | { |
99 | 243k | delete d; |
100 | 243k | } |
101 | | |
102 | | /** |
103 | | \returns the orientation of the page |
104 | | */ |
105 | | page::orientation_enum page::orientation() const |
106 | 0 | { |
107 | 0 | const int rotation = d->page->getRotate(); |
108 | 0 | switch (rotation) { |
109 | 0 | case 90: |
110 | 0 | return landscape; |
111 | 0 | break; |
112 | 0 | case 180: |
113 | 0 | return upside_down; |
114 | 0 | break; |
115 | 0 | case 270: |
116 | 0 | return seascape; |
117 | 0 | break; |
118 | 0 | default: |
119 | 0 | return portrait; |
120 | 0 | } |
121 | 0 | } |
122 | | |
123 | | /** |
124 | | The eventual duration the page can be hinted to be shown in a presentation. |
125 | | |
126 | | If this value is positive (usually different than -1) then a PDF viewer, when |
127 | | showing the page in a presentation, should show the page for at most for this |
128 | | number of seconds, and then switch to the next page (if any). Note this is |
129 | | purely a presentation attribute, it has no influence on the behaviour. |
130 | | |
131 | | \returns the duration time (in seconds) of the page |
132 | | */ |
133 | | double page::duration() const |
134 | 0 | { |
135 | 0 | return d->page->getDuration(); |
136 | 0 | } |
137 | | |
138 | | /** |
139 | | Returns the size of one rect of the page. |
140 | | |
141 | | \returns the size of the specified page rect |
142 | | */ |
143 | | rectf page::page_rect(page_box_enum box) const |
144 | 11.9k | { |
145 | 11.9k | std::optional<PDFRectangle> r; |
146 | 11.9k | switch (box) { |
147 | 0 | case media_box: |
148 | 0 | r = d->page->getMediaBox(); |
149 | 0 | break; |
150 | 11.9k | case crop_box: |
151 | 11.9k | r = d->page->getCropBox(); |
152 | 11.9k | break; |
153 | 0 | case bleed_box: |
154 | 0 | r = d->page->getBleedBox(); |
155 | 0 | break; |
156 | 0 | case trim_box: |
157 | 0 | r = d->page->getTrimBox(); |
158 | 0 | break; |
159 | 0 | case art_box: |
160 | 0 | r = d->page->getArtBox(); |
161 | 0 | break; |
162 | 11.9k | } |
163 | 11.9k | if (r) { |
164 | 11.9k | return detail::pdfrectangle_to_rectf(*r); |
165 | 11.9k | } |
166 | 0 | return rectf(); |
167 | 11.9k | } |
168 | | |
169 | | /** |
170 | | \returns the label of the page, if any |
171 | | */ |
172 | | ustring page::label() const |
173 | 33.4k | { |
174 | 33.4k | std::string label; |
175 | 33.4k | if (!d->doc->doc->getCatalog()->indexToLabel(d->index, &label)) { |
176 | 400 | return ustring(); |
177 | 400 | } |
178 | | |
179 | 33.0k | return detail::unicode_string_to_ustring(label); |
180 | 33.4k | } |
181 | | |
182 | | /** |
183 | | The transition from this page to the next one. |
184 | | |
185 | | If it is set, then a PDF viewer in a presentation should perform the |
186 | | specified transition effect when switching from this page to the next one. |
187 | | |
188 | | \returns the transition effect for the switch to the next page, if any |
189 | | */ |
190 | | page_transition *page::transition() const |
191 | 0 | { |
192 | 0 | if (!d->transition) { |
193 | 0 | Object o = d->page->getTrans(); |
194 | 0 | if (o.isDict()) { |
195 | 0 | d->transition = new page_transition(&o); |
196 | 0 | } |
197 | 0 | } |
198 | 0 | return d->transition; |
199 | 0 | } |
200 | | |
201 | | /** |
202 | | Search the page for some text. |
203 | | |
204 | | \param text the text to search |
205 | | \param[in,out] r the area where to start search, which will be set to the area |
206 | | of the match (if any) |
207 | | \param direction in which direction search for text |
208 | | \param case_sensitivity whether search in a case sensitive way |
209 | | \param rotation the rotation assumed for the page |
210 | | */ |
211 | | bool page::search(const ustring &text, rectf &r, search_direction_enum direction, case_sensitivity_enum case_sensitivity, rotation_enum rotation) const |
212 | 11.9k | { |
213 | 11.9k | const size_t len = text.length(); |
214 | | |
215 | 11.9k | if (len == 0) { |
216 | 942 | return false; |
217 | 942 | } |
218 | | |
219 | 10.9k | std::vector<Unicode> u(len); |
220 | 169k | for (size_t i = 0; i < len; ++i) { |
221 | 158k | u[i] = text[i]; |
222 | 158k | } |
223 | | |
224 | 10.9k | const bool sCase = case_sensitivity == case_sensitive; |
225 | 10.9k | const int rotation_value = static_cast<int>(rotation) * 90; |
226 | | |
227 | 10.9k | bool found = false; |
228 | 10.9k | double rect_left = r.left(); |
229 | 10.9k | double rect_top = r.top(); |
230 | 10.9k | double rect_right = r.right(); |
231 | 10.9k | double rect_bottom = r.bottom(); |
232 | | |
233 | 10.9k | TextOutputDev td(nullptr, true, 0, false, false); |
234 | 10.9k | d->doc->doc->displayPage(&td, d->index + 1, 72, 72, rotation_value, false, true, false); |
235 | 10.9k | std::unique_ptr<TextPage> text_page = td.takeText(); |
236 | | |
237 | 10.9k | switch (direction) { |
238 | 10.9k | case search_from_top: |
239 | 10.9k | found = text_page->findText(u.data(), len, true, true, false, false, sCase, false, false, &rect_left, &rect_top, &rect_right, &rect_bottom); |
240 | 10.9k | break; |
241 | 0 | case search_next_result: |
242 | 0 | found = text_page->findText(u.data(), len, false, true, true, false, sCase, false, false, &rect_left, &rect_top, &rect_right, &rect_bottom); |
243 | 0 | break; |
244 | 0 | case search_previous_result: |
245 | 0 | found = text_page->findText(u.data(), len, false, true, true, false, sCase, true, false, &rect_left, &rect_top, &rect_right, &rect_bottom); |
246 | 0 | break; |
247 | 10.9k | } |
248 | | |
249 | 10.9k | r.set_left(rect_left); |
250 | 10.9k | r.set_top(rect_top); |
251 | 10.9k | r.set_right(rect_right); |
252 | 10.9k | r.set_bottom(rect_bottom); |
253 | | |
254 | 10.9k | return found; |
255 | 10.9k | } |
256 | | |
257 | | /** |
258 | | Returns the text in the page, in its physical layout. |
259 | | |
260 | | \param r if not empty, it will be extracted the text in it; otherwise, the |
261 | | text of the whole page |
262 | | |
263 | | \returns the text of the page in the specified rect or in the whole page |
264 | | */ |
265 | | ustring page::text(const rectf &r) const |
266 | 0 | { |
267 | 0 | return text(r, physical_layout); |
268 | 0 | } |
269 | | |
270 | | static void appendToGooString(void *stream, const char *text, int len) |
271 | 0 | { |
272 | 0 | (static_cast<GooString *>(stream))->append(text, len); |
273 | 0 | } |
274 | | |
275 | | /** |
276 | | Returns the text in the page. |
277 | | |
278 | | \param rect if not empty, it will be extracted the text in it; otherwise, the |
279 | | text of the whole page |
280 | | \param layout_mode the layout of the text. non_raw_non_physical_layout is not |
281 | | supported together with a non empty rect |
282 | | |
283 | | \returns the text of the page in the specified rect or in the whole page |
284 | | |
285 | | \since 0.16 |
286 | | */ |
287 | | ustring page::text(const rectf &r, text_layout_enum layout_mode) const |
288 | 0 | { |
289 | 0 | GooString out; |
290 | 0 | const bool use_raw_order = (layout_mode == raw_order_layout); |
291 | 0 | const bool use_physical_layout = (layout_mode == physical_layout); |
292 | 0 | TextOutputDev td(&appendToGooString, &out, use_physical_layout, 0, use_raw_order, false); |
293 | 0 | if (r.is_empty()) { |
294 | 0 | d->doc->doc->displayPage(&td, d->index + 1, 72, 72, 0, false, true, false); |
295 | 0 | } else { |
296 | 0 | if (layout_mode == non_raw_non_physical_layout) { |
297 | 0 | detail::user_debug_function("non_raw_non_physical_layout is not supported together with a non empty rect", detail::debug_closure); |
298 | 0 | return {}; |
299 | 0 | } |
300 | 0 | d->doc->doc->displayPageSlice(&td, d->index + 1, 72, 72, 0, false, true, false, r.left(), r.top(), r.width(), r.height()); |
301 | 0 | } |
302 | 0 | return ustring::from_utf8(out.c_str()); |
303 | 0 | } |
304 | | |
305 | | /* |
306 | | * text_box_font_info object for text_box |
307 | | */ |
308 | 1.51M | text_box_font_info_data::~text_box_font_info_data() = default; |
309 | | |
310 | | /* |
311 | | * text_box object for page::text_list() |
312 | | */ |
313 | 759k | text_box_data::~text_box_data() = default; |
314 | | |
315 | 1.51M | text_box::~text_box() = default; |
316 | | |
317 | 0 | text_box &text_box::operator=(text_box &&a) noexcept = default; |
318 | 759k | text_box::text_box(text_box &&a) noexcept = default; |
319 | | |
320 | 759k | text_box::text_box(text_box_data *data) : m_data { data } { } |
321 | | |
322 | | ustring text_box::text() const |
323 | 0 | { |
324 | 0 | return m_data->text; |
325 | 0 | } |
326 | | |
327 | | rectf text_box::bbox() const |
328 | 0 | { |
329 | 0 | return m_data->bbox; |
330 | 0 | } |
331 | | |
332 | | int text_box::rotation() const |
333 | 0 | { |
334 | 0 | return m_data->rotation; |
335 | 0 | } |
336 | | |
337 | | rectf text_box::char_bbox(size_t i) const |
338 | 0 | { |
339 | 0 | if (i < m_data->char_bboxes.size()) { |
340 | 0 | return m_data->char_bboxes[i]; |
341 | 0 | } |
342 | 0 | return rectf(0, 0, 0, 0); |
343 | 0 | } |
344 | | |
345 | | bool text_box::has_space_after() const |
346 | 0 | { |
347 | 0 | return m_data->has_space_after; |
348 | 0 | } |
349 | | |
350 | | bool text_box::has_font_info() const |
351 | 0 | { |
352 | 0 | return (m_data->text_box_font != nullptr); |
353 | 0 | } |
354 | | |
355 | | text_box::writing_mode_enum text_box::get_wmode(int i) const |
356 | 0 | { |
357 | 0 | if (this->has_font_info()) { |
358 | 0 | return m_data->text_box_font->wmodes[i]; |
359 | 0 | } |
360 | 0 | return text_box::invalid_wmode; |
361 | 0 | } |
362 | | |
363 | | double text_box::get_font_size() const |
364 | 0 | { |
365 | 0 | if (this->has_font_info()) { |
366 | 0 | return m_data->text_box_font->font_size; |
367 | 0 | } |
368 | 0 | return -1; |
369 | 0 | } |
370 | | |
371 | | std::string text_box::get_font_name(int i) const |
372 | 0 | { |
373 | 0 | if (!this->has_font_info()) { |
374 | 0 | return std::string("*ignored*"); |
375 | 0 | } |
376 | | |
377 | 0 | int j = m_data->text_box_font->glyph_to_cache_index[i]; |
378 | 0 | if (j < 0) { |
379 | 0 | return std::string(""); |
380 | 0 | } |
381 | 0 | return m_data->text_box_font->font_info_cache[j].name(); |
382 | 0 | } |
383 | | |
384 | | std::vector<text_box> page::text_list(int opt_flag) const |
385 | 16.4k | { |
386 | 16.4k | std::vector<text_box> output_list; |
387 | | |
388 | | /* config values are same with Qt5 Page::TextList() */ |
389 | 16.4k | auto output_dev = std::make_unique<TextOutputDev>(nullptr, /* char* fileName */ |
390 | 16.4k | false, /* bool physLayoutA */ |
391 | 16.4k | 0, /* double fixedPitchA */ |
392 | 16.4k | false, /* bool rawOrderA */ |
393 | 16.4k | false /* bool append */ |
394 | 16.4k | ); |
395 | | |
396 | | /* |
397 | | * config values are same with Qt5 Page::TextList(), |
398 | | * but rotation is fixed to zero. |
399 | | * Few people use non-zero values. |
400 | | */ |
401 | 16.4k | d->doc->doc->displayPageSlice(output_dev.get(), d->index + 1, /* page */ |
402 | 16.4k | 72, 72, 0, /* hDPI, vDPI, rot */ |
403 | 16.4k | false, false, false, /* useMediaBox, crop, printing */ |
404 | 16.4k | -1, -1, -1, -1, /* sliceX, sliceY, sliceW, sliceH */ |
405 | 16.4k | nullptr, nullptr, /* abortCheckCbk(), abortCheckCbkData */ |
406 | 16.4k | nullptr, nullptr, /* annotDisplayDecideCbk(), annotDisplayDecideCbkData */ |
407 | 16.4k | true); /* copyXRef */ |
408 | | |
409 | 16.4k | const std::unique_ptr<TextWordList> word_list = output_dev->makeWordList(); |
410 | | |
411 | 16.4k | const std::vector<TextWord *> &words = word_list->getWords(); |
412 | 16.4k | output_list.reserve(words.size()); |
413 | 759k | for (const TextWord *word : words) { |
414 | 759k | const std::unique_ptr<std::string> wordText = word->getText(); |
415 | 759k | const ustring ustr = ustring::from_utf8(wordText->c_str()); |
416 | | |
417 | 759k | double xMin, yMin, xMax, yMax; |
418 | 759k | word->getBBox(&xMin, &yMin, &xMax, &yMax); |
419 | | |
420 | 759k | text_box tb { new text_box_data { .text = ustr, .bbox = { xMin, yMin, xMax - xMin, yMax - yMin }, .rotation = word->getRotation(), .char_bboxes = {}, .has_space_after = word->hasSpaceAfter(), .text_box_font = nullptr } }; |
421 | | |
422 | 759k | std::unique_ptr<text_box_font_info_data> tb_font_info; |
423 | 759k | if (opt_flag & page::text_list_include_font) { |
424 | 759k | d->init_font_info_cache(); |
425 | | |
426 | 759k | tb_font_info = std::make_unique<text_box_font_info_data>(text_box_font_info_data { |
427 | 759k | .font_size = word->getFontSize(), // double font_size |
428 | 759k | .wmodes = {}, // std::vector<text_box::writing_mode> wmodes; |
429 | 759k | .font_info_cache = d->font_info_cache, // std::vector<font_info> font_info_cache; |
430 | 759k | .glyph_to_cache_index = {} // std::vector<int> glyph_to_cache_index; |
431 | 759k | }); |
432 | 759k | }; |
433 | | |
434 | 759k | tb.m_data->char_bboxes.reserve(word->getLength()); |
435 | 4.21M | for (int j = 0; j < word->getLength(); j++) { |
436 | 3.45M | word->getCharBBox(j, &xMin, &yMin, &xMax, &yMax); |
437 | 3.45M | tb.m_data->char_bboxes.emplace_back(xMin, yMin, xMax - xMin, yMax - yMin); |
438 | 3.45M | } |
439 | | |
440 | 759k | if (tb_font_info && d->font_info_cache_initialized) { |
441 | 759k | tb_font_info->glyph_to_cache_index.reserve(word->getLength()); |
442 | 4.21M | for (int j = 0; j < word->getLength(); j++) { |
443 | 3.45M | const TextFontInfo *cur_text_font_info = word->getFontInfo(j); |
444 | | |
445 | 3.45M | switch (cur_text_font_info->getWMode()) { |
446 | 3.45M | case GfxFont::WritingMode::Horizontal: |
447 | 3.45M | tb_font_info->wmodes.push_back(text_box::horizontal_wmode); |
448 | 3.45M | break; |
449 | 4.79k | case GfxFont::WritingMode::Vertical: |
450 | 4.79k | tb_font_info->wmodes.push_back(text_box::vertical_wmode); |
451 | 4.79k | break; |
452 | 3.45M | }; |
453 | | |
454 | 3.45M | tb_font_info->glyph_to_cache_index.push_back(-1); |
455 | 8.94M | for (size_t k = 0; k < tb_font_info->font_info_cache.size(); k++) { |
456 | 8.94M | if (cur_text_font_info->matches(&(tb_font_info->font_info_cache[k].d->ref))) { |
457 | 3.45M | tb_font_info->glyph_to_cache_index[j] = k; |
458 | 3.45M | break; |
459 | 3.45M | } |
460 | 8.94M | } |
461 | 3.45M | } |
462 | 759k | tb.m_data->text_box_font = std::move(tb_font_info); |
463 | 759k | } |
464 | | |
465 | 759k | output_list.push_back(std::move(tb)); |
466 | 759k | } |
467 | | |
468 | 16.4k | return output_list; |
469 | 16.4k | } |
470 | | |
471 | | std::vector<text_box> page::text_list() const |
472 | 0 | { |
473 | 0 | return text_list(0); |
474 | 0 | } |