@pallasoft/ps-file-components 1.4.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) 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 +69 -21
  4. package/dist/components/ps-document-scanner.d.ts +1 -1
  5. package/dist/components/ps-document-scanner.js +2 -2
  6. package/dist/components/ps-file-picker-actions.d.ts +10 -0
  7. package/dist/components/ps-file-picker-actions.js +21 -0
  8. package/dist/components/ps-file-picker.d.ts +7 -1
  9. package/dist/components/ps-file-picker.js +30 -6
  10. package/dist/components/ps-scan-button.d.ts +13 -0
  11. package/dist/components/ps-scan-button.js +9 -0
  12. package/dist/components/ps-scan-camera-view.d.ts +17 -7
  13. package/dist/components/ps-scan-camera-view.js +23 -16
  14. package/dist/components/ps-scan-crop-editor.d.ts +2 -2
  15. package/dist/components/ps-scan-crop-editor.js +19 -3
  16. package/dist/components/ps-scan-denied-steps.d.ts +6 -0
  17. package/dist/components/ps-scan-denied-steps.js +33 -0
  18. package/dist/components/ps-scan-icon-button.d.ts +4 -3
  19. package/dist/components/ps-scan-icon-button.js +3 -1
  20. package/dist/components/ps-scan-illustration.d.ts +1 -0
  21. package/dist/components/ps-scan-illustration.js +6 -0
  22. package/dist/components/ps-scan-loupe.d.ts +10 -0
  23. package/dist/components/ps-scan-loupe.js +31 -0
  24. package/dist/components/ps-scan-page-list.d.ts +3 -1
  25. package/dist/components/ps-scan-page-list.js +7 -8
  26. package/dist/components/ps-scan-page-stack.d.ts +7 -0
  27. package/dist/components/ps-scan-page-stack.js +6 -0
  28. package/dist/components/ps-scan-permission-view.d.ts +11 -0
  29. package/dist/components/ps-scan-permission-view.js +48 -0
  30. package/dist/components/ps-scan-photo-button.d.ts +8 -0
  31. package/dist/components/ps-scan-photo-button.js +20 -0
  32. package/dist/components/ps-scan-review.d.ts +2 -1
  33. package/dist/components/ps-scan-review.js +21 -4
  34. package/dist/components/ps-scan-shutter.d.ts +7 -0
  35. package/dist/components/ps-scan-shutter.js +6 -0
  36. package/dist/components/ps-scan-viewfinder-overlay.d.ts +8 -0
  37. package/dist/components/ps-scan-viewfinder-overlay.js +28 -0
  38. package/dist/components/ps-upload-row.js +1 -1
  39. package/dist/constants/ps-document-scanner-labels.d.ts +49 -19
  40. package/dist/constants/ps-document-scanner-labels.js +49 -19
  41. package/dist/constants/ps-file-picker-labels.d.ts +5 -3
  42. package/dist/constants/ps-file-picker-labels.js +5 -3
  43. package/dist/constants/ps-scan-camera-status.d.ts +4 -0
  44. package/dist/constants/ps-scan-camera-status.js +4 -0
  45. package/dist/constants/ps-scan-chrome-classes.d.ts +12 -0
  46. package/dist/constants/ps-scan-chrome-classes.js +15 -0
  47. package/dist/constants/ps-scan-config.d.ts +18 -1
  48. package/dist/constants/ps-scan-config.js +18 -1
  49. package/dist/constants/ps-scan-platform.d.ts +6 -0
  50. package/dist/constants/ps-scan-platform.js +8 -0
  51. package/dist/esm/components/ps-document-scanner-sheet.js +71 -23
  52. package/dist/esm/components/ps-document-scanner.js +2 -2
  53. package/dist/esm/components/ps-file-picker-actions.js +17 -0
  54. package/dist/esm/components/ps-file-picker.js +31 -7
  55. package/dist/esm/components/ps-scan-button.js +5 -0
  56. package/dist/esm/components/ps-scan-camera-view.js +26 -19
  57. package/dist/esm/components/ps-scan-crop-editor.js +21 -5
  58. package/dist/esm/components/ps-scan-denied-steps.js +29 -0
  59. package/dist/esm/components/ps-scan-icon-button.js +3 -1
  60. package/dist/esm/components/ps-scan-illustration.js +2 -0
  61. package/dist/esm/components/ps-scan-loupe.js +27 -0
  62. package/dist/esm/components/ps-scan-page-list.js +7 -8
  63. package/dist/esm/components/ps-scan-page-stack.js +2 -0
  64. package/dist/esm/components/ps-scan-permission-view.js +44 -0
  65. package/dist/esm/components/ps-scan-photo-button.js +16 -0
  66. package/dist/esm/components/ps-scan-review.js +21 -4
  67. package/dist/esm/components/ps-scan-shutter.js +2 -0
  68. package/dist/esm/components/ps-scan-viewfinder-overlay.js +24 -0
  69. package/dist/esm/components/ps-upload-row.js +1 -1
  70. package/dist/esm/constants/ps-document-scanner-labels.js +49 -19
  71. package/dist/esm/constants/ps-file-picker-labels.js +5 -3
  72. package/dist/esm/constants/ps-scan-camera-status.js +4 -0
  73. package/dist/esm/constants/ps-scan-chrome-classes.js +12 -0
  74. package/dist/esm/constants/ps-scan-config.js +18 -1
  75. package/dist/esm/constants/ps-scan-platform.js +5 -0
  76. package/dist/esm/hooks/use-ps-scan-camera.js +52 -15
  77. package/dist/esm/hooks/use-ps-scan-live-detection.js +45 -0
  78. package/dist/esm/index.js +1 -0
  79. package/dist/esm/models/ps-file-picker-scan.js +1 -0
  80. package/dist/esm/models/ps-scan-live-detection.js +1 -0
  81. package/dist/esm/models/ps-scan-stability.js +1 -0
  82. package/dist/esm/services/ps-scan-camera-service.js +50 -5
  83. package/dist/esm/services/ps-scan-canvas-service.js +6 -0
  84. package/dist/esm/services/ps-scan-edge-service.js +1 -1
  85. package/dist/esm/services/ps-scan-geometry-service.js +20 -0
  86. package/dist/esm/services/ps-scan-platform-service.js +15 -0
  87. package/dist/esm/services/ps-scan-stability-service.js +41 -0
  88. package/dist/hooks/use-ps-scan-camera.d.ts +3 -1
  89. package/dist/hooks/use-ps-scan-camera.js +52 -15
  90. package/dist/hooks/use-ps-scan-live-detection.d.ts +3 -0
  91. package/dist/hooks/use-ps-scan-live-detection.js +49 -0
  92. package/dist/index.d.ts +1 -0
  93. package/dist/index.js +1 -0
  94. package/dist/models/ps-document-scanner-props.d.ts +1 -0
  95. package/dist/models/ps-document-scanner-sheet-props.d.ts +1 -0
  96. package/dist/models/ps-file-picker-scan.d.ts +4 -0
  97. package/dist/models/ps-file-picker-scan.js +2 -0
  98. package/dist/models/ps-scan-live-detection.d.ts +5 -0
  99. package/dist/models/ps-scan-live-detection.js +2 -0
  100. package/dist/models/ps-scan-stability.d.ts +8 -0
  101. package/dist/models/ps-scan-stability.js +2 -0
  102. package/dist/services/ps-scan-camera-service.d.ts +4 -0
  103. package/dist/services/ps-scan-camera-service.js +50 -5
  104. package/dist/services/ps-scan-canvas-service.d.ts +1 -0
  105. package/dist/services/ps-scan-canvas-service.js +6 -0
  106. package/dist/services/ps-scan-edge-service.js +1 -1
  107. package/dist/services/ps-scan-geometry-service.d.ts +2 -0
  108. package/dist/services/ps-scan-geometry-service.js +20 -0
  109. package/dist/services/ps-scan-platform-service.d.ts +4 -0
  110. package/dist/services/ps-scan-platform-service.js +19 -0
  111. package/dist/services/ps-scan-stability-service.d.ts +9 -0
  112. package/dist/services/ps-scan-stability-service.js +45 -0
  113. package/package.json +3 -3
@@ -6,18 +6,40 @@ import { PsScanCanvasService } from '../services/ps-scan-canvas-service';
6
6
  export const usePsScanCamera = (isActive) => {
7
7
  const videoRef = useRef(null);
8
8
  const sessionRef = useRef(null);
9
- const [status, setStatus] = useState(PsScanCameraStatus.Starting);
9
+ const [status, setStatus] = useState(PsScanCameraStatus.Checking);
10
+ const [request, setRequest] = useState(0);
10
11
  const [canTorch, setCanTorch] = useState(false);
11
12
  const [isTorchOn, setIsTorchOn] = useState(false);
12
13
  useEffect(() => {
13
- if (!isActive)
14
+ let isCancelled = false;
15
+ let permission = null;
16
+ const onChange = () => {
17
+ if (permission !== null && permission.state === 'granted')
18
+ setRequest((current) => current + 1);
19
+ };
20
+ PsScanCameraService.check().then(async (next) => {
21
+ if (isCancelled)
22
+ return;
23
+ if (next === PsScanCameraStatus.Starting)
24
+ setRequest(1);
25
+ else
26
+ setStatus(next);
27
+ permission = await PsScanCameraService.permission();
28
+ if (isCancelled || permission === null)
29
+ return;
30
+ permission.addEventListener('change', onChange);
31
+ });
32
+ return () => {
33
+ isCancelled = true;
34
+ if (permission !== null)
35
+ permission.removeEventListener('change', onChange);
36
+ };
37
+ }, []);
38
+ useEffect(() => {
39
+ if (!isActive || request === 0)
14
40
  return;
15
41
  let isCancelled = false;
16
42
  setIsTorchOn(false);
17
- if (!PsScanCameraService.isSupported()) {
18
- setStatus(PsScanCameraStatus.Unavailable);
19
- return;
20
- }
21
43
  setStatus(PsScanCameraStatus.Starting);
22
44
  PsScanCameraService.open()
23
45
  .then(async (session) => {
@@ -27,12 +49,10 @@ export const usePsScanCamera = (isActive) => {
27
49
  }
28
50
  sessionRef.current = session;
29
51
  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);
52
+ if (videoRef.current !== null)
53
+ await PsScanCameraService.attach(videoRef.current, session);
54
+ if (!isCancelled)
55
+ setStatus(PsScanCameraStatus.Ready);
36
56
  })
37
57
  .catch((error) => {
38
58
  if (!isCancelled)
@@ -45,7 +65,13 @@ export const usePsScanCamera = (isActive) => {
45
65
  if (videoRef.current !== null)
46
66
  videoRef.current.srcObject = null;
47
67
  };
48
- }, [isActive]);
68
+ }, [isActive, request]);
69
+ const attachVideo = useCallback((element) => {
70
+ videoRef.current = element;
71
+ if (element !== null && sessionRef.current !== null)
72
+ void PsScanCameraService.attach(element, sessionRef.current);
73
+ }, []);
74
+ const allow = useCallback(() => setRequest((current) => current + 1), []);
49
75
  const toggleTorch = useCallback(async () => {
50
76
  const session = sessionRef.current;
51
77
  if (session === null)
@@ -54,9 +80,20 @@ export const usePsScanCamera = (isActive) => {
54
80
  }, [isTorchOn]);
55
81
  const capture = useCallback(async () => {
56
82
  const video = videoRef.current;
57
- if (video === null || video.videoWidth === 0)
83
+ if (video === null ||
84
+ video.videoWidth === 0 ||
85
+ video.readyState < HTMLMediaElement.HAVE_CURRENT_DATA)
58
86
  return null;
59
87
  return await PsScanCanvasService.fromVideo(video, PsScanConfig.capturedJpegQuality);
60
88
  }, []);
61
- return { canTorch, capture, isTorchOn, status, toggleTorch, videoRef };
89
+ return {
90
+ allow,
91
+ attachVideo,
92
+ canTorch,
93
+ capture,
94
+ isTorchOn,
95
+ status,
96
+ toggleTorch,
97
+ videoRef,
98
+ };
62
99
  };
