Task 676: .SPIFF File Format

Task 676: .SPIFF File Format

File Format Specifications for the .SPIFF File Format

The .SPIFF file format, known as the Still Picture Interchange File Format, is a standard defined in ISO/IEC 10918-3:1994 (Annex F), also equivalent to ITU-T Recommendation T.84. It is designed for the storage and interchange of still images, supporting various compression methods including JPEG, JBIG, and uncompressed data. The format is binary, unstructured, and uses big-endian byte order. The file structure consists of a fixed 36-byte header, an optional directory with tagged entries, the image data stream, and optional indirect data. The format allows for transcoding from other image formats like JFIF and supports features such as thumbnails, metadata tags, and application profiles for specific use cases like continuous-tone or facsimile images.

1. List of All Properties Intrinsic to the File Format

The properties are divided into header fields (mandatory) and directory entries (optional except for the End of Directory marker). These properties define the image characteristics, metadata, and structure. The header is 36 bytes long, and directory entries are variable-length, each a multiple of 4 bytes, ending with the mandatory End of Directory entry.

Header Properties:

  • Magic Number (4 bytes, unsigned integer): Primary identification value, always 0xFFD8FFE8.
  • Header Length (2 bytes, unsigned short): Length of the header excluding the magic number; always 32 for version 1.0.
  • Identifier (6 bytes, string): Secondary identification, always "SPIFF\0".
  • Version (2 bytes, unsigned short): Major and minor version numbers (e.g., 0x0100 for version 1.0).
  • Profile Id (1 byte, unsigned byte): Application profile (0: no profile, 1: continuous-tone base, 2: continuous-tone progressive, 3: bilevel facsimile, 4: continuous-tone facsimile).
  • Number of Components (1 byte, unsigned byte): Number of color components in the primary image (e.g., 1 for grayscale, 3 for RGB).
  • Image Height (4 bytes, unsigned integer): Number of scan lines in the primary image.
  • Image Width (4 bytes, unsigned integer): Number of samples per line in the primary image.
  • Color Space (1 byte, unsigned byte): Color space code (e.g., 0: bilevel, 8: grayscale, 10: RGB; see specification for full list).
  • Bits Per Sample (1 byte, unsigned byte): Number of bits per sample (1 to 32).
  • Compression Type (1 byte, unsigned byte): Compression method (0: uncompressed, 4: JBIG, 5: JPEG; see specification for full list).
  • Resolution Units (1 byte, unsigned byte): Units for resolution (0: aspect ratio, 1: dots per inch, 2: dots per centimeter).
  • Vertical Resolution (4 bytes, unsigned integer): Vertical resolution or aspect ratio numerator.
  • Horizontal Resolution (4 bytes, unsigned integer): Horizontal resolution or aspect ratio denominator.

Directory Properties (tagged entries, optional except End of Directory):

  • End of Directory (mandatory, tag 1): Marks the end of the directory; no data.
  • Transfer Characteristics (tag 2): Gamma correction value.
  • Component Registration (tag 3): Location of color components.
  • Image Orientation (tag 4): Rotation or flip status.
  • Thumbnail Image (tag 5): Embedded thumbnail header and data.
  • Image Title (tag 6): Text string for image title.
  • Image Description (tag 7): Text string for image description.
  • Time Stamp (tag 8): ISO 8601 timestamp.
  • Version Number (tag 9): Image version number.
  • Creator Identification (tag 10): Text string for creator.
  • Protection Indicator (tag 11): Authenticity level.
  • Copyright Information (tag 12): Text string for copyright.
  • Contact Information (tag 13): Text string for contact details.
  • Tile Index (tag 14): Pointers to image tiles (indirect data).
  • Scan Index (tag 15): Pointers to progressive scans (indirect data).
  • Set Reference (tag 16): Relationship to other files in a set.

Despite an extensive search across web sources, standards repositories, and image format archives, no reliable direct download links for .SPIFF files could be located. The format is not widely adopted, and public sample files are not readily available. Common extensions for SPIFF files include .spf or .jpg (for JPEG variants), but no verified examples were found. If required, samples can be generated using tools like the Independent JPEG Group's library, which supports SPIFF creation, but no pre-existing direct links exist.

3. Ghost Blog Embedded HTML JavaScript for Drag-and-Drop .SPIFF File Dump

The following is an embedded HTML snippet with JavaScript that can be inserted into a Ghost blog post. It creates a drop zone for a .SPIFF file, parses the file upon drop, extracts the properties, and displays them on the screen. It uses the FileReader API and DataView for parsing.

