Task 331: .JPEG File Format

Task 331: .JPEG File Format

1. File Format Specifications for .JPEG

The .JPEG (or .JPG) file format is defined by the ISO/IEC 10918-1 standard (also known as ITU-T T.81), which specifies the processes for compressing and decompressing continuous-tone still images using Discrete Cosine Transform (DCT)-based methods. It supports baseline sequential, extended sequential, progressive, and lossless modes, with Huffman or arithmetic entropy coding. The format is a stream of markers (two-byte codes starting with 0xFF) and segments containing parameters and compressed data. The JPEG File Interchange Format (JFIF) is a minimal wrapper (Annex B of the standard) for interchange, adding an APP0 marker for metadata like density and thumbnails. Common extensions include EXIF for camera metadata in APP1 segments. The file starts with SOI (0xFFD8) and ends with EOI (0xFFD9).

2. List of Properties Intrinsic to the .JPEG File Format

Based on the core specification and JFIF extension, here is a comprehensive list of properties that are part of the file's internal structure (markers, segments, and fields). These are extracted from the binary stream and include header, frame, scan, table, and application-specific data. Properties are grouped by segment type for clarity:

  • File Signature: The starting bytes (always 0xFFD8 for SOI).
  • Image Height: Vertical dimension in pixels (2 bytes from SOF segment).
  • Image Width: Horizontal dimension in pixels (2 bytes from SOF segment).
  • Sample Precision: Bits per sample (1 byte from SOF, typically 8 for baseline).
  • Number of Components: Number of color channels (1 byte from SOF, e.g., 1 for grayscale, 3 for YCbCr).
  • Component Details: For each component - ID (1 byte), Horizontal Sampling Factor (4 bits), Vertical Sampling Factor (4 bits), Quantization Table ID (1 byte).
  • Quantization Tables: Table ID and precision (1 byte, high 4 bits for precision 0/1, low 4 bits ID), followed by 64 values (1 or 2 bytes each depending on precision).
  • Huffman Tables: Class (DC/AC, 4 bits), ID (4 bits), Number of codes per length (16 bytes), followed by variable code values.
  • Arithmetic Conditioning Tables (if arithmetic coding): Table class and destination (1 byte), followed by conditioning values.
  • Restart Interval: Number of MCUs between restarts (2 bytes from DRI segment).
  • Comments: Arbitrary string data (variable length from COM segment).
  • JFIF-Specific Properties (from APP0 "JFIF"):
  • Version: Major and minor (2 bytes, e.g., 0x0102).
  • Density Units: (1 byte, 0=no units/aspect ratio, 1=dots/inch, 2=dots/cm).
  • X Density: Horizontal resolution (2 bytes).
  • Y Density: Vertical resolution (2 bytes).
  • Thumbnail Width: Horizontal thumbnail pixels (1 byte).
  • Thumbnail Height: Vertical thumbnail pixels (1 byte).
  • Thumbnail Data: RGB pixels or compressed data (variable, if present).
  • EXIF-Specific Properties (if APP1 "Exif" present, parsed from TIFF-like structure):
  • Orientation: Image rotation/flip (short, 1-8).
  • X Resolution: Horizontal DPI (rational).
  • Y Resolution: Vertical DPI (rational).
  • Resolution Unit: (short, 1=none, 2=inch, 3=cm).
  • Make: Camera manufacturer (ASCII string).
  • Model: Camera model (ASCII string).
  • Software: Processing software (ASCII string).
  • DateTime: Capture time (ASCII, YYYY:MM:DD HH:MM:SS).
  • Other tags: Exposure time, F-number, ISO speed, GPS coordinates, etc. (various types).
  • Other Application Data: Arbitrary data from other APPn segments (e.g., ICC profiles in APP2).
  • Scan Details: From SOS - Number of components in scan (1 byte), component selectors and tables (variable), spectral selection (3 bytes), bit position (1 byte).

These properties are intrinsic to the format's binary structure and do not include external file system attributes like size or timestamps.

4. Ghost Blog Embedded HTML JavaScript for Drag and Drop .JPEG Property Dump

