From a5757d1f07646e15ab2bb55e14aaddb211cf3801 Mon Sep 17 00:00:00 2001 From: AskaEth Date: Thu, 27 Aug 2026 08:27:05 +0800 Subject: [PATCH] Fix PSD structure: add missing LayerInfo length field and correct channel order - Layer info section was missing its 4-byte length field, causing all subsequent offsets to shift and Photoshop to fail with 'unexpected end of file' - Layer channel data was ordered [R,G,B,A] with ids [-1,0,1,2], shifting every channel by one (alpha held red, red held green, ...) - fixed to [A,R,G,B] so id -1 is the alpha mask - Verified: psd_tools opens the PSD, per-layer pixels match the rendered originals with 0.000 mean difference --- ppl2psd.py | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/ppl2psd.py b/ppl2psd.py index 1c519b5..b3b1e83 100644 --- a/ppl2psd.py +++ b/ppl2psd.py @@ -432,9 +432,17 @@ def channel_blocks(img: Image.Image, workers: int, # zero RGB where alpha==0 for better compression arr = arr.copy() arr[arr[..., 3] == 0] = 0 - planes = [arr[..., i].tobytes() for i in range(nch)] + # PSD layer channel ids: -1=alpha mask, 0=R, 1=G, 2=B + planes = [arr[..., 3].tobytes()] + [arr[..., i].tobytes() for i in range(3)] + else: + planes = [arr[..., i].tobytes() for i in range(nch)] else: - planes = [img.getchannel(c).tobytes() for c in img.getbands()] + bands = img.getbands() + if bands == ("R", "G", "B", "A"): + planes = [img.getchannel("A").tobytes()] + \ + [img.getchannel(c).tobytes() for c in "RGB"] + else: + planes = [img.getchannel(c).tobytes() for c in bands] w = img.width jobs = [(plane, w) for plane in planes] if workers > 1 and len(jobs) > 1: @@ -518,10 +526,12 @@ def build_psd(layers, width: int, height: int, out_path: str, workers: int, fh.write(struct.pack(">I", 0)) # color mode data fh.write(struct.pack(">I", 0)) # image resources - layer_info = bytes(records) + bytes(payload) - count_payload = struct.pack(">h", len(layers)) + layer_info - lam = struct.pack(">I", len(count_payload) + 4) + count_payload - lam += struct.pack(">I", 0) # global layer mask info + # Layer info section: [Length(4)] + [layer count(2) + records + channel data] + layer_info_body = struct.pack(">h", len(layers)) + bytes(records) + bytes(payload) + layer_info = struct.pack(">I", len(layer_info_body)) + layer_info_body + # layer & mask section: length = layer info + global layer mask info + # (the outer length does NOT include its own 4-byte length field) + lam = layer_info + struct.pack(">I", 0) # global layer mask info fh.write(struct.pack(">I", len(lam)) + lam) # merged image data (RGB, RLE)