Task 510: .PAM File Format
Task 510: .PAM File Format
1. List of Properties Intrinsic to the .PAM File Format
The .PAM (Portable Arbitrary Map) file format is a flexible image format from the Netpbm suite. It consists of a header (ASCII text lines) followed by a binary raster. The intrinsic properties (header fields) define the structure and metadata of the file. These are:
- WIDTH: An integer specifying the width of the image in columns (pixels per row). Must be at least 1.
- HEIGHT: An integer specifying the height of the image in rows. Must be at least 1.
- DEPTH: An integer specifying the number of channels/planes per pixel (tuple degree). Must be at least 1 (e.g., 1 for grayscale, 3 for RGB, 4 for RGBA).
- MAXVAL: An integer specifying the maximum value for each sample (pixel component). Must be at least 1 and at most 65535. Samples are unsigned integers scaled from 0 to this value.
- TUPLTYPE: A string (possibly multi-line, concatenated with spaces) describing the meaning of the tuples (e.g., "RGB", "GRAYSCALE", "RGB_ALPHA"). Optional; defaults to an empty string if absent. Defines semantics but not encoding.
The header starts with the magic number "P7\n" and ends with "ENDHDR\n". Comments (lines starting with "#") and empty lines are ignored. The raster follows immediately after the header, with samples stored in big-endian binary (1-2 bytes per sample based on MAXVAL), row-major order, no padding.
2. Two Direct Download Links for .PAM Files
- https://filesamples.com/samples/image/pam/sample_640×426.pam
- https://filesamples.com/samples/image/pam/sample_1280×853.pam
3. Ghost Blog Embedded HTML/JavaScript for Drag-and-Drop .PAM File Dump
Here's an embeddable HTML snippet with JavaScript for a Ghost blog (or any HTML page). It creates a drop zone where users can drag and drop a .PAM file. The script reads the file as an ArrayBuffer, parses the header to extract properties, and displays them on the screen. It does not handle the raster data, only the properties.
4. Python Class for .PAM File Handling
import struct
import sys
class PamFile:
def __init__(self, filename=None):
self.properties = {'WIDTH': None, 'HEIGHT': None, 'DEPTH': None, 'MAXVAL': None, 'TUPLTYPE': ''}
self.raster = None # Binary data
if filename:
self.read(filename)
def read(self, filename):
with open(filename, 'rb') as f:
data = f.read()
# Parse header (ASCII)
header_end = data.find(b'ENDHDR\n')
if header_end == -1:
raise ValueError("Invalid .PAM: No ENDHDR")
header = data[:header_end + 7].decode('ascii').split('\n')
if header[0] != 'P7':
raise ValueError("Invalid .PAM: Missing P7 magic")
for line in header[1:]:
line = line.strip()
if not line or line.startswith('#'):
continue
tokens = line.split()
key = tokens[0]
if key == 'ENDHDR':
break
elif key == 'TUPLTYPE':
self.properties['TUPLTYPE'] += (' ' if self.properties['TUPLTYPE'] else '') + ' '.join(tokens[1:])
elif key in ['WIDTH', 'HEIGHT', 'DEPTH', 'MAXVAL']:
self.properties[key] = int(tokens[1])
# Validate required properties
if any(v is None for v in [self.properties[k] for k in ['WIDTH', 'HEIGHT', 'DEPTH', 'MAXVAL']]):
raise ValueError("Missing required .PAM properties")
# Raster starts after header
self.raster = data[header_end + 7:]
# Sample size: 1 byte if MAXVAL <= 255, else 2 bytes
sample_size = 1 if self.properties['MAXVAL'] <= 255 else 2
expected_raster_size = self.properties['WIDTH'] * self.properties['HEIGHT'] * self.properties['DEPTH'] * sample_size
if len(self.raster) != expected_raster_size:
raise ValueError("Invalid raster size")
def print_properties(self):
for key, value in self.properties.items():
print(f"{key}: {value}")
def write(self, filename, raster=None):
if any(v is None for v in [self.properties[k] for k in ['WIDTH', 'HEIGHT', 'DEPTH', 'MAXVAL']]):
raise ValueError("Missing properties for writing")
header = 'P7\n'
for key in ['WIDTH', 'HEIGHT', 'DEPTH', 'MAXVAL']:
header += f"{key} {self.properties[key]}\n"
if self.properties['TUPLTYPE']:
# Split if multi-word, but keep as one line for simplicity
header += f"TUPLTYPE {self.properties['TUPLTYPE']}\n"
header += 'ENDHDR\n'
with open(filename, 'wb') as f:
f.write(header.encode('ascii'))
f.write(raster if raster is not None else (self.raster or b''))
# Example usage:
# pam = PamFile('sample.pam')
# pam.print_properties()
# pam.write('output.pam')
5. Java Class for .PAM File Handling
import java.io.*;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.HashMap;
import java.util.Map;
public class PamFile {
private Map<String, Object> properties = new HashMap<>();
private byte[] raster;
public PamFile(String filename) throws IOException {
properties.put("WIDTH", null);
properties.put("HEIGHT", null);
properties.put("DEPTH", null);
properties.put("MAXVAL", null);
properties.put("TUPLTYPE", "");
if (filename != null) {
read(filename);
}
}
public void read(String filename) throws IOException {
byte[] data;
try (FileInputStream fis = new FileInputStream(filename)) {
data = fis.readAllBytes();
}
// Find header end
int headerEnd = -1;
for (int i = 0; i < data.length - 6; i++) {
if (data[i] == 'E' && data[i+1] == 'N' && data[i+2] == 'D' && data[i+3] == 'H' &&
data[i+4] == 'D' && data[i+5] == 'R' && data[i+6] == '\n') {
headerEnd = i + 7;
break;
}
}
if (headerEnd == -1) {
throw new IOException("Invalid .PAM: No ENDHDR");
}
String header = new String(data, 0, headerEnd, "ASCII");
String[] lines = header.split("\n");
if (!lines[0].equals("P7")) {
throw new IOException("Invalid .PAM: Missing P7 magic");
}
for (int i = 1; i < lines.length; i++) {
String line = lines[i].trim();
if (line.isEmpty() || line.startsWith("#")) {
continue;
}
String[] tokens = line.split("\\s+");
String key = tokens[0];
if (key.equals("ENDHDR")) {
break;
} else if (key.equals("TUPLTYPE")) {
String tupltype = (String) properties.get("TUPLTYPE");
tupltype += (tupltype.isEmpty() ? "" : " ") + String.join(" ", tokens).substring(8).trim();
properties.put("TUPLTYPE", tupltype);
} else if (key.equals("WIDTH") || key.equals("HEIGHT") || key.equals("DEPTH") || key.equals("MAXVAL")) {
properties.put(key, Integer.parseInt(tokens[1]));
}
}
// Validate
if (properties.get("WIDTH") == null || properties.get("HEIGHT") == null ||
properties.get("DEPTH") == null || properties.get("MAXVAL") == null) {
throw new IOException("Missing required .PAM properties");
}
// Raster
raster = new byte[data.length - headerEnd];
System.arraycopy(data, headerEnd, raster, 0, raster.length);
int sampleSize = ((Integer)properties.get("MAXVAL") <= 255) ? 1 : 2;
int expectedSize = (Integer)properties.get("WIDTH") * (Integer)properties.get("HEIGHT") *
(Integer)properties.get("DEPTH") * sampleSize;
if (raster.length != expectedSize) {
throw new IOException("Invalid raster size");
}
}
public void printProperties() {
for (Map.Entry<String, Object> entry : properties.entrySet()) {
System.out.println(entry.getKey() + ": " + entry.getValue());
}
}
public void write(String filename, byte[] newRaster) throws IOException {
if (properties.get("WIDTH") == null || properties.get("HEIGHT") == null ||
properties.get("DEPTH") == null || properties.get("MAXVAL") == null) {
throw new IOException("Missing properties for writing");
}
StringBuilder header = new StringBuilder("P7\n");
header.append("WIDTH ").append(properties.get("WIDTH")).append("\n");
header.append("HEIGHT ").append(properties.get("HEIGHT")).append("\n");
header.append("DEPTH ").append(properties.get("DEPTH")).append("\n");
header.append("MAXVAL ").append(properties.get("MAXVAL")).append("\n");
String tupltype = (String) properties.get("TUPLTYPE");
if (!tupltype.isEmpty()) {
header.append("TUPLTYPE ").append(tupltype).append("\n");
}
header.append("ENDHDR\n");
try (FileOutputStream fos = new FileOutputStream(filename)) {
fos.write(header.toString().getBytes("ASCII"));
fos.write(newRaster != null ? newRaster : (raster != null ? raster : new byte[0]));
}
}
// Example usage:
// public static void main(String[] args) throws IOException {
// PamFile pam = new PamFile("sample.pam");
// pam.printProperties();
// pam.write("output.pam", null);
// }
}
6. JavaScript Class for .PAM File Handling
This is a browser-compatible JavaScript class (using FileReader for reading). For writing, it uses Blob and URL.createObjectURL to trigger a download. Assumes console for printing.
class PamFile {
constructor(file = null) {
this.properties = { WIDTH: null, HEIGHT: null, DEPTH: null, MAXVAL: null, TUPLTYPE: '' };
this.raster = null; // ArrayBuffer
if (file) {
this.read(file);
}
}
read(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = (event) => {
const buffer = event.target.result;
const textDecoder = new TextDecoder('ascii');
let headerStr = textDecoder.decode(buffer.slice(0, 1024)); // Assume header <1KB
const headerEnd = headerStr.indexOf('ENDHDR\n');
if (headerEnd === -1) {
reject(new Error('Invalid .PAM: No ENDHDR'));
return;
}
const header = headerStr.substring(0, headerEnd + 7).split('\n');
if (header[0] !== 'P7') {
reject(new Error('Invalid .PAM: Missing P7 magic'));
return;
}
for (let i = 1; i < header.length; i++) {
let line = header[i].trim();
if (!line || line.startsWith('#')) continue;
const tokens = line.split(/\s+/);
const key = tokens[0];
if (key === 'ENDHDR') break;
if (key === 'TUPLTYPE') {
this.properties.TUPLTYPE += (this.properties.TUPLTYPE ? ' ' : '') + tokens.slice(1).join(' ');
} else if (['WIDTH', 'HEIGHT', 'DEPTH', 'MAXVAL'].includes(key)) {
this.properties[key] = parseInt(tokens[1], 10);
}
}
if (Object.values(this.properties).some(v => v === null && typeof v !== 'string')) {
reject(new Error('Missing required .PAM properties'));
return;
}
this.raster = buffer.slice(headerEnd + 7);
const sampleSize = this.properties.MAXVAL <= 255 ? 1 : 2;
const expectedSize = this.properties.WIDTH * this.properties.HEIGHT * this.properties.DEPTH * sampleSize;
if (this.raster.byteLength !== expectedSize) {
reject(new Error('Invalid raster size'));
return;
}
resolve();
};
reader.onerror = reject;
reader.readAsArrayBuffer(file);
});
}
printProperties() {
for (const [key, value] of Object.entries(this.properties)) {
console.log(`${key}: ${value}`);
}
}
write(filename = 'output.pam', newRaster = null) {
if (Object.values(this.properties).some(v => v === null && typeof v !== 'string')) {
throw new Error('Missing properties for writing');
}
let header = 'P7\n';
['WIDTH', 'HEIGHT', 'DEPTH', 'MAXVAL'].forEach(key => {
header += `${key} ${this.properties[key]}\n`;
});
if (this.properties.TUPLTYPE) {
header += `TUPLTYPE ${this.properties.TUPLTYPE}\n`;
}
header += 'ENDHDR\n';
const headerBuffer = new TextEncoder().encode(header);
const rasterToWrite = newRaster || this.raster || new ArrayBuffer(0);
const fullBuffer = new Uint8Array(headerBuffer.byteLength + rasterToWrite.byteLength);
fullBuffer.set(new Uint8Array(headerBuffer), 0);
fullBuffer.set(new Uint8Array(rasterToWrite), headerBuffer.byteLength);
const blob = new Blob([fullBuffer], { type: 'application/octet-stream' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
a.click();
URL.revokeObjectURL(url);
}
}
// Example usage:
// const input = document.querySelector('input[type="file"]');
// input.addEventListener('change', async (e) => {
// const pam = new PamFile(e.target.files[0]);
// await pam.read(e.target.files[0]);
// pam.printProperties();
// pam.write();
// });
7. C++ Class for .PAM File Handling
#include <iostream>
#include <fstream>
#include <sstream>
#include <string>
#include <vector>
#include <map>
#include <cstdint>
class PamFile {
private:
std::map<std::string, std::string> properties;
std::vector<uint8_t> raster;
public:
PamFile(const std::string& filename = "") {
properties["WIDTH"] = "";
properties["HEIGHT"] = "";
properties["DEPTH"] = "";
properties["MAXVAL"] = "";
properties["TUPLTYPE"] = "";
if (!filename.empty()) {
read(filename);
}
}
void read(const std::string& filename) {
std::ifstream file(filename, std::ios::binary);
if (!file) {
throw std::runtime_error("Cannot open file");
}
std::vector<uint8_t> data((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
auto it = std::search(data.begin(), data.end(), {'E','N','D','H','D','R','\n'});
if (it == data.end()) {
throw std::runtime_error("Invalid .PAM: No ENDHDR");
}
size_t headerEnd = std::distance(data.begin(), it) + 7;
std::string header(data.begin(), data.begin() + headerEnd);
std::istringstream headerStream(header);
std::string line;
std::getline(headerStream, line);
if (line != "P7") {
throw std::runtime_error("Invalid .PAM: Missing P7 magic");
}
while (std::getline(headerStream, line)) {
line.erase(0, line.find_first_not_of(" \t")); // Trim left
if (line.empty() || line[0] == '#') continue;
std::istringstream lineStream(line);
std::string key;
lineStream >> key;
if (key == "ENDHDR") break;
if (key == "TUPLTYPE") {
std::string value;
std::getline(lineStream, value);
value.erase(0, value.find_first_not_of(" \t"));
properties["TUPLTYPE"] += (properties["TUPLTYPE"].empty() ? "" : " ") + value;
} else if (key == "WIDTH" || key == "HEIGHT" || key == "DEPTH" || key == "MAXVAL") {
std::string value;
lineStream >> value;
properties[key] = value;
}
}
if (properties["WIDTH"].empty() || properties["HEIGHT"].empty() ||
properties["DEPTH"].empty() || properties["MAXVAL"].empty()) {
throw std::runtime_error("Missing required .PAM properties");
}
raster.assign(data.begin() + headerEnd, data.end());
int width = std::stoi(properties["WIDTH"]);
int height = std::stoi(properties["HEIGHT"]);
int depth = std::stoi(properties["DEPTH"]);
int maxval = std::stoi(properties["MAXVAL"]);
int sampleSize = (maxval <= 255) ? 1 : 2;
size_t expectedSize = static_cast<size_t>(width) * height * depth * sampleSize;
if (raster.size() != expectedSize) {
throw std::runtime_error("Invalid raster size");
}
}
void printProperties() const {
for (const auto& pair : properties) {
std::cout << pair.first << ": " << pair.second << std::endl;
}
}
void write(const std::string& filename, const std::vector<uint8_t>& newRaster = {}) const {
if (properties.at("WIDTH").empty() || properties.at("HEIGHT").empty() ||
properties.at("DEPTH").empty() || properties.at("MAXVAL").empty()) {
throw std::runtime_error("Missing properties for writing");
}
std::ofstream file(filename, std::ios::binary);
if (!file) {
throw std::runtime_error("Cannot open file for writing");
}
file << "P7\n";
file << "WIDTH " << properties.at("WIDTH") << "\n";
file << "HEIGHT " << properties.at("HEIGHT") << "\n";
file << "DEPTH " << properties.at("DEPTH") << "\n";
file << "MAXVAL " << properties.at("MAXVAL") << "\n";
if (!properties.at("TUPLTYPE").empty()) {
file << "TUPLTYPE " << properties.at("TUPLTYPE") << "\n";
}
file << "ENDHDR\n";
const auto& rasterToWrite = newRaster.empty() ? raster : newRaster;
file.write(reinterpret_cast<const char*>(rasterToWrite.data()), rasterToWrite.size());
}
};
// Example usage:
// int main() {
// try {
// PamFile pam("sample.pam");
// pam.printProperties();
// pam.write("output.pam");
// } catch (const std::exception& e) {
// std::cerr << e.what() << std::endl;
// }
// return 0;
// }