render: Render table_items instead of tables.
[pspp] / src / output / cairo.c
index a2d5fef9d1ddfd36914ef6f3fa22e55fdcb48424..3e8c256f8193f31e4ffc71b513c7e034b4b28556 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 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 <output/cairo.h>
-
-#include <libpspp/assertion.h>
-#include <libpspp/start-date.h>
-#include <libpspp/version.h>
-#include <output/afm.h>
-#include <output/chart-provider.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 "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/plot-hist.h"
+#include "output/charts/roc-chart.h"
+#include "output/charts/spreadlevel-plot.h"
+#include "output/charts/scree.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)
-
-   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=serif
-   emph-font=serif italic
-   fixed-font=monospace
-   font-size=10000
+/* This file uses TABLE_HORZ and TABLE_VERT enough to warrant abbreviating. */
+#define H TABLE_HORZ
+#define V TABLE_VERT
 
-   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
@@ -78,12 +75,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
   {
@@ -92,447 +83,610 @@ enum xr_output_type
     XR_SVG
   };
 
+/* Cairo fonts. */
+enum xr_font_type
+  {
+    XR_FONT_PROPORTIONAL,
+    XR_FONT_EMPHASIS,
+    XR_FONT_FIXED,
+    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
   {
-    cairo_t *cairo;
-    struct xr_font fonts[OUTP_FONT_CNT];
+    /* 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];
+
+    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. */
+
+    int min_break[TABLE_N_AXES]; /* Min cell size to break across pages. */
+
+    struct xr_color bg;    /* Background color */
+    struct xr_color fg;    /* Foreground color */
+
+    /* 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;
   };
 
-struct xr_driver_options
-  {
-    struct outp_driver *driver;
+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 *min, int *max);
+static int xr_measure_cell_height (void *, const struct table_cell *,
+                                   int width);
+static void xr_draw_cell (void *, const struct table_cell *,
+                          int bb[TABLE_N_AXES][2],
+                          int clip[TABLE_N_AXES][2]);
+static int xr_adjust_break (void *, const struct table_cell *,
+                            int width, int height);
+
+static struct xr_render_fsm *xr_render_output_item (
+  struct xr_driver *, const struct output_item *);
+\f
+/* Output driver basics. */
 
-    char *file_name;            /* Output file name. */
-    enum xr_output_type file_type; /* Type of output file. */
+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);
+}
 
-    bool portrait;              /* Portrait mode? */
+/* 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));
 
-    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. */
-  };
+  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;
+       }
+    }
 
-static bool handle_option (void *options, const char *key,
-                           const struct string *val);
-static void draw_headers (struct outp_driver *this);
+  free (string);
 
-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);
-\f
-/* Driver initialization. */
+  /* Convert 16 bit ints to float */
+  color->red = red / (double) 0xFFFF;
+  color->green = green / (double) 0xFFFF;
+  color->blue = blue / (double) 0xFFFF;
+}
 
