src/output/cairo.c (xr_chart_render): New variable chart_height
[pspp] / src / output / cairo.c
index 048ac7e9cd057a5600cb5186770edbaf11a0792d..8cb504d2dd0e877309fb916a241e69439de031bc 100644 (file)
@@ -1,5 +1,5 @@
 /* PSPP - a program for statistical analysis.
-   Copyright (C) 2009 Free Software Foundation, Inc.
+   Copyright (C) 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016 Free Software Foundation, Inc.
 
    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
 
 #include <config.h>
 
-#include <libpspp/assertion.h>
-#include <libpspp/start-date.h>
-#include <libpspp/version.h>
-#include <output/afm.h>
-#include <output/chart.h>
-#include <output/manager.h>
-#include <output/output.h>
+#include "output/cairo.h"
+
+#include "libpspp/assertion.h"
+#include "libpspp/cast.h"
+#include "libpspp/message.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/chart-item-provider.h"
+#include "output/charts/boxplot.h"
+#include "output/charts/np-plot.h"
+#include "output/charts/piechart.h"
+#include "output/charts/barchart.h"
+#include "output/charts/plot-hist.h"
+#include "output/charts/roc-chart.h"
+#include "output/charts/spreadlevel-plot.h"
+#include "output/charts/scree.h"
+#include "output/charts/scatterplot.h"
+#include "output/driver-provider.h"
+#include "output/message-item.h"
+#include "output/options.h"
+#include "output/render.h"
+#include "output/tab.h"
+#include "output/table-item.h"
+#include "output/table.h"
+#include "output/text-item.h"
 
 #include <cairo/cairo-pdf.h>
 #include <cairo/cairo-ps.h>
 #include <cairo/cairo-svg.h>
 #include <cairo/cairo.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 "error.h"
-#include "intprops.h"
-#include "minmax.h"
-#include "xalloc.h"
+#include "gl/intprops.h"
+#include "gl/minmax.h"
+#include "gl/xalloc.h"
 
 #include "gettext.h"
 #define _(msgid) gettext (msgid)
 
-/* Cairo driver options: (defaults listed first)
+/* This file uses TABLE_HORZ and TABLE_VERT enough to warrant abbreviating. */
+#define H TABLE_HORZ
+#define V TABLE_VERT
 
-   output-file="pspp.pdf"
-   output-type=pdf|ps|png|svg
-   paper-size=letter (see "papersize" file)
-   orientation=portrait|landscape
-   headers=on|off
-
-   left-margin=0.5in
-   right-margin=0.5in
-   top-margin=0.5in
-   bottom-margin=0.5in
-
-   prop-font=Times-Roman
-   emph-font=Times-Italic
-   fixed-font=Courier
-   font-size=10000
-
-   line-gutter=1pt
-   line-spacing=1pt
-   line-width=0.5pt
- */
-
-/* Measurements as we present to the rest of PSPP. */
+/* The unit used for internal measurements is inch/(72 * XR_POINT). */
 #define XR_POINT PANGO_SCALE
-#define XR_INCH (XR_POINT * 72)
 
 /* Conversions to and from points. */
 static double
@@ -76,12 +78,6 @@ xr_to_pt (int x)
   return x / (double) XR_POINT;
 }
 
-static int
-pt_to_xr (double x)
-{
-  return x * XR_POINT + 0.5;
-}
-
 /* Output types. */
 enum xr_output_type
   {
@@ -90,375 +86,580 @@ enum xr_output_type
     XR_SVG
   };
 
+/* Cairo fonts. */
+enum xr_font_type
+  {
+    XR_FONT_PROPORTIONAL,
+    XR_FONT_EMPHASIS,
+    XR_FONT_FIXED,
+    XR_FONT_MARKER,
+    XR_N_FONTS
+  };
+
 /* A font for use with Cairo. */
 struct xr_font
   {
-    char *string;
     PangoFontDescription *desc;
     PangoLayout *layout;
-    PangoFontMetrics *metrics;
   };
 
-/* Cairo output driver extension record. */
-struct xr_driver_ext
+/* An output item whose rendering is in progress. */
+struct xr_render_fsm
   {
-    char *file_name;            /* Output file name. */
-    enum xr_output_type file_type; /* Type of output file. */
-    cairo_t *cairo;
+    /* Renders as much of itself as it can on the current page.  Returns true
+       if rendering is complete, false if the output item needs another
+       page. */
+    bool (*render) (struct xr_render_fsm *, struct xr_driver *);
 
-    bool draw_headers;          /* Draw headers at top of page? */
-    int page_number;           /* Current page number. */
+    /* Destroys the output item. */
+    void (*destroy) (struct xr_render_fsm *);
+  };
+
+/* Cairo output driver. */
+struct xr_driver
+  {
+    struct output_driver driver;
+
+    /* User parameters. */
+    struct xr_font fonts[XR_N_FONTS];
 
-    bool portrait;              /* Portrait mode? */
-    int paper_width;            /* Width of paper before dropping margins. */
-    int paper_length;           /* Length of paper before dropping margins. */
-    int left_margin;           /* Left margin in XR units. */
-    int right_margin;          /* Right margin in XR units. */
-    int top_margin;            /* Top margin in XR units. */
-    int bottom_margin;         /* Bottom margin in XR units. */
+    int width;                  /* Page width minus margins. */
+    int length;                 /* Page length minus margins and header. */
+
+    int left_margin;            /* Left margin in inch/(72 * XR_POINT). */
+    int right_margin;           /* Right margin in inch/(72 * XR_POINT). */
+    int top_margin;             /* Top margin in inch/(72 * XR_POINT). */
+    int bottom_margin;          /* Bottom margin in inch/(72 * XR_POINT). */
 
     int line_gutter;           /* Space around lines. */
     int line_space;            /* Space between lines. */
     int line_width;            /* Width of lines. */
 
-    struct xr_font fonts[OUTP_FONT_CNT];
-  };
+    int cell_margin;
+
+    int min_break[TABLE_N_AXES]; /* Min cell size to break across pages. */
 
-static bool handle_option (struct outp_driver *this, const char *key,
-                           const struct string *val);
-static void draw_headers (struct outp_driver *this);
+    struct xr_color bg;    /* Background color */
+    struct xr_color fg;    /* Foreground color */
 
-static bool load_font (struct outp_driver *this, struct xr_font *);
-static void free_font (struct xr_font *);
-static int text_width (struct outp_driver *, const char *, enum outp_font);
+    /* Internal state. */
+    struct render_params *params;
+    int char_width, char_height;
+    char *command_name;
+    char *title;
+    char *subtitle;
+    cairo_t *cairo;
+    int page_number;           /* Current page number. */
+    int x, y;
+    struct xr_render_fsm *fsm;
+    int nest;
+  };
+
+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 *);
+
+static void xr_draw_line (void *, int bb[TABLE_N_AXES][2],
+                          enum render_line_style styles[TABLE_N_AXES][2]);
+static void xr_measure_cell_width (void *, const struct table_cell *,
+                                   int footnote_idx, int *min, int *max);
+static int xr_measure_cell_height (void *, const struct table_cell *,
+                                   int footnote_idx, int width);
+static void xr_draw_cell (void *, const struct table_cell *, int footnote_idx,
+                          int bb[TABLE_N_AXES][2],
+                          int clip[TABLE_N_AXES][2]);
+static int xr_adjust_break (void *, const struct table_cell *, int footnote_idx,
+                            int width, int height);
+
+static struct xr_render_fsm *xr_render_output_item (
+  struct xr_driver *, const struct output_item *);
 \f
