0c60b8dc269f39799e2aeeff41825084b318bc97
[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       pivot_table_dump (item->table, indentation + 1);
364       break;
365
366     case OUTPUT_ITEM_TEXT:
367       printf ("text %s \"%s\"\n",
368               text_item_subtype_to_string (item->text.subtype),
369               pivot_value_to_string_defaults (item->text.content));
370       break;
371     }
372 }
373 \f
374 void
375 output_iterator_init (struct output_iterator *iter,
376                       const struct output_item *item)
377 {
378   *iter = (struct output_iterator) OUTPUT_ITERATOR_INIT (item);
379 }
380
381 void
382 output_iterator_destroy (struct output_iterator *iter)
383 {
384   if (iter)
385     {
386       free (iter->nodes);
387       iter->nodes = NULL;
388       iter->n = iter->allocated = 0;
389     }
390 }
391
392 void
393 output_iterator_next (struct output_iterator *iter)
394 {
395   const struct output_item *cur = iter->cur;
396   if (cur)
397     {
398       if (cur->type == OUTPUT_ITEM_GROUP && cur->group.n_children > 0)
399         {
400           if (iter->n >= iter->allocated)
401             iter->nodes = x2nrealloc (iter->nodes, &iter->allocated,
402                                       sizeof *iter->nodes);
403           iter->nodes[iter->n++] = (struct output_iterator_node) {
404             .group = cur,
405             .idx = 0,
406           };
407           iter->cur = cur->group.children[0];
408           return;
409         }
410
411       for (; iter->n > 0; iter->n--)
412         {
413           struct output_iterator_node *node = &iter->nodes[iter->n - 1];
414           if (++node->idx < node->group->group.n_children)
415             {
416               iter->cur = node->group->group.children[node->idx];
417               return;
418             }
419         }
420
421       iter->cur = NULL;
422       output_iterator_destroy (iter);
423     }
424 }
425 \f
426 struct output_item *
427 chart_item_create (struct chart *chart)
428 {
429   struct output_item *item = xmalloc (sizeof *item);
430   *item = (struct output_item) {
431     OUTPUT_ITEM_INITIALIZER (OUTPUT_ITEM_CHART),
432     .chart = chart,
433   };
434   return item;
435 }
436 \f
437 struct output_item *
438 group_item_create (const char *command_name, const char *label)
439 {
440   return group_item_create_nocopy (
441     xstrdup_if_nonnull (command_name),
442     xstrdup_if_nonnull (label));
443 }
444
445 struct output_item *
446 group_item_create_nocopy (char *command_name, char *label)
447 {
448   struct output_item *item = xmalloc (sizeof *item);
449   *item = (struct output_item) {
450     OUTPUT_ITEM_INITIALIZER (OUTPUT_ITEM_GROUP),
451     .label = label,
452     .command_name = command_name,
453   };
454   return item;
455 }
456
457 /* Returns a new group item suitable as the root node of an output document.  A
458    root node is a group whose own properties are mostly disregarded.  Instead
459    of having root nodes, it would make just as much sense to just keep around
460    arrays of nodes that would serve as the top level of an output document, but
461    we'd need more special cases instead of just using the existing support for
462    group items. */
463 struct output_item *
464 root_item_create (void)
465 {
466   return group_item_create ("", _("Output"));
467 }
468
469 /* Returns a clone of OLD but without any of its children. */
470 struct output_item *
471 group_item_clone_empty (const struct output_item *old)
472 {
473   return output_item_clone_common (old);
474 }
475
476 /* Adds CHILD as a child of group item PARENT. */
477 void
478 group_item_add_child (struct output_item *parent, struct output_item *child)
479 {
480   assert (parent->type == OUTPUT_ITEM_GROUP);
481   assert (!output_item_is_shared (parent));
482   if (parent->group.n_children >= parent->group.allocated_children)
483     parent->group.children = x2nrealloc (parent->group.children,
484                                          &parent->group.allocated_children,
485                                          sizeof *parent->group.children);
486   parent->group.children[parent->group.n_children++] = child;
487 }
488 \f
489 /* Creates and returns a new output item containing IMAGE.  Takes ownership of
490    IMAGE. */
491 struct output_item *
492 image_item_create (cairo_surface_t *image)
493 {
494   struct output_item *item = xmalloc (sizeof *item);
495   *item = (struct output_item) {
496     OUTPUT_ITEM_INITIALIZER (OUTPUT_ITEM_IMAGE),
497     .image = image,
498   };
499   return item;
500 }
501 \f
502 struct output_item *
503 message_item_create (const struct msg *msg)
504 {
505   struct output_item *item = xmalloc (sizeof *item);
506   *item = (struct output_item) {
507     OUTPUT_ITEM_INITIALIZER (OUTPUT_ITEM_MESSAGE),
508     .message = msg_dup (msg),
509   };
510   return item;
511 }
512
513 const struct msg *
514 message_item_get_msg (const struct output_item *item)
515 {
516   assert (item->type == OUTPUT_ITEM_MESSAGE);
517   return item->message;
518 }
519
520 struct output_item *
521 message_item_to_text_item (struct output_item *message_item)
522 {
523   assert (message_item->type == OUTPUT_ITEM_MESSAGE);
524   struct output_item *text_item = text_item_create_nocopy (
525     TEXT_ITEM_LOG,
526     msg_to_string (message_item->message),
527     xstrdup (output_item_get_label (message_item)));
528   output_item_unref (message_item);
529   return text_item;
530 }
531 \f
532 struct output_item *
533 page_break_item_create (void)
534 {
535   struct output_item *item = xmalloc (sizeof *item);
536   *item = (struct output_item) {
537     OUTPUT_ITEM_INITIALIZER (OUTPUT_ITEM_PAGE_BREAK),
538   };
539   return item;
540 }
541 \f
542 /* Returns a new output_item for rendering TABLE.  Takes ownership of
543    TABLE. */
544 struct output_item *
545 table_item_create (struct pivot_table *table)
546 {
547   pivot_table_assign_label_depth (table);
548
549   struct output_item *item = xmalloc (sizeof *item);
550   *item = (struct output_item) {
551     OUTPUT_ITEM_INITIALIZER (OUTPUT_ITEM_TABLE),
552     .command_name = xstrdup_if_nonnull (table->command_c),
553     .table = table,
554   };
555   return item;
556 }
557 \f
558 /* Creates and returns a new text item containing TEXT and the specified
559    SUBTYPE and LABEL.  The new text item takes ownership of TEXT and LABEL.  If
560    LABEL is NULL, uses the default label for SUBTYPE. */
561 struct output_item *
562 text_item_create_nocopy (enum text_item_subtype subtype,
563                          char *text, char *label)
564 {
565   return text_item_create_value (subtype,
566                                  pivot_value_new_user_text_nocopy (text),
567                                  label);
568 }
569
570 /* Creates and returns a new text item containing a copy of TEXT and the
571    specified SUBTYPE and LABEL.  The caller retains ownership of TEXT and
572    LABEL.  If LABEL is null, uses a default label for SUBTYPE. */
573 struct output_item *
574 text_item_create (enum text_item_subtype subtype, const char *text,
575                   const char *label)
576 {
577   return text_item_create_nocopy (subtype, xstrdup (text),
578                                   xstrdup_if_nonnull (label));
579 }
580
581 /* Creates and returns a new text item containing VALUE, SUBTYPE, and LABEL.
582    Takes ownership of VALUE and LABEL.  If LABEL is null, uses a default label
583    for SUBTYPE. */
584 struct output_item *
585 text_item_create_value (enum text_item_subtype subtype,
586                         struct pivot_value *value, char *label)
587 {
588   if (subtype == TEXT_ITEM_SYNTAX || subtype == TEXT_ITEM_LOG)
589     {
590       struct pivot_value_ex *ex = pivot_value_ex_rw (value);
591       if (!ex->font_style)
592         {
593           ex->font_style = xmalloc (sizeof *value->ex->font_style);
594           *ex->font_style = (struct font_style) FONT_STYLE_INITIALIZER;
595         }
596
597       free (ex->font_style->typeface);
598       ex->font_style->typeface = xstrdup ("Monospaced");
599     }
600
601   struct output_item *item = XZALLOC (struct output_item);
602   *item = (struct output_item) {
603     OUTPUT_ITEM_INITIALIZER (OUTPUT_ITEM_TEXT),
604     .command_name = xstrdup_if_nonnull (output_get_command_name ()),
605     .label = label,
606     .text = { .subtype = subtype, .content = value },
607   };
608   return item;
609 }
610
611 /* Returns ITEM's subtype. */
612 enum text_item_subtype
613 text_item_get_subtype (const struct output_item *item)
614 {
615   assert (item->type == OUTPUT_ITEM_TEXT);
616   return item->text.subtype;
617 }
618
619 /* Returns ITEM's text, which the caller must eventually free. */
620 char *
621 text_item_get_plain_text (const struct output_item *item)
622 {
623   assert (item->type == OUTPUT_ITEM_TEXT);
624   return pivot_value_to_string_defaults (item->text.content);
625 }
626
627 static bool
628 nullable_font_style_equal (const struct font_style *a,
629                            const struct font_style *b)
630 {
631   return a && b ? font_style_equal (a, b) : !a && !b;
632 }
633
634 /* Attempts to append the text in SRC to DST.  If successful, returns true,
635    otherwise false.
636
637    Only TEXT_ITEM_SYNTAX and TEXT_ITEM_LOG items can be combined, and not with
638    each other.
639
640    DST must not be shared. */
641 bool
642 text_item_append (struct output_item *dst, const struct output_item *src)
643 {
644   assert (dst->type == OUTPUT_ITEM_TEXT);
645   assert (src->type == OUTPUT_ITEM_TEXT);
646   assert (!output_item_is_shared (dst));
647
648   enum text_item_subtype ds = dst->text.subtype;
649   enum text_item_subtype ss = src->text.subtype;
650
651   struct pivot_value *dc = dst->text.content;
652   const struct pivot_value *sc = src->text.content;
653
654   if (ds != ss
655       || (ds != TEXT_ITEM_SYNTAX && ds != TEXT_ITEM_LOG)
656       || strcmp (output_item_get_label (dst), output_item_get_label (src))
657       || !nullable_font_style_equal (dc->ex ? dc->ex->font_style : NULL,
658                                      sc->ex ? sc->ex->font_style : NULL)
659       || (dc->ex && dc->ex->font_style && dc->ex->font_style->markup)
660       || sc->type != PIVOT_VALUE_TEXT
661       || dc->type != PIVOT_VALUE_TEXT)
662     return false;
663   else
664     {
665       /* Calculate new text. */
666       char *new_text = xasprintf ("%s\n%s", dc->text.local, sc->text.local);
667
668       /* Free the old text. */
669       free (dc->text.local);
670       if (dc->text.c != dc->text.local)
671         free (dc->text.c);
672       if (dc->text.id != dc->text.local && dc->text.id != dc->text.c)
673         free (dc->text.id);
674
675       /* Put in new text. */
676       dc->text.local = new_text;
677       dc->text.c = new_text;
678       dc->text.id = new_text;
679
680       return true;
681     }
682 }
683
684 static const struct pivot_table_look *
685 text_item_table_look (void)
686 {
687   static struct pivot_table_look *look;
688   if (!look)
689     {
690       look = pivot_table_look_new_builtin_default ();
691
692       for (int a = 0; a < PIVOT_N_AREAS; a++)
693         memset (look->areas[a].cell_style.margin, 0,
694                 sizeof look->areas[a].cell_style.margin);
695       for (int b = 0; b < PIVOT_N_BORDERS; b++)
696         look->borders[b].stroke = TABLE_STROKE_NONE;
697     }
698   return look;
699 }
700
701 struct output_item *
702 text_item_to_table_item (struct output_item *text_item)
703 {
704   assert (text_item->type == OUTPUT_ITEM_TEXT);
705
706   /* Create a new table whose contents come from TEXT_ITEM. */
707   struct pivot_table *table = pivot_table_create__ (NULL, "Text");
708   pivot_table_set_look (table, text_item_table_look ());
709
710   struct pivot_dimension *d = pivot_dimension_create (
711     table, PIVOT_AXIS_ROW, N_("Text"));
712   d->hide_all_labels = true;
713   pivot_category_create_leaf (d->root, pivot_value_new_text ("null"));
714
715   pivot_table_put1 (table, 0, pivot_value_clone (text_item->text.content));
716
717   /* Free TEXT_ITEM. */
718   output_item_unref (text_item);
719
720   /* Return a new output item. */
721   return table_item_create (table);
722 }
723
724 const char *
725 text_item_subtype_to_string (enum text_item_subtype subtype)
726 {
727   switch (subtype)
728     {
729     case TEXT_ITEM_PAGE_TITLE:
730       return _("Page Title");
731
732     case TEXT_ITEM_TITLE:
733       return _("Title");
734
735     case TEXT_ITEM_SYNTAX:
736     case TEXT_ITEM_LOG:
737       return _("Log");
738
739     default:
740       return _("Text");
741     }
742 }
743 \f
744 void
745 spv_info_destroy (struct spv_info *spv_info)
746 {
747   if (spv_info)
748     {
749       zip_reader_unref (spv_info->zip_reader);
750       free (spv_info->structure_member);
751       free (spv_info->xml_member);
752       free (spv_info->bin_member);
753       free (spv_info->png_member);
754       free (spv_info);
755     }
756 }
757
758 struct spv_info *
759 spv_info_clone (const struct spv_info *old)
760 {
761   if (!old)
762     return NULL;
763
764   struct spv_info *new = xmalloc (sizeof *new);
765   *new = (struct spv_info) {
766     .zip_reader = old->zip_reader ? zip_reader_ref (old->zip_reader) : NULL,
767     .error = old->error,
768     .structure_member = xstrdup_if_nonnull (old->structure_member),
769     .xml_member = xstrdup_if_nonnull (old->xml_member),
770     .bin_member = xstrdup_if_nonnull (old->bin_member),
771     .png_member = xstrdup_if_nonnull (old->png_member),
772   };
773   return new;
774 }
775
776 size_t
777 spv_info_get_members (const struct spv_info *spv_info, const char **members,
778                       size_t allocated_members)
779 {
780   if (!spv_info)
781     return 0;
782
783   const char *s[] = {
784     spv_info->structure_member,
785     spv_info->xml_member,
786     spv_info->bin_member,
787     spv_info->png_member,
788   };
789   size_t n = 0;
790   for (size_t i = 0; i < sizeof s / sizeof *s; i++)
791     if (s[i] && n < allocated_members)
792       members[n++] = s[i];
793   return n;
794 }