Files
cbsnexora/src/app/clients/client-stepper/client-kyc-step/image-crop-dialog/image-crop-dialog.component.ts
T
Nitesh YadavandClaude Sonnet 4.6 d367e76104 merging code
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-20 14:26:25 +05:30

279 lines
7.7 KiB
TypeScript

/**
* Copyright since 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
import { Component, inject, ViewChild, ElementRef, AfterViewInit } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef, MatDialogContent, MatDialogActions } from '@angular/material/dialog';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
export interface ImageCropData {
file: File;
title: string;
aspectRatio: number; // width/height, e.g. 1 for square, 1.5 for 3:2
maxWidth: number;
maxHeight: number;
}
@Component({
selector: 'mifosx-image-crop-dialog',
templateUrl: './image-crop-dialog.component.html',
styleUrls: ['./image-crop-dialog.component.scss'],
imports: [
...STANDALONE_SHARED_IMPORTS,
FaIconComponent,
MatDialogContent,
MatDialogActions
]
})
export class ImageCropDialogComponent implements AfterViewInit {
@ViewChild('sourceCanvas') sourceCanvas!: ElementRef<HTMLCanvasElement>;
@ViewChild('previewCanvas') previewCanvas!: ElementRef<HTMLCanvasElement>;
imageLoaded = false;
private img = new Image();
private imgWidth = 0;
private imgHeight = 0;
// Crop box (relative to displayed image)
cropX = 0;
cropY = 0;
cropW = 200;
cropH = 200;
// Drag state
private dragging = false;
private dragStartX = 0;
private dragStartY = 0;
private dragStartCropX = 0;
private dragStartCropY = 0;
// Resize state
private resizing = false;
// Canvas display dimensions
private canvasDisplayW = 0;
private canvasDisplayH = 0;
private scaleX = 1;
private scaleY = 1;
zoom = 1;
dialogRef = inject(MatDialogRef<ImageCropDialogComponent>);
data = inject<ImageCropData>(MAT_DIALOG_DATA);
ngAfterViewInit() {
const reader = new FileReader();
reader.onload = (e: any) => {
this.img.onload = () => {
this.imageLoaded = true;
setTimeout(() => this.initCanvas());
};
this.img.src = e.target.result;
};
reader.readAsDataURL(this.data.file);
}
private initCanvas() {
const canvas = this.sourceCanvas.nativeElement;
const ctx = canvas.getContext('2d')!;
const container = canvas.parentElement!;
const maxW = container.clientWidth || 500;
const maxH = 400;
// Scale image to fit container
const ratio = Math.min(maxW / this.img.width, maxH / this.img.height, 1);
this.canvasDisplayW = Math.floor(this.img.width * ratio);
this.canvasDisplayH = Math.floor(this.img.height * ratio);
this.imgWidth = this.img.width;
this.imgHeight = this.img.height;
canvas.width = this.canvasDisplayW;
canvas.height = this.canvasDisplayH;
this.scaleX = this.img.width / this.canvasDisplayW;
this.scaleY = this.img.height / this.canvasDisplayH;
// Init crop box centered
const cropSize = Math.min(this.canvasDisplayW, this.canvasDisplayH) * 0.6;
if (this.data.aspectRatio >= 1) {
this.cropW = cropSize;
this.cropH = cropSize / this.data.aspectRatio;
} else {
this.cropH = cropSize;
this.cropW = cropSize * this.data.aspectRatio;
}
this.cropX = (this.canvasDisplayW - this.cropW) / 2;
this.cropY = (this.canvasDisplayH - this.cropH) / 2;
this.drawCanvas();
this.updatePreview();
}
private drawCanvas() {
const canvas = this.sourceCanvas.nativeElement;
const ctx = canvas.getContext('2d')!;
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(this.img, 0, 0, canvas.width, canvas.height);
// Dark overlay outside crop
ctx.fillStyle = 'rgba(0,0,0,0.5)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Clear crop area
ctx.clearRect(this.cropX, this.cropY, this.cropW, this.cropH);
ctx.drawImage(
this.img,
this.cropX * this.scaleX,
this.cropY * this.scaleY,
this.cropW * this.scaleX,
this.cropH * this.scaleY,
this.cropX,
this.cropY,
this.cropW,
this.cropH
);
// Crop border
ctx.strokeStyle = '#1353a0';
ctx.lineWidth = 2;
ctx.strokeRect(this.cropX, this.cropY, this.cropW, this.cropH);
// Corner handles
const hs = 8;
ctx.fillStyle = '#1353a0';
const corners = [
[
this.cropX,
this.cropY
],
[
this.cropX + this.cropW,
this.cropY
],
[
this.cropX,
this.cropY + this.cropH
],
[
this.cropX + this.cropW,
this.cropY + this.cropH
]
];
corners.forEach(
([
x,
y
]) => {
ctx.fillRect(x - hs / 2, y - hs / 2, hs, hs);
}
);
}
private updatePreview() {
if (!this.previewCanvas) return;
const pCanvas = this.previewCanvas.nativeElement;
const pCtx = pCanvas.getContext('2d')!;
const pw = this.data.maxWidth || 150;
const ph = this.data.maxHeight || 150;
pCanvas.width = pw;
pCanvas.height = ph;
pCtx.clearRect(0, 0, pw, ph);
pCtx.drawImage(
this.img,
this.cropX * this.scaleX,
this.cropY * this.scaleY,
this.cropW * this.scaleX,
this.cropH * this.scaleY,
0,
0,
pw,
ph
);
}
onMouseDown(event: MouseEvent) {
const rect = this.sourceCanvas.nativeElement.getBoundingClientRect();
const x = event.clientX - rect.left;
const y = event.clientY - rect.top;
// Check if near bottom-right corner for resize
const cornerX = this.cropX + this.cropW;
const cornerY = this.cropY + this.cropH;
if (Math.abs(x - cornerX) < 12 && Math.abs(y - cornerY) < 12) {
this.resizing = true;
return;
}
// Check if inside crop box for drag
if (x >= this.cropX && x <= this.cropX + this.cropW && y >= this.cropY && y <= this.cropY + this.cropH) {
this.dragging = true;
this.dragStartX = x;
this.dragStartY = y;
this.dragStartCropX = this.cropX;
this.dragStartCropY = this.cropY;
}
}
onMouseMove(event: MouseEvent) {
if (!this.dragging && !this.resizing) return;
const rect = this.sourceCanvas.nativeElement.getBoundingClientRect();
const x = event.clientX - rect.left;
const y = event.clientY - rect.top;
if (this.dragging) {
this.cropX = Math.max(0, Math.min(this.canvasDisplayW - this.cropW, this.dragStartCropX + (x - this.dragStartX)));
this.cropY = Math.max(0, Math.min(this.canvasDisplayH - this.cropH, this.dragStartCropY + (y - this.dragStartY)));
}
if (this.resizing) {
const newW = Math.max(40, Math.min(this.canvasDisplayW - this.cropX, x - this.cropX));
const newH = newW / this.data.aspectRatio;
if (this.cropY + newH <= this.canvasDisplayH) {
this.cropW = newW;
this.cropH = newH;
}
}
this.drawCanvas();
this.updatePreview();
}
onMouseUp() {
this.dragging = false;
this.resizing = false;
}
confirm() {
// Create cropped image as blob
const offscreen = document.createElement('canvas');
offscreen.width = this.data.maxWidth || 300;
offscreen.height = this.data.maxHeight || 300;
const ctx = offscreen.getContext('2d')!;
ctx.drawImage(
this.img,
this.cropX * this.scaleX,
this.cropY * this.scaleY,
this.cropW * this.scaleX,
this.cropH * this.scaleY,
0,
0,
offscreen.width,
offscreen.height
);
offscreen.toBlob((blob) => {
if (blob) {
const croppedFile = new File([blob], this.data.file.name, { type: 'image/png' });
this.dialogRef.close({ croppedFile, previewUrl: offscreen.toDataURL('image/png') });
}
}, 'image/png');
}
}