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