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.

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.

Drag and drop a .PAM file here

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;
// }