@scanmate/ocr 0.0.2 → 0.0.3

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/dist/index.esm.js CHANGED
@@ -611,12 +611,12 @@ const DEFAULT_RECHECK_PASSES = [{
611
611
  const FIGURE_CHARACTERS = '0123456789.,:/%()+-';
612
612
  /** White kept around a crop, in pixels: tesseract reads poorly off the edge of an image. */
613
613
  const MARGIN = 12;
614
- async function recheckRun(engine, image, run, options) {
614
+ async function recheckRun(engine, image, run, options, polarity) {
615
615
  const {
616
616
  passes = DEFAULT_RECHECK_PASSES,
617
617
  agree = 2
618
618
  } = options;
619
- const crop = cropRun(image.raster, image.dpi, run);
619
+ const crop = cropRun(image.raster, image.dpi, run, polarity);
620
620
  if (crop === null) return {
621
621
  cleared: false,
622
622
  reading: null,
@@ -632,7 +632,8 @@ async function recheckRun(engine, image, run, options) {
632
632
  tried++;
633
633
  const scale = Math.max(1, pass.dpi / image.dpi);
634
634
  const enlarged = scale === 1 ? crop : await resampleRaster(crop, Math.round(crop.width * scale), Math.round(crop.height * scale));
635
- const prepared = frame(pass.stretch === true ? stretch(enlarged) : enlarged);
635
+ // Inverted text keeps the bar's grey behind it; only a stretch makes that paper white.
636
+ const prepared = frame(polarity === 'light-on-dark' || pass.stretch === true ? stretch(enlarged) : enlarged);
636
637
  const read = await engine.recognise(prepared, {
637
638
  layout: pass.layout,
638
639
  characters
@@ -658,9 +659,10 @@ async function recheckRun(engine, image, run, options) {
658
659
  }
659
660
  /**
660
661
  * The run's box on the image, grown by a quarter of its height (at least 2 pt)
661
- * so the glyphs are whole; light text on a dark bar is turned dark on light.
662
+ * so the glyphs are whole; light text on a dark bar is turned dark on light -
663
+ * as the original prints it when that is known, else when the crop is dark.
662
664
  */
663
- function cropRun(raster, dpi, run) {
665
+ function cropRun(raster, dpi, run, polarity) {
664
666
  const s = dpi / 72;
665
667
  const pad = Math.max(2, run.height * 0.25);
666
668
  const left = Math.max(0, Math.floor((run.x - pad) * s));
@@ -672,9 +674,29 @@ function cropRun(raster, dpi, run) {
672
674
  for (let y = top; y < bottom; y++) out.data.set(raster.data.subarray((y * raster.width + left) * 4, (y * raster.width + right) * 4), (y - top) * out.width * 4);
673
675
  let sum = 0;
674
676
  for (let i = 0; i < out.data.length; i += 4) sum += luminance(out.data, i);
675
- if (sum / (out.width * out.height) < 110) for (let i = 0; i < out.data.length; i += 4) for (let c = 0; c < 3; c++) out.data[i + c] = 255 - out.data[i + c];
677
+ const invert = polarity === undefined ? sum / (out.width * out.height) < 110 : polarity === 'light-on-dark';
678
+ if (invert) for (let i = 0; i < out.data.length; i += 4) for (let c = 0; c < 3; c++) out.data[i + c] = 255 - out.data[i + c];
676
679
  return out;
677
680
  }
681
+ /**
682
+ * Which way the original prints a run, read off its own crisp rendering: the
683
+ * glyphs are the pixels far from the background, and the background is most of
684
+ * the box.
685
+ */
686
+ function printPolarity(raster, dpi, run) {
687
+ const s = dpi / 72;
688
+ const left = Math.max(0, Math.floor(run.x * s));
689
+ const top = Math.max(0, Math.floor(run.y * s));
690
+ const right = Math.min(raster.width, Math.ceil((run.x + run.width) * s));
691
+ const bottom = Math.min(raster.height, Math.ceil((run.y + run.height) * s));
692
+ const values = [];
693
+ for (let y = top; y < bottom; y++) for (let x = left; x < right; x++) values.push(luminance(raster.data, (y * raster.width + x) * 4));
694
+ if (values.length === 0) return 'dark-on-light';
695
+ const background = values.toSorted((a, b) => a - b)[Math.floor(values.length / 2)];
696
+ const glyphs = values.filter(v => Math.abs(v - background) > 48);
697
+ if (glyphs.length === 0) return 'dark-on-light';
698
+ return glyphs.reduce((a, b) => a + b, 0) / glyphs.length > background ? 'light-on-dark' : 'dark-on-light';
699
+ }
678
700
  /** Linear stretch so the darkest 2% become black and the lightest 2% white. */
679
701
  function stretch(raster) {
680
702
  const values = new Uint8Array(raster.width * raster.height);
@@ -837,7 +859,7 @@ async function readPage(page, engine, options) {
837
859
  const second = await recheckRun(engine, scanImage, run, {
838
860
  ...rules,
839
861
  ...recheck
840
- });
862
+ }, printPolarity(page.original.raster, originalDpi, run));
841
863
  if (second.reading === null) continue;
842
864
  claims.found[r] = second.reading;
843
865
  rechecks.cleared++;
@@ -943,5 +965,5 @@ function mean(values) {
943
965
  return values.length === 0 ? 0 : values.reduce((a, b) => a + b, 0) / values.length;
944
966
  }
945
967
 
946
- export { DEFAULT_NORMALISE, DEFAULT_RECHECK_PASSES, DEFAULT_TESSERACT_OPTIONS, claimWords, compareTexts, cosine, createTesseractEngine, diacriticsMap, dice, foldConfusables, foldDiacritics, jaccard, jaroWinkler, judgeRun, judgeRuns, levenshtein, levenshteinSimilarity, matchWords, normaliseText, ocrPages, recheckRun, tokenise, wordDistance, wordRecall };
968
+ export { DEFAULT_NORMALISE, DEFAULT_RECHECK_PASSES, DEFAULT_TESSERACT_OPTIONS, claimWords, compareTexts, cosine, createTesseractEngine, diacriticsMap, dice, foldConfusables, foldDiacritics, jaccard, jaroWinkler, judgeRun, judgeRuns, levenshtein, levenshteinSimilarity, matchWords, normaliseText, ocrPages, printPolarity, recheckRun, tokenise, wordDistance, wordRecall };
947
969
  //# sourceMappingURL=index.esm.js.map
@@ -30,8 +30,8 @@ export { compareTexts } from './text-similarity/index.js';
30
30
  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
- export { claimWords, DEFAULT_RECHECK_PASSES, judgeRun, judgeRuns, matchWords, recheckRun } from './page-reading/index.js';
34
- export type { Claims, MatchOptions, Recheck, RecheckOptions, RecheckPass, Reference, Verdict, WordMatch } from './page-reading/index.js';
33
+ export { claimWords, DEFAULT_RECHECK_PASSES, judgeRun, judgeRuns, matchWords, printPolarity, recheckRun } from './page-reading/index.js';
34
+ export type { Claims, MatchOptions, PrintPolarity, Recheck, RecheckOptions, RecheckPass, Reference, Verdict, WordMatch } from './page-reading/index.js';
35
35
  export { cosine, dice, jaccard, jaroWinkler, levenshtein, levenshteinSimilarity, wordDistance, wordRecall } from './text-similarity/index.js';
36
36
  export { diacriticsMap, foldConfusables, foldDiacritics } from './text-normalisation/index.js';
37
37
  //# sourceMappingURL=index.d.ts.map
@@ -3,6 +3,6 @@ export { claimWords, judgeRun, judgeRuns, matchWords } from './match-words.use-c
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
5
  export type { OcrOptions, OcrReport, PageOcr, PlacedText, PositionedText, ReadablePage, RunReading, SideText, TextDifference } from './ocr-report.contract.js';
6
- export { DEFAULT_RECHECK_PASSES, recheckRun } from './recheck-run.use-case.js';
7
- export type { Recheck, RecheckOptions, RecheckPass } from './recheck-run.use-case.js';
6
+ export { DEFAULT_RECHECK_PASSES, printPolarity, recheckRun } from './recheck-run.use-case.js';
7
+ export type { PrintPolarity, Recheck, RecheckOptions, RecheckPass } from './recheck-run.use-case.js';
8
8
  //# sourceMappingURL=index.d.ts.map
@@ -9,7 +9,10 @@ import type { MatchOptions, Reference } from './match-words.use-case.js';
9
9
  * a shaded bar - is at the mercy of the engine's layout analysis and of one
10
10
  * resolution. Cropped to itself, enlarged further, read as a single line (or
11
11
  * word, or as digits only when it is a figure), light-on-dark text inverted,
12
- * contrast stretched, it often reads cleanly. Measured on real scans at 120
12
+ * contrast stretched, it often reads cleanly. Whether the text is light on a
13
+ * dark bar is taken from the original, where it is certain, rather than
14
+ * guessed from the scan: a white total on a purple bar can scan as pale lilac
15
+ * on paler lilac, too light overall to look like a dark bar. Measured on real scans at 120
13
16
  * and 144 dpi, cropped multi-pass reading confirmed 96-97% of every printed
14
17
  * value on the page; at 93 dpi, a third.
15
18
  *
@@ -34,6 +37,8 @@ export interface RecheckOptions {
34
37
  agree?: number;
35
38
  }
36
39
  export declare const DEFAULT_RECHECK_PASSES: readonly RecheckPass[];
40
+ /** Which way the original prints a run: dark text on light, or light text on a dark bar. */
41
+ export type PrintPolarity = 'dark-on-light' | 'light-on-dark';
37
42
  export interface Recheck {
38
43
  cleared: boolean;
39
44
  /** An agreeing reading when cleared; `null` otherwise. */
@@ -45,5 +50,11 @@ export interface Recheck {
45
50
  export declare function recheckRun(engine: OcrEngine, image: {
46
51
  raster: Raster;
47
52
  dpi: number;
48
- }, run: Reference, options: MatchOptions & RecheckOptions): Promise<Recheck>;
53
+ }, run: Reference, options: MatchOptions & RecheckOptions, polarity?: PrintPolarity): Promise<Recheck>;
54
+ /**
55
+ * Which way the original prints a run, read off its own crisp rendering: the
56
+ * glyphs are the pixels far from the background, and the background is most of
57
+ * the box.
58
+ */
59
+ export declare function printPolarity(raster: Raster, dpi: number, run: Reference): PrintPolarity;
49
60
  //# sourceMappingURL=recheck-run.use-case.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scanmate/ocr",
3
- "version": "0.0.2",
3
+ "version": "0.0.3",
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",
@@ -51,6 +51,6 @@
51
51
  },
52
52
  "devDependencies": {
53
53
  "@scanmate/align": "^0.0.2",
54
- "@scanmate/extract": "^0.0.2"
54
+ "@scanmate/extract": "^0.0.3"
55
55
  }
56
56
  }