Output drivers: Remove assertions on OUTPUT_ITEM_GROUP
[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:
332       break;
333
334     case OUTPUT_ITEM_IMAGE:
335       {
336         char *file_name = xr_write_png_image (
337           item->image, tex->chart_file_name, tex->chart_cnt++);
338         if (file_name != NULL)
339           {
340             shipout (&tex->token_list, "\\includegraphics{%s}\n", file_name);
341             tex->require_graphics = true;
342             free (file_name);
343           }
344       }
345       break;
346
347     case OUTPUT_ITEM_MESSAGE:
348       {
349         char *s = msg_to_string (item->message);
350         tex_escape_string (tex, s, false);
351         shipout (&tex->token_list, "\\par\n");
352         free (s);
353       }
354       break;
355
356     case OUTPUT_ITEM_PAGE_BREAK:
357       break;
358
359     case OUTPUT_ITEM_TABLE:
360       tex_output_table (tex, item->table);
361       break;
362
363     case OUTPUT_ITEM_TEXT:
364       {
365         char *s = text_item_get_plain_text (item);
366
367         switch (item->text.subtype)
368           {
369           case TEXT_ITEM_PAGE_TITLE:
370             shipout (&tex->token_list, "\\headline={\\bf ");
371             tex_escape_string (tex, s, false);
372             shipout (&tex->token_list, "\\hfil}\n");
373             break;
374
375           case TEXT_ITEM_LOG:
376             shipout (&tex->token_list, "{\\tt ");
377             tex_escape_string (tex, s, false);
378             shipout (&tex->token_list, "}\\par\n\n");
379             break;
380
381           case TEXT_ITEM_SYNTAX:
382             /* So far as I'm aware, this can never happen.  */
383           default:
384             printf ("Unhandled type %d\n", item->text.subtype);
385             break;
386           }
387         free (s);
388       }
389       break;
390     }
391 }
392
393 static void
394 tex_put_footnote_markers (struct tex_driver *tex,
395                           const struct pivot_table *pt,
396                           const struct pivot_value_ex *ex)
397 {
398   size_t n_visible = 0;
399   for (size_t i = 0; i < ex->n_footnotes; i++)
400     {
401       const struct pivot_footnote *f = pt->footnotes[ex->footnote_indexes[i]];
402       if (f->show)
403         {
404           if (!n_visible++)
405             shipout (&tex->token_list, "$^{");
406
407           char *marker = pivot_footnote_marker_string (f, pt);
408           tex_escape_string (tex, marker, true);
409           free (marker);
410         }
411     }
412   if (n_visible)
413     shipout (&tex->token_list, "}$");
414 }
415
416 static void
417 tex_put_table_cell (struct tex_driver *tex, const struct pivot_table *pt,
418                     const struct table_cell *cell)
419 {
420   struct string s = DS_EMPTY_INITIALIZER;
421   pivot_value_format_body (cell->value, pt, &s);
422   tex_escape_string (tex, ds_cstr (&s), false);
423   ds_destroy (&s);
424
425   tex_put_footnote_markers (tex, pt, pivot_value_ex (cell->value));
426 }
427
428 static void
429 tex_output_table_layer (struct tex_driver *tex, const struct pivot_table *pt,
430                         const size_t *layer_indexes)
431 {
432   /* Tables are rendered in TeX with the \halign command.
433      This is described in the TeXbook Ch. 22 */
434   struct table *title, *layers, *body, *caption;
435   struct pivot_footnote **footnotes;
436   size_t n_footnotes;
437   pivot_output (pt, layer_indexes, true, &title, &layers, &body,
438                 &caption, NULL, &footnotes, &n_footnotes);
439
440   shipout (&tex->token_list, "\n{\\parindent=0pt\n");
441
442   if (caption)
443     {
444       shipout (&tex->token_list, "{\\sl ");
445       struct table_cell cell;
446       table_get_cell (caption, 0, 0, &cell);
447       tex_put_table_cell (tex, pt, &cell);
448       shipout (&tex->token_list, "}\n\n");
449     }
450
451   if (title || layers)
452     {
453       if (title)
454         {
455           shipout (&tex->token_list, "{\\bf ");
456           struct table_cell cell;
457           table_get_cell (title, 0, 0, &cell);
458           tex_put_table_cell (tex, pt, &cell);
459           shipout (&tex->token_list, "}\\par\n");
460         }
461
462       if (layers)
463         {
464           for (size_t y = 0; y < layers->n[V]; y++)
465             {
466               shipout (&tex->token_list, "{");
467               struct table_cell cell;
468               table_get_cell (layers, 0, y, &cell);
469               tex_put_table_cell (tex, pt, &cell);
470               shipout (&tex->token_list, "}\\par\n");
471             }
472         }
473     }
474
475   shipout (&tex->token_list, "\\offinterlineskip\\halign{\\strut%%\n");
476
477   /* Generate the preamble */
478   for (int x = 0; x < body->n[H]; ++x)
479     {
480       shipout (&tex->token_list, "{\\vbox{\\cell{%d}#}}", body->n[H]);
481
482       if (x < body->n[H] - 1)
483         {
484           shipout (&tex->token_list, "\\hskip\\psppcolumnspace\\hfil");
485           shipout (&tex->token_list, "&\\vrule\n");
486         }
487       else
488         shipout (&tex->token_list, "\\cr\n");
489     }
490
491   /* Emit the row data */
492   for (int y = 0; y < body->n[V]; y++)
493     {
494       enum { H = TABLE_HORZ, V = TABLE_VERT };
495       bool is_column_header = y < body->h[V][0] || y >= body->n[V] - body->h[V][1];
496       int prev_x = -1;
497       int skipped = 0;
498       for (int x = 0; x < body->n[H];)
499         {
500           struct table_cell cell;
501
502           table_get_cell (body, x, y, &cell);
503
504           int colspan = table_cell_colspan (&cell);
505           if (x > 0)
506             shipout (&tex->token_list, "&");
507           else
508             for (int i = 0; i < skipped - colspan; ++i)
509               shipout (&tex->token_list, "&");
510
511
512           if (x != cell.d[TABLE_HORZ][0] || y != cell.d[TABLE_VERT][0])
513             goto next_1;
514
515           /* bool is_header = (y < body->h[V][0] */
516           /*                   || y >= body->n[V] - body->h[V][1] */
517           /*                   || x < body->h[H][0] */
518           /*                   || x >= body->n[H] - body->h[H][1]); */
519
520           struct string s = DS_EMPTY_INITIALIZER;
521           bool numeric = pivot_value_format_body (cell.value, pt, &s);
522
523           enum table_halign halign = table_halign_interpret (
524             cell.cell_style->halign, numeric);
525
526           /* int rowspan = table_cell_rowspan (&cell); */
527
528           /* if (rowspan > 1) */
529           /*   fprintf (tex->file, " rowspan=\"%d\"", rowspan); */
530
531           if (colspan > 1)
532             {
533               shipout (&tex->token_list, "\\multispan{%d}\\span", colspan - 1);
534               shipout (&tex->token_list, "\\hsize=%d.0\\hsize", colspan);
535               shipout (&tex->token_list, "\\advance\\hsize%d.0\\psppcolumnspace ",
536                        colspan - 1);
537             }
538
539           if (halign == TABLE_HALIGN_CENTER)
540             shipout (&tex->token_list, "\\centre{");
541
542           if (halign == TABLE_HALIGN_RIGHT)
543             shipout (&tex->token_list, "\\right{");
544
545           /* Output cell contents. */
546           tex_escape_string (tex, ds_cstr (&s), true);
547           ds_destroy (&s);
548
549           tex_put_footnote_markers (tex, pt, pivot_value_ex (cell.value));
550           if (halign == TABLE_HALIGN_CENTER || halign == TABLE_HALIGN_RIGHT)
551             {
552               shipout (&tex->token_list, "}");
553             }
554
555         next_1:
556           skipped = x - prev_x;
557           prev_x = x;
558           x = cell.d[TABLE_HORZ][1];
559         }
560       shipout (&tex->token_list, "\\cr\n");
561       if (is_column_header)
562         shipout (&tex->token_list, "\\noalign{\\hrule\\vskip -\\normalbaselineskip}\\cr\n");
563     }
564
565   shipout (&tex->token_list, "}%% End of \\halign\n");
566
567   /* Shipout any footnotes.  */
568   if (n_footnotes > 0)
569     shipout (&tex->token_list, "\\vskip 0.5ex\n");
570
571   for (int i = 0; i < n_footnotes; ++i)
572     {
573       char *marker = pivot_footnote_marker_string (footnotes[i], pt);
574       char *content = pivot_value_to_string (footnotes[i]->content, pt);
575
576       shipout (&tex->token_list, "$^{");
577       tex_escape_string (tex, marker, false);
578       shipout (&tex->token_list, "}$");
579       tex_escape_string (tex, content, false);
580
581       free (content);
582       free (marker);
583     }
584
585   shipout (&tex->token_list, "}\n\\vskip 3ex\n\n");
586
587   table_unref (title);
588   table_unref (layers);
589   table_unref (body);
590   table_unref (caption);
591   free (footnotes);
592 }
593
594 static void
595 tex_output_table (struct tex_driver *tex, const struct pivot_table *pt)
596 {
597   size_t *layer_indexes;
598   PIVOT_OUTPUT_FOR_EACH_LAYER (layer_indexes, pt, true)
599     tex_output_table_layer (tex, pt, layer_indexes);
600 }
601
602 struct output_driver_factory tex_driver_factory =
603   { "tex", "pspp.tex", tex_create };
604
605 static const struct output_driver_class tex_driver_class =
606   {
607     .name = "tex",
608     .destroy = tex_destroy,
609     .submit = tex_submit,
610   };