Description of the bug
Page.apply_redactions() corrupts unredacted text: the leading glyph of a
text span is duplicated when the page content stream is regenerated. It
reproduces even when the redaction removes nothing — a dummy annotation over an
empty area with every removal flag set to *_NONE — so it is the content-stream
regeneration itself, not the removal, that introduces the extra glyph.
The trigger is a span drawn with an "overprint" construct: a glyph is shown, then
0 0 TD moves back to the line-matrix origin, then a word is shown starting at
that same x. The two draws overlap exactly, so the page correctly reads faeces.
After apply_redactions() regenerates the stream, the draws no longer coincide
and both become visible, yielding ffaeces.
The duplication is not cosmetic: after redaction the two f glyphs render at
distinct x-origins (confirmed via get_text("rawdict")), so both the rendered
page and the extracted text show the extra letter.
This is a regression — correct in every release from 1.24.14 through
1.27.2.3, broken only in 1.28.0 (MuPDF 1.29.0):
| PyMuPDF |
MuPDF |
text after no-op apply_redactions() |
| 1.24.14 |
1.24.11 |
faeces (correct) |
| 1.26.7 |
1.26.12 |
faeces (correct) |
| 1.27.1 |
1.27.1 |
faeces (correct) |
| 1.27.2 |
1.27.2 |
faeces (correct) |
| 1.27.2.3 |
1.27.2 |
faeces (correct) |
| 1.28.0 |
1.29.0 |
ffaeces (leading glyph duplicated) |
Environment: PyMuPDF 1.28.0 / MuPDF 1.29.0 (broken), last good 1.27.2.3 /
MuPDF 1.27.2; Python 3.13.13; Linux x86-64. The construct is emitted by
real-world producers (first hit on an InDesign-generated PDF, where the extra
glyph landed inside a math formula). Looks related to the MuPDF 1.29.0 text/font
content-stream changes also behind the other 1.28.0 regressions (#5049, #5052).
How to reproduce the bug
Fully self-contained. Step 1 builds a ~600-byte PDF with the standard library
only (Helvetica base-14, no embedded fonts, no third-party content); step 2 needs
PyMuPDF.
Step 1 — make_repro_pdf.py (writes overprint.pdf):
"""Write a minimal, content-free PDF that triggers the redaction glyph-duplication bug.
The page draws the letter 'f' twice using the "overprint" construct:
(f)Tj # draw a lone 'f' at the line-matrix origin
0 0 TD # TD moves relative to the LINE matrix, not the pen -> back to origin
(faeces)Tj # draw 'faeces' starting at the SAME x -> its 'f' overprints the lone 'f'
Both f's paint at the same position, so the page shows a single, correct
"faeces" and text extraction returns "faeces".
"""
import sys
CONTENT = b"BT\n/F1 24 Tf\n1 0 0 1 100 700 Tm\n(f)Tj\n0 0 TD\n(faeces)Tj\nET\n"
OBJECTS = [
b"<< /Type /Catalog /Pages 2 0 R >>",
b"<< /Type /Pages /Kids [3 0 R] /Count 1 >>",
b"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 300 800] "
b"/Resources << /Font << /F1 5 0 R >> >> /Contents 4 0 R >>",
b"<< /Length %d >>\nstream\n%s\nendstream" % (len(CONTENT), CONTENT),
b"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>",
]
def build() -> bytes:
pdf = b"%PDF-1.7\n"
offsets = []
for i, body in enumerate(OBJECTS, start=1):
offsets.append(len(pdf))
pdf += b"%d 0 obj\n%s\nendobj\n" % (i, body)
xref_pos = len(pdf)
pdf += b"xref\n0 %d\n" % (len(OBJECTS) + 1)
pdf += b"0000000000 65535 f \n"
for off in offsets:
pdf += b"%010d 00000 n \n" % off
pdf += b"trailer\n<< /Size %d /Root 1 0 R >>\nstartxref\n%d\n%%%%EOF\n" % (len(OBJECTS) + 1, xref_pos)
return pdf
if __name__ == "__main__":
out = sys.argv[1] if len(sys.argv) > 1 else "overprint.pdf"
with open(out, "wb") as fh:
fh.write(build())
print(f"wrote {out}")
Step 2 — repro.py (extract, no-op redact, extract again):
import pymupdf
doc = pymupdf.open("overprint.pdf")
page = doc[0]
before = page.get_text().strip()
# A redaction that targets nothing: empty corner, every removal flag NONE.
page.add_redact_annot(pymupdf.Rect(2, 2, 5, 5))
page.apply_redactions(
text=pymupdf.PDF_REDACT_TEXT_NONE,
images=pymupdf.PDF_REDACT_IMAGE_NONE,
graphics=pymupdf.PDF_REDACT_LINE_ART_NONE,
)
after = page.get_text().strip()
print(f"PyMuPDF {pymupdf.version[0]} / MuPDF {pymupdf.version[1]}")
print(f" before redaction: {before!r}")
print(f" after redaction: {after!r}")
Run:
python make_repro_pdf.py overprint.pdf
python repro.py
Observed on 1.28.0 (expected: after stays 'faeces'):
PyMuPDF 1.28.0 / MuPDF 1.29.0
before redaction: 'faeces'
after redaction: 'ffaeces'
PyMuPDF version
1.28.0
Operating system
Linux
Python version
3.13
Description of the bug
Page.apply_redactions()corrupts unredacted text: the leading glyph of atext span is duplicated when the page content stream is regenerated. It
reproduces even when the redaction removes nothing — a dummy annotation over an
empty area with every removal flag set to
*_NONE— so it is the content-streamregeneration itself, not the removal, that introduces the extra glyph.
The trigger is a span drawn with an "overprint" construct: a glyph is shown, then
0 0 TDmoves back to the line-matrix origin, then a word is shown starting atthat same x. The two draws overlap exactly, so the page correctly reads
faeces.After
apply_redactions()regenerates the stream, the draws no longer coincideand both become visible, yielding
ffaeces.The duplication is not cosmetic: after redaction the two
fglyphs render atdistinct x-origins (confirmed via
get_text("rawdict")), so both the renderedpage and the extracted text show the extra letter.
This is a regression — correct in every release from 1.24.14 through
1.27.2.3, broken only in 1.28.0 (MuPDF 1.29.0):
apply_redactions()faeces(correct)faeces(correct)faeces(correct)faeces(correct)faeces(correct)ffaeces(leading glyph duplicated)Environment: PyMuPDF 1.28.0 / MuPDF 1.29.0 (broken), last good 1.27.2.3 /
MuPDF 1.27.2; Python 3.13.13; Linux x86-64. The construct is emitted by
real-world producers (first hit on an InDesign-generated PDF, where the extra
glyph landed inside a math formula). Looks related to the MuPDF 1.29.0 text/font
content-stream changes also behind the other 1.28.0 regressions (#5049, #5052).
How to reproduce the bug
Fully self-contained. Step 1 builds a ~600-byte PDF with the standard library
only (Helvetica base-14, no embedded fonts, no third-party content); step 2 needs
PyMuPDF.
Step 1 —
make_repro_pdf.py(writesoverprint.pdf):Step 2 —
repro.py(extract, no-op redact, extract again):Run:
Observed on 1.28.0 (expected:
afterstays'faeces'):PyMuPDF version
1.28.0
Operating system
Linux
Python version
3.13