-/* Driver initialization. */
+/* Output driver basics. */
+
+static struct xr_driver *
+xr_driver_cast (struct output_driver *driver)
+{
+  assert (driver->class == &cairo_driver_class);
+  return UP_CAST (driver, struct xr_driver, driver);
+}
+
+static struct driver_option *
+opt (struct output_driver *d, struct string_map *options, const char *key,
+     const char *default_value)
+{
+  return driver_option_get (d, options, key, default_value);
+}
+
+/* Parse color information specified by KEY into {RED,GREEN,BLUE}.
+   Currently, the input string must be of the form "#RRRRGGGGBBBB"
+   Future implementations might allow things like "yellow" and
+   "sky-blue-ultra-brown"
+*/
+void
+parse_color (struct output_driver *d, struct string_map *options,
+            const char *key, const char *default_value,
+            struct xr_color *color)
+{
+  int red, green, blue;
+  char *string = parse_string (opt (d, options, key, default_value));
+
+  if (3 != sscanf (string, "#%04x%04x%04x", &red, &green, &blue))
+    {
+      /* If the parsed option string fails, then try the default value */
+      if ( 3 != sscanf (default_value, "#%04x%04x%04x", &red, &green, &blue))
+       {
+         /* ... and if that fails set everything to zero */
+         red = green = blue = 0;
+       }
+    }
+
+  free (string);
+
+  /* Convert 16 bit ints to float */
+  color->red = red / (double) 0xFFFF;
+  color->green = green / (double) 0xFFFF;
+  color->blue = blue / (double) 0xFFFF;
+}
+
+static PangoFontDescription *
+parse_font (struct output_driver *d, struct string_map *options,
+            const char *key, const char *default_value,
+            int default_size)
+{
+  PangoFontDescription *desc;
+  char *string;
+
+  /* Parse KEY as a font description. */
+  string = parse_string (opt (d, options, key, default_value));
+  desc = pango_font_description_from_string (string);
+  if (desc == NULL)
+    {
+      msg (MW, _("`%s': bad font specification"), string);
+
+      /* Fall back to DEFAULT_VALUE, which had better be a valid font
+         description. */
+      desc = pango_font_description_from_string (default_value);
+      assert (desc != NULL);
+    }
+  free (string);
+
+  /* If the font description didn't include an explicit font size, then set it
+     to DEFAULT_SIZE, which is in inch/72000 units. */
+  if (!(pango_font_description_get_set_fields (desc) & PANGO_FONT_MASK_SIZE))
+    pango_font_description_set_size (desc,
+                                     (default_size / 1000.0) * PANGO_SCALE);
+
+  return desc;
+}
+
+
+static void
+apply_options (struct xr_driver *xr, struct string_map *o)
+{
+  struct output_driver *d = &xr->driver;
+
+  /* In inch/72000 units used by parse_paper_size() and parse_dimension(). */
+  int left_margin, right_margin;
+  int top_margin, bottom_margin;
+  int paper_width, paper_length;
+  int font_size;
+  int min_break[TABLE_N_AXES];
+
+  /* Scale factor from inch/72000 to inch/(72 * XR_POINT). */
+  const double scale = XR_POINT / 1000.;
+
+  int i;
+
+  for (i = 0; i < XR_N_FONTS; i++)
+    {
+      struct xr_font *font = &xr->fonts[i];
+
+      if (font->desc != NULL)
+        pango_font_description_free (font->desc);
+    }
+
+  font_size = parse_int (opt (d, o, "font-size", "10000"), 1000, 1000000);
+  xr->fonts[XR_FONT_FIXED].desc = parse_font (d, o, "fixed-font", "monospace",
+                                              font_size);
+  xr->fonts[XR_FONT_PROPORTIONAL].desc = parse_font (d, o, "prop-font",
+                                                     "serif", font_size);
+  xr->fonts[XR_FONT_EMPHASIS].desc = parse_font (d, o, "emph-font",
+                                                 "serif italic", font_size);
+  xr->fonts[XR_FONT_MARKER].desc = parse_font (d, o, "marker-font", "serif",
+                                               font_size * PANGO_SCALE_X_SMALL);
+
+  xr->line_gutter = XR_POINT / 2;
+  xr->line_space = XR_POINT;
+  xr->line_width = XR_POINT / 2;
+  xr->page_number = 0;
+
+  parse_color (d, o, "background-color", "#FFFFFFFFFFFF", &xr->bg);
+  parse_color (d, o, "foreground-color", "#000000000000", &xr->fg);
+
+  /* Get dimensions.  */
+  parse_paper_size (opt (d, o, "paper-size", ""), &paper_width, &paper_length);
+  left_margin = parse_dimension (opt (d, o, "left-margin", ".5in"));
+  right_margin = parse_dimension (opt (d, o, "right-margin", ".5in"));
+  top_margin = parse_dimension (opt (d, o, "top-margin", ".5in"));
+  bottom_margin = parse_dimension (opt (d, o, "bottom-margin", ".5in"));
+
+  min_break[H] = parse_dimension (opt (d, o, "min-hbreak", NULL)) * scale;
+  min_break[V] = parse_dimension (opt (d, o, "min-vbreak", NULL)) * scale;
+
+  /* Convert to inch/(XR_POINT * 72). */
+  xr->left_margin = left_margin * scale;
+  xr->right_margin = right_margin * scale;
+  xr->top_margin = top_margin * scale;
+  xr->bottom_margin = bottom_margin * scale;
+  xr->width = (paper_width - left_margin - right_margin) * scale;
+  xr->length = (paper_length - top_margin - bottom_margin) * scale;
+  xr->min_break[H] = min_break[H] >= 0 ? min_break[H] : xr->width / 2;
+  xr->min_break[V] = min_break[V] >= 0 ? min_break[V] : xr->length / 2;
+}
+
+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);
+}
 
 static bool
-xr_open_driver (struct outp_driver *this, struct substring options)
+xr_set_cairo (struct xr_driver *xr, cairo_t *cairo)
 {
-  cairo_surface_t *surface;
-  cairo_status_t status;
-  struct xr_driver_ext *x;
-  double width_pt, length_pt;
-  size_t i;
+  int i;
+
+  xr->cairo = cairo;
+
+  cairo_set_line_width (xr->cairo, xr_to_pt (xr->line_width));
+
+  xr->char_width = 0;
+  xr->char_height = 0;
+  for (i = 0; i < XR_N_FONTS; i++)
+    {
+      struct xr_font *font = &xr->fonts[i];
+      int char_width, char_height;
+
+      font->layout = pango_cairo_create_layout (cairo);
+      pango_layout_set_font_description (font->layout, font->desc);
 
-  this->width = this->length = 0;
-  this->font_height = XR_POINT * 10;
-
-  this->ext = x = xmalloc (sizeof *x);
-  x->file_name = xstrdup ("pspp.pdf");
-  x->file_type = XR_PDF;
-  x->draw_headers = true;
-  x->page_number = 0;
-  x->portrait = true;
-  outp_get_paper_size ("", &x->paper_width, &x->paper_length);
-  x->left_margin = XR_INCH / 2;
-  x->right_margin = XR_INCH / 2;
-  x->top_margin = XR_INCH / 2;
-  x->bottom_margin = XR_INCH / 2;
-  x->line_gutter = XR_POINT;
-  x->line_space = XR_POINT;
-  x->line_width = XR_POINT / 2;
-  x->fonts[OUTP_FIXED].string = xstrdup ("monospace");
-  x->fonts[OUTP_PROPORTIONAL].string = xstrdup ("serif");
-  x->fonts[OUTP_EMPHASIS].string = xstrdup ("serif italic");
-  for (i = 0; i < OUTP_FONT_CNT; i++)
-    {
-      struct xr_font *font = &x->fonts[i];
-      font->desc = NULL;
-      font->metrics = NULL;
-      font->layout = NULL;
-    }
-
-  outp_parse_options (options, handle_option, this);
-
-  width_pt = x->paper_width / 1000.0;
-  length_pt = x->paper_length / 1000.0;
-  if (x->portrait)
-    {
-      this->width = pt_to_xr (width_pt);
-      this->length = pt_to_xr (length_pt);
+      pango_layout_set_text (font->layout, "0", 1);
+      pango_layout_get_size (font->layout, &char_width, &char_height);
+      xr->char_width = MAX (xr->char_width, pango_to_xr (char_width));
+      xr->char_height = MAX (xr->char_height, pango_to_xr (char_height));
     }
-  else
+  xr->cell_margin = xr->char_width;
+
+  if (xr->params == NULL)
     {
-      this->width = pt_to_xr (width_pt);
-      this->length = pt_to_xr (length_pt);
+      int single_width, double_width;
+
+      xr->params = xmalloc (sizeof *xr->params);
+      xr->params->draw_line = xr_draw_line;
+      xr->params->measure_cell_width = xr_measure_cell_width;
+      xr->params->measure_cell_height = xr_measure_cell_height;
+      xr->params->adjust_break = xr_adjust_break;
+      xr->params->draw_cell = xr_draw_cell;
+      xr->params->aux = xr;
+      xr->params->size[H] = xr->width;
+      xr->params->size[V] = xr->length;
+      xr->params->font_size[H] = xr->char_width;
+      xr->params->font_size[V] = xr->char_height;
+
+      single_width = 2 * xr->line_gutter + xr->line_width;
+      double_width = 2 * xr->line_gutter + xr->line_space + 2 * xr->line_width;
+      for (i = 0; i < TABLE_N_AXES; i++)
+        {
+          xr->params->line_widths[i][RENDER_LINE_NONE] = 0;
+          xr->params->line_widths[i][RENDER_LINE_SINGLE] = single_width;
+          xr->params->line_widths[i][RENDER_LINE_DOUBLE] = double_width;
+        }
+
+      for (i = 0; i < TABLE_N_AXES; i++)
+        xr->params->min_break[i] = xr->min_break[i];
     }
-  if (x->draw_headers)
-    x->top_margin += 3 * this->font_height;
-  this->width -= x->left_margin + x->right_margin;
-  this->length -= x->top_margin + x->bottom_margin;
 
-  if (x->file_type == XR_PDF)
-    surface = cairo_pdf_surface_create (x->file_name, width_pt, length_pt);
-  else if (x->file_type == XR_PS)
-    surface = cairo_ps_surface_create (x->file_name, width_pt, length_pt);
-  else if (x->file_type == XR_SVG)
-    surface = cairo_svg_surface_create (x->file_name, width_pt, length_pt);
+  cairo_set_source_rgb (xr->cairo, xr->fg.red, xr->fg.green, xr->fg.blue);
+
+  return true;
+}
+
+static struct output_driver *
+xr_create (const char *file_name, enum settings_output_devices device_type,
+           struct string_map *o, enum xr_output_type file_type)
+{
+  enum { MIN_WIDTH = 3, MIN_LENGTH = 3 };
+  struct xr_driver *xr;
+  cairo_surface_t *surface;
+  cairo_status_t status;
+  double width_pt, length_pt;
+
+  xr = xr_allocate (file_name, device_type, o);
+
+  width_pt = xr_to_pt (xr->width + xr->left_margin + xr->right_margin);
+  length_pt = xr_to_pt (xr->length + xr->top_margin + xr->bottom_margin);
+  if (file_type == XR_PDF)
+    surface = cairo_pdf_surface_create (file_name, width_pt, length_pt);
+  else if (file_type == XR_PS)
+    surface = cairo_ps_surface_create (file_name, width_pt, length_pt);
+  else if (file_type == XR_SVG)
+    surface = cairo_svg_surface_create (file_name, width_pt, length_pt);
   else
     NOT_REACHED ();
 
   status = cairo_surface_status (surface);
   if (status != CAIRO_STATUS_SUCCESS)
     {
-      error (0, 0, _("opening output file \"%s\": %s"),
-             x->file_name, cairo_status_to_string (status));
+      msg (ME, _("error opening output file `%s': %s"),
+             file_name, cairo_status_to_string (status));
       cairo_surface_destroy (surface);
       goto error;
     }
 
-  x->cairo = cairo_create (surface);
+  xr->cairo = cairo_create (surface);
   cairo_surface_destroy (surface);
 
-  cairo_translate (x->cairo,
-                   xr_to_pt (x->left_margin),
-                   xr_to_pt (x->top_margin));
-  cairo_set_line_width (x->cairo, xr_to_pt (x->line_width));
+  if (!xr_set_cairo (xr, xr->cairo))
+    goto error;
 
-  for (i = 0; i < OUTP_FONT_CNT; i++)
-    if (!load_font (this, &x->fonts[i]))
+  cairo_save (xr->cairo);
+  xr_driver_next_page (xr, xr->cairo);
+
+  if (xr->width / xr->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,
+             xr->width / xr->char_width);
       goto error;
+    }
 
-  if (this->length / this->font_height < 15)
+  if (xr->length / xr->char_height < MIN_LENGTH)
     {
-      error (0, 0, _("The defined page is not long "
-                     "enough to hold margins and headers, plus least 15 "
-                     "lines of the default fonts.  In fact, there's only "
+      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."),
-             this->length / this->font_height);
+             MIN_LENGTH,
+             xr->length / xr->char_height);
       goto error;
     }
 
-  this->fixed_width = text_width (this, "0", OUTP_FIXED);
-  this->prop_em_width = text_width (this, "0", OUTP_PROPORTIONAL);
+  return &xr->driver;
 
-  this->horiz_line_width[OUTP_L_NONE] = 0;
-  this->horiz_line_width[OUTP_L_SINGLE] = 2 * x->line_gutter + x->line_width;
-  this->horiz_line_width[OUTP_L_DOUBLE] = (2 * x->line_gutter + x->line_space
-                                           + 2 * x->line_width);
-  memcpy (this->vert_line_width, this->horiz_line_width,
-          sizeof this->vert_line_width);
+ error:
+  output_driver_destroy (&xr->driver);
+  return NULL;
+}
 
