@bitforgehq/angular-ionic-image-cropper
Version:
An Ionic Angular library for cross platform image cropping.
253 lines (245 loc) • 20.4 kB
JavaScript
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