From 29305e3bfa12c22798930321e4a34292700747c4 Mon Sep 17 00:00:00 2001 From: jeevan6996 Date: Mon, 12 Oct 2026 20:06:03 +0100 Subject: [PATCH 1/5] fix(psd): accept empty layer rectangles Allow empty dimensions only for layer records, preserving canvas and decoded channel validation and all upper size limits. Assisted-by: Codex / model identifier unavailable Signed-off-by: jeevan6996 --- CHANGES.md | 1 + src/cmake/testing.cmake | 1 + src/psd.imageio/psdinput.cpp | 13 ++-- testsuite/psd-empty-layers/ref/out.txt | 62 +++++++++++++++++++ testsuite/psd-empty-layers/run.py | 15 +++++ .../src/make-empty-layer-psds.py | 60 ++++++++++++++++++ 6 files changed, 147 insertions(+), 5 deletions(-) create mode 100644 testsuite/psd-empty-layers/ref/out.txt create mode 100644 testsuite/psd-empty-layers/run.py create mode 100644 testsuite/psd-empty-layers/src/make-empty-layer-psds.py diff --git a/CHANGES.md b/CHANGES.md index a592ad6e50..f0a8d9cc2a 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -23,6 +23,7 @@ Release 3.3 (anticipated Sep/Oct, 2027?) -- compared to 3.2.1.0 ### 🐛 Fixes and feature enhancements - *dicom*: A failed `seek_subimage` no longer leaves the reader pointing at a refused subimage, and frames past the first are now reachable. [#5474](https://github.com/AcademySoftwareFoundation/OpenImageIO/pull/5474) +- *psd*: Accept empty adjustment and pixel layer rectangles without rejecting the document, while retaining canvas and layer size limits. - *jpegxl*: Propagate encoder and write failures at `close()` to callers, reject incomplete pixel buffers before calling libjxl, and clear buffered scanlines when an output is reused. [#5509](https://github.com/AcademySoftwareFoundation/OpenImageIO/pull/5509) (by @zachlewis / Zach Lewis) - *png*: Preserve PNG Third Edition mastering display metadata (mDCV) as ten integer `mdcv_*` attributes on read (PNG and PNG-encoded ICO); written only when the record is complete, valid, and accompanied by cICP. [#5512](https://github.com/AcademySoftwareFoundation/OpenImageIO/pull/5512) (by @zachlewis / Zach Lewis) - *iconvert*: Use a private ImageCache to gather subimage specs, avoiding a shutdown-time crash from files left open in the shared cache. [#5508](https://github.com/AcademySoftwareFoundation/OpenImageIO/pull/5508) (by @zachlewis / Zach Lewis) diff --git a/src/cmake/testing.cmake b/src/cmake/testing.cmake index 15ffa40a0d..a8da380691 100644 --- a/src/cmake/testing.cmake +++ b/src/cmake/testing.cmake @@ -545,6 +545,7 @@ macro (oiio_add_all_tests) oiio_add_tests (psd psd-colormodes ENABLEVAR ENABLE_PSD IMAGEDIR oiio-images/psd) + oiio_add_tests (psd-empty-layers ENABLEVAR ENABLE_PSD) oiio_add_tests (ptex FOUNDVAR Ptex_FOUND ENABLEVAR ENABLE_PTEX) oiio_add_tests (raw raw-thumbnail diff --git a/src/psd.imageio/psdinput.cpp b/src/psd.imageio/psdinput.cpp index 83fcd54756..3b15d37973 100644 --- a/src/psd.imageio/psdinput.cpp +++ b/src/psd.imageio/psdinput.cpp @@ -237,7 +237,8 @@ class PSDInput final : public ImageInput { bool read_header(); bool validate_header(); static bool validate_signature(const char signature[4]); - bool validate_resolution(string_view name, uint32_t width, uint32_t height); + bool validate_resolution(string_view name, uint32_t width, uint32_t height, + bool allow_empty = false); //Color Mode Data bool load_color_data(); @@ -1010,13 +1011,14 @@ PSDInput::validate_signature(const char signature[4]) bool -PSDInput::validate_resolution(string_view name, uint32_t width, uint32_t height) +PSDInput::validate_resolution(string_view name, uint32_t width, uint32_t height, + bool allow_empty) { - if (width < 1 || width > m_maxres) { + if ((!allow_empty && width < 1) || width > m_maxres) { errorfmt("[{}] invalid image width {}", name, width); return false; } - if (height < 1 || height > m_maxres) { + if ((!allow_empty && height < 1) || height > m_maxres) { errorfmt("[{}] invalid image height {}", name, height); return false; } @@ -1632,7 +1634,8 @@ PSDInput::load_layer(Layer& layer) int64_t h = int64_t(layer.bottom) - int64_t(layer.top); layer.width = uint32_t(w < 0 ? -w : w); layer.height = uint32_t(h < 0 ? -h : h); - if (!validate_resolution("Layer Record", layer.width, layer.height)) + // Adjustment layers and empty pixel layers may have an empty rectangle. + if (!validate_resolution("Layer Record", layer.width, layer.height, true)) return false; layer.channel_info.resize(layer.channel_count); for (uint16_t channel = 0; channel < layer.channel_count; channel++) { diff --git a/testsuite/psd-empty-layers/ref/out.txt b/testsuite/psd-empty-layers/ref/out.txt new file mode 100644 index 0000000000..93384dbbac --- /dev/null +++ b/testsuite/psd-empty-layers/ref/out.txt @@ -0,0 +1,62 @@ +empty-layer-0x0-0.psd : 2 x 2, 3 channel, uint8 psd (4 subimages) + subimage 0: 2 x 2, 3 channel, uint8 psd + SHA-1: 6585CA1F6B702CF27768484219F6F9200F4E9192 + subimage 1: 1 x 1, 3 channel, uint8 psd (4 subimages) + SHA-1: E3FF44389E80210E4545A907BE55980CEB6EA821 + subimage 2: 0 x 0, 3 channel, uint8 psd (4 subimages) + SHA-1: DA39A3EE5E6B4B0D3255BFEF95601890AFD80709 + subimage 3: 1 x 1, 3 channel, uint8 psd (4 subimages) + SHA-1: 5818BEB65A1338D7C80684EEC2EA2ECB0DB183D0 +empty-layer-0x2-0.psd : 2 x 2, 3 channel, uint8 psd (4 subimages) + subimage 0: 2 x 2, 3 channel, uint8 psd + SHA-1: 6585CA1F6B702CF27768484219F6F9200F4E9192 + subimage 1: 1 x 1, 3 channel, uint8 psd (4 subimages) + SHA-1: E3FF44389E80210E4545A907BE55980CEB6EA821 + subimage 2: 0 x 2, 3 channel, uint8 psd (4 subimages) + SHA-1: DA39A3EE5E6B4B0D3255BFEF95601890AFD80709 + subimage 3: 1 x 1, 3 channel, uint8 psd (4 subimages) + SHA-1: 5818BEB65A1338D7C80684EEC2EA2ECB0DB183D0 +empty-layer-2x0-0.psd : 2 x 2, 3 channel, uint8 psd (4 subimages) + subimage 0: 2 x 2, 3 channel, uint8 psd + SHA-1: 6585CA1F6B702CF27768484219F6F9200F4E9192 + subimage 1: 1 x 1, 3 channel, uint8 psd (4 subimages) + SHA-1: E3FF44389E80210E4545A907BE55980CEB6EA821 + subimage 2: 2 x 0, 3 channel, uint8 psd (4 subimages) + SHA-1: DA39A3EE5E6B4B0D3255BFEF95601890AFD80709 + subimage 3: 1 x 1, 3 channel, uint8 psd (4 subimages) + SHA-1: 5818BEB65A1338D7C80684EEC2EA2ECB0DB183D0 +empty-layer-0x0-1.psd : 2 x 2, 3 channel, uint8 psd (4 subimages) + subimage 0: 2 x 2, 3 channel, uint8 psd + SHA-1: 6585CA1F6B702CF27768484219F6F9200F4E9192 + subimage 1: 1 x 1, 3 channel, uint8 psd (4 subimages) + SHA-1: E3FF44389E80210E4545A907BE55980CEB6EA821 + subimage 2: 0 x 0, 3 channel, uint8 psd (4 subimages) + SHA-1: DA39A3EE5E6B4B0D3255BFEF95601890AFD80709 + subimage 3: 1 x 1, 3 channel, uint8 psd (4 subimages) + SHA-1: 5818BEB65A1338D7C80684EEC2EA2ECB0DB183D0 +empty-layer-0x2-1.psd : 2 x 2, 3 channel, uint8 psd (4 subimages) + subimage 0: 2 x 2, 3 channel, uint8 psd + SHA-1: 6585CA1F6B702CF27768484219F6F9200F4E9192 + subimage 1: 1 x 1, 3 channel, uint8 psd (4 subimages) + SHA-1: E3FF44389E80210E4545A907BE55980CEB6EA821 + subimage 2: 0 x 2, 3 channel, uint8 psd (4 subimages) + SHA-1: DA39A3EE5E6B4B0D3255BFEF95601890AFD80709 + subimage 3: 1 x 1, 3 channel, uint8 psd (4 subimages) + SHA-1: 5818BEB65A1338D7C80684EEC2EA2ECB0DB183D0 +empty-layer-2x0-1.psd : 2 x 2, 3 channel, uint8 psd (4 subimages) + subimage 0: 2 x 2, 3 channel, uint8 psd + SHA-1: 6585CA1F6B702CF27768484219F6F9200F4E9192 + subimage 1: 1 x 1, 3 channel, uint8 psd (4 subimages) + SHA-1: E3FF44389E80210E4545A907BE55980CEB6EA821 + subimage 2: 2 x 0, 3 channel, uint8 psd (4 subimages) + SHA-1: DA39A3EE5E6B4B0D3255BFEF95601890AFD80709 + subimage 3: 1 x 1, 3 channel, uint8 psd (4 subimages) + SHA-1: 5818BEB65A1338D7C80684EEC2EA2ECB0DB183D0 +oiiotool ERROR: read : [Header] invalid image height 0 +failed to open "empty-layer-invalid-canvas.psd": failed load_header +Full command line was: +> oiiotool --info -v -a --hash empty-layer-invalid-canvas.psd +oiiotool ERROR: read : [Layer Record] invalid image height 30001 +failed to open "empty-layer-oversized.psd": failed load_layers +Full command line was: +> oiiotool --info -v -a --hash empty-layer-oversized.psd diff --git a/testsuite/psd-empty-layers/run.py b/testsuite/psd-empty-layers/run.py new file mode 100644 index 0000000000..c59d6f915f --- /dev/null +++ b/testsuite/psd-empty-layers/run.py @@ -0,0 +1,15 @@ +#!/usr/bin/env python + +# Copyright Contributors to the OpenImageIO project. +# SPDX-License-Identifier: Apache-2.0 +# https://github.com/AcademySoftwareFoundation/OpenImageIO + +redirect = ' >> out.txt 2>&1 ' +command += run_app(f'"{pythonbin}" src/make-empty-layer-psds.py') +for compression in (0, 1): + for width, height in ((0, 0), (0, 2), (2, 0)): + filename = f"empty-layer-{width}x{height}-{compression}.psd" + command += info_command(filename, verbose=False, extraargs="-a") + +command += info_command("empty-layer-invalid-canvas.psd", failureok=True) +command += info_command("empty-layer-oversized.psd", failureok=True) diff --git a/testsuite/psd-empty-layers/src/make-empty-layer-psds.py b/testsuite/psd-empty-layers/src/make-empty-layer-psds.py new file mode 100644 index 0000000000..2f399cd523 --- /dev/null +++ b/testsuite/psd-empty-layers/src/make-empty-layer-psds.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python + +# Copyright Contributors to the OpenImageIO project. +# SPDX-License-Identifier: Apache-2.0 +# https://github.com/AcademySoftwareFoundation/OpenImageIO + +import struct +from pathlib import Path + + +def layer(width, height, compression, name, values=(32, 96, 160)): + encoded_name = name.encode("ascii") + pascal_name = bytes([len(encoded_name)]) + encoded_name + pascal_name += bytes(-len(pascal_name) % 4) + extra = struct.pack(">II", 0, 0) + pascal_name + channels = [] + for value in values: + if not width or not height: + data = struct.pack(">H", compression) + else: + data = struct.pack(">H", 0) + bytes([value]) * width * height + channels.append(data) + record = struct.pack(">iiiiH", 0, 0, height, width, 3) + record += b"".join( + struct.pack(">hI", channel, len(data)) + for channel, data in enumerate(channels) + ) + record += b"8BIMnorm" + bytes([255, 0, 0, 0]) + record += struct.pack(">I", len(extra)) + extra + return record, b"".join(channels) + + +def write_psd(filename, width, height, compression, canvas_width=2): + header = b"8BPS" + struct.pack(">H6sHIIHH", 1, bytes(6), 3, + 2, canvas_width, 8, 3) + records = [layer(1, 1, 0, "before"), + layer(width, height, compression, "empty"), + layer(1, 1, 0, "after", values=(160, 96, 32))] + layer_info = struct.pack(">h", len(records)) + layer_info += b"".join(record for record, data in records) + layer_info += b"".join(data for record, data in records) + layer_info += bytes(len(layer_info) % 2) + layer_mask = struct.pack(">I", len(layer_info)) + layer_info + layer_mask += struct.pack(">I", 0) + composite = struct.pack(">H", 0) + composite += b"".join(bytes([v]) * 4 for v in (32, 96, 160)) + Path(filename).write_bytes( + header + struct.pack(">II", 0, 0) + + struct.pack(">I", len(layer_mask)) + layer_mask + composite + ) + + +for compression in (0, 1): + for width, height in ((0, 0), (0, 2), (2, 0)): + write_psd(f"empty-layer-{width}x{height}-{compression}.psd", + width, height, compression) + +# An empty layer must not bypass the existing document and layer size limits. +write_psd("empty-layer-invalid-canvas.psd", 0, 0, 0, canvas_width=0) +write_psd("empty-layer-oversized.psd", 0, 30001, 0) From f5860414afa4975339f1dc98fc42ab4d128fae3f Mon Sep 17 00:00:00 2001 From: jeevan6996 Date: Mon, 12 Oct 2026 20:31:27 +0100 Subject: [PATCH 2/5] test(psd): cover hidden empty layers Verify hidden empty layers retain subimage ordering and hashes, with a populated hidden-layer control. Assisted-by: Codex / model identifier unavailable Signed-off-by: jeevan6996 --- testsuite/psd-empty-layers/ref/out.txt | 27 +++++++++++++++++++ testsuite/psd-empty-layers/run.py | 4 +++ .../src/make-empty-layer-psds.py | 14 +++++++--- 3 files changed, 41 insertions(+), 4 deletions(-) diff --git a/testsuite/psd-empty-layers/ref/out.txt b/testsuite/psd-empty-layers/ref/out.txt index 93384dbbac..94e1febd91 100644 --- a/testsuite/psd-empty-layers/ref/out.txt +++ b/testsuite/psd-empty-layers/ref/out.txt @@ -52,6 +52,33 @@ empty-layer-2x0-1.psd : 2 x 2, 3 channel, uint8 psd (4 subimages) SHA-1: DA39A3EE5E6B4B0D3255BFEF95601890AFD80709 subimage 3: 1 x 1, 3 channel, uint8 psd (4 subimages) SHA-1: 5818BEB65A1338D7C80684EEC2EA2ECB0DB183D0 +hidden-empty-layer-0.psd : 2 x 2, 3 channel, uint8 psd (4 subimages) + subimage 0: 2 x 2, 3 channel, uint8 psd + SHA-1: 6585CA1F6B702CF27768484219F6F9200F4E9192 + subimage 1: 1 x 1, 3 channel, uint8 psd (4 subimages) + SHA-1: E3FF44389E80210E4545A907BE55980CEB6EA821 + subimage 2: 0 x 0, 3 channel, uint8 psd (4 subimages) + SHA-1: DA39A3EE5E6B4B0D3255BFEF95601890AFD80709 + subimage 3: 1 x 1, 3 channel, uint8 psd (4 subimages) + SHA-1: 5818BEB65A1338D7C80684EEC2EA2ECB0DB183D0 +hidden-empty-layer-1.psd : 2 x 2, 3 channel, uint8 psd (4 subimages) + subimage 0: 2 x 2, 3 channel, uint8 psd + SHA-1: 6585CA1F6B702CF27768484219F6F9200F4E9192 + subimage 1: 1 x 1, 3 channel, uint8 psd (4 subimages) + SHA-1: E3FF44389E80210E4545A907BE55980CEB6EA821 + subimage 2: 0 x 0, 3 channel, uint8 psd (4 subimages) + SHA-1: DA39A3EE5E6B4B0D3255BFEF95601890AFD80709 + subimage 3: 1 x 1, 3 channel, uint8 psd (4 subimages) + SHA-1: 5818BEB65A1338D7C80684EEC2EA2ECB0DB183D0 +hidden-populated-layer.psd : 2 x 2, 3 channel, uint8 psd (4 subimages) + subimage 0: 2 x 2, 3 channel, uint8 psd + SHA-1: 6585CA1F6B702CF27768484219F6F9200F4E9192 + subimage 1: 1 x 1, 3 channel, uint8 psd (4 subimages) + SHA-1: E3FF44389E80210E4545A907BE55980CEB6EA821 + subimage 2: 1 x 1, 3 channel, uint8 psd (4 subimages) + SHA-1: E3FF44389E80210E4545A907BE55980CEB6EA821 + subimage 3: 1 x 1, 3 channel, uint8 psd (4 subimages) + SHA-1: 5818BEB65A1338D7C80684EEC2EA2ECB0DB183D0 oiiotool ERROR: read : [Header] invalid image height 0 failed to open "empty-layer-invalid-canvas.psd": failed load_header Full command line was: diff --git a/testsuite/psd-empty-layers/run.py b/testsuite/psd-empty-layers/run.py index c59d6f915f..6781dee4c9 100644 --- a/testsuite/psd-empty-layers/run.py +++ b/testsuite/psd-empty-layers/run.py @@ -11,5 +11,9 @@ filename = f"empty-layer-{width}x{height}-{compression}.psd" command += info_command(filename, verbose=False, extraargs="-a") +for filename in ("hidden-empty-layer-0.psd", "hidden-empty-layer-1.psd", + "hidden-populated-layer.psd"): + command += info_command(filename, verbose=False, extraargs="-a") + command += info_command("empty-layer-invalid-canvas.psd", failureok=True) command += info_command("empty-layer-oversized.psd", failureok=True) diff --git a/testsuite/psd-empty-layers/src/make-empty-layer-psds.py b/testsuite/psd-empty-layers/src/make-empty-layer-psds.py index 2f399cd523..14e1a7d0f8 100644 --- a/testsuite/psd-empty-layers/src/make-empty-layer-psds.py +++ b/testsuite/psd-empty-layers/src/make-empty-layer-psds.py @@ -8,7 +8,7 @@ from pathlib import Path -def layer(width, height, compression, name, values=(32, 96, 160)): +def layer(width, height, compression, name, values=(32, 96, 160), hidden=False): encoded_name = name.encode("ascii") pascal_name = bytes([len(encoded_name)]) + encoded_name pascal_name += bytes(-len(pascal_name) % 4) @@ -25,16 +25,17 @@ def layer(width, height, compression, name, values=(32, 96, 160)): struct.pack(">hI", channel, len(data)) for channel, data in enumerate(channels) ) - record += b"8BIMnorm" + bytes([255, 0, 0, 0]) + record += b"8BIMnorm" + bytes([255, 0, 2 if hidden else 0, 0]) record += struct.pack(">I", len(extra)) + extra return record, b"".join(channels) -def write_psd(filename, width, height, compression, canvas_width=2): +def write_psd(filename, width, height, compression, canvas_width=2, + hidden=False): header = b"8BPS" + struct.pack(">H6sHIIHH", 1, bytes(6), 3, 2, canvas_width, 8, 3) records = [layer(1, 1, 0, "before"), - layer(width, height, compression, "empty"), + layer(width, height, compression, "empty", hidden=hidden), layer(1, 1, 0, "after", values=(160, 96, 32))] layer_info = struct.pack(">h", len(records)) layer_info += b"".join(record for record, data in records) @@ -55,6 +56,11 @@ def write_psd(filename, width, height, compression, canvas_width=2): write_psd(f"empty-layer-{width}x{height}-{compression}.psd", width, height, compression) +for compression in (0, 1): + write_psd(f"hidden-empty-layer-{compression}.psd", 0, 0, compression, + hidden=True) +write_psd("hidden-populated-layer.psd", 1, 1, 0, hidden=True) + # An empty layer must not bypass the existing document and layer size limits. write_psd("empty-layer-invalid-canvas.psd", 0, 0, 0, canvas_width=0) write_psd("empty-layer-oversized.psd", 0, 30001, 0) From c13d2e0cdeec3edb03168a4a2f2d1a23ce30e82f Mon Sep 17 00:00:00 2001 From: jeevan6996 Date: Sat, 17 Oct 2026 00:23:16 +0100 Subject: [PATCH 3/5] Remove PSD changelog entry per review Signed-off-by: jeevan6996 --- CHANGES.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index f0a8d9cc2a..a592ad6e50 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -23,7 +23,6 @@ Release 3.3 (anticipated Sep/Oct, 2027?) -- compared to 3.2.1.0 ### 🐛 Fixes and feature enhancements - *dicom*: A failed `seek_subimage` no longer leaves the reader pointing at a refused subimage, and frames past the first are now reachable. [#5474](https://github.com/AcademySoftwareFoundation/OpenImageIO/pull/5474) -- *psd*: Accept empty adjustment and pixel layer rectangles without rejecting the document, while retaining canvas and layer size limits. - *jpegxl*: Propagate encoder and write failures at `close()` to callers, reject incomplete pixel buffers before calling libjxl, and clear buffered scanlines when an output is reused. [#5509](https://github.com/AcademySoftwareFoundation/OpenImageIO/pull/5509) (by @zachlewis / Zach Lewis) - *png*: Preserve PNG Third Edition mastering display metadata (mDCV) as ten integer `mdcv_*` attributes on read (PNG and PNG-encoded ICO); written only when the record is complete, valid, and accompanied by cICP. [#5512](https://github.com/AcademySoftwareFoundation/OpenImageIO/pull/5512) (by @zachlewis / Zach Lewis) - *iconvert*: Use a private ImageCache to gather subimage specs, avoiding a shutdown-time crash from files left open in the shared cache. [#5508](https://github.com/AcademySoftwareFoundation/OpenImageIO/pull/5508) (by @zachlewis / Zach Lewis) From 51d3eedced7143eb89252ef5858951e1dc871a84 Mon Sep 17 00:00:00 2001 From: jeevan6996 Date: Sat, 17 Oct 2026 00:29:44 +0100 Subject: [PATCH 4/5] fix(psd): skip zero-pixel layer subimages Keep resolution validation strict and enforce layer size limits while tolerating empty rectangles. Expose only the composite and populated layers, deriving the public count from the resulting image specs. Bound empty channel payloads before seeking past RLE or ZIP data. Preserve independent layer-mask decoding and populated-layer ordering. Assisted-by: Codex / model identifier unavailable Signed-off-by: jeevan6996 --- src/psd.imageio/psdinput.cpp | 53 ++++++++++++++++++++---------------- 1 file changed, 30 insertions(+), 23 deletions(-) diff --git a/src/psd.imageio/psdinput.cpp b/src/psd.imageio/psdinput.cpp index 3b15d37973..82de843e70 100644 --- a/src/psd.imageio/psdinput.cpp +++ b/src/psd.imageio/psdinput.cpp @@ -147,6 +147,8 @@ class PSDInput final : public ImageInput { uint32_t width, height; uint16_t channel_count; + bool empty() const { return width == 0 || height == 0; } + std::vector channel_info; std::map channel_id_map; @@ -195,7 +197,7 @@ class PSDInput final : public ImageInput { std::string m_filename; //Current subimage int m_subimage; - //Subimage count (1 + layer count) + //Subimage count (composite plus nonempty layers) int m_subimage_count; std::vector m_specs; static const ResourceLoader resource_loaders[]; @@ -237,8 +239,7 @@ class PSDInput final : public ImageInput { bool read_header(); bool validate_header(); static bool validate_signature(const char signature[4]); - bool validate_resolution(string_view name, uint32_t width, uint32_t height, - bool allow_empty = false); + bool validate_resolution(string_view name, uint32_t width, uint32_t height); //Color Mode Data bool load_color_data(); @@ -652,8 +653,6 @@ PSDInput::open(const std::string& name, ImageSpec& newspec) return false; } - // Layer count + 1 for merged composite (Image Data Section) - m_subimage_count = m_layers.size() + 1; // Setup ImageSpecs and m_channels bool ok = setup(); @@ -1011,14 +1010,13 @@ PSDInput::validate_signature(const char signature[4]) bool -PSDInput::validate_resolution(string_view name, uint32_t width, uint32_t height, - bool allow_empty) +PSDInput::validate_resolution(string_view name, uint32_t width, uint32_t height) { - if ((!allow_empty && width < 1) || width > m_maxres) { + if (width < 1 || width > m_maxres) { errorfmt("[{}] invalid image width {}", name, width); return false; } - if ((!allow_empty && height < 1) || height > m_maxres) { + if (height < 1 || height > m_maxres) { errorfmt("[{}] invalid image height {}", name, height); return false; } @@ -1634,8 +1632,9 @@ PSDInput::load_layer(Layer& layer) int64_t h = int64_t(layer.bottom) - int64_t(layer.top); layer.width = uint32_t(w < 0 ? -w : w); layer.height = uint32_t(h < 0 ? -h : h); - // Adjustment layers and empty pixel layers may have an empty rectangle. - if (!validate_resolution("Layer Record", layer.width, layer.height, true)) + // Tolerate empty rectangles, but still enforce upper size limits. + if (!validate_resolution("Layer Record", std::max(layer.width, 1u), + std::max(layer.height, 1u))) return false; layer.channel_info.resize(layer.channel_count); for (uint16_t channel = 0; channel < layer.channel_count; channel++) { @@ -1759,6 +1758,22 @@ PSDInput::load_layer_channel(Layer& layer, ChannelInfo& channel_info) if (channel_info.data_length <= 2) return true; + // A channel's stored data, including the 2 byte compression tag already + // read, cannot extend past the end of the file. Compare unsigned: for PSB + // the length is 64 bits and a corrupt value would wrap negative if cast. + int64_t file_size = ioproxy() ? ioproxy()->size() : 0; + int64_t remaining = file_size - start_pos; + if (remaining < 0 || channel_info.data_length > uint64_t(remaining)) { + errorfmt("[Layer Channel] data length {} exceeds remaining file size", + channel_info.data_length); + return false; + } + + // Empty layers are not exposed as images, but their channels may still + // contain RLE row counts or ZIP data. Masks have independent dimensions. + if (layer.empty() && channel_info.channel_id != ChannelID_LayerMask) + return ioseek(start_pos + channel_info.data_length); + // Use mask_data size when channel_id is -2 uint32_t width, height; if (channel_info.channel_id == ChannelID_LayerMask) { @@ -1783,17 +1798,6 @@ PSDInput::load_layer_channel(Layer& layer, ChannelInfo& channel_info) channel_info.data_pos = iotell(); - // A channel's stored data, including the 2 byte compression tag already - // read, cannot extend past the end of the file. Compare unsigned: for PSB - // the length is 64 bits and a corrupt value would wrap negative if cast. - int64_t file_size = ioproxy() ? ioproxy()->size() : 0; - int64_t remaining = file_size - start_pos; - if (remaining < 0 || channel_info.data_length > uint64_t(remaining)) { - errorfmt("[Layer Channel] data length {} exceeds remaining file size", - channel_info.data_length); - return false; - } - channel_info.row_pos.resize(height); channel_info.row_length = (width * m_header.depth + 7) / 8; @@ -2161,13 +2165,15 @@ PSDInput::setup() spec.attribute("oiio:UnassociatedAlpha", 1); // Composite channels - m_channels.reserve(m_subimage_count); + m_channels.reserve(m_layers.size() + 1); m_channels.resize(1); m_channels[0].reserve(raw_channel_count); for (int i = 0; i < raw_channel_count; ++i) m_channels[0].push_back(&m_image_data.channel_info[i]); for (Layer& layer : m_layers) { + if (layer.empty()) + continue; spec_channel_count = m_WantRaw ? mode_channel_count[m_header.color_mode] : 3; raw_channel_count = mode_channel_count[m_header.color_mode]; @@ -2212,6 +2218,7 @@ PSDInput::setup() spec.attribute("oiio:subimagename", layer.name); } + m_subimage_count = int(m_specs.size()); return true; } From 6eb9ae856a594589113665e8b823ba6e48c47a3f Mon Sep 17 00:00:00 2001 From: jeevan6996 Date: Sat, 17 Oct 2026 00:29:44 +0100 Subject: [PATCH 5/5] test(psd): check in empty-layer regression fixtures Replace runtime generation with fixed PSD/PSB inputs and retain the generator for maintenance. Include RLE row-length tables so compressed empty channels exercise the real parsing path. Check subimage counts, layer order and pixel hashes in normal and raw modes. Cover hidden and trailing empty layers, masks, ZIP payloads, size limits and oversized declared channel lengths. Assisted-by: Codex / model identifier unavailable Signed-off-by: jeevan6996 --- testsuite/psd-empty-layers/ref/out.txt | 90 +----------------- testsuite/psd-empty-layers/run.py | 14 +-- testsuite/psd-empty-layers/src/all-empty.psd | Bin 0 -> 222 bytes .../src/check-empty-layers.py | 68 +++++++++++++ .../src/empty-layer-0x0-0.psd | Bin 0 -> 290 bytes .../src/empty-layer-0x0-1.psd | Bin 0 -> 290 bytes .../src/empty-layer-0x2-0.psd | Bin 0 -> 290 bytes .../src/empty-layer-0x2-1.psd | Bin 0 -> 302 bytes .../src/empty-layer-0x300000.psb | Bin 0 -> 334 bytes .../src/empty-layer-2x0-0.psd | Bin 0 -> 290 bytes .../src/empty-layer-2x0-1.psd | Bin 0 -> 290 bytes .../src/empty-layer-300000x0.psb | Bin 0 -> 334 bytes .../src/empty-layer-invalid-canvas.psd | Bin 0 -> 290 bytes .../src/empty-layer-oversized-width.psd | Bin 0 -> 290 bytes .../src/empty-layer-oversized.psd | Bin 0 -> 290 bytes .../src/empty-layer-truncated-channel.psd | Bin 0 -> 148 bytes .../src/empty-layer-with-mask.psd | Bin 0 -> 322 bytes .../psd-empty-layers/src/empty-layer-zip.psd | Bin 0 -> 314 bytes .../src/hidden-empty-layer-0.psd | Bin 0 -> 290 bytes .../src/hidden-empty-layer-1.psd | Bin 0 -> 290 bytes .../src/hidden-populated-layer.psd | Bin 0 -> 294 bytes .../src/make-empty-layer-psds.py | 57 ++++++++--- .../src/oversized-layer-0x300001.psb | Bin 0 -> 334 bytes .../src/oversized-layer-300001x0.psb | Bin 0 -> 334 bytes .../psd-empty-layers/src/trailing-empty.psd | Bin 0 -> 214 bytes 25 files changed, 114 insertions(+), 115 deletions(-) create mode 100644 testsuite/psd-empty-layers/src/all-empty.psd create mode 100644 testsuite/psd-empty-layers/src/check-empty-layers.py create mode 100644 testsuite/psd-empty-layers/src/empty-layer-0x0-0.psd create mode 100644 testsuite/psd-empty-layers/src/empty-layer-0x0-1.psd create mode 100644 testsuite/psd-empty-layers/src/empty-layer-0x2-0.psd create mode 100644 testsuite/psd-empty-layers/src/empty-layer-0x2-1.psd create mode 100644 testsuite/psd-empty-layers/src/empty-layer-0x300000.psb create mode 100644 testsuite/psd-empty-layers/src/empty-layer-2x0-0.psd create mode 100644 testsuite/psd-empty-layers/src/empty-layer-2x0-1.psd create mode 100644 testsuite/psd-empty-layers/src/empty-layer-300000x0.psb create mode 100644 testsuite/psd-empty-layers/src/empty-layer-invalid-canvas.psd create mode 100644 testsuite/psd-empty-layers/src/empty-layer-oversized-width.psd create mode 100644 testsuite/psd-empty-layers/src/empty-layer-oversized.psd create mode 100644 testsuite/psd-empty-layers/src/empty-layer-truncated-channel.psd create mode 100644 testsuite/psd-empty-layers/src/empty-layer-with-mask.psd create mode 100644 testsuite/psd-empty-layers/src/empty-layer-zip.psd create mode 100644 testsuite/psd-empty-layers/src/hidden-empty-layer-0.psd create mode 100644 testsuite/psd-empty-layers/src/hidden-empty-layer-1.psd create mode 100644 testsuite/psd-empty-layers/src/hidden-populated-layer.psd create mode 100644 testsuite/psd-empty-layers/src/oversized-layer-0x300001.psb create mode 100644 testsuite/psd-empty-layers/src/oversized-layer-300001x0.psb create mode 100644 testsuite/psd-empty-layers/src/trailing-empty.psd diff --git a/testsuite/psd-empty-layers/ref/out.txt b/testsuite/psd-empty-layers/ref/out.txt index 94e1febd91..66ea4da970 100644 --- a/testsuite/psd-empty-layers/ref/out.txt +++ b/testsuite/psd-empty-layers/ref/out.txt @@ -1,89 +1 @@ -empty-layer-0x0-0.psd : 2 x 2, 3 channel, uint8 psd (4 subimages) - subimage 0: 2 x 2, 3 channel, uint8 psd - SHA-1: 6585CA1F6B702CF27768484219F6F9200F4E9192 - subimage 1: 1 x 1, 3 channel, uint8 psd (4 subimages) - SHA-1: E3FF44389E80210E4545A907BE55980CEB6EA821 - subimage 2: 0 x 0, 3 channel, uint8 psd (4 subimages) - SHA-1: DA39A3EE5E6B4B0D3255BFEF95601890AFD80709 - subimage 3: 1 x 1, 3 channel, uint8 psd (4 subimages) - SHA-1: 5818BEB65A1338D7C80684EEC2EA2ECB0DB183D0 -empty-layer-0x2-0.psd : 2 x 2, 3 channel, uint8 psd (4 subimages) - subimage 0: 2 x 2, 3 channel, uint8 psd - SHA-1: 6585CA1F6B702CF27768484219F6F9200F4E9192 - subimage 1: 1 x 1, 3 channel, uint8 psd (4 subimages) - SHA-1: E3FF44389E80210E4545A907BE55980CEB6EA821 - subimage 2: 0 x 2, 3 channel, uint8 psd (4 subimages) - SHA-1: DA39A3EE5E6B4B0D3255BFEF95601890AFD80709 - subimage 3: 1 x 1, 3 channel, uint8 psd (4 subimages) - SHA-1: 5818BEB65A1338D7C80684EEC2EA2ECB0DB183D0 -empty-layer-2x0-0.psd : 2 x 2, 3 channel, uint8 psd (4 subimages) - subimage 0: 2 x 2, 3 channel, uint8 psd - SHA-1: 6585CA1F6B702CF27768484219F6F9200F4E9192 - subimage 1: 1 x 1, 3 channel, uint8 psd (4 subimages) - SHA-1: E3FF44389E80210E4545A907BE55980CEB6EA821 - subimage 2: 2 x 0, 3 channel, uint8 psd (4 subimages) - SHA-1: DA39A3EE5E6B4B0D3255BFEF95601890AFD80709 - subimage 3: 1 x 1, 3 channel, uint8 psd (4 subimages) - SHA-1: 5818BEB65A1338D7C80684EEC2EA2ECB0DB183D0 -empty-layer-0x0-1.psd : 2 x 2, 3 channel, uint8 psd (4 subimages) - subimage 0: 2 x 2, 3 channel, uint8 psd - SHA-1: 6585CA1F6B702CF27768484219F6F9200F4E9192 - subimage 1: 1 x 1, 3 channel, uint8 psd (4 subimages) - SHA-1: E3FF44389E80210E4545A907BE55980CEB6EA821 - subimage 2: 0 x 0, 3 channel, uint8 psd (4 subimages) - SHA-1: DA39A3EE5E6B4B0D3255BFEF95601890AFD80709 - subimage 3: 1 x 1, 3 channel, uint8 psd (4 subimages) - SHA-1: 5818BEB65A1338D7C80684EEC2EA2ECB0DB183D0 -empty-layer-0x2-1.psd : 2 x 2, 3 channel, uint8 psd (4 subimages) - subimage 0: 2 x 2, 3 channel, uint8 psd - SHA-1: 6585CA1F6B702CF27768484219F6F9200F4E9192 - subimage 1: 1 x 1, 3 channel, uint8 psd (4 subimages) - SHA-1: E3FF44389E80210E4545A907BE55980CEB6EA821 - subimage 2: 0 x 2, 3 channel, uint8 psd (4 subimages) - SHA-1: DA39A3EE5E6B4B0D3255BFEF95601890AFD80709 - subimage 3: 1 x 1, 3 channel, uint8 psd (4 subimages) - SHA-1: 5818BEB65A1338D7C80684EEC2EA2ECB0DB183D0 -empty-layer-2x0-1.psd : 2 x 2, 3 channel, uint8 psd (4 subimages) - subimage 0: 2 x 2, 3 channel, uint8 psd - SHA-1: 6585CA1F6B702CF27768484219F6F9200F4E9192 - subimage 1: 1 x 1, 3 channel, uint8 psd (4 subimages) - SHA-1: E3FF44389E80210E4545A907BE55980CEB6EA821 - subimage 2: 2 x 0, 3 channel, uint8 psd (4 subimages) - SHA-1: DA39A3EE5E6B4B0D3255BFEF95601890AFD80709 - subimage 3: 1 x 1, 3 channel, uint8 psd (4 subimages) - SHA-1: 5818BEB65A1338D7C80684EEC2EA2ECB0DB183D0 -hidden-empty-layer-0.psd : 2 x 2, 3 channel, uint8 psd (4 subimages) - subimage 0: 2 x 2, 3 channel, uint8 psd - SHA-1: 6585CA1F6B702CF27768484219F6F9200F4E9192 - subimage 1: 1 x 1, 3 channel, uint8 psd (4 subimages) - SHA-1: E3FF44389E80210E4545A907BE55980CEB6EA821 - subimage 2: 0 x 0, 3 channel, uint8 psd (4 subimages) - SHA-1: DA39A3EE5E6B4B0D3255BFEF95601890AFD80709 - subimage 3: 1 x 1, 3 channel, uint8 psd (4 subimages) - SHA-1: 5818BEB65A1338D7C80684EEC2EA2ECB0DB183D0 -hidden-empty-layer-1.psd : 2 x 2, 3 channel, uint8 psd (4 subimages) - subimage 0: 2 x 2, 3 channel, uint8 psd - SHA-1: 6585CA1F6B702CF27768484219F6F9200F4E9192 - subimage 1: 1 x 1, 3 channel, uint8 psd (4 subimages) - SHA-1: E3FF44389E80210E4545A907BE55980CEB6EA821 - subimage 2: 0 x 0, 3 channel, uint8 psd (4 subimages) - SHA-1: DA39A3EE5E6B4B0D3255BFEF95601890AFD80709 - subimage 3: 1 x 1, 3 channel, uint8 psd (4 subimages) - SHA-1: 5818BEB65A1338D7C80684EEC2EA2ECB0DB183D0 -hidden-populated-layer.psd : 2 x 2, 3 channel, uint8 psd (4 subimages) - subimage 0: 2 x 2, 3 channel, uint8 psd - SHA-1: 6585CA1F6B702CF27768484219F6F9200F4E9192 - subimage 1: 1 x 1, 3 channel, uint8 psd (4 subimages) - SHA-1: E3FF44389E80210E4545A907BE55980CEB6EA821 - subimage 2: 1 x 1, 3 channel, uint8 psd (4 subimages) - SHA-1: E3FF44389E80210E4545A907BE55980CEB6EA821 - subimage 3: 1 x 1, 3 channel, uint8 psd (4 subimages) - SHA-1: 5818BEB65A1338D7C80684EEC2EA2ECB0DB183D0 -oiiotool ERROR: read : [Header] invalid image height 0 -failed to open "empty-layer-invalid-canvas.psd": failed load_header -Full command line was: -> oiiotool --info -v -a --hash empty-layer-invalid-canvas.psd -oiiotool ERROR: read : [Layer Record] invalid image height 30001 -failed to open "empty-layer-oversized.psd": failed load_layers -Full command line was: -> oiiotool --info -v -a --hash empty-layer-oversized.psd +Empty-layer subimages, populated pixels, raw mode and size limits verified. diff --git a/testsuite/psd-empty-layers/run.py b/testsuite/psd-empty-layers/run.py index 6781dee4c9..88a4be4801 100644 --- a/testsuite/psd-empty-layers/run.py +++ b/testsuite/psd-empty-layers/run.py @@ -5,15 +5,5 @@ # https://github.com/AcademySoftwareFoundation/OpenImageIO redirect = ' >> out.txt 2>&1 ' -command += run_app(f'"{pythonbin}" src/make-empty-layer-psds.py') -for compression in (0, 1): - for width, height in ((0, 0), (0, 2), (2, 0)): - filename = f"empty-layer-{width}x{height}-{compression}.psd" - command += info_command(filename, verbose=False, extraargs="-a") - -for filename in ("hidden-empty-layer-0.psd", "hidden-empty-layer-1.psd", - "hidden-populated-layer.psd"): - command += info_command(filename, verbose=False, extraargs="-a") - -command += info_command("empty-layer-invalid-canvas.psd", failureok=True) -command += info_command("empty-layer-oversized.psd", failureok=True) +command += run_app(f'"{pythonbin}" src/check-empty-layers.py ' + f'"{oiio_app("oiiotool").strip()}" src') diff --git a/testsuite/psd-empty-layers/src/all-empty.psd b/testsuite/psd-empty-layers/src/all-empty.psd new file mode 100644 index 0000000000000000000000000000000000000000..d3fcbe312c3748816476b25d416747c472264e86 GIT binary patch literal 222 zcmcC;3J7LkWPku>AjJg691I{f46Fh&7J+48A`lv)h6${n38K@&$" + result.stdout + "") + actual = [ + (int(spec.findtext("width")), int(spec.findtext("height")), + spec.findtext("attrib[@name='oiio:subimagename']", ""), + spec.findtext("SHA1")) + for spec in specs + ] + assert actual == expected, (filename, raw, actual, expected) + assert all(int(spec.findtext("subimages", "1")) == len(expected) + for spec in specs), (filename, raw, "incorrect subimage count") + +for filename, diagnostic in ( + ("empty-layer-invalid-canvas.psd", "[Header]"), + ("empty-layer-oversized.psd", "[Layer Record]"), + ("empty-layer-oversized-width.psd", "[Layer Record]"), + ("oversized-layer-0x300001.psb", "[Layer Record]"), + ("oversized-layer-300001x0.psb", "[Layer Record]"), + ("empty-layer-truncated-channel.psd", "[Layer Channel]"), +): + result = subprocess.run( + [tool, "--info", str(root / filename)], + stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, + ) + assert result.returncode != 0 and diagnostic in result.stderr, ( + filename, result.returncode, result.stderr) + +print("Empty-layer subimages, populated pixels, raw mode and size limits verified.") diff --git a/testsuite/psd-empty-layers/src/empty-layer-0x0-0.psd b/testsuite/psd-empty-layers/src/empty-layer-0x0-0.psd new file mode 100644 index 0000000000000000000000000000000000000000..8df0442ee3861cfcdd23a69f84072ade6f7a4fea GIT binary patch literal 290 zcmcC;3J7LkWPku>AjJg691I{f47>v}o}q|exy#2zNF8B7q{ahbuInp;p(iO>pUk?aoE#I%ysB9Ivh3=9bj3=4oP TFjxTQDnK|2K#%|g3xEIs=tUe$ literal 0 HcmV?d00001 diff --git a/testsuite/psd-empty-layers/src/empty-layer-0x0-1.psd b/testsuite/psd-empty-layers/src/empty-layer-0x0-1.psd new file mode 100644 index 0000000000000000000000000000000000000000..e8cd0100c5f5fb5149c226f50579b03ae3d020bb GIT binary patch literal 290 zcmcC;3J7LkWPku>AjJg691I{f47>v}o}q|exy#2zNF8B7q{ahbuInp;p(iO>pUk?aoE#I%ysB9Ivh3=9bj3=4op VfFO_o;sOOhKmiC6fM5X-008Kp98LfL literal 0 HcmV?d00001 diff --git a/testsuite/psd-empty-layers/src/empty-layer-0x2-0.psd b/testsuite/psd-empty-layers/src/empty-layer-0x2-0.psd new file mode 100644 index 0000000000000000000000000000000000000000..8ca25d134ece44a377dcd7653e286c2c57b89d19 GIT binary patch literal 290 zcmcC;3J7LkWPku>AjJg691I{f47>v}o}q|gYN$R-A;8BAa^m>{;}GJ`cWx1gjFtP4hv><-q%w35^!kah(Ih6DzN W1wa-UEC6#AARGlCNC1KbKmY*glN?O| literal 0 HcmV?d00001 diff --git a/testsuite/psd-empty-layers/src/empty-layer-0x2-1.psd b/testsuite/psd-empty-layers/src/empty-layer-0x2-1.psd new file mode 100644 index 0000000000000000000000000000000000000000..6ca4826f3420afcc82ee974dc7eb070d8ceed2c9 GIT binary patch literal 302 zcmcC;3J7LkWPku>AjJg691I{f4EzEzKB0(!bTC3in8Es)!8(~OoIHK=@{4l+L-Y#3 zj9^PjP0KGzg>gYN$R-A;8EjxP*uY|JxXfTp%`GUY1nYtkB)fw(F|8!E2&7$sfgyo` dVFB1;kY_*uLIMRrQVKu;AW#5;1Rz)d1OOUy9CQEx literal 0 HcmV?d00001 diff --git a/testsuite/psd-empty-layers/src/empty-layer-0x300000.psb b/testsuite/psd-empty-layers/src/empty-layer-0x300000.psb new file mode 100644 index 0000000000000000000000000000000000000000..cf1f15ecc2dbbe1ddf9c3fe94ef55bfd36f0cd30 GIT binary patch literal 334 zcmcC;3J7LkVt@co+|*hqj;48mdLLXlwviZUXJFavoIz>F}}!pYM&FTW`F zKSV|Vs)B(nDK#y>C>6$KU|^a20L($y%>*}(32q(}F7sGZa|=o;5yk;o6uFNzF|8!E f2xOiD149A>!vY`+3>JX73J{J05F`M>0w4eYc4-_D literal 0 HcmV?d00001 diff --git a/testsuite/psd-empty-layers/src/empty-layer-2x0-0.psd b/testsuite/psd-empty-layers/src/empty-layer-2x0-0.psd new file mode 100644 index 0000000000000000000000000000000000000000..bd244768439f8b61f7655bdc3ac9d7e25dceada5 GIT binary patch literal 290 zcmcC;3J7LkWPku>AjJg691I{f47>v}o}q|hjt6VwbQuo+Ac+i{t}nwncsQVG)vrb%`OYhqeSY7t1U0s}(=1H%F! U3k(*3xe5@D0uUqs!2%!v0O^YyO#lD@ literal 0 HcmV?d00001 diff --git a/testsuite/psd-empty-layers/src/empty-layer-2x0-1.psd b/testsuite/psd-empty-layers/src/empty-layer-2x0-1.psd new file mode 100644 index 0000000000000000000000000000000000000000..4f9bcaeb7a0d729bdd83201f72c05a9cfd8503f8 GIT binary patch literal 290 zcmcC;3J7LkWPku>AjJg691I{f47>v}o}q|hjt6VwbQuo+Ac+i{t}nwncsQVG)vrb%`OYhqeSY7t1U0s}(=1H%HK V#UKb|fVeo+|*hqj;48mdLLXlwviZUXJFavoIz>F}}!pYM&FTW`F zKSV|Vs)B(nDK#y>C>6$q&@7W5AeqMmH;)N!9uqF}SW|NgN-9x|qsV=%iD@ONMIiGO b7#I>57#09oV6XtpRe*35fFJ<~761VNaE%-h literal 0 HcmV?d00001 diff --git a/testsuite/psd-empty-layers/src/empty-layer-invalid-canvas.psd b/testsuite/psd-empty-layers/src/empty-layer-invalid-canvas.psd new file mode 100644 index 0000000000000000000000000000000000000000..4d80a4189ede796414e2cc32f8f19e66d60cd99c GIT binary patch literal 290 zcmcC;3J7LkWPku>AjJe`axj3{Fz^n@c!nYZ(!mH7VFv4G2J2+DaPsuc%P-3P578?C zGlDHCH7&m=6~;x-5PO)wW-x)xV8UevYie#mNhLxnkVUdPSQFDqQj0)lC@?T2Ffc3t UvcO;gn5zKcC;&kM5G()!0Ol(kNdN!< literal 0 HcmV?d00001 diff --git a/testsuite/psd-empty-layers/src/empty-layer-oversized-width.psd b/testsuite/psd-empty-layers/src/empty-layer-oversized-width.psd new file mode 100644 index 0000000000000000000000000000000000000000..992efef8786839ab8a043396adfaa0017916c84a GIT binary patch literal 290 zcmcC;3J7LkWPku>AjJg691I{f47>v}o}q|j*DsUg%1Ca@Vy5ZiH?!J3*|P*RCv1j+7TO-w6EEdrULz`&5ez_0+w T0)qu$t^$Ok00ap@umA`EQhOZn literal 0 HcmV?d00001 diff --git a/testsuite/psd-empty-layers/src/empty-layer-oversized.psd b/testsuite/psd-empty-layers/src/empty-layer-oversized.psd new file mode 100644 index 0000000000000000000000000000000000000000..0d072ee983b5f2cf19740083c80a35987f142dfd GIT binary patch literal 290 zcmcC;3J7LkWPku>AjJg691I{f47>v}o}q|iv&sUesHv4;t41{1_~TxPJQ<`$GxA{ha41BvcnO-w6EEdrULz`&5e Xz_0+w0)qu$t^$Ok00ap@umA`ERTCWX literal 0 HcmV?d00001 diff --git a/testsuite/psd-empty-layers/src/empty-layer-truncated-channel.psd b/testsuite/psd-empty-layers/src/empty-layer-truncated-channel.psd new file mode 100644 index 0000000000000000000000000000000000000000..64aad57d12a166c1c4301bc4cabd1b85db9c3037 GIT binary patch literal 148 zcmcC;3J7LkWPku>AjJg691I{f3?u*<5pY!?QIIMIuo`|RH=qoNWCD>EPM*Gb`9-<^ h!3r1zAQS@wYie#mNhOd6u?v|5D^UP~1Rz)d1OVnP4oLt2 literal 0 HcmV?d00001 diff --git a/testsuite/psd-empty-layers/src/empty-layer-with-mask.psd b/testsuite/psd-empty-layers/src/empty-layer-with-mask.psd new file mode 100644 index 0000000000000000000000000000000000000000..5e154ce65470e413cc22b2cd480c9ed7541fc2b7 GIT binary patch literal 322 zcmcC;3J7LkWPku>AjJg691I{f6fp8JFfg(qi-2`7LPeOtJZ7*?W(y}z-@N>y-2V{0 z0x&b!l2X(1i&9}+1kD0wGJ(xtg4q85A5e@9%_J3|m!!G9nCa}&T)ATb3XNC1KbKmY(YJ0Z9L literal 0 HcmV?d00001 diff --git a/testsuite/psd-empty-layers/src/empty-layer-zip.psd b/testsuite/psd-empty-layers/src/empty-layer-zip.psd new file mode 100644 index 0000000000000000000000000000000000000000..6b292df7ddb2d663d0e9f1557cd956085b7c3678 GIT binary patch literal 314 zcmcC;3J7LkWPku>AjJg691I{f6fm*@8UK()z&aSABFtbOGgv3Hg_EamUVc&Te~4ZI zm>FzIscHE|sW2{x2HC^_HG>Om1{YY23zr$JsksFum0(>k0_1X%+`*cdR+3r-(yqY3 hkifvOfPtxE4%n}ZSXn>?AZ0+sAfNyQ2|%y_2ms889^?Q3 literal 0 HcmV?d00001 diff --git a/testsuite/psd-empty-layers/src/hidden-empty-layer-0.psd b/testsuite/psd-empty-layers/src/hidden-empty-layer-0.psd new file mode 100644 index 0000000000000000000000000000000000000000..2542b5eaf5ed3ec2a9510f87ad91e90a7613768e GIT binary patch literal 290 zcmcC;3J7LkWPku>AjJg691I{f47>v}o}q|exy#2zNF8B7q{5oUlK!vHsfH8r=Oq!OVO$RgPttchtQsYM_&6c`v1 Y7#J1+SzxdL%vFGJ6o4QB2o?YV0O?H}O#lD@ literal 0 HcmV?d00001 diff --git a/testsuite/psd-empty-layers/src/hidden-empty-layer-1.psd b/testsuite/psd-empty-layers/src/hidden-empty-layer-1.psd new file mode 100644 index 0000000000000000000000000000000000000000..554b2efb1334cf864c37bea2e1037a0006a78879 GIT binary patch literal 290 zcmcC;3J7LkWPku>AjJg691I{f47>v}o}q|exy#2zNF8B7q{5oUlK!vHsfH8r=Oq!OVO$RgPttchtQsYM_&6c`v1 a7#J1+jQ~L)1H=Uif`9@LBmltzAOHaAryNiK literal 0 HcmV?d00001 diff --git a/testsuite/psd-empty-layers/src/hidden-populated-layer.psd b/testsuite/psd-empty-layers/src/hidden-populated-layer.psd new file mode 100644 index 0000000000000000000000000000000000000000..8d21d10acda2360d8ef51779a79782f765939ac5 GIT binary patch literal 294 zcmcC;3J7LkWPku>AjJg691I{f415AIUZIG9bTC3in8Es)!8(~OoIHK=@{4l+L-Y#3 zj9^PjP0KGzg>gYN5oUlK!vHsfH8r=Oq!J_q2Sk_wR*NtrF|8!E2qdP!z>vVeumFhx TWCKMM7$86a2oiu`0T2KHa>X5b literal 0 HcmV?d00001 diff --git a/testsuite/psd-empty-layers/src/make-empty-layer-psds.py b/testsuite/psd-empty-layers/src/make-empty-layer-psds.py index 14e1a7d0f8..374562b1c9 100644 --- a/testsuite/psd-empty-layers/src/make-empty-layer-psds.py +++ b/testsuite/psd-empty-layers/src/make-empty-layer-psds.py @@ -5,49 +5,63 @@ # https://github.com/AcademySoftwareFoundation/OpenImageIO import struct +import zlib from pathlib import Path -def layer(width, height, compression, name, values=(32, 96, 160), hidden=False): +def layer(width, height, compression, name, values=(32, 96, 160), hidden=False, + mask=False, version=1, data_length_extra=0): encoded_name = name.encode("ascii") pascal_name = bytes([len(encoded_name)]) + encoded_name pascal_name += bytes(-len(pascal_name) % 4) - extra = struct.pack(">II", 0, 0) + pascal_name + mask_data = struct.pack(">iiiiBB2x", 0, 0, 2, 2, 255, 0) if mask else b"" + extra = struct.pack(">I", len(mask_data)) + mask_data + extra += struct.pack(">I", 0) + pascal_name channels = [] for value in values: if not width or not height: data = struct.pack(">H", compression) + if compression == 1: + data += bytes(height * (2 if version == 1 else 4)) + elif compression == 2: + data += zlib.compress(b"") else: data = struct.pack(">H", 0) + bytes([value]) * width * height - channels.append(data) - record = struct.pack(">iiiiH", 0, 0, height, width, 3) + channels.append((len(channels), data)) + if mask: + channels.append((-2, struct.pack(">H", 0) + bytes([255]) * 4)) + record = struct.pack(">iiiiH", 0, 0, height, width, len(channels)) record += b"".join( - struct.pack(">hI", channel, len(data)) - for channel, data in enumerate(channels) + struct.pack(">hI" if version == 1 else ">hQ", channel, + len(data) + data_length_extra) + for channel, data in channels ) record += b"8BIMnorm" + bytes([255, 0, 2 if hidden else 0, 0]) record += struct.pack(">I", len(extra)) + extra - return record, b"".join(channels) + return record, b"".join(data for channel, data in channels) def write_psd(filename, width, height, compression, canvas_width=2, - hidden=False): - header = b"8BPS" + struct.pack(">H6sHIIHH", 1, bytes(6), 3, + hidden=False, records=None, version=1): + header = b"8BPS" + struct.pack(">H6sHIIHH", version, bytes(6), 3, 2, canvas_width, 8, 3) - records = [layer(1, 1, 0, "before"), - layer(width, height, compression, "empty", hidden=hidden), - layer(1, 1, 0, "after", values=(160, 96, 32))] + if records is None: + records = [layer(1, 1, 0, "before", version=version), + layer(width, height, compression, "empty", hidden=hidden, + version=version), + layer(1, 1, 0, "after", values=(160, 96, 32), version=version)] layer_info = struct.pack(">h", len(records)) layer_info += b"".join(record for record, data in records) layer_info += b"".join(data for record, data in records) layer_info += bytes(len(layer_info) % 2) - layer_mask = struct.pack(">I", len(layer_info)) + layer_info + length_format = ">I" if version == 1 else ">Q" + layer_mask = struct.pack(length_format, len(layer_info)) + layer_info layer_mask += struct.pack(">I", 0) composite = struct.pack(">H", 0) composite += b"".join(bytes([v]) * 4 for v in (32, 96, 160)) Path(filename).write_bytes( header + struct.pack(">II", 0, 0) - + struct.pack(">I", len(layer_mask)) + layer_mask + composite + + struct.pack(length_format, len(layer_mask)) + layer_mask + composite ) @@ -64,3 +78,18 @@ def write_psd(filename, width, height, compression, canvas_width=2, # An empty layer must not bypass the existing document and layer size limits. write_psd("empty-layer-invalid-canvas.psd", 0, 0, 0, canvas_width=0) write_psd("empty-layer-oversized.psd", 0, 30001, 0) +write_psd("empty-layer-oversized-width.psd", 30001, 0, 0) +write_psd("all-empty.psd", 0, 0, 0, + records=[layer(0, 0, 0, "empty"), layer(0, 2, 1, "empty2")]) +write_psd("trailing-empty.psd", 0, 0, 0, + records=[layer(1, 1, 0, "before"), layer(0, 0, 0, "empty")]) +write_psd("empty-layer-with-mask.psd", 0, 0, 0, + records=[layer(1, 1, 0, "before"), layer(0, 0, 0, "empty", mask=True), + layer(1, 1, 0, "after", values=(160, 96, 32))]) +write_psd("empty-layer-zip.psd", 0, 2, 2) +write_psd("empty-layer-truncated-channel.psd", 0, 2, 1, + records=[layer(0, 2, 1, "empty", data_length_extra=1000000)]) +for width, height in ((0, 300000), (300000, 0)): + write_psd(f"empty-layer-{width}x{height}.psb", width, height, 0, version=2) +for width, height in ((0, 300001), (300001, 0)): + write_psd(f"oversized-layer-{width}x{height}.psb", width, height, 0, version=2) diff --git a/testsuite/psd-empty-layers/src/oversized-layer-0x300001.psb b/testsuite/psd-empty-layers/src/oversized-layer-0x300001.psb new file mode 100644 index 0000000000000000000000000000000000000000..9365475e518e3eb6be072ce613bece48faebe9c1 GIT binary patch literal 334 zcmcC;3J7LkVt@co+|*hqj;48mdLLXlwviZUXJFavoIz>F}}!pYM&FTW`F zKSV|Vs)B(nDK#y>C>6$KU|^a25X?c?%>*}(32q(}F7sGZa|=o;5yk;o6uFNzF|8!E f2xOiD149A>!vY`+3>JX73J{J05F`M>0w4eYcPJbZ literal 0 HcmV?d00001 diff --git a/testsuite/psd-empty-layers/src/oversized-layer-300001x0.psb b/testsuite/psd-empty-layers/src/oversized-layer-300001x0.psb new file mode 100644 index 0000000000000000000000000000000000000000..e5278fa7c93c9540a89f82b075d93a1f89187870 GIT binary patch literal 334 zcmcC;3J7LkVt@co+|*hqj;48mdLLXlwviZUXJFavoIz>F}}!pYM&FTW`F zKSV|Vs)B(nDK#y>C>6$q&@7W5BALeoH;)N!9uqF}SW|NgN-9x|qsV=%iD@ONMIiGO b7#I>57#09oV6XtpRe*35fFJ<~761VNaY!5z literal 0 HcmV?d00001 diff --git a/testsuite/psd-empty-layers/src/trailing-empty.psd b/testsuite/psd-empty-layers/src/trailing-empty.psd new file mode 100644 index 0000000000000000000000000000000000000000..c9d903d1c066f76379933b6906c80b4fafc8b74a GIT binary patch literal 214 zcmcC;3J7LkWPku>AjJg691I{f3@idNW`Sj3A|M@%P&Le8{mfu_W(y}z-@N>y-2V`j z0x%71NvUc1MX4|@f`-_`1U7>SVmmG~SW|NgN-9BG6&M&27#J1+SqPv21PMT}00;n1 C%M_0Q literal 0 HcmV?d00001