@pallasoft/ps-file-components 2.1.0 → 2.3.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 (43) 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 +10 -24
  4. package/dist/components/ps-document-scanner.d.ts +1 -1
  5. package/dist/components/ps-document-scanner.js +2 -2
  6. package/dist/components/ps-file-list.d.ts +2 -1
  7. package/dist/components/ps-file-list.js +2 -2
  8. package/dist/components/ps-scan-preview.d.ts +6 -2
  9. package/dist/components/ps-scan-preview.js +3 -2
  10. package/dist/constants/ps-document-scanner-labels.d.ts +2 -1
  11. package/dist/constants/ps-document-scanner-labels.js +2 -1
  12. package/dist/constants/ps-file-list-labels.d.ts +1 -0
  13. package/dist/constants/ps-file-list-labels.js +1 -0
  14. package/dist/constants/ps-scan-config.d.ts +4 -1
  15. package/dist/constants/ps-scan-config.js +5 -1
  16. package/dist/constants/ps-scan-format.d.ts +5 -0
  17. package/dist/constants/ps-scan-format.js +7 -0
  18. package/dist/esm/components/ps-document-scanner-sheet.js +10 -24
  19. package/dist/esm/components/ps-document-scanner.js +2 -2
  20. package/dist/esm/components/ps-file-list.js +2 -2
  21. package/dist/esm/components/ps-scan-preview.js +3 -2
  22. package/dist/esm/constants/ps-document-scanner-labels.js +2 -1
  23. package/dist/esm/constants/ps-file-list-labels.js +1 -0
  24. package/dist/esm/constants/ps-scan-config.js +5 -1
  25. package/dist/esm/constants/ps-scan-format.js +4 -0
  26. package/dist/esm/index.js +1 -0
  27. package/dist/esm/services/ps-scan-canvas-service.js +10 -0
  28. package/dist/esm/services/ps-scan-file-name-service.js +5 -1
  29. package/dist/esm/services/ps-scan-output-service.js +23 -0
  30. package/dist/esm/services/ps-scan-pdf-service.js +3 -52
  31. package/dist/index.d.ts +1 -0
  32. package/dist/index.js +1 -0
  33. package/dist/models/ps-document-scanner-props.d.ts +2 -0
  34. package/dist/models/ps-document-scanner-sheet-props.d.ts +2 -0
  35. package/dist/services/ps-scan-canvas-service.d.ts +1 -0
  36. package/dist/services/ps-scan-canvas-service.js +10 -0
  37. package/dist/services/ps-scan-file-name-service.d.ts +1 -0
  38. package/dist/services/ps-scan-file-name-service.js +5 -1
  39. package/dist/services/ps-scan-output-service.d.ts +6 -0
  40. package/dist/services/ps-scan-output-service.js +27 -0
  41. package/dist/services/ps-scan-pdf-service.d.ts +0 -2
  42. package/dist/services/ps-scan-pdf-service.js +3 -52
  43. package/package.json +2 -2
@@ -1 +1 @@
1
- export declare const PsDocumentScannerLoader: import("react").LazyExoticComponent<({ fileName, jpegQuality, labels, maxDimension, onClose, onComplete, pageLabels, }: 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, pageLabels, }: PsDocumentScannerSheetProps) => import("react").ReactPortal | null;
2
+ export declare const PsDocumentScannerSheet: ({ fileName, formats, jpegQuality, labels, maxDimension, onClose, onComplete, pageLabels, }: PsDocumentScannerSheetProps) => import("react").ReactPortal | null;
@@ -14,8 +14,7 @@ const use_ps_focus_trap_1 = require("../hooks/use-ps-focus-trap");
14
14
  const use_ps_scan_camera_1 = require("../hooks/use-ps-scan-camera");
15
15
  const use_ps_scan_live_detection_1 = require("../hooks/use-ps-scan-live-detection");
16
16
  const use_ps_scan_photo_1 = require("../hooks/use-ps-scan-photo");
17
- const ps_scan_file_name_service_1 = require("../services/ps-scan-file-name-service");
18
- const ps_scan_pdf_service_1 = require("../services/ps-scan-pdf-service");
17
+ const ps_scan_output_service_1 = require("../services/ps-scan-output-service");
19
18
  const ps_scan_pipeline_service_1 = require("../services/ps-scan-pipeline-service");
20
19
  const ps_scan_camera_view_1 = require("./ps-scan-camera-view");
21
20
  const ps_scan_crop_editor_1 = require("./ps-scan-crop-editor");
@@ -23,7 +22,7 @@ const ps_scan_icon_button_1 = require("./ps-scan-icon-button");
23
22
  const ps_scan_page_strip_1 = require("./ps-scan-page-strip");
24
23
  const ps_scan_permission_view_1 = require("./ps-scan-permission-view");
25
24
  const ps_scan_preview_1 = require("./ps-scan-preview");
26
- const PsDocumentScannerSheet = ({ fileName, jpegQuality, labels, maxDimension, onClose, onComplete, pageLabels, }) => {
25
+ const PsDocumentScannerSheet = ({ fileName, formats, jpegQuality, labels, maxDimension, onClose, onComplete, pageLabels, }) => {
27
26
  const tier = (0, ps_core_components_1.usePsViewport)();
28
27
  const zIndex = (0, ps_core_components_1.usePsLayerZIndex)(ps_design_system_1.PallasoftZIndex.Modal);
29
28
  const content = (0, react_1.useRef)(null);
@@ -146,20 +145,19 @@ const PsDocumentScannerSheet = ({ fileName, jpegQuality, labels, maxDimension, o
146
145
  URL.revokeObjectURL(removed.url);
147
146
  setPages((current) => current.filter((page) => page.id !== id));
148
147
  };
149
- const useAsPdf = async () => {
150
- if (isAddingPage)
148
+ const save = async (format) => {
149
+ if (result === null || isAddingPage)
151
150
  return;
152
151
  setIsAddingPage(true);
153
152
  setError('');
154
153
  try {
155
- const last = await renderPage();
154
+ const last = pages.length === 0 ? { ...result, id: 0 } : await renderPage();
156
155
  if (last === null)
157
156
  return;
158
- const pdf = await ps_scan_pdf_service_1.PsScanPdfService.build([...pages, last]);
159
- URL.revokeObjectURL(last.url);
160
- onComplete([
161
- new File([pdf], ps_scan_file_name_service_1.PsScanFileNameService.buildPdf(fileName), { type: 'application/pdf' }),
162
- ]);
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);
163
161
  onClose();
164
162
  }
165
163
  catch {
@@ -169,18 +167,6 @@ const PsDocumentScannerSheet = ({ fileName, jpegQuality, labels, maxDimension, o
169
167
  setIsAddingPage(false);
170
168
  }
171
169
  };
172
- const use = () => {
173
- if (result === null)
174
- return;
175
- if (pages.length > 0) {
176
- void useAsPdf();
177
- return;
178
- }
179
- onComplete([
180
- new File([result.blob], ps_scan_file_name_service_1.PsScanFileNameService.build(fileName), { type: 'image/jpeg' }),
181
- ]);
182
- onClose();
183
- };
184
170
  const multiPage = pageLabels === null
185
171
  ? undefined
186
172
  : {
@@ -196,6 +182,6 @@ const PsDocumentScannerSheet = ({ fileName, jpegQuality, labels, maxDimension, o
196
182
  ? 'fixed inset-0 flex items-center justify-center bg-black/70 p-6 backdrop-blur-sm animate-ps-fade-in'
197
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
198
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'
199
- : '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, multiPage: multiPage, onAdjust: adjust, onRetake: retake, onUse: use, 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);
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);
200
186
  };
201
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, isMultiPage, isOpen, jpegQuality, labels, maxDimension, onClose, onComplete, pageLabels, }: 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, isMultiPage = false, isOpen, jpegQuality = ps_scan_config_1.PsScanConfig.defaultJpegQuality, labels, maxDimension = ps_scan_config_1.PsScanConfig.defaultMaxDimension, onClose, onComplete, pageLabels, }) => {
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, pageLabels: isMultiPage && pageLabels !== undefined ? pageLabels : 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, 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;
@@ -8,5 +8,6 @@ export interface PsFileListProps {
8
8
  labels?: Partial<PsLabelSet<typeof PsFileListLabels>>;
9
9
  onDelete?: (file: PsViewerFile) => void;
10
10
  onOpen: (index: number) => void;
11
+ onRename?: (file: PsViewerFile) => void;
11
12
  }
12
- export declare const PsFileList: ({ busyIds, files, formatDate, labels, onDelete, onOpen, }: PsFileListProps) => import("react").JSX.Element;
13
+ export declare const PsFileList: ({ busyIds, files, formatDate, labels, onDelete, onOpen, onRename, }: PsFileListProps) => import("react").JSX.Element;
@@ -9,7 +9,7 @@ const ps_file_size_service_1 = require("../services/ps-file-size-service");
9
9
  const ps_file_type_service_1 = require("../services/ps-file-type-service");
10
10
  const ps_file_icon_button_1 = require("./ps-file-icon-button");
11
11
  const ps_file_kind_icon_1 = require("./ps-file-kind-icon");
12
- const PsFileList = ({ busyIds = [], files, formatDate, labels, onDelete, onOpen, }) => {
12
+ const PsFileList = ({ busyIds = [], files, formatDate, labels, onDelete, onOpen, onRename, }) => {
13
13
  const text = { ...ps_file_list_labels_1.PsFileListLabels, ...labels };
14
14
  if (files.length === 0)
15
15
  return (0, jsx_runtime_1.jsx)("p", { className: 'text-sm text-ink/60', children: text.empty });
@@ -21,7 +21,7 @@ const PsFileList = ({ busyIds = [], files, formatDate, labels, onDelete, onOpen,
21
21
  ? null
22
22
  : formatDate(file.createdAt),
23
23
  ].filter((value) => value !== null);
24
- return ((0, jsx_runtime_1.jsxs)("li", { className: 'group flex min-w-0 items-center gap-3 rounded-ps-control border border-line bg-surface p-2 transition hover:border-primary/40', children: [(0, jsx_runtime_1.jsxs)("button", { "aria-label": `${text.open} ${file.name}`, className: 'ps-focus-ring flex min-w-0 flex-1 items-center gap-3 rounded-ps-control text-left', onClick: () => onOpen(index), type: 'button', children: [isImage ? ((0, jsx_runtime_1.jsx)("img", { alt: '', className: 'h-12 w-12 shrink-0 rounded-ps-control border border-line object-cover', loading: 'lazy', src: file.url })) : ((0, jsx_runtime_1.jsx)(ps_file_kind_icon_1.PsFileKindIcon, { className: 'h-12 w-12', contentType: file.contentType, name: file.name })), (0, jsx_runtime_1.jsxs)("span", { className: 'flex min-w-0 flex-col', children: [(0, jsx_runtime_1.jsx)("span", { className: 'truncate text-sm font-medium text-ink group-hover:text-primary', children: file.name }), meta.length > 0 ? ((0, jsx_runtime_1.jsx)("span", { className: 'truncate text-xs text-ink/55', children: meta.join(' · ') })) : null] })] }), (0, jsx_runtime_1.jsxs)("div", { className: 'flex shrink-0 items-center gap-0.5', children: [(0, jsx_runtime_1.jsx)(ps_file_icon_button_1.PsFileIconButton, { download: file.name, href: file.downloadUrl === undefined ? file.url : file.downloadUrl, iconKey: ps_icons_1.PsIconKey.Download, label: `${text.download} ${file.name}` }), onDelete ? ((0, jsx_runtime_1.jsx)(ps_file_icon_button_1.PsFileIconButton, { disabled: busyIds.includes(file.id), iconKey: ps_icons_1.PsIconKey.Trash, label: `${text.delete} ${file.name}`, onClick: () => onDelete(file), tone: 'danger' })) : null] })] }, file.id));
24
+ return ((0, jsx_runtime_1.jsxs)("li", { className: 'group flex min-w-0 items-center gap-3 rounded-ps-control border border-line bg-surface p-2 transition hover:border-primary/40', children: [(0, jsx_runtime_1.jsxs)("button", { "aria-label": `${text.open} ${file.name}`, className: 'ps-focus-ring flex min-w-0 flex-1 items-center gap-3 rounded-ps-control text-left', onClick: () => onOpen(index), type: 'button', children: [isImage ? ((0, jsx_runtime_1.jsx)("img", { alt: '', className: 'h-12 w-12 shrink-0 rounded-ps-control border border-line object-cover', loading: 'lazy', src: file.url })) : ((0, jsx_runtime_1.jsx)(ps_file_kind_icon_1.PsFileKindIcon, { className: 'h-12 w-12', contentType: file.contentType, name: file.name })), (0, jsx_runtime_1.jsxs)("span", { className: 'flex min-w-0 flex-col', children: [(0, jsx_runtime_1.jsx)("span", { className: 'truncate text-sm font-medium text-ink group-hover:text-primary', children: file.name }), meta.length > 0 ? ((0, jsx_runtime_1.jsx)("span", { className: 'truncate text-xs text-ink/55', children: meta.join(' · ') })) : null] })] }), (0, jsx_runtime_1.jsxs)("div", { className: 'flex shrink-0 items-center gap-0.5', children: [(0, jsx_runtime_1.jsx)(ps_file_icon_button_1.PsFileIconButton, { download: file.name, href: file.downloadUrl === undefined ? file.url : file.downloadUrl, iconKey: ps_icons_1.PsIconKey.Download, label: `${text.download} ${file.name}` }), onRename ? ((0, jsx_runtime_1.jsx)(ps_file_icon_button_1.PsFileIconButton, { disabled: busyIds.includes(file.id), iconKey: ps_icons_1.PsIconKey.Pencil, label: `${text.rename} ${file.name}`, onClick: () => onRename(file) })) : null, onDelete ? ((0, jsx_runtime_1.jsx)(ps_file_icon_button_1.PsFileIconButton, { disabled: busyIds.includes(file.id), iconKey: ps_icons_1.PsIconKey.Trash, label: `${text.delete} ${file.name}`, onClick: () => onDelete(file), tone: 'danger' })) : null] })] }, file.id));
25
25
  }) }));
26
26
  };
27
27
  exports.PsFileList = PsFileList;
@@ -1,14 +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';
3
4
  import type { PsScanMultiPage } from '../models/ps-scan-multi-page';
4
5
  import type { PsScanResult } from '../models/ps-scan-result';
5
6
  export interface PsScanPreviewProps {
6
7
  error: string;
7
8
  labels: PsLabelSet<typeof PsDocumentScannerLabels>;
9
+ formats: readonly PsScanFormat[];
10
+ isSaving: boolean;
8
11
  multiPage?: PsScanMultiPage;
9
12
  onAdjust: () => void;
10
13
  onRetake: () => void;
11
- onUse: () => void;
14
+ onSave: (format: PsScanFormat) => void;
15
+ preferredFormat: PsScanFormat;
12
16
  result: PsScanResult | null;
13
17
  }
14
- export declare const PsScanPreview: ({ error, labels, multiPage, 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,11 +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");
7
8
  const ps_scan_template_service_1 = require("../services/ps-scan-template-service");
8
9
  const ps_scan_button_1 = require("./ps-scan-button");
9
10
  const ps_scan_icon_button_1 = require("./ps-scan-icon-button");
10
11
  const ps_scan_page_strip_1 = require("./ps-scan-page-strip");
11
- const PsScanPreview = ({ error, labels, multiPage, 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 })), 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, {
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, {
12
13
  count: multiPage.pages.length + 1,
13
- }) })] })), (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 })), (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 })] })] }));
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 })] })] }));
14
15
  exports.PsScanPreview = PsScanPreview;
