Task 332: .JPG File Format

Task 332: .JPG File Format

File Format Specifications for .JPG (JPEG)

The .JPG file format, commonly known as JPEG (Joint Photographic Experts Group), is defined by the ISO/IEC 10918-1 standard (also known as ITU-T.81). It specifies a lossy (and optionally lossless) compression method for digital images using discrete cosine transform (DCT) or predictive coding. The file structure is a sequence of binary segments, each starting with a marker (0xFF followed by a type byte). Common variants include JFIF (JPEG File Interchange Format) for basic interchange and Exif for metadata-rich files (e.g., from cameras). JPEG supports baseline sequential, progressive, hierarchical, and lossless modes, with color spaces like YCbCr, grayscale, or CMYK. Files typically start with SOI (0xFFD8) and end with EOI (0xFFD9), containing headers for dimensions, tables for quantization and entropy coding, and compressed data.

List of All Properties Intrinsic to the .JPG File Format

Based on the JPEG standard, the following are the key extractable properties from the file's binary structure (segments and markers). These are intrinsic to the format itself, not external file system metadata like timestamps or permissions. Properties are grouped by segment/marker for clarity:

  • Start of Image (SOI, 0xFFD8): Presence (boolean, always true for valid files).
  • Frame Header (SOF_n markers, e.g., 0xFFC0 for baseline):
  • Frame type (e.g., baseline DCT, progressive, lossless).
  • Sample precision (bits per sample, typically 8, range 2-16).
  • Image height (lines, 16-bit unsigned, 1-65535).
  • Image width (samples per line, 16-bit unsigned, 1-65535).
  • Number of color components (1-255, typically 1 for grayscale, 3 for YCbCr).
  • For each component: Component ID (unique 8-bit), Horizontal sampling factor (1-4), Vertical sampling factor (1-4), Quantization table ID (0-3).
  • Quantization Tables (DQT, 0xFFDB):
  • For each table (up to 4): Table ID (0-3), Precision (0 for 8-bit, 1 for 16-bit), 64 quantization values (8 or 16-bit each, zigzag order).
  • Huffman Tables (DHT, 0xFFC4):
  • For each table (up to 4 DC and 4 AC): Table class (0=DC, 1=AC), Table ID (0-3), 16 code lengths (counts of codes per bit length 1-16), Variable-length symbols (Huffman codes' values).
  • Arithmetic Conditioning Tables (DAC, 0xFFCC): For each table: Conditioning table ID, Lower/Upper bound values (rarely used in common files).
  • Restart Interval (DRI, 0xFFDD): Interval value (16-bit, number of MCUs between restarts, 0 if no restarts).
  • Scan Header (SOS, 0xFFDA):
  • Number of components in scan (1-4).
  • For each scan component: Selector (matches component ID), DC entropy table ID (0-3), AC entropy table ID (0-3).
  • Spectral selection start (0-63 for DCT).
  • Spectral selection end (0-63 for DCT).
  • Successive approximation bit high (0-13).
  • Successive approximation bit low (0-13).
  • Restart Markers (RST_n, 0xFFD0-0xFFD7): Cycle count (0-7, for error resilience).
  • Comment (COM, 0xFFFE): Arbitrary text string (variable length).
  • Application Segments (APP_n, 0xFFE0-0xFFEF):
  • JFIF (APP0 "JFIF"): Version (major/minor, e.g., 1.02), Units (0=no units/aspect ratio, 1=dpi, 2=dpcm), X density (horizontal, 16-bit), Y density (vertical, 16-bit), Thumbnail width (8-bit), Thumbnail height (8-bit), Thumbnail RGB data (3 bytes per pixel if present).
  • JFXX Extension (APP0 "JFXX"): Extension code (0x10=JPEG thumbnail, 0x11=1-byte/pixel palette thumbnail, 0x13=3-byte/pixel RGB thumbnail), Extension data (variable, e.g., compressed thumbnail or palette).
  • Exif (APP1 "Exif"): TIFF-based metadata including camera model, exposure, orientation, GPS (structure too complex for full list here, but extractable as key-value pairs).
  • Other APP_n: Vendor-specific data (e.g., Adobe APP14 for color space), ICC profiles, or thumbnails.
  • Hierarchical Markers (e.g., DHT for hierarchical, EXP 0xFFDF): Expansion factors (rare in standard files).
  • End of Image (EOI, 0xFFD9): Presence (boolean).
  • General Properties: Compression mode (inferred from markers, e.g., progressive if multiple scans), Color space (inferred from components and APP segments, e.g., YCbCr), Entropy coding type (Huffman or arithmetic), Byte stuffing presence in data.

These properties define the image's decoding parameters, metadata, and compressed data layout.

Two Direct Download Links for .JPG Files

Ghost Blog Embedded HTML JavaScript for Drag-and-Drop .JPG Property Dump

Here's a self-contained HTML snippet with JavaScript that can be embedded in a Ghost blog post (or any HTML-enabled blog). It creates a drag-and-drop area. When a .JPG file is dropped, it reads the file as binary, parses the JPEG markers, extracts the properties from the list above, and dumps them to the screen in a readable format. It handles basic validation and displays errors if the file isn't a valid JPEG.

Drag and drop a .JPG file here

Python Class for .JPG Handling

Here's a Python class JPEGHandler that can open a .JPG file, decode (parse) its structure, read and print all properties to console, and write (re-encode) the file (basic copy for now, as full encoding from scratch is complex; it parses and rewrites the segments).

import struct
import sys

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

    def open(self):
        with open(self.filepath, 'rb') as f:
            self.data = f.read()

    def decode(self):
        if self.data is None:
            raise ValueError("File not opened")
        offset = 0
        if self.data[offset:offset+2] != b'\xFF\xD8':
            raise ValueError("Invalid JPEG: Missing SOI")
        self.properties['SOI'] = 'Present'
        offset += 2

        while offset < len(self.data):
            if self.data[offset] != 0xFF:
                break
            marker = self.data[offset + 1]
            offset += 2
            if marker == 0xD9:
                self.properties['EOI'] = 'Present'
                break

            if marker in [0xD0, 0xD1, 0xD2, 0xD3, 0xD4, 0xD5, 0xD6, 0xD7]:  # RST
                self.properties.setdefault('Restart Markers', []).append(marker - 0xD0)
                continue

            length = struct.unpack('>H', self.data[offset:offset+2])[0]
            offset += 2

            if marker in range(0xC0, 0xD0) and marker not in [0xC4, 0xC8, 0xCC]:  # SOF
                frame_type = self.get_frame_type(marker)
                self.properties['Frame Type'] = frame_type
                precision, = struct.unpack('>B', self.data[offset:offset+1])
                offset += 1
                height, = struct.unpack('>H', self.data[offset:offset+2])
                offset += 2
                width, = struct.unpack('>H', self.data[offset:offset+2])
                offset += 2
                components, = struct.unpack('>B', self.data[offset:offset+1])
                offset += 1
                self.properties['Sample Precision'] = precision
                self.properties['Height'] = height
                self.properties['Width'] = width
                self.properties['Components'] = components
                comp_list = []
                for i in range(components):
                    id_, sampling, qtable = struct.unpack('>BBB', self.data[offset:offset+3])
                    h = sampling >> 4
                    v = sampling & 0x0F
                    comp_list.append({'ID': id_, 'H': h, 'V': v, 'Quant Table': qtable})
                    offset += 3
                self.properties['Component Details'] = comp_list
            elif marker == 0xDB:  # DQT
                qt_list = []
                end = offset + length - 2
                while offset < end:
                    info, = struct.unpack('>B', self.data[offset:offset+1])
                    offset += 1
                    prec = 16 if info >> 4 else 8
                    id_ = info & 0x0F
                    values = []
                    for _ in range(64):
                        if prec == 16:
                            val, = struct.unpack('>H', self.data[offset:offset+2])
                            offset += 2
                        else:
                            val, = struct.unpack('>B', self.data[offset:offset+1])
                            offset += 1
                        values.append(val)
                    qt_list.append({'ID': id_, 'Precision': prec, 'Values': values})
                self.properties['Quantization Tables'] = qt_list
            elif marker == 0xC4:  # DHT
                ht_list = []
                end = offset + length - 2
                while offset < end:
                    info, = struct.unpack('>B', self.data[offset:offset+1])
                    offset += 1
                    class_ = 'AC' if info >> 4 else 'DC'
                    id_ = info & 0x0F
                    lengths = list(struct.unpack('>16B', self.data[offset:offset+16]))
                    offset += 16
                    total_sym = sum(lengths)
                    symbols = list(struct.unpack(f'>{total_sym}B', self.data[offset:offset+total_sym]))
                    offset += total_sym
                    ht_list.append({'Class': class_, 'ID': id_, 'Code Lengths': lengths, 'Symbols': symbols})
                self.properties['Huffman Tables'] = ht_list
            elif marker == 0xDD:  # DRI
                interval, = struct.unpack('>H', self.data[offset:offset+2])
                self.properties['Restart Interval'] = interval
                offset += 2
            elif marker == 0xDA:  # SOS
                comps, = struct.unpack('>B', self.data[offset:offset+1])
                offset += 1
                scan_comps = []
                for _ in range(comps):
                    selector, tables = struct.unpack('>BB', self.data[offset:offset+2])
                    dc = tables >> 4
                    ac = tables & 0x0F
                    scan_comps.append({'Selector': selector, 'DC Table': dc, 'AC Table': ac})
                    offset += 2
                ss, se, approx = struct.unpack('>BBB', self.data[offset:offset+3])
                ah = approx >> 4
                al = approx & 0x0F
                self.properties['Scan Header'] = {
                    'Components in Scan': comps,
                    'Component Details': scan_comps,
                    'Spectral Start': ss,
                    'Spectral End': se,
                    'Approx High': ah,
                    'Approx Low': al
                }
                offset += 3
                # Skip entropy data until next marker
                while offset < len(self.data) and (self.data[offset] != 0xFF or self.data[offset+1] == 0x00):
                    offset += 1
                if self.data[offset] == 0xFF and self.data[offset+1] != 0x00:
                    offset -= 2  # Backtrack for next marker loop
            elif marker == 0xFE:  # COM
                comment = self.data[offset:offset + length - 2].decode('utf-8', errors='ignore')
                self.properties['Comment'] = comment
            elif marker in range(0xE0, 0xF0):  # APPn
                app_data = self.data[offset:offset+5].decode('ascii', errors='ignore')
                if app_data == 'JFIF\x00':
                    major, minor, units = struct.unpack('>BBB', self.data[offset+5:offset+8])
                    xdens, = struct.unpack('>H', self.data[offset+8:offset+10])
                    ydens, = struct.unpack('>H', self.data[offset+10:offset+12])
                    thumb_w, thumb_h = struct.unpack('>BB', self.data[offset+12:offset+14])
                    self.properties['JFIF'] = {
                        'Version': f'{major}.{minor}',
                        'Units': units,
                        'X Density': xdens,
                        'Y Density': ydens,
                        'Thumbnail Width': thumb_w,
                        'Thumbnail Height': thumb_h
                    }
                elif app_data == 'JFXX\x00':
                    ext_code, = struct.unpack('>B', self.data[offset+5:offset+6])
                    self.properties['JFXX'] = {'Extension Code': hex(ext_code)}
                else:
                    self.properties[f'APP{marker - 0xE0}'] = app_data
            offset += length - 2

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

    def write(self, output_path):
        if self.data is None:
            raise ValueError("No data to write")
        with open(output_path, 'wb') as f:
            f.write(self.data)  # Basic write; extend for modifications

    @staticmethod
    def get_frame_type(marker):
        types = {0xC0: 'Baseline DCT', 0xC1: 'Extended Sequential DCT', 0xC2: 'Progressive DCT', 0xC3: 'Lossless', 0xC5: 'Diff Sequential DCT', 0xC6: 'Diff Progressive DCT', 0xC7: 'Diff Lossless'}
        return types.get(marker, 'Unknown')

# Example usage:
# handler = JPEGHandler('example.jpg')
# handler.open()
# handler.decode()
# handler.print_properties()
# handler.write('output.jpg')

Java Class for .JPG Handling

Here's a Java class JPEGHandler that opens a .JPG file, decodes its structure, reads and prints properties to console, and writes the file (basic copy).

import java.io.*;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;

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

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

    public void open() throws IOException {
        try (FileInputStream fis = new FileInputStream(filepath)) {
            data = fis.readAllBytes();
        }
    }

    public void decode() {
        if (data == null) throw new IllegalStateException("File not opened");
        int offset = 0;
        if (data[offset] != (byte)0xFF || data[offset+1] != (byte)0xD8) {
            throw new IllegalStateException("Invalid JPEG: Missing SOI");
        }
        properties.put("SOI", "Present");
        offset += 2;

        while (offset < data.length) {
            if ((data[offset] & 0xFF) != 0xFF) break;
            int marker = data[offset + 1] & 0xFF;
            offset += 2;
            if (marker == 0xD9) {
                properties.put("EOI", "Present");
                break;
            }

            if (marker >= 0xD0 && marker <= 0xD7) { // RST
                java.util.List<Integer> rsts = (java.util.List<Integer>) properties.getOrDefault("Restart Markers", new java.util.ArrayList<>());
                rsts.add(marker - 0xD0);
                properties.put("Restart Markers", rsts);
                continue;
            }

            ByteBuffer bb = ByteBuffer.wrap(data, offset, data.length - offset).order(ByteOrder.BIG_ENDIAN);
            int length = bb.getShort() & 0xFFFF;
            offset += 2;

            if (marker >= 0xC0 && marker <= 0xCF && marker != 0xC4 && marker != 0xC8 && marker != 0xCC) { // SOF
                properties.put("Frame Type", getFrameType(marker));
                int precision = bb.get() & 0xFF;
                int height = bb.getShort() & 0xFFFF;
                int width = bb.getShort() & 0xFFFF;
                int components = bb.get() & 0xFF;
                properties.put("Sample Precision", precision);
                properties.put("Height", height);
                properties.put("Width", width);
                properties.put("Components", components);
                java.util.List<java.util.Map<String, Integer>> compList = new java.util.ArrayList<>();
                for (int i = 0; i < components; i++) {
                    int id = bb.get() & 0xFF;
                    int sampling = bb.get() & 0xFF;
                    int h = sampling >> 4;
                    int v = sampling & 0x0F;
                    int qTable = bb.get() & 0xFF;
                    java.util.Map<String, Integer> comp = new java.util.HashMap<>();
                    comp.put("ID", id);
                    comp.put("H", h);
                    comp.put("V", v);
                    comp.put("Quant Table", qTable);
                    compList.add(comp);
                }
                properties.put("Component Details", compList);
                offset += length - 2;
                continue;
            } else if (marker == 0xDB) { // DQT
                java.util.List<java.util.Map<String, Object>> qtList = new java.util.ArrayList<>();
                int end = offset + length - 2;
                while (offset < end) {
                    int info = bb.get() & 0xFF;
                    int prec = (info >> 4) == 1 ? 16 : 8;
                    int id = info & 0x0F;
                    int[] values = new int[64];
                    for (int i = 0; i < 64; i++) {
                        values[i] = prec == 16 ? bb.getShort() & 0xFFFF : bb.get() & 0xFF;
                    }
                    java.util.Map<String, Object> qt = new java.util.HashMap<>();
                    qt.put("ID", id);
                    qt.put("Precision", prec);
                    qt.put("Values", values);
                    qtList.add(qt);
                    offset = bb.position();
                }
                properties.put("Quantization Tables", qtList);
            } else if (marker == 0xC4) { // DHT
                java.util.List<java.util.Map<String, Object>> htList = new java.util.ArrayList<>();
                int end = offset + length - 2;
                while (offset < end) {
                    int info = bb.get() & 0xFF;
                    String classType = (info >> 4) == 1 ? "AC" : "DC";
                    int id = info & 0x0F;
                    int[] lengths = new int[16];
                    int totalSym = 0;
                    for (int i = 0; i < 16; i++) {
                        lengths[i] = bb.get() & 0xFF;
                        totalSym += lengths[i];
                    }
                    int[] symbols = new int[totalSym];
                    for (int i = 0; i < totalSym; i++) {
                        symbols[i] = bb.get() & 0xFF;
                    }
                    java.util.Map<String, Object> ht = new java.util.HashMap<>();
                    ht.put("Class", classType);
                    ht.put("ID", id);
                    ht.put("Code Lengths", lengths);
                    ht.put("Symbols", symbols);
                    htList.add(ht);
                    offset = bb.position();
                }
                properties.put("Huffman Tables", htList);
            } else if (marker == 0xDD) { // DRI
                int interval = bb.getShort() & 0xFFFF;
                properties.put("Restart Interval", interval);
            } else if (marker == 0xDA) { // SOS
                int comps = bb.get() & 0xFF;
                java.util.List<java.util.Map<String, Integer>> scanComps = new java.util.ArrayList<>();
                for (int i = 0; i < comps; i++) {
                    int selector = bb.get() & 0xFF;
                    int tables = bb.get() & 0xFF;
                    int dc = tables >> 4;
                    int ac = tables & 0x0F;
                    java.util.Map<String, Integer> sc = new java.util.HashMap<>();
                    sc.put("Selector", selector);
                    sc.put("DC Table", dc);
                    sc.put("AC Table", ac);
                    scanComps.add(sc);
                }
                int ss = bb.get() & 0xFF;
                int se = bb.get() & 0xFF;
                int approx = bb.get() & 0xFF;
                int ah = approx >> 4;
                int al = approx & 0x0F;
                java.util.Map<String, Object> scanHeader = new java.util.HashMap<>();
                scanHeader.put("Components in Scan", comps);
                scanHeader.put("Component Details", scanComps);
                scanHeader.put("Spectral Start", ss);
                scanHeader.put("Spectral End", se);
                scanHeader.put("Approx High", ah);
                scanHeader.put("Approx Low", al);
                properties.put("Scan Header", scanHeader);
                offset = bb.position();
                // Skip entropy data
                while (offset < data.length && ( (data[offset] & 0xFF) != 0xFF || (data[offset+1] & 0xFF) == 0x00 )) {
                    offset++;
                }
                if (offset < data.length && (data[offset] & 0xFF) == 0xFF) offset -= 2; // Backtrack
                continue;
            } else if (marker == 0xFE) { // COM
                String comment = new String(data, offset, length - 2);
                properties.put("Comment", comment);
            } else if (marker >= 0xE0 && marker <= 0xEF) { // APPn
                String appData = new String(data, offset, 5);
                if (appData.equals("JFIF\0")) {
                    int major = data[offset+5] & 0xFF;
                    int minor = data[offset+6] & 0xFF;
                    int units = data[offset+7] & 0xFF;
                    int xDensity = ((data[offset+8] & 0xFF) << 8) | (data[offset+9] & 0xFF);
                    int yDensity = ((data[offset+10] & 0xFF) << 8) | (data[offset+11] & 0xFF);
                    int thumbW = data[offset+12] & 0xFF;
                    int thumbH = data[offset+13] & 0xFF;
                    java.util.Map<String, Object> jfif = new java.util.HashMap<>();
                    jfif.put("Version", major + "." + minor);
                    jfif.put("Units", units);
                    jfif.put("X Density", xDensity);
                    jfif.put("Y Density", yDensity);
                    jfif.put("Thumbnail Width", thumbW);
                    jfif.put("Thumbnail Height", thumbH);
                    properties.put("JFIF", jfif);
                } else if (appData.equals("JFXX\0")) {
                    int extCode = data[offset+5] & 0xFF;
                    java.util.Map<String, Integer> jfxx = new java.util.HashMap<>();
                    jfxx.put("Extension Code", extCode);
                    properties.put("JFXX", jfxx);
                } else {
                    properties.put("APP" + (marker - 0xE0), appData);
                }
            }
            offset += length - 2;
        }
    }

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

    public void write(String outputPath) throws IOException {
        if (data == null) throw new IllegalStateException("No data to write");
        try (FileOutputStream fos = new FileOutputStream(outputPath)) {
            fos.write(data);
        }
    }

    private String getFrameType(int marker) {
        switch (marker) {
            case 0xC0: return "Baseline DCT";
            case 0xC1: return "Extended Sequential DCT";
            case 0xC2: return "Progressive DCT";
            case 0xC3: return "Lossless";
            case 0xC5: return "Diff Sequential DCT";
            case 0xC6: return "Diff Progressive DCT";
            case 0xC7: return "Diff Lossless";
            default: return "Unknown";
        }
    }

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

JavaScript Class for .JPG Handling

Here's a JavaScript class JPEGHandler for Node.js (using fs for file I/O). It opens, decodes, reads/prints properties to console, and writes the file.

const fs = require('fs');

class JPEGHandler {
    constructor(filepath) {
        this.filepath = filepath;
        this.data = null;
        this.properties = {};
    }

    open() {
        this.data = fs.readFileSync(this.filepath);
    }

    decode() {
        if (!this.data) throw new Error('File not opened');
        let offset = 0;
        if (this.data[offset] !== 0xFF || this.data[offset + 1] !== 0xD8) {
            throw new Error('Invalid JPEG: Missing SOI');
        }
        this.properties.SOI = 'Present';
        offset += 2;

        while (offset < this.data.length) {
            if (this.data[offset] !== 0xFF) break;
            const marker = this.data[offset + 1];
            offset += 2;
            if (marker === 0xD9) {
                this.properties.EOI = 'Present';
                break;
            }

            if (marker >= 0xD0 && marker <= 0xD7) { // RST
                this.properties.RestartMarkers = this.properties.RestartMarkers || [];
                this.properties.RestartMarkers.push(marker - 0xD0);
                continue;
            }

            let length = (this.data[offset] << 8) | this.data[offset + 1];
            offset += 2;

            if (marker >= 0xC0 && marker <= 0xCF && marker !== 0xC4 && marker !== 0xC8 && marker !== 0xCC) { // SOF
                this.properties['Frame Type'] = this.getFrameType(marker);
                const precision = this.data[offset++];
                const height = (this.data[offset] << 8) | this.data[offset + 1]; offset += 2;
                const width = (this.data[offset] << 8) | this.data[offset + 1]; offset += 2;
                const components = this.data[offset++];
                this.properties['Sample Precision'] = precision;
                this.properties.Height = height;
                this.properties.Width = width;
                this.properties.Components = components;
                this.properties['Component Details'] = [];
                for (let i = 0; i < components; i++) {
                    const id = this.data[offset++];
                    const sampling = this.data[offset++];
                    const h = sampling >> 4, v = sampling & 0x0F;
                    const qTable = this.data[offset++];
                    this.properties['Component Details'].push({ID: id, H: h, V: v, 'Quant Table': qTable});
                }
            } else if (marker === 0xDB) { // DQT
                this.properties['Quantization Tables'] = [];
                const end = offset + length - 2;
                while (offset < end) {
                    const info = this.data[offset++];
                    const prec = info >> 4 ? 16 : 8;
                    const id = info & 0x0F;
                    const values = [];
                    for (let i = 0; i < 64; i++) {
                        let val;
                        if (prec === 16) {
                            val = (this.data[offset] << 8) | this.data[offset + 1];
                            offset += 2;
                        } else {
                            val = this.data[offset++];
                        }
                        values.push(val);
                    }
                    this.properties['Quantization Tables'].push({ID: id, Precision: prec, Values: values});
                }
            } else if (marker === 0xC4) { // DHT
                this.properties['Huffman Tables'] = [];
                const end = offset + length - 2;
                while (offset < end) {
                    const info = this.data[offset++];
                    const classType = info >> 4 ? 'AC' : 'DC';
                    const id = info & 0x0F;
                    const lengths = [];
                    let totalSym = 0;
                    for (let i = 0; i < 16; i++) {
                        const count = this.data[offset++];
                        lengths.push(count);
                        totalSym += count;
                    }
                    const symbols = [];
                    for (let i = 0; i < totalSym; i++) {
                        symbols.push(this.data[offset++]);
                    }
                    this.properties['Huffman Tables'].push({Class: classType, ID: id, 'Code Lengths': lengths, Symbols: symbols});
                }
            } else if (marker === 0xDD) { // DRI
                const interval = (this.data[offset] << 8) | this.data[offset + 1];
                this.properties['Restart Interval'] = interval;
                offset += 2;
            } else if (marker === 0xDA) { // SOS
                const comps = this.data[offset++];
                this.properties['Scan Header'] = { 'Components in Scan': comps, 'Component Details': [] };
                for (let i = 0; i < comps; i++) {
                    const selector = this.data[offset++];
                    const tables = this.data[offset++];
                    const dc = tables >> 4, ac = tables & 0x0F;
                    this.properties['Scan Header']['Component Details'].push({Selector: selector, 'DC Table': dc, 'AC Table': ac});
                }
                const ss = this.data[offset++], se = this.data[offset++];
                const approx = this.data[offset++];
                const ah = approx >> 4, al = approx & 0x0F;
                this.properties['Scan Header']['Spectral Start'] = ss;
                this.properties['Scan Header']['Spectral End'] = se;
                this.properties['Scan Header']['Approx High'] = ah;
                this.properties['Scan Header']['Approx Low'] = al;
                // Skip entropy
                while (offset < this.data.length && (this.data[offset] !== 0xFF || this.data[offset + 1] === 0x00)) offset++;
                if (this.data[offset] === 0xFF && this.data[offset + 1] !== 0x00) offset -= 2;
            } else if (marker === 0xFE) { // COM
                const comment = this.data.slice(offset, offset + length - 2).toString('utf-8');
                this.properties.Comment = comment;
            } else if (marker >= 0xE0 && marker <= 0xEF) { // APPn
                const appData = this.data.slice(offset, offset + 5).toString('ascii');
                if (appData === 'JFIF\0') {
                    const major = this.data[offset + 5];
                    const minor = this.data[offset + 6];
                    const units = this.data[offset + 7];
                    const xDensity = (this.data[offset + 8] << 8) | this.data[offset + 9];
                    const yDensity = (this.data[offset + 10] << 8) | this.data[offset + 11];
                    const thumbW = this.data[offset + 12];
                    const thumbH = this.data[offset + 13];
                    this.properties.JFIF = {
                        Version: `${major}.${minor}`,
                        Units: units,
                        'X Density': xDensity,
                        'Y Density': yDensity,
                        'Thumbnail Width': thumbW,
                        'Thumbnail Height': thumbH
                    };
                } else if (appData === 'JFXX\0') {
                    const extCode = this.data[offset + 5];
                    this.properties.JFXX = { 'Extension Code': extCode.toString(16) };
                } else {
                    this.properties[`APP${marker - 0xE0}`] = appData;
                }
            }
            offset += length - 2;
        }
    }

    printProperties() {
        console.log(this.properties);
    }

    write(outputPath) {
        if (!this.data) throw new Error('No data to write');
        fs.writeFileSync(outputPath, this.data);
    }

    getFrameType(marker) {
        const types = {
            0xC0: 'Baseline DCT',
            0xC1: 'Extended Sequential DCT',
            0xC2: 'Progressive DCT',
            0xC3: 'Lossless',
            0xC5: 'Diff Sequential DCT',
            0xC6: 'Diff Progressive DCT',
            0xC7: 'Diff Lossless'
        };
        return types[marker] || 'Unknown';
    }
}

// Example usage:
// const handler = new JPEGHandler('example.jpg');
// handler.open();
// handler.decode();
// handler.printProperties();
// handler.write('output.jpg');

C "Class" (Struct with Functions) for .JPG Handling

In C, classes aren't native, so here's a struct JPEGHandler with associated functions. It opens, decodes, reads/prints properties to stdout, and writes the file. Compile with gcc -o jpeg_handler jpeg_handler.c.

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>

typedef struct {
    char* filepath;
    uint8_t* data;
    size_t size;
    // For properties, use a simple array of strings for demo; in real, use dict-like
    char* properties[100]; // Limited, for simplicity
    int prop_count;
} JPEGHandler;

void init_jpeg_handler(JPEGHandler* handler, const char* filepath) {
    handler->filepath = strdup(filepath);
    handler->data = NULL;
    handler->size = 0;
    handler->prop_count = 0;
}

void open_file(JPEGHandler* handler) {
    FILE* f = fopen(handler->filepath, "rb");
    if (!f) {
        perror("open_file");
        exit(1);
    }
    fseek(f, 0, SEEK_END);
    handler->size = ftell(f);
    fseek(f, 0, SEEK_SET);
    handler->data = malloc(handler->size);
    fread(handler->data, 1, handler->size, f);
    fclose(f);
}

void add_property(JPEGHandler* handler, const char* prop) {
    if (handler->prop_count < 100) {
        handler->properties[handler->prop_count++] = strdup(prop);
    }
}

void decode(JPEGHandler* handler) {
    if (!handler->data) {
        fprintf(stderr, "File not opened\n");
        return;
    }
    size_t offset = 0;
    if (handler->data[offset] != 0xFF || handler->data[offset + 1] != 0xD8) {
        fprintf(stderr, "Invalid JPEG: Missing SOI\n");
        return;
    }
    add_property(handler, "SOI: Present");
    offset += 2;

    char buf[256];
    while (offset < handler->size) {
        if (handler->data[offset] != 0xFF) break;
        uint8_t marker = handler->data[offset + 1];
        offset += 2;
        if (marker == 0xD9) {
            add_property(handler, "EOI: Present");
            break;
        }

        if (marker >= 0xD0 && marker <= 0xD7) { // RST
            snprintf(buf, sizeof(buf), "Restart Marker: %d", marker - 0xD0);
            add_property(handler, buf);
            continue;
        }

        uint16_t length = (handler->data[offset] << 8) | handler->data[offset + 1];
        offset += 2;

        if (marker >= 0xC0 && marker <= 0xCF && marker != 0xC4 && marker != 0xC8 && marker != 0xCC) { // SOF
            const char* type = get_frame_type(marker);
            snprintf(buf, sizeof(buf), "Frame Type: %s", type);
            add_property(handler, buf);
            uint8_t precision = handler->data[offset++];
            uint16_t height = (handler->data[offset] << 8) | handler->data[offset + 1]; offset += 2;
            uint16_t width = (handler->data[offset] << 8) | handler->data[offset + 1]; offset += 2;
            uint8_t components = handler->data[offset++];
            snprintf(buf, sizeof(buf), "Sample Precision: %u", precision);
            add_property(handler, buf);
            snprintf(buf, sizeof(buf), "Height: %u", height);
            add_property(handler, buf);
            snprintf(buf, sizeof(buf), "Width: %u", width);
            add_property(handler, buf);
            snprintf(buf, sizeof(buf), "Components: %u", components);
            add_property(handler, buf);
            add_property(handler, "Component Details:");
            for (uint8_t i = 0; i < components; i++) {
                uint8_t id = handler->data[offset++];
                uint8_t sampling = handler->data[offset++];
                uint8_t h = sampling >> 4, v = sampling & 0x0F;
                uint8_t qtable = handler->data[offset++];
                snprintf(buf, sizeof(buf), "  Component %u: ID=%u, H=%u, V=%u, Quant Table=%u", i+1, id, h, v, qtable);
                add_property(handler, buf);
            }
        } else if (marker == 0xDB) { // DQT
            add_property(handler, "Quantization Tables:");
            size_t end = offset + length - 2;
            while (offset < end) {
                uint8_t info = handler->data[offset++];
                uint8_t prec = info >> 4 ? 16 : 8;
                uint8_t id = info & 0x0F;
                snprintf(buf, sizeof(buf), "  Table ID: %u, Precision: %u-bit", id, prec);
                add_property(handler, buf);
                add_property(handler, "    Values:");
                char val_buf[1024] = "";
                for (int i = 0; i < 64; i++) {
                    uint16_t val;
                    if (prec == 16) {
                        val = (handler->data[offset] << 8) | handler->data[offset + 1];
                        offset += 2;
                    } else {
                        val = handler->data[offset++];
                    }
                    char temp[10];
                    snprintf(temp, sizeof(temp), "%u ", val);
                    strcat(val_buf, temp);
                    if (strlen(val_buf) > 900) { // Prevent overflow
                        add_property(handler, val_buf);
                        val_buf[0] = '\0';
                    }
                }
                add_property(handler, val_buf);
            }
        } else if (marker == 0xC4) { // DHT
            add_property(handler, "Huffman Tables:");
            size_t end = offset + length - 2;
            while (offset < end) {
                uint8_t info = handler->data[offset++];
                const char* class_type = (info >> 4) ? "AC" : "DC";
                uint8_t id = info & 0x0F;
                snprintf(buf, sizeof(buf), "  Table: %s ID=%u", class_type, id);
                add_property(handler, buf);
                add_property(handler, "    Code Lengths:");
                char len_buf[256] = "";
                int total_sym = 0;
                for (int i = 0; i < 16; i++) {
                    uint8_t count = handler->data[offset++];
                    char temp[10];
                    snprintf(temp, sizeof(temp), "%u ", count);
                    strcat(len_buf, temp);
                    total_sym += count;
                }
                add_property(handler, len_buf);
                add_property(handler, "    Symbols:");
                char sym_buf[1024] = "";
                for (int i = 0; i < total_sym; i++) {
                    uint8_t sym = handler->data[offset++];
                    char temp[10];
                    snprintf(temp, sizeof(temp), "%u ", sym);
                    strcat(sym_buf, temp);
                    if (strlen(sym_buf) > 900) {
                        add_property(handler, sym_buf);
                        sym_buf[0] = '\0';
                    }
                }
                add_property(handler, sym_buf);
            }
        } else if (marker == 0xDD) { // DRI
            uint16_t interval = (handler->data[offset] << 8) | handler->data[offset + 1];
            snprintf(buf, sizeof(buf), "Restart Interval: %u", interval);
            add_property(handler, buf);
            offset += 2;
        } else if (marker == 0xDA) { // SOS
            add_property(handler, "Scan Header:");
            uint8_t comps = handler->data[offset++];
            snprintf(buf, sizeof(buf), "  Components in Scan: %u", comps);
            add_property(handler, buf);
            for (uint8_t i = 0; i < comps; i++) {
                uint8_t selector = handler->data[offset++];
                uint8_t tables = handler->data[offset++];
                uint8_t dc = tables >> 4, ac = tables & 0x0F;
                snprintf(buf, sizeof(buf), "    Component %u: Selector=%u, DC Table=%u, AC Table=%u", i+1, selector, dc, ac);
                add_property(handler, buf);
            }
            uint8_t ss = handler->data[offset++], se = handler->data[offset++];
            uint8_t approx = handler->data[offset++];
            uint8_t ah = approx >> 4, al = approx & 0x0F;
            snprintf(buf, sizeof(buf), "  Spectral Start: %u, End: %u", ss, se);
            add_property(handler, buf);
            snprintf(buf, sizeof(buf), "  Approx High: %u, Low: %u", ah, al);
            add_property(handler, buf);
            // Skip entropy
            while (offset < handler->size && (handler->data[offset] != 0xFF || handler->data[offset + 1] == 0x00)) offset++;
            if (handler->data[offset] == 0xFF && handler->data[offset + 1] != 0x00) offset -= 2;
        } else if (marker == 0xFE) { // COM
            char* comment = malloc(length - 1);
            strncpy(comment, (const char*)&handler->data[offset], length - 2);
            comment[length - 2] = '\0';
            snprintf(buf, sizeof(buf), "Comment: %s", comment);
            add_property(handler, buf);
            free(comment);
        } else if (marker >= 0xE0 && marker <= 0xEF) { // APPn
            char app_data[6];
            strncpy(app_data, (const char*)&handler->data[offset], 5);
            app_data[5] = '\0';
            if (strcmp(app_data, "JFIF") == 0) { // Note: \0 is next byte
                uint8_t major = handler->data[offset + 5];
                uint8_t minor = handler->data[offset + 6];
                uint8_t units = handler->data[offset + 7];
                uint16_t xdens = (handler->data[offset + 8] << 8) | handler->data[offset + 9];
                uint16_t ydens = (handler->data[offset + 10] << 8) | handler->data[offset + 11];
                uint8_t thumb_w = handler->data[offset + 12];
                uint8_t thumb_h = handler->data[offset + 13];
                snprintf(buf, sizeof(buf), "JFIF: Version %u.%u, Units=%u, XDensity=%u, YDensity=%u, Thumbnail %ux%u", major, minor, units, xdens, ydens, thumb_w, thumb_h);
                add_property(handler, buf);
            } else if (strcmp(app_data, "JFXX") == 0) {
                uint8_t ext_code = handler->data[offset + 5];
                snprintf(buf, sizeof(buf), "JFXX Extension: Code 0x%02X", ext_code);
                add_property(handler, buf);
            } else {
                snprintf(buf, sizeof(buf), "APP%u: %s", marker - 0xE0, app_data);
                add_property(handler, buf);
            }
        }
        offset += length - 2;
    }
}

void print_properties(const JPEGHandler* handler) {
    for (int i = 0; i < handler->prop_count; i++) {
        printf("%s\n", handler->properties[i]);
    }
}

void write_file(const JPEGHandler* handler, const char* output_path) {
    if (!handler->data) {
        fprintf(stderr, "No data to write\n");
        return;
    }
    FILE* f = fopen(output_path, "wb");
    if (!f) {
        perror("write_file");
        return;
    }
    fwrite(handler->data, 1, handler->size, f);
    fclose(f);
}

void free_handler(JPEGHandler* handler) {
    free(handler->data);
    free(handler->filepath);
    for (int i = 0; i < handler->prop_count; i++) {
        free(handler->properties[i]);
    }
}

const char* get_frame_type(uint8_t marker) {
    switch (marker) {
        case 0xC0: return "Baseline DCT";
        case 0xC1: return "Extended Sequential DCT";
        case 0xC2: return "Progressive DCT";
        case 0xC3: return "Lossless";
        case 0xC5: return "Diff Sequential DCT";
        case 0xC6: return "Diff Progressive DCT";
        case 0xC7: return "Diff Lossless";
        default: return "Unknown";
    }
}

// Example usage:
// int main() {
//     JPEGHandler handler;
//     init_jpeg_handler(&handler, "example.jpg");
//     open_file(&handler);
//     decode(&handler);
//     print_properties(&handler);
//     write_file(&handler, "output.jpg");
//     free_handler(&handler);
//     return 0;
// }