diff --git a/packages/@uppy/thumbnail-generator/package-lock.json b/packages/@uppy/thumbnail-generator/package-lock.json index 286faf995b..b979ce3a0c 100644 --- a/packages/@uppy/thumbnail-generator/package-lock.json +++ b/packages/@uppy/thumbnail-generator/package-lock.json @@ -2,6 +2,11 @@ "requires": true, "lockfileVersion": 1, "dependencies": { + "exifr": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/exifr/-/exifr-4.3.3.tgz", + "integrity": "sha512-hUARnPHem9xcPxzfREzTZpj4PL/1WZQVPG0g0qSJoL/CLN9q9jEk7bv5rvCjOKVlW+kwMCzZ++malBfXF77buw==" + }, "namespace-emitter": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/namespace-emitter/-/namespace-emitter-2.0.1.tgz", diff --git a/packages/@uppy/thumbnail-generator/package.json b/packages/@uppy/thumbnail-generator/package.json index 7fb383021d..679ce37aa7 100644 --- a/packages/@uppy/thumbnail-generator/package.json +++ b/packages/@uppy/thumbnail-generator/package.json @@ -22,7 +22,8 @@ "url": "git+https://github.com/transloadit/uppy.git" }, "dependencies": { - "@uppy/utils": "file:../utils" + "@uppy/utils": "file:../utils", + "exifr": "^4.3.3" }, "peerDependencies": { "@uppy/core": "^1.0.0" diff --git a/packages/@uppy/thumbnail-generator/src/exif.js b/packages/@uppy/thumbnail-generator/src/exif.js deleted file mode 100644 index 3a250100ee..0000000000 --- a/packages/@uppy/thumbnail-generator/src/exif.js +++ /dev/null @@ -1,620 +0,0 @@ -// Adapted from https://github.com/exif-js/exif-js -// Removed XMP and IPTC metadata parsing and EXIF thumbnail generation -// MIT License (c) Jacob Seidelin https://github.com/exif-js/exif-js/blob/master/LICENSE.md - -const ExifTags = { - // version tags - 0x9000: 'ExifVersion', // EXIF version - 0xA000: 'FlashpixVersion', // Flashpix format version - - // colorspace tags - 0xA001: 'ColorSpace', // Color space information tag - - // image configuration - 0xA002: 'PixelXDimension', // Valid width of meaningful image - 0xA003: 'PixelYDimension', // Valid height of meaningful image - 0x9101: 'ComponentsConfiguration', // Information about channels - 0x9102: 'CompressedBitsPerPixel', // Compressed bits per pixel - - // user information - 0x927C: 'MakerNote', // Any desired information written by the manufacturer - 0x9286: 'UserComment', // Comments by user - - // related file - 0xA004: 'RelatedSoundFile', // Name of related sound file - - // date and time - 0x9003: 'DateTimeOriginal', // Date and time when the original image was generated - 0x9004: 'DateTimeDigitized', // Date and time when the image was stored digitally - 0x9290: 'SubsecTime', // Fractions of seconds for DateTime - 0x9291: 'SubsecTimeOriginal', // Fractions of seconds for DateTimeOriginal - 0x9292: 'SubsecTimeDigitized', // Fractions of seconds for DateTimeDigitized - - // picture-taking conditions - 0x829A: 'ExposureTime', // Exposure time (in seconds) - 0x829D: 'FNumber', // F number - 0x8822: 'ExposureProgram', // Exposure program - 0x8824: 'SpectralSensitivity', // Spectral sensitivity - 0x8827: 'ISOSpeedRatings', // ISO speed rating - 0x8828: 'OECF', // Optoelectric conversion factor - 0x9201: 'ShutterSpeedValue', // Shutter speed - 0x9202: 'ApertureValue', // Lens aperture - 0x9203: 'BrightnessValue', // Value of brightness - 0x9204: 'ExposureBias', // Exposure bias - 0x9205: 'MaxApertureValue', // Smallest F number of lens - 0x9206: 'SubjectDistance', // Distance to subject in meters - 0x9207: 'MeteringMode', // Metering mode - 0x9208: 'LightSource', // Kind of light source - 0x9209: 'Flash', // Flash status - 0x9214: 'SubjectArea', // Location and area of main subject - 0x920A: 'FocalLength', // Focal length of the lens in mm - 0xA20B: 'FlashEnergy', // Strobe energy in BCPS - 0xA20C: 'SpatialFrequencyResponse', // - 0xA20E: 'FocalPlaneXResolution', // Number of pixels in width direction per FocalPlaneResolutionUnit - 0xA20F: 'FocalPlaneYResolution', // Number of pixels in height direction per FocalPlaneResolutionUnit - 0xA210: 'FocalPlaneResolutionUnit', // Unit for measuring FocalPlaneXResolution and FocalPlaneYResolution - 0xA214: 'SubjectLocation', // Location of subject in image - 0xA215: 'ExposureIndex', // Exposure index selected on camera - 0xA217: 'SensingMethod', // Image sensor type - 0xA300: 'FileSource', // Image source (3 === DSC) - 0xA301: 'SceneType', // Scene type (1 === directly photographed) - 0xA302: 'CFAPattern', // Color filter array geometric pattern - 0xA401: 'CustomRendered', // Special processing - 0xA402: 'ExposureMode', // Exposure mode - 0xA403: 'WhiteBalance', // 1 = auto white balance, 2 = manual - 0xA404: 'DigitalZoomRation', // Digital zoom ratio - 0xA405: 'FocalLengthIn35mmFilm', // Equivalent foacl length assuming 35mm film camera (in mm) - 0xA406: 'SceneCaptureType', // Type of scene - 0xA407: 'GainControl', // Degree of overall image gain adjustment - 0xA408: 'Contrast', // Direction of contrast processing applied by camera - 0xA409: 'Saturation', // Direction of saturation processing applied by camera - 0xA40A: 'Sharpness', // Direction of sharpness processing applied by camera - 0xA40B: 'DeviceSettingDescription', // - 0xA40C: 'SubjectDistanceRange', // Distance to subject - - // other tags - 0xA005: 'InteroperabilityIFDPointer', - 0xA420: 'ImageUniqueID' // Identifier assigned uniquely to each image -} - -const TiffTags = { - 0x0100: 'ImageWidth', - 0x0101: 'ImageHeight', - 0x8769: 'ExifIFDPointer', - 0x8825: 'GPSInfoIFDPointer', - 0xA005: 'InteroperabilityIFDPointer', - 0x0102: 'BitsPerSample', - 0x0103: 'Compression', - 0x0106: 'PhotometricInterpretation', - 0x0112: 'Orientation', - 0x0115: 'SamplesPerPixel', - 0x011C: 'PlanarConfiguration', - 0x0212: 'YCbCrSubSampling', - 0x0213: 'YCbCrPositioning', - 0x011A: 'XResolution', - 0x011B: 'YResolution', - 0x0128: 'ResolutionUnit', - 0x0111: 'StripOffsets', - 0x0116: 'RowsPerStrip', - 0x0117: 'StripByteCounts', - 0x0201: 'JPEGInterchangeFormat', - 0x0202: 'JPEGInterchangeFormatLength', - 0x012D: 'TransferFunction', - 0x013E: 'WhitePoint', - 0x013F: 'PrimaryChromaticities', - 0x0211: 'YCbCrCoefficients', - 0x0214: 'ReferenceBlackWhite', - 0x0132: 'DateTime', - 0x010E: 'ImageDescription', - 0x010F: 'Make', - 0x0110: 'Model', - 0x0131: 'Software', - 0x013B: 'Artist', - 0x8298: 'Copyright' -} - -const GPSTags = { - 0x0000: 'GPSVersionID', - 0x0001: 'GPSLatitudeRef', - 0x0002: 'GPSLatitude', - 0x0003: 'GPSLongitudeRef', - 0x0004: 'GPSLongitude', - 0x0005: 'GPSAltitudeRef', - 0x0006: 'GPSAltitude', - 0x0007: 'GPSTimeStamp', - 0x0008: 'GPSSatellites', - 0x0009: 'GPSStatus', - 0x000A: 'GPSMeasureMode', - 0x000B: 'GPSDOP', - 0x000C: 'GPSSpeedRef', - 0x000D: 'GPSSpeed', - 0x000E: 'GPSTrackRef', - 0x000F: 'GPSTrack', - 0x0010: 'GPSImgDirectionRef', - 0x0011: 'GPSImgDirection', - 0x0012: 'GPSMapDatum', - 0x0013: 'GPSDestLatitudeRef', - 0x0014: 'GPSDestLatitude', - 0x0015: 'GPSDestLongitudeRef', - 0x0016: 'GPSDestLongitude', - 0x0017: 'GPSDestBearingRef', - 0x0018: 'GPSDestBearing', - 0x0019: 'GPSDestDistanceRef', - 0x001A: 'GPSDestDistance', - 0x001B: 'GPSProcessingMethod', - 0x001C: 'GPSAreaInformation', - 0x001D: 'GPSDateStamp', - 0x001E: 'GPSDifferential' -} - -const StringValues = { - ExposureProgram: { - 0: 'Not defined', - 1: 'Manual', - 2: 'Normal program', - 3: 'Aperture priority', - 4: 'Shutter priority', - 5: 'Creative program', - 6: 'Action program', - 7: 'Portrait mode', - 8: 'Landscape mode' - }, - MeteringMode: { - 0: 'Unknown', - 1: 'Average', - 2: 'CenterWeightedAverage', - 3: 'Spot', - 4: 'MultiSpot', - 5: 'Pattern', - 6: 'Partial', - 255: 'Other' - }, - LightSource: { - 0: 'Unknown', - 1: 'Daylight', - 2: 'Fluorescent', - 3: 'Tungsten (incandescent light)', - 4: 'Flash', - 9: 'Fine weather', - 10: 'Cloudy weather', - 11: 'Shade', - 12: 'Daylight fluorescent (D 5700 - 7100K)', - 13: 'Day white fluorescent (N 4600 - 5400K)', - 14: 'Cool white fluorescent (W 3900 - 4500K)', - 15: 'White fluorescent (WW 3200 - 3700K)', - 17: 'Standard light A', - 18: 'Standard light B', - 19: 'Standard light C', - 20: 'D55', - 21: 'D65', - 22: 'D75', - 23: 'D50', - 24: 'ISO studio tungsten', - 255: 'Other' - }, - Flash: { - 0x0000: 'Flash did not fire', - 0x0001: 'Flash fired', - 0x0005: 'Strobe return light not detected', - 0x0007: 'Strobe return light detected', - 0x0009: 'Flash fired, compulsory flash mode', - 0x000D: 'Flash fired, compulsory flash mode, return light not detected', - 0x000F: 'Flash fired, compulsory flash mode, return light detected', - 0x0010: 'Flash did not fire, compulsory flash mode', - 0x0018: 'Flash did not fire, auto mode', - 0x0019: 'Flash fired, auto mode', - 0x001D: 'Flash fired, auto mode, return light not detected', - 0x001F: 'Flash fired, auto mode, return light detected', - 0x0020: 'No flash function', - 0x0041: 'Flash fired, red-eye reduction mode', - 0x0045: 'Flash fired, red-eye reduction mode, return light not detected', - 0x0047: 'Flash fired, red-eye reduction mode, return light detected', - 0x0049: 'Flash fired, compulsory flash mode, red-eye reduction mode', - 0x004D: 'Flash fired, compulsory flash mode, red-eye reduction mode, return light not detected', - 0x004F: 'Flash fired, compulsory flash mode, red-eye reduction mode, return light detected', - 0x0059: 'Flash fired, auto mode, red-eye reduction mode', - 0x005D: 'Flash fired, auto mode, return light not detected, red-eye reduction mode', - 0x005F: 'Flash fired, auto mode, return light detected, red-eye reduction mode' - }, - SensingMethod: { - 1: 'Not defined', - 2: 'One-chip color area sensor', - 3: 'Two-chip color area sensor', - 4: 'Three-chip color area sensor', - 5: 'Color sequential area sensor', - 7: 'Trilinear sensor', - 8: 'Color sequential linear sensor' - }, - SceneCaptureType: { - 0: 'Standard', - 1: 'Landscape', - 2: 'Portrait', - 3: 'Night scene' - }, - SceneType: { - 1: 'Directly photographed' - }, - CustomRendered: { - 0: 'Normal process', - 1: 'Custom process' - }, - WhiteBalance: { - 0: 'Auto white balance', - 1: 'Manual white balance' - }, - GainControl: { - 0: 'None', - 1: 'Low gain up', - 2: 'High gain up', - 3: 'Low gain down', - 4: 'High gain down' - }, - Contrast: { - 0: 'Normal', - 1: 'Soft', - 2: 'Hard' - }, - Saturation: { - 0: 'Normal', - 1: 'Low saturation', - 2: 'High saturation' - }, - Sharpness: { - 0: 'Normal', - 1: 'Soft', - 2: 'Hard' - }, - SubjectDistanceRange: { - 0: 'Unknown', - 1: 'Macro', - 2: 'Close view', - 3: 'Distant view' - }, - FileSource: { - 3: 'DSC' - }, - - Components: { - 0: '', - 1: 'Y', - 2: 'Cb', - 3: 'Cr', - 4: 'R', - 5: 'G', - 6: 'B' - } -} - -function imageHasData (img) { - return !!(img.exifdata) -} - -function base64ToArrayBuffer (base64, contentType) { - contentType = contentType || base64.match(/^data:([^;]+);base64,/mi)[1] || '' // e.g. 'data:image/jpeg;base64,...' => 'image/jpeg' - base64 = base64.replace(/^data:([^;]+);base64,/gmi, '') - var binary = atob(base64) - var len = binary.length - var buffer = new ArrayBuffer(len) - // eslint-disable-next-line compat/compat - var view = new Uint8Array(buffer) - for (var i = 0; i < len; i++) { - view[i] = binary.charCodeAt(i) - } - return buffer -} - -function objectURLToBlob (url, callback) { - var http = new XMLHttpRequest() - http.open('GET', url, true) - http.responseType = 'blob' - http.onload = function (e) { - if (this.status === 200 || this.status === 0) { - callback(this.response) - } - } - http.send() -} - -function getImageData (img, callback) { - function handleBinaryFile (binFile) { - var data = findEXIFinJPEG(binFile) - img.exifdata = data || {} - if (callback) { - callback.call(img) - } - } - - if (img.src) { - if (/^data:/i.test(img.src)) { // Data URI - var arrayBuffer = base64ToArrayBuffer(img.src) - handleBinaryFile(arrayBuffer) - } else if (/^blob:/i.test(img.src)) { // Object URL - const fileReader = new FileReader() - fileReader.onload = function (e) { - handleBinaryFile(e.target.result) - } - objectURLToBlob(img.src, function (blob) { - fileReader.readAsArrayBuffer(blob) - }) - } else { - var http = new XMLHttpRequest() - http.onload = function () { - if (this.status === 200 || this.status === 0) { - handleBinaryFile(http.response) - } else { - throw new Error('Could not load image') - } - http = null - } - http.open('GET', img.src, true) - http.responseType = 'arraybuffer' - http.send(null) - } - } else if (self.FileReader && (img instanceof self.Blob || img instanceof self.File)) { - const fileReader = new FileReader() - fileReader.onload = function (e) { - handleBinaryFile(e.target.result) - } - - fileReader.readAsArrayBuffer(img) - } -} - -function findEXIFinJPEG (file) { - var dataView = new DataView(file) - - if ((dataView.getUint8(0) !== 0xFF) || (dataView.getUint8(1) !== 0xD8)) { - return false // not a valid jpeg - } - - var offset = 2 - var length = file.byteLength - var marker - - while (offset < length) { - if (dataView.getUint8(offset) !== 0xFF) { - return false // not a valid marker, something is wrong - } - - marker = dataView.getUint8(offset + 1) - - // we could implement handling for other markers here, - // but we're only looking for 0xFFE1 for EXIF data - - if (marker === 225) { - return readEXIFData(dataView, offset + 4, dataView.getUint16(offset + 2) - 2) - } else { - offset += 2 + dataView.getUint16(offset + 2) - } - } -} - -function readTags (file, tiffStart, dirStart, strings, bigEnd) { - var entries = file.getUint16(dirStart, !bigEnd) - var tags = {} - var entryOffset; var tag - var i - - for (i = 0; i < entries; i++) { - entryOffset = dirStart + i * 12 + 2 - tag = strings[file.getUint16(entryOffset, !bigEnd)] - tags[tag] = readTagValue(file, entryOffset, tiffStart, dirStart, bigEnd) - } - return tags -} - -function readTagValue (file, entryOffset, tiffStart, dirStart, bigEnd) { - var type = file.getUint16(entryOffset + 2, !bigEnd) - var numValues = file.getUint32(entryOffset + 4, !bigEnd) - var valueOffset = file.getUint32(entryOffset + 8, !bigEnd) + tiffStart - var offset - var vals; var val; var n - var numerator; var denominator - - switch (type) { - case 1: // byte, 8-bit unsigned int - case 7: // undefined, 8-bit byte, value depending on field - if (numValues === 1) { - return file.getUint8(entryOffset + 8, !bigEnd) - } else { - offset = numValues > 4 ? valueOffset : (entryOffset + 8) - vals = [] - for (n = 0; n < numValues; n++) { - vals[n] = file.getUint8(offset + n) - } - return vals - } - - case 2: // ascii, 8-bit byte - offset = numValues > 4 ? valueOffset : (entryOffset + 8) - return getStringFromDB(file, offset, numValues - 1) - - case 3: // short, 16 bit int - if (numValues === 1) { - return file.getUint16(entryOffset + 8, !bigEnd) - } else { - offset = numValues > 2 ? valueOffset : (entryOffset + 8) - vals = [] - for (n = 0; n < numValues; n++) { - vals[n] = file.getUint16(offset + 2 * n, !bigEnd) - } - return vals - } - - case 4: // long, 32 bit int - if (numValues === 1) { - return file.getUint32(entryOffset + 8, !bigEnd) - } else { - vals = [] - for (n = 0; n < numValues; n++) { - vals[n] = file.getUint32(valueOffset + 4 * n, !bigEnd) - } - return vals - } - - case 5: // rational = two long values, first is numerator, second is denominator - if (numValues === 1) { - numerator = file.getUint32(valueOffset, !bigEnd) - denominator = file.getUint32(valueOffset + 4, !bigEnd) - val = +(numerator / denominator) - return val - } else { - vals = [] - for (n = 0; n < numValues; n++) { - numerator = file.getUint32(valueOffset + 8 * n, !bigEnd) - denominator = file.getUint32(valueOffset + 4 + 8 * n, !bigEnd) - vals[n] = +(numerator / denominator) - } - return vals - } - - case 9: // slong, 32 bit signed int - if (numValues === 1) { - return file.getInt32(entryOffset + 8, !bigEnd) - } else { - vals = [] - for (n = 0; n < numValues; n++) { - vals[n] = file.getInt32(valueOffset + 4 * n, !bigEnd) - } - return vals - } - - case 10: // signed rational, two slongs, first is numerator, second is denominator - if (numValues === 1) { - return file.getInt32(valueOffset, !bigEnd) / file.getInt32(valueOffset + 4, !bigEnd) - } else { - vals = [] - for (n = 0; n < numValues; n++) { - vals[n] = file.getInt32(valueOffset + 8 * n, !bigEnd) / file.getInt32(valueOffset + 4 + 8 * n, !bigEnd) - } - return vals - } - } -} - -function getStringFromDB (buffer, start, length) { - var outstr = '' - for (let n = start; n < start + length; n++) { - outstr += String.fromCharCode(buffer.getUint8(n)) - } - return outstr -} - -function readEXIFData (file, start) { - if (getStringFromDB(file, start, 4) !== 'Exif') { - return false - } - - var bigEnd - var tags; var tag - var exifData; var gpsData - var tiffOffset = start + 6 - - // test for TIFF validity and endianness - if (file.getUint16(tiffOffset) === 0x4949) { - bigEnd = false - } else if (file.getUint16(tiffOffset) === 0x4D4D) { - bigEnd = true - } else { - return false - } - - if (file.getUint16(tiffOffset + 2, !bigEnd) !== 0x002A) { - return false - } - - var firstIFDOffset = file.getUint32(tiffOffset + 4, !bigEnd) - - if (firstIFDOffset < 0x00000008) { - return false - } - - tags = readTags(file, tiffOffset, tiffOffset + firstIFDOffset, TiffTags, bigEnd) - - if (tags.ExifIFDPointer) { - exifData = readTags(file, tiffOffset, tiffOffset + tags.ExifIFDPointer, ExifTags, bigEnd) - for (tag in exifData) { - switch (tag) { - case 'LightSource' : - case 'Flash' : - case 'MeteringMode' : - case 'ExposureProgram' : - case 'SensingMethod' : - case 'SceneCaptureType' : - case 'SceneType' : - case 'CustomRendered' : - case 'WhiteBalance' : - case 'GainControl' : - case 'Contrast' : - case 'Saturation' : - case 'Sharpness' : - case 'SubjectDistanceRange' : - case 'FileSource' : - exifData[tag] = StringValues[tag][exifData[tag]] - break - - case 'ExifVersion' : - case 'FlashpixVersion' : - exifData[tag] = String.fromCharCode(exifData[tag][0], exifData[tag][1], exifData[tag][2], exifData[tag][3]) - break - - case 'ComponentsConfiguration' : - exifData[tag] = - StringValues.Components[exifData[tag][0]] + - StringValues.Components[exifData[tag][1]] + - StringValues.Components[exifData[tag][2]] + - StringValues.Components[exifData[tag][3]] - break - } - tags[tag] = exifData[tag] - } - } - - if (tags.GPSInfoIFDPointer) { - gpsData = readTags(file, tiffOffset, tiffOffset + tags.GPSInfoIFDPointer, GPSTags, bigEnd) - for (tag in gpsData) { - switch (tag) { - case 'GPSVersionID' : - gpsData[tag] = gpsData[tag][0] + - '.' + gpsData[tag][1] + - '.' + gpsData[tag][2] + - '.' + gpsData[tag][3] - break - } - tags[tag] = gpsData[tag] - } - } - - return tags -} - -function getData (img, callback) { - if (((self.Image && img instanceof self.Image) || - (self.HTMLImageElement && img instanceof self.HTMLImageElement)) && - !img.complete) { return false } - - if (!imageHasData(img)) { - getImageData(img, callback) - } else { - if (callback) { - callback.call(img) - } - } - return true -} - -function getTag (img, tag) { - if (!imageHasData(img)) return - return img.exifdata[tag] -} - -function getAllTags (img) { - if (!imageHasData(img)) return {} - return img.exifdata -} - -module.exports = { - getAllTags, - getData, - getTag -} diff --git a/packages/@uppy/thumbnail-generator/src/index.js b/packages/@uppy/thumbnail-generator/src/index.js index 861e0eaf69..a52a40d6df 100644 --- a/packages/@uppy/thumbnail-generator/src/index.js +++ b/packages/@uppy/thumbnail-generator/src/index.js @@ -4,7 +4,7 @@ const dataURItoBlob = require('@uppy/utils/lib/dataURItoBlob') const isObjectURL = require('@uppy/utils/lib/isObjectURL') const isPreviewSupported = require('@uppy/utils/lib/isPreviewSupported') const ORIENTATIONS = require('./image-orientations') -const Exif = require('./exif') +const exifr = require('exifr/dist/mini.legacy.umd.js') /** * The Thumbnail Generator plugin @@ -74,10 +74,9 @@ module.exports = class ThumbnailGenerator extends Plugin { }) }) - return Promise.all([onload, this.getOrientation(file)]) - .then(values => { - const image = values[0] - const orientation = values[1] + return Promise.all([onload, exifr.orientation(file.data)]) + .then(([image, rawOrientation]) => { + const orientation = ORIENTATIONS[rawOrientation || 1] const dimensions = this.getProportionalDimensions(image, targetWidth, targetHeight, orientation.rotation) const rotatedImage = this.rotateImage(image, orientation) const resizedImage = this.resizeImage(rotatedImage, dimensions.width, dimensions.height) @@ -120,17 +119,6 @@ module.exports = class ThumbnailGenerator extends Plugin { } } - getOrientation (file) { - return new Promise((resolve) => { - const uppy = this.uppy - Exif.getData(file.data, function exifGetDataCallback () { - uppy.setFileMeta(file.id, { exifdata: Exif.getAllTags(this) }) - const orientation = Exif.getTag(this, 'Orientation') || 1 - resolve(ORIENTATIONS[orientation]) - }) - }) - } - /** * Make sure the image doesn’t exceed browser/device canvas limits. * For ios with 256 RAM and ie