react-native-rectangle-doc-scanner 1.13.0 → 1.14.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.
@@ -1,694 +0,0 @@
1
- import AVFoundation
2
- import CoreImage
3
- import Foundation
4
- import React
5
- import UIKit
6
- import Vision
7
-
8
- @objc(RNRDocScannerView)
9
- class RNRDocScannerView: UIView, AVCaptureVideoDataOutputSampleBufferDelegate, AVCapturePhotoCaptureDelegate {
10
- @objc var detectionCountBeforeCapture: NSNumber = 8
11
- @objc var autoCapture: Bool = true
12
- @objc var enableTorch: Bool = false {
13
- didSet {
14
- updateTorchMode()
15
- }
16
- }
17
- @objc var quality: NSNumber = 90
18
- @objc var useBase64: Bool = false
19
-
20
- @objc var onRectangleDetect: RCTDirectEventBlock?
21
- @objc var onPictureTaken: RCTDirectEventBlock?
22
-
23
- private let session = AVCaptureSession()
24
- private let sessionQueue = DispatchQueue(label: "com.reactnative.rectangledocscanner.session")
25
- private let analysisQueue = DispatchQueue(label: "com.reactnative.rectangledocscanner.analysis")
26
- private let ciContext = CIContext()
27
-
28
- private var previewLayer: AVCaptureVideoPreviewLayer?
29
- private let videoOutput = AVCaptureVideoDataOutput()
30
- private let photoOutput = AVCapturePhotoOutput()
31
- private var smoothedOverlayPoints: [CGPoint]?
32
- private let outlineLayer = CAShapeLayer()
33
- private let gridLayer = CAShapeLayer()
34
-
35
- private var currentStableCounter: Int = 0
36
- private var isProcessingFrame = false
37
- private var isCaptureInFlight = false
38
- private var lastObservation: VNRectangleObservation?
39
- private var missedDetectionFrames: Int = 0
40
- private let maxMissedDetections = 1
41
- private var lastFrameSize: CGSize = .zero
42
- private var photoCaptureCompletion: ((Result<RNRDocScannerCaptureResult, Error>) -> Void)?
43
-
44
- override init(frame: CGRect) {
45
- super.init(frame: frame)
46
- commonInit()
47
- }
48
-
49
- required init?(coder: NSCoder) {
50
- super.init(coder: coder)
51
- commonInit()
52
- }
53
-
54
- private func commonInit() {
55
- backgroundColor = .black
56
- configurePreviewLayer()
57
- configureOverlayLayers()
58
- configureSession()
59
- }
60
-
61
- private func configurePreviewLayer() {
62
- let layer = AVCaptureVideoPreviewLayer(session: session)
63
- layer.videoGravity = .resizeAspectFill
64
- self.layer.insertSublayer(layer, at: 0)
65
- previewLayer = layer
66
- }
67
-
68
- private func configureOverlayLayers() {
69
- outlineLayer.strokeColor = UIColor(red: 0.18, green: 0.6, blue: 0.95, alpha: 1.0).cgColor
70
- outlineLayer.fillColor = UIColor(red: 0.18, green: 0.6, blue: 0.95, alpha: 0.2).cgColor
71
- outlineLayer.lineWidth = 4
72
- outlineLayer.lineJoin = .round
73
- outlineLayer.isHidden = true
74
- layer.addSublayer(outlineLayer)
75
-
76
- gridLayer.strokeColor = UIColor(red: 0.18, green: 0.6, blue: 0.95, alpha: 0.35).cgColor
77
- gridLayer.fillColor = UIColor.clear.cgColor
78
- gridLayer.lineWidth = 1.5
79
- gridLayer.lineJoin = .round
80
- gridLayer.isHidden = true
81
- gridLayer.zPosition = outlineLayer.zPosition + 1
82
- layer.addSublayer(gridLayer)
83
- }
84
-
85
- private func configureSession() {
86
- sessionQueue.async { [weak self] in
87
- guard let self else { return }
88
-
89
- session.beginConfiguration()
90
- session.sessionPreset = .high
91
-
92
- defer {
93
- session.commitConfiguration()
94
- if !session.isRunning {
95
- session.startRunning()
96
- }
97
- }
98
-
99
- guard
100
- let videoDevice = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back),
101
- let videoInput = try? AVCaptureDeviceInput(device: videoDevice),
102
- session.canAddInput(videoInput)
103
- else {
104
- NSLog("[RNRDocScanner] Unable to create AVCaptureDeviceInput")
105
- return
106
- }
107
-
108
- session.addInput(videoInput)
109
-
110
- if session.canAddOutput(photoOutput) {
111
- photoOutput.isHighResolutionCaptureEnabled = true
112
- session.addOutput(photoOutput)
113
- if let connection = photoOutput.connection(with: .video), connection.isVideoOrientationSupported {
114
- connection.videoOrientation = .portrait
115
- }
116
- }
117
-
118
- videoOutput.videoSettings = [
119
- kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_420YpCbCr8BiPlanarFullRange,
120
- ]
121
- videoOutput.alwaysDiscardsLateVideoFrames = true
122
- videoOutput.setSampleBufferDelegate(self, queue: analysisQueue)
123
-
124
- if session.canAddOutput(videoOutput) {
125
- session.addOutput(videoOutput)
126
- if let connection = videoOutput.connection(with: .video), connection.isVideoOrientationSupported {
127
- connection.videoOrientation = .portrait
128
- }
129
- }
130
- }
131
- }
132
-
133
- override func layoutSubviews() {
134
- super.layoutSubviews()
135
- previewLayer?.frame = bounds
136
- if let connection = previewLayer?.connection, connection.isVideoOrientationSupported {
137
- connection.videoOrientation = .portrait
138
- }
139
- outlineLayer.frame = bounds
140
- gridLayer.frame = bounds
141
- }
142
-
143
- private func updateTorchMode() {
144
- sessionQueue.async { [weak self] in
145
- guard
146
- let self,
147
- let device = self.videoDevice(for: .back),
148
- device.hasTorch
149
- else {
150
- return
151
- }
152
-
153
- do {
154
- try device.lockForConfiguration()
155
- device.torchMode = self.enableTorch ? .on : .off
156
- device.unlockForConfiguration()
157
- } catch {
158
- NSLog("[RNRDocScanner] Failed to update torch mode: \(error)")
159
- }
160
- }
161
- }
162
-
163
- private func videoDevice(for position: AVCaptureDevice.Position) -> AVCaptureDevice? {
164
- if let device = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: position) {
165
- return device
166
- }
167
- return AVCaptureDevice.devices(for: .video).first(where: { $0.position == position })
168
- }
169
-
170
- // MARK: - Detection
171
-
172
- func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
173
- if isProcessingFrame {
174
- return
175
- }
176
- guard let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else {
177
- return
178
- }
179
-
180
- isProcessingFrame = true
181
- CVPixelBufferLockBaseAddress(pixelBuffer, .readOnly)
182
- let frameSize = CGSize(width: CVPixelBufferGetWidth(pixelBuffer), height: CVPixelBufferGetHeight(pixelBuffer))
183
- lastFrameSize = frameSize
184
- let orientation = currentExifOrientation()
185
-
186
- defer {
187
- CVPixelBufferUnlockBaseAddress(pixelBuffer, .readOnly)
188
- isProcessingFrame = false
189
- }
190
-
191
- let requestHandler: VNRequestCompletionHandler = { [weak self] request, error in
192
- guard let self = self else { return }
193
-
194
- if let error = error {
195
- NSLog("[RNRDocScanner] detection error: \(error)")
196
- self.lastObservation = nil
197
- self.handleDetectedRectangle(nil, frameSize: frameSize)
198
- return
199
- }
200
-
201
- guard let observations = request.results as? [VNRectangleObservation], !observations.isEmpty else {
202
- self.lastObservation = nil
203
- self.handleDetectedRectangle(nil, frameSize: frameSize)
204
- return
205
- }
206
-
207
- let filtered = observations.filter { $0.confidence >= 0.55 }
208
- let candidates = filtered.isEmpty ? observations : filtered
209
- let weighted: [VNRectangleObservation] = candidates.sorted { (lhs: VNRectangleObservation, rhs: VNRectangleObservation) -> Bool in
210
- let lhsScore: CGFloat = CGFloat(lhs.confidence) * lhs.boundingBox.area
211
- let rhsScore: CGFloat = CGFloat(rhs.confidence) * rhs.boundingBox.area
212
- return lhsScore > rhsScore
213
- }
214
-
215
- guard let best = weighted.first else {
216
- self.lastObservation = nil
217
- self.handleDetectedRectangle(nil, frameSize: frameSize)
218
- return
219
- }
220
-
221
- self.lastObservation = best
222
- self.missedDetectionFrames = 0
223
- self.handleDetectedRectangle(best, frameSize: frameSize)
224
- }
225
-
226
- let request = VNDetectRectanglesRequest(completionHandler: requestHandler)
227
-
228
- request.maximumObservations = 3
229
- request.minimumConfidence = 0.65
230
- request.minimumAspectRatio = 0.12
231
- request.maximumAspectRatio = 1.9
232
- request.minimumSize = 0.05
233
- if #available(iOS 13.0, *) {
234
- request.quadratureTolerance = 18
235
- }
236
-
237
- var processedImage = CIImage(cvPixelBuffer: pixelBuffer)
238
- processedImage = processedImage.applyingFilter("CIColorControls", parameters: [
239
- kCIInputContrastKey: 1.35,
240
- kCIInputBrightnessKey: 0.02,
241
- kCIInputSaturationKey: 1.05,
242
- ])
243
- processedImage = processedImage.applyingFilter("CISharpenLuminance", parameters: [kCIInputSharpnessKey: 0.5])
244
-
245
- let handler = VNImageRequestHandler(ciImage: processedImage, orientation: orientation, options: [:])
246
- do {
247
- try handler.perform([request])
248
- } catch {
249
- NSLog("[RNRDocScanner] Failed to run Vision request: \(error)")
250
- lastObservation = nil
251
- handleDetectedRectangle(nil, frameSize: frameSize)
252
- }
253
- }
254
-
255
- func handleDetectedRectangle(_ rectangle: VNRectangleObservation?, frameSize: CGSize) {
256
- guard let onRectangleDetect else { return }
257
-
258
- let effectiveObservation: VNRectangleObservation?
259
- if let rect = rectangle {
260
- effectiveObservation = rect
261
- lastObservation = rect
262
- missedDetectionFrames = 0
263
- } else if missedDetectionFrames < maxMissedDetections, let cached = lastObservation {
264
- missedDetectionFrames += 1
265
- effectiveObservation = cached
266
- } else {
267
- lastObservation = nil
268
- missedDetectionFrames = 0
269
- smoothedOverlayPoints = nil
270
- effectiveObservation = nil
271
- }
272
-
273
- let overlayStableThreshold = max(2, Int(truncating: detectionCountBeforeCapture) / 2)
274
- let payload: [String: Any?]
275
-
276
- if let observation = effectiveObservation {
277
- let points = [
278
- pointForOverlay(from: observation.topLeft, frameSize: frameSize),
279
- pointForOverlay(from: observation.topRight, frameSize: frameSize),
280
- pointForOverlay(from: observation.bottomRight, frameSize: frameSize),
281
- pointForOverlay(from: observation.bottomLeft, frameSize: frameSize),
282
- ]
283
-
284
- currentStableCounter = min(currentStableCounter + 1, Int(truncating: detectionCountBeforeCapture))
285
-
286
- let normalizedArea = observation.boundingBox.width * observation.boundingBox.height
287
- let meetsArea = normalizedArea >= 0.06 && normalizedArea <= 0.95
288
- let meetsConfidence = observation.confidence >= 0.65
289
- let shouldDisplayOverlay = currentStableCounter >= overlayStableThreshold && meetsArea && meetsConfidence
290
- updateNativeOverlay(with: shouldDisplayOverlay ? observation : nil)
291
-
292
- payload = [
293
- "rectangleCoordinates": shouldDisplayOverlay ? [
294
- "topLeft": ["x": points[0].x, "y": points[0].y],
295
- "topRight": ["x": points[1].x, "y": points[1].y],
296
- "bottomRight": ["x": points[2].x, "y": points[2].y],
297
- "bottomLeft": ["x": points[3].x, "y": points[3].y],
298
- ] : NSNull(),
299
- "stableCounter": currentStableCounter,
300
- "frameWidth": frameSize.width,
301
- "frameHeight": frameSize.height,
302
- ]
303
- } else {
304
- currentStableCounter = 0
305
- updateNativeOverlay(with: nil)
306
- payload = [
307
- "rectangleCoordinates": NSNull(),
308
- "stableCounter": currentStableCounter,
309
- "frameWidth": frameSize.width,
310
- "frameHeight": frameSize.height,
311
- ]
312
- }
313
-
314
- DispatchQueue.main.async {
315
- onRectangleDetect(payload.compactMapValues { $0 })
316
- }
317
- }
318
-
319
- private func pointForOverlay(from normalizedPoint: CGPoint, frameSize: CGSize) -> CGPoint {
320
- CGPoint(x: normalizedPoint.x * frameSize.width, y: (1 - normalizedPoint.y) * frameSize.height)
321
- }
322
-
323
- private func updateNativeOverlay(with observation: VNRectangleObservation?) {
324
- DispatchQueue.main.async {
325
- guard let observation else {
326
- self.outlineLayer.path = nil
327
- self.gridLayer.path = nil
328
- self.outlineLayer.isHidden = true
329
- self.gridLayer.isHidden = true
330
- self.smoothedOverlayPoints = nil
331
- return
332
- }
333
-
334
- guard let previewLayer = self.previewLayer else {
335
- return
336
- }
337
-
338
- let rawPoints = [
339
- self.convertToLayerPoint(observation.topLeft, previewLayer: previewLayer),
340
- self.convertToLayerPoint(observation.topRight, previewLayer: previewLayer),
341
- self.convertToLayerPoint(observation.bottomRight, previewLayer: previewLayer),
342
- self.convertToLayerPoint(observation.bottomLeft, previewLayer: previewLayer),
343
- ]
344
-
345
- let orderedPoints = self.orderPoints(rawPoints)
346
-
347
- let points: [CGPoint]
348
- if let previous = self.smoothedOverlayPoints, previous.count == 4 {
349
- points = zip(previous, orderedPoints).map { prev, next in
350
- CGPoint(x: prev.x * 0.7 + next.x * 0.3, y: prev.y * 0.7 + next.y * 0.3)
351
- }
352
- } else {
353
- points = orderedPoints
354
- }
355
-
356
- self.smoothedOverlayPoints = points
357
-
358
- let outline = UIBezierPath()
359
- outline.move(to: points[0])
360
- outline.addLine(to: points[1])
361
- outline.addLine(to: points[2])
362
- outline.addLine(to: points[3])
363
- outline.close()
364
-
365
- self.outlineLayer.path = outline.cgPath
366
- self.outlineLayer.isHidden = false
367
-
368
- let gridPath = UIBezierPath()
369
- let steps: [CGFloat] = [1.0 / 3.0, 2.0 / 3.0]
370
-
371
- for step in steps {
372
- let startVertical = self.interpolate(points[0], points[1], t: step)
373
- let endVertical = self.interpolate(points[3], points[2], t: step)
374
- gridPath.move(to: startVertical)
375
- gridPath.addLine(to: endVertical)
376
-
377
- let startHorizontal = self.interpolate(points[0], points[3], t: step)
378
- let endHorizontal = self.interpolate(points[1], points[2], t: step)
379
- gridPath.move(to: startHorizontal)
380
- gridPath.addLine(to: endHorizontal)
381
- }
382
-
383
- self.gridLayer.path = gridPath.cgPath
384
- self.gridLayer.isHidden = false
385
- }
386
- }
387
-
388
- private func convertToLayerPoint(_ normalizedPoint: CGPoint, previewLayer: AVCaptureVideoPreviewLayer) -> CGPoint {
389
- let devicePoint = CGPoint(x: normalizedPoint.x, y: 1 - normalizedPoint.y)
390
- return previewLayer.layerPointConverted(fromCaptureDevicePoint: devicePoint)
391
- }
392
-
393
- private func interpolate(_ start: CGPoint, _ end: CGPoint, t: CGFloat) -> CGPoint {
394
- CGPoint(x: start.x + (end.x - start.x) * t, y: start.y + (end.y - start.y) * t)
395
- }
396
-
397
- private func orderPoints(_ points: [CGPoint]) -> [CGPoint] {
398
- guard points.count == 4 else { return points }
399
-
400
- var topLeft = points[0]
401
- var topRight = points[0]
402
- var bottomRight = points[0]
403
- var bottomLeft = points[0]
404
-
405
- var minSum = CGFloat.greatestFiniteMagnitude
406
- var maxSum = -CGFloat.greatestFiniteMagnitude
407
- var minDiff = CGFloat.greatestFiniteMagnitude
408
- var maxDiff = -CGFloat.greatestFiniteMagnitude
409
-
410
- for point in points {
411
- let sum = point.x + point.y
412
- if sum < minSum {
413
- minSum = sum
414
- topLeft = point
415
- }
416
- if sum > maxSum {
417
- maxSum = sum
418
- bottomRight = point
419
- }
420
-
421
- let diff = point.x - point.y
422
- if diff < minDiff {
423
- minDiff = diff
424
- bottomLeft = point
425
- }
426
- if diff > maxDiff {
427
- maxDiff = diff
428
- topRight = point
429
- }
430
- }
431
-
432
- var ordered = [topLeft, topRight, bottomRight, bottomLeft]
433
- if cross(ordered[0], ordered[1], ordered[2]) < 0 {
434
- ordered = [topLeft, bottomLeft, bottomRight, topRight]
435
- }
436
- return ordered
437
- }
438
-
439
- private func cross(_ a: CGPoint, _ b: CGPoint, _ c: CGPoint) -> CGFloat {
440
- let abx = b.x - a.x
441
- let aby = b.y - a.y
442
- let acx = c.x - a.x
443
- let acy = c.y - a.y
444
- return abx * acy - aby * acx
445
- }
446
-
447
- // MARK: - Capture
448
-
449
- func capture(completion: @escaping (Result<RNRDocScannerCaptureResult, Error>) -> Void) {
450
- sessionQueue.async { [weak self] in
451
- guard let self else { return }
452
-
453
- if isCaptureInFlight {
454
- completion(.failure(RNRDocScannerError.captureInProgress))
455
- return
456
- }
457
-
458
- guard photoOutput.connection(with: .video) != nil else {
459
- completion(.failure(RNRDocScannerError.captureUnavailable))
460
- return
461
- }
462
-
463
- isCaptureInFlight = true
464
- photoCaptureCompletion = completion
465
-
466
- let settings = AVCapturePhotoSettings(format: [AVVideoCodecKey: AVVideoCodecType.jpeg])
467
- settings.isHighResolutionPhotoEnabled = photoOutput.isHighResolutionCaptureEnabled
468
- if photoOutput.supportedFlashModes.contains(.on) {
469
- settings.flashMode = enableTorch ? .on : .off
470
- }
471
-
472
- photoOutput.capturePhoto(with: settings, delegate: self)
473
- }
474
- }
475
-
476
- func resetStability() {
477
- currentStableCounter = 0
478
- lastObservation = nil
479
- }
480
-
481
- // MARK: - AVCapturePhotoCaptureDelegate
482
-
483
- func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) {
484
- guard let completion = photoCaptureCompletion else {
485
- isCaptureInFlight = false
486
- return
487
- }
488
-
489
- if let error {
490
- finishCapture(result: .failure(error))
491
- return
492
- }
493
-
494
- guard let data = photo.fileDataRepresentation() else {
495
- finishCapture(result: .failure(RNRDocScannerError.imageCreationFailed))
496
- return
497
- }
498
-
499
- let dimensions = photoDimensions(photo: photo)
500
- do {
501
- let original = try serializeImageData(data, suffix: "original")
502
- let croppedString: String?
503
-
504
- if let croppedData = generateCroppedImage(from: data) {
505
- croppedString = try serializeImageData(croppedData, suffix: "cropped").string
506
- } else {
507
- croppedString = original.string
508
- }
509
-
510
- let result = RNRDocScannerCaptureResult(
511
- croppedImage: croppedString,
512
- originalImage: original.string,
513
- width: dimensions.width,
514
- height: dimensions.height
515
- )
516
-
517
- finishCapture(result: .success(result))
518
- } catch {
519
- finishCapture(result: .failure(error))
520
- }
521
- }
522
-
523
- func photoOutput(_ output: AVCapturePhotoOutput, didFinishCaptureFor resolvedSettings: AVCaptureResolvedPhotoSettings, error: Error?) {
524
- if let error, isCaptureInFlight {
525
- finishCapture(result: .failure(error))
526
- }
527
- }
528
-
529
- private func finishCapture(result: Result<RNRDocScannerCaptureResult, Error>) {
530
- let completion = photoCaptureCompletion
531
- photoCaptureCompletion = nil
532
- isCaptureInFlight = false
533
-
534
- DispatchQueue.main.async {
535
- switch result {
536
- case let .success(payload):
537
- completion?(.success(payload))
538
- self.emitPictureTaken(payload)
539
- case let .failure(error):
540
- completion?(.failure(error))
541
- }
542
- }
543
- }
544
-
545
- private func emitPictureTaken(_ result: RNRDocScannerCaptureResult) {
546
- guard let onPictureTaken else { return }
547
- let payload: [String: Any] = [
548
- "croppedImage": result.croppedImage ?? NSNull(),
549
- "initialImage": result.originalImage,
550
- "width": result.width,
551
- "height": result.height,
552
- ]
553
- onPictureTaken(payload)
554
- }
555
-
556
- // MARK: - Helpers
557
-
558
- private func currentExifOrientation() -> CGImagePropertyOrientation {
559
- switch UIDevice.current.orientation {
560
- case .landscapeLeft:
561
- return .up
562
- case .landscapeRight:
563
- return .down
564
- case .portraitUpsideDown:
565
- return .left
566
- default:
567
- return .right
568
- }
569
- }
570
-
571
- private func photoDimensions(photo: AVCapturePhoto) -> CGSize {
572
- if let pixelBuffer = photo.pixelBuffer {
573
- return CGSize(width: CVPixelBufferGetWidth(pixelBuffer), height: CVPixelBufferGetHeight(pixelBuffer))
574
- }
575
-
576
- let width = photo.metadata[kCGImagePropertyPixelWidth as String] as? Int ?? Int(lastFrameSize.width)
577
- let height = photo.metadata[kCGImagePropertyPixelHeight as String] as? Int ?? Int(lastFrameSize.height)
578
- return CGSize(width: CGFloat(width), height: CGFloat(height))
579
- }
580
-
581
- private func serializeImageData(_ data: Data, suffix: String) throws -> (string: String, url: URL?) {
582
- let filename = "docscan-\(UUID().uuidString)-\(suffix).jpg"
583
- let url = FileManager.default.temporaryDirectory.appendingPathComponent(filename)
584
- do {
585
- try data.write(to: url, options: .atomic)
586
- } catch {
587
- throw RNRDocScannerError.fileWriteFailed
588
- }
589
- return (url.absoluteString, url)
590
- }
591
-
592
- private func generateCroppedImage(from data: Data) -> Data? {
593
- guard let ciImage = CIImage(data: data) else {
594
- return nil
595
- }
596
-
597
- var observation: VNRectangleObservation? = nil
598
- let request = VNDetectRectanglesRequest { request, _ in
599
- observation = (request.results as? [VNRectangleObservation])?.first
600
- }
601
- request.maximumObservations = 1
602
- request.minimumConfidence = 0.6
603
-
604
- let handler = VNImageRequestHandler(ciImage: ciImage, options: [:])
605
- try? handler.perform([request])
606
-
607
- guard let targetObservation = observation ?? lastObservation else {
608
- return nil
609
- }
610
-
611
- let size = ciImage.extent.size
612
- let topLeft = normalizedPoint(targetObservation.topLeft, in: size, flipY: false)
613
- let topRight = normalizedPoint(targetObservation.topRight, in: size, flipY: false)
614
- let bottomLeft = normalizedPoint(targetObservation.bottomLeft, in: size, flipY: false)
615
- let bottomRight = normalizedPoint(targetObservation.bottomRight, in: size, flipY: false)
616
-
617
- guard let filter = CIFilter(name: "CIPerspectiveCorrection") else {
618
- return nil
619
- }
620
-
621
- filter.setValue(ciImage, forKey: kCIInputImageKey)
622
- filter.setValue(CIVector(cgPoint: topLeft), forKey: "inputTopLeft")
623
- filter.setValue(CIVector(cgPoint: topRight), forKey: "inputTopRight")
624
- filter.setValue(CIVector(cgPoint: bottomLeft), forKey: "inputBottomLeft")
625
- filter.setValue(CIVector(cgPoint: bottomRight), forKey: "inputBottomRight")
626
-
627
- guard let corrected = filter.outputImage else {
628
- return nil
629
- }
630
-
631
- guard let cgImage = ciContext.createCGImage(corrected, from: corrected.extent) else {
632
- return nil
633
- }
634
-
635
- let cropped = UIImage(cgImage: cgImage)
636
- return cropped.jpegData(compressionQuality: CGFloat(max(0.05, min(1.0, quality.doubleValue / 100.0))))
637
- }
638
-
639
- private func normalizedPoint(_ point: CGPoint, in size: CGSize, flipY: Bool) -> CGPoint {
640
- let yValue = flipY ? (1 - point.y) : point.y
641
- return CGPoint(x: point.x * size.width, y: yValue * size.height)
642
- }
643
- }
644
-
645
- struct RNRDocScannerCaptureResult {
646
- let croppedImage: String?
647
- let originalImage: String
648
- let width: CGFloat
649
- let height: CGFloat
650
- }
651
-
652
- enum RNRDocScannerError: Error {
653
- case captureInProgress
654
- case captureUnavailable
655
- case imageCreationFailed
656
- case fileWriteFailed
657
- case viewNotFound
658
-
659
- var code: String {
660
- switch self {
661
- case .captureInProgress:
662
- return "capture_in_progress"
663
- case .captureUnavailable:
664
- return "capture_unavailable"
665
- case .imageCreationFailed:
666
- return "image_creation_failed"
667
- case .fileWriteFailed:
668
- return "file_write_failed"
669
- case .viewNotFound:
670
- return "view_not_found"
671
- }
672
- }
673
-
674
- var message: String {
675
- switch self {
676
- case .captureInProgress:
677
- return "A capture request is already in flight."
678
- case .captureUnavailable:
679
- return "Photo output is not configured yet."
680
- case .imageCreationFailed:
681
- return "Unable to create image data from capture."
682
- case .fileWriteFailed:
683
- return "Failed to persist captured image to disk."
684
- case .viewNotFound:
685
- return "Unable to locate the native DocScanner view."
686
- }
687
- }
688
- }
689
-
690
- private extension CGRect {
691
- var area: CGFloat {
692
- width * height
693
- }
694
- }
@@ -1,22 +0,0 @@
1
- #import <React/RCTBridge.h>
2
- #import <React/RCTUIManager.h>
3
- #import <React/RCTViewManager.h>
4
-
5
- // Swift bridging header not needed with @objc annotations in Swift files
6
- // #import "react-native-rectangle-doc-scanner-Swift.h"
7
-
8
- @interface RCT_EXTERN_MODULE(RNRDocScannerViewManager, RCTViewManager)
9
- RCT_EXPORT_VIEW_PROPERTY(detectionCountBeforeCapture, NSNumber)
10
- RCT_EXPORT_VIEW_PROPERTY(autoCapture, BOOL)
11
- RCT_EXPORT_VIEW_PROPERTY(enableTorch, BOOL)
12
- RCT_EXPORT_VIEW_PROPERTY(quality, NSNumber)
13
- RCT_EXPORT_VIEW_PROPERTY(useBase64, BOOL)
14
- RCT_EXPORT_VIEW_PROPERTY(onRectangleDetect, RCTDirectEventBlock)
15
- RCT_EXPORT_VIEW_PROPERTY(onPictureTaken, RCTDirectEventBlock)
16
-
17
- RCT_EXTERN_METHOD(capture:(nonnull NSNumber *)reactTag
18
- resolver:(RCTPromiseResolveBlock)resolve
19
- rejecter:(RCTPromiseRejectBlock)reject)
20
-
21
- RCT_EXTERN_METHOD(reset:(nonnull NSNumber *)reactTag)
22
- @end