Files
ElyOffice/crates/ely-pdf-engine/shim/ely_redact.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

424 lines
12 KiB
C

/* True removal into a new file, then checked (PDF-09, QA-14). */
#include <math.h>
#include <string.h>
#include "ely_shim.h"
/* Regions by page, page space. */
typedef struct
{
int page;
fz_rect rect;
} region;
static int touches(fz_rect a, fz_rect b)
{
return !fz_is_empty_rect(fz_intersect_rect(a, b));
}
/* Throws unless every region is whole and on a page. */
static region *regions_of(fz_context *ctx, fz_json *list, int pages, int *n)
{
region *out;
int i;
*n = list && list->type == FZ_JSON_ARRAY ? fz_json_array_length(ctx, list) : 0;
if (*n == 0)
fz_throw(ctx, FZ_ERROR_ARGUMENT, "nothing marked to remove");
out = fz_malloc_array(ctx, *n, region);
fz_try(ctx)
{
for (i = 0; i < *n; i++)
{
fz_json *spec = fz_json_array_get(ctx, list, i);
fz_json *page = ely_member(ctx, spec, "page", FZ_JSON_NUMBER);
if (!page || page->u.number < 0 || page->u.number >= pages)
fz_throw(ctx, FZ_ERROR_ARGUMENT, "a region off the pages");
out[i].page = (int)page->u.number;
out[i].rect = ely_rect(ctx, ely_member(ctx, spec, "rect", FZ_JSON_ARRAY), 1);
}
}
fz_catch(ctx)
{
fz_free(ctx, out);
fz_rethrow(ctx);
}
return out;
}
/* Drops annotations and fields that touch `area`; they carry text. */
static void drop_touching(fz_context *ctx, pdf_page *page, fz_rect area)
{
pdf_annot *annot;
int widgets;
for (widgets = 0; widgets < 2; widgets++)
{
restart:
for (annot = widgets ? pdf_first_widget(ctx, page) : pdf_first_annot(ctx, page); annot;
annot = widgets ? pdf_next_widget(ctx, annot) : pdf_next_annot(ctx, annot))
if (touches(pdf_bound_annot(ctx, annot), area))
{
pdf_obj *field = pdf_keep_obj(ctx, pdf_annot_obj(ctx, annot));
fz_try(ctx)
{
pdf_delete_annot(ctx, page, annot);
/* A field left with no widget still holds its value. */
if (widgets)
ely_unlink_field(ctx, page->doc, field);
}
fz_always(ctx)
pdf_drop_obj(ctx, field);
fz_catch(ctx)
fz_rethrow(ctx);
goto restart;
}
}
}
/* Applies `regions` to the copy, page by page. */
static void redact(fz_context *ctx, pdf_document *pdf, region *regions, int n)
{
pdf_redact_options opts = { 1, PDF_REDACT_IMAGE_PIXELS, PDF_REDACT_LINE_ART_REMOVE_IF_TOUCHED, PDF_REDACT_TEXT_REMOVE };
pdf_page *page = NULL;
int i, k;
fz_var(page);
fz_try(ctx)
{
for (i = 0; i < pdf_count_pages(ctx, pdf); i++)
{
int any = 0;
for (k = 0; k < n; k++)
any |= regions[k].page == i;
if (!any)
continue;
page = pdf_load_page(ctx, pdf, i);
for (k = 0; k < n; k++)
if (regions[k].page == i)
drop_touching(ctx, page, regions[k].rect);
/* Marks after drops, or a drop takes a mark. */
for (k = 0; k < n; k++)
if (regions[k].page == i)
{
pdf_annot *mark = pdf_create_annot(ctx, page, PDF_ANNOT_REDACT);
pdf_set_annot_rect(ctx, mark, regions[k].rect);
pdf_drop_annot(ctx, mark);
}
pdf_redact_page(ctx, pdf, page, &opts);
ely_drop_alt(ctx, pdf, page);
pdf_dict_del(ctx, page->obj, PDF_NAME(Thumb));
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);
}
/* A glyph box shrunk a tenth, as redaction judges. */
static fz_rect shrunk(fz_rect r)
{
float w = (r.x1 - r.x0) / 10, h = (r.y1 - r.y0) / 10;
return fz_make_rect(r.x0 + w, r.y0 + h, r.x1 - w, r.y1 - h);
}
/* Characters on `page` inside `area`, hidden ones too. */
static int chars_in(fz_context *ctx, fz_document *doc, int number, fz_rect area)
{
fz_stext_page *text = NULL;
int n = 0;
fz_var(text);
fz_try(ctx)
{
fz_stext_block *block;
fz_stext_line *line;
fz_stext_char *ch;
text = fz_new_stext_page_from_page_number(ctx, doc, number, NULL);
for (block = text->first_block; block; block = block->next)
if (block->type == FZ_STEXT_BLOCK_TEXT)
for (line = block->u.t.first_line; line; line = line->next)
for (ch = line->first_char; ch; ch = ch->next)
n += touches(shrunk(fz_rect_from_quad(ch->quad)), area);
}
fz_always(ctx)
fz_drop_stext_page(ctx, text);
fz_catch(ctx)
fz_rethrow(ctx);
return n;
}
/* Pixels in `area` that are not black once drawn. */
static int light_in(fz_context *ctx, fz_document *doc, int number, fz_rect area)
{
fz_pixmap *pix = NULL;
int n = 0;
fz_var(pix);
fz_try(ctx)
{
fz_page *page = fz_load_page(ctx, doc, number);
fz_irect box = fz_round_rect(fz_intersect_rect(area, fz_bound_page(ctx, page)));
unsigned char *s;
int x, y;
fz_drop_page(ctx, page);
/* One pixel in from each edge: boxes antialias. */
box.x0++, box.y0++, box.x1--, box.y1--;
if (box.x1 <= box.x0 || box.y1 <= box.y0)
break;
pix = fz_new_pixmap_with_bbox(ctx, fz_device_rgb(ctx), box, NULL, 0);
fz_clear_pixmap_with_value(ctx, pix, 255);
page = fz_load_page(ctx, doc, number);
fz_try(ctx)
{
fz_device *dev = fz_new_draw_device(ctx, fz_identity, pix);
fz_try(ctx)
{
fz_run_page(ctx, page, dev, fz_identity, NULL);
fz_close_device(ctx, dev);
}
fz_always(ctx)
fz_drop_device(ctx, dev);
fz_catch(ctx)
fz_rethrow(ctx);
}
fz_always(ctx)
fz_drop_page(ctx, page);
fz_catch(ctx)
fz_rethrow(ctx);
for (y = 0, s = pix->samples; y < pix->h; y++, s += pix->stride - pix->w * 3)
for (x = 0; x < pix->w; x++, s += 3)
n += s[0] > 40 || s[1] > 40 || s[2] > 40;
}
fz_always(ctx)
fz_drop_pixmap(ctx, pix);
fz_catch(ctx)
fz_rethrow(ctx);
return n;
}
/* Counts drawings in the regions, our boxes aside. */
typedef struct
{
fz_device super;
region *regions;
int n, page, leaks;
} leak_dev;
static int in_regions(leak_dev *d, fz_rect r)
{
int k;
for (k = 0; k < d->n; k++)
if (d->regions[k].page == d->page && touches(r, d->regions[k].rect))
return 1;
return 0;
}
static int black(fz_context *ctx, fz_colorspace *cs, const float *color)
{
float rgb[3];
if (!cs)
return 0;
fz_convert_color(ctx, cs, color, fz_device_rgb(ctx), rgb, NULL, fz_default_color_params);
return rgb[0] <= 0.02f && rgb[1] <= 0.02f && rgb[2] <= 0.02f;
}
/* Whether `r`, black, is one of the region boxes. */
static int is_box(fz_context *ctx, leak_dev *d, fz_rect r, fz_colorspace *cs, const float *color)
{
int k;
if (!black(ctx, cs, color))
return 0;
for (k = 0; k < d->n; k++)
{
fz_rect b = d->regions[k].rect;
if (d->regions[k].page == d->page && fabsf(r.x0 - b.x0) < 1 && fabsf(r.y0 - b.y0) < 1 &&
fabsf(r.x1 - b.x1) < 1 && fabsf(r.y1 - b.y1) < 1)
return 1;
}
return 0;
}
static void leak_fill_path(fz_context *ctx, fz_device *dev, const fz_path *path, int even_odd, fz_matrix ctm,
fz_colorspace *cs, const float *color, float alpha, fz_color_params cp)
{
leak_dev *d = (leak_dev *)dev;
fz_rect r = fz_bound_path(ctx, path, NULL, ctm);
(void)even_odd, (void)alpha, (void)cp;
if (in_regions(d, r) && !is_box(ctx, d, r, cs, color))
d->leaks++;
}
static void leak_stroke_path(fz_context *ctx, fz_device *dev, const fz_path *path, const fz_stroke_state *stroke,
fz_matrix ctm, fz_colorspace *cs, const float *color, float alpha, fz_color_params cp)
{
leak_dev *d = (leak_dev *)dev;
(void)cs, (void)color, (void)alpha, (void)cp;
d->leaks += in_regions(d, fz_bound_path(ctx, path, stroke, ctm));
}
static void leak_fill_shade(fz_context *ctx, fz_device *dev, fz_shade *shade, fz_matrix ctm, float alpha, fz_color_params cp)
{
leak_dev *d = (leak_dev *)dev;
(void)alpha, (void)cp;
d->leaks += in_regions(d, fz_bound_shade(ctx, shade, ctm));
}
static void leak_fill_image_mask(fz_context *ctx, fz_device *dev, fz_image *image, fz_matrix ctm, fz_colorspace *cs,
const float *color, float alpha, fz_color_params cp)
{
leak_dev *d = (leak_dev *)dev;
(void)image, (void)alpha, (void)cp;
d->leaks += in_regions(d, fz_transform_rect(fz_unit_rect, ctm)) && !black(ctx, cs, color);
}
/* Image pixels in `area` neither white nor black. */
static int image_light(fz_context *ctx, fz_image *image, fz_matrix ctm, fz_rect area)
{
fz_irect box = fz_round_rect(fz_intersect_rect(area, fz_transform_rect(fz_unit_rect, ctm)));
fz_pixmap *pix = NULL;
fz_device *draw = NULL;
int n = 0, x, y;
box.x0++, box.y0++, box.x1--, box.y1--;
if (box.x1 <= box.x0 || box.y1 <= box.y0)
return 0;
fz_var(pix);
fz_var(draw);
fz_try(ctx)
{
unsigned char *s;
pix = fz_new_pixmap_with_bbox(ctx, fz_device_rgb(ctx), box, NULL, 1);
fz_clear_pixmap(ctx, pix);
draw = fz_new_draw_device(ctx, fz_identity, pix);
fz_fill_image(ctx, draw, image, ctm, 1, fz_default_color_params);
fz_close_device(ctx, draw);
for (y = 0, s = pix->samples; y < pix->h; y++, s += pix->stride - pix->w * 4)
for (x = 0; x < pix->w; x++, s += 4)
{
int high = s[0] > 215 && s[1] > 215 && s[2] > 215, low = s[0] < 40 && s[1] < 40 && s[2] < 40;
n += s[3] > 200 && !high && !low;
}
}
fz_always(ctx)
{
fz_drop_device(ctx, draw);
fz_drop_pixmap(ctx, pix);
}
fz_catch(ctx)
fz_rethrow(ctx);
return n;
}
static void leak_fill_image(fz_context *ctx, fz_device *dev, fz_image *image, fz_matrix ctm, float alpha, fz_color_params cp)
{
leak_dev *d = (leak_dev *)dev;
int k;
(void)alpha, (void)cp;
for (k = 0; k < d->n; k++)
if (d->regions[k].page == d->page)
d->leaks += image_light(ctx, image, ctm, d->regions[k].rect);
}
/* Drawings left in the regions of page `number`. */
static int leaks_on(fz_context *ctx, fz_document *doc, int number, region *regions, int n)
{
leak_dev *d = fz_new_derived_device(ctx, leak_dev);
fz_page *page = NULL;
int leaks = 0;
fz_var(page);
d->super.fill_path = leak_fill_path;
d->super.stroke_path = leak_stroke_path;
d->super.fill_shade = leak_fill_shade;
d->super.fill_image = leak_fill_image;
d->super.fill_image_mask = leak_fill_image_mask;
d->regions = regions, d->n = n, d->page = number;
fz_try(ctx)
{
page = fz_load_page(ctx, doc, number);
fz_run_page(ctx, page, &d->super, fz_identity, NULL);
fz_close_device(ctx, &d->super);
leaks = d->leaks;
}
fz_always(ctx)
{
fz_drop_page(ctx, page);
fz_drop_device(ctx, &d->super);
}
fz_catch(ctx)
fz_rethrow(ctx);
return leaks;
}
typedef struct
{
region *regions;
int n;
} marked;
static void apply_marks(fz_context *ctx, fz_document *copy, void *arg)
{
marked *m = arg;
redact(ctx, ely_pdf_of(ctx, copy), m->regions, m->n);
}
/* Checks `out`: no text, no light in any region. */
static void check_redacted(fz_context *ctx, const char *out, const char *password, region *regions, int n, fz_buffer *report)
{
fz_document *doc = fz_open_document(ctx, out);
fz_try(ctx)
{
int i, text = 0, light = 0, objects = 0, versions;
if (fz_needs_password(ctx, doc) && !fz_authenticate_password(ctx, doc, password))
fz_throw(ctx, FZ_ERROR_ARGUMENT, "the written file refused its password");
for (i = 0; i < n; i++)
{
text += chars_in(ctx, doc, regions[i].page, regions[i].rect);
light += light_in(ctx, doc, regions[i].page, regions[i].rect);
objects += pdf_dict_get(ctx, pdf_lookup_page_obj(ctx, ely_pdf_of(ctx, doc), regions[i].page), PDF_NAME(Thumb)) != NULL;
}
for (i = 0; i < fz_count_pages(ctx, doc); i++)
objects += leaks_on(ctx, doc, i, regions, n);
versions = pdf_count_versions(ctx, ely_pdf_of(ctx, doc));
fz_append_printf(ctx, report, "{\"regions\":%d,\"text\":%d,\"light\":%d,\"objects\":%d,\"versions\":%d}", n, text,
light, objects, versions);
if (text || light || objects || versions != 1)
fz_throw(ctx, FZ_ERROR_FORMAT, "the written file still holds what was removed: %d characters, %d light pixels, %d drawings, %d versions",
text, light, objects, versions);
}
fz_always(ctx)
fz_drop_document(ctx, doc);
fz_catch(ctx)
fz_rethrow(ctx);
}
/* Removes the marked regions into `out`, then checks it. */
int ely_redact(fz_context *ctx, fz_document *doc, const char *json, const char *raw, const char *out,
const char *password, unsigned char **data, size_t *len)
{
fz_pool *pool = NULL;
marked m = { NULL, 0 };
fz_buffer *report = NULL;
fz_var(pool);
fz_var(m);
fz_var(report);
fz_try(ctx)
{
pdf_document *pdf = ely_pdf_of(ctx, doc);
pool = fz_new_pool(ctx);
m.regions = regions_of(ctx, fz_parse_json(ctx, pool, json), pdf_count_pages(ctx, pdf), &m.n);
ely_save_copy(ctx, pdf, raw, out, password, NULL, apply_marks, &m);
report = fz_new_buffer(ctx, 128);
check_redacted(ctx, out, password, m.regions, m.n, report);
*len = fz_buffer_extract(ctx, report, data);
}
fz_always(ctx)
{
fz_drop_buffer(ctx, report);
fz_free(ctx, m.regions);
fz_drop_pool(ctx, pool);
}
fz_catch(ctx)
return ely_fail(ctx);
return 0;
}