@mentra/crust 0.1.0-dev.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.
- package/README.md +35 -0
- package/android/build.gradle +146 -0
- package/android/src/internal/AndroidManifest.xml +9 -0
- package/android/src/internal/java/com/mentra/crust/receivers/CaptionsTesterIncidentReceiver.kt +46 -0
- package/android/src/main/AndroidManifest.xml +19 -0
- package/android/src/main/java/com/mentra/crust/CrustModule.kt +882 -0
- package/android/src/main/java/com/mentra/crust/CrustView.kt +30 -0
- package/android/src/main/java/com/mentra/crust/heading/HeadingManager.kt +150 -0
- package/android/src/main/java/com/mentra/crust/jsc/JSCDispatcher.kt +189 -0
- package/android/src/main/java/com/mentra/crust/jsc/JSCPolyfillBridge.kt +246 -0
- package/android/src/main/java/com/mentra/crust/jsc/JSCRuntime.kt +593 -0
- package/android/src/main/java/com/mentra/crust/navigation/NavigationManager.kt +1445 -0
- package/android/src/main/java/com/mentra/crust/services/NotificationListener.kt +319 -0
- package/android/src/main/java/com/mentra/crust/utils/ImageProcessor.java +452 -0
- package/android/src/main/java/com/mentra/crust/utils/VideoStabilizer.kt +556 -0
- package/android/src/main/res/values/strings.xml +3 -0
- package/app.plugin.js +3 -0
- package/build/Crust.types.d.ts +148 -0
- package/build/Crust.types.d.ts.map +1 -0
- package/build/Crust.types.js +2 -0
- package/build/Crust.types.js.map +1 -0
- package/build/CrustModule.d.ts +175 -0
- package/build/CrustModule.d.ts.map +1 -0
- package/build/CrustModule.js +4 -0
- package/build/CrustModule.js.map +1 -0
- package/build/CrustModule.web.d.ts +26 -0
- package/build/CrustModule.web.d.ts.map +1 -0
- package/build/CrustModule.web.js +54 -0
- package/build/CrustModule.web.js.map +1 -0
- package/build/CrustView.d.ts +4 -0
- package/build/CrustView.d.ts.map +1 -0
- package/build/CrustView.js +7 -0
- package/build/CrustView.js.map +1 -0
- package/build/CrustView.web.d.ts +4 -0
- package/build/CrustView.web.d.ts.map +1 -0
- package/build/CrustView.web.js +7 -0
- package/build/CrustView.web.js.map +1 -0
- package/build/index.d.ts +4 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +6 -0
- package/build/index.js.map +1 -0
- package/expo-module.config.json +9 -0
- package/ios/Crust.podspec +65 -0
- package/ios/CrustModule.swift +544 -0
- package/ios/CrustView.swift +38 -0
- package/ios/Resources/startup.js +814 -0
- package/ios/Source/JSCDispatcher.swift +226 -0
- package/ios/Source/JSCPolyfillBridge.swift +378 -0
- package/ios/Source/JSCRuntime.swift +673 -0
- package/ios/Source/utils/ImageProcessor.swift +392 -0
- package/ios/Source/utils/SystemGestures.swift +53 -0
- package/ios/Source/utils/VideoStabilizer.swift +374 -0
- package/ios/heading/HeadingManager.swift +74 -0
- package/ios/navigation/NavPayloads.swift +62 -0
- package/ios/navigation/NavigationManager.swift +720 -0
- package/package.json +69 -0
- package/plugin/build/index.d.ts +19 -0
- package/plugin/build/index.js +23 -0
- package/plugin/build/withAndroid.d.ts +2 -0
- package/plugin/build/withAndroid.js +78 -0
- package/src/Crust.types.ts +157 -0
- package/src/CrustModule.ts +186 -0
- package/src/CrustModule.web.ts +57 -0
- package/src/CrustView.tsx +10 -0
- package/src/CrustView.web.tsx +11 -0
- package/src/index.ts +5 -0
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
import CoreImage
|
|
2
|
+
import UIKit
|
|
3
|
+
|
|
4
|
+
/// Image processor for gallery photos synced from Mentra glasses.
|
|
5
|
+
/// Applies lens distortion correction and color correction using CoreImage.
|
|
6
|
+
class ImageProcessor {
|
|
7
|
+
private static let TAG = "ImageProcessor"
|
|
8
|
+
|
|
9
|
+
// --- Lens distortion coefficients ---
|
|
10
|
+
// Brown-Conrady model for the Mentra Live camera (Sony sensor, 118° FOV fisheye)
|
|
11
|
+
// Calibrated from chessboard photos at 3264x2448 native sensor resolution.
|
|
12
|
+
private static let k1: Double = -0.10
|
|
13
|
+
private static let k2: Double = 0.02
|
|
14
|
+
private static let p1: Double = 0.0
|
|
15
|
+
private static let p2: Double = 0.0
|
|
16
|
+
|
|
17
|
+
// --- Color pipeline tuning parameters ---
|
|
18
|
+
|
|
19
|
+
// Tone curve anchor points (CIToneCurve: x = input, y = output)
|
|
20
|
+
// Slight shadow lift for low-light camera, mild highlight compression, gentle S-curve.
|
|
21
|
+
private static let toneCurvePoint0 = CIVector(x: 0.00, y: 0.05)
|
|
22
|
+
private static let toneCurvePoint1 = CIVector(x: 0.25, y: 0.22)
|
|
23
|
+
private static let toneCurvePoint2 = CIVector(x: 0.50, y: 0.50)
|
|
24
|
+
private static let toneCurvePoint3 = CIVector(x: 0.75, y: 0.78)
|
|
25
|
+
private static let toneCurvePoint4 = CIVector(x: 1.00, y: 0.95)
|
|
26
|
+
|
|
27
|
+
// Vibrance: selective saturation boost for desaturated colors (0.0 = off, 1.0 = max)
|
|
28
|
+
private static let vibranceAmount: Double = 0.3
|
|
29
|
+
|
|
30
|
+
// Color correction matrix (CIColorMatrix vectors: [R, G, B, A])
|
|
31
|
+
// Adjusts warmth/white balance to compensate for the glasses camera's color cast.
|
|
32
|
+
private static let rVector = CIVector(x: 1.06, y: 0.02, z: -0.01, w: 0)
|
|
33
|
+
private static let gVector = CIVector(x: 0.01, y: 1.04, z: -0.01, w: 0)
|
|
34
|
+
private static let bVector = CIVector(x: -0.02, y: 0.01, z: 1.02, w: 0)
|
|
35
|
+
private static let aVector = CIVector(x: 0, y: 0, z: 0, w: 1)
|
|
36
|
+
private static let biasVector = CIVector(x: 5.0 / 255.0, y: 3.0 / 255.0, z: 0, w: 0)
|
|
37
|
+
|
|
38
|
+
/// Process a gallery image with the specified corrections.
|
|
39
|
+
/// - Parameters:
|
|
40
|
+
/// - inputPath: Path to the input JPEG file
|
|
41
|
+
/// - outputPath: Path to write the processed JPEG
|
|
42
|
+
/// - lensCorrection: Whether to apply barrel distortion correction
|
|
43
|
+
/// - colorCorrection: Whether to apply color/white balance correction
|
|
44
|
+
/// - Returns: Processing time in milliseconds, or -1 on failure
|
|
45
|
+
static func process(
|
|
46
|
+
inputPath: String,
|
|
47
|
+
outputPath: String,
|
|
48
|
+
lensCorrection: Bool,
|
|
49
|
+
colorCorrection: Bool
|
|
50
|
+
) -> Int64 {
|
|
51
|
+
let startTime = CFAbsoluteTimeGetCurrent()
|
|
52
|
+
|
|
53
|
+
guard let inputData = FileManager.default.contents(atPath: inputPath),
|
|
54
|
+
let ciImage = CIImage(data: inputData)
|
|
55
|
+
else {
|
|
56
|
+
NSLog("\(TAG): Failed to load image: \(inputPath)")
|
|
57
|
+
return -1
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
let w = ciImage.extent.width
|
|
61
|
+
let h = ciImage.extent.height
|
|
62
|
+
NSLog("\(TAG): Processing \(Int(w))x\(Int(h)) lens=\(lensCorrection) color=\(colorCorrection)")
|
|
63
|
+
|
|
64
|
+
var image = ciImage
|
|
65
|
+
|
|
66
|
+
// Step 1: Lens distortion correction
|
|
67
|
+
if lensCorrection {
|
|
68
|
+
image = applyLensCorrection(image)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Step 2: Tone mapping — S-curve + vibrance
|
|
72
|
+
if colorCorrection {
|
|
73
|
+
image = applyToneCurve(image)
|
|
74
|
+
image = applyVibrance(image)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Step 3: Color correction — linear warmth/tint
|
|
78
|
+
if colorCorrection {
|
|
79
|
+
image = applyColorCorrection(image)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Render to JPEG
|
|
83
|
+
let context = CIContext(options: [.useSoftwareRenderer: false])
|
|
84
|
+
let colorSpace = CGColorSpace(name: CGColorSpace.sRGB)!
|
|
85
|
+
|
|
86
|
+
guard
|
|
87
|
+
let jpegData = context.jpegRepresentation(
|
|
88
|
+
of: image,
|
|
89
|
+
colorSpace: colorSpace,
|
|
90
|
+
options: [kCGImageDestinationLossyCompressionQuality as CIImageRepresentationOption: 0.95]
|
|
91
|
+
)
|
|
92
|
+
else {
|
|
93
|
+
NSLog("\(TAG): Failed to render JPEG")
|
|
94
|
+
return -1
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
do {
|
|
98
|
+
try jpegData.write(to: URL(fileURLWithPath: outputPath))
|
|
99
|
+
} catch {
|
|
100
|
+
NSLog("\(TAG): Failed to write output: \(error.localizedDescription)")
|
|
101
|
+
return -1
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
let elapsed = Int64((CFAbsoluteTimeGetCurrent() - startTime) * 1000)
|
|
105
|
+
NSLog("\(TAG): Processing complete in \(elapsed)ms -> \(outputPath)")
|
|
106
|
+
return elapsed
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/// Apply Brown-Conrady lens distortion correction using CIFilter.
|
|
110
|
+
private static func applyLensCorrection(_ image: CIImage) -> CIImage {
|
|
111
|
+
// Use a CIWarpKernel for the distortion model
|
|
112
|
+
// The kernel maps output coordinates to input (distorted) coordinates
|
|
113
|
+
let w = image.extent.width
|
|
114
|
+
let h = image.extent.height
|
|
115
|
+
let cx = w / 2.0
|
|
116
|
+
let cy = h / 2.0
|
|
117
|
+
let norm = sqrt(cx * cx + cy * cy)
|
|
118
|
+
|
|
119
|
+
// Use vImage-style approach: render pixel-by-pixel via a custom CIKernel
|
|
120
|
+
// For performance, we use the CGImage path with a bitmap context
|
|
121
|
+
let context = CIContext(options: [.useSoftwareRenderer: false])
|
|
122
|
+
guard let cgImage = context.createCGImage(image, from: image.extent) else {
|
|
123
|
+
NSLog("\(TAG): Failed to create CGImage for lens correction")
|
|
124
|
+
return image
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
let width = cgImage.width
|
|
128
|
+
let height = cgImage.height
|
|
129
|
+
let bytesPerPixel = 4
|
|
130
|
+
let bytesPerRow = width * bytesPerPixel
|
|
131
|
+
let totalBytes = height * bytesPerRow
|
|
132
|
+
|
|
133
|
+
// Read source pixels
|
|
134
|
+
guard
|
|
135
|
+
let colorSpace = CGColorSpace(name: CGColorSpace.sRGB),
|
|
136
|
+
let srcContext = CGContext(
|
|
137
|
+
data: nil,
|
|
138
|
+
width: width,
|
|
139
|
+
height: height,
|
|
140
|
+
bitsPerComponent: 8,
|
|
141
|
+
bytesPerRow: bytesPerRow,
|
|
142
|
+
space: colorSpace,
|
|
143
|
+
bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue
|
|
144
|
+
)
|
|
145
|
+
else {
|
|
146
|
+
NSLog("\(TAG): Failed to create bitmap context")
|
|
147
|
+
return image
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
srcContext.draw(cgImage, in: CGRect(x: 0, y: 0, width: width, height: height))
|
|
151
|
+
guard let srcData = srcContext.data else { return image }
|
|
152
|
+
let srcPixels = srcData.bindMemory(to: UInt8.self, capacity: totalBytes)
|
|
153
|
+
|
|
154
|
+
// Create output context
|
|
155
|
+
guard
|
|
156
|
+
let dstContext = CGContext(
|
|
157
|
+
data: nil,
|
|
158
|
+
width: width,
|
|
159
|
+
height: height,
|
|
160
|
+
bitsPerComponent: 8,
|
|
161
|
+
bytesPerRow: bytesPerRow,
|
|
162
|
+
space: colorSpace,
|
|
163
|
+
bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue
|
|
164
|
+
)
|
|
165
|
+
else { return image }
|
|
166
|
+
|
|
167
|
+
guard let dstData = dstContext.data else { return image }
|
|
168
|
+
let dstPixels = dstData.bindMemory(to: UInt8.self, capacity: totalBytes)
|
|
169
|
+
|
|
170
|
+
let cxI = Double(width) / 2.0
|
|
171
|
+
let cyI = Double(height) / 2.0
|
|
172
|
+
let normI = sqrt(cxI * cxI + cyI * cyI)
|
|
173
|
+
|
|
174
|
+
// Remap each pixel with bilinear interpolation for sub-pixel accuracy
|
|
175
|
+
let maxX = width - 2 // -2 because bilinear reads x0+1
|
|
176
|
+
let maxY = height - 2
|
|
177
|
+
|
|
178
|
+
for y in 0 ..< height {
|
|
179
|
+
for x in 0 ..< width {
|
|
180
|
+
let xn = (Double(x) - cxI) / normI
|
|
181
|
+
let yn = (Double(y) - cyI) / normI
|
|
182
|
+
let r2 = xn * xn + yn * yn
|
|
183
|
+
let r4 = r2 * r2
|
|
184
|
+
|
|
185
|
+
let radial = 1.0 + k1 * r2 + k2 * r4
|
|
186
|
+
let xd = xn * radial + 2.0 * p1 * xn * yn + p2 * (r2 + 2.0 * xn * xn)
|
|
187
|
+
let yd = yn * radial + p1 * (r2 + 2.0 * yn * yn) + 2.0 * p2 * xn * yn
|
|
188
|
+
|
|
189
|
+
let srcXf = xd * normI + cxI
|
|
190
|
+
let srcYf = yd * normI + cyI
|
|
191
|
+
let x0 = Int(srcXf)
|
|
192
|
+
let y0 = Int(srcYf)
|
|
193
|
+
|
|
194
|
+
let dstIdx = (y * width + x) * bytesPerPixel
|
|
195
|
+
if x0 < 0 || x0 > maxX || y0 < 0 || y0 > maxY {
|
|
196
|
+
// Out of bounds — black pixel
|
|
197
|
+
dstPixels[dstIdx] = 0
|
|
198
|
+
dstPixels[dstIdx + 1] = 0
|
|
199
|
+
dstPixels[dstIdx + 2] = 0
|
|
200
|
+
dstPixels[dstIdx + 3] = 255
|
|
201
|
+
continue
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Fractional parts for bilinear blend
|
|
205
|
+
let fx = Float(srcXf - Double(x0))
|
|
206
|
+
let fy = Float(srcYf - Double(y0))
|
|
207
|
+
let ifx = 1.0 - fx
|
|
208
|
+
let ify = 1.0 - fy
|
|
209
|
+
|
|
210
|
+
// Four source pixels
|
|
211
|
+
let idx00 = (y0 * width + x0) * bytesPerPixel
|
|
212
|
+
let idx10 = idx00 + bytesPerPixel
|
|
213
|
+
let idx01 = idx00 + bytesPerRow
|
|
214
|
+
let idx11 = idx01 + bytesPerPixel
|
|
215
|
+
|
|
216
|
+
// Bilinear blend per channel
|
|
217
|
+
for c in 0 ..< 4 {
|
|
218
|
+
let v = ifx * ify * Float(srcPixels[idx00 + c])
|
|
219
|
+
+ fx * ify * Float(srcPixels[idx10 + c])
|
|
220
|
+
+ ifx * fy * Float(srcPixels[idx01 + c])
|
|
221
|
+
+ fx * fy * Float(srcPixels[idx11 + c])
|
|
222
|
+
dstPixels[dstIdx + c] = UInt8(min(255, max(0, Int(v + 0.5))))
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
guard let outputCGImage = dstContext.makeImage() else { return image }
|
|
228
|
+
return CIImage(cgImage: outputCGImage)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/// Apply S-curve tone mapping using CIToneCurve.
|
|
232
|
+
private static func applyToneCurve(_ image: CIImage) -> CIImage {
|
|
233
|
+
guard let filter = CIFilter(name: "CIToneCurve") else { return image }
|
|
234
|
+
filter.setValue(image, forKey: kCIInputImageKey)
|
|
235
|
+
filter.setValue(toneCurvePoint0, forKey: "inputPoint0")
|
|
236
|
+
filter.setValue(toneCurvePoint1, forKey: "inputPoint1")
|
|
237
|
+
filter.setValue(toneCurvePoint2, forKey: "inputPoint2")
|
|
238
|
+
filter.setValue(toneCurvePoint3, forKey: "inputPoint3")
|
|
239
|
+
filter.setValue(toneCurvePoint4, forKey: "inputPoint4")
|
|
240
|
+
return filter.outputImage ?? image
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/// Apply vibrance — selectively boosts undersaturated colors.
|
|
244
|
+
private static func applyVibrance(_ image: CIImage) -> CIImage {
|
|
245
|
+
guard let filter = CIFilter(name: "CIVibrance") else { return image }
|
|
246
|
+
filter.setValue(image, forKey: kCIInputImageKey)
|
|
247
|
+
filter.setValue(vibranceAmount, forKey: "inputAmount")
|
|
248
|
+
return filter.outputImage ?? image
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/// Apply color correction using CIColorMatrix filter.
|
|
252
|
+
private static func applyColorCorrection(_ image: CIImage) -> CIImage {
|
|
253
|
+
guard let filter = CIFilter(name: "CIColorMatrix") else {
|
|
254
|
+
NSLog("\(TAG): CIColorMatrix filter not available")
|
|
255
|
+
return image
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
filter.setValue(image, forKey: kCIInputImageKey)
|
|
259
|
+
filter.setValue(rVector, forKey: "inputRVector")
|
|
260
|
+
filter.setValue(gVector, forKey: "inputGVector")
|
|
261
|
+
filter.setValue(bVector, forKey: "inputBVector")
|
|
262
|
+
filter.setValue(aVector, forKey: "inputAVector")
|
|
263
|
+
filter.setValue(biasVector, forKey: "inputBiasVector")
|
|
264
|
+
|
|
265
|
+
return filter.outputImage ?? image
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/// Merge 3 exposure-bracketed images into a single HDR result using
|
|
269
|
+
/// simple exposure fusion (Mertens' method approximation).
|
|
270
|
+
/// - Parameters:
|
|
271
|
+
/// - underPath: Path to underexposed image (EV-2)
|
|
272
|
+
/// - normalPath: Path to normally exposed image (EV0)
|
|
273
|
+
/// - overPath: Path to overexposed image (EV+2)
|
|
274
|
+
/// - outputPath: Path to write the merged result
|
|
275
|
+
/// - Returns: Processing time in ms, or -1 on failure
|
|
276
|
+
static func mergeHdr(
|
|
277
|
+
underPath: String, normalPath: String, overPath: String, outputPath: String
|
|
278
|
+
) -> Int64 {
|
|
279
|
+
let startTime = CFAbsoluteTimeGetCurrent()
|
|
280
|
+
|
|
281
|
+
guard let underData = FileManager.default.contents(atPath: underPath),
|
|
282
|
+
let normalData = FileManager.default.contents(atPath: normalPath),
|
|
283
|
+
let overData = FileManager.default.contents(atPath: overPath),
|
|
284
|
+
let underImage = CIImage(data: underData),
|
|
285
|
+
let normalImage = CIImage(data: normalData),
|
|
286
|
+
let overImage = CIImage(data: overData)
|
|
287
|
+
else {
|
|
288
|
+
NSLog("\(TAG): Failed to load HDR bracket images")
|
|
289
|
+
return -1
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
let context = CIContext(options: [.useSoftwareRenderer: false])
|
|
293
|
+
let w = Int(normalImage.extent.width)
|
|
294
|
+
let h = Int(normalImage.extent.height)
|
|
295
|
+
|
|
296
|
+
// Render all three to CGImages for pixel access
|
|
297
|
+
guard let underCG = context.createCGImage(underImage, from: underImage.extent),
|
|
298
|
+
let normalCG = context.createCGImage(normalImage, from: normalImage.extent),
|
|
299
|
+
let overCG = context.createCGImage(overImage, from: overImage.extent)
|
|
300
|
+
else {
|
|
301
|
+
NSLog("\(TAG): Failed to create CGImages for HDR merge")
|
|
302
|
+
return -1
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
let bpp = 4
|
|
306
|
+
let bytesPerRow = w * bpp
|
|
307
|
+
let totalBytes = h * bytesPerRow
|
|
308
|
+
guard let colorSpace = CGColorSpace(name: CGColorSpace.sRGB) else { return -1 }
|
|
309
|
+
let bitmapInfo = CGImageAlphaInfo.premultipliedLast.rawValue
|
|
310
|
+
|
|
311
|
+
// Create contexts and read pixel data.
|
|
312
|
+
// IMPORTANT: We must retain the CGContexts so their backing memory stays valid
|
|
313
|
+
// while we access the pixel pointers.
|
|
314
|
+
func makePixelContext(_ cgImage: CGImage) -> CGContext? {
|
|
315
|
+
guard
|
|
316
|
+
let ctx = CGContext(
|
|
317
|
+
data: nil, width: w, height: h, bitsPerComponent: 8,
|
|
318
|
+
bytesPerRow: bytesPerRow, space: colorSpace, bitmapInfo: bitmapInfo
|
|
319
|
+
)
|
|
320
|
+
else { return nil }
|
|
321
|
+
ctx.draw(cgImage, in: CGRect(x: 0, y: 0, width: w, height: h))
|
|
322
|
+
return ctx
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
guard let underCtx = makePixelContext(underCG),
|
|
326
|
+
let normalCtx = makePixelContext(normalCG),
|
|
327
|
+
let overCtx = makePixelContext(overCG),
|
|
328
|
+
let uPx = underCtx.data?.bindMemory(to: UInt8.self, capacity: totalBytes),
|
|
329
|
+
let nPx = normalCtx.data?.bindMemory(to: UInt8.self, capacity: totalBytes),
|
|
330
|
+
let oPx = overCtx.data?.bindMemory(to: UInt8.self, capacity: totalBytes)
|
|
331
|
+
else { return -1 }
|
|
332
|
+
|
|
333
|
+
// Create output context
|
|
334
|
+
guard
|
|
335
|
+
let outCtx = CGContext(
|
|
336
|
+
data: nil, width: w, height: h, bitsPerComponent: 8,
|
|
337
|
+
bytesPerRow: bytesPerRow, space: colorSpace, bitmapInfo: bitmapInfo
|
|
338
|
+
)
|
|
339
|
+
else { return -1 }
|
|
340
|
+
guard let outData = outCtx.data else { return -1 }
|
|
341
|
+
let outPx = outData.bindMemory(to: UInt8.self, capacity: totalBytes)
|
|
342
|
+
|
|
343
|
+
// Exposure fusion
|
|
344
|
+
for i in stride(from: 0, to: totalBytes, by: bpp) {
|
|
345
|
+
let uR = Float(uPx[i])
|
|
346
|
+
let uG = Float(uPx[i + 1])
|
|
347
|
+
let uB = Float(uPx[i + 2])
|
|
348
|
+
let uLum = (uR + uG + uB) / 3.0 / 255.0
|
|
349
|
+
let uW = 4.0 * uLum * (1.0 - uLum) + 0.01
|
|
350
|
+
|
|
351
|
+
let nR = Float(nPx[i])
|
|
352
|
+
let nG = Float(nPx[i + 1])
|
|
353
|
+
let nB = Float(nPx[i + 2])
|
|
354
|
+
let nLum = (nR + nG + nB) / 3.0 / 255.0
|
|
355
|
+
let nW = 4.0 * nLum * (1.0 - nLum) + 0.01
|
|
356
|
+
|
|
357
|
+
let oR = Float(oPx[i])
|
|
358
|
+
let oG = Float(oPx[i + 1])
|
|
359
|
+
let oB = Float(oPx[i + 2])
|
|
360
|
+
let oLum = (oR + oG + oB) / 3.0 / 255.0
|
|
361
|
+
let oW = 4.0 * oLum * (1.0 - oLum) + 0.01
|
|
362
|
+
|
|
363
|
+
let total = uW + nW + oW
|
|
364
|
+
outPx[i] = UInt8(min(255, (uR * uW + nR * nW + oR * oW) / total))
|
|
365
|
+
outPx[i + 1] = UInt8(min(255, (uG * uW + nG * nW + oG * oW) / total))
|
|
366
|
+
outPx[i + 2] = UInt8(min(255, (uB * uW + nB * nW + oB * oW) / total))
|
|
367
|
+
outPx[i + 3] = 255
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
guard let outCGImage = outCtx.makeImage() else { return -1 }
|
|
371
|
+
let resultImage = CIImage(cgImage: outCGImage)
|
|
372
|
+
|
|
373
|
+
let resultColorSpace = CGColorSpace(name: CGColorSpace.sRGB)!
|
|
374
|
+
guard
|
|
375
|
+
let jpegData = context.jpegRepresentation(
|
|
376
|
+
of: resultImage, colorSpace: resultColorSpace,
|
|
377
|
+
options: [kCGImageDestinationLossyCompressionQuality as CIImageRepresentationOption: 0.95]
|
|
378
|
+
)
|
|
379
|
+
else { return -1 }
|
|
380
|
+
|
|
381
|
+
do {
|
|
382
|
+
try jpegData.write(to: URL(fileURLWithPath: outputPath))
|
|
383
|
+
} catch {
|
|
384
|
+
NSLog("\(TAG): Failed to write HDR result: \(error)")
|
|
385
|
+
return -1
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
let elapsed = Int64((CFAbsoluteTimeGetCurrent() - startTime) * 1000)
|
|
389
|
+
NSLog("\(TAG): HDR merge complete in \(elapsed)ms")
|
|
390
|
+
return elapsed
|
|
391
|
+
}
|
|
392
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import UIKit
|
|
2
|
+
|
|
3
|
+
/// Lets JS override `preferredScreenEdgesDeferringSystemGestures` on the
|
|
4
|
+
/// app's root view controllers. iOS 11+ requires a swipe across a screen
|
|
5
|
+
/// edge to first be "deferred" before the system gesture (Control Center,
|
|
6
|
+
/// Notification Center, Home indicator) fires — useful when an app wants
|
|
7
|
+
/// the user to confirm before backgrounding, e.g. a two-swipe exit.
|
|
8
|
+
///
|
|
9
|
+
/// Implemented via method swizzling because Expo creates the root view
|
|
10
|
+
/// controller internally — there is no host code path to subclass it
|
|
11
|
+
/// without forking ExpoModulesCore. The swizzle is installed lazily on
|
|
12
|
+
/// first call and is idempotent.
|
|
13
|
+
enum SystemGestures {
|
|
14
|
+
/// Edges to defer. `[]` means default behavior (system gestures fire
|
|
15
|
+
/// on first swipe). Read by the swizzled getter on every gesture.
|
|
16
|
+
static var deferredEdges: UIRectEdge = []
|
|
17
|
+
|
|
18
|
+
private static var swizzled = false
|
|
19
|
+
|
|
20
|
+
static func setDeferredEdges(_ edges: UIRectEdge) {
|
|
21
|
+
installSwizzleIfNeeded()
|
|
22
|
+
deferredEdges = edges
|
|
23
|
+
DispatchQueue.main.async {
|
|
24
|
+
for scene in UIApplication.shared.connectedScenes {
|
|
25
|
+
guard let windowScene = scene as? UIWindowScene else { continue }
|
|
26
|
+
for window in windowScene.windows {
|
|
27
|
+
window.rootViewController?.setNeedsUpdateOfScreenEdgesDeferringSystemGestures()
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
private static func installSwizzleIfNeeded() {
|
|
34
|
+
guard !swizzled else { return }
|
|
35
|
+
swizzled = true
|
|
36
|
+
|
|
37
|
+
let cls: AnyClass = UIViewController.self
|
|
38
|
+
let originalSel = #selector(getter: UIViewController.preferredScreenEdgesDeferringSystemGestures)
|
|
39
|
+
let swizzledSel = #selector(getter: UIViewController.crust_preferredScreenEdgesDeferringSystemGestures)
|
|
40
|
+
|
|
41
|
+
guard let original = class_getInstanceMethod(cls, originalSel),
|
|
42
|
+
let replacement = class_getInstanceMethod(cls, swizzledSel)
|
|
43
|
+
else { return }
|
|
44
|
+
|
|
45
|
+
method_exchangeImplementations(original, replacement)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
extension UIViewController {
|
|
50
|
+
@objc var crust_preferredScreenEdgesDeferringSystemGestures: UIRectEdge {
|
|
51
|
+
SystemGestures.deferredEdges
|
|
52
|
+
}
|
|
53
|
+
}
|