-static struct outp_driver *
-xr_allocate (const char *name, int types)
+static PangoFontDescription *
+parse_font (struct output_driver *d, struct string_map *options,
+            const char *key, const char *default_value,
+            int default_size)
 {
-  struct outp_driver *this;
-  struct xr_driver_ext *x;
-  size_t i;
+  PangoFontDescription *desc;
+  char *string;
 
-  this = outp_allocate_driver (&cairo_class, name, types);
-  this->width = this->length = 0;
-  this->font_height = XR_POINT * 10;
-  this->ext = x = xzalloc (sizeof *x);
-  x->cairo = NULL;
-  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++)
+  /* 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)
     {
-      struct xr_font *font = &x->fonts[i];
-      font->desc = NULL;
-      font->metrics = NULL;
-      font->layout = 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);
     }
-  x->draw_headers = true;
-  x->page_number = 0;
-  x->line_gutter = XR_POINT;
-  x->line_space = XR_POINT;
-  x->line_width = XR_POINT / 2;
+  free (string);
 
-  return this;
+  /* 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 bool
-xr_set_cairo (struct outp_driver *this, cairo_t *cairo)
+
+static void
+apply_options (struct xr_driver *xr, struct string_map *o)
 {
-  struct xr_driver_ext *x = this->ext;
+  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;
 
-  x->cairo = cairo;
+  for (i = 0; i < XR_N_FONTS; i++)
+    {
+      struct xr_font *font = &xr->fonts[i];
 
-  cairo_set_line_width (x->cairo, xr_to_pt (x->line_width));
+      if (font->desc != NULL)
+        pango_font_description_free (font->desc);
+    }
 
-  for (i = 0; i < OUTP_FONT_CNT; i++)
-    if (!load_font (this, &x->fonts[i]))
-      return false;
+  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->line_gutter = parse_dimension (opt (d, o, "gutter", "3pt")) * scale;
+  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;
+}
 
-  this->fixed_width = text_width (this, "0", OUTP_FIXED);
-  this->prop_em_width = text_width (this, "0", OUTP_PROPORTIONAL);
+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;
 
-  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);
+  output_driver_init (d, &cairo_driver_class, name, device_type);
 
-  return true;
+  apply_options (xr, o);
+
+  return xr;
 }
 
-struct outp_driver *
-xr_create_driver (cairo_t *cairo)
+static int
+pango_to_xr (int pango)
 {
-  struct outp_driver *this;
+  return (XR_POINT != PANGO_SCALE
+          ? ceil (pango * (1. * XR_POINT / PANGO_SCALE))
+          : pango);
+}
 
-  this = xr_allocate ("cairo", 0);
-  this->width = INT_MAX / 8;
-  this->length = INT_MAX / 8;
-  if (!xr_set_cairo (this, cairo))
-    {
-      this->class->close_driver (this);
-      outp_free_driver (this);
-      return NULL;
-    }
-  return this;
+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 (const char *name, int types, struct substring option_string)
+xr_set_cairo (struct xr_driver *xr, cairo_t *cairo)
 {
-  struct outp_driver *this;
-  struct xr_driver_ext *x;
-  struct xr_driver_options options;
-  cairo_surface_t *surface;
-  cairo_status_t status;
-  double width_pt, length_pt;
-
-  this = xr_allocate (name, types);
-  x = this->ext;
+  int i;
 
-  options.driver = this;
-  options.file_name = xstrdup ("pspp.pdf");
-  options.file_type = XR_PDF;
-  options.portrait = true;
-  outp_get_paper_size ("", &options.paper_width, &options.paper_length);
-  options.left_margin = XR_INCH / 2;
-  options.right_margin = XR_INCH / 2;
-  options.top_margin = XR_INCH / 2;
-  options.bottom_margin = XR_INCH / 2;
+  xr->cairo = cairo;
 
-  outp_parse_options (this->name, option_string, handle_option, &options);
+  cairo_set_line_width (xr->cairo, xr_to_pt (xr->line_width));
 
-  width_pt = options.paper_width / 1000.0;
-  length_pt = options.paper_length / 1000.0;
-  if (options.portrait)
+  xr->char_width = 0;
+  xr->char_height = 0;
+  for (i = 0; i < XR_N_FONTS; i++)
     {
-      this->width = pt_to_xr (width_pt);
-      this->length = pt_to_xr (length_pt);
+      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);
+
+      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
+
+  if (xr->params == NULL)
     {
-      this->width = pt_to_xr (width_pt);
-      this->length = pt_to_xr (length_pt);
-    }
-  if (x->draw_headers)
-    options.top_margin += 3 * this->font_height;
-  this->width -= options.left_margin + options.right_margin;
-  this->length -= options.top_margin + options.bottom_margin;
-
-  if (options.file_type == XR_PDF)
-    surface = cairo_pdf_surface_create (options.file_name,
-                                        width_pt, length_pt);
-  else if (options.file_type == XR_PS)
-    surface = cairo_ps_surface_create (options.file_name, width_pt, length_pt);
-  else if (options.file_type == XR_SVG)
-    surface = cairo_svg_surface_create (options.file_name,
-                                        width_pt, 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];
+    }
+
+  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"),
-             options.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 (options.left_margin),
-                   xr_to_pt (options.top_margin));
+  if (!xr_set_cairo (xr, xr->cairo))
+    goto error;
+
+  cairo_save (xr->cairo);
+  xr_driver_next_page (xr, xr->cairo);
 
-  if (this->length / this->font_height < 15)
+  if (xr->width / xr->char_width < MIN_WIDTH)
     {
-      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 "
-                     "room for %d lines."),
-             this->length / this->font_height);
+      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 (!xr_set_cairo (this, x->cairo))
-    goto error;
+  if (xr->length / xr->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,
+             xr->length / xr->char_height);
+      goto error;
+    }
 
-  outp_register_driver (this);
-  free (options.file_name);
-  return true;
+  return &xr->driver;
 
  error:
-  this->class->close_driver (this);
-  outp_free_driver (this);
-  free (options.file_name);
-  return false;
+  output_driver_destroy (&xr->driver);
+  return NULL;
 }
 
-static bool
-xr_close_driver (struct outp_driver *this)
+static struct output_driver *
+xr_pdf_create (const char *file_name, enum settings_output_devices device_type,
+               struct string_map *o)
+{
+  return xr_create (file_name, device_type, o, XR_PDF);
+}
+
+static struct output_driver *
+xr_ps_create (const char *file_name, enum settings_output_devices device_type,
+               struct string_map *o)
+{
+  return xr_create (file_name, device_type, o, XR_PS);
+}
+
+static struct output_driver *
+xr_svg_create (const char *file_name, enum settings_output_devices device_type,
+               struct string_map *o)
 {
-  struct xr_driver_ext *x = this->ext;
-  bool ok = true;
+  return xr_create (file_name, device_type, o, XR_SVG);
+}
+
+static void
+xr_destroy (struct output_driver *driver)
+{
+  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, _("error writing output file for %s driver: %s"),
-               this->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);
     }
 
-  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 (void *options_, const char *key, const struct string *val)
+static void
+xr_init_caption_cell (const char *caption, struct table_cell *cell,
+                      struct cell_contents *contents)
 {
-  struct xr_driver_options *options = options_;
-  struct outp_driver *this = options->driver;
-  struct xr_driver_ext *x = this->ext;
-  int subcat;
-  char *value = ds_cstr (val);
+  contents->options = TAB_LEFT;
+  contents->text = CONST_CAST (char *, caption);
+  contents->table = NULL;
+  cell->contents = contents;
+  cell->n_contents = 1;
+  cell->destructor = NULL;
+}
+
+static struct render_pager *
+xr_render_table_item (struct xr_driver *xr, const struct table_item *item,
+                      int *caption_widthp, int *caption_heightp)
+{
+  const char *caption = table_item_get_caption (item);
 
-  switch (outp_match_keyword (key, option_tab, &subcat))
+  if (caption != NULL)
     {
-    case -1:
-      error (0, 0,
-             _("unknown configuration parameter `%s' for %s device "
-               "driver"), key, this->class->name);
-      break;
-    case output_file_arg:
-      free (options->file_name);
-      options->file_name = xstrdup (value);
-      break;
-    case output_type_arg:
-      if (!strcmp (value, "pdf"))
-        options->file_type = XR_PDF;
-      else if (!strcmp (value, "ps"))
-        options->file_type = XR_PS;
-      else if (!strcmp (value, "svg"))
-        options->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,
-                           &options->paper_width, &options->paper_length);
-      break;
-    case orientation_arg:
-      if (!strcmp (value, "portrait"))
-       options->portrait = true;
-      else if (!strcmp (value, "landscape"))
-       options->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:
-           options->left_margin = dimension;
-           break;
-         case 1:
-           options->right_margin = dimension;
-           break;
-         case 2:
-           options->top_margin = dimension;
-           break;
-         case 3:
-           options->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 ();
+      /* XXX doesn't do well with very large captions */
+      struct cell_contents contents;
+      int min_width, max_width;
+      struct table_cell cell;
+
+      xr_init_caption_cell (caption, &cell, &contents);
+
+      xr_measure_cell_width (xr, &cell, &min_width, &max_width);
+      *caption_widthp = MIN (max_width, xr->width);
+      *caption_heightp = xr_measure_cell_height (xr, &cell, *caption_widthp);
     }
