improve scale summary test
[pspp] / utilities / pspp-output.c
index 8362ac57ff252ef78392c2c29fd33b809f52fbdd..70d5d0bb7c6ceba36f99511c11703373e5fde85c 100644 (file)
 
 #include <config.h>
 
 
 #include <config.h>
 
+#include <cairo.h>
 #include <getopt.h>
 #include <limits.h>
 #include <stdlib.h>
 #include <unistd.h>
 #include <getopt.h>
 #include <limits.h>
 #include <stdlib.h>
 #include <unistd.h>
+#include <unistr.h>
 
 #include "data/file-handle-def.h"
 #include "data/settings.h"
 
 #include "data/file-handle-def.h"
 #include "data/settings.h"
+#include "libpspp/encoding-guesser.h"
 #include "libpspp/i18n.h"
 #include "libpspp/message.h"
 #include "libpspp/string-map.h"
 #include "libpspp/string-set.h"
 #include "libpspp/i18n.h"
 #include "libpspp/message.h"
 #include "libpspp/string-map.h"
 #include "libpspp/string-set.h"
+#include "libpspp/zip-reader.h"
 #include "output/driver.h"
 #include "output/driver.h"
-#include "output/group-item.h"
-#include "output/page-setup-item.h"
+#include "output/output-item.h"
 #include "output/pivot-table.h"
 #include "output/pivot-table.h"
+#include "output/page-setup.h"
+#include "output/select.h"
 #include "output/spv/light-binary-parser.h"
 #include "output/spv/spv-legacy-data.h"
 #include "output/spv/light-binary-parser.h"
 #include "output/spv/spv-legacy-data.h"
-#include "output/spv/spv-output.h"
-#include "output/spv/spv-select.h"
+#include "output/spv/spv-light-decoder.h"
 #include "output/spv/spv-table-look.h"
 #include "output/spv/spv.h"
 #include "output/spv/spv-table-look.h"
 #include "output/spv/spv.h"
-#include "output/table-item.h"
-#include "output/text-item.h"
 
 #include "gl/c-ctype.h"
 #include "gl/error.h"
 
 #include "gl/c-ctype.h"
 #include "gl/error.h"
 static struct string_map output_options
     = STRING_MAP_INITIALIZER (output_options);
 
 static struct string_map output_options
     = STRING_MAP_INITIALIZER (output_options);
 
-/* --member-name: Include .zip member name in "dir" output. */
+/* --member-names: Include .zip member name in "dir" output. */
 static bool show_member_names;
 
 /* --show-hidden, --select, --commands, ...: Selection criteria. */
 static bool show_member_names;
 
 /* --show-hidden, --select, --commands, ...: Selection criteria. */
-static struct spv_criteria *criteria;
+static struct output_criteria *criteria;
 static size_t n_criteria, allocated_criteria;
 
 /* --or: Add new element to 'criteria' array. */
 static size_t n_criteria, allocated_criteria;
 
 /* --or: Add new element to 'criteria' array. */
@@ -70,108 +72,109 @@ static bool new_criteria;
 /* --sort: Sort members under dump-light-table, to make comparisons easier. */
 static bool sort;
 
 /* --sort: Sort members under dump-light-table, to make comparisons easier. */
 static bool sort;
 
-/* --raw: Dump raw binary data in dump-light-table. */
+/* --raw: Dump raw binary data in "dump-light-table"; dump all strings in
+     "strings". */
 static bool raw;
 
 static bool raw;
 
+/* --no-ascii-only: Drop all-ASCII strings in "strings". */
+static bool exclude_ascii_only;
+
+/* --utf8-only: Only print strings that have UTF-8 multibyte sequences in
+ * "strings". */
+static bool include_utf8_only;
+
 /* -f, --force: Keep output file even on error. */
 static bool force;
 
 /* --table-look: TableLook to replace table style for conversion. */
 /* -f, --force: Keep output file even on error. */
 static bool force;
 
 /* --table-look: TableLook to replace table style for conversion. */
-static struct spv_table_look *table_look;
+static struct pivot_table_look *table_look;
 
 /* Number of warnings issued. */
 static size_t n_warnings;
 
 static void usage (void);
 
 /* Number of warnings issued. */
 static size_t n_warnings;
 
 static void usage (void);
+static void developer_usage (void);
 static void parse_options (int argc, char **argv);
 
 static void parse_options (int argc, char **argv);
 
-static void
-dump_item (const struct spv_item *item)
+static struct output_item *
+annotate_member_names (const struct output_item *in)
 {
 {
-  if (show_member_names && (item->xml_member || item->bin_member))
-    {
-      const char *x = item->xml_member;
-      const char *b = item->bin_member;
-      char *s = (x && b
-                 ? xasprintf (_("%s and %s:"), x, b)
-                 : xasprintf ("%s:", x ? x : b));
-      text_item_submit (text_item_create_nocopy (TEXT_ITEM_TITLE, s));
-    }
-
-  switch (spv_item_get_type (item))
+  if (in->type == OUTPUT_ITEM_GROUP)
     {
     {
-    case SPV_ITEM_HEADING:
-      break;
-
-    case SPV_ITEM_TEXT:
-      spv_text_submit (item);
-      break;
-
-    case SPV_ITEM_TABLE:
-      pivot_table_submit (pivot_table_ref (spv_item_get_table (item)));
-      break;
-
-    case SPV_ITEM_GRAPH:
-      break;
-
-    case SPV_ITEM_MODEL:
-      break;
-
-    case SPV_ITEM_OBJECT:
-      break;
-
-    case SPV_ITEM_TREE:
-      break;
+      struct output_item *out = group_item_clone_empty (in);
+      for (size_t i = 0; i < in->group.n_children; i++)
+        {
+          const struct output_item *item = in->group.children[i];
+          const char *members[4];
+          size_t n = spv_info_get_members (item->spv_info, members,
+                                           sizeof members / sizeof *members);
+          if (n)
+            {
+              struct string s = DS_EMPTY_INITIALIZER;
+              ds_put_cstr (&s, members[0]);
+              for (size_t i = 1; i < n; i++)
+                ds_put_format (&s, " and %s", members[i]);
+              group_item_add_child (out, text_item_create_nocopy (
+                                      TEXT_ITEM_TITLE, ds_steal_cstr (&s),
+                                      xstrdup ("Member Names")));
+            }
 
 
-    default:
-      abort ();
+          group_item_add_child (out, output_item_ref (item));
+        }
+      return out;
     }
     }
+  else
+    return output_item_ref (in);
 }
 
 static void
 }
 
 static void
