@scanmate/ocr 0.4.0 → 0.5.0

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
@@ -26,7 +26,7 @@ const report = await ocrPages(await alignPages(pages))
26
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].text.printChecks // { checked, different }: figures matched against the original's glyphs
29
+ report.pages[0].text.printChecks // { checked, different, skipped }: figures matched against the original's glyphs
30
30
  report.score // document score, weighted by characters
31
31
  report.pages[0].text.metrics // levenshtein, jaccard, dice, cosine, CER, WER, word recall, ...
32
32
  report.pages[0].text.original.text // the original's text
@@ -41,6 +41,7 @@ report.pages[0].aligned.raster // the page itself comes back too
41
41
  - **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.
42
42
  - **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).
43
43
  - **Figures are matched, not only read.** Every printed figure is checked glyph by glyph against the original's own ink and against the other digits the page prints, at the scan's own sharpness. That settles what no reading of a coarse scan can: whether this is still the digit that was printed. On real returned scans it verified 35 of 40 printed figures at 125 dpi with no false calls, and read a digit replaced by another of the same run for what it is.
44
+ - **It says why it abstained.** `printChecks.skipped` counts each reason - `no-figure`, `unplaceable`, `few-rivals`, `too-coarse`, `undecided` - because "checked and agreed" and "never looked at" are otherwise the same silence, and telling them apart by inference is slow and can come out wrong.
44
45
  - **It abstains rather than confirm.** A page that prints too few digits to offer a full set of rivals, a run too small to segment, a cell too soft to call: each is left to the reading, and the answer is applied only to the cells it actually decided. Confirming a digit that had in fact been altered would be worse than saying nothing, so every threshold is set to fail that way.
45
46
  - **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.
46
47
 
package/dist/index.esm.js CHANGED
@@ -923,8 +923,38 @@ const MAX_PRINTED_SCORE = 0.85;
923
923
  const MATCH_HEIGHT = 16;
924
924
  /** The template is slid this far, in match pixels, to absorb a cell landing half a pixel out. */
925
925
  const SHIFT = 1;
926
- /** How much the print may be softened by to meet the scan, in match pixels. */
927
- const SOFTENING = [0, 0.5, 1, 1.5, 2];
926
+ /**
927
+ * The scan is sharpened to meet the print, rather than the print softened to
928
+ * meet the scan, and that direction is the whole of it.
929
+ *
930
+ * Softening the template was the obvious way round and it is wrong: the blur
931
+ * lands on the printed glyph and on all nine rivals alike, so above about one
932
+ * match pixel a `3` and an `8` become the same blob and every rival ties the
933
+ * truth. Measured on a returned order confirmation, the totals on its shaded
934
+ * bar fitted a softening of 1.5 and scored 0.937 against the digit actually
935
+ * printed and 0.928 against the best rival - a margin of 0.006 where 0.12 is
936
+ * needed, so not one of 109 cells could be decided. The same collapse hits
937
+ * ordinary black-on-white text on a 93 dpi scan, which is how we know it is
938
+ * the softening and not the shaded bar.
939
+ *
940
+ * Sharpening the scan puts back what the scanner took out and leaves the
941
+ * template's own detail intact, so what survives is what distinguishes one
942
+ * digit from another. On the same document that took light-on-dark cells from
943
+ * 0 decided to 12, and dark-on-light from 316 to 343, with no false call.
944
+ *
945
+ * `RECOVERY` is how far, in match pixels; `SHARPENING` is how hard.
946
+ */
947
+ const RECOVERY = [0, 0.5, 1, 1.5, 2];
948
+ /**
949
+ * A verdict has to hold at every one of these, or the cell is left undecided.
950
+ *
951
+ * Not a search for the amount that gives an answer - that is how a sweep turns
952
+ * into a way of manufacturing one. Sharpening can create a stroke that was
953
+ * never scanned, and a fabricated stroke favours whichever rival it happens to
954
+ * resemble, so a reading that moves as the amount moves is an artefact of the
955
+ * sharpening and is discarded.
956
+ */
957
+ const SHARPENING = [1, 1.5, 2];
928
958
  /**
929
959
  * Checks the figures of one run against the original's print.
930
960
  *
@@ -934,7 +964,7 @@ const SOFTENING = [0, 0.5, 1, 1.5, 2];
934
964
  * @param run - The run to check, from the original's text layer.
935
965
  * @param templates - Glyphs collected from the original by `collectTemplates`.
936
966
  * @param options - Margin, figure length and the characters figures use.
937
- * @returns What the ink says, or `null` when the run could not be checked.
967
+ * @returns What the ink says, or why it would not say.
938
968
  */
939
969
  function verifyPrintedRun(original, scan, dpi, run, templates, options = {}) {
940
970
  const {
@@ -949,69 +979,99 @@ function verifyPrintedRun(original, scan, dpi, run, templates, options = {}) {
949
979
  } = options;
950
980
  const printed = [...run.text].filter(character => character.trim() !== '');
951
981
  const wanted = scope === 'text' ? textCells(printed, characters) : figureCells(printed, minDigits);
952
- if (wanted.size === 0) return null;
982
+ if (wanted.size === 0) return {
983
+ verified: false,
984
+ because: 'no-figure'
985
+ };
953
986
  // A total on a shaded bar is printed light on dark; turned round, it is a figure like any other.
954
987
  const lightOnDark = printPolarity(original, dpi, run) === 'light-on-dark';
955
988
  const cells = placeGlyphs(original, dpi, run, run.text, {
956
989
  lightOnDark
957
990
  });
958
- if (cells === null) return null;
991
+ if (cells === null) return {
992
+ verified: false,
993
+ because: 'unplaceable'
994
+ };
959
995
  // Rivals: every character the page prints in this face and size that a figure could use.
960
996
  const rivals = [...characters].filter(character => templates.has(templateKey(run, character)));
961
- if (rivals.length < minRivals) return null;
962
- // How soft this scan's print is, measured where the answer is known: each cell
963
- // against the very glyph the original prints there.
964
- const pairs = [...wanted].flatMap(index => {
965
- const cell = cells[index];
966
- return cell === null ? [] : [{
967
- glyph: cut(scan, dpi, cell, lightOnDark),
968
- printed: cut(original, dpi, cell, lightOnDark)
969
- }];
970
- }).filter(pair => pair.glyph !== null && pair.printed !== null);
971
- const softening = softeningFor(pairs);
972
- const reading = [...printed];
973
- const scored = [];
974
- let confidence = 1;
975
- let checked = 0;
997
+ if (rivals.length < minRivals) return {
998
+ verified: false,
999
+ because: 'few-rivals'
1000
+ };
1001
+ // The cell exactly: a margin would bring in the neighbouring glyphs, which
1002
+ // both crops share, and shared ink correlates whatever the character is.
1003
+ const crops = new Map();
1004
+ let coarse = false;
976
1005
  for (const index of wanted) {
977
- // The cell exactly: a margin would bring in the neighbouring glyphs, which
978
- // both crops share, and shared ink correlates whatever the character is.
979
1006
  const cell = cells[index];
980
1007
  if (cell === null) continue;
981
1008
  const glyph = cut(scan, dpi, cell, lightOnDark);
982
1009
  // The original's own ink here: the same glyph, at the same size, in the same place.
983
1010
  const asPrinted = cut(original, dpi, cell, lightOnDark);
984
1011
  if (glyph === null || asPrinted === null) continue;
985
- const printedScore = correlate(glyph, asPrinted, softening);
986
- const others = rivals.filter(character => character !== printed[index]).map(character => ({
987
- character,
988
- score: bestMatch(glyph, templates.get(templateKey(run, character)) ?? [], softening)
989
- })).toSorted((a, b) => b.score - a.score);
990
- const rival = others[0];
991
- if (rival === undefined) continue;
992
- // Undecided unless one of the two wins clearly, and a change has to look like
993
- // the character it is being read as, not merely less like the printed one.
994
- const changed = rival.score >= printedScore + minMargin && rival.score >= minScore && printedScore <= maxPrinted;
995
- const unchanged = printedScore >= rival.score + minMargin && printedScore >= minPrinted;
1012
+ // Never invent resolution. Matching happens at MATCH_HEIGHT, so a shorter
1013
+ // cell is scaled up and the detail that decides between two digits is
1014
+ // interpolated rather than scanned - and an invented stroke favours
1015
+ // whichever rival it resembles. The one false call this check produced on
1016
+ // a 93 dpi scan came from cells of fourteen pixels.
1017
+ if (glyph.height < MATCH_HEIGHT) {
1018
+ coarse = true;
1019
+ continue;
1020
+ }
1021
+ crops.set(index, {
1022
+ glyph,
1023
+ printed: asPrinted
1024
+ });
1025
+ }
1026
+ if (crops.size === 0) return {
1027
+ verified: false,
1028
+ because: coarse ? 'too-coarse' : 'unplaceable'
1029
+ };
1030
+ // How far this scan's print has to be sharpened, measured where the answer is
1031
+ // known: each cell against the very glyph the original prints there.
1032
+ const pairs = [...crops.values()];
1033
+ const recoveries = SHARPENING.map(amount => recoveryFor(pairs, amount));
1034
+ const limits = {
1035
+ minMargin,
1036
+ minScore,
1037
+ maxPrinted,
1038
+ minPrinted
1039
+ };
1040
+ const reading = [...printed];
1041
+ const scored = [];
1042
+ let confidence = 1;
1043
+ let checked = 0;
1044
+ for (const [index, crop] of crops) {
1045
+ const passes = recoveries.map(recovery => judgeCell(crop, printed[index], rivals, run, templates, recovery, limits));
1046
+ const first = passes[0];
1047
+ if (first === undefined) continue;
1048
+ // Undecided unless every pass reads it the same way: a verdict that changes
1049
+ // as the sharpening changes is the sharpening talking, not the ink.
1050
+ const settled = first.read !== null && passes.every(pass => pass.read === first.read);
996
1051
  scored.push({
997
1052
  at: index,
998
1053
  printed: printed[index],
999
- printedScore,
1000
- rival: rival.character,
1001
- rivalScore: rival.score,
1002
- read: changed ? rival.character : unchanged ? printed[index] : null
1054
+ printedScore: first.printedScore,
1055
+ rival: first.rival,
1056
+ rivalScore: first.rivalScore,
1057
+ read: settled ? first.read : null
1003
1058
  });
1004
- if (!changed && !unchanged) continue;
1059
+ if (!settled || first.read === null) continue;
1005
1060
  checked++;
1006
- if (changed) reading[index] = rival.character;
1007
- confidence = Math.min(confidence, Math.abs(printedScore - rival.score));
1061
+ reading[index] = first.read;
1062
+ // The narrowest margin any pass decided by, so confidence is the weakest link.
1063
+ confidence = Math.min(confidence, ...passes.map(pass => Math.abs(pass.printedScore - pass.rivalScore)));
1008
1064
  }
1009
1065
  // Nothing decided is not an answer: say nothing rather than guess a digit.
1010
- if (checked === 0) return null;
1066
+ if (checked === 0) return {
1067
+ verified: false,
1068
+ because: 'undecided'
1069
+ };
1011
1070
  // Put the spaces back, so the reading reads like the run it is about.
1012
1071
  let cell = 0;
1013
1072
  const text = [...run.text].map(character => character.trim() === '' ? character : reading[cell++]).join('');
1014
1073
  return {
1074
+ verified: true,
1015
1075
  reading: text,
1016
1076
  agrees: text === run.text,
1017
1077
  checked,
@@ -1019,6 +1079,31 @@ function verifyPrintedRun(original, scan, dpi, run, templates, options = {}) {
1019
1079
  cells: scored
1020
1080
  };
1021
1081
  }
1082
+ /** What one pass of the sharpening sweep makes of a single cell. */
1083
+ function judgeCell(crop, printed, rivals, run, templates, recovery, limits) {
1084
+ const printedScore = correlate(crop.glyph, crop.printed, recovery);
1085
+ const others = rivals.filter(character => character !== printed).map(character => ({
1086
+ character,
1087
+ score: bestMatch(crop.glyph, templates.get(templateKey(run, character)) ?? [], recovery)
1088
+ })).toSorted((a, b) => b.score - a.score);
1089
+ const rival = others[0];
1090
+ if (rival === undefined) return {
1091
+ read: null,
1092
+ printedScore,
1093
+ rival: '',
1094
+ rivalScore: -1
1095
+ };
1096
+ // A change has to look like the character it is being read as, not merely
1097
+ // less like the printed one.
1098
+ const changed = rival.score >= printedScore + limits.minMargin && rival.score >= limits.minScore && printedScore <= limits.maxPrinted;
1099
+ const unchanged = printedScore >= rival.score + limits.minMargin && printedScore >= limits.minPrinted;
1100
+ return {
1101
+ read: changed ? rival.character : unchanged ? printed : null,
1102
+ printedScore,
1103
+ rival: rival.character,
1104
+ rivalScore: rival.score
1105
+ };
1106
+ }
1022
1107
  /**
1023
1108
  * The digits of every figure of at least `minDigits` digits.
1024
1109
  *
@@ -1047,38 +1132,62 @@ function figureCells(printed, minDigits) {
1047
1132
  return wanted;
1048
1133
  }
1049
1134
  /** How soft the print has to be drawn to sit best on the scan's own glyphs. */
1050
- function softeningFor(pairs) {
1135
+ function recoveryFor(pairs, amount) {
1051
1136
  let best = {
1052
- softening: 0,
1137
+ recovery: {
1138
+ sigma: 0,
1139
+ amount
1140
+ },
1053
1141
  score: -Infinity
1054
1142
  };
1055
- for (const softening of SOFTENING) {
1056
- const score = pairs.reduce((sum, pair) => sum + correlate(pair.glyph, pair.printed, softening), 0);
1143
+ for (const sigma of RECOVERY) {
1144
+ const recovery = {
1145
+ sigma,
1146
+ amount
1147
+ };
1148
+ const score = pairs.reduce((sum, pair) => sum + correlate(pair.glyph, pair.printed, recovery), 0);
1057
1149
  if (score > best.score) best = {
1058
- softening,
1150
+ recovery,
1059
1151
  score
1060
1152
  };
1061
1153
  }
1062
- return best.softening;
1154
+ return best.recovery;
1063
1155
  }
1064
1156
  /** The best correlation between one glyph and a character's templates. */
1065
- function bestMatch(glyph, templates, softening) {
1157
+ function bestMatch(glyph, templates, recovery) {
1066
1158
  let best = -1;
1067
- for (const template of templates) best = Math.max(best, correlate(glyph, template, softening));
1159
+ for (const template of templates) best = Math.max(best, correlate(glyph, template, recovery));
1068
1160
  return best;
1069
1161
  }
1162
+ /** An unsharp mask: what the scanner blurred away, added back. */
1163
+ function sharpen(image, {
1164
+ sigma,
1165
+ amount
1166
+ }) {
1167
+ if (sigma <= 0 || amount <= 0) return image;
1168
+ const blurred = soften(image, sigma);
1169
+ const data = new Float32Array(image.data.length);
1170
+ for (let i = 0; i < data.length; i++) data[i] = image.data[i] + amount * (image.data[i] - blurred.data[i]);
1171
+ return {
1172
+ width: image.width,
1173
+ height: image.height,
1174
+ data
1175
+ };
1176
+ }
1070
1177
  /**
1071
1178
  * How alike two glyphs are, each scaled to the same small box, and the template
1072
1179
  * slid a pixel each way to allow for a cell that landed a fraction out.
1073
1180
  *
1074
1181
  * Correlation, rather than a difference of pixels, because a scan is darker or
1075
- * lighter than the print it came from and that must not decide anything.
1182
+ * lighter than the print it came from and that must not decide anything. The
1183
+ * scan's glyph is sharpened; the template is left as it was printed. See
1184
+ * {@link RECOVERY} for why that direction and not the other.
1076
1185
  */
1077
- function correlate(glyph, template, softening = 0) {
1186
+ function correlate(glyph, template, recovery) {
1078
1187
  const height = MATCH_HEIGHT;
1079
1188
  const width = Math.max(2, Math.round(height * ((glyph.width / glyph.height + template.width / template.height) / 2)));
1080
- const a = resizeGray(glyph, width, height);
1081
- const b = soften(resizeGray(template, width, height), softening);
1189
+ const a = sharpen(resizeGray(glyph, width, height), recovery);
1190
+ const b = resizeGray(template, width, height);
1082
1191
  let best = -1;
1083
1192
  for (let dy = -SHIFT; dy <= SHIFT; dy++) for (let dx = -SHIFT; dx <= SHIFT; dx++) best = Math.max(best, pearson(a, b, dx, dy));
1084
1193
  return best;
@@ -1625,7 +1734,14 @@ async function readPage(page, engine, options, templates) {
1625
1734
  // reading of a returned scan can: whether this is still the digit that was printed.
1626
1735
  const printChecks = {
1627
1736
  checked: 0,
1628
- different: 0
1737
+ different: 0,
1738
+ skipped: {
1739
+ 'no-figure': 0,
1740
+ 'unplaceable': 0,
1741
+ 'few-rivals': 0,
1742
+ 'too-coarse': 0,
1743
+ 'undecided': 0
1744
+ }
1629
1745
  };
1630
1746
  const seenChanged = new Set();
1631
1747
  if (printCheck !== false && useLayer) {
@@ -1635,7 +1751,10 @@ async function readPage(page, engine, options, templates) {
1635
1751
  }));
1636
1752
  for (const [r, run] of printed.entries()) {
1637
1753
  const verified = verifyPrintedRun(originalGray, scanGray, originalDpi, run, templates, printCheck);
1638
- if (verified === null) continue;
1754
+ if (!verified.verified) {
1755
+ printChecks.skipped[verified.because]++;
1756
+ continue;
1757
+ }
1639
1758
  printChecks.checked++;
1640
1759
  if (!verified.agrees) {
1641
1760
  printChecks.different++;
@@ -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 { collectInto, collectTemplates, FIGURE_CHARACTERS, glyphCells, glyphWords, placeGlyphs, printPolarity, templateKey, TEXT_CHARACTERS, verifyPrintedRun } from './print-verification/index.js';
34
- export type { CellOptions, PrintPolarity, PrintVerification, TemplateStore, Templates, VerifyOptions } from './print-verification/index.js';
34
+ export type { CellOptions, PrintAbstention, PrintCheck, PrintPolarity, PrintVerification, TemplateStore, 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';
@@ -1,6 +1,6 @@
1
1
  import type { ProgressCallback, ReadablePage } from '@scanmate/ink';
2
2
  import type { OcrEngine, TesseractEngineOptions } from '../ocr-engine/index.js';
3
- import type { VerifyOptions } from '../print-verification/index.js';
3
+ import type { PrintAbstention, 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';
@@ -136,10 +136,16 @@ export interface PageOcr {
136
136
  attempted: number;
137
137
  cleared: number;
138
138
  };
139
- /** Printed figures matched against the original's own glyphs, and how many read as something else. */
139
+ /**
140
+ * Printed figures matched against the original's own glyphs: how many were
141
+ * decided, how many read as something else, and why the rest were not looked
142
+ * at. Without `skipped`, "checked and agreed" and "never checked" are the
143
+ * same silence from outside.
144
+ */
140
145
  printChecks: {
141
146
  checked: number;
142
147
  different: number;
148
+ skipped: Record<PrintAbstention, number>;
143
149
  };
144
150
  warnings: string[];
145
151
  }
@@ -6,5 +6,6 @@ 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';
8
8
  export { FIGURE_CHARACTERS, TEXT_CHARACTERS, verifyPrintedRun } from './verify-print.use-case.js';
9
+ export type { PrintAbstention, PrintCheck } from './verify-print.use-case.js';
9
10
  export type { PrintVerification, VerifyOptions } from './verify-print.use-case.js';
10
11
  //# sourceMappingURL=index.d.ts.map
@@ -95,6 +95,29 @@ export interface CellVerification {
95
95
  /** What this cell was taken to be, or `null` when it was too close to call. */
96
96
  read: string | null;
97
97
  }
98
+ /**
99
+ * Why a run was not checked.
100
+ *
101
+ * Reported rather than folded into a bare `null`, because "checked and agreed"
102
+ * and "never looked at" are the same silence from outside, and telling them
103
+ * apart by inference costs a consumer days and can still come out wrong.
104
+ *
105
+ * - `no-figure` - the run holds no group of digits long enough to be a figure.
106
+ * - `unplaceable` - the glyphs could not be cut apart.
107
+ * - `few-rivals` - the document prints too little of this face to rule other
108
+ * characters out.
109
+ * - `too-coarse` - the scan resolves the cell below the height it is matched
110
+ * at, so a verdict would rest on detail it never captured.
111
+ * - `undecided` - measured, and nothing won by enough to say.
112
+ */
113
+ export type PrintAbstention = 'no-figure' | 'unplaceable' | 'few-rivals' | 'too-coarse' | 'undecided';
114
+ /** What the ink said, or why it would not say. */
115
+ export type PrintCheck = ({
116
+ verified: true;
117
+ } & PrintVerification) | {
118
+ verified: false;
119
+ because: PrintAbstention;
120
+ };
98
121
  export interface PrintVerification {
99
122
  /** The run as its ink reads, printed characters kept where nothing was checked. */
100
123
  reading: string;
@@ -116,7 +139,7 @@ export interface PrintVerification {
116
139
  * @param run - The run to check, from the original's text layer.
117
140
  * @param templates - Glyphs collected from the original by `collectTemplates`.
118
141
  * @param options - Margin, figure length and the characters figures use.
119
- * @returns What the ink says, or `null` when the run could not be checked.
142
+ * @returns What the ink says, or why it would not say.
120
143
  */
121
- export declare function verifyPrintedRun(original: GrayImage, scan: GrayImage, dpi: number, run: TextRun, templates: Templates, options?: VerifyOptions): PrintVerification | null;
144
+ export declare function verifyPrintedRun(original: GrayImage, scan: GrayImage, dpi: number, run: TextRun, templates: Templates, options?: VerifyOptions): PrintCheck;
122
145
  //# 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.4.0",
3
+ "version": "0.5.0",
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.4.0",
44
+ "@scanmate/ink": "^0.5.0",
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.4.0",
54
- "@scanmate/extract": "^0.4.0"
53
+ "@scanmate/align": "^0.5.0",
54
+ "@scanmate/extract": "^0.5.0"
55
55
  }
56
56
  }