File Coverage

xs/extract.xs
Criterion Covered Total %
statement 192 223 86.1
branch 64 96 66.6
condition n/a
subroutine n/a
pod n/a
total 256 319 80.2


line stmt bran cond sub pod time code
1             MODULE = PDF::Make PACKAGE = PDF::Make::Extract
2             PROTOTYPES: ENABLE
3              
4             SV *
5             _extract_from_reader(class, reader_sv, page_index)
6             char *class
7             pdfmake_reader_xs_t *reader_sv
8             UV page_index
9             PREINIT:
10             pdfmake_err_t err;
11             pdfmake_arena_t *arena;
12             pdfmake_interp_t *interp;
13             pdfmake_textract_result_t *result;
14             pdfmake_textract_options_t opts;
15             pdfmake_reader_page_t *rpage;
16             pdfmake_buf_t content_buf;
17             pdfmake_obj_t *resources;
18             char outbuf[65536];
19             size_t outlen;
20             CODE:
21             PERL_UNUSED_VAR(class);
22 4           pdfmake_buf_init(&content_buf);
23              
24 4 50         if (page_index >= pdfmake_reader_page_count(reader_sv->reader))
25 0           croak("PDF::Make::Extract: page index %lu out of range", (unsigned long)page_index);
26              
27 4           rpage = pdfmake_reader_page_at(reader_sv->reader, page_index);
28 4 50         if (!rpage)
29 0           croak("PDF::Make::Extract: failed to get page %lu", (unsigned long)page_index);
30              
31             /* Get content stream bytes */
32 4           err = pdfmake_reader_page_content_bytes(reader_sv->reader, rpage, &content_buf);
33 4 50         if (err != PDFMAKE_OK || content_buf.len == 0) {
    50          
34 0           pdfmake_buf_free(&content_buf);
35 0           RETVAL = newSVpvn("", 0);
36             } else {
37             /* Create interpreter with resources */
38             /* Use the parser's arena so name IDs match across: stream dicts,
39             * font dicts (all from parser), merged resources (keys re-interned
40             * here during merge), and the interpreter's own lookups. */
41 4           arena = reader_sv->reader->parser->doc->arena;
42 4           interp = pdfmake_interp_new(arena);
43 4 50         if (!interp) {
44 0           pdfmake_buf_free(&content_buf);
45 0           croak("PDF::Make::Extract: failed to create interpreter");
46             }
47             /* Phase 7: enable Form XObject recursion */
48 4           pdfmake_interp_set_reader(interp, reader_sv->reader);
49              
50             /* Set page resources so Tf can resolve fonts */
51 4           resources = pdfmake_reader_page_resources(reader_sv->reader, rpage);
52 4 50         if (resources) {
53 4           pdfmake_interp_set_resources(interp, resources);
54             }
55              
56             /* Run extraction */
57 4           result = pdfmake_textract_new(arena);
58 4           pdfmake_textract_set_reader(result, reader_sv->reader);
59 4           opts = pdfmake_textract_default_options();
60 4           err = pdfmake_textract_run(interp, content_buf.data, content_buf.len, &opts, result);
61              
62 4           pdfmake_interp_free(interp);
63 4           pdfmake_buf_free(&content_buf);
64              
65 4 50         if (err != PDFMAKE_OK) {
66 0           pdfmake_textract_free(result);
67 0           croak("PDF::Make::Extract: extraction failed");
68             }
69              
70 4           outlen = pdfmake_textract_to_utf8(result, outbuf, sizeof(outbuf) - 1);
71 4           outbuf[outlen] = '\0';
72              
73 4           RETVAL = newSVpvn(outbuf, outlen);
74 4           SvUTF8_on(RETVAL);
75              
76 4           pdfmake_textract_free(result);
77             /* arena owned by reader; do not free here */
78             }
79             OUTPUT:
80             RETVAL
81              
82             SV *
83             _extract_structured(class, reader_sv, page_index, ...)
84             char *class
85             pdfmake_reader_xs_t *reader_sv
86             UV page_index
87             PREINIT:
88             pdfmake_err_t err;
89             pdfmake_arena_t *arena;
90             pdfmake_interp_t *interp;
91             pdfmake_textract_result_t *result;
92             pdfmake_textract_options_t opts;
93             pdfmake_reader_page_t *rpage;
94             pdfmake_buf_t content_buf;
95             pdfmake_obj_t *resources;
96 54           int include_invisible = 1; /* default: include Tr=3 text (OCR) */
97             CODE:
98             PERL_UNUSED_VAR(class);
99             /* Optional 4th arg: include_invisible (0 or 1). */
100 54 50         if (items > 3) include_invisible = (int)SvIV(ST(3));
101 54           pdfmake_buf_init(&content_buf);
102              
103 54 50         if (page_index >= pdfmake_reader_page_count(reader_sv->reader))
104 0           croak("PDF::Make::Extract: page index %lu out of range", (unsigned long)page_index);
105              
106 54           rpage = pdfmake_reader_page_at(reader_sv->reader, page_index);
107 54 50         if (!rpage)
108 0           croak("PDF::Make::Extract: failed to get page %lu", (unsigned long)page_index);
109              
110 54           err = pdfmake_reader_page_content_bytes(reader_sv->reader, rpage, &content_buf);
111 54 50         if (err != PDFMAKE_OK || content_buf.len == 0) {
    50          
112 0           pdfmake_buf_free(&content_buf);
113 0           RETVAL = newRV_noinc((SV *)newAV());
114             } else {
115             /* Use the parser's arena so name IDs match across: stream dicts,
116             * font dicts (all from parser), merged resources (keys re-interned
117             * here during merge), and the interpreter's own lookups. */
118 54           arena = reader_sv->reader->parser->doc->arena;
119 54           interp = pdfmake_interp_new(arena);
120 54 50         if (!interp) {
121 0           pdfmake_buf_free(&content_buf);
122 0           croak("PDF::Make::Extract: failed to create interpreter");
123             }
124             /* Phase 7: enable Form XObject recursion */
125 54           pdfmake_interp_set_reader(interp, reader_sv->reader);
126              
127 54           resources = pdfmake_reader_page_resources(reader_sv->reader, rpage);
128 54 50         if (resources)
129 54           pdfmake_interp_set_resources(interp, resources);
130              
131 54           result = pdfmake_textract_new(arena);
132 54           pdfmake_textract_set_reader(result, reader_sv->reader);
133 54           opts = pdfmake_textract_default_options();
134 54           opts.include_invisible = include_invisible;
135              
136             /* Phase 12: resolve /StructTreeRoot from the catalog (if the
137             * PDF is tagged) so the extractor can look up structure roles
138             * from MCIDs the visitor collects. */
139 54 50         if (reader_sv->reader->catalog) {
140 54           uint32_t str_key = pdfmake_arena_intern_name(
141             arena, "StructTreeRoot", 14);
142 54           pdfmake_obj_t *str_root = pdfmake_dict_get(
143 54           reader_sv->reader->catalog, str_key);
144 54 100         if (str_root) {
145 1           pdfmake_textract_resolve_struct_tree(
146             result, str_root, rpage->page_dict);
147             }
148             }
149              
150 54           err = pdfmake_textract_run(interp, content_buf.data, content_buf.len, &opts, result);
151              
152 54           pdfmake_interp_free(interp);
153 54           pdfmake_buf_free(&content_buf);
154              
155 54 50         if (err != PDFMAKE_OK) {
156 0           pdfmake_textract_free(result);
157 0           croak("PDF::Make::Extract: extraction failed");
158             }
159              
160             /* Build Perl data structure: blocks -> lines -> words -> glyphs */
161 54           AV *blocks_av = newAV();
162 292 100         for (size_t b = 0; b < result->len; b++) {
163 238           pdfmake_text_block_t *block = &result->blocks[b];
164 238           HV *block_hv = newHV();
165 238           hv_stores(block_hv, "x0", newSVnv(block->x0));
166 238           hv_stores(block_hv, "y0", newSVnv(block->y0));
167 238           hv_stores(block_hv, "x1", newSVnv(block->x1));
168 238           hv_stores(block_hv, "y1", newSVnv(block->y1));
169              
170 238           AV *lines_av = newAV();
171 910 100         for (size_t l = 0; l < block->len; l++) {
172 672           pdfmake_text_line_t *line = &block->lines[l];
173 672           HV *line_hv = newHV();
174 672           hv_stores(line_hv, "x0", newSVnv(line->x0));
175 672           hv_stores(line_hv, "y0", newSVnv(line->y0));
176 672           hv_stores(line_hv, "x1", newSVnv(line->x1));
177 672           hv_stores(line_hv, "y1", newSVnv(line->y1));
178 672           hv_stores(line_hv, "baseline", newSVnv(line->baseline_y));
179              
180 672           AV *words_av = newAV();
181 2763 100         for (size_t w = 0; w < line->len; w++) {
182 2091           pdfmake_text_word_t *word = &line->words[w];
183 2091           HV *word_hv = newHV();
184 2091           hv_stores(word_hv, "x0", newSVnv(word->x0));
185 2091           hv_stores(word_hv, "y0", newSVnv(word->y0));
186 2091           hv_stores(word_hv, "x1", newSVnv(word->x1));
187 2091           hv_stores(word_hv, "y1", newSVnv(word->y1));
188              
189             /* Build word text from glyphs */
190             pdfmake_buf_t wbuf;
191 2091           pdfmake_buf_init(&wbuf);
192 2091           double word_font_size = 0;
193 14246 100         for (size_t g = 0; g < word->len; g++) {
194 12155           pdfmake_text_glyph_t *gl = &word->glyphs[g];
195 12155 100         if (gl->font_size > word_font_size)
196 2091           word_font_size = gl->font_size;
197             /* Encode Unicode codepoint as UTF-8 */
198 12155           uint32_t cp = gl->unicode;
199 12155 100         if (cp < 0x80) {
200 12110           pdfmake_buf_append_byte(&wbuf, (uint8_t)cp);
201 45 50         } else if (cp < 0x800) {
202 0           pdfmake_buf_append_byte(&wbuf, 0xC0 | (cp >> 6));
203 0           pdfmake_buf_append_byte(&wbuf, 0x80 | (cp & 0x3F));
204 45 50         } else if (cp < 0x10000) {
205 45           pdfmake_buf_append_byte(&wbuf, 0xE0 | (cp >> 12));
206 45           pdfmake_buf_append_byte(&wbuf, 0x80 | ((cp >> 6) & 0x3F));
207 45           pdfmake_buf_append_byte(&wbuf, 0x80 | (cp & 0x3F));
208             } else {
209 0           pdfmake_buf_append_byte(&wbuf, 0xF0 | (cp >> 18));
210 0           pdfmake_buf_append_byte(&wbuf, 0x80 | ((cp >> 12) & 0x3F));
211 0           pdfmake_buf_append_byte(&wbuf, 0x80 | ((cp >> 6) & 0x3F));
212 0           pdfmake_buf_append_byte(&wbuf, 0x80 | (cp & 0x3F));
213             }
214             }
215 2091           SV *text_sv = newSVpvn((const char *)wbuf.data, wbuf.len);
216 2091           SvUTF8_on(text_sv);
217 2091           hv_stores(word_hv, "text", text_sv);
218 2091           hv_stores(word_hv, "font_size", newSVnv(word_font_size));
219 2091           pdfmake_buf_free(&wbuf);
220              
221             /* Phase 12: expose marked-content tag for this word */
222 2091 100         if (word->mcid >= 0) {
223 5           hv_stores(word_hv, "mcid", newSViv(word->mcid));
224 5           uint32_t role_id = pdfmake_textract_role_for_mcid(
225             result, word->mcid);
226 5 50         if (role_id) {
227             pdfmake_obj_t role_obj;
228 5           role_obj.kind = PDFMAKE_NAME;
229 5           role_obj.as.name.id = role_id;
230             const char *role_name =
231 5           pdfmake_get_name_bytes(arena, &role_obj);
232 5 50         if (role_name) {
233 5           hv_stores(word_hv, "tag",
234             newSVpv(role_name, 0));
235             }
236             }
237             }
238              
239 2091           av_push(words_av, newRV_noinc((SV *)word_hv));
240             }
241 672           hv_stores(line_hv, "words", newRV_noinc((SV *)words_av));
242 672           av_push(lines_av, newRV_noinc((SV *)line_hv));
243             }
244 238           hv_stores(block_hv, "lines", newRV_noinc((SV *)lines_av));
245 238           av_push(blocks_av, newRV_noinc((SV *)block_hv));
246             }
247              
248 54           RETVAL = newRV_noinc((SV *)blocks_av);
249              
250 54           pdfmake_textract_free(result);
251             /* arena owned by reader; do not free here */
252             }
253             OUTPUT:
254             RETVAL
255              
256             SV *
257             _detect_tables(class, reader_sv, page_index)
258             char *class
259             pdfmake_reader_xs_t *reader_sv
260             UV page_index
261             PREINIT:
262             pdfmake_err_t err;
263             pdfmake_arena_t *arena;
264             pdfmake_interp_t *interp;
265             pdfmake_textract_result_t *result;
266             pdfmake_textract_options_t opts;
267             pdfmake_reader_page_t *rpage;
268             pdfmake_buf_t content_buf;
269             pdfmake_obj_t *resources;
270             pdfmake_textract_table_list_t *tlist;
271             CODE:
272             PERL_UNUSED_VAR(class);
273 2           pdfmake_buf_init(&content_buf);
274              
275 2 50         if (page_index >= pdfmake_reader_page_count(reader_sv->reader))
276 0           croak("PDF::Make::Extract: page index %lu out of range",
277             (unsigned long)page_index);
278              
279 2           rpage = pdfmake_reader_page_at(reader_sv->reader, page_index);
280 2           err = pdfmake_reader_page_content_bytes(reader_sv->reader, rpage, &content_buf);
281 2 50         if (err != PDFMAKE_OK || content_buf.len == 0) {
    50          
282 0           pdfmake_buf_free(&content_buf);
283 0           RETVAL = newRV_noinc((SV *)newAV());
284             } else {
285 2           arena = reader_sv->reader->parser->doc->arena;
286 2           interp = pdfmake_interp_new(arena);
287 2 50         if (!interp) {
288 0           pdfmake_buf_free(&content_buf);
289 0           croak("PDF::Make::Extract: interp alloc failed");
290             }
291 2           pdfmake_interp_set_reader(interp, reader_sv->reader);
292 2           resources = pdfmake_reader_page_resources(reader_sv->reader, rpage);
293 2 50         if (resources) pdfmake_interp_set_resources(interp, resources);
294              
295 2           result = pdfmake_textract_new(arena);
296 2           pdfmake_textract_set_reader(result, reader_sv->reader);
297 2           opts = pdfmake_textract_default_options();
298 2           err = pdfmake_textract_run(interp, content_buf.data, content_buf.len, &opts, result);
299 2           pdfmake_interp_free(interp);
300 2           pdfmake_buf_free(&content_buf);
301 2 50         if (err != PDFMAKE_OK) {
302 0           pdfmake_textract_free(result);
303 0           croak("PDF::Make::Extract: extraction failed");
304             }
305              
306 2           tlist = pdfmake_textract_table_list_new(arena);
307 2           pdfmake_textract_detect_tables(result, NULL, tlist);
308              
309 2           AV *av = newAV();
310 3 100         for (size_t ti = 0; ti < tlist->len; ti++) {
311 1           pdfmake_textract_table_t *t = &tlist->items[ti];
312 1           HV *hv = newHV();
313 1           hv_stores(hv, "x0", newSVnv(t->x0));
314 1           hv_stores(hv, "y0", newSVnv(t->y0));
315 1           hv_stores(hv, "x1", newSVnv(t->x1));
316 1           hv_stores(hv, "y1", newSVnv(t->y1));
317 1           hv_stores(hv, "rows", newSVuv((UV)t->rows));
318 1           hv_stores(hv, "cols", newSVuv((UV)t->cols));
319              
320 1           AV *rows_av = newAV();
321 5 100         for (size_t r = 0; r < t->rows; r++) {
322 4           AV *row_av = newAV();
323 16 100         for (size_t c = 0; c < t->cols; c++) {
324 12           size_t idx = r * t->cols + c;
325 12 50         const char *txt = t->cells[idx] ? t->cells[idx] : "";
326 12           SV *sv = newSVpv(txt, 0);
327 12           SvUTF8_on(sv);
328 12           av_push(row_av, sv);
329             }
330 4           av_push(rows_av, newRV_noinc((SV *)row_av));
331             }
332 1           hv_stores(hv, "cells", newRV_noinc((SV *)rows_av));
333              
334 1           av_push(av, newRV_noinc((SV *)hv));
335             }
336              
337 2           pdfmake_textract_table_list_free(tlist);
338 2           pdfmake_textract_free(result);
339 2           RETVAL = newRV_noinc((SV *)av);
340             }
341             OUTPUT:
342             RETVAL
343              
344             SV *
345             _extract_annotations(class, reader_sv)
346             char *class
347             pdfmake_reader_xs_t *reader_sv
348             PREINIT:
349             pdfmake_annot_text_list_t *list;
350             pdfmake_arena_t *arena;
351             CODE:
352             PERL_UNUSED_VAR(class);
353 2           arena = reader_sv->reader->parser->doc->arena;
354 2           list = pdfmake_annot_text_list_new(arena);
355 2 50         if (!list) croak("PDF::Make::Extract: annot list alloc failed");
356              
357 2 50         if (pdfmake_textract_annotations(reader_sv->reader, list) != PDFMAKE_OK) {
358 0           pdfmake_annot_text_list_free(list);
359 0           croak("PDF::Make::Extract: annotation extraction failed");
360             }
361              
362 2           AV *av = newAV();
363 5 100         for (size_t i = 0; i < list->len; i++) {
364 3           pdfmake_annot_text_t *r = &list->items[i];
365 3           HV *hv = newHV();
366 3 50         if (r->kind) hv_stores(hv, "kind", newSVpv(r->kind, 0));
367 3 50         if (r->page_index != (size_t)-1)
368 3           hv_stores(hv, "page", newSVuv((UV)r->page_index));
369 3           AV *rect_av = newAV();
370 15 100         for (int k = 0; k < 4; k++)
371 12           av_push(rect_av, newSVnv(r->rect[k]));
372 3           hv_stores(hv, "rect", newRV_noinc((SV *)rect_av));
373              
374 3 50         SV *tsv = newSVpv(r->text ? r->text : "", 0);
375 3           SvUTF8_on(tsv);
376 3           hv_stores(hv, "text", tsv);
377              
378 3 100         if (r->author) {
379 2           SV *sv = newSVpv(r->author, 0);
380 2           SvUTF8_on(sv);
381 2           hv_stores(hv, "author", sv);
382             }
383 3 100         if (r->subject) {
384 2           SV *sv = newSVpv(r->subject, 0);
385 2           SvUTF8_on(sv);
386 2           hv_stores(hv, "subject", sv);
387             }
388 3 100         if (r->field_name) {
389 1           SV *sv = newSVpv(r->field_name, 0);
390 1           SvUTF8_on(sv);
391 1           hv_stores(hv, "field_name", sv);
392             }
393              
394 3           av_push(av, newRV_noinc((SV *)hv));
395             }
396              
397 2           pdfmake_annot_text_list_free(list);
398 2           RETVAL = newRV_noinc((SV *)av);
399             OUTPUT:
400             RETVAL