dc5cf94ca3cbf1115835b68d19bcd4993c4cce78
[pspp] / src / output / cairo.c
1 /* PSPP - a program for statistical analysis.
2    Copyright (C) 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016 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 "libpspp/assertion.h"
20 #include "libpspp/cast.h"
21 #include "libpspp/message.h"
22 #include "libpspp/str.h"
23 #include "libpspp/string-map.h"
24 #include "data/file-handle-def.h"
25 #include "output/cairo-fsm.h"
26 #include "output/cairo-pager.h"
27 #include "output/driver-provider.h"
28 #include "output/options.h"
29 #include "output/output-item.h"
30 #include "output/table.h"
31
32 #include <cairo/cairo-pdf.h>
33 #include <cairo/cairo-ps.h>
34 #include <cairo/cairo-svg.h>
35
36 #include <cairo/cairo.h>
37 #include <inttypes.h>
38 #include <math.h>
39 #include <pango/pango-font.h>
40 #include <stdlib.h>
41
42 #include "gl/c-strcase.h"
43 #include "gl/xalloc.h"
44
45 #include "gettext.h"
46 #define _(msgid) gettext (msgid)
47
48 /* This file uses TABLE_HORZ and TABLE_VERT enough to warrant abbreviating. */
49 #define H TABLE_HORZ
50 #define V TABLE_VERT
51
52 /* The unit used for internal measurements is inch/(72 * XR_POINT).
53    (Thus, XR_POINT units represent one point.) */
54 #define XR_POINT PANGO_SCALE
55
56 /* Conversions to and from points. */
57 static double
58 xr_to_pt (int x)
59 {
60   return x / (double) XR_POINT;
61 }
62
63 /* Output types. */
64 enum xr_output_type
65   {
66     XR_PDF,
67     XR_PS,
68     XR_SVG,
69     XR_PNG
70   };
71
72 /* Cairo output driver. */
73 struct xr_driver
74   {
75     struct output_driver driver;
76
77     enum xr_output_type output_type;
78     struct xr_fsm_style *fsm_style;
79     struct xr_page_style *page_style;
80     struct xr_pager *pager;
81     bool trim;
82
83     /* This is the surface where we're currently drawing.  It is always
84        nonnull.
85
86        If 'trim' is true, this is a special Cairo "recording surface" that we
87        are using to save output temporarily just to find out the bounding box,
88        then later replay it into the destination surface.
89
90        If 'trim' is false:
91
92          - For output to a PDF or PostScript file, it is the same pointer as
93            'dest_surface'.
94
95          - For output to a PNG file, it is an image surface.
96
97          - For output to an SVG file, it is a recording surface.
98     */
99     cairo_surface_t *drawing_surface;
100
101     /* - For output to a PDF or PostScript file, this is the surface for the
102          PDF or PostScript file where the output is ultimately going.
103
104        - For output to a PNG file, this is NULL, because Cairo has very
105          limited support for PNG.  Cairo can't open a PNG file for writing as
106          a surface, it can only save an existing surface to a PNG file.
107
108        - For output to a SVG file, this is NULL, because Cairo does not
109          permit resizing the SVG page size after creating the file, whereas
110          this driver needs to do that sometimes.  Also, SVG is not multi-page
111          (according to https://wiki.inkscape.org/wiki/index.php/Multipage).
112     */
113     cairo_surface_t *dest_surface;
114
115     /* Used only in file names, for PNG and SVG output where we can only write
116        one page per file. */
117     int page_number;
118   };
119
120 static const struct output_driver_class cairo_driver_class;
121
122 \f
123 /* Output driver basics. */
124
125 static struct xr_driver *
126 xr_driver_cast (struct output_driver *driver)
127 {
128   assert (driver->class == &cairo_driver_class);
129   return UP_CAST (driver, struct xr_driver, driver);
130 }
131
132 static struct driver_option *
133 opt (struct output_driver *d, struct string_map *options, const char *key,
134      const char *default_value)
135 {
136   return driver_option_get (d, options, key, default_value);
137 }
138
139 static PangoFontDescription *
140 parse_font (const char *font, int default_size, bool bold, bool italic)
141 {
142   if (!c_strcasecmp (font, "Monospaced"))
143     font = "Monospace";
144
145   PangoFontDescription *desc = pango_font_description_from_string (font);
146   if (desc == NULL)
147     return NULL;
148
149   /* If the font description didn't include an explicit font size, then set it
150      to DEFAULT_SIZE, which is in inch/72000 units. */
151   if (!(pango_font_description_get_set_fields (desc) & PANGO_FONT_MASK_SIZE))
152     pango_font_description_set_size (desc,
153                                      (default_size / 1000.0) * PANGO_SCALE);
154
155   pango_font_description_set_weight (desc, (bold
156                                             ? PANGO_WEIGHT_BOLD
157                                             : PANGO_WEIGHT_NORMAL));
158   pango_font_description_set_style (desc, (italic
159                                            ? PANGO_STYLE_ITALIC
160                                            : PANGO_STYLE_NORMAL));
161
162   return desc;
163 }
164
165 static PangoFontDescription *
166 parse_font_option (struct output_driver *d, struct string_map *options,
167                    const char *key, const char *default_value,
168                    int default_size, bool bold, bool italic)
169 {
170   char *string = parse_string (opt (d, options, key, default_value));
171   PangoFontDescription *desc = parse_font (string, default_size, bold, italic);
172   if (!desc)
173     {
174       msg (MW, _("`%s': bad font specification"), string);
175
176       /* Fall back to DEFAULT_VALUE, which had better be a valid font
177          description. */
178       desc = parse_font (default_value, default_size, bold, italic);
179       assert (desc != NULL);
180     }
181   free (string);
182
183   return desc;
184 }
185
186 static struct xr_driver *
187 xr_allocate (const char *name, int device_type,
188              enum xr_output_type output_type, struct string_map *o)
189 {
190   struct xr_driver *xr = xzalloc (sizeof *xr);
191   struct output_driver *d = &xr->driver;
192
193   output_driver_init (d, &cairo_driver_class, name, device_type);
194   xr->output_type = output_type;
195
196   /* Scale factor from inch/72000 to inch/(72 * XR_POINT). */
197   const double scale = XR_POINT / 1000.;
198
199   int paper[TABLE_N_AXES];
200   parse_paper_size (opt (d, o, "paper-size", ""), &paper[H], &paper[V]);
201   for (int a = 0; a < TABLE_N_AXES; a++)
202     paper[a] *= scale;
203
204   int margins[TABLE_N_AXES][2];
205   margins[H][0] = parse_dimension (opt (d, o, "left-margin", ".5in")) * scale;
206   margins[H][1] = parse_dimension (opt (d, o, "right-margin", ".5in")) * scale;
207   margins[V][0] = parse_dimension (opt (d, o, "top-margin", ".5in")) * scale;
208   margins[V][1] = parse_dimension (opt (d, o, "bottom-margin", ".5in")) * scale;
209
210   int size[TABLE_N_AXES];
211   for (int a = 0; a < TABLE_N_AXES; a++)
212     size[a] = paper[a] - margins[a][0] - margins[a][1];
213
214   int min_break[TABLE_N_AXES];
215   min_break[H] = parse_dimension (opt (d, o, "min-hbreak", NULL)) * scale;
216   min_break[V] = parse_dimension (opt (d, o, "min-vbreak", NULL)) * scale;
217   for (int a = 0; a < TABLE_N_AXES; a++)
218     if (min_break[a] <= 0)
219       min_break[a] = size[a] / 2;
220
221   int font_size = parse_int (opt (d, o, "font-size", "10000"), 1000, 1000000);
222   PangoFontDescription *font = parse_font_option (
223     d, o, "prop-font", "Sans Serif", font_size, false, false);
224
225   struct cell_color fg = parse_color (opt (d, o, "foreground-color", "black"));
226
227   bool systemcolors = parse_boolean (opt (d, o, "systemcolors", "false"));
228
229   int object_spacing
230     = parse_dimension (opt (d, o, "object-spacing", NULL)) * scale;
231   if (object_spacing <= 0)
232     object_spacing = XR_POINT * 12;
233
234   const char *default_resolution = (output_type == XR_PNG ? "96" : "72");
235   int font_resolution = parse_int (opt (d, o, "font-resolution",
236                                         default_resolution), 10, 1000);
237
238   xr->trim = parse_boolean (opt (d, o, "trim", "false"));
239
240   /* Cairo 1.16.0 has a bug that causes crashes if outlines are enabled at the
241      same time as trimming:
242      https://lists.cairographics.org/archives/cairo/2020-December/029151.html
243      For now, just disable the outline if trimming is enabled. */
244   bool include_outline
245     = (output_type == XR_PDF
246        && parse_boolean (opt (d, o, "outline", xr->trim ? "false" : "true")));
247
248   xr->page_style = xmalloc (sizeof *xr->page_style);
249   *xr->page_style = (struct xr_page_style) {
250     .ref_cnt = 1,
251
252     .margins = {
253       [H] = { margins[H][0], margins[H][1], },
254       [V] = { margins[V][0], margins[V][1], },
255     },
256
257     .initial_page_number = 1,
258     .include_outline = include_outline,
259   };
260
261   xr->fsm_style = xmalloc (sizeof *xr->fsm_style);
262   *xr->fsm_style = (struct xr_fsm_style) {
263     .ref_cnt = 1,
264     .size = { [H] = size[H], [V] = size[V] },
265     .min_break = { [H] = min_break[H], [V] = min_break[V] },
266     .font = font,
267     .fg = fg,
268     .use_system_colors = systemcolors,
269     .object_spacing = object_spacing,
270     .font_resolution = font_resolution,
271   };
272
273   return xr;
274 }
275
276 static struct output_driver *
277 xr_create (struct file_handle *fh, enum settings_output_devices device_type,
278            struct string_map *o, enum xr_output_type output_type)
279 {
280   const char *file_name = fh_get_file_name (fh);
281   struct xr_driver *xr = xr_allocate (file_name, device_type, output_type, o);
282
283   double paper[TABLE_N_AXES];
284   for (int a = 0; a < TABLE_N_AXES; a++)
285     paper[a] = xr_to_pt (xr_page_style_paper_size (xr->page_style,
286                                                    xr->fsm_style, a));
287
288   xr->dest_surface
289     = (output_type == XR_PDF
290        ? cairo_pdf_surface_create (file_name, paper[H], paper[V])
291        : output_type == XR_PS
292        ? cairo_ps_surface_create (file_name, paper[H], paper[V])
293        : NULL);
294   if (xr->dest_surface)
295     {
296       cairo_status_t status = cairo_surface_status (xr->dest_surface);
297       if (status != CAIRO_STATUS_SUCCESS)
298         {
299           msg (ME, _("error opening output file `%s': %s"),
300                file_name, cairo_status_to_string (status));
301           goto error;
302         }
303     }
304
305   xr->drawing_surface
306     = (xr->trim || output_type == XR_SVG
307        ? cairo_recording_surface_create (CAIRO_CONTENT_COLOR_ALPHA,
308                                          &(cairo_rectangle_t) {
309                                            .width = paper[H],
310                                            .height = paper[V] })
311        : output_type == XR_PNG
312        ? cairo_image_surface_create (CAIRO_FORMAT_ARGB32, paper[H], paper[V])
313        : xr->dest_surface);
314
315   fh_unref (fh);
316   return &xr->driver;
317
318  error:
319   fh_unref (fh);
320   output_driver_destroy (&xr->driver);
321   return NULL;
322 }
323
324 static struct output_driver *
325 xr_pdf_create (struct  file_handle *fh, enum settings_output_devices device_type,
326                struct string_map *o)
327 {
328   return xr_create (fh, device_type, o, XR_PDF);
329 }
330
331 static struct output_driver *
332 xr_ps_create (struct  file_handle *fh, enum settings_output_devices device_type,
333                struct string_map *o)
334 {
335   return xr_create (fh, device_type, o, XR_PS);
336 }
337
338 static struct output_driver *
339 xr_svg_create (struct file_handle *fh, enum settings_output_devices device_type,
340                struct string_map *o)
341 {
342   return xr_create (fh, device_type, o, XR_SVG);
343 }
344
345 static struct output_driver *
346 xr_png_create (struct file_handle *fh, enum settings_output_devices device_type,
347                struct string_map *o)
348 {
349   return xr_create (fh, device_type, o, XR_PNG);
350 }
351
352 static void
353 xr_set_surface_size (cairo_surface_t *surface, enum xr_output_type output_type,
354                      double width, double height)
355 {
356   switch (output_type)
357     {
358     case XR_PDF:
359       cairo_pdf_surface_set_size (surface, width, height);
360       break;
361
362     case XR_PS:
363       cairo_ps_surface_set_size (surface, width, height);
364       break;
365
366     case XR_SVG:
367     case XR_PNG:
368       NOT_REACHED ();
369     }
370 }
371
372 static void
373 xr_copy_surface (cairo_surface_t *dst, cairo_surface_t *src,
374                  double x, double y)
375 {
376   cairo_t *cr = cairo_create (dst);
377   cairo_set_source_surface (cr, src, x, y);
378   cairo_paint (cr);
379   cairo_destroy (cr);
380 }
381
382 static void
383 clear_rectangle (cairo_surface_t *surface,
384                  double x0, double y0, double x1, double y1)
385 {
386   cairo_t *cr = cairo_create (surface);
387   cairo_set_source_rgb (cr, 1, 1, 1);
388   cairo_new_path (cr);
389   cairo_rectangle (cr, x0, y0, x1 - x0, y1 - y0);
390   cairo_fill (cr);
391   cairo_destroy (cr);
392 }
393
394 static void
395 xr_report_error (cairo_status_t status, const char *file_name)
396 {
397   if (status != CAIRO_STATUS_SUCCESS)
398     fprintf (stderr,  "%s: %s\n", file_name, cairo_status_to_string (status));
399 }
400
401 static void
402 xr_finish_page (struct xr_driver *xr)
403 {
404   xr_pager_finish_page (xr->pager);
405
406   /* For 'trim' true:
407
408     - If the destination is PDF or PostScript, set the dest surface size, copy
409       ink extent, show_page.
410
411     - If the destination is PNG, create image surface, copy ink extent,
412       cairo_surface_write_to_png(), destroy image surface.
413
414     - If the destination is SVG, create svg surface, copy ink extent, close.
415
416     then destroy drawing_surface and make a new one.
417
418     For 'trim' false:
419
420     - If the destination is PDF or PostScript, show_page.
421
422     - If the destination is PNG, cairo_surface_write_to_png(), destroy image
423       surface, create new image surface.
424
425     - If the destination is SVG, create svg surface, copy whole thing, close.
426
427     */
428   double paper[TABLE_N_AXES];
429   for (int a = 0; a < TABLE_N_AXES; a++)
430     paper[a] = xr_to_pt (xr_page_style_paper_size (
431                            xr->page_style, xr->fsm_style, a));
432
433   xr->page_number++;
434   char *file_name = (xr->page_number > 1
435                      ? xasprintf ("%s-%d", xr->driver.name, xr->page_number)
436                      : xr->driver.name);
437
438   if (xr->trim)
439     {
440       /* Get the bounding box for the drawing surface. */
441       double ofs[TABLE_N_AXES], size[TABLE_N_AXES];
442       cairo_recording_surface_ink_extents (xr->drawing_surface,
443                                            &ofs[H], &ofs[V],
444                                            &size[H], &size[V]);
445       const int (*margins)[2] = xr->page_style->margins;
446       for (int a = 0; a < TABLE_N_AXES; a++)
447         {
448           double scale = XR_POINT;
449           size[a] += (margins[a][0] + margins[a][1]) / scale;
450           ofs[a] = -ofs[a] + margins[a][0] / scale;
451         }
452
453       switch (xr->output_type)
454         {
455         case XR_PDF:
456         case XR_PS:
457           xr_set_surface_size (xr->dest_surface, xr->output_type,
458                                size[H], size[V]);
459           xr_copy_surface (xr->dest_surface, xr->drawing_surface,
460                            ofs[H], ofs[V]);
461           cairo_surface_show_page (xr->dest_surface);
462           break;
463
464         case XR_SVG:
465           {
466             cairo_surface_t *svg = cairo_svg_surface_create (
467               file_name, size[H], size[V]);
468             xr_copy_surface (svg, xr->drawing_surface, ofs[H], ofs[V]);
469             xr_report_error (cairo_surface_status (svg), file_name);
470             cairo_surface_destroy (svg);
471           }
472           break;
473
474         case XR_PNG:
475           {
476             cairo_surface_t *png = cairo_image_surface_create (
477               CAIRO_FORMAT_ARGB32, size[H], size[V]);
478             clear_rectangle (png, 0, 0, size[H], size[V]);
479             xr_copy_surface (png, xr->drawing_surface, ofs[H], ofs[V]);
480             xr_report_error (cairo_surface_write_to_png (png, file_name),
481                              file_name);
482             cairo_surface_destroy (png);
483           }
484           break;
485         }
486
487       /* Destroy the recording surface and create a fresh one of the same
488          size. */
489       cairo_surface_destroy (xr->drawing_surface);
490       xr->drawing_surface = cairo_recording_surface_create (
491         CAIRO_CONTENT_COLOR_ALPHA,
492         &(cairo_rectangle_t) { .width = paper[H], .height = paper[V] });
493     }
494   else
495     {
496       switch (xr->output_type)
497         {
498         case XR_PDF:
499         case XR_PS:
500           cairo_surface_show_page (xr->dest_surface);
501           break;
502
503         case XR_SVG:
504           {
505             cairo_surface_t *svg = cairo_svg_surface_create (
506               file_name, paper[H], paper[V]);
507             xr_copy_surface (svg, xr->drawing_surface, 0.0, 0.0);
508             xr_report_error (cairo_surface_status (svg), file_name);
509             cairo_surface_destroy (svg);
510           }
511           break;
512
513         case XR_PNG:
514           xr_report_error (cairo_surface_write_to_png (xr->drawing_surface,
515                                                        file_name), file_name);
516           cairo_surface_destroy (xr->drawing_surface);
517           xr->drawing_surface = cairo_image_surface_create (
518             CAIRO_FORMAT_ARGB32, paper[H], paper[V]);
519           break;
520         }
521     }
522
523   if (file_name != xr->driver.name)
524     free (file_name);
525 }
526
527 static void
528 xr_destroy (struct output_driver *driver)
529 {
530   struct xr_driver *xr = xr_driver_cast (driver);
531
532   if (xr->pager)
533     xr_finish_page (xr);
534
535   xr_pager_destroy (xr->pager);
536
537   if (xr->drawing_surface && xr->drawing_surface != xr->dest_surface)
538     cairo_surface_destroy (xr->drawing_surface);
539   if (xr->dest_surface)
540     {
541       cairo_surface_finish (xr->dest_surface);
542       cairo_status_t status = cairo_surface_status (xr->dest_surface);
543       if (status != CAIRO_STATUS_SUCCESS)
544         fprintf (stderr,  _("error drawing output for %s driver: %s\n"),
545                  output_driver_get_name (driver),
546                  cairo_status_to_string (status));
547       cairo_surface_destroy (xr->dest_surface);
548     }
549
550   xr_page_style_unref (xr->page_style);
551   xr_fsm_style_unref (xr->fsm_style);
552   free (xr);
553 }
554
555 static void
556 xr_update_page_setup (struct output_driver *driver,
557                       const struct page_setup *setup)
558 {
559   struct xr_driver *xr = xr_driver_cast (driver);
560
561   const double scale = 72 * XR_POINT;
562
563   int swap = setup->orientation == PAGE_LANDSCAPE;
564   enum table_axis h = H ^ swap;
565   enum table_axis v = V ^ swap;
566
567   struct xr_page_style *old_ps = xr->page_style;
568   xr->page_style = xmalloc (sizeof *xr->page_style);
569   *xr->page_style = (struct xr_page_style) {
570     .ref_cnt = 1,
571
572     .margins = {
573       [H] = { setup->margins[h][0] * scale, setup->margins[h][1] * scale },
574       [V] = { setup->margins[v][0] * scale, setup->margins[v][1] * scale },
575     },
576
577     .initial_page_number = setup->initial_page_number,
578     .include_outline = old_ps->include_outline,
579   };
580   for (size_t i = 0; i < 2; i++)
581     page_heading_copy (&xr->page_style->headings[i], &setup->headings[i]);
582   xr_page_style_unref (old_ps);
583
584   struct xr_fsm_style *old_fs = xr->fsm_style;
585   xr->fsm_style = xmalloc (sizeof *xr->fsm_style);
586   *xr->fsm_style = (struct xr_fsm_style) {
587     .ref_cnt = 1,
588     .size = { [H] = setup->paper[H] * scale, [V] = setup->paper[V] * scale },
589     .min_break = {
590       [H] = setup->paper[H] * scale / 2,
591       [V] = setup->paper[V] * scale / 2,
592     },
593     .font = pango_font_description_copy (old_fs->font),
594     .fg = old_fs->fg,
595     .use_system_colors = old_fs->use_system_colors,
596     .object_spacing = setup->object_spacing * 72 * XR_POINT,
597     .font_resolution = old_fs->font_resolution,
598   };
599   xr_fsm_style_unref (old_fs);
600
601   xr_set_surface_size (xr->dest_surface, xr->output_type,
602                        setup->paper[H] * 72.0, setup->paper[V] * 72.0);
603 }
604
605 static void
606 xr_submit (struct output_driver *driver, const struct output_item *item)
607 {
608   struct xr_driver *xr = xr_driver_cast (driver);
609
610   if (!xr->pager)
611     {
612       xr->pager = xr_pager_create (xr->page_style, xr->fsm_style);
613       xr_pager_add_page (xr->pager, cairo_create (xr->drawing_surface));
614     }
615
616   xr_pager_add_item (xr->pager, item);
617   while (xr_pager_needs_new_page (xr->pager))
618     {
619       xr_finish_page (xr);
620       xr_pager_add_page (xr->pager, cairo_create (xr->drawing_surface));
621     }
622 }
623
624 static void
625 xr_setup (struct output_driver *driver, const struct page_setup *ps)
626 {
627   struct xr_driver *xr = xr_driver_cast (driver);
628
629   if (!xr->pager)
630     xr_update_page_setup (driver, ps);
631 }
632 \f
633 struct output_driver_factory pdf_driver_factory =
634   { "pdf", "pspp.pdf", xr_pdf_create };
635 struct output_driver_factory ps_driver_factory =
636   { "ps", "pspp.ps", xr_ps_create };
637 struct output_driver_factory svg_driver_factory =
638   { "svg", "pspp.svg", xr_svg_create };
639 struct output_driver_factory png_driver_factory =
640   { "png", "pspp.png", xr_png_create };
641
642 static const struct output_driver_class cairo_driver_class =
643 {
644   .name = "cairo",
645   .destroy = xr_destroy,
646   .submit = xr_submit,
647   .setup = xr_setup,
648   .handles_groups = true,
649 };