Skip to content

[Feature]: Expose ascent, descent, matrix and rendered_advance on TextChar #2926

Open

Description

@ankursri494

Problem Statement

TextChar in Rust carries ascent, descent, matrix and rendered_advance, but the Python bindings and the C API expose none of them.

Without them, bbox is the only vertical information about a glyph, and bbox always runs from the baseline up by the font size. Fonts with very different descents end up with identical boxes. Rotated text gets an axis-aligned box with no way to rebuild the glyph's actual orientation.

There's a second gap: when a font has no /Descent (or no FontDescriptor at all), the library falls back to the standard-14 metrics or to −0.35. A caller can't tell a real descent from that default.

With 0.3.78:

import pdf_oxide

b = pdf_oxide.DocumentBuilder()
page = b.letter_page().font("Helvetica", 10).at(72, 100).text("Ag")
page.done()
doc = pdf_oxide.PdfDocument.from_bytes(b.build())

g = doc.extract_chars(0)[1]
print([a for a in dir(g) if not a.startswith("_")])
# ['advance_width', 'bbox', 'char', 'color', 'font_name', 'font_size', 'font_weight',
#  'is_italic', 'is_monospace', 'mcid', 'origin_x', 'origin_y', 'rotation_degrees']

print(g.char, g.origin_y, g.bbox)
# g 100.0 (78.67, 100.0, 5.56, 10.0)   <- box starts at the baseline; the descender of "g" is outside it

print(hasattr(g, "descent"), hasattr(g, "ascent"), hasattr(g, "matrix"), hasattr(g, "rendered_advance"))
# False False False False

Proposed Solution

Expose on TextChar in Python and through the C API (pdf_oxide_char_get_*):

  1. ascent and descent: the values already computed, in user space (scaled by the effective font size).
  2. font_ascent and font_descent: the font's own /Ascent and /Descent as written in its FontDescriptor (or the standard-14 metrics), as a fraction of the em, or None when the font gives none and the library used its default.
  3. matrix: the full text rendering matrix (text matrix × CTM) for the glyph, six numbers.
  4. rendered_advance: the advance to the next glyph's origin, including character and word spacing.

Alternatives Considered

Re-parsing the font dictionaries with a second library to read /Descent. That doubles the parsing work, can disagree with pdf_oxide on which font a glyph used, and is still blind to the library's default when no descent is present. Rotation can be estimated from rotation_degrees, but without the matrix the glyph's actual corners, skew and direction can't be rebuilt.

Feature Area

Text Extraction

Priority

Would be helpful

Use Case

  • Glyph boxes under other conventions: pdfminer/pdfplumber (descent to descent + font size), the ascent-to-descent band used by MuPDF.
  • Baseline-relative layout: grouping glyphs of different fonts and sizes on one visual line.
  • Real corners for rotated or skewed text, to place and group it correctly.
  • Character and word spacing: knowing where glyph spacing came from (rendered_advance vs advance_width).

Example Usage (Optional)

for c in doc.extract_chars(0):
    a, b, cc, d, e, f = c.matrix
    bottom = c.origin_y + c.descent
    top = bottom + c.font_size
    print(c.char, c.font_descent, c.descent, c.rendered_advance, (bottom, top))

Additional Context

Tested on pdf_oxide 0.3.78 (PyPI wheel, Python 3.12, Linux x86_64).

Checklist

  • I have searched existing issues to make sure this is not a duplicate
  • I have provided a clear description of the feature
  • I understand a maintainer must accept this issue and agree on the approach before I write any implementation code
  • I am willing to contribute to the implementation (optional)

Activity

  1. added this to the v0.3.79 milestone on Oct 18, 2026
  2. yfedoseev commented on Oct 18, 2026

    @yfedoseev
    Owner

    Accepted for 0.3.79, approach as proposed:

    1. ascent and descent (already computed, user space) exposed.
    2. font_ascent and font_descent: the font's own /Ascent / /Descent (or standard-14 metrics) as a fraction of the em, None when the font gives none and the library used its default — so a caller can tell a real value from the fallback.
    3. matrix: the glyph's text rendering matrix (text matrix × CTM), six numbers.
    4. rendered_advance: the advance to the next glyph's origin, character and word spacing included.
      bbox keeps its current convention.

    Scope: every binding. Rust already carries 1, 3 and 4; the C API gains pdf_oxide_char_get_ascent/descent/font_ascent/font_descent/matrix/rendered_advance (header in include/), and every binding over it exposes them: Python, WASM/JS, Go, Java/Kotlin/Scala/Clojure (JNI), C#, C++, Dart, Elixir, Julia, Objective-C, PHP, R, Ruby, Swift, Zig. Values are tested in Rust with synthetic PDFs (a font with and without /Descent, a rotated run); each binding gets a surface test that the accessors exist and return the Rust values.

  3. yfedoseev commented on Oct 25, 2026

    @yfedoseev
    Owner

    Progress on release/v0.3.79 (not yet released): Python TextChar now has ascent, descent, rendered_advance and matrix, and the C API has pdf_oxide_char_get_ascent, _descent, _rendered_advance and _matrix (tests/test_ffi_a_character_gives_its_extent_advance_and_matrix.rs, python/tests/test_api_coverage.py). Still open: font_ascent and font_descent as the font declares them (None when the library used its default), which needs the font's declared metrics kept on FontInfo.

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

    enhancementNew feature or request

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions