output: Replace OUTPUT_ITEM_PAGE_SETUP by a new driver function.
[pspp] / src / output / cairo.c
index c3cef5c62ade2f032f7d5ec9b63a559e04fa3d90..dc5cf94ca3cbf1115835b68d19bcd4993c4cce78 100644 (file)
 
 #include <config.h>
 
-#include "output/cairo.h"
-
 #include "libpspp/assertion.h"
 #include "libpspp/cast.h"
-#include "libpspp/hash-functions.h"
 #include "libpspp/message.h"
-#include "libpspp/pool.h"
-#include "libpspp/start-date.h"
 #include "libpspp/str.h"
 #include "libpspp/string-map.h"
-#include "libpspp/version.h"
 #include "data/file-handle-def.h"
-#include "output/cairo-chart.h"
 #include "output/cairo-fsm.h"
-#include "output/chart-item-provider.h"
+#include "output/cairo-pager.h"
 #include "output/driver-provider.h"
-#include "output/group-item.h"
-#include "output/message-item.h"
 #include "output/options.h"
-#include "output/page-eject-item.h"
-#include "output/page-setup-item.h"
-#include "output/render.h"
-#include "output/table-item.h"
+#include "output/output-item.h"
 #include "output/table.h"
-#include "output/text-item.h"
 
 #include <cairo/cairo-pdf.h>
 #include <cairo/cairo-ps.h>
 #include <inttypes.h>
 #include <math.h>
 #include <pango/pango-font.h>
-#include <pango/pango-layout.h>
-#include <pango/pango.h>
-#include <pango/pangocairo.h>
 #include <stdlib.h>
 
-#include "gl/c-ctype.h"
 #include "gl/c-strcase.h"
-#include "gl/intprops.h"
-#include "gl/minmax.h"
 #include "gl/xalloc.h"
 
 #include "gettext.h"
@@ -79,16 +60,13 @@ xr_to_pt (int x)
   return x / (double) XR_POINT;
 }
 
-/* Dimensions for drawing lines in tables. */
-#define XR_LINE_WIDTH (XR_POINT / 2) /* Width of an ordinary line. */
-#define XR_LINE_SPACE XR_POINT       /* Space between double lines. */
-
 /* Output types. */
 enum xr_output_type
   {
     XR_PDF,
     XR_PS,
-    XR_SVG
+    XR_SVG,
+    XR_PNG
   };
 
 /* Cairo output driver. */
@@ -96,39 +74,51 @@ struct xr_driver
   {
     struct output_driver driver;
 
-    /* User parameters. */
-    PangoFontDescription *fonts[XR_N_FONTS];
-
-    /* Measurements all in inch/(72 * XR_POINT). */
-    int size[TABLE_N_AXES];     /* Page size with margins subtracted. */
-    int margins[TABLE_N_AXES][2]; /* Margins. */
-    int min_break[TABLE_N_AXES]; /* Min cell size to break across pages. */
-    int object_spacing;         /* Space between output objects. */
-
-    struct cell_color bg;       /* Background color */
-    struct cell_color fg;       /* Foreground color */
-    bool transparent;           /* true -> do not render background */
-    bool systemcolors;          /* true -> do not change colors     */
-
-    int initial_page_number;
-
-    struct page_heading headings[2]; /* Top and bottom headings. */
-    int headings_height[2];
-
-    /* Internal state. */
-    struct xr_fsm_style *style;
-    int char_width, char_height;
-    cairo_t *cairo;
-    cairo_surface_t *surface;
-    int page_number;           /* Current page number. */
-    int y;
-    struct xr_fsm *fsm;
+    enum xr_output_type output_type;
+    struct xr_fsm_style *fsm_style;
+    struct xr_page_style *page_style;
+    struct xr_pager *pager;
+    bool trim;
+
+    /* This is the surface where we're currently drawing.  It is always
+       nonnull.
+
+       If 'trim' is true, this is a special Cairo "recording surface" that we
+       are using to save output temporarily just to find out the bounding box,
+       then later replay it into the destination surface.
+
+       If 'trim' is false:
+
+         - For output to a PDF or PostScript file, it is the same pointer as
+           'dest_surface'.
+
+         - For output to a PNG file, it is an image surface.
+
+         - For output to an SVG file, it is a recording surface.
+    */
+    cairo_surface_t *drawing_surface;
+
+    /* - For output to a PDF or PostScript file, this is the surface for the
+         PDF or PostScript file where the output is ultimately going.
+
+       - For output to a PNG file, this is NULL, because Cairo has very
+         limited support for PNG.  Cairo can't open a PNG file for writing as
+         a surface, it can only save an existing surface to a PNG file.
+
+       - For output to a SVG file, this is NULL, because Cairo does not
+         permit resizing the SVG page size after creating the file, whereas
+         this driver needs to do that sometimes.  Also, SVG is not multi-page
+         (according to https://wiki.inkscape.org/wiki/index.php/Multipage).
+    */
+    cairo_surface_t *dest_surface;
+
+    /* Used only in file names, for PNG and SVG output where we can only write
+       one page per file. */
+    int page_number;
   };
 
 static const struct output_driver_class cairo_driver_class;
 
-static void xr_driver_destroy_fsm (struct xr_driver *);
-static void xr_driver_run_fsm (struct xr_driver *);
 \f
 /* Output driver basics. */
 
@@ -193,287 +183,134 @@ parse_font_option (struct output_driver *d, struct string_map *options,
   return desc;
 }
 
-static void
-apply_options (struct xr_driver *xr, struct string_map *o)
+static struct xr_driver *
+xr_allocate (const char *name, int device_type,
+             enum xr_output_type output_type, struct string_map *o)
 {
+  struct xr_driver *xr = xzalloc (sizeof *xr);
   struct output_driver *d = &xr->driver;
 
-  /* In inch/72000 units used by parse_paper_size() and parse_dimension(). */
+  output_driver_init (d, &cairo_driver_class, name, device_type);
+  xr->output_type = output_type;
 
   /* Scale factor from inch/72000 to inch/(72 * XR_POINT). */
   const double scale = XR_POINT / 1000.;
 
-  for (int i = 0; i < XR_N_FONTS; i++)
-    if (xr->fonts[i] != NULL)
-      pango_font_description_free (xr->fonts[i]);
-
-  int font_size = parse_int (opt (d, o, "font-size", "10000"), 1000, 1000000);
-  xr->fonts[XR_FONT_FIXED] = parse_font_option
-    (d, o, "fixed-font", "monospace", font_size, false, false);
-  xr->fonts[XR_FONT_PROPORTIONAL] = parse_font_option (
-    d, o, "prop-font", "sans serif", font_size, false, false);
-
-  xr->fg = parse_color (opt (d, o, "foreground-color", "#000000000000"));
-  xr->bg = parse_color (opt (d, o, "background-color", "#FFFFFFFFFFFF"));
-
-  xr->transparent = parse_boolean (opt (d, o, "transparent", "false"));
-  xr->systemcolors = parse_boolean (opt (d, o, "systemcolors", "false"));
-
-  /* Get dimensions.  */
   int paper[TABLE_N_AXES];
   parse_paper_size (opt (d, o, "paper-size", ""), &paper[H], &paper[V]);
+  for (int a = 0; a < TABLE_N_AXES; a++)
+    paper[a] *= scale;
 
   int margins[TABLE_N_AXES][2];
-  margins[H][0] = parse_dimension (opt (d, o, "left-margin", ".5in"));
-  margins[H][1] = parse_dimension (opt (d, o, "right-margin", ".5in"));
-  margins[V][0] = parse_dimension (opt (d, o, "top-margin", ".5in"));
-  margins[V][1] = parse_dimension (opt (d, o, "bottom-margin", ".5in"));
+  margins[H][0] = parse_dimension (opt (d, o, "left-margin", ".5in")) * scale;
+  margins[H][1] = parse_dimension (opt (d, o, "right-margin", ".5in")) * scale;
+  margins[V][0] = parse_dimension (opt (d, o, "top-margin", ".5in")) * scale;
+  margins[V][1] = parse_dimension (opt (d, o, "bottom-margin", ".5in")) * scale;
+
+  int size[TABLE_N_AXES];
+  for (int a = 0; a < TABLE_N_AXES; a++)
+    size[a] = paper[a] - margins[a][0] - margins[a][1];
 
   int min_break[TABLE_N_AXES];
   min_break[H] = parse_dimension (opt (d, o, "min-hbreak", NULL)) * scale;
   min_break[V] = parse_dimension (opt (d, o, "min-vbreak", NULL)) * scale;
-
-  int object_spacing = (parse_dimension (opt (d, o, "object-spacing", NULL))
-                        * scale);
-
-  /* Convert to inch/(XR_POINT * 72). */
   for (int a = 0; a < TABLE_N_AXES; a++)
-    {
-      for (int i = 0; i < 2; i++)
-        xr->margins[a][i] = margins[a][i] * scale;
-      xr->size[a] = (paper[a] - margins[a][0] - margins[a][1]) * scale;
-      xr->min_break[a] = min_break[a] >= 0 ? min_break[a] : xr->size[a] / 2;
-    }
-  xr->object_spacing = object_spacing >= 0 ? object_spacing : XR_POINT * 12;
-
-  /* There are no headings so headings_height can stay 0. */
-}
-
-static struct xr_driver *
-xr_allocate (const char *name, int device_type, struct string_map *o)
-{
-  struct xr_driver *xr = xzalloc (sizeof *xr);
-  struct output_driver *d = &xr->driver;
-
-  output_driver_init (d, &cairo_driver_class, name, device_type);
-
-  apply_options (xr, o);
-
-  return xr;
-}
-
-static int
-pango_to_xr (int pango)
-{
-  return (XR_POINT != PANGO_SCALE
-          ? ceil (pango * (1. * XR_POINT / PANGO_SCALE))
-          : pango);
-}
-
-static int
-xr_to_pango (int xr)
-{
-  return (XR_POINT != PANGO_SCALE
-          ? ceil (xr * (1. / XR_POINT * PANGO_SCALE))
-          : xr);
-}
+    if (min_break[a] <= 0)
+      min_break[a] = size[a] / 2;
 
-static void
-xr_measure_fonts (cairo_t *cairo, PangoFontDescription *fonts[XR_N_FONTS],
-                  int *char_width, int *char_height)
-{
-  *char_width = 0;
-  *char_height = 0;
-  for (int i = 0; i < XR_N_FONTS; i++)
-    {
-      PangoContext *context = pango_cairo_create_context (cairo);
-      pango_cairo_context_set_resolution (context, 72.0);
-      PangoLayout *layout = pango_layout_new (context);
-      g_object_unref (context);
-      pango_layout_set_font_description (layout, fonts[i]);
-
-      pango_layout_set_text (layout, "0", 1);
-
-      int cw, ch;
-      pango_layout_get_size (layout, &cw, &ch);
-      *char_width = MAX (*char_width, pango_to_xr (cw));
-      *char_height = MAX (*char_height, pango_to_xr (ch));
-
-      g_object_unref (G_OBJECT (layout));
-    }
-}
-
-static int
-get_layout_height (PangoLayout *layout)
-{
-  int w, h;
-  pango_layout_get_size (layout, &w, &h);
-  return h;
-}
-
-static int
-xr_render_page_heading (cairo_t *cairo, const PangoFontDescription *font,
-                        const struct page_heading *ph, int page_number,
-                        int width, bool draw, int base_y)
-{
-  PangoContext *context = pango_cairo_create_context (cairo);
-  pango_cairo_context_set_resolution (context, 72.0);
-  PangoLayout *layout = pango_layout_new (context);
-  g_object_unref (context);
-
-  pango_layout_set_font_description (layout, font);
-
-  int y = 0;
-  for (size_t i = 0; i < ph->n; i++)
-    {
-      const struct page_paragraph *pp = &ph->paragraphs[i];
-
-      char *markup = output_driver_substitute_heading_vars (pp->markup,
-                                                            page_number);
-      pango_layout_set_markup (layout, markup, -1);
-      free (markup);
-
-      pango_layout_set_alignment (
-        layout,
-        (pp->halign == TABLE_HALIGN_LEFT ? PANGO_ALIGN_LEFT
-         : pp->halign == TABLE_HALIGN_CENTER ? PANGO_ALIGN_CENTER
-         : pp->halign == TABLE_HALIGN_MIXED ? PANGO_ALIGN_LEFT
-         : PANGO_ALIGN_RIGHT));
-      pango_layout_set_width (layout, xr_to_pango (width));
-      if (draw)
-        {
-          cairo_save (cairo);
-          cairo_translate (cairo, 0, xr_to_pt (y + base_y));
-          pango_cairo_show_layout (cairo, layout);
-          cairo_restore (cairo);
-        }
-
-      y += pango_to_xr (get_layout_height (layout));
-    }
+  int font_size = parse_int (opt (d, o, "font-size", "10000"), 1000, 1000000);
+  PangoFontDescription *font = parse_font_option (
+    d, o, "prop-font", "Sans Serif", font_size, false, false);
 
-  g_object_unref (G_OBJECT (layout));
+  struct cell_color fg = parse_color (opt (d, o, "foreground-color", "black"));
 
-  return y;
-}
+  bool systemcolors = parse_boolean (opt (d, o, "systemcolors", "false"));
 
-static int
-xr_measure_headings (cairo_surface_t *surface,
-                     const PangoFontDescription *font,
-                     const struct page_heading headings[2],
-                     int width, int object_spacing, int height[2])
-{
-  cairo_t *cairo = cairo_create (surface);
-  int total = 0;
-  for (int i = 0; i < 2; i++)
-    {
-      int h = xr_render_page_heading (cairo, font, &headings[i], -1,
-                                      width, false, 0);
+  int object_spacing
+    = parse_dimension (opt (d, o, "object-spacing", NULL)) * scale;
+  if (object_spacing <= 0)
+    object_spacing = XR_POINT * 12;
 
-      /* If the top heading is nonempty, add some space below it. */
-      if (h && i == 0)
-        h += object_spacing;
+  const char *default_resolution = (output_type == XR_PNG ? "96" : "72");
+  int font_resolution = parse_int (opt (d, o, "font-resolution",
+                                        default_resolution), 10, 1000);
 
-      if (height)
-        height[i] = h;
-      total += h;
-    }
-  cairo_destroy (cairo);
-  return total;
-}
+  xr->trim = parse_boolean (opt (d, o, "trim", "false"));
 
