Added options to the ODF driver.
[pspp] / src / output / odt.c
1 /* PSPP - a program for statistical analysis.
2    Copyright (C) 2009 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 "gettext.h"
20 #define _(msgid) gettext (msgid)
21
22 /* A driver for creating OpenDocument Format text files from PSPP's output */
23
24 #include <libpspp/assertion.h>
25 #include <libpspp/version.h>
26
27 #include <output/manager.h>
28 #include <output/output.h>
29 #include <output/table.h>
30
31 #include <time.h>
32 #include <pwd.h>
33 #include <sys/stat.h>
34 #include <sys/types.h>
35
36 #include <libgen.h>
37
38 #include <libxml/xmlwriter.h>
39
40 #include "xalloc.h"
41
42 #include "error.h"
43
44 #define _xml(X) (const xmlChar *)(X)
45
46
47 struct odf_driver_options
48 {
49   char *file_name;            /* Output file name. */
50   bool debug;
51 };
52
53
54 struct odt_driver_ext 
55 {
56   /* The name of the temporary directory used to construct the ODF */
57   char *dirname;
58
59   /* Writer for the content.xml file */
60   xmlTextWriterPtr content_wtr;
61
62   /* Writer fot the manifest.xml file */
63   xmlTextWriterPtr manifest_wtr;
64
65   struct odf_driver_options opts;
66 };
67
68
69
70 /* Create the "mimetype" file needed by ODF */
71 static void
72 create_mimetype (const char *dirname)
73 {
74   FILE *fp;
75   struct string filename;
76   ds_init_cstr (&filename, dirname);
77   ds_put_cstr (&filename, "/mimetype");
78   fp = fopen (ds_cstr (&filename), "w");
79   ds_destroy (&filename);
80
81   assert (fp);
82   fprintf (fp, "application/vnd.oasis.opendocument.text");
83   fclose (fp);
84 }
85
86 /* Create a new XML file called FILENAME in the temp directory, and return a writer for it */
87 static xmlTextWriterPtr
88 create_writer (const struct odt_driver_ext *driver, const char *filename)
89 {
90   char *copy = NULL;
91   xmlTextWriterPtr w;
92   struct string str;
93   ds_init_cstr (&str, driver->dirname);
94   ds_put_cstr (&str, "/");
95   ds_put_cstr (&str, filename);
96
97   /* dirname modifies its argument, so we must copy it */
98   copy = xstrdup (ds_cstr (&str));
99   mkdir (dirname (copy), 0700);
100   free (copy);
101
102   w = xmlNewTextWriterFilename (ds_cstr (&str), 0);
103
104   ds_destroy (&str);
105
106   xmlTextWriterStartDocument (w, NULL, "UTF-8", NULL);
107
108   return w;
109 }
110
111
112 static void
113 register_file (struct odt_driver_ext *x, const char *filename)
114 {
115   assert (x->manifest_wtr);
116   xmlTextWriterStartElement (x->manifest_wtr, _xml("manifest:file-entry"));
117   xmlTextWriterWriteAttribute (x->manifest_wtr, _xml("manifest:media-type"),  _xml("text/xml"));
118   xmlTextWriterWriteAttribute (x->manifest_wtr, _xml("manifest:full-path"),  _xml (filename));
119   xmlTextWriterEndElement (x->manifest_wtr);
120 }
121
122 static void
123 write_style_data (struct odt_driver_ext *x)
124 {
125   xmlTextWriterPtr w = create_writer (x, "styles.xml");
126   register_file (x, "styles.xml");
127
128   xmlTextWriterStartElement (w, _xml ("office:document-styles"));
129   xmlTextWriterWriteAttribute (w, _xml ("xmlns:office"),
130                                _xml ("urn:oasis:names:tc:opendocument:xmlns:office:1.0"));
131
132   xmlTextWriterWriteAttribute (w, _xml ("xmlns:style"),
133                                _xml ("urn:oasis:names:tc:opendocument:xmlns:style:1.0"));
134
135   xmlTextWriterWriteAttribute (w, _xml ("xmlns:fo"),
136                                _xml ("urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0") );
137
138   xmlTextWriterWriteAttribute (w, _xml ("office:version"),  _xml ("1.1"));
139                                
140
141
142   xmlTextWriterStartElement (w, _xml ("office:styles"));
143
144
145   {
146     xmlTextWriterStartElement (w, _xml ("style:style"));
147     xmlTextWriterWriteAttribute (w, _xml ("style:name"),
148                                  _xml ("Standard"));
149
150     xmlTextWriterWriteAttribute (w, _xml ("style:family"),
151                                  _xml ("paragraph"));
152
153     xmlTextWriterWriteAttribute (w, _xml ("style:class"),
154                                  _xml ("text"));
155
156     xmlTextWriterEndElement (w); /* style:style */
157   }
158
159   {
160     xmlTextWriterStartElement (w, _xml ("style:style"));
161     xmlTextWriterWriteAttribute (w, _xml ("style:name"),
162                                  _xml ("Table_20_Contents"));
163
164     xmlTextWriterWriteAttribute (w, _xml ("style:display-name"),
165                                  _xml ("Table Contents"));
166
167     xmlTextWriterWriteAttribute (w, _xml ("style:family"),
168                                  _xml ("paragraph"));
169
170     xmlTextWriterWriteAttribute (w, _xml ("style:parent-style-name"),
171                                  _xml ("Standard"));
172
173     xmlTextWriterWriteAttribute (w, _xml ("style:class"),
174                                  _xml ("extra"));
175
176     xmlTextWriterEndElement (w); /* style:style */
177   }
178
179   {
180     xmlTextWriterStartElement (w, _xml ("style:style"));
181     xmlTextWriterWriteAttribute (w, _xml ("style:name"),
182                                  _xml ("Table_20_Heading"));
183
184     xmlTextWriterWriteAttribute (w, _xml ("style:display-name"),
185                                  _xml ("Table Heading"));
186
187     xmlTextWriterWriteAttribute (w, _xml ("style:family"),
188                                  _xml ("paragraph"));
189
190     xmlTextWriterWriteAttribute (w, _xml ("style:parent-style-name"),
191                                  _xml ("Table_20_Contents"));
192
193     xmlTextWriterWriteAttribute (w, _xml ("style:class"),
194                                  _xml ("extra"));
195
196
197     xmlTextWriterStartElement (w, _xml ("style:text-properties"));
198     xmlTextWriterWriteAttribute (w, _xml ("fo:font-weight"), _xml ("bold"));
199     xmlTextWriterWriteAttribute (w, _xml ("style:font-weight-asian"), _xml ("bold"));
200     xmlTextWriterWriteAttribute (w, _xml ("style:font-weight-complex"), _xml ("bold"));
201     xmlTextWriterEndElement (w); /* style:text-properties */
202
203     xmlTextWriterEndElement (w); /* style:style */
204   }
205
206
207   xmlTextWriterEndElement (w); /* office:styles */
208   xmlTextWriterEndElement (w); /* office:document-styles */
209
210   xmlTextWriterEndDocument (w);
211   xmlFreeTextWriter (w);
212 }
213
214 static void
215 write_meta_data (struct odt_driver_ext *x)
216 {
217   xmlTextWriterPtr w = create_writer (x, "meta.xml");
218   register_file (x, "meta.xml");
219
220   xmlTextWriterStartElement (w, _xml ("office:document-meta"));
221   xmlTextWriterWriteAttribute (w, _xml ("xmlns:office"), _xml ("urn:oasis:names:tc:opendocument:xmlns:office:1.0"));
222   xmlTextWriterWriteAttribute (w, _xml ("xmlns:dc"),  _xml ("http://purl.org/dc/elements/1.1/"));
223   xmlTextWriterWriteAttribute (w, _xml ("xmlns:meta"), _xml ("urn:oasis:names:tc:opendocument:xmlns:meta:1.0"));
224   xmlTextWriterWriteAttribute (w, _xml ("xmlns:ooo"), _xml("http://openoffice.org/2004/office"));
225   xmlTextWriterWriteAttribute (w, _xml ("office:version"),  _xml("1.1"));
226
227   xmlTextWriterStartElement (w, _xml ("office:meta"));
228   {
229     xmlTextWriterStartElement (w, _xml ("meta:generator"));
230     xmlTextWriterWriteString (w, _xml (stat_version));
231     xmlTextWriterEndElement (w);
232   }
233
234
235   {
236     char buf[30];
237     struct passwd *pw = getpwuid (getuid ());
238     time_t t = time (NULL);
239     struct tm *tm =  localtime (&t);
240
241     strftime (buf, 30, "%Y-%m-%dT%H:%M:%S", tm);
242
243     xmlTextWriterStartElement (w, _xml ("meta:initial-creator"));
244     xmlTextWriterWriteString (w, _xml (strtok (pw->pw_gecos, ",")));
245     xmlTextWriterEndElement (w);
246
247     xmlTextWriterStartElement (w, _xml ("meta:creation-date"));
248     xmlTextWriterWriteString (w, _xml (buf));
249     xmlTextWriterEndElement (w);
250
251     xmlTextWriterStartElement (w, _xml ("dc:creator"));
252     xmlTextWriterWriteString (w, _xml (strtok (pw->pw_gecos, ",")));
253
254     xmlTextWriterEndElement (w);
255
256     xmlTextWriterStartElement (w, _xml ("dc:date"));
257     xmlTextWriterWriteString (w, _xml (buf));
258     xmlTextWriterEndElement (w);
259   }
260
261   xmlTextWriterEndElement (w);
262   xmlTextWriterEndElement (w);
263   xmlTextWriterEndDocument (w);
264   xmlFreeTextWriter (w);
265 }
266
267 enum
268 {
269   output_file_arg,
270   boolean_arg,
271 };
272
273 static const struct outp_option option_tab[] =
274 {
275   {"output-file",               output_file_arg,0},
276
277   {"debug",                     boolean_arg,    1},
278
279   {NULL, 0, 0},
280 };
281
282 static bool
283 handle_option (void *options_, const char *key, const struct string *val)
284 {
285   struct odf_driver_options *options = options_;
286   int subcat;
287   char *value = ds_cstr (val);
288
289   switch (outp_match_keyword (key, option_tab, &subcat))
290     {
291     case -1:
292       error (0, 0,
293              _("unknown configuration parameter `%s' for ODF device "
294                "driver"), key);
295       break;
296     case output_file_arg:
297       free (options->file_name);
298       options->file_name = xstrdup (value);
299       break;
300     case boolean_arg:
301       if (!strcmp (value, "on") || !strcmp (value, "true")
302           || !strcmp (value, "yes") || atoi (value))
303         options->debug = true;
304       else if (!strcmp (value, "off") || !strcmp (value, "false")
305                || !strcmp (value, "no") || !strcmp (value, "0"))
306         options->debug = false;
307       else
308         {
309           error (0, 0, _("boolean value expected for %s"), key);
310           return false;
311         }
312       break;
313
314     default:
315       NOT_REACHED ();
316     }
317
318   return true;
319 }
320
321
322 static bool
323 odt_open_driver (const char *name, int types, struct substring option_string)
324 {
325   struct odt_driver_ext *x;
326   struct outp_driver *this = outp_allocate_driver (&odt_class, name, types);
327
328   this->ext = x = xmalloc (sizeof *x);
329
330   x->opts.file_name = xstrdup ("pspp.pdt");
331   x->opts.debug = false;
332
333   outp_parse_options (this->name, option_string, handle_option, &x->opts);
334
335   outp_register_driver (this);
336
337   x->dirname = xstrdup ("odt-XXXXXX");
338   mkdtemp (x->dirname);
339
340   create_mimetype (x->dirname);
341
342   /* Create the manifest */
343   x->manifest_wtr = create_writer (x, "META-INF/manifest.xml");
344
345   xmlTextWriterStartElement (x->manifest_wtr, _xml("manifest:manifest"));
346   xmlTextWriterWriteAttribute (x->manifest_wtr, _xml("xmlns:manifest"),
347                                _xml("urn:oasis:names:tc:opendocument:xmlns:manifest:1.0"));
348
349
350   /* Add a manifest entry for the document as a whole */
351   xmlTextWriterStartElement (x->manifest_wtr, _xml("manifest:file-entry"));
352   xmlTextWriterWriteAttribute (x->manifest_wtr, _xml("manifest:media-type"),  _xml("application/vnd.oasis.opendocument.text"));
353   xmlTextWriterWriteAttribute (x->manifest_wtr, _xml("manifest:full-path"),  _xml("/"));
354   xmlTextWriterEndElement (x->manifest_wtr);
355
356
357   write_meta_data (x);
358   write_style_data (x);
359
360   x->content_wtr = create_writer (x, "content.xml");
361   register_file (x, "content.xml");
362
363
364   /* Some necessary junk at the start */
365   xmlTextWriterStartElement (x->content_wtr, _xml("office:document-content"));
366   xmlTextWriterWriteAttribute (x->content_wtr, _xml("xmlns:office"),
367                                _xml("urn:oasis:names:tc:opendocument:xmlns:office:1.0"));
368
369   xmlTextWriterWriteAttribute (x->content_wtr, _xml("xmlns:text"),
370                                _xml("urn:oasis:names:tc:opendocument:xmlns:text:1.0"));
371
372   xmlTextWriterWriteAttribute (x->content_wtr, _xml("xmlns:table"),
373                                _xml("urn:oasis:names:tc:opendocument:xmlns:table:1.0"));
374
375   xmlTextWriterWriteAttribute (x->content_wtr, _xml("office:version"), _xml("1.1"));
376
377   xmlTextWriterStartElement (x->content_wtr, _xml("office:body"));
378   xmlTextWriterStartElement (x->content_wtr, _xml("office:text"));
379
380
381
382   /* Close the manifest */
383   xmlTextWriterEndElement (x->manifest_wtr);
384   xmlTextWriterEndDocument (x->manifest_wtr);
385   xmlFreeTextWriter (x->manifest_wtr);
386
387   return true;
388 }
389
390 static bool
391 odt_close_driver (struct outp_driver *this)
392 {
393   struct string zip_cmd;
394   struct string rm_cmd;
395   struct odt_driver_ext *x = this->ext;
396
397   xmlTextWriterEndElement (x->content_wtr); /* office:text */
398   xmlTextWriterEndElement (x->content_wtr); /* office:body */
399   xmlTextWriterEndElement (x->content_wtr); /* office:document-content */
400
401   xmlTextWriterEndDocument (x->content_wtr);
402   xmlFreeTextWriter (x->content_wtr);
403
404   /* Zip up the directory */
405   ds_init_empty (&zip_cmd);
406   ds_put_format (&zip_cmd,
407                  "cd %s ; rm -f ../%s; zip -q -X ../%s mimetype; zip -q -X -u -r ../pspp.odt .",
408                  x->dirname, x->opts.file_name, x->opts.file_name);
409   system (ds_cstr (&zip_cmd));
410   ds_destroy (&zip_cmd);
411
412
413   if ( !x->opts.debug )
414     {
415       /* Remove the temp dir */
416       ds_init_empty (&rm_cmd);
417       ds_put_format (&rm_cmd, "rm -r %s", x->dirname);
418       system (ds_cstr (&rm_cmd));
419       ds_destroy (&rm_cmd);
420     }
421   else
422     fprintf (stderr, "Not removing directory %s\n", x->dirname);
423
424   free (x->dirname);
425   free (x);
426
427   return true;
428 }
429
430 static void
431 odt_open_page (struct outp_driver *this)
432 {
433 }
434
435 static void
436 odt_close_page (struct outp_driver *this)
437 {
438 }
439
440 static void
441 odt_output_chart (struct outp_driver *this, const struct chart *chart)
442 {
443  printf ("%s\n", __FUNCTION__);
444 }
445
446
447 /* Submit a table to the ODT driver */
448 static void
449 odt_submit (struct outp_driver *this, struct som_entity *e)
450 {
451   int r, c;
452   
453   struct odt_driver_ext *x = this->ext;
454   struct tab_table *tab = e->ext;
455
456
457   /* Write a heading for the table */
458   xmlTextWriterStartElement (x->content_wtr, _xml("text:h"));
459   xmlTextWriterWriteFormatAttribute (x->content_wtr, _xml("text:level"), "%d", e->subtable_num == 1 ? 2 : 3);
460   xmlTextWriterWriteString (x->content_wtr, _xml (tab->title) );
461   xmlTextWriterEndElement (x->content_wtr);
462
463   /* Start table */
464   xmlTextWriterStartElement (x->content_wtr, _xml("table:table"));
465   xmlTextWriterWriteFormatAttribute (x->content_wtr, _xml("table:name"), 
466                                      "TABLE-%d.%d", e->table_num, e->subtable_num);
467
468
469   /* Start column definitions */
470   xmlTextWriterStartElement (x->content_wtr, _xml("table:table-column"));
471   xmlTextWriterWriteFormatAttribute (x->content_wtr, _xml("table:number-columns-repeated"), "%d", tab->nc);
472   xmlTextWriterEndElement (x->content_wtr);
473
474
475   /* Deal with row headers */
476   if ( tab->t > 0)
477     xmlTextWriterStartElement (x->content_wtr, _xml("table:table-header-rows"));
478     
479
480   /* Write all the rows */
481   for (r = 0 ; r < tab->nr; ++r)
482     {
483       int spanned_columns = 0;
484       /* Start row definition */
485       xmlTextWriterStartElement (x->content_wtr, _xml("table:table-row"));
486
487       /* Write all the columns */
488       for (c = 0 ; c < tab->nc ; ++c)
489         {
490           char *s = NULL;
491           unsigned int opts = tab->ct[tab->nc * r + c];
492           struct substring ss = tab->cc[tab->nc * r + c];
493
494           if (opts & TAB_EMPTY)
495             {
496               xmlTextWriterStartElement (x->content_wtr, _xml("table:table-cell"));
497               xmlTextWriterEndElement (x->content_wtr);
498               continue;
499             }
500
501           if ( opts & TAB_JOIN)
502             {
503               if ( spanned_columns == 0)
504                 {
505                   struct tab_joined_cell *j = (struct tab_joined_cell*) ss_data (ss);
506                   s = ss_xstrdup (j->contents);
507                 }
508             }
509           else
510             s = ss_xstrdup (ss);
511
512           if ( spanned_columns == 0 )
513             {
514               xmlTextWriterStartElement (x->content_wtr, _xml("table:table-cell"));
515               xmlTextWriterWriteAttribute (x->content_wtr, _xml("office:value-type"), _xml("string"));
516
517               if ( opts & TAB_JOIN )
518                 {
519                   struct tab_joined_cell *j = (struct tab_joined_cell*) ss_data (ss);
520                   spanned_columns = j->x2 - j->x1;
521
522                   xmlTextWriterWriteFormatAttribute (x->content_wtr,
523                                                      _xml("table:number-columns-spanned"),
524                                                      "%d", spanned_columns);
525                 }
526
527               xmlTextWriterStartElement (x->content_wtr, _xml("text:p"));
528
529               if ( r < tab->t || c < tab->l )
530                 xmlTextWriterWriteAttribute (x->content_wtr, _xml("text:style-name"), _xml("Table_20_Heading"));
531               else
532                 xmlTextWriterWriteAttribute (x->content_wtr, _xml("text:style-name"), _xml("Table_20_Contents"));
533
534               xmlTextWriterWriteString (x->content_wtr, _xml (s));
535           
536               xmlTextWriterEndElement (x->content_wtr); /* text:p */
537               xmlTextWriterEndElement (x->content_wtr); /* table:table-cell */
538             }
539           else
540             {
541               xmlTextWriterStartElement (x->content_wtr, _xml("table:covered-table-cell"));
542               xmlTextWriterEndElement (x->content_wtr);
543             }
544           if ( opts & TAB_JOIN )
545             spanned_columns --;
546
547           free (s);
548         }
549   
550       xmlTextWriterEndElement (x->content_wtr); /* row */
551
552       if ( tab->t > 0 && r == tab->t - 1)
553         xmlTextWriterEndElement (x->content_wtr); /* table-header-rows */
554     }
555
556   xmlTextWriterEndElement (x->content_wtr); /* table */
557 }
558
559
560 /* ODT driver class. */
561 const struct outp_class odt_class =
562 {
563   "odf",
564   1,
565
566   odt_open_driver,
567   odt_close_driver,
568
569   odt_open_page,
570   odt_close_page,
571   NULL,
572
573   odt_output_chart,
574   odt_submit,
575
576   NULL,
577   NULL,
578   NULL,
579 };