react-native-rectangle-doc-scanner 1.12.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,674 +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.55
230
- request.minimumAspectRatio = 0.1
231
- request.maximumAspectRatio = 2.0
232
- request.minimumSize = 0.05
233
- if #available(iOS 13.0, *) {
234
- request.quadratureTolerance = 20
235
- }
236
-
237
- let handler = VNImageRequestHandler(cvPixelBuffer: pixelBuffer, orientation: orientation, options: [:])
238
- do {
239
- try handler.perform([request])
240
- } catch {
241
- NSLog("[RNRDocScanner] Failed to run Vision request: \(error)")
242
- lastObservation = nil
243
- handleDetectedRectangle(nil, frameSize: frameSize)
244
- }
245
- }
246
-
247
- func handleDetectedRectangle(_ rectangle: VNRectangleObservation?, frameSize: CGSize) {
248
- guard let onRectangleDetect else { return }
249
-
250
- let effectiveObservation: VNRectangleObservation?
251
- if let rect = rectangle {
252
- effectiveObservation = rect
253
- lastObservation = rect
254
- missedDetectionFrames = 0
255
- } else if missedDetectionFrames < maxMissedDetections, let cached = lastObservation {
256
- missedDetectionFrames += 1
257
- effectiveObservation = cached
258
- } else {
259
- lastObservation = nil
260
- missedDetectionFrames = 0
261
- smoothedOverlayPoints = nil
262
- effectiveObservation = nil
263
- }
264
-
265
- let overlayStableThreshold = max(2, Int(truncating: detectionCountBeforeCapture) / 2)
266
- let payload: [String: Any?]
267
-
268
- if let observation = effectiveObservation {
269
- let points = [
270
- pointForOverlay(from: observation.topLeft, frameSize: frameSize),
271
- pointForOverlay(from: observation.topRight, frameSize: frameSize),
272
- pointForOverlay(from: observation.bottomRight, frameSize: frameSize),
273
- pointForOverlay(from: observation.bottomLeft, frameSize: frameSize),
274
- ]
275
-
276
- currentStableCounter = min(currentStableCounter + 1, Int(truncating: detectionCountBeforeCapture))
277
-
278
- let normalizedArea = observation.boundingBox.width * observation.boundingBox.height
279
- let meetsArea = normalizedArea >= 0.06 && normalizedArea <= 0.95
280
- let meetsConfidence = observation.confidence >= 0.65
281
- let shouldDisplayOverlay = currentStableCounter >= overlayStableThreshold && meetsArea && meetsConfidence
282
- updateNativeOverlay(with: shouldDisplayOverlay ? observation : nil)
283
-
284
- payload = [
285
- "rectangleCoordinates": shouldDisplayOverlay ? [
286
- "topLeft": ["x": points[0].x, "y": points[0].y],
287
- "topRight": ["x": points[1].x, "y": points[1].y],
288
- "bottomRight": ["x": points[2].x, "y": points[2].y],
289
- "bottomLeft": ["x": points[3].x, "y": points[3].y],
290
- ] : NSNull(),
291
- "stableCounter": currentStableCounter,
292
- "frameWidth": frameSize.width,
293
- "frameHeight": frameSize.height,
294
- ]
295
- } else {
296
- currentStableCounter = 0
297
- updateNativeOverlay(with: nil)
298
- payload = [
299
- "rectangleCoordinates": NSNull(),
300
- "stableCounter": currentStableCounter,
301
- "frameWidth": frameSize.width,
302
- "frameHeight": frameSize.height,
303
- ]
304
- }
305
-
306
- DispatchQueue.main.async {
307
- onRectangleDetect(payload.compactMapValues { $0 })
308
- }
309
- }
310
-
311
- private func pointForOverlay(from normalizedPoint: CGPoint, frameSize: CGSize) -> CGPoint {
312
- CGPoint(x: normalizedPoint.x * frameSize.width, y: (1 - normalizedPoint.y) * frameSize.height)
313
- }
314
-
315
- private func updateNativeOverlay(with observation: VNRectangleObservation?) {
316
- DispatchQueue.main.async {
317
- guard let observation else {
318
- self.outlineLayer.path = nil
319
- self.gridLayer.path = nil
320
- self.outlineLayer.isHidden = true
321
- self.gridLayer.isHidden = true
322
- self.smoothedOverlayPoints = nil
323
- return
324
- }
325
-
326
- guard let previewLayer = self.previewLayer else {
327
- return
328
- }
329
-
330
- let rawPoints = [
331
- self.convertToLayerPoint(observation.topLeft, previewLayer: previewLayer),
332
- self.convertToLayerPoint(observation.topRight, previewLayer: previewLayer),
333
- self.convertToLayerPoint(observation.bottomRight, previewLayer: previewLayer),
334
- self.convertToLayerPoint(observation.bottomLeft, previewLayer: previewLayer),
335
- ]
336
-
337
- let orderedPoints = self.orderPoints(rawPoints)
338
-
339
- let points: [CGPoint]
340
- if let previous = self.smoothedOverlayPoints, previous.count == 4 {
341
- points = zip(previous, orderedPoints).map { prev, next in
342
- CGPoint(x: prev.x * 0.75 + next.x * 0.25, y: prev.y * 0.75 + next.y * 0.25)
343
- }
344
- } else {
345
- points = orderedPoints
346
- }
347
-
348
- self.smoothedOverlayPoints = points
349
-
350
- let outline = UIBezierPath()
351
- outline.move(to: points[0])
352
- outline.addLine(to: points[1])
353
- outline.addLine(to: points[2])
354
- outline.addLine(to: points[3])
355
- outline.close()
356
-
357
- self.outlineLayer.path = outline.cgPath
358
- self.outlineLayer.isHidden = false
359
-
360
- let gridPath = UIBezierPath()
361
- let steps: [CGFloat] = [1.0 / 3.0, 2.0 / 3.0]
362
-
363
- for step in steps {
364
- let startVertical = self.interpolate(points[0], points[1], t: step)
365
- let endVertical = self.interpolate(points[3], points[2], t: step)
366
- gridPath.move(to: startVertical)
367
- gridPath.addLine(to: endVertical)
368
-
369
- let startHorizontal = self.interpolate(points[0], points[3], t: step)
370
- let endHorizontal = self.interpolate(points[1], points[2], t: step)
371
- gridPath.move(to: startHorizontal)
372
- gridPath.addLine(to: endHorizontal)
373
- }
374
-
375
- self.gridLayer.path = gridPath.cgPath
376
- self.gridLayer.isHidden = false
377
- }
378
- }
379
-
380
- private func convertToLayerPoint(_ normalizedPoint: CGPoint, previewLayer: AVCaptureVideoPreviewLayer) -> CGPoint {
381
- let devicePoint = CGPoint(x: normalizedPoint.x, y: 1 - normalizedPoint.y)
382
- return previewLayer.layerPointConverted(fromCaptureDevicePoint: devicePoint)
383
- }
384
-
385
- private func interpolate(_ start: CGPoint, _ end: CGPoint, t: CGFloat) -> CGPoint {
386
- CGPoint(x: start.x + (end.x - start.x) * t, y: start.y + (end.y - start.y) * t)
387
- }
388
-
389
- private func orderPoints(_ points: [CGPoint]) -> [CGPoint] {
390
- guard points.count == 4 else { return points }
391
-
392
- var topLeft = points[0]
393
- var topRight = points[0]
394
- var bottomRight = points[0]
395
- var bottomLeft = points[0]
396
-
397
- var minSum = CGFloat.greatestFiniteMagnitude
398
- var maxSum = -CGFloat.greatestFiniteMagnitude
399
- var minDiff = CGFloat.greatestFiniteMagnitude
400
- var maxDiff = -CGFloat.greatestFiniteMagnitude
401
-
402
- for point in points {
403
- let sum = point.x + point.y
404
- if sum < minSum {
405
- minSum = sum
406
- topLeft = point
407
- }
408
- if sum > maxSum {
409
- maxSum = sum
410
- bottomRight = point
411
- }
412
-
413
- let diff = point.x - point.y
414
- if diff < minDiff {
415
- minDiff = diff
416
- bottomLeft = point
417
- }
418
- if diff > maxDiff {
419
- maxDiff = diff
420
- topRight = point
421
- }
422
- }
423
-
424
- return [topLeft, topRight, bottomRight, bottomLeft]
425
- }
426
-
427
- // MARK: - Capture
428
-
429
- func capture(completion: @escaping (Result<RNRDocScannerCaptureResult, Error>) -> Void) {
430
- sessionQueue.async { [weak self] in
431
- guard let self else { return }
432
-
433
- if isCaptureInFlight {
434
- completion(.failure(RNRDocScannerError.captureInProgress))
435
- return
436
- }
437
-
438
- guard photoOutput.connection(with: .video) != nil else {
439
- completion(.failure(RNRDocScannerError.captureUnavailable))
440
- return
441
- }
442
-
443
- isCaptureInFlight = true
444
- photoCaptureCompletion = completion
445
-
446
- let settings = AVCapturePhotoSettings(format: [AVVideoCodecKey: AVVideoCodecType.jpeg])
447
- settings.isHighResolutionPhotoEnabled = photoOutput.isHighResolutionCaptureEnabled
448
- if photoOutput.supportedFlashModes.contains(.on) {
449
- settings.flashMode = enableTorch ? .on : .off
450
- }
451
-
452
- photoOutput.capturePhoto(with: settings, delegate: self)
453
- }
454
- }
455
-
456
- func resetStability() {
457
- currentStableCounter = 0
458
- lastObservation = nil
459
- }
460
-
461
- // MARK: - AVCapturePhotoCaptureDelegate
462
-
463
- func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) {
464
- guard let completion = photoCaptureCompletion else {
465
- isCaptureInFlight = false
466
- return
467
- }
468
-
469
- if let error {
470
- finishCapture(result: .failure(error))
471
- return
472
- }
473
-
474
- guard let data = photo.fileDataRepresentation() else {
475
- finishCapture(result: .failure(RNRDocScannerError.imageCreationFailed))
476
- return
477
- }
478
-
479
- let dimensions = photoDimensions(photo: photo)
480
- do {
481
- let original = try serializeImageData(data, suffix: "original")
482
- let croppedString: String?
483
-
484
- if let croppedData = generateCroppedImage(from: data) {
485
- croppedString = try serializeImageData(croppedData, suffix: "cropped").string
486
- } else {
487
- croppedString = original.string
488
- }
489
-
490
- let result = RNRDocScannerCaptureResult(
491
- croppedImage: croppedString,
492
- originalImage: original.string,
493
- width: dimensions.width,
494
- height: dimensions.height
495
- )
496
-
497
- finishCapture(result: .success(result))
498
- } catch {
499
- finishCapture(result: .failure(error))
500
- }
501
- }
502
-
503
- func photoOutput(_ output: AVCapturePhotoOutput, didFinishCaptureFor resolvedSettings: AVCaptureResolvedPhotoSettings, error: Error?) {
504
- if let error, isCaptureInFlight {
505
- finishCapture(result: .failure(error))
506
- }
507
- }
508
-
509
- private func finishCapture(result: Result<RNRDocScannerCaptureResult, Error>) {
510
- let completion = photoCaptureCompletion
511
- photoCaptureCompletion = nil
512
- isCaptureInFlight = false
513
-
514
- DispatchQueue.main.async {
515
- switch result {
516
- case let .success(payload):
517
- completion?(.success(payload))
518
- self.emitPictureTaken(payload)
519
- case let .failure(error):
520
- completion?(.failure(error))
521
- }
522
- }
523
- }
524
-
525
- private func emitPictureTaken(_ result: RNRDocScannerCaptureResult) {
526
- guard let onPictureTaken else { return }
527
- let payload: [String: Any] = [
528
- "croppedImage": result.croppedImage ?? NSNull(),
529
- "initialImage": result.originalImage,
530
- "width": result.width,
531
- "height": result.height,
532
- ]
533
- onPictureTaken(payload)
534
- }
535
-
536
- // MARK: - Helpers
537
-
538
- private func currentExifOrientation() -> CGImagePropertyOrientation {
539
- switch UIDevice.current.orientation {
540
- case .landscapeLeft:
541
- return .up
542
- case .landscapeRight:
543
- return .down
544
- case .portraitUpsideDown:
545
- return .left
546
- default:
547
- return .right
548
- }
549
- }
550
-
551
- private func photoDimensions(photo: AVCapturePhoto) -> CGSize {
552
- if let pixelBuffer = photo.pixelBuffer {
553
- return CGSize(width: CVPixelBufferGetWidth(pixelBuffer), height: CVPixelBufferGetHeight(pixelBuffer))
554
- }
555
-
556
- let width = photo.metadata[kCGImagePropertyPixelWidth as String] as? Int ?? Int(lastFrameSize.width)
557
- let height = photo.metadata[kCGImagePropertyPixelHeight as String] as? Int ?? Int(lastFrameSize.height)
558
- return CGSize(width: CGFloat(width), height: CGFloat(height))
559
- }
560
-
561
- private func serializeImageData(_ data: Data, suffix: String) throws -> (string: String, url: URL?) {
562
- let filename = "docscan-\(UUID().uuidString)-\(suffix).jpg"
563
- let url = FileManager.default.temporaryDirectory.appendingPathComponent(filename)
564
- do {
565
- try data.write(to: url, options: .atomic)
566
- } catch {
567
- throw RNRDocScannerError.fileWriteFailed
568
- }
569
- return (url.absoluteString, url)
570
- }
571
-
572
- private func generateCroppedImage(from data: Data) -> Data? {
573
- guard let ciImage = CIImage(data: data) else {
574
- return nil
575
- }
576
-
577
- var observation: VNRectangleObservation? = nil
578
- let request = VNDetectRectanglesRequest { request, _ in
579
- observation = (request.results as? [VNRectangleObservation])?.first
580
- }
581
- request.maximumObservations = 1
582
- request.minimumConfidence = 0.6
583
-
584
- let handler = VNImageRequestHandler(ciImage: ciImage, options: [:])
585
- try? handler.perform([request])
586
-
587
- guard let targetObservation = observation ?? lastObservation else {
588
- return nil
589
- }
590
-
591
- let size = ciImage.extent.size
592
- let topLeft = normalizedPoint(targetObservation.topLeft, in: size, flipY: false)
593
- let topRight = normalizedPoint(targetObservation.topRight, in: size, flipY: false)
594
- let bottomLeft = normalizedPoint(targetObservation.bottomLeft, in: size, flipY: false)
595
- let bottomRight = normalizedPoint(targetObservation.bottomRight, in: size, flipY: false)
596
-
597
- guard let filter = CIFilter(name: "CIPerspectiveCorrection") else {
598
- return nil
599
- }
600
-
601
- filter.setValue(ciImage, forKey: kCIInputImageKey)
602
- filter.setValue(CIVector(cgPoint: topLeft), forKey: "inputTopLeft")
603
- filter.setValue(CIVector(cgPoint: topRight), forKey: "inputTopRight")
604
- filter.setValue(CIVector(cgPoint: bottomLeft), forKey: "inputBottomLeft")
605
- filter.setValue(CIVector(cgPoint: bottomRight), forKey: "inputBottomRight")
606
-
607
- guard let corrected = filter.outputImage else {
608
- return nil
609
- }
610
-
611
- guard let cgImage = ciContext.createCGImage(corrected, from: corrected.extent) else {
612
- return nil
613
- }
614
-
615
- let cropped = UIImage(cgImage: cgImage)
616
- return cropped.jpegData(compressionQuality: CGFloat(max(0.05, min(1.0, quality.doubleValue / 100.0))))
617
- }
618
-
619
- private func normalizedPoint(_ point: CGPoint, in size: CGSize, flipY: Bool) -> CGPoint {
620
- let yValue = flipY ? (1 - point.y) : point.y
621
- return CGPoint(x: point.x * size.width, y: yValue * size.height)
622
- }
623
- }
624
-
625
- struct RNRDocScannerCaptureResult {
626
- let croppedImage: String?
627
- let originalImage: String
628
- let width: CGFloat
629
- let height: CGFloat
630
- }
631
-
632
- enum RNRDocScannerError: Error {
633
- case captureInProgress
634
- case captureUnavailable
635
- case imageCreationFailed
636
- case fileWriteFailed
637
- case viewNotFound
638
-
639
- var code: String {
640
- switch self {
641
- case .captureInProgress:
642
- return "capture_in_progress"
643
- case .captureUnavailable:
644
- return "capture_unavailable"
645
- case .imageCreationFailed:
646
- return "image_creation_failed"
647
- case .fileWriteFailed:
648
- return "file_write_failed"
649
- case .viewNotFound:
650
- return "view_not_found"
651
- }
652
- }
653
-
654
- var message: String {
655
- switch self {
656
- case .captureInProgress:
657
- return "A capture request is already in flight."
658
- case .captureUnavailable:
659
- return "Photo output is not configured yet."
660
- case .imageCreationFailed:
661
- return "Unable to create image data from capture."
662
- case .fileWriteFailed:
663
- return "Failed to persist captured image to disk."
664
- case .viewNotFound:
665
- return "Unable to locate the native DocScanner view."
666
- }
667
- }
668
- }
669
-
670
- private extension CGRect {
671
- var area: CGFloat {
672
- width * height
673
- }
674
- }
@@ -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