react-native-expo-cropper 1.2.40 → 1.2.41
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/dist/CustomCamera.js +139 -93
- package/dist/ImageCropper.js +61 -24
- package/package.json +6 -7
- package/src/CustomCamera.js +143 -119
- package/src/ImageCropper.js +31 -17
package/dist/CustomCamera.js
CHANGED
|
@@ -8,7 +8,7 @@ exports["default"] = CustomCamera;
|
|
|
8
8
|
var _react = _interopRequireWildcard(require("react"));
|
|
9
9
|
var _reactNative = require("react-native");
|
|
10
10
|
var _reactNativeSafeAreaContext = require("react-native-safe-area-context");
|
|
11
|
-
var
|
|
11
|
+
var _expoCamera = require("expo-camera");
|
|
12
12
|
function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function _interopRequireWildcard(e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, "default": e }; if (null === e || "object" != _typeof(e) && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (var _t2 in e) "default" !== _t2 && {}.hasOwnProperty.call(e, _t2) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, _t2)) && (i.get || i.set) ? o(f, _t2, i) : f[_t2] = e[_t2]); return f; })(e, t); }
|
|
13
13
|
function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
|
|
14
14
|
function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
|
|
@@ -38,31 +38,44 @@ function CustomCamera(_ref) {
|
|
|
38
38
|
_useState4 = _slicedToArray(_useState3, 2),
|
|
39
39
|
loadingBeforeCapture = _useState4[0],
|
|
40
40
|
setLoadingBeforeCapture = _useState4[1];
|
|
41
|
+
var _useState5 = (0, _react.useState)(null),
|
|
42
|
+
_useState6 = _slicedToArray(_useState5, 2),
|
|
43
|
+
hasPermission = _useState6[0],
|
|
44
|
+
setHasPermission = _useState6[1];
|
|
41
45
|
var cameraRef = (0, _react.useRef)(null);
|
|
42
46
|
var cameraWrapperRef = (0, _react.useRef)(null);
|
|
43
47
|
var insets = (0, _reactNativeSafeAreaContext.useSafeAreaInsets)();
|
|
44
|
-
var
|
|
48
|
+
var _useState7 = (0, _react.useState)({
|
|
45
49
|
width: 0,
|
|
46
50
|
height: 0,
|
|
47
51
|
x: 0,
|
|
48
52
|
y: 0
|
|
49
53
|
}),
|
|
50
|
-
_useState6 = _slicedToArray(_useState5, 2),
|
|
51
|
-
cameraWrapperLayout = _useState6[0],
|
|
52
|
-
setCameraWrapperLayout = _useState6[1];
|
|
53
|
-
var _useState7 = (0, _react.useState)(null),
|
|
54
54
|
_useState8 = _slicedToArray(_useState7, 2),
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
var
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
55
|
+
cameraWrapperLayout = _useState8[0],
|
|
56
|
+
setCameraWrapperLayout = _useState8[1];
|
|
57
|
+
var _useState9 = (0, _react.useState)(null),
|
|
58
|
+
_useState0 = _slicedToArray(_useState9, 2),
|
|
59
|
+
greenFrame = _useState0[0],
|
|
60
|
+
setGreenFrame = _useState0[1];
|
|
61
61
|
(0, _react.useEffect)(function () {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
62
|
+
_asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee() {
|
|
63
|
+
var _yield$Camera$request, status;
|
|
64
|
+
return _regenerator().w(function (_context) {
|
|
65
|
+
while (1) switch (_context.n) {
|
|
66
|
+
case 0:
|
|
67
|
+
_context.n = 1;
|
|
68
|
+
return _expoCamera.Camera.requestCameraPermissionsAsync();
|
|
69
|
+
case 1:
|
|
70
|
+
_yield$Camera$request = _context.v;
|
|
71
|
+
status = _yield$Camera$request.status;
|
|
72
|
+
setHasPermission(status === 'granted');
|
|
73
|
+
case 2:
|
|
74
|
+
return _context.a(2);
|
|
75
|
+
}
|
|
76
|
+
}, _callee);
|
|
77
|
+
}))();
|
|
78
|
+
}, []);
|
|
66
79
|
|
|
67
80
|
// Helper function to wait for multiple render cycles (works on iOS)
|
|
68
81
|
var waitForRender = function waitForRender() {
|
|
@@ -81,19 +94,26 @@ function CustomCamera(_ref) {
|
|
|
81
94
|
});
|
|
82
95
|
};
|
|
83
96
|
|
|
84
|
-
//
|
|
85
|
-
//
|
|
97
|
+
// ✅ CRITICAL FIX: Calculate green frame coordinates relative to camera preview
|
|
98
|
+
// The green frame should be calculated on the wrapper (as it's visually drawn there)
|
|
99
|
+
// But we store it with wrapper dimensions so ImageCropper can map it correctly
|
|
100
|
+
//
|
|
101
|
+
// NOTE: Camera capture aspect ratio (typically 4:3) may differ from wrapper aspect ratio (9:16)
|
|
102
|
+
// This is handled in ImageCropper by using "cover" mode to match preview content
|
|
86
103
|
var calculateGreenFrameCoordinates = function calculateGreenFrameCoordinates() {
|
|
87
104
|
var wrapperWidth = cameraWrapperLayout.width;
|
|
88
105
|
var wrapperHeight = cameraWrapperLayout.height;
|
|
89
106
|
if (wrapperWidth === 0 || wrapperHeight === 0) {
|
|
90
|
-
console.warn(
|
|
107
|
+
console.warn("Camera wrapper layout not ready, cannot calculate green frame");
|
|
91
108
|
return null;
|
|
92
109
|
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
var
|
|
96
|
-
var
|
|
110
|
+
|
|
111
|
+
// ✅ Calculate green frame as percentage of WRAPPER
|
|
112
|
+
var frameWidth = wrapperWidth * 0.85; // 85% of wrapper width
|
|
113
|
+
var frameHeight = wrapperHeight * 0.70; // 70% of wrapper height
|
|
114
|
+
var frameX = (wrapperWidth - frameWidth) / 2; // Centered horizontally
|
|
115
|
+
var frameY = (wrapperHeight - frameHeight) / 2; // Centered vertically
|
|
116
|
+
|
|
97
117
|
var frameCoords = {
|
|
98
118
|
x: frameX,
|
|
99
119
|
y: frameY,
|
|
@@ -101,14 +121,19 @@ function CustomCamera(_ref) {
|
|
|
101
121
|
height: frameHeight,
|
|
102
122
|
wrapperWidth: wrapperWidth,
|
|
103
123
|
wrapperHeight: wrapperHeight,
|
|
124
|
+
// ✅ Store percentages for easier mapping later
|
|
104
125
|
percentX: frameX / wrapperWidth * 100,
|
|
105
126
|
percentY: frameY / wrapperHeight * 100,
|
|
106
127
|
percentWidth: 85,
|
|
107
|
-
|
|
128
|
+
// 85% of wrapper width
|
|
129
|
+
percentHeight: 70 // 70% of wrapper height
|
|
108
130
|
};
|
|
109
|
-
console.log(
|
|
131
|
+
console.log("✅ Green frame coordinates calculated:", frameCoords);
|
|
110
132
|
return frameCoords;
|
|
111
133
|
};
|
|
134
|
+
|
|
135
|
+
// 🔁 Keep green frame state in sync with wrapper layout so we can both render it
|
|
136
|
+
// and send the exact same coordinates along with the captured photo.
|
|
112
137
|
(0, _react.useEffect)(function () {
|
|
113
138
|
var coords = calculateGreenFrameCoordinates();
|
|
114
139
|
if (coords) {
|
|
@@ -116,53 +141,86 @@ function CustomCamera(_ref) {
|
|
|
116
141
|
}
|
|
117
142
|
}, [cameraWrapperLayout]);
|
|
118
143
|
var takePicture = /*#__PURE__*/function () {
|
|
119
|
-
var
|
|
120
|
-
var
|
|
121
|
-
return _regenerator().w(function (
|
|
122
|
-
while (1) switch (
|
|
144
|
+
var _ref3 = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee2() {
|
|
145
|
+
var captureOptions, photo, capturedAspectRatio, greenFrameCoords, _t;
|
|
146
|
+
return _regenerator().w(function (_context2) {
|
|
147
|
+
while (1) switch (_context2.p = _context2.n) {
|
|
123
148
|
case 0:
|
|
124
|
-
if (!
|
|
125
|
-
|
|
149
|
+
if (!cameraRef.current) {
|
|
150
|
+
_context2.n = 7;
|
|
126
151
|
break;
|
|
127
152
|
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
_context.p = 1;
|
|
153
|
+
_context2.p = 1;
|
|
154
|
+
// Show loading after a delay (using setImmediate for iOS compatibility)
|
|
131
155
|
waitForRender(5).then(function () {
|
|
132
156
|
setLoadingBeforeCapture(true);
|
|
133
157
|
});
|
|
134
|
-
|
|
158
|
+
|
|
159
|
+
// Wait a bit before taking picture (works on iOS)
|
|
160
|
+
_context2.n = 2;
|
|
135
161
|
return waitForRender(2);
|
|
136
162
|
case 2:
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
163
|
+
// ✅ OPTIMIZED: Capture with maximum quality and native camera ratio
|
|
164
|
+
// Platform-specific optimizations for best quality
|
|
165
|
+
captureOptions = _objectSpread(_objectSpread({
|
|
166
|
+
// Maximum quality (0-1, 1 = best quality, no compression)
|
|
167
|
+
quality: 1,
|
|
168
|
+
// Disable shutter sound for better UX
|
|
169
|
+
shutterSound: false,
|
|
170
|
+
// ✅ CRITICAL: skipProcessing preserves original resolution and avoids interpolation
|
|
171
|
+
// This ensures pixel-perfect quality and prevents premature resizing
|
|
172
|
+
skipProcessing: true,
|
|
173
|
+
// Include EXIF metadata (orientation, camera settings, etc.)
|
|
174
|
+
exif: true
|
|
175
|
+
}, _reactNative.Platform.OS === 'ios' && {
|
|
176
|
+
// iOS: Use native capture format (typically 4:3 or 16:9 depending on device)
|
|
177
|
+
// No additional processing to preserve quality
|
|
178
|
+
}), _reactNative.Platform.OS === 'android' && {
|
|
179
|
+
// Android: Ensure maximum resolution capture
|
|
180
|
+
// skipProcessing already handles this, but we can add Android-specific options if needed
|
|
181
|
+
});
|
|
182
|
+
console.log("📸 Capturing photo with maximum quality settings:", {
|
|
183
|
+
platform: _reactNative.Platform.OS,
|
|
184
|
+
options: captureOptions,
|
|
185
|
+
wrapperSize: {
|
|
186
|
+
width: cameraWrapperLayout.width,
|
|
187
|
+
height: cameraWrapperLayout.height
|
|
188
|
+
}
|
|
141
189
|
});
|
|
190
|
+
_context2.n = 3;
|
|
191
|
+
return cameraRef.current.takePictureAsync(captureOptions);
|
|
142
192
|
case 3:
|
|
143
|
-
photo =
|
|
144
|
-
if (!(!photo.
|
|
145
|
-
|
|
193
|
+
photo = _context2.v;
|
|
194
|
+
if (!(!photo.width || !photo.height || photo.width === 0 || photo.height === 0)) {
|
|
195
|
+
_context2.n = 4;
|
|
146
196
|
break;
|
|
147
197
|
}
|
|
148
|
-
throw new Error(
|
|
198
|
+
throw new Error("Invalid photo dimensions received from camera");
|
|
149
199
|
case 4:
|
|
150
|
-
uri = photo.path.startsWith('file://') ? photo.path : "file://".concat(photo.path);
|
|
151
200
|
capturedAspectRatio = photo.width / photo.height;
|
|
152
|
-
console.log(
|
|
153
|
-
uri: uri,
|
|
201
|
+
console.log("✅ Photo captured with maximum quality:", {
|
|
202
|
+
uri: photo.uri,
|
|
154
203
|
width: photo.width,
|
|
155
204
|
height: photo.height,
|
|
156
|
-
aspectRatio: capturedAspectRatio.toFixed(3)
|
|
205
|
+
aspectRatio: capturedAspectRatio.toFixed(3),
|
|
206
|
+
expectedRatio: "~1.33 (4:3) or ~1.78 (16:9)",
|
|
207
|
+
exif: photo.exif ? "present" : "missing",
|
|
208
|
+
fileSize: photo.uri ? "available" : "unknown"
|
|
157
209
|
});
|
|
210
|
+
|
|
211
|
+
// ✅ CRITICAL FIX: Use the same green frame coordinates that are used for rendering
|
|
212
|
+
// Fallback to recalculation if, for some reason, state is not yet set
|
|
158
213
|
greenFrameCoords = greenFrame || calculateGreenFrameCoordinates();
|
|
159
214
|
if (greenFrameCoords) {
|
|
160
|
-
|
|
215
|
+
_context2.n = 5;
|
|
161
216
|
break;
|
|
162
217
|
}
|
|
163
|
-
throw new Error(
|
|
218
|
+
throw new Error("Green frame coordinates not available");
|
|
164
219
|
case 5:
|
|
165
|
-
|
|
220
|
+
// ✅ Send photo URI and frame data to ImageCropper
|
|
221
|
+
// The photo maintains its native resolution and aspect ratio
|
|
222
|
+
// ImageCropper will handle display and cropping while preserving quality
|
|
223
|
+
onPhotoCaptured(photo.uri, {
|
|
166
224
|
greenFrame: greenFrameCoords,
|
|
167
225
|
capturedImageSize: {
|
|
168
226
|
width: photo.width,
|
|
@@ -171,46 +229,23 @@ function CustomCamera(_ref) {
|
|
|
171
229
|
}
|
|
172
230
|
});
|
|
173
231
|
setLoadingBeforeCapture(false);
|
|
174
|
-
|
|
232
|
+
_context2.n = 7;
|
|
175
233
|
break;
|
|
176
234
|
case 6:
|
|
177
|
-
|
|
178
|
-
_t =
|
|
179
|
-
console.error(
|
|
235
|
+
_context2.p = 6;
|
|
236
|
+
_t = _context2.v;
|
|
237
|
+
console.error("❌ Error capturing photo:", _t);
|
|
180
238
|
setLoadingBeforeCapture(false);
|
|
181
|
-
_reactNative.Alert.alert(
|
|
239
|
+
_reactNative.Alert.alert("Erreur", "Impossible de capturer la photo: ".concat(_t.message || "Erreur inconnue", ". Veuillez r\xE9essayer."));
|
|
182
240
|
case 7:
|
|
183
|
-
return
|
|
241
|
+
return _context2.a(2);
|
|
184
242
|
}
|
|
185
|
-
},
|
|
243
|
+
}, _callee2, null, [[1, 6]]);
|
|
186
244
|
}));
|
|
187
245
|
return function takePicture() {
|
|
188
|
-
return
|
|
246
|
+
return _ref3.apply(this, arguments);
|
|
189
247
|
};
|
|
190
248
|
}();
|
|
191
|
-
if (!hasPermission) {
|
|
192
|
-
return /*#__PURE__*/_react["default"].createElement(_reactNative.SafeAreaView, {
|
|
193
|
-
style: styles.outerContainer
|
|
194
|
-
}, /*#__PURE__*/_react["default"].createElement(_reactNative.View, {
|
|
195
|
-
style: styles.permissionContainer
|
|
196
|
-
}, /*#__PURE__*/_react["default"].createElement(_reactNative.Text, {
|
|
197
|
-
style: styles.text
|
|
198
|
-
}, "Acc\xE8s \xE0 la cam\xE9ra requis pour prendre des photos."), /*#__PURE__*/_react["default"].createElement(_reactNative.TouchableOpacity, {
|
|
199
|
-
style: styles.button,
|
|
200
|
-
onPress: requestPermission
|
|
201
|
-
}, /*#__PURE__*/_react["default"].createElement(_reactNative.Text, {
|
|
202
|
-
style: styles.buttonText
|
|
203
|
-
}, "Autoriser la cam\xE9ra"))));
|
|
204
|
-
}
|
|
205
|
-
if (!device) {
|
|
206
|
-
return /*#__PURE__*/_react["default"].createElement(_reactNative.SafeAreaView, {
|
|
207
|
-
style: styles.outerContainer
|
|
208
|
-
}, /*#__PURE__*/_react["default"].createElement(_reactNative.View, {
|
|
209
|
-
style: styles.permissionContainer
|
|
210
|
-
}, /*#__PURE__*/_react["default"].createElement(_reactNative.Text, {
|
|
211
|
-
style: styles.text
|
|
212
|
-
}, "Aucune cam\xE9ra arri\xE8re disponible.")));
|
|
213
|
-
}
|
|
214
249
|
return /*#__PURE__*/_react["default"].createElement(_reactNative.SafeAreaView, {
|
|
215
250
|
style: styles.outerContainer
|
|
216
251
|
}, /*#__PURE__*/_react["default"].createElement(_reactNative.View, {
|
|
@@ -224,18 +259,19 @@ function CustomCamera(_ref) {
|
|
|
224
259
|
x: layout.x,
|
|
225
260
|
y: layout.y
|
|
226
261
|
});
|
|
227
|
-
console.log(
|
|
262
|
+
console.log("Camera wrapper layout updated:", layout);
|
|
228
263
|
}
|
|
229
|
-
}, /*#__PURE__*/_react["default"].createElement(
|
|
264
|
+
}, /*#__PURE__*/_react["default"].createElement(_expoCamera.CameraView, {
|
|
265
|
+
style: styles.camera,
|
|
266
|
+
facing: "back",
|
|
230
267
|
ref: cameraRef,
|
|
231
|
-
|
|
232
|
-
device: device,
|
|
233
|
-
isActive: true,
|
|
234
|
-
photo: true,
|
|
235
|
-
onInitialized: function onInitialized() {
|
|
268
|
+
onCameraReady: function onCameraReady() {
|
|
236
269
|
setIsReady(true);
|
|
237
|
-
console.log(
|
|
270
|
+
console.log("✅ Camera ready - Maximum quality capture enabled");
|
|
238
271
|
}
|
|
272
|
+
// ✅ Ensure camera uses native aspect ratio (typically 4:3 for most devices)
|
|
273
|
+
// The wrapper constrains the view, but capture uses full sensor resolution
|
|
274
|
+
// This ensures preview matches what will be captured
|
|
239
275
|
}), loadingBeforeCapture && /*#__PURE__*/_react["default"].createElement(_react["default"].Fragment, null, /*#__PURE__*/_react["default"].createElement(_reactNative.View, {
|
|
240
276
|
style: styles.loadingOverlay
|
|
241
277
|
}, /*#__PURE__*/_react["default"].createElement(_reactNative.Image, {
|
|
@@ -282,6 +318,7 @@ var styles = _reactNative.StyleSheet.create({
|
|
|
282
318
|
justifyContent: 'center',
|
|
283
319
|
position: 'relative'
|
|
284
320
|
},
|
|
321
|
+
camera: _objectSpread({}, _reactNative.StyleSheet.absoluteFillObject),
|
|
285
322
|
scanFrame: {
|
|
286
323
|
position: 'absolute',
|
|
287
324
|
borderWidth: 4,
|
|
@@ -303,6 +340,14 @@ var styles = _reactNative.StyleSheet.create({
|
|
|
303
340
|
zIndex: 21,
|
|
304
341
|
backgroundColor: 'transparent'
|
|
305
342
|
}),
|
|
343
|
+
cancelIcon: {
|
|
344
|
+
position: 'absolute',
|
|
345
|
+
top: 20,
|
|
346
|
+
left: 20,
|
|
347
|
+
backgroundColor: PRIMARY_GREEN,
|
|
348
|
+
borderRadius: 5,
|
|
349
|
+
padding: 8
|
|
350
|
+
},
|
|
306
351
|
buttonContainer: {
|
|
307
352
|
position: 'absolute',
|
|
308
353
|
bottom: 0,
|
|
@@ -324,15 +369,16 @@ var styles = _reactNative.StyleSheet.create({
|
|
|
324
369
|
fontSize: 18,
|
|
325
370
|
color: GLOW_WHITE
|
|
326
371
|
},
|
|
327
|
-
|
|
372
|
+
container: {
|
|
328
373
|
flex: 1,
|
|
374
|
+
backgroundColor: DEEP_BLACK,
|
|
329
375
|
justifyContent: 'center',
|
|
330
376
|
alignItems: 'center',
|
|
331
377
|
padding: 20
|
|
332
378
|
},
|
|
333
|
-
|
|
334
|
-
fontSize:
|
|
335
|
-
color:
|
|
379
|
+
iconText: {
|
|
380
|
+
fontSize: 18,
|
|
381
|
+
color: GLOW_WHITE,
|
|
336
382
|
fontWeight: '600'
|
|
337
383
|
}
|
|
338
384
|
});
|
package/dist/ImageCropper.js
CHANGED
|
@@ -35,12 +35,14 @@ function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r)
|
|
|
35
35
|
function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; }
|
|
36
36
|
function _iterableToArrayLimit(r, l) { var t = null == r ? null : "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (null != t) { var e, n, i, u, a = [], f = !0, o = !1; try { if (i = (t = t.call(r)).next, 0 === l) { if (Object(t) !== t) return; f = !1; } else for (; !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); f = !0); } catch (r) { o = !0, n = r; } finally { try { if (!f && null != t["return"] && (u = t["return"](), Object(u) !== u)) return; } finally { if (o) throw n; } } return a; } }
|
|
37
37
|
function _arrayWithHoles(r) { if (Array.isArray(r)) return r; }
|
|
38
|
+
var PRIMARY_GREEN = '#198754';
|
|
38
39
|
var ImageCropper = function ImageCropper(_ref) {
|
|
39
40
|
var _cameraFrameData$curr3;
|
|
40
41
|
var onConfirm = _ref.onConfirm,
|
|
41
42
|
openCameraFirst = _ref.openCameraFirst,
|
|
42
43
|
initialImage = _ref.initialImage,
|
|
43
|
-
addheight = _ref.addheight
|
|
44
|
+
addheight = _ref.addheight,
|
|
45
|
+
rotationLabel = _ref.rotationLabel;
|
|
44
46
|
var _useState = (0, _react.useState)(null),
|
|
45
47
|
_useState2 = _slicedToArray(_useState, 2),
|
|
46
48
|
image = _useState2[0],
|
|
@@ -242,6 +244,7 @@ var ImageCropper = function ImageCropper(_ref) {
|
|
|
242
244
|
_useState20 = _slicedToArray(_useState19, 2),
|
|
243
245
|
isRotating = _useState20[0],
|
|
244
246
|
setIsRotating = _useState20[1];
|
|
247
|
+
var rotationInProgressRef = (0, _react.useRef)(false); // block duplicate taps immediately
|
|
245
248
|
var lastValidPosition = (0, _react.useRef)(null);
|
|
246
249
|
var insets = (0, _reactNativeSafeAreaContext.useSafeAreaInsets)();
|
|
247
250
|
|
|
@@ -249,7 +252,7 @@ var ImageCropper = function ImageCropper(_ref) {
|
|
|
249
252
|
|
|
250
253
|
// No view-shot / captureRef / bitmap masking on device.
|
|
251
254
|
var enableMask = false;
|
|
252
|
-
var enableRotation =
|
|
255
|
+
var enableRotation = true; // rotation resets crop box so it is re-initialized on rotated image.
|
|
253
256
|
|
|
254
257
|
(0, _react.useEffect)(function () {
|
|
255
258
|
if (openCameraFirst) {
|
|
@@ -1212,45 +1215,54 @@ var ImageCropper = function ImageCropper(_ref) {
|
|
|
1212
1215
|
return _regenerator().w(function (_context) {
|
|
1213
1216
|
while (1) switch (_context.p = _context.n) {
|
|
1214
1217
|
case 0:
|
|
1215
|
-
if (
|
|
1218
|
+
if (image) {
|
|
1216
1219
|
_context.n = 1;
|
|
1217
1220
|
break;
|
|
1218
1221
|
}
|
|
1219
1222
|
return _context.a(2);
|
|
1220
1223
|
case 1:
|
|
1224
|
+
if (!rotationInProgressRef.current) {
|
|
1225
|
+
_context.n = 2;
|
|
1226
|
+
break;
|
|
1227
|
+
}
|
|
1228
|
+
return _context.a(2);
|
|
1229
|
+
case 2:
|
|
1230
|
+
// block duplicate taps immediately (no re-render delay)
|
|
1231
|
+
rotationInProgressRef.current = true;
|
|
1221
1232
|
setIsRotating(true);
|
|
1222
|
-
_context.p =
|
|
1223
|
-
// ✅ CORRECTION : appliquer la rotation de façon incrémentale sur le fichier (pas cumulée sur un fichier déjà roté).
|
|
1224
|
-
// L'ancienne version rotait l'image déjà rotée par l'angle total, ce qui donnait des rotations incorrectes (90 + 180 => 270).
|
|
1233
|
+
_context.p = 3;
|
|
1225
1234
|
rotationAngle.current = (rotationAngle.current + degrees) % 360;
|
|
1226
|
-
|
|
1235
|
+
|
|
1236
|
+
// Use JPEG for preview rotation (faster than PNG for large images; quality 0.92 is fine for preview)
|
|
1237
|
+
_context.n = 4;
|
|
1227
1238
|
return ImageManipulator.manipulateAsync(image, [{
|
|
1228
1239
|
rotate: degrees
|
|
1229
1240
|
}], {
|
|
1230
|
-
compress:
|
|
1231
|
-
|
|
1232
|
-
format: ImageManipulator.SaveFormat.PNG // Format sans perte
|
|
1241
|
+
compress: 0.92,
|
|
1242
|
+
format: ImageManipulator.SaveFormat.JPEG
|
|
1233
1243
|
});
|
|
1234
|
-
case
|
|
1244
|
+
case 4:
|
|
1235
1245
|
rotated = _context.v;
|
|
1236
|
-
|
|
1246
|
+
setPoints([]);
|
|
1247
|
+
hasInitializedCropBox.current = false;
|
|
1237
1248
|
setImage(rotated.uri);
|
|
1238
|
-
console.log("Rotation applied
|
|
1239
|
-
_context.n =
|
|
1249
|
+
console.log("Rotation applied:", degrees, "degrees; accumulated:", rotationAngle.current);
|
|
1250
|
+
_context.n = 6;
|
|
1240
1251
|
break;
|
|
1241
|
-
case
|
|
1242
|
-
_context.p =
|
|
1252
|
+
case 5:
|
|
1253
|
+
_context.p = 5;
|
|
1243
1254
|
_t = _context.v;
|
|
1244
1255
|
console.error("Error rotating image:", _t);
|
|
1245
1256
|
alert("Error rotating image");
|
|
1246
|
-
case 5:
|
|
1247
|
-
_context.p = 5;
|
|
1248
|
-
setIsRotating(false);
|
|
1249
|
-
return _context.f(5);
|
|
1250
1257
|
case 6:
|
|
1258
|
+
_context.p = 6;
|
|
1259
|
+
rotationInProgressRef.current = false;
|
|
1260
|
+
setIsRotating(false);
|
|
1261
|
+
return _context.f(6);
|
|
1262
|
+
case 7:
|
|
1251
1263
|
return _context.a(2);
|
|
1252
1264
|
}
|
|
1253
|
-
}, _callee, null, [[
|
|
1265
|
+
}, _callee, null, [[3, 5, 6, 7]]);
|
|
1254
1266
|
}));
|
|
1255
1267
|
return function rotatePreviewImage(_x) {
|
|
1256
1268
|
return _ref2.apply(this, arguments);
|
|
@@ -1411,17 +1423,42 @@ var ImageCropper = function ImageCropper(_ref) {
|
|
|
1411
1423
|
fill: "white"
|
|
1412
1424
|
});
|
|
1413
1425
|
}));
|
|
1414
|
-
}()))
|
|
1426
|
+
}())), isRotating && /*#__PURE__*/_react["default"].createElement(_reactNative.View, {
|
|
1427
|
+
style: {
|
|
1428
|
+
position: 'absolute',
|
|
1429
|
+
left: 0,
|
|
1430
|
+
right: 0,
|
|
1431
|
+
top: 0,
|
|
1432
|
+
bottom: 0,
|
|
1433
|
+
backgroundColor: 'rgba(0,0,0,0.6)',
|
|
1434
|
+
justifyContent: 'center',
|
|
1435
|
+
alignItems: 'center'
|
|
1436
|
+
}
|
|
1437
|
+
}, /*#__PURE__*/_react["default"].createElement(_reactNative.ActivityIndicator, {
|
|
1438
|
+
size: "large",
|
|
1439
|
+
color: PRIMARY_GREEN
|
|
1440
|
+
}), /*#__PURE__*/_react["default"].createElement(_reactNative.Text, {
|
|
1441
|
+
style: {
|
|
1442
|
+
color: PRIMARY_GREEN,
|
|
1443
|
+
marginTop: 8,
|
|
1444
|
+
fontSize: 14
|
|
1445
|
+
}
|
|
1446
|
+
}, rotationLabel !== null && rotationLabel !== void 0 ? rotationLabel : 'Rotation...'))), !showResult && image && /*#__PURE__*/_react["default"].createElement(_reactNative.View, {
|
|
1415
1447
|
style: [_ImageCropperStyles["default"].buttonContainerBelow, {
|
|
1416
1448
|
paddingBottom: Math.max(insets.bottom, 16)
|
|
1417
1449
|
}]
|
|
1418
1450
|
}, _reactNative.Platform.OS === 'android' && /*#__PURE__*/_react["default"].createElement(_reactNative.TouchableOpacity, {
|
|
1419
|
-
style: _ImageCropperStyles["default"].rotationButton,
|
|
1451
|
+
style: [_ImageCropperStyles["default"].rotationButton, isRotating && {
|
|
1452
|
+
opacity: 0.7
|
|
1453
|
+
}],
|
|
1420
1454
|
onPress: function onPress() {
|
|
1421
1455
|
return enableRotation && rotatePreviewImage(90);
|
|
1422
1456
|
},
|
|
1423
1457
|
disabled: isRotating
|
|
1424
|
-
}, /*#__PURE__*/_react["default"].createElement(
|
|
1458
|
+
}, isRotating ? /*#__PURE__*/_react["default"].createElement(_reactNative.ActivityIndicator, {
|
|
1459
|
+
size: "small",
|
|
1460
|
+
color: "white"
|
|
1461
|
+
}) : /*#__PURE__*/_react["default"].createElement(_vectorIcons.Ionicons, {
|
|
1425
1462
|
name: "sync",
|
|
1426
1463
|
size: 24,
|
|
1427
1464
|
color: "white"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-expo-cropper",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.41",
|
|
4
4
|
"description": "Recadrage polygonal d'images.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"author": "PCS AGRI",
|
|
@@ -21,7 +21,6 @@
|
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
23
|
"@expo/vector-icons": "^15.0.2",
|
|
24
|
-
"@react-native-masked-view/masked-view": "^0.3.1",
|
|
25
24
|
"expo": "54.0.0",
|
|
26
25
|
"expo-camera": "~17.0.8",
|
|
27
26
|
"expo-image-manipulator": "~14.0.7",
|
|
@@ -30,7 +29,7 @@
|
|
|
30
29
|
"expo-screen-orientation": "~9.0.7",
|
|
31
30
|
"react-native-svg": "15.12.1",
|
|
32
31
|
"react-native-view-shot": "4.0.3",
|
|
33
|
-
"react-native-
|
|
32
|
+
"@react-native-masked-view/masked-view": "^0.3.1"
|
|
34
33
|
},
|
|
35
34
|
"overrides": {
|
|
36
35
|
"glob": "^10.5.0"
|
|
@@ -46,10 +45,10 @@
|
|
|
46
45
|
"url": "https://github.com/pcsagri/react-native-expo-cropper/issues"
|
|
47
46
|
},
|
|
48
47
|
"homepage": "https://github.com/pcsagri/react-native-expo-cropper#readme",
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
48
|
+
"scripts": {
|
|
49
|
+
"test": "echo \"Error: no test specified\" && exit 1",
|
|
50
|
+
"build": "npx --package=@babel/cli babel src --out-dir dist"
|
|
51
|
+
},
|
|
53
52
|
"devDependencies": {
|
|
54
53
|
"@babel/cli": "^7.28.3",
|
|
55
54
|
"@babel/core": "^7.28.5",
|
package/src/CustomCamera.js
CHANGED
|
@@ -11,36 +11,25 @@ import {
|
|
|
11
11
|
Platform,
|
|
12
12
|
} from 'react-native';
|
|
13
13
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
14
|
-
import {
|
|
15
|
-
Camera,
|
|
16
|
-
useCameraDevice,
|
|
17
|
-
useCameraPermission,
|
|
18
|
-
} from 'react-native-vision-camera';
|
|
19
|
-
|
|
14
|
+
import { Camera, CameraView } from 'expo-camera';
|
|
20
15
|
const { width } = Dimensions.get('window');
|
|
21
16
|
|
|
22
17
|
export default function CustomCamera({ onPhotoCaptured, onFrameCalculated }) {
|
|
23
18
|
const [isReady, setIsReady] = useState(false);
|
|
24
19
|
const [loadingBeforeCapture, setLoadingBeforeCapture] = useState(false);
|
|
20
|
+
const [hasPermission, setHasPermission] = useState(null);
|
|
25
21
|
const cameraRef = useRef(null);
|
|
26
22
|
const cameraWrapperRef = useRef(null);
|
|
27
23
|
const insets = useSafeAreaInsets();
|
|
28
|
-
const [cameraWrapperLayout, setCameraWrapperLayout] = useState({
|
|
29
|
-
width: 0,
|
|
30
|
-
height: 0,
|
|
31
|
-
x: 0,
|
|
32
|
-
y: 0,
|
|
33
|
-
});
|
|
24
|
+
const [cameraWrapperLayout, setCameraWrapperLayout] = useState({ width: 0, height: 0, x: 0, y: 0 });
|
|
34
25
|
const [greenFrame, setGreenFrame] = useState(null);
|
|
35
26
|
|
|
36
|
-
const { hasPermission, requestPermission } = useCameraPermission();
|
|
37
|
-
const device = useCameraDevice('back');
|
|
38
|
-
|
|
39
27
|
useEffect(() => {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
28
|
+
(async () => {
|
|
29
|
+
const { status } = await Camera.requestCameraPermissionsAsync();
|
|
30
|
+
setHasPermission(status === 'granted');
|
|
31
|
+
})();
|
|
32
|
+
}, []);
|
|
44
33
|
|
|
45
34
|
// Helper function to wait for multiple render cycles (works on iOS)
|
|
46
35
|
const waitForRender = (cycles = 5) => {
|
|
@@ -58,23 +47,28 @@ export default function CustomCamera({ onPhotoCaptured, onFrameCalculated }) {
|
|
|
58
47
|
});
|
|
59
48
|
};
|
|
60
49
|
|
|
61
|
-
|
|
62
|
-
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
// ✅ CRITICAL FIX: Calculate green frame coordinates relative to camera preview
|
|
53
|
+
// The green frame should be calculated on the wrapper (as it's visually drawn there)
|
|
54
|
+
// But we store it with wrapper dimensions so ImageCropper can map it correctly
|
|
55
|
+
//
|
|
56
|
+
// NOTE: Camera capture aspect ratio (typically 4:3) may differ from wrapper aspect ratio (9:16)
|
|
57
|
+
// This is handled in ImageCropper by using "cover" mode to match preview content
|
|
63
58
|
const calculateGreenFrameCoordinates = () => {
|
|
64
59
|
const wrapperWidth = cameraWrapperLayout.width;
|
|
65
60
|
const wrapperHeight = cameraWrapperLayout.height;
|
|
66
61
|
|
|
67
62
|
if (wrapperWidth === 0 || wrapperHeight === 0) {
|
|
68
|
-
console.warn(
|
|
69
|
-
'Camera wrapper layout not ready, cannot calculate green frame'
|
|
70
|
-
);
|
|
63
|
+
console.warn("Camera wrapper layout not ready, cannot calculate green frame");
|
|
71
64
|
return null;
|
|
72
65
|
}
|
|
73
66
|
|
|
74
|
-
|
|
75
|
-
const
|
|
76
|
-
const
|
|
77
|
-
const
|
|
67
|
+
// ✅ Calculate green frame as percentage of WRAPPER
|
|
68
|
+
const frameWidth = wrapperWidth * 0.85; // 85% of wrapper width
|
|
69
|
+
const frameHeight = wrapperHeight * 0.70; // 70% of wrapper height
|
|
70
|
+
const frameX = (wrapperWidth - frameWidth) / 2; // Centered horizontally
|
|
71
|
+
const frameY = (wrapperHeight - frameHeight) / 2; // Centered vertically
|
|
78
72
|
|
|
79
73
|
const frameCoords = {
|
|
80
74
|
x: frameX,
|
|
@@ -83,16 +77,19 @@ export default function CustomCamera({ onPhotoCaptured, onFrameCalculated }) {
|
|
|
83
77
|
height: frameHeight,
|
|
84
78
|
wrapperWidth,
|
|
85
79
|
wrapperHeight,
|
|
80
|
+
// ✅ Store percentages for easier mapping later
|
|
86
81
|
percentX: (frameX / wrapperWidth) * 100,
|
|
87
82
|
percentY: (frameY / wrapperHeight) * 100,
|
|
88
|
-
percentWidth: 85,
|
|
89
|
-
percentHeight: 70
|
|
83
|
+
percentWidth: 85, // 85% of wrapper width
|
|
84
|
+
percentHeight: 70 // 70% of wrapper height
|
|
90
85
|
};
|
|
91
86
|
|
|
92
|
-
console.log(
|
|
87
|
+
console.log("✅ Green frame coordinates calculated:", frameCoords);
|
|
93
88
|
return frameCoords;
|
|
94
89
|
};
|
|
95
90
|
|
|
91
|
+
// 🔁 Keep green frame state in sync with wrapper layout so we can both render it
|
|
92
|
+
// and send the exact same coordinates along with the captured photo.
|
|
96
93
|
useEffect(() => {
|
|
97
94
|
const coords = calculateGreenFrameCoordinates();
|
|
98
95
|
if (coords) {
|
|
@@ -101,82 +98,99 @@ export default function CustomCamera({ onPhotoCaptured, onFrameCalculated }) {
|
|
|
101
98
|
}, [cameraWrapperLayout]);
|
|
102
99
|
|
|
103
100
|
const takePicture = async () => {
|
|
104
|
-
if (
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
const photo = await cameraRef.current.takePhoto({
|
|
112
|
-
enableShutterSound: false,
|
|
113
|
-
flash: 'off',
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
if (!photo.path || !photo.width || !photo.height) {
|
|
117
|
-
throw new Error('Invalid photo received from camera');
|
|
118
|
-
}
|
|
101
|
+
if (cameraRef.current) {
|
|
102
|
+
try {
|
|
103
|
+
// Show loading after a delay (using setImmediate for iOS compatibility)
|
|
104
|
+
waitForRender(5).then(() => {
|
|
105
|
+
setLoadingBeforeCapture(true);
|
|
106
|
+
});
|
|
119
107
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
const capturedAspectRatio = photo.width / photo.height;
|
|
108
|
+
// Wait a bit before taking picture (works on iOS)
|
|
109
|
+
await waitForRender(2);
|
|
123
110
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
});
|
|
111
|
+
// ✅ OPTIMIZED: Capture with maximum quality and native camera ratio
|
|
112
|
+
// Platform-specific optimizations for best quality
|
|
113
|
+
const captureOptions = {
|
|
114
|
+
// Maximum quality (0-1, 1 = best quality, no compression)
|
|
115
|
+
quality: 1,
|
|
130
116
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
throw new Error('Green frame coordinates not available');
|
|
134
|
-
}
|
|
117
|
+
// Disable shutter sound for better UX
|
|
118
|
+
shutterSound: false,
|
|
135
119
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
120
|
+
// ✅ CRITICAL: skipProcessing preserves original resolution and avoids interpolation
|
|
121
|
+
// This ensures pixel-perfect quality and prevents premature resizing
|
|
122
|
+
skipProcessing: true,
|
|
123
|
+
|
|
124
|
+
// Include EXIF metadata (orientation, camera settings, etc.)
|
|
125
|
+
exif: true,
|
|
126
|
+
|
|
127
|
+
// ✅ Platform-specific optimizations
|
|
128
|
+
...(Platform.OS === 'ios' && {
|
|
129
|
+
// iOS: Use native capture format (typically 4:3 or 16:9 depending on device)
|
|
130
|
+
// No additional processing to preserve quality
|
|
131
|
+
}),
|
|
132
|
+
...(Platform.OS === 'android' && {
|
|
133
|
+
// Android: Ensure maximum resolution capture
|
|
134
|
+
// skipProcessing already handles this, but we can add Android-specific options if needed
|
|
135
|
+
}),
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
console.log("📸 Capturing photo with maximum quality settings:", {
|
|
139
|
+
platform: Platform.OS,
|
|
140
|
+
options: captureOptions,
|
|
141
|
+
wrapperSize: { width: cameraWrapperLayout.width, height: cameraWrapperLayout.height }
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const photo = await cameraRef.current.takePictureAsync(captureOptions);
|
|
145
|
+
|
|
146
|
+
// ✅ Validate captured photo dimensions
|
|
147
|
+
if (!photo.width || !photo.height || photo.width === 0 || photo.height === 0) {
|
|
148
|
+
throw new Error("Invalid photo dimensions received from camera");
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const capturedAspectRatio = photo.width / photo.height;
|
|
152
|
+
console.log("✅ Photo captured with maximum quality:", {
|
|
153
|
+
uri: photo.uri,
|
|
154
|
+
width: photo.width,
|
|
155
|
+
height: photo.height,
|
|
156
|
+
aspectRatio: capturedAspectRatio.toFixed(3),
|
|
157
|
+
expectedRatio: "~1.33 (4:3) or ~1.78 (16:9)",
|
|
158
|
+
exif: photo.exif ? "present" : "missing",
|
|
159
|
+
fileSize: photo.uri ? "available" : "unknown"
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
// ✅ CRITICAL FIX: Use the same green frame coordinates that are used for rendering
|
|
163
|
+
// Fallback to recalculation if, for some reason, state is not yet set
|
|
164
|
+
const greenFrameCoords = greenFrame || calculateGreenFrameCoordinates();
|
|
165
|
+
|
|
166
|
+
if (!greenFrameCoords) {
|
|
167
|
+
throw new Error("Green frame coordinates not available");
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ✅ Send photo URI and frame data to ImageCropper
|
|
171
|
+
// The photo maintains its native resolution and aspect ratio
|
|
172
|
+
// ImageCropper will handle display and cropping while preserving quality
|
|
173
|
+
onPhotoCaptured(photo.uri, {
|
|
174
|
+
greenFrame: greenFrameCoords,
|
|
175
|
+
capturedImageSize: {
|
|
176
|
+
width: photo.width,
|
|
177
|
+
height: photo.height,
|
|
178
|
+
aspectRatio: capturedAspectRatio
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
setLoadingBeforeCapture(false);
|
|
183
|
+
} catch (error) {
|
|
184
|
+
console.error("❌ Error capturing photo:", error);
|
|
185
|
+
setLoadingBeforeCapture(false);
|
|
186
|
+
Alert.alert(
|
|
187
|
+
"Erreur",
|
|
188
|
+
`Impossible de capturer la photo: ${error.message || "Erreur inconnue"}. Veuillez réessayer.`
|
|
189
|
+
);
|
|
190
|
+
}
|
|
153
191
|
}
|
|
154
192
|
};
|
|
155
193
|
|
|
156
|
-
if (!hasPermission) {
|
|
157
|
-
return (
|
|
158
|
-
<SafeAreaView style={styles.outerContainer}>
|
|
159
|
-
<View style={styles.permissionContainer}>
|
|
160
|
-
<Text style={styles.text}>
|
|
161
|
-
Accès à la caméra requis pour prendre des photos.
|
|
162
|
-
</Text>
|
|
163
|
-
<TouchableOpacity style={styles.button} onPress={requestPermission}>
|
|
164
|
-
<Text style={styles.buttonText}>Autoriser la caméra</Text>
|
|
165
|
-
</TouchableOpacity>
|
|
166
|
-
</View>
|
|
167
|
-
</SafeAreaView>
|
|
168
|
-
);
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
if (!device) {
|
|
172
|
-
return (
|
|
173
|
-
<SafeAreaView style={styles.outerContainer}>
|
|
174
|
-
<View style={styles.permissionContainer}>
|
|
175
|
-
<Text style={styles.text}>Aucune caméra arrière disponible.</Text>
|
|
176
|
-
</View>
|
|
177
|
-
</SafeAreaView>
|
|
178
|
-
);
|
|
179
|
-
}
|
|
180
194
|
|
|
181
195
|
return (
|
|
182
196
|
<SafeAreaView style={styles.outerContainer}>
|
|
@@ -189,23 +203,25 @@ export default function CustomCamera({ onPhotoCaptured, onFrameCalculated }) {
|
|
|
189
203
|
width: layout.width,
|
|
190
204
|
height: layout.height,
|
|
191
205
|
x: layout.x,
|
|
192
|
-
y: layout.y
|
|
206
|
+
y: layout.y
|
|
193
207
|
});
|
|
194
|
-
console.log(
|
|
208
|
+
console.log("Camera wrapper layout updated:", layout);
|
|
195
209
|
}}
|
|
196
210
|
>
|
|
197
|
-
<
|
|
211
|
+
<CameraView
|
|
212
|
+
style={styles.camera}
|
|
213
|
+
facing="back"
|
|
198
214
|
ref={cameraRef}
|
|
199
|
-
|
|
200
|
-
device={device}
|
|
201
|
-
isActive={true}
|
|
202
|
-
photo={true}
|
|
203
|
-
onInitialized={() => {
|
|
215
|
+
onCameraReady={() => {
|
|
204
216
|
setIsReady(true);
|
|
205
|
-
console.log(
|
|
217
|
+
console.log("✅ Camera ready - Maximum quality capture enabled");
|
|
206
218
|
}}
|
|
219
|
+
// ✅ Ensure camera uses native aspect ratio (typically 4:3 for most devices)
|
|
220
|
+
// The wrapper constrains the view, but capture uses full sensor resolution
|
|
221
|
+
// This ensures preview matches what will be captured
|
|
207
222
|
/>
|
|
208
223
|
|
|
224
|
+
{/* Loading overlay */}
|
|
209
225
|
{loadingBeforeCapture && (
|
|
210
226
|
<>
|
|
211
227
|
<View style={styles.loadingOverlay}>
|
|
@@ -219,6 +235,7 @@ export default function CustomCamera({ onPhotoCaptured, onFrameCalculated }) {
|
|
|
219
235
|
</>
|
|
220
236
|
)}
|
|
221
237
|
|
|
238
|
+
{/* Cadre de scan - rendered using calculated coordinates (x, y, width, height) */}
|
|
222
239
|
{greenFrame && (
|
|
223
240
|
<View
|
|
224
241
|
style={[
|
|
@@ -234,12 +251,7 @@ export default function CustomCamera({ onPhotoCaptured, onFrameCalculated }) {
|
|
|
234
251
|
)}
|
|
235
252
|
</View>
|
|
236
253
|
|
|
237
|
-
<View
|
|
238
|
-
style={[
|
|
239
|
-
styles.buttonContainer,
|
|
240
|
-
{ bottom: (insets?.bottom || 0) + 16, marginBottom: 0 },
|
|
241
|
-
]}
|
|
242
|
-
>
|
|
254
|
+
<View style={[styles.buttonContainer, { bottom: (insets?.bottom || 0) + 16, marginBottom: 0 }]}>
|
|
243
255
|
<TouchableOpacity
|
|
244
256
|
style={styles.button}
|
|
245
257
|
onPress={takePicture}
|
|
@@ -270,6 +282,9 @@ const styles = StyleSheet.create({
|
|
|
270
282
|
justifyContent: 'center',
|
|
271
283
|
position: 'relative',
|
|
272
284
|
},
|
|
285
|
+
camera: {
|
|
286
|
+
...StyleSheet.absoluteFillObject,
|
|
287
|
+
},
|
|
273
288
|
scanFrame: {
|
|
274
289
|
position: 'absolute',
|
|
275
290
|
borderWidth: 4,
|
|
@@ -293,6 +308,14 @@ const styles = StyleSheet.create({
|
|
|
293
308
|
zIndex: 21,
|
|
294
309
|
backgroundColor: 'transparent',
|
|
295
310
|
},
|
|
311
|
+
cancelIcon: {
|
|
312
|
+
position: 'absolute',
|
|
313
|
+
top: 20,
|
|
314
|
+
left: 20,
|
|
315
|
+
backgroundColor: PRIMARY_GREEN,
|
|
316
|
+
borderRadius: 5,
|
|
317
|
+
padding: 8,
|
|
318
|
+
},
|
|
296
319
|
buttonContainer: {
|
|
297
320
|
position: 'absolute',
|
|
298
321
|
bottom: 0,
|
|
@@ -314,15 +337,16 @@ const styles = StyleSheet.create({
|
|
|
314
337
|
fontSize: 18,
|
|
315
338
|
color: GLOW_WHITE,
|
|
316
339
|
},
|
|
317
|
-
|
|
340
|
+
container: {
|
|
318
341
|
flex: 1,
|
|
342
|
+
backgroundColor: DEEP_BLACK,
|
|
319
343
|
justifyContent: 'center',
|
|
320
344
|
alignItems: 'center',
|
|
321
345
|
padding: 20,
|
|
322
346
|
},
|
|
323
|
-
|
|
324
|
-
fontSize:
|
|
325
|
-
color:
|
|
347
|
+
iconText: {
|
|
348
|
+
fontSize: 18,
|
|
349
|
+
color: GLOW_WHITE,
|
|
326
350
|
fontWeight: '600',
|
|
327
351
|
},
|
|
328
352
|
});
|
package/src/ImageCropper.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import styles from './ImageCropperStyles';
|
|
2
2
|
import React, { useState, useRef, useEffect } from 'react';
|
|
3
|
-
import { Modal, View, Image, Dimensions, TouchableOpacity, Animated, Text, Platform, SafeAreaView, PixelRatio, StyleSheet } from 'react-native';
|
|
3
|
+
import { Modal, View, Image, Dimensions, TouchableOpacity, Animated, Text, Platform, SafeAreaView, PixelRatio, StyleSheet, ActivityIndicator } from 'react-native';
|
|
4
4
|
import Svg, { Path, Circle } from 'react-native-svg';
|
|
5
5
|
import CustomCamera from './CustomCamera';
|
|
6
6
|
import * as ImageManipulator from 'expo-image-manipulator';
|
|
@@ -8,7 +8,8 @@ import { Ionicons } from '@expo/vector-icons';
|
|
|
8
8
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
9
9
|
import { applyMaskToImage, MaskView } from './ImageMaskProcessor';
|
|
10
10
|
|
|
11
|
-
const
|
|
11
|
+
const PRIMARY_GREEN = '#198754';
|
|
12
|
+
const ImageCropper = ({ onConfirm, openCameraFirst, initialImage, addheight, rotationLabel }) => {
|
|
12
13
|
|
|
13
14
|
|
|
14
15
|
const [image, setImage] = useState(null);
|
|
@@ -132,6 +133,7 @@ const ImageCropper = ({ onConfirm, openCameraFirst, initialImage ,addheight}) =>
|
|
|
132
133
|
const [isLoading, setIsLoading] = useState(false);
|
|
133
134
|
const [showFullScreenCapture, setShowFullScreenCapture] = useState(false);
|
|
134
135
|
const [isRotating, setIsRotating] = useState(false);
|
|
136
|
+
const rotationInProgressRef = useRef(false); // block duplicate taps immediately
|
|
135
137
|
const lastValidPosition = useRef(null);
|
|
136
138
|
const insets = useSafeAreaInsets();
|
|
137
139
|
|
|
@@ -139,7 +141,7 @@ const ImageCropper = ({ onConfirm, openCameraFirst, initialImage ,addheight}) =>
|
|
|
139
141
|
|
|
140
142
|
// No view-shot / captureRef / bitmap masking on device.
|
|
141
143
|
const enableMask = false;
|
|
142
|
-
const enableRotation =
|
|
144
|
+
const enableRotation = true; // rotation resets crop box so it is re-initialized on rotated image.
|
|
143
145
|
|
|
144
146
|
|
|
145
147
|
|
|
@@ -1027,32 +1029,34 @@ const ImageCropper = ({ onConfirm, openCameraFirst, initialImage ,addheight}) =>
|
|
|
1027
1029
|
|
|
1028
1030
|
// ✅ REFACTORISATION : Stocker l'angle de rotation au lieu de modifier l'image immédiatement
|
|
1029
1031
|
// La rotation sera appliquée uniquement lors du crop final pour éviter les interpolations multiples
|
|
1030
|
-
|
|
1031
|
-
if (!image
|
|
1032
|
+
const rotatePreviewImage = async (degrees) => {
|
|
1033
|
+
if (!image) return;
|
|
1034
|
+
if (rotationInProgressRef.current) return; // block duplicate taps immediately (no re-render delay)
|
|
1035
|
+
rotationInProgressRef.current = true;
|
|
1032
1036
|
setIsRotating(true);
|
|
1033
|
-
|
|
1037
|
+
|
|
1034
1038
|
try {
|
|
1035
|
-
// ✅ CORRECTION : appliquer la rotation de façon incrémentale sur le fichier (pas cumulée sur un fichier déjà roté).
|
|
1036
|
-
// L'ancienne version rotait l'image déjà rotée par l'angle total, ce qui donnait des rotations incorrectes (90 + 180 => 270).
|
|
1037
1039
|
rotationAngle.current = (rotationAngle.current + degrees) % 360;
|
|
1038
1040
|
|
|
1041
|
+
// Use JPEG for preview rotation (faster than PNG for large images; quality 0.92 is fine for preview)
|
|
1039
1042
|
const rotated = await ImageManipulator.manipulateAsync(
|
|
1040
1043
|
image,
|
|
1041
1044
|
[{ rotate: degrees }],
|
|
1042
|
-
{
|
|
1043
|
-
compress:
|
|
1044
|
-
format: ImageManipulator.SaveFormat.
|
|
1045
|
+
{
|
|
1046
|
+
compress: 0.92,
|
|
1047
|
+
format: ImageManipulator.SaveFormat.JPEG,
|
|
1045
1048
|
}
|
|
1046
1049
|
);
|
|
1047
|
-
|
|
1048
|
-
|
|
1050
|
+
|
|
1051
|
+
setPoints([]);
|
|
1052
|
+
hasInitializedCropBox.current = false;
|
|
1049
1053
|
setImage(rotated.uri);
|
|
1050
|
-
|
|
1051
|
-
console.log("Rotation applied (preview increment):", degrees, "degrees; accumulated:", rotationAngle.current);
|
|
1054
|
+
console.log("Rotation applied:", degrees, "degrees; accumulated:", rotationAngle.current);
|
|
1052
1055
|
} catch (error) {
|
|
1053
1056
|
console.error("Error rotating image:", error);
|
|
1054
1057
|
alert("Error rotating image");
|
|
1055
1058
|
} finally {
|
|
1059
|
+
rotationInProgressRef.current = false;
|
|
1056
1060
|
setIsRotating(false);
|
|
1057
1061
|
}
|
|
1058
1062
|
};
|
|
@@ -1214,6 +1218,12 @@ const ImageCropper = ({ onConfirm, openCameraFirst, initialImage ,addheight}) =>
|
|
|
1214
1218
|
})()}
|
|
1215
1219
|
</Svg>
|
|
1216
1220
|
</View>
|
|
1221
|
+
{isRotating && (
|
|
1222
|
+
<View style={{ position: 'absolute', left: 0, right: 0, top: 0, bottom: 0, backgroundColor: 'rgba(0,0,0,0.6)', justifyContent: 'center', alignItems: 'center' }}>
|
|
1223
|
+
<ActivityIndicator size="large" color={PRIMARY_GREEN} />
|
|
1224
|
+
<Text style={{ color: PRIMARY_GREEN, marginTop: 8, fontSize: 14 }}>{rotationLabel ?? 'Rotation...'}</Text>
|
|
1225
|
+
</View>
|
|
1226
|
+
)}
|
|
1217
1227
|
</View>
|
|
1218
1228
|
)}
|
|
1219
1229
|
|
|
@@ -1222,11 +1232,15 @@ const ImageCropper = ({ onConfirm, openCameraFirst, initialImage ,addheight}) =>
|
|
|
1222
1232
|
<View style={[styles.buttonContainerBelow, { paddingBottom: Math.max(insets.bottom, 16) }]}>
|
|
1223
1233
|
{Platform.OS === 'android' && (
|
|
1224
1234
|
<TouchableOpacity
|
|
1225
|
-
style={styles.rotationButton}
|
|
1235
|
+
style={[styles.rotationButton, isRotating && { opacity: 0.7 }]}
|
|
1226
1236
|
onPress={() => enableRotation && rotatePreviewImage(90)}
|
|
1227
1237
|
disabled={isRotating}
|
|
1228
1238
|
>
|
|
1229
|
-
|
|
1239
|
+
{isRotating ? (
|
|
1240
|
+
<ActivityIndicator size="small" color="white" />
|
|
1241
|
+
) : (
|
|
1242
|
+
<Ionicons name="sync" size={24} color="white" />
|
|
1243
|
+
)}
|
|
1230
1244
|
</TouchableOpacity>
|
|
1231
1245
|
)}
|
|
1232
1246
|
|