work on docs
[pspp] / utilities / pspp-output.c
index 58547196bec6451e2b7a1f5b755d4ca1109887a5..8e80faad511af37fcac25ed366526fb36d48c8ef 100644 (file)
 
 #include <config.h>
 
+#include <cairo.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 "libpspp/encoding-guesser.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/group-item.h"
-#include "output/page-setup-item.h"
+#include "output/output-item.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/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/table-item.h"
-#include "output/text-item.h"
 
 #include "gl/c-ctype.h"
 #include "gl/error.h"
 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 struct spv_criteria *criteria;
+static struct output_criteria *criteria;
 static size_t n_criteria, allocated_criteria;
 
 /* --or: Add new element to 'criteria' array. */
@@ -69,105 +72,109 @@ static bool new_criteria;
 /* --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;
 
+/* --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. */
+static struct pivot_table_look *table_look;
+
 /* 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
-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))
+  if (in->type == OUTPUT_ITEM_GROUP)
     {
-      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))
-    {
-    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
-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 ("    ");
 
-  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 (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);
     }
 
-  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);
 
-  const char *subtype = spv_item_get_subtype (item);
-  if (subtype && (!label || strcmp (label, subtype)))
-    printf (" subtype \"%s\"", subtype);
+  char *subtype = output_item_get_subtype (item);
+  if (subtype)
+    {
+      if (!label || strcmp (label, subtype))
+        printf (" subtype \"%s\"", subtype);
+      free (subtype);
+    }
 
-  if (!spv_item_is_visible (item))
-    printf (" (hidden)");
-  if (show_member_names && (item->xml_member || item->bin_member))
+  if (!item->show)
+    printf (" (%s)", item->type == OUTPUT_ITEM_GROUP ? "collapsed" : "hidden");
+
+  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');
+
+  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
@@ -178,105 +185,49 @@ run_detect (int argc UNUSED, char **argv)
     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);
-
-  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);
-}
-
-struct item_path
-  {
-    const struct spv_item **nodes;
-    size_t n;
-
-#define N_STUB 10
-    const struct spv_item *stub[N_STUB];
-  };
-
-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]);
+  return output_select (root, criteria, n_criteria);
 }
 
 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
-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)
 {
-  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)
+    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]);
@@ -286,26 +237,12 @@ run_convert (int argc UNUSED, char **argv)
     exit (EXIT_FAILURE);
   output_driver_register (driver);
 
-  const struct page_setup *ps = spv_get_page_setup (spv);
   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 ();
@@ -318,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
-run_dump (int argc UNUSED, char **argv)
+run_get_table_look (int argc UNUSED, char **argv)
+{
+  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);
+
+  pivot_table_look_unref (look);
+}
+
+static void
+run_convert_table_look (int argc UNUSED, char **argv)
 {
-  struct spv_reader *spv;
-  char *err = spv_open (argv[1], &spv);
+  struct pivot_table_look *look;
+  char *err = spv_table_look_read (argv[1], &look);
   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);
+  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
@@ -362,114 +343,108 @@ compare_cells (const void *a_, const void *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");
 
-  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)
-        {
-          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 spv_data data;
+      error = spv_read_legacy_data (item->spv_info->zip_reader,
+                                    item->spv_info->bin_member, &data);
+      if (!error)
         {
-          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)
 {
-  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.
@@ -565,7 +540,7 @@ dump_xml (int argc, char **argv, const char *member_name,
               xmlXPathFreeContext (xpath_ctx);
             }
           if (any_results)
-            putchar ('\n');;
+            putchar ('\n');
         }
       xmlFreeDoc (doc);
     }
@@ -578,79 +553,206 @@ dump_xml (int argc, char **argv, const char *member_name,
 }
 
 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)
 {
-  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;
-  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)
 {
-  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
@@ -665,6 +767,8 @@ static const struct command commands[] =
     { "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 },
@@ -673,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 },
+    { "strings", 1, 1, run_strings },
   };
 static const int n_commands = sizeof commands / sizeof *commands;
 
@@ -703,7 +808,7 @@ int
 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 ();
 
@@ -746,12 +851,13 @@ main (int argc, char **argv)
 
   c->run (argc, argv);
 
+  pivot_table_look_unref (table_look);
   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)
@@ -760,7 +866,8 @@ get_criteria (void)
       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];
@@ -776,32 +883,32 @@ parse_select (char *arg)
   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:"));
-          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
         {
-          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;
         }
     }
 
-  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)
 {
-  struct spv_criteria *c = get_criteria ();
+  struct output_criteria *c = get_criteria ();
   if ((*arg)[0] == '^')
     {
       (*arg)++;
@@ -814,28 +921,28 @@ get_criteria_match (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)
 {
-  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)
 {
-  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)
 {
-  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, ","))
@@ -852,7 +959,7 @@ parse_instances (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, ","))
@@ -868,10 +975,20 @@ parse_nth_commands (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)
+{
+  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);
+}
+
 static void
 parse_options (int argc, char *argv[])
 {
@@ -892,6 +1009,10 @@ parse_options (int argc, char *argv[])
           OPT_OR,
           OPT_SORT,
           OPT_RAW,
+          OPT_NO_ASCII_ONLY,
+          OPT_UTF8_ONLY,
+          OPT_TABLE_LOOK,
+          OPT_HELP_DEVELOPER,
         };
       static const struct option long_options[] =
         {
@@ -912,12 +1033,18 @@ parse_options (int argc, char *argv[])
 
           /* "convert" command options. */
           { "force", no_argument, NULL, 'f' },
+          { "table-look", required_argument, NULL, OPT_TABLE_LOOK },
 
           /* "dump-light-table" command options. */
           { "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-developer", no_argument, NULL, OPT_HELP_DEVELOPER },
           { "version", no_argument, NULL, 'v' },
 
           { NULL, 0, NULL, 0 },
@@ -987,6 +1114,18 @@ parse_options (int argc, char *argv[])
           raw = true;
           break;
 
+        case OPT_TABLE_LOOK:
+          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;
@@ -1000,6 +1139,10 @@ parse_options (int argc, char *argv[])
           usage ();
           exit (EXIT_SUCCESS);
 
+        case OPT_HELP_DEVELOPER:
+          developer_usage ();
+          exit (EXIT_SUCCESS);
+
         default:
           exit (EXIT_FAILURE);
         }
@@ -1030,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\
+  get-table-look SOURCE DEST  Copies first selected TableLook into DEST\n\
+  convert-table-look SOURCE DEST  Copies .tlo or .stt SOURCE into DEST\n\
 \n\
 Input selection options for \"dir\" and \"convert\":\n\
   --select=CLASS...   include only some kinds of objects\n\
@@ -1048,9 +1193,41 @@ The following options override \"convert\" behavior:\n\
   -O format=FORMAT          set destination format to FORMAT\n\
   -O OPTION=VALUE           set output option\n\
   -f, --force               keep output file even given errors\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);
 }
+
+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");
+}