Merge branch 'cropper-patch' into 'develop'

fix: 🐛 modification of the image generation from ultimate cropper

See merge request ldc-web/projets/ldc!61
This commit is contained in:
karl.muller 2025-11-19 16:43:10 +00:00
commit 057f9980b5
2 changed files with 57 additions and 111 deletions

View file

@ -9,7 +9,7 @@ import { Label } from "@/components/ui/label";
import { Button } from "@/components/ui/button";
// * Other
import InputError from "@/back/Components/InputError";
import ImageCropperLogic from "@/back/Components/Inputs/ImageCropper/ImageCropperLogic";
import ImageCropper from "@/back/Components/Inputs/ImageCropper";
import useTrans from "@/hooks/use-translations";
import str from "@/hooks/use-string";
import { useToast } from "@/hooks/use-toast";
@ -53,7 +53,7 @@ export default function ImageCropperWrapper(props: ImageCropperWrapperProps) {
const { url } = usePage().props.appEnv as AppEnv;
const [isReset, setIsReset] = useState<boolean>(false);
const finalFileName = `${fileName}.png`;
console.log(value)
const [selectedFile, setSelectedFile] = useState<FileWithPreview | null>(
() => {
// When there is an image selected but not stored yet.
@ -234,7 +234,7 @@ export default function ImageCropperWrapper(props: ImageCropperWrapperProps) {
{selectedFile ? (
<div className="relative">
{inputType === enumDynamicFieldTypes.image.value && (
<ImageCropperLogic
<ImageCropper
dialogOpen={isDialogOpen}
value={value}
reset={reset}

View file

@ -64,7 +64,7 @@ import { cn } from "@/lib/utils";
};
/* eslint-disable no-unused-vars, @typescript-eslint/no-unused-vars */
interface ImageCropperLogicProps {
interface ImageCropperProps {
dialogOpen: boolean;
name: string;
value?: string | File | null;
@ -80,7 +80,7 @@ import { cn } from "@/lib/utils";
}
/* eslint-enable no-unused-vars, @typescript-eslint/no-unused-vars */
export default function ImageCropperLogic({
export default function ImageCropper({
dialogOpen,
setDialogOpen,
selectedFile,
@ -93,7 +93,7 @@ import { cn } from "@/lib/utils";
isReset,
url,
finalFileName,
}: ImageCropperLogicProps) {
}: ImageCropperProps) {
const { errors } = usePage().props;
const aspect = 1;
@ -135,104 +135,59 @@ import { cn } from "@/lib/utils";
* @param cropLocalization Crop localization in the image.
* @return string
*/
function getCroppedImg(
image: HTMLImageElement,
cropLocalization: PixelCrop,
): string {
function getCroppedImg(image: HTMLImageElement, crop: PixelCrop): string {
const canvas = document.createElement("canvas");
// NOUVEAU CALCUL: l'image a une taille de base (scaleFactor) + zoom appliqué via transform
const baseDisplayedWidth = originalDimensions.width * scaleFactor;
const baseDisplayedHeight = originalDimensions.height * scaleFactor;
const effectiveZoom = 1 + zoom;
// Calculer l'échelle vers l'image naturelle
const scaleToNatural = image.naturalWidth / baseDisplayedWidth;
// Les coordonnées de crop doivent être ajustées pour le zoom transform
// Le zoom étant centré, on doit calculer le décalage
const zoomOffsetX =
(baseDisplayedWidth * effectiveZoom - baseDisplayedWidth) / 2;
const zoomOffsetY =
(baseDisplayedHeight * effectiveZoom - baseDisplayedHeight) / 2;
// Ajuster les coordonnées par rapport à la position ET au zoom
const adjustedCropX =
((cropLocalization.x - imagePosition.x + zoomOffsetX) / effectiveZoom) *
scaleToNatural;
const adjustedCropY =
((cropLocalization.y - imagePosition.y + zoomOffsetY) / effectiveZoom) *
scaleToNatural;
const cropWidth = (cropLocalization.width / effectiveZoom) * scaleToNatural;
const cropHeight =
(cropLocalization.height / effectiveZoom) * scaleToNatural;
// Calculer l'angle en radians
const rad = (rotation * Math.PI) / 180;
// Créer un canvas temporaire assez grand pour contenir toute la rotation
const tmpCanvas = document.createElement("canvas");
const tmpCtx = tmpCanvas.getContext("2d")!;
tmpCanvas.width = image.naturalWidth;
tmpCanvas.height = image.naturalHeight;
tmpCtx.save();
// Appliquer les filtres CSS au contexte du canvas (sauf blur qui ne marche pas bien)
tmpCtx.filter = `brightness(${brightness}%) contrast(${contrast}%) saturate(${saturation}%) sepia(${sepia}%) grayscale(${grayscale}%)`;
// Placer le centre de rotation
tmpCtx.translate(tmpCanvas.width / 2, tmpCanvas.height / 2);
// Appliquer seulement le flip (le scale est maintenant dans les dimensions)
tmpCtx.scale(flipX ? -1 : 1, flipY ? -1 : 1);
// Appliquer rotation
const getRotation = () => {
if (flipX && flipY) return rad;
if (flipX || flipY) return -rad;
return rad;
};
tmpCtx.rotate(getRotation());
// Dessiner l'image au centre avec les filtres appliqués
tmpCtx.drawImage(image, -image.naturalWidth / 2, -image.naturalHeight / 2);
tmpCtx.restore();
// Maintenant, extraire la zone cropée depuis le canvas temporaire
const imageData = tmpCtx.getImageData(
adjustedCropX,
adjustedCropY,
cropWidth,
cropHeight,
);
// IMPORTANT: Redimensionner à la taille finale souhaitée (validationRules)
canvas.width = validationRules.width;
canvas.height = validationRules.height;
const ctx = canvas.getContext("2d")!;
// Créer un canvas temporaire avec l'image cropée
const tempCanvas = document.createElement("canvas");
const tempCtx = tempCanvas.getContext("2d")!;
tempCanvas.width = cropWidth;
tempCanvas.height = cropHeight;
tempCtx.putImageData(imageData, 0, 0);
// Redimensionner l'image cropée aux dimensions finales souhaitées
ctx.drawImage(
tempCanvas,
0,
0,
cropWidth,
cropHeight, // Source
0,
0,
validationRules.width,
validationRules.height, // Destination
const tmpCanvas = document.createElement("canvas");
tmpCanvas.width = maxContainerWidth;
tmpCanvas.height = maxContainerHeight;
const tmpCtx = tmpCanvas.getContext("2d")!;
tmpCtx.save();
// Taille de l'image affichée avec zoom
const displayedWidth = originalDimensions.width * scaleFactor * (1 + zoom);
const displayedHeight = originalDimensions.height * scaleFactor * (1 + zoom);
// Coin supérieur gauche réel après zoom
const offsetX = imagePosition.x - (displayedWidth - originalDimensions.width * scaleFactor) / 2;
const offsetY = imagePosition.y - (displayedHeight - originalDimensions.height * scaleFactor) / 2;
// Déplacer au centre pour rotation + flip
tmpCtx.translate(offsetX + displayedWidth / 2, offsetY + displayedHeight / 2);
tmpCtx.rotate((rotation * Math.PI) / 180);
tmpCtx.scale(flipX ? -1 : 1, flipY ? -1 : 1);
// Appliquer les filtres
tmpCtx.filter = `brightness(${brightness}%) contrast(${contrast}%) saturate(${saturation}%) sepia(${sepia}%) grayscale(${grayscale}%)`;
// Dessiner l'image centrée
tmpCtx.drawImage(
image,
-displayedWidth / 2,
-displayedHeight / 2,
displayedWidth,
displayedHeight
);
tmpCtx.restore();
// Extraire la zone cropée visible
ctx.drawImage(
tmpCanvas,
crop.x,
crop.y,
crop.width,
crop.height,
0,
0,
canvas.width,
canvas.height
);
return canvas.toDataURL("image/png");
}
@ -726,7 +681,6 @@ import { cn } from "@/lib/utils";
<ReactCrop
crop={cropZone}
onChange={(_, percentCrop) => setCropZone(percentCrop)}
onComplete={() => {}}
aspect={aspect}
className="w-full"
locked
@ -752,20 +706,12 @@ import { cn } from "@/lib/utils";
position: "absolute",
left: `${imagePosition.x}px`,
top: `${imagePosition.y}px`,
width:
originalDimensions.width > 0
? `${originalDimensions.width * scaleFactor}px`
: "auto",
height:
originalDimensions.height > 0
? `${originalDimensions.height * scaleFactor}px`
: "auto",
width: originalDimensions.width > 0 ? `${originalDimensions.width * scaleFactor}px` : "auto",
height: originalDimensions.height > 0 ? `${originalDimensions.height * scaleFactor}px` : "auto",
transform: `scale(${1 + zoom}) rotate(${rotation}deg) scaleX(${flipX ? -1 : 1}) scaleY(${flipY ? -1 : 1})`,
filter: `brightness(${brightness}%) contrast(${contrast}%) saturate(${saturation}%) sepia(${sepia}%) grayscale(${grayscale}%)`,
transformOrigin: "center center",
transition: isDragging
? "none"
: "transform 0.2s ease, filter 0.2s ease",
transition: !isDragging ? "transform 0.2s ease, filter 0.2s ease" : "none",
cursor: isDragging ? "grabbing" : "grab",
userSelect: "none",
}}