output-item: Collapse the inheritance hierarchy into a single struct.
[pspp] / src / output / tex.c
1 /* PSPP - a program for statistical analysis.
2    Copyright (C) 2020 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 <errno.h>
20 #include <stdint.h>
21 #include <stdlib.h>
22 #include <ctype.h>
23 #include <time.h>
24 #include <unistd.h>
25 #include <locale.h>
26
27 #include "gl/mbiter.h"
28 #include "data/file-name.h"
29 #include "data/file-handle-def.h"
30 #include "libpspp/assertion.h"
31 #include "libpspp/cast.h"
32 #include "libpspp/compiler.h"
33 #include "libpspp/hmap.h"
34 #include "libpspp/ll.h"
35 #include "libpspp/i18n.h"
36 #include "libpspp/message.h"
37 #include "libpspp/temp-file.h"
38 #include "libpspp/version.h"
39 #include "output/cairo-chart.h"
40 #include "output/driver-provider.h"
41 #include "output/options.h"
42 #include "output/output-item.h"
43 #include "output/pivot-output.h"
44 #include "output/pivot-table.h"
45 #include "output/table-provider.h"
46 #include "output/tex-rendering.h"
47 #include "output/tex-parsing.h"
48
49
50 #include "tex-glyphs.h"
51
52 #include "gl/minmax.h"
53 #include "gl/xalloc.h"
54 #include "gl/c-vasnprintf.h"
55
56 #include "gettext.h"
57 #define _(msgid) gettext (msgid)
58
59 /* This file uses TABLE_HORZ and TABLE_VERT enough to warrant abbreviating. */
60 #define H TABLE_HORZ
61 #define V TABLE_VERT
62
63 /* The desired maximum line length in the TeX file.  */
64 #define TEX_LINE_MAX 80
65
66 struct tex_driver
67   {
68     struct output_driver driver;
69     /* A hash table containing any Tex macros which need to be emitted.  */
70     struct hmap macros;
71     bool require_graphics;
72     struct cell_color fg;
73     struct cell_color bg;
74     struct file_handle *handle;
75     char *chart_file_name;
76
77     FILE *file;
78     size_t chart_cnt;
79
80     struct ll_list preamble_list;
81     struct ll_list token_list;
82   };
83
84 /* Ships the string STR to the driver.  */
85 static void
86 shipout (struct ll_list *list, const char *str, ...)
87 {
88   va_list args;
89   va_start (args, str);
90
91   size_t length;
92   char *s = c_vasnprintf (NULL, &length, str, args);
93
94   tex_parse (s, list);
95
96   va_end (args);
97   free (s);
98 }
99
100 static const struct output_driver_class tex_driver_class;
101
102 static void tex_output_table (struct tex_driver *, const struct pivot_table *);
103
104 static struct tex_driver *
105 tex_driver_cast (struct output_driver *driver)
106 {
107   assert (driver->class == &tex_driver_class);
108   return UP_CAST (driver, struct tex_driver, driver);
109 }
110
111 static struct driver_option *
112 opt (struct output_driver *d, struct string_map *options, const char *key,
113      const char *default_value)
114 {
115   return driver_option_get (d, options, key, default_value);
116 }
117
118 static struct output_driver *
119 tex_create (struct file_handle *fh, enum settings_output_devices device_type,
120              struct string_map *o)
121 {
122   struct output_driver *d;
123   struct tex_driver *tex = XZALLOC (struct tex_driver);
124   hmap_init (&tex->macros);
125   ll_init (&tex->preamble_list);
126   ll_init (&tex->token_list);
127
128   d = &tex->driver;
129   output_driver_init (&tex->driver, &tex_driver_class, fh_get_file_name (fh),
130                       device_type);
131   tex->handle = fh;
132   tex->chart_file_name = parse_chart_file_name (opt (d, o, "charts",
133                                                       fh_get_file_name (fh)));
134   tex->chart_cnt = 1;
135   tex->bg = parse_color (opt (d, o, "background-color", "#FFFFFFFFFFFF"));
136   tex->fg = parse_color (opt (d, o, "foreground-color", "#000000000000"));
137
138   tex->file = fn_open (tex->handle, "w");
139   if (tex->file == NULL)
140     {
141       msg_error (errno, _("error opening output file `%s'"),
142                  fh_get_file_name (tex->handle));
143       goto error;
144     }
145
146   return d;
147
148  error:
149   output_driver_destroy (d);
150   return NULL;
151 }
152
153
154 /* Emit all the tokens in LIST to FILE.
155    Then destroy LIST and its contents.  */
156 static void
157 post_process_tokens (FILE *file, struct ll_list *list)
158 {
159   size_t line_len = 0;
160   struct tex_token *tt;
161   struct tex_token *ttnext;
162   ll_for_each_safe (tt, ttnext, struct tex_token, ll, list)
163     {
164       if (tt->cat == CAT_SPACE)
165         {
166           /* Count the number of characters up to the next space,
167              and if it'll not fit on to the line, then make a line
168              break here.  */
169           size_t word_len = 0;
170           struct tex_token *prev_x = NULL;
171           for (struct ll *x = ll_next (&tt->ll); x != ll_null (list);
172                x = ll_next (x))
173             {
174               struct tex_token *nt = ll_data (x, struct tex_token, ll);
175               if (nt->cat == CAT_SPACE || nt->cat == CAT_EOL)
176                 break;
177               if (prev_x && (prev_x->cat == CAT_COMMENT) && (nt->cat != CAT_COMMENT))
178                 break;
179               word_len += ds_length (&nt->str);
180               prev_x = nt;
181             }
182
183           if ((word_len < TEX_LINE_MAX) && (line_len + word_len >= TEX_LINE_MAX - 1))
184             {
185               fputs ("\n", file);
186               line_len = 0;
187               continue;
188             }
189         }
190
191       line_len += ds_length (&tt->str);
192       if (tt->cat == CAT_EOL)
193         line_len = 0;
194       if (line_len >= TEX_LINE_MAX)
195         {
196           fputs ("%\n", file);
197           line_len = ds_length (&tt->str);
198         }
199       if (tt->cat == CAT_COMMENT)
200         line_len = 0;
201       fputs (ds_cstr (&tt->str), file);
202       ds_destroy (&tt->str);
203       free (tt);
204     }
205 }
206
207
208 static void
209 tex_destroy (struct output_driver *driver)
210 {
211   struct tex_driver *tex = tex_driver_cast (driver);
212
213   shipout (&tex->preamble_list, "%%%% TeX output of pspp\n\n");
214   shipout (&tex->preamble_list, "%%%% Define the horizontal space between table columns\n");
215   shipout (&tex->preamble_list, "\\def\\psppcolumnspace{1mm}\n\n");
216
217   char *ln = get_language ();
218   if (ln)
219     shipout (&tex->preamble_list, "%%%% Language is \"%s\"\n", ln);
220   free (ln);
221   shipout (&tex->preamble_list, "\n");
222
223   shipout (&tex->preamble_list, "%%%% Sets the environment for rendering material in table cell\n");
224   shipout (&tex->preamble_list, "%%%% The parameter is the number of columns in the table\n");
225   shipout (&tex->preamble_list,
226            "\\def\\cell#1{\\normalbaselines\\advance\\hsize by -#1.0\\psppcolumnspace"
227            "\\advance\\hsize by \\psppcolumnspace"
228            "\\divide\\hsize by #1"
229            "\\noindent\\raggedright\\hskip0pt}\n\n");
230
231   /* centre macro */
232   shipout (&tex->preamble_list,
233            "%%%% Render the text centre justified\n"
234            "\\def\\startcentre{\\begingroup\\leftskip=0pt plus 1fil\n"
235            "\\rightskip=\\leftskip\\parfillskip=0pt}\n");
236   shipout (&tex->preamble_list, "\\def\\stopcentre{\\par\\endgroup}\n");
237   shipout (&tex->preamble_list, "\\long\\def\\centre#1{\\startcentre#1\\stopcentre}\n\n");
238
239
240   /* right macro */
241   shipout (&tex->preamble_list,
242            "%%%% Render the text right justified\n"
243            "\\def\\startright{\\begingroup\\leftskip=0pt plus 1fil\n"
244            "\\parfillskip=0pt}\n");
245   shipout (&tex->preamble_list, "\\def\\stopright{\\par\\endgroup}\n");
246   shipout (&tex->preamble_list, "\\long\\def\\right#1{\\startright#1\\stopright}\n\n");
247
248
249   /* Emit all the macro defintions.  */
250   struct tex_macro *m;
251   struct tex_macro *next;
252   HMAP_FOR_EACH_SAFE (m, next, struct tex_macro, node, &tex->macros)
253     {
254       shipout (&tex->preamble_list, "%s", tex_macro[m->index]);
255       shipout (&tex->preamble_list, "\n\n");
256       free (m);
257     }
258   hmap_destroy (&tex->macros);
259
260   if (tex->require_graphics)
261     shipout (&tex->preamble_list, "\\input graphicx\n\n");
262
263   post_process_tokens (tex->file, &tex->preamble_list);
264
265   shipout (&tex->token_list, "\n\\bye\n");
266
267   post_process_tokens (tex->file, &tex->token_list);
268
269   fn_close (tex->handle, tex->file);
270
271   free (tex->chart_file_name);
272   fh_unref (tex->handle);
273   free (tex);
274 }
275
276 /* Ship out TEXT (which must be a UTF-8 encoded string to the driver's output.
277    if TABULAR is true, then this text is within a table.  */
278 static void
279 tex_escape_string (struct tex_driver *tex, const char *text,
280                    bool tabular)
281 {
282   size_t n = strlen (text);
283   while (n > 0)
284     {
285       const char *frag = u8_to_tex_fragments (&text, &n, &tex->macros);
286       shipout (&tex->token_list, "%s", frag);
287       if (text[0] != '\0' && tabular && 0 == strcmp (frag, "."))
288         {
289           /* Peek ahead to the next code sequence */
290           size_t nn = n;
291           const char *t = text;
292           const char *next = u8_to_tex_fragments (&t, &nn, &tex->macros);
293           /* If a period followed by whitespace is encountered within tabular
294              material, then it is reasonable to assume, that it is an
295              abbreviation (like "Sig." or "Std. Deviation") rather than the
296              end of a sentance.  */
297           if (next && 0 == strcmp (" ", next))
298             {
299               shipout (&tex->token_list, "\\ ");
300             }
301         }
302     }
303 }
304
305 static void
306 tex_submit (struct output_driver *driver, const struct output_item *item)
307 {
308   struct tex_driver *tex = tex_driver_cast (driver);
309
310   switch (item->type)
311     {
312     case OUTPUT_ITEM_CHART:
313       if (tex->chart_file_name != NULL)
314         {
315           char *file_name = xr_draw_png_chart (item->chart,
316                                                tex->chart_file_name,
317                                                tex->chart_cnt++,
318                                                &tex->fg, &tex->bg);
319           if (file_name != NULL)
320             {
321               //const char *title = chart_item_get_title (chart_item);
322               //          printf ("The chart title is %s\n", title);
323
324               shipout (&tex->token_list, "\\includegraphics{%s}\n", file_name);
325               tex->require_graphics = true;
326               free (file_name);
327             }
328         }
329       break;
330
331     case OUTPUT_ITEM_GROUP_OPEN:
332       break;
333
334     case OUTPUT_ITEM_GROUP_CLOSE:
335       break;
336
337     case OUTPUT_ITEM_IMAGE:
338       {
339         char *file_name = xr_write_png_image (
340           item->image, tex->chart_file_name, tex->chart_cnt++);
341         if (file_name != NULL)
342           {
343             shipout (&tex->token_list, "\\includegraphics{%s}\n", file_name);
344             tex->require_graphics = true;
345             free (file_name);
346           }
347       }
348       break;
349
350     case OUTPUT_ITEM_MESSAGE:
351       {
352         char *s = msg_to_string (item->message);
353         tex_escape_string (tex, s, false);
354         shipout (&tex->token_list, "\\par\n");
355         free (s);
356       }
357       break;
358
359     case OUTPUT_ITEM_PAGE_BREAK:
360       break;
361
362     case OUTPUT_ITEM_PAGE_SETUP:
363       break;
364
365     case OUTPUT_ITEM_TABLE:
366       tex_output_table (tex, item->table);
367       break;
368
369     case OUTPUT_ITEM_TEXT:
370       {
371         char *s = text_item_get_plain_text (item);
372
373         switch (item->text.subtype)
374           {
375           case TEXT_ITEM_PAGE_TITLE:
376             shipout (&tex->token_list, "\\headline={\\bf ");
377             tex_escape_string (tex, s, false);
378             shipout (&tex->token_list, "\\hfil}\n");
379             break;
380
381           case TEXT_ITEM_LOG:
382             shipout (&tex->token_list, "{\\tt ");
383             tex_escape_string (tex, s, false);
384             shipout (&tex->token_list, "}\\par\n\n");
385             break;
386
387           case TEXT_ITEM_SYNTAX:
388             /* So far as I'm aware, this can never happen.  */
389           default:
390             printf ("Unhandled type %d\n", item->text.subtype);
391             break;
392           }
393         free (s);
394       }
395       break;
396     }
397 }
398
399 static void
400 tex_put_footnote_markers (struct tex_driver *tex,
401                           const struct pivot_table *pt,
402                           const size_t *footnote_indexes,
403                           size_t n_footnotes)
404 {
405   size_t n_visible = 0;
406   for (size_t i = 0; i < n_footnotes; i++)
407     {
408       const struct pivot_footnote *f = pt->footnotes[footnote_indexes[i]];
409       if (f->show)
410         {
411           if (!n_visible++)
412             shipout (&tex->token_list, "$^{");
413
414           char *marker = pivot_footnote_marker_string (f, pt);
415           tex_escape_string (tex, marker, true);
416           free (marker);
417         }
418     }
419   if (n_visible)
420     shipout (&tex->token_list, "}$");
421 }
422
423 static void
424 tex_put_table_cell (struct tex_driver *tex, const struct pivot_table *pt,
425                     const struct table_cell *cell)
426 {
427   struct string s = DS_EMPTY_INITIALIZER;
428   pivot_value_format_body (cell->value, pt, &s);
429   tex_escape_string (tex, ds_cstr (&s), false);
430   ds_destroy (&s);
431
432   tex_put_footnote_markers (tex, pt,
433                             cell->value->footnote_indexes,
434                             cell->value->n_footnotes);
435 }
436
437 static void
438 tex_output_table_layer (struct tex_driver *tex, const struct pivot_table *pt,
439                         const size_t *layer_indexes)
440 {
441   /* Tables are rendered in TeX with the \halign command.
442      This is described in the TeXbook Ch. 22 */
443   struct table *title, *layers, *body, *caption;
444   struct pivot_footnote **footnotes;
445   size_t n_footnotes;
446   pivot_output (pt, layer_indexes, true, &title, &layers, &body,
447                 &caption, NULL, &footnotes, &n_footnotes);
448
449   shipout (&tex->token_list, "\n{\\parindent=0pt\n");
450
451   if (caption)
452     {
453       shipout (&tex->token_list, "{\\sl ");
454       struct table_cell cell;
455       table_get_cell (caption, 0, 0, &cell);
456       tex_put_table_cell (tex, pt, &cell);
457       shipout (&tex->token_list, "}\n\n");
458     }
459
460   if (title || layers)
461     {
462       if (title)
463         {
464           shipout (&tex->token_list, "{\\bf ");
465           struct table_cell cell;
466           table_get_cell (title, 0, 0, &cell);
467           tex_put_table_cell (tex, pt, &cell);
468           shipout (&tex->token_list, "}\\par\n");
469         }
470
471       if (layers)
472         {
473           for (size_t y = 0; y < layers->n[V]; y++)
474             {
475               shipout (&tex->token_list, "{");
476               struct table_cell cell;
477               table_get_cell (layers, 0, y, &cell);
478               tex_put_table_cell (tex, pt, &cell);
479               shipout (&tex->token_list, "}\\par\n");
480             }
481         }
482     }
483
484   shipout (&tex->token_list, "\\offinterlineskip\\halign{\\strut%%\n");
485
486   /* Generate the preamble */
487   for (int x = 0; x < body->n[H]; ++x)
488     {
489       shipout (&tex->token_list, "{\\vbox{\\cell{%d}#}}", body->n[H]);
490
491       if (x < body->n[H] - 1)
492         {
493           shipout (&tex->token_list, "\\hskip\\psppcolumnspace\\hfil");
494           shipout (&tex->token_list, "&\\vrule\n");
495         }
496       else
497         shipout (&tex->token_list, "\\cr\n");
498     }
499
500   /* Emit the row data */
501   for (int y = 0; y < body->n[V]; y++)
502     {
503       enum { H = TABLE_HORZ, V = TABLE_VERT };
504       bool is_column_header = y < body->h[V][0] || y >= body->n[V] - body->h[V][1];
505       int prev_x = -1;
506       int skipped = 0;
507       for (int x = 0; x < body->n[H];)
508         {
509           struct table_cell cell;
510
511           table_get_cell (body, x, y, &cell);
512
513           int colspan = table_cell_colspan (&cell);
514           if (x > 0)
515             shipout (&tex->token_list, "&");
516           else
517             for (int i = 0; i < skipped - colspan; ++i)
518               shipout (&tex->token_list, "&");
519
520
521           if (x != cell.d[TABLE_HORZ][0] || y != cell.d[TABLE_VERT][0])
522             goto next_1;
523
524           /* bool is_header = (y < body->h[V][0] */
525           /*                   || y >= body->n[V] - body->h[V][1] */
526           /*                   || x < body->h[H][0] */
527           /*                   || x >= body->n[H] - body->h[H][1]); */
528
529           struct string s = DS_EMPTY_INITIALIZER;
530           bool numeric = pivot_value_format_body (cell.value, pt, &s);
531
532           enum table_halign halign = table_halign_interpret (
533             cell.cell_style->halign, numeric);
534
535           /* int rowspan = table_cell_rowspan (&cell); */
536
537           /* if (rowspan > 1) */
538           /*   fprintf (tex->file, " rowspan=\"%d\"", rowspan); */
539
540           if (colspan > 1)
541             {
542               shipout (&tex->token_list, "\\multispan{%d}\\span", colspan - 1);
543               shipout (&tex->token_list, "\\hsize=%d.0\\hsize", colspan);
544               shipout (&tex->token_list, "\\advance\\hsize%d.0\\psppcolumnspace ",
545                        colspan - 1);
546             }
547
548           if (halign == TABLE_HALIGN_CENTER)
549             shipout (&tex->token_list, "\\centre{");
550
551           if (halign == TABLE_HALIGN_RIGHT)
552             shipout (&tex->token_list, "\\right{");
553
554           /* Output cell contents. */
555           tex_escape_string (tex, ds_cstr (&s), true);
556           ds_destroy (&s);
557
558           tex_put_footnote_markers (tex, pt, cell.value->footnote_indexes,
559                                     cell.value->n_footnotes);
560           if (halign == TABLE_HALIGN_CENTER || halign == TABLE_HALIGN_RIGHT)
561             {
562               shipout (&tex->token_list, "}");
563             }
564
565         next_1:
566           skipped = x - prev_x;
567           prev_x = x;
568           x = cell.d[TABLE_HORZ][1];
569         }
570       shipout (&tex->token_list, "\\cr\n");
571       if (is_column_header)
572         shipout (&tex->token_list, "\\noalign{\\hrule\\vskip -\\normalbaselineskip}\\cr\n");
573     }
574
575   shipout (&tex->token_list, "}%% End of \\halign\n");
576
577   /* Shipout any footnotes.  */
578   if (n_footnotes > 0)
579     shipout (&tex->token_list, "\\vskip 0.5ex\n");
580
581   for (int i = 0; i < n_footnotes; ++i)
582     {
583       char *marker = pivot_footnote_marker_string (footnotes[i], pt);
584       char *content = pivot_value_to_string (footnotes[i]->content, pt);
585
586       shipout (&tex->token_list, "$^{");
587       tex_escape_string (tex, marker, false);
588       shipout (&tex->token_list, "}$");
589       tex_escape_string (tex, content, false);
590
591       free (content);
592       free (marker);
593     }
594
595   shipout (&tex->token_list, "}\n\\vskip 3ex\n\n");
596
597   table_unref (title);
598   table_unref (layers);
599   table_unref (body);
600   table_unref (caption);
601   free (footnotes);
602 }
603
604 static void
605 tex_output_table (struct tex_driver *tex, const struct pivot_table *pt)
606 {
607   size_t *layer_indexes;
608   PIVOT_OUTPUT_FOR_EACH_LAYER (layer_indexes, pt, true)
609     tex_output_table_layer (tex, pt, layer_indexes);
610 }
611
612 struct output_driver_factory tex_driver_factory =
613   { "tex", "pspp.tex", tex_create };
614
615 static const struct output_driver_class tex_driver_class =
616   {
617     "tex",
618     tex_destroy,
619     tex_submit,
620     NULL,
621   };