@pallasoft/ps-file-components 1.3.0 → 1.5.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/dist/components/ps-document-scanner-loader.d.ts +1 -0
- package/dist/components/ps-document-scanner-loader.js +40 -0
- package/dist/components/ps-document-scanner-sheet.d.ts +2 -0
- package/dist/components/ps-document-scanner-sheet.js +186 -0
- package/dist/components/ps-document-scanner.d.ts +2 -0
- package/dist/components/ps-document-scanner.js +17 -0
- package/dist/components/ps-file-picker-actions.d.ts +10 -0
- package/dist/components/ps-file-picker-actions.js +21 -0
- package/dist/components/ps-file-picker.d.ts +7 -1
- package/dist/components/ps-file-picker.js +30 -6
- package/dist/components/ps-file-viewer-pane.d.ts +2 -0
- package/dist/components/ps-file-viewer-pane.js +39 -0
- package/dist/components/ps-scan-button.d.ts +13 -0
- package/dist/components/ps-scan-button.js +9 -0
- package/dist/components/ps-scan-camera-view.d.ts +27 -0
- package/dist/components/ps-scan-camera-view.js +34 -0
- package/dist/components/ps-scan-crop-editor.d.ts +13 -0
- package/dist/components/ps-scan-crop-editor.js +69 -0
- package/dist/components/ps-scan-denied-steps.d.ts +6 -0
- package/dist/components/ps-scan-denied-steps.js +33 -0
- package/dist/components/ps-scan-icon-button.d.ts +11 -0
- package/dist/components/ps-scan-icon-button.js +9 -0
- package/dist/components/ps-scan-illustration.d.ts +1 -0
- package/dist/components/ps-scan-illustration.js +6 -0
- package/dist/components/ps-scan-loupe.d.ts +10 -0
- package/dist/components/ps-scan-loupe.js +31 -0
- package/dist/components/ps-scan-page-list.d.ts +14 -0
- package/dist/components/ps-scan-page-list.js +22 -0
- package/dist/components/ps-scan-page-stack.d.ts +7 -0
- package/dist/components/ps-scan-page-stack.js +6 -0
- package/dist/components/ps-scan-permission-view.d.ts +11 -0
- package/dist/components/ps-scan-permission-view.js +48 -0
- package/dist/components/ps-scan-photo-button.d.ts +8 -0
- package/dist/components/ps-scan-photo-button.js +20 -0
- package/dist/components/ps-scan-review.d.ts +24 -0
- package/dist/components/ps-scan-review.js +37 -0
- package/dist/components/ps-scan-shutter.d.ts +7 -0
- package/dist/components/ps-scan-shutter.js +6 -0
- package/dist/components/ps-scan-viewfinder-overlay.d.ts +8 -0
- package/dist/components/ps-scan-viewfinder-overlay.js +28 -0
- package/dist/components/ps-upload-row.js +1 -1
- package/dist/constants/ps-document-scanner-labels.d.ts +80 -0
- package/dist/constants/ps-document-scanner-labels.js +83 -0
- package/dist/constants/ps-file-picker-labels.d.ts +5 -3
- package/dist/constants/ps-file-picker-labels.js +5 -3
- package/dist/constants/ps-scan-camera-status.d.ts +11 -0
- package/dist/constants/ps-scan-camera-status.js +13 -0
- package/dist/constants/ps-scan-chrome-classes.d.ts +12 -0
- package/dist/constants/ps-scan-chrome-classes.js +15 -0
- package/dist/constants/ps-scan-config.d.ts +48 -0
- package/dist/constants/ps-scan-config.js +51 -0
- package/dist/constants/ps-scan-corner-order.d.ts +1 -0
- package/dist/constants/ps-scan-corner-order.js +4 -0
- package/dist/constants/ps-scan-filter.d.ts +7 -0
- package/dist/constants/ps-scan-filter.js +9 -0
- package/dist/constants/ps-scan-output-format.d.ts +5 -0
- package/dist/constants/ps-scan-output-format.js +7 -0
- package/dist/constants/ps-scan-platform.d.ts +6 -0
- package/dist/constants/ps-scan-platform.js +8 -0
- package/dist/constants/ps-scan-step.d.ts +6 -0
- package/dist/constants/ps-scan-step.js +8 -0
- package/dist/esm/components/ps-document-scanner-loader.js +4 -0
- package/dist/esm/components/ps-document-scanner-sheet.js +182 -0
- package/dist/esm/components/ps-document-scanner.js +13 -0
- package/dist/esm/components/ps-file-picker-actions.js +17 -0
- package/dist/esm/components/ps-file-picker.js +31 -7
- package/dist/esm/components/ps-file-viewer-pane.js +35 -0
- package/dist/esm/components/ps-scan-button.js +5 -0
- package/dist/esm/components/ps-scan-camera-view.js +30 -0
- package/dist/esm/components/ps-scan-crop-editor.js +65 -0
- package/dist/esm/components/ps-scan-denied-steps.js +29 -0
- package/dist/esm/components/ps-scan-icon-button.js +5 -0
- package/dist/esm/components/ps-scan-illustration.js +2 -0
- package/dist/esm/components/ps-scan-loupe.js +27 -0
- package/dist/esm/components/ps-scan-page-list.js +18 -0
- package/dist/esm/components/ps-scan-page-stack.js +2 -0
- package/dist/esm/components/ps-scan-permission-view.js +44 -0
- package/dist/esm/components/ps-scan-photo-button.js +16 -0
- package/dist/esm/components/ps-scan-review.js +33 -0
- package/dist/esm/components/ps-scan-shutter.js +2 -0
- package/dist/esm/components/ps-scan-viewfinder-overlay.js +24 -0
- package/dist/esm/components/ps-upload-row.js +1 -1
- package/dist/esm/constants/ps-document-scanner-labels.js +80 -0
- package/dist/esm/constants/ps-file-picker-labels.js +5 -3
- package/dist/esm/constants/ps-scan-camera-status.js +10 -0
- package/dist/esm/constants/ps-scan-chrome-classes.js +12 -0
- package/dist/esm/constants/ps-scan-config.js +48 -0
- package/dist/esm/constants/ps-scan-corner-order.js +1 -0
- package/dist/esm/constants/ps-scan-filter.js +6 -0
- package/dist/esm/constants/ps-scan-output-format.js +4 -0
- package/dist/esm/constants/ps-scan-platform.js +5 -0
- package/dist/esm/constants/ps-scan-step.js +5 -0
- package/dist/esm/hooks/use-ps-focus-trap.js +45 -0
- package/dist/esm/hooks/use-ps-scan-camera.js +99 -0
- package/dist/esm/hooks/use-ps-scan-live-detection.js +45 -0
- package/dist/esm/hooks/use-ps-scan-pages.js +62 -0
- package/dist/esm/hooks/use-ps-scan-previews.js +57 -0
- package/dist/esm/index.js +8 -0
- package/dist/esm/models/ps-document-scanner-props.js +1 -0
- package/dist/esm/models/ps-document-scanner-sheet-props.js +1 -0
- package/dist/esm/models/ps-file-picker-scan.js +1 -0
- package/dist/esm/models/ps-file-viewer-pane-props.js +1 -0
- package/dist/esm/models/ps-scan-added-page.js +1 -0
- package/dist/esm/models/ps-scan-camera-session.js +1 -0
- package/dist/esm/models/ps-scan-decoded-image.js +1 -0
- package/dist/esm/models/ps-scan-levels.js +1 -0
- package/dist/esm/models/ps-scan-live-detection.js +1 -0
- package/dist/esm/models/ps-scan-output-options.js +1 -0
- package/dist/esm/models/ps-scan-page.js +1 -0
- package/dist/esm/models/ps-scan-pdf-page.js +1 -0
- package/dist/esm/models/ps-scan-prepared.js +1 -0
- package/dist/esm/models/ps-scan-quad.js +1 -0
- package/dist/esm/models/ps-scan-raster.js +1 -0
- package/dist/esm/models/ps-scan-rendered-page.js +1 -0
- package/dist/esm/models/ps-scan-stability.js +1 -0
- package/dist/esm/services/ps-scan-camera-service.js +95 -0
- package/dist/esm/services/ps-scan-canvas-service.js +52 -0
- package/dist/esm/services/ps-scan-edge-service.js +156 -0
- package/dist/esm/services/ps-scan-enhance-service.js +97 -0
- package/dist/esm/services/ps-scan-file-name-service.js +25 -0
- package/dist/esm/services/ps-scan-geometry-service.js +116 -0
- package/dist/esm/services/ps-scan-homography-service.js +46 -0
- package/dist/esm/services/ps-scan-hull-service.js +59 -0
- package/dist/esm/services/ps-scan-image-filter-service.js +52 -0
- package/dist/esm/services/ps-scan-order-service.js +13 -0
- package/dist/esm/services/ps-scan-output-service.js +24 -0
- package/dist/esm/services/ps-scan-pdf-service.js +56 -0
- package/dist/esm/services/ps-scan-pipeline-service.js +40 -0
- package/dist/esm/services/ps-scan-platform-service.js +15 -0
- package/dist/esm/services/ps-scan-raster-service.js +67 -0
- package/dist/esm/services/ps-scan-stability-service.js +41 -0
- package/dist/esm/services/ps-scan-template-service.js +5 -0
- package/dist/esm/services/ps-scan-warp-service.js +35 -0
- package/dist/hooks/use-ps-focus-trap.d.ts +2 -0
- package/dist/hooks/use-ps-focus-trap.js +49 -0
- package/dist/hooks/use-ps-scan-camera.d.ts +11 -0
- package/dist/hooks/use-ps-scan-camera.js +103 -0
- package/dist/hooks/use-ps-scan-live-detection.d.ts +3 -0
- package/dist/hooks/use-ps-scan-live-detection.js +49 -0
- package/dist/hooks/use-ps-scan-pages.d.ts +12 -0
- package/dist/hooks/use-ps-scan-pages.js +66 -0
- package/dist/hooks/use-ps-scan-previews.d.ts +2 -0
- package/dist/hooks/use-ps-scan-previews.js +61 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +8 -0
- package/dist/models/ps-document-scanner-props.d.ts +16 -0
- package/dist/models/ps-document-scanner-props.js +2 -0
- package/dist/models/ps-document-scanner-sheet-props.d.ts +14 -0
- package/dist/models/ps-document-scanner-sheet-props.js +2 -0
- package/dist/models/ps-file-picker-scan.d.ts +4 -0
- package/dist/models/ps-file-picker-scan.js +2 -0
- package/dist/models/ps-file-viewer-pane-props.d.ts +12 -0
- package/dist/models/ps-file-viewer-pane-props.js +2 -0
- package/dist/models/ps-scan-added-page.d.ts +5 -0
- package/dist/models/ps-scan-added-page.js +2 -0
- package/dist/models/ps-scan-camera-session.d.ts +5 -0
- package/dist/models/ps-scan-camera-session.js +2 -0
- package/dist/models/ps-scan-decoded-image.d.ts +6 -0
- package/dist/models/ps-scan-decoded-image.js +2 -0
- package/dist/models/ps-scan-levels.d.ts +4 -0
- package/dist/models/ps-scan-levels.js +2 -0
- package/dist/models/ps-scan-live-detection.d.ts +5 -0
- package/dist/models/ps-scan-live-detection.js +2 -0
- package/dist/models/ps-scan-output-options.d.ts +7 -0
- package/dist/models/ps-scan-output-options.js +2 -0
- package/dist/models/ps-scan-page.d.ts +12 -0
- package/dist/models/ps-scan-page.js +2 -0
- package/dist/models/ps-scan-pdf-page.d.ts +5 -0
- package/dist/models/ps-scan-pdf-page.js +2 -0
- package/dist/models/ps-scan-prepared.d.ts +7 -0
- package/dist/models/ps-scan-prepared.js +2 -0
- package/dist/models/ps-scan-quad.d.ts +2 -0
- package/dist/models/ps-scan-quad.js +2 -0
- package/dist/models/ps-scan-raster.d.ts +5 -0
- package/dist/models/ps-scan-raster.js +2 -0
- package/dist/models/ps-scan-rendered-page.d.ts +5 -0
- package/dist/models/ps-scan-rendered-page.js +2 -0
- package/dist/models/ps-scan-stability.d.ts +8 -0
- package/dist/models/ps-scan-stability.js +2 -0
- package/dist/services/ps-scan-camera-service.d.ts +14 -0
- package/dist/services/ps-scan-camera-service.js +99 -0
- package/dist/services/ps-scan-canvas-service.d.ts +9 -0
- package/dist/services/ps-scan-canvas-service.js +56 -0
- package/dist/services/ps-scan-edge-service.d.ts +9 -0
- package/dist/services/ps-scan-edge-service.js +160 -0
- package/dist/services/ps-scan-enhance-service.d.ts +9 -0
- package/dist/services/ps-scan-enhance-service.js +101 -0
- package/dist/services/ps-scan-file-name-service.d.ts +6 -0
- package/dist/services/ps-scan-file-name-service.js +29 -0
- package/dist/services/ps-scan-geometry-service.d.ts +20 -0
- package/dist/services/ps-scan-geometry-service.js +120 -0
- package/dist/services/ps-scan-homography-service.d.ts +7 -0
- package/dist/services/ps-scan-homography-service.js +50 -0
- package/dist/services/ps-scan-hull-service.d.ts +6 -0
- package/dist/services/ps-scan-hull-service.js +63 -0
- package/dist/services/ps-scan-image-filter-service.d.ts +6 -0
- package/dist/services/ps-scan-image-filter-service.js +56 -0
- package/dist/services/ps-scan-order-service.d.ts +4 -0
- package/dist/services/ps-scan-order-service.js +17 -0
- package/dist/services/ps-scan-output-service.d.ts +5 -0
- package/dist/services/ps-scan-output-service.js +28 -0
- package/dist/services/ps-scan-pdf-service.d.ts +5 -0
- package/dist/services/ps-scan-pdf-service.js +60 -0
- package/dist/services/ps-scan-pipeline-service.d.ts +10 -0
- package/dist/services/ps-scan-pipeline-service.js +44 -0
- package/dist/services/ps-scan-platform-service.d.ts +4 -0
- package/dist/services/ps-scan-platform-service.js +19 -0
- package/dist/services/ps-scan-raster-service.d.ts +6 -0
- package/dist/services/ps-scan-raster-service.js +71 -0
- package/dist/services/ps-scan-stability-service.d.ts +9 -0
- package/dist/services/ps-scan-stability-service.js +45 -0
- package/dist/services/ps-scan-template-service.d.ts +3 -0
- package/dist/services/ps-scan-template-service.js +9 -0
- package/dist/services/ps-scan-warp-service.d.ts +6 -0
- package/dist/services/ps-scan-warp-service.js +39 -0
- package/package.json +9 -6
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { PsScanConfig } from '../constants/ps-scan-config';
|
|
2
|
+
import { PsScanFilter } from '../constants/ps-scan-filter';
|
|
3
|
+
import { PsScanImageFilterService } from './ps-scan-image-filter-service';
|
|
4
|
+
import { PsScanRasterService } from './ps-scan-raster-service';
|
|
5
|
+
export class PsScanEnhanceService {
|
|
6
|
+
static apply(source, filter) {
|
|
7
|
+
if (filter === PsScanFilter.Original)
|
|
8
|
+
return source;
|
|
9
|
+
if (filter === PsScanFilter.Colour)
|
|
10
|
+
return PsScanEnhanceService.colour(source);
|
|
11
|
+
if (filter === PsScanFilter.Greyscale)
|
|
12
|
+
return PsScanEnhanceService.greyscale(source);
|
|
13
|
+
return PsScanEnhanceService.document(source);
|
|
14
|
+
}
|
|
15
|
+
static colour(source) {
|
|
16
|
+
const gray = PsScanImageFilterService.grayscale(source);
|
|
17
|
+
const { black, white } = PsScanEnhanceService.levels(gray);
|
|
18
|
+
const gain = 255 / Math.max(1, white - black);
|
|
19
|
+
const target = PsScanRasterService.create(source.width, source.height);
|
|
20
|
+
for (let at = 0; at < source.data.length; at += 4) {
|
|
21
|
+
target.data[at] = (source.data[at] - black) * gain;
|
|
22
|
+
target.data[at + 1] = (source.data[at + 1] - black) * gain;
|
|
23
|
+
target.data[at + 2] = (source.data[at + 2] - black) * gain;
|
|
24
|
+
target.data[at + 3] = 255;
|
|
25
|
+
}
|
|
26
|
+
return target;
|
|
27
|
+
}
|
|
28
|
+
static document(source) {
|
|
29
|
+
const { height, width } = source;
|
|
30
|
+
const gray = PsScanImageFilterService.grayscale(source);
|
|
31
|
+
const integral = new Float64Array((width + 1) * (height + 1));
|
|
32
|
+
for (let y = 0; y < height; y += 1) {
|
|
33
|
+
let row = 0;
|
|
34
|
+
for (let x = 0; x < width; x += 1) {
|
|
35
|
+
row += gray[y * width + x];
|
|
36
|
+
integral[(y + 1) * (width + 1) + x + 1] = integral[y * (width + 1) + x + 1] + row;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
const half = Math.max(PsScanConfig.adaptiveWindowMin, Math.floor(Math.min(width, height) / PsScanConfig.adaptiveWindowDivisor)) >> 1;
|
|
40
|
+
const target = PsScanRasterService.create(width, height);
|
|
41
|
+
for (let y = 0; y < height; y += 1) {
|
|
42
|
+
const y0 = Math.max(0, y - half);
|
|
43
|
+
const y1 = Math.min(height, y + half + 1);
|
|
44
|
+
for (let x = 0; x < width; x += 1) {
|
|
45
|
+
const x0 = Math.max(0, x - half);
|
|
46
|
+
const x1 = Math.min(width, x + half + 1);
|
|
47
|
+
const sum = integral[y1 * (width + 1) + x1] -
|
|
48
|
+
integral[y0 * (width + 1) + x1] -
|
|
49
|
+
integral[y1 * (width + 1) + x0] +
|
|
50
|
+
integral[y0 * (width + 1) + x0];
|
|
51
|
+
const mean = sum / ((x1 - x0) * (y1 - y0));
|
|
52
|
+
const value = gray[y * width + x] < mean * PsScanConfig.adaptiveRatio ? 0 : 255;
|
|
53
|
+
const at = (y * width + x) * 4;
|
|
54
|
+
target.data[at] = value;
|
|
55
|
+
target.data[at + 1] = value;
|
|
56
|
+
target.data[at + 2] = value;
|
|
57
|
+
target.data[at + 3] = 255;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return target;
|
|
61
|
+
}
|
|
62
|
+
static greyscale(source) {
|
|
63
|
+
const gray = PsScanImageFilterService.grayscale(source);
|
|
64
|
+
const { black, white } = PsScanEnhanceService.levels(gray);
|
|
65
|
+
const gain = 255 / Math.max(1, white - black);
|
|
66
|
+
const target = PsScanRasterService.create(source.width, source.height);
|
|
67
|
+
for (let index = 0; index < gray.length; index += 1) {
|
|
68
|
+
const value = (gray[index] - black) * gain;
|
|
69
|
+
const at = index * 4;
|
|
70
|
+
target.data[at] = value;
|
|
71
|
+
target.data[at + 1] = value;
|
|
72
|
+
target.data[at + 2] = value;
|
|
73
|
+
target.data[at + 3] = 255;
|
|
74
|
+
}
|
|
75
|
+
return target;
|
|
76
|
+
}
|
|
77
|
+
static levels(gray) {
|
|
78
|
+
const bins = new Uint32Array(256);
|
|
79
|
+
gray.forEach((value) => {
|
|
80
|
+
bins[value] += 1;
|
|
81
|
+
});
|
|
82
|
+
const find = (fraction) => {
|
|
83
|
+
const target = gray.length * fraction;
|
|
84
|
+
let cumulative = 0;
|
|
85
|
+
for (let bin = 0; bin < 256; bin += 1) {
|
|
86
|
+
cumulative += bins[bin];
|
|
87
|
+
if (cumulative >= target)
|
|
88
|
+
return bin;
|
|
89
|
+
}
|
|
90
|
+
return 255;
|
|
91
|
+
};
|
|
92
|
+
return {
|
|
93
|
+
black: find(PsScanConfig.levelLowPercentile),
|
|
94
|
+
white: find(PsScanConfig.levelHighPercentile),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { PsScanConfig } from '../constants/ps-scan-config';
|
|
2
|
+
import { PsScanOutputFormat } from '../constants/ps-scan-output-format';
|
|
3
|
+
export class PsScanFileNameService {
|
|
4
|
+
static build(base, format, index, count) {
|
|
5
|
+
const stem = PsScanFileNameService.sanitize(base);
|
|
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';
|
|
14
|
+
}
|
|
15
|
+
static sanitize(base) {
|
|
16
|
+
const cleaned = Array.from(base)
|
|
17
|
+
.filter((char) => char.charCodeAt(0) >= 32)
|
|
18
|
+
.join('')
|
|
19
|
+
.replace(/\.(pdf|jpe?g)$/i, '')
|
|
20
|
+
.replace(/[\\/:*?"<>|]+/g, '-')
|
|
21
|
+
.replace(/\s+/g, ' ')
|
|
22
|
+
.replace(/^[\s.-]+|[\s.-]+$/g, '');
|
|
23
|
+
return cleaned.length === 0 ? PsScanConfig.defaultFileName : cleaned.slice(0, 80);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { PsScanConfig } from '../constants/ps-scan-config';
|
|
2
|
+
export class PsScanGeometryService {
|
|
3
|
+
static area(points) {
|
|
4
|
+
let sum = 0;
|
|
5
|
+
points.forEach((point, index) => {
|
|
6
|
+
const next = points[(index + 1) % points.length];
|
|
7
|
+
sum += point.x * next.y - next.x * point.y;
|
|
8
|
+
});
|
|
9
|
+
return Math.abs(sum) / 2;
|
|
10
|
+
}
|
|
11
|
+
static clamp(point) {
|
|
12
|
+
return { x: Math.min(1, Math.max(0, point.x)), y: Math.min(1, Math.max(0, point.y)) };
|
|
13
|
+
}
|
|
14
|
+
static cover(container, content) {
|
|
15
|
+
if (container.width <= 0 || container.height <= 0 || content.width <= 0 || content.height <= 0)
|
|
16
|
+
return { height: 0, width: 0 };
|
|
17
|
+
const scale = Math.max(container.width / content.width, container.height / content.height);
|
|
18
|
+
return { height: content.height * scale, width: content.width * scale };
|
|
19
|
+
}
|
|
20
|
+
static defaultQuad() {
|
|
21
|
+
const inset = 0.08;
|
|
22
|
+
return [
|
|
23
|
+
{ x: inset, y: inset },
|
|
24
|
+
{ x: 1 - inset, y: inset },
|
|
25
|
+
{ x: 1 - inset, y: 1 - inset },
|
|
26
|
+
{ x: inset, y: 1 - inset },
|
|
27
|
+
];
|
|
28
|
+
}
|
|
29
|
+
static distance(from, to) {
|
|
30
|
+
return Math.hypot(to.x - from.x, to.y - from.y);
|
|
31
|
+
}
|
|
32
|
+
static fit(container, content) {
|
|
33
|
+
if (container.width <= 0 || container.height <= 0 || content.width <= 0 || content.height <= 0)
|
|
34
|
+
return { height: 0, width: 0 };
|
|
35
|
+
const scale = Math.min(container.width / content.width, container.height / content.height);
|
|
36
|
+
return { height: content.height * scale, width: content.width * scale };
|
|
37
|
+
}
|
|
38
|
+
static fitWithin(size, maxDimension) {
|
|
39
|
+
const longest = Math.max(size.width, size.height);
|
|
40
|
+
if (longest <= maxDimension)
|
|
41
|
+
return { height: Math.round(size.height), width: Math.round(size.width) };
|
|
42
|
+
const scale = maxDimension / longest;
|
|
43
|
+
return {
|
|
44
|
+
height: Math.max(1, Math.round(size.height * scale)),
|
|
45
|
+
width: Math.max(1, Math.round(size.width * scale)),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
static inset(quad, distance) {
|
|
49
|
+
const centre = {
|
|
50
|
+
x: quad.reduce((sum, point) => sum + point.x, 0) / quad.length,
|
|
51
|
+
y: quad.reduce((sum, point) => sum + point.y, 0) / quad.length,
|
|
52
|
+
};
|
|
53
|
+
return quad.map((point) => {
|
|
54
|
+
const length = PsScanGeometryService.distance(point, centre);
|
|
55
|
+
const ratio = length <= distance ? 0 : (length - distance) / length;
|
|
56
|
+
return {
|
|
57
|
+
x: centre.x + (point.x - centre.x) * ratio,
|
|
58
|
+
y: centre.y + (point.y - centre.y) * ratio,
|
|
59
|
+
};
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
static isConvex(points) {
|
|
63
|
+
let sign = 0;
|
|
64
|
+
for (let index = 0; index < points.length; index += 1) {
|
|
65
|
+
const a = points[index];
|
|
66
|
+
const b = points[(index + 1) % points.length];
|
|
67
|
+
const c = points[(index + 2) % points.length];
|
|
68
|
+
const cross = (b.x - a.x) * (c.y - b.y) - (b.y - a.y) * (c.x - b.x);
|
|
69
|
+
if (Math.abs(cross) < 1e-9)
|
|
70
|
+
return false;
|
|
71
|
+
if (sign === 0)
|
|
72
|
+
sign = Math.sign(cross);
|
|
73
|
+
else if (Math.sign(cross) !== sign)
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
static isUsable(quad) {
|
|
79
|
+
return (PsScanGeometryService.isConvex(quad) &&
|
|
80
|
+
PsScanGeometryService.area(quad) > PsScanConfig.minCornerGap * PsScanConfig.minCornerGap);
|
|
81
|
+
}
|
|
82
|
+
static nudge(quad, index, dx, dy) {
|
|
83
|
+
const moved = PsScanGeometryService.clamp({ x: quad[index].x + dx, y: quad[index].y + dy });
|
|
84
|
+
const next = quad.map((point, at) => (at === index ? moved : point));
|
|
85
|
+
return PsScanGeometryService.isUsable(next) ? next : quad;
|
|
86
|
+
}
|
|
87
|
+
static order(points) {
|
|
88
|
+
const centre = {
|
|
89
|
+
x: points.reduce((sum, point) => sum + point.x, 0) / points.length,
|
|
90
|
+
y: points.reduce((sum, point) => sum + point.y, 0) / points.length,
|
|
91
|
+
};
|
|
92
|
+
const sorted = [...points].sort((a, b) => Math.atan2(a.y - centre.y, a.x - centre.x) - Math.atan2(b.y - centre.y, b.x - centre.x));
|
|
93
|
+
const sums = sorted.map((point) => point.x + point.y);
|
|
94
|
+
const start = sums.indexOf(Math.min(...sums));
|
|
95
|
+
return [0, 1, 2, 3].map((offset) => sorted[(start + offset) % 4]);
|
|
96
|
+
}
|
|
97
|
+
static outputSize(quad, raster) {
|
|
98
|
+
const pixels = PsScanGeometryService.toPixels(quad, raster);
|
|
99
|
+
const width = (PsScanGeometryService.distance(pixels[0], pixels[1]) +
|
|
100
|
+
PsScanGeometryService.distance(pixels[3], pixels[2])) /
|
|
101
|
+
2;
|
|
102
|
+
const height = (PsScanGeometryService.distance(pixels[0], pixels[3]) +
|
|
103
|
+
PsScanGeometryService.distance(pixels[1], pixels[2])) /
|
|
104
|
+
2;
|
|
105
|
+
return { height: Math.max(1, Math.round(height)), width: Math.max(1, Math.round(width)) };
|
|
106
|
+
}
|
|
107
|
+
static rotateSize(size, rotation) {
|
|
108
|
+
return rotation % 180 === 0 ? size : { height: size.width, width: size.height };
|
|
109
|
+
}
|
|
110
|
+
static toPixels(quad, size) {
|
|
111
|
+
return quad.map((point) => ({
|
|
112
|
+
x: point.x * size.width,
|
|
113
|
+
y: point.y * size.height,
|
|
114
|
+
}));
|
|
115
|
+
}
|
|
116
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export class PsScanHomographyService {
|
|
2
|
+
static apply(matrix, x, y) {
|
|
3
|
+
const w = matrix[6] * x + matrix[7] * y + matrix[8];
|
|
4
|
+
return {
|
|
5
|
+
x: (matrix[0] * x + matrix[1] * y + matrix[2]) / w,
|
|
6
|
+
y: (matrix[3] * x + matrix[4] * y + matrix[5]) / w,
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
static fromUnitSquare(quad) {
|
|
10
|
+
const unit = [
|
|
11
|
+
[0, 0],
|
|
12
|
+
[1, 0],
|
|
13
|
+
[1, 1],
|
|
14
|
+
[0, 1],
|
|
15
|
+
];
|
|
16
|
+
const rows = [];
|
|
17
|
+
unit.forEach(([u, v], index) => {
|
|
18
|
+
const { x, y } = quad[index];
|
|
19
|
+
rows.push([u, v, 1, 0, 0, 0, -u * x, -v * x, x]);
|
|
20
|
+
rows.push([0, 0, 0, u, v, 1, -u * y, -v * y, y]);
|
|
21
|
+
});
|
|
22
|
+
return [...PsScanHomographyService.solve(rows), 1];
|
|
23
|
+
}
|
|
24
|
+
static solve(rows) {
|
|
25
|
+
const size = rows.length;
|
|
26
|
+
const matrix = rows.map((row) => [...row]);
|
|
27
|
+
for (let column = 0; column < size; column += 1) {
|
|
28
|
+
let pivot = column;
|
|
29
|
+
for (let row = column + 1; row < size; row += 1)
|
|
30
|
+
if (Math.abs(matrix[row][column]) > Math.abs(matrix[pivot][column]))
|
|
31
|
+
pivot = row;
|
|
32
|
+
[matrix[column], matrix[pivot]] = [matrix[pivot], matrix[column]];
|
|
33
|
+
const lead = matrix[column][column];
|
|
34
|
+
if (Math.abs(lead) < 1e-12)
|
|
35
|
+
return [1, 0, 0, 0, 1, 0, 0, 0];
|
|
36
|
+
for (let row = 0; row < size; row += 1) {
|
|
37
|
+
if (row === column)
|
|
38
|
+
continue;
|
|
39
|
+
const factor = matrix[row][column] / lead;
|
|
40
|
+
for (let at = column; at <= size; at += 1)
|
|
41
|
+
matrix[row][at] -= factor * matrix[column][at];
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return matrix.map((row, index) => row[size] / row[index]);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { PsScanConfig } from '../constants/ps-scan-config';
|
|
2
|
+
import { PsScanGeometryService } from './ps-scan-geometry-service';
|
|
3
|
+
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
|
+
static convexHull(input) {
|
|
24
|
+
const points = [...input].sort((a, b) => (a.x === b.x ? a.y - b.y : a.x - b.x));
|
|
25
|
+
if (points.length < 3)
|
|
26
|
+
return points;
|
|
27
|
+
const cross = (o, a, b) => (a.x - o.x) * (b.y - o.y) - (a.y - o.y) * (b.x - o.x);
|
|
28
|
+
const build = (list) => {
|
|
29
|
+
const chain = [];
|
|
30
|
+
list.forEach((point) => {
|
|
31
|
+
while (chain.length >= 2 &&
|
|
32
|
+
cross(chain[chain.length - 2], chain[chain.length - 1], point) <= 0)
|
|
33
|
+
chain.pop();
|
|
34
|
+
chain.push(point);
|
|
35
|
+
});
|
|
36
|
+
chain.pop();
|
|
37
|
+
return chain;
|
|
38
|
+
};
|
|
39
|
+
return [...build(points), ...build([...points].reverse())];
|
|
40
|
+
}
|
|
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
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
export class PsScanImageFilterService {
|
|
2
|
+
static blur(gray, width, height) {
|
|
3
|
+
const kernel = [1, 4, 6, 4, 1];
|
|
4
|
+
const pass = new Float32Array(width * height);
|
|
5
|
+
const out = new Uint8ClampedArray(width * height);
|
|
6
|
+
for (let y = 0; y < height; y += 1)
|
|
7
|
+
for (let x = 0; x < width; x += 1) {
|
|
8
|
+
let sum = 0;
|
|
9
|
+
for (let k = -2; k <= 2; k += 1)
|
|
10
|
+
sum += gray[y * width + Math.min(width - 1, Math.max(0, x + k))] * kernel[k + 2];
|
|
11
|
+
pass[y * width + x] = sum / 16;
|
|
12
|
+
}
|
|
13
|
+
for (let y = 0; y < height; y += 1)
|
|
14
|
+
for (let x = 0; x < width; x += 1) {
|
|
15
|
+
let sum = 0;
|
|
16
|
+
for (let k = -2; k <= 2; k += 1)
|
|
17
|
+
sum += pass[Math.min(height - 1, Math.max(0, y + k)) * width + x] * kernel[k + 2];
|
|
18
|
+
out[y * width + x] = sum / 16;
|
|
19
|
+
}
|
|
20
|
+
return out;
|
|
21
|
+
}
|
|
22
|
+
static grayscale(raster) {
|
|
23
|
+
const out = new Uint8ClampedArray(raster.width * raster.height);
|
|
24
|
+
for (let index = 0; index < out.length; index += 1) {
|
|
25
|
+
const at = index * 4;
|
|
26
|
+
out[index] =
|
|
27
|
+
0.299 * raster.data[at] + 0.587 * raster.data[at + 1] + 0.114 * raster.data[at + 2];
|
|
28
|
+
}
|
|
29
|
+
return out;
|
|
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
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export class PsScanOrderService {
|
|
2
|
+
static move(items, from, to) {
|
|
3
|
+
if (from === to || from < 0 || to < 0 || from >= items.length || to >= items.length)
|
|
4
|
+
return items;
|
|
5
|
+
const next = [...items];
|
|
6
|
+
const [item] = next.splice(from, 1);
|
|
7
|
+
next.splice(to, 0, item);
|
|
8
|
+
return next;
|
|
9
|
+
}
|
|
10
|
+
static step(items, index, direction) {
|
|
11
|
+
return PsScanOrderService.move(items, index, index + direction);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { PsScanOutputFormat } from '../constants/ps-scan-output-format';
|
|
2
|
+
import { PsScanFileNameService } from './ps-scan-file-name-service';
|
|
3
|
+
import { PsScanPdfService } from './ps-scan-pdf-service';
|
|
4
|
+
import { PsScanPipelineService } from './ps-scan-pipeline-service';
|
|
5
|
+
export class PsScanOutputService {
|
|
6
|
+
static async build(pages, options) {
|
|
7
|
+
const rendered = [];
|
|
8
|
+
for (const page of pages)
|
|
9
|
+
rendered.push(await PsScanPipelineService.render(page, options.maxDimension, options.jpegQuality));
|
|
10
|
+
const type = PsScanFileNameService.mimeType(options.format);
|
|
11
|
+
if (options.format === PsScanOutputFormat.Jpeg)
|
|
12
|
+
return rendered.map((item, index) => new File([item.blob], PsScanFileNameService.build(options.fileName, options.format, index, rendered.length), { type }));
|
|
13
|
+
const jpegs = await Promise.all(rendered.map(async (item) => ({
|
|
14
|
+
height: item.height,
|
|
15
|
+
jpeg: new Uint8Array(await item.blob.arrayBuffer()),
|
|
16
|
+
width: item.width,
|
|
17
|
+
})));
|
|
18
|
+
return [
|
|
19
|
+
new File([PsScanPdfService.build(jpegs)], PsScanFileNameService.build(options.fileName, options.format, 0, 1), {
|
|
20
|
+
type,
|
|
21
|
+
}),
|
|
22
|
+
];
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { PsScanConfig } from '../constants/ps-scan-config';
|
|
2
|
+
export class PsScanPdfService {
|
|
3
|
+
static build(pages) {
|
|
4
|
+
const encoder = new TextEncoder();
|
|
5
|
+
const chunks = [];
|
|
6
|
+
const offsets = [];
|
|
7
|
+
let length = 0;
|
|
8
|
+
const push = (chunk) => {
|
|
9
|
+
chunks.push(chunk);
|
|
10
|
+
length += chunk.length;
|
|
11
|
+
};
|
|
12
|
+
const text = (value) => push(encoder.encode(value));
|
|
13
|
+
const begin = (id) => {
|
|
14
|
+
offsets[id] = length;
|
|
15
|
+
text(`${id} 0 obj\n`);
|
|
16
|
+
};
|
|
17
|
+
const kids = pages.map((_, index) => `${3 + index * 3} 0 R`).join(' ');
|
|
18
|
+
push(Uint8Array.from([
|
|
19
|
+
0x25, 0x50, 0x44, 0x46, 0x2d, 0x31, 0x2e, 0x34, 0x0a, 0x25, 0xe2, 0xe3, 0xcf, 0xd3, 0x0a,
|
|
20
|
+
]));
|
|
21
|
+
begin(1);
|
|
22
|
+
text('<< /Type /Catalog /Pages 2 0 R >>\nendobj\n');
|
|
23
|
+
begin(2);
|
|
24
|
+
text(`<< /Type /Pages /Kids [${kids}] /Count ${pages.length} >>\nendobj\n`);
|
|
25
|
+
pages.forEach((page, index) => {
|
|
26
|
+
const pageId = 3 + index * 3;
|
|
27
|
+
const widthPt = PsScanPdfService.points(page.width);
|
|
28
|
+
const heightPt = PsScanPdfService.points(page.height);
|
|
29
|
+
const content = `q ${widthPt} 0 0 ${heightPt} 0 0 cm /Im0 Do Q`;
|
|
30
|
+
begin(pageId);
|
|
31
|
+
text(`<< /Type /Page /Parent 2 0 R /MediaBox [0 0 ${widthPt} ${heightPt}] /Resources << /XObject << /Im0 ${pageId + 2} 0 R >> >> /Contents ${pageId + 1} 0 R >>\nendobj\n`);
|
|
32
|
+
begin(pageId + 1);
|
|
33
|
+
text(`<< /Length ${content.length} >>\nstream\n${content}\nendstream\nendobj\n`);
|
|
34
|
+
begin(pageId + 2);
|
|
35
|
+
text(`<< /Type /XObject /Subtype /Image /Width ${page.width} /Height ${page.height} /ColorSpace /DeviceRGB /BitsPerComponent 8 /Filter /DCTDecode /Length ${page.jpeg.length} >>\nstream\n`);
|
|
36
|
+
push(page.jpeg);
|
|
37
|
+
text('\nendstream\nendobj\n');
|
|
38
|
+
});
|
|
39
|
+
const count = 3 + pages.length * 3;
|
|
40
|
+
const xrefAt = length;
|
|
41
|
+
text(`xref\n0 ${count}\n0000000000 65535 f \n`);
|
|
42
|
+
for (let id = 1; id < count; id += 1)
|
|
43
|
+
text(`${String(offsets[id]).padStart(10, '0')} 00000 n \n`);
|
|
44
|
+
text(`trailer\n<< /Size ${count} /Root 1 0 R >>\nstartxref\n${xrefAt}\n%%EOF\n`);
|
|
45
|
+
const out = new Uint8Array(length);
|
|
46
|
+
let at = 0;
|
|
47
|
+
chunks.forEach((chunk) => {
|
|
48
|
+
out.set(chunk, at);
|
|
49
|
+
at += chunk.length;
|
|
50
|
+
});
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
static points(pixels) {
|
|
54
|
+
return Math.round((pixels * PsScanConfig.pdfPointsPerInch * 100) / PsScanConfig.pdfDpi) / 100;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { PsScanConfig } from '../constants/ps-scan-config';
|
|
2
|
+
import { PsScanCanvasService } from './ps-scan-canvas-service';
|
|
3
|
+
import { PsScanEdgeService } from './ps-scan-edge-service';
|
|
4
|
+
import { PsScanEnhanceService } from './ps-scan-enhance-service';
|
|
5
|
+
import { PsScanGeometryService } from './ps-scan-geometry-service';
|
|
6
|
+
import { PsScanRasterService } from './ps-scan-raster-service';
|
|
7
|
+
import { PsScanWarpService } from './ps-scan-warp-service';
|
|
8
|
+
export class PsScanPipelineService {
|
|
9
|
+
static async prepare(source) {
|
|
10
|
+
const decoded = await PsScanCanvasService.decode(source, PsScanConfig.detectMaxDimension * 2);
|
|
11
|
+
const detected = PsScanEdgeService.detect(decoded.raster);
|
|
12
|
+
return {
|
|
13
|
+
corners: detected === null ? PsScanGeometryService.defaultQuad() : detected,
|
|
14
|
+
height: decoded.naturalHeight,
|
|
15
|
+
isDetected: detected !== null,
|
|
16
|
+
width: decoded.naturalWidth,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
static async measure(source) {
|
|
20
|
+
const decoded = await PsScanCanvasService.decode(source, PsScanConfig.previewMaxDimension);
|
|
21
|
+
return decoded.raster;
|
|
22
|
+
}
|
|
23
|
+
static process(page, raster, maxDimension) {
|
|
24
|
+
const size = PsScanGeometryService.fitWithin(PsScanGeometryService.outputSize(page.corners, raster), maxDimension);
|
|
25
|
+
const warped = PsScanWarpService.warp(raster, PsScanGeometryService.toPixels(page.corners, {
|
|
26
|
+
height: raster.height - 1,
|
|
27
|
+
width: raster.width - 1,
|
|
28
|
+
}), 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
|
+
return {
|
|
35
|
+
blob: await PsScanCanvasService.encode(raster, quality),
|
|
36
|
+
height: raster.height,
|
|
37
|
+
width: raster.width,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { PsScanPlatform } from '../constants/ps-scan-platform';
|
|
2
|
+
export class PsScanPlatformService {
|
|
3
|
+
static detect() {
|
|
4
|
+
if (typeof navigator === 'undefined')
|
|
5
|
+
return PsScanPlatform.Desktop;
|
|
6
|
+
const agent = navigator.userAgent;
|
|
7
|
+
if (/iPhone|iPad|iPod/i.test(agent))
|
|
8
|
+
return PsScanPlatform.Ios;
|
|
9
|
+
if (/Macintosh/i.test(agent) && navigator.maxTouchPoints > 1)
|
|
10
|
+
return PsScanPlatform.Ios;
|
|
11
|
+
if (/Android/i.test(agent))
|
|
12
|
+
return PsScanPlatform.Android;
|
|
13
|
+
return PsScanPlatform.Desktop;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
export class PsScanRasterService {
|
|
2
|
+
static create(width, height) {
|
|
3
|
+
return { data: new Uint8ClampedArray(width * height * 4), height, width };
|
|
4
|
+
}
|
|
5
|
+
static resize(source, width, height) {
|
|
6
|
+
if (width === source.width && height === source.height)
|
|
7
|
+
return source;
|
|
8
|
+
const target = PsScanRasterService.create(width, height);
|
|
9
|
+
const scaleX = source.width / width;
|
|
10
|
+
const scaleY = source.height / height;
|
|
11
|
+
for (let y = 0; y < height; y += 1) {
|
|
12
|
+
const y0 = Math.floor(y * scaleY);
|
|
13
|
+
const y1 = Math.min(source.height, Math.max(y0 + 1, Math.ceil((y + 1) * scaleY)));
|
|
14
|
+
for (let x = 0; x < width; x += 1) {
|
|
15
|
+
const x0 = Math.floor(x * scaleX);
|
|
16
|
+
const x1 = Math.min(source.width, Math.max(x0 + 1, Math.ceil((x + 1) * scaleX)));
|
|
17
|
+
let r = 0;
|
|
18
|
+
let g = 0;
|
|
19
|
+
let b = 0;
|
|
20
|
+
let a = 0;
|
|
21
|
+
let count = 0;
|
|
22
|
+
for (let sy = y0; sy < y1; sy += 1)
|
|
23
|
+
for (let sx = x0; sx < x1; sx += 1) {
|
|
24
|
+
const at = (sy * source.width + sx) * 4;
|
|
25
|
+
r += source.data[at];
|
|
26
|
+
g += source.data[at + 1];
|
|
27
|
+
b += source.data[at + 2];
|
|
28
|
+
a += source.data[at + 3];
|
|
29
|
+
count += 1;
|
|
30
|
+
}
|
|
31
|
+
const out = (y * width + x) * 4;
|
|
32
|
+
target.data[out] = r / count;
|
|
33
|
+
target.data[out + 1] = g / count;
|
|
34
|
+
target.data[out + 2] = b / count;
|
|
35
|
+
target.data[out + 3] = a / count;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return target;
|
|
39
|
+
}
|
|
40
|
+
static rotate(source, degrees) {
|
|
41
|
+
const turns = ((Math.round(degrees / 90) % 4) + 4) % 4;
|
|
42
|
+
if (turns === 0)
|
|
43
|
+
return source;
|
|
44
|
+
const swapped = turns % 2 === 1;
|
|
45
|
+
const width = swapped ? source.height : source.width;
|
|
46
|
+
const height = swapped ? source.width : source.height;
|
|
47
|
+
const target = PsScanRasterService.create(width, height);
|
|
48
|
+
for (let y = 0; y < source.height; y += 1)
|
|
49
|
+
for (let x = 0; x < source.width; x += 1) {
|
|
50
|
+
let tx = x;
|
|
51
|
+
let ty = y;
|
|
52
|
+
if (turns === 1)
|
|
53
|
+
[tx, ty] = [source.height - 1 - y, x];
|
|
54
|
+
else if (turns === 2)
|
|
55
|
+
[tx, ty] = [source.width - 1 - x, source.height - 1 - y];
|
|
56
|
+
else
|
|
57
|
+
[tx, ty] = [y, source.width - 1 - x];
|
|
58
|
+
const from = (y * source.width + x) * 4;
|
|
59
|
+
const to = (ty * width + tx) * 4;
|
|
60
|
+
target.data[to] = source.data[from];
|
|
61
|
+
target.data[to + 1] = source.data[from + 1];
|
|
62
|
+
target.data[to + 2] = source.data[from + 2];
|
|
63
|
+
target.data[to + 3] = source.data[from + 3];
|
|
64
|
+
}
|
|
65
|
+
return target;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { PsScanConfig } from '../constants/ps-scan-config';
|
|
2
|
+
import { PsScanGeometryService } from './ps-scan-geometry-service';
|
|
3
|
+
export class PsScanStabilityService {
|
|
4
|
+
static disarm(state) {
|
|
5
|
+
return { ...state, isArmed: false, stableMs: 0 };
|
|
6
|
+
}
|
|
7
|
+
static initial() {
|
|
8
|
+
return { isArmed: true, misses: 0, quad: null, raw: null, stableMs: 0 };
|
|
9
|
+
}
|
|
10
|
+
static movement(from, to) {
|
|
11
|
+
return Math.max(...from.map((point, index) => PsScanGeometryService.distance(point, to[index])));
|
|
12
|
+
}
|
|
13
|
+
static next(state, detected, elapsedMs) {
|
|
14
|
+
if (detected === null) {
|
|
15
|
+
const misses = state.misses + 1;
|
|
16
|
+
return misses > PsScanConfig.liveMissLimit
|
|
17
|
+
? { isArmed: true, misses, quad: null, raw: null, stableMs: 0 }
|
|
18
|
+
: { ...state, misses, stableMs: 0 };
|
|
19
|
+
}
|
|
20
|
+
if (state.quad === null || state.raw === null)
|
|
21
|
+
return { ...state, misses: 0, quad: detected, raw: detected, stableMs: 0 };
|
|
22
|
+
const movement = PsScanStabilityService.movement(state.raw, detected);
|
|
23
|
+
const weight = PsScanConfig.liveSmoothing;
|
|
24
|
+
const quad = state.quad.map((point, index) => ({
|
|
25
|
+
x: point.x + (detected[index].x - point.x) * weight,
|
|
26
|
+
y: point.y + (detected[index].y - point.y) * weight,
|
|
27
|
+
}));
|
|
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);
|
|
40
|
+
}
|
|
41
|
+
}
|