mc-image-editor
Version:
An image editor library for magic-cut app (http://www.magic-cut.in/)
498 lines (486 loc) • 19 kB
JavaScript
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