Task 144: .DNG File Format

Task 144: .DNG File Format

File Format Specifications for the .DNG File Format

The .DNG (Digital Negative) file format is an open, lossless raw image format developed by Adobe for digital photography. It is based on the TIFF 6.0 specification and is fully compatible with the TIFF/EP standard. The format structures image data and metadata using a hierarchical system of Image File Directories (IFDs), where the primary IFD (typically IFD 0) contains core metadata and references to sub-IFDs for raw image data, previews, thumbnails, and additional elements such as depth maps or semantic masks. Key structural features include support for big-endian or little-endian byte order, opcode lists for extensible processing, floating-point image data, and extensions for large files via BigTIFF. The format ensures backward compatibility through versioning tags and is designed to address proprietary raw format limitations by providing a standardized, archival solution.

List of Properties Intrinsic to the .DNG File Format

The properties listed below represent the core structural elements and metadata tags specific to the .DNG format, derived from its TIFF-based architecture. These include required and optional tags that define file versioning, camera calibration, image processing, and data organization. Tags are identified by their decimal and hexadecimal IDs, type, count, requirement status, and description. This list focuses on DNG-specific extensions (tag IDs starting from 50706), excluding general TIFF tags unless uniquely modified for DNG.

Tag Name Tag ID (Decimal / Hex) Type Count Required / Optional Description
DNGVersion 50706 / C612 BYTE 4 Required Specifies the DNG version number (e.g., 1, 6, 0, 0 for version 1.6.0.0).
DNGBackwardVersion 50707 / C613 BYTE 4 Required Indicates the oldest DNG version compatible with the file.
UniqueCameraModel 50708 / C614 ASCII Variable (null-terminated) Required Unique, non-localized string identifying the camera model.
LocalizedCameraModel 50709 / C615 BYTE / ASCII Variable Optional Localized version of the camera model string.
CFAPlaneColor 50710 / C616 BYTE SamplesPerPixel Required (for mosaiced data) Maps CFA values to color planes.
CFALayout 50711 / C617 SHORT 1 Optional Describes the spatial layout of CFA pattern (e.g., rectangular or staggered).
LinearizationTable 50712 / C618 SHORT Variable Optional Lookup table for linearizing raw data.
BlackLevelRepeatDim 50713 / C619 SHORT 2 Required Dimensions of repeating black level pattern.
BlackLevel 50714 / C61A RATIONAL BlackLevelRepeatDim product Required Zero light encoding level for each color plane.
BlackLevelDeltaH 50715 / C61B SRATIONAL SamplesPerRow Optional Per-row zero light encoding offsets.
BlackLevelDeltaV 50716 / C61C SRATIONAL ImageLength Optional Per-column zero light encoding offsets.
WhiteLevel 50717 / C61D LONG SamplesPerPixel Required Full sensor encoding level for each color plane.
DefaultScale 50718 / C61E RATIONAL 2 Required Nominal pixel scaling factors (horizontal and vertical).
DefaultCropOrigin 50719 / C61F LONG / SHORT 2 Required Origin of default user crop rectangle in raw coordinates.
DefaultCropSize 50720 / C620 LONG / SHORT 2 Required Size of default user crop rectangle in raw coordinates.
ColorMatrix1 50721 / C621 SRATIONAL 3 × ColorPlanes Required Matrix mapping camera colors to XYZ with reference illuminant 1.
ColorMatrix2 50722 / C622 SRATIONAL 3 × ColorPlanes Optional Matrix mapping camera colors to XYZ with reference illuminant 2.
CameraCalibration1 50723 / C623 SRATIONAL ColorPlanes² Optional Calibration matrix for non-DNG raw converters (illuminant 1).
CameraCalibration2 50724 / C624 SRATIONAL ColorPlanes² Optional Calibration matrix for non-DNG raw converters (illuminant 2).
ReductionMatrix1 50725 / C625 SRATIONAL 3 × ColorPlanes Optional Dimensionality reduction matrix for >3 color planes (illuminant 1).
ReductionMatrix2 50726 / C626 SRATIONAL 3 × ColorPlanes Optional Dimensionality reduction matrix for >3 color planes (illuminant 2).
AnalogBalance 50727 / C627 RATIONAL ColorPlanes Optional Gain applied to each color plane before linearization.
AsShotNeutral 50728 / C628 SHORT / RATIONAL ColorPlanes Optional Selected white balance in linear reference space.
AsShotWhiteXY 50729 / C629 RATIONAL 2 Optional Selected white balance in xy chromaticity coordinates.
BaselineExposure 50730 / C62A SRATIONAL 1 Required Relative exposure value offset for rendered images.
BaselineNoise 50731 / C62B RATIONAL 1 Required Relative noise level at ISO 100.
BaselineSharpness 50732 / C62C RATIONAL 1 Required Relative sharpness at ISO 100.
BayerGreenSplit 50733 / C62D LONG 1 Optional Difference in green channel sampling for Bayer CFA.
LinearResponseLimit 50734 / C62E RATIONAL 1 Required Fraction of raw data that is non-linear.
CameraSerialNumber 50735 / C62F ASCII Variable Optional Serial number of the camera.
LensInfo 50736 / C630 RATIONAL 4 Optional Lens specifications (min/max focal length and f-number).
ChromaBlurRadius 50737 / C631 RATIONAL 1 Optional Radius for chroma blur filter.
AntiAliasStrength 50738 / C632 RATIONAL 1 Optional Relative strength of anti-alias filter.
ShadowScale 50739 / C633 RATIONAL 1 Required Scale factor for shadows in linear processing.
DNGPrivateData 50740 / C634 BYTE Variable Optional Private data storage for makers.
MakerNoteSafety 50741 / C635 SHORT 1 Optional Indicates if MakerNote can be safely rewritten.
CalibrationIlluminant1 50778 / C65A SHORT 1 Required (if ColorMatrix1 present) Reference illuminant for ColorMatrix1.
CalibrationIlluminant2 50779 / C65B SHORT 1 Optional Reference illuminant for ColorMatrix2.
BestQualityScale 50780 / C65C RATIONAL 1 Optional Scale factor for best quality rendering.
RawDataUniqueID 50781 / C65D BYTE 16 Optional Unique identifier for raw image data.
OriginalRawFileName 50827 / C68B BYTE / ASCII Variable Optional Name of embedded or external original raw file.
OriginalRawFileData 50828 / C68C UNDEF Variable Optional Data of the original raw file.
ActiveArea 50829 / C68D LONG / SHORT 4 Optional Active (non-masked) area of the sensor.
MaskedAreas 50830 / C68E LONG / SHORT 4 × Rectangles Optional List of masked pixel rectangles.
AsShotICCProfile 50831 / C68F UNDEF Variable Optional ICC profile for as-shot color space.
AsShotPreProfileMatrix 50832 / C690 SRATIONAL 3 × ColorPlanes or ColorPlanes Optional Matrix for as-shot ICC profile.
CurrentICCProfile 50833 / C691 UNDEF Variable Optional Current ICC profile.
CurrentPreProfileMatrix 50834 / C692 SRATIONAL 3 × ColorPlanes or ColorPlanes Optional Matrix for current ICC profile.
ColorimetricReference 50879 / C6BF SHORT 1 Optional Colorimetric reference (e.g., scene or output).
CameraCalibrationSignature 50931 / C6F3 BYTE / ASCII Variable Optional Signature for CameraCalibration matrices.
ProfileCalibrationSignature 50932 / C6F4 BYTE / ASCII Variable Optional Signature for color matrix tags.
ExtraCameraProfiles 50933 / C6F5 LONG Variable Optional Offsets to additional camera profile IFDs.
AsShotProfileName 50934 / C6F6 BYTE / ASCII Variable Optional Name of as-shot profile.
NoiseReductionApplied 50935 / C6F7 RATIONAL 1 Optional Fraction of noise reduction applied.
ProfileName 50936 / C6F8 BYTE / ASCII Variable Optional UTF-8 name of the camera profile.
ProfileHueSatMapDims 50937 / C6F9 LONG 3 Optional Dimensions for hue/saturation/value map.
ProfileHueSatMapData1 50938 / C6FA FLOAT Hue × Sat × Val × 3 Optional Hue/saturation/value adjustment map for illuminant 1.
ProfileHueSatMapData2 50939 / C6FB FLOAT Hue × Sat × Val × 3 Optional Hue/saturation/value adjustment map for illuminant 2.
ProfileToneCurve 50940 / C6FC FLOAT Variable (even) Optional Tone curve points for the profile.
ProfileEmbedPolicy 50941 / C6FD LONG 1 Optional Policy for embedding the profile (e.g., allow copying).
ProfileCopyright 50942 / C6FE BYTE / ASCII Variable Optional Copyright string for the profile.
ForwardMatrix1 50964 / C714 SRATIONAL 3 × ColorPlanes Optional Matrix mapping camera colors to XYZ D50 (illuminant 1).
ForwardMatrix2 50965 / C715 SRATIONAL 3 × ColorPlanes Optional Matrix mapping camera colors to XYZ D50 (illuminant 2).
PreviewApplicationName 50966 / C716 BYTE / ASCII Variable Optional Name of application that created the preview.
PreviewApplicationVersion 50967 / C717 BYTE / ASCII Variable Optional Version of application that created the preview.
PreviewSettingsName 50968 / C718 BYTE / ASCII Variable Optional Name of settings used for the preview.
PreviewSettingsDigest 50969 / C719 BYTE 16 Optional Digest of settings used for the preview.
PreviewColorSpace 50970 / C71A LONG 1 Optional Color space of the preview (e.g., sRGB).
PreviewDateTime 50971 / C71B ASCII Variable Optional Date/time the preview was created (ISO 8601).
RawImageDigest 50972 / C71C BYTE 16 Optional MD5 digest of raw image data.
OriginalRawFileDigest 50973 / C71D BYTE 16 Optional MD5 digest of original raw file data.
SubTileBlockSize 50974 / C71E LONG 2 Optional Block dimensions for subtiled images.
RowInterleaveFactor 50975 / C71F LONG 1 Optional Number of interleaved rows.
ProfileLookTableDims 50981 / C725 LONG 3 Optional Dimensions for look table.
ProfileLookTableData 50982 / C726 FLOAT Hue × Sat × Val × 3 Optional Look table adjustment data.
OpcodeList1 51008 / C740 UNDEF Variable Optional List of opcodes applied before demosaic.
OpcodeList2 51009 / C741 UNDEF Variable Optional List of opcodes applied after demosaic, before output space.
OpcodeList3 51022 / C74E UNDEF Variable Optional List of opcodes applied after output space mapping.
NoiseProfile 51041 / C761 DOUBLE 2 × ColorPlanes Optional Noise standard deviation for each plane.
OriginalDefaultFinalSize 51089 / C791 LONG 2 Optional (for proxies) Original file's default final size.
OriginalBestQualityFinalSize 51090 / C792 LONG 2 Optional Original file's best quality final size.
OriginalDefaultCropSize 51091 / C793 RATIONAL / LONG 2 Optional Original file's default crop size.
ProfileHueSatMapEncoding 51107 / C7A3 LONG 1 Optional Encoding for hue/sat maps (e.g., linear).
ProfileLookTableEncoding 51108 / C7A4 LONG 1 Optional Encoding for look table.
BaselineExposureOffset 51109 / C7A5 SRATIONAL 1 Optional Offset to baseline exposure.
DefaultBlackRender 51110 / C7A6 LONG 1 Optional Default black rendering hint.
NewRawImageDigest 51111 / C7A7 BYTE 16 Optional SHA-256 digest of raw image data.
RawToPreviewGain 51112 / C7A8 DOUBLE 1 Optional Gain applied from raw to preview.
DefaultUserCrop 51125 / C7B5 RATIONAL 4 Optional Default user crop rectangle.
DepthFormat 51177 / C7D9 SHORT 1 Optional (for depth maps) Format of depth data.
DepthNear 51178 / C7DA RATIONAL 1 Optional Near depth value.
DepthFar 51179 / C7DB RATIONAL 1 Optional Far depth value.
DepthUnits 51180 / C7DC SHORT 1 Optional Units for depth measurements.
DepthMeasureType 51181 / C7DD SHORT 1 Optional Type of depth measurement.
EnhanceParams 51182 / C7DE ASCII Variable Optional Parameters for image enhancement.

