pivot-table: Reduce size of struct pivot_value from 80 bytes to 40.
[pspp] / src / output / spv / spv.c
1 /* PSPP - a program for statistical analysis.
2    Copyright (C) 2017, 2018 Free Software Foundation, Inc.
3
4    This program is free software: you can redistribute it and/or modify
5    it under the terms of the GNU General Public License as published by
6    the Free Software Foundation, either version 3 of the License, or
7    (at your option) any later version.
8
9    This program is distributed in the hope that it will be useful,
10    but WITHOUT ANY WARRANTY; without even the implied warranty of
11    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12    GNU General Public License for more details.
13
14    You should have received a copy of the GNU General Public License
15    along with this program.  If not, see <http://www.gnu.org/licenses/>. */
16
17 #include <config.h>
18
19 #include "output/spv/spv.h"
20
21 #include <assert.h>
22 #include <cairo.h>
23 #include <inttypes.h>
24 #include <libxml/HTMLparser.h>
25 #include <libxml/xmlreader.h>
26 #include <stdarg.h>
27 #include <stdlib.h>
28
29 #include "libpspp/assertion.h"
30 #include "libpspp/cast.h"
31 #include "libpspp/hash-functions.h"
32 #include "libpspp/message.h"
33 #include "libpspp/str.h"
34 #include "libpspp/zip-reader.h"
35 #include "output/output-item.h"
36 #include "output/page-setup.h"
37 #include "output/pivot-table.h"
38 #include "output/spv/detail-xml-parser.h"
39 #include "output/spv/light-binary-parser.h"
40 #include "output/spv/spv-css-parser.h"
41 #include "output/spv/spv-legacy-data.h"
42 #include "output/spv/spv-legacy-decoder.h"
43 #include "output/spv/spv-light-decoder.h"
44 #include "output/spv/spv-table-look.h"
45 #include "output/spv/structure-xml-parser.h"
46
47 #include "gl/c-ctype.h"
48 #include "gl/intprops.h"
49 #include "gl/minmax.h"
50 #include "gl/xalloc.h"
51 #include "gl/xvasprintf.h"
52 #include "gl/xsize.h"
53
54 #include "gettext.h"
55 #define _(msgid) gettext (msgid)
56 #define N_(msgid) (msgid)
57
58 struct spv_reader
59   {
60     struct zip_reader *zip;
61     struct spv_item *root;
62     struct page_setup *page_setup;
63   };
64
65 static xmlNode *
66 find_xml_child_element (xmlNode *parent, const char *child_name)
67 {
68   for (xmlNode *node = parent->children; node; node = node->next)
69     if (node->type == XML_ELEMENT_NODE
70         && node->name
71         && !strcmp (CHAR_CAST (char *, node->name), child_name))
72       return node;
73
74   return NULL;
75 }
76
77 static char *
78 get_xml_attr (const xmlNode *node, const char *name)
79 {
80   return CHAR_CAST (char *, xmlGetProp (node, CHAR_CAST (xmlChar *, name)));
81 }
82
83 static void
84 put_xml_attr (const char *name, const char *value, struct string *dst)
85 {
86   if (!value)
87     return;
88
89   ds_put_format (dst, " %s=\"", name);
90   for (const char *p = value; *p; p++)
91     {
92       switch (*p)
93         {
94         case '\n':
95           ds_put_cstr (dst, "&#10;");
96           break;
97         case '&':
98           ds_put_cstr (dst, "&amp;");
99           break;
100         case '<':
101           ds_put_cstr (dst, "&lt;");
102           break;
103         case '>':
104           ds_put_cstr (dst, "&gt;");
105           break;
106         case '"':
107           ds_put_cstr (dst, "&quot;");
108           break;
109         default:
110           ds_put_byte (dst, *p);
111           break;
112         }
113     }
114   ds_put_byte (dst, '"');
115 }
116
117 static void
118 extract_html_text (const xmlNode *node, int base_font_size, struct string *s)
119 {
120   if (node->type == XML_ELEMENT_NODE)
121     {
122       const char *name = CHAR_CAST (char *, node->name);
123       if (!strcmp (name, "br"))
124         ds_put_byte (s, '\n');
125       else if (strcmp (name, "style"))
126         {
127           const char *tag = NULL;
128           if (strchr ("biu", name[0]) && name[1] == '\0')
129             {
130               tag = name;
131               ds_put_format (s, "<%s>", tag);
132             }
133           else if (!strcmp (name, "font"))
134             {
135               tag = "span";
136               ds_put_format (s, "<%s", tag);
137
138               char *face = get_xml_attr (node, "face");
139               put_xml_attr ("face", face, s);
140               free (face);
141
142               char *color = get_xml_attr (node, "color");
143               if (color)
144                 {
145                   if (color[0] == '#')
146                     put_xml_attr ("color", color, s);
147                   else
148                     {
149                       uint8_t r, g, b;
150                       if (sscanf (color, "rgb (%"SCNu8", %"SCNu8", %"SCNu8")",
151                                   &r, &g, &b) == 3)
152                         {
153                           char color2[8];
154                           snprintf (color2, sizeof color2,
155                                     "#%02"PRIx8"%02"PRIx8"%02"PRIx8,
156                                     r, g, b);
157                           put_xml_attr ("color", color2, s);
158                         }
159                     }
160                 }
161               free (color);
162
163               char *size_s = get_xml_attr (node, "size");
164               int html_size = size_s ? atoi (size_s) : 0;
165               free (size_s);
166               if (html_size >= 1 && html_size <= 7)
167                 {
168                   static const double scale[7] = {
169                     .444, .556, .667, .778, 1.0, 1.33, 2.0
170                   };
171                   double size = base_font_size * scale[html_size - 1];
172
173                   char size2[INT_BUFSIZE_BOUND (int)];
174                   snprintf (size2, sizeof size2, "%.0f", size * 1024.);
175                   put_xml_attr ("size", size2, s);
176                 }
177
178               ds_put_cstr (s, ">");
179             }
180           for (const xmlNode *child = node->children; child;
181                child = child->next)
182             extract_html_text (child, base_font_size, s);
183           if (tag)
184             ds_put_format (s, "</%s>", tag);
185         }
186     }
187   else if (node->type == XML_TEXT_NODE)
188     {
189       /* U+00A0 NONBREAKING SPACE is really, really common in SPV text and it
190          makes it impossible to break syntax across lines.  Translate it into a
191          regular space.  (Note that U+00A0 is C2 A0 in UTF-8.)
192
193          Do the same for U+2007 FIGURE SPACE, which also crops out weirdly
194          sometimes. */
195       ds_extend (s, ds_length (s) + xmlStrlen (node->content));
196       for (const uint8_t *p = node->content; *p;)
197         {
198           int c;
199           if (p[0] == 0xc2 && p[1] == 0xa0)
200             {
201               c = ' ';
202               p += 2;
203             }
204           else if (p[0] == 0xe2 && p[1] == 0x80 && p[2] == 0x87)
205             {
206               c = ' ';
207               p += 3;
208             }
209           else
210             c = *p++;
211
212           if (c_isspace (c))
213             {
214               int last = ds_last (s);
215               if (last != EOF && !c_isspace (last))
216                 ds_put_byte (s, c);
217             }
218           else if (c == '<')
219             ds_put_cstr (s, "&lt;");
220           else if (c == '>')
221             ds_put_cstr (s, "&gt;");
222           else if (c == '&')
223             ds_put_cstr (s, "&amp;");
224           else
225             ds_put_byte (s, c);
226         }
227     }
228 }
229
230 static xmlDoc *
231 parse_embedded_html (const xmlNode *node)
232 {
233   /* Extract HTML from XML node. */
234   char *html_s = CHAR_CAST (char *, xmlNodeGetContent (node));
235   if (!html_s)
236     xalloc_die ();
237
238   xmlDoc *html_doc = htmlReadMemory (
239     html_s, strlen (html_s),
240     NULL, "UTF-8", (HTML_PARSE_RECOVER | HTML_PARSE_NOERROR
241                     | HTML_PARSE_NOWARNING | HTML_PARSE_NOBLANKS
242                     | HTML_PARSE_NONET));
243   free (html_s);
244
245   return html_doc;
246 }
247
248 /* Given NODE, which should contain HTML content, returns the text within that
249    content as an allocated string.  The caller must eventually free the
250    returned string (with xmlFree()). */
251 static char *
252 decode_embedded_html (const xmlNode *node, struct font_style *font_style)
253 {
254   struct string markup = DS_EMPTY_INITIALIZER;
255   *font_style = (struct font_style) FONT_STYLE_INITIALIZER;
256   font_style->size = 10;
257
258   xmlDoc *html_doc = parse_embedded_html (node);
259   if (html_doc)
260     {
261       xmlNode *root = xmlDocGetRootElement (html_doc);
262       xmlNode *head = root ? find_xml_child_element (root, "head") : NULL;
263       xmlNode *style = head ? find_xml_child_element (head, "style") : NULL;
264       if (style)
265         {
266           uint8_t *style_s = xmlNodeGetContent (style);
267           spv_parse_css_style (CHAR_CAST (char *, style_s), font_style);
268           xmlFree (style_s);
269         }
270
271       if (root)
272         extract_html_text (root, font_style->size, &markup);
273       xmlFreeDoc (html_doc);
274     }
275
276   font_style->markup = true;
277   return ds_steal_cstr (&markup);
278 }
279
280 static struct output_item *
281 decode_container_text (const struct spvsx_container_text *ct)
282 {
283   struct font_style *font_style = xmalloc (sizeof *font_style);
284   char *text = decode_embedded_html (ct->html->node_.raw, font_style);
285
286   struct pivot_value *value = xmalloc (sizeof *value);
287   *value = (struct pivot_value) {
288     .text = {
289       .type = PIVOT_VALUE_TEXT,
290       .local = text,
291       .c = text,
292       .id = text,
293       .user_provided = true,
294     },
295   };
296   pivot_value_ex_rw (value)->font_style = font_style;
297
298   struct output_item *item = text_item_create_value (TEXT_ITEM_LOG,
299                                                      value, NULL);
300   output_item_set_command_name (item, ct->command_name);
301   return item;
302 }
303
304 static void
305 decode_page_p (const xmlNode *in, struct page_paragraph *out)
306 {
307   char *style = get_xml_attr (in, "style");
308   out->halign = (style && strstr (style, "center") ? TABLE_HALIGN_CENTER
309                  : style && strstr (style, "right") ? TABLE_HALIGN_RIGHT
310                  : TABLE_HALIGN_LEFT);
311   free (style);
312
313   struct font_style font_style;
314   out->markup = decode_embedded_html (in, &font_style);
315   font_style_uninit (&font_style);
316 }
317
318 static void
319 decode_page_paragraph (const struct spvsx_page_paragraph *page_paragraph,
320                        struct page_heading *ph)
321 {
322   memset (ph, 0, sizeof *ph);
323
324   const struct spvsx_page_paragraph_text *page_paragraph_text
325     = page_paragraph->page_paragraph_text;
326   if (!page_paragraph_text)
327     return;
328
329   xmlDoc *html_doc = parse_embedded_html (page_paragraph_text->node_.raw);
330   if (!html_doc)
331     return;
332
333   xmlNode *root = xmlDocGetRootElement (html_doc);
334   xmlNode *body = find_xml_child_element (root, "body");
335   if (body)
336     for (const xmlNode *node = body->children; node; node = node->next)
337       if (node->type == XML_ELEMENT_NODE
338           && !strcmp (CHAR_CAST (const char *, node->name), "p"))
339         {
340           ph->paragraphs = xrealloc (ph->paragraphs,
341                                      (ph->n + 1) * sizeof *ph->paragraphs);
342           decode_page_p (node, &ph->paragraphs[ph->n++]);
343         }
344   xmlFreeDoc (html_doc);
345 }
346
347 char * WARN_UNUSED_RESULT
348 spv_read_light_table (struct zip_reader *zip, const char *bin_member,
349                       struct spvlb_table **tablep)
350 {
351   *tablep = NULL;
352
353   void *data;
354   size_t size;
355   char *error = zip_member_read_all (zip, bin_member, &data, &size);
356   if (error)
357     return error;
358
359   struct spvbin_input input;
360   spvbin_input_init (&input, data, size);
361
362   struct spvlb_table *table = NULL;
363   error = (!size
364            ? xasprintf ("light table member is empty")
365            : !spvlb_parse_table (&input, &table)
366            ? spvbin_input_to_error (&input, NULL)
367            : input.ofs != input.size
368            ? xasprintf ("expected end of file at offset %#zx", input.ofs)
369            : NULL);
370   free (data);
371   if (!error)
372     *tablep = table;
373   return error;
374 }
375
376 static char * WARN_UNUSED_RESULT
377 pivot_table_open_light (struct zip_reader *zip, const char *bin_member,
378                         struct pivot_table **tablep)
379 {
380   *tablep = NULL;
381
382   struct spvlb_table *raw_table;
383   char *error = spv_read_light_table (zip, bin_member, &raw_table);
384   if (!error)
385     error = decode_spvlb_table (raw_table, tablep);
386   spvlb_free_table (raw_table);
387
388   return error;
389 }
390
391 char * WARN_UNUSED_RESULT
392 spv_read_legacy_data (struct zip_reader *zip, const char *bin_member,
393                       struct spv_data *data)
394 {
395   void *raw;
396   size_t size;
397   char *error = zip_member_read_all (zip, bin_member, &raw, &size);
398   if (!error)
399     {
400       error = spv_legacy_data_decode (raw, size, data);
401       free (raw);
402     }
403
404   return error;
405 }
406
407 char * WARN_UNUSED_RESULT
408 spv_read_xml_member (struct zip_reader *zip, const char *xml_member,
409                      bool keep_blanks, const char *root_element_name,
410                      xmlDoc **docp)
411 {
412   *docp = NULL;
413
414   struct zip_member *zm;
415   char *error = zip_member_open (zip, xml_member, &zm);
416   if (error)
417     return error;
418
419   xmlParserCtxt *parser;
420   xmlKeepBlanksDefault (keep_blanks);
421   parser = xmlCreatePushParserCtxt(NULL, NULL, NULL, 0, NULL);
422   if (!parser)
423     {
424       zip_member_finish (zm);
425       return xasprintf (_("%s: Failed to create XML parser"), xml_member);
426     }
427
428   int retval;
429   char buf[4096];
430   while ((retval = zip_member_read (zm, buf, sizeof buf)) > 0)
431     xmlParseChunk (parser, buf, retval, false);
432   xmlParseChunk (parser, NULL, 0, true);
433
434   xmlDoc *doc = parser->myDoc;
435   bool well_formed = parser->wellFormed;
436   xmlFreeParserCtxt (parser);
437
438   if (retval < 0)
439     {
440       char *error = zip_member_steal_error (zm);
441       zip_member_finish (zm);
442       xmlFreeDoc (doc);
443       return error;
444     }
445   zip_member_finish (zm);
446
447   if (!well_formed)
448     {
449       xmlFreeDoc (doc);
450       return xasprintf(_("%s: document is not well-formed"), xml_member);
451     }
452
453   const xmlNode *root_node = xmlDocGetRootElement (doc);
454   assert (root_node->type == XML_ELEMENT_NODE);
455   if (strcmp (CHAR_CAST (char *, root_node->name), root_element_name))
456     {
457       xmlFreeDoc (doc);
458       return xasprintf(_("%s: root node is \"%s\" but \"%s\" was expected"),
459                        xml_member,
460                        CHAR_CAST (char *, root_node->name), root_element_name);
461     }
462
463   *docp = doc;
464   return NULL;
465 }
466
467 static char * WARN_UNUSED_RESULT
468 pivot_table_open_legacy (struct zip_reader *zip, const char *bin_member,
469                          const char *xml_member, const char *subtype,
470                          const struct pivot_table_look *look,
471                          struct pivot_table **tablep)
472 {
473   *tablep = NULL;
474
475   struct spv_data data = SPV_DATA_INITIALIZER;
476   char *error = spv_read_legacy_data (zip, bin_member, &data);
477   if (error)
478     goto exit;
479
480   xmlDoc *doc;
481   error = spv_read_xml_member (zip, xml_member, false,
482                                "visualization", &doc);
483   if (error)
484     goto exit_free_data;
485
486   struct spvxml_context ctx = SPVXML_CONTEXT_INIT (ctx);
487   struct spvdx_visualization *v;
488   spvdx_parse_visualization (&ctx, xmlDocGetRootElement (doc), &v);
489   error = spvxml_context_finish (&ctx, &v->node_);
490   if (error)
491     goto exit_free_doc;
492
493   error = decode_spvdx_table (v, subtype, look, &data, tablep);
494
495   spvdx_free_visualization (v);
496 exit_free_doc:
497   if (doc)
498     xmlFreeDoc (doc);
499 exit_free_data:
500   spv_data_uninit (&data);
501 exit:
502   return error;
503 }
504
505 static struct output_item *
506 spv_read_table_item (struct zip_reader *zip,
507                      const struct spvsx_table *table)
508 {
509   const struct spvsx_table_structure *ts = table->table_structure;
510   const char *bin_member = ts->data_path->text;
511   const char *xml_member = ts->path ? ts->path->text : NULL;
512
513   struct pivot_table *pt = NULL;
514   char *error;
515   if (xml_member)
516     {
517       struct pivot_table_look *look;
518       error = (table->table_properties
519                ? spv_table_look_decode (table->table_properties, &look)
520                : xstrdup ("Legacy table lacks tableProperties"));
521       if (!error)
522         {
523           error = pivot_table_open_legacy (zip, bin_member, xml_member,
524                                            table->sub_type, look, &pt);
525           pivot_table_look_unref (look);
526         }
527     }
528   else
529     error = pivot_table_open_light (zip, bin_member, &pt);
530   if (error)
531     pt = pivot_table_create_for_text (
532       pivot_value_new_text (N_("Error")),
533       pivot_value_new_user_text_nocopy (error));
534
535   struct output_item *item = table_item_create (pt);
536   output_item_set_command_name (item, table->command_name);
537   output_item_add_spv_info (item);
538   item->spv_info->error = error != NULL;
539   item->spv_info->zip_reader = zip_reader_ref (zip);
540   item->spv_info->bin_member = xstrdup (bin_member);
541   item->spv_info->xml_member = xstrdup_if_nonnull (xml_member);
542   return item;
543 }
544
545 static cairo_status_t
546 read_from_zip_member (void *zm_, unsigned char *data, unsigned int length)
547 {
548   struct zip_member *zm = zm_;
549   if (!zm)
550     return CAIRO_STATUS_READ_ERROR;
551
552   while (length > 0)
553     {
554       int n = zip_member_read (zm, data, length);
555       if (n <= 0)
556         return CAIRO_STATUS_READ_ERROR;
557
558       data += n;
559       length -= n;
560     }
561
562   return CAIRO_STATUS_SUCCESS;
563 }
564
565 static char * WARN_UNUSED_RESULT
566 spv_read_image (struct zip_reader *zip, const char *png_member,
567                 const char *command_name, struct output_item **itemp)
568 {
569   struct zip_member *zm;
570   char *error = zip_member_open (zip, png_member, &zm);
571   if (error)
572     return error;
573
574   cairo_surface_t *surface = cairo_image_surface_create_from_png_stream (
575     read_from_zip_member, zm);
576   if (zm)
577     zip_member_finish (zm);
578
579   if (cairo_surface_status (surface) != CAIRO_STATUS_SUCCESS)
580     return xstrdup ("reading image failed");
581
582   struct output_item *item = image_item_create (surface);
583   output_item_set_command_name (item, command_name);
584   output_item_add_spv_info (item);
585   item->spv_info->zip_reader = zip_reader_ref (zip);
586   item->spv_info->png_member = xstrdup (png_member);
587   *itemp = item;
588   return NULL;
589 }
590
591 static struct output_item *
592 error_item_create (char *s)
593 {
594   struct output_item *item = text_item_create_nocopy (TEXT_ITEM_LOG, s,
595                                                       xstrdup ("Error"));
596   output_item_add_spv_info (item);
597   item->spv_info->error = true;
598   return item;
599 }
600
601 static struct output_item *
602 spv_decode_container (struct zip_reader *zip,
603                       const struct spvsx_container *c)
604 {
605   assert (c->n_seq == 1);
606   struct spvxml_node *content = c->seq[0];
607
608   struct output_item *item = NULL;
609   char *error;
610   if (spvsx_is_container_text (content))
611     {
612       item = decode_container_text (spvsx_cast_container_text (content));
613       error = NULL;
614     }
615   else if (spvsx_is_table (content))
616     {
617       item = spv_read_table_item (zip, spvsx_cast_table (content));
618       error = NULL;
619     }
620   else if (spvsx_is_object (content))
621     {
622       struct spvsx_object *object = spvsx_cast_object (content);
623       error = spv_read_image (zip, object->uri, object->command_name, &item);
624     }
625   else if (spvsx_is_image (content))
626     {
627       struct spvsx_image *image = spvsx_cast_image (content);
628       error = spv_read_image (zip, image->data_path->text, image->command_name,
629                               &item);
630     }
631   else if (spvsx_is_graph (content))
632     error = xstrdup ("graphs not yet implemented");
633   else if (spvsx_is_model (content))
634     error = xstrdup ("models not yet implemented");
635   else if (spvsx_is_tree (content))
636     error = xstrdup ("trees not yet implemented");
637   else
638     NOT_REACHED ();
639
640   if (error)
641     item = error_item_create (error);
642   else
643     output_item_set_label (item, c->label->text);
644   item->show = c->visibility == SPVSX_VISIBILITY_VISIBLE;
645
646   return item;
647 }
648
649 static void
650 set_structure_member (struct output_item *item, struct zip_reader *zip,
651                       const char *structure_member)
652 {
653   if (structure_member)
654     {
655       output_item_add_spv_info (item);
656       if (!item->spv_info->zip_reader)
657         item->spv_info->zip_reader = zip_reader_ref (zip);
658       if (!item->spv_info->structure_member)
659         item->spv_info->structure_member = xstrdup (structure_member);
660     }
661 }
662
663 static void
664 spv_decode_children (struct zip_reader *zip, const char *structure_member,
665                      struct spvxml_node **seq, size_t n_seq,
666                      struct output_item *parent)
667 {
668   for (size_t i = 0; i < n_seq; i++)
669     {
670       const struct spvxml_node *node = seq[i];
671
672       struct output_item *child;
673       if (spvsx_is_container (node))
674         {
675           const struct spvsx_container *container
676             = spvsx_cast_container (node);
677
678           if (container->page_break_before_present)
679             group_item_add_child (parent, page_break_item_create ());
680
681           child = spv_decode_container (zip, container);
682         }
683       else if (spvsx_is_heading (node))
684         {
685           const struct spvsx_heading *subheading = spvsx_cast_heading (node);
686
687           child = group_item_create (subheading->command_name,
688                                      subheading->label->text);
689           child->show = !subheading->heading_visibility_present;
690
691           /* Pass NULL for 'structure_member' so that only top-level items get
692              tagged that way.  Lower-level items are always in the same
693              structure member as their parent anyway. */
694            spv_decode_children (zip, NULL, subheading->seq,
695                                 subheading->n_seq, child);
696         }
697       else
698         NOT_REACHED ();
699
700       set_structure_member (child, zip, structure_member);
701       group_item_add_child (parent, child);
702     }
703 }
704
705 static struct page_setup *
706 decode_page_setup (const struct spvsx_page_setup *in, const char *file_name)
707 {
708   struct page_setup *out = xmalloc (sizeof *out);
709   *out = (struct page_setup) PAGE_SETUP_INITIALIZER;
710
711   out->initial_page_number = in->initial_page_number;
712
713   if (in->paper_width != DBL_MAX)
714     out->paper[TABLE_HORZ] = in->paper_width;
715   if (in->paper_height != DBL_MAX)
716     out->paper[TABLE_VERT] = in->paper_height;
717
718   if (in->margin_left != DBL_MAX)
719     out->margins[TABLE_HORZ][0] = in->margin_left;
720   if (in->margin_right != DBL_MAX)
721     out->margins[TABLE_HORZ][1] = in->margin_right;
722   if (in->margin_top != DBL_MAX)
723     out->margins[TABLE_VERT][0] = in->margin_top;
724   if (in->margin_bottom != DBL_MAX)
725     out->margins[TABLE_VERT][1] = in->margin_bottom;
726
727   if (in->space_after != DBL_MAX)
728     out->object_spacing = in->space_after;
729
730   if (in->chart_size)
731     out->chart_size = (in->chart_size == SPVSX_CHART_SIZE_FULL_HEIGHT
732                        ? PAGE_CHART_FULL_HEIGHT
733                        : in->chart_size == SPVSX_CHART_SIZE_HALF_HEIGHT
734                        ? PAGE_CHART_HALF_HEIGHT
735                        : in->chart_size == SPVSX_CHART_SIZE_QUARTER_HEIGHT
736                        ? PAGE_CHART_QUARTER_HEIGHT
737                        : PAGE_CHART_AS_IS);
738
739   decode_page_paragraph (in->page_header->page_paragraph, &out->headings[0]);
740   decode_page_paragraph (in->page_footer->page_paragraph, &out->headings[1]);
741
742   out->file_name = xstrdup (file_name);
743
744   return out;
745 }
746
747 static void
748 spv_add_error_heading (struct output_item *root_item,
749                        struct zip_reader *zip, const char *structure_member,
750                        char *error)
751 {
752   struct output_item *item = error_item_create (
753     xasprintf ("%s: %s", structure_member, error));
754   free (error);
755   set_structure_member (item, zip, structure_member);
756   group_item_add_child (root_item, item);
757 }
758
759 static void
760 spv_heading_read (struct zip_reader *zip, struct output_item *root_item,
761                   struct page_setup **psp, const char *file_name,
762                   const char *structure_member)
763 {
764   xmlDoc *doc;
765   char *error = spv_read_xml_member (zip, structure_member, true,
766                                      "heading", &doc);
767   if (error)
768     {
769       spv_add_error_heading (root_item, zip, structure_member, error);
770       return;
771     }
772
773   struct spvxml_context ctx = SPVXML_CONTEXT_INIT (ctx);
774   struct spvsx_root_heading *root;
775   spvsx_parse_root_heading (&ctx, xmlDocGetRootElement (doc), &root);
776   error = spvxml_context_finish (&ctx, &root->node_);
777   if (error)
778     {
779       xmlFreeDoc (doc);
780       spv_add_error_heading (root_item, zip, structure_member, error);
781       return;
782     }
783
784   if (root->page_setup && psp && !*psp)
785     *psp = decode_page_setup (root->page_setup, file_name);
786
787   for (size_t i = 0; i < root->n_seq; i++)
788     spv_decode_children (zip, structure_member, root->seq, root->n_seq,
789                          root_item);
790
791   spvsx_free_root_heading (root);
792   xmlFreeDoc (doc);
793 }
794
795 static int
796 spv_detect__ (struct zip_reader *zip, char **errorp)
797 {
798   *errorp = NULL;
799
800   const char *member = "META-INF/MANIFEST.MF";
801   if (!zip_reader_contains_member (zip, member))
802     return 0;
803
804   void *data;
805   size_t size;
806   *errorp = zip_member_read_all (zip, "META-INF/MANIFEST.MF",
807                                  &data, &size);
808   if (*errorp)
809     return -1;
810
811   const char *magic = "allowPivoting=true";
812   bool is_spv = size == strlen (magic) && !memcmp (magic, data, size);
813   free (data);
814
815   return is_spv;
816 }
817
818 /* Returns NULL if FILENAME is an SPV file, otherwise an error string that the
819    caller must eventually free(). */
820 char * WARN_UNUSED_RESULT
821 spv_detect (const char *filename)
822 {
823   struct zip_reader *zip;
824   char *error = zip_reader_create (filename, &zip);
825   if (error)
826     return error;
827
828   if (spv_detect__ (zip, &error) <= 0 && !error)
829     error = xasprintf("%s: not an SPV file", filename);
830   zip_reader_unref (zip);
831   return error;
832 }
833
834 char * WARN_UNUSED_RESULT
835 spv_read (const char *filename, struct output_item **outp,
836           struct page_setup **psp)
837 {
838   *outp = NULL;
839   if (psp)
840     *psp = NULL;
841
842   struct spv_reader *spv = xzalloc (sizeof *spv);
843   struct zip_reader *zip;
844   char *error = zip_reader_create (filename, &zip);
845   if (error)
846     return error;
847
848   int detect = spv_detect__ (zip, &error);
849   if (detect <= 0)
850     {
851       zip_reader_unref (zip);
852       return error ? error : xasprintf ("%s: not an SPV file", filename);
853     }
854
855   *outp = root_item_create ();
856   for (size_t i = 0; ; i++)
857     {
858       const char *structure_member = zip_reader_get_member_name (zip, i);
859       if (!structure_member)
860         break;
861
862       struct substring structure_member_ss = ss_cstr (structure_member);
863       if (ss_starts_with (structure_member_ss, ss_cstr ("outputViewer"))
864           && ss_ends_with (structure_member_ss, ss_cstr (".xml")))
865         spv_heading_read (zip, *outp, psp, filename, structure_member);
866     }
867
868   zip_reader_unref (zip);
869   return NULL;
870 }
871
872 char * WARN_UNUSED_RESULT
873 spv_decode_fmt_spec (uint32_t u32, struct fmt_spec *out)
874 {
875   if (!u32
876       || (u32 == 0x10000 || u32 == 1 /* both used as string formats */))
877     {
878       *out = fmt_for_output (FMT_F, 40, 2);
879       return NULL;
880     }
881
882   uint8_t raw_type = u32 >> 16;
883   uint8_t w = u32 >> 8;
884   uint8_t d = u32;
885
886   msg_disable ();
887   *out = (struct fmt_spec) { .type = FMT_F, .w = w, .d = d };
888   bool ok = raw_type >= 40 || fmt_from_io (raw_type, &out->type);
889   if (ok)
890     {
891       fmt_fix_output (out);
892       ok = fmt_check_width_compat (out, 0);
893     }
894   msg_enable ();
895
896   if (!ok)
897     {
898       *out = fmt_for_output (FMT_F, 40, 2);
899       return xasprintf ("bad format %#"PRIx32, u32);
900     }
901
902   return NULL;
903 }