cairo: Add support for png and trim.
[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   xr->page_style = xmalloc (sizeof *xr->page_style);
242   *xr->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     .object_spacing = object_spacing,
252   };
253
254   xr->fsm_style = xmalloc (sizeof *xr->fsm_style);
255   *xr->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     .fonts = {
260       [XR_FONT_PROPORTIONAL] = proportional_font,
261       [XR_FONT_FIXED] = fixed_font,
262     },
263     .fg = fg,
264     .use_system_colors = systemcolors,
265     .font_resolution = font_resolution,
266   };
267
268   return xr;
269 }
270
271 static struct output_driver *
272 xr_create (struct file_handle *fh, enum settings_output_devices device_type,
273            struct string_map *o, enum xr_output_type output_type)
274 {
275   const char *file_name = fh_get_file_name (fh);
276   struct xr_driver *xr = xr_allocate (file_name, device_type, output_type, o);
277
278   double paper[TABLE_N_AXES];
279   for (int a = 0; a < TABLE_N_AXES; a++)
280     paper[a] = xr_to_pt (xr_page_style_paper_size (xr->page_style,
281                                                    xr->fsm_style, a));
282
283   xr->dest_surface
284     = (output_type == XR_PDF
285        ? cairo_pdf_surface_create (file_name, paper[H], paper[V])
286        : output_type == XR_PS
287        ? cairo_ps_surface_create (file_name, paper[H], paper[V])
288        : NULL);
289   if (xr->dest_surface)
290     {
291       cairo_status_t status = cairo_surface_status (xr->dest_surface);
292       if (status != CAIRO_STATUS_SUCCESS)
293         {
294           msg (ME, _("error opening output file `%s': %s"),
295                file_name, cairo_status_to_string (status));
296           goto error;
297         }
298     }
299
300   xr->drawing_surface
301     = (xr->trim || output_type == XR_SVG
302        ? cairo_recording_surface_create (CAIRO_CONTENT_COLOR_ALPHA,
303                                          &(cairo_rectangle_t) {
304                                            .width = paper[H],
305                                            .height = paper[V] })
306        : output_type == XR_PNG
307        ? cairo_image_surface_create (CAIRO_FORMAT_ARGB32, paper[H], paper[V])
308        : xr->dest_surface);
309
310   fh_unref (fh);
311   return &xr->driver;
312
313  error:
314   fh_unref (fh);
315   output_driver_destroy (&xr->driver);
316   return NULL;
317 }
318
319 static struct output_driver *
320 xr_pdf_create (struct  file_handle *fh, enum settings_output_devices device_type,
321                struct string_map *o)
322 {
323   return xr_create (fh, device_type, o, XR_PDF);
324 }
325
326 static struct output_driver *
327 xr_ps_create (struct  file_handle *fh, enum settings_output_devices device_type,
328                struct string_map *o)
329 {
330   return xr_create (fh, device_type, o, XR_PS);
331 }
332
333 static struct output_driver *
334 xr_svg_create (struct file_handle *fh, enum settings_output_devices device_type,
335                struct string_map *o)
336 {
337   return xr_create (fh, device_type, o, XR_SVG);
338 }
339
340 static struct output_driver *
341 xr_png_create (struct file_handle *fh, enum settings_output_devices device_type,
342                struct string_map *o)
343 {
344   return xr_create (fh, device_type, o, XR_PNG);
345 }
346
347 static void
348 xr_set_surface_size (cairo_surface_t *surface, enum xr_output_type output_type,
349                      double width, double height)
350 {
351   switch (output_type)
352     {
353     case XR_PDF:
354       cairo_pdf_surface_set_size (surface, width, height);
355       break;
356
357     case XR_PS:
358       cairo_ps_surface_set_size (surface, width, height);
359       break;
360
361     case XR_SVG:
362     case XR_PNG:
363       NOT_REACHED ();
364     }
365 }
366
367 static void
368 xr_copy_surface (cairo_surface_t *dst, cairo_surface_t *src,
369                  double x, double y)
370 {
371   cairo_t *cr = cairo_create (dst);
372   cairo_set_source_surface (cr, src, x, y);
373   cairo_paint (cr);
374   cairo_destroy (cr);
375 }
376
377 static void
378 clear_rectangle (cairo_surface_t *surface,
379                  double x0, double y0, double x1, double y1)
380 {
381   cairo_t *cr = cairo_create (surface);
382   cairo_set_source_rgb (cr, 1, 1, 1);
383   cairo_new_path (cr);
384   cairo_rectangle (cr, x0, y0, x1 - x0, y1 - y0);
385   cairo_fill (cr);
386   cairo_destroy (cr);
387 }
388
389 static void
390 xr_report_error (cairo_status_t status, const char *file_name)
391 {
392   if (status != CAIRO_STATUS_SUCCESS)
393     fprintf (stderr,  "%s: %s", file_name, cairo_status_to_string (status));
394 }
395
396 static void
397 xr_finish_page (struct xr_driver *xr)
398 {
399   /* For 'trim' true:
400
401     - If the destination is PDF or PostScript, set the dest surface size, copy
402       ink extent, show_page.
403
404     - If the destination is PNG, create image surface, copy ink extent,
405       cairo_surface_write_to_png(), destroy image surface.
406
407     - If the destination is SVG, create svg surface, copy ink extent, close.
408
409     then destroy drawing_surface and make a new one.
410
411     For 'trim' false:
412
413     - If the destination is PDF or PostScript, show_page.
414
415     - If the destination is PNG, cairo_surface_write_to_png(), destroy image
416       surface, create new image surface.
417
418     - If the destination is SVG, create svg surface, copy whole thing, close.
419
420     */
421   double paper[TABLE_N_AXES];
422   for (int a = 0; a < TABLE_N_AXES; a++)
423     paper[a] = xr_to_pt (xr_page_style_paper_size (
424                            xr->page_style, xr->fsm_style, a));
425
426   xr->page_number++;
427   char *file_name = (xr->page_number > 1
428                      ? xasprintf ("%s-%d", xr->driver.name, xr->page_number)
429                      : xr->driver.name);
430
431   if (xr->trim)
432     {
433       /* Get the bounding box for the drawing surface. */
434       double ofs[TABLE_N_AXES], size[TABLE_N_AXES];
435       cairo_recording_surface_ink_extents (xr->drawing_surface,
436                                            &ofs[H], &ofs[V],
437                                            &size[H], &size[V]);
438       const int (*margins)[2] = xr->page_style->margins;
439       for (int a = 0; a < TABLE_N_AXES; a++)
440         {
441           double scale = XR_POINT;
442           size[a] += (margins[a][0] + margins[a][1]) / scale;
443           ofs[a] = -ofs[a] + margins[a][0] / scale;
444         }
445
446       switch (xr->output_type)
447         {
448         case XR_PDF:
449         case XR_PS:
450           xr_set_surface_size (xr->dest_surface, xr->output_type,
451                                size[H], size[V]);
452           xr_copy_surface (xr->dest_surface, xr->drawing_surface,
453                            ofs[H], ofs[V]);
454           cairo_surface_show_page (xr->dest_surface);
455           break;
456
457         case XR_SVG:
458           {
459             cairo_surface_t *svg = cairo_svg_surface_create (
460               file_name, size[H], size[V]);
461             xr_copy_surface (svg, xr->drawing_surface, ofs[H], ofs[V]);
462             xr_report_error (cairo_surface_status (svg), file_name);
463             cairo_surface_destroy (svg);
464           }
465           break;
466
467         case XR_PNG:
468           {
469             cairo_surface_t *png = cairo_image_surface_create (
470               CAIRO_FORMAT_ARGB32, size[H], size[V]);
471             clear_rectangle (png, 0, 0, size[H], size[V]);
472             xr_copy_surface (png, xr->drawing_surface, ofs[H], ofs[V]);
473             xr_report_error (cairo_surface_write_to_png (png, file_name),
474                              file_name);
475             cairo_surface_destroy (png);
476           }
477           break;
478         }
479
480       /* Destroy the recording surface and create a fresh one of the same
481          size. */
482       cairo_surface_destroy (xr->drawing_surface);
483       xr->drawing_surface = cairo_recording_surface_create (
484         CAIRO_CONTENT_COLOR_ALPHA,
485         &(cairo_rectangle_t) { .width = paper[H], .height = paper[V] });
486     }
487   else
488     {
489       switch (xr->output_type)
490         {
491         case XR_PDF:
492         case XR_PS:
493           cairo_surface_show_page (xr->dest_surface);
494           break;
495
496         case XR_SVG:
497           {
498             cairo_surface_t *svg = cairo_svg_surface_create (
499               file_name, paper[H], paper[V]);
500             xr_copy_surface (svg, xr->drawing_surface, 0.0, 0.0);
501             xr_report_error (cairo_surface_status (svg), file_name);
502             cairo_surface_destroy (svg);
503           }
504           break;
505
506         case XR_PNG:
507           xr_report_error (cairo_surface_write_to_png (xr->drawing_surface,
508                                                        file_name), file_name);
509           cairo_surface_destroy (xr->drawing_surface);
510           xr->drawing_surface = cairo_image_surface_create (
511             CAIRO_FORMAT_ARGB32, paper[H], paper[V]);
512           break;
513         }
514     }
515
516   if (file_name != xr->driver.name)
517     free (file_name);
518 }
519
520 static void
521 xr_destroy (struct output_driver *driver)
522 {
523   struct xr_driver *xr = xr_driver_cast (driver);
524
525   if (xr->pager)
526     xr_finish_page (xr);
527
528   if (xr->drawing_surface && xr->drawing_surface != xr->dest_surface)
529     cairo_surface_destroy (xr->drawing_surface);
530   if (xr->dest_surface)
531     {
532       cairo_surface_finish (xr->dest_surface);
533       cairo_status_t status = cairo_surface_status (xr->dest_surface);
534       if (status != CAIRO_STATUS_SUCCESS)
535         fprintf (stderr,  _("error drawing output for %s driver: %s"),
536                  output_driver_get_name (driver),
537                  cairo_status_to_string (status));
538       cairo_surface_destroy (xr->dest_surface);
539     }
540
541   xr_pager_destroy (xr->pager);
542   xr_page_style_unref (xr->page_style);
543   xr_fsm_style_unref (xr->fsm_style);
544   free (xr);
545 }
546
547 static void
548 xr_update_page_setup (struct output_driver *driver,
549                       const struct page_setup *ps)
550 {
551   struct xr_driver *xr = xr_driver_cast (driver);
552
553   const double scale = 72 * XR_POINT;
554
555   int swap = ps->orientation == PAGE_LANDSCAPE;
556   enum table_axis h = H ^ swap;
557   enum table_axis v = V ^ swap;
558
559   xr_page_style_unref (xr->page_style);
560   xr->page_style = xmalloc (sizeof *xr->page_style);
561   *xr->page_style = (struct xr_page_style) {
562     .ref_cnt = 1,
563
564     .margins = {
565       [H] = { ps->margins[h][0] * scale, ps->margins[h][1] * scale },
566       [V] = { ps->margins[v][0] * scale, ps->margins[v][1] * scale },
567     },
568
569     .initial_page_number = ps->initial_page_number,
570     .object_spacing = ps->object_spacing * 72 * XR_POINT,
571   };
572   for (size_t i = 0; i < 2; i++)
573     page_heading_copy (&xr->page_style->headings[i], &ps->headings[i]);
574
575   struct xr_fsm_style *old_fs = xr->fsm_style;
576   xr->fsm_style = xmalloc (sizeof *xr->fsm_style);
577   *xr->fsm_style = (struct xr_fsm_style) {
578     .ref_cnt = 1,
579     .size = { [H] = ps->paper[H] * scale, [V] = ps->paper[V] * scale },
580     .min_break = {
581       [H] = ps->paper[H] * scale / 2,
582       [V] = ps->paper[V] * scale / 2,
583     },
584     .fg = old_fs->fg,
585     .use_system_colors = old_fs->use_system_colors,
586     .font_resolution = old_fs->font_resolution,
587   };
588   for (size_t i = 0; i < XR_N_FONTS; i++)
589     xr->fsm_style->fonts[i] = pango_font_description_copy (old_fs->fonts[i]);
590   xr_fsm_style_unref (old_fs);
591
592   xr_set_surface_size (xr->dest_surface, xr->output_type, ps->paper[H] * 72.0,
593                        ps->paper[V] * 72.0);
594 }
595
596 static void
597 xr_submit (struct output_driver *driver, const struct output_item *output_item)
598 {
599   struct xr_driver *xr = xr_driver_cast (driver);
600
601   if (is_page_setup_item (output_item))
602     {
603       if (!xr->pager)
604         xr_update_page_setup (driver,
605                               to_page_setup_item (output_item)->page_setup);
606       return;
607     }
608
609   if (!xr->pager)
610     {
611       xr->pager = xr_pager_create (xr->page_style, xr->fsm_style);
612       xr_pager_add_page (xr->pager, cairo_create (xr->drawing_surface));
613     }
614
615   xr_pager_add_item (xr->pager, output_item);
616   while (xr_pager_needs_new_page (xr->pager))
617     {
618       xr_finish_page (xr);
619       xr_pager_add_page (xr->pager, cairo_create (xr->drawing_surface));
620     }
621 }
622 \f
623 struct output_driver_factory pdf_driver_factory =
624   { "pdf", "pspp.pdf", xr_pdf_create };
625 struct output_driver_factory ps_driver_factory =
626   { "ps", "pspp.ps", xr_ps_create };
627 struct output_driver_factory svg_driver_factory =
628   { "svg", "pspp.svg", xr_svg_create };
629 struct output_driver_factory png_driver_factory =
630   { "png", "pspp.png", xr_png_create };
631
632 static const struct output_driver_class cairo_driver_class =
633 {
634   "cairo",
635   xr_destroy,
636   xr_submit,
637   NULL,
638 };