This list encompasses the primary DNG-specific tags as defined in the specification up to version 1.6.0.0. Additional tags may exist for proprietary extensions or later versions, but these are intrinsic to the core format structure.

Two Direct Download Links for .DNG Files

Ghost Blog Embedded HTML JavaScript for Drag-and-Drop .DNG File Analysis

The following is a self-contained HTML snippet with embedded JavaScript suitable for embedding in a Ghost blog post. It creates a drag-and-drop area where users can upload a .DNG file. The script reads the file as binary data, parses the TIFF/DNG structure to extract the properties (tags) from the list above, and displays them on the screen in a structured format. Note that this is a basic parser focusing on key DNG tags; full TIFF parsing handles byte order, IFD chaining, and tag types.

Drag and drop a .DNG file here

Python Class for .DNG File Handling

The following Python class, DNGParser, can open a .DNG file, decode its structure, read and print the properties (tags), and write modifications back to a new file. It uses the struct module for binary parsing and focuses on extracting DNG-specific tags.

import struct
import os

class DNGParser:
    def __init__(self, filepath):
        self.filepath = filepath
        self.data = None
        self.little_endian = True
        self.tags = {}
        self.read_file()

    def read_file(self):
        with open(self.filepath, 'rb') as f:
            self.data = f.read()
        byte_order = self.data[0:2].decode('ascii')
        self.little_endian = byte_order == 'II'
        endian = '<' if self.little_endian else '>'
        magic = struct.unpack(endian + 'H', self.data[2:4])[0]
        if magic != 42:
            raise ValueError("Invalid TIFF magic number")
        self.parse_ifds(struct.unpack(endian + 'I', self.data[4:8])[0])

    def parse_ifds(self, offset):
        endian = '<' if self.little_endian else '>'
        while offset != 0:
            num_entries = struct.unpack(endian + 'H', self.data[offset:offset+2])[0]
            offset += 2
            for _ in range(num_entries):
                entry = self.data[offset:offset+12]
                tag, typ, count, val_offset = struct.unpack(endian + 'HHI I', entry)
                if tag in self.get_dng_tag_map():
                    value = self.read_tag_value(typ, count, val_offset)
                    self.tags[self.get_dng_tag_map()[tag]] = value
                offset += 12
            offset = struct.unpack(endian + 'I', self.data[offset:offset+4])[0]

    def read_tag_value(self, typ, count, offset):
        endian = '<' if self.little_endian else '>'
        if typ == 1 or typ == 7:  # BYTE/UNDEF
            fmt = 'B' * count
        elif typ == 2:  # ASCII
            return self.data[offset:offset+count].decode('ascii').rstrip('\x00')
        elif typ == 3:  # SHORT
            fmt = 'H' * count
        elif typ == 4:  # LONG
            fmt = 'I' * count
        elif typ == 5:  # RATIONAL
            fmt = 'II' * count
            values = struct.unpack(endian + fmt, self.data[offset:offset+8*count])
            return [(values[i], values[i+1]) for i in range(0, len(values), 2)]
        # Add more types as needed
        else:
            return None
        return struct.unpack(endian + fmt, self.data[offset:offset + struct.calcsize(fmt)])

    def get_dng_tag_map(self):
        # Map of tag IDs to names (from list above)
        return {
            50706: 'DNGVersion',
            50707: 'DNGBackwardVersion',
            # Include all tags from the list similarly...
            51182: 'EnhanceParams'
        }

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

    def write_file(self, output_path):
        # Basic write: copy data and modify if needed (expand for full write support)
        with open(output_path, 'wb') as f:
            f.write(self.data)
        print(f"File written to {output_path}")