@@ -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
  };
@@ -3,4 +3,5 @@ export declare const PsFileListLabels: {
3
3
  readonly download: "Download";
4
4
  readonly empty: "No files yet";
5
5
  readonly open: "Open";
6
+ readonly rename: "Rename";
6
7
  };
@@ -6,4 +6,5 @@ exports.PsFileListLabels = {
6
6
  download: 'Download',
7
7
  empty: 'No files yet',
8
8
  open: 'Open',
9
+ rename: 'Rename',
9
10
  };
@@ -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;
@@ -38,7 +39,9 @@ export declare const PsScanConfig: {
38
39
  readonly loupeZoom: 2.5;
39
40
  readonly minCornerGap: 0.02;
40
41
  readonly multiPageMaxDimension: 2400;
41
- readonly pdfLongEdgePoints: 842;
42
+ readonly jpegType: "image/jpeg";
43
+ readonly pdfType: "application/pdf";
44
+ readonly pngType: "image/png";
42
45
  readonly polygonEpsilons: readonly [0.008, 0.012, 0.018, 0.026, 0.036, 0.05, 0.07];
43
46
  readonly torchConstraint: "torch";
44
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,
@@ -41,7 +43,9 @@ exports.PsScanConfig = {
41
43
  loupeZoom: 2.5,
42
44
  minCornerGap: 0.02,
43
45
  multiPageMaxDimension: 2400,
44
- pdfLongEdgePoints: 842,
46
+ jpegType: 'image/jpeg',
47
+ pdfType: 'application/pdf',
48
+ pngType: 'image/png',
45
49
  polygonEpsilons: [0.008, 0.012, 0.018, 0.026, 0.036, 0.05, 0.07],
46
50
  torchConstraint: 'torch',
47
51
  warpChunkPixels: 400000,
@@ -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
+ };
@@ -11,8 +11,7 @@ import { usePsFocusTrap } from '../hooks/use-ps-focus-trap';
11
11
  import { usePsScanCamera } from '../hooks/use-ps-scan-camera';
12
12
  import { usePsScanLiveDetection } from '../hooks/use-ps-scan-live-detection';
13
13
  import { usePsScanPhoto } from '../hooks/use-ps-scan-photo';
14
- import { PsScanFileNameService } from '../services/ps-scan-file-name-service';
15
- import { PsScanPdfService } from '../services/ps-scan-pdf-service';
14
+ import { PsScanOutputService } from '../services/ps-scan-output-service';
16
15
  import { PsScanPipelineService } from '../services/ps-scan-pipeline-service';
17
16
  import { PsScanCameraView } from './ps-scan-camera-view';
18
17
  import { PsScanCropEditor } from './ps-scan-crop-editor';
@@ -20,7 +19,7 @@ import { PsScanIconButton } from './ps-scan-icon-button';
20
19
  import { PsScanPageStrip } from './ps-scan-page-strip';
21
20
  import { PsScanPermissionView } from './ps-scan-permission-view';
22
21
  import { PsScanPreview } from './ps-scan-preview';
23
- export const PsDocumentScannerSheet = ({ fileName, jpegQuality, labels, maxDimension, onClose, onComplete, pageLabels, }) => {
22
+ export const PsDocumentScannerSheet = ({ fileName, formats, jpegQuality, labels, maxDimension, onClose, onComplete, pageLabels, }) => {
24
23
  const tier = usePsViewport();
25
24
  const zIndex = usePsLayerZIndex(PallasoftZIndex.Modal);
26
25
  const content = useRef(null);
@@ -143,20 +142,19 @@ export const PsDocumentScannerSheet = ({ fileName, jpegQuality, labels, maxDimen
143
142
  URL.revokeObjectURL(removed.url);
144
143
  setPages((current) => current.filter((page) => page.id !== id));
145
144
  };
146
- const useAsPdf = async () => {
147
- if (isAddingPage)
145
+ const save = async (format) => {
146
+ if (result === null || isAddingPage)
148
147
  return;
149
148
  setIsAddingPage(true);
150
149
  setError('');
151
150
  try {
152
- const last = await renderPage();
151
+ const last = pages.length === 0 ? { ...result, id: 0 } : await renderPage();
153
152
  if (last === null)
154
153
  return;
155
- const pdf = await PsScanPdfService.build([...pages, last]);
156
- URL.revokeObjectURL(last.url);
157
- onComplete([
158
- new File([pdf], PsScanFileNameService.buildPdf(fileName), { type: 'application/pdf' }),
159
- ]);
154
+ const files = await PsScanOutputService.build([...pages, last], format, fileName);
155
+ if (pages.length > 0)
156
+ URL.revokeObjectURL(last.url);
157
+ onComplete(files);
160
158
  onClose();
161
159
  }
162
160
  catch {
@@ -166,18 +164,6 @@ export const PsDocumentScannerSheet = ({ fileName, jpegQuality, labels, maxDimen
166
164
  setIsAddingPage(false);
167
165
  }
168
166
  };
169
- const use = () => {
170
- if (result === null)
171
- return;
172
- if (pages.length > 0) {
173
- void useAsPdf();
174
- return;
175
- }
176
- onComplete([
177
- new File([result.blob], PsScanFileNameService.build(fileName), { type: 'image/jpeg' }),
178
- ]);
179
- onClose();
180
- };
181
167
  const multiPage = pageLabels === null
182
168
  ? undefined
183
169
  : {
@@ -193,5 +179,5 @@ export const PsDocumentScannerSheet = ({ fileName, jpegQuality, labels, maxDimen
193
179
  ? 'fixed inset-0 flex items-center justify-center bg-black/70 p-6 backdrop-blur-sm animate-ps-fade-in'
194
180
  : 'fixed inset-0 bg-black animate-ps-fade-in', role: 'dialog', style: { zIndex }, children: _jsx(PsLayer, { zIndex: zIndex, children: _jsxs("div", { className: isDesktop
195
181
  ? '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'
196
- : 'relative h-full overflow-hidden bg-black text-white', ref: content, children: [_jsx("p", { "aria-live": 'polite', className: 'sr-only', role: 'status', children: announcement }), isCameraStep && showsCamera ? (_jsx(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 ? (_jsxs(_Fragment, { children: [_jsx(PsScanPermissionView, { labels: labels, onAllow: camera.allow, onFile: accept, status: camera.status }), _jsx("div", { className: 'absolute left-4 top-[max(1rem,env(safe-area-inset-top))] flex gap-2', children: _jsx(PsScanIconButton, { iconKey: PsIconKey.Close, label: labels.close, onClick: onClose }) })] })) : null, step === PsScanStep.Crop && photo !== null ? (_jsx(PsScanCropEditor, { labels: labels, onAutoDetect: autoDetect, onChange: setCorners, onContinue: flatten, onRetake: retake, photo: photo })) : null, step === PsScanStep.Preview ? (_jsx(PsScanPreview, { error: error, labels: labels, multiPage: multiPage, onAdjust: adjust, onRetake: retake, onUse: use, result: result })) : null, isCameraStep && pageLabels !== null && pages.length > 0 ? (_jsx("div", { className: 'absolute inset-x-0 bottom-32 flex justify-center', children: _jsx(PsScanPageStrip, { labels: pageLabels, onRemove: removePage, pages: pages }) })) : null, isCameraStep && error !== '' ? (_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);
182
+ : 'relative h-full overflow-hidden bg-black text-white', ref: content, children: [_jsx("p", { "aria-live": 'polite', className: 'sr-only', role: 'status', children: announcement }), isCameraStep && showsCamera ? (_jsx(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 ? (_jsxs(_Fragment, { children: [_jsx(PsScanPermissionView, { labels: labels, onAllow: camera.allow, onFile: accept, status: camera.status }), _jsx("div", { className: 'absolute left-4 top-[max(1rem,env(safe-area-inset-top))] flex gap-2', children: _jsx(PsScanIconButton, { iconKey: PsIconKey.Close, label: labels.close, onClick: onClose }) })] })) : null, step === PsScanStep.Crop && photo !== null ? (_jsx(PsScanCropEditor, { labels: labels, onAutoDetect: autoDetect, onChange: setCorners, onContinue: flatten, onRetake: retake, photo: photo })) : null, step === PsScanStep.Preview ? (_jsx(PsScanPreview, { error: error, isSaving: isAddingPage, labels: labels, multiPage: multiPage, onAdjust: adjust, onRetake: retake, onSave: save, formats: formats, preferredFormat: PsScanOutputService.preferred(pages.length + 1, formats), result: result })) : null, isCameraStep && pageLabels !== null && pages.length > 0 ? (_jsx("div", { className: 'absolute inset-x-0 bottom-32 flex justify-center', children: _jsx(PsScanPageStrip, { labels: pageLabels, onRemove: removePage, pages: pages }) })) : null, isCameraStep && error !== '' ? (_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);
197
183
  };
@@ -4,9 +4,9 @@ import { usePsHydrated } from '@pallasoft/ps-core-components';
4
4
  import { PsDocumentScannerLabels } from '../constants/ps-document-scanner-labels';
5
5
  import { PsScanConfig } from '../constants/ps-scan-config';
6
6
  import { PsDocumentScannerLoader } from './ps-document-scanner-loader';
7
- export const PsDocumentScanner = ({ fileName = PsScanConfig.defaultFileName, isMultiPage = false, isOpen, jpegQuality = PsScanConfig.defaultJpegQuality, labels, maxDimension = PsScanConfig.defaultMaxDimension, onClose, onComplete, pageLabels, }) => {
7
+ export const PsDocumentScanner = ({ fileName = PsScanConfig.defaultFileName, formats = PsScanConfig.formats, isMultiPage = false, isOpen, jpegQuality = PsScanConfig.defaultJpegQuality, labels, maxDimension = PsScanConfig.defaultMaxDimension, onClose, onComplete, pageLabels, }) => {
8
8
  const isHydrated = usePsHydrated();
9
9
  if (!isOpen || !isHydrated)
10
10
  return null;
11
- return (_jsx(Suspense, { fallback: null, children: _jsx(PsDocumentScannerLoader, { fileName: fileName, jpegQuality: jpegQuality, labels: { ...PsDocumentScannerLabels, ...labels }, maxDimension: maxDimension, onClose: onClose, onComplete: onComplete, pageLabels: isMultiPage && pageLabels !== undefined ? pageLabels : null }) }));
11
+ return (_jsx(Suspense, { fallback: null, children: _jsx(PsDocumentScannerLoader, { fileName: fileName, formats: formats, jpegQuality: jpegQuality, labels: { ...PsDocumentScannerLabels, ...labels }, maxDimension: maxDimension, onClose: onClose, onComplete: onComplete, pageLabels: isMultiPage && pageLabels !== undefined ? pageLabels : null }) }));
12
12
  };
@@ -6,7 +6,7 @@ import { PsFileSizeService } from '../services/ps-file-size-service';
6
6
  import { PsFileTypeService } from '../services/ps-file-type-service';
7
7
  import { PsFileIconButton } from './ps-file-icon-button';
8
8
  import { PsFileKindIcon } from './ps-file-kind-icon';
9
- export const PsFileList = ({ busyIds = [], files, formatDate, labels, onDelete, onOpen, }) => {
9
+ export const PsFileList = ({ busyIds = [], files, formatDate, labels, onDelete, onOpen, onRename, }) => {
10
10
  const text = { ...PsFileListLabels, ...labels };
11
11
  if (files.length === 0)
12
12
  return _jsx("p", { className: 'text-sm text-ink/60', children: text.empty });
@@ -18,6 +18,6 @@ export const PsFileList = ({ busyIds = [], files, formatDate, labels, onDelete,
18
18
  ? null
19
19
  : formatDate(file.createdAt),
20
20
  ].filter((value) => value !== null);
21
- return (_jsxs("li", { className: 'group flex min-w-0 items-center gap-3 rounded-ps-control border border-line bg-surface p-2 transition hover:border-primary/40', children: [_jsxs("button", { "aria-label": `${text.open} ${file.name}`, className: 'ps-focus-ring flex min-w-0 flex-1 items-center gap-3 rounded-ps-control text-left', onClick: () => onOpen(index), type: 'button', children: [isImage ? (_jsx("img", { alt: '', className: 'h-12 w-12 shrink-0 rounded-ps-control border border-line object-cover', loading: 'lazy', src: file.url })) : (_jsx(PsFileKindIcon, { className: 'h-12 w-12', contentType: file.contentType, name: file.name })), _jsxs("span", { className: 'flex min-w-0 flex-col', children: [_jsx("span", { className: 'truncate text-sm font-medium text-ink group-hover:text-primary', children: file.name }), meta.length > 0 ? (_jsx("span", { className: 'truncate text-xs text-ink/55', children: meta.join(' · ') })) : null] })] }), _jsxs("div", { className: 'flex shrink-0 items-center gap-0.5', children: [_jsx(PsFileIconButton, { download: file.name, href: file.downloadUrl === undefined ? file.url : file.downloadUrl, iconKey: PsIconKey.Download, label: `${text.download} ${file.name}` }), onDelete ? (_jsx(PsFileIconButton, { disabled: busyIds.includes(file.id), iconKey: PsIconKey.Trash, label: `${text.delete} ${file.name}`, onClick: () => onDelete(file), tone: 'danger' })) : null] })] }, file.id));
21
+ return (_jsxs("li", { className: 'group flex min-w-0 items-center gap-3 rounded-ps-control border border-line bg-surface p-2 transition hover:border-primary/40', children: [_jsxs("button", { "aria-label": `${text.open} ${file.name}`, className: 'ps-focus-ring flex min-w-0 flex-1 items-center gap-3 rounded-ps-control text-left', onClick: () => onOpen(index), type: 'button', children: [isImage ? (_jsx("img", { alt: '', className: 'h-12 w-12 shrink-0 rounded-ps-control border border-line object-cover', loading: 'lazy', src: file.url })) : (_jsx(PsFileKindIcon, { className: 'h-12 w-12', contentType: file.contentType, name: file.name })), _jsxs("span", { className: 'flex min-w-0 flex-col', children: [_jsx("span", { className: 'truncate text-sm font-medium text-ink group-hover:text-primary', children: file.name }), meta.length > 0 ? (_jsx("span", { className: 'truncate text-xs text-ink/55', children: meta.join(' · ') })) : null] })] }), _jsxs("div", { className: 'flex shrink-0 items-center gap-0.5', children: [_jsx(PsFileIconButton, { download: file.name, href: file.downloadUrl === undefined ? file.url : file.downloadUrl, iconKey: PsIconKey.Download, label: `${text.download} ${file.name}` }), onRename ? (_jsx(PsFileIconButton, { disabled: busyIds.includes(file.id), iconKey: PsIconKey.Pencil, label: `${text.rename} ${file.name}`, onClick: () => onRename(file) })) : null, onDelete ? (_jsx(PsFileIconButton, { disabled: busyIds.includes(file.id), iconKey: PsIconKey.Trash, label: `${text.delete} ${file.name}`, onClick: () => onDelete(file), tone: 'danger' })) : null] })] }, file.id));
22
22
  }) }));
23
23
  };
@@ -1,10 +1,11 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { PsSpinner } from '@pallasoft/ps-core-components';
3
3
  import { PsIcon, PsIconKey } from '@pallasoft/ps-icons';
4
+ import { PsScanFormat } from '../constants/ps-scan-format';
4
5
  import { PsScanTemplateService } from '../services/ps-scan-template-service';
5
6
  import { PsScanButton } from './ps-scan-button';
6
7
  import { PsScanIconButton } from './ps-scan-icon-button';
7
8
  import { PsScanPageStrip } from './ps-scan-page-strip';
8
- export const PsScanPreview = ({ error, labels, multiPage, 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 })), multiPage === undefined || multiPage.pages.length === 0 ? null : (_jsxs("div", { className: 'shrink-0', children: [_jsx(PsScanPageStrip, { labels: multiPage.labels, onRemove: multiPage.onRemovePage, pages: multiPage.pages }), _jsx("p", { className: 'text-center text-sm text-white/75', children: PsScanTemplateService.fill(multiPage.labels.pageCount, {
9
+ export const PsScanPreview = ({ error, formats, isSaving, labels, multiPage, onAdjust, onRetake, onSave, preferredFormat, 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 })), multiPage === undefined || multiPage.pages.length === 0 ? null : (_jsxs("div", { className: 'shrink-0', children: [_jsx(PsScanPageStrip, { labels: multiPage.labels, onRemove: multiPage.onRemovePage, pages: multiPage.pages }), _jsx("p", { className: 'text-center text-sm text-white/75', children: PsScanTemplateService.fill(multiPage.labels.pageCount, {
9
10
  count: multiPage.pages.length + 1,
10
- }) })] })), _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 : (_jsx(PsScanButton, { className: 'col-span-2', disabled: result === null, isLoading: multiPage.isBusy, isLarge: true, leadingIcon: _jsx(PsIcon, { "aria-hidden": 'true', className: 'size-4', iconKey: PsIconKey.Plus }), onClick: multiPage.onAddPage, children: multiPage.labels.addPage })), _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 })] })] }));
11
+ }) })] })), _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 : (_jsx(PsScanButton, { className: 'col-span-2', disabled: result === null, isLoading: multiPage.isBusy, isLarge: true, leadingIcon: _jsx(PsIcon, { "aria-hidden": 'true', className: 'size-4', iconKey: PsIconKey.Plus }), onClick: multiPage.onAddPage, children: multiPage.labels.addPage })), formats.includes(PsScanFormat.Pdf) ? (_jsx(PsScanButton, { className: formats.length === 1 ? 'col-span-2' : undefined, disabled: result === null, isLarge: true, isLoading: isSaving, isPrimary: preferredFormat === PsScanFormat.Pdf, leadingIcon: _jsx(PsIcon, { "aria-hidden": 'true', className: 'size-4', iconKey: PsIconKey.File }), onClick: () => onSave(PsScanFormat.Pdf), children: labels.saveAsPdf })) : null, formats.includes(PsScanFormat.Png) ? (_jsx(PsScanButton, { className: formats.length === 1 ? 'col-span-2' : undefined, disabled: result === null, isLarge: true, isLoading: isSaving, isPrimary: preferredFormat === PsScanFormat.Png, leadingIcon: _jsx(PsIcon, { "aria-hidden": 'true', className: 'size-4', iconKey: PsIconKey.Image }), onClick: () => onSave(PsScanFormat.Png), children: labels.saveAsImages })) : null, _jsx(PsScanButton, { className: 'col-span-2', disabled: isSaving, isLarge: true, onClick: onRetake, children: labels.retake })] })] }));
@@ -41,11 +41,12 @@ export const PsDocumentScannerLabels = {
41
41
  primerTitle: 'Allow camera access to scan your documents',
42
42
  processFailed: 'The scan could not be prepared. Try again.',
43
43
  retake: 'Retake',
44
+ saveAsImages: 'Save as images',
45
+ saveAsPdf: 'Save as PDF',
44
46
  title: 'Scan document',
45
47
  torchOff: 'Turn flash off',
46
48
  torchOn: 'Turn flash on',
47
49
  tryAgain: 'Try again',
48
50
  unavailableBody: 'This device has no camera the browser can use. Choose a photo of the page instead.',
49
51
  unavailableTitle: 'No camera found',
50
- useScan: 'Use scan',
51
52
  };
