ppu-ocv 1.7.0 → 2.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 (54) hide show
  1. package/README.md +77 -9
  2. package/canvas-factory.d.ts +42 -0
  3. package/canvas-factory.js +1 -0
  4. package/canvas-toolkit.base.d.ts +49 -0
  5. package/canvas-toolkit.base.js +1 -0
  6. package/canvas-toolkit.d.ts +8 -87
  7. package/canvas-toolkit.js +1 -1
  8. package/contours.d.ts +4 -3
  9. package/contours.js +1 -1
  10. package/cv-provider.d.ts +45 -0
  11. package/cv-provider.js +1 -0
  12. package/image-analysis.d.ts +3 -3
  13. package/image-analysis.js +1 -1
  14. package/image-processor.d.ts +53 -53
  15. package/image-processor.js +1 -1
  16. package/index.d.ts +4 -1
  17. package/index.js +1 -1
  18. package/index.web.d.ts +12 -0
  19. package/index.web.js +1 -0
  20. package/operations/adaptive-threshold.d.ts +3 -3
  21. package/operations/adaptive-threshold.js +1 -1
  22. package/operations/blur.d.ts +3 -3
  23. package/operations/blur.js +1 -1
  24. package/operations/border.d.ts +3 -3
  25. package/operations/border.js +1 -1
  26. package/operations/canny.d.ts +3 -3
  27. package/operations/canny.js +1 -1
  28. package/operations/convert.d.ts +3 -3
  29. package/operations/convert.js +1 -1
  30. package/operations/dilate.d.ts +3 -3
  31. package/operations/dilate.js +1 -1
  32. package/operations/erode.d.ts +3 -3
  33. package/operations/erode.js +1 -1
  34. package/operations/grayscale.d.ts +3 -3
  35. package/operations/grayscale.js +1 -1
  36. package/operations/invert.d.ts +3 -3
  37. package/operations/invert.js +1 -1
  38. package/operations/morphological-gradient.d.ts +3 -3
  39. package/operations/morphological-gradient.js +1 -1
  40. package/operations/resize.d.ts +3 -3
  41. package/operations/resize.js +1 -1
  42. package/operations/rotate.d.ts +3 -3
  43. package/operations/rotate.js +1 -1
  44. package/operations/threshold.d.ts +3 -3
  45. package/operations/threshold.js +1 -1
  46. package/operations/warp.d.ts +4 -3
  47. package/operations/warp.js +1 -1
  48. package/package.json +17 -7
  49. package/pipeline/registry.d.ts +1 -1
  50. package/pipeline/types.d.ts +1 -1
  51. package/platform/node.d.ts +3 -0
  52. package/platform/node.js +1 -0
  53. package/platform/web.d.ts +3 -0
  54. 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 })
@@ -43,13 +43,7 @@ yarn add ppu-ocv
43
43
  bun add ppu-ocv
