react-native-expo-cropper 1.0.25 → 1.0.27
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/ImageCropper.js +81 -43
- package/src/ImageCropperStyles.js +15 -20
- package/src/ImageProcessor.js +44 -27
package/package.json
CHANGED
package/src/ImageCropper.js
CHANGED
|
@@ -40,26 +40,9 @@ const ImageCropper = ({ onConfirm, openCameraFirst, initialImage ,addheight}) =>
|
|
|
40
40
|
}, [openCameraFirst, initialImage]);
|
|
41
41
|
|
|
42
42
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
Image.getSize(image, (imgWidth, imgHeight) => {
|
|
47
|
-
const screenRatio = SCREEN_WIDTH / SCREEN_HEIGHT;
|
|
48
|
-
const imageRatio = imgWidth / imgHeight;
|
|
49
|
-
|
|
50
|
-
if (imageRatio > screenRatio) {
|
|
51
|
-
imageMeasure.current = {
|
|
52
|
-
width: SCREEN_WIDTH,
|
|
53
|
-
height: SCREEN_WIDTH / imageRatio,
|
|
54
|
-
};
|
|
55
|
-
} else {
|
|
56
|
-
imageMeasure.current = {
|
|
57
|
-
width: SCREEN_HEIGHT * imageRatio,
|
|
58
|
-
height: SCREEN_HEIGHT,
|
|
59
|
-
};
|
|
60
|
-
}
|
|
61
|
-
});
|
|
62
|
-
}, [image]);
|
|
43
|
+
// Measure based strictly on actual layout, not screen ratio
|
|
44
|
+
// to avoid mismatches that truncate the selectable bottom area
|
|
45
|
+
// (onImageLayout handles measurement updates)
|
|
63
46
|
|
|
64
47
|
// Perform capture after UI commits (avoids iOS timer/RAF awaits)
|
|
65
48
|
// iOS capture logic using useEffect with overlay readiness
|
|
@@ -96,6 +79,53 @@ const ImageCropper = ({ onConfirm, openCameraFirst, initialImage ,addheight}) =>
|
|
|
96
79
|
return () => { cancelled = true; };
|
|
97
80
|
}, [captureRequested, showResult, overlayReady, addheight, onConfirm]);
|
|
98
81
|
|
|
82
|
+
// Helpers to compute crop rectangle in original image pixels
|
|
83
|
+
const getImageSizeAsync = (uri) =>
|
|
84
|
+
new Promise((resolve, reject) => {
|
|
85
|
+
Image.getSize(uri, (w, h) => resolve({ width: w, height: h }), reject);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const computeCropRect = (pts, measure, origW, origH) => {
|
|
89
|
+
const containerW = measure.width;
|
|
90
|
+
const containerH = measure.height;
|
|
91
|
+
if (!containerW || !containerH || !origW || !origH || !pts || pts.length === 0) return null;
|
|
92
|
+
|
|
93
|
+
const ratio = origH / origW;
|
|
94
|
+
let dispW, dispH, offX, offY;
|
|
95
|
+
if (containerW * ratio <= containerH) {
|
|
96
|
+
// image constrained by width
|
|
97
|
+
dispW = containerW;
|
|
98
|
+
dispH = containerW * ratio;
|
|
99
|
+
offX = 0;
|
|
100
|
+
offY = (containerH - dispH) / 2;
|
|
101
|
+
} else {
|
|
102
|
+
// image constrained by height
|
|
103
|
+
dispH = containerH;
|
|
104
|
+
dispW = containerH / ratio;
|
|
105
|
+
offY = 0;
|
|
106
|
+
offX = (containerW - dispW) / 2;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const xs = pts.map(p => Math.max(0, Math.min(dispW, p.x - offX)));
|
|
110
|
+
const ys = pts.map(p => Math.max(0, Math.min(dispH, p.y - offY)));
|
|
111
|
+
const minX = Math.min(...xs);
|
|
112
|
+
const maxX = Math.max(...xs);
|
|
113
|
+
const minY = Math.min(...ys);
|
|
114
|
+
const maxY = Math.max(...ys);
|
|
115
|
+
|
|
116
|
+
const normMinX = minX / dispW;
|
|
117
|
+
const normMaxX = maxX / dispW;
|
|
118
|
+
const normMinY = minY / dispH;
|
|
119
|
+
const normMaxY = maxY / dispH;
|
|
120
|
+
|
|
121
|
+
const originX = Math.round(normMinX * origW);
|
|
122
|
+
const originY = Math.round(normMinY * origH);
|
|
123
|
+
const widthPx = Math.round((normMaxX - normMinX) * origW);
|
|
124
|
+
const heightPx = Math.round((normMaxY - normMinY) * origH);
|
|
125
|
+
|
|
126
|
+
return { x: originX, y: originY, width: widthPx, height: heightPx };
|
|
127
|
+
};
|
|
128
|
+
|
|
99
129
|
|
|
100
130
|
const initializeCropBox = () => {
|
|
101
131
|
const { width, height } = imageMeasure.current;
|
|
@@ -160,26 +190,6 @@ const ImageCropper = ({ onConfirm, openCameraFirst, initialImage ,addheight}) =>
|
|
|
160
190
|
const boundedX = Math.max(0, Math.min(moveX, width));
|
|
161
191
|
const boundedY = Math.max(0, Math.min(moveY, height));
|
|
162
192
|
|
|
163
|
-
const edgeThreshold = 10;
|
|
164
|
-
const isNearTopOrBottomEdge =
|
|
165
|
-
boundedY <= edgeThreshold || boundedY >= height - edgeThreshold;
|
|
166
|
-
|
|
167
|
-
const isNearLeftOrRightEdge =
|
|
168
|
-
boundedX <= edgeThreshold || boundedX >= width - edgeThreshold;
|
|
169
|
-
|
|
170
|
-
if (isNearTopOrBottomEdge || isNearLeftOrRightEdge) {
|
|
171
|
-
// Reset point to last known position
|
|
172
|
-
if (lastValidPosition.current && selectedPointIndex.current !== null) {
|
|
173
|
-
setPoints(prev =>
|
|
174
|
-
prev.map((p, i) =>
|
|
175
|
-
i === selectedPointIndex.current ? lastValidPosition.current : p
|
|
176
|
-
)
|
|
177
|
-
);
|
|
178
|
-
}
|
|
179
|
-
selectedPointIndex.current = null;
|
|
180
|
-
return;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
193
|
// Valid move — update point and store as new last valid position
|
|
184
194
|
const updatedPoint = { x: boundedX, y: boundedY };
|
|
185
195
|
lastValidPosition.current = updatedPoint;
|
|
@@ -211,8 +221,8 @@ const ImageCropper = ({ onConfirm, openCameraFirst, initialImage ,addheight}) =>
|
|
|
211
221
|
/>
|
|
212
222
|
) : (
|
|
213
223
|
<>
|
|
214
|
-
{!showResult && (
|
|
215
|
-
<View style={
|
|
224
|
+
{!showResult && !image && (
|
|
225
|
+
<View pointerEvents="box-none" style={styles.centerButtonsContainer}>
|
|
216
226
|
|
|
217
227
|
{image && (
|
|
218
228
|
<TouchableOpacity style={styles.button} onPress={handleReset}>
|
|
@@ -255,7 +265,7 @@ const ImageCropper = ({ onConfirm, openCameraFirst, initialImage ,addheight}) =>
|
|
|
255
265
|
}
|
|
256
266
|
}
|
|
257
267
|
}}
|
|
258
|
-
|
|
268
|
+
>
|
|
259
269
|
<Text style={styles.buttonText}>Confirm</Text>
|
|
260
270
|
</TouchableOpacity>
|
|
261
271
|
)}
|
|
@@ -276,6 +286,7 @@ const ImageCropper = ({ onConfirm, openCameraFirst, initialImage ,addheight}) =>
|
|
|
276
286
|
>
|
|
277
287
|
<Image source={{ uri: image }} style={styles.image} onLayout={onImageLayout} />
|
|
278
288
|
<Svg
|
|
289
|
+
pointerEvents="none"
|
|
279
290
|
key={showResult ? 'mask' : 'edit'}
|
|
280
291
|
style={styles.overlay}
|
|
281
292
|
onLayout={() => {
|
|
@@ -294,6 +305,33 @@ const ImageCropper = ({ onConfirm, openCameraFirst, initialImage ,addheight}) =>
|
|
|
294
305
|
<Circle key={index} cx={point.x} cy={point.y} r={10} fill="white" />
|
|
295
306
|
))}
|
|
296
307
|
</Svg>
|
|
308
|
+
</View>
|
|
309
|
+
)}
|
|
310
|
+
{!showResult && image && (
|
|
311
|
+
<View style={styles.buttonContainer}>
|
|
312
|
+
<TouchableOpacity style={styles.button} onPress={handleReset}>
|
|
313
|
+
<Text style={styles.buttonText}>Reset</Text>
|
|
314
|
+
</TouchableOpacity>
|
|
315
|
+
<TouchableOpacity
|
|
316
|
+
style={styles.button}
|
|
317
|
+
onPress={async () => {
|
|
318
|
+
try {
|
|
319
|
+
setIsLoading(true);
|
|
320
|
+
const { width: origW, height: origH } = await getImageSizeAsync(image);
|
|
321
|
+
const rect = computeCropRect(points, imageMeasure.current, origW, origH);
|
|
322
|
+
const enhancedUri = await enhanceImage(image, addheight, rect);
|
|
323
|
+
const name = `IMAGE XTK${Date.now()}.png`;
|
|
324
|
+
if (onConfirm) onConfirm(enhancedUri, name);
|
|
325
|
+
} catch (error) {
|
|
326
|
+
console.error('Erreur lors de la capture :', error);
|
|
327
|
+
alert('Erreur lors de la capture !');
|
|
328
|
+
} finally {
|
|
329
|
+
setIsLoading(false);
|
|
330
|
+
}
|
|
331
|
+
}}
|
|
332
|
+
>
|
|
333
|
+
<Text style={styles.buttonText}>Confirm</Text>
|
|
334
|
+
</TouchableOpacity>
|
|
297
335
|
</View>
|
|
298
336
|
)}
|
|
299
337
|
</>
|
|
@@ -15,27 +15,22 @@ const styles = StyleSheet.create({
|
|
|
15
15
|
backgroundColor: DEEP_BLACK,
|
|
16
16
|
},
|
|
17
17
|
buttonContainer: {
|
|
18
|
-
position: 'absolute',
|
|
19
|
-
bottom: 50,
|
|
20
|
-
left: 0,
|
|
21
|
-
right: 0,
|
|
22
18
|
flexDirection: 'row',
|
|
23
|
-
|
|
24
|
-
justifyContent: 'center',
|
|
19
|
+
justifyContent: 'space-between',
|
|
25
20
|
alignItems: 'center',
|
|
26
|
-
paddingHorizontal:
|
|
27
|
-
|
|
21
|
+
paddingHorizontal: 16,
|
|
22
|
+
paddingVertical: 12,
|
|
23
|
+
width: '100%',
|
|
24
|
+
backgroundColor: DEEP_BLACK,
|
|
25
|
+
gap: 10,
|
|
28
26
|
},
|
|
29
27
|
button: {
|
|
30
28
|
flex: 1,
|
|
31
|
-
|
|
32
|
-
padding: 10,
|
|
29
|
+
paddingVertical: 12,
|
|
33
30
|
alignItems: "center",
|
|
34
31
|
justifyContent: "center",
|
|
35
32
|
backgroundColor: "#549433",
|
|
36
|
-
borderRadius:
|
|
37
|
-
marginBottom: 20,
|
|
38
|
-
marginRight:5,
|
|
33
|
+
borderRadius: 8,
|
|
39
34
|
},
|
|
40
35
|
buttonText: {
|
|
41
36
|
color: 'white',
|
|
@@ -50,13 +45,13 @@ const styles = StyleSheet.create({
|
|
|
50
45
|
textAlign: 'center',
|
|
51
46
|
},
|
|
52
47
|
imageContainer: {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
},
|
|
48
|
+
width: IMAGE_WIDTH,
|
|
49
|
+
flex: 1,
|
|
50
|
+
justifyContent: 'center',
|
|
51
|
+
alignItems: 'center',
|
|
52
|
+
overflow: 'hidden',
|
|
53
|
+
backgroundColor: 'black',
|
|
54
|
+
},
|
|
60
55
|
|
|
61
56
|
image: {
|
|
62
57
|
width: '100%',
|
package/src/ImageProcessor.js
CHANGED
|
@@ -1,28 +1,45 @@
|
|
|
1
|
-
import * as ImageManipulator from 'expo-image-manipulator';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
1
|
+
import * as ImageManipulator from 'expo-image-manipulator';
|
|
2
|
+
|
|
3
|
+
// Crop the image to the provided rectangle (in original image pixels), then resize.
|
|
4
|
+
// cropRect: { x, y, width, height } in original image coordinates
|
|
5
|
+
export const enhanceImage = async (uri, addheight, cropRect) => {
|
|
6
|
+
try {
|
|
7
|
+
const actions = [];
|
|
8
|
+
if (cropRect && cropRect.width > 0 && cropRect.height > 0) {
|
|
9
|
+
actions.push({
|
|
10
|
+
crop: {
|
|
11
|
+
originX: Math.round(cropRect.x),
|
|
12
|
+
originY: Math.round(cropRect.y),
|
|
13
|
+
width: Math.round(cropRect.width),
|
|
14
|
+
height: Math.round(cropRect.height),
|
|
15
|
+
},
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// First: crop (if requested)
|
|
20
|
+
const cropped = await ImageManipulator.manipulateAsync(uri, actions, {
|
|
21
|
+
compress: 1,
|
|
22
|
+
format: ImageManipulator.SaveFormat.PNG,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// Then: resize to requested height while preserving aspect ratio
|
|
26
|
+
const targetHeight = addheight || cropped.height;
|
|
27
|
+
const ratio = cropped.height / cropped.width;
|
|
28
|
+
const newWidth = Math.round(targetHeight / ratio);
|
|
29
|
+
const newHeight = Math.round(newWidth * ratio);
|
|
30
|
+
|
|
31
|
+
const result = await ImageManipulator.manipulateAsync(
|
|
32
|
+
cropped.uri,
|
|
33
|
+
[{ resize: { width: newWidth, height: newHeight } }],
|
|
34
|
+
{
|
|
35
|
+
compress: 1,
|
|
36
|
+
format: ImageManipulator.SaveFormat.PNG,
|
|
37
|
+
}
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
return result.uri;
|
|
41
|
+
} catch (error) {
|
|
42
|
+
console.error('Erreur T404K:', error);
|
|
43
|
+
return uri;
|
|
44
|
+
}
|
|
28
45
|
};
|