@pallasoft/ps-file-components 2.0.1 → 2.2.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 (97) 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 +80 -9
  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-image-viewer.js +35 -16
  7. package/dist/components/ps-pdf-viewer.js +18 -1
  8. package/dist/components/ps-scan-camera-view.js +2 -2
  9. package/dist/components/ps-scan-page-strip.d.ts +8 -0
  10. package/dist/components/ps-scan-page-strip.js +8 -0
  11. package/dist/components/ps-scan-preview.d.ts +8 -2
  12. package/dist/components/ps-scan-preview.js +6 -1
  13. package/dist/components/ps-scan-viewfinder-overlay.d.ts +4 -2
  14. package/dist/components/ps-scan-viewfinder-overlay.js +6 -6
  15. package/dist/constants/ps-document-scanner-labels.d.ts +2 -1
  16. package/dist/constants/ps-document-scanner-labels.js +2 -1
  17. package/dist/constants/ps-file-viewer-config.d.ts +1 -0
  18. package/dist/constants/ps-file-viewer-config.js +1 -0
  19. package/dist/constants/ps-scan-config.d.ts +10 -3
  20. package/dist/constants/ps-scan-config.js +11 -3
  21. package/dist/constants/ps-scan-detect-profiles.d.ts +10 -0
  22. package/dist/constants/ps-scan-detect-profiles.js +8 -0
  23. package/dist/constants/ps-scan-format.d.ts +5 -0
  24. package/dist/constants/ps-scan-format.js +7 -0
  25. package/dist/esm/components/ps-document-scanner-sheet.js +80 -9
  26. package/dist/esm/components/ps-document-scanner.js +2 -2
  27. package/dist/esm/components/ps-image-viewer.js +35 -16
  28. package/dist/esm/components/ps-pdf-viewer.js +18 -1
  29. package/dist/esm/components/ps-scan-camera-view.js +2 -2
  30. package/dist/esm/components/ps-scan-page-strip.js +4 -0
  31. package/dist/esm/components/ps-scan-preview.js +6 -1
  32. package/dist/esm/components/ps-scan-viewfinder-overlay.js +6 -6
  33. package/dist/esm/constants/ps-document-scanner-labels.js +2 -1
  34. package/dist/esm/constants/ps-file-viewer-config.js +1 -0
  35. package/dist/esm/constants/ps-scan-config.js +11 -3
  36. package/dist/esm/constants/ps-scan-detect-profiles.js +5 -0
  37. package/dist/esm/constants/ps-scan-format.js +4 -0
  38. package/dist/esm/hooks/use-ps-drag-pan.js +66 -0
  39. package/dist/esm/hooks/use-ps-scan-live-detection.js +47 -22
  40. package/dist/esm/hooks/use-ps-scan-quad-tween.js +34 -0
  41. package/dist/esm/index.js +5 -0
  42. package/dist/esm/models/ps-document-scanner-page-labels.js +1 -0
  43. package/dist/esm/models/ps-drag-pan-handlers.js +1 -0
  44. package/dist/esm/models/ps-drag-pan-options.js +1 -0
  45. package/dist/esm/models/ps-scan-detect-profile.js +1 -0
  46. package/dist/esm/models/ps-scan-multi-page.js +1 -0
  47. package/dist/esm/models/ps-scan-page.js +1 -0
  48. package/dist/esm/models/ps-scan-pdf-page.js +1 -0
  49. package/dist/esm/services/ps-pan-service.js +13 -0
  50. package/dist/esm/services/ps-scan-canvas-service.js +10 -0
  51. package/dist/esm/services/ps-scan-edge-service.js +7 -4
  52. package/dist/esm/services/ps-scan-file-name-service.js +8 -1
  53. package/dist/esm/services/ps-scan-frame-service.js +10 -0
  54. package/dist/esm/services/ps-scan-output-service.js +23 -0
  55. package/dist/esm/services/ps-scan-pdf-service.js +20 -0
  56. package/dist/esm/services/ps-scan-quad-tween-service.js +10 -0
  57. package/dist/hooks/use-ps-drag-pan.d.ts +3 -0
  58. package/dist/hooks/use-ps-drag-pan.js +70 -0
  59. package/dist/hooks/use-ps-scan-live-detection.js +46 -21
  60. package/dist/hooks/use-ps-scan-quad-tween.d.ts +3 -0
  61. package/dist/hooks/use-ps-scan-quad-tween.js +38 -0
  62. package/dist/index.d.ts +5 -0
  63. package/dist/index.js +5 -0
  64. package/dist/models/ps-document-scanner-page-labels.d.ts +6 -0
  65. package/dist/models/ps-document-scanner-page-labels.js +2 -0
  66. package/dist/models/ps-document-scanner-props.d.ts +5 -0
  67. package/dist/models/ps-document-scanner-sheet-props.d.ts +4 -0
  68. package/dist/models/ps-drag-pan-handlers.d.ts +8 -0
  69. package/dist/models/ps-drag-pan-handlers.js +2 -0
  70. package/dist/models/ps-drag-pan-options.d.ts +9 -0
  71. package/dist/models/ps-drag-pan-options.js +2 -0
  72. package/dist/models/ps-scan-detect-profile.d.ts +4 -0
  73. package/dist/models/ps-scan-detect-profile.js +2 -0
  74. package/dist/models/ps-scan-live-detection.d.ts +3 -1
  75. package/dist/models/ps-scan-multi-page.d.ts +9 -0
  76. package/dist/models/ps-scan-multi-page.js +2 -0
  77. package/dist/models/ps-scan-page.d.ts +4 -0
  78. package/dist/models/ps-scan-page.js +2 -0
  79. package/dist/models/ps-scan-pdf-page.d.ts +5 -0
  80. package/dist/models/ps-scan-pdf-page.js +2 -0
  81. package/dist/services/ps-pan-service.d.ts +6 -0
  82. package/dist/services/ps-pan-service.js +17 -0
  83. package/dist/services/ps-scan-canvas-service.d.ts +1 -0
  84. package/dist/services/ps-scan-canvas-service.js +10 -0
  85. package/dist/services/ps-scan-edge-service.d.ts +2 -1
  86. package/dist/services/ps-scan-edge-service.js +7 -4
  87. package/dist/services/ps-scan-file-name-service.d.ts +2 -0
  88. package/dist/services/ps-scan-file-name-service.js +8 -1
  89. package/dist/services/ps-scan-frame-service.d.ts +3 -0
  90. package/dist/services/ps-scan-frame-service.js +14 -0
  91. package/dist/services/ps-scan-output-service.d.ts +6 -0
  92. package/dist/services/ps-scan-output-service.js +27 -0
  93. package/dist/services/ps-scan-pdf-service.d.ts +8 -0
  94. package/dist/services/ps-scan-pdf-service.js +24 -0
  95. package/dist/services/ps-scan-quad-tween-service.d.ts +4 -0
  96. package/dist/services/ps-scan-quad-tween-service.js +14 -0
  97. package/package.json +2 -2
@@ -1 +1 @@
1
- export declare const PsDocumentScannerLoader: import("react").LazyExoticComponent<({ fileName, jpegQuality, labels, maxDimension, onClose, onComplete, }: import("../models/ps-document-scanner-sheet-props").PsDocumentScannerSheetProps) => import("react").ReactPortal | null>;
1
+ export declare const PsDocumentScannerLoader: import("react").LazyExoticComponent<({ fileName, formats, jpegQuality, labels, maxDimension, onClose, onComplete, pageLabels, }: import("../models/ps-document-scanner-sheet-props").PsDocumentScannerSheetProps) => import("react").ReactPortal | null>;
@@ -1,2 +1,2 @@
1
1
  import type { PsDocumentScannerSheetProps } from '../models/ps-document-scanner-sheet-props';