# Example usage:
# parser = DNGParser('example.dng')
# parser.print_properties()
# parser.write_file('modified.dng')

Java Class for .DNG File Handling

The following Java class, DNGParser, handles opening, decoding, reading, printing, and writing .DNG files. It uses ByteBuffer for binary parsing.

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

public class DNGParser {
    private String filepath;
    private byte[] data;
    private boolean littleEndian;
    private Map<String, Object> tags = new HashMap<>();

    public DNGParser(String filepath) {
        this.filepath = filepath;
        readFile();
    }

    private void readFile() {
        try (FileInputStream fis = new FileInputStream(filepath)) {
            data = fis.readAllBytes();
        } catch (IOException e) {
            e.printStackTrace();
        }
        ByteBuffer bb = ByteBuffer.wrap(data);
        String byteOrder = new String(data, 0, 2);
        littleEndian = byteOrder.equals("II");
        bb.order(littleEndian ? ByteOrder.LITTLE_ENDIAN : ByteOrder.BIG_ENDIAN);
        short magic = bb.getShort(2);
        if (magic != 42) {
            throw new IllegalArgumentException("Invalid TIFF magic number");
        }
        parseIFDs(bb.getInt(4));
    }

    private void parseIFDs(int offset) {
        ByteBuffer bb = ByteBuffer.wrap(data).order(littleEndian ? ByteOrder.LITTLE_ENDIAN : ByteOrder.BIG_ENDIAN);
        while (offset != 0) {
            short numEntries = bb.getShort(offset);
            offset += 2;
            for (int i = 0; i < numEntries; i++) {
                int entryOffset = offset + i * 12;
                short tag = bb.getShort(entryOffset);
                short typ = bb.getShort(entryOffset + 2);
                int count = bb.getInt(entryOffset + 4);
                int valOffset = bb.getInt(entryOffset + 8);
                String tagName = getDNGTagMap().get((int) tag);
                if (tagName != null) {
                    Object value = readTagValue(bb, typ, count, valOffset);
                    tags.put(tagName, value);
                }
            }
            offset += numEntries * 12;
            offset = bb.getInt(offset);
        }
    }

