You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在JavaScript中拆解RAW DNG文件并提取全部数据?

在JavaScript中解析DNG文件:遍历IFD并提取全量数据

DNG是基于TIFF格式的RAW图像标准,核心结构为文件头 + 多个IFD(图像文件目录) + 对应数据块。每个IFD是一组元数据的容器,同时会记录下一个IFD的偏移地址——这是遍历所有IFD的核心依据。以下是具体实现步骤:

1. 解析文件头,定位第一个IFD

DNG文件头的前14字节包含关键定位信息:

  • 前2字节:字节序标识(II=小端字节序,MM=大端字节序)
  • 第3-4字节:TIFF版本(固定为0x002A)
  • 第5-8字节:第一个IFD相对于文件开头的偏移地址

代码示例(Node.js/浏览器通用逻辑)

// Node.js环境用Buffer,浏览器环境用Uint8Array+DataView
function parseDngHeader(bufferOrArrayBuffer) {
  const isNode = typeof Buffer !== 'undefined';
  const view = isNode ? bufferOrArrayBuffer : new DataView(bufferOrArrayBuffer);
  const isLittleEndian = isNode 
    ? bufferOrArrayBuffer.readUInt16BE(0) === 0x4949 
    : view.getUint16(0, false) === 0x4949;

  const tiffVersion = isNode
    ? bufferOrArrayBuffer.readUInt16(2, { endian: isLittleEndian ? 'little' : 'big' })
    : view.getUint16(2, isLittleEndian);

  const firstIfdOffset = isNode
    ? bufferOrArrayBuffer.readUInt32(4, { endian: isLittleEndian ? 'little' : 'big' })
    : view.getUint32(4, isLittleEndian);

  return { isLittleEndian, tiffVersion, firstIfdOffset };
}

2. 遍历所有IFD的通用逻辑

每个IFD的结构固定:

  • 2字节:当前IFD包含的标签数量
  • 每个标签占12字节:标签ID(2)、数据类型(2)、数据计数(4)、数据值/偏移(4)
  • 4字节:下一个IFD的偏移地址(0表示无后续IFD)

通过循环读取每个IFD,处理完后跳转到下一个IFD的偏移地址,直到偏移为0即可完成遍历:

function traverseAllIfds(bufferOrArrayBuffer, isLittleEndian, currentIfdOffset) {
  const isNode = typeof Buffer !== 'undefined';
  const view = isNode ? bufferOrArrayBuffer : new DataView(bufferOrArrayBuffer);
  const ifds = [];

  while (currentIfdOffset !== 0) {
    // 读取当前IFD的标签数量
    const tagCount = isNode
      ? bufferOrArrayBuffer.readUInt16(currentIfdOffset, { endian: isLittleEndian ? 'little' : 'big' })
      : view.getUint16(currentIfdOffset, isLittleEndian);

    const tags = [];
    let tagOffset = currentIfdOffset + 2; // 跳过标签计数字节

    // 逐个解析标签
    for (let i = 0; i < tagCount; i++) {
      const tagId = isNode
        ? bufferOrArrayBuffer.readUInt16(tagOffset, { endian: isLittleEndian ? 'little' : 'big' })
        : view.getUint16(tagOffset, isLittleEndian);

      const dataType = isNode
        ? bufferOrArrayBuffer.readUInt16(tagOffset + 2, { endian: isLittleEndian ? 'little' : 'big' })
        : view.getUint16(tagOffset + 2, isLittleEndian);

      const dataCount = isNode
        ? bufferOrArrayBuffer.readUInt32(tagOffset + 4, { endian: isLittleEndian ? 'little' : 'big' })
        : view.getUint32(tagOffset + 4, isLittleEndian);

      const dataValueOrOffset = isNode
        ? bufferOrArrayBuffer.readUInt32(tagOffset + 8, { endian: isLittleEndian ? 'little' : 'big' })
        : view.getUint32(tagOffset + 8, isLittleEndian);

      // 解析标签的实际值
      const tagValue = parseTagValue(bufferOrArrayBuffer, isLittleEndian, dataType, dataCount, dataValueOrOffset);
      tags.push({ tagId, dataType, value: tagValue });
      tagOffset += 12;
    }

    // 记录当前IFD数据
    ifds.push({ offset: currentIfdOffset, tags });

    // 获取下一个IFD的偏移地址
    currentIfdOffset = isNode
      ? bufferOrArrayBuffer.readUInt32(tagOffset, { endian: isLittleEndian ? 'little' : 'big' })
      : view.getUint32(tagOffset, isLittleEndian);
  }
  return ifds;
}

// 辅助函数:根据DNG规范解析不同类型的标签值
function parseTagValue(bufferOrArrayBuffer, isLittleEndian, dataType, dataCount, dataValueOrOffset) {
  const isNode = typeof Buffer !== 'undefined';
  const view = isNode ? bufferOrArrayBuffer : new DataView(bufferOrArrayBuffer);

  switch(dataType) {
    // ASCII字符串类型(标签ID如0x010F=设备厂商,0x0110=设备型号)
    case 2:
      const strStart = dataValueOrOffset;
      const strEnd = strStart + dataCount - 1; // 跳过末尾的null终止符
      return isNode
        ? bufferOrArrayBuffer.slice(strStart, strEnd).toString('ascii')
        : new TextDecoder('ascii').decode(new Uint8Array(bufferOrArrayBuffer, strStart, strEnd - strStart));
    
    // 16位短整数类型(如0x0100=图像宽度)
    case 3:
      if (dataCount === 1) {
        return isNode
          ? bufferOrArrayBuffer.readUInt16(dataValueOrOffset, { endian: isLittleEndian ? 'little' : 'big' })
          : view.getUint16(dataValueOrOffset, isLittleEndian);
      }
      const shortValues = [];
      for (let i = 0; i < dataCount; i++) {
        const pos = dataValueOrOffset + i * 2;
        shortValues.push(isNode
          ? bufferOrArrayBuffer.readUInt16(pos, { endian: isLittleEndian ? 'little' : 'big' })
          : view.getUint16(pos, isLittleEndian)
        );
      }
      return shortValues;
    
    // 32位长整数类型(如0x0111=图像数据偏移,0x0117=图像数据长度)
    case 4:
      if (dataCount === 1) {
        return isNode
          ? bufferOrArrayBuffer.readUInt32(dataValueOrOffset, { endian: isLittleEndian ? 'little' : 'big' })
          : view.getUint32(dataValueOrOffset, isLittleEndian);
      }
      const longValues = [];
      for (let i = 0; i < dataCount; i++) {
        const pos = dataValueOrOffset + i * 4;
        longValues.push(isNode
          ? bufferOrArrayBuffer.readUInt32(pos, { endian: isLittleEndian ? 'little' : 'big' })
          : view.getUint32(pos, isLittleEndian)
        );
      }
      return longValues;
    
    // 其他类型(如浮点、字节数组)可参考DNG规范扩展
    default:
      return dataValueOrOffset;
  }
}

3. 提取关键数据:主图像、缩略图、全量元数据

主图像数据

主图像信息通常在第一个IFD中,重点关注以下标签:

  • 0x0100(ImageWidth):图像宽度
  • 0x0101(ImageLength):图像高度
  • 0x0103(Compression):压缩类型(DNG常用0x8828表示Deflate压缩)
  • 0x0111(StripOffsets):图像数据块的起始偏移
  • 0x0117(StripByteCounts):图像数据块的总长度

如果是Deflate压缩,可使用pako库解压数据,得到原始RAW像素流。

缩略图数据

缩略图通常存放在第二个IFD中,同样提取宽度、高度、压缩类型、数据偏移和长度。如果是JPEG格式的缩略图,直接提取对应字节流即可生成可显示的图片。

全量元数据

遍历所有IFD后,可将标签ID映射为DNG规范中的可读名称(比如0x0131=软件版本,0x0132=拍摄日期),整理成结构化的元数据对象。

4. 对现有单IFD项目的扩展

你提到的现有项目仅支持单个IFD解析,可做以下修改:

  • 在解析完第一个IFD后,读取其末尾的下一个IFD偏移地址
  • 循环调用原有的IFD解析函数,直到偏移地址为0
  • 新增压缩数据的解压逻辑(引入pako处理Deflate压缩)

内容的提问来源于stack exchange,提问作者m-a-r-c-e-l-i-n-o

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.30 10:22:39