output: Use Cairo and Pango to draw charts, instead of libplot.
[pspp-builds.git] / src / output / ascii.c
index 8ed4f918fed26b4929084275001371d579910a33..b224b3ebea9e54bdfac5d6859a18793583b046f6 100644 (file)
@@ -1,5 +1,5 @@
 /* PSPP - a program for statistical analysis.
-   Copyright (C) 1997-9, 2000 Free Software Foundation, Inc.
+   Copyright (C) 1997-9, 2000, 2007, 2009 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 <ctype.h>
 #include <errno.h>
 #include <limits.h>
+#include <stdint.h>
 #include <stdlib.h>
 
 #include <data/file-name.h>
-#include <libpspp/alloc.h>
+#include <data/settings.h>
 #include <libpspp/assertion.h>
 #include <libpspp/compiler.h>
 #include <libpspp/pool.h>
 #include <libpspp/start-date.h>
 #include <libpspp/version.h>
+#include <output/chart-provider.h>
+#include <output/chart.h>
+#include <output/output.h>
 
-#include "chart.h"
 #include "error.h"
 #include "minmax.h"
-#include "output.h"
+#include "xalloc.h"
 
 #include "gettext.h"
 #define _(msgid) gettext (msgid)
 /* ASCII driver options: (defaults listed first)
 
    output-file="pspp.list"
+   append=no|yes                If output-file exists, append to it?
+   chart-files="pspp-#.png"     Name used for charts.
+   chart-type=png|none
+
    paginate=on|off              Formfeeds are desired?
    tab-width=8                  Width of a tab; 0 to not use tabs.
 
    headers=on|off               Put headers at top of page?
    emphasis=bold|underline|none Style to use for emphasis.
-   length=66
-   width=130
+   length=66|auto
+   width=79|auto
    squeeze=off|on               Squeeze multiple newlines into exactly one.
 
    top-margin=2
    bottom-margin=2
 
    box[x]="strng"               Sets box character X (X in base 4: 0-3333).
+   init="string"                Set initialization string.
  */
 
 /* Disable messages by failed range checks. */
@@ -95,36 +103,48 @@ struct ascii_driver_ext
     struct pool *pool;
 
     /* User parameters. */
+    bool append;                /* Append if output-file already exists? */
     bool headers;              /* Print headers at top of page? */
     bool paginate;             /* Insert formfeeds? */
     bool squeeze_blank_lines;   /* Squeeze multiple blank lines into one? */
     enum emphasis_style emphasis; /* How to emphasize text. */
     int tab_width;             /* Width of a tab; 0 not to use tabs. */
+    bool enable_charts;         /* Enable charts? */
+    const char *chart_file_name; /* Name of files used for charts. */
 
+    bool auto_width;            /* Use viewwidth as page width? */
+    bool auto_length;           /* Use viewlength as page width? */
     int page_length;           /* Page length before subtracting margins. */
     int top_margin;            /* Top margin in lines. */
     int bottom_margin;         /* Bottom margin in lines. */
 
     char *box[LNS_COUNT];       /* Line & box drawing characters. */
+    char *init;                 /* Device initialization string. */
 
     /* Internal state. */
     char *file_name;            /* Output file name. */
     FILE *file;                 /* Output file. */
+    bool reported_error;        /* Reported file open error? */
     int page_number;           /* Current page number. */
     struct line *lines;         /* Page content. */
     int line_cap;               /* Number of lines allocated. */
+    int chart_cnt;              /* Number of charts so far. */
   };
 
+static void ascii_flush (struct outp_driver *);
 static int get_default_box_char (size_t idx);
