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