docling-project / docling-project/docling-parse
drop cap is never merged with the word it begins, producing "R ealized"
- Dominant language
- C++
- Stars
- 333
- Forks
- 80
- Avg merge
- 2d 4h
- Merged PRs (30d)
- 12
Description
moved here from docling-project/docling#4120 at @cau-git's request. originally reported
as docling#3883. separate mechanism from the word spacing issue filed alongside this
one, and the two fixes do not touch each other.
## what happens
a drop cap, the oversized initial letter opening a paragraph, is its own text run at its
own size and on a lower baseline. it never merges into the word it begins, so it survives
as a separate cell and line assembly joins it with a space:
```
'R ealized through a combination of careful engineering'
```
expected:
```
'Realized through a combination of careful engineering'
```
## root cause
the pair never reaches the spacing decision, it fails adjacency first. measured corners:
```
R (x0,y0)=(72.00,618.96) (x1,y1)=(96.55,618.96) (x2,y2)=(96.55,650.41) (x3,y3)=(72.00,650.41)
ealized (x0,y0)=(96.00,637.52) (x1,y1)=(134.02,637.52) (x2,y2)=(134.02,648.62) (x3,y3)=(96.00,648.62)
```
`page_item::is_adjacent_to` in `src/parse/page_items/page_cell.h`:
```cpp
double d0 = std::sqrt((r_x1-other.r_x0)*(r_x1-other.r_x0) + (r_y1-other.r_y0)*(r_y1-other.r_y0));
double d1 = std::sqrt((r_x2-other.r_x3)*(r_x2-other.r_x3) + (r_y2-other.r_y3)*(r_y2-other.r_y3));
return ((d0 fails
d1 = 1.878 eps_d1 = 8.101 -> passes
```
the horizontal gap between the boxes is **-0.55pt**, they overlap. the baseline is
**18.56pt** lower. because d0 is a euclidean distance between the bottom corners it folds
the vertical offset into what otherwise reads as a horizontal gap test, and the vertical
term alone clears the threshold. the cell is rejected for sitting low, not for being far.
worth noting the existing ligature accommodation in `page_item_sanitators/cells.h` does
not cover this. it relaxes only `eps_d1`, for tall boxes inflating the top corner
distance, and d1 already passes here. the drop cap fails on the opposite corner.
## what i am going to do
follow the shape the ligature exception already established: keep `is_adjacent_to`
untouched, and condition the tolerance at the call site in
`contract_cells_into_lines_left_to_right` for the pair that matches a drop cap.
widening `eps_d0` unconditionally is the wrong lever, and the comment above the ligature
handling says why, that slack on that side risks "incorrectly bridging word-boundary
gaps". the gate will be narrow: line initial, single character, substantially taller than
the following cell, and horizontally touching or overlapping it.
there is a second half to this. once the pair merges, `merge_with` computes the same
euclidean d0 and would insert the space back, so the spacing decision for this pair has
to be made on the horizontal gap rather than the diagonal. that is contained in the same
change.
for context on why the gate is narrow rather than general: an unconditioned "no gap means
no space" rule, tried on the docling side, rewrote 59 lines across 10 files of a 76 pdf
corpus and nearly all were wrong, `'3x3 conv, 64'` becoming `'3x3conv, 64'` and similar.
rotated and diagrammatic text abuts constantly. the narrow signature took that to 0.
pr to follow, with unit tests built through `tests/pdf_builder.py` and the regression
corpus run to confirm nothing else moves.
Contributor guide
Research direction
Start with page_item< PAGE_CELL >::is_adjacent_to in src/parse/page_items/page_cell.h, then inspect the ligature handling and contract_cells_into_lines_left_to_right in page_item_sanitators/cells.h. Use tests/pdf_builder.py to exercise the drop-cap case and run the regression corpus. Done means the drop cap joins its word without a space while unrelated text assembly remains unchanged.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- cpp
- Domain
- data
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 55/100