diff --git a/frontend/js/LICENSE b/frontend/js/LICENSE new file mode 100644 index 0000000..fa0125f --- /dev/null +++ b/frontend/js/LICENSE @@ -0,0 +1,12 @@ + +Contents of the 'js' directory are from: + + * JavaScript Load Image Exif Parser + * https://github.com/blueimp/JavaScript-Load-Image + * + * Copyright 2013, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * https://opensource.org/licenses/MIT + diff --git a/frontend/js/load-image-exif-map.js b/frontend/js/load-image-exif-map.js new file mode 100644 index 0000000..ec23462 --- /dev/null +++ b/frontend/js/load-image-exif-map.js @@ -0,0 +1,388 @@ +/* + * JavaScript Load Image Exif Map + * https://github.com/blueimp/JavaScript-Load-Image + * + * Copyright 2013, Sebastian Tschan + * https://blueimp.net + * + * Exif tags mapping based on + * https://github.com/jseidelin/exif-js + * + * Licensed under the MIT license: + * https://opensource.org/licenses/MIT + */ + +/* global define, module, require */ + +;(function(factory) { + 'use strict' + if (typeof define === 'function' && define.amd) { + // Register as an anonymous AMD module: + define(['./load-image', './load-image-exif'], factory) + } else if (typeof module === 'object' && module.exports) { + factory(require('./load-image'), require('./load-image-exif')) + } else { + // Browser globals: + factory(window.loadImage) + } +})(function(loadImage) { + 'use strict' + + loadImage.ExifMap.prototype.tags = { + // ================= + // TIFF tags (IFD0): + // ================= + 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', + // ================== + // Exif Sub IFD tags: + // ================== + 0x9000: 'ExifVersion', // EXIF version + 0xa000: 'FlashpixVersion', // Flashpix format version + 0xa001: 'ColorSpace', // Color space information tag + 0xa002: 'PixelXDimension', // Valid width of meaningful image + 0xa003: 'PixelYDimension', // Valid height of meaningful image + 0xa500: 'Gamma', + 0x9101: 'ComponentsConfiguration', // Information about channels + 0x9102: 'CompressedBitsPerPixel', // Compressed bits per pixel + 0x927c: 'MakerNote', // Any desired information written by the manufacturer + 0x9286: 'UserComment', // Comments by user + 0xa004: 'RelatedSoundFile', // Name of related sound file + 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 + 0x829a: 'ExposureTime', // Exposure time (in seconds) + 0x829d: 'FNumber', + 0x8822: 'ExposureProgram', // Exposure program + 0x8824: 'SpectralSensitivity', // Spectral sensitivity + 0x8827: 'PhotographicSensitivity', // EXIF 2.3, ISOSpeedRatings in EXIF 2.2 + 0x8828: 'OECF', // Optoelectric conversion factor + 0x8830: 'SensitivityType', + 0x8831: 'StandardOutputSensitivity', + 0x8832: 'RecommendedExposureIndex', + 0x8833: 'ISOSpeed', + 0x8834: 'ISOSpeedLatitudeyyy', + 0x8835: 'ISOSpeedLatitudezzz', + 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 FPRUnit + 0xa20f: 'FocalPlaneYResolution', // Number of pixels in height direction per FPRUnit + 0xa210: 'FocalPlaneResolutionUnit', // Unit for measuring the focal plane resolution + 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: 'DigitalZoomRatio', // Digital zoom ratio + 0xa405: 'FocalLengthIn35mmFilm', + 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 + 0xa420: 'ImageUniqueID', // Identifier assigned uniquely to each image + 0xa430: 'CameraOwnerName', + 0xa431: 'BodySerialNumber', + 0xa432: 'LensSpecification', + 0xa433: 'LensMake', + 0xa434: 'LensModel', + 0xa435: 'LensSerialNumber', + // ============== + // GPS Info tags: + // ============== + 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', + 0x001f: 'GPSHPositioningError' + } + + loadImage.ExifMap.prototype.stringValues = { + ExposureProgram: { + 0: 'Undefined', + 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: 'Undefined', + 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' + }, + ComponentsConfiguration: { + 0: '', + 1: 'Y', + 2: 'Cb', + 3: 'Cr', + 4: 'R', + 5: 'G', + 6: 'B' + }, + Orientation: { + 1: 'top-left', + 2: 'top-right', + 3: 'bottom-right', + 4: 'bottom-left', + 5: 'left-top', + 6: 'right-top', + 7: 'right-bottom', + 8: 'left-bottom' + } + } + + loadImage.ExifMap.prototype.getText = function(id) { + var value = this.get(id) + switch (id) { + 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': + case 'Orientation': + return this.stringValues[id][value] + case 'ExifVersion': + case 'FlashpixVersion': + if (!value) return + return String.fromCharCode(value[0], value[1], value[2], value[3]) + case 'ComponentsConfiguration': + if (!value) return + return ( + this.stringValues[id][value[0]] + + this.stringValues[id][value[1]] + + this.stringValues[id][value[2]] + + this.stringValues[id][value[3]] + ) + case 'GPSVersionID': + if (!value) return + return value[0] + '.' + value[1] + '.' + value[2] + '.' + value[3] + } + return String(value) + } + ;(function(exifMapPrototype) { + var tags = exifMapPrototype.tags + var map = exifMapPrototype.map + var prop + // Map the tag names to tags: + for (prop in tags) { + if (Object.prototype.hasOwnProperty.call(tags, prop)) { + map[tags[prop]] = prop + } + } + })(loadImage.ExifMap.prototype) + + loadImage.ExifMap.prototype.getAll = function() { + var map = {} + var prop + var id + for (prop in this) { + if (Object.prototype.hasOwnProperty.call(this, prop)) { + id = this.tags[prop] + if (id) { + map[id] = this.getText(id) + } + } + } + return map + } +}) diff --git a/frontend/js/load-image-exif.js b/frontend/js/load-image-exif.js new file mode 100644 index 0000000..3e9701f --- /dev/null +++ b/frontend/js/load-image-exif.js @@ -0,0 +1,322 @@ +/* + * JavaScript Load Image Exif Parser + * https://github.com/blueimp/JavaScript-Load-Image + * + * Copyright 2013, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * https://opensource.org/licenses/MIT + */ + +/* global define, module, require */ + +/* eslint-disable no-console */ + +;(function(factory) { + 'use strict' + if (typeof define === 'function' && define.amd) { + // Register as an anonymous AMD module: + define(['./load-image', './load-image-meta'], factory) + } else if (typeof module === 'object' && module.exports) { + factory(require('./load-image'), require('./load-image-meta')) + } else { + // Browser globals: + factory(window.loadImage) + } +})(function(loadImage) { + 'use strict' + + loadImage.ExifMap = function() { + return this + } + + loadImage.ExifMap.prototype.map = { + Orientation: 0x0112 + } + + loadImage.ExifMap.prototype.get = function(id) { + return this[id] || this[this.map[id]] + } + + loadImage.getExifThumbnail = function(dataView, offset, length) { + if (!length || offset + length > dataView.byteLength) { + console.log('Invalid Exif data: Invalid thumbnail data.') + return + } + return loadImage.createObjectURL( + new Blob([dataView.buffer.slice(offset, offset + length)]) + ) + } + + loadImage.exifTagTypes = { + // byte, 8-bit unsigned int: + 1: { + getValue: function(dataView, dataOffset) { + return dataView.getUint8(dataOffset) + }, + size: 1 + }, + // ascii, 8-bit byte: + 2: { + getValue: function(dataView, dataOffset) { + return String.fromCharCode(dataView.getUint8(dataOffset)) + }, + size: 1, + ascii: true + }, + // short, 16 bit int: + 3: { + getValue: function(dataView, dataOffset, littleEndian) { + return dataView.getUint16(dataOffset, littleEndian) + }, + size: 2 + }, + // long, 32 bit int: + 4: { + getValue: function(dataView, dataOffset, littleEndian) { + return dataView.getUint32(dataOffset, littleEndian) + }, + size: 4 + }, + // rational = two long values, first is numerator, second is denominator: + 5: { + getValue: function(dataView, dataOffset, littleEndian) { + return ( + dataView.getUint32(dataOffset, littleEndian) / + dataView.getUint32(dataOffset + 4, littleEndian) + ) + }, + size: 8 + }, + // slong, 32 bit signed int: + 9: { + getValue: function(dataView, dataOffset, littleEndian) { + return dataView.getInt32(dataOffset, littleEndian) + }, + size: 4 + }, + // srational, two slongs, first is numerator, second is denominator: + 10: { + getValue: function(dataView, dataOffset, littleEndian) { + return ( + dataView.getInt32(dataOffset, littleEndian) / + dataView.getInt32(dataOffset + 4, littleEndian) + ) + }, + size: 8 + } + } + // undefined, 8-bit byte, value depending on field: + loadImage.exifTagTypes[7] = loadImage.exifTagTypes[1] + + loadImage.getExifValue = function( + dataView, + tiffOffset, + offset, + type, + length, + littleEndian + ) { + var tagType = loadImage.exifTagTypes[type] + var tagSize + var dataOffset + var values + var i + var str + var c + if (!tagType) { + console.log('Invalid Exif data: Invalid tag type.') + return + } + tagSize = tagType.size * length + // Determine if the value is contained in the dataOffset bytes, + // or if the value at the dataOffset is a pointer to the actual data: + dataOffset = + tagSize > 4 + ? tiffOffset + dataView.getUint32(offset + 8, littleEndian) + : offset + 8 + if (dataOffset + tagSize > dataView.byteLength) { + console.log('Invalid Exif data: Invalid data offset.') + return + } + if (length === 1) { + return tagType.getValue(dataView, dataOffset, littleEndian) + } + values = [] + for (i = 0; i < length; i += 1) { + values[i] = tagType.getValue( + dataView, + dataOffset + i * tagType.size, + littleEndian + ) + } + if (tagType.ascii) { + str = '' + // Concatenate the chars: + for (i = 0; i < values.length; i += 1) { + c = values[i] + // Ignore the terminating NULL byte(s): + if (c === '\u0000') { + break + } + str += c + } + return str + } + return values + } + + loadImage.parseExifTag = function( + dataView, + tiffOffset, + offset, + littleEndian, + data + ) { + var tag = dataView.getUint16(offset, littleEndian) + data.exif[tag] = loadImage.getExifValue( + dataView, + tiffOffset, + offset, + dataView.getUint16(offset + 2, littleEndian), // tag type + dataView.getUint32(offset + 4, littleEndian), // tag length + littleEndian + ) + } + + loadImage.parseExifTags = function( + dataView, + tiffOffset, + dirOffset, + littleEndian, + data + ) { + var tagsNumber, dirEndOffset, i + if (dirOffset + 6 > dataView.byteLength) { + console.log('Invalid Exif data: Invalid directory offset.') + return + } + tagsNumber = dataView.getUint16(dirOffset, littleEndian) + dirEndOffset = dirOffset + 2 + 12 * tagsNumber + if (dirEndOffset + 4 > dataView.byteLength) { + console.log('Invalid Exif data: Invalid directory size.') + return + } + for (i = 0; i < tagsNumber; i += 1) { + this.parseExifTag( + dataView, + tiffOffset, + dirOffset + 2 + 12 * i, // tag offset + littleEndian, + data + ) + } + // Return the offset to the next directory: + return dataView.getUint32(dirEndOffset, littleEndian) + } + + loadImage.parseExifData = function(dataView, offset, length, data, options) { + if (options.disableExif) { + return + } + var tiffOffset = offset + 10 + var littleEndian + var dirOffset + var thumbnailData + // Check for the ASCII code for "Exif" (0x45786966): + if (dataView.getUint32(offset + 4) !== 0x45786966) { + // No Exif data, might be XMP data instead + return + } + if (tiffOffset + 8 > dataView.byteLength) { + console.log('Invalid Exif data: Invalid segment size.') + return + } + // Check for the two null bytes: + if (dataView.getUint16(offset + 8) !== 0x0000) { + console.log('Invalid Exif data: Missing byte alignment offset.') + return + } + // Check the byte alignment: + switch (dataView.getUint16(tiffOffset)) { + case 0x4949: + littleEndian = true + break + case 0x4d4d: + littleEndian = false + break + default: + console.log('Invalid Exif data: Invalid byte alignment marker.') + return + } + // Check for the TIFF tag marker (0x002A): + if (dataView.getUint16(tiffOffset + 2, littleEndian) !== 0x002a) { + console.log('Invalid Exif data: Missing TIFF marker.') + return + } + // Retrieve the directory offset bytes, usually 0x00000008 or 8 decimal: + dirOffset = dataView.getUint32(tiffOffset + 4, littleEndian) + // Create the exif object to store the tags: + data.exif = new loadImage.ExifMap() + // Parse the tags of the main image directory and retrieve the + // offset to the next directory, usually the thumbnail directory: + dirOffset = loadImage.parseExifTags( + dataView, + tiffOffset, + tiffOffset + dirOffset, + littleEndian, + data + ) + if (dirOffset && !options.disableExifThumbnail) { + thumbnailData = { exif: {} } + dirOffset = loadImage.parseExifTags( + dataView, + tiffOffset, + tiffOffset + dirOffset, + littleEndian, + thumbnailData + ) + // Check for JPEG Thumbnail offset: + if (thumbnailData.exif[0x0201]) { + data.exif.Thumbnail = loadImage.getExifThumbnail( + dataView, + tiffOffset + thumbnailData.exif[0x0201], + thumbnailData.exif[0x0202] // Thumbnail data length + ) + } + } + // Check for Exif Sub IFD Pointer: + if (data.exif[0x8769] && !options.disableExifSub) { + loadImage.parseExifTags( + dataView, + tiffOffset, + tiffOffset + data.exif[0x8769], // directory offset + littleEndian, + data + ) + } + // Check for GPS Info IFD Pointer: + if (data.exif[0x8825] && !options.disableExifGps) { + loadImage.parseExifTags( + dataView, + tiffOffset, + tiffOffset + data.exif[0x8825], // directory offset + littleEndian, + data + ) + } + } + + // Registers the Exif parser for the APP1 JPEG meta data segment: + loadImage.metaDataParsers.jpeg[0xffe1].push(loadImage.parseExifData) + + // Adds the following properties to the parseMetaData callback data: + // * exif: The exif tags, parsed by the parseExifData method + + // Adds the following options to the parseMetaData method: + // * disableExif: Disables Exif parsing. + // * disableExifThumbnail: Disables parsing of the Exif Thumbnail. + // * disableExifSub: Disables parsing of the Exif Sub IFD. + // * disableExifGps: Disables parsing of the Exif GPS Info IFD. +}) diff --git a/frontend/js/load-image-fetch.js b/frontend/js/load-image-fetch.js new file mode 100644 index 0000000..5dcd119 --- /dev/null +++ b/frontend/js/load-image-fetch.js @@ -0,0 +1,44 @@ +/* + * JavaScript Load Image Fetch + * https://github.com/blueimp/JavaScript-Load-Image + * + * Copyright 2017, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * https://opensource.org/licenses/MIT + */ + +/* global define, module, require */ + +;(function(factory) { + 'use strict' + if (typeof define === 'function' && define.amd) { + // Register as an anonymous AMD module: + define(['./load-image', './load-image-meta'], factory) + } else if (typeof module === 'object' && module.exports) { + factory(require('./load-image'), require('./load-image-meta')) + } else { + // Browser globals: + factory(window.loadImage) + } +})(function(loadImage) { + 'use strict' + + if (typeof fetch !== 'undefined' && typeof Request !== 'undefined') { + loadImage.fetchBlob = function(url, callback, options) { + if (loadImage.hasMetaOption(options)) { + return fetch(new Request(url, options)) + .then(function(response) { + return response.blob() + }) + .then(callback) + .catch(function(err) { + console.log(err) // eslint-disable-line no-console + callback() + }) + } + callback() + } + } +}) diff --git a/frontend/js/load-image-iptc-map.js b/frontend/js/load-image-iptc-map.js new file mode 100644 index 0000000..5ed39de --- /dev/null +++ b/frontend/js/load-image-iptc-map.js @@ -0,0 +1,129 @@ +/* + * JavaScript Load Image IPTC Map + * https://github.com/blueimp/JavaScript-Load-Image + * + * Copyright 2013, Sebastian Tschan + * Copyright 2018, Dave Bevan + * + * IPTC tags mapping based on + * https://github.com/jseidelin/exif-js + * https://iptc.org/standards/photo-metadata + * http://www.iptc.org/std/IIM/4.1/specification/IIMV4.1.pdf + * + * Licensed under the MIT license: + * https://opensource.org/licenses/MIT + */ + +/* global define, module, require */ + +;(function(factory) { + 'use strict' + if (typeof define === 'function' && define.amd) { + // Register as an anonymous AMD module: + define(['./load-image', './load-image-iptc'], factory) + } else if (typeof module === 'object' && module.exports) { + factory(require('./load-image'), require('./load-image-iptc')) + } else { + // Browser globals: + factory(window.loadImage) + } +})(function(loadImage) { + 'use strict' + + loadImage.IptcMap.prototype.tags = { + // ========== + // IPTC tags: + // ========== + 0x03: 'ObjectType', + 0x04: 'ObjectAttribute', + 0x05: 'ObjectName', + 0x07: 'EditStatus', + 0x08: 'EditorialUpdate', + 0x0a: 'Urgency', + 0x0c: 'SubjectRef', + 0x0f: 'Category', + 0x14: 'SupplCategory', + 0x16: 'FixtureID', + 0x19: 'Keywords', + 0x1a: 'ContentLocCode', + 0x1b: 'ContentLocName', + 0x1e: 'ReleaseDate', + 0x23: 'ReleaseTime', + 0x25: 'ExpirationDate', + 0x26: 'ExpirationTime', + 0x28: 'SpecialInstructions', + 0x2a: 'ActionAdvised', + 0x2d: 'RefService', + 0x2f: 'RefDate', + 0x32: 'RefNumber', + 0x37: 'DateCreated', + 0x3c: 'TimeCreated', + 0x3e: 'DigitalCreationDate', + 0x3f: 'DigitalCreationTime', + 0x41: 'OriginatingProgram', + 0x46: 'ProgramVersion', + 0x4b: 'ObjectCycle', + 0x50: 'Byline', + 0x55: 'BylineTitle', + 0x5a: 'City', + 0x5c: 'Sublocation', + 0x5f: 'State', + 0x64: 'CountryCode', + 0x65: 'CountryName', + 0x67: 'OrigTransRef', + 0x69: 'Headline', + 0x6e: 'Credit', + 0x73: 'Source', + 0x74: 'CopyrightNotice', + 0x76: 'Contact', + 0x78: 'Caption', + 0x7a: 'WriterEditor', + 0x82: 'ImageType', + 0x83: 'ImageOrientation', + 0x87: 'LanguageID' + + // We don't record these tags: + // + // 0x00: 'RecordVersion', + // 0x7d: 'RasterizedCaption', + // 0x96: 'AudioType', + // 0x97: 'AudioSamplingRate', + // 0x98: 'AudioSamplingRes', + // 0x99: 'AudioDuration', + // 0x9a: 'AudioOutcue', + // 0xc8: 'PreviewFileFormat', + // 0xc9: 'PreviewFileFormatVer', + // 0xca: 'PreviewData' + } + + loadImage.IptcMap.prototype.getText = function(id) { + var value = this.get(id) + return String(value) + } + ;(function(iptcMapPrototype) { + var tags = iptcMapPrototype.tags + var map = iptcMapPrototype.map || {} + var prop + // Map the tag names to tags: + for (prop in tags) { + if (Object.prototype.hasOwnProperty.call(tags, prop)) { + map[tags[prop]] = prop + } + } + })(loadImage.IptcMap.prototype) + + loadImage.IptcMap.prototype.getAll = function() { + var map = {} + var prop + var id + for (prop in this) { + if (Object.prototype.hasOwnProperty.call(this, prop)) { + id = this.tags[prop] + if (id) { + map[id] = this.getText(id) + } + } + } + return map + } +}) diff --git a/frontend/js/load-image-iptc.js b/frontend/js/load-image-iptc.js new file mode 100644 index 0000000..be2e764 --- /dev/null +++ b/frontend/js/load-image-iptc.js @@ -0,0 +1,153 @@ +/* + * JavaScript Load Image IPTC Parser + * https://github.com/blueimp/JavaScript-Load-Image + * + * Copyright 2013, Sebastian Tschan + * Copyright 2018, Dave Bevan + * https://blueimp.net + * + * Licensed under the MIT license: + * https://opensource.org/licenses/MIT + */ + +/* global define, module, require, Buffer */ + +;(function(factory) { + 'use strict' + if (typeof define === 'function' && define.amd) { + // Register as an anonymous AMD module: + define(['./load-image', './load-image-meta'], factory) + } else if (typeof module === 'object' && module.exports) { + factory(require('./load-image'), require('./load-image-meta')) + } else { + // Browser globals: + factory(window.loadImage) + } +})(function(loadImage) { + 'use strict' + + loadImage.IptcMap = function() { + return this + } + + loadImage.IptcMap.prototype.map = { + ObjectName: 0x5 + } + + loadImage.IptcMap.prototype.get = function(id) { + return this[id] || this[this.map[id]] + } + + loadImage.parseIptcTags = function( + dataView, + startOffset, + sectionLength, + data + ) { + /** + * Retrieves string for the given Buffer and range + * + * @param {Buffer} buffer IPTC buffer + * @param {number} start Range start + * @param {number} length Range length + * @returns {string} String value + */ + function getStringFromDB(buffer, start, length) { + var outstr = '' + for (var n = start; n < start + length; n++) { + outstr += String.fromCharCode(buffer.getUint8(n)) + } + return outstr + } + var fieldValue, dataSize, segmentType + var segmentStartPos = startOffset + while (segmentStartPos < startOffset + sectionLength) { + // we currently handle the 2: class of iptc tag + if ( + dataView.getUint8(segmentStartPos) === 0x1c && + dataView.getUint8(segmentStartPos + 1) === 0x02 + ) { + segmentType = dataView.getUint8(segmentStartPos + 2) + // only store data for known tags + if (segmentType in data.iptc.tags) { + dataSize = dataView.getInt16(segmentStartPos + 3) + fieldValue = getStringFromDB(dataView, segmentStartPos + 5, dataSize) + // Check if we already stored a value with this name + if (Object.prototype.hasOwnProperty.call(data.iptc, segmentType)) { + // Value already stored with this name, create multivalue field + if (data.iptc[segmentType] instanceof Array) { + data.iptc[segmentType].push(fieldValue) + } else { + data.iptc[segmentType] = [data.iptc[segmentType], fieldValue] + } + } else { + data.iptc[segmentType] = fieldValue + } + } + } + segmentStartPos++ + } + } + + loadImage.parseIptcData = function(dataView, offset, length, data, options) { + if (options.disableIptc) { + return + } + var markerLength = offset + length + // Found '8BIM' ? + var isFieldSegmentStart = function(dataView, offset) { + return ( + dataView.getUint32(offset) === 0x3842494d && + dataView.getUint16(offset + 4) === 0x0404 + ) + } + // Hunt forward, looking for the correct IPTC block signature: + // Reference: https://metacpan.org/pod/distribution/Image-MetaData-JPEG/lib/Image/MetaData/JPEG/Structures.pod#Structure-of-a-Photoshop-style-APP13-segment + // From https://github.com/exif-js/exif-js/blob/master/exif.js ~ line 474 on + while (offset + 8 < markerLength) { + if (isFieldSegmentStart(dataView, offset)) { + var nameHeaderLength = dataView.getUint8(offset + 7) + if (nameHeaderLength % 2 !== 0) nameHeaderLength += 1 + // Check for pre photoshop 6 format + if (nameHeaderLength === 0) { + // Always 4 + nameHeaderLength = 4 + } + var startOffset = offset + 8 + nameHeaderLength + if (startOffset > markerLength) { + // eslint-disable-next-line no-console + console.log('Invalid IPTC data: Invalid segment offset.') + break + } + var sectionLength = dataView.getUint16(offset + 6 + nameHeaderLength) + if (offset + sectionLength > markerLength) { + // eslint-disable-next-line no-console + console.log('Invalid IPTC data: Invalid segment size.') + break + } + // Create the iptc object to store the tags: + data.iptc = new loadImage.IptcMap() + // Parse the tags + return loadImage.parseIptcTags( + dataView, + startOffset, + sectionLength, + data + ) + } + // eslint-disable-next-line no-param-reassign + offset++ + } + // eslint-disable-next-line no-console + console.log('No IPTC data at this offset - could be XMP') + } + + // Registers this IPTC parser for the APP13 JPEG meta data segment: + loadImage.metaDataParsers.jpeg[0xffed].push(loadImage.parseIptcData) + + // Adds the following properties to the parseMetaData callback data: + // * iptc: The iptc tags, parsed by the parseIptcData method + + // Adds the following options to the parseMetaData method: + // * disableIptc: Disables IPTC parsing. +}) diff --git a/frontend/js/load-image-meta.js b/frontend/js/load-image-meta.js new file mode 100644 index 0000000..c110036 --- /dev/null +++ b/frontend/js/load-image-meta.js @@ -0,0 +1,185 @@ +/* + * JavaScript Load Image Meta + * https://github.com/blueimp/JavaScript-Load-Image + * + * Copyright 2013, Sebastian Tschan + * https://blueimp.net + * + * Image meta data handling implementation + * based on the help and contribution of + * Achim Stöhr. + * + * Licensed under the MIT license: + * https://opensource.org/licenses/MIT + */ + +/* global define, module, require, DataView, Uint8Array */ + +;(function(factory) { + 'use strict' + if (typeof define === 'function' && define.amd) { + // Register as an anonymous AMD module: + define(['./load-image'], factory) + } else if (typeof module === 'object' && module.exports) { + factory(require('./load-image')) + } else { + // Browser globals: + factory(window.loadImage) + } +})(function(loadImage) { + 'use strict' + + var hasblobSlice = + typeof Blob !== 'undefined' && + (Blob.prototype.slice || + Blob.prototype.webkitSlice || + Blob.prototype.mozSlice) + + loadImage.blobSlice = + hasblobSlice && + function() { + var slice = this.slice || this.webkitSlice || this.mozSlice + return slice.apply(this, arguments) + } + + loadImage.metaDataParsers = { + jpeg: { + 0xffe1: [], // APP1 marker + 0xffed: [] // APP13 marker + } + } + + // Parses image meta data and calls the callback with an object argument + // with the following properties: + // * imageHead: The complete image head as ArrayBuffer (Uint8Array for IE10) + // The options argument accepts an object and supports the following + // properties: + // * maxMetaDataSize: Defines the maximum number of bytes to parse. + // * disableImageHead: Disables creating the imageHead property. + loadImage.parseMetaData = function(file, callback, options, data) { + // eslint-disable-next-line no-param-reassign + options = options || {} + // eslint-disable-next-line no-param-reassign + data = data || {} + var that = this + // 256 KiB should contain all EXIF/ICC/IPTC segments: + var maxMetaDataSize = options.maxMetaDataSize || 262144 + var noMetaData = !( + typeof DataView !== 'undefined' && + file && + file.size >= 12 && + file.type === 'image/jpeg' && + loadImage.blobSlice + ) + if ( + noMetaData || + !loadImage.readFile( + loadImage.blobSlice.call(file, 0, maxMetaDataSize), + function(e) { + if (e.target.error) { + // FileReader error + // eslint-disable-next-line no-console + console.log(e.target.error) + callback(data) + return + } + // Note on endianness: + // Since the marker and length bytes in JPEG files are always + // stored in big endian order, we can leave the endian parameter + // of the DataView methods undefined, defaulting to big endian. + var buffer = e.target.result + var dataView = new DataView(buffer) + var offset = 2 + var maxOffset = dataView.byteLength - 4 + var headLength = offset + var markerBytes + var markerLength + var parsers + var i + // Check for the JPEG marker (0xffd8): + if (dataView.getUint16(0) === 0xffd8) { + while (offset < maxOffset) { + markerBytes = dataView.getUint16(offset) + // Search for APPn (0xffeN) and COM (0xfffe) markers, + // which contain application-specific meta-data like + // Exif, ICC and IPTC data and text comments: + if ( + (markerBytes >= 0xffe0 && markerBytes <= 0xffef) || + markerBytes === 0xfffe + ) { + // The marker bytes (2) are always followed by + // the length bytes (2), indicating the length of the + // marker segment, which includes the length bytes, + // but not the marker bytes, so we add 2: + markerLength = dataView.getUint16(offset + 2) + 2 + if (offset + markerLength > dataView.byteLength) { + // eslint-disable-next-line no-console + console.log('Invalid meta data: Invalid segment size.') + break + } + parsers = loadImage.metaDataParsers.jpeg[markerBytes] + if (parsers) { + for (i = 0; i < parsers.length; i += 1) { + parsers[i].call( + that, + dataView, + offset, + markerLength, + data, + options + ) + } + } + offset += markerLength + headLength = offset + } else { + // Not an APPn or COM marker, probably safe to + // assume that this is the end of the meta data + break + } + } + // Meta length must be longer than JPEG marker (2) + // plus APPn marker (2), followed by length bytes (2): + if (!options.disableImageHead && headLength > 6) { + if (buffer.slice) { + data.imageHead = buffer.slice(0, headLength) + } else { + // Workaround for IE10, which does not yet + // support ArrayBuffer.slice: + data.imageHead = new Uint8Array(buffer).subarray(0, headLength) + } + } + } else { + // eslint-disable-next-line no-console + console.log('Invalid JPEG file: Missing JPEG marker.') + } + callback(data) + }, + 'readAsArrayBuffer' + ) + ) { + callback(data) + } + } + + // Determines if meta data should be loaded automatically: + loadImage.hasMetaOption = function(options) { + return options && options.meta + } + + var originalTransform = loadImage.transform + loadImage.transform = function(img, options, callback, file, data) { + if (loadImage.hasMetaOption(options)) { + loadImage.parseMetaData( + file, + function(data) { + originalTransform.call(loadImage, img, options, callback, file, data) + }, + options, + data + ) + } else { + originalTransform.apply(loadImage, arguments) + } + } +}) diff --git a/frontend/js/load-image-orientation.js b/frontend/js/load-image-orientation.js new file mode 100644 index 0000000..11a5ccf --- /dev/null +++ b/frontend/js/load-image-orientation.js @@ -0,0 +1,188 @@ +/* + * JavaScript Load Image Orientation + * https://github.com/blueimp/JavaScript-Load-Image + * + * Copyright 2013, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * https://opensource.org/licenses/MIT + */ + +/* global define, module, require */ + +;(function(factory) { + 'use strict' + if (typeof define === 'function' && define.amd) { + // Register as an anonymous AMD module: + define(['./load-image', './load-image-scale', './load-image-meta'], factory) + } else if (typeof module === 'object' && module.exports) { + factory( + require('./load-image'), + require('./load-image-scale'), + require('./load-image-meta') + ) + } else { + // Browser globals: + factory(window.loadImage) + } +})(function(loadImage) { + 'use strict' + + var originalHasCanvasOption = loadImage.hasCanvasOption + var originalHasMetaOption = loadImage.hasMetaOption + var originalTransformCoordinates = loadImage.transformCoordinates + var originalGetTransformedOptions = loadImage.getTransformedOptions + + // Determines if the target image should be a canvas element: + loadImage.hasCanvasOption = function(options) { + return ( + !!options.orientation || originalHasCanvasOption.call(loadImage, options) + ) + } + + // Determines if meta data should be loaded automatically: + loadImage.hasMetaOption = function(options) { + return ( + (options && options.orientation === true) || + originalHasMetaOption.call(loadImage, options) + ) + } + + // Transform image orientation based on + // the given EXIF orientation option: + loadImage.transformCoordinates = function(canvas, options) { + originalTransformCoordinates.call(loadImage, canvas, options) + var ctx = canvas.getContext('2d') + var width = canvas.width + var height = canvas.height + var styleWidth = canvas.style.width + var styleHeight = canvas.style.height + var orientation = options.orientation + if (!orientation || orientation > 8) { + return + } + if (orientation > 4) { + canvas.width = height + canvas.height = width + canvas.style.width = styleHeight + canvas.style.height = styleWidth + } + switch (orientation) { + case 2: + // horizontal flip + ctx.translate(width, 0) + ctx.scale(-1, 1) + break + case 3: + // 180° rotate left + ctx.translate(width, height) + ctx.rotate(Math.PI) + break + case 4: + // vertical flip + ctx.translate(0, height) + ctx.scale(1, -1) + break + case 5: + // vertical flip + 90 rotate right + ctx.rotate(0.5 * Math.PI) + ctx.scale(1, -1) + break + case 6: + // 90° rotate right + ctx.rotate(0.5 * Math.PI) + ctx.translate(0, -height) + break + case 7: + // horizontal flip + 90 rotate right + ctx.rotate(0.5 * Math.PI) + ctx.translate(width, -height) + ctx.scale(-1, 1) + break + case 8: + // 90° rotate left + ctx.rotate(-0.5 * Math.PI) + ctx.translate(-width, 0) + break + } + } + + // Transforms coordinate and dimension options + // based on the given orientation option: + loadImage.getTransformedOptions = function(img, opts, data) { + var options = originalGetTransformedOptions.call(loadImage, img, opts) + var orientation = options.orientation + var newOptions + var i + if (orientation === true && data && data.exif) { + orientation = data.exif.get('Orientation') + } + if (!orientation || orientation > 8 || orientation === 1) { + return options + } + newOptions = {} + for (i in options) { + if (Object.prototype.hasOwnProperty.call(options, i)) { + newOptions[i] = options[i] + } + } + newOptions.orientation = orientation + switch (orientation) { + case 2: + // horizontal flip + newOptions.left = options.right + newOptions.right = options.left + break + case 3: + // 180° rotate left + newOptions.left = options.right + newOptions.top = options.bottom + newOptions.right = options.left + newOptions.bottom = options.top + break + case 4: + // vertical flip + newOptions.top = options.bottom + newOptions.bottom = options.top + break + case 5: + // vertical flip + 90 rotate right + newOptions.left = options.top + newOptions.top = options.left + newOptions.right = options.bottom + newOptions.bottom = options.right + break + case 6: + // 90° rotate right + newOptions.left = options.top + newOptions.top = options.right + newOptions.right = options.bottom + newOptions.bottom = options.left + break + case 7: + // horizontal flip + 90 rotate right + newOptions.left = options.bottom + newOptions.top = options.right + newOptions.right = options.top + newOptions.bottom = options.left + break + case 8: + // 90° rotate left + newOptions.left = options.bottom + newOptions.top = options.left + newOptions.right = options.top + newOptions.bottom = options.right + break + } + if (newOptions.orientation > 4) { + newOptions.maxWidth = options.maxHeight + newOptions.maxHeight = options.maxWidth + newOptions.minWidth = options.minHeight + newOptions.minHeight = options.minWidth + newOptions.sourceWidth = options.sourceHeight + newOptions.sourceHeight = options.sourceWidth + } + return newOptions + } +}) diff --git a/frontend/js/load-image-scale.js b/frontend/js/load-image-scale.js new file mode 100644 index 0000000..39be9a5 --- /dev/null +++ b/frontend/js/load-image-scale.js @@ -0,0 +1,293 @@ +/* + * JavaScript Load Image Scaling + * https://github.com/blueimp/JavaScript-Load-Image + * + * Copyright 2011, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * https://opensource.org/licenses/MIT + */ + +/* global define, module, require */ + +;(function(factory) { + 'use strict' + if (typeof define === 'function' && define.amd) { + // Register as an anonymous AMD module: + define(['./load-image'], factory) + } else if (typeof module === 'object' && module.exports) { + factory(require('./load-image')) + } else { + // Browser globals: + factory(window.loadImage) + } +})(function(loadImage) { + 'use strict' + + var originalTransform = loadImage.transform + + loadImage.transform = function(img, options, callback, file, data) { + originalTransform.call( + loadImage, + loadImage.scale(img, options, data), + options, + callback, + file, + data + ) + } + + // Transform image coordinates, allows to override e.g. + // the canvas orientation based on the orientation option, + // gets canvas, options passed as arguments: + loadImage.transformCoordinates = function() {} + + // Returns transformed options, allows to override e.g. + // maxWidth, maxHeight and crop options based on the aspectRatio. + // gets img, options passed as arguments: + loadImage.getTransformedOptions = function(img, options) { + var aspectRatio = options.aspectRatio + var newOptions + var i + var width + var height + if (!aspectRatio) { + return options + } + newOptions = {} + for (i in options) { + if (Object.prototype.hasOwnProperty.call(options, i)) { + newOptions[i] = options[i] + } + } + newOptions.crop = true + width = img.naturalWidth || img.width + height = img.naturalHeight || img.height + if (width / height > aspectRatio) { + newOptions.maxWidth = height * aspectRatio + newOptions.maxHeight = height + } else { + newOptions.maxWidth = width + newOptions.maxHeight = width / aspectRatio + } + return newOptions + } + + // Canvas render method, allows to implement a different rendering algorithm: + loadImage.renderImageToCanvas = function( + canvas, + img, + sourceX, + sourceY, + sourceWidth, + sourceHeight, + destX, + destY, + destWidth, + destHeight + ) { + canvas + .getContext('2d') + .drawImage( + img, + sourceX, + sourceY, + sourceWidth, + sourceHeight, + destX, + destY, + destWidth, + destHeight + ) + return canvas + } + + // Determines if the target image should be a canvas element: + loadImage.hasCanvasOption = function(options) { + return options.canvas || options.crop || !!options.aspectRatio + } + + // Scales and/or crops the given image (img or canvas HTML element) + // using the given options. + // Returns a canvas object if the browser supports canvas + // and the hasCanvasOption method returns true or a canvas + // object is passed as image, else the scaled image: + loadImage.scale = function(img, options, data) { + // eslint-disable-next-line no-param-reassign + options = options || {} + var canvas = document.createElement('canvas') + var useCanvas = + img.getContext || + (loadImage.hasCanvasOption(options) && canvas.getContext) + var width = img.naturalWidth || img.width + var height = img.naturalHeight || img.height + var destWidth = width + var destHeight = height + var maxWidth + var maxHeight + var minWidth + var minHeight + var sourceWidth + var sourceHeight + var sourceX + var sourceY + var pixelRatio + var downsamplingRatio + var tmp + /** + * Scales up image dimensions + */ + function scaleUp() { + var scale = Math.max( + (minWidth || destWidth) / destWidth, + (minHeight || destHeight) / destHeight + ) + if (scale > 1) { + destWidth *= scale + destHeight *= scale + } + } + /** + * Scales down image dimensions + */ + function scaleDown() { + var scale = Math.min( + (maxWidth || destWidth) / destWidth, + (maxHeight || destHeight) / destHeight + ) + if (scale < 1) { + destWidth *= scale + destHeight *= scale + } + } + if (useCanvas) { + // eslint-disable-next-line no-param-reassign + options = loadImage.getTransformedOptions(img, options, data) + sourceX = options.left || 0 + sourceY = options.top || 0 + if (options.sourceWidth) { + sourceWidth = options.sourceWidth + if (options.right !== undefined && options.left === undefined) { + sourceX = width - sourceWidth - options.right + } + } else { + sourceWidth = width - sourceX - (options.right || 0) + } + if (options.sourceHeight) { + sourceHeight = options.sourceHeight + if (options.bottom !== undefined && options.top === undefined) { + sourceY = height - sourceHeight - options.bottom + } + } else { + sourceHeight = height - sourceY - (options.bottom || 0) + } + destWidth = sourceWidth + destHeight = sourceHeight + } + maxWidth = options.maxWidth + maxHeight = options.maxHeight + minWidth = options.minWidth + minHeight = options.minHeight + if (useCanvas && maxWidth && maxHeight && options.crop) { + destWidth = maxWidth + destHeight = maxHeight + tmp = sourceWidth / sourceHeight - maxWidth / maxHeight + if (tmp < 0) { + sourceHeight = (maxHeight * sourceWidth) / maxWidth + if (options.top === undefined && options.bottom === undefined) { + sourceY = (height - sourceHeight) / 2 + } + } else if (tmp > 0) { + sourceWidth = (maxWidth * sourceHeight) / maxHeight + if (options.left === undefined && options.right === undefined) { + sourceX = (width - sourceWidth) / 2 + } + } + } else { + if (options.contain || options.cover) { + minWidth = maxWidth = maxWidth || minWidth + minHeight = maxHeight = maxHeight || minHeight + } + if (options.cover) { + scaleDown() + scaleUp() + } else { + scaleUp() + scaleDown() + } + } + if (useCanvas) { + pixelRatio = options.pixelRatio + if (pixelRatio > 1) { + canvas.style.width = destWidth + 'px' + canvas.style.height = destHeight + 'px' + destWidth *= pixelRatio + destHeight *= pixelRatio + canvas.getContext('2d').scale(pixelRatio, pixelRatio) + } + downsamplingRatio = options.downsamplingRatio + if ( + downsamplingRatio > 0 && + downsamplingRatio < 1 && + destWidth < sourceWidth && + destHeight < sourceHeight + ) { + while (sourceWidth * downsamplingRatio > destWidth) { + canvas.width = sourceWidth * downsamplingRatio + canvas.height = sourceHeight * downsamplingRatio + loadImage.renderImageToCanvas( + canvas, + img, + sourceX, + sourceY, + sourceWidth, + sourceHeight, + 0, + 0, + canvas.width, + canvas.height + ) + sourceX = 0 + sourceY = 0 + sourceWidth = canvas.width + sourceHeight = canvas.height + // eslint-disable-next-line no-param-reassign + img = document.createElement('canvas') + img.width = sourceWidth + img.height = sourceHeight + loadImage.renderImageToCanvas( + img, + canvas, + 0, + 0, + sourceWidth, + sourceHeight, + 0, + 0, + sourceWidth, + sourceHeight + ) + } + } + canvas.width = destWidth + canvas.height = destHeight + loadImage.transformCoordinates(canvas, options) + return loadImage.renderImageToCanvas( + canvas, + img, + sourceX, + sourceY, + sourceWidth, + sourceHeight, + 0, + 0, + destWidth, + destHeight + ) + } + img.width = destWidth + img.height = destHeight + return img + } +}) diff --git a/frontend/js/load-image.all.min.js b/frontend/js/load-image.all.min.js new file mode 100644 index 0000000..3448289 --- /dev/null +++ b/frontend/js/load-image.all.min.js @@ -0,0 +1,2 @@ +!function(o){"use strict";function r(t,i,a){var o,n=document.createElement("img");return n.onerror=function(e){return r.onerror(n,e,t,i,a)},n.onload=function(e){return r.onload(n,e,t,i,a)},"string"==typeof t?(r.fetchBlob(t,function(e){e?o=r.createObjectURL(t=e):(o=t,a&&a.crossOrigin&&(n.crossOrigin=a.crossOrigin)),n.src=o},a),n):r.isInstanceOf("Blob",t)||r.isInstanceOf("File",t)?(o=n._objectURL=r.createObjectURL(t))?(n.src=o,n):r.readFile(t,function(e){var t=e.target;t&&t.result?n.src=t.result:i&&i(e)}):void 0}var t=o.createObjectURL&&o||o.URL&&URL.revokeObjectURL&&URL||o.webkitURL&&webkitURL;function n(e,t){!e._objectURL||t&&t.noRevoke||(r.revokeObjectURL(e._objectURL),delete e._objectURL)}r.fetchBlob=function(e,t){t()},r.isInstanceOf=function(e,t){return Object.prototype.toString.call(t)==="[object "+e+"]"},r.transform=function(e,t,i,a,o){i(e,o)},r.onerror=function(e,t,i,a,o){n(e,o),a&&a.call(e,t)},r.onload=function(e,t,i,a,o){n(e,o),a&&r.transform(e,o,a,i,{originalWidth:e.naturalWidth||e.width,originalHeight:e.naturalHeight||e.height})},r.createObjectURL=function(e){return!!t&&t.createObjectURL(e)},r.revokeObjectURL=function(e){return!!t&&t.revokeObjectURL(e)},r.readFile=function(e,t,i){if(o.FileReader){var a=new FileReader;if(a.onload=a.onerror=t,a[i=i||"readAsDataURL"])return a[i](e),a}return!1},"function"==typeof define&&define.amd?define(function(){return r}):"object"==typeof module&&module.exports?module.exports=r:o.loadImage=r}("undefined"!=typeof window&&window||this),function(e){"use strict";"function"==typeof define&&define.amd?define(["./load-image"],e):"object"==typeof module&&module.exports?e(require("./load-image")):e(window.loadImage)}(function(v){"use strict";var n=v.transform;v.transform=function(e,t,i,a,o){n.call(v,v.scale(e,t,o),t,i,a,o)},v.transformCoordinates=function(){},v.getTransformedOptions=function(e,t){var i,a,o,n,r=t.aspectRatio;if(!r)return t;for(a in i={},t)Object.prototype.hasOwnProperty.call(t,a)&&(i[a]=t[a]);return i.crop=!0,r<(o=e.naturalWidth||e.width)/(n=e.naturalHeight||e.height)?(i.maxWidth=n*r,i.maxHeight=n):(i.maxWidth=o,i.maxHeight=o/r),i},v.renderImageToCanvas=function(e,t,i,a,o,n,r,s,l,c){return e.getContext("2d").drawImage(t,i,a,o,n,r,s,l,c),e},v.hasCanvasOption=function(e){return e.canvas||e.crop||!!e.aspectRatio},v.scale=function(e,t,i){t=t||{};var a,o,n,r,s,l,c,d,u,f,g,p=document.createElement("canvas"),h=e.getContext||v.hasCanvasOption(t)&&p.getContext,m=e.naturalWidth||e.width,b=e.naturalHeight||e.height,y=m,S=b;function x(){var e=Math.max((n||y)/y,(r||S)/S);1r.byteLength){console.log("Invalid meta data: Invalid segment size.");break}if(a=p.metaDataParsers.jpeg[t])for(o=0;oe.byteLength))return g.createObjectURL(new Blob([e.buffer.slice(t,t+i)]));console.log("Invalid Exif data: Invalid thumbnail data.")},g.exifTagTypes={1:{getValue:function(e,t){return e.getUint8(t)},size:1},2:{getValue:function(e,t){return String.fromCharCode(e.getUint8(t))},size:1,ascii:!0},3:{getValue:function(e,t,i){return e.getUint16(t,i)},size:2},4:{getValue:function(e,t,i){return e.getUint32(t,i)},size:4},5:{getValue:function(e,t,i){return e.getUint32(t,i)/e.getUint32(t+4,i)},size:8},9:{getValue:function(e,t,i){return e.getInt32(t,i)},size:4},10:{getValue:function(e,t,i){return e.getInt32(t,i)/e.getInt32(t+4,i)},size:8}},g.exifTagTypes[7]=g.exifTagTypes[1],g.getExifValue=function(e,t,i,a,o,n){var r,s,l,c,d,u,f=g.exifTagTypes[a];if(f){if(!((s=4<(r=f.size*o)?t+e.getUint32(i+8,n):i+8)+r>e.byteLength)){if(1===o)return f.getValue(e,s,n);for(l=[],c=0;ce.byteLength)console.log("Invalid Exif data: Invalid directory offset.");else{if(!((r=i+2+12*(n=e.getUint16(i,a)))+4>e.byteLength)){for(s=0;se.byteLength)console.log("Invalid Exif data: Invalid segment size.");else if(0===e.getUint16(t+8)){switch(e.getUint16(l)){case 18761:n=!0;break;case 19789:n=!1;break;default:return void console.log("Invalid Exif data: Invalid byte alignment marker.")}42===e.getUint16(l+2,n)?(r=e.getUint32(l+4,n),a.exif=new g.ExifMap,(r=g.parseExifTags(e,l,l+r,n,a))&&!o.disableExifThumbnail&&(s={exif:{}},r=g.parseExifTags(e,l,l+r,n,s),s.exif[513]&&(a.exif.Thumbnail=g.getExifThumbnail(e,l+s.exif[513],s.exif[514]))),a.exif[34665]&&!o.disableExifSub&&g.parseExifTags(e,l,l+a.exif[34665],n,a),a.exif[34853]&&!o.disableExifGps&&g.parseExifTags(e,l,l+a.exif[34853],n,a)):console.log("Invalid Exif data: Missing TIFF marker.")}else console.log("Invalid Exif data: Missing byte alignment offset.")}},g.metaDataParsers.jpeg[65505].push(g.parseExifData)}),function(e){"use strict";"function"==typeof define&&define.amd?define(["./load-image","./load-image-exif"],e):"object"==typeof module&&module.exports?e(require("./load-image"),require("./load-image-exif")):e(window.loadImage)}(function(e){"use strict";e.ExifMap.prototype.tags={256:"ImageWidth",257:"ImageHeight",34665:"ExifIFDPointer",34853:"GPSInfoIFDPointer",40965:"InteroperabilityIFDPointer",258:"BitsPerSample",259:"Compression",262:"PhotometricInterpretation",274:"Orientation",277:"SamplesPerPixel",284:"PlanarConfiguration",530:"YCbCrSubSampling",531:"YCbCrPositioning",282:"XResolution",283:"YResolution",296:"ResolutionUnit",273:"StripOffsets",278:"RowsPerStrip",279:"StripByteCounts",513:"JPEGInterchangeFormat",514:"JPEGInterchangeFormatLength",301:"TransferFunction",318:"WhitePoint",319:"PrimaryChromaticities",529:"YCbCrCoefficients",532:"ReferenceBlackWhite",306:"DateTime",270:"ImageDescription",271:"Make",272:"Model",305:"Software",315:"Artist",33432:"Copyright",36864:"ExifVersion",40960:"FlashpixVersion",40961:"ColorSpace",40962:"PixelXDimension",40963:"PixelYDimension",42240:"Gamma",37121:"ComponentsConfiguration",37122:"CompressedBitsPerPixel",37500:"MakerNote",37510:"UserComment",40964:"RelatedSoundFile",36867:"DateTimeOriginal",36868:"DateTimeDigitized",37520:"SubSecTime",37521:"SubSecTimeOriginal",37522:"SubSecTimeDigitized",33434:"ExposureTime",33437:"FNumber",34850:"ExposureProgram",34852:"SpectralSensitivity",34855:"PhotographicSensitivity",34856:"OECF",34864:"SensitivityType",34865:"StandardOutputSensitivity",34866:"RecommendedExposureIndex",34867:"ISOSpeed",34868:"ISOSpeedLatitudeyyy",34869:"ISOSpeedLatitudezzz",37377:"ShutterSpeedValue",37378:"ApertureValue",37379:"BrightnessValue",37380:"ExposureBias",37381:"MaxApertureValue",37382:"SubjectDistance",37383:"MeteringMode",37384:"LightSource",37385:"Flash",37396:"SubjectArea",37386:"FocalLength",41483:"FlashEnergy",41484:"SpatialFrequencyResponse",41486:"FocalPlaneXResolution",41487:"FocalPlaneYResolution",41488:"FocalPlaneResolutionUnit",41492:"SubjectLocation",41493:"ExposureIndex",41495:"SensingMethod",41728:"FileSource",41729:"SceneType",41730:"CFAPattern",41985:"CustomRendered",41986:"ExposureMode",41987:"WhiteBalance",41988:"DigitalZoomRatio",41989:"FocalLengthIn35mmFilm",41990:"SceneCaptureType",41991:"GainControl",41992:"Contrast",41993:"Saturation",41994:"Sharpness",41995:"DeviceSettingDescription",41996:"SubjectDistanceRange",42016:"ImageUniqueID",42032:"CameraOwnerName",42033:"BodySerialNumber",42034:"LensSpecification",42035:"LensMake",42036:"LensModel",42037:"LensSerialNumber",0:"GPSVersionID",1:"GPSLatitudeRef",2:"GPSLatitude",3:"GPSLongitudeRef",4:"GPSLongitude",5:"GPSAltitudeRef",6:"GPSAltitude",7:"GPSTimeStamp",8:"GPSSatellites",9:"GPSStatus",10:"GPSMeasureMode",11:"GPSDOP",12:"GPSSpeedRef",13:"GPSSpeed",14:"GPSTrackRef",15:"GPSTrack",16:"GPSImgDirectionRef",17:"GPSImgDirection",18:"GPSMapDatum",19:"GPSDestLatitudeRef",20:"GPSDestLatitude",21:"GPSDestLongitudeRef",22:"GPSDestLongitude",23:"GPSDestBearingRef",24:"GPSDestBearing",25:"GPSDestDistanceRef",26:"GPSDestDistance",27:"GPSProcessingMethod",28:"GPSAreaInformation",29:"GPSDateStamp",30:"GPSDifferential",31:"GPSHPositioningError"},e.ExifMap.prototype.stringValues={ExposureProgram:{0:"Undefined",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:{0:"Flash did not fire",1:"Flash fired",5:"Strobe return light not detected",7:"Strobe return light detected",9:"Flash fired, compulsory flash mode",13:"Flash fired, compulsory flash mode, return light not detected",15:"Flash fired, compulsory flash mode, return light detected",16:"Flash did not fire, compulsory flash mode",24:"Flash did not fire, auto mode",25:"Flash fired, auto mode",29:"Flash fired, auto mode, return light not detected",31:"Flash fired, auto mode, return light detected",32:"No flash function",65:"Flash fired, red-eye reduction mode",69:"Flash fired, red-eye reduction mode, return light not detected",71:"Flash fired, red-eye reduction mode, return light detected",73:"Flash fired, compulsory flash mode, red-eye reduction mode",77:"Flash fired, compulsory flash mode, red-eye reduction mode, return light not detected",79:"Flash fired, compulsory flash mode, red-eye reduction mode, return light detected",89:"Flash fired, auto mode, red-eye reduction mode",93:"Flash fired, auto mode, return light not detected, red-eye reduction mode",95:"Flash fired, auto mode, return light detected, red-eye reduction mode"},SensingMethod:{1:"Undefined",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"},ComponentsConfiguration:{0:"",1:"Y",2:"Cb",3:"Cr",4:"R",5:"G",6:"B"},Orientation:{1:"top-left",2:"top-right",3:"bottom-right",4:"bottom-left",5:"left-top",6:"right-top",7:"right-bottom",8:"left-bottom"}},e.ExifMap.prototype.getText=function(e){var t=this.get(e);switch(e){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":case"Orientation":return this.stringValues[e][t];case"ExifVersion":case"FlashpixVersion":if(!t)return;return String.fromCharCode(t[0],t[1],t[2],t[3]);case"ComponentsConfiguration":if(!t)return;return this.stringValues[e][t[0]]+this.stringValues[e][t[1]]+this.stringValues[e][t[2]]+this.stringValues[e][t[3]];case"GPSVersionID":if(!t)return;return t[0]+"."+t[1]+"."+t[2]+"."+t[3]}return String(t)},function(e){var t,i=e.tags,a=e.map;for(t in i)Object.prototype.hasOwnProperty.call(i,t)&&(a[i[t]]=t)}(e.ExifMap.prototype),e.ExifMap.prototype.getAll=function(){var e,t,i={};for(e in this)Object.prototype.hasOwnProperty.call(this,e)&&(t=this.tags[e])&&(i[t]=this.getText(t));return i}}),function(e){"use strict";"function"==typeof define&&define.amd?define(["./load-image","./load-image-meta"],e):"object"==typeof module&&module.exports?e(require("./load-image"),require("./load-image-meta")):e(window.loadImage)}(function(u){"use strict";u.IptcMap=function(){return this},u.IptcMap.prototype.map={ObjectName:5},u.IptcMap.prototype.get=function(e){return this[e]||this[this.map[e]]},u.parseIptcTags=function(e,t,i,a){function o(e,t,i){for(var a="",o=t;o + + + + + + + + + + + + diff --git a/server/routes/photos.js b/server/routes/photos.js index 322db5a..311a485 100755 --- a/server/routes/photos.js +++ b/server/routes/photos.js @@ -743,6 +743,79 @@ router.get("/trash", function(req, res/*, next*/) { }); }); +router.get("/mvimg/*", function(req, res/*, next*/) { + let limit = parseInt(req.query.limit) || 50, + id, cursor, index; + + if (req.query.next) { + let parts = req.query.next.split("_"); + cursor = parts[0]; + id = parseInt(parts[1]); + } else { + cursor = ""; + id = -1; + } + + if (id == -1) { + index = ""; + } else { + index = "AND ((strftime('%Y-%m-%d',taken)=strftime('%Y-%m-%d',:cursor) AND photos.id<:id) OR " + + "strftime('%Y-%m-%d',taken) limit; /* We queried one extra item to see if there are more than LIMIT available */ + +// console.log("Requested " + limit + " and matched " + photos.length); + + let last; + if (more) { + photos.splice(limit); + last = photos[photos.length - 1]; + } + + let results = { + items: photos.sort(function(a, b) { + return new Date(b.taken) - new Date(a.taken); + }) + }; + + if (more) { + results.cursor = new Date(last.taken).toISOString().replace(/T.*/, "") + "_" + last.id; + results.more = true; + } + return res.status(200).json(results); + }).catch(function(error) { + console.error("Query failed: " + query); + return Promise.reject(error); + }); +}); + router.get("/*", function(req, res/*, next*/) { let limit = parseInt(req.query.limit) || 50, id, cursor, index;