ppu-ocv 3.0.0 → 3.1.2

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 CHANGED
@@ -1,10 +1,10 @@
1
1
  # ppu-ocv
2
2
 
3
- A type-safe, modular, chainable image processing library built on top of OpenCV.js with a fluent API leveraging pipeline processing.
3
+ [![NPM](https://img.shields.io/npm/dw/ppu-ocv)](https://www.npmjs.com/package/ppu-ocv) [![JSR](https://jsr.io/badges/@snowfluke/ppu-ocv)](https://jsr.io/@snowfluke/ppu-ocv)
4
4
 
5
- ![ppu-ocv pipeline demo](https://raw.githubusercontent.com/PT-Perkasa-Pilar-Utama/ppu-ocv/refs/heads/main/assets/ppu-ocv-demo.jpg)
5
+ A type-safe, modular, chainable image processing library built on top of OpenCV.js with a fluent API leveraging pipeline processing. Decoupled canvas utilities run anywhere — Node, Bun, browsers, browser extensions, and service workers — with or without OpenCV.
6
6
 
7
- Image manipulation as easy as:
7
+ ![ppu-ocv pipeline demo](https://raw.githubusercontent.com/PT-Perkasa-Pilar-Utama/ppu-ocv/refs/heads/main/assets/ppu-ocv-demo.jpg)
8
8
 
9
9
  ```ts
10
10
  const processor = new ImageProcessor(canvas);
@@ -17,23 +17,33 @@ const result = processor
17
17
  .dilate({ size: [20, 20], iter: 5 })
18
18
  .toCanvas();
19
19
 
20
- // Memory cleanup
21
20
  processor.destroy();
22
21
  ```
23
22
 
24
- This work is based on https://github.com/TechStark/opencv-js.
23
+ Based on [TechStark/opencv-js](https://github.com/TechStark/opencv-js).
25
24
 
26
- ## Why use this library?
25
+ ## Table of Contents
27
26
 
28
- OpenCV is powerful but can be cumbersome to use directly. This library provides:
27
+ - [Why ppu-ocv?](#why-ppu-ocv)
28
+ - [Installation](#installation)
29
+ - [Usage (Node.js / Bun)](#usage-nodejs--bun)
30
+ - [Canvas-only Usage (no OpenCV)](#canvas-only-usage-no-opencv)
31
+ - [Web / Browser Support](#web--browser-support)
32
+ - [Built-in Pipeline Operations](#built-in-pipeline-operations)
33
+ - [Extending Operations](#extending-operations)
34
+ - [Class Documentation](#class-documentation)
35
+ - [Migrating from v2](#migrating-from-v2)
36
+ - [Contributing](#contributing)
37
+ - [License](#license)
29
38
 
30
- 1. **Simplified API**: Transform complex OpenCV calls into simple chainable methods
31
- 2. **Reduced Boilerplate**: No need to manage memory, conversions, or dimensions manually
32
- 3. **Development Speed**: Add image processing to your app in minutes, not hours
33
- 4. **Extensibility**: Custom operations for your specific needs without library modifications
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
39
+ ## Why ppu-ocv?
40
+
41
+ - **Simplified API** chainable methods that hide OpenCV's verbose Mat allocation
42
+ - **No memory management** automatic Mat lifecycle within the pipeline
43
+ - **Type-safe** full TypeScript inference for operations and options
44
+ - **Extensible** register custom operations with `registry.register(...)` without forking
45
+ - **Cross-platform** same API in Node, Bun, browsers, and constrained runtimes
46
+ - **Loosely coupled** — canvas utilities work standalone; OpenCV is only loaded when actually needed
37
47
 
38
48
  ## Installation
39
49
 
@@ -125,9 +135,7 @@ const buffer = await CanvasProcessor.prepareBuffer(cropped);
125
135
  import { CanvasProcessor, CanvasToolkit } from "ppu-ocv/canvas-web";
126
136
 
127
137
  const response = await fetch("/image.jpg");
128
- const canvas = await CanvasProcessor.prepareCanvas(
129
- await response.arrayBuffer(),
130
- );
138
+ const canvas = await CanvasProcessor.prepareCanvas(await response.arrayBuffer());
131
139
  ```
132
140
 
133
141
  ## Web / Browser Support
@@ -171,9 +179,7 @@ processor.destroy();
171
179
  await ImageProcessor.initRuntime();
172
180
 
173
181
  const response = await fetch("/my-image.jpg");
174
- const canvas = await CanvasProcessor.prepareCanvas(
175
- await response.arrayBuffer(),
176
- );
182
+ const canvas = await CanvasProcessor.prepareCanvas(await response.arrayBuffer());
177
183
 
178
184
  const processor = new ImageProcessor(canvas);
179
185
  processor
@@ -251,13 +257,65 @@ See: [How to extend ppu-ocv operations](./docs/how-to-extend-ppu-ocv-operations.
251
257
 
252
258
  #### `CanvasProcessor`
253
259
 
254
- Canvas I/O utilities with **no OpenCV dependency**. Available from all entry points including `ppu-ocv/canvas` and `ppu-ocv/canvas-web`.
260
+ Canvas-native image processing with **no OpenCV dependency**. Available from all entry points including `ppu-ocv/canvas` and `ppu-ocv/canvas-web`. Provides a chainable instance API alongside static I/O helpers.
261
+
262
+ ```ts
263
+ const result = new CanvasProcessor(canvas)
264
+ .resize({ width: 360, height: 640 })
265
+ .grayscale()
266
+ .threshold({ thresh: 127 })
267
+ .invert()
268
+ .border({ size: 10, color: "white" })
269
+ .toCanvas();
270
+
271
+ // Detect connected white regions on a binary image
272
+ const regions = new CanvasProcessor(binaryCanvas).findRegions({
273
+ foreground: "light",
274
+ minArea: 20,
275
+ // thresh: 0 ← use on resized binary images to match OpenCV (any non-zero pixel = foreground)
276
+ // padding: { vertical: 0.4, horizontal: 0.6 } ← expand bbox by fraction of height
277
+ // scale: 1 / resizeRatio ← map coords back to original image space
278
+ });
279
+ regions.sort((a, b) => b.area - a.area); // largest first
280
+ // regions[0] → { bbox: { x0, y0, x1, y1 }, area }
281
+ ```
282
+
283
+ **Static I/O**
255
284
 
256
285
  | Method | Args | Description |
257
286
  | ---------------------- | ----------- | ----------------------------------------------------- |
258
287
  | static `prepareCanvas` | ArrayBuffer | Load image bytes into a `CanvasLike` |
259
288
  | static `prepareBuffer` | CanvasLike | Export a `CanvasLike` to an `ArrayBuffer` (PNG bytes) |
260
289
 
290
+ **Instance operations** (chainable, return `this`)
291
+
292
+ | Method | Options | OpenCV equivalent | Fidelity |
293
+ | ----------- | ---------------------------------- | ------------------------- | -------------- |
294
+ | `resize` | `width`, `height` | `cv.resize` INTER_LINEAR | 1:1 (↓), ≈ (↑) |
295
+ | `grayscale` | — | `COLOR_RGBA2GRAY` | **1:1** |
296
+ | `convert` | `alpha?`, `beta?` | `Mat.convertTo` (α·x + β) | **1:1** |
297
+ | `invert` | — | `cv.bitwise_not` | **1:1** ¹ |
298
+ | `threshold` | `thresh?` (127), `maxValue?` (255) | `THRESH_BINARY` | **1:1** |
299
+ | `border` | `size?` (10), `color?` (CSS) | `BORDER_CONSTANT` | **1:1** |
300
+ | `rotate` | `angle`, `cx?`, `cy?` | `warpAffine` | ≈ (±6 px) ² |
301
+ | `toCanvas` | — | — | — |
302
+
303
+ **Region detection** (returns data, does not mutate)
304
+
305
+ | Method | Options | Description |
306
+ | ------------- | ---------------------------------------------------------------------------------------- | -------------------------------------------------------------- |
307
+ | `findRegions` | `foreground?` (`"light"`), `thresh?` (127), `minArea?`, `maxArea?`, `padding?`, `scale?` | 8-connected flood-fill on a binary canvas → `DetectedRegion[]` |
308
+
309
+ `DetectedRegion` shape: `{ bbox: BoundingBox, area: number }` where `bbox` is `{ x0, y0, x1, y1 }` (x1/y1 exclusive). Equivalent to OpenCV's `findContours(RETR_EXTERNAL) + boundingRect` — all matched bboxes agree within ±1 px on solid binary images. ³
310
+
311
+ **`thresh` option** — pixel value threshold for foreground detection (default `127`). For resized binary images, use `thresh: 0` so anti-aliased border pixels (values 1–127) are included as foreground, matching OpenCV's non-zero threshold. With `thresh: 0` + `padding` + `scale`, full-pipeline IoU vs OpenCV is **98.4%** (all 21/21 boxes matched).
312
+
313
+ > ¹ Canvas `invert` preserves the alpha channel; OpenCV `bitwise_not` also inverts alpha. Results are identical when the source is opaque (alpha=255).
314
+ >
315
+ > ² Canvas uses anti-aliased bilinear interpolation; OpenCV uses plain bilinear. Difference is visually imperceptible and has no impact on OCR quality.
316
+ >
317
+ > ³ `RETR_LIST` may return additional inner-hole contours for white regions that contain dark sub-regions; `findRegions` counts each connected white component once regardless of interior holes.
318
+
261
319
  #### `ImageProcessor`
262
320
 
263
321
  Requires OpenCV. Available from `ppu-ocv` and `ppu-ocv/web`.
@@ -316,37 +374,24 @@ A collection of utility functions for analyzing image properties (requires OpenC
316
374
 
317
375
  ## Contributing
318
376
 
319
- Contributions are welcome! If you would like to contribute, please follow these steps:
320
-
321
- 1. **Fork the Repository:** Create your own fork of the project.
322
- 2. **Create a Feature Branch:** Use a descriptive branch name for your changes.
323
- 3. **Implement Changes:** Make your modifications, add tests, and ensure everything passes.
324
- 4. **Submit a Pull Request:** Open a pull request to discuss your changes and get feedback.
377
+ See [CONTRIBUTING.md](./CONTRIBUTING.md) for the full guide setup, commit conventions, quality checks, and PR flow. Also:
325
378
 
326
- ### Running Tests
379
+ - [Code of Conduct](./CODE_OF_CONDUCT.md) — community standards.
380
+ - [Security policy](./SECURITY.md) — how to report vulnerabilities privately.
381
+ - [Issue tracker](https://github.com/PT-Perkasa-Pilar-Utama/ppu-ocv/issues) — bug reports, feature requests, and docs gaps each have a template.
327
382
 
328
- This project uses Bun for testing. To run the tests locally, execute:
383
+ Quick local commands:
329
384
 
330
385
  ```bash
331
- bun test
386
+ bun install
387
+ bun test # run unit tests
388
+ bun run fmt # check formatting
389
+ bun run lint # check lint
390
+ bun run type-check # tsgo --noEmit
391
+ bun task build # emit ./lib
392
+ bun task bench # micro-bench the operations registry
332
393
  ```
333
394
 
334
- Ensure that all tests pass before submitting your pull request.
335
-
336
- ## Scripts
337
-
338
- Recommended development environment is in a Linux-based environment.
339
-
340
- Library template: https://github.com/aquapi/lib-template
341
-
342
- ### [Build](./scripts/build.ts)
343
-
344
- Emit `.js` and `.d.ts` files to [`lib`](./lib).
345
-
346
- ### [Publish](./scripts/publish.ts)
347
-
348
- Move [`package.json`](./package.json), [`README.md`](./README.md) to [`lib`](./lib) and publish the package.
349
-
350
395
  ## Migrating from v2
351
396
 
352
397
  See [MIGRATION.md](./MIGRATION.md) for a full guide. The short version:
@@ -29,6 +29,12 @@ export interface Context2DLike {
29
29
  strokeRect(x: number, y: number, w: number, h: number): void;
30
30
  strokeStyle: string | CanvasGradient | CanvasPattern;
31
31
  lineWidth: number;
32
+ fillStyle: string | CanvasGradient | CanvasPattern;
33
+ fillRect(x: number, y: number, w: number, h: number): void;
34
+ save(): void;
35
+ restore(): void;
36
+ translate(x: number, y: number): void;
37
+ rotate(angle: number): void;
32
38
  }
33
39
  /** Platform-specific canvas operations */
34
40
  export interface CanvasPlatform {
@@ -1,10 +1,192 @@
1
+ import type { BoundingBox } from "./index.interface.js";
1
2
  import type { CanvasLike } from "./canvas-factory.js";
2
3
  /**
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.
4
+ * A detected region returned by {@link CanvasProcessor.findRegions}.
5
+ */
6
+ export interface DetectedRegion {
7
+ /** Axis-aligned bounding box of the region (x1/y1 are exclusive). */
8
+ bbox: BoundingBox;
9
+ /** Number of foreground pixels in the region. */
10
+ area: number;
11
+ }
12
+ /**
13
+ * Canvas-native image processing with no OpenCV dependency.
14
+ *
15
+ * Provides two distinct APIs:
16
+ * - **Static I/O** (`prepareCanvas`, `prepareBuffer`) — format conversion helpers.
17
+ * - **Chainable instance operations** (`resize`, `grayscale`, `convert`, `invert`,
18
+ * `threshold`, `border`, `rotate`) — lightweight canvas-native pipeline, usable
19
+ * in constrained environments where OpenCV cannot run.
20
+ * - **Region detection** (`findRegions`) — 8-connected flood-fill bbox detection
21
+ * on binary images, with optional padding and coordinate scaling.
22
+ *
23
+ * @example
24
+ * ```ts
25
+ * import { CanvasProcessor } from "ppu-ocv/canvas";
26
+ *
27
+ * const canvas = await CanvasProcessor.prepareCanvas(arrayBuffer);
28
+ *
29
+ * // Pixel pipeline
30
+ * const result = new CanvasProcessor(canvas)
31
+ * .resize({ width: 800, height: 600 })
32
+ * .grayscale()
33
+ * .threshold({ thresh: 127 })
34
+ * .border({ size: 10, color: "white" })
35
+ * .toCanvas();
36
+ *
37
+ * // Region detection (equivalent to findContours + boundingRect)
38
+ * const regions = new CanvasProcessor(binaryCanvas).findRegions({
39
+ * foreground: "light",
40
+ * minArea: 20,
41
+ * padding: { vertical: 0.4, horizontal: 0.6 },
42
+ * scale: originalWidth / processedWidth,
43
+ * });
44
+ * ```
6
45
  */
7
46
  export declare class CanvasProcessor {
47
+ private _canvas;
48
+ constructor(source: CanvasLike);
49
+ get width(): number;
50
+ get height(): number;
51
+ /**
52
+ * Scale the canvas to new dimensions.
53
+ * Uses the platform's native drawImage interpolation (bilinear in most runtimes).
54
+ */
55
+ resize(options: {
56
+ width: number;
57
+ height: number;
58
+ }): this;
59
+ /**
60
+ * Convert to grayscale using BT.601 luma coefficients
61
+ * (matches OpenCV's COLOR_RGBA2GRAY: 0.299R + 0.587G + 0.114B).
62
+ *
63
+ * The result is still RGBA — R, G, and B channels all equal the luma value.
64
+ * The alpha channel is preserved unchanged.
65
+ */
66
+ grayscale(): this;
67
+ /**
68
+ * Apply a linear per-pixel transformation: `dst = clamp(alpha * src + beta)`.
69
+ * Applies independently to R, G, and B channels; alpha channel is unchanged.
70
+ *
71
+ * This is the canvas-native equivalent of OpenCV's `Mat.convertTo(dst, rtype, alpha, beta)`,
72
+ * limited to the pixel-math aspect. `rtype` is not applicable here — canvas ImageData
73
+ * is always 8-bit RGBA (`Uint8ClampedArray`), so type conversion has no meaning.
74
+ *
75
+ * Useful for brightness/contrast adjustment:
76
+ * - `alpha > 1` increases contrast
77
+ * - `beta > 0` increases brightness
78
+ * - `alpha = 0.5, beta = 0` halves contrast
79
+ */
80
+ convert(options?: {
81
+ alpha?: number;
82
+ beta?: number;
83
+ }): this;
84
+ /**
85
+ * Invert all RGB channels: `dst = 255 - src`.
86
+ * Alpha channel is preserved unchanged.
87
+ * Equivalent to OpenCV's `cv.bitwise_not`.
88
+ */
89
+ invert(): this;
90
+ /**
91
+ * Apply binary threshold: pixels with luma above `thresh` become `maxValue`,
92
+ * all others become 0.
93
+ *
94
+ * Equivalent to OpenCV's `cv.threshold(src, dst, thresh, maxval, THRESH_BINARY)`.
95
+ * Operates on the luma of each pixel (R channel is used directly when the
96
+ * image is already grayscale, i.e. R === G === B).
97
+ *
98
+ * Note: Otsu's automatic threshold (`THRESH_OTSU`) is not supported
99
+ * canvas-natively — use a fixed `thresh` value instead.
100
+ */
101
+ threshold(options?: {
102
+ thresh?: number;
103
+ maxValue?: number;
104
+ }): this;
105
+ /**
106
+ * Add a uniform border around the canvas.
107
+ * Equivalent to OpenCV's `cv.copyMakeBorder` with `BORDER_CONSTANT`.
108
+ *
109
+ * @param options.size Border width in pixels (default 10)
110
+ * @param options.color CSS color string for the border fill (default "white")
111
+ */
112
+ border(options?: {
113
+ size?: number;
114
+ color?: string;
115
+ }): this;
116
+ /**
117
+ * Rotate the canvas around its centre (or a custom pivot) while keeping the
118
+ * original canvas dimensions. Pixels that fall outside the bounds after
119
+ * rotation are clipped (transparent).
120
+ *
121
+ * Positive `angle` rotates counter-clockwise, matching the convention used
122
+ * by OpenCV's `getRotationMatrix2D`.
123
+ *
124
+ * @param options.angle Rotation angle in degrees
125
+ * @param options.cx Pivot X (default: canvas centre)
126
+ * @param options.cy Pivot Y (default: canvas centre)
127
+ */
128
+ rotate(options: {
129
+ angle: number;
130
+ cx?: number;
131
+ cy?: number;
132
+ }): this;
133
+ /**
134
+ * Detect connected regions on a binary (black-and-white) canvas and return
135
+ * their bounding boxes and pixel areas.
136
+ *
137
+ * Uses an 8-connected DFS flood-fill — equivalent to OpenCV's
138
+ * `findContours` with `RETR_LIST + CHAIN_APPROX_SIMPLE` on a binary image,
139
+ * returning bounding-box level information.
140
+ *
141
+ * Best called after `.grayscale().threshold()` to ensure a clean binary input.
142
+ *
143
+ * @param options.foreground Which pixel tone is the foreground to detect:
144
+ * `"light"` (white regions, default) or `"dark"` (black regions).
145
+ * @param options.thresh Luma threshold that separates foreground from background (default 127).
146
+ * For `foreground: "light"`: pixel is foreground when `r > thresh`.
147
+ * For `foreground: "dark"`: pixel is foreground when `r <= thresh`.
148
+ * **Use `thresh: 0` when working on a resized binary image** — resizing
149
+ * introduces anti-aliased gray border pixels (values 1–127) that the
150
+ * default threshold would miss, matching OpenCV's behaviour of treating
151
+ * any non-zero pixel as contour-adjacent.
152
+ * @param options.minArea Ignore regions smaller than this many pixels (default 1).
153
+ * @param options.maxArea Ignore regions larger than this many pixels (default unlimited).
154
+ * @param options.padding Expand each detected bbox by a fraction of its **height**.
155
+ * Mirrors `extractBoxesFromContours` padding logic:
156
+ * `vertical` and `horizontal` are both applied as
157
+ * `Math.round(bboxHeight × factor)` and clamped to the canvas bounds.
158
+ * Default: no padding.
159
+ * @param options.scale Multiply all bbox coordinates by this factor after padding.
160
+ * Use `originalWidth / processedWidth` (i.e. `1 / resizeRatio`)
161
+ * to convert from a resized canvas back to original image coordinates.
162
+ * Default: 1 (no scaling).
163
+ *
164
+ * @example
165
+ * ```ts
166
+ * // Direct equivalent of extractBoxesFromContours with default padding:
167
+ * const regions = new CanvasProcessor(binaryCanvas).findRegions({
168
+ * foreground: "light",
169
+ * minArea: 20,
170
+ * padding: { vertical: 0.4, horizontal: 0.6 },
171
+ * scale: originalWidth / processedWidth,
172
+ * });
173
+ * ```
174
+ */
175
+ findRegions(options?: {
176
+ foreground?: "light" | "dark";
177
+ thresh?: number;
178
+ minArea?: number;
179
+ maxArea?: number;
180
+ padding?: {
181
+ vertical?: number;
182
+ horizontal?: number;
183
+ };
184
+ scale?: number;
185
+ }): DetectedRegion[];
186
+ /**
187
+ * Return the current canvas state.
188
+ */
189
+ toCanvas(): CanvasLike;
8
190
  /**
9
191
  * Convert an ArrayBuffer (image file bytes) to a CanvasLike.
10
192
  * If the value is already a CanvasLike it is returned as-is.
@@ -1 +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";
1
+ export class CanvasProcessor{_canvas;constructor(source){this._canvas=source}get width(){return this._canvas.width}get height(){return this._canvas.height}resize(options){const{width,height}=options;let dst=getPlatform().createCanvas(width,height);dst.getContext("2d").drawImage(this._canvas,0,0,width,height);this._canvas=dst;return this}grayscale(){const{width,height}=this._canvas;let imageData=this._canvas.getContext("2d").getImageData(0,0,width,height);let d=imageData.data;for(let i=0;i<d.length;i+=4){let luma=Math.round(0.299*d[i]+0.587*d[i+1]+0.114*d[i+2]);d[i]=luma;d[i+1]=luma;d[i+2]=luma}let dst=getPlatform().createCanvas(width,height);dst.getContext("2d").putImageData(imageData,0,0);this._canvas=dst;return this}convert(options={}){const{alpha=1,beta=0}=options;if(alpha===1&&beta===0)return this;const{width,height}=this._canvas;let imageData=this._canvas.getContext("2d").getImageData(0,0,width,height);let d=imageData.data;for(let i=0;i<d.length;i+=4){d[i]=Math.round(d[i]*alpha+beta);d[i+1]=Math.round(d[i+1]*alpha+beta);d[i+2]=Math.round(d[i+2]*alpha+beta)}let dst=getPlatform().createCanvas(width,height);dst.getContext("2d").putImageData(imageData,0,0);this._canvas=dst;return this}invert(){const{width,height}=this._canvas;let imageData=this._canvas.getContext("2d").getImageData(0,0,width,height);let d=imageData.data;for(let i=0;i<d.length;i+=4){d[i]=255-d[i];d[i+1]=255-d[i+1];d[i+2]=255-d[i+2]}let dst=getPlatform().createCanvas(width,height);dst.getContext("2d").putImageData(imageData,0,0);this._canvas=dst;return this}threshold(options={}){const{thresh=127,maxValue=255}=options;const{width,height}=this._canvas;let imageData=this._canvas.getContext("2d").getImageData(0,0,width,height);let d=imageData.data;for(let i=0;i<d.length;i+=4){let luma=d[i]===d[i+1]&&d[i+1]===d[i+2]?d[i]:Math.round(0.299*d[i]+0.587*d[i+1]+0.114*d[i+2]);let val=luma>thresh?maxValue:0;d[i]=val;d[i+1]=val;d[i+2]=val}let dst=getPlatform().createCanvas(width,height);dst.getContext("2d").putImageData(imageData,0,0);this._canvas=dst;return this}border(options={}){const{size=10,color="white"}=options;const{width,height}=this._canvas;let dst=getPlatform().createCanvas(width+size*2,height+size*2);let ctx=dst.getContext("2d");ctx.fillStyle=color;ctx.fillRect(0,0,dst.width,dst.height);ctx.drawImage(this._canvas,size,size);this._canvas=dst;return this}rotate(options){const{angle,cx=this._canvas.width/2,cy=this._canvas.height/2}=options;if(angle===0)return this;const{width,height}=this._canvas;let dst=getPlatform().createCanvas(width,height);let ctx=dst.getContext("2d");ctx.save();ctx.translate(cx,cy);ctx.rotate(-angle*Math.PI/180);ctx.drawImage(this._canvas,-cx,-cy);ctx.restore();this._canvas=dst;return this}findRegions(options={}){const{foreground="light",thresh=127,minArea=1,maxArea=1/0,padding,scale=1}=options;const{width,height}=this._canvas;let data=this._canvas.getContext("2d").getImageData(0,0,width,height).data;let visited=new Uint8Array(width*height);let regions=[];let neighbours=[[-1,-1],[0,-1],[1,-1],[-1,0],[1,0],[-1,1],[0,1],[1,1]];let isForeground=(pixelIdx)=>{let r=data[pixelIdx];return foreground==="light"?r>thresh:r<=thresh};for(let startY=0;startY<height;startY++){for(let startX=0;startX<width;startX++){let startFlat=startY*width+startX;if(visited[startFlat])continue;visited[startFlat]=1;if(!isForeground(startFlat*4))continue;let stack=[startFlat];let minX=startX,maxX=startX;let minY=startY,maxY=startY;let area=0;while(stack.length>0){let flat=stack.pop();area++;let x=flat%width;let y=(flat-x)/width;if(x<minX)minX=x;else if(x>maxX)maxX=x;if(y<minY)minY=y;else if(y>maxY)maxY=y;for(const[dx,dy]of neighbours){let nx=x+dx;let ny=y+dy;if(nx<0||nx>=width||ny<0||ny>=height)continue;let nFlat=ny*width+nx;if(visited[nFlat])continue;visited[nFlat]=1;if(isForeground(nFlat*4))stack.push(nFlat)}}if(area>=minArea&&area<=maxArea){let x0=minX;let y0=minY;let x1=maxX+1;let y1=maxY+1;if(padding){let bboxH=y1-y0;let vPad=Math.round(bboxH*(padding.vertical??0));let hPad=Math.round(bboxH*(padding.horizontal??0));x0=Math.max(0,x0-hPad);y0=Math.max(0,y0-vPad);x1=Math.min(width,x1+hPad);y1=Math.min(height,y1+vPad)}if(scale!==1){x0=Math.max(0,Math.round(x0*scale));y0=Math.max(0,Math.round(y0*scale));x1=Math.round(x1*scale);y1=Math.round(y1*scale)}regions.push({bbox:{x0,y0,x1,y1},area})}}}return regions}toCanvas(){return this._canvas}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";
@@ -1,6 +1,6 @@
1
1
  export type { BoundingBox, Coordinate, Points } from "./index.interface.js";
2
2
  export { getPlatform, setPlatform } from "./canvas-factory.js";
3
- export type { CanvasLike, CanvasPlatform, Context2DLike, } from "./canvas-factory.js";
3
+ export type { CanvasLike, CanvasPlatform, Context2DLike } from "./canvas-factory.js";
4
4
  export { webPlatform } from "./platform/web.js";
5
5
  export { CanvasToolkitBase as CanvasToolkit, CanvasToolkitBase, type ContourLike, } from "./canvas-toolkit.base.js";
6
- export { CanvasProcessor } from "./canvas-processor.js";
6
+ export { CanvasProcessor, type DetectedRegion } from "./canvas-processor.js";
package/index.canvas.d.ts CHANGED
@@ -2,7 +2,7 @@ export { Canvas, createCanvas, ImageData, loadImage } from "@napi-rs/canvas";
2
2
  export type { SKRSContext2D } from "@napi-rs/canvas";
3
3
  export type { BoundingBox, Coordinate, Points } from "./index.interface.js";
4
4
  export { getPlatform, setPlatform } from "./canvas-factory.js";
5
- export type { CanvasLike, CanvasPlatform, Context2DLike, } from "./canvas-factory.js";
5
+ export type { CanvasLike, CanvasPlatform, Context2DLike } from "./canvas-factory.js";
6
6
  export { CanvasToolkitBase, type ContourLike } from "./canvas-toolkit.base.js";
7
7
  export { CanvasToolkit } from "./canvas-toolkit.js";
8
- export { CanvasProcessor } from "./canvas-processor.js";
8
+ export { CanvasProcessor, type DetectedRegion } from "./canvas-processor.js";
package/index.d.ts CHANGED
@@ -3,12 +3,12 @@ export { cv };
3
3
  export { Canvas, createCanvas, ImageData, loadImage } from "@napi-rs/canvas";
4
4
  export type { SKRSContext2D } from "@napi-rs/canvas";
5
5
  export type { BoundingBox, Coordinate, Points } from "./index.interface.js";
6
- export { executeOperation, OperationRegistry, registry, } from "./pipeline/index.js";
6
+ export { executeOperation, OperationRegistry, registry } from "./pipeline/index.js";
7
7
  export { getPlatform, setPlatform } from "./canvas-factory.js";
8
- export type { CanvasLike, CanvasPlatform, Context2DLike, } from "./canvas-factory.js";
8
+ export type { CanvasLike, CanvasPlatform, Context2DLike } from "./canvas-factory.js";
9
9
  export { CanvasToolkitBase, type ContourLike } from "./canvas-toolkit.base.js";
10
10
  export { CanvasToolkit } from "./canvas-toolkit.js";
11
- export { CanvasProcessor } from "./canvas-processor.js";
11
+ export { CanvasProcessor, type DetectedRegion } from "./canvas-processor.js";
12
12
  export { Contours } from "./contours.js";
13
13
  export { calculateMeanGrayscaleValue, calculateMeanNormalizedLabLightness, type CalculateMeanLightnessOptions, } from "./image-analysis.js";
14
14
  export { ImageProcessor } from "./image-processor.js";
package/index.web.d.ts CHANGED
@@ -1,12 +1,12 @@
1
1
  import { cv } from "./cv-provider.js";
2
2
  export { cv };
3
3
  export { getPlatform, setPlatform } from "./canvas-factory.js";
4
- export type { CanvasLike, CanvasPlatform, Context2DLike, } from "./canvas-factory.js";
4
+ export type { CanvasLike, CanvasPlatform, Context2DLike } from "./canvas-factory.js";
5
5
  export { webPlatform } from "./platform/web.js";
6
6
  export type { BoundingBox, Coordinate, Points } from "./index.interface.js";
7
- export { executeOperation, OperationRegistry, registry, } from "./pipeline/index.js";
7
+ export { executeOperation, OperationRegistry, registry } from "./pipeline/index.js";
8
8
  export { CanvasToolkitBase as CanvasToolkit, CanvasToolkitBase, type ContourLike, } from "./canvas-toolkit.base.js";
9
- export { CanvasProcessor } from "./canvas-processor.js";
9
+ export { CanvasProcessor, type DetectedRegion } from "./canvas-processor.js";
10
10
  export { Contours } from "./contours.js";
11
11
  export { calculateMeanGrayscaleValue, calculateMeanNormalizedLabLightness, type CalculateMeanLightnessOptions, } from "./image-analysis.js";
12
12
  export { ImageProcessor } from "./image-processor.js";
@@ -1,10 +1,5 @@
1
1
  import { cv } from "../cv-provider.js";
2
2
  import type { OperationResult, PartialOptions } from "../pipeline/types.js";
3
- declare module "../pipeline/types" {
4
- interface RegisteredOperations {
5
- adaptiveThreshold: AdaptiveThresholdOptions;
6
- }
7
- }
8
3
  export interface AdaptiveThresholdOptions extends PartialOptions {
9
4
  /** Upper threshold value (0-255) */
10
5
  upper: number;
@@ -1,10 +1,5 @@
1
1
  import { cv } from "../cv-provider.js";
2
2
  import type { OperationResult, PartialOptions } from "../pipeline/types.js";
3
- declare module "../pipeline/types" {
4
- interface RegisteredOperations {
5
- blur: BlurOptions;
6
- }
7
- }
8
3
  export interface BlurOptions extends PartialOptions {
9
4
  /** Size of the blur [x, y] */
10
5
  size: [number, number];
@@ -1,10 +1,5 @@
1
1
  import type { OperationResult, PartialOptions } from "../pipeline/types.js";
2
2
  import { cv } from "../cv-provider.js";
3
- declare module "../pipeline/types" {
4
- interface RegisteredOperations {
5
- border: BorderOptions;
6
- }
7
- }
8
3
  export interface BorderOptions extends PartialOptions {
9
4
  /** Size of the border in pixels */
10
5
  size: number;
@@ -1,10 +1,5 @@
1
1
  import type { OperationResult, PartialOptions } from "../pipeline/types.js";
2
2
  import { cv } from "../cv-provider.js";
3
- declare module "../pipeline/types" {
4
- interface RegisteredOperations {
5
- canny: CannyOptions;
6
- }
7
- }
8
3
  export interface CannyOptions extends PartialOptions {
9
4
  /** Lower threshold for the hysteresis procedure (0-255) */
10
5
  lower: number;
@@ -1,10 +1,5 @@
1
1
  import type { OperationResult, RequiredOptions } from "../pipeline/types.js";
2
2
  import { cv } from "../cv-provider.js";
3
- declare module "../pipeline/types" {
4
- interface RegisteredOperations {
5
- convert: ConvertOptions;
6
- }
7
- }
8
3
  export interface ConvertOptions extends RequiredOptions {
9
4
  /** Desired matrix type (cv.CV_...) if negative, it will be the same as input */
10
5
  rtype: number;
@@ -1,10 +1,5 @@
1
1
  import type { OperationResult, PartialOptions } from "../pipeline/types.js";
2
2
  import { cv } from "../cv-provider.js";
3
- declare module "../pipeline/types" {
4
- interface RegisteredOperations {
5
- dilate: DilateOptions;
6
- }
7
- }
8
3
  export interface DilateOptions extends PartialOptions {
9
4
  /** Size of the block [x, y] */
10
5
  size: [number, number];
@@ -1,10 +1,5 @@
1
1
  import type { OperationResult, PartialOptions } from "../pipeline/types.js";
2
2
  import { cv } from "../cv-provider.js";
3
- declare module "../pipeline/types" {
4
- interface RegisteredOperations {
5
- erode: ErodeOptions;
6
- }
7
- }
8
3
  export interface ErodeOptions extends PartialOptions {
9
4
  /** Size of the block [x, y] */
10
5
  size: [number, number];
@@ -1,10 +1,5 @@
1
1
  import type { OperationResult, PartialOptions } from "../pipeline/types.js";
2
2
  import { cv } from "../cv-provider.js";
3
- declare module "../pipeline/types" {
4
- interface RegisteredOperations {
5
- grayscale: GrayscaleOptions;
6
- }
7
- }
8
3
  export interface GrayscaleOptions extends PartialOptions {
9
4
  }
10
5
  export declare function grayscale(img: cv.Mat, options: GrayscaleOptions): OperationResult;
@@ -1,10 +1,5 @@
1
1
  import type { OperationResult, PartialOptions } from "../pipeline/types.js";
2
2
  import { cv } from "../cv-provider.js";
3
- declare module "../pipeline/types" {
4
- interface RegisteredOperations {
5
- invert: InvertOptions;
6
- }
7
- }
8
3
  export interface InvertOptions extends PartialOptions {
9
4
  }
10
5
  export declare function invert(img: cv.Mat, options: InvertOptions): OperationResult;
@@ -1,10 +1,5 @@
1
1
  import type { OperationResult, PartialOptions } from "../pipeline/types.js";
2
2
  import { cv } from "../cv-provider.js";
3
- declare module "../pipeline/types" {
4
- interface RegisteredOperations {
5
- morphologicalGradient: MorphologicalGradientOptions;
6
- }
7
- }
8
3
  export interface MorphologicalGradientOptions extends PartialOptions {
9
4
  /** Kernel size for the morphological gradient operation [x, y] */
10
5
  size: [number, number];
@@ -1,10 +1,5 @@
1
1
  import type { OperationResult, RequiredOptions } from "../pipeline/types.js";
2
2
  import { cv } from "../cv-provider.js";
3
- declare module "../pipeline/types" {
4
- interface RegisteredOperations {
5
- resize: ResizeOptions;
6
- }
7
- }
8
3
  export interface ResizeOptions extends RequiredOptions {
9
4
  /** Width of the resized image */
10
5
  width: number;
@@ -1,10 +1,5 @@
1
1
  import type { OperationResult, RequiredOptions } from "../pipeline/types.js";
2
2
  import { cv } from "../cv-provider.js";
3
- declare module "../pipeline/types" {
4
- interface RegisteredOperations {
5
- rotate: RotateOptions;
6
- }
7
- }
8
3
  export interface RotateOptions extends RequiredOptions {
9
4
  /** Angle of rotation in degrees (positive for counter-clockwise) */
10
5
  angle: number;
@@ -1,10 +1,5 @@
1
1
  import type { OperationResult, PartialOptions } from "../pipeline/types.js";
2
2
  import { cv } from "../cv-provider.js";
3
- declare module "../pipeline/types" {
4
- interface RegisteredOperations {
5
- threshold: ThresholdOptions;
6
- }
7
- }
8
3
  export interface ThresholdOptions extends PartialOptions {
9
4
  /** Lower threshold value (0-255) */
10
5
  lower: number;
@@ -1,11 +1,6 @@
1
1
  import { cv } from "../cv-provider.js";
2
2
  import type { BoundingBox, Points } from "../index.interface.js";
3
3
  import type { OperationResult, RequiredOptions } from "../pipeline/types.js";
4
- declare module "../pipeline/types" {
5
- interface RegisteredOperations {
6
- warp: WarpOptions;
7
- }
8
- }
9
4
  export interface WarpOptions extends RequiredOptions {
10
5
  /** Four points of the source image containing x and y point in
11
6
  * topLeft, topRight, bottomLeft and BottomRight.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ppu-ocv",
3
- "version": "3.0.0",
3
+ "version": "3.1.2",
4
4
  "description": "A type-safe, modular, chainable image processing library built on top of OpenCV.js with a fluent API leveraging pipeline processing.",
5
5
  "keywords": [
6
6
  "open-cv",
@@ -45,21 +45,29 @@
45
45
  },
46
46
  "scripts": {
47
47
  "task": "bun scripts/task.ts",
48
+ "build": "bun task build",
48
49
  "build:test": "bun task build && bun test",
49
50
  "build:publish": "bun task build && bun task report-size && bun task publish",
50
- "lint": "prettier --check ./src",
51
- "lint:fix": "prettier --write ./src",
52
- "demo": "bunx -y serve -l 4567 ."
51
+ "type-check": "bunx tsgo --noEmit",
52
+ "test": "bun test",
53
+ "lint": "oxlint",
54
+ "lint:fix": "oxlint --fix",
55
+ "fmt": "oxfmt --check",
56
+ "fmt:fix": "oxfmt .",
57
+ "demo": "bunx -y serve -l 4567 .",
58
+ "prepare": "husky"
53
59
  },
54
60
  "devDependencies": {
55
- "@stylistic/eslint-plugin": "latest",
56
61
  "@types/bun": "latest",
57
62
  "@types/uglify-js": "latest",
63
+ "@typescript/native-preview": "^7.0.0-dev.20260513.1",
64
+ "husky": "^9.1.7",
65
+ "lint-staged": "^16.0.0",
58
66
  "mitata": "latest",
59
- "prettier": "^3.8.1",
67
+ "oxfmt": "^0.48.0",
68
+ "oxlint": "^1.63.0",
60
69
  "tsx": "latest",
61
70
  "typescript": "latest",
62
- "typescript-eslint": "latest",
63
71
  "uglify-js": ">=2.4.24"
64
72
  },
65
73
  "repository": {
@@ -1,4 +1,4 @@
1
- import { cv } from "../cv-provider.js";
1
+ import type { cv } from "../cv-provider.js";
2
2
  import type { OperationFunction, OperationName, OperationOptions, OperationResult } from "./index.js";
3
3
  export declare class OperationRegistry {
4
4
  private operations;
@@ -1,4 +1,18 @@
1
- import { cv } from "../cv-provider.js";
1
+ import type { cv } from "../cv-provider.js";
2
+ import type { AdaptiveThresholdOptions } from "../operations/adaptive-threshold.js";
3
+ import type { BlurOptions } from "../operations/blur.js";
4
+ import type { BorderOptions } from "../operations/border.js";
5
+ import type { CannyOptions } from "../operations/canny.js";
6
+ import type { ConvertOptions } from "../operations/convert.js";
7
+ import type { DilateOptions } from "../operations/dilate.js";
8
+ import type { ErodeOptions } from "../operations/erode.js";
9
+ import type { GrayscaleOptions } from "../operations/grayscale.js";
10
+ import type { InvertOptions } from "../operations/invert.js";
11
+ import type { MorphologicalGradientOptions } from "../operations/morphological-gradient.js";
12
+ import type { ResizeOptions } from "../operations/resize.js";
13
+ import type { RotateOptions } from "../operations/rotate.js";
14
+ import type { ThresholdOptions } from "../operations/threshold.js";
15
+ import type { WarpOptions } from "../operations/warp.js";
2
16
  export interface OperationResult {
3
17
  img: cv.Mat;
4
18
  width: number;
@@ -14,11 +28,32 @@ export interface PartialOptions {
14
28
  }
15
29
  export type OperationFunction<T> = (img: cv.Mat, options: T) => OperationResult;
16
30
  /**
17
- * @description
18
- * Central registry mapping operation names to their specific option types.
19
- * Operation modules MUST augment this interface.
31
+ * Central registry mapping operation names to their option types. Each entry
32
+ * is the options type exported by the corresponding `src/operations/*.ts`
33
+ * file. Adding a new operation requires three changes: create the file,
34
+ * export the Options type, and add the entry below.
35
+ *
36
+ * Previously this used `declare module` augmentation so each operation file
37
+ * could register itself. JSR rejects that pattern because it modifies global
38
+ * types, so the registry is now explicit. Consumers can still extend this
39
+ * interface from their own code via `declare module "ppu-ocv"` — that's why
40
+ * it stays an interface rather than a type alias.
20
41
  */
21
42
  export interface RegisteredOperations {
43
+ adaptiveThreshold: AdaptiveThresholdOptions;
44
+ blur: BlurOptions;
45
+ border: BorderOptions;
46
+ canny: CannyOptions;
47
+ convert: ConvertOptions;
48
+ dilate: DilateOptions;
49
+ erode: ErodeOptions;
50
+ grayscale: GrayscaleOptions;
51
+ invert: InvertOptions;
52
+ morphologicalGradient: MorphologicalGradientOptions;
53
+ resize: ResizeOptions;
54
+ rotate: RotateOptions;
55
+ threshold: ThresholdOptions;
56
+ warp: WarpOptions;
22
57
  }
23
58
  export type OperationName = keyof RegisteredOperations;
24
59
  export type OperationOptions<N extends OperationName> = RegisteredOperations[N];