+  else
+    *caption_heightp = 0;
 
-  return true;
+  return render_pager_create (xr->params, item);
 }
-\f
-/* Basic file operations. */
 
 static void
-xr_open_page (struct outp_driver *this)
+xr_submit (struct output_driver *driver, const struct output_item *output_item)
 {
-  struct xr_driver_ext *x = this->ext;
+  struct xr_driver *xr = xr_driver_cast (driver);
 
-  x->page_number++;
+  output_driver_track_current_command (output_item, &xr->command_name);
 
-  if (x->draw_headers)
-    draw_headers (this);
+  xr_driver_output_item (xr, output_item);
+  while (xr_driver_need_new_page (xr))
+    {
+      cairo_restore (xr->cairo);
+      cairo_show_page (xr->cairo);
+      cairo_save (xr->cairo);
+      xr_driver_next_page (xr, xr->cairo);
+    }
 }
+\f
+/* Functions for rendering a series of output items to a series of Cairo
+   contexts, with pagination.
 
-static void
-xr_close_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.  CAIRO may be null to skip actually
+   rendering the page (which might be useful to find out how many pages an
+   output document has without actually rendering it). */
+void
+xr_driver_next_page (struct xr_driver *xr, cairo_t *cairo)
+{
+  if (cairo != NULL)
+    {
+      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);
+}
+
+/* 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)
 {
-  struct xr_driver_ext *x = this->ext;
-  cairo_show_page (x->cairo);
+  assert (xr->fsm == NULL);
+  xr->fsm = xr_render_output_item (xr, output_item);
+  xr_driver_run_fsm (xr);
 }
 
-static void
-xr_output_chart (struct outp_driver *this, const struct chart *chart)
+/* 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)
 {
-  struct xr_driver_ext *x = this->ext;
-  struct chart_geometry geom;
+  return xr->fsm != NULL;
+}
 
-  outp_eject_page (this);
-  outp_open_page (this);
+/* 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;
+}
 
-  cairo_save (x->cairo);
-  cairo_translate (x->cairo, 0.0, xr_to_pt (this->length));
-  cairo_scale (x->cairo, 1.0, -1.0);
-  chart_geometry_init (x->cairo, &geom,
-                       xr_to_pt (this->width), xr_to_pt (this->length));
-  chart_draw (chart, x->cairo, &geom);
-  chart_geometry_free (x->cairo);
-  cairo_restore (x->cairo);
+static void
+xr_driver_destroy_fsm (struct xr_driver *xr)
+{
+  if (xr->fsm != NULL)
+    {
+      xr->fsm->destroy (xr->fsm);
+      xr->fsm = NULL;
+    }
+}
 
-  outp_close_page (this);
+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 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)
 {
-  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 (y1 + xr->y));
+  cairo_stroke (xr->cairo);
+}
+
+static void UNUSED
+dump_rectangle (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 (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,
@@ -540,19 +694,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);
     }
 }
 
@@ -561,33 +714,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 left = styles[V][0];
+  const int bottom = styles[H][1];
+  const int right = styles[V][1];
+
   /* The algorithm here is somewhat subtle, to allow it to handle
      all the kinds of intersections that we need.
 
@@ -617,16 +771,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 = left == RENDER_LINE_DOUBLE || right == RENDER_LINE_DOUBLE;
 
   /* When horizontal lines are doubled,
      the left-side line along y1 normally runs from x0 to x2,
@@ -653,16 +807,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 = left == RENDER_LINE_DOUBLE;
+  bool shorten_x2_lines = right == 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;
@@ -670,242 +824,831 @@ 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, left, right, 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, left, right, shorten_y1_lines);
+      horz_line (xr, x0, x1, x2, x3, y2, left, right, 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 int
-text_width (struct outp_driver *this, const char *string, enum outp_font font)
+static void
+xr_measure_cell_width (void *xr_, const struct table_cell *cell,
+                       int *min_width, int *max_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 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, bb, clip, max_width, &h, NULL);
+
+  bb[H][1] = 1;
+  xr_layout_cell (xr, cell, bb, clip, min_width, &h, NULL);
+}
 
-  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 int
+xr_measure_cell_height (void *xr_, const struct table_cell *cell, 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;
+  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, bb, clip, &w, &h, NULL);
+  return h;
 }
 
-/* Writes LEFT left-justified and RIGHT right-justified within
-   (X0...X1) at Y.  LEFT or RIGHT or both may be null. */
 static void
-draw_header_line (struct outp_driver *this,
-                  const char *left, const char *right,
-                  int x0, int x1, int y)
+xr_draw_cell (void *xr_, const struct table_cell *cell,
+              int bb[TABLE_N_AXES][2], 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);
+  struct xr_driver *xr = xr_;
+  int w, h, brk;
+
+  xr_layout_cell (xr, cell, bb, clip, &w, &h, &brk);
 }
 
-/* Draw top of page headers for THIS driver. */
+static int
+xr_adjust_break (void *xr_, const struct table_cell *cell,
+                 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, width) < height)
+    return -1;
+
+  bb[H][0] = 0;
+  bb[H][1] = width;
+  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, bb, clip, &w, &h, &brk);
+  return brk;
+}
+\f
 static void
-draw_headers (struct outp_driver *this)
+xr_clip (struct xr_driver *xr, int clip[TABLE_N_AXES][2])
 {
-  struct xr_driver_ext *ext = this->ext;
-  char *r1, *r2;
-  int x0, x1;
-  int y;
+  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);
 
-  y = -3 * this->font_height;
-  x0 = this->prop_em_width;
-  x1 = this->width - this->prop_em_width;
+      cairo_rectangle (xr->cairo, x0, y0, x1 - x0, y1 - y0);
+      cairo_clip (xr->cairo);
+    }
+}
 
