@pallasoft/ps-file-components 1.5.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (147) hide show
  1. package/dist/components/ps-document-scanner-loader.d.ts +1 -1
  2. package/dist/components/ps-document-scanner-sheet.d.ts +1 -1
  3. package/dist/components/ps-document-scanner-sheet.js +40 -110
  4. package/dist/components/ps-document-scanner.d.ts +1 -1
  5. package/dist/components/ps-document-scanner.js +2 -3
  6. package/dist/components/ps-scan-camera-view.d.ts +2 -9
  7. package/dist/components/ps-scan-camera-view.js +3 -12
  8. package/dist/components/ps-scan-crop-editor.d.ts +4 -4
  9. package/dist/components/ps-scan-crop-editor.js +9 -9
  10. package/dist/components/ps-scan-permission-view.d.ts +2 -3
  11. package/dist/components/ps-scan-permission-view.js +2 -2
  12. package/dist/components/ps-scan-photo-button.d.ts +2 -3
  13. package/dist/components/ps-scan-photo-button.js +5 -5
  14. package/dist/components/ps-scan-preview.d.ts +12 -0
  15. package/dist/components/ps-scan-preview.js +10 -0
  16. package/dist/components/ps-scan-shutter.d.ts +1 -2
  17. package/dist/components/ps-scan-shutter.js +1 -1
  18. package/dist/constants/ps-document-scanner-labels.d.ts +5 -34
  19. package/dist/constants/ps-document-scanner-labels.js +5 -34
  20. package/dist/constants/ps-scan-chrome-classes.d.ts +0 -2
  21. package/dist/constants/ps-scan-chrome-classes.js +0 -2
  22. package/dist/constants/ps-scan-config.d.ts +24 -31
  23. package/dist/constants/ps-scan-config.js +24 -31
  24. package/dist/constants/ps-scan-step.d.ts +1 -1
  25. package/dist/constants/ps-scan-step.js +1 -1
  26. package/dist/esm/components/ps-document-scanner-sheet.js +41 -111
  27. package/dist/esm/components/ps-document-scanner.js +2 -3
  28. package/dist/esm/components/ps-scan-camera-view.js +3 -12
  29. package/dist/esm/components/ps-scan-crop-editor.js +9 -9
  30. package/dist/esm/components/ps-scan-permission-view.js +2 -2
  31. package/dist/esm/components/ps-scan-photo-button.js +5 -5
  32. package/dist/esm/components/ps-scan-preview.js +6 -0
  33. package/dist/esm/components/ps-scan-shutter.js +1 -1
  34. package/dist/esm/constants/ps-document-scanner-labels.js +5 -34
  35. package/dist/esm/constants/ps-scan-chrome-classes.js +0 -2
  36. package/dist/esm/constants/ps-scan-config.js +24 -31
  37. package/dist/esm/constants/ps-scan-step.js +1 -1
  38. package/dist/esm/hooks/use-ps-scan-live-detection.js +17 -21
  39. package/dist/esm/hooks/use-ps-scan-photo.js +42 -0
  40. package/dist/esm/index.js +0 -2
  41. package/dist/esm/services/ps-scan-camera-service.js +2 -2
  42. package/dist/esm/services/ps-scan-canny-service.js +93 -0
  43. package/dist/esm/services/ps-scan-canvas-service.js +7 -1
  44. package/dist/esm/services/ps-scan-edge-service.js +73 -137
  45. package/dist/esm/services/ps-scan-file-name-service.js +2 -11
  46. package/dist/esm/services/ps-scan-geometry-service.js +36 -0
  47. package/dist/esm/services/ps-scan-hull-service.js +0 -39
  48. package/dist/esm/services/ps-scan-image-filter-service.js +0 -21
  49. package/dist/esm/services/ps-scan-morphology-service.js +44 -0
  50. package/dist/esm/services/ps-scan-pipeline-service.js +9 -19
  51. package/dist/esm/services/ps-scan-polygon-service.js +75 -0
  52. package/dist/esm/services/ps-scan-region-service.js +52 -0
  53. package/dist/esm/services/ps-scan-stability-service.js +9 -22
  54. package/dist/esm/services/ps-scan-warp-service.js +8 -2
  55. package/dist/esm/services/ps-scan-yield-service.js +5 -0
  56. package/dist/hooks/use-ps-scan-live-detection.d.ts +1 -1
  57. package/dist/hooks/use-ps-scan-live-detection.js +16 -20
  58. package/dist/hooks/use-ps-scan-photo.d.ts +9 -0
  59. package/dist/hooks/use-ps-scan-photo.js +46 -0
  60. package/dist/index.d.ts +0 -2
  61. package/dist/index.js +0 -2
  62. package/dist/models/ps-document-scanner-props.d.ts +0 -4
  63. package/dist/models/ps-document-scanner-sheet-props.d.ts +0 -4
  64. package/dist/models/ps-scan-gradient.d.ts +4 -0
  65. package/dist/models/ps-scan-live-detection.d.ts +0 -1
  66. package/dist/models/ps-scan-located.d.ts +5 -0
  67. package/dist/models/ps-scan-photo.d.ts +9 -0
  68. package/dist/models/ps-scan-region.d.ts +5 -0
  69. package/dist/models/ps-scan-result.d.ts +6 -0
  70. package/dist/models/ps-scan-stability.d.ts +0 -3
  71. package/dist/services/ps-scan-camera-service.js +2 -2
  72. package/dist/services/ps-scan-canny-service.d.ts +7 -0
  73. package/dist/services/ps-scan-canny-service.js +97 -0
  74. package/dist/services/ps-scan-canvas-service.js +7 -1
  75. package/dist/services/ps-scan-edge-service.d.ts +5 -5
  76. package/dist/services/ps-scan-edge-service.js +73 -137
  77. package/dist/services/ps-scan-file-name-service.d.ts +1 -3
  78. package/dist/services/ps-scan-file-name-service.js +2 -11
  79. package/dist/services/ps-scan-geometry-service.d.ts +2 -0
  80. package/dist/services/ps-scan-geometry-service.js +36 -0
  81. package/dist/services/ps-scan-hull-service.d.ts +0 -2
  82. package/dist/services/ps-scan-hull-service.js +0 -39
  83. package/dist/services/ps-scan-image-filter-service.d.ts +0 -1
  84. package/dist/services/ps-scan-image-filter-service.js +0 -21
  85. package/dist/services/ps-scan-morphology-service.d.ts +7 -0
  86. package/dist/services/ps-scan-morphology-service.js +48 -0
  87. package/dist/services/ps-scan-pipeline-service.d.ts +2 -5
  88. package/dist/services/ps-scan-pipeline-service.js +9 -19
  89. package/dist/services/ps-scan-polygon-service.d.ts +8 -0
  90. package/dist/services/ps-scan-polygon-service.js +79 -0
  91. package/dist/services/ps-scan-region-service.d.ts +4 -0
  92. package/dist/services/ps-scan-region-service.js +56 -0
  93. package/dist/services/ps-scan-stability-service.d.ts +1 -3
  94. package/dist/services/ps-scan-stability-service.js +9 -22
  95. package/dist/services/ps-scan-warp-service.d.ts +1 -1
  96. package/dist/services/ps-scan-warp-service.js +8 -2
  97. package/dist/services/ps-scan-yield-service.d.ts +3 -0
  98. package/dist/services/ps-scan-yield-service.js +9 -0
  99. package/package.json +1 -1
  100. package/dist/components/ps-scan-page-list.d.ts +0 -14
  101. package/dist/components/ps-scan-page-list.js +0 -22
  102. package/dist/components/ps-scan-page-stack.d.ts +0 -7
  103. package/dist/components/ps-scan-page-stack.js +0 -6
  104. package/dist/components/ps-scan-review.d.ts +0 -24
  105. package/dist/components/ps-scan-review.js +0 -37
  106. package/dist/constants/ps-scan-filter.d.ts +0 -7
  107. package/dist/constants/ps-scan-filter.js +0 -9
  108. package/dist/constants/ps-scan-output-format.d.ts +0 -5
  109. package/dist/constants/ps-scan-output-format.js +0 -7
  110. package/dist/esm/components/ps-scan-page-list.js +0 -18
  111. package/dist/esm/components/ps-scan-page-stack.js +0 -2
  112. package/dist/esm/components/ps-scan-review.js +0 -33
  113. package/dist/esm/constants/ps-scan-filter.js +0 -6
  114. package/dist/esm/constants/ps-scan-output-format.js +0 -4
  115. package/dist/esm/hooks/use-ps-scan-pages.js +0 -62
  116. package/dist/esm/hooks/use-ps-scan-previews.js +0 -57
  117. package/dist/esm/services/ps-scan-enhance-service.js +0 -97
  118. package/dist/esm/services/ps-scan-order-service.js +0 -13
  119. package/dist/esm/services/ps-scan-output-service.js +0 -24
  120. package/dist/esm/services/ps-scan-pdf-service.js +0 -56
  121. package/dist/hooks/use-ps-scan-pages.d.ts +0 -12
  122. package/dist/hooks/use-ps-scan-pages.js +0 -66
  123. package/dist/hooks/use-ps-scan-previews.d.ts +0 -2
  124. package/dist/hooks/use-ps-scan-previews.js +0 -61
  125. package/dist/models/ps-scan-added-page.d.ts +0 -5
  126. package/dist/models/ps-scan-levels.d.ts +0 -4
  127. package/dist/models/ps-scan-output-options.d.ts +0 -7
  128. package/dist/models/ps-scan-page.d.ts +0 -12
  129. package/dist/models/ps-scan-pdf-page.d.ts +0 -5
  130. package/dist/services/ps-scan-enhance-service.d.ts +0 -9
  131. package/dist/services/ps-scan-enhance-service.js +0 -101
  132. package/dist/services/ps-scan-order-service.d.ts +0 -4
  133. package/dist/services/ps-scan-order-service.js +0 -17
  134. package/dist/services/ps-scan-output-service.d.ts +0 -5
  135. package/dist/services/ps-scan-output-service.js +0 -28
  136. package/dist/services/ps-scan-pdf-service.d.ts +0 -5
  137. package/dist/services/ps-scan-pdf-service.js +0 -60
  138. /package/dist/esm/models/{ps-scan-added-page.js → ps-scan-gradient.js} +0 -0
  139. /package/dist/esm/models/{ps-scan-levels.js → ps-scan-located.js} +0 -0
  140. /package/dist/esm/models/{ps-scan-output-options.js → ps-scan-photo.js} +0 -0
  141. /package/dist/esm/models/{ps-scan-page.js → ps-scan-region.js} +0 -0
  142. /package/dist/esm/models/{ps-scan-pdf-page.js → ps-scan-result.js} +0 -0
  143. /package/dist/models/{ps-scan-added-page.js → ps-scan-gradient.js} +0 -0
  144. /package/dist/models/{ps-scan-levels.js → ps-scan-located.js} +0 -0
  145. /package/dist/models/{ps-scan-output-options.js → ps-scan-photo.js} +0 -0
  146. /package/dist/models/{ps-scan-page.js → ps-scan-region.js} +0 -0
  147. /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, PsScanConfig.detectMaxDimension / Math.max(source.width, source.height));