2
- export declare const PsDocumentScannerSheet: ({ fileName, jpegQuality, labels, maxDimension, onClose, onComplete, }: PsDocumentScannerSheetProps) => import("react").ReactPortal | null;
2
+ export declare const PsDocumentScannerSheet: ({ fileName, formats, jpegQuality, labels, maxDimension, onClose, onComplete, pageLabels, }: PsDocumentScannerSheetProps) => import("react").ReactPortal | null;
@@ -8,25 +8,31 @@ const ps_design_system_1 = require("@pallasoft/ps-design-system");
8
8
  const ps_icons_1 = require("@pallasoft/ps-icons");
9
9
  const react_dom_1 = require("react-dom");
10
10
  const ps_scan_camera_status_1 = require("../constants/ps-scan-camera-status");
11
+ const ps_scan_config_1 = require("../constants/ps-scan-config");
11
12
  const ps_scan_step_1 = require("../constants/ps-scan-step");
12
13
  const use_ps_focus_trap_1 = require("../hooks/use-ps-focus-trap");
13
14
  const use_ps_scan_camera_1 = require("../hooks/use-ps-scan-camera");
14
15
  const use_ps_scan_live_detection_1 = require("../hooks/use-ps-scan-live-detection");
15
16
  const use_ps_scan_photo_1 = require("../hooks/use-ps-scan-photo");
16
- const ps_scan_file_name_service_1 = require("../services/ps-scan-file-name-service");
17
+ const ps_scan_output_service_1 = require("../services/ps-scan-output-service");
17
18
  const ps_scan_pipeline_service_1 = require("../services/ps-scan-pipeline-service");
18
19
  const ps_scan_camera_view_1 = require("./ps-scan-camera-view");
19
20
  const ps_scan_crop_editor_1 = require("./ps-scan-crop-editor");
20
21
  const ps_scan_icon_button_1 = require("./ps-scan-icon-button");
22
+ const ps_scan_page_strip_1 = require("./ps-scan-page-strip");
21
23
  const ps_scan_permission_view_1 = require("./ps-scan-permission-view");
22
24
  const ps_scan_preview_1 = require("./ps-scan-preview");
23
- const PsDocumentScannerSheet = ({ fileName, jpegQuality, labels, maxDimension, onClose, onComplete, }) => {
25
+ const PsDocumentScannerSheet = ({ fileName, formats, jpegQuality, labels, maxDimension, onClose, onComplete, pageLabels, }) => {
24
26
  const tier = (0, ps_core_components_1.usePsViewport)();
25
27
  const zIndex = (0, ps_core_components_1.usePsLayerZIndex)(ps_design_system_1.PallasoftZIndex.Modal);
26
28
  const content = (0, react_1.useRef)(null);
27
29
  const { clear, detectAgain, load, photo, setCorners } = (0, use_ps_scan_photo_1.usePsScanPhoto)();
28
30
  const [step, setStep] = (0, react_1.useState)(ps_scan_step_1.PsScanStep.Camera);
29
31
  const [result, setResult] = (0, react_1.useState)(null);
32
+ const [pages, setPages] = (0, react_1.useState)([]);
33
+ const [isAddingPage, setIsAddingPage] = (0, react_1.useState)(false);
34
+ const pagesRef = (0, react_1.useRef)([]);
35
+ const nextPageId = (0, react_1.useRef)(1);
30
36
  const [announcement, setAnnouncement] = (0, react_1.useState)('');
31
37
  const [error, setError] = (0, react_1.useState)('');
32
38
  const [isCapturing, setIsCapturing] = (0, react_1.useState)(false);
@@ -44,6 +50,12 @@ const PsDocumentScannerSheet = ({ fileName, jpegQuality, labels, maxDimension, o
44
50
  if (result !== null)
45
51
  URL.revokeObjectURL(result.url);
46
52
  }, [result]);
53
+ (0, react_1.useEffect)(() => {
54
+ pagesRef.current = pages;
55
+ }, [pages]);
56
+ (0, react_1.useEffect)(() => () => {
57
+ pagesRef.current.forEach((page) => URL.revokeObjectURL(page.url));
58
+ }, []);
47
59
  const edgeMessage = (isDetected) => isDetected ? labels.edgesFound : labels.edgesNotFound;
48
60
  const accept = async (source) => {
49
61
  setError('');
@@ -97,20 +109,79 @@ const PsDocumentScannerSheet = ({ fileName, jpegQuality, labels, maxDimension, o
97
109
  setError(labels.processFailed);
98
110
  }
99
111
  };
100
- const use = () => {
101
- if (result === null)
112
+ const multiPageDimension = Math.min(maxDimension, ps_scan_config_1.PsScanConfig.multiPageMaxDimension);
113
+ const renderPage = async () => {
114
+ if (photo === null)
115
+ return null;
116
+ const rendered = await ps_scan_pipeline_service_1.PsScanPipelineService.render(photo.source, photo.corners, multiPageDimension, jpegQuality);
117
+ const id = nextPageId.current;
118
+ nextPageId.current += 1;
119
+ return { ...rendered, id, url: URL.createObjectURL(rendered.blob) };
120
+ };
121
+ const addPage = async () => {
122
+ if (result === null || isAddingPage)
123
+ return;
124
+ setIsAddingPage(true);
125
+ setError('');
126
+ try {
127
+ const page = await renderPage();
128
+ if (page === null)
129
+ return;
130
+ setPages((current) => [...current, page]);
131
+ clear();
132
+ setResult(null);
133
+ setStep(ps_scan_step_1.PsScanStep.Camera);
134
+ }
135
+ catch {
136
+ setError(labels.processFailed);
137
+ }
138
+ finally {
139
+ setIsAddingPage(false);
140
+ }
141
+ };
142
+ const removePage = (id) => {
143
+ const removed = pages.find((page) => page.id === id);
144
+ if (removed !== undefined)
145
+ URL.revokeObjectURL(removed.url);
146
+ setPages((current) => current.filter((page) => page.id !== id));
147
+ };
148
+ const save = async (format) => {
149
+ if (result === null || isAddingPage)
102
150
  return;
103
- onComplete([
104
- new File([result.blob], ps_scan_file_name_service_1.PsScanFileNameService.build(fileName), { type: 'image/jpeg' }),
105
- ]);
106
- onClose();
151
+ setIsAddingPage(true);
152
+ setError('');
153
+ try {
154
+ const last = pages.length === 0 ? { ...result, id: 0 } : await renderPage();
155
+ if (last === null)
156
+ return;
157
+ const files = await ps_scan_output_service_1.PsScanOutputService.build([...pages, last], format, fileName);
158
+ if (pages.length > 0)
159
+ URL.revokeObjectURL(last.url);
160
+ onComplete(files);
161
+ onClose();
162
+ }
163
+ catch {
164
+ setError(labels.processFailed);
165
+ }
166
+ finally {
167
+ setIsAddingPage(false);
168
+ }
107
169
  };
170
+ const multiPage = pageLabels === null
171
+ ? undefined
172
+ : {
173
+ isBusy: isAddingPage,
174
+ labels: pageLabels,
175
+ onAddPage: addPage,
176
+ onRemovePage: removePage,
177
+ pages,
178
+ };
108
179
  if (typeof document === 'undefined')
109
180
  return null;
110
181
  return (0, react_dom_1.createPortal)((0, jsx_runtime_1.jsx)("div", { "aria-label": labels.title, "aria-modal": 'true', className: isDesktop
111
182
  ? 'fixed inset-0 flex items-center justify-center bg-black/70 p-6 backdrop-blur-sm animate-ps-fade-in'
112
183
  : 'fixed inset-0 bg-black animate-ps-fade-in', role: 'dialog', style: { zIndex }, children: (0, jsx_runtime_1.jsx)(ps_core_components_1.PsLayer, { zIndex: zIndex, children: (0, jsx_runtime_1.jsxs)("div", { className: isDesktop
113
184
  ? 'relative h-[min(52rem,calc(100dvh-3rem))] w-[min(76rem,calc(100vw-3rem))] overflow-hidden rounded-2xl bg-black text-white shadow-2xl ring-1 ring-white/10 animate-ps-scale-in'
114
- : 'relative h-full overflow-hidden bg-black text-white', ref: content, children: [(0, jsx_runtime_1.jsx)("p", { "aria-live": 'polite', className: 'sr-only', role: 'status', children: announcement }), isCameraStep && showsCamera ? ((0, jsx_runtime_1.jsx)(ps_scan_camera_view_1.PsScanCameraView, { canTorch: camera.canTorch, detection: detection, flashKey: flashKey, isBusy: isCapturing, isReady: isReady, isTorchOn: camera.isTorchOn, labels: labels, onCapture: capture, onClose: onClose, onFile: accept, onToggleTorch: camera.toggleTorch, videoRef: camera.videoRef, videoTarget: camera.attachVideo })) : null, isCameraStep && !showsCamera ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(ps_scan_permission_view_1.PsScanPermissionView, { labels: labels, onAllow: camera.allow, onFile: accept, status: camera.status }), (0, jsx_runtime_1.jsx)("div", { className: 'absolute left-4 top-[max(1rem,env(safe-area-inset-top))] flex gap-2', children: (0, jsx_runtime_1.jsx)(ps_scan_icon_button_1.PsScanIconButton, { iconKey: ps_icons_1.PsIconKey.Close, label: labels.close, onClick: onClose }) })] })) : null, step === ps_scan_step_1.PsScanStep.Crop && photo !== null ? ((0, jsx_runtime_1.jsx)(ps_scan_crop_editor_1.PsScanCropEditor, { labels: labels, onAutoDetect: autoDetect, onChange: setCorners, onContinue: flatten, onRetake: retake, photo: photo })) : null, step === ps_scan_step_1.PsScanStep.Preview ? ((0, jsx_runtime_1.jsx)(ps_scan_preview_1.PsScanPreview, { error: error, labels: labels, onAdjust: adjust, onRetake: retake, onUse: use, result: result })) : null, isCameraStep && error !== '' ? ((0, jsx_runtime_1.jsx)("p", { className: 'absolute inset-x-4 bottom-36 mx-auto max-w-sm rounded-ps-control bg-danger px-3 py-2 text-center text-sm text-white', role: 'alert', children: error })) : null] }) }) }), document.body);
185
+ : 'relative h-full overflow-hidden bg-black text-white', ref: content, children: [(0, jsx_runtime_1.jsx)("p", { "aria-live": 'polite', className: 'sr-only', role: 'status', children: announcement }), isCameraStep && showsCamera ? ((0, jsx_runtime_1.jsx)(ps_scan_camera_view_1.PsScanCameraView, { canTorch: camera.canTorch, detection: detection, flashKey: flashKey, isBusy: isCapturing, isReady: isReady, isTorchOn: camera.isTorchOn, labels: labels, onCapture: capture, onClose: onClose, onFile: accept, onToggleTorch: camera.toggleTorch, videoRef: camera.videoRef, videoTarget: camera.attachVideo })) : null, isCameraStep && !showsCamera ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(ps_scan_permission_view_1.PsScanPermissionView, { labels: labels, onAllow: camera.allow, onFile: accept, status: camera.status }), (0, jsx_runtime_1.jsx)("div", { className: 'absolute left-4 top-[max(1rem,env(safe-area-inset-top))] flex gap-2', children: (0, jsx_runtime_1.jsx)(ps_scan_icon_button_1.PsScanIconButton, { iconKey: ps_icons_1.PsIconKey.Close, label: labels.close, onClick: onClose }) })] })) : null, step === ps_scan_step_1.PsScanStep.Crop && photo !== null ? ((0, jsx_runtime_1.jsx)(ps_scan_crop_editor_1.PsScanCropEditor, { labels: labels, onAutoDetect: autoDetect, onChange: setCorners, onContinue: flatten, onRetake: retake, photo: photo })) : null, step === ps_scan_step_1.PsScanStep.Preview ? ((0, jsx_runtime_1.jsx)(ps_scan_preview_1.PsScanPreview, { error: error, isSaving: isAddingPage, labels: labels, multiPage: multiPage, onAdjust: adjust, onRetake: retake, onSave: save, formats: formats, preferredFormat: ps_scan_output_service_1.PsScanOutputService.preferred(pages.length + 1, formats), result: result })) : null, isCameraStep && pageLabels !== null && pages.length > 0 ? ((0, jsx_runtime_1.jsx)("div", { className: 'absolute inset-x-0 bottom-32 flex justify-center', children: (0, jsx_runtime_1.jsx)(ps_scan_page_strip_1.PsScanPageStrip, { labels: pageLabels, onRemove: removePage, pages: pages }) })) : null, isCameraStep && error !== '' ? ((0, jsx_runtime_1.jsx)("p", { className: 'absolute inset-x-4 bottom-36 mx-auto max-w-sm rounded-ps-control bg-danger px-3 py-2 text-center text-sm text-white', role: 'alert', children: error })) : null] }) }) }), document.body);
115
186
  };