Here's an HTML snippet with embedded JavaScript that can be embedded in a Ghost blog post (using the HTML card). It creates a drag-and-drop area where a user can drop a .JPEG file. The script reads the file as an ArrayBuffer, parses the JPEG structure, extracts the properties from the list above, and dumps them to the screen in a pre-formatted text area.

Drag and drop a .JPEG file here

Note: This is a basic parser; full EXIF and thumbnail data parsing is simplified. It dumps properties as JSON to the screen.

5. Python Class for .JPEG Handling

import struct
import os

class JPEGHandler:
    def __init__(self, filepath):
        self.filepath = filepath
        self.props = {}
        self.data = None

    def read_decode(self):
        with open(self.filepath, 'rb') as f:
            self.data = f.read()
        if self.data[:2] != b'\xFF\xD8':
            raise ValueError("Not a valid JPEG")
        self.props['File Signature'] = '0xFFD8'
        offset = 2
        while offset < len(self.data):
            if self.data[offset] != 0xFF:
                break
            marker = self.data[offset + 1]
            offset += 2
            if marker == 0xD9:
                break  # EOI
            if marker in range(0xD0, 0xD8) or marker in [0xD8, 0xD9]:
                continue
            length = struct.unpack('>H', self.data[offset:offset+2])[0]
            segment_data = self.data[offset+2:offset+length]
            self.parse_segment(marker, segment_data)
            offset += length

    def parse_segment(self, marker, data):
        if marker == 0xE0:  # APP0 JFIF
            if data[:5] == b'JFIF\x00':
                major, minor = data[5], data[6]
                self.props['JFIF Version'] = f'{major}.{minor}'
                self.props['Density Units'] = data[7]
                self.props['X Density'] = struct.unpack('>H', data[8:10])[0]
                self.props['Y Density'] = struct.unpack('>H', data[10:12])[0]
                self.props['Thumbnail Width'] = data[12]
                self.props['Thumbnail Height'] = data[13]
                # Thumbnail data omitted
        elif marker in [0xC0, 0xC1, 0xC2, 0xC3, 0xC5, 0xC6, 0xC7, 0xC9, 0xCA, 0xCB, 0xCD, 0xCE, 0xCF]:  # SOF
            self.props['Sample Precision'] = data[0]
            self.props['Image Height'] = struct.unpack('>H', data[1:3])[0]
            self.props['Image Width'] = struct.unpack('>H', data[3:5])[0]
            self.props['Number of Components'] = data[5]
            self.props['Component Details'] = []
            off = 6
            for _ in range(self.props['Number of Components']):
                cid = data[off]
                sampling = data[off + 1]
                h, v = sampling >> 4, sampling & 0x0F
                qid = data[off + 2]
                self.props['Component Details'].append({'ID': cid, 'H': h, 'V': v, 'QID': qid})
                off += 3
        elif marker == 0xDB:  # DQT
            self.props['Quantization Tables'] = self.props.get('Quantization Tables', [])
            off = 0
            while off < len(data):
                pq = data[off]
                p, qid = pq >> 4, pq & 0x0F
                values = []
                elem_size = 2 if p else 1
                for i in range(64):
                    val_off = off + 1 + i * elem_size
                    values.append(struct.unpack('>H' if elem_size == 2 else '>B', data[val_off:val_off + elem_size])[0])
                self.props['Quantization Tables'].append({'ID': qid, 'Precision': 16 if p else 8, 'Values': values})
                off += 1 + 64 * elem_size
        elif marker == 0xC4:  # DHT
            self.props['Huffman Tables'] = self.props.get('Huffman Tables', [])
            off = 0
            while off < len(data):
                tc = data[off]
                class_type, hid = tc >> 4, tc & 0x0F
                counts = list(data[off + 1:off + 17])
                total_codes = sum(counts)
                values = list(data[off + 17:off + 17 + total_codes])
                self.props['Huffman Tables'].append({'Class': 'AC' if class_type else 'DC', 'ID': hid, 'Counts': counts, 'Values': values})
                off += 17 + total_codes
        elif marker == 0xDD:  # DRI
            self.props['Restart Interval'] = struct.unpack('>H', data[0:2])[0]
        elif marker == 0xFE:  # COM
            self.props['Comments'] = self.props.get('Comments', [])
            self.props['Comments'].append(data.decode('utf-8', errors='ignore'))
        elif marker == 0xE1:  # APP1 EXIF
            if data[:6] == b'Exif\x00\x00':
                self.props['EXIF Tags'] = self.parse_exif(data[6:])
        # Add more as needed

    def parse_exif(self, data):
        exif = {}
        # Simplified EXIF
        byte_order = data[:2]
        little_endian = byte_order == b'II'
        fmt = '<' if little_endian else '>'
        if struct.unpack(fmt + 'H', data[2:4])[0] != 0x002A:
            return {'error': 'Invalid EXIF'}
        ifd_offset = struct.unpack(fmt + 'I', data[4:8])[0]
        num_entries = struct.unpack(fmt + 'H', data[ifd_offset:ifd_offset+2])[0]
        ifd_offset += 2
        for _ in range(num_entries):
            tag = struct.unpack(fmt + 'H', data[ifd_offset:ifd_offset+2])[0]
            typ = struct.unpack(fmt + 'H', data[ifd_offset+2:ifd_offset+4])[0]
            count = struct.unpack(fmt + 'I', data[ifd_offset+4:ifd_offset+8])[0]
            val_offset = struct.unpack(fmt + 'I', data[ifd_offset+8:ifd_offset+12])[0]
            if typ == 2:  # ASCII
                val = data[val_offset:val_offset+count].decode('ascii', errors='ignore').rstrip('\x00')
            elif typ == 3:  # Short
                val = struct.unpack(fmt + 'H', data[ifd_offset+8:ifd_offset+10])[0] if count == 1 else val_offset
            elif typ == 5:  # Rational
                num = struct.unpack(fmt + 'I', data[val_offset:val_offset+4])[0]
                den = struct.unpack(fmt + 'I', data[val_offset+4:val_offset+8])[0]
                val = num / den if den else 0
            else:
                val = None
            if tag == 0x010F: exif['Make'] = val
            elif tag == 0x0110: exif['Model'] = val
            elif tag == 0x0112: exif['Orientation'] = val
            elif tag == 0x0132: exif['DateTime'] = val
            # Add more
            ifd_offset += 12
        return exif

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

    def write(self, new_filepath=None):
        # Basic write: save original data (modify props if needed, but simplified to copy)
        filepath = new_filepath or self.filepath
        with open(filepath, 'wb') as f:
            f.write(self.data)
        print(f"File written to {filepath}")

# Example usage:
# handler = JPEGHandler('example.jpg')
# handler.read_decode()
# handler.print_properties()
# handler.write('modified.jpg')

Note: The write method currently copies the file; to fully support modifying properties, additional logic to rebuild segments would be needed.

6. Java Class for .JPEG Handling

import java.io.*;
import java.nio.*;
import java.util.*;

public class JPEGHandler {
    private String filepath;
    private Map<String, Object> props = new HashMap<>();
    private byte[] data;

    public JPEGHandler(String filepath) {
        this.filepath = filepath;
    }

    public void readDecode() throws IOException {
        File file = new File(filepath);
        data = new byte[(int) file.length()];
        try (FileInputStream fis = new FileInputStream(file)) {
            fis.read(data);
        }
        ByteBuffer buffer = ByteBuffer.wrap(data).order(ByteOrder.BIG_ENDIAN);
        if (buffer.getShort(0) != (short) 0xFFD8) {
            throw new IllegalArgumentException("Not a valid JPEG");
        }
        props.put("File Signature", "0xFFD8");
        int offset = 2;
        while (offset < data.length) {
            if ((data[offset] & 0xFF) != 0xFF) break;
            int marker = data[offset + 1] & 0xFF;
            offset += 2;
            if (marker == 0xD9) break; // EOI
            if (marker >= 0xD0 && marker <= 0xD7 || marker == 0xD8 || marker == 0xD9) continue;
            int length = (data[offset] & 0xFF) << 8 | (data[offset + 1] & 0xFF);
            ByteBuffer segBuffer = ByteBuffer.wrap(data, offset + 2, length - 2).order(ByteOrder.BIG_ENDIAN);
            parseSegment(marker, segBuffer);
            offset += length;
        }
    }

