import bpy from bpy_extras.io_utils import ImportHelper import sys import os import json #import psd_tools from pathlib import Path from time import time from . import core from . file_utils import install_module def print_progress(progress, min=0, max=100, barlen=50, prefix='', suffix='', line_width=80): total_len = max - min progress_float = (progress - min) / total_len bar_progress = int(progress_float * barlen) * '=' bar_empty = (barlen - len(bar_progress)) * ' ' percentage = ''.join((str(int(progress_float * 100)), '%')) progress_string = ''.join((prefix, '[', bar_progress, bar_empty, ']', ' ', percentage, suffix))[:line_width] print_string = ''.join((progress_string, ' ' * (line_width - len(progress_string)))) print(print_string, end='\r') def export_psd(psd_file, output=None, scale=0.5): ''' export_psd(string psd_file) -> list layers, list bboxes, tuple image_size, string png_dir Reads psd_file and exports all top level layers to png's. Returns a list of all the layer objects, the image size and the png export directory. string psd_file - the filepath of the psd file ''' psd_tools = install_module('psd_tools', 'psd-tools') # def get_layers(layer, all_layers=[]): # if not layer.is_group(): # return # for sub_layer in reversed(layer): # reversed() since psd_tools 1.8 # all_layers.append(sub_layer) # get_layers(sub_layer, all_layers=all_layers) # return all_layers def get_dimensions(layer, bbox): if bbox is not None: # print('with bbox') # pp(bbox) x = layer.bbox[0] + bbox[0] y = layer.bbox[1] + bbox[1] width = bbox[2] - bbox[0] height = bbox[3] - bbox[1] else: # print('layer bbox') # pp(layer.bbox[:]) x = layer.bbox[0] y = layer.bbox[1] width = layer.bbox[2] - x height = layer.bbox[3] - y # print('x', x) # print('y', y) # print('width', width) # print('height', height) return x, y, width, height def export_layers_as_png(layers, output, crop=False, scale=0.5): output = Path(output) box = layers.viewbox all_layers = [] for i, layer in enumerate(layers): # if (layer.is_group() or (not self.hidden_layers and not layer.is_visible())): # continue layer.visible = True if layer.is_group() and 'GUIDE' in layer.name: for l in layer: #print(l.name, l.visible) l.visible = True name = layer.name norm_name = core.norm_str(name, padding=2) # Gadget normstr print('name: ', name) png_output = (output/norm_name).with_suffix('.png') print('Layer Output', png_output) prefix = ' - exporting: ' suffix = ' - {}'.format(layer.name) print_progress(i+1, max=(len(layers)), barlen=40, prefix=prefix, suffix=suffix, line_width=120) # if self.clean_name: # name = bpy.path.clean_name(layer.name).rstrip('_') # else: # name = layer.name.replace('\x00', '') # name = name.rstrip('_') # if self.layer_index_name: # name = name + '_' + str(i) # composite return a PIL object if crop: # get pre-crop size # layer_image = layer.topil() layer_image = layer.composite(viewport=box) bbox = layer_image.getbbox() ## TODO layer bbox might be completely off (when it's a group) image = layer.composite()# This crop the image else: image = layer.composite(viewport=box, force=True)# This crop to canvas size before getting bbox ?? bbox = None if not image: continue ## optimisation reduce size #if scale != 1 : image = image.resize((image.width // int(1/scale), image.height // int(1/scale))) try: image.save(str(png_output)) except Exception as e: print(e) # if crop: # layer_box_tuple = get_dimensions(layer, bbox) # else: # layer_box_tuple = None lbbox = get_dimensions(layer, bbox) all_layers.append({'name': name, 'layer_bbox': lbbox, 'bbox': bbox, 'index': i, 'path': f'./{png_output.name}'}) ## No crop for now # try: # layer_image = layer.topil() # except ValueError: # print("Could not process layer " + layer.name) # bboxes.append(None) # continue # if layer_image is None: # bboxes.append(None) # continue # ## AUTOCROP # if self.crop_layers: # bbox = layer_image.getbbox() # bboxes.append(bbox) # # layer_image = layer_image.crop(bbox) # else: # bboxes.append(None) # layer_image.save(png_file) return all_layers print(f'Exporting: {psd_file}') psd_file = Path(psd_file) if not output: # export relative to psd location output = psd_file.parent / 'render' # f'{psd_file.stem}_pngs' output.mkdir(exist_ok = True) psd = psd_tools.PSDImage.open(psd_file) ## get all layer separately # layers = get_layers(psd) # bboxes = export_layers_as_png(layers, png_dir) ## export the main image (PSD, MAIN, COMPOSITE ?) image = psd.composite() org_image_size = [image.width, image.height] image = image.resize((image.width // 2, image.height // 2)) image.save(str((output / 'main.png'))) ## export top level layer passing directly psd all_layers = export_layers_as_png(psd, output, crop=False, scale=scale) bb = psd.bbox image_size = (bb[2] - bb[0], bb[3] - bb[1]) # return ([l.name for l in psd], bboxes, image_size, output) return (all_layers, image_size, org_image_size, output) def export_psd_bg(psd_fp, scale=0.5): '''Export layers of the psd, create and return a json file psd_fp :: filepath to the psd file scale :: output resolution of the layers (ex: 0.5 means divided by 2) ''' t0 = time() psd_fp = Path(psd_fp) ## FIXME: Choose how to handle this nomenclature output = psd_fp.parent / 'render' if scale >= 1: output = psd_fp.parent / 'render_hd' # print('Folder Output', output) ## note: No output passed create 'render' folder aside psd automatically layers, image_size, org_image_size, png_dir = export_psd(psd_fp, output, scale=scale) json_fp = png_dir/'setup.json' setup_dic = { 'psd': f'../{psd_fp.name}' ,'layers': layers, 'image_size': image_size, 'org_image_size' : org_image_size, } ## Dump data to json file json_file = png_dir / 'setup.json' # json.dump(str(json_file), setup_dic, ensure_ascii=False) json_file.write_text(json.dumps(setup_dic, indent=4, ensure_ascii=False), encoding='utf8') print(f'Exported in : {time() - t0:.2f}s') return json_fp class BPM_OT_export_psd_layers(bpy.types.Operator, ImportHelper): bl_idname = "bpm.export_psd_layers" bl_label = "Export Psd Layers" bl_description = "Export psd layers from a psd file in a render folder" bl_options = {"REGISTER", "INTERNAL"} # path_to_pal : bpy.props.StringProperty(name="paht to palette", description="path to the palette", default="") @classmethod def poll(cls, context): return context.object and context.object.type == 'GPENCIL' filename_ext = '.psd' filter_glob: bpy.props.StringProperty(default='*.psd', options={'HIDDEN'} ) filepath : bpy.props.StringProperty( name="File Path", description="File path used for import", maxlen= 1024) resolution_scale: bpy.props.FloatProperty( name='Resolution Scale', default=1.0, description='Export resolution ratio. Ex: 0.5 export at half resolution\ \nusefull for non-def lightweight backgrounds in Blender', options={'HIDDEN'}) def execute(self, context): json_fp = export_psd_bg(self.filepath, scale=self.resolution_scale) if not json_fp: self.report({'ERROR'}, f'Problem when exporting image for: {self.filepath}') return {'CANCELLED'} self.report({'INFO'}, f'BG exported, Json at: {json_fp}') return {"FINISHED"} classes = ( BPM_OT_export_psd_layers, ) def register(): for cls in classes: bpy.utils.register_class(cls) def unregister(): for cls in reversed(classes): bpy.utils.unregister_class(cls)