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
This commit is contained in:
AskaEth committed 2026-08-27 08:27:05 +08:00
1 parent 60624f01c2
commit a5757d1f07
1 file changed
+16 -6
+16 -6
View File
@@ -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)