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