@pallasoft/ps-file-components 1.3.0 → 1.4.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 (154) hide show
  1. package/dist/components/ps-document-scanner-loader.d.ts +1 -0
  2. package/dist/components/ps-document-scanner-loader.js +40 -0
  3. package/dist/components/ps-document-scanner-sheet.d.ts +2 -0
  4. package/dist/components/ps-document-scanner-sheet.js +138 -0
  5. package/dist/components/ps-document-scanner.d.ts +2 -0
  6. package/dist/components/ps-document-scanner.js +17 -0
  7. package/dist/components/ps-file-viewer-pane.d.ts +2 -0
  8. package/dist/components/ps-file-viewer-pane.js +39 -0
  9. package/dist/components/ps-scan-camera-view.d.ts +17 -0
  10. package/dist/components/ps-scan-camera-view.js +27 -0
  11. package/dist/components/ps-scan-crop-editor.d.ts +13 -0
  12. package/dist/components/ps-scan-crop-editor.js +53 -0
  13. package/dist/components/ps-scan-icon-button.d.ts +10 -0
  14. package/dist/components/ps-scan-icon-button.js +7 -0
  15. package/dist/components/ps-scan-page-list.d.ts +12 -0
  16. package/dist/components/ps-scan-page-list.js +23 -0
  17. package/dist/components/ps-scan-review.d.ts +23 -0
  18. package/dist/components/ps-scan-review.js +20 -0
  19. package/dist/constants/ps-document-scanner-labels.d.ts +50 -0
  20. package/dist/constants/ps-document-scanner-labels.js +53 -0
  21. package/dist/constants/ps-scan-camera-status.d.ts +7 -0
  22. package/dist/constants/ps-scan-camera-status.js +9 -0
  23. package/dist/constants/ps-scan-config.d.ts +31 -0
  24. package/dist/constants/ps-scan-config.js +34 -0
  25. package/dist/constants/ps-scan-corner-order.d.ts +1 -0
  26. package/dist/constants/ps-scan-corner-order.js +4 -0
  27. package/dist/constants/ps-scan-filter.d.ts +7 -0
  28. package/dist/constants/ps-scan-filter.js +9 -0
  29. package/dist/constants/ps-scan-output-format.d.ts +5 -0
  30. package/dist/constants/ps-scan-output-format.js +7 -0
  31. package/dist/constants/ps-scan-step.d.ts +6 -0
  32. package/dist/constants/ps-scan-step.js +8 -0
  33. package/dist/esm/components/ps-document-scanner-loader.js +4 -0
  34. package/dist/esm/components/ps-document-scanner-sheet.js +134 -0
  35. package/dist/esm/components/ps-document-scanner.js +13 -0
  36. package/dist/esm/components/ps-file-viewer-pane.js +35 -0
  37. package/dist/esm/components/ps-scan-camera-view.js +23 -0
  38. package/dist/esm/components/ps-scan-crop-editor.js +49 -0
  39. package/dist/esm/components/ps-scan-icon-button.js +3 -0
  40. package/dist/esm/components/ps-scan-page-list.js +19 -0
  41. package/dist/esm/components/ps-scan-review.js +16 -0
  42. package/dist/esm/constants/ps-document-scanner-labels.js +50 -0
  43. package/dist/esm/constants/ps-scan-camera-status.js +6 -0
  44. package/dist/esm/constants/ps-scan-config.js +31 -0
  45. package/dist/esm/constants/ps-scan-corner-order.js +1 -0
  46. package/dist/esm/constants/ps-scan-filter.js +6 -0
  47. package/dist/esm/constants/ps-scan-output-format.js +4 -0
  48. package/dist/esm/constants/ps-scan-step.js +5 -0
  49. package/dist/esm/hooks/use-ps-focus-trap.js +45 -0
  50. package/dist/esm/hooks/use-ps-scan-camera.js +62 -0
  51. package/dist/esm/hooks/use-ps-scan-pages.js +62 -0
  52. package/dist/esm/hooks/use-ps-scan-previews.js +57 -0
  53. package/dist/esm/index.js +7 -0
  54. package/dist/esm/models/ps-document-scanner-props.js +1 -0
  55. package/dist/esm/models/ps-document-scanner-sheet-props.js +1 -0
  56. package/dist/esm/models/ps-file-viewer-pane-props.js +1 -0
  57. package/dist/esm/models/ps-scan-added-page.js +1 -0
  58. package/dist/esm/models/ps-scan-camera-session.js +1 -0
  59. package/dist/esm/models/ps-scan-decoded-image.js +1 -0
  60. package/dist/esm/models/ps-scan-levels.js +1 -0
  61. package/dist/esm/models/ps-scan-output-options.js +1 -0
  62. package/dist/esm/models/ps-scan-page.js +1 -0
  63. package/dist/esm/models/ps-scan-pdf-page.js +1 -0
  64. package/dist/esm/models/ps-scan-prepared.js +1 -0
  65. package/dist/esm/models/ps-scan-quad.js +1 -0
  66. package/dist/esm/models/ps-scan-raster.js +1 -0
  67. package/dist/esm/models/ps-scan-rendered-page.js +1 -0
  68. package/dist/esm/services/ps-scan-camera-service.js +50 -0
  69. package/dist/esm/services/ps-scan-canvas-service.js +46 -0
  70. package/dist/esm/services/ps-scan-edge-service.js +156 -0
  71. package/dist/esm/services/ps-scan-enhance-service.js +97 -0
  72. package/dist/esm/services/ps-scan-file-name-service.js +25 -0
  73. package/dist/esm/services/ps-scan-geometry-service.js +96 -0
  74. package/dist/esm/services/ps-scan-homography-service.js +46 -0
  75. package/dist/esm/services/ps-scan-hull-service.js +59 -0
  76. package/dist/esm/services/ps-scan-image-filter-service.js +52 -0
  77. package/dist/esm/services/ps-scan-order-service.js +13 -0
  78. package/dist/esm/services/ps-scan-output-service.js +24 -0
  79. package/dist/esm/services/ps-scan-pdf-service.js +56 -0
  80. package/dist/esm/services/ps-scan-pipeline-service.js +40 -0
  81. package/dist/esm/services/ps-scan-raster-service.js +67 -0
  82. package/dist/esm/services/ps-scan-template-service.js +5 -0
  83. package/dist/esm/services/ps-scan-warp-service.js +35 -0
  84. package/dist/hooks/use-ps-focus-trap.d.ts +2 -0
  85. package/dist/hooks/use-ps-focus-trap.js +49 -0
  86. package/dist/hooks/use-ps-scan-camera.d.ts +9 -0
  87. package/dist/hooks/use-ps-scan-camera.js +66 -0
  88. package/dist/hooks/use-ps-scan-pages.d.ts +12 -0
  89. package/dist/hooks/use-ps-scan-pages.js +66 -0
  90. package/dist/hooks/use-ps-scan-previews.d.ts +2 -0
  91. package/dist/hooks/use-ps-scan-previews.js +61 -0
  92. package/dist/index.d.ts +7 -0
  93. package/dist/index.js +7 -0
  94. package/dist/models/ps-document-scanner-props.d.ts +15 -0
  95. package/dist/models/ps-document-scanner-props.js +2 -0
  96. package/dist/models/ps-document-scanner-sheet-props.d.ts +13 -0
  97. package/dist/models/ps-document-scanner-sheet-props.js +2 -0
  98. package/dist/models/ps-file-viewer-pane-props.d.ts +12 -0
  99. package/dist/models/ps-file-viewer-pane-props.js +2 -0
  100. package/dist/models/ps-scan-added-page.d.ts +5 -0
  101. package/dist/models/ps-scan-added-page.js +2 -0
  102. package/dist/models/ps-scan-camera-session.d.ts +5 -0
  103. package/dist/models/ps-scan-camera-session.js +2 -0
  104. package/dist/models/ps-scan-decoded-image.d.ts +6 -0
  105. package/dist/models/ps-scan-decoded-image.js +2 -0
  106. package/dist/models/ps-scan-levels.d.ts +4 -0
  107. package/dist/models/ps-scan-levels.js +2 -0
  108. package/dist/models/ps-scan-output-options.d.ts +7 -0
  109. package/dist/models/ps-scan-output-options.js +2 -0
  110. package/dist/models/ps-scan-page.d.ts +12 -0
  111. package/dist/models/ps-scan-page.js +2 -0
  112. package/dist/models/ps-scan-pdf-page.d.ts +5 -0
  113. package/dist/models/ps-scan-pdf-page.js +2 -0
  114. package/dist/models/ps-scan-prepared.d.ts +7 -0
  115. package/dist/models/ps-scan-prepared.js +2 -0
  116. package/dist/models/ps-scan-quad.d.ts +2 -0
  117. package/dist/models/ps-scan-quad.js +2 -0
  118. package/dist/models/ps-scan-raster.d.ts +5 -0
  119. package/dist/models/ps-scan-raster.js +2 -0
  120. package/dist/models/ps-scan-rendered-page.d.ts +5 -0
  121. package/dist/models/ps-scan-rendered-page.js +2 -0
  122. package/dist/services/ps-scan-camera-service.d.ts +10 -0
  123. package/dist/services/ps-scan-camera-service.js +54 -0
  124. package/dist/services/ps-scan-canvas-service.d.ts +8 -0
  125. package/dist/services/ps-scan-canvas-service.js +50 -0
  126. package/dist/services/ps-scan-edge-service.d.ts +9 -0
  127. package/dist/services/ps-scan-edge-service.js +160 -0
  128. package/dist/services/ps-scan-enhance-service.d.ts +9 -0
  129. package/dist/services/ps-scan-enhance-service.js +101 -0
  130. package/dist/services/ps-scan-file-name-service.d.ts +6 -0
  131. package/dist/services/ps-scan-file-name-service.js +29 -0
  132. package/dist/services/ps-scan-geometry-service.d.ts +18 -0
  133. package/dist/services/ps-scan-geometry-service.js +100 -0
  134. package/dist/services/ps-scan-homography-service.d.ts +7 -0
  135. package/dist/services/ps-scan-homography-service.js +50 -0
  136. package/dist/services/ps-scan-hull-service.d.ts +6 -0
  137. package/dist/services/ps-scan-hull-service.js +63 -0
  138. package/dist/services/ps-scan-image-filter-service.d.ts +6 -0
  139. package/dist/services/ps-scan-image-filter-service.js +56 -0
  140. package/dist/services/ps-scan-order-service.d.ts +4 -0
  141. package/dist/services/ps-scan-order-service.js +17 -0
  142. package/dist/services/ps-scan-output-service.d.ts +5 -0
  143. package/dist/services/ps-scan-output-service.js +28 -0
  144. package/dist/services/ps-scan-pdf-service.d.ts +5 -0
  145. package/dist/services/ps-scan-pdf-service.js +60 -0
  146. package/dist/services/ps-scan-pipeline-service.d.ts +10 -0
  147. package/dist/services/ps-scan-pipeline-service.js +44 -0
  148. package/dist/services/ps-scan-raster-service.d.ts +6 -0
  149. package/dist/services/ps-scan-raster-service.js +71 -0
  150. package/dist/services/ps-scan-template-service.d.ts +3 -0
  151. package/dist/services/ps-scan-template-service.js +9 -0
  152. package/dist/services/ps-scan-warp-service.d.ts +6 -0
  153. package/dist/services/ps-scan-warp-service.js +39 -0
  154. package/package.json +9 -6
