output: Replace OUTPUT_ITEM_PAGE_SETUP by a new driver function.
[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       NOT_REACHED ();
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 size_t *footnote_indexes,
397                           size_t n_footnotes)
398 {
399   size_t n_visible = 0;
400   for (size_t i = 0; i < n_footnotes; i++)
401     {
402       const struct pivot_footnote *f = pt->footnotes[footnote_indexes[i]];
403       if (f->show)
404         {
405           if (!n_visible++)
406             shipout (&tex->token_list, "$^{");
407
408           char *marker = pivot_footnote_marker_string (f, pt);
409           tex_escape_string (tex, marker, true);
410           free (marker);
411         }
412     }
413   if (n_visible)
414     shipout (&tex->token_list, "}$");
415 }
416
417 static void
418 tex_put_table_cell (struct tex_driver *tex, const struct pivot_table *pt,
419                     const struct table_cell *cell)
420 {
421   struct string s = DS_EMPTY_INITIALIZER;
422   pivot_value_format_body (cell->value, pt, &s);
423   tex_escape_string (tex, ds_cstr (&s), false);
424   ds_destroy (&s);
425
426   tex_put_footnote_markers (tex, pt,
427                             cell->value->footnote_indexes,
428                             cell->value->n_footnotes);
429 }
430
431 static void
432 tex_output_table_layer (struct tex_driver *tex, const struct pivot_table *pt,
433                         const size_t *layer_indexes)
434 {
435   /* Tables are rendered in TeX with the \halign command.
436      This is described in the TeXbook Ch. 22 */
437   struct table *title, *layers, *body, *caption;
438   struct pivot_footnote **footnotes;
439   size_t n_footnotes;
440   pivot_output (pt, layer_indexes, true, &title, &layers, &body,
441                 &caption, NULL, &footnotes, &n_footnotes);
442
443   shipout (&tex->token_list, "\n{\\parindent=0pt\n");
444
445   if (caption)
446     {
447       shipout (&tex->token_list, "{\\sl ");
448       struct table_cell cell;
449       table_get_cell (caption, 0, 0, &cell);
450       tex_put_table_cell (tex, pt, &cell);
451       shipout (&tex->token_list, "}\n\n");
452     }
453
454   if (title || layers)
455     {
456       if (title)
457         {
458           shipout (&tex->token_list, "{\\bf ");
459           struct table_cell cell;
460           table_get_cell (title, 0, 0, &cell);
461           tex_put_table_cell (tex, pt, &cell);
462           shipout (&tex->token_list, "}\\par\n");
463         }
464
465       if (layers)
466         {
467           for (size_t y = 0; y < layers->n[V]; y++)
468             {
469               shipout (&tex->token_list, "{");
470               struct table_cell cell;
471               table_get_cell (layers, 0, y, &cell);
472               tex_put_table_cell (tex, pt, &cell);
473               shipout (&tex->token_list, "}\\par\n");
474             }
475         }
476     }
477
478   shipout (&tex->token_list, "\\offinterlineskip\\halign{\\strut%%\n");
479
480   /* Generate the preamble */
481   for (int x = 0; x < body->n[H]; ++x)
482     {
483       shipout (&tex->token_list, "{\\vbox{\\cell{%d}#}}", body->n[H]);
484
485       if (x < body->n[H] - 1)
486         {
487           shipout (&tex->token_list, "\\hskip\\psppcolumnspace\\hfil");
488           shipout (&tex->token_list, "&\\vrule\n");
489         }
490       else
491         shipout (&tex->token_list, "\\cr\n");
492     }
493
494   /* Emit the row data */
495   for (int y = 0; y < body->n[V]; y++)
496     {
497       enum { H = TABLE_HORZ, V = TABLE_VERT };
498       bool is_column_header = y < body->h[V][0] || y >= body->n[V] - body->h[V][1];
499       int prev_x = -1;
500       int skipped = 0;
501       for (int x = 0; x < body->n[H];)
502         {
503           struct table_cell cell;
504
505           table_get_cell (body, x, y, &cell);
506
507           int colspan = table_cell_colspan (&cell);
508           if (x > 0)
509             shipout (&tex->token_list, "&");
510           else
511             for (int i = 0; i < skipped - colspan; ++i)
512               shipout (&tex->token_list, "&");
513
514
515           if (x != cell.d[TABLE_HORZ][0] || y != cell.d[TABLE_VERT][0])
516             goto next_1;
517
518           /* bool is_header = (y < body->h[V][0] */
519           /*                   || y >= body->n[V] - body->h[V][1] */
520           /*                   || x < body->h[H][0] */
521           /*                   || x >= body->n[H] - body->h[H][1]); */
522
523           struct string s = DS_EMPTY_INITIALIZER;
524           bool numeric = pivot_value_format_body (cell.value, pt, &s);
525
526           enum table_halign halign = table_halign_interpret (
527             cell.cell_style->halign, numeric);
528
529           /* int rowspan = table_cell_rowspan (&cell); */
530
531           /* if (rowspan > 1) */
532           /*   fprintf (tex->file, " rowspan=\"%d\"", rowspan); */
533
534           if (colspan > 1)
535             {
536               shipout (&tex->token_list, "\\multispan{%d}\\span", colspan - 1);
537               shipout (&tex->token_list, "\\hsize=%d.0\\hsize", colspan);
538               shipout (&tex->token_list, "\\advance\\hsize%d.0\\psppcolumnspace ",
539                        colspan - 1);
540             }
541
542           if (halign == TABLE_HALIGN_CENTER)
543             shipout (&tex->token_list, "\\centre{");
544
545           if (halign == TABLE_HALIGN_RIGHT)
546             shipout (&tex->token_list, "\\right{");
547
548           /* Output cell contents. */
549           tex_escape_string (tex, ds_cstr (&s), true);
550           ds_destroy (&s);
551
552           tex_put_footnote_markers (tex, pt, cell.value->footnote_indexes,
553                                     cell.value->n_footnotes);
554           if (halign == TABLE_HALIGN_CENTER || halign == TABLE_HALIGN_RIGHT)
555             {
556               shipout (&tex->token_list, "}");
557             }
558
559         next_1:
560           skipped = x - prev_x;
561           prev_x = x;
562           x = cell.d[TABLE_HORZ][1];
563         }
564       shipout (&tex->token_list, "\\cr\n");
565       if (is_column_header)
566         shipout (&tex->token_list, "\\noalign{\\hrule\\vskip -\\normalbaselineskip}\\cr\n");
567     }
568
569   shipout (&tex->token_list, "}%% End of \\halign\n");
570
571   /* Shipout any footnotes.  */
572   if (n_footnotes > 0)
573     shipout (&tex->token_list, "\\vskip 0.5ex\n");
574
575   for (int i = 0; i < n_footnotes; ++i)
576     {
577       char *marker = pivot_footnote_marker_string (footnotes[i], pt);
578       char *content = pivot_value_to_string (footnotes[i]->content, pt);
579
580       shipout (&tex->token_list, "$^{");
581       tex_escape_string (tex, marker, false);
582       shipout (&tex->token_list, "}$");
583       tex_escape_string (tex, content, false);
584
585       free (content);
586       free (marker);
587     }
588
589   shipout (&tex->token_list, "}\n\\vskip 3ex\n\n");
590
591   table_unref (title);
592   table_unref (layers);
593   table_unref (body);
594   table_unref (caption);
595   free (footnotes);
596 }
597
598 static void
599 tex_output_table (struct tex_driver *tex, const struct pivot_table *pt)
600 {
601   size_t *layer_indexes;
602   PIVOT_OUTPUT_FOR_EACH_LAYER (layer_indexes, pt, true)
603     tex_output_table_layer (tex, pt, layer_indexes);
604 }
605
606 struct output_driver_factory tex_driver_factory =
607   { "tex", "pspp.tex", tex_create };
608
609 static const struct output_driver_class tex_driver_class =
610   {
611     .name = "tex",
612     .destroy = tex_destroy,
613     .submit = tex_submit,
614   };