116
187
  exports.PsDocumentScannerSheet = PsDocumentScannerSheet;
@@ -1,2 +1,2 @@
1
1
  import type { PsDocumentScannerProps } from '../models/ps-document-scanner-props';
2
- export declare const PsDocumentScanner: ({ fileName, isOpen, jpegQuality, labels, maxDimension, onClose, onComplete, }: PsDocumentScannerProps) => import("react").JSX.Element | null;
2
+ export declare const PsDocumentScanner: ({ fileName, formats, isMultiPage, isOpen, jpegQuality, labels, maxDimension, onClose, onComplete, pageLabels, }: PsDocumentScannerProps) => import("react").JSX.Element | null;
@@ -7,10 +7,10 @@ const ps_core_components_1 = require("@pallasoft/ps-core-components");
7
7
  const ps_document_scanner_labels_1 = require("../constants/ps-document-scanner-labels");
8
8
  const ps_scan_config_1 = require("../constants/ps-scan-config");
9
9
  const ps_document_scanner_loader_1 = require("./ps-document-scanner-loader");
10
- const PsDocumentScanner = ({ fileName = ps_scan_config_1.PsScanConfig.defaultFileName, isOpen, jpegQuality = ps_scan_config_1.PsScanConfig.defaultJpegQuality, labels, maxDimension = ps_scan_config_1.PsScanConfig.defaultMaxDimension, onClose, onComplete, }) => {
10
+ const PsDocumentScanner = ({ fileName = ps_scan_config_1.PsScanConfig.defaultFileName, formats = ps_scan_config_1.PsScanConfig.formats, isMultiPage = false, isOpen, jpegQuality = ps_scan_config_1.PsScanConfig.defaultJpegQuality, labels, maxDimension = ps_scan_config_1.PsScanConfig.defaultMaxDimension, onClose, onComplete, pageLabels, }) => {
11
11
  const isHydrated = (0, ps_core_components_1.usePsHydrated)();
12
12
  if (!isOpen || !isHydrated)
13
13
  return null;
14
- return ((0, jsx_runtime_1.jsx)(react_1.Suspense, { fallback: null, children: (0, jsx_runtime_1.jsx)(ps_document_scanner_loader_1.PsDocumentScannerLoader, { fileName: fileName, jpegQuality: jpegQuality, labels: { ...ps_document_scanner_labels_1.PsDocumentScannerLabels, ...labels }, maxDimension: maxDimension, onClose: onClose, onComplete: onComplete }) }));
14
+ return ((0, jsx_runtime_1.jsx)(react_1.Suspense, { fallback: null, children: (0, jsx_runtime_1.jsx)(ps_document_scanner_loader_1.PsDocumentScannerLoader, { fileName: fileName, formats: formats, jpegQuality: jpegQuality, labels: { ...ps_document_scanner_labels_1.PsDocumentScannerLabels, ...labels }, maxDimension: maxDimension, onClose: onClose, onComplete: onComplete, pageLabels: isMultiPage && pageLabels !== undefined ? pageLabels : null }) }));
15
15
  };
16
16
  exports.PsDocumentScanner = PsDocumentScanner;
@@ -3,31 +3,50 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.PsImageViewer = void 0;
4
4
  const jsx_runtime_1 = require("react/jsx-runtime");
5
5
  const react_1 = require("react");
6
+ const use_ps_drag_pan_1 = require("../hooks/use-ps-drag-pan");
7
+ const ps_pan_service_1 = require("../services/ps-pan-service");
6
8
  const PsImageViewer = ({ file, labels, rotation, zoom }) => {
7
9
  const [offset, setOffset] = (0, react_1.useState)({ x: 0, y: 0 });
8
10
  const [hasFailed, setHasFailed] = (0, react_1.useState)(false);
9
- const drag = (0, react_1.useRef)(null);
11
+ const pan = (0, react_1.useRef)(null);
12
+ const base = (0, react_1.useRef)({ x: 0, y: 0 });
13
+ const frame = (0, react_1.useRef)(0);
14
+ const pending = (0, react_1.useRef)({ x: 0, y: 0 });
10
15
  (0, react_1.useEffect)(() => {
11
16
  setOffset({ x: 0, y: 0 });
12
17
  setHasFailed(false);
13
18
  }, [file.id, zoom === 1]);
14
- const onPointerDown = (event) => {
15
- if (zoom <= 1)
16
- return;
17
- drag.current = { x: event.clientX - offset.x, y: event.clientY - offset.y };
18
- event.currentTarget.setPointerCapture(event.pointerId);
19
- };
20
- const onPointerMove = (event) => {
21
- if (drag.current === null)
22
- return;
23
- setOffset({ x: event.clientX - drag.current.x, y: event.clientY - drag.current.y });
19
+ (0, react_1.useEffect)(() => () => cancelAnimationFrame(frame.current), []);
20
+ const write = (next) => {
21
+ if (pan.current !== null)
22
+ pan.current.style.transform = ps_pan_service_1.PsPanService.translate(next);
24
23
  };
24
+ const handlers = (0, use_ps_drag_pan_1.usePsDragPan)({
25
+ activationDistance: 0,
26
+ isEnabled: zoom > 1,
27
+ isMouseOnly: false,
28
+ onEnd: (delta) => {
29
+ cancelAnimationFrame(frame.current);
30
+ frame.current = 0;
31
+ const next = ps_pan_service_1.PsPanService.add(base.current, delta);
32
+ write(next);
33
+ setOffset(next);
34
+ },
35
+ onMove: (delta) => {
36
+ pending.current = ps_pan_service_1.PsPanService.add(base.current, delta);
37
+ if (frame.current !== 0)
38
+ return;
39
+ frame.current = requestAnimationFrame(() => {
40
+ frame.current = 0;
41
+ write(pending.current);
42
+ });
43
+ },
44
+ onStart: () => {
45
+ base.current = offset;
46
+ },
47
+ });
25
48
  if (hasFailed)
26
49
  return (0, jsx_runtime_1.jsx)("p", { className: 'm-auto text-sm text-white/70', children: labels.loadFailed });
27
- return ((0, jsx_runtime_1.jsx)("div", { className: `flex h-full w-full touch-none items-center justify-center overflow-hidden ${zoom > 1 ? 'cursor-grab active:cursor-grabbing' : ''}`, onPointerDown: onPointerDown, onPointerMove: onPointerMove, onPointerUp: () => {
28
- drag.current = null;
29
- }, children: (0, jsx_runtime_1.jsx)("img", { alt: file.name, className: 'max-h-full max-w-full select-none object-contain shadow-2xl transition-transform duration-150', draggable: false, onError: () => setHasFailed(true), src: file.url, style: {
30
- transform: `translate(${offset.x}px, ${offset.y}px) scale(${zoom}) rotate(${rotation}deg)`,
31
- } }) }));
50
+ return ((0, jsx_runtime_1.jsx)("div", { className: `h-full w-full touch-none overflow-hidden ${zoom > 1 ? 'cursor-grab active:cursor-grabbing' : ''}`, ...handlers, children: (0, jsx_runtime_1.jsx)("div", { className: 'flex h-full w-full items-center justify-center', ref: pan, style: { transform: ps_pan_service_1.PsPanService.translate(offset) }, children: (0, jsx_runtime_1.jsx)("img", { alt: file.name, className: 'max-h-full max-w-full select-none object-contain shadow-2xl transition-transform duration-150', draggable: false, onError: () => setHasFailed(true), src: file.url, style: { transform: `scale(${zoom}) rotate(${rotation}deg)` } }) }) }));
32
51
  };
33
52
  exports.PsImageViewer = PsImageViewer;
@@ -5,6 +5,7 @@ const jsx_runtime_1 = require("react/jsx-runtime");
5
5
  const react_1 = require("react");
6
6
  const ps_core_components_1 = require("@pallasoft/ps-core-components");
7
7
  const ps_file_viewer_config_1 = require("../constants/ps-file-viewer-config");
8
+ const use_ps_drag_pan_1 = require("../hooks/use-ps-drag-pan");
8
9
  const ps_pdf_service_1 = require("../services/ps-pdf-service");
9
10
  const ps_pdf_page_1 = require("./ps-pdf-page");
10
11
  const ps_pdf_thumbnail_1 = require("./ps-pdf-thumbnail");
@@ -16,6 +17,22 @@ const PsPdfViewer = ({ activeMatch, file, labels, onMatchCount, pdfWorkerSrc, qu
16
17
  const [currentPage, setCurrentPage] = (0, react_1.useState)(1);
17
18
  const [frame, frameSize] = (0, ps_core_components_1.usePsElementSize)();
18
19
  const scroller = (0, react_1.useRef)(null);
20
+ const scrollStart = (0, react_1.useRef)({ x: 0, y: 0 });
21
+ const handlers = (0, use_ps_drag_pan_1.usePsDragPan)({
22
+ activationDistance: ps_file_viewer_config_1.PsFileViewerConfig.dragActivationDistance,
23
+ isEnabled: true,
24
+ isMouseOnly: true,
25
+ onEnd: () => undefined,
26
+ onMove: (delta) => {
27
+ if (scroller.current === null)
28
+ return;
29
+ scroller.current.scrollLeft = scrollStart.current.x - delta.x;
30
+ scroller.current.scrollTop = scrollStart.current.y - delta.y;
31
+ },
32
+ onStart: (element) => {
33
+ scrollStart.current = { x: element.scrollLeft, y: element.scrollTop };
34
+ },
35
+ });
19
36
  (0, react_1.useEffect)(() => {
20
37
  let isCurrent = true;
21
38
  let opened = null;
@@ -86,7 +103,7 @@ const PsPdfViewer = ({ activeMatch, file, labels, onMatchCount, pdfWorkerSrc, qu
86
103
  return Math.max(0.1, fit * zoom);
87
104
  }, [frameSize.width, pageWidth, zoom]);
88
105
  const pages = document === null ? [] : Array.from({ length: document.numPages }, (_, index) => index + 1);
89
- return ((0, jsx_runtime_1.jsxs)("div", { className: 'flex h-full w-full min-w-0', children: [document !== null && document.numPages > 1 ? ((0, jsx_runtime_1.jsx)("nav", { className: 'ps-scrollbar-hidden hidden w-24 shrink-0 flex-col items-center gap-2 overflow-y-auto border-r border-white/10 py-3 md:flex', children: pages.map((pageNumber) => ((0, jsx_runtime_1.jsx)(ps_pdf_thumbnail_1.PsPdfThumbnail, { document: document, hasMatches: matches.some((match) => match.pageNumber === pageNumber), isCurrent: pageNumber === currentPage, label: labels.page, onSelect: () => scrollToPage(pageNumber), pageNumber: pageNumber, rotation: rotation }, pageNumber))) })) : null, (0, jsx_runtime_1.jsx)("div", { className: 'min-w-0 flex-1', ref: frame, children: hasFailed ? ((0, jsx_runtime_1.jsx)("p", { className: 'flex h-full items-center justify-center text-sm text-white/70', children: labels.loadFailed })) : document === null ? ((0, jsx_runtime_1.jsx)("div", { className: 'flex h-full items-center justify-center text-white', children: (0, jsx_runtime_1.jsx)(ps_core_components_1.PsSpinner, { color: 'current', size: 'large' }) })) : ((0, jsx_runtime_1.jsx)("div", { className: 'flex h-full flex-col items-center gap-4 overflow-auto px-6 py-6', onScroll: (event) => {
106
+ return ((0, jsx_runtime_1.jsxs)("div", { className: 'flex h-full w-full min-w-0', children: [document !== null && document.numPages > 1 ? ((0, jsx_runtime_1.jsx)("nav", { className: 'ps-scrollbar-hidden hidden w-24 shrink-0 flex-col items-center gap-2 overflow-y-auto border-r border-white/10 py-3 md:flex', children: pages.map((pageNumber) => ((0, jsx_runtime_1.jsx)(ps_pdf_thumbnail_1.PsPdfThumbnail, { document: document, hasMatches: matches.some((match) => match.pageNumber === pageNumber), isCurrent: pageNumber === currentPage, label: labels.page, onSelect: () => scrollToPage(pageNumber), pageNumber: pageNumber, rotation: rotation }, pageNumber))) })) : null, (0, jsx_runtime_1.jsx)("div", { className: 'min-w-0 flex-1', ref: frame, children: hasFailed ? ((0, jsx_runtime_1.jsx)("p", { className: 'flex h-full items-center justify-center text-sm text-white/70', children: labels.loadFailed })) : document === null ? ((0, jsx_runtime_1.jsx)("div", { className: 'flex h-full items-center justify-center text-white', children: (0, jsx_runtime_1.jsx)(ps_core_components_1.PsSpinner, { color: 'current', size: 'large' }) })) : ((0, jsx_runtime_1.jsx)("div", { className: 'flex h-full cursor-grab flex-col items-center gap-4 overflow-auto px-6 py-6 active:cursor-grabbing', ...handlers, onScroll: (event) => {
90
107
  const top = event.currentTarget.scrollTop + event.currentTarget.clientHeight / 3;
91
108
  const visible = Array.from(event.currentTarget.querySelectorAll('[data-ps-pdf-page]')).find((node) => node.offsetTop + node.offsetHeight > top);
92
109
  if (visible)
@@ -14,12 +14,12 @@ const ps_scan_viewfinder_overlay_1 = require("./ps-scan-viewfinder-overlay");
14
14
  const PsScanCameraView = ({ canTorch, detection, flashKey, isBusy, isReady, isTorchOn, labels, onCapture, onClose, onFile, onToggleTorch, videoRef, videoTarget, }) => {
15
15
  const [areaRef, area] = (0, ps_core_components_1.usePsElementSize)();
16
16
  const [video, setVideo] = (0, react_1.useState)({ height: 0, width: 0 });
17
- const hint = detection.quad === null ? labels.hintFind : labels.hintFound;
17
+ const hint = detection.hasQuad ? labels.hintFound : labels.hintFind;
18
18
  const measure = () => {
19
19
  const element = videoRef.current;
20
20
  if (element !== null)
21
21
  setVideo({ height: element.videoHeight, width: element.videoWidth });
22
22
  };
23
- return ((0, jsx_runtime_1.jsxs)("div", { className: 'relative h-full min-h-0 overflow-hidden bg-black text-white', children: [(0, jsx_runtime_1.jsxs)("div", { className: 'absolute inset-0', ref: areaRef, children: [(0, jsx_runtime_1.jsx)("video", { "aria-hidden": 'true', autoPlay: true, className: ps_design_system_1.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 ? ((0, jsx_runtime_1.jsx)(ps_scan_viewfinder_overlay_1.PsScanViewfinderOverlay, { area: area, quad: detection.quad, video: video })) : null] }), isReady ? null : ((0, jsx_runtime_1.jsxs)("div", { className: 'absolute inset-0 flex flex-col items-center justify-center gap-3 text-sm text-white/75', role: 'status', children: [(0, jsx_runtime_1.jsx)(ps_core_components_1.PsSpinner, { color: 'current', label: labels.cameraStarting, size: 'small' }), (0, jsx_runtime_1.jsx)("span", { children: labels.cameraStarting })] })), flashKey > 0 ? ((0, jsx_runtime_1.jsx)("div", { "aria-hidden": 'true', className: 'pointer-events-none absolute inset-0 bg-white animate-ps-fade-out' }, flashKey)) : null, (0, jsx_runtime_1.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: [(0, jsx_runtime_1.jsx)(ps_scan_icon_button_1.PsScanIconButton, { iconKey: ps_icons_1.PsIconKey.Close, label: labels.close, onClick: onClose }), (0, jsx_runtime_1.jsx)("div", { className: 'flex items-center gap-2', children: canTorch && isReady ? ((0, jsx_runtime_1.jsx)(ps_scan_icon_button_1.PsScanIconButton, { iconKey: ps_icons_1.PsIconKey.Flash, isPressed: isTorchOn, label: isTorchOn ? labels.torchOff : labels.torchOn, onClick: onToggleTorch })) : null })] }), isReady ? ((0, jsx_runtime_1.jsx)("div", { className: 'absolute inset-x-0 top-[calc(max(1rem,env(safe-area-inset-top))+3.75rem)] flex justify-center px-4', children: (0, jsx_runtime_1.jsx)("p", { "aria-live": 'polite', className: ps_scan_chrome_classes_1.PsScanChromeClasses.pill, children: hint }) })) : null, (0, jsx_runtime_1.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: [(0, jsx_runtime_1.jsx)("div", { className: 'flex justify-start', children: (0, jsx_runtime_1.jsx)(ps_scan_photo_button_1.PsScanPhotoButton, { isIconOnly: true, label: labels.choosePhoto, onFile: onFile }) }), (0, jsx_runtime_1.jsx)(ps_scan_shutter_1.PsScanShutter, { disabled: !isReady || isBusy, label: labels.capture, onCapture: onCapture }), (0, jsx_runtime_1.jsx)("span", { "aria-hidden": 'true' })] })] }));
23
+ return ((0, jsx_runtime_1.jsxs)("div", { className: 'relative h-full min-h-0 overflow-hidden bg-black text-white', children: [(0, jsx_runtime_1.jsxs)("div", { className: 'absolute inset-0', ref: areaRef, children: [(0, jsx_runtime_1.jsx)("video", { "aria-hidden": 'true', autoPlay: true, className: ps_design_system_1.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 ? ((0, jsx_runtime_1.jsx)(ps_scan_viewfinder_overlay_1.PsScanViewfinderOverlay, { area: area, hasQuad: detection.hasQuad, quadRef: detection.quadRef, video: video })) : null] }), isReady ? null : ((0, jsx_runtime_1.jsxs)("div", { className: 'absolute inset-0 flex flex-col items-center justify-center gap-3 text-sm text-white/75', role: 'status', children: [(0, jsx_runtime_1.jsx)(ps_core_components_1.PsSpinner, { color: 'current', label: labels.cameraStarting, size: 'small' }), (0, jsx_runtime_1.jsx)("span", { children: labels.cameraStarting })] })), flashKey > 0 ? ((0, jsx_runtime_1.jsx)("div", { "aria-hidden": 'true', className: 'pointer-events-none absolute inset-0 bg-white animate-ps-fade-out' }, flashKey)) : null, (0, jsx_runtime_1.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: [(0, jsx_runtime_1.jsx)(ps_scan_icon_button_1.PsScanIconButton, { iconKey: ps_icons_1.PsIconKey.Close, label: labels.close, onClick: onClose }), (0, jsx_runtime_1.jsx)("div", { className: 'flex items-center gap-2', children: canTorch && isReady ? ((0, jsx_runtime_1.jsx)(ps_scan_icon_button_1.PsScanIconButton, { iconKey: ps_icons_1.PsIconKey.Flash, isPressed: isTorchOn, label: isTorchOn ? labels.torchOff : labels.torchOn, onClick: onToggleTorch })) : null })] }), isReady ? ((0, jsx_runtime_1.jsx)("div", { className: 'absolute inset-x-0 top-[calc(max(1rem,env(safe-area-inset-top))+3.75rem)] flex justify-center px-4', children: (0, jsx_runtime_1.jsx)("p", { "aria-live": 'polite', className: ps_scan_chrome_classes_1.PsScanChromeClasses.pill, children: hint }) })) : null, (0, jsx_runtime_1.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: [(0, jsx_runtime_1.jsx)("div", { className: 'flex justify-start', children: (0, jsx_runtime_1.jsx)(ps_scan_photo_button_1.PsScanPhotoButton, { isIconOnly: true, label: labels.choosePhoto, onFile: onFile }) }), (0, jsx_runtime_1.jsx)(ps_scan_shutter_1.PsScanShutter, { disabled: !isReady || isBusy, label: labels.capture, onCapture: onCapture }), (0, jsx_runtime_1.jsx)("span", { "aria-hidden": 'true' })] })] }));
24
24
  };
25
25
  exports.PsScanCameraView = PsScanCameraView;
@@ -0,0 +1,8 @@
1
+ import type { PsDocumentScannerPageLabels } from '../models/ps-document-scanner-page-labels';
2
+ import type { PsScanPage } from '../models/ps-scan-page';
3
+ export interface PsScanPageStripProps {
4
+ labels: PsDocumentScannerPageLabels;
5
+ onRemove: (id: number) => void;
6
+ pages: PsScanPage[];
7
+ }
8
+ export declare const PsScanPageStrip: ({ labels, onRemove, pages }: PsScanPageStripProps) => import("react").JSX.Element;
@@ -0,0 +1,8 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PsScanPageStrip = void 0;
4
+ const jsx_runtime_1 = require("react/jsx-runtime");
5
+ const ps_icons_1 = require("@pallasoft/ps-icons");
6
+ const ps_scan_template_service_1 = require("../services/ps-scan-template-service");
7
+ const PsScanPageStrip = ({ labels, onRemove, pages }) => ((0, jsx_runtime_1.jsx)("ul", { className: 'ps-scrollbar-hidden flex min-w-0 items-center justify-center gap-3 overflow-x-auto px-4 py-2', children: pages.map((page, index) => ((0, jsx_runtime_1.jsxs)("li", { className: 'relative shrink-0', children: [(0, jsx_runtime_1.jsx)("img", { alt: ps_scan_template_service_1.PsScanTemplateService.fill(labels.pageLabel, { n: index + 1 }), className: 'h-16 w-auto rounded-sm bg-white object-contain ring-1 ring-white/25', src: page.url }), (0, jsx_runtime_1.jsx)("button", { "aria-label": ps_scan_template_service_1.PsScanTemplateService.fill(labels.removePage, { n: index + 1 }), className: 'ps-focus-ring ps-control-square ps-control-compact absolute -right-2 -top-2 inline-flex items-center justify-center rounded-ps-control bg-black/80 text-white transition hover:bg-black', onClick: () => onRemove(page.id), type: 'button', children: (0, jsx_runtime_1.jsx)(ps_icons_1.PsIcon, { "aria-hidden": 'true', className: 'size-4', iconKey: ps_icons_1.PsIconKey.Close }) })] }, page.id))) }));
8
+ exports.PsScanPageStrip = PsScanPageStrip;
@@ -1,12 +1,18 @@
1
1
  import { PsDocumentScannerLabels } from '../constants/ps-document-scanner-labels';
2
+ import { PsScanFormat } from '../constants/ps-scan-format';
2
3
  import type { PsLabelSet } from '../models/ps-label-set';
4
+ import type { PsScanMultiPage } from '../models/ps-scan-multi-page';
3
5
  import type { PsScanResult } from '../models/ps-scan-result';
4
6
  export interface PsScanPreviewProps {
5
7
  error: string;
6
8
  labels: PsLabelSet<typeof PsDocumentScannerLabels>;
9
+ formats: readonly PsScanFormat[];
10
+ isSaving: boolean;
11
+ multiPage?: PsScanMultiPage;
7
12
  onAdjust: () => void;
8
13
  onRetake: () => void;
9
- onUse: () => void;
14
+ onSave: (format: PsScanFormat) => void;
15
+ preferredFormat: PsScanFormat;
10
16
  result: PsScanResult | null;
11
17
  }
12
- export declare const PsScanPreview: ({ error, labels, onAdjust, onRetake, onUse, result, }: PsScanPreviewProps) => import("react").JSX.Element;
18
+ export declare const PsScanPreview: ({ error, formats, isSaving, labels, multiPage, onAdjust, onRetake, onSave, preferredFormat, result, }: PsScanPreviewProps) => import("react").JSX.Element;
@@ -4,7 +4,12 @@ exports.PsScanPreview = void 0;
4
4
  const jsx_runtime_1 = require("react/jsx-runtime");
5
5
  const ps_core_components_1 = require("@pallasoft/ps-core-components");
6
6
  const ps_icons_1 = require("@pallasoft/ps-icons");
7
+ const ps_scan_format_1 = require("../constants/ps-scan-format");
8
+ const ps_scan_template_service_1 = require("../services/ps-scan-template-service");
7
9
  const ps_scan_button_1 = require("./ps-scan-button");
8
10
  const ps_scan_icon_button_1 = require("./ps-scan-icon-button");
9
- const PsScanPreview = ({ error, labels, onAdjust, onRetake, onUse, result, }) => ((0, jsx_runtime_1.jsxs)("div", { className: 'flex h-full min-h-0 flex-col bg-black text-white animate-ps-fade-in', children: [(0, jsx_runtime_1.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: [(0, jsx_runtime_1.jsx)(ps_scan_icon_button_1.PsScanIconButton, { iconKey: ps_icons_1.PsIconKey.Crop, label: labels.cropTitle, onClick: onAdjust }), (0, jsx_runtime_1.jsx)("h2", { className: 'truncate text-center text-base font-semibold', children: labels.previewTitle }), (0, jsx_runtime_1.jsx)("span", { "aria-hidden": 'true' })] }), (0, jsx_runtime_1.jsx)("div", { className: 'flex min-h-0 flex-1 items-center justify-center px-6 py-4', children: result === null ? ((0, jsx_runtime_1.jsxs)("div", { className: 'flex flex-col items-center gap-3 text-sm text-white/75', role: 'status', children: [(0, jsx_runtime_1.jsx)(ps_core_components_1.PsSpinner, { color: 'current', label: labels.preparing, size: 'small' }), (0, jsx_runtime_1.jsx)("span", { children: labels.preparing })] })) : ((0, jsx_runtime_1.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 : ((0, jsx_runtime_1.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 })), (0, jsx_runtime_1.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: [(0, jsx_runtime_1.jsx)(ps_scan_button_1.PsScanButton, { isLarge: true, onClick: onRetake, children: labels.retake }), (0, jsx_runtime_1.jsx)(ps_scan_button_1.PsScanButton, { disabled: result === null, isLarge: true, isPrimary: true, leadingIcon: (0, jsx_runtime_1.jsx)(ps_icons_1.PsIcon, { "aria-hidden": 'true', className: 'size-4', iconKey: ps_icons_1.PsIconKey.Check }), onClick: onUse, children: labels.useScan })] })] }));
11
+ const ps_scan_page_strip_1 = require("./ps-scan-page-strip");
12
+ const PsScanPreview = ({ error, formats, isSaving, labels, multiPage, onAdjust, onRetake, onSave, preferredFormat, result, }) => ((0, jsx_runtime_1.jsxs)("div", { className: 'flex h-full min-h-0 flex-col bg-black text-white animate-ps-fade-in', children: [(0, jsx_runtime_1.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: [(0, jsx_runtime_1.jsx)(ps_scan_icon_button_1.PsScanIconButton, { iconKey: ps_icons_1.PsIconKey.Crop, label: labels.cropTitle, onClick: onAdjust }), (0, jsx_runtime_1.jsx)("h2", { className: 'truncate text-center text-base font-semibold', children: labels.previewTitle }), (0, jsx_runtime_1.jsx)("span", { "aria-hidden": 'true' })] }), (0, jsx_runtime_1.jsx)("div", { className: 'flex min-h-0 flex-1 items-center justify-center px-6 py-4', children: result === null ? ((0, jsx_runtime_1.jsxs)("div", { className: 'flex flex-col items-center gap-3 text-sm text-white/75', role: 'status', children: [(0, jsx_runtime_1.jsx)(ps_core_components_1.PsSpinner, { color: 'current', label: labels.preparing, size: 'small' }), (0, jsx_runtime_1.jsx)("span", { children: labels.preparing })] })) : ((0, jsx_runtime_1.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 : ((0, jsx_runtime_1.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 })), multiPage === undefined || multiPage.pages.length === 0 ? null : ((0, jsx_runtime_1.jsxs)("div", { className: 'shrink-0', children: [(0, jsx_runtime_1.jsx)(ps_scan_page_strip_1.PsScanPageStrip, { labels: multiPage.labels, onRemove: multiPage.onRemovePage, pages: multiPage.pages }), (0, jsx_runtime_1.jsx)("p", { className: 'text-center text-sm text-white/75', children: ps_scan_template_service_1.PsScanTemplateService.fill(multiPage.labels.pageCount, {
13
+ count: multiPage.pages.length + 1,
14
+ }) })] })), (0, jsx_runtime_1.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: [multiPage === undefined ? null : ((0, jsx_runtime_1.jsx)(ps_scan_button_1.PsScanButton, { className: 'col-span-2', disabled: result === null, isLoading: multiPage.isBusy, isLarge: true, leadingIcon: (0, jsx_runtime_1.jsx)(ps_icons_1.PsIcon, { "aria-hidden": 'true', className: 'size-4', iconKey: ps_icons_1.PsIconKey.Plus }), onClick: multiPage.onAddPage, children: multiPage.labels.addPage })), formats.includes(ps_scan_format_1.PsScanFormat.Pdf) ? ((0, jsx_runtime_1.jsx)(ps_scan_button_1.PsScanButton, { className: formats.length === 1 ? 'col-span-2' : undefined, disabled: result === null, isLarge: true, isLoading: isSaving, isPrimary: preferredFormat === ps_scan_format_1.PsScanFormat.Pdf, leadingIcon: (0, jsx_runtime_1.jsx)(ps_icons_1.PsIcon, { "aria-hidden": 'true', className: 'size-4', iconKey: ps_icons_1.PsIconKey.File }), onClick: () => onSave(ps_scan_format_1.PsScanFormat.Pdf), children: labels.saveAsPdf })) : null, formats.includes(ps_scan_format_1.PsScanFormat.Png) ? ((0, jsx_runtime_1.jsx)(ps_scan_button_1.PsScanButton, { className: formats.length === 1 ? 'col-span-2' : undefined, disabled: result === null, isLarge: true, isLoading: isSaving, isPrimary: preferredFormat === ps_scan_format_1.PsScanFormat.Png, leadingIcon: (0, jsx_runtime_1.jsx)(ps_icons_1.PsIcon, { "aria-hidden": 'true', className: 'size-4', iconKey: ps_icons_1.PsIconKey.Image }), onClick: () => onSave(ps_scan_format_1.PsScanFormat.Png), children: labels.saveAsImages })) : null, (0, jsx_runtime_1.jsx)(ps_scan_button_1.PsScanButton, { className: 'col-span-2', disabled: isSaving, isLarge: true, onClick: onRetake, children: labels.retake })] })] }));
10
15
  exports.PsScanPreview = PsScanPreview;
@@ -1,8 +1,10 @@
1
+ import type { RefObject } from 'react';
1
2
  import type { PsPageSize } from '../models/ps-page-size';
2
3
  import type { PsScanQuad } from '../models/ps-scan-quad';
3
4
  export interface PsScanViewfinderOverlayProps {
4
5
  area: PsPageSize;
5
- quad: PsScanQuad | null;
6
+ hasQuad: boolean;
7
+ quadRef: RefObject<PsScanQuad | null>;
6
8
  video: PsPageSize;
7
9
  }
8
- export declare const PsScanViewfinderOverlay: ({ area, quad, video }: PsScanViewfinderOverlayProps) => import("react").JSX.Element | null;
10
+ export declare const PsScanViewfinderOverlay: ({ area, hasQuad, quadRef, video, }: PsScanViewfinderOverlayProps) => import("react").JSX.Element | null;
@@ -3,13 +3,15 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.PsScanViewfinderOverlay = void 0;
4
4
  const jsx_runtime_1 = require("react/jsx-runtime");
5
5
  const ps_scan_config_1 = require("../constants/ps-scan-config");
6
+ const use_ps_scan_quad_tween_1 = require("../hooks/use-ps-scan-quad-tween");
6
7
  const ps_scan_geometry_service_1 = require("../services/ps-scan-geometry-service");
7
- const PsScanViewfinderOverlay = ({ area, quad, video }) => {
8
- if (area.width <= 0 || area.height <= 0)
9
- return null;
8
+ const PsScanViewfinderOverlay = ({ area, hasQuad, quadRef, video, }) => {
10
9
  const box = ps_scan_geometry_service_1.PsScanGeometryService.cover(area, video);
11
10
  const left = (area.width - box.width) / 2;
12
11
  const top = (area.height - box.height) / 2;
12
+ const polygon = (0, use_ps_scan_quad_tween_1.usePsScanQuadTween)(quadRef, (quad) => quad.map((point) => `${left + point.x * box.width},${top + point.y * box.height}`).join(' '));
13
+ if (area.width <= 0 || area.height <= 0)
14
+ return null;
13
15
  const frameHeight = Math.min(area.height * ps_scan_config_1.PsScanConfig.guideHeightRatio, (area.width * ps_scan_config_1.PsScanConfig.guideWidthRatio) / ps_scan_config_1.PsScanConfig.guideAspect);
14
16
  const frameWidth = frameHeight * ps_scan_config_1.PsScanConfig.guideAspect;
15
17
  const frameLeft = (area.width - frameWidth) / 2;
@@ -21,8 +23,6 @@ const PsScanViewfinderOverlay = ({ area, quad, video }) => {
21
23
  `M${frameLeft + frameWidth} ${frameTop + frameHeight - arm}V${frameTop + frameHeight}H${frameLeft + frameWidth - arm}`,
22
24
  `M${frameLeft + arm} ${frameTop + frameHeight}H${frameLeft}V${frameTop + frameHeight - arm}`,
23
25
  ].join('');
24
- return ((0, jsx_runtime_1.jsx)("svg", { "aria-hidden": 'true', className: 'pointer-events-none absolute inset-0 size-full', viewBox: `0 0 ${area.width} ${area.height}`, children: quad === null || box.width === 0 ? ((0, jsx_runtime_1.jsx)("path", { className: 'stroke-white/75 animate-ps-fade-in', d: corners, fill: 'none', strokeLinecap: 'round', strokeLinejoin: 'round', strokeWidth: '4' })) : ((0, jsx_runtime_1.jsx)("polygon", { className: 'fill-primary/10 stroke-primary', points: quad
25
- .map((point) => `${left + point.x * box.width},${top + point.y * box.height}`)
26
- .join(' '), strokeLinejoin: 'round', strokeWidth: '3' })) }));
26
+ return ((0, jsx_runtime_1.jsx)("svg", { "aria-hidden": 'true', className: 'pointer-events-none absolute inset-0 size-full', viewBox: `0 0 ${area.width} ${area.height}`, children: !hasQuad || box.width === 0 ? ((0, jsx_runtime_1.jsx)("path", { className: 'stroke-white/75 animate-ps-fade-in', d: corners, fill: 'none', strokeLinecap: 'round', strokeLinejoin: 'round', strokeWidth: '4' })) : ((0, jsx_runtime_1.jsx)("polygon", { className: 'fill-primary/10 stroke-primary', ref: polygon, strokeLinejoin: 'round', strokeWidth: '3' })) }));
27
27
  };
28
28
  exports.PsScanViewfinderOverlay = PsScanViewfinderOverlay;
@@ -41,11 +41,12 @@ export declare const PsDocumentScannerLabels: {
41
41
  readonly primerTitle: "Allow camera access to scan your documents";
42
42
  readonly processFailed: "The scan could not be prepared. Try again.";
43
43
  readonly retake: "Retake";
44
+ readonly saveAsImages: "Save as images";
45
+ readonly saveAsPdf: "Save as PDF";
44
46
  readonly title: "Scan document";
45
47
  readonly torchOff: "Turn flash off";
46
48
  readonly torchOn: "Turn flash on";
47
49
  readonly tryAgain: "Try again";
48
50
  readonly unavailableBody: "This device has no camera the browser can use. Choose a photo of the page instead.";
49
51
  readonly unavailableTitle: "No camera found";
50
- readonly useScan: "Use scan";
51
52
  };
@@ -44,11 +44,12 @@ exports.PsDocumentScannerLabels = {
44
44
  primerTitle: 'Allow camera access to scan your documents',
45
45
  processFailed: 'The scan could not be prepared. Try again.',
46
46
  retake: 'Retake',
47
+ saveAsImages: 'Save as images',
48
+ saveAsPdf: 'Save as PDF',
47
49
  title: 'Scan document',
48
50
  torchOff: 'Turn flash off',
49
51
  torchOn: 'Turn flash on',
50
52
  tryAgain: 'Try again',
51
53
  unavailableBody: 'This device has no camera the browser can use. Choose a photo of the page instead.',
52
54
  unavailableTitle: 'No camera found',
53
- useScan: 'Use scan',
54
55
  };
@@ -1,4 +1,5 @@
1
1
  export declare const PsFileViewerConfig: {
2
+ readonly dragActivationDistance: 4;
2
3
  readonly maxZoom: 4;
3
4
  readonly minZoom: 0.25;
4
5
  readonly pdfBaseScale: 1.4;
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.PsFileViewerConfig = void 0;
4
4
  exports.PsFileViewerConfig = {
5
+ dragActivationDistance: 4,
5
6
  maxZoom: 4,
6
7
  minZoom: 0.25,
7
8
  pdfBaseScale: 1.4,
@@ -20,6 +20,7 @@ export declare const PsScanConfig: {
20
20
  readonly detectMinAreaRatio: 0.1;
21
21
  readonly detectMinQuadFill: 0.88;
22
22
  readonly detectMinSideRatio: 0.06;
23
+ readonly formats: readonly ["pdf", "png"];
23
24
  readonly guideArm: 36;
24
25
  readonly guideArmRatio: 0.18;
25
26
  readonly guideAspect: 0.72;
@@ -27,14 +28,20 @@ export declare const PsScanConfig: {
27
28
  readonly guideLift: 0.03;
28
29
  readonly guideWidthRatio: 0.76;
29
30
  readonly liveDeadzone: 0.006;
30
- readonly liveDetectIntervalMs: 200;
31
+ readonly liveCloseRadii: readonly [2];
32
+ readonly liveDetectMinIntervalMs: 70;
31
33
  readonly liveDetectMaxDimension: 280;
32
- readonly liveMissLimit: 3;
33
- readonly liveSmoothing: 0.45;
34
+ readonly liveMissLimit: 1;
35
+ readonly liveSmoothing: 0.68;
36
+ readonly liveTweenMs: 55;
34
37
  readonly loupeOffset: 72;
35
38
  readonly loupeSize: 112;
36
39
  readonly loupeZoom: 2.5;
37
40
  readonly minCornerGap: 0.02;
41
+ readonly multiPageMaxDimension: 2400;
42
+ readonly jpegType: "image/jpeg";
43
+ readonly pdfType: "application/pdf";
44
+ readonly pngType: "image/png";
38
45
  readonly polygonEpsilons: readonly [0.008, 0.012, 0.018, 0.026, 0.036, 0.05, 0.07];
39
46
  readonly torchConstraint: "torch";
40
47
  readonly warpChunkPixels: 400000;
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.PsScanConfig = void 0;
4
+ const ps_scan_format_1 = require("./ps-scan-format");
4
5
  exports.PsScanConfig = {
5
6
  cameraPermission: 'camera',
6
7
  cannyHighPercentile: 0.9,
@@ -23,6 +24,7 @@ exports.PsScanConfig = {
23
24
  detectMinAreaRatio: 0.1,
24
25
  detectMinQuadFill: 0.88,
25
26
  detectMinSideRatio: 0.06,
27
+ formats: [ps_scan_format_1.PsScanFormat.Pdf, ps_scan_format_1.PsScanFormat.Png],
26
28
  guideArm: 36,
27
29
  guideArmRatio: 0.18,
28
30
  guideAspect: 0.72,
@@ -30,14 +32,20 @@ exports.PsScanConfig = {
30
32
  guideLift: 0.03,
31
33
  guideWidthRatio: 0.76,
32
34
  liveDeadzone: 0.006,
33
- liveDetectIntervalMs: 200,
35
+ liveCloseRadii: [2],
36
+ liveDetectMinIntervalMs: 70,
34
37
  liveDetectMaxDimension: 280,
35
- liveMissLimit: 3,
36
- liveSmoothing: 0.45,
38
+ liveMissLimit: 1,
39
+ liveSmoothing: 0.68,
40
+ liveTweenMs: 55,
37
41
  loupeOffset: 72,
38
42
  loupeSize: 112,
39
43
  loupeZoom: 2.5,
40
44
  minCornerGap: 0.02,
45
+ multiPageMaxDimension: 2400,
46
+ jpegType: 'image/jpeg',
47
+ pdfType: 'application/pdf',
48
+ pngType: 'image/png',
41
49
  polygonEpsilons: [0.008, 0.012, 0.018, 0.026, 0.036, 0.05, 0.07],
42
50
  torchConstraint: 'torch',
43
51
  warpChunkPixels: 400000,
@@ -0,0 +1,10 @@
1
+ export declare const PsScanDetectProfiles: {
2
+ readonly full: {
3
+ readonly closeRadii: readonly [1, 2, 4];
4
+ readonly isYielding: true;
5
+ };
6
+ readonly live: {
7
+ readonly closeRadii: readonly [2];
8
+ readonly isYielding: false;
9
+ };
10
+ };
@@ -0,0 +1,8 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PsScanDetectProfiles = void 0;
4
+ const ps_scan_config_1 = require("./ps-scan-config");
5
+ exports.PsScanDetectProfiles = {
6
+ full: { closeRadii: ps_scan_config_1.PsScanConfig.closeRadii, isYielding: true },
7
+ live: { closeRadii: ps_scan_config_1.PsScanConfig.liveCloseRadii, isYielding: false },
8
+ };
@@ -0,0 +1,5 @@
1
+ export declare const PsScanFormat: {
2
+ readonly Pdf: "pdf";
3
+ readonly Png: "png";
4
+ };
5
+ export type PsScanFormat = (typeof PsScanFormat)[keyof typeof PsScanFormat];
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PsScanFormat = void 0;
4
+ exports.PsScanFormat = {
5
+ Pdf: 'pdf',
6
+ Png: 'png',
7
+ };