Describe the bug
A float PFM that is taller than one ImageOutput::write_image() chunk is written with its rows scrambled. Each chunk is flipped on its own, and the chunks are then written top to bottom, so the file is neither bottom-row-first nor top-row-first.
PNMOutput::write_scanlines() applies the pnm:pfmflip flip (on by default, for TypeDesc::FLOAT data) only within the [ybegin, yend) range of that one call — src/pnm.imageio/pnmoutput.cpp lines 375-402 on main at 68dda81. write_image() calls write_scanlines() once per chunk. For a 3840-wide, 3-channel float image the file breaks at row 1472, which matches write_image()'s chunking (2^26 bytes per chunk, rounded up to 64 rows). A 2160-row UHD frame is therefore written as two separately flipped blocks.
Expected: the file stores the bottom row first, the PFM convention and what the same command writes for a narrower image. For a ramp that is 0 at the top and 1 at the bottom, the stored rows should fall steadily from 1 to 0.
OpenImageIO version and dependencies
OIIO 3.1.14.1 | MacOS/ARM
Build compiler: Apple clang 21.0 | C++17/201703
HW features enabled at build: neon
No CUDA support (disabled / unavailable at build time)
Homebrew, macOS 26.5.1, arm64. Not re-measured on 3.1.17.0, but the flip code on main is the same.
To Reproduce
oiiotool --pattern fill:top=0,0,0:bottom=1,1,1 3840x2160 3 -d float -o tall.pfm
oiiotool --pattern fill:top=0,0,0:bottom=1,1,1 64x2160 3 -d float -o narrow.pfm
Read the stored rows directly (a PFM is a three-line header, then raw float32 rows):
import struct
def stored_rows(path):
f = open(path, "rb"); f.readline(); w, h = map(int, f.readline().split())
e = "<" if float(f.readline()) < 0 else ">"
return [struct.unpack(e + "%df" % (w * 3), f.read(w * 12))[0] for _ in range(h)]
for p in ("narrow.pfm", "tall.pfm"):
r = stored_rows(p)
print("%-10s row 0 = %.4f row 1471 = %.4f row 1472 = %.4f row 2159 = %.4f" % (p, r[0], r[1471], r[1472], r[-1]))
Output:
narrow.pfm row 0 = 1.0000 row 1471 = 0.3187 row 1472 = 0.3182 row 2159 = 0.0000
tall.pfm row 0 = 0.6813 row 1471 = 0.0000 row 1472 = 1.0000 row 2159 = 0.6818
narrow.pfm is correct. In tall.pfm, stored rows 0-1471 hold the top 1472 image rows in reverse order, and rows 1472-2159 hold the remaining 688 rows, also reversed.
Evidence
- Writing with
--attrib pnm:pfmflip 0 gives a consistent top-row-first file at the same size (stored row 0 = 0.0000, 1471 = 0.6813, 1472 = 0.6818, 2159 = 1.0000). So the chunking alone is fine, and the per-call flip is the cause.
- Reading
tall.pfm back cannot recover the image, because the reader flips the whole image while the writer flipped each chunk.
A fix probably needs the whole image height in view, for example writing each call's rows to file position height - 1 - y, or buffering the image. The flip cannot be correct when it is done inside one chunk.
Describe the bug
A float PFM that is taller than one
ImageOutput::write_image()chunk is written with its rows scrambled. Each chunk is flipped on its own, and the chunks are then written top to bottom, so the file is neither bottom-row-first nor top-row-first.PNMOutput::write_scanlines()applies thepnm:pfmflipflip (on by default, forTypeDesc::FLOATdata) only within the[ybegin, yend)range of that one call —src/pnm.imageio/pnmoutput.cpplines 375-402 onmainat 68dda81.write_image()callswrite_scanlines()once per chunk. For a 3840-wide, 3-channel float image the file breaks at row 1472, which matcheswrite_image()'s chunking (2^26 bytes per chunk, rounded up to 64 rows). A 2160-row UHD frame is therefore written as two separately flipped blocks.Expected: the file stores the bottom row first, the PFM convention and what the same command writes for a narrower image. For a ramp that is 0 at the top and 1 at the bottom, the stored rows should fall steadily from 1 to 0.
OpenImageIO version and dependencies
Homebrew, macOS 26.5.1, arm64. Not re-measured on 3.1.17.0, but the flip code on
mainis the same.To Reproduce
Read the stored rows directly (a PFM is a three-line header, then raw float32 rows):
Output:
narrow.pfmis correct. Intall.pfm, stored rows 0-1471 hold the top 1472 image rows in reverse order, and rows 1472-2159 hold the remaining 688 rows, also reversed.Evidence
--attrib pnm:pfmflip 0gives a consistent top-row-first file at the same size (stored row 0 = 0.0000, 1471 = 0.6813, 1472 = 0.6818, 2159 = 1.0000). So the chunking alone is fine, and the per-call flip is the cause.tall.pfmback cannot recover the image, because the reader flips the whole image while the writer flipped each chunk.A fix probably needs the whole image height in view, for example writing each call's rows to file position
height - 1 - y, or buffering the image. The flip cannot be correct when it is done inside one chunk.