-  /* 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);
+static int
+xr_layout_cell_text (struct xr_driver *xr,
+                     const struct cell_contents *contents,
+                     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;
+  int w, h;
 
-  y += ext->line_width + ext->line_gutter;
+  font = (options & TAB_FIX ? &xr->fonts[XR_FONT_FIXED]
+          : options & TAB_EMPH ? &xr->fonts[XR_FONT_EMPHASIS]
+          : &xr->fonts[XR_FONT_PROPORTIONAL]);
 
-  r1 = xasprintf (_("%s - Page %d"), get_start_date (), ext->page_number);
-  r2 = xasprintf ("%s - %s", version, host_system);
+  pango_layout_set_text (font->layout, contents->text, -1);
 
-  draw_header_line (this, outp_title, r1, x0, x1, y);
-  y += this->font_height;
+  pango_layout_set_alignment (
+    font->layout,
+    ((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,
+    bb[H][1] == INT_MAX ? -1 : xr_to_pango (bb[H][1] - bb[H][0]));
+  pango_layout_set_wrap (font->layout, PANGO_WRAP_WORD);
 
-  draw_header_line (this, outp_subtitle, r2, x0, x1, y);
+  if (clip[H][0] != clip[H][1])
+    {
+      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)
+        {
+          PangoLayoutIter *iter;
+          iter = pango_layout_get_iter (font->layout);
+          do
+            {
+              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_restore (xr->cairo);
+    }
+
+  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])
+    {
+      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;
+              *brk = bottom;
+            }
+          else
+            break;
+        }
+      while (pango_layout_iter_next_line (iter));
 
-  free (r1);
-  free (r2);
+      /* 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);
+            }
+        }
+    }
+  return y + h;
 }
-\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 int
+xr_layout_cell_subtable (struct xr_driver *xr,
+                         const struct cell_contents *contents,
+                         int bb[TABLE_N_AXES][2],
+                         int clip[TABLE_N_AXES][2], int *widthp, int *brk)
+{
+  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
-text (struct outp_driver *this, const struct outp_text *text, bool draw,
-      int *width, int *height)
+xr_layout_cell (struct xr_driver *xr, const struct table_cell *cell,
+                int bb_[TABLE_N_AXES][2], int clip[TABLE_N_AXES][2],
+                int *width, int *height, int *brk)
 {
-  struct xr_driver_ext *ext = this->ext;
-  struct xr_font *font = &ext->fonts[text->font];
+  int bb[TABLE_N_AXES][2];
+  size_t i;
 
-  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
-     : 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. */
+  *width = 0;
+  *height = 0;
+  if (brk)
+    *brk = 0;
 
-  if (draw)
+  memcpy (bb, bb_, sizeof bb);
+
+  /* If enabled, draws a blue rectangle around the cell extents, which can be
+     useful for debugging layout. */
+  if (0)
     {
-      int x = text->x;
-      if (text->justification != OUTP_LEFT && text->h != INT_MAX)
+      if (clip[H][0] != clip[H][1])
         {
-          int w, h, excess;
-          pango_layout_get_size (font->layout, &w, &h);
-          excess = text->h - w;
-          if (excess > 0)
-            {
-              if (text->justification == OUTP_CENTER)
-                x += excess / 2;
-              else
-                x += excess;
-            }
+          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);
         }
-      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);
     }
 
