Task 445: .NEF File Format

Task 445: .NEF File Format

Nikon NEF File Format Specifications

The .NEF (Nikon Electronic Format) is Nikon's proprietary RAW image file format, based on the TIFF 6.0 specification with extensions for RAW data storage. It captures unprocessed sensor data from Nikon cameras, typically in 12-bit or 14-bit depth, and includes metadata in EXIF and MakerNote sections. The format supports lossless or lossy compression for RAW data and embeds thumbnails and preview images.

The structure follows TIFF conventions:

  • Header: 8-byte TIFF header indicating byte order (usually big-endian "MM"), magic number (42), and offset to the first Image File Directory (IFD).
  • IFD0 (Main IFD): Contains general metadata, a small thumbnail (160x120 uncompressed TIFF), and pointers to SubIFDs and EXIF IFD.
  • EXIF IFD: Standard EXIF metadata, including a pointer to the Nikon MakerNote IFD.
  • MakerNote IFD: Nikon-specific tags, some encrypted (e.g., ColorBalance for versions >= 200), using camera serial number and shutter count as keys.
  • SubIFDs (via tag 0x014a in IFD0): Typically two – one for a lossy JPEG preview (full resolution, absent in some models like D100), and one for the RAW data (compressed or uncompressed CFA).
  • Image Data: RAW sensor data (CFA pattern, e.g., RGGB), preview JPEG, and thumbnails.
  • Compression: RAW can be uncompressed (1), Nikon NEF Compressed (34713 – lossless or lossy types), or other variants defined in MakerNote tag 0x0093.
  • Encryption: Applied to certain MakerNote tags; decryption involves XOR with a key derived from serial number, shutter count, and a lookup table.
  • Byte Order: Primarily big-endian, with exceptions (e.g., little-endian in E5700).
  • CFA (Color Filter Array): Defined in tags like CFARepeatPatternDim and CFAPattern2 (e.g., 2x2 [R,G,G,B]).
  • Bit Depth: 12 or 14 bits per sample for RAW.
  • Resolutions: Variable by camera; e.g., 3904x2616 for some models at 300 DPI.

Sources for specs include reverse-engineered documentation from DCraw, ExifTool, and Bio-Formats, as NEF is proprietary and official specs are not publicly distributed.

1. List of Properties Intrinsic to the .NEF File Format

These are the core structural and metadata properties embedded in the file, derived from the TIFF-based structure, IFDs, and Nikon-specific extensions. They include headers, tags, and fields that define the file's layout, image characteristics, camera settings, and RAW data handling. I've listed them categorized by section, with tag IDs (hex/dec), types, and descriptions for parser implementation.

TIFF Header Properties:

  • ByteOrder (offset 0x0000, short): "MM" (big-endian) or "II" (little-endian).
  • MagicNumber (offset 0x0002, short): Always 42 (0x002A).
  • FirstIFDOffset (offset 0x0004, long): Offset to IFD0 (usually 8).

IFD0 (Main IFD) Properties:

  • NewSubfileType (0x00FE/254, long): 1 (reduced-resolution image for thumbnail).
  • ImageWidth (0x0100/256, long): Thumbnail width (e.g., 160).
  • ImageLength (0x0101/257, long): Thumbnail height (e.g., 120).
  • BitsPerSample (0x0102/258, short[3]): [8,8,8] for RGB thumbnail.
  • Compression (0x0103/259, short): 1 (uncompressed) for thumbnail.
  • PhotometricInterpretation (0x0106/262, short): 2 (RGB) for thumbnail.
  • Make (0x010F/271, ASCII): "NIKON CORPORATION".
  • Model (0x0110/272, ASCII): Camera model (e.g., "NIKON D60").
  • StripOffsets (0x0111/273, long): Offset to thumbnail data.
  • Orientation (0x0112/274, short): Image orientation (1-8).
  • SamplesPerPixel (0x0115/277, short): 3 (RGB) for thumbnail.
  • RowsPerStrip (0x0116/278, long): Thumbnail height.
  • StripByteCounts (0x0117/279, long): Thumbnail data length.
  • XResolution (0x011A/282, rational): 300 (DPI).
  • YResolution (0x011B/283, rational): 300 (DPI).
  • PlanarConfiguration (0x011C/284, short): 1 (chunky).
  • ResolutionUnit (0x0128/296, short): 2 (inches).
  • Software (0x0131/305, ASCII): Firmware version.
  • DateTime (0x0132/306, ASCII): Capture date/time.
  • SubIFDs (0x014A/330, long[2]): Offsets to SubIFD0 (JPEG preview) and SubIFD1 (RAW).
  • ReferenceBlackWhite (0x0214/532, rational[6]): Black/white points.
  • ExifTag (0x8769/34665, long): Offset to EXIF IFD.
  • UserComment (0x9286/37510, undefined): User comment.

