#include "mupdf/fitz.h" #include "mupdf/pdf.h" #include /* 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; const char *usage; int num_ui_entries; pdf_ocg_ui *ui; }; /* Get the number of layer configurations defined in this document. doc: The document in question. */ 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); if (pdf_mark_obj(ctx, o)) continue; fz_try(ctx) count += (pdf_is_array(ctx, o) ? count_entries(ctx, o) : 1); fz_always(ctx) pdf_unmark_obj(ctx, o); fz_catch(ctx) fz_rethrow(ctx); } 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)) { if (pdf_mark_obj(ctx, o)) continue; fz_try(ctx) ui = populate_ui(ctx, desc, ui, o, depth+1, rbgroups, locked); fz_always(ctx) pdf_unmark_obj(ctx, o); fz_catch(ctx) fz_rethrow(ctx); 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_dict_get_string(ctx, o, PDF_NAME(Name), NULL); 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)); if (!order) order = pdf_dict_getp(ctx, ocprops, "D/Order"); count = count_entries(ctx, order); rbgroups = pdf_dict_get(ctx, occg, PDF_NAME(RBGroups)); if (!rbgroups) rbgroups = pdf_dict_getp(ctx, ocprops, "D/RBGroups"); locked = pdf_dict_get(ctx, occg, PDF_NAME(Locked)); desc->num_ui_entries = count; if (desc->num_ui_entries == 0) return; 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); } } /* Set the current configuration. This updates the visibility of the optional content groups within the document. doc: The document in question. config_num: A value in the 0..n-1 range, where n is the value returned from pdf_count_layer_configs. */ 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); } /* Fetch the name (and optionally creator) of the given layer config. doc: The document in question. config_num: A value in the 0..n-1 range, where n is the value returned from pdf_count_layer_configs. info: Pointer to structure to fill in. Pointers within this structure may be set to NULL if no information is available. */ 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_dict_get_string(ctx, obj, PDF_NAME(Creator), NULL); info->name = pdf_dict_get_string(ctx, obj, PDF_NAME(Name), NULL); } 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; drop_ui(ctx, desc); 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; } } } } } /* Returns the number of entries in the 'UI' for this layer configuration. doc: The document in question. */ 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; } /* Select a checkbox/radiobox within the 'UI' for this layer configuration. Selecting a UI entry that is a radiobox may disable other UI entries. doc: The document in question. ui: A value in the 0..m-1 range, where m is the value returned by pdf_count_layer_config_ui. */ 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; } /* Toggle a checkbox/radiobox within the 'UI' for this layer configuration. Toggling a UI entry that is a radiobox may disable other UI entries. doc: The document in question. ui: A value in the 0..m-1 range, where m is the value returned by pdf_count_layer_config_ui. */ 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; } /* Select a checkbox/radiobox within the 'UI' for this layer configuration. doc: The document in question. ui: A value in the 0..m-1 range, where m is the value returned by pdf_count_layer_config_ui. */ void pdf_deselect_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 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; } /* Get the info for a given entry in the layer config ui. doc: The document in question. ui: A value in the 0..m-1 range, where m is the value returned by pdf_count_layer_config_ui. info: Pointer to a structure to fill in with information about the requested ui entry. */ 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, const 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)) { const 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++) { const 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