From 2286eb1b074ace425a2df249f6fca5a9ef2728e3 Mon Sep 17 00:00:00 2001 From: Larry Gritz Date: Thu, 22 Oct 2026 22:57:15 -0700 Subject: [PATCH] fix(jpeg2000): writer dropped tiled output and overran on last scanline Tiles are emulated by buffering the whole image and writing it out as scanlines on close(). But close() took a null m_stream to mean "already closed", and m_stream is only created once the last scanline is written (never, on the OpenJPH path), so the buffered image was silently dropped, leaving an empty or truncated file. Test for the image objects instead. Tests added for both the OpenJPEG and OpenJPH writers. Also, write_scanline() let y == height through, so copy_scanline() wrote a row past the end of the OpenJPEG component buffers, and negative y was not checked at all. And copy_scanline() subtracted m_spec.y a second time; that is latent only because check_open() zeroes the origin for this format. Assisted-by: Claude Code / Claude Opus 5.5 Signed-off-by: Larry Gritz --- src/jpeg2000.imageio/jpeg2000output.cpp | 16 +++++++++++----- testsuite/htj2k/ref/out.txt | 2 ++ testsuite/htj2k/run.py | 5 +++++ testsuite/jpeg2000/ref/out-icc.txt | 2 ++ testsuite/jpeg2000/ref/out.txt | 2 ++ testsuite/jpeg2000/run.py | 4 ++++ 6 files changed, 26 insertions(+), 5 deletions(-) diff --git a/src/jpeg2000.imageio/jpeg2000output.cpp b/src/jpeg2000.imageio/jpeg2000output.cpp index cadf9d6a23..5388610511 100644 --- a/src/jpeg2000.imageio/jpeg2000output.cpp +++ b/src/jpeg2000.imageio/jpeg2000output.cpp @@ -290,8 +290,9 @@ Jpeg2000Output::write_scanline(int y, int z, TypeDesc format, const void* data, stride_t xstride) { y -= m_spec.y; - if (y > m_spec.height) { - errorfmt("Attempt to write too many scanlines to {}", m_filename); + if (y < 0 || y >= m_spec.height) { + errorfmt("Attempt to write scanline {} outside the image to {}", + y + m_spec.y, m_filename); return false; } @@ -350,9 +351,14 @@ Jpeg2000Output::write_tile(int x, int y, int z, TypeDesc format, bool Jpeg2000Output::close() { - if (!m_stream) { // Already closed + // m_stream only exists after save_image(), which tiled output has not + // yet reached, so test for the images instead. + bool is_open = m_image != nullptr; +#ifdef USE_OPENJPH + is_open |= m_jph_image != nullptr; +#endif + if (!is_open) // Already closed return true; - } bool ok = true; if (m_spec.tile_width) { @@ -541,7 +547,7 @@ Jpeg2000Output::copy_scanline(int y, int /*z*/, const void* data) { int bits = sizeof(T) * 8; const T* scanline = static_cast(data); - const size_t scanline_pos = (y - m_spec.y) * m_spec.width; + const size_t scanline_pos = size_t(y) * m_spec.width; for (int i = 0, j = 0; i < m_spec.width; i++) { for (int c = 0; c < m_spec.nchannels; ++c) { unsigned int val = scanline[j++]; diff --git a/testsuite/htj2k/ref/out.txt b/testsuite/htj2k/ref/out.txt index 9c74f90d73..4868b3df0d 100644 --- a/testsuite/htj2k/ref/out.txt +++ b/testsuite/htj2k/ref/out.txt @@ -2,6 +2,8 @@ Comparing "../oiio-images/tahoe-gps.jpg" and "test.j2c" PASS Comparing "../oiio-images/dpx/dpx_nuke_10bits_rgb.dpx" and "testdpx.j2c" PASS +Comparing "../oiio-images/tahoe-gps.jpg" and "testtiled.j2c" +PASS Comparing "../oiio-images/tahoe-gps.jpg" and "testcompress.j2c" PASS oiiotool ERROR: read : "src/bomb-resolution.j2c": jpeg2000 image dimension 2000000000x2000000000 exceeds "limits:resolution" = 1048576 for a single dimension. Possible corrupt input? diff --git a/testsuite/htj2k/run.py b/testsuite/htj2k/run.py index 742c24f6aa..7ac22626a8 100644 --- a/testsuite/htj2k/run.py +++ b/testsuite/htj2k/run.py @@ -21,6 +21,11 @@ command += diff_command(OIIO_TESTSUITE_IMAGEDIR+"/dpx/dpx_nuke_10bits_rgb.dpx", "testdpx.j2c") +# Tiled output is emulated by buffering, and must still be written on close. +command += oiiotool(OIIO_TESTSUITE_IMAGEDIR+"/tahoe-gps.jpg" + " --tile 64 64 -o testtiled.j2c") +command += diff_command(OIIO_TESTSUITE_IMAGEDIR+"/tahoe-gps.jpg", "testtiled.j2c") + command += oiiotool(OIIO_TESTSUITE_IMAGEDIR+"/tahoe-gps.jpg" " --attrib qstep 0.03 -o testcompress.j2c") diff --git a/testsuite/jpeg2000/ref/out-icc.txt b/testsuite/jpeg2000/ref/out-icc.txt index a65442b281..7ee9724358 100644 --- a/testsuite/jpeg2000/ref/out-icc.txt +++ b/testsuite/jpeg2000/ref/out-icc.txt @@ -6,6 +6,8 @@ idiff ERROR: Could not read ../oiio-images/jpeg2000/broken/issue_3427.jp2: Failed to decode the codestream in the JP2 file Invalid image file "../oiio-images/jpeg2000/broken/issue_3427.jp2": Tile part length size inconsistent with stream length Failed to decode the codestream in the JP2 file +Comparing "../common/tahoe-tiny.tif" and "tiled.jp2" +PASS ICCProfile of tahoe-icc.jp2 ICCProfile: 0, 0, 2, 48, 65, 68, 66, 69, 2, 16, 0, 0, 109, 110, 116, 114, ... [560 x uint8] ICCProfile:attributes: "Reflective, Glossy, Positive, Color" diff --git a/testsuite/jpeg2000/ref/out.txt b/testsuite/jpeg2000/ref/out.txt index 49df15bdff..f1d7f6c53a 100644 --- a/testsuite/jpeg2000/ref/out.txt +++ b/testsuite/jpeg2000/ref/out.txt @@ -6,3 +6,5 @@ idiff ERROR: Could not read ../oiio-images/jpeg2000/broken/issue_3427.jp2: Failed to decode the codestream in the JP2 file Invalid image file "../oiio-images/jpeg2000/broken/issue_3427.jp2": Tile part length size inconsistent with stream length Failed to decode the codestream in the JP2 file +Comparing "../common/tahoe-tiny.tif" and "tiled.jp2" +PASS diff --git a/testsuite/jpeg2000/run.py b/testsuite/jpeg2000/run.py index d91767fb8b..fc6b6d59ad 100755 --- a/testsuite/jpeg2000/run.py +++ b/testsuite/jpeg2000/run.py @@ -16,6 +16,10 @@ for f in files: command += rw_command (imagedir, f, printinfo=False) +# Tiled output is emulated by buffering, and must still be written on close. +command += oiiotool("../common/tahoe-tiny.tif --tile 16 16 -o tiled.jp2") +command += diff_command("../common/tahoe-tiny.tif", "tiled.jp2") + # Try to parse library_list to find the OpenJPEG version. If found, return it as # a single number, e.g. 2.5.3 -> 20503. Return -1 if not found. def get_openjp_ver():