AcademySoftwareFoundation / AcademySoftwareFoundation/OpenImageIO

bug: PFM writer flips rows per write_scanlines() chunk, scrambling float PFMs taller than one chunk

Open
#5,471 0 comments 0 reactions 0 assignees View on GitHub
bug file formats
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.