ppu-doc-correction 1.0.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.
package/README.md ADDED
@@ -0,0 +1,208 @@
1
+ # ppu-doc-correction
2
+
3
+ A lightweight, type-safe document image correction library for Node.js, Bun, and browsers. Provides three **independent**, lazy-loading correction services powered by PaddlePaddle ONNX models:
4
+
5
+ 1. **`DocOrientService`** — document orientation classification (0/90/180/270 degrees)
6
+ 2. **`TextUnwarpService`** — geometric document unwarping (UVDoc model)
7
+ 3. **`TextOrientService`** — text line orientation classification (0/180 degrees)
8
+
9
+ Each service is fully self-contained: it handles model downloading, caching, ONNX session management, and cleanup independently. Models are lazily loaded on first use -- only the model you need is downloaded.
10
+
11
+ ## Installation
12
+
13
+ ```bash
14
+ npm install ppu-doc-correction
15
+ yarn add ppu-doc-correction
16
+ bun add ppu-doc-correction
17
+ ```
18
+
19
+ ## Usage
20
+
21
+ ### Document Orientation
22
+
23
+ ```ts
24
+ import { DocOrientService } from "ppu-doc-correction";
25
+
26
+ const service = new DocOrientService({
27
+ debugging: { verbose: true },
28
+ });
29
+
30
+ const result = await service.run(imageBuffer);
31
+ console.log(result.orientation); // 0 | 90 | 180 | 270
32
+ console.log(result.scores); // [0.82, 0.06, 0.03, 0.09]
33
+
34
+ await service.destroy();
35
+ ```
36
+
37
+ ### Text Image Unwarping
38
+
39
+ ```ts
40
+ import { TextUnwarpService } from "ppu-doc-correction";
41
+
42
+ const service = new TextUnwarpService();
43
+
44
+ const result = await service.run(warpedImageBuffer);
45
+ // result.correctedImage is the unwarped ArrayBuffer
46
+
47
+ await service.destroy();
48
+ ```
49
+
50
+ ### Text Line Orientation
51
+
52
+ ```ts
53
+ import { TextOrientService } from "ppu-doc-correction";
54
+
55
+ const service = new TextOrientService();
56
+
57
+ const result = await service.run(textLineBuffer);
58
+ console.log(result.orientation); // 0 | 180
59
+
60
+ await service.destroy();
61
+ ```
62
+
63
+ ### Custom Model Path
64
+
65
+ ```ts
66
+ const service = new DocOrientService({
67
+ model: "./models/PP-LCNet_x1_0_doc_ori.onnx",
68
+ });
69
+ ```
70
+
71
+ You can also pass an `ArrayBuffer` directly:
72
+
73
+ ```ts
74
+ const modelBuffer = await Bun.file("./models/PP-LCNet_x1_0_doc_ori.onnx").arrayBuffer();
75
+ const service = new DocOrientService({ model: modelBuffer });
76
+ ```
77
+
78
+ ### Debug Mode
79
+
80
+ When `debug: true` is set, corrected images are saved to the `debugFolder` (default: `out/`):
81
+
82
+ ```ts
83
+ const service = new DocOrientService({
84
+ debugging: { debug: true, verbose: true, debugFolder: "out" },
85
+ });
86
+ ```
87
+
88
+ ### Clearing Model Cache
89
+
90
+ ```ts
91
+ const service = new DocOrientService();
92
+ service.clearModelCache();
93
+ ```
94
+
95
+ Or via CLI:
96
+
97
+ ```bash
98
+ bun clear-cache
99
+ ```
100
+
101
+ ## Web / Browser Support
102
+
103
+ Import from `ppu-doc-correction/web`:
104
+
105
+ ```ts
106
+ import { DocOrientService } from "ppu-doc-correction/web";
107
+
108
+ const service = new DocOrientService();
109
+ const result = await service.run(canvas);
110
+ console.log(result.orientation);
111
+ await service.destroy();
112
+ ```
113
+
114
+ See the interactive demo: [Web Demo](https://pt-perkasa-pilar-utama.github.io/ppu-doc-correction/)
115
+
116
+ ## Models
117
+
118
+ | Model | Input Shape | Output Shape | Purpose |
119
+ | :--- | :--- | :--- | :--- |
120
+ | PP-LCNet_x1_0_doc_ori | [1, 3, 224, 224] | [1, 4] | Document orientation (4 classes) |
121
+ | UVDoc | [1, 3, H, W] | [1, 3, H, W] | Text image unwarping |
122
+ | PP-LCNet_x0_25_textline_ori | [1, 3, 80, 160] | [1, 2] | Text line orientation (2 classes) |
123
+
124
+ Models are sourced from [ppu-paddle-ocr-models](https://github.com/PT-Perkasa-Pilar-Utama/ppu-paddle-ocr-models).
125
+
126
+ ### Alternative Text Orientation Model
127
+
128
+ A larger, more accurate text orientation model:
129
+
130
+ ```ts
131
+ import { TextOrientService, MODEL_BASE_URL } from "ppu-doc-correction";
132
+
133
+ const service = new TextOrientService({
134
+ model: `${MODEL_BASE_URL}/correction/PP-LCNet_x1_0_textline_ori.onnx`,
135
+ });
136
+ ```
137
+
138
+ ## Configuration
139
+
140
+ ### Per-Service Options
141
+
142
+ Each service accepts its own options object:
143
+
144
+ | Property | Type | Description |
145
+ | :--- | :--- | :--- |
146
+ | `model` | `string \| ArrayBuffer` | Model path, URL, or buffer. Omit for default. |
147
+ | `debugging` | `DebuggingOptions` | Logging and image dump behavior |
148
+ | `session` | `SessionOptions` | ONNX Runtime session configuration |
149
+ | `docOrient` / `textUnwarp` / `textOrient` | preprocessing options | Service-specific preprocessing |
150
+
151
+ ### `DebuggingOptions`
152
+
153
+ | Property | Type | Default | Description |
154
+ | :--- | :---: | :---: | :--- |
155
+ | `verbose` | `boolean` | `false` | Enable detailed console logs |
156
+ | `debug` | `boolean` | `false` | Save corrected images to disk |
157
+ | `debugFolder` | `string` | `"out"` | Output directory for debug images |
158
+
159
+ ### `SessionOptions`
160
+
161
+ | Property | Type | Default | Description |
162
+ | :--- | :---: | :---: | :--- |
163
+ | `executionProviders` | `string[]` | `['cpu']` | ONNX execution providers |
164
+ | `graphOptimizationLevel` | `string` | `'all'` | Graph optimization level |
165
+ | `enableCpuMemArena` | `boolean` | `true` | CPU memory arena |
166
+ | `enableMemPattern` | `boolean` | `true` | Memory pattern optimization |
167
+
168
+ ## Benchmark
169
+
170
+ ```bash
171
+ bun task bench index
172
+ ```
173
+
174
+ ## Contributing
175
+
176
+ 1. **Fork** the repository
177
+ 2. **Create** a feature branch
178
+ 3. **Implement** changes and add tests
179
+ 4. **Submit** a pull request
180
+
181
+ ### Running Tests
182
+
183
+ ```bash
184
+ bun test
185
+ bun build:test
186
+ bun lint
187
+ ```
188
+
189
+ ## License
190
+
191
+ MIT — see [LICENSE](LICENSE).
192
+
193
+ ## Scripts
194
+
195
+ ### [Build](./scripts/build.ts)
196
+
197
+ Emit `.js` and `.d.ts` files to [`lib`](./lib).
198
+
199
+ ### [Publish](./scripts/publish.ts)
200
+
201
+ Move [`package.json`](./package.json), [`README.md`](./README.md) to [`lib`](./lib) and publish.
202
+
203
+ ### [Bench](./scripts/bench.ts)
204
+
205
+ ```bash
206
+ bun task bench index # Run bench/index.bench.ts
207
+ bun task bench --node # Run in Node.js
208
+ ```
package/constants.d.ts ADDED
@@ -0,0 +1,15 @@
1
+ import type { DebuggingOptions, DocOrientOptions, SessionOptions, TextOrientOptions, TextUnwarpOptions } from "./interface.js";
2
+ /** Base URL for downloading default correction model files from GitHub. */
3
+ export declare const MODEL_BASE_URL = "https://media.githubusercontent.com/media/PT-Perkasa-Pilar-Utama/ppu-paddle-ocr-models/main";
4
+ /** Default model file URLs for each correction model. */
5
+ export declare const MODEL_URLS: Record<string, string>;
6
+ /** Default debugging options -- logging and image dumps disabled. */
7
+ export declare const DEFAULT_DEBUGGING_OPTIONS: DebuggingOptions;
8
+ /** Default ONNX Runtime session options. */
9
+ export declare const DEFAULT_SESSION_OPTIONS: SessionOptions;
10
+ /** Default document orientation classification options. */
11
+ export declare const DEFAULT_DOC_ORIENT_OPTIONS: DocOrientOptions;
12
+ /** Default text image unwarping options. */
13
+ export declare const DEFAULT_TEXT_UNWARP_OPTIONS: TextUnwarpOptions;
14
+ /** Default text line orientation classification options. */
15
+ export declare const DEFAULT_TEXT_ORIENT_OPTIONS: TextOrientOptions;
package/constants.js ADDED
@@ -0,0 +1 @@
1
+ export let MODEL_BASE_URL="https://media.githubusercontent.com/media/PT-Perkasa-Pilar-Utama/ppu-paddle-ocr-models/main";export let MODEL_URLS={docOrient:`${MODEL_BASE_URL}/correction/PP-LCNet_x1_0_doc_ori.onnx`,textUnwarp:`${MODEL_BASE_URL}/correction/UVDoc.onnx`,textOrient:`${MODEL_BASE_URL}/correction/PP-LCNet_x0_25_textline_ori.onnx`};export let DEFAULT_DEBUGGING_OPTIONS={verbose:false,debug:false,debugFolder:"out"};export let DEFAULT_SESSION_OPTIONS={executionProviders:["cpu"],graphOptimizationLevel:"all",enableCpuMemArena:true,enableMemPattern:true,executionMode:"sequential",interOpNumThreads:0,intraOpNumThreads:0};export let DEFAULT_DOC_ORIENT_OPTIONS={mean:[0.485,0.456,0.406],stdDeviation:[0.229,0.224,0.225]};export let DEFAULT_TEXT_UNWARP_OPTIONS={preserveDimensions:true};export let DEFAULT_TEXT_ORIENT_OPTIONS={mean:[0.485,0.456,0.406],stdDeviation:[0.229,0.224,0.225]};
@@ -0,0 +1,45 @@
1
+ import type { InferenceSession } from "onnxruntime-common";
2
+ import type { DebuggingOptions, DocOrientOptions, DocOrientResult } from "../interface.js";
3
+ import type { CoreCanvas, PlatformProvider } from "./platform.js";
4
+ /**
5
+ * Platform-agnostic service for classifying document image orientation.
6
+ *
7
+ * Determines whether a document image is rotated 0, 90, 180, or 270 degrees
8
+ * and produces a corrected image.
9
+ */
10
+ export declare class BaseDocOrientService {
11
+ protected readonly options: DocOrientOptions;
12
+ protected readonly debugging: DebuggingOptions;
13
+ protected readonly session: InferenceSession;
14
+ protected readonly platform: PlatformProvider;
15
+ constructor(platform: PlatformProvider, session: InferenceSession, options?: Partial<DocOrientOptions>, debugging?: Partial<DebuggingOptions>);
16
+ protected log(message: string): void;
17
+ /**
18
+ * Classify the orientation of a document image and return the corrected result.
19
+ * @param image - Source image as ArrayBuffer or platform-specific Canvas.
20
+ */
21
+ run(image: ArrayBuffer | CoreCanvas): Promise<DocOrientResult>;
22
+ /**
23
+ * Preprocess the image into a normalized CHW float tensor.
24
+ *
25
+ * Uses letterbox resize: scales the image so the longer side fits 224px,
26
+ * preserves aspect ratio, and centers on a zero-padded 224x224 canvas.
27
+ */
28
+ private preprocess;
29
+ /**
30
+ * Run ONNX inference and return softmax scores.
31
+ */
32
+ private runInference;
33
+ /**
34
+ * Compute softmax over raw logits.
35
+ */
36
+ private softmax;
37
+ /**
38
+ * Determine orientation from softmax scores.
39
+ */
40
+ private getOrientation;
41
+ /**
42
+ * Rotate the canvas to correct orientation.
43
+ */
44
+ private applyRotation;
45
+ }
@@ -0,0 +1 @@
1
+ export class BaseDocOrientService{options;debugging;session;platform;constructor(platform,session,options={},debugging={}){this.platform=platform;this.session=session;this.options={...DEFAULT_DOC_ORIENT_OPTIONS,...options};this.debugging={...DEFAULT_DEBUGGING_OPTIONS,...debugging}}log(message){if(this.debugging.verbose){console.log(`[DocOrientService] ${message}`)}}async run(image){this.log("Starting document orientation classification");let canvas=this.platform.isCanvas(image)?image:await CanvasProcessor.prepareCanvas(image);let tensor=this.preprocess(canvas);let scores=await this.runInference(tensor);let orientation=this.getOrientation(scores);this.log(`Detected orientation: ${orientation} degrees`);let correctedCanvas=this.applyRotation(canvas,orientation);let correctedImage=await CanvasProcessor.prepareBuffer(correctedCanvas);if(this.debugging.debug&&this.debugging.debugFolder){await this.platform.saveDebugImage(correctedCanvas,"doc-orient-corrected",this.debugging.debugFolder);this.log(`Debug image saved to: ${this.debugging.debugFolder}/doc-orient-corrected`)}return{orientation,scores,correctedImage}}preprocess(canvas){const{width:srcW,height:srcH}=canvas;let scale=Math.min(INPUT_WIDTH/srcW,INPUT_HEIGHT/srcH);let resizedW=Math.round(srcW*scale);let resizedH=Math.round(srcH*scale);let offsetX=Math.floor((INPUT_WIDTH-resizedW)/2);let offsetY=Math.floor((INPUT_HEIGHT-resizedH)/2);let resized=new CanvasProcessor(canvas).resize({width:resizedW,height:resizedH}).toCanvas();let resizedCtx=resized.getContext("2d");let imageData=resizedCtx.getImageData(0,0,resizedW,resizedH);let rgbaData=imageData.data;let tensor=new Float32Array(NUM_CHANNELS*INPUT_HEIGHT*INPUT_WIDTH);const{mean,stdDeviation}=this.options;for(let h=0;h<resizedH;h++){for(let w=0;w<resizedW;w++){let rgbaIdx=(h*resizedW+w)*4;let tensorBaseIdx=(h+offsetY)*INPUT_WIDTH+(w+offsetX);for(let c=0;c<NUM_CHANNELS;c++){let pixelValue=rgbaData[rgbaIdx+c]/255;let normalizedValue=(pixelValue-mean[c])/stdDeviation[c];tensor[c*INPUT_HEIGHT*INPUT_WIDTH+tensorBaseIdx]=normalizedValue}}}this.log(`Preprocessed: ${srcW}x${srcH} -> ${resizedW}x${resizedH} (centered on ${INPUT_WIDTH}x${INPUT_HEIGHT})`);return tensor}async runInference(tensor){let inputTensor;try{this.log("Running inference...");inputTensor=new this.platform.ort.Tensor("float32",tensor,[1,NUM_CHANNELS,INPUT_HEIGHT,INPUT_WIDTH]);let feeds={x:inputTensor};let results=await this.session.run(feeds);let outputName=this.session.outputNames[0]||"fetch_name_0";let outputTensor=results[outputName];if(!outputTensor){throw new Error(`Output tensor '${outputName}' not found in results`)}let rawScores=outputTensor.data;let scores=this.softmax(rawScores);this.log(`Scores: [${scores.map((s)=>s.toFixed(4)).join(", ")}]`);return scores}finally{inputTensor?.dispose()}}softmax(logits){let maxLogit=Math.max(...logits);let exps=Array.from(logits,(v)=>Math.exp(v-maxLogit));let sumExp=exps.reduce((a,b)=>a+b,0);return exps.map((e)=>e/sumExp)}getOrientation(scores){let maxIdx=0;let maxScore=scores[0];for(let i=1;i<scores.length;i++){if(scores[i]>maxScore){maxScore=scores[i];maxIdx=i}}return ORIENTATIONS[maxIdx]}applyRotation(canvas,orientation){if(orientation===0){return canvas}const{width,height}=canvas;let swapDimensions=orientation===90||orientation===270;let outWidth=swapDimensions?height:width;let outHeight=swapDimensions?width:height;let rotated=this.platform.createCanvas(outWidth,outHeight);let ctx=rotated.getContext("2d");ctx.translate(outWidth/2,outHeight/2);ctx.rotate(-orientation*Math.PI/180);ctx.drawImage(canvas,-width/2,-height/2);return rotated}}import{CanvasProcessor}from"ppu-ocv/canvas";import{DEFAULT_DEBUGGING_OPTIONS,DEFAULT_DOC_ORIENT_OPTIONS}from"../constants.js";let INPUT_WIDTH=224;let INPUT_HEIGHT=224;let NUM_CHANNELS=3;let ORIENTATIONS=[0,90,180,270];
@@ -0,0 +1,49 @@
1
+ import type { InferenceSession } from "onnxruntime-common";
2
+ import type { DebuggingOptions, TextOrientOptions, TextOrientResult } from "../interface.js";
3
+ import type { CoreCanvas, PlatformProvider } from "./platform.js";
4
+ /**
5
+ * Platform-agnostic service for classifying text line orientation.
6
+ *
7
+ * Determines whether a text line image is upright (0 degrees) or
8
+ * upside-down (180 degrees) and produces a corrected image.
9
+ */
10
+ export declare class BaseTextOrientService {
11
+ protected readonly options: TextOrientOptions;
12
+ protected readonly debugging: DebuggingOptions;
13
+ protected readonly session: InferenceSession;
14
+ protected readonly platform: PlatformProvider;
15
+ constructor(platform: PlatformProvider, session: InferenceSession, options?: Partial<TextOrientOptions>, debugging?: Partial<DebuggingOptions>);
16
+ protected log(message: string): void;
17
+ /**
18
+ * Classify the orientation of a text line image and return the corrected result.
19
+ * @param image - Source image as ArrayBuffer or platform-specific Canvas.
20
+ */
21
+ run(image: ArrayBuffer | CoreCanvas): Promise<TextOrientResult>;
22
+ /**
23
+ * Preprocess the image into a normalized CHW float tensor.
24
+ *
25
+ * For landscape images (width >= height): scales height to INPUT_HEIGHT,
26
+ * preserves aspect ratio, and zero-pads the remaining width.
27
+ *
28
+ * For portrait images (height > width): scales width to INPUT_WIDTH,
29
+ * preserves aspect ratio, and center-crops the height to INPUT_HEIGHT.
30
+ * This avoids squishing a tall image into a tiny strip.
31
+ */
32
+ private preprocess;
33
+ /**
34
+ * Run ONNX inference and return softmax scores.
35
+ */
36
+ private runInference;
37
+ /**
38
+ * Compute softmax over raw logits.
39
+ */
40
+ private softmax;
41
+ /**
42
+ * Determine orientation from softmax scores.
43
+ */
44
+ private getOrientation;
45
+ /**
46
+ * Rotate the canvas 180 degrees if needed.
47
+ */
48
+ private applyRotation;
49
+ }
@@ -0,0 +1 @@
1
+ export class BaseTextOrientService{options;debugging;session;platform;constructor(platform,session,options={},debugging={}){this.platform=platform;this.session=session;this.options={...DEFAULT_TEXT_ORIENT_OPTIONS,...options};this.debugging={...DEFAULT_DEBUGGING_OPTIONS,...debugging}}log(message){if(this.debugging.verbose){console.log(`[TextOrientService] ${message}`)}}async run(image){this.log("Starting text line orientation classification");let canvas=this.platform.isCanvas(image)?image:await CanvasProcessor.prepareCanvas(image);let tensor=this.preprocess(canvas);let scores=await this.runInference(tensor);let orientation=this.getOrientation(scores);this.log(`Detected text orientation: ${orientation} degrees`);let correctedCanvas=this.applyRotation(canvas,orientation);let correctedImage=await CanvasProcessor.prepareBuffer(correctedCanvas);if(this.debugging.debug&&this.debugging.debugFolder){await this.platform.saveDebugImage(correctedCanvas,"text-orient-corrected",this.debugging.debugFolder);this.log(`Debug image saved to: ${this.debugging.debugFolder}/text-orient-corrected`)}return{orientation,scores,correctedImage}}preprocess(canvas){const{width:srcW,height:srcH}=canvas;let resizedW;let resizedH;let cropOffsetY=0;if(srcW>=srcH){let ratio=srcW/srcH;resizedW=Math.min(Math.ceil(INPUT_HEIGHT*ratio),INPUT_WIDTH);resizedH=INPUT_HEIGHT}else{let ratio=srcH/srcW;resizedW=INPUT_WIDTH;resizedH=Math.ceil(INPUT_WIDTH*ratio);cropOffsetY=Math.max(0,Math.floor((resizedH-INPUT_HEIGHT)/2))}let resized=new CanvasProcessor(canvas).resize({width:resizedW,height:resizedH}).toCanvas();let ctx=resized.getContext("2d");let readH=Math.min(resizedH-cropOffsetY,INPUT_HEIGHT);let imageData=ctx.getImageData(0,cropOffsetY,resizedW,readH);let rgbaData=imageData.data;let tensor=new Float32Array(NUM_CHANNELS*INPUT_HEIGHT*INPUT_WIDTH);const{mean,stdDeviation}=this.options;let fillW=Math.min(resizedW,INPUT_WIDTH);for(let h=0;h<readH;h++){for(let w=0;w<fillW;w++){let rgbaIdx=(h*resizedW+w)*4;let tensorBaseIdx=h*INPUT_WIDTH+w;for(let c=0;c<NUM_CHANNELS;c++){let pixelValue=rgbaData[rgbaIdx+c]/255;let normalizedValue=(pixelValue-mean[c])/stdDeviation[c];tensor[c*INPUT_HEIGHT*INPUT_WIDTH+tensorBaseIdx]=normalizedValue}}}this.log(`Preprocessed: ${srcW}x${srcH} -> ${resizedW}x${resizedH}`+(cropOffsetY>0?` (center-cropped to ${INPUT_WIDTH}x${INPUT_HEIGHT})`:` (padded to ${INPUT_WIDTH}x${INPUT_HEIGHT})`));return tensor}async runInference(tensor){let inputTensor;try{this.log("Running inference...");inputTensor=new this.platform.ort.Tensor("float32",tensor,[1,NUM_CHANNELS,INPUT_HEIGHT,INPUT_WIDTH]);let feeds={x:inputTensor};let results=await this.session.run(feeds);let outputName=this.session.outputNames[0]||"fetch_name_0";let outputTensor=results[outputName];if(!outputTensor){throw new Error(`Output tensor '${outputName}' not found in results`)}let rawScores=outputTensor.data;let scores=this.softmax(rawScores);this.log(`Scores: [${scores.map((s)=>s.toFixed(4)).join(", ")}]`);return scores}finally{inputTensor?.dispose()}}softmax(logits){let maxLogit=Math.max(...logits);let exps=Array.from(logits,(v)=>Math.exp(v-maxLogit));let sumExp=exps.reduce((a,b)=>a+b,0);return exps.map((e)=>e/sumExp)}getOrientation(scores){return scores[0]>=scores[1]?ORIENTATIONS[0]:ORIENTATIONS[1]}applyRotation(canvas,orientation){if(orientation===0){return canvas}const{width,height}=canvas;let rotated=this.platform.createCanvas(width,height);let ctx=rotated.getContext("2d");ctx.translate(width/2,height/2);ctx.rotate(Math.PI);ctx.drawImage(canvas,-width/2,-height/2);return rotated}}import{CanvasProcessor}from"ppu-ocv/canvas";import{DEFAULT_DEBUGGING_OPTIONS,DEFAULT_TEXT_ORIENT_OPTIONS}from"../constants.js";let INPUT_WIDTH=160;let INPUT_HEIGHT=80;let NUM_CHANNELS=3;let ORIENTATIONS=[0,180];
@@ -0,0 +1,34 @@
1
+ import type { InferenceSession } from "onnxruntime-common";
2
+ import type { DebuggingOptions, TextUnwarpOptions, TextUnwarpResult } from "../interface.js";
3
+ import type { CoreCanvas, PlatformProvider } from "./platform.js";
4
+ /**
5
+ * Platform-agnostic service for removing warping and distortion from document images.
6
+ *
7
+ * Uses the UVDoc model to perform geometric correction, transforming
8
+ * warped document photos into flat, readable images.
9
+ */
10
+ export declare class BaseTextUnwarpService {
11
+ protected readonly options: TextUnwarpOptions;
12
+ protected readonly debugging: DebuggingOptions;
13
+ protected readonly session: InferenceSession;
14
+ protected readonly platform: PlatformProvider;
15
+ constructor(platform: PlatformProvider, session: InferenceSession, options?: Partial<TextUnwarpOptions>, debugging?: Partial<DebuggingOptions>);
16
+ protected log(message: string): void;
17
+ /**
18
+ * Unwarp a distorted document image.
19
+ * @param image - Source image as ArrayBuffer or platform-specific Canvas.
20
+ */
21
+ run(image: ArrayBuffer | CoreCanvas): Promise<TextUnwarpResult>;
22
+ /**
23
+ * Normalize image to [0,1] float32 and convert HWC to CHW layout.
24
+ */
25
+ private preprocess;
26
+ /**
27
+ * Run UVDoc ONNX inference.
28
+ */
29
+ private runInference;
30
+ /**
31
+ * Convert CHW output tensor back to a canvas image.
32
+ */
33
+ private postprocess;
34
+ }
@@ -0,0 +1 @@
1
+ export class BaseTextUnwarpService{options;debugging;session;platform;constructor(platform,session,options={},debugging={}){this.platform=platform;this.session=session;this.options={...DEFAULT_TEXT_UNWARP_OPTIONS,...options};this.debugging={...DEFAULT_DEBUGGING_OPTIONS,...debugging}}log(message){if(this.debugging.verbose){console.log(`[TextUnwarpService] ${message}`)}}async run(image){this.log("Starting text image unwarping");let canvas=this.platform.isCanvas(image)?image:await CanvasProcessor.prepareCanvas(image);let originalWidth=canvas.width;let originalHeight=canvas.height;let tensor=this.preprocess(canvas);let outputData=await this.runInference(tensor,canvas.width,canvas.height);let correctedCanvas=this.postprocess(outputData.data,outputData.width,outputData.height);let finalCanvas=correctedCanvas;if(this.options.preserveDimensions&&(correctedCanvas.width!==originalWidth||correctedCanvas.height!==originalHeight)){finalCanvas=new CanvasProcessor(correctedCanvas).resize({width:originalWidth,height:originalHeight}).toCanvas()}let correctedImage=await CanvasProcessor.prepareBuffer(finalCanvas);if(this.debugging.debug&&this.debugging.debugFolder){await this.platform.saveDebugImage(finalCanvas,"text-unwarp-corrected",this.debugging.debugFolder);this.log(`Debug image saved to: ${this.debugging.debugFolder}/text-unwarp-corrected`)}this.log(`Unwarping complete: ${originalWidth}x${originalHeight} -> ${finalCanvas.width}x${finalCanvas.height}`);return{correctedImage}}preprocess(canvas){const{width,height}=canvas;let ctx=canvas.getContext("2d");let imageData=ctx.getImageData(0,0,width,height);let rgbaData=imageData.data;let tensor=new Float32Array(NUM_CHANNELS*height*width);for(let h=0;h<height;h++){for(let w=0;w<width;w++){let rgbaIdx=(h*width+w)*4;let tensorBaseIdx=h*width+w;for(let c=0;c<NUM_CHANNELS;c++){tensor[c*height*width+tensorBaseIdx]=rgbaData[rgbaIdx+c]/255}}}this.log(`Preprocessed: ${width}x${height}, tensor size: ${tensor.length}`);return tensor}async runInference(tensor,width,height){let inputTensor;try{this.log(`Running inference with input shape: [1, ${NUM_CHANNELS}, ${height}, ${width}]`);inputTensor=new this.platform.ort.Tensor("float32",tensor,[1,NUM_CHANNELS,height,width]);let feeds={image:inputTensor};let results=await this.session.run(feeds);let outputName=this.session.outputNames[0]||"fetch_name_0";let outputTensor=results[outputName];if(!outputTensor){throw new Error(`Output tensor '${outputName}' not found in results`)}let dims=outputTensor.dims;let outHeight=dims[2];let outWidth=dims[3];this.log(`Inference complete, output shape: [${dims.join(", ")}]`);return{data:outputTensor.data,width:outWidth,height:outHeight}}finally{inputTensor?.dispose()}}postprocess(data,width,height){let canvas=this.platform.createCanvas(width,height);let ctx=canvas.getContext("2d");let imageData=ctx.createImageData(width,height);let pixels=imageData.data;for(let h=0;h<height;h++){for(let w=0;w<width;w++){let pixelIdx=(h*width+w)*4;let tensorBaseIdx=h*width+w;for(let c=0;c<NUM_CHANNELS;c++){let value=data[c*height*width+tensorBaseIdx];pixels[pixelIdx+c]=Math.round(Math.min(1,Math.max(0,value))*255)}pixels[pixelIdx+3]=255}}ctx.putImageData(imageData,0,0);return canvas}}import{CanvasProcessor}from"ppu-ocv/canvas";import{DEFAULT_DEBUGGING_OPTIONS,DEFAULT_TEXT_UNWARP_OPTIONS}from"../constants.js";let NUM_CHANNELS=3;
@@ -0,0 +1,27 @@
1
+ import type { InferenceSession, Tensor } from "onnxruntime-common";
2
+ import type { Canvas } from "ppu-ocv";
3
+ import type { CanvasLike } from "ppu-ocv/web";
4
+ export type CoreCanvas = Canvas | CanvasLike;
5
+ /**
6
+ * A generic abstraction mapping specifically to pure runtime-level APIs
7
+ * (like ort/onnxruntime, canvas APIs, fetching mechanisms, etc).
8
+ *
9
+ * This injects the platform-specific dependencies into the shared Core logic.
10
+ */
11
+ export interface PlatformProvider<TCanvas = CoreCanvas> {
12
+ /** The specific pathing delimiter used on this platform (ie '/' vs '\') */
13
+ pathSeparator: string;
14
+ /** Platform-specific ONNX Runtime namespace (`onnxruntime-node` vs `onnxruntime-web`) */
15
+ ort: {
16
+ Tensor: typeof Tensor;
17
+ InferenceSession: typeof InferenceSession;
18
+ };
19
+ /** Platform-specific canvas constructor (`createCanvas` vs `getPlatform().createCanvas`) */
20
+ createCanvas: (width: number, height: number) => TCanvas;
21
+ /** Type guard determining if an object is a recognized Canvas API implementation */
22
+ isCanvas: (image: unknown) => image is TCanvas;
23
+ /** Resolves resources asynchronously via local FileSystem (`fs`) or HTTP (`fetch`) based on the environment */
24
+ loadResource: (source: string | ArrayBuffer | undefined, defaultUrl: string) => Promise<ArrayBuffer>;
25
+ /** Optionally dump a given Canvas representation directly onto the disk (No-Op on Web context) */
26
+ saveDebugImage: (canvas: TCanvas, filename: string, path: string) => Promise<void>;
27
+ }
package/index.d.ts ADDED
@@ -0,0 +1,25 @@
1
+ /**
2
+ * @module
3
+ *
4
+ * Blazing-fast document correction for Node.js and Bun.
5
+ *
6
+ * Three independent, lazy-loading services for document image correction:
7
+ * - `DocOrientService` — document orientation classification (0/90/180/270)
8
+ * - `TextUnwarpService` — geometric document unwarping (UVDoc)
9
+ * - `TextOrientService` — text line orientation classification (0/180)
10
+ *
11
+ * @example
12
+ * ```ts
13
+ * import { DocOrientService } from "ppu-doc-correction";
14
+ *
15
+ * const service = new DocOrientService();
16
+ * const result = await service.run(imageBuffer);
17
+ * console.log(result.orientation); // 0, 90, 180, 270
18
+ * await service.destroy();
19
+ * ```
20
+ */
21
+ export { DocOrientService } from "./processor/doc-orient.service.js";
22
+ export { TextOrientService } from "./processor/text-orient.service.js";
23
+ export { TextUnwarpService } from "./processor/text-unwarp.service.js";
24
+ export type { DebuggingOptions, DocOrientation, DocOrientOptions, DocOrientResult, DocOrientServiceOptions, SessionOptions, TextOrientation, TextOrientOptions, TextOrientResult, TextOrientServiceOptions, TextUnwarpOptions, TextUnwarpResult, TextUnwarpServiceOptions, } from "./interface.js";
25
+ export { DEFAULT_DEBUGGING_OPTIONS, DEFAULT_DOC_ORIENT_OPTIONS, DEFAULT_SESSION_OPTIONS, DEFAULT_TEXT_ORIENT_OPTIONS, DEFAULT_TEXT_UNWARP_OPTIONS, MODEL_BASE_URL, MODEL_URLS, } from "./constants.js";
package/index.js ADDED
@@ -0,0 +1 @@
1
+ export{DocOrientService}from"./processor/doc-orient.service.js";export{TextOrientService}from"./processor/text-orient.service.js";export{TextUnwarpService}from"./processor/text-unwarp.service.js";export{DEFAULT_DEBUGGING_OPTIONS,DEFAULT_DOC_ORIENT_OPTIONS,DEFAULT_SESSION_OPTIONS,DEFAULT_TEXT_ORIENT_OPTIONS,DEFAULT_TEXT_UNWARP_OPTIONS,MODEL_BASE_URL,MODEL_URLS}from"./constants.js";
package/interface.d.ts ADDED
@@ -0,0 +1,178 @@
1
+ /**
2
+ * ONNX Runtime session configuration options.
3
+ */
4
+ export interface SessionOptions {
5
+ /**
6
+ * Execution providers to use for inference (e.g., 'cpu', 'cuda').
7
+ * @default ['cpu']
8
+ */
9
+ executionProviders?: string[];
10
+ /**
11
+ * Graph optimization level for ONNX Runtime.
12
+ * @default 'all'
13
+ */
14
+ graphOptimizationLevel?: "disabled" | "basic" | "extended" | "layout" | "all";
15
+ /**
16
+ * Enable CPU memory arena for better memory management.
17
+ * @default true
18
+ */
19
+ enableCpuMemArena?: boolean;
20
+ /**
21
+ * Enable memory pattern optimization.
22
+ * @default true
23
+ */
24
+ enableMemPattern?: boolean;
25
+ /**
26
+ * Execution mode for the session.
27
+ * @default 'sequential'
28
+ */
29
+ executionMode?: "sequential" | "parallel";
30
+ /**
31
+ * Number of inter-op threads. 0 lets ONNX decide.
32
+ * @default 0
33
+ */
34
+ interOpNumThreads?: number;
35
+ /**
36
+ * Number of intra-op threads. 0 lets ONNX decide.
37
+ * @default 0
38
+ */
39
+ intraOpNumThreads?: number;
40
+ }
41
+ /**
42
+ * Controls verbose output and image dumps for debugging.
43
+ */
44
+ export interface DebuggingOptions {
45
+ /**
46
+ * Enable detailed logging of each processing step.
47
+ * @default false
48
+ */
49
+ verbose?: boolean;
50
+ /**
51
+ * Save intermediate image data to disk for inspection.
52
+ * @default false
53
+ */
54
+ debug?: boolean;
55
+ /**
56
+ * Directory where debug images will be written.
57
+ * Relative to the current working directory.
58
+ * @default "out"
59
+ */
60
+ debugFolder?: string;
61
+ }
62
+ /**
63
+ * Document image orientation (0, 90, 180, or 270 degrees).
64
+ */
65
+ export type DocOrientation = 0 | 90 | 180 | 270;
66
+ /**
67
+ * Text line orientation (0 or 180 degrees).
68
+ */
69
+ export type TextOrientation = 0 | 180;
70
+ /**
71
+ * Preprocessing options for document orientation classification.
72
+ */
73
+ export interface DocOrientOptions {
74
+ /**
75
+ * Per-channel mean values for input normalization [R, G, B].
76
+ * @default [0.485, 0.456, 0.406]
77
+ */
78
+ mean?: [number, number, number];
79
+ /**
80
+ * Per-channel standard deviation values for input normalization [R, G, B].
81
+ * @default [0.229, 0.224, 0.225]
82
+ */
83
+ stdDeviation?: [number, number, number];
84
+ }
85
+ /**
86
+ * Preprocessing options for text image unwarping.
87
+ */
88
+ export interface TextUnwarpOptions {
89
+ /**
90
+ * Whether to preserve the original image dimensions in the output.
91
+ * When true, the output is resized to match the input dimensions.
92
+ * @default true
93
+ */
94
+ preserveDimensions?: boolean;
95
+ }
96
+ /**
97
+ * Preprocessing options for text line orientation classification.
98
+ */
99
+ export interface TextOrientOptions {
100
+ /**
101
+ * Per-channel mean values for input normalization [R, G, B].
102
+ * @default [0.485, 0.456, 0.406]
103
+ */
104
+ mean?: [number, number, number];
105
+ /**
106
+ * Per-channel standard deviation values for input normalization [R, G, B].
107
+ * @default [0.229, 0.224, 0.225]
108
+ */
109
+ stdDeviation?: [number, number, number];
110
+ }
111
+ /**
112
+ * Result of document orientation classification.
113
+ */
114
+ export interface DocOrientResult {
115
+ /** Detected orientation angle in degrees. */
116
+ orientation: DocOrientation;
117
+ /** Confidence scores for each orientation class [0, 90, 180, 270]. */
118
+ scores: [number, number, number, number];
119
+ /** Corrected image as an ArrayBuffer (PNG encoded). */
120
+ correctedImage: ArrayBuffer;
121
+ }
122
+ /**
123
+ * Result of text image unwarping.
124
+ */
125
+ export interface TextUnwarpResult {
126
+ /** Unwarped image as an ArrayBuffer (PNG encoded). */
127
+ correctedImage: ArrayBuffer;
128
+ }
129
+ /**
130
+ * Result of text line orientation classification.
131
+ */
132
+ export interface TextOrientResult {
133
+ /** Detected orientation angle in degrees. */
134
+ orientation: TextOrientation;
135
+ /** Confidence scores for each orientation class [0, 180]. */
136
+ scores: [number, number];
137
+ /** Corrected image as an ArrayBuffer (PNG encoded). */
138
+ correctedImage: ArrayBuffer;
139
+ }
140
+ /**
141
+ * Options for the DocOrientService.
142
+ */
143
+ export interface DocOrientServiceOptions {
144
+ /** ONNX model source: file path, URL, or ArrayBuffer. Omit for default. */
145
+ model?: string | ArrayBuffer;
146
+ /** Preprocessing options. */
147
+ docOrient?: DocOrientOptions;
148
+ /** Debugging options. */
149
+ debugging?: DebuggingOptions;
150
+ /** ONNX Runtime session options. */
151
+ session?: SessionOptions;
152
+ }
153
+ /**
154
+ * Options for the TextUnwarpService.
155
+ */
156
+ export interface TextUnwarpServiceOptions {
157
+ /** ONNX model source: file path, URL, or ArrayBuffer. Omit for default. */
158
+ model?: string | ArrayBuffer;
159
+ /** Preprocessing options. */
160
+ textUnwarp?: TextUnwarpOptions;
161
+ /** Debugging options. */
162
+ debugging?: DebuggingOptions;
163
+ /** ONNX Runtime session options. */
164
+ session?: SessionOptions;
165
+ }
166
+ /**
167
+ * Options for the TextOrientService.
168
+ */
169
+ export interface TextOrientServiceOptions {
170
+ /** ONNX model source: file path, URL, or ArrayBuffer. Omit for default. */
171
+ model?: string | ArrayBuffer;
172
+ /** Preprocessing options. */
173
+ textOrient?: TextOrientOptions;
174
+ /** Debugging options. */
175
+ debugging?: DebuggingOptions;
176
+ /** ONNX Runtime session options. */
177
+ session?: SessionOptions;
178
+ }
package/package.json ADDED
@@ -0,0 +1,77 @@
1
+ {
2
+ "name": "ppu-doc-correction",
3
+ "version": "1.0.0",
4
+ "description": "Lightweight, type-safe document image correction for Node.js, Bun, and browsers. Provides orientation classification, text unwarping (UVDoc), and text line orientation correction.",
5
+ "keywords": [
6
+ "bun-compatible",
7
+ "computer-vision",
8
+ "document-ai",
9
+ "document-correction",
10
+ "document-processing",
11
+ "image-processing",
12
+ "image-unwarping",
13
+ "onnx",
14
+ "onnxruntime",
15
+ "orientation-detection",
16
+ "paddle-paddle",
17
+ "paddlepaddle",
18
+ "uvdoc"
19
+ ],
20
+ "author": "snowfluke",
21
+ "license": "MIT",
22
+ "type": "module",
23
+ "main": "./index.js",
24
+ "types": "./index.d.ts",
25
+ "exports": {
26
+ ".": {
27
+ "types": "./index.d.ts",
28
+ "default": "./index.js"
29
+ },
30
+ "./web": {
31
+ "types": "./web/index.d.ts",
32
+ "default": "./web/index.js"
33
+ }
34
+ },
35
+ "scripts": {
36
+ "task": "bun scripts/task.ts",
37
+ "build:test": "bun task build && bun test",
38
+ "build:publish": "bun task build && bun task report-size && bun task publish",
39
+ "clear-cache": "bun scripts/clear-cache.ts",
40
+ "lint": "prettier --check ./src",
41
+ "lint:fix": "prettier --write ./src",
42
+ "demo": "bunx -y serve -l 3456 ."
43
+ },
44
+ "devDependencies": {
45
+ "@stylistic/eslint-plugin": "latest",
46
+ "@types/bun": "latest",
47
+ "@types/uglify-js": "latest",
48
+ "canvas": "^3.2.1",
49
+ "mitata": "latest",
50
+ "onnxruntime-node": "^1.24.2",
51
+ "onnxruntime-web": "^1.24.2",
52
+ "prettier": "^3.8.1",
53
+ "tsx": "latest",
54
+ "typescript": "latest",
55
+ "typescript-eslint": "latest",
56
+ "uglify-js": ">=2.4.24"
57
+ },
58
+ "repository": {
59
+ "type": "git",
60
+ "url": "https://github.com/PT-Perkasa-Pilar-Utama/ppu-doc-correction.git"
61
+ },
62
+ "dependencies": {
63
+ "ppu-ocv": "^3.1.0"
64
+ },
65
+ "peerDependencies": {
66
+ "onnxruntime-node": "^1.24.2",
67
+ "onnxruntime-web": "^1.24.2"
68
+ },
69
+ "peerDependenciesMeta": {
70
+ "onnxruntime-node": {
71
+ "optional": true
72
+ },
73
+ "onnxruntime-web": {
74
+ "optional": true
75
+ }
76
+ }
77
+ }
@@ -0,0 +1,35 @@
1
+ import { BaseDocOrientService } from "../core/base-doc-orient.service.js";
2
+ import type { DocOrientServiceOptions } from "../interface.js";
3
+ /**
4
+ * Self-contained Node.js service for classifying document image orientation.
5
+ *
6
+ * Handles model loading, caching, and lazy initialization internally.
7
+ * The model is loaded on the first call to `run()`.
8
+ *
9
+ * @example
10
+ * ```ts
11
+ * const service = new DocOrientService();
12
+ * const result = await service.run(imageBuffer);
13
+ * console.log(result.orientation); // 0, 90, 180, or 270
14
+ * await service.destroy();
15
+ * ```
16
+ */
17
+ export declare class DocOrientService {
18
+ private readonly options;
19
+ private inner;
20
+ private session;
21
+ constructor(options?: DocOrientServiceOptions);
22
+ private log;
23
+ private ensureSession;
24
+ /**
25
+ * Classify the orientation of a document image.
26
+ * Lazily loads the model on first call.
27
+ */
28
+ run(...args: Parameters<BaseDocOrientService["run"]>): ReturnType<BaseDocOrientService["run"]>;
29
+ /** Check if the model session has been loaded. */
30
+ isInitialized(): boolean;
31
+ /** Release the ONNX session and free resources. */
32
+ destroy(): Promise<void>;
33
+ /** Delete all locally cached ONNX model files. */
34
+ clearModelCache(): void;
35
+ }
@@ -0,0 +1,3 @@
1
+ export class DocOrientService{options;inner=null;session=null;constructor(options={}){this.options=options}log(message){if(this.options.debugging?.verbose){console.log(`[DocOrientService] ${message}`)}}async ensureSession(){if(this.inner)return;let buffer=await loadResource(this.options.model,MODEL_URLS.docOrient,(msg)=>this.log(msg));let sessionOptions=this.options.session||DEFAULT_SESSION_OPTIONS;this.session=await ort.InferenceSession.create(new Uint8Array(buffer),sessionOptions);this.log(`Model loaded
2
+ input: ${this.session.inputNames}
3
+ output: ${this.session.outputNames}`);this.inner=new BaseDocOrientService(new NodePlatformProvider,this.session,this.options.docOrient,this.options.debugging)}async run(...args){await this.ensureSession();return this.inner.run(...args)}isInitialized(){return this.session!==null}async destroy(){await this.session?.release();this.session=null;this.inner=null}clearModelCache(){clearModelCache((msg)=>this.log(msg))}}import*as ort from"onnxruntime-node";import{BaseDocOrientService}from"../core/base-doc-orient.service.js";import{MODEL_URLS,DEFAULT_SESSION_OPTIONS}from"../constants.js";import{NodePlatformProvider}from"./platform.node.js";import{loadResource,clearModelCache}from"./model-loader.js";
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Fetch a resource from a URL and cache it locally.
3
+ */
4
+ export declare function fetchAndCache(url: string, log: (msg: string) => void): Promise<ArrayBuffer>;
5
+ /**
6
+ * Load a resource from a buffer, file path, URL, or default URL.
7
+ */
8
+ export declare function loadResource(source: string | ArrayBuffer | undefined, defaultUrl: string, log: (msg: string) => void): Promise<ArrayBuffer>;
9
+ /**
10
+ * Delete all locally cached ONNX model files.
11
+ */
12
+ export declare function clearModelCache(log: (msg: string) => void): void;
@@ -0,0 +1,3 @@
1
+ import{existsSync,mkdirSync,readFileSync,writeFileSync}from"fs";import*as os from"os";import*as path from"path";let CACHE_DIR=path.join(os.homedir(),".cache","ppu-doc-correction");export async function fetchAndCache(url,log){let fileName=path.basename(new URL(url).pathname);let cachePath=path.join(CACHE_DIR,fileName);if(existsSync(cachePath)){log(`Loading cached resource from: ${cachePath}`);let buf=readFileSync(cachePath);return buf.buffer.slice(buf.byteOffset,buf.byteOffset+buf.byteLength)}console.log(`Downloading resource: ${fileName}
2
+ `+` Cached at: ${CACHE_DIR}`);log(`Fetching resource from URL: ${url}`);let response=await fetch(url);if(!response.ok){throw new Error(`Failed to fetch resource from ${url}`)}if(!response.body){throw new Error("Response body is null or undefined")}let contentLength=response.headers.get("Content-Length");let totalLength=contentLength?parseInt(contentLength,10):0;let receivedLength=0;let chunks=[];let reader=response.body.getReader();while(true){const{done,value}=await reader.read();if(done)break;chunks.push(value);receivedLength+=value.length;if(totalLength>0){let percentage=(receivedLength/totalLength*100).toFixed(2);process.stdout.write(`\rDownloading... ${percentage}%`)}}process.stdout.write(`
3
+ `);let buffer=new Uint8Array(receivedLength);let position=0;for(let chunk of chunks){buffer.set(chunk,position);position+=chunk.length}log(`Caching resource to: ${cachePath}`);if(!existsSync(CACHE_DIR)){mkdirSync(CACHE_DIR,{recursive:true})}writeFileSync(cachePath,Buffer.from(buffer));return buffer.buffer}export async function loadResource(source,defaultUrl,log){if(source instanceof ArrayBuffer){log("Loading resource from ArrayBuffer");return source}if(typeof source==="string"){if(source.startsWith("http")){return fetchAndCache(source,log)}else{let resolvedPath=path.resolve(process.cwd(),source);log(`Loading resource from path: ${resolvedPath}`);let buf=readFileSync(resolvedPath);return buf.buffer.slice(buf.byteOffset,buf.byteOffset+buf.byteLength)}}return fetchAndCache(defaultUrl,log)}export function clearModelCache(log){const{rmSync}=require("fs");if(existsSync(CACHE_DIR)){log(`Clearing model cache at: ${CACHE_DIR}`);rmSync(CACHE_DIR,{recursive:true,force:true});console.log(`Model cache cleared: ${CACHE_DIR}`)}else{log("Cache directory does not exist, nothing to clear.")}}
@@ -0,0 +1,10 @@
1
+ import * as ort from "onnxruntime-node";
2
+ import type { CoreCanvas, PlatformProvider } from "../core/platform.js";
3
+ export declare class NodePlatformProvider implements PlatformProvider<CoreCanvas> {
4
+ readonly pathSeparator: string;
5
+ readonly ort: typeof ort;
6
+ createCanvas(width: number, height: number): CoreCanvas;
7
+ isCanvas(image: unknown): image is CoreCanvas;
8
+ loadResource(source: string | ArrayBuffer | undefined, defaultUrl: string): Promise<ArrayBuffer>;
9
+ saveDebugImage(canvas: CoreCanvas, filename: string, outputDir: string): Promise<void>;
10
+ }
@@ -0,0 +1 @@
1
+ export class NodePlatformProvider{pathSeparator=path.sep;ort=ort;createCanvas(width,height){return new Canvas(width,height)}isCanvas(image){return image instanceof Canvas}async loadResource(source,defaultUrl){if(source instanceof ArrayBuffer){return source}let sourceToLoad=typeof source==="string"?source:defaultUrl;if(sourceToLoad.startsWith("http")){let response=await fetch(sourceToLoad);if(!response.ok){throw new Error(`Failed to fetch resource from ${sourceToLoad}`)}return response.arrayBuffer()}let buffer=await fs.readFile(sourceToLoad);return buffer.buffer.slice(buffer.byteOffset,buffer.byteOffset+buffer.byteLength)}async saveDebugImage(canvas,filename,outputDir){await fs.mkdir(outputDir,{recursive:true});await CanvasToolkit.getInstance().saveImage({canvas,filename,path:outputDir})}}import*as fs from"fs/promises";import*as ort from"onnxruntime-node";import*as path from"path";import{Canvas,CanvasToolkit}from"ppu-ocv";
@@ -0,0 +1,35 @@
1
+ import { BaseTextOrientService } from "../core/base-text-orient.service.js";
2
+ import type { TextOrientServiceOptions } from "../interface.js";
3
+ /**
4
+ * Self-contained Node.js service for classifying text line orientation.
5
+ *
6
+ * Handles model loading, caching, and lazy initialization internally.
7
+ * The model is loaded on the first call to `run()`.
8
+ *
9
+ * @example
10
+ * ```ts
11
+ * const service = new TextOrientService();
12
+ * const result = await service.run(imageBuffer);
13
+ * console.log(result.orientation); // 0 or 180
14
+ * await service.destroy();
15
+ * ```
16
+ */
17
+ export declare class TextOrientService {
18
+ private readonly options;
19
+ private inner;
20
+ private session;
21
+ constructor(options?: TextOrientServiceOptions);
22
+ private log;
23
+ private ensureSession;
24
+ /**
25
+ * Classify the orientation of a text line image.
26
+ * Lazily loads the model on first call.
27
+ */
28
+ run(...args: Parameters<BaseTextOrientService["run"]>): ReturnType<BaseTextOrientService["run"]>;
29
+ /** Check if the model session has been loaded. */
30
+ isInitialized(): boolean;
31
+ /** Release the ONNX session and free resources. */
32
+ destroy(): Promise<void>;
33
+ /** Delete all locally cached ONNX model files. */
34
+ clearModelCache(): void;
35
+ }
@@ -0,0 +1,3 @@
1
+ export class TextOrientService{options;inner=null;session=null;constructor(options={}){this.options=options}log(message){if(this.options.debugging?.verbose){console.log(`[TextOrientService] ${message}`)}}async ensureSession(){if(this.inner)return;let buffer=await loadResource(this.options.model,MODEL_URLS.textOrient,(msg)=>this.log(msg));let sessionOptions=this.options.session||DEFAULT_SESSION_OPTIONS;this.session=await ort.InferenceSession.create(new Uint8Array(buffer),sessionOptions);this.log(`Model loaded
2
+ input: ${this.session.inputNames}
3
+ output: ${this.session.outputNames}`);this.inner=new BaseTextOrientService(new NodePlatformProvider,this.session,this.options.textOrient,this.options.debugging)}async run(...args){await this.ensureSession();return this.inner.run(...args)}isInitialized(){return this.session!==null}async destroy(){await this.session?.release();this.session=null;this.inner=null}clearModelCache(){clearModelCache((msg)=>this.log(msg))}}import*as ort from"onnxruntime-node";import{BaseTextOrientService}from"../core/base-text-orient.service.js";import{MODEL_URLS,DEFAULT_SESSION_OPTIONS}from"../constants.js";import{NodePlatformProvider}from"./platform.node.js";import{loadResource,clearModelCache}from"./model-loader.js";
@@ -0,0 +1,35 @@
1
+ import { BaseTextUnwarpService } from "../core/base-text-unwarp.service.js";
2
+ import type { TextUnwarpServiceOptions } from "../interface.js";
3
+ /**
4
+ * Self-contained Node.js service for unwarping distorted document images.
5
+ *
6
+ * Handles model loading, caching, and lazy initialization internally.
7
+ * The model is loaded on the first call to `run()`.
8
+ *
9
+ * @example
10
+ * ```ts
11
+ * const service = new TextUnwarpService();
12
+ * const result = await service.run(imageBuffer);
13
+ * // result.correctedImage contains the unwarped image
14
+ * await service.destroy();
15
+ * ```
16
+ */
17
+ export declare class TextUnwarpService {
18
+ private readonly options;
19
+ private inner;
20
+ private session;
21
+ constructor(options?: TextUnwarpServiceOptions);
22
+ private log;
23
+ private ensureSession;
24
+ /**
25
+ * Unwarp a distorted document image.
26
+ * Lazily loads the model on first call.
27
+ */
28
+ run(...args: Parameters<BaseTextUnwarpService["run"]>): ReturnType<BaseTextUnwarpService["run"]>;
29
+ /** Check if the model session has been loaded. */
30
+ isInitialized(): boolean;
31
+ /** Release the ONNX session and free resources. */
32
+ destroy(): Promise<void>;
33
+ /** Delete all locally cached ONNX model files. */
34
+ clearModelCache(): void;
35
+ }
@@ -0,0 +1,3 @@
1
+ export class TextUnwarpService{options;inner=null;session=null;constructor(options={}){this.options=options}log(message){if(this.options.debugging?.verbose){console.log(`[TextUnwarpService] ${message}`)}}async ensureSession(){if(this.inner)return;let buffer=await loadResource(this.options.model,MODEL_URLS.textUnwarp,(msg)=>this.log(msg));let sessionOptions=this.options.session||DEFAULT_SESSION_OPTIONS;this.session=await ort.InferenceSession.create(new Uint8Array(buffer),sessionOptions);this.log(`Model loaded
2
+ input: ${this.session.inputNames}
3
+ output: ${this.session.outputNames}`);this.inner=new BaseTextUnwarpService(new NodePlatformProvider,this.session,this.options.textUnwarp,this.options.debugging)}async run(...args){await this.ensureSession();return this.inner.run(...args)}isInitialized(){return this.session!==null}async destroy(){await this.session?.release();this.session=null;this.inner=null}clearModelCache(){clearModelCache((msg)=>this.log(msg))}}import*as ort from"onnxruntime-node";import{BaseTextUnwarpService}from"../core/base-text-unwarp.service.js";import{MODEL_URLS,DEFAULT_SESSION_OPTIONS}from"../constants.js";import{NodePlatformProvider}from"./platform.node.js";import{loadResource,clearModelCache}from"./model-loader.js";
package/utils.d.ts ADDED
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Deep merges multiple objects into the target object.
3
+ * Arrays are overwritten, not concatenated.
4
+ *
5
+ * @param target The target object to merge into.
6
+ * @param sources The source objects to merge from.
7
+ * @returns The merged target object.
8
+ */
9
+ export declare function deepMerge<T extends Record<string, unknown>>(target: T, ...sources: Partial<T>[]): T;
10
+ /**
11
+ * Checks if a value is a plain object.
12
+ *
13
+ * @param item The value to check.
14
+ * @returns True if the value is a plain object, false otherwise.
15
+ */
16
+ export declare function isObject(item: unknown): item is Record<string, unknown>;
package/utils.js ADDED
@@ -0,0 +1 @@
1
+ export function deepMerge(target,...sources){if(!sources.length)return target;let source=sources.shift();if(isObject(target)&&isObject(source)){for(let key in source){if(Object.prototype.hasOwnProperty.call(source,key)){let sourceValue=source[key];let targetValue=target[key];if(isObject(sourceValue)){if(!targetValue||!isObject(targetValue)){target[key]={}}deepMerge(target[key],sourceValue)}else if(sourceValue!==undefined){target[key]=sourceValue}}}}return deepMerge(target,...sources)}export function isObject(item){return item!==null&&typeof item==="object"&&!Array.isArray(item)&&!(item instanceof Date)&&!(item instanceof RegExp)&&!(item instanceof ArrayBuffer)&&!ArrayBuffer.isView(item)}
@@ -0,0 +1,20 @@
1
+ import { BaseDocOrientService } from "../core/base-doc-orient.service.js";
2
+ import type { DocOrientServiceOptions } from "../interface.js";
3
+ /**
4
+ * Self-contained browser service for classifying document image orientation.
5
+ *
6
+ * Handles model fetching and lazy initialization internally.
7
+ * The model is fetched on the first call to `run()`.
8
+ */
9
+ export declare class DocOrientService {
10
+ private readonly options;
11
+ private inner;
12
+ private session;
13
+ constructor(options?: DocOrientServiceOptions);
14
+ private log;
15
+ private ensureSession;
16
+ private loadModel;
17
+ run(...args: Parameters<BaseDocOrientService["run"]>): ReturnType<BaseDocOrientService["run"]>;
18
+ isInitialized(): boolean;
19
+ destroy(): Promise<void>;
20
+ }
@@ -0,0 +1,3 @@
1
+ export class DocOrientService{options;inner=null;session=null;constructor(options={}){this.options=options}log(message){if(this.options.debugging?.verbose){console.log(`[DocOrientService] ${message}`)}}async ensureSession(){if(this.inner)return;let buffer=await this.loadModel();let sessionOptions=this.options.session||DEFAULT_WEB_SESSION;this.session=await ort.InferenceSession.create(new Uint8Array(buffer),sessionOptions);this.log(`Model loaded
2
+ input: ${this.session.inputNames}
3
+ output: ${this.session.outputNames}`);this.inner=new BaseDocOrientService(new WebPlatformProvider,this.session,this.options.docOrient,this.options.debugging)}async loadModel(){if(this.options.model instanceof ArrayBuffer)return this.options.model;let url=typeof this.options.model==="string"?this.options.model:MODEL_URLS.docOrient;this.log(`Fetching model from: ${url}`);let response=await fetch(url);if(!response.ok)throw new Error(`Failed to fetch model from ${url}`);return response.arrayBuffer()}async run(...args){await this.ensureSession();return this.inner.run(...args)}isInitialized(){return this.session!==null}async destroy(){await this.session?.release();this.session=null;this.inner=null}}import*as ort from"onnxruntime-web";import{BaseDocOrientService}from"../core/base-doc-orient.service.js";import{MODEL_URLS}from"../constants.js";import{WebPlatformProvider}from"./platform.web.js";let DEFAULT_WEB_SESSION={executionProviders:["wasm"],graphOptimizationLevel:"all"};
package/web/index.d.ts ADDED
@@ -0,0 +1,25 @@
1
+ /**
2
+ * @module
3
+ *
4
+ * Document correction for browsers and web environments.
5
+ *
6
+ * Three independent, lazy-loading services using `onnxruntime-web` (WASM):
7
+ * - `DocOrientService` — document orientation classification
8
+ * - `TextUnwarpService` — geometric document unwarping
9
+ * - `TextOrientService` — text line orientation classification
10
+ *
11
+ * @example
12
+ * ```ts
13
+ * import { DocOrientService } from "ppu-doc-correction/web";
14
+ *
15
+ * const service = new DocOrientService();
16
+ * const result = await service.run(canvas);
17
+ * console.log(result.orientation);
18
+ * await service.destroy();
19
+ * ```
20
+ */
21
+ export { DocOrientService } from "./doc-orient.service.web.js";
22
+ export { TextOrientService } from "./text-orient.service.web.js";
23
+ export { TextUnwarpService } from "./text-unwarp.service.web.js";
24
+ export type { DebuggingOptions, DocOrientation, DocOrientOptions, DocOrientResult, DocOrientServiceOptions, SessionOptions, TextOrientation, TextOrientOptions, TextOrientResult, TextOrientServiceOptions, TextUnwarpOptions, TextUnwarpResult, TextUnwarpServiceOptions, } from "../interface.js";
25
+ export { DEFAULT_DEBUGGING_OPTIONS, DEFAULT_DOC_ORIENT_OPTIONS, DEFAULT_SESSION_OPTIONS, DEFAULT_TEXT_ORIENT_OPTIONS, DEFAULT_TEXT_UNWARP_OPTIONS, MODEL_URLS, } from "../constants.js";
package/web/index.js ADDED
@@ -0,0 +1 @@
1
+ export{DocOrientService}from"./doc-orient.service.web.js";export{TextOrientService}from"./text-orient.service.web.js";export{TextUnwarpService}from"./text-unwarp.service.web.js";export{DEFAULT_DEBUGGING_OPTIONS,DEFAULT_DOC_ORIENT_OPTIONS,DEFAULT_SESSION_OPTIONS,DEFAULT_TEXT_ORIENT_OPTIONS,DEFAULT_TEXT_UNWARP_OPTIONS,MODEL_URLS}from"../constants.js";
@@ -0,0 +1,12 @@
1
+ import type { CoreCanvas, PlatformProvider } from "../core/platform.js";
2
+ /**
3
+ * PlatformProvider implementation for browser environments.
4
+ */
5
+ export declare class WebPlatformProvider implements PlatformProvider<CoreCanvas> {
6
+ readonly pathSeparator = "/";
7
+ readonly ort: PlatformProvider["ort"];
8
+ createCanvas(_width: number, _height: number): CoreCanvas;
9
+ isCanvas(image: unknown): image is CoreCanvas;
10
+ loadResource(source: string | ArrayBuffer | undefined, defaultUrl: string): Promise<ArrayBuffer>;
11
+ saveDebugImage(_canvas: CoreCanvas, _filename: string, _outputDir: string): Promise<void>;
12
+ }
@@ -0,0 +1 @@
1
+ export class WebPlatformProvider{pathSeparator="/";ort=ort;createCanvas(_width,_height){let canvas=document.createElement("canvas");canvas.width=_width;canvas.height=_height;canvas.getContext("2d",{willReadFrequently:true});return canvas}isCanvas(image){return!!(image instanceof HTMLCanvasElement||typeof OffscreenCanvas!=="undefined"&&image instanceof OffscreenCanvas||image&&typeof image.getContext==="function")}async loadResource(source,defaultUrl){if(source instanceof ArrayBuffer){return source}let sourceToLoad=typeof source==="string"?source:defaultUrl;let response=await fetch(sourceToLoad);if(!response.ok){throw new Error(`Failed to fetch resource from ${sourceToLoad}`)}return response.arrayBuffer()}async saveDebugImage(_canvas,_filename,_outputDir){return Promise.resolve()}}import*as ort from"onnxruntime-web";if(typeof window!=="undefined"&&!ort.env.wasm.wasmPaths){ort.env.wasm.wasmPaths="https://cdn.jsdelivr.net/npm/onnxruntime-web@1.24.2/dist/"}
@@ -0,0 +1,20 @@
1
+ import { BaseTextOrientService } from "../core/base-text-orient.service.js";
2
+ import type { TextOrientServiceOptions } from "../interface.js";
3
+ /**
4
+ * Self-contained browser service for classifying text line orientation.
5
+ *
6
+ * Handles model fetching and lazy initialization internally.
7
+ * The model is fetched on the first call to `run()`.
8
+ */
9
+ export declare class TextOrientService {
10
+ private readonly options;
11
+ private inner;
12
+ private session;
13
+ constructor(options?: TextOrientServiceOptions);
14
+ private log;
15
+ private ensureSession;
16
+ private loadModel;
17
+ run(...args: Parameters<BaseTextOrientService["run"]>): ReturnType<BaseTextOrientService["run"]>;
18
+ isInitialized(): boolean;
19
+ destroy(): Promise<void>;
20
+ }
@@ -0,0 +1,3 @@
1
+ export class TextOrientService{options;inner=null;session=null;constructor(options={}){this.options=options}log(message){if(this.options.debugging?.verbose){console.log(`[TextOrientService] ${message}`)}}async ensureSession(){if(this.inner)return;let buffer=await this.loadModel();let sessionOptions=this.options.session||DEFAULT_WEB_SESSION;this.session=await ort.InferenceSession.create(new Uint8Array(buffer),sessionOptions);this.log(`Model loaded
2
+ input: ${this.session.inputNames}
3
+ output: ${this.session.outputNames}`);this.inner=new BaseTextOrientService(new WebPlatformProvider,this.session,this.options.textOrient,this.options.debugging)}async loadModel(){if(this.options.model instanceof ArrayBuffer)return this.options.model;let url=typeof this.options.model==="string"?this.options.model:MODEL_URLS.textOrient;this.log(`Fetching model from: ${url}`);let response=await fetch(url);if(!response.ok)throw new Error(`Failed to fetch model from ${url}`);return response.arrayBuffer()}async run(...args){await this.ensureSession();return this.inner.run(...args)}isInitialized(){return this.session!==null}async destroy(){await this.session?.release();this.session=null;this.inner=null}}import*as ort from"onnxruntime-web";import{BaseTextOrientService}from"../core/base-text-orient.service.js";import{MODEL_URLS}from"../constants.js";import{WebPlatformProvider}from"./platform.web.js";let DEFAULT_WEB_SESSION={executionProviders:["wasm"],graphOptimizationLevel:"all"};
@@ -0,0 +1,20 @@
1
+ import { BaseTextUnwarpService } from "../core/base-text-unwarp.service.js";
2
+ import type { TextUnwarpServiceOptions } from "../interface.js";
3
+ /**
4
+ * Self-contained browser service for unwarping distorted document images.
5
+ *
6
+ * Handles model fetching and lazy initialization internally.
7
+ * The model is fetched on the first call to `run()`.
8
+ */
9
+ export declare class TextUnwarpService {
10
+ private readonly options;
11
+ private inner;
12
+ private session;
13
+ constructor(options?: TextUnwarpServiceOptions);
14
+ private log;
15
+ private ensureSession;
16
+ private loadModel;
17
+ run(...args: Parameters<BaseTextUnwarpService["run"]>): ReturnType<BaseTextUnwarpService["run"]>;
18
+ isInitialized(): boolean;
19
+ destroy(): Promise<void>;
20
+ }
@@ -0,0 +1,3 @@
1
+ export class TextUnwarpService{options;inner=null;session=null;constructor(options={}){this.options=options}log(message){if(this.options.debugging?.verbose){console.log(`[TextUnwarpService] ${message}`)}}async ensureSession(){if(this.inner)return;let buffer=await this.loadModel();let sessionOptions=this.options.session||DEFAULT_WEB_SESSION;this.session=await ort.InferenceSession.create(new Uint8Array(buffer),sessionOptions);this.log(`Model loaded
2
+ input: ${this.session.inputNames}
3
+ output: ${this.session.outputNames}`);this.inner=new BaseTextUnwarpService(new WebPlatformProvider,this.session,this.options.textUnwarp,this.options.debugging)}async loadModel(){if(this.options.model instanceof ArrayBuffer)return this.options.model;let url=typeof this.options.model==="string"?this.options.model:MODEL_URLS.textUnwarp;this.log(`Fetching model from: ${url}`);let response=await fetch(url);if(!response.ok)throw new Error(`Failed to fetch model from ${url}`);return response.arrayBuffer()}async run(...args){await this.ensureSession();return this.inner.run(...args)}isInitialized(){return this.session!==null}async destroy(){await this.session?.release();this.session=null;this.inner=null}}import*as ort from"onnxruntime-web";import{BaseTextUnwarpService}from"../core/base-text-unwarp.service.js";import{MODEL_URLS}from"../constants.js";import{WebPlatformProvider}from"./platform.web.js";let DEFAULT_WEB_SESSION={executionProviders:["wasm"],graphOptimizationLevel:"all"};