@pallasoft/ps-file-components 1.5.0 → 2.0.1
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/dist/components/ps-document-scanner-loader.d.ts +1 -1
- package/dist/components/ps-document-scanner-sheet.d.ts +1 -1
- package/dist/components/ps-document-scanner-sheet.js +40 -110
- package/dist/components/ps-document-scanner.d.ts +1 -1
- package/dist/components/ps-document-scanner.js +2 -3
- package/dist/components/ps-file-delete-confirm.js +1 -1
- package/dist/components/ps-scan-camera-view.d.ts +2 -9
- package/dist/components/ps-scan-camera-view.js +3 -12
- package/dist/components/ps-scan-crop-editor.d.ts +4 -4
- package/dist/components/ps-scan-crop-editor.js +9 -9
- package/dist/components/ps-scan-permission-view.d.ts +2 -3
- package/dist/components/ps-scan-permission-view.js +2 -2
- package/dist/components/ps-scan-photo-button.d.ts +2 -3
- package/dist/components/ps-scan-photo-button.js +5 -5
- package/dist/components/ps-scan-preview.d.ts +12 -0
- package/dist/components/ps-scan-preview.js +10 -0
- package/dist/components/ps-scan-shutter.d.ts +1 -2
- package/dist/components/ps-scan-shutter.js +1 -1
- package/dist/components/ps-upload-modal.js +1 -1
- package/dist/constants/ps-document-scanner-labels.d.ts +5 -34
- package/dist/constants/ps-document-scanner-labels.js +5 -34
- package/dist/constants/ps-scan-chrome-classes.d.ts +0 -2
- package/dist/constants/ps-scan-chrome-classes.js +0 -2
- package/dist/constants/ps-scan-config.d.ts +24 -31
- package/dist/constants/ps-scan-config.js +24 -31
- package/dist/constants/ps-scan-step.d.ts +1 -1
- package/dist/constants/ps-scan-step.js +1 -1
- package/dist/esm/components/ps-document-scanner-sheet.js +41 -111
- package/dist/esm/components/ps-document-scanner.js +2 -3
- package/dist/esm/components/ps-file-delete-confirm.js +2 -2
- package/dist/esm/components/ps-scan-camera-view.js +3 -12
- package/dist/esm/components/ps-scan-crop-editor.js +9 -9
- package/dist/esm/components/ps-scan-permission-view.js +2 -2
- package/dist/esm/components/ps-scan-photo-button.js +5 -5
- package/dist/esm/components/ps-scan-preview.js +6 -0
- package/dist/esm/components/ps-scan-shutter.js +1 -1
- package/dist/esm/components/ps-upload-modal.js +2 -2
- package/dist/esm/constants/ps-document-scanner-labels.js +5 -34
- package/dist/esm/constants/ps-scan-chrome-classes.js +0 -2
- package/dist/esm/constants/ps-scan-config.js +24 -31
- package/dist/esm/constants/ps-scan-step.js +1 -1
- package/dist/esm/hooks/use-ps-scan-live-detection.js +17 -21
- package/dist/esm/hooks/use-ps-scan-photo.js +42 -0
- package/dist/esm/index.js +0 -2
- package/dist/esm/services/ps-scan-camera-service.js +2 -2
- package/dist/esm/services/ps-scan-canny-service.js +93 -0
- package/dist/esm/services/ps-scan-canvas-service.js +7 -1
- package/dist/esm/services/ps-scan-edge-service.js +73 -137
- package/dist/esm/services/ps-scan-file-name-service.js +2 -11
- package/dist/esm/services/ps-scan-geometry-service.js +36 -0
- package/dist/esm/services/ps-scan-hull-service.js +0 -39
- package/dist/esm/services/ps-scan-image-filter-service.js +0 -21
- package/dist/esm/services/ps-scan-morphology-service.js +44 -0
- package/dist/esm/services/ps-scan-pipeline-service.js +9 -19
- package/dist/esm/services/ps-scan-polygon-service.js +75 -0
- package/dist/esm/services/ps-scan-region-service.js +52 -0
- package/dist/esm/services/ps-scan-stability-service.js +9 -22
- package/dist/esm/services/ps-scan-warp-service.js +8 -2
- package/dist/esm/services/ps-scan-yield-service.js +5 -0
- package/dist/hooks/use-ps-scan-live-detection.d.ts +1 -1
- package/dist/hooks/use-ps-scan-live-detection.js +16 -20
- package/dist/hooks/use-ps-scan-photo.d.ts +9 -0
- package/dist/hooks/use-ps-scan-photo.js +46 -0
- package/dist/index.d.ts +0 -2
- package/dist/index.js +0 -2
- package/dist/models/ps-document-scanner-props.d.ts +0 -4
- package/dist/models/ps-document-scanner-sheet-props.d.ts +0 -4
- package/dist/models/ps-scan-gradient.d.ts +4 -0
- package/dist/models/ps-scan-live-detection.d.ts +0 -1
- package/dist/models/ps-scan-located.d.ts +5 -0
- package/dist/models/ps-scan-photo.d.ts +9 -0
- package/dist/models/ps-scan-region.d.ts +5 -0
- package/dist/models/ps-scan-result.d.ts +6 -0
- package/dist/models/ps-scan-stability.d.ts +0 -3
- package/dist/services/ps-scan-camera-service.js +2 -2
- package/dist/services/ps-scan-canny-service.d.ts +7 -0
- package/dist/services/ps-scan-canny-service.js +97 -0
- package/dist/services/ps-scan-canvas-service.js +7 -1
- package/dist/services/ps-scan-edge-service.d.ts +5 -5
- package/dist/services/ps-scan-edge-service.js +73 -137
- package/dist/services/ps-scan-file-name-service.d.ts +1 -3
- package/dist/services/ps-scan-file-name-service.js +2 -11
- package/dist/services/ps-scan-geometry-service.d.ts +2 -0
- package/dist/services/ps-scan-geometry-service.js +36 -0
- package/dist/services/ps-scan-hull-service.d.ts +0 -2
- package/dist/services/ps-scan-hull-service.js +0 -39
- package/dist/services/ps-scan-image-filter-service.d.ts +0 -1
- package/dist/services/ps-scan-image-filter-service.js +0 -21
- package/dist/services/ps-scan-morphology-service.d.ts +7 -0
- package/dist/services/ps-scan-morphology-service.js +48 -0
- package/dist/services/ps-scan-pipeline-service.d.ts +2 -5
- package/dist/services/ps-scan-pipeline-service.js +9 -19
- package/dist/services/ps-scan-polygon-service.d.ts +8 -0
- package/dist/services/ps-scan-polygon-service.js +79 -0
- package/dist/services/ps-scan-region-service.d.ts +4 -0
- package/dist/services/ps-scan-region-service.js +56 -0
- package/dist/services/ps-scan-stability-service.d.ts +1 -3
- package/dist/services/ps-scan-stability-service.js +9 -22
- package/dist/services/ps-scan-warp-service.d.ts +1 -1
- package/dist/services/ps-scan-warp-service.js +8 -2
- package/dist/services/ps-scan-yield-service.d.ts +3 -0
- package/dist/services/ps-scan-yield-service.js +9 -0
- package/package.json +3 -5
- package/dist/components/ps-scan-page-list.d.ts +0 -14
- package/dist/components/ps-scan-page-list.js +0 -22
- package/dist/components/ps-scan-page-stack.d.ts +0 -7
- package/dist/components/ps-scan-page-stack.js +0 -6
- package/dist/components/ps-scan-review.d.ts +0 -24
- package/dist/components/ps-scan-review.js +0 -37
- package/dist/constants/ps-scan-filter.d.ts +0 -7
- package/dist/constants/ps-scan-filter.js +0 -9
- package/dist/constants/ps-scan-output-format.d.ts +0 -5
- package/dist/constants/ps-scan-output-format.js +0 -7
- package/dist/esm/components/ps-scan-page-list.js +0 -18
- package/dist/esm/components/ps-scan-page-stack.js +0 -2
- package/dist/esm/components/ps-scan-review.js +0 -33
- package/dist/esm/constants/ps-scan-filter.js +0 -6
- package/dist/esm/constants/ps-scan-output-format.js +0 -4
- package/dist/esm/hooks/use-ps-scan-pages.js +0 -62
- package/dist/esm/hooks/use-ps-scan-previews.js +0 -57
- package/dist/esm/services/ps-scan-enhance-service.js +0 -97
- package/dist/esm/services/ps-scan-order-service.js +0 -13
- package/dist/esm/services/ps-scan-output-service.js +0 -24
- package/dist/esm/services/ps-scan-pdf-service.js +0 -56
- package/dist/hooks/use-ps-scan-pages.d.ts +0 -12
- package/dist/hooks/use-ps-scan-pages.js +0 -66
- package/dist/hooks/use-ps-scan-previews.d.ts +0 -2
- package/dist/hooks/use-ps-scan-previews.js +0 -61
- package/dist/models/ps-scan-added-page.d.ts +0 -5
- package/dist/models/ps-scan-levels.d.ts +0 -4
- package/dist/models/ps-scan-output-options.d.ts +0 -7
- package/dist/models/ps-scan-page.d.ts +0 -12
- package/dist/models/ps-scan-pdf-page.d.ts +0 -5
- package/dist/services/ps-scan-enhance-service.d.ts +0 -9
- package/dist/services/ps-scan-enhance-service.js +0 -101
- package/dist/services/ps-scan-order-service.d.ts +0 -4
- package/dist/services/ps-scan-order-service.js +0 -17
- package/dist/services/ps-scan-output-service.d.ts +0 -5
- package/dist/services/ps-scan-output-service.js +0 -28
- package/dist/services/ps-scan-pdf-service.d.ts +0 -5
- package/dist/services/ps-scan-pdf-service.js +0 -60
- /package/dist/esm/models/{ps-scan-added-page.js → ps-scan-gradient.js} +0 -0
- /package/dist/esm/models/{ps-scan-levels.js → ps-scan-located.js} +0 -0
- /package/dist/esm/models/{ps-scan-output-options.js → ps-scan-photo.js} +0 -0
- /package/dist/esm/models/{ps-scan-page.js → ps-scan-region.js} +0 -0
- /package/dist/esm/models/{ps-scan-pdf-page.js → ps-scan-result.js} +0 -0
- /package/dist/models/{ps-scan-added-page.js → ps-scan-gradient.js} +0 -0
- /package/dist/models/{ps-scan-levels.js → ps-scan-located.js} +0 -0
- /package/dist/models/{ps-scan-output-options.js → ps-scan-photo.js} +0 -0
- /package/dist/models/{ps-scan-page.js → ps-scan-region.js} +0 -0
- /package/dist/models/{ps-scan-pdf-page.js → ps-scan-result.js} +0 -0
|
@@ -1,156 +1,92 @@
|
|
|
1
1
|
import { PsScanConfig } from '../constants/ps-scan-config';
|
|
2
|
+
import { PsScanCannyService } from './ps-scan-canny-service';
|
|
2
3
|
import { PsScanGeometryService } from './ps-scan-geometry-service';
|
|
3
4
|
import { PsScanHullService } from './ps-scan-hull-service';
|
|
4
5
|
import { PsScanImageFilterService } from './ps-scan-image-filter-service';
|
|
6
|
+
import { PsScanMorphologyService } from './ps-scan-morphology-service';
|
|
7
|
+
import { PsScanPolygonService } from './ps-scan-polygon-service';
|
|
5
8
|
import { PsScanRasterService } from './ps-scan-raster-service';
|
|
9
|
+
import { PsScanRegionService } from './ps-scan-region-service';
|
|
10
|
+
import { PsScanYieldService } from './ps-scan-yield-service';
|
|
6
11
|
export class PsScanEdgeService {
|
|
7
|
-
static detect(source) {
|
|
8
|
-
const scale = Math.min(1,
|
|
9
|
-
const width = Math.max(
|
|
10
|
-
const height = Math.max(
|
|
12
|
+
static async detect(source, maxDimension) {
|
|
13
|
+
const scale = Math.min(1, maxDimension / Math.max(source.width, source.height));
|
|
14
|
+
const width = Math.max(16, Math.round(source.width * scale));
|
|
15
|
+
const height = Math.max(16, Math.round(source.height * scale));
|
|
11
16
|
const raster = PsScanRasterService.resize(source, width, height);
|
|
12
17
|
const gray = PsScanImageFilterService.blur(PsScanImageFilterService.grayscale(raster), width, height);
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
for (let
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
await PsScanYieldService.next();
|
|
19
|
+
const edges = PsScanCannyService.edges(gray, width, height);
|
|
20
|
+
for (let index = 0; index < width; index += 1) {
|
|
21
|
+
edges[index] = 1;
|
|
22
|
+
edges[(height - 1) * width + index] = 1;
|
|
23
|
+
}
|
|
24
|
+
for (let index = 0; index < height; index += 1) {
|
|
25
|
+
edges[index * width] = 1;
|
|
26
|
+
edges[index * width + width - 1] = 1;
|
|
27
|
+
}
|
|
28
|
+
let best = null;
|
|
29
|
+
let bestArea = 0;
|
|
30
|
+
for (const radius of PsScanConfig.closeRadii) {
|
|
31
|
+
await PsScanYieldService.next();
|
|
32
|
+
const closed = PsScanMorphologyService.close(edges, width, height, radius);
|
|
33
|
+
const regions = PsScanRegionService.enclosed(closed, width, height, width * height * PsScanConfig.detectMinAreaRatio * 0.5);
|
|
34
|
+
for (const region of regions) {
|
|
35
|
+
const quad = PsScanEdgeService.candidate(region.boundary, width, height);
|
|
36
|
+
if (quad === null)
|
|
37
|
+
continue;
|
|
38
|
+
const area = PsScanGeometryService.area(quad);
|
|
39
|
+
if (area > bestArea) {
|
|
40
|
+
bestArea = area;
|
|
41
|
+
best = quad;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (best === null)
|
|
20
46
|
return null;
|
|
21
|
-
|
|
47
|
+
return PsScanEdgeService.normalise(best, width, height);
|
|
48
|
+
}
|
|
49
|
+
static async locate(source, maxDimension) {
|
|
50
|
+
const quad = await PsScanEdgeService.detect(source, maxDimension);
|
|
51
|
+
return quad === null
|
|
52
|
+
? { isDetected: false, quad: PsScanGeometryService.defaultQuad() }
|
|
53
|
+
: { isDetected: true, quad };
|
|
54
|
+
}
|
|
55
|
+
static candidate(boundary, width, height) {
|
|
56
|
+
const points = boundary.map((point) => ({ x: point.x + 0.5, y: point.y + 0.5 }));
|
|
57
|
+
const hull = PsScanHullService.convexHull(points);
|
|
22
58
|
if (hull.length < 4)
|
|
23
59
|
return null;
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const
|
|
28
|
-
|
|
60
|
+
const corners = PsScanPolygonService.toQuad(hull);
|
|
61
|
+
if (corners === null)
|
|
62
|
+
return null;
|
|
63
|
+
const ordered = PsScanGeometryService.order(corners);
|
|
64
|
+
const margin = PsScanConfig.detectBorderMargin;
|
|
65
|
+
const onBorder = ordered.filter((point) => point.x <= margin ||
|
|
66
|
+
point.y <= margin ||
|
|
67
|
+
point.x >= width - margin ||
|
|
68
|
+
point.y >= height - margin);
|
|
69
|
+
if (onBorder.length >= 2)
|
|
29
70
|
return null;
|
|
30
|
-
|
|
71
|
+
const quad = PsScanGeometryService.expand(ordered, PsScanConfig.detectExpand);
|
|
72
|
+
const imageArea = width * height;
|
|
73
|
+
const area = PsScanGeometryService.area(quad);
|
|
74
|
+
const fill = PsScanGeometryService.area(ordered) / PsScanGeometryService.area(hull);
|
|
75
|
+
const ratio = area / imageArea;
|
|
76
|
+
if (ratio < PsScanConfig.detectMinAreaRatio || ratio > PsScanConfig.detectMaxAreaRatio)
|
|
31
77
|
return null;
|
|
32
|
-
if (
|
|
78
|
+
if (fill < PsScanConfig.detectMinQuadFill)
|
|
33
79
|
return null;
|
|
34
80
|
if (!PsScanGeometryService.isConvex(quad))
|
|
35
81
|
return null;
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
const out = new Uint8Array(mask.length);
|
|
43
|
-
for (let y = 0; y < height; y += 1)
|
|
44
|
-
for (let x = 0; x < width; x += 1) {
|
|
45
|
-
if (mask[y * width + x] === 0)
|
|
46
|
-
continue;
|
|
47
|
-
for (let dy = -1; dy <= 1; dy += 1)
|
|
48
|
-
for (let dx = -1; dx <= 1; dx += 1) {
|
|
49
|
-
const nx = x + dx;
|
|
50
|
-
const ny = y + dy;
|
|
51
|
-
if (nx >= 0 && ny >= 0 && nx < width && ny < height)
|
|
52
|
-
out[ny * width + nx] = 1;
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
return out;
|
|
56
|
-
}
|
|
57
|
-
static foreground(mask, width, height) {
|
|
58
|
-
const background = new Uint8Array(mask.length);
|
|
59
|
-
const stack = [];
|
|
60
|
-
const seed = (index) => {
|
|
61
|
-
if (mask[index] === 0 && background[index] === 0) {
|
|
62
|
-
background[index] = 1;
|
|
63
|
-
stack.push(index);
|
|
64
|
-
}
|
|
65
|
-
};
|
|
66
|
-
for (let x = 0; x < width; x += 1) {
|
|
67
|
-
seed(x);
|
|
68
|
-
seed((height - 1) * width + x);
|
|
69
|
-
}
|
|
70
|
-
for (let y = 0; y < height; y += 1) {
|
|
71
|
-
seed(y * width);
|
|
72
|
-
seed(y * width + width - 1);
|
|
73
|
-
}
|
|
74
|
-
while (stack.length > 0) {
|
|
75
|
-
const index = stack.pop();
|
|
76
|
-
if (index === undefined)
|
|
77
|
-
break;
|
|
78
|
-
const x = index % width;
|
|
79
|
-
const y = (index - x) / width;
|
|
80
|
-
if (x > 0)
|
|
81
|
-
seed(index - 1);
|
|
82
|
-
if (x < width - 1)
|
|
83
|
-
seed(index + 1);
|
|
84
|
-
if (y > 0)
|
|
85
|
-
seed(index - width);
|
|
86
|
-
if (y < height - 1)
|
|
87
|
-
seed(index + width);
|
|
88
|
-
}
|
|
89
|
-
return background.map((value) => 1 - value);
|
|
90
|
-
}
|
|
91
|
-
static largestComponent(mask, width, height) {
|
|
92
|
-
const seen = new Uint8Array(mask.length);
|
|
93
|
-
let best = [];
|
|
94
|
-
let bestSize = 0;
|
|
95
|
-
for (let start = 0; start < mask.length; start += 1) {
|
|
96
|
-
if (mask[start] === 0 || seen[start] === 1)
|
|
97
|
-
continue;
|
|
98
|
-
const members = [];
|
|
99
|
-
const stack = [start];
|
|
100
|
-
let size = 0;
|
|
101
|
-
seen[start] = 1;
|
|
102
|
-
while (stack.length > 0) {
|
|
103
|
-
const index = stack.pop();
|
|
104
|
-
if (index === undefined)
|
|
105
|
-
break;
|
|
106
|
-
const x = index % width;
|
|
107
|
-
const y = (index - x) / width;
|
|
108
|
-
let isBoundary = false;
|
|
109
|
-
size += 1;
|
|
110
|
-
for (let dy = -1; dy <= 1; dy += 1)
|
|
111
|
-
for (let dx = -1; dx <= 1; dx += 1) {
|
|
112
|
-
if (dx === 0 && dy === 0)
|
|
113
|
-
continue;
|
|
114
|
-
const nx = x + dx;
|
|
115
|
-
const ny = y + dy;
|
|
116
|
-
if (nx < 0 || ny < 0 || nx >= width || ny >= height) {
|
|
117
|
-
isBoundary = true;
|
|
118
|
-
continue;
|
|
119
|
-
}
|
|
120
|
-
const next = ny * width + nx;
|
|
121
|
-
if (mask[next] === 0)
|
|
122
|
-
isBoundary = true;
|
|
123
|
-
else if (seen[next] === 0) {
|
|
124
|
-
seen[next] = 1;
|
|
125
|
-
stack.push(next);
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
if (isBoundary)
|
|
129
|
-
members.push({ x, y });
|
|
130
|
-
}
|
|
131
|
-
if (size > bestSize) {
|
|
132
|
-
bestSize = size;
|
|
133
|
-
best = members;
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
return best;
|
|
82
|
+
const shortSide = Math.min(width, height) * PsScanConfig.detectMinSideRatio;
|
|
83
|
+
const isLongEnough = quad.every((point, index) => PsScanGeometryService.distance(point, quad[(index + 1) % quad.length]) >= shortSide);
|
|
84
|
+
if (!isLongEnough)
|
|
85
|
+
return null;
|
|
86
|
+
const isSane = PsScanGeometryService.angles(quad).every((angle) => angle >= PsScanConfig.detectMinAngle && angle <= PsScanConfig.detectMaxAngle);
|
|
87
|
+
return isSane ? quad : null;
|
|
137
88
|
}
|
|
138
|
-
static
|
|
139
|
-
|
|
140
|
-
edges.forEach((value) => {
|
|
141
|
-
bins[Math.min(511, value)] += 1;
|
|
142
|
-
});
|
|
143
|
-
const target = edges.length * PsScanConfig.detectPercentile;
|
|
144
|
-
let cumulative = 0;
|
|
145
|
-
let high = 0;
|
|
146
|
-
for (let bin = 0; bin < bins.length; bin += 1) {
|
|
147
|
-
cumulative += bins[bin];
|
|
148
|
-
if (cumulative >= target) {
|
|
149
|
-
high = bin;
|
|
150
|
-
break;
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
const limit = Math.max(PsScanConfig.detectMinEdgeStrength, high * PsScanConfig.detectRelativeStrength);
|
|
154
|
-
return Uint8Array.from(edges, (value) => (value >= limit ? 1 : 0));
|
|
89
|
+
static normalise(quad, width, height) {
|
|
90
|
+
return quad.map((point) => PsScanGeometryService.clamp({ x: point.x / width, y: point.y / height }));
|
|
155
91
|
}
|
|
156
92
|
}
|
|
@@ -1,16 +1,7 @@
|
|
|
1
1
|
import { PsScanConfig } from '../constants/ps-scan-config';
|
|
2
|
-
import { PsScanOutputFormat } from '../constants/ps-scan-output-format';
|
|
3
2
|
export class PsScanFileNameService {
|
|
4
|
-
static build(base
|
|
5
|
-
|
|
6
|
-
if (format === PsScanOutputFormat.Pdf)
|
|
7
|
-
return `${stem}.pdf`;
|
|
8
|
-
if (count <= 1)
|
|
9
|
-
return `${stem}.jpg`;
|
|
10
|
-
return `${stem}-${String(index + 1).padStart(String(count).length, '0')}.jpg`;
|
|
11
|
-
}
|
|
12
|
-
static mimeType(format) {
|
|
13
|
-
return format === PsScanOutputFormat.Pdf ? 'application/pdf' : 'image/jpeg';
|
|
3
|
+
static build(base) {
|
|
4
|
+
return `${PsScanFileNameService.sanitize(base)}.jpg`;
|
|
14
5
|
}
|
|
15
6
|
static sanitize(base) {
|
|
16
7
|
const cleaned = Array.from(base)
|
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
import { PsScanConfig } from '../constants/ps-scan-config';
|
|
2
2
|
export class PsScanGeometryService {
|
|
3
|
+
static angles(quad) {
|
|
4
|
+
return quad.map((point, index) => {
|
|
5
|
+
const before = quad[(index + quad.length - 1) % quad.length];
|
|
6
|
+
const after = quad[(index + 1) % quad.length];
|
|
7
|
+
const ax = before.x - point.x;
|
|
8
|
+
const ay = before.y - point.y;
|
|
9
|
+
const bx = after.x - point.x;
|
|
10
|
+
const by = after.y - point.y;
|
|
11
|
+
const cosine = (ax * bx + ay * by) / (Math.hypot(ax, ay) * Math.hypot(bx, by));
|
|
12
|
+
return (Math.acos(Math.min(1, Math.max(-1, cosine))) * 180) / Math.PI;
|
|
13
|
+
});
|
|
14
|
+
}
|
|
3
15
|
static area(points) {
|
|
4
16
|
let sum = 0;
|
|
5
17
|
points.forEach((point, index) => {
|
|
@@ -29,6 +41,30 @@ export class PsScanGeometryService {
|
|
|
29
41
|
static distance(from, to) {
|
|
30
42
|
return Math.hypot(to.x - from.x, to.y - from.y);
|
|
31
43
|
}
|
|
44
|
+
static expand(quad, distance) {
|
|
45
|
+
const lines = quad.map((point, index) => {
|
|
46
|
+
const next = quad[(index + 1) % quad.length];
|
|
47
|
+
const length = PsScanGeometryService.distance(point, next);
|
|
48
|
+
const nx = (next.y - point.y) / length;
|
|
49
|
+
const ny = -(next.x - point.x) / length;
|
|
50
|
+
return {
|
|
51
|
+
dx: next.x - point.x,
|
|
52
|
+
dy: next.y - point.y,
|
|
53
|
+
x: point.x + nx * distance,
|
|
54
|
+
y: point.y + ny * distance,
|
|
55
|
+
};
|
|
56
|
+
});
|
|
57
|
+
return quad.map((point, index) => {
|
|
58
|
+
const previous = lines[(index + lines.length - 1) % lines.length];
|
|
59
|
+
const current = lines[index];
|
|
60
|
+
const denominator = previous.dx * current.dy - previous.dy * current.dx;
|
|
61
|
+
if (Math.abs(denominator) < 1e-9)
|
|
62
|
+
return point;
|
|
63
|
+
const t = ((current.x - previous.x) * current.dy - (current.y - previous.y) * current.dx) /
|
|
64
|
+
denominator;
|
|
65
|
+
return { x: previous.x + previous.dx * t, y: previous.y + previous.dy * t };
|
|
66
|
+
});
|
|
67
|
+
}
|
|
32
68
|
static fit(container, content) {
|
|
33
69
|
if (container.width <= 0 || container.height <= 0 || content.width <= 0 || content.height <= 0)
|
|
34
70
|
return { height: 0, width: 0 };
|
|
@@ -1,25 +1,4 @@
|
|
|
1
|
-
import { PsScanConfig } from '../constants/ps-scan-config';
|
|
2
|
-
import { PsScanGeometryService } from './ps-scan-geometry-service';
|
|
3
1
|
export class PsScanHullService {
|
|
4
|
-
static bestQuad(hull) {
|
|
5
|
-
const points = PsScanHullService.reduce(hull, PsScanConfig.hullCandidateLimit);
|
|
6
|
-
if (points.length <= 4)
|
|
7
|
-
return points;
|
|
8
|
-
let best = points.slice(0, 4);
|
|
9
|
-
let bestArea = 0;
|
|
10
|
-
for (let a = 0; a < points.length - 3; a += 1)
|
|
11
|
-
for (let b = a + 1; b < points.length - 2; b += 1)
|
|
12
|
-
for (let c = b + 1; c < points.length - 1; c += 1)
|
|
13
|
-
for (let d = c + 1; d < points.length; d += 1) {
|
|
14
|
-
const candidate = [points[a], points[b], points[c], points[d]];
|
|
15
|
-
const area = PsScanGeometryService.area(candidate);
|
|
16
|
-
if (area > bestArea) {
|
|
17
|
-
bestArea = area;
|
|
18
|
-
best = candidate;
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
return best;
|
|
22
|
-
}
|
|
23
2
|
static convexHull(input) {
|
|
24
3
|
const points = [...input].sort((a, b) => (a.x === b.x ? a.y - b.y : a.x - b.x));
|
|
25
4
|
if (points.length < 3)
|
|
@@ -38,22 +17,4 @@ export class PsScanHullService {
|
|
|
38
17
|
};
|
|
39
18
|
return [...build(points), ...build([...points].reverse())];
|
|
40
19
|
}
|
|
41
|
-
static reduce(hull, limit) {
|
|
42
|
-
const points = [...hull];
|
|
43
|
-
while (points.length > limit) {
|
|
44
|
-
let weakest = 0;
|
|
45
|
-
let weakestArea = Infinity;
|
|
46
|
-
points.forEach((point, index) => {
|
|
47
|
-
const previous = points[(index - 1 + points.length) % points.length];
|
|
48
|
-
const next = points[(index + 1) % points.length];
|
|
49
|
-
const area = PsScanGeometryService.area([previous, point, next]);
|
|
50
|
-
if (area < weakestArea) {
|
|
51
|
-
weakestArea = area;
|
|
52
|
-
weakest = index;
|
|
53
|
-
}
|
|
54
|
-
});
|
|
55
|
-
points.splice(weakest, 1);
|
|
56
|
-
}
|
|
57
|
-
return points;
|
|
58
|
-
}
|
|
59
20
|
}
|
|
@@ -28,25 +28,4 @@ export class PsScanImageFilterService {
|
|
|
28
28
|
}
|
|
29
29
|
return out;
|
|
30
30
|
}
|
|
31
|
-
static sobel(gray, width, height) {
|
|
32
|
-
const out = new Uint16Array(width * height);
|
|
33
|
-
for (let y = 1; y < height - 1; y += 1)
|
|
34
|
-
for (let x = 1; x < width - 1; x += 1) {
|
|
35
|
-
const i = y * width + x;
|
|
36
|
-
const gx = -gray[i - width - 1] +
|
|
37
|
-
gray[i - width + 1] -
|
|
38
|
-
2 * gray[i - 1] +
|
|
39
|
-
2 * gray[i + 1] -
|
|
40
|
-
gray[i + width - 1] +
|
|
41
|
-
gray[i + width + 1];
|
|
42
|
-
const gy = -gray[i - width - 1] -
|
|
43
|
-
2 * gray[i - width] -
|
|
44
|
-
gray[i - width + 1] +
|
|
45
|
-
gray[i + width - 1] +
|
|
46
|
-
2 * gray[i + width] +
|
|
47
|
-
gray[i + width + 1];
|
|
48
|
-
out[i] = Math.hypot(gx, gy) / 4;
|
|
49
|
-
}
|
|
50
|
-
return out;
|
|
51
|
-
}
|
|
52
31
|
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export class PsScanMorphologyService {
|
|
2
|
+
static close(mask, width, height, radius) {
|
|
3
|
+
return PsScanMorphologyService.erode(PsScanMorphologyService.dilate(mask, width, height, radius), width, height, radius);
|
|
4
|
+
}
|
|
5
|
+
static dilate(mask, width, height, radius) {
|
|
6
|
+
return PsScanMorphologyService.vertical(PsScanMorphologyService.horizontal(mask, width, height, radius, 1), width, height, radius, 1);
|
|
7
|
+
}
|
|
8
|
+
static erode(mask, width, height, radius) {
|
|
9
|
+
return PsScanMorphologyService.vertical(PsScanMorphologyService.horizontal(mask, width, height, radius, 0), width, height, radius, 0);
|
|
10
|
+
}
|
|
11
|
+
static horizontal(mask, width, height, radius, target) {
|
|
12
|
+
const out = new Uint8Array(mask.length);
|
|
13
|
+
for (let y = 0; y < height; y += 1)
|
|
14
|
+
for (let x = 0; x < width; x += 1) {
|
|
15
|
+
const from = Math.max(0, x - radius);
|
|
16
|
+
const to = Math.min(width - 1, x + radius);
|
|
17
|
+
let hit = target === 1 ? 0 : 1;
|
|
18
|
+
for (let at = from; at <= to; at += 1)
|
|
19
|
+
if (mask[y * width + at] === target) {
|
|
20
|
+
hit = target;
|
|
21
|
+
break;
|
|
22
|
+
}
|
|
23
|
+
out[y * width + x] = hit;
|
|
24
|
+
}
|
|
25
|
+
return out;
|
|
26
|
+
}
|
|
27
|
+
static vertical(mask, width, height, radius, target) {
|
|
28
|
+
const out = new Uint8Array(mask.length);
|
|
29
|
+
for (let y = 0; y < height; y += 1) {
|
|
30
|
+
const from = Math.max(0, y - radius);
|
|
31
|
+
const to = Math.min(height - 1, y + radius);
|
|
32
|
+
for (let x = 0; x < width; x += 1) {
|
|
33
|
+
let hit = target === 1 ? 0 : 1;
|
|
34
|
+
for (let at = from; at <= to; at += 1)
|
|
35
|
+
if (mask[at * width + x] === target) {
|
|
36
|
+
hit = target;
|
|
37
|
+
break;
|
|
38
|
+
}
|
|
39
|
+
out[y * width + x] = hit;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return out;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -1,36 +1,26 @@
|
|
|
1
1
|
import { PsScanConfig } from '../constants/ps-scan-config';
|
|
2
2
|
import { PsScanCanvasService } from './ps-scan-canvas-service';
|
|
3
3
|
import { PsScanEdgeService } from './ps-scan-edge-service';
|
|
4
|
-
import { PsScanEnhanceService } from './ps-scan-enhance-service';
|
|
5
4
|
import { PsScanGeometryService } from './ps-scan-geometry-service';
|
|
6
|
-
import { PsScanRasterService } from './ps-scan-raster-service';
|
|
7
5
|
import { PsScanWarpService } from './ps-scan-warp-service';
|
|
8
6
|
export class PsScanPipelineService {
|
|
9
7
|
static async prepare(source) {
|
|
10
8
|
const decoded = await PsScanCanvasService.decode(source, PsScanConfig.detectMaxDimension * 2);
|
|
11
|
-
const
|
|
9
|
+
const located = await PsScanEdgeService.locate(decoded.raster, PsScanConfig.detectMaxDimension);
|
|
12
10
|
return {
|
|
13
|
-
corners:
|
|
11
|
+
corners: located.quad,
|
|
14
12
|
height: decoded.naturalHeight,
|
|
15
|
-
isDetected:
|
|
13
|
+
isDetected: located.isDetected,
|
|
16
14
|
width: decoded.naturalWidth,
|
|
17
15
|
};
|
|
18
16
|
}
|
|
19
|
-
static async
|
|
20
|
-
const decoded = await PsScanCanvasService.decode(source,
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const warped = PsScanWarpService.warp(raster, PsScanGeometryService.toPixels(page.corners, {
|
|
26
|
-
height: raster.height - 1,
|
|
27
|
-
width: raster.width - 1,
|
|
17
|
+
static async render(source, corners, maxDimension, quality) {
|
|
18
|
+
const decoded = await PsScanCanvasService.decode(source, Number.MAX_SAFE_INTEGER);
|
|
19
|
+
const size = PsScanGeometryService.fitWithin(PsScanGeometryService.outputSize(corners, decoded.raster), maxDimension);
|
|
20
|
+
const raster = await PsScanWarpService.warp(decoded.raster, PsScanGeometryService.toPixels(corners, {
|
|
21
|
+
height: decoded.raster.height - 1,
|
|
22
|
+
width: decoded.raster.width - 1,
|
|
28
23
|
}), size.width, size.height);
|
|
29
|
-
return PsScanRasterService.rotate(PsScanEnhanceService.apply(warped, page.filter), page.rotation);
|
|
30
|
-
}
|
|
31
|
-
static async render(page, maxDimension, quality) {
|
|
32
|
-
const decoded = await PsScanCanvasService.decode(page.source, PsScanConfig.decodeMaxDimension);
|
|
33
|
-
const raster = PsScanPipelineService.process(page, decoded.raster, maxDimension);
|
|
34
24
|
return {
|
|
35
25
|
blob: await PsScanCanvasService.encode(raster, quality),
|
|
36
26
|
height: raster.height,
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { PsScanConfig } from '../constants/ps-scan-config';
|
|
2
|
+
import { PsScanGeometryService } from './ps-scan-geometry-service';
|
|
3
|
+
export class PsScanPolygonService {
|
|
4
|
+
static perimeter(points) {
|
|
5
|
+
return points.reduce((sum, point, index) => sum + PsScanGeometryService.distance(point, points[(index + 1) % points.length]), 0);
|
|
6
|
+
}
|
|
7
|
+
static simplify(points, epsilon) {
|
|
8
|
+
if (points.length < 4)
|
|
9
|
+
return points;
|
|
10
|
+
let first = 0;
|
|
11
|
+
let second = 0;
|
|
12
|
+
let best = -1;
|
|
13
|
+
points.forEach((point, index) => {
|
|
14
|
+
const length = PsScanGeometryService.distance(points[0], point);
|
|
15
|
+
if (length > best) {
|
|
16
|
+
best = length;
|
|
17
|
+
second = index;
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
best = -1;
|
|
21
|
+
points.forEach((point, index) => {
|
|
22
|
+
const length = PsScanGeometryService.distance(points[second], point);
|
|
23
|
+
if (length > best) {
|
|
24
|
+
best = length;
|
|
25
|
+
first = index;
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
const low = Math.min(first, second);
|
|
29
|
+
const high = Math.max(first, second);
|
|
30
|
+
const forward = points.slice(low, high + 1);
|
|
31
|
+
const backward = [...points.slice(high), ...points.slice(0, low + 1)];
|
|
32
|
+
const left = PsScanPolygonService.chain(forward, epsilon);
|
|
33
|
+
const right = PsScanPolygonService.chain(backward, epsilon);
|
|
34
|
+
return [...left.slice(0, -1), ...right.slice(0, -1)];
|
|
35
|
+
}
|
|
36
|
+
static toQuad(hull) {
|
|
37
|
+
const perimeter = PsScanPolygonService.perimeter(hull);
|
|
38
|
+
for (const ratio of PsScanConfig.polygonEpsilons) {
|
|
39
|
+
const simple = PsScanPolygonService.simplify(hull, ratio * perimeter);
|
|
40
|
+
if (simple.length === 4)
|
|
41
|
+
return simple;
|
|
42
|
+
if (simple.length < 4)
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
static chain(points, epsilon) {
|
|
48
|
+
if (points.length < 3)
|
|
49
|
+
return points;
|
|
50
|
+
const start = points[0];
|
|
51
|
+
const end = points[points.length - 1];
|
|
52
|
+
let far = 0;
|
|
53
|
+
let farDistance = -1;
|
|
54
|
+
for (let index = 1; index < points.length - 1; index += 1) {
|
|
55
|
+
const length = PsScanPolygonService.lineDistance(points[index], start, end);
|
|
56
|
+
if (length > farDistance) {
|
|
57
|
+
farDistance = length;
|
|
58
|
+
far = index;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (farDistance <= epsilon)
|
|
62
|
+
return [start, end];
|
|
63
|
+
const left = PsScanPolygonService.chain(points.slice(0, far + 1), epsilon);
|
|
64
|
+
const right = PsScanPolygonService.chain(points.slice(far), epsilon);
|
|
65
|
+
return [...left.slice(0, -1), ...right];
|
|
66
|
+
}
|
|
67
|
+
static lineDistance(point, from, to) {
|
|
68
|
+
const dx = to.x - from.x;
|
|
69
|
+
const dy = to.y - from.y;
|
|
70
|
+
const length = Math.hypot(dx, dy);
|
|
71
|
+
if (length === 0)
|
|
72
|
+
return PsScanGeometryService.distance(point, from);
|
|
73
|
+
return Math.abs(dy * (point.x - from.x) - dx * (point.y - from.y)) / length;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
export class PsScanRegionService {
|
|
2
|
+
static enclosed(mask, width, height, minArea) {
|
|
3
|
+
const seen = new Uint8Array(mask.length);
|
|
4
|
+
const regions = [];
|
|
5
|
+
for (let start = 0; start < mask.length; start += 1) {
|
|
6
|
+
if (mask[start] === 1 || seen[start] === 1)
|
|
7
|
+
continue;
|
|
8
|
+
const boundary = [];
|
|
9
|
+
const stack = [start];
|
|
10
|
+
let area = 0;
|
|
11
|
+
seen[start] = 1;
|
|
12
|
+
while (stack.length > 0) {
|
|
13
|
+
const index = stack.pop();
|
|
14
|
+
if (index === undefined)
|
|
15
|
+
break;
|
|
16
|
+
const x = index % width;
|
|
17
|
+
const y = (index - x) / width;
|
|
18
|
+
let isBoundary = false;
|
|
19
|
+
area += 1;
|
|
20
|
+
const visit = (next) => {
|
|
21
|
+
if (mask[next] === 1)
|
|
22
|
+
isBoundary = true;
|
|
23
|
+
else if (seen[next] === 0) {
|
|
24
|
+
seen[next] = 1;
|
|
25
|
+
stack.push(next);
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
if (x > 0)
|
|
29
|
+
visit(index - 1);
|
|
30
|
+
else
|
|
31
|
+
isBoundary = true;
|
|
32
|
+
if (x < width - 1)
|
|
33
|
+
visit(index + 1);
|
|
34
|
+
else
|
|
35
|
+
isBoundary = true;
|
|
36
|
+
if (y > 0)
|
|
37
|
+
visit(index - width);
|
|
38
|
+
else
|
|
39
|
+
isBoundary = true;
|
|
40
|
+
if (y < height - 1)
|
|
41
|
+
visit(index + width);
|
|
42
|
+
else
|
|
43
|
+
isBoundary = true;
|
|
44
|
+
if (isBoundary)
|
|
45
|
+
boundary.push({ x, y });
|
|
46
|
+
}
|
|
47
|
+
if (area >= minArea)
|
|
48
|
+
regions.push({ area, boundary });
|
|
49
|
+
}
|
|
50
|
+
return regions;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -1,41 +1,28 @@
|
|
|
1
1
|
import { PsScanConfig } from '../constants/ps-scan-config';
|
|
2
2
|
import { PsScanGeometryService } from './ps-scan-geometry-service';
|
|
3
3
|
export class PsScanStabilityService {
|
|
4
|
-
static disarm(state) {
|
|
5
|
-
return { ...state, isArmed: false, stableMs: 0 };
|
|
6
|
-
}
|
|
7
4
|
static initial() {
|
|
8
|
-
return {
|
|
5
|
+
return { misses: 0, quad: null };
|
|
9
6
|
}
|
|
10
7
|
static movement(from, to) {
|
|
11
8
|
return Math.max(...from.map((point, index) => PsScanGeometryService.distance(point, to[index])));
|
|
12
9
|
}
|
|
13
|
-
static next(state, detected
|
|
10
|
+
static next(state, detected) {
|
|
14
11
|
if (detected === null) {
|
|
15
12
|
const misses = state.misses + 1;
|
|
16
13
|
return misses > PsScanConfig.liveMissLimit
|
|
17
|
-
? {
|
|
18
|
-
: {
|
|
14
|
+
? { misses, quad: null }
|
|
15
|
+
: { misses, quad: state.quad };
|
|
19
16
|
}
|
|
20
|
-
if (state.quad === null
|
|
21
|
-
return {
|
|
22
|
-
|
|
17
|
+
if (state.quad === null)
|
|
18
|
+
return { misses: 0, quad: detected };
|
|
19
|
+
if (PsScanStabilityService.movement(state.quad, detected) < PsScanConfig.liveDeadzone)
|
|
20
|
+
return { misses: 0, quad: state.quad };
|
|
23
21
|
const weight = PsScanConfig.liveSmoothing;
|
|
24
22
|
const quad = state.quad.map((point, index) => ({
|
|
25
23
|
x: point.x + (detected[index].x - point.x) * weight,
|
|
26
24
|
y: point.y + (detected[index].y - point.y) * weight,
|
|
27
25
|
}));
|
|
28
|
-
return {
|
|
29
|
-
isArmed: state.isArmed || movement > PsScanConfig.liveRearmMovement,
|
|
30
|
-
misses: 0,
|
|
31
|
-
quad,
|
|
32
|
-
raw: detected,
|
|
33
|
-
stableMs: movement <= PsScanConfig.liveStillMovement ? state.stableMs + elapsedMs : 0,
|
|
34
|
-
};
|
|
35
|
-
}
|
|
36
|
-
static progress(state) {
|
|
37
|
-
if (!state.isArmed || state.quad === null)
|
|
38
|
-
return 0;
|
|
39
|
-
return Math.min(1, state.stableMs / PsScanConfig.autoCaptureMs);
|
|
26
|
+
return { misses: 0, quad };
|
|
40
27
|
}
|
|
41
28
|
}
|