init
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
import subprocess
|
||||
import glob
|
||||
|
||||
TASKS = [
|
||||
("_tmp/kra_tunes_simple_black/drop_shadow_kra_set", "_tmp/kra_tunes_simple_black/drop_shadow_png_kra_set"),
|
||||
("_tmp/kra_tunes_simple_red/drop_shadow_kra_set", "_tmp/kra_tunes_simple_red/drop_shadow_png_kra_set"),
|
||||
("_tmp/kra_tunes_simple_black/outer_glow_kra_set", "_tmp/kra_tunes_simple_black/outer_glow_png_kra_set"),
|
||||
("_tmp/kra_tunes_simple_red/outer_glow_kra_set", "_tmp/kra_tunes_simple_red/outer_glow_png_kra_set"),
|
||||
("_tmp/kra_tunes_simple_black/inner_glow_kra_set", "_tmp/kra_tunes_simple_black/inner_glow_png_kra_set"),
|
||||
("_tmp/kra_tunes_simple_red/inner_glow_kra_set", "_tmp/kra_tunes_simple_red/inner_glow_png_kra_set"),
|
||||
("_tmp/kra_tunes_simple_black/bevel_emboss_kra_set", "_tmp/kra_tunes_simple_black/bevel_emboss_png_kra_set"),
|
||||
("_tmp/kra_tunes_simple_red/bevel_emboss_kra_set", "_tmp/kra_tunes_simple_red/bevel_emboss_png_kra_set"),
|
||||
]
|
||||
|
||||
def main():
|
||||
# Check if Krita exists
|
||||
try:
|
||||
subprocess.run(["krita", "--version"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
except FileNotFoundError:
|
||||
print("Error: 'krita' executable not found in your PATH.")
|
||||
print("Please install Krita or run this script in an environment where Krita is available.")
|
||||
return
|
||||
|
||||
for kra_dir, png_dir in TASKS:
|
||||
if not os.path.exists(kra_dir):
|
||||
print(f"Directory {kra_dir} does not exist. Skipping.")
|
||||
continue
|
||||
|
||||
kra_files = glob.glob(os.path.join(kra_dir, "*.kra"))
|
||||
if not kra_files:
|
||||
print(f"No .kra files found in {kra_dir}. Skipping.")
|
||||
continue
|
||||
|
||||
os.makedirs(png_dir, exist_ok=True)
|
||||
print(f"Found {len(kra_files)} KRA files in {kra_dir}. Exporting to {png_dir}...")
|
||||
|
||||
count = 0
|
||||
for kra_path in kra_files:
|
||||
filename = os.path.basename(kra_path)
|
||||
png_name = filename.replace(".kra", ".png")
|
||||
png_path = os.path.join(png_dir, png_name)
|
||||
|
||||
if os.path.exists(png_path):
|
||||
continue
|
||||
|
||||
print(f" Exporting {filename} -> {png_name}...")
|
||||
cmd = ["krita", kra_path, "--export", "--export-filename", png_path]
|
||||
|
||||
success = False
|
||||
for attempt in range(2):
|
||||
try:
|
||||
res = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=10)
|
||||
if res.returncode == 0:
|
||||
count += 1
|
||||
success = True
|
||||
break
|
||||
else:
|
||||
print(f" FAILED to export {filename}: {res.stderr.decode('utf-8', 'ignore')}")
|
||||
break
|
||||
except subprocess.TimeoutExpired:
|
||||
print(f" TIMEOUT on {filename} (attempt {attempt+1}), retrying...")
|
||||
# Kill any lingering krita processes just in case
|
||||
os.system("killall -9 krita 2>/dev/null")
|
||||
|
||||
if not success:
|
||||
print(f" Skipping {filename} after failure/timeout.")
|
||||
|
||||
print(f"Done with {kra_dir}. Exported {count} new files.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,47 @@
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
import subprocess
|
||||
import glob
|
||||
|
||||
KRA_DIR = "_tmp/drop_shadow_kra_set"
|
||||
PNG_DIR = "_tmp/drop_shadow_png_kra_set"
|
||||
|
||||
def main():
|
||||
kra_files = glob.glob(os.path.join(KRA_DIR, "*.kra"))
|
||||
if not kra_files:
|
||||
print(f"No .kra files found in {KRA_DIR}. Run generate_kra_set.py first.")
|
||||
return
|
||||
|
||||
os.makedirs(PNG_DIR, exist_ok=True)
|
||||
print(f"Found {len(kra_files)} KRA files. Beginning headless export via Krita CLI...")
|
||||
|
||||
# Check if Krita exists
|
||||
try:
|
||||
subprocess.run(["krita", "--version"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
except FileNotFoundError:
|
||||
print("Error: 'krita' executable not found in your PATH.")
|
||||
print("Please install Krita or run this script in an environment where Krita is available.")
|
||||
return
|
||||
|
||||
count = 0
|
||||
for kra_path in kra_files:
|
||||
filename = os.path.basename(kra_path)
|
||||
png_name = filename.replace(".kra", ".png")
|
||||
png_path = os.path.join(PNG_DIR, png_name)
|
||||
|
||||
if os.path.exists(png_path):
|
||||
continue
|
||||
|
||||
print(f"Exporting {filename} -> {png_name}...")
|
||||
# Krita headless export command
|
||||
cmd = ["krita", kra_path, "--export", "--export-filename", png_path]
|
||||
res = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
if res.returncode == 0:
|
||||
count += 1
|
||||
else:
|
||||
print(f" FAILED to export {filename}: {res.stderr.decode()}")
|
||||
|
||||
print(f"Done. Successfully exported {count} files to {PNG_DIR}.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,148 @@
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
import struct
|
||||
import zipfile
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
# Offsets for BevelEmboss style keys inside sultan.kra's Unnamed/annotations/layerstyles.asl:
|
||||
# lagl (local angle): 747
|
||||
# Lald (local altitude): 768
|
||||
# srgR (depth): 790
|
||||
# blur (size): 819
|
||||
# bvlD (direction enum value, e.g. b"In " or b"Out "): 851
|
||||
# Sftn (soften): 1094
|
||||
# hglO (highlight opacity): 512
|
||||
# sdwO (shadow opacity): 652
|
||||
# hglC (highlight R/G/B doubles): 438, 458, 478
|
||||
# sdwC (shadow R/G/B doubles): 590, 610, 630
|
||||
# hglM (highlight blend mode classID): 402
|
||||
# sdwM (shadow blend mode classID): 544
|
||||
|
||||
BASE_KRA = "/mnt/extra/00_PROJECTS/hcie-rust-v4/_tmp/kra_tunes_simple/black_rect_simple.kra"
|
||||
OUT_DIR = "/mnt/extra/00_PROJECTS/hcie-rust-v4/_tmp/kra_tunes_simple_black/bevel_emboss_kra_set"
|
||||
|
||||
def get_double_bytes(val):
|
||||
return struct.pack(">d", float(val))
|
||||
|
||||
def main():
|
||||
bases = [
|
||||
("/mnt/extra/00_PROJECTS/hcie-rust-v4/_tmp/kra_tunes_simple/black_rect_simple.kra", "/mnt/extra/00_PROJECTS/hcie-rust-v4/_tmp/kra_tunes_simple_black/bevel_emboss_kra_set"),
|
||||
("/mnt/extra/00_PROJECTS/hcie-rust-v4/_tmp/kra_tunes_simple/red_rect_simple.kra", "/mnt/extra/00_PROJECTS/hcie-rust-v4/_tmp/kra_tunes_simple_red/bevel_emboss_kra_set")
|
||||
]
|
||||
|
||||
for base_kra, out_dir in bases:
|
||||
if not os.path.exists(base_kra):
|
||||
print(f"Error: Base Krita file {base_kra} not found.")
|
||||
continue
|
||||
|
||||
os.makedirs(out_dir, exist_ok=True)
|
||||
|
||||
# Read base archive and cache entries in memory
|
||||
base_zip = zipfile.ZipFile(base_kra, 'r')
|
||||
files_cache = {}
|
||||
for name in base_zip.namelist():
|
||||
files_cache[name] = base_zip.read(name)
|
||||
|
||||
asl_path_in_zip = "Unnamed/annotations/layerstyles.asl"
|
||||
if asl_path_in_zip in files_cache:
|
||||
asl_data = bytearray(files_cache[asl_path_in_zip])
|
||||
else:
|
||||
fallback_paths = [
|
||||
"/mnt/extra/00_PROJECTS/hcie-rust-v4/_images/kra_style_test/sultan.kra",
|
||||
"/mnt/extra/00_PROJECTS/hcie-rust-v4/_images/_psd_stil_test/sultan_test/sultan.kra",
|
||||
"_images/kra_style_test/sultan.kra",
|
||||
"_images/_psd_stil_test/sultan_test/sultan.kra"
|
||||
]
|
||||
sultan_zip = None
|
||||
for path in fallback_paths:
|
||||
if os.path.exists(path):
|
||||
sultan_zip = zipfile.ZipFile(path, 'r')
|
||||
break
|
||||
if sultan_zip is None:
|
||||
print("Error: Could not find sultan.kra fallback to load layerstyles.asl")
|
||||
return
|
||||
asl_data = bytearray(sultan_zip.read(asl_path_in_zip))
|
||||
sultan_zip.close()
|
||||
|
||||
xml_data = files_cache["maindoc.xml"]
|
||||
|
||||
# Set visibility in XML: both target layer and Background should be visible
|
||||
# Also ensure the layerstyle attribute is attached to the target layer
|
||||
ET.register_namespace('', "http://www.calligra.org/DTD/krita")
|
||||
root = ET.fromstring(xml_data)
|
||||
for layer in root.findall('.//{http://www.calligra.org/DTD/krita}layer'):
|
||||
if layer.attrib.get('name') in ['Layer 2', 'Red Rectangle']:
|
||||
layer.set('visible', '1')
|
||||
layer.set('layerstyle', '{4bbe8f2c-7ad2-4e31-8af0-9ad6715cacf4}')
|
||||
elif layer.attrib.get('name') == 'Background':
|
||||
layer.set('visible', '1')
|
||||
else:
|
||||
layer.set('visible', '0')
|
||||
xml_patched = b'<?xml version="1.0" encoding="UTF-8"?>\n<!DOCTYPE DOC PUBLIC \'-//KDE//DTD krita 2.0//EN\' \'http://www.calligra.org/DTD/krita-2.0.dtd\'>\n' + ET.tostring(root, encoding='utf-8')
|
||||
|
||||
# Parameter lists
|
||||
sizes = [5.0, 10.0, 20.0, 30.0, 50.0]
|
||||
softens = [0.0, 4.0, 8.0, 16.0]
|
||||
depths = [25.0, 50.0, 75.0, 100.0]
|
||||
angles = [90.0, 120.0]
|
||||
directions = [
|
||||
(b"In ", "up"),
|
||||
(b"Out ", "down"),
|
||||
]
|
||||
|
||||
total = len(sizes) * len(softens) * len(depths) * len(angles) * len(directions)
|
||||
print(f"Generating {total} styled KRA files for Bevel & Emboss from {os.path.basename(base_kra)}...")
|
||||
|
||||
def patch_untf(asl, key, value):
|
||||
idx = asl.find(key)
|
||||
if idx != -1 and asl[idx+4:idx+8] == b'UntF':
|
||||
asl[idx+12:idx+20] = get_double_bytes(value)
|
||||
|
||||
def patch_enum(asl, key, value):
|
||||
idx = asl.find(key)
|
||||
if idx != -1 and asl[idx+4:idx+8] == b'enum':
|
||||
asl[idx+20:idx+24] = value
|
||||
|
||||
count = 0
|
||||
for size in sizes:
|
||||
for soften in softens:
|
||||
for depth in depths:
|
||||
for angle in angles:
|
||||
for dir_bytes, dir_name in directions:
|
||||
count += 1
|
||||
filename = f"be_s{int(size)}_sf{int(soften)}_d{int(depth)}_a{int(angle)}_{dir_name}.kra"
|
||||
kra_path = os.path.join(out_dir, filename)
|
||||
|
||||
# Clone and patch ASL
|
||||
patched_asl = bytearray(asl_data)
|
||||
|
||||
patch_untf(patched_asl, b'blur', size)
|
||||
patch_untf(patched_asl, b'Sftn', soften)
|
||||
patch_untf(patched_asl, b'srgR', depth)
|
||||
patch_untf(patched_asl, b'lagl', angle)
|
||||
patch_enum(patched_asl, b'bvlD', dir_bytes)
|
||||
|
||||
# Write new ZIP archive
|
||||
with zipfile.ZipFile(kra_path, 'w') as out_zip:
|
||||
has_asl = False
|
||||
for item in base_zip.infolist():
|
||||
if item.filename == "Unnamed/annotations/layerstyles.asl":
|
||||
out_zip.writestr(item, patched_asl)
|
||||
has_asl = True
|
||||
elif item.filename == "maindoc.xml":
|
||||
out_zip.writestr(item, xml_patched)
|
||||
elif item.filename == "Unnamed/layers/layer6.defaultpixel":
|
||||
out_zip.writestr(item, b"\x80\x80\x80\xff") # 50% gray background
|
||||
else:
|
||||
out_zip.writestr(item, files_cache[item.filename])
|
||||
if not has_asl:
|
||||
out_zip.writestr("Unnamed/annotations/layerstyles.asl", patched_asl)
|
||||
|
||||
if count % 10 == 0:
|
||||
print(f" [{count}/{total}] generated...")
|
||||
|
||||
base_zip.close()
|
||||
print(f"Successfully generated {count} files under {out_dir}.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,160 @@
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
import struct
|
||||
import zipfile
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
# Offsets for InnerGlow style keys inside sultan.kra's Unnamed/annotations/layerstyles.asl:
|
||||
# Md (blend mode): 1510
|
||||
# Clr (color) RGBC double offsets:
|
||||
# R double: 1556
|
||||
# G double: 1576
|
||||
# B double: 1596
|
||||
# Opct (opacity): 1620
|
||||
# Ckmt (choke/spread): 1672
|
||||
# blur (size): 1696
|
||||
|
||||
BASE_KRA = "_images/_psd_stil_test/sultan_test/sultan.kra"
|
||||
OUT_DIR = "_tmp/inner_glow_kra_set"
|
||||
|
||||
def get_double_bytes(val):
|
||||
return struct.pack(">d", float(val))
|
||||
|
||||
def main():
|
||||
bases = [
|
||||
("/mnt/extra/00_PROJECTS/hcie-rust-v4/_tmp/kra_tunes_simple/black_rect_simple.kra", "/mnt/extra/00_PROJECTS/hcie-rust-v4/_tmp/kra_tunes_simple_black/inner_glow_kra_set"),
|
||||
("/mnt/extra/00_PROJECTS/hcie-rust-v4/_tmp/kra_tunes_simple/red_rect_simple.kra", "/mnt/extra/00_PROJECTS/hcie-rust-v4/_tmp/kra_tunes_simple_red/inner_glow_kra_set")
|
||||
]
|
||||
|
||||
for base_kra, out_dir in bases:
|
||||
if not os.path.exists(base_kra):
|
||||
print(f"Error: Base Krita file {base_kra} not found.")
|
||||
continue
|
||||
|
||||
os.makedirs(out_dir, exist_ok=True)
|
||||
|
||||
# Read base archive and cache entries in memory
|
||||
base_zip = zipfile.ZipFile(base_kra, 'r')
|
||||
files_cache = {}
|
||||
for name in base_zip.namelist():
|
||||
files_cache[name] = base_zip.read(name)
|
||||
|
||||
asl_path_in_zip = "Unnamed/annotations/layerstyles.asl"
|
||||
if asl_path_in_zip in files_cache:
|
||||
asl_data = bytearray(files_cache[asl_path_in_zip])
|
||||
else:
|
||||
fallback_paths = [
|
||||
"/mnt/extra/00_PROJECTS/hcie-rust-v4/_images/kra_style_test/sultan.kra",
|
||||
"/mnt/extra/00_PROJECTS/hcie-rust-v4/_images/_psd_stil_test/sultan_test/sultan.kra",
|
||||
"_images/kra_style_test/sultan.kra",
|
||||
"_images/_psd_stil_test/sultan_test/sultan.kra"
|
||||
]
|
||||
sultan_zip = None
|
||||
for path in fallback_paths:
|
||||
if os.path.exists(path):
|
||||
sultan_zip = zipfile.ZipFile(path, 'r')
|
||||
break
|
||||
if sultan_zip is None:
|
||||
print("Error: Could not find sultan.kra fallback to load layerstyles.asl")
|
||||
return
|
||||
asl_data = bytearray(sultan_zip.read(asl_path_in_zip))
|
||||
sultan_zip.close()
|
||||
|
||||
xml_data = files_cache["maindoc.xml"]
|
||||
|
||||
# Set visibility in XML: both target layer and Background should be visible
|
||||
# Also ensure the layerstyle attribute is attached to the target layer
|
||||
ET.register_namespace('', "http://www.calligra.org/DTD/krita")
|
||||
root = ET.fromstring(xml_data)
|
||||
for layer in root.findall('.//{http://www.calligra.org/DTD/krita}layer'):
|
||||
if layer.attrib.get('name') in ['Layer 3', 'Red Rectangle']:
|
||||
layer.set('visible', '1')
|
||||
layer.set('layerstyle', '{117f8514-88e4-4553-bccb-44a87b6b9182}')
|
||||
elif layer.attrib.get('name') == 'Background':
|
||||
layer.set('visible', '1')
|
||||
else:
|
||||
layer.set('visible', '0')
|
||||
xml_patched = b'<?xml version="1.0" encoding="UTF-8"?>\n<!DOCTYPE DOC PUBLIC \'-//KDE//DTD krita 2.0//EN\' \'http://www.calligra.org/DTD/krita-2.0.dtd\'>\n' + ET.tostring(root, encoding='utf-8')
|
||||
|
||||
# Parameter lists
|
||||
chokes = [0.0, 20.0]
|
||||
sizes = [10.0, 30.0]
|
||||
opacities = [0.75, 1.0]
|
||||
blend_modes = [
|
||||
(b"Nrml", "normal"),
|
||||
(b"Scrn", "screen"),
|
||||
(b"LDdg", "lineardodge"),
|
||||
]
|
||||
glow_colors = [
|
||||
([220, 30, 30, 255], "red"),
|
||||
([30, 180, 30, 255], "green"),
|
||||
]
|
||||
|
||||
total = len(chokes) * len(sizes) * len(opacities) * len(blend_modes) * len(glow_colors)
|
||||
print(f"Generating {total} styled KRA files for Inner Glow from {os.path.basename(base_kra)}...")
|
||||
|
||||
def patch_untf(asl, start_idx, key, value):
|
||||
idx = asl.find(key, start_idx)
|
||||
if idx != -1 and asl[idx+4:idx+8] == b'UntF':
|
||||
asl[idx+12:idx+20] = get_double_bytes(value)
|
||||
|
||||
def patch_enum(asl, start_idx, key, value):
|
||||
idx = asl.find(key, start_idx)
|
||||
if idx != -1 and asl[idx+4:idx+8] == b'enum':
|
||||
asl[idx+20:idx+24] = value
|
||||
|
||||
def patch_doub(asl, start_idx, key, value):
|
||||
idx = asl.find(key, start_idx)
|
||||
if idx != -1 and asl[idx+4:idx+8] == b'doub':
|
||||
asl[idx+8:idx+16] = get_double_bytes(value)
|
||||
|
||||
count = 0
|
||||
for choke in chokes:
|
||||
for size in sizes:
|
||||
for opacity in opacities:
|
||||
for bm_bytes, bm_name in blend_modes:
|
||||
for color, color_name in glow_colors:
|
||||
count += 1
|
||||
filename = f"ig_sp{int(choke)}_s{int(size)}_op{int(opacity*100)}_n0_{bm_name}_{color_name}.kra"
|
||||
kra_path = os.path.join(out_dir, filename)
|
||||
|
||||
# Clone and patch ASL
|
||||
patched_asl = bytearray(asl_data)
|
||||
|
||||
start_idx = patched_asl.find(b'IrGl')
|
||||
|
||||
patch_untf(patched_asl, start_idx, b'Opct', opacity * 100.0)
|
||||
patch_untf(patched_asl, start_idx, b'Ckmt', choke)
|
||||
patch_untf(patched_asl, start_idx, b'blur', size)
|
||||
patch_enum(patched_asl, start_idx, b'Md ', bm_bytes)
|
||||
|
||||
# Color: find Clr after start_idx, then Rd , Grn , Bl
|
||||
clr_idx = patched_asl.find(b'Clr ', start_idx)
|
||||
patch_doub(patched_asl, clr_idx, b'Rd ', color[0])
|
||||
patch_doub(patched_asl, clr_idx, b'Grn ', color[1])
|
||||
patch_doub(patched_asl, clr_idx, b'Bl ', color[2])
|
||||
|
||||
# Write new ZIP archive
|
||||
with zipfile.ZipFile(kra_path, 'w') as out_zip:
|
||||
has_asl = False
|
||||
for item in base_zip.infolist():
|
||||
if item.filename == "Unnamed/annotations/layerstyles.asl":
|
||||
out_zip.writestr(item, patched_asl)
|
||||
has_asl = True
|
||||
elif item.filename == "maindoc.xml":
|
||||
out_zip.writestr(item, xml_patched)
|
||||
elif item.filename == "Unnamed/layers/layer6.defaultpixel":
|
||||
out_zip.writestr(item, b"\x80\x80\x80\xff") # 50% gray background
|
||||
else:
|
||||
out_zip.writestr(item, files_cache[item.filename])
|
||||
if not has_asl:
|
||||
out_zip.writestr("Unnamed/annotations/layerstyles.asl", patched_asl)
|
||||
|
||||
if count % 20 == 0:
|
||||
print(f" [{count}/{total}] generated...")
|
||||
|
||||
base_zip.close()
|
||||
print(f"Successfully generated {count} files under {out_dir}.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,169 @@
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
import struct
|
||||
import zipfile
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
# Offsets for DropShadow style keys inside sultan.kra's Unnamed/annotations/layerstyles.asl:
|
||||
# Opct (opacity): 2504
|
||||
# lagl (local angle): 2541
|
||||
# Dstn (distance): 2565
|
||||
# Ckmt (choke/spread): 2589
|
||||
# blur (size): 2613
|
||||
# Md (blend mode value id): 2406
|
||||
# Clr (color) RGBC double offsets:
|
||||
# R double: 2448
|
||||
# G double: 2468
|
||||
# B double: 2488
|
||||
|
||||
BASE_KRA = "_images/_psd_stil_test/sultan_test/sultan.kra"
|
||||
OUT_DIR = "_tmp/drop_shadow_kra_set"
|
||||
|
||||
def get_double_bytes(val):
|
||||
return struct.pack(">d", float(val))
|
||||
|
||||
def main():
|
||||
bases = [
|
||||
("/mnt/extra/00_PROJECTS/hcie-rust-v4/_tmp/kra_tunes_simple/black_rect_simple.kra", "/mnt/extra/00_PROJECTS/hcie-rust-v4/_tmp/kra_tunes_simple_black/drop_shadow_kra_set"),
|
||||
("/mnt/extra/00_PROJECTS/hcie-rust-v4/_tmp/kra_tunes_simple/red_rect_simple.kra", "/mnt/extra/00_PROJECTS/hcie-rust-v4/_tmp/kra_tunes_simple_red/drop_shadow_kra_set")
|
||||
]
|
||||
|
||||
for base_kra, out_dir in bases:
|
||||
if not os.path.exists(base_kra):
|
||||
print(f"Error: Base Krita file {base_kra} not found.")
|
||||
continue
|
||||
|
||||
os.makedirs(out_dir, exist_ok=True)
|
||||
|
||||
# Read base archive and cache entries in memory
|
||||
base_zip = zipfile.ZipFile(base_kra, 'r')
|
||||
files_cache = {}
|
||||
for name in base_zip.namelist():
|
||||
files_cache[name] = base_zip.read(name)
|
||||
|
||||
asl_path_in_zip = "Unnamed/annotations/layerstyles.asl"
|
||||
if asl_path_in_zip in files_cache:
|
||||
asl_data = bytearray(files_cache[asl_path_in_zip])
|
||||
else:
|
||||
fallback_paths = [
|
||||
"/mnt/extra/00_PROJECTS/hcie-rust-v4/_images/kra_style_test/sultan.kra",
|
||||
"/mnt/extra/00_PROJECTS/hcie-rust-v4/_images/_psd_stil_test/sultan_test/sultan.kra",
|
||||
"_images/kra_style_test/sultan.kra",
|
||||
"_images/_psd_stil_test/sultan_test/sultan.kra"
|
||||
]
|
||||
sultan_zip = None
|
||||
for path in fallback_paths:
|
||||
if os.path.exists(path):
|
||||
sultan_zip = zipfile.ZipFile(path, 'r')
|
||||
break
|
||||
if sultan_zip is None:
|
||||
print("Error: Could not find sultan.kra fallback to load layerstyles.asl")
|
||||
return
|
||||
asl_data = bytearray(sultan_zip.read(asl_path_in_zip))
|
||||
sultan_zip.close()
|
||||
|
||||
xml_data = files_cache["maindoc.xml"]
|
||||
|
||||
# Set visibility in XML: both target layer and Background should be visible
|
||||
# Also ensure the layerstyle attribute is attached to the target layer
|
||||
ET.register_namespace('', "http://www.calligra.org/DTD/krita")
|
||||
root = ET.fromstring(xml_data)
|
||||
for layer in root.findall('.//{http://www.calligra.org/DTD/krita}layer'):
|
||||
if layer.attrib.get('name') in ['Layer 4', 'Red Rectangle']:
|
||||
layer.set('visible', '1')
|
||||
layer.set('layerstyle', '{7d378444-e311-41d3-b9c6-a19e2a8e5899}')
|
||||
elif layer.attrib.get('name') == 'Background':
|
||||
layer.set('visible', '1')
|
||||
else:
|
||||
layer.set('visible', '0')
|
||||
xml_patched = b'<?xml version="1.0" encoding="UTF-8"?>\n<!DOCTYPE DOC PUBLIC \'-//KDE//DTD krita 2.0//EN\' \'http://www.calligra.org/DTD/krita-2.0.dtd\'>\n' + ET.tostring(root, encoding='utf-8')
|
||||
|
||||
# Parameter lists
|
||||
angles = [0.0, 90.0, 180.0, 270.0]
|
||||
distances = [5.0, 15.0, 30.0]
|
||||
sizes = [10.0, 30.0]
|
||||
spreads = [0.0, 20.0]
|
||||
opacities = [0.75, 1.0]
|
||||
blend_modes = [
|
||||
(b"Nrml", "normal"),
|
||||
(b"Mltp", "multiply"),
|
||||
(b"Scrn", "screen"),
|
||||
(b"LDdg", "lineardodge"),
|
||||
]
|
||||
shadow_colors = [
|
||||
([220, 30, 30, 255], "red"),
|
||||
([30, 180, 30, 255], "green"),
|
||||
]
|
||||
|
||||
total = len(angles) * len(distances) * len(sizes) * len(spreads) * len(opacities) * len(blend_modes) * len(shadow_colors)
|
||||
print(f"Generating {total} styled KRA files for Drop Shadow from {os.path.basename(base_kra)}...")
|
||||
|
||||
def patch_untf(asl, start_idx, key, value):
|
||||
idx = asl.find(key, start_idx)
|
||||
if idx != -1 and asl[idx+4:idx+8] == b'UntF':
|
||||
asl[idx+12:idx+20] = get_double_bytes(value)
|
||||
|
||||
def patch_enum(asl, start_idx, key, value):
|
||||
idx = asl.find(key, start_idx)
|
||||
if idx != -1 and asl[idx+4:idx+8] == b'enum':
|
||||
asl[idx+20:idx+24] = value
|
||||
|
||||
def patch_doub(asl, start_idx, key, value):
|
||||
idx = asl.find(key, start_idx)
|
||||
if idx != -1 and asl[idx+4:idx+8] == b'doub':
|
||||
asl[idx+8:idx+16] = get_double_bytes(value)
|
||||
|
||||
count = 0
|
||||
for angle in angles:
|
||||
for distance in distances:
|
||||
for size in sizes:
|
||||
for spread in spreads:
|
||||
for opacity in opacities:
|
||||
for bm_bytes, bm_name in blend_modes:
|
||||
for color, color_name in shadow_colors:
|
||||
count += 1
|
||||
filename = f"ds_a{int(angle)}_d{int(distance)}_s{int(size)}_sp{int(spread)}_op{int(opacity*100)}_n0_{bm_name}_{color_name}.kra"
|
||||
kra_path = os.path.join(out_dir, filename)
|
||||
|
||||
# Clone and patch ASL
|
||||
patched_asl = bytearray(asl_data)
|
||||
|
||||
start_idx = patched_asl.find(b'DrSh')
|
||||
|
||||
patch_untf(patched_asl, start_idx, b'Opct', opacity * 100.0)
|
||||
patch_untf(patched_asl, start_idx, b'lagl', angle)
|
||||
patch_untf(patched_asl, start_idx, b'Dstn', distance)
|
||||
patch_untf(patched_asl, start_idx, b'Ckmt', spread)
|
||||
patch_untf(patched_asl, start_idx, b'blur', size)
|
||||
patch_enum(patched_asl, start_idx, b'Md ', bm_bytes)
|
||||
|
||||
# Color: find Clr after start_idx, then Rd , Grn , Bl
|
||||
clr_idx = patched_asl.find(b'Clr ', start_idx)
|
||||
patch_doub(patched_asl, clr_idx, b'Rd ', color[0])
|
||||
patch_doub(patched_asl, clr_idx, b'Grn ', color[1])
|
||||
patch_doub(patched_asl, clr_idx, b'Bl ', color[2])
|
||||
|
||||
# Write new ZIP archive
|
||||
with zipfile.ZipFile(kra_path, 'w') as out_zip:
|
||||
has_asl = False
|
||||
for item in base_zip.infolist():
|
||||
if item.filename == "Unnamed/annotations/layerstyles.asl":
|
||||
out_zip.writestr(item, patched_asl)
|
||||
has_asl = True
|
||||
elif item.filename == "maindoc.xml":
|
||||
out_zip.writestr(item, xml_patched)
|
||||
elif item.filename == "Unnamed/layers/layer6.defaultpixel":
|
||||
out_zip.writestr(item, b"\x80\x80\x80\xff") # 50% gray background
|
||||
else:
|
||||
out_zip.writestr(item, files_cache[item.filename])
|
||||
if not has_asl:
|
||||
out_zip.writestr("Unnamed/annotations/layerstyles.asl", patched_asl)
|
||||
|
||||
if count % 20 == 0:
|
||||
print(f" [{count}/{total}] generated...")
|
||||
|
||||
base_zip.close()
|
||||
print(f"Successfully generated {count} files under {out_dir}.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,160 @@
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
import struct
|
||||
import zipfile
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
# Offsets for OuterGlow style keys inside sultan.kra's Unnamed/annotations/layerstyles.asl:
|
||||
# Md (blend mode): 3278
|
||||
# Clr (color) RGBC double offsets:
|
||||
# R double: 3324
|
||||
# G double: 3344
|
||||
# B double: 3364
|
||||
# Opct (opacity): 3388
|
||||
# Ckmt (choke/spread): 3440
|
||||
# blur (size): 3464
|
||||
|
||||
BASE_KRA = "_images/_psd_stil_test/sultan_test/sultan.kra"
|
||||
OUT_DIR = "_tmp/outer_glow_kra_set"
|
||||
|
||||
def get_double_bytes(val):
|
||||
return struct.pack(">d", float(val))
|
||||
|
||||
def main():
|
||||
bases = [
|
||||
("/mnt/extra/00_PROJECTS/hcie-rust-v4/_tmp/kra_tunes_simple/black_rect_simple.kra", "/mnt/extra/00_PROJECTS/hcie-rust-v4/_tmp/kra_tunes_simple_black/outer_glow_kra_set"),
|
||||
("/mnt/extra/00_PROJECTS/hcie-rust-v4/_tmp/kra_tunes_simple/red_rect_simple.kra", "/mnt/extra/00_PROJECTS/hcie-rust-v4/_tmp/kra_tunes_simple_red/outer_glow_kra_set")
|
||||
]
|
||||
|
||||
for base_kra, out_dir in bases:
|
||||
if not os.path.exists(base_kra):
|
||||
print(f"Error: Base Krita file {base_kra} not found.")
|
||||
continue
|
||||
|
||||
os.makedirs(out_dir, exist_ok=True)
|
||||
|
||||
# Read base archive and cache entries in memory
|
||||
base_zip = zipfile.ZipFile(base_kra, 'r')
|
||||
files_cache = {}
|
||||
for name in base_zip.namelist():
|
||||
files_cache[name] = base_zip.read(name)
|
||||
|
||||
asl_path_in_zip = "Unnamed/annotations/layerstyles.asl"
|
||||
if asl_path_in_zip in files_cache:
|
||||
asl_data = bytearray(files_cache[asl_path_in_zip])
|
||||
else:
|
||||
fallback_paths = [
|
||||
"/mnt/extra/00_PROJECTS/hcie-rust-v4/_images/kra_style_test/sultan.kra",
|
||||
"/mnt/extra/00_PROJECTS/hcie-rust-v4/_images/_psd_stil_test/sultan_test/sultan.kra",
|
||||
"_images/kra_style_test/sultan.kra",
|
||||
"_images/_psd_stil_test/sultan_test/sultan.kra"
|
||||
]
|
||||
sultan_zip = None
|
||||
for path in fallback_paths:
|
||||
if os.path.exists(path):
|
||||
sultan_zip = zipfile.ZipFile(path, 'r')
|
||||
break
|
||||
if sultan_zip is None:
|
||||
print("Error: Could not find sultan.kra fallback to load layerstyles.asl")
|
||||
return
|
||||
asl_data = bytearray(sultan_zip.read(asl_path_in_zip))
|
||||
sultan_zip.close()
|
||||
|
||||
xml_data = files_cache["maindoc.xml"]
|
||||
|
||||
# Set visibility in XML: both target layer and Background should be visible
|
||||
# Also ensure the layerstyle attribute is attached to the target layer
|
||||
ET.register_namespace('', "http://www.calligra.org/DTD/krita")
|
||||
root = ET.fromstring(xml_data)
|
||||
for layer in root.findall('.//{http://www.calligra.org/DTD/krita}layer'):
|
||||
if layer.attrib.get('name') in ['Layer 1', 'Red Rectangle']:
|
||||
layer.set('visible', '1')
|
||||
layer.set('layerstyle', '{d7a0844e-a522-4673-8c83-aa4f45e7782f}')
|
||||
elif layer.attrib.get('name') == 'Background':
|
||||
layer.set('visible', '1')
|
||||
else:
|
||||
layer.set('visible', '0')
|
||||
xml_patched = b'<?xml version="1.0" encoding="UTF-8"?>\n<!DOCTYPE DOC PUBLIC \'-//KDE//DTD krita 2.0//EN\' \'http://www.calligra.org/DTD/krita-2.0.dtd\'>\n' + ET.tostring(root, encoding='utf-8')
|
||||
|
||||
# Parameter lists
|
||||
spreads = [0.0, 20.0]
|
||||
sizes = [10.0, 30.0]
|
||||
opacities = [0.75, 1.0]
|
||||
blend_modes = [
|
||||
(b"Nrml", "normal"),
|
||||
(b"Scrn", "screen"),
|
||||
(b"LDdg", "lineardodge"),
|
||||
]
|
||||
glow_colors = [
|
||||
([220, 30, 30, 255], "red"),
|
||||
([30, 180, 30, 255], "green"),
|
||||
]
|
||||
|
||||
total = len(spreads) * len(sizes) * len(opacities) * len(blend_modes) * len(glow_colors)
|
||||
print(f"Generating {total} styled KRA files for Outer Glow from {os.path.basename(base_kra)}...")
|
||||
|
||||
def patch_untf(asl, start_idx, key, value):
|
||||
idx = asl.find(key, start_idx)
|
||||
if idx != -1 and asl[idx+4:idx+8] == b'UntF':
|
||||
asl[idx+12:idx+20] = get_double_bytes(value)
|
||||
|
||||
def patch_enum(asl, start_idx, key, value):
|
||||
idx = asl.find(key, start_idx)
|
||||
if idx != -1 and asl[idx+4:idx+8] == b'enum':
|
||||
asl[idx+20:idx+24] = value
|
||||
|
||||
def patch_doub(asl, start_idx, key, value):
|
||||
idx = asl.find(key, start_idx)
|
||||
if idx != -1 and asl[idx+4:idx+8] == b'doub':
|
||||
asl[idx+8:idx+16] = get_double_bytes(value)
|
||||
|
||||
count = 0
|
||||
for spread in spreads:
|
||||
for size in sizes:
|
||||
for opacity in opacities:
|
||||
for bm_bytes, bm_name in blend_modes:
|
||||
for color, color_name in glow_colors:
|
||||
count += 1
|
||||
filename = f"og_sp{int(spread)}_s{int(size)}_op{int(opacity*100)}_n0_{bm_name}_{color_name}.kra"
|
||||
kra_path = os.path.join(out_dir, filename)
|
||||
|
||||
# Clone and patch ASL
|
||||
patched_asl = bytearray(asl_data)
|
||||
|
||||
start_idx = patched_asl.find(b'OrGl')
|
||||
|
||||
patch_untf(patched_asl, start_idx, b'Opct', opacity * 100.0)
|
||||
patch_untf(patched_asl, start_idx, b'Ckmt', spread)
|
||||
patch_untf(patched_asl, start_idx, b'blur', size)
|
||||
patch_enum(patched_asl, start_idx, b'Md ', bm_bytes)
|
||||
|
||||
# Color: find Clr after start_idx, then Rd , Grn , Bl
|
||||
clr_idx = patched_asl.find(b'Clr ', start_idx)
|
||||
patch_doub(patched_asl, clr_idx, b'Rd ', color[0])
|
||||
patch_doub(patched_asl, clr_idx, b'Grn ', color[1])
|
||||
patch_doub(patched_asl, clr_idx, b'Bl ', color[2])
|
||||
|
||||
# Write new ZIP archive
|
||||
with zipfile.ZipFile(kra_path, 'w') as out_zip:
|
||||
has_asl = False
|
||||
for item in base_zip.infolist():
|
||||
if item.filename == "Unnamed/annotations/layerstyles.asl":
|
||||
out_zip.writestr(item, patched_asl)
|
||||
has_asl = True
|
||||
elif item.filename == "maindoc.xml":
|
||||
out_zip.writestr(item, xml_patched)
|
||||
elif item.filename == "Unnamed/layers/layer6.defaultpixel":
|
||||
out_zip.writestr(item, b"\x80\x80\x80\xff") # 50% gray background
|
||||
else:
|
||||
out_zip.writestr(item, files_cache[item.filename])
|
||||
if not has_asl:
|
||||
out_zip.writestr("Unnamed/annotations/layerstyles.asl", patched_asl)
|
||||
|
||||
if count % 20 == 0:
|
||||
print(f" [{count}/{total}] generated...")
|
||||
|
||||
base_zip.close()
|
||||
print(f"Successfully generated {count} files under {out_dir}.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,39 @@
|
||||
import os
|
||||
import zipfile
|
||||
import glob
|
||||
|
||||
def main():
|
||||
kra_files = glob.glob("_images/**/*.kra", recursive=True)
|
||||
print(f"Found KRA files: {kra_files}")
|
||||
|
||||
keys = [
|
||||
b"DrSh", b"IrSh", b"OrGl", b"IrGl", b"ebbl", b"FrFX", b"SoFi", b"GrFl", b"PtFl", b"Scl ", b"lagl", b"Dstn", b"blur", b"Ckmt", b"Opct", b"Md ", b"Clr ", b"Sz "
|
||||
]
|
||||
|
||||
for kra_path in kra_files:
|
||||
print(f"\n--- Analyzing {kra_path} ---")
|
||||
try:
|
||||
with zipfile.ZipFile(kra_path, 'r') as base_zip:
|
||||
asl_paths = [name for name in base_zip.namelist() if name.endswith("layerstyles.asl")]
|
||||
if not asl_paths:
|
||||
print(" No layerstyles.asl found.")
|
||||
continue
|
||||
for asl_path in asl_paths:
|
||||
asl_data = base_zip.read(asl_path)
|
||||
print(f" ASL path: {asl_path} ({len(asl_data)} bytes)")
|
||||
for key in keys:
|
||||
pos = 0
|
||||
found = []
|
||||
while True:
|
||||
idx = asl_data.find(key, pos)
|
||||
if idx == -1:
|
||||
break
|
||||
found.append(idx)
|
||||
pos = idx + len(key)
|
||||
if found:
|
||||
print(f" Key {key.decode('ascii', errors='replace')} found at offsets: {found}")
|
||||
except Exception as e:
|
||||
print(f" Error: {e}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,32 @@
|
||||
import os
|
||||
import zipfile
|
||||
|
||||
BASE_KRA = "/mnt/extra/00_PROJECTS/hcie-rust-v4/_images/kra_style_test/sultan.kra"
|
||||
|
||||
def main():
|
||||
if not os.path.exists(BASE_KRA):
|
||||
print(f"Error: {BASE_KRA} not found.")
|
||||
return
|
||||
|
||||
with zipfile.ZipFile(BASE_KRA, 'r') as base_zip:
|
||||
asl_data = base_zip.read("Unnamed/annotations/layerstyles.asl")
|
||||
print(f"ASL Data Size: {len(asl_data)} bytes")
|
||||
|
||||
# Search for common key signatures in the ASL descriptor format
|
||||
# Keys are usually 4-byte identifiers
|
||||
keys = [
|
||||
b"DrSh", b"IrSh", b"OrGl", b"IrGl", b"ebbl", b"FrFX", b"SoFi", b"GrFl", b"PtFl", b"Scl ", b"lagl", b"Dstn", b"blur", b"Ckmt", b"Opct", b"Md ", b"Clr ", b"Sz "
|
||||
]
|
||||
for key in keys:
|
||||
pos = 0
|
||||
found = []
|
||||
while True:
|
||||
idx = asl_data.find(key, pos)
|
||||
if idx == -1:
|
||||
break
|
||||
found.append(idx)
|
||||
pos = idx + len(key)
|
||||
print(f"Key {key.decode('ascii', errors='replace')} found at offsets: {found}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,41 @@
|
||||
# Photopea / Photoshop ExtendScript Dataset Generator
|
||||
|
||||
This folder contains JSX scripts to generate systematic layer-effect ground-truth images in Photopea or Photoshop.
|
||||
|
||||
## Files
|
||||
|
||||
- `generate_inner_shadow_dataset.jsx` — Short starter script for Inner Shadow.
|
||||
|
||||
## Usage
|
||||
|
||||
1. Open Photopea (https://www.photopea.com) in a browser.
|
||||
2. Press `Alt+Ctrl+I` (or use `File > Automate > Script`) to open the script runner.
|
||||
3. Paste the contents of the desired `.jsx` file and run it.
|
||||
4. Select an output folder when prompted.
|
||||
|
||||
Photopea will create a new document for each parameter combination, draw a black rectangle, apply the Inner Shadow effect, hide the background, and export a transparent PNG.
|
||||
|
||||
## Current parameter grid (short version)
|
||||
|
||||
| Parameter | Values |
|
||||
|---|---|
|
||||
| angle | 0, 90, 180, 270 |
|
||||
| distance | 5, 15, 30 |
|
||||
| size | 10, 30 |
|
||||
| choke | 0, 20 |
|
||||
| fx opacity | 75, 100 |
|
||||
| fx blend mode | Normal, Multiply |
|
||||
|
||||
Total images: `4 × 3 × 2 × 2 × 2 × 2 = 192`.
|
||||
|
||||
## Notes
|
||||
|
||||
- Blend mode keys use Photoshop's 4-character codes (`Nrml`, `Mltp`).
|
||||
- The output PNGs have transparent backgrounds, so MAE comparisons can ignore white reference pixels if needed.
|
||||
- ExtendScript error handling is basic; if a parameter combination fails, the script logs it and continues.
|
||||
|
||||
## Next steps
|
||||
|
||||
- Add more layer blend modes, fill opacity, and layer opacity variations.
|
||||
- Extend to Drop Shadow, Outer Glow, Inner Glow, Stroke, Bevel & Emboss.
|
||||
- Add a companion Rust generator that creates matching PSD files so the same dataset can be imported back into HCIE for MAE tuning.
|
||||
@@ -0,0 +1,292 @@
|
||||
/**
|
||||
* generate_inner_shadow_dataset.jsx
|
||||
*
|
||||
* Purpose:
|
||||
* Generate a systematic Inner Shadow effect dataset in Photopea.
|
||||
* Uses stringID-based ActionDescriptor for maximum Photopea compatibility.
|
||||
*
|
||||
* Logic & Workflow:
|
||||
* 1. Creates a new RGB document with transparent background.
|
||||
* 2. Adds a normal raster layer and fills the center with black.
|
||||
* 3. Applies one Inner Shadow effect via ActionDescriptor (stringID).
|
||||
* 4. Hides the background layer.
|
||||
* 5. Saves the result as a transparent PNG.
|
||||
*
|
||||
* Usage:
|
||||
* 1. Open https://www.photopea.com in a browser.
|
||||
* 2. Alt+Ctrl+I (or File > Automate > Script).
|
||||
* 3. Paste this script and run.
|
||||
* 4. Select output folder when prompted.
|
||||
*/
|
||||
|
||||
var outFolder = Folder.selectDialog("Select output folder for Inner Shadow dataset");
|
||||
if (!outFolder) {
|
||||
alert("No output folder selected. Exiting.");
|
||||
throw new Error("No output folder");
|
||||
}
|
||||
|
||||
var angles = [0, 90, 180, 270];
|
||||
var distances = [5, 15, 30];
|
||||
var sizes = [10, 30];
|
||||
var chokes = [0, 20];
|
||||
var fxOpacities = [75, 100];
|
||||
var fxBlendModes = [
|
||||
{ id: "normal", name: "normal" },
|
||||
{ id: "multiply", name: "multiply" }
|
||||
];
|
||||
|
||||
function createDocument(name, width, height) {
|
||||
var doc = app.documents.add(width, height, 72, name, NewDocumentMode.RGB, DocumentFill.TRANSPARENT);
|
||||
try {
|
||||
var bg = doc.artLayers[doc.artLayers.length - 1];
|
||||
if (bg.isBackgroundLayer) {
|
||||
bg.visible = false;
|
||||
}
|
||||
} catch (e) {}
|
||||
return doc;
|
||||
}
|
||||
|
||||
function addBlackRectangle(doc) {
|
||||
var w = doc.width;
|
||||
var h = doc.height;
|
||||
if (typeof w === "object" && typeof w.value === "function") { w = w.value; }
|
||||
if (typeof h === "object" && typeof h.value === "function") { h = h.value; }
|
||||
w = Number(w);
|
||||
h = Number(h);
|
||||
|
||||
var margin = Math.round(Math.min(w, h) * 0.25);
|
||||
|
||||
var layer = doc.artLayers.add();
|
||||
layer.name = "shape";
|
||||
|
||||
var region = [
|
||||
[margin, margin],
|
||||
[w - margin, margin],
|
||||
[w - margin, h - margin],
|
||||
[margin, h - margin]
|
||||
];
|
||||
|
||||
doc.selection.select(region);
|
||||
|
||||
var black = new SolidColor();
|
||||
black.rgb.red = 0;
|
||||
black.rgb.green = 0;
|
||||
black.rgb.blue = 0;
|
||||
|
||||
doc.selection.fill(black);
|
||||
doc.selection.deselect();
|
||||
|
||||
return layer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Try applying Inner Shadow via ActionDescriptor with stringID keys.
|
||||
* Returns true on success, false on failure.
|
||||
*/
|
||||
function applyInnerShadow(params) {
|
||||
try {
|
||||
var desc = new ActionDescriptor();
|
||||
|
||||
var ref = new ActionReference();
|
||||
ref.putProperty(stringIDToTypeID("property"), stringIDToTypeID("layerEffects"));
|
||||
ref.putEnumerated(stringIDToTypeID("layer"), stringIDToTypeID("ordinal"), stringIDToTypeID("targetEnum"));
|
||||
desc.putReference(stringIDToTypeID("null"), ref);
|
||||
|
||||
var fxDesc = new ActionDescriptor();
|
||||
|
||||
var isDesc = new ActionDescriptor();
|
||||
isDesc.putBoolean(stringIDToTypeID("enabled"), true);
|
||||
|
||||
// Blend mode
|
||||
isDesc.putEnumerated(stringIDToTypeID("mode"), stringIDToTypeID("blendMode"), stringIDToTypeID(params.blendMode));
|
||||
|
||||
// Color
|
||||
var colorDesc = new ActionDescriptor();
|
||||
colorDesc.putDouble(stringIDToTypeID("red"), 0);
|
||||
colorDesc.putDouble(stringIDToTypeID("grain"), 0);
|
||||
colorDesc.putDouble(stringIDToTypeID("blue"), 0);
|
||||
isDesc.putObject(stringIDToTypeID("color"), stringIDToTypeID("RGBColor"), colorDesc);
|
||||
|
||||
// Opacity (0-100)
|
||||
isDesc.putInteger(stringIDToTypeID("opacity"), params.opacity);
|
||||
|
||||
// Angle
|
||||
isDesc.putInteger(stringIDToTypeID("localLightingAngle"), params.angle);
|
||||
|
||||
// Global light
|
||||
isDesc.putBoolean(stringIDToTypeID("useGlobalAngle"), false);
|
||||
|
||||
// Distance
|
||||
isDesc.putInteger(stringIDToTypeID("distance"), params.distance);
|
||||
|
||||
// Choke
|
||||
isDesc.putInteger(stringIDToTypeID("chokeMatte"), params.choke);
|
||||
|
||||
// Blur (size)
|
||||
isDesc.putInteger(stringIDToTypeID("blur"), params.size);
|
||||
|
||||
// Noise
|
||||
isDesc.putInteger(stringIDToTypeID("noise"), 0);
|
||||
|
||||
// Anti-alias
|
||||
isDesc.putBoolean(stringIDToTypeID("antiAlias"), true);
|
||||
|
||||
// Transfer function (contour)
|
||||
isDesc.putBoolean(stringIDToTypeID("transferSpecIsScaled"), false);
|
||||
|
||||
fxDesc.putObject(stringIDToTypeID("innerShadow"), stringIDToTypeID("innerShadow"), isDesc);
|
||||
|
||||
desc.putObject(stringIDToTypeID("to"), stringIDToTypeID("layerEffects"), fxDesc);
|
||||
|
||||
executeAction(stringIDToTypeID("set"), desc, DialogModes.NO);
|
||||
return true;
|
||||
} catch (e) {
|
||||
$.writeln("stringID failed: " + e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fallback: apply via charID-based descriptor.
|
||||
*/
|
||||
function applyInnerShadowCharID(params) {
|
||||
try {
|
||||
var desc = new ActionDescriptor();
|
||||
|
||||
var ref = new ActionReference();
|
||||
ref.putProperty(charIDToTypeID("Prpr"), charIDToTypeID("Lefx"));
|
||||
ref.putEnumerated(charIDToTypeID("Lyr "), charIDToTypeID("Ordn"), charIDToTypeID("Trgt"));
|
||||
desc.putReference(charIDToTypeID("null"), ref);
|
||||
|
||||
var fxDesc = new ActionDescriptor();
|
||||
|
||||
var isDesc = new ActionDescriptor();
|
||||
isDesc.putBoolean(charIDToTypeID("enab"), true);
|
||||
isDesc.putBoolean(charIDToTypeID("present"), true);
|
||||
isDesc.putBoolean(charIDToTypeID("showInDialog"), true);
|
||||
|
||||
// Blend mode
|
||||
isDesc.putEnumerated(charIDToTypeID("Blnd"), charIDToTypeID("BlnM"), charIDToTypeID(params.blendModeCharID));
|
||||
|
||||
// Color
|
||||
var clr = new ActionDescriptor();
|
||||
clr.putDouble(charIDToTypeID("Rd "), 0);
|
||||
clr.putDouble(charIDToTypeID("Grn "), 0);
|
||||
clr.putDouble(charIDToTypeID("Bl "), 0);
|
||||
isDesc.putObject(charIDToTypeID("Clr "), charIDToTypeID("RGBC"), clr);
|
||||
|
||||
// Opacity: 0-100 integer
|
||||
isDesc.putInteger(charIDToTypeID("Opct"), params.opacity);
|
||||
|
||||
// Angle: use lagl (local angle) instead of Angl
|
||||
isDesc.putInteger(charIDToTypeID("lagl"), params.angle);
|
||||
isDesc.putBoolean(charIDToTypeID("uglg"), false);
|
||||
|
||||
// Distance
|
||||
isDesc.putInteger(charIDToTypeID("Dstn"), params.distance);
|
||||
|
||||
// Choke
|
||||
isDesc.putInteger(charIDToTypeID("Ckmt"), params.choke);
|
||||
|
||||
// Blur
|
||||
isDesc.putInteger(charIDToTypeID("blur"), params.size);
|
||||
|
||||
// Noise
|
||||
isDesc.putInteger(charIDToTypeID("Nose"), 0);
|
||||
|
||||
fxDesc.putObject(charIDToTypeID("IrSh"), charIDToTypeID("IrSh"), isDesc);
|
||||
|
||||
desc.putObject(charIDToTypeID("T "), charIDToTypeID("Lefx"), fxDesc);
|
||||
|
||||
executeAction(charIDToTypeID("setd"), desc, DialogModes.NO);
|
||||
return true;
|
||||
} catch (e) {
|
||||
$.writeln("charID failed: " + e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function saveTransparentPng(doc, file) {
|
||||
var pngOptions = new PNGSaveOptions();
|
||||
pngOptions.compression = 6;
|
||||
pngOptions.interlaced = false;
|
||||
doc.saveAs(file, pngOptions, true, Extension.LOWERCASE);
|
||||
}
|
||||
|
||||
var count = 0;
|
||||
var errors = 0;
|
||||
var total = angles.length * distances.length * sizes.length * chokes.length * fxOpacities.length * fxBlendModes.length;
|
||||
|
||||
var blendModeCharMap = {
|
||||
"normal": "Nrml",
|
||||
"multiply": "Mltp"
|
||||
};
|
||||
|
||||
for (var a = 0; a < angles.length; a++) {
|
||||
for (var d = 0; d < distances.length; d++) {
|
||||
for (var s = 0; s < sizes.length; s++) {
|
||||
for (var c = 0; c < chokes.length; c++) {
|
||||
for (var o = 0; o < fxOpacities.length; o++) {
|
||||
for (var b = 0; b < fxBlendModes.length; b++) {
|
||||
var bmName = fxBlendModes[b].name;
|
||||
var bmID = fxBlendModes[b].id;
|
||||
|
||||
var fileName = "is_" +
|
||||
"a" + angles[a] + "_" +
|
||||
"d" + distances[d] + "_" +
|
||||
"s" + sizes[s] + "_" +
|
||||
"c" + chokes[c] + "_" +
|
||||
"op" + fxOpacities[o] + "_" +
|
||||
"bm" + bmName +
|
||||
".png";
|
||||
|
||||
var file = new File(outFolder + "/" + fileName);
|
||||
if (file.exists) {
|
||||
count++;
|
||||
continue;
|
||||
}
|
||||
|
||||
var params = {
|
||||
angle: angles[a],
|
||||
distance: distances[d],
|
||||
size: sizes[s],
|
||||
choke: chokes[c],
|
||||
opacity: fxOpacities[o],
|
||||
blendMode: bmID,
|
||||
blendModeCharID: blendModeCharMap[bmID] || "Nrml"
|
||||
};
|
||||
|
||||
var doc = createDocument("inner_shadow_test", 256, 256);
|
||||
var layer = addBlackRectangle(doc);
|
||||
|
||||
var applied = applyInnerShadow(params);
|
||||
if (!applied) {
|
||||
applied = applyInnerShadowCharID(params);
|
||||
}
|
||||
|
||||
if (applied) {
|
||||
try {
|
||||
saveTransparentPng(doc, file);
|
||||
} catch (e2) {
|
||||
$.writeln("Save error: " + fileName + " — " + e2);
|
||||
errors++;
|
||||
}
|
||||
} else {
|
||||
$.writeln("FAILED to apply effect: " + fileName);
|
||||
errors++;
|
||||
}
|
||||
|
||||
doc.close(SaveOptions.DONOTSAVECHANGES);
|
||||
|
||||
count++;
|
||||
if (count % 10 === 0) {
|
||||
$.writeln("Progress: " + count + " / " + total + " (errors: " + errors + ")");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
alert("Done. Generated " + count + " images, " + errors + " errors.\n" + outFolder.fsName);
|
||||
@@ -0,0 +1,206 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Test Photopea inner shadow via CDP."""
|
||||
import json, time, subprocess, sys, os
|
||||
|
||||
CHROMIUM = "/snap/bin/chromium"
|
||||
PORT = 9555
|
||||
OUT_DIR = "/mnt/extra/00_PROJECTS/hcie-rust-v4/_tmp/photopea_test"
|
||||
os.makedirs(OUT_DIR, exist_ok=True)
|
||||
|
||||
def http_get(path):
|
||||
import urllib.request
|
||||
with urllib.request.urlopen(f"http://127.0.0.1:{PORT}{path}", timeout=10) as r:
|
||||
return json.loads(r.read())
|
||||
|
||||
class CDP:
|
||||
def __init__(self, ws_url):
|
||||
import websocket
|
||||
self.ws = websocket.create_connection(ws_url, timeout=60)
|
||||
self._id = 0
|
||||
|
||||
def send(self, method, params=None):
|
||||
self._id += 1
|
||||
msg = {"id": self._id, "method": method}
|
||||
if params:
|
||||
msg["params"] = params
|
||||
self.ws.send(json.dumps(msg))
|
||||
while True:
|
||||
resp = json.loads(self.ws.recv())
|
||||
if resp.get("id") == self._id:
|
||||
return resp
|
||||
|
||||
def eval_js(self, expr, await_promise=True, timeout_ms=45000):
|
||||
r = self.send("Runtime.evaluate", {
|
||||
"expression": expr,
|
||||
"awaitPromise": await_promise,
|
||||
"returnByValue": True,
|
||||
"timeout": timeout_ms,
|
||||
})
|
||||
val = r.get("result", {}).get("result", {})
|
||||
if "value" in val:
|
||||
return val["value"]
|
||||
return val
|
||||
|
||||
def close(self):
|
||||
try: self.ws.close()
|
||||
except: pass
|
||||
|
||||
def main():
|
||||
proc = subprocess.Popen([
|
||||
CHROMIUM,
|
||||
"--headless=new",
|
||||
"--no-sandbox",
|
||||
"--disable-gpu",
|
||||
"--disable-dev-shm-usage",
|
||||
f"--remote-debugging-port={PORT}",
|
||||
"--remote-allow-origins=*",
|
||||
"--window-size=1024,768",
|
||||
"about:blank",
|
||||
], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
|
||||
time.sleep(4)
|
||||
|
||||
try:
|
||||
targets = http_get("/json")
|
||||
ws_url = None
|
||||
for t in targets:
|
||||
if t.get("type") == "page":
|
||||
ws_url = t["webSocketDebuggerUrl"]
|
||||
break
|
||||
if not ws_url:
|
||||
print("FATAL: no page target")
|
||||
return 1
|
||||
|
||||
cdp = CDP(ws_url)
|
||||
print(f"Connected ({ws_url[:60]}...)")
|
||||
|
||||
print("[1] Navigating to photopea.com...")
|
||||
cdp.send("Page.navigate", {"url": "https://www.photopea.com"})
|
||||
|
||||
print("[2] Waiting for page load...")
|
||||
time.sleep(90)
|
||||
|
||||
r = cdp.eval_js('document.readyState', await_promise=False)
|
||||
print(f" readyState={r}")
|
||||
|
||||
# Check what globals exist
|
||||
print("[3] Probing global scope...")
|
||||
probe = cdp.eval_js("""
|
||||
JSON.stringify({
|
||||
hasApp: typeof app !== "undefined",
|
||||
hasApp2: typeof window.app !== "undefined",
|
||||
hasPhotoshop: typeof Photoshop !== "undefined",
|
||||
hasPhotoshopApp: typeof photoshop !== "undefined",
|
||||
bodyLen: document.body ? document.body.innerHTML.length : 0,
|
||||
scripts: document.scripts.length,
|
||||
title: document.title,
|
||||
iframes: document.querySelectorAll("iframe").length,
|
||||
})
|
||||
""", await_promise=False)
|
||||
print(f" globals: {probe}")
|
||||
|
||||
# Photopea uses a special mechanism - try postMessage approach
|
||||
# Photopea might listen for messages with specific format
|
||||
print("[4] Trying Photopea postMessage API...")
|
||||
|
||||
# Photopea's API: when loaded in an iframe, parent sends scripts via postMessage.
|
||||
# But we're ON the photopea.com page. Let's try app.executeScript or direct eval.
|
||||
r2 = cdp.eval_js("""
|
||||
(function() {
|
||||
// Try to find app in various scopes
|
||||
var found = [];
|
||||
try { if (window.app) found.push("window.app"); } catch(e) {}
|
||||
try { if (self.app) found.push("self.app"); } catch(e) {}
|
||||
try { if (globalThis.app) found.push("globalThis.app"); } catch(e) {}
|
||||
|
||||
// Check if there are any Photopea-specific globals
|
||||
var keys = Object.keys(window).filter(k =>
|
||||
k.toLowerCase().includes("photo") ||
|
||||
k.toLowerCase().includes("ppea") ||
|
||||
k.toLowerCase().includes("pp") ||
|
||||
k.toLowerCase().includes("app")
|
||||
);
|
||||
|
||||
return JSON.stringify({found: found, photoKeys: keys.slice(0, 20)});
|
||||
})()
|
||||
""", await_promise=False)
|
||||
print(f" probe result: {r2}")
|
||||
|
||||
# Try running a script via Photopea's mechanism
|
||||
# Photopea can receive scripts via URL hash: photopea.com/#script_here
|
||||
# But more importantly, it exposes app when loaded as intended
|
||||
print("[5] Trying hash-based script execution...")
|
||||
script = 'var doc = app.documents.add(256, 256, 72, "test", NewDocumentMode.RGB, DocumentFill.TRANSPARENT); app.echoToOE("OK");'
|
||||
import base64
|
||||
b64 = base64.b64encode(script.encode()).decode()
|
||||
cdp.send("Page.navigate", {"url": f"https://www.photopea.com#{script}"})
|
||||
time.sleep(15)
|
||||
|
||||
r3 = cdp.eval_js('typeof app !== "undefined" ? "app_FOUND" : "no_app"', await_promise=False)
|
||||
print(f" after hash nav: {r3}")
|
||||
|
||||
# Maybe we need to add --enable-webgl or similar
|
||||
# Let's try a completely different approach: use Photopea's RPC mechanism
|
||||
print("[6] Checking if Photopea uses MessagePort or ServiceWorker...")
|
||||
r4 = cdp.eval_js("""
|
||||
(function() {
|
||||
var info = {};
|
||||
try { info.workers = navigator.serviceWorker ? "available" : "not available"; } catch(e) { info.workers = "err:" + e; }
|
||||
try { info.sockets = typeof WebSocket !== "undefined" ? "available" : "not"; } catch(e) {}
|
||||
// Check for MessageChannel
|
||||
try {
|
||||
var ch = new MessageChannel();
|
||||
info.messageChannel = "available";
|
||||
} catch(e) { info.messageChannel = "err"; }
|
||||
|
||||
// Check canvas elements (Photopea renders to canvas)
|
||||
var canvases = document.querySelectorAll("canvas");
|
||||
info.canvasCount = canvases.length;
|
||||
info.canvasDims = [];
|
||||
canvases.forEach(function(c) { info.canvasDims.push(c.width + "x" + c.height); });
|
||||
|
||||
return JSON.stringify(info);
|
||||
})()
|
||||
""", await_promise=False)
|
||||
print(f" {r4}")
|
||||
|
||||
# Last resort: check the actual Photopea API docs
|
||||
# Photopea exposes `app` on window when the UI is loaded
|
||||
# It might need WebGL canvas to be created first
|
||||
# Let's check if there's a #pea-app or similar container
|
||||
r5 = cdp.eval_js("""
|
||||
(function() {
|
||||
var divs = document.querySelectorAll("div[id]");
|
||||
var ids = [];
|
||||
divs.forEach(function(d) { ids.push(d.id); });
|
||||
return JSON.stringify(ids.slice(0, 30));
|
||||
})()
|
||||
""", await_promise=False)
|
||||
print(f" div ids: {r5}")
|
||||
|
||||
print("\n=== CONCLUSION ===")
|
||||
print("Photopea loaded but `app` is NOT in the global JS scope.")
|
||||
print("This means Photopea's scripting API is only available via postMessage")
|
||||
print("when Photopea is embedded as an iframe in another page.")
|
||||
print("")
|
||||
print("The correct approach for batch generation is:")
|
||||
print(" - The user runs the JSX script manually in Photopea's Script runner")
|
||||
print(" - OR use a different tool (GIMP Script-Fu, Python PIL)")
|
||||
|
||||
return 0
|
||||
|
||||
except Exception as e:
|
||||
print(f"EXCEPTION: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return 1
|
||||
finally:
|
||||
try: cdp.close()
|
||||
except: pass
|
||||
try: proc.kill()
|
||||
except: pass
|
||||
try: proc.wait(timeout=5)
|
||||
except: pass
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main() or 0)
|
||||
@@ -0,0 +1,41 @@
|
||||
# Photoshop ExtendScript Dataset Generator
|
||||
|
||||
This folder contains JSX scripts to generate systematic layer-effect ground-truth images in Adobe Photoshop.
|
||||
|
||||
## Files
|
||||
|
||||
- `generate_inner_shadow_dataset.jsx` — Starter script for Inner Shadow.
|
||||
|
||||
## Usage
|
||||
|
||||
1. Open Adobe Photoshop.
|
||||
2. Choose `File > Scripts > Browse...`.
|
||||
3. Select `generate_inner_shadow_dataset.jsx`.
|
||||
4. Choose an output folder when prompted.
|
||||
|
||||
Photoshop will create a new document for each parameter combination, draw a filled black rectangle, apply the Inner Shadow effect, and export a transparent PNG.
|
||||
|
||||
## Current parameter grid (short version)
|
||||
|
||||
| Parameter | Values |
|
||||
|---|---|
|
||||
| angle | 0, 90, 180, 270 |
|
||||
| distance | 5, 15, 30 |
|
||||
| size | 10, 30 |
|
||||
| choke | 0, 20 |
|
||||
| fx opacity | 75, 100 |
|
||||
| fx blend mode | Normal, Multiply |
|
||||
|
||||
Total images: `4 × 3 × 2 × 2 × 2 × 2 = 192`.
|
||||
|
||||
## Notes
|
||||
|
||||
- Blend mode keys use Photoshop's 4-character codes (`Nrml`, `Mltp`).
|
||||
- The output PNGs have transparent backgrounds, so MAE comparisons can ignore white reference pixels if needed.
|
||||
- Opacity is passed as 0-100 (e.g. 75 means 75%).
|
||||
- If a combination fails, the script alerts and continues with the next one.
|
||||
|
||||
## Next steps
|
||||
|
||||
- Add layer blend mode, layer opacity, and fill opacity variations.
|
||||
- Extend to Drop Shadow, Outer Glow, Inner Glow, Stroke, Bevel & Emboss, Color Overlay.
|
||||
@@ -0,0 +1,25 @@
|
||||
# Photoshop Batch PNG Exporter
|
||||
|
||||
This script batch-exports PSD files to transparent PNGs.
|
||||
|
||||
## Files
|
||||
|
||||
- `export_psd_folder_to_png.jsx`
|
||||
|
||||
## Usage
|
||||
|
||||
1. Open Adobe Photoshop.
|
||||
2. Choose `File > Scripts > Browse...`.
|
||||
3. Select `export_psd_folder_to_png.jsx`.
|
||||
4. When prompted, choose the folder containing the PSD files generated by `generate_inner_shadow_psd_set.rs`.
|
||||
5. The script saves one PNG next to each PSD.
|
||||
|
||||
## Notes
|
||||
|
||||
- Existing PNGs are skipped, so you can re-run safely.
|
||||
- If any PSD fails to open or export, the script logs it and continues.
|
||||
- Output PNGs use transparent background.
|
||||
|
||||
## Photopea alternative
|
||||
|
||||
Photopea does not support transparent PNG export via JSX directly. For Photopea, open each PSD and use `File > Export As > PNG` with transparency enabled.
|
||||
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* export_psd_folder_to_png.jsx
|
||||
*
|
||||
* Purpose:
|
||||
* Batch-open every PSD in a folder and export it as a transparent PNG.
|
||||
*
|
||||
* Logic & Workflow:
|
||||
* 1. Prompts the user to select a folder containing PSD files.
|
||||
* 2. Iterates over all *.psd files in that folder.
|
||||
* 3. Opens each PSD, saves it as PNG with transparency, then closes without saving.
|
||||
*
|
||||
* Arguments:
|
||||
* None (interactive). User selects input folder via dialog.
|
||||
*
|
||||
* Side Effects:
|
||||
* - Opens and closes many Photoshop documents.
|
||||
* - Writes PNG files next to the original PSDs.
|
||||
*/
|
||||
|
||||
var inFolder = Folder.selectDialog("Select folder containing PSD files");
|
||||
if (!inFolder) {
|
||||
alert("No folder selected. Exiting.");
|
||||
throw new Error("No folder");
|
||||
}
|
||||
|
||||
var psdFiles = inFolder.getFiles("*.psd");
|
||||
if (psdFiles.length === 0) {
|
||||
alert("No PSD files found in " + inFolder.fsName);
|
||||
throw new Error("No PSD files");
|
||||
}
|
||||
|
||||
var pngOptions = new PNGSaveOptions();
|
||||
pngOptions.compression = 6;
|
||||
pngOptions.interlaced = false;
|
||||
|
||||
for (var i = 0; i < psdFiles.length; i++) {
|
||||
var psdFile = psdFiles[i];
|
||||
var baseName = psdFile.name.replace(/\.psd$/i, "");
|
||||
var pngFile = new File(inFolder + "/" + baseName + ".png");
|
||||
|
||||
if (pngFile.exists) {
|
||||
$.writeln("Skipping existing: " + pngFile.name);
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
var doc = app.open(psdFile);
|
||||
doc.saveAs(pngFile, pngOptions, true, Extension.LOWERCASE);
|
||||
doc.close(SaveOptions.DONOTSAVECHANGES);
|
||||
$.writeln("Exported: " + pngFile.name);
|
||||
} catch (e) {
|
||||
$.writeln("Error processing " + psdFile.name + ": " + e);
|
||||
alert("Error processing " + psdFile.name + ":\n" + e);
|
||||
}
|
||||
}
|
||||
|
||||
alert("Done. Processed " + psdFiles.length + " PSD files.");
|
||||
@@ -0,0 +1,73 @@
|
||||
/**
|
||||
* export_psd_folder_to_png.jsx
|
||||
*
|
||||
* Purpose:
|
||||
* Batch-open every PSD in a folder and export it as a transparent PNG
|
||||
* into a separate output folder.
|
||||
*
|
||||
* Logic & Workflow:
|
||||
* 1. Prompts user for input PSD folder.
|
||||
* 2. Prompts user for output PNG folder (default: input + "_png" suffix).
|
||||
* 3. Iterates over all *.psd files.
|
||||
* 4. Opens each PSD, saves as PNG with transparency, closes without saving.
|
||||
*
|
||||
* Arguments:
|
||||
* None (interactive).
|
||||
*
|
||||
* Side Effects:
|
||||
* - Opens and closes many Photoshop documents.
|
||||
* - Writes PNG files into the output folder.
|
||||
*/
|
||||
|
||||
var inFolder = Folder.selectDialog("Select PSD input folder");
|
||||
if (!inFolder) {
|
||||
alert("No folder selected. Exiting.");
|
||||
throw new Error("No folder");
|
||||
}
|
||||
|
||||
var defaultOut = new Folder(inFolder.parent.fsName + "/" + inFolder.name + "_png");
|
||||
var outFolder = Folder.selectDialog("Select PNG output folder", defaultOut);
|
||||
if (!outFolder) {
|
||||
outFolder = defaultOut;
|
||||
}
|
||||
if (!outFolder.exists) {
|
||||
outFolder.create();
|
||||
}
|
||||
|
||||
var psdFiles = inFolder.getFiles("*.psd");
|
||||
if (psdFiles.length === 0) {
|
||||
alert("No PSD files found in " + inFolder.fsName);
|
||||
throw new Error("No PSD files");
|
||||
}
|
||||
|
||||
var pngOptions = new PNGSaveOptions();
|
||||
pngOptions.compression = 6;
|
||||
pngOptions.interlaced = false;
|
||||
|
||||
for (var i = 0; i < psdFiles.length; i++) {
|
||||
var psdFile = psdFiles[i];
|
||||
var baseName = psdFile.name.replace(/\.psd$/i, "");
|
||||
var pngFile = new File(outFolder.fsName + "/" + baseName + ".png");
|
||||
|
||||
if (pngFile.exists) {
|
||||
$.writeln("Skipping existing: " + pngFile.name);
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
var doc = app.open(psdFile);
|
||||
// Ensure no background layer blocks transparency
|
||||
if (doc.backgroundLayer) {
|
||||
doc.activeLayer = doc.backgroundLayer;
|
||||
doc.activeLayer.remove();
|
||||
}
|
||||
doc.trim(TrimType.TRANSPARENT);
|
||||
doc.saveAs(pngFile, pngOptions, true, Extension.LOWERCASE);
|
||||
doc.close(SaveOptions.DONOTSAVECHANGES);
|
||||
$.writeln("Exported: " + pngFile.name);
|
||||
} catch (e) {
|
||||
$.writeln("Error processing " + psdFile.name + ": " + e);
|
||||
}
|
||||
}
|
||||
|
||||
alert("Done. Processed " + psdFiles.length + " PSD files.");
|
||||
@@ -0,0 +1,185 @@
|
||||
/**
|
||||
* generate_inner_shadow_dataset.jsx
|
||||
*
|
||||
* Purpose:
|
||||
* Generate a systematic Inner Shadow effect dataset in Adobe Photoshop.
|
||||
*
|
||||
* Logic & Workflow:
|
||||
* 1. Creates a new transparent RGB document.
|
||||
* 2. Adds a raster layer and fills a centered rectangle with black.
|
||||
* 3. Applies one Inner Shadow effect via Photoshop ActionDescriptor.
|
||||
* 4. Saves the result as a transparent PNG.
|
||||
*
|
||||
* Arguments:
|
||||
* None (interactive). The user selects an output folder via dialog.
|
||||
*
|
||||
* Side Effects:
|
||||
* - Creates and closes many temporary Photoshop documents.
|
||||
* - Writes PNG files to the selected folder.
|
||||
*/
|
||||
|
||||
var outFolder = Folder.selectDialog("Select output folder for Inner Shadow dataset");
|
||||
if (!outFolder) {
|
||||
alert("No output folder selected. Exiting.");
|
||||
throw new Error("No output folder");
|
||||
}
|
||||
|
||||
var angles = [0, 90, 180, 270];
|
||||
var distances = [5, 15, 30];
|
||||
var sizes = [10, 30];
|
||||
var chokes = [0, 20];
|
||||
var fxOpacities = [75, 100];
|
||||
var fxBlendModes = [
|
||||
{ key: "Nrml", name: "normal" },
|
||||
{ key: "Mltp", name: "multiply" }
|
||||
];
|
||||
|
||||
function pxValue(v) {
|
||||
if (v === undefined || v === null) return 0;
|
||||
if (typeof v === "number") return v;
|
||||
if (typeof v.as === "function") return v.as("px");
|
||||
if (typeof v.value !== "undefined") return v.value;
|
||||
return Number(v);
|
||||
}
|
||||
|
||||
function createDocument(name, width, height) {
|
||||
try {
|
||||
return app.documents.add(width, height, 72, name, NewDocumentMode.RGB, DocumentFill.TRANSPARENT);
|
||||
} catch (e) {
|
||||
return app.documents.add(width, height, 72, name, NewDocumentMode.RGB);
|
||||
}
|
||||
}
|
||||
|
||||
function addBlackRectangle(doc) {
|
||||
var w = pxValue(doc.width);
|
||||
var h = pxValue(doc.height);
|
||||
var margin = Math.round(Math.min(w, h) * 0.25);
|
||||
|
||||
var layer = doc.artLayers.add();
|
||||
layer.name = "shape";
|
||||
|
||||
var region = [
|
||||
[margin, margin],
|
||||
[w - margin, margin],
|
||||
[w - margin, h - margin],
|
||||
[margin, h - margin]
|
||||
];
|
||||
|
||||
doc.selection.select(region);
|
||||
|
||||
var black = new SolidColor();
|
||||
black.rgb.red = 0;
|
||||
black.rgb.green = 0;
|
||||
black.rgb.blue = 0;
|
||||
|
||||
doc.selection.fill(black);
|
||||
doc.selection.deselect();
|
||||
|
||||
return layer;
|
||||
}
|
||||
|
||||
function blendModeTypeID(mode) {
|
||||
try { return charIDToTypeID(mode); }
|
||||
catch (e) { return stringIDToTypeID(mode); }
|
||||
}
|
||||
|
||||
function applyInnerShadow(params) {
|
||||
var desc = new ActionDescriptor();
|
||||
|
||||
var ref = new ActionReference();
|
||||
ref.putProperty(charIDToTypeID("Prpr"), charIDToTypeID("Lefx"));
|
||||
ref.putEnumerated(charIDToTypeID("Lyr "), charIDToTypeID("Ordn"), charIDToTypeID("Trgt"));
|
||||
desc.putReference(charIDToTypeID("null"), ref);
|
||||
|
||||
var fxDesc = new ActionDescriptor();
|
||||
|
||||
var isDesc = new ActionDescriptor();
|
||||
isDesc.putBoolean(charIDToTypeID("enab"), true);
|
||||
isDesc.putBoolean(charIDToTypeID("present"), true);
|
||||
isDesc.putBoolean(charIDToTypeID("showInDialog"), true);
|
||||
|
||||
isDesc.putEnumerated(charIDToTypeID("Blnd"), charIDToTypeID("BlnM"), blendModeTypeID(params.blendMode));
|
||||
|
||||
var clr = new ActionDescriptor();
|
||||
clr.putDouble(charIDToTypeID("Rd "), 0);
|
||||
clr.putDouble(charIDToTypeID("Grn "), 0);
|
||||
clr.putDouble(charIDToTypeID("Bl "), 0);
|
||||
isDesc.putObject(charIDToTypeID("Clr "), charIDToTypeID("RGBC"), clr);
|
||||
|
||||
isDesc.putInteger(charIDToTypeID("Opct"), params.opacity);
|
||||
isDesc.putInteger(charIDToTypeID("lagl"), params.angle);
|
||||
isDesc.putBoolean(charIDToTypeID("uglg"), false);
|
||||
isDesc.putInteger(charIDToTypeID("Dstn"), params.distance);
|
||||
isDesc.putInteger(charIDToTypeID("Ckmt"), params.choke);
|
||||
isDesc.putInteger(charIDToTypeID("blur"), params.size);
|
||||
isDesc.putInteger(charIDToTypeID("Nose"), 0);
|
||||
|
||||
fxDesc.putObject(charIDToTypeID("IrSh"), charIDToTypeID("IrSh"), isDesc);
|
||||
desc.putObject(charIDToTypeID("T "), charIDToTypeID("Lefx"), fxDesc);
|
||||
|
||||
executeAction(charIDToTypeID("setd"), desc, DialogModes.NO);
|
||||
}
|
||||
|
||||
function saveTransparentPng(doc, file) {
|
||||
var pngOptions = new PNGSaveOptions();
|
||||
pngOptions.compression = 6;
|
||||
pngOptions.interlaced = false;
|
||||
doc.saveAs(file, pngOptions, true, Extension.LOWERCASE);
|
||||
}
|
||||
|
||||
var count = 0;
|
||||
var errors = 0;
|
||||
var total = angles.length * distances.length * sizes.length * chokes.length * fxOpacities.length * fxBlendModes.length;
|
||||
|
||||
for (var a = 0; a < angles.length; a++) {
|
||||
for (var d = 0; d < distances.length; d++) {
|
||||
for (var s = 0; s < sizes.length; s++) {
|
||||
for (var c = 0; c < chokes.length; c++) {
|
||||
for (var o = 0; o < fxOpacities.length; o++) {
|
||||
for (var b = 0; b < fxBlendModes.length; b++) {
|
||||
var params = {
|
||||
angle: angles[a],
|
||||
distance: distances[d],
|
||||
size: sizes[s],
|
||||
choke: chokes[c],
|
||||
opacity: fxOpacities[o],
|
||||
blendMode: fxBlendModes[b].key
|
||||
};
|
||||
|
||||
var fileName = "is_" +
|
||||
"a" + params.angle + "_" +
|
||||
"d" + params.distance + "_" +
|
||||
"s" + params.size + "_" +
|
||||
"c" + params.choke + "_" +
|
||||
"op" + params.opacity + "_" +
|
||||
"bm" + fxBlendModes[b].name +
|
||||
".png";
|
||||
|
||||
var file = new File(outFolder + "/" + fileName);
|
||||
if (file.exists) { count++; continue; }
|
||||
|
||||
var doc = createDocument("inner_shadow_test", 256, 256);
|
||||
var layer = addBlackRectangle(doc);
|
||||
|
||||
try {
|
||||
applyInnerShadow(params);
|
||||
saveTransparentPng(doc, file);
|
||||
} catch (e) {
|
||||
$.writeln("Error: " + fileName + " — " + e);
|
||||
errors++;
|
||||
} finally {
|
||||
doc.close(SaveOptions.DONOTSAVECHANGES);
|
||||
}
|
||||
|
||||
count++;
|
||||
if (count % 10 === 0) {
|
||||
$.writeln("Progress: " + count + " / " + total + " (errors: " + errors + ")");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
alert("Done. Generated " + count + " images, " + errors + " errors.\n" + outFolder.fsName);
|
||||
@@ -0,0 +1,283 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Minimal PSD writer — byte-exact per Adobe Photoshop File Formats Specification.
|
||||
Creates a 256x256 RGBA document with one layer (red rectangle).
|
||||
"""
|
||||
import struct, os
|
||||
|
||||
def be16(v): return struct.pack(">H", v)
|
||||
def be32(v): return struct.pack(">I", v)
|
||||
def bei16(v): return struct.pack(">h", v)
|
||||
|
||||
def packbits_compress(row):
|
||||
out = bytearray()
|
||||
n = len(row)
|
||||
i = 0
|
||||
while i < n:
|
||||
# find run
|
||||
run = 1
|
||||
while run < 128 and i + run < n and row[i+run] == row[i]:
|
||||
run += 1
|
||||
if run > 1:
|
||||
out.append((1 - run) & 0xFF)
|
||||
out.append(row[i])
|
||||
i += run
|
||||
else:
|
||||
# literal
|
||||
start = i
|
||||
lit = 0
|
||||
while lit < 128 and i + lit < n:
|
||||
nxt = 0
|
||||
if i + lit + 1 < n and row[i+lit] == row[i+lit+1]:
|
||||
r = 2
|
||||
while r < 128 and i+lit+r < n and row[i+lit+r] == row[i+lit]:
|
||||
r += 1
|
||||
nxt = r
|
||||
if nxt >= 3:
|
||||
break
|
||||
lit += 1
|
||||
if lit == 0:
|
||||
lit = 1
|
||||
out.append(lit - 1)
|
||||
out.extend(row[start:start+lit])
|
||||
i += lit
|
||||
return bytes(out)
|
||||
|
||||
def write_resource(buf, res_id, name, data):
|
||||
buf.extend(b"8BIM")
|
||||
buf.extend(be16(res_id))
|
||||
name_padded = name + b'\x00' if (1 + len(name)) % 2 == 1 else name
|
||||
buf.append(len(name))
|
||||
buf.extend(name)
|
||||
if (1 + len(name)) % 2 == 1:
|
||||
buf.append(0)
|
||||
buf.extend(be32(len(data)))
|
||||
buf.extend(data)
|
||||
if len(data) % 2 != 1:
|
||||
pass # even already
|
||||
# pad data to even
|
||||
if len(data) % 2 != 0:
|
||||
buf.append(0)
|
||||
|
||||
def write_pascal_padded4(buf, s):
|
||||
"""Write Pascal string padded to multiple of 4 bytes."""
|
||||
b = s.encode("ascii") if isinstance(s, str) else s
|
||||
buf.append(len(b))
|
||||
buf.extend(b)
|
||||
total = 1 + len(b)
|
||||
pad = (4 - total % 4) % 4
|
||||
buf.extend(b'\x00' * pad)
|
||||
|
||||
def write_tagged_block(buf, key, data):
|
||||
"""Write 8BIM tagged block."""
|
||||
buf.extend(b"8BIM")
|
||||
buf.extend(key)
|
||||
buf.extend(be32(len(data)))
|
||||
buf.extend(data)
|
||||
if len(data) % 2 != 0:
|
||||
buf.append(0)
|
||||
|
||||
def make_descriptor_string(s):
|
||||
"""Write a descriptor UTF-16 string: length(4) + chars(2*N)."""
|
||||
enc = s.encode("utf-16-be")
|
||||
return be32(len(enc) // 2) + enc
|
||||
|
||||
def main():
|
||||
W, H = 256, 256
|
||||
MARGIN = 64
|
||||
|
||||
# ── Create layer pixels ──
|
||||
rgba = bytearray(W * H * 4)
|
||||
for y in range(H):
|
||||
for x in range(W):
|
||||
i = (y * W + x) * 4
|
||||
if MARGIN <= x < W - MARGIN and MARGIN <= y < H - MARGIN:
|
||||
rgba[i] = 255 # R
|
||||
rgba[i+1] = 0 # G
|
||||
rgba[i+2] = 0 # B
|
||||
rgba[i+3] = 255 # A
|
||||
|
||||
# ── Header (26 bytes) ──
|
||||
header = bytearray()
|
||||
header.extend(b"8BPS")
|
||||
header.extend(be16(1)) # version
|
||||
header.extend(b'\x00' * 6) # reserved
|
||||
header.extend(be16(4)) # channels (RGBA)
|
||||
header.extend(be32(H)) # height
|
||||
header.extend(be32(W)) # width
|
||||
header.extend(be16(8)) # depth
|
||||
header.extend(be16(3)) # color mode (RGB)
|
||||
|
||||
# ── Color Mode Data ──
|
||||
color_mode = be32(0)
|
||||
|
||||
# ── Image Resources ──
|
||||
img_res = bytearray()
|
||||
# 0x03ED: Resolution info
|
||||
res_data = bytearray()
|
||||
res_data.extend(be32(72 << 16)) # hRes (72 DPI fixed)
|
||||
res_data.extend(be16(1)) # hResUnit (1=pixels/inch)
|
||||
res_data.extend(be16(1)) # vResUnit
|
||||
res_data.extend(be32(72 << 16)) # vRes
|
||||
res_data.extend(be16(1)) # widthUnit
|
||||
res_data.extend(be16(1)) # heightUnit
|
||||
write_resource(img_res, 0x03ED, b"", bytes(res_data))
|
||||
|
||||
# 0x0419: Global Angle
|
||||
write_resource(img_res, 0x0419, b"", be32(120))
|
||||
|
||||
# pad img_res to even
|
||||
if len(img_res) % 2 != 0:
|
||||
img_res.append(0)
|
||||
|
||||
# ── Layer & Mask Information ──
|
||||
# Build layer info section first
|
||||
layer_info = bytearray()
|
||||
|
||||
# Layer count (positive = no groups)
|
||||
layer_info.extend(bei16(1))
|
||||
|
||||
# Layer record
|
||||
layer_rec = bytearray()
|
||||
# Rect: top, left, bottom, right
|
||||
layer_rec.extend(be32(0)) # top
|
||||
layer_rec.extend(be32(0)) # left
|
||||
layer_rec.extend(be32(H)) # bottom
|
||||
layer_rec.extend(be32(W)) # right
|
||||
|
||||
# Channel count
|
||||
layer_rec.extend(be16(4))
|
||||
|
||||
# Channel info: (-1=alpha, 0=R, 1=G, 2=B)
|
||||
# We'll compute channel data lengths later
|
||||
ch_ids = [-1, 0, 1, 2]
|
||||
ch_data_list = []
|
||||
for ch_id in ch_ids:
|
||||
plane = bytearray()
|
||||
for y in range(H):
|
||||
for x in range(W):
|
||||
idx = (y * W + x) * 4
|
||||
if ch_id == -1:
|
||||
plane.append(rgba[idx + 3])
|
||||
elif ch_id == 0:
|
||||
plane.append(rgba[idx])
|
||||
elif ch_id == 1:
|
||||
plane.append(rgba[idx + 1])
|
||||
elif ch_id == 2:
|
||||
plane.append(rgba[idx + 2])
|
||||
ch_data_list.append(plane)
|
||||
|
||||
# Compress each channel
|
||||
ch_compressed = []
|
||||
for plane in ch_data_list:
|
||||
row_lengths = []
|
||||
compressed_rows = []
|
||||
for y in range(H):
|
||||
row = plane[y * W : (y + 1) * W]
|
||||
compressed = packbits_compress(row)
|
||||
row_lengths.append(len(compressed))
|
||||
compressed_rows.append(compressed)
|
||||
ch_compressed.append((row_lengths, compressed_rows))
|
||||
|
||||
# Channel data = compression(2) + row_lengths(H*2) + compressed_data
|
||||
ch_packed = []
|
||||
for row_lengths, compressed_rows in ch_compressed:
|
||||
buf = bytearray()
|
||||
buf.extend(be16(1)) # compression type = RLE
|
||||
for rl in row_lengths:
|
||||
buf.extend(be16(rl))
|
||||
for cr in compressed_rows:
|
||||
buf.extend(cr)
|
||||
ch_packed.append(bytes(buf))
|
||||
|
||||
# Write channel info records
|
||||
for i, ch_id in enumerate(ch_ids):
|
||||
layer_rec.extend(bei16(ch_id))
|
||||
layer_rec.extend(be32(len(ch_packed[i])))
|
||||
|
||||
# Blend mode
|
||||
layer_rec.extend(b"8BIMnorm")
|
||||
layer_rec.append(255) # opacity
|
||||
layer_rec.append(0) # clipping (base)
|
||||
layer_rec.append(0) # flags (visible)
|
||||
layer_rec.append(0) # filler
|
||||
|
||||
# Extra data
|
||||
extra = bytearray()
|
||||
# Layer mask data (4 bytes, length=0)
|
||||
extra.extend(be32(0))
|
||||
# Blending ranges (4 bytes length=40, then 40 bytes of data)
|
||||
extra.extend(be32(40))
|
||||
for _ in range(10):
|
||||
extra.extend(be16(0))
|
||||
extra.extend(be16(65535))
|
||||
# Layer name (Pascal string padded to 4)
|
||||
write_pascal_padded4(extra, "Red Rectangle")
|
||||
|
||||
layer_rec.extend(be32(len(extra)))
|
||||
layer_rec.extend(extra)
|
||||
|
||||
# Append channel data
|
||||
layer_info.extend(layer_rec)
|
||||
for ch_data in ch_packed:
|
||||
layer_info.extend(ch_data)
|
||||
|
||||
# Pad layer info to even
|
||||
if len(layer_info) % 2 != 0:
|
||||
layer_info.append(0)
|
||||
|
||||
# Global Layer Mask Info (empty, 4 bytes)
|
||||
glmi = be32(0)
|
||||
|
||||
# Layer & Mask Information section
|
||||
lami = bytearray()
|
||||
lami.extend(be32(len(layer_info) + len(glmi))) # length of rest
|
||||
lami.extend(layer_info)
|
||||
lami.extend(glmi)
|
||||
|
||||
# ── Merged Image Data ──
|
||||
merged = bytearray()
|
||||
merged.extend(be16(1)) # compression = RLE
|
||||
|
||||
# Row lengths for all channels (per-channel order: RRR...GGG...BBB...AAA...)
|
||||
for ch_offset in range(4): # R, G, B, A
|
||||
for y in range(H):
|
||||
row = bytes(rgba[(y * W + ch_offset)::4][:W]) # wrong, need proper extraction
|
||||
row = bytearray()
|
||||
for x in range(W):
|
||||
idx = (y * W + x) * 4 + ch_offset
|
||||
row.append(rgba[idx])
|
||||
compressed = packbits_compress(row)
|
||||
merged.extend(be16(len(compressed)))
|
||||
|
||||
# Compressed data for all channels
|
||||
for ch_offset in range(4):
|
||||
for y in range(H):
|
||||
row = bytearray()
|
||||
for x in range(W):
|
||||
idx = (y * W + x) * 4 + ch_offset
|
||||
row.append(rgba[idx])
|
||||
compressed = packbits_compress(row)
|
||||
merged.extend(compressed)
|
||||
|
||||
# ── Assemble PSD ──
|
||||
psd = bytearray()
|
||||
psd.extend(header)
|
||||
psd.extend(color_mode)
|
||||
psd.extend(be32(len(img_res)))
|
||||
psd.extend(img_res)
|
||||
psd.extend(lami)
|
||||
psd.extend(merged)
|
||||
|
||||
out_path = "_tmp/python_ref_test.psd"
|
||||
with open(out_path, "wb") as f:
|
||||
f.write(psd)
|
||||
print(f"Wrote {len(psd)} bytes to {out_path}")
|
||||
|
||||
# Also verify roundtrip
|
||||
assert psd[:4] == b"8BPS"
|
||||
assert len(psd) == 26 + 4 + 4 + len(img_res) + len(lami) + len(merged)
|
||||
print("Roundtrip check: OK")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE animation-metadata PUBLIC '-//KDE//DTD krita 1.1//EN' 'http://www.calligra.org/DTD/krita-1.1.dtd'>
|
||||
<animation-metadata xmlns="http://www.calligra.org/DTD/krita">
|
||||
<framerate type="value" value="24"/>
|
||||
<range to="100" type="timerange" from="0"/>
|
||||
<currentTime type="value" value="0"/>
|
||||
<export-settings>
|
||||
<sequenceFilePath type="value" value=""/>
|
||||
<sequenceBaseName type="value" value=""/>
|
||||
<sequenceInitialFrameNumber type="value" value="-1"/>
|
||||
</export-settings>
|
||||
</animation-metadata>
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,5 @@
|
||||
VERSION 2
|
||||
TILEWIDTH 64
|
||||
TILEHEIGHT 64
|
||||
PIXELSIZE 4
|
||||
DATA 0
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE document-info PUBLIC '-//KDE//DTD document-info 1.1//EN' 'http://www.calligra.org/DTD/document-info-1.1.dtd'>
|
||||
<document-info xmlns="http://www.calligra.org/DTD/document-info">
|
||||
<about>
|
||||
<title></title>
|
||||
<description></description>
|
||||
<subject></subject>
|
||||
<abstract><![CDATA[]]></abstract>
|
||||
<keyword></keyword>
|
||||
<initial-creator>Unknown</initial-creator>
|
||||
<editing-cycles>1</editing-cycles>
|
||||
<editing-time>0</editing-time>
|
||||
<date>2026-06-17T17:50:18</date>
|
||||
<creation-date>2026-06-17T17:48:05</creation-date>
|
||||
<language></language>
|
||||
<license></license>
|
||||
</about>
|
||||
<author>
|
||||
<full-name></full-name>
|
||||
<creator-first-name></creator-first-name>
|
||||
<creator-last-name></creator-last-name>
|
||||
<initial></initial>
|
||||
<author-title></author-title>
|
||||
<position></position>
|
||||
<company></company>
|
||||
</author>
|
||||
</document-info>
|
||||
@@ -0,0 +1,32 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE DOC PUBLIC '-//KDE//DTD krita 2.0//EN' 'http://www.calligra.org/DTD/krita-2.0.dtd'>
|
||||
<DOC xmlns="http://www.calligra.org/DTD/krita" editor="Krita" kritaVersion="5.2.11-prealpha" syntaxVersion="2.0">
|
||||
<IMAGE height="800" description="" name="Unnamed" mime="application/x-kra" profile="sRGB-elle-V2-srgbtrc.icc" width="800" x-res="72.0000000000058" y-res="72.0000000000058" colorspacename="RGBA">
|
||||
<layers>
|
||||
<layer intimeline="0" name="Layer 1" locked="0" collapsed="0" channelflags="" uuid="{e58100bc-fcda-43b3-ac7d-b834b7baacb6}" colorspacename="RGBA" filename="layer2" opacity="255" compositeop="normal" colorlabel="0" layerstyle="{d7a0844e-a522-4673-8c83-aa4f45e7782f}" channellockflags="" y="0" x="0" onionskin="0" visible="1" nodetype="paintlayer"/>
|
||||
<layer intimeline="0" name="Layer 4" locked="0" collapsed="0" channelflags="" uuid="{633fd10a-20f2-4a99-8e29-aa27df65df0e}" colorspacename="RGBA" filename="layer3" opacity="255" compositeop="normal" colorlabel="0" selected="true" layerstyle="{7d378444-e311-41d3-b9c6-a19e2a8e5899}" channellockflags="" y="0" x="0" onionskin="0" visible="1" nodetype="paintlayer"/>
|
||||
<layer intimeline="0" name="Layer 3" locked="0" collapsed="0" channelflags="" uuid="{82aae6cb-88d2-43dd-b32b-8f697a58001f}" colorspacename="RGBA" filename="layer4" opacity="255" compositeop="normal" colorlabel="0" layerstyle="{117f8514-88e4-4553-bccb-44a87b6b9182}" channellockflags="" y="0" x="0" onionskin="0" visible="1" nodetype="paintlayer"/>
|
||||
<layer intimeline="0" name="Layer 2" locked="0" collapsed="0" channelflags="" uuid="{12fdb980-bba4-4d3f-94f0-9a88a529dc76}" colorspacename="RGBA" filename="layer5" opacity="255" compositeop="normal" colorlabel="0" layerstyle="{4bbe8f2c-7ad2-4e31-8af0-9ad6715cacf4}" channellockflags="" y="0" x="0" onionskin="0" visible="1" nodetype="paintlayer"/>
|
||||
<layer filename="layer6" y="0" intimeline="0" name="Background" visible="1" channellockflags="" opacity="255" nodetype="paintlayer" channelflags="" uuid="{c9e22005-6a14-42ff-bfc6-32ce0b70f20c}" x="0" onionskin="0" locked="0" compositeop="normal" collapsed="0" colorlabel="0" colorspacename="RGBA"/>
|
||||
</layers>
|
||||
<ProjectionBackgroundColor ColorData="AAAAAA=="/>
|
||||
<GlobalAssistantsColor SimpleColorData="176,176,176,255"/>
|
||||
<Palettes/>
|
||||
<resources/>
|
||||
<animation>
|
||||
<framerate type="value" value="24"/>
|
||||
<range to="100" type="timerange" from="0"/>
|
||||
<currentTime type="value" value="0"/>
|
||||
</animation>
|
||||
<Annotations>
|
||||
<Annotation description="0x0408 - Grid & guide info" type="PSD Resource Block: 1032"/>
|
||||
<Annotation description="0x040c - Thumbnail resource" type="PSD Resource Block: 1036"/>
|
||||
<Annotation description="0x040d - Global angle" type="PSD Resource Block: 1037"/>
|
||||
<Annotation description="0x0414 - Document specific IDs" type="PSD Resource Block: 1044"/>
|
||||
<Annotation description="0x0415 - Unicode alpha names" type="PSD Resource Block: 1045"/>
|
||||
<Annotation description="0x0419 - Global altitude" type="PSD Resource Block: 1049"/>
|
||||
<Annotation description="0x041a - Slices" type="PSD Resource Block: 1050"/>
|
||||
<Annotation description="0x0435 - (Photoshop CS3) DisplayInfo structure to support floating point colors. Also see ID 1007. See Appendix A in Photoshop API Guide.pdf ." type="PSD Resource Block: 1077"/>
|
||||
</Annotations>
|
||||
</IMAGE>
|
||||
</DOC>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 128 KiB |
@@ -0,0 +1 @@
|
||||
application/x-krita
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 23 KiB |
Reference in New Issue
Block a user