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