Files
ElyOffice/crates/ely-pdf-engine/shim/ely_pages.c
T
ZacharyZhang-NY b7fca0146d Ely Office: six offline-first desktop apps in Rust and GPUI
ElyDoc, ElyTable, ElyShow (LibreOfficeKit), ElyPDF (MuPDF), ElyTree (Git),
ElyMark (Milkdown and CodeMirror in CEF). ElyMail and ElyAgent follow.
2026-10-08 21:20:08 -04:00

488 lines
14 KiB
C

/* Page edits, undo and saving; one undo step each. */
#include <stdlib.h>
#include "ely_shim.h"
pdf_document *ely_pdf_of(fz_context *ctx, fz_document *doc)
{
pdf_document *pdf = pdf_specifics(ctx, doc);
if (!pdf)
fz_throw(ctx, FZ_ERROR_ARGUMENT, "not a PDF");
return pdf;
}
/* MuPDF's font and image caches outlive an undo; they go. */
void ely_forget_resources(fz_context *ctx, pdf_document *pdf)
{
pdf_drop_resource_tables(ctx, pdf);
pdf->resources.fonts = NULL;
pdf->resources.colorspaces = NULL;
pdf->resources.images = NULL;
}
/* Reports the failure; a begun step is rolled back. */
int ely_edit_failed(fz_context *ctx, pdf_document *pdf, int begun)
{
int code = ely_fail(ctx);
if (begun)
{
fz_try(ctx)
{
pdf_abandon_operation(ctx, pdf);
ely_forget_resources(ctx, pdf);
}
fz_catch(ctx)
ely_fail_also(ctx, "rolling back");
}
return code;
}
int ely_journal(fz_context *ctx, fz_document *doc)
{
fz_try(ctx)
pdf_enable_journal(ctx, ely_pdf_of(ctx, doc));
fz_catch(ctx)
return ely_fail(ctx);
return 0;
}
int ely_undo(fz_context *ctx, fz_document *doc, int redo)
{
fz_try(ctx)
{
pdf_document *pdf = ely_pdf_of(ctx, doc);
if (redo)
pdf_redo(ctx, pdf);
else
pdf_undo(ctx, pdf);
ely_forget_resources(ctx, pdf);
}
fz_catch(ctx)
return ely_fail(ctx);
return 0;
}
/* MuPDF's undo position and step count; saves add steps. */
int ely_state(fz_context *ctx, fz_document *doc, int *at, int *count)
{
fz_try(ctx)
*at = pdf_undoredo_state(ctx, ely_pdf_of(ctx, doc), count);
fz_catch(ctx)
return ely_fail(ctx);
return 0;
}
static int by_number(const void *a, const void *b)
{
return *(const int *)a - *(const int *)b;
}
/* Object numbers, sorted for lookups. */
typedef struct
{
int *nums;
int n;
} gone_set;
static int is_gone(fz_context *ctx, gone_set *gone, pdf_obj *obj)
{
int num = pdf_to_num(ctx, obj);
return num && bsearch(&num, gone->nums, gone->n, sizeof(int), by_number) != NULL;
}
/* Deleted pages, and the annotations they held. */
typedef struct
{
gone_set pages;
gone_set annots;
} gone_pages;
/* The page a link, bookmark or action points to. */
pdf_obj *ely_target(fz_context *ctx, pdf_document *pdf, pdf_obj *obj)
{
pdf_obj *dest = pdf_dict_get(ctx, obj, PDF_NAME(Dest));
if (!dest)
{
pdf_obj *action = pdf_dict_get(ctx, obj, PDF_NAME(A));
if (!pdf_name_eq(ctx, pdf_dict_get(ctx, action, PDF_NAME(S)), PDF_NAME(GoTo)))
return NULL;
dest = pdf_dict_get(ctx, action, PDF_NAME(D));
}
if (pdf_is_name(ctx, dest) || pdf_is_string(ctx, dest))
dest = pdf_lookup_dest(ctx, pdf, dest);
if (pdf_is_dict(ctx, dest))
dest = pdf_dict_get(ctx, dest, PDF_NAME(D));
return pdf_is_array(ctx, dest) ? pdf_array_get(ctx, dest, 0) : NULL;
}
/* Bookmarks to gone pages keep their place, lose the jump. */
static void forget_in_outline(fz_context *ctx, pdf_document *pdf, pdf_obj *item, gone_set *gone)
{
for (; item; item = pdf_dict_get(ctx, item, PDF_NAME(Next)))
{
pdf_obj *page = ely_target(ctx, pdf, item);
if (page && is_gone(ctx, gone, page))
{
pdf_dict_del(ctx, item, PDF_NAME(Dest));
pdf_dict_del(ctx, item, PDF_NAME(A));
}
forget_in_outline(ctx, pdf, pdf_dict_get(ctx, item, PDF_NAME(First)), gone);
}
}
/* Whether a field keeps a widget on a kept page. */
static int prune_field(fz_context *ctx, pdf_obj *field, gone_pages *gone)
{
pdf_obj *kids = pdf_dict_get(ctx, field, PDF_NAME(Kids));
int i;
if (!kids)
{
/* A deleted page held it, or its /P names one. */
pdf_obj *page = pdf_dict_get(ctx, field, PDF_NAME(P));
if (!pdf_name_eq(ctx, pdf_dict_get(ctx, field, PDF_NAME(Subtype)), PDF_NAME(Widget)))
return 1;
return !is_gone(ctx, &gone->annots, field) && !(page && is_gone(ctx, &gone->pages, page));
}
for (i = pdf_array_len(ctx, kids) - 1; i >= 0; i--)
if (!prune_field(ctx, pdf_array_get(ctx, kids, i), gone))
pdf_array_delete(ctx, kids, i);
return pdf_array_len(ctx, kids) > 0;
}
/* Drops what pointed into deleted pages. */
static void forget_pages(fz_context *ctx, pdf_document *pdf, gone_pages *gone)
{
pdf_obj *root = pdf_dict_get(ctx, pdf_trailer(ctx, pdf), PDF_NAME(Root));
pdf_obj *outlines = pdf_dict_get(ctx, root, PDF_NAME(Outlines));
pdf_obj *fields = pdf_dict_getp(ctx, root, "AcroForm/Fields");
int i, j, count = pdf_count_pages(ctx, pdf);
forget_in_outline(ctx, pdf, pdf_dict_get(ctx, outlines, PDF_NAME(First)), &gone->pages);
for (i = 0; i < count; i++)
{
pdf_obj *annots = pdf_dict_get(ctx, pdf_lookup_page_obj(ctx, pdf, i), PDF_NAME(Annots));
for (j = pdf_array_len(ctx, annots) - 1; j >= 0; j--)
{
pdf_obj *annot = pdf_array_get(ctx, annots, j);
pdf_obj *page;
if (!pdf_name_eq(ctx, pdf_dict_get(ctx, annot, PDF_NAME(Subtype)), PDF_NAME(Link)))
continue;
page = ely_target(ctx, pdf, annot);
if (page && is_gone(ctx, &gone->pages, page))
pdf_array_delete(ctx, annots, j);
}
}
for (i = pdf_array_len(ctx, fields) - 1; i >= 0; i--)
if (!prune_field(ctx, pdf_array_get(ctx, fields, i), gone))
pdf_array_delete(ctx, fields, i);
}
/* Checks `pages` are sorted page numbers without repeats. */
static void check_pages(fz_context *ctx, pdf_document *pdf, const int *pages, int n)
{
int i, count = pdf_count_pages(ctx, pdf);
if (n <= 0)
fz_throw(ctx, FZ_ERROR_ARGUMENT, "no pages given");
for (i = 0; i < n; i++)
if (pages[i] < 0 || pages[i] >= count || (i > 0 && pages[i] <= pages[i - 1]))
fz_throw(ctx, FZ_ERROR_ARGUMENT, "page %d is out of order or range", pages[i]);
}
/* Deletes checked `pages` and what pointed there. */
static void delete_pages(fz_context *ctx, pdf_document *pdf, const int *pages, int n)
{
gone_pages gone = { { NULL, n }, { NULL, 0 } };
int i, j, held = 0;
fz_var(gone.pages.nums);
fz_var(gone.annots.nums);
fz_try(ctx)
{
gone.pages.nums = fz_malloc_array(ctx, n, int);
for (i = 0; i < n; i++)
{
pdf_obj *page = pdf_lookup_page_obj(ctx, pdf, pages[i]);
gone.pages.nums[i] = pdf_to_num(ctx, page);
held += pdf_array_len(ctx, pdf_dict_get(ctx, page, PDF_NAME(Annots)));
}
gone.annots.nums = fz_malloc_array(ctx, held + 1, int);
for (i = 0; i < n; i++)
{
pdf_obj *annots = pdf_dict_get(ctx, pdf_lookup_page_obj(ctx, pdf, pages[i]), PDF_NAME(Annots));
for (j = 0; j < pdf_array_len(ctx, annots); j++)
gone.annots.nums[gone.annots.n++] = pdf_to_num(ctx, pdf_array_get(ctx, annots, j));
}
qsort(gone.pages.nums, n, sizeof(int), by_number);
qsort(gone.annots.nums, gone.annots.n, sizeof(int), by_number);
for (i = n - 1; i >= 0; i--)
pdf_delete_page(ctx, pdf, pages[i]);
forget_pages(ctx, pdf, &gone);
}
fz_always(ctx)
{
fz_free(ctx, gone.pages.nums);
fz_free(ctx, gone.annots.nums);
}
fz_catch(ctx)
fz_rethrow(ctx);
}
int ely_delete_pages(fz_context *ctx, fz_document *doc, const int *pages, int n)
{
pdf_document *pdf = NULL;
int begun = 0;
fz_var(pdf);
fz_var(begun);
fz_try(ctx)
{
pdf = ely_pdf_of(ctx, doc);
check_pages(ctx, pdf, pages, n);
if (n == pdf_count_pages(ctx, pdf))
fz_throw(ctx, FZ_ERROR_ARGUMENT, "a PDF keeps at least one page");
pdf_begin_operation(ctx, pdf, "Delete pages");
begun = 1;
delete_pages(ctx, pdf, pages, n);
pdf_end_operation(ctx, pdf);
}
fz_catch(ctx)
return ely_edit_failed(ctx, pdf, begun);
return 0;
}
/* Moves `pages` before page `to`, in their order. */
int ely_move_pages(fz_context *ctx, fz_document *doc, const int *pages, int n, int to)
{
pdf_document *pdf = NULL;
pdf_obj **moved = NULL;
int begun = 0, i, at;
fz_var(pdf);
fz_var(begun);
fz_var(moved);
fz_try(ctx)
{
pdf = ely_pdf_of(ctx, doc);
check_pages(ctx, pdf, pages, n);
if (to < 0 || to > pdf_count_pages(ctx, pdf))
fz_throw(ctx, FZ_ERROR_ARGUMENT, "no place %d to move to", to);
moved = fz_calloc(ctx, n, sizeof *moved);
for (i = 0; i < n; i++)
moved[i] = pdf_keep_obj(ctx, pdf_lookup_page_obj(ctx, pdf, pages[i]));
pdf_begin_operation(ctx, pdf, "Move pages");
begun = 1;
for (i = n - 1; i >= 0; i--)
pdf_delete_page(ctx, pdf, pages[i]);
for (at = to, i = 0; i < n; i++)
at -= pages[i] < to;
for (i = 0; i < n; i++)
pdf_insert_page(ctx, pdf, at + i, moved[i]);
pdf_end_operation(ctx, pdf);
}
fz_always(ctx)
{
for (i = 0; moved && i < n; i++)
pdf_drop_obj(ctx, moved[i]);
fz_free(ctx, moved);
}
fz_catch(ctx)
return ely_edit_failed(ctx, pdf, begun);
return 0;
}
/* Turns `pages` by a multiple of 90 degrees. */
int ely_rotate_pages(fz_context *ctx, fz_document *doc, const int *pages, int n, int by)
{
pdf_document *pdf = NULL;
int begun = 0, i;
fz_var(pdf);
fz_var(begun);
fz_try(ctx)
{
pdf = ely_pdf_of(ctx, doc);
check_pages(ctx, pdf, pages, n);
if (by % 90)
fz_throw(ctx, FZ_ERROR_ARGUMENT, "a turn of %d degrees", by);
pdf_begin_operation(ctx, pdf, "Rotate pages");
begun = 1;
for (i = 0; i < n; i++)
{
pdf_obj *page = pdf_lookup_page_obj(ctx, pdf, pages[i]);
int now = pdf_to_int(ctx, pdf_dict_get_inheritable(ctx, page, PDF_NAME(Rotate)));
pdf_dict_put_int(ctx, page, PDF_NAME(Rotate), ((now + by) % 360 + 360) % 360);
}
pdf_end_operation(ctx, pdf);
}
fz_catch(ctx)
return ely_edit_failed(ctx, pdf, begun);
return 0;
}
/* Seen margins (top, right, bottom, left) to user sides. */
static fz_rect inset(fz_rect box, int rotate, const float seen[4])
{
/* User sides: 0 y1, 1 x1, 2 y0, 3 x0. */
static const int under[4][4] = { { 0, 1, 2, 3 }, { 3, 0, 1, 2 }, { 2, 3, 0, 1 }, { 1, 2, 3, 0 } };
float side[4] = { 0, 0, 0, 0 };
int i, turn = ((rotate % 360 + 360) % 360) / 90;
for (i = 0; i < 4; i++)
side[under[turn][i]] = seen[i];
box.y1 -= side[0];
box.x1 -= side[1];
box.y0 += side[2];
box.x0 += side[3];
return box;
}
/* Crops `pages` by seen margins in points; zeros uncrop. */
int ely_crop_pages(fz_context *ctx, fz_document *doc, const int *pages, int n, const float margins[4])
{
pdf_document *pdf = NULL;
int begun = 0, i;
fz_var(pdf);
fz_var(begun);
fz_try(ctx)
{
int none = margins[0] == 0 && margins[1] == 0 && margins[2] == 0 && margins[3] == 0;
pdf = ely_pdf_of(ctx, doc);
check_pages(ctx, pdf, pages, n);
pdf_begin_operation(ctx, pdf, "Crop pages");
begun = 1;
for (i = 0; i < n; i++)
{
pdf_obj *page = pdf_lookup_page_obj(ctx, pdf, pages[i]);
fz_rect media = pdf_to_rect(ctx, pdf_dict_get_inheritable(ctx, page, PDF_NAME(MediaBox)));
int rotate = pdf_to_int(ctx, pdf_dict_get_inheritable(ctx, page, PDF_NAME(Rotate)));
/* Points as shown; user units may be larger. */
float unit = pdf_to_real(ctx, pdf_dict_get(ctx, page, PDF_NAME(UserUnit)));
float user[4];
int side;
for (side = 0; side < 4; side++)
user[side] = margins[side] / (unit > 0 ? unit : 1);
fz_rect crop = inset(media, rotate, user);
if (none)
{
pdf_dict_del(ctx, page, PDF_NAME(CropBox));
continue;
}
if (crop.x1 - crop.x0 < 1 || crop.y1 - crop.y0 < 1)
fz_throw(ctx, FZ_ERROR_ARGUMENT, "the margins leave nothing of page %d", pages[i] + 1);
pdf_dict_put_rect(ctx, page, PDF_NAME(CropBox), crop);
}
pdf_end_operation(ctx, pdf);
}
fz_catch(ctx)
return ely_edit_failed(ctx, pdf, begun);
return 0;
}
/* A blank page of w by h points at `at`. */
int ely_insert_blank(fz_context *ctx, fz_document *doc, int at, float w, float h)
{
pdf_document *pdf = NULL;
pdf_obj *page = NULL;
int begun = 0;
fz_var(pdf);
fz_var(page);
fz_var(begun);
fz_try(ctx)
{
pdf = ely_pdf_of(ctx, doc);
if (at < 0 || at > pdf_count_pages(ctx, pdf) || w < 1 || h < 1)
fz_throw(ctx, FZ_ERROR_ARGUMENT, "a blank page at %d of %g by %g", at, w, h);
pdf_begin_operation(ctx, pdf, "Insert a blank page");
begun = 1;
page = pdf_add_page(ctx, pdf, fz_make_rect(0, 0, w, h), 0, NULL, NULL);
pdf_insert_page(ctx, pdf, at, page);
pdf_end_operation(ctx, pdf);
}
fz_always(ctx)
pdf_drop_obj(ctx, page);
fz_catch(ctx)
return ely_edit_failed(ctx, pdf, begun);
return 0;
}
/* Pages of `pdf` other than the sorted `keep`. */
static int *others(fz_context *ctx, pdf_document *pdf, const int *keep, int n, int *k)
{
int i, count = pdf_count_pages(ctx, pdf);
int *rest = fz_malloc_array(ctx, count + 1, int);
*k = 0;
for (i = 0; i < count; i++)
if (bsearch(&i, keep, n, sizeof(int), by_number) == NULL)
rest[(*k)++] = i;
return rest;
}
/* A snapshot, reopened, changed, unsigned, compacted to `out`. */
void ely_save_copy(fz_context *ctx, pdf_document *pdf, const char *raw, const char *out, const char *password,
const pdf_write_options *opts, ely_change *change, void *arg)
{
fz_document *copy = NULL;
fz_var(copy);
fz_try(ctx)
{
pdf_write_options compact = opts ? *opts : pdf_default_write_options;
pdf_save_snapshot(ctx, pdf, raw);
copy = fz_open_document(ctx, raw);
if (fz_needs_password(ctx, copy) && !fz_authenticate_password(ctx, copy, password))
fz_throw(ctx, FZ_ERROR_ARGUMENT, "the written copy refused its password");
if (change)
change(ctx, copy, arg);
ely_unsign(ctx, ely_pdf_of(ctx, copy));
/* Unused objects, such as deleted pages, go. */
compact.do_garbage = 2;
pdf_save_document(ctx, ely_pdf_of(ctx, copy), out, &compact);
}
fz_always(ctx)
fz_drop_document(ctx, copy);
fz_catch(ctx)
fz_rethrow(ctx);
}
typedef struct
{
const int *pages;
int n;
} kept;
/* Deletes every page not kept. */
static void keep_only(fz_context *ctx, fz_document *copy, void *arg)
{
kept *keep = arg;
pdf_document *written = ely_pdf_of(ctx, copy);
int k, *rest = others(ctx, written, keep->pages, keep->n, &k);
fz_try(ctx)
{
if (k > 0)
delete_pages(ctx, written, rest, k);
}
fz_always(ctx)
fz_free(ctx, rest);
fz_catch(ctx)
fz_rethrow(ctx);
}
int ely_save(fz_context *ctx, fz_document *doc, const char *raw, const char *out, const char *password)
{
fz_try(ctx)
ely_save_copy(ctx, ely_pdf_of(ctx, doc), raw, out, password, NULL, NULL, NULL);
fz_catch(ctx)
return ely_fail(ctx);
return 0;
}
/* Saves only `pages` to `out`; the document is untouched. */
int ely_extract(fz_context *ctx, fz_document *doc, const int *pages, int n,
const char *raw, const char *out, const char *password)
{
fz_try(ctx)
{
pdf_document *pdf = ely_pdf_of(ctx, doc);
kept keep = { pages, n };
check_pages(ctx, pdf, pages, n);
ely_save_copy(ctx, pdf, raw, out, password, NULL, keep_only, &keep);
}
fz_catch(ctx)
return ely_fail(ctx);
return 0;
}