2278f809ce188e58cb739ffc1953c67d5572877e
[pspp-builds.git] / src / output / ascii.c
1 /* PSPP - a program for statistical analysis.
2    Copyright (C) 1997-9, 2000, 2007 Free Software Foundation, Inc.
3
4    This program is free software: you can redistribute it and/or modify
5    it under the terms of the GNU General Public License as published by
6    the Free Software Foundation, either version 3 of the License, or
7    (at your option) any later version.
8
9    This program is distributed in the hope that it will be useful,
10    but WITHOUT ANY WARRANTY; without even the implied warranty of
11    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12    GNU General Public License for more details.
13
14    You should have received a copy of the GNU General Public License
15    along with this program.  If not, see <http://www.gnu.org/licenses/>. */
16
17 #include <config.h>
18
19 #include <ctype.h>
20 #include <errno.h>
21 #include <limits.h>
22 #include <stdint.h>
23 #include <stdlib.h>
24
25 #include <data/file-name.h>
26 #include <libpspp/alloc.h>
27 #include <libpspp/assertion.h>
28 #include <libpspp/compiler.h>
29 #include <libpspp/pool.h>
30 #include <libpspp/start-date.h>
31 #include <libpspp/version.h>
32
33 #include "chart.h"
34 #include "error.h"
35 #include "minmax.h"
36 #include "output.h"
37
38 #include "gettext.h"
39 #define _(msgid) gettext (msgid)
40
41 /* ASCII driver options: (defaults listed first)
42
43    output-file="pspp.list"
44    chart-files="pspp-#.png"     Name used for charts.
45    chart-type=png               Format of charts (use "none" to disable).
46
47    paginate=on|off              Formfeeds are desired?
48    tab-width=8                  Width of a tab; 0 to not use tabs.
49
50    headers=on|off               Put headers at top of page?
51    emphasis=bold|underline|none Style to use for emphasis.
52    length=66
53    width=130
54    squeeze=off|on               Squeeze multiple newlines into exactly one.
55
56    top-margin=2
57    bottom-margin=2
58
59    box[x]="strng"               Sets box character X (X in base 4: 0-3333).
60    init="string"                Set initialization string.
61  */
62
63 /* Disable messages by failed range checks. */
64 /*#define SUPPRESS_WARNINGS 1 */
65
66 /* Line styles bit shifts. */
67 enum
68   {
69     LNS_TOP = 0,
70     LNS_LEFT = 2,
71     LNS_BOTTOM = 4,
72     LNS_RIGHT = 6,
73
74     LNS_COUNT = 256
75   };
76
77 /* Character attributes. */
78 #define ATTR_EMPHASIS   0x100   /* Bold-face. */
79 #define ATTR_BOX        0x200   /* Line drawing character. */
80
81 /* A line of text. */
82 struct line
83   {
84     unsigned short *chars;      /* Characters and attributes. */
85     int char_cnt;               /* Length. */
86     int char_cap;               /* Allocated bytes. */
87   };
88
89 /* How to emphasize text. */
90 enum emphasis_style
91   {
92     EMPH_BOLD,                  /* Overstrike for bold. */
93     EMPH_UNDERLINE,             /* Overstrike for underlining. */
94     EMPH_NONE                   /* No emphasis. */
95   };
96
97 /* ASCII output driver extension record. */
98 struct ascii_driver_ext
99   {
100     struct pool *pool;
101
102     /* User parameters. */
103     bool headers;               /* Print headers at top of page? */
104     bool paginate;              /* Insert formfeeds? */
105     bool squeeze_blank_lines;   /* Squeeze multiple blank lines into one? */
106     enum emphasis_style emphasis; /* How to emphasize text. */
107     int tab_width;              /* Width of a tab; 0 not to use tabs. */
108     const char *chart_type;     /* Type of charts to output; NULL for none. */
109     const char *chart_file_name; /* Name of files used for charts. */
110
111     int page_length;            /* Page length before subtracting margins. */
112     int top_margin;             /* Top margin in lines. */
113     int bottom_margin;          /* Bottom margin in lines. */
114
115     char *box[LNS_COUNT];       /* Line & box drawing characters. */
116     char *init;                 /* Device initialization string. */
117
118     /* Internal state. */
119     char *file_name;            /* Output file name. */
120     FILE *file;                 /* Output file. */
121     int page_number;            /* Current page number. */
122     struct line *lines;         /* Page content. */
123     int line_cap;               /* Number of lines allocated. */
124     int chart_cnt;              /* Number of charts so far. */
125   };
126
127 static void ascii_flush (struct outp_driver *);
128 static int get_default_box_char (size_t idx);
129 static bool handle_option (struct outp_driver *this, const char *key,
130                            const struct string *val);
131
132 static bool
133 ascii_open_driver (struct outp_driver *this, struct substring options)
134 {
135   struct ascii_driver_ext *x;
136   int i;
137
138   this->width = 79;
139   this->font_height = 1;
140   this->prop_em_width = 1;
141   this->fixed_width = 1;
142   for (i = 0; i < OUTP_L_COUNT; i++)
143     this->horiz_line_width[i] = this->vert_line_width[i] = i != OUTP_L_NONE;
144
145   this->ext = x = pool_create_container (struct ascii_driver_ext, pool);
146   x->headers = true;
147   x->paginate = true;
148   x->squeeze_blank_lines = false;
149   x->emphasis = EMPH_BOLD;
150   x->tab_width = 8;
151   x->chart_file_name = pool_strdup (x->pool, "pspp-#.png");
152   x->chart_type = pool_strdup (x->pool, "png");
153   x->page_length = 66;
154   x->top_margin = 2;
155   x->bottom_margin = 2;
156   for (i = 0; i < LNS_COUNT; i++)
157     x->box[i] = NULL;
158   x->init = NULL;
159   x->file_name = pool_strdup (x->pool, "pspp.list");
160   x->file = NULL;
161   x->page_number = 0;
162   x->lines = NULL;
163   x->line_cap = 0;
164   x->chart_cnt = 0;
165
166   if (!outp_parse_options (options, handle_option, this))
167     goto error;
168
169   this->length = x->page_length - x->top_margin - x->bottom_margin - 1;
170   if (x->headers)
171     this->length -= 3;
172
173   if (this->width < 59 || this->length < 15)
174     {
175       error (0, 0,
176              _("ascii: page excluding margins and headers "
177                "must be at least 59 characters wide by 15 lines long, but as "
178                "configured is only %d characters by %d lines"),
179              this->width, this->length);
180       return false;
181     }
182
183   for (i = 0; i < LNS_COUNT; i++)
184     if (x->box[i] == NULL)
185       {
186         char s[2];
187         s[0] = get_default_box_char (i);
188         s[1] = '\0';
189         x->box[i] = pool_strdup (x->pool, s);
190       }
191
192   return true;
193
194  error:
195   pool_destroy (x->pool);
196   return false;
197 }
198
199 static int
200 get_default_box_char (size_t idx)
201 {
202   /* Disassemble IDX into components. */
203   unsigned top = (idx >> LNS_TOP) & 3;
204   unsigned left = (idx >> LNS_LEFT) & 3;
205   unsigned bottom = (idx >> LNS_BOTTOM) & 3;
206   unsigned right = (idx >> LNS_RIGHT) & 3;
207
208   /* Reassemble components into nibbles in the order TLBR.
209      This makes it easy to read the case labels. */
210   unsigned value = (top << 12) | (left << 8) | (bottom << 4) | (right << 0);
211   switch (value)
212     {
213     case 0x0000:
214       return ' ';
215
216     case 0x0100: case 0x0101: case 0x0001:
217       return '-';
218
219     case 0x1000: case 0x1010: case 0x0010:
220       return '|';
221
222     case 0x0300: case 0x0303: case 0x0003:
223     case 0x0200: case 0x0202: case 0x0002:
224       return '=';
225
226     default:
227       return left > 1 || top > 1 || right > 1 || bottom > 1 ? '#' : '+';
228     }
229 }
230
231 static bool
232 ascii_close_driver (struct outp_driver *this)
233 {
234   struct ascii_driver_ext *x = this->ext;
235
236   ascii_flush (this);
237   pool_detach_file (x->pool, x->file);
238   pool_destroy (x->pool);
239
240   return true;
241 }
242
243 /* Generic option types. */
244 enum
245   {
246     boolean_arg,
247     emphasis_arg,
248     nonneg_int_arg,
249     pos_int_arg,
250     string_arg
251   };
252
253 static const struct outp_option option_tab[] =
254   {
255     {"headers", boolean_arg, 0},
256     {"paginate", boolean_arg, 1},
257     {"squeeze", boolean_arg, 2},
258
259     {"emphasis", emphasis_arg, 0},
260
261     {"length", pos_int_arg, 0},
262     {"width", pos_int_arg, 1},
263
264     {"top-margin", nonneg_int_arg, 0},
265     {"bottom-margin", nonneg_int_arg, 1},
266     {"tab-width", nonneg_int_arg, 2},
267
268     {"output-file", string_arg, 0},
269     {"chart-files", string_arg, 1},
270     {"chart-type", string_arg, 2},
271     {"init", string_arg, 3},
272
273     {NULL, 0, 0},
274   };
275
276 static bool
277 handle_option (struct outp_driver *this, const char *key,
278                const struct string *val)
279 {
280   struct ascii_driver_ext *x = this->ext;
281   int subcat;
282   const char *value;
283
284   value = ds_cstr (val);
285   if (!strncmp (key, "box[", 4))
286     {
287       char *tail;
288       int indx = strtol (&key[4], &tail, 4);
289       if (*tail != ']' || indx < 0 || indx > LNS_COUNT)
290         {
291           error (0, 0, _("ascii: bad index value for `box' key: syntax "
292                          "is box[INDEX], 0 <= INDEX < %d decimal, with INDEX "
293                          "expressed in base 4"),
294                  LNS_COUNT);
295           return false;
296         }
297       if (x->box[indx] != NULL)
298         error (0, 0, _("ascii: multiple values for %s"), key);
299       x->box[indx] = pool_strdup (x->pool, value);
300       return true;
301     }
302
303   switch (outp_match_keyword (key, option_tab, &subcat))
304     {
305     case -1:
306       error (0, 0, _("ascii: unknown parameter `%s'"), key);
307       break;
308     case pos_int_arg:
309       {
310         char *tail;
311         int arg;
312
313         errno = 0;
314         arg = strtol (value, &tail, 0);
315         if (arg < 1 || errno == ERANGE || *tail)
316           {
317             error (0, 0, _("ascii: positive integer required as `%s' value"),
318                    key);
319             break;
320           }
321         switch (subcat)
322           {
323           case 0:
324             x->page_length = arg;
325             break;
326           case 1:
327             this->width = arg;
328             break;
329           default:
330             NOT_REACHED ();
331           }
332       }
333       break;
334     case emphasis_arg:
335       if (!strcmp (value, "bold"))
336         x->emphasis = EMPH_BOLD;
337       else if (!strcmp (value, "underline"))
338         x->emphasis = EMPH_UNDERLINE;
339       else if (!strcmp (value, "none"))
340         x->emphasis = EMPH_NONE;
341       else
342         error (0, 0,
343                _("ascii: `emphasis' value must be `bold', "
344                  "`underline', or `none'"));
345       break;
346     case nonneg_int_arg:
347       {
348         char *tail;
349         int arg;
350
351         errno = 0;
352         arg = strtol (value, &tail, 0);
353         if (arg < 0 || errno == ERANGE || *tail)
354           {
355             error (0, 0,
356                    _("ascii: zero or positive integer required as `%s' value"),
357                    key);
358             break;
359           }
360         switch (subcat)
361           {
362           case 0:
363             x->top_margin = arg;
364             break;
365           case 1:
366             x->bottom_margin = arg;
367             break;
368           case 2:
369             x->tab_width = arg;
370             break;
371           default:
372             NOT_REACHED ();
373           }
374       }
375       break;
376     case boolean_arg:
377       {
378         bool setting;
379         if (!strcmp (value, "on") || !strcmp (value, "true")
380             || !strcmp (value, "yes") || atoi (value))
381           setting = true;
382         else if (!strcmp (value, "off") || !strcmp (value, "false")
383                  || !strcmp (value, "no") || !strcmp (value, "0"))
384           setting = false;
385         else
386           {
387             error (0, 0, _("ascii: boolean value expected for `%s'"), key);
388             return false;
389           }
390         switch (subcat)
391           {
392           case 0:
393             x->headers = setting;
394             break;
395           case 1:
396             x->paginate = setting;
397             break;
398           case 2:
399             x->squeeze_blank_lines = setting;
400             break;
401           default:
402             NOT_REACHED ();
403           }
404       }
405       break;
406     case string_arg:
407       switch (subcat)
408         {
409         case 0:
410           x->file_name = pool_strdup (x->pool, value);
411           break;
412         case 1:
413           if (ds_find_char (val, '#') != SIZE_MAX)
414             x->chart_file_name = pool_strdup (x->pool, value);
415           else
416             error (0, 0, _("`chart-files' value must contain `#'"));
417           break;
418         case 2:
419           if (value[0] != '\0')
420             x->chart_type = pool_strdup (x->pool, value);
421           else
422             x->chart_type = NULL;
423           break;
424         case 3:
425           x->init = pool_strdup (x->pool, value);
426           break;
427         }
428       break;
429     default:
430       NOT_REACHED ();
431     }
432
433   return true;
434 }
435
436 static void
437 ascii_open_page (struct outp_driver *this)
438 {
439   struct ascii_driver_ext *x = this->ext;
440   int i;
441
442   if (x->file == NULL)
443     {
444       x->file = fn_open (x->file_name, "w");
445       if (x->file == NULL)
446         {
447           error (0, errno, _("ascii: opening output file \"%s\""),
448                  x->file_name);
449           return;
450         }
451       pool_attach_file (x->pool, x->file);
452
453       if (x->init != NULL)
454         fputs (x->init, x->file);
455     }
456
457   x->page_number++;
458
459   if (this->length > x->line_cap)
460     {
461       x->lines = pool_nrealloc (x->pool,
462                                 x->lines, this->length, sizeof *x->lines);
463       for (i = x->line_cap; i < this->length; i++)
464         {
465           struct line *line = &x->lines[i];
466           line->chars = NULL;
467           line->char_cap = 0;
468         }
469       x->line_cap = this->length;
470     }
471
472   for (i = 0; i < this->length; i++)
473     x->lines[i].char_cnt = 0;
474 }
475
476 /* Ensures that at least the first LENGTH characters of line Y in
477    THIS driver identified X have been cleared out. */
478 static inline void
479 expand_line (struct outp_driver *this, int y, int length)
480 {
481   struct ascii_driver_ext *ext = this->ext;
482   struct line *line = &ext->lines[y];
483   if (line->char_cnt < length)
484     {
485       int x;
486       if (line->char_cap < length)
487         {
488           line->char_cap = MIN (length * 2, this->width);
489           line->chars = pool_nrealloc (ext->pool,
490                                        line->chars,
491                                        line->char_cap, sizeof *line->chars);
492         }
493       for (x = line->char_cnt; x < length; x++)
494         line->chars[x] = ' ';
495       line->char_cnt = length;
496     }
497 }
498
499 static void
500 ascii_line (struct outp_driver *this,
501             int x0, int y0, int x1, int y1,
502             enum outp_line_style top, enum outp_line_style left,
503             enum outp_line_style bottom, enum outp_line_style right)
504 {
505   struct ascii_driver_ext *ext = this->ext;
506   int y;
507   unsigned short value;
508
509   assert (this->page_open);
510 #if DEBUGGING
511   if (x0 < 0 || x1 > this->width || y0 < 0 || y1 > this->length)
512     {
513 #if !SUPPRESS_WARNINGS
514       printf (_("ascii: bad line (%d,%d)-(%d,%d) out of (%d,%d)\n"),
515               x0, y0, x1, y1, this->width, this->length);
516 #endif
517       return;
518     }
519 #endif
520
521   value = ((left << LNS_LEFT) | (right << LNS_RIGHT)
522            | (top << LNS_TOP) | (bottom << LNS_BOTTOM) | ATTR_BOX);
523   for (y = y0; y < y1; y++)
524     {
525       int x;
526
527       expand_line (this, y, x1);
528       for (x = x0; x < x1; x++)
529         ext->lines[y].chars[x] = value;
530     }
531 }
532
533 static void
534 ascii_submit (struct outp_driver *this UNUSED, struct som_entity *s)
535 {
536   extern struct som_table_class tab_table_class;
537
538   assert (s->class == &tab_table_class);
539   assert (s->type == SOM_CHART);
540 }
541
542 static void
543 text_draw (struct outp_driver *this,
544            enum outp_font font,
545            int x, int y,
546            enum outp_justification justification, int width,
547            const char *string, size_t length)
548 {
549   struct ascii_driver_ext *ext = this->ext;
550   unsigned short attr = font == OUTP_EMPHASIS ? ATTR_EMPHASIS : 0;
551
552   int line_len;
553
554   switch (justification)
555     {
556     case OUTP_LEFT:
557       break;
558     case OUTP_CENTER:
559       x += (width - length + 1) / 2;
560       break;
561     case OUTP_RIGHT:
562       x += width - length;
563       break;
564     default:
565       NOT_REACHED ();
566     }
567
568   if (y >= this->length || x >= this->width)
569     return;
570
571   if (x + length > this->width)
572     length = this->width - x;
573
574   line_len = x + length;
575
576   expand_line (this, y, line_len);
577   while (length-- > 0)
578     ext->lines[y].chars[x++] = *string++ | attr;
579 }
580
581 /* Divides the text T->S into lines of width T->H.  Sets *WIDTH
582    to the maximum width of a line and *HEIGHT to the number of
583    lines, if those arguments are non-null.  Actually draws the
584    text if DRAW is true. */
585 static void
586 delineate (struct outp_driver *this, const struct outp_text *text, bool draw,
587            int *width, int *height)
588 {
589   int max_width;
590   int height_left;
591
592   const char *cp = ss_data (text->string);
593
594   max_width = 0;
595   height_left = text->v;
596
597   while (height_left > 0)
598     {
599       size_t chars_left;
600       size_t line_len;
601       const char *end;
602
603       /* Initially the line is up to text->h characters long. */
604       chars_left = ss_end (text->string) - cp;
605       if (chars_left == 0)
606         break;
607       line_len = MIN (chars_left, text->h);
608
609       /* A new-line terminates the line prematurely. */
610       end = memchr (cp, '\n', line_len);
611       if (end != NULL)
612         line_len = end - cp;
613
614       /* Don't cut off words if it can be avoided. */
615       if (cp + line_len < ss_end (text->string))
616         {
617           size_t space_len = line_len;
618           while (space_len > 0 && !isspace ((unsigned char) cp[space_len]))
619             space_len--;
620           if (space_len > 0)
621             line_len = space_len;
622         }
623
624       /* Draw text. */
625       if (draw)
626         text_draw (this,
627                    text->font,
628                    text->x, text->y + (text->v - height_left),
629                    text->justification, text->h,
630                    cp, line_len);
631
632       /* Update. */
633       height_left--;
634       if (line_len > max_width)
635         max_width = line_len;
636
637       /* Next line. */
638       cp += line_len;
639       if (cp < ss_end (text->string) && isspace ((unsigned char) *cp))
640         cp++;
641     }
642
643   if (width != NULL)
644     *width = max_width;
645   if (height != NULL)
646     *height = text->v - height_left;
647 }
648
649 static void
650 ascii_text_metrics (struct outp_driver *this, const struct outp_text *t,
651                     int *width, int *height)
652 {
653   delineate (this, t, false, width, height);
654 }
655
656 static void
657 ascii_text_draw (struct outp_driver *this, const struct outp_text *t)
658 {
659   assert (this->page_open);
660   delineate (this, t, true, NULL, NULL);
661 }
662 \f
663 /* ascii_close_page () and support routines. */
664
665 /* Writes the LENGTH characters in S to OUT.  */
666 static void
667 output_line (struct outp_driver *this, const struct line *line,
668              struct string *out)
669 {
670   struct ascii_driver_ext *ext = this->ext;
671   const unsigned short *s = line->chars;
672   size_t length;
673
674   for (length = line->char_cnt; length-- > 0; s++)
675     if (*s & ATTR_BOX)
676       ds_put_cstr (out, ext->box[*s & 0xff]);
677     else
678       {
679         if (*s & ATTR_EMPHASIS)
680           {
681             if (ext->emphasis == EMPH_BOLD)
682               {
683                 ds_put_char (out, *s);
684                 ds_put_char (out, '\b');
685               }
686             else if (ext->emphasis == EMPH_UNDERLINE)
687               ds_put_cstr (out, "_\b");
688           }
689         ds_put_char (out, *s);
690       }
691 }
692
693 static void
694 append_lr_justified (struct string *out, int width,
695                      const char *left, const char *right)
696 {
697   ds_put_char_multiple (out, ' ', width);
698   if (left != NULL)
699     {
700       size_t length = MIN (strlen (left), width);
701       memcpy (ds_end (out) - width, left, length);
702     }
703   if (right != NULL)
704     {
705       size_t length = MIN (strlen (right), width);
706       memcpy (ds_end (out) - length, right, length);
707     }
708   ds_put_char (out, '\n');
709 }
710
711 static void
712 dump_output (struct outp_driver *this, struct string *out)
713 {
714   struct ascii_driver_ext *x = this->ext;
715   fwrite (ds_data (out), ds_length (out), 1, x->file);
716   ds_clear (out);
717 }
718
719 static void
720 ascii_close_page (struct outp_driver *this)
721 {
722   struct ascii_driver_ext *x = this->ext;
723   struct string out;
724   int line_num;
725
726   if (x->file == NULL)
727     return;
728
729   ds_init_empty (&out);
730
731   ds_put_char_multiple (&out, '\n', x->top_margin);
732   if (x->headers)
733     {
734       char *r1, *r2;
735
736       r1 = xasprintf (_("%s - Page %d"), get_start_date (), x->page_number);
737       r2 = xasprintf ("%s - %s" , version, host_system);
738
739       append_lr_justified (&out, this->width, outp_title, r1);
740       append_lr_justified (&out, this->width, outp_subtitle, r2);
741       ds_put_char (&out, '\n');
742
743       free (r1);
744       free (r2);
745     }
746   dump_output (this, &out);
747
748   for (line_num = 0; line_num < this->length; line_num++)
749     {
750
751       /* Squeeze multiple blank lines into a single blank line if
752          requested. */
753       if (x->squeeze_blank_lines)
754         {
755           if (line_num >= x->line_cap)
756             break;
757           if (line_num > 0
758               && x->lines[line_num].char_cnt == 0
759               && x->lines[line_num - 1].char_cnt == 0)
760             continue;
761         }
762
763       if (line_num < x->line_cap)
764         output_line (this, &x->lines[line_num], &out);
765       ds_put_char (&out, '\n');
766       dump_output (this, &out);
767     }
768
769   ds_put_char_multiple (&out, '\n', x->bottom_margin);
770   if (x->paginate)
771     ds_put_char (&out, '\f');
772
773   dump_output (this, &out);
774   ds_destroy (&out);
775 }
776
777 /* Flushes all output to the user and lets the user deal with it.
778    This is applied only to output drivers that are designated as
779    "screen" drivers that the user is interacting with in real
780    time. */
781 static void
782 ascii_flush (struct outp_driver *this)
783 {
784   struct ascii_driver_ext *x = this->ext;
785
786   if (x->file != NULL)
787     {
788       if (fn_close (x->file_name, x->file) != 0)
789         error (0, errno, _("ascii: closing output file \"%s\""),
790                x->file_name);
791       pool_detach_file (x->pool, x->file);
792       x->file = NULL;
793     }
794 }
795
796 static void
797 ascii_chart_initialise (struct outp_driver *this, struct chart *ch)
798 {
799   struct ascii_driver_ext *x = this->ext;
800   struct outp_text t;
801   char *text;
802
803   if (x->chart_type == NULL)
804     return;
805
806   /* Initialize chart. */
807   chart_init_separate (ch, x->chart_type, x->chart_file_name, ++x->chart_cnt);
808   if (ch->file_name == NULL)
809     return;
810
811   /* Mention chart in output.
812      First advance current position. */
813   if (!this->page_open)
814     outp_open_page (this);
815   else
816     {
817       this->cp_y++;
818       if (this->cp_y >= this->length)
819         {
820           outp_close_page (this);
821           outp_open_page (this);
822         }
823     }
824
825   /* Then write the text. */
826   text = xasprintf ("See %s for a chart.", ch->file_name);
827   t.font = OUTP_FIXED;
828   t.justification = OUTP_LEFT;
829   t.string = ss_cstr (text);
830   t.h = this->width;
831   t.v = 1;
832   t.x = 0;
833   t.y = this->cp_y;
834   ascii_text_draw (this, &t);
835   this->cp_y++;
836
837   free (text);
838 }
839
840 static void
841 ascii_chart_finalise (struct outp_driver *this, struct chart *ch)
842 {
843   struct ascii_driver_ext *x = this->ext;
844   if (x->chart_type != NULL)
845     chart_finalise_separate (ch);
846 }
847
848 const struct outp_class ascii_class =
849 {
850   "ascii",
851   0,
852
853   ascii_open_driver,
854   ascii_close_driver,
855
856   ascii_open_page,
857   ascii_close_page,
858   ascii_flush,
859
860   ascii_submit,
861
862   ascii_line,
863   ascii_text_metrics,
864   ascii_text_draw,
865
866   ascii_chart_initialise,
867   ascii_chart_finalise
868 };