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