"""Images Blender launcher for Windows. Install once: python blender-launcher.py --install. Imports only a downloaded Images model matching the SHA-256 from the page. No network, credentials, arbitrary paths, or user-supplied code are accepted. """ import hashlib import json import os from pathlib import Path import re import shutil import subprocess import sys import time from urllib.parse import parse_qs, urlsplit def parse_request(uri): parsed = urlsplit(uri) if parsed.scheme != 'images-blender' or parsed.netloc != 'import' or parsed.fragment: raise ValueError('Invalid Images Blender request.') model = parsed.path.removeprefix('/') query = parse_qs(parsed.query, strict_parsing=True) if not re.fullmatch(r'(?:model|asset)_[a-f0-9]{32}', model) or set(query) != {'format', 'sha256'}: raise ValueError('Invalid model identifier.') if any(len(value) != 1 for value in query.values()): raise ValueError('Invalid model options.') extension, digest = query['format'][0], query['sha256'][0] if extension not in ('glb', 'fbx') or not re.fullmatch(r'[a-f0-9]{64}', digest): raise ValueError('Invalid model format or checksum.') return model, extension, digest def downloads_directory(): import winreg with winreg.OpenKey(winreg.HKEY_CURRENT_USER, r'Software\Microsoft\Windows\CurrentVersion\Explorer\User Shell Folders') as key: path = winreg.QueryValueEx(key, '{374DE290-123F-4565-9164-39C4925E467B}')[0] return Path(os.path.expandvars(path)).resolve() def find_model(directory, model, extension, digest): directory = directory.resolve() candidates = sorted(directory.glob(f'{model}*.{extension}'), key=lambda p: p.stat().st_mtime, reverse=True) for path in candidates: if not re.fullmatch(re.escape(model) + r'(?: \(\d+\))?\.' + extension, path.name): continue if path.resolve().parent != directory or not path.is_file() or not 20 <= path.stat().st_size <= 128 * 1024 * 1024: continue data = path.read_bytes() if hashlib.sha256(data).hexdigest() != digest: continue if extension == 'glb' and data[:4] != b'glTF': continue if extension == 'fbx' and data[:23] != b'Kaydara FBX Binary \x00\x1a\x00': continue return path return None IMPORT_SCRIPT = '''import bpy, sys from mathutils import Vector bpy.ops.wm.read_factory_settings(use_empty=True) path = sys.argv[sys.argv.index('--') + 1] if path.lower().endswith('.glb'): bpy.ops.import_scene.gltf(filepath=path) else: bpy.ops.import_scene.fbx(filepath=path) meshes = [o for o in bpy.context.scene.objects if o.type == 'MESH'] for o in bpy.context.selected_objects: o.select_set(False) for o in meshes: o.select_set(True) if meshes: bpy.context.view_layer.objects.active = meshes[0] points = [o.matrix_world @ Vector(c) for o in meshes for c in o.bound_box] lo = Vector(tuple(min(p[i] for p in points) for i in range(3))) hi = Vector(tuple(max(p[i] for p in points) for i in range(3))) center = (lo + hi) / 2 for screen in bpy.data.screens: for area in screen.areas: if area.type == 'VIEW_3D': region = area.spaces.active.region_3d region.view_location = center region.view_distance = max(hi - lo) * 1.8 region.view_rotation = Vector((-1, 0, 0)).to_track_quat('-Z', 'Y') ''' def blender_path(): executable = shutil.which('blender') if executable: return Path(executable) root = Path(os.environ.get('ProgramFiles', r'C:\Program Files')) / 'Blender Foundation' versions = sorted(root.glob('Blender */blender.exe'), reverse=True) if not versions: raise RuntimeError('Install Blender, then click Load in Blender again.') return versions[0] def install(): if sys.platform != 'win32': raise RuntimeError('This launcher installer supports Windows.') import winreg target = Path(os.environ['LOCALAPPDATA']) / 'ImagesBlenderLauncher' target.mkdir(exist_ok=True) destination = target / 'blender-launcher.py' if Path(__file__).resolve() != destination.resolve(): shutil.copyfile(__file__, destination) python = Path(sys.executable).with_name('pythonw.exe') if not python.is_file(): python = Path(sys.executable) with winreg.CreateKey(winreg.HKEY_CURRENT_USER, r'Software\Classes\images-blender') as key: winreg.SetValueEx(key, '', 0, winreg.REG_SZ, 'URL:Images Blender launcher') winreg.SetValueEx(key, 'URL Protocol', 0, winreg.REG_SZ, '') with winreg.CreateKey(winreg.HKEY_CURRENT_USER, r'Software\Classes\images-blender\shell\open\command') as key: winreg.SetValueEx(key, '', 0, winreg.REG_SZ, f'"{python}" "{destination}" "%1"') print(json.dumps({'installed': True, 'blender': str(blender_path())})) def launch(uri): model, extension, digest = parse_request(uri) directory = downloads_directory() deadline = time.monotonic() + 60 file = None while time.monotonic() < deadline: file = find_model(directory, model, extension, digest) if file: break time.sleep(.5) if file is None: raise RuntimeError('Save the model in Downloads, then click Load in Blender again.') script = Path(__file__).resolve().parent / 'import-model.py' script.write_text(IMPORT_SCRIPT, encoding='utf-8') # Argument array, never a shell. Blender opens a new empty scene, preserving existing work. subprocess.Popen([str(blender_path()), '--factory-startup', '--python', str(script), '--', str(file)], shell=False) if __name__ == '__main__': try: if len(sys.argv) != 2: raise ValueError('Expected one Images Blender request or --install.') install() if sys.argv[1] == '--install' else launch(sys.argv[1]) except Exception as error: if sys.platform == 'win32': import ctypes ctypes.windll.user32.MessageBoxW(None, str(error), 'Images ยท Blender', 0x10) else: print(str(error), file=sys.stderr) sys.exit(1)