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