Skip to content

Add optional_content() context manager for screen-only / print-only content#1870

Open
eugen-goebel wants to merge 3 commits into
py-pdf:masterfrom
eugen-goebel:feat-441-optional-content-groups
Open

Add optional_content() context manager for screen-only / print-only content#1870
eugen-goebel wants to merge 3 commits into
py-pdf:masterfrom
eugen-goebel:feat-441-optional-content-groups

Conversation

@eugen-goebel

Copy link
Copy Markdown

Link to issue

Closes #441.

Description of change

Adds an FPDF.optional_content() context manager that places the content drawn inside it in a PDF Optional Content Group (a "layer"), with independent on-screen and in-print visibility. This covers the screen-only / print-only images requested in the issue, and works for any content, not just images.

As discussed with @andersonhc in the issue, I went with the context-manager approach (more flexible than an image() argument, and it keeps the implementation simple). Under the hood it adds Optional Content Group and Usage objects, a /Properties entry in the page resource dictionary, /OCProperties in the document catalog (with an /AS usage-application array for the View and Print events), and an /OC marked-content sequence around the grouped content. The minimum PDF version is raised to 1.5.

Demo

pdf = FPDF()
pdf.add_page()
with pdf.optional_content(on_print=False):   # shown on screen, not printed
    pdf.image("background.png", x=0, y=0, w=pdf.epw)
with pdf.optional_content(on_view=False):    # printed, not shown on screen
    pdf.cell(text="Printed copy only")

Testing strategy

Adds test/test_optional_content.py with a reference-PDF test (assert_pdf_equal) rendering screen-only, print-only and always-visible content in one document. Documents without optional content are byte-identical to before.

Checklist

  • A unit test is covering the code added / modified by this PR
  • In case of a new feature, docstrings have been added, with also some documentation in the docs/ folder
  • A mention of the change is present in CHANGELOG.md
  • This PR is ready to be merged

By submitting this pull request, I confirm that my contribution is made under the terms of the GNU LGPL 3.0 license.

Closes py-pdf#441.

Adds an FPDF.optional_content() context manager that places the content
drawn inside it in a PDF Optional Content Group (a "layer"), with
independent on-screen and in-print visibility. This covers the screen-only
and print-only images requested in the issue, and works for any content,
not just images:

    with pdf.optional_content(on_print=False):
        pdf.image("background.png", x=0, y=0, w=pdf.epw)

Implementation:
- PDFOptionalContentGroup objects carrying a /Usage view/print state
- /OCProperties in the document catalog, with an /AS usage-application
  array that toggles groups for the View and Print events
- a /Properties entry in each page resource dictionary
- an /OC marked-content sequence around the grouped content

Includes a reference-PDF test, a CHANGELOG entry and a documentation page.
Based on a recipe by @digidigital.
Optional Content Groups are intentionally not PDF/A-compliant, so the
check-reference-pdf-files VeraPDF step flags test/optional_content.pdf.
This is handled the same way as fpdf2's other non-PDF/A features.

@andersonhc andersonhc left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the contribution.

Please see the comments.

Comment thread fpdf/output.py Outdated
self.struct_tree_root: Optional[PDFObject] = None
self.a_f: Optional[str] = None
self.page_labels: Optional[str] = None
# snake_case "o_c_properties" serializes to "/OCProperties" (cf. a_f -> /AF):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this comment is necessary.
The serialization conversion happens for every field and is commented on the syntax.py classes.

"6.1.10-1": "REASON: fpdf2 wants to support LZWDecode filter",
"6.1.11-1": "REASON: /EF is allowed in order for fpdf2 to be able to embed files",
"6.1.11-2": "REASON: /EmbeddedFiles is allowed in order for fpdf2 to be able to embed files",
"6.1.13-1": "REASON: fpdf2 supports Optional Content Groups (layers) via FPDF.optional_content(), which are not permitted in PDF/A",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's interesting. PDF/A-1 prohibits optional contents - while PDF/A-2 and later ones allow it.

Can you make it raise PDFAComplianceError on optional_content() if the user selected PDFA/1?

Also add some tests in test/pdf-a.

Optional content is prohibited in PDF/A-1 but permitted from PDF/A-2
onwards, so optional_content() now raises PDFAComplianceError only when
the document enforces PDF/A-1. Adds tests for the PDF/A-1 rejection and
for PDF/A-2 still being allowed, and drops a redundant comment.
@eugen-goebel

Copy link
Copy Markdown
Author

Thanks for the review. Both points are addressed in the latest commit:

  • Removed the explanatory comment on o_c_properties.
  • optional_content() now raises PDFAComplianceError when the document enforces PDF/A-1, since optional content is prohibited there but permitted from PDF/A-2 onwards. Added two tests in test/pdf-a/test_pdf_a_restrictions.py, one for the PDF/A-1 rejection and one confirming PDF/A-2 still works.

I kept the verapdf-ignore entry, as the reference PDF is still validated against the PDF/A profile in CI.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Allow to insert screen-only images that do not get printed

2 participants