Task 537: .PFM File Format
Task 537: .PFM File Format
File Format Specifications for .PFM
The .PFM file format refers to the Portable FloatMap (PFM), an unofficial extension of the Netpbm family of image formats designed for high dynamic range (HDR) imaging. It stores pixel values as 32-bit floating-point numbers, allowing for values beyond the typical 0-255 range used in standard images. This format is commonly used in computer graphics, image processing, and scientific applications where precision in light intensity is required.
The format consists of an ASCII text header followed by binary raster data:
- Header Structure: Three "lines" of ASCII text separated by whitespace (typically newlines, but any whitespace is allowed).
- Line 1: Identifier (magic number) – "PF" for RGB color (3 channels) or "Pf" for grayscale (1 channel).
- Line 2: Image dimensions – Width and height as ASCII decimal integers separated by whitespace (e.g., "800 600").
- Line 3: Scale factor / Endianness – A decimal floating-point number. The absolute value is the scale factor (often 1.0, which means no scaling). If positive, the binary data is big-endian; if negative, little-endian.
- Raster Data: Immediately follows the header (after the whitespace after the scale factor). Each pixel sample is a 4-byte IEEE 754 single-precision floating-point number. Pixels are stored row-major order, from left to right, and rows from bottom to top (note: some variants, like Adobe's, use top to bottom, which may result in flipped images when interchanging). For color images, each pixel has three floats (R, G, B); for grayscale, one float per pixel.
- Additional Notes: No maxval like in other Netpbm formats; values can be arbitrary floats. The format is simple and lacks compression or additional metadata. There may be variants (e.g., Adobe's row order), but the Netpbm-compatible version is described here.
List of Properties Intrinsic to the .PFM File Format:
- Identifier (Magic Number): A string indicating the type ("PF" for color RGB, "Pf" for grayscale).
- Width: Integer representing the image width in pixels.
- Height: Integer representing the image height in pixels.
- Scale Factor: Floating-point number (absolute value) that scales the pixel values (often 1.0 for no scaling).
- Endianness: String indicating byte order ("big" for big-endian if scale is positive, "little" for little-endian if negative).
- Channels: Integer derived from identifier (3 for "PF", 1 for "Pf").
Two Direct Download Links for .PFM Files:
- https://filesamples.com/samples/image/pfm/sample_640×426.pfm
- https://filesamples.com/samples/image/pfm/sample_1280×853.pfm
Ghost Blog Embedded HTML/JavaScript for Drag-and-Drop .PFM File Dump:
This is a self-contained HTML page with embedded JavaScript that can be embedded in a Ghost blog post (or any HTML context). It allows users to drag and drop a .PFM file, parses the header, and displays the properties on screen. It uses the File API and ArrayBuffer for parsing.
Python Class for .PFM Handling:
import struct
import os
class PFMFile:
def __init__(self):
self.identifier = None
self.width = None
self.height = None
self.scale = None
self.endianness = None
self.channels = None
self.data = None # List of floats for pixel data
def read(self, filepath):
with open(filepath, 'rb') as f:
# Read identifier
self.identifier = f.readline().strip().decode('ascii')
# Read dimensions
dims = f.readline().strip().decode('ascii').split()
self.width = int(dims[0])
self.height = int(dims[1])
# Read scale/endian
scale_str = f.readline().strip().decode('ascii')
scale_val = float(scale_str)
self.endianness = 'big' if scale_val > 0 else 'little'
self.scale = abs(scale_val)
self.channels = 3 if self.identifier == 'PF' else 1
# Read raster data
endian_char = '>' if self.endianness == 'big' else '<'
sample_count = self.width * self.height * self.channels
raw_data = f.read(sample_count * 4)
self.data = struct.unpack(f'{endian_char}{sample_count}f', raw_data)
def write(self, filepath, data=None):
if data is None:
data = self.data # Use existing if available
if not data or len(data) != self.width * self.height * self.channels:
raise ValueError("Data mismatch for width, height, channels")
with open(filepath, 'wb') as f:
# Write header
f.write(f"{self.identifier}\n".encode('ascii'))
f.write(f"{self.width} {self.height}\n".encode('ascii'))
sign = 1 if self.endianness == 'big' else -1
f.write(f"{sign * self.scale}\n".encode('ascii'))
# Write data
endian_char = '>' if self.endianness == 'big' else '<'
raw_data = struct.pack(f'{endian_char}{len(data)}f', *data)
f.write(raw_data)
def print_properties(self):
print(f"Identifier: {self.identifier}")
print(f"Width: {self.width}")
print(f"Height: {self.height}")
print(f"Scale Factor: {self.scale}")
print(f"Endianness: {self.endianness}")
print(f"Channels: {self.channels}")
# Example usage:
# pfm = PFMFile()
# pfm.read('example.pfm')
# pfm.print_properties()
# pfm.write('output.pfm') # Writes with existing data
Java Class for .PFM Handling:
import java.io.*;
import java.nio.*;
import java.util.*;
public class PFMFile {
private String identifier;
private int width;
private int height;
private double scale;
private String endianness;
private int channels;
private float[] data; // Array of floats for pixel data
public void read(String filepath) throws IOException {
try (BufferedReader reader = new BufferedReader(new FileReader(filepath));
RandomAccessFile raf = new RandomAccessFile(filepath, "r")) {
// Read header lines
identifier = reader.readLine().trim();
String[] dims = reader.readLine().trim().split("\\s+");
width = Integer.parseInt(dims[0]);
height = Integer.parseInt(dims[1]);
String scaleStr = reader.readLine().trim();
double scaleVal = Double.parseDouble(scaleStr);
endianness = (scaleVal > 0) ? "big" : "little";
scale = Math.abs(scaleVal);
channels = "PF".equals(identifier) ? 3 : 1;
// Position to start of raster (after header)
long headerLength = (identifier + "\n" + width + " " + height + "\n" + scaleStr + "\n").getBytes().length;
raf.seek(headerLength);
// Read data
data = new float[width * height * channels];
ByteBuffer buffer = ByteBuffer.allocate(data.length * 4);
raf.getChannel().read(buffer);
buffer.flip();
buffer.order(endianness.equals("big") ? ByteOrder.BIG_ENDIAN : ByteOrder.LITTLE_ENDIAN);
FloatBuffer floatBuffer = buffer.asFloatBuffer();
floatBuffer.get(data);
}
}
public void write(String filepath, float[] newData) throws IOException {
if (newData == null) newData = data;
if (newData.length != width * height * channels) {
throw new IllegalArgumentException("Data mismatch");
}
try (FileOutputStream fos = new FileOutputStream(filepath)) {
// Write header
fos.write((identifier + "\n").getBytes());
fos.write((width + " " + height + "\n").getBytes());
double signedScale = (endianness.equals("big") ? 1 : -1) * scale;
fos.write((signedScale + "\n").getBytes());
// Write data
ByteBuffer buffer = ByteBuffer.allocate(newData.length * 4);
buffer.order(endianness.equals("big") ? ByteOrder.BIG_ENDIAN : ByteOrder.LITTLE_ENDIAN);
FloatBuffer floatBuffer = buffer.asFloatBuffer();
floatBuffer.put(newData);
fos.write(buffer.array());
}
}
public void printProperties() {
System.out.println("Identifier: " + identifier);
System.out.println("Width: " + width);
System.out.println("Height: " + height);
System.out.println("Scale Factor: " + scale);
System.out.println("Endianness: " + endianness);
System.out.println("Channels: " + channels);
}
// Example usage:
// public static void main(String[] args) throws IOException {
// PFMFile pfm = new PFMFile();
// pfm.read("example.pfm");
// pfm.printProperties();
// pfm.write("output.pfm", null); // Writes with existing data
// }
}
JavaScript Class for .PFM Handling (Node.js compatible, using fs module):
const fs = require('fs');
class PFMFile {
constructor() {
this.identifier = null;
this.width = null;
this.height = null;
this.scale = null;
this.endianness = null;
this.channels = null;
this.data = null; // Buffer or Float32Array for pixel data
}
read(filepath) {
const buffer = fs.readFileSync(filepath);
let offset = 0;
let header = '';
// Helper to read until whitespace
const readToken = () => {
header = '';
while (offset < buffer.length && ![10, 13, 32, 9].includes(buffer[offset])) {
header += String.fromCharCode(buffer[offset]);
offset++;
}
while (offset < buffer.length && [10, 13, 32, 9].includes(buffer[offset])) offset++;
return header.trim();
};
this.identifier = readToken();
this.width = parseInt(readToken());
this.height = parseInt(readToken());
let scaleVal = parseFloat(readToken());
this.endianness = scaleVal > 0 ? 'big' : 'little';
this.scale = Math.abs(scaleVal);
this.channels = this.identifier === 'PF' ? 3 : 1;
// Read data
const sampleCount = this.width * this.height * this.channels;
const dataBuffer = buffer.slice(offset, offset + sampleCount * 4);
this.data = new Float32Array(dataBuffer.buffer, dataBuffer.byteOffset, sampleCount);
if (this.endianness !== (new DataView(new ArrayBuffer(4)).getFloat32(0, false) > 0 ? 'big' : 'little')) {
// Swap bytes if host endian differs (simplified, actual swap needed for cross-endian)
console.warn('Endian swap not implemented; assuming host matches file.');
}
}
write(filepath, data = null) {
if (!data) data = this.data;
if (data.length !== this.width * this.height * this.channels) {
throw new Error('Data mismatch');
}
const header = `${this.identifier}\n${this.width} ${this.height}\n${(this.endianness === 'big' ? '' : '-') + this.scale}\n`;
const headerBuffer = Buffer.from(header);
const dataBuffer = Buffer.alloc(data.length * 4);
const view = new DataView(dataBuffer.buffer);
const littleEndian = this.endianness === 'little';
for (let i = 0; i < data.length; i++) {
view.setFloat32(i * 4, data[i], littleEndian);
}
fs.writeFileSync(filepath, Buffer.concat([headerBuffer, dataBuffer]));
}
printProperties() {
console.log(`Identifier: ${this.identifier}`);
console.log(`Width: ${this.width}`);
console.log(`Height: ${this.height}`);
console.log(`Scale Factor: ${this.scale}`);
console.log(`Endianness: ${this.endianness}`);
console.log(`Channels: ${this.channels}`);
}
}
// Example usage:
// const pfm = new PFMFile();
// pfm.read('example.pfm');
// pfm.printProperties();
// pfm.write('output.pfm'); // Writes with existing data
C++ Class for .PFM Handling:
#include <iostream>
#include <fstream>
#include <vector>
#include <string>
#include <cmath>
#include <cstring>
class PFMFile {
private:
std::string identifier;
int width;
int height;
double scale;
std::string endianness;
int channels;
std::vector<float> data; // Vector of floats for pixel data
public:
void read(const std::string& filepath) {
std::ifstream file(filepath, std::ios::binary);
if (!file) {
std::cerr << "Failed to open file" << std::endl;
return;
}
std::getline(file, identifier);
identifier.erase(std::remove(identifier.begin(), identifier.end(), '\r'), identifier.end()); // Trim CR if any
std::string dimLine;
std::getline(file, dimLine);
std::sscanf(dimLine.c_str(), "%d %d", &width, &height);
std::string scaleLine;
std::getline(file, scaleLine);
double scaleVal;
std::sscanf(scaleLine.c_str(), "%lf", &scaleVal);
endianness = (scaleVal > 0) ? "big" : "little";
scale = std::abs(scaleVal);
channels = (identifier == "PF") ? 3 : 1;
// Read data
int sampleCount = width * height * channels;
data.resize(sampleCount);
file.read(reinterpret_cast<char*>(data.data()), sampleCount * sizeof(float));
// Handle endian swap if necessary (assuming host is little-endian for simplicity)
if (endianness == "big" && (reinterpret_cast<const uint8_t*>(&scaleVal)[0] != 0)) { // Crude host check
for (float& val : data) {
uint32_t* bytes = reinterpret_cast<uint32_t*>(&val);
*bytes = __builtin_bswap32(*bytes);
}
}
}
void write(const std::string& filepath, const std::vector<float>& newData = {}) {
auto writeData = newData.empty() ? data : newData;
if (writeData.size() != static_cast<size_t>(width * height * channels)) {
std::cerr << "Data mismatch" << std::endl;
return;
}
std::ofstream file(filepath, std::ios::binary);
if (!file) {
std::cerr << "Failed to open file" << std::endl;
return;
}
file << identifier << "\n";
file << width << " " << height << "\n";
double signedScale = (endianness == "big" ? 1 : -1) * scale;
file << signedScale << "\n";
// Write data (assuming no swap needed; adjust if cross-endian)
file.write(reinterpret_cast<const char*>(writeData.data()), writeData.size() * sizeof(float));
}
void printProperties() const {
std::cout << "Identifier: " << identifier << std::endl;
std::cout << "Width: " << width << std::endl;
std::cout << "Height: " << height << std::endl;
std::cout << "Scale Factor: " << scale << std::endl;
std::cout << "Endianness: " << endianness << std::endl;
std::cout << "Channels: " << channels << std::endl;
}
};
// Example usage:
// int main() {
// PFMFile pfm;
// pfm.read("example.pfm");
// pfm.printProperties();
// pfm.write("output.pfm");
// return 0;
// }