toolgarden.xyz
中文
OCR architecturePP-OCRv5PaddleOCR JSONNX Runtimetechnical selection

OCR Technology Choices and a Production PP-OCRv5 Implementation

Compare cloud OCR, PaddleOCR, Tesseract, and custom ONNX pipelines, then follow a real PP-OCRv5 implementation from model delivery and Workers to debugging and deployment.

ToolGarden tools prioritize browser-local processing, so files and text do not need to be uploaded to a server.

Published September 15, 202622 min readBy ToolGarden

There is no universally best OCR engine. The right choice depends on document complexity, language coverage, privacy constraints, traffic, latency, available infrastructure, and how much of the recognition pipeline a team is prepared to maintain.

This article starts with the decision rather than the code. It compares common OCR approaches, explains which environments each one fits, and then records how ToolGarden implemented PP-OCRv5 with PaddleOCR JS and ONNX Runtime. The browser is the concrete case study, not a restriction on the conclusions: the same evaluation method applies to servers, desktop applications, mobile clients, internal systems, and public web products.

Start with requirements, not a model name

OCR quality is not one number. A model can read a clean English scan well and fail on Chinese storefront text, rotated receipts, dense tables, handwriting, or a low-light phone photo. Before selecting a stack, build a representative evaluation set and define what a useful output means: plain text, positioned blocks, tables, key-value fields, searchable PDF, or a fully reconstructed document.

Operational constraints are equally important. Ask whether images may leave the device, whether the product must work offline, how much first-load download is acceptable, which browsers or operating systems must be supported, whether GPUs are available, and who will update models and runtime binaries. These answers often eliminate more options than an accuracy demo does.

  • Accuracy: languages, fonts, rotation, perspective, handwriting, tables, and small text.
  • Output: plain text, coordinates, reading order, structured fields, or layout recovery.
  • Operations: latency, throughput, cold start, memory, package size, and offline behavior.
  • Governance: upload policy, data region, retention, auditability, and vendor dependency.
  • Ownership: integration effort, model upgrades, preprocessing, post-processing, and testing.

Compare the main OCR implementation routes

The useful comparison is not simply local versus cloud. A cloud document API, a Python PaddleOCR service, Tesseract, PaddleOCR JS, and a custom ONNX pipeline expose different abstraction levels. They move cost between infrastructure, network transfer, client resources, engineering time, and vendor dependency.

This table is a selection guide, not a universal ranking. Accuracy must be measured on the same images, languages, preprocessing, and output requirements. Comparing a cloud table parser with a local plain-text recognizer answers the wrong question.

RouteBest fitStrengthsCosts and limits
Cloud OCR or document APIForms, tables, IDs, receipts, rapid deliveryManaged scaling, structured extraction, little client computeUploads, recurring cost, latency, retention review, lock-in
PaddleOCR Python or native serviceServers, desktop backends, private infrastructureComplete open pipeline, broad controls, easier GPU useNative dependencies, service operations, larger deployment
PaddleOCR JS with PP-OCRv5Web, Electron, offline-first, no-upload productsLocal inference, reusable pipeline, static deliveryWASM speed, model download, browser memory and compatibility
Tesseract or Tesseract.jsClean scans, simple layouts, established language packsMature ecosystem and predictable classic workflowUsually weaker on scene text and complex layouts
Custom ONNX pipelineSpecial models, hardware, or strict controlMaximum control over tensors, batching, and outputHighest pre/post-processing and compatibility maintenance

Choose by environment and product boundary

When uploads are acceptable and structured tables or form semantics are required immediately, a managed document API is often the shortest path. When data must remain inside controlled infrastructure and GPU throughput matters, PaddleOCR Python or another native stack is usually a better server foundation than forcing a browser runtime onto the backend.

Electron can reuse JavaScript and WASM, while a desktop sidecar can run Python or C++. Mobile products should benchmark native runtimes and device acceleration. Tesseract remains reasonable for clean, predictable scans. A custom ONNX pipeline is justified only when the extra control solves a measured problem.

ToolGarden chose PaddleOCR JS with PP-OCRv5 because its boundary was explicit: static hosting, no image upload, no OCR backend, multilingual printed text, and an acceptable one-time model download. Under different constraints, the recommendation changes.

Implementation path: establish a baseline first

Build a small golden set before writing adapters. Include screenshots, phone photos, simplified and traditional Chinese, English, Japanese, small type, rotation, low contrast, and at least one layout the product does not promise to preserve. Record expected text and critical fields rather than relying on visual impression.

Measure cold initialization, warm recognition, peak memory, model transfer size, detected blocks, and character or field accuracy. Run every candidate on the same originals. This is the only fair way to compare a website or model because preprocessing, detection, decoding, and reading order can matter as much as archive size.

