2023-09-28 11:34:41 +02:00
|
|
|
import bpy
|
|
|
|
from bpy_extras.io_utils import ImportHelper
|
|
|
|
|
|
|
|
import sys
|
|
|
|
import os
|
|
|
|
import json
|
2023-09-28 11:46:51 +02:00
|
|
|
#import psd_tools
|
2023-09-28 11:34:41 +02:00
|
|
|
from pathlib import Path
|
|
|
|
from time import time
|
2023-09-28 12:54:37 +02:00
|
|
|
from . import core
|
2023-09-28 11:46:51 +02:00
|
|
|
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
|
|
|
|
'''
|
|
|
|
|
2023-09-28 11:46:51 +02:00
|
|
|
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
|
2023-09-28 12:54:37 +02:00
|
|
|
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"}
|
|
|
|
|
|
|
|
|
2023-09-28 12:54:37 +02:00
|
|
|
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)
|