react-native-image-stitcher 0.1.1 → 0.1.3

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.
Files changed (35) hide show
  1. package/CHANGELOG.md +115 -1
  2. package/README.md +0 -9
  3. package/RNImageStitcher.podspec +10 -4
  4. package/android/src/main/cpp/keyframe_gate_jni.cpp +1 -1
  5. package/android/src/main/java/io/imagestitcher/rn/BatchStitcher.kt +1 -1
  6. package/android/src/main/java/io/imagestitcher/rn/FileBridge.kt +79 -0
  7. package/android/src/main/java/io/imagestitcher/rn/IncrementalStitcher.kt +11 -3
  8. package/android/src/main/java/io/imagestitcher/rn/KeyframeGate.kt +1 -1
  9. package/android/src/main/java/io/imagestitcher/rn/RNImageStitcherPackage.kt +1 -0
  10. package/dist/camera/Camera.d.ts +29 -1
  11. package/dist/camera/Camera.js +47 -37
  12. package/dist/camera/useCapture.d.ts +31 -1
  13. package/dist/camera/useCapture.js +23 -2
  14. package/dist/index.d.ts +1 -0
  15. package/dist/stitching/stitchFrames.d.ts +1 -1
  16. package/dist/stitching/stitchFrames.js +1 -1
  17. package/dist/utils/files.d.ts +44 -0
  18. package/dist/utils/files.js +84 -0
  19. package/dist/utils/paths.d.ts +30 -0
  20. package/dist/utils/paths.js +48 -0
  21. package/ios/Package.swift +1 -1
  22. package/ios/Sources/RNImageStitcher/FileBridge.m +20 -0
  23. package/ios/Sources/RNImageStitcher/FileBridge.swift +110 -0
  24. package/ios/Sources/RNImageStitcher/IncrementalStitcher.swift +4 -4
  25. package/ios/Sources/RNImageStitcher/KeyframeGate.swift +1 -1
  26. package/ios/Sources/RNImageStitcher/KeyframeGateBridge.h +1 -1
  27. package/ios/Sources/RNImageStitcher/KeyframeGateBridge.mm +1 -1
  28. package/ios/Sources/RNImageStitcher/RNSARSession.swift +1 -1
  29. package/package.json +1 -1
  30. package/src/camera/Camera.tsx +85 -35
  31. package/src/camera/useCapture.ts +62 -3
  32. package/src/index.ts +1 -0
  33. package/src/stitching/stitchFrames.ts +1 -1
  34. package/src/utils/files.ts +97 -0
  35. package/src/utils/paths.ts +42 -0
@@ -38,6 +38,12 @@ import {
38
38
 
39
39
  import { runQualityCheck } from '../quality/runQualityCheck';
40
40
  import { normaliseOrientation } from '../quality/normaliseOrientation';
41
+ import { toBareFilePath } from '../utils/paths';
42
+ import {
43
+ defaultPhotoFilename,
44
+ getDefaultCaptureDir,
45
+ moveFile,
46
+ } from '../utils/files';
41
47
  import type {
42
48
  CaptureResult,
43
49
  QualityReport,
@@ -91,6 +97,27 @@ export interface UseCaptureOptions {
91
97
  }
92
98
 
93
99
 
100
+ /**
101
+ * Per-call options for `takePhoto`. Separate from `UseCaptureOptions`
102
+ * (the hook-level config) so callers can vary the destination
103
+ * filename per capture without re-creating the hook.
104
+ */
105
+ export interface TakePhotoCallOptions {
106
+ /**
107
+ * Move the captured JPEG to this fully-resolved path after EXIF
108
+ * orientation correction. Requires `expo-file-system` in the
109
+ * host (declared as an OPTIONAL peer — only needed when
110
+ * `outputPath` is set). Host is responsible for the destination
111
+ * directory's existence and writability; lib rejects loudly on
112
+ * disk failure rather than silently falling back to a tmp path.
113
+ *
114
+ * Format: bare path (e.g. `/data/.../foo.jpg`) or `file://`-prefixed
115
+ * URI — both accepted; lib normalises internally.
116
+ */
117
+ outputPath?: string;
118
+ }
119
+
120
+
94
121
  /**
95
122
  * Hook output. Intentionally flat so destructuring a subset is
96
123
  * cheap and the API doesn't force callers to drill into nested
@@ -114,8 +141,19 @@ export interface UseCaptureReturn {
114
141
  * Take a photo. Single-flight: parallel calls return the in-flight
115
142
  * promise. Returns a CaptureResult (with an optional QualityReport
116
143
  * when ``enableQualityChecks`` is on).
144
+ *
145
+ * `outputPath` (optional): a fully-resolved destination path. When
146
+ * set, the lib moves the captured JPEG to that path after EXIF
147
+ * orientation correction, and the returned `compressedUri` points
148
+ * at the moved file. The host is responsible for ensuring the
149
+ * destination directory exists and is writable; on disk failure,
150
+ * the promise rejects with an error referencing `outputPath`.
151
+ *
152
+ * Requires `expo-file-system` to be installed in the host app
153
+ * (declared as an OPTIONAL peer dep — consumers that don't pass
154
+ * `outputPath` aren't required to have it).
117
155
  */
118
- takePhoto: () => Promise<CaptureResult>;
156
+ takePhoto: (options?: TakePhotoCallOptions) => Promise<CaptureResult>;
119
157
  /**
120
158
  * 2026-05-14 — physical lens types available on the chosen
121
159
  * `cameraPosition`. Computed once at the first vision-camera
@@ -217,7 +255,7 @@ export function useCapture(options: UseCaptureOptions = {}): UseCaptureReturn {
217
255
  setFlash((prev) => (prev === 'off' ? 'on' : 'off'));
218
256
  }, []);
219
257
 
220
- const takePhoto = useCallback(async (): Promise<CaptureResult> => {
258
+ const takePhoto = useCallback(async (callOptions?: TakePhotoCallOptions): Promise<CaptureResult> => {
221
259
  if (inFlightRef.current) {
222
260
  return inFlightRef.current;
223
261
  }
@@ -245,12 +283,33 @@ export function useCapture(options: UseCaptureOptions = {}): UseCaptureReturn {
245
283
  width: photo.width,
246
284
  height: photo.height,
247
285
  });
248
- const orientedPhoto: PhotoFile = {
286
+ let orientedPhoto: PhotoFile = {
249
287
  ...photo,
250
288
  width: normalised.width || photo.width,
251
289
  height: normalised.height || photo.height,
252
290
  };
253
291
 
292
+ // Move the orientation-corrected file to its final location.
293
+ // If the caller passed `outputPath`, use that. Otherwise, the
294
+ // lib publishes captures into its canonical default dir so
295
+ // returned paths are predictable across consumers (vs.
296
+ // vision-camera's auto-generated UUID-named tmp file). The
297
+ // move is performed via the `RNImageStitcherFileUtils` native
298
+ // bridge — no peer-dep on `expo-file-system` etc.
299
+ try {
300
+ const dstPath = callOptions?.outputPath
301
+ ? toBareFilePath(callOptions.outputPath)
302
+ : `${await getDefaultCaptureDir()}/${defaultPhotoFilename()}`;
303
+ await moveFile(orientedPhoto.path, dstPath);
304
+ orientedPhoto = { ...orientedPhoto, path: dstPath };
305
+ } catch (e) {
306
+ throw new Error(
307
+ 'useCapture.takePhoto: failed to move captured photo to its '
308
+ + `destination${callOptions?.outputPath ? ` (${callOptions.outputPath})` : ' (default capture dir)'}. `
309
+ + `Underlying: ${e instanceof Error ? e.message : String(e)}`,
310
+ );
311
+ }
312
+
254
313
  let report: QualityReport | undefined;
255
314
  if (enableQualityChecks && qualityThresholds) {
256
315
  report = await runQualityCheck(orientedPhoto.path, qualityThresholds);
package/src/index.ts CHANGED
@@ -98,6 +98,7 @@ export { ViewportCropOverlay } from './camera/ViewportCropOverlay';
98
98
  // vision-camera wrappers (useCapture / useVideoCapture) + a
99
99
  // device-orientation reader that works under iOS portrait-lock.
100
100
  export { useCapture } from './camera/useCapture';
101
+ export type { TakePhotoCallOptions } from './camera/useCapture';
101
102
  export { useVideoCapture } from './camera/useVideoCapture';
102
103
  export { useDeviceOrientation } from './camera/useDeviceOrientation';
103
104
 
@@ -6,7 +6,7 @@
6
6
  * - iOS: Swift native module that vendors upstream OpenCV's iOS
7
7
  * framework and calls `cv::Stitcher::SCANS` mode (designed for
8
8
  * translational shelf captures). Lives in
9
- * `retailens-capture-sdk/ios/Sources/RNImageStitcher/`.
9
+ * `react-native-image-stitcher/ios/Sources/RNImageStitcher/`.
10
10
  * - Android: deferred to Phase 3 — same OpenCV surface, different
11
11
  * build (NDK + Gradle). Until that lands, Android calls hit the
12
12
  * `StitchNotImplementedError` path below.
@@ -0,0 +1,97 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ /**
3
+ * Thin JS wrapper around the `RNImageStitcherFileUtils` native
4
+ * module (defined in `ios/Sources/RNImageStitcher/FileBridge.{swift,m}`
5
+ * and `android/src/main/.../FileBridge.kt`). Internal — not
6
+ * re-exported from `src/index.ts`.
7
+ *
8
+ * Two operations:
9
+ * - `moveFile(from, to)` — move a file, creating the destination's
10
+ * parent directory tree on demand. Used to relocate
11
+ * vision-camera's auto-named tmp output into the lib's canonical
12
+ * capture dir.
13
+ * - `getDefaultCaptureDir()` — resolve (and create on first call)
14
+ * the canonical default capture directory:
15
+ *
16
+ * iOS: `<NSCachesDirectory>/react-native-image-stitcher/`
17
+ * Android: `<context.cacheDir>/react-native-image-stitcher/`
18
+ *
19
+ * Predictable, evictable-by-OS, NOT backed up. Captures live
20
+ * here until the host moves them somewhere durable (which is
21
+ * intentional — the lib doesn't promise persistence beyond the
22
+ * immediate capture flow).
23
+ */
24
+
25
+ import { NativeModules } from 'react-native';
26
+
27
+
28
+ interface FileUtilsBridge {
29
+ moveFile(from: string, to: string): Promise<string>;
30
+ defaultCaptureDir(): Promise<string>;
31
+ }
32
+
33
+
34
+ function bridge(): FileUtilsBridge | null {
35
+ const m = (NativeModules as Record<string, unknown>).RNImageStitcherFileUtils;
36
+ if (!m || typeof m !== 'object') return null;
37
+ return m as FileUtilsBridge;
38
+ }
39
+
40
+
41
+ /**
42
+ * Move a file via the native bridge. Both paths accepted in bare
43
+ * or `file://`-prefixed form. Resolves to the bare destination
44
+ * path on success. Throws on disk failure.
45
+ */
46
+ export async function moveFile(from: string, to: string): Promise<string> {
47
+ const b = bridge();
48
+ if (!b) {
49
+ throw new Error(
50
+ 'react-native-image-stitcher: RNImageStitcherFileUtils native '
51
+ + 'module is not registered. Check that the host app has '
52
+ + 'rebuilt against the latest pod/Gradle install.',
53
+ );
54
+ }
55
+ return b.moveFile(from, to);
56
+ }
57
+
58
+
59
+ // Cached after the first resolve — the dir doesn't move during the
60
+ // lifetime of the app, and the on-first-call mkdir is idempotent.
61
+ let cachedDefaultDir: string | null = null;
62
+
63
+ /**
64
+ * Resolve the canonical default capture directory. Lazy +
65
+ * memoised — the native side creates the dir on first call, JS
66
+ * caches the result for the rest of the app session.
67
+ */
68
+ export async function getDefaultCaptureDir(): Promise<string> {
69
+ if (cachedDefaultDir !== null) return cachedDefaultDir;
70
+ const b = bridge();
71
+ if (!b) {
72
+ throw new Error(
73
+ 'react-native-image-stitcher: RNImageStitcherFileUtils native '
74
+ + 'module is not registered. Check that the host app has '
75
+ + 'rebuilt against the latest pod/Gradle install.',
76
+ );
77
+ }
78
+ cachedDefaultDir = await b.defaultCaptureDir();
79
+ return cachedDefaultDir;
80
+ }
81
+
82
+
83
+ /**
84
+ * Compose a default filename for a tap-photo capture, using a
85
+ * millisecond Unix timestamp for ordering. Pure helper; no I/O.
86
+ */
87
+ export function defaultPhotoFilename(): string {
88
+ return `photo-${Date.now()}.jpg`;
89
+ }
90
+
91
+
92
+ /**
93
+ * Same as `defaultPhotoFilename` but for panoramas.
94
+ */
95
+ export function defaultPanoramaFilename(): string {
96
+ return `panorama-${Date.now()}.jpg`;
97
+ }
@@ -0,0 +1,42 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ /**
3
+ * Path normalisation helpers. Internal — NOT re-exported from
4
+ * `src/index.ts`; the public surface intentionally doesn't promise
5
+ * these utilities to consumers (every host app has its own copy).
6
+ *
7
+ * Two shapes a file path can take when crossing the JS / native /
8
+ * React layers in this library:
9
+ *
10
+ * - **`file://`-prefixed URI** — what RN's `<Image source={{ uri }}>`
11
+ * (Android strict, iOS lenient) and `expo-file-system` APIs
12
+ * accept. Whenever this library emits a path to JS (via
13
+ * `onCapture`, the `IncrementalStateUpdate` event, etc.) it
14
+ * should be in this form so consumers can render it directly.
15
+ *
16
+ * - **Bare path** — what `fs`-style native APIs (`cv::imwrite`,
17
+ * `NSFileManager`, `BitmapFactory.decodeFile`) accept. These
18
+ * treat a `file://` prefix as part of the literal filename and
19
+ * fail to open it. Native bridges expect bare paths in.
20
+ *
21
+ * Both helpers are pure and idempotent. No-op on the empty string.
22
+ *
23
+ * (The Swift and Kotlin sides have their own `stripFileScheme` —
24
+ * cross-language sharing isn't worth a small helper. Keeping the
25
+ * JS copy here just centralises the rule for the TS surface.)
26
+ */
27
+
28
+ /** Add the `file://` scheme to a bare path, idempotently. */
29
+ export function toFileUri(path: string | null | undefined): string {
30
+ if (!path) return '';
31
+ if (path.startsWith('file://') || path.startsWith('content://') || path.startsWith('http')) {
32
+ return path;
33
+ }
34
+ return `file://${path}`;
35
+ }
36
+
37
+ /** Strip the `file://` scheme from a URI, idempotently. */
38
+ export function toBareFilePath(path: string | null | undefined): string {
39
+ if (!path) return '';
40
+ if (path.startsWith('file://')) return path.slice('file://'.length);
41
+ return path;
42
+ }