react-native-expo-cropper 1.2.36 → 1.2.37

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.
@@ -1,514 +1,1621 @@
1
- "use strict";
2
-
3
- function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); }
4
- Object.defineProperty(exports, "__esModule", {
5
- value: true
6
- });
7
- exports["default"] = void 0;
8
- var _ImageCropperStyles = _interopRequireDefault(require("./ImageCropperStyles"));
9
- var _react = _interopRequireWildcard(require("react"));
10
- var _reactNative = require("react-native");
11
- var _reactNativeSvg = _interopRequireWildcard(require("react-native-svg"));
12
- var _reactNativeViewShot = require("react-native-view-shot");
13
- var _CustomCamera = _interopRequireDefault(require("./CustomCamera"));
14
- var _ImageProcessor = require("./ImageProcessor");
15
- var ImageManipulator = _interopRequireWildcard(require("expo-image-manipulator"));
16
- var _vectorIcons = require("@expo/vector-icons");
17
- var _reactNativeSafeAreaContext = require("react-native-safe-area-context");
18
- 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 _t4 in e) "default" !== _t4 && {}.hasOwnProperty.call(e, _t4) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, _t4)) && (i.get || i.set) ? o(f, _t4, i) : f[_t4] = e[_t4]); return f; })(e, t); }
19
- function _interopRequireDefault(e) { return e && e.__esModule ? e : { "default": e }; }
20
- function _regenerator() { /*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/babel/babel/blob/main/packages/babel-helpers/LICENSE */ var e, t, r = "function" == typeof Symbol ? Symbol : {}, n = r.iterator || "@@iterator", o = r.toStringTag || "@@toStringTag"; function i(r, n, o, i) { var c = n && n.prototype instanceof Generator ? n : Generator, u = Object.create(c.prototype); return _regeneratorDefine2(u, "_invoke", function (r, n, o) { var i, c, u, f = 0, p = o || [], y = !1, G = { p: 0, n: 0, v: e, a: d, f: d.bind(e, 4), d: function d(t, r) { return i = t, c = 0, u = e, G.n = r, a; } }; function d(r, n) { for (c = r, u = n, t = 0; !y && f && !o && t < p.length; t++) { var o, i = p[t], d = G.p, l = i[2]; r > 3 ? (o = l === n) && (u = i[(c = i[4]) ? 5 : (c = 3, 3)], i[4] = i[5] = e) : i[0] <= d && ((o = r < 2 && d < i[1]) ? (c = 0, G.v = n, G.n = i[1]) : d < l && (o = r < 3 || i[0] > n || n > l) && (i[4] = r, i[5] = n, G.n = l, c = 0)); } if (o || r > 1) return a; throw y = !0, n; } return function (o, p, l) { if (f > 1) throw TypeError("Generator is already running"); for (y && 1 === p && d(p, l), c = p, u = l; (t = c < 2 ? e : u) || !y;) { i || (c ? c < 3 ? (c > 1 && (G.n = -1), d(c, u)) : G.n = u : G.v = u); try { if (f = 2, i) { if (c || (o = "next"), t = i[o]) { if (!(t = t.call(i, u))) throw TypeError("iterator result is not an object"); if (!t.done) return t; u = t.value, c < 2 && (c = 0); } else 1 === c && (t = i["return"]) && t.call(i), c < 2 && (u = TypeError("The iterator does not provide a '" + o + "' method"), c = 1); i = e; } else if ((t = (y = G.n < 0) ? u : r.call(n, G)) !== a) break; } catch (t) { i = e, c = 1, u = t; } finally { f = 1; } } return { value: t, done: y }; }; }(r, o, i), !0), u; } var a = {}; function Generator() {} function GeneratorFunction() {} function GeneratorFunctionPrototype() {} t = Object.getPrototypeOf; var c = [][n] ? t(t([][n]())) : (_regeneratorDefine2(t = {}, n, function () { return this; }), t), u = GeneratorFunctionPrototype.prototype = Generator.prototype = Object.create(c); function f(e) { return Object.setPrototypeOf ? Object.setPrototypeOf(e, GeneratorFunctionPrototype) : (e.__proto__ = GeneratorFunctionPrototype, _regeneratorDefine2(e, o, "GeneratorFunction")), e.prototype = Object.create(u), e; } return GeneratorFunction.prototype = GeneratorFunctionPrototype, _regeneratorDefine2(u, "constructor", GeneratorFunctionPrototype), _regeneratorDefine2(GeneratorFunctionPrototype, "constructor", GeneratorFunction), GeneratorFunction.displayName = "GeneratorFunction", _regeneratorDefine2(GeneratorFunctionPrototype, o, "GeneratorFunction"), _regeneratorDefine2(u), _regeneratorDefine2(u, o, "Generator"), _regeneratorDefine2(u, n, function () { return this; }), _regeneratorDefine2(u, "toString", function () { return "[object Generator]"; }), (_regenerator = function _regenerator() { return { w: i, m: f }; })(); }
21
- function _regeneratorDefine2(e, r, n, t) { var i = Object.defineProperty; try { i({}, "", {}); } catch (e) { i = 0; } _regeneratorDefine2 = function _regeneratorDefine(e, r, n, t) { function o(r, n) { _regeneratorDefine2(e, r, function (e) { return this._invoke(r, n, e); }); } r ? i ? i(e, r, { value: n, enumerable: !t, configurable: !t, writable: !t }) : e[r] = n : (o("next", 0), o("throw", 1), o("return", 2)); }, _regeneratorDefine2(e, r, n, t); }
22
- function asyncGeneratorStep(n, t, e, r, o, a, c) { try { var i = n[a](c), u = i.value; } catch (n) { return void e(n); } i.done ? t(u) : Promise.resolve(u).then(r, o); }
23
- function _asyncToGenerator(n) { return function () { var t = this, e = arguments; return new Promise(function (r, o) { var a = n.apply(t, e); function _next(n) { asyncGeneratorStep(a, r, o, _next, _throw, "next", n); } function _throw(n) { asyncGeneratorStep(a, r, o, _next, _throw, "throw", n); } _next(void 0); }); }; }
24
- 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; }
25
- 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; }
26
- function _defineProperty(e, r, t) { return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: !0, configurable: !0, writable: !0 }) : e[r] = t, e; }
27
- function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == _typeof(i) ? i : i + ""; }
28
- function _toPrimitive(t, r) { if ("object" != _typeof(t) || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != _typeof(i)) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
29
- function _toConsumableArray(r) { return _arrayWithoutHoles(r) || _iterableToArray(r) || _unsupportedIterableToArray(r) || _nonIterableSpread(); }
30
- function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }
31
- function _iterableToArray(r) { if ("undefined" != typeof Symbol && null != r[Symbol.iterator] || null != r["@@iterator"]) return Array.from(r); }
32
- function _arrayWithoutHoles(r) { if (Array.isArray(r)) return _arrayLikeToArray(r); }
33
- function _slicedToArray(r, e) { return _arrayWithHoles(r) || _iterableToArrayLimit(r, e) || _unsupportedIterableToArray(r, e) || _nonIterableRest(); }
34
- function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }
35
- function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } }
36
- 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; }
37
- 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; } }
38
- function _arrayWithHoles(r) { if (Array.isArray(r)) return r; }
39
- var ImageCropper = function ImageCropper(_ref) {
40
- var onConfirm = _ref.onConfirm,
41
- openCameraFirst = _ref.openCameraFirst,
42
- initialImage = _ref.initialImage,
43
- addheight = _ref.addheight;
44
- var _useState = (0, _react.useState)(null),
45
- _useState2 = _slicedToArray(_useState, 2),
46
- image = _useState2[0],
47
- setImage = _useState2[1];
48
- var _useState3 = (0, _react.useState)([]),
49
- _useState4 = _slicedToArray(_useState3, 2),
50
- points = _useState4[0],
51
- setPoints = _useState4[1];
52
- var _useState5 = (0, _react.useState)(false),
53
- _useState6 = _slicedToArray(_useState5, 2),
54
- showResult = _useState6[0],
55
- setShowResult = _useState6[1];
56
- var _useState7 = (0, _react.useState)(false),
57
- _useState8 = _slicedToArray(_useState7, 2),
58
- showCustomCamera = _useState8[0],
59
- setShowCustomCamera = _useState8[1];
60
- var viewRef = (0, _react.useRef)(null);
61
- var imageMeasure = (0, _react.useRef)({
62
- x: 0,
63
- y: 0,
64
- width: 0,
65
- height: 0
66
- });
67
- var selectedPointIndex = (0, _react.useRef)(null);
68
- var lastTap = (0, _react.useRef)(null);
69
- var _useState9 = (0, _react.useState)(false),
70
- _useState0 = _slicedToArray(_useState9, 2),
71
- isLoading = _useState0[0],
72
- setIsLoading = _useState0[1];
73
- var _useState1 = (0, _react.useState)(false),
74
- _useState10 = _slicedToArray(_useState1, 2),
75
- showFullScreenCapture = _useState10[0],
76
- setShowFullScreenCapture = _useState10[1];
77
- var _useState11 = (0, _react.useState)(false),
78
- _useState12 = _slicedToArray(_useState11, 2),
79
- isRotating = _useState12[0],
80
- setIsRotating = _useState12[1];
81
- var lastValidPosition = (0, _react.useRef)(null);
82
- var insets = (0, _reactNativeSafeAreaContext.useSafeAreaInsets)();
83
- (0, _react.useEffect)(function () {
84
- if (openCameraFirst) {
85
- setShowCustomCamera(true);
86
- } else if (initialImage) {
87
- setImage(initialImage);
88
- }
89
- }, [openCameraFirst, initialImage]);
90
- (0, _react.useEffect)(function () {
91
- if (!image) return;
92
- _reactNative.Image.getSize(image, function (imgWidth, imgHeight) {
93
- var screenRatio = SCREEN_WIDTH / SCREEN_HEIGHT;
94
- var imageRatio = imgWidth / imgHeight;
95
- if (imageRatio > screenRatio) {
96
- imageMeasure.current = {
97
- width: SCREEN_WIDTH,
98
- height: SCREEN_WIDTH / imageRatio
99
- };
100
- } else {
101
- imageMeasure.current = {
102
- width: SCREEN_HEIGHT * imageRatio,
103
- height: SCREEN_HEIGHT
104
- };
105
- }
106
- });
107
- }, [image]);
108
- var initializeCropBox = function initializeCropBox() {
109
- var _imageMeasure$current = imageMeasure.current,
110
- width = _imageMeasure$current.width,
111
- height = _imageMeasure$current.height;
112
- // if (width === 0 || height === 0 || points.length > 0) return;
113
- var boxWidth = width * 0.8;
114
- var boxHeight = height * 0.8;
115
- var centerX = width / 2;
116
- var centerY = height / 2;
117
- setPoints([{
118
- x: centerX - boxWidth / 2,
119
- y: centerY - boxHeight / 2
120
- }, {
121
- x: centerX + boxWidth / 2,
122
- y: centerY - boxHeight / 2
123
- }, {
124
- x: centerX + boxWidth / 2,
125
- y: centerY + boxHeight / 2
126
- }, {
127
- x: centerX - boxWidth / 2,
128
- y: centerY + boxHeight / 2
129
- }]);
130
- };
131
- var onImageLayout = function onImageLayout(e) {
132
- var layout = e.nativeEvent.layout;
133
- imageMeasure.current = {
134
- x: layout.x,
135
- y: layout.y,
136
- width: layout.width,
137
- height: layout.height
138
- };
139
- initializeCropBox();
140
- };
141
- var createPath = function createPath() {
142
- if (points.length < 1) return '';
143
- var path = "M ".concat(points[0].x, " ").concat(points[0].y, " ");
144
- points.forEach(function (point) {
145
- return path += "L ".concat(point.x, " ").concat(point.y, " ");
146
- });
147
- return path + 'Z';
148
- };
149
- var handleTap = function handleTap(e) {
150
- if (!image || showResult) return;
151
- var now = Date.now();
152
- var _e$nativeEvent = e.nativeEvent,
153
- tapX = _e$nativeEvent.locationX,
154
- tapY = _e$nativeEvent.locationY;
155
- if (lastTap.current && now - lastTap.current < 300) {
156
- var exists = points.some(function (p) {
157
- return Math.abs(p.x - tapX) < 15 && Math.abs(p.y - tapY) < 15;
158
- });
159
- if (!exists) setPoints([].concat(_toConsumableArray(points), [{
160
- x: tapX,
161
- y: tapY
162
- }]));
163
- lastTap.current = null;
164
- } else {
165
- var index = points.findIndex(function (p) {
166
- return Math.abs(p.x - tapX) < 20 && Math.abs(p.y - tapY) < 20;
167
- });
168
- if (index !== -1) {
169
- selectedPointIndex.current = index;
170
- lastValidPosition.current = _objectSpread({}, points[index]); // store original position before move
171
- }
172
- lastTap.current = now;
173
- }
174
- };
175
- var handleMove = function handleMove(e) {
176
- if (showResult || selectedPointIndex.current === null) return;
177
- var _e$nativeEvent2 = e.nativeEvent,
178
- moveX = _e$nativeEvent2.locationX,
179
- moveY = _e$nativeEvent2.locationY;
180
- var width = imageMeasure.current.width;
181
- var height = imageMeasure.current.height;
182
- var boundedX = Math.max(0, Math.min(moveX, width));
183
- var boundedY = Math.max(0, Math.min(moveY, height));
184
- var edgeThreshold = 10;
185
- var isNearTopOrBottomEdge = boundedY <= edgeThreshold || boundedY >= height - edgeThreshold;
186
- var isNearLeftOrRightEdge = boundedX <= edgeThreshold || boundedX >= width - edgeThreshold;
187
- if (isNearTopOrBottomEdge || isNearLeftOrRightEdge) {
188
- // Reset point to last known position
189
- if (lastValidPosition.current && selectedPointIndex.current !== null) {
190
- setPoints(function (prev) {
191
- return prev.map(function (p, i) {
192
- return i === selectedPointIndex.current ? lastValidPosition.current : p;
193
- });
194
- });
195
- }
196
- selectedPointIndex.current = null;
197
- return;
198
- }
199
-
200
- // Valid move update point and store as new last valid position
201
- var updatedPoint = {
202
- x: boundedX,
203
- y: boundedY
204
- };
205
- lastValidPosition.current = updatedPoint;
206
- setPoints(function (prev) {
207
- return prev.map(function (p, i) {
208
- return i === selectedPointIndex.current ? updatedPoint : p;
209
- });
210
- });
211
- };
212
- var handleRelease = function handleRelease() {
213
- selectedPointIndex.current = null;
214
- };
215
- var handleReset = function handleReset() {
216
- // setPoints([]);
217
- initializeCropBox();
218
- };
219
- var rotatePreviewImage = /*#__PURE__*/function () {
220
- var _ref2 = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee(degrees) {
221
- var rotated, _t;
222
- return _regenerator().w(function (_context) {
223
- while (1) switch (_context.p = _context.n) {
224
- case 0:
225
- if (!(!image || isRotating)) {
226
- _context.n = 1;
227
- break;
228
- }
229
- return _context.a(2);
230
- case 1:
231
- setIsRotating(true);
232
- _context.p = 2;
233
- _context.n = 3;
234
- return ImageManipulator.manipulateAsync(image, [{
235
- rotate: degrees
236
- }], {
237
- compress: 0.85,
238
- format: ImageManipulator.SaveFormat.JPEG
239
- });
240
- case 3:
241
- rotated = _context.v;
242
- // Update image - onImageLayout will call initializeCropBox automatically
243
- setImage(rotated.uri);
244
- _context.n = 5;
245
- break;
246
- case 4:
247
- _context.p = 4;
248
- _t = _context.v;
249
- console.error("Error rotating image:", _t);
250
- alert("Error rotating image");
251
- case 5:
252
- _context.p = 5;
253
- setIsRotating(false);
254
- return _context.f(5);
255
- case 6:
256
- return _context.a(2);
257
- }
258
- }, _callee, null, [[2, 4, 5, 6]]);
259
- }));
260
- return function rotatePreviewImage(_x) {
261
- return _ref2.apply(this, arguments);
262
- };
263
- }();
264
-
265
- // Helper function to wait for multiple render cycles (works on iOS)
266
- var waitForRender = function waitForRender() {
267
- var cycles = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 5;
268
- return new Promise(function (resolve) {
269
- var count = 0;
270
- var _tick = function tick() {
271
- count++;
272
- if (count >= cycles) {
273
- resolve();
274
- } else {
275
- setImmediate(_tick);
276
- }
277
- };
278
- setImmediate(_tick);
279
- });
280
- };
281
- return /*#__PURE__*/_react["default"].createElement(_reactNative.View, {
282
- style: _ImageCropperStyles["default"].container
283
- }, showCustomCamera ? /*#__PURE__*/_react["default"].createElement(_CustomCamera["default"], {
284
- onPhotoCaptured: function onPhotoCaptured(uri) {
285
- setImage(uri);
286
- setShowCustomCamera(false);
287
- },
288
- onCancel: function onCancel() {
289
- return setShowCustomCamera(false);
290
- }
291
- }) : /*#__PURE__*/_react["default"].createElement(_react["default"].Fragment, null, !showResult && /*#__PURE__*/_react["default"].createElement(_reactNative.View, {
292
- style: image ? _ImageCropperStyles["default"].buttonContainer : _ImageCropperStyles["default"].centerButtonsContainer
293
- }, image && _reactNative.Platform.OS === 'android' && /*#__PURE__*/_react["default"].createElement(_react["default"].Fragment, null, /*#__PURE__*/_react["default"].createElement(_reactNative.TouchableOpacity, {
294
- style: _ImageCropperStyles["default"].iconButton,
295
- onPress: function onPress() {
296
- return rotatePreviewImage(90);
297
- },
298
- disabled: isRotating
299
- }, /*#__PURE__*/_react["default"].createElement(_vectorIcons.Ionicons, {
300
- name: "sync",
301
- size: 24,
302
- color: "white"
303
- }))), image && /*#__PURE__*/_react["default"].createElement(_reactNative.TouchableOpacity, {
304
- style: _ImageCropperStyles["default"].button,
305
- onPress: handleReset
306
- }, /*#__PURE__*/_react["default"].createElement(_reactNative.Text, {
307
- style: _ImageCropperStyles["default"].buttonText
308
- }, "Reset")), image && /*#__PURE__*/_react["default"].createElement(_reactNative.TouchableOpacity, {
309
- style: _ImageCropperStyles["default"].button,
310
- onPress: /*#__PURE__*/_asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee2() {
311
- var capturedUri, enhancedUri, croppedUri, enhancedImageInfo, actualImageWidth, actualImageHeight, displayedWidth, displayedHeight, scaleX, scaleY, imagePoints, minX, minY, maxX, maxY, cropX, cropY, cropWidth, cropHeight, croppedResult, name, _t2, _t3;
312
- return _regenerator().w(function (_context2) {
313
- while (1) switch (_context2.p = _context2.n) {
314
- case 0:
315
- // setShowFullScreenCapture(true);
316
- setIsLoading(true);
317
- setShowResult(true);
318
- _context2.p = 1;
319
- if (!(_reactNative.Platform.OS === 'android')) {
320
- _context2.n = 3;
321
- break;
322
- }
323
- _context2.n = 2;
324
- return new Promise(function (resolve) {
325
- return requestAnimationFrame(resolve);
326
- });
327
- case 2:
328
- _context2.n = 4;
329
- break;
330
- case 3:
331
- _context2.n = 4;
332
- return waitForRender(5);
333
- case 4:
334
- console.log("Starting capture...");
335
- _context2.n = 5;
336
- return (0, _reactNativeViewShot.captureRef)(viewRef, {
337
- format: 'png',
338
- quality: 1
339
- });
340
- case 5:
341
- capturedUri = _context2.v;
342
- console.log("Capture successful:", capturedUri);
343
- if (capturedUri) {
344
- _context2.n = 6;
345
- break;
346
- }
347
- throw new Error("Capture returned empty URI");
348
- case 6:
349
- console.log("Enhancing image...");
350
- _context2.n = 7;
351
- return (0, _ImageProcessor.enhanceImage)(capturedUri, addheight);
352
- case 7:
353
- enhancedUri = _context2.v;
354
- console.log("Image enhanced:", enhancedUri);
355
-
356
- // Crop image based on points
357
- croppedUri = enhancedUri;
358
- if (!(points.length > 0)) {
359
- _context2.n = 14;
360
- break;
361
- }
362
- _context2.p = 8;
363
- console.log("Calculating crop boundaries from points...");
364
-
365
- // Get enhanced image dimensions
366
- _context2.n = 9;
367
- return ImageManipulator.manipulateAsync(enhancedUri, []);
368
- case 9:
369
- enhancedImageInfo = _context2.v;
370
- actualImageWidth = enhancedImageInfo.width;
371
- actualImageHeight = enhancedImageInfo.height; // Get displayed image dimensions
372
- displayedWidth = imageMeasure.current.width;
373
- displayedHeight = imageMeasure.current.height; // Calculate scale factors to convert screen coordinates to image coordinates
374
- scaleX = actualImageWidth / displayedWidth;
375
- scaleY = actualImageHeight / displayedHeight; // Convert points from screen coordinates to actual image coordinates
376
- imagePoints = points.map(function (point) {
377
- return {
378
- x: point.x * scaleX,
379
- y: point.y * scaleY
380
- };
381
- }); // Calculate bounding box: min X, min Y, max X, max Y
382
- minX = Math.min.apply(Math, _toConsumableArray(imagePoints.map(function (p) {
383
- return p.x;
384
- })));
385
- minY = Math.min.apply(Math, _toConsumableArray(imagePoints.map(function (p) {
386
- return p.y;
387
- })));
388
- maxX = Math.max.apply(Math, _toConsumableArray(imagePoints.map(function (p) {
389
- return p.x;
390
- })));
391
- maxY = Math.max.apply(Math, _toConsumableArray(imagePoints.map(function (p) {
392
- return p.y;
393
- }))); // Calculate crop dimensions and origin
394
- cropX = Math.max(0, Math.floor(minX));
395
- cropY = Math.max(0, Math.floor(minY));
396
- cropWidth = Math.min(actualImageWidth - cropX, Math.ceil(maxX - minX));
397
- cropHeight = Math.min(actualImageHeight - cropY, Math.ceil(maxY - minY));
398
- console.log("Crop parameters:", {
399
- x: cropX,
400
- y: cropY,
401
- width: cropWidth,
402
- height: cropHeight,
403
- imageWidth: actualImageWidth,
404
- imageHeight: actualImageHeight
405
- });
406
-
407
- // Crop the image
408
- if (!(cropWidth > 0 && cropHeight > 0)) {
409
- _context2.n = 11;
410
- break;
411
- }
412
- _context2.n = 10;
413
- return ImageManipulator.manipulateAsync(enhancedUri, [{
414
- crop: {
415
- originX: cropX,
416
- originY: cropY,
417
- width: cropWidth,
418
- height: cropHeight
419
- }
420
- }], {
421
- compress: 1,
422
- format: ImageManipulator.SaveFormat.PNG,
423
- base64: false
424
- });
425
- case 10:
426
- croppedResult = _context2.v;
427
- croppedUri = croppedResult.uri;
428
- console.log("Image cropped successfully:", croppedUri);
429
- _context2.n = 12;
430
- break;
431
- case 11:
432
- console.warn("Invalid crop dimensions, using enhanced image");
433
- case 12:
434
- _context2.n = 14;
435
- break;
436
- case 13:
437
- _context2.p = 13;
438
- _t2 = _context2.v;
439
- console.error("Error cropping image:", _t2);
440
- // Continue with enhanced image if cropping fails
441
- case 14:
442
- name = "IMAGE XTK".concat(Date.now(), ".png");
443
- if (onConfirm) {
444
- console.log("Calling onConfirm with:", croppedUri, name);
445
- onConfirm(croppedUri, name);
446
- }
447
- _context2.n = 16;
448
- break;
449
- case 15:
450
- _context2.p = 15;
451
- _t3 = _context2.v;
452
- console.error("Erreur lors de la capture :", _t3);
453
- alert("Erreur lors de la capture ! " + _t3.message);
454
- case 16:
455
- _context2.p = 16;
456
- setShowResult(false);
457
- setIsLoading(false);
458
- setShowFullScreenCapture(false);
459
- return _context2.f(16);
460
- case 17:
461
- return _context2.a(2);
462
- }
463
- }, _callee2, null, [[8, 13], [1, 15, 16, 17]]);
464
- }))
465
- }, /*#__PURE__*/_react["default"].createElement(_reactNative.Text, {
466
- style: _ImageCropperStyles["default"].buttonText
467
- }, "Confirm"))), image && /*#__PURE__*/_react["default"].createElement(_reactNative.View, {
468
- ref: viewRef,
469
- collapsable: false,
470
- style: showFullScreenCapture ? _ImageCropperStyles["default"].fullscreenImageContainer : _ImageCropperStyles["default"].imageContainer,
471
- onStartShouldSetResponder: function onStartShouldSetResponder() {
472
- return true;
473
- },
474
- onResponderStart: handleTap,
475
- onResponderMove: handleMove,
476
- onResponderRelease: handleRelease
477
- }, /*#__PURE__*/_react["default"].createElement(_reactNative.Image, {
478
- source: {
479
- uri: image
480
- },
481
- style: _ImageCropperStyles["default"].image,
482
- onLayout: onImageLayout
483
- }), /*#__PURE__*/_react["default"].createElement(_reactNativeSvg["default"], {
484
- style: _ImageCropperStyles["default"].overlay
485
- }, /*#__PURE__*/_react["default"].createElement(_reactNativeSvg.Path, {
486
- d: "M 0 0 H ".concat(imageMeasure.current.width, " V ").concat(imageMeasure.current.height, " H 0 Z ").concat(createPath()),
487
- fill: showResult ? 'white' : 'rgba(0, 0, 0, 0.8)',
488
- fillRule: "evenodd"
489
- }), !showResult && points.length > 0 && /*#__PURE__*/_react["default"].createElement(_reactNativeSvg.Path, {
490
- d: createPath(),
491
- fill: "transparent",
492
- stroke: "white",
493
- strokeWidth: 2
494
- }), !showResult && points.map(function (point, index) {
495
- return /*#__PURE__*/_react["default"].createElement(_reactNativeSvg.Circle, {
496
- key: index,
497
- cx: point.x,
498
- cy: point.y,
499
- r: 10,
500
- fill: "white"
501
- });
502
- })))), /*#__PURE__*/_react["default"].createElement(_reactNative.Modal, {
503
- visible: isLoading,
504
- transparent: true,
505
- animationType: "fade"
506
- }, /*#__PURE__*/_react["default"].createElement(_reactNative.View, {
507
- style: _ImageCropperStyles["default"].loadingOverlay
508
- }, /*#__PURE__*/_react["default"].createElement(_reactNative.Image, {
509
- source: require('../src/assets/loadingCamera.gif'),
510
- style: _ImageCropperStyles["default"].loadingGif,
511
- resizeMode: "contain"
512
- }))));
513
- };
1
+ "use strict";
2
+
3
+ function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); }
4
+ Object.defineProperty(exports, "__esModule", {
5
+ value: true
6
+ });
7
+ exports["default"] = void 0;
8
+ var _ImageCropperStyles = _interopRequireDefault(require("./ImageCropperStyles"));
9
+ var _react = _interopRequireWildcard(require("react"));
10
+ var _reactNative = require("react-native");
11
+ var _reactNativeSvg = _interopRequireWildcard(require("react-native-svg"));
12
+ var _reactNativeViewShot = require("react-native-view-shot");
13
+ var _CustomCamera = _interopRequireDefault(require("./CustomCamera"));
14
+ var ImageManipulator = _interopRequireWildcard(require("expo-image-manipulator"));
15
+ var FileSystem = _interopRequireWildcard(require("expo-file-system"));
16
+ var _vectorIcons = require("@expo/vector-icons");
17
+ var _reactNativeSafeAreaContext = require("react-native-safe-area-context");
18
+ var _ImageMaskProcessor = require("./ImageMaskProcessor");
19
+ 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 _t3 in e) "default" !== _t3 && {}.hasOwnProperty.call(e, _t3) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, _t3)) && (i.get || i.set) ? o(f, _t3, i) : f[_t3] = e[_t3]); return f; })(e, t); }
20
+ function _interopRequireDefault(e) { return e && e.__esModule ? e : { "default": e }; }
21
+ function _regenerator() { /*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/babel/babel/blob/main/packages/babel-helpers/LICENSE */ var e, t, r = "function" == typeof Symbol ? Symbol : {}, n = r.iterator || "@@iterator", o = r.toStringTag || "@@toStringTag"; function i(r, n, o, i) { var c = n && n.prototype instanceof Generator ? n : Generator, u = Object.create(c.prototype); return _regeneratorDefine2(u, "_invoke", function (r, n, o) { var i, c, u, f = 0, p = o || [], y = !1, G = { p: 0, n: 0, v: e, a: d, f: d.bind(e, 4), d: function d(t, r) { return i = t, c = 0, u = e, G.n = r, a; } }; function d(r, n) { for (c = r, u = n, t = 0; !y && f && !o && t < p.length; t++) { var o, i = p[t], d = G.p, l = i[2]; r > 3 ? (o = l === n) && (u = i[(c = i[4]) ? 5 : (c = 3, 3)], i[4] = i[5] = e) : i[0] <= d && ((o = r < 2 && d < i[1]) ? (c = 0, G.v = n, G.n = i[1]) : d < l && (o = r < 3 || i[0] > n || n > l) && (i[4] = r, i[5] = n, G.n = l, c = 0)); } if (o || r > 1) return a; throw y = !0, n; } return function (o, p, l) { if (f > 1) throw TypeError("Generator is already running"); for (y && 1 === p && d(p, l), c = p, u = l; (t = c < 2 ? e : u) || !y;) { i || (c ? c < 3 ? (c > 1 && (G.n = -1), d(c, u)) : G.n = u : G.v = u); try { if (f = 2, i) { if (c || (o = "next"), t = i[o]) { if (!(t = t.call(i, u))) throw TypeError("iterator result is not an object"); if (!t.done) return t; u = t.value, c < 2 && (c = 0); } else 1 === c && (t = i["return"]) && t.call(i), c < 2 && (u = TypeError("The iterator does not provide a '" + o + "' method"), c = 1); i = e; } else if ((t = (y = G.n < 0) ? u : r.call(n, G)) !== a) break; } catch (t) { i = e, c = 1, u = t; } finally { f = 1; } } return { value: t, done: y }; }; }(r, o, i), !0), u; } var a = {}; function Generator() {} function GeneratorFunction() {} function GeneratorFunctionPrototype() {} t = Object.getPrototypeOf; var c = [][n] ? t(t([][n]())) : (_regeneratorDefine2(t = {}, n, function () { return this; }), t), u = GeneratorFunctionPrototype.prototype = Generator.prototype = Object.create(c); function f(e) { return Object.setPrototypeOf ? Object.setPrototypeOf(e, GeneratorFunctionPrototype) : (e.__proto__ = GeneratorFunctionPrototype, _regeneratorDefine2(e, o, "GeneratorFunction")), e.prototype = Object.create(u), e; } return GeneratorFunction.prototype = GeneratorFunctionPrototype, _regeneratorDefine2(u, "constructor", GeneratorFunctionPrototype), _regeneratorDefine2(GeneratorFunctionPrototype, "constructor", GeneratorFunction), GeneratorFunction.displayName = "GeneratorFunction", _regeneratorDefine2(GeneratorFunctionPrototype, o, "GeneratorFunction"), _regeneratorDefine2(u), _regeneratorDefine2(u, o, "Generator"), _regeneratorDefine2(u, n, function () { return this; }), _regeneratorDefine2(u, "toString", function () { return "[object Generator]"; }), (_regenerator = function _regenerator() { return { w: i, m: f }; })(); }
22
+ function _regeneratorDefine2(e, r, n, t) { var i = Object.defineProperty; try { i({}, "", {}); } catch (e) { i = 0; } _regeneratorDefine2 = function _regeneratorDefine(e, r, n, t) { function o(r, n) { _regeneratorDefine2(e, r, function (e) { return this._invoke(r, n, e); }); } r ? i ? i(e, r, { value: n, enumerable: !t, configurable: !t, writable: !t }) : e[r] = n : (o("next", 0), o("throw", 1), o("return", 2)); }, _regeneratorDefine2(e, r, n, t); }
23
+ function asyncGeneratorStep(n, t, e, r, o, a, c) { try { var i = n[a](c), u = i.value; } catch (n) { return void e(n); } i.done ? t(u) : Promise.resolve(u).then(r, o); }
24
+ function _asyncToGenerator(n) { return function () { var t = this, e = arguments; return new Promise(function (r, o) { var a = n.apply(t, e); function _next(n) { asyncGeneratorStep(a, r, o, _next, _throw, "next", n); } function _throw(n) { asyncGeneratorStep(a, r, o, _next, _throw, "throw", n); } _next(void 0); }); }; }
25
+ 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; }
26
+ 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; }
27
+ function _defineProperty(e, r, t) { return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: !0, configurable: !0, writable: !0 }) : e[r] = t, e; }
28
+ function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == _typeof(i) ? i : i + ""; }
29
+ function _toPrimitive(t, r) { if ("object" != _typeof(t) || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != _typeof(i)) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
30
+ function _toConsumableArray(r) { return _arrayWithoutHoles(r) || _iterableToArray(r) || _unsupportedIterableToArray(r) || _nonIterableSpread(); }
31
+ function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }
32
+ function _iterableToArray(r) { if ("undefined" != typeof Symbol && null != r[Symbol.iterator] || null != r["@@iterator"]) return Array.from(r); }
33
+ function _arrayWithoutHoles(r) { if (Array.isArray(r)) return _arrayLikeToArray(r); }
34
+ function _slicedToArray(r, e) { return _arrayWithHoles(r) || _iterableToArrayLimit(r, e) || _unsupportedIterableToArray(r, e) || _nonIterableRest(); }
35
+ function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }
36
+ function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } }
37
+ 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; }
38
+ 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; } }
39
+ function _arrayWithHoles(r) { if (Array.isArray(r)) return r; }
40
+ var ImageCropper = function ImageCropper(_ref) {
41
+ var onConfirm = _ref.onConfirm,
42
+ openCameraFirst = _ref.openCameraFirst,
43
+ initialImage = _ref.initialImage,
44
+ addheight = _ref.addheight;
45
+ var _useState = (0, _react.useState)(null),
46
+ _useState2 = _slicedToArray(_useState, 2),
47
+ image = _useState2[0],
48
+ setImage = _useState2[1];
49
+ var _useState3 = (0, _react.useState)([]),
50
+ _useState4 = _slicedToArray(_useState3, 2),
51
+ points = _useState4[0],
52
+ setPoints = _useState4[1];
53
+ var _useState5 = (0, _react.useState)(false),
54
+ _useState6 = _slicedToArray(_useState5, 2),
55
+ showResult = _useState6[0],
56
+ setShowResult = _useState6[1];
57
+ var _useState7 = (0, _react.useState)(false),
58
+ _useState8 = _slicedToArray(_useState7, 2),
59
+ showCustomCamera = _useState8[0],
60
+ setShowCustomCamera = _useState8[1];
61
+ var viewRef = (0, _react.useRef)(null);
62
+ var maskViewRef = (0, _react.useRef)(null); // Ref pour la vue de masque (invisible)
63
+ var sourceImageUri = (0, _react.useRef)(null); // keep original image URI (full-res) for upload
64
+ var cameraFrameData = (0, _react.useRef)(null); // ✅ Store green frame coordinates from camera
65
+
66
+ // ✅ REFACTORISATION : Séparation claire entre dimensions originales et affichage
67
+ // Dimensions réelles de l'image originale (pixels)
68
+ var originalImageDimensions = (0, _react.useRef)({
69
+ width: 0,
70
+ height: 0
71
+ });
72
+ // Dimensions et position d'affichage à l'écran (pour le calcul des points de crop)
73
+ var displayedImageLayout = (0, _react.useRef)({
74
+ x: 0,
75
+ y: 0,
76
+ width: 0,
77
+ height: 0
78
+ });
79
+ // Conserver imageMeasure pour compatibilité avec le code existant (utilisé pour SVG overlay)
80
+ var imageMeasure = (0, _react.useRef)({
81
+ x: 0,
82
+ y: 0,
83
+ width: 0,
84
+ height: 0
85
+ });
86
+ // Rectangle réel de l'image affichée (quand resizeMode='contain') à l'intérieur du conteneur
87
+ // Sert à rendre la conversion coordonnées écran -> pixels image réellement pixel-perfect.
88
+ var displayedContentRect = (0, _react.useRef)({
89
+ x: 0,
90
+ y: 0,
91
+ width: 0,
92
+ height: 0
93
+ });
94
+ var updateDisplayedContentRect = function updateDisplayedContentRect(layoutWidth, layoutHeight) {
95
+ var iw = originalImageDimensions.current.width;
96
+ var ih = originalImageDimensions.current.height;
97
+ console.log("🔄 updateDisplayedContentRect called:", {
98
+ originalDimensions: {
99
+ width: iw,
100
+ height: ih
101
+ },
102
+ layoutDimensions: {
103
+ width: layoutWidth,
104
+ height: layoutHeight
105
+ }
106
+ });
107
+ if (iw > 0 && ih > 0 && layoutWidth > 0 && layoutHeight > 0) {
108
+ var scale = Math.min(layoutWidth / iw, layoutHeight / ih);
109
+ var contentW = iw * scale;
110
+ var contentH = ih * scale;
111
+ var offsetX = (layoutWidth - contentW) / 2;
112
+ var offsetY = (layoutHeight - contentH) / 2;
113
+ displayedContentRect.current = {
114
+ x: offsetX,
115
+ y: offsetY,
116
+ width: contentW,
117
+ height: contentH
118
+ };
119
+ console.log("✅ Displayed content rect (contain) calculated:", displayedContentRect.current);
120
+ return;
121
+ }
122
+
123
+ // ✅ FALLBACK: If original dimensions not available yet, use layout as temporary measure
124
+ if (layoutWidth > 0 && layoutHeight > 0) {
125
+ displayedContentRect.current = {
126
+ x: 0,
127
+ y: 0,
128
+ width: layoutWidth,
129
+ height: layoutHeight
130
+ };
131
+ console.log("⚠️ Using layout dimensions as fallback (original dimensions not available yet):", displayedContentRect.current);
132
+ } else {
133
+ displayedContentRect.current = {
134
+ x: 0,
135
+ y: 0,
136
+ width: 0,
137
+ height: 0
138
+ };
139
+ console.warn("❌ Cannot calculate displayedContentRect: missing dimensions");
140
+ }
141
+ };
142
+ var selectedPointIndex = (0, _react.useRef)(null);
143
+ var lastTap = (0, _react.useRef)(null);
144
+
145
+ // FREE DRAG: Store initial touch position and point position for delta-based movement
146
+ var initialTouchPosition = (0, _react.useRef)(null); // { x, y } - initial touch position when drag starts
147
+ var initialPointPosition = (0, _react.useRef)(null); // { x, y } - initial point position when drag starts
148
+ var lastTouchPosition = (0, _react.useRef)(null); // { x, y } - last touch position for incremental delta calculation
149
+
150
+ // Angle de rotation accumulé (pour éviter les rotations multiples)
151
+ var rotationAngle = (0, _react.useRef)(0);
152
+
153
+ // États pour la vue de masque temporaire
154
+ var _useState9 = (0, _react.useState)(null),
155
+ _useState0 = _slicedToArray(_useState9, 2),
156
+ maskImageUri = _useState0[0],
157
+ setMaskImageUri = _useState0[1];
158
+ var _useState1 = (0, _react.useState)([]),
159
+ _useState10 = _slicedToArray(_useState1, 2),
160
+ maskPoints = _useState10[0],
161
+ setMaskPoints = _useState10[1];
162
+ var _useState11 = (0, _react.useState)({
163
+ width: 0,
164
+ height: 0
165
+ }),
166
+ _useState12 = _slicedToArray(_useState11, 2),
167
+ maskDimensions = _useState12[0],
168
+ setMaskDimensions = _useState12[1];
169
+ var _useState13 = (0, _react.useState)(false),
170
+ _useState14 = _slicedToArray(_useState13, 2),
171
+ showMaskView = _useState14[0],
172
+ setShowMaskView = _useState14[1];
173
+ var _useState15 = (0, _react.useState)(false),
174
+ _useState16 = _slicedToArray(_useState15, 2),
175
+ isLoading = _useState16[0],
176
+ setIsLoading = _useState16[1];
177
+ var _useState17 = (0, _react.useState)(false),
178
+ _useState18 = _slicedToArray(_useState17, 2),
179
+ showFullScreenCapture = _useState18[0],
180
+ setShowFullScreenCapture = _useState18[1];
181
+ var _useState19 = (0, _react.useState)(false),
182
+ _useState20 = _slicedToArray(_useState19, 2),
183
+ isRotating = _useState20[0],
184
+ setIsRotating = _useState20[1];
185
+ var lastValidPosition = (0, _react.useRef)(null);
186
+ var insets = (0, _reactNativeSafeAreaContext.useSafeAreaInsets)();
187
+
188
+ // NEW ARCH: mobile does NOT export the final crop.
189
+ // We only compute crop metadata (bbox + polygon) and upload the ORIGINAL image to backend Python.
190
+ // No view-shot / captureRef / bitmap masking on device.
191
+ var enableMask = false;
192
+ var enableRotation = false; // rotation would require careful coord transforms; keep off for pixel-perfect pipeline.
193
+
194
+ (0, _react.useEffect)(function () {
195
+ if (openCameraFirst) {
196
+ setShowCustomCamera(true);
197
+ } else if (initialImage) {
198
+ setImage(initialImage);
199
+ sourceImageUri.current = initialImage;
200
+ // CRITICAL: Reset points when loading a new image from gallery
201
+ // This ensures the crop box will be automatically initialized
202
+ setPoints([]);
203
+ rotationAngle.current = 0;
204
+ // Clear camera frame data for gallery images
205
+ cameraFrameData.current = null;
206
+ }
207
+ }, [openCameraFirst, initialImage]);
208
+
209
+ // ✅ REFACTORISATION : Stocker uniquement les dimensions originales (pas de calcul théorique)
210
+ // ✅ CRITICAL FIX: Get dimensions and account for potential scale factor differences
211
+ // On Android, Image.getSize() returns physical pixels, but the image sent to backend
212
+ // might be at a different scale. We'll send original dimensions to backend for adjustment.
213
+ (0, _react.useEffect)(function () {
214
+ if (!image) {
215
+ originalImageDimensions.current = {
216
+ width: 0,
217
+ height: 0
218
+ };
219
+ return;
220
+ }
221
+ if (!sourceImageUri.current) {
222
+ sourceImageUri.current = image;
223
+ }
224
+
225
+ // CRITICAL FIX: If we have capturedImageSize from camera, use it as source of truth
226
+ // takePictureAsync returns physical dimensions, while Image.getSize() may return EXIF-oriented dimensions
227
+ if (cameraFrameData.current && cameraFrameData.current.capturedImageSize) {
228
+ var _cameraFrameData$curr = cameraFrameData.current.capturedImageSize,
229
+ capturedWidth = _cameraFrameData$curr.width,
230
+ capturedHeight = _cameraFrameData$curr.height;
231
+ originalImageDimensions.current = {
232
+ width: capturedWidth,
233
+ height: capturedHeight
234
+ };
235
+ console.log("✅ Using captured image dimensions from takePictureAsync:", {
236
+ width: capturedWidth,
237
+ height: capturedHeight,
238
+ source: 'takePictureAsync'
239
+ });
240
+ // ✅ CRITICAL: Use displayedImageLayout dimensions if available, otherwise wait for onImageLayout
241
+ var lw = displayedImageLayout.current.width;
242
+ var lh = displayedImageLayout.current.height;
243
+ if (lw > 0 && lh > 0) {
244
+ updateDisplayedContentRect(lw, lh);
245
+ // ✅ CRITICAL: Initialize crop box when we have camera frame data and image dimensions
246
+ if (points.length === 0) {
247
+ initializeCropBox();
248
+ }
249
+ }
250
+ return;
251
+ }
252
+
253
+ // ✅ FALLBACK: Use Image.getSize() if no captured dimensions available (e.g., from gallery)
254
+ _reactNative.Image.getSize(image, function (imgWidth, imgHeight) {
255
+ originalImageDimensions.current = {
256
+ width: imgWidth,
257
+ height: imgHeight
258
+ };
259
+ console.log("✅ Image dimensions from Image.getSize():", {
260
+ width: imgWidth,
261
+ height: imgHeight,
262
+ platform: _reactNative.Platform.OS,
263
+ pixelRatio: _reactNative.PixelRatio.get(),
264
+ uri: image,
265
+ source: 'Image.getSize()'
266
+ });
267
+
268
+ // IMPORTANT: onImageLayout peut se déclencher avant Image.getSize (race condition).
269
+ // Recalculer le contentRect dès qu'on connaît la taille originale, sinon les coords seront décalées.
270
+ var lw = displayedImageLayout.current.width;
271
+ var lh = displayedImageLayout.current.height;
272
+ if (lw > 0 && lh > 0) {
273
+ updateDisplayedContentRect(lw, lh);
274
+ // CRITICAL: Initialize crop box when we have image dimensions
275
+ // - If we have camera frame data, use it to match green frame exactly
276
+ // - If no camera frame data (gallery image), initialize with 70% default box
277
+ if (points.length === 0) {
278
+ initializeCropBox();
279
+ }
280
+ }
281
+ }, function (error) {
282
+ console.error("Error getting image size:", error);
283
+ });
284
+ }, [image]);
285
+
286
+ // ✅ CRITICAL FIX: Convert green frame coordinates (camera preview) to captured image coordinates
287
+ var convertGreenFrameToImageCoords = function convertGreenFrameToImageCoords(greenFrame, capturedImageSize, displayedImageRect) {
288
+ if (!greenFrame || !capturedImageSize) {
289
+ console.warn("Cannot convert green frame: missing data");
290
+ return null;
291
+ }
292
+ var frameX = greenFrame.x,
293
+ frameY = greenFrame.y,
294
+ frameWidth = greenFrame.width,
295
+ frameHeight = greenFrame.height,
296
+ wrapperWidth = greenFrame.wrapperWidth,
297
+ wrapperHeight = greenFrame.wrapperHeight;
298
+ var imgWidth = capturedImageSize.width,
299
+ imgHeight = capturedImageSize.height;
300
+ var displayX = displayedImageRect.x,
301
+ displayY = displayedImageRect.y,
302
+ displayWidth = displayedImageRect.width,
303
+ displayHeight = displayedImageRect.height;
304
+ console.log("🔄 Converting green frame:", {
305
+ greenFrame: {
306
+ frameX: frameX,
307
+ frameY: frameY,
308
+ frameWidth: frameWidth,
309
+ frameHeight: frameHeight,
310
+ wrapperWidth: wrapperWidth,
311
+ wrapperHeight: wrapperHeight
312
+ },
313
+ capturedImageSize: {
314
+ imgWidth: imgWidth,
315
+ imgHeight: imgHeight
316
+ },
317
+ displayedImageRect: {
318
+ displayX: displayX,
319
+ displayY: displayY,
320
+ displayWidth: displayWidth,
321
+ displayHeight: displayHeight
322
+ }
323
+ });
324
+
325
+ // ✅ SIMPLIFIED APPROACH: Assume CameraView fills the wrapper completely (no letterboxing in preview)
326
+ // The green frame is drawn as a percentage of the wrapper (95% width, 80% height)
327
+ // We need to map this directly to the captured image, accounting for aspect ratio differences
328
+
329
+ var previewAspect = wrapperWidth / wrapperHeight;
330
+ var capturedAspect = imgWidth / imgHeight;
331
+ console.log("📐 Aspect ratios:", {
332
+ previewAspect: previewAspect.toFixed(3),
333
+ capturedAspect: capturedAspect.toFixed(3),
334
+ wrapperSize: {
335
+ wrapperWidth: wrapperWidth,
336
+ wrapperHeight: wrapperHeight
337
+ },
338
+ capturedSize: {
339
+ imgWidth: imgWidth,
340
+ imgHeight: imgHeight
341
+ }
342
+ });
343
+
344
+ // KEY INSIGHT: The green frame is 95% of wrapper width and 80% of wrapper height
345
+ // If the captured image has a different aspect ratio, we need to map proportionally
346
+ // The green frame represents a region in the preview, which should map to the same region in the image
347
+
348
+ // Calculate green frame as percentage of wrapper
349
+ var greenFramePercentX = frameX / wrapperWidth;
350
+ var greenFramePercentY = frameY / wrapperHeight;
351
+ var greenFramePercentWidth = frameWidth / wrapperWidth;
352
+ var greenFramePercentHeight = frameHeight / wrapperHeight;
353
+ console.log("📊 Green frame as percentage of wrapper:", {
354
+ x: (greenFramePercentX * 100).toFixed(2) + '%',
355
+ y: (greenFramePercentY * 100).toFixed(2) + '%',
356
+ width: (greenFramePercentWidth * 100).toFixed(2) + '%',
357
+ height: (greenFramePercentHeight * 100).toFixed(2) + '%'
358
+ });
359
+
360
+ // ✅ DIRECT MAPPING: Map green frame percentage directly to captured image
361
+ // The green frame covers a certain percentage of the preview, which should map to the same percentage of the image
362
+ // However, we need to account for aspect ratio differences
363
+
364
+ // If preview and captured have same aspect ratio, direct mapping works
365
+ // If different, we need to account for letterboxing in the preview
366
+
367
+ // Calculate how the captured image would be displayed in the preview (aspect-fit)
368
+ var previewContentWidth, previewContentHeight, previewOffsetX, previewOffsetY;
369
+ if (Math.abs(capturedAspect - previewAspect) < 0.01) {
370
+ // Same aspect ratio → no letterboxing, direct mapping
371
+ previewContentWidth = wrapperWidth;
372
+ previewContentHeight = wrapperHeight;
373
+ previewOffsetX = 0;
374
+ previewOffsetY = 0;
375
+ } else if (capturedAspect > previewAspect) {
376
+ // Image is wider → fills width, letterboxing on top/bottom
377
+ previewContentWidth = wrapperWidth;
378
+ previewContentHeight = wrapperWidth / capturedAspect;
379
+ previewOffsetX = 0;
380
+ previewOffsetY = (wrapperHeight - previewContentHeight) / 2;
381
+ } else {
382
+ // Image is taller → fills height, letterboxing on left/right
383
+ previewContentHeight = wrapperHeight;
384
+ previewContentWidth = wrapperHeight * capturedAspect;
385
+ previewOffsetX = (wrapperWidth - previewContentWidth) / 2;
386
+ previewOffsetY = 0;
387
+ }
388
+ console.log("📐 Preview content area (actual image area in preview):", {
389
+ previewContentWidth: previewContentWidth.toFixed(2),
390
+ previewContentHeight: previewContentHeight.toFixed(2),
391
+ previewOffsetX: previewOffsetX.toFixed(2),
392
+ previewOffsetY: previewOffsetY.toFixed(2)
393
+ });
394
+
395
+ // Step 3: Convert green frame coordinates from wrapper space to preview content space
396
+ // CRITICAL FIX: The green frame is drawn on the wrapper, but we need to map it to the actual image area
397
+ // If the green frame overlaps letterboxing areas, we need to clip it to the actual image content area
398
+
399
+ // Calculate green frame bounds in wrapper coordinates
400
+ var frameLeft = frameX;
401
+ var frameTop = frameY;
402
+ var frameRight = frameX + frameWidth;
403
+ var frameBottom = frameY + frameHeight;
404
+
405
+ // Calculate preview content bounds in wrapper coordinates
406
+ var contentLeft = previewOffsetX;
407
+ var contentTop = previewOffsetY;
408
+ var contentRight = previewOffsetX + previewContentWidth;
409
+ var contentBottom = previewOffsetY + previewContentHeight;
410
+
411
+ // ✅ KEY INSIGHT: The green frame should map to the same percentage of the image content area
412
+ // But we need to account for letterboxing - the green frame might extend into letterboxing areas
413
+
414
+ // ✅ Clip green frame to preview content area (intersection)
415
+ var clippedLeft = Math.max(frameLeft, contentLeft);
416
+ var clippedTop = Math.max(frameTop, contentTop);
417
+ var clippedRight = Math.min(frameRight, contentRight);
418
+ var clippedBottom = Math.min(frameBottom, contentBottom);
419
+
420
+ // Calculate clipped green frame dimensions
421
+ var clippedWidth = Math.max(0, clippedRight - clippedLeft);
422
+ var clippedHeight = Math.max(0, clippedBottom - clippedTop);
423
+
424
+ // If green frame is completely outside content area, return null
425
+ if (clippedWidth <= 0 || clippedHeight <= 0) {
426
+ console.error("❌ Green frame is completely outside preview content area!");
427
+ return null;
428
+ }
429
+
430
+ // ✅ ALTERNATIVE APPROACH: Map green frame as percentage of image content area
431
+ // The green frame covers a certain percentage of the wrapper, but we want it to cover
432
+ // the same visual percentage of the image content area
433
+
434
+ // Calculate green frame center and size as percentage of wrapper
435
+ var greenFrameCenterX = (frameLeft + frameRight) / 2;
436
+ var greenFrameCenterY = (frameTop + frameBottom) / 2;
437
+ var greenFrameCenterPercentX = greenFrameCenterX / wrapperWidth;
438
+ var greenFrameCenterPercentY = greenFrameCenterY / wrapperHeight;
439
+
440
+ // Map center to image content area
441
+ var imageContentCenterX = previewOffsetX + previewContentWidth * greenFrameCenterPercentX;
442
+ var imageContentCenterY = previewOffsetY + previewContentHeight * greenFrameCenterPercentY;
443
+
444
+ // Calculate green frame size as percentage of image content area (not wrapper)
445
+ // The green frame should cover the same visual percentage of the image as it does of the wrapper
446
+ var imageContentFrameWidth = previewContentWidth * greenFramePercentWidth;
447
+ var imageContentFrameHeight = previewContentHeight * greenFramePercentHeight;
448
+
449
+ // Calculate final green frame in image content coordinates
450
+ var finalFrameX = imageContentCenterX - imageContentFrameWidth / 2;
451
+ var finalFrameY = imageContentCenterY - imageContentFrameHeight / 2;
452
+ var finalFrameWidth = imageContentFrameWidth;
453
+ var finalFrameHeight = imageContentFrameHeight;
454
+
455
+ // Clamp to image content bounds
456
+ var clampedFinalX = Math.max(previewOffsetX, Math.min(finalFrameX, previewOffsetX + previewContentWidth - finalFrameWidth));
457
+ var clampedFinalY = Math.max(previewOffsetY, Math.min(finalFrameY, previewOffsetY + previewContentHeight - finalFrameHeight));
458
+ var clampedFinalWidth = Math.min(finalFrameWidth, previewOffsetX + previewContentWidth - clampedFinalX);
459
+ var clampedFinalHeight = Math.min(finalFrameHeight, previewOffsetY + previewContentHeight - clampedFinalY);
460
+
461
+ // Convert to relative coordinates within preview content area
462
+ var relativeX = clampedFinalX - previewOffsetX;
463
+ var relativeY = clampedFinalY - previewOffsetY;
464
+
465
+ // Normalize to 0-1 range within the preview content area (actual image area)
466
+ var normalizedX = relativeX / previewContentWidth;
467
+ var normalizedY = relativeY / previewContentHeight;
468
+ var normalizedWidth = clampedFinalWidth / previewContentWidth;
469
+ var normalizedHeight = clampedFinalHeight / previewContentHeight;
470
+ console.log("✂️ Green frame mapping (percentage-based):", {
471
+ originalFrame: {
472
+ frameX: frameX,
473
+ frameY: frameY,
474
+ frameWidth: frameWidth,
475
+ frameHeight: frameHeight
476
+ },
477
+ greenFramePercentages: {
478
+ centerX: (greenFrameCenterPercentX * 100).toFixed(2) + '%',
479
+ centerY: (greenFrameCenterPercentY * 100).toFixed(2) + '%',
480
+ width: (greenFramePercentWidth * 100).toFixed(2) + '%',
481
+ height: (greenFramePercentHeight * 100).toFixed(2) + '%'
482
+ },
483
+ previewContent: {
484
+ previewOffsetX: previewOffsetX,
485
+ previewOffsetY: previewOffsetY,
486
+ previewContentWidth: previewContentWidth,
487
+ previewContentHeight: previewContentHeight
488
+ },
489
+ mappedFrame: {
490
+ finalX: finalFrameX.toFixed(2),
491
+ finalY: finalFrameY.toFixed(2),
492
+ finalWidth: finalFrameWidth.toFixed(2),
493
+ finalHeight: finalFrameHeight.toFixed(2)
494
+ },
495
+ clampedFrame: {
496
+ x: clampedFinalX.toFixed(2),
497
+ y: clampedFinalY.toFixed(2),
498
+ width: clampedFinalWidth.toFixed(2),
499
+ height: clampedFinalHeight.toFixed(2)
500
+ },
501
+ normalized: {
502
+ normalizedX: normalizedX.toFixed(4),
503
+ normalizedY: normalizedY.toFixed(4),
504
+ normalizedWidth: normalizedWidth.toFixed(4),
505
+ normalizedHeight: normalizedHeight.toFixed(4)
506
+ }
507
+ });
508
+ console.log("📊 Normalized coordinates:", {
509
+ relativeX: relativeX,
510
+ relativeY: relativeY,
511
+ normalizedX: normalizedX,
512
+ normalizedY: normalizedY,
513
+ normalizedWidth: normalizedWidth,
514
+ normalizedHeight: normalizedHeight
515
+ });
516
+
517
+ // Step 4: Convert normalized coordinates to captured image pixel coordinates
518
+ var imageX = normalizedX * imgWidth;
519
+ var imageY = normalizedY * imgHeight;
520
+ var imageWidth = normalizedWidth * imgWidth;
521
+ var imageHeight = normalizedHeight * imgHeight;
522
+ console.log("🖼️ Image pixel coordinates:", {
523
+ imageX: imageX,
524
+ imageY: imageY,
525
+ imageWidth: imageWidth,
526
+ imageHeight: imageHeight,
527
+ imgWidth: imgWidth,
528
+ imgHeight: imgHeight
529
+ });
530
+
531
+ // Step 5: Convert to displayed image coordinates (for white bounding box)
532
+ // The captured image is displayed with resizeMode='contain' in ImageCropper
533
+ var displayAspect = displayWidth / displayHeight;
534
+ var displayContentWidth, displayContentHeight, displayOffsetX, displayOffsetY;
535
+ if (capturedAspect > displayAspect) {
536
+ // Image is wider than display → letterboxing on top/bottom
537
+ displayContentWidth = displayWidth;
538
+ displayContentHeight = displayWidth / capturedAspect;
539
+ displayOffsetX = displayX;
540
+ displayOffsetY = displayY + (displayHeight - displayContentHeight) / 2;
541
+ } else {
542
+ // Image is taller than display → letterboxing on left/right
543
+ displayContentHeight = displayHeight;
544
+ displayContentWidth = displayHeight * capturedAspect;
545
+ displayOffsetX = displayX + (displayWidth - displayContentWidth) / 2;
546
+ displayOffsetY = displayY;
547
+ }
548
+
549
+ // Convert image pixel coordinates to display coordinates
550
+ var scaleX = displayContentWidth / imgWidth;
551
+ var scaleY = displayContentHeight / imgHeight;
552
+ var displayBoxX = displayOffsetX + imageX * scaleX;
553
+ var displayBoxY = displayOffsetY + imageY * scaleY;
554
+ var displayBoxWidth = imageWidth * scaleX;
555
+ var displayBoxHeight = imageHeight * scaleY;
556
+
557
+ // Clamp to displayed image bounds
558
+ var clampedX = Math.max(displayOffsetX, Math.min(displayBoxX, displayOffsetX + displayContentWidth - displayBoxWidth));
559
+ var clampedY = Math.max(displayOffsetY, Math.min(displayBoxY, displayOffsetY + displayContentHeight - displayBoxHeight));
560
+ var clampedWidth = Math.min(displayBoxWidth, displayOffsetX + displayContentWidth - clampedX);
561
+ var clampedHeight = Math.min(displayBoxHeight, displayOffsetY + displayContentHeight - clampedY);
562
+ var result = {
563
+ // Display coordinates (for white bounding box)
564
+ displayCoords: {
565
+ x: clampedX,
566
+ y: clampedY,
567
+ width: clampedWidth,
568
+ height: clampedHeight
569
+ },
570
+ // Image pixel coordinates (for backend crop)
571
+ imageCoords: {
572
+ x: Math.max(0, Math.min(imageX, imgWidth - imageWidth)),
573
+ y: Math.max(0, Math.min(imageY, imgHeight - imageHeight)),
574
+ width: Math.max(0, Math.min(imageWidth, imgWidth)),
575
+ height: Math.max(0, Math.min(imageHeight, imgHeight))
576
+ },
577
+ debug: {
578
+ previewAspect: previewAspect,
579
+ capturedAspect: capturedAspect,
580
+ displayAspect: displayAspect,
581
+ previewContentWidth: previewContentWidth,
582
+ previewContentHeight: previewContentHeight,
583
+ previewOffsetX: previewOffsetX,
584
+ previewOffsetY: previewOffsetY,
585
+ displayContentWidth: displayContentWidth,
586
+ displayContentHeight: displayContentHeight,
587
+ displayOffsetX: displayOffsetX,
588
+ displayOffsetY: displayOffsetY,
589
+ scaleX: scaleX,
590
+ scaleY: scaleY
591
+ }
592
+ };
593
+ console.log("✅ Green frame converted to image coordinates:", JSON.stringify(result, null, 2));
594
+
595
+ // ✅ VALIDATION: Ensure the white bounding box matches the green frame visually
596
+ // Calculate what percentage of the image the green frame covers
597
+ var greenFramePercentOfImage = {
598
+ width: imageWidth / imgWidth * 100,
599
+ height: imageHeight / imgHeight * 100
600
+ };
601
+ var greenFramePercentOfPreview = {
602
+ width: frameWidth / wrapperWidth * 100,
603
+ height: frameHeight / wrapperHeight * 100
604
+ };
605
+ console.log("📏 Green frame coverage:", {
606
+ percentOfImage: greenFramePercentOfImage,
607
+ percentOfPreview: greenFramePercentOfPreview,
608
+ shouldMatch: "Green frame should cover same % of image as it does of preview"
609
+ });
610
+ return result;
611
+ };
612
+
613
+ // ✅ REFACTORISATION : Initialiser le crop box avec les dimensions d'affichage réelles
614
+ // ✅ CRITICAL FIX: If camera frame data exists, use it to match green frame exactly
615
+ var initializeCropBox = function initializeCropBox() {
616
+ // ✅ CRITICAL: Ensure displayedContentRect is available
617
+ var contentRect = displayedContentRect.current;
618
+ var layout = displayedImageLayout.current;
619
+
620
+ // Recalculate if not available
621
+ if (contentRect.width === 0 || contentRect.height === 0) {
622
+ if (layout.width > 0 && layout.height > 0) {
623
+ updateDisplayedContentRect(layout.width, layout.height);
624
+ contentRect = displayedContentRect.current;
625
+ }
626
+ // If still not available, use layout as fallback
627
+ if (contentRect.width === 0 || contentRect.height === 0) {
628
+ if (layout.width > 0 && layout.height > 0) {
629
+ contentRect = {
630
+ x: layout.x,
631
+ y: layout.y,
632
+ width: layout.width,
633
+ height: layout.height
634
+ };
635
+ } else {
636
+ console.warn("Cannot initialize crop box: displayed dimensions are zero");
637
+ return;
638
+ }
639
+ }
640
+ }
641
+ var _contentRect = contentRect,
642
+ x = _contentRect.x,
643
+ y = _contentRect.y,
644
+ width = _contentRect.width,
645
+ height = _contentRect.height;
646
+
647
+ // ✅ PRIORITY: If we have green frame data from camera, use it to match exactly
648
+ if (cameraFrameData.current && cameraFrameData.current.greenFrame && originalImageDimensions.current.width > 0) {
649
+ var converted = convertGreenFrameToImageCoords(cameraFrameData.current.greenFrame, cameraFrameData.current.capturedImageSize || originalImageDimensions.current, contentRect);
650
+ if (converted && converted.displayCoords) {
651
+ var _converted$displayCoo = converted.displayCoords,
652
+ _boxX = _converted$displayCoo.x,
653
+ _boxY = _converted$displayCoo.y,
654
+ _boxWidth = _converted$displayCoo.width,
655
+ _boxHeight = _converted$displayCoo.height;
656
+ var _contentRect2 = contentRect,
657
+ contentX = _contentRect2.x,
658
+ contentY = _contentRect2.y,
659
+ contentWidth = _contentRect2.width,
660
+ contentHeight = _contentRect2.height;
661
+
662
+ // ✅ CRITICAL: Clamp points to displayed image bounds (double-check)
663
+ // Ensure the bounding box stays within contentRect, but preserve aspect ratio and percentage
664
+ // First, clamp position
665
+ var clampedBoxX = Math.max(contentX, Math.min(_boxX, contentX + contentWidth - _boxWidth));
666
+ var clampedBoxY = Math.max(contentY, Math.min(_boxY, contentY + contentHeight - _boxHeight));
667
+
668
+ // ✅ CRITICAL: Preserve the width and height from conversion (they should already be correct)
669
+ // Only adjust if they would exceed bounds, but try to maintain the 80% coverage
670
+ var clampedBoxWidth = _boxWidth;
671
+ var clampedBoxHeight = _boxHeight;
672
+
673
+ // Ensure the box fits within contentRect
674
+ if (clampedBoxX + clampedBoxWidth > contentX + contentWidth) {
675
+ clampedBoxWidth = contentX + contentWidth - clampedBoxX;
676
+ }
677
+ if (clampedBoxY + clampedBoxHeight > contentY + contentHeight) {
678
+ clampedBoxHeight = contentY + contentHeight - clampedBoxY;
679
+ }
680
+
681
+ // ✅ CRITICAL: If clamping reduced dimensions, adjust position to center the box
682
+ // This ensures the white bounding box maintains the same visual percentage as the green frame
683
+ if (clampedBoxWidth < _boxWidth || clampedBoxHeight < _boxHeight) {
684
+ // Re-center if possible
685
+ var idealX = contentX + (contentWidth - clampedBoxWidth) / 2;
686
+ var idealY = contentY + (contentHeight - clampedBoxHeight) / 2;
687
+ clampedBoxX = Math.max(contentX, Math.min(idealX, contentX + contentWidth - clampedBoxWidth));
688
+ clampedBoxY = Math.max(contentY, Math.min(idealY, contentY + contentHeight - clampedBoxHeight));
689
+ }
690
+
691
+ // ✅ CRITICAL: Ensure points are within contentRect bounds but not exactly at the edges
692
+ // This allows free movement in all directions
693
+ var minX = contentX;
694
+ var maxX = contentX + contentWidth;
695
+ var minY = contentY;
696
+ var maxY = contentY + contentHeight;
697
+
698
+ // Create points from the clamped green frame coordinates
699
+ // Clamp each point individually to ensure they're within bounds
700
+ var _newPoints = [{
701
+ x: Math.max(minX, Math.min(clampedBoxX, maxX)),
702
+ y: Math.max(minY, Math.min(clampedBoxY, maxY))
703
+ }, {
704
+ x: Math.max(minX, Math.min(clampedBoxX + clampedBoxWidth, maxX)),
705
+ y: Math.max(minY, Math.min(clampedBoxY, maxY))
706
+ }, {
707
+ x: Math.max(minX, Math.min(clampedBoxX + clampedBoxWidth, maxX)),
708
+ y: Math.max(minY, Math.min(clampedBoxY + clampedBoxHeight, maxY))
709
+ }, {
710
+ x: Math.max(minX, Math.min(clampedBoxX, maxX)),
711
+ y: Math.max(minY, Math.min(clampedBoxY + clampedBoxHeight, maxY))
712
+ }];
713
+
714
+ // ✅ VALIDATION: Verify the white bounding box matches green frame percentage (80%)
715
+ var whiteBoxPercentOfDisplay = {
716
+ width: clampedBoxWidth / contentRect.width * 100,
717
+ height: clampedBoxHeight / contentRect.height * 100
718
+ };
719
+ var greenFramePercentOfWrapper = {
720
+ width: cameraFrameData.current.greenFrame.width / cameraFrameData.current.greenFrame.wrapperWidth * 100,
721
+ height: cameraFrameData.current.greenFrame.height / cameraFrameData.current.greenFrame.wrapperHeight * 100
722
+ };
723
+
724
+ // ✅ DEBUG: Log if percentages don't match (should both be ~80%)
725
+ if (Math.abs(whiteBoxPercentOfDisplay.width - greenFramePercentOfWrapper.width) > 5 || Math.abs(whiteBoxPercentOfDisplay.height - greenFramePercentOfWrapper.height) > 5) {
726
+ console.warn("⚠️ White box percentage doesn't match green frame:", {
727
+ whiteBox: whiteBoxPercentOfDisplay,
728
+ greenFrame: greenFramePercentOfWrapper,
729
+ difference: {
730
+ width: Math.abs(whiteBoxPercentOfDisplay.width - greenFramePercentOfWrapper.width),
731
+ height: Math.abs(whiteBoxPercentOfDisplay.height - greenFramePercentOfWrapper.height)
732
+ }
733
+ });
734
+ }
735
+ console.log("✅ Initializing crop box from green frame (clamped):", {
736
+ greenFrame: cameraFrameData.current.greenFrame,
737
+ converted: converted,
738
+ clamped: {
739
+ x: clampedBoxX,
740
+ y: clampedBoxY,
741
+ width: clampedBoxWidth,
742
+ height: clampedBoxHeight
743
+ },
744
+ contentRect: contentRect,
745
+ points: _newPoints,
746
+ validation: {
747
+ whiteBoxPercentOfDisplay: whiteBoxPercentOfDisplay,
748
+ greenFramePercentOfWrapper: greenFramePercentOfWrapper,
749
+ shouldMatch: "White box % should match green frame % (80% - accounting for aspect ratio)"
750
+ }
751
+ });
752
+ setPoints(_newPoints);
753
+ // Clear camera frame data after use to avoid reusing it
754
+ cameraFrameData.current = null;
755
+ return;
756
+ }
757
+ }
758
+
759
+ // ✅ CRITICAL: Default crop box (70% of displayed area - centered)
760
+ var boxWidth = width * 0.70; // 70% width
761
+ var boxHeight = height * 0.70; // 70% height
762
+ var boxX = x + (width - boxWidth) / 2; // Centered horizontally
763
+ var boxY = y + (height - boxHeight) / 2; // Centered vertically
764
+ var newPoints = [{
765
+ x: boxX,
766
+ y: boxY
767
+ },
768
+ // Top-left
769
+ {
770
+ x: boxX + boxWidth,
771
+ y: boxY
772
+ },
773
+ // Top-right
774
+ {
775
+ x: boxX + boxWidth,
776
+ y: boxY + boxHeight
777
+ },
778
+ // Bottom-right
779
+ {
780
+ x: boxX,
781
+ y: boxY + boxHeight
782
+ } // Bottom-left
783
+ ];
784
+ console.log("Initializing crop box (default - 70% centered):", {
785
+ displayedWidth: width,
786
+ displayedHeight: height,
787
+ boxWidth: boxWidth,
788
+ boxHeight: boxHeight,
789
+ boxX: boxX,
790
+ boxY: boxY,
791
+ points: newPoints
792
+ });
793
+ setPoints(newPoints);
794
+ };
795
+
796
+ // ✅ REFACTORISATION : Mettre à jour les dimensions d'affichage et les dimensions pour SVG
797
+ var onImageLayout = function onImageLayout(e) {
798
+ var layout = e.nativeEvent.layout;
799
+
800
+ // Stocker les dimensions d'affichage réelles (pour conversion de coordonnées)
801
+ displayedImageLayout.current = {
802
+ x: layout.x,
803
+ y: layout.y,
804
+ width: layout.width,
805
+ height: layout.height
806
+ };
807
+
808
+ // Conserver aussi dans imageMeasure pour compatibilité avec SVG overlay
809
+ imageMeasure.current = {
810
+ x: layout.x,
811
+ y: layout.y,
812
+ width: layout.width,
813
+ height: layout.height
814
+ };
815
+
816
+ // ✅ CORRECTION: recalculer contentRect (contain). Si les dimensions originales ne sont pas encore connues,
817
+ // updateDisplayedContentRect va fallback et sera recalculé quand Image.getSize arrivera.
818
+ updateDisplayedContentRect(layout.width, layout.height);
819
+ console.log("Displayed image layout updated:", {
820
+ width: layout.width,
821
+ height: layout.height,
822
+ x: layout.x,
823
+ y: layout.y
824
+ });
825
+
826
+ // ✅ CRITICAL FIX: Only initialize crop box if:
827
+ // 1. Layout dimensions are available
828
+ // 2. We have no points yet (first initialization)
829
+ // 3. We have original image dimensions (either from camera or Image.getSize)
830
+ // This prevents initializing with wrong dimensions for subsequent images
831
+ if (layout.width > 0 && layout.height > 0 && points.length === 0) {
832
+ // ✅ CRITICAL: Wait for original dimensions before initializing
833
+ // If dimensions not available yet, initializeCropBox will be called from useEffect when Image.getSize completes
834
+ if (originalImageDimensions.current.width > 0 && originalImageDimensions.current.height > 0) {
835
+ initializeCropBox();
836
+ } else if (cameraFrameData.current && cameraFrameData.current.capturedImageSize) {
837
+ // ✅ If we have camera dimensions, use them immediately
838
+ originalImageDimensions.current = {
839
+ width: cameraFrameData.current.capturedImageSize.width,
840
+ height: cameraFrameData.current.capturedImageSize.height
841
+ };
842
+ initializeCropBox();
843
+ } else {
844
+ // ✅ For gallery images, we can initialize with layout dimensions as fallback
845
+ // The crop box will be recalculated when Image.getSize completes
846
+ // But we initialize now so the user sees a border immediately
847
+ initializeCropBox();
848
+ }
849
+ }
850
+ };
851
+ var createPath = function createPath() {
852
+ if (points.length < 1) return '';
853
+ var path = "M ".concat(points[0].x, " ").concat(points[0].y, " ");
854
+ points.forEach(function (point) {
855
+ return path += "L ".concat(point.x, " ").concat(point.y, " ");
856
+ });
857
+ return path + 'Z';
858
+ };
859
+ var handleTap = function handleTap(e) {
860
+ if (!image || showResult) return;
861
+ var now = Date.now();
862
+ var _e$nativeEvent = e.nativeEvent,
863
+ tapX = _e$nativeEvent.locationX,
864
+ tapY = _e$nativeEvent.locationY;
865
+
866
+ // ✅ CRITICAL: Ensure displayedContentRect is available
867
+ var contentRect = displayedContentRect.current;
868
+ var layout = displayedImageLayout.current;
869
+
870
+ // Recalculate if not available
871
+ if (contentRect.width === 0 || contentRect.height === 0) {
872
+ if (layout.width > 0 && layout.height > 0) {
873
+ updateDisplayedContentRect(layout.width, layout.height);
874
+ contentRect = displayedContentRect.current;
875
+ }
876
+ // If still not available, use layout as fallback
877
+ if (contentRect.width === 0 || contentRect.height === 0) {
878
+ if (layout.width > 0 && layout.height > 0) {
879
+ contentRect = {
880
+ x: layout.x,
881
+ y: layout.y,
882
+ width: layout.width,
883
+ height: layout.height
884
+ };
885
+ } else {
886
+ console.warn("⚠️ Cannot handle tap: no layout dimensions available");
887
+ return;
888
+ }
889
+ }
890
+ }
891
+
892
+ // ✅ Clamp to real displayed image content (avoid points outside image due to letterboxing)
893
+ var _contentRect3 = contentRect,
894
+ cx = _contentRect3.x,
895
+ cy = _contentRect3.y,
896
+ cw = _contentRect3.width,
897
+ ch = _contentRect3.height;
898
+ var boundedTapX = Math.max(cx, Math.min(tapX, cx + cw));
899
+ var boundedTapY = Math.max(cy, Math.min(tapY, cy + ch));
900
+ var selectRadius = 28; // easier to grab points
901
+ if (lastTap.current && now - lastTap.current < 300) {
902
+ var exists = points.some(function (p) {
903
+ return Math.abs(p.x - boundedTapX) < selectRadius && Math.abs(p.y - boundedTapY) < selectRadius;
904
+ });
905
+ if (!exists) setPoints([].concat(_toConsumableArray(points), [{
906
+ x: boundedTapX,
907
+ y: boundedTapY
908
+ }]));
909
+ lastTap.current = null;
910
+ } else {
911
+ var index = points.findIndex(function (p) {
912
+ return Math.abs(p.x - boundedTapX) < selectRadius && Math.abs(p.y - boundedTapY) < selectRadius;
913
+ });
914
+ if (index !== -1) {
915
+ selectedPointIndex.current = index;
916
+ // ✅ FREE DRAG: Store initial positions for delta-based movement
917
+ initialTouchPosition.current = {
918
+ x: tapX,
919
+ y: tapY
920
+ };
921
+ lastTouchPosition.current = {
922
+ x: tapX,
923
+ y: tapY
924
+ }; // Store last touch for incremental delta
925
+ initialPointPosition.current = _objectSpread({}, points[index]);
926
+ lastValidPosition.current = _objectSpread({}, points[index]); // store original position before move
927
+
928
+ // ✅ CRITICAL: Disable parent ScrollView scrolling when dragging a point
929
+ // This prevents ScrollView from intercepting vertical movement
930
+ try {
931
+ // Find and disable parent ScrollView if it exists
932
+ var _findScrollView = function findScrollView(node) {
933
+ if (!node) return null;
934
+ if (node._component && node._component.setNativeProps) {
935
+ // Try to disable scrolling
936
+ node._component.setNativeProps({
937
+ scrollEnabled: false
938
+ });
939
+ }
940
+ return _findScrollView(node._owner || node._parent);
941
+ };
942
+ // Note: This is a workaround - ideally we'd pass a ref to disable scroll
943
+ } catch (e) {
944
+ // Ignore errors
945
+ }
946
+ }
947
+ lastTap.current = now;
948
+ }
949
+ };
950
+ var handleMove = function handleMove(e) {
951
+ if (showResult || selectedPointIndex.current === null) return;
952
+
953
+ // ✅ FREE DRAG: Use delta-based movement for smooth, unconstrained dragging
954
+ // ✅ CRITICAL FIX: Use incremental delta calculation for more reliable vertical movement
955
+ // Instead of calculating delta from initial position, calculate from last position
956
+ // This works better when ScrollView intercepts some events
957
+ var nativeEvent = e.nativeEvent;
958
+ var currentX = nativeEvent.locationX;
959
+ var currentY = nativeEvent.locationY;
960
+
961
+ // ✅ Validate coordinates
962
+ if (currentX === undefined || currentY === undefined || isNaN(currentX) || isNaN(currentY)) {
963
+ console.warn("⚠️ Cannot get touch coordinates", {
964
+ locationX: nativeEvent.locationX,
965
+ locationY: nativeEvent.locationY
966
+ });
967
+ return;
968
+ }
969
+
970
+ // ✅ CRITICAL: Use incremental delta (from last position) instead of absolute delta
971
+ // This is more reliable when ScrollView affects coordinate updates
972
+ var deltaX, deltaY;
973
+ if (lastTouchPosition.current) {
974
+ // Calculate incremental delta from last touch position
975
+ deltaX = currentX - lastTouchPosition.current.x;
976
+ deltaY = currentY - lastTouchPosition.current.y;
977
+ } else if (initialTouchPosition.current) {
978
+ // Fallback to absolute delta if lastTouchPosition not set
979
+ deltaX = currentX - initialTouchPosition.current.x;
980
+ deltaY = currentY - initialTouchPosition.current.y;
981
+ } else {
982
+ console.warn("⚠️ No touch position reference available");
983
+ return;
984
+ }
985
+
986
+ // ✅ CRITICAL: Don't update lastTouchPosition here - update it AFTER clamping
987
+ // This ensures that if the point was clamped, lastTouchPosition reflects the actual
988
+ // touch position, allowing the next delta to be calculated correctly
989
+
990
+ // ✅ CRITICAL: Ensure displayedContentRect is available
991
+ var contentRect = displayedContentRect.current;
992
+ var layout = displayedImageLayout.current;
993
+
994
+ // Recalculate if not available
995
+ if (contentRect.width === 0 || contentRect.height === 0) {
996
+ if (layout.width > 0 && layout.height > 0) {
997
+ updateDisplayedContentRect(layout.width, layout.height);
998
+ contentRect = displayedContentRect.current;
999
+ }
1000
+ // If still not available, use layout as fallback
1001
+ if (contentRect.width === 0 || contentRect.height === 0) {
1002
+ if (layout.width > 0 && layout.height > 0) {
1003
+ contentRect = {
1004
+ x: layout.x,
1005
+ y: layout.y,
1006
+ width: layout.width,
1007
+ height: layout.height
1008
+ };
1009
+ } else {
1010
+ console.warn("⚠️ Cannot move point: no layout dimensions available");
1011
+ return;
1012
+ }
1013
+ }
1014
+ }
1015
+
1016
+ // ✅ FREE DRAG: Ensure initial positions are set
1017
+ if (!initialPointPosition.current) {
1018
+ var currentPoint = points[selectedPointIndex.current];
1019
+ if (currentPoint) {
1020
+ initialPointPosition.current = _objectSpread({}, currentPoint);
1021
+ } else {
1022
+ console.warn("⚠️ No point found for selected index");
1023
+ return;
1024
+ }
1025
+ }
1026
+
1027
+ // ✅ CRITICAL: deltaX and deltaY are already calculated above using incremental approach
1028
+ // This ensures smooth movement even when ScrollView affects coordinate updates
1029
+
1030
+ // ✅ CRITICAL FIX: Use CURRENT point position (after previous clamping) instead of initial position
1031
+ // This allows the point to move even if it was previously clamped to a limit
1032
+ // If lastValidPosition exists (from previous clamping), use it; otherwise use initial position
1033
+ var basePoint = lastValidPosition.current || initialPointPosition.current;
1034
+
1035
+ // ✅ DEBUG: Log movement to identify vertical movement issues
1036
+ if (Math.abs(deltaY) > 10) {
1037
+ var _initialTouchPosition;
1038
+ console.log("🔄 Movement detected:", {
1039
+ deltaX: deltaX.toFixed(2),
1040
+ deltaY: deltaY.toFixed(2),
1041
+ currentY: currentY.toFixed(2),
1042
+ initialY: (_initialTouchPosition = initialTouchPosition.current) === null || _initialTouchPosition === void 0 || (_initialTouchPosition = _initialTouchPosition.y) === null || _initialTouchPosition === void 0 ? void 0 : _initialTouchPosition.toFixed(2),
1043
+ pointInitialY: initialPointPosition.current.y.toFixed(2),
1044
+ basePointY: basePoint.y.toFixed(2),
1045
+ usingLastValid: !!lastValidPosition.current
1046
+ });
1047
+ }
1048
+
1049
+ // ✅ FREE DRAG: Apply delta to CURRENT point position (not initial)
1050
+ // - This allows movement even if point was previously clamped
1051
+ // - No constraints on distance, angle, or edge length
1052
+ // - No forced horizontal/vertical alignment
1053
+ // - No angle locking
1054
+ // - Direct mapping of gesture delta (dx, dy) - both X and Y treated equally
1055
+ var newX = basePoint.x + deltaX;
1056
+ var newY = basePoint.y + deltaY;
1057
+
1058
+ // ✅ ONLY CONSTRAINT: Clamp to image bounds to keep points inside the image
1059
+ // x ∈ [cx, cx + cw], y ∈ [cy, cy + ch]
1060
+ // This is the ONLY constraint - no other geometry normalization
1061
+ var _contentRect4 = contentRect,
1062
+ cx = _contentRect4.x,
1063
+ cy = _contentRect4.y,
1064
+ cw = _contentRect4.width,
1065
+ ch = _contentRect4.height;
1066
+ // ✅ CRITICAL: Calculate bounds correctly - maxX = cx + cw, maxY = cy + ch
1067
+ var maxX = cx + cw;
1068
+ var maxY = cy + ch;
1069
+ var boundedX = Math.max(cx, Math.min(newX, maxX));
1070
+ var boundedY = Math.max(cy, Math.min(newY, maxY));
1071
+
1072
+ // ✅ DEBUG: Log bounds calculation to verify clamping is correct
1073
+ if (Math.abs(newY - boundedY) > 1) {
1074
+ console.log("🔍 Y coordinate clamping:", {
1075
+ newY: newY.toFixed(2),
1076
+ boundedY: boundedY.toFixed(2),
1077
+ cy: cy.toFixed(2),
1078
+ maxY: maxY.toFixed(2),
1079
+ ch: ch.toFixed(2),
1080
+ deltaY: deltaY.toFixed(2),
1081
+ isAtMaxLimit: Math.abs(boundedY - maxY) < 0.01,
1082
+ isAtMinLimit: Math.abs(boundedY - cy) < 0.01
1083
+ });
1084
+ }
1085
+
1086
+ // ✅ DEBUG: Log if clamping is limiting movement
1087
+ if (Math.abs(newY - boundedY) > 1 || Math.abs(newX - boundedX) > 1) {
1088
+ console.log("⚠️ Movement clamped:", {
1089
+ requested: {
1090
+ x: newX.toFixed(2),
1091
+ y: newY.toFixed(2)
1092
+ },
1093
+ clamped: {
1094
+ x: boundedX.toFixed(2),
1095
+ y: boundedY.toFixed(2)
1096
+ },
1097
+ contentRect: {
1098
+ x: cx.toFixed(2),
1099
+ y: cy.toFixed(2),
1100
+ width: cw.toFixed(2),
1101
+ height: ch.toFixed(2)
1102
+ }
1103
+ });
1104
+ }
1105
+
1106
+ // ✅ FREE DRAG: Update point directly
1107
+ // - No polygon simplification
1108
+ // - No reordering of points
1109
+ // - No geometry normalization during drag
1110
+ var updatedPoint = {
1111
+ x: boundedX,
1112
+ y: boundedY
1113
+ };
1114
+ // ✅ CRITICAL: Always update lastValidPosition with the clamped position
1115
+ // This ensures that the next movement calculation uses the current (clamped) position
1116
+ // instead of the initial position, allowing movement even after clamping
1117
+ lastValidPosition.current = updatedPoint;
1118
+
1119
+ // ✅ CRITICAL: Update lastTouchPosition AFTER clamping
1120
+ // This ensures that the next delta calculation is relative to the current touch position
1121
+ // If the point was clamped, this allows movement in the opposite direction on the next move
1122
+ lastTouchPosition.current = {
1123
+ x: currentX,
1124
+ y: currentY
1125
+ };
1126
+ setPoints(function (prev) {
1127
+ return prev.map(function (p, i) {
1128
+ return i === selectedPointIndex.current ? updatedPoint : p;
1129
+ });
1130
+ });
1131
+ };
1132
+ var handleRelease = function handleRelease() {
1133
+ // ✅ FREE DRAG: Clear initial positions when drag ends
1134
+ initialTouchPosition.current = null;
1135
+ initialPointPosition.current = null;
1136
+ var wasDragging = selectedPointIndex.current !== null;
1137
+ selectedPointIndex.current = null;
1138
+
1139
+ // ✅ CRITICAL: Re-enable parent ScrollView scrolling when drag ends
1140
+ if (wasDragging) {
1141
+ try {
1142
+ // Re-enable scrolling after a short delay to avoid conflicts
1143
+ setTimeout(function () {
1144
+ // ScrollView will be re-enabled automatically when responder is released
1145
+ }, 100);
1146
+ } catch (e) {
1147
+ // Ignore errors
1148
+ }
1149
+ }
1150
+ };
1151
+ var handleReset = function handleReset() {
1152
+ // setPoints([]);
1153
+ initializeCropBox();
1154
+ };
1155
+
1156
+ // ✅ REFACTORISATION : Stocker l'angle de rotation au lieu de modifier l'image immédiatement
1157
+ // La rotation sera appliquée uniquement lors du crop final pour éviter les interpolations multiples
1158
+ var rotatePreviewImage = /*#__PURE__*/function () {
1159
+ var _ref2 = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee(degrees) {
1160
+ var rotated, _t;
1161
+ return _regenerator().w(function (_context) {
1162
+ while (1) switch (_context.p = _context.n) {
1163
+ case 0:
1164
+ if (!(!image || isRotating)) {
1165
+ _context.n = 1;
1166
+ break;
1167
+ }
1168
+ return _context.a(2);
1169
+ case 1:
1170
+ setIsRotating(true);
1171
+ _context.p = 2;
1172
+ // ✅ CORRECTION : appliquer la rotation de façon incrémentale sur le fichier (pas cumulée sur un fichier déjà roté).
1173
+ // L'ancienne version rotait l'image déjà rotée par l'angle total, ce qui donnait des rotations incorrectes (90 + 180 => 270).
1174
+ rotationAngle.current = (rotationAngle.current + degrees) % 360;
1175
+ _context.n = 3;
1176
+ return ImageManipulator.manipulateAsync(image, [{
1177
+ rotate: degrees
1178
+ }], {
1179
+ compress: 1,
1180
+ // Qualité maximale (pas de compression)
1181
+ format: ImageManipulator.SaveFormat.PNG // Format sans perte
1182
+ });
1183
+ case 3:
1184
+ rotated = _context.v;
1185
+ // Update image pour l'aperçu - onImageLayout will call initializeCropBox automatically
1186
+ setImage(rotated.uri);
1187
+ console.log("Rotation applied (preview increment):", degrees, "degrees; accumulated:", rotationAngle.current);
1188
+ _context.n = 5;
1189
+ break;
1190
+ case 4:
1191
+ _context.p = 4;
1192
+ _t = _context.v;
1193
+ console.error("Error rotating image:", _t);
1194
+ alert("Error rotating image");
1195
+ case 5:
1196
+ _context.p = 5;
1197
+ setIsRotating(false);
1198
+ return _context.f(5);
1199
+ case 6:
1200
+ return _context.a(2);
1201
+ }
1202
+ }, _callee, null, [[2, 4, 5, 6]]);
1203
+ }));
1204
+ return function rotatePreviewImage(_x) {
1205
+ return _ref2.apply(this, arguments);
1206
+ };
1207
+ }();
1208
+
1209
+ // Helper function to wait for multiple render cycles (works on iOS)
1210
+ var waitForRender = function waitForRender() {
1211
+ var cycles = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 5;
1212
+ return new Promise(function (resolve) {
1213
+ var count = 0;
1214
+ var _tick = function tick() {
1215
+ count++;
1216
+ if (count >= cycles) {
1217
+ resolve();
1218
+ } else {
1219
+ setImmediate(_tick);
1220
+ }
1221
+ };
1222
+ setImmediate(_tick);
1223
+ });
1224
+ };
1225
+ return /*#__PURE__*/_react["default"].createElement(_reactNative.View, {
1226
+ style: _ImageCropperStyles["default"].container
1227
+ }, showCustomCamera ? /*#__PURE__*/_react["default"].createElement(_CustomCamera["default"], {
1228
+ onPhotoCaptured: function onPhotoCaptured(uri, frameData) {
1229
+ // ✅ CRITICAL FIX: Store green frame coordinates for coordinate conversion
1230
+ if (frameData && frameData.greenFrame) {
1231
+ cameraFrameData.current = {
1232
+ greenFrame: frameData.greenFrame,
1233
+ capturedImageSize: frameData.capturedImageSize
1234
+ };
1235
+ console.log("✅ Camera frame data received:", cameraFrameData.current);
1236
+ }
1237
+ setImage(uri);
1238
+ setShowCustomCamera(false);
1239
+ // ✅ CORRECTION : Réinitialiser les points et l'angle de rotation quand une nouvelle photo est capturée
1240
+ setPoints([]);
1241
+ rotationAngle.current = 0;
1242
+ // ✅ CRITICAL: initializeCropBox will be called automatically when image layout is ready
1243
+ // The green frame coordinates are stored in cameraFrameData.current and will be used
1244
+ },
1245
+ onCancel: function onCancel() {
1246
+ return setShowCustomCamera(false);
1247
+ }
1248
+ }) : /*#__PURE__*/_react["default"].createElement(_react["default"].Fragment, null, !showResult && /*#__PURE__*/_react["default"].createElement(_reactNative.View, {
1249
+ style: image ? _ImageCropperStyles["default"].buttonContainer : _ImageCropperStyles["default"].centerButtonsContainer
1250
+ }, image && _reactNative.Platform.OS === 'android' && /*#__PURE__*/_react["default"].createElement(_react["default"].Fragment, null, /*#__PURE__*/_react["default"].createElement(_reactNative.TouchableOpacity, {
1251
+ style: _ImageCropperStyles["default"].iconButton,
1252
+ onPress: function onPress() {
1253
+ return enableRotation && rotatePreviewImage(90);
1254
+ },
1255
+ disabled: isRotating
1256
+ }, /*#__PURE__*/_react["default"].createElement(_vectorIcons.Ionicons, {
1257
+ name: "sync",
1258
+ size: 24,
1259
+ color: "white"
1260
+ }))), image && /*#__PURE__*/_react["default"].createElement(_reactNative.TouchableOpacity, {
1261
+ style: _ImageCropperStyles["default"].button,
1262
+ onPress: handleReset
1263
+ }, /*#__PURE__*/_react["default"].createElement(_reactNative.Text, {
1264
+ style: _ImageCropperStyles["default"].buttonText
1265
+ }, "Reset")), image && /*#__PURE__*/_react["default"].createElement(_reactNative.TouchableOpacity, {
1266
+ style: _ImageCropperStyles["default"].button,
1267
+ onPress: /*#__PURE__*/_asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee2() {
1268
+ var actualImageWidth, actualImageHeight, layout, contentRect, displayedWidth, displayedHeight, scaleX, scaleY, scale, originalUri, cropMeta, imagePoints, minX, minY, maxX, maxY, cropX, cropY, cropEndX, cropEndY, cropWidth, cropHeight, bbox, polygon, name, _t2;
1269
+ return _regenerator().w(function (_context2) {
1270
+ while (1) switch (_context2.p = _context2.n) {
1271
+ case 0:
1272
+ setIsLoading(true);
1273
+ _context2.p = 1;
1274
+ console.log("=== Starting pixel-perfect metadata export (no bitmap crop on mobile) ===");
1275
+
1276
+ // ✅ REFACTORISATION : Utiliser les dimensions stockées (plus efficace)
1277
+ actualImageWidth = originalImageDimensions.current.width;
1278
+ actualImageHeight = originalImageDimensions.current.height; // Vérifier que les dimensions sont valides
1279
+ if (!(actualImageWidth === 0 || actualImageHeight === 0)) {
1280
+ _context2.n = 2;
1281
+ break;
1282
+ }
1283
+ throw new Error("Original image dimensions not available. Please wait for image to load.");
1284
+ case 2:
1285
+ console.log("Original image dimensions:", {
1286
+ width: actualImageWidth,
1287
+ height: actualImageHeight
1288
+ });
1289
+
1290
+ // ✅ CORRECTION : Utiliser le rectangle de contenu réel (contain)
1291
+ // ✅ CRITICAL: Recalculate displayedContentRect if not available
1292
+ layout = displayedImageLayout.current;
1293
+ console.log("🔍 Checking displayedContentRect before crop:", {
1294
+ displayedContentRect: displayedContentRect.current,
1295
+ displayedImageLayout: layout,
1296
+ originalImageDimensions: originalImageDimensions.current
1297
+ });
1298
+ if (layout.width > 0 && layout.height > 0) {
1299
+ updateDisplayedContentRect(layout.width, layout.height);
1300
+ }
1301
+ contentRect = displayedContentRect.current;
1302
+ displayedWidth = contentRect.width;
1303
+ displayedHeight = contentRect.height; // Vérifier que les dimensions d'affichage sont valides
1304
+ if (!(displayedWidth === 0 || displayedHeight === 0)) {
1305
+ _context2.n = 4;
1306
+ break;
1307
+ }
1308
+ if (!(layout.width > 0 && layout.height > 0)) {
1309
+ _context2.n = 3;
1310
+ break;
1311
+ }
1312
+ console.warn("⚠️ displayedContentRect not available, using displayedImageLayout as fallback");
1313
+ // Use layout dimensions as fallback (assuming no letterboxing)
1314
+ contentRect = {
1315
+ x: layout.x,
1316
+ y: layout.y,
1317
+ width: layout.width,
1318
+ height: layout.height
1319
+ };
1320
+ displayedWidth = contentRect.width;
1321
+ displayedHeight = contentRect.height;
1322
+ // Update the ref for consistency
1323
+ displayedContentRect.current = contentRect;
1324
+ _context2.n = 4;
1325
+ break;
1326
+ case 3:
1327
+ throw new Error("Displayed image dimensions not available. Image may not be laid out yet. Please wait a moment and try again.");
1328
+ case 4:
1329
+ console.log("✅ Using contentRect for crop:", contentRect);
1330
+ console.log("Displayed image dimensions:", {
1331
+ width: displayedWidth,
1332
+ height: displayedHeight
1333
+ });
1334
+
1335
+ // ✅ CORRECTION : avec resizeMode='contain', l'échelle est UNIFORME + offsets.
1336
+ // Vérifier que le scale est cohérent (même ratio pour width et height)
1337
+ scaleX = actualImageWidth / displayedWidth;
1338
+ scaleY = actualImageHeight / displayedHeight; // Pour resizeMode='contain', scaleX et scaleY doivent être égaux
1339
+ // Si ce n'est pas le cas, il y a un problème de calcul
1340
+ if (Math.abs(scaleX - scaleY) > 0.01) {
1341
+ console.warn("Scale mismatch detected! This may cause incorrect crop coordinates.", {
1342
+ scaleX: scaleX,
1343
+ scaleY: scaleY,
1344
+ actualImageWidth: actualImageWidth,
1345
+ actualImageHeight: actualImageHeight,
1346
+ displayedWidth: displayedWidth,
1347
+ displayedHeight: displayedHeight
1348
+ });
1349
+ }
1350
+ scale = scaleX; // Utiliser scaleX (ou scaleY, ils devraient être égaux)
1351
+ console.log("Scale factor (contain, uniform):", {
1352
+ scale: scale,
1353
+ scaleX: scaleX,
1354
+ scaleY: scaleY,
1355
+ contentRect: contentRect,
1356
+ actualImageSize: {
1357
+ width: actualImageWidth,
1358
+ height: actualImageHeight
1359
+ },
1360
+ displayedSize: {
1361
+ width: displayedWidth,
1362
+ height: displayedHeight
1363
+ }
1364
+ });
1365
+ originalUri = sourceImageUri.current || image;
1366
+ cropMeta = null;
1367
+ if (points.length > 0) {
1368
+ try {
1369
+ console.log("Calculating crop boundaries from points...");
1370
+ console.log("Points (display coordinates):", points);
1371
+ console.log("Content rect (offsets):", contentRect);
1372
+
1373
+ // ✅ CORRECTION : conversion display -> image px (contain) avec offsets
1374
+ // S'assurer que les points sont bien dans les limites de l'image affichée
1375
+ imagePoints = points.map(function (point) {
1376
+ // Clamp les coordonnées d'affichage à la zone réelle de l'image
1377
+ var clampedX = Math.max(contentRect.x, Math.min(point.x, contentRect.x + contentRect.width));
1378
+ var clampedY = Math.max(contentRect.y, Math.min(point.y, contentRect.y + contentRect.height));
1379
+
1380
+ // Convertir en coordonnées de l'image originale
1381
+ var origX = (clampedX - contentRect.x) * scale;
1382
+ var origY = (clampedY - contentRect.y) * scale;
1383
+
1384
+ // Clamp aux dimensions de l'image originale
1385
+ var finalX = Math.max(0, Math.min(origX, actualImageWidth));
1386
+ var finalY = Math.max(0, Math.min(origY, actualImageHeight));
1387
+ return {
1388
+ x: finalX,
1389
+ y: finalY
1390
+ };
1391
+ });
1392
+ console.log("Converted image points (original coordinates):", imagePoints);
1393
+
1394
+ // Calculer la bounding box : min X, min Y, max X, max Y
1395
+ minX = Math.min.apply(Math, _toConsumableArray(imagePoints.map(function (p) {
1396
+ return p.x;
1397
+ })));
1398
+ minY = Math.min.apply(Math, _toConsumableArray(imagePoints.map(function (p) {
1399
+ return p.y;
1400
+ })));
1401
+ maxX = Math.max.apply(Math, _toConsumableArray(imagePoints.map(function (p) {
1402
+ return p.x;
1403
+ })));
1404
+ maxY = Math.max.apply(Math, _toConsumableArray(imagePoints.map(function (p) {
1405
+ return p.y;
1406
+ }))); // ✅ CORRECTION : arrondi "conservateur" (floor origin + ceil end)
1407
+ // évite de rogner des pixels et réduit le risque de crop plus petit (perte de détails).
1408
+ cropX = Math.max(0, Math.floor(minX));
1409
+ cropY = Math.max(0, Math.floor(minY));
1410
+ cropEndX = Math.min(actualImageWidth, Math.ceil(maxX));
1411
+ cropEndY = Math.min(actualImageHeight, Math.ceil(maxY));
1412
+ cropWidth = Math.max(0, cropEndX - cropX);
1413
+ cropHeight = Math.max(0, cropEndY - cropY);
1414
+ console.log("Crop parameters (pixel-perfect):", {
1415
+ x: cropX,
1416
+ y: cropY,
1417
+ width: cropWidth,
1418
+ height: cropHeight,
1419
+ imageWidth: actualImageWidth,
1420
+ imageHeight: actualImageHeight,
1421
+ boundingBox: {
1422
+ minX: minX,
1423
+ minY: minY,
1424
+ maxX: maxX,
1425
+ maxY: maxY
1426
+ }
1427
+ });
1428
+ if (cropWidth > 0 && cropHeight > 0) {
1429
+ // 1) bbox in ORIGINAL image pixel coords
1430
+ bbox = {
1431
+ x: cropX,
1432
+ y: cropY,
1433
+ width: cropWidth,
1434
+ height: cropHeight
1435
+ }; // 2) polygon points relative to bbox (still in ORIGINAL pixel grid)
1436
+ polygon = imagePoints.map(function (point) {
1437
+ return {
1438
+ x: point.x - cropX,
1439
+ y: point.y - cropY
1440
+ };
1441
+ });
1442
+ cropMeta = {
1443
+ bbox: bbox,
1444
+ polygon: polygon,
1445
+ rotation: 0,
1446
+ imageSize: {
1447
+ width: actualImageWidth,
1448
+ height: actualImageHeight
1449
+ }
1450
+ };
1451
+ console.log("Crop meta ready:", cropMeta);
1452
+ } else {
1453
+ console.warn("Invalid crop dimensions, cannot export crop meta");
1454
+ }
1455
+ } catch (cropError) {
1456
+ console.error("Error computing crop meta:", cropError);
1457
+ }
1458
+ } else {
1459
+ console.log("No crop points defined, using original image");
1460
+ }
1461
+ name = "IMAGE XTK".concat(Date.now());
1462
+ if (onConfirm) {
1463
+ console.log("Calling onConfirm with:", originalUri, name, cropMeta);
1464
+ onConfirm(originalUri, name, cropMeta);
1465
+ }
1466
+ _context2.n = 6;
1467
+ break;
1468
+ case 5:
1469
+ _context2.p = 5;
1470
+ _t2 = _context2.v;
1471
+ console.error("Erreur lors du crop :", _t2);
1472
+ alert("Erreur lors du crop ! " + _t2.message);
1473
+ case 6:
1474
+ _context2.p = 6;
1475
+ setShowResult(false);
1476
+ setIsLoading(false);
1477
+ setShowFullScreenCapture(false);
1478
+ return _context2.f(6);
1479
+ case 7:
1480
+ return _context2.a(2);
1481
+ }
1482
+ }, _callee2, null, [[1, 5, 6, 7]]);
1483
+ }))
1484
+ }, /*#__PURE__*/_react["default"].createElement(_reactNative.Text, {
1485
+ style: _ImageCropperStyles["default"].buttonText
1486
+ }, "Confirm"))), image && /*#__PURE__*/_react["default"].createElement(_reactNative.View, {
1487
+ ref: viewRef,
1488
+ collapsable: false,
1489
+ style: showFullScreenCapture ? _ImageCropperStyles["default"].fullscreenImageContainer : _ImageCropperStyles["default"].imageContainer,
1490
+ onStartShouldSetResponder: function onStartShouldSetResponder() {
1491
+ return true;
1492
+ },
1493
+ onMoveShouldSetResponder: function onMoveShouldSetResponder(evt, gestureState) {
1494
+ // ✅ CRITICAL: Always capture movement when a point is selected
1495
+ // This ensures vertical movement is captured correctly
1496
+ if (selectedPointIndex.current !== null) {
1497
+ return true;
1498
+ }
1499
+ // ✅ CRITICAL: Capture ANY movement immediately (even 0px) to prevent ScrollView interception
1500
+ // This is especially important for vertical movement which ScrollView tries to intercept
1501
+ // We return true for ANY movement to ensure we capture it before ScrollView
1502
+ var hasMovement = Math.abs(gestureState.dx) > 0 || Math.abs(gestureState.dy) > 0;
1503
+ if (hasMovement && Math.abs(gestureState.dy) > 5) {
1504
+ console.log("🔄 Vertical movement detected in responder:", {
1505
+ dx: gestureState.dx.toFixed(2),
1506
+ dy: gestureState.dy.toFixed(2),
1507
+ selectedPoint: selectedPointIndex.current
1508
+ });
1509
+ }
1510
+ return true;
1511
+ },
1512
+ onResponderGrant: function onResponderGrant(e) {
1513
+ // ✅ CRITICAL: Grant responder immediately to prevent ScrollView from intercepting
1514
+ // This ensures we capture all movement, especially vertical
1515
+ // Handle tap to select point if needed
1516
+ if (selectedPointIndex.current === null) {
1517
+ handleTap(e);
1518
+ }
1519
+ },
1520
+ onResponderStart: handleTap,
1521
+ onResponderMove: function onResponderMove(e) {
1522
+ // ✅ CRITICAL: Always handle move events to ensure smooth movement in all directions
1523
+ // This is called for every move event, ensuring vertical movement is captured
1524
+ // handleMove now uses incremental delta calculation which is more reliable
1525
+ handleMove(e);
1526
+ },
1527
+ onResponderRelease: handleRelease,
1528
+ onResponderTerminationRequest: function onResponderTerminationRequest() {
1529
+ // ✅ CRITICAL: Never allow termination when dragging a point
1530
+ // This prevents ScrollView from stealing the responder during vertical movement
1531
+ return selectedPointIndex.current === null;
1532
+ }
1533
+ // ✅ CRITICAL: Prevent parent ScrollView from intercepting touches
1534
+ // Capture responder BEFORE parent ScrollView can intercept
1535
+ ,
1536
+ onStartShouldSetResponderCapture: function onStartShouldSetResponderCapture() {
1537
+ // Always capture start events
1538
+ return true;
1539
+ },
1540
+ onMoveShouldSetResponderCapture: function onMoveShouldSetResponderCapture(evt, gestureState) {
1541
+ // ✅ CRITICAL: Always capture movement events before parent ScrollView
1542
+ // This is essential for vertical movement which ScrollView tries to intercept
1543
+ // Especially important when a point is selected or when there's any movement
1544
+ if (selectedPointIndex.current !== null) {
1545
+ return true;
1546
+ }
1547
+ // Also capture if there's any movement to prevent ScrollView from intercepting
1548
+ var hasMovement = Math.abs(gestureState.dx) > 0 || Math.abs(gestureState.dy) > 0;
1549
+ if (hasMovement && Math.abs(gestureState.dy) > 2) {
1550
+ console.log("🔄 Capturing vertical movement before ScrollView:", {
1551
+ dy: gestureState.dy.toFixed(2)
1552
+ });
1553
+ }
1554
+ return hasMovement;
1555
+ }
1556
+ // ✅ CRITICAL: Prevent ScrollView from scrolling by stopping propagation
1557
+ ,
1558
+ onResponderReject: function onResponderReject() {
1559
+ console.warn("⚠️ Responder rejected - ScrollView may intercept");
1560
+ }
1561
+ }, /*#__PURE__*/_react["default"].createElement(_reactNative.Image, {
1562
+ source: {
1563
+ uri: image
1564
+ },
1565
+ style: _ImageCropperStyles["default"].image,
1566
+ onLayout: onImageLayout
1567
+ }), /*#__PURE__*/_react["default"].createElement(_reactNativeSvg["default"], {
1568
+ style: _ImageCropperStyles["default"].overlay,
1569
+ pointerEvents: "none"
1570
+ }, /*#__PURE__*/_react["default"].createElement(_reactNativeSvg.Path, {
1571
+ d: "M 0 0 H ".concat(imageMeasure.current.width, " V ").concat(imageMeasure.current.height, " H 0 Z ").concat(createPath()),
1572
+ fill: showResult ? 'white' : 'rgba(0, 0, 0, 0.8)',
1573
+ fillRule: "evenodd"
1574
+ }), !showResult && points.length > 0 && /*#__PURE__*/_react["default"].createElement(_reactNativeSvg.Path, {
1575
+ d: createPath(),
1576
+ fill: "transparent",
1577
+ stroke: "white",
1578
+ strokeWidth: 2
1579
+ }), !showResult && points.map(function (point, index) {
1580
+ return /*#__PURE__*/_react["default"].createElement(_reactNativeSvg.Circle, {
1581
+ key: index,
1582
+ cx: point.x,
1583
+ cy: point.y,
1584
+ r: 10,
1585
+ fill: "white"
1586
+ });
1587
+ })))), showMaskView && maskImageUri && maskPoints.length > 0 && /*#__PURE__*/_react["default"].createElement(_reactNative.View, {
1588
+ style: {
1589
+ position: 'absolute',
1590
+ left: -maskDimensions.width - 100,
1591
+ // Hors écran mais pas trop loin
1592
+ top: -maskDimensions.height - 100,
1593
+ width: maskDimensions.width,
1594
+ height: maskDimensions.height,
1595
+ opacity: 1,
1596
+ // Opacité normale pour la capture
1597
+ pointerEvents: 'none',
1598
+ zIndex: 9999,
1599
+ // Z-index élevé pour s'assurer qu'elle est au-dessus
1600
+ overflow: 'hidden'
1601
+ },
1602
+ collapsable: false
1603
+ }, /*#__PURE__*/_react["default"].createElement(_ImageMaskProcessor.MaskView, {
1604
+ ref: maskViewRef,
1605
+ imageUri: maskImageUri,
1606
+ points: maskPoints,
1607
+ width: maskDimensions.width,
1608
+ height: maskDimensions.height
1609
+ })), /*#__PURE__*/_react["default"].createElement(_reactNative.Modal, {
1610
+ visible: isLoading,
1611
+ transparent: true,
1612
+ animationType: "fade"
1613
+ }, /*#__PURE__*/_react["default"].createElement(_reactNative.View, {
1614
+ style: _ImageCropperStyles["default"].loadingOverlay
1615
+ }, /*#__PURE__*/_react["default"].createElement(_reactNative.Image, {
1616
+ source: require('../src/assets/loadingCamera.gif'),
1617
+ style: _ImageCropperStyles["default"].loadingGif,
1618
+ resizeMode: "contain"
1619
+ }))));
1620
+ };
514
1621
  var _default = exports["default"] = ImageCropper;