-static bool
-xr_check_fonts (cairo_surface_t *surface,
-                PangoFontDescription *fonts[XR_N_FONTS],
-                int usable_width, int usable_length)
-{
-  cairo_t *cairo = cairo_create (surface);
-  int char_width, char_height;
-  xr_measure_fonts (cairo, fonts, &char_width, &char_height);
-  cairo_destroy (cairo);
-
-  bool ok = true;
-  enum { MIN_WIDTH = 3, MIN_LENGTH = 3 };
-  if (usable_width / char_width < MIN_WIDTH)
-    {
-      msg (ME, _("The defined page is not wide enough to hold at least %d "
-                 "characters in the default font.  In fact, there's only "
-                 "room for %d characters."),
-           MIN_WIDTH, usable_width / char_width);
-      ok = false;
-    }
-  if (usable_length / char_height < MIN_LENGTH)
-    {
-      msg (ME, _("The defined page is not long enough to hold at least %d "
-                 "lines in the default font.  In fact, there's only "
-                 "room for %d lines."),
-           MIN_LENGTH, usable_length / char_height);
-      ok = false;
-    }
-  return ok;
-}
+  /* Cairo 1.16.0 has a bug that causes crashes if outlines are enabled at the
+     same time as trimming:
+     https://lists.cairographics.org/archives/cairo/2020-December/029151.html
+     For now, just disable the outline if trimming is enabled. */
+  bool include_outline
+    = (output_type == XR_PDF
+       && parse_boolean (opt (d, o, "outline", xr->trim ? "false" : "true")));
 
