(function (root, factory) { if (typeof define === 'function' && define.amd) { // AMD define([], factory); } else if (typeof module === 'object' && module.exports) { // Node.js module.exports = factory(); } else { // Browser globals root.ImageExifReader = factory(); } }(typeof self !== 'undefined' ? self : this, function () { function ImageExifReader() { this.debug = true; this.tags = { // 基本 EXIF 标签 0x0100: "ImageWidth", 0x0101: "ImageHeight", 0x0112: "Orientation", 0x0132: "DateTime", 0x010F: "Make", 0x0110: "Model", // 富士相机特有的标签 0x0128: "ResolutionUnit", 0x011A: "XResolution", 0x011B: "YResolution", 0x0213: "YCbCrPositioning", // EXIF IFD 指针和常用标签 0x8769: "ExifIFDPointer", 0x829A: "ExposureTime", 0x829D: "FNumber", 0x8827: "ISOSpeedRatings", 0x9003: "DateTimeOriginal", 0x9004: "DateTimeDigitized", 0x920A: "FocalLength", 0xA402: "ExposureMode", 0xA403: "WhiteBalance", 0xA406: "SceneCaptureType", 0x9286: "UserComment", 0xA405: "FocalLengthIn35mmFilm", 0xA420: "ImageUniqueID", 0xA431: "SerialNumber", 0xA432: "LensInfo", 0xA433: "LensMake", 0xA434: "LensModel", 0xA435: "LensSerialNumber" }; // 添加标签类型定义 this.tagTypes = { Make: 'string', Model: 'string', DateTime: 'datetime', DateTimeOriginal: 'datetime', DateTimeDigitized: 'datetime', ExposureTime: 'exposuretime', FNumber: 'fnumber', FocalLength: 'focallength', ISOSpeedRatings: 'number', LensModel: 'string', LensSerialNumber: 'string', XResolution: 'resolution', YResolution: 'resolution' }; } ImageExifReader.prototype.readExifData = function(file, callback) { // 支持的图片类型 const supportedTypes = [ 'image/jpeg', 'image/jpg', 'image/tiff', 'image/heic', 'image/heif', 'image/png', 'image/webp' ]; if (!supportedTypes.some(type => file.type.startsWith(type))) { callback(new Error(`不支持的文件类型: ${file.type}`)); return; } const reader = new FileReader(); reader.onload = (e) => { try { const exifData = this.parseExif(e.target.result, file.type); callback(null, exifData); } catch (error) { callback(error); } }; reader.onerror = (error) => { callback(error); }; reader.readAsArrayBuffer(file); }; ImageExifReader.prototype.parseExif = function(arrayBuffer, fileType) { const dataView = new DataView(arrayBuffer); let offset = 0; const length = dataView.byteLength; let exifData = {}; try { switch (fileType) { case 'image/jpeg': case 'image/jpg': if (dataView.getUint16(0, false) !== 0xFFD8) { throw new Error('不是有效的 JPEG 图片'); } // 搜索 APP1 标记 offset = 2; while (offset < length) { const marker = dataView.getUint16(offset, false); offset += 2; if (marker === 0xFFE1) { const segmentLength = dataView.getUint16(offset, false); const exifHeader = this.getStringFromBuffer(dataView, offset + 2, 4); if (exifHeader === 'Exif') { const tiffOffset = offset + 8; const byteOrder = dataView.getUint16(tiffOffset, false); const bigEnd = byteOrder === 0x4D4D; if (dataView.getUint16(tiffOffset + 2, !bigEnd) === 42) { const firstIFDOffset = dataView.getUint32(tiffOffset + 4, !bigEnd); if (firstIFDOffset > 0) { this.readIFD(dataView, tiffOffset + firstIFDOffset, !bigEnd, exifData); } } } offset += segmentLength; } else if (marker === 0xFFDA) { break; // 到达图像数据 } else { offset += dataView.getUint16(offset, false); } } break; case 'image/tiff': this.parseTiffHeader(dataView, exifData); break; case 'image/heic': case 'image/heif': // HEIF 文件以 ftyp 开头 const brand = this.getStringFromBuffer(dataView, 4, 4); if (!['heic', 'heix', 'hevc', 'hevx'].includes(brand)) { throw new Error('不是有效的 HEIC/HEIF 图片'); } this.parseHeicMetadata(dataView, exifData); return exifData; case 'image/png': // PNG 文件以 89 50 4E 47 0D 0A 1A 0A 开头 if (dataView.getUint32(0, false) !== 0x89504E47) { throw new Error('不是有效的 PNG 图片'); } this.parsePngMetadata(dataView, exifData); return exifData; case 'image/webp': // WebP 文件以 RIFF....WEBP 开头 const webpHeader = this.getStringFromBuffer(dataView, 0, 4); if (webpHeader !== 'RIFF') { throw new Error('不是有效的 WebP 图片'); } this.parseWebpMetadata(dataView, exifData); return exifData; default: throw new Error(`不支持的文件类型: ${fileType}`); } return exifData; } catch (error) { console.error('解析元数据时出错:', error); return exifData; } }; ImageExifReader.prototype.parseTiffHeader = function(dataView, exifData) { try { // 检查字节序 const byteOrder = dataView.getUint16(0, false); const bigEnd = byteOrder === 0x4D4D; // 'MM' = big endian // 验证 TIFF 版本号 (应该是 42) const tiffMagic = dataView.getUint16(2, !bigEnd); if (tiffMagic !== 42) { throw new Error('无效的 TIFF 版本号'); } // 获取第一个 IFD 的偏移量 const ifdOffset = dataView.getUint32(4, !bigEnd); if (ifdOffset < 8 || ifdOffset > dataView.byteLength) { throw new Error('无效的 IFD 偏移量'); } // 读取 IFD this.readIFD(dataView, ifdOffset, !bigEnd, exifData); return exifData; } catch (error) { console.error('解析 TIFF 头部时出错:', error); return exifData; } }; ImageExifReader.prototype.parseHeicMetadata = function(dataView, exifData) { // HEIC/HEIF 解析逻辑 }; ImageExifReader.prototype.parsePngMetadata = function(dataView, exifData) { // PNG 解析逻辑 }; ImageExifReader.prototype.parseWebpMetadata = function(dataView, exifData) { // WebP 解析逻辑 }; ImageExifReader.prototype.readIFD = function(dataView, startOffset, bigEnd, exifData) { try { const entries = dataView.getUint16(startOffset, bigEnd); console.log(`IFD 位置: ${startOffset}, 条目数: ${entries}`); if (entries > 50 || entries === 0) { console.warn(`可疑的 IFD 条目数量: ${entries}`); return; } let offset = startOffset + 2; const baseOffset = startOffset - 8; for (let i = 0; i < entries; i++) { const tag = dataView.getUint16(offset, bigEnd); const type = dataView.getUint16(offset + 2, bigEnd); const numValues = dataView.getUint32(offset + 4, bigEnd); const valueOffset = dataView.getUint32(offset + 8, bigEnd); if (type < 1 || type > 12 || numValues > 65535 || valueOffset > dataView.byteLength) { offset += 12; continue; } try { if (this.tags[tag]) { const value = this.readTagValue(dataView, type, numValues, valueOffset, bigEnd, offset, tag); if (value !== undefined) { exifData[this.tags[tag]] = value; console.log(`读取标签 ${this.tags[tag]}: ${value}`); } } if (tag === 0x8769) { console.log('找到 EXIF IFD 指针,继续解析...'); const exifOffset = baseOffset + valueOffset; if (exifOffset > 0 && exifOffset < dataView.byteLength) { this.readIFD(dataView, exifOffset, bigEnd, exifData); } } } catch (e) { console.error(`处理标签 0x${tag.toString(16)} 时出错:`, e); } offset += 12; } } catch (e) { console.error('解析 IFD 时出错:', e); } }; ImageExifReader.prototype.readTagValue = function(dataView, type, numValues, valueOffset, bigEnd, baseOffset, tag) { try { const tagName = this.tags[tag]; const tagType = this.tagTypes[tagName]; const tiffOffset = 12; let actualOffset; if (type === 2) { actualOffset = numValues <= 4 ? baseOffset + 8 : tiffOffset + valueOffset; } else if (type === 5) { actualOffset = tiffOffset + valueOffset; } else { actualOffset = numValues <= 4 ? baseOffset + 8 : tiffOffset + valueOffset; } let value; switch (type) { case 2: // ascii string if (numValues <= 4) { const bytes = new Uint8Array(4); for (let i = 0; i < 4; i++) { bytes[i] = (valueOffset >> (i * 8)) & 0xFF; } value = this.decodeString(bytes.slice(0, numValues - 1)); } else { value = this.getStringFromBuffer(dataView, actualOffset, numValues - 1); } break; case 5: // unsigned rational try { const numerator = dataView.getUint32(actualOffset, bigEnd); const denominator = dataView.getUint32(actualOffset + 4, bigEnd); if (denominator === 0) { return undefined; } value = numerator / denominator; switch (tagName) { case 'FocalLength': if (value > 0 && value < 1000) { value = Math.round(value); } else { return undefined; } break; case 'FNumber': if (value > 0) { value = Math.round(value * 10) / 10; } else { return undefined; } break; case 'ExposureTime': if (value > 0) { if (value < 1) { const denomRounded = Math.round(1/value); value = `1/${denomRounded}`; } else { value = value.toFixed(1); } } else { return undefined; } break; case 'XResolution': case 'YResolution': if (value > 0) { value = Math.round(value); } else { return undefined; } break; } } catch (e) { console.error('读取 Rational 值时出错:', e); return undefined; } break; default: value = this.readBasicType(dataView, type, numValues, actualOffset, bigEnd); } if (value !== undefined && value !== '' && tagType) { switch (tagType) { case 'datetime': if (typeof value === 'string' && value.length >= 19) { const match = value.match(/(\d{4}):(\d{2}):(\d{2}) (\d{2}):(\d{2}):(\d{2})/); if (match) { value = `${match[1]}-${match[2]}-${match[3]} ${match[4]}:${match[5]}:${match[6]}`; } else { return undefined; } } else { return undefined; } break; case 'fnumber': value = `f/${value}`; break; case 'focallength': value = `${value}mm`; break; case 'string': value = this.cleanString(value); if (!value) { return undefined; } break; } } return value || undefined; } catch (error) { console.error('读取标签值时出错:', error, { tag: `0x${tag.toString(16)}`, tagName, type, numValues, valueOffset }); return undefined; } }; ImageExifReader.prototype.readBasicType = function(dataView, type, numValues, offset, bigEnd) { switch (type) { case 1: // unsigned byte if (numValues === 1) { return dataView.getUint8(offset); } var values = new Array(numValues); for (var i = 0; i < numValues; i++) { values[i] = dataView.getUint8(offset + i); } return values; case 3: // unsigned short if (numValues === 1) { return dataView.getUint16(offset, bigEnd); } var values = new Array(numValues); for (var i = 0; i < numValues; i++) { values[i] = dataView.getUint16(offset + i * 2, bigEnd); } return values; case 4: // unsigned long if (numValues === 1) { return dataView.getUint32(offset, bigEnd); } var values = new Array(numValues); for (var i = 0; i < numValues; i++) { values[i] = dataView.getUint32(offset + i * 4, bigEnd); } return values; default: return undefined; } }; ImageExifReader.prototype.getStringFromBuffer = function(dataView, start, length) { try { const bytes = []; for (let i = 0; i < length; i++) { const byte = dataView.getUint8(start + i); if (byte === 0) break; bytes.push(byte); } return this.decodeString(new Uint8Array(bytes)); } catch (error) { console.error('读取字符串时出错:', error, {start, length}); return ''; } }; // 改进字符串清理方法 ImageExifReader.prototype.cleanString = function(str) { if (typeof str !== 'string') return ''; // 移除控制字符、不可打印字符和特殊字符 return str.replace(/[\x00-\x1F\x7F-\x9F]/g, '') .replace(/[^\x20-\x7E\u4E00-\u9FFF]/g, '') // 只保留基本ASCII和中文字符 .replace(/\u0000/g, '') .trim(); }; // 改进字符串解码方法 ImageExifReader.prototype.decodeString = function(bytes) { try { // 兼容性更好的字符串解码 return bytes.reduce((str, byte) => { return str + String.fromCharCode(byte); }, ''); } catch (e) { console.error('字符串解码失败:', e); return ''; } }; // 添加一个辅助方法来检查数据块的有效性 ImageExifReader.prototype.isValidChunk = function(offset, length, totalLength) { return offset >= 0 && length > 0 && offset + length <= totalLength && length < totalLength; }; return ImageExifReader; }));