ngx-image-cropper
Version:
An image cropper for Angular
1 lines • 133 kB
Source Map (JSON)
{"version":3,"file":"ngx-image-cropper.mjs","sources":["../../../projects/ngx-image-cropper/src/lib/utils/cropper-position.utils.ts","../../../projects/ngx-image-cropper/src/lib/component/cropper.state.ts","../../../projects/ngx-image-cropper/src/lib/interfaces/move-start.interface.ts","../../../projects/ngx-image-cropper/src/lib/utils/resize.utils.ts","../../../projects/ngx-image-cropper/src/lib/utils/percentage.utils.ts","../../../projects/ngx-image-cropper/src/lib/services/crop.service.ts","../../../projects/ngx-image-cropper/src/lib/utils/exif.utils.ts","../../../projects/ngx-image-cropper/src/lib/services/load-image.service.ts","../../../projects/ngx-image-cropper/src/lib/utils/keyboard.utils.ts","../../../projects/ngx-image-cropper/src/lib/component/image-cropper.component.ts","../../../projects/ngx-image-cropper/src/lib/component/image-cropper.component.html","../../../projects/ngx-image-cropper/src/lib/utils/blob.utils.ts","../../../projects/ngx-image-cropper/src/ngx-image-cropper.ts"],"sourcesContent":["import { CropperPosition, MoveStart } from '../interfaces';\nimport { CropperState } from '../component/cropper.state';\nimport { BasicEvent } from '../interfaces/basic-event.interface';\n\nexport function checkCropperPosition(cropperPosition: CropperPosition, cropperState: CropperState, maintainSize: boolean): CropperPosition {\n cropperPosition = checkCropperSizeRestriction(cropperPosition, cropperState);\n return checkCropperWithinMaxSizeBounds(cropperPosition, cropperState, maintainSize);\n}\n\nexport function checkCropperSizeRestriction(cropperPosition: CropperPosition, cropperState: CropperState): CropperPosition {\n let cropperWidth = cropperPosition.x2 - cropperPosition.x1;\n let cropperHeight = cropperPosition.y2 - cropperPosition.y1;\n const centerX = cropperPosition.x1 + cropperWidth / 2;\n const centerY = cropperPosition.y1 + cropperHeight / 2;\n\n if (cropperState.options.cropperStaticHeight && cropperState.options.cropperStaticWidth) {\n cropperWidth = cropperState.maxSize().width > cropperState.options.cropperStaticWidth\n ? cropperState.options.cropperStaticWidth\n : cropperState.maxSize().width;\n cropperHeight = cropperState.maxSize().height > cropperState.options.cropperStaticHeight\n ? cropperState.options.cropperStaticHeight\n : cropperState.maxSize().height;\n } else {\n cropperWidth = Math.max(cropperState.cropperScaledMinWidth, Math.min(cropperWidth, cropperState.cropperScaledMaxWidth, cropperState.maxSize().width));\n cropperHeight = Math.max(cropperState.cropperScaledMinHeight, Math.min(cropperHeight, cropperState.cropperScaledMaxHeight, cropperState.maxSize().height));\n if (cropperState.options.maintainAspectRatio) {\n if (cropperState.maxSize().width / cropperState.options.aspectRatio < cropperState.maxSize().height) {\n cropperHeight = cropperWidth / cropperState.options.aspectRatio;\n } else {\n cropperWidth = cropperHeight * cropperState.options.aspectRatio;\n }\n }\n }\n\n const x1 = centerX - cropperWidth / 2;\n const x2 = x1 + cropperWidth;\n const y1 = centerY - cropperHeight / 2;\n const y2 = y1 + cropperHeight;\n return {x1, x2, y1, y2};\n}\n\nexport function checkCropperWithinMaxSizeBounds(position: CropperPosition, cropperState: CropperState, maintainSize = false): CropperPosition {\n if (position.x1 < 0) {\n position = {\n ...position,\n x1: 0,\n x2: position.x2 - (maintainSize ? position.x1 : 0)\n };\n }\n if (position.y1 < 0) {\n position = {\n ...position,\n y2: position.y2 - (maintainSize ? position.y1 : 0),\n y1: 0\n };\n }\n if (position.x2 > cropperState.maxSize().width) {\n position = {\n ...position,\n x1: position.x1 - (maintainSize ? (position.x2 - cropperState.maxSize().width) : 0),\n x2: cropperState.maxSize().width\n };\n }\n if (position.y2 > cropperState.maxSize().height) {\n position = {\n ...position,\n y1: position.y1 - (maintainSize ? (position.y2 - cropperState.maxSize().height) : 0),\n y2: cropperState.maxSize().height\n };\n }\n return position;\n}\n\nexport function moveCropper(event: Event | BasicEvent, moveStart: MoveStart): CropperPosition {\n const diffX = getClientX(event) - moveStart.clientX;\n const diffY = getClientY(event) - moveStart.clientY;\n\n return {\n x1: moveStart.cropper.x1 + diffX,\n y1: moveStart.cropper.y1 + diffY,\n x2: moveStart.cropper.x2 + diffX,\n y2: moveStart.cropper.y2 + diffY\n };\n}\n\nexport function resizeCropper(event: Event | BasicEvent, moveStart: MoveStart, cropperState: CropperState): CropperPosition {\n const cropperPosition = {...cropperState.cropper()};\n const moveX = getClientX(event) - moveStart.clientX;\n const moveY = getClientY(event) - moveStart.clientY;\n switch (moveStart.position) {\n case 'left':\n cropperPosition.x1 = Math.min(Math.max(moveStart.cropper.x1 + moveX, cropperPosition.x2 - cropperState.cropperScaledMaxWidth),\n cropperPosition.x2 - cropperState.cropperScaledMinWidth);\n break;\n case 'topleft':\n cropperPosition.x1 = Math.min(Math.max(moveStart.cropper.x1 + moveX, cropperPosition.x2 - cropperState.cropperScaledMaxWidth),\n cropperPosition.x2 - cropperState.cropperScaledMinWidth);\n cropperPosition.y1 = Math.min(Math.max(moveStart.cropper.y1 + moveY, cropperPosition.y2 - cropperState.cropperScaledMaxHeight),\n cropperPosition.y2 - cropperState.cropperScaledMinHeight);\n break;\n case 'top':\n cropperPosition.y1 = Math.min(Math.max(moveStart.cropper.y1 + moveY, cropperPosition.y2 - cropperState.cropperScaledMaxHeight),\n cropperPosition.y2 - cropperState.cropperScaledMinHeight);\n break;\n case 'topright':\n cropperPosition.x2 = Math.max(Math.min(moveStart.cropper.x2 + moveX, cropperPosition.x1 + cropperState.cropperScaledMaxWidth),\n cropperPosition.x1 + cropperState.cropperScaledMinWidth);\n cropperPosition.y1 = Math.min(Math.max(moveStart.cropper.y1 + moveY, cropperPosition.y2 - cropperState.cropperScaledMaxHeight),\n cropperPosition.y2 - cropperState.cropperScaledMinHeight);\n break;\n case 'right':\n cropperPosition.x2 = Math.max(Math.min(moveStart.cropper.x2 + moveX, cropperPosition.x1 + cropperState.cropperScaledMaxWidth),\n cropperPosition.x1 + cropperState.cropperScaledMinWidth);\n break;\n case 'bottomright':\n cropperPosition.x2 = Math.max(Math.min(moveStart.cropper.x2 + moveX, cropperPosition.x1 + cropperState.cropperScaledMaxWidth),\n cropperPosition.x1 + cropperState.cropperScaledMinWidth);\n cropperPosition.y2 = Math.max(Math.min(moveStart.cropper.y2 + moveY, cropperPosition.y1 + cropperState.cropperScaledMaxHeight),\n cropperPosition.y1 + cropperState.cropperScaledMinHeight);\n break;\n case 'bottom':\n cropperPosition.y2 = Math.max(Math.min(moveStart.cropper.y2 + moveY, cropperPosition.y1 + cropperState.cropperScaledMaxHeight),\n cropperPosition.y1 + cropperState.cropperScaledMinHeight);\n break;\n case 'bottomleft':\n cropperPosition.x1 = Math.min(Math.max(moveStart.cropper.x1 + moveX, cropperPosition.x2 - cropperState.cropperScaledMaxWidth),\n cropperPosition.x2 - cropperState.cropperScaledMinWidth);\n cropperPosition.y2 = Math.max(Math.min(moveStart.cropper.y2 + moveY, cropperPosition.y1 + cropperState.cropperScaledMaxHeight),\n cropperPosition.y1 + cropperState.cropperScaledMinHeight);\n break;\n case 'center':\n const scale = 'scale' in event ? event.scale as number : 1;\n const newWidth = Math.min(\n Math.max(cropperState.cropperScaledMinWidth, (Math.abs(moveStart.cropper.x2 - moveStart.cropper.x1)) * scale),\n cropperState.cropperScaledMaxWidth);\n const newHeight = Math.min(\n Math.max(cropperState.cropperScaledMinHeight, (Math.abs(moveStart.cropper.y2 - moveStart.cropper.y1)) * scale),\n cropperState.cropperScaledMaxHeight);\n cropperPosition.x1 = moveStart.clientX - newWidth / 2;\n cropperPosition.x2 = moveStart.clientX + newWidth / 2;\n cropperPosition.y1 = moveStart.clientY - newHeight / 2;\n cropperPosition.y2 = moveStart.clientY + newHeight / 2;\n if (cropperPosition.x1 < 0) {\n cropperPosition.x2 -= cropperPosition.x1;\n cropperPosition.x1 = 0;\n } else if (cropperPosition.x2 > cropperState.maxSize().width) {\n cropperPosition.x1 -= (cropperPosition.x2 - cropperState.maxSize().width);\n cropperPosition.x2 = cropperState.maxSize().width;\n }\n if (cropperPosition.y1 < 0) {\n cropperPosition.y2 -= cropperPosition.y1;\n cropperPosition.y1 = 0;\n } else if (cropperPosition.y2 > cropperState.maxSize().height) {\n cropperPosition.y1 -= (cropperPosition.y2 - cropperState.maxSize().height);\n cropperPosition.y2 = cropperState.maxSize().height;\n }\n break;\n }\n\n if (cropperState.options.maintainAspectRatio) {\n return checkAspectRatio(moveStart.position!, cropperPosition, cropperState);\n } else {\n return cropperPosition;\n }\n}\n\nexport function checkAspectRatio(position: string, cropperPosition: CropperPosition, cropperState: CropperState): CropperPosition {\n cropperPosition = {...cropperPosition};\n let overflowX = 0;\n let overflowY = 0;\n switch (position) {\n case 'top':\n cropperPosition.x2 = cropperPosition.x1 + (cropperPosition.y2 - cropperPosition.y1) * cropperState.options.aspectRatio;\n overflowX = Math.max(cropperPosition.x2 - cropperState.maxSize().width, 0);\n overflowY = Math.max(0 - cropperPosition.y1, 0);\n if (overflowX > 0 || overflowY > 0) {\n cropperPosition.x2 -= (overflowY * cropperState.options.aspectRatio) > overflowX ? (overflowY * cropperState.options.aspectRatio) : overflowX;\n cropperPosition.y1 += (overflowY * cropperState.options.aspectRatio) > overflowX ? overflowY : overflowX / cropperState.options.aspectRatio;\n }\n break;\n case 'bottom':\n cropperPosition.x2 = cropperPosition.x1 + (cropperPosition.y2 - cropperPosition.y1) * cropperState.options.aspectRatio;\n overflowX = Math.max(cropperPosition.x2 - cropperState.maxSize().width, 0);\n overflowY = Math.max(cropperPosition.y2 - cropperState.maxSize().height, 0);\n if (overflowX > 0 || overflowY > 0) {\n cropperPosition.x2 -= (overflowY * cropperState.options.aspectRatio) > overflowX ? (overflowY * cropperState.options.aspectRatio) : overflowX;\n cropperPosition.y2 -= (overflowY * cropperState.options.aspectRatio) > overflowX ? overflowY : (overflowX / cropperState.options.aspectRatio);\n }\n break;\n case 'topleft':\n cropperPosition.y1 = cropperPosition.y2 - (cropperPosition.x2 - cropperPosition.x1) / cropperState.options.aspectRatio;\n overflowX = Math.max(0 - cropperPosition.x1, 0);\n overflowY = Math.max(0 - cropperPosition.y1, 0);\n if (overflowX > 0 || overflowY > 0) {\n cropperPosition.x1 += (overflowY * cropperState.options.aspectRatio) > overflowX ? (overflowY * cropperState.options.aspectRatio) : overflowX;\n cropperPosition.y1 += (overflowY * cropperState.options.aspectRatio) > overflowX ? overflowY : overflowX / cropperState.options.aspectRatio;\n }\n break;\n case 'topright':\n cropperPosition.y1 = cropperPosition.y2 - (cropperPosition.x2 - cropperPosition.x1) / cropperState.options.aspectRatio;\n overflowX = Math.max(cropperPosition.x2 - cropperState.maxSize().width, 0);\n overflowY = Math.max(0 - cropperPosition.y1, 0);\n if (overflowX > 0 || overflowY > 0) {\n cropperPosition.x2 -= (overflowY * cropperState.options.aspectRatio) > overflowX ? (overflowY * cropperState.options.aspectRatio) : overflowX;\n cropperPosition.y1 += (overflowY * cropperState.options.aspectRatio) > overflowX ? overflowY : overflowX / cropperState.options.aspectRatio;\n }\n break;\n case 'right':\n case 'bottomright':\n cropperPosition.y2 = cropperPosition.y1 + (cropperPosition.x2 - cropperPosition.x1) / cropperState.options.aspectRatio;\n overflowX = Math.max(cropperPosition.x2 - cropperState.maxSize().width, 0);\n overflowY = Math.max(cropperPosition.y2 - cropperState.maxSize().height, 0);\n if (overflowX > 0 || overflowY > 0) {\n cropperPosition.x2 -= (overflowY * cropperState.options.aspectRatio) > overflowX ? (overflowY * cropperState.options.aspectRatio) : overflowX;\n cropperPosition.y2 -= (overflowY * cropperState.options.aspectRatio) > overflowX ? overflowY : overflowX / cropperState.options.aspectRatio;\n }\n break;\n case 'left':\n case 'bottomleft':\n cropperPosition.y2 = cropperPosition.y1 + (cropperPosition.x2 - cropperPosition.x1) / cropperState.options.aspectRatio;\n overflowX = Math.max(0 - cropperPosition.x1, 0);\n overflowY = Math.max(cropperPosition.y2 - cropperState.maxSize().height, 0);\n if (overflowX > 0 || overflowY > 0) {\n cropperPosition.x1 += (overflowY * cropperState.options.aspectRatio) > overflowX ? (overflowY * cropperState.options.aspectRatio) : overflowX;\n cropperPosition.y2 -= (overflowY * cropperState.options.aspectRatio) > overflowX ? overflowY : overflowX / cropperState.options.aspectRatio;\n }\n break;\n case 'center':\n cropperPosition.x2 = cropperPosition.x1 + (cropperPosition.y2 - cropperPosition.y1) * cropperState.options.aspectRatio;\n cropperPosition.y2 = cropperPosition.y1 + (cropperPosition.x2 - cropperPosition.x1) / cropperState.options.aspectRatio;\n const overflowX1 = Math.max(0 - cropperPosition.x1, 0);\n const overflowX2 = Math.max(cropperPosition.x2 - cropperState.maxSize().width, 0);\n const overflowY1 = Math.max(cropperPosition.y2 - cropperState.maxSize().height, 0);\n const overflowY2 = Math.max(0 - cropperPosition.y1, 0);\n if (overflowX1 > 0 || overflowX2 > 0 || overflowY1 > 0 || overflowY2 > 0) {\n cropperPosition.x1 += (overflowY1 * cropperState.options.aspectRatio) > overflowX1 ? (overflowY1 * cropperState.options.aspectRatio) : overflowX1;\n cropperPosition.x2 -= (overflowY2 * cropperState.options.aspectRatio) > overflowX2 ? (overflowY2 * cropperState.options.aspectRatio) : overflowX2;\n cropperPosition.y1 += (overflowY2 * cropperState.options.aspectRatio) > overflowX2 ? overflowY2 : overflowX2 / cropperState.options.aspectRatio;\n cropperPosition.y2 -= (overflowY1 * cropperState.options.aspectRatio) > overflowX1 ? overflowY1 : overflowX1 / cropperState.options.aspectRatio;\n }\n break;\n }\n return cropperPosition;\n}\n\nexport function getClientX(event: Event | BasicEvent | TouchEvent): number {\n if ('touches' in event && event.touches[0]) {\n return event.touches[0].clientX;\n } else if ('clientX' in event) {\n return event.clientX;\n }\n\n return 0;\n}\n\nexport function getClientY(event: Event | BasicEvent | TouchEvent): number {\n if ('touches' in event && event.touches[0]) {\n return event.touches[0].clientY;\n } else if ('clientX' in event) {\n return event.clientY;\n }\n\n return 0;\n}\n","import { CropInput, CropperOptions, CropperPosition, Dimensions, ImageTransform, LoadedImage } from '../interfaces';\nimport { signal, SimpleChanges } from '@angular/core';\nimport { checkCropperPosition } from '../utils/cropper-position.utils';\n\nexport class CropperState {\n\n readonly cropper = signal<CropperPosition>({x1: 0, x2: 0, y1: 0, y2: 0});\n\n loadedImage?: LoadedImage;\n maxSize = signal<Dimensions>({ width: 0, height: 0 });\n transform: ImageTransform = {};\n options: CropperOptions = {\n format: 'png',\n output: 'blob',\n autoCrop: true,\n maintainAspectRatio: true,\n aspectRatio: 1,\n resetCropOnAspectRatioChange: true,\n resizeToWidth: 0,\n resizeToHeight: 0,\n cropperMinWidth: 0,\n cropperMinHeight: 0,\n cropperMaxHeight: 0,\n cropperMaxWidth: 0,\n cropperStaticWidth: 0,\n cropperStaticHeight: 0,\n canvasRotation: 0,\n roundCropper: false,\n onlyScaleDown: false,\n imageQuality: 92,\n backgroundColor: undefined,\n containWithinAspectRatio: false,\n hideResizeSquares: false,\n alignImage: 'center',\n cropperFrameAriaLabel: undefined,\n checkImageType: true\n };\n\n // Internal\n cropperScaledMinWidth = 20;\n cropperScaledMinHeight = 20;\n cropperScaledMaxWidth = 20;\n cropperScaledMaxHeight = 20;\n stepSize = 3;\n\n setOptionsFromChanges(changes: SimpleChanges): void {\n if (changes['options']?.currentValue) {\n this.setOptions(changes['options'].currentValue);\n }\n const options = Object.entries(changes)\n .filter(([key]) => key in this.options)\n .reduce((acc, [key, change]) => ({\n ...acc,\n [key]: change.currentValue\n }), {} as Partial<CropperOptions>);\n if (Object.keys(options).length > 0) {\n this.setOptions(options);\n }\n }\n\n setOptions(options: Partial<CropperOptions>): void {\n this.options = {\n ...this.options,\n ...(options || {})\n };\n this.validateOptions();\n\n if (!this.loadedImage?.transformed.image.complete || !this.maxSize) {\n return;\n }\n\n let positionPossiblyChanged = false;\n if ((this.options.maintainAspectRatio && options['aspectRatio']) || 'maintainAspectRatio' in options) {\n this.setCropperScaledMinSize();\n this.setCropperScaledMaxSize();\n if (this.options.maintainAspectRatio && (this.options.resetCropOnAspectRatioChange || !this.aspectRatioIsCorrect())) {\n this.cropper.set(this.maxSizeCropperPosition());\n positionPossiblyChanged = true;\n }\n } else {\n if (options['cropperMinWidth'] || options['cropperMinHeight']) {\n this.setCropperScaledMinSize();\n positionPossiblyChanged = true;\n }\n if (options['cropperMaxWidth'] || options['cropperMaxHeight']) {\n this.setCropperScaledMaxSize();\n positionPossiblyChanged = true;\n }\n if (options['cropperStaticWidth'] || options['cropperStaticHeight']) {\n positionPossiblyChanged = true;\n }\n }\n\n if (positionPossiblyChanged) {\n this.cropper.update((cropper) => checkCropperPosition(cropper, this, false));\n }\n }\n\n private validateOptions(): void {\n if (this.options.maintainAspectRatio && !this.options.aspectRatio) {\n throw new Error('`aspectRatio` should > 0 when `maintainAspectRatio` is enabled');\n }\n }\n\n setMaxSize(width: number, height: number): void {\n this.maxSize.set({ width, height });\n this.setCropperScaledMinSize();\n this.setCropperScaledMaxSize();\n }\n\n setCropperScaledMinSize(): void {\n if (this.loadedImage?.transformed.size) {\n this.setCropperScaledMinWidth();\n this.setCropperScaledMinHeight();\n } else {\n this.cropperScaledMinWidth = 20;\n this.cropperScaledMinHeight = 20;\n }\n }\n\n setCropperScaledMinWidth(): void {\n this.cropperScaledMinWidth = this.options.cropperMinWidth > 0\n ? Math.max(20, this.options.cropperMinWidth / this.loadedImage!.transformed.size.width * this.maxSize().width)\n : 20;\n }\n\n setCropperScaledMinHeight(): void {\n if (this.options.maintainAspectRatio) {\n this.cropperScaledMinHeight = Math.max(20, this.cropperScaledMinWidth / this.options.aspectRatio);\n } else if (this.options.cropperMinHeight > 0) {\n this.cropperScaledMinHeight = Math.max(\n 20,\n this.options.cropperMinHeight / this.loadedImage!.transformed.size.height * this.maxSize().height\n );\n } else {\n this.cropperScaledMinHeight = 20;\n }\n }\n\n setCropperScaledMaxSize(): void {\n if (this.loadedImage?.transformed.size) {\n const ratio = this.loadedImage.transformed.size.width / this.maxSize().width;\n this.cropperScaledMaxWidth = this.options.cropperMaxWidth > 20 ? this.options.cropperMaxWidth / ratio : this.maxSize().width;\n this.cropperScaledMaxHeight = this.options.cropperMaxHeight > 20 ? this.options.cropperMaxHeight / ratio : this.maxSize().height;\n if (this.options.maintainAspectRatio) {\n if (this.cropperScaledMaxWidth > this.cropperScaledMaxHeight * this.options.aspectRatio) {\n this.cropperScaledMaxWidth = this.cropperScaledMaxHeight * this.options.aspectRatio;\n } else if (this.cropperScaledMaxWidth < this.cropperScaledMaxHeight * this.options.aspectRatio) {\n this.cropperScaledMaxHeight = this.cropperScaledMaxWidth / this.options.aspectRatio;\n }\n }\n } else {\n this.cropperScaledMaxWidth = this.maxSize().width;\n this.cropperScaledMaxHeight = this.maxSize().height;\n }\n }\n\n equalsCropperPosition(cropper?: CropperPosition): boolean {\n const localCropper = this.cropper();\n return localCropper == null && cropper == null\n || localCropper != null && cropper != null\n && localCropper.x1.toFixed(3) === cropper.x1.toFixed(3)\n && localCropper.y1.toFixed(3) === cropper.y1.toFixed(3)\n && localCropper.x2.toFixed(3) === cropper.x2.toFixed(3)\n && localCropper.y2.toFixed(3) === cropper.y2.toFixed(3);\n }\n\n equalsTransformTranslate(transform: ImageTransform): boolean {\n return (this.transform.translateH ?? 0) === (transform.translateH ?? 0)\n && (this.transform.translateV ?? 0) === (transform.translateV ?? 0);\n }\n\n equalsTransform(transform: ImageTransform): boolean {\n return this.equalsTransformTranslate(transform)\n && (this.transform.scale ?? 1) === (transform.scale ?? 1)\n && (this.transform.rotate ?? 0) === (transform.rotate ?? 0)\n && (this.transform.flipH ?? false) === (transform.flipH ?? false)\n && (this.transform.flipV ?? false) === (transform.flipV ?? false);\n }\n\n aspectRatioIsCorrect(): boolean {\n const localCropper = this.cropper();\n const currentCropAspectRatio = (localCropper.x2 - localCropper.x1) / (localCropper.y2 - localCropper.y1);\n return currentCropAspectRatio === this.options.aspectRatio;\n }\n\n resizeCropperPosition(oldMaxSize: Dimensions): void {\n if (oldMaxSize.width !== this.maxSize().width || oldMaxSize.height !== this.maxSize().height) {\n this.cropper.update(cropper => ({\n x1: cropper.x1 * this.maxSize().width / oldMaxSize.width,\n x2: cropper.x2 * this.maxSize().width / oldMaxSize.width,\n y1: cropper.y1 * this.maxSize().height / oldMaxSize.height,\n y2: cropper.y2 * this.maxSize().height / oldMaxSize.height\n }));\n }\n }\n\n maxSizeCropperPosition(): CropperPosition {\n return {\n x1: 0,\n y1: 0,\n x2: this.maxSize().width,\n y2: this.maxSize().height\n };\n }\n\n toCropInput(): CropInput {\n return {\n cropper: this.cropper(),\n maxSize: this.maxSize(),\n transform: this.transform,\n loadedImage: this.loadedImage!,\n options: {...this.options}\n };\n }\n}\n","import { CropperPosition, ImageTransform } from './';\n\nexport type Position = 'left' | 'topleft' | 'top' | 'topright' | 'right' | 'bottomright' | 'bottom' | 'bottomleft' | 'center';\n\nexport interface MoveStart {\n type: MoveTypes | null;\n position: Position | null;\n transform?: ImageTransform;\n cropper: CropperPosition;\n clientX: number;\n clientY: number;\n}\n\nexport enum MoveTypes {\n Drag = 'drag',\n Move = 'move',\n Resize = 'resize',\n Pinch = 'pinch'\n}\n","/*\n * Hermite resize - fast image resize/resample using Hermite filter.\n * https://github.com/viliusle/Hermite-resize\n */\n\nexport function resizeCanvas(canvas: HTMLCanvasElement, width: number, height: number) {\n const width_source = canvas.width;\n const height_source = canvas.height;\n width = Math.round(width);\n height = Math.round(height);\n\n const ratio_w = width_source / width;\n const ratio_h = height_source / height;\n const ratio_w_half = Math.ceil(ratio_w / 2);\n const ratio_h_half = Math.ceil(ratio_h / 2);\n\n const ctx = canvas.getContext('2d');\n if (ctx) {\n const img = ctx.getImageData(0, 0, width_source, height_source);\n const img2 = ctx.createImageData(width, height);\n const data = img.data;\n const data2 = img2.data;\n\n for (let j = 0; j < height; j++) {\n for (let i = 0; i < width; i++) {\n const x2 = (i + j * width) * 4;\n const center_y = j * ratio_h;\n let weight = 0;\n let weights = 0;\n let weights_alpha = 0;\n let gx_r = 0;\n let gx_g = 0;\n let gx_b = 0;\n let gx_a = 0;\n\n const xx_start = Math.floor(i * ratio_w);\n const yy_start = Math.floor(j * ratio_h);\n let xx_stop = Math.ceil((i + 1) * ratio_w);\n let yy_stop = Math.ceil((j + 1) * ratio_h);\n xx_stop = Math.min(xx_stop, width_source);\n yy_stop = Math.min(yy_stop, height_source);\n\n for (let yy = yy_start; yy < yy_stop; yy++) {\n const dy = Math.abs(center_y - yy) / ratio_h_half;\n const center_x = i * ratio_w;\n const w0 = dy * dy; //pre-calc part of w\n for (let xx = xx_start; xx < xx_stop; xx++) {\n const dx = Math.abs(center_x - xx) / ratio_w_half;\n const w = Math.sqrt(w0 + dx * dx);\n if (w >= 1) {\n //pixel too far\n continue;\n }\n //hermite filter\n weight = 2 * w * w * w - 3 * w * w + 1;\n const pos_x = 4 * (xx + yy * width_source);\n //alpha\n gx_a += weight * data[pos_x + 3];\n weights_alpha += weight;\n //colors\n if (data[pos_x + 3] < 255)\n weight = weight * data[pos_x + 3] / 250;\n gx_r += weight * data[pos_x];\n gx_g += weight * data[pos_x + 1];\n gx_b += weight * data[pos_x + 2];\n weights += weight;\n }\n }\n data2[x2] = gx_r / weights;\n data2[x2 + 1] = gx_g / weights;\n data2[x2 + 2] = gx_b / weights;\n data2[x2 + 3] = gx_a / weights_alpha;\n }\n }\n\n\n canvas.width = width;\n canvas.height = height;\n\n //draw\n ctx.putImageData(img2, 0, 0);\n }\n}\n","export function percentage(percent: number, totalValue: number) {\n return (percent / 100) * totalValue;\n} ","import { CropperPosition, ImageCroppedEvent } from '../interfaces';\nimport { resizeCanvas } from '../utils/resize.utils';\nimport { percentage } from '../utils/percentage.utils';\nimport { OutputType } from '../interfaces/cropper-options.interface';\nimport { CropInput } from '../interfaces/crop-input.interface';\n\nexport class CropService {\n\n crop(input: CropInput, output: 'blob'): Promise<ImageCroppedEvent> | null;\n crop(input: CropInput, output: 'base64'): ImageCroppedEvent | null;\n crop(input: CropInput, output: OutputType): Promise<ImageCroppedEvent> | ImageCroppedEvent | null {\n const imagePosition = this.getImagePosition(input);\n const width = imagePosition.x2 - imagePosition.x1;\n const height = imagePosition.y2 - imagePosition.y1;\n const cropCanvas = document.createElement('canvas') as HTMLCanvasElement;\n cropCanvas.width = width;\n cropCanvas.height = height;\n\n const ctx = cropCanvas.getContext('2d');\n if (!ctx) {\n return null;\n }\n if (input.options?.backgroundColor != null) {\n ctx.fillStyle = input.options.backgroundColor;\n ctx.fillRect(0, 0, width, height);\n }\n\n const scaleX = (input.transform?.scale || 1) * (input.transform?.flipH ? -1 : 1);\n const scaleY = (input.transform?.scale || 1) * (input.transform?.flipV ? -1 : 1);\n const {translateH, translateV} = this.getCanvasTranslate(input);\n\n const transformedImage = input.loadedImage!.transformed;\n ctx.setTransform(scaleX, 0, 0, scaleY, transformedImage.size.width / 2 + translateH, transformedImage.size.height / 2 + translateV);\n ctx.translate(-imagePosition.x1 / scaleX, -imagePosition.y1 / scaleY);\n ctx.rotate((input.transform?.rotate || 0) * Math.PI / 180);\n\n ctx.drawImage(\n transformedImage.image,\n -transformedImage.size.width / 2,\n -transformedImage.size.height / 2\n );\n\n const result: ImageCroppedEvent = {\n width, height,\n imagePosition,\n cropperPosition: {...input.cropper}\n };\n if (input.options?.containWithinAspectRatio) {\n result.offsetImagePosition = this.getOffsetImagePosition(input);\n }\n const resizeRatio = this.getResizeRatio(width, height, input.options);\n if (resizeRatio !== 1) {\n result.width = Math.round(width * resizeRatio);\n result.height = input.options?.maintainAspectRatio\n ? Math.round(result.width / (input.options?.aspectRatio ?? 1))\n : Math.round(height * resizeRatio);\n resizeCanvas(cropCanvas, result.width, result.height);\n }\n if (output === 'blob') {\n return this.cropToBlob(result, cropCanvas, input);\n } else {\n result.base64 = cropCanvas.toDataURL('image/' + (input.options?.format ?? 'png'), this.getQuality(input.options));\n return result;\n }\n }\n\n private async cropToBlob(output: ImageCroppedEvent, cropCanvas: HTMLCanvasElement, input: CropInput): Promise<ImageCroppedEvent> {\n output.blob = await new Promise<Blob | null>(resolve => cropCanvas.toBlob(resolve, 'image/' + (input.options?.format ?? 'png'), this.getQuality(input.options)));\n if (output.blob) {\n output.objectUrl = URL.createObjectURL(output.blob);\n }\n return output;\n }\n\n private getCanvasTranslate(input: CropInput): { translateH: number, translateV: number } {\n if (input.transform?.translateUnit === 'px') {\n const ratio = this.getRatio(input);\n return {\n translateH: (input.transform?.translateH || 0) * ratio,\n translateV: (input.transform?.translateV || 0) * ratio\n };\n } else {\n return {\n translateH: input.transform?.translateH ? percentage(input.transform.translateH, input.loadedImage!.transformed.size.width) : 0,\n translateV: input.transform?.translateV ? percentage(input.transform.translateV, input.loadedImage!.transformed.size.height) : 0\n };\n }\n }\n\n private getRatio(input: CropInput): number {\n return input.loadedImage!.transformed.size.width / input.maxSize.width;\n }\n\n private getImagePosition(cropperState: CropInput): CropperPosition {\n const ratio = this.getRatio(cropperState);\n const out: CropperPosition = {\n x1: Math.round(cropperState.cropper.x1 * ratio),\n y1: Math.round(cropperState.cropper.y1 * ratio),\n x2: Math.round(cropperState.cropper.x2 * ratio),\n y2: Math.round(cropperState.cropper.y2 * ratio)\n };\n\n if (!cropperState.options?.containWithinAspectRatio) {\n out.x1 = Math.max(out.x1, 0);\n out.y1 = Math.max(out.y1, 0);\n out.x2 = Math.min(out.x2, cropperState.loadedImage!.transformed.size.width);\n out.y2 = Math.min(out.y2, cropperState.loadedImage!.transformed.size.height);\n }\n\n return out;\n }\n\n private getOffsetImagePosition(input: CropInput): CropperPosition {\n const canvasRotation = (input.options?.canvasRotation ?? 0) + input.loadedImage!.exifTransform.rotate;\n const ratio = this.getRatio(input);\n let offsetX: number;\n let offsetY: number;\n\n if (canvasRotation % 2) {\n offsetX = (input.loadedImage!.transformed.size.width - input.loadedImage!.original.size.height) / 2;\n offsetY = (input.loadedImage!.transformed.size.height - input.loadedImage!.original.size.width) / 2;\n } else {\n offsetX = (input.loadedImage!.transformed.size.width - input.loadedImage!.original.size.width) / 2;\n offsetY = (input.loadedImage!.transformed.size.height - input.loadedImage!.original.size.height) / 2;\n }\n\n const cropper = input.cropper;\n const out: CropperPosition = {\n x1: Math.round(cropper.x1 * ratio) - offsetX,\n y1: Math.round(cropper.y1 * ratio) - offsetY,\n x2: Math.round(cropper.x2 * ratio) - offsetX,\n y2: Math.round(cropper.y2 * ratio) - offsetY\n };\n\n if (!input.options?.containWithinAspectRatio) {\n out.x1 = Math.max(out.x1, 0);\n out.y1 = Math.max(out.y1, 0);\n out.x2 = Math.min(out.x2, input.loadedImage!.transformed.size.width);\n out.y2 = Math.min(out.y2, input.loadedImage!.transformed.size.height);\n }\n\n return out;\n }\n\n getResizeRatio(width: number, height: number, options?: {\n resizeToWidth?: number;\n resizeToHeight?: number;\n onlyScaleDown?: boolean;\n }): number {\n const ratios = new Array<number>();\n if (options?.resizeToWidth && options.resizeToWidth > 0) {\n ratios.push(options.resizeToWidth / width);\n }\n if (options?.resizeToHeight && options.resizeToHeight > 0) {\n ratios.push(options.resizeToHeight / height);\n }\n\n const result = ratios.length === 0 ? 1 : Math.min(...ratios);\n\n if (result > 1 && !options?.onlyScaleDown) {\n return result;\n }\n return Math.min(result, 1);\n }\n\n getQuality(options?: { imageQuality?: number }): number {\n return Math.min(1, Math.max(0, (options?.imageQuality ?? 92) / 100));\n }\n}\n","import { ExifTransform } from '../interfaces/exif-transform.interface';\n\n// Black 2x1 JPEG, with the following meta information set:\n// - EXIF Orientation: 6 (Rotated 90° CCW)\n// Source: https://github.com/blueimp/JavaScript-Load-Image\nconst testAutoOrientationImageByteArray = [new Uint8Array([255, 216, 255, 225, 0, 34, 69, 120, 105, 102, 0, 0, 77, 77, 0, 42, 0, 0, 0, 8, 0, 1, 1, 18, 0, 3, 0, 0, 0, 1, 0, 6, 0, 0, 0, 0, 0, 0, 255, 219, 0, 132, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 255, 192, 0, 17, 8, 0, 1, 0, 2, 3, 1, 17, 0, 2, 17, 1, 3, 17, 1, 255, 196, 0, 74, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 16, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 17, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 218, 0, 12, 3, 1, 0, 2, 17, 3, 17, 0, 63, 0, 63, 240, 127, 255, 217])];\nconst testAutoOrientationImageURL = URL.createObjectURL(new Blob(testAutoOrientationImageByteArray, {type: 'image/jpeg'}));\n\nexport function supportsAutomaticRotation(): Promise<boolean> {\n return new Promise((resolve) => {\n const img = new Image();\n img.onload = () => {\n // Check if browser supports automatic image orientation:\n const supported = img.width === 1 && img.height === 2;\n resolve(supported);\n };\n img.src = testAutoOrientationImageURL;\n });\n}\n\nexport function getTransformationsFromExifData(exifRotationOrArrayBuffer: number | ArrayBufferLike): ExifTransform {\n if (typeof exifRotationOrArrayBuffer === 'object') {\n exifRotationOrArrayBuffer = getExifRotation(exifRotationOrArrayBuffer);\n }\n switch (exifRotationOrArrayBuffer) {\n case 2:\n return {rotate: 0, flip: true};\n case 3:\n return {rotate: 2, flip: false};\n case 4:\n return {rotate: 2, flip: true};\n case 5:\n return {rotate: 1, flip: true};\n case 6:\n return {rotate: 1, flip: false};\n case 7:\n return {rotate: 3, flip: true};\n case 8:\n return {rotate: 3, flip: false};\n default:\n return {rotate: 0, flip: false};\n }\n}\n\nfunction getExifRotation(arrayBuffer: ArrayBufferLike): number {\n const view = new DataView(arrayBuffer);\n if (view.getUint16(0, false) !== 0xFFD8) {\n return -2;\n }\n const length = view.byteLength;\n let offset = 2;\n while (offset < length) {\n if (view.getUint16(offset + 2, false) <= 8) return -1;\n const marker = view.getUint16(offset, false);\n offset += 2;\n if (marker == 0xFFE1) {\n if (view.getUint32(offset += 2, false) !== 0x45786966) {\n return -1;\n }\n\n const little = view.getUint16(offset += 6, false) == 0x4949;\n offset += view.getUint32(offset + 4, little);\n const tags = view.getUint16(offset, little);\n offset += 2;\n for (let i = 0; i < tags; i++) {\n if (view.getUint16(offset + (i * 12), little) == 0x0112) {\n return view.getUint16(offset + (i * 12) + 8, little);\n }\n }\n } else if ((marker & 0xFF00) !== 0xFF00) {\n break;\n } else {\n offset += view.getUint16(offset, false);\n }\n }\n return -1;\n}\n","import { Dimensions, LoadedImage, LoadImageOptions } from '../interfaces';\nimport { ExifTransform } from '../interfaces/exif-transform.interface';\nimport { getTransformationsFromExifData, supportsAutomaticRotation } from '../utils/exif.utils';\n\ninterface LoadImageArrayBuffer {\n originalImage: HTMLImageElement;\n originalArrayBuffer: ArrayBufferLike;\n originalObjectUrl: string;\n originalImageSize?: { width: number; height: number; } | null;\n}\n\nexport class LoadImageService {\n\n private autoRotateSupported: Promise<boolean> = supportsAutomaticRotation();\n\n async loadImageFile(file: File, options: LoadImageOptions): Promise<LoadedImage> {\n const arrayBuffer = await file.arrayBuffer();\n if (options.checkImageType) {\n return await this.checkImageTypeAndLoadImageFromArrayBuffer(arrayBuffer, file.type, options);\n }\n return await this.loadImageFromArrayBuffer(arrayBuffer, options);\n }\n\n private checkImageTypeAndLoadImageFromArrayBuffer(arrayBuffer: ArrayBufferLike, imageType: string, options: LoadImageOptions): Promise<LoadedImage> {\n if (!this.isValidImageType(imageType)) {\n return Promise.reject(new Error('Invalid image type'));\n }\n return this.loadImageFromArrayBuffer(arrayBuffer, options, imageType);\n }\n\n private isValidImageType(type: string): boolean {\n return /image\\/(png|jpg|jpeg|heic|bmp|gif|tiff|svg|webp|x-icon|vnd.microsoft.icon)/.test(type);\n }\n\n async loadImageFromURL(url: string, options: LoadImageOptions): Promise<LoadedImage> {\n const res = await fetch(url);\n const blob = await res.blob();\n const buffer = await blob.arrayBuffer();\n return await this.loadImageFromArrayBuffer(buffer, options, blob.type);\n }\n\n loadBase64Image(imageBase64: string, options: LoadImageOptions): Promise<LoadedImage> {\n const arrayBuffer = this.base64ToArrayBuffer(imageBase64);\n return this.loadImageFromArrayBuffer(arrayBuffer, options);\n }\n\n private base64ToArrayBuffer(imageBase64: string): ArrayBufferLike {\n imageBase64 = imageBase64.replace(/^data:([^;]+);base64,/gmi, '');\n const binaryString = atob(imageBase64);\n const len = binaryString.length;\n const bytes = new Uint8Array(len);\n for (let i = 0; i < len; i++) {\n bytes[i] = binaryString.charCodeAt(i);\n }\n return bytes.buffer;\n }\n\n private async loadImageFromArrayBuffer(arrayBuffer: ArrayBufferLike, options: LoadImageOptions, imageType?: string): Promise<LoadedImage> {\n const res = await new Promise<LoadImageArrayBuffer>(async (resolve, reject) => {\n try {\n const blob = new Blob([arrayBuffer], imageType ? {type: imageType} : undefined);\n const objectUrl = URL.createObjectURL(blob);\n const originalImage = new Image();\n const isSvg = imageType === 'image/svg+xml';\n const originalImageSize = isSvg ? await this.getSvgImageSize(blob) : undefined;\n originalImage.onload = () => resolve({\n originalImage,\n originalImageSize,\n originalObjectUrl: objectUrl,\n originalArrayBuffer: arrayBuffer\n });\n originalImage.onerror = reject;\n originalImage.src = objectUrl;\n } catch (e) {\n reject(e);\n }\n });\n return await this.transformImageFromArrayBuffer(res, options, res.originalImageSize != null);\n }\n\n private async getSvgImageSize(blob: Blob): Promise<{ width: number; height: number; } | null> {\n const parser = new DOMParser();\n const doc = parser.parseFromString(await blob.text(), 'image/svg+xml');\n const svgElement = doc.querySelector('svg');\n if (!svgElement) {\n throw Error('Failed to parse SVG image');\n }\n const widthAttr = svgElement.getAttribute('width');\n const heightAttr = svgElement.getAttribute('height');\n if (widthAttr && heightAttr) {\n return null;\n }\n const viewBoxAttr = svgElement.getAttribute('viewBox')\n || svgElement.getAttribute('viewbox');\n if (viewBoxAttr) {\n const viewBox = viewBoxAttr.split(' ');\n return {\n width: +viewBox[2],\n height: +viewBox[3]\n };\n }\n throw Error('Failed to load SVG image. SVG must have width + height or viewBox definition.');\n }\n\n private async transformImageFromArrayBuffer(res: LoadImageArrayBuffer, options: LoadImageOptions, forceTransform = false): Promise<LoadedImage> {\n const autoRotate = await this.autoRotateSupported;\n const exifTransform = getTransformationsFromExifData(autoRotate ? -1 : res.originalArrayBuffer);\n if (!res.originalImage || !res.originalImage.complete) {\n return Promise.reject(new Error('No image loaded'));\n }\n const loadedImage = {\n original: {\n objectUrl: res.originalObjectUrl,\n image: res.originalImage,\n size: res.originalImageSize ?? {\n width: res.originalImage.naturalWidth,\n height: res.originalImage.naturalHeight\n }\n },\n exifTransform\n };\n return this.transformLoadedImage(loadedImage, options, forceTransform);\n }\n\n async transformLoadedImage(loadedImage: Partial<LoadedImage>, options: LoadImageOptions, forceTransform = false): Promise<LoadedImage> {\n const canvasRotation = (options.canvasRotation ?? 0) + loadedImage.exifTransform!.rotate;\n const originalSize = loadedImage.original!.size;\n if (!forceTransform && canvasRotation === 0 && !loadedImage.exifTransform!.flip && !options.containWithinAspectRatio) {\n return {\n original: {\n objectUrl: loadedImage.original!.objectUrl,\n image: loadedImage.original!.image,\n size: {...originalSize}\n },\n transformed: {\n objectUrl: loadedImage.original!.objectUrl,\n image: loadedImage.original!.image,\n size: {...originalSize}\n },\n exifTransform: loadedImage.exifTransform!\n };\n }\n\n const transformedSize = this.getTransformedSize(originalSize, loadedImage.exifTransform!, options);\n const canvas = document.createElement('canvas');\n canvas.width = transformedSize.width;\n canvas.height = transformedSize.height;\n const ctx = canvas.getContext('2d');\n ctx?.setTransform(\n loadedImage.exifTransform!.flip ? -1 : 1,\n 0,\n 0,\n 1,\n canvas.width / 2,\n canvas.height / 2\n );\n ctx?.rotate(Math.PI * (canvasRotation / 2));\n ctx?.drawImage(\n loadedImage.original!.image,\n -originalSize.width / 2,\n -originalSize.height / 2\n );\n const blob = await new Promise<Blob | null>(resolve => canvas.toBlob(resolve, 'image/' + (options.format ?? 'png')));\n if (!blob) {\n throw new Error('Failed to get Blob for transformed image.');\n }\n const objectUrl = URL.createObjectURL(blob);\n const transformedImage = await this.loadImageFromObjectUrl(objectUrl);\n return {\n original: {\n objectUrl: loadedImage.original!.objectUrl,\n image: loadedImage.original!.image,\n size: {...originalSize}\n },\n transformed: {\n objectUrl: objectUrl,\n image: transformedImage,\n size: {\n width: transformedImage.width,\n height: transformedImage.height\n }\n },\n exifTransform: loadedImage.exifTransform!\n };\n }\n\n private loadImageFromObjectUrl(objectUrl: string): Promise<HTMLImageElement> {\n return new Promise<HTMLImageElement>(((resolve, reject) => {\n const image = new Image();\n image.onload = () => resolve(image);\n image.onerror = reject;\n image.src = objectUrl;\n }));\n }\n\n private getTransformedSize(\n originalSize: { width: number, height: number },\n exifTransform: ExifTransform,\n options: LoadImageOptions\n ): Dimensions {\n const canvasRotation = (options.canvasRotation ?? 0) + exifTransform.rotate;\n if (options.containWithinAspectRatio) {\n if (canvasRotation % 2) {\n const minWidthToContain = originalSize.width * (options.aspectRatio ?? 1);\n const minHeightToContain = originalSize.height / (options.aspectRatio ?? 1);\n return {\n width: Math.max(originalSize.height, minWidthToContain),\n height: Math.max(originalSize.width, minHeightToContain)\n };\n } else {\n const minWidthToContain = originalSize.height * (options.aspectRatio ?? 1);\n const minHeightToContain = originalSize.width / (options.aspectRatio ?? 1);\n return {\n width: Math.max(originalSize.width, minWidthToContain),\n height: Math.max(originalSize.height, minHeightToContain)\n };\n }\n }\n\n if (canvasRotation % 2) {\n return {\n height: originalSize.width,\n width: originalSize.height\n };\n }\n return {\n width: originalSize.width,\n height: originalSize.height\n };\n }\n}\n","import {BasicEvent} from \"../interfaces/basic-event.interface\";\nimport {Position} from \"../interfaces/move-start.interface\";\n\nexport function getPositionForKey(key: string): Position {\n switch (key) {\n case 'ArrowUp':\n return 'top';\n case 'ArrowRight':\n return 'right';\n case 'ArrowDown':\n return 'bottom';\n case 'ArrowLeft':\n default:\n return 'left';\n }\n}\n\nexport function getInvertedPositionForKey(key: string): Position {\n switch (key) {\n case 'ArrowUp':\n return 'bottom';\n case 'ArrowRight':\n return 'left';\n case 'ArrowDown':\n return 'top';\n case 'ArrowLeft':\n default:\n return 'right';\n }\n}\n\nexport function getEventForKey(key: string, stepSize: number): BasicEvent {\n switch (key) {\n case 'ArrowUp':\n return {clientX: 0, clientY: stepSize * -1};\n case 'ArrowRight':\n return {clientX: stepSize, clientY: 0};\n case 'ArrowDown':\n return {clientX: 0, clientY: stepSize};\n case 'ArrowLeft':\n default:\n return {clientX: stepSize * -1, clientY: 0};\n }\n}\n","import {\n ChangeDetectionStrategy,\n Component,\n ElementRef,\n HostBinding,\n HostListener,\n Input,\n OnChanges,\n OnDestroy,\n OnInit,\n output,\n signal,\n SimpleChanges,\n ViewChild\n} from '@angular/core';\nimport { DomSanitizer, SafeStyle, SafeUrl } from '@angular/platform-browser';\nimport {\n CropperOptions,\n CropperPosition,\n Dimensions,\n ImageCroppedEvent,\n ImageTransform,\n LoadedImage,\n MoveStart\n} from '../interfaces';\nimport { OutputFormat, OutputType } from '../interfaces/cropper-options.interface';\nimport { CropperState } from './cropper.state';\nimport { MoveTypes, Position } from '../interfaces/move-start.interface';\nimport { CropService } from '../services/crop.service';\nimport { LoadImageService } from '../services/load-image.service';\nimport { getEventForKey, getInvertedPositionForKey, getPositionForKey } from '../utils/keyboard.utils';\nimport { first, takeUntil } from 'rxjs/operators';\nimport { fromEvent, merge, Subject } from 'rxjs';\nimport { NgIf } from '@angular/common';\nimport { BasicEvent } from '../interfaces/basic-event.interface';\nimport {\n checkCropperPosition,\n checkCropperWithinMaxSizeBounds,\n getClientX,\n getClientY,\n moveCropper,\n resizeCropper\n} from '../utils/cropper-position.utils';\n\n@Component({\n selector: 'image-cropper',\n templateUrl: './image-cropper.component.html',\n styleUrls: ['./image-cropper.component.scss'],\n changeDetection: ChangeDetectionStrategy.OnPush,\n standalone: true,\n imports: [NgIf],\n})\nexport class ImageCropperComponent implements OnChanges, OnInit, OnDestroy {\n\n private readonly pinchStart$ = new Subject<void>();\n private readonly cropService = new CropService();\n private readonly loadImageService = new LoadImageService();\n\n private setImageMaxSizeRetries = 0;\n private moveStart?: MoveStart;\n private resizedWhileHidden = false;\n\n protected readonly moveTypes = MoveTypes;\n protected readonly state = new CropperState();\n\n readonly safeImgDataUrl = signal<SafeUrl | string | undefined>(undefined);\n readonly safeTransformStyle = signal<SafeStyle | string | undefined>(undefined);\n marginLeft: SafeStyle | string = '0px';\n imageVisible = false;\n\n @ViewChild('wrapper', {static: true}) wrapper!: ElementRef<HTMLDivElement>;\n @ViewChild('sourceImage', {static: false}) sourceImage!: ElementRef<HTMLDivElement>;\n\n @Input() imageChangedEvent?: Event | null;\n @Input() imageURL?: string;\n @Input() imageBase64?: string;\n @Input() imageFile?: File;\n @Input() imageAltText?: string;\n\n @Input() options?: Partial<CropperOptions>;\n @Input() cropperFrameAriaLabel?: string;\n @Input() output?: 'blob' | 'base64';\n @Input() format?: OutputFormat;\n @Input() autoCrop?: boolean;\n @Input() cropper?: CropperPosition;\n @Input() transform?: ImageTransform;\n @Input() maintainAspectRatio?: boolean;\n @Input() aspectRatio?: