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,47 +0,0 @@
1
- import Foundation
2
- import React
3
-
4
- @objc(RNRDocScannerViewManager)
5
- class RNRDocScannerViewManager: RCTViewManager {
6
- override static func requiresMainQueueSetup() -> Bool {
7
- true
8
- }
9
-
10
- override func view() -> UIView! {
11
- RNRDocScannerView()
12
- }
13
-
14
- @objc func capture(_ reactTag: NSNumber, resolver resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) {
15
- bridge.uiManager.addUIBlock { _, viewRegistry in
16
- guard let view = viewRegistry?[reactTag] as? RNRDocScannerView else {
17
- reject(RNRDocScannerError.viewNotFound.code, RNRDocScannerError.viewNotFound.message, nil)
18
- return
19
- }
20
-
21
- view.capture { result in
22
- switch result {
23
- case let .success(payload):
24
- resolve([
25
- "croppedImage": payload.croppedImage ?? NSNull(),
26
- "initialImage": payload.originalImage,
27
- "width": payload.width,
28
- "height": payload.height,
29
- ])
30
- case let .failure(error as RNRDocScannerError):
31
- reject(error.code, error.message, error)
32
- case let .failure(error):
33
- reject("capture_failed", error.localizedDescription, error)
34
- }
35
- }
36
- }
37
- }
38
-
39
- @objc func reset(_ reactTag: NSNumber) {
40
- bridge.uiManager.addUIBlock { _, viewRegistry in
41
- guard let view = viewRegistry?[reactTag] as? RNRDocScannerView else {
42
- return
43
- }
44
- view.resetStability()
45
- }
46
- }
47
- }
@@ -1,22 +0,0 @@
1
- require 'json'
2
-
3
- package = JSON.parse(File.read(File.join(__dir__, 'package.json')))
4
-
5
- Pod::Spec.new do |s|
6
- s.name = 'react-native-rectangle-doc-scanner'
7
- s.version = package['version']
8
- s.summary = package.fetch('description', 'Document scanner with native camera overlay support for React Native.')
9
- s.homepage = package['homepage'] || 'https://github.com/danchew90/react-native-rectangle-doc-scanner'
10
- s.license = package['license'] || { :type => 'MIT' }
11
- s.author = package['author'] || { 'react-native-rectangle-doc-scanner' => 'opensource@example.com' }
12
- s.source = { :git => package.dig('repository', 'url') || s.homepage, :tag => "v#{s.version}" }
13
-
14
- s.platform = :ios, '13.0'
15
- s.swift_version = '5.0'
16
-
17
- s.source_files = 'ios/**/*.{h,m,mm,swift}'
18
- s.public_header_files = 'ios/**/*.h'
19
- s.requires_arc = true
20
-
21
- s.dependency 'React-Core'
22
- end
@@ -1,208 +0,0 @@
1
- import React, { useMemo } from 'react';
2
- import { View, StyleSheet, useWindowDimensions } from 'react-native';
3
- import { Canvas, Path, Skia } from '@shopify/react-native-skia';
4
- import type { Point } from '../types';
5
-
6
- const lerp = (start: Point, end: Point, t: number): Point => ({
7
- x: start.x + (end.x - start.x) * t,
8
- y: start.y + (end.y - start.y) * t,
9
- });
10
-
11
- const withAlpha = (value: string, alpha: number): string => {
12
- const hexMatch = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(value.trim());
13
- if (!hexMatch) {
14
- return `rgba(231, 166, 73, ${alpha})`;
15
- }
16
-
17
- const hex = hexMatch[1];
18
- const normalize = hex.length === 3
19
- ? hex.split('').map((ch) => ch + ch).join('')
20
- : hex;
21
-
22
- const r = parseInt(normalize.slice(0, 2), 16);
23
- const g = parseInt(normalize.slice(2, 4), 16);
24
- const b = parseInt(normalize.slice(4, 6), 16);
25
- return `rgba(${r}, ${g}, ${b}, ${alpha})`;
26
- };
27
-
28
- type OverlayProps = {
29
- quad: Point[] | null;
30
- color?: string;
31
- frameSize: { width: number; height: number } | null;
32
- showGrid?: boolean;
33
- gridColor?: string;
34
- gridLineWidth?: number;
35
- };
36
-
37
- type OverlayGeometry = {
38
- outlinePath: ReturnType<typeof Skia.Path.Make> | null;
39
- gridPaths: ReturnType<typeof Skia.Path.Make>[];
40
- };
41
-
42
- const buildPath = (points: Point[]) => {
43
- const path = Skia.Path.Make();
44
- path.moveTo(points[0].x, points[0].y);
45
- points.slice(1).forEach((p) => path.lineTo(p.x, p.y));
46
- path.close();
47
- return path;
48
- };
49
-
50
- const orderQuad = (points: Point[]): Point[] => {
51
- if (points.length !== 4) {
52
- return points;
53
- }
54
-
55
- const center = points.reduce(
56
- (acc, point) => ({ x: acc.x + point.x / 4, y: acc.y + point.y / 4 }),
57
- { x: 0, y: 0 },
58
- );
59
-
60
- const sorted = [...points].sort((a, b) => {
61
- const angleA = Math.atan2(a.y - center.y, a.x - center.x);
62
- const angleB = Math.atan2(b.y - center.y, b.x - center.x);
63
- return angleA - angleB;
64
- });
65
-
66
- // Ensure the first point is the top-left (smallest y, then smallest x)
67
- let startIndex = 0;
68
- for (let i = 1; i < sorted.length; i += 1) {
69
- const current = sorted[i];
70
- const candidate = sorted[startIndex];
71
- if (current.y < candidate.y || (current.y === candidate.y && current.x < candidate.x)) {
72
- startIndex = i;
73
- }
74
- }
75
-
76
- return [
77
- sorted[startIndex % 4],
78
- sorted[(startIndex + 1) % 4],
79
- sorted[(startIndex + 2) % 4],
80
- sorted[(startIndex + 3) % 4],
81
- ];
82
- };
83
-
84
- export const Overlay: React.FC<OverlayProps> = ({
85
- quad,
86
- color = '#e7a649',
87
- frameSize,
88
- showGrid = true,
89
- gridColor = 'rgba(231, 166, 73, 0.35)',
90
- gridLineWidth = 2,
91
- }) => {
92
- const { width: screenWidth, height: screenHeight } = useWindowDimensions();
93
- const fillColor = useMemo(() => withAlpha(color, 0.2), [color]);
94
-
95
- const { outlinePath, gridPaths }: OverlayGeometry = useMemo(() => {
96
- let transformedQuad: Point[] | null = null;
97
- let sourceQuad: Point[] | null = null;
98
- let sourceFrameSize = frameSize;
99
-
100
- if (quad && frameSize) {
101
- sourceQuad = quad;
102
- } else {
103
- // No detection yet – skip drawing
104
- return { outlinePath: null, gridPaths: [] };
105
- }
106
-
107
- if (sourceQuad && sourceFrameSize) {
108
- if (__DEV__) {
109
- console.log('[Overlay] drawing quad:', sourceQuad);
110
- console.log('[Overlay] color:', color);
111
- console.log('[Overlay] screen dimensions:', screenWidth, 'x', screenHeight);
112
- console.log('[Overlay] frame dimensions:', sourceFrameSize.width, 'x', sourceFrameSize.height);
113
- }
114
-
115
- const isFrameLandscape = sourceFrameSize.width > sourceFrameSize.height;
116
- const isScreenPortrait = screenHeight > screenWidth;
117
- const needsRotation = isFrameLandscape && isScreenPortrait;
118
-
119
- if (needsRotation) {
120
- const scaleX = screenWidth / sourceFrameSize.height;
121
- const scaleY = screenHeight / sourceFrameSize.width;
122
-
123
- transformedQuad = sourceQuad.map((p) => ({
124
- x: p.y * scaleX,
125
- y: (sourceFrameSize.width - p.x) * scaleY,
126
- }));
127
- } else {
128
- const scaleX = screenWidth / sourceFrameSize.width;
129
- const scaleY = screenHeight / sourceFrameSize.height;
130
-
131
- transformedQuad = sourceQuad.map((p) => ({
132
- x: p.x * scaleX,
133
- y: p.y * scaleY,
134
- }));
135
- }
136
- }
137
-
138
- if (!transformedQuad) {
139
- return { outlinePath: null, gridPaths: [] };
140
- }
141
-
142
- const normalizedQuad = orderQuad(transformedQuad);
143
- const skPath = buildPath(normalizedQuad);
144
- const grid: ReturnType<typeof Skia.Path.Make>[] = [];
145
-
146
- if (showGrid) {
147
- const [topLeft, topRight, bottomRight, bottomLeft] = normalizedQuad;
148
- const steps = [1 / 3, 2 / 3];
149
-
150
- steps.forEach((t) => {
151
- const start = lerp(topLeft, topRight, t);
152
- const end = lerp(bottomLeft, bottomRight, t);
153
- const verticalPath = Skia.Path.Make();
154
- verticalPath.moveTo(start.x, start.y);
155
- verticalPath.lineTo(end.x, end.y);
156
- grid.push(verticalPath);
157
- });
158
-
159
- steps.forEach((t) => {
160
- const start = lerp(topLeft, bottomLeft, t);
161
- const end = lerp(topRight, bottomRight, t);
162
- const horizontalPath = Skia.Path.Make();
163
- horizontalPath.moveTo(start.x, start.y);
164
- horizontalPath.lineTo(end.x, end.y);
165
- grid.push(horizontalPath);
166
- });
167
- }
168
-
169
- return { outlinePath: skPath, gridPaths: grid };
170
- }, [quad, screenWidth, screenHeight, frameSize, showGrid, color]);
171
-
172
- if (__DEV__) {
173
- console.log('[Overlay] rendering Canvas with dimensions:', screenWidth, 'x', screenHeight);
174
- }
175
-
176
- return (
177
- <View style={styles.container} pointerEvents="none">
178
- <Canvas style={{ width: screenWidth, height: screenHeight }}>
179
- {outlinePath && (
180
- <>
181
- <Path path={outlinePath} color={color} style="stroke" strokeWidth={8} />
182
- <Path path={outlinePath} color={fillColor} style="fill" />
183
- {gridPaths.map((gridPath, index) => (
184
- <Path
185
- // eslint-disable-next-line react/no-array-index-key
186
- key={`grid-${index}`}
187
- path={gridPath}
188
- color={gridColor}
189
- style="stroke"
190
- strokeWidth={gridLineWidth}
191
- />
192
- ))}
193
- </>
194
- )}
195
- </Canvas>
196
- </View>
197
- );
198
- };
199
-
200
- const styles = StyleSheet.create({
201
- container: {
202
- position: 'absolute',
203
- top: 0,
204
- left: 0,
205
- right: 0,
206
- bottom: 0,
207
- },
208
- });
package/src/utils/quad.ts DELETED
@@ -1,181 +0,0 @@
1
- import type { Point } from '../types';
2
-
3
- const POINT_EPSILON = 1e-3;
4
-
5
- const isFiniteNumber = (value: number): boolean => Number.isFinite(value) && !Number.isNaN(value);
6
-
7
- export const isValidPoint = (point: Point | null | undefined): point is Point => {
8
- if (!point) {
9
- return false;
10
- }
11
-
12
- if (!isFiniteNumber(point.x) || !isFiniteNumber(point.y)) {
13
- return false;
14
- }
15
-
16
- if (Math.abs(point.x) > 1_000_000 || Math.abs(point.y) > 1_000_000) {
17
- return false;
18
- }
19
-
20
- return true;
21
- };
22
-
23
- export const isValidQuad = (quad: Point[] | null | undefined): quad is Point[] => {
24
- return Array.isArray(quad) && quad.length === 4 && quad.every(isValidPoint);
25
- };
26
-
27
- const clonePoint = (point: Point): Point => ({ x: point.x, y: point.y });
28
-
29
- export const orderQuadPoints = (quad: Point[]): Point[] => {
30
- const centroid = quad.reduce(
31
- (acc, point) => ({ x: acc.x + point.x / quad.length, y: acc.y + point.y / quad.length }),
32
- { x: 0, y: 0 },
33
- );
34
-
35
- const sorted = quad
36
- .slice()
37
- .map(clonePoint)
38
- .sort((a, b) => {
39
- const angleA = Math.atan2(a.y - centroid.y, a.x - centroid.x);
40
- const angleB = Math.atan2(b.y - centroid.y, b.x - centroid.x);
41
- return angleA - angleB;
42
- });
43
-
44
- const topLeftIndex = sorted.reduce((selectedIndex, point, index) => {
45
- const currentScore = point.x + point.y;
46
- const selectedPoint = sorted[selectedIndex];
47
- const selectedScore = selectedPoint.x + selectedPoint.y;
48
- if (currentScore < selectedScore - POINT_EPSILON) {
49
- return index;
50
- }
51
- return selectedIndex;
52
- }, 0);
53
-
54
- return [...sorted.slice(topLeftIndex), ...sorted.slice(0, topLeftIndex)];
55
- };
56
-
57
- export const quadDistance = (a: Point[], b: Point[]): number => {
58
- if (!isValidQuad(a) || !isValidQuad(b)) {
59
- return Number.POSITIVE_INFINITY;
60
- }
61
-
62
- let total = 0;
63
- for (let i = 0; i < 4; i += 1) {
64
- total += Math.hypot(a[i].x - b[i].x, a[i].y - b[i].y);
65
- }
66
-
67
- return total / 4;
68
- };
69
-
70
- export const averageQuad = (quads: Point[][]): Point[] => {
71
- if (!Array.isArray(quads) || quads.length === 0) {
72
- throw new Error('Cannot average empty quad array');
73
- }
74
-
75
- const accum: Point[] = quads[0].map(() => ({ x: 0, y: 0 }));
76
-
77
- quads.forEach((quad) => {
78
- quad.forEach((point, index) => {
79
- accum[index].x += point.x;
80
- accum[index].y += point.y;
81
- });
82
- });
83
-
84
- return accum.map((point) => ({ x: point.x / quads.length, y: point.y / quads.length }));
85
- };
86
-
87
- export const blendQuads = (base: Point[], target: Point[], alpha: number): Point[] => {
88
- if (alpha <= 0) {
89
- return base.map(clonePoint);
90
- }
91
-
92
- if (alpha >= 1) {
93
- return target.map(clonePoint);
94
- }
95
-
96
- return base.map((point, index) => ({
97
- x: point.x * (1 - alpha) + target[index].x * alpha,
98
- y: point.y * (1 - alpha) + target[index].y * alpha,
99
- }));
100
- };
101
-
102
- export const sanitizeQuad = (quad: Point[]): Point[] => {
103
- if (!isValidQuad(quad)) {
104
- throw new Error('Cannot sanitise invalid quad');
105
- }
106
-
107
- return quad.map((point) => ({
108
- x: Number.isFinite(point.x) ? point.x : 0,
109
- y: Number.isFinite(point.y) ? point.y : 0,
110
- }));
111
- };
112
-
113
- export const quadArea = (quad: Point[]): number => {
114
- if (!isValidQuad(quad)) {
115
- return 0;
116
- }
117
-
118
- let area = 0;
119
- for (let i = 0; i < 4; i += 1) {
120
- const current = quad[i];
121
- const next = quad[(i + 1) % 4];
122
- area += current.x * next.y - next.x * current.y;
123
- }
124
-
125
- return Math.abs(area) / 2;
126
- };
127
-
128
- export const quadCenter = (quad: Point[]): Point => {
129
- if (!isValidQuad(quad)) {
130
- return { x: 0, y: 0 };
131
- }
132
-
133
- const sum = quad.reduce(
134
- (acc, point) => ({ x: acc.x + point.x, y: acc.y + point.y }),
135
- { x: 0, y: 0 },
136
- );
137
-
138
- return {
139
- x: sum.x / quad.length,
140
- y: sum.y / quad.length,
141
- };
142
- };
143
-
144
- export const quadEdgeLengths = (quad: Point[]): number[] => {
145
- if (!isValidQuad(quad)) {
146
- return [0, 0, 0, 0];
147
- }
148
-
149
- const lengths: number[] = [];
150
-
151
- for (let i = 0; i < 4; i += 1) {
152
- const current = quad[i];
153
- const next = quad[(i + 1) % 4];
154
- lengths.push(Math.hypot(next.x - current.x, next.y - current.y));
155
- }
156
-
157
- return lengths;
158
- };
159
-
160
- export const weightedAverageQuad = (quads: Point[][]): Point[] => {
161
- if (!Array.isArray(quads) || quads.length === 0) {
162
- throw new Error('Cannot average empty quad array');
163
- }
164
-
165
- const weights = quads.map((_, index) => index + 1);
166
- const totalWeight = weights.reduce((acc, weight) => acc + weight, 0);
167
-
168
- const accum: Point[] = quads[0].map(() => ({ x: 0, y: 0 }));
169
-
170
- quads.forEach((quad, quadIndex) => {
171
- quad.forEach((point, pointIndex) => {
172
- accum[pointIndex].x += point.x * weights[quadIndex];
173
- accum[pointIndex].y += point.y * weights[quadIndex];
174
- });
175
- });
176
-
177
- return accum.map((point) => ({
178
- x: point.x / totalWeight,
179
- y: point.y / totalWeight,
180
- }));
181
- };
@@ -1,32 +0,0 @@
1
- import type { Point } from '../types';
2
- import { isValidQuad, quadDistance } from './quad';
3
-
4
- let last: Point[] | null = null;
5
- let stable = 0;
6
-
7
- const STABILITY_DISTANCE = 8;
8
-
9
- export function checkStability(current: Point[] | null): number {
10
- if (!isValidQuad(current)) {
11
- stable = 0;
12
- last = null;
13
- return 0;
14
- }
15
-
16
- if (!last) {
17
- last = current;
18
- stable = 1;
19
- return stable;
20
- }
21
-
22
- const diff = quadDistance(current, last);
23
-
24
- if (diff < STABILITY_DISTANCE) {
25
- stable++;
26
- } else {
27
- stable = 0;
28
- }
29
-
30
- last = current;
31
- return stable;
32
- }