@@ -0,0 +1,62 @@
1
+ import { useCallback, useEffect, useRef, useState } from 'react';
2
+ import { PsScanCameraStatus } from '../constants/ps-scan-camera-status';
3
+ import { PsScanConfig } from '../constants/ps-scan-config';
4
+ import { PsScanCameraService } from '../services/ps-scan-camera-service';
5
+ import { PsScanCanvasService } from '../services/ps-scan-canvas-service';
6
+ export const usePsScanCamera = (isActive) => {
7
+ const videoRef = useRef(null);
8
+ const sessionRef = useRef(null);
9
+ const [status, setStatus] = useState(PsScanCameraStatus.Starting);
10
+ const [canTorch, setCanTorch] = useState(false);
11
+ const [isTorchOn, setIsTorchOn] = useState(false);
12
+ useEffect(() => {
13
+ if (!isActive)
14
+ return;
15
+ let isCancelled = false;
16
+ setIsTorchOn(false);
17
+ if (!PsScanCameraService.isSupported()) {
18
+ setStatus(PsScanCameraStatus.Unavailable);
19
+ return;
20
+ }
21
+ setStatus(PsScanCameraStatus.Starting);
22
+ PsScanCameraService.open()
23
+ .then(async (session) => {
24
+ if (isCancelled) {
25
+ PsScanCameraService.stop(session.stream);
26
+ return;
27
+ }
28
+ sessionRef.current = session;
29
+ setCanTorch(session.canTorch);
30
+ const video = videoRef.current;
31
+ if (video !== null) {
32
+ video.srcObject = session.stream;
33
+ await video.play().catch(() => undefined);
34
+ }
35
+ setStatus(PsScanCameraStatus.Ready);
36
+ })
37
+ .catch((error) => {
38
+ if (!isCancelled)
39
+ setStatus(PsScanCameraService.classify(error));
40
+ });
41
+ return () => {
42
+ isCancelled = true;
43
+ PsScanCameraService.stop(sessionRef.current === null ? null : sessionRef.current.stream);
44
+ sessionRef.current = null;
45
+ if (videoRef.current !== null)
46
+ videoRef.current.srcObject = null;
47
+ };
48
+ }, [isActive]);
49
+ const toggleTorch = useCallback(async () => {
50
+ const session = sessionRef.current;
51
+ if (session === null)
52
+ return;
53
+ setIsTorchOn(await PsScanCameraService.setTorch(session.track, !isTorchOn));
54
+ }, [isTorchOn]);
55
+ const capture = useCallback(async () => {
56
+ const video = videoRef.current;
57
+ if (video === null || video.videoWidth === 0)
58
+ return null;
59
+ return await PsScanCanvasService.fromVideo(video, PsScanConfig.capturedJpegQuality);
60
+ }, []);
61
+ return { canTorch, capture, isTorchOn, status, toggleTorch, videoRef };
62
+ };
@@ -0,0 +1,62 @@
1
+ import { useCallback, useEffect, useRef, useState } from 'react';
2
+ import { PsScanFilter } from '../constants/ps-scan-filter';
3
+ import { PsScanOrderService } from '../services/ps-scan-order-service';
4
+ import { PsScanPipelineService } from '../services/ps-scan-pipeline-service';
5
+ export const usePsScanPages = () => {
6
+ const [pages, setPages] = useState([]);
7
+ const urls = useRef(new Set());
8
+ const counter = useRef(0);
9
+ useEffect(() => () => {
10
+ urls.current.forEach((url) => URL.revokeObjectURL(url));
11
+ urls.current.clear();
12
+ }, []);
13
+ const build = useCallback(async (source) => {
14
+ const prepared = await PsScanPipelineService.prepare(source);
15
+ const sourceUrl = URL.createObjectURL(source);
16
+ counter.current += 1;
17
+ urls.current.add(sourceUrl);
18
+ return {
19
+ isDetected: prepared.isDetected,
20
+ page: {
21
+ corners: prepared.corners,
22
+ filter: PsScanFilter.Document,
23
+ height: prepared.height,
24
+ id: `scan-page-${counter.current}`,
25
+ rotation: 0,
26
+ source,
27
+ sourceUrl,
28
+ width: prepared.width,
29
+ },
30
+ };
31
+ }, []);
32
+ const add = useCallback(async (sources) => {
33
+ const built = [];
34
+ for (const source of sources)
35
+ built.push(await build(source));
36
+ setPages((current) => [...current, ...built.map((item) => item.page)]);
37
+ return built;
38
+ }, [build]);
39
+ const replace = useCallback(async (id, source) => {
40
+ const built = await build(source);
41
+ setPages((current) => current.map((page) => {
42
+ if (page.id !== id)
43
+ return page;
44
+ URL.revokeObjectURL(page.sourceUrl);
45
+ urls.current.delete(page.sourceUrl);
46
+ return { ...built.page, filter: page.filter, id: page.id };
47
+ }));
48
+ return built;
49
+ }, [build]);
50
+ const update = useCallback((id, patch) => setPages((current) => current.map((page) => (page.id === id ? { ...page, ...patch } : page))), []);
51
+ const setCorners = useCallback((id, corners) => update(id, { corners }), [update]);
52
+ const remove = useCallback((id) => setPages((current) => {
53
+ const target = current.find((page) => page.id === id);
54
+ if (target !== undefined) {
55
+ URL.revokeObjectURL(target.sourceUrl);
56
+ urls.current.delete(target.sourceUrl);
57
+ }
58
+ return current.filter((page) => page.id !== id);
59
+ }), []);
60
+ const move = useCallback((from, to) => setPages((current) => PsScanOrderService.move(current, from, to)), []);
61
+ return { add, move, pages, remove, replace, setCorners, update };
62
+ };
@@ -0,0 +1,57 @@
1
+ import { useEffect, useRef, useState } from 'react';
2
+ import { PsScanConfig } from '../constants/ps-scan-config';
3
+ import { PsScanCanvasService } from '../services/ps-scan-canvas-service';
4
+ import { PsScanPipelineService } from '../services/ps-scan-pipeline-service';
5
+ export const usePsScanPreviews = (pages) => {
6
+ const rasters = useRef(new Map());
7
+ const urls = useRef(new Map());
8
+ const [previews, setPreviews] = useState({});
9
+ const signature = pages
10
+ .map((page) => `${page.id}:${page.filter}:${page.rotation}:${page.corners.map((point) => `${point.x.toFixed(4)},${point.y.toFixed(4)}`).join(';')}`)
11
+ .join('|');
12
+ useEffect(() => {
13
+ let isCancelled = false;
14
+ const timer = setTimeout(async () => {
15
+ const stale = [];
16
+ for (const page of pages) {
17
+ if (isCancelled)
18
+ return;
19
+ let raster = rasters.current.get(page.id);
20
+ if (raster === undefined) {
21
+ raster = await PsScanPipelineService.measure(page.source);
22
+ rasters.current.set(page.id, raster);
23
+ }
24
+ const processed = PsScanPipelineService.process(page, raster, PsScanConfig.previewMaxDimension);
25
+ const blob = await PsScanCanvasService.encode(processed, PsScanConfig.defaultJpegQuality);
26
+ if (isCancelled)
27
+ return;
28
+ const previous = urls.current.get(page.id);
29
+ if (previous !== undefined)
30
+ stale.push(previous);
31
+ urls.current.set(page.id, URL.createObjectURL(blob));
32
+ }
33
+ if (isCancelled)
34
+ return;
35
+ const ids = new Set(pages.map((page) => page.id));
36
+ urls.current.forEach((url, id) => {
37
+ if (ids.has(id))
38
+ return;
39
+ stale.push(url);
40
+ urls.current.delete(id);
41
+ rasters.current.delete(id);
42
+ });
43
+ setPreviews(Object.fromEntries(urls.current));
44
+ setTimeout(() => stale.forEach((url) => URL.revokeObjectURL(url)), 1000);
45
+ }, PsScanConfig.previewDebounceMs);
46
+ return () => {
47
+ isCancelled = true;
48
+ clearTimeout(timer);
49
+ };
50
+ }, [signature]);
51
+ useEffect(() => () => {
52
+ urls.current.forEach((url) => URL.revokeObjectURL(url));
53
+ urls.current.clear();
54
+ rasters.current.clear();
55
+ }, []);
56
+ return previews;
57
+ };
package/dist/esm/index.js CHANGED
@@ -5,6 +5,7 @@ export * from './components/ps-file-list';
5
5
  export * from './components/ps-file-picker';
