#include "pdf_bridge.h" #include #include #include #include #include #include #include #include #define PARDES_PDF_INITIAL_RESULT_QUADS 64 struct pardes_pdf_document { pardes_pdf_allocator allocator; fz_alloc_context mupdf_allocator; int uses_allocator; fz_context *ctx; fz_document *doc; int page_count; /* One UI thread owns a document. Page switches invalidate this cache. */ int cached_page_number; fz_page *cached_page; fz_stext_page *cached_text; fz_display_list *cached_display_list; fz_rect cached_bounds; fz_link *cached_links; int cached_links_loaded; /* Promote only a page which is rendered twice without changing pages. */ int display_list_candidate_page_number; }; static pardes_pdf_allocator pardes_pdf_global_allocator; static int pardes_pdf_allocator_installed; static _Atomic size_t pardes_pdf_active_documents; static atomic_flag pardes_pdf_allocator_lock = ATOMIC_FLAG_INIT; static void pardes_pdf_lock_allocator(void) { while (atomic_flag_test_and_set_explicit( &pardes_pdf_allocator_lock, memory_order_acquire)) ; } static void pardes_pdf_unlock_allocator(void) { atomic_flag_clear_explicit( &pardes_pdf_allocator_lock, memory_order_release); } int pardes_pdf_set_allocator(const pardes_pdf_allocator *allocator) { int status = PARDES_PDF_OK; if (allocator != NULL && (allocator->alloc == NULL || allocator->realloc == NULL || allocator->free == NULL)) return PARDES_PDF_ERROR; pardes_pdf_lock_allocator(); if (atomic_load(&pardes_pdf_active_documents) != 0) { status = PARDES_PDF_ERROR; } else if (allocator == NULL) { memset(&pardes_pdf_global_allocator, 0, sizeof(pardes_pdf_global_allocator)); pardes_pdf_allocator_installed = 0; } else { pardes_pdf_global_allocator = *allocator; pardes_pdf_allocator_installed = 1; } pardes_pdf_unlock_allocator(); return status; } static pardes_pdf_document * pardes_pdf_allocate_document(void) { pardes_pdf_document *document; pardes_pdf_allocator allocator; int uses_allocator; pardes_pdf_lock_allocator(); uses_allocator = pardes_pdf_allocator_installed; allocator = pardes_pdf_global_allocator; atomic_fetch_add(&pardes_pdf_active_documents, 1); pardes_pdf_unlock_allocator(); if (uses_allocator) { document = (pardes_pdf_document *)allocator.alloc( allocator.user, sizeof(*document)); } else { document = (pardes_pdf_document *)calloc(1, sizeof(*document)); } if (document == NULL) { atomic_fetch_sub(&pardes_pdf_active_documents, 1); return NULL; } if (!uses_allocator) return document; memset(document, 0, sizeof(*document)); document->allocator = allocator; document->uses_allocator = 1; document->mupdf_allocator.user = allocator.user; document->mupdf_allocator.malloc = allocator.alloc; document->mupdf_allocator.realloc = allocator.realloc; document->mupdf_allocator.free = allocator.free; return document; } static void pardes_pdf_free_document(pardes_pdf_document *document) { if (document->uses_allocator) document->allocator.free(document->allocator.user, document); else free(document); } static void pardes_pdf_drop_cached_page(pardes_pdf_document *document) { fz_drop_link(document->ctx, document->cached_links); fz_drop_display_list(document->ctx, document->cached_display_list); fz_drop_stext_page(document->ctx, document->cached_text); fz_drop_page(document->ctx, document->cached_page); document->cached_display_list = NULL; document->cached_text = NULL; document->cached_page = NULL; document->cached_links = NULL; document->cached_links_loaded = 0; document->cached_page_number = -1; document->display_list_candidate_page_number = -1; } static int pardes_pdf_valid_bounds(fz_rect bounds) { return isfinite(bounds.x0) && isfinite(bounds.y0) && isfinite(bounds.x1) && isfinite(bounds.y1) && bounds.x1 > bounds.x0 && bounds.y1 > bounds.y0; } /* * Load into temporaries first, then replace the cache in one non-throwing * commit. Thus a failed page/text load leaves the previous cached page valid. */ static void pardes_pdf_cache_page( pardes_pdf_document *document, int page_number, int require_text) { fz_context *ctx = document->ctx; fz_page *page = NULL; fz_stext_page *text = NULL; fz_rect bounds; fz_stext_options options = {0}; options.flags = FZ_STEXT_DEHYPHENATE; if (document->cached_page_number == page_number) { if (require_text && document->cached_text == NULL) document->cached_text = fz_new_stext_page_from_page( ctx, document->cached_page, &options); return; } fz_var(page); fz_var(text); fz_try(ctx) { page = fz_load_page(ctx, document->doc, page_number); bounds = fz_bound_page(ctx, page); if (!pardes_pdf_valid_bounds(bounds)) fz_throw(ctx, FZ_ERROR_FORMAT, "PDF page has empty bounds"); if (require_text) text = fz_new_stext_page_from_page(ctx, page, &options); } fz_catch(ctx) { fz_drop_stext_page(ctx, text); fz_drop_page(ctx, page); fz_rethrow(ctx); } pardes_pdf_drop_cached_page(document); document->cached_page_number = page_number; document->cached_page = page; document->cached_text = text; document->cached_bounds = bounds; } /* * Build only after one successful direct render of the still-cached page. * This keeps cold renders and sequential document passes on their original * path instead of recording a list which the next page would immediately * evict. The temporary list is committed only after its device has closed; * any partially recorded list is dropped before the exception propagates. */ static void pardes_pdf_cache_display_list(pardes_pdf_document *document) { fz_context *ctx = document->ctx; fz_display_list *list = NULL; fz_device *device = NULL; if (document->cached_display_list != NULL) return; fz_var(list); fz_var(device); fz_try(ctx) { list = fz_new_display_list(ctx, document->cached_bounds); device = fz_new_list_device(ctx, list); fz_run_page(ctx, document->cached_page, device, fz_identity, NULL); fz_close_device(ctx, device); } fz_always(ctx) { fz_drop_device(ctx, device); } fz_catch(ctx) { fz_drop_display_list(ctx, list); fz_rethrow(ctx); } document->cached_display_list = list; } static float pardes_pdf_clamp_unit(float value) { if (value < 0.0f) return 0.0f; if (value > 1.0f) return 1.0f; return value; } static int pardes_pdf_valid_point(pardes_pdf_point point) { return isfinite(point.x) && isfinite(point.y); } static fz_point pardes_pdf_point_to_page(pardes_pdf_document *document, pardes_pdf_point point) { fz_rect bounds = document->cached_bounds; float x = pardes_pdf_clamp_unit(point.x); float y = pardes_pdf_clamp_unit(point.y); return fz_make_point( bounds.x0 + x * (bounds.x1 - bounds.x0), bounds.y0 + y * (bounds.y1 - bounds.y0)); } static pardes_pdf_point pardes_pdf_point_from_page(pardes_pdf_document *document, fz_point point) { fz_rect bounds = document->cached_bounds; pardes_pdf_point result; result.x = pardes_pdf_clamp_unit( (point.x - bounds.x0) / (bounds.x1 - bounds.x0)); result.y = pardes_pdf_clamp_unit( (point.y - bounds.y0) / (bounds.y1 - bounds.y0)); return result; } static pardes_pdf_quad pardes_pdf_quad_from_page(pardes_pdf_document *document, fz_quad quad) { pardes_pdf_quad result; result.ul = pardes_pdf_point_from_page(document, quad.ul); result.ur = pardes_pdf_point_from_page(document, quad.ur); result.ll = pardes_pdf_point_from_page(document, quad.ll); result.lr = pardes_pdf_point_from_page(document, quad.lr); return result; } static int pardes_pdf_valid_quad(pardes_pdf_quad quad) { return pardes_pdf_valid_point(quad.ul) && pardes_pdf_valid_point(quad.ur) && pardes_pdf_valid_point(quad.ll) && pardes_pdf_valid_point(quad.lr); } static fz_quad pardes_pdf_quad_to_page(pardes_pdf_document *document, pardes_pdf_quad quad) { fz_quad result; result.ul = pardes_pdf_point_to_page(document, quad.ul); result.ur = pardes_pdf_point_to_page(document, quad.ur); result.ll = pardes_pdf_point_to_page(document, quad.ll); result.lr = pardes_pdf_point_to_page(document, quad.lr); return result; } static size_t pardes_pdf_grown_capacity(size_t current, size_t needed, size_t maximum) { size_t capacity = current == 0 ? PARDES_PDF_INITIAL_RESULT_QUADS : current; while (capacity < needed) { if (capacity > maximum / 2) { capacity = maximum; break; } capacity *= 2; } return capacity; } static pardes_pdf_document * pardes_pdf_open_source(const char *path, const unsigned char *bytes, size_t length, int *page_count) { pardes_pdf_document *state; fz_context *ctx; fz_document *doc = NULL; fz_buffer *buffer = NULL; fz_stream *stream = NULL; int pages = 0; if ((path == NULL && bytes == NULL) || page_count == NULL) return NULL; state = pardes_pdf_allocate_document(); if (state == NULL) return NULL; state->cached_page_number = -1; state->display_list_candidate_page_number = -1; ctx = fz_new_context( state->uses_allocator ? &state->mupdf_allocator : NULL, NULL, PARDES_PDF_STORE_LIMIT_BYTES); if (ctx == NULL) { pardes_pdf_free_document(state); atomic_fetch_sub(&pardes_pdf_active_documents, 1); return NULL; } fz_var(doc); fz_var(buffer); fz_var(stream); fz_try(ctx) { fz_register_document_handlers(ctx); if (path != NULL) { doc = fz_open_document(ctx, path); } else { buffer = fz_new_buffer_from_copied_data(ctx, bytes, length); stream = fz_open_buffer(ctx, buffer); doc = fz_open_document_with_stream(ctx, "application/pdf", stream); } pages = fz_count_pages(ctx, doc); if (pages < 1) fz_throw(ctx, FZ_ERROR_FORMAT, "PDF has no pages"); } fz_always(ctx) { fz_drop_stream(ctx, stream); fz_drop_buffer(ctx, buffer); } fz_catch(ctx) { fz_report_error(ctx); fz_drop_document(ctx, doc); fz_drop_context(ctx); pardes_pdf_free_document(state); atomic_fetch_sub(&pardes_pdf_active_documents, 1); return NULL; } state->ctx = ctx; state->doc = doc; state->page_count = pages; *page_count = pages; return state; } pardes_pdf_document * pardes_pdf_open(const char *path, int *page_count) { return pardes_pdf_open_source(path, NULL, 0, page_count); } pardes_pdf_document * pardes_pdf_open_memory(const unsigned char *bytes, size_t length, int *page_count) { return pardes_pdf_open_source(NULL, bytes, length, page_count); } void pardes_pdf_close(pardes_pdf_document *document) { if (document == NULL) return; pardes_pdf_drop_cached_page(document); fz_drop_document(document->ctx, document->doc); fz_drop_context(document->ctx); pardes_pdf_free_document(document); atomic_fetch_sub(&pardes_pdf_active_documents, 1); } /* * A page's SIZE, without building a page. * * fz_bound_page(fz_load_page(n)) is the obvious spelling and it is what this * used to do, but fz_load_page builds a whole pdf_page: it resolves the page * dictionary, then loads and parses every link annotation on it. Laying out a * 5363-page manual's strip asks for 5363 sizes, and profiling an open showed * pdf_load_link_annots alone at 29% of the run — parsing links for pages * nobody has looked at yet, to answer a question about their height. * * The page OBJECT answers it directly, and identically: fz_bound_page on a PDF * is pdf_bound_page(FZ_CROP_BOX), which is pdf_page_obj_transform_box on * page->obj followed by fz_transform_rect — exactly the two calls below, from * exactly the same object. Non-PDF documents (cbz, xps, svg) have no page * objects and keep the loading path. */ int pardes_pdf_get_page_size( pardes_pdf_document *document, int page_number, pardes_pdf_page_size *out) { fz_context *ctx; fz_rect bounds; pdf_document *pdf; if (document == NULL || out == NULL || page_number < 0 || page_number >= document->page_count) return PARDES_PDF_ERROR; memset(out, 0, sizeof(*out)); ctx = document->ctx; fz_try(ctx) { pdf = pdf_specifics(ctx, document->doc); if (pdf != NULL) { fz_matrix page_ctm; fz_rect cropbox; pdf_obj *page_obj = pdf_lookup_page_obj(ctx, pdf, page_number); pdf_page_obj_transform_box(ctx, page_obj, &cropbox, &page_ctm, FZ_CROP_BOX); bounds = fz_transform_rect(cropbox, page_ctm); } else { pardes_pdf_cache_page(document, page_number, 0); bounds = document->cached_bounds; } out->width = bounds.x1 - bounds.x0; out->height = bounds.y1 - bounds.y0; if (!(out->width > 0.0f) || !(out->height > 0.0f)) fz_throw(ctx, FZ_ERROR_FORMAT, "PDF page has empty bounds"); } fz_catch(ctx) { fz_report_error(ctx); memset(out, 0, sizeof(*out)); return PARDES_PDF_ERROR; } return PARDES_PDF_OK; } static void pardes_pdf_render_geometry( pardes_pdf_document *document, int page_number, int dpi, int minimum_width, int minimum_height, int max_dimension, fz_matrix *ctm_out, fz_irect *bbox_out) { fz_rect transformed; fz_rect bounds; float page_width; float page_height; float longest; float scale; pardes_pdf_cache_page(document, page_number, 0); bounds = document->cached_bounds; page_width = bounds.x1 - bounds.x0; page_height = bounds.y1 - bounds.y0; longest = fmaxf(page_width, page_height); if (!(longest > 0.0f)) fz_throw(document->ctx, FZ_ERROR_FORMAT, "PDF page has empty bounds"); scale = (float)dpi / 72.0f; if (minimum_width > 0) scale = fmaxf(scale, (float)minimum_width / page_width); if (minimum_height > 0) scale = fmaxf(scale, (float)minimum_height / page_height); if (longest * scale > (float)max_dimension) scale = (float)max_dimension / longest; /* * Keep this identical to fz_new_pixmap_from_page/display_list: transform * non-zero crop boxes to the origin, transform the bounds, then round the * complete rectangle once. The same CTM also aligns highlight geometry. */ *ctm_out = fz_transform_page(bounds, scale * 72.0f, 0.0f); transformed = fz_transform_rect(bounds, *ctm_out); *bbox_out = fz_round_rect(transformed); if (fz_irect_width(*bbox_out) < 1 || fz_irect_height(*bbox_out) < 1) fz_throw(document->ctx, FZ_ERROR_FORMAT, "PDF page rendered empty"); } int pardes_pdf_measure_render( pardes_pdf_document *document, int page_number, int dpi, int minimum_width, int minimum_height, int max_dimension, pardes_pdf_raster_layout *out) { fz_context *ctx; fz_matrix ctm; fz_irect bbox; int width = 0; int height = 0; int stride = 0; size_t samples_len = 0; if (out == NULL) return PARDES_PDF_ERROR; memset(out, 0, sizeof(*out)); if (document == NULL || page_number < 0 || page_number >= document->page_count || dpi < 1 || minimum_width < 0 || minimum_height < 0 || max_dimension < 1) return PARDES_PDF_ERROR; ctx = document->ctx; fz_try(ctx) { pardes_pdf_render_geometry(document, page_number, dpi, minimum_width, minimum_height, max_dimension, &ctm, &bbox); width = fz_irect_width(bbox); height = fz_irect_height(bbox); if (width > INT_MAX / 4) fz_throw(ctx, FZ_ERROR_LIMIT, "PDF raster row is too large"); stride = width * 4; if ((size_t)height > SIZE_MAX / (size_t)stride) fz_throw(ctx, FZ_ERROR_LIMIT, "PDF raster is too large"); samples_len = (size_t)stride * (size_t)height; } fz_catch(ctx) { fz_report_error(ctx); memset(out, 0, sizeof(*out)); return PARDES_PDF_ERROR; } out->width = width; out->height = height; out->stride = stride; out->samples_len = samples_len; return PARDES_PDF_OK; } /* The highlight pass of a render: fills over whatever the pixmap holds. */ static void pardes_pdf_fill_highlights(fz_context *ctx, pardes_pdf_document *document, fz_device *device, const pardes_pdf_highlight *highlights, size_t highlight_count) { fz_path *path = NULL; size_t i; fz_var(path); fz_try(ctx) { for (i = 0; i < highlight_count; ++i) { fz_quad quad; float color[3]; float alpha; if (!pardes_pdf_valid_quad(highlights[i].quad)) fz_throw(ctx, FZ_ERROR_ARGUMENT, "PDF highlight contains a non-finite point"); if (highlights[i].rgba[3] == 0) continue; /* Kind is semantic metadata; caller-supplied RGBA is final. */ (void)highlights[i].kind; quad = pardes_pdf_quad_to_page(document, highlights[i].quad); color[0] = highlights[i].rgba[0] / 255.0f; color[1] = highlights[i].rgba[1] / 255.0f; color[2] = highlights[i].rgba[2] / 255.0f; alpha = highlights[i].rgba[3] / 255.0f; path = fz_new_path(ctx); fz_moveto(ctx, path, quad.ul.x, quad.ul.y); fz_lineto(ctx, path, quad.ur.x, quad.ur.y); fz_lineto(ctx, path, quad.lr.x, quad.lr.y); fz_lineto(ctx, path, quad.ll.x, quad.ll.y); fz_closepath(ctx, path); fz_fill_path(ctx, device, path, 0, fz_identity, fz_device_rgb(ctx), color, alpha, fz_default_color_params); fz_drop_path(ctx, path); path = NULL; } } fz_always(ctx) fz_drop_path(ctx, path); fz_catch(ctx) fz_rethrow(ctx); } int pardes_pdf_render_into( pardes_pdf_document *document, int page_number, int dpi, int minimum_width, int minimum_height, int max_dimension, const pardes_pdf_highlight *highlights, size_t highlight_count, unsigned char *samples, size_t samples_len, int width, int height, int stride, int band_y, int band_height, int transparent_paper) { fz_context *ctx; fz_pixmap *pixmap = NULL; fz_device *device = NULL; fz_matrix ctm; fz_irect bbox; if (document == NULL || page_number < 0 || page_number >= document->page_count || dpi < 1 || minimum_width < 0 || minimum_height < 0 || max_dimension < 1 || (highlight_count != 0 && highlights == NULL) || highlight_count > PARDES_PDF_MAX_RESULT_QUADS || samples == NULL || width < 1 || height < 1 || stride < 1 || width > INT_MAX / 4 || stride != width * 4 || band_y < 0 || band_height < 1 || band_y > height - band_height || (size_t)band_height > SIZE_MAX / (size_t)stride || samples_len != (size_t)stride * (size_t)band_height) return PARDES_PDF_ERROR; ctx = document->ctx; fz_var(pixmap); fz_var(device); fz_try(ctx) { pardes_pdf_render_geometry(document, page_number, dpi, minimum_width, minimum_height, max_dimension, &ctm, &bbox); if (fz_irect_width(bbox) != width || fz_irect_height(bbox) != height) fz_throw(ctx, FZ_ERROR_ARGUMENT, "PDF raster layout changed between measure and render"); /* * The BAND: rows [band_y, band_y + band_height) of the page raster, * and nothing else. A pixmap's bbox IS the draw device's clip, so the * page runs under the same CTM it would for a full raster and MuPDF * discards everything outside these rows. The rows are NOT always * bit-identical to the full page's: rows at the band's edges, and a * resampled image anywhere in it, can differ (measured on * docs/registry.pdf and docs/design.pdf). Good enough for a band that * is only shown while flinging; never use a band to patch rows into a * whole-page raster -- `pardes_pdf_paint_highlights` exists for that. */ bbox.y0 += band_y; bbox.y1 = bbox.y0 + band_height; /* External samples are never marked FZ_PIXMAP_FLAG_FREE_SAMPLES. */ pixmap = fz_new_pixmap_with_bbox_and_data( ctx, fz_device_rgb(ctx), bbox, NULL, 1, samples); if (fz_pixmap_samples(ctx, pixmap) != samples || fz_pixmap_width(ctx, pixmap) != width || fz_pixmap_height(ctx, pixmap) != band_height || fz_pixmap_stride(ctx, pixmap) != stride || fz_pixmap_components(ctx, pixmap) != 4) fz_throw(ctx, FZ_ERROR_FORMAT, "MuPDF wrapped an unexpected RGBA layout"); /* * ponytail: this memset is ~9% of a fast scroll's profile and it has * twice measured as unremovable. Filling with 32-byte vector stores * instead (a memset of a multi-megabyte raster goes out through * non-temporal stores, 7.4 GB/s against 41.7 for a store loop) changed * a fling's median frame by nothing at all, and skipping the fill * ENTIRELY changed it by 1-2%: the cache misses it is blamed for are * paid either way by the glyph spans and the tint pass that walk the * same buffer immediately afterwards. Leave it alone. */ if (transparent_paper) fz_clear_pixmap(ctx, pixmap); else fz_clear_pixmap_with_value(ctx, pixmap, 0xFF); if (document->cached_display_list != NULL || document->display_list_candidate_page_number == page_number) { pardes_pdf_cache_display_list(document); device = fz_new_draw_device(ctx, ctm, pixmap); fz_run_display_list(ctx, document->cached_display_list, device, fz_identity, fz_infinite_rect, NULL); } else { device = fz_new_draw_device(ctx, ctm, pixmap); fz_run_page(ctx, document->cached_page, device, fz_identity, NULL); } fz_close_device(ctx, device); fz_drop_device(ctx, device); device = NULL; if (highlight_count != 0) { device = fz_new_draw_device(ctx, ctm, pixmap); pardes_pdf_fill_highlights(ctx, document, device, highlights, highlight_count); fz_close_device(ctx, device); } } fz_always(ctx) { fz_drop_device(ctx, device); /* Drops only the wrapper: caller-owned samples remain untouched. */ fz_drop_pixmap(ctx, pixmap); } fz_catch(ctx) { fz_report_error(ctx); return PARDES_PDF_ERROR; } /* MuPDF draws premultiplied samples. The public RGBA contract and tint * pass use straight alpha; preserve coverage and undo premultiplication. */ if (transparent_paper) { size_t pixel; for (pixel = 0; pixel < samples_len; pixel += 4) { unsigned int a = samples[pixel + 3]; int channel; if (a == 0 || a == 255) continue; for (channel = 0; channel < 3; ++channel) { unsigned int straight = (samples[pixel + channel] * 255u + a / 2) / a; samples[pixel + channel] = (unsigned char)(straight > 255 ? 255 : straight); } } } if (document->cached_display_list == NULL) document->display_list_candidate_page_number = page_number; return PARDES_PDF_OK; } int pardes_pdf_paint_highlights( pardes_pdf_document *document, int page_number, int dpi, int minimum_width, int minimum_height, int max_dimension, const pardes_pdf_highlight *highlights, size_t highlight_count, unsigned char *samples, size_t samples_len, int width, int height, int stride, int band_y, int band_height) { fz_context *ctx; fz_pixmap *pixmap = NULL; fz_device *device = NULL; fz_matrix ctm; fz_irect bbox; if (document == NULL || page_number < 0 || page_number >= document->page_count || dpi < 1 || minimum_width < 0 || minimum_height < 0 || max_dimension < 1 || (highlight_count != 0 && highlights == NULL) || highlight_count > PARDES_PDF_MAX_RESULT_QUADS || samples == NULL || width < 1 || height < 1 || width > INT_MAX / 4 || stride != width * 4 || band_y < 0 || band_height < 1 || band_y > height - band_height || (size_t)band_height > SIZE_MAX / (size_t)stride || samples_len != (size_t)stride * (size_t)band_height) return PARDES_PDF_ERROR; if (highlight_count == 0) return PARDES_PDF_OK; ctx = document->ctx; fz_var(pixmap); fz_var(device); fz_try(ctx) { pardes_pdf_render_geometry(document, page_number, dpi, minimum_width, minimum_height, max_dimension, &ctm, &bbox); if (fz_irect_width(bbox) != width || fz_irect_height(bbox) != height) fz_throw(ctx, FZ_ERROR_ARGUMENT, "PDF raster layout changed between measure and paint"); bbox.y0 += band_y; bbox.y1 = bbox.y0 + band_height; pixmap = fz_new_pixmap_with_bbox_and_data( ctx, fz_device_rgb(ctx), bbox, NULL, 1, samples); device = fz_new_draw_device(ctx, ctm, pixmap); pardes_pdf_fill_highlights(ctx, document, device, highlights, highlight_count); fz_close_device(ctx, device); } fz_always(ctx) { fz_drop_device(ctx, device); fz_drop_pixmap(ctx, pixmap); } fz_catch(ctx) { fz_report_error(ctx); return PARDES_PDF_ERROR; } return PARDES_PDF_OK; } int pardes_pdf_highlight_rows( pardes_pdf_document *document, int page_number, int dpi, int minimum_width, int minimum_height, int max_dimension, const pardes_pdf_highlight *highlights, size_t highlight_count, int *row_start, int *row_end) { fz_context *ctx; fz_matrix ctm; fz_irect bbox; float y0 = 0, y1 = 0; int any = 0; size_t i; if (row_start == NULL || row_end == NULL) return PARDES_PDF_ERROR; *row_start = *row_end = 0; if (document == NULL || page_number < 0 || page_number >= document->page_count || dpi < 1 || minimum_width < 0 || minimum_height < 0 || max_dimension < 1 || (highlight_count != 0 && highlights == NULL)) return PARDES_PDF_ERROR; ctx = document->ctx; fz_try(ctx) { pardes_pdf_render_geometry(document, page_number, dpi, minimum_width, minimum_height, max_dimension, &ctm, &bbox); for (i = 0; i < highlight_count; ++i) { fz_rect r; if (highlights[i].rgba[3] == 0 || !pardes_pdf_valid_quad(highlights[i].quad)) continue; r = fz_rect_from_quad(fz_transform_quad( pardes_pdf_quad_to_page(document, highlights[i].quad), ctm)); if (!any || r.y0 < y0) y0 = r.y0; if (!any || r.y1 > y1) y1 = r.y1; any = 1; } } fz_catch(ctx) { fz_report_error(ctx); return PARDES_PDF_ERROR; } if (any) { /* * Antialiasing covers the pixel a fractional edge falls in, and the * rasterizer paints row floor(y1) even for a zero-height quad. */ int lo = (int)floorf(y0) - bbox.y0; int hi = (int)floorf(y1) + 1 - bbox.y0; int h = fz_irect_height(bbox); *row_start = lo < 0 ? 0 : (lo > h ? h : lo); *row_end = hi < *row_start ? *row_start : (hi > h ? h : hi); } return PARDES_PDF_OK; } int pardes_pdf_page_text( pardes_pdf_document *document, int page_number, pardes_pdf_text *out) { fz_context *ctx; fz_buffer *buffer = NULL; unsigned char *data = NULL; size_t len = 0; if (document == NULL || out == NULL || page_number < 0 || page_number >= document->page_count) return PARDES_PDF_ERROR; memset(out, 0, sizeof(*out)); ctx = document->ctx; fz_var(buffer); fz_try(ctx) { pardes_pdf_cache_page(document, page_number, 1); buffer = fz_new_buffer_from_stext_page(ctx, document->cached_text); len = fz_buffer_storage(ctx, buffer, &data); } fz_catch(ctx) { fz_report_error(ctx); fz_drop_buffer(ctx, buffer); return PARDES_PDF_ERROR; } out->handle = buffer; out->data = data; out->len = len; return PARDES_PDF_OK; } void pardes_pdf_drop_text(pardes_pdf_document *document, void *buffer) { if (document != NULL && buffer != NULL) fz_drop_buffer(document->ctx, (fz_buffer *)buffer); } int pardes_pdf_search_page( pardes_pdf_document *document, int page_number, const char *needle, pardes_pdf_search_result *out) { fz_context *ctx; fz_search *search = NULL; pardes_pdf_search_quad *items = NULL; size_t capacity = 0; size_t quad_count = 0; size_t hit_count = 0; int status = PARDES_PDF_ERROR; if (document == NULL || needle == NULL || out == NULL || page_number < 0 || page_number >= document->page_count) return PARDES_PDF_ERROR; memset(out, 0, sizeof(*out)); ctx = document->ctx; fz_var(search); fz_var(items); fz_var(status); fz_try(ctx) { pardes_pdf_cache_page(document, page_number, 1); search = fz_new_search(ctx); fz_search_set_options( ctx, search, FZ_SEARCH_IGNORE_CASE, needle); /* fz_feed_search takes ownership; retain the document's cache ref. */ fz_feed_search(ctx, search, fz_keep_stext_page(ctx, document->cached_text), page_number); for (;;) { fz_search_result result = fz_search_forwards(ctx, search); if (result.reason == FZ_SEARCH_COMPLETE) break; if (result.reason == FZ_SEARCH_MORE_INPUT) { /* This is deliberately a one-page search. */ fz_feed_search(ctx, search, NULL, result.u.more_input.seq_needed); continue; } if (result.reason == FZ_SEARCH_MATCH) { fz_search_result_details *details = result.u.match.result; size_t match_quads; size_t needed; size_t next_capacity; size_t i; if (details == NULL || details->num_quads < 0 || (details->num_quads > 0 && details->quads == NULL)) fz_throw(ctx, FZ_ERROR_FORMAT, "MuPDF returned malformed search geometry"); if (hit_count >= PARDES_PDF_MAX_RESULT_QUADS) { status = PARDES_PDF_LIMIT_EXCEEDED; fz_throw(ctx, FZ_ERROR_LIMIT, "PDF search result limit exceeded"); } match_quads = (size_t)details->num_quads; if (match_quads > PARDES_PDF_MAX_RESULT_QUADS - quad_count) { status = PARDES_PDF_LIMIT_EXCEEDED; fz_throw(ctx, FZ_ERROR_LIMIT, "PDF search quad limit exceeded"); } needed = quad_count + match_quads; if (needed > capacity) { next_capacity = pardes_pdf_grown_capacity( capacity, needed, PARDES_PDF_MAX_RESULT_QUADS); items = fz_realloc_array( ctx, items, next_capacity, pardes_pdf_search_quad); capacity = next_capacity; } for (i = 0; i < match_quads; ++i) { items[quad_count + i].quad = pardes_pdf_quad_from_page( document, details->quads[i].quad); items[quad_count + i].hit = hit_count; } quad_count = needed; ++hit_count; continue; } fz_throw(ctx, FZ_ERROR_FORMAT, "MuPDF returned an unknown search state"); } status = PARDES_PDF_OK; } fz_always(ctx) { fz_drop_search(ctx, search); } fz_catch(ctx) { if (status != PARDES_PDF_LIMIT_EXCEEDED) fz_report_error(ctx); fz_free(ctx, items); return status; } out->handle = items; out->quads = items; out->quad_count = quad_count; out->hit_count = hit_count; return PARDES_PDF_OK; } void pardes_pdf_drop_search_result(pardes_pdf_document *document, void *result) { if (document != NULL && result != NULL) fz_free(document->ctx, result); } int pardes_pdf_select( pardes_pdf_document *document, int page_number, pardes_pdf_point start, pardes_pdf_point end, pardes_pdf_selection *out) { fz_context *ctx; fz_quad *raw = NULL; pardes_pdf_quad *items = NULL; fz_point a; fz_point b; int capacity = PARDES_PDF_INITIAL_RESULT_QUADS; int count = 0; int status = PARDES_PDF_ERROR; int i; if (document == NULL || out == NULL || page_number < 0 || page_number >= document->page_count || !pardes_pdf_valid_point(start) || !pardes_pdf_valid_point(end)) return PARDES_PDF_ERROR; memset(out, 0, sizeof(*out)); ctx = document->ctx; fz_var(raw); fz_var(items); fz_var(status); fz_try(ctx) { pardes_pdf_cache_page(document, page_number, 1); a = pardes_pdf_point_to_page(document, start); b = pardes_pdf_point_to_page(document, end); (void)fz_snap_selection( ctx, document->cached_text, &a, &b, FZ_SELECT_WORDS); raw = fz_malloc_array(ctx, capacity, fz_quad); for (;;) { int next_capacity; count = fz_highlight_selection( ctx, document->cached_text, a, b, raw, capacity); if (count < 0) fz_throw(ctx, FZ_ERROR_FORMAT, "MuPDF returned an invalid selection count"); if (count < capacity) break; /* One extra slot distinguishes exactly-at-limit from truncation. */ if (capacity >= PARDES_PDF_MAX_RESULT_QUADS + 1) { status = PARDES_PDF_LIMIT_EXCEEDED; fz_throw(ctx, FZ_ERROR_LIMIT, "PDF selection quad limit exceeded"); } next_capacity = capacity * 2; if (next_capacity > PARDES_PDF_MAX_RESULT_QUADS + 1) next_capacity = PARDES_PDF_MAX_RESULT_QUADS + 1; raw = fz_realloc_array(ctx, raw, next_capacity, fz_quad); capacity = next_capacity; } if (count > PARDES_PDF_MAX_RESULT_QUADS) { status = PARDES_PDF_LIMIT_EXCEEDED; fz_throw(ctx, FZ_ERROR_LIMIT, "PDF selection quad limit exceeded"); } if (count != 0) items = fz_malloc_array(ctx, (size_t)count, pardes_pdf_quad); for (i = 0; i < count; ++i) items[i] = pardes_pdf_quad_from_page(document, raw[i]); status = PARDES_PDF_OK; } fz_always(ctx) { fz_free(ctx, raw); } fz_catch(ctx) { if (status != PARDES_PDF_LIMIT_EXCEEDED) fz_report_error(ctx); fz_free(ctx, items); return status; } out->handle = items; out->quads = items; out->quad_count = (size_t)count; out->start = pardes_pdf_point_from_page(document, a); out->end = pardes_pdf_point_from_page(document, b); return PARDES_PDF_OK; } void pardes_pdf_drop_selection(pardes_pdf_document *document, void *selection) { if (document != NULL && selection != NULL) fz_free(document->ctx, selection); } int pardes_pdf_point_inside_quad(pardes_pdf_point point, pardes_pdf_quad quad) { fz_quad native; if (!pardes_pdf_valid_point(point) || !pardes_pdf_valid_quad(quad)) return 0; native.ul = fz_make_point(quad.ul.x, quad.ul.y); native.ur = fz_make_point(quad.ur.x, quad.ur.y); native.ll = fz_make_point(quad.ll.x, quad.ll.y); native.lr = fz_make_point(quad.lr.x, quad.lr.y); return fz_is_point_inside_quad( fz_make_point(point.x, point.y), native) != 0; } int pardes_pdf_link_at( pardes_pdf_document *document, int page_number, pardes_pdf_point point, pardes_pdf_link *out) { fz_context *ctx; if (out == NULL) return PARDES_PDF_ERROR; memset(out, 0, sizeof(*out)); out->page = -1; if (document == NULL || page_number < 0 || page_number >= document->page_count) return PARDES_PDF_ERROR; if (!pardes_pdf_valid_point(point) || point.x < 0 || point.x > 1 || point.y < 0 || point.y > 1) return PARDES_PDF_OK; ctx = document->ctx; fz_try(ctx) { fz_link *link; fz_point page_point; pardes_pdf_cache_page(document, page_number, 0); if (!document->cached_links_loaded) { document->cached_links = fz_load_links(ctx, document->cached_page); document->cached_links_loaded = 1; } page_point = pardes_pdf_point_to_page(document, point); for (link = document->cached_links; link != NULL; link = link->next) { size_t len; int destination_kind; int destination_page = -1; fz_link_dest destination = fz_make_link_dest_none(); if (!pardes_pdf_valid_bounds(link->rect) || !fz_is_point_inside_rect(page_point, link->rect) || link->uri == NULL || link->uri[0] == 0) continue; for (len = 0; len <= PARDES_PDF_MAX_LINK_URI_BYTES && link->uri[len] != 0; ++len) ; if (len > PARDES_PDF_MAX_LINK_URI_BYTES) continue; destination_kind = PARDES_PDF_OUTLINE_DESTINATION_EXTERNAL; if (!fz_is_external_link(ctx, link->uri)) { destination = fz_resolve_link_dest(ctx, document->doc, link->uri); if (destination.loc.chapter >= 0 && destination.loc.page >= 0) destination_page = fz_page_number_from_location( ctx, document->doc, destination.loc); if (destination_page >= 0 && destination_page < document->page_count) destination_kind = PARDES_PDF_OUTLINE_DESTINATION_INTERNAL; else if (link->uri[0] == '#') continue; } out->quad = pardes_pdf_quad_from_page( document, fz_quad_from_rect(link->rect)); out->destination_kind = destination_kind; if (destination_kind == PARDES_PDF_OUTLINE_DESTINATION_INTERNAL) { out->page = destination_page; out->has_x = isfinite(destination.x); out->has_y = isfinite(destination.y); if (out->has_x) out->x = destination.x; if (out->has_y) out->y = destination.y; } out->uri = (const unsigned char *)link->uri; out->uri_len = len; break; } } fz_catch(ctx) { memset(out, 0, sizeof(*out)); return PARDES_PDF_ERROR; } return PARDES_PDF_OK; } int pardes_pdf_copy_selection( pardes_pdf_document *document, int page_number, pardes_pdf_point start, pardes_pdf_point end, pardes_pdf_owned_text *out) { fz_context *ctx; char *text = NULL; fz_point a; fz_point b; if (document == NULL || out == NULL || page_number < 0 || page_number >= document->page_count || !pardes_pdf_valid_point(start) || !pardes_pdf_valid_point(end)) return PARDES_PDF_ERROR; memset(out, 0, sizeof(*out)); ctx = document->ctx; fz_var(text); fz_try(ctx) { pardes_pdf_cache_page(document, page_number, 1); a = pardes_pdf_point_to_page(document, start); b = pardes_pdf_point_to_page(document, end); text = fz_copy_selection(ctx, document->cached_text, a, b, 0); } fz_catch(ctx) { fz_report_error(ctx); fz_free(ctx, text); return PARDES_PDF_ERROR; } out->handle = text; out->data = (const unsigned char *)text; out->len = text == NULL ? 0 : strlen(text); return PARDES_PDF_OK; } void pardes_pdf_drop_owned_text(pardes_pdf_document *document, void *text) { if (document != NULL && text != NULL) fz_free(document->ctx, text); } int pardes_pdf_copy_rectangle( pardes_pdf_document *document, int page_number, pardes_pdf_quad quad, pardes_pdf_owned_text *out) { fz_context *ctx; char *text = NULL; fz_quad native; if (document == NULL || out == NULL || page_number < 0 || page_number >= document->page_count || !pardes_pdf_valid_quad(quad)) return PARDES_PDF_ERROR; memset(out, 0, sizeof(*out)); ctx = document->ctx; fz_var(text); fz_try(ctx) { pardes_pdf_cache_page(document, page_number, 1); native.ul = pardes_pdf_point_to_page(document, quad.ul); native.ur = pardes_pdf_point_to_page(document, quad.ur); native.ll = pardes_pdf_point_to_page(document, quad.ll); native.lr = pardes_pdf_point_to_page(document, quad.lr); text = fz_copy_rectangle(ctx, document->cached_text, fz_rect_from_quad(native), 0); } fz_catch(ctx) { fz_free(ctx, text); return PARDES_PDF_ERROR; } out->handle = text; out->data = (const unsigned char *)text; out->len = text == NULL ? 0 : strlen(text); return PARDES_PDF_OK; } 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); }