-print_item_directory (const struct spv_item *item)
+print_item_directory (const struct output_item *item, int level)
 {
 {
-  for (int i = 1; i < spv_item_get_level (item); i++)
+  for (int i = 0; i < level; i++)
     printf ("    ");
 
     printf ("    ");
 
-  enum spv_item_type type = spv_item_get_type (item);
-  printf ("- %s", spv_item_type_to_string (type));
+  printf ("- %s", output_item_type_to_string (item->type));
 
 
-  const char *label = spv_item_get_label (item);
+  const char *label = output_item_get_label (item);
   if (label)
     printf (" \"%s\"", label);
 
   if (label)
     printf (" \"%s\"", label);
 
-  if (type == SPV_ITEM_TABLE)
+  if (item->type == OUTPUT_ITEM_TABLE)
     {
     {
-      const struct pivot_table *table = spv_item_get_table (item);
-      char *title = pivot_value_to_string (table->title,
-                                           SETTINGS_VALUE_SHOW_DEFAULT,
-                                           SETTINGS_VALUE_SHOW_DEFAULT);
+      char *title = pivot_value_to_string (item->table->title, item->table);
       if (!label || strcmp (title, label))
         printf (" title \"%s\"", title);
       free (title);
     }
 
       if (!label || strcmp (title, label))
         printf (" title \"%s\"", title);
       free (title);
     }
 
-  const char *command_id = spv_item_get_command_id (item);
-  if (command_id)
-    printf (" command \"%s\"", command_id);
+  if (item->command_name)
+    printf (" command \"%s\"", item->command_name);
+
+  char *subtype = output_item_get_subtype (item);
+  if (subtype)
+    {
+      if (!label || strcmp (label, subtype))
+        printf (" subtype \"%s\"", subtype);
+      free (subtype);
+    }
 
 
-  const char *subtype = spv_item_get_subtype (item);
-  if (subtype && (!label || strcmp (label, subtype)))
-    printf (" subtype \"%s\"", subtype);
+  if (!item->show)
+    printf (" (%s)", item->type == OUTPUT_ITEM_GROUP ? "collapsed" : "hidden");
 
 
-  if (!spv_item_is_visible (item))
-    printf (" (hidden)");
-  if (show_member_names && (item->xml_member || item->bin_member))
+  if (show_member_names)
     {
     {
-      if (item->xml_member && item->bin_member)
-        printf (" in %s and %s", item->xml_member, item->bin_member);
-      else if (item->xml_member)
-        printf (" in %s", item->xml_member);
-      else if (item->bin_member)
-        printf (" in %s", item->bin_member);
+      const char *members[4];
+      size_t n = spv_info_get_members (item->spv_info, members,
+                                       sizeof members / sizeof *members);
+
+      for (size_t i = 0; i < n; i++)
+          printf (" %s %s", i == 0 ? "in" : "and", members[i]);
     }
   putchar ('\n');
     }
   putchar ('\n');
+
+  if (item->type == OUTPUT_ITEM_GROUP)
+    for (size_t i = 0; i < item->group.n_children; i++)
+      print_item_directory (item->group.children[i], level + 1);
 }
 
 static void
 }
 
 static void
@@ -182,108 +185,49 @@ run_detect (int argc UNUSED, char **argv)
     error (1, 0, "%s", err);
 }
 
     error (1, 0, "%s", err);
 }
 
-static void
-run_directory (int argc UNUSED, char **argv)
+static struct output_item *
+read_and_filter_spv (const char *name, struct page_setup **psp)
 {
 {
-  struct spv_reader *spv;
-  char *err = spv_open (argv[1], &spv);
+  struct output_item *root;
+  char *err = spv_read (name, &root, psp);
   if (err)
     error (1, 0, "%s", err);
   if (err)
     error (1, 0, "%s", err);
-
-  struct spv_item **items;
-  size_t n_items;
-  spv_select (spv, criteria, n_criteria, &items, &n_items);
-  for (size_t i = 0; i < n_items; i++)
-    print_item_directory (items[i]);
-  free (items);
-
-  spv_close (spv);
+  return output_select (root, criteria, n_criteria);
 }
 
 }
 
-struct item_path
-  {
-    const struct spv_item **nodes;
-    size_t n;
-
-#define N_STUB 10
-    const struct spv_item *stub[N_STUB];
-  };
-
 static void
 static void
-swap_nodes (const struct spv_item **a, const struct spv_item **b)
-{
-  const struct spv_item *tmp = *a;
-  *a = *b;
-  *b = tmp;
-}
-
-static void
-get_path (const struct spv_item *item, struct item_path *path)
-{
-  size_t allocated = 10;
-  path->nodes = path->stub;
-  path->n = 0;
-
-  while (item)
-    {
-      if (path->n >= allocated)
-        {
-          if (path->nodes == path->stub)
-            path->nodes = xmemdup (path->stub, sizeof path->stub);
-          path->nodes = x2nrealloc (path->nodes, &allocated,
-                                    sizeof *path->nodes);
-        }
-      path->nodes[path->n++] = item;
-      item = item->parent;
-    }
-
-  for (size_t i = 0; i < path->n / 2; i++)
-    swap_nodes (&path->nodes[i], &path->nodes[path->n - i - 1]);
-}
-
-static void
-free_path (struct item_path *path)
+run_directory (int argc UNUSED, char **argv)
 {
 {
-  if (path && path->nodes != path->stub)
-    free (path->nodes);
+  struct output_item *root = read_and_filter_spv (argv[1], NULL);
+  for (size_t i = 0; i < root->group.n_children; i++)
+    print_item_directory (root->group.children[i], 0);
+  output_item_unref (root);
 }
 
 static void
 }
 
 static void
-dump_heading_transition (const struct spv_item *old,
-                         const struct spv_item *new)
+set_table_look_recursively (struct output_item *item,
+                            const struct pivot_table_look *look)
 {
 {
-  if (old == new)
-    return;
-
-  struct item_path old_path, new_path;
-  get_path (old, &old_path);
-  get_path (new, &new_path);
-
-  size_t common = 0;
-  for (; common < old_path.n && common < new_path.n; common++)
-    if (old_path.nodes[common] != new_path.nodes[common])
-      break;
-
-  for (size_t i = common; i < old_path.n; i++)
-    group_close_item_submit (group_close_item_create ());
-  for (size_t i = common; i < new_path.n; i++)
-    group_open_item_submit (group_open_item_create (
-                              new_path.nodes[i]->command_id));
-
-  free_path (&old_path);
-  free_path (&new_path);
+  if (item->type == OUTPUT_ITEM_TABLE)
+    pivot_table_set_look (item->table, look);
+  else if (item->type == OUTPUT_ITEM_GROUP)
+    for (size_t i = 0; i < item->group.n_children; i++)
+      set_table_look_recursively (item->group.children[i], look);
 }
 
 static void
 run_convert (int argc UNUSED, char **argv)
 {
 }
 
 static void
 run_convert (int argc UNUSED, char **argv)
 {
-  struct spv_reader *spv;
-  char *err = spv_open (argv[1], &spv);
-  if (err)
-    error (1, 0, "%s", err);
-
+  struct page_setup *ps;
+  struct output_item *root = read_and_filter_spv (argv[1], &ps);
   if (table_look)
   if (table_look)
-    spv_item_set_table_look (spv_get_root (spv), table_look);
+    set_table_look_recursively (root, table_look);
+  if (show_member_names)
+    {
+      struct output_item *new_root = annotate_member_names (root);
+      output_item_unref (root);
+      root = new_root;
+    }
 
   output_engine_push ();
   output_set_filename (argv[1]);
 
   output_engine_push ();
   output_set_filename (argv[1]);
@@ -293,26 +237,12 @@ run_convert (int argc UNUSED, char **argv)
     exit (EXIT_FAILURE);
   output_driver_register (driver);
 
     exit (EXIT_FAILURE);
   output_driver_register (driver);
 
-  const struct page_setup *ps = spv_get_page_setup (spv);
   if (ps)
   if (ps)
-    page_setup_item_submit (page_setup_item_create (ps));
-
-  struct spv_item **items;
-  size_t n_items;
-  spv_select (spv, criteria, n_criteria, &items, &n_items);
-  struct spv_item *prev_heading = spv_get_root (spv);
-  for (size_t i = 0; i < n_items; i++)
     {
     {
-      struct spv_item *heading
-        = items[i]->type == SPV_ITEM_HEADING ? items[i] : items[i]->parent;
-      dump_heading_transition (prev_heading, heading);
-      dump_item (items[i]);
-      prev_heading = heading;
+      output_set_page_setup (ps);
+      page_setup_destroy (ps);
     }
     }
-  dump_heading_transition (prev_heading, spv_get_root (spv));
-  free (items);
-
-  spv_close (spv);
+  output_item_submit_children (root);
 
   output_engine_pop ();
   fh_done ();
 
   output_engine_pop ();
   fh_done ();
@@ -325,26 +255,70 @@ run_convert (int argc UNUSED, char **argv)
     }
 }
 
     }
 }
 
+static const struct pivot_table *
+get_first_table (const struct output_item *item)
+{
+  if (item->type == OUTPUT_ITEM_TABLE)
+    return item->table;
+  else if (item->type == OUTPUT_ITEM_GROUP)
+    for (size_t i = 0; i < item->group.n_children; i++)
+      {
+        const struct pivot_table *table
+          = get_first_table (item->group.children[i]);
+        if (table)
+          return table;
+      }
+
+  return NULL;
+}
+
 static void
 static void
-run_dump (int argc UNUSED, char **argv)
+run_get_table_look (int argc UNUSED, char **argv)
 {
 {
-  struct spv_reader *spv;
-  char *err = spv_open (argv[1], &spv);
+  struct pivot_table_look *look;
+  if (strcmp (argv[1], "-"))
+    {
+      struct output_item *root = read_and_filter_spv (argv[1], NULL);
+      const struct pivot_table *table = get_first_table (root);
+      if (!table)
+        error (1, 0, "%s: no tables found", argv[1]);
+
+      look = pivot_table_look_ref (pivot_table_get_look (table));
+
+      output_item_unref (root);
+    }
+  else
+    look = pivot_table_look_ref (pivot_table_look_builtin_default ());
+
+  char *err = spv_table_look_write (argv[2], look);
   if (err)
     error (1, 0, "%s", err);
 
   if (err)
     error (1, 0, "%s", err);
 
-  struct spv_item **items;
-  size_t n_items;
-  spv_select (spv, criteria, n_criteria, &items, &n_items);
-  for (size_t i = 0; i < n_items; i++)
-    if (items[i]->type == SPV_ITEM_TABLE)
-      {
-        pivot_table_dump (spv_item_get_table (items[i]), 0);
-        putchar ('\n');
-      }
-  free (items);
+  pivot_table_look_unref (look);
+}
+
+static void
+run_convert_table_look (int argc UNUSED, char **argv)
+{
+  struct pivot_table_look *look;
+  char *err = spv_table_look_read (argv[1], &look);
+  if (err)
+    error (1, 0, "%s", err);
+
+  err = spv_table_look_write (argv[2], look);
+  if (err)
+    error (1, 0, "%s", err);
+
+  pivot_table_look_unref (look);
+  free (look);
+}
 
 
-  spv_close (spv);
+static void
+run_dump (int argc UNUSED, char **argv)
+{
+  struct output_item *root = read_and_filter_spv (argv[1], NULL);
+  output_item_dump (root, 0);
+  output_item_unref (root);
 }
 
 static int
 }
 
 static int
