@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.
Files changed (236) hide show
  1. package/__mocks__/canvas.ts +8 -0
  2. package/components.json +21 -0
  3. package/eslint.config.js +4 -0
  4. package/jest-environment.js +37 -0
  5. package/jest.config.ts +47 -0
  6. package/jest.setup.ts +69 -0
  7. package/package.json +124 -0
  8. package/postcss.config.mjs +6 -0
  9. package/src/aria/index.tsx +1 -0
  10. package/src/aria/number-field.tsx +41 -0
  11. package/src/components/.gitkeep +0 -0
  12. package/src/components/accordion.tsx +66 -0
  13. package/src/components/alert-dialog.tsx +157 -0
  14. package/src/components/alert.tsx +70 -0
  15. package/src/components/aspect-ratio.tsx +11 -0
  16. package/src/components/avatar.tsx +53 -0
  17. package/src/components/badge.tsx +67 -0
  18. package/src/components/breadcrumb.tsx +109 -0
  19. package/src/components/button-group.tsx +83 -0
  20. package/src/components/button.tsx +68 -0
  21. package/src/components/calendar.tsx +219 -0
  22. package/src/components/card.tsx +92 -0
  23. package/src/components/carousel.tsx +241 -0
  24. package/src/components/chart.tsx +363 -0
  25. package/src/components/checkbox.tsx +32 -0
  26. package/src/components/collapsible.tsx +33 -0
  27. package/src/components/command.tsx +184 -0
  28. package/src/components/context-menu.tsx +252 -0
  29. package/src/components/dialog.tsx +144 -0
  30. package/src/components/drawer.tsx +135 -0
  31. package/src/components/dropdown-menu.tsx +258 -0
  32. package/src/components/empty.tsx +100 -0
  33. package/src/components/field.tsx +248 -0
  34. package/src/components/form.tsx +169 -0
  35. package/src/components/hover-card.tsx +44 -0
  36. package/src/components/input-group.tsx +170 -0
  37. package/src/components/input-otp.tsx +77 -0
  38. package/src/components/input.tsx +21 -0
  39. package/src/components/item.tsx +193 -0
  40. package/src/components/kbd.tsx +28 -0
  41. package/src/components/label.tsx +24 -0
  42. package/src/components/menubar.tsx +276 -0
  43. package/src/components/navigation-menu.tsx +168 -0
  44. package/src/components/pagination.tsx +130 -0
  45. package/src/components/popover.tsx +88 -0
  46. package/src/components/progress.tsx +31 -0
  47. package/src/components/radio-group.tsx +45 -0
  48. package/src/components/resizable.tsx +56 -0
  49. package/src/components/scroll-area.tsx +58 -0
  50. package/src/components/select.tsx +189 -0
  51. package/src/components/separator.tsx +28 -0
  52. package/src/components/sheet.tsx +140 -0
  53. package/src/components/sidebar.tsx +862 -0
  54. package/src/components/skeleton.tsx +13 -0
  55. package/src/components/slider.tsx +63 -0
  56. package/src/components/sonner.tsx +40 -0
  57. package/src/components/spinner.tsx +16 -0
  58. package/src/components/stepper.tsx +291 -0
  59. package/src/components/switch.tsx +31 -0
  60. package/src/components/table.tsx +133 -0
  61. package/src/components/tabs.tsx +66 -0
  62. package/src/components/textarea.tsx +18 -0
  63. package/src/components/toggle-group.tsx +83 -0
  64. package/src/components/toggle.tsx +47 -0
  65. package/src/components/tooltip.tsx +66 -0
  66. package/src/custom/action-button.tsx +48 -0
  67. package/src/custom/async-select.tsx +287 -0
  68. package/src/custom/awesome-not-found.tsx +116 -0
  69. package/src/custom/charts/area-chart.tsx +147 -0
  70. package/src/custom/charts/bar-chart.tsx +233 -0
  71. package/src/custom/charts/chart-card.tsx +103 -0
  72. package/src/custom/charts/index.tsx +16 -0
  73. package/src/custom/charts/pie-chart.tsx +168 -0
  74. package/src/custom/charts/radar-chart.tsx +126 -0
  75. package/src/custom/checkbox-tree.tsx +100 -0
  76. package/src/custom/combobox.tsx +296 -0
  77. package/src/custom/confirm-dialog.tsx +102 -0
  78. package/src/custom/country-selector.tsx +204 -0
  79. package/src/custom/date-picker/calendar-rac.tsx +109 -0
  80. package/src/custom/date-picker/datefield-rac.tsx +84 -0
  81. package/src/custom/date-picker/index.tsx +273 -0
  82. package/src/custom/date-picker/types/index.ts +4 -0
  83. package/src/custom/date-picker/utils/index.ts +42 -0
  84. package/src/custom/date-picker-old.tsx +50 -0
  85. package/src/custom/date-tooltip.tsx +98 -0
  86. package/src/custom/document-scanner/consts.ts +5 -0
  87. package/src/custom/document-scanner/corner-adjustment/action-buttons.tsx +33 -0
  88. package/src/custom/document-scanner/corner-adjustment/corner-handle.tsx +43 -0
  89. package/src/custom/document-scanner/corner-adjustment/hooks/use-corner-drag.ts +85 -0
  90. package/src/custom/document-scanner/corner-adjustment/index.tsx +125 -0
  91. package/src/custom/document-scanner/corner-adjustment/types.ts +53 -0
  92. package/src/custom/document-scanner/corner-adjustment/utils/clip-path.ts +22 -0
  93. package/src/custom/document-scanner/corner-adjustment/zoom-magnifier.tsx +115 -0
  94. package/src/custom/document-scanner/hooks/use-document-capture.ts +81 -0
  95. package/src/custom/document-scanner/hooks/use-document-scanner.ts +80 -0
  96. package/src/custom/document-scanner/hooks/use-perspective-crop.ts +38 -0
  97. package/src/custom/document-scanner/index.tsx +255 -0
  98. package/src/custom/document-scanner/lib.ts +407 -0
  99. package/src/custom/document-scanner/types.ts +205 -0
  100. package/src/custom/document-scanner/utils/perspective-correction.ts +139 -0
  101. package/src/custom/document-viewer/controllers.tsx +98 -0
  102. package/src/custom/document-viewer/index.tsx +43 -0
  103. package/src/custom/document-viewer/renderers/image.tsx +37 -0
  104. package/src/custom/document-viewer/renderers/index.tsx +2 -0
  105. package/src/custom/document-viewer/renderers/pdf.tsx +105 -0
  106. package/src/custom/email-input/domains.json +159 -0
  107. package/src/custom/email-input/email.tsx +229 -0
  108. package/src/custom/email-input/index.tsx +4 -0
  109. package/src/custom/email-input/types.ts +104 -0
  110. package/src/custom/file-uploader.tsx +541 -0
  111. package/src/custom/filter-component/fields/async-select.tsx +33 -0
  112. package/src/custom/filter-component/fields/date.tsx +60 -0
  113. package/src/custom/filter-component/fields/multi-select.tsx +30 -0
  114. package/src/custom/filter-component/index.tsx +217 -0
  115. package/src/custom/image-canvas.tsx +260 -0
  116. package/src/custom/json-editor.tsx +22 -0
  117. package/src/custom/master-data-grid/components/dialogs/column-settings-dialog.tsx +100 -0
  118. package/src/custom/master-data-grid/components/dialogs/index.ts +1 -0
  119. package/src/custom/master-data-grid/components/filters/client-filter.tsx +368 -0
  120. package/src/custom/master-data-grid/components/filters/filter-input.tsx +256 -0
  121. package/src/custom/master-data-grid/components/filters/index.ts +3 -0
  122. package/src/custom/master-data-grid/components/filters/inline-column-filter.tsx +233 -0
  123. package/src/custom/master-data-grid/components/filters/multi-filter-dialog.tsx +90 -0
  124. package/src/custom/master-data-grid/components/filters/server-filter.tsx +255 -0
  125. package/src/custom/master-data-grid/components/master-data-grid.tsx +472 -0
  126. package/src/custom/master-data-grid/components/pagination/index.ts +1 -0
  127. package/src/custom/master-data-grid/components/pagination/pagination.tsx +178 -0
  128. package/src/custom/master-data-grid/components/table/cell-renderer.tsx +634 -0
  129. package/src/custom/master-data-grid/components/table/header-cell.tsx +162 -0
  130. package/src/custom/master-data-grid/components/table/index.ts +4 -0
  131. package/src/custom/master-data-grid/components/table/table-body-renderer.tsx +113 -0
  132. package/src/custom/master-data-grid/components/table/virtual-body.tsx +138 -0
  133. package/src/custom/master-data-grid/components/toolbar/index.ts +1 -0
  134. package/src/custom/master-data-grid/components/toolbar/toolbar.tsx +314 -0
  135. package/src/custom/master-data-grid/hooks/index.ts +3 -0
  136. package/src/custom/master-data-grid/hooks/use-columns.tsx +332 -0
  137. package/src/custom/master-data-grid/hooks/use-editing.ts +106 -0
  138. package/src/custom/master-data-grid/hooks/use-table-state-reducer.ts +157 -0
  139. package/src/custom/master-data-grid/hooks/use-table-state.ts +31 -0
  140. package/src/custom/master-data-grid/index.ts +16 -0
  141. package/src/custom/master-data-grid/types.ts +466 -0
  142. package/src/custom/master-data-grid/utils/column-generator.tsx +306 -0
  143. package/src/custom/master-data-grid/utils/export-utils.ts +67 -0
  144. package/src/custom/master-data-grid/utils/filter-fns.ts +290 -0
  145. package/src/custom/master-data-grid/utils/index.ts +8 -0
  146. package/src/custom/master-data-grid/utils/pinning-utils.ts +88 -0
  147. package/src/custom/master-data-grid/utils/translation-utils.ts +42 -0
  148. package/src/custom/multi-select.tsx +432 -0
  149. package/src/custom/password-input.tsx +194 -0
  150. package/src/custom/phone-input.tsx +172 -0
  151. package/src/custom/schema-form/custom/index.tsx +1 -0
  152. package/src/custom/schema-form/custom/label.tsx +53 -0
  153. package/src/custom/schema-form/fields/base-input-field.tsx +82 -0
  154. package/src/custom/schema-form/fields/field.tsx +67 -0
  155. package/src/custom/schema-form/fields/index.tsx +5 -0
  156. package/src/custom/schema-form/fields/object.tsx +12 -0
  157. package/src/custom/schema-form/fields/table-array/array-field-item.tsx +90 -0
  158. package/src/custom/schema-form/fields/table-array/array-field-template.tsx +115 -0
  159. package/src/custom/schema-form/index.tsx +259 -0
  160. package/src/custom/schema-form/templates/description.tsx +20 -0
  161. package/src/custom/schema-form/templates/index.tsx +2 -0
  162. package/src/custom/schema-form/templates/submit.tsx +32 -0
  163. package/src/custom/schema-form/types.ts +64 -0
  164. package/src/custom/schema-form/utils/index.ts +4 -0
  165. package/src/custom/schema-form/utils/schema-dependency.ts +655 -0
  166. package/src/custom/schema-form/utils/schemas.ts +289 -0
  167. package/src/custom/schema-form/utils/validation.ts +23 -0
  168. package/src/custom/schema-form/widgets/boolean.tsx +77 -0
  169. package/src/custom/schema-form/widgets/combobox.tsx +274 -0
  170. package/src/custom/schema-form/widgets/date.tsx +59 -0
  171. package/src/custom/schema-form/widgets/email.tsx +34 -0
  172. package/src/custom/schema-form/widgets/index.tsx +10 -0
  173. package/src/custom/schema-form/widgets/password.tsx +40 -0
  174. package/src/custom/schema-form/widgets/phone.tsx +40 -0
  175. package/src/custom/schema-form/widgets/select.tsx +105 -0
  176. package/src/custom/schema-form/widgets/selectable.tsx +25 -0
  177. package/src/custom/schema-form/widgets/string-array.tsx +296 -0
  178. package/src/custom/schema-form/widgets/url.tsx +56 -0
  179. package/src/custom/section-layout-v2.tsx +212 -0
  180. package/src/custom/select-tabs.tsx +109 -0
  181. package/src/custom/selectable.tsx +316 -0
  182. package/src/custom/stepper.tsx +236 -0
  183. package/src/custom/tab-layout.tsx +213 -0
  184. package/src/custom/tanstack-table/fields/index.tsx +12 -0
  185. package/src/custom/tanstack-table/fields/tanstack-table-action-dialogs.tsx +89 -0
  186. package/src/custom/tanstack-table/fields/tanstack-table-column-header.tsx +66 -0
  187. package/src/custom/tanstack-table/fields/tanstack-table-filter-date.tsx +180 -0
  188. package/src/custom/tanstack-table/fields/tanstack-table-filter-faceted.tsx +158 -0
  189. package/src/custom/tanstack-table/fields/tanstack-table-filter-text.tsx +76 -0
  190. package/src/custom/tanstack-table/fields/tanstack-table-pagination.tsx +136 -0
  191. package/src/custom/tanstack-table/fields/tanstack-table-plain-table.tsx +142 -0
  192. package/src/custom/tanstack-table/fields/tanstack-table-row-actions-confirmation.tsx +77 -0
  193. package/src/custom/tanstack-table/fields/tanstack-table-row-actions-custom-dialog.tsx +87 -0
  194. package/src/custom/tanstack-table/fields/tanstack-table-row-actions.tsx +151 -0
  195. package/src/custom/tanstack-table/fields/tanstack-table-table-actions-custom-dialog.tsx +88 -0
  196. package/src/custom/tanstack-table/fields/tanstack-table-table-actions-schemaform-dialog.tsx +47 -0
  197. package/src/custom/tanstack-table/fields/tanstack-table-toolbar.tsx +143 -0
  198. package/src/custom/tanstack-table/fields/tanstack-table-view-options.tsx +171 -0
  199. package/src/custom/tanstack-table/index.tsx +244 -0
  200. package/src/custom/tanstack-table/types/index.ts +328 -0
  201. package/src/custom/tanstack-table/utils/cell-with-actions.tsx +21 -0
  202. package/src/custom/tanstack-table/utils/column-names.ts +26 -0
  203. package/src/custom/tanstack-table/utils/columns-by-row-data.tsx +312 -0
  204. package/src/custom/tanstack-table/utils/editable-columns-by-row-data.tsx +219 -0
  205. package/src/custom/tanstack-table/utils/faceted-boolean-options.tsx +22 -0
  206. package/src/custom/tanstack-table/utils/index.tsx +10 -0
  207. package/src/custom/tanstack-table/utils/pinning-styles.ts +57 -0
  208. package/src/custom/tanstack-table/utils/table.tsx +83 -0
  209. package/src/custom/tanstack-table/utils/test-conditions.ts +17 -0
  210. package/src/custom/timeline.tsx +208 -0
  211. package/src/custom/tree.tsx +200 -0
  212. package/src/custom/tscanify/browser.ts +66 -0
  213. package/src/custom/tscanify/index.ts +51 -0
  214. package/src/custom/tscanify/tscanify-browser.ts +522 -0
  215. package/src/custom/tscanify/tscanify.ts +262 -0
  216. package/src/custom/tscanify/types.ts +22 -0
  217. package/src/custom/webcam.tsx +737 -0
  218. package/src/hooks/.gitkeep +0 -0
  219. package/src/hooks/use-callback-ref.ts +27 -0
  220. package/src/hooks/use-controllable-state.ts +67 -0
  221. package/src/hooks/use-debounce.ts +19 -0
  222. package/src/hooks/use-is-visible.ts +23 -0
  223. package/src/hooks/use-media-query.ts +21 -0
  224. package/src/hooks/use-mobile.ts +21 -0
  225. package/src/hooks/use-on-window-resize.ts +15 -0
  226. package/src/hooks/use-scroll.tsx +22 -0
  227. package/src/lib/utils.ts +61 -0
  228. package/src/lib/zod.ts +2 -0
  229. package/src/styles/core.css +57 -0
  230. package/src/styles/globals.css +130 -0
  231. package/src/test/email-input.test.tsx +217 -0
  232. package/src/test/password-input.test.tsx +92 -0
  233. package/src/test/select-tabs.test.tsx +302 -0
  234. package/src/test/selectable.test.tsx +1093 -0
  235. package/tsconfig.json +13 -0
  236. 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
+ }