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