@eris/exif
Advanced tools
Comparing version 0.4.2-alpha.13 to 0.4.2-alpha.14
@@ -52,2 +52,3 @@ "use strict"; | ||
case types_1.IFDDataType.Undefined: | ||
case types_1.IFDDataType.Unknown: | ||
return ''; | ||
@@ -78,3 +79,3 @@ default: | ||
const dataReader = reader.readAsReader(4); | ||
log.verbose(`read tag ${tags_1.getFriendlyName(tag)} (${tag})`); | ||
log.verbose(`read tag ${tags_1.getFriendlyName(tag)} (tag: ${tag}, length: ${length})`); | ||
return new IFDEntry(startOffset, tag, dataType, length, dataReader); | ||
@@ -81,0 +82,0 @@ } |
@@ -154,3 +154,3 @@ "use strict"; | ||
static isJPEG(buffer) { | ||
return buffer[0] === 0xff && buffer[1] === 0xd8; | ||
return buffer[0] === 0xff && buffer[1] === 0xd8 && buffer[2] === 0xff; | ||
} | ||
@@ -157,0 +157,0 @@ static injectEXIFMetadata(jpegBuffer, exifBuffer) { |
@@ -6,2 +6,3 @@ /// <reference types="node" /> | ||
private readonly _reader; | ||
private _variant; | ||
private _ifds; | ||
@@ -15,2 +16,8 @@ private _cachedMetadata; | ||
private _readStripOffsetsAsJPEG; | ||
/** | ||
* Panasonic mislabels all of their tags so the names make no sense. | ||
* The only thing they really label is the offset of the RAW data and the JPEG is before that. | ||
* So we'll just search for JPEG markers in between the IFDs and the offset of the raw data. | ||
*/ | ||
private _readPanasonicJPEG; | ||
private _readLargestJPEG; | ||
@@ -17,0 +24,0 @@ extractJPEG(options?: IJPEGOptions): IBufferLike; |
@@ -11,3 +11,10 @@ "use strict"; | ||
const ifd_entry_1 = require("./ifd-entry"); | ||
const TIFF_MAGIC_VERSION = 42; | ||
var Variant; | ||
(function (Variant) { | ||
Variant["Standard"] = "standard"; | ||
Variant["Olympus"] = "olympus"; | ||
Variant["Panasonic"] = "panasonic"; | ||
Variant["Unknown"] = "unknown"; | ||
})(Variant || (Variant = {})); | ||
const TIFF_MAGIC_VERSION = 0x2a; | ||
/** | ||
@@ -18,2 +25,7 @@ * Olympus raw files are basically TIFFs but with a replaced magic text | ||
const OLYMPUS_MAGIC_VERSION = 0x4f52; | ||
/** | ||
* Panasonic raw files are basically TIFFs but with a replaced magic text | ||
* @see https://libopenraw.freedesktop.org/formats/rw2/ | ||
*/ | ||
const PANASONIC_MAGIC_VERSION = 0x55; | ||
const log = log_1.createLogger('decoder'); | ||
@@ -40,3 +52,9 @@ class TIFFDecoder { | ||
const version = this._reader.read(2); | ||
if (version !== TIFF_MAGIC_VERSION && version !== OLYMPUS_MAGIC_VERSION) { | ||
if (version === TIFF_MAGIC_VERSION) | ||
this._variant = Variant.Standard; | ||
if (version === OLYMPUS_MAGIC_VERSION) | ||
this._variant = Variant.Olympus; | ||
if (version === PANASONIC_MAGIC_VERSION) | ||
this._variant = Variant.Panasonic; | ||
if (this._variant === Variant.Unknown) { | ||
throw new Error(`Unrecognized TIFF version: ${version.toString(16)}`); | ||
@@ -122,2 +140,50 @@ } | ||
} | ||
/** | ||
* Panasonic mislabels all of their tags so the names make no sense. | ||
* The only thing they really label is the offset of the RAW data and the JPEG is before that. | ||
* So we'll just search for JPEG markers in between the IFDs and the offset of the raw data. | ||
*/ | ||
_readPanasonicJPEG() { | ||
if (this._variant !== Variant.Panasonic) | ||
return; | ||
const ifdEntries = this.extractIFDEntries(); | ||
const searchStart = Math.max(...ifdEntries.map(value => value.startOffset)); | ||
const endEntry = this._ifds[0].entries.find(entry => entry.tag === types_1.IFDTag.PanasonicJPEGEnd); | ||
if (!searchStart || !endEntry) | ||
return; | ||
const endEntryValue = endEntry.getValue(); | ||
if (typeof endEntryValue !== 'number') | ||
return; | ||
let startIndex = Infinity; | ||
let endIndex = Infinity; | ||
const buffer = this._reader.getBuffer(); | ||
for (let i = searchStart; i < endEntryValue; i++) { | ||
if (buffer[i] !== 0xff) | ||
continue; | ||
if (buffer[i + 1] !== 0xd8) | ||
continue; | ||
if (buffer[i + 2] !== 0xff) | ||
continue; | ||
startIndex = i; | ||
break; | ||
} | ||
for (let i = endEntryValue; i > searchStart; i--) { | ||
if (buffer[i - 1] !== 0xff) | ||
continue; | ||
if (buffer[i] !== 0xd9) | ||
continue; | ||
endIndex = i; | ||
break; | ||
} | ||
if (!Number.isFinite(startIndex)) | ||
return; | ||
if (!Number.isFinite(endIndex)) | ||
return; | ||
if (endIndex - startIndex < 4000) | ||
return; | ||
const jpegBuffer = buffer.slice(startIndex, endIndex + 1); | ||
if (!jpeg_decoder_1.JPEGDecoder.isJPEG(jpegBuffer)) | ||
return; | ||
return jpegBuffer; | ||
} | ||
_readLargestJPEG() { | ||
@@ -138,4 +204,8 @@ // Try to read the JPEG thumbnail first | ||
return maxThumbnailJPEG; | ||
// Try the manufacturer specific previews if none the standard ones worked | ||
const panasonicJPEG = this._readPanasonicJPEG(); | ||
if (panasonicJPEG) | ||
return panasonicJPEG; | ||
// Fail loudly if all else fails | ||
throw new Error('Could not find thumbnail or read StripOffsets IFDs'); | ||
throw new Error('Could not find an embedded JPEG'); | ||
} | ||
@@ -168,2 +238,6 @@ extractJPEG(options = {}) { | ||
target[name] = value; | ||
const panasonicName = tags_1.panasonicConversionTags[name]; | ||
if (this._variant === Variant.Panasonic && panasonicName) { | ||
target[panasonicName] = value; | ||
} | ||
}); | ||
@@ -170,0 +244,0 @@ }); |
@@ -7,2 +7,3 @@ import { IBufferLike, IReader, Endian } from '../utils/types'; | ||
constructor(_buffer: IBufferLike, _position?: number, _endianness?: Endian); | ||
getBuffer(): IBufferLike; | ||
hasNext(): boolean; | ||
@@ -9,0 +10,0 @@ getPosition(): number; |
@@ -10,2 +10,5 @@ "use strict"; | ||
} | ||
getBuffer() { | ||
return this._buffer; | ||
} | ||
hasNext() { | ||
@@ -12,0 +15,0 @@ return this._position < this._buffer.length; |
@@ -7,2 +7,3 @@ import { IIFDTagDefinition, IFDTagName, XMPTagName } from './types'; | ||
export declare const xmpTags: Record<XMPTagName, boolean>; | ||
export declare const panasonicConversionTags: Partial<Record<IFDTagName, IFDTagName>>; | ||
export declare function getFriendlyName(code: number): IFDTagName; |
@@ -14,2 +14,5 @@ "use strict"; | ||
}; | ||
exports.panasonicConversionTags = { | ||
GPSDestBearingRef: 'ISO', | ||
}; | ||
// TODO: fill in all IFDDataTypes with -1 | ||
@@ -16,0 +19,0 @@ const _tags = [ |
@@ -14,2 +14,3 @@ /// <reference types="node" /> | ||
export interface IReader { | ||
getBuffer(): IBufferLike; | ||
hasNext(): boolean; | ||
@@ -77,3 +78,4 @@ getPosition(): number; | ||
ThumbnailLength = 514, | ||
EXIFOffset = 34665 | ||
EXIFOffset = 34665, | ||
PanasonicJPEGEnd = 280 | ||
} | ||
@@ -80,0 +82,0 @@ export declare enum IFDGroup { |
@@ -23,2 +23,3 @@ "use strict"; | ||
IFDTag[IFDTag["EXIFOffset"] = 34665] = "EXIFOffset"; | ||
IFDTag[IFDTag["PanasonicJPEGEnd"] = 280] = "PanasonicJPEGEnd"; | ||
})(IFDTag = exports.IFDTag || (exports.IFDTag = {})); | ||
@@ -25,0 +26,0 @@ var IFDGroup; |
@@ -57,2 +57,3 @@ import {getFriendlyName} from '../utils/tags' | ||
case IFDDataType.Undefined: | ||
case IFDDataType.Unknown: | ||
return '' | ||
@@ -89,3 +90,3 @@ default: | ||
log.verbose(`read tag ${getFriendlyName(tag)} (${tag})`) | ||
log.verbose(`read tag ${getFriendlyName(tag)} (tag: ${tag}, length: ${length})`) | ||
return new IFDEntry(startOffset, tag, dataType, length, dataReader) | ||
@@ -92,0 +93,0 @@ } |
@@ -199,3 +199,3 @@ import {TIFFDecoder} from '../decoder/tiff-decoder' | ||
public static isJPEG(buffer: IBufferLike): boolean { | ||
return buffer[0] === 0xff && buffer[1] === 0xd8 | ||
return buffer[0] === 0xff && buffer[1] === 0xd8 && buffer[2] === 0xff | ||
} | ||
@@ -202,0 +202,0 @@ |
import {IFD} from '../decoder/ifd' | ||
import {getFriendlyName} from '../utils/tags' | ||
import {getFriendlyName, panasonicConversionTags} from '../utils/tags' | ||
import {Reader} from '../utils/reader' | ||
@@ -23,3 +23,10 @@ import {TIFFEncoder} from '../encoder/tiff-encoder' | ||
const TIFF_MAGIC_VERSION = 42 | ||
enum Variant { | ||
Standard = 'standard', | ||
Olympus = 'olympus', | ||
Panasonic = 'panasonic', | ||
Unknown = 'unknown', | ||
} | ||
const TIFF_MAGIC_VERSION = 0x2a | ||
/** | ||
@@ -30,2 +37,7 @@ * Olympus raw files are basically TIFFs but with a replaced magic text | ||
const OLYMPUS_MAGIC_VERSION = 0x4f52 | ||
/** | ||
* Panasonic raw files are basically TIFFs but with a replaced magic text | ||
* @see https://libopenraw.freedesktop.org/formats/rw2/ | ||
*/ | ||
const PANASONIC_MAGIC_VERSION = 0x55 | ||
@@ -43,2 +55,3 @@ const log = createLogger('decoder') | ||
private readonly _reader: IReader | ||
private _variant: Variant | ||
private _ifds: IIFD[] | ||
@@ -67,3 +80,6 @@ private _cachedMetadata: IGenericMetadata | ||
const version = this._reader.read(2) | ||
if (version !== TIFF_MAGIC_VERSION && version !== OLYMPUS_MAGIC_VERSION) { | ||
if (version === TIFF_MAGIC_VERSION) this._variant = Variant.Standard | ||
if (version === OLYMPUS_MAGIC_VERSION) this._variant = Variant.Olympus | ||
if (version === PANASONIC_MAGIC_VERSION) this._variant = Variant.Panasonic | ||
if (this._variant === Variant.Unknown) { | ||
throw new Error(`Unrecognized TIFF version: ${version.toString(16)}`) | ||
@@ -164,2 +180,45 @@ } | ||
/** | ||
* Panasonic mislabels all of their tags so the names make no sense. | ||
* The only thing they really label is the offset of the RAW data and the JPEG is before that. | ||
* So we'll just search for JPEG markers in between the IFDs and the offset of the raw data. | ||
*/ | ||
private _readPanasonicJPEG(): IBufferLike | undefined { | ||
if (this._variant !== Variant.Panasonic) return | ||
const ifdEntries = this.extractIFDEntries() | ||
const searchStart = Math.max(...ifdEntries.map(value => value.startOffset)) | ||
const endEntry = this._ifds[0].entries.find(entry => entry.tag === IFDTag.PanasonicJPEGEnd) | ||
if (!searchStart || !endEntry) return | ||
const endEntryValue = endEntry.getValue() | ||
if (typeof endEntryValue !== 'number') return | ||
let startIndex = Infinity | ||
let endIndex = Infinity | ||
const buffer = this._reader.getBuffer() | ||
for (let i = searchStart; i < endEntryValue; i++) { | ||
if (buffer[i] !== 0xff) continue | ||
if (buffer[i + 1] !== 0xd8) continue | ||
if (buffer[i + 2] !== 0xff) continue | ||
startIndex = i | ||
break | ||
} | ||
for (let i = endEntryValue; i > searchStart; i--) { | ||
if (buffer[i - 1] !== 0xff) continue | ||
if (buffer[i] !== 0xd9) continue | ||
endIndex = i | ||
break | ||
} | ||
if (!Number.isFinite(startIndex)) return | ||
if (!Number.isFinite(endIndex)) return | ||
if (endIndex - startIndex < 4000) return | ||
const jpegBuffer = buffer.slice(startIndex, endIndex + 1) | ||
if (!JPEGDecoder.isJPEG(jpegBuffer)) return | ||
return jpegBuffer | ||
} | ||
private _readLargestJPEG(): IBufferLike { | ||
@@ -178,4 +237,9 @@ // Try to read the JPEG thumbnail first | ||
if (maxThumbnailJPEG) return maxThumbnailJPEG | ||
// Try the manufacturer specific previews if none the standard ones worked | ||
const panasonicJPEG = this._readPanasonicJPEG() | ||
if (panasonicJPEG) return panasonicJPEG | ||
// Fail loudly if all else fails | ||
throw new Error('Could not find thumbnail or read StripOffsets IFDs') | ||
throw new Error('Could not find an embedded JPEG') | ||
} | ||
@@ -212,2 +276,7 @@ | ||
target[name] = value | ||
const panasonicName = panasonicConversionTags[name] | ||
if (this._variant === Variant.Panasonic && panasonicName) { | ||
target[panasonicName] = value | ||
} | ||
}) | ||
@@ -214,0 +283,0 @@ }) |
@@ -10,2 +10,6 @@ import {IBufferLike, IReader, Endian} from '../utils/types' | ||
public getBuffer(): IBufferLike { | ||
return this._buffer | ||
} | ||
public hasNext(): boolean { | ||
@@ -12,0 +16,0 @@ return this._position < this._buffer.length |
@@ -17,2 +17,9 @@ import {IIFDTagDefinition, IFDTagName, IFDGroup, IFDDataType, XMPTagName} from './types' | ||
export const panasonicConversionTags: Partial<Record<IFDTagName, IFDTagName>> = { | ||
GPSDestBearingRef: 'ISO', | ||
// Just figure these out with XNViewMP :/ | ||
// GPSLatitude: 'SensorWidth' | ||
// GPSLongitudeRef: 'SensorHeight | ||
} | ||
// TODO: fill in all IFDDataTypes with -1 | ||
@@ -19,0 +26,0 @@ const _tags: Array<[IFDTagName, number, IFDDataType, IFDGroup]> = [ |
@@ -17,2 +17,3 @@ export interface ILogger { | ||
export interface IReader { | ||
getBuffer(): IBufferLike | ||
hasNext(): boolean | ||
@@ -89,2 +90,3 @@ getPosition(): number | ||
EXIFOffset = 34665, | ||
PanasonicJPEGEnd = 280, // MinSampleValue normally, marks the start of the RAW data | ||
} | ||
@@ -91,0 +93,0 @@ |
{ | ||
"name": "@eris/exif", | ||
"version": "0.4.2-alpha.13", | ||
"version": "0.4.2-alpha.14", | ||
"description": "Parses EXIF data.", | ||
@@ -41,3 +41,3 @@ "main": "./dist/index.js", | ||
}, | ||
"gitHead": "88663db1a984e24d9a12a050161d805be91d8cb6" | ||
"gitHead": "7ef5d393ef281749cb3ba173abf70056f879dad5" | ||
} |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
270294
4928