File Coverage

src/pdfmake_textract.c
Criterion Covered Total %
statement 989 1135 87.1
branch 669 1102 60.7
condition n/a
subroutine n/a
pod n/a
total 1658 2237 74.1


line stmt bran cond sub pod time code
1             /*
2             * pdfmake_textract.c — Text extraction with coordinates.
3             *
4             * Pipeline: interpreter → visitor → raw glyphs → words → lines → blocks
5             *
6             * §9.4 Text objects, §9.10 Extraction of text content
7             */
8              
9             #include "pdfmake_textract.h"
10             #include "pdfmake_font.h"
11             #include "pdfmake_interpreter.h"
12             #include "pdfmake_cmap.h"
13             #include "pdfmake_reader.h"
14             #include "pdfmake_parser.h"
15             #include "pdfmake_buf.h"
16             #include
17             #include
18             #include
19             #include
20              
21             /*============================================================================
22             * WinAnsi encoding → Unicode mapping (§D.1)
23             *==========================================================================*/
24              
25             /* Bytes 0x00–0x7F map to U+0000–U+007F (ASCII).
26             * Bytes 0x80–0x9F have special mappings.
27             * Bytes 0xA0–0xFF map to U+00A0–U+00FF (Latin-1 supplement).
28             */
29             static const uint32_t winansi_0x80[32] = {
30             0x20AC, /* 0x80: Euro sign */
31             0xFFFD, /* 0x81: undefined */
32             0x201A, /* 0x82: single low-9 quotation mark */
33             0x0192, /* 0x83: latin small f with hook */
34             0x201E, /* 0x84: double low-9 quotation mark */
35             0x2026, /* 0x85: horizontal ellipsis */
36             0x2020, /* 0x86: dagger */
37             0x2021, /* 0x87: double dagger */
38             0x02C6, /* 0x88: modifier letter circumflex accent */
39             0x2030, /* 0x89: per mille sign */
40             0x0160, /* 0x8A: latin capital S with caron */
41             0x2039, /* 0x8B: single left-pointing angle quotation */
42             0x0152, /* 0x8C: latin capital ligature OE */
43             0xFFFD, /* 0x8D: undefined */
44             0x017D, /* 0x8E: latin capital Z with caron */
45             0xFFFD, /* 0x8F: undefined */
46             0xFFFD, /* 0x90: undefined */
47             0x2018, /* 0x91: left single quotation mark */
48             0x2019, /* 0x92: right single quotation mark */
49             0x201C, /* 0x93: left double quotation mark */
50             0x201D, /* 0x94: right double quotation mark */
51             0x2022, /* 0x95: bullet */
52             0x2013, /* 0x96: en dash */
53             0x2014, /* 0x97: em dash */
54             0x02DC, /* 0x98: small tilde */
55             0x2122, /* 0x99: trade mark sign */
56             0x0161, /* 0x9A: latin small s with caron */
57             0x203A, /* 0x9B: single right-pointing angle quotation */
58             0x0153, /* 0x9C: latin small ligature oe */
59             0xFFFD, /* 0x9D: undefined */
60             0x017E, /* 0x9E: latin small z with caron */
61             0x0178, /* 0x9F: latin capital Y with diaeresis */
62             };
63              
64 0           uint32_t pdfmake_winansi_to_unicode(uint8_t byte) {
65 0 0         if (byte < 0x80) return (uint32_t)byte;
66 0 0         if (byte < 0xA0) return winansi_0x80[byte - 0x80];
67 0           return (uint32_t)byte; /* 0xA0–0xFF: same as Unicode */
68             }
69              
70             /*============================================================================
71             * Result management
72             *==========================================================================*/
73              
74 60           pdfmake_textract_options_t pdfmake_textract_default_options(void) {
75             pdfmake_textract_options_t opts;
76 60           opts.word_gap_factor = 0.3;
77 60           opts.line_tolerance = 0.5;
78 60           opts.block_leading = 1.5;
79 60           opts.include_invisible = 1;
80 60           return opts;
81             }
82              
83 60           pdfmake_textract_result_t *pdfmake_textract_new(pdfmake_arena_t *arena) {
84 60           pdfmake_textract_result_t *r = calloc(1, sizeof(*r));
85 60 50         if (r) {
86 60           r->arena = arena;
87 60           r->include_invisible = 1; /* default: include OCR text */
88 60           r->current_mcid = -1; /* Phase 12: no active MCID */
89             }
90 60           return r;
91             }
92              
93 707           static void free_words(pdfmake_text_word_t *words, size_t len) {
94             size_t i;
95 3034 100         for (i = 0; i < len; i++) {
96 2327           free(words[i].glyphs);
97             }
98 707           free(words);
99 707           }
100              
101 266           static void free_lines(pdfmake_text_line_t *lines, size_t len) {
102             size_t i;
103 973 100         for (i = 0; i < len; i++) {
104 707           free_words(lines[i].words, lines[i].len);
105             }
106 266           free(lines);
107 266           }
108              
109 60           void pdfmake_textract_free(pdfmake_textract_result_t *result) {
110             size_t i;
111 60 50         if (!result) return;
112 326 100         for (i = 0; i < result->len; i++) {
113 266           free_lines(result->blocks[i].lines, result->blocks[i].len);
114             }
115 60           free(result->blocks);
116 60           free(result->raw_glyphs);
117 60           free(result->font_cache);
118 60           free(result->struct_map);
119 60           free(result);
120             }
121              
122 60           void pdfmake_textract_set_reader(pdfmake_textract_result_t *result,
123             struct pdfmake_reader *reader) {
124 60 50         if (!result) return;
125 60           result->reader = reader;
126             }
127              
128             /*============================================================================
129             * Font resolution cache
130             *
131             * Lazily resolves per-font metadata (ToUnicode CMap, Type0/CID flag, Std14 ID)
132             * on first use. Subsequent glyphs from the same font dict hit the cache.
133             *==========================================================================*/
134              
135 10451           static pdfmake_resolved_font_t *font_cache_find(
136             pdfmake_textract_result_t *r, pdfmake_obj_t *font_dict)
137             {
138             size_t i;
139 19658 100         for (i = 0; i < r->font_cache_len; i++) {
140 19529 100         if (r->font_cache[i].font_dict == font_dict) return &r->font_cache[i];
141             }
142 129           return NULL;
143             }
144              
145 129           static pdfmake_resolved_font_t *font_cache_add(
146             pdfmake_textract_result_t *r, pdfmake_obj_t *font_dict)
147             {
148             pdfmake_resolved_font_t *slot;
149 129 100         if (r->font_cache_len >= r->font_cache_cap) {
150 74 100         size_t new_cap = r->font_cache_cap == 0 ? 4 : r->font_cache_cap * 2;
151 74           pdfmake_resolved_font_t *n = realloc(
152 74           r->font_cache, new_cap * sizeof(*n));
153 74 50         if (!n) return NULL;
154 74           r->font_cache = n;
155 74           r->font_cache_cap = new_cap;
156             }
157 129           slot = &r->font_cache[r->font_cache_len++];
158 129           memset(slot, 0, sizeof(*slot));
159 129           slot->font_dict = font_dict;
160 129           slot->std14_id = -1;
161 129           return slot;
162             }
163              
164             /* Resolve the ToUnicode CMap for a font dict. Returns NULL if not present
165             * or if resolution fails. Caches the result (including NULL) per-font. */
166 3483           static pdfmake_cmap_t *resolve_to_unicode(pdfmake_textract_result_t *r,
167             pdfmake_resolved_font_t *rf)
168             {
169             uint32_t key;
170             pdfmake_obj_t *tu;
171             pdfmake_buf_t buf;
172             pdfmake_err_t err;
173 3483 100         if (rf->to_unicode_tried) return rf->to_unicode;
174 129           rf->to_unicode_tried = 1;
175              
176 129 50         if (!r->reader || !rf->font_dict || !r->arena) return NULL;
    50          
    50          
177              
178 129           key = pdfmake_arena_intern_name(r->arena, "ToUnicode", 9);
179 129           tu = pdfmake_dict_get(rf->font_dict, key);
180 129 100         if (!tu || tu->kind != PDFMAKE_REF) return NULL;
    50          
181              
182 19           pdfmake_buf_init(&buf);
183 19           err = pdfmake_reader_resolve_stream(
184 19           r->reader, tu->as.ref.num, tu->as.ref.gen, &buf);
185 19 50         if (err != PDFMAKE_OK || buf.len == 0) {
    50          
186 0           pdfmake_buf_free(&buf);
187 0           return NULL;
188             }
189              
190 19           rf->to_unicode = pdfmake_cmap_parse(r->arena, buf.data, buf.len);
191 19           pdfmake_buf_free(&buf);
192 19           return rf->to_unicode;
193             }
194              
195             /* Resolve /Widths or /W for the font, plus font descriptor metrics. */
196 46           static void resolve_widths(pdfmake_textract_result_t *r,
197             pdfmake_resolved_font_t *rf)
198             {
199 46 50         if (rf->widths_resolved) return;
200 46           rf->widths_resolved = 1;
201              
202 46 50         if (!rf->font_dict || !r->arena) {
    50          
203 0           pdfmake_font_widths_init(&rf->widths);
204 0           return;
205             }
206              
207 46 100         if (rf->is_cid) {
208 1           pdfmake_font_widths_from_cid(r->arena, rf->font_dict, &rf->widths);
209             } else {
210 45           pdfmake_font_widths_from_simple(r->arena, rf->font_dict, &rf->widths);
211             }
212              
213             /* Phase 6: overlay with TTF metrics from /FontFile2 if present.
214             * For simple fonts this requires the resolved /Encoding first; callers
215             * arrange the sequence in resolve_font(). */
216 46 50         if (r->reader) {
217 46           const uint32_t *byte_to_uni = NULL;
218 46 100         if (!rf->is_cid && rf->encoding_resolved) {
    50          
219 45           byte_to_uni = rf->encoding.map;
220             }
221 46           pdfmake_font_widths_enhance_with_ttf(
222             r->arena,
223             (struct pdfmake_reader *)r->reader,
224             rf->font_dict,
225             rf->is_cid,
226             byte_to_uni,
227             &rf->widths);
228             }
229             }
230              
231             /* Resolve the /Encoding entry for a simple font. Populates rf->encoding.
232             * Std14 fonts without an explicit /Encoding default to WinAnsi in practice;
233             * other Type1 fonts default to StandardEncoding. */
234 128           static void resolve_encoding(pdfmake_textract_result_t *r,
235             pdfmake_resolved_font_t *rf)
236             {
237             uint32_t enc_key;
238             pdfmake_obj_t *enc_obj;
239             pdfmake_obj_t *resolved;
240             const pdfmake_std14_data_t *d;
241 128 50         if (rf->encoding_resolved) return;
242 128           rf->encoding_resolved = 1;
243              
244 128 50         if (!rf->font_dict || !r->arena) {
    50          
245 0           pdfmake_font_encoding_init_winansi(&rf->encoding);
246 0           return;
247             }
248              
249 128           enc_key = pdfmake_arena_intern_name(r->arena, "Encoding", 8);
250 128           enc_obj = pdfmake_dict_get(rf->font_dict, enc_key);
251              
252             /* Resolve indirect reference if needed */
253 128 100         if (enc_obj && enc_obj->kind == PDFMAKE_REF && r->reader) {
    100          
    50          
254             /* We need to resolve the ref through the parser; expose a tiny helper */
255             /* For now, the parser returns the concrete obj via the reader's parser */
256 14           resolved = pdfmake_parser_resolve(
257 14           ((pdfmake_reader_t *)r->reader)->parser, enc_obj->as.ref);
258 14 50         if (resolved) enc_obj = resolved;
259             }
260              
261 128 100         if (enc_obj) {
262 35           pdfmake_font_encoding_from_dict(r->arena, enc_obj, &rf->encoding);
263             } else {
264             /* No /Encoding: pick a sane default based on Std14 kind */
265 93 100         if (rf->std14_id >= 0) {
266             /* Symbol and ZapfDingbats have their own encoding */
267 48           d = pdfmake_std14_get((pdfmake_std14_id_t)rf->std14_id);
268 48 50         if (d && d->name) {
    50          
269 96 50         if (strcmp(d->name, "Symbol") == 0)
270 0           pdfmake_font_encoding_init_symbol(&rf->encoding);
271 48 50         else if (strcmp(d->name, "ZapfDingbats") == 0)
272 0           pdfmake_font_encoding_init_zapfdingbats(&rf->encoding);
273             else
274 48           pdfmake_font_encoding_init_standard(&rf->encoding);
275             } else {
276 0           pdfmake_font_encoding_init_standard(&rf->encoding);
277             }
278             } else {
279             /* Non-Std14 with no /Encoding: StandardEncoding per §9.6.5 */
280 45           pdfmake_font_encoding_init_standard(&rf->encoding);
281             }
282             }
283             }
284              
285             /* If `obj` is an indirect reference, resolve it through the reader's
286             * parser; otherwise return it unchanged. Returns NULL only when an
287             * attempted resolve failed — direct NULL/DICT/etc. inputs pass through. */
288             static PDFMAKE_INLINE pdfmake_obj_t *
289 10454           textract_follow_ref(pdfmake_textract_result_t *r, pdfmake_obj_t *obj)
290             {
291 10454 50         if (obj && obj->kind == PDFMAKE_REF && r->reader) {
    100          
    50          
292 10452           pdfmake_reader_t *rd = (pdfmake_reader_t *)r->reader;
293 10452 50         if (rd->parser) return pdfmake_parser_resolve(rd->parser, obj->as.ref);
294             }
295 2           return obj;
296             }
297              
298             /* For Type0/CID fonts, derive writing mode and default vertical advance:
299             * - /Encoding: name ending in "-V" → vertical; a CMap stream with
300             * /WMode 1 overrides (§9.7.5.2).
301             * - When vertical, /DescendantFonts[0]/DW2 = [v_origin_y v_advance]
302             * replaces the -1000 spec default for default_v_advance. */
303             static void
304 1           resolve_cid_font_modes(pdfmake_textract_result_t *r,
305             pdfmake_obj_t *font_dict,
306             pdfmake_resolved_font_t *rf)
307             {
308             uint32_t enc_key;
309             pdfmake_obj_t *enc;
310             const char *nm;
311             size_t ln;
312             pdfmake_dict_t *sd;
313             pdfmake_obj_t sd_obj;
314             uint32_t wm_key;
315             pdfmake_obj_t *wm;
316             uint32_t df_key;
317             pdfmake_obj_t *df;
318             pdfmake_obj_t *desc;
319             uint32_t dw2_key;
320             pdfmake_obj_t *dw2;
321             pdfmake_obj_t *a;
322              
323 1           enc_key = pdfmake_arena_intern_name(r->arena, "Encoding", 8);
324 1           enc = textract_follow_ref(r, pdfmake_dict_get(font_dict, enc_key));
325              
326 1 50         if (enc && enc->kind == PDFMAKE_NAME) {
    50          
327 1           nm = pdfmake_get_name_bytes(r->arena, enc);
328 1 50         ln = nm ? strlen(nm) : 0;
329 1 50         if (ln >= 2 && nm[ln - 2] == '-' &&
    50          
330 1 50         (nm[ln - 1] == 'V' || nm[ln - 1] == 'v')) {
    0          
331 1           rf->wmode = 1;
332             }
333 0 0         } else if (enc && enc->kind == PDFMAKE_STREAM) {
    0          
334 0           sd = pdfmake_stream_dict(enc);
335 0 0         if (sd) {
336 0           sd_obj.kind = PDFMAKE_DICT;
337 0           sd_obj.as.dict = sd;
338 0           wm_key = pdfmake_arena_intern_name(r->arena, "WMode", 5);
339 0           wm = pdfmake_dict_get(&sd_obj, wm_key);
340 0 0         if (wm && wm->kind == PDFMAKE_INT && wm->as.i == 1) rf->wmode = 1;
    0          
    0          
341             }
342             }
343 1 50         if (!rf->wmode) return;
344              
345 1           df_key = pdfmake_arena_intern_name(r->arena, "DescendantFonts", 15);
346 1           df = textract_follow_ref(r, pdfmake_dict_get(font_dict, df_key));
347 1 50         if (!df || df->kind != PDFMAKE_ARRAY || pdfmake_array_len(df) == 0) return;
    50          
    50          
348              
349 1           desc = textract_follow_ref(r, pdfmake_array_get(df, 0));
350 1 50         if (!desc || desc->kind != PDFMAKE_DICT) return;
    50          
351              
352 1           dw2_key = pdfmake_arena_intern_name(r->arena, "DW2", 3);
353 1           dw2 = pdfmake_dict_get(desc, dw2_key);
354 1 50         if (dw2 && dw2->kind == PDFMAKE_ARRAY && pdfmake_array_len(dw2) >= 2) {
    50          
    50          
355 1           a = pdfmake_array_get(dw2, 1);
356 1 50         if (a) rf->default_v_advance = (int16_t)pdfmake_get_number(a);
357             }
358             }
359              
360             /* Resolve the font dict into a cached resolved_font. NULL if dict is NULL. */
361 10451           static pdfmake_resolved_font_t *resolve_font(
362             pdfmake_textract_result_t *r, pdfmake_obj_t *font_dict)
363             {
364             pdfmake_resolved_font_t *rf;
365             uint32_t bf_key;
366             pdfmake_obj_t *bf;
367             const char *name;
368             uint32_t sub_key;
369             pdfmake_obj_t *sub;
370             const char *st;
371 10451 50         if (!font_dict || !r->arena) return NULL;
    50          
372              
373             /* Follow indirect references — resources often hold refs, not inline dicts */
374 10451           font_dict = textract_follow_ref(r, font_dict);
375 10451 50         if (!font_dict || font_dict->kind != PDFMAKE_DICT) return NULL;
    50          
376              
377 10451           rf = font_cache_find(r, font_dict);
378 10451 100         if (rf) return rf;
379 129           rf = font_cache_add(r, font_dict);
380 129 50         if (!rf) return NULL;
381              
382             /* Std14 lookup */
383 129           bf_key = pdfmake_arena_intern_name(r->arena, "BaseFont", 8);
384 129           bf = pdfmake_dict_get(font_dict, bf_key);
385 129 50         if (bf && bf->kind == PDFMAKE_NAME) {
    50          
386 129           name = pdfmake_get_name_bytes(r->arena, bf);
387 129 50         if (name) rf->std14_id = pdfmake_std14_lookup(name);
388             }
389              
390             /* Type0/CID detection */
391 129           sub_key = pdfmake_arena_intern_name(r->arena, "Subtype", 7);
392 129           sub = pdfmake_dict_get(font_dict, sub_key);
393 129 50         if (sub && sub->kind == PDFMAKE_NAME) {
    50          
394 129           st = pdfmake_get_name_bytes(r->arena, sub);
395 129 50         if (st && strcmp(st, "Type0") == 0) rf->is_cid = 1;
    100          
396             }
397              
398             /* Phase 14: writing mode + default vertical advance (Type0 only). */
399 129           rf->wmode = 0;
400 129           rf->default_v_advance = -1000; /* spec default for vertical */
401 129 100         if (rf->is_cid) resolve_cid_font_modes(r, font_dict, rf);
402              
403             /* Resolve /Encoding for simple fonts only; CID fonts use ToUnicode */
404 129 100         if (!rf->is_cid) resolve_encoding(r, rf);
405              
406             /* Resolve widths + descriptor metrics for all non-Std14 fonts */
407 129 100         if (rf->std14_id < 0) resolve_widths(r, rf);
408              
409 129           return rf;
410             }
411              
412             /*============================================================================
413             * Raw glyph collection
414             *==========================================================================*/
415              
416 14756           static int result_push_glyph(pdfmake_textract_result_t *r,
417             const pdfmake_text_glyph_t *g) {
418 14756 100         if (r->raw_len >= r->raw_cap) {
419 136 100         size_t new_cap = r->raw_cap == 0 ? 64 : r->raw_cap * 2;
420 136           pdfmake_text_glyph_t *new_arr = realloc(r->raw_glyphs,
421             new_cap * sizeof(pdfmake_text_glyph_t));
422 136 50         if (!new_arr) return 0;
423 136           r->raw_glyphs = new_arr;
424 136           r->raw_cap = new_cap;
425             }
426 14756           r->raw_glyphs[r->raw_len++] = *g;
427 14756           return 1;
428             }
429              
430             /*============================================================================
431             * Visitor: on_text_show callback
432             *
433             * Called for each Tj/TJ/' /" operator. Decodes bytes, computes glyph
434             * positions in user space, emits raw glyphs.
435             *==========================================================================*/
436              
437             /* Legacy: resolve the Standard 14 font ID from the graphics state font dict.
438             * Superseded by resolve_font() + font cache. Kept for potential reuse. */
439             __attribute__((unused))
440 0           static int resolve_std14(const pdfmake_gstate_t *gs, pdfmake_arena_t *arena) {
441             uint32_t basefont_key;
442             pdfmake_obj_t *basefont;
443             const char *name;
444 0 0         if (!gs->font || !arena) return -1;
    0          
445 0 0         if (gs->font->kind != PDFMAKE_DICT) return -1;
446              
447             /* Look up /BaseFont in the font dictionary */
448 0           basefont_key = pdfmake_arena_intern_name(arena, "BaseFont", 8);
449 0           basefont = pdfmake_dict_get(gs->font, basefont_key);
450 0 0         if (!basefont || basefont->kind != PDFMAKE_NAME) return -1;
    0          
451              
452 0           name = pdfmake_get_name_bytes(arena, basefont);
453 0 0         if (!name) return -1;
454              
455 0           return pdfmake_std14_lookup(name);
456             }
457              
458             /* Horizontal + vertical advance for a glyph, plus a reliability flag that
459             * feeds the downstream word-boundary heuristic. */
460             typedef struct {
461             double horizontal; /* x-advance in text space (Tj/TJ direction) */
462             double vertical; /* y-advance magnitude for vertical writing */
463             int reliable; /* 1 iff horizontal came from real font metrics */
464             } textract_advance_t;
465              
466             /* Map one character code to its Unicode codepoint(s). Returns the count
467             * (always ≥ 1). Ligature-style ToUnicode mappings may yield several
468             * codepoints (e.g. "fi" → U+0066 U+0069); uni_list must hold at least
469             * PDFMAKE_CMAP_MAX_UNI entries. On failure we emit U+FFFD. */
470             static PDFMAKE_INLINE size_t
471 14754           textract_code_to_unicode(const pdfmake_resolved_font_t *rf,
472             pdfmake_cmap_t *to_uni,
473             int is_cid, uint32_t code,
474             uint32_t *uni_list)
475             {
476 14754 100         if (to_uni) {
477 103           size_t cnt = 0;
478 103 50         if (pdfmake_cmap_lookup(to_uni, code, uni_list, &cnt) && cnt > 0) {
    50          
479 103           return cnt;
480             }
481 0           uni_list[0] = 0xFFFD;
482 0           return 1;
483             }
484 14651 50         if (!is_cid && rf && rf->encoding_resolved) {
    50          
    50          
485 14651           uint32_t cp = rf->encoding.map[code & 0xFF];
486 14651 50         uni_list[0] = cp ? cp : 0xFFFD;
487 14651           return 1;
488             }
489 0 0         if (!is_cid) {
490 0           uni_list[0] = pdfmake_winansi_to_unicode((uint8_t)code);
491 0           return 1;
492             }
493             /* CID fallback: use the raw code. */
494 0           uni_list[0] = code;
495 0           return 1;
496             }
497              
498             /* Compute the glyph advance (both horizontal and vertical) in text-space
499             * units, trying /Widths first, then Std14 AFM, then a 0.5-em / 0.25-em
500             * placeholder. Vertical advance prefers the font's default_v_advance
501             * (CIDFont DW2) when available. */
502             static PDFMAKE_INLINE textract_advance_t
503 14754           textract_glyph_advance(const pdfmake_resolved_font_t *rf,
504             int std14_id, int is_cid,
505             uint32_t code, uint32_t unicode,
506             double font_size)
507             {
508 14754           textract_advance_t adv = { 0.0, 0.0, 0 };
509              
510 14754           int16_t w1000 = 0;
511 14754 50         if (rf && rf->widths_resolved) {
    100          
512 6349           w1000 = pdfmake_font_widths_lookup(&rf->widths, code);
513             }
514 14754 100         if (w1000 > 0) {
515 6349           adv.horizontal = (double)w1000 / 1000.0 * font_size;
516 6349           adv.reliable = 1;
517 16810 50         } else if (std14_id >= 0 && !is_cid) {
    50          
518 8405           int w = pdfmake_std14_width((pdfmake_std14_id_t)std14_id, unicode);
519 8405 50         if (w > 0) {
520 8405           adv.horizontal = (double)w / 1000.0 * font_size;
521 8405           adv.reliable = 1;
522             } else {
523 0           adv.horizontal = 0.25 * font_size;
524             }
525             } else {
526 0           adv.horizontal = 0.5 * font_size;
527             }
528              
529             /* Default vertical advance falls back to horizontal magnitude. CJK
530             * fonts with DW2/W2 override this via rf->default_v_advance (negative
531             * because vertical flow is downward in text space). */
532 14754           adv.vertical = adv.horizontal;
533 14754 50         if (rf) {
534 14754 50         double mag = (double)(rf->default_v_advance < 0
535 14754           ? -rf->default_v_advance : rf->default_v_advance);
536 14754 50         if (mag > 0) adv.vertical = mag / 1000.0 * font_size;
537             }
538 14754           return adv;
539             }
540              
541             /* Transform a text-space point through the text matrix and CTM to final
542             * user/device coordinates. */
543             static PDFMAKE_INLINE void
544 14754           textract_text_to_device(const double *tm, const double *ctm,
545             double gx, double gy, double *fx, double *fy)
546             {
547 14754           double ux = tm[0] * gx + tm[2] * gy + tm[4];
548 14754           double uy = tm[1] * gx + tm[3] * gy + tm[5];
549 14754           *fx = ctm[0] * ux + ctm[2] * uy + ctm[4];
550 14754           *fy = ctm[1] * ux + ctm[3] * uy + ctm[5];
551 14754           }
552              
553 3484           static void textract_on_text_show(void *ctx,
554             const pdfmake_gstate_t *gs,
555             const uint8_t *bytes,
556             size_t len)
557             {
558 3484           pdfmake_textract_result_t *result = (pdfmake_textract_result_t *)ctx;
559             double font_size;
560             double char_space;
561             double word_space;
562             double h_scale;
563             double rise;
564             pdfmake_resolved_font_t *rf;
565             int std14_id;
566             int is_cid;
567             int is_vertical;
568             pdfmake_cmap_t *to_uni;
569             double ascent_ratio;
570             double descent_ratio;
571             const pdfmake_std14_data_t *data;
572             double sx;
573             double sy;
574             double effective_size;
575             double ascent;
576             double descent;
577             double accum_x;
578             size_t pos;
579             uint32_t code;
580             size_t code_width;
581             uint32_t uni_list[PDFMAKE_CMAP_MAX_UNI];
582             size_t uni_list_n;
583             uint32_t unicode;
584             textract_advance_t glyph_adv;
585             double gx;
586             double gy;
587             double fx;
588             double fy;
589             double adv;
590             double sub_adv;
591             pdfmake_text_glyph_t g;
592             double ws;
593             size_t k;
594 3485 50         if (!gs || !bytes || len == 0) return;
    50          
    50          
595              
596             /* Phase 11: skip invisible text (Tr=3) when the caller opted out.
597             * The interpreter still advances text_matrix via get_string_advance,
598             * so positioning of subsequent visible glyphs stays correct. */
599 3484 100         if (!result->include_invisible &&
600 3 100         gs->render_mode == PDFMAKE_RENDER_INVISIBLE) {
601 1           return;
602             }
603              
604 3483           font_size = gs->font_size;
605 3483           char_space = gs->char_space;
606 3483           word_space = gs->word_space;
607 3483           h_scale = gs->h_scale / 100.0; /* Tz is percentage */
608 3483           rise = gs->rise;
609              
610             /* Resolve the font (cached per-font dict) */
611 3483           rf = resolve_font(result, gs->font);
612 3483 50         std14_id = rf ? rf->std14_id : -1;
613 3483 50         is_cid = rf ? rf->is_cid : 0;
614 3483 50         is_vertical = rf ? rf->wmode : 0; /* Phase 14 */
615 3483 50         to_uni = rf ? resolve_to_unicode(result, rf) : NULL;
616              
617             /* Font metrics for ascent/descent.
618             * Priority: Std14 AFM data → /FontDescriptor → 0.8/-0.2 fallback. */
619 3483           ascent_ratio = 0.8;
620 3483           descent_ratio = -0.2;
621 3483 100         if (std14_id >= 0) {
622 935           data = pdfmake_std14_get((pdfmake_std14_id_t)std14_id);
623 935 50         if (data) {
624 935           ascent_ratio = data->metrics.ascent / 1000.0;
625 935           descent_ratio = data->metrics.descent / 1000.0;
626             }
627 2548 50         } else if (rf && rf->widths_resolved) {
    50          
628 2548 50         if (rf->widths.ascent) ascent_ratio = rf->widths.ascent / 1000.0;
629 2548 50         if (rf->widths.descent) descent_ratio = rf->widths.descent / 1000.0;
630             }
631             /* Effective font sizes in user space (account for text matrix scaling).
632             * sx scales horizontally (advances); sy scales vertically (ascent/descent). */
633 3483           sx = sqrt(gs->text_matrix[0] * gs->text_matrix[0] +
634 3483           gs->text_matrix[1] * gs->text_matrix[1]);
635 3483           sy = sqrt(gs->text_matrix[2] * gs->text_matrix[2] +
636 3483           gs->text_matrix[3] * gs->text_matrix[3]);
637 3483 50         if (sy == 0) sy = sx;
638 3483           effective_size = font_size * sx;
639 3483           ascent = ascent_ratio * font_size * sy;
640 3483           descent = descent_ratio * font_size * sy;
641              
642             /* Accumulated displacement through the string. For horizontal text
643             * (WMode 0) this is the x-offset in text space; for vertical text
644             * (WMode 1) it's the y-offset (negative because glyphs flow downward). */
645 3483           accum_x = 0.0; /* reused for both axes, name kept for symmetry */
646              
647 3483           pos = 0;
648 18237 100         while (pos < len) {
649             /* Extract the character code (1 or 2 bytes depending on font type).
650             * CID fonts are 2-byte by default; simple fonts are 1-byte. */
651 14754 100         if (is_cid && pos + 1 < len) {
    50          
652 3           code = ((uint32_t)bytes[pos] << 8) | bytes[pos + 1];
653 3           code_width = 2;
654             } else {
655 14751           code = bytes[pos];
656 14751           code_width = 1;
657             }
658              
659             /* Phase 9: ligature-style mappings may yield multiple codepoints
660             * for a single glyph; emit one sub-glyph per codepoint, sharing
661             * the glyph's bounding box (advance split uniformly). */
662 14754           uni_list_n = textract_code_to_unicode(rf, to_uni, is_cid, code, uni_list);
663 14754           unicode = uni_list[0];
664              
665 14754           glyph_adv = textract_glyph_advance(
666             rf, std14_id, is_cid, code, unicode, font_size);
667              
668 14754           pos += code_width;
669              
670             /* Glyph origin in text space — for horizontal writing the pen
671             * travels along +x; for vertical writing it travels along -y.
672             * `rise` still adjusts the cross axis. */
673 14754 100         gx = is_vertical ? rise : accum_x;
674 14754 100         gy = is_vertical ? accum_x : rise;
675              
676 14754           textract_text_to_device(gs->text_matrix, gs->ctm, gx, gy, &fx, &fy);
677              
678             /* Bounding box — for horizontal, extent runs along x; for vertical
679             * it runs along -y (each glyph sits below the previous one). */
680 14754 100         adv = (is_vertical ? glyph_adv.vertical : glyph_adv.horizontal) * sx * h_scale;
681              
682             /* Phase 9: emit one glyph per decoded codepoint. */
683 14754 50         sub_adv = (uni_list_n > 0) ? adv / (double)uni_list_n : adv;
684 29510 100         for (k = 0; k < uni_list_n; k++) {
685 14756           g.unicode = uni_list[k];
686 14756 100         if (is_vertical) {
687             /* Vertical: each sub-glyph occupies a horizontal strip whose
688             * height is sub_adv and width is effective_size (1 em). */
689 3           double top = fy - sub_adv * (double)k;
690 3           double bottom = fy - sub_adv * (double)(k + 1);
691 3           g.x0 = fx - effective_size * 0.5;
692 3           g.x1 = fx + effective_size * 0.5;
693 3           g.y0 = bottom;
694 3           g.y1 = top;
695             } else {
696 14753           g.x0 = fx + sub_adv * (double)k;
697 14753           g.y0 = fy + descent;
698 14753           g.x1 = fx + sub_adv * (double)(k + 1);
699 14753           g.y1 = fy + ascent;
700             }
701 14756           g.advance = sub_adv;
702 14756           g.font_size = effective_size;
703 14756           g.reliable_advance = (uint8_t)glyph_adv.reliable;
704 14756           g.mcid = result->current_mcid; /* Phase 12 */
705 14756           g.vertical = (uint8_t)is_vertical; /* Phase 14 */
706              
707 14756 50         if (!result_push_glyph(result, &g)) break;
708             }
709              
710             /* Advance text position: §9.4.4. Horizontal moves along +x by the
711             * glyph's horizontal advance; vertical moves along -y by the
712             * vertical advance. Word-spacing only triggers on a space. */
713 14754 100         ws = (unicode == 0x20) ? word_space : 0.0;
714 14754 100         if (is_vertical) {
715 3           accum_x -= (glyph_adv.vertical + char_space + ws) * h_scale;
716             } else {
717 14751           accum_x += (glyph_adv.horizontal + char_space + ws) * h_scale;
718             }
719             }
720             }
721              
722             /* Phase 8: compute the real text-space advance for a string so the
723             * interpreter can keep text_matrix in sync with our glyph positions.
724             * Mirrors the advance logic from textract_on_text_show. */
725 3484           static double textract_get_string_advance(void *ctx,
726             const pdfmake_gstate_t *gs,
727             const uint8_t *bytes, size_t len)
728             {
729 3484           pdfmake_textract_result_t *result = (pdfmake_textract_result_t *)ctx;
730             pdfmake_resolved_font_t *rf;
731             int std14_id;
732             int is_cid;
733             int is_vertical;
734             double font_size;
735             double char_space;
736             double word_space;
737             double accum;
738             size_t pos;
739             uint32_t code;
740             size_t code_w;
741             int16_t w1000;
742             double glyph_advance;
743             uint32_t uni;
744             int w;
745             double mag;
746             int is_space;
747             double ws;
748 3484 50         if (!gs || !bytes || len == 0) return 0;
    50          
    50          
749              
750 3484           rf = resolve_font(result, gs->font);
751 3484 50         std14_id = rf ? rf->std14_id : -1;
752 3484 50         is_cid = rf ? rf->is_cid : 0;
753 3484 50         is_vertical = rf ? rf->wmode : 0;
754 3484           font_size = gs->font_size;
755 3484           char_space = gs->char_space;
756 3484           word_space = gs->word_space;
757              
758 3484           accum = 0;
759 3484           pos = 0;
760 18249 100         while (pos < len) {
761 14765 100         if (is_cid && pos + 1 < len) {
    50          
762 3           code = ((uint32_t)bytes[pos] << 8) | bytes[pos + 1];
763 3           code_w = 2;
764             } else {
765 14762           code = bytes[pos];
766 14762           code_w = 1;
767             }
768              
769             /* Lookup advance in 1/1000 em, same priority order as on_text_show */
770 14765           w1000 = 0;
771 14765 50         if (rf && rf->widths_resolved) {
    100          
772 6349           w1000 = pdfmake_font_widths_lookup(&rf->widths, code);
773             }
774 14765 100         if (w1000 > 0) {
775 6349           glyph_advance = (double)w1000 / 1000.0 * font_size;
776 8416 50         } else if (std14_id >= 0 && !is_cid) {
    50          
777             /* Resolve unicode via encoding for the Std14 lookup */
778 8416 50         uni = rf && rf->encoding_resolved
779 8416           ? rf->encoding.map[code & 0xFF]
780 16832 50         : (uint32_t)code;
781 8416           w = pdfmake_std14_width((pdfmake_std14_id_t)std14_id, uni);
782 8416           glyph_advance = (w > 0) ? (double)w / 1000.0 * font_size
783 8416 50         : 0.25 * font_size;
784             } else {
785 0           glyph_advance = 0.5 * font_size;
786             }
787              
788             /* In vertical mode use the magnitude of /DW2 second element as the
789             * advance; the sign is absorbed by the text-matrix branch in the
790             * interpreter, which treats the returned value as a signed
791             * displacement along -y. */
792 14765 100         if (is_vertical && rf) {
    50          
793 3 50         mag = (double)(rf->default_v_advance < 0
794 3           ? -rf->default_v_advance : rf->default_v_advance);
795 3 50         if (mag > 0) glyph_advance = mag / 1000.0 * font_size;
796             }
797              
798             /* Add spacing contributions (matches on_text_show) */
799 14765           is_space = (code == 0x20);
800 14765 100         ws = is_space ? word_space : 0.0;
801 14765           accum += glyph_advance + char_space + ws;
802              
803 14765           pos += code_w;
804             }
805 3484           return accum;
806             }
807              
808             /* Phase 12: BDC / BMC — push the current MCID on entry to a marked-content
809             * item. MCID comes from the /MCID integer in the properties dict (if any).
810             * BMC has no properties dict so it never carries an MCID. */
811 3           static void textract_on_mc_begin(void *ctx,
812             const pdfmake_gstate_t *gs,
813             uint32_t tag,
814             pdfmake_obj_t *properties)
815             {
816 3           pdfmake_textract_result_t *result = (pdfmake_textract_result_t *)ctx;
817 3           int32_t mcid = -1;
818             (void)gs;
819             (void)tag;
820 3 50         if (!result) return;
821              
822 3 50         if (properties && properties->kind == PDFMAKE_DICT && result->arena) {
    50          
    50          
823 3           uint32_t mcid_key = pdfmake_arena_intern_name(result->arena, "MCID", 4);
824 3           pdfmake_obj_t *v = pdfmake_dict_get(properties, mcid_key);
825 3 50         if (v && v->kind == PDFMAKE_INT) {
    50          
826 3           mcid = (int32_t)v->as.i;
827             }
828             }
829              
830 3 50         if (result->mcid_depth <
831             (int)(sizeof(result->mcid_stack)/sizeof(result->mcid_stack[0]))) {
832 3           result->mcid_stack[result->mcid_depth++] = result->current_mcid;
833             }
834             /* Nested marked content without its own /MCID inherits the parent's. */
835 3 50         if (mcid >= 0) result->current_mcid = mcid;
836             }
837              
838 3           static void textract_on_mc_end(void *ctx, const pdfmake_gstate_t *gs)
839             {
840 3           pdfmake_textract_result_t *result = (pdfmake_textract_result_t *)ctx;
841             (void)gs;
842 3 50         if (!result) return;
843 3 50         if (result->mcid_depth > 0) {
844 3           result->current_mcid = result->mcid_stack[--result->mcid_depth];
845             } else {
846 0           result->current_mcid = -1;
847             }
848             }
849              
850             /* Phase 14: report the current font's writing mode so the interpreter can
851             * advance text_matrix on the right axis. */
852 3484           static int textract_is_vertical(void *ctx, const pdfmake_gstate_t *gs) {
853 3484           pdfmake_textract_result_t *result = (pdfmake_textract_result_t *)ctx;
854             pdfmake_resolved_font_t *rf;
855 3484 50         if (!result || !gs) return 0;
    50          
856 3484           rf = resolve_font(result, gs->font);
857 3484 50         return rf ? rf->wmode : 0;
858             }
859              
860 60           pdfmake_visitor_t pdfmake_textract_visitor(pdfmake_textract_result_t *result) {
861             pdfmake_visitor_t v;
862 60           memset(&v, 0, sizeof(v));
863 60           v.ctx = result;
864 60           v.on_text_show = textract_on_text_show;
865 60           v.get_string_advance = textract_get_string_advance;
866 60           v.on_marked_content_begin = textract_on_mc_begin;
867 60           v.on_marked_content_end = textract_on_mc_end;
868 60           v.is_vertical_writing = textract_is_vertical; /* Phase 14 */
869 60           return v;
870             }
871              
872             /*============================================================================
873             * Aggregation: raw glyphs → words → lines → blocks
874             *==========================================================================*/
875              
876             /* qsort() has no ctx param — use a file-static pointer for column info
877             * during the sort. pdfmake_textract_aggregate is single-threaded so this
878             * is safe. */
879             static const double *g_sort_column_splits = NULL;
880             static int g_sort_column_count = 0;
881              
882 100130           static int glyph_column(const pdfmake_text_glyph_t *g) {
883 100130           int col = 0;
884             int i;
885 129358 100         for (i = 0; i < g_sort_column_count; i++) {
886 100130 100         if (g->x0 >= g_sort_column_splits[i]) col++;
887 70902           else break;
888             }
889 100130           return col;
890             }
891              
892             /* Compare glyphs by column, then y descending, then x ascending. */
893 80836           static int cmp_glyph_position(const void *a, const void *b) {
894 80836           const pdfmake_text_glyph_t *ga = (const pdfmake_text_glyph_t *)a;
895 80836           const pdfmake_text_glyph_t *gb = (const pdfmake_text_glyph_t *)b;
896             double col_tol;
897             double dx_cx;
898             double dy2;
899             int ca;
900             int cb;
901             double dy;
902             double dx;
903              
904             /* Phase 14: vertical glyphs sort right-to-left, top-to-bottom so
905             * reading order matches CJK conventions. */
906 80836 100         if (ga->vertical && gb->vertical) {
    50          
907             /* Group into columns by x (tolerance = font size). Right-most
908             * column comes first. */
909 2           col_tol = ga->font_size * 0.5;
910 2           dx_cx = gb->x0 - ga->x0;
911 2 50         if (fabs(dx_cx) > col_tol) {
912 0 0         return dx_cx > 0 ? -1 : 1; /* larger x first */
913             }
914             /* Same column: higher y first (top-to-bottom). */
915 2           dy2 = gb->y0 - ga->y0;
916 2 50         if (dy2 < 0) return -1;
917 0 0         if (dy2 > 0) return 1;
918 0           return 0;
919             }
920             /* If one is vertical and the other horizontal, emit vertical last so
921             * the horizontal reading order isn't interleaved. (Truly mixed pages
922             * are rare; the user can filter per glyph->vertical if they need to.) */
923 80834 50         if (ga->vertical != gb->vertical) {
924 0 0         return ga->vertical ? 1 : -1;
925             }
926              
927             /* Phase 10: columns first so column 1 flows fully before column 2 */
928 80834 100         if (g_sort_column_count > 0) {
929 50065           ca = glyph_column(ga);
930 50065           cb = glyph_column(gb);
931 50065 100         if (ca != cb) return ca - cb;
932             }
933              
934             /* Group by baseline (y0): higher y first (PDF y increases upward) */
935 74488           dy = gb->y0 - ga->y0;
936 74488 100         if (fabs(dy) > ga->font_size * 0.3) {
937 39102 100         return dy > 0 ? 1 : -1;
938             }
939             /* Same line: left to right */
940 35386           dx = ga->x0 - gb->x0;
941 35386 50         if (dx < 0) return -1;
942 0 0         if (dx > 0) return 1;
943 0           return 0;
944             }
945              
946             /* Detect column splits via largest-gap analysis on x0 values.
947             *
948             * Algorithm: take all unique-ish glyph x0 values, sort them, look for
949             * contiguous runs where no glyph starts. The widest such run in the page
950             * interior (not at extremes) is a likely gutter; split at its midpoint.
951             * Recurse on each half to find multi-column layouts up to 8 total columns.
952             *
953             * Thresholds:
954             * - min gutter width: 20pt (so we don't split on letter spacing)
955             * - gutter must be in middle 70% of x-range (not at margins)
956             * - only split if the two sides each contain ≥ 4 glyphs
957             */
958             /* Histogram bin size for column-gutter detection, in user-space points */
959             #define COLUMN_BIN_SIZE 5.0
960              
961 59           static void detect_column_splits_in_range(
962             const pdfmake_text_glyph_t *glyphs, size_t n,
963             double x_lo, double x_hi,
964             double *out_splits, int *out_count, int max_count)
965             {
966             size_t n_bins;
967             uint32_t *hist;
968             size_t i;
969             uint32_t max_cnt;
970             uint32_t empty_thr;
971             size_t safe_lo_bin;
972             size_t safe_hi_bin;
973             size_t best_run;
974             size_t best_start;
975             size_t cur_run;
976             size_t cur_start;
977             size_t mid;
978             size_t min_gutter_bins;
979             size_t active_left;
980             size_t active_right;
981 59 50         if (*out_count >= max_count) return;
982 59 100         if (x_hi - x_lo < 100) return;
983              
984 55           n_bins = (size_t)((x_hi - x_lo) / COLUMN_BIN_SIZE) + 1;
985 55 50         if (n_bins < 16 || n_bins > 10000) return;
    50          
986              
987 55           hist = calloc(n_bins, sizeof(uint32_t));
988 55 50         if (!hist) return;
989              
990             /* Populate histogram by glyph bbox coverage (not just x0) */
991 31263 100         for (i = 0; i < n; i++) {
992 31208           const pdfmake_text_glyph_t *g = &glyphs[i];
993             double a;
994             double b;
995             size_t ba;
996             size_t bb;
997 31208 100         if (g->x1 < x_lo || g->x0 > x_hi) continue;
    100          
998 22827 100         a = g->x0 > x_lo ? g->x0 : x_lo;
999 22827 100         b = g->x1 < x_hi ? g->x1 : x_hi;
1000 22827 50         if (b <= a) continue;
1001 22827           ba = (size_t)((a - x_lo) / COLUMN_BIN_SIZE);
1002 22827           bb = (size_t)((b - x_lo) / COLUMN_BIN_SIZE);
1003 22827 50         if (bb >= n_bins) bb = n_bins - 1;
1004             {
1005             size_t k;
1006 70018 100         for (k = ba; k <= bb; k++) hist[k]++;
1007             }
1008             }
1009              
1010 55           max_cnt = 0;
1011 3340 100         for (i = 0; i < n_bins; i++)
1012 3285 100         if (hist[i] > max_cnt) max_cnt = hist[i];
1013 55 100         if (max_cnt < 4) { free(hist); return; }
1014              
1015             /* "Near-empty" = less than 15% of peak density. Higher than 2% because
1016             * full-width headers/footers contribute thin residual occupancy across
1017             * the gutter, and we don't want those to mask real column gaps. */
1018 54           empty_thr = max_cnt / 6 + 1;
1019              
1020             /* Only consider gutters whose midpoint is in the middle 70% of range */
1021 54           safe_lo_bin = n_bins * 15 / 100;
1022 54           safe_hi_bin = n_bins * 85 / 100;
1023              
1024 54           best_run = 0;
1025 54           best_start = 0;
1026 54           cur_run = 0;
1027 54           cur_start = 0;
1028 3295 100         for (i = 0; i < n_bins; i++) {
1029 3241 100         if (hist[i] < empty_thr) {
1030 1315 100         if (cur_run == 0) cur_start = i;
1031 1315           cur_run++;
1032             } else {
1033 1926 100         if (cur_run > best_run) {
1034 73           mid = cur_start + cur_run / 2;
1035 73 100         if (mid >= safe_lo_bin && mid <= safe_hi_bin) {
    50          
1036 71           best_run = cur_run;
1037 71           best_start = cur_start;
1038             }
1039             }
1040 1926           cur_run = 0;
1041             }
1042             }
1043 54 100         if (cur_run > best_run) {
1044 44           mid = cur_start + cur_run / 2;
1045 44 50         if (mid >= safe_lo_bin && mid <= safe_hi_bin) {
    100          
1046 23           best_run = cur_run;
1047 23           best_start = cur_start;
1048             }
1049             }
1050              
1051 54           min_gutter_bins = (size_t)(20.0 / COLUMN_BIN_SIZE); /* 20pt */
1052 54 100         if (best_run >= min_gutter_bins) {
1053 44           active_left = 0;
1054 44           active_right = 0;
1055 1351 100         for (i = 0; i < best_start; i++)
1056 1307 100         if (hist[i] >= empty_thr) active_left++;
1057 659 100         for (i = best_start + best_run; i < n_bins; i++)
1058 615 100         if (hist[i] >= empty_thr) active_right++;
1059 44 100         if (active_left >= 4 && active_right >= 4) {
    100          
1060 17           double mid_x = x_lo + (best_start + best_run / 2.0) * COLUMN_BIN_SIZE;
1061              
1062 17           int pos = *out_count;
1063 17 50         while (pos > 0 && out_splits[pos - 1] > mid_x) {
    0          
1064 0           out_splits[pos] = out_splits[pos - 1]; pos--;
1065             }
1066 17           out_splits[pos] = mid_x;
1067 17           (*out_count)++;
1068              
1069 17           free(hist);
1070             /* Recurse into each half to find further column splits */
1071 17           detect_column_splits_in_range(glyphs, n,
1072             x_lo, mid_x, out_splits, out_count, max_count);
1073 17           detect_column_splits_in_range(glyphs, n,
1074             mid_x, x_hi, out_splits, out_count, max_count);
1075 17           return;
1076             }
1077             }
1078              
1079 37           free(hist);
1080             }
1081              
1082 60           static void detect_column_splits(pdfmake_textract_result_t *result) {
1083             double x_lo;
1084             double x_hi;
1085             size_t i;
1086 60           result->column_split_count = 0;
1087 60 100         if (result->raw_len < 16) return;
1088              
1089 31           x_lo = 1e18;
1090 31           x_hi = -1e18;
1091 14476 100         for (i = 0; i < result->raw_len; i++) {
1092 14445 100         if (result->raw_glyphs[i].x0 < x_lo) x_lo = result->raw_glyphs[i].x0;
1093 14445 100         if (result->raw_glyphs[i].x1 > x_hi) x_hi = result->raw_glyphs[i].x1;
1094             }
1095 31 100         if (x_hi - x_lo < 100) return;
1096              
1097 25           detect_column_splits_in_range(result->raw_glyphs, result->raw_len,
1098 25           x_lo, x_hi, result->column_splits, &result->column_split_count,
1099             (int)(sizeof(result->column_splits) / sizeof(double)));
1100             }
1101              
1102 13288           static void update_bbox_from_glyph(double *x0, double *y0, double *x1, double *y1,
1103             const pdfmake_text_glyph_t *g) {
1104 13288 100         if (g->x0 < *x0) *x0 = g->x0;
1105 13288 100         if (g->y0 < *y0) *y0 = g->y0;
1106 13288 100         if (g->x1 > *x1) *x1 = g->x1;
1107 13288 100         if (g->y1 > *y1) *y1 = g->y1;
1108 13288           }
1109              
1110 13288           static int word_push_glyph(pdfmake_text_word_t *w, const pdfmake_text_glyph_t *g) {
1111 13288 100         if (w->len >= w->cap) {
1112 2766 100         size_t new_cap = w->cap == 0 ? 8 : w->cap * 2;
1113 2766           pdfmake_text_glyph_t *arr = realloc(w->glyphs, new_cap * sizeof(*arr));
1114 2766 50         if (!arr) return 0;
1115 2766           w->glyphs = arr;
1116 2766           w->cap = new_cap;
1117             }
1118 13288           w->glyphs[w->len++] = *g;
1119 13288           return 1;
1120             }
1121              
1122 2327           static int line_push_word(pdfmake_text_line_t *l, const pdfmake_text_word_t *w) {
1123 2327 100         if (l->len >= l->cap) {
1124 756 100         size_t new_cap = l->cap == 0 ? 8 : l->cap * 2;
1125 756           pdfmake_text_word_t *arr = realloc(l->words, new_cap * sizeof(*arr));
1126 756 50         if (!arr) return 0;
1127 756           l->words = arr;
1128 756           l->cap = new_cap;
1129             }
1130 2327           l->words[l->len++] = *w;
1131 2327           return 1;
1132             }
1133              
1134 707           static int block_push_line(pdfmake_text_block_t *b, const pdfmake_text_line_t *l) {
1135 707 100         if (b->len >= b->cap) {
1136 299 100         size_t new_cap = b->cap == 0 ? 8 : b->cap * 2;
1137 299           pdfmake_text_line_t *arr = realloc(b->lines, new_cap * sizeof(*arr));
1138 299 50         if (!arr) return 0;
1139 299           b->lines = arr;
1140 299           b->cap = new_cap;
1141             }
1142 707           b->lines[b->len++] = *l;
1143 707           return 1;
1144             }
1145              
1146 266           static int result_push_block(pdfmake_textract_result_t *r, const pdfmake_text_block_t *b) {
1147 266 100         if (r->len >= r->cap) {
1148 98 100         size_t new_cap = r->cap == 0 ? 4 : r->cap * 2;
1149 98           pdfmake_text_block_t *arr = realloc(r->blocks, new_cap * sizeof(*arr));
1150 98 50         if (!arr) return 0;
1151 98           r->blocks = arr;
1152 98           r->cap = new_cap;
1153             }
1154 266           r->blocks[r->len++] = *b;
1155 266           return 1;
1156             }
1157              
1158 60           pdfmake_err_t pdfmake_textract_aggregate(
1159             pdfmake_textract_result_t *result,
1160             const pdfmake_textract_options_t *options)
1161             {
1162             pdfmake_textract_options_t opts;
1163             pdfmake_text_word_t *words;
1164             size_t words_len;
1165             size_t words_cap;
1166             pdfmake_text_word_t cur_word;
1167             pdfmake_text_line_t *lines;
1168             size_t lines_len;
1169             size_t lines_cap;
1170             pdfmake_text_line_t cur_line;
1171             pdfmake_text_block_t cur_block;
1172             size_t i;
1173 60 50         if (!result || result->raw_len == 0) return PDFMAKE_OK;
    50          
1174              
1175 60 50         opts = options ? *options : pdfmake_textract_default_options();
1176              
1177             /* Phase 10: detect column splits before sorting so reading order is
1178             * column-major (fill column 1 top-to-bottom, then column 2, ...). */
1179 60           detect_column_splits(result);
1180 60           g_sort_column_splits = result->column_splits;
1181 60           g_sort_column_count = result->column_split_count;
1182              
1183             /* Sort glyphs into reading order */
1184 60           qsort(result->raw_glyphs, result->raw_len, sizeof(pdfmake_text_glyph_t),
1185             cmp_glyph_position);
1186              
1187             /* Don't leak static pointers past this function */
1188 60           g_sort_column_splits = NULL;
1189 60           g_sort_column_count = 0;
1190              
1191             /* Phase 5: Kern-aware word grouping.
1192             *
1193             * We split into a new word when ONE of:
1194             * - previous glyph is a space (U+0020)
1195             * - current glyph is a space (low priority)
1196             * - baseline jumped significantly (implies new line)
1197             * - horizontal gap exceeds a context-sensitive threshold
1198             *
1199             * Key insight from Phase 5: the gap threshold must be wider when the
1200             * previous glyph's advance is a fallback (i.e., we didn't know the real
1201             * width). In that case a "real-looking" Td that's actually intra-word
1202             * spacing (author-driven font-change mid-word in subsetted fonts) looks
1203             * like a big visible gap. Using ~0.9 em here keeps "Data" glued together
1204             * across font changes while still catching inter-word spaces.
1205             *
1206             * When we trust the advance, we use the tighter 0.3-em threshold —
1207             * that's what PDF typesetters use as a natural space width.
1208             *
1209             * We never split on negative or zero gaps; those come from kerning
1210             * overlap in TJ arrays and always represent intra-word positioning.
1211             */
1212 60           words = NULL;
1213 60           words_len = 0;
1214 60           words_cap = 0;
1215              
1216 60           memset(&cur_word, 0, sizeof(cur_word));
1217 60           cur_word.x0 = cur_word.y0 = 1e18;
1218 60           cur_word.x1 = cur_word.y1 = -1e18;
1219 60           cur_word.mcid = -1;
1220              
1221 14816 100         for (i = 0; i < result->raw_len; i++) {
1222 14756           pdfmake_text_glyph_t *g = &result->raw_glyphs[i];
1223              
1224 14756           int new_word = 0;
1225 14756 100         if (cur_word.len == 0) {
1226 1514           new_word = 0; /* first glyph */
1227             } else {
1228 13242           pdfmake_text_glyph_t *prev = &cur_word.glyphs[cur_word.len - 1];
1229              
1230 13242 100         if (g->vertical) {
1231             /* Phase 14: vertical word — split on column change (x jump)
1232             * or on a large vertical gap. */
1233 2           double col_diff = fabs(g->x0 - prev->x0);
1234             /* prev->y0 is the bottom; g->y1 is the top of the new glyph.
1235             * In top-to-bottom flow, gap = prev->y0 - g->y1 > 0. */
1236 2           double v_gap = prev->y0 - g->y1;
1237              
1238 2 50         if (col_diff > g->font_size * 0.5) {
1239 0           new_word = 1;
1240             }
1241 2 50         if (!new_word && v_gap > opts.word_gap_factor * g->font_size) {
    50          
1242 0           new_word = 1;
1243             }
1244             } else {
1245 13240           double gap = g->x0 - prev->x1;
1246 13240           double baseline_diff = fabs(g->y0 - prev->y0);
1247              
1248             /* Rule 1: baseline change = new line = new word */
1249 13240 100         if (baseline_diff > opts.line_tolerance * g->font_size) {
1250 523           new_word = 1;
1251             }
1252              
1253             /* Rule 2: explicit space character = word boundary */
1254 13240 100         if (!new_word &&
1255 12717 50         (prev->unicode == 0x20 || g->unicode == 0x20)) {
    100          
1256 1395           new_word = 1;
1257             }
1258              
1259             /* Rule 3: gap threshold based on advance reliability.
1260             * - Reliable advances: tight ~0.3 em threshold
1261             * - Fallback advances: generous ~0.9 em threshold
1262             * - Negative/zero gaps: never split (kerning) */
1263 13240 100         if (!new_word && gap > 0) {
    100          
1264             double threshold;
1265 6166 50         if (prev->reliable_advance) {
1266 6166           threshold = opts.word_gap_factor * g->font_size;
1267             } else {
1268             /* Unreliable widths: need a bigger gap to call it a space */
1269 0           threshold = 0.9 * g->font_size;
1270             }
1271 6166 100         if (gap > threshold) new_word = 1;
1272             }
1273             }
1274             }
1275              
1276 14756 100         if (new_word && cur_word.len > 0) {
    50          
1277             /* Flush current word */
1278 2281 100         if (words_len >= words_cap) {
1279 113 100         words_cap = words_cap == 0 ? 16 : words_cap * 2;
1280 113           words = realloc(words, words_cap * sizeof(*words));
1281 113 50         if (!words) return PDFMAKE_ENOMEM;
1282             }
1283 2281           words[words_len++] = cur_word;
1284 2281           memset(&cur_word, 0, sizeof(cur_word));
1285 2281           cur_word.x0 = cur_word.y0 = 1e18;
1286 2281           cur_word.x1 = cur_word.y1 = -1e18;
1287 2281           cur_word.mcid = -1;
1288             }
1289              
1290             /* Skip spaces (don't include in words) */
1291 14756 100         if (g->unicode == 0x20) continue;
1292              
1293             /* Phase 12: first glyph sets the word's MCID */
1294 13288 100         if (cur_word.len == 0) cur_word.mcid = g->mcid;
1295              
1296 13288           word_push_glyph(&cur_word, g);
1297 13288           update_bbox_from_glyph(&cur_word.x0, &cur_word.y0,
1298             &cur_word.x1, &cur_word.y1, g);
1299             }
1300             /* Flush last word */
1301 60 100         if (cur_word.len > 0) {
1302 46 100         if (words_len >= words_cap) {
1303 6 100         words_cap = words_cap == 0 ? 16 : words_cap * 2;
1304 6           words = realloc(words, words_cap * sizeof(*words));
1305             }
1306 46 50         if (words) words[words_len++] = cur_word;
1307             }
1308              
1309 60 50         if (words_len == 0) {
1310 0           free(words);
1311 0           return PDFMAKE_OK;
1312             }
1313              
1314             /* Phase 2: Group words into lines (same baseline) */
1315 60           lines = NULL;
1316 60           lines_len = 0;
1317 60           lines_cap = 0;
1318              
1319 60           memset(&cur_line, 0, sizeof(cur_line));
1320 60           cur_line.x0 = cur_line.y0 = 1e18;
1321 60           cur_line.x1 = cur_line.y1 = -1e18;
1322              
1323 2387 100         for (i = 0; i < words_len; i++) {
1324 2327           pdfmake_text_word_t *w = &words[i];
1325              
1326             /* Phase 14: detect whether this word is vertical by checking its
1327             * first glyph. Line grouping then compares x (column) instead of
1328             * baseline y. */
1329 2327 50         int w_vertical = (w->len > 0 && w->glyphs[0].vertical);
    100          
1330              
1331 2327           int new_line = 0;
1332 2327 100         if (cur_line.len == 0) {
1333 60           cur_line.baseline_y = w->y0;
1334             } else {
1335 2267           double ref_size = cur_line.words[0].glyphs[0].font_size;
1336 2267 50         if (w_vertical) {
1337             /* Same column = same vertical line. */
1338 0           double col_diff = fabs(w->x0 - cur_line.words[0].x0);
1339 0 0         if (col_diff > ref_size * 0.5) new_line = 1;
1340             } else {
1341 2267           double baseline_diff = fabs(w->y0 - cur_line.baseline_y);
1342 2267 100         if (baseline_diff > opts.line_tolerance * ref_size) {
1343 647           new_line = 1;
1344             }
1345             }
1346             }
1347              
1348 2327 100         if (new_line && cur_line.len > 0) {
    50          
1349 647 100         if (lines_len >= lines_cap) {
1350 79 100         lines_cap = lines_cap == 0 ? 8 : lines_cap * 2;
1351 79           lines = realloc(lines, lines_cap * sizeof(*lines));
1352 79 50         if (!lines) { free(words); return PDFMAKE_ENOMEM; }
1353             }
1354 647           lines[lines_len++] = cur_line;
1355 647           memset(&cur_line, 0, sizeof(cur_line));
1356 647           cur_line.x0 = cur_line.y0 = 1e18;
1357 647           cur_line.x1 = cur_line.y1 = -1e18;
1358 647           cur_line.baseline_y = w->y0;
1359             }
1360              
1361 2327           line_push_word(&cur_line, w);
1362 2327 100         if (w->x0 < cur_line.x0) cur_line.x0 = w->x0;
1363 2327 100         if (w->y0 < cur_line.y0) cur_line.y0 = w->y0;
1364 2327 100         if (w->x1 > cur_line.x1) cur_line.x1 = w->x1;
1365 2327 100         if (w->y1 > cur_line.y1) cur_line.y1 = w->y1;
1366             }
1367 60 50         if (cur_line.len > 0) {
1368 60 100         if (lines_len >= lines_cap) {
1369 27 100         lines_cap = lines_cap == 0 ? 8 : lines_cap * 2;
1370 27           lines = realloc(lines, lines_cap * sizeof(*lines));
1371             }
1372 60 50         if (lines) lines[lines_len++] = cur_line;
1373             }
1374 60           free(words); /* words are now owned by lines */
1375              
1376             /* Phase 3: Group lines into blocks */
1377 60           memset(&cur_block, 0, sizeof(cur_block));
1378 60           cur_block.x0 = cur_block.y0 = 1e18;
1379 60           cur_block.x1 = cur_block.y1 = -1e18;
1380              
1381 767 100         for (i = 0; i < lines_len; i++) {
1382 707           pdfmake_text_line_t *l = &lines[i];
1383              
1384 707           int new_block = 0;
1385 707 100         if (cur_block.len > 0) {
1386 647           pdfmake_text_line_t *prev = &cur_block.lines[cur_block.len - 1];
1387 647           double ref_size = prev->words[0].glyphs[0].font_size;
1388 1294 50         int l_vertical = (l->words[0].len > 0 &&
1389 647 50         l->words[0].glyphs[0].vertical);
1390             double gap;
1391 647 50         if (l_vertical) {
1392             /* Vertical: block boundary when columns are far apart in x. */
1393 0           gap = fabs(prev->words[0].x0 - l->words[0].x0);
1394             } else {
1395 647           gap = fabs(prev->baseline_y - l->baseline_y);
1396             }
1397 647 100         if (gap > opts.block_leading * ref_size) {
1398 206           new_block = 1;
1399             }
1400             }
1401              
1402 707 100         if (new_block && cur_block.len > 0) {
    50          
1403 206           result_push_block(result, &cur_block);
1404 206           memset(&cur_block, 0, sizeof(cur_block));
1405 206           cur_block.x0 = cur_block.y0 = 1e18;
1406 206           cur_block.x1 = cur_block.y1 = -1e18;
1407             }
1408              
1409 707           block_push_line(&cur_block, l);
1410 707 100         if (l->x0 < cur_block.x0) cur_block.x0 = l->x0;
1411 707 50         if (l->y0 < cur_block.y0) cur_block.y0 = l->y0;
1412 707 100         if (l->x1 > cur_block.x1) cur_block.x1 = l->x1;
1413 707 100         if (l->y1 > cur_block.y1) cur_block.y1 = l->y1;
1414             }
1415 60 50         if (cur_block.len > 0) {
1416 60           result_push_block(result, &cur_block);
1417             }
1418 60           free(lines); /* lines are now owned by blocks */
1419              
1420 60           return PDFMAKE_OK;
1421             }
1422              
1423             /*============================================================================
1424             * Convenience: run extraction in one call
1425             *==========================================================================*/
1426              
1427 60           pdfmake_err_t pdfmake_textract_run(
1428             pdfmake_interp_t *interp,
1429             const uint8_t *content, size_t content_len,
1430             const pdfmake_textract_options_t *options,
1431             pdfmake_textract_result_t *result)
1432             {
1433             pdfmake_visitor_t visitor;
1434             pdfmake_err_t err;
1435 60 50         if (!interp || !content || !result) return PDFMAKE_EINVAL;
    50          
    50          
1436              
1437             /* Propagate per-run options that the visitor needs to see during
1438             * interpretation (aggregate-phase options stay in `options`). */
1439 60 50         if (options) {
1440 60           result->include_invisible = options->include_invisible;
1441             }
1442              
1443             /* Set up visitor */
1444 60           visitor = pdfmake_textract_visitor(result);
1445 60           pdfmake_interp_set_visitor(interp, &visitor);
1446              
1447             /* Interpret content stream */
1448 60           err = pdfmake_interpret(interp, content, content_len);
1449 60 50         if (err != PDFMAKE_OK) return err;
1450              
1451             /* Aggregate */
1452 60           return pdfmake_textract_aggregate(result, options);
1453             }
1454              
1455             /*============================================================================
1456             * Phase 12: Structure tree resolution
1457             *
1458             * Walks a /StructTreeRoot subtree and populates result->struct_map with one
1459             * entry per MCID encountered, pairing it with the /S role of the nearest
1460             * enclosing StructElem. The resulting flat map is consulted after extraction
1461             * so each word can be tagged with its structure role.
1462             *==========================================================================*/
1463              
1464 3           static int struct_map_push(pdfmake_textract_result_t *r, int32_t mcid,
1465             uint32_t role_id)
1466             {
1467 3 50         if (mcid < 0 || role_id == 0) return 1;
    50          
1468 3 100         if (r->struct_map_len >= r->struct_map_cap) {
1469 1 50         size_t new_cap = r->struct_map_cap == 0 ? 16 : r->struct_map_cap * 2;
1470 1           pdfmake_struct_map_entry_t *n = realloc(
1471 1           r->struct_map, new_cap * sizeof(*n));
1472 1 50         if (!n) return 0;
1473 1           r->struct_map = n;
1474 1           r->struct_map_cap = new_cap;
1475             }
1476 3           r->struct_map[r->struct_map_len].mcid = mcid;
1477 3           r->struct_map[r->struct_map_len].role_id = role_id;
1478 3           r->struct_map_len++;
1479 3           return 1;
1480             }
1481              
1482             /* Recursively walk a StructElem dict or its /K entry. role_id is the /S of
1483             * the nearest enclosing StructElem. page_dict is the target page (NULL to
1484             * accept all pages). */
1485 7           static void walk_struct_node(pdfmake_textract_result_t *r,
1486             pdfmake_obj_t *node,
1487             uint32_t role_id,
1488             pdfmake_obj_t *page_dict,
1489             int depth)
1490             {
1491             size_t i;
1492             size_t n;
1493             pdfmake_obj_t *item;
1494             uint32_t type_key;
1495             pdfmake_obj_t *type_v;
1496             const char *type_name;
1497             uint32_t mcid_key;
1498             pdfmake_obj_t *mcid_v;
1499             uint32_t pg_key;
1500             pdfmake_obj_t *pg;
1501             pdfmake_reader_t *rd;
1502             pdfmake_obj_t *resolved;
1503             uint32_t s_key;
1504             pdfmake_obj_t *s_v;
1505             uint32_t this_role;
1506             uint32_t k_key;
1507             pdfmake_obj_t *k;
1508 7 50         if (!node || depth > 32) return;
    50          
1509              
1510             /* Follow indirect references */
1511 7 100         if (node->kind == PDFMAKE_REF && r->reader) {
    50          
1512 3           pdfmake_reader_t *rd = (pdfmake_reader_t *)r->reader;
1513 3 50         if (!rd->parser) return;
1514 3           node = pdfmake_parser_resolve(rd->parser, node->as.ref);
1515 3 50         if (!node) return;
1516             }
1517              
1518             /* Integer leaf: this MCID belongs to role_id */
1519 7 100         if (node->kind == PDFMAKE_INT) {
1520 3           struct_map_push(r, (int32_t)node->as.i, role_id);
1521 3           return;
1522             }
1523              
1524             /* Array: recurse into each kid, keeping the current role_id */
1525 4 100         if (node->kind == PDFMAKE_ARRAY) {
1526 1           n = pdfmake_array_len(node);
1527 4 100         for (i = 0; i < n; i++) {
1528 3           item = pdfmake_array_get(node, i);
1529 3 50         if (item) walk_struct_node(r, item, role_id, page_dict, depth + 1);
1530             }
1531 1           return;
1532             }
1533              
1534 3 50         if (node->kind != PDFMAKE_DICT) return;
1535              
1536             /* MCR dict: { /Type /MCR, /Pg , /MCID n } — ignore /Pg for now
1537             * since per-page extraction already limits us to one page's content. */
1538 3           type_key = pdfmake_arena_intern_name(r->arena, "Type", 4);
1539 3           type_v = pdfmake_dict_get(node, type_key);
1540 3           type_name = NULL;
1541 3 50         if (type_v && type_v->kind == PDFMAKE_NAME) {
    50          
1542 3           type_name = pdfmake_get_name_bytes(r->arena, type_v);
1543             }
1544              
1545 3           mcid_key = pdfmake_arena_intern_name(r->arena, "MCID", 4);
1546 3           mcid_v = pdfmake_dict_get(node, mcid_key);
1547              
1548 3 50         if (type_name && strcmp(type_name, "MCR") == 0) {
    50          
1549 0           pg_key = pdfmake_arena_intern_name(r->arena, "Pg", 2);
1550 0           pg = pdfmake_dict_get(node, pg_key);
1551 0 0         if (pg && page_dict && pg->kind == PDFMAKE_REF && r->reader) {
    0          
    0          
    0          
1552             /* Skip MCRs that target a different page */
1553 0           rd = (pdfmake_reader_t *)r->reader;
1554 0           resolved = rd->parser
1555 0 0         ? pdfmake_parser_resolve(rd->parser, pg->as.ref) : NULL;
1556 0 0         if (resolved && resolved != page_dict) return;
    0          
1557             }
1558 0 0         if (mcid_v && mcid_v->kind == PDFMAKE_INT) {
    0          
1559 0           struct_map_push(r, (int32_t)mcid_v->as.i, role_id);
1560             }
1561 0           return;
1562             }
1563              
1564             /* OBJR (object reference to an annotation) — no MCID, skip */
1565 3 50         if (type_name && strcmp(type_name, "OBJR") == 0) return;
    50          
1566              
1567             /* StructElem: adopt its /S as the current role, then recurse into /K */
1568 3           s_key = pdfmake_arena_intern_name(r->arena, "S", 1);
1569 3           s_v = pdfmake_dict_get(node, s_key);
1570 3           this_role = role_id;
1571 3 50         if (s_v && s_v->kind == PDFMAKE_NAME) this_role = s_v->as.name.id;
    50          
1572              
1573             /* /Pg filter: if this StructElem binds a specific page and it doesn't
1574             * match ours, we can still recurse — children may override /Pg. */
1575 3           k_key = pdfmake_arena_intern_name(r->arena, "K", 1);
1576 3           k = pdfmake_dict_get(node, k_key);
1577 3 50         if (!k) return;
1578              
1579             /* /K can be an integer (MCID), a dict (StructElem/MCR/OBJR), a ref, or
1580             * an array of any of the above. walk_struct_node handles all cases. */
1581 3           walk_struct_node(r, k, this_role, page_dict, depth + 1);
1582             }
1583              
1584 1           pdfmake_err_t pdfmake_textract_resolve_struct_tree(
1585             pdfmake_textract_result_t *result,
1586             pdfmake_obj_t *struct_root,
1587             pdfmake_obj_t *page_dict)
1588             {
1589             pdfmake_reader_t *rd;
1590             uint32_t k_key;
1591             pdfmake_obj_t *k;
1592 1 50         if (!result) return PDFMAKE_EINVAL;
1593 1 50         if (!struct_root || !result->arena) return PDFMAKE_OK; /* no-op */
    50          
1594              
1595             /* Follow an indirect reference to the StructTreeRoot dict */
1596 1 50         if (struct_root->kind == PDFMAKE_REF && result->reader) {
    50          
1597 1           rd = (pdfmake_reader_t *)result->reader;
1598 1 50         if (rd->parser) {
1599 1           struct_root = pdfmake_parser_resolve(rd->parser, struct_root->as.ref);
1600 1 50         if (!struct_root) return PDFMAKE_OK;
1601             }
1602             }
1603 1 50         if (struct_root->kind != PDFMAKE_DICT) return PDFMAKE_OK;
1604              
1605             /* Start at /K of the StructTreeRoot; role 0 means "none" (children will
1606             * overwrite as soon as they hit a StructElem with /S). */
1607 1           k_key = pdfmake_arena_intern_name(result->arena, "K", 1);
1608 1           k = pdfmake_dict_get(struct_root, k_key);
1609 1 50         if (k) walk_struct_node(result, k, 0, page_dict, 0);
1610              
1611 1           return PDFMAKE_OK;
1612             }
1613              
1614 5           uint32_t pdfmake_textract_role_for_mcid(
1615             const pdfmake_textract_result_t *result, int32_t mcid)
1616             {
1617             size_t i;
1618 5 50         if (!result || mcid < 0) return 0;
    50          
1619 11 50         for (i = 0; i < result->struct_map_len; i++) {
1620 11 100         if (result->struct_map[i].mcid == mcid) {
1621 5           return result->struct_map[i].role_id;
1622             }
1623             }
1624 0           return 0;
1625             }
1626              
1627             /*============================================================================
1628             * Phase 13 — Annotation + form field text extraction
1629             *==========================================================================*/
1630              
1631             /* Decode a PDF "text string" (§7.9.2) to UTF-8, arena-allocated.
1632             * Handles UTF-16BE (with BOM FE FF) and UTF-8-with-BOM (EF BB BF); everything
1633             * else is treated as PDFDocEncoding, which matches ISO-8859-1 for the ASCII
1634             * subset used by every real-world annotation we've seen. */
1635 8           static const char *decode_pdf_text(pdfmake_arena_t *arena,
1636             const uint8_t *b, size_t n)
1637             {
1638             size_t i;
1639             pdfmake_buf_t out;
1640             char *s;
1641             uint32_t cp;
1642             uint32_t lo;
1643             uint8_t c;
1644 8 50         if (!arena || !b) return NULL;
    50          
1645              
1646             /* UTF-16BE BOM */
1647 8 50         if (n >= 2 && b[0] == 0xFE && b[1] == 0xFF) {
    50          
    0          
1648 0           pdfmake_buf_init(&out);
1649 0           s = NULL;
1650 0 0         for (i = 2; i + 1 < n; i += 2) {
1651 0           cp = ((uint32_t)b[i] << 8) | b[i + 1];
1652 0 0         if (cp >= 0xD800 && cp <= 0xDBFF && i + 3 < n) {
    0          
    0          
1653 0           lo = ((uint32_t)b[i + 2] << 8) | b[i + 3];
1654 0 0         if (lo >= 0xDC00 && lo <= 0xDFFF) {
    0          
1655 0           cp = 0x10000 + ((cp - 0xD800) << 10) + (lo - 0xDC00);
1656 0           i += 2;
1657             }
1658             }
1659 0 0         if (cp < 0x80) {
1660 0           pdfmake_buf_append_byte(&out, (uint8_t)cp);
1661 0 0         } else if (cp < 0x800) {
1662 0           pdfmake_buf_append_byte(&out, 0xC0 | (cp >> 6));
1663 0           pdfmake_buf_append_byte(&out, 0x80 | (cp & 0x3F));
1664 0 0         } else if (cp < 0x10000) {
1665 0           pdfmake_buf_append_byte(&out, 0xE0 | (cp >> 12));
1666 0           pdfmake_buf_append_byte(&out, 0x80 | ((cp >> 6) & 0x3F));
1667 0           pdfmake_buf_append_byte(&out, 0x80 | (cp & 0x3F));
1668             } else {
1669 0           pdfmake_buf_append_byte(&out, 0xF0 | (cp >> 18));
1670 0           pdfmake_buf_append_byte(&out, 0x80 | ((cp >> 12) & 0x3F));
1671 0           pdfmake_buf_append_byte(&out, 0x80 | ((cp >> 6) & 0x3F));
1672 0           pdfmake_buf_append_byte(&out, 0x80 | (cp & 0x3F));
1673             }
1674             }
1675 0           s = pdfmake_arena_alloc(arena, out.len + 1);
1676 0 0         if (s) { memcpy(s, out.data, out.len); s[out.len] = 0; }
1677 0           pdfmake_buf_free(&out);
1678 0           return s;
1679             }
1680              
1681             /* UTF-8 with BOM (PDF 2.0) */
1682 8 50         if (n >= 3 && b[0] == 0xEF && b[1] == 0xBB && b[2] == 0xBF) {
    50          
    0          
    0          
1683 0           b += 3; n -= 3;
1684             }
1685              
1686             /* PDFDocEncoding → promote high-bit bytes to 2-byte UTF-8. */
1687 8           pdfmake_buf_init(&out);
1688 106 100         for (i = 0; i < n; i++) {
1689 98           c = b[i];
1690 98 50         if (c < 0x80) {
1691 98           pdfmake_buf_append_byte(&out, c);
1692             } else {
1693 0           pdfmake_buf_append_byte(&out, 0xC0 | (c >> 6));
1694 0           pdfmake_buf_append_byte(&out, 0x80 | (c & 0x3F));
1695             }
1696             }
1697 8           s = pdfmake_arena_alloc(arena, out.len + 1);
1698 8 50         if (s) { memcpy(s, out.data, out.len); s[out.len] = 0; }
1699 8           pdfmake_buf_free(&out);
1700 8           return s;
1701             }
1702              
1703             /* Pull a PDF text string out of a dict (resolving one level of indirection).
1704             * Returns NULL if the key is missing or not a string. */
1705 8           static const char *dict_get_text(pdfmake_reader_t *rd,
1706             pdfmake_arena_t *arena,
1707             pdfmake_obj_t *dict,
1708             const char *name)
1709             {
1710             uint32_t key;
1711             pdfmake_obj_t *v;
1712 8 50         if (!dict || dict->kind != PDFMAKE_DICT) return NULL;
    50          
1713 8           key = pdfmake_arena_intern_name(arena, name, strlen(name));
1714 8           v = pdfmake_dict_get(dict, key);
1715 8 100         if (v && v->kind == PDFMAKE_REF && rd && rd->parser) {
    50          
    0          
    0          
1716 0           v = pdfmake_parser_resolve(rd->parser, v->as.ref);
1717             }
1718 8 100         if (!v || v->kind != PDFMAKE_STR) return NULL;
    50          
1719 7           return decode_pdf_text(arena, v->as.str.bytes, v->as.str.len);
1720             }
1721              
1722             /* Resolve a dict entry that's allowed to be an indirect reference. */
1723 1           static pdfmake_obj_t *dict_get_resolved(pdfmake_reader_t *rd,
1724             pdfmake_arena_t *arena,
1725             pdfmake_obj_t *dict,
1726             const char *name)
1727             {
1728             uint32_t key;
1729             pdfmake_obj_t *v;
1730 1 50         if (!dict || dict->kind != PDFMAKE_DICT) return NULL;
    50          
1731 1           key = pdfmake_arena_intern_name(arena, name, strlen(name));
1732 1           v = pdfmake_dict_get(dict, key);
1733 1 50         if (v && v->kind == PDFMAKE_REF && rd && rd->parser) {
    50          
    50          
    50          
1734 1           v = pdfmake_parser_resolve(rd->parser, v->as.ref);
1735             }
1736 1           return v;
1737             }
1738              
1739 2           pdfmake_annot_text_list_t *pdfmake_annot_text_list_new(pdfmake_arena_t *arena) {
1740 2           pdfmake_annot_text_list_t *l = calloc(1, sizeof(*l));
1741 2 50         if (l) l->arena = arena;
1742 2           return l;
1743             }
1744              
1745 2           void pdfmake_annot_text_list_free(pdfmake_annot_text_list_t *list) {
1746 2 50         if (!list) return;
1747 2           free(list->items);
1748 2           free(list);
1749             }
1750              
1751 3           static int annot_list_push(pdfmake_annot_text_list_t *l,
1752             const pdfmake_annot_text_t *rec)
1753             {
1754 3 100         if (l->len >= l->cap) {
1755 1 50         size_t ncap = l->cap == 0 ? 8 : l->cap * 2;
1756 1           pdfmake_annot_text_t *n = realloc(l->items, ncap * sizeof(*n));
1757 1 50         if (!n) return 0;
1758 1           l->items = n; l->cap = ncap;
1759             }
1760 3           l->items[l->len++] = *rec;
1761 3           return 1;
1762             }
1763              
1764             /* Pull a 4-element numeric array (Rect / FreeText BBox) into out[4].
1765             * Zeros the array if the entry is missing or malformed. */
1766 3           static void dict_get_rect(pdfmake_obj_t *dict, uint32_t key, double out[4]) {
1767             int i;
1768             pdfmake_obj_t *v;
1769             pdfmake_obj_t *n;
1770 3           out[0] = out[1] = out[2] = out[3] = 0;
1771 3           v = pdfmake_dict_get(dict, key);
1772 3 50         if (!v || v->kind != PDFMAKE_ARRAY) return;
    50          
1773 3 50         if (pdfmake_array_len(v) < 4) return;
1774 15 100         for (i = 0; i < 4; i++) {
1775 12           n = pdfmake_array_get(v, i);
1776 12 50         if (n) out[i] = pdfmake_get_number(n);
1777             }
1778             }
1779              
1780             /* Walk one page's /Annots array. */
1781 2           static void collect_page_annots(pdfmake_reader_t *rd,
1782             pdfmake_annot_text_list_t *out,
1783             pdfmake_reader_page_t *page,
1784             size_t page_index)
1785             {
1786             size_t i;
1787             pdfmake_arena_t *arena;
1788             uint32_t annots_key;
1789             pdfmake_obj_t *annots;
1790             uint32_t rect_key;
1791             size_t n;
1792             pdfmake_obj_t *a;
1793             uint32_t sub_key;
1794             pdfmake_obj_t *sub;
1795             const char *kind;
1796             const char *nm;
1797             const char *contents;
1798             const char *author;
1799             const char *subject;
1800             pdfmake_annot_text_t rec;
1801 3 50         if (!page || !page->page_dict) return;
    50          
1802 2           arena = rd->parser->doc->arena;
1803              
1804 2           annots_key = pdfmake_arena_intern_name(arena, "Annots", 6);
1805 2           annots = pdfmake_dict_get(page->page_dict, annots_key);
1806 2 100         if (annots && annots->kind == PDFMAKE_REF) {
    50          
1807 0           annots = pdfmake_parser_resolve(rd->parser, annots->as.ref);
1808             }
1809 2 100         if (!annots || annots->kind != PDFMAKE_ARRAY) return;
    50          
1810              
1811 1           rect_key = pdfmake_arena_intern_name(arena, "Rect", 4);
1812 1           n = pdfmake_array_len(annots);
1813 4 100         for (i = 0; i < n; i++) {
1814 3           a = pdfmake_array_get(annots, i);
1815 3 50         if (a && a->kind == PDFMAKE_REF) {
    50          
1816 3           a = pdfmake_parser_resolve(rd->parser, a->as.ref);
1817             }
1818 3 50         if (!a || a->kind != PDFMAKE_DICT) continue;
    50          
1819              
1820             /* Subtype decides the kind label; we surface every textual annot. */
1821 3           sub_key = pdfmake_arena_intern_name(arena, "Subtype", 7);
1822 3           sub = pdfmake_dict_get(a, sub_key);
1823 3           kind = "Annot";
1824 3 50         if (sub && sub->kind == PDFMAKE_NAME) {
    50          
1825 3           nm = pdfmake_get_name_bytes(arena, sub);
1826 3 50         if (nm) kind = nm;
1827             }
1828              
1829             /* Skip Link/Widget/PrinterMark etc. that never carry user text.
1830             * Widgets are surfaced via the AcroForm walker instead. */
1831 3 50         if (strcmp(kind, "Link") == 0 ||
1832 3 100         strcmp(kind, "Widget") == 0 ||
1833 2 50         strcmp(kind, "PrinterMark") == 0 ||
1834 2 50         strcmp(kind, "TrapNet") == 0) {
1835 1           continue;
1836             }
1837              
1838 2           contents = dict_get_text(rd, arena, a, "Contents");
1839 2           author = dict_get_text(rd, arena, a, "T");
1840 2           subject = dict_get_text(rd, arena, a, "Subj");
1841              
1842             /* Even with no /Contents we emit the record if there's author or
1843             * subject text — a Popup without Contents still tells you who
1844             * highlighted what. */
1845 2 50         if (!contents && !author && !subject) continue;
    0          
    0          
1846              
1847 2           memset(&rec, 0, sizeof(rec));
1848 2           rec.kind = pdfmake_arena_strdup(arena, kind);
1849 2           rec.page_index = page_index;
1850 2           dict_get_rect(a, rect_key, rec.rect);
1851 2 50         rec.text = contents ? contents : "";
1852 2           rec.author = author;
1853 2           rec.subject = subject;
1854 2           annot_list_push(out, &rec);
1855             }
1856             }
1857              
1858             /* Recursively walk /AcroForm /Fields; /Kids trees inherit qualified names
1859             * via dot-joining (§12.7.3.2). */
1860 1           static void collect_form_fields(pdfmake_reader_t *rd,
1861             pdfmake_annot_text_list_t *out,
1862             pdfmake_obj_t *field,
1863             const char *parent_name,
1864             int depth)
1865             {
1866             size_t i;
1867             size_t pi;
1868             pdfmake_arena_t *arena;
1869             const char *t;
1870             const char *full_name;
1871             size_t pl;
1872             size_t tl;
1873             char *buf;
1874             uint32_t v_key;
1875             pdfmake_obj_t *v;
1876             const char *value;
1877             const char *nm;
1878             const char *tooltip;
1879             pdfmake_annot_text_t rec;
1880             uint32_t rect_key;
1881             pdfmake_obj_t *p;
1882             uint32_t kids_key;
1883             pdfmake_obj_t *kids;
1884             size_t n;
1885             pdfmake_obj_t *kid;
1886 1 50         if (!field || depth > 32) return;
    50          
1887 1           arena = rd->parser->doc->arena;
1888 1 50         if (field->kind == PDFMAKE_REF) {
1889 1           field = pdfmake_parser_resolve(rd->parser, field->as.ref);
1890             }
1891 1 50         if (!field || field->kind != PDFMAKE_DICT) return;
    50          
1892              
1893             /* Build fully qualified name = parent.T joined with this.T */
1894 1           t = dict_get_text(rd, arena, field, "T");
1895 1           full_name = t;
1896 1 50         if (parent_name && t) {
    0          
1897 0           pl = strlen(parent_name);
1898 0           tl = strlen(t);
1899 0           buf = pdfmake_arena_alloc(arena, pl + 1 + tl + 1);
1900 0 0         if (buf) {
1901 0           memcpy(buf, parent_name, pl);
1902 0           buf[pl] = '.';
1903 0           memcpy(buf + pl + 1, t, tl);
1904 0           buf[pl + 1 + tl] = 0;
1905 0           full_name = buf;
1906             }
1907 1 50         } else if (parent_name && !t) {
    0          
1908 0           full_name = parent_name;
1909             }
1910              
1911             /* Emit a record for leaf fields that carry a /V (value). /V can be a
1912             * string (text, choice) or a name (button state). Skip anything else. */
1913 1           v_key = pdfmake_arena_intern_name(arena, "V", 1);
1914 1           v = pdfmake_dict_get(field, v_key);
1915 1 50         if (v && v->kind == PDFMAKE_REF) {
    50          
1916 0           v = pdfmake_parser_resolve(rd->parser, v->as.ref);
1917             }
1918 1           value = NULL;
1919 1 50         if (v) {
1920 1 50         if (v->kind == PDFMAKE_STR) {
1921 1           value = decode_pdf_text(arena, v->as.str.bytes, v->as.str.len);
1922 0 0         } else if (v->kind == PDFMAKE_NAME) {
1923 0           nm = pdfmake_get_name_bytes(arena, v);
1924 0 0         if (nm) value = pdfmake_arena_strdup(arena, nm);
1925             }
1926             }
1927              
1928 1           tooltip = dict_get_text(rd, arena, field, "TU");
1929              
1930             /* Fields may be pure intermediaries (have /Kids but no /V/T/TU). We
1931             * only emit when there's something to surface. */
1932 1 50         if (value || tooltip) {
    0          
1933 1           memset(&rec, 0, sizeof(rec));
1934 1           rec.kind = "FormField";
1935 1           rec.page_index = (size_t)-1; /* not page-anchored by default */
1936 1 50         rec.text = value ? value : "";
1937 1           rec.subject = tooltip;
1938 1           rec.field_name = full_name;
1939              
1940             /* If this field is a single-widget field (/Subtype /Widget) it
1941             * carries a /Rect; otherwise rect stays zero. */
1942 1           rect_key = pdfmake_arena_intern_name(arena, "Rect", 4);
1943 1           dict_get_rect(field, rect_key, rec.rect);
1944              
1945             /* /P may point at the owning page — use it to stamp page_index. */
1946 1           p = dict_get_resolved(rd, arena, field, "P");
1947 1 50         if (p) {
1948 1 50         for (pi = 0; pi < rd->page_count; pi++) {
1949 1 50         if (rd->pages[pi].page_dict == p) {
1950 1           rec.page_index = pi;
1951 1           break;
1952             }
1953             }
1954             }
1955              
1956 1           annot_list_push(out, &rec);
1957             }
1958              
1959 1           kids_key = pdfmake_arena_intern_name(arena, "Kids", 4);
1960 1           kids = pdfmake_dict_get(field, kids_key);
1961 1 50         if (kids && kids->kind == PDFMAKE_REF) {
    0          
1962 0           kids = pdfmake_parser_resolve(rd->parser, kids->as.ref);
1963             }
1964 1 50         if (kids && kids->kind == PDFMAKE_ARRAY) {
    0          
1965 0           n = pdfmake_array_len(kids);
1966 0 0         for (i = 0; i < n; i++) {
1967 0           kid = pdfmake_array_get(kids, i);
1968 0           collect_form_fields(rd, out, kid, full_name, depth + 1);
1969             }
1970             }
1971             }
1972              
1973 2           pdfmake_err_t pdfmake_textract_annotations(
1974             pdfmake_reader_t *reader,
1975             pdfmake_annot_text_list_t *out)
1976             {
1977             size_t i;
1978             pdfmake_arena_t *arena;
1979             uint32_t af_key;
1980             pdfmake_obj_t *af;
1981             uint32_t fields_key;
1982             pdfmake_obj_t *fields;
1983             size_t n;
1984 2 50         if (!reader || !out) return PDFMAKE_EINVAL;
    50          
1985 2 50         if (!reader->parser || !reader->parser->doc) return PDFMAKE_EINVAL;
    50          
1986              
1987 2           arena = reader->parser->doc->arena;
1988              
1989             /* Per-page /Annots */
1990 4 100         for (i = 0; i < reader->page_count; i++) {
1991 2           collect_page_annots(reader, out, &reader->pages[i], i);
1992             }
1993              
1994             /* Document-level /AcroForm /Fields */
1995 2 50         if (reader->catalog) {
1996 2           af_key = pdfmake_arena_intern_name(arena, "AcroForm", 8);
1997 2           af = pdfmake_dict_get(reader->catalog, af_key);
1998 2 100         if (af && af->kind == PDFMAKE_REF) {
    50          
1999 1           af = pdfmake_parser_resolve(reader->parser, af->as.ref);
2000             }
2001 2 100         if (af && af->kind == PDFMAKE_DICT) {
    50          
2002 1           fields_key = pdfmake_arena_intern_name(arena, "Fields", 6);
2003 1           fields = pdfmake_dict_get(af, fields_key);
2004 1 50         if (fields && fields->kind == PDFMAKE_REF) {
    50          
2005 0           fields = pdfmake_parser_resolve(reader->parser, fields->as.ref);
2006             }
2007 1 50         if (fields && fields->kind == PDFMAKE_ARRAY) {
    50          
2008 1           n = pdfmake_array_len(fields);
2009 2 100         for (i = 0; i < n; i++) {
2010 1           collect_form_fields(reader, out,
2011             pdfmake_array_get(fields, i), NULL, 0);
2012             }
2013             }
2014             }
2015             }
2016              
2017 2           return PDFMAKE_OK;
2018             }
2019              
2020             /* Forward declaration — definition appears later under UTF-8 output. */
2021             static size_t encode_utf8(uint32_t cp, char *buf);
2022              
2023             /*============================================================================
2024             * Phase 15 — Table detection
2025             *
2026             * A page's words already come out sorted into rows via the aggregator, so
2027             * detection reduces to finding a maximal run of contiguous rows whose
2028             * column x-positions align within a small tolerance. Cells are then
2029             * populated by assigning each word to the column whose center it's closest
2030             * to, and joining the texts of any words that land in the same cell.
2031             *==========================================================================*/
2032              
2033 2           pdfmake_textract_table_opts_t pdfmake_textract_table_default_opts(void) {
2034             pdfmake_textract_table_opts_t o;
2035 2           o.min_rows = 3;
2036 2           o.min_cols = 2;
2037 2           o.x_tolerance = 5.0;
2038 2           o.row_tolerance = 0.5;
2039 2           return o;
2040             }
2041              
2042 2           pdfmake_textract_table_list_t *pdfmake_textract_table_list_new(pdfmake_arena_t *arena) {
2043 2           pdfmake_textract_table_list_t *l = calloc(1, sizeof(*l));
2044 2 50         if (l) l->arena = arena;
2045 2           return l;
2046             }
2047              
2048 2           void pdfmake_textract_table_list_free(pdfmake_textract_table_list_t *list) {
2049             size_t i;
2050 2 50         if (!list) return;
2051 3 100         for (i = 0; i < list->len; i++) {
2052 1           free(list->items[i].cells);
2053 1           free(list->items[i].cell_x0);
2054 1           free(list->items[i].cell_y0);
2055 1           free(list->items[i].cell_x1);
2056 1           free(list->items[i].cell_y1);
2057             }
2058 2           free(list->items);
2059 2           free(list);
2060             }
2061              
2062             /* A logical table row built from raw glyphs. Each entry is one cell
2063             * (= a group of contiguous glyphs with no large x-gap between them). */
2064             typedef struct {
2065             double x0, x1; /* cell horizontal extent */
2066             double y0, y1; /* cell vertical extent */
2067             char *text; /* arena-owned UTF-8 */
2068             } tcell_t;
2069              
2070             typedef struct {
2071             tcell_t *cells;
2072             size_t ncells;
2073             size_t cap;
2074             double baseline;
2075             double font_size;
2076             } trow_t;
2077              
2078 17           static void trow_push(trow_t *r, const tcell_t *c) {
2079 17 100         if (r->ncells >= r->cap) {
2080 9 50         size_t nc = r->cap == 0 ? 4 : r->cap * 2;
2081 9           tcell_t *n = realloc(r->cells, nc * sizeof(*n));
2082 9 50         if (!n) return;
2083 9           r->cells = n; r->cap = nc;
2084             }
2085 17           r->cells[r->ncells++] = *c;
2086             }
2087              
2088             /* Sort raw glyphs by y desc, then x asc — independent of the column-split
2089             * based sort used by the main aggregator. */
2090 591           static int cmp_glyph_row_major(const void *a, const void *b) {
2091 591           const pdfmake_text_glyph_t *ga = a;
2092 591           const pdfmake_text_glyph_t *gb = b;
2093             double dy;
2094             double tol;
2095             double dx;
2096 591 50         if (ga->vertical != gb->vertical) return ga->vertical ? 1 : -1;
    0          
2097 591           dy = gb->y0 - ga->y0;
2098 591 50         tol = (ga->font_size > 0 ? ga->font_size : 10.0) * 0.3;
2099 591 100         if (fabs(dy) > tol) return dy > 0 ? 1 : -1;
    100          
2100 389           dx = ga->x0 - gb->x0;
2101 389 50         if (dx < 0) return -1;
2102 0 0         if (dx > 0) return 1;
2103 0           return 0;
2104             }
2105              
2106 2           pdfmake_err_t pdfmake_textract_detect_tables(
2107             const pdfmake_textract_result_t *result,
2108             const pdfmake_textract_table_opts_t *options,
2109             pdfmake_textract_table_list_t *out)
2110             {
2111             size_t i;
2112             size_t j;
2113             size_t k;
2114             size_t r;
2115             size_t c;
2116             size_t m;
2117             pdfmake_textract_table_opts_t opts;
2118             pdfmake_text_glyph_t *glyphs;
2119             size_t row_cap;
2120             size_t nrows;
2121             trow_t *rows;
2122             size_t gi;
2123             double ry;
2124             double rfs;
2125             size_t rend;
2126             size_t nc;
2127             trow_t *tmp;
2128             trow_t *rrow;
2129             size_t cstart;
2130             int split;
2131             double gap;
2132             tcell_t cell;
2133             pdfmake_buf_t tb;
2134             char utf8_tmp[4];
2135             size_t n;
2136             size_t ncols;
2137             int ok;
2138             double dx;
2139             size_t run;
2140             pdfmake_textract_table_t t;
2141             const trow_t *rp;
2142             const tcell_t *cellp;
2143             size_t idx;
2144             pdfmake_textract_table_t *nt;
2145 2 50         if (!result || !out) return PDFMAKE_EINVAL;
    50          
2146 2 50         opts = options ? *options : pdfmake_textract_table_default_opts();
2147 2 50         if (result->raw_len == 0) return PDFMAKE_OK;
2148              
2149             /* Work on a private copy of the raw glyphs so we can re-sort without
2150             * touching the main extraction state (which was sorted by the Phase 10
2151             * column-aware comparator that would split rows across columns). */
2152 2           glyphs = malloc(result->raw_len * sizeof(*glyphs));
2153 2 50         if (!glyphs) return PDFMAKE_ENOMEM;
2154 2           memcpy(glyphs, result->raw_glyphs, result->raw_len * sizeof(*glyphs));
2155 2           qsort(glyphs, result->raw_len, sizeof(*glyphs), cmp_glyph_row_major);
2156              
2157             /* 1) Cluster glyphs into rows by y. Within a row, group consecutive
2158             * glyphs into cells split on "big" horizontal gaps (≥ 1.5 em).
2159             * That keeps "Apple" together while splitting it from "12" sitting
2160             * at the next column origin. */
2161 2           row_cap = 64;
2162 2           nrows = 0;
2163 2           rows = calloc(row_cap, sizeof(trow_t));
2164 2 50         if (!rows) { free(glyphs); return PDFMAKE_ENOMEM; }
2165              
2166 2           gi = 0;
2167 11 100         while (gi < result->raw_len) {
2168 9           ry = glyphs[gi].y0;
2169 9 50         rfs = glyphs[gi].font_size > 0 ? glyphs[gi].font_size : 10.0;
2170 9 50         if (glyphs[gi].vertical) { gi++; continue; }
2171              
2172             /* Rows go until baseline jump > row_tolerance × fs. */
2173 9           rend = gi + 1;
2174 176 100         while (rend < result->raw_len && !glyphs[rend].vertical &&
    50          
2175 174 100         fabs(glyphs[rend].y0 - ry) <= rfs * opts.row_tolerance) {
2176 167           rend++;
2177             }
2178              
2179             /* Grow rows */
2180 9 50         if (nrows >= row_cap) {
2181 0           nc = row_cap * 2;
2182 0           tmp = realloc(rows, nc * sizeof(trow_t));
2183 0 0         if (!tmp) { free(rows); free(glyphs); return PDFMAKE_ENOMEM; }
2184 0           rows = tmp;
2185 0           memset(rows + row_cap, 0, (nc - row_cap) * sizeof(trow_t));
2186 0           row_cap = nc;
2187             }
2188 9           rrow = &rows[nrows++];
2189 9           rrow->baseline = ry;
2190 9           rrow->font_size = rfs;
2191              
2192             /* Walk glyphs[gi..rend-1] and split into cells on large x gaps. */
2193 9           cstart = gi;
2194 185 100         for (k = gi + 1; k <= rend; k++) {
2195 176           split = (k == rend);
2196 176 100         if (!split) {
2197 167           gap = glyphs[k].x0 - glyphs[k - 1].x1;
2198 167 100         if (gap > 1.5 * rfs) split = 1;
2199             }
2200 176 100         if (split) {
2201             /* Emit a cell from cstart..k-1 */
2202 17           memset(&cell, 0, sizeof(cell));
2203 17           pdfmake_buf_init(&tb);
2204 17           cell.x0 = glyphs[cstart].x0;
2205 17           cell.x1 = glyphs[k - 1].x1;
2206 17           cell.y0 = glyphs[cstart].y0;
2207 17           cell.y1 = glyphs[cstart].y1;
2208 193 100         for (j = cstart; j < k; j++) {
2209 176           n = encode_utf8(glyphs[j].unicode, utf8_tmp);
2210 176 50         if (glyphs[j].x0 < cell.x0) cell.x0 = glyphs[j].x0;
2211 176 50         if (glyphs[j].x1 > cell.x1) cell.x1 = glyphs[j].x1;
2212 176 50         if (glyphs[j].y0 < cell.y0) cell.y0 = glyphs[j].y0;
2213 176 50         if (glyphs[j].y1 > cell.y1) cell.y1 = glyphs[j].y1;
2214 352 100         for (m = 0; m < n; m++)
2215 176           pdfmake_buf_append_byte(&tb, utf8_tmp[m]);
2216             }
2217 17           cell.text = pdfmake_arena_alloc(out->arena, tb.len + 1);
2218 17 50         if (cell.text) { memcpy(cell.text, tb.data, tb.len); cell.text[tb.len] = 0; }
2219 17           pdfmake_buf_free(&tb);
2220 17           trow_push(rrow, &cell);
2221 17           cstart = k;
2222             }
2223             }
2224 9           gi = rend;
2225             }
2226              
2227             /* 2) Find maximal runs of consecutive rows with matching column layout.
2228             * "Same layout" = same cell count AND every cell-origin within
2229             * opts.x_tolerance. */
2230 2           i = 0;
2231 8 100         while (i < nrows) {
2232 6 100         if (rows[i].ncells < opts.min_cols) { i++; continue; }
2233 1           ncols = rows[i].ncells;
2234              
2235 1           j = i + 1;
2236 4 50         while (j < nrows) {
2237 4 100         if (rows[j].ncells != ncols) break;
2238 3           ok = 1;
2239 12 100         for (c = 0; c < ncols; c++) {
2240 9           dx = rows[i].cells[c].x0 - rows[j].cells[c].x0;
2241 9 50         if (dx < 0) dx = -dx;
2242 9 50         if (dx > opts.x_tolerance) { ok = 0; break; }
2243             }
2244 3 50         if (!ok) break;
2245 3           j++;
2246             }
2247              
2248 1           run = j - i;
2249 1 50         if (run < opts.min_rows) { i = j; continue; }
2250              
2251             /* 3) Emit the table. */
2252 1           memset(&t, 0, sizeof(t));
2253 1           t.rows = run;
2254 1           t.cols = ncols;
2255 1           t.cells = calloc(run * ncols, sizeof(*t.cells));
2256 1           t.cell_x0 = calloc(run * ncols, sizeof(double));
2257 1           t.cell_y0 = calloc(run * ncols, sizeof(double));
2258 1           t.cell_x1 = calloc(run * ncols, sizeof(double));
2259 1           t.cell_y1 = calloc(run * ncols, sizeof(double));
2260 1 50         if (!t.cells || !t.cell_x0 || !t.cell_y0 || !t.cell_x1 || !t.cell_y1) {
    50          
    50          
    50          
    50          
2261 0           free(t.cells); free(t.cell_x0); free(t.cell_y0);
2262 0           free(t.cell_x1); free(t.cell_y1);
2263 0 0         for (k = 0; k < nrows; k++) free(rows[k].cells);
2264 0           free(rows); free(glyphs);
2265 0           return PDFMAKE_ENOMEM;
2266             }
2267              
2268 1           t.x0 = t.y0 = 1e18; t.x1 = t.y1 = -1e18;
2269 5 100         for (r = 0; r < run; r++) {
2270 4           rp = &rows[i + r];
2271 16 100         for (c = 0; c < ncols; c++) {
2272 12           cellp = &rp->cells[c];
2273 12           idx = r * ncols + c;
2274 12 50         t.cells[idx] = cellp->text ? cellp->text : "";
2275 12           t.cell_x0[idx] = cellp->x0;
2276 12           t.cell_y0[idx] = cellp->y0;
2277 12           t.cell_x1[idx] = cellp->x1;
2278 12           t.cell_y1[idx] = cellp->y1;
2279 12 100         if (cellp->x0 < t.x0) t.x0 = cellp->x0;
2280 12 100         if (cellp->y0 < t.y0) t.y0 = cellp->y0;
2281 12 100         if (cellp->x1 > t.x1) t.x1 = cellp->x1;
2282 12 100         if (cellp->y1 > t.y1) t.y1 = cellp->y1;
2283             }
2284             }
2285              
2286 1 50         if (out->len >= out->cap) {
2287 1 50         nc = out->cap == 0 ? 4 : out->cap * 2;
2288 1           nt = realloc(out->items, nc * sizeof(*nt));
2289 1 50         if (!nt) {
2290 0           free(t.cells); free(t.cell_x0); free(t.cell_y0);
2291 0           free(t.cell_x1); free(t.cell_y1);
2292 0 0         for (k = 0; k < nrows; k++) free(rows[k].cells);
2293 0           free(rows); free(glyphs);
2294 0           return PDFMAKE_ENOMEM;
2295             }
2296 1           out->items = nt; out->cap = nc;
2297             }
2298 1           out->items[out->len++] = t;
2299              
2300 1           i = j;
2301             }
2302              
2303 11 100         for (k = 0; k < nrows; k++) free(rows[k].cells);
2304 2           free(rows);
2305 2           free(glyphs);
2306 2           return PDFMAKE_OK;
2307             }
2308              
2309             /*============================================================================
2310             * UTF-8 output
2311             *==========================================================================*/
2312              
2313             /* Encode a single Unicode codepoint to UTF-8, return bytes written */
2314 1158           static size_t encode_utf8(uint32_t cp, char *buf) {
2315 1158 100         if (cp < 0x80) {
2316 1152           buf[0] = (char)cp;
2317 1152           return 1;
2318 6 50         } else if (cp < 0x800) {
2319 0           buf[0] = (char)(0xC0 | (cp >> 6));
2320 0           buf[1] = (char)(0x80 | (cp & 0x3F));
2321 0           return 2;
2322 6 50         } else if (cp < 0x10000) {
2323 6           buf[0] = (char)(0xE0 | (cp >> 12));
2324 6           buf[1] = (char)(0x80 | ((cp >> 6) & 0x3F));
2325 6           buf[2] = (char)(0x80 | (cp & 0x3F));
2326 6           return 3;
2327 0 0         } else if (cp < 0x110000) {
2328 0           buf[0] = (char)(0xF0 | (cp >> 18));
2329 0           buf[1] = (char)(0x80 | ((cp >> 12) & 0x3F));
2330 0           buf[2] = (char)(0x80 | ((cp >> 6) & 0x3F));
2331 0           buf[3] = (char)(0x80 | (cp & 0x3F));
2332 0           return 4;
2333             }
2334 0           return 0;
2335             }
2336              
2337 4           size_t pdfmake_textract_to_utf8(
2338             const pdfmake_textract_result_t *result,
2339             char *buf, size_t buf_cap)
2340             {
2341             size_t bi;
2342             size_t li;
2343             size_t wi;
2344             size_t gi;
2345             size_t k;
2346             size_t written;
2347             const pdfmake_text_block_t *block;
2348             const pdfmake_text_line_t *line;
2349             const pdfmake_text_word_t *word;
2350             size_t n;
2351             char tmp[4];
2352 4 50         if (!result) return 0;
2353              
2354 4           written = 0;
2355              
2356 19 100         for (bi = 0; bi < result->len; bi++) {
2357 15           block = &result->blocks[bi];
2358 37 100         for (li = 0; li < block->len; li++) {
2359 22           line = &block->lines[li];
2360 216 100         for (wi = 0; wi < line->len; wi++) {
2361 194           word = &line->words[wi];
2362 194 100         if (wi > 0 && written < buf_cap) {
    50          
2363 172           buf[written++] = ' '; /* space between words */
2364             }
2365 1176 100         for (gi = 0; gi < word->len; gi++) {
2366 982           n = encode_utf8(word->glyphs[gi].unicode, tmp);
2367 1976 100         for (k = 0; k < n && written < buf_cap; k++) {
    50          
2368 994           buf[written++] = tmp[k];
2369             }
2370             }
2371             }
2372             /* Newline between lines */
2373 22 100         if (li + 1 < block->len && written < buf_cap) {
    50          
2374 7           buf[written++] = '\n';
2375             }
2376             }
2377             /* Double newline between blocks */
2378 15 100         if (bi + 1 < result->len && written + 1 < buf_cap) {
    50          
2379 11           buf[written++] = '\n';
2380 11           buf[written++] = '\n';
2381             }
2382             }
2383              
2384             /* Null-terminate if room */
2385 4 50         if (written < buf_cap) buf[written] = '\0';
2386              
2387 4           return written;
2388             }