Files
petfeed/src/lib/crop-image.ts
T
admin 6fd5f2dff8 feat(pet): circular avatar cropping with zoom before upload
Adds an AvatarCropDialog step (react-easy-crop, cropShape="round")
between file selection and the presigned-URL upload in AvatarUpload:
the user crops/zooms a circular region, which is re-encoded via canvas
to the original file's mime type before the existing upload flow runs.
File-size/type validation moved from uploadFile into the new
selectFile gate that opens the dialog.

Gitea #13
2026-08-12 12:52:32 +02:00

55 lines
1.3 KiB
TypeScript

import type { Area } from "react-easy-crop";
function loadImage(url: string): Promise<HTMLImageElement> {
return new Promise((resolve, reject) => {
const image = new Image();
image.addEventListener("load", () => resolve(image));
image.addEventListener("error", reject);
image.crossOrigin = "anonymous";
image.src = url;
});
}
/**
* Renders the cropped region of an image onto an off-screen canvas and
* returns it as a Blob — the standard react-easy-crop output recipe.
*/
export async function getCroppedImageBlob(
imageSrc: string,
cropAreaPixels: Area,
mimeType: string
): Promise<Blob> {
const image = await loadImage(imageSrc);
const canvas = document.createElement("canvas");
canvas.width = cropAreaPixels.width;
canvas.height = cropAreaPixels.height;
const ctx = canvas.getContext("2d");
if (!ctx) {
throw new Error("Canvas 2D context not available");
}
ctx.drawImage(
image,
cropAreaPixels.x,
cropAreaPixels.y,
cropAreaPixels.width,
cropAreaPixels.height,
0,
0,
cropAreaPixels.width,
cropAreaPixels.height
);
return new Promise((resolve, reject) => {
canvas.toBlob(
(blob) => {
if (blob) resolve(blob);
else reject(new Error("Canvas produced an empty blob"));
},
mimeType,
0.92
);
});
}