MeasurementWhy it mattersTest condition
Cold startIncludes runtime, download, extraction, and sessionsFresh cache and slow network
Warm latencyRepresents repeated useSecond and later images
Recognition qualityShows substitutions, omissions, and ordering errorsEach language and image category
Resource useReveals mobile instabilityLarge image and many text boxes
Failure recoveryConfirms retry really worksOffline, corrupt asset, terminated Worker

Implementation path: isolate expensive work and define a protocol

The page owns UI state, validation, progress, and localized errors. A module Worker owns decoding, OpenCV preprocessing, model initialization, inference, and result conversion. Every message carries an ID so stale progress cannot settle a newer request.

The file becomes a transferable ArrayBuffer, moving ownership instead of cloning a large image. Temporary listeners are removed on completion. A crashed or timed-out Worker is terminated and uncached so retry starts from clean WASM state.

const worker = new Worker(
  new URL('../workers/ocr-accurate.worker.ts', import.meta.url),
  { type: 'module' },
);
const data = await file.arrayBuffer();

worker.postMessage({
  id: requestId,
  type: 'recognize',
  file: { data, type: file.type, name: file.name, size: file.size },
  language,
}, [data]);

Implementation path: adapt decoding to the runtime

A Worker provides Blob, createImageBitmap, ImageData, and OffscreenCanvas, but not document or HTML element constructors. Some image dependencies still check those globals. That difference caused document is not defined and HTMLImageElement is not defined.

The adapter installs only the globals the dependency reads. Canvas maps to OffscreenCanvas; sourceToMat decodes a Blob, draws it offscreen, reads ImageData, and creates an OpenCV Mat. Disposal deletes the Mat and closes the bitmap. Node, Electron main processes, and native applications should replace this adapter with their own decoder rather than expanding the shim.

function installWorkerCanvasDomShim() {
  Object.defineProperty(globalThis, 'HTMLCanvasElement', {
    configurable: true,
    value: OffscreenCanvas,
  });
  Object.defineProperty(globalThis, 'document', {
    configurable: true,
    value: {
      createElement(tagName: string) {
        if (tagName.toLowerCase() !== 'canvas') throw new Error('Unsupported element');
        return new OffscreenCanvas(1, 1);
      },
    },
  });
}

Implementation path: load PP-OCRv5 as one versioned unit

Exact detection and recognition assets are passed to PaddleOCR JS instead of relying on remote defaults. Languages map to Paddle identifiers and initialized OCR instances are cached by language. Detection and recognition batch sizes can be tuned separately.

ONNX Runtime uses WASM with SIMD, no proxy, and one thread. One thread avoids cross-origin-isolation requirements. The shown side limit and thresholds are product tuning values, not universal constants; tiny text, scene images, and mobile memory limits need their own benchmark.

const ocr = await PaddleOCR.create({
  lang: toPaddleLanguage(language),
  ocrVersion: 'PP-OCRv5',
  textDetectionModelName: 'PP-OCRv5_mobile_det',
  textDetectionModelAsset: {
    url: '/models/paddleocr/ppocr-v5/PP-OCRv5_mobile_det_onnx_infer.tar',
  },
  textRecognitionModelName: 'PP-OCRv5_mobile_rec',
  textRecognitionModelAsset: {
    url: '/models/paddleocr/ppocr-v5/PP-OCRv5_mobile_rec_onnx_infer.tar',
  },
  sourceToMat: sourceToMatInWorker,
  ortOptions: {
    backend: 'wasm',
    wasmPaths: {
      mjs: '/models/paddleocr/onnxruntime-web/ort-wasm-simd-threaded.mjs',
      wasm: '/models/paddleocr/onnxruntime-web/ort-wasm-simd-threaded.wasm',
    },
    numThreads: 1,
    simd: true,
    proxy: false,
  },
});

const [result] = await ocr.predict(image, {
  textDetLimitSideLen: 1280,
  textDetThresh: 0.24,
  textDetBoxThresh: 0.34,
  textDetUnclipRatio: 1.8,
  textRecScoreThresh: 0.28,
});

Implementation path: own the result contract

Library output is converted into an application-owned discriminated union. Success contains text, blocks, confidence, boxes, source dimensions, and duration. Failures use stable codes such as model_load_failed, worker_timeout, recognition_failed, and no_text_detected, so UI copy never parses exceptions.

Polygons become display boxes, weak items are filtered, and blocks are grouped into rows by vertical center and average height. Rows sort top to bottom and blocks left to right. This provides useful plain text, but it does not claim to reconstruct tables, columns, or original document styling.

Important details: liveness, delivery, caching, and memory

A fixed timeout confuses slow progress with failure. The Worker sends a heartbeat every ten seconds around initialization and prediction. The page resets an inactivity timer on each matching message and maintains separate hard limits for model and processing phases. Every failure path discards the Worker, so retry is defined.