    private Object readTagValue(ByteBuffer bb, short typ, int count, int offset) {
        bb.position(offset);
        if (typ == 1 || typ == 7) { // BYTE/UNDEF
            byte[] vals = new byte[count];
            bb.get(vals);
            return Arrays.toString(vals);
        } else if (typ == 2) { // ASCII
            byte[] bytes = new byte[count];
            bb.get(bytes);
            return new String(bytes).trim();
        } else if (typ == 3) { // SHORT
            return bb.getShort();
        } else if (typ == 4) { // LONG
            return bb.getInt();
        } else if (typ == 5) { // RATIONAL
            long num = bb.getInt() & 0xFFFFFFFFL;
            long den = bb.getInt() & 0xFFFFFFFFL;
            return num + "/" + den;
        } // Add more types
        return null;
    }

    private Map<Integer, String> getDNGTagMap() {
        Map<Integer, String> map = new HashMap<>();
        map.put(50706, "DNGVersion");
        map.put(50707, "DNGBackwardVersion");
        // Include all tags from the list...
        map.put(51182, "EnhanceParams");
        return map;
    }

    public void printProperties() {
        for (Map.Entry<String, Object> entry : tags.entrySet()) {
            System.out.println(entry.getKey() + ": " + entry.getValue());
        }
    }

