fix: duplication

This commit is contained in:
Moyasee
2025-11-04 22:22:02 +02:00
parent e04a94d10d
commit 363e52cdb6
2 changed files with 243 additions and 124 deletions

View File

@@ -58,9 +58,15 @@ export function ImageCropper({
const containerRect = container.getBoundingClientRect(); const containerRect = container.getBoundingClientRect();
if (containerRect.width === 0 || containerRect.height === 0) return null; if (containerRect.width === 0 || containerRect.height === 0) return null;
const computedStyle = globalThis.getComputedStyle(container);
const paddingLeft = Number.parseFloat(computedStyle.paddingLeft) || 0;
const paddingRight = Number.parseFloat(computedStyle.paddingRight) || 0;
const paddingTop = Number.parseFloat(computedStyle.paddingTop) || 0;
const paddingBottom = Number.parseFloat(computedStyle.paddingBottom) || 0;
return { return {
maxWidth: containerRect.width - 40, maxWidth: containerRect.width - paddingLeft - paddingRight,
maxHeight: containerRect.height - 200, maxHeight: containerRect.height - paddingTop - paddingBottom,
}; };
}, []); }, []);
@@ -212,16 +218,17 @@ export function ImageCropper({
}; };
}; };
const adjustAspectRatio = useCallback( const enforceAspectRatio = (
(width: number, height: number, ratio: number) => { width: number,
const currentRatio = width / height; height: number,
if (currentRatio > ratio) { ratio: number
return { width, height: width / ratio }; ): { width: number; height: number } => {
} const currentRatio = width / height;
return { width: height * ratio, height }; if (currentRatio > ratio) {
}, return { width, height: width / ratio };
[] }
); return { width: height * ratio, height };
};
const calculateMinSize = useCallback( const calculateMinSize = useCallback(
(effectiveAspectRatio: number | undefined) => { (effectiveAspectRatio: number | undefined) => {
@@ -233,39 +240,78 @@ export function ImageCropper({
[minCropSize] [minCropSize]
); );
const getImageWrapperBounds = useCallback(() => {
const imageWrapper = imageRef.current?.parentElement;
if (!imageWrapper) return { width: 0, height: 0 };
const rect = imageWrapper.getBoundingClientRect();
return { width: rect.width, height: rect.height };
}, []);
const constrainCropArea = useCallback( const constrainCropArea = useCallback(
(area: CropArea): CropArea => { (area: CropArea): CropArea => {
const displaySize = getDisplaySize(); const displaySize = getDisplaySize();
const wrapperBounds = getImageWrapperBounds();
const actualBounds = {
width:
wrapperBounds.width > 0 ? wrapperBounds.width : displaySize.width,
height:
wrapperBounds.height > 0 ? wrapperBounds.height : displaySize.height,
};
let { x, y, width, height } = area; let { x, y, width, height } = area;
const effectiveAspectRatio = circular ? 1 : aspectRatio; const effectiveAspectRatio = circular ? 1 : aspectRatio;
if (effectiveAspectRatio) {
({ width, height } = adjustAspectRatio(
width,
height,
effectiveAspectRatio
));
}
const minSize = calculateMinSize(effectiveAspectRatio); const minSize = calculateMinSize(effectiveAspectRatio);
width = Math.max(minSize, Math.min(width, displaySize.width));
height = Math.max(minSize, Math.min(height, displaySize.height));
if (effectiveAspectRatio) { if (effectiveAspectRatio) {
({ width, height } = adjustAspectRatio( ({ width, height } = enforceAspectRatio(
width, width,
height, height,
effectiveAspectRatio effectiveAspectRatio
)); ));
const maxWidth = Math.min(
actualBounds.width,
actualBounds.height * effectiveAspectRatio
);
const maxHeight = Math.min(
actualBounds.height,
actualBounds.width / effectiveAspectRatio
);
width = Math.max(minSize, Math.min(width, maxWidth));
height = Math.max(minSize, Math.min(height, maxHeight));
({ width, height } = enforceAspectRatio(
width,
height,
effectiveAspectRatio
));
width = Math.min(width, actualBounds.width);
height = Math.min(height, actualBounds.height);
({ width, height } = enforceAspectRatio(
width,
height,
effectiveAspectRatio
));
} else {
width = Math.max(minSize, Math.min(width, actualBounds.width));
height = Math.max(minSize, Math.min(height, actualBounds.height));
} }
x = Math.max(0, Math.min(x, displaySize.width - width)); x = Math.max(0, Math.min(x, actualBounds.width - width));
y = Math.max(0, Math.min(y, displaySize.height - height)); y = Math.max(0, Math.min(y, actualBounds.height - height));
return { x, y, width, height }; return { x, y, width, height };
}, },
[getDisplaySize, circular, aspectRatio, adjustAspectRatio, calculateMinSize] [
getDisplaySize,
getImageWrapperBounds,
circular,
aspectRatio,
calculateMinSize,
]
); );
const getRelativeCoordinates = ( const getRelativeCoordinates = (
@@ -275,10 +321,10 @@ export function ImageCropper({
if (!imageWrapper) return null; if (!imageWrapper) return null;
const rect = imageWrapper.getBoundingClientRect(); const rect = imageWrapper.getBoundingClientRect();
return { const x = e.clientX - rect.left;
x: e.clientX - rect.left, const y = e.clientY - rect.top;
y: e.clientY - rect.top,
}; return { x, y };
}; };
const handleDrag = useCallback( const handleDrag = useCallback(
@@ -393,11 +439,48 @@ export function ImageCropper({
resizeHandle: string, resizeHandle: string,
aspectRatio: number aspectRatio: number
) => { ) => {
const { width: adjustedWidth, height: adjustedHeight } = adjustAspectRatio( const deltaX = Math.abs(newWidth - cropStart.width);
newWidth, const deltaY = Math.abs(newHeight - cropStart.height);
newHeight,
aspectRatio let adjustedWidth: number;
let adjustedHeight: number;
if (deltaX > deltaY) {
adjustedWidth = newWidth;
adjustedHeight = newWidth / aspectRatio;
} else {
adjustedHeight = newHeight;
adjustedWidth = newHeight * aspectRatio;
}
const wrapperBounds = getImageWrapperBounds();
const displaySize = getDisplaySize();
const actualBounds = {
width: wrapperBounds.width > 0 ? wrapperBounds.width : displaySize.width,
height:
wrapperBounds.height > 0 ? wrapperBounds.height : displaySize.height,
};
const maxWidth = Math.min(
actualBounds.width,
actualBounds.height * aspectRatio
); );
const maxHeight = Math.min(
actualBounds.height,
actualBounds.width / aspectRatio
);
adjustedWidth = Math.min(adjustedWidth, maxWidth);
adjustedHeight = Math.min(adjustedHeight, maxHeight);
const finalRatio = adjustedWidth / adjustedHeight;
if (Math.abs(finalRatio - aspectRatio) > 0.001) {
if (finalRatio > aspectRatio) {
adjustedHeight = adjustedWidth / aspectRatio;
} else {
adjustedWidth = adjustedHeight * aspectRatio;
}
}
const { x, y } = adjustPositionForHandle( const { x, y } = adjustPositionForHandle(
resizeHandle, resizeHandle,
@@ -575,7 +658,15 @@ export function ImageCropper({
return ( return (
<div className="image-cropper"> <div className="image-cropper">
<div className="image-cropper__container" ref={containerRef}> <div className="image-cropper__container" ref={containerRef}>
<div className="image-cropper__image-wrapper"> <div
className="image-cropper__image-wrapper"
style={{
width: `${displaySize.width}px`,
height: `${displaySize.height}px`,
maxWidth: "100%",
maxHeight: "100%",
}}
>
{imageLoaded && ( {imageLoaded && (
<img <img
ref={imageRef} ref={imageRef}

View File

@@ -1,20 +1,17 @@
/** import type { CropArea } from "@renderer/components";
* Crops an image using HTML5 Canvas API
* @param imagePath - Path to the image file type DrawImageCallback = (
* @param cropArea - Crop area coordinates and dimensions ctx: CanvasRenderingContext2D,
* @param outputFormat - Output image format (default: 'image/png') img: HTMLImageElement,
* @returns Promise resolving to cropped image as Uint8Array cropArea: CropArea
*/ ) => void;
export async function cropImage(
const loadImageAndProcess = async (
imagePath: string, imagePath: string,
cropArea: { cropArea: CropArea,
x: number; outputFormat: string,
y: number; drawCallback: DrawImageCallback
width: number; ): Promise<Uint8Array> => {
height: number;
},
outputFormat: string = "image/png"
): Promise<Uint8Array> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const img = new Image(); const img = new Image();
@@ -27,35 +24,25 @@ export async function cropImage(
return; return;
} }
canvas.width = cropArea.width; drawCallback(ctx, img, cropArea);
canvas.height = cropArea.height;
ctx.drawImage( const convertBlobToUint8Array = async (
img, blob: Blob
cropArea.x, ): Promise<Uint8Array> => {
cropArea.y, const buffer = await blob.arrayBuffer();
cropArea.width, return new Uint8Array(buffer);
cropArea.height, };
0,
0,
cropArea.width,
cropArea.height
);
canvas.toBlob( const handleBlob = (blob: Blob | null) => {
(blob) => { if (!blob) {
if (!blob) { reject(new Error("Failed to create blob from canvas"));
reject(new Error("Failed to create blob from canvas")); return;
return; }
}
blob.arrayBuffer().then((buffer) => { convertBlobToUint8Array(blob).then(resolve).catch(reject);
resolve(new Uint8Array(buffer)); };
});
}, canvas.toBlob(handleBlob, outputFormat, 0.95);
outputFormat,
0.95
);
}; };
img.onerror = () => { img.onerror = () => {
@@ -64,6 +51,73 @@ export async function cropImage(
img.src = imagePath.startsWith("local:") ? imagePath : `local:${imagePath}`; img.src = imagePath.startsWith("local:") ? imagePath : `local:${imagePath}`;
}); });
};
const setCanvasDimensions = (
canvas: HTMLCanvasElement,
width: number,
height: number
): void => {
canvas.width = width;
canvas.height = height;
};
type DrawImageParams = {
sourceX: number;
sourceY: number;
sourceWidth: number;
sourceHeight: number;
destWidth: number;
destHeight: number;
};
const drawCroppedImage = (
ctx: CanvasRenderingContext2D,
img: HTMLImageElement,
params: DrawImageParams
): void => {
ctx.drawImage(
img,
params.sourceX,
params.sourceY,
params.sourceWidth,
params.sourceHeight,
0,
0,
params.destWidth,
params.destHeight
);
};
/**
* Crops an image using HTML5 Canvas API
* @param imagePath - Path to the image file
* @param cropArea - Crop area coordinates and dimensions
* @param outputFormat - Output image format (default: 'image/png')
* @returns Promise resolving to cropped image as Uint8Array
*/
export async function cropImage(
imagePath: string,
cropArea: CropArea,
outputFormat: string = "image/png"
): Promise<Uint8Array> {
return loadImageAndProcess(
imagePath,
cropArea,
outputFormat,
(ctx, img, area) => {
const canvas = ctx.canvas;
setCanvasDimensions(canvas, area.width, area.height);
drawCroppedImage(ctx, img, {
sourceX: area.x,
sourceY: area.y,
sourceWidth: area.width,
sourceHeight: area.height,
destWidth: area.width,
destHeight: area.height,
});
}
);
} }
/** /**
@@ -75,56 +129,30 @@ export async function cropImage(
*/ */
export async function cropImageToCircle( export async function cropImageToCircle(
imagePath: string, imagePath: string,
cropArea: { cropArea: CropArea,
x: number;
y: number;
width: number;
height: number;
},
outputFormat: string = "image/png" outputFormat: string = "image/png"
): Promise<Uint8Array> { ): Promise<Uint8Array> {
return new Promise((resolve, reject) => { return loadImageAndProcess(
const img = new Image(); imagePath,
cropArea,
img.onload = () => { outputFormat,
const canvas = document.createElement("canvas"); (ctx, img, area) => {
const ctx = canvas.getContext("2d"); const size = Math.min(area.width, area.height);
const canvas = ctx.canvas;
if (!ctx) { setCanvasDimensions(canvas, size, size);
reject(new Error("Failed to get canvas context"));
return;
}
const size = Math.min(cropArea.width, cropArea.height);
canvas.width = size;
canvas.height = size;
ctx.beginPath(); ctx.beginPath();
ctx.arc(size / 2, size / 2, size / 2, 0, Math.PI * 2); ctx.arc(size / 2, size / 2, size / 2, 0, Math.PI * 2);
ctx.clip(); ctx.clip();
ctx.drawImage(img, cropArea.x, cropArea.y, size, size, 0, 0, size, size); drawCroppedImage(ctx, img, {
sourceX: area.x,
canvas.toBlob( sourceY: area.y,
(blob) => { sourceWidth: size,
if (!blob) { sourceHeight: size,
reject(new Error("Failed to create blob from canvas")); destWidth: size,
return; destHeight: size,
} });
}
blob.arrayBuffer().then((buffer) => { );
resolve(new Uint8Array(buffer));
});
},
outputFormat,
0.95
);
};
img.onerror = () => {
reject(new Error("Failed to load image"));
};
img.src = imagePath.startsWith("local:") ? imagePath : `local:${imagePath}`;
});
} }