@@ -369,114 +343,108 @@ compare_cells (const void *a_, const void *b_)
   return a < b ? -1 : a > b;
 }
 
   return a < b ? -1 : a > b;
 }
 
+static char * WARN_UNUSED_RESULT
+dump_raw (struct zip_reader *zr, const char *member_name)
+{
+  void *data;
+  size_t size;
+  char *error = zip_member_read_all (zr, member_name, &data, &size);
+  if (!error)
+    {
+      fwrite (data, size, 1, stdout);
+      free (data);
+    }
+  return error;
+}
+
+static void
+dump_light_table (const struct output_item *item)
+{
+  char *error;
+  if (raw)
+    error = dump_raw (item->spv_info->zip_reader,
+                      item->spv_info->bin_member);
+  else
+    {
+      struct spvlb_table *table;
+      error = spv_read_light_table (item->spv_info->zip_reader,
+                                    item->spv_info->bin_member, &table);
+      if (!error)
+        {
+          if (sort)
+            {
+              qsort (table->borders->borders, table->borders->n_borders,
+                     sizeof *table->borders->borders, compare_borders);
+              qsort (table->cells->cells, table->cells->n_cells,
+                     sizeof *table->cells->cells, compare_cells);
+            }
+          spvlb_print_table (item->spv_info->bin_member, 0, table);
+          spvlb_free_table (table);
+        }
+    }
+  if (error)
+    {
+      msg (ME, "%s", error);
+      free (error);
+    }
+}
+
 static void
 run_dump_light_table (int argc UNUSED, char **argv)
 {
   if (raw && isatty (STDOUT_FILENO))
     error (1, 0, "not writing binary data to tty");
 
 static void
 run_dump_light_table (int argc UNUSED, char **argv)
 {
   if (raw && isatty (STDOUT_FILENO))
     error (1, 0, "not writing binary data to tty");
 
-  struct spv_reader *spv;
-  char *err = spv_open (argv[1], &spv);
-  if (err)
-    error (1, 0, "%s", err);
+  struct output_item *root = read_and_filter_spv (argv[1], NULL);
+  struct output_iterator iter;
+  OUTPUT_ITEM_FOR_EACH (&iter, root)
+    if (iter.cur->type == OUTPUT_ITEM_TABLE && !iter.cur->spv_info->xml_member)
+      dump_light_table (iter.cur);
+  output_item_unref (root);
+}
 
 
-  struct spv_item **items;
-  size_t n_items;
-  spv_select (spv, criteria, n_criteria, &items, &n_items);
-  for (size_t i = 0; i < n_items; i++)
+static void
+dump_legacy_data (const struct output_item *item)
+{
+  char *error;
+  if (raw)
+    error = dump_raw (item->spv_info->zip_reader,
+                      item->spv_info->bin_member);
+  else
     {
     {
-      if (!spv_item_is_light_table (items[i]))
-        continue;
-
-      char *error;
-      if (raw)
+      struct spv_data data;
+      error = spv_read_legacy_data (item->spv_info->zip_reader,
+                                    item->spv_info->bin_member, &data);
+      if (!error)
         {
         {
-          void *data;
-          size_t size;
-          error = spv_item_get_raw_light_table (items[i], &data, &size);
-          if (!error)
-            {
-              fwrite (data, size, 1, stdout);
-              free (data);
-            }
-        }
-      else
-        {
-          struct spvlb_table *table;
-          error = spv_item_get_light_table (items[i], &table);
-          if (!error)
-            {
-              if (sort)
-                {
-                  qsort (table->borders->borders, table->borders->n_borders,
-                         sizeof *table->borders->borders, compare_borders);
-                  qsort (table->cells->cells, table->cells->n_cells,
-                         sizeof *table->cells->cells, compare_cells);
-                }
-              spvlb_print_table (items[i]->bin_member, 0, table);
-              spvlb_free_table (table);
-            }
-        }
-      if (error)
-        {
-          msg (ME, "%s", error);
-          free (error);
+          printf ("%s:\n", item->spv_info->bin_member);
+          spv_data_dump (&data, stdout);
+          spv_data_uninit (&data);
+          printf ("\n");
         }
     }
 
         }
     }
 
-  free (items);
-
-  spv_close (spv);
+  if (error)
+    {
+      msg (ME, "%s", error);
+      free (error);
+    }
 }
 
 static void
 run_dump_legacy_data (int argc UNUSED, char **argv)
 {
 }
 
 static void
 run_dump_legacy_data (int argc UNUSED, char **argv)
 {
-  struct spv_reader *spv;
-  char *err = spv_open (argv[1], &spv);
-  if (err)
-    error (1, 0, "%s", err);
-
-  struct spv_item **items;
-  size_t n_items;
-  spv_select (spv, criteria, n_criteria, &items, &n_items);
-  for (size_t i = 0; i < n_items; i++)
-    if (spv_item_is_legacy_table (items[i]))
-      {
-        struct spv_data data;
-        char *error;
-        if (raw)
-          {
-            void *data;
-            size_t size;
-            error = spv_item_get_raw_legacy_data (items[i], &data, &size);
-            if (!error)
-              {
-                fwrite (data, size, 1, stdout);
-                free (data);
-              }
-          }
-        else
-          {
-            error = spv_item_get_legacy_data (items[i], &data);
-            if (!error)
-              {
-                printf ("%s:\n", items[i]->bin_member);
-                spv_data_dump (&data, stdout);
-                spv_data_uninit (&data);
-                printf ("\n");
-              }
-          }
-
-        if (error)
-          {
-            msg (ME, "%s", error);
-            free (error);
-          }
-      }
-  free (items);
+  if (raw && isatty (STDOUT_FILENO))
+    error (1, 0, "not writing binary data to tty");
 
 
-  spv_close (spv);
+  struct output_item *root = read_and_filter_spv (argv[1], NULL);
+  struct output_iterator iter;
+  OUTPUT_ITEM_FOR_EACH (&iter, root)
+    if (iter.cur->type == OUTPUT_ITEM_TABLE
+        && iter.cur->spv_info->xml_member
+        && iter.cur->spv_info->bin_member)
+      dump_legacy_data (iter.cur);
+  output_item_unref (root);
 }
 
 /* This is really bogus.
 }
 
 /* This is really bogus.
@@ -572,7 +540,7 @@ dump_xml (int argc, char **argv, const char *member_name,
               xmlXPathFreeContext (xpath_ctx);
             }
           if (any_results)
               xmlXPathFreeContext (xpath_ctx);
             }
           if (any_results)
-            putchar ('\n');;
+            putchar ('\n');
         }
       xmlFreeDoc (doc);
     }
         }
       xmlFreeDoc (doc);
     }
@@ -585,79 +553,206 @@ dump_xml (int argc, char **argv, const char *member_name,
 }
 
 static void
 }
 
 static void
-run_dump_legacy_table (int argc, char **argv)
+dump_legacy_table (int argc, char **argv, const struct output_item *item)
 {
 {
-  struct spv_reader *spv;
-  char *err = spv_open (argv[1], &spv);
-  if (err)
-    error (1, 0, "%s", err);
+  xmlDoc *doc;
+  char *error_s = spv_read_xml_member (item->spv_info->zip_reader,
+                                       item->spv_info->xml_member,
+                                       false, "visualization", &doc);
+  dump_xml (argc, argv, item->spv_info->xml_member, error_s, doc);
+}
 
 
-  struct spv_item **items;
-  size_t n_items;
-  spv_select (spv, criteria, n_criteria, &items, &n_items);
-  for (size_t i = 0; i < n_items; i++)
-    if (spv_item_is_legacy_table (items[i]))
-      {
-        xmlDoc *doc;
-        char *error_s = spv_item_get_legacy_table (items[i], &doc);
-        dump_xml (argc, argv, items[i]->xml_member, error_s, doc);
-      }
-  free (items);
+static void
+run_dump_legacy_table (int argc, char **argv)
+{
+  struct output_item *root = read_and_filter_spv (argv[1], NULL);
+  struct output_iterator iter;
+  OUTPUT_ITEM_FOR_EACH (&iter, root)
+    if (iter.cur->type == OUTPUT_ITEM_TABLE
+        && iter.cur->spv_info->xml_member)
+      dump_legacy_table (argc, argv, iter.cur);
+  output_item_unref (root);
+}
 
 
-  spv_close (spv);
+static void
+dump_structure (int argc, char **argv, const struct output_item *item)
+{
+  xmlDoc *doc;
+  char *error_s = spv_read_xml_member (item->spv_info->zip_reader,
+                                       item->spv_info->structure_member,
+                                       true, "heading", &doc);
+  dump_xml (argc, argv, item->spv_info->structure_member, error_s, doc);
 }
 
 static void
 run_dump_structure (int argc, char **argv)
 {
 }
 
 static void
 run_dump_structure (int argc, char **argv)
 {
-  struct spv_reader *spv;
-  char *err = spv_open (argv[1], &spv);
-  if (err)
-    error (1, 0, "%s", err);
+  struct output_item *root = read_and_filter_spv (argv[1], NULL);
 
 
-  struct spv_item **items;
-  size_t n_items;
-  spv_select (spv, criteria, n_criteria, &items, &n_items);
   const char *last_structure_member = NULL;
   const char *last_structure_member = NULL;
-  for (size_t i = 0; i < n_items; i++)
-    if (!last_structure_member || strcmp (items[i]->structure_member,
-                                          last_structure_member))
-      {
-        last_structure_member = items[i]->structure_member;
-
-        xmlDoc *doc;
-        char *error_s = spv_item_get_structure (items[i], &doc);
-        dump_xml (argc, argv, items[i]->structure_member, error_s, doc);
-      }
-  free (items);
+  struct output_iterator iter;
+  OUTPUT_ITEM_FOR_EACH (&iter, root)
+    {
+      const struct output_item *item = iter.cur;
+      if (item->spv_info->structure_member
+          && (!last_structure_member
+              || strcmp (item->spv_info->structure_member,
+                         last_structure_member)))
+        {
+          last_structure_member = item->spv_info->structure_member;
+          dump_structure (argc, argv, item);
+        }
+    }
+  output_item_unref (root);
+}
 
 
-  spv_close (spv);
+static bool
+is_any_legacy (const struct output_item *item)
+{
+  if (item->type == OUTPUT_ITEM_TABLE)
+    return item->spv_info->xml_member != NULL;
+  else if (item->type == OUTPUT_ITEM_GROUP)
+    for (size_t i = 0; i < item->group.n_children; i++)
+      if (is_any_legacy (item->group.children[i]))
+        return true;
+
+  return false;
 }
 
 static void
 run_is_legacy (int argc UNUSED, char **argv)
 {
 }
 
 static void
 run_is_legacy (int argc UNUSED, char **argv)
 {
-  struct spv_reader *spv;
-  char *err = spv_open (argv[1], &spv);
-  if (err)
-    error (1, 0, "%s", err);
+  struct output_item *root = read_and_filter_spv (argv[1], NULL);
+  bool is_legacy = is_any_legacy (root);
+  output_item_unref (root);
 
 
-  bool is_legacy = false;
+  exit (is_legacy ? EXIT_SUCCESS : EXIT_FAILURE);
+}
 
 
-  struct spv_item **items;
-  size_t n_items;
-  spv_select (spv, criteria, n_criteria, &items, &n_items);
-  for (size_t i = 0; i < n_items; i++)
-    if (spv_item_is_legacy_table (items[i]))
-      {
-        is_legacy = true;
-        break;
-      }
-  free (items);
+static bool
+is_all_ascii (const char *s)
+{
+  for (; *s; s++)
+    if (!encoding_guess_is_ascii_text (*s))
+      return false;
 
 
-  spv_close (spv);
+  return true;
+}
 
 
-  exit (is_legacy ? EXIT_SUCCESS : EXIT_FAILURE);
+static void
+dump_strings (const char *encoding, struct string_array *strings)
+{
+  string_array_sort (strings);
+  string_array_uniq (strings);
+
+  if (raw)
+    {
+      if (exclude_ascii_only || include_utf8_only)
+        {
+          size_t i = 0;
+          for (size_t j = 0; j < strings->n; j++)
+            {
+              char *s = strings->strings[j];
+              bool is_ascii = is_all_ascii (s);
+              bool is_utf8 = !u8_check (CHAR_CAST (uint8_t *, s), strlen (s));
+              if (!is_ascii && (!include_utf8_only || is_utf8))
+                strings->strings[i++] = s;
+              else
+                free (s);
+            }
+          strings->n = i;
+        }
+      for (size_t i = 0; i < strings->n; i++)
+        puts (strings->strings[i]);
+    }
+  else
+    {
+      size_t n_nonascii = 0;
+      size_t n_utf8 = 0;
+      for (size_t i = 0; i < strings->n; i++)
+        {
+          const char *s = strings->strings[i];
+          if (!is_all_ascii (s))
+            {
+              n_nonascii++;
+              if (!u8_check (CHAR_CAST (uint8_t *, s), strlen (s)))
+                n_utf8++;
+            }
+        }
+      printf ("%s: %zu unique strings, %zu non-ASCII, %zu UTF-8.\n",
+              encoding, strings->n, n_nonascii, n_utf8);
+    }
+}
+
+struct encoded_strings
+  {
+    char *encoding;
+    struct string_array strings;
+  };
+
+struct encoded_strings_table
+  {
+    struct encoded_strings *es;
+    size_t n, allocated;
+  };
+
+static void
+collect_strings (const struct output_item *item,
+                 struct encoded_strings_table *t)
+{
+  char *error;
+  struct spvlb_table *table;
+  error = spv_read_light_table (item->spv_info->zip_reader,
+                                item->spv_info->bin_member, &table);
+  if (error)
+    {
+      msg (ME, "%s", error);
+      free (error);
+      return;
+    }
+
+  const char *table_encoding = spvlb_table_get_encoding (table);
+  size_t j = 0;
+  for (j = 0; j < t->n; j++)
+    if (!strcmp (t->es[j].encoding, table_encoding))
+      break;
+  if (j >= t->n)
+    {
+      if (t->n >= t->allocated)
+        t->es = x2nrealloc (t->es, &t->allocated, sizeof *t->es);
+      t->es[t->n++] = (struct encoded_strings) {
+        .encoding = xstrdup (table_encoding),
+        .strings = STRING_ARRAY_INITIALIZER,
+      };
+    }
+  collect_spvlb_strings (table, &t->es[j].strings);
+}
+
+static void
+run_strings (int argc UNUSED, char **argv)
+{
+  struct output_item *root = read_and_filter_spv (argv[1], NULL);
+
+  struct encoded_strings_table t = { .es = NULL };
+  struct output_iterator iter;
+  OUTPUT_ITEM_FOR_EACH (&iter, root)
+    {
+      const struct output_item *item = iter.cur;
+      if (item->type == OUTPUT_ITEM_TABLE
+          && !item->spv_info->xml_member
+          && item->spv_info->bin_member)
+        collect_strings (item, &t);
+    }
+
+  for (size_t i = 0; i < t.n; i++)
+    {
+      dump_strings (t.es[i].encoding, &t.es[i].strings);
+      free (t.es[i].encoding);
+      string_array_destroy (&t.es[i].strings);
+    }
+  free (t.es);
+
+  output_item_unref (root);
 }
 
 struct command
 }
 
 struct command
@@ -672,6 +767,8 @@ static const struct command commands[] =
     { "detect", 1, 1, run_detect },
     { "dir", 1, 1, run_directory },
     { "convert", 2, 2, run_convert },
     { "detect", 1, 1, run_detect },
     { "dir", 1, 1, run_directory },
     { "convert", 2, 2, run_convert },
+    { "get-table-look", 2, 2, run_get_table_look },
+    { "convert-table-look", 2, 2, run_convert_table_look },
 
     /* Undocumented commands. */
     { "dump", 1, 1, run_dump },
 
     /* Undocumented commands. */
     { "dump", 1, 1, run_dump },
@@ -680,6 +777,7 @@ static const struct command commands[] =
     { "dump-legacy-table", 1, INT_MAX, run_dump_legacy_table },
     { "dump-structure", 1, INT_MAX, run_dump_structure },
     { "is-legacy", 1, 1, run_is_legacy },
     { "dump-legacy-table", 1, INT_MAX, run_dump_legacy_table },
     { "dump-structure", 1, INT_MAX, run_dump_structure },
     { "is-legacy", 1, 1, run_is_legacy },
+    { "strings", 1, 1, run_strings },
   };
 static const int n_commands = sizeof commands / sizeof *commands;
 
   };
 static const int n_commands = sizeof commands / sizeof *commands;
 
