From da50c6f300e565c8eedf4ce252c44b5ddbb95cee Mon Sep 17 00:00:00 2001 From: Robin Watts Date: Wed, 9 Nov 2016 17:41:43 +0000 Subject: Add pdf_layer configuration API. Add API to: * allow enumeration of layer configs (OCCDs) within PDF files. * allow selection of layer configs. * allow enumeration of the "UI" (or "Human readable") form of layer configs. * allow selection/toggling of entries in the UI. --- source/pdf/pdf-layer.c | 715 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 715 insertions(+) create mode 100644 source/pdf/pdf-layer.c (limited to 'source/pdf/pdf-layer.c') diff --git a/source/pdf/pdf-layer.c b/source/pdf/pdf-layer.c new file mode 100644 index 00000000..70dfcd50 --- /dev/null +++ b/source/pdf/pdf-layer.c @@ -0,0 +1,715 @@ +#include "mupdf/fitz.h" +#include "pdf-imp.h" + +/* + Notes on OCGs etc. + + PDF Documents may contain Optional Content Groups. Which of + these is shown at any given time is dependent on which + Optional Content Configuration Dictionary is in force at the + time. + + A pdf_document, once loaded, contains some state saying which + OCGs are enabled/disabled, and which 'Intent' (or 'Intents') + a file is being used for. This information is held outside of + the actual PDF file. + + An Intent (just 'View' or 'Design' or 'All', according to + PDF 2.0, but theoretically more) says which OCGs to consider + or ignore in calculating the visibility of content. The + Intent (or Intents, for there can be an array) is set by the + current OCCD. + + When first loaded, we turn all OCGs on, then load the default + OCCD. This may turn some OCGs off, and sets the document Intent. + + Callers can ask how many OCCDs there are, read the names/creators + for each, and then select any one of them. That updates which + OCGs are selected, and resets the Intent. + + Once an OCCD has been selected, a caller can enumerate the + 'displayable configuration'. This is a list of labels/radio + buttons/check buttons that can be used to enable/disable + given OCGs. The caller can then enable/disable OCGs by + asking to select (or toggle) given entries in that list. + + Thus the handling of radio button groups, and 'locked' + elements is kept within the core of MuPDF. + + Finally, the caller can set the 'usage' for a document. This + can be 'View', 'Print', or 'Export'. +*/ + +typedef struct +{ + pdf_obj *obj; + int state; +} pdf_ocg_entry; + +typedef struct +{ + int ocg; + const char *name; + int depth; + unsigned int button_flags : 2; + unsigned int locked : 1; +} pdf_ocg_ui; + +struct pdf_ocg_descriptor_s +{ + int current; + int num_configs; + + int len; + pdf_ocg_entry *ocgs; + + pdf_obj *intent; + char *usage; + + int num_ui_entries; + pdf_ocg_ui *ui; +}; + +int +pdf_count_layer_configs(fz_context *ctx, pdf_document *doc) +{ + /* If no OCProperties, then no OCGs */ + if (!doc || !doc->ocg) + return 0; + return doc->ocg->num_configs; +} + + +static int +count_entries(fz_context *ctx, pdf_obj *obj) +{ + int len = pdf_array_len(ctx, obj); + int i; + int count = 0; + + for (i = 0; i < len; i++) + { + pdf_obj *o = pdf_array_get(ctx, obj, i); + count += (pdf_is_array(ctx, o) ? count_entries(ctx, o) : 1); + } + return count; +} + +static pdf_ocg_ui * +populate_ui(fz_context *ctx, pdf_ocg_descriptor *desc, pdf_ocg_ui *ui, pdf_obj *order, int depth, pdf_obj *rbgroups, pdf_obj *locked) +{ + int len = pdf_array_len(ctx, order); + int i, j; + + for (i = 0; i < len; i++) + { + pdf_obj *o = pdf_array_get(ctx, order, i); + if (pdf_is_array(ctx, o)) + { + ui = populate_ui(ctx, desc, ui, o, depth+1, rbgroups, locked); + continue; + } + ui->depth = depth; + if (pdf_is_string(ctx, o)) + { + ui->ocg = -1; + ui->name = pdf_to_str_buf(ctx, o); + ui->button_flags = PDF_LAYER_UI_LABEL; + ui->locked = 1; + ui++; + continue; + } + + for (j = 0; j < desc->len; j++) + { + if (!pdf_objcmp_resolve(ctx, o, desc->ocgs[j].obj)) + break; + } + if (j == desc->len) + continue; /* OCG not found in main list! Just ignore it */ + ui->ocg = j; + ui->name = pdf_to_str_buf(ctx, pdf_dict_get(ctx, o, PDF_NAME_Name)); + ui->button_flags = pdf_array_contains(ctx, o, rbgroups) ? PDF_LAYER_UI_RADIOBOX : PDF_LAYER_UI_CHECKBOX; + ui->locked = pdf_array_contains(ctx, o, locked); + ui++; + } + return ui; +} + +static void +drop_ui(fz_context *ctx, pdf_ocg_descriptor *desc) +{ + if (!desc) + return; + + fz_free(ctx, desc->ui); + desc->ui = NULL; +} + +static void +load_ui(fz_context *ctx, pdf_ocg_descriptor *desc, pdf_obj *ocprops, pdf_obj *occg) +{ + pdf_obj *order; + pdf_obj *rbgroups; + pdf_obj *locked; + int count; + + /* Count the number of entries */ + order = pdf_dict_get(ctx, occg, PDF_NAME_Order); + count = count_entries(ctx, order); + rbgroups = pdf_dict_get(ctx, occg, PDF_NAME_RBGroups); + locked = pdf_dict_get(ctx, occg, PDF_NAME_Locked); + + desc->num_ui_entries = count; + desc->ui = Memento_label(fz_calloc(ctx, count, sizeof(pdf_ocg_ui)), "pdf_ocg_ui"); + fz_try(ctx) + { + (void)populate_ui(ctx, desc, desc->ui, order, 0, rbgroups, locked); + } + fz_catch(ctx) + { + drop_ui(ctx, desc); + fz_rethrow(ctx); + } +} + +void +pdf_select_layer_config(fz_context *ctx, pdf_document *doc, int config) +{ + int i, j, len, len2; + pdf_ocg_descriptor *desc = doc->ocg; + pdf_obj *obj, *cobj; + pdf_obj *name; + + obj = pdf_dict_get(ctx, pdf_dict_get(ctx, pdf_trailer(ctx, doc), PDF_NAME_Root), PDF_NAME_OCProperties); + if (!obj) + { + if (config == 0) + return; + else + fz_throw(ctx, FZ_ERROR_GENERIC, "Unknown Layer config (None known!)"); + } + + cobj = pdf_array_get(ctx, pdf_dict_get(ctx, obj, PDF_NAME_Configs), config); + if (!cobj) + { + if (config != 0) + fz_throw(ctx, FZ_ERROR_GENERIC, "Illegal Layer config"); + cobj = pdf_dict_get(ctx, obj, PDF_NAME_D); + if (!cobj) + fz_throw(ctx, FZ_ERROR_GENERIC, "No default Layer config"); + } + + pdf_drop_obj(ctx, desc->intent); + desc->intent = pdf_keep_obj(ctx, pdf_dict_get(ctx, cobj, PDF_NAME_Intent)); + + len = desc->len; + name = pdf_dict_get(ctx, cobj, PDF_NAME_BaseState); + if (pdf_name_eq(ctx, name, PDF_NAME_Unchanged)) + { + /* Do nothing */ + } + else if (pdf_name_eq(ctx, name, PDF_NAME_OFF)) + { + for (i = 0; i < len; i++) + { + desc->ocgs[i].state = 0; + } + } + else /* Default to ON */ + { + for (i = 0; i < len; i++) + { + desc->ocgs[i].state = 1; + } + } + + obj = pdf_dict_get(ctx, cobj, PDF_NAME_ON); + len2 = pdf_array_len(ctx, obj); + for (i = 0; i < len2; i++) + { + pdf_obj *o = pdf_array_get(ctx, obj, i); + for (j=0; j < len; j++) + { + if (!pdf_objcmp_resolve(ctx, desc->ocgs[j].obj, o)) + { + desc->ocgs[j].state = 1; + break; + } + } + } + + obj = pdf_dict_get(ctx, cobj, PDF_NAME_OFF); + len2 = pdf_array_len(ctx, obj); + for (i = 0; i < len2; i++) + { + pdf_obj *o = pdf_array_get(ctx, obj, i); + for (j=0; j < len; j++) + { + if (!pdf_objcmp_resolve(ctx, desc->ocgs[j].obj, o)) + { + desc->ocgs[j].state = 0; + break; + } + } + } + + desc->current = config; + + drop_ui(ctx, desc); + load_ui(ctx, desc, obj, cobj); +} + +void +pdf_layer_config_info(fz_context *ctx, pdf_document *doc, int config_num, pdf_layer_config *info) +{ + pdf_obj *ocprops; + pdf_obj *obj; + + if (!info) + return; + + info->name = NULL; + info->creator = NULL; + + if (doc == NULL || doc->ocg == NULL) + return; + if (config_num < 0 || config_num >= doc->ocg->num_configs) + fz_throw(ctx, FZ_ERROR_GENERIC, "Invalid layer config number"); + + ocprops = pdf_dict_getp(ctx, pdf_trailer(ctx, doc), "Root/OCProperties"); + if (!ocprops) + return; + + obj = pdf_dict_get(ctx, ocprops, PDF_NAME_Configs); + if (pdf_is_array(ctx, obj)) + obj = pdf_array_get(ctx, obj, config_num); + else if (config_num == 0) + obj = pdf_dict_get(ctx, ocprops, PDF_NAME_D); + else + fz_throw(ctx, FZ_ERROR_GENERIC, "Invalid layer config number"); + + info->creator = pdf_to_str_buf(ctx, pdf_dict_get(ctx, obj, PDF_NAME_Creator)); + info->name = pdf_to_str_buf(ctx, pdf_dict_get(ctx, obj, PDF_NAME_Name)); +} + +void +pdf_drop_ocg(fz_context *ctx, pdf_document *doc) +{ + pdf_ocg_descriptor *desc; + int i; + + if (!doc) + return; + desc = doc->ocg; + if (!desc) + return; + + pdf_drop_obj(ctx, desc->intent); + for (i = 0; i < desc->len; i++) + pdf_drop_obj(ctx, desc->ocgs[i].obj); + fz_free(ctx, desc->ocgs); + fz_free(ctx, desc); +} + +static void +clear_radio_group(fz_context *ctx, pdf_document *doc, pdf_obj *ocg) +{ + pdf_obj *rbgroups = pdf_dict_getp(ctx, pdf_trailer(ctx, doc), "Root/OCProperties/RBGroups"); + int len, i; + + len = pdf_array_len(ctx, rbgroups); + for (i = 0; i < len; i++) + { + pdf_obj *group = pdf_array_get(ctx, rbgroups, i); + + if (pdf_array_contains(ctx, ocg, group)) + { + int len2 = pdf_array_len(ctx, group); + int j; + + for (j = 0; j < len2; j++) + { + pdf_obj *g = pdf_array_get(ctx, group, j); + int k; + for (k = 0; k < doc->ocg->len; k++) + { + pdf_ocg_entry *s = &doc->ocg->ocgs[k]; + + if (!pdf_objcmp_resolve(ctx, s->obj, g)) + s->state = 0; + } + } + } + } +} + +int pdf_count_layer_config_ui(fz_context *ctx, pdf_document *doc) +{ + if (doc == NULL || doc->ocg == NULL) + return 0; + + return doc->ocg->num_ui_entries; +} + +void pdf_select_layer_config_ui(fz_context *ctx, pdf_document *doc, int ui) +{ + pdf_ocg_ui *entry; + + if (doc == NULL || doc->ocg == NULL) + return; + + if (ui < 0 || ui >= doc->ocg->num_ui_entries) + fz_throw(ctx, FZ_ERROR_GENERIC, "Out of range UI entry selected"); + + entry = &doc->ocg->ui[ui]; + if (entry->button_flags != PDF_LAYER_UI_RADIOBOX && + entry->button_flags != PDF_LAYER_UI_CHECKBOX) + return; + if (entry->locked) + return; + + if (entry->button_flags == PDF_LAYER_UI_RADIOBOX) + clear_radio_group(ctx, doc, doc->ocg->ocgs[entry->ocg].obj); + + doc->ocg->ocgs[entry->ocg].state = 1; +} + +void pdf_toggle_layer_config_ui(fz_context *ctx, pdf_document *doc, int ui) +{ + pdf_ocg_ui *entry; + int selected; + + if (doc == NULL || doc->ocg == NULL) + return; + + if (ui < 0 || ui >= doc->ocg->num_ui_entries) + fz_throw(ctx, FZ_ERROR_GENERIC, "Out of range UI entry toggled"); + + entry = &doc->ocg->ui[ui]; + if (entry->button_flags != PDF_LAYER_UI_RADIOBOX && + entry->button_flags != PDF_LAYER_UI_CHECKBOX) + return; + if (entry->locked) + return; + + selected = doc->ocg->ocgs[entry->ocg].state; + + if (entry->button_flags == PDF_LAYER_UI_RADIOBOX) + clear_radio_group(ctx, doc, doc->ocg->ocgs[entry->ocg].obj); + + doc->ocg->ocgs[entry->ocg].state = !selected; +} + +void pdf_deselect_layer_ui(fz_context *ctx, pdf_document *doc, int ui) +{ + pdf_ocg_ui *entry; + + if (doc == NULL || doc->ocg == NULL) + return; + + if (ui < 0 || ui >= doc->ocg->num_ui_entries) + fz_throw(ctx, FZ_ERROR_GENERIC, "Out of range UI entry deselected"); + + entry = &doc->ocg->ui[ui]; + if (entry->button_flags != PDF_LAYER_UI_RADIOBOX && + entry->button_flags != PDF_LAYER_UI_CHECKBOX) + return; + if (entry->locked) + return; + + doc->ocg->ocgs[entry->ocg].state = 0; +} + +void +pdf_layer_config_ui_info(fz_context *ctx, pdf_document *doc, int ui, pdf_layer_config_ui *info) +{ + pdf_ocg_ui *entry; + + if (!info) + return; + + info->depth = 0; + info->locked = 0; + info->selected = 0; + info->text = NULL; + info->type = 0; + + if (doc == NULL || doc->ocg == NULL) + return; + + if (ui < 0 || ui >= doc->ocg->num_ui_entries) + fz_throw(ctx, FZ_ERROR_GENERIC, "Out of range UI entry selected"); + + entry = &doc->ocg->ui[ui]; + info->type = entry->button_flags; + info->depth = entry->depth; + info->selected = doc->ocg->ocgs[entry->ocg].state; + info->locked = entry->locked; + info->text = entry->name; +} + +static int +ocg_intents_include(fz_context *ctx, pdf_ocg_descriptor *desc, char *name) +{ + int i, len; + + if (strcmp(name, "All") == 0) + return 1; + + /* In the absence of a specified intent, it's 'View' */ + if (!desc->intent) + return (strcmp(name, "View") == 0); + + if (pdf_is_name(ctx, desc->intent)) + { + char *intent = pdf_to_name(ctx, desc->intent); + if (strcmp(intent, "All") == 0) + return 1; + return (strcmp(intent, name) == 0); + } + if (!pdf_is_array(ctx, desc->intent)) + return 0; + + len = pdf_array_len(ctx, desc->intent); + for (i=0; i < len; i++) + { + char *intent = pdf_to_name(ctx, pdf_array_get(ctx, desc->intent, i)); + if (strcmp(intent, "All") == 0) + return 1; + if (strcmp(intent, name) == 0) + return 1; + } + return 0; +} + +int +pdf_is_hidden_ocg(fz_context *ctx, pdf_ocg_descriptor *desc, pdf_obj *rdb, const char *usage, pdf_obj *ocg) +{ + char event_state[16]; + pdf_obj *obj, *obj2, *type; + + /* Avoid infinite recursions */ + if (pdf_obj_marked(ctx, ocg)) + return 0; + + /* If no usage, everything is visible */ + if (!usage) + return 0; + + /* If no ocg descriptor, everything is visible */ + if (!desc) + return 0; + + /* If we've been handed a name, look it up in the properties. */ + if (pdf_is_name(ctx, ocg)) + { + ocg = pdf_dict_get(ctx, pdf_dict_get(ctx, rdb, PDF_NAME_Properties), ocg); + } + /* If we haven't been given an ocg at all, then we're visible */ + if (!ocg) + return 0; + + fz_strlcpy(event_state, usage, sizeof event_state); + fz_strlcat(event_state, "State", sizeof event_state); + + type = pdf_dict_get(ctx, ocg, PDF_NAME_Type); + + if (pdf_name_eq(ctx, type, PDF_NAME_OCG)) + { + /* An Optional Content Group */ + int default_value = 0; + int len = desc->len; + int i; + pdf_obj *es; + + /* by default an OCG is visible, unless it's explicitly hidden */ + for (i = 0; i < len; i++) + { + if (!pdf_objcmp_resolve(ctx, desc->ocgs[i].obj, ocg)) + { + default_value = !desc->ocgs[i].state; + break; + } + } + + /* Check Intents; if our intent is not part of the set given + * by the current config, we should ignore it. */ + obj = pdf_dict_get(ctx, ocg, PDF_NAME_Intent); + if (pdf_is_name(ctx, obj)) + { + /* If it doesn't match, it's hidden */ + if (ocg_intents_include(ctx, desc, pdf_to_name(ctx, obj)) == 0) + return 1; + } + else if (pdf_is_array(ctx, obj)) + { + int match = 0; + len = pdf_array_len(ctx, obj); + for (i=0; i