Skip to content

Commit 342c069

Browse files
committed
fix: OCR sidecar bbox now matches the glyph box ALTO emits
Addresses the Copilot review on PR #234. The sidecar bbox was recomputed in drawChar with a rotation-0 formula, raw getFontSize() and raw GfxFont ascent/descent, so it did not match TextRawWord::addChar (rotation-aware, transformed font size, clamped TextFontInfo metrics). For Type 3 / rotated / CTM-scaled text the boxes were wrong -- e.g. jstage glyphs came out ~1pt tall, useless for cropping. Record the sidecar glyph from the actual TextChar that addChar creates (TextPage::getLastAddedChar) so the box is identical to the one ALTO uses; skip glyphs addChar filtered out (out-of-bounds/tiny/space). jstage boxes go from ~1pt to correct ~10pt height; all 1095 corrected, none invalid. Also document (fontName,charCode) as the canonical correction key (robust past the 6400-placeholder saturation point), per the same review.
1 parent 9eeade0 commit 342c069

3 files changed

Lines changed: 51 additions & 30 deletions

File tree

Readme.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,10 +104,15 @@ jq -c '.glyphs[] | .occurrences[] as $o | {placeholder, charCode, page: $o.page,
104104
# ... invoke your OCR engine here, then write a corrections JSON ...
105105
done
106106
# apply corrections back: replace placeholder codepoints in out.xml
107-
# with the recovered Unicode, matched by (page, bbox) or (placeholder)
107+
# with the recovered Unicode, matched by (fontName, charCode) — the canonical
108+
# sidecar key — or by (page, bbox)
108109
```
109110

110-
The placeholder codepoints in the sidecar are identical to the ones in the
111+
Each distinct `(fontName, charCode)` gets its own placeholder, so the codepoint
112+
alone is a stable key for documents with up to 6400 distinct unmapped glyphs;
113+
beyond that the allocation saturates, so prefer `(fontName, charCode)` as the
114+
authoritative key when applying corrections. The placeholder codepoints in the
115+
sidecar are identical to the ones in the
111116
ALTO file, so a corrections step can simply substitute them in-place.
112117

113118
### Extra script to get only text content

src/XmlAltoOutputDev.cc

Lines changed: 34 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1863,6 +1863,7 @@ TextPage::TextPage(GBool verboseA, Catalog *catalog, xmlNodePtr node,
18631863
actualTextNBytes = 0;
18641864

18651865
curWord = NULL;
1866+
lastAddedChar = NULL;
18661867
charPos = 0;
18671868
curFont = NULL;
18681869
curFontSize = 0;
@@ -2124,6 +2125,7 @@ void TextPage::clear() {
21242125
actualTextNBytes = 0;
21252126

21262127
curWord = NULL;
2128+
lastAddedChar = NULL;
21272129
charPos = 0;
21282130
curFont = NULL;
21292131
curFontSize = 0;
@@ -2767,6 +2769,10 @@ void TextPage::addCharToRawWord(GfxState *state, double x, double y, double dx,
27672769
GBool overlap = gFalse;
27682770
int uBufLen, i;
27692771

2772+
// Reset per-call; set below only if a TextChar is actually created, so the
2773+
// OCR sidecar reads back the real glyph bbox (or nothing for filtered chars).
2774+
lastAddedChar = NULL;
2775+
27702776
if (uLen == 0) {
27712777
endWord();
27722778
return;
@@ -2960,6 +2966,12 @@ void TextPage::addCharToRawWord(GfxState *state, double x, double y, double dx,
29602966
(overlap || sp < -minDupBreakOverlap * curWord->fontSize), curFont, curFontSize,
29612967
splashFont, nBytes, curRot, isNonUnicodeGlyph);
29622968
}
2969+
// Expose the just-created TextChar (last in the word) so drawChar can
2970+
// record the OCR sidecar bbox from the same rotation-aware, transformed
2971+
// box ALTO uses, rather than recomputing it.
2972+
if (curWord && curWord->chars->getLength() > 0) {
2973+
lastAddedChar = (TextChar *) curWord->chars->get(curWord->chars->getLength() - 1);
2974+
}
29632975
}
29642976

29652977
if (curWord) {
@@ -9694,6 +9706,10 @@ void XmlAltoOutputDev::drawChar(GfxState *state, double x, double y, double dx,
96949706
GBool fill, GBool stroke, GBool makePath) {
96959707

96969708
GBool isNonUnicodeGlyph = gFalse;
9709+
// OCR-sidecar record deferred until after addChar (see below), so the bbox
9710+
// can be taken from the real TextChar instead of a recomputed one.
9711+
Unicode ocrPlaceholder = 0;
9712+
std::string ocrFontName;
96979713

96989714
SplashFont *splashFont = NULL;
96999715

@@ -9735,41 +9751,31 @@ void XmlAltoOutputDev::drawChar(GfxState *state, double x, double y, double dx,
97359751
uLen = 1;
97369752
isNonUnicodeGlyph = gTrue;
97379753

9738-
// Record the glyph metadata for the OCR sidecar. Bbox is
9739-
// computed in page space; y is kept in xpdf's bottom-up
9740-
// convention (matching state->transform) — downstream
9741-
// consumers that need ALTO's top-down frame should flip using
9742-
// the page height. Height uses the font's ascent/descent
9743-
// scaled by the current font size, mirroring the computation
9744-
// in TextRawWord::addChar for a consistent bbox.
9745-
double rx, ry, rw, rh;
9746-
state->transform(x, y, &rx, &ry);
9747-
state->transformDelta(dx, dy, &rw, &rh);
9748-
double fs = state->getFontSize();
9754+
// Capture the placeholder + font name; the sidecar bbox is recorded
9755+
// after addChar below, from the real TextChar, so it matches the
9756+
// rotation-aware, transformed box ALTO uses.
9757+
ocrPlaceholder = mapped_unicode;
97499758
GfxFont *gfxFont = state->getFont();
9750-
double asc = gfxFont && gfxFont->getAscent() != 0.0
9751-
? gfxFont->getAscent() * fs : 0.75 * fs;
9752-
double desc = gfxFont && gfxFont->getDescent() != 0.0
9753-
? gfxFont->getDescent() * fs : -0.25 * fs;
9754-
double x0 = rx, x1 = rx + rw;
9755-
double y0 = ry - asc, y1 = ry - desc;
9756-
double rxMin = x0 < x1 ? x0 : x1;
9757-
double rxMax = x0 < x1 ? x1 : x0;
9758-
double ryMin = y0 < y1 ? y0 : y1;
9759-
double ryMax = y0 < y1 ? y1 : y0;
9760-
const char *cleanFontName = NULL;
97619759
GString *rawFontName = gfxFont ? gfxFont->getName() : NULL;
9762-
if (rawFontName) {
9763-
cleanFontName = rawFontName->getCString();
9764-
}
9765-
recordNonUnicodeGlyph(text->getPageNumber(),
9766-
rxMin, ryMin, rxMax, ryMax,
9767-
mapped_unicode, c, cleanFontName);
9760+
ocrFontName = rawFontName ? rawFontName->getCString() : "";
97689761
}
97699762
} else if(uLen > 1 && (globalParams->getTextEncodingName()->cmp(ENCODING_UTF8)==0)&& !isUTF8(u, uLen))
97709763
return;
97719764

97729765
text->addChar(state, x, y, dx, dy, c, nBytes, u, uLen, splashFont, isNonUnicodeGlyph);
9766+
9767+
if (isNonUnicodeGlyph) {
9768+
// Record the OCR-sidecar glyph from the TextChar addChar just created,
9769+
// so the bbox is identical to the one ALTO emits (correct for rotated,
9770+
// CTM-scaled and Type 3 text). If the char was filtered out
9771+
// (out-of-bounds/tiny/space), there is nothing to OCR, so skip it.
9772+
TextChar *tc = text->getLastAddedChar();
9773+
if (tc) {
9774+
recordNonUnicodeGlyph(text->getPageNumber(),
9775+
tc->xMin, tc->yMin, tc->xMax, tc->yMax,
9776+
ocrPlaceholder, c, ocrFontName.c_str());
9777+
}
9778+
}
97739779
}
97749780

97759781
void XmlAltoOutputDev::recordNonUnicodeGlyph(int page,

src/XmlAltoOutputDev.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -936,6 +936,10 @@ class TextPage {
936936

937937
int getPageNumber() {return num;}
938938

939+
/** The TextChar created by the most recent addChar call, or NULL if that
940+
* call added none (filtered char, space, out-of-bounds glyph). */
941+
TextChar *getLastAddedChar() { return lastAddedChar; }
942+
939943
/** Update the current font
940944
* @param state The state description */
941945
void updateFont(GfxState *state);
@@ -1409,6 +1413,12 @@ class TextPage {
14091413
/** The currently active string */
14101414
TextRawWord *curWord;
14111415

1416+
/** The TextChar created by the most recent addCharToRawWord call, or NULL
1417+
* if that call added none (filtered/space/out-of-bounds char). Lets the
1418+
* OCR sidecar read back the exact glyph bbox ALTO uses, instead of
1419+
* recomputing a simplified one. */
1420+
TextChar *lastAddedChar;
1421+
14121422
/** The next character position (within content stream) */
14131423
int charPos;
14141424

0 commit comments

Comments
 (0)