@@ -710,7 +808,7 @@ int
 main (int argc, char **argv)
 {
   set_program_name (argv[0]);
 main (int argc, char **argv)
 {
   set_program_name (argv[0]);
-  msg_set_handler (emit_msg, NULL);
+  msg_set_handler (&(struct msg_handler) { .output_msg = emit_msg });
   settings_init ();
   i18n_init ();
 
   settings_init ();
   i18n_init ();
 
@@ -753,13 +851,13 @@ main (int argc, char **argv)
 
   c->run (argc, argv);
 
 
   c->run (argc, argv);
 
-  spv_table_look_destroy (table_look);
+  pivot_table_look_unref (table_look);
   i18n_done ();
 
   return n_warnings ? EXIT_FAILURE : EXIT_SUCCESS;
 }
 
   i18n_done ();
 
   return n_warnings ? EXIT_FAILURE : EXIT_SUCCESS;
 }
 
-static struct spv_criteria *
+static struct output_criteria *
 get_criteria (void)
 {
   if (!n_criteria || new_criteria)
 get_criteria (void)
 {
   if (!n_criteria || new_criteria)
@@ -768,7 +866,8 @@ get_criteria (void)
       if (n_criteria >= allocated_criteria)
         criteria = x2nrealloc (criteria, &allocated_criteria,
                                sizeof *criteria);
       if (n_criteria >= allocated_criteria)
         criteria = x2nrealloc (criteria, &allocated_criteria,
                                sizeof *criteria);
-      criteria[n_criteria++] = (struct spv_criteria) SPV_CRITERIA_INITIALIZER;
+      criteria[n_criteria++]
+        = (struct output_criteria) OUTPUT_CRITERIA_INITIALIZER;
     }
 
   return &criteria[n_criteria - 1];
     }
 
   return &criteria[n_criteria - 1];
