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