@@ -0,0 +1,45 @@
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 { PsScanEdgeService } from '../services/ps-scan-edge-service';
5
+ import { PsScanStabilityService } from '../services/ps-scan-stability-service';
6
+ export const usePsScanLiveDetection = (videoRef, isActive, isAuto, onSteady) => {
7
+ const [detection, setDetection] = useState({ progress: 0, quad: null });
8
+ const isAutoRef = useRef(isAuto);
9
+ const onSteadyRef = useRef(onSteady);
10
+ isAutoRef.current = isAuto;
11
+ onSteadyRef.current = onSteady;
12
+ useEffect(() => {
13
+ if (!isActive) {
14
+ setDetection({ progress: 0, quad: null });
15
+ return;
16
+ }
17
+ let state = PsScanStabilityService.initial();
18
+ let last = performance.now();
19
+ let timer;
20
+ const tick = () => {
21
+ const video = videoRef.current;
22
+ const now = performance.now();
23
+ if (video !== null && video.videoWidth > 0 && document.visibilityState === 'visible') {
24
+ try {
25
+ const raster = PsScanCanvasService.sample(video, PsScanConfig.liveDetectMaxDimension);
26
+ state = PsScanStabilityService.next(state, PsScanEdgeService.detect(raster), now - last);
27
+ }
28
+ catch {
29
+ state = PsScanStabilityService.initial();
30
+ }
31
+ const progress = isAutoRef.current ? PsScanStabilityService.progress(state) : 0;
32
+ setDetection({ progress, quad: state.quad });
33
+ if (progress >= 1) {
34
+ state = PsScanStabilityService.disarm(state);
35
+ onSteadyRef.current();
36
+ }
37
+ }
38
+ last = now;
39
+ timer = setTimeout(tick, PsScanConfig.liveDetectIntervalMs);
40
+ };
41
+ timer = setTimeout(tick, PsScanConfig.liveDetectIntervalMs);
42
+ return () => clearTimeout(timer);
43
+ }, [isActive]);
44
+ return detection;
45
+ };
package/dist/esm/index.js CHANGED
@@ -74,3 +74,4 @@ export * from './constants/ps-document-scanner-labels';
74
74
  export * from './constants/ps-scan-filter';
