Metadata-Version: 2.4
Name: justpfm
Version: 1.1.0
Summary: Read and write PFM files
Home-page: https://github.com/MartinPeris/justPFM
Author: Martin Peris Martorell
Author-email: cosmoperis@gmail.com
Project-URL: Bug Reports, https://github.com/MartinPeris/justPFM/issues
Project-URL: Source, https://github.com/MartinPeris/justPFM
Keywords: pfm,portable,float,map
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Science/Research
Classifier: Topic :: Scientific/Engineering :: Image Processing
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.7
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Programming Language :: Python :: 3 :: Only
Requires-Python: >=3.7, <4
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: numpy>=1.21
Provides-Extra: dev
Requires-Dist: check-manifest; extra == "dev"
Provides-Extra: test
Requires-Dist: pytest; extra == "test"
Requires-Dist: pytest-cov; extra == "test"
Requires-Dist: hypothesis==6.79.4; python_version < "3.10" and extra == "test"
Requires-Dist: hypothesis==6.168.0; python_version >= "3.10" and extra == "test"
Dynamic: author
Dynamic: author-email
Dynamic: classifier
Dynamic: description
Dynamic: description-content-type
Dynamic: home-page
Dynamic: keywords
Dynamic: license-file
Dynamic: project-url
Dynamic: provides-extra
Dynamic: requires-dist
Dynamic: requires-python
Dynamic: summary

# justPFM

A small NumPy-based Python module to read and write Portable Float Map images.

## Install

Supported Python versions: **3.7–3.14**. See [SUPPORT.md](SUPPORT.md) for the
compatibility policy and interpreter-specific test tooling.

```bash
pip install justpfm
```

## Usage

```python
from pathlib import Path

import numpy as np
from justpfm import read_pfm, write_pfm

# Grayscale: the first array axis is height, and rows are top-first.
path = Path("grayscale.pfm")
pixels = np.arange(15, dtype=np.float32).reshape(3, 5)
write_pfm(path, pixels)
loaded = read_pfm(path)
assert loaded.shape == (3, 5, 1)
np.testing.assert_array_equal(loaded[..., 0], pixels)

# RGB uses the last axis for red, green, and blue.
rgb = np.zeros((3, 5, 3), dtype=np.float32)
rgb[..., 0] = 1
write_pfm("rgb.pfm", rgb, scale=0.5)
np.testing.assert_array_equal(read_pfm("rgb.pfm"), rgb * 0.5)
```

The existing `from justpfm import justpfm` import remains supported.

## API contract

`write_pfm(file_name, data, scale=1) -> None`

- `file_name` accepts a string or a filesystem path object such as `pathlib.Path`.
- `data` must be a NumPy float32 array with positive height and width, shaped
  `(H, W)`, `(H, W, 1)`, or `(H, W, 3)`. Both little- and big-endian float32
  dtypes are supported, including noncontiguous arrays. Pixel NaNs and infinities
  are allowed.
- `scale` must be positive and finite. The writer stores the original pixel
  samples and records the scale in the header. The header sign describes the
  array's byte order; callers supply only the magnitude.
- Rows are written bottom-first as required by PFM.

`read_pfm(file_name, *, max_pixels=None) -> numpy.ndarray`

- Returns float32 pixels shaped `(H, W, C)`, where `C` is 1 for grayscale or 3
  for RGB. The returned dtype retains the file's byte order.
- Applies the header scale magnitude to pixel values, except magnitudes within
  `math.isclose(scale, 1.0)` (relative tolerance `1e-9`). Thus nonunit write scales
  generally change values when read back; scaling uses float32 arithmetic and
  may overflow or underflow for extreme values.
- Returns top-first rows using a view with negative row strides. For native
  byte order and contiguous storage, use
  `np.ascontiguousarray(loaded, dtype=np.float32)`. Use `loaded[..., 0]` to remove
  the grayscale channel axis.
- Requires positive dimensions, a finite nonzero header scale, and exactly the
  expected raster bytes. It checks the file size before allocating pixel data
  and limits reads to the declared number of samples.
- Each of the three header lines must end in a newline and occupy at most
  **4096 bytes including the newline**. Overlong and truncated headers raise
  `ValueError`; header reads use bounded buffers.
- Set the keyword-only `max_pixels` argument to a positive built-in integer to
  reject images whose `H * W` exceeds that limit before allocating their raster.
  The default `None` imposes no pixel-count limit. Booleans, other types, zero,
  and negative limits raise `ValueError` before the file is opened. The limit
  counts pixels rather than channel samples, so RGB and grayscale images of
  equal dimensions have the same pixel count.

Invalid shapes, dtypes, dimensions, scales, headers, and payload lengths raise
`ValueError`. Filesystem failures such as missing files or denied access raise
`OSError` subclasses. Both functions operate on filesystem files, not in-memory
file-like objects. Sufficient memory is still required. For untrusted files,
choose a pixel limit appropriate for the application, for example:

```python
loaded = read_pfm("upload.pfm", max_pixels=4_000_000)
```

The raster needs 4 bytes per grayscale pixel or 12 bytes per RGB pixel. This
limit bounds that allocation, not total process memory, application copies, or
other overhead. With `max_pixels=None`, large valid images remain supported.

### Interchange with Netpbm

Use `scale=1` to exchange normalized pixel intensities with Netpbm. Its
`pfmtopam` reader divides samples by the header magnitude, while justPFM
multiplies by it. Nonunit scales therefore have different intensity semantics.
The tests use files generated by the actual Netpbm implementation and verify
both directions; [fixture provenance and examples](tests/fixtures/netpbm/README.md)
document the difference.

### Saving files

Writes use a temporary file in the destination directory, then atomically replace
its directory entry after writing and closing. A failed write preserves an
existing destination and cleans up the temporary file. The destination directory
must permit creating temporary files and replacing entries.

Existing regular file permission bits are preserved. New files have private
permissions (`0600` on POSIX). A destination symlink is replaced with a regular
file; its target is left unchanged. Other metadata, including ownership, is not
copied. Atomic replacement does not guarantee durability after a power failure.

Writes use bulk serialization with reusable pixel scratch space bounded by the
larger of 8 MiB and one image row. Data already contiguous in file order needs
no pixel scratch buffer. See the [before/after measurements](BENCHMARKS.md#bulk-writer-comparison)
for the speed and memory tradeoff.

## Development

See [CONTRIBUTING.md](CONTRIBUTING.md) for setup and commit-hook installation.
Run `pre-commit run --all-files` to execute the same lint, formatting, tests,
100% statement/branch coverage, and packaging gates used by CI.

Property tests vary dimensions, pixel values, array layout, byte order, scale,
and malformed payload sizes. They also check the reader against independently
encoded binary fixtures. Local and CI runs use the same deterministic Hypothesis
profile: 60 examples per property, no example database, and no timing deadline.
Health checks remain enabled. Hypothesis uses `6.79.4` on Python 3.7–3.9 and
`6.168.0` on Python 3.10–3.14, with matching pins in the test extra and tox.
Each interpreter runs the same properties; deterministic examples are repeatable
within its pinned toolchain, not guaranteed identical across Hypothesis versions.
