UNPKG

@bitforgehq/angular-ionic-image-cropper

Version:

An Ionic Angular library for cross platform image cropping.

253 lines (245 loc) 20.4 kB
import * as i0 from '@angular/core'; import { EventEmitter, ViewChild, Output, Input, Inject, ViewEncapsulation, Component, Injectable } from '@angular/core'; import * as i1 from '@ionic/angular/standalone'; import { ModalController } from '@ionic/angular/standalone'; import Cropper from 'cropperjs'; import { Camera, CameraSource, CameraResultType } from '@capacitor/camera'; class ImageCropperComponent { modalCtrl; imageUrl; aspectRatio = 16 / 9; cancelText = 'Cancel'; doneText = 'Done'; quality = 0.9; outputFormat = 'image/jpeg'; cropComplete = new EventEmitter(); imageElement; cropperContainer; cropper; constructor(modalCtrl) { this.modalCtrl = modalCtrl; } ngAfterViewInit() { const imgEl = this.imageElement.nativeElement; const containerEl = this.cropperContainer.nativeElement; containerEl.style.width = '100%'; containerEl.style.height = '100%'; containerEl.style.overflow = 'hidden'; const initWhenReady = () => { requestAnimationFrame(() => { const width = imgEl.naturalWidth; const height = imgEl.naturalHeight; if (width >= 300 && height >= 200) { // Force layout + redraw tricks imgEl.style.display = 'none'; setTimeout(() => { imgEl.style.display = 'block'; window.dispatchEvent(new Event('resize')); this.initializeCropper(); }, 20); } else { setTimeout(initWhenReady, 50); } }); }; if (imgEl.complete) { initWhenReady(); } else { imgEl.onload = () => initWhenReady(); } } initializeCropper() { this.cropper = new Cropper(this.imageElement.nativeElement, { aspectRatio: this.aspectRatio, viewMode: 1, autoCropArea: 0.8, responsive: true, restore: false, center: true, highlight: false, cropBoxMovable: true, cropBoxResizable: true, toggleDragModeOnDblclick: false, ready: () => { const bg = document.querySelector('.cropper-bg'); if (bg) { bg.style.backgroundImage = 'none'; } else { setTimeout(() => { const retry = document.querySelector('.cropper-bg'); if (retry) retry.style.backgroundImage = 'none'; }, 200); } } }); } crop() { this.getCroppedImage().then((blob) => { this.modalCtrl.dismiss(blob); }); } cancel() { this.modalCtrl.dismiss(); } getCroppedImage() { return new Promise((resolve) => { const canvas = this.cropper.getCroppedCanvas(); canvas.toBlob((blob) => { if (blob) this.cropComplete.emit(blob); resolve(blob); this.modalCtrl.dismiss(blob); }, this.outputFormat, this.quality); }); } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: ImageCropperComponent, deps: [{ token: ModalController }], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.1.3", type: ImageCropperComponent, isStandalone: true, selector: "lib-angular-ionic-image-cropper", inputs: { imageUrl: "imageUrl", aspectRatio: "aspectRatio", cancelText: "cancelText", doneText: "doneText", quality: "quality", outputFormat: "outputFormat" }, outputs: { cropComplete: "cropComplete" }, viewQueries: [{ propertyName: "imageElement", first: true, predicate: ["imageElement"], descendants: true }, { propertyName: "cropperContainer", first: true, predicate: ["cropperContainer"], descendants: true }], ngImport: i0, template: ` <div class="lib-cropper-modal-container"> <div class="lib-cropper-content"> <div class="lib-cropper-container" #cropperContainer> <img #imageElement [src]="imageUrl" crossorigin="anonymous" style="max-width: 100%; object-fit: contain; display: block;" /> </div> </div> <div class="lib-cropper-footer"> <div class="lib-cropper-actions ios-footer"> <button class="btn btn-cancel" (click)="cancel()">{{ cancelText }}</button> <button class="btn btn-crop" (click)="crop()">{{ doneText }}</button> </div> </div> </div> `, isInline: true, styles: [".cropper-container{direction:ltr;font-size:0;line-height:0;position:relative;-ms-touch-action:none;touch-action:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.cropper-container img{-webkit-backface-visibility:hidden;backface-visibility:hidden;display:block;height:100%;image-orientation:0deg;max-height:none!important;max-width:none!important;min-height:0!important;min-width:0!important;width:100%}.cropper-wrap-box,.cropper-canvas,.cropper-drag-box,.cropper-crop-box,.cropper-modal{inset:0;position:absolute}.cropper-wrap-box,.cropper-canvas{overflow:hidden}.cropper-drag-box{background-color:#fff;opacity:0}.cropper-modal{background-color:#000;opacity:.5}.cropper-view-box{display:block;height:100%;outline:1px solid #39f;outline-color:#3399ffbf;overflow:hidden;width:100%}.cropper-dashed{border:0 dashed #eee;display:block;opacity:.5;position:absolute}.cropper-dashed.dashed-h{border-bottom-width:1px;border-top-width:1px;height:calc(100% / 3);left:0;top:calc(100% / 3);width:100%}.cropper-dashed.dashed-v{border-left-width:1px;border-right-width:1px;height:100%;left:calc(100% / 3);top:0;width:calc(100% / 3)}.cropper-center{display:block;height:0;left:50%;opacity:.75;position:absolute;top:50%;width:0}.cropper-center:before,.cropper-center:after{background-color:#eee;content:\" \";display:block;position:absolute}.cropper-center:before{height:1px;left:-3px;top:0;width:7px}.cropper-center:after{height:7px;left:0;top:-3px;width:1px}.cropper-face,.cropper-line,.cropper-point{display:block;height:100%;opacity:.1;position:absolute;width:100%}.cropper-face{background-color:#fff;left:0;top:0}.cropper-line{background-color:#39f}.cropper-line.line-e{cursor:ew-resize;right:-3px;top:0;width:5px}.cropper-line.line-n{cursor:ns-resize;height:5px;left:0;top:-3px}.cropper-line.line-w{cursor:ew-resize;left:-3px;top:0;width:5px}.cropper-line.line-s{bottom:-3px;cursor:ns-resize;height:5px;left:0}.cropper-point{background-color:#39f;height:5px;opacity:.75;width:5px}.cropper-point.point-e{cursor:ew-resize;margin-top:-3px;right:-3px;top:50%}.cropper-point.point-n{cursor:ns-resize;left:50%;margin-left:-3px;top:-3px}.cropper-point.point-w{cursor:ew-resize;left:-3px;margin-top:-3px;top:50%}.cropper-point.point-s{bottom:-3px;cursor:s-resize;left:50%;margin-left:-3px}.cropper-point.point-ne{cursor:nesw-resize;right:-3px;top:-3px}.cropper-point.point-nw{cursor:nwse-resize;left:-3px;top:-3px}.cropper-point.point-sw{bottom:-3px;cursor:nesw-resize;left:-3px}.cropper-point.point-se{bottom:-3px;cursor:nwse-resize;height:20px;opacity:1;right:-3px;width:20px}@media (min-width: 768px){.cropper-point.point-se{height:15px;width:15px}}@media (min-width: 992px){.cropper-point.point-se{height:10px;width:10px}}@media (min-width: 1200px){.cropper-point.point-se{height:5px;opacity:.75;width:5px}}.cropper-point.point-se:before{background-color:#39f;bottom:-50%;content:\" \";display:block;height:200%;opacity:0;position:absolute;right:-50%;width:200%}.cropper-invisible{opacity:0}.cropper-bg{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAAA3NCSVQICAjb4U/gAAAABlBMVEXMzMz////TjRV2AAAACXBIWXMAAArrAAAK6wGCiw1aAAAAHHRFWHRTb2Z0d2FyZQBBZG9iZSBGaXJld29ya3MgQ1M26LyyjAAAABFJREFUCJlj+M/AgBVhF/0PAH6/D/HkDxOGAAAAAElFTkSuQmCC)}.cropper-hide{display:block;height:0;position:absolute;width:0}.cropper-hidden{display:none!important}.cropper-move{cursor:move}.cropper-crop{cursor:crosshair}.cropper-disabled .cropper-drag-box,.cropper-disabled .cropper-face,.cropper-disabled .cropper-line,.cropper-disabled .cropper-point{cursor:not-allowed}.lib-cropper-modal-container{display:flex;flex-direction:column;height:100dvh;width:100vw;background:#000;-webkit-overflow-scrolling:touch;backface-visibility:hidden;z-index:9999}.lib-cropper-content{flex:1;display:flex;flex-direction:column}.lib-cropper-container img{display:block;height:100%;width:100%;object-fit:contain;max-width:100%;-webkit-transform:translateZ(0);image-rendering:-webkit-optimize-contrast}.lib-cropper-container{position:relative;width:100%;height:100%;flex:1;overflow:hidden}.lib-cropper-footer{display:flex;justify-content:center;align-items:center;padding:16px;background:#121213;flex-shrink:0;padding-bottom:calc(16px + env(safe-area-inset-bottom))}.lib-cropper-actions{display:flex;width:100%;flex-direction:row;justify-content:space-between}.btn{padding:8px 16px;border:none;border-radius:4px;font-size:14px;cursor:pointer;font-weight:500}.ios-footer .btn-cancel{background:transparent;color:#0480f6}.ios-footer .btn-crop{background:transparent;color:#fecc01}.cropper-container{font-family:inherit}.cropper-view-box,.cropper-face{border-radius:0}.cropper-bg{background-image:none!important}@media screen and (-webkit-min-device-pixel-ratio: 0){.cropper-container,.cropper-view-box{-webkit-transform:translateZ(0);transform:translateZ(0)}}\n/*! Bundled license information:\n\ncropperjs/dist/cropper.css:\n (*!\n * Cropper.js v1.5.13\n * https://fengyuanchen.github.io/cropperjs\n *\n * Copyright 2015-present Chen Fengyuan\n * Released under the MIT license\n *\n * Date: 2022-11-20T05:30:43.444Z\n *)\n*/\n"], encapsulation: i0.ViewEncapsulation.None }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: ImageCropperComponent, decorators: [{ type: Component, args: [{ selector: 'lib-angular-ionic-image-cropper', template: ` <div class="lib-cropper-modal-container"> <div class="lib-cropper-content"> <div class="lib-cropper-container" #cropperContainer> <img #imageElement [src]="imageUrl" crossorigin="anonymous" style="max-width: 100%; object-fit: contain; display: block;" /> </div> </div> <div class="lib-cropper-footer"> <div class="lib-cropper-actions ios-footer"> <button class="btn btn-cancel" (click)="cancel()">{{ cancelText }}</button> <button class="btn btn-crop" (click)="crop()">{{ doneText }}</button> </div> </div> </div> `, encapsulation: ViewEncapsulation.None, standalone: true, styles: [".cropper-container{direction:ltr;font-size:0;line-height:0;position:relative;-ms-touch-action:none;touch-action:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.cropper-container img{-webkit-backface-visibility:hidden;backface-visibility:hidden;display:block;height:100%;image-orientation:0deg;max-height:none!important;max-width:none!important;min-height:0!important;min-width:0!important;width:100%}.cropper-wrap-box,.cropper-canvas,.cropper-drag-box,.cropper-crop-box,.cropper-modal{inset:0;position:absolute}.cropper-wrap-box,.cropper-canvas{overflow:hidden}.cropper-drag-box{background-color:#fff;opacity:0}.cropper-modal{background-color:#000;opacity:.5}.cropper-view-box{display:block;height:100%;outline:1px solid #39f;outline-color:#3399ffbf;overflow:hidden;width:100%}.cropper-dashed{border:0 dashed #eee;display:block;opacity:.5;position:absolute}.cropper-dashed.dashed-h{border-bottom-width:1px;border-top-width:1px;height:calc(100% / 3);left:0;top:calc(100% / 3);width:100%}.cropper-dashed.dashed-v{border-left-width:1px;border-right-width:1px;height:100%;left:calc(100% / 3);top:0;width:calc(100% / 3)}.cropper-center{display:block;height:0;left:50%;opacity:.75;position:absolute;top:50%;width:0}.cropper-center:before,.cropper-center:after{background-color:#eee;content:\" \";display:block;position:absolute}.cropper-center:before{height:1px;left:-3px;top:0;width:7px}.cropper-center:after{height:7px;left:0;top:-3px;width:1px}.cropper-face,.cropper-line,.cropper-point{display:block;height:100%;opacity:.1;position:absolute;width:100%}.cropper-face{background-color:#fff;left:0;top:0}.cropper-line{background-color:#39f}.cropper-line.line-e{cursor:ew-resize;right:-3px;top:0;width:5px}.cropper-line.line-n{cursor:ns-resize;height:5px;left:0;top:-3px}.cropper-line.line-w{cursor:ew-resize;left:-3px;top:0;width:5px}.cropper-line.line-s{bottom:-3px;cursor:ns-resize;height:5px;left:0}.cropper-point{background-color:#39f;height:5px;opacity:.75;width:5px}.cropper-point.point-e{cursor:ew-resize;margin-top:-3px;right:-3px;top:50%}.cropper-point.point-n{cursor:ns-resize;left:50%;margin-left:-3px;top:-3px}.cropper-point.point-w{cursor:ew-resize;left:-3px;margin-top:-3px;top:50%}.cropper-point.point-s{bottom:-3px;cursor:s-resize;left:50%;margin-left:-3px}.cropper-point.point-ne{cursor:nesw-resize;right:-3px;top:-3px}.cropper-point.point-nw{cursor:nwse-resize;left:-3px;top:-3px}.cropper-point.point-sw{bottom:-3px;cursor:nesw-resize;left:-3px}.cropper-point.point-se{bottom:-3px;cursor:nwse-resize;height:20px;opacity:1;right:-3px;width:20px}@media (min-width: 768px){.cropper-point.point-se{height:15px;width:15px}}@media (min-width: 992px){.cropper-point.point-se{height:10px;width:10px}}@media (min-width: 1200px){.cropper-point.point-se{height:5px;opacity:.75;width:5px}}.cropper-point.point-se:before{background-color:#39f;bottom:-50%;content:\" \";display:block;height:200%;opacity:0;position:absolute;right:-50%;width:200%}.cropper-invisible{opacity:0}.cropper-bg{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAAA3NCSVQICAjb4U/gAAAABlBMVEXMzMz////TjRV2AAAACXBIWXMAAArrAAAK6wGCiw1aAAAAHHRFWHRTb2Z0d2FyZQBBZG9iZSBGaXJld29ya3MgQ1M26LyyjAAAABFJREFUCJlj+M/AgBVhF/0PAH6/D/HkDxOGAAAAAElFTkSuQmCC)}.cropper-hide{display:block;height:0;position:absolute;width:0}.cropper-hidden{display:none!important}.cropper-move{cursor:move}.cropper-crop{cursor:crosshair}.cropper-disabled .cropper-drag-box,.cropper-disabled .cropper-face,.cropper-disabled .cropper-line,.cropper-disabled .cropper-point{cursor:not-allowed}.lib-cropper-modal-container{display:flex;flex-direction:column;height:100dvh;width:100vw;background:#000;-webkit-overflow-scrolling:touch;backface-visibility:hidden;z-index:9999}.lib-cropper-content{flex:1;display:flex;flex-direction:column}.lib-cropper-container img{display:block;height:100%;width:100%;object-fit:contain;max-width:100%;-webkit-transform:translateZ(0);image-rendering:-webkit-optimize-contrast}.lib-cropper-container{position:relative;width:100%;height:100%;flex:1;overflow:hidden}.lib-cropper-footer{display:flex;justify-content:center;align-items:center;padding:16px;background:#121213;flex-shrink:0;padding-bottom:calc(16px + env(safe-area-inset-bottom))}.lib-cropper-actions{display:flex;width:100%;flex-direction:row;justify-content:space-between}.btn{padding:8px 16px;border:none;border-radius:4px;font-size:14px;cursor:pointer;font-weight:500}.ios-footer .btn-cancel{background:transparent;color:#0480f6}.ios-footer .btn-crop{background:transparent;color:#fecc01}.cropper-container{font-family:inherit}.cropper-view-box,.cropper-face{border-radius:0}.cropper-bg{background-image:none!important}@media screen and (-webkit-min-device-pixel-ratio: 0){.cropper-container,.cropper-view-box{-webkit-transform:translateZ(0);transform:translateZ(0)}}\n/*! Bundled license information:\n\ncropperjs/dist/cropper.css:\n (*!\n * Cropper.js v1.5.13\n * https://fengyuanchen.github.io/cropperjs\n *\n * Copyright 2015-present Chen Fengyuan\n * Released under the MIT license\n *\n * Date: 2022-11-20T05:30:43.444Z\n *)\n*/\n"] }] }], ctorParameters: () => [{ type: i1.ModalController, decorators: [{ type: Inject, args: [ModalController] }] }], propDecorators: { imageUrl: [{ type: Input }], aspectRatio: [{ type: Input }], cancelText: [{ type: Input }], doneText: [{ type: Input }], quality: [{ type: Input }], outputFormat: [{ type: Input }], cropComplete: [{ type: Output }], imageElement: [{ type: ViewChild, args: ['imageElement'] }], cropperContainer: [{ type: ViewChild, args: ['cropperContainer'] }] } }); // projects/image-cropper/src/lib/image-cropper.service.ts /** * Service for handling image cropping operations with Ionic integration */ class ImageCropperService { modalCtrl; constructor(modalCtrl) { this.modalCtrl = modalCtrl; } /** * Opens the image cropper modal with the specified image * @param imagePath - Path or URL to the image to crop * @param config - Configuration options for the cropper * @returns Promise that resolves to the cropped image as a Blob * @throws Error if the crop operation is cancelled */ async openCropper(imagePath, config = {}) { const { cancelText = 'Cancel', doneText = 'Done', aspectRatio = 16 / 9, quality = 0.9, outputFormat = 'image/jpeg' } = config; const modal = await this.modalCtrl.create({ component: ImageCropperComponent, componentProps: { imageUrl: imagePath, aspectRatio, cancelText, doneText, quality, outputFormat, modalCtrl: this.modalCtrl, }, cssClass: 'full-height-modal', }); await modal.present(); const { data } = await modal.onWillDismiss(); if (!data) throw new Error('Crop cancelled'); return data; } /** * Takes a photo using the device camera and opens the cropper * @param config - Configuration options for the cropper * @returns Promise that resolves to the cropped image as a Blob * @throws Error if photo capture or crop operation fails */ async takeAndCropPhoto(config = {}) { try { const photo = await Camera.getPhoto({ quality: 90, resultType: CameraResultType.Uri, source: CameraSource.Camera, allowEditing: false, }); // Use webPath for web environments, fallback to path for native const imagePath = photo.webPath || photo.path; if (!imagePath) { throw new Error('Failed to capture photo - no image path available'); } return this.openCropper(imagePath, config); } catch (error) { console.error('Error in takeAndCropPhoto:', error); throw error; } } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: ImageCropperService, deps: [{ token: i1.ModalController }], target: i0.ɵɵFactoryTarget.Injectable }); static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: ImageCropperService }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: ImageCropperService, decorators: [{ type: Injectable }], ctorParameters: () => [{ type: i1.ModalController }] }); /* * Public API Surface of angular-ionic-image-cropper */ // Export styles for manual import if needed const STYLES = './styles.scss'; /** * Generated bundle index. Do not edit. */ export { ImageCropperComponent, ImageCropperService, STYLES }; //# sourceMappingURL=bitforgehq-angular-ionic-image-cropper.mjs.map