ppu-ocv 3.1.2 → 3.1.4

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.
@@ -2,44 +2,71 @@
2
2
  * Platform abstraction layer for canvas operations.
3
3
  * Allows ppu-ocv to work with both @napi-rs/canvas (Node) and browser canvas APIs.
4
4
  */
5
- /** Structural type satisfied by both @napi-rs/canvas Canvas and HTMLCanvasElement/OffscreenCanvas */
5
+ /** Structural type satisfied by both @napi-rs/canvas Canvas and HTMLCanvasElement/OffscreenCanvas. */
6
6
  export interface CanvasLike {
7
+ /** Canvas width in pixels. */
7
8
  width: number;
9
+ /** Canvas height in pixels. */
8
10
  height: number;
11
+ /** Return a 2D rendering context for drawing on the canvas. */
9
12
  getContext(contextId: "2d"): any;
13
+ /** Serialize the canvas to a binary buffer (Node-side `@napi-rs/canvas`). Absent on browser canvases. */
10
14
  toBuffer?: (...args: any[]) => Buffer;
15
+ /** Serialize the canvas to a data-URL string (browser canvases). Absent on Node-side `@napi-rs/canvas`. */
11
16
  toDataURL?: (...args: any[]) => string;
12
17
  }
13
- /** Structural type for 2D rendering context */
18
+ /** Structural type for 2D rendering context, matching the cross-runtime subset used by ppu-ocv. */
14
19
  export interface Context2DLike {
20
+ /** The canvas this context is bound to. */
15
21
  canvas: any;
22
+ /** Draw an image, canvas, or bitmap onto the context. Signature follows the standard Canvas2D `drawImage`. */
16
23
  drawImage(...args: any[]): void;
24
+ /** Read raw RGBA pixel data from a rectangular region of the canvas. */
17
25
  getImageData(sx: number, sy: number, sw: number, sh: number): {
18
26
  data: Uint8ClampedArray;
19
27
  width: number;
20
28
  height: number;
21
29
  };
30
+ /** Write raw RGBA pixel data back to the canvas at `(dx, dy)`. */
22
31
  putImageData(imageData: any, dx: number, dy: number): void;
32
+ /** Allocate a blank `ImageData` of the given size. */
23
33
  createImageData(width: number, height: number): any;
34
+ /** Start a new path for stroke/fill commands. */
24
35
  beginPath(): void;
36
+ /** Close the current sub-path by connecting the last point to the first. */
25
37
  closePath(): void;
38
+ /** Move the path cursor to `(x, y)` without drawing. */
26
39
  moveTo(x: number, y: number): void;
40
+ /** Draw a straight line from the current path point to `(x, y)`. */
27
41
  lineTo(x: number, y: number): void;
42
+ /** Stroke the current path with the current `strokeStyle` and `lineWidth`. */
28
43
  stroke(): void;
44
+ /** Stroke an axis-aligned rectangle outline. */
29
45
  strokeRect(x: number, y: number, w: number, h: number): void;
46
+ /** Color, gradient, or pattern used by `stroke` and `strokeRect`. */
30
47
  strokeStyle: string | CanvasGradient | CanvasPattern;
48
+ /** Width in pixels of the line drawn by `stroke` / `strokeRect`. */
31
49
  lineWidth: number;
50
+ /** Color, gradient, or pattern used by `fill` and `fillRect`. */
32
51
  fillStyle: string | CanvasGradient | CanvasPattern;
52
+ /** Fill an axis-aligned rectangle with the current `fillStyle`. */
33
53
  fillRect(x: number, y: number, w: number, h: number): void;
54
+ /** Save the current drawing state (transform, styles) onto the state stack. */
34
55
  save(): void;
56
+ /** Restore the most recently saved drawing state. */
35
57
  restore(): void;
58
+ /** Translate the coordinate system by `(x, y)`. */
36
59
  translate(x: number, y: number): void;
60
+ /** Rotate the coordinate system clockwise by `angle` radians. */
37
61
  rotate(angle: number): void;
38
62
  }
39
- /** Platform-specific canvas operations */
63
+ /** Platform-specific canvas operations. Each runtime entry point registers an implementation via {@link setPlatform}. */
40
64
  export interface CanvasPlatform {
65
+ /** Create a blank canvas of the given width and height. */
41
66
  createCanvas(width: number, height: number): CanvasLike;
67
+ /** Decode an image from a buffer or URL and draw it onto a fresh canvas. */
42
68
  loadImage(source: ArrayBuffer | string): Promise<CanvasLike>;
69
+ /** Type guard for "is this value a canvas of this platform?". */
43
70
  isCanvas(value: unknown): value is CanvasLike;
44
71
  }
45
72
  /** Register the platform-specific canvas implementation */