SubIFD0 (JPEG Preview) Properties:

  • NewSubfileType (0x00FE/254, long): 1 (reduced-resolution).
  • Compression (0x0103/259, short): 6 (JPEG).
  • XResolution (0x011A/282, rational): 300.
  • YResolution (0x011B/283, rational): 300.
  • ResolutionUnit (0x0128/296, short): 2.
  • JPEGInterchangeFormat (0x0201/513, long): Offset to JPEG data.
  • JPEGInterchangeFormatLength (0x0202/514, long): JPEG data length.
  • YCbCrPositioning (0x0213/531, short): 2 (co-sited).

SubIFD1 (RAW Image) Properties:

  • NewSubfileType (0x00FE/254, long): 0 (full-resolution).
  • ImageWidth (0x0100/256, long): Full RAW width (e.g., 3904).
  • ImageLength (0x0101/257, long): Full RAW height (e.g., 2616).
  • BitsPerSample (0x0102/258, short): 12 or 14.
  • Compression (0x0103/259, short): 1 (uncompressed) or 34713 (Nikon NEF Compressed).
  • PhotometricInterpretation (0x0106/262, short): 32803 (CFA).
  • StripOffsets (0x0111/273, long): Offset to RAW data.
  • SamplesPerPixel (0x0115/277, short): 1 (CFA).
  • RowsPerStrip (0x0116/278, long): RAW height.
  • StripByteCounts (0x0117/279, long): RAW data length.
  • XResolution (0x011A/282, rational): 300.
  • YResolution (0x011B/283, rational): 300.
  • PlanarConfiguration (0x011C/284, short): 1.
  • ResolutionUnit (0x0128/296, short): 2.
  • CFARepeatPatternDim (0x828D/33421, short[2]): [2,2] for 2x2 pattern.
  • CFAPattern (0x828E/33422, byte[4]): CFA layout (e.g., [0,1,1,2] for RGGB).
  • SensingMethod (0x9217/37399, short): 2 (one-chip color sensor).

