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