AcademySoftwareFoundation / AcademySoftwareFoundation/OpenImageIO
bug: PFM writer flips rows per write_scanlines() chunk, scrambling float PFMs taller than one chunk
- Dominant language
- C++
- Stars
- 2.4k
- Forks
- 698
- Avg merge
- 3d 4h
- Merged PRs (30d)
- 47
Description
**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 68dda81c1d. `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):
```python
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.
Contributor guide
Research direction
Start with src/pnm.imageio/pnmoutput.cpp lines 375-402 and reproduce the issue with the two oiiotool commands. Check write_image() chunking and write_scanlines() behavior; done when a 3840x2160 float PFM stores rows bottom-first like narrow.pfm, without reversal at the chunk boundary.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- cpp
- Domain
- computer-graphics
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 65/100