@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,522 @@
|
|
|
1
|
+
/*! tscanify-browser v1.0.0 | Based on jscanify v1.4.0 | (c) ColonelParrot and other contributors | MIT License */
|
|
2
|
+
|
|
3
|
+
import cv, { Mat } from "opencv-ts";
|
|
4
|
+
import { CornerPoints, HighlightOptions, Point, WindowWithCV } from "./types";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Calculates distance between two points.
|
|
8
|
+
* @param p1 point 1
|
|
9
|
+
* @param p2 point 2
|
|
10
|
+
* @returns distance between two points
|
|
11
|
+
*/
|
|
12
|
+
function distance(p1: Point, p2: Point): number {
|
|
13
|
+
return Math.hypot(p1.x - p2.x, p1.y - p2.y);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export class TScanifyBrowser {
|
|
17
|
+
constructor() {}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Loads OpenCV module
|
|
21
|
+
* @param callback Function to call when OpenCV is initialized
|
|
22
|
+
*/
|
|
23
|
+
loadOpenCV(callback: (opencv: typeof cv) => void): void {
|
|
24
|
+
// First check if global cv is available (browser-loaded version)
|
|
25
|
+
if (
|
|
26
|
+
(window as unknown as WindowWithCV).cv &&
|
|
27
|
+
(window as unknown as WindowWithCV).cv.Mat
|
|
28
|
+
) {
|
|
29
|
+
callback((window as unknown as WindowWithCV).cv);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Next, check if the imported cv is available
|
|
34
|
+
if (cv && (cv as typeof cv).Mat) {
|
|
35
|
+
callback(cv);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Wait for opencv to be ready (check both global and imported)
|
|
40
|
+
const checkInterval = setInterval(() => {
|
|
41
|
+
// First priority: global cv
|
|
42
|
+
if (
|
|
43
|
+
(window as unknown as WindowWithCV).cv &&
|
|
44
|
+
(window as unknown as WindowWithCV).cv.Mat
|
|
45
|
+
) {
|
|
46
|
+
clearInterval(checkInterval);
|
|
47
|
+
callback((window as unknown as WindowWithCV).cv);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Second priority: imported cv
|
|
52
|
+
if (cv && (cv as typeof cv).Mat) {
|
|
53
|
+
clearInterval(checkInterval);
|
|
54
|
+
callback(cv);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
}, 200);
|
|
58
|
+
|
|
59
|
+
// Set timeout to avoid infinite waiting
|
|
60
|
+
setTimeout(() => {
|
|
61
|
+
clearInterval(checkInterval);
|
|
62
|
+
// Try to load it dynamically as a last resort
|
|
63
|
+
this.loadOpenCVDynamically().then((loadedCv) => {
|
|
64
|
+
callback(loadedCv);
|
|
65
|
+
});
|
|
66
|
+
}, 8000); // 8 seconds timeout
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Dynamically loads OpenCV.js as a last resort
|
|
71
|
+
* @returns Promise that resolves with the OpenCV object
|
|
72
|
+
*/
|
|
73
|
+
private loadOpenCVDynamically(): Promise<typeof cv> {
|
|
74
|
+
return new Promise((resolve, reject) => {
|
|
75
|
+
if (
|
|
76
|
+
(window as unknown as WindowWithCV).cv &&
|
|
77
|
+
(window as unknown as WindowWithCV).cv.Mat
|
|
78
|
+
) {
|
|
79
|
+
resolve((window as unknown as WindowWithCV).cv);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Try to load OpenCV.js dynamically
|
|
84
|
+
const script = document.createElement("script");
|
|
85
|
+
script.setAttribute("async", "true");
|
|
86
|
+
script.setAttribute("type", "text/javascript");
|
|
87
|
+
script.setAttribute("src", "https://docs.opencv.org/4.5.5/opencv.js");
|
|
88
|
+
|
|
89
|
+
script.onload = () => {
|
|
90
|
+
// Check if OpenCV is available
|
|
91
|
+
const checkInterval = setInterval(() => {
|
|
92
|
+
if (
|
|
93
|
+
(window as unknown as WindowWithCV).cv &&
|
|
94
|
+
(window as unknown as WindowWithCV).cv.Mat
|
|
95
|
+
) {
|
|
96
|
+
clearInterval(checkInterval);
|
|
97
|
+
resolve((window as unknown as WindowWithCV).cv);
|
|
98
|
+
}
|
|
99
|
+
}, 100);
|
|
100
|
+
|
|
101
|
+
// Timeout after 5 seconds
|
|
102
|
+
setTimeout(() => {
|
|
103
|
+
clearInterval(checkInterval);
|
|
104
|
+
reject(new Error("OpenCV dynamic load timed out"));
|
|
105
|
+
}, 5000);
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
script.onerror = () => {
|
|
109
|
+
reject(new Error("Failed to load OpenCV.js dynamically"));
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
document.body.appendChild(script);
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Creates a new canvas of the given width and height
|
|
118
|
+
* @param width Canvas width
|
|
119
|
+
* @param height Canvas height
|
|
120
|
+
* @returns Canvas object
|
|
121
|
+
*/
|
|
122
|
+
createCanvas(width: number, height: number): HTMLCanvasElement {
|
|
123
|
+
const canvas = document.createElement("canvas");
|
|
124
|
+
canvas.width = width;
|
|
125
|
+
canvas.height = height;
|
|
126
|
+
return canvas;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Gets a Mat from an image or canvas
|
|
131
|
+
* @param image Image or Canvas element
|
|
132
|
+
* @returns OpenCV Mat
|
|
133
|
+
*/
|
|
134
|
+
imageToMat(image: HTMLImageElement | HTMLCanvasElement): Mat {
|
|
135
|
+
const canvas = document.createElement("canvas");
|
|
136
|
+
const ctx = canvas.getContext("2d")!;
|
|
137
|
+
|
|
138
|
+
// Set canvas dimensions to match input
|
|
139
|
+
canvas.width = image.width as number;
|
|
140
|
+
canvas.height = image.height as number;
|
|
141
|
+
|
|
142
|
+
// Draw the image to the canvas
|
|
143
|
+
ctx.drawImage(image, 0, 0, image.width as number, image.height as number);
|
|
144
|
+
|
|
145
|
+
// Get the image data from the canvas
|
|
146
|
+
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
|
147
|
+
|
|
148
|
+
// Convert to Mat
|
|
149
|
+
return cv.matFromImageData(imageData);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Resizes an image to fit within the given maximum width and height
|
|
154
|
+
* @param image Image to resize
|
|
155
|
+
* @param maxWidth Maximum width
|
|
156
|
+
* @param maxHeight Maximum height
|
|
157
|
+
* @returns Resized image as Canvas
|
|
158
|
+
*/
|
|
159
|
+
resizeImage(
|
|
160
|
+
image: HTMLImageElement | HTMLCanvasElement,
|
|
161
|
+
maxWidth = 500,
|
|
162
|
+
maxHeight = 500
|
|
163
|
+
): HTMLCanvasElement {
|
|
164
|
+
const canvas = this.createCanvas(1, 1);
|
|
165
|
+
const ctx = canvas.getContext("2d")!;
|
|
166
|
+
|
|
167
|
+
const originalWidth = image.width as number;
|
|
168
|
+
const originalHeight = image.height as number;
|
|
169
|
+
|
|
170
|
+
// Calculate new dimensions
|
|
171
|
+
let newWidth = originalWidth;
|
|
172
|
+
let newHeight = originalHeight;
|
|
173
|
+
|
|
174
|
+
if (originalWidth > maxWidth) {
|
|
175
|
+
newWidth = maxWidth;
|
|
176
|
+
newHeight = (originalHeight * maxWidth) / originalWidth;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (newHeight > maxHeight) {
|
|
180
|
+
newHeight = maxHeight;
|
|
181
|
+
newWidth = (newWidth * maxHeight) / newHeight;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Set canvas to new dimensions
|
|
185
|
+
canvas.width = newWidth;
|
|
186
|
+
canvas.height = newHeight;
|
|
187
|
+
|
|
188
|
+
// Draw the image at the new size
|
|
189
|
+
ctx.drawImage(image, 0, 0, newWidth, newHeight);
|
|
190
|
+
return canvas;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Finds the corners of a document in an image
|
|
195
|
+
* @param src Input image as Mat
|
|
196
|
+
* @returns Four corner points of the document
|
|
197
|
+
*/
|
|
198
|
+
findDocumentCorners(src: Mat): CornerPoints | null {
|
|
199
|
+
try {
|
|
200
|
+
// Convert to grayscale
|
|
201
|
+
const gray = new cv.Mat();
|
|
202
|
+
cv.cvtColor(src, gray, cv.COLOR_RGBA2GRAY);
|
|
203
|
+
|
|
204
|
+
// Apply Gaussian blur
|
|
205
|
+
const blur = new cv.Mat();
|
|
206
|
+
const ksize = new cv.Size(5, 5);
|
|
207
|
+
cv.GaussianBlur(gray, blur, ksize, 0, 0, cv.BORDER_DEFAULT);
|
|
208
|
+
|
|
209
|
+
// Apply Canny edge detection
|
|
210
|
+
const edges = new cv.Mat();
|
|
211
|
+
cv.Canny(blur, edges, 75, 200);
|
|
212
|
+
|
|
213
|
+
// Find contours
|
|
214
|
+
const contours = new cv.MatVector();
|
|
215
|
+
const hierarchy = new cv.Mat();
|
|
216
|
+
cv.findContours(
|
|
217
|
+
edges,
|
|
218
|
+
contours,
|
|
219
|
+
hierarchy,
|
|
220
|
+
cv.RETR_EXTERNAL,
|
|
221
|
+
cv.CHAIN_APPROX_SIMPLE
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
// Find largest contour
|
|
225
|
+
let maxArea = 0;
|
|
226
|
+
let largestContourIndex = -1;
|
|
227
|
+
|
|
228
|
+
for (let i = 0; i < contours.size(); i++) {
|
|
229
|
+
const contour = contours.get(i);
|
|
230
|
+
const area = cv.contourArea(contour);
|
|
231
|
+
|
|
232
|
+
if (area > maxArea) {
|
|
233
|
+
maxArea = area;
|
|
234
|
+
largestContourIndex = i;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
contour.delete();
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// If no contour found, return null
|
|
241
|
+
if (largestContourIndex === -1) {
|
|
242
|
+
// Clean up
|
|
243
|
+
gray.delete();
|
|
244
|
+
blur.delete();
|
|
245
|
+
edges.delete();
|
|
246
|
+
contours.delete();
|
|
247
|
+
hierarchy.delete();
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Get largest contour
|
|
252
|
+
const largestContour = contours.get(largestContourIndex);
|
|
253
|
+
|
|
254
|
+
// Approximate the contour with a polygon
|
|
255
|
+
const epsilon = 0.02 * cv.arcLength(largestContour, true);
|
|
256
|
+
const approx = new cv.Mat();
|
|
257
|
+
cv.approxPolyDP(largestContour, approx, epsilon, true);
|
|
258
|
+
|
|
259
|
+
// If the approximation has 4 points, we assume it's a quadrilateral
|
|
260
|
+
let corners: CornerPoints | null = null;
|
|
261
|
+
|
|
262
|
+
if (approx.rows === 4) {
|
|
263
|
+
const points: Point[] = [];
|
|
264
|
+
|
|
265
|
+
for (let i = 0; i < 4; i++) {
|
|
266
|
+
points.push({
|
|
267
|
+
x: approx.data32S[i * 2] as number,
|
|
268
|
+
y: approx.data32S[i * 2 + 1] as number,
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Order the points: top-left, top-right, bottom-right, bottom-left
|
|
273
|
+
points.sort((a, b) => a.y - b.y);
|
|
274
|
+
|
|
275
|
+
// Top points (sorted by y)
|
|
276
|
+
const topPoints = points.slice(0, 2).sort((a, b) => a.x - b.x);
|
|
277
|
+
const topLeftCorner = topPoints[0]!;
|
|
278
|
+
const topRightCorner = topPoints[1]!;
|
|
279
|
+
|
|
280
|
+
// Bottom points (sorted by y)
|
|
281
|
+
const bottomPoints = points.slice(2).sort((a, b) => a.x - b.x);
|
|
282
|
+
const bottomLeftCorner = bottomPoints[0]!;
|
|
283
|
+
const bottomRightCorner = bottomPoints[1]!;
|
|
284
|
+
|
|
285
|
+
corners = {
|
|
286
|
+
topLeftCorner,
|
|
287
|
+
topRightCorner,
|
|
288
|
+
bottomRightCorner,
|
|
289
|
+
bottomLeftCorner,
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Clean up
|
|
294
|
+
gray.delete();
|
|
295
|
+
blur.delete();
|
|
296
|
+
edges.delete();
|
|
297
|
+
contours.delete();
|
|
298
|
+
hierarchy.delete();
|
|
299
|
+
approx.delete();
|
|
300
|
+
largestContour.delete();
|
|
301
|
+
|
|
302
|
+
return corners;
|
|
303
|
+
} catch {
|
|
304
|
+
return null;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Performs perspective transform on an image based on corner points
|
|
310
|
+
* @param src Source image as Mat
|
|
311
|
+
* @param corners Corner points of document
|
|
312
|
+
* @param width Output width
|
|
313
|
+
* @param height Output height
|
|
314
|
+
* @returns Transformed Mat
|
|
315
|
+
*/
|
|
316
|
+
warpPerspective(
|
|
317
|
+
src: Mat,
|
|
318
|
+
corners: CornerPoints,
|
|
319
|
+
width: number,
|
|
320
|
+
height: number
|
|
321
|
+
): Mat {
|
|
322
|
+
// Create matrices for source and destination points
|
|
323
|
+
const srcPoints = cv.matFromArray(4, 1, cv.CV_32FC2, [
|
|
324
|
+
corners.topLeftCorner.x,
|
|
325
|
+
corners.topLeftCorner.y,
|
|
326
|
+
corners.topRightCorner.x,
|
|
327
|
+
corners.topRightCorner.y,
|
|
328
|
+
corners.bottomRightCorner.x,
|
|
329
|
+
corners.bottomRightCorner.y,
|
|
330
|
+
corners.bottomLeftCorner.x,
|
|
331
|
+
corners.bottomLeftCorner.y,
|
|
332
|
+
]);
|
|
333
|
+
|
|
334
|
+
const dstPoints = cv.matFromArray(4, 1, cv.CV_32FC2, [
|
|
335
|
+
0,
|
|
336
|
+
0,
|
|
337
|
+
width,
|
|
338
|
+
0,
|
|
339
|
+
width,
|
|
340
|
+
height,
|
|
341
|
+
0,
|
|
342
|
+
height,
|
|
343
|
+
]);
|
|
344
|
+
|
|
345
|
+
// Get perspective transform matrix
|
|
346
|
+
const M = cv.getPerspectiveTransform(srcPoints, dstPoints);
|
|
347
|
+
|
|
348
|
+
// Apply perspective transform
|
|
349
|
+
const dst = new cv.Mat();
|
|
350
|
+
cv.warpPerspective(
|
|
351
|
+
src,
|
|
352
|
+
dst,
|
|
353
|
+
M,
|
|
354
|
+
new cv.Size(width, height),
|
|
355
|
+
cv.INTER_LINEAR,
|
|
356
|
+
cv.BORDER_CONSTANT,
|
|
357
|
+
new cv.Scalar()
|
|
358
|
+
);
|
|
359
|
+
|
|
360
|
+
// Clean up
|
|
361
|
+
srcPoints.delete();
|
|
362
|
+
dstPoints.delete();
|
|
363
|
+
M.delete();
|
|
364
|
+
|
|
365
|
+
return dst;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Highlights corners of a detected document in an image
|
|
370
|
+
* @param image Source image
|
|
371
|
+
* @param corners Corner points to highlight
|
|
372
|
+
* @param options Highlight options
|
|
373
|
+
* @returns Canvas with highlighted corners
|
|
374
|
+
*/
|
|
375
|
+
highlightCorners(
|
|
376
|
+
image: HTMLImageElement | HTMLCanvasElement,
|
|
377
|
+
corners: CornerPoints,
|
|
378
|
+
options: HighlightOptions = {}
|
|
379
|
+
): HTMLCanvasElement {
|
|
380
|
+
// Use our own default options since the HighlightOptions interface only has color and thickness
|
|
381
|
+
const thickness = options.thickness || 2;
|
|
382
|
+
const color = options.color || "#3cba54";
|
|
383
|
+
const pointRadius = 3;
|
|
384
|
+
const pointColor = "#db3236";
|
|
385
|
+
|
|
386
|
+
const canvas = this.createCanvas(
|
|
387
|
+
image.width as number,
|
|
388
|
+
image.height as number
|
|
389
|
+
);
|
|
390
|
+
const ctx = canvas.getContext("2d")!;
|
|
391
|
+
|
|
392
|
+
// Draw the original image
|
|
393
|
+
ctx.drawImage(image, 0, 0);
|
|
394
|
+
|
|
395
|
+
// Draw lines connecting the corners
|
|
396
|
+
ctx.strokeStyle = color;
|
|
397
|
+
ctx.lineWidth = thickness;
|
|
398
|
+
ctx.beginPath();
|
|
399
|
+
ctx.moveTo(corners.topLeftCorner.x, corners.topLeftCorner.y);
|
|
400
|
+
ctx.lineTo(corners.topRightCorner.x, corners.topRightCorner.y);
|
|
401
|
+
ctx.lineTo(corners.bottomRightCorner.x, corners.bottomRightCorner.y);
|
|
402
|
+
ctx.lineTo(corners.bottomLeftCorner.x, corners.bottomLeftCorner.y);
|
|
403
|
+
ctx.lineTo(corners.topLeftCorner.x, corners.topLeftCorner.y);
|
|
404
|
+
ctx.stroke();
|
|
405
|
+
|
|
406
|
+
// Draw dots at the corners
|
|
407
|
+
const cornerPoints = [
|
|
408
|
+
corners.topLeftCorner,
|
|
409
|
+
corners.topRightCorner,
|
|
410
|
+
corners.bottomRightCorner,
|
|
411
|
+
corners.bottomLeftCorner,
|
|
412
|
+
];
|
|
413
|
+
|
|
414
|
+
ctx.fillStyle = pointColor;
|
|
415
|
+
cornerPoints.forEach((point) => {
|
|
416
|
+
ctx.beginPath();
|
|
417
|
+
ctx.arc(point.x, point.y, pointRadius, 0, Math.PI * 2);
|
|
418
|
+
ctx.fill();
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
return canvas;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Processes an image to detect and extract a document
|
|
426
|
+
* @param image Input image
|
|
427
|
+
* @returns Object containing original, highlighted and processed images as canvases
|
|
428
|
+
*/
|
|
429
|
+
async processImage(image: HTMLImageElement): Promise<{
|
|
430
|
+
original: HTMLCanvasElement;
|
|
431
|
+
highlighted: HTMLCanvasElement | null;
|
|
432
|
+
processed: HTMLCanvasElement | null;
|
|
433
|
+
}> {
|
|
434
|
+
// Resize the image for processing
|
|
435
|
+
const resized = this.resizeImage(image);
|
|
436
|
+
|
|
437
|
+
// Convert to Mat
|
|
438
|
+
const src = this.imageToMat(resized);
|
|
439
|
+
|
|
440
|
+
// Find document corners
|
|
441
|
+
const corners = this.findDocumentCorners(src);
|
|
442
|
+
|
|
443
|
+
if (!corners) {
|
|
444
|
+
// No document found
|
|
445
|
+
src.delete();
|
|
446
|
+
return {
|
|
447
|
+
original: resized,
|
|
448
|
+
highlighted: null,
|
|
449
|
+
processed: null,
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// Highlight corners
|
|
454
|
+
const highlighted = this.highlightCorners(resized, corners);
|
|
455
|
+
|
|
456
|
+
// Calculate output dimensions
|
|
457
|
+
const width = Math.max(
|
|
458
|
+
distance(corners.topRightCorner, corners.topLeftCorner),
|
|
459
|
+
distance(corners.bottomRightCorner, corners.bottomLeftCorner)
|
|
460
|
+
);
|
|
461
|
+
|
|
462
|
+
const height = Math.max(
|
|
463
|
+
distance(corners.topLeftCorner, corners.bottomLeftCorner),
|
|
464
|
+
distance(corners.topRightCorner, corners.bottomRightCorner)
|
|
465
|
+
);
|
|
466
|
+
|
|
467
|
+
// Apply perspective transform
|
|
468
|
+
const warped = this.warpPerspective(src, corners, width, height);
|
|
469
|
+
|
|
470
|
+
// Convert Mat to Canvas
|
|
471
|
+
const processed = this.createCanvas(width, height);
|
|
472
|
+
const ctx = processed.getContext("2d")!;
|
|
473
|
+
|
|
474
|
+
const imgData = new ImageData(
|
|
475
|
+
new Uint8ClampedArray(warped.data),
|
|
476
|
+
warped.cols,
|
|
477
|
+
warped.rows
|
|
478
|
+
);
|
|
479
|
+
|
|
480
|
+
ctx.putImageData(imgData, 0, 0);
|
|
481
|
+
|
|
482
|
+
// Clean up
|
|
483
|
+
src.delete();
|
|
484
|
+
warped.delete();
|
|
485
|
+
|
|
486
|
+
return {
|
|
487
|
+
original: resized,
|
|
488
|
+
highlighted,
|
|
489
|
+
processed,
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Converts a canvas to a data URL
|
|
495
|
+
* @param canvas Canvas to convert
|
|
496
|
+
* @param type Image type (default: 'image/png')
|
|
497
|
+
* @param quality Image quality for JPEG (0-1)
|
|
498
|
+
* @returns Data URL string
|
|
499
|
+
*/
|
|
500
|
+
canvasToDataURL(
|
|
501
|
+
canvas: HTMLCanvasElement,
|
|
502
|
+
type = "image/png",
|
|
503
|
+
quality = 0.9
|
|
504
|
+
): string {
|
|
505
|
+
return canvas.toDataURL(type, quality);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Creates an image element from a data URL
|
|
510
|
+
* @param dataURL Data URL string
|
|
511
|
+
* @returns Promise resolving to an Image element
|
|
512
|
+
*/
|
|
513
|
+
dataURLToImage(dataURL: string): Promise<HTMLImageElement> {
|
|
514
|
+
return new Promise((resolve, reject) => {
|
|
515
|
+
const img = new Image();
|
|
516
|
+
img.onload = () => resolve(img);
|
|
517
|
+
img.onerror = () =>
|
|
518
|
+
reject(new Error("Failed to load image from data URL"));
|
|
519
|
+
img.src = dataURL;
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
}
|