-  return true;
+static struct output_driver *
+xr_pdf_create (struct  file_handle *fh, enum settings_output_devices device_type,
+               struct string_map *o)
+{
+  struct output_driver *od = xr_create (fh_get_file_name (fh), device_type, o, XR_PDF);
+  fh_unref (fh);
+  return od ;
+}
 
- error:
-  this->class->close_driver (this);
-  return false;
+static struct output_driver *
+xr_ps_create (struct  file_handle *fh, enum settings_output_devices device_type,
+               struct string_map *o)
+{
+  struct output_driver *od =  xr_create (fh_get_file_name (fh), device_type, o, XR_PS);
+  fh_unref (fh);
+  return od ;
 }
 
-static bool
-xr_close_driver (struct outp_driver *this)
+static struct output_driver *
+xr_svg_create (struct file_handle *fh, enum settings_output_devices device_type,
+               struct string_map *o)
+{
+  struct output_driver *od = xr_create (fh_get_file_name (fh), device_type, o, XR_SVG);
+  fh_unref (fh);
+  return od ;
+}
+
+static void
+xr_destroy (struct output_driver *driver)
 {
-  struct xr_driver_ext *x = this->ext;
-  bool ok = true;
+  struct xr_driver *xr = xr_driver_cast (driver);
   size_t i;
 
-  if (x->cairo != NULL)
+  xr_driver_destroy_fsm (xr);
+
+  if (xr->cairo != NULL)
     {
       cairo_status_t status;
 
-      cairo_surface_finish (cairo_get_target (x->cairo));
-      status = cairo_status (x->cairo);
+      cairo_surface_finish (cairo_get_target (xr->cairo));
+      status = cairo_status (xr->cairo);
       if (status != CAIRO_STATUS_SUCCESS)
-        error (0, 0, _("writing output file \"%s\": %s"),
-               x->file_name, cairo_status_to_string (status));
-      cairo_destroy (x->cairo);
+        msg (ME, _("error drawing output for %s driver: %s"),
+               output_driver_get_name (driver),
+               cairo_status_to_string (status));
+      cairo_destroy (xr->cairo);
     }
 
-  free (x->file_name);
-  for (i = 0; i < OUTP_FONT_CNT; i++)
-    free_font (&x->fonts[i]);
-  free (x);
+  free (xr->command_name);
+  for (i = 0; i < XR_N_FONTS; i++)
+    {
+      struct xr_font *font = &xr->fonts[i];
+
+      if (font->desc != NULL)
+        pango_font_description_free (font->desc);
+      if (font->layout != NULL)
+        g_object_unref (font->layout);
+    }
 
-  return ok;
+  free (xr->params);
+  free (xr);
 }
 
-/* Generic option types. */
-enum
+static void
+xr_flush (struct output_driver *driver)
 {
-  output_file_arg,
-  output_type_arg,
-  paper_size_arg,
-  orientation_arg,
-  line_style_arg,
-  boolean_arg,
-  dimension_arg,
-  string_arg,
-  nonneg_int_arg
-};
+  struct xr_driver *xr = xr_driver_cast (driver);
 
-/* All the options that the Cairo driver supports. */
-static const struct outp_option option_tab[] =
-{
-  {"output-file",              output_file_arg,0},
-  {"output-type",               output_type_arg,0},
-  {"paper-size",               paper_size_arg, 0},
-  {"orientation",              orientation_arg,0},
-
-  {"headers",                  boolean_arg,    1},
-
-  {"prop-font",                string_arg,     OUTP_PROPORTIONAL},
-  {"emph-font",                string_arg,     OUTP_EMPHASIS},
-  {"fixed-font",               string_arg,     OUTP_FIXED},
-
-  {"left-margin",              dimension_arg,  0},
-  {"right-margin",             dimension_arg,  1},
-  {"top-margin",               dimension_arg,  2},
-  {"bottom-margin",            dimension_arg,  3},
-  {"font-size",                        dimension_arg,  4},
-  {"line-width",               dimension_arg,  5},
-  {"line-gutter",              dimension_arg,  6},
-  {"line-width",               dimension_arg,  7},
-  {NULL, 0, 0},
-};
+  cairo_surface_flush (cairo_get_target (xr->cairo));
+}
 
-static bool
-handle_option (struct outp_driver *this, const char *key,
-               const struct string *val)
+static void
+xr_submit (struct output_driver *driver, const struct output_item *output_item)
 {
-  struct xr_driver_ext *x = this->ext;
-  int subcat;
-  char *value = ds_cstr (val);
+  struct xr_driver *xr = xr_driver_cast (driver);
+
+  output_driver_track_current_command (output_item, &xr->command_name);
 
-  switch (outp_match_keyword (key, option_tab, &subcat))
+  xr_driver_output_item (xr, output_item);
+  while (xr_driver_need_new_page (xr))
     {
-    case -1:
-      error (0, 0,
-             _("unknown configuration parameter `%s' for Cairo device "
-               "driver"), key);
-      break;
-    case output_file_arg:
-      free (x->file_name);
-      x->file_name = xstrdup (value);
-      break;
-    case output_type_arg:
-      if (!strcmp (value, "pdf"))
-        x->file_type = XR_PDF;
-      else if (!strcmp (value, "ps"))
-        x->file_type = XR_PS;
-      else if (!strcmp (value, "svg"))
-        x->file_type = XR_SVG;
-      else
-        {
-          error (0, 0, _("unknown Cairo output type \"%s\""), value);
-          return false;
-        }
-      break;
-    case paper_size_arg:
-      outp_get_paper_size (value, &x->paper_width, &x->paper_length);
-      break;
-    case orientation_arg:
-      if (!strcmp (value, "portrait"))
-       x->portrait = true;
-      else if (!strcmp (value, "landscape"))
-       x->portrait = false;
-      else
-       error (0, 0, _("unknown orientation `%s' (valid orientations are "
-                       "`portrait' and `landscape')"), value);
-      break;
-    case boolean_arg:
-      if (!strcmp (value, "on") || !strcmp (value, "true")
-          || !strcmp (value, "yes") || atoi (value))
-        x->draw_headers = true;
-      else if (!strcmp (value, "off") || !strcmp (value, "false")
-               || !strcmp (value, "no") || !strcmp (value, "0"))
-        x->draw_headers = false;
-      else
-        {
-          error (0, 0, _("boolean value expected for %s"), key);
-          return false;
-        }
-      break;
-    case dimension_arg:
-      {
-       int dimension = outp_evaluate_dimension (value);
-
-       if (dimension <= 0)
-          break;
-       switch (subcat)
-         {
-         case 0:
-           x->left_margin = dimension;
-           break;
-         case 1:
-           x->right_margin = dimension;
-           break;
-         case 2:
-           x->top_margin = dimension;
-           break;
-         case 3:
-           x->bottom_margin = dimension;
-           break;
-         case 4:
-           this->font_height = dimension;
-           break;
-         case 5:
-           x->line_width = dimension;
-           break;
-         case 6:
-           x->line_gutter = dimension;
-           break;
-         case 7:
-           x->line_width = dimension;
-           break;
-         default:
-           NOT_REACHED ();
-         }
-      }
-      break;
-    case string_arg:
-      free (x->fonts[subcat].string);
-      x->fonts[subcat].string = ds_xstrdup (val);
-      break;
-    default:
-      NOT_REACHED ();
+      cairo_restore (xr->cairo);
+      cairo_show_page (xr->cairo);
+      cairo_save (xr->cairo);
+      xr_driver_next_page (xr, xr->cairo);
     }
-
-  return true;
 }
 \f
