UNPKG

mc-image-editor

Version:

An image editor library for magic-cut app (http://www.magic-cut.in/)

498 lines (486 loc) 19 kB
import { ɵɵdefineInjectable, Injectable, EventEmitter, Component, ChangeDetectionStrategy, Output, Input, HostListener, ɵɵinject, ElementRef, ViewChild, NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import * as _ from 'lodash'; import { ReplaySubject, fromEvent } from 'rxjs'; class FileRepositoryService { constructor() { this.scopes = {}; } scope(scope) { if (!this.scopes[scope]) { this.scopes[scope] = []; } return this.scopes[scope]; } } FileRepositoryService.ɵprov = ɵɵdefineInjectable({ factory: function FileRepositoryService_Factory() { return new FileRepositoryService(); }, token: FileRepositoryService, providedIn: "root" }); FileRepositoryService.decorators = [ { type: Injectable, args: [{ providedIn: 'root', },] } ]; class FileReaderComponent { constructor(fileRepositoryService) { this.fileRepositoryService = fileRepositoryService; this.fileAppend = new EventEmitter(); this.accept = '*'; this.scope = ''; } set multiple(value) { this._multiple = typeof value !== 'undefined'; } get multiple() { return this._multiple; } set dropzone(value) { this._dropzone = typeof value !== 'undefined'; if (!this._dropzone) { this._multiple = true; } } get dropzone() { return this._dropzone; } onDragOver($event) { $event.stopPropagation(); $event.preventDefault(); } onDrop($event) { $event.stopPropagation(); $event.preventDefault(); } readFiles(files) { const changed = [...files].map((f) => { if (f.type.match(this.accept.replace('*', '.*'))) { this.fileRepositoryService.scope(this.scope).push(f); return f; } }); this.fileAppend.emit(changed); return changed; } onFilesAppend(files) { this.fileAppend.emit(this.readFiles(files)); } } FileReaderComponent.decorators = [ { type: Component, args: [{ selector: 'mc-file-reader', template: "<div *ngIf=\"dropzone\" (drop)=\"onFilesAppend($event.dataTransfer.files)\">\n\t<ng-content></ng-content>\n</div>\n<input\n\t*ngIf=\"!dropzone\"\n\ttype=\"file\"\n\t[multiple]=\"multiple\"\n\t[accept]=\"accept\"\n\t(change)=\"readFiles($event.target.files)\"\n/>\n", changeDetection: ChangeDetectionStrategy.OnPush, styles: [""] },] } ]; FileReaderComponent.ctorParameters = () => [ { type: FileRepositoryService } ]; FileReaderComponent.propDecorators = { fileAppend: [{ type: Output }], accept: [{ type: Input }], scope: [{ type: Input }], multiple: [{ type: Input }], dropzone: [{ type: Input }], onDragOver: [{ type: HostListener, args: ['dragover', ['$event'],] }], onDrop: [{ type: HostListener, args: ['drop', ['$event'],] }] }; class ImageCropperService { constructor() { this.name = 'crop'; } apply(ctx, image, sx, sy, sw, sh, dx, dy, dw, dh) { ctx.drawImage(image, sx, sy, sw, sh, dx, dy, dw, dh); } } ImageCropperService.ɵprov = ɵɵdefineInjectable({ factory: function ImageCropperService_Factory() { return new ImageCropperService(); }, token: ImageCropperService, providedIn: "root" }); ImageCropperService.decorators = [ { type: Injectable, args: [{ providedIn: 'root', },] } ]; class AvailableFeatures { constructor(imageCropper) { this.imageCropper = imageCropper; } } AvailableFeatures.ɵprov = ɵɵdefineInjectable({ factory: function AvailableFeatures_Factory() { return new AvailableFeatures(ɵɵinject(ImageCropperService)); }, token: AvailableFeatures, providedIn: "root" }); AvailableFeatures.decorators = [ { type: Injectable, args: [{ providedIn: 'root', },] } ]; AvailableFeatures.ctorParameters = () => [ { type: ImageCropperService } ]; class EditableImageService { constructor(data, editor) { this.editor = editor; this.image = new Image(); this.imageLoaded = new ReplaySubject(); this.canvas = document.createElement('canvas'); this.context = this.canvas.getContext('2d'); const reader = new FileReader(); if (data instanceof File) { this.name = data.name; } fromEvent(reader, 'load').subscribe((e) => { this.image.src = e.target.result; }); fromEvent(this.image, 'load').subscribe((e) => { this.imageLoaded.next(e); }); reader.readAsDataURL(data); } get ready() { return this.imageLoaded.asObservable(); } apply(feature, ...args) { this.ready.subscribe(() => { this.editor[feature].apply(this.context, this.image, ...args); }); return this; } applyCustom(fn) { this.ready.subscribe(() => { fn(this.context, this.image); }); return this; } getDataURL(type = 'image/png', quality = 0.92) { this.dataURL = this.canvas.toDataURL(type, quality); return this.dataURL; } getBlob(type = 'image/png', quality = 0.92) { const base64 = this.canvas.toDataURL(type, quality); const binStr = atob(base64.split(',')[1]); const len = binStr.length; const arr = new Uint8Array(len); for (let i = 0; i < len; i++) { arr[i] = binStr.charCodeAt(i); } this.blob = new Blob([arr], { type }); this.blob.hasDataURL = true; this.blob.toJSON = function () { return { name: this.name, size: this.size, type: this.type, dataURL: base64, }; }; this.blob.toString = this.blob.toJSON; return this.blob; } } class ImageEditorService { constructor(availableFeatures) { this.features = {}; for (const f in availableFeatures) { if (availableFeatures[f]) { this.registerFeature(availableFeatures[f]); } } } registerFeature(feature) { if (this.features[feature.name] && this.features[feature.name] === feature) { throw new Error(`${feature.name} is already registered`); } this.features[feature.name] = feature; } edit(image) { return new EditableImageService(image, this.features); } } ImageEditorService.ɵprov = ɵɵdefineInjectable({ factory: function ImageEditorService_Factory() { return new ImageEditorService(ɵɵinject(AvailableFeatures)); }, token: ImageEditorService, providedIn: "root" }); ImageEditorService.decorators = [ { type: Injectable, args: [{ providedIn: 'root', },] } ]; ImageEditorService.ctorParameters = () => [ { type: AvailableFeatures } ]; class ImageCropperComponent { constructor(editor, el) { this.editor = editor; this.el = el; this.imagePosition = { x: 0, y: 0 }; this.originalImageDimensions = { width: 0, height: 0 }; this.imageDimensions = { width: 0, height: 0 }; this.zoomConfig = { value: 1, max: 4 }; this.configChange = new EventEmitter(); } set image(value) { this.initImageCrop(value); } set zoom(value) { this.editable.ready.subscribe(() => { this.setZoom(value); }); } get zoom() { return this.zoomConfig.value; } set top(value) { this.editable.ready.subscribe(() => { this.imagePosition.y = 0; this.moveDelta = { y: 0, x: this.imagePosition.x }; this.move({ clientY: parseInt(value, 10), clientX: this.imagePosition.x, }); delete this.moveDelta; }); } get top() { return this.imagePosition.y; } set left(value) { this.editable.ready.subscribe(() => { this.imagePosition.x = 0; this.moveDelta = { x: 0, y: this.imagePosition.y }; this.move({ clientX: parseInt(value, 10), clientY: this.imagePosition.y, }); delete this.moveDelta; }); } get left() { return this.imagePosition.x; } set maxZoom(value) { this.zoomConfig.max = parseFloat(value); } get maxZoom() { return this.zoomConfig.max; } triggerMove() { window.addEventListener('mousemove', this.eventListeners.mousemove, false); window.addEventListener('mouseup', this.eventListeners.mouseup, false); return false; } initImageCrop(image) { this.editable = this.editor.edit(image); this.editable.ready.subscribe(() => { const previewDimensions = this.getComputedDimensions(this.el.nativeElement); const fittingDimensions = this.fitArea({ width: this.cropWidth, height: this.cropHeight }, { width: previewDimensions.width, height: previewDimensions.height }); const { vborder, hborder } = this.addTransparentBorder(previewDimensions, fittingDimensions); this.borders = { vborder, hborder }; const fillingDimensions = this.fillArea({ width: this.editable.image.width, height: this.editable.image.height, }, { width: fittingDimensions.width, height: fittingDimensions.height }); this.editable.canvas.width = this.cropWidth; this.editable.canvas.height = this.cropHeight; this.previewImage.nativeElement.src = this.editable.image.src; const img = this.previewImage.nativeElement; img.style.left = hborder + (fittingDimensions.width - fillingDimensions.width) / 2 + 'px'; img.style.top = vborder + (fittingDimensions.height - fillingDimensions.height) / 2 + 'px'; img.style.width = fillingDimensions.width + 'px'; img.style.height = fillingDimensions.height + 'px'; this.originalImageDimensions = fillingDimensions; this.imageDimensions.width = fillingDimensions.width; this.imageDimensions.height = fillingDimensions.height; this.cropDimensions = fittingDimensions; this.imagePosition = { x: hborder + (fittingDimensions.width - fillingDimensions.width) / 2, y: vborder + (fittingDimensions.height - fillingDimensions.height) / 2, }; }); this.eventListeners = { mousemove: this.move.bind(this), mouseup: this.stopMove.bind(this), }; } move(e) { if (!this.moveDelta) { this.moveDelta = { x: e.clientX, y: e.clientY }; } this.imagePosition.x += e.clientX - this.moveDelta.x; this.imagePosition.y += e.clientY - this.moveDelta.y; this.moveDelta.x = e.clientX; this.moveDelta.y = e.clientY; const correctedPosition = this.getCorrectedPosition(this.imagePosition); this.previewImage.nativeElement.style.left = correctedPosition.x + 'px'; this.imagePosition.x = correctedPosition.x; this.previewImage.nativeElement.style.top = correctedPosition.y + 'px'; this.imagePosition.y = correctedPosition.y; return false; } setZoom(value) { let parsedValue = parseFloat(value); if (parsedValue <= 0) { parsedValue = 1; } if (parsedValue > this.zoomConfig.max) { parsedValue = this.zoomConfig.max; } this.zoomConfig.value = parsedValue; const newDimensions = { width: this.originalImageDimensions.width * this.zoomConfig.value, height: this.originalImageDimensions.height * this.zoomConfig.value, }; const hdiff = newDimensions.width - this.imageDimensions.width; const vdiff = newDimensions.height - this.imageDimensions.height; this.imageDimensions.width = newDimensions.width; this.imageDimensions.height = newDimensions.height; const img = this.previewImage.nativeElement; img.style.width = newDimensions.width + 'px'; img.style.height = newDimensions.height + 'px'; const correctedPosition = this.getCorrectedPosition({ x: this.imagePosition.x - hdiff / 2, y: this.imagePosition.y - vdiff / 2, }); img.style.left = correctedPosition.x + 'px'; img.style.top = correctedPosition.y + 'px'; this.imagePosition.x = correctedPosition.x; this.imagePosition.y = correctedPosition.y; img.className = 'zooming'; let finishTransition; finishTransition = () => { img.className = ''; img.removeEventListener('transitionend', finishTransition); this.configChange.emit({ left: this.imagePosition.x, top: this.imagePosition.y, zoom: this.zoom, }); }; img.addEventListener('transitionend', finishTransition); } getBlob() { this.crop(); return this.editable.getBlob(); } getDataURL() { this.crop(); return this.editable.getDataURL(); } crop() { const proportions = this.imageDimensions.width / this.editable.image.width; const sx = (this.borders.hborder - this.imagePosition.x) / proportions; const sy = (this.borders.vborder - this.imagePosition.y) / proportions; const sw = this.cropDimensions.width / proportions; const sh = sw / (this.cropWidth / this.cropHeight); this.editable.apply('crop', sx, sy, sw, sh, 0, 0, this.cropWidth, this.cropHeight); } getCorrectedPosition(pos) { const corrected = { x: 0, y: 0 }; const hdiff = this.cropDimensions.width - this.imageDimensions.width; const vdiff = this.cropDimensions.height - this.imageDimensions.height; if (pos.x < this.borders.hborder) { if (pos.x < this.borders.hborder + hdiff) { corrected.x = this.borders.hborder + hdiff; } else { corrected.x = pos.x; } } else { corrected.x = this.borders.hborder; } if (pos.y < this.borders.vborder) { if (pos.y < this.borders.vborder + vdiff) { corrected.y = this.borders.vborder + vdiff; } else { corrected.y = pos.y; } } else { corrected.y = this.borders.vborder; } return corrected; } stopMove() { delete this.moveDelta; window.removeEventListener('mousemove', this.eventListeners.mousemove); window.removeEventListener('mouseup', this.eventListeners.mouseup); this.configChange.emit({ left: this.imagePosition.x, top: this.imagePosition.y, zoom: this.zoom, }); return false; } fitArea(object, area) { const ph = object.height / area.height; const pw = object.width / area.width; const scale = ph > pw ? ph : pw; return { width: object.width / scale, height: object.height / scale }; } fillArea(object, area) { const ph = object.height / area.height; const pw = object.width / area.width; const scale = ph > pw ? pw : ph; return { width: object.width / scale, height: object.height / scale }; } addTransparentBorder(previewDimensions, fittingDimensions) { const vborder = (previewDimensions.height - fittingDimensions.height) / 2; const hborder = (previewDimensions.width - fittingDimensions.width) / 2; const borderElement = _(this.el.nativeElement.childNodes).find((c) => !!c.className && c.className.indexOf('border') >= 0); const cs = window.getComputedStyle(borderElement); if (borderElement) { borderElement.style.borderBottomWidth = vborder + 'px'; borderElement.style.borderTopWidth = vborder + 'px'; borderElement.style.borderLeftWidth = hborder + 'px'; borderElement.style.borderRightWidth = hborder + 'px'; } return { vborder, hborder }; } getComputedDimensions(element) { const cs = window.getComputedStyle(element); return { width: parseInt(cs.width, 10), height: parseInt(cs.height, 10), }; } } ImageCropperComponent.decorators = [ { type: Component, args: [{ selector: 'mc-image-cropper', template: "<img #previewImage />\n<div class=\"border\"></div>\n", changeDetection: ChangeDetectionStrategy.OnPush, styles: [".border{box-sizing:border-box;height:100%;left:0;position:absolute;top:0;width:100%}:host{cursor:move;display:block;overflow:hidden;position:relative}img{position:absolute}img.zooming{transition:width .3s,height .3s,top .3s,left .3s}/deep/ .border{border:0 solid rgba(0,0,0,.3)}"] },] } ]; ImageCropperComponent.ctorParameters = () => [ { type: ImageEditorService }, { type: ElementRef } ]; ImageCropperComponent.propDecorators = { configChange: [{ type: Output }], image: [{ type: Input, args: ['src',] }], cropWidth: [{ type: Input }], cropHeight: [{ type: Input }], zoom: [{ type: Input }], top: [{ type: Input }], left: [{ type: Input }], maxZoom: [{ type: Input }], previewImage: [{ type: ViewChild, args: ['previewImage',] }], triggerMove: [{ type: HostListener, args: ['mousedown',] }] }; class ImageEditorModule { } ImageEditorModule.decorators = [ { type: NgModule, args: [{ imports: [CommonModule], declarations: [FileReaderComponent, ImageCropperComponent], exports: [FileReaderComponent, ImageCropperComponent], },] } ]; /* * Public API Surface of image-editor */ /** * Generated bundle index. Do not edit. */ export { FileReaderComponent, FileRepositoryService, ImageCropperComponent, ImageEditorModule, ImageEditorService, AvailableFeatures as ɵa, ImageCropperService as ɵb }; //# sourceMappingURL=mc-image-editor.js.map