@scanmate/ocr 0.1.0 → 0.2.1

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/README.md CHANGED
@@ -23,14 +23,15 @@ const { pages } = await extractPair({ original: 'fw9-issued.pdf', scanned: 'fw9-
23
23
  const report = await ocrPages(await alignPages(pages))
24
24
 
25
25
  // The account number above, read for what it is:
26
- report.pages[0].differences // [{ kind: 'changed', expected: 'Account 4412-9087-3355',
26
+ report.pages[0].text.differences // [{ kind: 'changed', expected: 'Account 4412-9087-3355',
27
27
  // found: 'Account 4412-9987-3355', reason: 'numbers',
28
28
  // verified: true, x, y, width, height }]
29
- report.pages[0].printChecks // { checked, different }: figures matched against the original's glyphs
29
+ report.pages[0].text.printChecks // { checked, different }: figures matched against the original's glyphs
30
30
  report.score // document score, weighted by characters
31
- report.pages[0].metrics // levenshtein, jaccard, dice, cosine, CER, WER, word recall, ...
32
- report.pages[0].original.text // the original's text
33
- report.pages[0].scanned.text // the scan's
31
+ report.pages[0].text.metrics // levenshtein, jaccard, dice, cosine, CER, WER, word recall, ...
32
+ report.pages[0].text.original.text // the original's text
33
+ report.pages[0].text.scanned.text // the scan's
34
+ report.pages[0].aligned.raster // the page itself comes back too
34
35
  ```
35
36
 
36
37
  ## How it reads
package/dist/index.esm.js CHANGED
@@ -440,6 +440,33 @@ function union(texts) {
440
440
  };
441
441
  }
442
442
 
443
+ /**
444
+ * Where each printed character sits inside a run, found in the original's own
445
+ * rendering rather than guessed from font metrics.
446
+ *
447
+ * A generated PDF renders its glyphs cleanly: between two characters there is a
448
+ * column of paper - whatever the page puts behind the run, white or the colour
449
+ * of a total bar - and the only thing that varies is how wide. So the run's box
450
+ * is read as a column profile - how much ink stands in each column - and the
451
+ * groups of inked columns are the characters, in order. Nothing here needs to
452
+ * know the font, its advance widths, or its kerning, all of which a text layer
453
+ * leaves out and every face does differently.
454
+ *
455
+ * When the groups do not come to the number of characters expected - glyphs
456
+ * that touch, a comma that merges with the digit beside it - the run is left
457
+ * alone rather than guessed at: the caller can only verify what it can place.
458
+ * That is all-or-nothing over a whole run, which costs more the longer the run
459
+ * is: one pair of touching letters in a 52-character sentence loses the other
460
+ * fifty. `glyphWords` splits a run at its spaces first - the widest gaps in the
461
+ * same profile - so a sentence is verified word by word and only the word that
462
+ * will not segment is given up.
463
+ *
464
+ * **Direction.** A run's characters advance along the run, which is not always
465
+ * left to right: a form's margin instruction is often printed at a right angle
466
+ * to the page. The profile is taken along whichever axis the run's `angle`
467
+ * says, and quarter turns are the only ones handled - anything between would
468
+ * need the crop resampled, and is left unverifiable instead of guessed at.
469
+ */
443
470
  /**
444
471
  * The least a pixel may stand out from the paper around it and still count as
445
472
  * print, `0` to `1`. The run's own contrast decides above this.
@@ -1442,7 +1469,10 @@ async function ocrPages(pages, options = {}) {
1442
1469
  total: pages.length
1443
1470
  });
1444
1471
  const result = await readPage(page, engine, options);
1445
- results.push(result);
1472
+ results.push({
1473
+ ...page,
1474
+ text: result
1475
+ });
1446
1476
  options.onProgress?.({
1447
1477
  stage: 'ocr',
1448
1478
  phase: 'done',
@@ -1460,10 +1490,11 @@ async function ocrPages(pages, options = {}) {
1460
1490
  }
1461
1491
  });
1462
1492
  }
1463
- const characters = results.reduce((sum, r) => sum + r.metrics.characters, 0);
1493
+ const readings = results.map(page => page.text);
1494
+ const characters = readings.reduce((sum, reading) => sum + reading.metrics.characters, 0);
1464
1495
  return {
1465
- score: characters === 0 ? mean(results.map(r => r.score)) : results.reduce((sum, r) => sum + r.score * r.metrics.characters, 0) / characters,
1466
- pageMean: mean(results.map(r => r.score)),
1496
+ score: characters === 0 ? mean(readings.map(r => r.score)) : readings.reduce((sum, r) => sum + r.score * r.metrics.characters, 0) / characters,
1497
+ pageMean: mean(readings.map(r => r.score)),
1467
1498
  pages: results,
1468
1499
  engine: {
1469
1500
  name: engine.name,
@@ -23,7 +23,7 @@
23
23
  * typography, line-end hyphens, diacritics, case, and OCR noise.
24
24
  */
25
25
  export { ocrPages } from './page-reading/index.js';
26
- export type { OcrOptions, OcrReport, PageOcr, PlacedText, PositionedText, ReadablePage, RunReading, SideText, TextDifference } from './page-reading/index.js';
26
+ export type { OcrOptions, OcrReport, PageOcr, PlacedText, ReadPage, RunReading, SideText, TextDifference } from './page-reading/index.js';
27
27
  export { createTesseractEngine, DEFAULT_TESSERACT_OPTIONS } from './ocr-engine/index.js';
28
28
  export type { OcrEngine, OcrLine, OcrWord, RecognisedText, RecogniseHints, TesseractCacheOptions, TesseractEngine, TesseractEngineOptions, TesseractSettings } from './ocr-engine/index.js';
29
29
  export { compareTexts } from './text-similarity/index.js';
@@ -31,7 +31,7 @@ export type { ScoreMetric, TextMetrics } from './text-similarity/index.js';
31
31
  export { DEFAULT_NORMALISE, normaliseText, tokenise } from './text-normalisation/index.js';
32
32
  export type { NormaliseOptions } from './text-normalisation/index.js';
33
33
  export { collectTemplates, FIGURE_CHARACTERS, glyphCells, glyphWords, placeGlyphs, printPolarity, templateKey, TEXT_CHARACTERS, verifyPrintedRun } from './print-verification/index.js';
34
- export type { Box, CellOptions, PrintedRun, PrintPolarity, PrintVerification, Templates, VerifyOptions } from './print-verification/index.js';
34
+ export type { CellOptions, PrintPolarity, PrintVerification, Templates, VerifyOptions } from './print-verification/index.js';
35
35
  export { claimWords, DEFAULT_RECHECK_PASSES, judgeRun, judgeRuns, matchWords, readRun, recheckRun } from './page-reading/index.js';
36
36
  export type { Claims, MatchOptions, Recheck, RecheckOptions, RecheckPass, Reference, Verdict, WordMatch } from './page-reading/index.js';
37
37
  export { cosine, dice, jaccard, jaroWinkler, levenshtein, levenshteinSimilarity, wordDistance, wordRecall } from './text-similarity/index.js';
@@ -2,7 +2,7 @@
2
2
  export { claimWords, judgeRun, judgeRuns, matchWords } from './match-words.use-case.js';
3
3
  export type { Claims, MatchOptions, Reference, Verdict, WordMatch } from './match-words.use-case.js';
4
4
  export { ocrPages } from './ocr-pages.use-case.js';
5
- export type { OcrOptions, OcrReport, PageOcr, PlacedText, PositionedText, ReadablePage, RunReading, SideText, TextDifference } from './ocr-report.contract.js';
5
+ export type { OcrOptions, OcrReport, ReadPage, PageOcr, PlacedText, RunReading, SideText, TextDifference } from './ocr-report.contract.js';
6
6
  export { DEFAULT_RECHECK_PASSES, readRun, recheckRun } from './recheck-run.use-case.js';
7
7
  export type { Recheck, RecheckOptions, RecheckPass } from './recheck-run.use-case.js';
8
8
  //# sourceMappingURL=index.d.ts.map
@@ -1,4 +1,5 @@
1
- import type { OcrOptions, OcrReport, ReadablePage } from './ocr-report.contract.js';
1
+ import type { ReadablePage } from '@scanmate/ink';
2
+ import type { OcrOptions, OcrReport } from './ocr-report.contract.js';
2
3
  /**
3
4
  * Read every aligned page and say how closely the scan's text matches the
4
5
  * original's - per page, and for the document.
@@ -13,5 +14,5 @@ import type { OcrOptions, OcrReport, ReadablePage } from './ocr-report.contract.
13
14
  * One engine serves the whole call. Pass `engine` to share one across calls;
14
15
  * it is then left running.
15
16
  */
16
- export declare function ocrPages<Page extends ReadablePage>(pages: readonly Page[], options?: OcrOptions): Promise<OcrReport>;
17
+ export declare function ocrPages<Page extends ReadablePage>(pages: readonly Page[], options?: OcrOptions): Promise<OcrReport<Page>>;
17
18
  //# sourceMappingURL=ocr-pages.use-case.d.ts.map
@@ -1,21 +1,10 @@
1
- import type { AlignedPage, PageImage, ProgressCallback } from '@scanmate/ink';
1
+ import type { ProgressCallback, ReadablePage } from '@scanmate/ink';
2
2
  import type { OcrEngine, TesseractEngineOptions } from '../ocr-engine/index.js';
3
3
  import type { VerifyOptions } from '../print-verification/index.js';
4
4
  import type { RecheckOptions } from './recheck-run.use-case.js';
5
5
  import type { NormaliseOptions } from '../text-normalisation/index.js';
6
6
  import type { ScoreMetric, TextMetrics } from '../text-similarity/index.js';
7
7
  /** A run of text placed on the page, in PDF points from the top-left - what `@scanmate/extract` calls a text item. */
8
- export interface PositionedText {
9
- text: string;
10
- x: number;
11
- y: number;
12
- width: number;
13
- height: number;
14
- endsLine?: boolean;
15
- /** How the run is set, when the text layer says: a figure is matched against glyphs of the same face and size. */
16
- fontName?: string;
17
- fontSize?: number;
18
- }
19
8
  /**
20
9
  * What `ocrPages` reads: an aligned page, and optionally its enhanced image
21
10
  * (`@scanmate/enhance`) and the original's text layer (`@scanmate/extract`).
@@ -23,14 +12,6 @@ export interface PositionedText {
23
12
  * ignored, deliberately - hidden or stale text must not vouch for what the
24
13
  * paper shows.
25
14
  */
26
- export type ReadablePage = AlignedPage & {
27
- enhanced?: PageImage;
28
- metadata?: {
29
- original?: {
30
- textItems?: readonly PositionedText[] | null;
31
- };
32
- };
33
- };
34
15
  export interface OcrOptions {
35
16
  /** An engine to use and leave running. Default: a tesseract engine made for this call and terminated after it. */
36
17
  engine?: OcrEngine;
@@ -162,12 +143,23 @@ export interface PageOcr {
162
143
  };
163
144
  warnings: string[];
164
145
  }
165
- export interface OcrReport {
146
+ /** A page with what the scan reads on it, beside what the original says. */
147
+ export type ReadPage<Page extends ReadablePage = ReadablePage> = Page & {
148
+ text: PageOcr;
149
+ };
150
+ export interface OcrReport<Page extends ReadablePage = ReadablePage> {
166
151
  /** Page scores weighted by each page's expected characters - a three-word page does not outvote a dense one. */
167
152
  score: number;
168
153
  /** Unweighted mean of page scores; the two disagreeing says the short pages read differently. */
169
154
  pageMean: number;
170
- pages: PageOcr[];
155
+ /**
156
+ * The pages handed in, each carrying its reading.
157
+ *
158
+ * The pages come back rather than a bare list of results, so a later stage
159
+ * can take these straight and whatever the producer attached is still on
160
+ * them. `page.text` is this page's reading.
161
+ */
162
+ pages: Array<ReadPage<Page>>;
171
163
  engine: {
172
164
  name: string;
173
165
  version: string;
@@ -1,40 +1,4 @@
1
- import type { GrayImage } from '@scanmate/ink';
2
- /**
3
- * Where each printed character sits inside a run, found in the original's own
4
- * rendering rather than guessed from font metrics.
5
- *
6
- * A generated PDF renders its glyphs cleanly: between two characters there is a
7
- * column of paper - whatever the page puts behind the run, white or the colour
8
- * of a total bar - and the only thing that varies is how wide. So the run's box
9
- * is read as a column profile - how much ink stands in each column - and the
10
- * groups of inked columns are the characters, in order. Nothing here needs to
11
- * know the font, its advance widths, or its kerning, all of which a text layer
12
- * leaves out and every face does differently.
13
- *
14
- * When the groups do not come to the number of characters expected - glyphs
15
- * that touch, a comma that merges with the digit beside it - the run is left
16
- * alone rather than guessed at: the caller can only verify what it can place.
17
- * That is all-or-nothing over a whole run, which costs more the longer the run
18
- * is: one pair of touching letters in a 52-character sentence loses the other
19
- * fifty. `glyphWords` splits a run at its spaces first - the widest gaps in the
20
- * same profile - so a sentence is verified word by word and only the word that
21
- * will not segment is given up.
22
- *
23
- * **Direction.** A run's characters advance along the run, which is not always
24
- * left to right: a form's margin instruction is often printed at a right angle
25
- * to the page. The profile is taken along whichever axis the run's `angle`
26
- * says, and quarter turns are the only ones handled - anything between would
27
- * need the crop resampled, and is left unverifiable instead of guessed at.
28
- */
29
- /** A rectangle in PDF points from the page's top-left corner. */
30
- export interface Box {
31
- x: number;
32
- y: number;
33
- width: number;
34
- height: number;
35
- /** Degrees the run is turned by, if it is; only quarter turns are handled. */
36
- angle?: number;
37
- }
1
+ import type { GrayImage, ScanmateOrientedRect } from '@scanmate/ink';
38
2
  export interface CellOptions {
39
3
  /** The least a pixel may stand out from the run's own paper and count as print. Default `0.19`. */
40
4
  contrast?: number;
@@ -53,7 +17,7 @@ export interface CellOptions {
53
17
  * @param options - Contrast against the run's paper, column joining, and polarity.
54
18
  * @returns One box per character, or `null` when they cannot be told apart.
55
19
  */
56
- export declare function glyphCells(page: GrayImage, dpi: number, run: Box, count: number, options?: CellOptions): Box[] | null;
20
+ export declare function glyphCells(page: GrayImage, dpi: number, run: ScanmateOrientedRect, count: number, options?: CellOptions): ScanmateOrientedRect[] | null;
57
21
  /**
58
22
  * The box of each word of a run, in order, split at the run's own spaces.
59
23
  *
@@ -64,7 +28,7 @@ export declare function glyphCells(page: GrayImage, dpi: number, run: Box, count
64
28
  * @param options - Contrast against the run's paper, column joining, and polarity.
65
29
  * @returns One box per word, or `null` when the words cannot be told apart.
66
30
  */
67
- export declare function glyphWords(page: GrayImage, dpi: number, run: Box, counts: readonly number[], options?: CellOptions): Box[] | null;
31
+ export declare function glyphWords(page: GrayImage, dpi: number, run: ScanmateOrientedRect, counts: readonly number[], options?: CellOptions): ScanmateOrientedRect[] | null;
68
32
  /**
69
33
  * Where every character of a run sits, or `null` where it could not be placed.
70
34
  *
@@ -82,5 +46,5 @@ export declare function glyphWords(page: GrayImage, dpi: number, run: Box, count
82
46
  * @returns One entry per printed character, spaces excluded, or `null` when not
83
47
  * even the words could be told apart.
84
48
  */
85
- export declare function placeGlyphs(page: GrayImage, dpi: number, run: Box, text: string, options?: CellOptions): Array<Box | null> | null;
49
+ export declare function placeGlyphs(page: GrayImage, dpi: number, run: ScanmateOrientedRect, text: string, options?: CellOptions): Array<ScanmateOrientedRect | null> | null;
86
50
  //# sourceMappingURL=glyph-cells.use-case.d.ts.map
@@ -1,5 +1,4 @@
1
- import type { GrayImage } from '@scanmate/ink';
2
- import type { Box } from './glyph-cells.use-case.js';
1
+ import type { GrayImage, ScanmateOrientedRect, TextRun } from '@scanmate/ink';
3
2
  /**
4
3
  * What each character looks like in print, taken from the original itself.
5
4
  *
@@ -14,15 +13,10 @@ import type { Box } from './glyph-cells.use-case.js';
14
13
  * about it.
15
14
  */
16
15
  /** A run of the original's text layer, where it sits and how it is set. */
17
- export interface PrintedRun extends Box {
18
- text: string;
19
- fontName?: string;
20
- fontSize?: number;
21
- }
22
16
  /** Glyph images by face, size and character. */
23
17
  export type Templates = ReadonlyMap<string, GrayImage[]>;
24
18
  /** The key a run's glyphs are filed under. */
25
- export declare function templateKey(run: PrintedRun, character: string): string;
19
+ export declare function templateKey(run: TextRun, character: string): string;
26
20
  /**
27
21
  * Collects a glyph image for every character the page prints and can place.
28
22
  *
@@ -31,10 +25,10 @@ export declare function templateKey(run: PrintedRun, character: string): string;
31
25
  * @param runs - Its text layer.
32
26
  * @returns Glyph images, by {@link templateKey}.
33
27
  */
34
- export declare function collectTemplates(page: GrayImage, dpi: number, runs: readonly PrintedRun[]): Templates;
28
+ export declare function collectTemplates(page: GrayImage, dpi: number, runs: readonly TextRun[]): Templates;
35
29
  /**
36
30
  * The greyscale of one box of the page, or `null` when it lies outside it;
37
31
  * inverted when the run it belongs to is printed light on dark.
38
32
  */
39
- export declare function cut(page: GrayImage, dpi: number, box: Box, invert?: boolean): GrayImage | null;
33
+ export declare function cut(page: GrayImage, dpi: number, box: ScanmateOrientedRect, invert?: boolean): GrayImage | null;
40
34
  //# sourceMappingURL=glyph-templates.use-case.d.ts.map
@@ -1,7 +1,7 @@
1
1
  export { glyphCells, glyphWords, placeGlyphs } from './glyph-cells.use-case.js';
2
- export type { Box, CellOptions } from './glyph-cells.use-case.js';
2
+ export type { CellOptions } from './glyph-cells.use-case.js';
3
3
  export { collectTemplates, templateKey } from './glyph-templates.use-case.js';
4
- export type { PrintedRun, Templates } from './glyph-templates.use-case.js';
4
+ export type { Templates } from './glyph-templates.use-case.js';
5
5
  export { mergeVerifiedFigures } from './merge-reading.mapper.js';
6
6
  export { printPolarity } from './print-polarity.policy.js';
7
7
  export type { PrintPolarity } from './print-polarity.policy.js';
@@ -1,5 +1,5 @@
1
- import type { PrintedRun } from './glyph-templates.use-case.js';
2
1
  import type { PrintVerification } from './verify-print.use-case.js';
2
+ import type { TextRun } from '@scanmate/ink';
3
3
  /**
4
4
  * Puts what the ink settled into what the scan was read as.
5
5
  *
@@ -15,5 +15,5 @@ import type { PrintVerification } from './verify-print.use-case.js';
15
15
  * @param verification - What `verifyPrintedRun` decided about it.
16
16
  * @returns The reading with the decided figures settled.
17
17
  */
18
- export declare function mergeVerifiedFigures(read: string, run: PrintedRun, verification: PrintVerification): string;
18
+ export declare function mergeVerifiedFigures(read: string, run: TextRun, verification: PrintVerification): string;
19
19
  //# sourceMappingURL=merge-reading.mapper.d.ts.map
@@ -1,5 +1,4 @@
1
- import type { GrayImage } from '@scanmate/ink';
2
- import type { Box } from './glyph-cells.use-case.js';
1
+ import type { GrayImage, ScanmateOrientedRect } from '@scanmate/ink';
3
2
  /** Which way the original prints a run: dark text on light, or light text on a dark bar. */
4
3
  export type PrintPolarity = 'dark-on-light' | 'light-on-dark';
5
4
  /**
@@ -7,5 +6,5 @@ export type PrintPolarity = 'dark-on-light' | 'light-on-dark';
7
6
  * glyphs are the pixels far from the background, and the background is most of
8
7
  * the box.
9
8
  */
10
- export declare function printPolarity(page: GrayImage, dpi: number, run: Box): PrintPolarity;
9
+ export declare function printPolarity(page: GrayImage, dpi: number, run: ScanmateOrientedRect): PrintPolarity;
11
10
  //# sourceMappingURL=print-polarity.policy.d.ts.map
@@ -1,5 +1,5 @@
1
- import type { GrayImage } from '@scanmate/ink';
2
- import type { PrintedRun, Templates } from './glyph-templates.use-case.js';
1
+ import type { GrayImage, TextRun } from '@scanmate/ink';
2
+ import type { Templates } from './glyph-templates.use-case.js';
3
3
  /**
4
4
  * Whether a printed figure is still the figure that was printed, decided by
5
5
  * matching its ink rather than by reading it.
@@ -118,5 +118,5 @@ export interface PrintVerification {
118
118
  * @param options - Margin, figure length and the characters figures use.
119
119
  * @returns What the ink says, or `null` when the run could not be checked.
120
120
  */
121
- export declare function verifyPrintedRun(original: GrayImage, scan: GrayImage, dpi: number, run: PrintedRun, templates: Templates, options?: VerifyOptions): PrintVerification | null;
121
+ export declare function verifyPrintedRun(original: GrayImage, scan: GrayImage, dpi: number, run: TextRun, templates: Templates, options?: VerifyOptions): PrintVerification | null;
122
122
  //# sourceMappingURL=verify-print.use-case.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scanmate/ocr",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "description": "How closely a scan's text matches the original's: OCR matched run by run on the page, figures held exact, every difference located.",
5
5
  "license": "MIT",
6
6
  "author": "Eduardo Russo",
@@ -41,7 +41,7 @@
41
41
  "!**/*.js.map"
42
42
  ],
43
43
  "dependencies": {
44
- "@scanmate/ink": "^0.1.0",
44
+ "@scanmate/ink": "^0.2.1",
45
45
  "@tesseract.js-data/eng": "^1.0.0",
46
46
  "fastest-levenshtein": "^1.0.16",
47
47
  "tesseract.js": "^7.0.0"
@@ -50,7 +50,7 @@
50
50
  "access": "public"
51
51
  },
52
52
  "devDependencies": {
53
- "@scanmate/align": "^0.1.0",
54
- "@scanmate/extract": "^0.1.0"
53
+ "@scanmate/align": "^0.2.1",
54
+ "@scanmate/extract": "^0.2.1"
55
55
  }
56
56
  }