    private void parseSegment(int marker, ByteBuffer data) {
        if (marker == 0xE0) { // APP0 JFIF
            byte[] id = new byte[5];
            data.get(id);
            if (new String(id).equals("JFIF\0")) {
                props.put("JFIF Version", data.get() + "." + data.get());
                props.put("Density Units", data.get() & 0xFF);
                props.put("X Density", data.getShort() & 0xFFFF);
                props.put("Y Density", data.getShort() & 0xFFFF);
                props.put("Thumbnail Width", data.get() & 0xFF);
                props.put("Thumbnail Height", data.get() & 0xFF);
                // Thumbnail data omitted
            }
        } else if (marker >= 0xC0 && marker <= 0xCF && marker != 0xC4 && marker != 0xC8 && marker != 0xCC) { // SOF
            props.put("Sample Precision", data.get() & 0xFF);
            props.put("Image Height", data.getShort() & 0xFFFF);
            props.put("Image Width", data.getShort() & 0xFFFF);
            int nf = data.get() & 0xFF;
            props.put("Number of Components", nf);
            List<Map<String, Integer>> comps = new ArrayList<>();
            for (int i = 0; i < nf; i++) {
                Map<String, Integer> comp = new HashMap<>();
                comp.put("ID", data.get() & 0xFF);
                int sampling = data.get() & 0xFF;
                comp.put("H", sampling >> 4);
                comp.put("V", sampling & 0x0F);
                comp.put("QID", data.get() & 0xFF);
                comps.add(comp);
            }
            props.put("Component Details", comps);
        } else if (marker == 0xDB) { // DQT
            List<Map<String, Object>> qts = (List) props.getOrDefault("Quantization Tables", new ArrayList<>());
            int off = 0;
            while (off < data.limit()) {
                Map<String, Object> qt = new HashMap<>();
                int pq = data.get(off) & 0xFF;
                int p = pq >> 4;
                int qid = pq & 0x0F;
                qt.put("ID", qid);
                qt.put("Precision", p == 0 ? 8 : 16);
                List<Integer> values = new ArrayList<>();
                int elemSize = p + 1;
                for (int i = 0; i < 64; i++) {
                    int valOff = off + 1 + i * elemSize;
                    values.add(elemSize == 1 ? data.get(valOff) & 0xFF : data.getShort(valOff) & 0xFFFF);
                }
                qt.put("Values", values);
                qts.add(qt);
                off += 1 + 64 * elemSize;
            }
            props.put("Quantization Tables", qts);
        } else if (marker == 0xC4) { // DHT
            List<Map<String, Object>> hts = (List) props.getOrDefault("Huffman Tables", new ArrayList<>());
            int off = 0;
            while (off < data.limit()) {
                Map<String, Object> ht = new HashMap<>();
                int tc = data.get(off) & 0xFF;
                String classType = (tc >> 4) == 0 ? "DC" : "AC";
                int hid = tc & 0x0F;
                ht.put("Class", classType);
                ht.put("ID", hid);
                List<Integer> counts = new ArrayList<>();
                for (int i = 1; i <= 16; i++) counts.add(data.get(off + i) & 0xFF);
                ht.put("Counts", counts);
                int totalCodes = counts.stream().mapToInt(Integer::intValue).sum();
                List<Integer> values = new ArrayList<>();
                for (int i = 0; i < totalCodes; i++) values.add(data.get(off + 17 + i) & 0xFF);
                ht.put("Values", values);
                hts.add(ht);
                off += 17 + totalCodes;
            }
            props.put("Huffman Tables", hts);
        } else if (marker == 0xDD) { // DRI
            props.put("Restart Interval", data.getShort() & 0xFFFF);
        } else if (marker == 0xFE) { // COM
            List<String> comments = (List) props.getOrDefault("Comments", new ArrayList<>());
            comments.add(new String(Arrays.copyOfRange(data.array(), data.position(), data.limit())));
            props.put("Comments", comments);
        } else if (marker == 0xE1) { // APP1 EXIF
            byte[] id = new byte[6];
            data.get(id);
            if (new String(id).equals("Exif\0\0")) {
                props.put("EXIF Tags", parseExif(ByteBuffer.wrap(data.array(), data.position(), data.remaining()).order(ByteOrder.BIG_ENDIAN)));
            }
        }
    }