@@ -784,32 +883,32 @@ parse_select (char *arg)
   for (char *token = strtok (arg, ","); token; token = strtok (NULL, ","))
     {
       if (!strcmp (arg, "all"))
   for (char *token = strtok (arg, ","); token; token = strtok (NULL, ","))
     {
       if (!strcmp (arg, "all"))
-        classes = SPV_ALL_CLASSES;
+        classes = OUTPUT_ALL_CLASSES;
       else if (!strcmp (arg, "help"))
         {
           puts (_("The following object classes are supported:"));
       else if (!strcmp (arg, "help"))
         {
           puts (_("The following object classes are supported:"));
-          for (int class = 0; class < SPV_N_CLASSES; class++)
-            printf ("- %s\n", spv_item_class_to_string (class));
+          for (int class = 0; class < OUTPUT_N_CLASSES; class++)
+            printf ("- %s\n", output_item_class_to_string (class));
           exit (0);
         }
       else
         {
           exit (0);
         }
       else
         {
-          int class = spv_item_class_from_string (token);
-          if (class == SPV_N_CLASSES)
-            error (1, 0, _("%s: unknown object class (use --select=help "
-                           "for help"), arg);
+          int class = output_item_class_from_string (token);
+          if (class == OUTPUT_N_CLASSES)
+            error (1, 0, _("unknown object class \"%s\" (use --select=help "
+                           "for help)"), arg);
           classes |= 1u << class;
         }
     }
 
           classes |= 1u << class;
         }
     }
 