44
44
  ```
45
45
 
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.
51
-
52
- ## Usage
46
+ ## Usage (Node.js / Bun)
53
47
 
54
48
  Note that Operation order is matter, you should atleast know some basic in using OpenCV. See the operations table below this.
55
49
 
@@ -63,7 +57,7 @@ const canvas = await ImageProcessor.prepareCanvas(image);
63
57
  await ImageProcessor.initRuntime();
64
58
 
65
59
  const processor = new ImageProcessor(canvas);
66
- processor.grayscale().blur({ size: 5 }).threshold();
60
+ processor.grayscale().blur({ size: [5, 5] }).threshold();
67
61
 
68
62
  const resultCanvas = processor.toCanvas();
69
63
  processor.destroy();
@@ -101,6 +95,80 @@ await canvasToolkit.saveImage({
101
95
 
102
96
  For more advanced usage, see: [Example usage of ppu-ocv](./examples)
103
97
 
98
+ ## Web / Browser Support
99
+
100
+ Starting from v2.0.0, ppu-ocv supports running in the browser. Import from `ppu-ocv/web` instead of `ppu-ocv` to use the browser-native canvas APIs (`HTMLCanvasElement` / `OffscreenCanvas`) instead of `@napi-rs/canvas`.
101
+
102
+ ### With a bundler (Vite, webpack, etc.)
103
+
104
+ ```ts
105
+ import { ImageProcessor, cv } from "ppu-ocv/web";
106
+
107
+ await ImageProcessor.initRuntime();
108
+
109
+ // From a file input or fetch
110
+ const response = await fetch("/my-image.jpg");
111
+ const buffer = await response.arrayBuffer();
112
+
113
+ const canvas = await ImageProcessor.prepareCanvas(buffer);
114
+ const processor = new ImageProcessor(canvas);
115
+
116
+ processor.grayscale().blur({ size: [5, 5] }).threshold();
117
+
118
+ const result = processor.toCanvas(); // returns HTMLCanvasElement
119
+ document.body.appendChild(result);
120
+
121
+ processor.destroy();
122
+ ```
123
+
124
+ ### Vanilla HTML (no bundler)
125
+
126
+ `initRuntime()` automatically loads `@techstark/opencv-js` from the npm CDN if it's not already available. No extra script tags or import maps needed:
127
+
128
+ ```html
129
+ <script type="module">
130
+ import { ImageProcessor } from "https://cdn.jsdelivr.net/npm/ppu-ocv@2/index.web.js";
131
+ await ImageProcessor.initRuntime();
132
+
133
+ const processor = new ImageProcessor(canvas);
134
+ processor.grayscale().blur({ size: [5, 5] }).threshold();
135
+
136
+ const result = processor.toCanvas();
137
+ processor.destroy();
138
+ </script>
139
+ ```
140
+
141
+ > **Note:** ES modules require HTTP/HTTPS — use a local server (`npx serve .`) for dev, or deploy to GitHub Pages.
142
+
143
+ See the [interactive demo](./index.html) for a full working example.
144
+
145
+ ### Node vs Web differences
146
+
147
+ | Feature | `ppu-ocv` (Node) | `ppu-ocv/web` (Browser) |
148
+ | --- | --- | --- |
149
+ | Canvas backend | `@napi-rs/canvas` | `HTMLCanvasElement` / `OffscreenCanvas` |
150
+ | `CanvasToolkit` | Full (includes `saveImage`, `clearOutput`) | Base only (`crop`, `isDirty`, `drawLine`, `drawContour`) |
151
+ | File I/O | ✅ `saveImage`, `clearOutput` | ❌ Not available (use download links or `toDataURL`) |
152
+ | `ImageProcessor` | ✅ | ✅ Same API |
153
+ | `Contours` | ✅ | ✅ Same API |
154
+ | Image analysis | ✅ | ✅ Same API |
155
+
156
+ ### Platform abstraction
157
+
158
+ Under the hood, ppu-ocv uses a platform abstraction layer. The `ppu-ocv` entry point auto-registers the Node platform, and `ppu-ocv/web` auto-registers the browser platform. You can also set a custom platform:
159
+
160
+ ```ts
161
+ import { setPlatform, type CanvasPlatform } from "ppu-ocv/web";
162
+
163
+ const myPlatform: CanvasPlatform = {
164
+ createCanvas(width, height) { /* ... */ },
165
+ loadImage(source) { /* ... */ },
166
+ isCanvas(value) { /* ... */ },
167
+ };
168
+
169
+ setPlatform(myPlatform);
170
+ ```
171
+
104
172
  ## Built-in pipeline operations
105
173
 
106
174
  To avoid bloat, we only ship essential operations for chaining. Currently shipped operations are:
@@ -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) or "ppu-ocv/web" (browser) to auto-register.')}return _platform}
@@ -0,0 +1,49 @@
1
+ import type { BoundingBox } from "./index.interface.js";
2
+ import type { CanvasLike, Context2DLike } from "./canvas-factory.js";
3
+ import { cv } from "./cv-provider.js";
4
+ /**
5
+ * Cross-platform base class for canvas manipulation utilities.
6
+ * Contains only methods that work in both Node and browser environments.
7
+ */
8
+ export declare class CanvasToolkitBase {
9
+ protected static _baseInstance: CanvasToolkitBase | null;
10
+ protected step: number;
11
+ protected constructor();
12
+ static getInstance(): CanvasToolkitBase;
13
+ /**
14
+ * Crop a part of source canvas and return a new canvas of the cropped part
15
+ */
16
+ crop(options: {
17
+ bbox: BoundingBox;
18
+ canvas: CanvasLike;
19
+ }): CanvasLike;
20
+ /**
21
+ * Check whether a binary canvas is dirty (full of major color either black or white) or not
22
+ */
23
+ isDirty(options: {
24
+ canvas: CanvasLike;
25
+ threshold?: number;
26
+ majorColorThreshold?: number;
27
+ }): boolean;
28
+ /**
29
+ * Draw a non-filled rectangle on the canvas
30
+ */
31
+ drawLine(options: {
32
+ ctx: Context2DLike;
33
+ x: number;
34
+ y: number;
35
+ width: number;
36
+ height: number;
37
+ lineWidth?: number;
38
+ color?: string;
39
+ }): void;
40
+ /**
41
+ * Draw a contour on the canvas
42
+ */
43
+ drawContour(options: {
44
+ ctx: Context2DLike;
45
+ contour: cv.Mat;
46
+ strokeStyle?: string;
47
+ lineWidth?: number;
48
+ }): void;
49
+ }
@@ -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}}
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Lazy OpenCV accessor.
3
+ *
4
+ * In Node (with @techstark/opencv-js installed), `cv` is available after
5
+ * `import cv from "@techstark/opencv-js"`.
6
+ *
7
+ * In the browser, `cv` is set on `globalThis` after OpenCV.js is loaded
8
+ * (either via a <script> tag or dynamically by `initRuntime()`).
9
+ *
10
+ * This module re-exports `cv` as a lazy proxy so that static `import`
11
+ * resolution does NOT require `@techstark/opencv-js` to be present
12
+ * as a resolvable bare specifier at module-load time.
13
+ */
14
+ import type _cvType from "@techstark/opencv-js";
15
+ type CV = typeof _cvType;
16
+ /**
17
+ * Set the cv instance (called by platform entry points).
18
+ */
19
+ export declare function setCv(instance: CV): void;
20
+ /**
21
+ * TypeScript Declaration Merging:
22
+ * By exporting both a `namespace cv` and a `const cv`, consumers importing `{ cv }`
23
+ * get BOTH the types (e.g. `cv.Mat`) AND the runtime Proxy object.
24
+ */
25
+ export declare namespace cv {
26
+ type Mat = _cvType.Mat;
27
+ type MatVector = _cvType.MatVector;
28
+ type Point = _cvType.Point;
29
+ type Rect = _cvType.Rect;
30
+ type Size = _cvType.Size;
31
+ type Scalar = _cvType.Scalar;
32
+ type AdaptiveThresholdTypes = _cvType.AdaptiveThresholdTypes;
33
+ type ThresholdTypes = _cvType.ThresholdTypes;
34
+ type LineTypes = _cvType.LineTypes;
35
+ type RetrievalModes = _cvType.RetrievalModes;
36
+ type ContourApproximationModes = _cvType.ContourApproximationModes;
37
+ type BorderTypes = _cvType.BorderTypes;
38
+ type InterpolationFlags = _cvType.InterpolationFlags;
39
+ type ColorConversionCodes = _cvType.ColorConversionCodes;
40
+ type MorphShapes = _cvType.MorphShapes;
41
+ type MorphTypes = _cvType.MorphTypes;
42
+ type int = number;
43
+ }
44
+ export declare const cv: CV;
45
+ export {};
package/cv-provider.js ADDED
@@ -0,0 +1 @@
1
+ let _cv=null;function getCv(){if(_cv)return _cv;if(typeof globalThis!=="undefined"&&globalThis.cv){_cv=globalThis.cv||null;return _cv}throw new Error("OpenCV is not loaded. Call ImageProcessor.initRuntime() first.")}export function setCv(instance){_cv=instance;if(typeof globalThis!=="undefined"){globalThis.cv=instance}}export let cv=new Proxy({},{get(_target,prop){if(prop===Symbol.toPrimitive||prop===Symbol.toStringTag){return}return getCv()[prop]},set(_target,prop,value){getCv()[prop]=value;return true},has(_target,prop){try{return prop in getCv()}catch{return false}}});
@@ -3,13 +3,13 @@
3
3
  * !IMPORTANT: Ensure ImageProcessor.initRuntime() has been called successfully
4
4
  * once before using any functions from this module.
5
5
  */
6
- import type { Canvas } from "./index.js";
6
+ import type { CanvasLike } from "./canvas-factory.js";
7
7
  /**
8
8
  * Options for calculating mean Lab lightness.
9
9
  */
10
10
  export interface CalculateMeanLightnessOptions {
11
11
  /** The canvas containing the image to be processed. */
12
- canvas: Canvas;
12
+ canvas: CanvasLike;
13
13
  /** The target dimensions for analysis (resizes internally). */
14
14
  dimension: {
15
15
  width: number;
@@ -32,4 +32,4 @@ export declare function calculateMeanNormalizedLabLightness(options: CalculateMe
32
32
  * @returns Mean grayscale value (typically 0-255).
33
33
  * @throws Error if OpenCV operations fail.
34
34
  */
35
- export declare function calculateMeanGrayscaleValue(canvas: Canvas): number;
35
+ export declare function calculateMeanGrayscaleValue(canvas: CanvasLike): number;
package/image-analysis.js CHANGED
@@ -1 +1 @@
1
- import{ImageProcessor,cv}from"./index.js";export function calculateMeanNormalizedLabLightness(options){const{canvas,dimension}=options;let processor=null;let resized=null;let labImg=null;let channels=null;let L=null;let mask=null;let scalarMat=null;try{processor=new ImageProcessor(canvas);resized=processor.execute("resize",{width:dimension.width,height:dimension.height}).toMat();labImg=new cv.Mat;cv.cvtColor(resized,labImg,cv.COLOR_BGR2Lab);channels=new cv.MatVector;cv.split(labImg,channels);L=channels.get(0);mask=new cv.Mat;let maxLocResult=cv.minMaxLoc(L,mask);let maxPixelValue=maxLocResult.maxVal;if(maxPixelValue===0){return 0}scalarMat=new cv.Mat(L.rows,L.cols,L.type(),new cv.Scalar(maxPixelValue));cv.divide(L,scalarMat,L,1,-1);let meanL=cv.mean(L)[0];return meanL??0}finally{processor?.destroy();labImg?.delete();channels?.delete();L?.delete();mask?.delete();scalarMat?.delete()}}export function calculateMeanGrayscaleValue(canvas){let processor=null;let grayscaleImg=null;try{processor=new ImageProcessor(canvas);grayscaleImg=processor.blur().grayscale().toMat();let mean=cv.mean(grayscaleImg)[0];return mean??0}finally{processor?.destroy()}}
1
+ import{cv}from"./cv-provider.js";import{ImageProcessor}from"./image-processor.js";export function calculateMeanNormalizedLabLightness(options){const{canvas,dimension}=options;let processor=null;let resized=null;let labImg=null;let channels=null;let L=null;let mask=null;let scalarMat=null;try{processor=new ImageProcessor(canvas);resized=processor.execute("resize",{width:dimension.width,height:dimension.height}).toMat();labImg=new cv.Mat;cv.cvtColor(resized,labImg,cv.COLOR_BGR2Lab);channels=new cv.MatVector;cv.split(labImg,channels);L=channels.get(0);mask=new cv.Mat;let maxLocResult=cv.minMaxLoc(L,mask);let maxPixelValue=maxLocResult.maxVal;if(maxPixelValue===0){return 0}scalarMat=new cv.Mat(L.rows,L.cols,L.type(),new cv.Scalar(maxPixelValue));cv.divide(L,scalarMat,L,1,-1);let meanL=cv.mean(L)[0];return meanL??0}finally{processor?.destroy();labImg?.delete();channels?.delete();L?.delete();mask?.delete();scalarMat?.delete()}}export function calculateMeanGrayscaleValue(canvas){let processor=null;let grayscaleImg=null;try{processor=new ImageProcessor(canvas);grayscaleImg=processor.blur().grayscale().toMat();let mean=cv.mean(grayscaleImg)[0];return mean??0}finally{processor?.destroy()}}
@@ -1,6 +1,6 @@
1
- import { Canvas, cv } from "./index.js";
2
- import type { AdaptiveThresholdOptions, BlurOptions, BorderOptions, CannyOptions, DilateOptions, ErodeOptions, GrayscaleOptions, InvertOptions, MorphologicalGradientOptions, OperationName, OperationOptions, RequiredOptions, ResizeOptions, RotateOptions, ThresholdOptions, WarpOptions } from "./index.js";
3
- import type { ConvertOptions } from "./pipeline/index.js";
1
+ import type { CanvasLike } from "./canvas-factory.js";
2
+ import { cv } from "./cv-provider.js";
3
+ import type { AdaptiveThresholdOptions, BlurOptions, BorderOptions, CannyOptions, ConvertOptions, DilateOptions, ErodeOptions, GrayscaleOptions, InvertOptions, MorphologicalGradientOptions, OperationName, OperationOptions, RequiredOptions, ResizeOptions, RotateOptions, ThresholdOptions, WarpOptions } from "./pipeline/index.js";
4
4
  type NameWithRequiredOptions = {
5
5
  [N in OperationName]: OperationOptions<N> extends RequiredOptions ? N : never;
6
6
  }[OperationName];
@@ -11,19 +11,23 @@ export declare class ImageProcessor {
11
11
  height: number;
12
12
  /**
13
13
  * Create an ImageProcessor instance from a Canvas or cv.Mat
14
- * @param source Source image as Canvas or cv.Mat
14
+ * @param source Source image as CanvasLike or cv.Mat
15
15
  */
16
- constructor(source: Canvas | cv.Mat);
16
+ constructor(source: CanvasLike | cv.Mat);
17
17
  /**
18
18
  * Convert array buffer to canvas
19
19
  */
20
- static prepareCanvas(file: ArrayBuffer): Promise<Canvas>;
20
+ static prepareCanvas(file: ArrayBuffer): Promise<CanvasLike>;
21
21
  /**
22
22
  * Convert canvas to array buffer
23
23
  */
24
- static prepareBuffer(canvas: Canvas): Promise<ArrayBuffer>;
24
+ static prepareBuffer(canvas: CanvasLike): Promise<ArrayBuffer>;
25
25
  /**
26
- * Initialize OpenCV runtime, this is recommended to be called before any image processing
26
+ * Initialize OpenCV runtime. Must be called before any image processing.
27
+ *
28
+ * - **Node.js**: Uses `@techstark/opencv-js` from node_modules (loaded by entry point).
29
+ * - **Browser with bundler**: Resolves `@techstark/opencv-js` via the bundler.
30
+ * - **Browser without bundler**: Falls back to loading `@techstark/opencv-js` from npm CDN.
27
31
  */
28
32
  static initRuntime(): Promise<void>;
29
33
  /**
@@ -31,47 +35,43 @@ export declare class ImageProcessor {
31
35
  * @param operationName Name of the operation (e.g., "resize")
32
36
  * @param options Required options for the operation
33
37
  */
34
- execute<Name extends NameWithRequiredOptions>(operationName: Name, options: OperationOptions<Name>): this;
38
+ execute<N extends NameWithRequiredOptions>(operationName: N, options: OperationOptions<N>): this;
35
39
  /**
36
- * Execute a registered pipeline operation where options are optional or have defaults.
37
- * @param operationName Name of the operation (e.g., "blur", "grayscale")
38
- * @param options Optional or partial options for the operation
40
+ * Execute a registered pipeline operation that has default options.
41
+ * @param operationName Name of the operation (e.g., "blur")
42
+ * @param options Optional override of the default options
39
43
  */
40
- execute<Name extends NameWithOptionalOptions>(operationName: Name, options?: Partial<OperationOptions<Name>>): this;
44
+ execute<N extends NameWithOptionalOptions>(operationName: N, options?: Partial<OperationOptions<N>>): this;
41
45
  /**
42
46
  * Convert image to grayscale
43
47
  * @description Usage order: independent
44
48
  * @param options Optional configuration for grayscale conversion
45
49
  */
46
50
  grayscale(options?: Partial<GrayscaleOptions>): this;
47
- /**
48
- * Invert image colors
49
- * @description Usage order: ideally (after) threshold or adaptiveThreshold
50
- * @param options Optional configuration for inversion
51
- */
52
- invert(options?: Partial<InvertOptions>): this;
53
- /**
54
- * Add border to image
55
- * @description Usage order: independent
56
- * @param options Border configuration options
57
- */
58
- border(options?: Partial<BorderOptions>): this;
59
51
  /**
60
52
  * Bluring image to reduce noise using Gaussian Blur
61
53
  * @description Usage order: (ideally after) grayscale
62
54
  * @param options Blur configuration options
63
55
  */
64
56
  blur(options?: Partial<BlurOptions>): this;
65
- /** Thresholding to convert image to binary
57
+ /**
58
+ * Thresholding to convert image to binary
66
59
  * @description Usage order: (after) grayscale (and optionally blur)
67
60
  * @param options Thresholding configuration options
68
61
  */
69
62
  threshold(options?: Partial<ThresholdOptions>): this;
70
- /** Adaptive thresholding to convert image to binary
63
+ /**
64
+ * Adaptive thresholding to convert image to binary
71
65
  * @description Usage order: (after) grayscale (and optionally blur)
72
66
  * @param options Adaptive thresholding configuration options
73
67
  */
74
68
  adaptiveThreshold(options?: Partial<AdaptiveThresholdOptions>): this;
69
+ /**
70
+ * Invert image colors
71
+ * @description Usage order: ideally (after) threshold or adaptiveThreshold
72
+ * @param options Optional configuration for inversion
73
+ */
74
+ invert(options?: Partial<InvertOptions>): this;
75
75
  /**
76
76
  * Canny edge detection to detect edges in the image
77
77
  * @description Usage order: (after) grayscale + blur
@@ -79,11 +79,11 @@ export declare class ImageProcessor {
79
79
  */
80
80
  canny(options?: Partial<CannyOptions>): this;
81
81
  /**
82
- * Morphological gradient to highlight the edges in the image
83
- * @description Usage order: (after) dilation + erosion (or threshold)
84
- * @param options Morphological gradient configuration options
82
+ * Dilate image to increase the size of the foreground object
83
+ * @description Usage order: (after) threshold or edge detection
84
+ * @param options Dilation configuration options
85
85
  */
86
- morphologicalGradient(options?: Partial<MorphologicalGradientOptions>): this;
86
+ dilate(options?: Partial<DilateOptions>): this;
87
87
  /**
88
88
  * Erode image to reduce noise
89
89
  * @description Usage order: (after) threshold or edge detection
@@ -91,29 +91,29 @@ export declare class ImageProcessor {
91
91
  */
92
92
  erode(options?: Partial<ErodeOptions>): this;
93
93
  /**
94
- * Dilate image to increase the size of the foreground object
95
- * @description Usage order: (after) threshold or edge detection
96
- * @param options Dilation configuration options
94
+ * Add border to image
95
+ * @description Usage order: independent
96
+ * @param options Border configuration options
97
97
  */
98
- dilate(options?: Partial<DilateOptions>): this;
98
+ border(options?: Partial<BorderOptions>): this;
99
99
  /**
100
100
  * Resize image to a new width and height
101
101
  * @description Usage order: independent
102
- * @param options Resize configuration options
102
+ * @param options Resize configuration options
103
103
  */
104
104
  resize(options: ResizeOptions): this;
105
- /**
106
- * Warp image to a new perspective
107
- * @description Usage order: independent
108
- * @param options Warp configuration options
109
- */
110
- warp(options: WarpOptions): this;
111
105
  /**
112
106
  * Rotate image by a given angle
113
107
  * @description Usage order: independent
114
108
  * @param options Rotate configuration options
115
109
  */
116
110
  rotate(options: RotateOptions): this;
111
+ /**
112
+ * Warp image to a new perspective
113
+ * @description Usage order: independent
114
+ * @param options Warp configuration options
115
+ */
116
+ warp(options: WarpOptions): this;
117
117
  /**
118
118
  * Convert image matrix into new matrix type
119
119
  * @description Usage order: independent
@@ -121,22 +121,22 @@ export declare class ImageProcessor {
121
121
  */
122
122
  convert(options: ConvertOptions): this;
123
123
  /**
124
- * Destroy the image (cv.Mat) stored in image processor state
125
- * @kind non-chainable
126
- * @returns void
124
+ * Morphological gradient to highlight the edges in the image
125
+ * @description Usage order: (after) dilation + erosion (or threshold)
126
+ * @param options Morphological gradient configuration options
127
127
  */
128
- destroy(): void;
128
+ morphologicalGradient(options?: Partial<MorphologicalGradientOptions>): this;
129
129
  /**
130
- * Convert image to cv.Mat
131
- * @kind non-chainable
132
- * @returns cv.Mat
130
+ * Get the result as a cv.Mat
133
131
  */
134
132
  toMat(): cv.Mat;
135
133
  /**
136
- * Convert image (cv.Mat) to Canvas
137
- * @kind non-chainable
138
- * @returns Canvas
134
+ * Get the result canvas
139
135
  */
140
- toCanvas(): Canvas;
136
+ toCanvas(): CanvasLike;
137
+ /**
138
+ * Clean up cv.Mat to free memory
139
+ */
140
+ destroy(): void;
141
141
  }
142
142
  export {};
@@ -1 +1 @@
1
- export class ImageProcessor{img;width;height;constructor(source){if(source instanceof Canvas){let ctx=source.getContext("2d");let imageData=ctx.getImageData(0,0,source.width,source.height);this.img=cv.matFromImageData(imageData);this.width=source.width;this.height=source.height}else if(source instanceof cv.Mat){this.img=source;this.width=source.cols;this.height=source.rows}else{throw new Error("Invalid source type. Must be either Canvas or cv.Mat.")}}static async prepareCanvas(file){if(file instanceof Canvas)return file;let img=await loadImage(file);let canvas=createCanvas(img.width,img.height);let ctx=canvas.getContext("2d");ctx.drawImage(img,0,0);return canvas}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 buffer=Buffer.from(base64Data,"base64");let arrayBuffer=new ArrayBuffer(buffer.byteLength);new Uint8Array(arrayBuffer).set(new Uint8Array(buffer));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}static async initRuntime(){return new Promise((res)=>{if(cv&&cv.Mat){res()}else{cv["onRuntimeInitialized"]=()=>{res()}}})}execute(operationName,options){if(!registry.hasOperation(operationName)){throw new Error(`Operation "${operationName}" not found`)}try{let result=executeOperation(operationName,this.img,options);this.img=result.img;this.width=result.width;this.height=result.height}catch(error){console.error(`Error executing operation "${operationName}":`,error);throw error}return this}grayscale(options={}){return this.execute("grayscale",options)}invert(options={}){return this.execute("invert",options)}border(options={}){return this.execute("border",options)}blur(options={}){return this.execute("blur",options)}threshold(options={}){return this.execute("threshold",options)}adaptiveThreshold(options={}){return this.execute("adaptiveThreshold",options)}canny(options={}){return this.execute("canny",options)}morphologicalGradient(options={}){return this.execute("morphologicalGradient",options)}erode(options={}){return this.execute("erode",options)}dilate(options={}){return this.execute("dilate",options)}resize(options){return this.execute("resize",options)}warp(options){return this.execute("warp",options)}rotate(options){return this.execute("rotate",options)}convert(options){return this.execute("convert",options)}destroy(){this.img.delete()}toMat(){return this.img}toCanvas(){let canvas=createCanvas(this.width,this.height);let ctx=canvas.getContext("2d");let imgData=ctx.createImageData(this.width,this.height);if(this.img.channels()===1){let data=imgData.data;let gray=new Uint8Array(this.img.data);for(let i=0;i<gray.length;i++){data[i*4]=gray[i];data[i*4+1]=gray[i];data[i*4+2]=gray[i];data[i*4+3]=255}}else{imgData.data.set(new Uint8ClampedArray(this.img.data))}ctx.putImageData(imgData,0,0);return canvas}}import{Canvas,createCanvas,cv,loadImage}from"./index.js";import{executeOperation,registry}from"./index.js";
1
+ export class ImageProcessor{img;width;height;constructor(source){if(getPlatform().isCanvas(source)){let ctx=source.getContext("2d");let imageData=ctx.getImageData(0,0,source.width,source.height);this.img=cv.matFromImageData(imageData);this.width=source.width;this.height=source.height}else if(source instanceof cv.Mat){this.img=source;this.width=source.cols;this.height=source.rows}else{throw new Error("Invalid source type. Must be either Canvas or cv.Mat.")}}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}static async initRuntime(){if(globalThis.cv?.Mat){setCv(globalThis.cv);return}try{let mod=await import("@techstark/opencv-js");let _cv=mod.default||mod;setCv(_cv);if(!_cv.Mat){await new Promise((res)=>{_cv["onRuntimeInitialized"]=()=>res()})}return}catch{}if(typeof document!=="undefined"){await new Promise((resolve,reject)=>{let script=document.createElement("script");script.src="https://cdn.jsdelivr.net/npm/@techstark/opencv-js@4.10.0-release.1/dist/opencv.js";script.async=true;script.onload=()=>{let g=globalThis;if(g.cv?.Mat){setCv(g.cv);resolve()}else if(g.cv){g.cv["onRuntimeInitialized"]=()=>{setCv(g.cv);resolve()}}else{reject(new Error("OpenCV.js loaded but cv not found on globalThis"))}};script.onerror=()=>reject(new Error("Failed to load @techstark/opencv-js from CDN"));document.head.appendChild(script)});return}throw new Error("Cannot initialize OpenCV runtime. Install @techstark/opencv-js or run in a browser.")}execute(operationName,options){let result=executeOperation(operationName,this.img,options);this.img=result.img;this.width=result.width;this.height=result.height;return this}grayscale(options){return this.execute("grayscale",options)}blur(options){return this.execute("blur",options)}threshold(options){return this.execute("threshold",options)}adaptiveThreshold(options){return this.execute("adaptiveThreshold",options)}invert(options){return this.execute("invert",options)}canny(options){return this.execute("canny",options)}dilate(options){return this.execute("dilate",options)}erode(options){return this.execute("erode",options)}border(options){return this.execute("border",options)}resize(options){return this.execute("resize",options)}rotate(options){return this.execute("rotate",options)}warp(options){return this.execute("warp",options)}convert(options){return this.execute("convert",options)}morphologicalGradient(options){return this.execute("morphologicalGradient",options)}toMat(){return this.img}toCanvas(){let platform=getPlatform();let canvas=platform.createCanvas(this.width,this.height);try{cv.imshow(canvas,this.img)}catch(e){let ctx=canvas.getContext("2d");if(!ctx)throw new Error("Could not get 2d context from canvas");let imgData=ctx.createImageData(this.width,this.height);if(this.img.channels()===1){let data=imgData.data;let gray=new Uint8Array(this.img.data);for(let i=0;i<gray.length;i++){data[i*4]=gray[i];data[i*4+1]=gray[i];data[i*4+2]=gray[i];data[i*4+3]=255}}else{imgData.data.set(new Uint8ClampedArray(this.img.data))}ctx.putImageData(imgData,0,0)}return canvas}destroy(){try{this.img.delete()}catch{}}}import{getPlatform}from"./canvas-factory.js";import{cv,setCv}from"./cv-provider.js";import{executeOperation}from"./pipeline/index.js";
package/index.d.ts CHANGED
@@ -1,9 +1,12 @@
1
- import cv from "@techstark/opencv-js";
1
+ import { cv } from "./cv-provider.js";
2
2
  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
6
  export { executeOperation, OperationRegistry, registry, } from "./pipeline/index.js";
7
+ export { getPlatform, setPlatform } from "./canvas-factory.js";
8
+ export type { CanvasLike, CanvasPlatform, Context2DLike, } from "./canvas-factory.js";
9
+ export { CanvasToolkitBase } from "./canvas-toolkit.base.js";
7
10
  export { CanvasToolkit } from "./canvas-toolkit.js";
8
11
  export { Contours } from "./contours.js";
9
12
  export { calculateMeanGrayscaleValue, calculateMeanNormalizedLabLightness, type CalculateMeanLightnessOptions, } from "./image-analysis.js";
package/index.js CHANGED
@@ -1 +1 @@
1
- import cv from"@techstark/opencv-js";export{cv};export{Canvas,createCanvas,ImageData,loadImage}from"@napi-rs/canvas";export{executeOperation,OperationRegistry,registry}from"./pipeline/index.js";export{CanvasToolkit}from"./canvas-toolkit.js";export{Contours}from"./contours.js";export{calculateMeanGrayscaleValue,calculateMeanNormalizedLabLightness}from"./image-analysis.js";export{ImageProcessor}from"./image-processor.js";
1
+ import _cv from"@techstark/opencv-js";import{cv,setCv}from"./cv-provider.js";setCv(_cv);export{cv};import{setPlatform}from"./canvas-factory.js";import{nodePlatform}from"./platform/node.js";setPlatform(nodePlatform);export{Canvas,createCanvas,ImageData,loadImage}from"@napi-rs/canvas";export{executeOperation,OperationRegistry,registry}from"./pipeline/index.js";export{getPlatform,setPlatform}from"./canvas-factory.js";export{CanvasToolkitBase}from"./canvas-toolkit.base.js";export{CanvasToolkit}from"./canvas-toolkit.js";export{Contours}from"./contours.js";export{calculateMeanGrayscaleValue,calculateMeanNormalizedLabLightness}from"./image-analysis.js";export{ImageProcessor}from"./image-processor.js";
package/index.web.d.ts ADDED
@@ -0,0 +1,12 @@
1
+ import { cv } from "./cv-provider.js";
2
+ export { cv };
3
+ export { getPlatform, setPlatform } from "./canvas-factory.js";
4
+ export type { CanvasLike, CanvasPlatform, Context2DLike, } from "./canvas-factory.js";
5
+ export { webPlatform } from "./platform/web.js";
6
+ export type { BoundingBox, Coordinate, Points } from "./index.interface.js";
7
+ export { executeOperation, OperationRegistry, registry, } from "./pipeline/index.js";
8
+ export { CanvasToolkitBase as CanvasToolkit, CanvasToolkitBase, } from "./canvas-toolkit.base.js";
9
+ export { Contours } from "./contours.js";
10
+ export { calculateMeanGrayscaleValue, calculateMeanNormalizedLabLightness, type CalculateMeanLightnessOptions, } from "./image-analysis.js";
11
+ export { ImageProcessor } from "./image-processor.js";
12
+ export type { AdaptiveThresholdOptions, BlurOptions, BorderOptions, CannyOptions, DilateOptions, ErodeOptions, GrayscaleOptions, InvertOptions, MorphologicalGradientOptions, OperationFunction, OperationName, OperationOptions, OperationResult, PartialOptions, RegisteredOperations, RequiredOptions, ResizeOptions, RotateOptions, ThresholdOptions, WarpOptions, } from "./pipeline/index.js";
package/index.web.js ADDED
@@ -0,0 +1 @@
1
+ import{cv}from"./cv-provider.js";export{cv};import{setPlatform}from"./canvas-factory.js";import{webPlatform}from"./platform/web.js";setPlatform(webPlatform);export{getPlatform,setPlatform}from"./canvas-factory.js";export{webPlatform}from"./platform/web.js";export{executeOperation,OperationRegistry,registry}from"./pipeline/index.js";export{CanvasToolkitBase as CanvasToolkit,CanvasToolkitBase}from"./canvas-toolkit.base.js";export{Contours}from"./contours.js";export{calculateMeanGrayscaleValue,calculateMeanNormalizedLabLightness}from"./image-analysis.js";export{ImageProcessor}from"./image-processor.js";
@@ -1,6 +1,6 @@
1
- import type { OperationResult, PartialOptions } from "../index.js";
2
- import { cv } from "../index.js";
3
- declare module "../index" {
1
+ import { cv } from "../cv-provider.js";
2
+ import type { OperationResult, PartialOptions } from "../pipeline/types.js";
3
+ declare module "../pipeline/types" {
4
4
  interface RegisteredOperations {
5
5
  adaptiveThreshold: AdaptiveThresholdOptions;
6
6
  }
@@ -1 +1 @@
1
- import{cv,registry}from"../index.js";function defaultOptions(){return{upper:255,method:cv.ADAPTIVE_THRESH_GAUSSIAN_C,type:cv.THRESH_BINARY_INV,size:7,constant:2}}export function adaptiveThreshold(img,options){let imgAdaptiveThreshold=new cv.Mat;cv.adaptiveThreshold(img,imgAdaptiveThreshold,options.upper,options.method,options.type,options.size,options.constant);img.delete();return{img:imgAdaptiveThreshold,width:imgAdaptiveThreshold.cols,height:imgAdaptiveThreshold.rows}}registry.register("adaptiveThreshold",adaptiveThreshold,defaultOptions);
1
+ import{cv}from"../cv-provider.js";import{registry}from"../pipeline/registry.js";function defaultOptions(){return{upper:255,method:cv.ADAPTIVE_THRESH_GAUSSIAN_C,type:cv.THRESH_BINARY_INV,size:7,constant:2}}export function adaptiveThreshold(img,options){let imgAdaptiveThreshold=new cv.Mat;cv.adaptiveThreshold(img,imgAdaptiveThreshold,options.upper,options.method,options.type,options.size,options.constant);img.delete();return{img:imgAdaptiveThreshold,width:imgAdaptiveThreshold.cols,height:imgAdaptiveThreshold.rows}}registry.register("adaptiveThreshold",adaptiveThreshold,defaultOptions);
@@ -1,6 +1,6 @@
1
- import type { OperationResult, PartialOptions } from "../index.js";
2
- import { cv } from "../index.js";
3
- declare module "../index" {
1
+ import { cv } from "../cv-provider.js";
2
+ import type { OperationResult, PartialOptions } from "../pipeline/types.js";
3
+ declare module "../pipeline/types" {
4
4
  interface RegisteredOperations {
5
5
  blur: BlurOptions;
6
6
  }
@@ -1 +1 @@
1
- import{cv,registry}from"../index.js";function defaultOptions(){return{size:[5,5],sigma:0}}export function blur(img,options){let imgBlur=new cv.Mat;cv.GaussianBlur(img,imgBlur,new cv.Size(options.size[0],options.size[1]),options.sigma);img.delete();return{img:imgBlur,width:imgBlur.cols,height:imgBlur.rows}}registry.register("blur",blur,defaultOptions);
1
+ import{cv}from"../cv-provider.js";import{registry}from"../pipeline/registry.js";function defaultOptions(){return{size:[5,5],sigma:0}}export function blur(img,options){let imgBlur=new cv.Mat;cv.GaussianBlur(img,imgBlur,new cv.Size(options.size[0],options.size[1]),options.sigma);img.delete();return{img:imgBlur,width:imgBlur.cols,height:imgBlur.rows}}registry.register("blur",blur,defaultOptions);
@@ -1,6 +1,6 @@
1
- import type { OperationResult, PartialOptions } from "../index.js";
2
- import { cv } from "../index.js";
3
- declare module "../index" {
1
+ import type { OperationResult, PartialOptions } from "../pipeline/types.js";
2
+ import { cv } from "../cv-provider.js";
3
+ declare module "../pipeline/types" {
4
4
  interface RegisteredOperations {
5
5
  border: BorderOptions;
6
6
  }
@@ -1 +1 @@
1
- import{cv,registry}from"../index.js";function defaultOptions(){return{size:10,borderType:cv.BORDER_CONSTANT,borderColor:[255,255,255,255]}}export function border(img,options){let imgBorder=new cv.Mat;cv.copyMakeBorder(img,imgBorder,options.size,options.size,options.size,options.size,options.borderType,options.borderColor);img.delete();return{img:imgBorder,width:imgBorder.cols,height:imgBorder.rows}}registry.register("border",border,defaultOptions);
1
+ import{cv}from"../cv-provider.js";import{registry}from"../pipeline/registry.js";function defaultOptions(){return{size:10,borderType:cv.BORDER_CONSTANT,borderColor:[255,255,255,255]}}export function border(img,options){let imgBorder=new cv.Mat;cv.copyMakeBorder(img,imgBorder,options.size,options.size,options.size,options.size,options.borderType,options.borderColor);img.delete();return{img:imgBorder,width:imgBorder.cols,height:imgBorder.rows}}registry.register("border",border,defaultOptions);
@@ -1,6 +1,6 @@
1
- import type { OperationResult, PartialOptions } from "../index.js";
2
- import { cv } from "../index.js";
3
- declare module "../index" {
1
+ import type { OperationResult, PartialOptions } from "../pipeline/types.js";
2
+ import { cv } from "../cv-provider.js";
3
+ declare module "../pipeline/types" {
4
4
  interface RegisteredOperations {
5
5
  canny: CannyOptions;
6
6
  }
@@ -1 +1 @@
1
- import{cv,registry}from"../index.js";function defaultOptions(){return{lower:50,upper:150}}export function canny(img,options){let imgCanny=new cv.Mat;cv.Canny(img,imgCanny,options.lower,options.upper);img.delete();return{img:imgCanny,width:imgCanny.cols,height:imgCanny.rows}}registry.register("canny",canny,defaultOptions);
1
+ import{cv}from"../cv-provider.js";import{registry}from"../pipeline/registry.js";function defaultOptions(){return{lower:50,upper:150}}export function canny(img,options){let imgCanny=new cv.Mat;cv.Canny(img,imgCanny,options.lower,options.upper);img.delete();return{img:imgCanny,width:imgCanny.cols,height:imgCanny.rows}}registry.register("canny",canny,defaultOptions);
@@ -1,6 +1,6 @@
1
- import type { OperationResult, RequiredOptions } from "../index.js";
2
- import { cv } from "../index.js";
3
- declare module "../index" {
1
+ import type { OperationResult, RequiredOptions } from "../pipeline/types.js";
2
+ import { cv } from "../cv-provider.js";
3
+ declare module "../pipeline/types" {
4
4
  interface RegisteredOperations {
5
5
  convert: ConvertOptions;
6
6
  }
@@ -1 +1 @@
1
- import{cv,registry}from"../index.js";export function convert(img,options){if(options.rtype===undefined){throw new Error("Invalid options: rtype is required")}let imgConvert=new cv.Mat;img.convertTo(imgConvert,options.rtype);img.delete();return{img:imgConvert,width:imgConvert.cols,height:imgConvert.rows}}registry.register("convert",convert);
1
+ import{cv}from"../cv-provider.js";import{registry}from"../pipeline/registry.js";export function convert(img,options){if(options.rtype===undefined){throw new Error("Invalid options: rtype is required")}let imgConvert=new cv.Mat;img.convertTo(imgConvert,options.rtype);img.delete();return{img:imgConvert,width:imgConvert.cols,height:imgConvert.rows}}registry.register("convert",convert);
@@ -1,6 +1,6 @@
1
- import type { OperationResult, PartialOptions } from "../index.js";
2
- import { cv } from "../index.js";
3
- declare module "../index" {
1
+ import type { OperationResult, PartialOptions } from "../pipeline/types.js";
2
+ import { cv } from "../cv-provider.js";
3
+ declare module "../pipeline/types" {
4
4
  interface RegisteredOperations {
5
5
  dilate: DilateOptions;
6
6
  }
@@ -1 +1 @@
1
- import{cv,registry}from"../index.js";function defaultOptions(){return{size:[5,5],iter:1}}export function dilate(img,options){let imgDilate=new cv.Mat;let kernel=cv.getStructuringElement(cv.MORPH_RECT,new cv.Size(options.size[0],options.size[1]));cv.dilate(img,imgDilate,kernel,new cv.Point(-1,-1),options.iter);img.delete();return{img:imgDilate,width:imgDilate.cols,height:imgDilate.rows}}registry.register("dilate",dilate,defaultOptions);
1
+ import{cv}from"../cv-provider.js";import{registry}from"../pipeline/registry.js";function defaultOptions(){return{size:[5,5],iter:1}}export function dilate(img,options){let imgDilate=new cv.Mat;let kernel=cv.getStructuringElement(cv.MORPH_RECT,new cv.Size(options.size[0],options.size[1]));cv.dilate(img,imgDilate,kernel,new cv.Point(-1,-1),options.iter);img.delete();return{img:imgDilate,width:imgDilate.cols,height:imgDilate.rows}}registry.register("dilate",dilate,defaultOptions);
@@ -1,6 +1,6 @@
1
- import type { OperationResult, PartialOptions } from "../index.js";
2
- import { cv } from "../index.js";
3
- declare module "../index" {
1
+ import type { OperationResult, PartialOptions } from "../pipeline/types.js";
2
+ import { cv } from "../cv-provider.js";
3
+ declare module "../pipeline/types" {
4
4
  interface RegisteredOperations {
5
5
  erode: ErodeOptions;
6
6
  }
@@ -1 +1 @@
1
- import{cv,registry}from"../index.js";function defaultOptions(){return{size:[5,5],iter:1}}export function erode(img,options){let imgErode=new cv.Mat;let kernel=cv.getStructuringElement(cv.MORPH_RECT,new cv.Size(options.size[0],options.size[1]));cv.erode(img,imgErode,kernel,new cv.Point(-1,-1),options.iter);img.delete();return{img:imgErode,width:imgErode.cols,height:imgErode.rows}}registry.register("erode",erode,defaultOptions);
1
+ import{cv}from"../cv-provider.js";import{registry}from"../pipeline/registry.js";function defaultOptions(){return{size:[5,5],iter:1}}export function erode(img,options){let imgErode=new cv.Mat;let kernel=cv.getStructuringElement(cv.MORPH_RECT,new cv.Size(options.size[0],options.size[1]));cv.erode(img,imgErode,kernel,new cv.Point(-1,-1),options.iter);img.delete();return{img:imgErode,width:imgErode.cols,height:imgErode.rows}}registry.register("erode",erode,defaultOptions);
@@ -1,6 +1,6 @@
1
- import type { OperationResult, PartialOptions } from "../index.js";
2
- import { cv } from "../index.js";
3
- declare module "../index" {
1
+ import type { OperationResult, PartialOptions } from "../pipeline/types.js";
2
+ import { cv } from "../cv-provider.js";
3
+ declare module "../pipeline/types" {
4
4
  interface RegisteredOperations {
5
5
  grayscale: GrayscaleOptions;
6
6
  }
@@ -1 +1 @@
1
- import{cv,registry}from"../index.js";function defaultOptions(){return{}}export function grayscale(img,options){let imgGrayscale=new cv.Mat;cv.cvtColor(img,imgGrayscale,cv.COLOR_RGBA2GRAY);img.delete();return{img:imgGrayscale,width:imgGrayscale.cols,height:imgGrayscale.rows}}registry.register("grayscale",grayscale,defaultOptions);
1
+ import{cv}from"../cv-provider.js";import{registry}from"../pipeline/registry.js";function defaultOptions(){return{}}export function grayscale(img,options){let imgGrayscale=new cv.Mat;cv.cvtColor(img,imgGrayscale,cv.COLOR_RGBA2GRAY);img.delete();return{img:imgGrayscale,width:imgGrayscale.cols,height:imgGrayscale.rows}}registry.register("grayscale",grayscale,defaultOptions);
@@ -1,6 +1,6 @@
1
- import type { OperationResult, PartialOptions } from "../index.js";
2
- import { cv } from "../index.js";
3
- declare module "../index" {
1
+ import type { OperationResult, PartialOptions } from "../pipeline/types.js";
2
+ import { cv } from "../cv-provider.js";
3
+ declare module "../pipeline/types" {
4
4
  interface RegisteredOperations {
5
5
  invert: InvertOptions;
6
6
  }
@@ -1 +1 @@
1
- import{cv,registry}from"../index.js";function defaultOptions(){return{}}export function invert(img,options){let imgInvert=new cv.Mat;cv.bitwise_not(img,imgInvert);img.delete();return{img:imgInvert,width:imgInvert.cols,height:imgInvert.rows}}registry.register("invert",invert,defaultOptions);
1
+ import{cv}from"../cv-provider.js";import{registry}from"../pipeline/registry.js";function defaultOptions(){return{}}export function invert(img,options){let imgInvert=new cv.Mat;cv.bitwise_not(img,imgInvert);img.delete();return{img:imgInvert,width:imgInvert.cols,height:imgInvert.rows}}registry.register("invert",invert,defaultOptions);
@@ -1,6 +1,6 @@
1
- import type { OperationResult, PartialOptions } from "../index.js";
2
- import { cv } from "../index.js";
3
- declare module "../index" {
1
+ import type { OperationResult, PartialOptions } from "../pipeline/types.js";
2
+ import { cv } from "../cv-provider.js";
3
+ declare module "../pipeline/types" {
4
4
  interface RegisteredOperations {
5
5
  morphologicalGradient: MorphologicalGradientOptions;
6
6
  }
@@ -1 +1 @@
1
- import{cv,registry}from"../index.js";function defaultOptions(){return{size:[3,3]}}export function morphologicalGradient(img,options){let imgMorphologicalGradient=new cv.Mat;let kernel=cv.getStructuringElement(cv.MORPH_RECT,new cv.Size(options.size[0],options.size[1]));cv.morphologyEx(img,imgMorphologicalGradient,cv.MORPH_GRADIENT,kernel);img.delete();return{img:imgMorphologicalGradient,width:imgMorphologicalGradient.cols,height:imgMorphologicalGradient.rows}}registry.register("morphologicalGradient",morphologicalGradient,defaultOptions);
1
+ import{cv}from"../cv-provider.js";import{registry}from"../pipeline/registry.js";function defaultOptions(){return{size:[3,3]}}export function morphologicalGradient(img,options){let imgMorphologicalGradient=new cv.Mat;let kernel=cv.getStructuringElement(cv.MORPH_RECT,new cv.Size(options.size[0],options.size[1]));cv.morphologyEx(img,imgMorphologicalGradient,cv.MORPH_GRADIENT,kernel);img.delete();return{img:imgMorphologicalGradient,width:imgMorphologicalGradient.cols,height:imgMorphologicalGradient.rows}}registry.register("morphologicalGradient",morphologicalGradient,defaultOptions);
@@ -1,6 +1,6 @@
1
- import type { OperationResult, RequiredOptions } from "../index.js";
2
- import { cv } from "../index.js";
3
- declare module "../index" {
1
+ import type { OperationResult, RequiredOptions } from "../pipeline/types.js";
2
+ import { cv } from "../cv-provider.js";
3
+ declare module "../pipeline/types" {
4
4
  interface RegisteredOperations {
5
5
  resize: ResizeOptions;
6
6
  }
@@ -1 +1 @@
1
- import{cv,registry}from"../index.js";export function resize(img,options){if(!options.width||!options.height){throw new Error("Invalid options: width and height are required")}let imgResize=new cv.Mat;cv.resize(img,imgResize,new cv.Size(options.width,options.height));img.delete();return{img:imgResize,width:imgResize.cols,height:imgResize.rows}}registry.register("resize",resize);
1
+ import{cv}from"../cv-provider.js";import{registry}from"../pipeline/registry.js";export function resize(img,options){if(!options.width||!options.height){throw new Error("Invalid options: width and height are required")}let imgResize=new cv.Mat;cv.resize(img,imgResize,new cv.Size(options.width,options.height));img.delete();return{img:imgResize,width:imgResize.cols,height:imgResize.rows}}registry.register("resize",resize);
@@ -1,6 +1,6 @@
1
- import type { OperationResult, RequiredOptions } from "../index.js";
2
- import { cv } from "../index.js";
3
- declare module "../index" {
1
+ import type { OperationResult, RequiredOptions } from "../pipeline/types.js";
2
+ import { cv } from "../cv-provider.js";
3
+ declare module "../pipeline/types" {
4
4
  interface RegisteredOperations {
5
5
  rotate: RotateOptions;
6
6
  }
@@ -1 +1 @@
1
- import{cv,registry}from"../index.js";export function rotate(img,options){let center=options.center||new cv.Point(img.cols/2,img.rows/2);let M=cv.getRotationMatrix2D(center,options.angle,1);let dsize=new cv.Size(img.cols,img.rows);let rotatedImg=new cv.Mat;cv.warpAffine(img,rotatedImg,M,dsize,cv.INTER_LINEAR,cv.BORDER_CONSTANT,new cv.Scalar);img.delete();M.delete();return{img:rotatedImg,width:rotatedImg.cols,height:rotatedImg.rows}}registry.register("rotate",rotate);
1
+ import{cv}from"../cv-provider.js";import{registry}from"../pipeline/registry.js";export function rotate(img,options){let center=options.center||new cv.Point(img.cols/2,img.rows/2);let M=cv.getRotationMatrix2D(center,options.angle,1);let dsize=new cv.Size(img.cols,img.rows);let rotatedImg=new cv.Mat;cv.warpAffine(img,rotatedImg,M,dsize,cv.INTER_LINEAR,cv.BORDER_CONSTANT,new cv.Scalar);img.delete();M.delete();return{img:rotatedImg,width:rotatedImg.cols,height:rotatedImg.rows}}registry.register("rotate",rotate);
@@ -1,6 +1,6 @@
1
- import type { OperationResult, PartialOptions } from "../index.js";
2
- import { cv } from "../index.js";
3
- declare module "../index" {
1
+ import type { OperationResult, PartialOptions } from "../pipeline/types.js";
2
+ import { cv } from "../cv-provider.js";
3
+ declare module "../pipeline/types" {
4
4
  interface RegisteredOperations {
5
5
  threshold: ThresholdOptions;
6
6
  }
@@ -1 +1 @@
1
- import{cv,registry}from"../index.js";function defaultOptions(){return{lower:0,upper:255,type:cv.THRESH_BINARY_INV+cv.THRESH_OTSU}}export function threshold(img,options){let imgThreshold=new cv.Mat;cv.threshold(img,imgThreshold,options.lower,options.upper,options.type);img.delete();return{img:imgThreshold,width:imgThreshold.cols,height:imgThreshold.rows}}registry.register("threshold",threshold,defaultOptions);
1
+ import{cv}from"../cv-provider.js";import{registry}from"../pipeline/registry.js";function defaultOptions(){return{lower:0,upper:255,type:cv.THRESH_BINARY_INV+cv.THRESH_OTSU}}export function threshold(img,options){let imgThreshold=new cv.Mat;cv.threshold(img,imgThreshold,options.lower,options.upper,options.type);img.delete();return{img:imgThreshold,width:imgThreshold.cols,height:imgThreshold.rows}}registry.register("threshold",threshold,defaultOptions);
@@ -1,6 +1,7 @@
1
- import type { BoundingBox, OperationResult, Points, RequiredOptions } from "../index.js";
2
- import { cv } from "../index.js";
3
- declare module "../index" {
1
+ import { cv } from "../cv-provider.js";
2
+ import type { BoundingBox, Points } from "../index.interface.js";
3
+ import type { OperationResult, RequiredOptions } from "../pipeline/types.js";
4
+ declare module "../pipeline/types" {
4
5
  interface RegisteredOperations {
5
6
  warp: WarpOptions;
6
7
  }
@@ -1 +1 @@
1
- import{cv,registry}from"../index.js";export function warp(img,options){if(!options.points||!options.bbox){throw new Error("Invalid options: points and bbox are required")}const{points,bbox}=options;let imgWarp=new cv.Mat;let targetWidth=bbox.x1-bbox.x0;let targetHeight=bbox.y1-bbox.y0;let destArray=[0,0,targetWidth-1,0,targetWidth-1,targetHeight-1,0,targetHeight-1];let srcArray=[points.topLeft.x,points.topLeft.y,points.topRight.x,points.topRight.y,points.bottomRight.x,points.bottomRight.y,points.bottomLeft.x,points.bottomLeft.y];let dest=cv.matFromArray(4,1,cv.CV_32FC2,destArray);let src=cv.matFromArray(4,1,cv.CV_32FC2,srcArray);let M=cv.getPerspectiveTransform(src,dest);let dsize=new cv.Size(targetWidth,targetHeight);cv.warpPerspective(img,imgWarp,M,dsize);M.delete();src.delete();dest.delete();img.delete();return{img:imgWarp,width:imgWarp.cols,height:imgWarp.rows}}registry.register("warp",warp);
1
+ import{cv}from"../cv-provider.js";import{registry}from"../pipeline/registry.js";export function warp(img,options){if(!options.points||!options.bbox){throw new Error("Invalid options: points and bbox are required")}const{points,bbox}=options;let imgWarp=new cv.Mat;let targetWidth=bbox.x1-bbox.x0;let targetHeight=bbox.y1-bbox.y0;let destArray=[0,0,targetWidth-1,0,targetWidth-1,targetHeight-1,0,targetHeight-1];let srcArray=[points.topLeft.x,points.topLeft.y,points.topRight.x,points.topRight.y,points.bottomRight.x,points.bottomRight.y,points.bottomLeft.x,points.bottomLeft.y];let dest=cv.matFromArray(4,1,cv.CV_32FC2,destArray);let src=cv.matFromArray(4,1,cv.CV_32FC2,srcArray);let M=cv.getPerspectiveTransform(src,dest);let dsize=new cv.Size(targetWidth,targetHeight);cv.warpPerspective(img,imgWarp,M,dsize);M.delete();src.delete();dest.delete();img.delete();return{img:imgWarp,width:imgWarp.cols,height:imgWarp.rows}}registry.register("warp",warp);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ppu-ocv",
3
- "version": "1.7.0",
3
+ "version": "2.0.0",
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",
@@ -16,28 +16,38 @@
16
16
  "type-safe",
17
17
  "modular",
18
18
  "chainable",
19
- "bun"
19
+ "bun",
20
+ "browser",
21
+ "web"
20
22
  ],
21
23
  "author": "snowfluke",
22
24
  "license": "MIT",
23
25
  "type": "module",
24
26
  "main": "./index.js",
25
27
  "types": "./index.d.ts",
28
+ "exports": {
29
+ ".": {
30
+ "types": "./index.d.ts",
31
+ "default": "./index.js"
32
+ },
33
+ "./web": {
34
+ "types": "./index.web.d.ts",
35
+ "default": "./index.web.js"
36
+ }
37
+ },
26
38
  "scripts": {
27
39
  "task": "bun scripts/task.ts",
28
40
  "build:test": "bun task build && bun test",
29
41
  "build:publish": "bun task build && bun task report-size && bun task publish",
30
- "lint": "eslint ./src",
31
- "lint:fix": "eslint ./src --fix"
42
+ "lint": "prettier --check ./src",
43
+ "lint:fix": "prettier --write ./src"
32
44
  },
33
45
  "devDependencies": {
34
46
  "@stylistic/eslint-plugin": "latest",
35
47
  "@types/bun": "latest",
36
48
  "@types/uglify-js": "latest",
37
- "eslint": "latest",
38
- "eslint-plugin-jsdoc": "latest",
39
49
  "mitata": "latest",
40
- "prettier": "^3.6.2",
50
+ "prettier": "^3.8.1",
41
51
  "tsx": "latest",
42
52
  "typescript": "latest",
43
53
  "typescript-eslint": "latest",
@@ -1,4 +1,4 @@
1
- import { cv } from "../index.js";
1
+ import { 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,4 @@
1
- import cv from "@techstark/opencv-js";
1
+ import { cv } from "../cv-provider.js";
2
2
  export interface OperationResult {
3
3
  img: cv.Mat;
4
4
  width: number;
@@ -0,0 +1,3 @@
1
+ import type { CanvasPlatform } from "../canvas-factory.js";
2
+ /** Node.js canvas platform backed by @napi-rs/canvas */
3
+ export declare const nodePlatform: CanvasPlatform;
@@ -0,0 +1 @@
1
+ import{Canvas,createCanvas as napiCreateCanvas,loadImage as napiLoadImage}from"@napi-rs/canvas";export let nodePlatform={createCanvas(width,height){return napiCreateCanvas(width,height)},async loadImage(source){let img=await napiLoadImage(source);let canvas=napiCreateCanvas(img.width,img.height);let ctx=canvas.getContext("2d");ctx.drawImage(img,0,0);return canvas},isCanvas(value){return value instanceof Canvas}};
@@ -0,0 +1,3 @@
1
+ import type { CanvasPlatform } from "../canvas-factory.js";
2
+ /** Browser canvas platform using native DOM / OffscreenCanvas APIs */
3
+ export declare const webPlatform: CanvasPlatform;
@@ -0,0 +1 @@
1
+ export let webPlatform={createCanvas(width,height){if(typeof OffscreenCanvas!=="undefined"){return new OffscreenCanvas(width,height)}if(typeof document!=="undefined"){let c=document.createElement("canvas");c.width=width;c.height=height;return c}throw new Error("No canvas implementation available in this environment.")},async loadImage(source){let blob;if(source instanceof ArrayBuffer){blob=new Blob([source])}else if(typeof source==="string"){let res=await fetch(source);blob=await res.blob()}else{throw new Error("loadImage: unsupported source type")}let bitmap=await createImageBitmap(blob);let canvas=webPlatform.createCanvas(bitmap.width,bitmap.height);let ctx=canvas.getContext("2d");ctx.drawImage(bitmap,0,0);bitmap.close();return canvas},isCanvas(value){if(typeof HTMLCanvasElement!=="undefined"&&value instanceof HTMLCanvasElement){return true}if(typeof OffscreenCanvas!=="undefined"&&value instanceof OffscreenCanvas){return true}return false}};