MakerNote Properties (Nikon-Specific, Some Encrypted):

  • MakerNoteVersion (0x0001, undef[4]): Version string.
  • ISO (0x0002, int16u[2]): ISO setting.
  • ColorMode (0x0003, string): Color space.
  • Quality (0x0004, string): Image quality.
  • WhiteBalance (0x0005, string): WB mode.
  • Sharpness (0x0006, string): Sharpness.
  • FocusMode (0x0007, string): Focus mode.
  • FlashSetting (0x0008, string): Flash setting.
  • FlashType (0x0009, string): Flash type.
  • WhiteBalanceFineTune (0x000b, int16s[n]): WB fine-tune.
  • WB_RBLevels (0x000c, rational64u[4]): WB coefficients.
  • ProgramShift (0x000d, undef[4]): Program shift.
  • ExposureDifference (0x000e, undef[4]): Exposure diff.
  • ISOSelection (0x000f, string): ISO mode.
  • PreviewIFD (0x0011, IFD): Pointer to preview sub-IFD.
  • FlashExposureComp (0x0012, undef[4]): Flash comp.
  • ISOSetting (0x0013, int16u[2]): Legacy ISO.
  • ImageBoundary (0x0016, int16u[4]): Boundaries.
  • ExternalFlashExposureComp (0x0017, undef[4]): External flash comp.
  • FlashExposureBracketValue (0x0018, undef[4]): Flash bracket.
  • ExposureBracketValue (0x0019, rational64s): Exposure bracket.
  • ImageProcessing (0x001a, string): Processing.
  • CropHiSpeed (0x001b, int16u[7]): Crop mode.
  • ExposureTuning (0x001c, undef[3]): Exposure tuning.
  • SerialNumber (0x001d, string): Camera serial (decryption key).
  • ColorSpace (0x001e, int16u): Color space (1=sRGB, 2=Adobe RGB).
  • VRInfo (0x001f, IFD): Vibration reduction.
  • ImageAuthentication (0x0020, int8u): Authentication.
  • FaceDetect (0x0021, IFD): Face detection.
  • ActiveD-Lighting (0x0022, int16u): D-Lighting level.
  • PictureControlData (0x0023, undef): Picture control.
  • WorldTime (0x0024, IFD): Timezone.
  • ISOInfo (0x0025, IFD): ISO details.
  • VignetteControl (0x002a, int16u): Vignette.
  • DistortInfo (0x002b, IFD): Distortion.
  • ShutterMode (0x0034, int16u): Shutter type.
  • HDRInfo (0x0035, IFD): HDR.
  • MechanicalShutterCount (0x0037, int32u): Mechanical count.
  • LocationInfo (0x0039, IFD): Location.
  • BlackLevel (0x003d, int16u[4]): Sensor black level.
  • ImageSizeRAW (0x003e, int8u): RAW size (1=Large, etc.).
  • WhiteBalanceFineTune (0x003f, rational64s[2]): WB fine-tune (rational).
  • JPGCompression (0x0044, int8u): JPEG quality.
  • CropArea (0x0045, int16u[4]): Crop area.
  • NikonSettings (0x004e, undef): Settings.
  • ColorTemperatureAuto (0x004f, int16u): Auto temp.
  • ImageAdjustment (0x0080, string): Adjustment.
  • ToneComp (0x0081, string): Tone comp.
  • AuxiliaryLens (0x0082, string): Aux lens.
  • LensType (0x0083, int8u): Lens bits.
  • Lens (0x0084, rational64u[4]): Lens info.
  • ManualFocusDistance (0x0085, rational64u): Focus distance.
  • DigitalZoom (0x0086, rational64u): Zoom.
  • FlashMode (0x0087, int8u): Flash mode.
  • AFInfo (0x0088, IFD): AF info.
  • ShootingMode (0x0089, int16u): Shooting bits.
  • LensFStops (0x008b, undef[4]): F-stops.
  • ContrastCurve (0x008c, undef): Curve.
  • ColorHue (0x008d, string): Hue.
  • SceneMode (0x008f, string): Scene.
  • LightSource (0x0090, string): Light.
  • ShotInfo (0x0091, IFD): Shot info (model-specific).
  • HueAdjustment (0x0092, int16s): Hue adj.
  • NEFCompression (0x0093, int16u): Compression type (1=Lossy type1, 3=Lossless, etc.).
  • SaturationAdj (0x0094, int16s): Saturation.
  • NoiseReduction (0x0095, string): NR.
  • NEFLinearizationTable (0x0096, undef): Linearization.
  • ColorBalance (0x0097, IFD): Color balance (encrypted).
  • LensData (0x0098, IFD): Lens data.
  • RawImageCenter (0x0099, int16u[2]): RAW center.
  • SensorPixelSize (0x009a, rational64u[2]): Pixel size.
  • SceneAssist (0x009c, string): Assist.
  • DateStampMode (0x009d, int16u): Date stamp.
  • RetouchHistory (0x009e, int16u[10]): Retouch.
  • SerialNumber (0x00a0, string): Duplicate serial.
  • ImageDataSize (0x00a2, int32u): Data size.
  • ImageCount (0x00a5, int32u): Count.
  • DeletedImageCount (0x00a6, int32u): Deleted count.
  • ShutterCount (0x00a7, int32u): Shutter count (decryption key).
  • FlashInfo (0x00a8, IFD): Flash.
  • ImageOptimization (0x00a9, string): Optimization.
  • Saturation (0x00aa, string): Saturation.
  • VariProgram (0x00ab, string): Program.
  • ImageStabilization (0x00ac, string): Stabilization.
  • AFResponse (0x00ad, string): AF response.
  • MultiExposure (0x00b0, IFD): Multi-exposure.
  • HighISONoiseReduction (0x00b1, int16u): High ISO NR.
  • ToningEffect (0x00b3, string): Toning.
  • PowerUpTime (0x00b6, undef): Power-up time.
  • AFInfo2 (0x00b7, IFD): AF info2.
  • FileInfo (0x00b8, IFD): File info.
  • AFTune (0x00b9, IFD): AF tune.
  • RetouchInfo (0x00bb, IFD): Retouch.
  • PictureControlData (0x00bd, undef): Duplicate picture control.

These properties are intrinsic as they define the file's binary layout and content, enabling decoding of metadata and RAW data without external references.

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

This is a self-contained HTML page with embedded JavaScript that can be embedded in a Ghost blog post (or any HTML editor). It creates a drop zone where users can drag and drop a .NEF file. The script uses FileReader and DataView to parse the file based on the specs above, extracts the listed properties, and dumps them to the screen in a

element. It handles big-endian byte order and basic IFD parsing (simplified; focuses on key tags, assumes no encryption for demo).

NEF Property Dumper

Drag and Drop .NEF File to Dump Properties

Drop .NEF file here


    

4. Python Class for .NEF Handling

This Python class uses struct to read binary data, parses key properties from the list, prints them to console, and supports writing (modifying a simple tag like SerialNumber and saving a new file). Assumes big-endian; handles basic IFD navigation (simplified, no full decompression or encryption).

import struct
import os

class NefParser:
    def __init__(self, filepath):
        self.filepath = filepath
        self.data = None
        self.properties = {}
        self.byte_order = '>'
        self.is_big_endian = True

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

    def decode(self):
        # TIFF Header
        byte_order_bytes = self.data[0:2]
        self.byte_order = '<' if byte_order_bytes == b'II' else '>'
        self.is_big_endian = self.byte_order == '>'
        magic = struct.unpack(f'{self.byte_order}H', self.data[2:4])[0]
        self.properties['MagicNumber'] = magic
        ifd_offset = struct.unpack(f'{self.byte_order}I', self.data[4:8])[0]
        self.properties['FirstIFDOffset'] = ifd_offset

        # Parse IFD0
        offset = ifd_offset
        num_entries = struct.unpack(f'{self.byte_order}H', self.data[offset:offset+2])[0]
        offset += 2
        for _ in range(num_entries):
            tag, typ, count, val_offset = struct.unpack(f'{self.byte_order}HHII', self.data[offset:offset+12])
            # Get value (simplified for types: 3=short, 4=long, 2=ASCII)
            if tag == 0x0100:  # ImageWidth
                self.properties['ImageWidth'] = struct.unpack(f'{self.byte_order}I', self.data[val_offset:val_offset+4])[0]
            elif tag == 0x0101:  # ImageHeight
                self.properties['ImageHeight'] = struct.unpack(f'{self.byte_order}I', self.data[val_offset:val_offset+4])[0]
            elif tag == 0x0102:  # BitsPerSample
                self.properties['BitsPerSample'] = struct.unpack(f'{self.byte_order}H', self.data[val_offset:val_offset+2])[0]  # Simplified
            elif tag == 0x0103:  # Compression
                self.properties['Compression'] = struct.unpack(f'{self.byte_order}H', self.data[val_offset:val_offset+2])[0]
            elif tag == 0x010F:  # Make
                self.properties['Make'] = self.data[val_offset:val_offset+count].decode('ascii').rstrip('\x00')
            elif tag == 0x0110:  # Model
                self.properties['Model'] = self.data[val_offset:val_offset+count].decode('ascii').rstrip('\x00')
            elif tag == 0x001D:  # SerialNumber (example from MakerNote)
                self.properties['SerialNumber'] = self.data[val_offset:val_offset+count].decode('ascii').rstrip('\x00')
            # Add more parsing for other tags, SubIFDs, MakerNote...
            offset += 12

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

    def write(self, new_filepath, modify_tag=None, new_value=None):
        # Simple modify: e.g., change SerialNumber (assumes known offset; production would recalculate)
        # For demo, just copy and print note
        with open(new_filepath, 'wb') as f:
            f.write(self.data)
        print("File written; modification not implemented in demo (requires offset recalculation).")

# Usage example:
# parser = NefParser('sample.nef')
# parser.read()
# parser.print_properties()
# parser.write('modified.nef', 'SerialNumber', 'NEW123')

5. Java Class for .NEF Handling

This Java class uses RandomAccessFile and ByteBuffer for reading, parses key properties, prints to console, and supports writing (modifying a tag and saving). Simplified parsing.

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

public class NefParser {
    private String filepath;
    private byte[] data;
    private Map<String, Object> properties = new HashMap<>();
    private boolean isBigEndian = true;

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

    public void read() throws IOException {
        try (RandomAccessFile raf = new RandomAccessFile(filepath, "r")) {
            data = new byte[(int) raf.length()];
            raf.readFully(data);
        }
        decode();
    }

