diff --git a/ppl2psd.py b/ppl2psd.py index 612dafe..6c65434 100644 --- a/ppl2psd.py +++ b/ppl2psd.py @@ -501,7 +501,10 @@ def _layer_record(top, left, bottom, right, name, opacity, visible, channels, # extra data extra = bytearray() extra += struct.pack(">I", 0) # layer mask data length - extra += struct.pack(">I", 8) + b"\x00" * 8 # blending ranges (default) + # blending ranges: composite + 4 channels, ALL full range (0..65535). + # PS writes these as 0x0000/0xffff; a zero composite range tells PS the + # layer does not participate in blending -> the canvas renders empty. + extra += struct.pack(">I", 40) + b"\x00\x00\xff\xff\x00\x00\xff\xff" * 5 nb = name.encode("utf-8") extra += bytes([min(len(nb), 255)]) + nb[:255] # pad pascal string to a multiple of 4 bytes @@ -548,27 +551,30 @@ def build_psd(layers, width: int, height: int, out_path: str, workers: int, for name, opacity, visible, (lx, ly), img in layers: if visible and opacity > 0: merged.paste(img, (lx, ly), img) - merged_rgb = merged.convert("RGB") # transparent -> black - # Merged image data is written RAW (compression=0) for maximum - # compatibility with third-party viewers, which often only parse RAW - # merged data and stretch RLE data. Per spec there is ONE 2-byte - # compression field followed by all planes in planar order (RRR GGG BBB). + merged_rgb = None # keep the RGBA merged image for the 4-channel output + # Merged image data is written RAW with a single compression field and + # all planes in planar order (R G B A). 4 channels because the document + # has transparency (negative layer count references the merged alpha). if np is not None: - arr = np.asarray(merged_rgb) + arr = np.asarray(merged) merged_block = b"\x00\x00" + b"".join( - arr[..., i].tobytes() for i in range(3)) + arr[..., i].tobytes() for i in range(4)) else: merged_block = b"\x00\x00" + b"".join( - merged_rgb.getchannel(c).tobytes() for c in "RGB") + merged.getchannel(c).tobytes() for c in "RGBA") - # assemble file + # assemble file. Header channels = 4 (document has transparency, like PS + # writes when layers have alpha). fh = open(out_path, "wb") - fh.write(struct.pack(">4sH6sHIIHH", b"8BPS", 1, b"\x00" * 6, 3, height, width, 8, 3)) + fh.write(struct.pack(">4sH6sHIIHH", b"8BPS", 1, b"\x00" * 6, 4, height, width, 8, 3)) fh.write(struct.pack(">I", 0)) # color mode data fh.write(struct.pack(">I", 0)) # image resources # Layer info section: [Length(4)] + [layer count(2) + records + channel data] - layer_info_body = struct.pack(">h", len(layers)) + bytes(records) + bytes(payload) + # Negative layer count: abs = layer count, first alpha channel of the + # merged image holds the merged transparency (PS writes this when the + # document has transparent layers). + 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)