Skip to content

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

Description

@phpanzini

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugCrash or wrong behavior of an existing feature.file formatsImage file formats, ImageInput, ImageOutput

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions