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