-  struct spv_criteria *c = get_criteria ();
-  c->classes = invert ? classes ^ SPV_ALL_CLASSES : classes;
+  struct output_criteria *c = get_criteria ();
+  c->classes = invert ? classes ^ OUTPUT_ALL_CLASSES : classes;
 }
 
 }
 
-static struct spv_criteria_match *
+static struct output_criteria_match *
 get_criteria_match (const char **arg)
 {
 get_criteria_match (const char **arg)
 {
-  struct spv_criteria *c = get_criteria ();
+  struct output_criteria *c = get_criteria ();
   if ((*arg)[0] == '^')
     {
       (*arg)++;
   if ((*arg)[0] == '^')
     {
       (*arg)++;
@@ -822,28 +921,28 @@ get_criteria_match (const char **arg)
 static void
 parse_commands (const char *arg)
 {
 static void
 parse_commands (const char *arg)
 {
-  struct spv_criteria_match *cm = get_criteria_match (&arg);
+  struct output_criteria_match *cm = get_criteria_match (&arg);
   string_array_parse (&cm->commands, ss_cstr (arg), ss_cstr (","));
 }
 
 static void
 parse_subtypes (const char *arg)
 {
   string_array_parse (&cm->commands, ss_cstr (arg), ss_cstr (","));
 }
 
 static void
 parse_subtypes (const char *arg)
 {
-  struct spv_criteria_match *cm = get_criteria_match (&arg);
+  struct output_criteria_match *cm = get_criteria_match (&arg);
   string_array_parse (&cm->subtypes, ss_cstr (arg), ss_cstr (","));
 }
 
 static void
 parse_labels (const char *arg)
 {
   string_array_parse (&cm->subtypes, ss_cstr (arg), ss_cstr (","));
 }
 
 static void
 parse_labels (const char *arg)
 {
-  struct spv_criteria_match *cm = get_criteria_match (&arg);
+  struct output_criteria_match *cm = get_criteria_match (&arg);
   string_array_parse (&cm->labels, ss_cstr (arg), ss_cstr (","));
 }
 
 static void
 parse_instances (char *arg)
 {
   string_array_parse (&cm->labels, ss_cstr (arg), ss_cstr (","));
 }
 
 static void
 parse_instances (char *arg)
 {
-  struct spv_criteria *c = get_criteria ();
+  struct output_criteria *c = get_criteria ();
   size_t allocated_instances = c->n_instances;
 
   for (char *token = strtok (arg, ","); token; token = strtok (NULL, ","))
   size_t allocated_instances = c->n_instances;
 
   for (char *token = strtok (arg, ","); token; token = strtok (NULL, ","))
@@ -860,7 +959,7 @@ parse_instances (char *arg)
 static void
 parse_nth_commands (char *arg)
 {
 static void
 parse_nth_commands (char *arg)
 {
-  struct spv_criteria *c = get_criteria ();
+  struct output_criteria *c = get_criteria ();
   size_t allocated_commands = c->n_commands;
 
   for (char *token = strtok (arg, ","); token; token = strtok (NULL, ","))
   size_t allocated_commands = c->n_commands;
 
   for (char *token = strtok (arg, ","); token; token = strtok (NULL, ","))
@@ -876,15 +975,16 @@ parse_nth_commands (char *arg)
 static void
 parse_members (const char *arg)
 {
 static void
 parse_members (const char *arg)
 {
-  struct spv_criteria *cm = get_criteria ();
+  struct output_criteria *cm = get_criteria ();
   string_array_parse (&cm->members, ss_cstr (arg), ss_cstr (","));
 }
 
 static void
 parse_table_look (const char *arg)
 {
   string_array_parse (&cm->members, ss_cstr (arg), ss_cstr (","));
 }
 
 static void
 parse_table_look (const char *arg)
 {
-  spv_table_look_destroy (table_look);
-  char *error_s = spv_table_look_read (arg, &table_look);
+  pivot_table_look_unref (table_look);
+
+  char *error_s = pivot_table_look_read (arg, &table_look);
   if (error_s)
     error (1, 0, "%s", error_s);
 }
   if (error_s)
     error (1, 0, "%s", error_s);
 }
@@ -909,7 +1009,10 @@ parse_options (int argc, char *argv[])
           OPT_OR,
           OPT_SORT,
           OPT_RAW,
           OPT_OR,
           OPT_SORT,
           OPT_RAW,
+          OPT_NO_ASCII_ONLY,
+          OPT_UTF8_ONLY,
           OPT_TABLE_LOOK,
           OPT_TABLE_LOOK,
+          OPT_HELP_DEVELOPER,
         };
       static const struct option long_options[] =
         {
         };
       static const struct option long_options[] =
         {
@@ -936,7 +1039,12 @@ parse_options (int argc, char *argv[])
           { "sort", no_argument, NULL, OPT_SORT },
           { "raw", no_argument, NULL, OPT_RAW },
 
           { "sort", no_argument, NULL, OPT_SORT },
           { "raw", no_argument, NULL, OPT_RAW },
 
+          /* "strings" command options. */
+          { "no-ascii-only", no_argument, NULL, OPT_NO_ASCII_ONLY },
+          { "utf8-only", no_argument, NULL, OPT_UTF8_ONLY },
+
           { "help", no_argument, NULL, 'h' },
           { "help", no_argument, NULL, 'h' },
+          { "help-developer", no_argument, NULL, OPT_HELP_DEVELOPER },
           { "version", no_argument, NULL, 'v' },
 
           { NULL, 0, NULL, 0 },
           { "version", no_argument, NULL, 'v' },
 
           { NULL, 0, NULL, 0 },
@@ -1010,6 +1118,14 @@ parse_options (int argc, char *argv[])
           parse_table_look (optarg);
           break;
 
           parse_table_look (optarg);
           break;
 
+        case OPT_NO_ASCII_ONLY:
+          exclude_ascii_only = true;
+          break;
+
+        case OPT_UTF8_ONLY:
+          include_utf8_only = true;
+          break;
+
         case 'f':
           force = true;
           break;
         case 'f':
           force = true;
           break;
@@ -1023,6 +1139,10 @@ parse_options (int argc, char *argv[])
           usage ();
           exit (EXIT_SUCCESS);
 
           usage ();
           exit (EXIT_SUCCESS);
 
+        case OPT_HELP_DEVELOPER:
+          developer_usage ();
+          exit (EXIT_SUCCESS);
+
         default:
           exit (EXIT_FAILURE);
         }
         default:
           exit (EXIT_FAILURE);
         }
@@ -1053,6 +1173,8 @@ The following commands are available:\n\
   detect FILE            Detect whether FILE is an SPV file.\n\
   dir FILE               List tables and other items in FILE.\n\
   convert SOURCE DEST    Convert .spv SOURCE to DEST.\n\
   detect FILE            Detect whether FILE is an SPV file.\n\
   dir FILE               List tables and other items in FILE.\n\
   convert SOURCE DEST    Convert .spv SOURCE to DEST.\n\
+  get-table-look SOURCE DEST  Copies first selected TableLook into DEST\n\
+  convert-table-look SOURCE DEST  Copies .tlo or .stt SOURCE into .stt DEST\n\
 \n\
 Input selection options for \"dir\" and \"convert\":\n\
   --select=CLASS...   include only some kinds of objects\n\
 \n\
 Input selection options for \"dir\" and \"convert\":\n\
   --select=CLASS...   include only some kinds of objects\n\
@@ -1074,7 +1196,38 @@ The following options override \"convert\" behavior:\n\
   --table-look=FILE         override tables' style with TableLook from FILE\n\
 Other options:\n\
   --help              display this help and exit\n\
   --table-look=FILE         override tables' style with TableLook from FILE\n\
 Other options:\n\
   --help              display this help and exit\n\
+  --help-developer    display help for developer commands and exit\n\
   --version           output version information and exit\n",
           program_name, program_name, ds_cstr (&s));
   ds_destroy (&s);
 }
   --version           output version information and exit\n",
           program_name, program_name, ds_cstr (&s));
   ds_destroy (&s);
 }
+
+static void
+developer_usage (void)
+{
+  printf ("\
+The following developer commands are available:\n\
+  dump FILE              Dump pivot table structure\n\
+  [--raw | --sort] dump-light-table FILE  Dump light tables\n\
+  [--raw] dump-legacy-data FILE  Dump legacy table data\n\
+  dump-legacy-table FILE [XPATH]...  Dump legacy table XML\n\
+  dump-structure FILE [XPATH]...  Dump structure XML\n\
+  is-legacy FILE         Exit with status 0 if any legacy table selected\n\
+  strings FILE           Dump analysis of strings\n\
+\n\
+Additional input selection options:\n\
+  --members=MEMBER...    include only objects with these Zip member names\n\
+  --errors               include only objects that cannot be loaded\n\
+\n\
+Additional options for \"dir\" command:\n\
+  --member-names         show Zip member names with objects\n\
+\n\
+Options for the \"strings\" command:\n\
+  --raw                  Dump all (unique) strings\n\
+  --raw --no-ascii-only  Dump all strings that contain non-ASCII characters\n\
+  --raw --utf8-only      Dump all non-ASCII strings that are valid UTF-8\n\
+\n\
+Other options:\n\
+  --raw                  print raw binary data instead of a parsed version\n\
+  --sort                 sort borders and areas for shorter \"diff\" output\n");
+}