@succinctlabs/react-native-zcam1 0.2.5

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 (139) hide show
  1. package/README.md +61 -0
  2. package/Zcam1Sdk.podspec +157 -0
  3. package/app.plugin.js +11 -0
  4. package/cpp/generated/zcam1_c2pa_utils.cpp +4091 -0
  5. package/cpp/generated/zcam1_c2pa_utils.hpp +367 -0
  6. package/cpp/generated/zcam1_certs_utils.cpp +1799 -0
  7. package/cpp/generated/zcam1_certs_utils.hpp +72 -0
  8. package/cpp/generated/zcam1_verify_utils.cpp +1857 -0
  9. package/cpp/generated/zcam1_verify_utils.hpp +79 -0
  10. package/cpp/proving/generated/zcam1_proving_utils.cpp +3661 -0
  11. package/cpp/proving/generated/zcam1_proving_utils.hpp +275 -0
  12. package/cpp/proving/zcam1-proving.cpp +16 -0
  13. package/cpp/proving/zcam1-proving.h +15 -0
  14. package/cpp/zcam1-sdk.cpp +20 -0
  15. package/cpp/zcam1-sdk.h +15 -0
  16. package/ios/Zcam1Camera.swift +2945 -0
  17. package/ios/Zcam1CameraFilmStyle.swift +191 -0
  18. package/ios/Zcam1CameraViewManager.m +86 -0
  19. package/ios/Zcam1Capture.h +13 -0
  20. package/ios/Zcam1Capture.mm +500 -0
  21. package/ios/Zcam1DepthData.swift +417 -0
  22. package/ios/Zcam1Sdk.h +16 -0
  23. package/ios/Zcam1Sdk.mm +66 -0
  24. package/ios/proving/Zcam1Proving.h +16 -0
  25. package/ios/proving/Zcam1Proving.mm +66 -0
  26. package/lib/module/NativeZcam1Capture.js +12 -0
  27. package/lib/module/NativeZcam1Capture.js.map +1 -0
  28. package/lib/module/NativeZcam1Sdk.js +7 -0
  29. package/lib/module/NativeZcam1Sdk.js.map +1 -0
  30. package/lib/module/bindings.js +51 -0
  31. package/lib/module/bindings.js.map +1 -0
  32. package/lib/module/camera.js +522 -0
  33. package/lib/module/camera.js.map +1 -0
  34. package/lib/module/capture.js +120 -0
  35. package/lib/module/capture.js.map +1 -0
  36. package/lib/module/common.js +35 -0
  37. package/lib/module/common.js.map +1 -0
  38. package/lib/module/generated/zcam1_c2pa_utils-ffi.js +43 -0
  39. package/lib/module/generated/zcam1_c2pa_utils-ffi.js.map +1 -0
  40. package/lib/module/generated/zcam1_c2pa_utils.js +1202 -0
  41. package/lib/module/generated/zcam1_c2pa_utils.js.map +1 -0
  42. package/lib/module/generated/zcam1_certs_utils-ffi.js +43 -0
  43. package/lib/module/generated/zcam1_certs_utils-ffi.js.map +1 -0
  44. package/lib/module/generated/zcam1_certs_utils.js +399 -0
  45. package/lib/module/generated/zcam1_certs_utils.js.map +1 -0
  46. package/lib/module/generated/zcam1_proving_utils-ffi.js +43 -0
  47. package/lib/module/generated/zcam1_proving_utils-ffi.js.map +1 -0
  48. package/lib/module/generated/zcam1_proving_utils.js +515 -0
  49. package/lib/module/generated/zcam1_proving_utils.js.map +1 -0
  50. package/lib/module/generated/zcam1_verify_utils-ffi.js +43 -0
  51. package/lib/module/generated/zcam1_verify_utils-ffi.js.map +1 -0
  52. package/lib/module/generated/zcam1_verify_utils.js +252 -0
  53. package/lib/module/generated/zcam1_verify_utils.js.map +1 -0
  54. package/lib/module/index.js +31 -0
  55. package/lib/module/index.js.map +1 -0
  56. package/lib/module/package.json +1 -0
  57. package/lib/module/picker.js +222 -0
  58. package/lib/module/picker.js.map +1 -0
  59. package/lib/module/proving/NativeZcam1Proving.js +7 -0
  60. package/lib/module/proving/NativeZcam1Proving.js.map +1 -0
  61. package/lib/module/proving/bindings.js +46 -0
  62. package/lib/module/proving/bindings.js.map +1 -0
  63. package/lib/module/proving/index.js +5 -0
  64. package/lib/module/proving/index.js.map +1 -0
  65. package/lib/module/proving/prove.js +346 -0
  66. package/lib/module/proving/prove.js.map +1 -0
  67. package/lib/module/utils.js +27 -0
  68. package/lib/module/utils.js.map +1 -0
  69. package/lib/module/verify.js +82 -0
  70. package/lib/module/verify.js.map +1 -0
  71. package/lib/typescript/package.json +1 -0
  72. package/lib/typescript/src/NativeZcam1Capture.d.ts +280 -0
  73. package/lib/typescript/src/NativeZcam1Capture.d.ts.map +1 -0
  74. package/lib/typescript/src/NativeZcam1Sdk.d.ts +8 -0
  75. package/lib/typescript/src/NativeZcam1Sdk.d.ts.map +1 -0
  76. package/lib/typescript/src/bindings.d.ts +14 -0
  77. package/lib/typescript/src/bindings.d.ts.map +1 -0
  78. package/lib/typescript/src/camera.d.ts +300 -0
  79. package/lib/typescript/src/camera.d.ts.map +1 -0
  80. package/lib/typescript/src/capture.d.ts +59 -0
  81. package/lib/typescript/src/capture.d.ts.map +1 -0
  82. package/lib/typescript/src/common.d.ts +10 -0
  83. package/lib/typescript/src/common.d.ts.map +1 -0
  84. package/lib/typescript/src/generated/zcam1_c2pa_utils-ffi.d.ts +175 -0
  85. package/lib/typescript/src/generated/zcam1_c2pa_utils-ffi.d.ts.map +1 -0
  86. package/lib/typescript/src/generated/zcam1_c2pa_utils.d.ts +811 -0
  87. package/lib/typescript/src/generated/zcam1_c2pa_utils.d.ts.map +1 -0
  88. package/lib/typescript/src/generated/zcam1_certs_utils-ffi.d.ts +82 -0
  89. package/lib/typescript/src/generated/zcam1_certs_utils-ffi.d.ts.map +1 -0
  90. package/lib/typescript/src/generated/zcam1_certs_utils.d.ts +413 -0
  91. package/lib/typescript/src/generated/zcam1_certs_utils.d.ts.map +1 -0
  92. package/lib/typescript/src/generated/zcam1_proving_utils-ffi.d.ts +153 -0
  93. package/lib/typescript/src/generated/zcam1_proving_utils-ffi.d.ts.map +1 -0
  94. package/lib/typescript/src/generated/zcam1_proving_utils.d.ts +321 -0
  95. package/lib/typescript/src/generated/zcam1_proving_utils.d.ts.map +1 -0
  96. package/lib/typescript/src/generated/zcam1_verify_utils-ffi.d.ts +84 -0
  97. package/lib/typescript/src/generated/zcam1_verify_utils-ffi.d.ts.map +1 -0
  98. package/lib/typescript/src/generated/zcam1_verify_utils.d.ts +286 -0
  99. package/lib/typescript/src/generated/zcam1_verify_utils.d.ts.map +1 -0
  100. package/lib/typescript/src/index.d.ts +29 -0
  101. package/lib/typescript/src/index.d.ts.map +1 -0
  102. package/lib/typescript/src/picker.d.ts +103 -0
  103. package/lib/typescript/src/picker.d.ts.map +1 -0
  104. package/lib/typescript/src/proving/NativeZcam1Proving.d.ts +8 -0
  105. package/lib/typescript/src/proving/NativeZcam1Proving.d.ts.map +1 -0
  106. package/lib/typescript/src/proving/bindings.d.ts +8 -0
  107. package/lib/typescript/src/proving/bindings.d.ts.map +1 -0
  108. package/lib/typescript/src/proving/index.d.ts +3 -0
  109. package/lib/typescript/src/proving/index.d.ts.map +1 -0
  110. package/lib/typescript/src/proving/prove.d.ts +74 -0
  111. package/lib/typescript/src/proving/prove.d.ts.map +1 -0
  112. package/lib/typescript/src/utils.d.ts +2 -0
  113. package/lib/typescript/src/utils.d.ts.map +1 -0
  114. package/lib/typescript/src/verify.d.ts +45 -0
  115. package/lib/typescript/src/verify.d.ts.map +1 -0
  116. package/package.json +118 -0
  117. package/src/NativeZcam1Capture.ts +335 -0
  118. package/src/NativeZcam1Sdk.ts +10 -0
  119. package/src/bindings.tsx +49 -0
  120. package/src/camera.tsx +705 -0
  121. package/src/capture.tsx +165 -0
  122. package/src/common.tsx +46 -0
  123. package/src/generated/zcam1_c2pa_utils-ffi.ts +456 -0
  124. package/src/generated/zcam1_c2pa_utils.ts +1866 -0
  125. package/src/generated/zcam1_certs_utils-ffi.ts +187 -0
  126. package/src/generated/zcam1_certs_utils.ts +549 -0
  127. package/src/generated/zcam1_proving_utils-ffi.ts +374 -0
  128. package/src/generated/zcam1_proving_utils.ts +804 -0
  129. package/src/generated/zcam1_verify_utils-ffi.ts +196 -0
  130. package/src/generated/zcam1_verify_utils.ts +372 -0
  131. package/src/index.ts +73 -0
  132. package/src/picker.tsx +342 -0
  133. package/src/proving/NativeZcam1Proving.ts +10 -0
  134. package/src/proving/bindings.tsx +50 -0
  135. package/src/proving/index.ts +8 -0
  136. package/src/proving/prove.tsx +492 -0
  137. package/src/utils.ts +38 -0
  138. package/src/verify.tsx +119 -0
  139. package/turbo.json +27 -0
package/src/camera.tsx ADDED
@@ -0,0 +1,705 @@
1
+ import JailMonkey from "jail-monkey";
2
+ import React from "react";
3
+ import { requireNativeComponent, type StyleProp, type ViewStyle } from "react-native";
4
+ import { Dirs, Util } from "react-native-file-access";
5
+
6
+ import {
7
+ buildSelfSignedCertificate,
8
+ computeHash,
9
+ DepthData,
10
+ ExistingCertChain,
11
+ formatFromPath,
12
+ ManifestEditor,
13
+ type PhotoMetadataInfo,
14
+ SelfSignedCertChain,
15
+ type VideoMetadataInfo,
16
+ } from "./bindings";
17
+ import { type CaptureInfo, ZPhoto } from "./capture";
18
+ import NativeZcam1Sdk, {
19
+ type AspectRatio,
20
+ type DeviceOrientation,
21
+ type FlashMode,
22
+ type Orientation,
23
+ type StartNativeVideoRecordingResult,
24
+ type StopNativeVideoRecordingResult,
25
+ } from "./NativeZcam1Capture";
26
+ import { generateAppAttestAssertion } from "./utils";
27
+
28
+ export const CERT_KEY_TAG = "CERT_KEY_TAG";
29
+
30
+ /**
31
+ * Capture format produced by the native Swift camera.
32
+ * - "jpeg": standard compressed JPEG file
33
+ * - "dng": RAW DNG file (original), C2PA-signed JPEG copy is still produced
34
+ */
35
+ export type CaptureFormat = "jpeg" | "dng";
36
+
37
+ /**
38
+ * Camera film style presets.
39
+ * - "normal": No film style (default)
40
+ * - "mellow": Negative Film Gold style - warm, saturated, lifted shadows
41
+ * - "nostalgic": Kodak Portra 400 style - warm amber, faded, bright
42
+ * - "bw": Contrasty B&W with warm tint
43
+ */
44
+ export type CameraFilmStyle = "normal" | "mellow" | "nostalgic" | "bw";
45
+
46
+ // ─────────────────────────────────────────────────────────────────────────────
47
+ // Custom Film Style Recipe Types
48
+ // ─────────────────────────────────────────────────────────────────────────────
49
+
50
+ /** White balance adjustment configuration. */
51
+ export type WhiteBalanceConfig = {
52
+ /** Color temperature in Kelvin (e.g., 5500 for daylight, 6500 for cloudy). */
53
+ temperature: number;
54
+ /** Tint adjustment (-100 to 100, green to magenta). Defaults to 0. */
55
+ tint?: number;
56
+ };
57
+
58
+ /** Highlight and shadow adjustment configuration. */
59
+ export type HighlightShadowConfig = {
60
+ /** Highlight adjustment (0 = no change, negative = reduce, positive = boost). */
61
+ highlights: number;
62
+ /** Shadow adjustment (0 = no change, positive = lift shadows). */
63
+ shadows: number;
64
+ };
65
+
66
+ /** Monochrome (black & white) film style configuration. */
67
+ export type MonochromeConfig = {
68
+ /** Intensity of the monochrome effect (0 = none, 1 = full B&W). */
69
+ intensity: number;
70
+ /** Optional tint color for the monochrome effect. */
71
+ color?: { r: number; g: number; b: number };
72
+ };
73
+
74
+ /**
75
+ * Individual film style effect that can be combined into a recipe.
76
+ * Effects are applied in the order they appear in the recipe array.
77
+ */
78
+ export type FilmStyleEffect =
79
+ | { type: "whiteBalance"; config: WhiteBalanceConfig }
80
+ | { type: "saturation"; value: number }
81
+ | { type: "contrast"; value: number }
82
+ | { type: "brightness"; value: number }
83
+ | { type: "hue"; value: number }
84
+ | { type: "vibrance"; value: number }
85
+ | { type: "highlightShadow"; config: HighlightShadowConfig }
86
+ | { type: "monochrome"; config: MonochromeConfig };
87
+
88
+ /**
89
+ * A film style recipe is an ordered array of film style effects.
90
+ * Effects are applied sequentially to produce the final look.
91
+ */
92
+ export type FilmStyleRecipe = FilmStyleEffect[];
93
+
94
+ /**
95
+ * Default film style recipes for built-in presets.
96
+ */
97
+ const DEFAULT_FILM_STYLE_RECIPES: Record<CameraFilmStyle, FilmStyleRecipe> = {
98
+ normal: [],
99
+ // Mellow: Negative Film Gold - warm amber/magenta, saturated, lifted shadows.
100
+ mellow: [
101
+ { type: "whiteBalance", config: { temperature: 6900, tint: 40 } },
102
+ { type: "saturation", value: 1.4 },
103
+ { type: "contrast", value: 0.8 },
104
+ { type: "brightness", value: -0.1 },
105
+ { type: "highlightShadow", config: { highlights: 0, shadows: 0.4 } },
106
+ ],
107
+ // Nostalgic: Kodak Portra 400 - warm amber, faded, lifted shadows, bright.
108
+ nostalgic: [
109
+ { type: "whiteBalance", config: { temperature: 7000, tint: 0 } },
110
+ { type: "saturation", value: 1.1 },
111
+ { type: "contrast", value: 0.7 },
112
+ { type: "brightness", value: 0.15 },
113
+ { type: "highlightShadow", config: { highlights: -0.4, shadows: 0.5 } },
114
+ ],
115
+ // B&W: Contrasty black and white with subtle warm tint.
116
+ bw: [
117
+ {
118
+ type: "monochrome",
119
+ config: { intensity: 1.0, color: { r: 0.6, g: 0.55, b: 0.5 } },
120
+ },
121
+ { type: "contrast", value: 1.2 },
122
+ { type: "brightness", value: -0.1 },
123
+ ],
124
+ };
125
+
126
+ export interface ZCameraProps {
127
+ /** Which camera to use. Defaults to "back". */
128
+ position?: "front" | "back";
129
+ /** Whether the camera is actively running. Defaults to true. */
130
+ isActive?: boolean;
131
+ /** Desired capture format. Defaults to "jpeg". */
132
+ captureFormat?: CaptureFormat;
133
+ /**
134
+ * Zoom factor. For back camera devices with ultra-wide lens, 1.0 = ultra-wide (0.5x user-facing),
135
+ * 2.0 = wide-angle (1x user-facing). Use getMinZoom/getMaxZoom for valid range.
136
+ * Defaults to 2.0 (1x user-facing) for back camera, 1.0 for front camera (to avoid digital zoom).
137
+ */
138
+ zoom?: number;
139
+ /** Whether torch (flashlight) is enabled during preview. Defaults to false. */
140
+ torch?: boolean;
141
+ /** Exposure compensation in EV units. Defaults to 0. */
142
+ exposure?: number;
143
+ /** Film style preset to apply to preview and captured photos. Defaults to "normal". */
144
+ filmStyle?: CameraFilmStyle;
145
+ /**
146
+ * Override built-in film style presets with custom recipes.
147
+ * When a preset name is used with `filmStyle` prop and an override exists,
148
+ * the custom recipe is applied instead of the built-in preset.
149
+ */
150
+ filmStyleOverrides?: Partial<Record<CameraFilmStyle, FilmStyleRecipe>>;
151
+ /**
152
+ * Define additional custom film styles referenced by name.
153
+ * Use with `filmStyle` prop by casting the custom name: `filmStyle={"myStyle" as CameraFilmStyle}`.
154
+ */
155
+ customFilmStyles?: Record<string, FilmStyleRecipe>;
156
+ /**
157
+ * Enable depth data capture at session level.
158
+ * When true, depth data can be captured but zoom may be restricted on dual-camera devices.
159
+ * When false (default), full zoom range is available.
160
+ * Use isDepthSupported() and hasDepthZoomLimitations() to check device capabilities.
161
+ * @default false
162
+ */
163
+ depthEnabled?: boolean;
164
+ /**
165
+ * Callback fired when the device physical orientation changes.
166
+ * Uses accelerometer data to detect orientation even when iOS orientation lock is enabled.
167
+ * @param orientation The new physical orientation of the device.
168
+ */
169
+ onOrientationChange?: (orientation: DeviceOrientation) => void;
170
+ /** Capture information used to generate C2PA bindings for each photo. */
171
+ captureInfo: CaptureInfo;
172
+ /** Optional certificate chain used to sign the C2PA manifest. */
173
+ certChain?: SelfSignedCertChain | ExistingCertChain;
174
+ /** Optional style for the underlying native view. */
175
+ style?: StyleProp<ViewStyle>;
176
+ }
177
+
178
+ /** Options for a single capture call. */
179
+ export interface TakePhotoOptions {
180
+ format?: CaptureFormat;
181
+ /** Flash mode for this capture. Defaults to "off". */
182
+ flash?: FlashMode;
183
+ /**
184
+ * Whether to include depth data (if available) in the capture results.
185
+ * - When true: depth data is embedded into the C2PA metadata.
186
+ * - When false (default): depth data is omitted.
187
+ */
188
+ includeDepthData?: boolean;
189
+ /** Aspect ratio for the captured photo. Defaults to "4:3". */
190
+ aspectRatio?: AspectRatio;
191
+ /** Orientation for the crop. Defaults to "auto". */
192
+ orientation?: Orientation;
193
+ }
194
+
195
+ /** Props passed to the native Swift camera view. */
196
+ type NativeCameraViewProps = {
197
+ style?: StyleProp<ViewStyle>;
198
+ isActive?: boolean;
199
+ position?: "front" | "back";
200
+ captureFormat?: CaptureFormat;
201
+ zoom?: number;
202
+ torch?: boolean;
203
+ exposure?: number;
204
+ filmStyle?: CameraFilmStyle;
205
+ filmStyleOverrides?: Record<string, FilmStyleEffect[]>;
206
+ customFilmStyles?: Record<string, FilmStyleEffect[]>;
207
+ depthEnabled?: boolean;
208
+ onOrientationChange?: (event: { nativeEvent: { orientation: string } }) => void;
209
+ };
210
+
211
+ /**
212
+ * Native Swift-backed camera preview view.
213
+ * You must implement a matching iOS view manager named "Zcam1CameraView".
214
+ */
215
+ const Zcam1CameraView = requireNativeComponent<NativeCameraViewProps>("Zcam1CameraView");
216
+
217
+ /**
218
+ * React wrapper around the native Swift camera.
219
+ *
220
+ * Responsibilities:
221
+ * - Render native camera preview (AVFoundation in Swift).
222
+ * - Trigger native capture (JPEG / DNG) via the TurboModule `Zcam1Sdk`.
223
+ * - Run C2PA signing on the captured image and return a `ZPhoto`.
224
+ *
225
+ * Exposed API remains compatible with the previous VisionCamera-based
226
+ * implementation: `cameraRef.current?.takePhoto()`.
227
+ */
228
+ export class ZCamera extends React.PureComponent<ZCameraProps> {
229
+ /** Reference to the underlying native view (if needed later). */
230
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
231
+ private nativeRef = React.createRef<any>();
232
+
233
+ /** Best-effort JS-side guard; native is the source of truth. */
234
+ private recordingInProgress: boolean = false;
235
+
236
+ /** Captured for convenience/debugging; cleared after stop. */
237
+ private lastVideoStartResult: StartNativeVideoRecordingResult | null = null;
238
+
239
+ private certChainPem: string;
240
+
241
+ constructor(props: ZCameraProps) {
242
+ super(props);
243
+ let certChainPem: string;
244
+
245
+ if (props.certChain && "pem" in props.certChain) {
246
+ certChainPem = props.certChain.pem;
247
+ } else {
248
+ console.warn("[ZCAM1] Using a self signed certificate");
249
+
250
+ certChainPem = buildSelfSignedCertificate(
251
+ props.captureInfo.contentPublicKey,
252
+ props.certChain,
253
+ );
254
+ }
255
+
256
+ this.certChainPem = certChainPem;
257
+ }
258
+
259
+ /**
260
+ * Resolve the current film style info for embedding in capture metadata.
261
+ * Returns null for "normal" with no overrides (no filter applied).
262
+ */
263
+ private resolveFilmStyleInfo():
264
+ | {
265
+ name: string;
266
+ source: string;
267
+ recipe: string;
268
+ }
269
+ | undefined {
270
+ const { filmStyle = "normal", filmStyleOverrides, customFilmStyles } = this.props;
271
+
272
+ // Determine the source of the active film style.
273
+ const isOverride = filmStyleOverrides?.[filmStyle] !== undefined;
274
+ const isCustom = customFilmStyles?.[filmStyle] !== undefined;
275
+ const source = isOverride ? "override" : isCustom ? "custom" : "builtin";
276
+
277
+ // Resolve the actual recipe that was applied.
278
+ const recipe =
279
+ filmStyleOverrides?.[filmStyle] ??
280
+ customFilmStyles?.[filmStyle] ??
281
+ DEFAULT_FILM_STYLE_RECIPES[filmStyle] ??
282
+ [];
283
+
284
+ // Skip embedding for unmodified "normal" (no effects applied).
285
+ if (filmStyle === "normal" && source === "builtin") {
286
+ return undefined;
287
+ }
288
+
289
+ return {
290
+ name: filmStyle,
291
+ source,
292
+ recipe: JSON.stringify(recipe),
293
+ };
294
+ }
295
+
296
+ /**
297
+ * Get the minimum supported zoom factor.
298
+ * For virtual devices with ultra-wide, this is 1.0 (corresponds to 0.5x user-facing).
299
+ */
300
+ async getMinZoom(): Promise<number> {
301
+ return NativeZcam1Sdk.getMinZoom();
302
+ }
303
+
304
+ /**
305
+ * Get the maximum supported zoom factor (capped at 15x for UX).
306
+ */
307
+ async getMaxZoom(): Promise<number> {
308
+ return NativeZcam1Sdk.getMaxZoom();
309
+ }
310
+
311
+ /**
312
+ * Get the zoom factors where the device switches between physical lenses.
313
+ * Returns empty array for single-camera devices.
314
+ * For triple camera: typically [2.0, 6.0] meaning:
315
+ * - Below 2.0: ultra-wide lens (0.5x-1x user-facing)
316
+ * - At 2.0: switches FROM ultra-wide TO wide lens (1x user-facing)
317
+ * - At 6.0: switches FROM wide TO telephoto lens (3x user-facing)
318
+ */
319
+ async getSwitchOverZoomFactors(): Promise<number[]> {
320
+ return NativeZcam1Sdk.getSwitchOverZoomFactors();
321
+ }
322
+
323
+ /**
324
+ * Check if the current device has an ultra-wide camera.
325
+ * This is true for builtInTripleCamera and builtInDualWideCamera (iPhone 11+, Pro models).
326
+ * This is false for builtInDualCamera (Wide + Telephoto, e.g., iPhone X/XS) and single-lens devices.
327
+ *
328
+ * Use this to correctly interpret zoom factors:
329
+ * - If hasUltraWide: minZoom (1.0) = 0.5x user-facing, switchOverFactors[0] (2.0) = 1x user-facing
330
+ * - If !hasUltraWide: minZoom (1.0) = 1x user-facing, switchOverFactors[0] (2.0) = 2x user-facing (telephoto)
331
+ */
332
+ async hasUltraWideCamera(): Promise<boolean> {
333
+ return NativeZcam1Sdk.hasUltraWideCamera();
334
+ }
335
+
336
+ /**
337
+ * Get the supported exposure compensation range in EV units.
338
+ * Returns the device's min and max exposure target bias values.
339
+ * Use this to configure slider bounds for exposure UI controls.
340
+ */
341
+ async getExposureRange(): Promise<{ min: number; max: number }> {
342
+ return NativeZcam1Sdk.getExposureRange();
343
+ }
344
+
345
+ /**
346
+ * Reset exposure compensation to neutral (0 EV).
347
+ * Convenience method equivalent to setting the exposure prop to 0.
348
+ */
349
+ resetExposure(): void {
350
+ NativeZcam1Sdk.resetExposure();
351
+ }
352
+
353
+ /**
354
+ * Focus at a point in the preview. Also adjusts exposure point if supported.
355
+ * @param x Normalized x coordinate (0-1, left to right)
356
+ * @param y Normalized y coordinate (0-1, top to bottom)
357
+ */
358
+ focusAtPoint(x: number, y: number): void {
359
+ NativeZcam1Sdk.focusAtPoint(x, y);
360
+ }
361
+
362
+ /**
363
+ * Set zoom with smooth animation. Recommended for pinch-to-zoom gestures.
364
+ * Uses native AVFoundation ramp for smooth transitions across lens switchover boundaries.
365
+ * This method bypasses React re-renders for lowest latency during continuous gestures.
366
+ * @param factor Device zoom factor (use getMinZoom/getMaxZoom for valid range)
367
+ */
368
+ setZoomAnimated(factor: number): void {
369
+ NativeZcam1Sdk.setZoomAnimated(factor);
370
+ }
371
+
372
+ /**
373
+ * Get diagnostic info about the current camera device for debugging.
374
+ * Returns device type, supported zoom range, switching behavior, and more.
375
+ * Useful for debugging zoom issues on different device configurations.
376
+ */
377
+ async getDeviceDiagnostics(): Promise<{
378
+ deviceType: string;
379
+ minZoom: number;
380
+ maxZoom: number;
381
+ currentZoom: number;
382
+ switchOverFactors: number[];
383
+ switchingBehavior: number;
384
+ isVirtualDevice: boolean;
385
+ currentExposureBias: number;
386
+ minExposureBias: number;
387
+ maxExposureBias: number;
388
+ currentISO: number;
389
+ exposureDuration: number;
390
+ }> {
391
+ return NativeZcam1Sdk.getDeviceDiagnostics();
392
+ }
393
+
394
+ /**
395
+ * Check if the current camera device supports depth data capture.
396
+ * Returns true for dual/triple rear cameras and TrueDepth front camera.
397
+ * Returns false for single rear cameras (iPhone SE, 16e, Air).
398
+ */
399
+ async isDepthSupported(): Promise<boolean> {
400
+ return NativeZcam1Sdk.isDepthSupported();
401
+ }
402
+
403
+ /**
404
+ * Check if enabling depth would restrict zoom on this device.
405
+ * Returns true if zoom is limited to discrete levels (min == max in all ranges).
406
+ * This typically happens on dual-camera devices (iPhone 12-16 base).
407
+ * Returns false for triple-camera devices (Pro) and TrueDepth front cameras.
408
+ */
409
+ async hasDepthZoomLimitations(): Promise<boolean> {
410
+ return NativeZcam1Sdk.hasDepthZoomLimitations();
411
+ }
412
+
413
+ /**
414
+ * Get zoom ranges supported when depth data delivery is enabled.
415
+ * Returns array of [min, max] pairs. If min == max, it's a discrete level.
416
+ * Empty array means no depth support or no zoom restrictions.
417
+ *
418
+ * Example for dual-camera iPhone: [[2.0, 2.0], [4.0, 4.0]] (discrete 1x and 2x only)
419
+ * Example for triple-camera iPhone: [[1.0, 6.0]] (continuous zoom supported)
420
+ */
421
+ async getDepthSupportedZoomRanges(): Promise<number[][]> {
422
+ return NativeZcam1Sdk.getDepthSupportedZoomRanges();
423
+ }
424
+
425
+ /**
426
+ * Start recording a native video to a temporary `.mov` file.
427
+ *
428
+ * Promise resolves once the native recorder reports it has started.
429
+ *
430
+ * @param position Which camera to record from.
431
+ * @param options Optional recording configuration.
432
+ * @param options.maxDurationSeconds Maximum recording duration in seconds.
433
+ * The native layer will automatically stop the recording when this limit is
434
+ * reached. Pass 0 or omit for unlimited recording.
435
+ */
436
+ async startVideoRecording(
437
+ position: "front" | "back" = this.props.position || "back",
438
+ options?: { maxDurationSeconds?: number },
439
+ ): Promise<StartNativeVideoRecordingResult> {
440
+ if (this.recordingInProgress) {
441
+ throw new Error("Video recording is already in progress. Call stopVideoRecording() first.");
442
+ }
443
+
444
+ this.recordingInProgress = true;
445
+ this.lastVideoStartResult = null;
446
+
447
+ try {
448
+ const result = await NativeZcam1Sdk.startNativeVideoRecording(
449
+ position,
450
+ options?.maxDurationSeconds ?? 0,
451
+ );
452
+ this.lastVideoStartResult = result;
453
+ return result;
454
+ } catch (e) {
455
+ // Roll back local state if native failed to start.
456
+ this.recordingInProgress = false;
457
+ this.lastVideoStartResult = null;
458
+ throw e;
459
+ }
460
+ }
461
+
462
+ /**
463
+ * Stop the current native video recording and return the finalized file path + metadata.
464
+ */
465
+ async stopVideoRecording(): Promise<StopNativeVideoRecordingResult> {
466
+ if (!this.recordingInProgress) {
467
+ throw new Error("No video recording is in progress. Call startVideoRecording() first.");
468
+ }
469
+
470
+ try {
471
+ const result = await NativeZcam1Sdk.stopNativeVideoRecording();
472
+ const when = new Date().toISOString().replace("T", " ").split(".")[0]!;
473
+ const isJailBroken = JailMonkey.isJailBroken();
474
+ const isLocationSpoofingAvailable = JailMonkey.canMockLocation();
475
+
476
+ result.filePath = await embedBindings(
477
+ result.filePath,
478
+ when,
479
+ {
480
+ deviceMake: result.deviceMake,
481
+ deviceModel: result.deviceModel,
482
+ softwareVersion: result.softwareVersion,
483
+ format: result.format,
484
+ hasAudio: result.hasAudio,
485
+ durationSeconds: result.durationSeconds,
486
+ fileSizeBytes: result.fileSizeBytes,
487
+ width: result.width,
488
+ height: result.height,
489
+ rotationDegrees: result.rotationDegrees,
490
+ frameRate: result.frameRate,
491
+ videoCodec: result.videoCodec,
492
+ audioCodec: result.audioCodec,
493
+ audioSampleRate: result.audioSampleRate,
494
+ audioChannels: result.audioChannels,
495
+ authenticityData: {
496
+ isJailBroken,
497
+ isLocationSpoofingAvailable,
498
+ },
499
+ filmStyle: this.resolveFilmStyleInfo(),
500
+ },
501
+ this.props.captureInfo,
502
+ this.certChainPem,
503
+ );
504
+
505
+ return result;
506
+ } finally {
507
+ // Always clear local state regardless of native outcome.
508
+ this.recordingInProgress = false;
509
+ this.lastVideoStartResult = null;
510
+ }
511
+ }
512
+
513
+ /**
514
+ * Capture a photo using the native Swift camera and return a signed `ZPhoto`.
515
+ *
516
+ * The native side is expected to expose a `capturePhoto` method on the
517
+ * `Zcam1Sdk` TurboModule with signature:
518
+ *
519
+ * capturePhoto(options: {
520
+ * position?: "front" | "back";
521
+ * format?: "jpeg" | "dng";
522
+ * }): Promise<{ path: string; metadata?: any }>
523
+ */
524
+ async takePhoto(options: TakePhotoOptions = {}): Promise<ZPhoto> {
525
+ const format: CaptureFormat = options.format ?? this.props.captureFormat ?? "jpeg";
526
+ const flash: FlashMode = options.flash ?? "off";
527
+ const includeDepthData: boolean = options.includeDepthData ?? false;
528
+ const aspectRatio: AspectRatio = options.aspectRatio ?? "4:3";
529
+ const orientation: Orientation = options.orientation ?? "auto";
530
+
531
+ console.log("[ZCamera] takePhoto: calling native with includeDepthData =", includeDepthData);
532
+
533
+ const result = await NativeZcam1Sdk.takeNativePhoto(
534
+ format,
535
+ this.props.position || "back",
536
+ flash,
537
+ includeDepthData,
538
+ aspectRatio,
539
+ orientation,
540
+ false, // skipPostProcessing - default to false for normal operation
541
+ );
542
+
543
+ console.log("[ZCamera] takePhoto: native result:", {
544
+ filePath: result?.filePath ? "present" : "missing",
545
+ format: result?.format,
546
+ hasMetadata: !!result?.metadata,
547
+ hasDepthData: !!result?.depthData,
548
+ depthDataKeys: result?.depthData ? Object.keys(result.depthData) : null,
549
+ });
550
+
551
+ if (!result || !result.filePath) {
552
+ throw new Error("Native camera capture did not return a valid file path.");
553
+ }
554
+
555
+ const originalPath = result.filePath;
556
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
557
+ const metadata = (result.metadata as any) ?? {};
558
+
559
+ const exif = metadata["{Exif}"] ?? {};
560
+ const tiff = metadata["{TIFF}"] ?? {};
561
+
562
+ const when = tiff.DateTime || new Date().toISOString().replace("T", " ").split(".")[0];
563
+ const deviceMake = tiff.Make || "Apple";
564
+ const deviceModel = tiff.Model || "Unknown";
565
+ const softwareVersion = tiff.Software || "Unknown";
566
+ const isJailBroken = JailMonkey.isJailBroken();
567
+ const isLocationSpoofingAvailable = JailMonkey.canMockLocation();
568
+
569
+ const destinationPath = await embedBindings(
570
+ originalPath,
571
+ when,
572
+ {
573
+ deviceMake: deviceMake,
574
+ deviceModel: deviceModel,
575
+ softwareVersion: softwareVersion,
576
+ xResolution: exif.PixelXDimension,
577
+ yResolution: exif.PixelYDimension,
578
+ orientation: metadata.Orientation,
579
+ iso: exif.ISOSpeedRatings?.toString(),
580
+ exposureTime: exif.ExposureTime,
581
+ depthOfField: exif.FNumber,
582
+ focalLength: exif.FocalLength,
583
+ authenticityData: {
584
+ isJailBroken,
585
+ isLocationSpoofingAvailable,
586
+ },
587
+ depthData: result.depthData as DepthData | undefined,
588
+ filmStyle: this.resolveFilmStyleInfo(),
589
+ },
590
+ this.props.captureInfo,
591
+ this.certChainPem,
592
+ );
593
+
594
+ return new ZPhoto(originalPath, destinationPath);
595
+ }
596
+
597
+ /* Render the native Swift camera preview view. */
598
+ public render(): React.ReactNode {
599
+ const {
600
+ isActive = true,
601
+ position = "back",
602
+ captureFormat,
603
+ // Default to 2.0 (1x user-facing) for back camera on devices with ultra-wide.
604
+ // On single-camera devices, 2.0 will be clamped to device's max (usually 1.0).
605
+ // For front camera, default to 1.0 to avoid digital zoom (front cameras are single-lens).
606
+ zoom = position === "front" ? 1.0 : 2.0,
607
+ torch = false,
608
+ exposure = 0,
609
+ filmStyle = "normal",
610
+ filmStyleOverrides,
611
+ customFilmStyles,
612
+ depthEnabled = false,
613
+ onOrientationChange,
614
+ style,
615
+ } = this.props;
616
+
617
+ // Merge default recipes with user overrides (user overrides take precedence).
618
+ const mergedFilmStyleOverrides = {
619
+ ...DEFAULT_FILM_STYLE_RECIPES,
620
+ ...filmStyleOverrides,
621
+ };
622
+
623
+ return (
624
+ <Zcam1CameraView
625
+ ref={this.nativeRef}
626
+ style={[{ flex: 1 }, style]}
627
+ isActive={isActive}
628
+ position={position}
629
+ captureFormat={captureFormat}
630
+ zoom={zoom}
631
+ torch={torch}
632
+ exposure={exposure}
633
+ filmStyle={filmStyle}
634
+ filmStyleOverrides={mergedFilmStyleOverrides}
635
+ customFilmStyles={customFilmStyles}
636
+ depthEnabled={depthEnabled}
637
+ onOrientationChange={
638
+ onOrientationChange
639
+ ? (event) => onOrientationChange(event.nativeEvent.orientation as DeviceOrientation)
640
+ : undefined
641
+ }
642
+ />
643
+ );
644
+ }
645
+ }
646
+
647
+ /**
648
+ * Embeds C2PA bindings and capture metadata into a photo, producing a new signed file.
649
+ */
650
+ async function embedBindings(
651
+ originalPath: string,
652
+ when: string,
653
+ metadata: PhotoMetadataInfo | VideoMetadataInfo,
654
+ captureInfo: CaptureInfo,
655
+ certChainPem: string,
656
+ ): Promise<string> {
657
+ originalPath = originalPath.replace("file://", "");
658
+ const dataHash = computeHash(originalPath);
659
+ const format = formatFromPath(originalPath);
660
+ const ext = Util.extname(originalPath);
661
+
662
+ if (format === undefined) {
663
+ throw new Error(`Unsupported file format: ${originalPath}`);
664
+ }
665
+
666
+ const destinationPath =
667
+ Dirs.CacheDir + `/zcam-${Date.now()}-${Math.random().toString(36).slice(2, 10)}.${ext}`;
668
+
669
+ const manifestEditor = new ManifestEditor(
670
+ originalPath,
671
+ captureInfo.contentKeyId.buffer as ArrayBuffer,
672
+ certChainPem,
673
+ );
674
+
675
+ // Add the "capture" action to the manifest.
676
+ let normalizedMetadata = undefined;
677
+ if (format.indexOf("video") < 0) {
678
+ normalizedMetadata = manifestEditor.addPhotoMetadataAction(metadata as PhotoMetadataInfo, when);
679
+ } else {
680
+ console.log("Metadata", metadata);
681
+ normalizedMetadata = manifestEditor.addVideoMetadataAction(metadata as VideoMetadataInfo, when);
682
+ }
683
+
684
+ const assertion = await generateAppAttestAssertion(
685
+ dataHash,
686
+ normalizedMetadata,
687
+ captureInfo.deviceKeyId,
688
+ );
689
+
690
+ // Add an assertion containing all data needed to later generate a proof
691
+ manifestEditor.addAssertion(
692
+ "succinct.bindings",
693
+ JSON.stringify({
694
+ app_id: captureInfo.appId,
695
+ device_key_id: captureInfo.deviceKeyId,
696
+ attestation: captureInfo.attestation,
697
+ assertion,
698
+ }),
699
+ );
700
+
701
+ // Sign the captured image with C2PA, producing a new signed file.
702
+ await manifestEditor.embedManifestToFile(destinationPath, format);
703
+
704
+ return destinationPath;
705
+ }