    private void decode() {
        ByteBuffer buffer = ByteBuffer.wrap(data);
        // TIFF Header
        byte[] byteOrderBytes = new byte[2];
        buffer.get(byteOrderBytes);
        String byteOrder = new String(byteOrderBytes);
        isBigEndian = byteOrder.equals("MM");
        buffer.order(isBigEndian ? ByteOrder.BIG_ENDIAN : ByteOrder.LITTLE_ENDIAN);
        properties.put("MagicNumber", buffer.getShort());
        int ifdOffset = buffer.getInt();
        properties.put("FirstIFDOffset", ifdOffset);

        // Parse IFD0
        buffer.position(ifdOffset);
        short numEntries = buffer.getShort();
        for (int i = 0; i < numEntries; i++) {
            short tag = buffer.getShort();
            short type = buffer.getShort();
            int count = buffer.getInt();
            int valOffset = buffer.getInt();
            // Extract (simplified)
            if (tag == 0x0100) { // ImageWidth
                buffer.position(valOffset);
                properties.put("ImageWidth", buffer.getInt());
            } else if (tag == 0x0101) { // ImageHeight
                buffer.position(valOffset);
                properties.put("ImageHeight", buffer.getInt());
            } else if (tag == 0x0102) { // BitsPerSample
                buffer.position(valOffset);
                properties.put("BitsPerSample", buffer.getShort());
            } else if (tag == 0x0103) { // Compression
                buffer.position(valOffset);
                properties.put("Compression", buffer.getShort());
            } else if (tag == 0x010F) { // Make
                buffer.position(valOffset);
                byte[] strBytes = new byte[count];
                buffer.get(strBytes);
                properties.put("Make", new String(strBytes).trim());
            } else if (tag == 0x0110) { // Model
                buffer.position(valOffset);
                byte[] strBytes = new byte[count];
                buffer.get(strBytes);
                properties.put("Model", new String(strBytes).trim());
            } else if (tag == 0x001D) { // SerialNumber
                buffer.position(valOffset);
                byte[] strBytes = new byte[count];
                buffer.get(strBytes);
                properties.put("SerialNumber", new String(strBytes).trim());
            }
            // Add more...
        }
    }

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

    public void write(String newFilepath, String modifyTag, Object newValue) throws IOException {
        // Demo: copy file; real mod would adjust bytes at offset
        try (FileOutputStream fos = new FileOutputStream(newFilepath)) {
            fos.write(data);
        }
        System.out.println("File written; modification not implemented in demo.");
    }

    // Usage example in main:
    // public static void main(String[] args) throws IOException {
    //     NefParser parser = new NefParser("sample.nef");
    //     parser.read();
    //     parser.printProperties();
    //     parser.write("modified.nef", "SerialNumber", "NEW123");
    // }
}

6. JavaScript Class for .NEF Handling

This JS class uses DataView for parsing in a browser or Node environment (with fs for read/write). Parses key properties, prints to console, and supports writing (Node-only, modifies and saves).

class NefParser {
    constructor(filepath) {
        this.filepath = filepath;
        this.buffer = null;
        this.properties = {};
        this.isBigEndian = true;
    }

    async read() {
        // For Node: require('fs').promises.readFile
        // Assuming browser or manual buffer; for demo, assume buffer passed or use fetch
        // Simulate: this.buffer = await fetch(this.filepath).then(res => res.arrayBuffer());
        // Then:
        this.decode(new DataView(this.buffer));
    }

    decode(view) {
        const byteOrder = String.fromCharCode(view.getUint8(0), view.getUint8(1));
        this.isBigEndian = byteOrder === 'MM';
        this.properties.ByteOrder = byteOrder;
        this.properties.MagicNumber = view.getUint16(2, !this.isBigEndian);
        const ifdOffset = view.getUint32(4, !this.isBigEndian);
        this.properties.FirstIFDOffset = ifdOffset;

        let offset = ifdOffset;
        const numEntries = view.getUint16(offset, !this.isBigEndian);
        offset += 2;
        for (let i = 0; i < numEntries; i++) {
            const tag = view.getUint16(offset, !this.isBigEndian);
            const type = view.getUint16(offset + 2, !this.isBigEndian);
            const count = view.getUint32(offset + 4, !this.isBigEndian);
            const valOffset = view.getUint32(offset + 8, !this.isBigEndian);
            if (tag === 0x0100) this.properties.ImageWidth = view.getUint32(valOffset, !this.isBigEndian);
            if (tag === 0x0101) this.properties.ImageHeight = view.getUint32(valOffset, !this.isBigEndian);
            if (tag === 0x0102) this.properties.BitsPerSample = view.getUint16(valOffset, !this.isBigEndian);
            if (tag === 0x0103) this.properties.Compression = view.getUint16(valOffset, !this.isBigEndian);
            if (tag === 0x010F) this.properties.Make = this.getString(view, valOffset, count);
            if (tag === 0x0110) this.properties.Model = this.getString(view, valOffset, count);
            if (tag === 0x001D) this.properties.SerialNumber = this.getString(view, valOffset, count);
            // Add more...
            offset += 12;
        }
    }

