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

490 lines
16 KiB
C

/* Form fields: list, add, fill, change (PDF-06). */
#include <stdio.h>
#include <string.h>
#include "ely_shim.h"
static const char *kind_name(enum pdf_widget_type type)
{
switch (type)
{
case PDF_WIDGET_TYPE_TEXT: return "text";
case PDF_WIDGET_TYPE_CHECKBOX: return "checkbox";
case PDF_WIDGET_TYPE_RADIOBUTTON: return "radio";
case PDF_WIDGET_TYPE_COMBOBOX: return "choice";
case PDF_WIDGET_TYPE_LISTBOX: return "list";
case PDF_WIDGET_TYPE_SIGNATURE: return "signature";
default: return "button";
}
}
/* The field that names `obj`. */
static pdf_obj *head_of(fz_context *ctx, pdf_obj *obj)
{
while (!pdf_dict_get(ctx, obj, PDF_NAME(T)) && pdf_dict_get(ctx, obj, PDF_NAME(Parent)))
obj = pdf_dict_get(ctx, obj, PDF_NAME(Parent));
return obj;
}
static pdf_obj *fields_of(fz_context *ctx, pdf_document *pdf)
{
pdf_obj *root = pdf_dict_get(ctx, pdf_trailer(ctx, pdf), PDF_NAME(Root));
pdf_obj *form = pdf_dict_get(ctx, root, PDF_NAME(AcroForm));
if (!form)
form = pdf_dict_put_dict(ctx, root, PDF_NAME(AcroForm), 2);
if (!pdf_is_array(ctx, pdf_dict_get(ctx, form, PDF_NAME(Fields))))
pdf_dict_put_array(ctx, form, PDF_NAME(Fields), 4);
return pdf_dict_get(ctx, form, PDF_NAME(Fields));
}
/* A value as text; names and streams too. */
static void write_value(fz_context *ctx, fz_output *out, pdf_obj *v)
{
char *text = NULL;
fz_var(text);
fz_try(ctx)
{
if (pdf_is_name(ctx, v))
ely_json_string(ctx, out, pdf_to_name(ctx, v));
else if (pdf_is_stream(ctx, v))
{
text = pdf_new_utf8_from_pdf_stream_obj(ctx, v);
ely_json_string(ctx, out, text);
}
else
ely_json_string(ctx, out, pdf_to_text_string(ctx, v));
}
fz_always(ctx)
fz_free(ctx, text);
fz_catch(ctx)
fz_rethrow(ctx);
}
static void write_options(fz_context *ctx, fz_output *out, pdf_obj *obj, int export)
{
int i, n = pdf_choice_field_option_count(ctx, obj);
fz_write_byte(ctx, out, '[');
for (i = 0; i < n; i++)
{
if (i)
fz_write_byte(ctx, out, ',');
ely_json_string(ctx, out, pdf_choice_field_option(ctx, obj, export, i));
}
fz_write_byte(ctx, out, ']');
}
void ely_write_fields(fz_context *ctx, fz_output *out, pdf_page *page)
{
pdf_annot *widget;
char *name = NULL;
fz_var(name);
fz_try(ctx)
{
for (widget = pdf_first_widget(ctx, page); widget; widget = pdf_next_widget(ctx, widget))
{
pdf_obj *obj = pdf_annot_obj(ctx, widget);
enum pdf_widget_type type = pdf_widget_type(ctx, widget);
fz_rect r = pdf_bound_widget(ctx, widget);
int button = type == PDF_WIDGET_TYPE_CHECKBOX || type == PDF_WIDGET_TYPE_RADIOBUTTON;
pdf_obj *as = pdf_dict_get(ctx, obj, PDF_NAME(AS));
fz_free(ctx, name);
name = NULL;
name = pdf_load_field_name(ctx, obj);
fz_write_printf(ctx, out, "%s{\"id\":%d,\"kind\":\"%s\",\"name\":", widget == pdf_first_widget(ctx, page) ? "" : ",",
pdf_to_num(ctx, obj), kind_name(type));
ely_json_string(ctx, out, name);
fz_write_printf(ctx, out, ",\"rect\":[%g,%g,%g,%g],\"value\":", r.x0, r.y0, r.x1, r.y1);
write_value(ctx, out, pdf_dict_get_inheritable(ctx, obj, PDF_NAME(V)));
fz_write_string(ctx, out, ",\"on\":");
ely_json_string(ctx, out, button ? pdf_to_name(ctx, pdf_button_field_on_state(ctx, obj)) : "");
fz_write_printf(ctx, out, ",\"checked\":%s,\"readonly\":%s,\"options\":",
button && pdf_is_name(ctx, as) && !pdf_name_eq(ctx, as, PDF_NAME(Off)) ? "true" : "false",
pdf_widget_is_readonly(ctx, widget) ? "true" : "false");
write_options(ctx, out, obj, 1);
fz_write_string(ctx, out, ",\"labels\":");
write_options(ctx, out, obj, 0);
fz_write_byte(ctx, out, '}');
}
}
fz_always(ctx)
fz_free(ctx, name);
fz_catch(ctx)
fz_rethrow(ctx);
}
/* Shows `state` on each button under `node`, else Off. */
static void show_state(fz_context *ctx, pdf_obj *node, pdf_obj *state)
{
pdf_obj *kids = pdf_dict_get(ctx, node, PDF_NAME(Kids));
int i;
if (kids)
{
for (i = 0; i < pdf_array_len(ctx, kids); i++)
show_state(ctx, pdf_array_get(ctx, kids, i), state);
return;
}
pdf_dict_put(ctx, node, PDF_NAME(AS), pdf_dict_get(ctx, pdf_dict_getp(ctx, node, "AP/N"), state) ? state : PDF_NAME(Off));
}
/* Fills field widget `id` with `value`, in one step. */
int ely_fill(fz_context *ctx, fz_document *doc, int number, int id, const char *value)
{
pdf_document *pdf = NULL;
pdf_page *page = NULL;
pdf_obj *state = NULL;
int begun = 0;
fz_var(pdf);
fz_var(page);
fz_var(state);
fz_var(begun);
fz_try(ctx)
{
pdf_annot *widget;
pdf_obj *obj;
enum pdf_widget_type type;
pdf = ely_pdf_of(ctx, doc);
page = pdf_load_page(ctx, pdf, number);
widget = ely_find_annot(ctx, page, id, 1);
obj = pdf_annot_obj(ctx, widget);
type = pdf_widget_type(ctx, widget);
if (pdf_widget_is_readonly(ctx, widget))
fz_throw(ctx, FZ_ERROR_ARGUMENT, "the field is read only");
if (type == PDF_WIDGET_TYPE_CHECKBOX || type == PDF_WIDGET_TYPE_RADIOBUTTON)
{
state = pdf_new_name(ctx, value);
if (!pdf_name_eq(ctx, state, PDF_NAME(Off)) && !pdf_name_eq(ctx, state, pdf_button_field_on_state(ctx, obj)))
fz_throw(ctx, FZ_ERROR_ARGUMENT, "the button has no state %s", value);
}
else if (type != PDF_WIDGET_TYPE_TEXT && type != PDF_WIDGET_TYPE_COMBOBOX && type != PDF_WIDGET_TYPE_LISTBOX)
fz_throw(ctx, FZ_ERROR_ARGUMENT, "a %s field cannot be filled", kind_name(type));
pdf_begin_operation(ctx, pdf, "Fill a field");
begun = 1;
if (state)
{
pdf_dict_put(ctx, head_of(ctx, obj), PDF_NAME(V), state);
show_state(ctx, head_of(ctx, obj), state);
}
else if (!pdf_set_field_value(ctx, pdf, obj, value, 1))
fz_throw(ctx, FZ_ERROR_ARGUMENT, "the field refused the value");
pdf_update_annot(ctx, widget);
pdf_end_operation(ctx, pdf);
}
fz_always(ctx)
{
pdf_drop_obj(ctx, state);
fz_drop_page(ctx, (fz_page *)page);
}
fz_catch(ctx)
return ely_edit_failed(ctx, pdf, begun);
return 0;
}
/* Throws unless `prefix` plus `name` is free for `kind`. */
static pdf_obj *check_name(fz_context *ctx, pdf_obj *fields, const char *prefix, const char *name, const char *kind,
pdf_obj *self)
{
pdf_obj *found = NULL;
char *full = NULL;
if (!name[0] || strchr(name, '.'))
fz_throw(ctx, FZ_ERROR_ARGUMENT, "a bad field name");
fz_var(full);
fz_try(ctx)
{
full = fz_asprintf(ctx, "%s%s", prefix, name);
found = pdf_lookup_field(ctx, fields, full);
}
fz_always(ctx)
fz_free(ctx, full);
fz_catch(ctx)
fz_rethrow(ctx);
if (!found || (self && !pdf_objcmp(ctx, found, self)))
return NULL;
if (!strcmp(kind, "radio") && pdf_field_type(ctx, found) == PDF_WIDGET_TYPE_RADIOBUTTON &&
pdf_dict_get(ctx, found, PDF_NAME(Kids)))
return found;
fz_throw(ctx, FZ_ERROR_ARGUMENT, "a field named %s%s exists", prefix, name);
}
/* The full name of `field`'s parent, with a dot. */
static char *parent_path(fz_context *ctx, pdf_obj *field)
{
pdf_obj *parent = pdf_dict_get(ctx, field, PDF_NAME(Parent));
char *name = parent ? pdf_load_field_name(ctx, parent) : NULL, *path = NULL;
fz_try(ctx)
path = fz_asprintf(ctx, "%s%s", name ? name : "", name && name[0] ? "." : "");
fz_always(ctx)
fz_free(ctx, name);
fz_catch(ctx)
fz_rethrow(ctx);
return path;
}
static void put_options(fz_context *ctx, pdf_obj *field, fz_json *options)
{
pdf_obj *opt = pdf_dict_put_array(ctx, field, PDF_NAME(Opt), 4);
int i, n = fz_json_array_length(ctx, options);
for (i = 0; i < n; i++)
pdf_array_push_text_string(ctx, opt, fz_json_array_get(ctx, options, i)->u.string);
}
static void check_options(fz_context *ctx, fz_json *options)
{
int i, n = options ? fz_json_array_length(ctx, options) : 0;
if (n == 0)
fz_throw(ctx, FZ_ERROR_ARGUMENT, "a choice with no options");
for (i = 0; i < n; i++)
if (fz_json_array_get(ctx, options, i)->type != FZ_JSON_STRING || !fz_json_array_get(ctx, options, i)->u.string[0])
fz_throw(ctx, FZ_ERROR_ARGUMENT, "a bad option");
}
/* An on state no button in `group` has. */
static pdf_obj *new_state(fz_context *ctx, pdf_obj *group)
{
pdf_obj *kids = pdf_dict_get(ctx, group, PDF_NAME(Kids));
char name[32];
int n, i, taken;
for (n = pdf_array_len(ctx, kids) + 1;; n++)
{
snprintf(name, sizeof name, "Choice%d", n);
for (i = 0, taken = 0; i < pdf_array_len(ctx, kids); i++)
taken |= !strcmp(pdf_to_name(ctx, pdf_button_field_on_state(ctx, pdf_array_get(ctx, kids, i))), name);
if (!taken)
return pdf_new_name(ctx, name);
}
}
/* Adds a field of four kinds, in one step. */
int ely_add_field(fz_context *ctx, fz_document *doc, int number, const char *json)
{
pdf_document *pdf = NULL;
pdf_page *page = NULL;
pdf_annot *widget = NULL;
pdf_obj *state = NULL;
fz_pool *pool = NULL;
int begun = 0;
fz_var(pdf);
fz_var(page);
fz_var(widget);
fz_var(state);
fz_var(pool);
fz_var(begun);
fz_try(ctx)
{
fz_json *spec, *options, *kindj, *namej;
const char *kind, *name;
pdf_obj *fields, *group, *obj, *mk;
fz_rect rect;
pdf = ely_pdf_of(ctx, doc);
page = pdf_load_page(ctx, pdf, number);
pool = fz_new_pool(ctx);
spec = fz_parse_json(ctx, pool, json);
kindj = ely_member(ctx, spec, "kind", FZ_JSON_STRING);
namej = ely_member(ctx, spec, "name", FZ_JSON_STRING);
options = ely_member(ctx, spec, "options", FZ_JSON_ARRAY);
if (!kindj || !namej)
fz_throw(ctx, FZ_ERROR_ARGUMENT, "a field with no kind or name");
kind = kindj->u.string, name = namej->u.string;
if (strcmp(kind, "text") && strcmp(kind, "checkbox") && strcmp(kind, "radio") && strcmp(kind, "choice"))
fz_throw(ctx, FZ_ERROR_ARGUMENT, "no field kind %s", kind);
if (!strcmp(kind, "choice"))
check_options(ctx, options);
rect = ely_rect(ctx, ely_member(ctx, spec, "rect", FZ_JSON_ARRAY), 4);
group = check_name(ctx, pdf_dict_getp(ctx, pdf_trailer(ctx, pdf), "Root/AcroForm/Fields"), "", name, kind, NULL);
pdf_begin_operation(ctx, pdf, "Add a field");
begun = 1;
fields = fields_of(ctx, pdf);
widget = pdf_create_annot_raw(ctx, page, PDF_ANNOT_WIDGET);
obj = pdf_annot_obj(ctx, widget);
pdf_dict_put(ctx, obj, PDF_NAME(P), page->obj);
pdf_dict_put_int(ctx, obj, PDF_NAME(F), PDF_ANNOT_IS_PRINT);
pdf_dict_put_rect(ctx, obj, PDF_NAME(Rect), fz_transform_rect(rect, ely_to_user(ctx, page)));
pdf_dict_put_text_string(ctx, obj, PDF_NAME(DA), "/Helv 0 Tf 0 g");
mk = pdf_dict_put_dict(ctx, obj, PDF_NAME(MK), 1);
pdf_array_push_real(ctx, pdf_dict_put_array(ctx, mk, PDF_NAME(BC), 1), 0);
if (!strcmp(kind, "radio"))
{
if (!group)
{
group = pdf_add_new_dict(ctx, pdf, 4);
pdf_dict_put(ctx, group, PDF_NAME(FT), PDF_NAME(Btn));
pdf_dict_put_int(ctx, group, PDF_NAME(Ff), PDF_BTN_FIELD_IS_RADIO | PDF_BTN_FIELD_IS_NO_TOGGLE_TO_OFF);
pdf_dict_put_text_string(ctx, group, PDF_NAME(T), name);
pdf_dict_put(ctx, group, PDF_NAME(V), PDF_NAME(Off));
pdf_dict_put_array(ctx, group, PDF_NAME(Kids), 2);
pdf_array_push_drop(ctx, fields, group);
}
state = new_state(ctx, group);
pdf_dict_put(ctx, obj, PDF_NAME(Parent), group);
pdf_array_push(ctx, pdf_dict_get(ctx, group, PDF_NAME(Kids)), obj);
}
else
{
pdf_dict_put_text_string(ctx, obj, PDF_NAME(T), name);
pdf_array_push(ctx, fields, obj);
}
if (!strcmp(kind, "text"))
pdf_dict_put(ctx, obj, PDF_NAME(FT), PDF_NAME(Tx));
else if (!strcmp(kind, "choice"))
{
pdf_dict_put(ctx, obj, PDF_NAME(FT), PDF_NAME(Ch));
pdf_dict_put_int(ctx, obj, PDF_NAME(Ff), PDF_CH_FIELD_IS_COMBO);
put_options(ctx, obj, options);
}
else if (!strcmp(kind, "checkbox"))
{
pdf_dict_put(ctx, obj, PDF_NAME(FT), PDF_NAME(Btn));
pdf_dict_put(ctx, obj, PDF_NAME(V), PDF_NAME(Off));
state = pdf_new_name(ctx, "Yes");
}
/* The on look is drawn under its own state. */
if (state)
pdf_dict_put(ctx, obj, PDF_NAME(AS), state);
pdf_annot_request_resynthesis(ctx, widget);
pdf_update_annot(ctx, widget);
if (state)
pdf_dict_put(ctx, obj, PDF_NAME(AS), PDF_NAME(Off));
pdf_end_operation(ctx, pdf);
}
fz_always(ctx)
{
fz_drop_pool(ctx, pool);
pdf_drop_obj(ctx, state);
pdf_drop_annot(ctx, widget);
fz_drop_page(ctx, (fz_page *)page);
}
fz_catch(ctx)
return ely_edit_failed(ctx, pdf, begun);
return 0;
}
/* Renames a field or sets its options. */
int ely_change_field(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;
char *path = NULL;
int begun = 0;
fz_var(pdf);
fz_var(page);
fz_var(pool);
fz_var(path);
fz_var(begun);
fz_try(ctx)
{
pdf_annot *widget;
pdf_obj *obj, *head;
fz_json *spec, *name, *options;
enum pdf_widget_type type;
pdf = ely_pdf_of(ctx, doc);
page = pdf_load_page(ctx, pdf, number);
widget = ely_find_annot(ctx, page, id, 1);
obj = pdf_annot_obj(ctx, widget);
head = head_of(ctx, obj);
type = pdf_widget_type(ctx, widget);
pool = fz_new_pool(ctx);
spec = fz_parse_json(ctx, pool, json);
name = ely_member(ctx, spec, "name", FZ_JSON_STRING);
options = ely_member(ctx, spec, "options", FZ_JSON_ARRAY);
if (!name && !options)
fz_throw(ctx, FZ_ERROR_ARGUMENT, "no change");
if (name)
{
path = parent_path(ctx, head);
check_name(ctx, pdf_dict_getp(ctx, pdf_trailer(ctx, pdf), "Root/AcroForm/Fields"), path, name->u.string, "", head);
}
if (options && type != PDF_WIDGET_TYPE_COMBOBOX && type != PDF_WIDGET_TYPE_LISTBOX)
fz_throw(ctx, FZ_ERROR_ARGUMENT, "only a choice has options");
if (options)
check_options(ctx, options);
pdf_begin_operation(ctx, pdf, "Change a field");
begun = 1;
if (name)
pdf_dict_put_text_string(ctx, head, PDF_NAME(T), name->u.string);
if (options)
{
int i, kept = 0;
const char *value = pdf_to_text_string(ctx, pdf_dict_get_inheritable(ctx, obj, PDF_NAME(V)));
for (i = 0; i < fz_json_array_length(ctx, options); i++)
kept |= !strcmp(fz_json_array_get(ctx, options, i)->u.string, value);
if (obj != head)
pdf_dict_del(ctx, obj, PDF_NAME(Opt));
put_options(ctx, head, options);
if (!kept)
pdf_dict_del(ctx, head, PDF_NAME(V));
}
pdf_annot_request_resynthesis(ctx, widget);
pdf_update_annot(ctx, widget);
pdf_end_operation(ctx, pdf);
}
fz_always(ctx)
{
fz_drop_pool(ctx, pool);
fz_free(ctx, path);
fz_drop_page(ctx, (fz_page *)page);
}
fz_catch(ctx)
return ely_edit_failed(ctx, pdf, begun);
return 0;
}
/* Writes each named node under `kids`, with its full name. */
static void write_names(fz_context *ctx, fz_output *out, pdf_obj *kids, const char *prefix, int depth, int *first)
{
int i;
if (depth > 32)
fz_throw(ctx, FZ_ERROR_FORMAT, "a field tree too deep");
for (i = 0; i < pdf_array_len(ctx, kids); i++)
{
pdf_obj *node = pdf_array_get(ctx, kids, i);
pdf_obj *t = pdf_dict_get(ctx, node, PDF_NAME(T));
char *name = NULL;
fz_var(name);
fz_try(ctx)
{
name = t ? fz_asprintf(ctx, "%s%s%s", prefix, prefix[0] ? "." : "", pdf_to_text_string(ctx, t)) : fz_strdup(ctx, prefix);
if (t)
{
fz_write_string(ctx, out, *first ? "" : ",");
ely_json_string(ctx, out, name);
*first = 0;
}
write_names(ctx, out, pdf_dict_get(ctx, node, PDF_NAME(Kids)), name, depth + 1, first);
}
fz_always(ctx)
fz_free(ctx, name);
fz_catch(ctx)
fz_rethrow(ctx);
}
}
/* Every field name in the form, widgets or not. */
int ely_field_names(fz_context *ctx, fz_document *doc, unsigned char **data, size_t *len)
{
fz_buffer *buf = NULL;
fz_output *out = NULL;
fz_var(buf);
fz_var(out);
fz_try(ctx)
{
int first = 1;
pdf_document *pdf = ely_pdf_of(ctx, doc);
buf = fz_new_buffer(ctx, 256);
out = fz_new_output_with_buffer(ctx, buf);
fz_write_byte(ctx, out, '[');
write_names(ctx, out, pdf_dict_getp(ctx, pdf_trailer(ctx, pdf), "Root/AcroForm/Fields"), "", 0, &first);
fz_write_byte(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_catch(ctx)
return ely_fail(ctx);
return 0;
}