Self-hosted models remove third-party CORS and availability risk, but URLs, MIME types, cache updates, file limits, and first-load UX remain. ONNX Runtime JavaScript and WASM must come from the same release. OpenCV Mats, ImageBitmaps, URLs, listeners, timers, rejected initialization Promises, and Service Worker caches all require explicit lifecycle management.

async function withProgressHeartbeat(progress, operation) {
  postProgress(progress);
  const heartbeat = setInterval(() => postProgress(progress), 10_000);
  try {
    return await operation();
  } finally {
    clearInterval(heartbeat);
  }
}

const IDLE_TIMEOUT = 180_000;
const MODEL_PHASE_TIMEOUT = 12 * 60_000;
const PROCESSING_PHASE_TIMEOUT = 6 * 60_000;

function failTimeout() {
  worker.terminate();
  cachedWorker = null;
  resolve({ ok: false, code: 'worker_timeout' });
}
  • Pin package and binary versions together and inspect the final bundle.
  • Serve models, MJS, and WASM with stable URLs and correct content types.
  • Cache successful initialization but remove rejected Promises.
  • Limit image pixels before canvas allocation and avoid default concurrency.
  • Version Service Worker caches from built content, not a handwritten constant.

Problems encountered and what they actually meant

Failures occurred at installation, bundling, asset delivery, Worker compatibility, runtime ABI, and caching layers. Treating every message as an isolated npm issue caused rework. The useful debugging move was to identify the layer before changing dependencies.

The final timeout was deceptive. A longer timer could never fix an incompatible runtime. A deterministic image with known text, sent through the production Worker and public asset paths, exposed the hidden _OrtGetInputName failure.

SymptomRoot causeDurable fix
npm edgesOut failureInstaller dependency-tree failureUse exact compatible dependencies and reproducible install mode
Cannot resolve ort.bundle.min.mjsExpected runtime entry was unavailableAlias a real browser entry from the selected release
No matching ORT versionRequested release did not existVerify the registry and pin a published version
WASM exceeded file limitWrong runtime variant entered outputShip only required files and check build sizes
Dynamic MJS fetch failedURL or module deployment was wrongUse same-origin explicit URLs and verify responses
document or HTMLImageElement missingDOM code ran inside a WorkerProvide a narrow OffscreenCanvas adapter
_OrtGetInputName missingJavaScript and WASM ABIs differedVendor and checksum one matching runtime set
Worker timed outFixed timer hid initialization failureExpose layer errors and use heartbeat liveness

Verify the complete path, not only the build

Type checking cannot prove that archives download, MJS locates WASM, or a Worker decodes an image. Verification must use production public paths, cache behavior, Worker entry, and OCR API. Start with a generated image containing known text, then use the golden set for quality.

Test cold and warm caches, slow network, offline-after-cache, rotation, languages, large images, forced Worker termination, and retry. Validate deployments on a fresh origin or after clearing the Service Worker. Build-time SHA-256 checks prevent missing or mixed assets from reaching users.

for (const asset of pinnedOcrAssets) {
  const actual = createHash('sha256')
    .update(readFileSync(asset.path))
    .digest('hex');

  if (actual !== asset.sha256) {
    throw new Error(`OCR asset checksum mismatch: ${asset.path}`);
  }
}

Summary

Choose OCR by task and operating boundary, not by model size or one demo. Cloud document APIs fit managed structured extraction. Native PaddleOCR fits controlled servers and GPU workloads. Tesseract remains useful for predictable scans. PaddleOCR JS with PP-OCRv5 fits local web delivery. In every route, preprocessing, runtime compatibility, result contracts, liveness, caching, memory, and end-to-end verification are parts of the OCR system. That decision trail is more reusable than any single code sample.

Frequently asked questions

Q.Is PP-OCRv5 always more accurate than Tesseract or cloud OCR?

No. Evaluate candidates on the same representative files and score the languages, layouts, and fields that matter to the product.

Q.Should a server project use PaddleOCR JS?

Not by default. Servers with Python or native support can usually use the full PaddleOCR stack and acceleration more directly. JavaScript and WASM fit code sharing, sandboxing, Electron, or browser delivery.

Q.Does local OCR mean completely offline?

Only after the application, runtime, models, and related assets are cached or bundled. First use normally requires a download.

Q.Why can a smaller model appear better?

Detection, crop quality, vocabulary, decoding, thresholds, reading order, preprocessing, and quantization can matter more than archive size on a particular image.

Q.Why use a Worker?

It isolates expensive image and WASM work from rendering and provides a boundary that can be terminated and recreated after failure.

Q.What must be checked after an OCR dependency update?

Verify the deployed JavaScript, MJS, WASM, model hashes, URLs, MIME types, cold start, real recognition, retry, and Service Worker replacement.