    private Map<String, Object> parseExif(ByteBuffer data) {
        Map<String, Object> exif = new HashMap<>();
        // Simplified
        String byteOrder = new String(new byte[]{data.get(), data.get()});
        boolean littleEndian = byteOrder.equals("II");
        data.order(littleEndian ? ByteOrder.LITTLE_ENDIAN : ByteOrder.BIG_ENDIAN);
        if (data.getShort() != 0x002A) return Map.of("error", "Invalid EXIF");
        int ifdOffset = data.getInt();
        int numEntries = data.getShort(ifdOffset);
        ifdOffset += 2;
        for (int i = 0; i < numEntries; i++) {
            int tag = data.getShort(ifdOffset) & 0xFFFF;
            int type = data.getShort(ifdOffset + 2) & 0xFFFF;
            int count = data.getInt(ifdOffset + 4);
            int valOffset = data.getInt(ifdOffset + 8);
            Object value = null;
            if (type == 2) { // ASCII
                byte[] strBytes = new byte[count];
                data.position(valOffset);
                data.get(strBytes);
                value = new String(strBytes).trim();
            } else if (type == 3) { // Short
                value = data.getShort(ifdOffset + 8) & 0xFFFF;
            } else if (type == 5) { // Rational
                long num = data.getInt(valOffset) & 0xFFFFFFFFL;
                long den = data.getInt(valOffset + 4) & 0xFFFFFFFFL;
                value = (double) num / den;
            }
            if (tag == 0x010F) exif.put("Make", value);
            else if (tag == 0x0110) exif.put("Model", value);
            else if (tag == 0x0112) exif.put("Orientation", value);
            else if (tag == 0x0132) exif.put("DateTime", value);
            // Add more
            ifdOffset += 12;
        }
        return exif;
    }

    public void printProperties() {
        props.forEach((key, value) -> System.out.println(key + ": " + value));
    }

    public void write(String newFilepath) throws IOException {
        // Basic write: copy data
        try (FileOutputStream fos = new FileOutputStream(newFilepath == null ? filepath : newFilepath)) {
            fos.write(data);
        }
        System.out.println("File written to " + (newFilepath == null ? filepath : newFilepath));
    }

    // Example usage:
    // public static void main(String[] args) throws IOException {
    //     JPEGHandler handler = new JPEGHandler("example.jpg");
    //     handler.readDecode();
    //     handler.printProperties();
    //     handler.write("modified.jpg");
    // }
}

Note: Write method copies the file; full property modification requires segment rebuilding.

7. JavaScript Class for .JPEG Handling

class JPEGHandler {
  constructor(filepath) {
    this.filepath = filepath; // Note: In Node.js, use fs; here assuming Node with fs.promises
    this.props = {};
    this.data = null;
  }

  async readDecode() {
    const fs = require('fs').promises;
    this.data = await fs.readFile(this.filepath);
    const view = new DataView(this.data.buffer);
    if (view.getUint16(0) !== 0xFFD8) {
      throw new Error('Not a valid JPEG');
    }
    this.props['File Signature'] = '0xFFD8';
    let offset = 2;
    while (offset < this.data.length) {
      if (this.data[offset] !== 0xFF) break;
      const marker = this.data[offset + 1];
      offset += 2;
      if (marker === 0xD9) break;
      if (marker >= 0xD0 && marker <= 0xD7 || marker === 0xD8 || marker === 0xD9) continue;
      const length = view.getUint16(offset);
      const segView = new DataView(this.data.buffer, offset + 2, length - 2);
      this.parseSegment(marker, segView);
      offset += length;
    }
  }

  parseSegment(marker, data) {
    // Same as the HTML JS parseSegment function above, omitted for brevity; copy from there
    // Include parseExif as well
  }

  printProperties() {
    console.log(JSON.stringify(this.props, null, 2));
  }

  async write(newFilepath = this.filepath) {
    const fs = require('fs').promises;
    await fs.writeFile(newFilepath, this.data);
    console.log(`File written to ${newFilepath}`);
  }
}