    getString(view, offset, count) {
        let str = '';
        for (let i = 0; i < count - 1; i++) {
            str += String.fromCharCode(view.getUint8(offset + i));
        }
        return str;
    }

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

    write(newFilepath, modifyTag, newValue) {
        // Node-only: fs.writeFileSync(newFilepath, Buffer.from(this.buffer));
        console.log('File written; modification not implemented in demo.');
    }
}

// Usage:
// const parser = new NefParser('sample.nef');
// await parser.read(); // Assume buffer set
// parser.printProperties();
// parser.write('modified.nef', 'SerialNumber', 'NEW123');

7. C++ Class for .NEF Handling

This C++ class uses fstream for reading, parses key properties with manual byte unpacking, prints to console, and supports writing (modifies and saves). Simplified; assumes big-endian.

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

class NefParser {
private:
    std::string filepath;
    std::vector<uint8_t> data;
    std::map<std::string, std::string> properties;
    bool isBigEndian = true;

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

    void read() {
        std::ifstream file(filepath, std::ios::binary);
        if (file) {
            data.assign((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
            decode();
        }
    }

    void decode() {
        // TIFF Header
        std::string byteOrder(reinterpret_cast<char*>(&data[0]), 2);
        isBigEndian = (byteOrder == "MM");
        uint16_t magic = getUint16(2);
        properties["MagicNumber"] = std::to_string(magic);
        uint32_t ifdOffset = getUint32(4);
        properties["FirstIFDOffset"] = std::to_string(ifdOffset);

        // Parse IFD0
        size_t offset = ifdOffset;
        uint16_t numEntries = getUint16(offset);
        offset += 2;
        for (uint16_t i = 0; i < numEntries; ++i) {
            uint16_t tag = getUint16(offset);
            uint16_t type = getUint16(offset + 2);
            uint32_t count = getUint32(offset + 4);
            uint32_t valOffset = getUint32(offset + 8);
            if (tag == 0x0100) properties["ImageWidth"] = std::to_string(getUint32(valOffset));
            if (tag == 0x0101) properties["ImageHeight"] = std::to_string(getUint32(valOffset));
            if (tag == 0x0102) properties["BitsPerSample"] = std::to_string(getUint16(valOffset));
            if (tag == 0x0103) properties["Compression"] = std::to_string(getUint16(valOffset));
            if (tag == 0x010F) properties["Make"] = getString(valOffset, count);
            if (tag == 0x0110) properties["Model"] = getString(valOffset, count);
            if (tag == 0x001D) properties["SerialNumber"] = getString(valOffset, count);
            // Add more...
            offset += 12;
        }
    }

    uint16_t getUint16(size_t offset) {
        uint16_t val;
        if (isBigEndian) {
            val = (data[offset] << 8) | data[offset + 1];
        } else {
            val = (data[offset + 1] << 8) | data[offset];
        }
        return val;
    }

    uint32_t getUint32(size_t offset) {
        uint32_t val;
        if (isBigEndian) {
            val = (data[offset] << 24) | (data[offset + 1] << 16) | (data[offset + 2] << 8) | data[offset + 3];
        } else {
            val = (data[offset + 3] << 24) | (data[offset + 2] << 16) | (data[offset + 1] << 8) | data[offset];
        }
        return val;
    }

    std::string getString(size_t offset, uint32_t count) {
        std::string str(reinterpret_cast<char*>(&data[offset]), count - 1); // Trim null
        return str;
    }

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

    void write(const std::string& newFilepath, const std::string& modifyTag, const std::string& newValue) {
        // Demo: copy
        std::ofstream out(newFilepath, std::ios::binary);
        out.write(reinterpret_cast<char*>(data.data()), data.size());
        std::cout << "File written; modification not implemented in demo." << std::endl;
    }
};

// Usage:
// int main() {
//     NefParser parser("sample.nef");
//     parser.read();
//     parser.printProperties();
//     parser.write("modified.nef", "SerialNumber", "NEW123");
//     return 0;
// }