Drag and drop a .SPIFF file here

4. Python Class for .SPIFF File Handling

The following Python class can open, decode, read, write, and print the properties of a .SPIFF file. It uses the struct module for packing and unpacking binary data. The write method assumes basic usage without indirect data and requires image data to be provided.

import struct

class SPIFFFile:
    def __init__(self, filename=None):
        self.properties = {'Header': {}, 'Directory': {}}
        if filename:
            self.read(filename)

    def read(self, filename):
        with open(filename, 'rb') as f:
            data = f.read()
        offset = 0
        magic = struct.unpack_from('>I', data, offset)[0]
        offset += 4
        if magic != 0xFFD8FFE8:
            raise ValueError("Not a valid .SPIFF file")
        self.properties['Header']['Magic Number'] = hex(magic)
        self.properties['Header']['Header Length'] = struct.unpack_from('>H', data, offset)[0]
        offset += 2
        self.properties['Header']['Identifier'] = struct.unpack_from('6s', data, offset)[0].decode('ascii')
        offset += 6
        version = struct.unpack_from('>H', data, offset)[0]
        offset += 2
        self.properties['Header']['Version'] = f"{version >> 8}.{version & 0xFF}"
        self.properties['Header']['Profile Id'] = struct.unpack_from('B', data, offset)[0]
        offset += 1
        self.properties['Header']['Number of Components'] = struct.unpack_from('B', data, offset)[0]
        offset += 1
        self.properties['Header']['Image Height'] = struct.unpack_from('>I', data, offset)[0]
        offset += 4
        self.properties['Header']['Image Width'] = struct.unpack_from('>I', data, offset)[0]
        offset += 4
        self.properties['Header']['Color Space'] = struct.unpack_from('B', data, offset)[0]
        offset += 1
        self.properties['Header']['Bits Per Sample'] = struct.unpack_from('B', data, offset)[0]
        offset += 1
        self.properties['Header']['Compression Type'] = struct.unpack_from('B', data, offset)[0]
        offset += 1
        self.properties['Header']['Resolution Units'] = struct.unpack_from('B', data, offset)[0]
        offset += 1
        self.properties['Header']['Vertical Resolution'] = struct.unpack_from('>I', data, offset)[0]
        offset += 4
        self.properties['Header']['Horizontal Resolution'] = struct.unpack_from('>I', data, offset)[0]
        offset += 4
        while True:
            entry_magic = struct.unpack_from('>H', data, offset)[0]
            offset += 2
            if entry_magic != 0xFFE8:
                raise ValueError("Invalid directory entry")
            entry_length = struct.unpack_from('>H', data, offset)[0]
            offset += 2
            entry_tag = struct.unpack_from('>I', data, offset)[0]
            offset += 4
            if entry_tag == 1:
                break  # EOD
            data_length = entry_length - 6
            entry_data = data[offset:offset + data_length]
            tag_name = self._get_tag_name(entry_tag)
            if tag_name in ['Image Title', 'Image Description', 'Creator Identification', 'Copyright Information', 'Contact Information']:
                value = entry_data.decode('utf-8', 'ignore')
            else:
                value = entry_data.hex()
            self.properties['Directory'][tag_name] = value
            offset += data_length
            pad = (4 - (entry_length % 4)) % 4
            offset += pad

    def _get_tag_name(self, tag):
        tags = {
            2: 'Transfer Characteristics',
            3: 'Component Registration',
            4: 'Image Orientation',
            5: 'Thumbnail Image',
            6: 'Image Title',
            7: 'Image Description',
            8: 'Time Stamp',
            9: 'Version Number',
            10: 'Creator Identification',
            11: 'Protection Indicator',
            12: 'Copyright Information',
            13: 'Contact Information',
            14: 'Tile Index',
            15: 'Scan Index',
            16: 'Set Reference'
        }
        return tags.get(tag, f'Unknown Tag {tag}')

    def print_properties(self):
        for section, props in self.properties.items():
            print(f"[{section}]")
            for key, value in props.items():
                print(f"{key}: {value}")

    def write(self, filename, image_data=b'', indirect_data=b''):
        header = b''
        h = self.properties['Header']
        header += struct.pack('>I', int(h['Magic Number'], 16))
        header += struct.pack('>H', h['Header Length'])
        header += h['Identifier'].encode('ascii')
        major, minor = map(int, h['Version'].split('.'))
        version = (major << 8) | minor
        header += struct.pack('>H', version)
        header += struct.pack('B', h['Profile Id'])
        header += struct.pack('B', h['Number of Components'])
        header += struct.pack('>I', h['Image Height'])
        header += struct.pack('>I', h['Image Width'])
        header += struct.pack('B', h['Color Space'])
        header += struct.pack('B', h['Bits Per Sample'])
        header += struct.pack('B', h['Compression Type'])
        header += struct.pack('B', h['Resolution Units'])
        header += struct.pack('>I', h['Vertical Resolution'])
        header += struct.pack('>I', h['Horizontal Resolution'])
        directory = b''
        for tag_name, value in self.properties['Directory'].items():
            tag = next((k for k, v in self._get_tag_name(0).items() if v == tag_name), None)
            if tag is None:
                continue
            if isinstance(value, str):
                entry_data = value.encode('utf-8')
            else:
                entry_data = bytes.fromhex(value)
            entry_length = 6 + len(entry_data)
            pad = (4 - (entry_length % 4)) % 4
            entry_length += pad
            directory += struct.pack('>H', 0xFFE8)
            directory += struct.pack('>H', entry_length)
            directory += struct.pack('>I', tag)
            directory += entry_data
            directory += b'\x00' * pad
        directory += struct.pack('>H', 0xFFE8)
        directory += struct.pack('>H', 8)
        directory += struct.pack('>I', 1)
        with open(filename, 'wb') as f:
            f.write(header)
            f.write(directory)
            f.write(image_data)
            f.write(indirect_data)

5. Java Class for .SPIFF File Handling

The following Java class can open, decode, read, write, and print the properties of a .SPIFF file. It uses ByteBuffer for parsing and assumes big-endian order.

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.channels.FileChannel;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;

public class SpiffFile {
    private Map<String, Map<String, Object>> properties = new HashMap<>();

    public SpiffFile(String filename) throws IOException {
        properties.put("Header", new HashMap<>());
        properties.put("Directory", new HashMap<>());
        if (filename != null) {
            read(filename);
        }
    }

    public void read(String filename) throws IOException {
        byte[] data = Files.readAllBytes(Paths.get(filename));
        ByteBuffer buffer = ByteBuffer.wrap(data).order(ByteOrder.BIG_ENDIAN);
        int offset = 0;
        int magic = buffer.getInt(offset);
        offset += 4;
        if (magic != (int) 0xFFD8FFE8L) {
            throw new IOException("Not a valid .SPIFF file");
        }
        properties.get("Header").put("Magic Number", "0x" + Integer.toHexString(magic).toUpperCase());
        properties.get("Header").put("Header Length", (int) buffer.getShort(offset));
        offset += 2;
        byte[] idBytes = new byte[6];
        buffer.position(offset);
        buffer.get(idBytes);
        properties.get("Header").put("Identifier", new String(idBytes));
        offset += 6;
        short version = buffer.getShort(offset);
        offset += 2;
        properties.get("Header").put("Version", (version >> 8) + "." + (version & 0xFF));
        properties.get("Header").put("Profile Id", Byte.toUnsignedInt(buffer.get(offset++)));
        properties.get("Header").put("Number of Components", Byte.toUnsignedInt(buffer.get(offset++)));
        properties.get("Header").put("Image Height", Integer.toUnsignedLong(buffer.getInt(offset)));
        offset += 4;
        properties.get("Header").put("Image Width", Integer.toUnsignedLong(buffer.get(offset)));
        offset += 4;
        properties.get("Header").put("Color Space", Byte.toUnsignedInt(buffer.get(offset++)));
        properties.get("Header").put("Bits Per Sample", Byte.toUnsignedInt(buffer.get(offset++)));
        properties.get("Header").put("Compression Type", Byte.toUnsignedInt(buffer.get(offset++)));
        properties.get("Header").put("Resolution Units", Byte.toUnsignedInt(buffer.get(offset++)));
        properties.get("Header").put("Vertical Resolution", Integer.toUnsignedLong(buffer.getInt(offset)));
        offset += 4;
        properties.get("Header").put("Horizontal Resolution", Integer.toUnsignedLong(buffer.getInt(offset)));
        offset += 4;
        while (true) {
            short entryMagic = buffer.getShort(offset);
            offset += 2;
            if (entryMagic != (short) 0xFFE8) {
                throw new IOException("Invalid directory entry");
            }
            short entryLength = buffer.getShort(offset);
            offset += 2;
            int entryTag = buffer.getInt(offset);
            offset += 4;
            if (entryTag == 1) {
                break; // EOD
            }
            int dataLength = entryLength - 6;
            byte[] entryData = new byte[dataLength];
            buffer.position(offset);
            buffer.get(entryData);
            String tagName = getTagName(entryTag);
            Object value;
            if (tagName.contains("Title") || tagName.contains("Description") || tagName.contains("Identification") || tagName.contains("Copyright") || tagName.contains("Contact")) {
                value = new String(entryData);
            } else {
                StringBuilder hex = new StringBuilder();
                for (byte b : entryData) {
                    hex.append(String.format("%02X ", b));
                }
                value = hex.toString().trim();
            }
            properties.get("Directory").put(tagName, value);
            offset += dataLength;
            int pad = (4 - (entryLength % 4)) % 4;
            offset += pad;
        }
    }

    private String getTagName(int tag) {
        Map<Integer, String> tags = new HashMap<>();
        tags.put(2, "Transfer Characteristics");
        tags.put(3, "Component Registration");
        tags.put(4, "Image Orientation");
        tags.put(5, "Thumbnail Image");
        tags.put(6, "Image Title");
        tags.put(7, "Image Description");
        tags.put(8, "Time Stamp");
        tags.put(9, "Version Number");
        tags.put(10, "Creator Identification");
        tags.put(11, "Protection Indicator");
        tags.put(12, "Copyright Information");
        tags.put(13, "Contact Information");
        tags.put(14, "Tile Index");
        tags.put(15, "Scan Index");
        tags.put(16, "Set Reference");
        return tags.getOrDefault(tag, "Unknown Tag " + tag);
    }

    public void printProperties() {
        for (String section : properties.keySet()) {
            System.out.println("[" + section + "]");
            for (Map.Entry<String, Object> entry : properties.get(section).entrySet()) {
                System.out.println(entry.getKey() + ": " + entry.getValue());
            }
        }
    }

    public void write(String filename, byte[] imageData, byte[] indirectData) throws IOException {
        ByteBuffer buffer = ByteBuffer.allocate(1024 * 1024).order(ByteOrder.BIG_ENDIAN); // Arbitrary large buffer
        Map<String, Object> h = properties.get("Header");
        buffer.putInt(Integer.parseUnsignedInt((String) h.get("Magic Number"), 16));
        buffer.putShort(Short.parseShort(h.get("Header Length").toString()));
        buffer.put(((String) h.get("Identifier")).getBytes());
        String[] ver = ((String) h.get("Version")).split("\\.");
        short version = (short) ((Integer.parseInt(ver[0]) << 8) | Integer.parseInt(ver[1]));
        buffer.putShort(version);
        buffer.put((byte) Integer.parseInt(h.get("Profile Id").toString()));
        buffer.put((byte) Integer.parseInt(h.get("Number of Components").toString()));
        buffer.putInt(Integer.parseUnsignedInt(h.get("Image Height").toString()));
        buffer.putInt(Integer.parseUnsignedInt(h.get("Image Width").toString()));
        buffer.put((byte) Integer.parseInt(h.get("Color Space").toString()));
        buffer.put((byte) Integer.parseInt(h.get("Bits Per Sample").toString()));
        buffer.put((byte) Integer.parseInt(h.get("Compression Type").toString()));
        buffer.put((byte) Integer.parseInt(h.get("Resolution Units").toString()));
        buffer.putInt(Integer.parseUnsignedInt(h.get("Vertical Resolution").toString()));
        buffer.putInt(Integer.parseUnsignedInt(h.get("Horizontal Resolution").toString()));
        for (Map.Entry<String, Object> entry : properties.get("Directory").entrySet()) {
            int tag = getTagName(0).entrySet().stream().filter(e -> e.getValue().equals(entry.getKey())).findFirst().map(Map.Entry::getKey).orElse(0);
            if (tag == 0) continue;
            byte[] entryData;
            if (entry.getValue() instanceof String && ((String) entry.getValue()).matches("^[0-9A-F ]+$")) {
                String[] hex = ((String) entry.getValue()).split(" ");
                entryData = new byte[hex.length];
                for (int i = 0; i < hex.length; i++) {
                    entryData[i] = (byte) Integer.parseInt(hex[i], 16);
                }
            } else {
                entryData = ((String) entry.getValue()).getBytes();
            }
            int entryLength = 6 + entryData.length;
            int pad = (4 - (entryLength % 4)) % 4;
            entryLength += pad;
            buffer.putShort((short) 0xFFE8);
            buffer.putShort((short) entryLength);
            buffer.putInt(tag);
            buffer.put(entryData);
            for (int i = 0; i < pad; i++) buffer.put((byte) 0);
        }
        buffer.putShort((short) 0xFFE8);
        buffer.putShort((short) 8);
        buffer.putInt(1);
        buffer.put(imageData);
        buffer.put(indirectData);
        buffer.flip();
        try (FileChannel channel = new FileOutputStream(filename).getChannel()) {
            channel.write(buffer);
        }
    }
}