-static bool handle_option (struct outp_driver *this, const char *key,
+static bool update_page_size (struct outp_driver *, bool issue_error);
+static bool handle_option (void *this, const char *key,
                            const struct string *val);
 
 static bool
-ascii_open_driver (struct outp_driver *this, struct substring options)
+ascii_open_driver (const char *name, int types, struct substring options)
 {
+  struct outp_driver *this;
   struct ascii_driver_ext *x;
   int i;
 
+  this = outp_allocate_driver (&ascii_class, name, types);
   this->width = 79;
   this->font_height = 1;
   this->prop_em_width = 1;
@@ -133,45 +153,35 @@ ascii_open_driver (struct outp_driver *this, struct substring options)
     this->horiz_line_width[i] = this->vert_line_width[i] = i != OUTP_L_NONE;
 
   this->ext = x = pool_create_container (struct ascii_driver_ext, pool);
+  x->append = false;
   x->headers = true;
   x->paginate = true;
   x->squeeze_blank_lines = false;
   x->emphasis = EMPH_BOLD;
   x->tab_width = 8;
+  x->chart_file_name = pool_strdup (x->pool, "pspp-#.png");
+  x->enable_charts = true;
+  x->auto_width = false;
+  x->auto_length = false;
   x->page_length = 66;
   x->top_margin = 2;
   x->bottom_margin = 2;
   for (i = 0; i < LNS_COUNT; i++)
     x->box[i] = NULL;
+  x->init = NULL;
   x->file_name = pool_strdup (x->pool, "pspp.list");
   x->file = NULL;
+  x->reported_error = false;
   x->page_number = 0;
   x->lines = NULL;
   x->line_cap = 0;
+  x->chart_cnt = 1;
 
-  if (!outp_parse_options (options, handle_option, this))
+  if (!outp_parse_options (this->name, options, handle_option, this))
     goto error;
 
-  x->file = pool_fopen (x->pool, x->file_name, "w");
-  if (x->file == NULL)
-    {
-      error (0, errno, _("ascii: opening output file \"%s\""), x->file_name);
-      goto error;
-    }
-
-  this->length = x->page_length - x->top_margin - x->bottom_margin - 1;
-  if (x->headers)
-    this->length -= 3;
-
-  if (this->width < 59 || this->length < 15)
-    {
-      error (0, 0,
-             _("ascii: page excluding margins and headers "
-               "must be at least 59 characters wide by 15 lines long, but as "
-               "configured is only %d characters by %d lines"),
-             this->width, this->length);
-      return false;
-    }
+  if (!update_page_size (this, true))
+    goto error;
 
   for (i = 0; i < LNS_COUNT; i++)
     if (x->box[i] == NULL)
@@ -182,10 +192,13 @@ ascii_open_driver (struct outp_driver *this, struct substring options)
         x->box[i] = pool_strdup (x->pool, s);
       }
 
+  outp_register_driver (this);
+
   return true;
 
  error:
   pool_destroy (x->pool);
+  outp_free_driver (this);
   return false;
 }
 
@@ -221,13 +234,49 @@ get_default_box_char (size_t idx)
     }
 }
 
+/* Re-calculates the page width and length based on settings,
+   margins, and, if "auto" is set, the size of the user's
+   terminal window or GUI output window. */
+static bool
+update_page_size (struct outp_driver *this, bool issue_error)
+{
+  struct ascii_driver_ext *x = this->ext;
+  int margins = x->top_margin + x->bottom_margin + 1 + (x->headers ? 3 : 0);
+
+  if (x->auto_width)
+    this->width = settings_get_viewwidth ();
+  if (x->auto_length)
+    x->page_length = settings_get_viewlength ();
+
+  this->length = x->page_length - margins;
+
+  if (this->width < 59 || this->length < 15)
+    {
+      if (issue_error)
+        error (0, 0,
+               _("ascii: page excluding margins and headers "
+                 "must be at least 59 characters wide by 15 lines long, but "
+                 "as configured is only %d characters by %d lines"),
+             this->width, this->length);
+      if (this->width < 59)
+        this->width = 59;
+      if (this->length < 15)
+        {
+          this->length = 15;
+          x->page_length = this->length + margins;
+        }
+      return false;
+    }
+
+  return true;
+}
+
 static bool
 ascii_close_driver (struct outp_driver *this)
 {
   struct ascii_driver_ext *x = this->ext;
 
-  if (fn_close (x->file_name, x->file) != 0)
-    error (0, errno, _("ascii: closing output file \"%s\""), x->file_name);
+  ascii_flush (this);
   pool_detach_file (x->pool, x->file);
   pool_destroy (x->pool);
 
@@ -238,10 +287,10 @@ ascii_close_driver (struct outp_driver *this)
 enum
   {
     boolean_arg,
-    string_arg,
+    emphasis_arg,
     nonneg_int_arg,
-    pos_int_arg,
-    output_file_arg
+    page_size_arg,
+    string_arg
   };
 
 static const struct outp_option option_tab[] =
@@ -249,25 +298,30 @@ static const struct outp_option option_tab[] =
     {"headers", boolean_arg, 0},
     {"paginate", boolean_arg, 1},
     {"squeeze", boolean_arg, 2},
+    {"append", boolean_arg, 3},
 
-    {"emphasis", string_arg, 3},
+    {"emphasis", emphasis_arg, 0},
 
-    {"output-file", output_file_arg, 0},
-
-    {"length", pos_int_arg, 0},
-    {"width", pos_int_arg, 1},
+    {"length", page_size_arg, 0},
+    {"width", page_size_arg, 1},
 
     {"top-margin", nonneg_int_arg, 0},
     {"bottom-margin", nonneg_int_arg, 1},
     {"tab-width", nonneg_int_arg, 2},
 
+    {"output-file", string_arg, 0},
+    {"chart-files", string_arg, 1},
+    {"chart-type", string_arg, 2},
+    {"init", string_arg, 3},
+
     {NULL, 0, 0},
   };
 
 static bool
