summaryrefslogtreecommitdiff
path: root/src/pdf_bridge.c
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-08-03 09:16:50 -0300
committerGabriel Schneider <[email protected]>2026-08-10 09:17:07 -0300
commita69cd4a7ef5c4527e3a7cd325d49b6bf445bd810 (patch)
tree6a689cdea0f46ed520e5347fbf18ca7303943ae9 /src/pdf_bridge.c
parenta774cad033fdc371af9f89b9a787a30ea017481c (diff)
downloadpardes-a69cd4a7ef5c4527e3a7cd325d49b6bf445bd810.tar.gz
pardes-a69cd4a7ef5c4527e3a7cd325d49b6bf445bd810.zip
expose MuPDF PDF outline metadata
Diffstat (limited to 'src/pdf_bridge.c')
-rw-r--r--src/pdf_bridge.c298
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);
+}