cairo-pager: Add outline to PDF output.
authorBen Pfaff <blp@cs.stanford.edu>
Fri, 25 Dec 2020 20:14:38 +0000 (12:14 -0800)
committerBen Pfaff <blp@cs.stanford.edu>
Sat, 26 Dec 2020 06:11:03 +0000 (22:11 -0800)
NEWS
doc/invoking.texi
src/output/cairo-pager.c
src/output/cairo-pager.h
src/output/cairo.c

diff --git a/NEWS b/NEWS
index 7da3c91cbf02a210843bc395fbec896a9a59d8b5..491168199b88abc86fcd3d98178c4e49b2960f45 100644 (file)
--- a/NEWS
+++ b/NEWS
@@ -31,6 +31,9 @@ Changes from 1.4.1 to 1.5.2:
    - New driver option "trim" to remove empty space from PDF,
      PostScript, SVG, and PNG output files.
 
    - New driver option "trim" to remove empty space from PDF,
      PostScript, SVG, and PNG output files.
 
+   - The PDF output driver now adds an outline to allow PDF viewers to display
+     as a "table of contents" for the file.
+
    - The HTML output driver has a new option "bare".
 
  * New features in pspp-output:
    - The HTML output driver has a new option "bare".
 
  * New features in pspp-output:
index b9a0df8acf09f6b17e068b66e3ec2496ddc4b686..4d06f376dd2662ec70216b66b7cfa2fc230c3aa0 100644 (file)
@@ -304,6 +304,15 @@ This option makes PSPP trim empty space around each page of output,
 before adding the margins.  This can make the output easier to include
 in other documents.
 
 before adding the margins.  This can make the output easier to include
 in other documents.
 
+@item @option{-O outline=@var{boolean}}
+For PDF output only, this option controls whether PSPP includes an
+outline in the output file.  PDF viewers usually display the outline
+as a side bar that allows for easy navigation of the file.
+The default is true unless @option{-O trim=true} is also specified.
+(The Cairo graphics library that PSPP uses to produce PDF output has a
+bug that can cause a crash when outlines and trimming are used
+together.)
+
 @item @option{-O font-resolution=@var{dpi}}
 Sets the resolution for font rendering, in dots per inch.  For PDF,
 PostScript, and SVG output, the default is 72 dpi, so that a 10-point
 @item @option{-O font-resolution=@var{dpi}}
 Sets the resolution for font rendering, in dots per inch.  For PDF,
 PostScript, and SVG output, the default is 72 dpi, so that a 10-point
index e771e1a03b11d3fb1fd4c7952f8d45ca56bf33ba..6249a3ab8e09829a5aecff5df351b5f4625007cc 100644 (file)
 #include "output/cairo-pager.h"
 
 #include <math.h>
 #include "output/cairo-pager.h"
 
 #include <math.h>
+#include <cairo/cairo-pdf.h>
 #include <pango/pango-layout.h>
 #include <pango/pangocairo.h>
 
 #include <pango/pango-layout.h>
 #include <pango/pangocairo.h>
 
+#include "output/chart-item.h"
 #include "output/driver-provider.h"
 #include "output/driver-provider.h"
+#include "output/group-item.h"
+#include "output/message-item.h"
+#include "output/page-eject-item.h"
+#include "output/page-setup-item.h"
+#include "output/table-item.h"
+#include "output/text-item.h"
 
 #include "gl/xalloc.h"
 
 
 #include "gl/xalloc.h"
 
@@ -101,6 +109,19 @@ struct xr_pager
     /* Current output item. */
     struct xr_fsm *fsm;
     struct output_item *item;
     /* Current output item. */
     struct xr_fsm *fsm;
     struct output_item *item;
+    int slice_idx;
+
+    /* Grouping, for constructing the outline for PDFs.
+
+       The 'group_ids' were returned by cairo_pdf_surface_add_outline() and
+       represent the groups within which upcoming output is nested.  The
+       'group_opens' will be passed to cairo_pdf_surface_add_outline() when the
+       next item is rendered (we defer it so that the location associated with
+       the outline item can be the first object actually output in it). */
+    int *group_ids;
+    size_t n_group_ids, allocated_group_ids;
+    struct group_open_item **group_opens;
+    size_t n_opens, allocated_opens;
 
     /* Current output page. */
     cairo_t *cr;
 
     /* Current output page. */
     cairo_t *cr;
