X-Git-Url: https://pintos-os.org/cgi-bin/gitweb.cgi?a=blobdiff_plain;f=src%2Foutput%2Fspv%2Fspv.c;fp=src%2Foutput%2Fspv%2Fspv.c;h=3f5a743c82bcb298d99e357d3a31eb7ccfc65176;hb=bcaaee5f0bd21f443c8dcb5f67114e63d43673af;hp=0000000000000000000000000000000000000000;hpb=1abd7f599dd0d773add0a98fa3b612bc15aaf422;p=pspp diff --git a/src/output/spv/spv.c b/src/output/spv/spv.c new file mode 100644 index 0000000000..3f5a743c82 --- /dev/null +++ b/src/output/spv/spv.c @@ -0,0 +1,1181 @@ +/* PSPP - a program for statistical analysis. + Copyright (C) 2017, 2018 Free Software Foundation, Inc. + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . */ + +#include + +#include "output/spv/spv.h" + +#include +#include +#include +#include +#include +#include + +#include "libpspp/assertion.h" +#include "libpspp/cast.h" +#include "libpspp/hash-functions.h" +#include "libpspp/message.h" +#include "libpspp/str.h" +#include "libpspp/zip-reader.h" +#include "output/page-setup-item.h" +#include "output/pivot-table.h" +#include "output/spv/detail-xml-parser.h" +#include "output/spv/light-binary-parser.h" +#include "output/spv/spv-css-parser.h" +#include "output/spv/spv-legacy-data.h" +#include "output/spv/spv-legacy-decoder.h" +#include "output/spv/spv-light-decoder.h" +#include "output/spv/structure-xml-parser.h" + +#include "gl/c-ctype.h" +#include "gl/intprops.h" +#include "gl/minmax.h" +#include "gl/xalloc.h" +#include "gl/xvasprintf.h" +#include "gl/xsize.h" + +#include "gettext.h" +#define _(msgid) gettext (msgid) +#define N_(msgid) (msgid) + +struct spv_reader + { + struct string zip_errs; + struct zip_reader *zip; + struct spv_item *root; + struct page_setup *page_setup; + }; + +const struct page_setup * +spv_get_page_setup (const struct spv_reader *spv) +{ + return spv->page_setup; +} + +const char * +spv_item_type_to_string (enum spv_item_type type) +{ + switch (type) + { + case SPV_ITEM_HEADING: return "heading"; + case SPV_ITEM_TEXT: return "text"; + case SPV_ITEM_TABLE: return "table"; + case SPV_ITEM_GRAPH: return "graph"; + case SPV_ITEM_MODEL: return "model"; + case SPV_ITEM_OBJECT: return "object"; + default: return "**error**"; + } +} + +const char * +spv_item_class_to_string (enum spv_item_class class) +{ + switch (class) + { +#define SPV_CLASS(ENUM, NAME) case SPV_CLASS_##ENUM: return NAME; + SPV_CLASSES +#undef SPV_CLASS + default: return NULL; + } +} + +enum spv_item_class +spv_item_class_from_string (const char *name) +{ +#define SPV_CLASS(ENUM, NAME) \ + if (!strcmp (name, NAME)) return SPV_CLASS_##ENUM; + SPV_CLASSES +#undef SPV_CLASS + + return SPV_N_CLASSES; +} + +enum spv_item_type +spv_item_get_type (const struct spv_item *item) +{ + return item->type; +} + +enum spv_item_class +spv_item_get_class (const struct spv_item *item) +{ + const char *label = spv_item_get_label (item); + if (!label) + label = ""; + + switch (item->type) + { + case SPV_ITEM_HEADING: + return SPV_CLASS_OUTLINEHEADERS; + + case SPV_ITEM_TEXT: + return (!strcmp (label, "Title") ? SPV_CLASS_HEADINGS + : !strcmp (label, "Log") ? SPV_CLASS_LOGS + : !strcmp (label, "Page Title") ? SPV_CLASS_PAGETITLE + : SPV_CLASS_TEXTS); + + case SPV_ITEM_TABLE: + return (!strcmp (label, "Warnings") ? SPV_CLASS_WARNINGS + : !strcmp (label, "Notes") ? SPV_CLASS_NOTES + : SPV_CLASS_TABLES); + + case SPV_ITEM_GRAPH: + return SPV_CLASS_CHARTS; + + case SPV_ITEM_MODEL: + return SPV_CLASS_MODELS; + + case SPV_ITEM_OBJECT: + return SPV_CLASS_OTHER; + + default: + return SPV_CLASS_UNKNOWN; + } +} + +const char * +spv_item_get_label (const struct spv_item *item) +{ + return item->label; +} + +bool +spv_item_is_heading (const struct spv_item *item) +{ + return item->type == SPV_ITEM_HEADING; +} + +size_t +spv_item_get_n_children (const struct spv_item *item) +{ + return item->n_children; +} + +struct spv_item * +spv_item_get_child (const struct spv_item *item, size_t idx) +{ + assert (idx < item->n_children); + return item->children[idx]; +} + +bool +spv_item_is_table (const struct spv_item *item) +{ + return item->type == SPV_ITEM_TABLE; +} + +bool +spv_item_is_text (const struct spv_item *item) +{ + return item->type == SPV_ITEM_TEXT; +} + +const struct pivot_value * +spv_item_get_text (const struct spv_item *item) +{ + assert (spv_item_is_text (item)); + return item->text; +} + +struct spv_item * +spv_item_next (const struct spv_item *item) +{ + if (item->n_children) + return item->children[0]; + + while (item->parent) + { + size_t idx = item->parent_idx + 1; + item = item->parent; + if (idx < item->n_children) + return item->children[idx]; + } + + return NULL; +} + +const struct spv_item * +spv_item_get_parent (const struct spv_item *item) +{ + return item->parent; +} + +size_t +spv_item_get_level (const struct spv_item *item) +{ + int level = 0; + for (; item->parent; item = item->parent) + level++; + return level; +} + +const char * +spv_item_get_command_id (const struct spv_item *item) +{ + return item->command_id; +} + +const char * +spv_item_get_subtype (const struct spv_item *item) +{ + return item->subtype; +} + +bool +spv_item_is_visible (const struct spv_item *item) +{ + return item->visible; +} + +static void +spv_item_destroy (struct spv_item *item) +{ + if (item) + { + free (item->structure_member); + + free (item->label); + free (item->command_id); + + for (size_t i = 0; i < item->n_children; i++) + spv_item_destroy (item->children[i]); + free (item->children); + + pivot_table_unref (item->table); + spv_legacy_properties_destroy (item->legacy_properties); + free (item->bin_member); + free (item->xml_member); + free (item->subtype); + + pivot_value_destroy (item->text); + + free (item->object_type); + free (item->uri); + + free (item); + } +} + +static void +spv_heading_add_child (struct spv_item *parent, struct spv_item *child) +{ + assert (parent->type == SPV_ITEM_HEADING); + assert (!child->parent); + + child->parent = parent; + child->parent_idx = parent->n_children; + + if (parent->n_children >= parent->allocated_children) + parent->children = x2nrealloc (parent->children, + &parent->allocated_children, + sizeof *parent->children); + parent->children[parent->n_children++] = child; +} + +static xmlNode * +find_xml_child_element (xmlNode *parent, const char *child_name) +{ + for (xmlNode *node = parent->children; node; node = node->next) + if (node->type == XML_ELEMENT_NODE + && node->name + && !strcmp (CHAR_CAST (char *, node->name), child_name)) + return node; + + return NULL; +} + +static char * +get_xml_attr (const xmlNode *node, const char *name) +{ + return CHAR_CAST (char *, xmlGetProp (node, CHAR_CAST (xmlChar *, name))); +} + +static void +put_xml_attr (const char *name, const char *value, struct string *dst) +{ + if (!value) + return; + + ds_put_format (dst, " %s=\"", name); + for (const char *p = value; *p; p++) + { + switch (*p) + { + case '\n': + ds_put_cstr (dst, " "); + break; + case '&': + ds_put_cstr (dst, "&"); + break; + case '<': + ds_put_cstr (dst, "<"); + break; + case '>': + ds_put_cstr (dst, ">"); + break; + case '"': + ds_put_cstr (dst, """); + break; + default: + ds_put_byte (dst, *p); + break; + } + } + ds_put_byte (dst, '"'); +} + +static void +extract_html_text (const xmlNode *node, int base_font_size, struct string *s) +{ + if (node->type == XML_ELEMENT_NODE) + { + const char *name = CHAR_CAST (char *, node->name); + if (!strcmp (name, "br")) + ds_put_byte (s, '\n'); + else if (strcmp (name, "style")) + { + const char *tag = NULL; + if (strchr ("biu", name[0]) && name[1] == '\0') + { + tag = name; + ds_put_format (s, "<%s>", tag); + } + else if (!strcmp (name, "font")) + { + tag = "span"; + ds_put_format (s, "<%s", tag); + + char *face = get_xml_attr (node, "face"); + put_xml_attr ("face", face, s); + free (face); + + char *color = get_xml_attr (node, "color"); + if (color) + { + if (color[0] == '#') + put_xml_attr ("color", color, s); + else + { + uint8_t r, g, b; + if (sscanf (color, "rgb (%"SCNu8", %"SCNu8", %"SCNu8" )", + &r, &g, &b) == 3) + { + char color2[8]; + snprintf (color2, sizeof color2, + "#%02"PRIx8"%02"PRIx8"%02"PRIx8, + r, g, b); + put_xml_attr ("color", color2, s); + } + } + } + free (color); + + char *size_s = get_xml_attr (node, "size"); + int html_size = size_s ? atoi (size_s) : 0; + free (size_s); + if (html_size >= 1 && html_size <= 7) + { + static const double scale[7] = { + .444, .556, .667, .778, 1.0, 1.33, 2.0 + }; + double size = base_font_size * scale[html_size - 1]; + + char size2[INT_BUFSIZE_BOUND (int)]; + snprintf (size2, sizeof size2, "%.0f", size * 1024.); + put_xml_attr ("size", size2, s); + } + + ds_put_cstr (s, ">"); + } + for (const xmlNode *child = node->children; child; + child = child->next) + extract_html_text (child, base_font_size, s); + if (tag) + ds_put_format (s, "", tag); + } + } + else if (node->type == XML_TEXT_NODE) + { + /* U+00A0 NONBREAKING SPACE is really, really common in SPV text and it + makes it impossible to break syntax across lines. Translate it into a + regular space. (Note that U+00A0 is C2 A0 in UTF-8.) + + Do the same for U+2007 FIGURE SPACE, which also crops out weirdly + sometimes. */ + ds_extend (s, ds_length (s) + xmlStrlen (node->content)); + for (const uint8_t *p = node->content; *p; ) + { + int c; + if (p[0] == 0xc2 && p[1] == 0xa0) + { + c = ' '; + p += 2; + } + else if (p[0] == 0xe2 && p[1] == 0x80 && p[2] == 0x87) + { + c = ' '; + p += 3; + } + else + c = *p++; + + if (c_isspace (c)) + { + int last = ds_last (s); + if (last != EOF && !c_isspace (last)) + ds_put_byte (s, c); + } + else if (c == '<') + ds_put_cstr (s, "<"); + else if (c == '>') + ds_put_cstr (s, ">"); + else if (c == '&') + ds_put_cstr (s, "&"); + else + ds_put_byte (s, c); + } + } +} + +static xmlDoc * +parse_embedded_html (const xmlNode *node) +{ + /* Extract HTML from XML node. */ + char *html_s = CHAR_CAST (char *, xmlNodeGetContent (node)); + if (!html_s) + xalloc_die (); + + xmlDoc *html_doc = htmlReadMemory ( + html_s, strlen (html_s), + NULL, "UTF-8", (HTML_PARSE_RECOVER | HTML_PARSE_NOERROR + | HTML_PARSE_NOWARNING | HTML_PARSE_NOBLANKS + | HTML_PARSE_NONET)); + free (html_s); + + return html_doc; +} + +/* Given NODE, which should contain HTML content, returns the text within that + content as an allocated string. The caller must eventually free the + returned string (with xmlFree()). */ +static char * +decode_embedded_html (const xmlNode *node, struct font_style *font_style) +{ + struct string markup = DS_EMPTY_INITIALIZER; + *font_style = (struct font_style) FONT_STYLE_INITIALIZER; + font_style->size = 10; + + xmlDoc *html_doc = parse_embedded_html (node); + if (html_doc) + { + xmlNode *root = xmlDocGetRootElement (html_doc); + xmlNode *head = root ? find_xml_child_element (root, "head") : NULL; + xmlNode *style = head ? find_xml_child_element (head, "style") : NULL; + if (style) + { + uint8_t *style_s = xmlNodeGetContent (style); + spv_parse_css_style (CHAR_CAST (char *, style_s), font_style); + xmlFree (style_s); + } + + if (root) + extract_html_text (root, font_style->size, &markup); + xmlFreeDoc (html_doc); + } + + font_style->markup = true; + return ds_steal_cstr (&markup); +} + +static char * +xstrdup_if_nonempty (const char *s) +{ + return s && s[0] ? xstrdup (s) : NULL; +} + +static void +decode_container_text (const struct spvsx_container_text *ct, + struct spv_item *item) +{ + item->type = SPV_ITEM_TEXT; + item->command_id = xstrdup_if_nonempty (ct->command_name); + + item->text = xzalloc (sizeof *item->text); + item->text->type = PIVOT_VALUE_TEXT; + item->text->font_style = xmalloc (sizeof *item->text->font_style); + item->text->text.local = decode_embedded_html (ct->html->node_.raw, + item->text->font_style); +} + +static void +decode_page_p (const xmlNode *in, struct page_paragraph *out) +{ + char *style = get_xml_attr (in, "style"); + out->halign = (style && strstr (style, "center") ? TABLE_HALIGN_CENTER + : style && strstr (style, "right") ? TABLE_HALIGN_RIGHT + : TABLE_HALIGN_LEFT); + free (style); + + struct font_style font_style; + out->markup = decode_embedded_html (in, &font_style); + font_style_uninit (&font_style); +} + +static void +decode_page_paragraph (const struct spvsx_page_paragraph *page_paragraph, + struct page_heading *ph) +{ + memset (ph, 0, sizeof *ph); + + const struct spvsx_page_paragraph_text *page_paragraph_text + = page_paragraph->page_paragraph_text; + if (!page_paragraph_text) + return; + + xmlDoc *html_doc = parse_embedded_html (page_paragraph_text->node_.raw); + if (!html_doc) + return; + + xmlNode *root = xmlDocGetRootElement (html_doc); + xmlNode *body = find_xml_child_element (root, "body"); + if (body) + for (const xmlNode *node = body->children; node; node = node->next) + if (node->type == XML_ELEMENT_NODE + && !strcmp (CHAR_CAST (const char *, node->name), "p")) + { + ph->paragraphs = xrealloc (ph->paragraphs, + (ph->n + 1) * sizeof *ph->paragraphs); + decode_page_p (node, &ph->paragraphs[ph->n++]); + } + xmlFreeDoc (html_doc); +} + +void +spv_item_load (const struct spv_item *item) +{ + if (spv_item_is_table (item)) + spv_item_get_table (item); +} + +bool +spv_item_is_light_table (const struct spv_item *item) +{ + return item->type == SPV_ITEM_TABLE && !item->xml_member; +} + +char * WARN_UNUSED_RESULT +spv_item_get_raw_light_table (const struct spv_item *item, + void **data, size_t *size) +{ + return zip_member_read_all (item->spv->zip, item->bin_member, data, size); +} + +char * WARN_UNUSED_RESULT +spv_item_get_light_table (const struct spv_item *item, + struct spvlb_table **tablep) +{ + *tablep = NULL; + + if (!spv_item_is_light_table (item)) + return xstrdup ("not a light binary table object"); + + void *data; + size_t size; + char *error = spv_item_get_raw_light_table (item, &data, &size); + if (error) + return error; + + struct spvbin_input input; + spvbin_input_init (&input, data, size); + +#if 0 + struct spvlb_header *header; + if (!spvlb_parse_header (&input, &header)) + return xstrdup("bad header"); + spvlb_print_header ("file", 0, header); + + struct spvlb_titles *titles; + if (!spvlb_parse_titles (&input, &titles)) + return xstrdup("bad titles"); + spvlb_print_titles ("file", 0, titles); + + struct spvlb_footnotes *footnotes; + if (!spvlb_parse_footnotes (&input, &footnotes)) + return xstrdup("bad footnotes"); + spvlb_print_footnotes ("file", 0, footnotes); + + struct spvlb_areas *areas; + if (!spvlb_parse_areas (&input, &areas)) + return xstrdup("bad areas"); + spvlb_print_areas ("file", 0, areas); + + struct spvlb_borders *borders; + if (!spvlb_parse_borders (&input, &borders)) + return xstrdup("bad borders"); + spvlb_print_borders ("file", 0, borders); + + struct spvlb_print_settings *print_settings; + if (!spvlb_parse_print_settings (&input, &print_settings)) + return xstrdup("bad print_settings"); + spvlb_print_print_settings ("file", 0, print_settings); + + struct spvlb_table_settings *table_settings; + if (!spvlb_parse_table_settings (&input, &table_settings)) + return xstrdup("bad table_settings"); + spvlb_print_table_settings ("file", 0, table_settings); + + input.ofs = 0; +#endif + struct spvlb_table *table; + error = (!spvlb_parse_table (&input, &table) + ? spvbin_input_to_error (&input, item->bin_member) + : input.ofs != input.size + ? xasprintf ("%s: expected end of file at offset %#zx", + item->bin_member, input.ofs) + : NULL); + free (data); + if (!error) + *tablep = table; + return error; +} + +static char * +pivot_table_open_light (struct spv_item *item) +{ + assert (spv_item_is_light_table (item)); + + struct spvlb_table *raw_table; + char *error = spv_item_get_light_table (item, &raw_table); + if (!error) + error = decode_spvlb_table (raw_table, &item->table); + spvlb_free_table (raw_table); + + return error; +} + +bool +spv_item_is_legacy_table (const struct spv_item *item) +{ + return item->type == SPV_ITEM_TABLE && item->xml_member; +} + +char * WARN_UNUSED_RESULT +spv_item_get_raw_legacy_data (const struct spv_item *item, + void **data, size_t *size) +{ + if (!spv_item_is_legacy_table (item)) + return xstrdup ("not a legacy table object"); + + return zip_member_read_all (item->spv->zip, item->bin_member, data, size); +} + +char * WARN_UNUSED_RESULT +spv_item_get_legacy_data (const struct spv_item *item, struct spv_data *data) +{ + void *raw; + size_t size; + char *error = spv_item_get_raw_legacy_data (item, &raw, &size); + if (!error) + { + error = spv_legacy_data_decode (raw, size, data); + free (raw); + } + + return error; +} + +static char * WARN_UNUSED_RESULT +spv_read_xml_member (struct spv_reader *spv, const char *member_name, + bool keep_blanks, const char *root_element_name, + xmlDoc **docp) +{ + *docp = NULL; + + struct zip_member *zm = zip_member_open (spv->zip, member_name); + if (!zm) + return ds_steal_cstr (&spv->zip_errs); + + xmlParserCtxt *parser; + xmlKeepBlanksDefault (keep_blanks); + parser = xmlCreatePushParserCtxt(NULL, NULL, NULL, 0, NULL); + if (!parser) + { + zip_member_finish (zm); + return xasprintf (_("%s: Failed to create XML parser"), member_name); + } + + int retval; + char buf[4096]; + while ((retval = zip_member_read (zm, buf, sizeof buf)) > 0) + xmlParseChunk (parser, buf, retval, false); + xmlParseChunk (parser, NULL, 0, true); + + xmlDoc *doc = parser->myDoc; + bool well_formed = parser->wellFormed; + xmlFreeParserCtxt (parser); + + if (retval < 0) + { + char *error = ds_steal_cstr (&spv->zip_errs); + zip_member_finish (zm); + xmlFreeDoc (doc); + return error; + } + zip_member_finish (zm); + + if (!well_formed) + { + xmlFreeDoc (doc); + return xasprintf(_("%s: document is not well-formed"), member_name); + } + + const xmlNode *root_node = xmlDocGetRootElement (doc); + assert (root_node->type == XML_ELEMENT_NODE); + if (strcmp (CHAR_CAST (char *, root_node->name), root_element_name)) + { + xmlFreeDoc (doc); + return xasprintf(_("%s: root node is \"%s\" but \"%s\" was expected"), + member_name, + CHAR_CAST (char *, root_node->name), root_element_name); + } + + *docp = doc; + return NULL; +} + +char * WARN_UNUSED_RESULT +spv_item_get_legacy_table (const struct spv_item *item, xmlDoc **docp) +{ + assert (spv_item_is_legacy_table (item)); + + return spv_read_xml_member (item->spv, item->xml_member, false, + "visualization", docp); +} + +char * WARN_UNUSED_RESULT +spv_item_get_structure (const struct spv_item *item, struct _xmlDoc **docp) +{ + return spv_read_xml_member (item->spv, item->structure_member, false, + "heading", docp); +} + +static char * WARN_UNUSED_RESULT +pivot_table_open_legacy (struct spv_item *item) +{ + assert (spv_item_is_legacy_table (item)); + + struct spv_data data; + char *error = spv_item_get_legacy_data (item, &data); + if (error) + { + char *s = xasprintf ("%s: %s", item->bin_member, error); + free (error); + return s; + } + + xmlDoc *doc; + error = spv_read_xml_member (item->spv, item->xml_member, false, + "visualization", &doc); + if (error) + { + spv_data_uninit (&data); + return error; + } + + struct spvxml_context ctx = SPVXML_CONTEXT_INIT (ctx); + struct spvdx_visualization *v; + spvdx_parse_visualization (&ctx, xmlDocGetRootElement (doc), &v); + error = spvxml_context_finish (&ctx, &v->node_); + + if (!error) + error = decode_spvdx_table (v, item->legacy_properties, &data, + &item->table); + + if (error) + { + char *s = xasprintf ("%s: %s", item->xml_member, error); + free (error); + error = s; + } + + spv_data_uninit (&data); + spvdx_free_visualization (v); + if (doc) + xmlFreeDoc (doc); + + return error; +} + +struct pivot_table * +spv_item_get_table (const struct spv_item *item_) +{ + struct spv_item *item = CONST_CAST (struct spv_item *, item_); + + assert (spv_item_is_table (item)); + if (!item->table) + { + char *error = (item->xml_member + ? pivot_table_open_legacy (item) + : pivot_table_open_light (item)); + if (error) + { + item->error = true; + msg (ME, "%s", error); + item->table = pivot_table_create_for_text ( + pivot_value_new_text (N_("Error")), + pivot_value_new_user_text (error, -1)); + free (error); + } + } + + return item->table; +} + +/* Constructs a new spv_item from XML and stores it in *ITEMP. Returns NULL if + successful, otherwise an error message for the caller to use and free (with + free()). + + XML should be a 'heading' or 'container' element. */ +static char * WARN_UNUSED_RESULT +spv_decode_container (const struct spvsx_container *c, + const char *structure_member, + struct spv_item *parent) +{ + struct spv_item *item = xzalloc (sizeof *item); + item->spv = parent->spv; + item->label = xstrdup (c->label->text); + item->visible = c->visibility == SPVSX_VISIBILITY_VISIBLE; + item->structure_member = xstrdup (structure_member); + + assert (c->n_seq == 1); + struct spvxml_node *content = c->seq[0]; + if (spvsx_is_container_text (content)) + decode_container_text (spvsx_cast_container_text (content), item); + else if (spvsx_is_table (content)) + { + item->type = SPV_ITEM_TABLE; + + struct spvsx_table *table = spvsx_cast_table (content); + const struct spvsx_table_structure *ts = table->table_structure; + item->bin_member = xstrdup (ts->data_path->text); + item->command_id = xstrdup_if_nonempty (table->command_name); + if (ts->path) + { + item->xml_member = ts->path ? xstrdup (ts->path->text) : NULL; + char *error = decode_spvsx_legacy_properties ( + table->table_properties, &item->legacy_properties); + if (error) + { + spv_item_destroy (item); + return error; + } + } + } + else if (spvsx_is_graph (content)) + { + struct spvsx_graph *graph = spvsx_cast_graph (content); + item->type = SPV_ITEM_GRAPH; + item->command_id = xstrdup_if_nonempty (graph->command_name); + /* XXX */ + } + else if (spvsx_is_model (content)) + { + struct spvsx_model *model = spvsx_cast_model (content); + item->type = SPV_ITEM_MODEL; + item->command_id = xstrdup_if_nonempty (model->command_name); + /* XXX */ + } + else if (spvsx_is_object (content)) + { + struct spvsx_object *object = spvsx_cast_object (content); + item->type = SPV_ITEM_OBJECT; + item->object_type = xstrdup (object->type); + item->uri = xstrdup (object->uri); + } + else if (spvsx_is_image (content)) + { + struct spvsx_image *image = spvsx_cast_image (content); + item->type = SPV_ITEM_OBJECT; + item->object_type = xstrdup ("image"); + item->uri = xstrdup (image->data_path->text); + } + else + NOT_REACHED (); + + spv_heading_add_child (parent, item); + return NULL; +} + +static char * WARN_UNUSED_RESULT +spv_decode_children (struct spv_reader *spv, const char *structure_member, + struct spvxml_node **seq, size_t n_seq, + struct spv_item *parent) +{ + for (size_t i = 0; i < n_seq; i++) + { + const struct spvxml_node *node = seq[i]; + + char *error; + if (spvsx_is_container (node)) + { + const struct spvsx_container *container + = spvsx_cast_container (node); + error = spv_decode_container (container, structure_member, parent); + } + else if (spvsx_is_heading (node)) + { + const struct spvsx_heading *subheading = spvsx_cast_heading (node); + struct spv_item *subitem = xzalloc (sizeof *subitem); + subitem->structure_member = xstrdup (structure_member); + subitem->spv = parent->spv; + subitem->type = SPV_ITEM_HEADING; + subitem->label = xstrdup (subheading->label->text); + if (subheading->command_name) + subitem->command_id = xstrdup (subheading->command_name); + subitem->visible = !subheading->heading_visibility_present; + spv_heading_add_child (parent, subitem); + + error = spv_decode_children (spv, structure_member, + subheading->seq, subheading->n_seq, + subitem); + } + else + NOT_REACHED (); + + if (error) + return error; + } + + return NULL; +} + +static struct page_setup * +decode_page_setup (const struct spvsx_page_setup *in, const char *file_name) +{ + struct page_setup *out = xmalloc (sizeof *out); + *out = (struct page_setup) PAGE_SETUP_INITIALIZER; + + out->initial_page_number = in->initial_page_number; + + if (in->paper_width != DBL_MAX) + out->paper[TABLE_HORZ] = in->paper_width; + if (in->paper_height != DBL_MAX) + out->paper[TABLE_VERT] = in->paper_height; + + if (in->margin_left != DBL_MAX) + out->margins[TABLE_HORZ][0] = in->margin_left; + if (in->margin_right != DBL_MAX) + out->margins[TABLE_HORZ][1] = in->margin_right; + if (in->margin_top != DBL_MAX) + out->margins[TABLE_VERT][0] = in->margin_top; + if (in->margin_bottom != DBL_MAX) + out->margins[TABLE_VERT][1] = in->margin_bottom; + + if (in->space_after != DBL_MAX) + out->object_spacing = in->space_after; + + if (in->chart_size) + out->chart_size = (in->chart_size == SPVSX_CHART_SIZE_FULL_HEIGHT + ? PAGE_CHART_FULL_HEIGHT + : in->chart_size == SPVSX_CHART_SIZE_HALF_HEIGHT + ? PAGE_CHART_HALF_HEIGHT + : in->chart_size == SPVSX_CHART_SIZE_QUARTER_HEIGHT + ? PAGE_CHART_QUARTER_HEIGHT + : PAGE_CHART_AS_IS); + + decode_page_paragraph (in->page_header->page_paragraph, &out->headings[0]); + decode_page_paragraph (in->page_footer->page_paragraph, &out->headings[1]); + + out->file_name = xstrdup (file_name); + + return out; +} + +static char * WARN_UNUSED_RESULT +spv_heading_read (struct spv_reader *spv, + const char *file_name, const char *member_name) +{ + xmlDoc *doc; + char *error = spv_read_xml_member (spv, member_name, true, "heading", &doc); + if (error) + return error; + + struct spvxml_context ctx = SPVXML_CONTEXT_INIT (ctx); + struct spvsx_root_heading *root; + spvsx_parse_root_heading (&ctx, xmlDocGetRootElement (doc), &root); + error = spvxml_context_finish (&ctx, &root->node_); + + if (!error && root->page_setup) + spv->page_setup = decode_page_setup (root->page_setup, file_name); + + for (size_t i = 0; !error && i < root->n_seq; i++) + error = spv_decode_children (spv, member_name, root->seq, root->n_seq, + spv->root); + + if (error) + { + char *s = xasprintf ("%s: %s", member_name, error); + free (error); + error = s; + } + + spvsx_free_root_heading (root); + xmlFreeDoc (doc); + + return error; +} + +struct spv_item * +spv_get_root (const struct spv_reader *spv) +{ + return spv->root; +} + +static int +spv_detect__ (struct zip_reader *zip, char **errorp) +{ + *errorp = NULL; + + const char *member = "META-INF/MANIFEST.MF"; + if (!zip_reader_contains_member (zip, member)) + return 0; + + void *data; + size_t size; + *errorp = zip_member_read_all (zip, "META-INF/MANIFEST.MF", + &data, &size); + if (*errorp) + return -1; + + const char *magic = "allowPivoting=true"; + bool is_spv = size == strlen (magic) && !memcmp (magic, data, size); + free (data); + + return is_spv; +} + +/* Returns NULL if FILENAME is an SPV file, otherwise an error string that the + caller must eventually free(). */ +char * WARN_UNUSED_RESULT +spv_detect (const char *filename) +{ + struct string zip_error; + struct zip_reader *zip = zip_reader_create (filename, &zip_error); + if (!zip) + return ds_steal_cstr (&zip_error); + + char *error; + if (spv_detect__ (zip, &error) <= 0 && !error) + error = xasprintf("%s: not an SPV file", filename); + zip_reader_destroy (zip); + ds_destroy (&zip_error); + return error; +} + + +char * WARN_UNUSED_RESULT +spv_open (const char *filename, struct spv_reader **spvp) +{ + *spvp = NULL; + + struct spv_reader *spv = xzalloc (sizeof *spv); + ds_init_empty (&spv->zip_errs); + spv->zip = zip_reader_create (filename, &spv->zip_errs); + if (!spv->zip) + { + char *error = ds_steal_cstr (&spv->zip_errs); + spv_close (spv); + return error; + } + + char *error; + int detect = spv_detect__ (spv->zip, &error); + if (detect <= 0) + { + spv_close (spv); + return error ? error : xasprintf("%s: not an SPV file", filename); + } + + spv->root = xzalloc (sizeof *spv->root); + spv->root->spv = spv; + spv->root->type = SPV_ITEM_HEADING; + for (size_t i = 0; ; i++) + { + const char *member_name = zip_reader_get_member_name (spv->zip, i); + if (!member_name) + break; + + struct substring member_name_ss = ss_cstr (member_name); + if (ss_starts_with (member_name_ss, ss_cstr ("outputViewer")) + && ss_ends_with (member_name_ss, ss_cstr (".xml"))) + { + char *error = spv_heading_read (spv, filename, member_name); + if (error) + { + spv_close (spv); + return error; + } + } + } + + *spvp = spv; + return NULL; +} + +void +spv_close (struct spv_reader *spv) +{ + if (spv) + { + ds_destroy (&spv->zip_errs); + zip_reader_destroy (spv->zip); + spv_item_destroy (spv->root); + page_setup_destroy (spv->page_setup); + free (spv); + } +} + +struct fmt_spec +spv_decode_fmt_spec (uint32_t u32) +{ + if (!u32 + || (u32 == 0x10000 || u32 == 1 /* both used as string formats */)) + return fmt_for_output (FMT_F, 40, 2); + + uint8_t raw_type = u32 >> 16; + uint8_t w = u32 >> 8; + uint8_t d = u32; + + msg_disable (); + struct fmt_spec spec = { .type = FMT_F, .w = w, .d = d }; + bool ok = raw_type >= 40 || fmt_from_io (raw_type, &spec.type); + if (ok) + { + fmt_fix_output (&spec); + ok = fmt_check_width_compat (&spec, 0); + } + msg_enable (); + + if (!ok) + { + fprintf (stderr, "bad format %#"PRIx32"\n", u32); /* XXX */ + spec = fmt_for_output (FMT_F, 40, 2); + exit (1); + } + + return spec; +}