9
- const width = Math.max(8, Math.round(source.width * scale));
10
- const height = Math.max(8, Math.round(source.height * scale));
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
- const edges = PsScanImageFilterService.sobel(gray, width, height);
14
- let mask = PsScanEdgeService.threshold(edges);
15
- for (let step = 0; step < PsScanConfig.detectDilations; step += 1)
16
- mask = PsScanEdgeService.dilate(mask, width, height);
17
- const foreground = PsScanEdgeService.foreground(mask, width, height);
18
- const component = PsScanEdgeService.largestComponent(foreground, width, height);
19
- if (component.length < 8)
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
- const hull = PsScanHullService.convexHull(component);
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 quad = PsScanGeometryService.order(PsScanHullService.bestQuad(hull));
25
- const imageArea = width * height;
26
- const quadArea = PsScanGeometryService.area(quad);
27
- const hullArea = PsScanGeometryService.area(hull);
28
- if (quadArea / imageArea < PsScanConfig.detectMinAreaRatio)
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
- if (quadArea / imageArea > PsScanConfig.detectMaxAreaRatio)
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 (hullArea > 0 && quadArea / hullArea < PsScanConfig.detectMinHullFill)
78
+ if (fill < PsScanConfig.detectMinQuadFill)
33
79
  return null;
34
80
  if (!PsScanGeometryService.isConvex(quad))
35
81
  return null;
36
- return PsScanGeometryService.inset(quad, PsScanConfig.detectInset).map((point) => ({
37
- x: Math.min(1, Math.max(0, point.x / (width - 1))),
38
- y: Math.min(1, Math.max(0, point.y / (height - 1))),
39
- }));
40
- }
41
- static dilate(mask, width, height) {
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 threshold(edges) {
139
- const bins = new Uint32Array(512);
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, 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';
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 detected = PsScanEdgeService.detect(decoded.raster);
9
+ const located = await PsScanEdgeService.locate(decoded.raster, PsScanConfig.detectMaxDimension);
12
10
  return {
13
- corners: detected === null ? PsScanGeometryService.defaultQuad() : detected,
11
+ corners: located.quad,
14
12
  height: decoded.naturalHeight,
15
- isDetected: detected !== null,
13
+ isDetected: located.isDetected,
16
14
  width: decoded.naturalWidth,
17
15
  };
18
16
  }
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,
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 { isArmed: true, misses: 0, quad: null, raw: null, stableMs: 0 };
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, elapsedMs) {
10
+ static next(state, detected) {
14
11
  if (detected === null) {
15
12
  const misses = state.misses + 1;
16
13
  return misses > PsScanConfig.liveMissLimit
17
- ? { isArmed: true, misses, quad: null, raw: null, stableMs: 0 }
18
- : { ...state, misses, stableMs: 0 };
14
+ ? { misses, quad: null }
15
+ : { misses, quad: state.quad };
19
16
  }
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);
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
  }