@@ -3,4 +3,5 @@ export const PsFileListLabels = {
3
3
  download: 'Download',
4
4
  empty: 'No files yet',
5
5
  open: 'Open',
6
+ rename: 'Rename',
6
7
  };
@@ -1,3 +1,4 @@
1
+ import { PsScanFormat } from './ps-scan-format';
1
2
  export const PsScanConfig = {
2
3
  cameraPermission: 'camera',
3
4
  cannyHighPercentile: 0.9,
@@ -20,6 +21,7 @@ export const PsScanConfig = {
20
21
  detectMinAreaRatio: 0.1,
21
22
  detectMinQuadFill: 0.88,
22
23
  detectMinSideRatio: 0.06,
24
+ formats: [PsScanFormat.Pdf, PsScanFormat.Png],
23
25
  guideArm: 36,
24
26
  guideArmRatio: 0.18,
25
27
  guideAspect: 0.72,
@@ -38,7 +40,9 @@ export const PsScanConfig = {
38
40
  loupeZoom: 2.5,
39
41
  minCornerGap: 0.02,
40
42
  multiPageMaxDimension: 2400,
41
- pdfLongEdgePoints: 842,
43
+ jpegType: 'image/jpeg',
44
+ pdfType: 'application/pdf',
45
+ pngType: 'image/png',
42
46
  polygonEpsilons: [0.008, 0.012, 0.018, 0.026, 0.036, 0.05, 0.07],
43
47
  torchConstraint: 'torch',
44
48
  warpChunkPixels: 400000,
@@ -0,0 +1,4 @@
1
+ export const PsScanFormat = {
2
+ Pdf: 'pdf',
3
+ Png: 'png',
4
+ };
package/dist/esm/index.js CHANGED
@@ -74,6 +74,7 @@ export * from './hooks/use-ps-in-view';
74
74
  export * from './hooks/use-ps-upload-queue';
75
75
  export * from './components/ps-document-scanner';
76
76
  export * from './constants/ps-document-scanner-labels';
77
+ export * from './constants/ps-scan-format';
77
78
  export * from './models/ps-document-scanner-page-labels';
78
79
  export * from './models/ps-document-scanner-props';
79
80
  export * from './models/ps-file-picker-scan';
@@ -28,6 +28,16 @@ export class PsScanCanvasService {
28
28
  return await new Promise((resolve, reject) => canvas.toBlob((blob) => (blob === null ? reject(new Error('encode')) : resolve(blob)), 'image/jpeg', quality));
29
29
  return await canvas.convertToBlob({ quality, type: 'image/jpeg' });
30
30
  }
31
+ static async toPng(blob) {
32
+ const bitmap = await createImageBitmap(blob);
33
+ const context = PsScanCanvasService.context(bitmap.width, bitmap.height);
34
+ context.drawImage(bitmap, 0, 0);
35
+ bitmap.close();
36
+ const canvas = context.canvas;
37
+ if (canvas instanceof HTMLCanvasElement)
38
+ return await new Promise((resolve, reject) => canvas.toBlob((png) => (png === null ? reject(new Error('encode')) : resolve(png)), PsScanConfig.pngType));
39
+ return await canvas.convertToBlob({ type: PsScanConfig.pngType });
40
+ }
31
41
  static async fromVideo(video, quality) {
32
42
  const context = PsScanCanvasService.context(video.videoWidth, video.videoHeight);
33
43
  context.drawImage(video, 0, 0);
@@ -3,6 +3,10 @@ export class PsScanFileNameService {
3
3
  static build(base) {
4
4
  return `${PsScanFileNameService.sanitize(base)}.jpg`;
5
5
  }
6
+ static buildPng(base, index, count) {
7
+ const name = PsScanFileNameService.sanitize(base);
8
+ return count > 1 ? `${name}-${index + 1}.png` : `${name}.png`;
9
+ }
6
10
  static buildPdf(base) {
7
11
  return `${PsScanFileNameService.sanitize(base)}.pdf`;
8
12
  }
@@ -10,7 +14,7 @@ export class PsScanFileNameService {
10
14
  const cleaned = Array.from(base)
11
15
  .filter((char) => char.charCodeAt(0) >= 32)
12
16
  .join('')
13
- .replace(/\.(pdf|jpe?g)$/i, '')
17
+ .replace(/\.(pdf|jpe?g|png)$/i, '')
14
18
  .replace(/[\\/:*?"<>|]+/g, '-')
15
19
  .replace(/\s+/g, ' ')
16
20
  .replace(/^[\s.-]+|[\s.-]+$/g, '');
@@ -0,0 +1,23 @@
1
+ import { PsScanConfig } from '../constants/ps-scan-config';
2
+ import { PsScanFormat } from '../constants/ps-scan-format';
3
+ import { PsScanCanvasService } from './ps-scan-canvas-service';
4
+ import { PsScanFileNameService } from './ps-scan-file-name-service';
5
+ import { PsScanPdfService } from './ps-scan-pdf-service';
6
+ export class PsScanOutputService {
7
+ static async build(pages, format, fileName) {
8
+ if (format === PsScanFormat.Pdf)
9
+ return [
10
+ new File([await PsScanPdfService.build(pages)], PsScanFileNameService.buildPdf(fileName), {
11
+ type: PsScanConfig.pdfType,
12
+ }),
13
+ ];
14
+ const files = [];
15
+ for (const [index, page] of pages.entries())
16
+ files.push(new File([await PsScanCanvasService.toPng(page.blob)], PsScanFileNameService.buildPng(fileName, index, pages.length), { type: PsScanConfig.pngType }));
17
+ return files;
18
+ }
19
+ static preferred(pageCount, formats) {
20
+ const wanted = pageCount > 1 ? PsScanFormat.Pdf : PsScanFormat.Png;
21
+ return formats.includes(wanted) ? wanted : formats[0];
22
+ }
23
+ }
@@ -1,3 +1,4 @@
1
+ import { Common } from '@pallasoft/ps-utilities';
1
2
  import { PsScanConfig } from '../constants/ps-scan-config';
2
3
  export class PsScanPdfService {
3
4
  static async build(pages) {
@@ -11,59 +12,9 @@ export class PsScanPdfService {
11
12
  return PsScanPdfService.fromJpegs(loaded);
12
13
  }
13
14
  static fromJpegs(pages) {
14
- const chunks = [];
15
- const offsets = [];
16
- let length = 0;
17
- const push = (chunk) => {
18
- chunks.push(chunk);
19
- length += chunk.length;
20
- };
21
- const begin = (objectNumber) => {
22
- offsets[objectNumber] = length;
23
- push(PsScanPdfService.text(`${objectNumber} 0 obj\n`));
24
- };
25
- push(PsScanPdfService.text('%PDF-1.4\n'));
26
- push(new Uint8Array([0x25, 0xe2, 0xe3, 0xcf, 0xd3, 0x0a]));
27
- begin(1);
28
- push(PsScanPdfService.text('<< /Type /Catalog /Pages 2 0 R >>\nendobj\n'));
29
- const kids = pages.map((_, index) => `${PsScanPdfService.pageObject(index)} 0 R`).join(' ');
30
- begin(2);
31
- push(PsScanPdfService.text(`<< /Type /Pages /Kids [${kids}] /Count ${pages.length} >>\nendobj\n`));
32
- pages.forEach((page, index) => {
33
- const size = PsScanPdfService.pageSize(page);
34
- const pageNumber = PsScanPdfService.pageObject(index);
35
- const content = `q ${size.width} 0 0 ${size.height} 0 0 cm /Im0 Do Q`;
36
- begin(pageNumber);
37
- push(PsScanPdfService.text(`<< /Type /Page /Parent 2 0 R /MediaBox [0 0 ${size.width} ${size.height}] /Resources << /XObject << /Im0 ${pageNumber + 1} 0 R >> >> /Contents ${pageNumber + 2} 0 R >>\nendobj\n`));
38
- begin(pageNumber + 1);
39
- push(PsScanPdfService.text(`<< /Type /XObject /Subtype /Image /Width ${page.width} /Height ${page.height} /ColorSpace /DeviceRGB /BitsPerComponent 8 /Filter /DCTDecode /Length ${page.bytes.length} >>\nstream\n`));
40
- push(page.bytes);
41
- push(PsScanPdfService.text('\nendstream\nendobj\n'));
42
- begin(pageNumber + 2);
43
- push(PsScanPdfService.text(`<< /Length ${content.length} >>\nstream\n${content}\nendstream\nendobj\n`));
44
- });
45
- const count = 3 + pages.length * 3;
46
- const entries = Array.from({ length: count - 1 }, (_, index) => `${String(offsets[index + 1]).padStart(10, '0')} 00000 n \n`).join('');
47
- push(PsScanPdfService.text(`xref\n0 ${count}\n0000000000 65535 f \n${entries}trailer\n<< /Size ${count} /Root 1 0 R >>\nstartxref\n${length}\n%%EOF\n`));
48
- const merged = new Uint8Array(length);
49
- let cursor = 0;
50
- for (const chunk of chunks) {
51
- merged.set(chunk, cursor);
52
- cursor += chunk.length;
53
- }
54
- return new Blob([merged], { type: 'application/pdf' });
15
+ return new Blob([Common.JpegPdfService.fromJpegs(pages)], { type: PsScanConfig.pdfType });
55
16
  }
56
17
  static pageSize(page) {
57
- const scale = PsScanConfig.pdfLongEdgePoints / Math.max(page.width, page.height);
58
- return {
59
- height: Math.round(page.height * scale * 100) / 100,
60
- width: Math.round(page.width * scale * 100) / 100,
61
- };
62
- }
63
- static pageObject(index) {
64
- return 3 + index * 3;
65
- }
66
- static text(value) {
67
- return new TextEncoder().encode(value);
18
+ return Common.JpegPdfService.pageSize(page);
68
19
  }
69
20
  }
package/dist/index.d.ts CHANGED
@@ -74,6 +74,7 @@ export * from './hooks/use-ps-in-view';
74
74
  export * from './hooks/use-ps-upload-queue';
75
75
  export * from './components/ps-document-scanner';
76
76
  export * from './constants/ps-document-scanner-labels';
77
+ export * from './constants/ps-scan-format';
77
78
  export * from './models/ps-document-scanner-page-labels';
78
79
  export * from './models/ps-document-scanner-props';
79
80
  export * from './models/ps-file-picker-scan';
package/dist/index.js CHANGED
@@ -90,6 +90,7 @@ __exportStar(require("./hooks/use-ps-in-view"), exports);
90
90
  __exportStar(require("./hooks/use-ps-upload-queue"), exports);
91
91
  __exportStar(require("./components/ps-document-scanner"), exports);
92
92
  __exportStar(require("./constants/ps-document-scanner-labels"), exports);
93
+ __exportStar(require("./constants/ps-scan-format"), exports);
93
94
  __exportStar(require("./models/ps-document-scanner-page-labels"), exports);
94
95
  __exportStar(require("./models/ps-document-scanner-props"), exports);
95
96
  __exportStar(require("./models/ps-file-picker-scan"), exports);
@@ -1,9 +1,11 @@
1
1
  import type { PsDocumentScannerLabels } from '../constants/ps-document-scanner-labels';
2
+ import type { PsScanFormat } from '../constants/ps-scan-format';
2
3
  import type { PsDocumentScannerPageLabels } from './ps-document-scanner-page-labels';
3
4
  import type { PsLabelSet } from './ps-label-set';
4
5
  export interface PsDocumentScannerProps {
5
6
  className?: string;
6
7
  fileName?: string;
8
+ formats?: readonly PsScanFormat[];
7
9
  isMultiPage?: boolean;
8
10
  isOpen: boolean;
9
11
  jpegQuality?: number;
@@ -1,8 +1,10 @@
1
1
  import type { PsDocumentScannerLabels } from '../constants/ps-document-scanner-labels';
2
+ import type { PsScanFormat } from '../constants/ps-scan-format';
2
3
  import type { PsDocumentScannerPageLabels } from './ps-document-scanner-page-labels';
3
4
  import type { PsLabelSet } from './ps-label-set';
4
5
  export interface PsDocumentScannerSheetProps {
5
6
  fileName: string;
7
+ formats: readonly PsScanFormat[];
6
8
  jpegQuality: number;
7
9
  labels: PsLabelSet<typeof PsDocumentScannerLabels>;
8
10
  maxDimension: number;
@@ -3,6 +3,7 @@ import type { PsScanRaster } from '../models/ps-scan-raster';
3
3
  export declare class PsScanCanvasService {
4
4
  static decode(blob: Blob, maxDimension: number): Promise<PsScanDecodedImage>;
5
5
  static encode(raster: PsScanRaster, quality: number): Promise<Blob>;
6
+ static toPng(blob: Blob): Promise<Blob>;
6
7
  static fromVideo(video: HTMLVideoElement, quality: number): Promise<Blob>;
7
8
  static sample(video: HTMLVideoElement, maxDimension: number): PsScanRaster;
8
9
  private static context;
@@ -31,6 +31,16 @@ class PsScanCanvasService {
31
31
  return await new Promise((resolve, reject) => canvas.toBlob((blob) => (blob === null ? reject(new Error('encode')) : resolve(blob)), 'image/jpeg', quality));
32
32
  return await canvas.convertToBlob({ quality, type: 'image/jpeg' });
33
33
  }
34
+ static async toPng(blob) {
35
+ const bitmap = await createImageBitmap(blob);
36
+ const context = PsScanCanvasService.context(bitmap.width, bitmap.height);
37
+ context.drawImage(bitmap, 0, 0);
38
+ bitmap.close();
39
+ const canvas = context.canvas;
40
+ if (canvas instanceof HTMLCanvasElement)
41
+ return await new Promise((resolve, reject) => canvas.toBlob((png) => (png === null ? reject(new Error('encode')) : resolve(png)), ps_scan_config_1.PsScanConfig.pngType));
42
+ return await canvas.convertToBlob({ type: ps_scan_config_1.PsScanConfig.pngType });
43
+ }
34
44
  static async fromVideo(video, quality) {
35
45
  const context = PsScanCanvasService.context(video.videoWidth, video.videoHeight);
36
46
  context.drawImage(video, 0, 0);
@@ -1,5 +1,6 @@
1
1
  export declare class PsScanFileNameService {
2
2
  static build(base: string): string;
3
+ static buildPng(base: string, index: number, count: number): string;
3
4
  static buildPdf(base: string): string;
4
5
  static sanitize(base: string): string;
5
6
  }
@@ -6,6 +6,10 @@ class PsScanFileNameService {
6
6
  static build(base) {
7
7
  return `${PsScanFileNameService.sanitize(base)}.jpg`;
8
8
  }
9
+ static buildPng(base, index, count) {
10
+ const name = PsScanFileNameService.sanitize(base);
11
+ return count > 1 ? `${name}-${index + 1}.png` : `${name}.png`;
12
+ }
9
13
  static buildPdf(base) {
10
14
  return `${PsScanFileNameService.sanitize(base)}.pdf`;
11
15
  }
@@ -13,7 +17,7 @@ class PsScanFileNameService {
13
17
  const cleaned = Array.from(base)
14
18
  .filter((char) => char.charCodeAt(0) >= 32)
15
19
  .join('')
16
- .replace(/\.(pdf|jpe?g)$/i, '')
20
+ .replace(/\.(pdf|jpe?g|png)$/i, '')
17
21
  .replace(/[\\/:*?"<>|]+/g, '-')
18
22
  .replace(/\s+/g, ' ')
19
23
  .replace(/^[\s.-]+|[\s.-]+$/g, '');
@@ -0,0 +1,6 @@
1
+ import { PsScanFormat } from '../constants/ps-scan-format';
2
+ import type { PsScanPage } from '../models/ps-scan-page';
3
+ export declare class PsScanOutputService {
4
+ static build(pages: PsScanPage[], format: PsScanFormat, fileName: string): Promise<File[]>;
5
+ static preferred(pageCount: number, formats: readonly PsScanFormat[]): PsScanFormat;
6
+ }
@@ -0,0 +1,27 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PsScanOutputService = void 0;
4
+ const ps_scan_config_1 = require("../constants/ps-scan-config");
5
+ const ps_scan_format_1 = require("../constants/ps-scan-format");
6
+ const ps_scan_canvas_service_1 = require("./ps-scan-canvas-service");
7
+ const ps_scan_file_name_service_1 = require("./ps-scan-file-name-service");
8
+ const ps_scan_pdf_service_1 = require("./ps-scan-pdf-service");
9
+ class PsScanOutputService {
10
+ static async build(pages, format, fileName) {
11
+ if (format === ps_scan_format_1.PsScanFormat.Pdf)
12
+ return [
13
+ new File([await ps_scan_pdf_service_1.PsScanPdfService.build(pages)], ps_scan_file_name_service_1.PsScanFileNameService.buildPdf(fileName), {
14
+ type: ps_scan_config_1.PsScanConfig.pdfType,
15
+ }),
16
+ ];
17
+ const files = [];
18
+ for (const [index, page] of pages.entries())
19
+ files.push(new File([await ps_scan_canvas_service_1.PsScanCanvasService.toPng(page.blob)], ps_scan_file_name_service_1.PsScanFileNameService.buildPng(fileName, index, pages.length), { type: ps_scan_config_1.PsScanConfig.pngType }));
20
+ return files;
21
+ }
22
+ static preferred(pageCount, formats) {
23
+ const wanted = pageCount > 1 ? ps_scan_format_1.PsScanFormat.Pdf : ps_scan_format_1.PsScanFormat.Png;
24
+ return formats.includes(wanted) ? wanted : formats[0];
25
+ }
26
+ }
27
+ exports.PsScanOutputService = PsScanOutputService;
@@ -5,6 +5,4 @@ export declare class PsScanPdfService {
5
5
  static build(pages: PsScanPage[]): Promise<Blob>;
6
6
  static fromJpegs(pages: PsScanPdfPage[]): Blob;
7
7
  static pageSize(page: PsScanPdfPage): PsPageSize;
8
- private static pageObject;
9
- private static text;
10
8
  }
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.PsScanPdfService = void 0;
4
+ const ps_utilities_1 = require("@pallasoft/ps-utilities");
4
5
  const ps_scan_config_1 = require("../constants/ps-scan-config");
5
6
  class PsScanPdfService {
6
7
  static async build(pages) {
@@ -14,60 +15,10 @@ class PsScanPdfService {
14
15
  return PsScanPdfService.fromJpegs(loaded);
15
16
  }
16
17
  static fromJpegs(pages) {
17
- const chunks = [];
18
- const offsets = [];
19
- let length = 0;
20
- const push = (chunk) => {
21
- chunks.push(chunk);
22
- length += chunk.length;
23
- };
24
- const begin = (objectNumber) => {
25
- offsets[objectNumber] = length;
26
- push(PsScanPdfService.text(`${objectNumber} 0 obj\n`));
27
- };
28
- push(PsScanPdfService.text('%PDF-1.4\n'));
29
- push(new Uint8Array([0x25, 0xe2, 0xe3, 0xcf, 0xd3, 0x0a]));
30
- begin(1);
31
- push(PsScanPdfService.text('<< /Type /Catalog /Pages 2 0 R >>\nendobj\n'));
32
- const kids = pages.map((_, index) => `${PsScanPdfService.pageObject(index)} 0 R`).join(' ');
33
- begin(2);
34
- push(PsScanPdfService.text(`<< /Type /Pages /Kids [${kids}] /Count ${pages.length} >>\nendobj\n`));
35
- pages.forEach((page, index) => {
36
- const size = PsScanPdfService.pageSize(page);
37
- const pageNumber = PsScanPdfService.pageObject(index);
38
- const content = `q ${size.width} 0 0 ${size.height} 0 0 cm /Im0 Do Q`;
39
- begin(pageNumber);
40
- push(PsScanPdfService.text(`<< /Type /Page /Parent 2 0 R /MediaBox [0 0 ${size.width} ${size.height}] /Resources << /XObject << /Im0 ${pageNumber + 1} 0 R >> >> /Contents ${pageNumber + 2} 0 R >>\nendobj\n`));
41
- begin(pageNumber + 1);
42
- push(PsScanPdfService.text(`<< /Type /XObject /Subtype /Image /Width ${page.width} /Height ${page.height} /ColorSpace /DeviceRGB /BitsPerComponent 8 /Filter /DCTDecode /Length ${page.bytes.length} >>\nstream\n`));
43
- push(page.bytes);
44
- push(PsScanPdfService.text('\nendstream\nendobj\n'));
45
- begin(pageNumber + 2);
46
- push(PsScanPdfService.text(`<< /Length ${content.length} >>\nstream\n${content}\nendstream\nendobj\n`));
47
- });
48
- const count = 3 + pages.length * 3;
49
- const entries = Array.from({ length: count - 1 }, (_, index) => `${String(offsets[index + 1]).padStart(10, '0')} 00000 n \n`).join('');
50
- push(PsScanPdfService.text(`xref\n0 ${count}\n0000000000 65535 f \n${entries}trailer\n<< /Size ${count} /Root 1 0 R >>\nstartxref\n${length}\n%%EOF\n`));
51
- const merged = new Uint8Array(length);
52
- let cursor = 0;
53
- for (const chunk of chunks) {
54
- merged.set(chunk, cursor);
55
- cursor += chunk.length;
56
- }
57
- return new Blob([merged], { type: 'application/pdf' });
18
+ return new Blob([ps_utilities_1.Common.JpegPdfService.fromJpegs(pages)], { type: ps_scan_config_1.PsScanConfig.pdfType });
58
19
  }
59
20
  static pageSize(page) {
60
- const scale = ps_scan_config_1.PsScanConfig.pdfLongEdgePoints / Math.max(page.width, page.height);
61
- return {
62
- height: Math.round(page.height * scale * 100) / 100,
63
- width: Math.round(page.width * scale * 100) / 100,
64
- };
65
- }
66
- static pageObject(index) {
67
- return 3 + index * 3;
68
- }
69
- static text(value) {
70
- return new TextEncoder().encode(value);
21
+ return ps_utilities_1.Common.JpegPdfService.pageSize(page);
71
22
  }
72
23
  }
73
24
  exports.PsScanPdfService = PsScanPdfService;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pallasoft/ps-file-components",
3
- "version": "2.1.0",
3
+ "version": "2.3.0",
4
4
  "private": false,
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/esm/index.js",
@@ -33,7 +33,7 @@
33
33
  "@pallasoft/ps-core-components": "^2.37.0",
34
34
  "@pallasoft/ps-icons": "^1.9.0",
35
35
  "@pallasoft/ps-popovers": "^1.13.0",
36
- "@pallasoft/ps-utilities": "^2.19.1",
36
+ "@pallasoft/ps-utilities": "^2.28.0",
37
37
  "pdfjs-dist": "^6.3.289"
38
38
  },
39
39
  "peerDependencies": {