@ayasofyazilim/ui 0.0.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/__mocks__/canvas.ts +8 -0
- package/components.json +21 -0
- package/eslint.config.js +4 -0
- package/jest-environment.js +37 -0
- package/jest.config.ts +47 -0
- package/jest.setup.ts +69 -0
- package/package.json +124 -0
- package/postcss.config.mjs +6 -0
- package/src/aria/index.tsx +1 -0
- package/src/aria/number-field.tsx +41 -0
- package/src/components/.gitkeep +0 -0
- package/src/components/accordion.tsx +66 -0
- package/src/components/alert-dialog.tsx +157 -0
- package/src/components/alert.tsx +70 -0
- package/src/components/aspect-ratio.tsx +11 -0
- package/src/components/avatar.tsx +53 -0
- package/src/components/badge.tsx +67 -0
- package/src/components/breadcrumb.tsx +109 -0
- package/src/components/button-group.tsx +83 -0
- package/src/components/button.tsx +68 -0
- package/src/components/calendar.tsx +219 -0
- package/src/components/card.tsx +92 -0
- package/src/components/carousel.tsx +241 -0
- package/src/components/chart.tsx +363 -0
- package/src/components/checkbox.tsx +32 -0
- package/src/components/collapsible.tsx +33 -0
- package/src/components/command.tsx +184 -0
- package/src/components/context-menu.tsx +252 -0
- package/src/components/dialog.tsx +144 -0
- package/src/components/drawer.tsx +135 -0
- package/src/components/dropdown-menu.tsx +258 -0
- package/src/components/empty.tsx +100 -0
- package/src/components/field.tsx +248 -0
- package/src/components/form.tsx +169 -0
- package/src/components/hover-card.tsx +44 -0
- package/src/components/input-group.tsx +170 -0
- package/src/components/input-otp.tsx +77 -0
- package/src/components/input.tsx +21 -0
- package/src/components/item.tsx +193 -0
- package/src/components/kbd.tsx +28 -0
- package/src/components/label.tsx +24 -0
- package/src/components/menubar.tsx +276 -0
- package/src/components/navigation-menu.tsx +168 -0
- package/src/components/pagination.tsx +130 -0
- package/src/components/popover.tsx +88 -0
- package/src/components/progress.tsx +31 -0
- package/src/components/radio-group.tsx +45 -0
- package/src/components/resizable.tsx +56 -0
- package/src/components/scroll-area.tsx +58 -0
- package/src/components/select.tsx +189 -0
- package/src/components/separator.tsx +28 -0
- package/src/components/sheet.tsx +140 -0
- package/src/components/sidebar.tsx +862 -0
- package/src/components/skeleton.tsx +13 -0
- package/src/components/slider.tsx +63 -0
- package/src/components/sonner.tsx +40 -0
- package/src/components/spinner.tsx +16 -0
- package/src/components/stepper.tsx +291 -0
- package/src/components/switch.tsx +31 -0
- package/src/components/table.tsx +133 -0
- package/src/components/tabs.tsx +66 -0
- package/src/components/textarea.tsx +18 -0
- package/src/components/toggle-group.tsx +83 -0
- package/src/components/toggle.tsx +47 -0
- package/src/components/tooltip.tsx +66 -0
- package/src/custom/action-button.tsx +48 -0
- package/src/custom/async-select.tsx +287 -0
- package/src/custom/awesome-not-found.tsx +116 -0
- package/src/custom/charts/area-chart.tsx +147 -0
- package/src/custom/charts/bar-chart.tsx +233 -0
- package/src/custom/charts/chart-card.tsx +103 -0
- package/src/custom/charts/index.tsx +16 -0
- package/src/custom/charts/pie-chart.tsx +168 -0
- package/src/custom/charts/radar-chart.tsx +126 -0
- package/src/custom/checkbox-tree.tsx +100 -0
- package/src/custom/combobox.tsx +296 -0
- package/src/custom/confirm-dialog.tsx +102 -0
- package/src/custom/country-selector.tsx +204 -0
- package/src/custom/date-picker/calendar-rac.tsx +109 -0
- package/src/custom/date-picker/datefield-rac.tsx +84 -0
- package/src/custom/date-picker/index.tsx +273 -0
- package/src/custom/date-picker/types/index.ts +4 -0
- package/src/custom/date-picker/utils/index.ts +42 -0
- package/src/custom/date-picker-old.tsx +50 -0
- package/src/custom/date-tooltip.tsx +98 -0
- package/src/custom/document-scanner/consts.ts +5 -0
- package/src/custom/document-scanner/corner-adjustment/action-buttons.tsx +33 -0
- package/src/custom/document-scanner/corner-adjustment/corner-handle.tsx +43 -0
- package/src/custom/document-scanner/corner-adjustment/hooks/use-corner-drag.ts +85 -0
- package/src/custom/document-scanner/corner-adjustment/index.tsx +125 -0
- package/src/custom/document-scanner/corner-adjustment/types.ts +53 -0
- package/src/custom/document-scanner/corner-adjustment/utils/clip-path.ts +22 -0
- package/src/custom/document-scanner/corner-adjustment/zoom-magnifier.tsx +115 -0
- package/src/custom/document-scanner/hooks/use-document-capture.ts +81 -0
- package/src/custom/document-scanner/hooks/use-document-scanner.ts +80 -0
- package/src/custom/document-scanner/hooks/use-perspective-crop.ts +38 -0
- package/src/custom/document-scanner/index.tsx +255 -0
- package/src/custom/document-scanner/lib.ts +407 -0
- package/src/custom/document-scanner/types.ts +205 -0
- package/src/custom/document-scanner/utils/perspective-correction.ts +139 -0
- package/src/custom/document-viewer/controllers.tsx +98 -0
- package/src/custom/document-viewer/index.tsx +43 -0
- package/src/custom/document-viewer/renderers/image.tsx +37 -0
- package/src/custom/document-viewer/renderers/index.tsx +2 -0
- package/src/custom/document-viewer/renderers/pdf.tsx +105 -0
- package/src/custom/email-input/domains.json +159 -0
- package/src/custom/email-input/email.tsx +229 -0
- package/src/custom/email-input/index.tsx +4 -0
- package/src/custom/email-input/types.ts +104 -0
- package/src/custom/file-uploader.tsx +541 -0
- package/src/custom/filter-component/fields/async-select.tsx +33 -0
- package/src/custom/filter-component/fields/date.tsx +60 -0
- package/src/custom/filter-component/fields/multi-select.tsx +30 -0
- package/src/custom/filter-component/index.tsx +217 -0
- package/src/custom/image-canvas.tsx +260 -0
- package/src/custom/json-editor.tsx +22 -0
- package/src/custom/master-data-grid/components/dialogs/column-settings-dialog.tsx +100 -0
- package/src/custom/master-data-grid/components/dialogs/index.ts +1 -0
- package/src/custom/master-data-grid/components/filters/client-filter.tsx +368 -0
- package/src/custom/master-data-grid/components/filters/filter-input.tsx +256 -0
- package/src/custom/master-data-grid/components/filters/index.ts +3 -0
- package/src/custom/master-data-grid/components/filters/inline-column-filter.tsx +233 -0
- package/src/custom/master-data-grid/components/filters/multi-filter-dialog.tsx +90 -0
- package/src/custom/master-data-grid/components/filters/server-filter.tsx +255 -0
- package/src/custom/master-data-grid/components/master-data-grid.tsx +472 -0
- package/src/custom/master-data-grid/components/pagination/index.ts +1 -0
- package/src/custom/master-data-grid/components/pagination/pagination.tsx +178 -0
- package/src/custom/master-data-grid/components/table/cell-renderer.tsx +634 -0
- package/src/custom/master-data-grid/components/table/header-cell.tsx +162 -0
- package/src/custom/master-data-grid/components/table/index.ts +4 -0
- package/src/custom/master-data-grid/components/table/table-body-renderer.tsx +113 -0
- package/src/custom/master-data-grid/components/table/virtual-body.tsx +138 -0
- package/src/custom/master-data-grid/components/toolbar/index.ts +1 -0
- package/src/custom/master-data-grid/components/toolbar/toolbar.tsx +314 -0
- package/src/custom/master-data-grid/hooks/index.ts +3 -0
- package/src/custom/master-data-grid/hooks/use-columns.tsx +332 -0
- package/src/custom/master-data-grid/hooks/use-editing.ts +106 -0
- package/src/custom/master-data-grid/hooks/use-table-state-reducer.ts +157 -0
- package/src/custom/master-data-grid/hooks/use-table-state.ts +31 -0
- package/src/custom/master-data-grid/index.ts +16 -0
- package/src/custom/master-data-grid/types.ts +466 -0
- package/src/custom/master-data-grid/utils/column-generator.tsx +306 -0
- package/src/custom/master-data-grid/utils/export-utils.ts +67 -0
- package/src/custom/master-data-grid/utils/filter-fns.ts +290 -0
- package/src/custom/master-data-grid/utils/index.ts +8 -0
- package/src/custom/master-data-grid/utils/pinning-utils.ts +88 -0
- package/src/custom/master-data-grid/utils/translation-utils.ts +42 -0
- package/src/custom/multi-select.tsx +432 -0
- package/src/custom/password-input.tsx +194 -0
- package/src/custom/phone-input.tsx +172 -0
- package/src/custom/schema-form/custom/index.tsx +1 -0
- package/src/custom/schema-form/custom/label.tsx +53 -0
- package/src/custom/schema-form/fields/base-input-field.tsx +82 -0
- package/src/custom/schema-form/fields/field.tsx +67 -0
- package/src/custom/schema-form/fields/index.tsx +5 -0
- package/src/custom/schema-form/fields/object.tsx +12 -0
- package/src/custom/schema-form/fields/table-array/array-field-item.tsx +90 -0
- package/src/custom/schema-form/fields/table-array/array-field-template.tsx +115 -0
- package/src/custom/schema-form/index.tsx +259 -0
- package/src/custom/schema-form/templates/description.tsx +20 -0
- package/src/custom/schema-form/templates/index.tsx +2 -0
- package/src/custom/schema-form/templates/submit.tsx +32 -0
- package/src/custom/schema-form/types.ts +64 -0
- package/src/custom/schema-form/utils/index.ts +4 -0
- package/src/custom/schema-form/utils/schema-dependency.ts +655 -0
- package/src/custom/schema-form/utils/schemas.ts +289 -0
- package/src/custom/schema-form/utils/validation.ts +23 -0
- package/src/custom/schema-form/widgets/boolean.tsx +77 -0
- package/src/custom/schema-form/widgets/combobox.tsx +274 -0
- package/src/custom/schema-form/widgets/date.tsx +59 -0
- package/src/custom/schema-form/widgets/email.tsx +34 -0
- package/src/custom/schema-form/widgets/index.tsx +10 -0
- package/src/custom/schema-form/widgets/password.tsx +40 -0
- package/src/custom/schema-form/widgets/phone.tsx +40 -0
- package/src/custom/schema-form/widgets/select.tsx +105 -0
- package/src/custom/schema-form/widgets/selectable.tsx +25 -0
- package/src/custom/schema-form/widgets/string-array.tsx +296 -0
- package/src/custom/schema-form/widgets/url.tsx +56 -0
- package/src/custom/section-layout-v2.tsx +212 -0
- package/src/custom/select-tabs.tsx +109 -0
- package/src/custom/selectable.tsx +316 -0
- package/src/custom/stepper.tsx +236 -0
- package/src/custom/tab-layout.tsx +213 -0
- package/src/custom/tanstack-table/fields/index.tsx +12 -0
- package/src/custom/tanstack-table/fields/tanstack-table-action-dialogs.tsx +89 -0
- package/src/custom/tanstack-table/fields/tanstack-table-column-header.tsx +66 -0
- package/src/custom/tanstack-table/fields/tanstack-table-filter-date.tsx +180 -0
- package/src/custom/tanstack-table/fields/tanstack-table-filter-faceted.tsx +158 -0
- package/src/custom/tanstack-table/fields/tanstack-table-filter-text.tsx +76 -0
- package/src/custom/tanstack-table/fields/tanstack-table-pagination.tsx +136 -0
- package/src/custom/tanstack-table/fields/tanstack-table-plain-table.tsx +142 -0
- package/src/custom/tanstack-table/fields/tanstack-table-row-actions-confirmation.tsx +77 -0
- package/src/custom/tanstack-table/fields/tanstack-table-row-actions-custom-dialog.tsx +87 -0
- package/src/custom/tanstack-table/fields/tanstack-table-row-actions.tsx +151 -0
- package/src/custom/tanstack-table/fields/tanstack-table-table-actions-custom-dialog.tsx +88 -0
- package/src/custom/tanstack-table/fields/tanstack-table-table-actions-schemaform-dialog.tsx +47 -0
- package/src/custom/tanstack-table/fields/tanstack-table-toolbar.tsx +143 -0
- package/src/custom/tanstack-table/fields/tanstack-table-view-options.tsx +171 -0
- package/src/custom/tanstack-table/index.tsx +244 -0
- package/src/custom/tanstack-table/types/index.ts +328 -0
- package/src/custom/tanstack-table/utils/cell-with-actions.tsx +21 -0
- package/src/custom/tanstack-table/utils/column-names.ts +26 -0
- package/src/custom/tanstack-table/utils/columns-by-row-data.tsx +312 -0
- package/src/custom/tanstack-table/utils/editable-columns-by-row-data.tsx +219 -0
- package/src/custom/tanstack-table/utils/faceted-boolean-options.tsx +22 -0
- package/src/custom/tanstack-table/utils/index.tsx +10 -0
- package/src/custom/tanstack-table/utils/pinning-styles.ts +57 -0
- package/src/custom/tanstack-table/utils/table.tsx +83 -0
- package/src/custom/tanstack-table/utils/test-conditions.ts +17 -0
- package/src/custom/timeline.tsx +208 -0
- package/src/custom/tree.tsx +200 -0
- package/src/custom/tscanify/browser.ts +66 -0
- package/src/custom/tscanify/index.ts +51 -0
- package/src/custom/tscanify/tscanify-browser.ts +522 -0
- package/src/custom/tscanify/tscanify.ts +262 -0
- package/src/custom/tscanify/types.ts +22 -0
- package/src/custom/webcam.tsx +737 -0
- package/src/hooks/.gitkeep +0 -0
- package/src/hooks/use-callback-ref.ts +27 -0
- package/src/hooks/use-controllable-state.ts +67 -0
- package/src/hooks/use-debounce.ts +19 -0
- package/src/hooks/use-is-visible.ts +23 -0
- package/src/hooks/use-media-query.ts +21 -0
- package/src/hooks/use-mobile.ts +21 -0
- package/src/hooks/use-on-window-resize.ts +15 -0
- package/src/hooks/use-scroll.tsx +22 -0
- package/src/lib/utils.ts +61 -0
- package/src/lib/zod.ts +2 -0
- package/src/styles/core.css +57 -0
- package/src/styles/globals.css +130 -0
- package/src/test/email-input.test.tsx +217 -0
- package/src/test/password-input.test.tsx +92 -0
- package/src/test/select-tabs.test.tsx +302 -0
- package/src/test/selectable.test.tsx +1093 -0
- package/tsconfig.json +13 -0
- package/tsconfig.lint.json +8 -0
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
import { createScanner } from "../tscanify/browser";
|
|
2
|
+
import { DEFAULT_MAX_DOCUMENT_SIZE, DEFAULT_MIN_DOCUMENT_SIZE } from "./consts";
|
|
3
|
+
import { DocumentCorners } from "./types";
|
|
4
|
+
import cv, { Mat } from "opencv-ts";
|
|
5
|
+
|
|
6
|
+
export async function scanDocument(
|
|
7
|
+
imageBase64: string,
|
|
8
|
+
videoDimensions: { width: number; height: number },
|
|
9
|
+
onSuccess: (corners: DocumentCorners) => void,
|
|
10
|
+
onError: (error: string) => void,
|
|
11
|
+
options?: {
|
|
12
|
+
minDocumentSize?: number;
|
|
13
|
+
maxDocumentSize?: number;
|
|
14
|
+
detectionSensitivity?: number;
|
|
15
|
+
detectionConfidence?: number;
|
|
16
|
+
}
|
|
17
|
+
) {
|
|
18
|
+
try {
|
|
19
|
+
const scanner = createScanner();
|
|
20
|
+
const inputCanvas = document.createElement("canvas");
|
|
21
|
+
const img = new Image();
|
|
22
|
+
|
|
23
|
+
await new Promise<void>((resolve, reject) => {
|
|
24
|
+
img.onload = () => {
|
|
25
|
+
try {
|
|
26
|
+
const ctx = inputCanvas.getContext("2d");
|
|
27
|
+
if (!ctx) {
|
|
28
|
+
reject(new Error("Failed to get canvas context"));
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
inputCanvas.width = img.width;
|
|
33
|
+
inputCanvas.height = img.height;
|
|
34
|
+
ctx.drawImage(img, 0, 0);
|
|
35
|
+
|
|
36
|
+
const src = cv.imread(inputCanvas);
|
|
37
|
+
let detectedCorners: DocumentCorners | null = null;
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
// Use configurable detection settings
|
|
41
|
+
const minDocumentSize =
|
|
42
|
+
options?.minDocumentSize ?? DEFAULT_MIN_DOCUMENT_SIZE;
|
|
43
|
+
const maxDocumentSize =
|
|
44
|
+
options?.maxDocumentSize ?? DEFAULT_MAX_DOCUMENT_SIZE;
|
|
45
|
+
|
|
46
|
+
// Try different detection methods
|
|
47
|
+
detectedCorners = findDocumentSimple(src, img.width, img.height);
|
|
48
|
+
|
|
49
|
+
if (!detectedCorners) {
|
|
50
|
+
detectedCorners = scanner.findDocumentCorners(src);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (!detectedCorners) {
|
|
54
|
+
detectedCorners = findDocumentCornersAdvanced(
|
|
55
|
+
src,
|
|
56
|
+
img.width,
|
|
57
|
+
img.height
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (detectedCorners) {
|
|
62
|
+
// Validate corners with configurable sizes
|
|
63
|
+
const {
|
|
64
|
+
topLeftCorner,
|
|
65
|
+
topRightCorner,
|
|
66
|
+
bottomRightCorner,
|
|
67
|
+
bottomLeftCorner,
|
|
68
|
+
} = detectedCorners;
|
|
69
|
+
const width = Math.abs(topRightCorner.x - topLeftCorner.x);
|
|
70
|
+
const height = Math.abs(bottomLeftCorner.y - topLeftCorner.y);
|
|
71
|
+
const minSize = Math.min(img.width, img.height) * minDocumentSize;
|
|
72
|
+
const maxSize = Math.min(img.width, img.height) * maxDocumentSize;
|
|
73
|
+
|
|
74
|
+
if (
|
|
75
|
+
width >= minSize &&
|
|
76
|
+
height >= minSize &&
|
|
77
|
+
width <= maxSize &&
|
|
78
|
+
height <= maxSize
|
|
79
|
+
) {
|
|
80
|
+
// Scale corners to video dimensions
|
|
81
|
+
const scaledCorners = {
|
|
82
|
+
topLeftCorner: {
|
|
83
|
+
x: (topLeftCorner.x / img.width) * videoDimensions.width,
|
|
84
|
+
y: (topLeftCorner.y / img.height) * videoDimensions.height,
|
|
85
|
+
},
|
|
86
|
+
topRightCorner: {
|
|
87
|
+
x: (topRightCorner.x / img.width) * videoDimensions.width,
|
|
88
|
+
y: (topRightCorner.y / img.height) * videoDimensions.height,
|
|
89
|
+
},
|
|
90
|
+
bottomRightCorner: {
|
|
91
|
+
x:
|
|
92
|
+
(bottomRightCorner.x / img.width) * videoDimensions.width,
|
|
93
|
+
y:
|
|
94
|
+
(bottomRightCorner.y / img.height) *
|
|
95
|
+
videoDimensions.height,
|
|
96
|
+
},
|
|
97
|
+
bottomLeftCorner: {
|
|
98
|
+
x: (bottomLeftCorner.x / img.width) * videoDimensions.width,
|
|
99
|
+
y:
|
|
100
|
+
(bottomLeftCorner.y / img.height) *
|
|
101
|
+
videoDimensions.height,
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
onSuccess(scaledCorners);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
} finally {
|
|
108
|
+
// Always cleanup OpenCV resources
|
|
109
|
+
src.delete();
|
|
110
|
+
}
|
|
111
|
+
resolve();
|
|
112
|
+
} catch (error) {
|
|
113
|
+
reject(error);
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
img.onerror = () => reject(new Error("Failed to load image"));
|
|
118
|
+
img.src = imageBase64;
|
|
119
|
+
});
|
|
120
|
+
} catch (error) {
|
|
121
|
+
const errorMessage =
|
|
122
|
+
error instanceof Error
|
|
123
|
+
? error.message
|
|
124
|
+
: "Unknown error occurred during document scanning";
|
|
125
|
+
onError(errorMessage);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Document scanning function
|
|
130
|
+
|
|
131
|
+
// Simple document detection fallback
|
|
132
|
+
export function findDocumentSimple(
|
|
133
|
+
src: Mat,
|
|
134
|
+
width: number,
|
|
135
|
+
height: number
|
|
136
|
+
): DocumentCorners | null {
|
|
137
|
+
try {
|
|
138
|
+
const gray = new cv.Mat();
|
|
139
|
+
const binary = new cv.Mat();
|
|
140
|
+
|
|
141
|
+
cv.cvtColor(src, gray, cv.COLOR_RGBA2GRAY);
|
|
142
|
+
cv.threshold(gray, binary, 0, 255, cv.THRESH_BINARY + cv.THRESH_OTSU);
|
|
143
|
+
|
|
144
|
+
const contours = new cv.MatVector();
|
|
145
|
+
const hierarchy = new cv.Mat();
|
|
146
|
+
|
|
147
|
+
cv.findContours(
|
|
148
|
+
binary,
|
|
149
|
+
contours,
|
|
150
|
+
hierarchy,
|
|
151
|
+
cv.RETR_EXTERNAL,
|
|
152
|
+
cv.CHAIN_APPROX_SIMPLE
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
for (let i = 0; i < contours.size(); i++) {
|
|
156
|
+
const contour = contours.get(i);
|
|
157
|
+
const area = cv.contourArea(contour);
|
|
158
|
+
const minArea = width * height * 0.1;
|
|
159
|
+
|
|
160
|
+
if (area > minArea) {
|
|
161
|
+
const approx = new cv.Mat();
|
|
162
|
+
const peri = cv.arcLength(contour, true);
|
|
163
|
+
cv.approxPolyDP(contour, approx, 0.02 * peri, true);
|
|
164
|
+
|
|
165
|
+
if (approx.rows === 4) {
|
|
166
|
+
const points: Array<{ x: number; y: number }> = [];
|
|
167
|
+
for (let j = 0; j < 4; j++) {
|
|
168
|
+
const point = approx.data32S;
|
|
169
|
+
points.push({ x: point[j * 2] || 0, y: point[j * 2 + 1] || 0 });
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Sort points to match expected corners
|
|
173
|
+
points.sort((a, b) => a.y - b.y);
|
|
174
|
+
const topPoints = points.slice(0, 2).sort((a, b) => a.x - b.x);
|
|
175
|
+
const bottomPoints = points.slice(2, 4).sort((a, b) => a.x - b.x);
|
|
176
|
+
|
|
177
|
+
const corners = {
|
|
178
|
+
topLeftCorner: topPoints[0],
|
|
179
|
+
topRightCorner: topPoints[1],
|
|
180
|
+
bottomRightCorner: bottomPoints[1],
|
|
181
|
+
bottomLeftCorner: bottomPoints[0],
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
approx.delete();
|
|
185
|
+
contour.delete();
|
|
186
|
+
gray.delete();
|
|
187
|
+
binary.delete();
|
|
188
|
+
contours.delete();
|
|
189
|
+
hierarchy.delete();
|
|
190
|
+
|
|
191
|
+
return corners as DocumentCorners;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
approx.delete();
|
|
195
|
+
}
|
|
196
|
+
contour.delete();
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
gray.delete();
|
|
200
|
+
binary.delete();
|
|
201
|
+
contours.delete();
|
|
202
|
+
hierarchy.delete();
|
|
203
|
+
|
|
204
|
+
return null;
|
|
205
|
+
} catch {
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Advanced document detection
|
|
211
|
+
export function findDocumentCornersAdvanced(
|
|
212
|
+
src: Mat,
|
|
213
|
+
width: number,
|
|
214
|
+
height: number
|
|
215
|
+
): DocumentCorners | null {
|
|
216
|
+
try {
|
|
217
|
+
const gray = new cv.Mat();
|
|
218
|
+
cv.cvtColor(src, gray, cv.COLOR_RGBA2GRAY);
|
|
219
|
+
|
|
220
|
+
const approaches = [
|
|
221
|
+
{
|
|
222
|
+
name: "OTSU + Gaussian",
|
|
223
|
+
process: (input: any) => {
|
|
224
|
+
const blurred = new cv.Mat();
|
|
225
|
+
const binary = new cv.Mat();
|
|
226
|
+
cv.GaussianBlur(input, blurred, new cv.Size(9, 9), 0, 0, 0);
|
|
227
|
+
cv.threshold(
|
|
228
|
+
blurred,
|
|
229
|
+
binary,
|
|
230
|
+
0,
|
|
231
|
+
255,
|
|
232
|
+
cv.THRESH_BINARY + cv.THRESH_OTSU
|
|
233
|
+
);
|
|
234
|
+
blurred.delete();
|
|
235
|
+
return binary;
|
|
236
|
+
},
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
name: "Adaptive Threshold",
|
|
240
|
+
process: (input: Mat) => {
|
|
241
|
+
const binary = new cv.Mat();
|
|
242
|
+
cv.adaptiveThreshold(
|
|
243
|
+
input,
|
|
244
|
+
binary,
|
|
245
|
+
255,
|
|
246
|
+
cv.ADAPTIVE_THRESH_GAUSSIAN_C,
|
|
247
|
+
cv.THRESH_BINARY,
|
|
248
|
+
15,
|
|
249
|
+
10
|
|
250
|
+
);
|
|
251
|
+
return binary;
|
|
252
|
+
},
|
|
253
|
+
},
|
|
254
|
+
];
|
|
255
|
+
|
|
256
|
+
let bestCorners: DocumentCorners | null = null;
|
|
257
|
+
let bestScore = 0;
|
|
258
|
+
|
|
259
|
+
Array.from(approaches).forEach((approach) => {
|
|
260
|
+
const binary = approach.process(gray);
|
|
261
|
+
const corners = findCornersFromBinary(binary, width, height);
|
|
262
|
+
|
|
263
|
+
if (corners) {
|
|
264
|
+
const score = scoreCornersQuality(corners, width, height);
|
|
265
|
+
if (score > bestScore) {
|
|
266
|
+
bestScore = score;
|
|
267
|
+
bestCorners = corners;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
binary.delete();
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
gray.delete();
|
|
275
|
+
return bestCorners;
|
|
276
|
+
} catch {
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export function findCornersFromBinary(
|
|
282
|
+
binary: Mat,
|
|
283
|
+
width: number,
|
|
284
|
+
height: number
|
|
285
|
+
): DocumentCorners | null {
|
|
286
|
+
try {
|
|
287
|
+
const contours = new cv.MatVector();
|
|
288
|
+
const hierarchy = new cv.Mat();
|
|
289
|
+
cv.findContours(
|
|
290
|
+
binary,
|
|
291
|
+
contours,
|
|
292
|
+
hierarchy,
|
|
293
|
+
cv.RETR_EXTERNAL,
|
|
294
|
+
cv.CHAIN_APPROX_SIMPLE
|
|
295
|
+
);
|
|
296
|
+
|
|
297
|
+
let bestCorners: DocumentCorners | null = null;
|
|
298
|
+
let bestArea = 0;
|
|
299
|
+
|
|
300
|
+
for (let i = 0; i < contours.size(); i++) {
|
|
301
|
+
const contour = contours.get(i);
|
|
302
|
+
const area = cv.contourArea(contour);
|
|
303
|
+
const minArea = width * height * 0.05;
|
|
304
|
+
|
|
305
|
+
if (area > minArea && area > bestArea) {
|
|
306
|
+
const approx = new cv.Mat();
|
|
307
|
+
const peri = cv.arcLength(contour, true);
|
|
308
|
+
cv.approxPolyDP(contour, approx, 0.02 * peri, true);
|
|
309
|
+
|
|
310
|
+
if (approx.rows === 4) {
|
|
311
|
+
const points: Array<{ x: number; y: number }> = [];
|
|
312
|
+
for (let j = 0; j < 4; j++) {
|
|
313
|
+
const point = approx.data32S;
|
|
314
|
+
points.push({ x: point[j * 2] || 0, y: point[j * 2 + 1] || 0 });
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
points.sort((a, b) => a.y - b.y);
|
|
318
|
+
const topPoints = points.slice(0, 2).sort((a, b) => a.x - b.x);
|
|
319
|
+
const bottomPoints = points.slice(2, 4).sort((a, b) => a.x - b.x);
|
|
320
|
+
|
|
321
|
+
bestCorners = {
|
|
322
|
+
topLeftCorner: topPoints[0] as DocumentCorners["topLeftCorner"],
|
|
323
|
+
topRightCorner: topPoints[1] as DocumentCorners["topRightCorner"],
|
|
324
|
+
bottomRightCorner:
|
|
325
|
+
bottomPoints[1] as DocumentCorners["bottomRightCorner"],
|
|
326
|
+
bottomLeftCorner:
|
|
327
|
+
bottomPoints[0] as DocumentCorners["bottomLeftCorner"],
|
|
328
|
+
};
|
|
329
|
+
bestArea = area;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
approx.delete();
|
|
333
|
+
}
|
|
334
|
+
contour.delete();
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
contours.delete();
|
|
338
|
+
hierarchy.delete();
|
|
339
|
+
return bestCorners;
|
|
340
|
+
} catch {
|
|
341
|
+
return null;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
export function scoreCornersQuality(
|
|
346
|
+
corners: DocumentCorners,
|
|
347
|
+
width: number,
|
|
348
|
+
height: number
|
|
349
|
+
): number {
|
|
350
|
+
try {
|
|
351
|
+
const {
|
|
352
|
+
topLeftCorner,
|
|
353
|
+
topRightCorner,
|
|
354
|
+
bottomRightCorner,
|
|
355
|
+
bottomLeftCorner,
|
|
356
|
+
} = corners;
|
|
357
|
+
|
|
358
|
+
// Calculate all four side lengths for more accurate measurement
|
|
359
|
+
const topWidth = Math.abs(topRightCorner.x - topLeftCorner.x);
|
|
360
|
+
const bottomWidth = Math.abs(bottomRightCorner.x - bottomLeftCorner.x);
|
|
361
|
+
const leftHeight = Math.abs(bottomLeftCorner.y - topLeftCorner.y);
|
|
362
|
+
const rightHeight = Math.abs(bottomRightCorner.y - topRightCorner.y);
|
|
363
|
+
|
|
364
|
+
// Use average width and height for better accuracy
|
|
365
|
+
const avgWidth = (topWidth + bottomWidth) / 2;
|
|
366
|
+
const avgHeight = (leftHeight + rightHeight) / 2;
|
|
367
|
+
const area = avgWidth * avgHeight;
|
|
368
|
+
|
|
369
|
+
// Calculate how much the shape deviates from a perfect rectangle
|
|
370
|
+
const widthDifference =
|
|
371
|
+
Math.abs(topWidth - bottomWidth) / Math.max(topWidth, bottomWidth);
|
|
372
|
+
const heightDifference =
|
|
373
|
+
Math.abs(leftHeight - rightHeight) / Math.max(leftHeight, rightHeight);
|
|
374
|
+
const rectangularityScore = 1 - (widthDifference + heightDifference) / 2;
|
|
375
|
+
|
|
376
|
+
// Area score (normalized)
|
|
377
|
+
const maxArea = width * height;
|
|
378
|
+
const areaScore = Math.min(area / (maxArea * 0.8), 1);
|
|
379
|
+
|
|
380
|
+
// Aspect ratio score (favor reasonable document proportions)
|
|
381
|
+
const aspectRatio = avgWidth / avgHeight;
|
|
382
|
+
const aspectScore = aspectRatio > 0.3 && aspectRatio < 3 ? 1 : 0.5;
|
|
383
|
+
|
|
384
|
+
// Size validation score (penalize very small or very large detections)
|
|
385
|
+
const minSize = Math.min(width, height) * 0.15; // At least 15% of image
|
|
386
|
+
const maxSize = Math.min(width, height) * 0.95; // At most 95% of image
|
|
387
|
+
const sizeScore =
|
|
388
|
+
avgWidth >= minSize &&
|
|
389
|
+
avgHeight >= minSize &&
|
|
390
|
+
avgWidth <= maxSize &&
|
|
391
|
+
avgHeight <= maxSize
|
|
392
|
+
? 1
|
|
393
|
+
: 0.3;
|
|
394
|
+
|
|
395
|
+
// Combine all scores with weights
|
|
396
|
+
return (
|
|
397
|
+
areaScore * 0.3 +
|
|
398
|
+
rectangularityScore * 0.4 +
|
|
399
|
+
aspectScore * 0.2 +
|
|
400
|
+
sizeScore * 0.1
|
|
401
|
+
);
|
|
402
|
+
} catch {
|
|
403
|
+
return 0;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// Adjustable corners component
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { ReactNode, PointerEvent } from "react";
|
|
2
|
+
import { WebcamProps } from "../webcam";
|
|
3
|
+
|
|
4
|
+
// Core types
|
|
5
|
+
export interface DocumentCorners {
|
|
6
|
+
topLeftCorner: { x: number; y: number };
|
|
7
|
+
topRightCorner: { x: number; y: number };
|
|
8
|
+
bottomRightCorner: { x: number; y: number };
|
|
9
|
+
bottomLeftCorner: { x: number; y: number };
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface Dimensions {
|
|
13
|
+
width: number;
|
|
14
|
+
height: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface Position {
|
|
18
|
+
x: number;
|
|
19
|
+
y: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Callback types
|
|
23
|
+
export interface DocumentScannerCallbacks {
|
|
24
|
+
onDocumentCropped?: (croppedImage: string) => void;
|
|
25
|
+
onDocumentDetected?: (
|
|
26
|
+
corners: DocumentCorners,
|
|
27
|
+
capturedImage: string
|
|
28
|
+
) => void;
|
|
29
|
+
onCornersChanged?: (corners: DocumentCorners) => void;
|
|
30
|
+
onError?: (error: string) => void;
|
|
31
|
+
onStatusChange?: (status: ScannerStatus) => void;
|
|
32
|
+
onCameraReady?: (dimensions: Dimensions) => void;
|
|
33
|
+
onScanAttempt?: () => void;
|
|
34
|
+
onImageCapture?: (image: string) => void;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export type ScannerStatus =
|
|
38
|
+
| "scanning"
|
|
39
|
+
| "detected"
|
|
40
|
+
| "cropped"
|
|
41
|
+
| "error"
|
|
42
|
+
| "processing";
|
|
43
|
+
|
|
44
|
+
export interface DetectionSettings {
|
|
45
|
+
minDocumentSize: number;
|
|
46
|
+
maxDocumentSize: number;
|
|
47
|
+
detectionConfidence: number;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Main component props
|
|
51
|
+
export interface DocumentScannerProps {
|
|
52
|
+
// Core callbacks
|
|
53
|
+
onDocumentCropped?: (croppedImage: string) => void;
|
|
54
|
+
onDocumentDetected?: (
|
|
55
|
+
corners: DocumentCorners,
|
|
56
|
+
capturedImage: string
|
|
57
|
+
) => void;
|
|
58
|
+
onCornersChanged?: (corners: DocumentCorners) => void;
|
|
59
|
+
onError?: (error: string) => void;
|
|
60
|
+
onStatusChange?: (status: ScannerStatus) => void;
|
|
61
|
+
onCameraReady?: (dimensions: Dimensions) => void;
|
|
62
|
+
onScanAttempt?: () => void;
|
|
63
|
+
onImageCapture?: (image: string) => void;
|
|
64
|
+
|
|
65
|
+
// Basic configuration
|
|
66
|
+
className?: string;
|
|
67
|
+
captureInterval?: number;
|
|
68
|
+
|
|
69
|
+
// Feature toggles
|
|
70
|
+
allowCrop?: boolean;
|
|
71
|
+
allowRetry?: boolean;
|
|
72
|
+
allowCornerAdjustment?: boolean;
|
|
73
|
+
allowCameraSwitch?: boolean;
|
|
74
|
+
showWebcamControls?: boolean;
|
|
75
|
+
|
|
76
|
+
// UI Text customization
|
|
77
|
+
cropButtonText?: string;
|
|
78
|
+
retryButtonText?: string;
|
|
79
|
+
|
|
80
|
+
// Visual styling
|
|
81
|
+
cornerColor?: string;
|
|
82
|
+
cornerTouchAreaSize?: number;
|
|
83
|
+
|
|
84
|
+
// Detection settings
|
|
85
|
+
minDocumentSize?: number;
|
|
86
|
+
maxDocumentSize?: number;
|
|
87
|
+
detectionConfidence?: number;
|
|
88
|
+
|
|
89
|
+
// Image processing
|
|
90
|
+
imageQuality?: number;
|
|
91
|
+
|
|
92
|
+
// Magnifier settings
|
|
93
|
+
showMagnifier?: boolean;
|
|
94
|
+
magnifierSize?: number;
|
|
95
|
+
zoomLevel?: number;
|
|
96
|
+
|
|
97
|
+
// Advanced settings
|
|
98
|
+
customDetectionAlgorithm?: (
|
|
99
|
+
image: string,
|
|
100
|
+
dimensions: Dimensions
|
|
101
|
+
) => Promise<DocumentCorners | null>;
|
|
102
|
+
|
|
103
|
+
// Custom components
|
|
104
|
+
customControls?: ReactNode;
|
|
105
|
+
customOverlay?: ReactNode;
|
|
106
|
+
|
|
107
|
+
// Webcam interface
|
|
108
|
+
interfaceLocation?: WebcamProps["interfaceLocation"];
|
|
109
|
+
showBorder?: WebcamProps["showBorder"];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Corner adjustment types
|
|
113
|
+
export interface CornerAdjustmentProps {
|
|
114
|
+
capturedImage: string;
|
|
115
|
+
detectedCorners: DocumentCorners;
|
|
116
|
+
videoDimensions: Dimensions;
|
|
117
|
+
onCornersChange: (corners: DocumentCorners) => void;
|
|
118
|
+
onCrop: () => void;
|
|
119
|
+
onRetry: () => void;
|
|
120
|
+
|
|
121
|
+
// Styling props
|
|
122
|
+
cornerColor?: string;
|
|
123
|
+
cornerTouchAreaSize?: number;
|
|
124
|
+
|
|
125
|
+
// Feature toggles
|
|
126
|
+
allowCrop?: boolean;
|
|
127
|
+
allowRetry?: boolean;
|
|
128
|
+
|
|
129
|
+
// Text customization
|
|
130
|
+
cropButtonText?: string;
|
|
131
|
+
retryButtonText?: string;
|
|
132
|
+
|
|
133
|
+
// Magnifier settings
|
|
134
|
+
showMagnifier?: boolean;
|
|
135
|
+
magnifierSize?: number;
|
|
136
|
+
zoomLevel?: number;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export interface DragState {
|
|
140
|
+
isDragging: string | null;
|
|
141
|
+
offset: Position;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export interface CornerHandleProps {
|
|
145
|
+
cornerKey: string;
|
|
146
|
+
corner: Position;
|
|
147
|
+
videoDimensions: Dimensions;
|
|
148
|
+
isDragging: boolean;
|
|
149
|
+
cornerColor: string;
|
|
150
|
+
cornerTouchAreaSize: number;
|
|
151
|
+
onDragStart: (cornerKey: string, event: PointerEvent) => void;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export interface ActionButtonsProps {
|
|
155
|
+
allowCrop: boolean;
|
|
156
|
+
allowRetry: boolean;
|
|
157
|
+
cropButtonText: string;
|
|
158
|
+
retryButtonText: string;
|
|
159
|
+
onCrop: () => void;
|
|
160
|
+
onRetry: () => void;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export interface ZoomMagnifierProps {
|
|
164
|
+
capturedImage: string;
|
|
165
|
+
detectedCorners: DocumentCorners;
|
|
166
|
+
videoDimensions: Dimensions;
|
|
167
|
+
containerDimensions: Dimensions;
|
|
168
|
+
draggedCorner: string | null;
|
|
169
|
+
zoomLevel?: number;
|
|
170
|
+
magnifierSize?: number;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Hook types
|
|
174
|
+
export interface UseDocumentScannerReturn {
|
|
175
|
+
videoDimensions: Dimensions;
|
|
176
|
+
detectedCorners: DocumentCorners | undefined;
|
|
177
|
+
capturedImage: string | null;
|
|
178
|
+
handleVideoReady: (dimensions: Dimensions) => void;
|
|
179
|
+
handleCornersChange: (newCorners: DocumentCorners) => void;
|
|
180
|
+
handleError: (error: string) => void;
|
|
181
|
+
handleImageCrop: (croppedImageBase64: string) => void;
|
|
182
|
+
handleRetry: () => void;
|
|
183
|
+
setCapturedImage: (image: string | null) => void;
|
|
184
|
+
setDetectedCorners: (corners: DocumentCorners | undefined) => void;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export interface UseDocumentCaptureProps {
|
|
188
|
+
videoDimensions: Dimensions;
|
|
189
|
+
callbacks: DocumentScannerCallbacks;
|
|
190
|
+
customDetectionAlgorithm?: (
|
|
191
|
+
image: string,
|
|
192
|
+
dimensions: Dimensions
|
|
193
|
+
) => Promise<DocumentCorners | null>;
|
|
194
|
+
detectionSettings: DetectionSettings;
|
|
195
|
+
setCapturedImage: (image: string | null) => void;
|
|
196
|
+
setDetectedCorners: (corners: DocumentCorners | undefined) => void;
|
|
197
|
+
handleError: (error: string) => void;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export interface UsePerspectiveCropProps {
|
|
201
|
+
videoDimensions: Dimensions;
|
|
202
|
+
imageQuality: number;
|
|
203
|
+
handleImageCrop: (croppedImageBase64: string) => void;
|
|
204
|
+
handleError: (error: string) => void;
|
|
205
|
+
}
|