-static void
-xr_set_cairo (struct xr_driver *xr, cairo_t *cairo)
-{
-  xr->cairo = cairo;
+  xr->page_style = xmalloc (sizeof *xr->page_style);
+  *xr->page_style = (struct xr_page_style) {
+    .ref_cnt = 1,
 
-  cairo_set_line_width (xr->cairo, xr_to_pt (XR_LINE_WIDTH));
+    .margins = {
+      [H] = { margins[H][0], margins[H][1], },
+      [V] = { margins[V][0], margins[V][1], },
+    },
 
-  xr_measure_fonts (xr->cairo, xr->fonts, &xr->char_width, &xr->char_height);
+    .initial_page_number = 1,
+    .include_outline = include_outline,
+  };
 
-  if (xr->style == NULL)
-    {
-      xr->style = xmalloc (sizeof *xr->style);
-      *xr->style = (struct xr_fsm_style) {
-        .ref_cnt = 1,
-        .size = { [H] = xr->size[H], [V] = xr->size[V] },
-        .min_break = { [H] = xr->min_break[H], [V] = xr->min_break[V] },
-        .use_system_colors = xr->systemcolors,
-        .transparent = xr->transparent,
-        .font_resolution = 72.0,
-      };
-
-      for (size_t i = 0; i < XR_N_FONTS; i++)
-        xr->style->fonts[i] = pango_font_description_copy (xr->fonts[i]);
-    }
+  xr->fsm_style = xmalloc (sizeof *xr->fsm_style);
+  *xr->fsm_style = (struct xr_fsm_style) {
+    .ref_cnt = 1,
+    .size = { [H] = size[H], [V] = size[V] },
+    .min_break = { [H] = min_break[H], [V] = min_break[V] },
+    .font = font,
+    .fg = fg,
+    .use_system_colors = systemcolors,
+    .object_spacing = object_spacing,
+    .font_resolution = font_resolution,
+  };
 
-  if (!xr->systemcolors)
-    cairo_set_source_rgb (xr->cairo,
-                         xr->fg.r / 255.0, xr->fg.g / 255.0, xr->fg.b / 255.0);
+  return xr;
 }
 
 static struct output_driver *
 xr_create (struct file_handle *fh, enum settings_output_devices device_type,
-           struct string_map *o, enum xr_output_type file_type)
+           struct string_map *o, enum xr_output_type output_type)
 {
   const char *file_name = fh_get_file_name (fh);
-  struct xr_driver *xr = xr_allocate (file_name, device_type, o);
+  struct xr_driver *xr = xr_allocate (file_name, device_type, output_type, o);
 
-  double paper_pt[TABLE_N_AXES];
+  double paper[TABLE_N_AXES];
   for (int a = 0; a < TABLE_N_AXES; a++)
-    paper_pt[a] = xr_to_pt (xr->size[a]
-                            + xr->margins[a][0] + xr->margins[a][1]);
-  if (file_type == XR_PDF)
-    xr->surface = cairo_pdf_surface_create (file_name,
-                                            paper_pt[H], paper_pt[V]);
-  else if (file_type == XR_PS)
-    xr->surface = cairo_ps_surface_create (file_name, paper_pt[H], paper_pt[V]);
-  else if (file_type == XR_SVG)
-    xr->surface = cairo_svg_surface_create (file_name,
-                                            paper_pt[H], paper_pt[V]);
-  else
-    NOT_REACHED ();
-
-  cairo_status_t status = cairo_surface_status (xr->surface);
-  if (status != CAIRO_STATUS_SUCCESS)
+    paper[a] = xr_to_pt (xr_page_style_paper_size (xr->page_style,
+                                                   xr->fsm_style, a));
+
+  xr->dest_surface
+    = (output_type == XR_PDF
+       ? cairo_pdf_surface_create (file_name, paper[H], paper[V])
+       : output_type == XR_PS
+       ? cairo_ps_surface_create (file_name, paper[H], paper[V])
+       : NULL);
+  if (xr->dest_surface)
     {
-      msg (ME, _("error opening output file `%s': %s"),
-           file_name, cairo_status_to_string (status));
-      goto error;
+      cairo_status_t status = cairo_surface_status (xr->dest_surface);
+      if (status != CAIRO_STATUS_SUCCESS)
+        {
+          msg (ME, _("error opening output file `%s': %s"),
+               file_name, cairo_status_to_string (status));
+          goto error;
+        }
     }
 
-  if (!xr_check_fonts (xr->surface, xr->fonts, xr->size[H], xr->size[V]))
-    goto error;
+  xr->drawing_surface
+    = (xr->trim || output_type == XR_SVG
+       ? cairo_recording_surface_create (CAIRO_CONTENT_COLOR_ALPHA,
+                                         &(cairo_rectangle_t) {
+                                           .width = paper[H],
+                                           .height = paper[V] })
+       : output_type == XR_PNG
+       ? cairo_image_surface_create (CAIRO_FORMAT_ARGB32, paper[H], paper[V])
+       : xr->dest_surface);
 
   fh_unref (fh);
   return &xr->driver;
@@ -505,208 +342,292 @@ xr_svg_create (struct file_handle *fh, enum settings_output_devices device_type,
   return xr_create (fh, device_type, o, XR_SVG);
 }
 
-static void
-xr_destroy (struct output_driver *driver)
+static struct output_driver *
+xr_png_create (struct file_handle *fh, enum settings_output_devices device_type,
+               struct string_map *o)
 {
-  struct xr_driver *xr = xr_driver_cast (driver);
-  size_t i;
-
-  xr_driver_destroy_fsm (xr);
+  return xr_create (fh, device_type, o, XR_PNG);
+}
 
-  if (xr->cairo != NULL)
+static void
+xr_set_surface_size (cairo_surface_t *surface, enum xr_output_type output_type,
+                     double width, double height)
+{
+  switch (output_type)
     {
-      cairo_surface_finish (xr->surface);
-      cairo_status_t status = cairo_status (xr->cairo);
-      if (status != CAIRO_STATUS_SUCCESS)
-        fprintf (stderr,  _("error drawing output for %s driver: %s"),
-                 output_driver_get_name (driver),
-                 cairo_status_to_string (status));
-      cairo_surface_destroy (xr->surface);
+    case XR_PDF:
+      cairo_pdf_surface_set_size (surface, width, height);
+      break;
 
-      cairo_destroy (xr->cairo);
+    case XR_PS:
+      cairo_ps_surface_set_size (surface, width, height);
+      break;
+
+    case XR_SVG:
+    case XR_PNG:
+      NOT_REACHED ();
     }
+}
 
-  for (i = 0; i < XR_N_FONTS; i++)
-    if (xr->fonts[i] != NULL)
-      pango_font_description_free (xr->fonts[i]);
+static void
+xr_copy_surface (cairo_surface_t *dst, cairo_surface_t *src,
+                 double x, double y)
+{
+  cairo_t *cr = cairo_create (dst);
+  cairo_set_source_surface (cr, src, x, y);
+  cairo_paint (cr);
+  cairo_destroy (cr);
+}
 
-  xr_fsm_style_unref (xr->style);
-  free (xr);
+static void
+clear_rectangle (cairo_surface_t *surface,
+                 double x0, double y0, double x1, double y1)
+{
+  cairo_t *cr = cairo_create (surface);
+  cairo_set_source_rgb (cr, 1, 1, 1);
+  cairo_new_path (cr);
+  cairo_rectangle (cr, x0, y0, x1 - x0, y1 - y0);
+  cairo_fill (cr);
+  cairo_destroy (cr);
 }
 
 static void
-xr_flush (struct output_driver *driver)
+xr_report_error (cairo_status_t status, const char *file_name)
 {
-  struct xr_driver *xr = xr_driver_cast (driver);
-
-  cairo_surface_flush (cairo_get_target (xr->cairo));
+  if (status != CAIRO_STATUS_SUCCESS)
+    fprintf (stderr,  "%s: %s\n", file_name, cairo_status_to_string (status));
 }
 
 static void
-xr_update_page_setup (struct output_driver *driver,
-                      const struct page_setup *ps)
+xr_finish_page (struct xr_driver *xr)
 {
-  struct xr_driver *xr = xr_driver_cast (driver);
+  xr_pager_finish_page (xr->pager);
 
-  xr->initial_page_number = ps->initial_page_number;
-  xr->object_spacing = ps->object_spacing * 72 * XR_POINT;
+  /* For 'trim' true:
 
-  if (xr->cairo)
-    return;
+    - If the destination is PDF or PostScript, set the dest surface size, copy
+      ink extent, show_page.
 
-  int size[TABLE_N_AXES];
-  for (int a = 0; a < TABLE_N_AXES; a++)
-    {
-      double total_margin = ps->margins[a][0] + ps->margins[a][1];
-      size[a] = (ps->paper[a] - total_margin) * 72 * XR_POINT;
-    }
+    - If the destination is PNG, create image surface, copy ink extent,
+      cairo_surface_write_to_png(), destroy image surface.
 
-  int headings_height[2];
-  size[V] -= xr_measure_headings (
-    xr->surface, xr->fonts[XR_FONT_PROPORTIONAL], ps->headings,
-    size[H], xr->object_spacing, headings_height);
+    - If the destination is SVG, create svg surface, copy ink extent, close.
 
-  int swap = ps->orientation == PAGE_LANDSCAPE;
-  enum table_axis h = H ^ swap;
-  enum table_axis v = V ^ swap;
-  if (!xr_check_fonts (xr->surface, xr->fonts, size[h], size[v]))
-    return;
+    then destroy drawing_surface and make a new one.
 
-  for (int i = 0; i < 2; i++)
-    {
-      page_heading_uninit (&xr->headings[i]);
-      page_heading_copy (&xr->headings[i], &ps->headings[i]);
-      xr->headings_height[i] = headings_height[i];
-    }
+    For 'trim' false:
+
+    - If the destination is PDF or PostScript, show_page.
 
+    - If the destination is PNG, cairo_surface_write_to_png(), destroy image
+      surface, create new image surface.
+
+    - If the destination is SVG, create svg surface, copy whole thing, close.
+
+    */
+  double paper[TABLE_N_AXES];
   for (int a = 0; a < TABLE_N_AXES; a++)
+    paper[a] = xr_to_pt (xr_page_style_paper_size (
+                           xr->page_style, xr->fsm_style, a));
+
+  xr->page_number++;
+  char *file_name = (xr->page_number > 1
+                     ? xasprintf ("%s-%d", xr->driver.name, xr->page_number)
+                     : xr->driver.name);
+
+  if (xr->trim)
+    {
+      /* Get the bounding box for the drawing surface. */
+      double ofs[TABLE_N_AXES], size[TABLE_N_AXES];
+      cairo_recording_surface_ink_extents (xr->drawing_surface,
+                                           &ofs[H], &ofs[V],
+                                           &size[H], &size[V]);
+      const int (*margins)[2] = xr->page_style->margins;
+      for (int a = 0; a < TABLE_N_AXES; a++)
+        {
+          double scale = XR_POINT;
+          size[a] += (margins[a][0] + margins[a][1]) / scale;
+          ofs[a] = -ofs[a] + margins[a][0] / scale;
+        }
+
+      switch (xr->output_type)
+        {
+        case XR_PDF:
+        case XR_PS:
+          xr_set_surface_size (xr->dest_surface, xr->output_type,
+                               size[H], size[V]);
+          xr_copy_surface (xr->dest_surface, xr->drawing_surface,
+                           ofs[H], ofs[V]);
+          cairo_surface_show_page (xr->dest_surface);
+          break;
+
+        case XR_SVG:
+          {
+            cairo_surface_t *svg = cairo_svg_surface_create (
+              file_name, size[H], size[V]);
+            xr_copy_surface (svg, xr->drawing_surface, ofs[H], ofs[V]);
+            xr_report_error (cairo_surface_status (svg), file_name);
+            cairo_surface_destroy (svg);
+          }
+          break;
+
+        case XR_PNG:
+          {
+            cairo_surface_t *png = cairo_image_surface_create (
+              CAIRO_FORMAT_ARGB32, size[H], size[V]);
+            clear_rectangle (png, 0, 0, size[H], size[V]);
+            xr_copy_surface (png, xr->drawing_surface, ofs[H], ofs[V]);
+            xr_report_error (cairo_surface_write_to_png (png, file_name),
+                             file_name);
+            cairo_surface_destroy (png);
+          }
+          break;
+        }
+
+      /* Destroy the recording surface and create a fresh one of the same
+         size. */
+      cairo_surface_destroy (xr->drawing_surface);
+      xr->drawing_surface = cairo_recording_surface_create (
+        CAIRO_CONTENT_COLOR_ALPHA,
+        &(cairo_rectangle_t) { .width = paper[H], .height = paper[V] });
+    }
+  else
     {
-      xr->size[a] = size[a ^ swap];
-      for (int i = 0; i < 2; i++)
-        xr->margins[a][i] = ps->margins[a ^ swap][i] * 72 * XR_POINT;
+      switch (xr->output_type)
+        {
+        case XR_PDF:
+        case XR_PS:
+          cairo_surface_show_page (xr->dest_surface);
+          break;
+
+        case XR_SVG:
+          {
+            cairo_surface_t *svg = cairo_svg_surface_create (
+              file_name, paper[H], paper[V]);
+            xr_copy_surface (svg, xr->drawing_surface, 0.0, 0.0);
+            xr_report_error (cairo_surface_status (svg), file_name);
+            cairo_surface_destroy (svg);
+          }
+          break;
+
+        case XR_PNG:
+          xr_report_error (cairo_surface_write_to_png (xr->drawing_surface,
+                                                       file_name), file_name);
+          cairo_surface_destroy (xr->drawing_surface);
+          xr->drawing_surface = cairo_image_surface_create (
+            CAIRO_FORMAT_ARGB32, paper[H], paper[V]);
+          break;
+        }
     }
-  cairo_pdf_surface_set_size (xr->surface,
-                              ps->paper[h] * 72.0, ps->paper[v] * 72.0);
+
+  if (file_name != xr->driver.name)
+    free (file_name);
 }
 
 static void
-xr_submit (struct output_driver *driver, const struct output_item *output_item)
+xr_destroy (struct output_driver *driver)
 {
   struct xr_driver *xr = xr_driver_cast (driver);
 
-  if (is_page_setup_item (output_item))
-    {
-      xr_update_page_setup (driver,
-                            to_page_setup_item (output_item)->page_setup);
-      return;
-    }
+  if (xr->pager)
+    xr_finish_page (xr);
 
-  if (!xr->cairo)
-    {
-      xr->page_number = xr->initial_page_number - 1;
-      xr_set_cairo (xr, cairo_create (xr->surface));
-      cairo_save (xr->cairo);
-      xr_driver_next_page (xr, xr->cairo);
-    }
+  xr_pager_destroy (xr->pager);
 
-  xr_driver_output_item (xr, output_item);
-  while (xr_driver_need_new_page (xr))
+  if (xr->drawing_surface && xr->drawing_surface != xr->dest_surface)
+    cairo_surface_destroy (xr->drawing_surface);
+  if (xr->dest_surface)
     {
-      cairo_restore (xr->cairo);
-      cairo_show_page (xr->cairo);
-      cairo_save (xr->cairo);
-      xr_driver_next_page (xr, xr->cairo);
+      cairo_surface_finish (xr->dest_surface);
+      cairo_status_t status = cairo_surface_status (xr->dest_surface);
+      if (status != CAIRO_STATUS_SUCCESS)
+        fprintf (stderr,  _("error drawing output for %s driver: %s\n"),
+                 output_driver_get_name (driver),
+                 cairo_status_to_string (status));
+      cairo_surface_destroy (xr->dest_surface);
     }
-}
-\f
-/* Functions for rendering a series of output items to a series of Cairo
-   contexts, with pagination.
 
-   Used by PSPPIRE for printing, and by the basic Cairo output driver above as
-   its underlying implementation.
-
-   See the big comment in cairo.h for intended usage. */
+  xr_page_style_unref (xr->page_style);
+  xr_fsm_style_unref (xr->fsm_style);
+  free (xr);
+}
 
