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)