bl_info = { "name": "Render Output Presets", "author": "Assistant", "version": (1, 2, 0), "blender": (3, 0, 0), "location": "Properties > Output Properties", "description": "Save and load render output presets (format, color management, etc.)", "category": "Render", } __package__ = "render_output_presets" import bpy import json import os import subprocess import platform from bpy.types import Operator, Panel, PropertyGroup from bpy.props import StringProperty, CollectionProperty, IntProperty class RenderPreset(PropertyGroup): name: StringProperty(name="Preset Name", default="Preset") data: StringProperty(name="Preset Data", default="{}") def get_presets_filepath(): """Get the path to the presets JSON file""" config_path = bpy.utils.user_resource('CONFIG') presets_dir = os.path.join(config_path, "render_output_presets") os.makedirs(presets_dir, exist_ok=True) return os.path.join(presets_dir, "presets.json") def save_presets_to_file(context): """Save all presets to a JSON file""" presets = context.scene.render_output_presets presets_data = [{"name": p.name, "data": p.data} for p in presets] filepath = get_presets_filepath() try: with open(filepath, 'w') as f: json.dump(presets_data, f, indent=2) print(f"Presets saved successfully to: {filepath}") except Exception as e: print(f"Error saving presets to {filepath}: {e}") def load_presets_from_file(context): """Load presets from JSON file""" filepath = get_presets_filepath() print(f"Looking for presets at: {filepath}") if not os.path.exists(filepath): print("Presets file not found") return try: with open(filepath, 'r') as f: presets_data = json.load(f) # Clear existing presets context.scene.render_output_presets.clear() # Load presets from file for preset_dict in presets_data: preset = context.scene.render_output_presets.add() preset.name = preset_dict["name"] preset.data = preset_dict["data"] print(f"Loaded {len(presets_data)} presets successfully") except Exception as e: print(f"Error loading presets from {filepath}: {e}") class RENDER_OT_save_preset(Operator): bl_idname = "render.save_output_preset" bl_label = "Save Preset" bl_description = "Save current render output settings as a preset" preset_name: StringProperty(name="Preset Name", default="New Preset") def _get_media_type(self, file_format): """Determine media type from file format""" video_formats = ['FFMPEG', 'AVI_JPEG', 'AVI_RAW'] if file_format in video_formats: return "VIDEO" else: return "IMAGE" def invoke(self, context, event): return context.window_manager.invoke_props_dialog(self) def draw(self, context): layout = self.layout layout.prop(self, "preset_name") def execute(self, context): scene = context.scene settings = scene.render img_settings = settings.image_settings # Collect render output settings preset_data = { # Media type - determine from file format "media_type": self._get_media_type(img_settings.file_format), # File Format settings "file_format": img_settings.file_format, "color_mode": img_settings.color_mode, "color_depth": img_settings.color_depth, "compression": img_settings.compression, "quality": img_settings.quality, # Color space for the image file (e.g., OpenEXR, PNG color space) "color_management": img_settings.color_management, "linear_colorspace_settings": { "name": img_settings.linear_colorspace_settings.name, }, # Color Management "display_settings": { "display_device": scene.display_settings.display_device, }, "view_settings": { "view_transform": scene.view_settings.view_transform, "look": scene.view_settings.look, "exposure": scene.view_settings.exposure, "gamma": scene.view_settings.gamma, }, "sequencer_colorspace_settings": { "name": scene.sequencer_colorspace_settings.name, }, } # Format-specific settings if img_settings.file_format == 'FFMPEG': preset_data["ffmpeg"] = { "format": settings.ffmpeg.format, "codec": settings.ffmpeg.codec, "video_bitrate": settings.ffmpeg.video_bitrate, "audio_codec": settings.ffmpeg.audio_codec, "audio_bitrate": settings.ffmpeg.audio_bitrate, "gopsize": settings.ffmpeg.gopsize, "constant_rate_factor": settings.ffmpeg.constant_rate_factor, "ffmpeg_preset": settings.ffmpeg.ffmpeg_preset, } # Add preset to collection presets = scene.render_output_presets preset = presets.add() preset.name = self.preset_name preset.data = json.dumps(preset_data) scene.render_output_presets_index = len(presets) - 1 # Save to file save_presets_to_file(context) self.report({'INFO'}, f"Preset '{self.preset_name}' saved") return {'FINISHED'} class RENDER_OT_load_preset(Operator): bl_idname = "render.load_output_preset" bl_label = "Load Preset" bl_description = "Load selected render output preset" def execute(self, context): scene = context.scene presets = scene.render_output_presets idx = scene.render_output_presets_index if idx < 0 or idx >= len(presets): self.report({'WARNING'}, "No preset selected") return {'CANCELLED'} preset = presets[idx] preset_data = json.loads(preset.data) settings = scene.render img_settings = settings.image_settings # Get saved media type and file format media_type = preset_data.get("media_type", "IMAGE") file_format = preset_data.get("file_format", "PNG") print(f"Loading preset: media_type={media_type}, file_format={file_format}") print(f"Current media_type: {img_settings.media_type}, file_format: {img_settings.file_format}") # First, set the media type directly try: img_settings.media_type = media_type print(f"Set media_type to: {media_type}") except Exception as e: print(f"Failed to set media_type: {e}") # Verify the media type switch by checking available formats print("Available formats after media type switch:") available_formats = [] try: for item in img_settings.bl_rna.properties['file_format'].enum_items: available_formats.append(item.identifier) print(f" - {item.identifier}") except: pass # Now try to apply the actual file format # Map old format names to new ones for cross-version compatibility format_mapping = { "OPEN_EXR": "OPEN_EXR_MULTILAYER", # Fallback mapping if needed } # Try to apply the file format applied = False formats_to_try = [file_format] # Add mapped format if exists if file_format in format_mapping: formats_to_try.append(format_mapping[file_format]) # Also try common variants for EXR if "EXR" in file_format.upper(): formats_to_try.extend(["OPEN_EXR", "OPEN_EXR_MULTILAYER"]) print(f"Attempting to set formats in order: {formats_to_try}") for fmt in formats_to_try: if fmt in available_formats: try: print(f"Trying to set format: {fmt}") img_settings.file_format = fmt applied = True print(f"Successfully set format to: {fmt}") if fmt != file_format: print(f"Mapped format '{file_format}' to '{fmt}'") break except TypeError as e: print(f"Failed to set format '{fmt}': {e}") continue else: print(f"Format '{fmt}' not in available formats list") if not applied: self.report({'ERROR'}, f"File format '{file_format}' not available. Check console for details.") return {'CANCELLED'} try: img_settings.color_mode = preset_data.get("color_mode", "RGB") except TypeError: pass try: img_settings.color_depth = preset_data.get("color_depth", "8") except TypeError: pass try: img_settings.compression = preset_data.get("compression", 15) except (TypeError, AttributeError): pass try: img_settings.quality = preset_data.get("quality", 90) except (TypeError, AttributeError): pass # Apply color space for the image file if "color_management" in preset_data: try: img_settings.color_management = preset_data["color_management"] except (TypeError, AttributeError): pass if "linear_colorspace_settings" in preset_data: try: img_settings.linear_colorspace_settings.name = preset_data["linear_colorspace_settings"].get("name", "Linear") except (TypeError, AttributeError, KeyError): pass # Apply color management if "display_settings" in preset_data: try: scene.display_settings.display_device = preset_data["display_settings"].get("display_device", "sRGB") except TypeError: pass if "view_settings" in preset_data: vs = preset_data["view_settings"] try: scene.view_settings.view_transform = vs.get("view_transform", "Standard") except TypeError: pass try: scene.view_settings.look = vs.get("look", "None") except TypeError: pass try: scene.view_settings.exposure = vs.get("exposure", 0.0) except (TypeError, AttributeError): pass try: scene.view_settings.gamma = vs.get("gamma", 1.0) except (TypeError, AttributeError): pass if "sequencer_colorspace_settings" in preset_data: try: scene.sequencer_colorspace_settings.name = preset_data["sequencer_colorspace_settings"].get("name", "sRGB") except (TypeError, AttributeError, KeyError): pass # Apply format-specific settings if "ffmpeg" in preset_data and img_settings.file_format == 'FFMPEG': ff = preset_data["ffmpeg"] try: settings.ffmpeg.format = ff.get("format", "MPEG4") except TypeError: pass try: settings.ffmpeg.codec = ff.get("codec", "H264") except TypeError: pass try: settings.ffmpeg.video_bitrate = ff.get("video_bitrate", 6000) except (TypeError, AttributeError): pass try: settings.ffmpeg.audio_codec = ff.get("audio_codec", "AAC") except TypeError: pass try: settings.ffmpeg.audio_bitrate = ff.get("audio_bitrate", 192) except (TypeError, AttributeError): pass try: settings.ffmpeg.gopsize = ff.get("gopsize", 18) except (TypeError, AttributeError): pass try: settings.ffmpeg.constant_rate_factor = ff.get("constant_rate_factor", "MEDIUM") except TypeError: pass try: settings.ffmpeg.ffmpeg_preset = ff.get("ffmpeg_preset", "GOOD") except TypeError: pass self.report({'INFO'}, f"Preset '{preset.name}' loaded") return {'FINISHED'} class RENDER_OT_delete_preset(Operator): bl_idname = "render.delete_output_preset" bl_label = "Delete Preset" bl_description = "Delete selected preset" def execute(self, context): scene = context.scene presets = scene.render_output_presets idx = scene.render_output_presets_index if idx < 0 or idx >= len(presets): self.report({'WARNING'}, "No preset selected") return {'CANCELLED'} preset_name = presets[idx].name presets.remove(idx) if scene.render_output_presets_index >= len(presets): scene.render_output_presets_index = len(presets) - 1 # Save to file save_presets_to_file(context) self.report({'INFO'}, f"Preset '{preset_name}' deleted") return {'FINISHED'} class RENDER_OT_refresh_presets(Operator): bl_idname = "render.refresh_output_presets" bl_label = "Refresh Presets" bl_description = "Reload presets from file" def execute(self, context): load_presets_from_file(context) self.report({'INFO'}, "Presets refreshed") return {'FINISHED'} class RENDER_OT_copy_preset_path(Operator): bl_idname = "render.copy_preset_path" bl_label = "Copy Path" bl_description = "Copy preset folder path to clipboard" def execute(self, context): filepath = get_presets_filepath() folder_path = os.path.dirname(filepath) context.window_manager.clipboard = folder_path self.report({'INFO'}, "Folder path copied to clipboard") return {'FINISHED'} class RENDER_OT_open_preset_folder(Operator): bl_idname = "render.open_preset_folder" bl_label = "Open Folder" bl_description = "Open preset folder in system explorer" def execute(self, context): filepath = get_presets_filepath() folder_path = os.path.dirname(filepath) # Ensure folder exists if not os.path.exists(folder_path): os.makedirs(folder_path, exist_ok=True) try: system = platform.system() if system == "Windows": os.startfile(folder_path) elif system == "Darwin": # macOS subprocess.Popen(["open", folder_path]) else: # Linux and other Unix-like subprocess.Popen(["xdg-open", folder_path]) self.report({'INFO'}, "Opened preset folder") except Exception as e: self.report({'ERROR'}, f"Failed to open folder: {e}") return {'CANCELLED'} return {'FINISHED'} class RENDER_PT_output_presets(Panel): bl_label = "Render Output Presets" bl_idname = "RENDER_PT_output_presets" bl_space_type = 'PROPERTIES' bl_region_type = 'WINDOW' bl_context = "output" def draw(self, context): layout = self.layout scene = context.scene # Show preset file location with copy button filepath = get_presets_filepath() box = layout.box() row = box.row() row.label(text="Presets saved to:", icon='FILE_FOLDER') row.operator("render.open_preset_folder", text="", icon='FILEBROWSER') row.operator("render.copy_preset_path", text="", icon='COPYDOWN') # Make path selectable by using a text property col = box.column(align=True) col.scale_y = 0.7 col.enabled = True col.active = True # Display full path as selectable text split = col.split(factor=0.01) split.label(text="") text_col = split.column() text_col.label(text=filepath) layout.separator() row = layout.row() row.template_list( "UI_UL_list", "render_presets", scene, "render_output_presets", scene, "render_output_presets_index", rows=4 ) col = row.column(align=True) col.operator("render.save_output_preset", icon='ADD', text="") col.operator("render.delete_output_preset", icon='REMOVE', text="") col.separator() col.operator("render.refresh_output_presets", icon='FILE_REFRESH', text="") if len(scene.render_output_presets) > 0: layout.operator("render.load_output_preset", icon='IMPORT') def load_presets_handler(dummy): """Handler to load presets after a file is loaded""" if bpy.context.scene: load_presets_from_file(bpy.context) def load_presets_on_startup(dummy): """Handler to load presets on startup - runs once then removes itself""" try: if bpy.context.scene: load_presets_from_file(bpy.context) # Remove this handler after first successful load if load_presets_on_startup in bpy.app.handlers.depsgraph_update_post: bpy.app.handlers.depsgraph_update_post.remove(load_presets_on_startup) except: pass def register(): bpy.utils.register_class(RenderPreset) bpy.utils.register_class(RENDER_OT_save_preset) bpy.utils.register_class(RENDER_OT_load_preset) bpy.utils.register_class(RENDER_OT_delete_preset) bpy.utils.register_class(RENDER_OT_refresh_presets) bpy.utils.register_class(RENDER_OT_copy_preset_path) bpy.utils.register_class(RENDER_OT_open_preset_folder) bpy.utils.register_class(RENDER_PT_output_presets) bpy.types.Scene.render_output_presets = CollectionProperty(type=RenderPreset) bpy.types.Scene.render_output_presets_index = IntProperty() # Add handler to load presets when opening a file if load_presets_handler not in bpy.app.handlers.load_post: bpy.app.handlers.load_post.append(load_presets_handler) # Add handler to load presets on depsgraph update (after scene is ready) if load_presets_on_startup not in bpy.app.handlers.depsgraph_update_post: bpy.app.handlers.depsgraph_update_post.append(load_presets_on_startup) def unregister(): # Remove handlers if load_presets_handler in bpy.app.handlers.load_post: bpy.app.handlers.load_post.remove(load_presets_handler) if load_presets_on_startup in bpy.app.handlers.depsgraph_update_post: bpy.app.handlers.depsgraph_update_post.remove(load_presets_on_startup) bpy.utils.unregister_class(RENDER_PT_output_presets) bpy.utils.unregister_class(RENDER_OT_open_preset_folder) bpy.utils.unregister_class(RENDER_OT_copy_preset_path) bpy.utils.unregister_class(RENDER_OT_refresh_presets) bpy.utils.unregister_class(RENDER_OT_delete_preset) bpy.utils.unregister_class(RENDER_OT_load_preset) bpy.utils.unregister_class(RENDER_OT_save_preset) bpy.utils.unregister_class(RenderPreset) del bpy.types.Scene.render_output_presets del bpy.types.Scene.render_output_presets_index if __name__ == "__main__": register()