ppu-paddle-ocr
Version:
Lightweight, probably the fastest PaddleOCR SDK in TypeScript. Runs anywhere JavaScript runs: Node.js, Bun, Deno, mobile react-native, web browsers, and browser extensions. Docker & CLI supported. The official SDK is browser-only. Accurate text detection
2 lines • 5.62 kB
JavaScript
export class BasePaddleOcrService{options=DEFAULT_PADDLE_OPTIONS;detectionSession=null;recognitionSession=null;detector=null;recognitor=null;platform;constructor(platform,options){this.platform=platform;this.options=deepMerge({},DEFAULT_PADDLE_OPTIONS,options);this.options.session=this.options.session||DEFAULT_PADDLE_OPTIONS.session}log(message){if(this.options.debugging?.verbose){console.log(`[PaddleOcrService:Base] ${message}`)}}async recognize(image,options){if(!this.detector||!this.recognitor){await this.initSessions()}try{let imageBuffer;if(typeof image==="string"){if(!image.startsWith("http")&&!image.startsWith("/")){throw new Error("Invalid image string format. Must be an HTTP URL, an absolute path, ArrayBuffer, or Canvas")}imageBuffer=await this.platform.loadResource(image,image)}else if(image instanceof ArrayBuffer){imageBuffer=image}else{if(typeof image.toBuffer==="function"){let canvasWithBuffer=image;let buffer=canvasWithBuffer.toBuffer("image/png");imageBuffer=buffer.buffer.slice(buffer.byteOffset,buffer.byteOffset+buffer.byteLength)}else{let canvasWithCtx=image;let ctx=canvasWithCtx.getContext("2d",{willReadFrequently:true});let imageData=ctx.getImageData(0,0,canvasWithCtx.width,canvasWithCtx.height);let data=imageData.data;imageBuffer=data.buffer.slice(data.byteOffset,data.byteOffset+data.byteLength)}}let cacheKey=ImageCache.generateKey(imageBuffer);if(!options?.noCache&&!options?.dictionary){let cacheResult=globalImageCache.get(cacheKey);if(cacheResult){this.log("Using cached OCR result");if(options?.flatten){return{text:cacheResult.text,results:cacheResult.lines?cacheResult.lines.flat():cacheResult.results??[],confidence:cacheResult.confidence}}return cacheResult}}let boxes=[];let canvas=typeof image==="string"||image instanceof ArrayBuffer?await this.platform.canvas.prepareCanvas(imageBuffer):image;boxes=await this.detector.run(canvas);if(boxes.length===0){return options?.flatten?{text:"",results:[],confidence:0}:{text:"",lines:[],confidence:0}}let dict=this.options.recognition?.charactersDictionary;if(options?.dictionary){let dictionaryContent="";if(typeof options.dictionary==="string"){let dictBuffer=await this.platform.loadResource(options.dictionary,options.dictionary);dictionaryContent=new TextDecoder("utf-8").decode(dictBuffer)}else{dictionaryContent=new TextDecoder("utf-8").decode(options.dictionary)}dict=parseDictionary(dictionaryContent)}let strategy=options?.strategy??this.options.recognition?.strategy??"per-line";let results=await this.recognitor.run(canvas,boxes,dict,strategy);let groupedResult=this.groupResultsByLine(results);let finalResult=options?.flatten?this.flattenResults(results):groupedResult;if(!options?.noCache&&!options?.dictionary){globalImageCache.set(cacheKey,finalResult)}return finalResult}catch(e){let err=e instanceof Error?e:new Error(String(e));console.error("recognize: error",err.message,err.stack);throw e}}async batchRecognize(images,options){let settle=options?.settle??false;let collected=[];await runPool(images,{concurrency:this.resolveConcurrency(options?.concurrency),settle,signal:options?.signal,onProgress:options?.onProgress,total:Array.isArray(images)?images.length:undefined},(image)=>this.recognize(image,options),(result)=>{collected[result.index]=result});if(settle)return collected;return collected.map((item)=>item.status==="fulfilled"?item.value:undefined)}async*batchRecognizeStream(images,options){let queue=createAsyncQueue();let pump=(async()=>{try{await runPool(images,{concurrency:this.resolveConcurrency(options?.concurrency),settle:options?.settle??false,signal:options?.signal,onProgress:options?.onProgress,total:Array.isArray(images)?images.length:undefined},(image)=>this.recognize(image,options),(result)=>queue.push(result));queue.close()}catch(error){queue.fail(error)}})();yield*queue.drain();await pump}resolveConcurrency(value){if(typeof value==="number"&&value>0)return Math.floor(value);let providers=this.options.session?.executionProviders??[];let usesAccelerator=providers.some((provider)=>{let name=(typeof provider==="string"?provider:provider.name).toLowerCase();return name!=="cpu"&&name!=="wasm"});return usesAccelerator?1:4}flattenResults(results){if(results.length===0){return{text:"",results:[],confidence:0}}let text=results.map((r)=>r.text).join(" ");let avgConfidence=results.reduce((sum,r)=>sum+r.confidence,0)/results.length;return{text,results,confidence:avgConfidence}}groupResultsByLine(results){if(results.length===0){return{text:"",lines:[],confidence:0}}let lines=[];let currentLine=[];let firstResult=results[0];if(!firstResult)return{text:"",lines:[],confidence:0};let currentY=firstResult.box.y;let avgHeight=firstResult.box.height;for(let result of results){const{box}=result;if(Math.abs(box.y-currentY)<avgHeight/2){currentLine.push(result);avgHeight=(avgHeight*(currentLine.length-1)+box.height)/currentLine.length}else{currentLine.sort((a,b)=>a.box.x-b.box.x);lines.push(currentLine);currentLine=[result];currentY=box.y;avgHeight=box.height}}if(currentLine.length>0){currentLine.sort((a,b)=>a.box.x-b.box.x);lines.push(currentLine)}let fullText=lines.map((line)=>line.map((r)=>r.text).join(" ")).join(`
`);let totalConfidence=lines.reduce((sum,line)=>sum+line.reduce((s,r)=>s+r.confidence,0),0);let totalItems=lines.reduce((sum,line)=>sum+line.length,0);return{text:fullText,lines,confidence:totalItems>0?totalConfidence/totalItems:0}}}import{DEFAULT_PADDLE_OPTIONS}from"../constants.js";import{deepMerge,parseDictionary}from"../utils.js";import{createAsyncQueue,runPool}from"./batch.js";import{globalImageCache,ImageCache}from"./image-cache.js";