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