@pallasoft/ps-file-components 1.2.4 → 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 (160) 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-picker.d.ts +2 -1
  8. package/dist/components/ps-file-picker.js +2 -2
  9. package/dist/components/ps-file-viewer-pane.d.ts +2 -0
  10. package/dist/components/ps-file-viewer-pane.js +39 -0
  11. package/dist/components/ps-scan-camera-view.d.ts +17 -0
  12. package/dist/components/ps-scan-camera-view.js +27 -0
  13. package/dist/components/ps-scan-crop-editor.d.ts +13 -0
  14. package/dist/components/ps-scan-crop-editor.js +53 -0
  15. package/dist/components/ps-scan-icon-button.d.ts +10 -0
  16. package/dist/components/ps-scan-icon-button.js +7 -0
  17. package/dist/components/ps-scan-page-list.d.ts +12 -0
  18. package/dist/components/ps-scan-page-list.js +23 -0
  19. package/dist/components/ps-scan-review.d.ts +23 -0
  20. package/dist/components/ps-scan-review.js +20 -0
  21. package/dist/constants/ps-document-scanner-labels.d.ts +50 -0
  22. package/dist/constants/ps-document-scanner-labels.js +53 -0
  23. package/dist/constants/ps-file-picker-labels.d.ts +1 -0
  24. package/dist/constants/ps-file-picker-labels.js +1 -0
  25. package/dist/constants/ps-scan-camera-status.d.ts +7 -0
  26. package/dist/constants/ps-scan-camera-status.js +9 -0
  27. package/dist/constants/ps-scan-config.d.ts +31 -0
  28. package/dist/constants/ps-scan-config.js +34 -0
  29. package/dist/constants/ps-scan-corner-order.d.ts +1 -0
  30. package/dist/constants/ps-scan-corner-order.js +4 -0
  31. package/dist/constants/ps-scan-filter.d.ts +7 -0
  32. package/dist/constants/ps-scan-filter.js +9 -0
  33. package/dist/constants/ps-scan-output-format.d.ts +5 -0
  34. package/dist/constants/ps-scan-output-format.js +7 -0
  35. package/dist/constants/ps-scan-step.d.ts +6 -0
  36. package/dist/constants/ps-scan-step.js +8 -0
  37. package/dist/esm/components/ps-document-scanner-loader.js +4 -0
  38. package/dist/esm/components/ps-document-scanner-sheet.js +134 -0
  39. package/dist/esm/components/ps-document-scanner.js +13 -0
  40. package/dist/esm/components/ps-file-picker.js +3 -3
  41. package/dist/esm/components/ps-file-viewer-pane.js +35 -0
  42. package/dist/esm/components/ps-scan-camera-view.js +23 -0
  43. package/dist/esm/components/ps-scan-crop-editor.js +49 -0
  44. package/dist/esm/components/ps-scan-icon-button.js +3 -0
  45. package/dist/esm/components/ps-scan-page-list.js +19 -0
  46. package/dist/esm/components/ps-scan-review.js +16 -0
  47. package/dist/esm/constants/ps-document-scanner-labels.js +50 -0
  48. package/dist/esm/constants/ps-file-picker-labels.js +1 -0
  49. package/dist/esm/constants/ps-scan-camera-status.js +6 -0
  50. package/dist/esm/constants/ps-scan-config.js +31 -0
  51. package/dist/esm/constants/ps-scan-corner-order.js +1 -0
  52. package/dist/esm/constants/ps-scan-filter.js +6 -0
  53. package/dist/esm/constants/ps-scan-output-format.js +4 -0
  54. package/dist/esm/constants/ps-scan-step.js +5 -0
  55. package/dist/esm/hooks/use-ps-focus-trap.js +45 -0
  56. package/dist/esm/hooks/use-ps-scan-camera.js +62 -0
  57. package/dist/esm/hooks/use-ps-scan-pages.js +62 -0
  58. package/dist/esm/hooks/use-ps-scan-previews.js +57 -0
  59. package/dist/esm/index.js +7 -0
  60. package/dist/esm/models/ps-document-scanner-props.js +1 -0
  61. package/dist/esm/models/ps-document-scanner-sheet-props.js +1 -0
  62. package/dist/esm/models/ps-file-viewer-pane-props.js +1 -0
  63. package/dist/esm/models/ps-scan-added-page.js +1 -0
  64. package/dist/esm/models/ps-scan-camera-session.js +1 -0
  65. package/dist/esm/models/ps-scan-decoded-image.js +1 -0
  66. package/dist/esm/models/ps-scan-levels.js +1 -0
  67. package/dist/esm/models/ps-scan-output-options.js +1 -0
  68. package/dist/esm/models/ps-scan-page.js +1 -0
  69. package/dist/esm/models/ps-scan-pdf-page.js +1 -0
  70. package/dist/esm/models/ps-scan-prepared.js +1 -0
  71. package/dist/esm/models/ps-scan-quad.js +1 -0
  72. package/dist/esm/models/ps-scan-raster.js +1 -0
  73. package/dist/esm/models/ps-scan-rendered-page.js +1 -0
  74. package/dist/esm/services/ps-scan-camera-service.js +50 -0
  75. package/dist/esm/services/ps-scan-canvas-service.js +46 -0
  76. package/dist/esm/services/ps-scan-edge-service.js +156 -0
  77. package/dist/esm/services/ps-scan-enhance-service.js +97 -0
  78. package/dist/esm/services/ps-scan-file-name-service.js +25 -0
  79. package/dist/esm/services/ps-scan-geometry-service.js +96 -0
  80. package/dist/esm/services/ps-scan-homography-service.js +46 -0
  81. package/dist/esm/services/ps-scan-hull-service.js +59 -0
  82. package/dist/esm/services/ps-scan-image-filter-service.js +52 -0
  83. package/dist/esm/services/ps-scan-order-service.js +13 -0
  84. package/dist/esm/services/ps-scan-output-service.js +24 -0
  85. package/dist/esm/services/ps-scan-pdf-service.js +56 -0
  86. package/dist/esm/services/ps-scan-pipeline-service.js +40 -0
  87. package/dist/esm/services/ps-scan-raster-service.js +67 -0
  88. package/dist/esm/services/ps-scan-template-service.js +5 -0
  89. package/dist/esm/services/ps-scan-warp-service.js +35 -0
  90. package/dist/hooks/use-ps-focus-trap.d.ts +2 -0
  91. package/dist/hooks/use-ps-focus-trap.js +49 -0
  92. package/dist/hooks/use-ps-scan-camera.d.ts +9 -0
  93. package/dist/hooks/use-ps-scan-camera.js +66 -0
  94. package/dist/hooks/use-ps-scan-pages.d.ts +12 -0
  95. package/dist/hooks/use-ps-scan-pages.js +66 -0
  96. package/dist/hooks/use-ps-scan-previews.d.ts +2 -0
  97. package/dist/hooks/use-ps-scan-previews.js +61 -0
  98. package/dist/index.d.ts +7 -0
  99. package/dist/index.js +7 -0
  100. package/dist/models/ps-document-scanner-props.d.ts +15 -0
  101. package/dist/models/ps-document-scanner-props.js +2 -0
  102. package/dist/models/ps-document-scanner-sheet-props.d.ts +13 -0
  103. package/dist/models/ps-document-scanner-sheet-props.js +2 -0
  104. package/dist/models/ps-file-viewer-pane-props.d.ts +12 -0
  105. package/dist/models/ps-file-viewer-pane-props.js +2 -0
  106. package/dist/models/ps-scan-added-page.d.ts +5 -0
  107. package/dist/models/ps-scan-added-page.js +2 -0
  108. package/dist/models/ps-scan-camera-session.d.ts +5 -0
  109. package/dist/models/ps-scan-camera-session.js +2 -0
  110. package/dist/models/ps-scan-decoded-image.d.ts +6 -0
  111. package/dist/models/ps-scan-decoded-image.js +2 -0
  112. package/dist/models/ps-scan-levels.d.ts +4 -0
  113. package/dist/models/ps-scan-levels.js +2 -0
  114. package/dist/models/ps-scan-output-options.d.ts +7 -0
  115. package/dist/models/ps-scan-output-options.js +2 -0
  116. package/dist/models/ps-scan-page.d.ts +12 -0
  117. package/dist/models/ps-scan-page.js +2 -0
  118. package/dist/models/ps-scan-pdf-page.d.ts +5 -0
  119. package/dist/models/ps-scan-pdf-page.js +2 -0
  120. package/dist/models/ps-scan-prepared.d.ts +7 -0
  121. package/dist/models/ps-scan-prepared.js +2 -0
  122. package/dist/models/ps-scan-quad.d.ts +2 -0
  123. package/dist/models/ps-scan-quad.js +2 -0
  124. package/dist/models/ps-scan-raster.d.ts +5 -0
  125. package/dist/models/ps-scan-raster.js +2 -0
  126. package/dist/models/ps-scan-rendered-page.d.ts +5 -0
  127. package/dist/models/ps-scan-rendered-page.js +2 -0
  128. package/dist/services/ps-scan-camera-service.d.ts +10 -0
  129. package/dist/services/ps-scan-camera-service.js +54 -0
  130. package/dist/services/ps-scan-canvas-service.d.ts +8 -0
  131. package/dist/services/ps-scan-canvas-service.js +50 -0
  132. package/dist/services/ps-scan-edge-service.d.ts +9 -0
  133. package/dist/services/ps-scan-edge-service.js +160 -0
  134. package/dist/services/ps-scan-enhance-service.d.ts +9 -0
  135. package/dist/services/ps-scan-enhance-service.js +101 -0
  136. package/dist/services/ps-scan-file-name-service.d.ts +6 -0
  137. package/dist/services/ps-scan-file-name-service.js +29 -0
  138. package/dist/services/ps-scan-geometry-service.d.ts +18 -0
  139. package/dist/services/ps-scan-geometry-service.js +100 -0
  140. package/dist/services/ps-scan-homography-service.d.ts +7 -0
  141. package/dist/services/ps-scan-homography-service.js +50 -0
  142. package/dist/services/ps-scan-hull-service.d.ts +6 -0
  143. package/dist/services/ps-scan-hull-service.js +63 -0
  144. package/dist/services/ps-scan-image-filter-service.d.ts +6 -0
  145. package/dist/services/ps-scan-image-filter-service.js +56 -0
  146. package/dist/services/ps-scan-order-service.d.ts +4 -0
  147. package/dist/services/ps-scan-order-service.js +17 -0
  148. package/dist/services/ps-scan-output-service.d.ts +5 -0
  149. package/dist/services/ps-scan-output-service.js +28 -0
  150. package/dist/services/ps-scan-pdf-service.d.ts +5 -0
  151. package/dist/services/ps-scan-pdf-service.js +60 -0
  152. package/dist/services/ps-scan-pipeline-service.d.ts +10 -0
  153. package/dist/services/ps-scan-pipeline-service.js +44 -0
  154. package/dist/services/ps-scan-raster-service.d.ts +6 -0
  155. package/dist/services/ps-scan-raster-service.js +71 -0
  156. package/dist/services/ps-scan-template-service.d.ts +3 -0
  157. package/dist/services/ps-scan-template-service.js +9 -0
  158. package/dist/services/ps-scan-warp-service.d.ts +6 -0
  159. package/dist/services/ps-scan-warp-service.js +39 -0
  160. package/package.json +9 -6
