UNPKG

ngx-document-scanner

Version:

Angular 2+ component for cropping and enhancing images of documents

738 lines 96.1 kB
import { __awaiter, __decorate, __metadata } from "tslib"; import { Component, ElementRef, EventEmitter, Input, Output, ViewChild } from '@angular/core'; import { LimitsService, PointPositionChange, PositionChangeData, RolesArray } from '../../services/limits.service'; import { MatBottomSheet } from '@angular/material/bottom-sheet'; import { NgxFilterMenuComponent } from '../filter-menu/ngx-filter-menu.component'; import { NgxOpenCVService } from 'ngx-opencv'; let NgxDocScannerComponent = class NgxDocScannerComponent { 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: () => { 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: () => __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: () => { this.mode = 'crop'; this.loadFile(this.originalImage); }, icon: 'arrow_back', type: 'fab', mode: 'color' }, { name: 'filter', action: () => { 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((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(points => { this.points = points; }); } /** * returns an array of buttons according to the editor mode */ get displayedButtons() { return this.editorButtons.filter(button => { return button.mode === this.mode; }); } // ****** // // INPUTS // // ****** // /** * set image for editing * @param file - file from form input */ set file(file) { if (file) { setTimeout(() => { this.processing.emit(true); }, 5); this.imageLoaded = false; this.originalImage = file; this.ngxOpenCv.cvState.subscribe((cvState) => __awaiter(this, void 0, void 0, function* () { if (cvState.ready) { // read file to image & canvas yield this.loadFile(file); this.processing.emit(false); } })); } } ngOnInit() { // set options from config object this.options = new ImageEditorConfig(this.config); // set export image icon this.editorButtons.forEach(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 */ exit() { this.exitEditor.emit('canceled'); } /** * applies the selected filter, and when done emits the resulted image */ exportImage() { return __awaiter(this, void 0, void 0, function* () { yield this.applyFilter(false); if (this.options.maxImageDimensions) { this.resize(this.editedImage) .then(resizeResult => { resizeResult.toBlob((blob) => { this.editResult.emit(blob); this.processing.emit(false); }, this.originalImage.type); }); } else { this.editedImage.toBlob((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 */ chooseFilters() { const data = { filter: this.selectedFilter }; const bottomSheetRef = this.bottomSheet.open(NgxFilterMenuComponent, { data: data }); bottomSheetRef.afterDismissed().subscribe(() => { this.selectedFilter = data.filter; this.applyFilter(true); }); } // *************************** // // File Input & Output Methods // // *************************** // /** * load image from input field */ loadFile(file) { return new Promise((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(() => __awaiter(this, void 0, void 0, function* () { yield this.detectContours(); this.processing.emit(false); resolve(); }), 15); })); } /** * read image from File object */ readImage(file) { return new Promise((resolve, reject) => __awaiter(this, void 0, void 0, function* () { let imageSrc; try { imageSrc = yield readFile(); } catch (err) { reject(err); } const img = new Image(); img.onload = () => __awaiter(this, void 0, void 0, function* () { // set edited image canvas and dimensions this.editedImage = document.createElement('canvas'); this.editedImage.width = img.width; this.editedImage.height = img.height; const ctx = this.editedImage.getContext('2d'); ctx.drawImage(img, 0, 0); // resize image if larger than max image size 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 */ function readFile() { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = (event) => { resolve(reader.result); }; reader.onerror = (err) => { reject(err); }; reader.readAsDataURL(file); }); } } // ************************ // // Image Processing Methods // // ************************ // /** * rotate image 90 degrees */ rotateImage() { return new Promise((resolve, reject) => { this.processing.emit(true); setTimeout(() => { 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 const initialPreviewDimensions = { width: 0, height: 0 }; Object.assign(initialPreviewDimensions, this.previewDimensions); const initialPositions = Array.from(this.points); // get new dimensions // set new preview pane dimensions this.setPreviewPaneDimensions(this.editedImage); // get preview pane resize ratio 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(() => { this.processing.emit(false); resolve(); }); }, 30); }); } /** * detects the contours of the document and **/ detectContours() { return new Promise((resolve, reject) => { this.processing.emit(true); setTimeout(() => { // load the image and compute the ratio of the old height to the new height, clone it, and resize it const processingResizeRatio = 0.5; const dst = cv.imread(this.editedImage); const dsize = new cv.Size(dst.rows * processingResizeRatio, dst.cols * processingResizeRatio); 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); const contours = new cv.MatVector(); const hierarchy = new cv.Mat(); cv.findContours(dst, contours, hierarchy, cv.RETR_CCOMP, cv.CHAIN_APPROX_SIMPLE); const rect = cv.boundingRect(dst); dst.delete(); hierarchy.delete(); contours.delete(); // transform the rectangle into a set of points Object.keys(rect).forEach(key => { rect[key] = rect[key] * this.imageResizeRatio; }); 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 */ transform() { return new Promise((resolve, reject) => { this.processing.emit(true); setTimeout(() => { const dst = cv.imread(this.editedImage); // create source coordinates matrix const sourceCoordinates = [ this.getPoint(['top', 'left']), this.getPoint(['top', 'right']), this.getPoint(['bottom', 'right']), this.getPoint(['bottom', 'left']) ].map(point => { return [point.x / this.imageResizeRatio, point.y / this.imageResizeRatio]; }); // get max width const bottomWidth = this.getPoint(['bottom', 'right']).x - this.getPoint(['bottom', 'left']).x; const topWidth = this.getPoint(['top', 'right']).x - this.getPoint(['top', 'left']).x; const maxWidth = Math.max(bottomWidth, topWidth) / this.imageResizeRatio; // get max height const leftHeight = this.getPoint(['bottom', 'left']).y - this.getPoint(['top', 'left']).y; const rightHeight = this.getPoint(['bottom', 'right']).y - this.getPoint(['top', 'right']).y; const maxHeight = Math.max(leftHeight, rightHeight) / this.imageResizeRatio; // create dest coordinates matrix const destCoordinates = [ [0, 0], [maxWidth - 1, 0], [maxWidth - 1, maxHeight - 1], [0, maxHeight - 1] ]; // convert to open cv matrix objects const Ms = cv.matFromArray(4, 1, cv.CV_32FC2, [].concat(...sourceCoordinates)); const Md = cv.matFromArray(4, 1, cv.CV_32FC2, [].concat(...destCoordinates)); const transformMatrix = cv.getPerspectiveTransform(Ms, Md); // set new image size 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(() => { this.processing.emit(false); resolve(); }); }, 30); }); } /** * applies the selected filter to the image * @param preview - when true, will not apply the filter to the edited image but only display a preview. * when false, will apply to editedImage */ applyFilter(preview) { return new Promise((resolve, reject) => __awaiter(this, void 0, void 0, function* () { this.processing.emit(true); // default options const options = { blur: false, th: true, thMode: cv.ADAPTIVE_THRESH_MEAN_C, thMeanCorrection: 10, thBlockSize: 25, thMax: 255, grayScale: true, }; const dst = cv.imread(this.editedImage); switch (this.selectedFilter) { case 'original': options.th = false; options.grayScale = false; options.blur = false; break; case 'magic_color': options.grayScale = false; break; case 'bw2': options.thMode = cv.ADAPTIVE_THRESH_GAUSSIAN_C; options.thMeanCorrection = 15; options.thBlockSize = 15; break; case 'bw3': options.blur = true; options.thMeanCorrection = 15; break; } setTimeout(() => __awaiter(this, void 0, void 0, function* () { if (options.grayScale) { cv.cvtColor(dst, dst, cv.COLOR_RGBA2GRAY, 0); } if (options.blur) { const ksize = new cv.Size(5, 5); cv.GaussianBlur(dst, dst, ksize, 0, 0, cv.BORDER_DEFAULT); } if (options.th) { if (options.grayScale) { cv.adaptiveThreshold(dst, dst, options.thMax, options.thMode, cv.THRESH_BINARY, options.thBlockSize, options.thMeanCorrection); } else { dst.convertTo(dst, -1, 1, 60); cv.threshold(dst, dst, 170, 255, cv.THRESH_BINARY); } } if (!preview) { cv.imshow(this.editedImage, dst); } yield this.showPreview(dst); this.processing.emit(false); resolve(); }), 30); })); } /** * resize an image to fit constraints set in options.maxImageDimensions */ resize(image) { return new Promise((resolve, reject) => { this.processing.emit(true); setTimeout(() => { const src = cv.imread(image); const currentDimensions = { width: src.size().width, height: src.size().height }; const resizeDimensions = { width: 0, height: 0 }; if (currentDimensions.width > this.options.maxImageDimensions.width) { resizeDimensions.width = this.options.maxImageDimensions.width; resizeDimensions.height = this.options.maxImageDimensions.width / currentDimensions.width * currentDimensions.height; if (resizeDimensions.height > this.options.maxImageDimensions.height) { resizeDimensions.height = this.options.maxImageDimensions.height; resizeDimensions.width = this.options.maxImageDimensions.height / currentDimensions.height * currentDimensions.width; } const dsize = new cv.Size(Math.floor(resizeDimensions.width), Math.floor(resizeDimensions.height)); cv.resize(src, src, dsize, 0, 0, cv.INTER_AREA); const resizeResult = document.createElement('canvas'); cv.imshow(resizeResult, src); src.delete(); this.processing.emit(false); resolve(resizeResult); } else { this.processing.emit(false); resolve(image); } }, 30); }); } /** * display a preview of the image on the preview canvas */ showPreview(image) { return new Promise((resolve, reject) => { let src; if (image) { src = image; } else { src = cv.imread(this.editedImage); } const dst = new cv.Mat(); const dsize = new cv.Size(0, 0); cv.resize(src, dst, dsize, this.imageResizeRatio, this.imageResizeRatio, cv.INTER_AREA); cv.imshow(this.previewCanvas.nativeElement, dst); src.delete(); dst.delete(); resolve(); }); } // *************** // // Utility Methods // // *************** // /** * set preview canvas dimensions according to the canvas element of the original image */ setPreviewPaneDimensions(img) { // set preview pane dimensions this.previewDimensions = this.calculateDimensions(img.width, img.height); this.previewCanvas.nativeElement.width = this.previewDimensions.width; this.previewCanvas.nativeElement.height = this.previewDimensions.height; this.imageResizeRatio = this.previewDimensions.width / img.width; this.imageDivStyle = { width: this.previewDimensions.width + this.options.cropToolDimensions.width + 'px', height: this.previewDimensions.height + this.options.cropToolDimensions.height + 'px', 'margin-left': `calc((100% - ${this.previewDimensions.width + 10}px) / 2 + ${this.options.cropToolDimensions.width / 2}px)`, 'margin-right': `calc((100% - ${this.previewDimensions.width + 10}px) / 2 - ${this.options.cropToolDimensions.width / 2}px)`, }; this.limitsService.setPaneDimensions({ width: this.previewDimensions.width, height: this.previewDimensions.height }); } /** * calculate dimensions of the preview canvas */ calculateDimensions(width, height) { const ratio = width / height; const maxWidth = this.screenDimensions.width > this.maxPreviewWidth ? this.maxPreviewWidth : this.screenDimensions.width - 40; const maxHeight = this.screenDimensions.height - 240; const calculated = { width: maxWidth, height: Math.round(maxWidth / ratio), ratio: ratio }; if (calculated.height > maxHeight) { calculated.height = maxHeight; calculated.width = Math.round(maxHeight * ratio); } return calculated; } /** * returns a point by it's roles * @param roles - an array of roles by which the point will be fetched */ getPoint(roles) { return this.points.find(point => { return this.limitsService.compareArray(point.roles, roles); }); } }; NgxDocScannerComponent.ctorParameters = () => [ { type: NgxOpenCVService }, { type: LimitsService }, { type: MatBottomSheet } ]; __decorate([ ViewChild('PreviewCanvas', { read: ElementRef, static: true }), __metadata("design:type", ElementRef) ], NgxDocScannerComponent.prototype, "previewCanvas", void 0); __decorate([ Output(), __metadata("design:type", EventEmitter) ], NgxDocScannerComponent.prototype, "exitEditor", void 0); __decorate([ Output(), __metadata("design:type", EventEmitter) ], NgxDocScannerComponent.prototype, "editResult", void 0); __decorate([ Output(), __metadata("design:type", EventEmitter) ], NgxDocScannerComponent.prototype, "error", void 0); __decorate([ Output(), __metadata("design:type", EventEmitter) ], NgxDocScannerComponent.prototype, "ready", void 0); __decorate([ Output(), __metadata("design:type", EventEmitter) ], NgxDocScannerComponent.prototype, "processing", void 0); __decorate([ Input(), __metadata("design:type", File), __metadata("design:paramtypes", [File]) ], NgxDocScannerComponent.prototype, "file", null); __decorate([ Input(), __metadata("design:type", Object) ], NgxDocScannerComponent.prototype, "config", void 0); NgxDocScannerComponent = __decorate([ Component({ selector: 'ngx-doc-scanner', template: "<div [ngStyle]=\"editorStyle\" fxLayoutAlign=\"space-around\" style=\"direction: ltr !important\">\r\n <div #imageContainer [ngStyle]=\"imageDivStyle\" style=\"margin: auto;\" >\r\n <ng-container *ngIf=\"imageLoaded && mode === 'crop'\">\r\n <ngx-shape-outine #shapeOutline [color]=\"options.cropToolColor\" [weight]=\"options.cropToolLineWeight\" [dimensions]=\"previewDimensions\"></ngx-shape-outine>\r\n <ngx-draggable-point #topLeft [pointOptions]=\"options.pointOptions\" [startPosition]=\"{x: 0, y: 0}\" [limitRoles]=\"['top', 'left']\" [container]=\"imageContainer\"></ngx-draggable-point>\r\n <ngx-draggable-point #topRight [pointOptions]=\"options.pointOptions\" [startPosition]=\"{x: previewDimensions.width, y: 0}\" [limitRoles]=\"['top', 'right']\" [container]=\"imageContainer\"></ngx-draggable-point>\r\n <ngx-draggable-point #bottomLeft [pointOptions]=\"options.pointOptions\" [startPosition]=\"{x: 0, y: previewDimensions.height}\" [limitRoles]=\"['bottom', 'left']\" [container]=\"imageContainer\"></ngx-draggable-point>\r\n <ngx-draggable-point #bottomRight [pointOptions]=\"options.pointOptions\" [startPosition]=\"{x: previewDimensions.width, y: previewDimensions.height}\" [limitRoles]=\"['bottom', 'right']\" [container]=\"imageContainer\"></ngx-draggable-point>\r\n </ng-container>\r\n <canvas #PreviewCanvas [ngStyle]=\"{'max-width': options.maxPreviewWidth}\" style=\"z-index: 5\" ></canvas>\r\n </div>\r\n <div class=\"editor-actions\" fxLayout=\"row\" fxLayoutAlign=\"space-around\" style=\"position: absolute; bottom: 0; width: 100vw\">\r\n <ng-container *ngFor=\"let button of displayedButtons\" [ngSwitch]=\"button.type\">\r\n <button mat-mini-fab *ngSwitchCase=\"'fab'\" [name]=\"button.name\" (click)=\"button.action()\" [color]=\"options.buttonThemeColor\">\r\n <mat-icon>{{button.icon}}</mat-icon>\r\n </button>\r\n <button mat-raised-button *ngSwitchCase=\"'button'\" [name]=\"button.name\" (click)=\"button.action()\" [color]=\"options.buttonThemeColor\">\r\n <mat-icon>{{button.icon}}</mat-icon>\r\n <span>{{button.text}}}</span>\r\n </button>\r\n </ng-container>\r\n </div>\r\n</div>\r\n\r\n\r\n", styles: [".editor-actions{padding:12px}.editor-actions button{margin:5px}"] }), __metadata("design:paramtypes", [NgxOpenCVService, LimitsService, MatBottomSheet]) ], NgxDocScannerComponent); export { NgxDocScannerComponent }; /** * a class for generating configuration objects for the editor */ class ImageEditorConfig { constructor(options) { /** * max dimensions of oputput image. if set to zero */ this.maxImageDimensions = { width: 800, height: 1200 }; /** * background color of the main editor div */ this.editorBackgroundColor = '#fefefe'; /** * css properties for the main editor div */ this.editorDimensions = { width: '100vw', height: '100vh' }; /** * css that will be added to the main div of the editor component */ this.extraCss = { position: 'absolute', top: 0, left: 0 }; /** * material design theme color name */ this.buttonThemeColor = 'accent'; /** * icon for the button that completes the editing and emits the edited image */ this.exportImageIcon = 'cloud_upload'; /** * color of the crop tool */ this.cropToolColor = '#3cabe2'; /** * shape of the crop tool, can be either a rectangle or a circle */ this.cropToolShape = 'rect'; /** * dimensions of the crop tool */ this.cropToolDimensions = { width: 10, height: 10 }; /** * crop tool outline width */ this.cropToolLineWeight = 3; /** * maximum size of the preview pane */ this.maxPreviewWidth = 800; if (options) { Object.keys(options).forEach(key => { this[key] = options[key]; }); } this.editorStyle = { 'background-color': this.editorBackgroundColor }; Object.assign(this.editorStyle, this.editorDimensions); Object.assign(this.editorStyle, this.extraCss); this.pointOptions = { shape: this.cropToolShape, color: this.cropToolColor, width: 0, height: 0 }; Object.assign(this.pointOptions, this.cropToolDimensions); } } //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibmd4LWRvYy1zY2FubmVyLmNvbXBvbmVudC5qcyIsInNvdXJjZVJvb3QiOiJuZzovL25neC1kb2N1bWVudC1zY2FubmVyLyIsInNvdXJjZXMiOlsibGliL2NvbXBvbmVudHMvaW1hZ2UtZWRpdG9yL25neC1kb2Mtc2Nhbm5lci5jb21wb25lbnQudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IjtBQUFBLE9BQU8sRUFBQyxTQUFTLEVBQUUsVUFBVSxFQUFFLFlBQVksRUFBRSxLQUFLLEVBQVUsTUFBTSxFQUFFLFNBQVMsRUFBQyxNQUFNLGVBQWUsQ0FBQztBQUNwRyxPQUFPLEVBQUMsYUFBYSxFQUFFLG1CQUFtQixFQUFFLGtCQUFrQixFQUFFLFVBQVUsRUFBQyxNQUFNLCtCQUErQixDQUFDO0FBQ2pILE9BQU8sRUFBRSxjQUFjLEVBQUUsTUFBTSxnQ0FBZ0MsQ0FBQztBQUNoRSxPQUFPLEVBQUMsc0JBQXNCLEVBQUMsTUFBTSwwQ0FBMEMsQ0FBQztBQUtoRixPQUFPLEVBQUMsZ0JBQWdCLEVBQUMsTUFBTSxZQUFZLENBQUM7QUFTNUMsSUFBYSxzQkFBc0IsR0FBbkMsTUFBYSxzQkFBc0I7SUF5TWpDLFlBQW9CLFNBQTJCLEVBQVUsYUFBNEIsRUFBVSxXQUEyQjtRQUF0RyxjQUFTLEdBQVQsU0FBUyxDQUFrQjtRQUFVLGtCQUFhLEdBQWIsYUFBYSxDQUFlO1FBQVUsZ0JBQVcsR0FBWCxXQUFXLENBQWdCO1FBcE0xSCxtQkFBbUI7UUFDbkIsbUJBQW1CO1FBQ25CLG1CQUFtQjtRQUNuQjs7V0FFRztRQUNLLGtCQUFhLEdBQThCO1lBQ2pEO2dCQUNFLElBQUksRUFBRSxNQUFNO2dCQUNaLE1BQU0sRUFBRSxHQUFHLEVBQUU7b0JBQ1gsSUFBSSxDQUFDLFVBQVUsQ0FBQyxJQUFJLENBQUMsVUFBVSxDQUFDLENBQUM7Z0JBQ25DLENBQUM7Z0JBQ0QsSUFBSSxFQUFFLFlBQVk7Z0JBQ2xCLElBQUksRUFBRSxLQUFLO2dCQUNYLElBQUksRUFBRSxNQUFNO2FBQ2I7WUFD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