@pallasoft/ps-file-components 1.5.0 → 2.0.1

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 (151) hide show
  1. package/dist/components/ps-document-scanner-loader.d.ts +1 -1
  2. package/dist/components/ps-document-scanner-sheet.d.ts +1 -1
  3. package/dist/components/ps-document-scanner-sheet.js +40 -110
  4. package/dist/components/ps-document-scanner.d.ts +1 -1
  5. package/dist/components/ps-document-scanner.js +2 -3
  6. package/dist/components/ps-file-delete-confirm.js +1 -1
  7. package/dist/components/ps-scan-camera-view.d.ts +2 -9
  8. package/dist/components/ps-scan-camera-view.js +3 -12
  9. package/dist/components/ps-scan-crop-editor.d.ts +4 -4
  10. package/dist/components/ps-scan-crop-editor.js +9 -9
  11. package/dist/components/ps-scan-permission-view.d.ts +2 -3
  12. package/dist/components/ps-scan-permission-view.js +2 -2
  13. package/dist/components/ps-scan-photo-button.d.ts +2 -3
  14. package/dist/components/ps-scan-photo-button.js +5 -5
  15. package/dist/components/ps-scan-preview.d.ts +12 -0
  16. package/dist/components/ps-scan-preview.js +10 -0
  17. package/dist/components/ps-scan-shutter.d.ts +1 -2
  18. package/dist/components/ps-scan-shutter.js +1 -1
  19. package/dist/components/ps-upload-modal.js +1 -1
  20. package/dist/constants/ps-document-scanner-labels.d.ts +5 -34
  21. package/dist/constants/ps-document-scanner-labels.js +5 -34
  22. package/dist/constants/ps-scan-chrome-classes.d.ts +0 -2
  23. package/dist/constants/ps-scan-chrome-classes.js +0 -2
  24. package/dist/constants/ps-scan-config.d.ts +24 -31
  25. package/dist/constants/ps-scan-config.js +24 -31
  26. package/dist/constants/ps-scan-step.d.ts +1 -1
  27. package/dist/constants/ps-scan-step.js +1 -1
  28. package/dist/esm/components/ps-document-scanner-sheet.js +41 -111
  29. package/dist/esm/components/ps-document-scanner.js +2 -3
  30. package/dist/esm/components/ps-file-delete-confirm.js +2 -2
  31. package/dist/esm/components/ps-scan-camera-view.js +3 -12
  32. package/dist/esm/components/ps-scan-crop-editor.js +9 -9
  33. package/dist/esm/components/ps-scan-permission-view.js +2 -2
  34. package/dist/esm/components/ps-scan-photo-button.js +5 -5
  35. package/dist/esm/components/ps-scan-preview.js +6 -0
  36. package/dist/esm/components/ps-scan-shutter.js +1 -1
  37. package/dist/esm/components/ps-upload-modal.js +2 -2
  38. package/dist/esm/constants/ps-document-scanner-labels.js +5 -34
  39. package/dist/esm/constants/ps-scan-chrome-classes.js +0 -2
  40. package/dist/esm/constants/ps-scan-config.js +24 -31
  41. package/dist/esm/constants/ps-scan-step.js +1 -1
  42. package/dist/esm/hooks/use-ps-scan-live-detection.js +17 -21
  43. package/dist/esm/hooks/use-ps-scan-photo.js +42 -0
  44. package/dist/esm/index.js +0 -2
  45. package/dist/esm/services/ps-scan-camera-service.js +2 -2
  46. package/dist/esm/services/ps-scan-canny-service.js +93 -0
  47. package/dist/esm/services/ps-scan-canvas-service.js +7 -1
  48. package/dist/esm/services/ps-scan-edge-service.js +73 -137
  49. package/dist/esm/services/ps-scan-file-name-service.js +2 -11
  50. package/dist/esm/services/ps-scan-geometry-service.js +36 -0
  51. package/dist/esm/services/ps-scan-hull-service.js +0 -39
  52. package/dist/esm/services/ps-scan-image-filter-service.js +0 -21
  53. package/dist/esm/services/ps-scan-morphology-service.js +44 -0
  54. package/dist/esm/services/ps-scan-pipeline-service.js +9 -19
  55. package/dist/esm/services/ps-scan-polygon-service.js +75 -0
  56. package/dist/esm/services/ps-scan-region-service.js +52 -0
  57. package/dist/esm/services/ps-scan-stability-service.js +9 -22
  58. package/dist/esm/services/ps-scan-warp-service.js +8 -2
  59. package/dist/esm/services/ps-scan-yield-service.js +5 -0
  60. package/dist/hooks/use-ps-scan-live-detection.d.ts +1 -1
  61. package/dist/hooks/use-ps-scan-live-detection.js +16 -20
  62. package/dist/hooks/use-ps-scan-photo.d.ts +9 -0
  63. package/dist/hooks/use-ps-scan-photo.js +46 -0
  64. package/dist/index.d.ts +0 -2
  65. package/dist/index.js +0 -2
  66. package/dist/models/ps-document-scanner-props.d.ts +0 -4
  67. package/dist/models/ps-document-scanner-sheet-props.d.ts +0 -4
  68. package/dist/models/ps-scan-gradient.d.ts +4 -0
  69. package/dist/models/ps-scan-live-detection.d.ts +0 -1
  70. package/dist/models/ps-scan-located.d.ts +5 -0
  71. package/dist/models/ps-scan-photo.d.ts +9 -0
  72. package/dist/models/ps-scan-region.d.ts +5 -0
  73. package/dist/models/ps-scan-result.d.ts +6 -0
  74. package/dist/models/ps-scan-stability.d.ts +0 -3
  75. package/dist/services/ps-scan-camera-service.js +2 -2
  76. package/dist/services/ps-scan-canny-service.d.ts +7 -0
  77. package/dist/services/ps-scan-canny-service.js +97 -0
  78. package/dist/services/ps-scan-canvas-service.js +7 -1
  79. package/dist/services/ps-scan-edge-service.d.ts +5 -5
  80. package/dist/services/ps-scan-edge-service.js +73 -137
  81. package/dist/services/ps-scan-file-name-service.d.ts +1 -3
  82. package/dist/services/ps-scan-file-name-service.js +2 -11
  83. package/dist/services/ps-scan-geometry-service.d.ts +2 -0
  84. package/dist/services/ps-scan-geometry-service.js +36 -0
  85. package/dist/services/ps-scan-hull-service.d.ts +0 -2
  86. package/dist/services/ps-scan-hull-service.js +0 -39
  87. package/dist/services/ps-scan-image-filter-service.d.ts +0 -1
  88. package/dist/services/ps-scan-image-filter-service.js +0 -21
  89. package/dist/services/ps-scan-morphology-service.d.ts +7 -0
  90. package/dist/services/ps-scan-morphology-service.js +48 -0
  91. package/dist/services/ps-scan-pipeline-service.d.ts +2 -5
  92. package/dist/services/ps-scan-pipeline-service.js +9 -19
  93. package/dist/services/ps-scan-polygon-service.d.ts +8 -0
  94. package/dist/services/ps-scan-polygon-service.js +79 -0
  95. package/dist/services/ps-scan-region-service.d.ts +4 -0
  96. package/dist/services/ps-scan-region-service.js +56 -0
  97. package/dist/services/ps-scan-stability-service.d.ts +1 -3
  98. package/dist/services/ps-scan-stability-service.js +9 -22
  99. package/dist/services/ps-scan-warp-service.d.ts +1 -1
  100. package/dist/services/ps-scan-warp-service.js +8 -2
  101. package/dist/services/ps-scan-yield-service.d.ts +3 -0
  102. package/dist/services/ps-scan-yield-service.js +9 -0
  103. package/package.json +3 -5
  104. package/dist/components/ps-scan-page-list.d.ts +0 -14
  105. package/dist/components/ps-scan-page-list.js +0 -22
  106. package/dist/components/ps-scan-page-stack.d.ts +0 -7
  107. package/dist/components/ps-scan-page-stack.js +0 -6
  108. package/dist/components/ps-scan-review.d.ts +0 -24
  109. package/dist/components/ps-scan-review.js +0 -37
  110. package/dist/constants/ps-scan-filter.d.ts +0 -7
  111. package/dist/constants/ps-scan-filter.js +0 -9
  112. package/dist/constants/ps-scan-output-format.d.ts +0 -5
  113. package/dist/constants/ps-scan-output-format.js +0 -7
  114. package/dist/esm/components/ps-scan-page-list.js +0 -18
  115. package/dist/esm/components/ps-scan-page-stack.js +0 -2
  116. package/dist/esm/components/ps-scan-review.js +0 -33
  117. package/dist/esm/constants/ps-scan-filter.js +0 -6
  118. package/dist/esm/constants/ps-scan-output-format.js +0 -4
  119. package/dist/esm/hooks/use-ps-scan-pages.js +0 -62
  120. package/dist/esm/hooks/use-ps-scan-previews.js +0 -57
  121. package/dist/esm/services/ps-scan-enhance-service.js +0 -97
  122. package/dist/esm/services/ps-scan-order-service.js +0 -13
  123. package/dist/esm/services/ps-scan-output-service.js +0 -24
  124. package/dist/esm/services/ps-scan-pdf-service.js +0 -56
  125. package/dist/hooks/use-ps-scan-pages.d.ts +0 -12
  126. package/dist/hooks/use-ps-scan-pages.js +0 -66
  127. package/dist/hooks/use-ps-scan-previews.d.ts +0 -2
  128. package/dist/hooks/use-ps-scan-previews.js +0 -61
  129. package/dist/models/ps-scan-added-page.d.ts +0 -5
  130. package/dist/models/ps-scan-levels.d.ts +0 -4
  131. package/dist/models/ps-scan-output-options.d.ts +0 -7
  132. package/dist/models/ps-scan-page.d.ts +0 -12
  133. package/dist/models/ps-scan-pdf-page.d.ts +0 -5
  134. package/dist/services/ps-scan-enhance-service.d.ts +0 -9
  135. package/dist/services/ps-scan-enhance-service.js +0 -101
  136. package/dist/services/ps-scan-order-service.d.ts +0 -4
  137. package/dist/services/ps-scan-order-service.js +0 -17
  138. package/dist/services/ps-scan-output-service.d.ts +0 -5
  139. package/dist/services/ps-scan-output-service.js +0 -28
  140. package/dist/services/ps-scan-pdf-service.d.ts +0 -5
  141. package/dist/services/ps-scan-pdf-service.js +0 -60
  142. /package/dist/esm/models/{ps-scan-added-page.js → ps-scan-gradient.js} +0 -0
  143. /package/dist/esm/models/{ps-scan-levels.js → ps-scan-located.js} +0 -0
  144. /package/dist/esm/models/{ps-scan-output-options.js → ps-scan-photo.js} +0 -0
  145. /package/dist/esm/models/{ps-scan-page.js → ps-scan-region.js} +0 -0
  146. /package/dist/esm/models/{ps-scan-pdf-page.js → ps-scan-result.js} +0 -0
  147. /package/dist/models/{ps-scan-added-page.js → ps-scan-gradient.js} +0 -0
  148. /package/dist/models/{ps-scan-levels.js → ps-scan-located.js} +0 -0
  149. /package/dist/models/{ps-scan-output-options.js → ps-scan-photo.js} +0 -0
  150. /package/dist/models/{ps-scan-page.js → ps-scan-region.js} +0 -0
  151. /package/dist/models/{ps-scan-pdf-page.js → ps-scan-result.js} +0 -0
@@ -4,27 +4,18 @@ import { PsSpinner, usePsElementSize } from '@pallasoft/ps-core-components';
4
4
  import { PallasoftClassNameService } from '@pallasoft/ps-design-system';
5
5
  import { PsIconKey } from '@pallasoft/ps-icons';
6
6
  import { PsScanChromeClasses } from '../constants/ps-scan-chrome-classes';
7
- import { PsScanTemplateService } from '../services/ps-scan-template-service';
8
- import { PsScanButton } from './ps-scan-button';
9
7
  import { PsScanIconButton } from './ps-scan-icon-button';
10
- import { PsScanPageStack } from './ps-scan-page-stack';
11
8
  import { PsScanPhotoButton } from './ps-scan-photo-button';
12
9
  import { PsScanShutter } from './ps-scan-shutter';
13
10
  import { PsScanViewfinderOverlay } from './ps-scan-viewfinder-overlay';
14
- export const PsScanCameraView = ({ canTorch, detection, flashKey, isAuto, isBusy, isFull, isMultiple, isReady, isTorchOn, labels, lastPreview, onCapture, onClose, onDone, onFiles, onToggleAuto, onToggleTorch, pageCount, videoRef, videoTarget, }) => {
11
+ export const PsScanCameraView = ({ canTorch, detection, flashKey, isBusy, isReady, isTorchOn, labels, onCapture, onClose, onFile, onToggleTorch, videoRef, videoTarget, }) => {
15
12
  const [areaRef, area] = usePsElementSize();
16
13
  const [video, setVideo] = useState({ height: 0, width: 0 });
17
- const hint = isFull
18
- ? labels.maxPages
19
- : detection.quad === null
20
- ? labels.hintFind
21
- : isAuto
22
- ? labels.hintHold
23
- : labels.hintFound;
14
+ const hint = detection.quad === null ? labels.hintFind : labels.hintFound;
24
15
  const measure = () => {
25
16
  const element = videoRef.current;
26
17
  if (element !== null)
27
18
  setVideo({ height: element.videoHeight, width: element.videoWidth });
28
19
  };
29
- return (_jsxs("div", { className: 'relative h-full min-h-0 overflow-hidden bg-black text-white', children: [_jsxs("div", { className: 'absolute inset-0', ref: areaRef, children: [_jsx("video", { "aria-hidden": 'true', autoPlay: true, className: PallasoftClassNameService.merge('size-full object-cover transition-opacity duration-300', isReady ? 'opacity-100' : 'opacity-0'), muted: true, onLoadedMetadata: measure, onResize: measure, playsInline: true, ref: videoTarget }), isReady ? (_jsx(PsScanViewfinderOverlay, { area: area, quad: detection.quad, video: video })) : null] }), isReady ? null : (_jsxs("div", { className: 'absolute inset-0 flex flex-col items-center justify-center gap-3 text-sm text-white/75', role: 'status', children: [_jsx(PsSpinner, { color: 'current', label: labels.cameraStarting, size: 'small' }), _jsx("span", { children: labels.cameraStarting })] })), flashKey > 0 ? (_jsx("div", { "aria-hidden": 'true', className: 'pointer-events-none absolute inset-0 bg-white animate-ps-fade-out' }, flashKey)) : null, _jsxs("div", { className: 'absolute inset-x-0 top-0 flex items-start justify-between gap-3 bg-gradient-to-b from-black/60 to-transparent px-4 pb-10 pt-[max(1rem,env(safe-area-inset-top))]', children: [_jsx(PsScanIconButton, { iconKey: PsIconKey.Close, label: labels.close, onClick: onClose }), _jsxs("div", { className: 'flex items-center gap-2', children: [canTorch && isReady ? (_jsx(PsScanIconButton, { iconKey: PsIconKey.Flash, isPressed: isTorchOn, label: isTorchOn ? labels.torchOff : labels.torchOn, onClick: onToggleTorch })) : null, _jsx("button", { "aria-label": labels.autoCapture, "aria-pressed": isAuto, className: PallasoftClassNameService.merge(PsScanChromeClasses.button, 'border-transparent', isAuto ? PsScanChromeClasses.iconOn : PsScanChromeClasses.iconOff), onClick: onToggleAuto, title: labels.autoCapture, type: 'button', children: labels.auto })] })] }), isReady ? (_jsx("div", { className: 'absolute inset-x-0 top-[calc(max(1rem,env(safe-area-inset-top))+3.75rem)] flex justify-center px-4', children: _jsx("p", { "aria-live": 'polite', className: PsScanChromeClasses.pill, children: hint }) })) : null, _jsxs("div", { className: 'absolute inset-x-0 bottom-0 grid grid-cols-[1fr_auto_1fr] items-center gap-4 bg-gradient-to-t from-black/80 to-transparent px-5 pb-[max(1.5rem,env(safe-area-inset-bottom))] pt-12', children: [_jsx("div", { className: 'flex justify-start', children: pageCount > 0 ? (_jsx(PsScanPageStack, { count: pageCount, label: PsScanTemplateService.fill(labels.viewPages, { count: pageCount }), onOpen: onDone, preview: lastPreview })) : (_jsx(PsScanPhotoButton, { isIconOnly: true, isMultiple: isMultiple, label: labels.choosePhotos, onFiles: onFiles })) }), _jsx(PsScanShutter, { disabled: !isReady || isFull || isBusy, label: labels.capture, onCapture: onCapture, progress: detection.progress }), _jsx("div", { className: 'flex justify-end', children: pageCount > 0 ? (_jsx(PsScanButton, { isPrimary: true, onClick: onDone, children: labels.done })) : null })] })] }));
20
+ return (_jsxs("div", { className: 'relative h-full min-h-0 overflow-hidden bg-black text-white', children: [_jsxs("div", { className: 'absolute inset-0', ref: areaRef, children: [_jsx("video", { "aria-hidden": 'true', autoPlay: true, className: PallasoftClassNameService.merge('size-full object-cover transition-opacity duration-300', isReady ? 'opacity-100' : 'opacity-0'), muted: true, onLoadedMetadata: measure, onResize: measure, playsInline: true, ref: videoTarget }), isReady ? (_jsx(PsScanViewfinderOverlay, { area: area, quad: detection.quad, video: video })) : null] }), isReady ? null : (_jsxs("div", { className: 'absolute inset-0 flex flex-col items-center justify-center gap-3 text-sm text-white/75', role: 'status', children: [_jsx(PsSpinner, { color: 'current', label: labels.cameraStarting, size: 'small' }), _jsx("span", { children: labels.cameraStarting })] })), flashKey > 0 ? (_jsx("div", { "aria-hidden": 'true', className: 'pointer-events-none absolute inset-0 bg-white animate-ps-fade-out' }, flashKey)) : null, _jsxs("div", { className: 'absolute inset-x-0 top-0 flex items-start justify-between gap-3 bg-gradient-to-b from-black/60 to-transparent px-4 pb-10 pt-[max(1rem,env(safe-area-inset-top))]', children: [_jsx(PsScanIconButton, { iconKey: PsIconKey.Close, label: labels.close, onClick: onClose }), _jsx("div", { className: 'flex items-center gap-2', children: canTorch && isReady ? (_jsx(PsScanIconButton, { iconKey: PsIconKey.Flash, isPressed: isTorchOn, label: isTorchOn ? labels.torchOff : labels.torchOn, onClick: onToggleTorch })) : null })] }), isReady ? (_jsx("div", { className: 'absolute inset-x-0 top-[calc(max(1rem,env(safe-area-inset-top))+3.75rem)] flex justify-center px-4', children: _jsx("p", { "aria-live": 'polite', className: PsScanChromeClasses.pill, children: hint }) })) : null, _jsxs("div", { className: 'absolute inset-x-0 bottom-0 grid grid-cols-[1fr_auto_1fr] items-center gap-4 bg-gradient-to-t from-black/80 to-transparent px-5 pb-[max(1.5rem,env(safe-area-inset-bottom))] pt-12', children: [_jsx("div", { className: 'flex justify-start', children: _jsx(PsScanPhotoButton, { isIconOnly: true, label: labels.choosePhoto, onFile: onFile }) }), _jsx(PsScanShutter, { disabled: !isReady || isBusy, label: labels.capture, onCapture: onCapture }), _jsx("span", { "aria-hidden": 'true' })] })] }));
30
21
  };
@@ -9,19 +9,19 @@ import { PsScanGeometryService } from '../services/ps-scan-geometry-service';
9
9
  import { PsScanButton } from './ps-scan-button';
10
10
  import { PsScanIconButton } from './ps-scan-icon-button';
11
11
  import { PsScanLoupe } from './ps-scan-loupe';
12
- export const PsScanCropEditor = ({ labels, onAutoDetect, onChange, onKeep, onRetake, page, }) => {
12
+ export const PsScanCropEditor = ({ labels, onAutoDetect, onChange, onContinue, onRetake, photo, }) => {
13
13
  const [areaRef, area] = usePsElementSize();
14
14
  const stageRef = useRef(null);
15
15
  const [activeIndex, setActiveIndex] = useState(null);
16
16
  const hintId = 'ps-scan-crop-hint';
17
- const size = PsScanGeometryService.fit(area, { height: page.height, width: page.width });
17
+ const size = PsScanGeometryService.fit(area, { height: photo.height, width: photo.width });
18
18
  const cornerLabels = [
19
19
  labels.cornerTopLeft,
20
20
  labels.cornerTopRight,
21
21
  labels.cornerBottomRight,
22
22
  labels.cornerBottomLeft,
23
23
  ];
24
- const outline = page.corners
24
+ const outline = photo.corners
25
25
  .map((point) => `${point.x * size.width},${point.y * size.height}`)
26
26
  .join(' L');
27
27
  const move = (index, event) => {
@@ -33,7 +33,7 @@ export const PsScanCropEditor = ({ labels, onAutoDetect, onChange, onKeep, onRet
33
33
  x: (event.clientX - rect.left) / rect.width,
34
34
  y: (event.clientY - rect.top) / rect.height,
35
35
  });
36
- const next = page.corners.map((corner, at) => (at === index ? point : corner));
36
+ const next = photo.corners.map((corner, at) => (at === index ? point : corner));
37
37
  if (PsScanGeometryService.isUsable(next))
38
38
  onChange(next);
39
39
  };
@@ -53,13 +53,13 @@ export const PsScanCropEditor = ({ labels, onAutoDetect, onChange, onKeep, onRet
53
53
  if (delta === undefined)
54
54
  return;
55
55
  event.preventDefault();
56
- onChange(PsScanGeometryService.nudge(page.corners, index, delta[0], delta[1]));
56
+ onChange(PsScanGeometryService.nudge(photo.corners, index, delta[0], delta[1]));
57
57
  };
58
- return (_jsxs("div", { className: 'flex h-full min-h-0 flex-col bg-black text-white animate-ps-fade-in', children: [_jsxs("header", { className: 'grid shrink-0 grid-cols-[2.75rem_1fr_2.75rem] items-center gap-3 px-4 pb-1 pt-[max(1rem,env(safe-area-inset-top))]', children: [_jsx("span", { "aria-hidden": 'true' }), _jsx("h2", { className: 'truncate text-center text-base font-semibold', children: labels.cropTitle }), _jsx(PsScanIconButton, { iconKey: PsIconKey.Spark, label: labels.autoDetect, onClick: onAutoDetect })] }), _jsx("p", { className: 'shrink-0 px-6 text-center text-sm text-white/65', id: hintId, children: labels.cropHint }), _jsx("div", { className: 'flex min-h-0 flex-1 items-center justify-center px-8 py-6', ref: areaRef, children: _jsxs("div", { className: 'relative touch-none select-none', ref: stageRef, style: { height: size.height, width: size.width }, children: [_jsx("img", { alt: '', className: 'absolute inset-0 size-full rounded-sm', draggable: false, src: page.sourceUrl }), _jsxs("svg", { "aria-hidden": 'true', className: 'pointer-events-none absolute inset-0 size-full overflow-visible', viewBox: `0 0 ${Math.max(1, size.width)} ${Math.max(1, size.height)}`, children: [_jsx("path", { className: 'fill-black/55', d: `M0 0H${size.width}V${size.height}H0Z M${outline}Z`, fillRule: 'evenodd' }), _jsx("path", { className: 'fill-none stroke-primary', d: `M${outline}Z`, strokeLinejoin: 'round', strokeWidth: '2.5' })] }), PsScanCornerOrder.map((key, index) => (_jsx("button", { "aria-describedby": hintId, "aria-label": cornerLabels[index], className: 'ps-focus-ring absolute flex size-12 -translate-x-1/2 -translate-y-1/2 cursor-grab touch-none items-center justify-center rounded-full active:cursor-grabbing', onKeyDown: (event) => nudge(index, event), onPointerCancel: release, onPointerDown: (event) => {
58
+ return (_jsxs("div", { className: 'flex h-full min-h-0 flex-col bg-black text-white animate-ps-fade-in', children: [_jsxs("header", { className: 'grid shrink-0 grid-cols-[2.75rem_1fr_2.75rem] items-center gap-3 px-4 pb-1 pt-[max(1rem,env(safe-area-inset-top))]', children: [_jsx("span", { "aria-hidden": 'true' }), _jsx("h2", { className: 'truncate text-center text-base font-semibold', children: labels.cropTitle }), _jsx(PsScanIconButton, { iconKey: PsIconKey.Spark, label: labels.autoDetect, onClick: onAutoDetect })] }), _jsxs("p", { className: 'shrink-0 px-6 text-center text-sm text-white/65', id: hintId, children: [photo.isDetected ? labels.edgesFound : labels.edgesNotFound, " ", labels.cropHint] }), _jsx("div", { className: 'flex min-h-0 flex-1 items-center justify-center px-8 py-6', ref: areaRef, children: _jsxs("div", { className: 'relative touch-none select-none', ref: stageRef, style: { height: size.height, width: size.width }, children: [_jsx("img", { alt: '', className: 'absolute inset-0 size-full rounded-sm', draggable: false, src: photo.sourceUrl }), _jsxs("svg", { "aria-hidden": 'true', className: 'pointer-events-none absolute inset-0 size-full overflow-visible', viewBox: `0 0 ${Math.max(1, size.width)} ${Math.max(1, size.height)}`, children: [_jsx("path", { className: 'fill-black/55', d: `M0 0H${size.width}V${size.height}H0Z M${outline}Z`, fillRule: 'evenodd' }), _jsx("path", { className: 'fill-none stroke-primary', d: `M${outline}Z`, strokeLinejoin: 'round', strokeWidth: '2.5' })] }), PsScanCornerOrder.map((key, index) => (_jsx("button", { "aria-describedby": hintId, "aria-label": cornerLabels[index], className: 'ps-focus-ring absolute flex size-11 -translate-x-1/2 -translate-y-1/2 cursor-grab touch-none items-center justify-center rounded-full active:cursor-grabbing', onKeyDown: (event) => nudge(index, event), onPointerCancel: release, onPointerDown: (event) => {
59
59
  event.currentTarget.setPointerCapture(event.pointerId);
60
60
  setActiveIndex(index);
61
61
  }, onPointerMove: (event) => move(index, event), onPointerUp: release, style: {
62
- left: `${page.corners[index].x * 100}%`,
63
- top: `${page.corners[index].y * 100}%`,
64
- }, type: 'button', children: _jsx("span", { className: PallasoftClassNameService.merge('size-6 rounded-full border-[3px] border-white bg-primary shadow-lg transition-transform duration-150', activeIndex === index ? 'scale-125' : '') }) }, key))), activeIndex === null ? null : (_jsx(PsScanLoupe, { corners: page.corners, point: page.corners[activeIndex], source: page.sourceUrl, stage: size }))] }) }), _jsxs("footer", { className: 'mx-auto grid w-full max-w-md shrink-0 grid-cols-2 gap-3 px-4 pb-[max(1.25rem,env(safe-area-inset-bottom))] pt-2', children: [_jsx(PsScanButton, { isLarge: true, onClick: onRetake, children: labels.retake }), _jsx(PsScanButton, { isLarge: true, isPrimary: true, onClick: onKeep, children: labels.keepScan })] })] }));
62
+ left: `${photo.corners[index].x * 100}%`,
63
+ top: `${photo.corners[index].y * 100}%`,
64
+ }, type: 'button', children: _jsx("span", { className: PallasoftClassNameService.merge('size-6 rounded-full border-[3px] border-white bg-primary shadow-lg transition-transform duration-150', activeIndex === index ? 'scale-125' : '') }) }, key))), activeIndex === null ? null : (_jsx(PsScanLoupe, { corners: photo.corners, point: photo.corners[activeIndex], source: photo.sourceUrl, stage: size }))] }) }), _jsxs("footer", { className: 'mx-auto grid w-full max-w-md shrink-0 grid-cols-2 gap-3 px-4 pb-[max(1.25rem,env(safe-area-inset-bottom))] pt-2', children: [_jsx(PsScanButton, { isLarge: true, onClick: onRetake, children: labels.retake }), _jsx(PsScanButton, { isLarge: true, isPrimary: true, onClick: onContinue, children: labels.continue })] })] }));
65
65
  };
@@ -6,7 +6,7 @@ import { PsScanButton } from './ps-scan-button';
6
6
  import { PsScanDeniedSteps } from './ps-scan-denied-steps';
7
7
  import { PsScanIllustration } from './ps-scan-illustration';
8
8
  import { PsScanPhotoButton } from './ps-scan-photo-button';
9
- export const PsScanPermissionView = ({ isMultiple, labels, onAllow, onFiles, status, }) => {
9
+ export const PsScanPermissionView = ({ labels, onAllow, onFile, status, }) => {
10
10
  if (status === PsScanCameraStatus.Checking || status === PsScanCameraStatus.Starting)
11
11
  return (_jsxs("div", { className: 'flex h-full flex-col items-center justify-center gap-3 text-sm text-white/75', role: 'status', children: [_jsx(PsSpinner, { color: 'current', label: labels.cameraStarting, size: 'small' }), _jsx("span", { children: labels.cameraStarting })] }));
12
12
  const screens = {
@@ -40,5 +40,5 @@ export const PsScanPermissionView = ({ isMultiple, labels, onAllow, onFiles, sta
40
40
  const screen = screens[status];
41
41
  const isPrimer = status === PsScanCameraStatus.Prompt;
42
42
  const canRetry = status === PsScanCameraStatus.Denied || status === PsScanCameraStatus.Busy;
43
- return (_jsx("div", { className: 'flex h-full min-h-0 flex-col overflow-y-auto', children: _jsxs("div", { className: 'm-auto flex w-full max-w-sm flex-col items-center gap-6 px-6 pb-[max(1.5rem,env(safe-area-inset-bottom))] pt-20 text-center animate-ps-slide-up', children: [isPrimer ? (_jsx(PsScanIllustration, {})) : (_jsx("span", { className: 'flex size-20 items-center justify-center rounded-full bg-white/10 text-white', children: _jsx(PsIcon, { "aria-hidden": 'true', className: 'size-9', iconKey: screen.iconKey }) })), _jsxs("div", { className: 'flex flex-col gap-2', children: [_jsx("h2", { className: 'text-xl font-semibold leading-tight text-white sm:text-2xl', children: screen.title }), _jsx("p", { className: 'text-sm leading-6 text-white/70', children: screen.body })] }), status === PsScanCameraStatus.Denied ? _jsx(PsScanDeniedSteps, { labels: labels }) : null, _jsxs("div", { className: 'flex w-full flex-col gap-3', children: [isPrimer || canRetry ? (_jsx(PsScanButton, { className: 'w-full', isLarge: true, isPrimary: true, leadingIcon: isPrimer ? (_jsx(PsIcon, { "aria-hidden": 'true', className: 'size-4', iconKey: PsIconKey.Camera })) : undefined, onClick: onAllow, children: isPrimer ? labels.allowCamera : labels.tryAgain })) : null, _jsx(PsScanPhotoButton, { className: 'w-full', isMultiple: isMultiple, label: labels.choosePhoto, onFiles: onFiles })] })] }) }));
43
+ return (_jsx("div", { className: 'flex h-full min-h-0 flex-col overflow-y-auto', children: _jsxs("div", { className: 'm-auto flex w-full max-w-sm flex-col items-center gap-6 px-6 pb-[max(1.5rem,env(safe-area-inset-bottom))] pt-20 text-center animate-ps-slide-up', children: [isPrimer ? (_jsx(PsScanIllustration, {})) : (_jsx("span", { className: 'flex size-20 items-center justify-center rounded-full bg-white/10 text-white', children: _jsx(PsIcon, { "aria-hidden": 'true', className: 'size-9', iconKey: screen.iconKey }) })), _jsxs("div", { className: 'flex flex-col gap-2', children: [_jsx("h2", { className: 'text-xl font-semibold leading-tight text-white sm:text-2xl', children: screen.title }), _jsx("p", { className: 'text-sm leading-6 text-white/70', children: screen.body })] }), status === PsScanCameraStatus.Denied ? _jsx(PsScanDeniedSteps, { labels: labels }) : null, _jsxs("div", { className: 'flex w-full flex-col gap-3', children: [isPrimer || canRetry ? (_jsx(PsScanButton, { className: 'w-full', isLarge: true, isPrimary: true, leadingIcon: isPrimer ? (_jsx(PsIcon, { "aria-hidden": 'true', className: 'size-4', iconKey: PsIconKey.Camera })) : undefined, onClick: onAllow, children: isPrimer ? labels.allowCamera : labels.tryAgain })) : null, _jsx(PsScanPhotoButton, { className: 'w-full', label: labels.choosePhoto, onFile: onFile })] })] }) }));
44
44
  };
@@ -3,14 +3,14 @@ import { useRef } from 'react';
3
3
  import { PsIcon, PsIconKey } from '@pallasoft/ps-icons';
4
4
  import { PsScanButton } from './ps-scan-button';
5
5
  import { PsScanIconButton } from './ps-scan-icon-button';
6
- export const PsScanPhotoButton = ({ className, isIconOnly = false, isMultiple, label, onFiles, }) => {
6
+ export const PsScanPhotoButton = ({ className, isIconOnly = false, label, onFile, }) => {
7
7
  const input = useRef(null);
8
8
  const open = () => input.current?.click();
9
9
  const onChange = (event) => {
10
- const files = Array.from(event.target.files === null ? [] : event.target.files);
10
+ const file = event.target.files === null ? undefined : event.target.files[0];
11
11
  event.target.value = '';
12
- if (files.length > 0)
13
- onFiles(files);
12
+ if (file !== undefined)
13
+ onFile(file);
14
14
  };
15
- return (_jsxs(_Fragment, { children: [isIconOnly ? (_jsx(PsScanIconButton, { className: className, iconKey: PsIconKey.Image, label: label, onClick: open })) : (_jsx(PsScanButton, { className: className, isLarge: true, leadingIcon: _jsx(PsIcon, { "aria-hidden": 'true', className: 'size-4', iconKey: PsIconKey.Image }), onClick: open, children: label })), _jsx("input", { accept: 'image/*', "aria-hidden": 'true', className: 'sr-only', multiple: isMultiple, onChange: onChange, ref: input, tabIndex: -1, type: 'file' })] }));
15
+ return (_jsxs(_Fragment, { children: [isIconOnly ? (_jsx(PsScanIconButton, { className: className, iconKey: PsIconKey.Image, label: label, onClick: open })) : (_jsx(PsScanButton, { className: className, isLarge: true, leadingIcon: _jsx(PsIcon, { "aria-hidden": 'true', className: 'size-4', iconKey: PsIconKey.Image }), onClick: open, children: label })), _jsx("input", { accept: 'image/*', "aria-hidden": 'true', className: 'sr-only', onChange: onChange, ref: input, tabIndex: -1, type: 'file' })] }));
16
16
  };
@@ -0,0 +1,6 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { PsSpinner } from '@pallasoft/ps-core-components';
3
+ import { PsIcon, PsIconKey } from '@pallasoft/ps-icons';
4
+ import { PsScanButton } from './ps-scan-button';
5
+ import { PsScanIconButton } from './ps-scan-icon-button';
6
+ export const PsScanPreview = ({ error, labels, onAdjust, onRetake, onUse, result, }) => (_jsxs("div", { className: 'flex h-full min-h-0 flex-col bg-black text-white animate-ps-fade-in', children: [_jsxs("header", { className: 'grid shrink-0 grid-cols-[2.75rem_1fr_2.75rem] items-center gap-3 px-4 pb-1 pt-[max(1rem,env(safe-area-inset-top))]', children: [_jsx(PsScanIconButton, { iconKey: PsIconKey.Crop, label: labels.cropTitle, onClick: onAdjust }), _jsx("h2", { className: 'truncate text-center text-base font-semibold', children: labels.previewTitle }), _jsx("span", { "aria-hidden": 'true' })] }), _jsx("div", { className: 'flex min-h-0 flex-1 items-center justify-center px-6 py-4', children: result === null ? (_jsxs("div", { className: 'flex flex-col items-center gap-3 text-sm text-white/75', role: 'status', children: [_jsx(PsSpinner, { color: 'current', label: labels.preparing, size: 'small' }), _jsx("span", { children: labels.preparing })] })) : (_jsx("img", { alt: labels.previewAlt, className: 'max-h-full max-w-full rounded-sm bg-white object-contain shadow-[0_24px_60px_-20px_rgb(0_0_0/0.9)] animate-ps-fade-in', src: result.url })) }), error === '' ? null : (_jsx("p", { className: 'mx-auto mb-2 max-w-sm rounded-ps-control bg-danger px-3 py-2 text-center text-sm text-white', role: 'alert', children: error })), _jsxs("footer", { className: 'mx-auto grid w-full max-w-md shrink-0 grid-cols-2 gap-3 px-4 pb-[max(1.25rem,env(safe-area-inset-bottom))] pt-2', children: [_jsx(PsScanButton, { isLarge: true, onClick: onRetake, children: labels.retake }), _jsx(PsScanButton, { disabled: result === null, isLarge: true, isPrimary: true, leadingIcon: _jsx(PsIcon, { "aria-hidden": 'true', className: 'size-4', iconKey: PsIconKey.Check }), onClick: onUse, children: labels.useScan })] })] }));
@@ -1,2 +1,2 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- export const PsScanShutter = ({ disabled, label, onCapture, progress }) => (_jsxs("button", { "aria-label": label, className: 'ps-focus-ring group relative flex size-[4.75rem] shrink-0 items-center justify-center rounded-full disabled:opacity-40', disabled: disabled, onClick: onCapture, title: label, type: 'button', children: [_jsxs("svg", { "aria-hidden": 'true', className: 'absolute inset-0 size-full -rotate-90', viewBox: '0 0 76 76', children: [_jsx("circle", { className: 'stroke-white', cx: '38', cy: '38', fill: 'none', r: '35', strokeWidth: '4' }), _jsx("circle", { className: 'stroke-primary transition-[stroke-dashoffset] duration-150 ease-linear', cx: '38', cy: '38', fill: 'none', pathLength: '100', r: '35', strokeDasharray: '100', strokeDashoffset: 100 - Math.round(progress * 100), strokeLinecap: 'round', strokeWidth: '4' })] }), _jsx("span", { className: 'size-[3.75rem] rounded-full bg-white transition-transform duration-150 group-active:scale-90' })] }));
2
+ export const PsScanShutter = ({ disabled, label, onCapture }) => (_jsxs("button", { "aria-label": label, className: 'ps-focus-ring group relative flex size-[4.75rem] shrink-0 items-center justify-center rounded-full disabled:opacity-40', disabled: disabled, onClick: onCapture, title: label, type: 'button', children: [_jsx("svg", { "aria-hidden": 'true', className: 'absolute inset-0 size-full -rotate-90', viewBox: '0 0 76 76', children: _jsx("circle", { className: 'stroke-white', cx: '38', cy: '38', fill: 'none', r: '35', strokeWidth: '4' }) }), _jsx("span", { className: 'size-[3.75rem] rounded-full bg-white transition-transform duration-150 group-active:scale-90' })] }));
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { PsButton } from '@pallasoft/ps-core-components';
3
3
  import { PsModal } from '@pallasoft/ps-popovers';
4
4
  import { PsUploadLabels } from '../constants/ps-upload-labels';
@@ -8,5 +8,5 @@ export const PsUploadModal = ({ isOpen, labels, onClose, queue }) => {
8
8
  const text = { ...PsUploadLabels, ...labels };
9
9
  const summary = PsUploadQueueService.summary(queue.items);
10
10
  const title = queue.isBusy ? text.title : summary.failed > 0 ? text.titleFailed : text.titleDone;
11
- return (_jsx(PsModal, { isOpen: isOpen, onClose: onClose, size: 'md', title: title, children: _jsxs("div", { className: 'flex flex-col gap-5', children: [_jsx("ul", { className: 'flex max-h-[50vh] flex-col gap-2 overflow-y-auto', children: queue.items.map((item) => (_jsx(PsUploadRow, { item: item, labels: text, onCancel: () => queue.cancel(item.id), onRemove: () => queue.remove(item.id), onRetry: () => queue.retry(item.id) }, item.id))) }), _jsxs("div", { className: 'flex flex-wrap items-center justify-end gap-2 border-t border-line pt-4', children: [queue.isBusy ? (_jsx(PsButton, { onClick: () => queue.items.forEach((item) => queue.cancel(item.id)), type: 'button', variant: 'ghost', children: text.cancelAll })) : null, _jsx(PsButton, { onClick: onClose, type: 'button', variant: 'primary', children: text.finish })] })] }) }));
11
+ return (_jsx(PsModal, { footer: _jsxs(_Fragment, { children: [queue.isBusy ? (_jsx(PsButton, { onClick: () => queue.items.forEach((item) => queue.cancel(item.id)), type: 'button', variant: 'surface', children: text.cancelAll })) : null, _jsx(PsButton, { onClick: onClose, type: 'button', variant: 'primary', children: text.finish })] }), isOpen: isOpen, onClose: onClose, size: 'md', title: title, children: _jsx("ul", { className: 'flex flex-col gap-2', children: queue.items.map((item) => (_jsx(PsUploadRow, { item: item, labels: text, onCancel: () => queue.cancel(item.id), onRemove: () => queue.remove(item.id), onRetry: () => queue.retry(item.id) }, item.id))) }) }));
12
12
  };
@@ -1,23 +1,19 @@
1
1
  export const PsDocumentScannerLabels = {
2
- addPage: 'Add page',
3
2
  allowCamera: 'Allow camera',
4
- auto: 'Auto',
5
- autoCapture: 'Capture automatically when the page is steady',
6
3
  autoDetect: 'Detect edges',
7
4
  busyBody: 'Another app is using the camera. Close it, then try again.',
8
5
  busyTitle: 'Your camera is busy',
9
6
  cameraStarting: 'Starting camera',
10
7
  capture: 'Take photo',
11
8
  choosePhoto: 'Choose a photo instead',
12
- choosePhotos: 'Choose photos',
13
9
  close: 'Close scanner',
10
+ continue: 'Continue',
14
11
  cornerBottomLeft: 'Bottom left corner',
15
12
  cornerBottomRight: 'Bottom right corner',
16
13
  cornerTopLeft: 'Top left corner',
17
14
  cornerTopRight: 'Top right corner',
18
15
  cropHint: 'Drag the corners to the edges of the page.',
19
16
  cropTitle: 'Adjust corners',
20
- deletePage: 'Delete',
21
17
  deniedAndroid1: 'Tap the icon to the left of the address bar.',
22
18
  deniedAndroid2: 'Open Permissions, then Camera.',
23
19
  deniedAndroid3: 'Choose Allow, then come back and tap Try again.',
@@ -29,52 +25,27 @@ export const PsDocumentScannerLabels = {
29
25
  deniedIos2: 'Set Camera to Allow.',
30
26
  deniedIos3: 'Come back and tap Try again.',
31
27
  deniedTitle: 'Camera access is off',
32
- done: 'Done',
33
- dragPage: 'Drag {page} to reorder',
34
- editCrop: 'Crop',
35
28
  edgesFound: 'Page edges found.',
36
29
  edgesNotFound: 'Edges not found. Drag the corners to the page.',
37
- filterColour: 'Colour',
38
- filterDocument: 'Document',
39
- filterGreyscale: 'Greyscale',
40
- filterOriginal: 'Original',
41
- filters: 'Filter',
42
- finish: 'Upload {count} pages',
43
- finishOne: 'Upload 1 page',
44
- finishing: 'Preparing your scan',
45
30
  hintFind: 'Point your camera at a document',
46
31
  hintFound: 'Document found',
47
- hintHold: 'Hold steady',
48
32
  insecureBody: 'The camera only works on a secure (https) page. You can still choose a photo.',
49
33
  insecureTitle: 'Camera not available here',
50
- keepScan: 'Keep scan',
51
- maxPages: 'You reached the page limit.',
52
- nextPage: 'Next page',
53
- page: 'Page',
54
- pageAdded: 'Page {position} added.',
55
- pageDeleted: 'Page deleted.',
56
- pageMoved: '{page} moved to position {position} of {count}.',
57
- pageOf: '{position} of {count}',
58
- pages: 'Pages',
59
34
  platformAndroid: 'Android',
60
35
  platformDesktop: 'Computer',
61
36
  platformIos: 'iPhone',
62
- previousPage: 'Previous page',
37
+ preparing: 'Preparing your scan',
38
+ previewAlt: 'Your flattened scan',
39
+ previewTitle: 'Your scan',
63
40
  primerBody: 'The camera is only used while the scanner is open. Nothing is saved until you upload.',
64
41
  primerTitle: 'Allow camera access to scan your documents',
65
42
  processFailed: 'The scan could not be prepared. Try again.',
66
- reorderCancelled: '{page} stays where it was.',
67
- reorderDropped: '{page} dropped at {position} of {count}.',
68
- reorderInstructions: 'Press space to lift a page, the arrow keys to move it and space to drop it.',
69
- reorderLifted: '{page} lifted from {position} of {count}.',
70
- reorderMoved: '{page} is now {position} of {count}.',
71
43
  retake: 'Retake',
72
- rotate: 'Rotate',
73
44
  title: 'Scan document',
74
45
  torchOff: 'Turn flash off',
75
46
  torchOn: 'Turn flash on',
76
47
  tryAgain: 'Try again',
77
48
  unavailableBody: 'This device has no camera the browser can use. Choose a photo of the page instead.',
78
49
  unavailableTitle: 'No camera found',
79
- viewPages: 'Review {count} pages',
50
+ useScan: 'Use scan',
80
51
  };
@@ -3,10 +3,8 @@ export const PsScanChromeClasses = {
3
3
  buttonLarge: 'ps-control-large ps-control-padding-large',
4
4
  buttonPrimary: 'border-primary bg-primary text-[color:var(--ps-color-primary-contrast)] hover:border-primary-dark hover:bg-primary-dark',
5
5
  buttonQuiet: 'border-white/15 bg-white/10 text-white hover:border-white/25 hover:bg-white/20',
6
- chip: 'ps-focus-ring ps-control inline-flex min-w-0 items-center justify-center rounded-ps-control px-2 text-sm font-semibold transition',
7
6
  icon: 'ps-focus-ring ps-control ps-control-square inline-flex shrink-0 items-center justify-center rounded-ps-control text-white transition disabled:pointer-events-none disabled:opacity-40',
8
7
  iconOff: 'bg-black/45 backdrop-blur-md hover:bg-white/20',
9
8
  iconOn: 'bg-white text-black hover:bg-white/90',
10
9
  pill: 'pointer-events-none rounded-ps-pill bg-black/55 px-3.5 py-1.5 text-sm font-medium text-white backdrop-blur-md',
11
- tool: 'ps-focus-ring flex min-h-14 min-w-16 flex-col items-center justify-center gap-1 rounded-ps-control px-2 text-xs font-medium text-white/80 transition hover:bg-white/10 hover:text-white disabled:pointer-events-none disabled:opacity-40',
12
10
  };
@@ -1,48 +1,41 @@
1
1
  export const PsScanConfig = {
2
- adaptiveRatio: 0.92,
3
- adaptiveWindowDivisor: 16,
4
- adaptiveWindowMin: 15,
5
- autoCaptureMs: 1000,
6
2
  cameraPermission: 'camera',
7
- capturedJpegQuality: 0.92,
3
+ cannyHighPercentile: 0.9,
4
+ cannyLowRatio: 0.45,
5
+ cannyMinHigh: 12,
6
+ capturedJpegQuality: 0.97,
7
+ closeRadii: [1, 2, 4],
8
8
  cornerNudge: 0.005,
9
9
  cornerNudgeLarge: 0.025,
10
- decodeMaxDimension: 3000,
10
+ decodeMaxPixels: 16000000,
11
11
  defaultFileName: 'scan',
12
- defaultJpegQuality: 0.82,
13
- defaultMaxDimension: 2000,
14
- defaultMaxPages: 30,
15
- detectDilations: 2,
16
- detectInset: 3,
17
- detectMaxDimension: 320,
18
- detectMinAreaRatio: 0.12,
19
- detectMaxAreaRatio: 0.995,
20
- detectMinEdgeStrength: 24,
21
- detectMinHullFill: 0.85,
22
- detectPercentile: 0.98,
23
- detectRelativeStrength: 0.3,
12
+ defaultJpegQuality: 0.92,
13
+ defaultMaxDimension: 4000,
14
+ detectBorderMargin: 3,
15
+ detectExpand: 0.3,
16
+ detectMaxAngle: 140,
17
+ detectMaxAreaRatio: 0.95,
18
+ detectMaxDimension: 480,
19
+ detectMinAngle: 40,
20
+ detectMinAreaRatio: 0.1,
21
+ detectMinQuadFill: 0.88,
22
+ detectMinSideRatio: 0.06,
24
23
  guideArm: 36,
25
24
  guideArmRatio: 0.18,
26
25
  guideAspect: 0.72,
27
26
  guideHeightRatio: 0.58,
28
27
  guideLift: 0.03,
29
28
  guideWidthRatio: 0.76,
30
- hullCandidateLimit: 18,
31
- levelHighPercentile: 0.99,
32
- levelLowPercentile: 0.01,
33
- liveDetectIntervalMs: 160,
34
- liveDetectMaxDimension: 240,
35
- liveMissLimit: 2,
36
- liveRearmMovement: 0.08,
37
- liveSmoothing: 0.5,
38
- liveStillMovement: 0.02,
29
+ liveDeadzone: 0.006,
30
+ liveDetectIntervalMs: 200,
31
+ liveDetectMaxDimension: 280,
32
+ liveMissLimit: 3,
33
+ liveSmoothing: 0.45,
39
34
  loupeOffset: 72,
40
35
  loupeSize: 112,
41
36
  loupeZoom: 2.5,
42
37
  minCornerGap: 0.02,
43
- pdfDpi: 150,
44
- pdfPointsPerInch: 72,
45
- previewDebounceMs: 120,
46
- previewMaxDimension: 1400,
38
+ polygonEpsilons: [0.008, 0.012, 0.018, 0.026, 0.036, 0.05, 0.07],
47
39
  torchConstraint: 'torch',
40
+ warpChunkPixels: 400000,
48
41
  };
@@ -1,5 +1,5 @@
1
1
  export const PsScanStep = {
2
2
  Camera: 'camera',
3
3
  Crop: 'crop',
4
- Review: 'review',
4
+ Preview: 'preview',
5
5
  };
@@ -1,45 +1,41 @@
1
- import { useEffect, useRef, useState } from 'react';
1
+ import { useEffect, useState } from 'react';
2
2
  import { PsScanConfig } from '../constants/ps-scan-config';
3
3
  import { PsScanCanvasService } from '../services/ps-scan-canvas-service';
4
4
  import { PsScanEdgeService } from '../services/ps-scan-edge-service';
5
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;
6
+ export const usePsScanLiveDetection = (videoRef, isActive) => {
7
+ const [detection, setDetection] = useState({ quad: null });
12
8
  useEffect(() => {
13
9
  if (!isActive) {
14
- setDetection({ progress: 0, quad: null });
10
+ setDetection({ quad: null });
15
11
  return;
16
12
  }
13
+ let isCancelled = false;
17
14
  let state = PsScanStabilityService.initial();
18
- let last = performance.now();
19
15
  let timer;
20
- const tick = () => {
16
+ const tick = async () => {
21
17
  const video = videoRef.current;
22
- const now = performance.now();
23
18
  if (video !== null && video.videoWidth > 0 && document.visibilityState === 'visible') {
24
19
  try {
25
20
  const raster = PsScanCanvasService.sample(video, PsScanConfig.liveDetectMaxDimension);
26
- state = PsScanStabilityService.next(state, PsScanEdgeService.detect(raster), now - last);
21
+ const quad = await PsScanEdgeService.detect(raster, PsScanConfig.liveDetectMaxDimension);
22
+ if (isCancelled)
23
+ return;
24
+ state = PsScanStabilityService.next(state, quad);
27
25
  }
28
26
  catch {
29
27
  state = PsScanStabilityService.initial();
30
28
  }
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
- }
29
+ setDetection((current) => (current.quad === state.quad ? current : { quad: state.quad }));
37
30
  }
38
- last = now;
39
- timer = setTimeout(tick, PsScanConfig.liveDetectIntervalMs);
31
+ if (!isCancelled)
32
+ timer = setTimeout(tick, PsScanConfig.liveDetectIntervalMs);
40
33
  };
41
34
  timer = setTimeout(tick, PsScanConfig.liveDetectIntervalMs);
42
- return () => clearTimeout(timer);
35
+ return () => {
36
+ isCancelled = true;
37
+ clearTimeout(timer);
38
+ };
43
39
  }, [isActive]);
44
40
  return detection;
45
41
  };
@@ -0,0 +1,42 @@
1
+ import { useCallback, useEffect, useRef, useState } from 'react';
2
+ import { PsScanPipelineService } from '../services/ps-scan-pipeline-service';
3
+ export const usePsScanPhoto = () => {
4
+ const [photo, setPhoto] = useState(null);
5
+ const url = useRef('');
6
+ const release = useCallback(() => {
7
+ if (url.current !== '')
8
+ URL.revokeObjectURL(url.current);
9
+ url.current = '';
10
+ }, []);
11
+ useEffect(() => release, [release]);
12
+ const load = useCallback(async (source) => {
13
+ const prepared = await PsScanPipelineService.prepare(source);
14
+ const next = {
15
+ corners: prepared.corners,
16
+ height: prepared.height,
17
+ isDetected: prepared.isDetected,
18
+ source,
19
+ sourceUrl: URL.createObjectURL(source),
20
+ width: prepared.width,
21
+ };
22
+ release();
23
+ url.current = next.sourceUrl;
24
+ setPhoto(next);
25
+ return next;
26
+ }, [release]);
27
+ const setCorners = useCallback((corners) => setPhoto((current) => (current === null ? current : { ...current, corners })), []);
28
+ const detectAgain = useCallback(async () => {
29
+ if (photo === null)
30
+ return false;
31
+ const prepared = await PsScanPipelineService.prepare(photo.source);
32
+ setPhoto((current) => current === null
33
+ ? current
34
+ : { ...current, corners: prepared.corners, isDetected: prepared.isDetected });
35
+ return prepared.isDetected;
36
+ }, [photo]);
37
+ const clear = useCallback(() => {
38
+ release();
39
+ setPhoto(null);
40
+ }, [release]);
41
+ return { clear, detectAgain, load, photo, setCorners };
42
+ };
package/dist/esm/index.js CHANGED
@@ -71,7 +71,5 @@ export * from './hooks/use-ps-in-view';
71
71
  export * from './hooks/use-ps-upload-queue';
72
72
  export * from './components/ps-document-scanner';
73
73
  export * from './constants/ps-document-scanner-labels';
74
- export * from './constants/ps-scan-filter';
75
- export * from './constants/ps-scan-output-format';
76
74
  export * from './models/ps-document-scanner-props';
77
75
  export * from './models/ps-file-picker-scan';
@@ -51,8 +51,8 @@ export class PsScanCameraService {
51
51
  audio: false,
52
52
  video: {
53
53
  facingMode: { ideal: 'environment' },
54
- height: { ideal: 1440 },
55
- width: { ideal: 1920 },
54
+ height: { ideal: 2160 },
55
+ width: { ideal: 3840 },
56
56
  },
57
57
  });
58
58
  const track = stream.getVideoTracks()[0];
@@ -0,0 +1,93 @@
1
+ import { PsScanConfig } from '../constants/ps-scan-config';
2
+ export class PsScanCannyService {
3
+ static edges(gray, width, height) {
4
+ const gradient = PsScanCannyService.gradient(gray, width, height);
5
+ const ridges = PsScanCannyService.suppress(gradient, width, height);
6
+ const high = PsScanCannyService.highThreshold(ridges);
7
+ return PsScanCannyService.hysteresis(ridges, width, height, high * PsScanConfig.cannyLowRatio, high);
8
+ }
9
+ static gradient(gray, width, height) {
10
+ const magnitude = new Float32Array(width * height);
11
+ const direction = new Uint8Array(width * height);
12
+ for (let y = 1; y < height - 1; y += 1)
13
+ for (let x = 1; x < width - 1; x += 1) {
14
+ const i = y * width + x;
15
+ const gx = -gray[i - width - 1] +
16
+ gray[i - width + 1] -
17
+ 2 * gray[i - 1] +
18
+ 2 * gray[i + 1] -
19
+ gray[i + width - 1] +
20
+ gray[i + width + 1];
21
+ const gy = -gray[i - width - 1] -
22
+ 2 * gray[i - width] -
23
+ gray[i - width + 1] +
24
+ gray[i + width - 1] +
25
+ 2 * gray[i + width] +
26
+ gray[i + width + 1];
27
+ const ax = Math.abs(gx);
28
+ const ay = Math.abs(gy);
29
+ magnitude[i] = Math.hypot(gx, gy) / 4;
30
+ if (ay <= ax * 0.41421356)
31
+ direction[i] = 0;
32
+ else if (ay >= ax * 2.41421356)
33
+ direction[i] = 2;
34
+ else
35
+ direction[i] = gx * gy > 0 ? 1 : 3;
36
+ }
37
+ return { direction, magnitude };
38
+ }
39
+ static highThreshold(ridges) {
40
+ const values = [];
41
+ for (let index = 0; index < ridges.length; index += 1)
42
+ if (ridges[index] > 0)
43
+ values.push(ridges[index]);
44
+ if (values.length === 0)
45
+ return PsScanConfig.cannyMinHigh;
46
+ values.sort((a, b) => a - b);
47
+ const at = Math.min(values.length - 1, Math.floor(values.length * PsScanConfig.cannyHighPercentile));
48
+ return Math.max(PsScanConfig.cannyMinHigh, values[at]);
49
+ }
50
+ static hysteresis(ridges, width, height, low, high) {
51
+ const out = new Uint8Array(width * height);
52
+ const stack = [];
53
+ for (let index = 0; index < ridges.length; index += 1)
54
+ if (ridges[index] >= high) {
55
+ out[index] = 1;
56
+ stack.push(index);
57
+ }
58
+ while (stack.length > 0) {
59
+ const index = stack.pop();
60
+ if (index === undefined)
61
+ break;
62
+ const x = index % width;
63
+ const y = (index - x) / width;
64
+ for (let dy = -1; dy <= 1; dy += 1)
65
+ for (let dx = -1; dx <= 1; dx += 1) {
66
+ const nx = x + dx;
67
+ const ny = y + dy;
68
+ if (nx < 0 || ny < 0 || nx >= width || ny >= height)
69
+ continue;
70
+ const next = ny * width + nx;
71
+ if (out[next] === 0 && ridges[next] >= low) {
72
+ out[next] = 1;
73
+ stack.push(next);
74
+ }
75
+ }
76
+ }
77
+ return out;
78
+ }
79
+ static suppress(gradient, width, height) {
80
+ const { direction, magnitude } = gradient;
81
+ const out = new Float32Array(width * height);
82
+ const offsets = [1, width + 1, width, width - 1];
83
+ for (let y = 1; y < height - 1; y += 1)
84
+ for (let x = 1; x < width - 1; x += 1) {
85
+ const i = y * width + x;
86
+ const offset = offsets[direction[i]];
87
+ const value = magnitude[i];
88
+ if (value >= magnitude[i - offset] && value > magnitude[i + offset])
89
+ out[i] = value;
90
+ }
91
+ return out;
92
+ }
93
+ }
@@ -1,8 +1,14 @@
1
+ import { PsScanConfig } from '../constants/ps-scan-config';
1
2
  import { PsScanGeometryService } from './ps-scan-geometry-service';
2
3
  export class PsScanCanvasService {
3
4
  static async decode(blob, maxDimension) {
4
5
  const bitmap = await createImageBitmap(blob, { imageOrientation: 'from-image' });
5
- const size = PsScanGeometryService.fitWithin({ height: bitmap.height, width: bitmap.width }, maxDimension);
6
+ const capped = PsScanGeometryService.fitWithin({ height: bitmap.height, width: bitmap.width }, maxDimension);
7
+ const shrink = Math.min(1, Math.sqrt(PsScanConfig.decodeMaxPixels / (capped.width * capped.height)));
8
+ const size = {
9
+ height: Math.max(1, Math.round(capped.height * shrink)),
10
+ width: Math.max(1, Math.round(capped.width * shrink)),
11
+ };
6
12
  const context = PsScanCanvasService.context(size.width, size.height);
7
13
  context.drawImage(bitmap, 0, 0, size.width, size.height);
8
14
  const naturalHeight = bitmap.height;