# The full ID format is something like: # # Fills: [symbol name]_[symbol frame idx]_Layer[layer idx]_[element idx]_FILL # Strokes: [symbol name]_[symbol frame idx]_Layer[layer idx]_[element idx]_[stroke idx]_STROKES # Nested groups: [symbol name]_[symbol frame idx]_Layer[layer idx]_[element idx]_MEMBER_[member idx]_FILL # DOMShape in mask layer: Mask_[symbol name]_[symbol frame idx]_[?]_MASK_[element idx]_FILL # # Frames/layers/members are indexed from 0, strokes are indexed from 1. # Everything regarding how masks are named is a guess--I currently don't # understand the naming process. In particular, I don't get what the [?] stands # for and exactly when shapes have `Mask_` and/or `_MASK_`. import re from glob import glob import os from xml.etree import ElementTree # In an XFL, layers are stored as a flattened tree. Here's a diagram which # tries to mimick the layout of layers in Animate. The layer index is on the # left, the tree structure is shown in the middle, and the parent index of each # layer is on the right. Layers which don't have a parent will have a parent # index of -1. # # Index Tree Parent # ===== ==== ====== # # 0 * -1 # 1 * 0 # 2 * 1 # 3 * 1 # 4 * 0 # 5 * 0 # 6 * -1 # # # To match the order of the SVG, layers must be processed in reverse order, # with parents coming before their children. For the layers above, this means # the order is: [6, 0, 5, 4, 1, 3, 2] # # Index Tree Parent Order # ===== ==== ====== ===== # # 0 * -1 +> 0 # 1 * 0 | | +> 1 # 2 * 1 | | | | +> 2 # 3 * 1 | | | +> 3 # 4 * 0 | | +> 4 # 5 * 0 | +> 5 # 6 * -1 6 # # # The following function reorders the layers to match this traversal order. # `layers` is a list of (layer_idx, layer element), i.e. `list(enumerate(layers))` def reorder_layers(layers, parent_idx=-1): reordered_layers = [] while layers: # Look at the parent index of the last layer layer_parent_idx = int(layers[-1][1].get("parentLayerIndex") or -1) if parent_idx < layer_parent_idx: # We've reached the bottom (start) of a new group of layers child_layers = reorder_layers(layers, layer_parent_idx) if layers: # If there are no layers left, then we've reached the end of # the "-1" group (i.e. we've processed all layers). If there # are layers left, then the next layer is the parent of the # group we just processed, so add that layer first. reordered_layers.append(layers.pop()) # Add the children reordered_layers.extend(child_layers) elif parent_idx > layer_parent_idx: # We've reached the top (end) of this group of layers break else: # This layer is part of the current group reordered_layers.append(layers.pop()) return reordered_layers class XFL: def __init__(self, xfl_dir: str): # Dict of {symbol_name: list of layer elements} self.symbols = {} for filename in glob(os.path.join(xfl_dir, "LIBRARY", "*.xml")): symbol = ElementTree.parse(filename).getroot() name = symbol.get("name") layers = symbol.findall(".//{*}DOMLayer") self.symbols[name] = reorder_layers(list(enumerate(layers))) def symbol_to_svg(self, name: str, frame_idx: int): """Print out a symbol in an "SVG ID" format. For debugging/demonstration only. Args: name: Symbol name (`name` attribute of DOMSymbolItem). Should be ugly and may contain entities (e.g. "*"). Don't pass the pretty `sourceLibraryItemHRef` name. frame_idx: Frame number (starting at 0) """ # Replace entities id_name = re.sub(r"&#(\d{3})", lambda x: chr(int(x.group(1))), name) # Replace special characters # XXX: This regex is conservative and only contains characters I've # seen so far. If it fails, just add in the offending characters. id_name = re.sub(r"[~* ()-]", "_", id_name) # Append frame index id_name += f"_{frame_idx}" for layer_idx, layer in self.symbols[name]: # Invisible and guide layers aren't included in the SVG if layer.get("visible") == "false" or layer.get("layerType") == "guide": continue # Find frame frame = None for f in layer.iterfind(".//{*}DOMFrame"): index = int(f.get("index")) duration = int(f.get("duration") or 1) if index <= frame_idx < index + duration: frame = f break if frame is None: continue if layer.get("layerType") == "mask": # Add mask designation # TODO: What is the `?` number supposed to be? And are we # really supposed to add "Mask_" here? layer_id_name = f"Mask_{id_name}_[?]_MASK" else: # Append layer index layer_id_name = f"{id_name}_Layer{layer_idx}" # Elements are ordered from back to front, so we don't reverse. for element_idx, element in enumerate(frame.find(".//{*}elements")): # Append element index element_id_name = f"{layer_id_name}_{element_idx}" self.element_to_svg(element, element_id_name) def element_to_svg(self, element, id_name): if element.tag.endswith("DOMSymbolInstance"): # Only graphic symbols are supported assert element.get("symbolType") == "graphic" self.symbol_to_svg( element.get("libraryItemName"), int(element.get("firstFrame") or 0), ) elif element.tag.endswith("DOMShape"): # Assume that every DOMShape has a fill print(f"{id_name}_FILL") # Assume that there is one stroke for each StrokeStyle for stroke in element.iterfind(".//{*}StrokeStyle"): stroke_idx = stroke.get("index") # NOTE: The number before STROKES seems to be the stroke style # index, but I'm not completely sure print(f"{id_name}_{stroke_idx}_STROKES") elif element.tag.endswith("DOMGroup"): for member_idx, member in enumerate(element.find(".//{*}members")): if member.tag.endswith("DOMGroup"): # For some reason, elements in nested DOMGroups get an additional designation self.element_to_svg(member, f"{id_name}_MEMBER_{member_idx}") else: self.element_to_svg(member, id_name) else: raise Exception(f"Unknown element: {element.tag}") if __name__ == "__main__": import argparse parser = argparse.ArgumentParser(description="'Convert' an XFL to SVG IDs") parser.add_argument("xfl_dir", type=str, help="Root directory of the unzipped XFL") parser.add_argument( "symbol_name", type=str, help="Name of symbol (`name` attribute of DOMSymbolItem. May have entities like '*')", ) parser.add_argument("frame", type=int, help="Frame number (starts from 0)") args = parser.parse_args() xfl = XFL(args.xfl_dir) xfl.symbol_to_svg(args.symbol_name, args.frame)