Skip to content

Text rendering mode is reset at every BT and ignored when set outside a text object #572

Description

@abimaelmartell

Summary

Text render mode is part of the graphics state and persists across text objects (ISO 32000-1, 8.4.1 and 9.3.6): a 3 Tr (invisible) set in one BT … ET still applies to the next text object, and one set outside any text object applies to all that follow. The extractor forgets the mode at the next BT and ignores one set outside a text object, so with the default include_invisible: false it serves text that no viewer shows.

Reproduction

Two one-page PDFs, Helvetica as /F1:

  1. Mode set inside the first text object, second text object relying on it:

    BT /F1 12 Tf 72 700 Td 3 Tr (alpha) Tj ET
    BT /F1 12 Tf 72 680 Td (beta) Tj ET
    

    Expected (invisible text skipped): nothing. Actual: beta.

  2. Mode set outside any text object:

    3 Tr
    BT /F1 12 Tf 72 700 Td (alpha) Tj ET
    BT /F1 12 Tf 72 680 Td (beta) Tj ET
    

    Expected: nothing. Actual: alpha beta.

A viewer renders both pages blank (rendered with MuPDF: 0 dark pixels in the text area, against 597 for the same text under 0 Tr).

Where

The page walker in src/extractor/content_stream.rs tracks render_mode per text object rather than as graphics state (and the q/Q stack). The detector's executed-content scan (src/detector/content_scan.rs) already follows Tr through q/Q and across text objects; the extractor should read it the same way.

Impact

Searchable-scan producers that set 3 Tr once per page in the first text object (or before it) have their invisible layer served as the page's text. Seen on main at 6f24415 and 1.22.1.

Suggested fix

Keep text_rendering_mode as graphics state in the page walker: initialise it once per content stream (a form XObject inherits the invoker's value, as src/extractor/xobjects.rs already does), leave it untouched at BT/ET, apply Tr wherever it appears, and keep the existing q/Q save and restore. The comment at the BT handler notes that the separate paint state already retains Tr for weight inference, so the two can share one reading. The form-XObject walker does not reset the mode at BT, so today the page walker and the form walker disagree on the same operators.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions