File Coverage

src/pdfmake_outline.c
Criterion Covered Total %
statement 153 261 58.6
branch 47 112 41.9
condition n/a
subroutine n/a
pod n/a
total 200 373 53.6


line stmt bran cond sub pod time code
1             /*
2             * pdfmake_outline.c — Document outline (bookmarks) implementation.
3             *
4             * Creates and manages the outline tree structure that appears as
5             * bookmarks in PDF reader sidebars.
6             */
7              
8             #include "pdfmake_outline.h"
9             #include "pdfmake_arena.h"
10             #include "pdfmake_page.h"
11             #include "pdfmake.h"
12             #include
13             #include
14              
15             /*----------------------------------------------------------------------------
16             * Destination builders
17             *--------------------------------------------------------------------------*/
18              
19 2           pdfmake_dest_t pdfmake_dest_xyz(size_t page_index, double left, double top, double zoom)
20             {
21 2           pdfmake_dest_t dest = {0};
22 2           dest.type = PDFMAKE_DEST_XYZ;
23 2           dest.page_index = page_index;
24 2           dest.left = left;
25 2           dest.top = top;
26 2           dest.zoom = zoom;
27 2           return dest;
28             }
29              
30 36           pdfmake_dest_t pdfmake_dest_fit(size_t page_index)
31             {
32 36           pdfmake_dest_t dest = {0};
33 36           dest.type = PDFMAKE_DEST_FIT;
34 36           dest.page_index = page_index;
35 36           return dest;
36             }
37              
38 2           pdfmake_dest_t pdfmake_dest_fith(size_t page_index, double top)
39             {
40 2           pdfmake_dest_t dest = {0};
41 2           dest.type = PDFMAKE_DEST_FITH;
42 2           dest.page_index = page_index;
43 2           dest.top = top;
44 2           return dest;
45             }
46              
47 1           pdfmake_dest_t pdfmake_dest_fitv(size_t page_index, double left)
48             {
49 1           pdfmake_dest_t dest = {0};
50 1           dest.type = PDFMAKE_DEST_FITV;
51 1           dest.page_index = page_index;
52 1           dest.left = left;
53 1           return dest;
54             }
55              
56 0           pdfmake_dest_t pdfmake_dest_fitr(size_t page_index,
57             double left, double bottom,
58             double right, double top)
59             {
60 0           pdfmake_dest_t dest = {0};
61 0           dest.type = PDFMAKE_DEST_FITR;
62 0           dest.page_index = page_index;
63 0           dest.left = left;
64 0           dest.bottom = bottom;
65 0           dest.right = right;
66 0           dest.top = top;
67 0           return dest;
68             }
69              
70 0           pdfmake_dest_t pdfmake_dest_fitb(size_t page_index)
71             {
72 0           pdfmake_dest_t dest = {0};
73 0           dest.type = PDFMAKE_DEST_FITB;
74 0           dest.page_index = page_index;
75 0           return dest;
76             }
77              
78 0           pdfmake_dest_t pdfmake_dest_fitbh(size_t page_index, double top)
79             {
80 0           pdfmake_dest_t dest = {0};
81 0           dest.type = PDFMAKE_DEST_FITBH;
82 0           dest.page_index = page_index;
83 0           dest.top = top;
84 0           return dest;
85             }
86              
87 0           pdfmake_dest_t pdfmake_dest_fitbv(size_t page_index, double left)
88             {
89 0           pdfmake_dest_t dest = {0};
90 0           dest.type = PDFMAKE_DEST_FITBV;
91 0           dest.page_index = page_index;
92 0           dest.left = left;
93 0           return dest;
94             }
95              
96             /*----------------------------------------------------------------------------
97             * Destination to object conversion
98             *--------------------------------------------------------------------------*/
99              
100 11           pdfmake_obj_t pdfmake_dest_to_obj(pdfmake_arena_t *arena,
101             pdfmake_doc_t *doc,
102             pdfmake_dest_t dest)
103             {
104             pdfmake_page_t *page;
105             pdfmake_obj_t page_ref;
106             pdfmake_obj_t arr;
107              
108             /* Get page reference */
109 11 50         if (dest.page_index >= doc->page_count) {
110 0           return pdfmake_null();
111             }
112            
113 11           page = doc->pages[dest.page_index];
114 11           page_ref = pdfmake_ref(page->page_num, 0);
115            
116 11           arr = pdfmake_array_new(arena);
117 11           pdfmake_array_push(arena, &arr, page_ref);
118            
119 11           switch (dest.type) {
120 1           case PDFMAKE_DEST_XYZ:
121 1           pdfmake_array_push(arena, &arr, pdfmake_name(arena, "XYZ", 3));
122 1 50         pdfmake_array_push(arena, &arr, (dest.left == 0) ? pdfmake_null() : pdfmake_real(dest.left));
123 1 50         pdfmake_array_push(arena, &arr, (dest.top == 0) ? pdfmake_null() : pdfmake_real(dest.top));
124 1 50         pdfmake_array_push(arena, &arr, (dest.zoom == 0) ? pdfmake_null() : pdfmake_real(dest.zoom));
125 1           break;
126            
127 10           case PDFMAKE_DEST_FIT:
128 10           pdfmake_array_push(arena, &arr, pdfmake_name(arena, "Fit", 3));
129 10           break;
130            
131 0           case PDFMAKE_DEST_FITH:
132 0           pdfmake_array_push(arena, &arr, pdfmake_name(arena, "FitH", 4));
133 0           pdfmake_array_push(arena, &arr, pdfmake_real(dest.top));
134 0           break;
135            
136 0           case PDFMAKE_DEST_FITV:
137 0           pdfmake_array_push(arena, &arr, pdfmake_name(arena, "FitV", 4));
138 0           pdfmake_array_push(arena, &arr, pdfmake_real(dest.left));
139 0           break;
140            
141 0           case PDFMAKE_DEST_FITR:
142 0           pdfmake_array_push(arena, &arr, pdfmake_name(arena, "FitR", 4));
143 0           pdfmake_array_push(arena, &arr, pdfmake_real(dest.left));
144 0           pdfmake_array_push(arena, &arr, pdfmake_real(dest.bottom));
145 0           pdfmake_array_push(arena, &arr, pdfmake_real(dest.right));
146 0           pdfmake_array_push(arena, &arr, pdfmake_real(dest.top));
147 0           break;
148            
149 0           case PDFMAKE_DEST_FITB:
150 0           pdfmake_array_push(arena, &arr, pdfmake_name(arena, "FitB", 4));
151 0           break;
152            
153 0           case PDFMAKE_DEST_FITBH:
154 0           pdfmake_array_push(arena, &arr, pdfmake_name(arena, "FitBH", 5));
155 0           pdfmake_array_push(arena, &arr, pdfmake_real(dest.top));
156 0           break;
157            
158 0           case PDFMAKE_DEST_FITBV:
159 0           pdfmake_array_push(arena, &arr, pdfmake_name(arena, "FitBV", 5));
160 0           pdfmake_array_push(arena, &arr, pdfmake_real(dest.left));
161 0           break;
162             }
163            
164 11           return arr;
165             }
166              
167             /*----------------------------------------------------------------------------
168             * Outline item creation
169             *--------------------------------------------------------------------------*/
170              
171 27           static pdfmake_outline_item_t *create_outline_item(pdfmake_doc_t *doc,
172             const char *title,
173             pdfmake_dest_t dest)
174             {
175 27           pdfmake_arena_t *arena = doc->arena;
176 27           pdfmake_outline_item_t *item = pdfmake_arena_alloc(arena, sizeof(pdfmake_outline_item_t));
177 27 50         if (!item) return NULL;
178            
179 27           memset(item, 0, sizeof(*item));
180            
181 27           item->doc = doc; /* Store doc pointer for arena access */
182            
183 27 50         if (title) {
184 27           size_t len = strlen(title);
185 27           item->title = pdfmake_arena_alloc(arena, len + 1);
186 27 50         if (!item->title) return NULL;
187 27           memcpy(item->title, title, len + 1);
188             }
189            
190 27           item->dest = dest;
191 27           item->open = 1; /* Default to open/expanded */
192            
193 27           return item;
194             }
195              
196             /*
197             * Recalculate count for an item and all ancestors.
198             * Count = number of visible descendants (when open).
199             */
200 17           static void update_counts(pdfmake_outline_item_t *item)
201             {
202             pdfmake_outline_item_t *child;
203             int count;
204 30 100         while (item) {
205 13           count = 0;
206 33 100         for (child = item->first; child; child = child->next) {
207 20           count++;
208 20 50         if (child->open && child->count > 0) {
    50          
209 0           count += child->count;
210             }
211             }
212 13           item->count = count;
213 13           item = item->parent;
214             }
215 17           }
216              
217             /*----------------------------------------------------------------------------
218             * Document outline storage
219             *
220             * The outline root is stored directly in the pdfmake_doc_t structure
221             * via the outline_root field, avoiding global state issues.
222             *--------------------------------------------------------------------------*/
223              
224             /*----------------------------------------------------------------------------
225             * Public API - Document outline
226             *--------------------------------------------------------------------------*/
227              
228 163           pdfmake_outline_item_t *pdfmake_doc_get_outline(pdfmake_doc_t *doc)
229             {
230 163 50         if (!doc) return NULL;
231 163           return (pdfmake_outline_item_t *)doc->outline_root;
232             }
233              
234 17           pdfmake_outline_item_t *pdfmake_doc_add_outline_root(pdfmake_doc_t *doc,
235             const char *title,
236             pdfmake_dest_t dest)
237             {
238             pdfmake_outline_item_t *root;
239              
240 17 50         if (!doc) return NULL;
241              
242 17 100         if (doc->outline_root) {
243             /* Already have a root - return it */
244 3           return (pdfmake_outline_item_t *)doc->outline_root;
245             }
246              
247 14           root = create_outline_item(doc, title, dest);
248 14 50         if (!root) return NULL;
249              
250 14           doc->outline_root = root;
251 14           return root;
252             }
253              
254 13           pdfmake_outline_item_t *pdfmake_outline_add_child(pdfmake_outline_item_t *parent,
255             const char *title,
256             pdfmake_dest_t dest)
257             {
258             pdfmake_outline_item_t *child;
259              
260 13 50         if (!parent || !parent->doc) return NULL;
    50          
261            
262 13           child = create_outline_item(parent->doc, title, dest);
263 13 50         if (!child) return NULL;
264            
265 13           child->parent = parent;
266            
267             /* Add to end of children list */
268 13 100         if (!parent->first) {
269 8           parent->first = child;
270 8           parent->last = child;
271             } else {
272 5           parent->last->next = child;
273 5           child->prev = parent->last;
274 5           parent->last = child;
275             }
276            
277 13           update_counts(parent);
278            
279 13           return child;
280             }
281              
282 0           pdfmake_err_t pdfmake_outline_set_title(pdfmake_outline_item_t *item,
283             const char *title)
284             {
285             size_t new_len;
286              
287 0 0         if (!item || !title) return PDFMAKE_EINVAL;
    0          
288            
289             /* Can't easily reallocate in arena - just update pointer if same length or smaller */
290             /* For now, this is a limitation */
291 0           new_len = strlen(title);
292 0 0         if (item->title && strlen(item->title) >= new_len) {
    0          
293 0           memcpy(item->title, title, new_len + 1);
294 0           return PDFMAKE_OK;
295             }
296            
297             /* Would need arena access to allocate new string */
298 0           return PDFMAKE_EINVAL;
299             }
300              
301 0           pdfmake_err_t pdfmake_outline_set_dest(pdfmake_outline_item_t *item,
302             pdfmake_dest_t dest)
303             {
304 0 0         if (!item) return PDFMAKE_EINVAL;
305 0           item->dest = dest;
306 0           return PDFMAKE_OK;
307             }
308              
309 4           void pdfmake_outline_set_open(pdfmake_outline_item_t *item, int open)
310             {
311 4 50         if (!item) return;
312 4           item->open = open ? 1 : 0;
313 4           update_counts(item->parent);
314             }
315              
316 0           pdfmake_err_t pdfmake_outline_remove(pdfmake_outline_item_t *item)
317             {
318             pdfmake_outline_item_t *parent;
319              
320 0 0         if (!item || !item->parent) return PDFMAKE_EINVAL;
    0          
321            
322 0           parent = item->parent;
323            
324             /* Unlink from siblings */
325 0 0         if (item->prev) {
326 0           item->prev->next = item->next;
327             } else {
328 0           parent->first = item->next;
329             }
330            
331 0 0         if (item->next) {
332 0           item->next->prev = item->prev;
333             } else {
334 0           parent->last = item->prev;
335             }
336            
337 0           update_counts(parent);
338            
339             /* Item and children are now orphaned but remain in arena */
340 0           return PDFMAKE_OK;
341             }
342              
343 0           size_t pdfmake_outline_count(pdfmake_outline_item_t *root)
344             {
345             size_t count;
346             pdfmake_outline_item_t *child;
347              
348 0 0         if (!root) return 0;
349            
350 0           count = 1;
351 0 0         for (child = root->first; child; child = child->next) {
352 0           count += pdfmake_outline_count(child);
353             }
354 0           return count;
355             }
356              
357             /*----------------------------------------------------------------------------
358             * Outline finalization - write to PDF
359             *--------------------------------------------------------------------------*/
360              
361 11           static uint32_t write_outline_item(pdfmake_doc_t *doc,
362             pdfmake_outline_item_t *item,
363             uint32_t parent_num)
364             {
365 11           pdfmake_arena_t *arena = doc->arena;
366             pdfmake_obj_t dict;
367             uint32_t parent_key;
368             uint32_t item_num;
369             pdfmake_outline_item_t *child;
370             uint32_t first_key;
371             uint32_t last_key;
372             pdfmake_obj_t *obj_ptr;
373              
374             /* Build the outline item dictionary */
375 11           dict = pdfmake_dict_new(arena);
376            
377             /* /Title */
378 11 50         if (item->title) {
379 11           uint32_t key = pdfmake_arena_intern_name(arena, "Title", 5);
380 11           pdfmake_obj_t title_str = pdfmake_str(arena, item->title, strlen(item->title));
381 11           pdfmake_dict_set(arena, &dict, key, title_str);
382             }
383            
384             /* /Parent */
385 11           parent_key = pdfmake_arena_intern_name(arena, "Parent", 6);
386 11           pdfmake_dict_set(arena, &dict, parent_key, pdfmake_ref(parent_num, 0));
387            
388             /* /Dest */
389 11 50         if (item->title) { /* Only add dest if this is a real item, not root container */
390 11           uint32_t dest_key = pdfmake_arena_intern_name(arena, "Dest", 4);
391 11           pdfmake_obj_t dest_arr = pdfmake_dest_to_obj(arena, doc, item->dest);
392 11           pdfmake_dict_set(arena, &dict, dest_key, dest_arr);
393             }
394            
395             /* Reserve object number first so we can pass to children */
396 11           item_num = pdfmake_doc_add(doc, pdfmake_null());
397 11           item->obj_num = item_num;
398            
399             /* Write children first to get their object numbers */
400 11 100         if (item->first) {
401 4           uint32_t first_num = 0, last_num = 0;
402            
403 10 100         for (child = item->first; child; child = child->next) {
404 6           uint32_t child_num = write_outline_item(doc, child, item_num);
405 6 100         if (!first_num) first_num = child_num;
406 6           last_num = child_num;
407             }
408            
409             /* Now update children with sibling links */
410 10 100         for (child = item->first; child; child = child->next) {
411 6           pdfmake_obj_t *child_obj = pdfmake_doc_get(doc, child->obj_num);
412 6 50         if (child_obj) {
413 6 100         if (child->prev) {
414 2           uint32_t prev_key = pdfmake_arena_intern_name(arena, "Prev", 4);
415 2           pdfmake_dict_set(arena, child_obj, prev_key, pdfmake_ref(child->prev->obj_num, 0));
416             }
417 6 100         if (child->next) {
418 2           uint32_t next_key = pdfmake_arena_intern_name(arena, "Next", 4);
419 2           pdfmake_dict_set(arena, child_obj, next_key, pdfmake_ref(child->next->obj_num, 0));
420             }
421             }
422             }
423            
424             /* /First and /Last */
425 4           first_key = pdfmake_arena_intern_name(arena, "First", 5);
426 4           last_key = pdfmake_arena_intern_name(arena, "Last", 4);
427 4           pdfmake_dict_set(arena, &dict, first_key, pdfmake_ref(first_num, 0));
428 4           pdfmake_dict_set(arena, &dict, last_key, pdfmake_ref(last_num, 0));
429             }
430            
431             /* /Count */
432 11 100         if (item->count > 0) {
433 4           uint32_t count_key = pdfmake_arena_intern_name(arena, "Count", 5);
434 4 50         int count_val = item->open ? item->count : -item->count;
435 4           pdfmake_dict_set(arena, &dict, count_key, pdfmake_int(count_val));
436             }
437            
438             /* Update the reserved object */
439 11           obj_ptr = pdfmake_doc_get(doc, item_num);
440 11 50         if (obj_ptr) {
441 11           *obj_ptr = dict;
442             }
443            
444 11           return item_num;
445             }
446              
447 5           uint32_t pdfmake_outline_finalize(pdfmake_doc_t *doc,
448             pdfmake_outline_item_t *root)
449             {
450             pdfmake_arena_t *arena;
451             pdfmake_obj_t outlines_dict;
452             uint32_t type_key;
453             uint32_t outlines_num;
454             pdfmake_outline_item_t *child;
455             uint32_t first_key;
456             uint32_t last_key;
457             uint32_t count_key;
458             pdfmake_obj_t *outlines_ptr;
459              
460 5 50         if (!doc || !root) return 0;
    50          
461            
462 5           arena = doc->arena;
463            
464             /* Create /Outlines dictionary */
465 5           outlines_dict = pdfmake_dict_new(arena);
466            
467 5           type_key = pdfmake_arena_intern_name(arena, "Type", 4);
468 5           pdfmake_dict_set(arena, &outlines_dict, type_key, pdfmake_name(arena, "Outlines", 8));
469            
470 5           outlines_num = pdfmake_doc_add(doc, outlines_dict);
471            
472             /* If root has a title, it's a real item; otherwise just use its children */
473 5 50         if (root->title) {
474             /* Root is a real outline item */
475 5           uint32_t root_item_num = write_outline_item(doc, root, outlines_num);
476            
477 5           first_key = pdfmake_arena_intern_name(arena, "First", 5);
478 5           last_key = pdfmake_arena_intern_name(arena, "Last", 4);
479 5           count_key = pdfmake_arena_intern_name(arena, "Count", 5);
480            
481 5           outlines_ptr = pdfmake_doc_get(doc, outlines_num);
482 5 50         if (outlines_ptr) {
483 5           pdfmake_dict_set(arena, outlines_ptr, first_key, pdfmake_ref(root_item_num, 0));
484 5           pdfmake_dict_set(arena, outlines_ptr, last_key, pdfmake_ref(root_item_num, 0));
485 5           pdfmake_dict_set(arena, outlines_ptr, count_key, pdfmake_int(1 + root->count));
486             }
487 0 0         } else if (root->first) {
488             /* Root is a container - write its children directly under /Outlines */
489 0           uint32_t first_num = 0, last_num = 0;
490 0           int total_count = 0;
491            
492 0 0         for (child = root->first; child; child = child->next) {
493 0           uint32_t child_num = write_outline_item(doc, child, outlines_num);
494 0 0         if (!first_num) first_num = child_num;
495 0           last_num = child_num;
496 0           total_count++;
497 0 0         if (child->open) total_count += child->count;
498             }
499            
500             /* Update sibling links */
501 0 0         for (child = root->first; child; child = child->next) {
502 0           pdfmake_obj_t *child_obj = pdfmake_doc_get(doc, child->obj_num);
503 0 0         if (child_obj) {
504 0 0         if (child->prev) {
505 0           uint32_t prev_key = pdfmake_arena_intern_name(arena, "Prev", 4);
506 0           pdfmake_dict_set(arena, child_obj, prev_key, pdfmake_ref(child->prev->obj_num, 0));
507             }
508 0 0         if (child->next) {
509 0           uint32_t next_key = pdfmake_arena_intern_name(arena, "Next", 4);
510 0           pdfmake_dict_set(arena, child_obj, next_key, pdfmake_ref(child->next->obj_num, 0));
511             }
512             }
513             }
514            
515 0           first_key = pdfmake_arena_intern_name(arena, "First", 5);
516 0           last_key = pdfmake_arena_intern_name(arena, "Last", 4);
517 0           count_key = pdfmake_arena_intern_name(arena, "Count", 5);
518            
519 0           outlines_ptr = pdfmake_doc_get(doc, outlines_num);
520 0 0         if (outlines_ptr) {
521 0           pdfmake_dict_set(arena, outlines_ptr, first_key, pdfmake_ref(first_num, 0));
522 0           pdfmake_dict_set(arena, outlines_ptr, last_key, pdfmake_ref(last_num, 0));
523 0           pdfmake_dict_set(arena, outlines_ptr, count_key, pdfmake_int(total_count));
524             }
525             }
526            
527 5           return outlines_num;
528             }