@@ -1,5 +1,6 @@
1
1
  export const PsFilePickerLabels = {
2
2
  browse: 'or click to browse your files',
3
+ browseTouch: 'or tap to choose from your device',
3
4
  dropActive: 'Drop to upload',
4
5
  formats: 'Accepted formats:',
5
6
  invalidType: 'is not a supported file type.',
@@ -0,0 +1,6 @@
1
+ export const PsScanCameraStatus = {
2
+ Denied: 'denied',
3
+ Ready: 'ready',
4
+ Starting: 'starting',
5
+ Unavailable: 'unavailable',
6
+ };
@@ -0,0 +1,31 @@
1
+ export const PsScanConfig = {
2
+ adaptiveRatio: 0.92,
3
+ adaptiveWindowDivisor: 16,
4
+ adaptiveWindowMin: 15,
5
+ capturedJpegQuality: 0.92,
6
+ cornerNudge: 0.005,
7
+ cornerNudgeLarge: 0.025,
8
+ decodeMaxDimension: 3000,
9
+ defaultFileName: 'scan',
10
+ defaultJpegQuality: 0.82,
11
+ defaultMaxDimension: 2000,
12
+ defaultMaxPages: 30,
13
+ detectDilations: 2,
14
+ detectMaxDimension: 320,
15
+ detectMinAreaRatio: 0.12,
16
+ detectMaxAreaRatio: 0.995,
17
+ detectMinEdgeStrength: 24,
18
+ detectMinHullFill: 0.85,
19
+ detectPercentile: 0.98,
20
+ detectRelativeStrength: 0.3,
21
+ hullCandidateLimit: 18,
22
+ levelHighPercentile: 0.99,
23
+ levelLowPercentile: 0.01,
24
+ minCornerGap: 0.02,
25
+ pdfDpi: 150,
26
+ pdfPointsPerInch: 72,
27
+ previewDebounceMs: 120,
28
+ previewMaxDimension: 1400,
29
+ thumbnailHeight: 56,
30
+ torchConstraint: 'torch',
31
+ };
@@ -0,0 +1 @@
1
+ export const PsScanCornerOrder = ['topLeft', 'topRight', 'bottomRight', 'bottomLeft'];
@@ -0,0 +1,6 @@
1
+ export const PsScanFilter = {
2
+ Colour: 'colour',
3
+ Document: 'document',
4
+ Greyscale: 'greyscale',
5
+ Original: 'original',
6
+ };
@@ -0,0 +1,4 @@
1
+ export const PsScanOutputFormat = {
2
+ Jpeg: 'jpeg',
3
+ Pdf: 'pdf',
4
+ };
@@ -0,0 +1,5 @@
1
+ export const PsScanStep = {
2
+ Camera: 'camera',
3
+ Crop: 'crop',
4
+ Review: 'review',
5
+ };
@@ -0,0 +1,45 @@
1
+ import { useEffect } from 'react';
2
+ export const usePsFocusTrap = (ref, isActive, onEscape) => {
3
+ useEffect(() => {
4
+ if (!isActive || ref.current === null)
5
+ return;
6
+ const dialog = ref.current.closest('[role="dialog"]');
7
+ const root = dialog === null ? ref.current : dialog;
8
+ const focusable = () => Array.from(root.querySelectorAll('button:not([disabled]), [href], input:not([disabled]), [tabindex]:not([tabindex="-1"])')).filter((element) => element.offsetParent !== null || element === document.activeElement);
9
+ if (!root.contains(document.activeElement)) {
10
+ root.setAttribute('tabindex', '-1');
11
+ root.focus({ preventScroll: true });
12
+ }
13
+ const onKeyDown = (event) => {
14
+ if (event.key === 'Escape') {
15
+ event.stopPropagation();
16
+ onEscape();
17
+ return;
18
+ }
19
+ if (event.key !== 'Tab')
20
+ return;
21
+ const items = focusable();
22
+ if (items.length === 0) {
23
+ event.preventDefault();
24
+ return;
25
+ }
26
+ const first = items[0];
27
+ const last = items[items.length - 1];
28
+ const current = document.activeElement;
29
+ if (!root.contains(current)) {
30
+ event.preventDefault();
31
+ first.focus();
32
+ }
33
+ else if (event.shiftKey && (current === first || current === root)) {
34
+ event.preventDefault();
35
+ last.focus();
36
+ }
37
+ else if (!event.shiftKey && current === last) {
38
+ event.preventDefault();
39
+ first.focus();
40
+ }
41
+ };
42
+ document.addEventListener('keydown', onKeyDown, true);
43
+ return () => document.removeEventListener('keydown', onKeyDown, true);
44
+ }, [isActive, onEscape]);
45
+ };
@@ -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
+ }