imageExifReader/imageExifReader.js
2025-01-03 19:07:17 +08:00

486 lines
18 KiB
JavaScript

(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;
}));