pivot-table: Allow all pivot_value formatting functions to use defaults.
[pspp] / src / output / output-item.c
1 /* PSPP - a program for statistical analysis.
2    Copyright (C) 2009, 2011 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 "output/output-item.h"
20
21 #include <assert.h>
22 #include <stdlib.h>
23
24 #include "libpspp/assertion.h"
25 #include "libpspp/cast.h"
26 #include "libpspp/message.h"
27 #include "libpspp/str.h"
28 #include "libpspp/zip-reader.h"
29 #include "output/chart.h"
30 #include "output/driver.h"
31 #include "output/pivot-table.h"
32
33 #include "gl/xalloc.h"
34
35 #include "gettext.h"
36 #define _(msgid) gettext (msgid)
37 #define N_(msgid) msgid
38 \f
39 const char *
40 output_item_type_to_string (enum output_item_type type)
41 {
42   switch (type)
43     {
44     case OUTPUT_ITEM_CHART: return "chart";
45     case OUTPUT_ITEM_GROUP: return "group";
46     case OUTPUT_ITEM_IMAGE: return "image";
47     case OUTPUT_ITEM_MESSAGE: return "message";
48     case OUTPUT_ITEM_PAGE_BREAK: return "page break";
49     case OUTPUT_ITEM_TABLE: return "table";
50     case OUTPUT_ITEM_TEXT: return "text";
51     }
52
53   NOT_REACHED ();
54 }
55 \f
56 #define OUTPUT_ITEM_INITIALIZER(TYPE) .type = TYPE, .ref_cnt = 1, .show = true
57
58 /* Increases ITEM's reference count, indicating that it has an additional
59    owner.  An output item that is shared among multiple owners must not be
60    modified. */
61 struct output_item *
62 output_item_ref (const struct output_item *item_)
63 {
64   struct output_item *item = CONST_CAST (struct output_item *, item_);
65   assert (item->ref_cnt > 0);
66   item->ref_cnt++;
67   return item;
68 }
69
70 /* Decreases ITEM's reference count, indicating that it has one fewer owner.
71    If ITEM no longer has any owners, it is freed. */
72 void
73 output_item_unref (struct output_item *item)
74 {
75   if (item != NULL)
76     {
77       assert (item->ref_cnt > 0);
78       if (--item->ref_cnt == 0)
79         {
80           switch (item->type)
81             {
82             case OUTPUT_ITEM_CHART:
83               chart_unref (item->chart);
84               break;
85
86             case OUTPUT_ITEM_GROUP:
87               for (size_t i = 0; i < item->group.n_children; i++)
88                 output_item_unref (item->group.children[i]);
89               free (item->group.children);
90               break;
91
92             case OUTPUT_ITEM_IMAGE:
93               cairo_surface_destroy (item->image);
94               break;
95
96             case OUTPUT_ITEM_MESSAGE:
97               msg_destroy (item->message);
98               break;
99
100             case OUTPUT_ITEM_PAGE_BREAK:
101               break;
102
103             case OUTPUT_ITEM_TABLE:
104               pivot_table_unref (item->table);
105               break;
106
107             case OUTPUT_ITEM_TEXT:
108               pivot_value_destroy (item->text.content);
109               break;
110             }
111
112           free (item->label);
113           free (item->command_name);
114           free (item->cached_label);
115           spv_info_destroy (item->spv_info);
116           free (item);
117         }
118     }
119 }
120
121 /* Returns true if ITEM has more than one owner.  An output item that is shared
122    among multiple owners must not be modified. */
123 bool
124 output_item_is_shared (const struct output_item *item)
125 {
126   return item->ref_cnt > 1;
127 }
128
129 /* Returns a clone of OLD, without initializing type-specific fields.  */
130 static struct output_item *
131 output_item_clone_common (const struct output_item *old)
132 {
133   struct output_item *new = xmalloc (sizeof *new);
134   *new = (struct output_item) {
135     .ref_cnt = 1,
136     .label = xstrdup_if_nonnull (old->label),
137     .command_name = xstrdup_if_nonnull (old->command_name),
138     .type = old->type,
139     .show = old->show,
140     .spv_info = spv_info_clone (old->spv_info),
141   };
142   return new;
143 }
144
145 struct output_item *
146 output_item_unshare (struct output_item *old)
147 {
148   assert (old->ref_cnt > 0);
149   if (!output_item_is_shared (old))
150     return old;
151   output_item_unref (old);
152
153   struct output_item *new = output_item_clone_common (old);
154   switch (old->type)
155     {
156     case OUTPUT_ITEM_CHART:
157       new->chart = chart_ref (old->chart);
158       break;
159
160     case OUTPUT_ITEM_GROUP:
161       new->group.children = xmemdup (
162         old->group.children,
163         old->group.n_children * sizeof *old->group.children);
164       new->group.n_children = new->group.allocated_children
165         = old->group.n_children;
166
167       for (size_t i = 0; i < new->group.n_children; i++)
168         new->group.children[i] = output_item_ref (new->group.children[i]);
169       break;
170
171     case OUTPUT_ITEM_IMAGE:
172       new->image = cairo_surface_reference (old->image);
173       break;
174
175     case OUTPUT_ITEM_MESSAGE:
176       new->message = msg_dup (old->message);
177       break;
178
179     case OUTPUT_ITEM_PAGE_BREAK:
180       break;
181
182     case OUTPUT_ITEM_TABLE:
183       new->table = pivot_table_ref (old->table);
184       break;
185
186     case OUTPUT_ITEM_TEXT:
187       new->text.subtype = old->text.subtype;
188       new->text.content = pivot_value_clone (old->text.content);
189       break;
190     }
191   return new;
192 }
193
194 void
195 output_item_submit (struct output_item *item)
196 {
197   output_submit (item);
198 }
199
200 /* If ROOT is a group item, submits each of its children, but not ROOT itself.
201    This is useful if ROOT is being used as a container for output items but it
202    has no significance itself.
203
204    If ROOT is not a group, submits it the normal way.
205
206    Takes ownership of ROOT. */
207 void
208 output_item_submit_children (struct output_item *root)
209 {
210   assert (!output_item_is_shared (root));
211   if (root->type == OUTPUT_ITEM_GROUP)
212     {
213       for (size_t i = 0; i < root->group.n_children; i++)
214         output_submit (root->group.children[i]);
215       root->group.n_children = 0;
216       output_item_unref (root);
217     }
218   else
219     output_submit (root);
220 }
221
222 /* Returns the label for ITEM, which the caller must not modify or free. */
223 const char *
224 output_item_get_label (const struct output_item *item)
225 {
226   if (item->label)
227     return item->label;
228
229   switch (item->type)
230     {
231     case OUTPUT_ITEM_CHART:
232       return item->chart->title ? item->chart->title : _("Chart");
233
234     case OUTPUT_ITEM_GROUP:
235       return item->command_name ? item->command_name : _("Group");
236
237     case OUTPUT_ITEM_IMAGE:
238       return "Image";
239
240     case OUTPUT_ITEM_MESSAGE:
241       return (item->message->severity == MSG_S_ERROR ? _("Error")
242               : item->message->severity == MSG_S_WARNING ? _("Warning")
243               : _("Note"));
244
245     case OUTPUT_ITEM_PAGE_BREAK:
246       return _("Page Break");
247
248     case OUTPUT_ITEM_TABLE:
249       if (!item->cached_label)
250         {
251           if (!item->table->title)
252             return _("Table");
253
254           struct output_item *item_rw = CONST_CAST (struct output_item *, item);
255           item_rw->cached_label = pivot_value_to_string (item->table->title,
256                                                          item->table);
257         }
258       return item->cached_label;
259
260     case OUTPUT_ITEM_TEXT:
261       return text_item_subtype_to_string (item->text.subtype);
262     }
263
264   NOT_REACHED ();
265 }
266
267 /* Sets the label for ITEM to LABEL.  The caller retains ownership of LABEL.
268    If LABEL is nonnull, it overrides any previously set label and the default
269    label.  If LABEL is null, ITEM will now use its default label.
270
271    ITEM must not be shared. */
272 void
273 output_item_set_label (struct output_item *item, const char *label)
274 {
275   output_item_set_label_nocopy (item, xstrdup_if_nonnull (label));
276 }
277
278 /* Sets the label for ITEM to LABEL, transferring ownership of LABEL to ITEM.
279    If LABEL is nonnull, it overrides any previously set label and the default
280    label.  If LABEL is null, ITEM will now use its default label.
281
282    ITEM must not be shared. */
283 void
284 output_item_set_label_nocopy (struct output_item *item, char *label)
285 {
286   assert (!output_item_is_shared (item));
287   free (item->label);
288   item->label = label;
289 }
290
291 void
292 output_item_set_command_name (struct output_item *item, const char *name)
293 {
294   output_item_set_command_name_nocopy (item, xstrdup_if_nonnull (name));
295 }
296
297 void
298 output_item_set_command_name_nocopy (struct output_item *item, char *name)
299 {
300   free (item->command_name);
301   item->command_name = name;
302 }
303
304 char *
305 output_item_get_subtype (const struct output_item *item)
306 {
307   return (item->type == OUTPUT_ITEM_TABLE
308           ? pivot_value_to_string (item->table->subtype, item->table)
309           : NULL);
310 }
311
312 void
313 output_item_add_spv_info (struct output_item *item)
314 {
315   assert (!output_item_is_shared (item));
316   if (!item->spv_info)
317     item->spv_info = xzalloc (sizeof *item->spv_info);
318 }
319 \f
320 static void
321 indent (int indentation)
322 {
323   for (int i = 0; i < indentation * 2; i++)
324     putchar (' ');
325 }
326
327 void
328 output_item_dump (const struct output_item *item, int indentation)
329 {
330   indent (indentation);
331   if (item->label)
332     printf ("label=\"%s\" ", item->label);
333   if (item->command_name)
334     printf ("command=\"%s\" ", item->command_name);
335   if (!item->show)
336     printf ("(%s) ", item->type == OUTPUT_ITEM_GROUP ? "collapsed" : "hidden");
337
338   switch (item->type)
339     {
340     case OUTPUT_ITEM_CHART:
341       printf ("chart \"%s\"\n", item->chart->title ? item->chart->title : "");
342       break;
343
344     case OUTPUT_ITEM_GROUP:
345       printf ("group\n");
346       for (size_t i = 0; i < item->group.n_children; i++)
347         output_item_dump (item->group.children[i], indentation + 1);
348       break;
349
350     case OUTPUT_ITEM_IMAGE:
351       printf ("image\n");
352       break;
353
354     case OUTPUT_ITEM_MESSAGE:
355       printf ("message\n");
356       break;
357
358     case OUTPUT_ITEM_PAGE_BREAK:
359       printf ("page break\n");
360       break;
361
362     case OUTPUT_ITEM_TABLE:
363       printf ("table\n");
364       pivot_table_dump (item->table, indentation + 1);
365       break;
366
367     case OUTPUT_ITEM_TEXT:
368       {
369         char *s = pivot_value_to_string (item->text.content, NULL);
370         printf ("text %s \"%s\"\n",
371                 text_item_subtype_to_string (item->text.subtype), s);
372         free (s);
373       }
374       break;
375     }
376 }
377 \f
378 void
379 output_iterator_init (struct output_iterator *iter,
380                       const struct output_item *item)
381 {
382   *iter = (struct output_iterator) OUTPUT_ITERATOR_INIT (item);
383 }
384
385 void
386 output_iterator_destroy (struct output_iterator *iter)
387 {
388   if (iter)
389     {
390       free (iter->nodes);
391       iter->nodes = NULL;
392       iter->n = iter->allocated = 0;
393     }
394 }
395
396 void
397 output_iterator_next (struct output_iterator *iter)
398 {
399   const struct output_item *cur = iter->cur;
400   if (cur)
401     {
402       if (cur->type == OUTPUT_ITEM_GROUP && cur->group.n_children > 0)
403         {
404           if (iter->n >= iter->allocated)
405             iter->nodes = x2nrealloc (iter->nodes, &iter->allocated,
406                                       sizeof *iter->nodes);
407           iter->nodes[iter->n++] = (struct output_iterator_node) {
408             .group = cur,
409             .idx = 0,
410           };
411           iter->cur = cur->group.children[0];
412           return;
413         }
414
415       for (; iter->n > 0; iter->n--)
416         {
417           struct output_iterator_node *node = &iter->nodes[iter->n - 1];
418           if (++node->idx < node->group->group.n_children)
419             {
420               iter->cur = node->group->group.children[node->idx];
421               return;
422             }
423         }
424
425       iter->cur = NULL;
426       output_iterator_destroy (iter);
427     }
428 }
429 \f
430 struct output_item *
431 chart_item_create (struct chart *chart)
432 {
433   struct output_item *item = xmalloc (sizeof *item);
434   *item = (struct output_item) {
435     OUTPUT_ITEM_INITIALIZER (OUTPUT_ITEM_CHART),
436     .chart = chart,
437   };
438   return item;
439 }
440 \f
441 struct output_item *
442 group_item_create (const char *command_name, const char *label)
443 {
444   return group_item_create_nocopy (
445     xstrdup_if_nonnull (command_name),
446     xstrdup_if_nonnull (label));
447 }
448
449 struct output_item *
450 group_item_create_nocopy (char *command_name, char *label)
451 {
452   struct output_item *item = xmalloc (sizeof *item);
453   *item = (struct output_item) {
454     OUTPUT_ITEM_INITIALIZER (OUTPUT_ITEM_GROUP),
455     .label = label,
456     .command_name = command_name,
457   };
458   return item;
459 }
460
461 /* Returns a new group item suitable as the root node of an output document.  A
462    root node is a group whose own properties are mostly disregarded.  Instead
463    of having root nodes, it would make just as much sense to just keep around
464    arrays of nodes that would serve as the top level of an output document, but
465    we'd need more special cases instead of just using the existing support for
466    group items. */
467 struct output_item *
468 root_item_create (void)
469 {
470   return group_item_create ("", _("Output"));
471 }
472
473 /* Returns a clone of OLD but without any of its children. */
474 struct output_item *
475 group_item_clone_empty (const struct output_item *old)
476 {
477   return output_item_clone_common (old);
478 }
479
480 /* Adds CHILD as a child of group item PARENT. */
481 void
482 group_item_add_child (struct output_item *parent, struct output_item *child)
483 {
484   assert (parent->type == OUTPUT_ITEM_GROUP);
485   assert (!output_item_is_shared (parent));
486   if (parent->group.n_children >= parent->group.allocated_children)
487     parent->group.children = x2nrealloc (parent->group.children,
488                                          &parent->group.allocated_children,
489                                          sizeof *parent->group.children);
490   parent->group.children[parent->group.n_children++] = child;
491 }
492 \f
493 /* Creates and returns a new output item containing IMAGE.  Takes ownership of
494    IMAGE. */
495 struct output_item *
496 image_item_create (cairo_surface_t *image)
497 {
498   struct output_item *item = xmalloc (sizeof *item);
499   *item = (struct output_item) {
500     OUTPUT_ITEM_INITIALIZER (OUTPUT_ITEM_IMAGE),
501     .image = image,
502   };
503   return item;
504 }
505 \f
506 struct output_item *
507 message_item_create (const struct msg *msg)
508 {
509   struct output_item *item = xmalloc (sizeof *item);
510   *item = (struct output_item) {
511     OUTPUT_ITEM_INITIALIZER (OUTPUT_ITEM_MESSAGE),
512     .message = msg_dup (msg),
513   };
514   return item;
515 }
516
517 const struct msg *
518 message_item_get_msg (const struct output_item *item)
519 {
520   assert (item->type == OUTPUT_ITEM_MESSAGE);
521   return item->message;
522 }
523
524 struct output_item *
525 message_item_to_text_item (struct output_item *message_item)
526 {
527   assert (message_item->type == OUTPUT_ITEM_MESSAGE);
528   struct output_item *text_item = text_item_create_nocopy (
529     TEXT_ITEM_LOG,
530     msg_to_string (message_item->message),
531     xstrdup (output_item_get_label (message_item)));
532   output_item_unref (message_item);
533   return text_item;
534 }
535 \f
536 struct output_item *
537 page_break_item_create (void)
538 {
539   struct output_item *item = xmalloc (sizeof *item);
540   *item = (struct output_item) {
541     OUTPUT_ITEM_INITIALIZER (OUTPUT_ITEM_PAGE_BREAK),
542   };
543   return item;
544 }
545 \f
546 /* Returns a new output_item for rendering TABLE.  Takes ownership of
547    TABLE. */
548 struct output_item *
549 table_item_create (struct pivot_table *table)
550 {
551   pivot_table_assign_label_depth (table);
552
553   struct output_item *item = xmalloc (sizeof *item);
554   *item = (struct output_item) {
555     OUTPUT_ITEM_INITIALIZER (OUTPUT_ITEM_TABLE),
556     .command_name = xstrdup_if_nonnull (table->command_c),
557     .table = table,
558   };
559   return item;
560 }
561 \f
562 /* Creates and returns a new text item containing TEXT and the specified
563    SUBTYPE and LABEL.  The new text item takes ownership of TEXT and LABEL.  If
564    LABEL is NULL, uses the default label for SUBTYPE. */
565 struct output_item *
566 text_item_create_nocopy (enum text_item_subtype subtype,
567                          char *text, char *label)
568 {
569   return text_item_create_value (subtype,
570                                  pivot_value_new_user_text_nocopy (text),
571                                  label);
572 }
573
574 /* Creates and returns a new text item containing a copy of TEXT and the
575    specified SUBTYPE and LABEL.  The caller retains ownership of TEXT and
576    LABEL.  If LABEL is null, uses a default label for SUBTYPE. */
577 struct output_item *
578 text_item_create (enum text_item_subtype subtype, const char *text,
579                   const char *label)
580 {
581   return text_item_create_nocopy (subtype, xstrdup (text),
582                                   xstrdup_if_nonnull (label));
583 }
584
585 /* Creates and returns a new text item containing VALUE, SUBTYPE, and LABEL.
586    Takes ownership of VALUE and LABEL.  If LABEL is null, uses a default label
587    for SUBTYPE. */
588 struct output_item *
589 text_item_create_value (enum text_item_subtype subtype,
590                         struct pivot_value *value, char *label)
591 {
592   if (subtype == TEXT_ITEM_SYNTAX || subtype == TEXT_ITEM_LOG)
593     {
594       struct pivot_value_ex *ex = pivot_value_ex_rw (value);
595       if (!ex->font_style)
596         {
597           ex->font_style = xmalloc (sizeof *value->ex->font_style);
598           *ex->font_style = (struct font_style) FONT_STYLE_INITIALIZER;
599         }
600
601       free (ex->font_style->typeface);
602       ex->font_style->typeface = xstrdup ("Monospaced");
603     }
604
605   struct output_item *item = XZALLOC (struct output_item);
606   *item = (struct output_item) {
607     OUTPUT_ITEM_INITIALIZER (OUTPUT_ITEM_TEXT),
608     .command_name = xstrdup_if_nonnull (output_get_command_name ()),
609     .label = label,
610     .text = { .subtype = subtype, .content = value },
611   };
612   return item;
613 }
614
615 /* Returns ITEM's subtype. */
616 enum text_item_subtype
617 text_item_get_subtype (const struct output_item *item)
618 {
619   assert (item->type == OUTPUT_ITEM_TEXT);
620   return item->text.subtype;
621 }
622
623 /* Returns ITEM's text, which the caller must eventually free. */
624 char *
625 text_item_get_plain_text (const struct output_item *item)
626 {
627   assert (item->type == OUTPUT_ITEM_TEXT);
628   return pivot_value_to_string (item->text.content, NULL);
629 }
630
631 static bool
632 nullable_font_style_equal (const struct font_style *a,
633                            const struct font_style *b)
634 {
635   return a && b ? font_style_equal (a, b) : !a && !b;
636 }
637
638 /* Attempts to append the text in SRC to DST.  If successful, returns true,
639    otherwise false.
640
641    Only TEXT_ITEM_SYNTAX and TEXT_ITEM_LOG items can be combined, and not with
642    each other.
643
644    DST must not be shared. */
645 bool
646 text_item_append (struct output_item *dst, const struct output_item *src)
647 {
648   assert (dst->type == OUTPUT_ITEM_TEXT);
649   assert (src->type == OUTPUT_ITEM_TEXT);
650   assert (!output_item_is_shared (dst));
651
652   enum text_item_subtype ds = dst->text.subtype;
653   enum text_item_subtype ss = src->text.subtype;
654
655   struct pivot_value *dc = dst->text.content;
656   const struct pivot_value *sc = src->text.content;
657
658   if (ds != ss
659       || (ds != TEXT_ITEM_SYNTAX && ds != TEXT_ITEM_LOG)
660       || strcmp (output_item_get_label (dst), output_item_get_label (src))
661       || !nullable_font_style_equal (dc->ex ? dc->ex->font_style : NULL,
662                                      sc->ex ? sc->ex->font_style : NULL)
663       || (dc->ex && dc->ex->font_style && dc->ex->font_style->markup)
664       || sc->type != PIVOT_VALUE_TEXT
665       || dc->type != PIVOT_VALUE_TEXT)
666     return false;
667   else
668     {
669       /* Calculate new text. */
670       char *new_text = xasprintf ("%s\n%s", dc->text.local, sc->text.local);
671
672       /* Free the old text. */
673       free (dc->text.local);
674       if (dc->text.c != dc->text.local)
675         free (dc->text.c);
676       if (dc->text.id != dc->text.local && dc->text.id != dc->text.c)
677         free (dc->text.id);
678
679       /* Put in new text. */
680       dc->text.local = new_text;
681       dc->text.c = new_text;
682       dc->text.id = new_text;
683
684       return true;
685     }
686 }
687
688 static const struct pivot_table_look *
689 text_item_table_look (void)
690 {
691   static struct pivot_table_look *look;
692   if (!look)
693     {
694       look = pivot_table_look_new_builtin_default ();
695
696       for (int a = 0; a < PIVOT_N_AREAS; a++)
697         memset (look->areas[a].cell_style.margin, 0,
698                 sizeof look->areas[a].cell_style.margin);
699       for (int b = 0; b < PIVOT_N_BORDERS; b++)
700         look->borders[b].stroke = TABLE_STROKE_NONE;
701     }
702   return look;
703 }
704
705 struct output_item *
706 text_item_to_table_item (struct output_item *text_item)
707 {
708   assert (text_item->type == OUTPUT_ITEM_TEXT);
709
710   /* Create a new table whose contents come from TEXT_ITEM. */
711   struct pivot_table *table = pivot_table_create__ (NULL, "Text");
712   pivot_table_set_look (table, text_item_table_look ());
713
714   struct pivot_dimension *d = pivot_dimension_create (
715     table, PIVOT_AXIS_ROW, N_("Text"));
716   d->hide_all_labels = true;
717   pivot_category_create_leaf (d->root, pivot_value_new_text ("null"));
718
719   pivot_table_put1 (table, 0, pivot_value_clone (text_item->text.content));
720
721   /* Free TEXT_ITEM. */
722   output_item_unref (text_item);
723
724   /* Return a new output item. */
725   return table_item_create (table);
726 }
727
728 const char *
729 text_item_subtype_to_string (enum text_item_subtype subtype)
730 {
731   switch (subtype)
732     {
733     case TEXT_ITEM_PAGE_TITLE:
734       return _("Page Title");
735
736     case TEXT_ITEM_TITLE:
737       return _("Title");
738
739     case TEXT_ITEM_SYNTAX:
740     case TEXT_ITEM_LOG:
741       return _("Log");
742
743     default:
744       return _("Text");
745     }
746 }
747 \f
748 void
749 spv_info_destroy (struct spv_info *spv_info)
750 {
751   if (spv_info)
752     {
753       zip_reader_unref (spv_info->zip_reader);
754       free (spv_info->structure_member);
755       free (spv_info->xml_member);
756       free (spv_info->bin_member);
757       free (spv_info->png_member);
758       free (spv_info);
759     }
760 }
761
762 struct spv_info *
763 spv_info_clone (const struct spv_info *old)
764 {
765   if (!old)
766     return NULL;
767
768   struct spv_info *new = xmalloc (sizeof *new);
769   *new = (struct spv_info) {
770     .zip_reader = old->zip_reader ? zip_reader_ref (old->zip_reader) : NULL,
771     .error = old->error,
772     .structure_member = xstrdup_if_nonnull (old->structure_member),
773     .xml_member = xstrdup_if_nonnull (old->xml_member),
774     .bin_member = xstrdup_if_nonnull (old->bin_member),
775     .png_member = xstrdup_if_nonnull (old->png_member),
776   };
777   return new;
778 }
779
780 size_t
781 spv_info_get_members (const struct spv_info *spv_info, const char **members,
782                       size_t allocated_members)
783 {
784   if (!spv_info)
785     return 0;
786
787   const char *s[] = {
788     spv_info->structure_member,
789     spv_info->xml_member,
790     spv_info->bin_member,
791     spv_info->png_member,
792   };
793   size_t n = 0;
794   for (size_t i = 0; i < sizeof s / sizeof *s; i++)
795     if (s[i] && n < allocated_members)
796       members[n++] = s[i];
797   return n;
798 }