Reading SKP Files

Practical patterns for loading and extracting data from .skp files. Except for the first snippet, the examples build on the loaded model variable.


Basic load

Import the package and pass a filesystem path to the single public loader:

import skppy

model = skppy.load("model.skp")

load() detects the modern ZIP and legacy pre-ZIP containers automatically. It returns the same Model API for both; malformed existing files raise skppy.InvalidSkpError.


Inspecting the header

Loaded models retain both parsed header fields and source-container metadata:

h = model.header
print(h.product_name)     # "SketchUp 2023"
print(h.version_string)   # "{23.1.340}"
print(h.version_tuple)    # (23, 1, 340)
if model.document is not None:
    print(model.document.filepath)  # path passed to skppy.load()

header describes the SKP envelope. document describes the embedded ZIP and is available for modern files; legacy-specific provenance is instead exposed through model.legacy_archive.


Listing materials

Iterate the model registry to inspect normalized appearance values:

for mat in model.materials:
    print(f"  {mat.name!r}")
    print(f"    color:    {mat.color}")
    print(f"    alpha:    {mat.alpha}")
    print(f"    metallic: {mat.metallic}")
    print(f"    roughness:{mat.roughness}")
    if mat.has_texture and mat.texture:
        t = mat.texture
        print(f"    texture:  {t.filename!r} ({t.x_scale:.1f} x {t.y_scale:.1f} in)")

Material IDs, not list positions, are used by faces and component instances. Build an {id: material} dictionary when resolving many references.


Listing layers

Visibility is stored on each layer independently of entity visibility:

for layer in model.layers:
    print(f"  {'[ok]' if layer.visible else '[x]'}  {layer.name}")

Layers are called Tags in current SketchUp versions. Entity layer_id values refer to Layer.id; a missing ID means the default/untagged layer state.


Walking the instance hierarchy

Root-level instances reference definitions:

defn_map = {d.id: d for d in model.definitions}

def walk(entities, depth=0, active_definition_ids=()):
    """Print component/group nesting while stopping recursive definitions."""
    indent = "  " * depth
    placed_entities = [*entities.component_instances, *entities.groups]
    for placed in placed_entities:
        defn = defn_map.get(placed.definition_id)
        if defn is None:
            print(f"{indent}Missing definition {placed.definition_id}")
            continue
        if defn.id in active_definition_ids:
            print(f"{indent}Recursive reference to {defn.name!r}; stopped")
            continue

        kind = type(placed).__name__
        local = skppy.Transform(placed.transform).translation()
        print(f"{indent}{kind} {placed.name!r} -> {defn.name!r}")
        print(f"{indent}  local translation: {local.to_tuple()} inches")
        walk(defn.entities, depth + 1, (*active_definition_ids, defn.id))

walk(model.entities)

Each stored transform is local to its parent. To calculate world coordinates, compose the matrices along the current recursion path as shown in Working with Geometry, especially its world-space example.


Extracting all face geometry

The renderer-neutral scene graph expands nested placements while keeping each mesh in definition-local coordinates:

def walk_nodes(node):
    """Yield a SceneNode and all descendants in depth-first order."""
    yield node
    for child in node.children:
        yield from walk_nodes(child)


# to_scene() expands nested components and groups with cycle detection.
for node in walk_nodes(model.to_scene()):
    if node.mesh is None:
        continue
    for face in node.mesh.faces:
        # A face material wins; otherwise use the instance-level override.
        effective_material = face.material_name or node.material_name
        print(node.name, face.vertex_positions, effective_material)

PreparedFace.vertex_positions remain local to their SceneNode. Apply the node transform and all parent transforms for world-space coordinates. Keeping them local allows repeated component definitions to share one prepared mesh.


Dumping textures to disk

Texture objects retain encoded image bytes when the source resource was available. Treat serialized filenames as untrusted metadata:

from pathlib import Path, PureWindowsPath

output_dir = Path("textures_out")
output_dir.mkdir(exist_ok=True)

for mat in model.materials:
    if mat.has_texture and mat.texture and mat.texture.data:
        # Serialized filenames are informational and may contain directories.
        safe_name = PureWindowsPath(mat.texture.filename).name or f"material-{mat.id}"
        path = output_dir / safe_name
        path.write_bytes(mat.texture.data)
        print(f"Saved {path}")

Using only the final filename component prevents a stored source path from escaping textures_out. Applications should also choose a collision policy when two materials use the same filename.


Inspecting the raw ZIP

Modern files expose ZIP entry metadata through model.document. Legacy files set this field to None because their CArchive container has no ZIP entries.

if model.document is not None:
    for entry in model.document.zip_entries:
        print(f"  {entry.name}: {entry.file_size} bytes")

SkpZipEntry stores metadata, not an eager copy of every resource. Use dump_zip() when the actual entries are needed:

To extract everything from a modern model:

model.dump_zip("/tmp/skp_extracted/")