Skip to content

EPUB: TOC entries anchored on nested <span> ids all jump to the top of the chapter #5943

Description

@kjk

Reported for a Dostoevsky collection: nine TOC entries that point into the middle of one chapter file all resolve to the same page, both in the Bookmarks tree and when clicked.

РОМАН В ДЕВЯТИ ПИСЬМАХ   OPS/ch1-24.xhtml#id17   -> page 263
I                        OPS/ch1-24.xhtml#id18   -> page 263
II                       OPS/ch1-24.xhtml#id19   -> page 263
...
VIII                     OPS/ch1-24.xhtml#id25   -> page 263

The chapter is 11 pages long (the next TOC entry is page 274) and the anchors are spread over 45 KB of XHTML, so they should land on different pages.

Repro

Minimal EPUB attached (uploaded as .zip, rename to .epub). It has one chapter with two kinds of anchor, so the difference is visible in one file:

TOC entry page
span-anchored section 1 1
span-anchored section 2 1 wrong, should be ~6
span-anchored section 3 1 wrong, should be ~12
div-anchored section 1 3 correct
div-anchored section 2 9 correct
div-anchored section 3 14 correct

Both anchor kinds sit at the same places in the document; only the element carrying the id differs.

Cause

This is a MuPDF bug, not our link handling — the wrong page is already in what fz_resolve_link_dest() hands back.

The book puts every id on an inline <span> that wraps a block <div>, and nests them:

<span id="id17"><div class="title3"><p>РОМАН В ДЕВЯТИ ПИСЬМАХ</p></div>
  <span id="id18"><div class="title4"><p>I</p></div> ...content...
  </span><span id="id19"><div class="title4"><p>II</p></div> ...content...
  </span>...
</span>

That is invalid nesting (block inside inline). Tracing it:

  1. EngineMupdf.cpp ResolveLink() gets chapter=25, page=0, y=11.0 from fz_resolve_link_dest() for #id17 and for #id18#id25.
  2. In epub_resolve_link() (source/html/epub-doc.c), fz_find_html_target() returns y = 11.0 for every id, in every chapter, with page_h = 435. So page = y / ph is 0 every time and the destination is the chapter's first page.
  3. The match happens in find_flow_target() (source/html/html-outline.c), and the flow node it finds for each id is:
id=id17 type=6 y=11.0 x=11.0 w=0.0 h=11.0 boxtype=2 tag=span

type=6 is FLOW_ANCHOR, boxtype=2 is BOX_INLINE. MuPDF does emit a zero-width anchor node per id, but because the <span> has no inline content of its own it is never positioned by layout and keeps the initial line position (11, 11) — the top-left of the chapter's content box — for all of them.

find_box_target()'s box->s.layout.y fallback never applies either: inline boxes are only reachable through the flow list, so the box branch never visits them.

A flat sequence of <span id><div>…</div></span> siblings resolves fine; it takes the outer <span> wrapping the inner ones to trigger it, which is why the attached repro nests them.

Fix

Would have to be in MuPDF. The flow node's position is the broken input, so find_flow_target() returning flow->y can't be patched in isolation — the plausible fix is: when the id is found on an anchor whose box is BOX_INLINE with no laid-out content, walk from that box in document order to the next box that does have a position and use its y. That's surgery in html layout affecting every reflowable format, so filing this first rather than patching speculatively.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions