background_plane_manager/export_psd_layers.py

283 lines
9.0 KiB
Python
Raw Normal View History

2023-09-28 11:34:41 +02:00
import bpy
from bpy_extras.io_utils import ImportHelper
import sys
import os
import json
#import psd_tools
2023-09-28 11:34:41 +02:00
from pathlib import Path
from time import time
from . import core
from . file_utils import install_module
2023-09-28 11:34:41 +02:00
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')
2023-09-28 11:34:41 +02:00
# 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
2023-09-28 11:34:41 +02:00
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,
2023-09-28 11:34:41 +02:00
)
def register():
for cls in classes:
bpy.utils.register_class(cls)
def unregister():
for cls in reversed(classes):
bpy.utils.unregister_class(cls)