@@ -5303,7 +5303,114 @@ int find_index_relaxed(vector<double> positions, double val, double margin) {
53035303 return index;
53045304}
53055305
5306- bool TextPage::markLineNumber () {
5306+ // Non-negative integer value of an all-digit word, or -1 if it holds a non-digit.
5307+ static long word_number_value (TextRawWord *word) {
5308+ long val = 0 ;
5309+ for (int i = 0 ; i < word->len ; i++) {
5310+ Unicode u = ((TextChar *) word->chars ->get (i))->c ;
5311+ if (u < ' 0' || u > ' 9' )
5312+ return -1 ;
5313+ val = val * 10 + (long ) (u - ' 0' );
5314+ }
5315+ return val;
5316+ }
5317+
5318+ // A margin cluster of numbers is line numbering even at low page coverage when its values
5319+ // form a regular arithmetic progression (dominant constant increment). Two accept paths:
5320+ // - "big numbers": the smallest value already exceeds this page's line count, so the numbers
5321+ // cannot be a per-page list (references / figure labels start near 1) - they are line
5322+ // numbers accumulated from earlier pages (e.g. 428..444 on a late page). Accept regardless
5323+ // of vertical spacing, which a figure or column break may disrupt.
5324+ // - "even pitch": otherwise (fresh, small-valued numbering such as every-5th-line on page 1)
5325+ // require roughly even vertical spacing.
5326+ // Footnote / bibliographic reference numbers match neither: they restart near 1 and are not
5327+ // evenly spaced down the page.
5328+ static bool is_regular_number_sequence (const vector<int > &cluster,
5329+ const vector<TextRawWord *> &lineNumberWords,
5330+ int totalNumberOfLines) {
5331+ int n = (int ) cluster.size ();
5332+ if (n < 3 )
5333+ return false ;
5334+
5335+ // collect (yMin, value), ordered top-to-bottom
5336+ vector<pair<double , long > > seq;
5337+ for (int i = 0 ; i < n; i++) {
5338+ TextRawWord *w = lineNumberWords[cluster[i]];
5339+ long v = word_number_value (w);
5340+ if (v < 0 )
5341+ return false ;
5342+ seq.push_back (make_pair (w->yMin , v));
5343+ }
5344+ sort (seq.begin (), seq.end ());
5345+
5346+ // values must strictly increase going down the page
5347+ vector<long > dv;
5348+ vector<double > dy;
5349+ for (int i = 1 ; i < (int ) seq.size (); i++) {
5350+ long d = seq[i].second - seq[i - 1 ].second ;
5351+ double gy = seq[i].first - seq[i - 1 ].first ;
5352+ if (d <= 0 || gy <= 0 )
5353+ return false ;
5354+ dv.push_back (d);
5355+ dy.push_back (gy);
5356+ }
5357+
5358+ // dominant (median) increment; line numbering steps are small (1, 2, 5, 10, 25...)
5359+ vector<long > sortedDv = dv;
5360+ sort (sortedDv.begin (), sortedDv.end ());
5361+ long g = sortedDv[sortedDv.size () / 2 ];
5362+ if (g < 1 || g > 50 )
5363+ return false ;
5364+
5365+ // a strong majority of steps must share that exact increment; a line number missed
5366+ // by extraction shows up as a multiple of g and is simply not counted here
5367+ int agree = 0 ;
5368+ for (int i = 0 ; i < (int ) dv.size (); i++)
5369+ if (dv[i] == g)
5370+ agree++;
5371+ if (agree < (int ) ((dv.size () * 3 + 4 ) / 5 )) // >= 60%
5372+ return false ;
5373+
5374+ // "big numbers" path: smallest value exceeds this page's line count -> accumulated line
5375+ // numbers, accept without requiring even pitch (figure/column breaks disrupt spacing).
5376+ long minVal = seq[0 ].second ;
5377+ if (minVal > (long ) totalNumberOfLines)
5378+ return true ;
5379+
5380+ // otherwise require roughly even vertical pitch per unit increment
5381+ vector<double > pitch;
5382+ for (int i = 0 ; i < (int ) dv.size (); i++)
5383+ if (dv[i] == g)
5384+ pitch.push_back (dy[i] / (double ) dv[i]);
5385+ if ((int ) pitch.size () < 2 )
5386+ return false ;
5387+ double mean = 0.0 ;
5388+ for (int i = 0 ; i < (int ) pitch.size (); i++)
5389+ mean += pitch[i];
5390+ mean /= pitch.size ();
5391+ if (mean <= 0.0 )
5392+ return false ;
5393+ double var = 0.0 ;
5394+ for (int i = 0 ; i < (int ) pitch.size (); i++)
5395+ var += (pitch[i] - mean) * (pitch[i] - mean);
5396+ double cv = sqrt (var / pitch.size ()) / mean;
5397+ return cv < 0.30 ;
5398+ }
5399+
5400+ // Minimum y (top) of the margin number column near x=clusterX, across the cluster's page.
5401+ // Line numbers jitter over a few x-positions, so we scan all candidates near that x.
5402+ static double column_top (const vector<TextRawWord *> &lineNumberWords, double clusterX) {
5403+ double top = 1e18 ;
5404+ for (int k = 0 ; k < (int ) lineNumberWords.size (); k++) {
5405+ TextRawWord *w = lineNumberWords[k];
5406+ if (fabs (w->xMin - clusterX) < 12.0 || fabs (w->xMax - clusterX) < 12.0 )
5407+ if (w->yMin < top)
5408+ top = w->yMin ;
5409+ }
5410+ return top;
5411+ }
5412+
5413+ bool TextPage::markLineNumber (vector<double > &lineNumberColumnsX) {
53075414 // Detect the presence of line number column in the page and mark the corresponding TextWord objects for further appropriate handling
53085415
53095416 // Line number conditions:
@@ -5333,6 +5440,11 @@ bool TextPage::markLineNumber() {
53335440 int rightMostBoundary = 0 ;
53345441 int leftMostBoundary = 999990 ;
53355442
5443+ // vertical extent of the page's text content (used to tell a full-height line-number column
5444+ // from a footnote/reference number block, which is confined to the bottom of the page)
5445+ double contentTop = 1e9 ;
5446+ double contentBottom = -1e9 ;
5447+
53365448 // the total number of lines on the page
53375449 int totalNumberOfLines = 0 ;
53385450
@@ -5375,6 +5487,10 @@ bool TextPage::markLineNumber() {
53755487 leftMostBoundary = line1->xMin ;
53765488 if (line1->xMax > rightMostBoundary)
53775489 rightMostBoundary = line1->xMax ;
5490+ if (line1->yMin < contentTop)
5491+ contentTop = line1->yMin ;
5492+ if (line1->yMax > contentBottom)
5493+ contentBottom = line1->yMax ;
53785494 }
53795495
53805496 if (par->xMin < leftMostBoundary)
@@ -5626,8 +5742,38 @@ bool TextPage::markLineNumber() {
56265742 else
56275743 requiredCoverage = 0.5 ;
56285744
5629- if (coverage < requiredCoverage)
5630- continue ;
5745+ // Low coverage can still be genuine sparse line numbering (every Nth line). Accept it
5746+ // only when the cluster is a regular arithmetic progression at even vertical pitch AND
5747+ // sits at the extreme page margin. Figure panels / legends with small ascending labels
5748+ // (e.g. rows numbered 1..6) are regular too but are inset from the margin, so they must
5749+ // not be stripped; footnote/reference numbers fail the regular-sequence test.
5750+ if (coverage < requiredCoverage) {
5751+ double span = (double ) (rightMostBoundary - leftMostBoundary);
5752+ double marginTol = span * 0.06 ;
5753+ bool atMargin = (final_vpos - leftMostBoundary <= marginTol) ||
5754+ ((double ) rightMostBoundary - final_vpos <= marginTol);
5755+ if (!(atMargin &&
5756+ is_regular_number_sequence (clusters[bestClusterIndex[j]], lineNumberWords, totalNumberOfLines)))
5757+ continue ;
5758+
5759+ // Document-level footnote guard. A line-number column reaches the top of the page's
5760+ // text on some page of the document (numbering starts at the top and runs down every
5761+ // page); a footnote / reference block never does - it sits at the bottom. Accept this
5762+ // low-coverage column only if it reaches the top HERE, or was already confirmed as a
5763+ // line-number column on an earlier page (2-column pages and figures push the numbers
5764+ // down, so a confirmed column may start mid-page on some pages).
5765+ double contentHeight = contentBottom - contentTop;
5766+ double colTop = column_top (lineNumberWords, final_vpos);
5767+ bool reachesTop = (contentHeight <= 0 ) ||
5768+ ((colTop - contentTop) <= 0.4 * contentHeight);
5769+ bool confirmed = false ;
5770+ for (int c = 0 ; c < (int ) lineNumberColumnsX.size (); c++)
5771+ if (fabs (lineNumberColumnsX[c] - final_vpos) < 15.0 ) { confirmed = true ; break ; }
5772+ if (!reachesTop && !confirmed)
5773+ continue ;
5774+ if (reachesTop && !confirmed)
5775+ lineNumberColumnsX.push_back (final_vpos);
5776+ }
56315777 validClusterIndices.push_back (bestClusterIndex[j]);
56325778 }
56335779
@@ -5706,7 +5852,7 @@ static void clampIllustrationBox(double &x, double &y, double &w, double &h) {
57065852 if (h < 0 ) { h = 0 ; }
57075853}
57085854
5709- void TextPage::dump (GBool noLineNumbers, GBool fullFontName, const vector<bool > &lineNumberStatus) {
5855+ void TextPage::dump (GBool noLineNumbers, GBool fullFontName, const vector<bool > &lineNumberStatus, vector< double > &lineNumberColumnsX ) {
57105856 // Output the page in raw (content stream) order
57115857 // Release the previous page's block tree before building this page's.
57125858 // The TextRawWords held by these lines are borrowed: TextPage::words owns
@@ -6313,7 +6459,7 @@ void TextPage::dump(GBool noLineNumbers, GBool fullFontName, const vector<bool>
63136459
63146460 bool hasLineNumber = false ;
63156461 if ( (currentPageNumber < nbTotalPage/2 ) || (previousLineNumber && nbTotalPage>4 )) {
6316- hasLineNumber = markLineNumber ();
6462+ hasLineNumber = markLineNumber (lineNumberColumnsX );
63176463 }
63186464 setLineNumber (hasLineNumber);
63196465
@@ -9407,7 +9553,7 @@ void XmlAltoOutputDev::endPage() {
94079553// if (readingOrder) {
94089554// text->dumpInReadingOrder(noLineNumbers, fullFontName);
94099555// } else
9410- text->dump (noLineNumbers, fullFontName, lineNumberStatus);
9556+ text->dump (noLineNumbers, fullFontName, lineNumberStatus, lineNumberColumnsX );
94119557 appendLineNumberStatus (text->getLineNumber ());
94129558 }
94139559
0 commit comments