Created
February 2, 2025 00:14
-
-
Save bandinopla/86995dc9112e6c657e89c611b1c7a417 to your computer and use it in GitHub Desktop.
Blender Addon to pull images from directories outside of the .blend file's directory.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# | |
# will create a menu item in the File menu, when clicked it will track all image files pointing to files outside of the .blend file directory and pull them in in a folder. | |
# | |
bl_info = { | |
"name": "Pull External Images", | |
"blender": (2, 93, 0), # Adjust to the version you're using | |
"category": "File", | |
"author": "Bandinopla + ChatGPT", | |
"description": "Copy external images used by the blend file to a new directory and update material image paths.", | |
"version": (1, 0, 0), | |
"location": "File > Pull External Images", | |
} | |
import bpy | |
import shutil | |
import os | |
from pathlib import Path | |
# If the file is relative to us... | |
def is_near_us( us_path, target_path): | |
# Convert to Path objects and resolve to get absolute paths | |
base_path = Path(us_path).resolve() | |
target_path = Path(target_path).resolve() | |
# Check if the resolved target path is within the resolved base path | |
return base_path in target_path.parents | |
# For files pointing outside of the blendfile, the goal here is to put them in a folder called the same as the blend file + --textures | |
# If the file is close to us just leave it there. Since the goal is to move the images near us anyway... | |
def get_target_directory( blend_file_path, report, desiredFolderName=None ): | |
#blend_file_path = bpy.data.filepath | |
if not blend_file_path: | |
raise RuntimeError("No .blend file is open!") | |
# Current path of the blend file | |
blend_directory = os.path.dirname(blend_file_path) | |
blend_filename = os.path.splitext(os.path.basename(blend_file_path))[0] | |
goal_folder_name = desiredFolderName or f"{blend_filename}--textures" | |
target_dir = os.path.join(blend_directory, goal_folder_name) | |
# Ensure the target directory exists | |
if not os.path.exists(target_dir): | |
os.makedirs(target_dir) | |
report(f"Created folder {target_dir}") | |
return (blend_directory, target_dir, goal_folder_name) | |
# This function updates the material's image texture paths | |
def update_material_image_paths( ok_dir, target_dir, report): | |
report("Updating material nodes...") | |
for material in bpy.data.materials: | |
# Ensure the material has nodes enabled | |
if material.use_nodes: | |
for node in material.node_tree.nodes: | |
# Check if the node is an image texture | |
if node.type == 'TEX_IMAGE': | |
image = node.image | |
if image: | |
# Skip if the image is embedded (internal to .blend) | |
if image.source == 'FILE': | |
continue # Skip embedded images | |
image_path = bpy.path.abspath( image.filepath ) | |
if not is_near_us( ok_dir, image_path ): # Image is outside the blend file directory | |
# Get the image file name and make a new path in the target directory | |
filename = os.path.basename(image_path) | |
new_image_path = os.path.join(target_dir, filename) | |
new_relative_file = os.path.join("//", os.path.basename(target_dir), filename) | |
node.image.filepath = new_relative_file | |
report(f"Update material '{material.name}' to use: {new_relative_file}") | |
def copy_outside_images(ok_dir, target_dir, report): | |
report("Scanning for images used in the blend file...") | |
# Iterate through all images used in the .blend file and copy them | |
for image in bpy.data.images: | |
if image.is_dirty: # Skip unsaved images | |
continue | |
image_path = bpy.path.abspath(image.filepath) | |
if not is_near_us( ok_dir, image_path ): # Image is outside the blend file directory | |
if os.path.exists(image_path): | |
filename = os.path.basename(image_path) | |
new_image_path = os.path.join(target_dir, filename) | |
if not os.path.exists(new_image_path): | |
# Copy the image to the new directory | |
report(f"COPY {image_path} ---TO---> {new_image_path}") | |
shutil.copy(image_path, new_image_path) | |
# Operator to pull and copy external images and update materials | |
class OBJECT_OT_pull_external_images(bpy.types.Operator): | |
bl_idname = "file.pull_external_images" | |
bl_label = "Pull External Images" | |
bl_options = {'REGISTER', 'UNDO'} | |
goalFolderName: bpy.props.StringProperty(name="Folder name (to put external images)") | |
def invoke(self, context, event): | |
report = lambda msg, level='INFO': self.report({level}, msg) | |
dirs = get_target_directory( bpy.data.filepath, report ) | |
self.goalFolderName = dirs[2] | |
# Display the dialog for input | |
return context.window_manager.invoke_props_dialog(self) | |
def execute(self, context): | |
self.report({'INFO'}, f"*****************") | |
# Get the target directory based on the Blender file name | |
report = lambda msg, level='INFO': self.report({level}, msg) | |
try: | |
dirs = get_target_directory( bpy.data.filepath, report, self.goalFolderName ) | |
report(f"Pulling images outside of '{dirs[0]}' to be inside of our goal: '{dirs[1]}'") | |
copy_outside_images( dirs[0], dirs[1], report) | |
update_material_image_paths( dirs[0], dirs[1], report ) | |
report("̣\nAll done! All images are now relative to the .blend file\n") | |
except Exception as e: | |
self.report({'ERROR'}, f"Oops! {str(e)}") | |
return {'FINISHED'} | |
# Menu to add the option in the File menu | |
def menu_func(self, context): | |
layout = self.layout | |
# Adding a separator to place the operator visually below other menu items | |
layout.separator() # This adds a visual break before the operator appears | |
# Adding the operator in the File menu after the separator | |
layout.operator(OBJECT_OT_pull_external_images.bl_idname) | |
# Register the addon | |
def register(): | |
bpy.utils.register_class(OBJECT_OT_pull_external_images) | |
bpy.types.TOPBAR_MT_file.append(menu_func) | |
# Unregister the addon | |
def unregister(): | |
bpy.utils.unregister_class(OBJECT_OT_pull_external_images) | |
bpy.types.TOPBAR_MT_file.remove(menu_func) | |
if __name__ == "__main__": | |
register() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment