spv: Fix crash reading a page_setup without headings.
[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   if (!page_paragraph)
325     return;
326
327   const struct spvsx_page_paragraph_text *page_paragraph_text
328     = page_paragraph->page_paragraph_text;
329   if (!page_paragraph_text)
330     return;
331
332   xmlDoc *html_doc = parse_embedded_html (page_paragraph_text->node_.raw);
333   if (!html_doc)
334     return;
335
336   xmlNode *root = xmlDocGetRootElement (html_doc);
337   xmlNode *body = find_xml_child_element (root, "body");
338   if (body)
339     for (const xmlNode *node = body->children; node; node = node->next)
340       if (node->type == XML_ELEMENT_NODE
341           && !strcmp (CHAR_CAST (const char *, node->name), "p"))
342         {
343           ph->paragraphs = xrealloc (ph->paragraphs,
344                                      (ph->n + 1) * sizeof *ph->paragraphs);
345           decode_page_p (node, &ph->paragraphs[ph->n++]);
346         }
347   xmlFreeDoc (html_doc);
348 }
349
350 char * WARN_UNUSED_RESULT
351 spv_read_light_table (struct zip_reader *zip, const char *bin_member,
352                       struct spvlb_table **tablep)
353 {
354   *tablep = NULL;
355
356   void *data;
357   size_t size;
358   char *error = zip_member_read_all (zip, bin_member, &data, &size);
359   if (error)
360     return error;
361
362   struct spvbin_input input;
363   spvbin_input_init (&input, data, size);
364
365   struct spvlb_table *table = NULL;
366   error = (!size
367            ? xasprintf ("light table member is empty")
368            : !spvlb_parse_table (&input, &table)
369            ? spvbin_input_to_error (&input, NULL)
370            : input.ofs != input.size
371            ? xasprintf ("expected end of file at offset %#zx", input.ofs)
372            : NULL);
373   free (data);
374   if (!error)
375     *tablep = table;
376   return error;
377 }
378
379 static char * WARN_UNUSED_RESULT
380 pivot_table_open_light (struct zip_reader *zip, const char *bin_member,
381                         struct pivot_table **tablep)
382 {
383   *tablep = NULL;
384
385   struct spvlb_table *raw_table;
386   char *error = spv_read_light_table (zip, bin_member, &raw_table);
387   if (!error)
388     error = decode_spvlb_table (raw_table, tablep);
389   spvlb_free_table (raw_table);
390
391   return error;
392 }
393
394 char * WARN_UNUSED_RESULT
395 spv_read_legacy_data (struct zip_reader *zip, const char *bin_member,
396                       struct spv_data *data)
397 {
398   void *raw;
399   size_t size;
400   char *error = zip_member_read_all (zip, bin_member, &raw, &size);
401   if (!error)
402     {
403       error = spv_legacy_data_decode (raw, size, data);
404       free (raw);
405     }
406
407   return error;
408 }
409
410 char * WARN_UNUSED_RESULT
411 spv_read_xml_member (struct zip_reader *zip, const char *xml_member,
412                      bool keep_blanks, const char *root_element_name,
413                      xmlDoc **docp)
414 {
415   *docp = NULL;
416
417   struct zip_member *zm;
418   char *error = zip_member_open (zip, xml_member, &zm);
419   if (error)
420     return error;
421
422   xmlParserCtxt *parser;
423   xmlKeepBlanksDefault (keep_blanks);
424   parser = xmlCreatePushParserCtxt(NULL, NULL, NULL, 0, NULL);
425   if (!parser)
426     {
427       zip_member_finish (zm);
428       return xasprintf (_("%s: Failed to create XML parser"), xml_member);
429     }
430
431   int retval;
432   char buf[4096];
433   while ((retval = zip_member_read (zm, buf, sizeof buf)) > 0)
434     xmlParseChunk (parser, buf, retval, false);
435   xmlParseChunk (parser, NULL, 0, true);
436
437   xmlDoc *doc = parser->myDoc;
438   bool well_formed = parser->wellFormed;
439   xmlFreeParserCtxt (parser);
440
441   if (retval < 0)
442     {
443       char *error = zip_member_steal_error (zm);
444       zip_member_finish (zm);
445       xmlFreeDoc (doc);
446       return error;
447     }
448   zip_member_finish (zm);
449
450   if (!well_formed)
451     {
452       xmlFreeDoc (doc);
453       return xasprintf(_("%s: document is not well-formed"), xml_member);
454     }
455
456   const xmlNode *root_node = xmlDocGetRootElement (doc);
457   assert (root_node->type == XML_ELEMENT_NODE);
458   if (strcmp (CHAR_CAST (char *, root_node->name), root_element_name))
459     {
460       xmlFreeDoc (doc);
461       return xasprintf(_("%s: root node is \"%s\" but \"%s\" was expected"),
462                        xml_member,
463                        CHAR_CAST (char *, root_node->name), root_element_name);
464     }
465
466   *docp = doc;
467   return NULL;
468 }
469
470 static char * WARN_UNUSED_RESULT
471 pivot_table_open_legacy (struct zip_reader *zip, const char *bin_member,
472                          const char *xml_member, const char *subtype,
473                          const struct pivot_table_look *look,
474                          struct pivot_table **tablep)
475 {
476   *tablep = NULL;
477
478   struct spv_data data = SPV_DATA_INITIALIZER;
479   char *error = spv_read_legacy_data (zip, bin_member, &data);
480   if (error)
481     goto exit;
482
483   xmlDoc *doc;
484   error = spv_read_xml_member (zip, xml_member, false,
485                                "visualization", &doc);
486   if (error)
487     goto exit_free_data;
488
489   struct spvxml_context ctx = SPVXML_CONTEXT_INIT (ctx);
490   struct spvdx_visualization *v;
491   spvdx_parse_visualization (&ctx, xmlDocGetRootElement (doc), &v);
492   error = spvxml_context_finish (&ctx, &v->node_);
493   if (error)
494     goto exit_free_doc;
495
496   error = decode_spvdx_table (v, subtype, look, &data, tablep);
497
498   spvdx_free_visualization (v);
499 exit_free_doc:
500   if (doc)
501     xmlFreeDoc (doc);
502 exit_free_data:
503   spv_data_uninit (&data);
504 exit:
505   return error;
506 }
507
508 static struct output_item *
509 spv_read_table_item (struct zip_reader *zip,
510                      const struct spvsx_table *table)
511 {
512   const struct spvsx_table_structure *ts = table->table_structure;
513   const char *bin_member = ts->data_path->text;
514   const char *xml_member = ts->path ? ts->path->text : NULL;
515
516   struct pivot_table *pt = NULL;
517   char *error;
518   if (xml_member)
519     {
520       struct pivot_table_look *look;
521       error = (table->table_properties
522                ? spv_table_look_decode (table->table_properties, &look)
523                : xstrdup ("Legacy table lacks tableProperties"));
524       if (!error)
525         {
526           error = pivot_table_open_legacy (zip, bin_member, xml_member,
527                                            table->sub_type, look, &pt);
528           pivot_table_look_unref (look);
529         }
530     }
531   else
532     error = pivot_table_open_light (zip, bin_member, &pt);
533   if (error)
534     pt = pivot_table_create_for_text (
535       pivot_value_new_text (N_("Error")),
536       pivot_value_new_user_text_nocopy (error));
537
538   struct output_item *item = table_item_create (pt);
539   output_item_set_command_name (item, table->command_name);
540   output_item_add_spv_info (item);
541   item->spv_info->error = error != NULL;
542   item->spv_info->zip_reader = zip_reader_ref (zip);
543   item->spv_info->bin_member = xstrdup (bin_member);
544   item->spv_info->xml_member = xstrdup_if_nonnull (xml_member);
545   return item;
546 }
547
548 static cairo_status_t
549 read_from_zip_member (void *zm_, unsigned char *data, unsigned int length)
550 {
551   struct zip_member *zm = zm_;
552   if (!zm)
553     return CAIRO_STATUS_READ_ERROR;
554
555   while (length > 0)
556     {
557       int n = zip_member_read (zm, data, length);
558       if (n <= 0)
559         return CAIRO_STATUS_READ_ERROR;
560
561       data += n;
562       length -= n;
563     }
564
565   return CAIRO_STATUS_SUCCESS;
566 }
567
568 static char * WARN_UNUSED_RESULT
569 spv_read_image (struct zip_reader *zip, const char *png_member,
570                 const char *command_name, struct output_item **itemp)
571 {
572   struct zip_member *zm;
573   char *error = zip_member_open (zip, png_member, &zm);
574   if (error)
575     return error;
576
577   cairo_surface_t *surface = cairo_image_surface_create_from_png_stream (
578     read_from_zip_member, zm);
579   if (zm)
580     zip_member_finish (zm);
581
582   if (cairo_surface_status (surface) != CAIRO_STATUS_SUCCESS)
583     return xstrdup ("reading image failed");
584
585   struct output_item *item = image_item_create (surface);
586   output_item_set_command_name (item, command_name);
587   output_item_add_spv_info (item);
588   item->spv_info->zip_reader = zip_reader_ref (zip);
589   item->spv_info->png_member = xstrdup (png_member);
590   *itemp = item;
591   return NULL;
592 }
593
594 static struct output_item *
595 error_item_create (char *s)
596 {
597   struct output_item *item = text_item_create_nocopy (TEXT_ITEM_LOG, s,
598                                                       xstrdup ("Error"));
599   output_item_add_spv_info (item);
600   item->spv_info->error = true;
601   return item;
602 }
603
604 static struct output_item *
605 spv_decode_container (struct zip_reader *zip,
606                       const struct spvsx_container *c)
607 {
608   assert (c->n_seq == 1);
609   struct spvxml_node *content = c->seq[0];
610
611   struct output_item *item = NULL;
612   char *error;
613   if (spvsx_is_container_text (content))
614     {
615       item = decode_container_text (spvsx_cast_container_text (content));
616       error = NULL;
617     }
618   else if (spvsx_is_table (content))
619     {
620       item = spv_read_table_item (zip, spvsx_cast_table (content));
621       error = NULL;
622     }
623   else if (spvsx_is_object (content))
624     {
625       struct spvsx_object *object = spvsx_cast_object (content);
626       error = spv_read_image (zip, object->uri, object->command_name, &item);
627     }
628   else if (spvsx_is_image (content))
629     {
630       struct spvsx_image *image = spvsx_cast_image (content);
631       error = spv_read_image (zip, image->data_path->text, image->command_name,
632                               &item);
633     }
634   else if (spvsx_is_graph (content))
635     error = xstrdup ("graphs not yet implemented");
636   else if (spvsx_is_model (content))
637     error = xstrdup ("models not yet implemented");
638   else if (spvsx_is_tree (content))
639     error = xstrdup ("trees not yet implemented");
640   else
641     NOT_REACHED ();
642
643   if (error)
644     item = error_item_create (error);
645   else
646     output_item_set_label (item, c->label->text);
647   item->show = c->visibility == SPVSX_VISIBILITY_VISIBLE;
648
649   return item;
650 }
651
652 static void
653 set_structure_member (struct output_item *item, struct zip_reader *zip,
654                       const char *structure_member)
655 {
656   if (structure_member)
657     {
658       output_item_add_spv_info (item);
659       if (!item->spv_info->zip_reader)
660         item->spv_info->zip_reader = zip_reader_ref (zip);
661       if (!item->spv_info->structure_member)
662         item->spv_info->structure_member = xstrdup (structure_member);
663     }
664 }
665
666 static void
667 spv_decode_children (struct zip_reader *zip, const char *structure_member,
668                      struct spvxml_node **seq, size_t n_seq,
669                      struct output_item *parent)
670 {
671   for (size_t i = 0; i < n_seq; i++)
672     {
673       const struct spvxml_node *node = seq[i];
674
675       struct output_item *child;
676       if (spvsx_is_container (node))
677         {
678           const struct spvsx_container *container
679             = spvsx_cast_container (node);
680
681           if (container->page_break_before_present)
682             group_item_add_child (parent, page_break_item_create ());
683
684           child = spv_decode_container (zip, container);
685         }
686       else if (spvsx_is_heading (node))
687         {
688           const struct spvsx_heading *subheading = spvsx_cast_heading (node);
689
690           child = group_item_create (subheading->command_name,
691                                      subheading->label->text);
692           child->show = !subheading->heading_visibility_present;
693
694           /* Pass NULL for 'structure_member' so that only top-level items get
695              tagged that way.  Lower-level items are always in the same
696              structure member as their parent anyway. */
697            spv_decode_children (zip, NULL, subheading->seq,
698                                 subheading->n_seq, child);
699         }
700       else
701         NOT_REACHED ();
702
703       set_structure_member (child, zip, structure_member);
704       group_item_add_child (parent, child);
705     }
706 }
707
708 static struct page_setup *
709 decode_page_setup (const struct spvsx_page_setup *in, const char *file_name)
710 {
711   struct page_setup *out = xmalloc (sizeof *out);
712   *out = (struct page_setup) PAGE_SETUP_INITIALIZER;
713
714   out->initial_page_number = in->initial_page_number;
715
716   if (in->paper_width != DBL_MAX)
717     out->paper[TABLE_HORZ] = in->paper_width;
718   if (in->paper_height != DBL_MAX)
719     out->paper[TABLE_VERT] = in->paper_height;
720
721   if (in->margin_left != DBL_MAX)
722     out->margins[TABLE_HORZ][0] = in->margin_left;
723   if (in->margin_right != DBL_MAX)
724     out->margins[TABLE_HORZ][1] = in->margin_right;
725   if (in->margin_top != DBL_MAX)
726     out->margins[TABLE_VERT][0] = in->margin_top;
727   if (in->margin_bottom != DBL_MAX)
728     out->margins[TABLE_VERT][1] = in->margin_bottom;
729
730   if (in->space_after != DBL_MAX)
731     out->object_spacing = in->space_after;
732
733   if (in->chart_size)
734     out->chart_size = (in->chart_size == SPVSX_CHART_SIZE_FULL_HEIGHT
735                        ? PAGE_CHART_FULL_HEIGHT
736                        : in->chart_size == SPVSX_CHART_SIZE_HALF_HEIGHT
737                        ? PAGE_CHART_HALF_HEIGHT
738                        : in->chart_size == SPVSX_CHART_SIZE_QUARTER_HEIGHT
739                        ? PAGE_CHART_QUARTER_HEIGHT
740                        : PAGE_CHART_AS_IS);
741
742   decode_page_paragraph (in->page_header->page_paragraph, &out->headings[0]);
743   decode_page_paragraph (in->page_footer->page_paragraph, &out->headings[1]);
744
745   out->file_name = xstrdup (file_name);
746
747   return out;
748 }
749
750 static void
751 spv_add_error_heading (struct output_item *root_item,
752                        struct zip_reader *zip, const char *structure_member,
753                        char *error)
754 {
755   struct output_item *item = error_item_create (
756     xasprintf ("%s: %s", structure_member, error));
757   free (error);
758   set_structure_member (item, zip, structure_member);
759   group_item_add_child (root_item, item);
760 }
761
762 static void
763 spv_heading_read (struct zip_reader *zip, struct output_item *root_item,
764                   struct page_setup **psp, const char *file_name,
765                   const char *structure_member)
766 {
767   xmlDoc *doc;
768   char *error = spv_read_xml_member (zip, structure_member, true,
769                                      "heading", &doc);
770   if (error)
771     {
772       spv_add_error_heading (root_item, zip, structure_member, error);
773       return;
774     }
775
776   struct spvxml_context ctx = SPVXML_CONTEXT_INIT (ctx);
777   struct spvsx_root_heading *root;
778   spvsx_parse_root_heading (&ctx, xmlDocGetRootElement (doc), &root);
779   error = spvxml_context_finish (&ctx, &root->node_);
780   if (error)
781     {
782       xmlFreeDoc (doc);
783       spv_add_error_heading (root_item, zip, structure_member, error);
784       return;
785     }
786
787   if (root->page_setup && psp && !*psp)
788     *psp = decode_page_setup (root->page_setup, file_name);
789
790   for (size_t i = 0; i < root->n_seq; i++)
791     spv_decode_children (zip, structure_member, root->seq, root->n_seq,
792                          root_item);
793
794   spvsx_free_root_heading (root);
795   xmlFreeDoc (doc);
796 }
797
798 static int
799 spv_detect__ (struct zip_reader *zip, char **errorp)
800 {
801   *errorp = NULL;
802
803   const char *member = "META-INF/MANIFEST.MF";
804   if (!zip_reader_contains_member (zip, member))
805     return 0;
806
807   void *data;
808   size_t size;
809   *errorp = zip_member_read_all (zip, "META-INF/MANIFEST.MF",
810                                  &data, &size);
811   if (*errorp)
812     return -1;
813
814   const char *magic = "allowPivoting=true";
815   bool is_spv = size == strlen (magic) && !memcmp (magic, data, size);
816   free (data);
817
818   return is_spv;
819 }
820
821 /* Returns NULL if FILENAME is an SPV file, otherwise an error string that the
822    caller must eventually free(). */
823 char * WARN_UNUSED_RESULT
824 spv_detect (const char *filename)
825 {
826   struct zip_reader *zip;
827   char *error = zip_reader_create (filename, &zip);
828   if (error)
829     return error;
830
831   if (spv_detect__ (zip, &error) <= 0 && !error)
832     error = xasprintf("%s: not an SPV file", filename);
833   zip_reader_unref (zip);
834   return error;
835 }
836
837 char * WARN_UNUSED_RESULT
838 spv_read (const char *filename, struct output_item **outp,
839           struct page_setup **psp)
840 {
841   *outp = NULL;
842   if (psp)
843     *psp = NULL;
844
845   struct zip_reader *zip;
846   char *error = zip_reader_create (filename, &zip);
847   if (error)
848     return error;
849
850   int detect = spv_detect__ (zip, &error);
851   if (detect <= 0)
852     {
853       zip_reader_unref (zip);
854       return error ? error : xasprintf ("%s: not an SPV file", filename);
855     }
856
857   *outp = root_item_create ();
858   for (size_t i = 0; ; i++)
859     {
860       const char *structure_member = zip_reader_get_member_name (zip, i);
861       if (!structure_member)
862         break;
863
864       struct substring structure_member_ss = ss_cstr (structure_member);
865       if (ss_starts_with (structure_member_ss, ss_cstr ("outputViewer"))
866           && ss_ends_with (structure_member_ss, ss_cstr (".xml")))
867         spv_heading_read (zip, *outp, psp, filename, structure_member);
868     }
869
870   zip_reader_unref (zip);
871   return NULL;
872 }
873
874 char * WARN_UNUSED_RESULT
875 spv_decode_fmt_spec (uint32_t u32, struct fmt_spec *out)
876 {
877   if (!u32
878       || (u32 == 0x10000 || u32 == 1 /* both used as string formats */))
879     {
880       *out = fmt_for_output (FMT_F, 40, 2);
881       return NULL;
882     }
883
884   uint8_t raw_type = u32 >> 16;
885   uint8_t w = u32 >> 8;
886   uint8_t d = u32;
887
888   *out = (struct fmt_spec) { .type = FMT_F, .w = w, .d = d };
889   bool ok = raw_type >= 40 || fmt_from_io (raw_type, &out->type);
890   if (ok)
891     {
892       fmt_fix_output (out);
893       ok = fmt_check_width_compat (out, 0);
894     }
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 }