UNPKG

ngx-image-cropper-facet

Version:
2,067 lines 81.1 kB
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"
              >&nbsp;</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