ElyDoc, ElyTable, ElyShow (LibreOfficeKit), ElyPDF (MuPDF), ElyTree (Git), ElyMark (Milkdown and CodeMirror in CEF). ElyMail and ElyAgent follow.
462 lines
14 KiB
C
462 lines
14 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 };
|
|
|
|
void ely_json_string(fz_context *ctx, fz_output *out, const char *s)
|
|
{
|
|
fz_write_byte(ctx, out, '"');
|
|
for (; *s; s++)
|
|
{
|
|
unsigned char c = (unsigned char)*s;
|
|
if (c == '"' || c == '\\')
|
|
fz_write_printf(ctx, out, "\\%c", c);
|
|
else if (c < 32)
|
|
fz_write_printf(ctx, out, "\\u%04x", c);
|
|
else
|
|
fz_write_byte(ctx, out, c);
|
|
}
|
|
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;
|
|
}
|