bdc500e54b2e9c8d37aa9e28c440b5d643369ea5
[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 driver_options *options, const char *key, const char *default_value)
134 {
135   return driver_option_get (options, key, default_value);
136 }
137
138 static PangoFontDescription *
139 parse_font (const char *font, int default_size, bool bold, bool italic)
140 {
141   if (!c_strcasecmp (font, "Monospaced"))
142     font = "Monospace";
143
144   PangoFontDescription *desc = pango_font_description_from_string (font);
145   if (desc == NULL)
146     return NULL;
147
148   /* If the font description didn't include an explicit font size, then set it
149      to DEFAULT_SIZE, which is in inch/72000 units. */
150   if (!(pango_font_description_get_set_fields (desc) & PANGO_FONT_MASK_SIZE))
151     pango_font_description_set_size (desc,
152                                      (default_size / 1000.0) * PANGO_SCALE);
153
154   pango_font_description_set_weight (desc, (bold
155                                             ? PANGO_WEIGHT_BOLD
156                                             : PANGO_WEIGHT_NORMAL));
157   pango_font_description_set_style (desc, (italic
158                                            ? PANGO_STYLE_ITALIC
159                                            : PANGO_STYLE_NORMAL));
160
161   return desc;
162 }
163
164 static PangoFontDescription *
165 parse_font_option (struct driver_options *options,
166                    const char *key, const char *default_value,
167                    int default_size, bool bold, bool italic)
168 {
169   char *string = parse_string (opt (options, key, default_value));
170   PangoFontDescription *desc = parse_font (string, default_size, bold, italic);
171   if (!desc)
172     {
173       msg (MW, _("`%s': bad font specification"), string);
174
175       /* Fall back to DEFAULT_VALUE, which had better be a valid font
176          description. */
177       desc = parse_font (default_value, default_size, bold, italic);
178       assert (desc != NULL);
179     }
180   free (string);
181
182   return desc;
183 }
184
185 /* Scale INCHES into inch/(72 * XR_POINT). */
186 static int
187 scale (double inches)
188 {
189   return inches * 72 * XR_POINT + 0.5;
190 }
191
192 static struct xr_driver *
193 xr_allocate (const char *name, int device_type,
194              enum xr_output_type output_type, struct driver_options *o)
195 {
196   struct page_setup *ps = page_setup_parse (o);
197
198   int size[TABLE_N_AXES];
199   for (int a = 0; a < TABLE_N_AXES; a++)
200     size[a] = scale (ps->paper[a] - ps->margins[a][0] - ps->margins[a][1]);
201
202   int min_break[TABLE_N_AXES];
203   min_break[H] = scale (parse_dimension (opt (o, "min-hbreak", NULL)));
204   min_break[V] = scale (parse_dimension (opt (o, "min-vbreak", NULL)));
205   for (int a = 0; a < TABLE_N_AXES; a++)
206     if (min_break[a] <= 0)
207       min_break[a] = size[a] / 2;
208
209   int object_spacing = scale (ps->object_spacing);
210   if (object_spacing <= 0)
211     object_spacing = scale (12.0 / 72.0);
212
213   int font_size = parse_int (opt (o, "font-size", "10000"), 1000, 1000000);
214   PangoFontDescription *font = parse_font_option (
215     o, "prop-font", "Sans Serif", font_size, false, false);
216
217   struct cell_color fg = parse_color (opt (o, "foreground-color", "black"));
218
219   bool systemcolors = parse_boolean (opt (o, "systemcolors", "false"));
220
221   const char *default_resolution = (output_type == XR_PNG ? "96" : "72");
222   int font_resolution = parse_int (opt (o, "font-resolution",
223                                         default_resolution), 10, 1000);
224
225   bool trim = parse_boolean (opt (o, "trim", "false"));
226
227   /* Cairo 1.16.0 has a bug that causes crashes if outlines are enabled at the
228      same time as trimming:
229      https://lists.cairographics.org/archives/cairo/2020-December/029151.html
230      For now, just disable the outline if trimming is enabled. */
231   bool include_outline
232     = (output_type == XR_PDF
233        && parse_boolean (opt (o, "outline", trim ? "false" : "true")));
234
235   struct xr_page_style *page_style = xmalloc (sizeof *page_style);
236   *page_style = (struct xr_page_style) {
237     .ref_cnt = 1,
238
239     .margins = {
240       [H] = { scale (ps->margins[H][0]), scale (ps->margins[H][1]) },
241       [V] = { scale (ps->margins[V][0]), scale (ps->margins[V][1]) },
242     },
243
244     .initial_page_number = 1,
245     .include_outline = include_outline,
246   };
247
248   struct xr_fsm_style *fsm_style = xmalloc (sizeof *fsm_style);
249   *fsm_style = (struct xr_fsm_style) {
250     .ref_cnt = 1,
251     .size = { [H] = size[H], [V] = size[V] },
252     .min_break = { [H] = min_break[H], [V] = min_break[V] },
253     .font = font,
254     .fg = fg,
255     .use_system_colors = systemcolors,
256     .object_spacing = object_spacing,
257     .font_resolution = font_resolution,
258   };
259
260   struct xr_driver *xr = xmalloc (sizeof *xr);
261   *xr = (struct xr_driver) {
262     .driver = {
263       .class = &cairo_driver_class,
264       .name = xstrdup (name),
265       .device_type = device_type,
266     },
267     .output_type = output_type,
268     .fsm_style = fsm_style,
269     .page_style = page_style,
270     .trim = trim,
271   };
272
273   page_setup_destroy (ps);
274
275   return xr;
276 }
277
278 static struct output_driver *
279 xr_create (struct file_handle *fh, enum settings_output_devices device_type,
280            struct driver_options *o, enum xr_output_type output_type)
281 {
282   const char *file_name = fh_get_file_name (fh);
283   struct xr_driver *xr = xr_allocate (file_name, device_type, output_type, o);
284
285   double paper[TABLE_N_AXES];
286   for (int a = 0; a < TABLE_N_AXES; a++)
287     paper[a] = xr_to_pt (xr_page_style_paper_size (xr->page_style,
288                                                    xr->fsm_style, a));
289
290   xr->dest_surface
291     = (output_type == XR_PDF
292        ? cairo_pdf_surface_create (file_name, paper[H], paper[V])
293        : output_type == XR_PS
294        ? cairo_ps_surface_create (file_name, paper[H], paper[V])
295        : NULL);
296   if (xr->dest_surface)
297     {
298       cairo_status_t status = cairo_surface_status (xr->dest_surface);
299       if (status != CAIRO_STATUS_SUCCESS)
300         {
301           msg (ME, _("error opening output file `%s': %s"),
302                file_name, cairo_status_to_string (status));
303           goto error;
304         }
305     }
306
307   xr->drawing_surface
308     = (xr->trim || output_type == XR_SVG
309        ? cairo_recording_surface_create (CAIRO_CONTENT_COLOR_ALPHA,
310                                          &(cairo_rectangle_t) {
311                                            .width = paper[H],
312                                            .height = paper[V] })
313        : output_type == XR_PNG
314        ? cairo_image_surface_create (CAIRO_FORMAT_ARGB32, paper[H], paper[V])
315        : xr->dest_surface);
316
317   fh_unref (fh);
318   return &xr->driver;
319
320  error:
321   fh_unref (fh);
322   output_driver_destroy (&xr->driver);
323   return NULL;
324 }
325
326 static struct output_driver *
327 xr_pdf_create (struct file_handle *fh, enum settings_output_devices device_type,
328                struct driver_options *o)
329 {
330   return xr_create (fh, device_type, o, XR_PDF);
331 }
332
333 static struct output_driver *
334 xr_ps_create (struct file_handle *fh, enum settings_output_devices device_type,
335               struct driver_options *o)
336 {
337   return xr_create (fh, device_type, o, XR_PS);
338 }
339
340 static struct output_driver *
341 xr_svg_create (struct file_handle *fh, enum settings_output_devices device_type,
342                struct driver_options *o)
343 {
344   return xr_create (fh, device_type, o, XR_SVG);
345 }
346
347 static struct output_driver *
348 xr_png_create (struct file_handle *fh, enum settings_output_devices device_type,
349                struct driver_options *o)
350 {
351   return xr_create (fh, device_type, o, XR_PNG);
352 }
353
354 static void
355 xr_set_surface_size (cairo_surface_t *surface, enum xr_output_type output_type,
356                      double width, double height)
357 {
358   switch (output_type)
359     {
360     case XR_PDF:
361       cairo_pdf_surface_set_size (surface, width, height);
362       break;
363
364     case XR_PS:
365       cairo_ps_surface_set_size (surface, width, height);
366       break;
367
368     case XR_SVG:
369     case XR_PNG:
370       NOT_REACHED ();
371     }
372 }
373
374 static void
375 xr_copy_surface (cairo_surface_t *dst, cairo_surface_t *src,
376                  double x, double y)
377 {
378   cairo_t *cr = cairo_create (dst);
379   cairo_set_source_surface (cr, src, x, y);
380   cairo_paint (cr);
381   cairo_destroy (cr);
382 }
383
384 static void
385 clear_rectangle (cairo_surface_t *surface,
386                  double x0, double y0, double x1, double y1)
387 {
388   cairo_t *cr = cairo_create (surface);
389   cairo_set_source_rgb (cr, 1, 1, 1);
390   cairo_new_path (cr);
391   cairo_rectangle (cr, x0, y0, x1 - x0, y1 - y0);
392   cairo_fill (cr);
393   cairo_destroy (cr);
394 }
395
396 static void
397 xr_report_error (cairo_status_t status, const char *file_name)
398 {
399   if (status != CAIRO_STATUS_SUCCESS)
400     fprintf (stderr,  "%s: %s\n", file_name, cairo_status_to_string (status));
401 }
402
403 static void
404 xr_finish_page (struct xr_driver *xr)
405 {
406   xr_pager_finish_page (xr->pager);
407
408   /* For 'trim' true:
409
410     - If the destination is PDF or PostScript, set the dest surface size, copy
411       ink extent, show_page.
412
413     - If the destination is PNG, create image surface, copy ink extent,
414       cairo_surface_write_to_png(), destroy image surface.
415
416     - If the destination is SVG, create svg surface, copy ink extent, close.
417
418     then destroy drawing_surface and make a new one.
419
420     For 'trim' false:
421
422     - If the destination is PDF or PostScript, show_page.
423
424     - If the destination is PNG, cairo_surface_write_to_png(), destroy image
425       surface, create new image surface.
426
427     - If the destination is SVG, create svg surface, copy whole thing, close.
428
429     */
430   double paper[TABLE_N_AXES];
431   for (int a = 0; a < TABLE_N_AXES; a++)
432     paper[a] = xr_to_pt (xr_page_style_paper_size (
433                            xr->page_style, xr->fsm_style, a));
434
435   xr->page_number++;
436   char *file_name = (xr->page_number > 1
437                      ? xasprintf ("%s-%d", xr->driver.name, xr->page_number)
438                      : xr->driver.name);
439
440   if (xr->trim)
441     {
442       /* Get the bounding box for the drawing surface. */
443       double ofs[TABLE_N_AXES], size[TABLE_N_AXES];
444       cairo_recording_surface_ink_extents (xr->drawing_surface,
445                                            &ofs[H], &ofs[V],
446                                            &size[H], &size[V]);
447       const int (*margins)[2] = xr->page_style->margins;
448       for (int a = 0; a < TABLE_N_AXES; a++)
449         {
450           double scale = XR_POINT;
451           size[a] += (margins[a][0] + margins[a][1]) / scale;
452           ofs[a] = -ofs[a] + margins[a][0] / scale;
453         }
454
455       switch (xr->output_type)
456         {
457         case XR_PDF:
458         case XR_PS:
459           xr_set_surface_size (xr->dest_surface, xr->output_type,
460                                size[H], size[V]);
461           xr_copy_surface (xr->dest_surface, xr->drawing_surface,
462                            ofs[H], ofs[V]);
463           cairo_surface_show_page (xr->dest_surface);
464           break;
465
466         case XR_SVG:
467           {
468             cairo_surface_t *svg = cairo_svg_surface_create (
469               file_name, size[H], size[V]);
470             xr_copy_surface (svg, xr->drawing_surface, ofs[H], ofs[V]);
471             xr_report_error (cairo_surface_status (svg), file_name);
472             cairo_surface_destroy (svg);
473           }
474           break;
475
476         case XR_PNG:
477           {
478             cairo_surface_t *png = cairo_image_surface_create (
479               CAIRO_FORMAT_ARGB32, size[H], size[V]);
480             clear_rectangle (png, 0, 0, size[H], size[V]);
481             xr_copy_surface (png, xr->drawing_surface, ofs[H], ofs[V]);
482             xr_report_error (cairo_surface_write_to_png (png, file_name),
483                              file_name);
484             cairo_surface_destroy (png);
485           }
486           break;
487         }
488
489       /* Destroy the recording surface and create a fresh one of the same
490          size. */
491       cairo_surface_destroy (xr->drawing_surface);
492       xr->drawing_surface = cairo_recording_surface_create (
493         CAIRO_CONTENT_COLOR_ALPHA,
494         &(cairo_rectangle_t) { .width = paper[H], .height = paper[V] });
495     }
496   else
497     {
498       switch (xr->output_type)
499         {
500         case XR_PDF:
501         case XR_PS:
502           cairo_surface_show_page (xr->dest_surface);
503           break;
504
505         case XR_SVG:
506           {
507             cairo_surface_t *svg = cairo_svg_surface_create (
508               file_name, paper[H], paper[V]);
509             xr_copy_surface (svg, xr->drawing_surface, 0.0, 0.0);
510             xr_report_error (cairo_surface_status (svg), file_name);
511             cairo_surface_destroy (svg);
512           }
513           break;
514
515         case XR_PNG:
516           xr_report_error (cairo_surface_write_to_png (xr->drawing_surface,
517                                                        file_name), file_name);
518           cairo_surface_destroy (xr->drawing_surface);
519           xr->drawing_surface = cairo_image_surface_create (
520             CAIRO_FORMAT_ARGB32, paper[H], paper[V]);
521           break;
522         }
523     }
524
525   if (file_name != xr->driver.name)
526     free (file_name);
527 }
528
529 static void
530 xr_destroy (struct output_driver *driver)
531 {
532   struct xr_driver *xr = xr_driver_cast (driver);
533
534   if (xr->pager)
535     xr_finish_page (xr);
536
537   xr_pager_destroy (xr->pager);
538
539   if (xr->drawing_surface && xr->drawing_surface != xr->dest_surface)
540     cairo_surface_destroy (xr->drawing_surface);
541   if (xr->dest_surface)
542     {
543       cairo_surface_finish (xr->dest_surface);
544       cairo_status_t status = cairo_surface_status (xr->dest_surface);
545       if (status != CAIRO_STATUS_SUCCESS)
546         fprintf (stderr,  _("error drawing output for %s driver: %s\n"),
547                  output_driver_get_name (driver),
548                  cairo_status_to_string (status));
549       cairo_surface_destroy (xr->dest_surface);
550     }
551
552   xr_page_style_unref (xr->page_style);
553   xr_fsm_style_unref (xr->fsm_style);
554   free (xr);
555 }
556
557 static void
558 xr_update_page_setup (struct output_driver *driver,
559                       const struct page_setup *setup)
560 {
561   struct xr_driver *xr = xr_driver_cast (driver);
562
563   const double scale = 72 * XR_POINT;
564
565   int swap = setup->orientation == PAGE_LANDSCAPE;
566   enum table_axis h = H ^ swap;
567   enum table_axis v = V ^ swap;
568
569   struct xr_page_style *old_ps = xr->page_style;
570   xr->page_style = xmalloc (sizeof *xr->page_style);
571   *xr->page_style = (struct xr_page_style) {
572     .ref_cnt = 1,
573
574     .margins = {
575       [H] = { setup->margins[h][0] * scale, setup->margins[h][1] * scale },
576       [V] = { setup->margins[v][0] * scale, setup->margins[v][1] * scale },
577     },
578
579     .initial_page_number = setup->initial_page_number,
580     .include_outline = old_ps->include_outline,
581   };
582   for (size_t i = 0; i < 2; i++)
583     page_heading_copy (&xr->page_style->headings[i], &setup->headings[i]);
584   xr_page_style_unref (old_ps);
585
586   struct xr_fsm_style *old_fs = xr->fsm_style;
587   xr->fsm_style = xmalloc (sizeof *xr->fsm_style);
588   *xr->fsm_style = (struct xr_fsm_style) {
589     .ref_cnt = 1,
590     .size = { [H] = setup->paper[H] * scale, [V] = setup->paper[V] * scale },
591     .min_break = {
592       [H] = setup->paper[H] * scale / 2,
593       [V] = setup->paper[V] * scale / 2,
594     },
595     .font = pango_font_description_copy (old_fs->font),
596     .fg = old_fs->fg,
597     .use_system_colors = old_fs->use_system_colors,
598     .object_spacing = setup->object_spacing * 72 * XR_POINT,
599     .font_resolution = old_fs->font_resolution,
600   };
601   xr_fsm_style_unref (old_fs);
602
603   xr_set_surface_size (xr->dest_surface, xr->output_type,
604                        setup->paper[H] * 72.0, setup->paper[V] * 72.0);
605 }
606
607 static void
608 xr_submit (struct output_driver *driver, const struct output_item *item)
609 {
610   struct xr_driver *xr = xr_driver_cast (driver);
611
612   if (!xr->pager)
613     {
614       xr->pager = xr_pager_create (xr->page_style, xr->fsm_style);
615       xr_pager_add_page (xr->pager, cairo_create (xr->drawing_surface));
616     }
617
618   xr_pager_add_item (xr->pager, item);
619   while (xr_pager_needs_new_page (xr->pager))
620     {
621       xr_finish_page (xr);
622       xr_pager_add_page (xr->pager, cairo_create (xr->drawing_surface));
623     }
624 }
625
626 static void
627 xr_setup (struct output_driver *driver, const struct page_setup *ps)
628 {
629   struct xr_driver *xr = xr_driver_cast (driver);
630
631   if (!xr->pager)
632     xr_update_page_setup (driver, ps);
633 }
634 \f
635 struct output_driver_factory pdf_driver_factory =
636   { "pdf", "pspp.pdf", xr_pdf_create };
637 struct output_driver_factory ps_driver_factory =
638   { "ps", "pspp.ps", xr_ps_create };
639 struct output_driver_factory svg_driver_factory =
640   { "svg", "pspp.svg", xr_svg_create };
641 struct output_driver_factory png_driver_factory =
642   { "png", "pspp.png", xr_png_create };
643
644 static const struct output_driver_class cairo_driver_class =
645 {
646   .name = "cairo",
647   .destroy = xr_destroy,
648   .submit = xr_submit,
649   .setup = xr_setup,
650   .handles_groups = true,
651 };