ngx-image-drawing
Version:
Angular module to draw on images
549 lines (543 loc) • 23.7 kB
JavaScript
import { __decorate, __metadata } from 'tslib';
import { CommonModule } from '@angular/common';
import { EventEmitter, Input, TemplateRef, Output, Component, NgModule } from '@angular/core';
import { fabric } from 'fabric';
import { Observable, of } from 'rxjs';
import { switchMap } from 'rxjs/operators';
const I18nFr = {
saveBtn: 'Enregistrer',
cancelBtn: 'Annuler',
loadImage: 'Charger une image depuis votre PC',
loadImageUrl: 'Charger une image depuis une URL',
loading: 'Chargement',
loadError: 'Erreur de chargement %@',
removeImage: 'Supprimer l\'image',
sizes: {
small: 'Petit',
medium: 'Moyen',
large: 'Gros'
},
undo: 'Annuler',
redo: 'Répter',
clear: 'Effacer',
colors: {
black: 'Noir',
white: 'Blanc',
yellow: 'Jaune',
red: 'Rouge',
green: 'Vert',
blue: 'Blue',
purple: 'Violet'
},
tools: {
brush: 'Pinceau'
}
};
const I18nEn = {
saveBtn: 'Save',
cancelBtn: 'Cancel',
loadImage: 'Load image',
loadImageUrl: 'Load image from URL',
loading: 'Loading',
loadError: 'Error loading %@',
removeImage: 'Remove image',
sizes: {
small: 'Small',
medium: 'Medium',
large: 'Large'
},
undo: 'Undo',
redo: 'Redo',
clear: 'Clear',
colors: {
black: 'Black',
white: 'White',
yellow: 'Yellow',
red: 'Red',
green: 'Green',
blue: 'Blue',
purple: 'Purple',
},
tools: {
brush: 'Brush'
}
};
const i18nLanguages = {
fr: I18nFr,
en: I18nEn
};
let ImageDrawingComponent = class ImageDrawingComponent {
constructor() {
this.forceSizeCanvas = true;
this.forceSizeExport = false;
this.enableRemoveImage = false;
this.enableLoadAnotherImage = false;
this.enableTooltip = true;
this.showCancelButton = true;
this.locale = 'en';
/* @deprecated Use i18n.saveBtn */
this.saveBtnText = 'Save';
/* @deprecated Use i18n.cancelBtn */
this.cancelBtnText = 'Cancel';
/* @deprecated Use i18n.loading */
this.loadingText = 'Loading…';
/* @deprecated Use i18n.loadError */
this.errorText = 'Error loading %@';
this.outputMimeType = 'image/jpeg';
this.outputQuality = 0.8;
this.borderCss = 'none';
this.drawingSizes = {
small: 5,
medium: 10,
large: 25,
};
this.colors = {
black: '#000',
white: '#fff',
yellow: '#ffeb3b',
red: '#f44336',
blue: '#2196f3',
green: '#4caf50',
purple: '#7a08af',
};
this.save = new EventEmitter();
this.cancel = new EventEmitter();
this.currentTool = 'brush';
this.currentSize = 'medium';
this.currentColor = 'black';
this.i18n = I18nEn;
this.canUndo = false;
this.canRedo = false;
this.isLoading = false;
this.hasError = false;
this.errorMessage = '';
this.stack = [];
this.colorsName = [];
this.drawingSizesName = [];
}
ngOnInit() {
this.colorsName = Object.keys(this.colors);
this.drawingSizesName = Object.keys(this.drawingSizes);
this.canvas = new fabric.Canvas('canvas', {
hoverCursor: 'pointer',
isDrawingMode: true,
});
this.canvas.backgroundColor = 'white';
if (this.src) {
this.importPhotoFromSrc(this.src);
}
else {
if (!this.width || !this.height) {
throw new Error('No width or hight given !');
}
this.canvas.setWidth(this.width);
this.canvas.setHeight(this.height);
}
this.canvas.on('path:created', () => {
this.stack = [];
this.setUndoRedo();
});
this.selectTool(this.currentTool);
this.selectColor(this.currentColor);
this.selectDrawingSize(this.currentSize);
if (this.locale && i18nLanguages[this.locale.toLowerCase()]) {
this.i18n = i18nLanguages[this.locale.toLowerCase()];
}
// FIXME remove after a while because properties are now deprecated
if (this.saveBtnText) {
this.i18n.saveBtn = this.saveBtnText;
}
if (this.cancelBtnText) {
this.i18n.cancelBtn = this.cancelBtnText;
}
if (this.loadingText) {
this.i18n.loading = this.loadingText;
}
if (this.errorText) {
this.i18n.loadError = this.errorText;
}
}
// Tools
selectTool(tool) {
this.currentTool = tool;
}
selectDrawingSize(size) {
this.currentSize = size;
if (this.canvas) {
this.canvas.freeDrawingBrush.width = this.drawingSizes[size];
}
}
selectColor(color) {
this.currentColor = color;
if (this.canvas) {
this.canvas.freeDrawingBrush.color = this.colors[color];
}
}
// Actions
undo() {
if (this.canUndo) {
const lastId = this.canvas.getObjects().length - 1;
const lastObj = this.canvas.getObjects()[lastId];
this.stack.push(lastObj);
this.canvas.remove(lastObj);
this.setUndoRedo();
}
}
redo() {
if (this.canRedo) {
const firstInStack = this.stack.splice(-1, 1)[0];
if (firstInStack) {
this.canvas.insertAt(firstInStack, this.canvas.getObjects().length - 1, false);
}
this.setUndoRedo();
}
}
clearCanvas() {
if (this.canvas) {
this.canvas.remove(...this.canvas.getObjects());
this.setUndoRedo();
}
}
saveImage() {
if (!this.forceSizeExport || (this.forceSizeExport && this.width && this.height)) {
const canvasScaledElement = document.createElement('canvas');
const canvasScaled = new fabric.Canvas(canvasScaledElement);
canvasScaled.backgroundColor = 'white';
new Observable(observer => {
if (this.imageUsed) {
if (this.forceSizeExport) {
canvasScaled.setWidth(this.width);
canvasScaled.setHeight(this.height);
this.imageUsed.cloneAsImage(imageCloned => {
imageCloned.scaleToWidth(this.width, false);
imageCloned.scaleToHeight(this.height, false);
canvasScaled.setBackgroundImage(imageCloned, (img) => {
if (!img) {
observer.error(new Error('Impossible to draw the image on the temporary canvas'));
}
observer.next(canvasScaled);
observer.complete();
}, {
crossOrigin: 'anonymous',
originX: 'left',
originY: 'top'
});
});
}
else {
canvasScaled.setBackgroundImage(this.imageUsed, (img) => {
if (!img) {
observer.error(new Error('Impossible to draw the image on the temporary canvas'));
}
canvasScaled.setWidth(img.width);
canvasScaled.setHeight(img.height);
observer.next(canvasScaled);
observer.complete();
}, {
crossOrigin: 'anonymous',
originX: 'left',
originY: 'top'
});
}
}
else {
canvasScaled.setWidth(this.width);
canvasScaled.setHeight(this.height);
}
}).pipe(switchMap(() => {
let process = of(canvasScaled);
if (this.canvas.getObjects().length > 0) {
const ratioX = canvasScaled.getWidth() / this.canvas.getWidth();
const ratioY = canvasScaled.getHeight() / this.canvas.getHeight();
this.canvas.getObjects().forEach((originalObject, i) => {
process = process.pipe(switchMap(() => {
return new Observable(observerObject => {
originalObject.clone((clonedObject) => {
clonedObject.set('left', originalObject.left * ratioX);
clonedObject.set('top', originalObject.top * ratioY);
clonedObject.scaleToWidth(originalObject.width * ratioX);
clonedObject.scaleToHeight(originalObject.height * ratioY);
canvasScaled.insertAt(clonedObject, i, false);
canvasScaled.renderAll();
observerObject.next(canvasScaled);
observerObject.complete();
});
});
}));
});
}
return process;
})).subscribe(() => {
canvasScaled.renderAll();
canvasScaled.getElement().toBlob((data) => {
this.save.emit(data);
}, this.outputMimeType, this.outputQuality);
});
}
else {
this.canvas.getElement().toBlob((data) => {
this.save.emit(data);
}, this.outputMimeType, this.outputQuality);
}
}
cancelAction() {
this.cancel.emit();
}
getTextTranslated(name) {
let strOk = name.split('.').reduce((o, i) => o[i], this.i18n);
if (this.i18nUser) {
try {
const str = name.split('.').reduce((o, i) => o[i], this.i18nUser);
if (str) {
strOk = str;
}
}
catch (e) {
// if we pass here, ignored
}
}
if (!strOk) {
console.error(name + ' translation not found !');
}
return strOk;
}
getTooltipTranslated(name) {
if (this.enableTooltip) {
return this.getTextTranslated(name);
}
else {
return '';
}
}
setUndoRedo() {
this.canUndo = this.canvas.getObjects().length > 0;
this.canRedo = this.stack.length > 0;
this.canvas.renderAll();
}
importPhotoFromFile(event) {
if (event.target.files && event.target.files.length > 0) {
const file = event.target.files[0];
if (file.type.match('image.*')) {
this.importPhotoFromBlob(file);
}
else {
throw new Error('Not an image !');
}
}
}
removeImage() {
if (this.imageUsed) {
this.imageUsed.dispose();
this.imageUsed = null;
}
this.canvas.backgroundImage = null;
if (this.width && this.height) {
this.canvas.setWidth(this.width);
this.canvas.setHeight(this.height);
}
this.canvas.renderAll();
}
get hasImage() {
return !!this.canvas.backgroundImage;
}
importPhotoFromSrc(src) {
this.isLoading = true;
let isFirstTry = true;
const imgEl = new Image();
imgEl.setAttribute('crossOrigin', 'anonymous');
imgEl.src = src;
imgEl.onerror = () => {
// Retry with cors proxy
if (isFirstTry) {
imgEl.src = 'https://cors-anywhere.herokuapp.com/' + this.src;
isFirstTry = false;
}
else {
this.isLoading = false;
this.hasError = true;
this.errorMessage = this.getTextTranslated('loadError').replace('%@', this.src);
}
};
imgEl.onload = () => {
this.isLoading = false;
this.imageUsed = new fabric.Image(imgEl);
this.imageUsed.cloneAsImage(image => {
let width = imgEl.width;
let height = imgEl.height;
if (this.width) {
width = this.width;
}
if (this.height) {
height = this.height;
}
image.scaleToWidth(width, false);
image.scaleToHeight(height, false);
this.canvas.setBackgroundImage(image, ((img) => {
if (img) {
if (this.forceSizeCanvas) {
this.canvas.setWidth(width);
this.canvas.setHeight(height);
}
else {
this.canvas.setWidth(image.getScaledWidth());
this.canvas.setHeight(image.getScaledHeight());
}
}
}), {
crossOrigin: 'anonymous',
originX: 'left',
originY: 'top'
});
});
};
}
importPhotoFromBlob(file) {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = (evtReader) => {
if (evtReader.target.readyState == FileReader.DONE) {
this.importPhotoFromSrc(evtReader.target.result);
}
};
}
importPhotoFromUrl() {
const url = prompt(this.getTooltipTranslated('loadImageUrl'));
if (url) {
this.importPhotoFromSrc(url);
}
}
ngOnChanges(changes) {
if (changes.src && !changes.src.firstChange && changes.src.currentValue) {
if (typeof changes.src.currentValue === 'string') {
this.importPhotoFromSrc(changes.src.currentValue);
}
else if (changes.src.currentValue instanceof Blob) {
this.importPhotoFromBlob(changes.src.currentValue);
}
}
}
};
__decorate([
Input(),
__metadata("design:type", String)
], ImageDrawingComponent.prototype, "src", void 0);
__decorate([
Input(),
__metadata("design:type", Number)
], ImageDrawingComponent.prototype, "width", void 0);
__decorate([
Input(),
__metadata("design:type", Number)
], ImageDrawingComponent.prototype, "height", void 0);
__decorate([
Input(),
__metadata("design:type", Object)
], ImageDrawingComponent.prototype, "forceSizeCanvas", void 0);
__decorate([
Input(),
__metadata("design:type", Object)
], ImageDrawingComponent.prototype, "forceSizeExport", void 0);
__decorate([
Input(),
__metadata("design:type", Object)
], ImageDrawingComponent.prototype, "enableRemoveImage", void 0);
__decorate([
Input(),
__metadata("design:type", Object)
], ImageDrawingComponent.prototype, "enableLoadAnotherImage", void 0);
__decorate([
Input(),
__metadata("design:type", Object)
], ImageDrawingComponent.prototype, "enableTooltip", void 0);
__decorate([
Input(),
__metadata("design:type", Object)
], ImageDrawingComponent.prototype, "showCancelButton", void 0);
__decorate([
Input('i18n'),
__metadata("design:type", Object)
], ImageDrawingComponent.prototype, "i18nUser", void 0);
__decorate([
Input(),
__metadata("design:type", String)
], ImageDrawingComponent.prototype, "locale", void 0);
__decorate([
Input(),
__metadata("design:type", Object)
], ImageDrawingComponent.prototype, "saveBtnText", void 0);
__decorate([
Input(),
__metadata("design:type", Object)
], ImageDrawingComponent.prototype, "cancelBtnText", void 0);
__decorate([
Input(),
__metadata("design:type", Object)
], ImageDrawingComponent.prototype, "loadingText", void 0);
__decorate([
Input(),
__metadata("design:type", Object)
], ImageDrawingComponent.prototype, "errorText", void 0);
__decorate([
Input(),
__metadata("design:type", TemplateRef)
], ImageDrawingComponent.prototype, "loadingTemplate", void 0);
__decorate([
Input(),
__metadata("design:type", TemplateRef)
], ImageDrawingComponent.prototype, "errorTemplate", void 0);
__decorate([
Input(),
__metadata("design:type", Object)
], ImageDrawingComponent.prototype, "outputMimeType", void 0);
__decorate([
Input(),
__metadata("design:type", Object)
], ImageDrawingComponent.prototype, "outputQuality", void 0);
__decorate([
Input(),
__metadata("design:type", String)
], ImageDrawingComponent.prototype, "borderCss", void 0);
__decorate([
Input(),
__metadata("design:type", Object)
], ImageDrawingComponent.prototype, "drawingSizes", void 0);
__decorate([
Input(),
__metadata("design:type", Object)
], ImageDrawingComponent.prototype, "colors", void 0);
__decorate([
Output(),
__metadata("design:type", EventEmitter)
], ImageDrawingComponent.prototype, "save", void 0);
__decorate([
Output(),
__metadata("design:type", EventEmitter)
], ImageDrawingComponent.prototype, "cancel", void 0);
ImageDrawingComponent = __decorate([
Component({
selector: 'image-drawing',
template: "<link href=\"https://fonts.googleapis.com/icon?family=Material+Icons\" rel=\"stylesheet\" />\n\n<div class=\"loading\" *ngIf=\"isLoading\">\n <ng-container *ngTemplateOutlet=\"loadingTemplate ? loadingTemplate : defaultLoading\"></ng-container>\n</div>\n<div class=\"error\" *ngIf=\"hasError\">\n <ng-container *ngTemplateOutlet=\"errorTemplate ? errorTemplate : defaultError\"></ng-container>\n</div>\n\n<ng-template #defaultLoading><p>{{ getTextTranslated('loading') }}</p></ng-template>\n<ng-template #defaultError> <p>{{ errorMessage }}</p> </ng-template>\n\n<div [ngStyle]=\"{ border: borderCss }\">\n <canvas id=\"canvas\"></canvas>\n</div>\n<div class=\"toolbar\" *ngIf=\"!isLoading\">\n <div class=\"tools\">\n <div class=\"row\">\n <i class=\"material-icons btn\" [class.selected]=\"currentTool == 'brush'\" (click)=\"selectTool('brush')\"\n [title]=\"getTooltipTranslated('tools.brush')\">brush</i>\n <span *ngFor=\"let drawingSizeName of drawingSizesName\" class=\"size btn\"\n [style.width.px]=\"drawingSizes[drawingSizeName] * 0.8 + 8\"\n [style.height.px]=\"drawingSizes[drawingSizeName] * 0.8 + 8\"\n [style.borderRadius.px]=\"drawingSizes[drawingSizeName] * 0.4 + 4\"\n [class.selected]=\"currentSize == drawingSizeName\"\n [title]=\"getTooltipTranslated('sizes.' + drawingSizeName)\"\n (click)=\"selectDrawingSize(drawingSizeName)\">\n </span>\n\n <input style=\"display: none\" type=\"file\" #fileInput (change)=\"importPhotoFromFile($event)\"\n accept=\"image/*\"/>\n <i class=\"material-icons btn\" *ngIf=\"enableLoadAnotherImage && !hasImage\" (click)=\"fileInput.click();\"\n [title]=\"getTooltipTranslated('loadImage')\">attach_file</i>\n <i class=\"material-icons btn\" *ngIf=\"enableLoadAnotherImage && !hasImage\" (click)=\"importPhotoFromUrl()\"\n [title]=\"getTooltipTranslated('loadImageUrl')\">insert_drive_file</i>\n <i class=\"material-icons btn\" *ngIf=\"enableRemoveImage && hasImage\" (click)=\"removeImage()\"\n [title]=\"getTooltipTranslated('removeImage')\">clear</i>\n\n <i class=\"material-icons btn\" [class.disabled]=\"!canUndo\" (click)=\"undo()\"\n [title]=\"getTooltipTranslated('undo')\">undo</i>\n <i class=\"material-icons btn\" [class.disabled]=\"!canRedo\" (click)=\"redo()\"\n [title]=\"getTooltipTranslated('redo')\">redo</i>\n <i class=\"material-icons btn\" (click)=\"clearCanvas()\" [title]=\"getTooltipTranslated('clear')\">delete</i>\n </div>\n <div class=\"row\">\n <div *ngFor=\"let colorName of colorsName\" [class.selected]=\"currentColor == colorName\" class=\"color\"\n [ngClass]=\"colorName\"\n [style.background]=\"colors[colorName]\" [title]=\"getTooltipTranslated('colors.' + colorName)\"\n (click)=\"selectColor(colorName)\">\n </div>\n </div>\n </div>\n <div class=\"buttons\">\n <a href=\"#\" class=\"button btn-primary\"\n (click)=\"saveImage(); $event.preventDefault()\">{{ getTextTranslated('saveBtn') }}</a>\n <a href=\"#\" class=\"button btn-light\" *ngIf=\"showCancelButton\"\n (click)=\"cancelAction(); $event.preventDefault()\">{{ getTextTranslated('cancelBtn') }}</a>\n </div>\n <!-- Any additional toolbar buttons to be projected by the consuming app -->\n <ng-content></ng-content>\n</div>\n",
styles: [":host{display:flex;flex-direction:column;align-items:center}:host .toolbar{display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap}:host .tools{display:inline-flex;flex-direction:column;padding:20px;margin:10px;background:#fff;border-radius:6px;box-shadow:0 3px 10px rgba(0,0,0,.4)}:host .row{display:flex;width:300px;justify-content:space-around;align-items:center}:host .row:first-child{margin-bottom:10px}:host .btn{cursor:pointer}:host .btn.selected{color:#bdbdbd}:host .btn.disabled{cursor:initial;color:#bdbdbd}:host .size{background-color:#000}:host .size.selected{background-color:#bdbdbd}:host .color{width:28px;height:28px;border-radius:14px;cursor:pointer;display:flex;align-items:center;justify-content:center}:host .color.selected::after{content:\"\";width:10px;height:10px;background:#000;display:flex;border-radius:5px}:host .color.black{background-color:#000}:host .color.black.selected::after{background:#fff}:host .color.white{border:1px solid #a7a7a7}:host .buttons{margin:10px;display:flex;flex-direction:column}:host .button{cursor:pointer;outline:0;border:none;white-space:nowrap;text-decoration:none;vertical-align:baseline;text-align:center;min-width:64px;line-height:36px;padding:3px 16px;border-radius:4px;overflow:visible;transition:background .4s cubic-bezier(.25,.8,.25,1),box-shadow 280ms cubic-bezier(.4,0,.2,1);margin:10px}:host .button:hover{text-decoration:none!important}:host .button.btn-primary{background-color:#ef5f27;color:#fff}:host .button.btn-primary:hover{background-color:rgba(239,95,39,.8)}:host .button.btn-light{color:#ef5f27}:host .button.btn-light:hover{background-color:rgba(239,95,39,.1)}"]
}),
__metadata("design:paramtypes", [])
], ImageDrawingComponent);
let ImageDrawingModule = class ImageDrawingModule {
};
ImageDrawingModule = __decorate([
NgModule({
declarations: [
ImageDrawingComponent
],
exports: [
ImageDrawingComponent,
],
imports: [
CommonModule
]
})
], ImageDrawingModule);
/**
* Generated bundle index. Do not edit.
*/
export { ImageDrawingComponent, ImageDrawingModule };
//# sourceMappingURL=ngx-image-drawing.js.map