6
6
  export * from './components/ps-file-viewer-button';
7
7
  export * from './components/ps-file-viewer-content';
8
+ export * from './components/ps-file-viewer-pane';
8
9
  export * from './components/ps-file-viewer';
9
10
  export * from './components/ps-highlighted-text';
10
11
  export * from './components/ps-image-viewer';
@@ -39,6 +40,7 @@ export * from './models/ps-file-selection';
39
40
  export * from './models/ps-file-text-state';
40
41
  export * from './models/ps-file-viewer-button-tone';
41
42
  export * from './models/ps-file-viewer-content-props';
43
+ export * from './models/ps-file-viewer-pane-props';
42
44
  export * from './models/ps-label-set';
43
45
  export * from './models/ps-page-size';
44
46
  export * from './models/ps-pdf-match';
@@ -67,3 +69,8 @@ export * from './hooks/use-ps-active-match-scroll';
67
69
  export * from './hooks/use-ps-file-text';
68
70
  export * from './hooks/use-ps-in-view';
69
71
  export * from './hooks/use-ps-upload-queue';
72
+ export * from './components/ps-document-scanner';
73
+ export * from './constants/ps-document-scanner-labels';
74
+ export * from './constants/ps-scan-filter';
75
+ export * from './constants/ps-scan-output-format';
76
+ export * from './models/ps-document-scanner-props';
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,50 @@
1
+ import { PsScanCameraStatus } from '../constants/ps-scan-camera-status';
2
+ import { PsScanConfig } from '../constants/ps-scan-config';
3
+ export class PsScanCameraService {
4
+ static classify(error) {
5
+ const name = typeof error === 'object' && error !== null && 'name' in error ? String(error.name) : '';
6
+ return name === 'NotAllowedError' ||
7
+ name === 'SecurityError' ||
8
+ name === 'PermissionDeniedError'
9
+ ? PsScanCameraStatus.Denied
10
+ : PsScanCameraStatus.Unavailable;
11
+ }
12
+ static isSupported() {
13
+ return (typeof navigator !== 'undefined' &&
14
+ typeof navigator.mediaDevices !== 'undefined' &&
15
+ typeof navigator.mediaDevices.getUserMedia === 'function');
16
+ }
17
+ static async open() {
18
+ const stream = await navigator.mediaDevices.getUserMedia({
19
+ audio: false,
20
+ video: {
21
+ facingMode: { ideal: 'environment' },
22
+ height: { ideal: 1440 },
23
+ width: { ideal: 1920 },
24
+ },
25
+ });
26
+ const track = stream.getVideoTracks()[0];
27
+ return { canTorch: PsScanCameraService.supportsTorch(track), stream, track };
28
+ }
29
+ static async setTorch(track, isOn) {
30
+ try {
31
+ await track.applyConstraints({
32
+ advanced: [{ [PsScanConfig.torchConstraint]: isOn }],
33
+ });
34
+ return isOn;
35
+ }
36
+ catch {
37
+ return false;
38
+ }
39
+ }
40
+ static stop(stream) {
41
+ if (stream === null)
42
+ return;
43
+ stream.getTracks().forEach((track) => track.stop());
44
+ }
45
+ static supportsTorch(track) {
46
+ if (track === undefined || typeof track.getCapabilities !== 'function')
47
+ return false;
48
+ return PsScanConfig.torchConstraint in track.getCapabilities();
49
+ }
50
+ }
@@ -0,0 +1,46 @@
1
+ import { PsScanGeometryService } from './ps-scan-geometry-service';
2
+ export class PsScanCanvasService {
3
+ static async decode(blob, maxDimension) {
4
+ const bitmap = await createImageBitmap(blob, { imageOrientation: 'from-image' });
5
+ const size = PsScanGeometryService.fitWithin({ height: bitmap.height, width: bitmap.width }, maxDimension);
6
+ const context = PsScanCanvasService.context(size.width, size.height);
7
+ context.drawImage(bitmap, 0, 0, size.width, size.height);
8
+ const naturalHeight = bitmap.height;
9
+ const naturalWidth = bitmap.width;
10
+ bitmap.close();
11
+ return {
12
+ naturalHeight,
13
+ naturalWidth,
14
+ raster: context.getImageData(0, 0, size.width, size.height),
15
+ };
16
+ }
17
+ static async encode(raster, quality) {
18
+ const context = PsScanCanvasService.context(raster.width, raster.height);
19
+ context.putImageData(new ImageData(raster.data, raster.width, raster.height), 0, 0);
20
+ const canvas = context.canvas;
21
+ if (canvas instanceof HTMLCanvasElement)
22
+ return await new Promise((resolve, reject) => canvas.toBlob((blob) => (blob === null ? reject(new Error('encode')) : resolve(blob)), 'image/jpeg', quality));
23
+ return await canvas.convertToBlob({ quality, type: 'image/jpeg' });
24
+ }
25
+ static async fromVideo(video, quality) {
26
+ const context = PsScanCanvasService.context(video.videoWidth, video.videoHeight);
27
+ context.drawImage(video, 0, 0);
28
+ return await PsScanCanvasService.encode(context.getImageData(0, 0, video.videoWidth, video.videoHeight), quality);
29
+ }
30
+ static context(width, height) {
31
+ if (typeof OffscreenCanvas !== 'undefined') {
32
+ const context = new OffscreenCanvas(width, height).getContext('2d', {
33
+ willReadFrequently: true,
34
+ });
35
+ if (context !== null)
36
+ return context;
37
+ }
38
+ const canvas = document.createElement('canvas');
39
+ canvas.width = width;
40
+ canvas.height = height;
41
+ const context = canvas.getContext('2d', { willReadFrequently: true });
42
+ if (context === null)
43
+ throw new Error('canvas');
44
+ return context;
45
+ }
46
+ }
@@ -0,0 +1,156 @@
1
+ import { PsScanConfig } from '../constants/ps-scan-config';
2
+ import { PsScanGeometryService } from './ps-scan-geometry-service';
3
+ import { PsScanHullService } from './ps-scan-hull-service';
4
+ import { PsScanImageFilterService } from './ps-scan-image-filter-service';
5
+ import { PsScanRasterService } from './ps-scan-raster-service';
6
+ 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));
11
+ const raster = PsScanRasterService.resize(source, width, height);
12
+ 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)
20
+ return null;
21
+ const hull = PsScanHullService.convexHull(component);
22
+ if (hull.length < 4)
23
+ 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)
29
+ return null;
30
+ if (quadArea / imageArea > PsScanConfig.detectMaxAreaRatio)
31
+ return null;
32
+ if (hullArea > 0 && quadArea / hullArea < PsScanConfig.detectMinHullFill)
33
+ return null;
34
+ if (!PsScanGeometryService.isConvex(quad))
35
+ return null;
36
+ return quad.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;
137
+ }
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));
155
+ }
156
+ }
@@ -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
+ }