Files
ElyOffice/crates/ely-pdf-engine/shim/ely_annot.c
T

478 lines
15 KiB
C

/* Annotations: list, add, change, delete (PDF-05). */
#include <math.h>
#include <string.h>
#include "ely_shim.h"
static const char *stamps[] = { "Approved", "AsIs", "Confidential", "Departmental", "Draft", "Experimental",
"Expired", "Final", "ForComment", "ForPublicRelease", "NotApproved", "NotForPublicRelease", "Sold",
"TopSecret", NULL };
/* Valid UTF-8 always: a stray byte becomes U+FFFD. */
void ely_json_string(fz_context *ctx, fz_output *out, const char *s)
{
char utf8[FZ_UTFMAX];
int rune;
fz_write_byte(ctx, out, '"');
while (*s)
{
s += fz_chartorune(&rune, s);
/* A surrogate pair joins; a lone one is no character. */
if (rune >= 0xD800 && rune < 0xDC00)
{
int low;
int n = fz_chartorune(&low, s);
if (low >= 0xDC00 && low < 0xE000)
{
rune = 0x10000 + ((rune - 0xD800) << 10) + (low - 0xDC00);
s += n;
}
}
if ((rune >= 0xD800 && rune < 0xE000) || rune > 0x10FFFF)
rune = FZ_REPLACEMENT_CHARACTER;
if (rune == '"' || rune == '\\')
fz_write_printf(ctx, out, "\\%c", rune);
else if (rune < 32)
fz_write_printf(ctx, out, "\\u%04x", rune);
else
fz_write_data(ctx, out, utf8, fz_runetochar(utf8, rune));
}
fz_write_byte(ctx, out, '"');
}
fz_json *ely_member(fz_context *ctx, fz_json *obj, const char *key, int type)
{
fz_json *value = fz_json_object_get(ctx, obj, key);
if (value && value->type != type)
fz_throw(ctx, FZ_ERROR_ARGUMENT, "a bad %s", key);
return value;
}
float ely_number(fz_context *ctx, fz_json *array, int at)
{
fz_json *value = fz_json_array_get(ctx, array, at);
if (!value || value->type != FZ_JSON_NUMBER)
fz_throw(ctx, FZ_ERROR_ARGUMENT, "a bad number list");
return (float)value->u.number;
}
/* Four numbers, at least `least` wide and high. */
fz_rect ely_rect(fz_context *ctx, fz_json *array, float least)
{
fz_rect r;
if (!array || array->type != FZ_JSON_ARRAY || fz_json_array_length(ctx, array) != 4)
fz_throw(ctx, FZ_ERROR_ARGUMENT, "a bad rectangle");
r = fz_make_rect(ely_number(ctx, array, 0), ely_number(ctx, array, 1), ely_number(ctx, array, 2),
ely_number(ctx, array, 3));
if (r.x1 - r.x0 < least || r.y1 - r.y0 < least)
fz_throw(ctx, FZ_ERROR_ARGUMENT, "a rectangle too small");
return r;
}
static enum pdf_annot_type kind_of(fz_context *ctx, const char *kind)
{
if (!strcmp(kind, "highlight")) return PDF_ANNOT_HIGHLIGHT;
if (!strcmp(kind, "underline")) return PDF_ANNOT_UNDERLINE;
if (!strcmp(kind, "strikeout")) return PDF_ANNOT_STRIKE_OUT;
if (!strcmp(kind, "note")) return PDF_ANNOT_TEXT;
if (!strcmp(kind, "square")) return PDF_ANNOT_SQUARE;
if (!strcmp(kind, "circle")) return PDF_ANNOT_CIRCLE;
if (!strcmp(kind, "line")) return PDF_ANNOT_LINE;
if (!strcmp(kind, "ink")) return PDF_ANNOT_INK;
if (!strcmp(kind, "stamp")) return PDF_ANNOT_STAMP;
fz_throw(ctx, FZ_ERROR_ARGUMENT, "no annotation kind %s", kind);
}
static int rgb_of(fz_context *ctx, pdf_annot *annot)
{
float c[4], k;
int n;
pdf_annot_color(ctx, annot, &n, c);
if (n == 1)
c[1] = c[2] = c[0];
else if (n == 4)
{
k = 1 - c[3];
c[0] = (1 - c[0]) * k, c[1] = (1 - c[1]) * k, c[2] = (1 - c[2]) * k;
}
else if (n != 3)
return -1;
return ((int)(c[0] * 255 + 0.5f) << 16) | ((int)(c[1] * 255 + 0.5f) << 8) | (int)(c[2] * 255 + 0.5f);
}
static int color_of(fz_context *ctx, fz_json *spec)
{
fz_json *color = ely_member(ctx, spec, "color", FZ_JSON_NUMBER);
if (!color)
return -1;
if (color->u.number < 0 || color->u.number > 0xffffff)
fz_throw(ctx, FZ_ERROR_ARGUMENT, "a bad color");
return (int)color->u.number;
}
static const char *text_of(fz_context *ctx, fz_json *spec, const char *key)
{
fz_json *text = ely_member(ctx, spec, key, FZ_JSON_STRING);
return text ? text->u.string : NULL;
}
static int is_stamp(const char *name)
{
int i;
for (i = 0; name && stamps[i]; i++)
if (!strcmp(stamps[i], name))
return 1;
return 0;
}
/* Throws unless `spec` makes a whole annotation. */
static enum pdf_annot_type check(fz_context *ctx, fz_json *spec, int pages)
{
fz_json *page = ely_member(ctx, spec, "page", FZ_JSON_NUMBER);
fz_json *kind = ely_member(ctx, spec, "kind", FZ_JSON_STRING);
fz_json *list;
enum pdf_annot_type type;
int i, n;
if (!page || page->u.number < 0 || page->u.number >= pages || !kind)
fz_throw(ctx, FZ_ERROR_ARGUMENT, "an annotation with no page or kind");
type = kind_of(ctx, kind->u.string);
color_of(ctx, spec);
text_of(ctx, spec, "contents");
text_of(ctx, spec, "author");
switch (type)
{
case PDF_ANNOT_HIGHLIGHT:
case PDF_ANNOT_UNDERLINE:
case PDF_ANNOT_STRIKE_OUT:
list = ely_member(ctx, spec, "rects", FZ_JSON_ARRAY);
n = list ? fz_json_array_length(ctx, list) : 0;
if (n == 0)
fz_throw(ctx, FZ_ERROR_ARGUMENT, "a mark over nothing");
for (i = 0; i < n; i++)
ely_rect(ctx, fz_json_array_get(ctx, list, i), 0.1f);
break;
case PDF_ANNOT_TEXT:
list = ely_member(ctx, spec, "at", FZ_JSON_ARRAY);
if (!list || fz_json_array_length(ctx, list) != 2)
fz_throw(ctx, FZ_ERROR_ARGUMENT, "a note with no place");
ely_number(ctx, list, 0), ely_number(ctx, list, 1);
break;
case PDF_ANNOT_LINE:
list = ely_member(ctx, spec, "line", FZ_JSON_ARRAY);
if (!list || fz_json_array_length(ctx, list) != 4)
fz_throw(ctx, FZ_ERROR_ARGUMENT, "a line with no ends");
if (fabsf(ely_number(ctx, list, 2) - ely_number(ctx, list, 0)) +
fabsf(ely_number(ctx, list, 3) - ely_number(ctx, list, 1)) < 2)
fz_throw(ctx, FZ_ERROR_ARGUMENT, "a line too short");
break;
case PDF_ANNOT_INK:
list = ely_member(ctx, spec, "strokes", FZ_JSON_ARRAY);
n = list ? fz_json_array_length(ctx, list) : 0;
if (n == 0)
fz_throw(ctx, FZ_ERROR_ARGUMENT, "ink with no strokes");
for (i = 0; i < n; i++)
{
fz_json *stroke = fz_json_array_get(ctx, list, i);
int k, m = stroke->type == FZ_JSON_ARRAY ? fz_json_array_length(ctx, stroke) : 0;
if (m < 4 || m % 2)
fz_throw(ctx, FZ_ERROR_ARGUMENT, "a bad stroke");
for (k = 0; k < m; k++)
ely_number(ctx, stroke, k);
}
break;
case PDF_ANNOT_STAMP:
if (!is_stamp(text_of(ctx, spec, "icon")))
fz_throw(ctx, FZ_ERROR_ARGUMENT, "no such stamp");
ely_rect(ctx, ely_member(ctx, spec, "rect", FZ_JSON_ARRAY), 2);
break;
default:
ely_rect(ctx, ely_member(ctx, spec, "rect", FZ_JSON_ARRAY), 2);
}
return type;
}
static fz_point point_at(fz_context *ctx, fz_json *list, int at)
{
return fz_make_point(ely_number(ctx, list, at), ely_number(ctx, list, at + 1));
}
/* Makes a checked `spec` inside the open step. */
static void make(fz_context *ctx, pdf_document *pdf, fz_json *spec, enum pdf_annot_type type)
{
pdf_page *page = NULL;
pdf_annot *annot = NULL;
fz_point *points = NULL;
fz_var(page);
fz_var(annot);
fz_var(points);
fz_try(ctx)
{
fz_json *list;
int i, k, n, color = color_of(ctx, spec);
page = pdf_load_page(ctx, pdf, (int)ely_member(ctx, spec, "page", FZ_JSON_NUMBER)->u.number);
annot = pdf_create_annot(ctx, page, type);
switch (type)
{
case PDF_ANNOT_HIGHLIGHT:
case PDF_ANNOT_UNDERLINE:
case PDF_ANNOT_STRIKE_OUT:
list = ely_member(ctx, spec, "rects", FZ_JSON_ARRAY);
n = fz_json_array_length(ctx, list);
for (i = 0; i < n; i++)
pdf_add_annot_quad_point(ctx, annot, fz_quad_from_rect(ely_rect(ctx, fz_json_array_get(ctx, list, i), 0.1f)));
break;
case PDF_ANNOT_TEXT:
{
fz_point at = point_at(ctx, ely_member(ctx, spec, "at", FZ_JSON_ARRAY), 0);
pdf_set_annot_rect(ctx, annot, fz_make_rect(at.x, at.y, at.x + 16, at.y + 16));
pdf_set_annot_popup(ctx, annot, fz_make_rect(at.x + 20, at.y, at.x + 220, at.y + 100));
break;
}
case PDF_ANNOT_LINE:
list = ely_member(ctx, spec, "line", FZ_JSON_ARRAY);
pdf_set_annot_line(ctx, annot, point_at(ctx, list, 0), point_at(ctx, list, 2));
break;
case PDF_ANNOT_INK:
list = ely_member(ctx, spec, "strokes", FZ_JSON_ARRAY);
n = fz_json_array_length(ctx, list);
for (i = 0; i < n; i++)
{
fz_json *stroke = fz_json_array_get(ctx, list, i);
int m = fz_json_array_length(ctx, stroke) / 2;
fz_free(ctx, points);
points = NULL;
points = fz_malloc_array(ctx, m, fz_point);
for (k = 0; k < m; k++)
points[k] = point_at(ctx, stroke, k * 2);
pdf_add_annot_ink_list(ctx, annot, m, points);
}
break;
case PDF_ANNOT_STAMP:
pdf_set_annot_icon_name(ctx, annot, text_of(ctx, spec, "icon"));
pdf_set_annot_rect(ctx, annot, ely_rect(ctx, ely_member(ctx, spec, "rect", FZ_JSON_ARRAY), 2));
break;
default:
pdf_set_annot_rect(ctx, annot, ely_rect(ctx, ely_member(ctx, spec, "rect", FZ_JSON_ARRAY), 2));
}
if (color >= 0)
{
float rgb[3] = { (color >> 16) / 255.0f, ((color >> 8) & 255) / 255.0f, (color & 255) / 255.0f };
pdf_set_annot_color(ctx, annot, 3, rgb);
}
if (text_of(ctx, spec, "contents"))
pdf_set_annot_contents(ctx, annot, text_of(ctx, spec, "contents"));
if (text_of(ctx, spec, "author"))
pdf_set_annot_author(ctx, annot, text_of(ctx, spec, "author"));
pdf_update_annot(ctx, annot);
}
fz_always(ctx)
{
fz_free(ctx, points);
pdf_drop_annot(ctx, annot);
fz_drop_page(ctx, (fz_page *)page);
}
fz_catch(ctx)
fz_rethrow(ctx);
}
/* A JSON list of annotations, added in one step. */
int ely_annotate(fz_context *ctx, fz_document *doc, const char *json)
{
pdf_document *pdf = NULL;
fz_pool *pool = NULL;
enum pdf_annot_type *types = NULL;
int begun = 0;
fz_var(pdf);
fz_var(pool);
fz_var(types);
fz_var(begun);
fz_try(ctx)
{
fz_json *specs;
int i, n, pages;
pdf = ely_pdf_of(ctx, doc);
pages = pdf_count_pages(ctx, pdf);
pool = fz_new_pool(ctx);
specs = fz_parse_json(ctx, pool, json);
n = specs->type == FZ_JSON_ARRAY ? fz_json_array_length(ctx, specs) : 0;
if (n == 0)
fz_throw(ctx, FZ_ERROR_ARGUMENT, "no annotations to add");
types = fz_malloc_array(ctx, n, enum pdf_annot_type);
for (i = 0; i < n; i++)
types[i] = check(ctx, fz_json_array_get(ctx, specs, i), pages);
pdf_begin_operation(ctx, pdf, "Annotate");
begun = 1;
for (i = 0; i < n; i++)
make(ctx, pdf, fz_json_array_get(ctx, specs, i), types[i]);
pdf_end_operation(ctx, pdf);
}
fz_always(ctx)
{
fz_free(ctx, types);
fz_drop_pool(ctx, pool);
}
fz_catch(ctx)
return ely_edit_failed(ctx, pdf, begun);
return 0;
}
/* The annotation or widget object `id` on `page`. */
pdf_annot *ely_find_annot(fz_context *ctx, pdf_page *page, int id, int widget)
{
pdf_annot *annot = widget ? pdf_first_widget(ctx, page) : pdf_first_annot(ctx, page);
for (; annot; annot = widget ? pdf_next_widget(ctx, annot) : pdf_next_annot(ctx, annot))
if (pdf_to_num(ctx, pdf_annot_obj(ctx, annot)) == id)
return annot;
fz_throw(ctx, FZ_ERROR_ARGUMENT, "no %s %d on the page", widget ? "field" : "annotation", id);
}
/* Sets an annotation's colour or contents, in one step. */
int ely_change_annot(fz_context *ctx, fz_document *doc, int number, int id, const char *json)
{
pdf_document *pdf = NULL;
pdf_page *page = NULL;
fz_pool *pool = NULL;
int begun = 0;
fz_var(pdf);
fz_var(page);
fz_var(pool);
fz_var(begun);
fz_try(ctx)
{
pdf_annot *annot;
fz_json *spec;
const char *contents;
int color;
pdf = ely_pdf_of(ctx, doc);
page = pdf_load_page(ctx, pdf, number);
annot = ely_find_annot(ctx, page, id, 0);
pool = fz_new_pool(ctx);
spec = fz_parse_json(ctx, pool, json);
color = color_of(ctx, spec);
contents = text_of(ctx, spec, "contents");
if (color < 0 && !contents)
fz_throw(ctx, FZ_ERROR_ARGUMENT, "no change");
pdf_begin_operation(ctx, pdf, "Change an annotation");
begun = 1;
if (color >= 0)
{
float rgb[3] = { (color >> 16) / 255.0f, ((color >> 8) & 255) / 255.0f, (color & 255) / 255.0f };
pdf_set_annot_color(ctx, annot, 3, rgb);
}
if (contents)
pdf_set_annot_contents(ctx, annot, contents);
pdf_update_annot(ctx, annot);
pdf_end_operation(ctx, pdf);
}
fz_always(ctx)
{
fz_drop_pool(ctx, pool);
fz_drop_page(ctx, (fz_page *)page);
}
fz_catch(ctx)
return ely_edit_failed(ctx, pdf, begun);
return 0;
}
/* Unlinks `field`; an emptied parent goes too. */
void ely_unlink_field(fz_context *ctx, pdf_document *pdf, pdf_obj *field)
{
pdf_obj *parent = pdf_dict_get(ctx, field, PDF_NAME(Parent));
pdf_obj *kids = parent ? pdf_dict_get(ctx, parent, PDF_NAME(Kids)) : pdf_dict_getp(ctx, pdf_trailer(ctx, pdf), "Root/AcroForm/Fields");
int at = pdf_array_find(ctx, kids, field);
if (at >= 0)
pdf_array_delete(ctx, kids, at);
if (parent && pdf_array_len(ctx, kids) == 0)
ely_unlink_field(ctx, pdf, parent);
}
/* Deletes annotation or field `id`, in one step. */
int ely_delete_annot(fz_context *ctx, fz_document *doc, int number, int id, int widget)
{
pdf_document *pdf = NULL;
pdf_page *page = NULL;
pdf_obj *field = NULL;
int begun = 0;
fz_var(pdf);
fz_var(page);
fz_var(field);
fz_var(begun);
fz_try(ctx)
{
pdf_annot *annot;
pdf = ely_pdf_of(ctx, doc);
page = pdf_load_page(ctx, pdf, number);
annot = ely_find_annot(ctx, page, id, widget);
field = pdf_keep_obj(ctx, pdf_annot_obj(ctx, annot));
pdf_begin_operation(ctx, pdf, widget ? "Delete a field" : "Delete an annotation");
begun = 1;
pdf_delete_annot(ctx, page, annot);
if (widget)
ely_unlink_field(ctx, pdf, field);
pdf_end_operation(ctx, pdf);
}
fz_always(ctx)
{
pdf_drop_obj(ctx, field);
fz_drop_page(ctx, (fz_page *)page);
}
fz_catch(ctx)
return ely_edit_failed(ctx, pdf, begun);
return 0;
}
static void write_annot(fz_context *ctx, fz_output *out, pdf_annot *annot)
{
fz_rect r = pdf_bound_annot(ctx, annot);
fz_write_printf(ctx, out, "{\"id\":%d,\"kind\":", pdf_to_num(ctx, pdf_annot_obj(ctx, annot)));
ely_json_string(ctx, out, pdf_string_from_annot_type(ctx, pdf_annot_type(ctx, annot)));
fz_write_printf(ctx, out, ",\"rect\":[%g,%g,%g,%g],\"color\":%d,\"contents\":", r.x0, r.y0, r.x1, r.y1,
rgb_of(ctx, annot));
ely_json_string(ctx, out, pdf_annot_contents(ctx, annot));
fz_write_string(ctx, out, ",\"author\":");
ely_json_string(ctx, out, pdf_annot_has_author(ctx, annot) ? pdf_annot_author(ctx, annot) : "");
fz_write_string(ctx, out, ",\"icon\":");
ely_json_string(ctx, out, pdf_annot_has_icon_name(ctx, annot) ? pdf_annot_icon_name(ctx, annot) : "");
fz_write_byte(ctx, out, '}');
}
/* JSON of a page's annotations and form fields. */
int ely_annots(fz_context *ctx, fz_document *doc, int number, unsigned char **data, size_t *len)
{
pdf_page *page = NULL;
fz_buffer *buf = NULL;
fz_output *out = NULL;
fz_var(page);
fz_var(buf);
fz_var(out);
fz_try(ctx)
{
pdf_annot *annot;
page = pdf_load_page(ctx, ely_pdf_of(ctx, doc), number);
buf = fz_new_buffer(ctx, 1024);
out = fz_new_output_with_buffer(ctx, buf);
fz_write_string(ctx, out, "{\"annots\":[");
for (annot = pdf_first_annot(ctx, page); annot; annot = pdf_next_annot(ctx, annot))
{
if (annot != pdf_first_annot(ctx, page))
fz_write_byte(ctx, out, ',');
write_annot(ctx, out, annot);
}
fz_write_string(ctx, out, "],\"fields\":[");
ely_write_fields(ctx, out, page);
fz_write_string(ctx, out, "]}");
fz_close_output(ctx, out);
*len = fz_buffer_extract(ctx, buf, data);
}
fz_always(ctx)
{
fz_drop_output(ctx, out);
fz_drop_buffer(ctx, buf);
fz_drop_page(ctx, (fz_page *)page);
}
fz_catch(ctx)
return ely_fail(ctx);
return 0;
}