-/* Basic file operations. */
+/* Functions for rendering a series of output items to a series of Cairo
+   contexts, with pagination.
 
-static void
-xr_open_page (struct outp_driver *this)
+   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. */
+
+/* Gives new page CAIRO to XR for output. */
+void
+xr_driver_next_page (struct xr_driver *xr, cairo_t *cairo)
 {
-  struct xr_driver_ext *x = this->ext;
+  cairo_save (cairo);
+  cairo_set_source_rgb (cairo, xr->bg.red, xr->bg.green, xr->bg.blue);
+  cairo_rectangle (cairo, 0, 0, xr->width, xr->length);
+  cairo_fill (cairo);
+  cairo_restore (cairo);
+
+  cairo_translate (cairo,
+                   xr_to_pt (xr->left_margin),
+                   xr_to_pt (xr->top_margin));
+
+  xr->page_number++;
+  xr->cairo = cairo;
+  xr->x = xr->y = 0;
+  xr_driver_run_fsm (xr);
+}
 
-  x->page_number++;
+/* 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_render_output_item (xr, output_item);
+  xr_driver_run_fsm (xr);
+}
 
-  if (x->draw_headers)
-    draw_headers (this);
+/* 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;
 }
 
-static void
-xr_close_page (struct outp_driver *this)
+/* Returns true if the current page doesn't have any content yet. */
+bool
+xr_driver_is_page_blank (const struct xr_driver *xr)
 {
-  struct xr_driver_ext *x = this->ext;
-  cairo_show_page (x->cairo);
+  return xr->y == 0;
 }
 
 static void
-xr_submit (struct outp_driver *this UNUSED, struct som_entity *s)
+xr_driver_destroy_fsm (struct xr_driver *xr)
 {
-  switch (s->type)
+  if (xr->fsm != NULL)
     {
-    case SOM_CHART:
-      break;
-    default:
-      NOT_REACHED ();
+      xr->fsm->destroy (xr->fsm);
+      xr->fsm = NULL;
     }
 }
+
+static void
+xr_driver_run_fsm (struct xr_driver *xr)
+{
+  if (xr->fsm != NULL && !xr->fsm->render (xr->fsm, xr))
+    xr_driver_destroy_fsm (xr);
+}
 \f
-/* Draws a line from (x0,y0) to (x1,y1). */
 static void
-dump_line (struct outp_driver *this, int x0, int y0, int x1, int y1)
+xr_layout_cell (struct xr_driver *, const struct table_cell *, int footnote_idx,
+                int bb[TABLE_N_AXES][2], int clip[TABLE_N_AXES][2],
+                int *width, int *height, int *brk);
+
+static void
+dump_line (struct xr_driver *xr, int x0, int y0, int x1, int y1)
+{
+  cairo_new_path (xr->cairo);
+  cairo_move_to (xr->cairo, xr_to_pt (x0 + xr->x), xr_to_pt (y0 + xr->y));
+  cairo_line_to (xr->cairo, xr_to_pt (x1 + xr->x), xr_to_pt (y1 + xr->y));
+  cairo_stroke (xr->cairo);
+}
+
+static void UNUSED
+dump_rectangle (struct xr_driver *xr, int x0, int y0, int x1, int y1)
 {
-  struct xr_driver_ext *x = this->ext;
-  cairo_new_path (x->cairo);
-  cairo_move_to (x->cairo, xr_to_pt (x0), xr_to_pt (y0));
-  cairo_line_to (x->cairo, xr_to_pt (x1), xr_to_pt (y1));
-  cairo_stroke (x->cairo);
+  cairo_new_path (xr->cairo);
+  cairo_move_to (xr->cairo, xr_to_pt (x0 + xr->x), xr_to_pt (y0 + xr->y));
+  cairo_line_to (xr->cairo, xr_to_pt (x1 + xr->x), xr_to_pt (y0 + xr->y));
+  cairo_line_to (xr->cairo, xr_to_pt (x1 + xr->x), xr_to_pt (y1 + xr->y));
+  cairo_line_to (xr->cairo, xr_to_pt (x0 + xr->x), xr_to_pt (y1 + xr->y));
+  cairo_close_path (xr->cairo);
+  cairo_stroke (xr->cairo);
 }
 
 /* Draws a horizontal line X0...X2 at Y if LEFT says so,
@@ -466,19 +667,18 @@ dump_line (struct outp_driver *this, int x0, int y0, int x1, int y1)
    Draws a horizontal line X1...X3 at Y if RIGHT says so,
    shortening it to X2...X3 if SHORTEN is true. */
 static void
-horz_line (struct outp_driver *this,
-           int x0, int x1, int x2, int x3, int y,
-           enum outp_line_style left, enum outp_line_style right,
+horz_line (struct xr_driver *xr, int x0, int x1, int x2, int x3, int y,
+           enum render_line_style left, enum render_line_style right,
            bool shorten)
 {
-  if (left != OUTP_L_NONE && right != OUTP_L_NONE && !shorten)
-    dump_line (this, x0, y, x3, y);
+  if (left != RENDER_LINE_NONE && right != RENDER_LINE_NONE && !shorten)
+    dump_line (xr, x0, y, x3, y);
   else
     {
-      if (left != OUTP_L_NONE)
-        dump_line (this, x0, y, shorten ? x1 : x2, y);
-      if (right != OUTP_L_NONE)
-        dump_line (this, shorten ? x2 : x1, y, x3, y);
+      if (left != RENDER_LINE_NONE)
+        dump_line (xr, x0, y, shorten ? x1 : x2, y);
+      if (right != RENDER_LINE_NONE)
+        dump_line (xr, shorten ? x2 : x1, y, x3, y);
     }
 }
 
@@ -487,33 +687,34 @@ horz_line (struct outp_driver *this,
    Draws a vertical line Y1...Y3 at X if BOTTOM says so,
    shortening it to Y2...Y3 if SHORTEN is true. */
 static void
-vert_line (struct outp_driver *this,
-           int y0, int y1, int y2, int y3, int x,
-           enum outp_line_style top, enum outp_line_style bottom,
+vert_line (struct xr_driver *xr, int y0, int y1, int y2, int y3, int x,
+           enum render_line_style top, enum render_line_style bottom,
            bool shorten)
 {
-  if (top != OUTP_L_NONE && bottom != OUTP_L_NONE && !shorten)
-    dump_line (this, x, y0, x, y3);
+  if (top != RENDER_LINE_NONE && bottom != RENDER_LINE_NONE && !shorten)
+    dump_line (xr, x, y0, x, y3);
   else
     {
-      if (top != OUTP_L_NONE)
-        dump_line (this, x, y0, x, shorten ? y1 : y2);
-      if (bottom != OUTP_L_NONE)
-        dump_line (this, x, shorten ? y2 : y1, x, y3);
+      if (top != RENDER_LINE_NONE)
+        dump_line (xr, x, y0, x, shorten ? y1 : y2);
+      if (bottom != RENDER_LINE_NONE)
+        dump_line (xr, x, shorten ? y2 : y1, x, y3);
     }
 }
 
-/* Draws a generalized intersection of lines in the rectangle
-   (X0,Y0)-(X3,Y3).  The line coming from the top to the center
-   is of style TOP, from left to center of style LEFT, from
-   bottom to center of style BOTTOM, and from right to center of
-   style RIGHT. */
 static void
-xr_line (struct outp_driver *this,
-         int x0, int y0, int x3, int y3,
-         enum outp_line_style top, enum outp_line_style left,
-         enum outp_line_style bottom, enum outp_line_style right)
+xr_draw_line (void *xr_, int bb[TABLE_N_AXES][2],
+              enum render_line_style styles[TABLE_N_AXES][2])
 {
+  const int x0 = bb[H][0];
+  const int y0 = bb[V][0];
+  const int x3 = bb[H][1];
+  const int y3 = bb[V][1];
+  const int top = styles[H][0];
+  const int bottom = styles[H][1];
+  const int start_of_line = render_direction_rtl() ? styles[V][1]: styles[V][0];
+  const int end_of_line   = render_direction_rtl() ? styles[V][0]: styles[V][1];
+
   /* The algorithm here is somewhat subtle, to allow it to handle
      all the kinds of intersections that we need.
 
@@ -543,16 +744,16 @@ xr_line (struct outp_driver *this,
                   |        #     #       |
                y3 |________#_____#_______|
   */
-  struct xr_driver_ext *ext = this->ext;
+  struct xr_driver *xr = xr_;
 
   /* Offset from center of each line in a pair of double lines. */
-  int double_line_ofs = (ext->line_space + ext->line_width) / 2;
+  int double_line_ofs = (xr->line_space + xr->line_width) / 2;
 
   /* Are the lines along each axis single or double?
      (It doesn't make sense to have different kinds of line on the
      same axis, so we don't try to gracefully handle that case.) */
-  bool double_vert = top == OUTP_L_DOUBLE || bottom == OUTP_L_DOUBLE;
-  bool double_horz = left == OUTP_L_DOUBLE || right == OUTP_L_DOUBLE;
+  bool double_vert = top == RENDER_LINE_DOUBLE || bottom == RENDER_LINE_DOUBLE;
+  bool double_horz = start_of_line == RENDER_LINE_DOUBLE || end_of_line == RENDER_LINE_DOUBLE;
 
   /* When horizontal lines are doubled,
      the left-side line along y1 normally runs from x0 to x2,
@@ -579,16 +780,16 @@ xr_line (struct outp_driver *this,
      single.  We actually choose to cut off the line anyhow, as
      shown in the first diagram above.
   */
-  bool shorten_y1_lines = top == OUTP_L_DOUBLE;
-  bool shorten_y2_lines = bottom == OUTP_L_DOUBLE;
+  bool shorten_y1_lines = top == RENDER_LINE_DOUBLE;
+  bool shorten_y2_lines = bottom == RENDER_LINE_DOUBLE;
   bool shorten_yc_line = shorten_y1_lines && shorten_y2_lines;
   int horz_line_ofs = double_vert ? double_line_ofs : 0;
   int xc = (x0 + x3) / 2;
   int x1 = xc - horz_line_ofs;
   int x2 = xc + horz_line_ofs;
 
-  bool shorten_x1_lines = left == OUTP_L_DOUBLE;
-  bool shorten_x2_lines = right == OUTP_L_DOUBLE;
+  bool shorten_x1_lines = start_of_line == RENDER_LINE_DOUBLE;
+  bool shorten_x2_lines = end_of_line == RENDER_LINE_DOUBLE;
   bool shorten_xc_line = shorten_x1_lines && shorten_x2_lines;
   int vert_line_ofs = double_horz ? double_line_ofs : 0;
   int yc = (y0 + y3) / 2;
@@ -596,262 +797,894 @@ xr_line (struct outp_driver *this,
   int y2 = yc + vert_line_ofs;
 
   if (!double_horz)
-    horz_line (this, x0, x1, x2, x3, yc, left, right, shorten_yc_line);
+    horz_line (xr, x0, x1, x2, x3, yc, start_of_line, end_of_line, shorten_yc_line);
   else
     {
-      horz_line (this, x0, x1, x2, x3, y1, left, right, shorten_y1_lines);
-      horz_line (this, x0, x1, x2, x3, y2, left, right, shorten_y2_lines);
+      horz_line (xr, x0, x1, x2, x3, y1, start_of_line, end_of_line, shorten_y1_lines);
+      horz_line (xr, x0, x1, x2, x3, y2, start_of_line, end_of_line, shorten_y2_lines);
     }
 
   if (!double_vert)
-    vert_line (this, y0, y1, y2, y3, xc, top, bottom, shorten_xc_line);
+    vert_line (xr, y0, y1, y2, y3, xc, top, bottom, shorten_xc_line);
   else
     {
-      vert_line (this, y0, y1, y2, y3, x1, top, bottom, shorten_x1_lines);
-      vert_line (this, y0, y1, y2, y3, x2, top, bottom, shorten_x2_lines);
+      vert_line (xr, y0, y1, y2, y3, x1, top, bottom, shorten_x1_lines);
+      vert_line (xr, y0, y1, y2, y3, x2, top, bottom, shorten_x2_lines);
     }
 }
 
-/* Writes STRING at location (X,Y) trimmed to the given MAX_WIDTH
-   and with the given JUSTIFICATION for THIS driver. */
-static int
-draw_text (struct outp_driver *this,
-           const char *string, int x, int y, int max_width,
-           enum outp_justification justification)
-{
-  struct outp_text text;
-  int width;
-
-  text.font = OUTP_PROPORTIONAL;
-  text.justification = justification;
-  text.string = ss_cstr (string);
-  text.h = max_width;
-  text.v = this->font_height;
-  text.x = x;
-  text.y = y;
-  this->class->text_metrics (this, &text, &width, NULL);
-  this->class->text_draw (this, &text);
-  return width;
-}
-
-/* Writes STRING at location (X,Y) trimmed to the given MAX_WIDTH
-   and with the given JUSTIFICATION for THIS driver. */
+static void
+xr_measure_cell_width (void *xr_, const struct table_cell *cell,
+                       int footnote_idx, int *min_width, int *max_width)
+{
+  struct xr_driver *xr = xr_;
+  int bb[TABLE_N_AXES][2];
+  int clip[TABLE_N_AXES][2];
+  int h;
+
+  bb[H][0] = 0;
+  bb[H][1] = INT_MAX;
+  bb[V][0] = 0;
+  bb[V][1] = INT_MAX;
+  clip[H][0] = clip[H][1] = clip[V][0] = clip[V][1] = 0;
+  xr_layout_cell (xr, cell, footnote_idx, bb, clip, max_width, &h, NULL);
+
+  bb[H][1] = 1;
+  xr_layout_cell (xr, cell, footnote_idx, bb, clip, min_width, &h, NULL);
+
+  if (*min_width > 0)
+    *min_width += xr->cell_margin * 2;
+  if (*max_width > 0)
+    *max_width += xr->cell_margin * 2;
+}
+
 static int
-text_width (struct outp_driver *this, const char *string, enum outp_font font)
+xr_measure_cell_height (void *xr_, const struct table_cell *cell,
+                        int footnote_idx, int width)
 {
-  struct outp_text text;
-  int width;
+  struct xr_driver *xr = xr_;
+  int bb[TABLE_N_AXES][2];
+  int clip[TABLE_N_AXES][2];
+  int w, h;
+
+  bb[H][0] = 0;
+  bb[H][1] = width - xr->cell_margin * 2;
+  bb[V][0] = 0;
+  bb[V][1] = INT_MAX;
+  clip[H][0] = clip[H][1] = clip[V][0] = clip[V][1] = 0;
+  xr_layout_cell (xr, cell, footnote_idx, bb, clip, &w, &h, NULL);
+  return h;
+}
 
-  text.font = font;
-  text.justification = OUTP_LEFT;
-  text.string = ss_cstr (string);
-  text.h = INT_MAX;
-  text.v = this->font_height;
-  text.x = 0;
-  text.y = 0;
-  this->class->text_metrics (this, &text, &width, NULL);
-  return width;
+static void
+xr_draw_cell (void *xr_, const struct table_cell *cell, int footnote_idx,
+              int bb[TABLE_N_AXES][2], int clip[TABLE_N_AXES][2])
+{
+  struct xr_driver *xr = xr_;
+  int w, h, brk;
+
+  bb[H][0] += xr->cell_margin;
+  bb[H][1] -= xr->cell_margin;
+  if (bb[H][0] >= bb[H][1])
+    return;
+  xr_layout_cell (xr, cell, footnote_idx, bb, clip, &w, &h, &brk);
 }
 
-/* Writes LEFT left-justified and RIGHT right-justified within
-   (X0...X1) at Y.  LEFT or RIGHT or both may be null. */
+static int
+xr_adjust_break (void *xr_, const struct table_cell *cell, int footnote_idx,
+                 int width, int height)
+{
+  struct xr_driver *xr = xr_;
+  int bb[TABLE_N_AXES][2];
+  int clip[TABLE_N_AXES][2];
+  int w, h, brk;
+
+  if (xr_measure_cell_height (xr_, cell, footnote_idx, width) < height)
+    return -1;
+
+  bb[H][0] = 0;
+  bb[H][1] = width - 2 * xr->cell_margin;
+  if (bb[H][1] <= 0)
+    return 0;
+  bb[V][0] = 0;
+  bb[V][1] = height;
+  clip[H][0] = clip[H][1] = clip[V][0] = clip[V][1] = 0;
+  xr_layout_cell (xr, cell, footnote_idx, bb, clip, &w, &h, &brk);
+  return brk;
+}
+\f
 static void
-draw_header_line (struct outp_driver *this,
-                  const char *left, const char *right,
-                  int x0, int x1, int y)
+xr_clip (struct xr_driver *xr, int clip[TABLE_N_AXES][2])
 {
-  int right_width = 0;
-  if (right != NULL)
-    right_width = (draw_text (this, right, x0, y, x1 - x0, OUTP_RIGHT)
-                   + this->prop_em_width);
-  if (left != NULL)
-    draw_text (this, left, x0, y, x1 - x0 - right_width, OUTP_LEFT);
+  if (clip[H][1] != INT_MAX || clip[V][1] != INT_MAX)
+    {
+      double x0 = xr_to_pt (clip[H][0] + xr->x);
+      double y0 = xr_to_pt (clip[V][0] + xr->y);
+      double x1 = xr_to_pt (clip[H][1] + xr->x);
+      double y1 = xr_to_pt (clip[V][1] + xr->y);
+
+      cairo_rectangle (xr->cairo, x0, y0, x1 - x0, y1 - y0);
+      cairo_clip (xr->cairo);
+    }
 }
 
-/* Draw top of page headers for THIS driver. */
 static void
-draw_headers (struct outp_driver *this)
+add_attr_with_start (PangoAttrList *list, PangoAttribute *attr, guint start_index)
 {
-  struct xr_driver_ext *ext = this->ext;
-  char *r1, *r2;
-  int x0, x1;
-  int y;
+  attr->start_index = start_index;
+  pango_attr_list_insert (list, attr);
+}
 
-  y = -3 * this->font_height;
-  x0 = this->prop_em_width;
-  x1 = this->width - this->prop_em_width;
+static int
+xr_layout_cell_text (struct xr_driver *xr,
+                     const struct cell_contents *contents, int footnote_idx,
+                     int bb[TABLE_N_AXES][2], int clip[TABLE_N_AXES][2],
+                     int y, int *widthp, int *brk)
+{
+  unsigned int options = contents->options;
+  struct xr_font *font;
+  bool merge_footnotes;
+  size_t length;
+  int w, h;
+
+  if (contents->n_footnotes == 0)
+    merge_footnotes = false;
+  else if (contents->n_footnotes == 1 && (options & TAB_ALIGNMENT) == TAB_RIGHT)
+    {
+      PangoAttrList *attrs;
+      char marker[16];
 
-  /* Draw box. */
-  cairo_rectangle (ext->cairo, 0, xr_to_pt (y), xr_to_pt (this->width),
-                   xr_to_pt (2 * (this->font_height
-                                  + ext->line_width + ext->line_gutter)));
-  cairo_save (ext->cairo);
-  cairo_set_source_rgb (ext->cairo, 0.9, 0.9, 0.9);
-  cairo_fill_preserve (ext->cairo);
-  cairo_restore (ext->cairo);
-  cairo_stroke (ext->cairo);
+      font = &xr->fonts[XR_FONT_MARKER];
 
-  y += ext->line_width + ext->line_gutter;
+      str_format_26adic (footnote_idx + 1, false, marker, sizeof marker);
+      pango_layout_set_text (font->layout, marker, strlen (marker));
 
-  r1 = xasprintf (_("%s - Page %d"), get_start_date (), ext->page_number);
-  r2 = xasprintf ("%s - %s", version, host_system);
+      attrs = pango_attr_list_new ();
+      pango_attr_list_insert (attrs, pango_attr_rise_new (7000));
+      pango_layout_set_attributes (font->layout, attrs);
+      pango_attr_list_unref (attrs);
 
-  draw_header_line (this, outp_title, r1, x0, x1, y);
-  y += this->font_height;
+      pango_layout_get_size (font->layout, &w, &h);
+      merge_footnotes = w > xr->cell_margin;
+      if (!merge_footnotes && clip[H][0] != clip[H][1])
+        {
+          cairo_save (xr->cairo);
+          xr_clip (xr, clip);
+          cairo_translate (xr->cairo,
+                           xr_to_pt (bb[H][1] + xr->x),
+                           xr_to_pt (y + xr->y));
+          pango_layout_set_alignment (font->layout, PANGO_ALIGN_LEFT);
+          pango_layout_set_width (font->layout, -1);
+          pango_cairo_show_layout (xr->cairo, font->layout);
+          cairo_restore (xr->cairo);
+        }
 
-  draw_header_line (this, outp_subtitle, r2, x0, x1, y);
+      pango_layout_set_attributes (font->layout, NULL);
+    }
+  else
+    merge_footnotes = true;
 
-  free (r1);
-  free (r2);
-}
-\f
-/* Format TEXT on THIS driver.
-   If DRAW is nonzero, draw the text.
-   The width of the widest line is stored into *WIDTH, if WIDTH
-   is nonnull.
-   The total height of the text written is stored into *HEIGHT,
-   if HEIGHT is nonnull. */
-static void
-text (struct outp_driver *this, const struct outp_text *text, bool draw,
-      int *width, int *height)
-{
-  struct xr_driver_ext *ext = this->ext;
-  struct xr_font *font = &ext->fonts[text->font];
+  font = (options & TAB_FIX ? &xr->fonts[XR_FONT_FIXED]
+          : options & TAB_EMPH ? &xr->fonts[XR_FONT_EMPHASIS]
+          : &xr->fonts[XR_FONT_PROPORTIONAL]);
+
+  length = strlen (contents->text);
+  if (merge_footnotes)
+    {
+      PangoAttrList *attrs;
+      struct string s;
+      size_t i;
+
+      bb[H][1] += xr->cell_margin;
+
+      ds_init_empty (&s);
+      ds_extend (&s, length + contents->n_footnotes * 10);
+      ds_put_cstr (&s, contents->text);
+      for (i = 0; i < contents->n_footnotes; i++)
+        {
+          char marker[16];
+
+          if (i > 0)
+            ds_put_byte (&s, ',');
+          str_format_26adic (footnote_idx + i + 1, false, marker, sizeof marker);
+          ds_put_cstr (&s, marker);
+        }
+      pango_layout_set_text (font->layout, ds_cstr (&s), ds_length (&s));
+      ds_destroy (&s);
+
+      attrs = pango_attr_list_new ();
+      add_attr_with_start (attrs, pango_attr_rise_new (7000), length);
+      add_attr_with_start (
+        attrs, pango_attr_font_desc_new (xr->fonts[XR_FONT_MARKER].desc), length);
+      pango_layout_set_attributes (font->layout, attrs);
+      pango_attr_list_unref (attrs);
+    }
+  else
+    pango_layout_set_text (font->layout, contents->text, -1);
 
-  pango_layout_set_text (font->layout,
-                         text->string.string, text->string.length);
   pango_layout_set_alignment (
     font->layout,
-    (text->justification == OUTP_RIGHT ? PANGO_ALIGN_RIGHT
-     : text->justification == OUTP_LEFT ? PANGO_ALIGN_LEFT
+    ((options & TAB_ALIGNMENT) == TAB_RIGHT ? PANGO_ALIGN_RIGHT
+     : (options & TAB_ALIGNMENT) == TAB_LEFT ? PANGO_ALIGN_LEFT
      : PANGO_ALIGN_CENTER));
-  pango_layout_set_width (font->layout, text->h == INT_MAX ? -1 : text->h);
-  pango_layout_set_wrap (font->layout, PANGO_WRAP_WORD_CHAR);
-  /* XXX need to limit number of lines to those that fit in text->v. */
+  pango_layout_set_width (
+    font->layout,
+    bb[H][1] == INT_MAX ? -1 : xr_to_pango (bb[H][1] - bb[H][0]));
+  pango_layout_set_wrap (font->layout, PANGO_WRAP_WORD);
 
-  if (draw)
+  if (clip[H][0] != clip[H][1])
     {
-      int x = text->x;
-      if (text->justification != OUTP_LEFT && text->h != INT_MAX)
+      cairo_save (xr->cairo);
+      xr_clip (xr, clip);
+      cairo_translate (xr->cairo,
+                       xr_to_pt (bb[H][0] + xr->x),
+                       xr_to_pt (y + xr->y));
+      pango_cairo_show_layout (xr->cairo, font->layout);
+
+      /* If enabled, this draws a blue rectangle around the extents of each
+         line of text, which can be rather useful for debugging layout
+         issues. */
+      if (0)
         {
-          int w, h, excess;
-          pango_layout_get_size (font->layout, &w, &h);
-          excess = text->h - w;
-          if (excess > 0)
+          PangoLayoutIter *iter;
+          iter = pango_layout_get_iter (font->layout);
+          do
             {
-              if (text->justification == OUTP_CENTER)
-                x += excess / 2;
-              else
-                x += excess;
+              PangoRectangle extents;
+
+              pango_layout_iter_get_line_extents (iter, &extents, NULL);
+              cairo_save (xr->cairo);
+              cairo_set_source_rgb (xr->cairo, 1, 0, 0);
+              dump_rectangle (xr,
+                              pango_to_xr (extents.x) - xr->x,
+                              pango_to_xr (extents.y) - xr->y,
+                              pango_to_xr (extents.x + extents.width) - xr->x,
+                              pango_to_xr (extents.y + extents.height) - xr->y);
+              cairo_restore (xr->cairo);
             }
+          while (pango_layout_iter_next_line (iter));
+          pango_layout_iter_free (iter);
         }
-      cairo_save (ext->cairo);
-      cairo_translate (ext->cairo, xr_to_pt (text->x), xr_to_pt (text->y));
-      pango_cairo_show_layout (ext->cairo, font->layout);
-      cairo_restore (ext->cairo);
+
+      cairo_restore (xr->cairo);
     }
 
-  if (width != NULL || height != NULL)
+  pango_layout_get_size (font->layout, &w, &h);
+  w = pango_to_xr (w);
+  h = pango_to_xr (h);
+  if (w > *widthp)
+    *widthp = w;
+  if (y + h >= bb[V][1])
     {
-      int w, h;
-      pango_layout_get_size (font->layout, &w, &h);
-      if (width != NULL)
-        *width = w;
-      if (height != NULL)
-        *height = h;
+      PangoLayoutIter *iter;
+      int best UNUSED = 0;
+
+      /* Choose a breakpoint between lines instead of in the middle of one. */
+      iter = pango_layout_get_iter (font->layout);
+      do
+        {
+          PangoRectangle extents;
+          int y0, y1;
+          int bottom;
+
+          pango_layout_iter_get_line_extents (iter, NULL, &extents);
+          pango_layout_iter_get_line_yrange (iter, &y0, &y1);
+          extents.x = pango_to_xr (extents.x);
+          extents.y = pango_to_xr (y0);
+          extents.width = pango_to_xr (extents.width);
+          extents.height = pango_to_xr (y1 - y0);
+          bottom = y + extents.y + extents.height;
+          if (bottom < bb[V][1])
+            {
+              if (brk && clip[H][0] != clip[H][1])
+                best = bottom;
+              if (brk)
+                *brk = bottom;
+            }
+          else
+            break;
+        }
+      while (pango_layout_iter_next_line (iter));
+      pango_layout_iter_free (iter);
+
+      /* If enabled, draws a green line across the chosen breakpoint, which can
+         be useful for debugging issues with breaking.  */
+      if (0)
+        {
+          if (best && !xr->nest)
+            {
+              cairo_save (xr->cairo);
+              cairo_set_source_rgb (xr->cairo, 0, 1, 0);
+              dump_line (xr, -xr->left_margin, best, xr->width + xr->right_margin, best);
+              cairo_restore (xr->cairo);
+            }
+        }
     }
+
+  pango_layout_set_attributes (font->layout, NULL);
+  return y + h;
 }
 
-static void
-xr_text_metrics (struct outp_driver *this, const struct outp_text *t,
-                 int *width, int *height)
+static int
+xr_layout_cell_subtable (struct xr_driver *xr,
+                         const struct cell_contents *contents,
+                         int footnote_idx UNUSED,
+                         int bb[TABLE_N_AXES][2],
+                         int clip[TABLE_N_AXES][2], int *widthp, int *brk)
 {
-  text (this, t, false, width, height);
+  int single_width, double_width;
+  struct render_params params;
+  struct render_pager *p;
+  int r[TABLE_N_AXES][2];
+  int width, height;
+  int i;
+
+  params.draw_line = xr_draw_line;
+  params.measure_cell_width = xr_measure_cell_width;
+  params.measure_cell_height = xr_measure_cell_height;
+  params.adjust_break = NULL;
+  params.draw_cell = xr_draw_cell;
+  params.aux = xr;
+  params.size[H] = bb[H][1] - bb[H][0];
+  params.size[V] = bb[V][1] - bb[V][0];
+  params.font_size[H] = xr->char_width;
+  params.font_size[V] = xr->char_height;
+
+  single_width = 2 * xr->line_gutter + xr->line_width;
+  double_width = 2 * xr->line_gutter + xr->line_space + 2 * xr->line_width;
+  for (i = 0; i < TABLE_N_AXES; i++)
+    {
+      params.line_widths[i][RENDER_LINE_NONE] = 0;
+      params.line_widths[i][RENDER_LINE_SINGLE] = single_width;
+      params.line_widths[i][RENDER_LINE_DOUBLE] = double_width;
+    }
+
+  xr->nest++;
+  p = render_pager_create (&params, contents->table);
+  width = render_pager_get_size (p, H);
+  height = render_pager_get_size (p, V);
+  if (bb[V][0] + height >= bb[V][1])
+    *brk = bb[V][0] + render_pager_get_best_breakpoint (p, bb[V][1] - bb[V][0]);
+
+  /* r = intersect(bb, clip) - bb. */
+  for (i = 0; i < TABLE_N_AXES; i++)
+    {
+      r[i][0] = MAX (bb[i][0], clip[i][0]) - bb[i][0];
+      r[i][1] = MIN (bb[i][1], clip[i][1]) - bb[i][0];
+    }
+
+  if (r[H][0] < r[H][1] && r[V][0] < r[V][1])
+    {
+      unsigned int alignment = contents->options & TAB_ALIGNMENT;
+      int save_x = xr->x;
+
+      cairo_save (xr->cairo);
+      xr_clip (xr, clip);
+      xr->x += bb[H][0];
+      if (alignment == TAB_RIGHT)
+        xr->x += params.size[H] - width;
+      else if (alignment == TAB_CENTER)
+        xr->x += (params.size[H] - width) / 2;
+      xr->y += bb[V][0];
+      render_pager_draw_region (p, r[H][0], r[V][0],
+                                r[H][1] - r[H][0], r[V][1] - r[V][0]);
+      xr->y -= bb[V][0];
+      xr->x = save_x;
+      cairo_restore (xr->cairo);
+    }
+  render_pager_destroy (p);
+  xr->nest--;
+
+  if (width > *widthp)
+    *widthp = width;
+  return bb[V][0] + height;
 }
 
 static void
-xr_text_draw (struct outp_driver *this, const struct outp_text *t)
+xr_layout_cell (struct xr_driver *xr, const struct table_cell *cell,
+                int footnote_idx,
+                int bb_[TABLE_N_AXES][2], int clip[TABLE_N_AXES][2],
+                int *width, int *height, int *brk)
 {
-  assert (this->page_open);
-  text (this, t, true, NULL, NULL);
+  int bb[TABLE_N_AXES][2];
+  size_t i;
+
+  *width = 0;
+  *height = 0;
+  if (brk)
+    *brk = 0;
+
+  memcpy (bb, bb_, sizeof bb);
+
+  /* If enabled, draws a blue rectangle around the cell extents, which can be
+     useful for debugging layout. */
+  if (0)
+    {
+      if (clip[H][0] != clip[H][1])
+        {
+          int offset = (xr->nest) * XR_POINT;
+
+          cairo_save (xr->cairo);
+          cairo_set_source_rgb (xr->cairo, 0, 0, 1);
+          dump_rectangle (xr,
+                          bb[H][0] + offset, bb[V][0] + offset,
+                          bb[H][1] - offset, bb[V][1] - offset);
+          cairo_restore (xr->cairo);
+        }
+    }
+
+  for (i = 0; i < cell->n_contents && bb[V][0] < bb[V][1]; i++)
+    {
+      const struct cell_contents *contents = &cell->contents[i];
+
+      if (brk)
+        *brk = bb[V][0];
+      if (i > 0)
+        {
+          bb[V][0] += xr->char_height / 2;
+          if (bb[V][0] >= bb[V][1])
+            break;
+          if (brk)
+            *brk = bb[V][0];
+        }
+
+      if (contents->text)
+        bb[V][0] = xr_layout_cell_text (xr, contents, footnote_idx, bb, clip,
+                                        bb[V][0], width, brk);
+      else
+        bb[V][0] = xr_layout_cell_subtable (xr, contents, footnote_idx,
+                                            bb, clip, width, brk);
+      footnote_idx += contents->n_footnotes;
+    }
+  *height = bb[V][0] - bb_[V][0];
 }
 \f
+struct output_driver_factory pdf_driver_factory =
+  { "pdf", "pspp.pdf", xr_pdf_create };
+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 };
+
+static const struct output_driver_class cairo_driver_class =
+{
+  "cairo",
+  xr_destroy,
+  xr_submit,
+  xr_flush,
+};
+\f
+/* GUI rendering helpers. */
+
+struct xr_rendering
+  {
+    struct output_item *item;
+
+    /* Table items. */
+    struct render_pager *p;
+    struct xr_driver *xr;
+  };
+
+#define CHART_WIDTH 500
+#define CHART_HEIGHT 375
+
+
+
+struct xr_driver *
+xr_driver_create (cairo_t *cairo, struct string_map *options)
+{
+  struct xr_driver *xr = xr_allocate ("cairo", 0, options);
+  if (!xr_set_cairo (xr, cairo))
+    {
+      output_driver_destroy (&xr->driver);
+      return NULL;
+    }
+  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);
+    }
+}
+
+static struct xr_rendering *
+xr_rendering_create_text (struct xr_driver *xr, const char *text, cairo_t *cr)
+{
+  struct table_item *table_item;
+  struct xr_rendering *r;
+
+  table_item = table_item_create (table_from_string (TAB_LEFT, text),
+                                  NULL, NULL);
+  r = xr_rendering_create (xr, &table_item->output_item, cr);
+  table_item_unref (table_item);
+
+  return r;
+}
+
+void
+xr_rendering_apply_options (struct xr_rendering *xr, struct string_map *o)
+{
+  if (is_table_item (xr->item))
+    apply_options (xr->xr, o);
+}
+
+struct xr_rendering *
+xr_rendering_create (struct xr_driver *xr, const struct output_item *item,
+                     cairo_t *cr)
+{
+  struct xr_rendering *r = NULL;
+
+  if (is_text_item (item))
+    r = xr_rendering_create_text (xr, text_item_get_text (to_text_item (item)),
+                                  cr);
+  else if (is_message_item (item))
+    {
+      const struct message_item *message_item = to_message_item (item);
+      const struct msg *msg = message_item_get_msg (message_item);
+      char *s = msg_to_string (msg, NULL);
+      r = xr_rendering_create_text (xr, s, cr);
+      free (s);
+    }
+  else if (is_table_item (item))
+    {
+      r = xzalloc (sizeof *r);
+      r->item = output_item_ref (item);
+      r->xr = xr;
+      xr_set_cairo (xr, cr);
+      r->p = render_pager_create (xr->params, to_table_item (item));
+    }
+  else if (is_chart_item (item))
+    {
+      r = xzalloc (sizeof *r);
+      r->item = output_item_ref (item);
+    }
+
+  return r;
+}
+
+void
+xr_rendering_destroy (struct xr_rendering *r)
+{
+  if (r)
+    {
+      output_item_unref (r->item);
+      render_pager_destroy (r->p);
+      free (r);
+    }
+}
+
+void
+xr_rendering_measure (struct xr_rendering *r, int *w, int *h)
+{
+  if (is_table_item (r->item))
+    {
+      *w = render_pager_get_size (r->p, H) / XR_POINT;
+      *h = render_pager_get_size (r->p, V) / XR_POINT;
+    }
+  else
+    {
+      *w = CHART_WIDTH;
+      *h = CHART_HEIGHT;
+    }
+}
+
+static void xr_draw_chart (const struct chart_item *, cairo_t *,
+                    double x, double y, double width, double height);
+
+/* Draws onto CR */
+void
+xr_rendering_draw_all (struct xr_rendering *r, cairo_t *cr)
+{
+  if (is_table_item (r->item))
+    {
+      struct xr_driver *xr = r->xr;
+
+      xr_set_cairo (xr, cr);
+
+      render_pager_draw (r->p);
+
+    }
+  else
+    xr_draw_chart (to_chart_item (r->item), cr,
+                   0, 0, CHART_WIDTH, CHART_HEIGHT);
+}
+
 static void
-xr_chart_initialise (struct outp_driver *this UNUSED, struct chart *ch UNUSED)
+xr_draw_chart (const struct chart_item *chart_item, cairo_t *cr,
+               double x, double y, double width, double height)
+{
+  struct xrchart_geometry geom;
+
+  cairo_save (cr);
+  cairo_translate (cr, x, y + height);
+  cairo_scale (cr, 1.0, -1.0);
+  xrchart_geometry_init (cr, &geom, width, height);
+  if (is_boxplot (chart_item))
+    xrchart_draw_boxplot (chart_item, cr, &geom);
+  else if (is_histogram_chart (chart_item))
+    xrchart_draw_histogram (chart_item, cr, &geom);
+  else if (is_np_plot_chart (chart_item))
+    xrchart_draw_np_plot (chart_item, cr, &geom);
+  else if (is_piechart (chart_item))
+    xrchart_draw_piechart (chart_item, cr, &geom);
+  else if (is_barchart (chart_item))
+    xrchart_draw_barchart (chart_item, cr, &geom);
+  else if (is_roc_chart (chart_item))
+    xrchart_draw_roc (chart_item, cr, &geom);
+  else if (is_scree (chart_item))
+    xrchart_draw_scree (chart_item, cr, &geom);
+  else if (is_spreadlevel_plot_chart (chart_item))
+    xrchart_draw_spreadlevel (chart_item, cr, &geom);
+  else if (is_scatterplot_chart (chart_item))
+    xrchart_draw_scatterplot (chart_item, cr, &geom);
+  else
+    NOT_REACHED ();
+  xrchart_geometry_free (cr, &geom);
+
+  cairo_restore (cr);
+}
+
+char *
+xr_draw_png_chart (const struct chart_item *item,
+                   const char *file_name_template, int number,
+                  const struct xr_color *fg,
+                  const struct xr_color *bg
+                  )
+{
+  const int width = 640;
+  const int length = 480;
+
+  cairo_surface_t *surface;
+  cairo_status_t status;
+  const char *number_pos;
+  char *file_name;
+  cairo_t *cr;
+
+  number_pos = strchr (file_name_template, '#');
+  if (number_pos != NULL)
+    file_name = xasprintf ("%.*s%d%s", (int) (number_pos - file_name_template),
+                           file_name_template, number, number_pos + 1);
+  else
+    file_name = xstrdup (file_name_template);
+
+  surface = cairo_image_surface_create (CAIRO_FORMAT_RGB24, width, length);
+  cr = cairo_create (surface);
+
+  cairo_set_source_rgb (cr, bg->red, bg->green, bg->blue);
+  cairo_paint (cr);
+
+  cairo_set_source_rgb (cr, fg->red, fg->green, fg->blue);
+
+  xr_draw_chart (item, cr, 0.0, 0.0, width, length);
+
+  status = cairo_surface_write_to_png (surface, file_name);
+  if (status != CAIRO_STATUS_SUCCESS)
+    msg (ME, _("error writing output file `%s': %s"),
+           file_name, cairo_status_to_string (status));
+
+  cairo_destroy (cr);
+  cairo_surface_destroy (surface);
+
+  return file_name;
+}
+\f
+struct xr_table_state
+  {
+    struct xr_render_fsm fsm;
+    struct table_item *table_item;
+    struct render_pager *p;
+  };
+
+static bool
+xr_table_render (struct xr_render_fsm *fsm, struct xr_driver *xr)
 {
-#ifdef NO_CHARTS
-  ch->lp = NULL;
-#else
-  /* XXX libplot doesn't support Cairo yet. */
-#endif
+  struct xr_table_state *ts = UP_CAST (fsm, struct xr_table_state, fsm);
+
+  while (render_pager_has_next (ts->p))
+    {
+      int used;
+
+      used = render_pager_draw_next (ts->p, xr->length - xr->y);
+      if (!used)
+        {
+          assert (xr->y > 0);
+          return true;
+        }
+      else
+        xr->y += used;
+    }
+  return false;
 }
 
 static void
-xr_chart_finalise (struct outp_driver *this UNUSED, struct chart *ch UNUSED)
+xr_table_destroy (struct xr_render_fsm *fsm)
+{
+  struct xr_table_state *ts = UP_CAST (fsm, struct xr_table_state, fsm);
+
+  table_item_unref (ts->table_item);
+  render_pager_destroy (ts->p);
+  free (ts);
+}
+
+static struct xr_render_fsm *
+xr_render_table (struct xr_driver *xr, const struct table_item *table_item)
 {
-#ifndef NO_CHARTS
-  /* XXX libplot doesn't support Cairo yet. */
-#endif
+  struct xr_table_state *ts;
+
+  ts = xmalloc (sizeof *ts);
+  ts->fsm.render = xr_table_render;
+  ts->fsm.destroy = xr_table_destroy;
+  ts->table_item = table_item_ref (table_item);
+
+  if (xr->y > 0)
+    xr->y += xr->char_height;
+
+  ts->p = render_pager_create (xr->params, table_item);
+
+  return &ts->fsm;
 }
 \f
-/* Attempts to load FONT, initializing its other members based on
-   its 'string' member and the information in THIS.  Returns true
-   if successful, otherwise false. */
+struct xr_chart_state
+  {
+    struct xr_render_fsm fsm;
+    struct chart_item *chart_item;
+  };
+
 static bool
-load_font (struct outp_driver *this, struct xr_font *font)
+xr_chart_render (struct xr_render_fsm *fsm, struct xr_driver *xr)
 {
-  struct xr_driver_ext *x = this->ext;
-  PangoContext *context;
-  PangoLanguage *language;
+  struct xr_chart_state *cs = UP_CAST (fsm, struct xr_chart_state, fsm);
+
+  const int chart_height = xr->length;
 
-  font->desc = pango_font_description_from_string (font->string);
-  if (font->desc == NULL)
+  if (xr->y > xr->length - chart_height)
+    return true;
+
+  if (xr->cairo != NULL)
     {
-      error (0, 0, _("\"%s\": bad font specification"), font->string);
-      return false;
+      xr_draw_chart (cs->chart_item, xr->cairo,
+                    0.0,
+                    xr_to_pt (xr->y),
+                    xr_to_pt (xr->width),
+                    xr_to_pt (chart_height));
     }
-  pango_font_description_set_absolute_size (font->desc, this->font_height);
+  xr->y += chart_height;
 
-  font->layout = pango_cairo_create_layout (x->cairo);
-  pango_layout_set_font_description (font->layout, font->desc);
+  return false;
+}
 
-  language = pango_language_get_default ();
-  context = pango_layout_get_context (font->layout);
-  font->metrics = pango_context_get_metrics (context, font->desc, language);
+static void
+xr_chart_destroy (struct xr_render_fsm *fsm)
+{
+  struct xr_chart_state *cs = UP_CAST (fsm, struct xr_chart_state, fsm);
 
-  return true;
+  chart_item_unref (cs->chart_item);
+  free (cs);
+}
+
+static struct xr_render_fsm *
+xr_render_chart (const struct chart_item *chart_item)
+{
+  struct xr_chart_state *cs;
+
+  cs = xmalloc (sizeof *cs);
+  cs->fsm.render = xr_chart_render;
+  cs->fsm.destroy = xr_chart_destroy;
+  cs->chart_item = chart_item_ref (chart_item);
+
+  return &cs->fsm;
+}
+\f
+static bool
+xr_eject_render (struct xr_render_fsm *fsm UNUSED, struct xr_driver *xr)
+{
+  return xr->y > 0;
 }
 
-/* Frees FONT. */
 static void
-free_font (struct xr_font *font)
+xr_eject_destroy (struct xr_render_fsm *fsm UNUSED)
 {
-  free (font->string);
-  if (font->desc != NULL)
-    pango_font_description_free (font->desc);
-  pango_font_metrics_unref (font->metrics);
-  g_object_unref (font->layout);
+  /* Nothing to do. */
 }
 
-/* Cairo driver class. */
-const struct outp_class cairo_class =
+static struct xr_render_fsm *
+xr_render_eject (void)
 {
-  "cairo",
-  0,
+  static struct xr_render_fsm eject_renderer =
+    {
+      xr_eject_render,
+      xr_eject_destroy
+    };
 
-  xr_open_driver,
-  xr_close_driver,
+  return &eject_renderer;
+}
+\f
+static struct xr_render_fsm *
+xr_create_text_renderer (struct xr_driver *xr, const char *text)
+{
+  struct table_item *table_item;
+  struct xr_render_fsm *fsm;
 
-  xr_open_page,
-  xr_close_page,
-  NULL,
+  table_item = table_item_create (table_from_string (TAB_LEFT, text),
+                                  NULL, NULL);
+  fsm = xr_render_table (xr, table_item);
+  table_item_unref (table_item);
 
-  xr_submit,
+  return fsm;
+}
+
+static struct xr_render_fsm *
+xr_render_text (struct xr_driver *xr, const struct text_item *text_item)
+{
+  enum text_item_type type = text_item_get_type (text_item);
+  const char *text = text_item_get_text (text_item);
 
-  xr_line,
-  xr_text_metrics,
-  xr_text_draw,
+  switch (type)
+    {
+    case TEXT_ITEM_TITLE:
+      free (xr->title);
+      xr->title = xstrdup (text);
+      break;
 
-  xr_chart_initialise,
-  xr_chart_finalise
-};
+    case TEXT_ITEM_SUBTITLE:
+      free (xr->subtitle);
+      xr->subtitle = xstrdup (text);
+      break;
+
+    case TEXT_ITEM_COMMAND_CLOSE:
+      break;
+
+    case TEXT_ITEM_BLANK_LINE:
+      if (xr->y > 0)
+        xr->y += xr->char_height;
+      break;
+
+    case TEXT_ITEM_EJECT_PAGE:
+      if (xr->y > 0)
+        return xr_render_eject ();
+      break;
+
+    default:
+      return xr_create_text_renderer (xr, text);
+    }
+
+  return NULL;
+}
+
+static struct xr_render_fsm *
+xr_render_message (struct xr_driver *xr,
+                   const struct message_item *message_item)
+{
+  const struct msg *msg = message_item_get_msg (message_item);
+  struct xr_render_fsm *fsm;
+  char *s;
+
+  s = msg_to_string (msg, xr->command_name);
+  fsm = xr_create_text_renderer (xr, s);
+  free (s);
+
+  return fsm;
+}
+
+static struct xr_render_fsm *
+xr_render_output_item (struct xr_driver *xr,
+                       const struct output_item *output_item)
+{
+  if (is_table_item (output_item))
+    return xr_render_table (xr, to_table_item (output_item));
+  else if (is_chart_item (output_item))
+    return xr_render_chart (to_chart_item (output_item));
+  else if (is_text_item (output_item))
+    return xr_render_text (xr, to_text_item (output_item));
+  else if (is_message_item (output_item))
+    return xr_render_message (xr, to_message_item (output_item));
+  else
+    return NULL;
+}