"""File -> Export jobs using Blender's registered exporters and RNA validation.""" import hashlib import json from pathlib import Path import shutil import zipfile import bpy from server_operator_properties import validated_operator_properties FORMATS = { 'GLB': ('export_scene.gltf', 'glb', 'use_selection', {'export_format': 'GLB'}), 'GLTF': ('export_scene.gltf', 'gltf', 'use_selection', {'export_format': 'GLTF_SEPARATE', 'export_texture_dir': 'textures'}), 'FBX': ('export_scene.fbx', 'fbx', 'use_selection', {'path_mode': 'COPY', 'embed_textures': True}), 'OBJ': ('wm.obj_export', 'obj', 'export_selected_objects', {'path_mode': 'COPY'}), 'STL': ('wm.stl_export', 'stl', 'export_selected_objects', {}), 'PLY': ('wm.ply_export', 'ply', 'export_selected_objects', {}), 'USD': ('wm.usd_export', 'usdc', 'selected_objects_only', {'export_textures': True, 'relative_paths': True}), 'ALEMBIC': ('wm.alembic_export', 'abc', 'selected', {}), } def export_scene(root, spec, artifact, output): identifier, extension, selection_key, defaults = FORMATS[spec['format']] family, name = identifier.split('.') if family == 'export_scene': import addon_utils addon_utils.enable('io_scene_gltf2' if name == 'gltf' else 'io_scene_fbx', default_set=False, persistent=False) operator = getattr(getattr(bpy.ops, family), name) try: rna = operator.get_rna_type() except RuntimeError as error: raise RuntimeError('NATIVE_EXPORTER_UNAVAILABLE: ' + identifier) from error destination = root / 'export' destination.mkdir() filename = destination / ('scene.' + extension) supplied = spec.get('properties', {}) # Output locations and batch modes belong to this job, never to caller input. blocked = {'filepath', 'directory', 'filename', 'files', 'check_existing', 'filter_glob', 'batch_mode', 'use_batch_own_dir', 'as_background_job', 'export_format', 'export_texture_dir', 'export_animation', 'use_selection', 'export_selected_objects', 'selected_objects_only', 'selected'} for key in supplied: if key in blocked or any(token in key.lower() for token in ('path', 'directory', 'filename')): raise RuntimeError('NATIVE_EXPORT_MANAGED_PROPERTY: ' + key) values = dict(defaults, **supplied) values.update(filepath=str(filename), **{selection_key: spec['selectedOnly']}) if 'check_existing' in rna.properties: values['check_existing'] = False if 'as_background_job' in rna.properties: values['as_background_job'] = False if spec['selectedOnly'] and not any(obj.select_get() for obj in bpy.context.view_layer.objects): raise RuntimeError('NATIVE_EXPORT_SELECTION_EMPTY') values = validated_operator_properties(operator, values) if not operator.poll(): raise RuntimeError('NATIVE_EXPORT_CONTEXT_UNAVAILABLE: ' + identifier) state = operator('EXEC_DEFAULT', **values) if 'FINISHED' not in state or not filename.is_file() or filename.stat().st_size == 0: raise RuntimeError('NATIVE_EXPORT_NO_OUTPUT: ' + identifier) files = [] for file in sorted(destination.rglob('*')): if file.is_symlink(): raise RuntimeError('NATIVE_EXPORT_SYMLINK') if file.is_file(): files.append({'name': file.relative_to(destination).as_posix(), 'bytes': file.stat().st_size, 'sha256': hashlib.sha256(file.read_bytes()).hexdigest()}) if len(files) > 10000 or sum(item['bytes'] for item in files) > 128 * 1024 * 1024: raise RuntimeError('NATIVE_EXPORT_PACKAGE_BUDGET') receipt = {'format': spec['format'], 'operator': identifier, 'blenderVersion': bpy.app.version_string, 'selectedOnly': spec['selectedOnly'], 'files': files} (destination / 'export-receipt.json').write_text(json.dumps(receipt, indent=2), encoding='utf8') package = root / 'export-package.zip' with zipfile.ZipFile(package, 'w', zipfile.ZIP_DEFLATED) as bundle: for file in sorted(destination.rglob('*')): if file.is_file(): bundle.write(file, file.relative_to(destination).as_posix()) artifact(package, 'application/zip') # Multi-file formats must be downloaded as the package to retain relative resources. if len(files) == 1: standalone = root / filename.name shutil.copyfile(filename, standalone) artifact(standalone, 'model/gltf-binary' if extension == 'glb' else 'application/octet-stream') output.write(json.dumps(receipt, indent=2) + '\n')