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