@@ -234,6 +255,11 @@ xr_pager_destroy (struct xr_pager *p)
 {
   if (p)
     {
 {
   if (p)
     {
+      free (p->group_ids);
+      for (size_t i = 0; i < p->n_opens; i++)
+        group_open_item_unref (p->group_opens[i]);
+      free (p->group_opens);
+
       xr_page_style_unref (p->page_style);
       xr_fsm_style_unref (p->fsm_style);
 
       xr_page_style_unref (p->page_style);
       xr_fsm_style_unref (p->fsm_style);
 
@@ -260,6 +286,7 @@ xr_pager_add_item (struct xr_pager *p, const struct output_item *item)
 {
   assert (!p->item);
   p->item = output_item_ref (item);
 {
   assert (!p->item);
   p->item = output_item_ref (item);
+  p->slice_idx = 0;
   xr_pager_run (p);
 }
 
   xr_pager_run (p);
 }
 
@@ -321,6 +348,18 @@ xr_pager_needs_new_page (struct xr_pager *p)
     return false;
 }
 
     return false;
 }
 
+static int
+add_outline (cairo_t *cr, int parent_id,
+             const char *utf8, const char *link_attribs,
+             cairo_pdf_outline_flags_t flags)
+{
+  cairo_surface_t *surface = cairo_get_target (cr);
+  return (cairo_surface_get_type (surface) == CAIRO_SURFACE_TYPE_PDF
+          ? cairo_pdf_surface_add_outline (surface, parent_id,
+                                           utf8, link_attribs, flags)
+          : 0);
+}
+
 static void
 xr_pager_run (struct xr_pager *p)
 {
 static void
 xr_pager_run (struct xr_pager *p)
 {
@@ -328,6 +367,27 @@ xr_pager_run (struct xr_pager *p)
     {
       if (!p->fsm)
         {
     {
       if (!p->fsm)
         {
+          if (is_group_open_item (p->item))
+            {
+              if (p->n_opens >= p->allocated_opens)
+                p->group_opens = x2nrealloc (p->group_opens,
+                                             &p->allocated_opens,
+                                             sizeof p->group_opens);
+              p->group_opens[p->n_opens++] = group_open_item_ref (
+                to_group_open_item (p->item));
+            }
+          else if (is_group_close_item (p->item))
+            {
+              if (p->n_opens)
+                group_open_item_unref (p->group_opens[--p->n_opens]);
+              else if (p->n_group_ids)
+                p->n_group_ids--;
+              else
+                {
+                  /* Something wrong! */
+                }
+            }
+
           p->fsm = xr_fsm_create (p->item, p->fsm_style, p->cr);
           if (!p->fsm)
             {
           p->fsm = xr_fsm_create (p->item, p->fsm_style, p->cr);
           if (!p->fsm)
             {
@@ -340,12 +400,75 @@ xr_pager_run (struct xr_pager *p)
 
       for (;;)
         {
 
       for (;;)
         {
+          char *dest_name = NULL;
+          if (p->page_style->include_outline)
+            {
+              static int counter = 0;
+              dest_name = xasprintf ("dest%d", counter++);
+              char *attrs = xasprintf ("name='%s'", dest_name);
+              cairo_tag_begin (p->cr, CAIRO_TAG_DEST, attrs);
+              free (attrs);
+            }
+
           int spacing = p->page_style->object_spacing;
           int chunk = xr_fsm_draw_slice (p->fsm, p->cr,
                                          p->fsm_style->size[V] - p->y);
           p->y += chunk + spacing;
           cairo_translate (p->cr, 0, xr_to_pt (chunk + spacing));
 
           int spacing = p->page_style->object_spacing;
           int chunk = xr_fsm_draw_slice (p->fsm, p->cr,
                                          p->fsm_style->size[V] - p->y);
           p->y += chunk + spacing;
           cairo_translate (p->cr, 0, xr_to_pt (chunk + spacing));
 
+          if (p->page_style->include_outline)
+            {
+              cairo_tag_end (p->cr, CAIRO_TAG_DEST);
+
+              if (chunk && p->slice_idx++ == 0)
+                {
+                  char *attrs = xasprintf ("dest='%s'", dest_name);
+
+                  int parent_group_id = (p->n_group_ids
+                                         ? p->group_ids[p->n_group_ids - 1]
+                                         : CAIRO_PDF_OUTLINE_ROOT);
+                  for (size_t i = 0; i < p->n_opens; i++)
+                    {
+                      parent_group_id = add_outline (
+                        p->cr, parent_group_id,
+                        p->group_opens[i]->command_name, attrs,
+                        CAIRO_PDF_OUTLINE_FLAG_OPEN);
+                      group_open_item_unref (p->group_opens[i]);
+
+                      if (p->n_group_ids >= p->allocated_group_ids)
+                        p->group_ids = x2nrealloc (p->group_ids,
+                                                   &p->allocated_group_ids,
+                                                   sizeof *p->group_ids);
+                      p->group_ids[p->n_group_ids++] = parent_group_id;
+                    }
+                  p->n_opens = 0;
+
+                  const char *text;
+                  if (is_table_item (p->item))
+                    {
+                      const struct table_item_text *title
+                        = table_item_get_title (to_table_item (p->item));
+                      text = title ? title->content : "Table";
+                    }
+                  else if (is_chart_item (p->item))
+                    {
+                      const char *title
+                        = chart_item_get_title (to_chart_item (p->item));
+                      text = title ? title : "Chart";
+                    }
+                  else
+                    text = (is_page_eject_item (p->item) ? "Page Break"
+                            : is_page_setup_item (p->item) ? "Page Setup"
+                            : is_message_item (p->item) ? "Message"
+                            : is_text_item (p->item) ? "Text"
+                            : NULL);
+                  if (text)
+                    add_outline (p->cr, parent_group_id, text, attrs, 0);
+                  free (attrs);
+                }
+              free (dest_name);
+            }
+
           if (xr_fsm_is_empty (p->fsm))
             {
               xr_fsm_destroy (p->fsm);
           if (xr_fsm_is_empty (p->fsm))
             {
               xr_fsm_destroy (p->fsm);
index 47b49199dfec604304c08fd18abb411182e334bd..bd7fa55125115a0b5ab3377cd25af3d2990554cd 100644 (file)
@@ -39,6 +39,10 @@ struct xr_page_style
 
     int initial_page_number;
     int object_spacing;
 
     int initial_page_number;
     int object_spacing;
+
+    /* Whether to include an outline in PDF output.  (The only reason I know to
+       omit it is to avoid a Cairo bug that caused crashes in some cases.) */
+    bool include_outline;
   };
 struct xr_page_style *xr_page_style_ref (const struct xr_page_style *);
 struct xr_page_style *xr_page_style_unshare (struct xr_page_style *);
   };
 struct xr_page_style *xr_page_style_ref (const struct xr_page_style *);
 struct xr_page_style *xr_page_style_unshare (struct xr_page_style *);
index 00ac472f7c4d0229fd146b6d84585dfce28acdda..313456c663e07290efb5a88f3104741c8df35d33 100644 (file)
@@ -238,6 +238,14 @@ xr_allocate (const char *name, int device_type,
 
   xr->trim = parse_boolean (opt (d, o, "trim", "false"));
 
 
   xr->trim = parse_boolean (opt (d, o, "trim", "false"));
 
+  /* Cairo 1.16.0 has a bug that causes crashes if outlines are enabled at the
+     same time as trimming:
+     https://lists.cairographics.org/archives/cairo/2020-December/029151.html
+     For now, just disable the outline if trimming is enabled. */
+  bool include_outline
+    = (output_type == XR_PDF
+       && parse_boolean (opt (d, o, "outline", xr->trim ? "false" : "true")));
+
   xr->page_style = xmalloc (sizeof *xr->page_style);
   *xr->page_style = (struct xr_page_style) {
     .ref_cnt = 1,
   xr->page_style = xmalloc (sizeof *xr->page_style);
   *xr->page_style = (struct xr_page_style) {
     .ref_cnt = 1,
@@ -249,6 +257,7 @@ xr_allocate (const char *name, int device_type,
 
     .initial_page_number = 1,
     .object_spacing = object_spacing,
 
     .initial_page_number = 1,
     .object_spacing = object_spacing,
+    .include_outline = include_outline,
   };
 
   xr->fsm_style = xmalloc (sizeof *xr->fsm_style);
   };
 
   xr->fsm_style = xmalloc (sizeof *xr->fsm_style);
@@ -549,40 +558,42 @@ xr_destroy (struct output_driver *driver)
 
 static void
 xr_update_page_setup (struct output_driver *driver,
 
 static void
 xr_update_page_setup (struct output_driver *driver,
-                      const struct page_setup *ps)
+                      const struct page_setup *setup)
 {
   struct xr_driver *xr = xr_driver_cast (driver);
 
   const double scale = 72 * XR_POINT;
 
 {
   struct xr_driver *xr = xr_driver_cast (driver);
 
   const double scale = 72 * XR_POINT;
 
-  int swap = ps->orientation == PAGE_LANDSCAPE;
+  int swap = setup->orientation == PAGE_LANDSCAPE;
   enum table_axis h = H ^ swap;
   enum table_axis v = V ^ swap;
 
   enum table_axis h = H ^ swap;
   enum table_axis v = V ^ swap;
 
-  xr_page_style_unref (xr->page_style);
+  struct xr_page_style *old_ps = xr->page_style;
   xr->page_style = xmalloc (sizeof *xr->page_style);
   *xr->page_style = (struct xr_page_style) {
     .ref_cnt = 1,
 
     .margins = {
   xr->page_style = xmalloc (sizeof *xr->page_style);
   *xr->page_style = (struct xr_page_style) {
     .ref_cnt = 1,
 
     .margins = {
-      [H] = { ps->margins[h][0] * scale, ps->margins[h][1] * scale },
-      [V] = { ps->margins[v][0] * scale, ps->margins[v][1] * scale },
+      [H] = { setup->margins[h][0] * scale, setup->margins[h][1] * scale },
+      [V] = { setup->margins[v][0] * scale, setup->margins[v][1] * scale },
     },
 
     },
 
-    .initial_page_number = ps->initial_page_number,
-    .object_spacing = ps->object_spacing * 72 * XR_POINT,
+    .initial_page_number = setup->initial_page_number,
+    .object_spacing = setup->object_spacing * 72 * XR_POINT,
+    .include_outline = old_ps->include_outline,
   };
   for (size_t i = 0; i < 2; i++)
   };
   for (size_t i = 0; i < 2; i++)
-    page_heading_copy (&xr->page_style->headings[i], &ps->headings[i]);
+    page_heading_copy (&xr->page_style->headings[i], &setup->headings[i]);
+  xr_page_style_unref (old_ps);
 
   struct xr_fsm_style *old_fs = xr->fsm_style;
   xr->fsm_style = xmalloc (sizeof *xr->fsm_style);
   *xr->fsm_style = (struct xr_fsm_style) {
     .ref_cnt = 1,
 
   struct xr_fsm_style *old_fs = xr->fsm_style;
   xr->fsm_style = xmalloc (sizeof *xr->fsm_style);
   *xr->fsm_style = (struct xr_fsm_style) {
     .ref_cnt = 1,
-    .size = { [H] = ps->paper[H] * scale, [V] = ps->paper[V] * scale },
+    .size = { [H] = setup->paper[H] * scale, [V] = setup->paper[V] * scale },
     .min_break = {
     .min_break = {
-      [H] = ps->paper[H] * scale / 2,
-      [V] = ps->paper[V] * scale / 2,
+      [H] = setup->paper[H] * scale / 2,
+      [V] = setup->paper[V] * scale / 2,
     },
     .fg = old_fs->fg,
     .use_system_colors = old_fs->use_system_colors,
     },
     .fg = old_fs->fg,
     .use_system_colors = old_fs->use_system_colors,
@@ -592,8 +603,8 @@ xr_update_page_setup (struct output_driver *driver,
     xr->fsm_style->fonts[i] = pango_font_description_copy (old_fs->fonts[i]);
   xr_fsm_style_unref (old_fs);
 
     xr->fsm_style->fonts[i] = pango_font_description_copy (old_fs->fonts[i]);
   xr_fsm_style_unref (old_fs);
 
-  xr_set_surface_size (xr->dest_surface, xr->output_type, ps->paper[H] * 72.0,
-                       ps->paper[V] * 72.0);
+  xr_set_surface_size (xr->dest_surface, xr->output_type,
+                       setup->paper[H] * 72.0, setup->paper[V] * 72.0);
 }
 
 static void
 }
 
 static void