@eris/exif
Advanced tools
Comparing version 0.2.1-alpha.7 to 0.2.1-alpha.8
@@ -9,8 +9,19 @@ import { IBufferLike, IGenericMetadata } from '../utils/types'; | ||
private _exifBuffers; | ||
private _xmpBuffers; | ||
constructor(buffer: IBufferLike); | ||
_readFileMarkers(): void; | ||
private _handleEXIFMarker; | ||
/** | ||
* @see https://en.wikipedia.org/wiki/Extensible_Metadata_Platform#Example | ||
* @see https://wwwimages2.adobe.com/content/dam/acom/en/devnet/xmp/pdfs/XMP%20SDK%20Release%20cc-2016-08/XMPSpecificationPart3.pdf | ||
*/ | ||
private _handleXMPMarker; | ||
private _handleNonAppMarker; | ||
private _handleMarker; | ||
private _readFileMarkers; | ||
extractMetadata(): IGenericMetadata; | ||
extractMetadataBuffer(): IBufferLike | undefined; | ||
extractEXIFBuffer(): IBufferLike | undefined; | ||
extractXMPBuffer(): IBufferLike | undefined; | ||
static isJPEG(buffer: IBufferLike): boolean; | ||
static injectMetadata(jpegBuffer: IBufferLike, exifBuffer: IBufferLike): IBufferLike; | ||
static injectEXIFMetadata(jpegBuffer: IBufferLike, exifBuffer: IBufferLike): IBufferLike; | ||
static injectXMPMetadata(jpegBuffer: IBufferLike, xmpBuffer: IBufferLike): IBufferLike; | ||
} |
@@ -7,3 +7,6 @@ "use strict"; | ||
const writer_1 = require("../utils/writer"); | ||
const xmp_decoder_1 = require("./xmp-decoder"); | ||
const EXIF_HEADER = 0x45786966; // "Exif" | ||
const XMP_HEADER = 0x68747470; // The "http" in "http://ns.adobe.com/xap/1.0/" | ||
const XMP_URL = 'http://ns.adobe.com/xap/1.0/\x00'; | ||
const APP1 = 0xffe1; | ||
@@ -27,2 +30,71 @@ const START_OF_IMAGE = 0xffd8; | ||
} | ||
_handleEXIFMarker(state) { | ||
const reader = this._reader; | ||
const lastMarker = this._markers[this._markers.length - 1]; | ||
const exifBuffers = this._exifBuffers; | ||
// mark the last marker as an EXIF marker | ||
lastMarker.isEXIF = true; | ||
// skip over the 4 header bytes and 2 empty bytes | ||
reader.skip(6); | ||
// the data is the rest of the marker (-6 for 2 empty bytes and 4 for EXIF header) | ||
exifBuffers.push(reader.readAsBuffer(state.length - 6)); | ||
return { nextMarker: reader.read(2) }; | ||
} | ||
/** | ||
* @see https://en.wikipedia.org/wiki/Extensible_Metadata_Platform#Example | ||
* @see https://wwwimages2.adobe.com/content/dam/acom/en/devnet/xmp/pdfs/XMP%20SDK%20Release%20cc-2016-08/XMPSpecificationPart3.pdf | ||
*/ | ||
_handleXMPMarker(state) { | ||
const reader = this._reader; | ||
const lastMarker = this._markers[this._markers.length - 1]; | ||
const xmpBuffers = this._xmpBuffers; | ||
// Let's double check we're actually looking at XMP data | ||
const fullHeader = reader.readAsBuffer(XMP_URL.length).toString(); | ||
if (fullHeader !== XMP_URL) { | ||
// We aren't actually looking at XMP data, let's abort | ||
reader.seek(state.nextPosition); | ||
return { nextMarker: reader.read(2) }; | ||
} | ||
xmpBuffers.push(reader.readAsBuffer(state.length - XMP_URL.length)); | ||
// mark the last marker as an XMP marker | ||
lastMarker.isXMP = true; | ||
return { nextMarker: reader.read(2) }; | ||
} | ||
_handleNonAppMarker(state) { | ||
const reader = this._reader; | ||
const { marker, nextPosition } = state; | ||
// Skip through the other header payloads that aren't APP1 | ||
// Width and Height information will be in the Start Of Frame (SOFx) payloads | ||
if (marker === START_OF_FRAME0 || marker === START_OF_FRAME1 || marker === START_OF_FRAME2) { | ||
reader.skip(1); | ||
this._height = reader.read(2); | ||
this._width = reader.read(2); | ||
} | ||
reader.seek(nextPosition); | ||
return { nextMarker: reader.read(2) }; | ||
} | ||
_handleMarker(state) { | ||
const reader = this._reader; | ||
const { marker, nextPosition } = state; | ||
if (marker === APP1) { | ||
// Read the EXIF/XMP data from APP1 Marker | ||
const header = reader.use(() => reader.read(4)); | ||
if (header === EXIF_HEADER) { | ||
return this._handleEXIFMarker(state); | ||
} | ||
else if (header === XMP_HEADER) { | ||
return this._handleXMPMarker(state); | ||
} | ||
else { | ||
reader.seek(nextPosition); | ||
return { nextMarker: reader.read(2) }; | ||
} | ||
} | ||
else if (marker >> 8 === 0xff) { | ||
return this._handleNonAppMarker(state); | ||
} | ||
else { | ||
throw new Error(`Unrecognized marker: ${marker.toString(16)}`); | ||
} | ||
} | ||
_readFileMarkers() { | ||
@@ -32,5 +104,7 @@ if (this._markers) { | ||
} | ||
const markers = [[START_OF_IMAGE, Buffer.from([]), false]]; | ||
const baseMarker = { isEXIF: false, isXMP: false }; | ||
const reader = this._reader; | ||
const exifBuffers = []; | ||
this._markers = [Object.assign({ marker: START_OF_IMAGE, buffer: Buffer.from([]) }, baseMarker)]; | ||
this._exifBuffers = []; | ||
this._xmpBuffers = []; | ||
reader.seek(2); | ||
@@ -47,43 +121,9 @@ let marker = reader.read(2); | ||
// Push the marker and data onto our markers list | ||
markers.push([marker, markerBuffer, false]); | ||
this._markers.push(Object.assign({ marker, buffer: markerBuffer }, baseMarker)); | ||
// Skip over the length we just read | ||
reader.skip(2); | ||
if (marker === APP1) { | ||
// Read the EXIF data from APP1 Marker | ||
const nextPosition = reader.getPosition() + length; | ||
const header = reader.read(4); | ||
if (header !== EXIF_HEADER) { | ||
reader.seek(nextPosition); | ||
marker = reader.read(2); | ||
continue; | ||
} | ||
// mark the last marker as an EXIF marker | ||
markers[markers.length - 1][2] = true; | ||
// skip over the 2 empty bytes | ||
reader.skip(2); | ||
// the data is the rest of the marker (-6 for 2 empty bytes and 4 for EXIF header) | ||
exifBuffers.push(reader.readAsBuffer(length - 6)); | ||
marker = reader.read(2); | ||
} | ||
else if (marker >> 8 === 0xff) { | ||
// Skip through the other header payloads that aren't APP1 | ||
const nextPosition = reader.getPosition() + length; | ||
// Width and Height information will be in the Start Of Frame (SOFx) payloads | ||
if (marker === START_OF_FRAME0 || | ||
marker === START_OF_FRAME1 || | ||
marker === START_OF_FRAME2) { | ||
reader.skip(1); | ||
this._height = reader.read(2); | ||
this._width = reader.read(2); | ||
} | ||
reader.seek(nextPosition); | ||
marker = reader.read(2); | ||
} | ||
else { | ||
throw new Error(`Unrecognized marker: ${marker.toString(16)}`); | ||
} | ||
const nextPosition = reader.getPosition() + length; | ||
marker = this._handleMarker({ marker, nextPosition, length }).nextMarker; | ||
} | ||
markers.push([marker, this._buffer.slice(reader.getPosition()), false]); | ||
this._markers = markers; | ||
this._exifBuffers = exifBuffers; | ||
this._markers.push(Object.assign({ marker, buffer: this._buffer.slice(reader.getPosition()) }, baseMarker)); | ||
} | ||
@@ -100,20 +140,32 @@ extractMetadata() { | ||
} | ||
for (const xmpBuffer of this._xmpBuffers) { | ||
const decoder = new xmp_decoder_1.XMPDecoder(xmpBuffer); | ||
Object.assign(metadata, decoder.extractMetadata()); | ||
} | ||
return metadata; | ||
} | ||
extractMetadataBuffer() { | ||
extractEXIFBuffer() { | ||
this._readFileMarkers(); | ||
return this._exifBuffers[0]; | ||
} | ||
extractXMPBuffer() { | ||
this._readFileMarkers(); | ||
return this._xmpBuffers[0]; | ||
} | ||
static isJPEG(buffer) { | ||
return buffer[0] === 0xff && buffer[1] === 0xd8; | ||
} | ||
static injectMetadata(jpegBuffer, exifBuffer) { | ||
static injectEXIFMetadata(jpegBuffer, exifBuffer) { | ||
const decoder = new JPEGDecoder(jpegBuffer); | ||
decoder._readFileMarkers(); | ||
const hasEXIFDataAlready = decoder._markers.some(marker => marker.isEXIF); | ||
const buffers = []; | ||
for (const [marker, buffer, isEXIF] of decoder._markers) { | ||
if (marker === START_OF_IMAGE) { | ||
buffers.push(bufferFromNumber(START_OF_IMAGE)); | ||
for (const { marker, buffer, isEXIF } of decoder._markers) { | ||
if (isEXIF || (marker === START_OF_IMAGE && !hasEXIFDataAlready)) { | ||
if (marker === START_OF_IMAGE) | ||
buffers.push(bufferFromNumber(START_OF_IMAGE)); | ||
buffers.push(bufferFromNumber(APP1)); | ||
buffers.push(bufferFromNumber(exifBuffer.length + 8)); | ||
// add 8 bytes to the buffer length | ||
// 4 bytes for header, 2 bytes of empty space, 2 bytes for length itself | ||
buffers.push(bufferFromNumber(exifBuffer.length + 8, 2)); | ||
buffers.push(bufferFromNumber(EXIF_HEADER, 4)); | ||
@@ -123,3 +175,3 @@ buffers.push(bufferFromNumber(0, 2)); | ||
} | ||
else if (!isEXIF) { | ||
else { | ||
buffers.push(bufferFromNumber(marker), buffer); | ||
@@ -131,4 +183,26 @@ } | ||
} | ||
static injectXMPMetadata(jpegBuffer, xmpBuffer) { | ||
const decoder = new JPEGDecoder(jpegBuffer); | ||
decoder._readFileMarkers(); | ||
const hasXMPDataAlready = decoder._markers.some(marker => marker.isXMP); | ||
const buffers = []; | ||
for (const { marker, buffer, isXMP } of decoder._markers) { | ||
if (isXMP || (marker === START_OF_IMAGE && !hasXMPDataAlready)) { | ||
if (marker === START_OF_IMAGE) | ||
buffers.push(bufferFromNumber(START_OF_IMAGE)); | ||
buffers.push(bufferFromNumber(APP1)); | ||
// add 2 bytes to the buffer length for length itself | ||
buffers.push(bufferFromNumber(xmpBuffer.length + XMP_URL.length + 2, 2)); | ||
buffers.push(Buffer.from(XMP_URL)); | ||
buffers.push(xmpBuffer); | ||
} | ||
else { | ||
buffers.push(bufferFromNumber(marker), buffer); | ||
} | ||
} | ||
// @ts-ignore - TODO investigate why this is error-y | ||
return Buffer.concat(buffers); | ||
} | ||
} | ||
exports.JPEGDecoder = JPEGDecoder; | ||
//# sourceMappingURL=jpeg-decoder.js.map |
@@ -119,3 +119,3 @@ "use strict"; | ||
const metadataBuffer = tiff_encoder_1.TIFFEncoder.encode(metadata); | ||
this._cachedJPEG = jpeg_decoder_1.JPEGDecoder.injectMetadata(jpeg, metadataBuffer); | ||
this._cachedJPEG = jpeg_decoder_1.JPEGDecoder.injectEXIFMetadata(jpeg, metadataBuffer); | ||
return this._cachedJPEG.slice(); | ||
@@ -122,0 +122,0 @@ } |
import { IBufferLike, IGenericMetadata } from '../utils/types'; | ||
export declare class XMPDecoder { | ||
private static _tagsByLowerCaseKey; | ||
private readonly _text; | ||
constructor(buffer: IBufferLike); | ||
private _precomputeIfdTags; | ||
private _processRdfDescription; | ||
private _handleMatch; | ||
extractMetadata(): IGenericMetadata; | ||
static isXMP(buffer: IBufferLike): boolean; | ||
} |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
const tags_1 = require("../utils/tags"); | ||
const EXIF_ATTR_GLOBAL_REGEX = /(exif|tiff):([0-9a-z]+?)="(.*?)"/gim; | ||
const EXIF_ATTR_REGEX = /f:([0-9a-z]+?)="(.*?)"/i; | ||
const EXIF_ATTR_GLOBAL_REGEX = /(xmp|exif|tiff):([0-9a-z]+?)="(.*?)"/gim; | ||
const EXIF_ATTR_REGEX = /:([0-9a-z]+?)="(.*?)"$/i; | ||
function isSimpleNumber(s) { | ||
@@ -15,39 +15,23 @@ return /^(([1-9]\d*)|0)$/.test(s); | ||
this._text = buffer.toString(); | ||
this._precomputeIfdTags(); | ||
} | ||
_precomputeIfdTags() { | ||
if (XMPDecoder._tagsByLowerCaseKey) | ||
_handleMatch(key, value, genericMetadata) { | ||
// TODO: support mixed case in the XMP | ||
if (!(key in tags_1.tags) && !(key in tags_1.xmpTags)) | ||
return; | ||
const tagsByLowerCaseKey = {}; | ||
for (const tagName of Object.keys(tags_1.tags)) { | ||
tagsByLowerCaseKey[tagName.toLowerCase()] = tags_1.tags[tagName]; | ||
const knownKey = key; | ||
let realValue; | ||
if (isSimpleNumber(value)) { | ||
realValue = Number(value); | ||
} | ||
XMPDecoder._tagsByLowerCaseKey = tagsByLowerCaseKey; | ||
} | ||
_processRdfDescription(attributes, genericMetadata) { | ||
for (const key of Object.keys(attributes)) { | ||
if (!key.startsWith('exif:') && !key.startsWith('tiff:')) | ||
continue; | ||
const exifLowercaseTagName = key.slice(5); | ||
const ifdDefinition = XMPDecoder._tagsByLowerCaseKey[exifLowercaseTagName]; | ||
if (!ifdDefinition) | ||
continue; | ||
const value = attributes[key]; | ||
let realValue; | ||
if (isSimpleNumber(value)) { | ||
realValue = Number(value); | ||
} | ||
else if (isComplexNumber(value)) { | ||
const [numerator, denominator] = value.split('/'); | ||
realValue = Number(numerator) / Number(denominator); | ||
} | ||
else { | ||
realValue = value; | ||
} | ||
genericMetadata[ifdDefinition.name] = realValue; | ||
else if (isComplexNumber(value)) { | ||
const [numerator, denominator] = value.split('/'); | ||
realValue = Number(numerator) / Number(denominator); | ||
} | ||
else { | ||
realValue = value; | ||
} | ||
genericMetadata[knownKey] = realValue; | ||
} | ||
extractMetadata() { | ||
const metadata = {}; | ||
const attributes = {}; | ||
const matches = this._text.match(EXIF_ATTR_GLOBAL_REGEX); | ||
@@ -57,9 +41,14 @@ for (const match of matches || []) { | ||
const [_, key, value] = match.match(EXIF_ATTR_REGEX); | ||
attributes[`exif:${key.toLowerCase()}`] = value; | ||
this._handleMatch(key, value, metadata); | ||
} | ||
this._processRdfDescription(attributes, metadata); | ||
return metadata; | ||
} | ||
static isXMP(buffer) { | ||
return buffer[0] === '<'.charCodeAt(0); | ||
const xmpHeader = '<x:xmpmet'; | ||
const xmpAltHeader = '<?xpacket'; | ||
for (let i = 0; i < xmpHeader.length; i++) { | ||
if (buffer[i] !== xmpHeader.charCodeAt(i) && buffer[i] !== xmpAltHeader.charCodeAt(i)) | ||
return false; | ||
} | ||
return true; | ||
} | ||
@@ -66,0 +55,0 @@ } |
import { IGenericMetadata, IBufferLike, IIFDTagDefinition } from '../utils/types'; | ||
export declare class TIFFEncoder { | ||
static isSupportedEntry(tag: IIFDTagDefinition, value: any): boolean; | ||
static isSupportedEntry(tag: IIFDTagDefinition | undefined, value: any): boolean; | ||
static encode(metadata: IGenericMetadata): IBufferLike; | ||
} |
@@ -7,2 +7,3 @@ "use strict"; | ||
exports.TIFFDecoder = tiff_decoder_1.TIFFDecoder; | ||
const xmp_decoder_1 = require("./decoder/xmp-decoder"); | ||
const tiff_encoder_1 = require("./encoder/tiff-encoder"); | ||
@@ -28,2 +29,5 @@ exports.TIFFEncoder = tiff_encoder_1.TIFFEncoder; | ||
} | ||
else if (xmp_decoder_1.XMPDecoder.isXMP(bufferOrDecoder)) { | ||
return new xmp_decoder_1.XMPDecoder(bufferOrDecoder); | ||
} | ||
else { | ||
@@ -30,0 +34,0 @@ throw new Error('Unrecognizable file type'); |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
const isColonDate = (s) => /^\d+:\d+:\d+ \d+:\d+:\d+$/.test(s); | ||
const isISODateWithTz = (s) => /^\d{4}-\d{2}-\d{2}T[0-9.:]+(-|\+)\d{2}:\d{2}/.test(s); | ||
const isISODate = (s) => /^\d{4}-\d{2}-\d{2}T/.test(s); | ||
function parseNumericDate(timestamp) { | ||
@@ -13,2 +15,3 @@ return new Date(timestamp * 1000); | ||
} | ||
// TODO: Accept optional target timezone instead of assuming GMT with appending `Z` | ||
function parseDate(date) { | ||
@@ -22,2 +25,8 @@ let parsed = undefined; | ||
} | ||
else if (isISODateWithTz(date)) { | ||
parsed = new Date(date); | ||
} | ||
else if (isISODate(date)) { | ||
parsed = new Date(`${date}Z`); | ||
} | ||
return parsed && parsed.getTime() ? parsed : undefined; | ||
@@ -24,0 +33,0 @@ } |
@@ -22,2 +22,4 @@ "use strict"; | ||
lens: [lens_parser_1.parseLens], | ||
rating: ['Rating'], | ||
colorLabel: ['Label'], | ||
}; | ||
@@ -52,2 +54,7 @@ function getResultValue(item, results) { | ||
} | ||
if ((results.Orientation || 0) > 4) { | ||
const height = output.width; | ||
output.width = output.height; | ||
output.height = height; | ||
} | ||
return output; | ||
@@ -54,0 +61,0 @@ } |
@@ -1,2 +0,2 @@ | ||
import { IIFDTagDefinition, IFDTagName } from './types'; | ||
import { IIFDTagDefinition, IFDTagName, XMPTagName } from './types'; | ||
export declare const tags: Record<IFDTagName, IIFDTagDefinition>; | ||
@@ -6,2 +6,3 @@ export declare const tagsByCode: { | ||
}; | ||
export declare const xmpTags: Record<XMPTagName, boolean>; | ||
export declare function getFriendlyName(code: number): IFDTagName; |
@@ -8,2 +8,7 @@ "use strict"; | ||
exports.tagsByCode = {}; | ||
exports.xmpTags = { | ||
Rating: true, | ||
Label: true, | ||
MetadataDate: true, | ||
}; | ||
// TODO: fill in all IFDDataTypes with -1 | ||
@@ -10,0 +15,0 @@ const _tags = [ |
@@ -94,3 +94,3 @@ /// <reference types="node" /> | ||
} | ||
export declare type IGenericMetadata = Partial<Record<IFDTagName, string | number | undefined>>; | ||
export declare type IGenericMetadata = Partial<Record<IFDTagName | XMPTagName, string | number | undefined>>; | ||
export interface IParsedLens { | ||
@@ -119,4 +119,7 @@ make?: string; | ||
lens?: IParsedLens; | ||
rating?: number; | ||
colorLabel?: 'Blue' | 'Red' | 'Purple' | 'Yellow' | 'Green'; | ||
} | ||
export declare function getDataTypeSize(dataType: number, name?: string | number): number; | ||
export declare type XMPTagName = 'Rating' | 'Label' | 'MetadataDate'; | ||
export declare type IFDTagName = 'Unknown' | 'ImageWidth' | 'NewSubfileType' | 'SubfileType' | 'ImageWidth' | 'ImageLength' | 'BitsPerSample' | 'Compression' | 'PhotometricInterpretation' | 'Thresholding' | 'CellWidth' | 'CellLength' | 'FillOrder' | 'DocumentName' | 'ImageDescription' | 'Make' | 'Model' | 'StripOffsets' | 'Orientation' | 'SamplesPerPixel' | 'RowsPerStrip' | 'StripByteCounts' | 'MinSampleValue' | 'MaxSampleValue' | 'XResolution' | 'YResolution' | 'PlanarConfiguration' | 'PageName' | 'XPosition' | 'YPosition' | 'FreeOffsets' | 'FreeByteCounts' | 'GrayResponseUnit' | 'GrayResponseCurve' | 'T4Options' | 'T6Options' | 'ResolutionUnit' | 'PageNumber' | 'ColorResponseUnit' | 'TransferFunction' | 'Software' | 'ModifyDate' | 'Artist' | 'HostComputer' | 'Predictor' | 'WhitePoint' | 'PrimaryChromaticities' | 'ColorMap' | 'HalftoneHints' | 'TileWidth' | 'TileLength' | 'TileOffsets' | 'TileByteCounts' | 'BadFaxLines' | 'CleanFaxData' | 'ConsecutiveBadFaxLines' | 'SubIFD' | 'InkSet' | 'InkNames' | 'NumberOfInks' | 'DotRange' | 'TargetPrinter' | 'ExtraSamples' | 'SampleFormat' | 'SMinSampleValue' | 'SMaxSampleValue' | 'TransferRange' | 'ClipPath' | 'XClipPathUnits' | 'YClipPathUnits' | 'Indexed' | 'JPEGTables' | 'OPIProxy' | 'GlobalParametersIFD' | 'ProfileType' | 'FaxProfile' | 'CodingMethods' | 'VersionYear' | 'ModeNumber' | 'Decode' | 'DefaultImageColor' | 'T82Options' | 'JPEGProc' | 'JPEGInterchangeFormat' | 'JPEGInterchangeFormatLength' | 'JPEGRestartInterval' | 'JPEGLosslessPredictors' | 'JPEGPointTransforms' | 'JPEGQTables' | 'JPEGDCTables' | 'JPEGACTables' | 'YCbCrCoefficients' | 'YCbCrSubSampling' | 'YCbCrPositioning' | 'ReferenceBlackWhite' | 'StripRowCounts' | 'XMLPacket' | 'USPTOMiscellaneous' | 'RelatedImageFileFormat' | 'RelatedImageWidth' | 'RelatedImageHeight' | 'Rating' | 'XP_DIP_XML' | 'StitchInfo' | 'RatingPercent' | 'ImageID' | 'WangTag1' | 'WangAnnotation' | 'WangTag3' | 'WangTag4' | 'Matteing' | 'DataType' | 'ImageDepth' | 'TileDepth' | 'Model2' | 'CFARepeatPatternDim' | 'CFAPattern' | 'BatteryLevel' | 'KodakIFD' | 'Copyright' | 'ExposureTime' | 'FNumber' | 'MDFileTag' | 'MDScalePixel' | 'MDColorTable' | 'MDLabName' | 'MDSampleInfo' | 'MDPrepDate' | 'MDPrepTime' | 'MDFileUnits' | 'PixelScale' | 'AdventScale' | 'AdventRevision' | 'UIC1Tag' | 'UIC2Tag' | 'UIC3Tag' | 'UIC4Tag' | 'IPTC-NAA' | 'IntergraphPacketData' | 'IntergraphFlagRegisters' | 'IntergraphMatrix' | 'INGRReserved' | 'ModelTiePoint' | 'Site' | 'ColorSequence' | 'IT8Header' | 'RasterPadding' | 'BitsPerRunLength' | 'BitsPerExtendedRunLength' | 'ColorTable' | 'ImageColorIndicator' | 'BackgroundColorIndicator' | 'ImageColorValue' | 'BackgroundColorValue' | 'PixelIntensityRange' | 'TransparencyIndicator' | 'ColorCharacterization' | 'HCUsage' | 'TrapIndicator' | 'CMYKEquivalent' | 'SEMInfo' | 'AFCP_IPTC' | 'PixelMagicJBIGOptions' | 'ModelTransform' | 'WB_GRGBLevels' | 'LeafData' | 'PhotoshopSettings' | 'EXIFTag' | 'InterColorProfile' | 'TIFF_FXExtensions' | 'MultiProfiles' | 'SharedData' | 'T88Options' | 'ImageLayer' | 'GeoTiffDirectory' | 'GeoTiffDoubleParams' | 'GeoTiffAsciiParams' | 'ExposureProgram' | 'SpectralSensitivity' | 'GPSTag' | 'ISO' | 'Opto-ElectricConvFactor' | 'Interlace' | 'TimeZoneOffset' | 'SelfTimerMode' | 'SensitivityType' | 'StandardOutputSensitivity' | 'RecommendedExposureIndex' | 'ISOSpeed' | 'ISOSpeedLatitudeyyy' | 'ISOSpeedLatitudezzz' | 'FaxRecvParams' | 'FaxSubAddress' | 'FaxRecvTime' | 'LeafSubIFD' | 'EXIFVersion' | 'DateTimeOriginal' | 'CreateDate' | 'ComponentsConfiguration' | 'CompressedBitsPerPixel' | 'ShutterSpeedValue' | 'ApertureValue' | 'BrightnessValue' | 'ExposureCompensation' | 'MaxApertureValue' | 'SubjectDistance' | 'MeteringMode' | 'LightSource' | 'Flash' | 'FocalLength' | 'FlashEnergy' | 'SpatialFrequencyResponse' | 'Noise' | 'FocalPlaneXResolution' | 'FocalPlaneYResolution' | 'FocalPlaneResolutionUnit' | 'ImageNumber' | 'SecurityClassification' | 'ImageHistory' | 'SubjectArea' | 'ExposureIndex' | 'TIFFEPStandardID' | 'SensingMethod' | 'CIP3DataFile' | 'CIP3Sheet' | 'CIP3Side' | 'StoNits' | 'MakerNote' | 'UserComment' | 'SubSecTime' | 'SubSecTimeOriginal' | 'SubSecTimeDigitized' | 'MSDocumentText' | 'MSPropertySetStorage' | 'MSDocumentTextPosition' | 'ImageSourceData' | 'XPTitle' | 'XPComment' | 'XPAuthor' | 'XPKeywords' | 'XPSubject' | 'FlashpixVersion' | 'ColorSpace' | 'EXIFImageWidth' | 'EXIFImageHeight' | 'RelatedSoundFile' | 'InteropOffset' | 'SubjectLocation' | 'TIFF-EPStandardID' | 'FileSource' | 'SceneType' | 'CustomRendered' | 'ExposureMode' | 'WhiteBalance' | 'DigitalZoomRatio' | 'FocalLengthIn35mmFormat' | 'SceneCaptureType' | 'GainControl' | 'Contrast' | 'Saturation' | 'Sharpness' | 'DeviceSettingDescription' | 'SubjectDistanceRange' | 'ImageUniqueID' | 'OwnerName' | 'SerialNumber' | 'LensMake' | 'LensModel' | 'LensSerialNumber' | 'GDALMetadata' | 'GDALNoData' | 'Gamma' | 'ExpandSoftware' | 'ExpandLens' | 'ExpandFilm' | 'ExpandFilterLens' | 'ExpandScanner' | 'ExpandFlashLamp' | 'PixelFormat' | 'Transformation' | 'Uncompressed' | 'ImageType' | 'WidthResolution' | 'HeightResolution' | 'ImageOffset' | 'ImageByteCount' | 'AlphaOffset' | 'AlphaByteCount' | 'ImageDataDiscard' | 'AlphaDataDiscard' | 'OceScanjobDesc' | 'OceApplicationSelector' | 'OceIDNumber' | 'OceImageLogic' | 'Annotations' | 'PrintImageMatching' | 'USPTOOriginalContentType' | 'DNGVersion' | 'DNGBackwardVersion' | 'UniqueCameraModel' | 'LocalizedCameraModel' | 'CFAPlaneColor' | 'CFALayout' | 'LinearizationTable' | 'BlackLevelRepeatDim' | 'BlackLevel' | 'BlackLevelDeltaH' | 'BlackLevelDeltaV' | 'WhiteLevel' | 'DefaultScale' | 'DefaultCropOrigin' | 'DefaultCropSize' | 'ColorMatrix1' | 'ColorMatrix2' | 'CameraCalibration1' | 'CameraCalibration2' | 'ReductionMatrix1' | 'ReductionMatrix2' | 'AnalogBalance' | 'AsShotNeutral' | 'AsShotWhiteXY' | 'BaselineExposure' | 'BaselineNoise' | 'BaselineSharpness' | 'BayerGreenSplit' | 'LinearResponseLimit' | 'CameraSerialNumber' | 'LensInfo' | 'ChromaBlurRadius' | 'AntiAliasStrength' | 'ShadowScale' | 'DNGPrivateData' | 'MakerNoteSafety' | 'RawImageSegmentation' | 'CalibrationIlluminant1' | 'CalibrationIlluminant2' | 'BestQualityScale' | 'RawDataUniqueID' | 'AliasLayerMetadata' | 'OriginalRawFileName' | 'OriginalRawFileData' | 'ActiveArea' | 'MaskedAreas' | 'AsShotICCProfile' | 'AsShotPreProfileMatrix' | 'CurrentICCProfile' | 'CurrentPreProfileMatrix' | 'ColorimetricReference' | 'PanasonicTitle' | 'PanasonicTitle2' | 'CameraCalibrationSignature' | 'ProfileCalibrationSignature' | 'ProfileIFD' | 'AsShotProfileName' | 'NoiseReductionApplied' | 'ProfileName' | 'ProfileHueSatMapDims' | 'ProfileHueSatMapData1' | 'ProfileHueSatMapData2' | 'ProfileToneCurve' | 'ProfileEmbedPolicy' | 'ProfileCopyright' | 'ForwardMatrix1' | 'ForwardMatrix2' | 'PreviewApplicationName' | 'PreviewApplicationVersion' | 'PreviewSettingsName' | 'PreviewSettingsDigest' | 'PreviewColorSpace' | 'PreviewDateTime' | 'RawImageDigest' | 'OriginalRawFileDigest' | 'SubTileBlockSize' | 'RowInterleaveFactor' | 'ProfileLookTableDims' | 'ProfileLookTableData' | 'OpcodeList1' | 'OpcodeList2' | 'OpcodeList3' | 'NoiseProfile' | 'TimeCodes' | 'FrameRate' | 'TStop' | 'ReelName' | 'OriginalDefaultFinalSize' | 'OriginalBestQualitySize' | 'OriginalDefaultCropSize' | 'CameraLabel' | 'ProfileHueSatMapEncoding' | 'ProfileLookTableEncoding' | 'BaselineExposureOffset' | 'DefaultBlackRender' | 'NewRawImageDigest' | 'RawToPreviewGain' | 'DefaultUserCrop' | 'Padding' | 'OffsetSchema' | 'OwnerName' | 'SerialNumber' | 'Lens' | 'KDC_IFD' | 'RawFile' | 'Converter' | 'WhiteBalance' | 'Exposure' | 'Shadows' | 'Brightness' | 'Contrast' | 'Saturation' | 'Sharpness' | 'Smoothness' | 'MoireFilter' | 'GPSVersionID' | 'GPSLatitudeRef' | 'GPSLatitude' | 'GPSLongitudeRef' | 'GPSLongitude' | 'GPSAltitudeRef' | 'GPSAltitude' | 'GPSTimeStamp' | 'GPSSatellites' | 'GPSStatus' | 'GPSMeasureMode' | 'GPSDOP' | 'GPSSpeedRef' | 'GPSSpeed' | 'GPSTrackRef' | 'GPSTrack' | 'GPSImgDirectionRef' | 'GPSImgDirection' | 'GPSMapDatum' | 'GPSDestLatitudeRef' | 'GPSDestLatitude' | 'GPSDestLongitudeRef' | 'GPSDestLongitude' | 'GPSDestBearingRef' | 'GPSDestBearing' | 'GPSDestDistanceRef' | 'GPSDestDistance' | 'GPSProcessingMethod' | 'GPSAreaInformation' | 'GPSDateStamp' | 'GPSDifferential' | 'GPSHPositioningError'; |
@@ -5,4 +5,7 @@ import {TIFFDecoder} from '../decoder/tiff-decoder' | ||
import {Writer} from '../utils/writer' | ||
import {XMPDecoder} from './xmp-decoder' | ||
const EXIF_HEADER = 0x45786966 // "Exif" | ||
const XMP_HEADER = 0x68747470 // The "http" in "http://ns.adobe.com/xap/1.0/" | ||
const XMP_URL = 'http://ns.adobe.com/xap/1.0/\x00' | ||
const APP1 = 0xffe1 | ||
@@ -22,4 +25,15 @@ const START_OF_IMAGE = 0xffd8 | ||
type Marker = [number, IBufferLike, boolean] | ||
interface Marker { | ||
marker: number | ||
buffer: IBufferLike | ||
isEXIF: boolean | ||
isXMP: boolean | ||
} | ||
interface DecoderState { | ||
marker: number | ||
length: number | ||
nextPosition: number | ||
} | ||
export class JPEGDecoder { | ||
@@ -33,2 +47,3 @@ private readonly _buffer: IBufferLike | ||
private _exifBuffers: IBufferLike[] | undefined | ||
private _xmpBuffers: IBufferLike[] | undefined | ||
@@ -41,3 +56,79 @@ public constructor(buffer: IBufferLike) { | ||
public _readFileMarkers(): void { | ||
private _handleEXIFMarker(state: DecoderState): {nextMarker: number} { | ||
const reader = this._reader | ||
const lastMarker = this._markers![this._markers!.length - 1] | ||
const exifBuffers = this._exifBuffers! | ||
// mark the last marker as an EXIF marker | ||
lastMarker.isEXIF = true | ||
// skip over the 4 header bytes and 2 empty bytes | ||
reader.skip(6) | ||
// the data is the rest of the marker (-6 for 2 empty bytes and 4 for EXIF header) | ||
exifBuffers.push(reader.readAsBuffer(state.length - 6)) | ||
return {nextMarker: reader.read(2)} | ||
} | ||
/** | ||
* @see https://en.wikipedia.org/wiki/Extensible_Metadata_Platform#Example | ||
* @see https://wwwimages2.adobe.com/content/dam/acom/en/devnet/xmp/pdfs/XMP%20SDK%20Release%20cc-2016-08/XMPSpecificationPart3.pdf | ||
*/ | ||
private _handleXMPMarker(state: DecoderState): {nextMarker: number} { | ||
const reader = this._reader | ||
const lastMarker = this._markers![this._markers!.length - 1] | ||
const xmpBuffers = this._xmpBuffers! | ||
// Let's double check we're actually looking at XMP data | ||
const fullHeader = reader.readAsBuffer(XMP_URL.length).toString() | ||
if (fullHeader !== XMP_URL) { | ||
// We aren't actually looking at XMP data, let's abort | ||
reader.seek(state.nextPosition) | ||
return {nextMarker: reader.read(2)} | ||
} | ||
xmpBuffers.push(reader.readAsBuffer(state.length - XMP_URL.length)) | ||
// mark the last marker as an XMP marker | ||
lastMarker.isXMP = true | ||
return {nextMarker: reader.read(2)} | ||
} | ||
private _handleNonAppMarker(state: DecoderState): {nextMarker: number} { | ||
const reader = this._reader | ||
const {marker, nextPosition} = state | ||
// Skip through the other header payloads that aren't APP1 | ||
// Width and Height information will be in the Start Of Frame (SOFx) payloads | ||
if (marker === START_OF_FRAME0 || marker === START_OF_FRAME1 || marker === START_OF_FRAME2) { | ||
reader.skip(1) | ||
this._height = reader.read(2) | ||
this._width = reader.read(2) | ||
} | ||
reader.seek(nextPosition) | ||
return {nextMarker: reader.read(2)} | ||
} | ||
private _handleMarker(state: DecoderState): {nextMarker: number} { | ||
const reader = this._reader | ||
const {marker, nextPosition} = state | ||
if (marker === APP1) { | ||
// Read the EXIF/XMP data from APP1 Marker | ||
const header = reader.use(() => reader.read(4)) | ||
if (header === EXIF_HEADER) { | ||
return this._handleEXIFMarker(state) | ||
} else if (header === XMP_HEADER) { | ||
return this._handleXMPMarker(state) | ||
} else { | ||
reader.seek(nextPosition) | ||
return {nextMarker: reader.read(2)} | ||
} | ||
} else if (marker >> 8 === 0xff) { | ||
return this._handleNonAppMarker(state) | ||
} else { | ||
throw new Error(`Unrecognized marker: ${marker.toString(16)}`) | ||
} | ||
} | ||
private _readFileMarkers(): void { | ||
if (this._markers) { | ||
@@ -47,5 +138,7 @@ return | ||
const markers: Marker[] = [[START_OF_IMAGE, Buffer.from([]), false]] | ||
const baseMarker = {isEXIF: false, isXMP: false} | ||
const reader = this._reader | ||
const exifBuffers = [] | ||
this._markers = [{marker: START_OF_IMAGE, buffer: Buffer.from([]), ...baseMarker}] | ||
this._exifBuffers = [] | ||
this._xmpBuffers = [] | ||
reader.seek(2) | ||
@@ -64,50 +157,11 @@ | ||
// Push the marker and data onto our markers list | ||
markers.push([marker, markerBuffer, false]) | ||
this._markers.push({marker, buffer: markerBuffer, ...baseMarker}) | ||
// Skip over the length we just read | ||
reader.skip(2) | ||
if (marker === APP1) { | ||
// Read the EXIF data from APP1 Marker | ||
const nextPosition = reader.getPosition() + length | ||
const header = reader.read(4) | ||
if (header !== EXIF_HEADER) { | ||
reader.seek(nextPosition) | ||
marker = reader.read(2) | ||
continue | ||
} | ||
// mark the last marker as an EXIF marker | ||
markers[markers.length - 1][2] = true | ||
// skip over the 2 empty bytes | ||
reader.skip(2) | ||
// the data is the rest of the marker (-6 for 2 empty bytes and 4 for EXIF header) | ||
exifBuffers.push(reader.readAsBuffer(length - 6)) | ||
marker = reader.read(2) | ||
} else if (marker >> 8 === 0xff) { | ||
// Skip through the other header payloads that aren't APP1 | ||
const nextPosition = reader.getPosition() + length | ||
// Width and Height information will be in the Start Of Frame (SOFx) payloads | ||
if ( | ||
marker === START_OF_FRAME0 || | ||
marker === START_OF_FRAME1 || | ||
marker === START_OF_FRAME2 | ||
) { | ||
reader.skip(1) | ||
this._height = reader.read(2) | ||
this._width = reader.read(2) | ||
} | ||
reader.seek(nextPosition) | ||
marker = reader.read(2) | ||
} else { | ||
throw new Error(`Unrecognized marker: ${marker.toString(16)}`) | ||
} | ||
const nextPosition = reader.getPosition() + length | ||
marker = this._handleMarker({marker, nextPosition, length}).nextMarker | ||
} | ||
markers.push([marker, this._buffer.slice(reader.getPosition()), false]) | ||
this._markers = markers | ||
this._exifBuffers = exifBuffers | ||
this._markers.push({marker, buffer: this._buffer.slice(reader.getPosition()), ...baseMarker}) | ||
} | ||
@@ -128,6 +182,11 @@ | ||
for (const xmpBuffer of this._xmpBuffers!) { | ||
const decoder = new XMPDecoder(xmpBuffer) | ||
Object.assign(metadata, decoder.extractMetadata()) | ||
} | ||
return metadata | ||
} | ||
public extractMetadataBuffer(): IBufferLike | undefined { | ||
public extractEXIFBuffer(): IBufferLike | undefined { | ||
this._readFileMarkers() | ||
@@ -137,2 +196,7 @@ return this._exifBuffers![0] | ||
public extractXMPBuffer(): IBufferLike | undefined { | ||
this._readFileMarkers() | ||
return this._xmpBuffers![0] | ||
} | ||
public static isJPEG(buffer: IBufferLike): boolean { | ||
@@ -142,16 +206,20 @@ return buffer[0] === 0xff && buffer[1] === 0xd8 | ||
public static injectMetadata(jpegBuffer: IBufferLike, exifBuffer: IBufferLike): IBufferLike { | ||
public static injectEXIFMetadata(jpegBuffer: IBufferLike, exifBuffer: IBufferLike): IBufferLike { | ||
const decoder = new JPEGDecoder(jpegBuffer) | ||
decoder._readFileMarkers() | ||
const hasEXIFDataAlready = decoder._markers!.some(marker => marker.isEXIF) | ||
const buffers: IBufferLike[] = [] | ||
for (const [marker, buffer, isEXIF] of decoder._markers!) { | ||
if (marker === START_OF_IMAGE) { | ||
buffers.push(bufferFromNumber(START_OF_IMAGE)) | ||
for (const {marker, buffer, isEXIF} of decoder._markers!) { | ||
if (isEXIF || (marker === START_OF_IMAGE && !hasEXIFDataAlready)) { | ||
if (marker === START_OF_IMAGE) buffers.push(bufferFromNumber(START_OF_IMAGE)) | ||
buffers.push(bufferFromNumber(APP1)) | ||
buffers.push(bufferFromNumber(exifBuffer.length + 8)) | ||
// add 8 bytes to the buffer length | ||
// 4 bytes for header, 2 bytes of empty space, 2 bytes for length itself | ||
buffers.push(bufferFromNumber(exifBuffer.length + 8, 2)) | ||
buffers.push(bufferFromNumber(EXIF_HEADER, 4)) | ||
buffers.push(bufferFromNumber(0, 2)) | ||
buffers.push(exifBuffer) | ||
} else if (!isEXIF) { | ||
} else { | ||
buffers.push(bufferFromNumber(marker), buffer) | ||
@@ -164,2 +232,26 @@ } | ||
} | ||
public static injectXMPMetadata(jpegBuffer: IBufferLike, xmpBuffer: IBufferLike): IBufferLike { | ||
const decoder = new JPEGDecoder(jpegBuffer) | ||
decoder._readFileMarkers() | ||
const hasXMPDataAlready = decoder._markers!.some(marker => marker.isXMP) | ||
const buffers: IBufferLike[] = [] | ||
for (const {marker, buffer, isXMP} of decoder._markers!) { | ||
if (isXMP || (marker === START_OF_IMAGE && !hasXMPDataAlready)) { | ||
if (marker === START_OF_IMAGE) buffers.push(bufferFromNumber(START_OF_IMAGE)) | ||
buffers.push(bufferFromNumber(APP1)) | ||
// add 2 bytes to the buffer length for length itself | ||
buffers.push(bufferFromNumber(xmpBuffer.length + XMP_URL.length + 2, 2)) | ||
buffers.push(Buffer.from(XMP_URL)) | ||
buffers.push(xmpBuffer) | ||
} else { | ||
buffers.push(bufferFromNumber(marker), buffer) | ||
} | ||
} | ||
// @ts-ignore - TODO investigate why this is error-y | ||
return Buffer.concat(buffers) | ||
} | ||
} |
@@ -159,3 +159,3 @@ import {IFD} from '../decoder/ifd' | ||
this._cachedJPEG = JPEGDecoder.injectMetadata(jpeg, metadataBuffer) | ||
this._cachedJPEG = JPEGDecoder.injectEXIFMetadata(jpeg, metadataBuffer) | ||
return this._cachedJPEG.slice() | ||
@@ -162,0 +162,0 @@ } |
@@ -1,6 +0,6 @@ | ||
import {IBufferLike, IGenericMetadata, IIFDTagDefinition, IFDTagName} from '../utils/types' | ||
import {tags} from '../utils/tags' | ||
import {IBufferLike, IGenericMetadata, IFDTagName, XMPTagName} from '../utils/types' | ||
import {tags, xmpTags} from '../utils/tags' | ||
const EXIF_ATTR_GLOBAL_REGEX = /(exif|tiff):([0-9a-z]+?)="(.*?)"/gim | ||
const EXIF_ATTR_REGEX = /f:([0-9a-z]+?)="(.*?)"/i | ||
const EXIF_ATTR_GLOBAL_REGEX = /(xmp|exif|tiff):([0-9a-z]+?)="(.*?)"/gim | ||
const EXIF_ATTR_REGEX = /:([0-9a-z]+?)="(.*?)"$/i | ||
@@ -16,4 +16,2 @@ function isSimpleNumber(s: string): boolean { | ||
export class XMPDecoder { | ||
private static _tagsByLowerCaseKey: Record<string, IIFDTagDefinition> | ||
private readonly _text: string | ||
@@ -23,44 +21,24 @@ | ||
this._text = buffer.toString() | ||
this._precomputeIfdTags() | ||
} | ||
private _precomputeIfdTags(): void { | ||
if (XMPDecoder._tagsByLowerCaseKey) return | ||
const tagsByLowerCaseKey: Record<string, IIFDTagDefinition> = {} | ||
private _handleMatch(key: string, value: string, genericMetadata: IGenericMetadata): void { | ||
// TODO: support mixed case in the XMP | ||
if (!(key in tags) && !(key in xmpTags)) return | ||
const knownKey = key as IFDTagName | XMPTagName | ||
for (const tagName of Object.keys(tags)) { | ||
tagsByLowerCaseKey[tagName.toLowerCase()] = tags[tagName as IFDTagName] | ||
let realValue: number | string | undefined | ||
if (isSimpleNumber(value)) { | ||
realValue = Number(value) | ||
} else if (isComplexNumber(value)) { | ||
const [numerator, denominator] = value.split('/') | ||
realValue = Number(numerator) / Number(denominator) | ||
} else { | ||
realValue = value | ||
} | ||
XMPDecoder._tagsByLowerCaseKey = tagsByLowerCaseKey | ||
genericMetadata[knownKey] = realValue | ||
} | ||
private _processRdfDescription( | ||
attributes: Record<string, string>, | ||
genericMetadata: IGenericMetadata, | ||
): void { | ||
for (const key of Object.keys(attributes)) { | ||
if (!key.startsWith('exif:') && !key.startsWith('tiff:')) continue | ||
const exifLowercaseTagName = key.slice(5) | ||
const ifdDefinition = XMPDecoder._tagsByLowerCaseKey[exifLowercaseTagName] | ||
if (!ifdDefinition) continue | ||
const value = attributes[key] | ||
let realValue: number | string | undefined | ||
if (isSimpleNumber(value)) { | ||
realValue = Number(value) | ||
} else if (isComplexNumber(value)) { | ||
const [numerator, denominator] = value.split('/') | ||
realValue = Number(numerator) / Number(denominator) | ||
} else { | ||
realValue = value | ||
} | ||
genericMetadata[ifdDefinition.name] = realValue | ||
} | ||
} | ||
public extractMetadata(): IGenericMetadata { | ||
const metadata: IGenericMetadata = {} | ||
const attributes: Record<string, string> = {} | ||
const matches = this._text.match(EXIF_ATTR_GLOBAL_REGEX) | ||
@@ -71,7 +49,5 @@ | ||
const [_, key, value] = match.match(EXIF_ATTR_REGEX) | ||
attributes[`exif:${key.toLowerCase()}`] = value | ||
this._handleMatch(key, value, metadata) | ||
} | ||
this._processRdfDescription(attributes, metadata) | ||
return metadata | ||
@@ -81,4 +57,11 @@ } | ||
public static isXMP(buffer: IBufferLike): boolean { | ||
return buffer[0] === '<'.charCodeAt(0) | ||
const xmpHeader = '<x:xmpmet' | ||
const xmpAltHeader = '<?xpacket' | ||
for (let i = 0; i < xmpHeader.length; i++) { | ||
if (buffer[i] !== xmpHeader.charCodeAt(i) && buffer[i] !== xmpAltHeader.charCodeAt(i)) | ||
return false | ||
} | ||
return true | ||
} | ||
} |
@@ -18,3 +18,3 @@ import { | ||
export class TIFFEncoder { | ||
public static isSupportedEntry(tag: IIFDTagDefinition, value: any): boolean { | ||
public static isSupportedEntry(tag: IIFDTagDefinition | undefined, value: any): boolean { | ||
if (!tag) return false | ||
@@ -21,0 +21,0 @@ if (tag.group !== IFDGroup.EXIF) return false |
import {JPEGDecoder} from './decoder/jpeg-decoder' | ||
import {TIFFDecoder} from './decoder/tiff-decoder' | ||
import {XMPDecoder} from './decoder/xmp-decoder' | ||
import {TIFFEncoder} from './encoder/tiff-encoder' | ||
@@ -22,2 +23,4 @@ import {normalizeMetadata} from './metadata/normalize' | ||
return new JPEGDecoder(bufferOrDecoder) | ||
} else if (XMPDecoder.isXMP(bufferOrDecoder)) { | ||
return new XMPDecoder(bufferOrDecoder) | ||
} else { | ||
@@ -24,0 +27,0 @@ throw new Error('Unrecognizable file type') |
const isColonDate = (s: string) => /^\d+:\d+:\d+ \d+:\d+:\d+$/.test(s) | ||
const isISODateWithTz = (s: string) => /^\d{4}-\d{2}-\d{2}T[0-9.:]+(-|\+)\d{2}:\d{2}/.test(s) | ||
const isISODate = (s: string) => /^\d{4}-\d{2}-\d{2}T/.test(s) | ||
@@ -14,2 +16,3 @@ function parseNumericDate(timestamp: number): Date { | ||
// TODO: Accept optional target timezone instead of assuming GMT with appending `Z` | ||
export function parseDate(date: string | number): Date | undefined { | ||
@@ -21,2 +24,6 @@ let parsed = undefined | ||
parsed = parseColonDate(date) | ||
} else if (isISODateWithTz(date)) { | ||
parsed = new Date(date) | ||
} else if (isISODate(date)) { | ||
parsed = new Date(`${date}Z`) | ||
} | ||
@@ -23,0 +30,0 @@ |
import {parseDate} from '../metadata/date-parser' | ||
import {parseLens} from '../metadata/lens-parser' | ||
import {INormalizedMetadata, IGenericMetadata, IFDTagName} from '../utils/types' | ||
import {INormalizedMetadata, IGenericMetadata, IFDTagName, XMPTagName} from '../utils/types' | ||
type Omit<T, K> = Pick<T, Exclude<keyof T, K>> | ||
type TagName = IFDTagName | XMPTagName | ||
type ParseFn = (results: any) => any | ||
type PropertyDefn = IFDTagName | [IFDTagName, ParseFn] | ParseFn | ||
type PropertyDefn = TagName | [TagName, ParseFn] | ParseFn | ||
const properties: {[k: string]: PropertyDefn[]} = { | ||
type NormalizedKey = keyof Omit<INormalizedMetadata, '_raw'> | ||
const properties: Record<NormalizedKey, PropertyDefn[]> = { | ||
// TODO: look into how to normalize GPS coordinates | ||
@@ -31,2 +37,5 @@ make: ['Make'], | ||
lens: [parseLens], | ||
rating: ['Rating'], | ||
colorLabel: ['Label'], | ||
} | ||
@@ -51,3 +60,3 @@ | ||
for (const key of Object.keys(properties)) { | ||
const candidates = properties[key] | ||
const candidates = properties[key as NormalizedKey] | ||
let value = undefined | ||
@@ -64,3 +73,9 @@ for (const candidate of candidates) { | ||
if ((results.Orientation || 0) > 4) { | ||
const height = output.width | ||
output.width = output.height | ||
output.height = height | ||
} | ||
return output | ||
} |
@@ -1,2 +0,2 @@ | ||
import {IIFDTagDefinition, IFDTagName, IFDGroup, IFDDataType} from './types' | ||
import {IIFDTagDefinition, IFDTagName, IFDGroup, IFDDataType, XMPTagName} from './types' | ||
@@ -10,2 +10,8 @@ // From https://raw.githubusercontent.com/hMatoba/piexifjs/master/piexif.js | ||
export const xmpTags: Record<XMPTagName, boolean> = { | ||
Rating: true, | ||
Label: true, | ||
MetadataDate: true, | ||
} | ||
// TODO: fill in all IFDDataTypes with -1 | ||
@@ -12,0 +18,0 @@ const _tags: Array<[IFDTagName, number, IFDDataType, IFDGroup]> = [ |
@@ -110,3 +110,3 @@ export interface ILogger { | ||
export type IGenericMetadata = Partial<Record<IFDTagName, string | number | undefined>> | ||
export type IGenericMetadata = Partial<Record<IFDTagName | XMPTagName, string | number | undefined>> | ||
@@ -143,2 +143,6 @@ export interface IParsedLens { | ||
lens?: IParsedLens | ||
// XMP metadata | ||
rating?: number | ||
colorLabel?: 'Blue' | 'Red' | 'Purple' | 'Yellow' | 'Green' | ||
} | ||
@@ -169,2 +173,4 @@ | ||
export type XMPTagName = 'Rating' | 'Label' | 'MetadataDate' | ||
export type IFDTagName = | ||
@@ -171,0 +177,0 @@ | 'Unknown' |
{ | ||
"name": "@eris/exif", | ||
"version": "0.2.1-alpha.7", | ||
"version": "0.2.1-alpha.8", | ||
"description": "Parses EXIF data.", | ||
@@ -5,0 +5,0 @@ "main": "./dist/index.js", |
@@ -63,1 +63,3 @@ # exif | ||
- [EXIF Tags](http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/EXIF.html) | ||
- [XMP Specification (Part 1)](https://wwwimages2.adobe.com/content/dam/acom/en/devnet/xmp/pdfs/XMP%20SDK%20Release%20cc-2016-08/XMPSpecificationPart1.pdf) | ||
- [XMP Storage Specification (Part 3)](https://wwwimages2.adobe.com/content/dam/acom/en/devnet/xmp/pdfs/XMP%20SDK%20Release%20cc-2016-08/XMPSpecificationPart3.pdf) |
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
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
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
206519
67
3853
65