UNPKG

@asoftwareworld/form-builder-pro

Version:

ASW Form Builder Pro helps you with rapid development and designed web forms which includes several controls. The key feature of Form Builder is to make your content attractive and effective. We can customize our control at run time and preview the same b

1 lines 107 kB
{"version":3,"file":"asoftwareworld-form-builder-pro-image-crop.mjs","sources":["../../src/components/image-crop/services/crop.service.ts","../../src/components/image-crop/services/cropper-position.service.ts","../../src/components/image-crop/services/load-image.service.ts","../../src/components/image-crop/image-crop.component.ts","../../src/components/image-crop/image-crop.component.html","../../src/components/image-crop/image-crop.module.ts","../../src/components/image-crop/public_api.ts","../../src/components/image-crop/asoftwareworld-form-builder-pro-image-crop.ts"],"sourcesContent":["/**\n * @license\n * Copyright ASW (A Software World) All Rights Reserved.\n *\n * Use of this source code is governed by an MIT-style license that can be\n * found in the LICENSE file\n */\n\nimport { ElementRef, Injectable } from '@angular/core';\nimport { CropperPosition, Dimensions, CropperSettings, ImageCroppedEvent, LoadedImage, OutputType } from '@asoftwareworld/form-builder-pro/api';\nimport { percentage, resizeCanvas } from '@asoftwareworld/form-builder-pro/utils';\n\n@Injectable({ providedIn: 'root' })\nexport class CropService {\n crop(loadedImage: LoadedImage, cropper: CropperPosition, settings: CropperSettings, output: 'blob', maxSize: Dimensions): Promise<ImageCroppedEvent> | null;\n crop(loadedImage: LoadedImage, cropper: CropperPosition, settings: CropperSettings, output: 'base64', maxSize: Dimensions): ImageCroppedEvent | null;\n crop(loadedImage: LoadedImage, cropper: CropperPosition, settings: CropperSettings, output: OutputType, maxSize: Dimensions): Promise<ImageCroppedEvent> | ImageCroppedEvent | null {\n const imagePosition = this.getImagePosition(loadedImage, cropper, settings, maxSize);\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 (settings.backgroundColor != null) {\n ctx.fillStyle = settings.backgroundColor;\n ctx.fillRect(0, 0, width, height);\n }\n\n const scaleX = (settings.transform.scale || 1) * (settings.transform.flipH ? -1 : 1);\n const scaleY = (settings.transform.scale || 1) * (settings.transform.flipV ? -1 : 1);\n const { translateH, translateV } = this.getCanvasTranslate(loadedImage, settings, maxSize);\n\n const transformedImage = 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(((settings.transform.rotate || 0) * Math.PI) / 180);\n\n ctx.drawImage(transformedImage.image, -transformedImage.size.width / 2, -transformedImage.size.height / 2);\n\n const result: ImageCroppedEvent = {\n width,\n height,\n imagePosition,\n cropperPosition: { ...cropper }\n };\n if (settings.containWithinAspectRatio) {\n result.offsetImagePosition = this.getOffsetImagePosition(loadedImage, cropper, settings, maxSize);\n }\n const resizeRatio = this.getResizeRatio(width, height, settings);\n if (resizeRatio !== 1) {\n result.width = Math.round(width * resizeRatio);\n result.height = settings.maintainAspectRatio ? Math.round(result.width / settings.aspectRatio) : Math.round(height * resizeRatio);\n resizeCanvas(cropCanvas, result.width, result.height);\n }\n if (output === 'blob') {\n return this.cropToBlob(result, cropCanvas, settings);\n } else {\n result.base64 = cropCanvas.toDataURL('image/' + settings.format, this.getQuality(settings));\n return result;\n }\n }\n\n private async cropToBlob(output: ImageCroppedEvent, cropCanvas: HTMLCanvasElement, settings: CropperSettings): Promise<ImageCroppedEvent> {\n output.blob = await new Promise<Blob | null>((resolve) => cropCanvas.toBlob(resolve, 'image/' + settings.format, this.getQuality(settings)));\n if (output.blob) {\n output.objectUrl = URL.createObjectURL(output.blob);\n }\n return output;\n }\n\n private getCanvasTranslate(loadedImage: LoadedImage, settings: CropperSettings, maxSize: Dimensions): { translateH: number; translateV: number } {\n if (settings.transform.translateUnit === 'px') {\n const ratio = this.getRatio(loadedImage, maxSize);\n return {\n translateH: (settings.transform.translateH || 0) * ratio,\n translateV: (settings.transform.translateV || 0) * ratio\n };\n } else {\n return {\n translateH: settings.transform.translateH ? percentage(settings.transform.translateH, loadedImage.transformed.size.width) : 0,\n translateV: settings.transform.translateV ? percentage(settings.transform.translateV, loadedImage.transformed.size.height) : 0\n };\n }\n }\n\n private getRatio(loadedImage: LoadedImage, maxSize: Dimensions): number {\n return loadedImage.transformed.size.width / maxSize.width;\n }\n\n private getImagePosition(loadedImage: LoadedImage, cropper: CropperPosition, settings: CropperSettings, maxSize: Dimensions): CropperPosition {\n const ratio = this.getRatio(loadedImage, maxSize);\n const out: CropperPosition = {\n x1: Math.round(cropper.x1 * ratio),\n y1: Math.round(cropper.y1 * ratio),\n x2: Math.round(cropper.x2 * ratio),\n y2: Math.round(cropper.y2 * ratio)\n };\n\n if (!settings.containWithinAspectRatio) {\n out.x1 = Math.max(out.x1, 0);\n out.y1 = Math.max(out.y1, 0);\n out.x2 = Math.min(out.x2, loadedImage.transformed.size.width);\n out.y2 = Math.min(out.y2, loadedImage.transformed.size.height);\n }\n\n return out;\n }\n\n private getOffsetImagePosition(loadedImage: LoadedImage, cropper: CropperPosition, settings: CropperSettings, maxSize: Dimensions): CropperPosition {\n const canvasRotation = settings.canvasRotation + loadedImage.exifTransform.rotate;\n const ratio = this.getRatio(loadedImage, maxSize);\n let offsetX: number;\n let offsetY: number;\n\n if (canvasRotation % 2) {\n offsetX = (loadedImage.transformed.size.width - loadedImage.original.size.height) / 2;\n offsetY = (loadedImage.transformed.size.height - loadedImage.original.size.width) / 2;\n } else {\n offsetX = (loadedImage.transformed.size.width - loadedImage.original.size.width) / 2;\n offsetY = (loadedImage.transformed.size.height - loadedImage.original.size.height) / 2;\n }\n\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 (!settings.containWithinAspectRatio) {\n out.x1 = Math.max(out.x1, 0);\n out.y1 = Math.max(out.y1, 0);\n out.x2 = Math.min(out.x2, loadedImage.transformed.size.width);\n out.y2 = Math.min(out.y2, loadedImage.transformed.size.height);\n }\n\n return out;\n }\n\n getResizeRatio(width: number, height: number, settings: CropperSettings): number {\n const ratioWidth = settings.resizeToWidth / width;\n const ratioHeight = settings.resizeToHeight / height;\n const ratios = new Array<number>();\n\n if (settings.resizeToWidth > 0) {\n ratios.push(ratioWidth);\n }\n if (settings.resizeToHeight > 0) {\n ratios.push(ratioHeight);\n }\n\n const result = ratios.length === 0 ? 1 : Math.min(...ratios);\n\n if (result > 1 && !settings.onlyScaleDown) {\n return result;\n }\n return Math.min(result, 1);\n }\n\n getQuality(settings: CropperSettings): number {\n return Math.min(1, Math.max(0, settings.imageQuality / 100));\n }\n}\n","/**\n * @license\n * Copyright ASW (A Software World) All Rights Reserved.\n *\n * Use of this source code is governed by an MIT-style license that can be\n * found in the LICENSE file\n */\n\nimport { ElementRef, Injectable } from '@angular/core';\nimport { CropperPosition, CropperSettings, Dimensions, MoveStart } from '@asoftwareworld/form-builder-pro/api';\n\n@Injectable({ providedIn: 'root' })\nexport class CropperPositionService {\n resetCropperPosition(sourceImage: ElementRef, cropperPosition: CropperPosition, settings: CropperSettings, maxSize: Dimensions): void {\n if (!sourceImage?.nativeElement) {\n return;\n }\n if (settings.cropperStaticHeight && settings.cropperStaticWidth) {\n cropperPosition.x1 = 0;\n cropperPosition.x2 = maxSize.width > settings.cropperStaticWidth ? settings.cropperStaticWidth : maxSize.width;\n cropperPosition.y1 = 0;\n cropperPosition.y2 = maxSize.height > settings.cropperStaticHeight ? settings.cropperStaticHeight : maxSize.height;\n } else {\n const cropperWidth = Math.min(settings.cropperScaledMaxWidth, maxSize.width);\n const cropperHeight = Math.min(settings.cropperScaledMaxHeight, maxSize.height);\n if (!settings.maintainAspectRatio) {\n cropperPosition.x1 = 0;\n cropperPosition.x2 = cropperWidth;\n cropperPosition.y1 = 0;\n cropperPosition.y2 = cropperHeight;\n } else if (maxSize.width / settings.aspectRatio < maxSize.height) {\n cropperPosition.x1 = 0;\n cropperPosition.x2 = cropperWidth;\n const cropperHeightWithAspectRatio = cropperWidth / settings.aspectRatio;\n cropperPosition.y1 = (maxSize.height - cropperHeightWithAspectRatio) / 2;\n cropperPosition.y2 = cropperPosition.y1 + cropperHeightWithAspectRatio;\n } else {\n cropperPosition.y1 = 0;\n cropperPosition.y2 = cropperHeight;\n const cropperWidthWithAspectRatio = cropperHeight * settings.aspectRatio;\n cropperPosition.x1 = (maxSize.width - cropperWidthWithAspectRatio) / 2;\n cropperPosition.x2 = cropperPosition.x1 + cropperWidthWithAspectRatio;\n }\n }\n }\n\n move(event: any, moveStart: MoveStart, cropperPosition: CropperPosition) {\n const diffX = this.getClientX(event) - moveStart.clientX;\n const diffY = this.getClientY(event) - moveStart.clientY;\n\n cropperPosition.x1 = moveStart.x1 + diffX;\n cropperPosition.y1 = moveStart.y1 + diffY;\n cropperPosition.x2 = moveStart.x2 + diffX;\n cropperPosition.y2 = moveStart.y2 + diffY;\n }\n\n resize(event: any, moveStart: MoveStart, cropperPosition: CropperPosition, maxSize: Dimensions, settings: CropperSettings): void {\n const moveX = this.getClientX(event) - moveStart.clientX;\n const moveY = this.getClientY(event) - moveStart.clientY;\n switch (moveStart.position) {\n case 'left':\n cropperPosition.x1 = Math.min(Math.max(moveStart.x1 + moveX, cropperPosition.x2 - settings.cropperScaledMaxWidth), cropperPosition.x2 - settings.cropperScaledMinWidth);\n break;\n case 'topleft':\n cropperPosition.x1 = Math.min(Math.max(moveStart.x1 + moveX, cropperPosition.x2 - settings.cropperScaledMaxWidth), cropperPosition.x2 - settings.cropperScaledMinWidth);\n cropperPosition.y1 = Math.min(Math.max(moveStart.y1 + moveY, cropperPosition.y2 - settings.cropperScaledMaxHeight), cropperPosition.y2 - settings.cropperScaledMinHeight);\n break;\n case 'top':\n cropperPosition.y1 = Math.min(Math.max(moveStart.y1 + moveY, cropperPosition.y2 - settings.cropperScaledMaxHeight), cropperPosition.y2 - settings.cropperScaledMinHeight);\n break;\n case 'topright':\n cropperPosition.x2 = Math.max(Math.min(moveStart.x2 + moveX, cropperPosition.x1 + settings.cropperScaledMaxWidth), cropperPosition.x1 + settings.cropperScaledMinWidth);\n cropperPosition.y1 = Math.min(Math.max(moveStart.y1 + moveY, cropperPosition.y2 - settings.cropperScaledMaxHeight), cropperPosition.y2 - settings.cropperScaledMinHeight);\n break;\n case 'right':\n cropperPosition.x2 = Math.max(Math.min(moveStart.x2 + moveX, cropperPosition.x1 + settings.cropperScaledMaxWidth), cropperPosition.x1 + settings.cropperScaledMinWidth);\n break;\n case 'bottomright':\n cropperPosition.x2 = Math.max(Math.min(moveStart.x2 + moveX, cropperPosition.x1 + settings.cropperScaledMaxWidth), cropperPosition.x1 + settings.cropperScaledMinWidth);\n cropperPosition.y2 = Math.max(Math.min(moveStart.y2 + moveY, cropperPosition.y1 + settings.cropperScaledMaxHeight), cropperPosition.y1 + settings.cropperScaledMinHeight);\n break;\n case 'bottom':\n cropperPosition.y2 = Math.max(Math.min(moveStart.y2 + moveY, cropperPosition.y1 + settings.cropperScaledMaxHeight), cropperPosition.y1 + settings.cropperScaledMinHeight);\n break;\n case 'bottomleft':\n cropperPosition.x1 = Math.min(Math.max(moveStart.x1 + moveX, cropperPosition.x2 - settings.cropperScaledMaxWidth), cropperPosition.x2 - settings.cropperScaledMinWidth);\n cropperPosition.y2 = Math.max(Math.min(moveStart.y2 + moveY, cropperPosition.y1 + settings.cropperScaledMaxHeight), cropperPosition.y1 + settings.cropperScaledMinHeight);\n break;\n case 'center':\n const scale = event.scale;\n const newWidth = Math.min(Math.max(settings.cropperScaledMinWidth, Math.abs(moveStart.x2 - moveStart.x1) * scale), settings.cropperScaledMaxWidth);\n const newHeight = Math.min(Math.max(settings.cropperScaledMinHeight, Math.abs(moveStart.y2 - moveStart.y1) * scale), settings.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 > maxSize.width) {\n cropperPosition.x1 -= cropperPosition.x2 - maxSize.width;\n cropperPosition.x2 = maxSize.width;\n }\n if (cropperPosition.y1 < 0) {\n cropperPosition.y2 -= cropperPosition.y1;\n cropperPosition.y1 = 0;\n } else if (cropperPosition.y2 > maxSize.height) {\n cropperPosition.y1 -= cropperPosition.y2 - maxSize.height;\n cropperPosition.y2 = maxSize.height;\n }\n break;\n }\n\n if (settings.maintainAspectRatio) {\n this.checkAspectRatio(moveStart.position!, cropperPosition, maxSize, settings);\n }\n }\n\n checkAspectRatio(position: string, cropperPosition: CropperPosition, maxSize: Dimensions, settings: CropperSettings): void {\n let overflowX = 0;\n let overflowY = 0;\n\n switch (position) {\n case 'top':\n cropperPosition.x2 = cropperPosition.x1 + (cropperPosition.y2 - cropperPosition.y1) * settings.aspectRatio;\n overflowX = Math.max(cropperPosition.x2 - maxSize.width, 0);\n overflowY = Math.max(0 - cropperPosition.y1, 0);\n if (overflowX > 0 || overflowY > 0) {\n cropperPosition.x2 -= overflowY * settings.aspectRatio > overflowX ? overflowY * settings.aspectRatio : overflowX;\n cropperPosition.y1 += overflowY * settings.aspectRatio > overflowX ? overflowY : overflowX / settings.aspectRatio;\n }\n break;\n case 'bottom':\n cropperPosition.x2 = cropperPosition.x1 + (cropperPosition.y2 - cropperPosition.y1) * settings.aspectRatio;\n overflowX = Math.max(cropperPosition.x2 - maxSize.width, 0);\n overflowY = Math.max(cropperPosition.y2 - maxSize.height, 0);\n if (overflowX > 0 || overflowY > 0) {\n cropperPosition.x2 -= overflowY * settings.aspectRatio > overflowX ? overflowY * settings.aspectRatio : overflowX;\n cropperPosition.y2 -= overflowY * settings.aspectRatio > overflowX ? overflowY : overflowX / settings.aspectRatio;\n }\n break;\n case 'topleft':\n cropperPosition.y1 = cropperPosition.y2 - (cropperPosition.x2 - cropperPosition.x1) / settings.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 * settings.aspectRatio > overflowX ? overflowY * settings.aspectRatio : overflowX;\n cropperPosition.y1 += overflowY * settings.aspectRatio > overflowX ? overflowY : overflowX / settings.aspectRatio;\n }\n break;\n case 'topright':\n cropperPosition.y1 = cropperPosition.y2 - (cropperPosition.x2 - cropperPosition.x1) / settings.aspectRatio;\n overflowX = Math.max(cropperPosition.x2 - maxSize.width, 0);\n overflowY = Math.max(0 - cropperPosition.y1, 0);\n if (overflowX > 0 || overflowY > 0) {\n cropperPosition.x2 -= overflowY * settings.aspectRatio > overflowX ? overflowY * settings.aspectRatio : overflowX;\n cropperPosition.y1 += overflowY * settings.aspectRatio > overflowX ? overflowY : overflowX / settings.aspectRatio;\n }\n break;\n case 'right':\n case 'bottomright':\n cropperPosition.y2 = cropperPosition.y1 + (cropperPosition.x2 - cropperPosition.x1) / settings.aspectRatio;\n overflowX = Math.max(cropperPosition.x2 - maxSize.width, 0);\n overflowY = Math.max(cropperPosition.y2 - maxSize.height, 0);\n if (overflowX > 0 || overflowY > 0) {\n cropperPosition.x2 -= overflowY * settings.aspectRatio > overflowX ? overflowY * settings.aspectRatio : overflowX;\n cropperPosition.y2 -= overflowY * settings.aspectRatio > overflowX ? overflowY : overflowX / settings.aspectRatio;\n }\n break;\n case 'left':\n case 'bottomleft':\n cropperPosition.y2 = cropperPosition.y1 + (cropperPosition.x2 - cropperPosition.x1) / settings.aspectRatio;\n overflowX = Math.max(0 - cropperPosition.x1, 0);\n overflowY = Math.max(cropperPosition.y2 - maxSize.height, 0);\n if (overflowX > 0 || overflowY > 0) {\n cropperPosition.x1 += overflowY * settings.aspectRatio > overflowX ? overflowY * settings.aspectRatio : overflowX;\n cropperPosition.y2 -= overflowY * settings.aspectRatio > overflowX ? overflowY : overflowX / settings.aspectRatio;\n }\n break;\n case 'center':\n cropperPosition.x2 = cropperPosition.x1 + (cropperPosition.y2 - cropperPosition.y1) * settings.aspectRatio;\n cropperPosition.y2 = cropperPosition.y1 + (cropperPosition.x2 - cropperPosition.x1) / settings.aspectRatio;\n const overflowX1 = Math.max(0 - cropperPosition.x1, 0);\n const overflowX2 = Math.max(cropperPosition.x2 - maxSize.width, 0);\n const overflowY1 = Math.max(cropperPosition.y2 - 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 * settings.aspectRatio > overflowX1 ? overflowY1 * settings.aspectRatio : overflowX1;\n cropperPosition.x2 -= overflowY2 * settings.aspectRatio > overflowX2 ? overflowY2 * settings.aspectRatio : overflowX2;\n cropperPosition.y1 += overflowY2 * settings.aspectRatio > overflowX2 ? overflowY2 : overflowX2 / settings.aspectRatio;\n cropperPosition.y2 -= overflowY1 * settings.aspectRatio > overflowX1 ? overflowY1 : overflowX1 / settings.aspectRatio;\n }\n break;\n }\n }\n\n getClientX(event: any): number {\n return event.touches?.[0].clientX || event.clientX || 0;\n }\n\n getClientY(event: any): number {\n return event.touches?.[0].clientY || event.clientY || 0;\n }\n}\n","/**\n * @license\n * Copyright ASW (A Software World) All Rights Reserved.\n *\n * Use of this source code is governed by an MIT-style license that can be\n * found in the LICENSE file\n */\n\nimport { Injectable } from '@angular/core';\nimport { CropperSettings, Dimensions, ExifTransform, LoadedImage } from '@asoftwareworld/form-builder-pro/api';\nimport { getTransformationsFromExifData, supportsAutomaticRotation } from '@asoftwareworld/form-builder-pro/utils';\n\ninterface LoadImageArrayBuffer {\n originalImage: HTMLImageElement;\n originalArrayBuffer: ArrayBufferLike;\n originalObjectUrl: string;\n}\n\n@Injectable({ providedIn: 'root' })\nexport class LoadImageService {\n private autoRotateSupported: Promise<boolean> = supportsAutomaticRotation();\n\n loadImageFile(file: File, cropperSettings: CropperSettings): Promise<LoadedImage> {\n return file.arrayBuffer().then((arrayBuffer) => this.checkImageTypeAndLoadImageFromArrayBuffer(arrayBuffer, file.type, cropperSettings));\n }\n\n private checkImageTypeAndLoadImageFromArrayBuffer(arrayBuffer: ArrayBufferLike, imageType: string, cropperSettings: CropperSettings): Promise<LoadedImage> {\n if (!this.isValidImageType(imageType)) {\n return Promise.reject(new Error('Invalid image type'));\n }\n return this.loadImageFromArrayBuffer(arrayBuffer, cropperSettings);\n }\n\n private isValidImageType(type: string): boolean {\n return /image\\/(png|jpg|jpeg|bmp|gif|tiff|webp|x-icon|vnd.microsoft.icon)/.test(type);\n }\n\n loadImageFromURL(url: string, cropperSettings: CropperSettings): Promise<LoadedImage> {\n return fetch(url)\n .then((res) => res.arrayBuffer())\n .then((buffer) => this.loadImageFromArrayBuffer(buffer, cropperSettings));\n }\n\n loadBase64Image(imageBase64: string, cropperSettings: CropperSettings): Promise<LoadedImage> {\n const arrayBuffer = this.base64ToArrayBuffer(imageBase64);\n return this.loadImageFromArrayBuffer(arrayBuffer, cropperSettings);\n }\n\n private base64ToArrayBuffer(imageBase64: string): ArrayBufferLike {\n imageBase64 = imageBase64.replace(/^data\\:([^\\;]+)\\;base64,/gim, '');\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 loadImageFromArrayBuffer(arrayBuffer: ArrayBufferLike, cropperSettings: CropperSettings): Promise<LoadedImage> {\n return new Promise<LoadImageArrayBuffer>((resolve, reject) => {\n const blob = new Blob([arrayBuffer]);\n const objectUrl = URL.createObjectURL(blob);\n const originalImage = new Image();\n originalImage.onload = () =>\n resolve({\n originalImage,\n originalObjectUrl: objectUrl,\n originalArrayBuffer: arrayBuffer\n });\n originalImage.onerror = reject;\n originalImage.src = objectUrl;\n }).then((res: LoadImageArrayBuffer) => this.transformImageFromArrayBuffer(res, cropperSettings));\n }\n\n private async transformImageFromArrayBuffer(res: LoadImageArrayBuffer, cropperSettings: CropperSettings): Promise<LoadedImage> {\n const autoRotate = await this.autoRotateSupported;\n const exifTransform = await 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: {\n width: res.originalImage.naturalWidth,\n height: res.originalImage.naturalHeight\n }\n },\n exifTransform\n };\n return this.transformLoadedImage(loadedImage, cropperSettings);\n }\n\n async transformLoadedImage(loadedImage: Partial<LoadedImage>, cropperSettings: CropperSettings): Promise<LoadedImage> {\n const canvasRotation = cropperSettings.canvasRotation + loadedImage.exifTransform!.rotate;\n const originalSize = {\n width: loadedImage.original!.image.naturalWidth,\n height: loadedImage.original!.image.naturalHeight\n };\n if (canvasRotation === 0 && !loadedImage.exifTransform!.flip && !cropperSettings.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!, cropperSettings);\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(loadedImage.exifTransform!.flip ? -1 : 1, 0, 0, 1, canvas.width / 2, canvas.height / 2);\n ctx?.rotate(Math.PI * (canvasRotation / 2));\n ctx?.drawImage(loadedImage.original!.image, -originalSize.width / 2, -originalSize.height / 2);\n const blob = await new Promise<Blob | null>((resolve) => canvas.toBlob(resolve, cropperSettings.format));\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(originalSize: { width: number; height: number }, exifTransform: ExifTransform, cropperSettings: CropperSettings): Dimensions {\n const canvasRotation = cropperSettings.canvasRotation + exifTransform.rotate;\n if (cropperSettings.containWithinAspectRatio) {\n if (canvasRotation % 2) {\n const minWidthToContain = originalSize.width * cropperSettings.aspectRatio;\n const minHeightToContain = originalSize.height / cropperSettings.aspectRatio;\n return {\n width: Math.max(originalSize.height, minWidthToContain),\n height: Math.max(originalSize.width, minHeightToContain)\n };\n } else {\n const minWidthToContain = originalSize.height * cropperSettings.aspectRatio;\n const minHeightToContain = originalSize.width / cropperSettings.aspectRatio;\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","/**\n * @license\n * Copyright ASW (A Software World) All Rights Reserved.\n *\n * Use of this source code is governed by an MIT-style license that can be\n * found in the LICENSE file\n */\n\nimport { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, EventEmitter, HostBinding, HostListener, Inject, Input, isDevMode, NgZone, OnChanges, OnInit, Optional, Output, SimpleChanges, ViewChild } from '@angular/core';\nimport { DomSanitizer, HAMMER_LOADER, HammerLoader, SafeStyle, SafeUrl } from '@angular/platform-browser';\nimport { CropperPosition, CropperSettings, Dimensions, ImageCroppedEvent, ImageTransform, LoadedImage, MoveStart, MoveTypes, OutputFormat, OutputType } from '@asoftwareworld/form-builder-pro/api';\nimport { getEventForKey, getInvertedPositionForKey, getPositionForKey, HammerStatic } from '@asoftwareworld/form-builder-pro/utils';\nimport { first, fromEvent, merge, takeUntil } from 'rxjs';\nimport { CropService } from './services/crop.service';\nimport { CropperPositionService } from './services/cropper-position.service';\nimport { LoadImageService } from './services/load-image.service';\n\n@Component({\n selector: 'asw-image-crop',\n templateUrl: './image-crop.component.html',\n styleUrls: ['./image-crop.component.scss'],\n changeDetection: ChangeDetectionStrategy.OnPush\n})\nexport class AswImageCropComponent implements OnChanges, OnInit {\n private settings = new CropperSettings();\n private setImageMaxSizeRetries = 0;\n private moveStart?: MoveStart;\n private loadedImage?: LoadedImage;\n private resizedWhileHidden = false;\n\n safeImgDataUrl?: SafeUrl | string;\n safeTransformStyle?: SafeStyle | string;\n marginLeft: SafeStyle | string = '0px';\n maxSize: Dimensions = {\n width: 0,\n height: 0\n };\n moveTypes = MoveTypes;\n imageVisible = false;\n\n @ViewChild('wrapper', { static: true }) wrapper!: ElementRef<HTMLDivElement>;\n @ViewChild('sourceImage', { static: false }) sourceImage!: ElementRef<HTMLDivElement>;\n\n @Input() imageChangedEvent?: any;\n @Input() imageURL?: string;\n @Input() imageBase64?: string;\n @Input() imageFile?: File;\n @Input() imageAltText?: string;\n @Input() cropperFrameAriaLabel = this.settings.cropperFrameAriaLabel;\n @Input() output: 'blob' | 'base64' = this.settings.output;\n @Input() format: OutputFormat = this.settings.format;\n @Input() transform: ImageTransform = {};\n @Input() maintainAspectRatio = this.settings.maintainAspectRatio;\n @Input() aspectRatio = this.settings.aspectRatio;\n @Input() resetCropOnAspectRatioChange = this.settings.resetCropOnAspectRatioChange;\n @Input() resizeToWidth = this.settings.resizeToWidth;\n @Input() resizeToHeight = this.settings.resizeToHeight;\n @Input() cropperMinWidth = this.settings.cropperMinWidth;\n @Input() cropperMinHeight = this.settings.cropperMinHeight;\n @Input() cropperMaxHeight = this.settings.cropperMaxHeight;\n @Input() cropperMaxWidth = this.settings.cropperMaxWidth;\n @Input() cropperStaticWidth = this.settings.cropperStaticWidth;\n @Input() cropperStaticHeight = this.settings.cropperStaticHeight;\n @Input() canvasRotation = this.settings.canvasRotation;\n @Input() initialStepSize = this.settings.initialStepSize;\n @Input() roundCropper = this.settings.roundCropper;\n @Input() onlyScaleDown = this.settings.onlyScaleDown;\n @Input() imageQuality = this.settings.imageQuality;\n @Input() autoCrop = this.settings.autoCrop;\n @Input() backgroundColor = this.settings.backgroundColor;\n @Input() containWithinAspectRatio = this.settings.containWithinAspectRatio;\n @Input() hideResizeSquares = this.settings.hideResizeSquares;\n @Input() allowMoveImage = false;\n @Input() cropper: CropperPosition = {\n x1: -100,\n y1: -100,\n x2: 10000,\n y2: 10000\n };\n @HostBinding('style.text-align')\n @Input()\n alignImage: 'left' | 'center' = this.settings.alignImage;\n @HostBinding('class.disabled')\n @Input()\n disabled = false;\n @HostBinding('class.asw-ix-hidden')\n @Input()\n hidden = false;\n\n @Output() imageCropped = new EventEmitter<ImageCroppedEvent>();\n @Output() startCropImage = new EventEmitter<void>();\n @Output() imageLoaded = new EventEmitter<LoadedImage>();\n @Output() cropperReady = new EventEmitter<Dimensions>();\n @Output() loadImageFailed = new EventEmitter<void>();\n @Output() transformChange = new EventEmitter<ImageTransform>();\n\n constructor(\n private cropService: CropService,\n private cropperPositionService: CropperPositionService,\n private loadImageService: LoadImageService,\n private sanitizer: DomSanitizer,\n private cd: ChangeDetectorRef,\n private zone: NgZone,\n @Optional() @Inject(HAMMER_LOADER) private readonly hammerLoader: HammerLoader | null\n ) {\n this.reset();\n }\n\n ngOnChanges(changes: SimpleChanges): void {\n this.onChangesUpdateSettings(changes);\n this.onChangesInputImage(changes);\n\n if (this.loadedImage?.original.image.complete && (changes['containWithinAspectRatio'] || changes['canvasRotation'])) {\n this.loadImageService\n .transformLoadedImage(this.loadedImage, this.settings)\n .then((res) => this.setLoadedImage(res))\n .catch((err) => this.loadImageError(err));\n }\n if (changes['cropper'] || changes['maintainAspectRatio'] || changes['aspectRatio']) {\n this.setMaxSize();\n this.setCropperScaledMinSize();\n this.setCropperScaledMaxSize();\n if (this.maintainAspectRatio && (this.resetCropOnAspectRatioChange || !this.aspectRatioIsCorrect()) && (changes['maintainAspectRatio'] || changes['aspectRatio'])) {\n this.resetCropperPosition();\n } else if (changes['cropper']) {\n this.checkCropperPosition(false);\n this.doAutoCrop();\n }\n }\n if (changes['transform']) {\n this.transform = this.transform || {};\n this.setCssTransform();\n this.doAutoCrop();\n }\n if (changes['hidden'] && this.resizedWhileHidden && !this.hidden) {\n setTimeout(() => {\n this.onResize();\n this.resizedWhileHidden = false;\n });\n }\n }\n\n private onChangesUpdateSettings(changes: SimpleChanges) {\n this.settings.setOptionsFromChanges(changes);\n\n if (this.settings.cropperStaticHeight && this.settings.cropperStaticWidth) {\n this.hideResizeSquares = true;\n this.settings.setOptions({\n hideResizeSquares: true,\n cropperMinWidth: this.settings.cropperStaticWidth,\n cropperMinHeight: this.settings.cropperStaticHeight,\n cropperMaxHeight: this.settings.cropperStaticHeight,\n cropperMaxWidth: this.settings.cropperStaticWidth,\n maintainAspectRatio: false\n });\n }\n }\n\n private onChangesInputImage(changes: SimpleChanges): void {\n if (changes['imageChangedEvent'] || changes['imageURL'] || changes['imageBase64'] || changes['imageFile']) {\n this.reset();\n }\n if (changes['imageChangedEvent'] && this.isValidImageChangedEvent()) {\n this.loadImageFile(this.imageChangedEvent.target.files[0]);\n }\n if (changes['imageURL'] && this.imageURL) {\n this.loadImageFromURL(this.imageURL);\n }\n if (changes['imageBase64'] && this.imageBase64) {\n this.loadBase64Image(this.imageBase64);\n }\n if (changes['imageFile'] && this.imageFile) {\n this.loadImageFile(this.imageFile);\n }\n }\n\n private isValidImageChangedEvent(): boolean {\n return this.imageChangedEvent?.target?.files?.length > 0;\n }\n\n private setCssTransform() {\n const translateUnit = this.transform?.translateUnit || '%';\n this.safeTransformStyle = this.sanitizer.bypassSecurityTrustStyle(\n `translate(${this.transform.translateH || 0}${translateUnit}, ${this.transform.translateV || 0}${translateUnit})` +\n ' scaleX(' +\n (this.transform.scale || 1) * (this.transform.flipH ? -1 : 1) +\n ')' +\n ' scaleY(' +\n (this.transform.scale || 1) * (this.transform.flipV ? -1 : 1) +\n ')' +\n ' rotate(' +\n (this.transform.rotate || 0) +\n 'deg)'\n );\n }\n\n ngOnInit(): void {\n this.settings.stepSize = this.initialStepSize;\n this.activatePinchGesture();\n }\n\n private reset(): void {\n this.imageVisible = false;\n this.loadedImage = undefined;\n this.safeImgDataUrl = 'data:image/png;base64,iVBORw0KGg' + 'oAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAC0lEQVQYV2NgAAIAAAU' + 'AAarVyFEAAAAASUVORK5CYII=';\n this.moveStart = {\n active: false,\n type: null,\n position: null,\n x1: 0,\n y1: 0,\n x2: 0,\n y2: 0,\n clientX: 0,\n clientY: 0\n };\n this.maxSize = {\n width: 0,\n height: 0\n };\n this.cropper.x1 = -100;\n this.cropper.y1 = -100;\n this.cropper.x2 = 10000;\n this.cropper.y2 = 10000;\n }\n\n private loadImageFile(file: File): void {\n this.loadImageService\n .loadImageFile(file, this.settings)\n .then((res) => this.setLoadedImage(res))\n .catch((err) => this.loadImageError(err));\n }\n\n private loadBase64Image(imageBase64: string): void {\n this.loadImageService\n .loadBase64Image(imageBase64, this.settings)\n .then((res) => this.setLoadedImage(res))\n .catch((err) => this.loadImageError(err));\n }\n\n private loadImageFromURL(url: string): void {\n this.loadImageService\n .loadImageFromURL(url, this.settings)\n .then((res) => this.setLoadedImage(res))\n .catch((err) => this.loadImageError(err));\n }\n\n private setLoadedImage(loadedImage: LoadedImage): void {\n this.loadedImage = loadedImage;\n this.safeImgDataUrl = this.sanitizer.bypassSecurityTrustResourceUrl(loadedImage.transformed.objectUrl);\n this.cd.markForCheck();\n }\n\n public loadImageError(error: any): void {\n console.log(error);\n this.loadImageFailed.emit();\n }\n\n imageLoadedInView(): void {\n if (this.loadedImage != null) {\n this.imageLoaded.emit(this.loadedImage);\n this.setImageMaxSizeRetries = 0;\n setTimeout(() => this.checkImageMaxSizeRecursively());\n }\n }\n\n private checkImageMaxSizeRecursively(): void {\n if (this.setImageMaxSizeRetries > 40) {\n this.loadImageFailed.emit();\n } else if (this.sourceImageLoaded()) {\n this.setMaxSize();\n this.setCropperScaledMinSize();\n this.setCropperScaledMaxSize();\n this.resetCropperPosition();\n this.cropperReady.emit({ ...this.maxSize });\n this.cd.markForCheck();\n } else {\n this.setImageMaxSizeRetries++;\n setTimeout(() => this.checkImageMaxSizeRecursively(), 50);\n }\n }\n\n private sourceImageLoaded(): boolean {\n return this.sourceImage?.nativeElement?.offsetWidth > 0;\n }\n\n @HostListener('window:resize')\n onResize(): void {\n if (!this.loadedImage) {\n return;\n }\n if (this.hidden) {\n this.resizedWhileHidden = true;\n } else {\n const oldMaxSize = { ...this.maxSize };\n this.setMaxSize();\n this.resizeCropperPosition(oldMaxSize);\n this.setCropperScaledMinSize();\n this.setCropperScaledMaxSize();\n }\n }\n\n private async activatePinchGesture() {\n // Loads HammerJS via angular APIs if configured\n await this.hammerLoader?.();\n\n const Hammer = (window as unknown as Window & { Hammer?: HammerStatic })?.['Hammer'] || null;\n\n if (Hammer) {\n const hammer = new Hammer(this.wrapper.nativeElement);\n hammer.get('pinch').set({ enable: true });\n hammer.on('pinchmove', this.onPinch.bind(this));\n hammer.on('pinchend', this.pinchStop.bind(this));\n hammer.on('pinchstart', this.startPinch.bind(this));\n } else if (isDevMode()) {\n console.warn(\"[AswImageCropper] Could not find HammerJS - Pinch Gesture won't work\");\n }\n }\n\n private resizeCropperPosition(oldMaxSize: Dimensions): void {\n if (oldMaxSize.width !== this.maxSize.width || oldMaxSize.height !== this.maxSize.height) {\n this.cropper.x1 = (this.cropper.x1 * this.maxSize.width) / oldMaxSize.width;\n this.cropper.x2 = (this.cropper.x2 * this.maxSize.width) / oldMaxSize.width;\n this.cropper.y1 = (this.cropper.y1 * this.maxSize.height) / oldMaxSize.height;\n this.cropper.y2 = (this.cropper.y2 * this.maxSize.height) / oldMaxSize.height;\n }\n }\n\n resetCropperPosition(): void {\n this.cropperPositionService.resetCropperPosition(this.sourceImage, this.cropper, this.settings, this.maxSize);\n this.doAutoCrop();\n this.imageVisible = true;\n }\n\n keyboardAccess(event: KeyboardEvent) {\n this.changeKeyboardStepSize(event);\n this.keyboardMoveCropper(event);\n }\n\n private changeKeyboardStepSize(event: KeyboardEvent): void {\n const key = +event.key;\n if (key >= 1 && key <= 9) {\n this.settings.stepSize = key;\n }\n }\n\n private keyboardMoveCropper(event: any) {\n const keyboardWhiteList: string[] = ['ArrowUp', 'ArrowDown', 'ArrowRight', 'ArrowLeft'];\n if (!keyboardWhiteList.includes(event.key)) {\n return;\n }\n const moveType = event.shiftKey ? MoveTypes.Resize : MoveTypes.Move;\n const position = event.altKey ? getInvertedPositionForKey(event.key) : getPositionForKey(event.key);\n const moveEvent = getEventForKey(event.key, this.settings.stepSize);\n event.preventDefault();\n event.stopPropagation();\n this.startMove({ clientX: 0, clientY: 0 }, moveType, position);\n this.handleMouseMove(moveEvent);\n this.handleMouseUp();\n }\n\n startMove(event: any, moveType: MoveTypes, position: string | null = null): void {\n if (this.disabled || (this.moveStart?.active && this.moveStart?.type === MoveTypes.Pinch) || (moveType === MoveTypes.Drag && !this.allowMoveImage)) {\n return;\n }\n if (event.preventDefault) {\n event.preventDefault();\n }\n this.moveStart = {\n active: true,\n type: moveType,\n position,\n transform: { ...this.transform },\n clientX: this.cropperPositionService.getClientX(event),\n clientY: this.cropperPositionService.getClientY(event),\n ...this.cropper\n };\n this.initMouseMove();\n }\n\n private initMouseMove(): void {\n merge(fromEvent(document, 'mousemove'), fromEvent(document, 'touchmove'))\n .pipe(takeUntil(merge(fromEvent(document, 'mouseup'), fromEvent(document, 'touchend')).pipe(first())))\n .subscribe({\n next: (event) =>\n this.zone.run(() => {\n this.handleMouseMove(event);\n this.cd.markForCheck();\n }),\n complete: () =>\n this.zone.run(() => {\n this.handleMouseUp();\n this.cd.markForCheck();\n })\n });\n }\n\n startPinch(event: any) {\n if (!this.safeImgDataUrl) {\n return;\n }\n if (event.preventDefault) {\n event.preventDefault();\n }\n this.moveStart = {\n active: true,\n type: MoveTypes.Pinch,\n position: 'center',\n clientX: this.cropper.x1 + (this.cropper.x2 - this.cropper.x1) / 2,\n clientY: this.cropper.y1 + (this.cropper.y2 - this.cropper.y1) / 2,\n ...this.cropper\n };\n }\n\n private handleMouseMove(event: any): void {\n if (this.moveStart!.active) {\n if (event.stopPropagation) {\n event.stopPropagation();\n }\n if (event.preventDefault) {\n event.preventDefault();\n }\n if (this.moveStart!.type === MoveTypes.Move) {\n this.cropperPositionService.move(event, this.moveStart!, this.cropper);\n this.checkCropperPosition(true);\n } else if (this.moveStart!.type === MoveTypes.Resize) {\n if (!this.cropperStaticWidth && !this.cropperStaticHeight) {\n this.cropperPositionService.resize(event, this.moveStart!, this.cropper, this.maxSize, this.settings);\n }\n this.checkCropperPosition(false);\n } else if (this.moveStart!.type === MoveTypes.Drag) {\n const diffX = this.cropperPositionService.getClientX(event) - this.moveStart!.clientX;\n const diffY = this.cropperPositionService.getClientY(event) - this.moveStart!.clientY;\n this.transform = {\n ...this.transform,\n translateH: (this.moveStart!.transform?.translateH || 0) + diffX,\n translateV: (this.moveStart!.transform?.translateV || 0) + diffY\n };\n this.setCssTransform();\n }\n }\n }\n\n onPinch(event: any) {\n if (this.moveStart!.active) {\n if (event.stopPropagation) {\n event.stopPropagation();\n }\n if (event.preventDefault) {\n event.preventDefault();\n }\n if (this.moveStart!.type === MoveTypes.Pinch) {\n this.cropperPositionService.resize(event, this.moveStart!, this.cropper, this.maxSize, this.settings);\n this.checkCropperPosition(false);\n }\n this.cd.markForCheck();\n }\n }\n\n private setMaxSize(): void {\n if (this.sourceImage) {\n const sourceImageStyle = getComputedStyle(this.sourceImage.nativeElement);\n this.maxSize.width = parseFloat(sourceImageStyle.width);\n this.maxSize.height = parseFloat(sourceImageStyle.height);\n this.marginLeft = this.sanitizer.bypassSecurityTrustStyle('calc(50% - ' + this.maxSize.width / 2 + 'px)');\n }\n }\n\n private setCropperScaledMinSize(): void {\n if (this.loadedImage?.transformed?.image) {\n this.setCropperScaledMinWidth();\n this.setCropperScaledMinHeight();\n } else {\n this.settings.cropperScaledMinWidth = 20;\n this.settings.cropperScaledMinHeight = 20;\n }\n }\n\n private setCropperScaledMinWidth(): void {\n this.settings.cropperScale