-handle_option (struct outp_driver *this, const char *key,
+handle_option (void *this_, const char *key,
                const struct string *val)
 {
+  struct outp_driver *this = this_;
   struct ascii_driver_ext *x = this->ext;
   int subcat;
   const char *value;
@@ -296,36 +350,54 @@ handle_option (struct outp_driver *this, const char *key,
     case -1:
       error (0, 0, _("ascii: unknown parameter `%s'"), key);
       break;
-    case output_file_arg:
-      x->file_name = pool_strdup (x->pool, value);
-      break;
-    case pos_int_arg:
+    case page_size_arg:
       {
        char *tail;
        int arg;
 
-       errno = 0;
-       arg = strtol (value, &tail, 0);
-       if (arg < 1 || errno == ERANGE || *tail)
-         {
-           error (0, 0, _("ascii: positive integer required as `%s' value"),
-                   key);
-           break;
-         }
-       switch (subcat)
-         {
-         case 0:
-           x->page_length = arg;
-           break;
-         case 1:
-           this->width = arg;
-           break;
-         default:
-           NOT_REACHED ();
-         }
+        if (ss_equals_case (ds_ss (val), ss_cstr ("auto")))
+          {
+            if (!(this->device & OUTP_DEV_SCREEN))
+              {
+                /* We only let `screen' devices have `auto'
+                   length or width because output to such devices
+                   is flushed before each new command.  Resizing
+                   a device in the middle of output seems like a
+                   bad idea. */
+                error (0, 0, _("ascii: only screen devices may have `auto' "
+                               "length or width"));
+              }
+            else if (subcat == 0)
+              x->auto_length = true;
+            else
+              x->auto_width = true;
+          }
+        else
+          {
+            errno = 0;
+            arg = strtol (value, &tail, 0);
+            if (arg < 1 || errno == ERANGE || *tail)
+              {
+                error (0, 0, _("ascii: positive integer required as "
+                               "`%s' value"),
+                       key);
+                break;
+              }
+            switch (subcat)
+              {
+              case 0:
+                x->page_length = arg;
+                break;
+              case 1:
+                this->width = arg;
+                break;
+              default:
+                NOT_REACHED ();
+              }
+          }
       }
       break;
-    case string_arg:
+    case emphasis_arg:
       if (!strcmp (value, "bold"))
         x->emphasis = EMPH_BOLD;
       else if (!strcmp (value, "underline"))
@@ -392,11 +464,43 @@ handle_option (struct outp_driver *this, const char *key,
           case 2:
             x->squeeze_blank_lines = setting;
             break;
+          case 3:
+            x->append = setting;
+            break;
          default:
            NOT_REACHED ();
          }
       }
       break;
+    case string_arg:
+      switch (subcat)
+        {
+        case 0:
+          x->file_name = pool_strdup (x->pool, value);
+          break;
+        case 1:
+          if (ds_find_char (val, '#') != SIZE_MAX)
+            x->chart_file_name = pool_strdup (x->pool, value);
+          else
+            error (0, 0, _("`chart-files' value must contain `#'"));
+          break;
+        case 2:
+          if (!strcmp (value, "png"))
+            x->enable_charts = true;
+          else if (!strcmp (value, "none"))
+            x->enable_charts = false;
+          else
+            {
+              error (0, 0,
+                     _("ascii: `png' or `none' expected for `chart-type'"));
+              return false;
+            }
+          break;
+        case 3:
+          x->init = pool_strdup (x->pool, value);
+          break;
+        }
+      break;
     default:
       NOT_REACHED ();
     }
@@ -410,6 +514,32 @@ ascii_open_page (struct outp_driver *this)
   struct ascii_driver_ext *x = this->ext;
   int i;
 
+  update_page_size (this, false);
+
+  if (x->file == NULL)
+    {
+      x->file = fn_open (x->file_name, x->append ? "a" : "w");
+      if (x->file != NULL)
+        {
+          pool_attach_file (x->pool, x->file);
+          if (x->init != NULL)
+            fputs (x->init, x->file);
+        }
+      else
+        {
+          /* Report the error to the user and complete
+             initialization.  If we do not finish initialization,
+             then calls to other driver functions will segfault
+             later.  It would be better to simply drop the driver
+             entirely, but we do not have a convenient mechanism
+             for this (yet). */
+          if (!x->reported_error)
+            error (0, errno, _("ascii: opening output file \"%s\""),
+                   x->file_name);
+          x->reported_error = true;
+        }
+    }
+
   x->page_number++;
 
   if (this->length > x->line_cap)
@@ -525,9 +655,10 @@ text_draw (struct outp_driver *this,
     ext->lines[y].chars[x++] = *string++ | attr;
 }
 
-/* Divides the text T->S into lines of width T->H.  Sets T->V to the
-   number of lines necessary.  Actually draws the text if DRAW is
-   true. */
+/* Divides the text T->S into lines of width T->H.  Sets *WIDTH
+   to the maximum width of a line and *HEIGHT to the number of
+   lines, if those arguments are non-null.  Actually draws the
+   text if DRAW is true. */
 static void
 delineate (struct outp_driver *this, const struct outp_text *text, bool draw,
            int *width, int *height)
@@ -605,7 +736,6 @@ ascii_text_draw (struct outp_driver *this, const struct outp_text *t)
   assert (this->page_open);
   delineate (this, t, true, NULL, NULL);
 }
-
 \f
 /* ascii_close_page () and support routines. */
 
@@ -670,6 +800,9 @@ ascii_close_page (struct outp_driver *this)
   struct string out;
   int line_num;
 
+  if (x->file == NULL)
+    return;
+
   ds_init_empty (&out);
 
   ds_put_char_multiple (&out, '\n', x->top_margin);
@@ -718,17 +851,63 @@ ascii_close_page (struct outp_driver *this)
   ds_destroy (&out);
 }
 
+/* Flushes all output to the user and lets the user deal with it.
+   This is applied only to output drivers that are designated as
+   "screen" drivers that the user is interacting with in real
+   time. */
 static void
-ascii_chart_initialise (struct outp_driver *d UNUSED, struct chart *ch)
+ascii_flush (struct outp_driver *this)
 {
-  error (0, 0, _("ascii: charts are unsupported by this driver"));
-  ch->lp = 0;
+  struct ascii_driver_ext *x = this->ext;
+  if (x->file != NULL)
+    {
+      if (fn_close (x->file_name, x->file) != 0)
+        error (0, errno, _("ascii: closing output file \"%s\""),
+               x->file_name);
+      pool_detach_file (x->pool, x->file);
+      x->file = NULL;
+    }
 }
 
 static void
-ascii_chart_finalise (struct outp_driver *d UNUSED, struct chart *ch UNUSED)
+ascii_output_chart (struct outp_driver *this, const struct chart *chart)
 {
+  struct ascii_driver_ext *x = this->ext;
+  struct outp_text t;
+  char *file_name;
+  char *text;
+
+  /* Draw chart into separate file */
+  file_name = chart_draw_png (chart, x->chart_file_name, x->chart_cnt++);
+
+  /* Mention chart in output.
+     First advance current position. */
+  if (!this->page_open)
+    outp_open_page (this);
+  else
+    {
+      this->cp_y++;
+      if (this->cp_y >= this->length)
+        {
+          outp_close_page (this);
+          outp_open_page (this);
+        }
+    }
 
+  /* Then write the text. */
+  text = xasprintf ("See %s for a chart.", file_name);
+  t.font = OUTP_FIXED;
+  t.justification = OUTP_LEFT;
+  t.string = ss_cstr (text);
+  t.h = this->width;
+  t.v = 1;
+  t.x = 0;
+  t.y = this->cp_y;
+  ascii_text_draw (this, &t);
+  this->cp_y++;
+
+  free (file_name);
+  free (text);
 }
 
 const struct outp_class ascii_class =
@@ -741,13 +920,13 @@ const struct outp_class ascii_class =
 
   ascii_open_page,
   ascii_close_page,
+  ascii_flush,
 
-  NULL,
+  ascii_output_chart,
+
+  NULL,                         /* submit */
 
   ascii_line,
   ascii_text_metrics,
   ascii_text_draw,
-
-  ascii_chart_initialise,
-  ascii_chart_finalise
 };