49266d899aba8c368e5c04f5435c0bb773f9492e
[pspp] / src / output / spv / spv-table-look.c
1 /* PSPP - a program for statistical analysis.
2    Copyright (C) 2017, 2018, 2020 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-table-look.h"
20
21 #include <errno.h>
22 #include <inttypes.h>
23 #include <libxml/xmlreader.h>
24 #include <libxml/xmlwriter.h>
25 #include <string.h>
26
27 #include "libpspp/i18n.h"
28 #include "output/spv/structure-xml-parser.h"
29 #include "output/spv/tlo-parser.h"
30 #include "output/pivot-table.h"
31
32 #include "gl/read-file.h"
33 #include "gl/xalloc.h"
34 #include "gl/xmemdup0.h"
35
36 #include "gettext.h"
37 #define _(msgid) gettext (msgid)
38
39 static struct cell_color
40 optional_color (int color, struct cell_color default_color)
41 {
42   return (color >= 0
43           ? (struct cell_color) CELL_COLOR (color >> 16, color >> 8, color)
44           : default_color);
45 }
46
47 static int
48 optional_length (const char *s, int default_length)
49 {
50   /* There is usually a "pt" suffix.  We ignore it. */
51   int length;
52   return s && sscanf (s, "%d", &length) == 1 ? length : default_length;
53 }
54
55 static int
56 optional_px (double inches, int default_px)
57 {
58   return inches != DBL_MAX ? inches * 96.0 : default_px;
59 }
60
61 static int
62 optional_int (int x, int default_value)
63 {
64   return x != INT_MIN ? x : default_value;
65 }
66
67 static int
68 optional_pt (double inches, int default_pt)
69 {
70   return inches != DBL_MAX ? inches * 72.0 + .5 : default_pt;
71 }
72
73 static const char *pivot_area_names[PIVOT_N_AREAS] = {
74   [PIVOT_AREA_TITLE] = "title",
75   [PIVOT_AREA_CAPTION] = "caption",
76   [PIVOT_AREA_FOOTER] = "footnotes",
77   [PIVOT_AREA_CORNER] = "cornerLabels",
78   [PIVOT_AREA_COLUMN_LABELS] = "columnLabels",
79   [PIVOT_AREA_ROW_LABELS] = "rowLabels",
80   [PIVOT_AREA_DATA] = "data",
81   [PIVOT_AREA_LAYERS] = "layers",
82 };
83
84 static enum pivot_area
85 pivot_area_from_name (const char *name)
86 {
87   enum pivot_area area;
88   for (area = 0; area < PIVOT_N_AREAS; area++)
89     if (!strcmp (name, pivot_area_names[area]))
90       break;
91   return area;
92 }
93
94 static const char *pivot_border_names[PIVOT_N_BORDERS] = {
95   [PIVOT_BORDER_TITLE] = "titleLayerSeparator",
96   [PIVOT_BORDER_OUTER_LEFT] = "leftOuterFrame",
97   [PIVOT_BORDER_OUTER_TOP] = "topOuterFrame",
98   [PIVOT_BORDER_OUTER_RIGHT] = "rightOuterFrame",
99   [PIVOT_BORDER_OUTER_BOTTOM] = "bottomOuterFrame",
100   [PIVOT_BORDER_INNER_LEFT] = "leftInnerFrame",
101   [PIVOT_BORDER_INNER_TOP] = "topInnerFrame",
102   [PIVOT_BORDER_INNER_RIGHT] = "rightInnerFrame",
103   [PIVOT_BORDER_INNER_BOTTOM] = "bottomInnerFrame",
104   [PIVOT_BORDER_DATA_LEFT] = "dataAreaLeft",
105   [PIVOT_BORDER_DATA_TOP] = "dataAreaTop",
106   [PIVOT_BORDER_DIM_ROW_HORZ] = "horizontalDimensionBorderRows",
107   [PIVOT_BORDER_DIM_ROW_VERT] = "verticalDimensionBorderRows",
108   [PIVOT_BORDER_DIM_COL_HORZ] = "horizontalDimensionBorderColumns",
109   [PIVOT_BORDER_DIM_COL_VERT] = "verticalDimensionBorderColumns",
110   [PIVOT_BORDER_CAT_ROW_HORZ] = "horizontalCategoryBorderRows",
111   [PIVOT_BORDER_CAT_ROW_VERT] = "verticalCategoryBorderRows",
112   [PIVOT_BORDER_CAT_COL_HORZ] = "horizontalCategoryBorderColumns",
113   [PIVOT_BORDER_CAT_COL_VERT] = "verticalCategoryBorderColumns",
114 };
115
116 static enum pivot_border
117 pivot_border_from_name (const char *name)
118 {
119   enum pivot_border border;
120   for (border = 0; border < PIVOT_N_BORDERS; border++)
121     if (!strcmp (name, pivot_border_names[border]))
122       break;
123   return border;
124 }
125
126 char * WARN_UNUSED_RESULT
127 spv_table_look_decode (const struct spvsx_table_properties *in,
128                        struct pivot_table_look **outp)
129 {
130   struct pivot_table_look *out = pivot_table_look_new_builtin_default ();
131   char *error = NULL;
132
133   out->name = xstrdup_if_nonnull (in->name);
134
135   const struct spvsx_general_properties *g = in->general_properties;
136   out->omit_empty = g->hide_empty_rows != 0;
137   out->width_ranges[TABLE_HORZ][0] = optional_pt (g->minimum_column_width, -1);
138   out->width_ranges[TABLE_HORZ][1] = optional_pt (g->maximum_column_width, -1);
139   out->width_ranges[TABLE_VERT][0] = optional_pt (g->minimum_row_width, -1);
140   out->width_ranges[TABLE_VERT][1] = optional_pt (g->maximum_row_width, -1);
141   out->row_labels_in_corner
142     = g->row_dimension_labels != SPVSX_ROW_DIMENSION_LABELS_NESTED;
143
144   const struct spvsx_footnote_properties *f = in->footnote_properties;
145   out->footnote_marker_superscripts
146     = (f->marker_position != SPVSX_MARKER_POSITION_SUBSCRIPT);
147   out->show_numeric_markers
148     = (f->number_format == SPVSX_NUMBER_FORMAT_NUMERIC);
149
150   const struct spvsx_cell_format_properties *cfp = in->cell_format_properties;
151   for (size_t i = 0; i < cfp->n_cell_style; i++)
152     {
153       const struct spvsx_cell_style *c = cfp->cell_style[i];
154       const char *name = CHAR_CAST (const char *, c->node_.raw->name);
155       enum pivot_area area = pivot_area_from_name (name);
156       if (area == PIVOT_N_AREAS)
157         {
158           error = xasprintf ("unknown area \"%s\" in cellFormatProperties",
159                              name);
160           goto error;
161         }
162
163       struct table_area_style *a = &out->areas[area];
164       const struct spvsx_style *s = c->style;
165       if (s->font_weight)
166         a->font_style.bold = s->font_weight == SPVSX_FONT_WEIGHT_BOLD;
167       if (s->font_style)
168         a->font_style.italic = s->font_style == SPVSX_FONT_STYLE_ITALIC;
169       if (s->font_underline)
170         a->font_style.underline
171           = s->font_underline == SPVSX_FONT_UNDERLINE_UNDERLINE;
172       if (s->color >= 0)
173         a->font_style.fg[0] = optional_color (
174           s->color, (struct cell_color) CELL_COLOR_BLACK);
175       if (c->alternating_text_color >= 0 || s->color >= 0)
176         a->font_style.fg[1] = optional_color (c->alternating_text_color,
177                                               a->font_style.fg[0]);
178       if (s->color2 >= 0)
179         a->font_style.bg[0] = optional_color (
180           s->color2, (struct cell_color) CELL_COLOR_WHITE);
181       if (c->alternating_color >= 0 || s->color2 >= 0)
182         a->font_style.bg[1] = optional_color (c->alternating_color,
183                                               a->font_style.bg[0]);
184       if (s->font_family)
185         {
186           free (a->font_style.typeface);
187           a->font_style.typeface = xstrdup (s->font_family);
188         }
189
190       if (s->font_size)
191         a->font_style.size = optional_length (s->font_size, 0);
192
193       if (s->text_alignment)
194         a->cell_style.halign
195           = (s->text_alignment == SPVSX_TEXT_ALIGNMENT_LEFT
196              ? TABLE_HALIGN_LEFT
197              : s->text_alignment == SPVSX_TEXT_ALIGNMENT_RIGHT
198              ? TABLE_HALIGN_RIGHT
199              : s->text_alignment == SPVSX_TEXT_ALIGNMENT_CENTER
200              ? TABLE_HALIGN_CENTER
201              : s->text_alignment == SPVSX_TEXT_ALIGNMENT_DECIMAL
202              ? TABLE_HALIGN_DECIMAL
203              : TABLE_HALIGN_MIXED);
204       if (s->label_location_vertical)
205         a->cell_style.valign
206           = (s->label_location_vertical == SPVSX_LABEL_LOCATION_VERTICAL_NEGATIVE
207              ? TABLE_VALIGN_BOTTOM
208              : s->label_location_vertical == SPVSX_LABEL_LOCATION_VERTICAL_POSITIVE
209              ? TABLE_VALIGN_TOP
210              : TABLE_VALIGN_CENTER);
211
212       if (s->decimal_offset != DBL_MAX)
213         a->cell_style.decimal_offset = optional_px (s->decimal_offset, 0);
214
215       if (s->margin_left != DBL_MAX)
216         a->cell_style.margin[TABLE_HORZ][0] = optional_px (s->margin_left, 8);
217       if (s->margin_right != DBL_MAX)
218         a->cell_style.margin[TABLE_HORZ][1] = optional_px (s->margin_right,
219                                                            11);
220       if (s->margin_top != DBL_MAX)
221         a->cell_style.margin[TABLE_VERT][0] = optional_px (s->margin_top, 1);
222       if (s->margin_bottom != DBL_MAX)
223         a->cell_style.margin[TABLE_VERT][1] = optional_px (s->margin_bottom,
224                                                            1);
225     }
226
227   const struct spvsx_border_properties *bp = in->border_properties;
228   for (size_t i = 0; i < bp->n_border_style; i++)
229     {
230       const struct spvsx_border_style *bin = bp->border_style[i];
231       const char *name = CHAR_CAST (const char *, bin->node_.raw->name);
232       enum pivot_border border = pivot_border_from_name (name);
233       if (border == PIVOT_N_BORDERS)
234         {
235           error = xasprintf ("unknown border \"%s\" parsing borderProperties",
236                              name);
237           goto error;
238         }
239
240       struct table_border_style *bout = &out->borders[border];
241       bout->stroke
242         = (bin->border_style_type == SPVSX_BORDER_STYLE_TYPE_NONE
243            ? TABLE_STROKE_NONE
244            : bin->border_style_type == SPVSX_BORDER_STYLE_TYPE_DASHED
245            ? TABLE_STROKE_DASHED
246            : bin->border_style_type == SPVSX_BORDER_STYLE_TYPE_THICK
247            ? TABLE_STROKE_THICK
248            : bin->border_style_type == SPVSX_BORDER_STYLE_TYPE_THIN
249            ? TABLE_STROKE_THIN
250            : bin->border_style_type == SPVSX_BORDER_STYLE_TYPE_DOUBLE
251            ? TABLE_STROKE_DOUBLE
252            : TABLE_STROKE_SOLID);
253       bout->color = optional_color (bin->color,
254                                     (struct cell_color) CELL_COLOR_BLACK);
255     }
256
257   const struct spvsx_printing_properties *pp = in->printing_properties;
258   out->print_all_layers = pp->print_all_layers > 0;
259   out->paginate_layers = pp->print_each_layer_on_separate_page > 0;
260   out->shrink_to_fit[TABLE_HORZ] = pp->rescale_wide_table_to_fit_page > 0;
261   out->shrink_to_fit[TABLE_VERT] = pp->rescale_long_table_to_fit_page > 0;
262   out->top_continuation = pp->continuation_text_at_top > 0;
263   out->bottom_continuation = pp->continuation_text_at_bottom > 0;
264   free (out->continuation);
265   out->continuation = xstrdup (pp->continuation_text
266                                ? pp->continuation_text : "(cont.)");
267   out->n_orphan_lines = optional_int (pp->window_orphan_lines, 2);
268
269   *outp = out;
270   return NULL;
271
272 error:
273   pivot_table_look_unref (out);
274   *outp = NULL;
275   return error;
276 }
277 \f
278 static struct cell_color
279 tlo_decode_color (uint32_t c)
280 {
281   return (struct cell_color) CELL_COLOR (c, c >> 8, c >> 16);
282 }
283
284 static void
285 tlo_decode_border (const struct tlo_separator *in,
286                    struct table_border_style *out)
287 {
288   if (in->type == 0)
289     {
290       out->stroke = TABLE_STROKE_NONE;
291       return;
292     }
293
294   out->color = tlo_decode_color (in->type_01.color);
295
296   switch (in->type_01.style)
297     {
298     case 0:
299       out->stroke = (in->type_01.width == 0 ? TABLE_STROKE_THIN
300                      : in->type_01.width == 1 ? TABLE_STROKE_SOLID
301                      : TABLE_STROKE_THICK);
302       break;
303
304     case 1:
305       out->stroke = TABLE_STROKE_DOUBLE;
306       break;
307
308     case 2:
309       out->stroke = TABLE_STROKE_DASHED;
310       break;
311     }
312 }
313
314 static struct cell_color
315 interpolate_colors (struct cell_color c0, struct cell_color c1, int shading)
316 {
317   if (shading <= 0)
318     return c0;
319   else if (shading >= 10)
320     return c1;
321   else
322     {
323       int x0 = 10 - shading;
324       int x1 = shading;
325
326       return (struct cell_color) CELL_COLOR ((c0.r * x0 + c1.r * x1) / 10,
327                                              (c0.g * x0 + c1.g * x1) / 10,
328                                              (c0.b * x0 + c1.b * x1) / 10);
329     }
330 }
331
332 static void
333 tlo_decode_area (const struct tlo_area_color *color,
334                  const struct tlo_area_style *style,
335                  struct table_area_style *out)
336 {
337   out->cell_style.halign = (style->halign == 0 ? TABLE_HALIGN_LEFT
338                             : style->halign == 1 ? TABLE_HALIGN_RIGHT
339                             : style->halign == 2 ? TABLE_HALIGN_CENTER
340                             : style->halign == 4 ? TABLE_HALIGN_DECIMAL
341                             : TABLE_HALIGN_MIXED);
342   out->cell_style.valign = (style->valign == 0 ? TABLE_VALIGN_TOP
343                             : style->valign == 1 ? TABLE_VALIGN_BOTTOM
344                             : TABLE_VALIGN_CENTER);
345   out->cell_style.decimal_offset = style->decimal_offset / 20;
346   out->cell_style.decimal_char = '.';                  /* XXX */
347   out->cell_style.margin[TABLE_HORZ][0] = style->left_margin / 20;
348   out->cell_style.margin[TABLE_HORZ][1] = style->right_margin / 20;
349   out->cell_style.margin[TABLE_VERT][0] = style->top_margin / 20;
350   out->cell_style.margin[TABLE_VERT][1] = style->bottom_margin / 20;
351
352   out->font_style.bold = style->weight > 400;
353   out->font_style.italic = style->italic;
354   out->font_style.underline = style->underline;
355   out->font_style.markup = false;
356
357   out->font_style.fg[0] = out->font_style.fg[1]
358     = tlo_decode_color (style->text_color);
359
360   struct cell_color c0 = tlo_decode_color (color->color0);
361   struct cell_color c10 = tlo_decode_color (color->color10);
362   struct cell_color bg = interpolate_colors (c0, c10, color->shading);
363   out->font_style.bg[0] = out->font_style.bg[1] = bg;
364
365   free (out->font_style.typeface);
366   out->font_style.typeface = recode_string (
367     "UTF-8", "ISO-8859-1",
368     CHAR_CAST (char *, style->font_name), style->font_name_len);
369   out->font_style.size = -style->font_size * 3 / 4;
370 }
371
372 static struct pivot_table_look *
373 tlo_decode (const struct tlo_table_look *in)
374 {
375   struct pivot_table_look *out = pivot_table_look_new_builtin_default ();
376
377   const uint16_t flags = in->tl->flags;
378
379   out->omit_empty = (flags & 0x02) != 0;
380   out->row_labels_in_corner = !in->tl->nested_row_labels;
381   if (in->v2_styles)
382     {
383       out->width_ranges[TABLE_HORZ][0] = in->v2_styles->min_col_width;
384       out->width_ranges[TABLE_HORZ][1] = in->v2_styles->max_col_width;
385       out->width_ranges[TABLE_VERT][0] = in->v2_styles->min_row_height;
386       out->width_ranges[TABLE_VERT][1] = in->v2_styles->max_row_height;
387     }
388   else
389     {
390       out->width_ranges[TABLE_HORZ][0] = 36;
391       out->width_ranges[TABLE_HORZ][1] = 72;
392       out->width_ranges[TABLE_VERT][0] = 36;
393       out->width_ranges[TABLE_VERT][1] = 120;
394     }
395
396   out->show_numeric_markers = flags & 0x04;
397   out->footnote_marker_superscripts = !in->tl->footnote_marker_subscripts;
398
399   for (int i = 0; i < 4; i++)
400     {
401       static const enum pivot_border map[4] =
402         {
403           PIVOT_BORDER_DIM_ROW_HORZ,
404           PIVOT_BORDER_DIM_ROW_VERT,
405           PIVOT_BORDER_CAT_ROW_HORZ,
406           PIVOT_BORDER_CAT_ROW_VERT,
407         };
408       tlo_decode_border (in->ss->sep1[i], &out->borders[map[i]]);
409     }
410
411   for (int i = 0; i < 4; i++)
412     {
413       static const enum pivot_border map[4] =
414         {
415           PIVOT_BORDER_DIM_COL_HORZ,
416           PIVOT_BORDER_DIM_COL_VERT,
417           PIVOT_BORDER_CAT_COL_HORZ,
418           PIVOT_BORDER_CAT_COL_VERT,
419         };
420       tlo_decode_border (in->ss->sep2[i], &out->borders[map[i]]);
421     }
422
423   if (in->v2_styles)
424     for (int i = 0; i < 11; i++)
425       {
426         static const enum pivot_border map[11] =
427           {
428             PIVOT_BORDER_TITLE,
429             PIVOT_BORDER_INNER_LEFT,
430             PIVOT_BORDER_INNER_RIGHT,
431             PIVOT_BORDER_INNER_TOP,
432             PIVOT_BORDER_INNER_BOTTOM,
433             PIVOT_BORDER_OUTER_LEFT,
434             PIVOT_BORDER_OUTER_RIGHT,
435             PIVOT_BORDER_OUTER_TOP,
436             PIVOT_BORDER_OUTER_BOTTOM,
437             PIVOT_BORDER_DATA_LEFT,
438             PIVOT_BORDER_DATA_TOP,
439           };
440         tlo_decode_border (in->v2_styles->sep3[i], &out->borders[map[i]]);
441       }
442   else
443     {
444       out->borders[PIVOT_BORDER_TITLE].stroke = TABLE_STROKE_NONE;
445       out->borders[PIVOT_BORDER_INNER_LEFT].stroke = TABLE_STROKE_SOLID;
446       out->borders[PIVOT_BORDER_INNER_TOP].stroke = TABLE_STROKE_SOLID;
447       out->borders[PIVOT_BORDER_INNER_RIGHT].stroke = TABLE_STROKE_SOLID;
448       out->borders[PIVOT_BORDER_INNER_BOTTOM].stroke = TABLE_STROKE_SOLID;
449       out->borders[PIVOT_BORDER_OUTER_LEFT].stroke = TABLE_STROKE_NONE;
450       out->borders[PIVOT_BORDER_OUTER_TOP].stroke = TABLE_STROKE_NONE;
451       out->borders[PIVOT_BORDER_OUTER_RIGHT].stroke = TABLE_STROKE_NONE;
452       out->borders[PIVOT_BORDER_OUTER_BOTTOM].stroke = TABLE_STROKE_NONE;
453       out->borders[PIVOT_BORDER_DATA_LEFT].stroke = TABLE_STROKE_NONE;
454       out->borders[PIVOT_BORDER_DATA_TOP].stroke = TABLE_STROKE_NONE;
455     }
456
457   tlo_decode_area (in->cs->title_color, in->ts->title_style,
458                    &out->areas[PIVOT_AREA_TITLE]);
459   for (int i = 0; i < 7; i++)
460     {
461       static const enum pivot_area map[7] = {
462         PIVOT_AREA_LAYERS,
463         PIVOT_AREA_CORNER,
464         PIVOT_AREA_ROW_LABELS,
465         PIVOT_AREA_COLUMN_LABELS,
466         PIVOT_AREA_DATA,
467         PIVOT_AREA_CAPTION,
468         PIVOT_AREA_FOOTER
469       };
470       tlo_decode_area (in->ts->most_areas[i]->color,
471                        in->ts->most_areas[i]->style,
472                        &out->areas[map[i]]);
473     }
474
475   out->print_all_layers = flags & 0x08;
476   out->paginate_layers = flags & 0x40;
477   out->shrink_to_fit[TABLE_HORZ] = flags & 0x10;
478   out->shrink_to_fit[TABLE_VERT] = flags & 0x20;
479   out->top_continuation = flags & 0x80;
480   out->bottom_continuation = flags & 0x100;
481   if (in->v2_styles)
482     {
483       free (out->continuation);
484       out->continuation = xmemdup0 (in->v2_styles->continuation,
485                                     in->v2_styles->continuation_len);
486     }
487   /* n_orphan_lines isn't in .tlo files AFAICT. */
488
489   return out;
490 }
491 \f
492 char * WARN_UNUSED_RESULT
493 spv_table_look_read (const char *filename, struct pivot_table_look **outp)
494 {
495   *outp = NULL;
496
497   size_t length;
498   char *file = read_file (filename, 0, &length);
499   if (!file)
500     return xasprintf ("%s: failed to read file (%s)",
501                       filename, strerror (errno));
502
503   if ((uint8_t) file[0] == 0xff)
504     {
505       struct spvbin_input input;
506       spvbin_input_init (&input, file, length);
507
508       struct tlo_table_look *look;
509       char *error = NULL;
510       if (!tlo_parse_table_look (&input, &look))
511         error = spvbin_input_to_error (&input, NULL);
512       else
513         {
514           *outp = tlo_decode (look);
515           tlo_free_table_look (look);
516         }
517       return error;
518     }
519   else
520     {
521       xmlDoc *doc = xmlReadMemory (file, length, NULL, NULL, XML_PARSE_NOBLANKS);
522       free (file);
523       if (!doc)
524         return xasprintf ("%s: failed to parse XML", filename);
525
526       struct spvxml_context ctx = SPVXML_CONTEXT_INIT (ctx);
527       struct spvsx_table_properties *tp;
528       spvsx_parse_table_properties (&ctx, xmlDocGetRootElement (doc), &tp);
529       char *error = spvxml_context_finish (&ctx, &tp->node_);
530
531       if (!error)
532         error = spv_table_look_decode (tp, outp);
533
534       spvsx_free_table_properties (tp);
535       xmlFreeDoc (doc);
536
537       return error;
538     }
539 }
540
541 static void
542 write_attr (xmlTextWriter *xml, const char *name, const char *value)
543 {
544   xmlTextWriterWriteAttribute (xml,
545                                CHAR_CAST (xmlChar *, name),
546                                CHAR_CAST (xmlChar *, value));
547 }
548
549 static void PRINTF_FORMAT (3, 4)
550 write_attr_format (xmlTextWriter *xml, const char *name,
551                    const char *format, ...)
552 {
553   va_list args;
554   va_start (args, format);
555   char *value = xvasprintf (format, args);
556   va_end (args);
557
558   write_attr (xml, name, value);
559   free (value);
560 }
561
562 static void
563 write_attr_color (xmlTextWriter *xml, const char *name,
564                   const struct cell_color *color)
565 {
566   write_attr_format (xml, name, "#%02"PRIx8"%02"PRIx8"%02"PRIx8,
567                      color->r, color->g, color->b);
568 }
569
570 static void
571 write_attr_dimension (xmlTextWriter *xml, const char *name, int px)
572 {
573   int pt = px / 96.0 * 72.0;
574   write_attr_format (xml, name, "%dpt", pt);
575 }
576
577 static void
578 write_attr_bool (xmlTextWriter *xml, const char *name, bool b)
579 {
580   write_attr (xml, name, b ? "true" : "false");
581 }
582
583 static void
584 start_elem (xmlTextWriter *xml, const char *name)
585 {
586   xmlTextWriterStartElement (xml, CHAR_CAST (xmlChar *, name));
587 }
588
589 static void
590 end_elem (xmlTextWriter *xml)
591 {
592   xmlTextWriterEndElement (xml);
593 }
594
595 char * WARN_UNUSED_RESULT
596 spv_table_look_write (const char *filename, const struct pivot_table_look *look)
597 {
598   FILE *file = fopen (filename, "w");
599   if (!file)
600     return xasprintf (_("%s: create failed (%s)"), filename, strerror (errno));
601
602   xmlTextWriter *xml = xmlNewTextWriter (xmlOutputBufferCreateFile (
603                                            file, NULL));
604   if (!xml)
605     {
606       fclose (file);
607       return xasprintf (_("%s: failed to start writing XML"), filename);
608     }
609
610   xmlTextWriterSetIndent (xml, 1);
611   xmlTextWriterSetIndentString (xml, CHAR_CAST (xmlChar *, "    "));
612
613   xmlTextWriterStartDocument (xml, NULL, "UTF-8", NULL);
614   start_elem (xml, "tableProperties");
615   if (look->name)
616     write_attr (xml, "name", look->name);
617   write_attr (xml, "xmlns", "http://www.ibm.com/software/analytics/spss/xml/table-looks");
618   write_attr (xml, "xmlns:vizml", "http://www.ibm.com/software/analytics/spss/xml/visualization");
619   write_attr (xml, "xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance");
620   write_attr (xml, "xsi:schemaLocation", "http://www.ibm.com/software/analytics/spss/xml/table-looks http://www.ibm.com/software/analytics/spss/xml/table-looks/table-looks-1.4.xsd");
621
622   start_elem (xml, "generalProperties");
623   write_attr_bool (xml, "hideEmptyRows", look->omit_empty);
624   const int (*wr)[2] = look->width_ranges;
625   write_attr_format (xml, "maximumColumnWidth", "%d", wr[TABLE_HORZ][1]);
626   write_attr_format (xml, "maximumRowWidth", "%d", wr[TABLE_VERT][1]);
627   write_attr_format (xml, "minimumColumnWidth", "%d", wr[TABLE_HORZ][0]);
628   write_attr_format (xml, "minimumRowWidth", "%d", wr[TABLE_VERT][0]);
629   write_attr (xml, "rowDimensionLabels",
630               look->row_labels_in_corner ? "inCorner" : "nested");
631   end_elem (xml);
632
633   start_elem (xml, "footnoteProperties");
634   write_attr (xml, "markerPosition",
635               look->footnote_marker_superscripts ? "superscript" : "subscript");
636   write_attr (xml, "numberFormat",
637               look->show_numeric_markers ? "numeric" : "alphabetic");
638   end_elem (xml);
639
640   start_elem (xml, "cellFormatProperties");
641   for (enum pivot_area a = 0; a < PIVOT_N_AREAS; a++)
642     {
643       const struct table_area_style *area = &look->areas[a];
644       const struct font_style *font = &area->font_style;
645       const struct cell_style *cell = &area->cell_style;
646
647       start_elem (xml, pivot_area_names[a]);
648       if (a == PIVOT_AREA_DATA
649           && (!cell_color_equal (font->fg[0], font->fg[1])
650               || !cell_color_equal (font->bg[0], font->bg[1])))
651         {
652           write_attr_color (xml, "alternatingColor", &font->bg[1]);
653           write_attr_color (xml, "alternatingTextColor", &font->fg[1]);
654         }
655
656       start_elem (xml, "vizml:style");
657       write_attr_color (xml, "color", &font->fg[0]);
658       write_attr_color (xml, "color2", &font->bg[0]);
659       write_attr (xml, "font-family", font->typeface);
660       write_attr_format (xml, "font-size", "%dpt", font->size);
661       write_attr (xml, "font-weight", font->bold ? "bold" : "regular");
662       write_attr (xml, "font-underline",
663                   font->underline ? "underline" : "none");
664       write_attr (xml, "labelLocationVertical",
665                   cell->valign == TABLE_VALIGN_BOTTOM ? "negative"
666                   : cell->valign == TABLE_VALIGN_TOP ? "positive"
667                   : "center");
668       write_attr_dimension (xml, "margin-bottom", cell->margin[TABLE_VERT][1]);
669       write_attr_dimension (xml, "margin-left", cell->margin[TABLE_HORZ][0]);
670       write_attr_dimension (xml, "margin-right", cell->margin[TABLE_HORZ][1]);
671       write_attr_dimension (xml, "margin-top", cell->margin[TABLE_VERT][0]);
672       write_attr (xml, "textAlignment",
673                   cell->halign == TABLE_HALIGN_LEFT ? "left"
674                   : cell->halign == TABLE_HALIGN_RIGHT ? "right"
675                   : cell->halign == TABLE_HALIGN_CENTER ? "center"
676                   : cell->halign == TABLE_HALIGN_DECIMAL ? "decimal"
677                   : "mixed");
678       if (cell->halign == TABLE_HALIGN_DECIMAL)
679         write_attr_dimension (xml, "decimal-offset", cell->decimal_offset);
680       end_elem (xml);
681
682       end_elem (xml);
683     }
684   end_elem (xml);
685
686   start_elem (xml, "borderProperties");
687   for (enum pivot_border b = 0; b < PIVOT_N_BORDERS; b++)
688     {
689       const struct table_border_style *border = &look->borders[b];
690
691       start_elem (xml, pivot_border_names[b]);
692
693       static const char *table_stroke_names[TABLE_N_STROKES] =
694         {
695           [TABLE_STROKE_NONE] = "none",
696           [TABLE_STROKE_SOLID] = "solid",
697           [TABLE_STROKE_DASHED] = "dashed",
698           [TABLE_STROKE_THICK] = "thick",
699           [TABLE_STROKE_THIN] = "thin",
700           [TABLE_STROKE_DOUBLE] = "double",
701         };
702       write_attr (xml, "borderStyleType", table_stroke_names[border->stroke]);
703       write_attr_color (xml, "color", &border->color);
704       end_elem (xml);
705     }
706   end_elem (xml);
707
708   start_elem (xml, "printingProperties");
709   write_attr_bool (xml, "printAllLayers", look->print_all_layers);
710   write_attr_bool (xml, "rescaleLongTableToFitPage",
711                    look->shrink_to_fit[TABLE_HORZ]);
712   write_attr_bool (xml, "rescaleWideTableToFitPage",
713                    look->shrink_to_fit[TABLE_VERT]);
714   write_attr_format (xml, "windowOrphanLines", "%zu", look->n_orphan_lines);
715   if (look->continuation && look->continuation[0]
716       && (look->top_continuation || look->bottom_continuation))
717     {
718       write_attr (xml, "continuationText", look->continuation);
719       write_attr_bool (xml, "continuationTextAtTop", look->top_continuation);
720       write_attr_bool (xml, "continuationTextAtBottom",
721                        look->bottom_continuation);
722     }
723   end_elem (xml);
724
725   xmlTextWriterEndDocument (xml);
726
727   xmlFreeTextWriter (xml);
728
729   fflush (file);
730   bool ok = !ferror (file);
731   if (fclose (file) == EOF)
732     ok = false;
733
734   if (!ok)
735     return xasprintf (_("%s: error writing file (%s)"),
736                       filename, strerror (errno));
737
738   return NULL;
739 }