ngx-image-cropper-facet
Version:
An image cropper for Angular
2,067 lines • 81.1 kB
JavaScript
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, EventEmitter, HostListener, Injectable, Input, NgModule, Output } from '@angular/core';
import { Observable as Observable$1 } from 'rxjs/Observable';
import { CommonModule } from '@angular/common';
import { DomSanitizer } from '@angular/platform-browser';
class ExifService {
constructor() {
/**
* Private variables
*/
this._debug = false;
this._isXmpEnabled = false;
this._fileType = 'image/jpg';
this._errorMessage = {
imageFailedToLoad: 'Could not load image',
invalidImageFormat: 'Invalid image format provided'
};
/**
* Public variables
*/
this.exifTags = {
// version tags
0x9000: 'ExifVersion',
0xA000: 'FlashpixVersion',
// colorspace tags
0xA001: 'ColorSpace',
// image configuration
0xA002: 'PixelXDimension',
0xA003: 'PixelYDimension',
0x9101: 'ComponentsConfiguration',
0x9102: 'CompressedBitsPerPixel',
// user information
0x927C: 'MakerNote',
0x9286: 'UserComment',
// related file
0xA004: 'RelatedSoundFile',
// date and time
0x9003: 'DateTimeOriginal',
0x9004: 'DateTimeDigitized',
0x9290: 'SubsecTime',
0x9291: 'SubsecTimeOriginal',
0x9292: 'SubsecTimeDigitized',
// picture-taking conditions
0x829A: 'ExposureTime',
0x829D: 'FNumber',
0x8822: 'ExposureProgram',
0x8824: 'SpectralSensitivity',
0x8827: 'ISOSpeedRatings',
0x8828: 'OECF',
0x9201: 'ShutterSpeedValue',
0x9202: 'ApertureValue',
0x9203: 'BrightnessValue',
0x9204: 'ExposureBias',
0x9205: 'MaxApertureValue',
0x9206: 'SubjectDistance',
0x9207: 'MeteringMode',
0x9208: 'LightSource',
0x9209: 'Flash',
0x9214: 'SubjectArea',
0x920A: 'FocalLength',
0xA20B: 'FlashEnergy',
0xA20C: 'SpatialFrequencyResponse',
0xA20E: 'FocalPlaneXResolution',
0xA20F: 'FocalPlaneYResolution',
0xA210: 'FocalPlaneResolutionUnit',
0xA214: 'SubjectLocation',
0xA215: 'ExposureIndex',
0xA217: 'SensingMethod',
0xA300: 'FileSource',
0xA301: 'SceneType',
0xA302: 'CFAPattern',
0xA401: 'CustomRendered',
0xA402: 'ExposureMode',
0xA403: 'WhiteBalance',
0xA404: 'DigitalZoomRation',
0xA405: 'FocalLengthIn35mmFilm',
0xA406: 'SceneCaptureType',
0xA407: 'GainControl',
0xA408: 'Contrast',
0xA409: 'Saturation',
0xA40A: 'Sharpness',
0xA40B: 'DeviceSettingDescription',
0xA40C: 'SubjectDistanceRange',
// other tags
0xA005: 'InteroperabilityIFDPointer',
0xA420: 'ImageUniqueID' // Identifier assigned uniquely to each image
};
this.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'
};
this.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',
0x001F: 'GPSHPositioningError'
};
/**
* EXIF 2.3 Spec
*/
this.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',
0x0202: 'JpegIFByteCount',
0x0211: 'YCbCrCoefficients',
0x0212: 'YCbCrSubSampling',
0x0213: 'YCbCrPositioning',
0x0214: 'ReferenceBlackWhite'
};
this.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'
}
};
this.iptcFieldMap = {
0x78: 'caption',
0x6E: 'credit',
0x16: 'fixtureID',
0x19: 'keywords',
0x37: 'dateCreated',
0x50: 'byline',
0x55: 'bylineTitle',
0x7A: 'captionWriter',
0x69: 'headline',
0x74: 'copyright',
0x0F: 'category'
};
}
/**
* @return {?}
*/
enableXmp() {
this._isXmpEnabled = true;
}
/**
* @return {?}
*/
disableXmp() {
this._isXmpEnabled = false;
}
/**
* @param {?} imageFormat
* @return {?}
*/
fileType(imageFormat) {
this._fileType = imageFormat;
}
/**
* @param {?} image
* @return {?}
*/
getData(image) {
return Observable$1.create((observer) => {
this.getImageData(image).subscribe((image) => {
observer.next(image);
}, (error) => {
observer.error(error);
});
});
}
/**
* @param {?} imageData
* @param {?} tag
* @return {?}
*/
getTag(imageData, tag) {
if (!this.imageHasData(imageData)) {
return;
}
return imageData.exif[tag];
}
/**
* @param {?} imageData
* @param {?} tag
* @return {?}
*/
getIptcTag(imageData, tag) {
if (!this.imageHasData(imageData)) {
return;
}
return imageData.iptc[tag];
}
/**
* @param {?} imageData
* @return {?}
*/
getAllTags(imageData) {
if (!this.imageHasData(imageData)) {
return {};
}
let /** @type {?} */ a;
let /** @type {?} */ data = imageData.exif;
let /** @type {?} */ tags = {};
for (a in data) {
if (data.hasOwnProperty(a)) {
tags[a] = data[a];
}
}
return tags;
}
/**
* @param {?} imageData
* @return {?}
*/
getAllIptcTags(imageData) {
if (!this.imageHasData(imageData)) {
return {};
}
let /** @type {?} */ a;
let /** @type {?} */ data = imageData.iptc;
let /** @type {?} */ tags = {};
for (a in data) {
if (data.hasOwnProperty(a)) {
tags[a] = data[a];
}
}
return tags;
}
/**
* @param {?} imageData
* @return {?}
*/
pretty(imageData) {
if (!this.imageHasData(imageData)) {
return '';
}
let /** @type {?} */ a;
let /** @type {?} */ data = imageData.exif;
let /** @type {?} */ prettyString = '';
for (a in data) {
if (data.hasOwnProperty(a)) {
if (typeof data[a] === 'object') {
if (data[a] instanceof Number) {
prettyString += `${a} : ${data[a]} [${data[a].numerator}/${data[a].denominator}]\r\n`;
}
else {
prettyString += `${a} : [${data[a].length} values]\r\n`;
}
}
else {
prettyString += `${a} : ${data[a]}\r\n`;
}
}
}
return prettyString;
}
/**
* @param {?} image
* @return {?}
*/
getImageData(image) {
return Observable$1.create((observer) => {
// If the image value provided is a Blob or File then use the file reader
// to convert the image to an ArrayBuffer so we can extract the Exif, IPTC, XMP data
if (FileReader && (image instanceof Blob || image instanceof File)) {
let /** @type {?} */ reader = new FileReader();
reader.onload = (event) => {
const /** @type {?} */ binaryFile = event.target.result;
let /** @type {?} */ imageData = this.findAndSetImageTagData(binaryFile);
imageData.onload = () => {
observer.next(imageData);
};
imageData = this.addImageSourceFromArrayBuffer(imageData, binaryFile);
};
if (image instanceof Blob) {
reader.readAsArrayBuffer(image);
}
else {
reader.readAsDataURL(image);
}
}
else if (image instanceof ArrayBuffer) {
let /** @type {?} */ imageData = this.findAndSetImageTagData(image);
imageData.onload = () => {
observer.next(imageData);
};
imageData = this.addImageSourceFromArrayBuffer(imageData, image);
}
else if (image instanceof HTMLImageElement) {
const /** @type {?} */ imageSource = image.src;
// it take a long time to test the entire image string with
// the initial regex and the "data" portion we are testing for
// is always at the beginning so let's trim the string and only
// test what needs to be tested.
const /** @type {?} */ fileToTest = imageSource.slice(0, 25);
if (/^data\:/i.test(fileToTest)) {
let /** @type {?} */ arrayBuffer = this.base64ToArrayBuffer(imageSource);
let /** @type {?} */ imageData = this.findAndSetImageTagData(arrayBuffer);
imageData.onload = () => {
observer.next(imageData);
};
imageData = this.addImageSource(imageData, imageSource);
}
else if (/^blob\:/i.test(fileToTest)) {
let /** @type {?} */ fileReader = new FileReader();
fileReader.onload = (event) => {
const /** @type {?} */ binaryFile = event.target.result;
let /** @type {?} */ imageData = this.findAndSetImageTagData(binaryFile);
imageData.onload = () => {
observer.next(imageData);
};
imageData = this.addImageSourceFromArrayBuffer(imageData, binaryFile);
};
this.objectURLToBlob(imageSource, (blob) => {
fileReader.readAsArrayBuffer(blob);
});
}
else {
let /** @type {?} */ http = new XMLHttpRequest();
http.onload = () => {
if (http.status == 200 || http.status === 0) {
const /** @type {?} */ binaryFile = http.response;
let /** @type {?} */ imageData = this.findAndSetImageTagData(binaryFile);
imageData.onload = () => {
observer.next(imageData);
};
imageData = this.addImageSourceFromArrayBuffer(imageData, binaryFile);
}
else {
observer.error(this._errorMessage.imageFailedToLoad);
}
};
http.open('GET', imageSource, true);
http.responseType = 'arraybuffer';
http.send(null);
}
}
else {
const /** @type {?} */ imageSource = ((image));
const /** @type {?} */ stringToTest = imageSource.slice(0, 25);
if (/^data\:/i.test(stringToTest)) {
let /** @type {?} */ arrayBuffer = this.base64ToArrayBuffer(imageSource);
let /** @type {?} */ imageData = this.findAndSetImageTagData(arrayBuffer);
imageData.onload = () => {
observer.next(imageData);
};
imageData = this.addImageSource(imageData, imageSource);
}
else {
observer.error(this._errorMessage.invalidImageFormat);
}
}
});
}
/**
* @param {?} binaryFile
* @return {?}
*/
findAndSetImageTagData(binaryFile) {
const /** @type {?} */ exifData = this.findEXIFinJPEG(binaryFile);
const /** @type {?} */ iptcData = this.findIPTCinJPEG(binaryFile);
let /** @type {?} */ image = new Image();
let /** @type {?} */ data = ((image));
data.exif = exifData || {};
data.iptc = iptcData || {};
if (this._isXmpEnabled) {
data.xmp = this.findXMPinJPEG(binaryFile);
}
return data;
}
/**
* @param {?} data
* @param {?} binaryFile
* @return {?}
*/
addImageSourceFromArrayBuffer(data, binaryFile) {
const /** @type {?} */ base64Image = this.arrayBufferToBase64(binaryFile);
const /** @type {?} */ properBase64Image = `data:${this._fileType};base64,${base64Image}`;
return this.addImageSource(data, properBase64Image);
}
/**
* @param {?} data
* @param {?} imageString
* @return {?}
*/
addImageSource(data, imageString) {
data.src = imageString;
return data;
}
/**
* Convert the ArrayBuffer to a base 64 string
*
* @param {?} buffer ArrayBuffer returned from the file reader
*
* @return {?} A base64 string of the provided image
*/
arrayBufferToBase64(buffer) {
let /** @type {?} */ binary = '';
const /** @type {?} */ bytes = new Uint8Array(buffer);
const /** @type {?} */ length = bytes.byteLength;
for (let /** @type {?} */ i = 0; i < length; i++) {
binary += String.fromCharCode(bytes[i]);
}
return window.btoa(binary);
}
/**
* @param {?} base64
* @return {?}
*/
base64ToArrayBuffer(base64) {
base64 = base64.replace(/^data\:([^\;]+)\;base64,/gmi, '');
let /** @type {?} */ binary = atob(base64);
let /** @type {?} */ len = binary.length;
let /** @type {?} */ buffer = new ArrayBuffer(len);
let /** @type {?} */ view = new Uint8Array(buffer);
for (let /** @type {?} */ i = 0; i < len; i++) {
view[i] = binary.charCodeAt(i);
}
return buffer;
}
/**
* @param {?} url
* @param {?} callback
* @return {?}
*/
objectURLToBlob(url, callback) {
let /** @type {?} */ http = new XMLHttpRequest();
http.open('GET', url, true);
http.responseType = 'blob';
http.onload = () => {
if (http.status == 200 || http.status === 0) {
callback(http.response);
}
};
http.send();
}
/**
* @param {?} file
* @return {?}
*/
findEXIFinJPEG(file) {
let /** @type {?} */ dataView = new DataView(file);
this.log(`Got file of length ${file.byteLength}`);
if ((dataView.getUint8(0) != 0xFF) || (dataView.getUint8(1) != 0xD8)) {
this.log('Not a valid JPEG');
return false; // not a valid jpeg
}
let /** @type {?} */ offset = 2;
let /** @type {?} */ length = file.byteLength;
let /** @type {?} */ marker;
while (offset < length) {
if (dataView.getUint8(offset) != 0xFF) {
this.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);
this.log(marker);
// we could implement handling for other markers here,
// but we're only looking for 0xFFE1 for EXIF data
if (marker == 225) {
this.log('Found 0xFFE1 marker');
return this.readEXIFData(dataView, offset + 4);
}
else {
offset += 2 + dataView.getUint16(offset + 2);
}
}
}
/**
* @param {?} file
* @return {?}
*/
findIPTCinJPEG(file) {
let /** @type {?} */ dataView = new DataView(file);
this.log(`Got file of length ${file.byteLength}`);
if ((dataView.getUint8(0) != 0xFF) || (dataView.getUint8(1) != 0xD8)) {
this.log('Not a valid JPEG');
return false; // not a valid jpeg
}
let /** @type {?} */ offset = 2;
let /** @type {?} */ length = file.byteLength;
let /** @type {?} */ isFieldSegmentStart = (view, _offset) => {
return (view.getUint8(_offset) === 0x38 &&
view.getUint8(_offset + 1) === 0x42 &&
view.getUint8(_offset + 2) === 0x49 &&
view.getUint8(_offset + 3) === 0x4D &&
view.getUint8(_offset + 4) === 0x04 &&
view.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)
let /** @type {?} */ nameHeaderLength = dataView.getUint8(offset + 7);
if (nameHeaderLength % 2 !== 0) {
nameHeaderLength += 1;
}
// Check for pre photoshop 6 format
if (nameHeaderLength === 0) {
// Always 4
nameHeaderLength = 4;
}
let /** @type {?} */ startOffset = offset + 8 + nameHeaderLength;
let /** @type {?} */ sectionLength = dataView.getUint16(offset + 6 + nameHeaderLength);
return this.readIPTCData(file, startOffset, sectionLength);
}
// Not the marker, continue searching
offset++;
}
}
/**
* @param {?} file
* @param {?} startOffset
* @param {?} sectionLength
* @return {?}
*/
readIPTCData(file, startOffset, sectionLength) {
let /** @type {?} */ dataView = new DataView(file);
let /** @type {?} */ data = {};
let /** @type {?} */ fieldValue, /** @type {?} */ fieldName, /** @type {?} */ dataSize, /** @type {?} */ segmentType;
let /** @type {?} */ segmentStartPos = startOffset;
while (segmentStartPos < startOffset + sectionLength) {
if (dataView.getUint8(segmentStartPos) === 0x1C && dataView.getUint8(segmentStartPos + 1) === 0x02) {
segmentType = dataView.getUint8(segmentStartPos + 2);
if (segmentType in this.iptcFieldMap) {
dataSize = dataView.getInt16(segmentStartPos + 3);
fieldName = this.iptcFieldMap[segmentType];
fieldValue = this.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;
}
/**
* @param {?} file
* @param {?} tiffStart
* @param {?} dirStart
* @param {?} strings
* @param {?} bigEnd
* @return {?}
*/
readTags(file, tiffStart, dirStart, strings, bigEnd) {
let /** @type {?} */ entries = file.getUint16(dirStart, !bigEnd);
let /** @type {?} */ tags = {};
let /** @type {?} */ entryOffset;
let /** @type {?} */ tag;
for (let /** @type {?} */ i = 0; i < entries; i++) {
entryOffset = dirStart + i * 12 + 2;
tag = strings[file.getUint16(entryOffset, !bigEnd)];
if (!tag) {
this.log('Unknown tag: ' + file.getUint16(entryOffset, !bigEnd));
}
tags[tag] = this.readTagValue(file, entryOffset, tiffStart, dirStart, bigEnd);
}
return tags;
}
/**
* @param {?} file
* @param {?} entryOffset
* @param {?} tiffStart
* @param {?} dirStart
* @param {?} bigEnd
* @return {?}
*/
readTagValue(file, entryOffset, tiffStart, dirStart, bigEnd) {
let /** @type {?} */ type = file.getUint16(entryOffset + 2, !bigEnd);
let /** @type {?} */ numValues = file.getUint32(entryOffset + 4, !bigEnd);
let /** @type {?} */ valueOffset = file.getUint32(entryOffset + 8, !bigEnd) + tiffStart;
let /** @type {?} */ offset;
let /** @type {?} */ vals, /** @type {?} */ val, /** @type {?} */ n;
let /** @type {?} */ numerator;
let /** @type {?} */ denominator;
switch (type) {
case 1: // byte, 8-bit unsigned int
case 7:// undefined, 8-bit byte, value depending on field
if (numValues === 1) {
return file.getUint8(entryOffset + 8, !bigEnd);
}
else {
offset = numValues > 4 ? valueOffset : (entryOffset + 8);
vals = [];
for (n = 0; n < numValues; n++) {
vals[n] = file.getUint8(offset + n);
}
return vals;
}
case 2:// ascii, 8-bit byte
offset = numValues > 4 ? valueOffset : (entryOffset + 8);
return this.getStringFromDB(file, offset, numValues - 1);
case 3:// short, 16 bit int
if (numValues === 1) {
return file.getUint16(entryOffset + 8, !bigEnd);
}
else {
offset = numValues > 2 ? valueOffset : (entryOffset + 8);
vals = [];
for (n = 0; n < numValues; n++) {
vals[n] = file.getUint16(offset + 2 * n, !bigEnd);
}
return vals;
}
case 4:// long, 32 bit int
if (numValues === 1) {
return file.getUint32(entryOffset + 8, !bigEnd);
}
else {
vals = [];
for (n = 0; n < numValues; n++) {
vals[n] = file.getUint32(valueOffset + 4 * n, !bigEnd);
}
return vals;
}
case 5:// rational = two long values, first is numerator, second is denominator
if (numValues === 1) {
numerator = file.getUint32(valueOffset, !bigEnd);
denominator = file.getUint32(valueOffset + 4, !bigEnd);
return {
numerator,
denominator,
value: numerator / denominator
};
}
else {
vals = [];
for (n = 0; n < numValues; n++) {
numerator = file.getUint32(valueOffset + 8 * n, !bigEnd);
denominator = file.getUint32(valueOffset + 4 + 8 * n, !bigEnd);
vals[n] = {
numerator,
denominator,
value: numerator / denominator
};
}
return vals;
}
case 9:// slong, 32 bit signed int
if (numValues === 1) {
return file.getInt32(entryOffset + 8, !bigEnd);
}
else {
vals = [];
for (n = 0; n < numValues; n++) {
vals[n] = file.getInt32(valueOffset + 4 * n, !bigEnd);
}
return vals;
}
case 10:// signed rational, two slongs, first is numerator, second is denominator
if (numValues === 1) {
let /** @type {?} */ numerator = file.getInt32(valueOffset, !bigEnd);
let /** @type {?} */ denominator = file.getInt32(valueOffset + 4, !bigEnd);
return numerator / denominator;
}
else {
vals = [];
for (n = 0; n < numValues; n++) {
let /** @type {?} */ numerator = file.getInt32(valueOffset + 8 * n, !bigEnd);
let /** @type {?} */ denominator = file.getInt32(valueOffset + 4 + 8 * n, !bigEnd);
vals[n] = numerator / denominator;
}
return vals;
}
default:
break;
}
}
/**
* @param {?} dataView
* @param {?} tiffStart
* @param {?} firstIFDOffset
* @param {?} bigEnd
* @return {?}
*/
readThumbnailImage(dataView, tiffStart, firstIFDOffset, bigEnd) {
// get the IFD1 offset
let /** @type {?} */ IFD1OffsetPointer = this.getNextIFDOffset(dataView, tiffStart + firstIFDOffset, bigEnd);
if (!IFD1OffsetPointer) {
return {};
}
else if (IFD1OffsetPointer > dataView.byteLength) {
// this should not happen
return {};
}
let /** @type {?} */ thumbTags = this.readTags(dataView, tiffStart, tiffStart + IFD1OffsetPointer, this.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']) {
switch (thumbTags['Compression']) {
case 6:
if (thumbTags['JpegIFOffset'] && thumbTags['JpegIFByteCount']) {
// extract the thumbnail
let /** @type {?} */ tOffset = tiffStart + thumbTags['JpegIFOffset'];
let /** @type {?} */ tLength = thumbTags['JpegIFByteCount'];
thumbTags['blob'] = new Blob([new Uint8Array(dataView.buffer, tOffset, tLength)], {
type: 'image/jpeg'
});
}
break;
case 1:
this.log('Thumbnail image format is TIFF, which is not implemented.');
break;
default:
this.log(`Unknown thumbnail image format: ${thumbTags['Compression']}`);
}
}
else if (thumbTags['PhotometricInterpretation'] == 2) {
this.log('Thumbnail image format is RGB, which is not implemented.');
}
return thumbTags;
}
/**
* Given an IFD (Image File Directory) start offset
* returns an offset to next IFD or 0 if it's the last IFD.
* @param {?} dataView
* @param {?} dirStart
* @param {?} bigEnd
* @return {?}
*/
getNextIFDOffset(dataView, dirStart, bigEnd) {
//the first 2bytes means the number of directory entries contains in this IFD
let /** @type {?} */ entries = dataView.getUint16(dirStart, !bigEnd);
// After last directory entry, there is a 4bytes of data,
// it means an offset to next IFD.
// If its value is '0x00000000', it means this is the last IFD and there is no linked IFD.
return dataView.getUint32(dirStart + 2 + entries * 12, !bigEnd); // each entry is 12 bytes long
}
/**
* @param {?} file
* @param {?} start
* @return {?}
*/
readEXIFData(file, start) {
if (this.getStringFromDB(file, start, 4) != 'Exif') {
this.log(`Not valid EXIF data! ${this.getStringFromDB(file, start, 4)}`);
return false;
}
let /** @type {?} */ bigEnd, /** @type {?} */ tags, /** @type {?} */ tag, /** @type {?} */ exifData, /** @type {?} */ gpsData;
let /** @type {?} */ 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 {
this.log('Not valid TIFF data! (no 0x4949 or 0x4D4D)');
return false;
}
if (file.getUint16(tiffOffset + 2, !bigEnd) != 0x002A) {
this.log('Not valid TIFF data! (no 0x002A)');
return false;
}
let /** @type {?} */ firstIFDOffset = file.getUint32(tiffOffset + 4, !bigEnd);
if (firstIFDOffset < 0x00000008) {
this.log('Not valid TIFF data! (First offset less than 8)', file.getUint32(tiffOffset + 4, !bigEnd));
return false;
}
tags = this.readTags(file, tiffOffset, tiffOffset + firstIFDOffset, this.tiffTags, bigEnd);
if (tags.ExifIFDPointer) {
exifData = this.readTags(file, tiffOffset, tiffOffset + tags.ExifIFDPointer, this.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] = this.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] =
this.stringValues.Components[exifData[tag][0]] +
this.stringValues.Components[exifData[tag][1]] +
this.stringValues.Components[exifData[tag][2]] +
this.stringValues.Components[exifData[tag][3]];
break;
default:
break;
}
tags[tag] = exifData[tag];
}
}
if (tags.GPSInfoIFDPointer) {
gpsData = this.readTags(file, tiffOffset, tiffOffset + tags.GPSInfoIFDPointer, this.gpsTags, bigEnd);
for (tag in gpsData) {
switch (tag) {
case 'GPSVersionID':
// Skip broken GPSVersionID - iggfisk
// https://github.com/exif-js/exif-js/pull/161
if (gpsData[tag]) {
gpsData[tag] = `${gpsData[tag][0]}.${gpsData[tag][1]}.${gpsData[tag][2]}.${gpsData[tag][3]}`;
}
break;
default:
break;
}
tags[tag] = gpsData[tag];
}
}
// extract thumbnail
tags.thumbnail = this.readThumbnailImage(file, tiffOffset, firstIFDOffset, bigEnd);
return tags;
}
/**
* @param {?} buffer
* @param {?} start
* @param {?} length
* @return {?}
*/
getStringFromDB(buffer, start, length) {
let /** @type {?} */ outputString = '';
for (let /** @type {?} */ n = start; n < start + length; n++) {
outputString += String.fromCharCode(buffer.getUint8(n));
}
return outputString;
}
/**
* @param {?} file
* @return {?}
*/
findXMPinJPEG(file) {
if (!('DOMParser' in self)) {
return;
}
let /** @type {?} */ dataView = new DataView(file);
this.log(`Got file of length ${file.byteLength}`);
if ((dataView.getUint8(0) != 0xFF) || (dataView.getUint8(1) != 0xD8)) {
this.log('Not a valid JPEG');
return false; // not a valid jpeg
}
let /** @type {?} */ offset = 2;
let /** @type {?} */ length = file.byteLength;
let /** @type {?} */ dom = new DOMParser();
while (offset < (length - 4)) {
if (this.getStringFromDB(dataView, offset, 4) == 'http') {
let /** @type {?} */ startOffset = offset - 1;
let /** @type {?} */ sectionLength = dataView.getUint16(offset - 2) - 1;
let /** @type {?} */ xmpString = this.getStringFromDB(dataView, startOffset, sectionLength);
let /** @type {?} */ xmpEndIndex = xmpString.indexOf('xmpmeta>') + 8;
xmpString = xmpString.substring(xmpString.indexOf('<x:xmpmeta'), xmpEndIndex);
let /** @type {?} */ indexOfXmp = xmpString.indexOf('x:xmpmeta') + 10;
// Many custom written programs embed xmp/xml without any namespace. Following are some of them.
// Without these namespaces, XML is thought to be invalid by parsers
xmpString = xmpString.slice(0, indexOfXmp)
+ 'xmlns:Iptc4xmpCore="http://iptc.org/std/Iptc4xmpCore/1.0/xmlns/" '
+ 'xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" '
+ 'xmlns:tiff="http://ns.adobe.com/tiff/1.0/" '
+ 'xmlns:plus="http://schemas.android.com/apk/lib/com.google.android.gms.plus" '
+ 'xmlns:ext="http://www.gettyimages.com/xsltExtension/1.0" '
+ 'xmlns:exif="http://ns.adobe.com/exif/1.0/" '
+ 'xmlns:stEvt="http://ns.adobe.com/xap/1.0/sType/ResourceEvent#" '
+ 'xmlns:stRef="http://ns.adobe.com/xap/1.0/sType/ResourceRef#" '
+ 'xmlns:crs="http://ns.adobe.com/camera-raw-settings/1.0/" '
+ 'xmlns:xapGImg="http://ns.adobe.com/xap/1.0/g/img/" '
+ 'xmlns:Iptc4xmpExt="http://iptc.org/std/Iptc4xmpExt/2008-02-29/" '
+ xmpString.slice(indexOfXmp);
let /** @type {?} */ domDocument = dom.parseFromString(xmpString, 'text/xml');
return this.xml2Object(domDocument);
}
else {
offset++;
}
}
}
/**
* @param {?} xml
* @return {?}
*/
xml2json(xml) {
let /** @type {?} */ json = {};
if (xml.nodeType == 1) {
if (xml.attributes.length > 0) {
json['@attributes'] = {};
for (let /** @type {?} */ j = 0; j < xml.attributes.length; j++) {
let /** @type {?} */ attribute = xml.attributes.item(j);
json['@attributes'][attribute.nodeName] = attribute.nodeValue;
}
}
}
else if (xml.nodeType == 3) {
return xml.nodeValue;
}
// deal with children
if (xml.hasChildNodes()) {
for (let /** @type {?} */ i = 0; i < xml.childNodes.length; i++) {
let /** @type {?} */ child = xml.childNodes.item(i);
let /** @type {?} */ nodeName = child.nodeName;
if (json[nodeName] == null) {
json[nodeName] = this.xml2json(child);
}
else {
if (json[nodeName].push == null) {
let /** @type {?} */ old = json[nodeName];
json[nodeName] = [];
json[nodeName].push(old);
}
json[nodeName].push(this.xml2json(child));
}
}
}
return json;
}
/**
* @param {?} xml
* @return {?}
*/
xml2Object(xml) {
let /** @type {?} */ obj = {};
if (xml.children.length > 0) {
try {
for (let /** @type {?} */ i = 0; i < xml.children.length; i++) {
let /** @type {?} */ item = xml.children.item(i);
let /** @type {?} */ attributes = item.attributes;
for (let /** @type {?} */ idx in attributes) {
let /** @type {?} */ itemAtt = attributes[idx];
let /** @type {?} */ dataKey = itemAtt.nodeName;
let /** @type {?} */ dataValue = itemAtt.nodeValue;
if (dataKey !== undefined) {
obj[dataKey] = dataValue;
}
}
let /** @type {?} */ nodeName = item.nodeName;
if (typeof (obj[nodeName]) == 'undefined') {
obj[nodeName] = this.xml2json(item);
}
else {
if (typeof (obj[nodeName].push) == 'undefined') {
let /** @type {?} */ old = obj[nodeName];
obj[nodeName] = [];
obj[nodeName].push(old);
}
obj[nodeName].push(this.xml2json(item));
}
}
}
catch (error) {
this.log(error.message);
}
}
else {
obj = xml.textContent;
}
return obj;
}
/**
* @param {?} imageData
* @return {?}
*/
imageHasData(imageData) {
return !!(imageData.exif);
}
/**
* @param {...?} args
* @return {?}
*/
log(...args) {
if (this._debug) {
console.log(args);
}
}
}
ExifService.decorators = [
{ type: Injectable },
];
/**
* @nocollapse
*/
ExifService.ctorParameters = () => [];
class ImageCropperExifService {
constructor() {
this.invalidImageFormatProvided = 'Invalid image format provided.';
}
/**
* Check the image orientation from the EXIF data associated with the image.
* If necessary, rotate and flip the image based on the orientation parameter.
*
* @param {?} image Original image provided by the user
* @param {?} originalFileType File type of the image that was provided
*
* @return {?} An Observable containing either the original image if the orientation
* value is 1 or the modified image if the orientation is not 1.
*/
getOrientedImage(image, originalFileType) {
return Observable$1.create((observer) => {
if (!image) {
observer.error(new Error(this.invalidImageFormatProvided));
return;
}
const /** @type {?} */ exifService = new ExifService();
exifService.fileType(originalFileType);
exifService.getData(image).subscribe((imageData) => {
const /** @type {?} */ imageOrientation = exifService.getTag(imageData, "Orientation");
// if the orientation is set to 1, exit because we do not
// need to do anything with the image as it is already in
// the proper orientation
if (!imageOrientation || imageOrientation === 1) {
observer.next(imageData);
return;
}
let /** @type {?} */ width = imageData.width;
if (width === 0 && imageData.exif.ImageWidth > 0) {
width = imageData.exif.ImageWidth;
imageData.width = width;
}
let /** @type {?} */ height = imageData.height;
if (height === 0 && imageData.exif.ImageHeight > 0) {
height = imageData.exif.ImageHeight;
imageData.height = height;
}
const /** @type {?} */ destinationParameters = this.destinationParameters(imageOrientation, width, height);
this.modifyImage(destinationParameters, imageData, originalFileType).subscribe((modifiedImage) => {
// Return the modified image
observer.next(modifiedImage);
}, (error) => {
observer.error(new Error(error));
});
}, (error) => {
observer.error(new Error(error));
});
});
}
/**
* Determine the parameters to use for the destination canvas so that images
* are not rotated horizontally when taking a photo on a mobile device.
*
* @param {?} imageOrientation The orientation of the image as extracted from the EXIF data
* @param {?} width Original width of the provided image
* @param {?} height Original height of the provided image
*
* @return {?} Canvas destination values
*/
destinationParameters(imageOrientation, width, height) {
let /** @type {?} */ destinationX = 0;
let /** @type {?} */ destinationY = 0;
let /** @type {?} */ destinationWidth = width;
let /** @type {?} */ destinationHeight = height;
let /** @type {?} */ rotationDegree = 0;
switch (imageOrientation) {
case 3:
case 4:
destinationX = -width;
destinationY = -height;
rotationDegree = 180;
break;
case 5:
case 6:
destinationWidth = height;
destinationHeight = width;
destinationY = -height;
rotationDegree = 90;
break;
case 7:
case 8:
destinationWidth = height;
destinationHeight = width;
destinationX = -width;
rotationDegree = 270;
break;
default:
break;
}
return {
x: destinationX,
y: destinationY,
width: destinationWidth,
height: destinationHeight,
rotationDegree,
imageOrientation
};
}
/**
* Check the image orientation and modify the image if necessary
*
* @param {?} destinationParameters Parameters to use for the new image. i.e. height, width, rotation, etc.
* @param {?} image The image to be modified
* @param {?} fileType
* @return {?} A new Image that has been rotated so that it appears in portait mode rather than landscape
*/
modifyImage(destinationParameters, image, fileType) {
return Observable$1.create((observer) => {
const /** @type {?} */ rotateCanvas = document.createElement("canvas");
const /** @type {?} */ rotateContext = (rotateCanvas.getContext('2d'));
rotateCanvas.width = destinationParameters.width;
rotateCanvas.height = destinationParameters.height;
if (rotateContext) {
if ([2, 4, 5, 7].indexOf(destinationParameters.imageOrientation) > -1) {
// flip image
rotateContext.translate(destinationParameters.width, 0);
rotateContext.scale(-1, 1);
}
// Rotate the image as necessary by converting degrees to radians.
// The rotationDegree should only be set to something other than 0
// when the image provided is from a mobile device.
rotateContext.rotate(destinationParameters.rotationDegree * Math.PI / 180);
rotateContext.drawImage(image, destinationParameters.x, destinationParameters.y);
const /** @type {?} */ modifiedImage = new Image();
modifiedImage.width = destinationParameters.width;
modifiedImage.height = destinationParameters.height;
modifiedImage.onload = () => {
observer.next(modifiedImage);
};
modifiedImage.src = rotateCanvas.toDataURL(fileType, 1);
}
else {
observer.next(image);
}
});
}
}
ImageCropperExifService.decorators = [
{ type: Injectable },
];
/**
* @nocollapse
*/
ImageCropperExifService.ctorParameters = () => [];
class ImageCropperComponent {
/**
* @param {?} elementRef
* @param {?} sanitizer
* @param {?} cd
* @param {?} exifService
*/
constructor(elementRef, sanitizer, cd, exifService) {
this.elementRef = elementRef;
this.sanitizer = sanitizer;
this.cd = cd;
this.exifService = exifService;
this.marginLeft = '0px';
this.imageVisible = false;
this.format = 'png';
this.maintainAspectRatio = true;
this.aspectRatio = 1;
this.resizeToWidth = 0;
this.onlyScaleDown = false;
this.imageQuality = 92;
this.cropper = {
x1: -100,
y1: -100,
x2: 10000,
y2: 10000
};
this.imageCropped = new EventEmitter();
this.imageLoaded = new EventEmitter();
this.loadImageFailed = new EventEmitter();
this.initCropper();
}
/**
* @param {?} event
* @return {?}
*/
set imageChangedEvent(event) {
this.initCropper();
if (event && event.target && event.target.files && event.target.files.length > 0) {
this.loadImage(event);
}
}
/**
* @param {?} imageBase64
* @return {?}
*/
set imageBase64(imageBase64) {
this.initCropper();
this.loadBase64Image(imageBase64);
}
/**
* @param {?} changes
* @return {?}
*/
ngOnChanges(changes) {
if (changes['cropper']) {
setTimeout(() => {
this.setMaxSize();
this.checkCropperPosition(false);
this.crop();
this.cd.markForCheck();
});
}
}
/**
* @return {?}
*/
initCropper() {
this.imageVisible = false;
this.originalImage = null;
this.safeImgDataUrl = 'data:image/png;base64,iVBORw0KGg'
+ 'oAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAC0lEQVQYV2NgAAIAAAU'
+ 'AAarVyFEAAAAASUVORK5CYII=';
this.moveStart = {
active: false,
type: null,
position: null,
x1: 0,
y1: 0,
x2: 0,
y2: 0,
clientX: 0,
clientY: 0
};
this.maxSize = {
width: 0,
height: 0
};
this.originalSize = {
width: 0,
height: 0
};
this.cropper.x1 = -100;
this.cropper.y1 = -100;
this.cropper.x2 = 10000;
this.cropper.y2 = 10000;
}
/**
* @param {?} event
* @return {?}
*/
loadImage(event) {
const /** @type {?} */ file = event.target.files[0];
// Check the file type to see if a match is found.
// If a match is found then the file uploaded is a valid image type
if (/(jpe?g|png|gif)/i.test(file.type) === false) {
this.loadImageFailed.emit();
return;
}
let /** @type {?} */ fileReader = new FileReader();
fileReader.onloadend = (loadEvent) => {
const /** @type {?} */ image = loadEvent.target.result;
this.exifService.getOrientedImage(image, file.type).subscribe((modifiedImage) => {
this.loadBase64Image(modifiedImage.src);
}, (error) => {
this.loadImageFailed.emit();
});
};
fileReader.readAsArrayBuffer(file);
}
/**
* @param {?} imageBase64
* @return {?}
*/
loadBase64Image(imageBase64) {
this.originalImage = new Image();
this.originalImage.onload = () => {
this.originalSize.width = this.originalImage.width;
this.originalSize.height = this.originalImage.height;
this.cd.markForCheck();
};
this.safeImgDataUrl = this.sanitizer.bypassSecurityTrustResourceUrl(imageBase64);
this.originalImage.src = imageBase64;
}
/**
* @return {?}
*/
imageLoadedInView() {
if (this.originalImage != null) {
this.imageLoaded.emit();
setTimeout(() => {
this.setMaxSize();
this.resetCropperPosition();
this.cd.markForCheck();
});
}
}
/**
* @param {?} event
* @return {?}
*/
imageResizedInView(event) {
this.resizeCropperPosition();
this.setMaxSize();
}
/**
* @return {?}
*/
resizeCropperPosition() {
const /** @type {?} */ displayedImage = this.elementRef.nativeElement.querySelector('.source-image');
if (this.maxSize.width !== displayedImage.offsetWidth || this.maxSize.height !== displayedImage.offsetHeight) {
this.cropper.x1 = this.cropper.x1 * displayedImage.offsetWidth / this.maxSize.width;
this.cropper.x2 = this.cropper.x2 * displayedImage.offsetWidth / this.maxSize.width;
this.cropper.y1 = this.cropper.y1 * displayedImage.offsetHeight / this.maxSize.height;
this.cropper.y2 = this.cropper.y2 * displayedImage.offsetHeight / this.maxSize.height;
}
}
/**
* @return {?}
*/
resetCropperPosition() {
const /** @type {?} */ displayedImage = this.elementRef.nativeElement.querySelector('.source-image');
if (displayedImage.offsetWidth / this.aspectRatio < displayedImage.offsetHeight) {
this.cropper.x1 = 0;
this.cropper.x2 = displayedImage.offsetWidth;
const /** @type {?} */ cropperHeight = displayedImage.offsetWidth / this.aspectRatio;
this.cropper.y1 = (displayedImage.offsetHeight - cropperHeight) / 2;
this.cropper.y2 = this.cropper.y1 + cropperHeight;
}
else {
this.cropper.y1 = 0;
this.cropper.y2 = displayedImage.offsetHeight;
const /** @type {?} */ cropperWidth = displayedImage.offsetHeight * this.aspectRatio;
this.cropper.x1 = (displayedImage.offsetWidth - cropperWidth) / 2;
this.cropper.x2 = this.cropper.x1 + cropperWidth;
}
this.crop();
this.imageVisible = true;
}
/**
* @param {?} event
* @param {?} moveType
* @param {?=} position
* @return {?}
*/
startMove(event, moveType, position = null) {
this.moveStart.active = true;
this.moveStart.type = moveType;
this.moveStart.position = position;
this.moveStart.clientX = this.getClientX(event);
this.moveStart.clientY = this.getClientY(event);
Object.assign(this.moveStart, this.cropper);
this.cd.markForCheck();
}
/**
* @param {?} event
* @return {?}
*/
moveImg(event) {
if (this.moveStart.active) {
event.stopPropagation();
event.preventDefault();
this.setMaxSize();
if (this.moveStart.type === 'move') {
this.move(event);
this.checkCropperPosition(true);
}
else if (this.moveStart.type === 'resize') {
this.resize(event);
this.checkCropperPosition(false);
}
this.cd.markForCheck();
}
}
/**
* @return {?}
*/
setMaxSize() {
const /** @type {?} */ el = this.elementRef.nativeElement.querySelector('.source-image');
this.maxSize.width = el.offsetWidth;
this.maxSize.height = el.offsetHeight;
this.marginLeft = this.sanitizer.bypassSecurityTrustStyle('calc(50% - ' + this.maxSize.width / 2 + 'px)');
}
/**
* @param {?=} maintainSize
* @return {?}
*/
checkCropperPosition(maintainSize = false) {
if (this.cropper.x1 < 0) {
this.cropper.x2 -= maintainSize ? this.cropper.x1 : 0;
this.cropper.x1 = 0;
}
if (this.cropper.y1 < 0) {
this.cropper.y2 -= maintainSize ? this.cropper.y1 : 0;
this.cropper.y1 = 0;
}
if (this.cropper.x2 > this.maxSize.width) {
this.cropper.x1 -= maintainSize ? (this.cropper.x2 - this.maxSize.width) : 0;
this.cropper.x2 = this.maxSize.width;
}
if (this.cropper.y2 > this.maxSize.height) {
this.cropper.y1 -= maintainSize ? (this.cropper.y2 - this.maxSize.height) : 0;
this.cropper.y2 = this.maxSize.height;
}
}
/**
* @param {?} event
* @return {?}
*/
moveStop(event) {
if (this.moveStart.active) {
this.moveStart.active = false;
this.crop();
this.cd.markForCheck();
}
}
/**
* @param {?} event
* @return {?}
*/
move(event) {
const /** @type {?} */ diffX = this.getClientX(event) - this.moveStart.clientX;
const /** @type {?} */ diffY = this.getClientY(event) - this.moveStart.clientY;
this.cropper.x1 = this.moveStart.x1 + diffX;
this.cropper.y1 = this.moveStart.y1 + diffY;
this.cropper.x2 = this.moveStart.x2 + diffX;
this.cropper.y2 = this.moveStart.y2 + diffY;
}
/**
* @param {?} event
* @return {?}
*/
resize(event) {
const /** @type {?} */ diffX = this.getClientX(event) - this.moveStart.clientX;
const /** @type {?} */ diffY = this.getClientY(event) - this.moveStart.clientY;
switch (this.moveStart.position) {
case 'left':
this.cropper.x1 = Math.min(this.moveStart.x1 + diffX, this.cropper.x2 - 20);
break;
case 'topleft':
this.cropper.x1 = Math.min(this.moveStart.x1 + diffX, this.cropper.x2 - 20);
this.cropper.y1 = Math.min(this.moveStart.y1 + diffY, this.cropper.y2 - 20);
break;
case 'top':
this.cropper.y1 = Math.min(this.moveStart.y1 + diffY, this.cropper.y2 - 20);
break;
case 'topright':
this.cropper.x2 = Math.max(this.moveStart.x2 + diffX, this.cropper.x1 + 20);
this.cropper.y1 = Math.min(this.moveStart.y1 + diffY, this.cropper.y2 - 20);
break;
case 'right':
this.cropper.x2 = Math.max(this.moveStart.x2 + diffX, this.cropper.x1 + 20);
break;
case 'bottomright':
this.cropper.x2 = Math.max(this.moveStart.x2 + diffX, this.cropper.x1 + 20);
this.cropper.y2 = Math.max(this.moveStart.y2 + diffY, this.cropper.y1 + 20);
break;
case 'bottom':
this.cropper.y2 = Math.max(this.moveStart.y2 + diffY, this.cropper.y1 + 20);
break;
case 'bottomleft':
this.cropper.x1 = Math.min(this.moveStart.x1 + diffX, this.cropper.x2 - 20);
this.cropper.y2 = Math.max(this.moveStart.y2 + diffY, this.cropper.y1 + 20);
break;
}
if (this.maintainAspectRatio) {
this.checkAspectRatio();
}
}
/**
* @return {?}
*/
checkAspectRatio() {
let /** @type {?} */ overflowX = 0;
let /** @type {?} */ overflowY = 0;
switch (this.moveStart.position) {
case 'top':
this.cropper.x2 = this.cropper.x1 + (this.cropper.y2 - this.cropper.y1) * this.aspectRatio;
overflowX = Math.max(this.cropper.x2 - this.maxSize.width, 0);
overflowY = Math.max(0 - this.cropper.y1, 0);
if (overflowX > 0 || overflowY > 0) {
this.cropper.x2 -= (overflowY * this.aspectRatio) > overflowX ? (overflowY * this.aspectRatio) : overflowX;
this.cropper.y1 += (overflowY * this.aspectRatio) > overflowX ? overflowY : overflowX / this.aspectRatio;
}
break;
case 'bottom':
this.cropper.x2 = this.cropper.x1 + (this.cropper.y2 - this.cropper.y1) * this.aspectRatio;
overflowX = Math.max(this.cropper.x2 - this.maxSize.width, 0);
overflowY = Math.max(this.cropper.y2 - this.maxSize.height, 0);
if (overflowX > 0 || overflowY > 0) {
this.cropper.x2 -= (overflowY * this.aspectRatio) > overflowX ? (overflowY * this.aspectRatio) : overflowX;
this.cropper.y2 -= (overflowY * this.aspectRatio) > overflowX ? overflowY : (overflowX / this.aspectRatio);
}
break;
case 'topleft':
this.cropper.y1 = this.cropper.y2 - (this.cropper.x2 - this.cropper.x1) / this.aspectRatio;
overflowX = Math.max(0 - this.cropper.x1, 0);
overflowY = Math.max(0 - this.cropper.y1, 0);
if (overflowX > 0 || overflowY > 0) {
this.cropper.x1 += (overflowY * this.aspectRatio) > overflowX ? (overflowY * this.aspectRatio) : overflowX;
this.cropper.y1 += (overflowY * this.aspectRatio) > overflowX ? overflowY : overflowX / this.aspectRatio;
}
break;
case 'topright':
this.cropper.y1 = this.cropper.y2 - (this.cropper.x2 - this.cropper.x1) / this.aspectRatio;
overflowX = Math.max(this.cropper.x2 - this.maxSize.width, 0);
overflowY = Math.max(0 - this.cropper.y1, 0);
if (overflowX > 0 || overflowY > 0) {
this.cropper.x2 -= (overflowY * this.aspectRatio) > overflowX ? (overflowY * this.aspectRatio) : overflowX;
this.cropper.y1 += (overflowY * this.aspectRatio) > overflowX ? overflowY : overflowX / this.aspectRatio;
}
break;
case 'right':
case 'bottomright':
this.cropper.y2 = this.cropper.y1 + (this.cropper.x2 - this.cropper.x1) / this.aspectRatio;
overflowX = Math.max(this.cropper.x2 - this.maxSize.width, 0);
overflowY = Math.max(this.cropper.y2 - this.maxSize.height, 0);
if (overflowX > 0 || overflowY > 0) {
this.cropper.x2 -= (overflowY * this.aspectRatio) > overflowX ? (overflowY * this.aspectRatio) : overflowX;
this.cropper.y2 -= (overflowY * this.aspectRatio) > overflowX ? overflowY : overflowX / this.aspectRatio;
}
break;
case 'left':
case 'bottomleft':
this.cropper.y2 = this.cropper.y1 + (this.cropper.x2 - this.cropper.x1) / this.aspectRatio;
overflowX = Math.max(0 - this.cropper.x1, 0);
overflowY = Math.max(this.cropper.y2 - this.maxSize.height, 0);
if (overflowX > 0 || overflowY > 0) {
this.cropper.x1 += (overflowY * this.aspectRatio) > overflowX ? (overflowY * this.aspectRatio) : overflowX;
this.cropper.y2 -= (overflowY * this.aspectRatio) > overflowX ? overflowY : overflowX / this.aspectRatio;
}
break;
}
}
/**
* @return {?}
*/
crop() {
const /** @type {?} */ displayedImage = this.elementRef.nativeElement.querySelector('.source-image');
if (displayedImage && this.originalImage != null) {
const /** @type {?} */ ratio = this.calculateRatio(this.originalImage, displayedImage);
const /** @type {?} */ left = Math.round(this.cropper.x1 * ratio);
const /** @type {?} */ top = Math.round(this.cropper.y1 * ratio);
const /** @type {?} */ width = Math.round((this.cropper.x2 - this.cropper.x1) * ratio);
const /** @type {?} */ height = Math.round((this.cropper.y2 - this.cropper.y1) * ratio);
const /** @type {?} */ resizeRatio = this.getResizeRatio(width);
const /** @type {?} */ resizedWidth = width * resizeRatio;
const /** @type {?} */ resizedHeight = height * resizeRatio;
const /** @type {?} */ cropCanvas = (document.createElement('canvas'));
cropCanvas.width = resizedWidth;
cropCanvas.height = resizedHeight;
const /** @type {?} */ context = cropCanvas.getContext('2d');
if (context) {
// https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/drawImage
// drawImage(
// image,
// source x,
// source y,
// source width,
// source height,
// destination x,
// destination y,
// destination width,
// destination height
// )
context.drawImage(this.originalImage, left, top, width, height, 0, 0, resizedWidth, resizedHeight);
const /** @type {?} */ quality = Math.min(1, Math.max(0, this.imageQuality / 100));
const /** @type {?} */ croppedImage = cropCanvas.toDataURL(`image/${this.format}`, quality);
if (croppedImage.length > 10) {
this.imageCropped.emit(croppedImage);
}
}
}
}
/**
* @param {?} width
* @return {?}
*/
getResizeRatio(width) {
return this.resizeToWidth > 0 && (!this.onlyScaleDown || width > this.resizeToWidth)
? this.resizeToWidth / width
: 1;
}
/**
* @param {?} event
* @return {?}
*/
getClientX(event) {
return event.clientX != null ? event.clientX : event.touches[0].clientX;
}
/**
* @param {?} event
* @return {?}
*/
getClientY(event) {
return event.clientY != null ? event.clientY : event.touches[0].clientY;
}
/**
* Calculate the ratio to use when determining the proper
* cropping coordinates the user has chosen
*
* @param {?} workingImage The image we are currently working with. This may be the original
* image the user took or a rotated version of that image
* @param {?} displayedImage Image that is currently set in the img component in the HTML view
* @return {?}
*/
calculateRatio(workingImage, displayedImage) {
return workingImage.width / displayedImage.offsetWidth;
}
}
ImageCropperComponent.decorators = [
{ type: Component, args: [{
selector: 'image-cropper',
template: `
<div>
<img
[src]="safeImgDataUrl"
[style.visibility]="imageVisible ? 'visible' : 'hidden'"
(load)="imageLoadedInView()"
class="source-image"
/>
<div class="cropper"
[style.top.px]="cropper.y1"
[style.left.px]="cropper.x1"
[style.width.px]="cropper.x2 - cropper.x1"
[style.height.px]="cropper.y2 - cropper.y1"
[style.margin-left]="marginLeft"
[style.visibility]="imageVisible ? 'visible' : 'hidden'"
>
<div
(mousedown)="startMove($event, 'move')"
(touchstart)="startMove($event, 'move')"
class="move"
> </div>
<span
class="resize topleft"
(mousedown)="startMove($event, 'resize', 'topleft')"
(touchstart)="startMove($event, 'resize', 'topleft')"
><span class="square"></span></span>
<span
class="resize top"
><span class="square"></span></span>
<span
class="resize topright"
(mousedown)="startMove($event, 'resize', 'topright')"
(touchstart)="startMove($event, 'resize', 'topright')"
><span class="square"></span></span>
<span
class="resize right"
><span class="square"></span></span>
<span
class="resize bottomright"
(mousedown)="startMove($event, 'resize', 'bottomright')"
(touchstart)="startMove($event, 'resize', 'bottomright')"
><span class="square"></span></span>
<span
class="resize bottom"
><span class="square"></span></span>
<span
class="resize bottomleft"
(mousedown)="startMove($event, 'resize', 'bottomleft')"
(touchstart)="startMove($event, 'resize', 'bottomleft')"
><span class="square"></span></span>
<span
class="resize left"
><span class="square"></span></span>
<span
class="resize-bar top"
(mousedown)="startMove($event, 'resize', 'top')"
(touchstart)="startMove($event, 'resize', 'top')"
></span>
<span
class="resize-bar right"
(mousedown)="startMove($event, 'resize', 'right')"
(touchstart)="startMove($event, 'resize', 'right')"
></span>
<span
class="resize-bar bottom"
(mousedown)="startMove($event, 'resize', 'bottom')"
(touchstart)="startMove($event, 'resize', 'bottom')"
></span>
<span
class="resize-bar left"
(mousedown)="startMove($event, 'resize', 'left')"
(touchstart)="startMove($event, 'resize', 'left')"
></span>
</div>
</div>
`,
styles: [`
:host {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
position: relative;
width: 100%;
max-width: 100%;
max-height: 100%;
overflow: hidden;
padding: 5px;
text-align: center;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none; }
:host > div {
position: relative;
width: 100%; }
:host > div .source-image {
max-width: 100%;
max-height: 100%; }
:host .cropper {
position: absolute;
display: -webkit-box;
display: -ms-flexbox;
display: flex;
color: #53535C !important;
background: transparent !important;
outline-color: rgba(255, 255, 255, 0.3);
outline-width: 1000px;
outline-style: solid;
-ms-touch-action: none;
touch-action: none; }
:host .cropper:after {
position: absolute;
content: '';
top: 0;
bottom: 0;
left: 0;
right: 0;
pointer-events: none;
border: dashed 1px;
opacity: .75;
color: inherit;
z-index: 1; }
:host .cropper .move {
width: 100%;
cursor: move;
border: 1px solid rgba(255, 255, 255, 0.5); }
:host .cropper .resize {
position: absolute;
display: inline-block;
line-height: 6px;
padding: 8px;
opacity: .85;
z-index: 1; }
:host .cropper .resize .square {
display: inline-block;
background: #53535C !important;
width: 6px;
height: 6px;
border: 1px solid rgba(255, 255, 255, 0.5); }
:host .cropper .resize.topleft {
top: -12px;
left: -12px;
cursor: nw-resize; }
:host .cropper .resize.top {
top: -12px;
left: calc(50% - 12px);
cursor: n-resize; }
:host .cropper .resize.topright {
top: -12px;
right: -12px;
cursor: ne-resize; }
:host .cropper .resize.right {
top: calc(50% - 12px);
right: -12px;
cursor: e-resize; }
:host .cropper .resize.bottomright {
bottom: -12px;
right: -12px;
cursor: se-resize; }
:host .cropper .resize.bottom {
bottom: -12px;
left: calc(50% - 12px);
cursor: s-resize; }
:host .cropper .resize.bottomleft {
bottom: -12px;
left: -12px;
cursor: sw-resize; }
:host .cropper .resize.left {
top: calc(50% - 12px);
left: -12px;
cursor: w-resize; }
:host .cropper .resize-bar {
position: absolute;
z-index: 1; }
:host .cropper .resize-bar.top {
top: -11px;
left: 11px;
width: calc(100% - 22px);
height: 22px;
cursor: n-resize; }
:host .cropper .resize-bar.right {
top: 11px;
right: -11px;
height: calc(100% - 22px);
width: 22px;
cursor: e-resize; }
:host .cropper .resize-bar.bottom {
bottom: -11px;
left: 11px;
width: calc(100% - 22px);
height: 22px;
cursor: s-resize; }
:host .cropper .resize-bar.left {
top: 11px;
left: -11px;
height: calc(100% - 22px);
width: 22px;
cursor: w-resize; }
`],
changeDetection: ChangeDetectionStrategy.OnPush
},] },
];
/**
* @nocollapse
*/
ImageCropperComponent.ctorParameters = () => [
{ type: ElementRef, },
{ type: DomSanitizer, },
{ type: ChangeDetectorRef, },
{ type: ImageCropperExifService, },
];
ImageCropperComponent.propDecorators = {
'imageChangedEvent': [{ type: Input },],
'imageBase64': [{ type: Input },],
'format': [{ type: Input },],
'maintainAspectRatio': [{ type: Input },],
'aspectRatio': [{ type: Input },],
'resizeToWidth': [{ type: Input },],
'onlyScaleDown': [{ type: Input },],
'imageQuality': [{ type: Input },],
'cropper': [{ type: Input },],
'imageCropped': [{ type: Output },],
'imageLoaded': [{ type: Output },],
'loadImageFailed': [{ type: Output },],
'imageResizedInView': [{ type: HostListener, args: ['window:resize', ['$event'],] },],
'moveImg': [{ type: HostListener, args: ['document:mousemove', ['$event'],] }, { type: HostListener, args: ['document:touchmove', ['$event'],] },],
'moveStop': [{ type: HostListener, args: ['document:mouseup', ['$event'],] }, { type: HostListener, args: ['document:touchend', ['$event'],] },],
};
class ImageCropperModule {
}
ImageCropperModule.decorators = [
{ type: NgModule, args: [{
imports: [
CommonModule
],
declarations: [
ImageCropperComponent
],
exports: [
ImageCropperComponent
],
/**
* Expose Services and Providers into Angular's dependency injection.
*/
providers: [
ExifService,
ImageCropperExifService
]
},] },
];
/**
* @nocollapse
*/
ImageCropperModule.ctorParameters = () => [];
/**
* Generated bundle index. Do not edit.
*/
export { ExifService, ImageCropperModule, ImageCropperComponent, ImageCropperExifService };
//# sourceMappingURL=ngx-image-cropper-facet.js.map