New Case Study:See how Anthropic automated 95% of dependency reviews with Socket.Learn More
Socket
Sign inDemoInstall
Socket

@eris/exif

Package Overview
Dependencies
Maintainers
1
Versions
109
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@eris/exif - npm Package Compare versions

Comparing version 0.2.1-alpha.7 to 0.2.1-alpha.8

dist/encoder/xmp-encoder.d.ts

17

dist/decoder/jpeg-decoder.d.ts

@@ -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;
}

168

dist/decoder/jpeg-decoder.js

@@ -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

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc