QUELLCODE / PY

public_export.py

design/public_export.py

1# OpenAss: nur privater, experimenteller Gebrauch; auf eigene Verantwortung.2# Niemals Unternehmensrechner, Produktivsysteme, Industrieanlagen oder3# sicherheitskritische Systeme anschließen oder bedienen.4# Haftungshinweis und gesetzliche Grenzen: ../DISCLAIMER.txt.5"""Export Blender models without local user paths or editor histories.67Run inside Blender's Python runtime. The public copy is saved through a neutral8temporary directory, so Blender's FileGlobal block cannot retain the workspace9path. PNG stamps must be disabled before rendering as well as before saving.10"""1112from pathlib import Path13import re14import shutil15import tempfile1617import bpy181920def protect_render_metadata(scene):21    """Disable Blender's metadata stamps, including the invisible PNG stamps."""22    for prop in scene.render.bl_rna.properties:23        if prop.type == "BOOLEAN" and not prop.is_readonly:24            if prop.identifier == "use_stamp" or prop.identifier.startswith("use_stamp_"):25                setattr(scene.render, prop.identifier, False)262728def _wipe_string(owner, key, value):29    # Blender serializes entire fixed-size C string buffers. A shorter value30    # alone can leave an earlier absolute path after its terminating NUL byte.31    prop = owner.bl_rna.properties[key]32    length = getattr(prop, "length_max", 0)33    if length > 1:34        padding = "_" * (length - 1)35        setattr(owner, key, padding.encode() if isinstance(value, bytes) else padding)36    setattr(owner, key, value)373839def _private_markers():40    home = Path.home()41    source_root = Path(__file__).resolve().parents[2]42    markers = [str(home), str(source_root)]43    # Include path components to detect stale suffixes in fixed-size buffers.44    if len(home.name) >= 6:45        markers.append(home.name)46    if "." in source_root.name:47        markers.append(source_root.name)48    return tuple(x.lower().encode() for x in markers if len(x) >= 6)495051def _contains_private(value, markers):52    raw = value.encode() if isinstance(value, str) else bytes(value)53    lower = raw.lower()54    if any(marker in lower for marker in markers):55        return True56    return bool(re.search(rb"(?:/(?:Users|home)/|[A-Za-z]:[\\/]Users[\\/])[^\\/\x00\s]+", raw))575859def _neutralize_editor_state():60    for screen in bpy.data.screens:61        for area in screen.areas:62            for space in area.spaces:63                for collection in ("history", "scrollback"):64                    if hasattr(space, collection):65                        for line in getattr(space, collection):66                            line.body = ""67                if space.type == "FILE_BROWSER" and space.params:68                    _wipe_string(space.params, "directory", b"")697071def _neutralize_dependencies(markers):72    for name in ("images", "sounds", "movieclips", "fonts", "cache_files", "libraries", "texts"):73        for block in getattr(bpy.data, name, []):74            if not hasattr(block, "filepath"):75                continue76            path = block.filepath77            if not path or path == "<builtin>" or not _contains_private(path, markers):78                continue79            if name == "texts":80                _wipe_string(block, "filepath", "")81            elif name == "images" and block.packed_file:82                _wipe_string(block, "filepath", "//assets/" + Path(path).name)83            else:84                raise RuntimeError("Public export stopped: an external dependency has a private path.")858687def save_public_blend(destination):88    """Save a compressed public .blend without personal metadata or backups.8990    Verify the uncompressed neutral staging file before producing the compressed91    copy. This works with Blender Python versions that lack a zstd decoder.92    The destination is copied atomically and no .blend1 is created beside it.93    """94    destination = Path(destination).resolve()95    destination.parent.mkdir(parents=True, exist_ok=True)96    markers = _private_markers()97    for scene in bpy.data.scenes:98        protect_render_metadata(scene)99        _wipe_string(scene.render, "filepath", "//renders/LINK-render.png")100    _neutralize_editor_state()101    _neutralize_dependencies(markers)102    # Keep unused palettes available; Blender normally discards these on save.103    for material in bpy.data.materials:104        if material.users == 0:105            material.use_fake_user = True106107    # Do not derive staging paths or filenames from a private workspace path.108    with tempfile.TemporaryDirectory(prefix="openass-public-export-", dir="/tmp") as stage:109        stage = Path(stage)110        plain = stage / "LINK-public-check.blend"111        compressed = stage / "LINK-public.blend"112        bpy.ops.wm.save_as_mainfile(filepath=str(plain), compress=False, relative_remap=False)113        if _contains_private(plain.read_bytes(), markers):114            raise RuntimeError("Public export stopped: private path or user metadata remains.")115        bpy.ops.wm.save_as_mainfile(filepath=str(compressed), compress=True, relative_remap=False)116        temporary = destination.with_name(destination.name + ".public-tmp")117        try:118            shutil.copyfile(compressed, temporary)119            temporary.replace(destination)120        finally:121            temporary.unlink(missing_ok=True)