-/* Gives new page CAIRO to XR for output. */
-void
-xr_driver_next_page (struct xr_driver *xr, cairo_t *cairo)
+static void
+xr_update_page_setup (struct output_driver *driver,
+                      const struct page_setup *setup)
 {
-  if (!xr->transparent)
-    {
-      cairo_save (cairo);
-      cairo_set_source_rgb (cairo,
-                           xr->bg.r / 255.0, xr->bg.g / 255.0, xr->bg.b / 255.0);
-      cairo_rectangle (cairo, 0, 0, xr->size[H], xr->size[V]);
-      cairo_fill (cairo);
-      cairo_restore (cairo);
-    }
-  cairo_translate (cairo,
-                   xr_to_pt (xr->margins[H][0]),
-                   xr_to_pt (xr->margins[V][0] + xr->headings_height[0]));
+  struct xr_driver *xr = xr_driver_cast (driver);
 
-  xr->page_number++;
-  xr->cairo = cairo;
-  xr->y = 0;
+  const double scale = 72 * XR_POINT;
 
-  xr_render_page_heading (xr->cairo, xr->fonts[XR_FONT_PROPORTIONAL],
-                          &xr->headings[0], xr->page_number, xr->size[H], true,
-                          -xr->headings_height[0]);
-  xr_render_page_heading (xr->cairo, xr->fonts[XR_FONT_PROPORTIONAL],
-                          &xr->headings[1], xr->page_number, xr->size[H], true,
-                          xr->size[V]);
+  int swap = setup->orientation == PAGE_LANDSCAPE;
+  enum table_axis h = H ^ swap;
+  enum table_axis v = V ^ swap;
 
-  xr_driver_run_fsm (xr);
-}
+  struct xr_page_style *old_ps = xr->page_style;
+  xr->page_style = xmalloc (sizeof *xr->page_style);
+  *xr->page_style = (struct xr_page_style) {
+    .ref_cnt = 1,
 
-/* Start rendering OUTPUT_ITEM to XR.  Only valid if XR is not in the middle of
-   rendering a previous output item, that is, only if xr_driver_need_new_page()
-   returns false. */
-void
-xr_driver_output_item (struct xr_driver *xr,
-                       const struct output_item *output_item)
-{
-  assert (xr->fsm == NULL);
-  xr->fsm = xr_fsm_create (output_item, xr->style, xr->cairo);
-  xr_driver_run_fsm (xr);
-}
+    .margins = {
+      [H] = { setup->margins[h][0] * scale, setup->margins[h][1] * scale },
+      [V] = { setup->margins[v][0] * scale, setup->margins[v][1] * scale },
+    },
 
-/* Returns true if XR is in the middle of rendering an output item and needs a
-   new page to be appended using xr_driver_next_page() to make progress,
-   otherwise false. */
-bool
-xr_driver_need_new_page (const struct xr_driver *xr)
-{
-  return xr->fsm != NULL;
-}
+    .initial_page_number = setup->initial_page_number,
+    .include_outline = old_ps->include_outline,
+  };
+  for (size_t i = 0; i < 2; i++)
+    page_heading_copy (&xr->page_style->headings[i], &setup->headings[i]);
+  xr_page_style_unref (old_ps);
+
+  struct xr_fsm_style *old_fs = xr->fsm_style;
+  xr->fsm_style = xmalloc (sizeof *xr->fsm_style);
+  *xr->fsm_style = (struct xr_fsm_style) {
+    .ref_cnt = 1,
+    .size = { [H] = setup->paper[H] * scale, [V] = setup->paper[V] * scale },
+    .min_break = {
+      [H] = setup->paper[H] * scale / 2,
+      [V] = setup->paper[V] * scale / 2,
+    },
+    .font = pango_font_description_copy (old_fs->font),
+    .fg = old_fs->fg,
+    .use_system_colors = old_fs->use_system_colors,
+    .object_spacing = setup->object_spacing * 72 * XR_POINT,
+    .font_resolution = old_fs->font_resolution,
+  };
+  xr_fsm_style_unref (old_fs);
 
