diff options
| author | Gabriel Schneider <[email protected]> | 2026-08-03 09:16:50 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-08-10 09:17:07 -0300 |
| commit | a69cd4a7ef5c4527e3a7cd325d49b6bf445bd810 (patch) | |
| tree | 6a689cdea0f46ed520e5347fbf18ca7303943ae9 /src/pdf_bridge.c | |
| parent | a774cad033fdc371af9f89b9a787a30ea017481c (diff) | |
| download | pardes-a69cd4a7ef5c4527e3a7cd325d49b6bf445bd810.tar.gz pardes-a69cd4a7ef5c4527e3a7cd325d49b6bf445bd810.zip | |
expose MuPDF PDF outline metadata
Diffstat (limited to 'src/pdf_bridge.c')
| -rw-r--r-- | src/pdf_bridge.c | 298 |
1 files changed, 298 insertions, 0 deletions
diff --git a/src/pdf_bridge.c b/src/pdf_bridge.c index 202ef4f7..e0969018 100644 --- a/src/pdf_bridge.c +++ b/src/pdf_bridge.c @@ -859,3 +859,301 @@ pardes_pdf_drop_owned_text(pardes_pdf_document *document, void *text) if (document != NULL && text != NULL) fz_free(document->ctx, text); } + +typedef struct pardes_pdf_outline_measurement { + size_t item_count; + size_t bytes_len; + int status; +} pardes_pdf_outline_measurement; + +static int +pardes_pdf_outline_destination(const fz_outline *node, fz_context *ctx) +{ + if (node->page.chapter >= 0 && node->page.page >= 0) + return PARDES_PDF_OUTLINE_DESTINATION_INTERNAL; + if (node->page.chapter >= 0 || node->page.page >= 0) + fz_throw(ctx, FZ_ERROR_FORMAT, + "PDF outline has a partially resolved destination"); + if (node->uri != NULL && fz_is_external_link(ctx, node->uri)) + return PARDES_PDF_OUTLINE_DESTINATION_EXTERNAL; + return PARDES_PDF_OUTLINE_DESTINATION_NONE; +} + +static size_t +pardes_pdf_bounded_outline_string( + fz_context *ctx, + const char *value, + size_t remaining, + int *status) +{ + size_t len; + + if (value == NULL) + return 0; + len = strnlen(value, remaining + 1); + if (len > remaining) { + *status = PARDES_PDF_LIMIT_EXCEEDED; + fz_throw(ctx, FZ_ERROR_LIMIT, + "PDF outline metadata byte limit exceeded"); + } + return len; +} + +static void +pardes_pdf_measure_outline( + fz_context *ctx, + const fz_outline *node, + unsigned int depth, + pardes_pdf_outline_measurement *measurement) +{ + for (; node != NULL; node = node->next) { + size_t remaining; + int destination; + + if (depth >= PARDES_PDF_MAX_OUTLINE_DEPTH || + measurement->item_count >= PARDES_PDF_MAX_OUTLINE_ITEMS) { + measurement->status = PARDES_PDF_LIMIT_EXCEEDED; + fz_throw(ctx, FZ_ERROR_LIMIT, "PDF outline limit exceeded"); + } + ++measurement->item_count; + + remaining = PARDES_PDF_MAX_OUTLINE_BYTES - measurement->bytes_len; + measurement->bytes_len += pardes_pdf_bounded_outline_string( + ctx, node->title, remaining, &measurement->status); + + destination = pardes_pdf_outline_destination(node, ctx); + if (destination == PARDES_PDF_OUTLINE_DESTINATION_EXTERNAL) { + remaining = PARDES_PDF_MAX_OUTLINE_BYTES - + measurement->bytes_len; + measurement->bytes_len += pardes_pdf_bounded_outline_string( + ctx, node->uri, remaining, &measurement->status); + } + + if (node->down != NULL) + pardes_pdf_measure_outline( + ctx, node->down, depth + 1, measurement); + } +} + +static void +pardes_pdf_flatten_outline( + pardes_pdf_document *document, + const fz_outline *node, + unsigned int depth, + pardes_pdf_outline_item *items, + unsigned char *bytes, + size_t item_count, + size_t bytes_len, + size_t *item_at, + size_t *byte_at) +{ + fz_context *ctx = document->ctx; + + for (; node != NULL; node = node->next) { + pardes_pdf_outline_item *item; + int destination; + size_t len; + + if (*item_at >= item_count) + fz_throw(ctx, FZ_ERROR_FORMAT, + "PDF outline changed while flattening"); + item = &items[(*item_at)++]; + memset(item, 0, sizeof(*item)); + item->depth = depth; + item->page = -1; + item->is_open = node->is_open != 0; + item->flags = (unsigned char)node->flags; + item->r = (unsigned char)node->r; + item->g = (unsigned char)node->g; + item->b = (unsigned char)node->b; + + if (node->title != NULL) { + len = strlen(node->title); + if (*byte_at > bytes_len || len > bytes_len - *byte_at) + fz_throw(ctx, FZ_ERROR_FORMAT, + "PDF outline title changed while flattening"); + item->title_present = 1; + item->title_offset = *byte_at; + item->title_len = len; + memcpy(bytes + *byte_at, node->title, len); + *byte_at += len; + } + + destination = pardes_pdf_outline_destination(node, ctx); + item->destination_kind = destination; + if (destination == PARDES_PDF_OUTLINE_DESTINATION_INTERNAL) { + item->page = fz_page_number_from_location( + ctx, document->doc, node->page); + if (item->page < 0 || item->page >= document->page_count) + fz_throw(ctx, FZ_ERROR_FORMAT, + "PDF outline resolved outside the document"); + item->has_x = isfinite(node->x); + item->has_y = isfinite(node->y); + if (item->has_x) + item->x = node->x; + if (item->has_y) + item->y = node->y; + } else if (destination == PARDES_PDF_OUTLINE_DESTINATION_EXTERNAL) { + len = strlen(node->uri); + if (*byte_at > bytes_len || len > bytes_len - *byte_at) + fz_throw(ctx, FZ_ERROR_FORMAT, + "PDF outline URI changed while flattening"); + item->uri_offset = *byte_at; + item->uri_len = len; + memcpy(bytes + *byte_at, node->uri, len); + *byte_at += len; + } + + if (node->down != NULL) + pardes_pdf_flatten_outline(document, node->down, depth + 1, + items, bytes, item_count, bytes_len, item_at, byte_at); + } +} + +static unsigned char +pardes_pdf_outline_color(fz_context *ctx, float value) +{ + if (!isfinite(value) || value < 0.0f || value > 255.0f) + fz_throw(ctx, FZ_ERROR_FORMAT, + "PDF outline has an invalid style color"); + return (unsigned char)(value + 0.5f); +} + +/* + * MuPDF 1.27's generic fz_load_outline adapter does not copy the iterator's + * style fields into fz_outline. Overlay them from a second, allocation-light + * iterator pass while retaining the loaded tree as the destination authority. + */ +static void +pardes_pdf_apply_outline_styles( + fz_context *ctx, + fz_outline_iterator *iterator, + unsigned int depth, + pardes_pdf_outline_item *items, + size_t item_count, + size_t *item_at) +{ + int moved; + + do { + fz_outline_item *style = fz_outline_iterator_item(ctx, iterator); + pardes_pdf_outline_item *item; + + if (style == NULL) + return; + if (*item_at >= item_count || items[*item_at].depth != depth || + style->flags < 0 || style->flags > 127) + fz_throw(ctx, FZ_ERROR_FORMAT, + "PDF outline changed while reading styles"); + item = &items[(*item_at)++]; + item->flags = (unsigned char)style->flags; + item->r = pardes_pdf_outline_color(ctx, style->r); + item->g = pardes_pdf_outline_color(ctx, style->g); + item->b = pardes_pdf_outline_color(ctx, style->b); + + moved = fz_outline_iterator_down(ctx, iterator); + if (moved == FZ_OUTLINE_ITERATOR_AT_ITEM) + pardes_pdf_apply_outline_styles(ctx, iterator, depth + 1, + items, item_count, item_at); + if (moved >= 0) + (void)fz_outline_iterator_up(ctx, iterator); + } while (fz_outline_iterator_next(ctx, iterator) == + FZ_OUTLINE_ITERATOR_AT_ITEM); +} + +int +pardes_pdf_load_outline( + pardes_pdf_document *document, + pardes_pdf_outline_result *out) +{ + fz_context *ctx; + fz_outline *outline = NULL; + fz_outline_iterator *iterator = NULL; + unsigned char *block = NULL; + pardes_pdf_outline_measurement measurement = {0}; + size_t item_bytes; + size_t total_bytes; + size_t item_at = 0; + size_t byte_at = 0; + size_t style_at = 0; + int status = PARDES_PDF_ERROR; + + if (document == NULL || out == NULL) + return PARDES_PDF_ERROR; + memset(out, 0, sizeof(*out)); + ctx = document->ctx; + measurement.status = PARDES_PDF_ERROR; + + fz_var(outline); + fz_var(iterator); + fz_var(block); + fz_var(measurement); + fz_var(status); + fz_try(ctx) + { + outline = fz_load_outline(ctx, document->doc); + pardes_pdf_measure_outline(ctx, outline, 0, &measurement); + if (measurement.item_count > + SIZE_MAX / sizeof(pardes_pdf_outline_item) || + measurement.bytes_len > SIZE_MAX - measurement.item_count * + sizeof(pardes_pdf_outline_item)) { + measurement.status = PARDES_PDF_LIMIT_EXCEEDED; + fz_throw(ctx, FZ_ERROR_LIMIT, + "PDF outline allocation size overflow"); + } + item_bytes = measurement.item_count * + sizeof(pardes_pdf_outline_item); + total_bytes = item_bytes + measurement.bytes_len; + if (total_bytes != 0) { + block = fz_malloc(ctx, total_bytes); + pardes_pdf_flatten_outline(document, outline, 0, + (pardes_pdf_outline_item *)block, block + item_bytes, + measurement.item_count, measurement.bytes_len, + &item_at, &byte_at); + if (item_at != measurement.item_count || + byte_at != measurement.bytes_len) + fz_throw(ctx, FZ_ERROR_FORMAT, + "PDF outline changed while flattening"); + iterator = fz_new_outline_iterator(ctx, document->doc); + if (iterator != NULL) { + pardes_pdf_apply_outline_styles(ctx, iterator, 0, + (pardes_pdf_outline_item *)block, + measurement.item_count, &style_at); + if (style_at != measurement.item_count) + fz_throw(ctx, FZ_ERROR_FORMAT, + "PDF outline changed while reading styles"); + } + } + status = PARDES_PDF_OK; + } + fz_always(ctx) + { + fz_drop_outline_iterator(ctx, iterator); + fz_drop_outline(ctx, outline); + } + fz_catch(ctx) + { + if (measurement.status != PARDES_PDF_LIMIT_EXCEEDED) + fz_report_error(ctx); + fz_free(ctx, block); + memset(out, 0, sizeof(*out)); + return measurement.status == PARDES_PDF_LIMIT_EXCEEDED ? + PARDES_PDF_LIMIT_EXCEEDED : PARDES_PDF_ERROR; + } + + out->handle = block; + out->items = (const pardes_pdf_outline_item *)block; + out->item_count = measurement.item_count; + out->bytes = block == NULL ? NULL : block + item_bytes; + out->bytes_len = measurement.bytes_len; + return status; +} + +void +pardes_pdf_drop_outline_result( + pardes_pdf_document *document, + void *outline) +{ + if (document != NULL && outline != NULL) + fz_free(document->ctx, outline); +} |
