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