-/* Returns true if the current page doesn't have any content yet. */
-bool
-xr_driver_is_page_blank (const struct xr_driver *xr)
-{
-  return xr->y == 0;
+  xr_set_surface_size (xr->dest_surface, xr->output_type,
+                       setup->paper[H] * 72.0, setup->paper[V] * 72.0);
 }
 
 static void
-xr_driver_destroy_fsm (struct xr_driver *xr)
+xr_submit (struct output_driver *driver, const struct output_item *item)
 {
-  xr_fsm_destroy (xr->fsm);
-  xr->fsm = NULL;
+  struct xr_driver *xr = xr_driver_cast (driver);
+
+  if (!xr->pager)
+    {
+      xr->pager = xr_pager_create (xr->page_style, xr->fsm_style);
+      xr_pager_add_page (xr->pager, cairo_create (xr->drawing_surface));
+    }
+
+  xr_pager_add_item (xr->pager, item);
+  while (xr_pager_needs_new_page (xr->pager))
+    {
+      xr_finish_page (xr);
+      xr_pager_add_page (xr->pager, cairo_create (xr->drawing_surface));
+    }
 }
 
 static void
-xr_driver_run_fsm (struct xr_driver *xr)
+xr_setup (struct output_driver *driver, const struct page_setup *ps)
 {
-  if (xr->fsm != NULL)
-    {
-      cairo_save (xr->cairo);
-      cairo_translate (xr->cairo, 0, xr_to_pt (xr->y));
-      int used = xr_fsm_draw_slice (xr->fsm, xr->cairo, xr->size[V] - xr->y);
-      xr->y += used;
-      cairo_restore (xr->cairo);
-
-      if (xr_fsm_is_empty (xr->fsm))
-        xr_driver_destroy_fsm (xr);
-    }
+  struct xr_driver *xr = xr_driver_cast (driver);
+
+  if (!xr->pager)
+    xr_update_page_setup (driver, ps);
 }
 \f
 struct output_driver_factory pdf_driver_factory =
