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