static void add_properties(GHashTable *ht, char **props) {
  if (*props == NULL) {
    return;
  }

  g_hash_table_insert(ht,
		      g_strdup(OPENSLIDE_PROPERTY_NAME_VENDOR),
		      g_strdup("aperio"));

  // ignore first property in Aperio
  for(char **p = props + 1; *p != NULL; p++) {
    char **pair = g_strsplit(*p, "=", 2);

    if (pair) {
      char *name = g_strstrip(pair[0]);
      if (name) {
	char *value = g_strstrip(pair[1]);

	g_hash_table_insert(ht,
			    g_strdup_printf("aperio.%s", name),
			    g_strdup(value));
      }
    }
    g_strfreev(pair);
  }

  _openslide_duplicate_int_prop(ht, "aperio.AppMag",
                                OPENSLIDE_PROPERTY_NAME_OBJECTIVE_POWER);
  _openslide_duplicate_double_prop(ht, "aperio.MPP",
                                   OPENSLIDE_PROPERTY_NAME_MPP_X);
  _openslide_duplicate_double_prop(ht, "aperio.MPP",
                                   OPENSLIDE_PROPERTY_NAME_MPP_Y);
}
static bool parse_initial_xml(openslide_t *osr, const char *xml,
                              GError **err) {
  xmlDoc *doc = NULL;
  GError *tmp_err = NULL;

  // quick check for plausible XML string before parsing
  if (!strstr(xml, INITIAL_ROOT_TAG)) {
    g_set_error(err, OPENSLIDE_ERROR, OPENSLIDE_ERROR_FORMAT_NOT_SUPPORTED,
                "%s not in XMLPacket", INITIAL_ROOT_TAG);
    goto FAIL;
  }

  // parse
  doc = _openslide_xml_parse(xml, &tmp_err);
  if (!doc) {
    g_set_error(err, OPENSLIDE_ERROR, OPENSLIDE_ERROR_FORMAT_NOT_SUPPORTED,
                "%s", tmp_err->message);
    g_clear_error(&tmp_err);
    goto FAIL;
  }
  xmlNode *root = xmlDocGetRootElement(doc);

  // check root tag name
  if (xmlStrcmp(root->name, BAD_CAST INITIAL_ROOT_TAG)) {
    g_set_error(err, OPENSLIDE_ERROR, OPENSLIDE_ERROR_FORMAT_NOT_SUPPORTED,
                "Root tag not %s", INITIAL_ROOT_TAG);
    goto FAIL;
  }

  // okay, assume Ventana slide

  // we don't know how to handle multiple Z layers
  int64_t z_layers;
  PARSE_INT_ATTRIBUTE_OR_FAIL(root, ATTR_Z_LAYERS, z_layers);
  if (z_layers != 1) {
    g_set_error(err, OPENSLIDE_ERROR, OPENSLIDE_ERROR_BAD_DATA,
                "Slides with multiple Z layers are not supported");
    goto FAIL;
  }

  if (osr) {
    // copy all iScan attributes to vendor properties
    for (xmlAttr *attr = root->properties; attr; attr = attr->next) {
      xmlChar *value = xmlGetNoNsProp(root, attr->name);
      if (value && *value) {
        g_hash_table_insert(osr->properties,
                            g_strdup_printf("ventana.%s", attr->name),
                            g_strdup((char *) value));
      }
      xmlFree(value);
    }

    // set standard properties
    _openslide_duplicate_int_prop(osr->properties, "ventana.Magnification",
                                  OPENSLIDE_PROPERTY_NAME_OBJECTIVE_POWER);
    _openslide_duplicate_double_prop(osr->properties, "ventana.ScanRes",
                                     OPENSLIDE_PROPERTY_NAME_MPP_X);
    _openslide_duplicate_double_prop(osr->properties, "ventana.ScanRes",
                                     OPENSLIDE_PROPERTY_NAME_MPP_Y);
  }

  // clean up
  xmlFreeDoc(doc);
  return true;

FAIL:
  if (doc) {
    xmlFreeDoc(doc);
  }
  return false;
}