@@ -45,8 +45,11 @@ export interface DetectedRegion {
45
45
  */
46
46
  export declare class CanvasProcessor {
47
47
  private _canvas;
48
+ /** Create a processor wrapping the given canvas. */
48
49
  constructor(source: CanvasLike);
50
+ /** Current canvas width in pixels. */
49
51
  get width(): number;
52
+ /** Current canvas height in pixels. */
50
53
  get height(): number;
51
54
  /**
52
55
  * Scale the canvas to new dimensions.
@@ -1 +1 @@
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
+ 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)+0.587*(d[i+1]??0)+0.114*(d[i+2]??0));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]??0)*alpha+beta);d[i+1]=Math.round((d[i+1]??0)*alpha+beta);d[i+2]=Math.round((d[i+2]??0)*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]??0);d[i+1]=255-(d[i+1]??0);d[i+2]=255-(d[i+2]??0)}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]??0:Math.round(0.299*(d[i]??0)+0.587*(d[i+1]??0)+0.114*(d[i+2]??0));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]??0;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();if(flat===undefined)break;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,7 +1,8 @@
1
1
  import type { BoundingBox } from "./index.interface.js";
2
2
  import type { CanvasLike, Context2DLike } from "./canvas-factory.js";
3
- /** Structural interface for contour-like objects with 32-bit signed integer point data */
3
+ /** Structural interface for contour-like objects with 32-bit signed integer point data. */
4
4
  export interface ContourLike {
5
+ /** Flat `[x0, y0, x1, y1, ...]` point array. Matches `cv.Mat.data32S` for contour Mats. */
5
6
  data32S: Int32Array | number[];
6
7
  }
7
8
  /**
@@ -12,6 +13,7 @@ export declare class CanvasToolkitBase {
12
13
  protected static _baseInstance: CanvasToolkitBase | null;
13
14
  protected step: number;
14
15
  protected constructor();
16
+ /** Return the singleton instance of {@link CanvasToolkitBase}. */
15
17
  static getInstance(): CanvasToolkitBase;
16
18
  /**
17
19
  * Crop a part of source canvas and return a new canvas of the cropped part
@@ -1 +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
+ 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]??0,pts[1]??0);for(let i=2;i<pts.length;i+=2){ctx.lineTo(pts[i]??0,pts[i+1]??0)}ctx.closePath();ctx.stroke()}}import{getPlatform}from"./canvas-factory.js";
@@ -7,6 +7,7 @@ import { CanvasToolkitBase } from "./canvas-toolkit.base.js";
7
7
  export declare class CanvasToolkit extends CanvasToolkitBase {
8
8
  private static _nodeInstance;
9
9
  protected constructor();
10
+ /** Return the singleton instance of {@link CanvasToolkit}. */
10
11
  static getInstance(): CanvasToolkit;
11
12
  /**
12
13
  * Save a canvas to an image file
package/canvas-toolkit.js CHANGED
@@ -1 +1 @@
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)}}}}
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);if(typeof canvas.toBuffer!=="function")throw new Error("toBuffer not available on this canvas");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,12 +1,17 @@
1
1
  import type { CanvasLike } from "./canvas-factory.js";
2
2
  import { cv } from "./cv-provider.js";
3
3
  import type { BoundingBox, Points } from "./index.interface.js";
4
+ /** Options for configuring contour detection. */
4
5
  export interface ContoursOptions {
5
6
  /** The contour retrieval mode. (cv.RETR_...) */
6
7
  mode: cv.RetrievalModes;
7
8
  /** The contour approximation method. (cv.CHAIN_...) */
8
9
  method: cv.ContourApproximationModes;
9
10
  }
11
+ /**
12
+ * Wrapper around OpenCV's `findContours` that provides convenient accessors
13
+ * for iterating, filtering, and analyzing contours in a binary image.
14
+ */
10
15
  export declare class Contours {
11
16
  private contours;
12
17
  /** The constructor for the Contours class. It takes an image and options as parameters. */
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"./cv-provider.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;cv.findContours(img,contours,hierarchy,opts.mode,opts.method);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{}}}import{cv}from"./cv-provider.js";function defaultOptions(){return{mode:cv.RETR_EXTERNAL,method:cv.CHAIN_APPROX_SIMPLE}}
package/cv-provider.d.ts CHANGED
@@ -18,28 +18,54 @@ type CV = typeof _cvType;
18
18
  */
19
19
  export declare function setCv(instance: CV): void;
20
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.
21
+ * Type-side companion to the {@link cv} runtime proxy.
22
+ *
23
+ * Re-exports the OpenCV.js type aliases (Mat, Rect, Size, enum constants…) under
24
+ * the `cv.` namespace, so consumers importing `{ cv }` get the types AND the
25
+ * runtime Proxy object via TypeScript's declaration-merging rules.
24
26
  */
25
27
  export declare namespace cv {
28
+ /** OpenCV Mat (matrix / image buffer). */
26
29
  type Mat = _cvType.Mat;
30
+ /** A vector of Mat objects, used for contours. */
27
31
  type MatVector = _cvType.MatVector;
32
+ /** A 2D point `{ x, y }`. */
28
33
  type Point = _cvType.Point;
34
+ /** An axis-aligned rectangle `{ x, y, width, height }`. */
29
35
  type Rect = _cvType.Rect;
36
+ /** A 2D size `{ width, height }`. */
30
37
  type Size = _cvType.Size;
38
+ /** A 4-element scalar value, often used for colors `[b, g, r, a]`. */
31
39
  type Scalar = _cvType.Scalar;
40
+ /** Adaptive thresholding method constants (e.g., `cv.ADAPTIVE_THRESH_GAUSSIAN_C`). */
32
41
  type AdaptiveThresholdTypes = _cvType.AdaptiveThresholdTypes;
42
+ /** Thresholding type constants (e.g., `cv.THRESH_BINARY`). */
33
43
  type ThresholdTypes = _cvType.ThresholdTypes;
44
+ /** Line type constants (e.g., `cv.LINE_8`). */
34
45
  type LineTypes = _cvType.LineTypes;
46
+ /** Contour retrieval mode constants (e.g., `cv.RETR_EXTERNAL`). */
35
47
  type RetrievalModes = _cvType.RetrievalModes;
48
+ /** Contour approximation method constants (e.g., `cv.CHAIN_APPROX_SIMPLE`). */
36
49
  type ContourApproximationModes = _cvType.ContourApproximationModes;
50
+ /** Border type constants (e.g., `cv.BORDER_CONSTANT`). */
37
51
  type BorderTypes = _cvType.BorderTypes;
52
+ /** Interpolation flag constants (e.g., `cv.INTER_LINEAR`). */
38
53
  type InterpolationFlags = _cvType.InterpolationFlags;
54
+ /** Color conversion code constants (e.g., `cv.COLOR_RGBA2GRAY`). */
39
55
  type ColorConversionCodes = _cvType.ColorConversionCodes;
56
+ /** Morphological structuring element shape constants (e.g., `cv.MORPH_RECT`). */
40
57
  type MorphShapes = _cvType.MorphShapes;
58
+ /** Morphological operation type constants (e.g., `cv.MORPH_GRADIENT`). */
41
59
  type MorphTypes = _cvType.MorphTypes;
60
+ /** Integer alias — opencv-js represents `int` as a plain `number`. */
42
61
  type int = number;
43
62
  }
63
+ /**
64
+ * Lazy proxy for the OpenCV runtime.
65
+ * Access any OpenCV constant or constructor (e.g. `cv.Mat`, `cv.RETR_EXTERNAL`)
66
+ * through this object. The underlying instance is resolved on first access,
67
+ * so importing this module never throws at module-load time — only when a
68
+ * property is actually accessed before {@link ImageProcessor.initRuntime} has run.
69
+ */
44
70
  export declare const cv: CV;
45
71
  export {};
package/deskew.d.ts CHANGED
@@ -38,6 +38,10 @@ export interface DeskewOptions {
38
38
  export declare class DeskewService {
39
39
  private readonly verbose;
40
40
  private readonly minimumAreaThreshold;
41
+ /**
42
+ * Create a DeskewService.
43
+ * @param options - Configuration. See {@link DeskewOptions}.
44
+ */
41
45
  constructor(options?: DeskewOptions);
42
46
  private log;
43
47
  /**
package/deskew.js CHANGED
@@ -1 +1 @@
1
- export class DeskewService{verbose;minimumAreaThreshold;constructor(options={}){this.verbose=options.verbose??false;this.minimumAreaThreshold=options.minimumAreaThreshold??20}log(message){if(this.verbose){console.log(`[DeskewService] ${message}`)}}async calculateSkewAngle(canvas){let processor=new ImageProcessor(canvas);let mat=processor.grayscale().threshold({lower:0,upper:255,type:cv.THRESH_BINARY+cv.THRESH_OTSU}).toMat();let contours=new Contours(mat,{mode:cv.RETR_LIST,method:cv.CHAIN_APPROX_SIMPLE});processor.destroy();let minAngle=-20;let maxAngle=20;let minArea=this.minimumAreaThreshold;let textRegions=[];contours.iterate((contour)=>{let rect=contours.getRect(contour);let area=rect.width*rect.height;if(area<minArea)return;let aspectRatio=rect.width/rect.height;if(aspectRatio>0.2&&aspectRatio<10){textRegions.push({rect,contour,area,aspectRatio})}});if(textRegions.length===0){this.log("No valid text regions found for skew calculation.");contours.destroy();return 0}let averageHeight=textRegions.reduce((sum,region)=>sum+region.rect.height,0)/textRegions.length;let filteredRegions=textRegions.filter((region)=>{return region.rect.height<=averageHeight*1.5});this.log(`Found ${filteredRegions.length} text regions for skew analysis.`);let minRectAngles=this.calculateMinRectAngles(filteredRegions,contours);let baselineAngles=this.calculateBaselineAngles(filteredRegions);let houghAngles=this.calculateHoughAngles(mat,minAngle,maxAngle);contours.destroy();let allAngles=[...minRectAngles.map((a)=>({...a,method:"minRect"})),...baselineAngles.map((a)=>({...a,method:"baseline"})),...houghAngles.map((a)=>({...a,method:"hough"}))];if(allAngles.length===0){this.log("No angles detected from any method.");return 0}let consensusAngle=this.calculateConsensusAngle(allAngles,minAngle,maxAngle);this.log(`Calculated skew angle: ${consensusAngle.toFixed(3)}° (from ${allAngles.length} measurements)`);return consensusAngle}async deskewImage(canvas){this.log("Starting image deskewing process");let angle=await this.calculateSkewAngle(canvas);this.log(`Detected skew angle: ${angle.toFixed(2)}°. Rotating image by ${-angle.toFixed(2)}°...`);let processor=new ImageProcessor(canvas);try{let rotatedCanvas=processor.rotate({angle}).toCanvas();return rotatedCanvas}finally{processor.destroy()}}calculateMinRectAngles(textRegions,contours){let angles=[];for(let region of textRegions){try{let minRect=cv.minAreaRect(region.contour);if(!minRect)continue;let angle=minRect.angle;if(angle>45){angle-=90}else if(angle<-45){angle+=90}let areaWeight=Math.log(region.area+1);let aspectWeight=Math.min(region.aspectRatio,1/region.aspectRatio)*2;let weight=areaWeight*aspectWeight;angles.push({angle,weight})}catch(error){continue}}return angles}calculateBaselineAngles(textRegions){let angles=[];for(let region of textRegions){try{let points=region.contour.data32S;if(!points||points.length<8)continue;let bottomPoints=[];for(let i=0;i<points.length;i+=2){let x=points[i];let y=points[i+1];if(x!==undefined&&y!==undefined){bottomPoints.push({x,y})}}if(bottomPoints.length<3)continue;bottomPoints.sort((a,b)=>a.x-b.x);let segments=3;let segmentSize=Math.floor(bottomPoints.length/segments);let baselinePoints=[];for(let seg=0;seg<segments;seg++){let start=seg*segmentSize;let end=seg===segments-1?bottomPoints.length:(seg+1)*segmentSize;let segmentPoints=bottomPoints.slice(start,end);if(segmentPoints.length>0){let maxYPoint=segmentPoints.reduce((max,point)=>point.y>max.y?point:max);baselinePoints.push(maxYPoint)}}if(baselinePoints.length>=2){let angle=this.calculateLineAngle(baselinePoints);let weight=region.area*Math.min(region.aspectRatio,1/region.aspectRatio);angles.push({angle,weight})}}catch(error){continue}}return angles}calculateHoughAngles(mat,minAngle,maxAngle){let angles=[];try{let kernel=cv.getStructuringElement(cv.MORPH_RECT,new cv.Size(3,1));let morphed=new cv.Mat;cv.morphologyEx(mat,morphed,cv.MORPH_CLOSE,kernel);let lines=new cv.Mat;cv.HoughLinesP(morphed,lines,1,Math.PI/180,30,50,10);for(let i=0;i<lines.rows;i++){let line=lines.data32S.subarray(i*4,(i+1)*4);const[x1,y1,x2,y2]=line;if(x1!==undefined&&y1!==undefined&&x2!==undefined&&y2!==undefined){let dx=x2-x1;let dy=y2-y1;if(Math.abs(dx)>1){let angle=Math.atan2(dy,dx)*180/Math.PI;if(angle>45)angle-=90;if(angle<-45)angle+=90;if(angle>=minAngle&&angle<=maxAngle){let lineLength=Math.sqrt(dx*dx+dy*dy);angles.push({angle,weight:lineLength})}}}}morphed.delete();lines.delete();kernel.delete()}catch(error){this.log("Hough transform failed, skipping this method.")}return angles}calculateLineAngle(points){if(points.length<2)return 0;let n=points.length;let sumX=points.reduce((sum,p)=>sum+p.x,0);let sumY=points.reduce((sum,p)=>sum+p.y,0);let sumXY=points.reduce((sum,p)=>sum+p.x*p.y,0);let sumXX=points.reduce((sum,p)=>sum+p.x*p.x,0);let denominator=n*sumXX-sumX*sumX;if(Math.abs(denominator)<0.0000000001)return 0;let slope=(n*sumXY-sumX*sumY)/denominator;let angle=Math.atan(slope)*180/Math.PI;if(angle>45)angle-=90;if(angle<-45)angle+=90;return angle}calculateConsensusAngle(angles,minAngle,maxAngle){if(angles.length===0)return 0;let sortedAngles=[...angles].sort((a,b)=>a.angle-b.angle);let q1Index=Math.floor(sortedAngles.length*0.25);let q3Index=Math.floor(sortedAngles.length*0.75);let q1=sortedAngles[q1Index]?.angle||0;let q3=sortedAngles[q3Index]?.angle||0;let iqr=q3-q1;let lowerBound=q1-1.5*iqr;let upperBound=q3+1.5*iqr;let filteredAngles=angles.filter((a)=>a.angle>=lowerBound&&a.angle<=upperBound&&a.angle>=minAngle&&a.angle<=maxAngle);if(filteredAngles.length===0){this.log("All angles filtered out as outliers, using median of original set.");let medianIndex=Math.floor(sortedAngles.length/2);return sortedAngles[medianIndex]?.angle||0}let totalWeight=filteredAngles.reduce((sum,a)=>sum+a.weight,0);if(totalWeight===0){let average=filteredAngles.reduce((sum,a)=>sum+a.angle,0)/filteredAngles.length;return average}let weightedSum=filteredAngles.reduce((sum,a)=>sum+a.angle*a.weight,0);let weightedAverage=weightedSum/totalWeight;let methodCounts=filteredAngles.reduce((counts,a)=>{counts[a.method]=(counts[a.method]||0)+1;return counts},{});this.log(`Angle methods used: ${Object.entries(methodCounts).map(([method,count])=>`${method}:${count}`).join(", ")}`);return Math.max(minAngle,Math.min(maxAngle,weightedAverage))}}import{Contours}from"./contours.js";import{cv}from"./cv-provider.js";import{ImageProcessor}from"./image-processor.js";
1
+ export class DeskewService{verbose;minimumAreaThreshold;constructor(options={}){this.verbose=options.verbose??false;this.minimumAreaThreshold=options.minimumAreaThreshold??20}log(message){if(this.verbose){console.log(`[DeskewService] ${message}`)}}async calculateSkewAngle(canvas){let processor=new ImageProcessor(canvas);let mat=processor.grayscale().threshold({lower:0,upper:255,type:cv.THRESH_BINARY+cv.THRESH_OTSU}).toMat();let contours=new Contours(mat,{mode:cv.RETR_LIST,method:cv.CHAIN_APPROX_SIMPLE});processor.destroy();let minAngle=-20;let maxAngle=20;let minArea=this.minimumAreaThreshold;let textRegions=[];contours.iterate((contour)=>{let rect=contours.getRect(contour);let area=rect.width*rect.height;if(area<minArea)return;let aspectRatio=rect.width/rect.height;if(aspectRatio>0.2&&aspectRatio<10){textRegions.push({rect,contour,area,aspectRatio})}});if(textRegions.length===0){this.log("No valid text regions found for skew calculation.");contours.destroy();return 0}let averageHeight=textRegions.reduce((sum,region)=>sum+region.rect.height,0)/textRegions.length;let filteredRegions=textRegions.filter((region)=>{return region.rect.height<=averageHeight*1.5});this.log(`Found ${filteredRegions.length} text regions for skew analysis.`);let minRectAngles=this.calculateMinRectAngles(filteredRegions,contours);let baselineAngles=this.calculateBaselineAngles(filteredRegions);let houghAngles=this.calculateHoughAngles(mat,minAngle,maxAngle);contours.destroy();let allAngles=[...minRectAngles.map((a)=>({...a,method:"minRect"})),...baselineAngles.map((a)=>({...a,method:"baseline"})),...houghAngles.map((a)=>({...a,method:"hough"}))];if(allAngles.length===0){this.log("No angles detected from any method.");return 0}let consensusAngle=this.calculateConsensusAngle(allAngles,minAngle,maxAngle);this.log(`Calculated skew angle: ${consensusAngle.toFixed(3)}° (from ${allAngles.length} measurements)`);return consensusAngle}async deskewImage(canvas){this.log("Starting image deskewing process");let angle=await this.calculateSkewAngle(canvas);this.log(`Detected skew angle: ${angle.toFixed(2)}°. Rotating image by ${-angle.toFixed(2)}°...`);let processor=new ImageProcessor(canvas);try{let rotatedCanvas=processor.rotate({angle}).toCanvas();return rotatedCanvas}finally{processor.destroy()}}calculateMinRectAngles(textRegions,_contours){let angles=[];for(let region of textRegions){try{let minRect=cv.minAreaRect(region.contour);if(!minRect)continue;let angle=minRect.angle;if(angle>45){angle-=90}else if(angle<-45){angle+=90}let areaWeight=Math.log(region.area+1);let aspectWeight=Math.min(region.aspectRatio,1/region.aspectRatio)*2;let weight=areaWeight*aspectWeight;angles.push({angle,weight})}catch{continue}}return angles}calculateBaselineAngles(textRegions){let angles=[];for(let region of textRegions){try{let points=region.contour.data32S;if(!points||points.length<8)continue;let bottomPoints=[];for(let i=0;i<points.length;i+=2){let x=points[i];let y=points[i+1];if(x!==undefined&&y!==undefined){bottomPoints.push({x,y})}}if(bottomPoints.length<3)continue;bottomPoints.sort((a,b)=>a.x-b.x);let segments=3;let segmentSize=Math.floor(bottomPoints.length/segments);let baselinePoints=[];for(let seg=0;seg<segments;seg++){let start=seg*segmentSize;let end=seg===segments-1?bottomPoints.length:(seg+1)*segmentSize;let segmentPoints=bottomPoints.slice(start,end);if(segmentPoints.length>0){let maxYPoint=segmentPoints.reduce((max,point)=>point.y>max.y?point:max);baselinePoints.push(maxYPoint)}}if(baselinePoints.length>=2){let angle=this.calculateLineAngle(baselinePoints);let weight=region.area*Math.min(region.aspectRatio,1/region.aspectRatio);angles.push({angle,weight})}}catch{continue}}return angles}calculateHoughAngles(mat,minAngle,maxAngle){let angles=[];try{let kernel=cv.getStructuringElement(cv.MORPH_RECT,new cv.Size(3,1));let morphed=new cv.Mat;cv.morphologyEx(mat,morphed,cv.MORPH_CLOSE,kernel);let lines=new cv.Mat;cv.HoughLinesP(morphed,lines,1,Math.PI/180,30,50,10);for(let i=0;i<lines.rows;i++){let line=lines.data32S.subarray(i*4,(i+1)*4);const[x1,y1,x2,y2]=line;if(x1!==undefined&&y1!==undefined&&x2!==undefined&&y2!==undefined){let dx=x2-x1;let dy=y2-y1;if(Math.abs(dx)>1){let angle=Math.atan2(dy,dx)*180/Math.PI;if(angle>45)angle-=90;if(angle<-45)angle+=90;if(angle>=minAngle&&angle<=maxAngle){let lineLength=Math.sqrt(dx*dx+dy*dy);angles.push({angle,weight:lineLength})}}}}morphed.delete();lines.delete();kernel.delete()}catch{this.log("Hough transform failed, skipping this method.")}return angles}calculateLineAngle(points){if(points.length<2)return 0;let n=points.length;let sumX=points.reduce((sum,p)=>sum+p.x,0);let sumY=points.reduce((sum,p)=>sum+p.y,0);let sumXY=points.reduce((sum,p)=>sum+p.x*p.y,0);let sumXX=points.reduce((sum,p)=>sum+p.x*p.x,0);let denominator=n*sumXX-sumX*sumX;if(Math.abs(denominator)<0.0000000001)return 0;let slope=(n*sumXY-sumX*sumY)/denominator;let angle=Math.atan(slope)*180/Math.PI;if(angle>45)angle-=90;if(angle<-45)angle+=90;return angle}calculateConsensusAngle(angles,minAngle,maxAngle){if(angles.length===0)return 0;let sortedAngles=[...angles].sort((a,b)=>a.angle-b.angle);let q1Index=Math.floor(sortedAngles.length*0.25);let q3Index=Math.floor(sortedAngles.length*0.75);let q1=sortedAngles[q1Index]?.angle||0;let q3=sortedAngles[q3Index]?.angle||0;let iqr=q3-q1;let lowerBound=q1-1.5*iqr;let upperBound=q3+1.5*iqr;let filteredAngles=angles.filter((a)=>a.angle>=lowerBound&&a.angle<=upperBound&&a.angle>=minAngle&&a.angle<=maxAngle);if(filteredAngles.length===0){this.log("All angles filtered out as outliers, using median of original set.");let medianIndex=Math.floor(sortedAngles.length/2);return sortedAngles[medianIndex]?.angle||0}let totalWeight=filteredAngles.reduce((sum,a)=>sum+a.weight,0);if(totalWeight===0){let average=filteredAngles.reduce((sum,a)=>sum+a.angle,0)/filteredAngles.length;return average}let weightedSum=filteredAngles.reduce((sum,a)=>sum+a.angle*a.weight,0);let weightedAverage=weightedSum/totalWeight;let methodCounts=filteredAngles.reduce((counts,a)=>{counts[a.method]=(counts[a.method]||0)+1;return counts},{});this.log(`Angle methods used: ${Object.entries(methodCounts).map(([method,count])=>`${method}:${count}`).join(", ")}`);return Math.max(minAngle,Math.min(maxAngle,weightedAverage))}}import{Contours}from"./contours.js";import{cv}from"./cv-provider.js";import{ImageProcessor}from"./image-processor.js";
@@ -5,9 +5,31 @@ type NameWithRequiredOptions = {
5
5
  [N in OperationName]: OperationOptions<N> extends RequiredOptions ? N : never;
6
6
  }[OperationName];
7
7
  type NameWithOptionalOptions = Exclude<OperationName, NameWithRequiredOptions>;
8
+ /**
9
+ * OpenCV-powered image processing pipeline.
10
+ *
11
+ * Wraps a `cv.Mat` and exposes a chainable API of named operations
12
+ * (grayscale, blur, threshold, etc.). Each method mutates the internal
13
+ * state and returns `this`, so operations can be chained fluently.
14
+ *
15
+ * Call {@link ImageProcessor.initRuntime} once before creating any instances.
16
+ *
17
+ * @example
18
+ * ```ts
19
+ * await ImageProcessor.initRuntime();
20
+ * const result = new ImageProcessor(canvas)
21
+ * .grayscale()
22
+ * .blur()
23
+ * .threshold()
24
+ * .toCanvas();
25
+ * ```
26
+ */
8
27
  export declare class ImageProcessor {
28
+ /** Underlying OpenCV Mat. Each operation deletes the previous Mat and replaces this reference. */
9
29
  img: cv.Mat;
30
+ /** Current image width in pixels, kept in sync with `img.cols`. */
10
31
  width: number;
32
+ /** Current image height in pixels, kept in sync with `img.rows`. */
11
33
  height: number;
12
34
  /**
13
35
  * Create an ImageProcessor instance from a Canvas or cv.Mat
@@ -15,11 +37,8 @@ export declare class ImageProcessor {
15
37
  */
16
38
  constructor(source: CanvasLike | cv.Mat);
17
39
  /**
18
- * Initialize OpenCV runtime. Must be called before any image processing.
19
- *
20
- * - **Node.js**: Uses `@techstark/opencv-js` from node_modules (loaded by entry point).
21
- * - **Browser with bundler**: Resolves `@techstark/opencv-js` via the bundler.
22
- * - **Browser without bundler**: Falls back to loading `@techstark/opencv-js` from npm CDN.
40
+ * Initialize OpenCV runtime. This is recommended to be called before any
41
+ * image processing.
23
42
  */
24
43
  static initRuntime(): Promise<void>;
25
44
  /**
@@ -1 +1 @@
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 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";
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 initRuntime(){return new Promise((res)=>{if(cv&&cv.Mat){res()}else{cv["onRuntimeInitialized"]=()=>{res()}}})}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{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}from"./cv-provider.js";import{executeOperation}from"./pipeline/index.js";
@@ -1,16 +1,32 @@
1
+ /** A 2D point in canvas pixel coordinates. */
1
2
  export interface Coordinate {
3
+ /** X coordinate in pixels, measured from the left edge. */
2
4
  x: number;
5
+ /** Y coordinate in pixels, measured from the top edge. */
3
6
  y: number;
4
7
  }
8
+ /** The four corner points of an axis-aligned rectangle, e.g. a contour bounding box. */
5
9
  export interface Points {
10
+ /** Top-left corner. */
6
11
  topLeft: Coordinate;
12
+ /** Top-right corner. */
7
13
  topRight: Coordinate;
14
+ /** Bottom-left corner. */
8
15
  bottomLeft: Coordinate;
16
+ /** Bottom-right corner. */
9
17
  bottomRight: Coordinate;
10
18
  }
19
+ /**
20
+ * An axis-aligned bounding box with exclusive `x1` / `y1`.
21
+ * Width is `x1 - x0`; height is `y1 - y0`.
22
+ */
11
23
  export interface BoundingBox {
24
+ /** Left edge, inclusive. */
12
25
  x0: number;
26
+ /** Top edge, inclusive. */
13
27
  y0: number;
28
+ /** Right edge, exclusive. */
14
29
  x1: number;
30
+ /** Bottom edge, exclusive. */
15
31
  y1: number;
16
32
  }
package/index.web.d.ts CHANGED
@@ -1,3 +1,27 @@
1
+ /**
2
+ * Web entry point — browsers with OpenCV + DOM canvas.
3
+ *
4
+ * Use this when you need the full image-processing pipeline in a browser.
5
+ * Canvas operations target `HTMLCanvasElement` / `OffscreenCanvas` instead
6
+ * of `@napi-rs/canvas`. OpenCV is loaded via `ImageProcessor.initRuntime()`
7
+ * — either from the bundled `@techstark/opencv-js` or from jsDelivr CDN
8
+ * when no bundler is in play.
9
+ *
10
+ * Not suitable for Manifest V3 Chrome extensions or other CSP-restricted
11
+ * runtimes — OpenCV.js uses Emscripten embind which calls `new Function`.
12
+ * For those environments, import `ppu-ocv/canvas-web` (no OpenCV).
13
+ *
14
+ * @example
15
+ * ```ts
16
+ * import { ImageProcessor, CanvasProcessor } from "ppu-ocv/web";
17
+ *
18
+ * await ImageProcessor.initRuntime();
19
+ * const canvas = await CanvasProcessor.prepareCanvas(await response.arrayBuffer());
20
+ * const result = new ImageProcessor(canvas).grayscale().toCanvas();
21
+ * ```
22
+ *
23
+ * @module
24
+ */
1
25
  import { cv } from "./cv-provider.js";
2
26
  export { cv };
3
27
  export { getPlatform, setPlatform } from "./canvas-factory.js";
@@ -1,5 +1,6 @@
1
1
  import { cv } from "../cv-provider.js";
2
2
  import type { OperationResult, PartialOptions } from "../pipeline/types.js";
3
+ /** Options for the adaptive threshold operation. */
3
4
  export interface AdaptiveThresholdOptions extends PartialOptions {
4
5
  /** Upper threshold value (0-255) */
5
6
  upper: number;
@@ -12,4 +13,5 @@ export interface AdaptiveThresholdOptions extends PartialOptions {
12
13
  /** Constant subtracted from the mean or weighted mean */
13
14
  constant: number;
14
15
  }
16
+ /** Apply adaptive thresholding to convert a grayscale image to binary. */
15
17
  export declare function adaptiveThreshold(img: cv.Mat, options: AdaptiveThresholdOptions): OperationResult;
@@ -1,9 +1,11 @@
1
1
  import { cv } from "../cv-provider.js";
2
2
  import type { OperationResult, PartialOptions } from "../pipeline/types.js";
3
+ /** Options for the Gaussian blur operation. */
3
4
  export interface BlurOptions extends PartialOptions {
4
5
  /** Size of the blur [x, y] */
5
6
  size: [number, number];
6
7
  /** Gaussian kernel standard deviation on x axis */
7
8
  sigma: number;
8
9
  }
10
+ /** Apply Gaussian blur to reduce noise. */
9
11
  export declare function blur(img: cv.Mat, options: BlurOptions): OperationResult;
@@ -1,5 +1,6 @@
1
1
  import type { OperationResult, PartialOptions } from "../pipeline/types.js";
2
2
  import { cv } from "../cv-provider.js";
3
+ /** Options for adding a constant-color border around the image. */
3
4
  export interface BorderOptions extends PartialOptions {
4
5
  /** Size of the border in pixels */
5
6
  size: number;
@@ -8,4 +9,5 @@ export interface BorderOptions extends PartialOptions {
8
9
  /** Border color in [B, G, R, A] format */
9
10
  borderColor: [cv.int, cv.int, cv.int, cv.int];
10
11
  }
12
+ /** Add a uniform border around the image using `cv.copyMakeBorder`. */
11
13
  export declare function border(img: cv.Mat, options: BorderOptions): OperationResult;
@@ -1,9 +1,11 @@
1
1
  import type { OperationResult, PartialOptions } from "../pipeline/types.js";
2
2
  import { cv } from "../cv-provider.js";
3
+ /** Options for the Canny edge-detection operation. */
3
4
  export interface CannyOptions extends PartialOptions {
4
5
  /** Lower threshold for the hysteresis procedure (0-255) */
5
6
  lower: number;
6
7
  /** Upper threshold for the hysteresis procedure (0-255) */
7
8
  upper: number;
8
9
  }
10
+ /** Detect edges using the Canny algorithm. */
9
11
  export declare function canny(img: cv.Mat, options: CannyOptions): OperationResult;
@@ -1,7 +1,9 @@
1
1
  import type { OperationResult, RequiredOptions } from "../pipeline/types.js";
2
2
  import { cv } from "../cv-provider.js";
3
+ /** Options for converting a Mat to a different depth/channel type. */
3
4
  export interface ConvertOptions extends RequiredOptions {
4
5
  /** Desired matrix type (cv.CV_...) if negative, it will be the same as input */
5
6
  rtype: number;
6
7
  }
8
+ /** Convert the image matrix to a new type via `Mat.convertTo`. */
7
9
  export declare function convert(img: cv.Mat, options: ConvertOptions): OperationResult;
@@ -1,9 +1,11 @@
1
1
  import type { OperationResult, PartialOptions } from "../pipeline/types.js";
2
2
  import { cv } from "../cv-provider.js";
3
+ /** Options for the morphological dilation operation. */
3
4
  export interface DilateOptions extends PartialOptions {
4
5
  /** Size of the block [x, y] */
5
6
  size: [number, number];
6
7
  /** Number of iterations for the dilation operation */
7
8
  iter: number;
8
9
  }
10
+ /** Dilate the image to expand foreground regions. */
9
11
  export declare function dilate(img: cv.Mat, options: DilateOptions): OperationResult;
@@ -1,9 +1,11 @@
1
1
  import type { OperationResult, PartialOptions } from "../pipeline/types.js";
2
2
  import { cv } from "../cv-provider.js";
3
+ /** Options for the morphological erosion operation. */
3
4
  export interface ErodeOptions extends PartialOptions {
4
5
  /** Size of the block [x, y] */
5
6
  size: [number, number];
6
7
  /** Number of iterations for the erosion operation */
7
8
  iter: number;
8
9
  }
10
+ /** Erode the image to shrink foreground regions and remove small noise. */
9
11
  export declare function erode(img: cv.Mat, options: ErodeOptions): OperationResult;
@@ -1,5 +1,7 @@
1
1
  import type { OperationResult, PartialOptions } from "../pipeline/types.js";
2
2
  import { cv } from "../cv-provider.js";
3
+ /** Options for the grayscale conversion operation (no configurable fields). */
3
4
  export interface GrayscaleOptions extends PartialOptions {
4
5
  }
5
- export declare function grayscale(img: cv.Mat, options: GrayscaleOptions): OperationResult;
6
+ /** Convert the image to grayscale using `COLOR_RGBA2GRAY`. */
7
+ export declare function grayscale(img: cv.Mat, _options: GrayscaleOptions): OperationResult;
@@ -1 +1 @@
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
+ 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,5 +1,7 @@
1
1
  import type { OperationResult, PartialOptions } from "../pipeline/types.js";
2
2
  import { cv } from "../cv-provider.js";
3
+ /** Options for the bitwise-NOT color inversion operation (no configurable fields). */
3
4
  export interface InvertOptions extends PartialOptions {
4
5
  }
5
- export declare function invert(img: cv.Mat, options: InvertOptions): OperationResult;
6
+ /** Invert all pixel values using `cv.bitwise_not`. */
7
+ export declare function invert(img: cv.Mat, _options: InvertOptions): OperationResult;
@@ -1 +1 @@
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
+ 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,7 +1,9 @@
1
1
  import type { OperationResult, PartialOptions } from "../pipeline/types.js";
2
2
  import { cv } from "../cv-provider.js";
3
+ /** Options for the morphological gradient operation. */
3
4
  export interface MorphologicalGradientOptions extends PartialOptions {
4
5
  /** Kernel size for the morphological gradient operation [x, y] */
5
6
  size: [number, number];
6
7
  }
8
+ /** Apply morphological gradient to highlight edges (dilation minus erosion). */
7
9
  export declare function morphologicalGradient(img: cv.Mat, options: MorphologicalGradientOptions): OperationResult;
@@ -1,9 +1,11 @@
1
1
  import type { OperationResult, RequiredOptions } from "../pipeline/types.js";
2
2
  import { cv } from "../cv-provider.js";
3
+ /** Options for resizing the image to exact pixel dimensions. */
3
4
  export interface ResizeOptions extends RequiredOptions {
4
5
  /** Width of the resized image */
5
6
  width: number;
6
7
  /** Height of the resized image */
7
8
  height: number;
8
9
  }
10
+ /** Resize the image to the given width and height. */
9
11
  export declare function resize(img: cv.Mat, options: ResizeOptions): OperationResult;
@@ -1,9 +1,11 @@
1
1
  import type { OperationResult, RequiredOptions } from "../pipeline/types.js";
2
2
  import { cv } from "../cv-provider.js";
3
+ /** Options for rotating the image around a pivot point. */
3
4
  export interface RotateOptions extends RequiredOptions {
4
5
  /** Angle of rotation in degrees (positive for counter-clockwise) */
5
6
  angle: number;
6
7
  /** Optional center of rotation. Defaults to the image center. */
7
8
  center?: cv.Point;
8
9
  }
10
+ /** Rotate the image by the given angle around its centre (or a custom pivot). */
9
11
  export declare function rotate(img: cv.Mat, options: RotateOptions): OperationResult;
@@ -1,5 +1,6 @@
1
1
  import type { OperationResult, PartialOptions } from "../pipeline/types.js";
2
2
  import { cv } from "../cv-provider.js";
3
+ /** Options for the global threshold operation. */
3
4
  export interface ThresholdOptions extends PartialOptions {
4
5
  /** Lower threshold value (0-255) */
5
6
  lower: number;
@@ -8,4 +9,5 @@ export interface ThresholdOptions extends PartialOptions {
8
9
  /** Type of thresholding (cv.THRESH_...) */
9
10
  type: cv.ThresholdTypes;
10
11
  }
12
+ /** Apply a global threshold to convert a grayscale image to binary. */
11
13
  export declare function threshold(img: cv.Mat, options: ThresholdOptions): OperationResult;
@@ -1,6 +1,7 @@
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
+ /** Options for the perspective warp (four-point transform) operation. */
4
5
  export interface WarpOptions extends RequiredOptions {
5
6
  /** Four points of the source image containing x and y point in
6
7
  * topLeft, topRight, bottomLeft and BottomRight.
@@ -10,4 +11,5 @@ export interface WarpOptions extends RequiredOptions {
10
11
  /** A destination canvas bounding box for cropping the original canvas */
11
12
  bbox: BoundingBox;
12
13
  }
14
+ /** Apply a perspective warp using four source/destination corner points. */
13
15
  export declare function warp(img: cv.Mat, options: WarpOptions): OperationResult;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ppu-ocv",
3
- "version": "3.1.2",
3
+ "version": "3.1.4",
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",
@@ -49,7 +49,7 @@
49
49
  "build:test": "bun task build && bun test",
50
50
  "build:publish": "bun task build && bun task report-size && bun task publish",
51
51
  "type-check": "bunx tsgo --noEmit",
52
- "test": "bun test",
52
+ "test": "bun test --parallel=$(ls tests/*.test.ts | wc -l | tr -d ' ')",
53
53
  "lint": "oxlint",
54
54
  "lint:fix": "oxlint --fix",
55
55
  "fmt": "oxfmt --check",
@@ -61,6 +61,7 @@
61
61
  "@types/bun": "latest",
62
62
  "@types/uglify-js": "latest",
63
63
  "@typescript/native-preview": "^7.0.0-dev.20260513.1",
64
+ "canvas": "^3.2.3",
64
65
  "husky": "^9.1.7",
65
66
  "lint-staged": "^16.0.0",
66
67
  "mitata": "latest",
@@ -1,13 +1,39 @@
1
1
  import type { cv } from "../cv-provider.js";
2
2
  import type { OperationFunction, OperationName, OperationOptions, OperationResult } from "./index.js";
3
+ /**
4
+ * Registry that maps operation names to their implementation functions and
5
+ * default-option factories. Operation files call {@link OperationRegistry.register}
6
+ * at module-load time to make themselves available to {@link executeOperation}.
7
+ */
3
8
  export declare class OperationRegistry {
4
9
  private operations;
5
10
  private defaultOptions;
11
+ /**
12
+ * Register a named operation.
13
+ * @param name Unique operation name (must match a key in {@link RegisteredOperations}).
14
+ * @param operation The function that performs the operation.
15
+ * @param defaultOptions Optional factory returning default option values.
16
+ */
6
17
  register<Name extends OperationName>(name: Name, operation: OperationFunction<OperationOptions<Name>>, defaultOptions?: () => Partial<OperationOptions<Name>>): void;
18
+ /** Look up the implementation for an operation by name. */
7
19
  getOperation(name: string): OperationFunction<any> | undefined;
20
+ /** Return the default-options factory for an operation, or an empty object if none was registered. */
8
21
  getDefaultOptionsGenerator(name: string): any;
22
+ /** Return `true` if an operation with the given name has been registered. */
9
23
  hasOperation(name: string): boolean;
24
+ /** Return the names of all registered operations. */
10
25
  getOperationNames(): OperationName[];
11
26
  }
27
+ /** Singleton registry populated by each `src/operations/*.ts` module at load time. */
12
28
  export declare const registry: OperationRegistry;
29
+ /**
30
+ * Look up and execute a registered operation, merging caller-supplied options
31
+ * with the operation's defaults.
32
+ *
33
+ * @param operationName Name of the registered operation.
34
+ * @param img Input OpenCV Mat. The Mat is consumed (deleted) by the operation.
35
+ * @param options Partial options to merge with the operation's defaults.
36
+ * @returns The operation result containing the transformed Mat and its dimensions.
37
+ * @throws {Error} If no operation with `operationName` is registered.
38
+ */
13
39
  export declare function executeOperation<Name extends OperationName>(operationName: Name, img: cv.Mat, options?: Partial<OperationOptions<Name>>): OperationResult;
@@ -13,19 +13,33 @@ import type { ResizeOptions } from "../operations/resize.js";
13
13
  import type { RotateOptions } from "../operations/rotate.js";
14
14
  import type { ThresholdOptions } from "../operations/threshold.js";
15
15
  import type { WarpOptions } from "../operations/warp.js";
16
+ /** The output produced by every pipeline operation: the transformed Mat plus its dimensions. */
16
17
  export interface OperationResult {
18
+ /** Resulting OpenCV Mat after the operation. The caller is responsible for deleting it. */
17
19
  img: cv.Mat;
20
+ /** Width of the resulting image in pixels. */
18
21
  width: number;
22
+ /** Height of the resulting image in pixels. */
19
23
  height: number;
20
24
  }
21
25
  declare const RequiredBrand: unique symbol;
26
+ /**
27
+ * Marker interface for operation options that have no usable defaults and
28
+ * must be supplied by the caller. Operation option types that extend this
29
+ * cannot be omitted when calling {@link ImageProcessor.execute}.
30
+ */
22
31
  export interface RequiredOptions {
23
32
  [RequiredBrand]?: never;
24
33
  }
25
34
  declare const PartialBrand: unique symbol;
35
+ /**
36
+ * Marker interface for operation options that have sensible defaults.
37
+ * Operation option types that extend this may be omitted or partially supplied.
38
+ */
26
39
  export interface PartialOptions {
27
40
  [PartialBrand]?: never;
28
41
  }
42
+ /** Signature every registered operation function must conform to. */
29
43
  export type OperationFunction<T> = (img: cv.Mat, options: T) => OperationResult;
30
44
  /**
31
45
  * Central registry mapping operation names to their option types. Each entry
@@ -40,21 +54,37 @@ export type OperationFunction<T> = (img: cv.Mat, options: T) => OperationResult;
40
54
  * it stays an interface rather than a type alias.
41
55
  */
42
56
  export interface RegisteredOperations {
57
+ /** Adaptive (windowed) thresholding. See {@link AdaptiveThresholdOptions}. */
43
58
  adaptiveThreshold: AdaptiveThresholdOptions;
59
+ /** Gaussian blur. See {@link BlurOptions}. */
44
60
  blur: BlurOptions;
61
+ /** Constant-color border around the image. See {@link BorderOptions}. */
45
62
  border: BorderOptions;
63
+ /** Canny edge detection. See {@link CannyOptions}. */
46
64
  canny: CannyOptions;
65
+ /** Convert Mat depth/channel type. See {@link ConvertOptions}. */
47
66
  convert: ConvertOptions;
67
+ /** Morphological dilation. See {@link DilateOptions}. */
48
68
  dilate: DilateOptions;
69
+ /** Morphological erosion. See {@link ErodeOptions}. */
49
70
  erode: ErodeOptions;
71
+ /** Convert to grayscale via `COLOR_RGBA2GRAY`. See {@link GrayscaleOptions}. */
50
72
  grayscale: GrayscaleOptions;
73
+ /** Bitwise-NOT color inversion. See {@link InvertOptions}. */
51
74
  invert: InvertOptions;
75
+ /** Morphological gradient (dilation minus erosion). See {@link MorphologicalGradientOptions}. */
52
76
  morphologicalGradient: MorphologicalGradientOptions;
77
+ /** Resize to absolute pixel dimensions. See {@link ResizeOptions}. */
53
78
  resize: ResizeOptions;
79
+ /** Affine rotation around a pivot point. See {@link RotateOptions}. */
54
80
  rotate: RotateOptions;
81
+ /** Global threshold (including Otsu). See {@link ThresholdOptions}. */
55
82
  threshold: ThresholdOptions;
83
+ /** Four-point perspective warp. See {@link WarpOptions}. */
56
84
  warp: WarpOptions;
57
85
  }
86
+ /** Union of all registered operation names. Extend {@link RegisteredOperations} to add new ones. */
58
87
  export type OperationName = keyof RegisteredOperations;
88
+ /** Resolve the options type for a given operation name. */
59
89
  export type OperationOptions<N extends OperationName> = RegisteredOperations[N];
60
90
  export {};