@@ -715,31 +636,14 @@ struct output_driver_factory ps_driver_factory =
   { "ps", "pspp.ps", xr_ps_create };
 struct output_driver_factory svg_driver_factory =
   { "svg", "pspp.svg", xr_svg_create };
+struct output_driver_factory png_driver_factory =
+  { "png", "pspp.png", xr_png_create };
 
 static const struct output_driver_class cairo_driver_class =
 {
-  "cairo",
-  xr_destroy,
-  xr_submit,
-  xr_flush,
+  .name = "cairo",
+  .destroy = xr_destroy,
+  .submit = xr_submit,
+  .setup = xr_setup,
+  .handles_groups = true,
 };
-\f
-struct xr_driver *
-xr_driver_create (cairo_t *cairo, struct string_map *options)
-{
-  struct xr_driver *xr = xr_allocate ("cairo", 0, options);
-  xr_set_cairo (xr, cairo);
-  return xr;
-}
-
-/* Destroy XR, which should have been created with xr_driver_create().  Any
-   cairo_t added to XR is not destroyed, because it is owned by the client. */
-void
-xr_driver_destroy (struct xr_driver *xr)
-{
-  if (xr != NULL)
-    {
-      xr->cairo = NULL;
-      output_driver_destroy (&xr->driver);
-    }
-}