6. JavaScript Class for .SPIFF File Handling

The following JavaScript class can open, decode, read, write, and print the properties of a .SPIFF file. It is designed for Node.js, using the fs module for file I/O and Buffer for parsing.

const fs = require('fs');

class SpiffFile {
  constructor(filename = null) {
    this.properties = { Header: {}, Directory: {} };
    if (filename) {
      this.read(filename);
    }
  }

  read(filename) {
    const data = fs.readFileSync(filename);
    let offset = 0;
    const view = new DataView(data.buffer);
    const magic = view.getUint32(offset, false);
    offset += 4;
    if (magic !== 0xFFD8FFE8) {
      throw new Error('Not a valid .SPIFF file');
    }
    this.properties.Header['Magic Number'] = '0x' + magic.toString(16).toUpperCase();
    this.properties.Header['Header Length'] = view.getUint16(offset, false);
    offset += 2;
    let id = '';
    for (let i = 0; i < 6; i++) id += String.fromCharCode(view.getUint8(offset + i));
    this.properties.Header['Identifier'] = id;
    offset += 6;
    const version = view.getUint16(offset, false);
    offset += 2;
    this.properties.Header['Version'] = (version >> 8) + '.' + (version & 0xFF);
    this.properties.Header['Profile Id'] = view.getUint8(offset++);
    this.properties.Header['Number of Components'] = view.getUint8(offset++);
    this.properties.Header['Image Height'] = view.getUint32(offset, false);
    offset += 4;
    this.properties.Header['Image Width'] = view.getUint32(offset, false);
    offset += 4;
    this.properties.Header['Color Space'] = view.getUint8(offset++);
    this.properties.Header['Bits Per Sample'] = view.getUint8(offset++);
    this.properties.Header['Compression Type'] = view.getUint8(offset++);
    this.properties.Header['Resolution Units'] = view.getUint8(offset++);
    this.properties.Header['Vertical Resolution'] = view.getUint32(offset, false);
    offset += 4;
    this.properties.Header['Horizontal Resolution'] = view.getUint32(offset, false);
    offset += 4;
    while (true) {
      const entryMagic = view.getUint16(offset, false);
      offset += 2;
      if (entryMagic !== 0xFFE8) {
        throw new Error('Invalid directory entry');
      }
      const entryLength = view.getUint16(offset, false);
      offset += 2;
      const entryTag = view.getUint32(offset, false);
      offset += 4;
      if (entryTag === 1) break;
      const dataLength = entryLength - 6;
      let value = '';
      for (let i = 0; i < dataLength; i++) {
        value += view.getUint8(offset + i).toString(16).padStart(2, '0').toUpperCase() + ' ';
      }
      value = value.trim();
      const tagName = this.getTagName(entryTag);
      if (['Image Title', 'Image Description', 'Creator Identification', 'Copyright Information', 'Contact Information'].includes(tagName)) {
        value = new TextDecoder().decode(data.subarray(offset, offset + dataLength));
      }
      this.properties.Directory[tagName] = value;
      offset += dataLength;
      const pad = (4 - (entryLength % 4)) % 4;
      offset += pad;
    }
  }

  getTagName(tag) {
    const tags = {
      2: 'Transfer Characteristics',
      3: 'Component Registration',
      4: 'Image Orientation',
      5: 'Thumbnail Image',
      6: 'Image Title',
      7: 'Image Description',
      8: 'Time Stamp',
      9: 'Version Number',
      10: 'Creator Identification',
      11: 'Protection Indicator',
      12: 'Copyright Information',
      13: 'Contact Information',
      14: 'Tile Index',
      15: 'Scan Index',
      16: 'Set Reference'
    };
    return tags[tag] || `Unknown Tag ${tag}`;
  }

  printProperties() {
    for (const section in this.properties) {
      console.log(`[${section}]`);
      for (const key in this.properties[section]) {
        console.log(`${key}: ${this.properties[section][key]}`);
      }
    }
  }

