如何在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
相关产品推荐
相关产品推荐

