@scanmate/ocr 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Eduardo Russo
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,77 @@
1
+ ![scanmate ocr](./scanmate-ocr.svg)
2
+
3
+ # `@scanmate/ocr`
4
+
5
+ Measures how closely a scan's text matches the original's: a score per page and for the document, ten measures, both texts in full, and every difference with its position on the page.
6
+
7
+ ```ts
8
+ import { alignPages } from '@scanmate/align'
9
+ import { extractPair } from '@scanmate/extract'
10
+ import { ocrPages } from '@scanmate/ocr'
11
+
12
+ const { pages } = await extractPair({ original: 'contract.pdf', scanned: 'returned.pdf' })
13
+ const report = await ocrPages(await alignPages(pages))
14
+
15
+ report.score // document score, weighted by characters
16
+ report.pages[0].metrics // levenshtein, jaccard, dice, cosine, CER, WER, word recall, ...
17
+ report.pages[0].differences // { kind: 'changed' | 'missing' | 'added', expected, found, x, y, ... }
18
+ report.pages[0].original.text // the original's text
19
+ report.pages[0].scanned.text // the scan's
20
+ ```
21
+
22
+ ## How it reads
23
+
24
+ - **The original's side** comes from its PDF text layer whenever it has one. The layer is exact, so every error counted belongs to the scan. The scan's own text layer is never used, because hidden or stale text must not vouch for what the paper shows.
25
+ - **The scan** is read with tesseract, at 300 dpi (it is enlarged first if lower), or from its `enhanced` image when `@scanmate/enhance` ran first.
26
+ - **Matching by place, not order.** Alignment puts the scan on the original's canvas, so each word read is claimed by the run of the original printed where it was read. A two-column page read column by column is therefore not a page of errors, and every difference has a position.
27
+ - **Figures must keep their digits.** A run whose digits read back differently has changed, however similar the rest is: "Total 1,250.00" read as "Total 7,250.00" is 93% similar. A figure whose separators alone differ ("5.768.700 00") is the same figure. Words keep OCR's tolerance (`matchThreshold`, 0.8).
28
+ - **What is not an addition.** Words over something the original prints without text, such as a logo, are not additions. Nor are specks under 4 pt tall.
29
+
30
+ ## The recheck
31
+
32
+ Before a run counts as a difference, it is cropped and re-read on its own, in up to six passes:
33
+
34
+ - enlarged to 300–600 dpi;
35
+ - read as a line or as a single word;
36
+ - read as digits only, when the run is a figure;
37
+ - with contrast stretched, and with light-on-dark text inverted.
38
+
39
+ A run is cleared only when **two** passes agree with the original, judged by the same rules as the page. A single agreeing pass let forged digits through on a 93-dpi scan. Set `recheck: false` to report the page reading as it is.
40
+
41
+ Measured on three real scans of a 7-page order form, with the text layer as ground truth:
42
+
43
+ | Scan | Score | False differences | One-digit forgeries flagged |
44
+ |---|---|---|---|
45
+ | 144 dpi | 0.997 | 0 | 4/4 |
46
+ | 120 dpi | 0.934 | 20 (217 without the recheck) | 4/4 |
47
+ | 93 dpi | 0.660 | many: below what OCR can verify | 4/4, among many misreadings |
48
+
49
+ ## Text normalisation
50
+
51
+ Text is normalised the same way on both sides before comparing: NFKC, typography (quotes, dashes, spaces), line-end hyphens, diacritics (the project's 86-base table), case, and OCR noise such as table rules and specks. Punctuation and OCR confusables (`0/o`, `1/l`, `rn/m`) are **kept** by default. Folding them hides exactly the substitutions a forger makes. Every step is an option (`normalise`).
52
+
53
+ ## The engine, and writing nothing to disk
54
+
55
+ tesseract.js 7 (WebAssembly) with the English best_int model, which read as well as the standard model on real scans at a quarter of the size. Other languages: `npm install @tesseract.js-data/<code>` and pass `tesseract: { languages: ['eng', 'por'] }`.
56
+
57
+ Nothing is downloaded and, by default, nothing is written. This matters where deployments are read-only, as on Azure Functions: tesseract.js would otherwise cache language data in the current directory. The only writes are explicit:
58
+
59
+ | Option | Default | |
60
+ |---|---|---|
61
+ | `tesseract.cache.method` | `'none'` | `'write'` keeps unpacked data for reuse; `'readOnly'` uses data a previous run left; `'refresh'` rewrites it. |
62
+ | `tesseract.cache.path` | `<os temp>/scanmate-ocr` | Where anything written goes, including language files staged when more than one language is read. |
63
+ | `tesseract.model` | `'best'` | `'standard'` for the larger 4.0.0 model. |
64
+ | `tesseract.languageData` | the data packages | A folder of `<code>.traineddata.gz` files instead. |
65
+
66
+ `createTesseractEngine()` returns an engine whose `settings` report exactly what was resolved. Pass it as `engine` to share one across calls and pay the start-up once. Any other OCR engine can be used by implementing `OcrEngine`.
67
+
68
+ ## Options
69
+
70
+ | Option | Default | |
71
+ |---|---|---|
72
+ | `original` | `'auto'` | Text layer when the original has one; `'text-layer'` insists on it; `'ocr'` always reads the original. |
73
+ | `targetDpi` | `300` | Enlarge below this before reading; `null` reads the image as it is. |
74
+ | `scoreMetric` | `'levenshteinSimilarity'` | Or `'wordRecall'`, `'jaccard'`, `'dice'`, `'cosine'`. |
75
+ | `matchThreshold` | `0.8` | Word similarity below which a run has changed. |
76
+ | `minWordConfidence` | `60` | For words to count as added. |
77
+ | `recheck` | on | `{ passes, agree }`, or `false`. |
@@ -0,0 +1 @@
1
+ export * from "./src/index.js";