Skip to content

ApplyRedactions on an image shared across pages burns the redaction into the shared image; PDFOptimizer then strips the image from non-redacted pages (18.0.5PlusP1y) #115

Description

@BenasJanusonis

Environment

  • Datalogics.PDFL for .NET, APDFL 18.0.5PlusP1y (managed Datalogics.PDFL.dll FileVersion 18.0.5.57, native DL180pdfl.dll FileVersion 18.0.5.19, ProductName "APDFL 18.0.5PlusP1y")
  • Windows 11 x64, .NET 10

Summary

When a document contains an image XObject that is shared by multiple pages (one image object, referenced from each page's resources via a Form XObject — a common structure for scanned letterheads), applying a redaction to ONE page and then optimizing the document corrupts every other page:

  1. Document.ApplyRedactions() mutates the shared image object in place — the redaction square is burned into the image pixels, so after a plain Save(SaveFlags.Full) the black box is visible on every page that draws that image, not just the redacted page.
  2. If, instead of a plain save, the still-open document is passed to PDFOptimizer.Optimize(), the optimizer removes the XObject resource entries from every non-redacted page. The content streams still contain the /Fm0 Do operators, but the resources are gone, so viewers silently render nothing (MuPDF reports syntax error: cannot find XObject resource 'Fm0'). Backgrounds/photos disappear from all pages except the redacted one.

Neither step alone corrupts the file — see the matrix below.

Reproduction

Input: a 3-page PDF where each page's /Resources /XObject /Fm0 points at the same Form XObject, whose own resources contain one DCTDecode image (/Im0) drawn full-page. Pages also have a line of text. (Generator script for the exact file at the bottom; I can also send the 8 KB sample PDF on request.)

using Datalogics.PDFL;

using Library library = new();
using Document document = new("shared-image-3pages.pdf");

// one redaction on page 2 (0-based index 1), over the text line
using (Page page = document.GetPage(1))
{
    using Quad quad = new(
        new Point(100, 712), new Point(240, 712),
        new Point(100, 696), new Point(240, 696));
    using Color black = new(0, 0, 0);
    using Redaction redaction = new(page, new List<Quad> { quad }, black) { FillNormal = true };
}

document.ApplyRedactions();

// variant A: plain save  -> defect 1 (burn-in on all pages)
using (MemoryStream regular = new())
{
    document.Save(SaveFlags.Full, regular);
    File.WriteAllBytes("variantA.pdf", regular.ToArray());
}

// variant B: optimize the live document -> defect 2 (other pages lose the image)
using (MemoryStream optimized = new())
using (PDFOptimizer optimizer = new())
{
    optimizer.Optimize(document, optimized);
    File.WriteAllBytes("variantB.pdf", optimized.ToArray());
}

Observed results (all on 18.0.5PlusP1y)

Scenario Result
Redact p2 + ApplyRedactions + Save(SaveFlags.Full) Structurally valid, but the redaction square is burned into the shared image → box visible on pages 1 and 3 too (defect 1)
No redaction + PDFOptimizer only Correct (images downsampled/recompressed as expected, all references valid)
Redact p2 + ApplyRedactions + PDFOptimizer on the live Document Pages 1 and 3 lose their /XObject resource entries — content streams still call /Fm0 Do but the resource dictionaries are empty; the shared image survives only on page 2 (defect 2)
Redact p2 + ApplyRedactions + Save(SaveFlags.Full) → reopen from bytes → PDFOptimizer Resources intact on all pages (workaround for defect 2; defect 1 burn-in still present)

The same behavior reproduces on a real-world 26-page document (scanned letterhead shared by all pages): after redacting one page and optimizing, 25 of 26 pages ended up with dangling Do operators and no images.

Expected

  • Redacting page N must not alter the rendered appearance of any other page. If the redaction area intersects an image object shared with other pages, the page being redacted should get its own (sanitized) copy — mutating the shared object leaks the redaction onto unrelated pages.
  • The optimizer should never drop a resource that is still referenced by a page's content stream.

Workarounds we ship in the meantime

  • Deep-copy the page to be redacted into a temporary Document via InsertPages (this un-shares the XObjects), apply redactions there, and swap the result back with ReplacePages.
  • Always Save(SaveFlags.Full) + reopen before running PDFOptimizer on a document that had redactions applied in the same session.

Related release-note entries

The release notes list SF#46189 ("applying a Redaction could cause non-adjacent images to be removed"), SF#46980 ("form on multiple pages … holes punched through the form"), SF#46981 ("misplaced instances when an image was reused"), and SF#46629 ("optimizing images shared across different pages") as fixed in earlier releases — but the combination above still reproduces on 18.0.5PlusP1y.

Input file generator (Python, pikepdf + Pillow)

Script that produces the exact 3-page repro PDF
import io
import pikepdf
from pikepdf import Name
from PIL import Image as PILImage, ImageDraw

img = PILImage.new("RGB", (400, 566), "white")
d = ImageDraw.Draw(img)
d.rectangle([0, 0, 400, 80], fill=(230, 90, 30))
d.polygon([(400, 566), (400, 420), (260, 566)], fill=(230, 90, 30))
buf = io.BytesIO()
img.save(buf, "JPEG", quality=70)

pdf = pikepdf.new()
image = pikepdf.Stream(pdf, buf.getvalue())
image.Type = Name.XObject
image.Subtype = Name.Image
image.Width = 400
image.Height = 566
image.ColorSpace = Name.DeviceRGB
image.BitsPerComponent = 8
image.Filter = Name.DCTDecode
image = pdf.make_indirect(image)

form = pikepdf.Stream(pdf, b"q /GS0 gs 595 0 0 842 0 0 cm /Im0 Do Q")
form.Type = Name.XObject
form.Subtype = Name.Form
form.BBox = pikepdf.Array([0, 0, 595, 842])
form.Resources = pikepdf.Dictionary(
    XObject=pikepdf.Dictionary(Im0=image),
    ExtGState=pikepdf.Dictionary(GS0=pikepdf.Dictionary(Type=Name.ExtGState)),
)
form = pdf.make_indirect(form)

font = pdf.make_indirect(pikepdf.Dictionary(Type=Name.Font, Subtype=Name.Type1, BaseFont=Name.Helvetica))

for i in range(3):
    text = f"Page {i + 1} confidential number 123456789".encode()
    content = (
        b"/Artifact <</Subtype /Watermark /Type /Pagination >>BDC q 1 0 0 1 0 0 cm /Fm0 Do Q EMC "
        b"BT 0 0 0 rg /F1 12 Tf 100 700 Td (" + text + b") Tj ET"
    )
    page = pdf.make_indirect(
        pikepdf.Dictionary(
            Type=Name.Page,
            MediaBox=pikepdf.Array([0, 0, 595, 842]),
            Resources=pikepdf.Dictionary(
                XObject=pikepdf.Dictionary(Fm0=form),
                Font=pikepdf.Dictionary(F1=font),
                ProcSet=pikepdf.Array([Name.PDF, Name.Text, Name.ImageC]),
            ),
            Contents=pikepdf.Stream(pdf, content),
        )
    )
    pdf.pages.append(pikepdf.Page(page))

pdf.save("shared-image-3pages.pdf")

Posting here since this repo is the public .NET channel — happy to move this to the support portal and provide the sample PDFs + broken outputs there if that is preferred.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions