Fix blending ranges: zero composite range made PS skip layer blending
PS reads layer blending ranges to decide if a layer participates in compositing. psd_tools wrote all-zero ranges (composite 0..0), which tells Photoshop the layer has an empty blend range -> the canvas renders empty even though layer pixels are correct (Ctrl+C/V shows content, export PNG is transparent, save-as by PS keeps the bad ranges). Now writes full-range (0..65535) composite + channel ranges like PS-generated files, verified against the reference PSD.
This commit is contained in:
1 parent
8af857d45a
commit
edef8799ce
1 file changed
+18
-12
+18
-12
@@ -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)
|
||||
|
||||
Reference in new issue
Block a user