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