diff --git a/wcs/qommon/http_response.py b/wcs/qommon/http_response.py index 165c07aca..f93b2858c 100644 --- a/wcs/qommon/http_response.py +++ b/wcs/qommon/http_response.py @@ -87,6 +87,7 @@ class HTTPResponse(quixote.http_response.HTTPResponse): 'jquery.js', 'jquery-ui.js', 'jquery.iframe-transport.js', + 'exif.js', 'jquery.fileupload.js', ] ) diff --git a/wcs/qommon/static/js/exif.js b/wcs/qommon/static/js/exif.js new file mode 100644 index 000000000..2d44cd075 --- /dev/null +++ b/wcs/qommon/static/js/exif.js @@ -0,0 +1,1064 @@ +/* Copied from https://github.com/exif-js/exif-js/blob/v2.3.0/exif.js + * Copyright (c) 2008 Jacob Seidelin + * License: MIT, https://github.com/exif-js/exif-js/blob/v2.3.0/LICENSE.md + */ + +(function() { + + var debug = false; + + var root = this; + + var EXIF = function(obj) { + if (obj instanceof EXIF) return obj; + if (!(this instanceof EXIF)) return new EXIF(obj); + this.EXIFwrapped = obj; + }; + + if (typeof exports !== 'undefined') { + if (typeof module !== 'undefined' && module.exports) { + exports = module.exports = EXIF; + } + exports.EXIF = EXIF; + } else { + root.EXIF = EXIF; + } + + var ExifTags = EXIF.Tags = { + + // version tags + 0x9000 : "ExifVersion", // EXIF version + 0xA000 : "FlashpixVersion", // Flashpix format version + + // colorspace tags + 0xA001 : "ColorSpace", // Color space information tag + + // image configuration + 0xA002 : "PixelXDimension", // Valid width of meaningful image + 0xA003 : "PixelYDimension", // Valid height of meaningful image + 0x9101 : "ComponentsConfiguration", // Information about channels + 0x9102 : "CompressedBitsPerPixel", // Compressed bits per pixel + + // user information + 0x927C : "MakerNote", // Any desired information written by the manufacturer + 0x9286 : "UserComment", // Comments by user + + // related file + 0xA004 : "RelatedSoundFile", // Name of related sound file + + // date and time + 0x9003 : "DateTimeOriginal", // Date and time when the original image was generated + 0x9004 : "DateTimeDigitized", // Date and time when the image was stored digitally + 0x9290 : "SubsecTime", // Fractions of seconds for DateTime + 0x9291 : "SubsecTimeOriginal", // Fractions of seconds for DateTimeOriginal + 0x9292 : "SubsecTimeDigitized", // Fractions of seconds for DateTimeDigitized + + // picture-taking conditions + 0x829A : "ExposureTime", // Exposure time (in seconds) + 0x829D : "FNumber", // F number + 0x8822 : "ExposureProgram", // Exposure program + 0x8824 : "SpectralSensitivity", // Spectral sensitivity + 0x8827 : "ISOSpeedRatings", // ISO speed rating + 0x8828 : "OECF", // Optoelectric conversion factor + 0x9201 : "ShutterSpeedValue", // Shutter speed + 0x9202 : "ApertureValue", // Lens aperture + 0x9203 : "BrightnessValue", // Value of brightness + 0x9204 : "ExposureBias", // Exposure bias + 0x9205 : "MaxApertureValue", // Smallest F number of lens + 0x9206 : "SubjectDistance", // Distance to subject in meters + 0x9207 : "MeteringMode", // Metering mode + 0x9208 : "LightSource", // Kind of light source + 0x9209 : "Flash", // Flash status + 0x9214 : "SubjectArea", // Location and area of main subject + 0x920A : "FocalLength", // Focal length of the lens in mm + 0xA20B : "FlashEnergy", // Strobe energy in BCPS + 0xA20C : "SpatialFrequencyResponse", // + 0xA20E : "FocalPlaneXResolution", // Number of pixels in width direction per FocalPlaneResolutionUnit + 0xA20F : "FocalPlaneYResolution", // Number of pixels in height direction per FocalPlaneResolutionUnit + 0xA210 : "FocalPlaneResolutionUnit", // Unit for measuring FocalPlaneXResolution and FocalPlaneYResolution + 0xA214 : "SubjectLocation", // Location of subject in image + 0xA215 : "ExposureIndex", // Exposure index selected on camera + 0xA217 : "SensingMethod", // Image sensor type + 0xA300 : "FileSource", // Image source (3 == DSC) + 0xA301 : "SceneType", // Scene type (1 == directly photographed) + 0xA302 : "CFAPattern", // Color filter array geometric pattern + 0xA401 : "CustomRendered", // Special processing + 0xA402 : "ExposureMode", // Exposure mode + 0xA403 : "WhiteBalance", // 1 = auto white balance, 2 = manual + 0xA404 : "DigitalZoomRation", // Digital zoom ratio + 0xA405 : "FocalLengthIn35mmFilm", // Equivalent foacl length assuming 35mm film camera (in mm) + 0xA406 : "SceneCaptureType", // Type of scene + 0xA407 : "GainControl", // Degree of overall image gain adjustment + 0xA408 : "Contrast", // Direction of contrast processing applied by camera + 0xA409 : "Saturation", // Direction of saturation processing applied by camera + 0xA40A : "Sharpness", // Direction of sharpness processing applied by camera + 0xA40B : "DeviceSettingDescription", // + 0xA40C : "SubjectDistanceRange", // Distance to subject + + // other tags + 0xA005 : "InteroperabilityIFDPointer", + 0xA420 : "ImageUniqueID" // Identifier assigned uniquely to each image + }; + + var TiffTags = EXIF.TiffTags = { + 0x0100 : "ImageWidth", + 0x0101 : "ImageHeight", + 0x8769 : "ExifIFDPointer", + 0x8825 : "GPSInfoIFDPointer", + 0xA005 : "InteroperabilityIFDPointer", + 0x0102 : "BitsPerSample", + 0x0103 : "Compression", + 0x0106 : "PhotometricInterpretation", + 0x0112 : "Orientation", + 0x0115 : "SamplesPerPixel", + 0x011C : "PlanarConfiguration", + 0x0212 : "YCbCrSubSampling", + 0x0213 : "YCbCrPositioning", + 0x011A : "XResolution", + 0x011B : "YResolution", + 0x0128 : "ResolutionUnit", + 0x0111 : "StripOffsets", + 0x0116 : "RowsPerStrip", + 0x0117 : "StripByteCounts", + 0x0201 : "JPEGInterchangeFormat", + 0x0202 : "JPEGInterchangeFormatLength", + 0x012D : "TransferFunction", + 0x013E : "WhitePoint", + 0x013F : "PrimaryChromaticities", + 0x0211 : "YCbCrCoefficients", + 0x0214 : "ReferenceBlackWhite", + 0x0132 : "DateTime", + 0x010E : "ImageDescription", + 0x010F : "Make", + 0x0110 : "Model", + 0x0131 : "Software", + 0x013B : "Artist", + 0x8298 : "Copyright" + }; + + var GPSTags = EXIF.GPSTags = { + 0x0000 : "GPSVersionID", + 0x0001 : "GPSLatitudeRef", + 0x0002 : "GPSLatitude", + 0x0003 : "GPSLongitudeRef", + 0x0004 : "GPSLongitude", + 0x0005 : "GPSAltitudeRef", + 0x0006 : "GPSAltitude", + 0x0007 : "GPSTimeStamp", + 0x0008 : "GPSSatellites", + 0x0009 : "GPSStatus", + 0x000A : "GPSMeasureMode", + 0x000B : "GPSDOP", + 0x000C : "GPSSpeedRef", + 0x000D : "GPSSpeed", + 0x000E : "GPSTrackRef", + 0x000F : "GPSTrack", + 0x0010 : "GPSImgDirectionRef", + 0x0011 : "GPSImgDirection", + 0x0012 : "GPSMapDatum", + 0x0013 : "GPSDestLatitudeRef", + 0x0014 : "GPSDestLatitude", + 0x0015 : "GPSDestLongitudeRef", + 0x0016 : "GPSDestLongitude", + 0x0017 : "GPSDestBearingRef", + 0x0018 : "GPSDestBearing", + 0x0019 : "GPSDestDistanceRef", + 0x001A : "GPSDestDistance", + 0x001B : "GPSProcessingMethod", + 0x001C : "GPSAreaInformation", + 0x001D : "GPSDateStamp", + 0x001E : "GPSDifferential" + }; + + // EXIF 2.3 Spec + var IFD1Tags = EXIF.IFD1Tags = { + 0x0100: "ImageWidth", + 0x0101: "ImageHeight", + 0x0102: "BitsPerSample", + 0x0103: "Compression", + 0x0106: "PhotometricInterpretation", + 0x0111: "StripOffsets", + 0x0112: "Orientation", + 0x0115: "SamplesPerPixel", + 0x0116: "RowsPerStrip", + 0x0117: "StripByteCounts", + 0x011A: "XResolution", + 0x011B: "YResolution", + 0x011C: "PlanarConfiguration", + 0x0128: "ResolutionUnit", + 0x0201: "JpegIFOffset", // When image format is JPEG, this value show offset to JPEG data stored.(aka "ThumbnailOffset" or "JPEGInterchangeFormat") + 0x0202: "JpegIFByteCount", // When image format is JPEG, this value shows data size of JPEG image (aka "ThumbnailLength" or "JPEGInterchangeFormatLength") + 0x0211: "YCbCrCoefficients", + 0x0212: "YCbCrSubSampling", + 0x0213: "YCbCrPositioning", + 0x0214: "ReferenceBlackWhite" + }; + + var StringValues = EXIF.StringValues = { + ExposureProgram : { + 0 : "Not defined", + 1 : "Manual", + 2 : "Normal program", + 3 : "Aperture priority", + 4 : "Shutter priority", + 5 : "Creative program", + 6 : "Action program", + 7 : "Portrait mode", + 8 : "Landscape mode" + }, + MeteringMode : { + 0 : "Unknown", + 1 : "Average", + 2 : "CenterWeightedAverage", + 3 : "Spot", + 4 : "MultiSpot", + 5 : "Pattern", + 6 : "Partial", + 255 : "Other" + }, + LightSource : { + 0 : "Unknown", + 1 : "Daylight", + 2 : "Fluorescent", + 3 : "Tungsten (incandescent light)", + 4 : "Flash", + 9 : "Fine weather", + 10 : "Cloudy weather", + 11 : "Shade", + 12 : "Daylight fluorescent (D 5700 - 7100K)", + 13 : "Day white fluorescent (N 4600 - 5400K)", + 14 : "Cool white fluorescent (W 3900 - 4500K)", + 15 : "White fluorescent (WW 3200 - 3700K)", + 17 : "Standard light A", + 18 : "Standard light B", + 19 : "Standard light C", + 20 : "D55", + 21 : "D65", + 22 : "D75", + 23 : "D50", + 24 : "ISO studio tungsten", + 255 : "Other" + }, + Flash : { + 0x0000 : "Flash did not fire", + 0x0001 : "Flash fired", + 0x0005 : "Strobe return light not detected", + 0x0007 : "Strobe return light detected", + 0x0009 : "Flash fired, compulsory flash mode", + 0x000D : "Flash fired, compulsory flash mode, return light not detected", + 0x000F : "Flash fired, compulsory flash mode, return light detected", + 0x0010 : "Flash did not fire, compulsory flash mode", + 0x0018 : "Flash did not fire, auto mode", + 0x0019 : "Flash fired, auto mode", + 0x001D : "Flash fired, auto mode, return light not detected", + 0x001F : "Flash fired, auto mode, return light detected", + 0x0020 : "No flash function", + 0x0041 : "Flash fired, red-eye reduction mode", + 0x0045 : "Flash fired, red-eye reduction mode, return light not detected", + 0x0047 : "Flash fired, red-eye reduction mode, return light detected", + 0x0049 : "Flash fired, compulsory flash mode, red-eye reduction mode", + 0x004D : "Flash fired, compulsory flash mode, red-eye reduction mode, return light not detected", + 0x004F : "Flash fired, compulsory flash mode, red-eye reduction mode, return light detected", + 0x0059 : "Flash fired, auto mode, red-eye reduction mode", + 0x005D : "Flash fired, auto mode, return light not detected, red-eye reduction mode", + 0x005F : "Flash fired, auto mode, return light detected, red-eye reduction mode" + }, + SensingMethod : { + 1 : "Not defined", + 2 : "One-chip color area sensor", + 3 : "Two-chip color area sensor", + 4 : "Three-chip color area sensor", + 5 : "Color sequential area sensor", + 7 : "Trilinear sensor", + 8 : "Color sequential linear sensor" + }, + SceneCaptureType : { + 0 : "Standard", + 1 : "Landscape", + 2 : "Portrait", + 3 : "Night scene" + }, + SceneType : { + 1 : "Directly photographed" + }, + CustomRendered : { + 0 : "Normal process", + 1 : "Custom process" + }, + WhiteBalance : { + 0 : "Auto white balance", + 1 : "Manual white balance" + }, + GainControl : { + 0 : "None", + 1 : "Low gain up", + 2 : "High gain up", + 3 : "Low gain down", + 4 : "High gain down" + }, + Contrast : { + 0 : "Normal", + 1 : "Soft", + 2 : "Hard" + }, + Saturation : { + 0 : "Normal", + 1 : "Low saturation", + 2 : "High saturation" + }, + Sharpness : { + 0 : "Normal", + 1 : "Soft", + 2 : "Hard" + }, + SubjectDistanceRange : { + 0 : "Unknown", + 1 : "Macro", + 2 : "Close view", + 3 : "Distant view" + }, + FileSource : { + 3 : "DSC" + }, + + Components : { + 0 : "", + 1 : "Y", + 2 : "Cb", + 3 : "Cr", + 4 : "R", + 5 : "G", + 6 : "B" + } + }; + + function addEvent(element, event, handler) { + if (element.addEventListener) { + element.addEventListener(event, handler, false); + } else if (element.attachEvent) { + element.attachEvent("on" + event, handler); + } + } + + function imageHasData(img) { + return !!(img.exifdata); + } + + + function base64ToArrayBuffer(base64, contentType) { + contentType = contentType || base64.match(/^data\:([^\;]+)\;base64,/mi)[1] || ''; // e.g. 'data:image/jpeg;base64,...' => 'image/jpeg' + base64 = base64.replace(/^data\:([^\;]+)\;base64,/gmi, ''); + var binary = atob(base64); + var len = binary.length; + var buffer = new ArrayBuffer(len); + var view = new Uint8Array(buffer); + for (var i = 0; i < len; i++) { + view[i] = binary.charCodeAt(i); + } + return buffer; + } + + function objectURLToBlob(url, callback) { + var http = new XMLHttpRequest(); + http.open("GET", url, true); + http.responseType = "blob"; + http.onload = function(e) { + if (this.status == 200 || this.status === 0) { + callback(this.response); + } + }; + http.send(); + } + + function getImageData(img, callback) { + function handleBinaryFile(binFile) { + var data = findEXIFinJPEG(binFile); + img.exifdata = data || {}; + var iptcdata = findIPTCinJPEG(binFile); + img.iptcdata = iptcdata || {}; + if (EXIF.isXmpEnabled) { + var xmpdata= findXMPinJPEG(binFile); + img.xmpdata = xmpdata || {}; + } + if (callback) { + callback.call(img); + } + } + + if (img.src) { + if (/^data\:/i.test(img.src)) { // Data URI + var arrayBuffer = base64ToArrayBuffer(img.src); + handleBinaryFile(arrayBuffer); + + } else if (/^blob\:/i.test(img.src)) { // Object URL + var fileReader = new FileReader(); + fileReader.onload = function(e) { + handleBinaryFile(e.target.result); + }; + objectURLToBlob(img.src, function (blob) { + fileReader.readAsArrayBuffer(blob); + }); + } else { + var http = new XMLHttpRequest(); + http.onload = function() { + if (this.status == 200 || this.status === 0) { + handleBinaryFile(http.response); + } else { + throw "Could not load image"; + } + http = null; + }; + http.open("GET", img.src, true); + http.responseType = "arraybuffer"; + http.send(null); + } + } else if (self.FileReader && (img instanceof self.Blob || img instanceof self.File)) { + var fileReader = new FileReader(); + fileReader.onload = function(e) { + if (debug) console.log("Got file of length " + e.target.result.byteLength); + handleBinaryFile(e.target.result); + }; + + fileReader.readAsArrayBuffer(img); + } + } + + function findEXIFinJPEG(file) { + var dataView = new DataView(file); + + if (debug) console.log("Got file of length " + file.byteLength); + if ((dataView.getUint8(0) != 0xFF) || (dataView.getUint8(1) != 0xD8)) { + if (debug) console.log("Not a valid JPEG"); + return false; // not a valid jpeg + } + + var offset = 2, + length = file.byteLength, + marker; + + while (offset < length) { + if (dataView.getUint8(offset) != 0xFF) { + if (debug) console.log("Not a valid marker at offset " + offset + ", found: " + dataView.getUint8(offset)); + return false; // not a valid marker, something is wrong + } + + marker = dataView.getUint8(offset + 1); + if (debug) console.log(marker); + + // we could implement handling for other markers here, + // but we're only looking for 0xFFE1 for EXIF data + + if (marker == 225) { + if (debug) console.log("Found 0xFFE1 marker"); + + return readEXIFData(dataView, offset + 4, dataView.getUint16(offset + 2) - 2); + + // offset += 2 + file.getShortAt(offset+2, true); + + } else { + offset += 2 + dataView.getUint16(offset+2); + } + + } + + } + + function findIPTCinJPEG(file) { + var dataView = new DataView(file); + + if (debug) console.log("Got file of length " + file.byteLength); + if ((dataView.getUint8(0) != 0xFF) || (dataView.getUint8(1) != 0xD8)) { + if (debug) console.log("Not a valid JPEG"); + return false; // not a valid jpeg + } + + var offset = 2, + length = file.byteLength; + + + var isFieldSegmentStart = function(dataView, offset){ + return ( + dataView.getUint8(offset) === 0x38 && + dataView.getUint8(offset+1) === 0x42 && + dataView.getUint8(offset+2) === 0x49 && + dataView.getUint8(offset+3) === 0x4D && + dataView.getUint8(offset+4) === 0x04 && + dataView.getUint8(offset+5) === 0x04 + ); + }; + + while (offset < length) { + + if ( isFieldSegmentStart(dataView, offset )){ + + // Get the length of the name header (which is padded to an even number of bytes) + 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; + var sectionLength = dataView.getUint16(offset + 6 + nameHeaderLength); + + return readIPTCData(file, startOffset, sectionLength); + + break; + + } + + + // Not the marker, continue searching + offset++; + + } + + } + var IptcFieldMap = { + 0x78 : 'caption', + 0x6E : 'credit', + 0x19 : 'keywords', + 0x37 : 'dateCreated', + 0x50 : 'byline', + 0x55 : 'bylineTitle', + 0x7A : 'captionWriter', + 0x69 : 'headline', + 0x74 : 'copyright', + 0x0F : 'category' + }; + function readIPTCData(file, startOffset, sectionLength){ + var dataView = new DataView(file); + var data = {}; + var fieldValue, fieldName, dataSize, segmentType, segmentSize; + var segmentStartPos = startOffset; + while(segmentStartPos < startOffset+sectionLength) { + if(dataView.getUint8(segmentStartPos) === 0x1C && dataView.getUint8(segmentStartPos+1) === 0x02){ + segmentType = dataView.getUint8(segmentStartPos+2); + if(segmentType in IptcFieldMap) { + dataSize = dataView.getInt16(segmentStartPos+3); + segmentSize = dataSize + 5; + fieldName = IptcFieldMap[segmentType]; + fieldValue = getStringFromDB(dataView, segmentStartPos+5, dataSize); + // Check if we already stored a value with this name + if(data.hasOwnProperty(fieldName)) { + // Value already stored with this name, create multivalue field + if(data[fieldName] instanceof Array) { + data[fieldName].push(fieldValue); + } + else { + data[fieldName] = [data[fieldName], fieldValue]; + } + } + else { + data[fieldName] = fieldValue; + } + } + + } + segmentStartPos++; + } + return data; + } + + + + function readTags(file, tiffStart, dirStart, strings, bigEnd) { + var entries = file.getUint16(dirStart, !bigEnd), + tags = {}, + entryOffset, tag, + i; + + for (i=0;i 4 ? valueOffset : (entryOffset + 8); + vals = []; + for (n=0;n 4 ? valueOffset : (entryOffset + 8); + return getStringFromDB(file, offset, numValues-1); + + case 3: // short, 16 bit int + if (numValues == 1) { + return file.getUint16(entryOffset + 8, !bigEnd); + } else { + offset = numValues > 2 ? valueOffset : (entryOffset + 8); + vals = []; + for (n=0;n dataView.byteLength) { // this should not happen + // console.log('******** IFD1Offset is outside the bounds of the DataView ********'); + return {}; + } + // console.log('******* thumbnail IFD offset (IFD1) is: %s', IFD1OffsetPointer); + + var thumbTags = readTags(dataView, tiffStart, tiffStart + IFD1OffsetPointer, IFD1Tags, bigEnd) + + // EXIF 2.3 specification for JPEG format thumbnail + + // If the value of Compression(0x0103) Tag in IFD1 is '6', thumbnail image format is JPEG. + // Most of Exif image uses JPEG format for thumbnail. In that case, you can get offset of thumbnail + // by JpegIFOffset(0x0201) Tag in IFD1, size of thumbnail by JpegIFByteCount(0x0202) Tag. + // Data format is ordinary JPEG format, starts from 0xFFD8 and ends by 0xFFD9. It seems that + // JPEG format and 160x120pixels of size are recommended thumbnail format for Exif2.1 or later. + + if (thumbTags['Compression']) { + // console.log('Thumbnail image found!'); + + switch (thumbTags['Compression']) { + case 6: + // console.log('Thumbnail image format is JPEG'); + if (thumbTags.JpegIFOffset && thumbTags.JpegIFByteCount) { + // extract the thumbnail + var tOffset = tiffStart + thumbTags.JpegIFOffset; + var tLength = thumbTags.JpegIFByteCount; + thumbTags['blob'] = new Blob([new Uint8Array(dataView.buffer, tOffset, tLength)], { + type: 'image/jpeg' + }); + } + break; + + case 1: + console.log("Thumbnail image format is TIFF, which is not implemented."); + break; + default: + console.log("Unknown thumbnail image format '%s'", thumbTags['Compression']); + } + } + else if (thumbTags['PhotometricInterpretation'] == 2) { + console.log("Thumbnail image format is RGB, which is not implemented."); + } + return thumbTags; + } + + function getStringFromDB(buffer, start, length) { + var outstr = ""; + for (n = start; n < start+length; n++) { + outstr += String.fromCharCode(buffer.getUint8(n)); + } + return outstr; + } + + function readEXIFData(file, start) { + if (getStringFromDB(file, start, 4) != "Exif") { + if (debug) console.log("Not valid EXIF data! " + getStringFromDB(file, start, 4)); + return false; + } + + var bigEnd, + tags, tag, + exifData, gpsData, + tiffOffset = start + 6; + + // test for TIFF validity and endianness + if (file.getUint16(tiffOffset) == 0x4949) { + bigEnd = false; + } else if (file.getUint16(tiffOffset) == 0x4D4D) { + bigEnd = true; + } else { + if (debug) console.log("Not valid TIFF data! (no 0x4949 or 0x4D4D)"); + return false; + } + + if (file.getUint16(tiffOffset+2, !bigEnd) != 0x002A) { + if (debug) console.log("Not valid TIFF data! (no 0x002A)"); + return false; + } + + var firstIFDOffset = file.getUint32(tiffOffset+4, !bigEnd); + + if (firstIFDOffset < 0x00000008) { + if (debug) console.log("Not valid TIFF data! (First offset less than 8)", file.getUint32(tiffOffset+4, !bigEnd)); + return false; + } + + tags = readTags(file, tiffOffset, tiffOffset + firstIFDOffset, TiffTags, bigEnd); + + if (tags.ExifIFDPointer) { + exifData = readTags(file, tiffOffset, tiffOffset + tags.ExifIFDPointer, ExifTags, bigEnd); + for (tag in exifData) { + switch (tag) { + case "LightSource" : + case "Flash" : + case "MeteringMode" : + case "ExposureProgram" : + case "SensingMethod" : + case "SceneCaptureType" : + case "SceneType" : + case "CustomRendered" : + case "WhiteBalance" : + case "GainControl" : + case "Contrast" : + case "Saturation" : + case "Sharpness" : + case "SubjectDistanceRange" : + case "FileSource" : + exifData[tag] = StringValues[tag][exifData[tag]]; + break; + + case "ExifVersion" : + case "FlashpixVersion" : + exifData[tag] = String.fromCharCode(exifData[tag][0], exifData[tag][1], exifData[tag][2], exifData[tag][3]); + break; + + case "ComponentsConfiguration" : + exifData[tag] = + StringValues.Components[exifData[tag][0]] + + StringValues.Components[exifData[tag][1]] + + StringValues.Components[exifData[tag][2]] + + StringValues.Components[exifData[tag][3]]; + break; + } + tags[tag] = exifData[tag]; + } + } + + if (tags.GPSInfoIFDPointer) { + gpsData = readTags(file, tiffOffset, tiffOffset + tags.GPSInfoIFDPointer, GPSTags, bigEnd); + for (tag in gpsData) { + switch (tag) { + case "GPSVersionID" : + gpsData[tag] = gpsData[tag][0] + + "." + gpsData[tag][1] + + "." + gpsData[tag][2] + + "." + gpsData[tag][3]; + break; + } + tags[tag] = gpsData[tag]; + } + } + + // extract thumbnail + tags['thumbnail'] = readThumbnailImage(file, tiffOffset, firstIFDOffset, bigEnd); + + return tags; + } + + function findXMPinJPEG(file) { + + if (!('DOMParser' in self)) { + // console.warn('XML parsing not supported without DOMParser'); + return; + } + var dataView = new DataView(file); + + if (debug) console.log("Got file of length " + file.byteLength); + if ((dataView.getUint8(0) != 0xFF) || (dataView.getUint8(1) != 0xD8)) { + if (debug) console.log("Not a valid JPEG"); + return false; // not a valid jpeg + } + + var offset = 2, + length = file.byteLength, + dom = new DOMParser(); + + while (offset < (length-4)) { + if (getStringFromDB(dataView, offset, 4) == "http") { + var startOffset = offset - 1; + var sectionLength = dataView.getUint16(offset - 2) - 1; + var xmpString = getStringFromDB(dataView, startOffset, sectionLength) + var xmpEndIndex = xmpString.indexOf('xmpmeta>') + 8; + xmpString = xmpString.substring( xmpString.indexOf( ' 0) { + json['@attributes'] = {}; + for (var j = 0; j < xml.attributes.length; j++) { + var attribute = xml.attributes.item(j); + json['@attributes'][attribute.nodeName] = attribute.nodeValue; + } + } + } else if (xml.nodeType == 3) { // text node + return xml.nodeValue; + } + + // deal with children + if (xml.hasChildNodes()) { + for(var i = 0; i < xml.childNodes.length; i++) { + var child = xml.childNodes.item(i); + var nodeName = child.nodeName; + if (json[nodeName] == null) { + json[nodeName] = xml2json(child); + } else { + if (json[nodeName].push == null) { + var old = json[nodeName]; + json[nodeName] = []; + json[nodeName].push(old); + } + json[nodeName].push(xml2json(child)); + } + } + } + + return json; + } + + function xml2Object(xml) { + try { + var obj = {}; + if (xml.children.length > 0) { + for (var i = 0; i < xml.children.length; i++) { + var item = xml.children.item(i); + var attributes = item.attributes; + for(var idx in attributes) { + var itemAtt = attributes[idx]; + var dataKey = itemAtt.nodeName; + var dataValue = itemAtt.nodeValue; + + if(dataKey !== undefined) { + obj[dataKey] = dataValue; + } + } + var nodeName = item.nodeName; + + if (typeof (obj[nodeName]) == "undefined") { + obj[nodeName] = xml2json(item); + } else { + if (typeof (obj[nodeName].push) == "undefined") { + var old = obj[nodeName]; + + obj[nodeName] = []; + obj[nodeName].push(old); + } + obj[nodeName].push(xml2json(item)); + } + } + } else { + obj = xml.textContent; + } + return obj; + } catch (e) { + console.log(e.message); + } + } + + EXIF.enableXmp = function() { + EXIF.isXmpEnabled = true; + } + + EXIF.disableXmp = function() { + EXIF.isXmpEnabled = false; + } + + EXIF.getData = function(img, callback) { + if (((self.Image && img instanceof self.Image) + || (self.HTMLImageElement && img instanceof self.HTMLImageElement)) + && !img.complete) + return false; + + if (!imageHasData(img)) { + getImageData(img, callback); + } else { + if (callback) { + callback.call(img); + } + } + return true; + } + + EXIF.getTag = function(img, tag) { + if (!imageHasData(img)) return; + return img.exifdata[tag]; + } + + EXIF.getIptcTag = function(img, tag) { + if (!imageHasData(img)) return; + return img.iptcdata[tag]; + } + + EXIF.getAllTags = function(img) { + if (!imageHasData(img)) return {}; + var a, + data = img.exifdata, + tags = {}; + for (a in data) { + if (data.hasOwnProperty(a)) { + tags[a] = data[a]; + } + } + return tags; + } + + EXIF.getAllIptcTags = function(img) { + if (!imageHasData(img)) return {}; + var a, + data = img.iptcdata, + tags = {}; + for (a in data) { + if (data.hasOwnProperty(a)) { + tags[a] = data[a]; + } + } + return tags; + } + + EXIF.pretty = function(img) { + if (!imageHasData(img)) return ""; + var a, + data = img.exifdata, + strPretty = ""; + for (a in data) { + if (data.hasOwnProperty(a)) { + if (typeof data[a] == "object") { + if (data[a] instanceof Number) { + strPretty += a + " : " + data[a] + " [" + data[a].numerator + "/" + data[a].denominator + "]\r\n"; + } else { + strPretty += a + " : [" + data[a].length + " values]\r\n"; + } + } else { + strPretty += a + " : " + data[a] + "\r\n"; + } + } + } + return strPretty; + } + + EXIF.readFromBinaryFile = function(file) { + return findEXIFinJPEG(file); + } + + if (typeof define === 'function' && define.amd) { + define('exif-js', [], function() { + return EXIF; + }); + } +}.call(this)); + diff --git a/wcs/qommon/static/js/qommon.fileupload.js b/wcs/qommon/static/js/qommon.fileupload.js index f978bdf93..f4c3cb8d9 100644 --- a/wcs/qommon/static/js/qommon.fileupload.js +++ b/wcs/qommon/static/js/qommon.fileupload.js @@ -255,49 +255,109 @@ $.WcsFileUpload = { var original_image_64 = e.target.result; var img = document.createElement("img"); img.onload = function() { - var canvas = document.createElement("canvas"); - var ctx = canvas.getContext('2d'); - ctx.drawImage(img, 0, 0); - - var MAX_WIDTH = 2000; - var MAX_HEIGHT = 2000; - var width = img.width; - var height = img.height; - - if (width > height && width > MAX_WIDTH) { - height *= MAX_WIDTH / width; - width = MAX_WIDTH; - } else if (height > MAX_HEIGHT) { - width *= MAX_HEIGHT / height; - height = MAX_HEIGHT; - } - if (img.width != width || img.height != height) { - canvas.width = width; - canvas.height = height; + var adapt_image = function(orientation) { + /* + * 1 = Horizontal (normal) + * 2 = Mirror horizontal + * 3 = Rotate 180 + * 4 = Mirror vertical + * 5 = Mirror horizontal and rotate 270 CW + * 6 = Rotate 90 CW + * 7 = Mirror horizontal and rotate 90 CW + * 8 = Rotate 270 CW */ + var canvas = document.createElement("canvas"); var ctx = canvas.getContext('2d'); - ctx.drawImage(img, 0, 0, width, height); - var new_image_64 = canvas.toDataURL('image/jpeg', 0.95); - var blob = null; - if (data.files[0].type == 'image/jpeg') { - blob = ExifRestorer.restore_as_blob(original_image_64, new_image_64); - blob.name = data.files[0].name; - } else { - // adapted from dataURItoBlob, from - // https://stackoverflow.com/questions/12168909/blob-from-dataurl#12300351 - var byteString = atob(new_image_64.split(',')[1]); - var mimeString = new_image_64.split(',')[0].split(':')[1].split(';')[0] - var ab = new ArrayBuffer(byteString.length); - var ia = new Uint8Array(ab); - for (var i = 0; i < byteString.length; i++) { - ia[i] = byteString.charCodeAt(i); - } - blob = new Blob([ab], {type: mimeString}); - blob.name = data.files[0].name + '.jpg'; + ctx.drawImage(img, 0, 0); + + var MAX_WIDTH = 2000; + var MAX_HEIGHT = 2000; + var width = img.width; + var height = img.height; + + if (width > height && width > MAX_WIDTH) { + height *= MAX_WIDTH / width; + width = MAX_WIDTH; + } else if (height > MAX_HEIGHT) { + width *= MAX_HEIGHT / height; + height = MAX_HEIGHT; } - data.files[0] = blob; + if (img.width != width || img.height != height) { + canvas.width = width; + canvas.height = height; + var ctx = canvas.getContext('2d'); + switch (orientation) { + case 1: + break; + case 2: + ctx.translate(width, 0); + ctx.scale(-1, 1); + break; + case 3: + ctx.translate(width, height); + ctx.rotate(180 / 180 * Math.PI); + break; + case 4: + ctx.translate(0, height); + ctx.scale(1, -1); + break; + case 5: + canvas.width = height; + canvas.height = width; + ctx.rotate(90 / 180 * Math.PI); + ctx.scale(1, -1); + break; + case 6: + canvas.width = height; + canvas.height = width; + ctx.rotate(-90 / 180 * Math.PI); + ctx.translate(-width, 0); + break; + case 7: + canvas.width = height; + canvas.height = width; + ctx.rotate(270 / 180 * Math.PI); + ctx.translate(-width, height); + ctx.scale(1, -1); + break; + case 8: + canvas.width = height; + canvas.height = width; + ctx.translate(height, 0); + ctx.rotate(-270 / 180 * Math.PI); + break; + } + ctx.drawImage(img, 0, 0, width, height); + var new_image_64 = canvas.toDataURL('image/jpeg', 0.95); + var blob = null; + if (data.files[0].type == 'image/jpeg') { + blob = ExifRestorer.restore_as_blob(original_image_64, new_image_64); + blob.name = data.files[0].name; + } else { + // adapted from dataURItoBlob, from + // https://stackoverflow.com/questions/12168909/blob-from-dataurl#12300351 + var byteString = atob(new_image_64.split(',')[1]); + var mimeString = new_image_64.split(',')[0].split(':')[1].split(';')[0] + var ab = new ArrayBuffer(byteString.length); + var ia = new Uint8Array(ab); + for (var i = 0; i < byteString.length; i++) { + ia[i] = byteString.charCodeAt(i); + } + blob = new Blob([ab], {type: mimeString}); + blob.name = data.files[0].name + '.jpg'; + } + data.files[0] = blob; + } + $(base_widget).find('.file-button').trigger('wcs:image-blob', data.files[0]); + return $.WcsFileUpload.upload(base_widget, data); + }; + if (data.files[0].type == 'image/jpeg') { + EXIF.getData(img, function () { + var orientation = +EXIF.getTag(this, "Orientation"); + adapt_image(orientation); + }); + } else { + adapt_image(0); } - $(base_widget).find('.file-button').trigger('wcs:image-blob', data.files[0]); - return $.WcsFileUpload.upload(base_widget, data); } img.src = e.target.result; }