优化后端

This commit is contained in:
Miayui
2026-09-11 17:56:56 +08:00
parent 7eed11b774
commit 812e67422d
7299 changed files with 1744959 additions and 112 deletions
+201
View File
@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "{}"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright {yyyy} {name of copyright owner}
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+198
View File
@@ -0,0 +1,198 @@
<p align="center">
<a href="https://tesseract.projectnaptha.com/">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./docs/images/tesseract_dark.png">
<img width="256px" height="256px" alt="Tesseract.js" src="./docs/images/tesseract.png">
</picture>
</a>
</p>
![Lint & Test](https://github.com/naptha/tesseract.js/workflows/Node.js%20CI/badge.svg)
![CodeQL](https://github.com/naptha/tesseract.js/workflows/CodeQL/badge.svg)
[![Gitpod Ready-to-Code](https://img.shields.io/badge/Gitpod-ready--to--code-blue?logo=gitpod)](https://github.com/naptha/tesseract.js)
[![Financial Contributors on Open Collective](https://opencollective.com/tesseractjs/all/badge.svg?label=financial+contributors)](https://opencollective.com/tesseractjs) [![npm version](https://badge.fury.io/js/tesseract.js.svg)](https://badge.fury.io/js/tesseract.js)
[![Maintenance](https://img.shields.io/badge/Maintained%3F-yes-green.svg)](https://github.com/naptha/tesseract.js/graphs/commit-activity)
[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
[![Code Style](https://badgen.net/badge/code%20style/airbnb/ff5a5f?icon=airbnb)](https://github.com/airbnb/javascript)
![npm](https://img.shields.io/npm/dm/tesseract.js?label=npm%20downloads)
![jsDelivr hits (npm)](https://img.shields.io/jsdelivr/npm/hm/tesseract.js?label=jsdelivr%20hits)
Tesseract.js is a javascript library that gets words in [almost any language](./docs/tesseract_lang_list.md) out of images. ([Demo](http://tesseract.projectnaptha.com/))
Image Recognition
[![fancy demo gif](./docs/images/demo.gif)](http://tesseract.projectnaptha.com)
Video Real-time Recognition
<p align="center">
<a href="https://github.com/jeromewu/tesseract.js-video"><img alt="Tesseract.js Video" src="./docs/images/video-demo.gif"></a>
</p>
Tesseract.js works in the browser using [webpack](https://webpack.js.org/), esm, or plain script tags with a [CDN](#CDN) and on the server with [Node.js](https://nodejs.org/en/).
After you [install it](#installation), using it is as simple as:
```javascript
import { createWorker } from 'tesseract.js';
(async () => {
const worker = await createWorker('eng');
const ret = await worker.recognize('https://tesseract.projectnaptha.com/img/eng_bw.png');
console.log(ret.data.text);
await worker.terminate();
})();
```
When recognizing multiple images, users should create a worker once, run `worker.recognize` for each image, and then run `worker.terminate()` once at the end (rather than running the above snippet for every image).
## Installation
Tesseract.js works with a `<script>` tag via local copy or CDN, with webpack via `npm` and on Node.js with `npm/yarn`.
### CDN
```html
<!-- v5 -->
<script src='https://cdn.jsdelivr.net/npm/tesseract.js@5/dist/tesseract.min.js'></script>
```
After including the script the `Tesseract` variable will be globally available and a worker can be created using `Tesseract.createWorker`.
Alternatively, an ESM build (used with `import` syntax) can be found at `https://cdn.jsdelivr.net/npm/tesseract.js@5/dist/tesseract.esm.min.js`.
### Node.js
**Tesseract.js v7 requires Node.js v16 or newer.** (Tesseract.js v6 requires Node.js v14 or newer.)
```shell
# For latest version
npm install tesseract.js
yarn add tesseract.js
# For old versions
npm install tesseract.js@3.0.3
yarn add tesseract.js@3.0.3
```
## Project Scope
Tesseract.js aims to bring the [Tesseract](https://github.com/tesseract-ocr/tesseract) OCR engine (a separate project) to the browser and Node.js, and works by wrapping a [WebAssembly port](https://github.com/naptha/tesseract.js-core) of Tesseract. This project does not modify core Tesseract features. Most notably, **Tesseract.js does not support PDF files and does not modify the Tesseract recognition model to improve accuracy.**
If your project requires features outside of this scope, consider the [Scribe.js library](https://github.com/scribeocr/scribe.js). Scribe.js is an alternative library created to accommodate common feature requests that are outside of the scope of this repo. Scribe.js includes improvements to the Tesseract recognition model and supports extracting text from PDF documents, among other features. For more information see [Scribe.js vs. Tesseract.js](https://github.com/scribeocr/scribe.js/blob/master/docs/scribe_vs_tesseract.md).
## Documentation
* [Workers vs. Schedulers](./docs/workers_vs_schedulers.md)
* [Examples](./docs/examples.md)
* [Supported Image Formats](./docs/image-format.md)
* [API](./docs/api.md)
* [Local Installation](./docs/local-installation.md)
* [FAQ](./docs/faq.md)
## Community Projects and Examples
The following are examples and projects built by the community using Tesseract.js. Officially supported examples are found in the [examples](https://github.com/naptha/tesseract.js/tree/master/examples) directory.
- Projects
- Scribe OCR: web application for scanning documents (images and PDFs)
- Site at [scribeocr.com](https://scribeocr.com/), repo at [github.com/scribeocr/scribeocr](https://github.com/scribeocr/scribeocr)
- Chrome Extension (with Manifest V3): https://github.com/Tshetrim/Image-To-Text-OCR-extension-for-ChatGPT
- Examples
- Converting PDF to text: https://github.com/racosa/pdf2text-ocr
- Use `blocks` output to generate granular data [word/symbol level]: https://github.com/Kishlay-notabot/tesseract-bbox-examples
- Electron: https://github.com/Balearica/tesseract.js-electron
- Typescript: https://github.com/Balearica/tesseract.js-typescript
If you have a project or example repo that uses Tesseract.js, feel free to add it to this list using a pull request. Examples submitted should be well documented such that new users can run them; projects should be functional and actively maintained.
## Major changes in v6
Version 6 changes are documented in [this issue](https://github.com/naptha/tesseract.js/issues/993). Highlights are below.
- Fixed memory leak in previous versions
- Overall reductions in runtime and memory usage
- Breaking changes:
- All outputs formats other than `text` are disabled by default.
- To re-enable the `hocr` output (for example), set the following: `worker.recognize(image, {}, { hocr: true })`
- Minor changes to the structure of the JavaScript object (`blocks`) output
- See [this issue](https://github.com/naptha/tesseract.js/issues/993) for full list
## Major changes in v5
Version 5 changes are documented in [this issue](https://github.com/naptha/tesseract.js/issues/820). Highlights are below.
- Significantly smaller files by default (54% smaller for English, 73% smaller for Chinese)
- This results in a ~50% reduction in runtime for first-time users (who do not have the files cached yet)
- Significantly lower memory usage
- Breaking changes:
- `createWorker` arguments changed
- Setting non-default language and OEM now happens in `createWorker`
- E.g. `createWorker("chi_sim", 1)`
- `worker.initialize` and `worker.loadLanguage` functions should be deleted from code
- See [this issue](https://github.com/naptha/tesseract.js/issues/820) for full list
Upgrading from v2 to v5? See [this guide](https://github.com/naptha/tesseract.js/issues/771).
## Major changes in v4
Version 4 includes many new features and bug fixes--see [this issue](https://github.com/naptha/tesseract.js/issues/662) for a full list. Several highlights are below.
- Added rotation preprocessing options (including auto-rotate) for significantly better accuracy
- Processed images (rotated, grayscale, binary) can now be retrieved
- Improved support for parallel processing (schedulers)
- Breaking changes:
- `createWorker` is now async
- `getPDF` function replaced by `pdf` recognize option
## Contributing
### Development
To run a development copy of Tesseract.js do the following:
```shell
# First we clone the repository
git clone https://github.com/naptha/tesseract.js.git
cd tesseract.js
# Then we install the dependencies
npm install
# And finally we start the development server
npm start
```
The development server will be available at http://localhost:3000/examples/browser/basic-efficient.html in your favorite browser.
It will automatically rebuild `tesseract.min.js` and `worker.min.js` when you change files in the **src** folder.
### Building Static Files
To build the compiled static files just execute the following:
```shell
npm run build
```
This will output the files into the `dist` directory.
### Run Tests
**Always confirm the automated tests pass before submitting a pull request.** To run the automated tests locally, run the following commands.
```shell
npm run lint
npm run test
```
## Contributors
### Code Contributors
This project exists thanks to all the people who contribute. [[Contribute](https://github.com/naptha/tesseract.js?tab=readme-ov-file#contributing)].
<a href="https://github.com/naptha/tesseract.js/graphs/contributors"><img src="https://opencollective.com/tesseractjs/contributors.svg?width=890&button=false" /></a>
### Financial Contributors
Become a financial contributor and help us sustain our community. [[Contribute](https://opencollective.com/tesseractjs/contribute)]
#### Individuals
<a href="https://opencollective.com/tesseractjs"><img src="https://opencollective.com/tesseractjs/individuals.svg?width=890"></a>
#### Organizations
Support this project with your organization. Your logo will show up here with a link to your website. [[Contribute](https://opencollective.com/tesseractjs/contribute)]
<a href="https://opencollective.com/tesseractjs/organization/0/website"><img src="https://opencollective.com/tesseractjs/organization/0/avatar.svg"></a>
<a href="https://opencollective.com/tesseractjs/organization/1/website"><img src="https://opencollective.com/tesseractjs/organization/1/avatar.svg"></a>
<a href="https://opencollective.com/tesseractjs/organization/2/website"><img src="https://opencollective.com/tesseractjs/organization/2/avatar.svg"></a>
<a href="https://opencollective.com/tesseractjs/organization/3/website"><img src="https://opencollective.com/tesseractjs/organization/3/avatar.svg"></a>
<a href="https://opencollective.com/tesseractjs/organization/4/website"><img src="https://opencollective.com/tesseractjs/organization/4/avatar.svg"></a>
<a href="https://opencollective.com/tesseractjs/organization/5/website"><img src="https://opencollective.com/tesseractjs/organization/5/avatar.svg"></a>
<a href="https://opencollective.com/tesseractjs/organization/6/website"><img src="https://opencollective.com/tesseractjs/organization/6/avatar.svg"></a>
<a href="https://opencollective.com/tesseractjs/organization/7/website"><img src="https://opencollective.com/tesseractjs/organization/7/avatar.svg"></a>
<a href="https://opencollective.com/tesseractjs/organization/8/website"><img src="https://opencollective.com/tesseractjs/organization/8/avatar.svg"></a>
<a href="https://opencollective.com/tesseractjs/organization/9/website"><img src="https://opencollective.com/tesseractjs/organization/9/avatar.svg"></a>
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
/*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/facebook/regenerator/blob/main/LICENSE */
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,12 @@
/*!
* The buffer module from node.js, for the browser.
*
* @author Feross Aboukhadijeh <https://feross.org>
* @license MIT
*/
/*! ieee754. BSD-3-Clause License. Feross Aboukhadijeh <https://feross.org/opensource> */
/*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/facebook/regenerator/blob/main/LICENSE */
/** @license zlib.js 2012 - imaya [ https://github.com/imaya/zlib.js ] The MIT License */
File diff suppressed because one or more lines are too long
+408
View File
@@ -0,0 +1,408 @@
# API
- [createWorker()](#create-worker)
- [Worker.recognize](#worker-recognize)
- [Worker.setParameters](#worker-set-parameters)
- [Worker.reinitialize](#worker-reinitialize)
- [Worker.detect](#worker-detect)
- [Worker.terminate](#worker-terminate)
- [Worker.writeText](#worker-writeText)
- [Worker.readText](#worker-readText)
- [Worker.removeFile](#worker-removeFile)
- [Worker.FS](#worker-FS)
- [createScheduler()](#create-scheduler)
- [Scheduler.addWorker](#scheduler-add-worker)
- [Scheduler.addJob](#scheduler-add-job)
- [Scheduler.getQueueLen](#scheduler-get-queue-len)
- [Scheduler.getNumWorkers](#scheduler-get-num-workers)
- [setLogging()](#set-logging)
- [recognize()](#recognize)
- [detect()](#detect)
- [PSM](#psm)
- [OEM](#oem)
---
<a name="create-worker"></a>
## createWorker(options): Worker
`createWorker` is a function that creates a Tesseract.js worker. A Tesseract.js worker is an object that creates and manages an instance of Tesseract running in a web worker (browser) or worker thread (Node.js). Once created, OCR jobs are sent through the worker.
**Arguments:**
- `langs` a string to indicate the languages traineddata to download, multiple languages are specified using an array (['eng', 'chi_sim'])
- `oem` a enum to indicate the OCR Engine Mode you use
- `options` an object of customized options
- `corePath` path to a directory containing **all of** the following files from [Tesseract.js-core](https://www.npmjs.com/package/tesseract.js-core) package:
- `tesseract-core.wasm.js`
- `tesseract-core-simd.wasm.js`
- `tesseract-core-lstm.wasm.js`
- `tesseract-core-simd-lstm.wasm.js`
- Some code snippets found online set `corePath` to a specific `.js` file. This is **strongly discouraged.** To provide the best performance and lowest network usage, Tesseract.js needs to be able to pick between builds.
- `langPath` path for downloading traineddata, do not include `/` at the end of the path
- `workerPath` path for downloading worker script
- `dataPath` path for saving traineddata in WebAssembly file system, not common to modify
- `cachePath` path for the cached traineddata, more useful for Node, for browser it only changes the key in IndexDB
- `cacheMethod` a string to indicate the method of cache management, should be one of the following options
- write: read cache and write back (default method)
- readOnly: read cache and not to write back
- refresh: not to read cache and write back
- none: not to read cache and not to write back
- `legacyCore` set to `true` to ensure any code downloaded supports the Legacy model (in addition to LSTM model)
- `legacyLang` set to `true` to ensure any language data downloaded supports the Legacy model (in addition to LSTM model)
- `workerBlobURL` a boolean to define whether to use Blob URL for worker script, default: true
- `gzip` a boolean to define whether the traineddata from the remote is gzipped, default: true
- `logger` a function to log the progress, a quick example is `m => console.log(m)`
- `errorHandler` a function to handle worker errors, a quick example is `err => console.error(err)`
- `config` an object of customized options which are set prior to initialization
- This argument allows for setting "init only" Tesseract parameters
- Most Tesseract parameters can be set after a worker is initialized, using either `worker.setParameters` or the `options` argument of `worker.recognize`.
- A handful of Tesseract parameters, referred to as "init only" parameters in Tesseract documentation, cannot be modified after Tesseract is initialized--these can only be set using this argument
- Examples include `load_system_dawg`, `load_number_dawg`, and `load_punc_dawg`
**Examples:**
```javascript
const { createWorker } = Tesseract;
const worker = await createWorker('eng', 1, {
langPath: '...',
logger: m => console.log(m),
});
```
<a name="worker-recognize"></a>
### worker.recognize(image, options, output, jobId): Promise
`worker.recognize` provides core function of Tesseract.js as it executes OCR
Figures out what words are in `image`, where the words are in `image`, etc.
> [!TIP]
> Note: `image` should be sufficiently high resolution.
> Often, the same image will get much better results if you upscale it before calling `recognize`.
**Arguments:**
- `image` see [Image Format](./image-format.md) for more details.
- `options` an object of customized options
- `rectangle` an object to specify the regions you want to recognized in the image, should contain top, left, width and height, see example below.
- `output` an object specifying which output formats to return (by default only `text` is returned)
- Other options include `blocks` (json), `hocr`, and `tsv`
- `jobId` Please see details above
**Output:**
`worker.recognize` returns a promise to an object containing `jobId` and `data` properties. The `data` property contains output in all of the formats specified using the `output` argument.
> [!NOTE]
> `worker.recognize` still returns an output object even if no text is detected (the outputs will simply contain no words). No exception is thrown as determining the page is empty is considered a valid result.
**Examples:**
```javascript
const { createWorker } = Tesseract;
(async () => {
const worker = await createWorker('eng');
const { data: { text } } = await worker.recognize(image);
console.log(text);
})();
```
With rectangle
```javascript
const { createWorker } = Tesseract;
(async () => {
const worker = await createWorker('eng');
const { data: { text } } = await worker.recognize(image, {
rectangle: { top: 0, left: 0, width: 100, height: 100 },
});
console.log(text);
})();
```
<a name="worker-set-parameters"></a>
### worker.setParameters(params, jobId): Promise
`worker.setParameters()` set parameters for Tesseract API (using SetVariable()), it changes the behavior of Tesseract and some parameters like tessedit\_char\_whitelist is very useful.
**Arguments:**
- `params` an object with key and value of the parameters
- `jobId` Please see details above
Note: `worker.setParameters` cannot be used to change the `oem`, as this value is set at initialization. `oem` is initially set using an argument of `createWorker`. After a worker already exists, changing `oem` requires running `worker.reinitialize`.
**Useful Parameters:**
| name | type | default value | description |
| --------------------------- | ------ | ----------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| tessedit\_pageseg\_mode | enum | PSM.SINGLE\_BLOCK | Check [HERE](https://github.com/tesseract-ocr/tesseract/blob/4.0.0/src/ccstruct/publictypes.h#L163) for definition of each mode |
| tessedit\_char\_whitelist | string | '' | setting white list characters makes the result only contains these characters, useful if content in image is limited |
| preserve\_interword\_spaces | string | '0' | '0' or '1', keeps the space between words |
| user\_defined\_dpi | string | '' | Define custom dpi, use to fix **Warning: Invalid resolution 0 dpi. Using 70 instead.** |
This list is incomplete. As Tesseract.js passes parameters to the Tesseract engine, all parameters supported by the underlying version of Tesseract should also be supported by Tesseract.js. (Note that parameters marked as “init only” in Tesseract documentation cannot be set by `setParameters` or `recognize`.)
**Examples:**
```javascript
(async () => {
await worker.setParameters({
tessedit_char_whitelist: '0123456789',
});
})
```
<a name="worker-reinitialize"></a>
### worker.reinitialize(langs, oem, jobId): Promise
`worker.reinitialize()` re-initializes an existing Tesseract.js worker with different `langs` and `oem` arguments.
**Arguments:**
- `langs` a string to indicate the languages traineddata to download, multiple languages are specified using an array (['eng', 'chi_sim'])
- `oem` a enum to indicate the OCR Engine Mode you use
- `config` an object of customized options which are set prior to initialization (see details above)
- `jobId` Please see details above
Note: to switch from Tesseract LSTM (`oem` value `1`) to Tesseract Legacy (`oem` value `0`) using `worker.reinitialize()`, the worker must already contain the code required to run the Tesseract Legacy model. Setting `legacyCore: true` and `legacyLang: true` in `createWorker` options ensures this is the case.
**Examples:**
```javascript
await worker.reinitialize('eng', 1);
```
<a name="worker-detect"></a>
### worker.detect(image, jobId): Promise
`worker.detect` does OSD (Orientation and Script Detection) to the image instead of OCR.
> [!NOTE]
> Running `worker.detect` requires a worker with code and language data that supports Tesseract Legacy (this is not enabled by default). If you want to run `worker.detect`, set `legacyCore` and `legacyLang` to `true` in the `createWorker` options.
**Arguments:**
- `image` see [Image Format](./image-format.md) for more details.
- `jobId` Please see details above
**Examples:**
```javascript
const { createWorker } = Tesseract;
(async () => {
const worker = await createWorker('eng', 1, {legacyCore: true, legacyLang: true});
const { data } = await worker.detect(image);
console.log(data);
})();
```
<a name="worker-terminate"></a>
### worker.terminate(jobId): Promise
`worker.terminate` terminates the worker and cleans up
```javascript
(async () => {
await worker.terminate();
})();
```
<a name="worker-writeText"></a>
### Worker.writeText(path, text, jobId): Promise
`worker.writeText` writes a text file to the path specified in MEMFS, it is useful when you want to use some features that requires tesseract.js
to read file from file system.
**Arguments:**
- `path` text file path
- `text` content of the text file
- `jobId` Please see details above
**Examples:**
```javascript
(async () => {
await worker.writeText('tmp.txt', 'Hi\nTesseract.js\n');
})();
```
<a name="worker-readText"></a>
### worker.readText(path, jobId): Promise
`worker.readText` reads a text file to the path specified in MEMFS, it is useful when you want to check the content.
**Arguments:**
- `path` text file path
- `jobId` Please see details above
**Examples:**
```javascript
(async () => {
const { data } = await worker.readText('tmp.txt');
console.log(data);
})();
```
<a name="worker-removeFile"></a>
### worker.removeFile(path, jobId): Promise
`worker.removeFile` removes a file in MEMFS, it is useful when you want to free the memory.
**Arguments:**
- `path` file path
- `jobId` Please see details above
**Examples:**
```javascript
(async () => {
await worker.removeFile('tmp.txt');
})();
```
<a name="worker-FS"></a>
### worker.FS(method, args, jobId): Promise
`worker.FS` is a generic FS function to do anything you want, you can check [HERE](https://emscripten.org/docs/api_reference/Filesystem-API.html) for all functions.
**Arguments:**
- `method` method name
- `args` array of arguments to pass
- `jobId` Please see details above
**Examples:**
```javascript
(async () => {
await worker.FS('writeFile', ['tmp.txt', 'Hi\nTesseract.js\n']);
// equal to:
// await worker.writeText('tmp.txt', 'Hi\nTesseract.js\n');
})();
```
<a name="create-scheduler"></a>
## createScheduler(): Scheduler
`createScheduler` is a factory function to create a scheduler, a scheduler manages a job queue and workers to enable multiple workers to work together, it is useful when you want to speed up your performance.
**Examples:**
```javascript
const { createScheduler } = Tesseract;
const scheduler = createScheduler();
```
### Scheduler
<a name="scheduler-add-worker"></a>
### scheduler.addWorker(worker): string
`scheduler.addWorker` adds a worker into the worker pool inside scheduler, it is suggested to add one worker to only one scheduler.
**Arguments:**
- `worker` see Worker above
**Examples:**
```javascript
const { createWorker, createScheduler } = Tesseract;
const scheduler = createScheduler();
const worker = await createWorker();
scheduler.addWorker(worker);
```
<a name="scheduler-add-job"></a>
### scheduler.addJob(action, ...payload): Promise
`scheduler.addJob` adds a job to the job queue and scheduler waits and finds an idle worker to take the job.
**Arguments:**
- `action` a string to indicate the action you want to do, right now only **recognize** and **detect** are supported
- `payload` a arbitrary number of args depending on the action you called.
**Examples:**
```javascript
(async () => {
const { data: { text } } = await scheduler.addJob('recognize', image, options);
const { data } = await scheduler.addJob('detect', image);
})();
```
<a name="scheduler-get-queue-len"></a>
### scheduler.getQueueLen(): number
`scheduler.getQueueLen()` returns the length of job queue.
<a name="scheduler-get-num-workers"></a>
### Scheduler.getNumWorkers(): number
`Scheduler.getNumWorkers()` returns number of workers added into the scheduler
<a name="scheduler-terminate"></a>
### scheduler.terminate(): Promise
`scheduler.terminate()` terminates all workers added, useful to do quick clean up.
**Examples:**
```javascript
(async () => {
await scheduler.terminate();
})();
```
<a name="set-logging"></a>
## setLogging(logging: boolean)
`setLogging` sets the logging flag, you can `setLogging(true)` to see detailed information, useful for debugging.
**Arguments:**
- `logging` boolean to define whether to see detailed logs, default: false
**Examples:**
```javascript
const { setLogging } = Tesseract;
setLogging(true);
```
<a name="recognize"></a>
## recognize(image, langs, options): Promise
> [!WARNING]
> This function is depreciated and should be replaced with `worker.recognize` (see above).
`recognize` works the same as `worker.recognize`, except that a new worker is created, loaded, and destroyed every time the function is called.
See [Tesseract.js](../src/Tesseract.js)
<a name="detect"></a>
## detect(image, options): Promise
> [!WARNING]
> This function is depreciated and should be replaced with `worker.detect` (see above).
`detect` works the same as `worker.detect`, except that a new worker is created, loaded, and destroyed every time the function is called.
See [Tesseract.js](../src/Tesseract.js)
<a name="psm"></a>
## PSM
See [PSM.js](../src/constants/PSM.js)
<a name="oem"></a>
## OEM
See [OEM.js](../src/constants/OEM.js)
+193
View File
@@ -0,0 +1,193 @@
# Tesseract.js Examples
You can also check [examples](../examples) folder.
### basic
```javascript
const { createWorker } = require('tesseract.js');
const worker = await createWorker('eng');
(async () => {
const { data: { text } } = await worker.recognize('https://tesseract.projectnaptha.com/img/eng_bw.png');
console.log(text);
await worker.terminate();
})();
```
### with detailed progress
```javascript
const { createWorker } = require('tesseract.js');
const worker = await createWorker('eng', 1, {
logger: m => console.log(m), // Add logger here
});
(async () => {
const { data: { text } } = await worker.recognize('https://tesseract.projectnaptha.com/img/eng_bw.png');
console.log(text);
await worker.terminate();
})();
```
### with multiple languages, separate by '+'
```javascript
const { createWorker } = require('tesseract.js');
const worker = await createWorker(['eng', 'chi_tra']);
(async () => {
const { data: { text } } = await worker.recognize('https://tesseract.projectnaptha.com/img/eng_bw.png');
console.log(text);
await worker.terminate();
})();
```
### with whitelist char
```javascript
const { createWorker } = require('tesseract.js');
const worker = await createWorker('eng');
(async () => {
await worker.setParameters({
tessedit_char_whitelist: '0123456789',
});
const { data: { text } } = await worker.recognize('https://tesseract.projectnaptha.com/img/eng_bw.png');
console.log(text);
await worker.terminate();
})();
```
### with different pageseg mode
Check here for more details of pageseg mode: https://github.com/tesseract-ocr/tesseract/blob/4.0.0/src/ccstruct/publictypes.h#L163
```javascript
const { createWorker, PSM } = require('tesseract.js');
const worker = await createWorker('eng');
(async () => {
await worker.setParameters({
tessedit_pageseg_mode: PSM.SINGLE_BLOCK,
});
const { data: { text } } = await worker.recognize('https://tesseract.projectnaptha.com/img/eng_bw.png');
console.log(text);
await worker.terminate();
})();
```
### with pdf output
Please check **examples** folder for details.
Browser: [download-pdf.html](../examples/browser/download-pdf.html)
Node: [download-pdf.js](../examples/node/download-pdf.js)
### with only part of the image (^2.0.1)
**One rectangle**
```javascript
const { createWorker } = require('tesseract.js');
const worker = await createWorker('eng');
const rectangle = { left: 0, top: 0, width: 500, height: 250 };
(async () => {
const { data: { text } } = await worker.recognize('https://tesseract.projectnaptha.com/img/eng_bw.png', { rectangle });
console.log(text);
await worker.terminate();
})();
```
**Multiple Rectangles**
```javascript
const { createWorker } = require('tesseract.js');
const worker = await createWorker('eng');
const rectangles = [
{
left: 0,
top: 0,
width: 500,
height: 250,
},
{
left: 500,
top: 0,
width: 500,
height: 250,
},
];
(async () => {
const values = [];
for (let i = 0; i < rectangles.length; i++) {
const { data: { text } } = await worker.recognize('https://tesseract.projectnaptha.com/img/eng_bw.png', { rectangle: rectangles[i] });
values.push(text);
}
console.log(values);
await worker.terminate();
})();
```
**Multiple Rectangles (with scheduler to do recognition in parallel)**
```javascript
const { createWorker, createScheduler } = require('tesseract.js');
const scheduler = createScheduler();
const worker1 = await createWorker('eng');
const worker2 = await createWorker('eng');
const rectangles = [
{
left: 0,
top: 0,
width: 500,
height: 250,
},
{
left: 500,
top: 0,
width: 500,
height: 250,
},
];
(async () => {
scheduler.addWorker(worker1);
scheduler.addWorker(worker2);
const results = await Promise.all(rectangles.map((rectangle) => (
scheduler.addJob('recognize', 'https://tesseract.projectnaptha.com/img/eng_bw.png', { rectangle })
)));
console.log(results.map(r => r.data.text));
await scheduler.terminate();
})();
```
### with multiple workers to speed up
```javascript
const { createWorker, createScheduler } = require('tesseract.js');
const scheduler = createScheduler();
const worker1 = await createWorker('eng');
const worker2 = await createWorker('eng');
(async () => {
scheduler.addWorker(worker1);
scheduler.addWorker(worker2);
/** Add 10 recognition jobs */
const results = await Promise.all(Array(10).fill(0).map(() => (
scheduler.addJob('recognize', 'https://tesseract.projectnaptha.com/img/eng_bw.png')
)))
console.log(results);
await scheduler.terminate(); // It also terminates all workers.
})();
```
+75
View File
@@ -0,0 +1,75 @@
FAQ
===
# Project
## What is the scope of this project?
Tesseract.js is the JavaScript/Webassembly port of the Tesseract OCR engine. We do not edit the underlying Tesseract recognition engine in any way. Therefore, if you encounter bugs caused by the Tesseract engine you may open an issue here for the purposes of raising awareness to other users, but fixing is outside the scope of this repository.
If you encounter a Tesseract bug you would like to see fixed you should confirm the behavior is the same in the [main (CLI) version](https://github.com/tesseract-ocr/tesseract) of Tesseract and then open a Git Issue in that repository.
# Frameworks
## What JavaScript frameworks are supported?
Tesseract.js supports all frameworks that support JavaScript and WebAssembly. The only common JavaScript framework known to not be supported is React Native, as it does not support WebAssembly.
## Why am I getting a `Cannot find module` error when running in my project/framework?
If you are able to run the examples in the [examples directory](https://github.com/naptha/tesseract.js/tree/master/examples), however are getting a `cannot find module` error when run in your framework, this indicates the main Tesseract.js thread is unable to find the worker code.
This can be resolved by manually setting the `workerPath` argument to point to the local copy of `worker-script/node/index.js` (Node.js) or `worker.min.js` (browser). For example, the using the following arguments resolved for one Node.js user in [this issue](https://github.com/naptha/tesseract.js/issues/868#issuecomment-1879235802). You may need to edit the file paths to work with your system/project.
```
const worker = await createWorker("eng", 1, {workerPath: "./node_modules/tesseract.js/src/worker-script/node/index.js"});
```
For context, Tesseract.js "workers" get their own web worker (browser) or worker thread (Node.js), which is independent code that uses a different entry point. When Tesseract.js is used on its own, this entrypoint should be identified automatically. However, this may not hold with build systems implemented by various frameworks, as these build systems copy around files in a way that violates Tesseract.js's assumptions for where files are located.
# Recognizing Text
## Are PDF files supported?
Tesseract.js does not support PDF files. If you need to run OCR on PDF files, possible options are below.
### Use Scribe.js
[Scribe.js](https://github.com/scribeocr/scribe.js) is a library that builds on Tesseract.js and includes additional features, including native PDF support. Scribe.js supports running OCR on PDF files. Additionally, Scribe.js supports extracting text directly from text-native PDF files, which is significantly faster and more accurate compared to running OCR.
### Render PDFs to Images
The only way to recognize PDF files using Tesseract.js is to use a third-party library to render the `.pdf` file to a series of `.png` images, and then recognize those images using Tesseract.js. Libraries to consider are listed below.
1. [PDF.js](https://github.com/mozilla/pdf.js/) (Apache-2.0 license)
2. [muPDF](https://github.com/ArtifexSoftware/mupdf) (AGPL-3.0 license)
## What configuration settings should I use?
Default settings should provide optimal results for most users. If you do want to experiment with configuration settings, Tesseract does include many settings to change—the vast majority are documented in the [main Tesseract project](https://github.com/tesseract-ocr/tesseract) and not here. As noted above (“what is the scope of this project”), the core recognition engine is inherited from the main Tesseract project—all of the configuration settings in Tesseract work identically in Tesseract.js. Therefore, for specific questions about configuring recognition settings (e.g. “how can I make noise removal more/less aggressive” or “what settings work best for license plates”) you are more likely to find an answer in the Tesseract documentation/discussion versus only looking in this repo.
## Is handwritten text supported?
No. The Tesseract OCR model is built around assumptions that only hold for printed text. No combination of options will significantly improve performance with handwritten text. Unless your handwriting is so good that it closely resembles printed text, the results will be poor.
## Why am I getting different results vs. Tesseract CLI?
Tesseract.js should produce results identical to using the Tesseract CLI, as long as the settings, language data, and version are identical. If you are observing differences between Tesseract.js and the Tesseract CLI/API, and this difference is problematic, perform the following troubleshooting steps.
1. Confirm parameters are identical.
1. Manually set `oem` and `psm` in both to account for different defaults.
1. Tesseract.js and the Tesseract CLI use different default `oem` and `psm` settings.
1. Tesseract.js uses a default `oem` of `1` (`lstm` model only), while the Tesseract CLI uses a default `oem` of `2` (`lstm` with `legacy` fallback).
2. Tesseract.js and the Tesseract API use a default `pms` of `6` (`PSM_SINGLE_BLOCK`), while the Tesseract CLI uses a default `psm` of `3` (`PSM_AUTO`).
2. Confirm that all user-set parameters are identical.
2. Confirm language data is identical.
1. By default, when run with `oem` value `0` or `2`, Tesseract.js uses [these](https://github.com/naptha/tessdata/tree/gh-pages/4.0.0) language files.
1. These were taken from the [tessdata](https://github.com/tesseract-ocr/tessdata) repo in the main Tesseract project.
3. By default, when run with `oem` value `1`, Tesseract.js uses [these](https://github.com/naptha/tessdata/tree/gh-pages/4.0.0_best_int) language files.
1. These were created by integerizing the language files from the [tessdata_best](https://github.com/tesseract-ocr/tessdata_best) repo in the main Tesseract project.
1. This should be equivalent to using the LSTM language files from the [tessdata](https://github.com/tesseract-ocr/tessdata) which are created by combining an integerized version of `tessdata_best` with data for the Legacy model.
3. Confirm version is identical.
1. Using a different version of Tesseract may result in different recognition results.
2. The exact version of Tesseract used for Tesseract.js can be found by clicking on the `tesseract` submodule in this directory:
1. https://github.com/naptha/tesseract.js-core/tree/master/third_party
If you find that results differ between Tesseract.js and Tesseract CLI and the settings, language data, and version are identical, feel free to open a Git Issue with a reproducible example.
Additionally, feel free to open a Git Issue (with reproducible example) if you find that a **newer** version of Tesseract produces significantly better results, and we can prioritize updating Tesseract to the latest version. If an older version of Tesseract produces significantly better results, then that regression should be raised with the main Tesseract project, as Tesseract.js will not be downgraded to an earlier version.
# Trained Data
## How does tesseract.js download and keep \*.traineddata?
During the downloading of language model, Tesseract.js will first check if \*.traineddata already exists. (browser: [IndexedDB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API), Node.js: fs, in the folder you execute the command) If the \*.traineddata doesn't exist, it will fetch \*.traineddata.gz from [tessdata](https://github.com/naptha/tessdata), ungzip and store in IndexedDB or fs, you can delete it manually and it will download again for you.
## How can I train my own \*.traineddata?
See the documentation from the main [Tesseract project](https://tesseract-ocr.github.io/tessdoc/) for training instructions.
@@ -0,0 +1,18 @@
# Image Format
The main Tesseract.js functions (ex. recognize, detect) take an `image` parameter. The image formats and data types supported are listed below.
Support Image Formats: **bmp, jpg, png, pbm, webp, gif \[non-animated\]**.
For browser and Node, supported data types are:
- string with base64 encoded image (fits `data:image\/([a-zA-Z]*);base64,([^"]*)` regexp)
- buffer
For browser only, supported data types are:
- `File` or `Blob` object
- `img` or `canvas` element
For Node only, supported data types are:
- string containing a path to local image
Note: images must be a supported image format **and** a supported data type. For example, a buffer containing a png image is supported. A buffer containing raw pixel data is not supported.
Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 237 KiB

@@ -0,0 +1,37 @@
## Local Installation
Check here for examples: https://github.com/naptha/tesseract.js/blob/master/docs/examples.md
In browser environment, `tesseract.js` simply provides the API layer. Internally, it opens a WebWorker to handle requests. That worker itself loads code from the Emscripten-built `tesseract.js-core` which itself is hosted on a CDN. Then it dynamically loads language files hosted on another CDN.
Because of this we recommend loading `tesseract.js` from a CDN. But if you really need to have all your files local, you can pass extra arguments to `TesseractWorker` to specify custom paths for workers, languages, and core.
In Node.js environment, the only path you may want to customize is languages/langPath.
```javascript
const worker = await createWorker('eng', 1, {
workerPath: 'https://cdn.jsdelivr.net/npm/tesseract.js@v5.0.0/dist/worker.min.js',
langPath: 'https://tessdata.projectnaptha.com/4.0.0',
corePath: 'https://cdn.jsdelivr.net/npm/tesseract.js-core@v5.0.0',
});
```
### workerPath
A string specifying the location of the `worker.js` file.
### langPath
A string specifying the location of the tesseract language files. Language file URLs are calculated according to the formula `langPath + langCode + '.traineddata.gz'`. If `langPath` is not specified by the user, then the correct language data will be automatically downloaded from the jsDelivr CDN.
### corePath
A string specifying the location of the [tesseract.js-core](https://github.com/naptha/tesseract.js-core) files, with default value 'https://cdn.jsdelivr.net/npm/tesseract.js-core@v5.0.0'.
If you set the `corePath` argument, be sure to set it to a directory that contains **all 4** of these files:
1. `tesseract-core.wasm.js`
2. `tesseract-core-simd.wasm.js`
3. `tesseract-core-lstm.wasm.js`
4. `tesseract-core-simd-lstm.wasm.js`
Tesseract.js will pick the correct file based on your users' device and the `createWorker` options.
To avoid breaking old code, when `corePath` is set to a specific `.js` file (e.g. `https://cdn.jsdelivr.net/npm/tesseract.js-core@v5.0.0/tesseract-core.wasm.js`), it will load that file regardless of whether the users' device supports SIMD or not. This behavior only exists to preserve backwards compatibility—setting `corePath` to a specific `.js` file is strongly discouraged. Doing so will either result in much slower performance (if `tesseract-core.wasm.js` is specified) or failure to run on certain devices (if `tesseract-core-simd.wasm.js` is specified).
@@ -0,0 +1,38 @@
# Overview
This guide contains tips and strategies for getting the fastest performance from Tesseract.js. While some of the tips below involve avoiding pitfalls and should be universally implemented, other strategies (changing the language data or recognition model) may harm recognition quality. Therefore, whether these strategies are appropriate depends on the application, and users should always benchmark performance and quality before changing important settings from their defaults.
# Reducing Setup Time
Within certain applications, the majority of runtime may be attributable to setup steps (`createWorker`) rather than recognition (`worker.recognize`). Implementing the strategies in this section should reduce the time spent on these steps.
Notably, the time spent on setup for first-time users may not be apparent to developers, as Tesseract.js caches language data after it is downloaded for the first time. To experience Tesseract.js as a first-time user, set `cacheMethod: 'none'` in the [createWorker options](./api.md#createworkeroptions-worker). Be sure to remove this setting before publishing your app.
### Reuse Workers
When recognizing multiple images, some users will create/load/destroy a new worker for each image. This is never the correct option. If the images are being recognized one after the other, all of the extra steps required to create/load/destroy a new worker are wasted runtime, as `worker.recognize` could be run with the same `worker`. Workers do not break after one use.
Alternatively, if images are being recognized in parallel, then creating a new worker for each recognition job is likely to cause crashes due to resource limitations. As each Tesseract.js worker uses a high amount of memory, code should never be able to create an arbitrary number of `workers`. Instead, schedulers should be used to create a specific pool for workers (say, 4 workers), and then jobs assigned through the scheduler.
### Set Up Workers Ahead of Time
Rather than waiting until the last minute to load code and data, you can set up a worker ahead of time. Doing so greatly reduces runtime the first time a user run recognition. An example where a worker is prepared ahead of time can be found [here](../examples/browser/basic-efficient.html).
The appropriate time to load Tesseract.js workers and data is application-specific. For example, if you have an web app where only 5% of users need OCR, it likely does not make sense to download ~15MB in code and data upon a page load. You could consider instead loading Tesseract.js when a user indicates they want to perform OCR, but before they select a specific image.
### Do Not Disable Language Data Caching
Language data is one of the largest downloads required to run Tesseract.js. While most language data files (including the default English file) are ~2MB, in a worst-case scenario they can be much larger. For example, setting the recognition model (`oem`) to Tesseract Legacy and language to Chinese (simplified) results in a ~20MB file being downloaded.
To avoid downloading language data multiple times, Tesseract.js caches `.traineddata` files. In past versions of Tesseract.js, this caching behavior contained bugs, so some users disabled it (setting `cacheMethod: 'none'` or `cacheMethod: 'refresh'`). As these bugs were fixed in [v4.0.6](https://github.com/naptha/tesseract.js/releases/tag/v4.0.6), it is now recommended that users use the default `cacheMethod` value (i.e. just ignore the `cacheMethod` argument).
# Reducing Recognition Runtime
### Use the Latest Version of Tesseract.js
Old versions of Tesseract.js are significantly slower. Notably, v2 (now depreciated) takes 10x longer to recognize certain images compared to the latest version.
### Do Not Set `corePath` to a Single `.js` file
If you set the `corePath` argument, be sure to set it to a directory that contains **all 4** of these files:
1. `tesseract-core.wasm.js`
2. `tesseract-core-simd.wasm.js`
3. `tesseract-core-lstm.wasm.js`
4. `tesseract-core-simd-lstm.wasm.js`
Tesseract.js needs to be able to pick between these files—setting `corePath` to a specific `.js` file will significantly degrade performance or compatibility.
### Consider Using "Fast" Language Data
By default, Tesseract.js uses language data that is optimized for quality rather than speed. You can also experiment with using language data that is optimized for speed by setting `langPath` to `https://tessdata.projectnaptha.com/4.0.0_fast`. We have not benchmarked the impact this has on performance or accuracy, so feel free to open a Git Issue if you do so and wish to share results.
@@ -0,0 +1,3 @@
# Tesseract Languages
Please check [HERE](https://tesseract-ocr.github.io/tessdoc/Data-Files#data-files-for-version-400-november-29-2016) for supported languages
@@ -0,0 +1,57 @@
# Overview
Tesseract.js offers 2 ways to run recognition jobs: (1) using a worker directly, or (2) using a scheduler to run jobs on multiple workers in parallel. The syntax for the latter is more complicated, but using parallel processing via schedulers provides significantly better performance for large jobs. For more detailed documentation on each function, see the [api page](./api.md).
# Option 1: Using Workers Directly
The following snippet recognizes text from an image using a single worker.
```javascript
(async () => {
const worker = await Tesseract.createWorker('eng');
const { data: { text } } = await worker.recognize('https://tesseract.projectnaptha.com/img/eng_bw.png');
console.log(text);
await worker.terminate();
})();
```
In actual use, the `createWorker` step should be separated from the `worker.recognize` step. Doing so enables the following benefits:
1. Workers can be prepared ahead of time
- E.g. a worker can be created when the page is first loaded, rather than waiting for a user to upload an image to recognize
1. Workers can be reused for multiple recognition jobs, rather than creating a new worker and loading language data for every image recognized
- Remember to call `worker.terminate()` after all recognition is complete to free memory
# Option 2: Using Schedulers + Workers
Tesseract.js also supports executing jobs using schedulers. A scheduler is an object that contains multiple workers, which it uses to execute jobs in parallel. For example, the following code executes 10 jobs in parallel using 4 workers.
```javascript
const scheduler = Tesseract.createScheduler();
// Creates worker and adds to scheduler
const workerGen = async () => {
const worker = await Tesseract.createWorker('eng');
scheduler.addWorker(worker);
}
const workerN = 4;
(async () => {
const resArr = Array(workerN);
for (let i=0; i<workerN; i++) {
resArr[i] = workerGen();
}
await Promise.all(resArr);
/** Add 10 recognition jobs */
const results = await Promise.all(Array(10).fill(0).map(() => (
scheduler.addJob('recognize', 'https://tesseract.projectnaptha.com/img/eng_bw.png').then((x) => console.log(x.data.text))
)))
await scheduler.terminate(); // It also terminates all workers.
})();
```
While using schedulers is no more efficient for a single job, they allow for quickly executing large numbers of jobs in parallel.
When working with schedulers, note that workers added to the same scheduler should all be homogenous—they should have the same language be configured with the same parameters. Schedulers assign jobs to workers in a non-deterministic manner, so if the workers are not identical then recognition results will depend on which worker the job is assigned to.
# Reusing Workers in Node.js Server Code
While workers and schedulers are reusable, and we recommend reusing them between jobs, using the same worker/scheduler for a week straight within Node.js server code will cause problems. Therefore, when using workers/schedulers within long-running Node.js server code, workers/schedulers should be killed and re-created every so often. For example, a scheduler could be terminated and re-created after every 500 jobs.
There are a couple reasons why periodically “resetting” workers/schedulers within server code is a good practice. First, due to general WebAssembly limitations, the memory allocated to workers can only expand over time. Therefore, a single large image will permanently increase the memory footprint of a worker. Second, workers “learn” over time by adding additional words they encounter in jobs to their internal dictionaries. While this behavior is useful within the context of a single document or group of documents, it is not necessarily desirable if recognizing hundreds of unrelated documents. If a single scheduler runs thousands of jobs over an entire week, the internal dictionary will eventually become bloated and include typos.
@@ -0,0 +1,29 @@
<!DOCTYPE HTML>
<html>
<head>
<script src="/dist/tesseract.min.js"></script>
</head>
<body>
<input type="file" id="uploader" multiple>
<script type="module">
// A worker is created once and used every time a user uploads a new file.
const worker = await Tesseract.createWorker("eng", 1, {
corePath: '../../node_modules/tesseract.js-core',
workerPath: "/dist/worker.min.js",
logger: function(m){console.log(m);}
});
const recognize = async function(evt){
const files = evt.target.files;
for (let i=0; i<files.length; i++) {
const ret = await worker.recognize(files[i]);
console.log(ret.data.text);
}
}
const elm = document.getElementById('uploader');
elm.addEventListener('change', recognize);
</script>
</body>
</html>
@@ -0,0 +1,47 @@
<!DOCTYPE HTML>
<html>
<head>
<script src="/dist/tesseract.min.js"></script>
</head>
<body>
<input type="file" id="uploader" multiple>
<script type="module">
// This example builds on "basic-efficient.html".
// Rather than using a single worker, a scheduler manages a pool of multiple workers.
// While performance is similar for a single file, this parallel processing results in significantly
// faster speeds when used with multiple files.
const scheduler = Tesseract.createScheduler();
// Creates worker and adds to scheduler
const workerGen = async () => {
const worker = await Tesseract.createWorker("eng", 1, {
corePath: '../../node_modules/tesseract.js-core',
workerPath: "/dist/worker.min.js",
logger: function(m){console.log(m);}
});
scheduler.addWorker(worker);
}
const workerN = 4;
(async () => {
const resArr = Array(workerN);
for (let i=0; i<workerN; i++) {
resArr[i] = await workerGen();
}
})();
const recognize = async function(evt){
const files = evt.target.files;
for (let i=0; i<files.length; i++) {
scheduler.addJob('recognize', files[i]).then((x) => console.log(x.data.text))
}
}
const elm = document.getElementById('uploader');
elm.addEventListener('change', recognize);
</script>
</body>
</html>
@@ -0,0 +1,52 @@
<html>
<head>
<script src="/dist/tesseract.min.js"></script>
</head>
<body>
<div>
<input type="file" id="uploader">
<button id="download-pdf" disabled="true">Download PDF</button>
</div>
<textarea id="board" readonly rows="8" cols="80">Upload an image file</textarea>
<script type="module">
const { createWorker } = Tesseract;
const worker = await createWorker("eng", 1, {
corePath: '/node_modules/tesseract.js-core',
workerPath: "/dist/worker.min.js",
logger: m => console.log(m),
});
const uploader = document.getElementById('uploader');
const dlBtn = document.getElementById('download-pdf');
let pdf;
const recognize = async ({ target: { files } }) => {
const res = await worker.recognize(files[0],{pdfTitle: "Example PDF"},{pdf: true});
pdf = res.data.pdf;
const text = res.data.text;
const board = document.getElementById('board');
board.value = text;
dlBtn.disabled = false;
};
const downloadPDF = async () => {
const filename = 'tesseract-ocr-result.pdf';
const blob = new Blob([new Uint8Array(pdf)], { type: 'application/pdf' });
if (navigator.msSaveBlob) {
// IE 10+
navigator.msSaveBlob(blob, filename);
} else {
const link = document.createElement('a');
if (link.download !== undefined) {
const url = URL.createObjectURL(blob);
link.setAttribute('href', url);
link.setAttribute('download', filename);
link.style.visibility = 'hidden';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
}
};
uploader.addEventListener('change', recognize);
dlBtn.addEventListener('click', downloadPDF);
</script>
</body>
</html>
@@ -0,0 +1,55 @@
<html>
<head>
<script src="/dist/tesseract.min.js"></script>
<style>
.column {
float: left;
width: 20%;
padding: 5px;
}
</style>
</head>
<body>
<input type="file" id="uploader">
<div class="row">
<div class="column">
<p>Input Image</p>
<img id="imgInput" style="max-width:500px;">
</div>
<div class="column">
<p>Rotated, Original Color</p>
<img id="imgOriginal" style="max-width:500px;">
</div>
<div class="column">
<p>Rotated, Grey</p>
<img id="imgGrey" style="max-width:500px;">
</div>
<div class="column">
<p>Rotated, Binary</p>
<img id="imgBinary" style="max-width:500px;">
</div>
</div>
<script>
const recognize = async ({ target: { files } }) => {
document.getElementById("imgInput").src = URL.createObjectURL(files[0]);
const worker = await Tesseract.createWorker("eng", 1, {
// corePath: '/tesseract-core-simd.wasm.js',
workerPath: "/dist/worker.min.js"
});
const ret = await worker.recognize(files[0], {rotateAuto: true}, {imageColor: true, imageGrey: true, imageBinary: true});
document.getElementById("imgOriginal").src = ret.data.imageColor;
document.getElementById("imgGrey").src = ret.data.imageGrey;
document.getElementById("imgBinary").src = ret.data.imageBinary;
}
const elm = document.getElementById('uploader');
elm.addEventListener('change', recognize);
</script>
</body>
</html>
@@ -0,0 +1,21 @@
#!/usr/bin/env node
'use strict';
const path = require('path');
const fs = require('fs');
const { createWorker } = require('../..');
const [,, imagePath] = process.argv;
const image = path.resolve(__dirname, (imagePath || '../../tests/assets/images/cosmic.png'));
console.log(`Recognizing ${image}`);
(async () => {
const worker = await createWorker();
const { data: { text, pdf } } = await worker.recognize(image, { pdfTitle: 'Example PDF' }, { pdf: true });
console.log(text);
fs.writeFileSync('tesseract-ocr-result.pdf', Buffer.from(pdf));
console.log('Generate PDF: tesseract-ocr-result.pdf');
await worker.terminate();
})();
@@ -0,0 +1,37 @@
#!/usr/bin/env node
'use strict';
const path = require('path');
const fs = require('fs');
const { createWorker } = require('../..');
const [,, imagePath] = process.argv;
const image = path.resolve(__dirname, (imagePath || '../data/meditations.jpg'));
console.log(`Recognizing ${image}`);
// Tesseract.js returns images (imageColor, imageGrey, imageBinary) as strings
// to be used as source tags.
// This function converts to Uint8Array data for saving to disk.
const convertImage = (imageSrc) => {
const data = atob(imageSrc.split(',')[1])
.split('')
.map((c) => c.charCodeAt(0));
return new Uint8Array(data);
};
(async () => {
const worker = await createWorker();
const { data: { imageColor, imageGrey, imageBinary } } = await worker.recognize(image,
{ rotateAuto: true }, { imageColor: true, imageGrey: true, imageBinary: true });
console.log('Saving intermediate images: imageColor.png, imageGrey.png, imageBinary.png');
fs.writeFileSync('imageColor.png', convertImage(imageColor));
fs.writeFileSync('imageGrey.png', convertImage(imageGrey));
fs.writeFileSync('imageBinary.png', convertImage(imageBinary));
await worker.terminate();
})();
@@ -0,0 +1,20 @@
#!/usr/bin/env node
'use strict';
const path = require('path');
const { createWorker } = require('../..');
const [,, imagePath] = process.argv;
const image = path.resolve(__dirname, (imagePath || '../../tests/assets/images/cosmic.png'));
console.log(`Recognizing ${image}`);
(async () => {
const worker = await createWorker('eng', 1, {
logger: (m) => console.log(m),
});
const { data: { text } } = await worker.recognize(image);
console.log(text);
await worker.terminate();
})();
@@ -0,0 +1,40 @@
'use strict';
const path = require('path');
const { createWorker, createScheduler } = require('../..');
const [,, imagePath] = process.argv;
// Note: This example recognizes the same image 4 times in parallel
// to show how schedulers can be used to speed up bulk jobs.
// In actual use you would (obviously) not want to run multiple identical jobs.
const image = path.resolve(__dirname, (imagePath || '../../tests/assets/images/cosmic.png'));
const imageArr = [image, image, image, image];
const scheduler = createScheduler();
// Creates worker and adds to scheduler
const workerGen = async () => {
const worker = await createWorker('eng', 1, { cachePath: '.' });
scheduler.addWorker(worker);
};
const workerN = 4;
(async () => {
const resArr = Array(workerN);
for (let i = 0; i < workerN; i++) {
resArr[i] = workerGen();
}
await Promise.all(resArr);
const resArr2 = Array(imageArr.length);
for (let i = 0; i < imageArr.length; i++) {
resArr2[i] = scheduler.addJob('recognize', image).then((x) => console.log(x.data.text));
}
await Promise.all(resArr2);
await scheduler.terminate(); // It also terminates all workers.
})();
+35
View File
@@ -0,0 +1,35 @@
'use strict';
module.exports = function (config) {
config.set({
frameworks: ['mocha'],
files: [
{ pattern: 'dist/tesseract.min.js', included: true },
{ pattern: 'dist/worker.min.js', included: false, served: true },
{ pattern: 'node_modules/tesseract.js-core/tesseract-core-simd-lstm.wasm.js', included: false, served: true },
{ pattern: 'node_modules/tesseract.js-core/tesseract-core-simd.wasm.js', included: false, served: true },
{ pattern: 'node_modules/tesseract.js-core/tesseract-core-relaxedsimd-lstm.wasm.js', included: false, served: true },
{ pattern: 'node_modules/tesseract.js-core/tesseract-core-relaxedsimd.wasm.js', included: false, served: true },
{ pattern: 'tests/constants.mjs', included: false, served: true },
{ pattern: 'node_modules/expect.js/index.js', included: true },
{ pattern: 'tests/*.test.mjs', type: 'module' },
{ pattern: 'tests/assets/images/**', included: false, served: true },
],
reporters: ['progress'],
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: false,
browsers: ['ChromeHeadless', 'FirefoxHeadless'],
singleRun: true,
concurrency: 1,
proxies: {
'/dist/worker.min.js': '/base/dist/worker.min.js',
'/node_modules/tesseract.js-core/tesseract-core-simd-lstm.wasm.js': '/base/node_modules/tesseract.js-core/tesseract-core-simd-lstm.wasm.js',
'/node_modules/tesseract.js-core/tesseract-core-simd.wasm.js': '/base/node_modules/tesseract.js-core/tesseract-core-simd.wasm.js',
'/node_modules/tesseract.js-core/tesseract-core-relaxedsimd-lstm.wasm.js': '/base/node_modules/tesseract.js-core/tesseract-core-relaxedsimd-lstm.wasm.js',
'/node_modules/tesseract.js-core/tesseract-core-relaxedsimd.wasm.js': '/base/node_modules/tesseract.js-core/tesseract-core-relaxedsimd.wasm.js',
'/tests/assets/images': '/base/tests/assets/images',
},
});
};
+91
View File
@@ -0,0 +1,91 @@
{
"name": "tesseract.js",
"version": "7.0.0",
"description": "Pure Javascript Multilingual OCR",
"main": "src/index.js",
"type": "commonjs",
"types": "src/index.d.ts",
"unpkg": "dist/tesseract.min.js",
"jsdelivr": "dist/tesseract.min.js",
"scripts": {
"start": "node scripts/server.js",
"build": "rimraf dist && webpack --config scripts/webpack.config.prod.js && rollup -c scripts/rollup.esm.mjs",
"profile:tesseract": "webpack-bundle-analyzer dist/tesseract-stats.json",
"profile:worker": "webpack-bundle-analyzer dist/worker-stats.json",
"prepublishOnly": "npm run build",
"wait": "rimraf dist && wait-on http://localhost:3000/dist/tesseract.min.js",
"test": "npm-run-all -p -r start test:all",
"test:all": "npm-run-all wait test:browser test:node:all",
"test:browser": "karma start karma.conf.js",
"test:node": "nyc mocha --exit --bail --require ./scripts/test-helper.mjs",
"test:node:all": "npm run test:node -- ./tests/*.test.mjs",
"lint": "eslint src",
"lint:fix": "eslint --fix src",
"postinstall": "opencollective-postinstall || true"
},
"browser": {
"./src/worker/node/index.js": "./src/worker/browser/index.js"
},
"author": "",
"contributors": [
"jeromewu"
],
"license": "Apache-2.0",
"devDependencies": {
"@babel/core": "^7.21.4",
"@babel/eslint-parser": "^7.21.3",
"@babel/preset-env": "^7.21.4",
"@rollup/plugin-commonjs": "^24.1.0",
"acorn": "^8.8.2",
"babel-loader": "^9.1.2",
"buffer": "^6.0.3",
"cors": "^2.8.5",
"eslint": "^7.32.0",
"eslint-config-airbnb-base": "^14.2.1",
"eslint-plugin-import": "^2.27.5",
"expect.js": "^0.3.1",
"express": "^4.18.2",
"mocha": "^10.2.0",
"npm-run-all": "^4.1.5",
"karma": "^6.4.2",
"karma-chrome-launcher": "^3.2.0",
"karma-firefox-launcher": "^2.1.2",
"karma-mocha": "^2.0.1",
"karma-webpack": "^5.0.0",
"nyc": "^15.1.0",
"rimraf": "^5.0.0",
"rollup": "^3.20.7",
"wait-on": "^7.0.1",
"webpack": "^5.79.0",
"webpack-bundle-analyzer": "^4.8.0",
"webpack-cli": "^5.0.1",
"webpack-dev-middleware": "^6.0.2",
"rollup-plugin-sourcemaps": "^0.6.3"
},
"dependencies": {
"bmp-js": "^0.1.0",
"idb-keyval": "^6.2.0",
"is-url": "^1.2.4",
"node-fetch": "^2.6.9",
"opencollective-postinstall": "^2.0.3",
"regenerator-runtime": "^0.13.3",
"tesseract.js-core": "^7.0.0",
"wasm-feature-detect": "^1.8.0",
"zlibjs": "^0.3.1"
},
"overrides": {
"@rollup/pluginutils": "^5.0.2"
},
"repository": {
"type": "git",
"url": "https://github.com/naptha/tesseract.js.git"
},
"bugs": {
"url": "https://github.com/naptha/tesseract.js/issues"
},
"homepage": "https://github.com/naptha/tesseract.js",
"collective": {
"type": "opencollective",
"url": "https://opencollective.com/tesseractjs"
}
}
@@ -0,0 +1,15 @@
import commonjs from '@rollup/plugin-commonjs';
import sourcemaps from 'rollup-plugin-sourcemaps';
export default [
{
input: 'dist/tesseract.min.js',
output: {
file: 'dist/tesseract.esm.min.js',
format: 'esm',
sourcemap: true,
banner: '/* eslint-disable */',
},
plugins: [commonjs(), sourcemaps()],
},
];
+27
View File
@@ -0,0 +1,27 @@
'use strict';
const webpack = require('webpack');
const middleware = require('webpack-dev-middleware');
const express = require('express');
const path = require('path');
const cors = require('cors');
const webpackConfig = require('./webpack.config.prod');
const compiler = webpack(webpackConfig);
const app = express();
app.use(cors());
app.use(middleware(compiler, { publicPath: '/dist', writeToDisk: true }));
// These headers are required to measure memory within the benchmark code.
// If they are problematic within other contexts they can be removed.
app.use(express.static(path.resolve(__dirname, '..'), {
setHeaders: (res) => {
res.set('Cross-Origin-Opener-Policy', 'same-origin');
res.set('Cross-Origin-Embedder-Policy', 'require-corp');
}
}));
module.exports = app.listen(3000, () => {
console.log('Server is running on the port no. 3000');
});
@@ -0,0 +1,8 @@
import expect from 'expect.js';
import fs from 'fs';
// eslint-disable-next-line import/extensions
import Tesseract from '../src/index.js';
global.Tesseract = Tesseract;
global.expect = expect;
global.fs = fs;
@@ -0,0 +1,30 @@
'use strict';
module.exports = {
resolve: {
fallback: {
buffer: require.resolve('buffer/'),
},
},
module: {
rules: [
{
test: /\.m?js$/,
// exclude: /(node_modules|bower_components)/,
use: {
loader: 'babel-loader',
options: {
presets: [
[
'@babel/preset-env',
{
targets: 'last 2 versions',
},
],
],
},
},
},
],
},
};
@@ -0,0 +1,38 @@
'use strict';
const path = require('path');
const common = require('./webpack.config.common');
const webpack = require('webpack');
const genConfig = ({
entry, filename, library, libraryTarget,
}) => ({
...common,
mode: 'production',
devtool: 'source-map',
entry,
output: {
path: path.resolve(__dirname, '..', 'dist'),
filename,
library,
libraryTarget,
},
plugins: [
new webpack.ProvidePlugin({
Buffer: ['buffer', 'Buffer'],
}),
]
});
module.exports = [
genConfig({
entry: path.resolve(__dirname, '..', 'src', 'index.js'),
filename: 'tesseract.min.js',
library: 'Tesseract',
libraryTarget: 'umd',
}),
genConfig({
entry: path.resolve(__dirname, '..', 'src', 'worker-script', 'browser', 'index.js'),
filename: 'worker.min.js',
}),
];
+24
View File
@@ -0,0 +1,24 @@
'use strict';
const createWorker = require('./createWorker');
const recognize = async (image, langs, options) => {
const worker = await createWorker(langs, 1, options);
return worker.recognize(image)
.finally(async () => {
await worker.terminate();
});
};
const detect = async (image, options) => {
const worker = await createWorker('osd', 0, options);
return worker.detect(image)
.finally(async () => {
await worker.terminate();
});
};
module.exports = {
recognize,
detect,
};
@@ -0,0 +1,14 @@
'use strict';
/*
* OEM = OCR Engine Mode, and there are 4 possible modes.
*
* By default tesseract.js uses LSTM_ONLY mode.
*
*/
module.exports = {
TESSERACT_ONLY: 0,
LSTM_ONLY: 1,
TESSERACT_LSTM_COMBINED: 2,
DEFAULT: 3,
};
@@ -0,0 +1,21 @@
'use strict';
/*
* PSM = Page Segmentation Mode
*/
module.exports = {
OSD_ONLY: '0',
AUTO_OSD: '1',
AUTO_ONLY: '2',
AUTO: '3',
SINGLE_COLUMN: '4',
SINGLE_BLOCK_VERT_TEXT: '5',
SINGLE_BLOCK: '6',
SINGLE_LINE: '7',
SINGLE_WORD: '8',
CIRCLE_WORD: '9',
SINGLE_CHAR: '10',
SPARSE_TEXT: '11',
SPARSE_TEXT_OSD: '12',
RAW_LINE: '13',
};
@@ -0,0 +1,11 @@
'use strict';
module.exports = {
/*
* Use BlobURL for worker script by default
* TODO: remove this option
*
*/
workerBlobURL: true,
logger: () => {},
};
@@ -0,0 +1,7 @@
'use strict';
module.exports = {
COLOR: 0,
GREY: 1,
BINARY: 2,
};
@@ -0,0 +1,220 @@
'use strict';
/*
* languages with existing tesseract traineddata
* https://tesseract-ocr.github.io/tessdoc/Data-Files#data-files-for-version-400-november-29-2016
*/
/**
* @typedef {object} Languages
* @property {string} AFR Afrikaans
* @property {string} AMH Amharic
* @property {string} ARA Arabic
* @property {string} ASM Assamese
* @property {string} AZE Azerbaijani
* @property {string} AZE_CYRL Azerbaijani - Cyrillic
* @property {string} BEL Belarusian
* @property {string} BEN Bengali
* @property {string} BOD Tibetan
* @property {string} BOS Bosnian
* @property {string} BUL Bulgarian
* @property {string} CAT Catalan; Valencian
* @property {string} CEB Cebuano
* @property {string} CES Czech
* @property {string} CHI_SIM Chinese - Simplified
* @property {string} CHI_TRA Chinese - Traditional
* @property {string} CHR Cherokee
* @property {string} CYM Welsh
* @property {string} DAN Danish
* @property {string} DEU German
* @property {string} DZO Dzongkha
* @property {string} ELL Greek, Modern (1453-)
* @property {string} ENG English
* @property {string} ENM English, Middle (1100-1500)
* @property {string} EPO Esperanto
* @property {string} EST Estonian
* @property {string} EUS Basque
* @property {string} FAS Persian
* @property {string} FIN Finnish
* @property {string} FRA French
* @property {string} FRK German Fraktur
* @property {string} FRM French, Middle (ca. 1400-1600)
* @property {string} GLE Irish
* @property {string} GLG Galician
* @property {string} GRC Greek, Ancient (-1453)
* @property {string} GUJ Gujarati
* @property {string} HAT Haitian; Haitian Creole
* @property {string} HEB Hebrew
* @property {string} HIN Hindi
* @property {string} HRV Croatian
* @property {string} HUN Hungarian
* @property {string} IKU Inuktitut
* @property {string} IND Indonesian
* @property {string} ISL Icelandic
* @property {string} ITA Italian
* @property {string} ITA_OLD Italian - Old
* @property {string} JAV Javanese
* @property {string} JPN Japanese
* @property {string} KAN Kannada
* @property {string} KAT Georgian
* @property {string} KAT_OLD Georgian - Old
* @property {string} KAZ Kazakh
* @property {string} KHM Central Khmer
* @property {string} KIR Kirghiz; Kyrgyz
* @property {string} KOR Korean
* @property {string} KUR Kurdish
* @property {string} LAO Lao
* @property {string} LAT Latin
* @property {string} LAV Latvian
* @property {string} LIT Lithuanian
* @property {string} MAL Malayalam
* @property {string} MAR Marathi
* @property {string} MKD Macedonian
* @property {string} MLT Maltese
* @property {string} MSA Malay
* @property {string} MYA Burmese
* @property {string} NEP Nepali
* @property {string} NLD Dutch; Flemish
* @property {string} NOR Norwegian
* @property {string} ORI Oriya
* @property {string} PAN Panjabi; Punjabi
* @property {string} POL Polish
* @property {string} POR Portuguese
* @property {string} PUS Pushto; Pashto
* @property {string} RON Romanian; Moldavian; Moldovan
* @property {string} RUS Russian
* @property {string} SAN Sanskrit
* @property {string} SIN Sinhala; Sinhalese
* @property {string} SLK Slovak
* @property {string} SLV Slovenian
* @property {string} SPA Spanish; Castilian
* @property {string} SPA_OLD Spanish; Castilian - Old
* @property {string} SQI Albanian
* @property {string} SRP Serbian
* @property {string} SRP_LATN Serbian - Latin
* @property {string} SWA Swahili
* @property {string} SWE Swedish
* @property {string} SYR Syriac
* @property {string} TAM Tamil
* @property {string} TEL Telugu
* @property {string} TGK Tajik
* @property {string} TGL Tagalog
* @property {string} THA Thai
* @property {string} TIR Tigrinya
* @property {string} TUR Turkish
* @property {string} UIG Uighur; Uyghur
* @property {string} UKR Ukrainian
* @property {string} URD Urdu
* @property {string} UZB Uzbek
* @property {string} UZB_CYRL Uzbek - Cyrillic
* @property {string} VIE Vietnamese
* @property {string} YID Yiddish
*/
/**
* @type {Languages}
*/
module.exports = {
AFR: 'afr',
AMH: 'amh',
ARA: 'ara',
ASM: 'asm',
AZE: 'aze',
AZE_CYRL: 'aze_cyrl',
BEL: 'bel',
BEN: 'ben',
BOD: 'bod',
BOS: 'bos',
BUL: 'bul',
CAT: 'cat',
CEB: 'ceb',
CES: 'ces',
CHI_SIM: 'chi_sim',
CHI_TRA: 'chi_tra',
CHR: 'chr',
CYM: 'cym',
DAN: 'dan',
DEU: 'deu',
DZO: 'dzo',
ELL: 'ell',
ENG: 'eng',
ENM: 'enm',
EPO: 'epo',
EST: 'est',
EUS: 'eus',
FAS: 'fas',
FIN: 'fin',
FRA: 'fra',
FRK: 'frk',
FRM: 'frm',
GLE: 'gle',
GLG: 'glg',
GRC: 'grc',
GUJ: 'guj',
HAT: 'hat',
HEB: 'heb',
HIN: 'hin',
HRV: 'hrv',
HUN: 'hun',
IKU: 'iku',
IND: 'ind',
ISL: 'isl',
ITA: 'ita',
ITA_OLD: 'ita_old',
JAV: 'jav',
JPN: 'jpn',
KAN: 'kan',
KAT: 'kat',
KAT_OLD: 'kat_old',
KAZ: 'kaz',
KHM: 'khm',
KIR: 'kir',
KOR: 'kor',
KUR: 'kur',
LAO: 'lao',
LAT: 'lat',
LAV: 'lav',
LIT: 'lit',
MAL: 'mal',
MAR: 'mar',
MKD: 'mkd',
MLT: 'mlt',
MSA: 'msa',
MYA: 'mya',
NEP: 'nep',
NLD: 'nld',
NOR: 'nor',
ORI: 'ori',
PAN: 'pan',
POL: 'pol',
POR: 'por',
PUS: 'pus',
RON: 'ron',
RUS: 'rus',
SAN: 'san',
SIN: 'sin',
SLK: 'slk',
SLV: 'slv',
SPA: 'spa',
SPA_OLD: 'spa_old',
SQI: 'sqi',
SRP: 'srp',
SRP_LATN: 'srp_latn',
SWA: 'swa',
SWE: 'swe',
SYR: 'syr',
TAM: 'tam',
TEL: 'tel',
TGK: 'tgk',
TGL: 'tgl',
THA: 'tha',
TIR: 'tir',
TUR: 'tur',
UIG: 'uig',
UKR: 'ukr',
URD: 'urd',
UZB: 'uzb',
UZB_CYRL: 'uzb_cyrl',
VIE: 'vie',
YID: 'yid',
};
+23
View File
@@ -0,0 +1,23 @@
'use strict';
const getId = require('./utils/getId');
let jobCounter = 0;
module.exports = ({
id: _id,
action,
payload = {},
}) => {
let id = _id;
if (typeof id === 'undefined') {
id = getId('Job', jobCounter);
jobCounter += 1;
}
return {
id,
action,
payload,
};
};
@@ -0,0 +1,82 @@
'use strict';
const createJob = require('./createJob');
const { log } = require('./utils/log');
const getId = require('./utils/getId');
let schedulerCounter = 0;
module.exports = () => {
const id = getId('Scheduler', schedulerCounter);
const workers = {};
const runningWorkers = {};
let jobQueue = [];
schedulerCounter += 1;
const getQueueLen = () => jobQueue.length;
const getNumWorkers = () => Object.keys(workers).length;
const dequeue = () => {
if (jobQueue.length !== 0) {
const wIds = Object.keys(workers);
for (let i = 0; i < wIds.length; i += 1) {
if (typeof runningWorkers[wIds[i]] === 'undefined') {
jobQueue[0](workers[wIds[i]]);
break;
}
}
}
};
const queue = (action, payload) => (
new Promise((resolve, reject) => {
const job = createJob({ action, payload });
jobQueue.push(async (w) => {
jobQueue.shift();
runningWorkers[w.id] = job;
try {
resolve(await w[action].apply(this, [...payload, job.id]));
} catch (err) {
reject(err);
} finally {
delete runningWorkers[w.id];
dequeue();
}
});
log(`[${id}]: Add ${job.id} to JobQueue`);
log(`[${id}]: JobQueue length=${jobQueue.length}`);
dequeue();
})
);
const addWorker = (w) => {
workers[w.id] = w;
log(`[${id}]: Add ${w.id}`);
log(`[${id}]: Number of workers=${getNumWorkers()}`);
dequeue();
return w.id;
};
const addJob = async (action, ...payload) => {
if (getNumWorkers() === 0) {
throw Error(`[${id}]: You need to have at least one worker before adding jobs`);
}
return queue(action, payload);
};
const terminate = async () => {
Object.keys(workers).forEach(async (wid) => {
await workers[wid].terminate();
});
jobQueue = [];
};
return {
addWorker,
addJob,
terminate,
getQueueLen,
getNumWorkers,
};
};
@@ -0,0 +1,246 @@
'use strict';
const resolvePaths = require('./utils/resolvePaths');
const createJob = require('./createJob');
const { log } = require('./utils/log');
const getId = require('./utils/getId');
const OEM = require('./constants/OEM');
const {
defaultOptions,
spawnWorker,
terminateWorker,
onMessage,
loadImage,
send,
} = require('./worker/node');
let workerCounter = 0;
module.exports = async (langs = 'eng', oem = OEM.LSTM_ONLY, _options = {}, config = {}) => {
const id = getId('Worker', workerCounter);
const {
logger,
errorHandler,
...options
} = resolvePaths({
...defaultOptions,
..._options,
});
const promises = {};
// Current langs, oem, and config file.
// Used if the user ever re-initializes the worker using `worker.reinitialize`.
const currentLangs = typeof langs === 'string' ? langs.split('+') : langs;
let currentOem = oem;
let currentConfig = config;
const lstmOnlyCore = [OEM.DEFAULT, OEM.LSTM_ONLY].includes(oem) && !options.legacyCore;
let workerResReject;
let workerResResolve;
const workerRes = new Promise((resolve, reject) => {
workerResResolve = resolve;
workerResReject = reject;
});
const workerError = (event) => { workerResReject(event.message); };
let worker = spawnWorker(options);
worker.onerror = workerError;
workerCounter += 1;
const startJob = ({ id: jobId, action, payload }) => (
new Promise((resolve, reject) => {
log(`[${id}]: Start ${jobId}, action=${action}`);
// Using both `action` and `jobId` in case user provides non-unique `jobId`.
const promiseId = `${action}-${jobId}`;
promises[promiseId] = { resolve, reject };
send(worker, {
workerId: id,
jobId,
action,
payload,
});
})
);
const load = () => (
console.warn('`load` is depreciated and should be removed from code (workers now come pre-loaded)')
);
const loadInternal = (jobId) => (
startJob(createJob({
id: jobId, action: 'load', payload: { options: { lstmOnly: lstmOnlyCore, corePath: options.corePath, logging: options.logging } },
}))
);
const writeText = (path, text, jobId) => (
startJob(createJob({
id: jobId,
action: 'FS',
payload: { method: 'writeFile', args: [path, text] },
}))
);
const readText = (path, jobId) => (
startJob(createJob({
id: jobId,
action: 'FS',
payload: { method: 'readFile', args: [path, { encoding: 'utf8' }] },
}))
);
const removeFile = (path, jobId) => (
startJob(createJob({
id: jobId,
action: 'FS',
payload: { method: 'unlink', args: [path] },
}))
);
const FS = (method, args, jobId) => (
startJob(createJob({
id: jobId,
action: 'FS',
payload: { method, args },
}))
);
const loadLanguageInternal = (_langs, jobId) => startJob(createJob({
id: jobId,
action: 'loadLanguage',
payload: {
langs: _langs,
options: {
langPath: options.langPath,
dataPath: options.dataPath,
cachePath: options.cachePath,
cacheMethod: options.cacheMethod,
gzip: options.gzip,
lstmOnly: [OEM.DEFAULT, OEM.LSTM_ONLY].includes(currentOem)
&& !options.legacyLang,
},
},
}));
const initializeInternal = (_langs, _oem, _config, jobId) => (
startJob(createJob({
id: jobId,
action: 'initialize',
payload: { langs: _langs, oem: _oem, config: _config },
}))
);
const reinitialize = (langs = 'eng', oem, config, jobId) => { // eslint-disable-line
if (lstmOnlyCore && [OEM.TESSERACT_ONLY, OEM.TESSERACT_LSTM_COMBINED].includes(oem)) throw Error('Legacy model requested but code missing.');
const _oem = oem || currentOem;
currentOem = _oem;
const _config = config || currentConfig;
currentConfig = _config;
// Only load langs that are not already loaded.
// This logic fails if the user downloaded the LSTM-only English data for a language
// and then uses `worker.reinitialize` to switch to the Legacy engine.
// However, the correct data will still be downloaded after initialization fails
// and this can be avoided entirely if the user loads the correct data ahead of time.
const langsArr = typeof langs === 'string' ? langs.split('+') : langs;
const _langs = langsArr.filter((x) => !currentLangs.includes(x));
currentLangs.push(..._langs);
if (_langs.length > 0) {
return loadLanguageInternal(_langs, jobId)
.then(() => initializeInternal(langs, _oem, _config, jobId));
}
return initializeInternal(langs, _oem, _config, jobId);
};
const setParameters = (params = {}, jobId) => (
startJob(createJob({
id: jobId,
action: 'setParameters',
payload: { params },
}))
);
const recognize = async (image, opts = {}, output = {
text: true,
}, jobId) => (
startJob(createJob({
id: jobId,
action: 'recognize',
payload: { image: await loadImage(image), options: opts, output },
}))
);
const detect = async (image, jobId) => {
if (lstmOnlyCore) throw Error('`worker.detect` requires Legacy model, which was not loaded.');
return startJob(createJob({
id: jobId,
action: 'detect',
payload: { image: await loadImage(image) },
}));
};
const terminate = async () => {
if (worker !== null) {
/*
await startJob(createJob({
id: jobId,
action: 'terminate',
}));
*/
terminateWorker(worker);
worker = null;
}
return Promise.resolve();
};
onMessage(worker, ({
workerId, jobId, status, action, data,
}) => {
const promiseId = `${action}-${jobId}`;
if (status === 'resolve') {
log(`[${workerId}]: Complete ${jobId}`);
promises[promiseId].resolve({ jobId, data });
delete promises[promiseId];
} else if (status === 'reject') {
promises[promiseId].reject(data);
delete promises[promiseId];
if (action === 'load') workerResReject(data);
if (errorHandler) {
errorHandler(data);
} else {
throw Error(data);
}
} else if (status === 'progress') {
logger({ ...data, userJobId: jobId });
}
});
const resolveObj = {
id,
worker,
load,
writeText,
readText,
removeFile,
FS,
reinitialize,
setParameters,
recognize,
detect,
terminate,
};
loadInternal()
.then(() => loadLanguageInternal(langs))
.then(() => initializeInternal(langs, oem, config))
.then(() => workerResResolve(resolveObj))
.catch(() => {});
return workerRes;
};
+234
View File
@@ -0,0 +1,234 @@
declare namespace Tesseract {
function createScheduler(): Scheduler
function createWorker(langs?: string | string[] | Lang[], oem?: OEM, options?: Partial<WorkerOptions>, config?: string | Partial<InitOptions>): Promise<Worker>
function setLogging(logging: boolean): void
function recognize(image: ImageLike, langs?: string, options?: Partial<WorkerOptions>): Promise<RecognizeResult>
function detect(image: ImageLike, options?: Partial<WorkerOptions>): any
interface Scheduler {
addWorker(worker: Worker): string
addJob(action: 'recognize', ...args: Parameters<Worker['recognize']>): Promise<RecognizeResult>
addJob(action: 'detect', ...args: Parameters<Worker['detect']>): Promise<DetectResult>
terminate(): Promise<any>
getQueueLen(): number
getNumWorkers(): number
}
interface Worker {
load(jobId?: string): Promise<ConfigResult>
writeText(path: string, text: string, jobId?: string): Promise<ConfigResult>
readText(path: string, jobId?: string): Promise<ConfigResult>
removeText(path: string, jobId?: string): Promise<ConfigResult>
FS(method: string, args: any[], jobId?: string): Promise<ConfigResult>
reinitialize(langs?: string | Lang[], oem?: OEM, config?: string | Partial<InitOptions>, jobId?: string): Promise<ConfigResult>
setParameters(params: Partial<WorkerParams>, jobId?: string): Promise<ConfigResult>
getImage(type: imageType): string
recognize(image: ImageLike, options?: Partial<RecognizeOptions>, output?: Partial<OutputFormats>, jobId?: string): Promise<RecognizeResult>
detect(image: ImageLike, jobId?: string): Promise<DetectResult>
terminate(jobId?: string): Promise<ConfigResult>
}
interface Lang {
code: string;
data: unknown;
}
interface InitOptions {
load_system_dawg: string
load_freq_dawg: string
load_unambig_dawg: string
load_punc_dawg: string
load_number_dawg: string
load_bigram_dawg: string
}
type LoggerMessage = {
jobId: string
progress: number
status: string
userJobId: string
workerId: string
}
interface WorkerOptions {
corePath: string
langPath: string
cachePath: string
dataPath: string
workerPath: string
cacheMethod: string
workerBlobURL: boolean
gzip: boolean
legacyLang: boolean
legacyCore: boolean
logger: (arg: LoggerMessage) => void,
errorHandler: (arg: any) => void
}
interface WorkerParams {
tessedit_pageseg_mode: PSM
tessedit_char_whitelist: string
tessedit_char_blacklist: string
preserve_interword_spaces: string
user_defined_dpi: string
[propName: string]: any
}
interface OutputFormats {
text: boolean;
blocks: boolean;
layoutBlocks: boolean;
hocr: boolean;
tsv: boolean;
box: boolean;
unlv: boolean;
osd: boolean;
pdf: boolean;
imageColor: boolean;
imageGrey: boolean;
imageBinary: boolean;
debug: boolean;
}
interface RecognizeOptions {
rectangle: Rectangle
pdfTitle: string
pdfTextOnly: boolean
rotateAuto: boolean
rotateRadians: number
}
interface ConfigResult {
jobId: string
data: any
}
interface RecognizeResult {
jobId: string
data: Page
}
interface DetectResult {
jobId: string
data: DetectData
}
interface DetectData {
tesseract_script_id: number | null
script: string | null
script_confidence: number | null
orientation_degrees: number | null
orientation_confidence: number | null
}
interface Rectangle {
left: number
top: number
width: number
height: number
}
enum OEM {
TESSERACT_ONLY,
LSTM_ONLY,
TESSERACT_LSTM_COMBINED,
DEFAULT,
}
enum PSM {
OSD_ONLY = '0',
AUTO_OSD = '1',
AUTO_ONLY = '2',
AUTO = '3',
SINGLE_COLUMN = '4',
SINGLE_BLOCK_VERT_TEXT = '5',
SINGLE_BLOCK = '6',
SINGLE_LINE = '7',
SINGLE_WORD = '8',
CIRCLE_WORD = '9',
SINGLE_CHAR = '10',
SPARSE_TEXT = '11',
SPARSE_TEXT_OSD = '12',
RAW_LINE = '13'
}
const enum imageType {
COLOR = 0,
GREY = 1,
BINARY = 2
}
type ImageLike = string | HTMLImageElement | HTMLCanvasElement | HTMLVideoElement
| CanvasRenderingContext2D | File | Blob | Buffer | OffscreenCanvas;
interface Block {
paragraphs: Paragraph[];
text: string;
confidence: number;
bbox: Bbox;
blocktype: string;
page: Page;
}
interface Baseline {
x0: number;
y0: number;
x1: number;
y1: number;
}
interface RowAttributes {
ascenders: number;
descenders: number;
rowHeight: number;
}
interface Bbox {
x0: number;
y0: number;
x1: number;
y1: number;
}
interface Line {
words: Word[];
text: string;
confidence: number;
baseline: Baseline;
rowAttributes: RowAttributes
bbox: Bbox;
}
interface Paragraph {
lines: Line[];
text: string;
confidence: number;
bbox: Bbox;
is_ltr: boolean;
}
interface Symbol {
text: string;
confidence: number;
bbox: Bbox;
is_superscript: boolean;
is_subscript: boolean;
is_dropcap: boolean;
}
interface Choice {
text: string;
confidence: number;
}
interface Word {
symbols: Symbol[];
choices: Choice[];
text: string;
confidence: number;
bbox: Bbox;
font_name: string;
}
interface Page {
blocks: Block[] | null;
confidence: number;
oem: string;
osd: string;
psm: string;
text: string;
version: string;
hocr: string | null;
tsv: string | null;
box: string | null;
unlv: string | null;
sd: string | null;
imageColor: string | null;
imageGrey: string | null;
imageBinary: string | null;
rotateRadians: number | null;
pdf: number[] | null;
debug: string | null;
}
}
export = Tesseract;
export as namespace Tesseract;
+29
View File
@@ -0,0 +1,29 @@
'use strict';
/**
*
* Entry point for tesseract.js, should be the entry when bundling.
*
* @fileoverview entry point for tesseract.js
* @author Kevin Kwok <antimatter15@gmail.com>
* @author Guillermo Webster <gui@mit.edu>
* @author Jerome Wu <jeromewus@gmail.com>
*/
require('regenerator-runtime/runtime');
const createScheduler = require('./createScheduler');
const createWorker = require('./createWorker');
const Tesseract = require('./Tesseract');
const languages = require('./constants/languages');
const OEM = require('./constants/OEM');
const PSM = require('./constants/PSM');
const { setLogging } = require('./utils/log');
module.exports = {
languages,
OEM,
PSM,
createScheduler,
createWorker,
setLogging,
...Tesseract,
};
@@ -0,0 +1,19 @@
'use strict';
module.exports = (key) => {
const env = {};
if (typeof WorkerGlobalScope !== 'undefined') {
env.type = 'webworker';
} else if (typeof document === 'object') {
env.type = 'browser';
} else if (typeof process === 'object' && typeof require === 'function') {
env.type = 'node';
}
if (typeof key === 'undefined') {
return env;
}
return env[key];
};
@@ -0,0 +1,5 @@
'use strict';
module.exports = (prefix, cnt) => (
`${prefix}-${cnt}-${Math.random().toString(16).slice(3, 8)}`
);
+11
View File
@@ -0,0 +1,11 @@
'use strict';
let logging = false;
exports.logging = logging;
exports.setLogging = (_logging) => {
logging = _logging;
};
exports.log = (...args) => (logging ? console.log.apply(this, args) : null);
@@ -0,0 +1,15 @@
'use strict';
const isBrowser = require('./getEnvironment')('type') === 'browser';
const resolveURL = isBrowser ? s => (new URL(s, window.location.href)).href : s => s; // eslint-disable-line
module.exports = (options) => {
const opts = { ...options };
['corePath', 'workerPath', 'langPath'].forEach((key) => {
if (options[key]) {
opts[key] = resolveURL(opts[key]);
}
});
return opts;
};
@@ -0,0 +1,12 @@
'use strict';
const { set, get, del } = require('idb-keyval');
module.exports = {
readCache: get,
writeCache: set,
deleteCache: del,
checkCache: (path) => (
get(path).then((v) => typeof v !== 'undefined')
),
};
@@ -0,0 +1,59 @@
'use strict';
const { simd, relaxedSimd } = require('wasm-feature-detect');
const coreVersion = require('../../../package.json').dependencies['tesseract.js-core'];
module.exports = async (lstmOnly, corePath, res) => {
if (typeof global.TesseractCore === 'undefined') {
const statusText = 'loading tesseract core';
res.progress({ status: statusText, progress: 0 });
// If the user specifies a core path, we use that
// Otherwise, default to CDN
const corePathImport = corePath || `https://cdn.jsdelivr.net/npm/tesseract.js-core@v${coreVersion.substring(1)}`;
// If a user specifies a specific JavaScript file, load that file.
// Otherwise, assume a directory has been provided, and load either
// tesseract-core.wasm.js or tesseract-core-simd.wasm.js depending
// on whether this device has SIMD support.
let corePathImportFile;
if (corePathImport.slice(-2) === 'js') {
corePathImportFile = corePathImport;
} else {
const simdSupport = await simd();
const relaxedSimdSupport = await relaxedSimd();
if (relaxedSimdSupport) {
if (lstmOnly) {
corePathImportFile = `${corePathImport.replace(/\/$/, '')}/tesseract-core-relaxedsimd-lstm.wasm.js`;
} else {
corePathImportFile = `${corePathImport.replace(/\/$/, '')}/tesseract-core-relaxedsimd.wasm.js`;
}
} else if (simdSupport) {
if (lstmOnly) {
corePathImportFile = `${corePathImport.replace(/\/$/, '')}/tesseract-core-simd-lstm.wasm.js`;
} else {
corePathImportFile = `${corePathImport.replace(/\/$/, '')}/tesseract-core-simd.wasm.js`;
}
} else if (lstmOnly) {
corePathImportFile = `${corePathImport.replace(/\/$/, '')}/tesseract-core-lstm.wasm.js`;
} else {
corePathImportFile = `${corePathImport.replace(/\/$/, '')}/tesseract-core.wasm.js`;
}
}
// Create a module named `global.TesseractCore`
global.importScripts(corePathImportFile);
// Tesseract.js-core versions through 4.0.3 create a module named `global.TesseractCoreWASM`,
// so we account for that here to preserve backwards compatibility.
// This part can be removed when Tesseract.js-core v4.0.3 becomes incompatible for other reasons
if (typeof global.TesseractCore === 'undefined' && typeof global.TesseractCoreWASM !== 'undefined' && typeof WebAssembly === 'object') {
global.TesseractCore = global.TesseractCoreWASM;
} else if (typeof global.TesseractCore === 'undefined') {
throw Error('Failed to load TesseractCore');
}
res.progress({ status: statusText, progress: 1 });
}
return global.TesseractCore;
};
@@ -0,0 +1,3 @@
'use strict';
module.exports = require('zlibjs').gunzipSync;
@@ -0,0 +1,34 @@
'use strict';
/**
*
* Browser worker scripts
*
* @fileoverview Browser worker implementation
* @author Kevin Kwok <antimatter15@gmail.com>
* @author Guillermo Webster <gui@mit.edu>
* @author Jerome Wu <jeromewus@gmail.com>
*/
const worker = require('..');
const getCore = require('./getCore');
const gunzip = require('./gunzip');
const cache = require('./cache');
/*
* register message handler
*/
global.addEventListener('message', ({ data }) => {
worker.dispatchHandlers(data, (obj) => postMessage(obj));
});
/*
* getCore is a sync function to load and return
* TesseractCore.
*/
worker.setAdapter({
getCore,
gunzip,
fetch: () => {},
...cache,
});
@@ -0,0 +1,21 @@
'use strict';
/*
* default output formats for tesseract.js
*/
module.exports = {
text: true,
blocks: false,
layoutBlocks: false,
hocr: false,
tsv: false,
box: false,
unlv: false,
osd: false,
pdf: false,
imageColor: false,
imageGrey: false,
imageBinary: false,
debug: false,
};
@@ -0,0 +1,543 @@
'use strict';
/**
*
* Worker script for browser and node
*
* @fileoverview Worker script for browser and node
* @author Kevin Kwok <antimatter15@gmail.com>
* @author Guillermo Webster <gui@mit.edu>
* @author Jerome Wu <jeromewus@gmail.com>
*/
require('regenerator-runtime/runtime');
const isURL = require('is-url');
const dump = require('./utils/dump');
const env = require('../utils/getEnvironment')('type');
const setImage = require('./utils/setImage');
const defaultOutput = require('./constants/defaultOutput');
const { log, setLogging } = require('../utils/log');
const PSM = require('../constants/PSM');
/*
* Tesseract Module returned by TesseractCore.
*/
let TessModule;
/*
* TessearctBaseAPI instance
*/
let api = null;
let latestJob;
let adapter = {};
let params = {};
let loadLanguageLangsWorker;
let loadLanguageOptionsWorker;
let dataFromCache = false;
const load = async ({ workerId, jobId, payload: { options: { lstmOnly, corePath, logging } } }, res) => { // eslint-disable-line max-len
setLogging(logging);
const statusText = 'initializing tesseract';
if (!TessModule) {
const Core = await adapter.getCore(lstmOnly, corePath, res);
res.progress({ workerId, status: statusText, progress: 0 });
Core({
TesseractProgress(percent) {
latestJob.progress({
workerId,
jobId,
status: 'recognizing text',
progress: Math.max(0, (percent - 30) / 70),
});
},
}).then((tessModule) => {
TessModule = tessModule;
res.progress({ workerId, status: statusText, progress: 1 });
res.resolve({ loaded: true });
});
} else {
res.resolve({ loaded: true });
}
};
const FS = async ({ workerId, payload: { method, args } }, res) => {
log(`[${workerId}]: FS.${method}`);
res.resolve(TessModule.FS[method](...args));
};
const loadLanguage = async ({
workerId,
payload: {
langs,
options: {
langPath,
dataPath,
cachePath,
cacheMethod,
gzip = true,
lstmOnly,
},
},
},
res) => {
// Remember options for later, as cache may be deleted if `initialize` fails
loadLanguageLangsWorker = langs;
loadLanguageOptionsWorker = {
langPath,
dataPath,
cachePath,
cacheMethod,
gzip,
lstmOnly,
};
const statusText = 'loading language traineddata';
const langsArr = typeof langs === 'string' ? langs.split('+') : langs;
let progress = 0;
const loadAndGunzipFile = async (_lang) => {
const lang = typeof _lang === 'string' ? _lang : _lang.code;
const readCache = ['refresh', 'none'].includes(cacheMethod)
? () => Promise.resolve()
: adapter.readCache;
let data = null;
let newData = false;
// Check for existing .traineddata file in cache
// This automatically fails if cacheMethod is set to 'refresh' or 'none'
try {
const _data = await readCache(`${cachePath || '.'}/${lang}.traineddata`);
if (typeof _data !== 'undefined') {
log(`[${workerId}]: Load ${lang}.traineddata from cache`);
data = _data;
dataFromCache = true;
} else {
throw Error('Not found in cache');
}
// Attempt to fetch new .traineddata file
} catch (e) {
newData = true;
log(`[${workerId}]: Load ${lang}.traineddata from ${langPath}`);
if (typeof _lang === 'string') {
let path = null;
// If `langPath` if not explicitly set by the user, the jsdelivr CDN is used.
// Data supporting the Legacy model is only included if `lstmOnly` is not true.
// This saves a significant amount of data for the majority of users that use LSTM only.
const langPathDownload = langPath || (lstmOnly ? `https://cdn.jsdelivr.net/npm/@tesseract.js-data/${lang}/4.0.0_best_int` : `https://cdn.jsdelivr.net/npm/@tesseract.js-data/${lang}/4.0.0`);
// For Node.js, langPath may be a URL or local file path
// The is-url package is used to tell the difference
// For the browser version, langPath is assumed to be a URL
if (env !== 'node' || isURL(langPathDownload) || langPathDownload.startsWith('moz-extension://') || langPathDownload.startsWith('chrome-extension://') || langPathDownload.startsWith('file://')) { /** When langPathDownload is an URL */
path = langPathDownload.replace(/\/$/, '');
}
// langPathDownload is a URL, fetch from server
if (path !== null) {
const fetchUrl = `${path}/${lang}.traineddata${gzip ? '.gz' : ''}`;
const resp = await (env === 'webworker' ? fetch : adapter.fetch)(fetchUrl);
if (!resp.ok) {
throw Error(`Network error while fetching ${fetchUrl}. Response code: ${resp.status}`);
}
data = new Uint8Array(await resp.arrayBuffer());
// langPathDownload is a local file, read .traineddata from local filesystem
// (adapter.readCache is a generic file read function in Node.js version)
} else {
data = await adapter.readCache(`${langPathDownload}/${lang}.traineddata${gzip ? '.gz' : ''}`);
}
} else {
data = _lang.data; // eslint-disable-line
}
}
progress += 0.5 / langsArr.length;
if (res) res.progress({ workerId, status: statusText, progress });
// Check for gzip magic numbers (1F and 8B in hex)
const isGzip = (data[0] === 31 && data[1] === 139) || (data[1] === 31 && data[0] === 139);
if (isGzip) {
data = adapter.gunzip(data);
}
if (TessModule) {
if (dataPath) {
try {
TessModule.FS.mkdir(dataPath);
} catch (err) {
if (res) res.reject(err.toString());
}
}
TessModule.FS.writeFile(`${dataPath || '.'}/${lang}.traineddata`, data);
}
if (newData && ['write', 'refresh', undefined].includes(cacheMethod)) {
try {
await adapter.writeCache(`${cachePath || '.'}/${lang}.traineddata`, data);
} catch (err) {
log(`[${workerId}]: Failed to write ${lang}.traineddata to cache due to error:`);
log(err.toString());
}
}
progress += 0.5 / langsArr.length;
// Make sure last progress message is 1 (not 0.9999)
if (Math.round(progress * 100) === 100) progress = 1;
if (res) res.progress({ workerId, status: statusText, progress });
};
if (res) res.progress({ workerId, status: statusText, progress: 0 });
try {
await Promise.all(langsArr.map(loadAndGunzipFile));
if (res) res.resolve(langs);
} catch (err) {
if (res) res.reject(err.toString());
}
};
const setParameters = async ({ payload: { params: _params } }, res) => {
// A small number of parameters can only be set at initialization.
// These can only be set using (1) the `oem` argument of `initialize` (for setting the oem)
// or (2) the `config` argument of `initialize` (for all other settings).
// Attempting to set these using this function will have no impact so a warning is printed.
// This list is generated by searching the Tesseract codebase for parameters
// defined with `[type]_INIT_MEMBER` rather than `[type]_MEMBER`.
const initParamNames = ['ambigs_debug_level', 'user_words_suffix', 'user_patterns_suffix', 'user_patterns_suffix',
'load_system_dawg', 'load_freq_dawg', 'load_unambig_dawg', 'load_punc_dawg', 'load_number_dawg', 'load_bigram_dawg',
'tessedit_ocr_engine_mode', 'tessedit_init_config_only', 'language_model_ngram_on', 'language_model_use_sigmoidal_certainty'];
const initParamStr = Object.keys(_params)
.filter((k) => initParamNames.includes(k))
.join(', ');
if (initParamStr.length > 0) console.log(`Attempted to set parameters that can only be set during initialization: ${initParamStr}`);
Object.keys(_params)
.filter((k) => !k.startsWith('tessjs_'))
.forEach((key) => {
api.SetVariable(key, _params[key]);
});
params = { ...params, ..._params };
if (typeof res !== 'undefined') {
res.resolve(params);
}
};
const initialize = async ({
workerId,
payload: { langs: _langs, oem, config },
}, res) => {
const langs = (typeof _langs === 'string')
? _langs
: _langs.map((l) => ((typeof l === 'string') ? l : l.data)).join('+');
const statusText = 'initializing api';
try {
res.progress({
workerId, status: statusText, progress: 0,
});
if (api !== null) {
api.End();
}
let configFile;
let configStr;
// config argument may either be config file text, or object with key/value pairs
// In the latter case we convert to config file text here
if (config && typeof config === 'object' && Object.keys(config).length > 0) {
configStr = JSON.stringify(config).replace(/,/g, '\n').replace(/:/g, ' ').replace(/["'{}]/g, '');
} else if (config && typeof config === 'string') {
configStr = config;
}
if (typeof configStr === 'string') {
configFile = '/config';
TessModule.FS.writeFile(configFile, configStr);
}
api = new TessModule.TessBaseAPI();
let status = api.Init(null, langs, oem, configFile);
if (status === -1) {
// Cache is deleted if initialization fails to avoid keeping bad data in cache
// This assumes that initialization failing only occurs due to bad .traineddata,
// this should be refined if other reasons for init failing are encountered.
// The "if" condition skips this section if either (1) cache is disabled [so the issue
// is definitely unrelated to cached data] or (2) cache is set to read-only
// [so we do not have permission to make any changes].
if (['write', 'refresh', undefined].includes(loadLanguageOptionsWorker.cacheMethod)) {
const langsArr = langs.split('+');
const delCachePromise = langsArr.map((lang) => adapter.deleteCache(`${loadLanguageOptionsWorker.cachePath || '.'}/${lang}.traineddata`));
await Promise.all(delCachePromise);
// Check for the case when (1) data was loaded from the cache and
// (2) the data does not support the requested OEM.
// In this case, loadLanguage is re-run and initialization is attempted a second time.
// This is because `loadLanguage` has no mechanism for checking whether the cached data
// supports the requested model, so this only becomes apparent when initialization fails.
// Check for this error message:
// eslint-disable-next-line
// "Tesseract (legacy) engine requested, but components are not present in ./eng.traineddata!!""
// The .wasm build of Tesseract saves this message in a separate file
// (in addition to the normal debug file location).
const debugStr = TessModule.FS.readFile('/debugDev.txt', { encoding: 'utf8', flags: 'a+' });
if (dataFromCache && /components are not present/.test(debugStr)) {
log('Data from cache missing requested OEM model. Attempting to refresh cache with new language data.');
// In this case, language data is re-loaded
await loadLanguage({ workerId, payload: { langs: loadLanguageLangsWorker, options: loadLanguageOptionsWorker } }); // eslint-disable-line max-len
status = api.Init(null, langs, oem, configFile);
if (status === -1) {
log('Language data refresh failed.');
const delCachePromise2 = langsArr.map((lang) => adapter.deleteCache(`${loadLanguageOptionsWorker.cachePath || '.'}/${lang}.traineddata`));
await Promise.all(delCachePromise2);
} else {
log('Language data refresh successful.');
}
}
}
}
if (status === -1) {
res.reject('initialization failed');
}
res.progress({
workerId, status: statusText, progress: 1,
});
res.resolve();
} catch (err) {
res.reject(err.toString());
}
};
// Combines default output with user-specified options and
// counts (1) total output formats requested and (2) outputs that require OCR
const processOutput = (output) => {
const workingOutput = JSON.parse(JSON.stringify(defaultOutput));
const nonRecOutputs = ['imageColor', 'imageGrey', 'imageBinary', 'layoutBlocks', 'debug'];
let recOutputCount = 0;
for (const prop of Object.keys(output)) {
workingOutput[prop] = output[prop];
}
for (const prop of Object.keys(workingOutput)) {
if (workingOutput[prop]) {
if (!nonRecOutputs.includes(prop)) {
recOutputCount += 1;
}
}
}
const skipRecognition = recOutputCount === 0;
return { workingOutput, skipRecognition };
};
// List of options for Tesseract.js (rather than passed through to Tesseract),
// not including those with prefix "tessjs_"
const tessjsOptions = ['rectangle', 'pdfTitle', 'pdfTextOnly', 'rotateAuto', 'rotateRadians'];
const recognize = async ({
payload: {
image, options, output,
},
}, res) => {
try {
const optionsTess = {};
if (typeof options === 'object' && Object.keys(options).length > 0) {
// The options provided by users contain a mix of options for Tesseract.js
// and parameters passed through to Tesseract.
for (const param of Object.keys(options)) {
if (!param.startsWith('tessjs_') && !tessjsOptions.includes(param)) {
optionsTess[param] = options[param];
}
}
}
if (output.debug) {
optionsTess.debug_file = '/debugInternal.txt';
TessModule.FS.writeFile('/debugInternal.txt', '');
}
// If any parameters are changed here they are changed back at the end
if (Object.keys(optionsTess).length > 0) {
api.SaveParameters();
for (const prop of Object.keys(optionsTess)) {
api.SetVariable(prop, optionsTess[prop]);
}
}
const { workingOutput, skipRecognition } = processOutput(output);
// When the auto-rotate option is True, setImage is called with no angle,
// then the angle is calculated by Tesseract and then setImage is re-called.
// Otherwise, setImage is called once using the user-provided rotateRadiansFinal value.
let rotateRadiansFinal;
if (options.rotateAuto) {
// The angle is only detected if auto page segmentation is used
// Therefore, if this is not the mode specified by the user, it is enabled temporarily here
const psmInit = api.GetPageSegMode();
let psmEdit = false;
if (![PSM.AUTO, PSM.AUTO_ONLY, PSM.OSD].includes(String(psmInit))) {
psmEdit = true;
api.SetVariable('tessedit_pageseg_mode', String(PSM.AUTO));
}
setImage(TessModule, api, image);
api.FindLines();
// The function GetAngle will be replaced with GetGradient in 4.0.4,
// but for now we want to maintain compatibility.
// We can switch to only using GetGradient in v5.
const rotateRadiansCalc = api.GetGradient ? api.GetGradient() : api.GetAngle();
// Restore user-provided PSM setting
if (psmEdit) {
api.SetVariable('tessedit_pageseg_mode', String(psmInit));
}
// Small angles (<0.005 radians/~0.3 degrees) are ignored to save on runtime
if (Math.abs(rotateRadiansCalc) >= 0.005) {
rotateRadiansFinal = rotateRadiansCalc;
setImage(TessModule, api, image, rotateRadiansFinal);
} else {
// Image needs to be reset if run with different PSM setting earlier
if (psmEdit) {
setImage(TessModule, api, image);
}
rotateRadiansFinal = 0;
}
} else {
rotateRadiansFinal = options.rotateRadians || 0;
setImage(TessModule, api, image, rotateRadiansFinal);
}
const rec = options.rectangle;
if (typeof rec === 'object') {
api.SetRectangle(rec.left, rec.top, rec.width, rec.height);
}
if (!skipRecognition) {
api.Recognize(null);
} else {
if (output.layoutBlocks) {
api.AnalyseLayout();
}
log('Skipping recognition: all output options requiring recognition are disabled.');
}
const { pdfTitle } = options;
const { pdfTextOnly } = options;
const result = dump(TessModule, api, workingOutput, { pdfTitle, pdfTextOnly, skipRecognition });
result.rotateRadians = rotateRadiansFinal;
if (output.debug) TessModule.FS.unlink('/debugInternal.txt');
if (Object.keys(optionsTess).length > 0) {
api.RestoreParameters();
}
res.resolve(result);
} catch (err) {
res.reject(err.toString());
}
};
const detect = async ({ payload: { image } }, res) => {
try {
setImage(TessModule, api, image);
const results = new TessModule.OSResults();
if (!api.DetectOS(results)) {
res.resolve({
tesseract_script_id: null,
script: null,
script_confidence: null,
orientation_degrees: null,
orientation_confidence: null,
});
} else {
const best = results.best_result;
const oid = best.orientation_id;
const sid = best.script_id;
res.resolve({
tesseract_script_id: sid,
script: results.unicharset.get_script_from_script_id(sid),
script_confidence: best.sconfidence,
orientation_degrees: [0, 270, 180, 90][oid],
orientation_confidence: best.oconfidence,
});
}
} catch (err) {
res.reject(err.toString());
}
};
const terminate = async (_, res) => {
try {
if (api !== null) {
api.End();
}
res.resolve({ terminated: true });
} catch (err) {
res.reject(err.toString());
}
};
/**
* dispatchHandlers
*
* @name dispatchHandlers
* @function worker data handler
* @access public
* @param {object} data
* @param {string} data.jobId - unique job id
* @param {string} data.action - action of the job, only recognize and detect for now
* @param {object} data.payload - data for the job
* @param {function} send - trigger job to work
*/
exports.dispatchHandlers = (packet, send) => {
const res = (status, data) => {
// Return only the necessary info to avoid sending unnecessarily large messages
const packetRes = {
jobId: packet.jobId,
workerId: packet.workerId,
action: packet.action,
};
send({
...packetRes,
status,
data,
});
};
res.resolve = res.bind(this, 'resolve');
res.reject = res.bind(this, 'reject');
res.progress = res.bind(this, 'progress');
latestJob = res;
({
load,
FS,
loadLanguage,
initialize,
setParameters,
recognize,
detect,
terminate,
})[packet.action](packet, res)
.catch((err) => res.reject(err.toString()));
};
/**
* setAdapter
*
* @name setAdapter
* @function
* @access public
* @param {object} adapter - implementation of the worker, different in browser and node environment
*/
exports.setAdapter = (_adapter) => {
adapter = _adapter;
};
@@ -0,0 +1,18 @@
'use strict';
const util = require('util');
const fs = require('fs');
module.exports = {
readCache: util.promisify(fs.readFile),
writeCache: util.promisify(fs.writeFile),
deleteCache: (path) => (
util.promisify(fs.unlink)(path)
.catch(() => {})
),
checkCache: (path) => (
util.promisify(fs.access)(path, fs.F_OK)
.then((err) => (err === null))
.catch(() => false)
),
};
@@ -0,0 +1,38 @@
'use strict';
const { simd, relaxedSimd } = require('wasm-feature-detect');
const OEM = require('../../constants/OEM');
let TesseractCore = null;
/*
* getCore is a sync function to load and return
* TesseractCore.
*/
module.exports = async (oem, _, res) => {
if (TesseractCore === null) {
const statusText = 'loading tesseract core';
const simdSupport = await simd();
const relaxedSimdSupport = await relaxedSimd();
res.progress({ status: statusText, progress: 0 });
if (relaxedSimdSupport) {
if ([OEM.DEFAULT, OEM.LSTM_ONLY].includes(oem)) {
TesseractCore = require('tesseract.js-core/tesseract-core-relaxedsimd-lstm');
} else {
TesseractCore = require('tesseract.js-core/tesseract-core-relaxedsimd');
}
} else if (simdSupport) {
if ([OEM.DEFAULT, OEM.LSTM_ONLY].includes(oem)) {
TesseractCore = require('tesseract.js-core/tesseract-core-simd-lstm');
} else {
TesseractCore = require('tesseract.js-core/tesseract-core-simd');
}
} else if ([OEM.DEFAULT, OEM.LSTM_ONLY].includes(oem)) {
TesseractCore = require('tesseract.js-core/tesseract-core-lstm');
} else {
TesseractCore = require('tesseract.js-core/tesseract-core');
}
res.progress({ status: statusText, progress: 1 });
}
return TesseractCore;
};
@@ -0,0 +1,3 @@
'use strict';
module.exports = require('zlib').gunzipSync;
@@ -0,0 +1,32 @@
'use strict';
/**
*
* Tesseract Worker Script for Node
*
* @fileoverview Node worker implementation
* @author Kevin Kwok <antimatter15@gmail.com>
* @author Guillermo Webster <gui@mit.edu>
* @author Jerome Wu <jeromewus@gmail.com>
*/
// Use built-in fetch if available, otherwise fallback to node-fetch
const fetch = global.fetch || require('node-fetch');
const { parentPort } = require('worker_threads');
const worker = require('..');
const getCore = require('./getCore');
const gunzip = require('./gunzip');
const cache = require('./cache');
/*
* register message handler
*/
parentPort.on('message', (packet) => {
worker.dispatchHandlers(packet, (obj) => parentPort.postMessage(obj));
});
worker.setAdapter({
getCore,
gunzip,
fetch,
...cache,
});
@@ -0,0 +1,58 @@
'use strict';
// Copied from https://gist.github.com/jonleighton/958841
// Copyright 2011 Jon Leighton, MIT LICENSE
/* eslint no-bitwise: 0 */
module.exports = (arrayBuffer) => {
let base64 = '';
const encodings = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
const bytes = new Uint8Array(arrayBuffer);
const { byteLength } = bytes;
const byteRemainder = byteLength % 3;
const mainLength = byteLength - byteRemainder;
let a; let b; let c; let
d;
let chunk;
// Main loop deals with bytes in chunks of 3
for (let i = 0; i < mainLength; i += 3) {
// Combine the three bytes into a single integer
chunk = (bytes[i] << 16) | (bytes[i + 1] << 8) | bytes[i + 2];
// Use bitmasks to extract 6-bit segments from the triplet
a = (chunk & 16515072) >> 18; // 16515072 = (2^6 - 1) << 18
b = (chunk & 258048) >> 12; // 258048 = (2^6 - 1) << 12
c = (chunk & 4032) >> 6; // 4032 = (2^6 - 1) << 6
d = chunk & 63; // 63 = 2^6 - 1
// Convert the raw binary segments to the appropriate ASCII encoding
base64 += encodings[a] + encodings[b] + encodings[c] + encodings[d];
}
// Deal with the remaining bytes and padding
if (byteRemainder === 1) {
chunk = bytes[mainLength];
a = (chunk & 252) >> 2; // 252 = (2^6 - 1) << 2
// Set the 4 least significant bits to zero
b = (chunk & 3) << 4; // 3 = 2^2 - 1
base64 += `${encodings[a] + encodings[b]}==`;
} else if (byteRemainder === 2) {
chunk = (bytes[mainLength] << 8) | bytes[mainLength + 1];
a = (chunk & 64512) >> 10; // 64512 = (2^6 - 1) << 10
b = (chunk & 1008) >> 4; // 1008 = (2^6 - 1) << 4
// Set the 2 least significant bits to zero
c = (chunk & 15) << 2; // 15 = 2^4 - 1
base64 += `${encodings[a] + encodings[b] + encodings[c]}=`;
}
return base64;
};
@@ -0,0 +1,89 @@
'use strict';
/**
*
* Dump data to a big JSON tree
*
* @fileoverview dump data to JSON tree
* @author Kevin Kwok <antimatter15@gmail.com>
* @author Guillermo Webster <gui@mit.edu>
* @author Jerome Wu <jeromewus@gmail.com>
*/
const arrayBufferToBase64 = require('./arrayBufferToBase64');
const imageType = require('../../constants/imageType');
/**
* deindent
*
* The generated HOCR is excessively indented, so
* we get rid of that indentation
*
* @name deindent
* @function deindent string
* @access public
*/
const deindent = (html) => {
const lines = html.split('\n');
if (lines[0].substring(0, 2) === ' ') {
for (let i = 0; i < lines.length; i += 1) {
if (lines[i].substring(0, 2) === ' ') {
lines[i] = lines[i].slice(2);
}
}
}
return lines.join('\n');
};
/**
* dump
*
* @name dump
* @function dump recognition result to a JSON object
* @access public
*/
module.exports = (TessModule, api, output, options) => {
const enumToString = (value, prefix) => (
Object.keys(TessModule)
.filter((e) => (e.startsWith(`${prefix}_`) && TessModule[e] === value))
.map((e) => e.slice(prefix.length + 1))[0]
);
const getImage = (type) => {
api.WriteImage(type, '/image.png');
const pngBuffer = TessModule.FS.readFile('/image.png');
const pngStr = `data:image/png;base64,${arrayBufferToBase64(pngBuffer.buffer)}`;
TessModule.FS.unlink('/image.png');
return pngStr;
};
const getPDFInternal = (title, textonly) => {
const pdfRenderer = new TessModule.TessPDFRenderer('tesseract-ocr', '/', textonly);
pdfRenderer.BeginDocument(title);
pdfRenderer.AddImage(api);
pdfRenderer.EndDocument();
TessModule._free(pdfRenderer);
return TessModule.FS.readFile('/tesseract-ocr.pdf');
};
return {
text: output.text ? api.GetUTF8Text() : null,
hocr: output.hocr ? deindent(api.GetHOCRText()) : null,
tsv: output.tsv ? api.GetTSVText() : null,
box: output.box ? api.GetBoxText() : null,
unlv: output.unlv ? api.GetUNLVText() : null,
osd: output.osd ? api.GetOsdText() : null,
pdf: output.pdf ? getPDFInternal(options.pdfTitle ?? 'Tesseract OCR Result', options.pdfTextOnly ?? false) : null,
imageColor: output.imageColor ? getImage(imageType.COLOR) : null,
imageGrey: output.imageGrey ? getImage(imageType.GREY) : null,
imageBinary: output.imageBinary ? getImage(imageType.BINARY) : null,
confidence: !options.skipRecognition ? api.MeanTextConf() : null,
blocks: output.blocks && !options.skipRecognition ? JSON.parse(api.GetJSONText()).blocks : null,
layoutBlocks: output.layoutBlocks && options.skipRecognition
? JSON.parse(api.GetJSONText()).blocks : null,
psm: enumToString(api.GetPageSegMode(), 'PSM'),
oem: enumToString(api.oem(), 'OEM'),
version: api.Version(),
debug: output.debug ? TessModule.FS.readFile('/debugInternal.txt', { encoding: 'utf8', flags: 'a+' }) : null,
};
};
@@ -0,0 +1,34 @@
'use strict';
const bmp = require('bmp-js');
/**
* setImage
*
* @name setImage
* @function set image in tesseract for recognition
* @access public
*/
module.exports = (TessModule, api, image, angle = 0) => {
// Check for bmp magic numbers (42 and 4D in hex)
const isBmp = (image[0] === 66 && image[1] === 77) || (image[1] === 66 && image[0] === 77);
const exif = parseInt(image.slice(0, 500).join(' ').match(/1 18 0 3 0 0 0 1 0 (\d)/)?.[1], 10) || 1;
// /*
// * Leptonica supports some but not all bmp files
// * @see https://github.com/DanBloomberg/leptonica/issues/607#issuecomment-1068802516
// * We therefore use bmp-js to convert all bmp files into a format Leptonica is known to support
// */
if (isBmp) {
// Not sure what this line actually does, but removing breaks the function
const buf = Buffer.from(Array.from({ ...image, length: Object.keys(image).length }));
const bmpBuf = bmp.decode(buf);
TessModule.FS.writeFile('/input', bmp.encode(bmpBuf).data);
} else {
TessModule.FS.writeFile('/input', image);
}
const res = api.SetImageFile(exif, angle);
if (res === 1) throw Error('Error attempting to read image.');
};
@@ -0,0 +1,12 @@
'use strict';
const version = require('../../../package.json').version;
const defaultOptions = require('../../constants/defaultOptions');
/*
* Default options for browser worker
*/
module.exports = {
...defaultOptions,
workerPath: `https://cdn.jsdelivr.net/npm/tesseract.js@v${version}/dist/worker.min.js`,
};
@@ -0,0 +1,26 @@
'use strict';
/**
*
* Tesseract Worker adapter for browser
*
* @fileoverview Tesseract Worker adapter for browser
* @author Kevin Kwok <antimatter15@gmail.com>
* @author Guillermo Webster <gui@mit.edu>
* @author Jerome Wu <jeromewus@gmail.com>
*/
const defaultOptions = require('./defaultOptions');
const spawnWorker = require('./spawnWorker');
const terminateWorker = require('./terminateWorker');
const onMessage = require('./onMessage');
const send = require('./send');
const loadImage = require('./loadImage');
module.exports = {
defaultOptions,
spawnWorker,
terminateWorker,
onMessage,
send,
loadImage,
};
@@ -0,0 +1,71 @@
'use strict';
/**
* readFromBlobOrFile
*
* @name readFromBlobOrFile
* @function
* @access private
*/
const readFromBlobOrFile = (blob) => (
new Promise((resolve, reject) => {
const fileReader = new FileReader();
fileReader.onload = () => {
resolve(fileReader.result);
};
fileReader.onerror = ({ target: { error: { code } } }) => {
reject(Error(`File could not be read! Code=${code}`));
};
fileReader.readAsArrayBuffer(blob);
})
);
/**
* loadImage
*
* @name loadImage
* @function load image from different source
* @access private
*/
const loadImage = async (image) => {
let data = image;
if (typeof image === 'undefined') {
return 'undefined';
}
if (typeof image === 'string') {
// Base64 Image
if (/data:image\/([a-zA-Z]*);base64,([^"]*)/.test(image)) {
data = atob(image.split(',')[1])
.split('')
.map((c) => c.charCodeAt(0));
} else {
const resp = await fetch(image);
data = await resp.arrayBuffer();
}
} else if (typeof HTMLElement !== 'undefined' && image instanceof HTMLElement) {
if (image.tagName === 'IMG') {
data = await loadImage(image.src);
}
if (image.tagName === 'VIDEO') {
data = await loadImage(image.poster);
}
if (image.tagName === 'CANVAS') {
await new Promise((resolve) => {
image.toBlob(async (blob) => {
data = await readFromBlobOrFile(blob);
resolve();
});
});
}
} else if (typeof OffscreenCanvas !== 'undefined' && image instanceof OffscreenCanvas) {
const blob = await image.convertToBlob();
data = await readFromBlobOrFile(blob);
} else if (image instanceof File || image instanceof Blob) {
data = await readFromBlobOrFile(image);
}
return new Uint8Array(data);
};
module.exports = loadImage;
@@ -0,0 +1,7 @@
'use strict';
module.exports = (worker, handler) => {
worker.onmessage = ({ data }) => { // eslint-disable-line
handler(data);
};
};
@@ -0,0 +1,12 @@
'use strict';
/**
* send
*
* @name send
* @function send packet to worker and create a job
* @access public
*/
module.exports = async (worker, packet) => {
worker.postMessage(packet);
};
@@ -0,0 +1,22 @@
'use strict';
/**
* spawnWorker
*
* @name spawnWorker
* @function create a new Worker in browser
* @access public
*/
module.exports = ({ workerPath, workerBlobURL }) => {
let worker;
if (Blob && URL && workerBlobURL) {
const blob = new Blob([`importScripts("${workerPath}");`], {
type: 'application/javascript',
});
worker = new Worker(URL.createObjectURL(blob));
} else {
worker = new Worker(workerPath);
}
return worker;
};
@@ -0,0 +1,12 @@
'use strict';
/**
* terminateWorker
*
* @name terminateWorker
* @function terminate worker
* @access public
*/
module.exports = (worker) => {
worker.terminate();
};
@@ -0,0 +1,12 @@
'use strict';
const path = require('path');
const defaultOptions = require('../../constants/defaultOptions');
/*
* Default options for node worker
*/
module.exports = {
...defaultOptions,
workerPath: path.join(__dirname, '..', '..', 'worker-script', 'node', 'index.js'),
};
@@ -0,0 +1,26 @@
'use strict';
/**
*
* Tesseract Worker impl. for node (using child_process)
*
* @fileoverview Tesseract Worker impl. for node
* @author Kevin Kwok <antimatter15@gmail.com>
* @author Guillermo Webster <gui@mit.edu>
* @author Jerome Wu <jeromewus@gmail.com>
*/
const defaultOptions = require('./defaultOptions');
const spawnWorker = require('./spawnWorker');
const terminateWorker = require('./terminateWorker');
const onMessage = require('./onMessage');
const send = require('./send');
const loadImage = require('./loadImage');
module.exports = {
defaultOptions,
spawnWorker,
terminateWorker,
onMessage,
send,
loadImage,
};
@@ -0,0 +1,38 @@
'use strict';
const util = require('util');
const fs = require('fs');
// Use built-in fetch if available, otherwise fallback to node-fetch
const fetch = global.fetch || require('node-fetch');
const isURL = require('is-url');
const readFile = util.promisify(fs.readFile);
/**
* loadImage
*
* @name loadImage
* @function load image from different source
* @access public
*/
module.exports = async (image) => {
let data = image;
if (typeof image === 'undefined') {
return image;
}
if (typeof image === 'string') {
if (isURL(image) || image.startsWith('moz-extension://') || image.startsWith('chrome-extension://') || image.startsWith('file://')) {
const resp = await fetch(image);
data = await resp.arrayBuffer();
} else if (/data:image\/([a-zA-Z]*);base64,([^"]*)/.test(image)) {
data = Buffer.from(image.split(',')[1], 'base64');
} else {
data = await readFile(image);
}
} else if (Buffer.isBuffer(image)) {
data = image;
}
return new Uint8Array(data);
};
@@ -0,0 +1,5 @@
'use strict';
module.exports = (worker, handler) => {
worker.on('message', handler);
};
@@ -0,0 +1,12 @@
'use strict';
/**
* send
*
* @name send
* @function send packet to worker and create a job
* @access public
*/
module.exports = async (worker, packet) => {
worker.postMessage(packet);
};
@@ -0,0 +1,12 @@
'use strict';
const { Worker } = require('worker_threads');
/**
* spawnWorker
*
* @name spawnWorker
* @function fork a new process in node
* @access public
*/
module.exports = ({ workerPath }) => new Worker(workerPath);
@@ -0,0 +1,12 @@
'use strict';
/**
* terminateWorker
*
* @name terminateWorker
* @function kill worker
* @access public
*/
module.exports = (worker) => {
worker.terminate();
};