File Coverage

src/pdfmake_reader.c
Criterion Covered Total %
statement 322 385 83.6
branch 192 328 58.5
condition n/a
subroutine n/a
pod n/a
total 514 713 72.0


line stmt bran cond sub pod time code
1             /*
2             * pdfmake_reader.c — Document reader implementation
3             *
4             * Flattens page tree, resolves inheritable attributes, extracts content streams.
5             */
6              
7             #include "pdfmake_reader.h"
8             #include "pdfmake_parser.h"
9             #include "pdfmake_filter.h"
10             #include "pdfmake_arena.h"
11              
12             #include
13             #include
14             #include
15              
16             /*----------------------------------------------------------------------------
17             * Internal helpers
18             *--------------------------------------------------------------------------*/
19              
20             /* Set error message */
21 0           static void reader_set_error(pdfmake_reader_t *reader, pdfmake_err_t err, const char *msg) {
22 0           reader->last_err = err;
23 0           snprintf(reader->err_msg, sizeof(reader->err_msg), "%s", msg);
24 0           }
25              
26             /* Resolve indirect reference if needed */
27 1973           static pdfmake_obj_t *resolve_ref(pdfmake_reader_t *reader, pdfmake_obj_t *obj) {
28 1973 100         if (!obj) return NULL;
29 1447 100         if (obj->kind == PDFMAKE_REF) {
30 481           return pdfmake_parser_resolve(reader->parser, obj->as.ref);
31             }
32 966           return obj;
33             }
34              
35             /* Get dict entry by C string key */
36 1848           static pdfmake_obj_t *dict_get_cstr(pdfmake_reader_t *reader, pdfmake_obj_t *dict, const char *key) {
37             uint32_t key_id;
38 1848 50         if (!dict || dict->kind != PDFMAKE_DICT) return NULL;
    50          
39 1848           key_id = pdfmake_arena_intern_name(reader->doc->arena, key, strlen(key));
40 1848 50         if (key_id == 0) return NULL;
41 1848           return pdfmake_dict_get(dict, key_id);
42             }
43              
44             /* Get name value as C string (from interned id) */
45 265           static const char *name_cstr(pdfmake_reader_t *reader, pdfmake_obj_t *obj) {
46 265 50         if (!obj || obj->kind != PDFMAKE_NAME) return NULL;
    50          
47 265           return pdfmake_arena_name_bytes(reader->doc->arena, obj->as.name.id);
48             }
49              
50             /* Parse a rectangle array [llx, lly, urx, ury] */
51 51           static int parse_rect(pdfmake_reader_t *reader, pdfmake_obj_t *arr, double out[4]) {
52             int i;
53 51 50         if (!arr || arr->kind != PDFMAKE_ARRAY) return 0;
    50          
54 51 50         if (pdfmake_array_len(arr) < 4) return 0;
55            
56 255 100         for (i = 0; i < 4; i++) {
57 204           pdfmake_obj_t *item = pdfmake_array_get(arr, i);
58 204           item = resolve_ref(reader, item);
59 204 50         if (!item) return 0;
60            
61 204 50         if (item->kind == PDFMAKE_INT) {
62 204           out[i] = (double)item->as.i;
63 0 0         } else if (item->kind == PDFMAKE_REAL) {
64 0           out[i] = item->as.r;
65             } else {
66 0           return 0;
67             }
68             }
69 51           return 1;
70             }
71              
72             /*----------------------------------------------------------------------------
73             * Cycle detection for page tree traversal
74             *--------------------------------------------------------------------------*/
75              
76             typedef struct {
77             pdfmake_obj_t **visited;
78             size_t count;
79             size_t cap;
80             } cycle_detector_t;
81              
82 96           static void cycle_detector_init(cycle_detector_t *cd) {
83 96           cd->visited = NULL;
84 96           cd->count = 0;
85 96           cd->cap = 0;
86 96           }
87              
88 96           static void cycle_detector_free(cycle_detector_t *cd) {
89 96           free(cd->visited);
90 96           cd->visited = NULL;
91 96           cd->count = 0;
92 96           cd->cap = 0;
93 96           }
94              
95 265           static int cycle_detector_check(cycle_detector_t *cd, pdfmake_obj_t *node) {
96             size_t i;
97             /* Check if we've seen this node before */
98 574 100         for (i = 0; i < cd->count; i++) {
99 309 50         if (cd->visited[i] == node) {
100 0           return 1; /* Cycle detected */
101             }
102             }
103            
104             /* Add node to visited list */
105 265 100         if (cd->count >= cd->cap) {
106 96 50         size_t new_cap = cd->cap ? cd->cap * 2 : 16;
107 96           pdfmake_obj_t **new_visited = realloc(cd->visited, new_cap * sizeof(*cd->visited));
108 96 50         if (!new_visited) return -1; /* Allocation failure */
109 96           cd->visited = new_visited;
110 96           cd->cap = new_cap;
111             }
112 265           cd->visited[cd->count++] = node;
113 265           return 0; /* No cycle */
114             }
115              
116             /*----------------------------------------------------------------------------
117             * Page tree flattening
118             *--------------------------------------------------------------------------*/
119              
120             /* Forward declaration */
121             static pdfmake_err_t flatten_pages_recursive(pdfmake_reader_t *reader,
122             pdfmake_obj_t *node,
123             cycle_detector_t *cd);
124              
125             /* Add a page to the reader's page list */
126 169           static pdfmake_err_t add_page(pdfmake_reader_t *reader, pdfmake_obj_t *page_dict) {
127             pdfmake_reader_page_t *page;
128 169 100         if (reader->page_count >= reader->page_cap) {
129 96 50         size_t new_cap = reader->page_cap ? reader->page_cap * 2 : 16;
130 96           pdfmake_reader_page_t *new_pages = realloc(reader->pages,
131             new_cap * sizeof(*reader->pages));
132 96 50         if (!new_pages) {
133 0           reader_set_error(reader, PDFMAKE_ENOMEM, "Failed to allocate pages array");
134 0           return PDFMAKE_ENOMEM;
135             }
136 96           reader->pages = new_pages;
137 96           reader->page_cap = new_cap;
138             }
139            
140 169           page = &reader->pages[reader->page_count++];
141 169           memset(page, 0, sizeof(*page));
142 169           page->page_dict = page_dict;
143            
144 169           return PDFMAKE_OK;
145             }
146              
147             /* Recursively flatten pages tree */
148 265           static pdfmake_err_t flatten_pages_recursive(pdfmake_reader_t *reader,
149             pdfmake_obj_t *node,
150             cycle_detector_t *cd) {
151             int cycle_result;
152             pdfmake_obj_t *type_obj;
153             const char *type_name;
154             pdfmake_obj_t *kids;
155             size_t kids_len;
156             size_t i;
157              
158 265 50         if (!node) {
159 0           reader_set_error(reader, PDFMAKE_EBADPAGE, "Null page tree node");
160 0           return PDFMAKE_EBADPAGE;
161             }
162            
163             /* Resolve indirect reference */
164 265           node = resolve_ref(reader, node);
165 265 50         if (!node || node->kind != PDFMAKE_DICT) {
    50          
166 0           reader_set_error(reader, PDFMAKE_EBADPAGE, "Page tree node is not a dictionary");
167 0           return PDFMAKE_EBADPAGE;
168             }
169            
170             /* Check for cycles */
171 265           cycle_result = cycle_detector_check(cd, node);
172 265 50         if (cycle_result < 0) {
173 0           reader_set_error(reader, PDFMAKE_ENOMEM, "Cycle detector allocation failed");
174 0           return PDFMAKE_ENOMEM;
175             }
176 265 50         if (cycle_result > 0) {
177 0           reader_set_error(reader, PDFMAKE_ECYCLE_PAGE, "Cycle detected in page tree");
178 0           return PDFMAKE_ECYCLE_PAGE;
179             }
180            
181             /* Get /Type to determine if this is a /Page or /Pages node */
182 265           type_obj = dict_get_cstr(reader, node, "Type");
183 265           type_obj = resolve_ref(reader, type_obj);
184 265           type_name = name_cstr(reader, type_obj);
185            
186 265 50         if (!type_name) {
187 0           reader_set_error(reader, PDFMAKE_EBADPAGE, "Page tree node missing /Type");
188 0           return PDFMAKE_EBADPAGE;
189             }
190            
191 265 100         if (strcmp(type_name, "Page") == 0) {
192             /* Leaf page — add to list */
193 169           return add_page(reader, node);
194 96 50         } else if (strcmp(type_name, "Pages") == 0) {
195             /* Intermediate node — recurse into /Kids */
196 96           kids = dict_get_cstr(reader, node, "Kids");
197 96           kids = resolve_ref(reader, kids);
198            
199 96 50         if (!kids || kids->kind != PDFMAKE_ARRAY) {
    50          
200 0           reader_set_error(reader, PDFMAKE_EBADPAGE, "/Pages node missing /Kids array");
201 0           return PDFMAKE_EBADPAGE;
202             }
203            
204 96           kids_len = pdfmake_array_len(kids);
205 265 100         for (i = 0; i < kids_len; i++) {
206 169           pdfmake_obj_t *kid = pdfmake_array_get(kids, i);
207 169           pdfmake_err_t err = flatten_pages_recursive(reader, kid, cd);
208 169 50         if (err != PDFMAKE_OK) {
209 0           return err;
210             }
211             }
212 96           return PDFMAKE_OK;
213             } else {
214             char msg[128];
215 0           snprintf(msg, sizeof(msg), "Unknown page tree node type: %s", type_name);
216 0           reader_set_error(reader, PDFMAKE_EBADPAGE, msg);
217 0           return PDFMAKE_EBADPAGE;
218             }
219             }
220              
221 96           pdfmake_err_t pdfmake_reader_flatten_pages(pdfmake_reader_t *reader,
222             pdfmake_obj_t *pages_node) {
223             cycle_detector_t cd;
224             pdfmake_err_t err;
225 96           cycle_detector_init(&cd);
226            
227 96           err = flatten_pages_recursive(reader, pages_node, &cd);
228            
229 96           cycle_detector_free(&cd);
230 96           return err;
231             }
232              
233             /*----------------------------------------------------------------------------
234             * Inheritable attribute resolution
235             *--------------------------------------------------------------------------*/
236              
237 104           pdfmake_obj_t *pdfmake_reader_resolve_inheritable(pdfmake_reader_t *reader,
238             pdfmake_obj_t *page_dict,
239             const char *key) {
240 104           pdfmake_obj_t *current = page_dict;
241             pdfmake_obj_t *value;
242             pdfmake_obj_t *parent;
243            
244 174 100         while (current) {
245 140           current = resolve_ref(reader, current);
246 140 50         if (!current || current->kind != PDFMAKE_DICT) break;
    50          
247            
248             /* Check for the key on this node */
249 140           value = dict_get_cstr(reader, current, key);
250 140 100         if (value) {
251 70           return resolve_ref(reader, value);
252             }
253            
254             /* Walk up to /Parent */
255 70           parent = dict_get_cstr(reader, current, "Parent");
256 70           current = parent;
257             }
258            
259 34           return NULL; /* Not found */
260             }
261              
262             /*----------------------------------------------------------------------------
263             * Resource merging
264             *--------------------------------------------------------------------------*/
265              
266             /* Merge a single resource category (e.g., /Font, /XObject) */
267 480           static void merge_resource_category(pdfmake_reader_t *reader,
268             pdfmake_obj_t *dest,
269             pdfmake_obj_t *src,
270             const char *category) {
271             pdfmake_obj_t *src_cat;
272             pdfmake_obj_t *dest_cat;
273             pdfmake_arena_t *key_arena;
274             pdfmake_dict_iter_t iter;
275              
276 480           src_cat = dict_get_cstr(reader, src, category);
277 480           src_cat = resolve_ref(reader, src_cat);
278 480 100         if (!src_cat || src_cat->kind != PDFMAKE_DICT) return;
    100          
279            
280 104           dest_cat = dict_get_cstr(reader, dest, category);
281 104           dest_cat = resolve_ref(reader, dest_cat);
282            
283             /* All name interning here uses the parser's arena so downstream
284             * consumers (interpreter, textract) get a single consistent arena. */
285 208           key_arena = reader->parser
286 104           ? reader->parser->doc->arena
287 104 50         : reader->arena;
288              
289 104 50         if (!dest_cat) {
290             uint32_t cat_key;
291             /* Category doesn't exist in dest — create it */
292 104           dest_cat = pdfmake_arena_alloc(reader->arena, sizeof(pdfmake_obj_t));
293 104 50         if (!dest_cat) return;
294 104           *dest_cat = pdfmake_dict_new(reader->arena);
295              
296 104           cat_key = pdfmake_arena_intern_name(key_arena, category, strlen(category));
297 104 50         if (cat_key) {
298 104           pdfmake_dict_set(reader->arena, dest, cat_key, *dest_cat);
299             }
300             }
301            
302             /* Copy entries from src to dest (dest wins on conflict). Keys in
303             * src_cat are interned in the parser's arena; we preserve that so all
304             * downstream consumers (interpreter, textract) can use the parser
305             * arena for consistent name-ID lookups. */
306 104           pdfmake_dict_iter_init(&iter, src_cat);
307 385 100         while (pdfmake_dict_iter_next(&iter)) {
308 177 50         if (!pdfmake_dict_has(dest_cat, iter.current_key)) {
309 177           pdfmake_dict_set(reader->arena, dest_cat, iter.current_key, *iter.current_value);
310             }
311             }
312             }
313              
314 60           pdfmake_obj_t *pdfmake_reader_merge_resources(pdfmake_reader_t *reader,
315             pdfmake_obj_t *page_dict) {
316             pdfmake_obj_t *merged;
317             pdfmake_obj_t *resource_chain[32]; /* Max depth */
318 60           size_t chain_len = 0;
319             pdfmake_obj_t *current;
320             size_t i;
321             const char **cat;
322              
323             /* Create merged resources dict */
324 60           merged = pdfmake_arena_alloc(reader->arena, sizeof(pdfmake_obj_t));
325 60 50         if (!merged) return NULL;
326 60           *merged = pdfmake_dict_new(reader->arena);
327            
328             /* Collect all resource dicts from page up to root (child first) */
329 60           current = page_dict;
330 180 100         while (current && chain_len < 32) {
    50          
331             pdfmake_obj_t *res;
332 120           current = resolve_ref(reader, current);
333 120 50         if (!current || current->kind != PDFMAKE_DICT) break;
    50          
334            
335 120           res = dict_get_cstr(reader, current, "Resources");
336 120           res = resolve_ref(reader, res);
337 120 100         if (res && res->kind == PDFMAKE_DICT) {
    50          
338 60           resource_chain[chain_len++] = res;
339             }
340            
341 120           current = dict_get_cstr(reader, current, "Parent");
342             }
343            
344             /* Merge from root (ancestors) down to leaf (child wins) */
345             {
346             static const char *categories[] = {
347             "ExtGState", "ColorSpace", "Pattern", "Shading",
348             "XObject", "Font", "Properties", "ProcSet", NULL
349             };
350              
351 120 100         for (i = chain_len; i > 0; i--) {
352 60           pdfmake_obj_t *res = resource_chain[i - 1];
353 540 100         for (cat = categories; *cat; cat++) {
354 480           merge_resource_category(reader, merged, res, *cat);
355             }
356             }
357             }
358            
359 60           return merged;
360             }
361              
362             /*----------------------------------------------------------------------------
363             * Reader lifecycle
364             *--------------------------------------------------------------------------*/
365              
366 96           pdfmake_reader_t *pdfmake_reader_new(pdfmake_parser_t *parser) {
367             pdfmake_reader_t *reader;
368 96 50         if (!parser) return NULL;
369            
370 96           reader = calloc(1, sizeof(*reader));
371 96 50         if (!reader) return NULL;
372            
373 96           reader->parser = parser;
374 96           reader->doc = NULL; /* Will be set by pdfmake_parser_run */
375 96           reader->arena = pdfmake_arena_new();
376 96 50         if (!reader->arena) {
377 0           free(reader);
378 0           return NULL;
379             }
380            
381 96           return reader;
382             }
383              
384 96           void pdfmake_reader_free(pdfmake_reader_t *reader) {
385 96 50         if (!reader) return;
386            
387 96           free(reader->pages);
388 96           pdfmake_arena_free(reader->arena);
389 96           free(reader);
390             }
391              
392 96           pdfmake_err_t pdfmake_reader_init(pdfmake_reader_t *reader) {
393             pdfmake_doc_t *doc;
394             pdfmake_err_t err;
395             pdfmake_ref_t root_ref;
396             pdfmake_obj_t *pages_ref;
397             pdfmake_ref_t enc_ref;
398             pdfmake_obj_t *enc_dict;
399              
400 96 50         if (!reader || !reader->parser) {
    50          
401 0           return PDFMAKE_EINVAL;
402             }
403            
404             /* Get document from parser */
405             /* Note: parser should have already parsed the document */
406 96           doc = NULL;
407 96           err = pdfmake_parser_run(reader->parser, &doc);
408 96 50         if (err != PDFMAKE_OK) {
409 0           reader_set_error(reader, err, "Parser failed");
410 0           return err;
411             }
412 96           reader->doc = doc;
413            
414             /* Get catalog from /Root */
415 96 50         if (reader->parser->root_num == 0) {
416 0           reader_set_error(reader, PDFMAKE_ENOROOT, "Missing /Root in trailer");
417 0           return PDFMAKE_ENOROOT;
418             }
419            
420 96           root_ref.num = reader->parser->root_num;
421 96           root_ref.gen = reader->parser->root_gen;
422 96           reader->catalog = pdfmake_parser_resolve(reader->parser, root_ref);
423 96 50         if (!reader->catalog || reader->catalog->kind != PDFMAKE_DICT) {
    50          
424 0           reader_set_error(reader, PDFMAKE_ENOROOT, "Invalid /Root catalog");
425 0           return PDFMAKE_ENOROOT;
426             }
427            
428             /* Get /Pages from catalog */
429 96           pages_ref = dict_get_cstr(reader, reader->catalog, "Pages");
430 96 50         if (!pages_ref) {
431 0           reader_set_error(reader, PDFMAKE_ENOPAGES, "Missing /Pages in catalog");
432 0           return PDFMAKE_ENOPAGES;
433             }
434            
435             /* Flatten the page tree */
436 96           err = pdfmake_reader_flatten_pages(reader, pages_ref);
437 96 50         if (err != PDFMAKE_OK) {
438 0           return err;
439             }
440              
441             /* Set up decryption if /Encrypt is present */
442 96 100         if (reader->parser->encrypt_num > 0) {
443 25           reader->encrypted = 1;
444              
445             /* Resolve the /Encrypt dictionary */
446 25           enc_ref.num = reader->parser->encrypt_num;
447 25           enc_ref.gen = reader->parser->encrypt_gen;
448 25           enc_dict = pdfmake_parser_resolve(reader->parser, enc_ref);
449 25 50         if (enc_dict && enc_dict->kind == PDFMAKE_DICT) {
    50          
450 25           pdfmake_obj_t *V_obj = dict_get_cstr(reader, enc_dict, "V");
451 25           pdfmake_obj_t *R_obj = dict_get_cstr(reader, enc_dict, "R");
452 25           pdfmake_obj_t *O_obj = dict_get_cstr(reader, enc_dict, "O");
453 25           pdfmake_obj_t *U_obj = dict_get_cstr(reader, enc_dict, "U");
454 25           pdfmake_obj_t *P_obj = dict_get_cstr(reader, enc_dict, "P");
455 25           pdfmake_obj_t *len_obj = dict_get_cstr(reader, enc_dict, "Length");
456              
457             /* Optional AES-256 fields */
458 25           pdfmake_obj_t *OE_obj = dict_get_cstr(reader, enc_dict, "OE");
459 25           pdfmake_obj_t *UE_obj = dict_get_cstr(reader, enc_dict, "UE");
460 25           pdfmake_obj_t *Perms_obj = dict_get_cstr(reader, enc_dict, "Perms");
461 25           pdfmake_obj_t *em_obj = dict_get_cstr(reader, enc_dict, "EncryptMetadata");
462              
463 25 50         int V = (V_obj && V_obj->kind == PDFMAKE_INT) ? (int)V_obj->as.i : 0;
    50          
464 25 50         int R = (R_obj && R_obj->kind == PDFMAKE_INT) ? (int)R_obj->as.i : 0;
    50          
465 9 50         int key_length = (len_obj && len_obj->kind == PDFMAKE_INT)
466 34 100         ? (int)len_obj->as.i : 40;
467 25 50         int32_t P = (P_obj && P_obj->kind == PDFMAKE_INT)
468 50 50         ? (int32_t)P_obj->as.i : 0;
469 25           int encrypt_metadata = 1;
470             const uint8_t *O;
471             size_t O_len;
472             const uint8_t *U;
473             size_t U_len;
474 25           const uint8_t *OE = NULL; size_t OE_len = 0;
475 25           const uint8_t *UE = NULL; size_t UE_len = 0;
476 25           const uint8_t *Perms = NULL; size_t Perms_len = 0;
477              
478 25 50         if (em_obj && em_obj->kind == PDFMAKE_BOOL && !em_obj->as.b)
    0          
    0          
479 0           encrypt_metadata = 0;
480              
481 25 50         O = (O_obj && O_obj->kind == PDFMAKE_STR) ? O_obj->as.str.bytes : NULL;
    50          
482 25 50         O_len = (O_obj && O_obj->kind == PDFMAKE_STR) ? O_obj->as.str.len : 0;
    50          
483 25 50         U = (U_obj && U_obj->kind == PDFMAKE_STR) ? U_obj->as.str.bytes : NULL;
    50          
484 25 50         U_len = (U_obj && U_obj->kind == PDFMAKE_STR) ? U_obj->as.str.len : 0;
    50          
485              
486 25 100         if (OE_obj && OE_obj->kind == PDFMAKE_STR) { OE = OE_obj->as.str.bytes; OE_len = OE_obj->as.str.len; }
    50          
487 25 100         if (UE_obj && UE_obj->kind == PDFMAKE_STR) { UE = UE_obj->as.str.bytes; UE_len = UE_obj->as.str.len; }
    50          
488 25 100         if (Perms_obj && Perms_obj->kind == PDFMAKE_STR) { Perms = Perms_obj->as.str.bytes; Perms_len = Perms_obj->as.str.len; }
    50          
489              
490 25 50         if (O && U) {
    50          
491 25           reader->crypt = pdfmake_arena_alloc(reader->arena, sizeof(pdfmake_crypt_ctx_t));
492 25 50         if (reader->crypt) {
493             int rc;
494 25           pdfmake_crypt_init(reader->crypt);
495 25           rc = pdfmake_crypt_load(reader->crypt,
496             V, R, key_length, O, O_len, U, U_len,
497             OE, OE_len, UE, UE_len, Perms, Perms_len,
498 25           P, reader->parser->doc_id, reader->parser->doc_id_len,
499             encrypt_metadata);
500 25 50         if (rc == 0) {
501             /* Try empty password first */
502 25           int auth = pdfmake_crypt_authenticate(reader->crypt, "");
503 25 100         if (auth >= 0) {
504 17           reader->authenticated = 1;
505             }
506             }
507             }
508             }
509             }
510             }
511              
512 96           return PDFMAKE_OK;
513             }
514              
515 8           int pdfmake_reader_set_password(pdfmake_reader_t *reader, const char *password) {
516             int auth;
517 8 50         if (!reader || !reader->crypt) return -1;
    50          
518 8 50         auth = pdfmake_crypt_authenticate(reader->crypt, password ? password : "");
519 8 100         if (auth >= 0) reader->authenticated = 1;
520 8           return auth;
521             }
522              
523 83           int pdfmake_reader_is_encrypted(pdfmake_reader_t *reader) {
524 83 50         return reader ? reader->encrypted : 0;
525             }
526              
527 24           int pdfmake_reader_is_authenticated(pdfmake_reader_t *reader) {
528 24 50         return reader ? reader->authenticated : 0;
529             }
530              
531 1           const char *pdfmake_reader_errmsg(pdfmake_reader_t *reader) {
532 1 50         return reader ? reader->err_msg : "NULL reader";
533             }
534              
535             /*----------------------------------------------------------------------------
536             * Page enumeration
537             *--------------------------------------------------------------------------*/
538              
539 90           size_t pdfmake_reader_page_count(pdfmake_reader_t *reader) {
540 90 50         return reader ? reader->page_count : 0;
541             }
542              
543 113           pdfmake_reader_page_t *pdfmake_reader_page_at(pdfmake_reader_t *reader, size_t idx) {
544 113 50         if (!reader || idx >= reader->page_count) return NULL;
    100          
545 112           return &reader->pages[idx];
546             }
547              
548             /*----------------------------------------------------------------------------
549             * Page attributes
550             *--------------------------------------------------------------------------*/
551              
552 54           pdfmake_err_t pdfmake_reader_page_media_box(pdfmake_reader_t *reader,
553             pdfmake_reader_page_t *page,
554             double out[4]) {
555             pdfmake_obj_t *media_box;
556              
557 54 50         if (!reader || !page || !out) return PDFMAKE_EINVAL;
    50          
    50          
558            
559             /* Check cache */
560 54 100         if (page->media_box_set) {
561 3           memcpy(out, page->media_box, sizeof(page->media_box));
562 3           return PDFMAKE_OK;
563             }
564            
565             /* Resolve inheritable /MediaBox */
566 51           media_box = pdfmake_reader_resolve_inheritable(reader,
567             page->page_dict,
568             "MediaBox");
569 51 50         if (!media_box) {
570 0           reader_set_error(reader, PDFMAKE_ENOMEDIABOX, "No MediaBox found");
571 0           return PDFMAKE_ENOMEDIABOX;
572             }
573            
574 51 50         if (!parse_rect(reader, media_box, page->media_box)) {
575 0           reader_set_error(reader, PDFMAKE_EBADPAGE, "Invalid MediaBox format");
576 0           return PDFMAKE_EBADPAGE;
577             }
578            
579 51           page->media_box_set = 1;
580 51           memcpy(out, page->media_box, sizeof(page->media_box));
581 51           return PDFMAKE_OK;
582             }
583              
584 3           pdfmake_err_t pdfmake_reader_page_crop_box(pdfmake_reader_t *reader,
585             pdfmake_reader_page_t *page,
586             double out[4]) {
587             pdfmake_obj_t *crop_box;
588             pdfmake_err_t err;
589              
590 3 50         if (!reader || !page || !out) return PDFMAKE_EINVAL;
    50          
    50          
591            
592             /* Check cache */
593 3 50         if (page->crop_box_set) {
594 0           memcpy(out, page->crop_box, sizeof(page->crop_box));
595 0           return PDFMAKE_OK;
596             }
597            
598             /* Try /CropBox first */
599 3           crop_box = pdfmake_reader_resolve_inheritable(reader,
600             page->page_dict,
601             "CropBox");
602 3 50         if (crop_box && parse_rect(reader, crop_box, page->crop_box)) {
    0          
603 0           page->crop_box_set = 1;
604 0           memcpy(out, page->crop_box, sizeof(page->crop_box));
605 0           return PDFMAKE_OK;
606             }
607            
608             /* Fall back to MediaBox */
609 3           err = pdfmake_reader_page_media_box(reader, page, page->crop_box);
610 3 50         if (err != PDFMAKE_OK) return err;
611            
612 3           page->crop_box_set = 1;
613 3           memcpy(out, page->crop_box, sizeof(page->crop_box));
614 3           return PDFMAKE_OK;
615             }
616              
617 50           int pdfmake_reader_page_rotation(pdfmake_reader_t *reader,
618             pdfmake_reader_page_t *page) {
619             pdfmake_obj_t *rotate;
620              
621 50 50         if (!reader || !page) return 0;
    50          
622            
623             /* Check cache */
624 50 50         if (page->rotation_set) {
625 0           return page->rotation;
626             }
627            
628             /* Resolve inheritable /Rotate */
629 50           rotate = pdfmake_reader_resolve_inheritable(reader,
630             page->page_dict,
631             "Rotate");
632 50 100         if (rotate && rotate->kind == PDFMAKE_INT) {
    50          
633 19           int r = (int)rotate->as.i;
634             /* Normalize to 0, 90, 180, 270 */
635 19           r = ((r % 360) + 360) % 360;
636 19 100         if (r != 0 && r != 90 && r != 180 && r != 270) {
    50          
    0          
    0          
637 0           r = 0; /* Invalid rotation, default to 0 */
638             }
639 19           page->rotation = r;
640             } else {
641 31           page->rotation = 0;
642             }
643            
644 50           page->rotation_set = 1;
645 50           return page->rotation;
646             }
647              
648 60           pdfmake_obj_t *pdfmake_reader_page_resources(pdfmake_reader_t *reader,
649             pdfmake_reader_page_t *page) {
650 60 50         if (!reader || !page) return NULL;
    50          
651            
652             /* Check cache */
653 60 50         if (page->resources_set) {
654 0           return page->resources;
655             }
656            
657             /* Merge resources from page and ancestors */
658 60           page->resources = pdfmake_reader_merge_resources(reader, page->page_dict);
659 60           page->resources_set = 1;
660            
661 60           return page->resources;
662             }
663              
664             /*----------------------------------------------------------------------------
665             * Content stream extraction
666             *--------------------------------------------------------------------------*/
667              
668             /*
669             * Decrypt (if needed) and decompress a stream object.
670             * obj_num/gen are used for per-object key derivation.
671             */
672 181           static pdfmake_err_t reader_decode_stream(pdfmake_reader_t *reader,
673             pdfmake_obj_t *stream_obj,
674             uint32_t obj_num, uint16_t gen,
675             pdfmake_buf_t *out) {
676             pdfmake_stream_t *stream;
677 181           uint8_t *decoded = NULL;
678 181           size_t decoded_len = 0;
679             pdfmake_err_t err;
680              
681 181 50         if (!stream_obj || stream_obj->kind != PDFMAKE_STREAM)
    50          
682 0           return PDFMAKE_EINVAL;
683              
684 181           stream = stream_obj->as.stream;
685              
686             /* If encrypted and authenticated, decrypt the raw data first */
687 181 100         if (reader->crypt && reader->authenticated && !stream->filtered) {
    50          
    50          
688 101           uint8_t *decrypted = NULL;
689 101           size_t dec_len = 0;
690 101           int rc = pdfmake_crypt_decrypt_stream(reader->crypt,
691             (int)obj_num, (int)gen,
692 101           stream->raw, stream->raw_len,
693             &decrypted, &dec_len);
694 101 50         if (rc >= 0 && decrypted) {
    50          
695             /* Replace raw data with decrypted data for filter decode */
696 101           uint8_t *arena_copy = pdfmake_arena_alloc(reader->arena, dec_len);
697 101 50         if (arena_copy) {
698 101           memcpy(arena_copy, decrypted, dec_len);
699 101           stream->raw = arena_copy;
700 101           stream->raw_len = dec_len;
701             }
702 101           free(decrypted);
703             }
704             }
705              
706             /* Now decompress through filter chain */
707 181           err = pdfmake_decode_stream(reader->parser,
708             stream, &decoded, &decoded_len);
709 181 50         if (err != PDFMAKE_OK) return err;
710              
711 181           pdfmake_buf_append(out, decoded, decoded_len);
712 181           return PDFMAKE_OK;
713             }
714              
715 74           pdfmake_err_t pdfmake_reader_resolve_stream(pdfmake_reader_t *reader,
716             uint32_t obj_num,
717             uint16_t gen,
718             pdfmake_buf_t *out) {
719             pdfmake_ref_t ref;
720             pdfmake_obj_t *obj;
721 74 50         if (!reader || !out) return PDFMAKE_EINVAL;
    50          
722 74           ref.num = obj_num;
723 74           ref.gen = gen;
724 74           obj = pdfmake_parser_resolve(reader->parser, ref);
725 74 50         if (!obj || obj->kind != PDFMAKE_STREAM) return PDFMAKE_EINVAL;
    50          
726 74           return reader_decode_stream(reader, obj, obj_num, gen, out);
727             }
728              
729 107           pdfmake_err_t pdfmake_reader_page_content_bytes(pdfmake_reader_t *reader,
730             pdfmake_reader_page_t *page,
731             pdfmake_buf_t *out) {
732             pdfmake_obj_t *contents_raw;
733 107           uint32_t cont_obj_num = 0;
734 107           uint16_t cont_gen = 0;
735             pdfmake_obj_t *contents;
736             size_t arr_len;
737             size_t i;
738              
739 107 50         if (!reader || !page || !out) return PDFMAKE_EINVAL;
    50          
    50          
740            
741             /* Get /Contents from page */
742 107           contents_raw = dict_get_cstr(reader, page->page_dict, "Contents");
743 107 100         if (!contents_raw) {
744             /* Page with no content is valid — empty content */
745 2           return PDFMAKE_OK;
746             }
747              
748             /* Track object number for decryption key derivation */
749 105 100         if (contents_raw->kind == PDFMAKE_REF) {
750 103           cont_obj_num = contents_raw->as.ref.num;
751 103           cont_gen = contents_raw->as.ref.gen;
752             }
753              
754 105           contents = resolve_ref(reader, contents_raw);
755 105 50         if (!contents) {
756 0           reader_set_error(reader, PDFMAKE_EBADPAGE, "Could not resolve /Contents");
757 0           return PDFMAKE_EBADPAGE;
758             }
759              
760             /* Handle single stream or array of streams */
761 105 100         if (contents->kind == PDFMAKE_STREAM) {
762 103           pdfmake_err_t err = reader_decode_stream(reader, contents,
763             cont_obj_num, cont_gen, out);
764 103 50         if (err != PDFMAKE_OK) {
765 0           reader_set_error(reader, err, "Failed to decode content stream");
766 0           return err;
767             }
768              
769 2 50         } else if (contents->kind == PDFMAKE_ARRAY) {
770 2           arr_len = pdfmake_array_len(contents);
771              
772 6 100         for (i = 0; i < arr_len; i++) {
773             pdfmake_obj_t *stream_ref;
774 4           uint32_t s_num = 0;
775 4           uint16_t s_gen = 0;
776             pdfmake_obj_t *stream;
777             pdfmake_err_t err;
778              
779 4 100         if (i > 0)
780 2           pdfmake_buf_append(out, (const uint8_t *)" ", 1);
781              
782 4           stream_ref = pdfmake_array_get(contents, i);
783 4 50         if (stream_ref && stream_ref->kind == PDFMAKE_REF) {
    50          
784 4           s_num = stream_ref->as.ref.num;
785 4           s_gen = stream_ref->as.ref.gen;
786             }
787 4           stream = resolve_ref(reader, stream_ref);
788              
789 4 50         if (!stream || stream->kind != PDFMAKE_STREAM) {
    50          
790 0           reader_set_error(reader, PDFMAKE_EBADPAGE, "/Contents array has non-stream element");
791 0           return PDFMAKE_EBADPAGE;
792             }
793              
794 4           err = reader_decode_stream(reader, stream,
795             s_num, s_gen, out);
796 4 50         if (err != PDFMAKE_OK) {
797             char msg[128];
798 0           snprintf(msg, sizeof(msg), "Failed to decode content stream %zu", i);
799 0           reader_set_error(reader, err, msg);
800 0           return err;
801             }
802             }
803              
804             } else {
805 0           reader_set_error(reader, PDFMAKE_EBADPAGE, "/Contents is neither stream nor array");
806 0           return PDFMAKE_EBADPAGE;
807             }
808            
809 105           return PDFMAKE_OK;
810             }