@@ -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
97759781void XmlAltoOutputDev::recordNonUnicodeGlyph (int page,
0 commit comments