react-native-image-stitcher 0.1.0

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 (151) hide show
  1. package/CHANGELOG.md +96 -0
  2. package/LICENSE +201 -0
  3. package/NOTICE +21 -0
  4. package/README.md +189 -0
  5. package/RNImageStitcher.podspec +76 -0
  6. package/android/build.gradle +224 -0
  7. package/android/src/main/AndroidManifest.xml +3 -0
  8. package/android/src/main/cpp/CMakeLists.txt +124 -0
  9. package/android/src/main/cpp/image_stitcher_jni.cpp +145 -0
  10. package/android/src/main/cpp/keyframe_gate_jni.cpp +204 -0
  11. package/android/src/main/java/io/imagestitcher/rn/BatchStitcher.kt +426 -0
  12. package/android/src/main/java/io/imagestitcher/rn/IncrementalFirstwinsEngine.kt +960 -0
  13. package/android/src/main/java/io/imagestitcher/rn/IncrementalStitcher.kt +2371 -0
  14. package/android/src/main/java/io/imagestitcher/rn/KeyframeGate.kt +256 -0
  15. package/android/src/main/java/io/imagestitcher/rn/QualityChecker.kt +167 -0
  16. package/android/src/main/java/io/imagestitcher/rn/RNImageStitcherPackage.kt +39 -0
  17. package/android/src/main/java/io/imagestitcher/rn/RNSARCameraView.kt +558 -0
  18. package/android/src/main/java/io/imagestitcher/rn/RNSARCameraViewManager.kt +35 -0
  19. package/android/src/main/java/io/imagestitcher/rn/RNSARSession.kt +784 -0
  20. package/android/src/main/java/io/imagestitcher/rn/ar/BackgroundRenderer.kt +176 -0
  21. package/android/src/main/java/io/imagestitcher/rn/ar/ShaderUtil.kt +67 -0
  22. package/android/src/main/java/io/imagestitcher/rn/ar/YuvImageConverter.kt +201 -0
  23. package/cpp/ar_frame_pose.h +63 -0
  24. package/cpp/keyframe_gate.cpp +927 -0
  25. package/cpp/keyframe_gate.hpp +240 -0
  26. package/cpp/stitcher.cpp +2207 -0
  27. package/cpp/stitcher.hpp +275 -0
  28. package/dist/ar/useARSession.d.ts +102 -0
  29. package/dist/ar/useARSession.js +133 -0
  30. package/dist/camera/ARCameraView.d.ts +93 -0
  31. package/dist/camera/ARCameraView.js +170 -0
  32. package/dist/camera/Camera.d.ts +134 -0
  33. package/dist/camera/Camera.js +688 -0
  34. package/dist/camera/CameraShutter.d.ts +80 -0
  35. package/dist/camera/CameraShutter.js +237 -0
  36. package/dist/camera/CameraView.d.ts +65 -0
  37. package/dist/camera/CameraView.js +117 -0
  38. package/dist/camera/CaptureControlsBar.d.ts +87 -0
  39. package/dist/camera/CaptureControlsBar.js +82 -0
  40. package/dist/camera/CaptureHeader.d.ts +62 -0
  41. package/dist/camera/CaptureHeader.js +81 -0
  42. package/dist/camera/CapturePreview.d.ts +70 -0
  43. package/dist/camera/CapturePreview.js +188 -0
  44. package/dist/camera/CaptureStatusOverlay.d.ts +75 -0
  45. package/dist/camera/CaptureStatusOverlay.js +326 -0
  46. package/dist/camera/CaptureThumbnailStrip.d.ts +87 -0
  47. package/dist/camera/CaptureThumbnailStrip.js +177 -0
  48. package/dist/camera/IncrementalPanGuide.d.ts +83 -0
  49. package/dist/camera/IncrementalPanGuide.js +267 -0
  50. package/dist/camera/PanoramaBandOverlay.d.ts +107 -0
  51. package/dist/camera/PanoramaBandOverlay.js +399 -0
  52. package/dist/camera/PanoramaConfirmModal.d.ts +57 -0
  53. package/dist/camera/PanoramaConfirmModal.js +128 -0
  54. package/dist/camera/PanoramaGuidance.d.ts +79 -0
  55. package/dist/camera/PanoramaGuidance.js +246 -0
  56. package/dist/camera/PanoramaSettingsModal.d.ts +311 -0
  57. package/dist/camera/PanoramaSettingsModal.js +611 -0
  58. package/dist/camera/ViewportCropOverlay.d.ts +46 -0
  59. package/dist/camera/ViewportCropOverlay.js +67 -0
  60. package/dist/camera/useCapture.d.ts +111 -0
  61. package/dist/camera/useCapture.js +160 -0
  62. package/dist/camera/useDeviceOrientation.d.ts +48 -0
  63. package/dist/camera/useDeviceOrientation.js +131 -0
  64. package/dist/camera/useVideoCapture.d.ts +79 -0
  65. package/dist/camera/useVideoCapture.js +151 -0
  66. package/dist/index.d.ts +26 -0
  67. package/dist/index.js +39 -0
  68. package/dist/quality/normaliseOrientation.d.ts +36 -0
  69. package/dist/quality/normaliseOrientation.js +62 -0
  70. package/dist/quality/runQualityCheck.d.ts +41 -0
  71. package/dist/quality/runQualityCheck.js +98 -0
  72. package/dist/sensors/useIMUTranslationGate.d.ts +70 -0
  73. package/dist/sensors/useIMUTranslationGate.js +235 -0
  74. package/dist/stitching/IncrementalStitcherView.d.ts +41 -0
  75. package/dist/stitching/IncrementalStitcherView.js +157 -0
  76. package/dist/stitching/incremental.d.ts +930 -0
  77. package/dist/stitching/incremental.js +133 -0
  78. package/dist/stitching/stitchFrames.d.ts +55 -0
  79. package/dist/stitching/stitchFrames.js +56 -0
  80. package/dist/stitching/stitchVideo.d.ts +119 -0
  81. package/dist/stitching/stitchVideo.js +57 -0
  82. package/dist/stitching/useIncrementalJSDriver.d.ts +74 -0
  83. package/dist/stitching/useIncrementalJSDriver.js +199 -0
  84. package/dist/stitching/useIncrementalStitcher.d.ts +58 -0
  85. package/dist/stitching/useIncrementalStitcher.js +172 -0
  86. package/dist/types.d.ts +58 -0
  87. package/dist/types.js +15 -0
  88. package/ios/Package.swift +72 -0
  89. package/ios/Sources/RNImageStitcher/ARCameraViewManager.m +33 -0
  90. package/ios/Sources/RNImageStitcher/ARCameraViewManager.swift +40 -0
  91. package/ios/Sources/RNImageStitcher/ARSessionBridge.m +55 -0
  92. package/ios/Sources/RNImageStitcher/ARSessionBridge.swift +149 -0
  93. package/ios/Sources/RNImageStitcher/IncrementalStitcher.swift +2727 -0
  94. package/ios/Sources/RNImageStitcher/IncrementalStitcherBridge.m +85 -0
  95. package/ios/Sources/RNImageStitcher/IncrementalStitcherBridge.swift +625 -0
  96. package/ios/Sources/RNImageStitcher/KeyframeGate.swift +328 -0
  97. package/ios/Sources/RNImageStitcher/KeyframeGateBridge.h +141 -0
  98. package/ios/Sources/RNImageStitcher/KeyframeGateBridge.mm +278 -0
  99. package/ios/Sources/RNImageStitcher/OpenCVIncrementalStitcher.h +473 -0
  100. package/ios/Sources/RNImageStitcher/OpenCVIncrementalStitcher.mm +1326 -0
  101. package/ios/Sources/RNImageStitcher/OpenCVKeyframeCollector.h +97 -0
  102. package/ios/Sources/RNImageStitcher/OpenCVKeyframeCollector.mm +296 -0
  103. package/ios/Sources/RNImageStitcher/OpenCVSlitScanStitcher.h +103 -0
  104. package/ios/Sources/RNImageStitcher/OpenCVSlitScanStitcher.mm +3285 -0
  105. package/ios/Sources/RNImageStitcher/OpenCVStitcher.h +238 -0
  106. package/ios/Sources/RNImageStitcher/OpenCVStitcher.mm +1880 -0
  107. package/ios/Sources/RNImageStitcher/QualityChecker.swift +252 -0
  108. package/ios/Sources/RNImageStitcher/QualityCheckerBridge.m +26 -0
  109. package/ios/Sources/RNImageStitcher/QualityCheckerBridge.swift +72 -0
  110. package/ios/Sources/RNImageStitcher/RNSARCameraView.swift +114 -0
  111. package/ios/Sources/RNImageStitcher/RNSARSession.swift +1111 -0
  112. package/ios/Sources/RNImageStitcher/Stitcher.swift +243 -0
  113. package/ios/Sources/RNImageStitcher/StitcherBridge.m +28 -0
  114. package/ios/Sources/RNImageStitcher/StitcherBridge.swift +246 -0
  115. package/package.json +73 -0
  116. package/react-native.config.js +34 -0
  117. package/scripts/opencv-version.txt +1 -0
  118. package/scripts/postinstall-fetch-binaries.js +286 -0
  119. package/src/ar/useARSession.ts +210 -0
  120. package/src/camera/.gitkeep +0 -0
  121. package/src/camera/ARCameraView.tsx +256 -0
  122. package/src/camera/Camera.tsx +1053 -0
  123. package/src/camera/CameraShutter.tsx +292 -0
  124. package/src/camera/CameraView.tsx +157 -0
  125. package/src/camera/CaptureControlsBar.tsx +204 -0
  126. package/src/camera/CaptureHeader.tsx +184 -0
  127. package/src/camera/CapturePreview.tsx +318 -0
  128. package/src/camera/CaptureStatusOverlay.tsx +391 -0
  129. package/src/camera/CaptureThumbnailStrip.tsx +277 -0
  130. package/src/camera/IncrementalPanGuide.tsx +328 -0
  131. package/src/camera/PanoramaBandOverlay.tsx +498 -0
  132. package/src/camera/PanoramaConfirmModal.tsx +206 -0
  133. package/src/camera/PanoramaGuidance.tsx +327 -0
  134. package/src/camera/PanoramaSettingsModal.tsx +1357 -0
  135. package/src/camera/ViewportCropOverlay.tsx +81 -0
  136. package/src/camera/useCapture.ts +279 -0
  137. package/src/camera/useDeviceOrientation.ts +140 -0
  138. package/src/camera/useVideoCapture.ts +236 -0
  139. package/src/index.ts +53 -0
  140. package/src/quality/.gitkeep +0 -0
  141. package/src/quality/normaliseOrientation.ts +79 -0
  142. package/src/quality/runQualityCheck.ts +131 -0
  143. package/src/sensors/useIMUTranslationGate.ts +347 -0
  144. package/src/stitching/.gitkeep +0 -0
  145. package/src/stitching/IncrementalStitcherView.tsx +198 -0
  146. package/src/stitching/incremental.ts +1021 -0
  147. package/src/stitching/stitchFrames.ts +88 -0
  148. package/src/stitching/stitchVideo.ts +153 -0
  149. package/src/stitching/useIncrementalJSDriver.ts +273 -0
  150. package/src/stitching/useIncrementalStitcher.ts +252 -0
  151. package/src/types.ts +78 -0
@@ -0,0 +1,238 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ //
3
+ // OpenCVStitcher.h
4
+ //
5
+ // Objective-C interface to the OpenCV stitcher. All C++ types
6
+ // (`cv::Stitcher`, `cv::Mat`, `std::vector`) are confined to the
7
+ // implementation file (`.mm`) so this header can be imported from
8
+ // pure Swift without dragging in the C++ standard library.
9
+ //
10
+ // Why the layered design (ObjC interface ↔ ObjC++ impl ↔ C++ lib)?
11
+ // The Swift importer does not understand C++. Without this layer
12
+ // we'd have to write a much heavier @objc shim using opaque void*
13
+ // pointers. Letting Objective-C own the boundary types gives us
14
+ // automatic memory management for NSString/NSError/NSDictionary
15
+ // and zero copy on the boundary — the .mm only does the C++→C++
16
+ // work, never marshalling.
17
+ //
18
+
19
+ #import <Foundation/Foundation.h>
20
+
21
+ NS_ASSUME_NONNULL_BEGIN
22
+
23
+ /// NSError domain raised by OpenCVStitcher errors. Codes match the
24
+ /// `cv::Stitcher::Status` enum values so callers can branch on
25
+ /// "needs more images" vs. "homography failed".
26
+ extern NSString *const RNImageStitcherErrorDomain;
27
+
28
+ /// Result of a successful stitch — pixel dimensions of the panorama
29
+ /// plus the path it was written to (host app passed it in).
30
+ @interface RNStitchResult : NSObject
31
+ @property (nonatomic, copy, readonly) NSString *outputPath;
32
+ @property (nonatomic, assign, readonly) NSInteger width;
33
+ @property (nonatomic, assign, readonly) NSInteger height;
34
+ @property (nonatomic, assign, readonly) double durationMs;
35
+ /// 2026-05-16 (Issue 5) — C+D progressive-confidence retry telemetry
36
+ /// sourced from `retailens::StitchResult`. Surface in the JS finalize
37
+ /// dict so the host can render a debug toast on retry.
38
+ ///
39
+ /// framesRequested: number of keyframes handed to the stitcher
40
+ /// framesIncluded: number retained after leaveBiggestComponent
41
+ /// finalConfidenceThresh: threshold the successful attempt used
42
+ /// (1.0 / 0.5 / 0.3); -1.0 when the
43
+ /// retry path didn't run (rare error paths)
44
+ @property (nonatomic, assign, readonly) NSInteger framesRequested;
45
+ @property (nonatomic, assign, readonly) NSInteger framesIncluded;
46
+ @property (nonatomic, assign, readonly) double finalConfidenceThresh;
47
+ - (instancetype)initWithOutputPath:(NSString *)outputPath
48
+ width:(NSInteger)width
49
+ height:(NSInteger)height
50
+ durationMs:(double)durationMs
51
+ framesRequested:(NSInteger)framesRequested
52
+ framesIncluded:(NSInteger)framesIncluded
53
+ finalConfidenceThresh:(double)finalConfidenceThresh NS_DESIGNATED_INITIALIZER;
54
+ /// Convenience initializer for paths that don't carry C+D retry
55
+ /// telemetry (e.g. stitchVideoAtPath / stitchKeyframePaths). Sets
56
+ /// the telemetry fields to sentinel values (-1) so JS callers can
57
+ /// detect "no retry data available" cleanly.
58
+ - (instancetype)initWithOutputPath:(NSString *)outputPath
59
+ width:(NSInteger)width
60
+ height:(NSInteger)height
61
+ durationMs:(double)durationMs;
62
+ - (instancetype)init NS_UNAVAILABLE;
63
+ @end
64
+
65
+
66
+ @interface OpenCVStitcher : NSObject
67
+
68
+ /// Stitch the images at `framePaths` into a single panoramic JPEG
69
+ /// at `outputPath`.
70
+ ///
71
+ /// `quality`: JPEG quality [0..100]. Caller-clamped; 0 / 101 will
72
+ /// be coerced into range by the impl.
73
+ ///
74
+ /// On success returns the result object; on failure populates
75
+ /// `error` (NSError, RNImageStitcherErrorDomain) and returns nil.
76
+ /// `warperType`: one of @"plane" / @"cylindrical" / @"spherical".
77
+ /// Pass nil/empty for the default (@"plane"). Different
78
+ /// projections suit different gestures — see the field A/B
79
+ /// testing settings UI for guidance.
80
+ /// `blenderType`: one of @"multiband" / @"feather". Pass nil for
81
+ /// the default (@"multiband").
82
+ /// `seamFinderType`: one of @"graphcut" / @"skip". Pass nil for
83
+ /// the default (@"graphcut").
84
+ /// - "graphcut" runs cv::detail::GraphCutSeamFinder over all
85
+ /// warped frames before blending — produces clean seams,
86
+ /// pairs well with MultiBandBlender, but holds all warped
87
+ /// frames in memory simultaneously (higher peak).
88
+ /// - "skip" streams warp+feed in a single pass and never holds
89
+ /// more than one warped frame. Lower peak memory. Use on
90
+ /// low-RAM devices or for fastest path with FeatherBlender.
91
+ /// AR-STITCHING-TWO-MODES — see memory/ar-stitching-two-modes.md
92
+ ///
93
+ /// - `captureOrientation` ("portrait" | "portrait-upside-down" |
94
+ /// "landscape-left" | "landscape-right"): physical phone hold at
95
+ /// capture start, sourced from the JS-side accelerometer hook.
96
+ /// Drives the OUTPUT panorama's bake-rotation per the two
97
+ /// supported capture modes:
98
+ /// portrait → no bake-rotation
99
+ /// portrait-upside-down → bake ROTATE_180
100
+ /// landscape-left → bake ROTATE_90_COUNTERCLOCKWISE
101
+ /// landscape-right → bake ROTATE_90_CLOCKWISE
102
+ /// Output JPEG is always written with EXIF=1 (no metadata
103
+ /// rotation) since the rotation is baked into the pixels.
104
+ ///
105
+ /// - **Maximum-inscribed-rectangle crop** instead of bounding-
106
+ /// rectangle. cv::Stitcher's output has irregular black corners
107
+ /// where the projection didn't fill; bbox crop still included
108
+ /// them. With `useInscribedRectCrop:YES` we find the largest
109
+ /// axis-aligned rectangle entirely inside the non-zero region
110
+ /// and crop to that — clean output with no black corners.
111
+ + (nullable RNStitchResult *)stitchFramePaths:(NSArray<NSString *> *)framePaths
112
+ outputPath:(NSString *)outputPath
113
+ jpegQuality:(NSInteger)quality
114
+ warperType:(nullable NSString *)warperType
115
+ blenderType:(nullable NSString *)blenderType
116
+ seamFinderType:(nullable NSString *)seamFinderType
117
+ captureOrientation:(nullable NSString *)captureOrientation
118
+ useInscribedRectCrop:(BOOL)useInscribedRectCrop
119
+ error:(NSError **)error;
120
+
121
+ /// Extract `maxFrames` evenly-spaced frames from the video at
122
+ /// `videoPath`, write each as a JPEG into `outputDir`, return the
123
+ /// list of file paths in capture order.
124
+ ///
125
+ /// Used as the first half of the panorama pipeline: the host app
126
+ /// records video while the user holds the shutter, then we sample
127
+ /// it down to N still frames the stitcher can consume. cv::Stitcher
128
+ /// works best with 5-15 well-spaced frames — much more is redundant
129
+ /// and slow; much less risks gaps in the seam.
130
+ ///
131
+ /// Implementation uses AVAssetImageGenerator (Foundation), not
132
+ /// OpenCV, so no C++ touches this path; cheap enough that we can
133
+ /// expose it as a separate primitive too.
134
+ + (nullable NSArray<NSString *> *)extractFramesFromVideoAtPath:(NSString *)videoPath
135
+ outputDir:(NSString *)outputDir
136
+ maxFrames:(NSInteger)maxFrames
137
+ jpegQuality:(NSInteger)quality
138
+ error:(NSError **)error;
139
+
140
+ /// One-shot helper: extract frames from `videoPath`, stitch them
141
+ /// into a panorama at `outputPath`, delete the temporary frames,
142
+ /// return the result. This is what the JS shutter-hold flow calls;
143
+ /// callers don't have to manage their own tmp directory or clean
144
+ /// up partial state on failure.
145
+ + (nullable RNStitchResult *)stitchVideoAtPath:(NSString *)videoPath
146
+ outputPath:(NSString *)outputPath
147
+ maxFrames:(NSInteger)maxFrames
148
+ jpegQuality:(NSInteger)quality
149
+ warperType:(nullable NSString *)warperType
150
+ blenderType:(nullable NSString *)blenderType
151
+ seamFinderType:(nullable NSString *)seamFinderType
152
+ error:(NSError **)error;
153
+
154
+ /// Phase 5: pose-driven stitch. Same end-to-end shape as
155
+ /// `stitchVideoAtPath` but consumes pre-computed camera poses
156
+ /// (from ARKit/ARCore via RNSARSession) and skips the
157
+ /// brittle features → matching → BundleAdjuster steps. Internally:
158
+ ///
159
+ /// 1. Extract maxFrames evenly-spaced frames from the video.
160
+ /// 2. Compute each frame's timestamp (fraction × totalSeconds).
161
+ /// 3. Match each frame to the nearest pose in `poses` (within
162
+ /// a 100 ms tolerance).
163
+ /// 4. Build cv::detail::CameraParams directly from the pose's
164
+ /// quaternion + intrinsics — flips coordinate conventions
165
+ /// between ARKit (Y-up, -Z forward) and OpenCV (Y-down,
166
+ /// +Z forward).
167
+ /// 5. Hand cameras to the existing warp + seam + blend pipeline.
168
+ ///
169
+ /// `poses` is an NSArray of NSDictionary; each entry has the keys
170
+ /// matching `RNSARFramePose.asDictionary()`:
171
+ /// tx, ty, tz, qx, qy, qz, qw, fx, fy, cx, cy,
172
+ /// imageWidth, imageHeight, timestampMs, trackingState
173
+ /// Frames whose closest pose is missing or beyond tolerance fall
174
+ /// back to the feature-matched path frame-by-frame (degraded but
175
+ /// functional). When ALL poses are missing the method returns
176
+ /// the same NSError code (1030) so the host can opt to retry via
177
+ /// the non-pose path.
178
+ + (nullable RNStitchResult *)stitchVideoAtPath:(NSString *)videoPath
179
+ outputPath:(NSString *)outputPath
180
+ maxFrames:(NSInteger)maxFrames
181
+ jpegQuality:(NSInteger)quality
182
+ warperType:(nullable NSString *)warperType
183
+ blenderType:(nullable NSString *)blenderType
184
+ seamFinderType:(nullable NSString *)seamFinderType
185
+ poses:(NSArray<NSDictionary *> *)poses
186
+ error:(NSError **)error;
187
+
188
+ /// V16 Phase 1: pose-driven stitch over an explicit list of frame
189
+ /// paths. Sibling of `stitchVideoAtPath:withPoses:` — same compose
190
+ /// stage, but the caller supplies frames as already-on-disk JPEGs
191
+ /// + a 1:1 pose array, so the video extraction + timestamp matching
192
+ /// steps are skipped entirely.
193
+ ///
194
+ /// This is the hot path for the "batch-on-AR-keyframes" flow: the
195
+ /// Swift `KeyframeGate` accepts ≤6 frames per capture, each saved
196
+ /// to disk with a known pose; on shutter release we feed those
197
+ /// straight into the same `BundleAdjuster + GraphCutSeamFinder +
198
+ /// MultiBandBlender` pipeline that the video-driven path uses.
199
+ ///
200
+ /// `framePaths.count` MUST equal `poses.count` (1:1 mapping; any
201
+ /// downstream filtering happens inside this method). `framePaths`
202
+ /// must be at least 2 entries. Pose dictionaries follow the same
203
+ /// shape as `RNSARFramePose.asDictionary()`.
204
+ + (nullable RNStitchResult *)stitchKeyframePaths:(NSArray<NSString *> *)framePaths
205
+ outputPath:(NSString *)outputPath
206
+ jpegQuality:(NSInteger)quality
207
+ warperType:(nullable NSString *)warperType
208
+ blenderType:(nullable NSString *)blenderType
209
+ seamFinderType:(nullable NSString *)seamFinderType
210
+ poses:(NSArray<NSDictionary *> *)poses
211
+ error:(NSError **)error;
212
+
213
+ /// Normalise the EXIF orientation of `imagePath` in place.
214
+ ///
215
+ /// vision-camera writes photos with the camera-sensor's native
216
+ /// landscape pixels and an EXIF Orientation tag describing how to
217
+ /// rotate them for display. Most consumers (iOS UIImage, RN's
218
+ /// <Image>) honour the tag, but Sentry breadcrumbs, share sheets,
219
+ /// downstream image-manipulation libs, and the cv::Stitcher all
220
+ /// read raw pixels and end up sideways.
221
+ ///
222
+ /// This method round-trips the file through cv::imread (which
223
+ /// honours EXIF and gives us the post-rotation pixel buffer) and
224
+ /// cv::imwrite (which writes a plain JPEG with NO EXIF), so the
225
+ /// saved file ends up with rotation baked into pixels and no
226
+ /// orientation metadata. Idempotent on already-normalised images.
227
+ ///
228
+ /// Returns `@{ @"width": NSNumber, @"height": NSNumber }` post
229
+ /// rotation so the caller can update its CaptureResult dimensions
230
+ /// to match what's now on disk. NSDictionary is used (rather than
231
+ /// CGSize) because Swift can't translate `(CGSize) + (NSError**)`
232
+ /// into a throwing API — a nullable reference type is required.
233
+ + (nullable NSDictionary<NSString *, NSNumber *> *)normaliseImageAtPath:(NSString *)imagePath
234
+ error:(NSError **)error;
235
+
236
+ @end
237
+
238
+ NS_ASSUME_NONNULL_END