Added MVIMG support!

Signed-off-by: James Ketrenos <james_gitlab@ketrenos.com>
This commit is contained in:
James Ketrenos 2019-11-28 02:56:45 -08:00
parent b0c7f61e58
commit 017560fa1a
14 changed files with 2126 additions and 0 deletions

12
frontend/js/LICENSE Normal file
View File

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

View File

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

View File

@ -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.
})

View File

@ -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()
}
}
})

View File

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

View File

@ -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<EOT><EOT>' ?
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.
})

View File

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

View File

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

View File

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

2
frontend/js/load-image.all.min.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

163
frontend/js/load-image.js Normal file
View File

@ -0,0 +1,163 @@
/*
* JavaScript Load Image
* 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, webkitURL, module */
;(function($) {
'use strict'
/**
* Loads an image for a given File object.
* Invokes the callback with an img or optional canvas element
* (if supported by the browser) as parameter:.
*
* @param {File|Blob|string} file File or Blob object or image URL
* @param {Function} [callback] Image load event callback
* @param {object} [options] Options object
* @returns {HTMLImageElement|HTMLCanvasElement|FileReader} image object
*/
function loadImage(file, callback, options) {
var img = document.createElement('img')
var url
img.onerror = function(event) {
return loadImage.onerror(img, event, file, callback, options)
}
img.onload = function(event) {
return loadImage.onload(img, event, file, callback, options)
}
if (typeof file === 'string') {
loadImage.fetchBlob(
file,
function(blob) {
if (blob) {
// eslint-disable-next-line no-param-reassign
file = blob
url = loadImage.createObjectURL(file)
} else {
url = file
if (options && options.crossOrigin) {
img.crossOrigin = options.crossOrigin
}
}
img.src = url
},
options
)
return img
} else if (
loadImage.isInstanceOf('Blob', file) ||
// Files are also Blob instances, but some browsers
// (Firefox 3.6) support the File API but not Blobs:
loadImage.isInstanceOf('File', file)
) {
url = img._objectURL = loadImage.createObjectURL(file)
if (url) {
img.src = url
return img
}
return loadImage.readFile(file, function(e) {
var target = e.target
if (target && target.result) {
img.src = target.result
} else if (callback) {
callback(e)
}
})
}
}
// The check for URL.revokeObjectURL fixes an issue with Opera 12,
// which provides URL.createObjectURL but doesn't properly implement it:
var urlAPI =
($.createObjectURL && $) ||
($.URL && URL.revokeObjectURL && URL) ||
($.webkitURL && webkitURL)
/**
* Helper function to revoke an object URL
*
* @param {HTMLImageElement} img Image element
* @param {object} [options] Options object
*/
function revokeHelper(img, options) {
if (img._objectURL && !(options && options.noRevoke)) {
loadImage.revokeObjectURL(img._objectURL)
delete img._objectURL
}
}
// If the callback given to this function returns a blob, it is used as image
// source instead of the original url and overrides the file argument used in
// the onload and onerror event callbacks:
loadImage.fetchBlob = function(url, callback) {
callback()
}
loadImage.isInstanceOf = function(type, obj) {
// Cross-frame instanceof check
return Object.prototype.toString.call(obj) === '[object ' + type + ']'
}
loadImage.transform = function(img, options, callback, file, data) {
callback(img, data)
}
loadImage.onerror = function(img, event, file, callback, options) {
revokeHelper(img, options)
if (callback) {
callback.call(img, event)
}
}
loadImage.onload = function(img, event, file, callback, options) {
revokeHelper(img, options)
if (callback) {
loadImage.transform(img, options, callback, file, {
originalWidth: img.naturalWidth || img.width,
originalHeight: img.naturalHeight || img.height
})
}
}
loadImage.createObjectURL = function(file) {
return urlAPI ? urlAPI.createObjectURL(file) : false
}
loadImage.revokeObjectURL = function(url) {
return urlAPI ? urlAPI.revokeObjectURL(url) : false
}
// Loads a given File object via FileReader interface,
// invokes the callback with the event object (load or error).
// The result can be read via event.target.result:
loadImage.readFile = function(file, callback, method) {
if ($.FileReader) {
var fileReader = new FileReader()
fileReader.onload = fileReader.onerror = callback
// eslint-disable-next-line no-param-reassign
method = method || 'readAsDataURL'
if (fileReader[method]) {
fileReader[method](file)
return fileReader
}
}
return false
}
if (typeof define === 'function' && define.amd) {
define(function() {
return loadImage
})
} else if (typeof module === 'object' && module.exports) {
module.exports = loadImage
} else {
$.loadImage = loadImage
}
})((typeof window !== 'undefined' && window) || this)

173
frontend/mvimg.html Normal file
View File

@ -0,0 +1,173 @@
<html>
<script>'<base href="BASEPATH">';</script>
<style>
.static {
display: inline-block;
margin: 1em;
position: relative;
width: 320px;
height: 240px;
background-position: center;
background-size: contain;
background-color: black;
cursor: pointer;
overflow: hidden;
opacity: 0;
}
.static canvas {
display: inline-block;
pointer-events: none;
position: absolute;
top: 0px;
z-index: 0;
}
.video {
border: none;
z-index: 10;
position: relative;
display: inline-block;
opacity: 0;
width: 100%;
height: 100%;
}
.video:hover {
opacity: 1;
}
.video.loading:hover {
opacity: 0.5;
}
</style>
<script src="js/load-image.js"></script>
<script src="js/load-image-scale.js"></script>
<script src="js/load-image-meta.js"></script>
<script src="js/load-image-orientation.js"></script>
<script src="js/load-image-exif.js"></script>
<script src="js/load-image-exif-map.js"></script>
<script>
function createMvElement(url) {
var static = document.createElement("div");
static.classList.add("static");
var video = document.createElement("video");
video.classList.add("video");
video.classList.add("loading");
video.setAttribute("controls", true);
video.setAttribute("loop", true);
video.setAttribute("autoplay", true);
var xmlhttp = new XMLHttpRequest();
xmlhttp.open("GET", url, true);
xmlhttp.responseType = 'arraybuffer';
xmlhttp.onprogress = function (event) {
var alpha = 0;
if (event.total) {
alpha = event.loaded / event.total;
// console.log("Loaded: " + Math.ceil(100 * alpha) + "%");
} else {
// console.log("Loaded: N/A");
alhpa = 0;
}
static.style.opacity = alpha;
};
xmlhttp.onload = function(event) {
static.style.opacity = 1;
var uInt8Array = new Uint8Array(this.response),
i, codes = new Uint8Array(8);
codes.forEach(function(char, index) {
codes[index] = "ftypmp42".charCodeAt(index);
});
/* MP4 starts 4 bytes prior to the ftypmp42 header */
for (i = 4; i < uInt8Array.length - 8; i++) {
if (
(uInt8Array[i + 0] == codes[0]) &&
(uInt8Array[i + 1] == codes[1]) &&
(uInt8Array[i + 2] == codes[2]) &&
(uInt8Array[i + 3] == codes[3]) &&
(uInt8Array[i + 4] == codes[4]) &&
(uInt8Array[i + 5] == codes[5]) &&
(uInt8Array[i + 6] == codes[6]) &&
(uInt8Array[i + 7] == codes[7])
) {
break;
}
}
if (i == uInt8Array.length - 8) {
console.log("No MP4 found.");
i = this.response.byteLength;
} else {
console.log("Found MP4 at: " + i);
i -= 4; /* Go back to the start of the MP4 */
}
/* https://github.com/blueimp/JavaScript-Load-Image */
var imageBlob = new Blob([this.response.slice(0, i)], { type: "image/jpeg" }),
options = {
maxWidth: static.offsetWidth,
maxHeight: static.offsetHeight,
canvas: true,
pixelRatio: window.devicePixelRatio,
downsamplingRatio: 0.5,
meta: true,
contain: true,
orientation: true
};
/* Load the image and center it in our element */
loadImage(imageBlob, function (image, data) {
/*
if (data.exif) {
console.log("Orientation: ", data.exif.get('Orientation'));
}
*/
image.style.left = Math.floor((static.offsetWidth - image.width) * 0.5) + "px";
static.appendChild(image);
}, options);
/* If an MP4 wasn't found, we're done */
if (i == this.response.byteLength) {
return;
}
static.appendChild(video);
/* Set the src= *after* the element has been added to the DOM */
setTimeout(function(buffer) {
/* This can take some time to chunk through... can we spin it into
* a WebWorker or do something to prevent it from janking the UI
* thread? Maybe split it into chunks and do it 100k at a time or
* something? Or queue them up so only one is being processed? */
var base64 = window.btoa(new Uint8Array(buffer).reduce(function (data, byte) {
return data + String.fromCharCode(byte);
}, ''));
video.src = 'data:video/mp4;base64,' + base64;
video.classList.remove("loading");
}.bind(this, this.response.slice(i)), 5000);
};
xmlhttp.send();
return static;
}
document.addEventListener("DOMContentLoaded", function() {
window.fetch("api/v1/photos/mvimg/")
.then(res => res.json())
.then((data) => {
data.items.forEach((movie) => {
document.body.appendChild(createMvElement(movie.filename));
});
}).catch((error) => {
console.error(error);
});
});
</script>
<body>
</body>

View File

@ -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)<strftime('%Y-%m-%d',:cursor))";
}
console.log(req.url);
let path = decodeURI(req.url).replace(/\?.*$/, "").replace(/^\//, "").replace(/^mvimg\//, ""),
query = "SELECT photos.*,albums.path AS path FROM photos " +
"INNER JOIN albums ON (albums.id=photos.albumId AND albums.path LIKE :path) " +
"WHERE (photos.name LIKE 'MVIMG%' AND photos.duplicate=0 AND photos.deleted=0 AND photos.scanned NOT NULL) " + index + " " +
"ORDER BY strftime('%Y-%m-%d',taken) DESC,id DESC LIMIT " + (limit + 1),
replacements = {
id: id,
cursor: cursor,
path: path + "%"
};
console.log("Trying path as: " + path);
// console.log("Fetching from: " + JSON.stringify(replacements, null, 2) + "\nwith:\n" + query);
return photoDB.sequelize.query(query, {
replacements: replacements,
type: photoDB.Sequelize.QueryTypes.SELECT
}).then(function(photos) {
photos.forEach(function(photo) {
for (var key in photo) {
if (photo[key] instanceof Date) {
photo[key] = moment(photo[key]);
}
}
});
let more = photos.length > 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*/) { router.get("/*", function(req, res/*, next*/) {
let limit = parseInt(req.query.limit) || 50, let limit = parseInt(req.query.limit) || 50,
id, cursor, index; id, cursor, index;