175 lines
7.0 KiB
Python
175 lines
7.0 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
PSD FX Test Generator v3 - lrFX (legacy) based
|
|
Uses ImageMagick for correct PSD base, patches in lrFX effects.
|
|
"""
|
|
import struct, os, subprocess
|
|
from PIL import Image
|
|
|
|
OUTPUT_DIR = "/home/hc/Pictures/_psd_stil_test"
|
|
W, H = 512, 512
|
|
PREFIX = "fx_"
|
|
|
|
def create_l_shape():
|
|
img = Image.new('RGBA', (W, H), (0, 0, 0, 0))
|
|
for y in range(H):
|
|
for x in range(W):
|
|
if (100 <= x < 380) and (180 <= y < 230):
|
|
img.putpixel((x, y), (70, 130, 180, 255))
|
|
elif (140 <= x < 190) and (180 <= y < 400):
|
|
img.putpixel((x, y), (70, 130, 180, 255))
|
|
return img
|
|
|
|
def create_diamond():
|
|
img = Image.new('RGBA', (W, H), (0, 0, 0, 0))
|
|
cx, cy, rx, ry = 256, 250, 160, 120
|
|
for y in range(H):
|
|
for x in range(W):
|
|
if abs(x - cx) / rx + abs(y - cy) / ry <= 1.0:
|
|
img.putpixel((x, y), (180, 80, 60, 255))
|
|
return img
|
|
|
|
def create_triangle():
|
|
img = Image.new('RGBA', (W, H), (0, 0, 0, 0))
|
|
x0, y0, x1, y1, x2, y2 = 200, 140, 380, 320, 150, 350
|
|
def sign(px, py, ax, ay, bx, by):
|
|
return (px - bx) * (ay - by) - (ax - bx) * (py - by)
|
|
for y in range(H):
|
|
for x in range(W):
|
|
d1 = sign(x, y, x0, y0, x1, y1)
|
|
d2 = sign(x, y, x1, y1, x2, y2)
|
|
d3 = sign(x, y, x2, y2, x0, y0)
|
|
if not ((d1 < 0 or d2 < 0 or d3 < 0) and (d1 > 0 or d2 > 0 or d3 > 0)):
|
|
img.putpixel((x, y), (60, 160, 80, 255))
|
|
return img
|
|
|
|
# === lrFX builders ===
|
|
|
|
def psd_color(r, g, b):
|
|
return struct.pack('>H', 0) + struct.pack('>HHHH', r*256, g*256, b*256, 0)
|
|
|
|
def common_state():
|
|
return struct.pack('>I', 7) + struct.pack('>I', 0) + b'\x01\x00\x00'
|
|
|
|
def lrfx_drop_shadow(color=(0,0,0), opacity=0.75, angle=135, distance=15, size=10):
|
|
d = struct.pack('>I', 41) + struct.pack('>I', 0)
|
|
d += struct.pack('>i', size) + struct.pack('>i', 0) + struct.pack('>i', int(angle*65536)) + struct.pack('>i', int(distance*65536))
|
|
d += psd_color(*color) + b'8BIMnorm' + b'\x01\x01' + bytes([int(opacity*255)])
|
|
return b'8BIMdsdw' + d
|
|
|
|
def lrfx_inner_shadow(color=(0,0,0), opacity=0.75, angle=135, distance=10, size=8):
|
|
d = struct.pack('>I', 41) + struct.pack('>I', 0)
|
|
d += struct.pack('>i', size) + struct.pack('>i', 0) + struct.pack('>i', int(angle*65536)) + struct.pack('>i', int(distance*65536))
|
|
d += psd_color(*color) + b'8BIMnorm' + b'\x01\x01' + bytes([int(opacity*255)])
|
|
return b'8BIMisdw' + d
|
|
|
|
def lrfx_outer_glow(color=(255,200,0), opacity=0.75, size=20):
|
|
d = struct.pack('>I', 32) + struct.pack('>I', 0)
|
|
d += struct.pack('>i', size) + struct.pack('>i', 0) + psd_color(*color)
|
|
d += b'8BIMscrn' + b'\x01' + bytes([int(opacity*255)])
|
|
return b'8BIMoglw' + d
|
|
|
|
def lrfx_inner_glow(color=(255,100,0), opacity=0.80, size=15):
|
|
d = struct.pack('>I', 33) + struct.pack('>I', 0)
|
|
d += struct.pack('>i', size) + struct.pack('>i', 0) + psd_color(*color)
|
|
d += b'8BIMscrn' + b'\x01' + bytes([int(opacity*255)]) + b'\x00'
|
|
return b'8BIMiglw' + d
|
|
|
|
def lrfx_bevel(style_val, angle=120, depth=200, size=10):
|
|
d = struct.pack('>I', 58) + struct.pack('>I', 0)
|
|
d += struct.pack('>i', int(angle*65536)) + struct.pack('>i', depth) + struct.pack('>i', size)
|
|
d += b'8BIMscrn' + b'8BIMmul ' # hl_blend, sh_blend
|
|
d += psd_color(255,255,255) + psd_color(0,0,0) # hl_color, sh_color
|
|
d += bytes([style_val, int(0.75*255), int(0.75*255), 1, 1, 0])
|
|
return b'8BIMbevl' + d
|
|
|
|
def lrfx_color_overlay(color=(220,50,50), opacity=0.80):
|
|
d = struct.pack('>I', 34) + struct.pack('>I', 2)
|
|
d += b'8BIMnorm' + psd_color(*color)
|
|
d += bytes([int(opacity*255), 1]) + psd_color(*color)
|
|
return b'8BIMsofi' + d
|
|
|
|
def build_lrfx_block(effects):
|
|
buf = bytearray()
|
|
buf.extend(struct.pack('>I', 0)) # version
|
|
buf.extend(struct.pack('>H', len(effects)))
|
|
for e in effects:
|
|
buf.extend(e)
|
|
return bytes(buf)
|
|
|
|
# === PSD patcher ===
|
|
|
|
def patch_psd_with_lrfx(base_psd_path, out_path, lrfx_data):
|
|
data = open(base_psd_path, 'rb').read()
|
|
pos = 26
|
|
cm = struct.unpack('>I', data[pos:pos+4])[0]; pos += 4 + cm
|
|
ir = struct.unpack('>I', data[pos:pos+4])[0]; pos += 4 + ir
|
|
lami_pos = pos
|
|
lami_len = struct.unpack('>I', data[pos:pos+4])[0]; pos += 4
|
|
li_len_pos = pos
|
|
li_len = struct.unpack('>I', data[pos:pos+4])[0]; pos += 4
|
|
lc = struct.unpack('>h', data[pos:pos+2])[0]; pos += 2
|
|
pos += 16
|
|
ch_cnt = struct.unpack('>H', data[pos:pos+2])[0]; pos += 2
|
|
for c in range(ch_cnt): pos += 6
|
|
pos += 8; pos += 4
|
|
extra_len_pos = pos
|
|
extra_len = struct.unpack('>I', data[pos:pos+4])[0]; pos += 4
|
|
extra_start = pos
|
|
|
|
tagged = bytearray(b'8BIMlrFX')
|
|
tagged.extend(struct.pack('>I', len(lrfx_data)))
|
|
tagged.extend(lrfx_data)
|
|
if len(tagged) % 2: tagged.append(0)
|
|
|
|
new_extra_len = extra_len + len(tagged)
|
|
new_data = bytearray()
|
|
new_data.extend(data[:extra_start+extra_len])
|
|
new_data.extend(tagged)
|
|
new_data.extend(data[extra_start+extra_len:])
|
|
new_data[extra_len_pos:extra_len_pos+4] = struct.pack('>I', new_extra_len)
|
|
|
|
delta = len(tagged)
|
|
new_data[li_len_pos:li_len_pos+4] = struct.pack('>I', li_len + delta)
|
|
new_data[lami_pos:lami_pos+4] = struct.pack('>I', lami_len + delta)
|
|
|
|
with open(out_path, 'wb') as f: f.write(new_data)
|
|
return len(new_data)
|
|
|
|
# === Main ===
|
|
|
|
def main():
|
|
os.makedirs(OUTPUT_DIR, exist_ok=True)
|
|
img = create_l_shape()
|
|
img.save('/tmp/_fx_shape.png')
|
|
|
|
# Base PSD from ImageMagick
|
|
subprocess.run(['convert', '/tmp/_fx_shape.png', '/tmp/_fx_base.psd'], check=True, capture_output=True)
|
|
|
|
# All effects
|
|
for fname, label, fx_func in [
|
|
("drop_shadow", "Drop Shadow", lambda: build_lrfx_block([common_state(), lrfx_drop_shadow()])),
|
|
("inner_shadow", "Inner Shadow", lambda: build_lrfx_block([common_state(), lrfx_inner_shadow()])),
|
|
("outer_glow", "Outer Glow", lambda: build_lrfx_block([common_state(), lrfx_outer_glow()])),
|
|
("inner_glow", "Inner Glow", lambda: build_lrfx_block([common_state(), lrfx_inner_glow()])),
|
|
("bevel_inner", "Bevel Inner", lambda: build_lrfx_block([common_state(), lrfx_bevel(1)])),
|
|
("bevel_outer", "Bevel Outer", lambda: build_lrfx_block([common_state(), lrfx_bevel(2)])),
|
|
("emboss", "Emboss", lambda: build_lrfx_block([common_state(), lrfx_bevel(3)])),
|
|
("pillow_emboss", "Pillow Emboss", lambda: build_lrfx_block([common_state(), lrfx_bevel(4)])),
|
|
("color_overlay", "Color Overlay", lambda: build_lrfx_block([common_state(), lrfx_color_overlay()])),
|
|
]:
|
|
out = os.path.join(OUTPUT_DIR, f'{PREFIX}{fname}.psd')
|
|
lrfx = fx_func()
|
|
sz = patch_psd_with_lrfx('/tmp/_fx_base.psd', out, lrfx)
|
|
print(f' {PREFIX}{fname}.psd {sz}B')
|
|
|
|
# Baseline (no effects)
|
|
baseline = os.path.join(OUTPUT_DIR, f'{PREFIX}baseline.psd')
|
|
subprocess.run(['cp', '/tmp/_fx_base.psd', baseline], check=True)
|
|
print(f' {PREFIX}baseline.psd')
|
|
|
|
print(f'\nDone! {OUTPUT_DIR}')
|
|
|
|
if __name__ == '__main__':
|
|
main()
|