// Example usage (Node.js):
// const handler = new JPEGHandler('example.jpg');
// await handler.readDecode();
// handler.printProperties();
// await handler.write('modified.jpg');

Note: Requires Node.js for file I/O. Parse logic is identical to the HTML script; full implementation would copy the functions.

8. C++ Class for .JPEG Handling

#include <iostream>
#include <fstream>
#include <vector>
#include <map>
#include <string>
#include <cstdint>
#include <iomanip>

class JPEGHandler {
private:
    std::string filepath;
    std::map<std::string, std::string> props; // Simplified to string values for print
    std::vector<uint8_t> data;

    uint16_t getUint16(const uint8_t* ptr, bool bigEndian = true) {
        if (bigEndian) return (ptr[0] << 8) | ptr[1];
        return (ptr[1] << 8) | ptr[0];
    }

    uint32_t getUint32(const uint8_t* ptr, bool bigEndian = true) {
        if (bigEndian) return (ptr[0] << 24) | (ptr[1] << 16) | (ptr[2] << 8) | ptr[3];
        return (ptr[3] << 24) | (ptr[2] << 16) | (ptr[1] << 8) | ptr[0];
    }

    void parseSegment(uint8_t marker, const uint8_t* segData, size_t length) {
        if (marker == 0xE0) { // APP0 JFIF
            if (std::string(reinterpret_cast<const char*>(segData), 5) == "JFIF\0") {
                props["JFIF Version"] = std::to_string(segData[5]) + "." + std::to_string(segData[6]);
                props["Density Units"] = std::to_string(segData[7]);
                props["X Density"] = std::to_string(getUint16(segData + 8));
                props["Y Density"] = std::to_string(getUint16(segData + 10));
                props["Thumbnail Width"] = std::to_string(segData[12]);
                props["Thumbnail Height"] = std::to_string(segData[13]);
            }
        } else if (marker >= 0xC0 && marker <= 0xCF && marker != 0xC4 && marker != 0xC8 && marker != 0xCC) { // SOF
            props["Sample Precision"] = std::to_string(segData[0]);
            props["Image Height"] = std::to_string(getUint16(segData + 1));
            props["Image Width"] = std::to_string(getUint16(segData + 3));
            int nf = segData[5];
            props["Number of Components"] = std::to_string(nf);
            // Component details as string for simplicity
            std::string compStr;
            const uint8_t* off = segData + 6;
            for (int i = 0; i < nf; ++i) {
                compStr += "ID:" + std::to_string(off[0]) + " H:" + std::to_string(off[1] >> 4) + " V:" + std::to_string(off[1] & 0x0F) + " QID:" + std::to_string(off[2]) + "; ";
                off += 3;
            }
            props["Component Details"] = compStr;
        } else if (marker == 0xDB) { // DQT
            std::string qtStr;
            size_t off = 0;
            while (off < length) {
                uint8_t pq = segData[off];
                int p = pq >> 4;
                int qid = pq & 0x0F;
                qtStr += "ID:" + std::to_string(qid) + " Precision:" + std::to_string(p ? 16 : 8) + " Values:[";
                int elemSize = p + 1;
                for (int i = 0; i < 64; ++i) {
                    uint16_t val = elemSize == 1 ? segData[off + 1 + i] : getUint16(segData + off + 1 + i * 2);
                    qtStr += std::to_string(val) + ",";
                }
                qtStr += "]; ";
                off += 1 + 64 * elemSize;
            }
            props["Quantization Tables"] = qtStr;
        } else if (marker == 0xC4) { // DHT
            std::string htStr;
            size_t off = 0;
            while (off < length) {
                uint8_t tc = segData[off];
                std::string classType = (tc >> 4) ? "AC" : "DC";
                int hid = tc & 0x0F;
                htStr += "Class:" + classType + " ID:" + std::to_string(hid) + " Counts:[";
                for (int i = 1; i <= 16; ++i) htStr += std::to_string(segData[off + i]) + ",";
                int totalCodes = 0;
                for (int i = 1; i <= 16; ++i) totalCodes += segData[off + i];
                htStr += "] Values:[";
                for (int i = 0; i < totalCodes; ++i) htStr += std::to_string(segData[off + 17 + i]) + ",";
                htStr += "]; ";
                off += 17 + totalCodes;
            }
            props["Huffman Tables"] = htStr;
        } else if (marker == 0xDD) { // DRI
            props["Restart Interval"] = std::to_string(getUint16(segData));
        } else if (marker == 0xFE) { // COM
            std::string comment = std::string(reinterpret_cast<const char*>(segData), length);
            props["Comments"] = (props.count("Comments") ? props["Comments"] + "; " : "") + comment;
        } else if (marker == 0xE1) { // APP1 EXIF
            if (std::string(reinterpret_cast<const char*>(segData), 6) == "Exif\0\0") {
                props["EXIF Tags"] = parseExif(segData + 6, length - 6);
            }
        }
    }

    std::string parseExif(const uint8_t* exData, size_t length) {
        std::string exifStr;
        bool littleEndian = std::string(reinterpret_cast<const char*>(exData), 2) == "II";
        uint16_t tiffTag = getUint16(exData + 2, !littleEndian);
        if (tiffTag != 0x002A) return "Invalid EXIF";
        uint32_t ifdOffset = getUint32(exData + 4, !littleEndian);
        uint16_t numEntries = getUint16(exData + ifdOffset, !littleEndian);
        ifdOffset += 2;
        for (uint16_t i = 0; i < numEntries; ++i) {
            uint16_t tag = getUint16(exData + ifdOffset, !littleEndian);
            uint16_t type = getUint16(exData + ifdOffset + 2, !littleEndian);
            uint32_t count = getUint32(exData + ifdOffset + 4, !littleEndian);
            uint32_t valOffset = getUint32(exData + ifdOffset + 8, !littleEndian);
            std::string value;
            if (type == 2) { // ASCII
                value = std::string(reinterpret_cast<const char*>(exData + valOffset), count - 1);
            } else if (type == 3) { // Short
                value = std::to_string(getUint16(exData + ifdOffset + 8, !littleEndian));
            } else if (type == 5) { // Rational
                uint32_t num = getUint32(exData + valOffset, !littleEndian);
                uint32_t den = getUint32(exData + valOffset + 4, !littleEndian);
                value = std::to_string(static_cast<double>(num) / den);
            }
            if (tag == 0x010F) exifStr += "Make:" + value + "; ";
            else if (tag == 0x0110) exifStr += "Model:" + value + "; ";
            else if (tag == 0x0112) exifStr += "Orientation:" + value + "; ";
            else if (tag == 0x0132) exifStr += "DateTime:" + value + "; ";
            // Add more
            ifdOffset += 12;
        }
        return exifStr;
    }

public:
    JPEGHandler(const std::string& fp) : filepath(fp) {}

    void readDecode() {
        std::ifstream file(filepath, 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);
        data.resize(size);
        file.read(reinterpret_cast<char*>(data.data()), size);
        if (getUint16(data.data()) != 0xFFD8) throw std::runtime_error("Not a valid JPEG");
        props["File Signature"] = "0xFFD8";
        size_t offset = 2;
        while (offset < size) {
            if (data[offset] != 0xFF) break;
            uint8_t marker = data[offset + 1];
            offset += 2;
            if (marker == 0xD9) break;
            if (marker >= 0xD0 && marker <= 0xD7 || marker == 0xD8 || marker == 0xD9) continue;
            uint16_t length = getUint16(data.data() + offset);
            parseSegment(marker, data.data() + offset + 2, length - 2);
            offset += length;
        }
    }

    void printProperties() {
        for (const auto& p : props) {
            std::cout << p.first << ": " << p.second << std::endl;
        }
    }

    void write(const std::string& newFilepath = "") {
        std::string outPath = newFilepath.empty() ? filepath : newFilepath;
        std::ofstream out(outPath, std::ios::binary);
        out.write(reinterpret_cast<const char*>(data.data()), data.size());
        std::cout << "File written to " << outPath << std::endl;
    }
};

// Example usage:
// int main() {
//     JPEGHandler handler("example.jpg");
//     handler.readDecode();
//     handler.printProperties();
//     handler.write("modified.jpg");
//     return 0;
// }

Note: Properties are stringified for simplicity; write copies the file. Full modification would require rebuilding the byte stream.