ppu-ocv 1.7.0 → 3.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.
Files changed (62) hide show
  1. package/README.md +200 -47
  2. package/canvas-factory.d.ts +42 -0
  3. package/canvas-factory.js +1 -0
  4. package/canvas-processor.d.ts +18 -0
  5. package/canvas-processor.js +1 -0
  6. package/canvas-toolkit.base.d.ts +52 -0
  7. package/canvas-toolkit.base.js +1 -0
  8. package/canvas-toolkit.d.ts +8 -87
  9. package/canvas-toolkit.js +1 -1
  10. package/contours.d.ts +4 -3
  11. package/contours.js +1 -1
  12. package/cv-provider.d.ts +45 -0
  13. package/cv-provider.js +1 -0
  14. package/deskew.d.ts +68 -0
  15. package/deskew.js +1 -0
  16. package/image-analysis.d.ts +3 -3
  17. package/image-analysis.js +1 -1
  18. package/image-processor.d.ts +51 -59
  19. package/image-processor.js +1 -1
  20. package/index.canvas-web.d.ts +6 -0
  21. package/index.canvas-web.js +1 -0
  22. package/index.canvas.d.ts +8 -0
  23. package/index.canvas.js +1 -0
  24. package/index.d.ts +6 -1
  25. package/index.js +1 -1
  26. package/index.web.d.ts +14 -0
  27. package/index.web.js +1 -0
  28. package/operations/adaptive-threshold.d.ts +3 -3
  29. package/operations/adaptive-threshold.js +1 -1
  30. package/operations/blur.d.ts +3 -3
  31. package/operations/blur.js +1 -1
  32. package/operations/border.d.ts +3 -3
  33. package/operations/border.js +1 -1
  34. package/operations/canny.d.ts +3 -3
  35. package/operations/canny.js +1 -1
  36. package/operations/convert.d.ts +3 -3
  37. package/operations/convert.js +1 -1
  38. package/operations/dilate.d.ts +3 -3
  39. package/operations/dilate.js +1 -1
  40. package/operations/erode.d.ts +3 -3
  41. package/operations/erode.js +1 -1
  42. package/operations/grayscale.d.ts +3 -3
  43. package/operations/grayscale.js +1 -1
  44. package/operations/invert.d.ts +3 -3
  45. package/operations/invert.js +1 -1
  46. package/operations/morphological-gradient.d.ts +3 -3
  47. package/operations/morphological-gradient.js +1 -1
  48. package/operations/resize.d.ts +3 -3
  49. package/operations/resize.js +1 -1
  50. package/operations/rotate.d.ts +3 -3
  51. package/operations/rotate.js +1 -1
  52. package/operations/threshold.d.ts +3 -3
  53. package/operations/threshold.js +1 -1
  54. package/operations/warp.d.ts +4 -3
  55. package/operations/warp.js +1 -1
  56. package/package.json +26 -7
  57. package/pipeline/registry.d.ts +1 -1
  58. package/pipeline/types.d.ts +1 -1
  59. package/platform/node.d.ts +3 -0
  60. package/platform/node.js +1 -0
  61. package/platform/web.d.ts +3 -0
  62. package/platform/web.js +1 -0
package/README.md CHANGED
@@ -11,7 +11,7 @@ const processor = new ImageProcessor(canvas);
11
11
 
12
12
  const result = processor
13
13
  .grayscale()
14
- .blur({ size: 5 })
14
+ .blur({ size: [5, 5] })
15
15
  .threshold()
16
16
  .invert()
17
17
  .dilate({ size: [20, 20], iter: 5 })
@@ -32,6 +32,8 @@ OpenCV is powerful but can be cumbersome to use directly. This library provides:
32
32
  3. **Development Speed**: Add image processing to your app in minutes, not hours
33
33
  4. **Extensibility**: Custom operations for your specific needs without library modifications
34
34
  5. **TypeScript Integration**: Full IntelliSense support with parameter validation
35
+ 6. **Web Support**: Supports running directly in the browser
36
+ 7. **Loosely Coupled**: Canvas utilities are fully decoupled from OpenCV. Usable in Browser Extensions, Service Workers, and other constrained environments where OpenCV cannot be initialised
35
37
 
36
38
  ## Installation
37
39
 
@@ -43,48 +45,45 @@ yarn add ppu-ocv
43
45
  bun add ppu-ocv
44
46
  ```
45
47
 
46
- > [!NOTE]
47
- > This project is developed and tested primarily with Bun.
48
- > Support for Node.js, Deno, or browser environments is **not guaranteed**.
49
- > If you choose to use it outside of Bun and encounter any issues, feel free to report them.
50
- > I'm open to fixing bugs for other runtimes with community help.
48
+ ## Usage (Node.js / Bun)
51
49
 
52
- ## Usage
53
-
54
- Note that Operation order is matter, you should atleast know some basic in using OpenCV. See the operations table below this.
50
+ Note that operation order matters — you should have at least basic familiarity with OpenCV. See the operations table below.
55
51
 
56
52
  ```ts
57
- import { ImageProcessor } from "ppu-ocv";
53
+ import { CanvasProcessor, ImageProcessor } from "ppu-ocv";
58
54
 
59
55
  const file = Bun.file("./assets/receipt.jpg");
60
56
  const image = await file.arrayBuffer();
61
57
 
62
- const canvas = await ImageProcessor.prepareCanvas(image);
63
- await ImageProcessor.initRuntime();
58
+ await ImageProcessor.initRuntime(); // init opencv
59
+ const canvas = await CanvasProcessor.prepareCanvas(image);
64
60
 
65
61
  const processor = new ImageProcessor(canvas);
66
- processor.grayscale().blur({ size: 5 }).threshold();
62
+ processor
63
+ .grayscale()
64
+ .blur({ size: [5, 5] })
65
+ .threshold();
67
66
 
68
67
  const resultCanvas = processor.toCanvas();
69
68
  processor.destroy();
70
69
  ```
71
70
 
72
- Or you can do directly via execute api:
71
+ Or use the `execute` API directly:
73
72
 
74
73
  ```ts
75
- import { CanvasToolkit, ImageProcessor, cv } from "ppu-ocv";
74
+ import { CanvasProcessor, CanvasToolkit, ImageProcessor, cv } from "ppu-ocv";
76
75
 
77
76
  const file = Bun.file("./assets/receipt.jpg");
78
77
  const image = await file.arrayBuffer();
79
78
 
80
79
  const canvasToolkit = CanvasToolkit.getInstance();
81
- const canvas = await ImageProcessor.prepareCanvas(image);
82
80
  await ImageProcessor.initRuntime();
81
+ const canvas = await CanvasProcessor.prepareCanvas(image);
83
82
 
84
83
  const processor = new ImageProcessor(canvas);
85
84
  const grayscaleImg = processor.execute("grayscale").toCanvas();
86
85
 
87
- // the pipeline operation continued from grayscaled image
86
+ // The pipeline continues from the grayscaled image
88
87
  const thresholdImg = processor
89
88
  .execute("blur")
90
89
  .execute("threshold", {
@@ -101,6 +100,127 @@ await canvasToolkit.saveImage({
101
100
 
102
101
  For more advanced usage, see: [Example usage of ppu-ocv](./examples)
103
102
 
103
+ ## Canvas-only usage (no OpenCV)
104
+
105
+ Starting from v3.0.0, canvas utilities are fully decoupled from OpenCV. If you only need canvas I/O (e.g. loading/saving images, cropping, drawing) without any image processing, import from `ppu-ocv/canvas` (Node) or `ppu-ocv/canvas-web` (browser). OpenCV is **never imported or initialised** by these entry points, making them safe for use in Browser Extensions, Service Workers, and edge runtimes.
106
+
107
+ ```ts
108
+ // Node.js — zero OpenCV dependency
109
+ import { CanvasProcessor, CanvasToolkit } from "ppu-ocv/canvas";
110
+
111
+ const file = Bun.file("./assets/image.jpg");
112
+ const canvas = await CanvasProcessor.prepareCanvas(await file.arrayBuffer());
113
+
114
+ const toolkit = CanvasToolkit.getInstance();
115
+ const cropped = toolkit.crop({
116
+ canvas,
117
+ bbox: { x0: 0, y0: 0, x1: 100, y1: 100 },
118
+ });
119
+
120
+ const buffer = await CanvasProcessor.prepareBuffer(cropped);
121
+ ```
122
+
123
+ ```ts
124
+ // Browser Extension background script — zero OpenCV dependency
125
+ import { CanvasProcessor, CanvasToolkit } from "ppu-ocv/canvas-web";
126
+
127
+ const response = await fetch("/image.jpg");
128
+ const canvas = await CanvasProcessor.prepareCanvas(
129
+ await response.arrayBuffer(),
130
+ );
131
+ ```
132
+
133
+ ## Web / Browser Support
134
+
135
+ Import from `ppu-ocv/web` to use the browser-native canvas APIs (`HTMLCanvasElement` / `OffscreenCanvas`) instead of `@napi-rs/canvas`.
136
+
137
+ ### With a bundler (Vite, webpack, etc.)
138
+
139
+ ```ts
140
+ import { CanvasProcessor, ImageProcessor, cv } from "ppu-ocv/web";
141
+
142
+ await ImageProcessor.initRuntime();
143
+
144
+ const response = await fetch("/my-image.jpg");
145
+ const buffer = await response.arrayBuffer();
146
+
147
+ const canvas = await CanvasProcessor.prepareCanvas(buffer);
148
+ const processor = new ImageProcessor(canvas);
149
+
150
+ processor
151
+ .grayscale()
152
+ .blur({ size: [5, 5] })
153
+ .threshold();
154
+
155
+ const result = processor.toCanvas(); // returns HTMLCanvasElement
156
+ document.body.appendChild(result);
157
+
158
+ processor.destroy();
159
+ ```
160
+
161
+ ### Vanilla HTML (no bundler)
162
+
163
+ `initRuntime()` automatically loads `@techstark/opencv-js` from the npm CDN if it's not already available. No extra script tags or import maps needed:
164
+
165
+ ```html
166
+ <script type="module">
167
+ import {
168
+ CanvasProcessor,
169
+ ImageProcessor,
170
+ } from "https://cdn.jsdelivr.net/npm/ppu-ocv@3/index.web.js";
171
+ await ImageProcessor.initRuntime();
172
+
173
+ const response = await fetch("/my-image.jpg");
174
+ const canvas = await CanvasProcessor.prepareCanvas(
175
+ await response.arrayBuffer(),
176
+ );
177
+
178
+ const processor = new ImageProcessor(canvas);
179
+ processor
180
+ .grayscale()
181
+ .blur({ size: [5, 5] })
182
+ .threshold();
183
+
184
+ const result = processor.toCanvas();
185
+ processor.destroy();
186
+ </script>
187
+ ```
188
+
189
+ > **Note:** ES modules require HTTP/HTTPS — use a local server (`npx serve .`) for dev, or deploy to GitHub Pages.
190
+
191
+ See the [interactive demo](./index.html) for a full working example.
192
+
193
+ ### Entry point reference
194
+
195
+ | Import path | OpenCV | Canvas backend | `CanvasToolkit` | Use case |
196
+ | -------------------- | ------ | ------------------------------------- | -------------------- | ------------------------------------------ |
197
+ | `ppu-ocv` | ✅ | `@napi-rs/canvas` | Full (with file I/O) | Full pipeline, Node.js / Bun |
198
+ | `ppu-ocv/web` | ✅ | `HTMLCanvasElement`/`OffscreenCanvas` | Base only | Full pipeline, browser |
199
+ | `ppu-ocv/canvas` | ❌ | `@napi-rs/canvas` | Full (with file I/O) | Canvas-only, Node (extensions, edge, etc.) |
200
+ | `ppu-ocv/canvas-web` | ❌ | `HTMLCanvasElement`/`OffscreenCanvas` | Base only | Canvas-only, browser extensions / SW |
201
+
202
+ ### Platform abstraction
203
+
204
+ Under the hood, ppu-ocv uses a platform abstraction layer. Each entry point auto-registers its platform. You can also register a custom platform:
205
+
206
+ ```ts
207
+ import { setPlatform, type CanvasPlatform } from "ppu-ocv/web";
208
+
209
+ const myPlatform: CanvasPlatform = {
210
+ createCanvas(width, height) {
211
+ /* ... */
212
+ },
213
+ loadImage(source) {
214
+ /* ... */
215
+ },
216
+ isCanvas(value) {
217
+ /* ... */
218
+ },
219
+ };
220
+
221
+ setPlatform(myPlatform);
222
+ ```
223
+
104
224
  ## Built-in pipeline operations
105
225
 
106
226
  To avoid bloat, we only ship essential operations for chaining. Currently shipped operations are:
@@ -123,23 +243,34 @@ To avoid bloat, we only ship essential operations for chaining. Currently shippe
123
243
 
124
244
  ## Extending operations
125
245
 
126
- You can easily add your own by creating a prototype method or extend the class of `ImageProcessor`.
246
+ You can easily add your own by creating a prototype method or extending the `ImageProcessor` class.
127
247
 
128
248
  See: [How to extend ppu-ocv operations](./docs/how-to-extend-ppu-ocv-operations.md)
129
249
 
130
250
  ## Class documentation
131
251
 
252
+ #### `CanvasProcessor`
253
+
254
+ Canvas I/O utilities with **no OpenCV dependency**. Available from all entry points including `ppu-ocv/canvas` and `ppu-ocv/canvas-web`.
255
+
256
+ | Method | Args | Description |
257
+ | ---------------------- | ----------- | ----------------------------------------------------- |
258
+ | static `prepareCanvas` | ArrayBuffer | Load image bytes into a `CanvasLike` |
259
+ | static `prepareBuffer` | CanvasLike | Export a `CanvasLike` to an `ArrayBuffer` (PNG bytes) |
260
+
132
261
  #### `ImageProcessor`
133
262
 
134
- | Method | Args | Description |
135
- | ---------------------- | ---------------- | --------------------------------------------------------------------------- |
136
- | constructor | cv.Mat or Canvas | Instantiate processor with initial image |
137
- | static `prepareCanvas` | ArrayBuffer | Utility to load image from file buffer to canvas |
138
- | static `initRuntime` | | Important open-cv runtime initialization, required to call once per runtime |
139
- | operations | depends | Chainable operations like `blur`, `grayscale`, `resize` and so on |
140
- | `execute` | name, options | Chainable operations directly via `execute` api |
141
- | outputs | | Non-chainable & non-interupting method for output like `toMat`, `toCanvas` |
142
- | `destroy` | | Non-chainable clean-up memory to destroy the object and the state |
263
+ Requires OpenCV. Available from `ppu-ocv` and `ppu-ocv/web`.
264
+
265
+ | Method | Args | Description |
266
+ | -------------------- | ---------------- | ------------------------------------------------------------------ |
267
+ | constructor | cv.Mat or Canvas | Instantiate processor with initial image |
268
+ | static `initRuntime` | | OpenCV runtime initialization required once per runtime |
269
+ | operations | depends | Chainable operations like `blur`, `grayscale`, `resize`, and so on |
270
+ | `execute` | name, options | Chainable operations via the `execute` API |
271
+ | `toMat` | | Return the current image as a `cv.Mat` |
272
+ | `toCanvas` | | Return the current image as a `CanvasLike` |
273
+ | `destroy` | | Clean up `cv.Mat` memory |
143
274
 
144
275
  #### `CanvasToolkit`
145
276
 
@@ -147,31 +278,41 @@ See: [How to extend ppu-ocv operations](./docs/how-to-extend-ppu-ocv-operations.
147
278
  | ------------- | ---------------------- | ----------------------------------------------------------------------------------------- |
148
279
  | `crop` | BoundingBox, Canvas | Crop a part of source canvas and return a new canvas of the cropped part |
149
280
  | `isDirty` | Canvas, threshold | Check whether a binary canvas is dirty (full of major color either black or white) or not |
150
- | `saveImage` | Canvas, filename, path | Save a canvas to an image file |
151
- | `clearOutput` | path | Clear the output folder |
281
+ | `saveImage` | Canvas, filename, path | Save a canvas to an image file _(Node only)_ |
282
+ | `clearOutput` | path | Clear the output folder _(Node only)_ |
152
283
  | `drawLine` | ctx, coordinate, style | Draw a non-filled rectangle outline on the canvas |
153
- | `drawContour` | ctx, contour, style | Draw a contour on the canvas |
284
+ | `drawContour` | ctx, contour, style | Draw a contour on the canvas — accepts any `ContourLike` (`{ data32S }`) |
285
+
286
+ #### `DeskewService`
287
+
288
+ Detects and corrects text skew in document images using a multi-method consensus approach (minAreaRect, baseline analysis, Hough transform). Requires OpenCV. Available from `ppu-ocv` and `ppu-ocv/web`.
289
+
290
+ | Method | Args | Description |
291
+ | -------------------- | ------------- | ------------------------------------ |
292
+ | constructor | DeskewOptions | `verbose`, `minimumAreaThreshold` |
293
+ | `calculateSkewAngle` | CanvasLike | Detect skew angle in degrees |
294
+ | `deskewImage` | CanvasLike | Return a deskewed copy of the canvas |
154
295
 
155
296
  #### `Contours`
156
297
 
157
- | Method | Args | Description |
158
- | ----------------------- | --------------- | ----------------------------------------------------------------------------------------- |
159
- | constructor | cv.Mat, options | Instantiate Contours and automatically find & store contour list from args |
160
- | `getAll` | | Crop a part of source canvas and return a new canvas of the cropped part |
161
- | `getFromIndex` | index | Get contour at a specific index |
162
- | `getRect` | | Get the rectangle that bounds the contour |
163
- | `iterate` | callback | Iterate over all contours and call the callback function for each contour |
164
- | `getLargestContourArea` | | Get the largest contour area |
165
- | `getCornerPoints` | Canvas, contour | Get four corner points for a given contour. Useful for perspective transformation (warp). |
166
- | `destroy` | | Destroy & clean-up the memory from the contours |
298
+ | Method | Args | Description |
299
+ | -------------------------------- | --------------- | ---------------------------------------------------------------- |
300
+ | constructor | cv.Mat, options | Instantiate Contours and automatically find & store contour list |
301
+ | `getAll` | | Return the full `cv.MatVector` of contours |
302
+ | `getFromIndex` | index | Get contour at a specific index |
303
+ | `getRect` | contour | Get the bounding rectangle of a contour |
304
+ | `iterate` | callback | Iterate over all contours |
305
+ | `getLargestContourArea` | | Return the contour with the largest area |
306
+ | `getCornerPoints` | options | Get four corner points for perspective transformation (warp) |
307
+ | `getApproximateRectangleContour` | options | Simplify a contour to an approximate rectangle |
308
+ | `destroy` | | Destroy and clean up contour memory |
167
309
 
168
310
  #### `ImageAnalysis`
169
311
 
170
- Just a collection of utility functions for analyzing image properties.
312
+ A collection of utility functions for analyzing image properties (requires OpenCV).
171
313
 
172
- - `calculateMeanNormalizedLabLightness`: Calculates the mean normalized lightness of an image using the L channel of the Lab color space. Lightness is normalized based on the image's own maximum lightness value before averaging.
173
-
174
- - `calculateMeanGrayscaleValue`: Calculates the mean pixel value of the image after converting it to grayscale.
314
+ - `calculateMeanNormalizedLabLightness`: Calculates the mean normalized lightness of an image using the L channel of the Lab color space.
315
+ - `calculateMeanGrayscaleValue`: Calculates the mean pixel value after converting to grayscale.
175
316
 
176
317
  ## Contributing
177
318
 
@@ -194,12 +335,10 @@ Ensure that all tests pass before submitting your pull request.
194
335
 
195
336
  ## Scripts
196
337
 
197
- Recommended development environment is in linux-based environment.
338
+ Recommended development environment is in a Linux-based environment.
198
339
 
199
340
  Library template: https://github.com/aquapi/lib-template
200
341
 
201
- All script sources and usage.
202
-
203
342
  ### [Build](./scripts/build.ts)
204
343
 
205
344
  Emit `.js` and `.d.ts` files to [`lib`](./lib).
@@ -208,6 +347,20 @@ Emit `.js` and `.d.ts` files to [`lib`](./lib).
208
347
 
209
348
  Move [`package.json`](./package.json), [`README.md`](./README.md) to [`lib`](./lib) and publish the package.
210
349
 
350
+ ## Migrating from v2
351
+
352
+ See [MIGRATION.md](./MIGRATION.md) for a full guide. The short version:
353
+
354
+ ```diff
355
+ - import { ImageProcessor } from "ppu-ocv";
356
+ - const canvas = await ImageProcessor.prepareCanvas(buffer);
357
+ - const buf = await ImageProcessor.prepareBuffer(canvas);
358
+
359
+ + import { CanvasProcessor } from "ppu-ocv";
360
+ + const canvas = await CanvasProcessor.prepareCanvas(buffer);
361
+ + const buf = await CanvasProcessor.prepareBuffer(canvas);
362
+ ```
363
+
211
364
  ## License
212
365
 
213
366
  This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Platform abstraction layer for canvas operations.
3
+ * Allows ppu-ocv to work with both @napi-rs/canvas (Node) and browser canvas APIs.
4
+ */
5
+ /** Structural type satisfied by both @napi-rs/canvas Canvas and HTMLCanvasElement/OffscreenCanvas */
6
+ export interface CanvasLike {
7
+ width: number;
8
+ height: number;
9
+ getContext(contextId: "2d"): any;
10
+ toBuffer?: (...args: any[]) => Buffer;
11
+ toDataURL?: (...args: any[]) => string;
12
+ }
13
+ /** Structural type for 2D rendering context */
14
+ export interface Context2DLike {
15
+ canvas: any;
16
+ drawImage(...args: any[]): void;
17
+ getImageData(sx: number, sy: number, sw: number, sh: number): {
18
+ data: Uint8ClampedArray;
19
+ width: number;
20
+ height: number;
21
+ };
22
+ putImageData(imageData: any, dx: number, dy: number): void;
23
+ createImageData(width: number, height: number): any;
24
+ beginPath(): void;
25
+ closePath(): void;
26
+ moveTo(x: number, y: number): void;
27
+ lineTo(x: number, y: number): void;
28
+ stroke(): void;
29
+ strokeRect(x: number, y: number, w: number, h: number): void;
30
+ strokeStyle: string | CanvasGradient | CanvasPattern;
31
+ lineWidth: number;
32
+ }
33
+ /** Platform-specific canvas operations */
34
+ export interface CanvasPlatform {
35
+ createCanvas(width: number, height: number): CanvasLike;
36
+ loadImage(source: ArrayBuffer | string): Promise<CanvasLike>;
37
+ isCanvas(value: unknown): value is CanvasLike;
38
+ }
39
+ /** Register the platform-specific canvas implementation */
40
+ export declare function setPlatform(platform: CanvasPlatform): void;
41
+ /** Get the registered platform. Throws if none has been set. */
42
+ export declare function getPlatform(): CanvasPlatform;
@@ -0,0 +1 @@
1
+ let _platform=null;export function setPlatform(platform){_platform=platform}export function getPlatform(){if(!_platform){throw new Error("No canvas platform registered. "+'Import "ppu-ocv" (Node), "ppu-ocv/web" (browser), '+'"ppu-ocv/canvas" (Node canvas-only), or "ppu-ocv/canvas-web" (browser canvas-only) to auto-register.')}return _platform}
@@ -0,0 +1,18 @@
1
+ import type { CanvasLike } from "./canvas-factory.js";
2
+ /**
3
+ * Canvas I/O utilities that work without OpenCV.
4
+ * Safe to use in constrained environments (e.g. Browser Extensions)
5
+ * where OpenCV cannot be initialized.
6
+ */
7
+ export declare class CanvasProcessor {
8
+ /**
9
+ * Convert an ArrayBuffer (image file bytes) to a CanvasLike.
10
+ * If the value is already a CanvasLike it is returned as-is.
11
+ */
12
+ static prepareCanvas(file: ArrayBuffer): Promise<CanvasLike>;
13
+ /**
14
+ * Convert a CanvasLike to an ArrayBuffer (PNG bytes).
15
+ * If the value is already an ArrayBuffer it is returned as-is.
16
+ */
17
+ static prepareBuffer(canvas: CanvasLike): Promise<ArrayBuffer>;
18
+ }
@@ -0,0 +1 @@
1
+ export class CanvasProcessor{static async prepareCanvas(file){if(getPlatform().isCanvas(file))return file;return getPlatform().loadImage(file)}static async prepareBuffer(canvas){if(canvas instanceof ArrayBuffer)return canvas;if(typeof canvas.toBuffer==="function"){let buffer=canvas.toBuffer("image/png");let arrayBuffer=new ArrayBuffer(buffer.byteLength);new Uint8Array(arrayBuffer).set(new Uint8Array(buffer));return arrayBuffer}if(typeof canvas.toDataURL==="function"){let dataURL=canvas.toDataURL("image/png");let base64Data=dataURL.replace(/^data:image\/png;base64,/,"");let binaryString=atob(base64Data);let arrayBuffer=new ArrayBuffer(binaryString.length);let bytes=new Uint8Array(arrayBuffer);for(let i=0;i<binaryString.length;i++){bytes[i]=binaryString.charCodeAt(i)}return arrayBuffer}let ctx=canvas.getContext("2d");let imageData=ctx.getImageData(0,0,canvas.width,canvas.height);let canvasBuffer=new ArrayBuffer(imageData.data.byteLength);new Uint8Array(canvasBuffer).set(new Uint8Array(imageData.data.buffer,imageData.data.byteOffset,imageData.data.byteLength));return canvasBuffer}}import{getPlatform}from"./canvas-factory.js";
@@ -0,0 +1,52 @@
1
+ import type { BoundingBox } from "./index.interface.js";
2
+ import type { CanvasLike, Context2DLike } from "./canvas-factory.js";
3
+ /** Structural interface for contour-like objects with 32-bit signed integer point data */
4
+ export interface ContourLike {
5
+ data32S: Int32Array | number[];
6
+ }
7
+ /**
8
+ * Cross-platform base class for canvas manipulation utilities.
9
+ * Contains only methods that work in both Node and browser environments.
10
+ */
11
+ export declare class CanvasToolkitBase {
12
+ protected static _baseInstance: CanvasToolkitBase | null;
13
+ protected step: number;
14
+ protected constructor();
15
+ static getInstance(): CanvasToolkitBase;
16
+ /**
17
+ * Crop a part of source canvas and return a new canvas of the cropped part
18
+ */
19
+ crop(options: {
20
+ bbox: BoundingBox;
21
+ canvas: CanvasLike;
22
+ }): CanvasLike;
23
+ /**
24
+ * Check whether a binary canvas is dirty (full of major color either black or white) or not
25
+ */
26
+ isDirty(options: {
27
+ canvas: CanvasLike;
28
+ threshold?: number;
29
+ majorColorThreshold?: number;
30
+ }): boolean;
31
+ /**
32
+ * Draw a non-filled rectangle on the canvas
33
+ */
34
+ drawLine(options: {
35
+ ctx: Context2DLike;
36
+ x: number;
37
+ y: number;
38
+ width: number;
39
+ height: number;
40
+ lineWidth?: number;
41
+ color?: string;
42
+ }): void;
43
+ /**
44
+ * Draw a contour on the canvas
45
+ */
46
+ drawContour(options: {
47
+ ctx: Context2DLike;
48
+ contour: ContourLike;
49
+ strokeStyle?: string;
50
+ lineWidth?: number;
51
+ }): void;
52
+ }
@@ -0,0 +1 @@
1
+ export class CanvasToolkitBase{static _baseInstance=null;step=0;constructor(){}static getInstance(){if(!CanvasToolkitBase._baseInstance){CanvasToolkitBase._baseInstance=new CanvasToolkitBase}return CanvasToolkitBase._baseInstance}crop(options){const{bbox,canvas}=options;let croppedCanvas=getPlatform().createCanvas(bbox.x1-bbox.x0,bbox.y1-bbox.y0);let croppedCtx=croppedCanvas.getContext("2d");croppedCtx.drawImage(canvas,bbox.x0,bbox.y0,bbox.x1-bbox.x0,bbox.y1-bbox.y0,0,0,croppedCanvas.width,croppedCanvas.height);return croppedCanvas}isDirty(options){const{canvas,threshold=127.5,majorColorThreshold=0.97}=options;let whiteCount=0;let blackCount=0;let borderlessCanvas=this.crop({bbox:{x0:canvas.width*0.1,y0:canvas.height*0.1,x1:canvas.width*0.9,y1:canvas.height*0.9},canvas});let ctx=borderlessCanvas.getContext("2d");let colorData=ctx.getImageData(0,0,borderlessCanvas.width,borderlessCanvas.height).data;for(let i=0;i<colorData.length;i+=4){let red=colorData[i];let green=colorData[i+1];let blue=colorData[i+2];if(red>=threshold&&green>=threshold&&blue>=threshold){whiteCount++}else{blackCount++}}let majorColorRatio=Math.max(whiteCount,blackCount)/(blackCount+whiteCount);return majorColorRatio<majorColorThreshold}drawLine(options){const{ctx,x,y,width,height,lineWidth=2,color="blue"}=options;ctx.beginPath();ctx.strokeStyle=color;ctx.lineWidth=lineWidth;ctx.strokeRect(x,y,width,height);ctx.closePath()}drawContour(options){const{ctx,contour,strokeStyle="red",lineWidth=2}=options;let pts=contour.data32S;if(pts.length<4)return;ctx.strokeStyle=strokeStyle;ctx.lineWidth=lineWidth;ctx.beginPath();ctx.moveTo(pts[0],pts[1]);for(let i=2;i<pts.length;i+=2){ctx.lineTo(pts[i],pts[i+1])}ctx.closePath();ctx.stroke()}}import{getPlatform}from"./canvas-factory.js";
@@ -1,58 +1,13 @@
1
- import type { BoundingBox, SKRSContext2D } from "./index.js";
2
- import { Canvas, cv } from "./index.js";
1
+ import type { CanvasLike } from "./canvas-factory.js";
2
+ import { CanvasToolkitBase } from "./canvas-toolkit.base.js";
3
3
  /**
4
- * Singleton class for canvas manipulation utilities
4
+ * Node.js canvas toolkit with file-system operations.
5
+ * Extends CanvasToolkitBase with saveImage() and clearOutput().
5
6
  */
6
- export declare class CanvasToolkit {
7
- private static instance;
8
- private step;
9
- /**
10
- * Private constructor to prevent direct instantiation
11
- */
12
- private constructor();
13
- /**
14
- * Get the singleton instance of CanvasToolkit
15
- * @returns The singleton instance
16
- * @example
17
- * const canvasToolkit = CanvasToolkit.getInstance();
18
- */
7
+ export declare class CanvasToolkit extends CanvasToolkitBase {
8
+ private static _nodeInstance;
9
+ protected constructor();
19
10
  static getInstance(): CanvasToolkit;
20
- /**
21
- * Crop a part of source canvas and return a new canvas of the cropped part
22
- * @param options
23
- * @param options.bbox Bounding box of the cropped part
24
- * @param options.canvas Source canvas
25
- * @returns A new canvas of the cropped part
26
- * @example
27
- * const croppedCanvas = CanvasToolkit.getInstance().crop({
28
- * bbox: { x0: 10, y0: 10, x1: 100, y1: 100 },
29
- * canvas: sourceCanvas,
30
- * });
31
- */
32
- crop(options: {
33
- bbox: BoundingBox;
34
- canvas: Canvas;
35
- }): Canvas;
36
- /**
37
- * Check whether a binary canvas is dirty (full of major color either black or white) or not
38
- * @param options
39
- * @param options.canvas Source canvas
40
- * @param options.threshold Threshold for color detection (default: 127.5)
41
- * @param options.majorColorThreshold Major color threshold (default: 0.97)
42
- * @returns true if the canvas is dirty, false otherwise
43
- * @example
44
- * const isDirty = CanvasToolkit.getInstance().isDirty({
45
- * canvas: sourceCanvas,
46
- * threshold: 127.5,
47
- * majorColorThreshold: 0.97,
48
- * });
49
- * console.log(isDirty); // true or false
50
- */
51
- isDirty(options: {
52
- canvas: Canvas;
53
- threshold?: number;
54
- majorColorThreshold?: number;
55
- }): boolean;
56
11
  /**
57
12
  * Save a canvas to an image file
58
13
  * @param options
@@ -67,7 +22,7 @@ export declare class CanvasToolkit {
67
22
  * });
68
23
  */
69
24
  saveImage(options: {
70
- canvas: Canvas;
25
+ canvas: CanvasLike;
71
26
  filename: string;
72
27
  path: string;
73
28
  }): Promise<void>;
@@ -76,38 +31,4 @@ export declare class CanvasToolkit {
76
31
  * @param path Path to the output folder (default: "out")
77
32
  */
78
33
  clearOutput(path?: string): void;
79
- /**
80
- * Draw a non-filled rectangle on the canvas
81
- * @param options
82
- * @param options.ctx Canvas rendering context
83
- * @param options.x X coordinate of the top-left corner
84
- * @param options.y Y coordinate of the top-left corner
85
- * @param options.width Width of the rectangle
86
- * @param options.height Height of the rectangle
87
- * @param options.lineWidth Line width (default: 2)
88
- * @param options.color Color of the rectangle (default: "blue")
89
- */
90
- drawLine(options: {
91
- ctx: SKRSContext2D;
92
- x: number;
93
- y: number;
94
- width: number;
95
- height: number;
96
- lineWidth?: number;
97
- color?: string;
98
- }): void;
99
- /**
100
- * Draw a contour on the canvas
101
- * @param options
102
- * @param options.ctx Canvas rendering context
103
- * @param options.contour Contour to be drawn
104
- * @param options.strokeStyle Stroke color (default: "red")
105
- * @param options.lineWidth Line width (default: 2)
106
- */
107
- drawContour(options: {
108
- ctx: SKRSContext2D;
109
- contour: cv.Mat;
110
- strokeStyle?: string;
111
- lineWidth?: number;
112
- }): void;
113
34
  }
package/canvas-toolkit.js CHANGED
@@ -1 +1 @@
1
- export class CanvasToolkit{static instance=null;step=0;constructor(){}static getInstance(){if(!CanvasToolkit.instance){CanvasToolkit.instance=new CanvasToolkit}return CanvasToolkit.instance}crop(options){const{bbox,canvas}=options;let croppedCanvas=createCanvas(bbox.x1-bbox.x0,bbox.y1-bbox.y0);let croppedCtx=croppedCanvas.getContext("2d");croppedCtx.drawImage(canvas,bbox.x0,bbox.y0,bbox.x1-bbox.x0,bbox.y1-bbox.y0,0,0,croppedCanvas.width,croppedCanvas.height);return croppedCanvas}isDirty(options){const{canvas,threshold=127.5,majorColorThreshold=0.97}=options;let whiteCount=0;let blackCount=0;let borderlessCanvas=this.crop({bbox:{x0:canvas.width*0.1,y0:canvas.height*0.1,x1:canvas.width*0.9,y1:canvas.height*0.9},canvas});let ctx=borderlessCanvas.getContext("2d");let colorData=ctx.getImageData(0,0,borderlessCanvas.width,borderlessCanvas.height).data;for(let i=0;i<colorData.length;i+=4){let red=colorData[i];let green=colorData[i+1];let blue=colorData[i+2];if(red>=threshold&&green>=threshold&&blue>=threshold){whiteCount++}else{blackCount++}}let majorColorRatio=Math.max(whiteCount,blackCount)/(blackCount+whiteCount);return majorColorRatio<majorColorThreshold}saveImage(options){const{canvas,filename,path="out"}=options;let folderPath=join(process.cwd(),path);if(!existsSync(folderPath)){mkdirSync(folderPath,{recursive:true})}let filePath=join(folderPath,`${this.step++}. ${filename}.png`);let out=createWriteStream(filePath);let buffer=canvas.toBuffer("image/png");return new Promise((res,rej)=>{out.write(buffer,(err)=>{if(err){rej(err)}else{res()}})})}clearOutput(path="out"){let folderPath=join(process.cwd(),path);if(existsSync(folderPath)){let files=readdirSync(folderPath);for(let file of files){if(file===".gitignore")continue;let filePath=join(folderPath,file);unlinkSync(filePath)}}}drawLine(options){const{ctx,x,y,width,height,lineWidth=2,color="blue"}=options;ctx.beginPath();ctx.strokeStyle=color;ctx.lineWidth=lineWidth;ctx.strokeRect(x,y,width,height);ctx.closePath()}drawContour(options){const{ctx,contour,strokeStyle="red",lineWidth=2}=options;let pts=contour.data32S;if(pts.length<4)return;ctx.strokeStyle=strokeStyle;ctx.lineWidth=lineWidth;ctx.beginPath();ctx.moveTo(pts[0],pts[1]);for(let i=2;i<pts.length;i+=2){ctx.lineTo(pts[i],pts[i+1])}ctx.closePath();ctx.stroke()}}import{createCanvas}from"./index.js";import{createWriteStream,existsSync,mkdirSync,readdirSync,unlinkSync}from"fs";import{join}from"path";
1
+ import{CanvasToolkitBase}from"./canvas-toolkit.base.js";import{createWriteStream,existsSync,mkdirSync,readdirSync,unlinkSync}from"fs";import{join}from"path";export class CanvasToolkit extends CanvasToolkitBase{static _nodeInstance=null;constructor(){super()}static getInstance(){if(!CanvasToolkit._nodeInstance){CanvasToolkit._nodeInstance=new CanvasToolkit}return CanvasToolkit._nodeInstance}saveImage(options){const{canvas,filename,path="out"}=options;let folderPath=join(process.cwd(),path);if(!existsSync(folderPath)){mkdirSync(folderPath,{recursive:true})}let filePath=join(folderPath,`${this.step++}. ${filename}.png`);let out=createWriteStream(filePath);let buffer=canvas.toBuffer("image/png");return new Promise((res,rej)=>{out.write(buffer,(err)=>{if(err){rej(err)}else{res()}})})}clearOutput(path="out"){let folderPath=join(process.cwd(),path);if(existsSync(folderPath)){let files=readdirSync(folderPath);for(let file of files){if(file===".gitignore")continue;let filePath=join(folderPath,file);unlinkSync(filePath)}}}}
package/contours.d.ts CHANGED
@@ -1,5 +1,6 @@
1
- import type { BoundingBox, Canvas, Points } from "./index.js";
2
- import { cv } from "./index.js";
1
+ import type { CanvasLike } from "./canvas-factory.js";
2
+ import { cv } from "./cv-provider.js";
3
+ import type { BoundingBox, Points } from "./index.interface.js";
3
4
  export interface ContoursOptions {
4
5
  /** The contour retrieval mode. (cv.RETR_...) */
5
6
  mode: cv.RetrievalModes;
@@ -62,7 +63,7 @@ export declare class Contours {
62
63
  * @returns The four corner points of the contour (topLeft, topRight, bottomLeft, bottomRight) and the bounding box.
63
64
  */
64
65
  getCornerPoints(options: {
65
- canvas: Canvas;
66
+ canvas: CanvasLike;
66
67
  contour?: cv.Mat;
67
68
  }): {
68
69
  points: Points;
package/contours.js CHANGED
@@ -1 +1 @@
1
- export class Contours{contours;constructor(img,options={}){let opts={...defaultOptions(),...options};if(img instanceof cv.Mat){let contours=new cv.MatVector;let hierarchy=new cv.Mat;try{cv.findContours(img,contours,hierarchy,opts.mode,opts.method)}catch(error){throw error}hierarchy.delete();this.contours=contours}else{throw new Error("Invalid img type. Must be cv.Mat.")}}getAll(){return this.contours}getSize(){return this.contours.size()}getFromIndex(index){if(index<this.contours.size()){return this.contours.get(index)}return new cv.Mat}getRect(contour){return cv.boundingRect(contour)}iterate(callback){for(let i=0,len=this.contours.size();i<len;i++){let contour=this.contours.get(i);callback(contour)}return this}getLargestContourArea(){let maxArea=0;let largestContour=null;this.iterate((contour)=>{let area=cv.contourArea(contour);if(area>maxArea){maxArea=area;largestContour=contour}});return largestContour}getCornerPoints(options){const{canvas,contour=this.getLargestContourArea()}=options;let bbox={x0:0,y0:0,x1:canvas.width,y1:canvas.height};if(!contour){return{points:{topLeft:{x:bbox.x0,y:bbox.y0},topRight:{x:bbox.x1,y:bbox.y0},bottomLeft:{x:bbox.x0,y:bbox.y1},bottomRight:{x:bbox.x1,y:bbox.y1}},bbox}}let rect=cv.minAreaRect(contour);let vertices=cv.RotatedRect.points(rect);let points={topLeft:{x:0,y:0},topRight:{x:0,y:0},bottomRight:{x:0,y:0},bottomLeft:{x:0,y:0}};let sums=vertices.map((pt)=>pt.x+pt.y);let diffs=vertices.map((pt)=>pt.y-pt.x);let topLeftIdx=sums.indexOf(Math.min(...sums));let topRightIdx=diffs.indexOf(Math.min(...diffs));let bottomRightIdx=sums.indexOf(Math.max(...sums));let bottomLeftIdx=diffs.indexOf(Math.max(...diffs));if(!vertices[topLeftIdx]||!vertices[topRightIdx]||!vertices[bottomRightIdx]||!vertices[bottomLeftIdx]){return{points:{topLeft:{x:bbox.x0,y:bbox.y0},topRight:{x:bbox.x1,y:bbox.y0},bottomLeft:{x:bbox.x0,y:bbox.y1},bottomRight:{x:bbox.x1,y:bbox.y1}},bbox}}points.topLeft={x:vertices[topLeftIdx].x,y:vertices[topLeftIdx].y};points.topRight={x:vertices[topRightIdx].x,y:vertices[topRightIdx].y};points.bottomRight={x:vertices[bottomRightIdx].x,y:vertices[bottomRightIdx].y};points.bottomLeft={x:vertices[bottomLeftIdx].x,y:vertices[bottomLeftIdx].y};contour.delete();let ensureInBounds=(p)=>{p.x=Math.max(0,Math.min(canvas.width,p.x));p.y=Math.max(0,Math.min(canvas.height,p.y));return p};points.topLeft=ensureInBounds(points.topLeft);points.topRight=ensureInBounds(points.topRight);points.bottomLeft=ensureInBounds(points.bottomLeft);points.bottomRight=ensureInBounds(points.bottomRight);return{points,bbox}}getApproximateRectangleContour(options){const{threshold=0.02,contour=this.getLargestContourArea()}=options??{};if(!contour)return;let epsilon=threshold*cv.arcLength(contour,true);let approxContour=new cv.Mat;cv.approxPolyDP(contour,approxContour,epsilon,true);contour.delete();return approxContour}destroy(){try{this.contours.delete()}catch(error){}}}import{cv}from"./index.js";function defaultOptions(){return{mode:cv.RETR_EXTERNAL,method:cv.CHAIN_APPROX_SIMPLE}}
1
+ export class Contours{contours;constructor(img,options={}){let opts={...defaultOptions(),...options};if(img instanceof cv.Mat){let contours=new cv.MatVector;let hierarchy=new cv.Mat;try{cv.findContours(img,contours,hierarchy,opts.mode,opts.method)}catch(error){throw error}hierarchy.delete();this.contours=contours}else{throw new Error("Invalid img type. Must be cv.Mat.")}}getAll(){return this.contours}getSize(){return this.contours.size()}getFromIndex(index){if(index<this.contours.size()){return this.contours.get(index)}return new cv.Mat}getRect(contour){return cv.boundingRect(contour)}iterate(callback){for(let i=0,len=this.contours.size();i<len;i++){let contour=this.contours.get(i);callback(contour)}return this}getLargestContourArea(){let maxArea=0;let largestContour=null;this.iterate((contour)=>{let area=cv.contourArea(contour);if(area>maxArea){maxArea=area;largestContour=contour}});return largestContour}getCornerPoints(options){const{canvas,contour=this.getLargestContourArea()}=options;let bbox={x0:0,y0:0,x1:canvas.width,y1:canvas.height};if(!contour){return{points:{topLeft:{x:bbox.x0,y:bbox.y0},topRight:{x:bbox.x1,y:bbox.y0},bottomLeft:{x:bbox.x0,y:bbox.y1},bottomRight:{x:bbox.x1,y:bbox.y1}},bbox}}let rect=cv.minAreaRect(contour);let vertices=cv.RotatedRect.points(rect);let points={topLeft:{x:0,y:0},topRight:{x:0,y:0},bottomRight:{x:0,y:0},bottomLeft:{x:0,y:0}};let sums=vertices.map((pt)=>pt.x+pt.y);let diffs=vertices.map((pt)=>pt.y-pt.x);let topLeftIdx=sums.indexOf(Math.min(...sums));let topRightIdx=diffs.indexOf(Math.min(...diffs));let bottomRightIdx=sums.indexOf(Math.max(...sums));let bottomLeftIdx=diffs.indexOf(Math.max(...diffs));if(!vertices[topLeftIdx]||!vertices[topRightIdx]||!vertices[bottomRightIdx]||!vertices[bottomLeftIdx]){return{points:{topLeft:{x:bbox.x0,y:bbox.y0},topRight:{x:bbox.x1,y:bbox.y0},bottomLeft:{x:bbox.x0,y:bbox.y1},bottomRight:{x:bbox.x1,y:bbox.y1}},bbox}}points.topLeft={x:vertices[topLeftIdx].x,y:vertices[topLeftIdx].y};points.topRight={x:vertices[topRightIdx].x,y:vertices[topRightIdx].y};points.bottomRight={x:vertices[bottomRightIdx].x,y:vertices[bottomRightIdx].y};points.bottomLeft={x:vertices[bottomLeftIdx].x,y:vertices[bottomLeftIdx].y};contour.delete();let ensureInBounds=(p)=>{p.x=Math.max(0,Math.min(canvas.width,p.x));p.y=Math.max(0,Math.min(canvas.height,p.y));return p};points.topLeft=ensureInBounds(points.topLeft);points.topRight=ensureInBounds(points.topRight);points.bottomLeft=ensureInBounds(points.bottomLeft);points.bottomRight=ensureInBounds(points.bottomRight);return{points,bbox}}getApproximateRectangleContour(options){const{threshold=0.02,contour=this.getLargestContourArea()}=options??{};if(!contour)return;let epsilon=threshold*cv.arcLength(contour,true);let approxContour=new cv.Mat;cv.approxPolyDP(contour,approxContour,epsilon,true);contour.delete();return approxContour}destroy(){try{this.contours.delete()}catch(error){}}}import{cv}from"./cv-provider.js";function defaultOptions(){return{mode:cv.RETR_EXTERNAL,method:cv.CHAIN_APPROX_SIMPLE}}