UNPKG

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

1 lines 6.08 kB
import{decodeResults}from"./ctc.js";import{MIN_CROP_WIDTH}from"./ctc.js";import{preprocessImage}from"./image-tensor.js";import{distributeLineText,groupBoxesIntoLines,mergeLineCrop,packIntoBatches,splitBatchTextByWidths}from"./line-grouping.js";function cropRegion(sourceCanvas,box,canvasOps){return canvasOps.getToolkit().crop({bbox:{x0:box.x,y0:box.y,x1:box.x+box.width,y1:box.y+box.height},canvas:sourceCanvas})}async function recognizeText(cropCanvas,ctx,charactersDictionary){let targetHeight=ctx.options.imageHeight??48;let imageProcessor=ctx.engine==="opencv"?ctx.platform.imageProcessor:undefined;const{imageTensor,tensorWidth,tensorHeight}=await preprocessImage(cropCanvas,targetHeight,imageProcessor,ctx.platform.canvas.createProcessor.bind(ctx.platform.canvas));let inputTensor;try{inputTensor=new ctx.platform.ort.Tensor("float32",imageTensor,[1,3,tensorHeight,tensorWidth]);let result=await ctx.runInference(inputTensor);let dict=charactersDictionary??ctx.options.charactersDictionary??[];return decodeResults(result,dict,tensorWidth,ctx.debugging.verbose)}finally{inputTensor?.dispose()}}function sortByReadingOrder(results){return[...results].sort((a,b)=>{if(Math.abs(a.box.y-b.box.y)<(a.box.height+b.box.height)/4){return a.box.x-b.box.x}return a.box.y-b.box.y})}export async function runPerBoxStrategy(sourceCanvas,validBoxes,ctx,processBox,charactersDictionary){let cropsDebugPath=ctx.debugging.debugFolder?`${ctx.debugging.debugFolder}${ctx.platform.pathSeparator}crops`:"";if(ctx.debugging.debug&&cropsDebugPath){let toolkit=ctx.platform.canvas.getToolkit();if("clearOutput"in toolkit&&typeof toolkit.clearOutput==="function"){toolkit.clearOutput(cropsDebugPath)}}let results=[];for(const{box,index}of validBoxes){let result=await processBox(sourceCanvas,box,index,validBoxes.length,cropsDebugPath,charactersDictionary);if(result!==null){results.push(result)}}return sortByReadingOrder(results)}export async function runLineStrategy(sourceCanvas,validBoxes,ctx,charactersDictionary){let lines=groupBoxesIntoLines(validBoxes);let results=[];for(let lineBoxes of lines){if(lineBoxes.length===1){let lineBox=lineBoxes[0];if(!lineBox)continue;const{box}=lineBox;let cropCanvas=cropRegion(sourceCanvas,box,ctx.platform.canvas);const{text,confidence}=await recognizeText(cropCanvas,ctx,charactersDictionary);results.push({text,box,confidence})}else{const{mergedCanvas}=mergeLineCrop(sourceCanvas,lineBoxes,ctx.platform.createCanvas.bind(ctx.platform),ctx.platform.canvas);const{text:lineText,confidence:lineConf}=await recognizeText(mergedCanvas,ctx,charactersDictionary);let totalWidth=lineBoxes.reduce((sum,b)=>sum+b.box.width,0);let words=lineText.trim().split(/\s+/).filter((w)=>w.length>0);if(words.length===0||lineBoxes.length===0){for(const{box}of lineBoxes){results.push({text:lineText,box,confidence:lineConf})}}else if(words.length>=lineBoxes.length){let wordIdx=0;for(let i=0;i<lineBoxes.length;i++){let lb=lineBoxes[i];if(!lb)continue;let proportion=lb.box.width/totalWidth;let wordsForBox=Math.max(1,Math.round(words.length*proportion));let end=Math.min(wordIdx+wordsForBox,words.length);results.push({text:words.slice(wordIdx,end).join(" "),box:lb.box,confidence:lineConf});wordIdx=end}if(wordIdx<words.length){let lastResult=results[results.length-1];if(lastResult)lastResult.text+=` ${words.slice(wordIdx).join(" ")}`}}else{for(const{box}of lineBoxes.slice(0,words.length)){results.push({text:words.shift()??"",box,confidence:lineConf})}for(const{box}of lineBoxes.slice(words.length)){results.push({text:"",box,confidence:lineConf})}}}}return sortByReadingOrder(results)}export async function runCrossLineStrategy(sourceCanvas,validBoxes,ctx,charactersDictionary){let lines=groupBoxesIntoLines(validBoxes);let targetHeight=ctx.options.imageHeight??48;let SEPARATOR_GAP=20;let lineCrops=[];for(let lineBoxes of lines){if(lineBoxes.length===1){let first=lineBoxes[0];if(!first)continue;lineCrops.push({canvas:cropRegion(sourceCanvas,first.box,ctx.platform.canvas),boxes:lineBoxes})}else{const{mergedCanvas}=mergeLineCrop(sourceCanvas,lineBoxes,ctx.platform.createCanvas.bind(ctx.platform),ctx.platform.canvas);lineCrops.push({canvas:mergedCanvas,boxes:lineBoxes})}}let resized=lineCrops.map(({canvas,boxes},i)=>{let ar=canvas.width/canvas.height;let resizedWidth=Math.max(MIN_CROP_WIDTH,Math.round(targetHeight*ar));return{canvas,boxes,resizedWidth,originalHeight:canvas.height,index:i}});let maxWidth=Math.max(...resized.map((r)=>r.resizedWidth));let widthFactor=ctx.options.crossLineWidthFactor??1.5;let batchTargetWidth=Math.round(maxWidth*widthFactor);let batches=packIntoBatches(resized,(item)=>item.resizedWidth,batchTargetWidth,SEPARATOR_GAP);let results=[];for(let batch of batches){let batchSorted=[...batch].sort((a,b)=>a.index-b.index);let maxOriginalHeight=Math.max(...batchSorted.map((item)=>item.originalHeight));let stretchedWidths=batchSorted.map((item)=>{if(item.originalHeight>=maxOriginalHeight)return item.resizedWidth;let heightScale=maxOriginalHeight/item.originalHeight;return Math.max(MIN_CROP_WIDTH,Math.round(item.resizedWidth*heightScale))});let totalCropWidth=stretchedWidths.reduce((sum,w)=>sum+w,0);let totalWidth=totalCropWidth+SEPARATOR_GAP*(batchSorted.length-1);let batchCanvas=ctx.platform.createCanvas(totalWidth,targetHeight);let bctx=batchCanvas.getContext("2d");bctx.fillStyle="white";bctx.fillRect(0,0,totalWidth,targetHeight);let offsetX=0;for(let i=0;i<batchSorted.length;i++){let item=batchSorted[i];let drawWidth=stretchedWidths[i];if(item===undefined||drawWidth===undefined)continue;bctx.drawImage(item.canvas,0,0,item.canvas.width,item.canvas.height,offsetX,0,drawWidth,targetHeight);offsetX+=drawWidth;if(i<batchSorted.length-1)offsetX+=SEPARATOR_GAP}const{text:batchText,confidence:batchConf}=await recognizeText(batchCanvas,ctx,charactersDictionary);let lineTexts=splitBatchTextByWidths(batchText,stretchedWidths);for(let i=0;i<batchSorted.length;i++){let item=batchSorted[i];if(!item)continue;results.push(...distributeLineText(item.boxes,lineTexts[i]??"",batchConf))}}return sortByReadingOrder(results)}