75
75
  export * from './constants/ps-scan-output-format';
76
76
  export * from './models/ps-document-scanner-props';
77
+ export * from './models/ps-file-picker-scan';
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -1,13 +1,45 @@
1
1
  import { PsScanCameraStatus } from '../constants/ps-scan-camera-status';
2
2
  import { PsScanConfig } from '../constants/ps-scan-config';
3
3
  export class PsScanCameraService {
4
+ static async attach(video, session) {
5
+ if (video.srcObject !== session.stream)
6
+ video.srcObject = session.stream;
7
+ await video.play().catch(() => undefined);
8
+ }
9
+ static async check() {
10
+ if (globalThis.isSecureContext === false)
11
+ return PsScanCameraStatus.Insecure;
12
+ if (!PsScanCameraService.isSupported())
13
+ return PsScanCameraStatus.Unavailable;
14
+ if (!(await PsScanCameraService.hasCamera()))
15
+ return PsScanCameraStatus.Unavailable;
16
+ const permission = await PsScanCameraService.permission();
17
+ if (permission === null)
18
+ return PsScanCameraStatus.Prompt;
19
+ if (permission.state === 'granted')
20
+ return PsScanCameraStatus.Starting;
21
+ return permission.state === 'denied' ? PsScanCameraStatus.Denied : PsScanCameraStatus.Prompt;
22
+ }
4
23
  static classify(error) {
5
24
  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;
25
+ if (name === 'NotAllowedError' || name === 'PermissionDeniedError')
26
+ return PsScanCameraStatus.Denied;
27
+ if (name === 'SecurityError')
28
+ return PsScanCameraStatus.Insecure;
29
+ if (name === 'NotReadableError' || name === 'TrackStartError' || name === 'AbortError')
30
+ return PsScanCameraStatus.Busy;
31
+ return PsScanCameraStatus.Unavailable;
32
+ }
33
+ static async hasCamera() {
34
+ if (typeof navigator.mediaDevices.enumerateDevices !== 'function')
35
+ return true;
36
+ try {
37
+ const devices = await navigator.mediaDevices.enumerateDevices();
38
+ return devices.length === 0 || devices.some((device) => device.kind === 'videoinput');
39
+ }
40
+ catch {
41
+ return true;
42
+ }
11
43
  }
12
44
  static isSupported() {
13
45
  return (typeof navigator !== 'undefined' &&
@@ -26,6 +58,19 @@ export class PsScanCameraService {
26
58
  const track = stream.getVideoTracks()[0];
27
59
  return { canTorch: PsScanCameraService.supportsTorch(track), stream, track };
28
60
  }
61
+ static async permission() {
62
+ if (typeof navigator.permissions === 'undefined' ||
63
+ typeof navigator.permissions.query !== 'function')
64
+ return null;
65
+ try {
66
+ return await navigator.permissions.query({
67
+ name: PsScanConfig.cameraPermission,
68
+ });
69
+ }
70
+ catch {
71
+ return null;
72
+ }
73
+ }
29
74
  static async setTorch(track, isOn) {
30
75
  try {
31
76
  await track.applyConstraints({
@@ -27,6 +27,12 @@ export class PsScanCanvasService {
27
27
  context.drawImage(video, 0, 0);
28
28
  return await PsScanCanvasService.encode(context.getImageData(0, 0, video.videoWidth, video.videoHeight), quality);
29
29
  }
30
+ static sample(video, maxDimension) {
31
+ const size = PsScanGeometryService.fitWithin({ height: video.videoHeight, width: video.videoWidth }, maxDimension);
32
+ const context = PsScanCanvasService.context(size.width, size.height);
33
+ context.drawImage(video, 0, 0, size.width, size.height);
34
+ return context.getImageData(0, 0, size.width, size.height);
35
+ }
30
36
  static context(width, height) {
31
37
  if (typeof OffscreenCanvas !== 'undefined') {
32
38
  const context = new OffscreenCanvas(width, height).getContext('2d', {
@@ -33,7 +33,7 @@ export class PsScanEdgeService {
33
33
  return null;
34
34
  if (!PsScanGeometryService.isConvex(quad))
35
35
  return null;
36
- return quad.map((point) => ({
36
+ return PsScanGeometryService.inset(quad, PsScanConfig.detectInset).map((point) => ({
37
37
  x: Math.min(1, Math.max(0, point.x / (width - 1))),
38
38
  y: Math.min(1, Math.max(0, point.y / (height - 1))),
39
39
  }));
@@ -11,6 +11,12 @@ export class PsScanGeometryService {
11
11
  static clamp(point) {
12
12
  return { x: Math.min(1, Math.max(0, point.x)), y: Math.min(1, Math.max(0, point.y)) };
13
13
  }
14
+ static cover(container, content) {
15
+ if (container.width <= 0 || container.height <= 0 || content.width <= 0 || content.height <= 0)
16
+ return { height: 0, width: 0 };
17
+ const scale = Math.max(container.width / content.width, container.height / content.height);
18
+ return { height: content.height * scale, width: content.width * scale };
19
+ }
14
20
  static defaultQuad() {
15
21
  const inset = 0.08;
16
22
  return [
@@ -39,6 +45,20 @@ export class PsScanGeometryService {
39
45
  width: Math.max(1, Math.round(size.width * scale)),
40
46
  };
41
47
  }
48
+ static inset(quad, distance) {
49
+ const centre = {
50
+ x: quad.reduce((sum, point) => sum + point.x, 0) / quad.length,
51
+ y: quad.reduce((sum, point) => sum + point.y, 0) / quad.length,
52
+ };
53
+ return quad.map((point) => {
54
+ const length = PsScanGeometryService.distance(point, centre);
55
+ const ratio = length <= distance ? 0 : (length - distance) / length;
56
+ return {
57
+ x: centre.x + (point.x - centre.x) * ratio,
58
+ y: centre.y + (point.y - centre.y) * ratio,
59
+ };
60
+ });
61
+ }
42
62
  static isConvex(points) {
43
63
  let sign = 0;
44
64
  for (let index = 0; index < points.length; index += 1) {
@@ -0,0 +1,15 @@
1
+ import { PsScanPlatform } from '../constants/ps-scan-platform';
2
+ export class PsScanPlatformService {
3
+ static detect() {
4
+ if (typeof navigator === 'undefined')
5
+ return PsScanPlatform.Desktop;
6
+ const agent = navigator.userAgent;
7
+ if (/iPhone|iPad|iPod/i.test(agent))
8
+ return PsScanPlatform.Ios;
9
+ if (/Macintosh/i.test(agent) && navigator.maxTouchPoints > 1)
10
+ return PsScanPlatform.Ios;
11
+ if (/Android/i.test(agent))
12
+ return PsScanPlatform.Android;
13
+ return PsScanPlatform.Desktop;
14
+ }
15
+ }
@@ -0,0 +1,41 @@
1
+ import { PsScanConfig } from '../constants/ps-scan-config';
2
+ import { PsScanGeometryService } from './ps-scan-geometry-service';
3
+ export class PsScanStabilityService {
4
+ static disarm(state) {
5
+ return { ...state, isArmed: false, stableMs: 0 };
6
+ }
7
+ static initial() {
8
+ return { isArmed: true, misses: 0, quad: null, raw: null, stableMs: 0 };
9
+ }
10
+ static movement(from, to) {
11
+ return Math.max(...from.map((point, index) => PsScanGeometryService.distance(point, to[index])));
12
+ }
13
+ static next(state, detected, elapsedMs) {
14
+ if (detected === null) {
15
+ const misses = state.misses + 1;
16
+ return misses > PsScanConfig.liveMissLimit
17
+ ? { isArmed: true, misses, quad: null, raw: null, stableMs: 0 }
18
+ : { ...state, misses, stableMs: 0 };
19
+ }
20
+ if (state.quad === null || state.raw === null)
21
+ return { ...state, misses: 0, quad: detected, raw: detected, stableMs: 0 };
22
+ const movement = PsScanStabilityService.movement(state.raw, detected);
23
+ const weight = PsScanConfig.liveSmoothing;
24
+ const quad = state.quad.map((point, index) => ({
25
+ x: point.x + (detected[index].x - point.x) * weight,
26
+ y: point.y + (detected[index].y - point.y) * weight,
27
+ }));
28
+ return {
29
+ isArmed: state.isArmed || movement > PsScanConfig.liveRearmMovement,
30
+ misses: 0,
31
+ quad,
32
+ raw: detected,
33
+ stableMs: movement <= PsScanConfig.liveStillMovement ? state.stableMs + elapsedMs : 0,
34
+ };
35
+ }
36
+ static progress(state) {
37
+ if (!state.isArmed || state.quad === null)
38
+ return 0;
39
+ return Math.min(1, state.stableMs / PsScanConfig.autoCaptureMs);
40
+ }
41
+ }
@@ -1,9 +1,11 @@
1
1
  import { PsScanCameraStatus } from '../constants/ps-scan-camera-status';
2
2
  export declare const usePsScanCamera: (isActive: boolean) => {
3
+ allow: () => void;
4
+ attachVideo: (element: HTMLVideoElement | null) => void;
3
5
  canTorch: boolean;
4
6
  capture: () => Promise<Blob | null>;
5
7
  isTorchOn: boolean;
6
8
  status: PsScanCameraStatus;
7
9
  toggleTorch: () => Promise<void>;
8
- videoRef: import("react").RefObject<HTMLVideoElement>;
10
+ videoRef: import("react").MutableRefObject<HTMLVideoElement | null>;
9
11
  };
@@ -9,18 +9,40 @@ const ps_scan_canvas_service_1 = require("../services/ps-scan-canvas-service");
9
9
  const usePsScanCamera = (isActive) => {
10
10
  const videoRef = (0, react_1.useRef)(null);
11
11
  const sessionRef = (0, react_1.useRef)(null);
12
- const [status, setStatus] = (0, react_1.useState)(ps_scan_camera_status_1.PsScanCameraStatus.Starting);
12
+ const [status, setStatus] = (0, react_1.useState)(ps_scan_camera_status_1.PsScanCameraStatus.Checking);
13
+ const [request, setRequest] = (0, react_1.useState)(0);
13
14
  const [canTorch, setCanTorch] = (0, react_1.useState)(false);
14
15
  const [isTorchOn, setIsTorchOn] = (0, react_1.useState)(false);
15
16
  (0, react_1.useEffect)(() => {
16
- if (!isActive)
17
+ let isCancelled = false;
18
+ let permission = null;
19
+ const onChange = () => {
20
+ if (permission !== null && permission.state === 'granted')
21
+ setRequest((current) => current + 1);
22
+ };
23
+ ps_scan_camera_service_1.PsScanCameraService.check().then(async (next) => {
24
+ if (isCancelled)
25
+ return;
26
+ if (next === ps_scan_camera_status_1.PsScanCameraStatus.Starting)
27
+ setRequest(1);
28
+ else
29
+ setStatus(next);
30
+ permission = await ps_scan_camera_service_1.PsScanCameraService.permission();
31
+ if (isCancelled || permission === null)
32
+ return;
33
+ permission.addEventListener('change', onChange);
34
+ });
35
+ return () => {
36
+ isCancelled = true;
37
+ if (permission !== null)
38
+ permission.removeEventListener('change', onChange);
39
+ };
40
+ }, []);
41
+ (0, react_1.useEffect)(() => {
42
+ if (!isActive || request === 0)
17
43
  return;
18
44
  let isCancelled = false;
19
45
  setIsTorchOn(false);
20
- if (!ps_scan_camera_service_1.PsScanCameraService.isSupported()) {
21
- setStatus(ps_scan_camera_status_1.PsScanCameraStatus.Unavailable);
22
- return;
23
- }
24
46
  setStatus(ps_scan_camera_status_1.PsScanCameraStatus.Starting);
25
47
  ps_scan_camera_service_1.PsScanCameraService.open()
26
48
  .then(async (session) => {
@@ -30,12 +52,10 @@ const usePsScanCamera = (isActive) => {
30
52
  }
31
53
  sessionRef.current = session;
32
54
  setCanTorch(session.canTorch);
33
- const video = videoRef.current;
34
- if (video !== null) {
35
- video.srcObject = session.stream;
36
- await video.play().catch(() => undefined);
37
- }
38
- setStatus(ps_scan_camera_status_1.PsScanCameraStatus.Ready);
55
+ if (videoRef.current !== null)
56
+ await ps_scan_camera_service_1.PsScanCameraService.attach(videoRef.current, session);
57
+ if (!isCancelled)
58
+ setStatus(ps_scan_camera_status_1.PsScanCameraStatus.Ready);
39
59
  })
40
60
  .catch((error) => {
41
61
  if (!isCancelled)
@@ -48,7 +68,13 @@ const usePsScanCamera = (isActive) => {
48
68
  if (videoRef.current !== null)
49
69
  videoRef.current.srcObject = null;
50
70
  };
51
- }, [isActive]);
71
+ }, [isActive, request]);
72
+ const attachVideo = (0, react_1.useCallback)((element) => {
73
+ videoRef.current = element;
74
+ if (element !== null && sessionRef.current !== null)
75
+ void ps_scan_camera_service_1.PsScanCameraService.attach(element, sessionRef.current);
76
+ }, []);
77
+ const allow = (0, react_1.useCallback)(() => setRequest((current) => current + 1), []);
52
78
  const toggleTorch = (0, react_1.useCallback)(async () => {
53
79
  const session = sessionRef.current;
54
80
  if (session === null)
@@ -57,10 +83,21 @@ const usePsScanCamera = (isActive) => {
57
83
  }, [isTorchOn]);
58
84
  const capture = (0, react_1.useCallback)(async () => {
59
85
  const video = videoRef.current;
60
- if (video === null || video.videoWidth === 0)
86
+ if (video === null ||
87
+ video.videoWidth === 0 ||
88
+ video.readyState < HTMLMediaElement.HAVE_CURRENT_DATA)
61
89
  return null;
62
90
  return await ps_scan_canvas_service_1.PsScanCanvasService.fromVideo(video, ps_scan_config_1.PsScanConfig.capturedJpegQuality);
63
91
  }, []);
64
- return { canTorch, capture, isTorchOn, status, toggleTorch, videoRef };
92
+ return {
93
+ allow,
94
+ attachVideo,
95
+ canTorch,
96
+ capture,
97
+ isTorchOn,
98
+ status,
99
+ toggleTorch,
100
+ videoRef,
101
+ };
65
102
  };
66
103
  exports.usePsScanCamera = usePsScanCamera;
@@ -0,0 +1,3 @@
1
+ import { type RefObject } from 'react';
2
+ import type { PsScanLiveDetection } from '../models/ps-scan-live-detection';
3
+ export declare const usePsScanLiveDetection: (videoRef: RefObject<HTMLVideoElement | null>, isActive: boolean, isAuto: boolean, onSteady: () => void) => PsScanLiveDetection;
@@ -0,0 +1,49 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.usePsScanLiveDetection = void 0;
4
+ const react_1 = require("react");
5
+ const ps_scan_config_1 = require("../constants/ps-scan-config");
6
+ const ps_scan_canvas_service_1 = require("../services/ps-scan-canvas-service");
7
+ const ps_scan_edge_service_1 = require("../services/ps-scan-edge-service");
8
+ const ps_scan_stability_service_1 = require("../services/ps-scan-stability-service");
9
+ const usePsScanLiveDetection = (videoRef, isActive, isAuto, onSteady) => {
10
+ const [detection, setDetection] = (0, react_1.useState)({ progress: 0, quad: null });
11
+ const isAutoRef = (0, react_1.useRef)(isAuto);
12
+ const onSteadyRef = (0, react_1.useRef)(onSteady);
13
+ isAutoRef.current = isAuto;
14
+ onSteadyRef.current = onSteady;
15
+ (0, react_1.useEffect)(() => {
16
+ if (!isActive) {
17
+ setDetection({ progress: 0, quad: null });
18
+ return;
19
+ }
20
+ let state = ps_scan_stability_service_1.PsScanStabilityService.initial();
21
+ let last = performance.now();
22
+ let timer;
23
+ const tick = () => {
24
+ const video = videoRef.current;
25
+ const now = performance.now();
26
+ if (video !== null && video.videoWidth > 0 && document.visibilityState === 'visible') {
27
+ try {
28
+ const raster = ps_scan_canvas_service_1.PsScanCanvasService.sample(video, ps_scan_config_1.PsScanConfig.liveDetectMaxDimension);
29
+ state = ps_scan_stability_service_1.PsScanStabilityService.next(state, ps_scan_edge_service_1.PsScanEdgeService.detect(raster), now - last);
30
+ }
31
+ catch {
32
+ state = ps_scan_stability_service_1.PsScanStabilityService.initial();
33
+ }
34
+ const progress = isAutoRef.current ? ps_scan_stability_service_1.PsScanStabilityService.progress(state) : 0;
35
+ setDetection({ progress, quad: state.quad });
36
+ if (progress >= 1) {
37
+ state = ps_scan_stability_service_1.PsScanStabilityService.disarm(state);
38
+ onSteadyRef.current();
39
+ }
40
+ }
41
+ last = now;
42
+ timer = setTimeout(tick, ps_scan_config_1.PsScanConfig.liveDetectIntervalMs);
43
+ };
44
+ timer = setTimeout(tick, ps_scan_config_1.PsScanConfig.liveDetectIntervalMs);
45
+ return () => clearTimeout(timer);
46
+ }, [isActive]);
47
+ return detection;
48
+ };
49
+ exports.usePsScanLiveDetection = usePsScanLiveDetection;
package/dist/index.d.ts CHANGED
@@ -74,3 +74,4 @@ export * from './constants/ps-document-scanner-labels';
74
74
  export * from './constants/ps-scan-filter';
75
75
  export * from './constants/ps-scan-output-format';
76
76
  export * from './models/ps-document-scanner-props';
77
+ export * from './models/ps-file-picker-scan';
package/dist/index.js CHANGED
@@ -90,3 +90,4 @@ __exportStar(require("./constants/ps-document-scanner-labels"), exports);
90
90
  __exportStar(require("./constants/ps-scan-filter"), exports);
91
91
  __exportStar(require("./constants/ps-scan-output-format"), exports);
92
92
  __exportStar(require("./models/ps-document-scanner-props"), exports);
93
+ __exportStar(require("./models/ps-file-picker-scan"), exports);
@@ -4,6 +4,7 @@ import type { PsLabelSet } from './ps-label-set';
4
4
  export interface PsDocumentScannerProps {
5
5
  className?: string;
6
6
  fileName?: string;
7
+ isAutoCapture?: boolean;
7
8
  isOpen: boolean;
8
9
  jpegQuality?: number;
9
10
  labels?: Partial<PsLabelSet<typeof PsDocumentScannerLabels>>;
@@ -3,6 +3,7 @@ import type { PsScanOutputFormat } from '../constants/ps-scan-output-format';
3
3
  import type { PsLabelSet } from './ps-label-set';
4
4
  export interface PsDocumentScannerSheetProps {
5
5
  fileName: string;
6
+ isAutoCapture: boolean;
6
7
  jpegQuality: number;
7
8
  labels: PsLabelSet<typeof PsDocumentScannerLabels>;
8
9
  maxDimension: number;
@@ -0,0 +1,4 @@
1
+ import type { PsDocumentScannerProps } from './ps-document-scanner-props';
2
+ export interface PsFilePickerScan extends Omit<PsDocumentScannerProps, 'fileName' | 'isOpen' | 'onClose' | 'onComplete'> {
3
+ getFileName?: () => string;
4
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,5 @@
1
+ import type { PsScanQuad } from './ps-scan-quad';
2
+ export interface PsScanLiveDetection {
3
+ progress: number;
4
+ quad: PsScanQuad | null;
5
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,8 @@
1
+ import type { PsScanQuad } from './ps-scan-quad';
2
+ export interface PsScanStability {
3
+ isArmed: boolean;
4
+ misses: number;
5
+ quad: PsScanQuad | null;
6
+ raw: PsScanQuad | null;
7
+ stableMs: number;
8
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -1,9 +1,13 @@
1
1
  import { PsScanCameraStatus } from '../constants/ps-scan-camera-status';
2
2
  import type { PsScanCameraSession } from '../models/ps-scan-camera-session';
3
3
  export declare class PsScanCameraService {
4
+ static attach(video: HTMLVideoElement, session: PsScanCameraSession): Promise<void>;
5
+ static check(): Promise<PsScanCameraStatus>;
4
6
  static classify(error: unknown): PsScanCameraStatus;
7
+ static hasCamera(): Promise<boolean>;
5
8
  static isSupported(): boolean;
6
9
  static open(): Promise<PsScanCameraSession>;
10
+ static permission(): Promise<PermissionStatus | null>;
7
11
  static setTorch(track: MediaStreamTrack, isOn: boolean): Promise<boolean>;
8
12
  static stop(stream: MediaStream | null): void;
9
13
  static supportsTorch(track: MediaStreamTrack | undefined): boolean;