From 00f2cbe71a39a2bf551bfa29fbb60bf3657a0d4e Mon Sep 17 00:00:00 2001 From: AskaEth Date: Thu, 27 Aug 2026 09:06:03 +0800 Subject: [PATCH] Write standard tagged blocks (lnsr/lyid/clbl/infx/knko/lspf/lclr/fxrp) per layer Photoshop reads a layer's tagged blocks until it sees a non-8BIM signature. With only 'luni' the next bytes are the channel data, which PS mis-parses and the layer pixels come out empty. PS writes the standard block set, so we now mirror it (verified: blocks list now matches the reference PSD). --- ppl2psd.py | 32 +++++++++++++++++++++++++++++--- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/ppl2psd.py b/ppl2psd.py index 1361ed7..8f6dc52 100644 --- a/ppl2psd.py +++ b/ppl2psd.py @@ -463,7 +463,29 @@ def channel_blocks(img: Image.Image, workers: int, return list(zip(ids, enc)) -def _layer_record(top, left, bottom, right, name, opacity, visible, channels) -> bytes: +def _standard_blocks(layer_id: int) -> bytes: + """Standard tagged blocks Photoshop writes for every layer. PS reads + tagged blocks until it hits a non-8BIM signature; with only 'luni' the + next bytes are channel data and PS mis-parses the record, so we write + the usual block set like PS itself does.""" + items = [ + (b"lnsr", b"\x00\x00\x00\x00"), # name source + (b"lyid", struct.pack(">I", layer_id)), # layer id + (b"clbl", b"\x00\x00\x00\x00"), # blend clipping + (b"infx", b"\x00\x00\x00\x00"), # blend interior + (b"knko", b"\x00\x00\x00\x00"), # knockout + (b"lspf", b"\x00\x00\x00\x00"), # protected + (b"lclr", b"\x00" * 8), # sheet color + (b"fxrp", b"\x00" * 16), # reference point + ] + out = bytearray() + for key, payload in items: + out += b"8BIM" + key + struct.pack(">I", len(payload)) + payload + return bytes(out) + + +def _layer_record(top, left, bottom, right, name, opacity, visible, channels, + layer_id=1) -> bytes: rec = bytearray() rec += struct.pack(">4i", top, left, bottom, right) rec += struct.pack(">H", len(channels)) @@ -489,6 +511,8 @@ def _layer_record(top, left, bottom, right, name, opacity, visible, channels) -> # luni: unicode layer name uni = name.encode("utf-16-be") extra += b"8BIMluni" + struct.pack(">I", len(uni)) + uni + # standard blocks Photoshop expects (see _standard_blocks) + extra += _standard_blocks(layer_id) rec += struct.pack(">I", len(extra)) rec += extra return bytes(rec) @@ -506,11 +530,13 @@ def build_psd(layers, width: int, height: int, out_path: str, workers: int, # layer records + channel payloads records = bytearray() payload = bytearray() - for (name, opacity, visible, (lx, ly), img), ch in zip(layers, chan_data): + for li, (name, opacity, visible, (lx, ly), img) in enumerate(layers): + ch = chan_data[li] l, t = lx, ly r = l + img.width b = t + img.height - records += _layer_record(t, l, b, r, name, opacity, visible, ch) + records += _layer_record(t, l, b, r, name, opacity, visible, ch, + layer_id=li + 1) for _, p in ch: payload += p