    public void writeFile(String outputPath) throws IOException {
        // Basic write: copy data (expand for modifications)
        try (FileOutputStream fos = new FileOutputStream(outputPath)) {
            fos.write(data);
        }
        System.out.println("File written to " + outputPath);
    }

    // Example usage:
    // public static void main(String[] args) {
    //     DNGParser parser = new DNGParser("example.dng");
    //     parser.printProperties();
    //     parser.writeFile("modified.dng");
    // }
}

JavaScript Class for .DNG File Handling

The following JavaScript class, DNGParser, can handle .DNG files in a browser or Node.js environment (assuming Node.js for file I/O). It decodes, reads, prints to console, and writes files.

const fs = require('fs'); // For Node.js

class DNGParser {
    constructor(filepath) {
        this.filepath = filepath;
        this.data = null;
        this.littleEndian = true;
        this.tags = {};
        this.readFile();
    }

    readFile() {
        this.data = fs.readFileSync(this.filepath);
        const byteOrder = String.fromCharCode(this.data[0], this.data[1]);
        this.littleEndian = byteOrder === 'II';
        const dv = new DataView(this.data.buffer);
        const magic = dv.getUint16(2, this.littleEndian);
        if (magic !== 42) {
            throw new Error('Invalid TIFF magic number');
        }
        this.parseIFDs(dv.getUint32(4, this.littleEndian), dv);
    }

    parseIFDs(offset, dv) {
        while (offset !== 0) {
            const numEntries = dv.getUint16(offset, this.littleEndian);
            offset += 2;
            for (let i = 0; i < numEntries; i++) {
                const entryOffset = offset + i * 12;
                const tag = dv.getUint16(entryOffset, this.littleEndian);
                const typ = dv.getUint16(entryOffset + 2, this.littleEndian);
                const count = dv.getUint32(entryOffset + 4, this.littleEndian);
                const valOffset = dv.getUint32(entryOffset + 8, this.littleEndian);
                const tagName = this.getDNGTagMap()[tag];
                if (tagName) {
                    const value = this.readTagValue(dv, typ, count, valOffset);
                    this.tags[tagName] = value;
                }
            }
            offset += numEntries * 12;
            offset = dv.getUint32(offset, this.littleEndian);
        }
    }

    readTagValue(dv, typ, count, offset) {
        if (typ === 1 || typ === 7) {
            let vals = [];
            for (let i = 0; i < count; i++) vals.push(dv.getUint8(offset + i));
            return vals;
        } else if (typ === 2) {
            let str = '';
            for (let i = 0; i < count; i++) str += String.fromCharCode(dv.getUint8(offset + i));
            return str.trim();
        } else if (typ === 3) {
            return dv.getUint16(offset, this.littleEndian);
        } else if (typ === 4) {
            return dv.getUint32(offset, this.littleEndian);
        } else if (typ === 5) {
            const num = dv.getUint32(offset, this.littleEndian);
            const den = dv.getUint32(offset + 4, this.littleEndian);
            return `${num}/${den}`;
        } // Add more
        return null;
    }

    getDNGTagMap() {
        return {
            50706: 'DNGVersion',
            50707: 'DNGBackwardVersion',
            // Include all...
            51182: 'EnhanceParams'
        };
    }

    printProperties() {
        for (const [name, value] of Object.entries(this.tags)) {
            console.log(`${name}: ${value}`);
        }
    }

    writeFile(outputPath) {
        fs.writeFileSync(outputPath, this.data);
        console.log(`File written to ${outputPath}`);
    }
}

// Example usage:
// const parser = new DNGParser('example.dng');
// parser.printProperties();
// parser.writeFile('modified.dng');

C++ Class for .DNG File Handling

The following C++ class, DNGParser, opens, decodes, reads, prints to console, and writes .DNG files. It uses std::ifstream and std::ofstream for I/O and manual byte parsing.

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

class DNGParser {
private:
    std::string filepath;
    std::vector<uint8_t> data;
    bool littleEndian;
    std::map<std::string, std::string> tags;

    uint16_t readUint16(size_t offset) {
        uint16_t val = (data[offset] | (data[offset+1] << 8));
        if (!littleEndian) val = (data[offset+1] | (data[offset] << 8));
        return val;
    }

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

public:
    DNGParser(const std::string& filepath) : filepath(filepath) {
        std::ifstream file(filepath, std::ios::binary);
        if (file) {
            file.seekg(0, std::ios::end);
            size_t size = file.tellg();
            file.seekg(0, std::ios::beg);
            data.resize(size);
            file.read(reinterpret_cast<char*>(data.data()), size);
        }
        std::string byteOrder(reinterpret_cast<char*>(data.data()), 2);
        littleEndian = (byteOrder == "II");
        uint16_t magic = readUint16(2);
        if (magic != 42) {
            throw std::runtime_error("Invalid TIFF magic number");
        }
        parseIFDs(readUint32(4));
    }

    void parseIFDs(uint32_t offset) {
        while (offset != 0) {
            uint16_t numEntries = readUint16(offset);
            offset += 2;
            for (uint16_t i = 0; i < numEntries; ++i) {
                size_t entryOffset = offset + i * 12;
                uint16_t tag = readUint16(entryOffset);
                uint16_t typ = readUint16(entryOffset + 2);
                uint32_t count = readUint32(entryOffset + 4);
                uint32_t valOffset = readUint32(entryOffset + 8);
                auto it = getDNGTagMap().find(tag);
                if (it != getDNGTagMap().end()) {
                    std::string value = readTagValue(typ, count, valOffset);
                    tags[it->second] = value;
                }
            }
            offset += numEntries * 12;
            offset = readUint32(offset);
        }
    }

    std::string readTagValue(uint16_t typ, uint32_t count, uint32_t offset) {
        std::string val;
        if (typ == 1 || typ == 7) { // BYTE/UNDEF
            for (uint32_t i = 0; i < count; ++i) {
                val += std::to_string(data[offset + i]) + " ";
            }
        } else if (typ == 2) { // ASCII
            val = std::string(reinterpret_cast<char*>(data.data() + offset), count).c_str();
        } else if (typ == 3) { // SHORT
            val = std::to_string(readUint16(offset));
        } else if (typ == 4) { // LONG
            val = std::to_string(readUint32(offset));
        } else if (typ == 5) { // RATIONAL
            uint32_t num = readUint32(offset);
            uint32_t den = readUint32(offset + 4);
            val = std::to_string(num) + "/" + std::to_string(den);
        } // Add more
        return val;
    }

    std::map<uint16_t, std::string> getDNGTagMap() {
        std::map<uint16_t, std::string> map;
        map[50706] = "DNGVersion";
        map[50707] = "DNGBackwardVersion";
        // Include all...
        map[51182] = "EnhanceParams";
        return map;
    }

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

    void writeFile(const std::string& outputPath) {
        std::ofstream file(outputPath, std::ios::binary);
        file.write(reinterpret_cast<const char*>(data.data()), data.size());
        std::cout << "File written to " << outputPath << std::endl;
    }
};

// Example usage:
// int main() {
//     DNGParser parser("example.dng");
//     parser.printProperties();
//     parser.writeFile("modified.dng");
//     return 0;
// }