82480bb3e365e58caffcb0218c0e05f4377539a5
[pspp] / src / output / odt.c
1 /* PSPP - a program for statistical analysis.
2    Copyright (C) 2009-2014 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 /* A driver for creating OpenDocument Format text files from PSPP's output */
20
21 #include <errno.h>
22 #include <libgen.h>
23 #include <libxml/xmlwriter.h>
24 #ifdef HAVE_PWD_H
25 #include <pwd.h>
26 #endif
27 #include <sys/stat.h>
28 #include <sys/types.h>
29 #include <time.h>
30 #include <unistd.h>
31
32 #include "libpspp/assertion.h"
33 #include "libpspp/cast.h"
34 #include "libpspp/message.h"
35 #include "libpspp/str.h"
36 #include "libpspp/temp-file.h"
37 #include "libpspp/version.h"
38 #include "libpspp/zip-writer.h"
39 #include "data/file-handle-def.h"
40 #include "output/driver-provider.h"
41 #include "output/message-item.h"
42 #include "output/options.h"
43 #include "output/pivot-table.h"
44 #include "output/pivot-output.h"
45 #include "output/table-item.h"
46 #include "output/table-provider.h"
47 #include "output/text-item.h"
48
49 #include "gl/xalloc.h"
50
51 #include "gettext.h"
52 #define _(msgid) gettext (msgid)
53
54 #define _xml(X) (CHAR_CAST (const xmlChar *, X))
55
56 /* This file uses TABLE_HORZ and TABLE_VERT enough to warrant abbreviating. */
57 #define H TABLE_HORZ
58 #define V TABLE_VERT
59
60 struct odt_driver
61 {
62   struct output_driver driver;
63
64   struct zip_writer *zip;     /* ZIP file writer. */
65   struct file_handle *handle; /* Handle for 'file_name'. */
66   char *file_name;            /* Output file name. */
67
68   /* content.xml */
69   xmlTextWriterPtr content_wtr; /* XML writer. */
70   FILE *content_file;           /* Temporary file. */
71
72   /* manifest.xml */
73   xmlTextWriterPtr manifest_wtr; /* XML writer. */
74   FILE *manifest_file;           /* Temporary file. */
75
76   /* Number of tables so far. */
77   int table_num;
78 };
79
80 static const struct output_driver_class odt_driver_class;
81
82 static struct odt_driver *
83 odt_driver_cast (struct output_driver *driver)
84 {
85   assert (driver->class == &odt_driver_class);
86   return UP_CAST (driver, struct odt_driver, driver);
87 }
88
89 /* Creates a new temporary file and stores it in *FILE, then creates an XML
90    writer for it and stores it in *W. */
91 static void
92 create_writer (FILE **file, xmlTextWriterPtr *w)
93 {
94   /* XXX this can fail */
95   *file = create_temp_file ();
96   *w = xmlNewTextWriter (xmlOutputBufferCreateFile (*file, NULL));
97
98   xmlTextWriterStartDocument (*w, NULL, "UTF-8", NULL);
99 }
100
101
102 static void
103 register_file (struct odt_driver *odt, const char *filename)
104 {
105   assert (odt->manifest_wtr);
106   xmlTextWriterStartElement (odt->manifest_wtr, _xml("manifest:file-entry"));
107   xmlTextWriterWriteAttribute (odt->manifest_wtr, _xml("manifest:media-type"),  _xml("text/xml"));
108   xmlTextWriterWriteAttribute (odt->manifest_wtr, _xml("manifest:full-path"),  _xml (filename));
109   xmlTextWriterEndElement (odt->manifest_wtr);
110 }
111
112 static void
113 write_style_data (struct odt_driver *odt)
114 {
115   xmlTextWriterPtr w;
116   FILE *file;
117
118   create_writer (&file, &w);
119   register_file (odt, "styles.xml");
120
121   xmlTextWriterStartElement (w, _xml ("office:document-styles"));
122   xmlTextWriterWriteAttribute (w, _xml ("xmlns:office"),
123                                _xml ("urn:oasis:names:tc:opendocument:xmlns:office:1.0"));
124
125   xmlTextWriterWriteAttribute (w, _xml ("xmlns:style"),
126                                _xml ("urn:oasis:names:tc:opendocument:xmlns:style:1.0"));
127
128   xmlTextWriterWriteAttribute (w, _xml ("xmlns:fo"),
129                                _xml ("urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0"));
130
131   xmlTextWriterWriteAttribute (w, _xml ("office:version"),  _xml ("1.1"));
132
133
134
135   xmlTextWriterStartElement (w, _xml ("office:styles"));
136
137
138   {
139     xmlTextWriterStartElement (w, _xml ("style:style"));
140     xmlTextWriterWriteAttribute (w, _xml ("style:name"),
141                                  _xml ("Standard"));
142
143     xmlTextWriterWriteAttribute (w, _xml ("style:family"),
144                                  _xml ("paragraph"));
145
146     xmlTextWriterWriteAttribute (w, _xml ("style:class"),
147                                  _xml ("text"));
148
149     xmlTextWriterEndElement (w); /* style:style */
150   }
151
152   {
153     xmlTextWriterStartElement (w, _xml ("style:style"));
154     xmlTextWriterWriteAttribute (w, _xml ("style:name"),
155                                  _xml ("Table_20_Contents"));
156
157     xmlTextWriterWriteAttribute (w, _xml ("style:display-name"),
158                                  _xml ("Table Contents"));
159
160     xmlTextWriterWriteAttribute (w, _xml ("style:family"),
161                                  _xml ("paragraph"));
162
163     xmlTextWriterWriteAttribute (w, _xml ("style:parent-style-name"),
164                                  _xml ("Standard"));
165
166     xmlTextWriterWriteAttribute (w, _xml ("style:class"),
167                                  _xml ("extra"));
168
169     xmlTextWriterEndElement (w); /* style:style */
170   }
171
172   {
173     xmlTextWriterStartElement (w, _xml ("style:style"));
174     xmlTextWriterWriteAttribute (w, _xml ("style:name"),
175                                  _xml ("Table_20_Heading"));
176
177     xmlTextWriterWriteAttribute (w, _xml ("style:display-name"),
178                                  _xml ("Table Heading"));
179
180     xmlTextWriterWriteAttribute (w, _xml ("style:family"),
181                                  _xml ("paragraph"));
182
183     xmlTextWriterWriteAttribute (w, _xml ("style:parent-style-name"),
184                                  _xml ("Table_20_Contents"));
185
186     xmlTextWriterWriteAttribute (w, _xml ("style:class"),
187                                  _xml ("extra"));
188
189
190     xmlTextWriterStartElement (w, _xml ("style:text-properties"));
191     xmlTextWriterWriteAttribute (w, _xml ("fo:font-weight"), _xml ("bold"));
192     xmlTextWriterWriteAttribute (w, _xml ("style:font-weight-asian"), _xml ("bold"));
193     xmlTextWriterWriteAttribute (w, _xml ("style:font-weight-complex"), _xml ("bold"));
194     xmlTextWriterEndElement (w); /* style:text-properties */
195
196     xmlTextWriterEndElement (w); /* style:style */
197   }
198
199   {
200     xmlTextWriterStartElement (w, _xml ("style:style"));
201     xmlTextWriterWriteAttribute (w, _xml ("style:name"), _xml ("superscript"));
202     xmlTextWriterWriteAttribute (w, _xml ("style:family"), _xml ("text"));
203
204     xmlTextWriterStartElement (w, _xml ("style:text-properties"));
205     xmlTextWriterWriteAttribute (w, _xml ("style:text-position"),
206                                  _xml ("super 58%"));
207     xmlTextWriterEndElement (w); /* style:text-properties */
208
209     xmlTextWriterEndElement (w); /* style:style */
210   }
211
212   xmlTextWriterEndElement (w); /* office:styles */
213   xmlTextWriterEndElement (w); /* office:document-styles */
214
215   xmlTextWriterEndDocument (w);
216   xmlFreeTextWriter (w);
217   zip_writer_add (odt->zip, file, "styles.xml");
218   close_temp_file (file);
219 }
220
221 static void
222 write_meta_data (struct odt_driver *odt)
223 {
224   xmlTextWriterPtr w;
225   FILE *file;
226
227   create_writer (&file, &w);
228   register_file (odt, "meta.xml");
229
230   xmlTextWriterStartElement (w, _xml ("office:document-meta"));
231   xmlTextWriterWriteAttribute (w, _xml ("xmlns:office"), _xml ("urn:oasis:names:tc:opendocument:xmlns:office:1.0"));
232   xmlTextWriterWriteAttribute (w, _xml ("xmlns:dc"),  _xml ("http://purl.org/dc/elements/1.1/"));
233   xmlTextWriterWriteAttribute (w, _xml ("xmlns:meta"), _xml ("urn:oasis:names:tc:opendocument:xmlns:meta:1.0"));
234   xmlTextWriterWriteAttribute (w, _xml ("xmlns:ooo"), _xml("http://openoffice.org/2004/office"));
235   xmlTextWriterWriteAttribute (w, _xml ("office:version"),  _xml("1.1"));
236
237   xmlTextWriterStartElement (w, _xml ("office:meta"));
238   {
239     xmlTextWriterStartElement (w, _xml ("meta:generator"));
240     xmlTextWriterWriteString (w, _xml (version));
241     xmlTextWriterEndElement (w);
242   }
243
244
245   {
246     char buf[30];
247     time_t t = time (NULL);
248     struct tm *tm =  localtime (&t);
249
250     strftime (buf, 30, "%Y-%m-%dT%H:%M:%S", tm);
251
252     xmlTextWriterStartElement (w, _xml ("meta:creation-date"));
253     xmlTextWriterWriteString (w, _xml (buf));
254     xmlTextWriterEndElement (w);
255
256     xmlTextWriterStartElement (w, _xml ("dc:date"));
257     xmlTextWriterWriteString (w, _xml (buf));
258     xmlTextWriterEndElement (w);
259   }
260
261 #ifdef HAVE_PWD_H
262   {
263     struct passwd *pw = getpwuid (getuid ());
264     if (pw != NULL)
265       {
266         xmlTextWriterStartElement (w, _xml ("meta:initial-creator"));
267         xmlTextWriterWriteString (w, _xml (strtok (pw->pw_gecos, ",")));
268         xmlTextWriterEndElement (w);
269
270         xmlTextWriterStartElement (w, _xml ("dc:creator"));
271         xmlTextWriterWriteString (w, _xml (strtok (pw->pw_gecos, ",")));
272         xmlTextWriterEndElement (w);
273       }
274   }
275 #endif
276
277   xmlTextWriterEndElement (w);
278   xmlTextWriterEndElement (w);
279   xmlTextWriterEndDocument (w);
280   xmlFreeTextWriter (w);
281   zip_writer_add (odt->zip, file, "meta.xml");
282   close_temp_file (file);
283 }
284
285 static struct output_driver *
286 odt_create (struct file_handle *fh, enum settings_output_devices device_type,
287             struct string_map *o UNUSED)
288 {
289   struct output_driver *d;
290   struct odt_driver *odt;
291   struct zip_writer *zip;
292   const char *file_name = fh_get_file_name (fh);
293
294   zip = zip_writer_create (file_name);
295   if (zip == NULL)
296     return NULL;
297
298   odt = xzalloc (sizeof *odt);
299   d = &odt->driver;
300
301   output_driver_init (d, &odt_driver_class, file_name, device_type);
302
303   odt->zip = zip;
304   odt->handle = fh;
305   odt->file_name = xstrdup (file_name);
306
307   zip_writer_add_string (zip, "mimetype",
308                          "application/vnd.oasis.opendocument.text");
309
310   /* Create the manifest */
311   create_writer (&odt->manifest_file, &odt->manifest_wtr);
312
313   xmlTextWriterStartElement (odt->manifest_wtr, _xml("manifest:manifest"));
314   xmlTextWriterWriteAttribute (odt->manifest_wtr, _xml("xmlns:manifest"),
315                                _xml("urn:oasis:names:tc:opendocument:xmlns:manifest:1.0"));
316
317
318   /* Add a manifest entry for the document as a whole */
319   xmlTextWriterStartElement (odt->manifest_wtr, _xml("manifest:file-entry"));
320   xmlTextWriterWriteAttribute (odt->manifest_wtr, _xml("manifest:media-type"),  _xml("application/vnd.oasis.opendocument.text"));
321   xmlTextWriterWriteAttribute (odt->manifest_wtr, _xml("manifest:full-path"),  _xml("/"));
322   xmlTextWriterEndElement (odt->manifest_wtr);
323
324
325   write_meta_data (odt);
326   write_style_data (odt);
327
328   create_writer (&odt->content_file, &odt->content_wtr);
329   register_file (odt, "content.xml");
330
331
332   /* Some necessary junk at the start */
333   xmlTextWriterStartElement (odt->content_wtr, _xml("office:document-content"));
334   xmlTextWriterWriteAttribute (odt->content_wtr, _xml("xmlns:office"),
335                                _xml("urn:oasis:names:tc:opendocument:xmlns:office:1.0"));
336
337   xmlTextWriterWriteAttribute (odt->content_wtr, _xml("xmlns:text"),
338                                _xml("urn:oasis:names:tc:opendocument:xmlns:text:1.0"));
339
340   xmlTextWriterWriteAttribute (odt->content_wtr, _xml("xmlns:table"),
341                                _xml("urn:oasis:names:tc:opendocument:xmlns:table:1.0"));
342
343   xmlTextWriterWriteAttribute (odt->content_wtr, _xml("office:version"), _xml("1.1"));
344
345   xmlTextWriterStartElement (odt->content_wtr, _xml("office:body"));
346   xmlTextWriterStartElement (odt->content_wtr, _xml("office:text"));
347
348
349
350   /* Close the manifest */
351   xmlTextWriterEndElement (odt->manifest_wtr);
352   xmlTextWriterEndDocument (odt->manifest_wtr);
353   xmlFreeTextWriter (odt->manifest_wtr);
354   zip_writer_add (odt->zip, odt->manifest_file, "META-INF/manifest.xml");
355   close_temp_file (odt->manifest_file);
356
357   return d;
358 }
359
360 static void
361 odt_destroy (struct output_driver *driver)
362 {
363   struct odt_driver *odt = odt_driver_cast (driver);
364
365   if (odt->content_wtr != NULL)
366     {
367       xmlTextWriterEndElement (odt->content_wtr); /* office:text */
368       xmlTextWriterEndElement (odt->content_wtr); /* office:body */
369       xmlTextWriterEndElement (odt->content_wtr); /* office:document-content */
370
371       xmlTextWriterEndDocument (odt->content_wtr);
372       xmlFreeTextWriter (odt->content_wtr);
373       zip_writer_add (odt->zip, odt->content_file, "content.xml");
374       close_temp_file (odt->content_file);
375
376       zip_writer_close (odt->zip);
377     }
378
379   fh_unref (odt->handle);
380   free (odt->file_name);
381   free (odt);
382 }
383
384 static void
385 write_xml_with_line_breaks (struct odt_driver *odt, const char *line_)
386 {
387   xmlTextWriterPtr writer = odt->content_wtr;
388
389   if (!strchr (line_, '\n'))
390     xmlTextWriterWriteString (writer, _xml(line_));
391   else
392     {
393       char *line = xstrdup (line_);
394       char *newline;
395       char *p;
396
397       for (p = line; *p; p = newline + 1)
398         {
399           newline = strchr (p, '\n');
400
401           if (!newline)
402             {
403               xmlTextWriterWriteString (writer, _xml(p));
404               free (line);
405               return;
406             }
407
408           if (newline > p && newline[-1] == '\r')
409             newline[-1] = '\0';
410           else
411             *newline = '\0';
412           xmlTextWriterWriteString (writer, _xml(p));
413           xmlTextWriterWriteElement (writer, _xml("text:line-break"), _xml(""));
414         }
415     }
416 }
417
418 static void
419 write_footnotes (struct odt_driver *odt,
420                  const struct pivot_table *pt,
421                  const size_t *footnote_indexes,
422                  size_t n_footnotes)
423 {
424   for (size_t i = 0; i < n_footnotes; i++)
425     {
426       const struct pivot_footnote *f = pt->footnotes[footnote_indexes[i]];
427       if (f->show)
428         {
429           xmlTextWriterStartElement (odt->content_wtr, _xml("text:span"));
430           xmlTextWriterWriteAttribute (odt->content_wtr,
431                                        _xml("text:style-name"),
432                                        _xml("superscript"));
433           char *s = pivot_footnote_marker_string (f, pt);
434           write_xml_with_line_breaks (odt, s);
435           free (s);
436           xmlTextWriterEndElement (odt->content_wtr);
437         }
438     }
439 }
440
441 static void
442 write_table_item_cell (struct odt_driver *odt,
443                        const struct pivot_table *pt,
444                        const struct table_cell *cell)
445 {
446   struct string body = DS_EMPTY_INITIALIZER;
447   pivot_value_format_body (cell->value, pt, &body);
448   xmlTextWriterWriteString (odt->content_wtr, _xml (ds_cstr (&body)));
449   ds_destroy (&body);
450
451   write_footnotes (odt, pt, cell->value->footnote_indexes,
452                    cell->value->n_footnotes);
453 }
454
455 static void
456 write_table__ (struct odt_driver *odt, const struct pivot_table *pt,
457                const struct table *t)
458 {
459   if (t)
460     {
461       for (size_t y = 0; y < t->n[V]; y++)
462         {
463           xmlTextWriterStartElement (odt->content_wtr, _xml("text:h"));
464           xmlTextWriterWriteFormatAttribute (odt->content_wtr,
465                                              _xml("text:outline-level"), "%d", 2);
466
467           struct table_cell cell;
468           table_get_cell (t, 0, y, &cell);
469           write_table_item_cell (odt, pt, &cell);
470
471           xmlTextWriterEndElement (odt->content_wtr);
472         }
473     }
474 }
475
476 static void
477 write_table_layer (struct odt_driver *odt, const struct pivot_table *pt,
478                    const size_t *layer_indexes)
479 {
480   struct table *title, *layers, *body, *caption, *footnotes;
481   pivot_output (pt, layer_indexes, true, &title, &layers, &body,
482                 &caption, &footnotes, NULL, NULL);
483
484   /* Write a heading for the table */
485   write_table__ (odt, pt, title);
486   write_table__ (odt, pt, layers);
487
488   /* Start table */
489   xmlTextWriterStartElement (odt->content_wtr, _xml("table:table"));
490   xmlTextWriterWriteFormatAttribute (odt->content_wtr, _xml("table:name"),
491                                      "TABLE-%d", odt->table_num++);
492
493
494   /* Start column definitions */
495   xmlTextWriterStartElement (odt->content_wtr, _xml("table:table-column"));
496   xmlTextWriterWriteFormatAttribute (odt->content_wtr, _xml("table:number-columns-repeated"), "%d", body->n[H]);
497   xmlTextWriterEndElement (odt->content_wtr);
498
499
500   /* Deal with row headers */
501   if (body->h[V][0] > 0)
502     xmlTextWriterStartElement (odt->content_wtr, _xml("table:table-header-rows"));
503
504
505   /* Write all the rows */
506   for (int r = 0 ; r < body->n[V]; ++r)
507     {
508       /* Start row definition */
509       xmlTextWriterStartElement (odt->content_wtr, _xml("table:table-row"));
510
511       /* Write all the columns */
512       for (int c = 0 ; c < body->n[H] ; ++c)
513         {
514           struct table_cell cell;
515
516           table_get_cell (body, c, r, &cell);
517
518           if (c == cell.d[H][0] && r == cell.d[V][0])
519             {
520               int colspan = table_cell_colspan (&cell);
521               int rowspan = table_cell_rowspan (&cell);
522
523               xmlTextWriterStartElement (odt->content_wtr, _xml("table:table-cell"));
524               xmlTextWriterWriteAttribute (odt->content_wtr, _xml("office:value-type"), _xml("string"));
525
526               if (colspan > 1)
527                 xmlTextWriterWriteFormatAttribute (
528                   odt->content_wtr, _xml("table:number-columns-spanned"),
529                   "%d", colspan);
530
531               if (rowspan > 1)
532                 xmlTextWriterWriteFormatAttribute (
533                   odt->content_wtr, _xml("table:number-rows-spanned"),
534                   "%d", rowspan);
535
536               xmlTextWriterStartElement (odt->content_wtr, _xml("text:p"));
537
538               if (r < body->h[V][0] || c < body->h[H][0])
539                 xmlTextWriterWriteAttribute (odt->content_wtr, _xml("text:style-name"), _xml("Table_20_Heading"));
540               else
541                 xmlTextWriterWriteAttribute (odt->content_wtr, _xml("text:style-name"), _xml("Table_20_Contents"));
542
543               write_table_item_cell (odt, pt, &cell);
544
545               xmlTextWriterEndElement (odt->content_wtr); /* text:p */
546               xmlTextWriterEndElement (odt->content_wtr); /* table:table-cell */
547             }
548           else
549             {
550               xmlTextWriterStartElement (odt->content_wtr, _xml("table:covered-table-cell"));
551               xmlTextWriterEndElement (odt->content_wtr);
552             }
553         }
554
555       xmlTextWriterEndElement (odt->content_wtr); /* row */
556
557       int ht = body->h[V][0];
558       if (ht > 0 && r == ht - 1)
559         xmlTextWriterEndElement (odt->content_wtr); /* table-header-rows */
560     }
561
562   xmlTextWriterEndElement (odt->content_wtr); /* table */
563
564   /* Write a caption for the table */
565   write_table__ (odt, pt, caption);
566   write_table__ (odt, pt, footnotes);
567
568   table_unref (title);
569   table_unref (layers);
570   table_unref (body);
571   table_unref (caption);
572   table_unref (footnotes);
573 }
574
575 static void
576 write_table (struct odt_driver *odt, const struct table_item *item)
577 {
578   size_t *layer_indexes;
579   PIVOT_OUTPUT_FOR_EACH_LAYER (layer_indexes, item->pt, true)
580     write_table_layer (odt, item->pt, layer_indexes);
581 }
582
583 static void
584 odt_output_text (struct odt_driver *odt, const char *text)
585 {
586   xmlTextWriterStartElement (odt->content_wtr, _xml("text:p"));
587   xmlTextWriterWriteString (odt->content_wtr, _xml(text));
588   xmlTextWriterEndElement (odt->content_wtr);
589 }
590
591 /* Submit a table to the ODT driver */
592 static void
593 odt_submit (struct output_driver *driver,
594             const struct output_item *output_item)
595 {
596   struct odt_driver *odt = odt_driver_cast (driver);
597
598   if (is_table_item (output_item))
599     write_table (odt, to_table_item (output_item));
600   else if (is_text_item (output_item))
601     {
602       char *text = text_item_get_plain_text (to_text_item (output_item));
603       odt_output_text (odt, text);
604       free (text);
605     }
606   else if (is_message_item (output_item))
607     {
608       const struct message_item *message_item = to_message_item (output_item);
609       char *s = msg_to_string (message_item_get_msg (message_item));
610       odt_output_text (odt, s);
611       free (s);
612     }
613 }
614
615 struct output_driver_factory odt_driver_factory =
616   { "odt", "pspp.odf", odt_create };
617
618 static const struct output_driver_class odt_driver_class =
619 {
620   "odf",
621   odt_destroy,
622   odt_submit,
623   NULL,
624 };