UNPKG

@autonomdev/ngx-document-scanner

Version:

Angular 2+ component for cropping and enhancing images of documents

1,524 lines (1,519 loc) 74.2 kB
import { Injectable, ɵɵdefineInjectable, Component, Input, EventEmitter, Inject, Output, ViewChild, ElementRef, NgModule } from '@angular/core'; import { BehaviorSubject } from 'rxjs'; import { MatBottomSheetRef, MAT_BOTTOM_SHEET_DATA, MatBottomSheet, MatBottomSheetModule } from '@angular/material/bottom-sheet'; import { __awaiter } from 'tslib'; import { NgxOpenCVService, OpenCvConfigToken, NgxOpenCVModule } from 'ngx-opencv'; import { FlexLayoutModule } from '@angular/flex-layout'; import { AngularDraggableModule } from 'angular2-draggable'; import { CommonModule } from '@angular/common'; import { MatButtonModule } from '@angular/material/button'; import { MatIconModule } from '@angular/material/icon'; import { MatListModule } from '@angular/material/list'; /** * @fileoverview added by tsickle * Generated from: lib/services/limits.service.ts * @suppress {checkTypes,constantProperty,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc */ class LimitsService { constructor() { this.limitDirections = ['left', 'right', 'top', 'bottom']; /** * stores the crop limits limits */ this._limits = { top: 0, bottom: 0, right: 0, left: 0 }; /** * stores the array of the draggable points displayed on the crop area */ this._points = []; // *********** // // Observables // // *********** // this.positions = new BehaviorSubject(Array.from(this._points)); this.repositionEvent = new BehaviorSubject([]); this.limits = new BehaviorSubject(this._limits); this.paneDimensions = new BehaviorSubject({ width: 0, height: 0 }); } /** * set privew pane dimensions * @param {?} dimensions * @return {?} */ setPaneDimensions(dimensions) { return new Promise((/** * @param {?} resolve * @param {?} reject * @return {?} */ (resolve, reject) => { this._paneDimensions = dimensions; this.paneDimensions.next(dimensions); resolve(); })); } /** * repositions points externally * @param {?} positions * @return {?} */ repositionPoints(positions) { this._points = positions; positions.forEach((/** * @param {?} position * @return {?} */ position => { this.positionChange(position); })); this.repositionEvent.next(positions); } /** * updates limits and point positions and calls next on the observables * @param {?} positionChangeData - position change event data * @return {?} */ positionChange(positionChangeData) { // update positions according to current position change this.updatePosition(positionChangeData); // for each direction: // 1. filter the _points that have a role as the direction's limit // 2. for top and left find max x | y values, and min for right and bottom this.limitDirections.forEach((/** * @param {?} direction * @return {?} */ direction => { /** @type {?} */ const relevantPoints = this._points.filter((/** * @param {?} point * @return {?} */ point => { return point.roles.includes(direction); })) .map((/** * @param {?} point * @return {?} */ (point) => { return point[this.getDirectionAxis(direction)]; })); /** @type {?} */ let limit; if (direction === 'top' || direction === 'left') { limit = Math.max(...relevantPoints); } if (direction === 'right' || direction === 'bottom') { limit = Math.min(...relevantPoints); } this._limits[direction] = limit; })); this.limits.next(this._limits); this.positions.next(Array.from(this._points)); } /** * updates the position of the point * @param {?} positionChange - position change event data * @return {?} */ updatePosition(positionChange) { // finds the current position of the point by it's roles, than splices it for the new position or pushes it if it's not yet in the array /** @type {?} */ const index = this._points.findIndex((/** * @param {?} point * @return {?} */ point => { return this.compareArray(positionChange.roles, point.roles); })); if (index === -1) { this._points.push(positionChange); } else { this._points.splice(index, 1, positionChange); } } /** * check if a position change event exceeds the limits * @param {?} positionChange - position change event data * @return {?} LimitException0 */ exceedsLimit(positionChange) { /** @type {?} */ const pointLimits = this.limitDirections.filter((/** * @param {?} direction * @return {?} */ direction => { return !positionChange.roles.includes(direction); })); /** @type {?} */ const limitException = { exceeds: false, resetCoefficients: { x: 0, y: 0 }, resetCoordinates: { x: positionChange.x, y: positionChange.y } }; // limit directions are the opposite sides of the point's roles pointLimits.forEach((/** * @param {?} direction * @return {?} */ direction => { /** @type {?} */ const directionAxis = this.getDirectionAxis(direction); if (direction === 'top' || direction === 'left') { if (positionChange[directionAxis] < this._limits[direction]) { limitException.resetCoefficients[directionAxis] = 1; limitException.resetCoordinates[directionAxis] = this._limits[direction]; } } else if (direction === 'right' || direction === 'bottom') { if (positionChange[directionAxis] > this._limits[direction]) { limitException.resetCoefficients[directionAxis] = -1; limitException.resetCoordinates[directionAxis] = this._limits[direction]; } } })); if (limitException.resetCoefficients.x !== 0 || limitException.resetCoefficients.y !== 0) { limitException.exceeds = true; } return limitException; } /** * rotate crop tool points clockwise * @param {?} resizeRatios - ratio between the new dimensions and the previous * @param {?} initialPreviewDimensions - preview pane dimensions before rotation * @param {?} initialPositions - current positions before rotation * @return {?} */ rotateClockwise(resizeRatios, initialPreviewDimensions, initialPositions) { // convert positions to ratio between position to initial pane dimension initialPositions = initialPositions.map((/** * @param {?} point * @return {?} */ point => { return new PositionChangeData({ x: point.x / initialPreviewDimensions.width, y: point.y / initialPreviewDimensions.height, }, point.roles); })); this.repositionPoints(initialPositions.map((/** * @param {?} point * @return {?} */ point => { return this.rotateCornerClockwise(point); }))); } /** * returns the corner positions after a 90 degrees clockwise rotation * @private * @param {?} corner * @return {?} */ rotateCornerClockwise(corner) { /** @type {?} */ const rotated = { x: this._paneDimensions.width * (1 - corner.y), y: this._paneDimensions.height * corner.x, roles: [] }; // rotates corner according to order /** @type {?} */ const order = [ ['bottom', 'left'], ['top', 'left'], ['top', 'right'], ['bottom', 'right'], ['bottom', 'left'] ]; rotated.roles = order[order.findIndex((/** * @param {?} roles * @return {?} */ roles => { return this.compareArray(roles, corner.roles); })) + 1]; return rotated; } /** * checks if two array contain the same values * @param {?} array1 - array 1 * @param {?} array2 - array 2 * @return {?} boolean */ compareArray(array1, array2) { return array1.every((/** * @param {?} element * @return {?} */ (element) => { return array2.includes(element); })) && array1.length === array2.length; } /** * @private * @param {?} direction * @return {?} */ getDirectionAxis(direction) { return { left: 'x', right: 'x', top: 'y', bottom: 'y' }[direction]; } } LimitsService.decorators = [ { type: Injectable, args: [{ providedIn: 'root' },] } ]; /** @nocollapse */ LimitsService.ctorParameters = () => []; /** @nocollapse */ LimitsService.ɵprov = ɵɵdefineInjectable({ factory: function LimitsService_Factory() { return new LimitsService(); }, token: LimitsService, providedIn: "root" }); if (false) { /** * @type {?} * @private */ LimitsService.prototype.limitDirections; /** * stores the crop limits limits * @type {?} * @private */ LimitsService.prototype._limits; /** * stores the array of the draggable points displayed on the crop area * @type {?} * @private */ LimitsService.prototype._points; /** * stores the pane dimensions * @type {?} * @private */ LimitsService.prototype._paneDimensions; /** @type {?} */ LimitsService.prototype.positions; /** @type {?} */ LimitsService.prototype.repositionEvent; /** @type {?} */ LimitsService.prototype.limits; /** @type {?} */ LimitsService.prototype.paneDimensions; } /** * @record */ function PointPositionChange() { } if (false) { /** @type {?} */ PointPositionChange.prototype.x; /** @type {?} */ PointPositionChange.prototype.y; /** @type {?} */ PointPositionChange.prototype.roles; } /** * @record */ function AreaLimits() { } if (false) { /** @type {?} */ AreaLimits.prototype.top; /** @type {?} */ AreaLimits.prototype.bottom; /** @type {?} */ AreaLimits.prototype.right; /** @type {?} */ AreaLimits.prototype.left; } class PositionChangeData { /** * @param {?} position * @param {?} roles */ constructor(position, roles) { this.x = position.x; this.y = position.y; this.roles = roles; } } if (false) { /** @type {?} */ PositionChangeData.prototype.x; /** @type {?} */ PositionChangeData.prototype.y; /** @type {?} */ PositionChangeData.prototype.roles; } /** * @fileoverview added by tsickle * Generated from: lib/components/draggable-point/ngx-draggable-point.component.ts * @suppress {checkTypes,constantProperty,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc */ class NgxDraggablePointComponent { /** * @param {?} limitsService */ constructor(limitsService) { this.limitsService = limitsService; this.width = 10; this.height = 10; this.color = '#3cabe2'; this.shape = 'rect'; this.pointOptions = 'rect'; this.position = { x: 0, y: 0 }; } /** * @return {?} */ ngAfterViewInit() { Object.keys(this.pointOptions).forEach((/** * @param {?} key * @return {?} */ key => { this[key] = this.pointOptions[key]; })); // subscribe to pane dimensions changes this.limitsService.paneDimensions.subscribe((/** * @param {?} dimensions * @return {?} */ dimensions => { if (dimensions.width > 0 && dimensions.width > 0) { this._paneDimensions = { width: dimensions.width, height: dimensions.height }; this.position = this.getInitialPosition(dimensions); this.limitsService.positionChange(new PositionChangeData(this.position, this.limitRoles)); } })); // subscribe to external reposition events this.limitsService.repositionEvent.subscribe((/** * @param {?} positions * @return {?} */ positions => { if (positions.length > 0) { this.externalReposition(positions); } })); } /** * returns a css style object for the point * @return {?} */ pointStyle() { return { width: this.width + 'px', height: this.height + 'px', 'background-color': this.color, 'border-radius': this.shape === 'circle' ? '100%' : 0, position: 'absolute' }; } /** * registers a position change on the limits service, and adjusts position if necessary * @param {?} position - the current position of the point * @return {?} */ positionChange(position) { /** @type {?} */ const positionChangeData = new PositionChangeData(position, this.limitRoles); /** @type {?} */ const limitException = this.limitsService.exceedsLimit(positionChangeData); if (limitException.exceeds) { // if exceeds limits, reposition this.resetPosition = limitException.resetCoordinates; } else { this.limitsService.positionChange(positionChangeData); this._currentPosition = position; } } /** * adjusts the position of the point after a limit exception * @private * @param {?} limitException * @return {?} */ adjustPosition(limitException) { /** @type {?} */ const newPosition = { x: 0, y: 0 }; Object.keys(this.startPosition).forEach((/** * @param {?} axis * @return {?} */ axis => { newPosition[axis] = limitException.resetCoordinates[axis] + limitException.resetCoefficients[axis]; })); this.position = newPosition; this.limitsService.positionChange(new PositionChangeData(this.position, this.limitRoles)); } /** * called on movement end, checks if last position exceeded the limits ad adjusts * @param {?} position * @return {?} */ movementEnd(position) { /** @type {?} */ let positionChangeData = new PositionChangeData(position, this.limitRoles); /** @type {?} */ const limitException = this.limitsService.exceedsLimit(positionChangeData); if (limitException.exceeds) { this.resetPosition = limitException.resetCoordinates; if (limitException.exceeds) { this.adjustPosition(limitException); positionChangeData = new PositionChangeData(this.position, this.limitRoles); this.limitsService.updatePosition(positionChangeData); } } } /** * calculates the initial positions of the point by it's roles * @private * @param {?} dimensions - dimensions of the pane in which the point is located * @return {?} */ getInitialPosition(dimensions) { return { x: this.limitRoles.includes('left') ? 0 : dimensions.width - this.width / 2, y: this.limitRoles.includes('top') ? 0 : dimensions.height - this.height / 2 }; } /** * repositions the point after an external reposition event * @private * @param {?} positions - an array of all points on the pane * @return {?} */ externalReposition(positions) { positions.forEach((/** * @param {?} position * @return {?} */ position => { if (this.limitsService.compareArray(this.limitRoles, position.roles)) { position = this.enforcePaneLimits(position); this.position = { x: position.x, y: position.y }; } })); } /** * returns a new point position if the movement exceeded the pane limit * @private * @param {?} position * @return {?} */ enforcePaneLimits(position) { if (this._paneDimensions.width === 0 || this._paneDimensions.height === 0) { return position; } else { if (position.x > this._paneDimensions.width) { position.x = this._paneDimensions.width; } if (position.x < 0) { position.x = 1; } if (position.y > this._paneDimensions.height) { position.y = this._paneDimensions.height; } if (position.y < 0) { position.y = 1; } } return position; } } NgxDraggablePointComponent.decorators = [ { type: Component, args: [{ selector: 'ngx-draggable-point', template: "<div #point ngDraggable=\"draggable\"\n (movingOffset)=\"positionChange($event)\"\n [ngStyle]=\"pointStyle()\"\n [position]=\"position\"\n [bounds]=\"container\"\n [inBounds]=\"true\"\n (endOffset)=\"movementEnd($event)\"\n style=\"z-index: 1000\">\n</div>\n" }] } ]; /** @nocollapse */ NgxDraggablePointComponent.ctorParameters = () => [ { type: LimitsService } ]; NgxDraggablePointComponent.propDecorators = { width: [{ type: Input }], height: [{ type: Input }], color: [{ type: Input }], shape: [{ type: Input }], pointOptions: [{ type: Input }], limitRoles: [{ type: Input }], startPosition: [{ type: Input }], container: [{ type: Input }], _currentPosition: [{ type: Input }] }; if (false) { /** @type {?} */ NgxDraggablePointComponent.prototype.width; /** @type {?} */ NgxDraggablePointComponent.prototype.height; /** @type {?} */ NgxDraggablePointComponent.prototype.color; /** @type {?} */ NgxDraggablePointComponent.prototype.shape; /** @type {?} */ NgxDraggablePointComponent.prototype.pointOptions; /** @type {?} */ NgxDraggablePointComponent.prototype.limitRoles; /** @type {?} */ NgxDraggablePointComponent.prototype.startPosition; /** @type {?} */ NgxDraggablePointComponent.prototype.container; /** * @type {?} * @private */ NgxDraggablePointComponent.prototype._currentPosition; /** @type {?} */ NgxDraggablePointComponent.prototype.position; /** * @type {?} * @private */ NgxDraggablePointComponent.prototype._paneDimensions; /** @type {?} */ NgxDraggablePointComponent.prototype.resetPosition; /** * @type {?} * @private */ NgxDraggablePointComponent.prototype.limitsService; } /** * @fileoverview added by tsickle * Generated from: lib/components/filter-menu/ngx-filter-menu.component.ts * @suppress {checkTypes,constantProperty,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc */ class NgxFilterMenuComponent { /** * @param {?} bottomSheetRef * @param {?} data */ constructor(bottomSheetRef, data) { this.bottomSheetRef = bottomSheetRef; this.data = data; this.filterOptions = [ { name: 'bw2', icon: 'filter_b_and_w', action: (/** * @param {?} filter * @return {?} */ (filter) => { this.filterSelected.emit(filter); }), text: 'B&W 2' }, { name: 'bw3', icon: 'blur_on', action: (/** * @param {?} filter * @return {?} */ (filter) => { this.filterSelected.emit(filter); }), text: 'B&W 3' }, { name: 'magic_color', icon: 'filter_vintage', action: (/** * @param {?} filter * @return {?} */ (filter) => { this.filterSelected.emit(filter); }), text: 'Magic Color' }, { name: 'default', icon: 'crop_original', action: (/** * @param {?} filter * @return {?} */ (filter) => { this.filterSelected.emit(filter); }), text: 'Original' }, ]; this.filterSelected = new EventEmitter(); } /** * @param {?} optionName * @return {?} */ selectOption(optionName) { this.data.filter = optionName; this.bottomSheetRef.dismiss(); } } NgxFilterMenuComponent.decorators = [ { type: Component, args: [{ selector: 'ngx-filter-menu', template: "<mat-action-list>\n <button mat-list-item *ngFor=\"let option of filterOptions\" (click)=\"selectOption(option.name)\">\n <mat-icon>{{option.icon}}</mat-icon>\n <span fxFlex=\"100\" style=\"text-align: start; margin: 5px\">{{option.text}}</span>\n <span fxFlex=\"100\"></span>\n <mat-icon *ngIf=\"option.name === data.filter\">done</mat-icon>\n </button>\n</mat-action-list>\n" }] } ]; /** @nocollapse */ NgxFilterMenuComponent.ctorParameters = () => [ { type: MatBottomSheetRef }, { type: undefined, decorators: [{ type: Inject, args: [MAT_BOTTOM_SHEET_DATA,] }] } ]; NgxFilterMenuComponent.propDecorators = { filterSelected: [{ type: Output }] }; if (false) { /** @type {?} */ NgxFilterMenuComponent.prototype.filterOptions; /** @type {?} */ NgxFilterMenuComponent.prototype.filterSelected; /** * @type {?} * @private */ NgxFilterMenuComponent.prototype.bottomSheetRef; /** @type {?} */ NgxFilterMenuComponent.prototype.data; } /** * @fileoverview added by tsickle * Generated from: lib/components/shape-outline/ngx-shape-outline.component.ts * @suppress {checkTypes,constantProperty,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc */ class NgxShapeOutlineComponent { /** * @param {?} limitsService */ constructor(limitsService) { this.limitsService = limitsService; this.color = '#3cabe2'; } /** * @return {?} */ ngAfterViewInit() { // init drawing canvas dimensions this.canvas.nativeElement.width = this.dimensions.width; this.canvas.nativeElement.height = this.dimensions.height; this.limitsService.positions.subscribe((/** * @param {?} positions * @return {?} */ positions => { if (positions.length === 4) { this._points = positions; this.sortPoints(); this.clearCanvas(); this.drawShape(); } })); // subscribe to changes in the pane's dimensions this.limitsService.paneDimensions.subscribe((/** * @param {?} dimensions * @return {?} */ dimensions => { this.clearCanvas(); this.canvas.nativeElement.width = dimensions.width; this.canvas.nativeElement.height = dimensions.height; })); // subscribe to reposition events this.limitsService.repositionEvent.subscribe((/** * @param {?} positions * @return {?} */ positions => { if (positions.length === 4) { setTimeout((/** * @return {?} */ () => { this.clearCanvas(); this.sortPoints(); this.drawShape(); }), 10); } })); } /** * clears the shape canvas * @private * @return {?} */ clearCanvas() { /** @type {?} */ const canvas = this.canvas.nativeElement; /** @type {?} */ const ctx = canvas.getContext('2d'); ctx.clearRect(0, 0, this.dimensions.width, this.dimensions.height); } /** * sorts the array of points according to their clockwise alignment * @private * @return {?} */ sortPoints() { /** @type {?} */ const _points = Array.from(this._points); /** @type {?} */ const sortedPoints = []; /** @type {?} */ const sortOrder = { vertical: ['top', 'top', 'bottom', 'bottom'], horizontal: ['left', 'right', 'right', 'left'] }; for (let i = 0; i < 4; i++) { /** @type {?} */ const roles = Array.from([sortOrder.vertical[i], sortOrder.horizontal[i]]); sortedPoints.push(_points.filter((/** * @param {?} point * @return {?} */ (point) => { return this.limitsService.compareArray(point.roles, roles); }))[0]); } this._sortedPoints = sortedPoints; } /** * draws a line between the points according to their order * @private * @return {?} */ drawShape() { /** @type {?} */ const canvas = this.canvas.nativeElement; /** @type {?} */ const ctx = canvas.getContext('2d'); ctx.lineWidth = this.weight; ctx.strokeStyle = this.color; ctx.beginPath(); this._sortedPoints.forEach((/** * @param {?} point * @param {?} index * @return {?} */ (point, index) => { if (index === 0) { ctx.moveTo(point.x, point.y); } if (index !== this._sortedPoints.length - 1) { /** @type {?} */ const nextPoint = this._sortedPoints[index + 1]; ctx.lineTo(nextPoint.x, nextPoint.y); } else { ctx.closePath(); } })); ctx.stroke(); } } NgxShapeOutlineComponent.decorators = [ { type: Component, args: [{ selector: 'ngx-shape-outine', template: "<canvas #outline\n style=\"position: absolute; z-index: 1000\"\n [ngStyle]=\"{width: dimensions.width + 'px', height: dimensions.height + 'px'}\"\n *ngIf=\"dimensions\">\n</canvas>\n" }] } ]; /** @nocollapse */ NgxShapeOutlineComponent.ctorParameters = () => [ { type: LimitsService } ]; NgxShapeOutlineComponent.propDecorators = { color: [{ type: Input }], weight: [{ type: Input }], dimensions: [{ type: Input }], canvas: [{ type: ViewChild, args: ['outline',] }] }; if (false) { /** @type {?} */ NgxShapeOutlineComponent.prototype.color; /** @type {?} */ NgxShapeOutlineComponent.prototype.weight; /** @type {?} */ NgxShapeOutlineComponent.prototype.dimensions; /** @type {?} */ NgxShapeOutlineComponent.prototype.canvas; /** * @type {?} * @private */ NgxShapeOutlineComponent.prototype._points; /** * @type {?} * @private */ NgxShapeOutlineComponent.prototype._sortedPoints; /** * @type {?} * @private */ NgxShapeOutlineComponent.prototype.limitsService; } /** * @fileoverview added by tsickle * Generated from: lib/components/image-editor/ngx-doc-scanner.component.ts * @suppress {checkTypes,constantProperty,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc */ class NgxDocScannerComponent { /** * @param {?} ngxOpenCv * @param {?} limitsService * @param {?} bottomSheet */ constructor(ngxOpenCv, limitsService, bottomSheet) { this.ngxOpenCv = ngxOpenCv; this.limitsService = limitsService; this.bottomSheet = bottomSheet; // ************* // // EDITOR CONFIG // // ************* // /** * an array of action buttons displayed on the editor screen */ this.editorButtons = [ { name: 'exit', action: (/** * @return {?} */ () => { this.exitEditor.emit('canceled'); }), icon: 'arrow_back', type: 'fab', mode: 'crop' }, { name: 'rotate', action: this.rotateImage.bind(this), icon: 'rotate_right', type: 'fab', mode: 'crop' }, { name: 'done_crop', action: (/** * @return {?} */ () => __awaiter(this, void 0, void 0, function* () { this.mode = 'color'; yield this.transform(); yield this.applyFilter(true); })), icon: 'done', type: 'fab', mode: 'crop' }, { name: 'back', action: (/** * @return {?} */ () => { this.mode = 'crop'; this.loadFile(this.originalImage); }), icon: 'arrow_back', type: 'fab', mode: 'color' }, { name: 'filter', action: (/** * @return {?} */ () => { return this.chooseFilters(); }), icon: 'photo_filter', type: 'fab', mode: 'color' }, { name: 'upload', action: this.exportImage.bind(this), icon: 'cloud_upload', type: 'fab', mode: 'color' }, ]; /** * true after the image is loaded and preview is displayed */ this.imageLoaded = false; /** * editor mode */ this.mode = 'crop'; /** * filter selected by the user, returned by the filter selector bottom sheet */ this.selectedFilter = 'default'; /** * image dimensions */ this.imageDimensions = { width: 0, height: 0 }; // ************** // // EVENT EMITTERS // // ************** // /** * optional binding to the exit button of the editor */ this.exitEditor = new EventEmitter(); /** * fires on edit completion */ this.editResult = new EventEmitter(); /** * emits errors, can be linked to an error handler of choice */ this.error = new EventEmitter(); /** * emits the loading status of the cv module. */ this.ready = new EventEmitter(); /** * emits true when processing is done, false when completed */ this.processing = new EventEmitter(); this.screenDimensions = { width: window.innerWidth, height: window.innerHeight }; // subscribe to status of cv module this.ngxOpenCv.cvState.subscribe((/** * @param {?} cvState * @return {?} */ (cvState) => { this.cvState = cvState.state; this.ready.emit(cvState.ready); if (cvState.error) { this.error.emit(new Error('error loading cv')); } else if (cvState.loading) { this.processing.emit(true); } else if (cvState.ready) { this.processing.emit(false); } })); // subscribe to positions of crop tool this.limitsService.positions.subscribe((/** * @param {?} points * @return {?} */ points => { this.points = points; })); } /** * returns an array of buttons according to the editor mode * @return {?} */ get displayedButtons() { return this.editorButtons.filter((/** * @param {?} button * @return {?} */ button => { return button.mode === this.mode; })); } // ****** // // INPUTS // // ****** // /** * set image for editing * @param {?} file - file from form input * @return {?} */ set file(file) { if (file) { setTimeout((/** * @return {?} */ () => { this.processing.emit(true); }), 5); this.imageLoaded = false; this.originalImage = file; this.ngxOpenCv.cvState.subscribe((/** * @param {?} cvState * @return {?} */ (cvState) => __awaiter(this, void 0, void 0, function* () { if (cvState.ready) { // read file to image & canvas yield this.loadFile(file); this.processing.emit(false); } }))); } } /** * @return {?} */ ngOnInit() { // set options from config object this.options = new ImageEditorConfig(this.config); // set export image icon this.editorButtons.forEach((/** * @param {?} button * @return {?} */ button => { if (button.name === 'upload') { button.icon = this.options.exportImageIcon; } })); this.maxPreviewWidth = this.options.maxPreviewWidth; this.editorStyle = this.options.editorStyle; } // ***************************** // // editor action buttons methods // // ***************************** // /** * emits the exitEditor event * @return {?} */ exit() { this.exitEditor.emit('canceled'); } /** * applies the selected filter, and when done emits the resulted image * @private * @return {?} */ exportImage() { return __awaiter(this, void 0, void 0, function* () { yield this.applyFilter(false); if (this.options.maxImageDimensions) { this.resize(this.editedImage) .then((/** * @param {?} resizeResult * @return {?} */ resizeResult => { resizeResult.toBlob((/** * @param {?} blob * @return {?} */ (blob) => { this.editResult.emit(blob); this.processing.emit(false); }), this.originalImage.type); })); } else { this.editedImage.toBlob((/** * @param {?} blob * @return {?} */ (blob) => { this.editResult.emit(blob); this.processing.emit(false); }), this.originalImage.type); } }); } /** * open the bottom sheet for selecting filters, and applies the selected filter in preview mode * @private * @return {?} */ chooseFilters() { /** @type {?} */ const data = { filter: this.selectedFilter }; /** @type {?} */ const bottomSheetRef = this.bottomSheet.open(NgxFilterMenuComponent, { data: data }); bottomSheetRef.afterDismissed().subscribe((/** * @return {?} */ () => { this.selectedFilter = data.filter; this.applyFilter(true); })); } // *************************** // // File Input & Output Methods // // *************************** // /** * load image from input field * @private * @param {?} file * @return {?} */ loadFile(file) { return new Promise((/** * @param {?} resolve * @param {?} reject * @return {?} */ (resolve, reject) => __awaiter(this, void 0, void 0, function* () { this.processing.emit(true); try { yield this.readImage(file); } catch (err) { console.error(err); this.error.emit(new Error(err)); } try { yield this.showPreview(); } catch (err) { console.error(err); this.error.emit(new Error(err)); } // set pane limits // show points this.imageLoaded = true; yield this.limitsService.setPaneDimensions({ width: this.previewDimensions.width, height: this.previewDimensions.height }); setTimeout((/** * @return {?} */ () => __awaiter(this, void 0, void 0, function* () { yield this.detectContours(); this.processing.emit(false); resolve(); })), 15); }))); } /** * read image from File object * @private * @param {?} file * @return {?} */ readImage(file) { return new Promise((/** * @param {?} resolve * @param {?} reject * @return {?} */ (resolve, reject) => __awaiter(this, void 0, void 0, function* () { /** @type {?} */ let imageSrc; try { imageSrc = yield readFile(); } catch (err) { reject(err); } /** @type {?} */ const img = new Image(); img.onload = (/** * @return {?} */ () => __awaiter(this, void 0, void 0, function* () { // set edited image canvas and dimensions this.editedImage = (/** @type {?} */ (document.createElement('canvas'))); this.editedImage.width = img.width; this.editedImage.height = img.height; /** @type {?} */ const ctx = this.editedImage.getContext('2d'); ctx.drawImage(img, 0, 0); // resize image if larger than max image size /** @type {?} */ const width = img.width > img.height ? img.height : img.width; if (width > this.options.maxImageDimensions.width) { this.editedImage = yield this.resize(this.editedImage); } this.imageDimensions.width = this.editedImage.width; this.imageDimensions.height = this.editedImage.height; this.setPreviewPaneDimensions(this.editedImage); resolve(); })); img.src = imageSrc; }))); /** * read file from input field * @return {?} */ function readFile() { return new Promise((/** * @param {?} resolve * @param {?} reject * @return {?} */ (resolve, reject) => { /** @type {?} */ const reader = new FileReader(); reader.onload = (/** * @param {?} event * @return {?} */ (event) => { resolve(reader.result); }); reader.onerror = (/** * @param {?} err * @return {?} */ (err) => { reject(err); }); reader.readAsDataURL(file); })); } } // ************************ // // Image Processing Methods // // ************************ // /** * rotate image 90 degrees * @private * @return {?} */ rotateImage() { return new Promise((/** * @param {?} resolve * @param {?} reject * @return {?} */ (resolve, reject) => { this.processing.emit(true); setTimeout((/** * @return {?} */ () => { /** @type {?} */ const dst = cv.imread(this.editedImage); // const dst = new cv.Mat(); cv.transpose(dst, dst); cv.flip(dst, dst, 1); cv.imshow(this.editedImage, dst); // src.delete(); dst.delete(); // save current preview dimensions and positions /** @type {?} */ const initialPreviewDimensions = { width: 0, height: 0 }; Object.assign(initialPreviewDimensions, this.previewDimensions); /** @type {?} */ const initialPositions = Array.from(this.points); // get new dimensions // set new preview pane dimensions this.setPreviewPaneDimensions(this.editedImage); // get preview pane resize ratio /** @type {?} */ const previewResizeRatios = { width: this.previewDimensions.width / initialPreviewDimensions.width, height: this.previewDimensions.height / initialPreviewDimensions.height }; // set new preview pane dimensions this.limitsService.rotateClockwise(previewResizeRatios, initialPreviewDimensions, initialPositions); this.showPreview().then((/** * @return {?} */ () => { this.processing.emit(false); resolve(); })); }), 30); })); } /** * detects the contours of the document and * * @private * @return {?} */ detectContours() { return new Promise((/** * @param {?} resolve * @param {?} reject * @return {?} */ (resolve, reject) => { this.processing.emit(true); setTimeout((/** * @return {?} */ () => { // load the image and compute the ratio of the old height to the new height, clone it, and resize it /** @type {?} */ const processingResizeRatio = 0.5; /** @type {?} */ const dst = cv.imread(this.editedImage); /** @type {?} */ const dsize = new cv.Size(dst.rows * processingResizeRatio, dst.cols * processingResizeRatio); /** @type {?} */ const ksize = new cv.Size(5, 5); // convert the image to grayscale, blur it, and find edges in the image cv.cvtColor(dst, dst, cv.COLOR_RGBA2GRAY, 0); cv.GaussianBlur(dst, dst, ksize, 0, 0, cv.BORDER_DEFAULT); cv.Canny(dst, dst, 75, 200); // find contours cv.threshold(dst, dst, 120, 200, cv.THRESH_BINARY); /** @type {?} */ const contours = new cv.MatVector(); /** @type {?} */ const hierarchy = new cv.Mat(); cv.findContours(dst, contours, hierarchy, cv.RETR_CCOMP, cv.CHAIN_APPROX_SIMPLE); /** @type {?} */ const rect = cv.boundingRect(dst); dst.delete(); hierarchy.delete(); contours.delete(); // transform the rectangle into a set of points Object.keys(rect).forEach((/** * @param {?} key * @return {?} */ key => { rect[key] = rect[key] * this.imageResizeRatio; })); /** @type {?} */ const contourCoordinates = [ new PositionChangeData({ x: rect.x, y: rect.y }, ['left', 'top']), new PositionChangeData({ x: rect.x + rect.width, y: rect.y }, ['right', 'top']), new PositionChangeData({ x: rect.x + rect.width, y: rect.y + rect.height }, ['right', 'bottom']), new PositionChangeData({ x: rect.x, y: rect.y + rect.height }, ['left', 'bottom']), ]; this.limitsService.repositionPoints(contourCoordinates); // this.processing.emit(false); resolve(); }), 30); })); } /** * apply perspective transform * @private * @return {?} */ transform() { return new Promise((/** * @param {?} resolve * @param {?} reject * @return {?} */ (resolve, reject) => { this.processing.emit(true); setTimeout((/** * @return {?} */ () => { /** @type {?} */ const dst = cv.imread(this.editedImage); // create source coordinates matrix /** @type {?} */ const sourceCoordinates = [ this.getPoint(['top', 'left']), this.getPoint(['top', 'right']), this.getPoint(['bottom', 'right']), this.getPoint(['bottom', 'left']) ].map((/** * @param {?} point * @return {?} */ point => { return [point.x / this.imageResizeRatio, point.y / this.imageResizeRatio]; })); // get max width /** @type {?} */ const bottomWidth = this.getPoint(['bottom', 'right']).x - this.getPoint(['bottom', 'left']).x; /** @type {?} */ const topWidth = this.getPoint(['top', 'right']).x - this.getPoint(['top', 'left']).x; /** @type {?} */ const maxWidth = Math.max(bottomWidth, topWidth) / this.imageResizeRatio; // get max height /** @type {?} */ const leftHeight = this.getPoint(['bottom', 'left']).y - this.getPoint(['top', 'left']).y; /** @type {?} */ const rightHeight = this.getPoint(['bottom', 'right']).y - this.getPoint(['top', 'right']).y; /** @type {?} */ const maxHeight = Math.max(leftHeight, rightHeight) / this.imageResizeRatio; // create dest coordinates matrix /** @type {?} */ const destCoordinates = [ [0, 0], [maxWidth - 1, 0], [maxWidth - 1, maxHeight - 1], [0, maxHeight - 1] ]; // convert to open cv matrix objects /** @type {?} */ const Ms = cv.matFromArray(4, 1, cv.CV_32FC2, [].concat(...sourceCoordinates)); /** @type {?} */ const Md = cv.matFromArray(4, 1, cv.CV_32FC2, [].concat(...destCoordinates)); /** @type {?} */ const transformMatrix = cv.getPerspectiveTransform(Ms, Md); // set new image size /** @type {?} */ const dsize = new cv.Size(maxWidth, maxHeight); // perform warp cv.warpPerspective(dst, dst, transformMatrix, dsize, cv.INTER_LINEAR, cv.BORDER_CONSTANT, new cv.Scalar()); cv.imshow(this.editedImage, dst); dst.delete(); Ms.delete(); Md.delete(); transformMatrix.delete(); this.setPreviewPaneDimensions(this.editedImage); this.showPreview().then((/** * @return {?} */ () => { this.processing.emit(false); resolve(); })); }), 30); })); } /** * applies the selected filter to the image * @private * @param {?} preview - when true, will not apply the filter to the edited image but only display a preview. * when false, will apply to editedImage * @return {?} */ applyFilter(preview) { return new Promise((/** * @param {?} resolve * @param {?} reject * @return {?} */ (resolve, reject) => __awaiter(