  write(filename, imageData = Buffer.alloc(0), indirectData = Buffer.alloc(0)) {
    let buffer = Buffer.alloc(1024 * 1024); // Arbitrary large
    let offset = 0;
    const h = this.properties.Header;
    buffer.writeUInt32BE(parseInt(h['Magic Number'], 16), offset);
    offset += 4;
    buffer.writeUInt16BE(h['Header Length'], offset);
    offset += 2;
    buffer.write(h['Identifier'], offset, 6, 'ascii');
    offset += 6;
    const [major, minor] = h['Version'].split('.').map(Number);
    buffer.writeUInt16BE((major << 8) | minor, offset);
    offset += 2;
    buffer.writeUInt8(h['Profile Id'], offset++);
    buffer.writeUInt8(h['Number of Components'], offset++);
    buffer.writeUInt32BE(h['Image Height'], offset);
    offset += 4;
    buffer.writeUInt32BE(h['Image Width'], offset);
    offset += 4;
    buffer.writeUInt8(h['Color Space'], offset++);
    buffer.writeUInt8(h['Bits Per Sample'], offset++);
    buffer.writeUInt8(h['Compression Type'], offset++);
    buffer.writeUInt8(h['Resolution Units'], offset++);
    buffer.writeUInt32BE(h['Vertical Resolution'], offset);
    offset += 4;
    buffer.writeUInt32BE(h['Horizontal Resolution'], offset);
    offset += 4;
    for (const tagName in this.properties.Directory) {
      const tag = Object.entries(this.getTagName(0)).find(([k, v]) => v === tagName)?.[0];
      if (!tag) continue;
      let entryData;
      const value = this.properties.Directory[tagName];
      if (typeof value === 'string' && /^[0-9A-F ]+$/i.test(value)) {
        entryData = Buffer.from(value.split(' ').map(hex => parseInt(hex, 16)));
      } else {
        entryData = Buffer.from(value, 'utf-8');
      }
      let entryLength = 6 + entryData.length;
      const pad = (4 - (entryLength % 4)) % 4;
      entryLength += pad;
      buffer.writeUInt16BE(0xFFE8, offset);
      offset += 2;
      buffer.writeUInt16BE(entryLength, offset);
      offset += 2;
      buffer.writeUInt32BE(tag, offset);
      offset += 4;
      entryData.copy(buffer, offset);
      offset += entryData.length;
      for (let i = 0; i < pad; i++) buffer[offset++] = 0;
    }
    buffer.writeUInt16BE(0xFFE8, offset);
    offset += 2;
    buffer.writeUInt16BE(8, offset);
    offset += 2;
    buffer.writeUInt32BE(1, offset);
    offset += 4;
    imageData.copy(buffer, offset);
    offset += imageData.length;
    indirectData.copy(buffer, offset);
    offset += indirectData.length;
    fs.writeFileSync(filename, buffer.slice(0, offset));
  }
}

7. C Class for .SPIFF File Handling

The following C++ class (as C does not have native classes) can open, decode, read, write, and print the properties of a .SPIFF file. It uses standard I/O and memory management for parsing.

#include <iostream>
#include <fstream>
#include <iomanip>
#include <map>
#include <string>
#include <vector>
#include <cstring>
#include <endian.h> // For big-endian conversion if needed; assume host is little-endian

class SpiffFile {
private:
    std::map<std::string, std::map<std::string, std::string>> properties;

public:
    SpiffFile(const std::string& filename = "") {
        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");
        }
        file.seekg(0, std::ios::end);
        size_t size = file.tellg();
        file.seekg(0, std::ios::beg);
        std::vector<char> data(size);
        file.read(data.data(), size);
        size_t offset = 0;
        uint32_t magic;
        memcpy(&magic, data.data() + offset, 4);
        magic = be32toh(magic);
        offset += 4;
        if (magic != 0xFFD8FFE8) {
            throw std::runtime_error("Not a valid .SPIFF file");
        }
        properties["Header"]["Magic Number"] = "0x" + std::to_string(magic);
        uint16_t header_length;
        memcpy(&header_length, data.data() + offset, 2);
        header_length = be16toh(header_length);
        offset += 2;
        properties["Header"]["Header Length"] = std::to_string(header_length);
        char id[7];
        memcpy(id, data.data() + offset, 6);
        id[6] = '\0';
        properties["Header"]["Identifier"] = id;
        offset += 6;
        uint16_t version;
        memcpy(&version, data.data() + offset, 2);
        version = be16toh(version);
        offset += 2;
        properties["Header"]["Version"] = std::to_string(version >> 8) + "." + std::to_string(version & 0xFF);
        uint8_t profile_id = static_cast<uint8_t>(data[offset++]);
        properties["Header"]["Profile Id"] = std::to_string(profile_id);
        uint8_t num_components = static_cast<uint8_t>(data[offset++]);
        properties["Header"]["Number of Components"] = std::to_string(num_components);
        uint32_t image_height;
        memcpy(&image_height, data.data() + offset, 4);
        image_height = be32toh(image_height);
        offset += 4;
        properties["Header"]["Image Height"] = std::to_string(image_height);
        uint32_t image_width;
        memcpy(&image_width, data.data() + offset, 4);
        image_width = be32toh(image_width);
        offset += 4;
        properties["Header"]["Image Width"] = std::to_string(image_width);
        uint8_t color_space = static_cast<uint8_t>(data[offset++]);
        properties["Header"]["Color Space"] = std::to_string(color_space);
        uint8_t bits_per_sample = static_cast<uint8_t>(data[offset++]);
        properties["Header"]["Bits Per Sample"] = std::to_string(bits_per_sample);
        uint8_t compression_type = static_cast<uint8_t>(data[offset++]);
        properties["Header"]["Compression Type"] = std::to_string(compression_type);
        uint8_t resolution_units = static_cast<uint8_t>(data[offset++]);
        properties["Header"]["Resolution Units"] = std::to_string(resolution_units);
        uint32_t vertical_resolution;
        memcpy(&vertical_resolution, data.data() + offset, 4);
        vertical_resolution = be32toh(vertical_resolution);
        offset += 4;
        properties["Header"]["Vertical Resolution"] = std::to_string(vertical_resolution);
        uint32_t horizontal_resolution;
        memcpy(&horizontal_resolution, data.data() + offset, 4);
        horizontal_resolution = be32toh(horizontal_resolution);
        offset += 4;
        properties["Header"]["Horizontal Resolution"] = std::to_string(horizontal_resolution);
        while (true) {
            uint16_t entry_magic;
            memcpy(&entry_magic, data.data() + offset, 2);
            entry_magic = be16toh(entry_magic);
            offset += 2;
            if (entry_magic != 0xFFE8) {
                throw std::runtime_error("Invalid directory entry");
            }
            uint16_t entry_length;
            memcpy(&entry_length, data.data() + offset, 2);
            entry_length = be16toh(entry_length);
            offset += 2;
            uint32_t entry_tag;
            memcpy(&entry_tag, data.data() + offset, 4);
            entry_tag = be32toh(entry_tag);
            offset += 4;
            if (entry_tag == 1) {
                break; // EOD
            }
            size_t data_length = entry_length - 6;
            std::string value;
            for (size_t i = 0; i < data_length; i++) {
                value += std::to_string(static_cast<unsigned char>(data[offset + i])) + " ";
            }
            value = value.substr(0, value.size() - 1); // Trim space
            std::string tag_name = getTagName(entry_tag);
            if (tag_name.find("Title") != std::string::npos || tag_name.find("Description") != std::string::npos || tag_name.find("Identification") != std::string::npos || tag_name.find("Copyright") != std::string::npos || tag_name.find("Contact") != std::string::npos) {
                value = std::string(data.data() + offset, data_length);
            }
            properties["Directory"][tag_name] = value;
            offset += data_length;
            int pad = (4 - (entry_length % 4)) % 4;
            offset += pad;
        }
    }

    std::string getTagName(uint32_t tag) {
        std::map<uint32_t, std::string> tags = {
            {2, "Transfer Characteristics"},
            {3, "Component Registration"},
            {4, "Image Orientation"},
            {5, "Thumbnail Image"},
            {6, "Image Title"},
            {7, "Image Description"},
            {8, "Time Stamp"},
            {9, "Version Number"},
            {10, "Creator Identification"},
            {11, "Protection Indicator"},
            {12, "Copyright Information"},
            {13, "Contact Information"},
            {14, "Tile Index"},
            {15, "Scan Index"},
            {16, "Set Reference"}
        };
        auto it = tags.find(tag);
        if (it != tags.end()) return it->second;
        return "Unknown Tag " + std::to_string(tag);
    }

    void printProperties() {
        for (const auto& section : properties) {
            std::cout << "[" << section.first << "]" << std::endl;
            for (const auto& prop : section.second) {
                std::cout << prop.first << ": " << prop.second << std::endl;
            }
        }
    }

    void write(const std::string& filename, const std::vector<char>& imageData = {}, const std::vector<char>& indirectData = {}) {
        std::ofstream file(filename, std::ios::binary);
        if (!file) {
            throw std::runtime_error("Cannot open file for writing");
        }
        auto h = properties["Header"];
        uint32_t magic = std::stoul(h["Magic Number"], nullptr, 0);
        magic = htobe32(magic);
        file.write(reinterpret_cast<char*>(&magic), 4);
        uint16_t header_length = std::stoi(h["Header Length"]);
        header_length = htobe16(header_length);
        file.write(reinterpret_cast<char*>(&header_length), 2);
        file.write(h["Identifier"].c_str(), 6);
        auto [major_str, minor_str] = [ & ]() -> std::pair<std::string, std::string> {
            size_t dot = h["Version"].find('.');
            return {h["Version"].substr(0, dot), h["Version"].substr(dot + 1)};
        }();
        uint16_t version = (std::stoi(major_str) << 8) | std::stoi(minor_str);
        version = htobe16(version);
        file.write(reinterpret_cast<char*>(&version), 2);
        uint8_t profile_id = std::stoi(h["Profile Id"]);
        file.write(reinterpret_cast<char*>(&profile_id), 1);
        uint8_t num_components = std::stoi(h["Number of Components"]);
        file.write(reinterpret_cast<char*>(&num_components), 1);
        uint32_t image_height = std::stoul(h["Image Height"]);
        image_height = htobe32(image_height);
        file.write(reinterpret_cast<char*>(&image_height), 4);
        uint32_t image_width = std::stoul(h["Image Width"]);
        image_width = htobe32(image_width);
        file.write(reinterpret_cast<char*>(&image_width), 4);
        uint8_t color_space = std::stoi(h["Color Space"]);
        file.write(reinterpret_cast<char*>(&color_space), 1);
        uint8_t bits_per_sample = std::stoi(h["Bits Per Sample"]);
        file.write(reinterpret_cast<char*>(&bits_per_sample), 1);
        uint8_t compression_type = std::stoi(h["Compression Type"]);
        file.write(reinterpret_cast<char*>(&compression_type), 1);
        uint8_t resolution_units = std::stoi(h["Resolution Units"]);
        file.write(reinterpret_cast<char*>(&resolution_units), 1);
        uint32_t vertical_resolution = std::stoul(h["Vertical Resolution"]);
        vertical_resolution = htobe32(vertical_resolution);
        file.write(reinterpret_cast<char*>(&vertical_resolution), 4);
        uint32_t horizontal_resolution = std::stoul(h["Horizontal Resolution"]);
        horizontal_resolution = htobe32(horizontal_resolution);
        file.write(reinterpret_cast<char*>(&horizontal_resolution), 4);
        auto dir = properties["Directory"];
        for (const auto& entry : dir) {
            uint32_t tag = 0;
            for (const auto& t : getTagName(0)) { // Assuming getTagName returns map
                if (t.second == entry.first) {
                    tag = t.first;
                    break;
                }
            }
            if (tag == 0) continue;
            std::vector<char> entryData;
            const std::string& value = entry.second;
            if (value.find(' ') != std::string::npos) { // Hex
                std::istringstream iss(value);
                std::string hex;
                while (iss >> hex) {
                    entryData.push_back(static_cast<char>(std::stoi(hex, nullptr, 16)));
                }
            } else {
                entryData = std::vector<char>(value.begin(), value.end());
            }
            uint16_t entry_length = 6 + entryData.size();
            int pad = (4 - (entry_length % 4)) % 4;
            entry_length += pad;
            uint16_t entry_magic = htobe16(0xFFE8);
            file.write(reinterpret_cast<char*>(&entry_magic), 2);
            entry_length = htobe16(entry_length);
            file.write(reinterpret_cast<char*>(&entry_length), 2);
            tag = htobe32(tag);
            file.write(reinterpret_cast<char*>(&tag), 4);
            file.write(entryData.data(), entryData.size());
            for (int i = 0; i < pad; i++) file.put(0);
        }
        uint16_t eod_magic = htobe16(0xFFE8);
        file.write(reinterpret_cast<char*>(&eod_magic), 2);
        uint16_t eod_length = htobe16(8);
        file.write(reinterpret_cast<char*>(&eod_length), 2);
        uint32_t eod_tag = htobe32(1);
        file.write(reinterpret_cast<char*>(&eod_tag), 4);
        file.write(imageData.data(), imageData.size());
        file.write(indirectData.data(), indirectData.size());
    }
};