react-native-expo-cropper
Version:
Recadrage polygonal d'images.
144 lines (130 loc) • 5.4 kB
JavaScript
import React, { forwardRef } from 'react';
import { View } from 'react-native';
import Svg, { Path, Defs, ClipPath, Image as SvgImage } from 'react-native-svg';
import { captureRef } from 'react-native-view-shot';
/**
* ImageMaskProcessor - Module pour appliquer un masque alpha à une image
*
* NOTE: Ce module est actuellement désactivé par défaut (enableMask = false)
* car captureRef peut se bloquer sur certaines configurations.
*
* Pour activer le masque, mettre enableMask = true dans ImageCropper.js
* ATTENTION: Peut causer des blocages sur certaines configurations
*/
/**
* Crée un chemin SVG à partir de points
*/
const createPathFromPoints = (points) => {
if (points.length < 3) return '';
let path = `M ${points[0].x} ${points[0].y} `;
for (let i = 1; i < points.length; i++) {
path += `L ${points[i].x} ${points[i].y} `;
}
return path + 'Z';
};
/**
* Composant MaskView - Vue avec image et masque SVG
* Utilisé pour capturer l'image avec le masque appliqué
*/
export const MaskView = forwardRef(({ imageUri, points, width, height }, ref) => {
const path = createPathFromPoints(points);
// ✅ CORRECTION CRITIQUE :
// Utiliser un VRAI clip SVG (ClipPath + SvgImage) au lieu d'un overlay noir.
// Sinon captureRef peut capturer seulement l'overlay (noir/transparence) si l'Image RN n'est pas prête.
return (
<View
ref={ref}
collapsable={false}
style={{
width,
height,
overflow: 'hidden',
backgroundColor: 'transparent',
}}
>
<Svg width={width} height={height}>
<Defs>
<ClipPath id="cropClip">
<Path d={path} />
</ClipPath>
</Defs>
{/* Image rendue dans SVG + clip => extérieur transparent, intérieur = image */}
<SvgImage
width={width}
height={height}
href={{ uri: imageUri }}
preserveAspectRatio="xMidYMid slice"
clipPath="url(#cropClip)"
/>
</Svg>
</View>
);
});
MaskView.displayName = 'MaskView';
/**
* Applique un masque alpha à une image en utilisant captureRef
*
* @param {string} imageUri - URI de l'image à masquer
* @param {Array} points - Points définissant le masque (coordonnées normalisées)
* @param {number} width - Largeur de l'image
* @param {number} height - Hauteur de l'image
* @param {React.Ref} maskViewRef - Référence à la vue MaskView
* @returns {Promise<string>} URI de l'image masquée
*
* NOTE: Cette fonction utilise captureRef qui peut se bloquer.
* Utiliser uniquement si enableMask = true dans ImageCropper.js
*/
// width/height = dimensions de sortie souhaitées EN PIXELS (pas dp)
export const applyMaskToImage = async (imageUri, points, width, height, maskViewRef) => {
try {
if (!maskViewRef?.current) {
console.warn("MaskView ref not available in applyMaskToImage");
return imageUri;
}
console.log("Applying mask with captureRef...", {
imageUri,
pointsCount: points.length,
width,
height,
refAvailable: !!maskViewRef.current
});
// ✅ CORRECTION : Capturer la vue avec le masque appliqué
// captureRef supporte la transparence PNG pour préserver la forme exacte
// Utiliser une qualité maximale et un format PNG pour préserver la transparence
// Ajouter un petit délai supplémentaire pour s'assurer que la vue est complètement rendue
await new Promise(resolve => setTimeout(resolve, 100));
// ⚠️ LIMITATION CRITIQUE : captureRef capture à la résolution de la vue React Native
// La vue MaskView doit avoir les dimensions EXACTES de l'image croppée pour préserver la qualité
// Si width/height sont les dimensions réelles de l'image croppée, la qualité sera préservée
// Si width/height sont plus petits, la qualité sera réduite
console.log("📸 Capturing mask view at requested output size (px):", {
outputPx: { width, height },
imageUri,
note: "✅ captureRef width/height forces output resolution; view can stay smaller in dp"
});
// ✅ CORRECTION : Utiliser les dimensions réelles de l'image croppée
// Si width/height correspondent aux dimensions réelles de croppedResult, la qualité sera maximale
const maskedUri = await captureRef(maskViewRef.current, {
format: 'png', // PNG supporte la transparence alpha
quality: 1, // Qualité maximale (0-1)
result: 'tmpfile', // Fichier temporaire
// ✅ CORRECTION : forcer la résolution de sortie (pixels)
// évite une capture 'écran' basée uniquement sur les dp de la vue (risque de downscale).
width,
height,
// ✅ IMPORTANT : La résolution de capture = taille de la vue (width x height)
// Si width/height = dimensions réelles de l'image croppée, qualité préservée
// Si width/height < dimensions réelles, qualité réduite
});
console.log("✅ Mask applied successfully:", {
maskedUri,
viewSize: `${width}x${height}`,
qualityStatus: width >= 1000 && height >= 1000 ? "✓ High quality preserved" : "⚠️ Check if dimensions match original"
});
return maskedUri;
} catch (error) {
console.error("Error applying mask:", error);
// En cas d'erreur, retourner l'image originale (bounding box)
return imageUri;
}
};