-  if (width != NULL || height != NULL)
+  for (i = 0; i < cell->n_contents && bb[V][0] < bb[V][1]; i++)
     {
-      int w, h;
-      pango_layout_get_size (font->layout, &w, &h);
-      if (width != NULL)
-        *width = w;
-      if (height != NULL)
-        *height = h;
+      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, bb, clip,
+                                        bb[V][0], width, brk);
+      else
+        bb[V][0] = xr_layout_cell_subtable (xr, contents, bb, clip, width, brk);
     }
+  *height = bb[V][0] - bb_[V][0];
 }
 
 static void
-xr_text_metrics (struct outp_driver *this, const struct outp_text *t,
-                 int *width, int *height)
+xr_draw_title (struct xr_driver *xr, const char *title,
+               int title_width, int title_height)
+{
+  struct cell_contents contents;
+  struct table_cell cell;
+  int bb[TABLE_N_AXES][2];
+
+  xr_init_caption_cell (title, &cell, &contents);
+  bb[H][0] = 0;
+  bb[H][1] = title_width;
+  bb[V][0] = 0;
+  bb[V][1] = title_height;
+  xr_draw_cell (xr, &cell, bb, bb);
+}
+\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;
+    int title_width;
+    int title_height;
+  };
+
+#define CHART_WIDTH 500
+#define CHART_HEIGHT 375
+
+
+
+struct xr_driver *
+xr_driver_create (cairo_t *cairo, struct string_map *options)
 {
-  text (this, t, false, width, height);
+  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);
+  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 = xr_render_table_item (xr, to_table_item (item),
+                                   &r->title_width, &r->title_height);
+    }
+  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))
+    {
+      int w0 = render_pager_get_size (r->p, H);
+      int w1 = r->title_width;
+      *w = MAX (w0, w1) / XR_POINT;
+      *h = (render_pager_get_size (r->p, V) + r->title_height) / 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 at least the region of R that is enclosed in (X,Y)-(X+W,Y+H),
+   and possibly some additional parts. */
+void
+xr_rendering_draw (struct xr_rendering *r, cairo_t *cr,
+                   int x, int y, int w, int h)
+{
+  if (is_table_item (r->item))
+    {
+      struct xr_driver *xr = r->xr;
+
+      xr_set_cairo (xr, cr);
+
+      if (r->title_height > 0)
+        {
+          xr->y = 0;
+          xr_draw_title (xr, table_item_get_caption (to_table_item (r->item)),
+                         r->title_width, r->title_height);
+        }
+
+      xr->y = r->title_height;
+      render_pager_draw_region (r->p,
+                                x * XR_POINT, (y * XR_POINT) - r->title_height,
+                                w * XR_POINT, h * XR_POINT);
+    }
+  else
+    xr_draw_chart (to_chart_item (r->item), cr,
+                   0, 0, CHART_WIDTH, CHART_HEIGHT);
 }
 
 static void
-xr_text_draw (struct outp_driver *this, const struct outp_text *t)
+xr_draw_chart (const struct chart_item *chart_item, cairo_t *cr,
+               double x, double y, double width, double height)
 {
-  text (this, t, true, NULL, NULL);
+  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_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
+    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
-/* 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_table_state
+  {
+    struct xr_render_fsm fsm;
+    struct table_item *table_item;
+    struct render_pager *p;
+    int caption_height;
+  };
+
 static bool
-load_font (struct outp_driver *this, struct xr_font *font)
+xr_table_render (struct xr_render_fsm *fsm, struct xr_driver *xr)
 {
-  struct xr_driver_ext *x = this->ext;
-  PangoContext *context;
-  PangoLanguage *language;
+  struct xr_table_state *ts = UP_CAST (fsm, struct xr_table_state, fsm);
 
-  font->desc = pango_font_description_from_string (font->string);
-  if (font->desc == NULL)
+  while (render_pager_has_next (ts->p))
     {
-      error (0, 0, _("\"%s\": bad font specification"), font->string);
-      return false;
+      int caption_height = ts->caption_height;
+      int used;
+
+      xr->y += caption_height;
+      used = render_pager_draw_next (ts->p, xr->length - xr->y);
+      xr->y -= caption_height;
+      if (!used)
+        {
+          assert (xr->y > 0);
+          return true;
+        }
+      else
+        {
+          if (ts->caption_height)
+            {
+              if (xr->cairo)
+                xr_draw_title (xr, table_item_get_caption (ts->table_item),
+                               xr->width, ts->caption_height);
+              ts->caption_height = 0;
+            }
+          xr->y += caption_height + used;
+        }
     }
-  pango_font_description_set_absolute_size (font->desc, this->font_height);
+  return false;
+}
 
-  font->layout = pango_cairo_create_layout (x->cairo);
-  pango_layout_set_font_description (font->layout, font->desc);
+static void
+xr_table_destroy (struct xr_render_fsm *fsm)
+{
+  struct xr_table_state *ts = UP_CAST (fsm, struct xr_table_state, fsm);
 
-  language = pango_language_get_default ();
-  context = pango_layout_get_context (font->layout);
-  font->metrics = pango_context_get_metrics (context, font->desc, language);
+  table_item_unref (ts->table_item);
+  render_pager_destroy (ts->p);
+  free (ts);
+}
 
-  return true;
+static struct xr_render_fsm *
+xr_render_table (struct xr_driver *xr, const struct table_item *table_item)
+{
+  struct xr_table_state *ts;
+  int caption_width;
+
+  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 = xr_render_table_item (xr, table_item,
+                                &caption_width, &ts->caption_height);
+  xr->params->size[V] = xr->length - ts->caption_height;
+
+  return &ts->fsm;
+}
+\f
+struct xr_chart_state
+  {
+    struct xr_render_fsm fsm;
+    struct chart_item *chart_item;
+  };
+
+static bool
+xr_chart_render (struct xr_render_fsm *fsm, struct xr_driver *xr)
+{
+  struct xr_chart_state *cs = UP_CAST (fsm, struct xr_chart_state, fsm);
+
+  if (xr->y > 0)
+    return true;
+
+  if (xr->cairo != NULL)
+    xr_draw_chart (cs->chart_item, xr->cairo, 0.0, 0.0,
+                   xr_to_pt (xr->width), xr_to_pt (xr->length));
+  xr->y = xr->length;
+
+  return false;
 }
 
-/* Frees FONT. */
 static void
-free_font (struct xr_font *font)
+xr_chart_destroy (struct xr_render_fsm *fsm)
 {
-  free (font->string);
-  if (font->desc != NULL)
-    pango_font_description_free (font->desc);
-  pango_font_metrics_unref (font->metrics);
-  g_object_unref (font->layout);
+  struct xr_chart_state *cs = UP_CAST (fsm, struct xr_chart_state, fsm);
+
+  chart_item_unref (cs->chart_item);
+  free (cs);
 }
 
-/* Cairo driver class. */
-const struct outp_class cairo_class =
+static struct xr_render_fsm *
+xr_render_chart (const struct chart_item *chart_item)
 {
-  "cairo",
-  0,
+  struct xr_chart_state *cs;
 
-  xr_open_driver,
-  xr_close_driver,
+  cs = xmalloc (sizeof *cs);
+  cs->fsm.render = xr_chart_render;
+  cs->fsm.destroy = xr_chart_destroy;
+  cs->chart_item = chart_item_ref (chart_item);
 
-  xr_open_page,
-  xr_close_page,
-  NULL,
+  return &cs->fsm;
+}
+\f
+static bool
+xr_eject_render (struct xr_render_fsm *fsm UNUSED, struct xr_driver *xr)
+{
+  return xr->y > 0;
+}
 
-  xr_output_chart,
+static void
+xr_eject_destroy (struct xr_render_fsm *fsm UNUSED)
+{
+  /* Nothing to do. */
+}
 
-  NULL,
+static struct xr_render_fsm *
+xr_render_eject (void)
+{
+  static struct xr_render_fsm eject_renderer =
+    {
+      xr_eject_render,
+      xr_eject_destroy
+    };
 
-  xr_line,
-  xr_text_metrics,
-  xr_text_draw,
-};
+  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;
+
+  table_item = table_item_create (table_from_string (TAB_LEFT, text), NULL);
+  fsm = xr_render_table (xr, table_item);
+  table_item_unref (table_item);
+
+  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);
+
+  switch (type)
+    {
+    case TEXT_ITEM_TITLE:
+      free (xr->title);
+      xr->title = xstrdup (text);
+      break;
+
+    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;
+}