@scanfit/browser 0.1.0-alpha.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.
@@ -0,0 +1 @@
1
+ .sf-scanner{--sf-accent: #22644c;--sf-accent-soft: #edf5ef;--sf-ink: #202e27;--sf-muted: #66736c;--sf-border: #dce4dc;--sf-surface: #fff;--sf-radius: 16px;color:var(--sf-ink);background:var(--sf-surface);font:14px/1.5 ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;border:1px solid var(--sf-border);border-radius:var(--sf-radius);overflow:hidden;text-align:start}.sf-scanner *{box-sizing:border-box}.sf-scanner button,.sf-scanner select{font:inherit;color:inherit;min-height:40px;border:1px solid var(--sf-border);border-radius:8px;background:var(--sf-surface);padding:8px 12px;cursor:pointer}.sf-scanner button:hover:not(:disabled){border-color:var(--sf-accent);background:var(--sf-accent-soft)}.sf-scanner button:disabled{opacity:.5;cursor:not-allowed}.sf-scanner :focus-visible{outline:3px solid #4d9a7d;outline-offset:3px}.sf-scanner h2,.sf-scanner h3,.sf-scanner h4,.sf-scanner p{margin:0}.sf-scanner h2{font-size:20px;letter-spacing:-.5px}.sf-scanner h3{font-size:20px;letter-spacing:-.4px}.sf-scanner p{color:var(--sf-muted)}.sf-header{padding:24px 28px;display:flex;justify-content:space-between;align-items:center;border-bottom:1px solid var(--sf-border);gap:16px}.sf-header p{margin-top:4px}.sf-scanner .sf-primary{background:var(--sf-accent);border-color:var(--sf-accent);color:#fff;font-weight:600;display:inline-flex;align-items:center;justify-content:center;gap:20px}.sf-scanner .sf-primary:hover:not(:disabled){background:#174c39;color:#fff}.sf-scanner .sf-icon-button{font-size:24px;line-height:1;width:40px;padding:0}.sf-file-input{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%)}.sf-empty{padding:54px 24px 48px;text-align:center;background:radial-gradient(ellipse at 50% 20%,#f0f7ed,transparent 65%)}.sf-empty h3{font-size:25px;margin-top:28px}.sf-empty>p{max-width:360px;margin:10px auto 24px}.sf-empty-actions{display:flex;justify-content:center;gap:10px;margin-bottom:16px}.sf-empty small{color:var(--sf-muted);font-size:12px}.sf-paper-illustration{height:122px;width:92px;margin:auto;background:#fff;box-shadow:0 8px 24px #314d2312;border:1px solid #cbd9c7;transform:rotate(-8deg);border-radius:8px;padding:25px 16px;position:relative}.sf-paper-illustration span{display:block;height:4px;background:#d9e4d4;margin-bottom:9px;border-radius:2px}.sf-paper-illustration span:nth-child(2){width:75%}.sf-paper-illustration span:nth-child(4){width:65%}.sf-paper-illustration i{position:absolute;right:-15px;bottom:10px;width:37px;height:37px;display:grid;place-items:center;background:var(--sf-accent);color:#fff;font:24px system-ui;border:5px solid #f1f7ed;border-radius:50%;box-sizing:content-box}.sf-privacy{padding:16px 24px;border-top:1px solid var(--sf-border);font-size:12px;text-align:center;color:var(--sf-accent);background:#fafcf9}.sf-privacy small{display:block;font-size:11px;color:var(--sf-muted);margin-top:4px}.sf-toolbar,.sf-footer{padding:18px 24px;display:flex;align-items:center;justify-content:space-between;gap:12px}.sf-toolbar{border-bottom:1px solid var(--sf-border)}.sf-toolbar>div:last-child{display:flex;gap:8px}.sf-count{font-weight:600}.sf-workspace{display:grid;grid-template-columns:100px minmax(0,1fr);min-height:400px}.sf-pages{padding:20px 12px;background:#f7f9f5;border-inline-end:1px solid var(--sf-border);display:flex;flex-direction:column;gap:12px}.sf-scanner .sf-thumbnail{display:block;width:72px;height:94px;padding:5px;position:relative;overflow:hidden}.sf-thumbnail img{width:100%;height:100%;object-fit:contain}.sf-thumbnail span{position:absolute;bottom:4px;inset-inline-start:4px;background:#fff;border:1px solid var(--sf-border);font-size:10px;line-height:16px;padding:0 4px;border-radius:3px}.sf-scanner .sf-thumbnail.sf-current{border:2px solid var(--sf-accent);box-shadow:0 0 0 3px #22644c12}.sf-editor{padding:18px 24px;min-width:0}.sf-editor-heading,.sf-section-heading{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:20px}.sf-editor-heading>span{font-size:12px;color:var(--sf-muted)}.sf-tabs{display:flex;gap:4px;background:#f3f5f0;padding:4px;border-radius:8px}.sf-scanner .sf-tabs button{border:0;background:transparent;min-height:32px;padding:5px 12px;font-size:12px}.sf-scanner .sf-tabs button[aria-pressed=true]{background:#fff;box-shadow:0 1px 4px #00000010;color:var(--sf-accent)}.sf-crop{padding:12px 0}.sf-image-wrap{position:relative;width:min(100%,400px);margin:8px auto 28px;background:#eef1eb}.sf-image-wrap>img{width:100%;height:100%;display:block;object-fit:contain}.sf-outline{position:absolute;inset:0;width:100%;height:100%;pointer-events:none;overflow:visible}.sf-outline polygon{fill:#21664912;stroke:#27875f;stroke-width:2;vector-effect:non-scaling-stroke}.sf-scanner .sf-handle{position:absolute;transform:translate(-50%,-50%);width:44px;height:44px;min-height:44px;border:0;background:transparent;padding:0;touch-action:none;border-radius:50%}.sf-handle:after{content:"";display:block;width:15px;height:15px;border:3px solid #fff;border-radius:50%;background:var(--sf-accent);box-shadow:0 1px 5px #0007;margin:auto}.sf-scanner .sf-handle:hover:not(:disabled),.sf-handle.sf-selected{background:#22644c15}.sf-corner-tools{display:flex;align-items:center;justify-content:center;flex-wrap:wrap;gap:16px}.sf-magnifier{width:96px;height:96px;border:2px solid var(--sf-border);border-radius:12px;background-repeat:no-repeat;flex-shrink:0;display:grid;place-items:center;background-color:#eef1eb}.sf-magnifier span{font:28px monospace;color:#cf3e32;text-shadow:0 0 2px #fff}.sf-label{display:flex;align-items:center;gap:8px;font-size:12px}.sf-nudges{display:flex;gap:4px;margin-top:6px}.sf-scanner .sf-nudges button{min-width:40px;padding:6px}.sf-crop-actions{display:flex;gap:6px;flex-direction:column}.sf-scanner .sf-hint{font-size:12px;color:var(--sf-muted);margin-top:12px}.sf-page-actions{display:flex;flex-wrap:wrap;gap:6px;align-items:center;margin-top:20px;border-top:1px solid var(--sf-border);padding-top:16px}.sf-page-actions label{display:flex;gap:6px;align-items:center;font-size:12px}.sf-page-actions button,.sf-page-actions select{font-size:12px}.sf-scanner .sf-danger{color:#a43d32}.sf-footer{border-top:1px solid var(--sf-border)}.sf-footer label{display:flex;gap:8px;align-items:center}.sf-export-actions{display:flex;align-items:center;gap:16px}.sf-export-actions>span{font-size:12px;color:var(--sf-muted)}.sf-error{color:#8e2b21;background:#fff2ee;border-bottom:1px solid #f3d5cc;padding:14px 24px}.sf-progress{padding:12px 24px;background:var(--sf-accent-soft)}.sf-progress>div{display:flex;align-items:center;justify-content:space-between}.sf-progress progress{width:100%;height:6px;accent-color:var(--sf-accent)}.sf-camera{padding:24px;text-align:center}.sf-camera video{display:block;width:100%;max-height:55vh;background:#16271f;border-radius:10px}.sf-camera p{margin:12px 0}.sf-processed{min-height:250px;display:grid;place-items:center;background:#f3f5f0;padding:16px}.sf-processed img{max-height:520px;max-width:100%;object-fit:contain}.sf-result-banner{display:flex;align-items:flex-start;gap:14px;padding:24px;background:#f3f8f0}.sf-result-banner p{margin-top:6px;font-size:13px}.sf-result-icon{width:32px;height:32px;display:grid;place-items:center;background:var(--sf-accent);color:#fff;border-radius:50%;flex-shrink:0}.sf-warning{background:#fff6e8}.sf-warning .sf-result-icon{background:#a66a17}.sf-review-layout{display:grid;grid-template-columns:minmax(0,1fr) 250px}.sf-final-stage{padding:20px;min-width:0;background:#f6f8f3}.sf-zoom-viewport{overflow:auto;max-height:520px;border:1px solid var(--sf-border);background:#e5e8e1}.sf-zoom-viewport img{display:block}.sf-report{padding:24px;border-inline-start:1px solid var(--sf-border)}.sf-size-number{font-size:32px;font-weight:650;letter-spacing:-1px}.sf-report>p{font-size:12px}.sf-budget-track{height:5px;background:#edf0e8;margin:16px 0 24px;border-radius:3px;overflow:hidden}.sf-budget-track span{height:100%;background:var(--sf-accent);display:block}.sf-report ol{list-style:none;padding:0;margin:10px 0}.sf-report li{display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid #edf0e8;font-size:12px}.sf-scanner .sf-report li button{border:0;background:transparent;padding-inline:0;font-size:12px}.sf-report dl{font-size:12px;margin-top:24px}.sf-report dt{color:var(--sf-muted)}.sf-report dd{margin:2px 0 12px}.sf-dialog{width:min(980px,calc(100vw - 24px));max-height:calc(100dvh - 32px);padding:0;border:0;border-radius:16px;box-shadow:0 20px 100px #0003}.sf-dialog::backdrop{background:#15261eb0}.sf-dialog .sf-scanner{border:0}@media(max-width:650px){.sf-header{padding:18px}.sf-header h2{font-size:18px}.sf-workspace{grid-template-columns:1fr}.sf-pages{flex-direction:row;overflow:auto;border-inline-end:0;border-bottom:1px solid var(--sf-border);padding:12px}.sf-scanner .sf-thumbnail{width:52px;height:68px}.sf-editor{padding:16px}.sf-image-wrap{width:min(90%,350px)}.sf-crop-actions{flex-direction:row}.sf-magnifier{width:96px;height:96px}.sf-footer{flex-wrap:wrap;padding:16px}.sf-export-actions{width:100%;justify-content:space-between}.sf-toolbar{padding:12px}.sf-toolbar button{font-size:12px;padding:6px 8px}.sf-review-layout{grid-template-columns:1fr}.sf-report{border-inline-start:0;border-top:1px solid var(--sf-border)}.sf-empty{padding:40px 16px}.sf-empty h3{font-size:22px}.sf-result-banner{padding:18px}.sf-report ol{display:grid;grid-template-columns:1fr 1fr;gap:0 20px}.sf-page-actions{gap:8px}.sf-section-heading{flex-wrap:wrap}.sf-dialog{width:calc(100vw - 16px);max-height:calc(100dvh - 16px)}}@media(prefers-reduced-motion:reduce){.sf-scanner *{scroll-behavior:auto!important}}
@@ -0,0 +1,59 @@
1
+ import { jsxs as x, Fragment as E, jsx as o } from "react/jsx-runtime";
2
+ import { useState as c, useRef as s, useEffect as g } from "react";
3
+ function v({
4
+ children: p = "Scan documents",
5
+ loadingLabel: h = "Loading scanner…",
6
+ ...C
7
+ }) {
8
+ const [l, u] = c(!1), [a, i] = c(!1), [f, d] = c(""), [r, y] = c(null), t = s(null), m = s(null), n = s(!0);
9
+ g(() => (n.current = !0, () => {
10
+ n.current = !1;
11
+ }), []), g(() => {
12
+ l && r && t.current?.showModal();
13
+ }, [l, r]);
14
+ const S = () => {
15
+ t.current?.close(), u(!1), m.current?.focus();
16
+ };
17
+ async function b() {
18
+ i(!0), d("");
19
+ try {
20
+ const e = await import("./react.js");
21
+ n.current && (y(() => e.DocumentScanner), u(!0));
22
+ } catch (e) {
23
+ n.current && d(
24
+ e instanceof Error ? e.message : "Could not load the scanner."
25
+ );
26
+ } finally {
27
+ n.current && i(!1);
28
+ }
29
+ }
30
+ return /* @__PURE__ */ x(E, { children: [
31
+ /* @__PURE__ */ o(
32
+ "button",
33
+ {
34
+ type: "button",
35
+ ref: m,
36
+ disabled: a,
37
+ onClick: () => {
38
+ b();
39
+ },
40
+ children: a ? h : p
41
+ }
42
+ ),
43
+ f ? /* @__PURE__ */ o("p", { role: "alert", children: f }) : null,
44
+ l && r ? /* @__PURE__ */ o(
45
+ "dialog",
46
+ {
47
+ ref: t,
48
+ className: "sf-dialog",
49
+ onCancel: (e) => {
50
+ e.preventDefault(), t.current?.querySelector(".sf-icon-button")?.click();
51
+ },
52
+ children: /* @__PURE__ */ o(r, { ...C, onClose: S })
53
+ }
54
+ ) : null
55
+ ] });
56
+ }
57
+ export {
58
+ v as ScannerTrigger
59
+ };
@@ -0,0 +1,12 @@
1
+ import type { BridgeAction } from "./protocol";
2
+ /** Native canvas decode/encode only. Pixel transforms never run in this bridge. */
3
+ export declare function canvasBridge(action: BridgeAction): Promise<{
4
+ buffer: ArrayBuffer;
5
+ width: number;
6
+ height: number;
7
+ } | {
8
+ width: number;
9
+ height: number;
10
+ blob: Blob;
11
+ buffer?: undefined;
12
+ }>;
@@ -0,0 +1,8 @@
1
+ import { type PageEdits, type Quad } from "./types";
2
+ export declare function validateQuad(q: Quad): void;
3
+ export declare function fitDimensions(width: number, height: number, longEdge?: number, maxPixels?: number): {
4
+ width: number;
5
+ height: number;
6
+ };
7
+ /** Pure pixel transform; invoked only by the processing worker. */
8
+ export declare function warpPixels(source: ImageData, edits: PageEdits): ImageData;
@@ -0,0 +1,10 @@
1
+ import { type SafetyLimits } from "./types";
2
+ export interface ImageHeader {
3
+ width: number;
4
+ height: number;
5
+ orientation: number;
6
+ mime: string;
7
+ }
8
+ /** Bounded header inspection before any image decoder is called. */
9
+ export declare function inspectImageBytes(bytes: Uint8Array): ImageHeader;
10
+ export declare function inspectImage(blob: Blob, limits: SafetyLimits): Promise<ImageHeader>;
@@ -0,0 +1,20 @@
1
+ import { type ExportOptions, type ExportResult, type PageEdits, type ScanPage, type ScanSnapshot, type SessionOptions } from "./types";
2
+ export * from "./types";
3
+ export { inspectImageBytes } from "./headers";
4
+ export { validateQuad } from "./geometry";
5
+ export interface ScanSession {
6
+ getSnapshot(): ScanSnapshot;
7
+ subscribe(listener: () => void): () => void;
8
+ addFiles(files: Iterable<Blob>, options?: {
9
+ signal?: AbortSignal;
10
+ replacePageId?: string;
11
+ }): Promise<ScanPage[]>;
12
+ updatePage(id: string, edits: Partial<PageEdits>): void;
13
+ movePage(id: string, index: number): void;
14
+ removePage(id: string): void;
15
+ renderPage(id: string, signal?: AbortSignal): Promise<Blob>;
16
+ exportPdf(options: ExportOptions): Promise<ExportResult>;
17
+ cancel(): void;
18
+ dispose(): void;
19
+ }
20
+ export declare function createScanSession(options?: SessionOptions): ScanSession;
@@ -0,0 +1,2 @@
1
+ /** Remove EXIF/XMP APP1 segments from a freshly encoded JPEG. */
2
+ export declare function stripJpegApp1(blob: Blob): Promise<Blob>;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,34 @@
1
+ import type { ImageHeader } from "./headers";
2
+ import type { SessionOptions, StoredPage, ExportOptions } from "./types";
3
+ export type WorkerTask = {
4
+ kind: "analyze";
5
+ blob: Blob;
6
+ header: ImageHeader;
7
+ options: Pick<SessionOptions, "detector" | "detectorModule" | "detectorOptions">;
8
+ } | {
9
+ kind: "render";
10
+ page: StoredPage;
11
+ } | {
12
+ kind: "export";
13
+ pages: StoredPage[];
14
+ options: Omit<ExportOptions, "signal">;
15
+ };
16
+ export type BridgeAction = {
17
+ kind: "decode";
18
+ blob: Blob;
19
+ width: number;
20
+ height: number;
21
+ region?: {
22
+ x: number;
23
+ y: number;
24
+ width: number;
25
+ height: number;
26
+ };
27
+ } | {
28
+ kind: "encode";
29
+ buffer: ArrayBuffer;
30
+ width: number;
31
+ height: number;
32
+ quality: number;
33
+ longEdge: number;
34
+ };
@@ -0,0 +1,132 @@
1
+ export interface Point {
2
+ x: number;
3
+ y: number;
4
+ }
5
+ /** Clockwise TL, TR, BR, BL, normalized in the oriented source image. */
6
+ export type Quad = [Point, Point, Point, Point];
7
+ export type Filter = "natural" | "grayscale" | "contrast";
8
+ export type Warning = "manual-crop" | "detection-unavailable" | "possibly-blurry" | "possibly-dark" | "low-resolution";
9
+ export interface PageEdits {
10
+ corners: Quad;
11
+ rotation: 0 | 90 | 180 | 270;
12
+ filter: Filter;
13
+ }
14
+ export interface ScanPage {
15
+ id: string;
16
+ width: number;
17
+ height: number;
18
+ sourceBytes: number;
19
+ preview: Blob;
20
+ thumbnail: Blob;
21
+ edits: PageEdits;
22
+ warnings: Warning[];
23
+ /** Detector evidence for observability and honest automatic-crop fallbacks. */
24
+ detection?: DetectionDiagnostics;
25
+ }
26
+ export interface StoredPage extends ScanPage {
27
+ source: Blob;
28
+ }
29
+ export type DetectionFallbackReason = "invalid-image" | "uniform-image" | "no-candidate" | "low-confidence";
30
+ export interface DetectionDiagnostics {
31
+ engine: string;
32
+ confidence: number;
33
+ candidateCount: number;
34
+ edgeThreshold: number;
35
+ edgeDensity: number;
36
+ durationMs: number;
37
+ coverage: number;
38
+ edgeSupport: number;
39
+ contrast: number;
40
+ rectangularity: number;
41
+ source?: "edges" | "hough-lines" | "bright-region" | "dark-region" | "border-contrast" | string;
42
+ fallbackReason?: DetectionFallbackReason;
43
+ }
44
+ export interface DetectionResult {
45
+ corners: Quad | null;
46
+ confidence?: number;
47
+ /** Optional evidence for debugging, benchmarks, and honest fallbacks. */
48
+ diagnostics?: DetectionDiagnostics;
49
+ }
50
+ export interface DocumentDetector {
51
+ detect(image: ImageData): Promise<DetectionResult>;
52
+ }
53
+ export interface DetectorOptions {
54
+ /** Minimum heuristic evidence confidence required to accept automatic corners. */
55
+ minConfidence?: number;
56
+ /** Maximum connected regions evaluated per evidence map. */
57
+ maxComponents?: number;
58
+ /** Maximum unique quadrilateral candidates scored. */
59
+ maxCandidates?: number;
60
+ }
61
+ export interface SafetyLimits {
62
+ maxPages: number;
63
+ maxFileBytes: number;
64
+ maxPixels: number;
65
+ maxSessionBytes: number;
66
+ }
67
+ export interface SessionOptions {
68
+ limits?: Partial<SafetyLimits>;
69
+ detector?: "auto" | "none";
70
+ detectorOptions?: DetectorOptions;
71
+ /** Optional same-origin ES module exporting createDetector(): DocumentDetector. */
72
+ detectorModule?: string;
73
+ /** For hosts with a custom worker asset pipeline. Must be same-origin. */
74
+ workerUrl?: string;
75
+ }
76
+ export type PageSize = "a4" | "letter" | "image";
77
+ export interface ExportOptions {
78
+ maxBytes: number;
79
+ pageSize?: PageSize;
80
+ minQuality?: number;
81
+ minLongEdge?: number;
82
+ signal?: AbortSignal;
83
+ }
84
+ export interface PageReport {
85
+ id: string;
86
+ imageBytes: number;
87
+ width: number;
88
+ height: number;
89
+ quality: number;
90
+ attempts: number;
91
+ warnings: Warning[];
92
+ preview: Blob;
93
+ }
94
+ export interface ExportReport {
95
+ bytes: number;
96
+ maxBytes: number;
97
+ pageSize: PageSize;
98
+ pages: PageReport[];
99
+ limits: {
100
+ minQuality: number;
101
+ minLongEdge: number;
102
+ maxEncodesPerPage: 12;
103
+ };
104
+ }
105
+ export type ExportResult = {
106
+ status: "ready";
107
+ file: File;
108
+ report: ExportReport;
109
+ } | {
110
+ status: "cannot-fit";
111
+ candidateBytes: number;
112
+ report: ExportReport;
113
+ } | {
114
+ status: "cancelled";
115
+ };
116
+ export interface ScanSnapshot {
117
+ pages: readonly ScanPage[];
118
+ status: "idle" | "importing" | "exporting";
119
+ progress: number;
120
+ result: ExportResult | null;
121
+ error: ScanError | null;
122
+ }
123
+ export type ErrorCode = "INVALID_INPUT" | "UNSUPPORTED_FORMAT" | "LIMIT_EXCEEDED" | "INVALID_CORNERS" | "PROCESSING_FAILED" | "UNSUPPORTED_BROWSER" | "DISPOSED" | "CAMERA_UNAVAILABLE";
124
+ export declare class ScanError extends Error {
125
+ readonly code: ErrorCode;
126
+ constructor(code: ErrorCode, message: string, options?: ErrorOptions);
127
+ }
128
+ export declare const DEFAULT_LIMITS: SafetyLimits;
129
+ export declare const FULL_QUAD: Quad;
130
+ export declare const copyQuad: (q: Quad) => Quad;
131
+ export declare function abortIfNeeded(signal?: AbortSignal): void;
132
+ export declare function asScanError(error: unknown): ScanError;
@@ -0,0 +1,13 @@
1
+ import type { WorkerTask } from "./protocol";
2
+ export declare class WorkerClient {
3
+ private url?;
4
+ private worker;
5
+ private sequence;
6
+ private generation;
7
+ private tail;
8
+ private pending;
9
+ constructor(url?: string | undefined);
10
+ private ensure;
11
+ run<T>(task: WorkerTask, signal?: AbortSignal, progress?: (p: number) => void): Promise<T>;
12
+ reset(error?: unknown): void;
13
+ }
@@ -0,0 +1,11 @@
1
+ import { type ClassicalDetectorOptions } from "./scanfit-classical";
2
+ import type { DocumentDetector } from "../core/types";
3
+ export type { DetectionDiagnostics, DetectionResult, DocumentDetector, } from "../core/types";
4
+ export type { ClassicalDetectorOptions } from "./scanfit-classical";
5
+ /**
6
+ * Scanfit's independent, worker-safe classical detector.
7
+ *
8
+ * The implementation uses standard image-processing techniques over typed
9
+ * arrays and contains no Scanic source or derived code.
10
+ */
11
+ export declare function createDetector(options?: ClassicalDetectorOptions): DocumentDetector;
@@ -0,0 +1,4 @@
1
+ import type { DetectionResult, DetectorOptions } from "../core/types";
2
+ export type ClassicalDetectorOptions = DetectorOptions;
3
+ /** Independent detector entry point, exported separately for deterministic tests. */
4
+ export declare function detectDocument(image: ImageData, options?: ClassicalDetectorOptions): DetectionResult;
@@ -0,0 +1,25 @@
1
+ import { type ExportOptions, type ExportReport, type PageSize, type Warning } from "../core/types";
2
+ export interface EncodedPage {
3
+ id: string;
4
+ jpeg: Blob;
5
+ width: number;
6
+ height: number;
7
+ quality: number;
8
+ warnings: Warning[];
9
+ }
10
+ export interface PdfPageSource {
11
+ id: string;
12
+ warnings: Warning[];
13
+ }
14
+ export interface PdfEncoder {
15
+ encode(index: number, longEdge: number, quality: number): Promise<EncodedPage>;
16
+ }
17
+ export interface PdfFitResult {
18
+ status: "ready" | "cannot-fit";
19
+ blob: Blob;
20
+ report: ExportReport;
21
+ }
22
+ export declare function validateExportOptions(options: ExportOptions): void;
23
+ export declare function buildPdf(pages: readonly EncodedPage[], pageSize?: PageSize): Promise<Blob>;
24
+ /** Bounded, measured search. A cannot-fit result is not a mathematical impossibility proof. */
25
+ export declare function fitPdf(sources: readonly PdfPageSource[], encoder: PdfEncoder, options: ExportOptions, progress?: (value: number) => void): Promise<PdfFitResult>;
@@ -0,0 +1,6 @@
1
+ import type { ScannerMessages } from "./messages";
2
+ export declare function Camera({ onCapture, onClose, messages: m, }: {
3
+ onCapture: (file: Blob) => void;
4
+ onClose: () => void;
5
+ messages: ScannerMessages;
6
+ }): import("react").JSX.Element;
@@ -0,0 +1,8 @@
1
+ import { type Quad, type ScanPage } from "../core";
2
+ import type { ScannerMessages } from "./messages";
3
+ export declare function CornerEditor({ page, onApply, messages: m, disabled, }: {
4
+ page: ScanPage;
5
+ onApply: (q: Quad) => void;
6
+ messages: ScannerMessages;
7
+ disabled: boolean;
8
+ }): import("react").JSX.Element;
@@ -0,0 +1,28 @@
1
+ import { type ReactNode } from "react";
2
+ import { type ScanSession, type SessionOptions, type ScanPage, type ExportReport } from "../core";
3
+ import { type ScannerMessages } from "./messages";
4
+ import "./styles.css";
5
+ export interface DocumentScannerProps {
6
+ maxBytes: number;
7
+ onComplete: (result: {
8
+ file: File;
9
+ report: ExportReport;
10
+ }) => void;
11
+ onClose?: () => void;
12
+ onError?: (error: Error) => void;
13
+ session?: ScanSession;
14
+ options?: SessionOptions;
15
+ qualityLimits?: {
16
+ minQuality?: number;
17
+ minLongEdge?: number;
18
+ };
19
+ messages?: Partial<ScannerMessages>;
20
+ dir?: "ltr" | "rtl";
21
+ className?: string;
22
+ renderHeader?: (context: {
23
+ pageCount: number;
24
+ maxBytes: number;
25
+ }) => ReactNode;
26
+ renderPageSummary?: (page: ScanPage, index: number) => ReactNode;
27
+ }
28
+ export declare function DocumentScanner(props: DocumentScannerProps): import("react").JSX.Element;
@@ -0,0 +1,10 @@
1
+ import { type ScanSession, type SessionOptions } from "../core";
2
+ export declare function useScanSession(options?: SessionOptions, external?: ScanSession): {
3
+ pages: readonly import("../core").ScanPage[];
4
+ status: "idle" | "importing" | "exporting";
5
+ progress: number;
6
+ result: import("../core").ExportResult | null;
7
+ error: import("../core").ScanError | null;
8
+ session: ScanSession | null;
9
+ };
10
+ export declare function useObjectUrl(blob: Blob | null | undefined): string;
@@ -0,0 +1,5 @@
1
+ export { DocumentScanner } from "./DocumentScanner";
2
+ export type { DocumentScannerProps } from "./DocumentScanner";
3
+ export { useScanSession } from "./hooks";
4
+ export { defaultMessages, formatBytes } from "./messages";
5
+ export type { ScannerMessages } from "./messages";
@@ -0,0 +1,77 @@
1
+ export declare const defaultMessages: {
2
+ title: string;
3
+ subtitle: string;
4
+ addPhotos: string;
5
+ camera: string;
6
+ close: string;
7
+ cancel: string;
8
+ emptyTitle: string;
9
+ emptyDescription: string;
10
+ choosePhotos: string;
11
+ formats: string;
12
+ privacy: string;
13
+ page: string;
14
+ pages: string;
15
+ edit: string;
16
+ preview: string;
17
+ rotate: string;
18
+ retake: string;
19
+ remove: string;
20
+ moveUp: string;
21
+ moveDown: string;
22
+ natural: string;
23
+ grayscale: string;
24
+ contrast: string;
25
+ filter: string;
26
+ corner: string;
27
+ topLeft: string;
28
+ topRight: string;
29
+ bottomRight: string;
30
+ bottomLeft: string;
31
+ nudgeUp: string;
32
+ nudgeDown: string;
33
+ nudgeLeft: string;
34
+ nudgeRight: string;
35
+ resetCrop: string;
36
+ cropHelp: string;
37
+ prepare: string;
38
+ preparing: string;
39
+ importing: string;
40
+ pageSize: string;
41
+ a4: string;
42
+ letter: string;
43
+ image: string;
44
+ limit: string;
45
+ actualSize: string;
46
+ imageBytes: string;
47
+ quality: string;
48
+ dimensions: string;
49
+ qualityFloor: string;
50
+ resolutionFloor: string;
51
+ invalidLimit: string;
52
+ reviewTitle: string;
53
+ reviewDescription: string;
54
+ cannotFitTitle: string;
55
+ cannotFitDescription: string;
56
+ confirm: string;
57
+ back: string;
58
+ zoom: string;
59
+ qualityNote: string;
60
+ noRecovery: string;
61
+ discard: string;
62
+ removeConfirm: string;
63
+ cameraTitle: string;
64
+ shutter: string;
65
+ cameraStarting: string;
66
+ cameraHelp: string;
67
+ cameraError: string;
68
+ correctedPreview: string;
69
+ sourcePreview: string;
70
+ manualCrop: string;
71
+ detectorUnavailable: string;
72
+ blurry: string;
73
+ dark: string;
74
+ lowResolution: string;
75
+ };
76
+ export type ScannerMessages = typeof defaultMessages;
77
+ export declare const formatBytes: (bytes: number) => string;
@@ -0,0 +1,7 @@
1
+ import { type ReactNode } from "react";
2
+ import type { DocumentScannerProps } from "../react/DocumentScanner";
3
+ /** Import this entry to keep the workflow and worker out of the initial JS graph. */
4
+ export declare function ScannerTrigger({ children, loadingLabel, ...props }: Omit<DocumentScannerProps, "onClose"> & {
5
+ children?: ReactNode;
6
+ loadingLabel?: string;
7
+ }): import("react").JSX.Element;
@@ -0,0 +1 @@
1
+ export { ScannerTrigger } from "./ScannerTrigger";
package/package.json ADDED
@@ -0,0 +1,72 @@
1
+ {
2
+ "name": "@scanfit/browser",
3
+ "version": "0.1.0-alpha.0",
4
+ "description": "Local, size-constrained scan-to-PDF workflows for the browser",
5
+ "keywords": [
6
+ "document-scanner",
7
+ "pdf",
8
+ "react",
9
+ "scan-to-pdf",
10
+ "typescript",
11
+ "web-worker"
12
+ ],
13
+ "homepage": "https://scanfit-two.vercel.app",
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/Ahmedsultan09/scanfit.git",
17
+ "directory": "packages/scanfit"
18
+ },
19
+ "bugs": {
20
+ "url": "https://github.com/Ahmedsultan09/scanfit/issues"
21
+ },
22
+ "license": "MIT",
23
+ "type": "module",
24
+ "engines": {
25
+ "node": ">=20"
26
+ },
27
+ "sideEffects": [
28
+ "**/*.css"
29
+ ],
30
+ "files": [
31
+ "dist",
32
+ "LICENSE",
33
+ "THIRD_PARTY_NOTICES.md"
34
+ ],
35
+ "exports": {
36
+ "./core": {
37
+ "types": "./dist/types/core/index.d.ts",
38
+ "import": "./dist/core.js"
39
+ },
40
+ "./react": {
41
+ "types": "./dist/types/react/index.d.ts",
42
+ "import": "./dist/react.js"
43
+ },
44
+ "./trigger": {
45
+ "types": "./dist/types/trigger/index.d.ts",
46
+ "import": "./dist/trigger.js"
47
+ },
48
+ "./detector": {
49
+ "types": "./dist/types/detector/index.d.ts",
50
+ "import": "./dist/detector.js"
51
+ },
52
+ "./pdf": {
53
+ "types": "./dist/types/pdf/index.d.ts",
54
+ "import": "./dist/pdf.js"
55
+ },
56
+ "./styles.css": "./dist/styles.css"
57
+ },
58
+ "peerDependencies": {
59
+ "react": "^18.2.0 || ^19.0.0"
60
+ },
61
+ "peerDependenciesMeta": {
62
+ "react": {
63
+ "optional": true
64
+ }
65
+ },
66
+ "dependencies": {
67
+ "tinypdf": "0.4.1"
68
+ },
69
+ "publishConfig": {
70
+ "access": "public"
71
+ }
72
+ }