ElyDoc, ElyTable, ElyShow (LibreOfficeKit), ElyPDF (MuPDF), ElyTree (Git), ElyMark (Milkdown and CodeMirror in CEF). ElyMail and ElyAgent follow.
396 lines
12 KiB
C
396 lines
12 KiB
C
/* Cleaning into a new file, counted before and after (PDF-10). */
|
|
|
|
#include <string.h>
|
|
|
|
#include "ely_shim.h"
|
|
|
|
enum { CLEAN_META = 1, CLEAN_ANNOTS = 2, CLEAN_FILES = 4, CLEAN_HIDDEN = 8, CLEAN_ALL = 15 };
|
|
|
|
/* What cleaning reaches, counted. */
|
|
typedef struct
|
|
{
|
|
int meta, annots, files, hidden_pages, scripts, hidden_annots;
|
|
} reach;
|
|
|
|
/* The filter running a page, and what it drops. */
|
|
typedef struct
|
|
{
|
|
pdf_processor *top;
|
|
int dropped;
|
|
} hiding;
|
|
|
|
static pdf_processor *hide_factory(fz_context *ctx, pdf_document *doc, pdf_processor *chain, int struct_parents,
|
|
fz_matrix transform, pdf_filter_options *options, void *sopts)
|
|
{
|
|
hiding *h = ((pdf_sanitize_filter_options *)sopts)->opaque;
|
|
h->top = pdf_new_sanitize_filter(ctx, doc, chain, struct_parents, transform, options, sopts);
|
|
/* Layers count as hidden only for a viewer's usage. */
|
|
h->top->usage = "View";
|
|
return h->top;
|
|
}
|
|
|
|
/* Unseen text: modes 3 and 7, or zero opacity. */
|
|
static int hide_text(fz_context *ctx, void *opaque, int *ucs, int len, fz_matrix trm, fz_matrix ctm, fz_rect bbox,
|
|
int tr, float ca, float CA)
|
|
{
|
|
hiding *h = opaque;
|
|
int fills = tr == 0 || tr == 2 || tr == 4 || tr == 6, strokes = tr == 1 || tr == 2 || tr == 5 || tr == 6;
|
|
int drop = h->top->hidden > 0 || tr == 3 || tr == 7 || ((!fills || ca == 0) && (!strokes || CA == 0));
|
|
(void)ctx, (void)ucs, (void)len, (void)trm, (void)ctm, (void)bbox;
|
|
h->dropped += drop;
|
|
return drop;
|
|
}
|
|
|
|
/* Drawing inside a layer that is off. */
|
|
static int hide_cull(fz_context *ctx, void *opaque, fz_rect bbox, fz_cull_type type)
|
|
{
|
|
hiding *h = opaque;
|
|
(void)ctx, (void)bbox, (void)type;
|
|
h->dropped += h->top->hidden > 0;
|
|
return h->top->hidden > 0;
|
|
}
|
|
|
|
/* Drops hidden text and hidden layers from `page`; how many. */
|
|
static int drop_hidden(fz_context *ctx, pdf_document *pdf, pdf_page *page)
|
|
{
|
|
hiding h = { NULL, 0 };
|
|
pdf_sanitize_filter_options sopts = { 0 };
|
|
pdf_filter_factory list[2] = { { hide_factory, NULL }, { NULL, NULL } };
|
|
pdf_filter_options opts = { 0 };
|
|
sopts.opaque = &h;
|
|
sopts.text_filter = hide_text;
|
|
sopts.culler = hide_cull;
|
|
list[0].options = &sopts;
|
|
opts.recurse = 1;
|
|
opts.filters = list;
|
|
pdf_filter_page_contents(ctx, pdf, page, &opts);
|
|
if (h.dropped)
|
|
ely_drop_alt(ctx, pdf, page);
|
|
return h.dropped;
|
|
}
|
|
|
|
static int is_script(fz_context *ctx, pdf_obj *action)
|
|
{
|
|
return pdf_is_dict(ctx, action) &&
|
|
(pdf_name_eq(ctx, pdf_dict_get(ctx, action, PDF_NAME(S)), PDF_NAME(JavaScript)) || pdf_dict_get(ctx, action, PDF_NAME(JS)));
|
|
}
|
|
|
|
/* `action` without its scripts, their Next chains kept. */
|
|
static pdf_obj *spliced(fz_context *ctx, pdf_document *pdf, pdf_obj *action, int *n)
|
|
{
|
|
pdf_obj *out = NULL, *each = NULL;
|
|
int i, k;
|
|
fz_var(each);
|
|
if (is_script(ctx, action))
|
|
{
|
|
if (pdf_mark_obj(ctx, action))
|
|
return NULL;
|
|
(*n)++;
|
|
fz_try(ctx)
|
|
out = spliced(ctx, pdf, pdf_dict_get(ctx, action, PDF_NAME(Next)), n);
|
|
fz_always(ctx)
|
|
pdf_unmark_obj(ctx, action);
|
|
fz_catch(ctx)
|
|
fz_rethrow(ctx);
|
|
return out;
|
|
}
|
|
if (!pdf_is_array(ctx, action))
|
|
return pdf_keep_obj(ctx, action);
|
|
out = pdf_new_array(ctx, pdf, pdf_array_len(ctx, action));
|
|
fz_try(ctx)
|
|
for (i = 0; i < pdf_array_len(ctx, action); i++)
|
|
{
|
|
each = spliced(ctx, pdf, pdf_array_get(ctx, action, i), n);
|
|
if (pdf_is_array(ctx, each))
|
|
for (k = 0; k < pdf_array_len(ctx, each); k++)
|
|
pdf_array_push(ctx, out, pdf_array_get(ctx, each, k));
|
|
else if (each)
|
|
pdf_array_push(ctx, out, each);
|
|
pdf_drop_obj(ctx, each);
|
|
each = NULL;
|
|
}
|
|
fz_always(ctx)
|
|
pdf_drop_obj(ctx, each);
|
|
fz_catch(ctx)
|
|
{
|
|
pdf_drop_obj(ctx, out);
|
|
fz_rethrow(ctx);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
/* Splices scripts out of `dict[key]`; how many. */
|
|
static int splice_key(fz_context *ctx, pdf_document *pdf, pdf_obj *dict, const char *key)
|
|
{
|
|
pdf_obj *now = pdf_dict_gets(ctx, dict, key), *kept;
|
|
int n = 0;
|
|
if (!now)
|
|
return 0;
|
|
kept = spliced(ctx, pdf, now, &n);
|
|
if (n == 0)
|
|
pdf_drop_obj(ctx, kept);
|
|
else if (kept && !(pdf_is_array(ctx, kept) && pdf_array_len(ctx, kept) == 0))
|
|
pdf_dict_puts_drop(ctx, dict, key, kept);
|
|
else
|
|
{
|
|
pdf_drop_obj(ctx, kept);
|
|
pdf_dict_dels(ctx, dict, key);
|
|
}
|
|
return n;
|
|
}
|
|
|
|
static int drop_key(fz_context *ctx, pdf_obj *dict, const char *key)
|
|
{
|
|
int had = pdf_dict_gets(ctx, dict, key) != NULL;
|
|
pdf_dict_dels(ctx, dict, key);
|
|
return had;
|
|
}
|
|
|
|
/* Cleans `obj` and the direct values inside it. */
|
|
static void clean_obj(fz_context *ctx, pdf_document *pdf, pdf_obj *obj, int mask, reach *r)
|
|
{
|
|
pdf_obj *aa;
|
|
int i;
|
|
if (pdf_is_dict(ctx, obj))
|
|
{
|
|
if (mask & CLEAN_META)
|
|
r->meta += drop_key(ctx, obj, "Metadata") + drop_key(ctx, obj, "PieceInfo");
|
|
if (mask & CLEAN_FILES)
|
|
{
|
|
r->files += drop_key(ctx, obj, "EF");
|
|
drop_key(ctx, obj, "RF");
|
|
drop_key(ctx, obj, "AF");
|
|
}
|
|
if (mask & CLEAN_HIDDEN)
|
|
{
|
|
r->scripts += splice_key(ctx, pdf, obj, "A") + splice_key(ctx, pdf, obj, "OpenAction") +
|
|
splice_key(ctx, pdf, obj, "Next");
|
|
aa = pdf_dict_get(ctx, obj, PDF_NAME(AA));
|
|
for (i = pdf_dict_len(ctx, aa) - 1; i >= 0; i--)
|
|
r->scripts += splice_key(ctx, pdf, aa, pdf_to_name(ctx, pdf_dict_get_key(ctx, aa, i)));
|
|
if (pdf_is_dict(ctx, aa) && pdf_dict_len(ctx, aa) == 0)
|
|
pdf_dict_del(ctx, obj, PDF_NAME(AA));
|
|
}
|
|
for (i = 0; i < pdf_dict_len(ctx, obj); i++)
|
|
if (!pdf_is_indirect(ctx, pdf_dict_get_val(ctx, obj, i)))
|
|
clean_obj(ctx, pdf, pdf_dict_get_val(ctx, obj, i), mask, r);
|
|
}
|
|
else if (pdf_is_array(ctx, obj))
|
|
for (i = 0; i < pdf_array_len(ctx, obj); i++)
|
|
if (!pdf_is_indirect(ctx, pdf_array_get(ctx, obj, i)))
|
|
clean_obj(ctx, pdf, pdf_array_get(ctx, obj, i), mask, r);
|
|
}
|
|
|
|
/* Every object: metadata, embedded bytes, scripts. */
|
|
static void clean_all(fz_context *ctx, pdf_document *pdf, int mask, reach *r)
|
|
{
|
|
int i;
|
|
for (i = 1; i < pdf_xref_len(ctx, pdf); i++)
|
|
{
|
|
pdf_obj *obj = pdf_new_indirect(ctx, pdf, i, 0);
|
|
fz_try(ctx)
|
|
clean_obj(ctx, pdf, pdf_resolve_indirect(ctx, obj), mask, r);
|
|
fz_always(ctx)
|
|
pdf_drop_obj(ctx, obj);
|
|
fz_catch(ctx)
|
|
fz_rethrow(ctx);
|
|
}
|
|
}
|
|
|
|
static int unseen(fz_context *ctx, pdf_obj *annot)
|
|
{
|
|
return (pdf_dict_get_int(ctx, annot, PDF_NAME(F)) & (PDF_ANNOT_IS_HIDDEN | PDF_ANNOT_IS_INVISIBLE)) != 0;
|
|
}
|
|
|
|
/* Hidden fields go with their values. */
|
|
static int drop_hidden_widgets(fz_context *ctx, pdf_page *page)
|
|
{
|
|
pdf_annot *widget;
|
|
int n = 0;
|
|
restart:
|
|
for (widget = pdf_first_widget(ctx, page); widget; widget = pdf_next_widget(ctx, widget))
|
|
if (unseen(ctx, pdf_annot_obj(ctx, widget)))
|
|
{
|
|
pdf_obj *field = pdf_keep_obj(ctx, pdf_annot_obj(ctx, widget));
|
|
fz_try(ctx)
|
|
{
|
|
pdf_delete_annot(ctx, page, widget);
|
|
ely_unlink_field(ctx, page->doc, field);
|
|
}
|
|
fz_always(ctx)
|
|
pdf_drop_obj(ctx, field);
|
|
fz_catch(ctx)
|
|
fz_rethrow(ctx);
|
|
n++;
|
|
goto restart;
|
|
}
|
|
return n;
|
|
}
|
|
|
|
/* Links are not in MuPDF's annotation list. */
|
|
static int drop_hidden_links(fz_context *ctx, pdf_page *page)
|
|
{
|
|
pdf_obj *annots = pdf_dict_get(ctx, page->obj, PDF_NAME(Annots));
|
|
int i, n = 0;
|
|
for (i = pdf_array_len(ctx, annots) - 1; i >= 0; i--)
|
|
{
|
|
pdf_obj *annot = pdf_array_get(ctx, annots, i);
|
|
if (pdf_name_eq(ctx, pdf_dict_get(ctx, annot, PDF_NAME(Subtype)), PDF_NAME(Link)) && unseen(ctx, annot))
|
|
{
|
|
pdf_array_delete(ctx, annots, i);
|
|
n++;
|
|
}
|
|
}
|
|
return n;
|
|
}
|
|
|
|
static void sweep_page(fz_context *ctx, pdf_document *pdf, pdf_page *page, int mask, reach *r)
|
|
{
|
|
pdf_annot *annot;
|
|
if (mask & CLEAN_HIDDEN)
|
|
r->hidden_pages += drop_hidden(ctx, pdf, page) > 0;
|
|
restart:
|
|
for (annot = pdf_first_annot(ctx, page); annot; annot = pdf_next_annot(ctx, annot))
|
|
{
|
|
int file = pdf_annot_type(ctx, annot) == PDF_ANNOT_FILE_ATTACHMENT;
|
|
int hidden = (pdf_annot_flags(ctx, annot) & (PDF_ANNOT_IS_HIDDEN | PDF_ANNOT_IS_INVISIBLE)) != 0;
|
|
if ((file && (mask & CLEAN_FILES)) || (!file && (mask & CLEAN_ANNOTS)) || (hidden && (mask & CLEAN_HIDDEN)))
|
|
{
|
|
r->hidden_annots += hidden && !file;
|
|
r->annots += !file && !hidden;
|
|
pdf_delete_annot(ctx, page, annot);
|
|
goto restart;
|
|
}
|
|
}
|
|
if (mask & CLEAN_HIDDEN)
|
|
r->hidden_annots += drop_hidden_widgets(ctx, page) + drop_hidden_links(ctx, page);
|
|
}
|
|
|
|
/* Drops what `mask` names from `doc`, counting what was there. */
|
|
static reach sweep(fz_context *ctx, fz_document *doc, int mask)
|
|
{
|
|
pdf_document *pdf = ely_pdf_of(ctx, doc);
|
|
pdf_obj *trailer = pdf_trailer(ctx, pdf), *root = pdf_dict_get(ctx, trailer, PDF_NAME(Root));
|
|
pdf_obj *names = pdf_dict_get(ctx, root, PDF_NAME(Names)), *tree;
|
|
pdf_page *page = NULL;
|
|
reach r = { 0 };
|
|
int i;
|
|
fz_var(page);
|
|
if (mask & CLEAN_META)
|
|
{
|
|
r.meta = pdf_dict_len(ctx, pdf_dict_get(ctx, trailer, PDF_NAME(Info)));
|
|
pdf_dict_del(ctx, trailer, PDF_NAME(Info));
|
|
}
|
|
if ((mask & CLEAN_FILES) && names)
|
|
pdf_dict_del(ctx, names, PDF_NAME(EmbeddedFiles));
|
|
if (mask & CLEAN_HIDDEN)
|
|
{
|
|
tree = pdf_load_name_tree(ctx, pdf, PDF_NAME(JavaScript));
|
|
r.scripts = pdf_dict_len(ctx, tree);
|
|
pdf_drop_obj(ctx, tree);
|
|
if (names)
|
|
pdf_dict_del(ctx, names, PDF_NAME(JavaScript));
|
|
}
|
|
clean_all(ctx, pdf, mask, &r);
|
|
fz_try(ctx)
|
|
{
|
|
for (i = 0; i < pdf_count_pages(ctx, pdf); i++)
|
|
{
|
|
page = pdf_load_page(ctx, pdf, i);
|
|
sweep_page(ctx, pdf, page, mask, &r);
|
|
fz_drop_page(ctx, (fz_page *)page);
|
|
page = NULL;
|
|
}
|
|
}
|
|
fz_always(ctx)
|
|
fz_drop_page(ctx, (fz_page *)page);
|
|
fz_catch(ctx)
|
|
fz_rethrow(ctx);
|
|
return r;
|
|
}
|
|
|
|
static void write_reach(fz_context *ctx, fz_buffer *buf, reach r)
|
|
{
|
|
fz_append_printf(ctx, buf, "{\"meta\":%d,\"annots\":%d,\"files\":%d,\"hidden\":%d,\"scripts\":%d,\"hidden_annots\":%d}",
|
|
r.meta, r.annots, r.files, r.hidden_pages, r.scripts, r.hidden_annots);
|
|
}
|
|
|
|
/* Counts by sweeping a throwaway copy of `pdf`. */
|
|
static reach count_copy(fz_context *ctx, pdf_document *pdf, const char *raw, const char *password)
|
|
{
|
|
fz_document *copy = NULL;
|
|
reach r;
|
|
fz_var(copy);
|
|
fz_try(ctx)
|
|
{
|
|
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 copy refused its password");
|
|
r = sweep(ctx, copy, CLEAN_ALL);
|
|
}
|
|
fz_always(ctx)
|
|
fz_drop_document(ctx, copy);
|
|
fz_catch(ctx)
|
|
fz_rethrow(ctx);
|
|
return r;
|
|
}
|
|
|
|
/* What cleaning would reach, as JSON. */
|
|
int ely_clean_scan(fz_context *ctx, fz_document *doc, const char *raw, const char *password, unsigned char **data, size_t *len)
|
|
{
|
|
fz_buffer *buf = NULL;
|
|
fz_var(buf);
|
|
fz_try(ctx)
|
|
{
|
|
buf = fz_new_buffer(ctx, 128);
|
|
write_reach(ctx, buf, count_copy(ctx, ely_pdf_of(ctx, doc), raw, password));
|
|
*len = fz_buffer_extract(ctx, buf, data);
|
|
}
|
|
fz_always(ctx)
|
|
fz_drop_buffer(ctx, buf);
|
|
fz_catch(ctx)
|
|
return ely_fail(ctx);
|
|
return 0;
|
|
}
|
|
|
|
static void apply_clean(fz_context *ctx, fz_document *copy, void *arg)
|
|
{
|
|
sweep(ctx, copy, *(int *)arg);
|
|
}
|
|
|
|
/* Cleans `mask` into `out`, then counts what is left. */
|
|
int ely_clean(fz_context *ctx, fz_document *doc, int mask, const char *raw, const char *out, const char *password,
|
|
unsigned char **data, size_t *len)
|
|
{
|
|
fz_document *written = NULL;
|
|
fz_buffer *buf = NULL;
|
|
fz_var(written);
|
|
fz_var(buf);
|
|
fz_try(ctx)
|
|
{
|
|
reach left;
|
|
if (mask <= 0 || mask > CLEAN_ALL)
|
|
fz_throw(ctx, FZ_ERROR_ARGUMENT, "nothing chosen to clean");
|
|
ely_save_copy(ctx, ely_pdf_of(ctx, doc), raw, out, password, NULL, apply_clean, &mask);
|
|
written = fz_open_document(ctx, out);
|
|
if (fz_needs_password(ctx, written) && !fz_authenticate_password(ctx, written, password))
|
|
fz_throw(ctx, FZ_ERROR_ARGUMENT, "the written file refused its password");
|
|
left = count_copy(ctx, ely_pdf_of(ctx, written), raw, password);
|
|
buf = fz_new_buffer(ctx, 128);
|
|
write_reach(ctx, buf, left);
|
|
if (((mask & CLEAN_META) && left.meta) || ((mask & CLEAN_ANNOTS) && left.annots) || ((mask & CLEAN_FILES) && left.files) ||
|
|
((mask & CLEAN_HIDDEN) && (left.hidden_pages || left.scripts || left.hidden_annots)))
|
|
fz_throw(ctx, FZ_ERROR_FORMAT, "the written file still holds what was cleaned");
|
|
*len = fz_buffer_extract(ctx, buf, data);
|
|
}
|
|
fz_always(ctx)
|
|
{
|
|
fz_drop_buffer(ctx, buf);
|
|
fz_drop_document(ctx, written);
|
|
}
|
|
fz_catch(ctx)
|
|
return ely_fail(ctx);
|
|
return 0;
|
|
}
|