Skip to content

feat(pdf): attach the declared language and document properties to the document - #4781

Open
wittjeff wants to merge 3 commits into
docling-project:mainfrom
wittjeff:feat/pdf-document-metadata
Open

wittjeff wants to merge 3 commits into
docling-project:mainfrom
wittjeff:feat/pdf-document-metadata

Conversation

@wittjeff

@wittjeff wittjeff commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

Closes #4741. Addresses the reading side of #1826 (XMP metadata): the XMP packet is read on the docling-parse backend and /Info on the pypdfium2 backend, standard schemas only; custom namespaces, nested xmpMM structures, and metadata editing stay out of scope.

Problem

A PDF's catalog /Lang entry and its document properties (/Info dictionary, XMP packet) never reach the DoclingDocument. docling-parse exposes them in its document annotations (language, meta_xml), but the pipeline reads those only for the outline, and PdfDocumentBackend has no hook for document-level metadata.

Change

  • PdfDocumentBackend.get_document_metadata() with an empty default, implemented for both PDF backends. pypdfium2 reads /Lang (FPDFCatalog_GetLanguage) and /Info; docling-parse reads /Lang and the XMP packet. /Info keys are mapped to the XMP names ISO 32000-2 Table 349 pairs them with (Title -> dc:title, Producer -> pdf:Producer, ...), and PDF date strings become ISO 8601, so both backends report one vocabulary.
  • PdfPipelineOptions.attach_document_metadata (CLI --document-metadata). When on, the pipeline fetches the metadata while the backend is open and records it on DoclingDocument.body.meta at assembly, the document-level meta slot per docling-core#544:
    • body.meta.language = LanguageMetaField(code=<primary subtag>, created_by="pdf:/Lang"), so a declared language is distinguishable from a detected one;
    • pdf__lang custom field with the full BCP 47 tag (en-US), because HumanLanguageLabel accepts primary subtags only (docling-core#868);
    • one namespaced custom field per property: dc__title, dc__creator, pdf__producer, xmp__create_date, pdfuaid__part.
  • New docling/utils/pdf_metadata.py follows the pdf_outline.py pattern: lazy pypdfium2 import so the service_client chain stays importable on docling-slim.

Why opt-in

docling-core's Markdown and HTML serializers still render body meta fields as content (docling-core#745): with the field set, export_to_markdown() appends confidence=None created_by='pdf:/Lang' code=<HumanLanguageLabel.EN: 'en'> as a paragraph. The option stays off by default until that fix is released and docling bumps its docling-core floor; the JSON export is already correct.

Tests

  • tests/test_pdf_document_metadata.py (core lane): XMP flattening (rdf:Alt x-default, rdf:Seq join, attribute properties, unknown namespaces skipped, malformed packet), PDF date conversion, both backends on the new fixture tests/data/pdf/document_metadata.pdf (built with pypdf: /Lang, /Info, XMP) and on a plain PDF, and the attach semantics (existing meta kept, unknown primary subtag keeps only pdf__lang, empty metadata leaves body.meta unset).
  • tests/test_pdf_document_metadata_pipeline.py (ml_pdf_model): end-to-end with the option on and off.
  • make validate passes; tests/test_heading_hierarchy_bookmarks.py, tests/test_cli.py, tests/test_options.py, tests/test_backend_optional_dependencies.py pass locally.

Docs

docs/usage/advanced_options.md section "PDF document metadata"; the SDK and CLI references in the packaged usage skill.

🤖 Generated with Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

✅ DCO Check Passed

Thanks @wittjeff, all your commits are properly signed off. 🎉

@mergify

mergify Bot commented Oct 12, 2026

Copy link
Copy Markdown
Contributor

Merge Protections

🔴 1 of 2 protections blocking · waiting on 👀 reviews

Protection Waiting on
🔴 Require two reviewer for test updates 👀 reviews
🟢 Enforce conventional commit —

🔴 Require two reviewer for test updates

Waiting for

  • #approved-reviews-by >= 2
This rule is failing.

When test data is updated, we require two reviewers

  • #approved-reviews-by >= 2

Show 1 satisfied protection

🟢 Enforce conventional commit

Make sure that we follow https://www.conventionalcommits.org/en/v1.0.0/

  • title ~= ^(fix|feat|docs|style|refactor|perf|test|build|ci|chore|revert)(?:\(.+\))?(!)?:

@github-actions github-actions Bot added ai:ci-safe AI triage found no CI-safety concern (advisory) CLI issues related to the command line interface (CLI) docling-document enhancement New feature or request language support pdf PDF issue (except docling-parse) pdf parsing PDF issue related to docling-parse labels Oct 12, 2026
@github-actions

github-actions Bot commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

AI triage (advisory)

Checked commit fe043a5d57. The analysis read the diff only. It did not run code from this pull request.

The PR adds opt-in PDF document metadata extraction (attach_document_metadata) to both PDF backends, mapping /Lang, /Info, and XMP packet properties to DoclingDocument.body.meta, with a new docling/utils/pdf_metadata.py utility, a CLI flag --document-metadata, and focused unit and pipeline tests.

CI safety: no concern found. A maintainer must still approve CI.

2 related pull request(s) in the same code
PR State Signal Assessment
#4654 open changes the same code related: PR 4654 adds OCR-based broken-text-layer replacement and touches pipeline_options.py, standard_pdf_pipeline.py, cli/main.py, and the agent skill references, but it solves a different problem and shares no overlapping logic with this PR's metadata extraction.
#4897 open changes the same code related: PR 4897 registers a new Nemotron VLM preset and adds --trust-remote-code; it touches cli/main.py and pipeline_options.py in different sections and solves a completely different problem.

The ai:* labels are advisory. They do not approve CI and they do not add tests:full. A new push runs this check again.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

AI first review (advisory): no blocker found

This review covers the full diff at 32245ecccc. It read the code and did not run it.

The change adds opt-in PDF catalog language and document property extraction for both PDFium and docling-parse backends. Check that the custom property names on DoclingDocument.body.meta match downstream serializer expectations.

A maintainer makes the merge decision. This review never approves the pull request.

@wittjeff

Copy link
Copy Markdown
Contributor Author

The docling-core side that gates the default is now open: docling-project/docling-core#869 (Markdown and plain text stop rendering meta.language as prose; HTML emits <html lang> and per-element lang). Once that is released and docling's docling-core floor is bumped, attach_document_metadata can default to on in a follow-up.

@codecov

codecov Bot commented Oct 14, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 92.85714% with 12 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
docling/utils/pdf_metadata.py 93.91% 5 Missing and 4 partials ⚠️
docling/backend/pypdfium2_backend.py 60.00% 1 Missing and 1 partial ⚠️
docling/backend/pdf_backend.py 66.66% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

wittjeff and others added 2 commits October 19, 2026 11:35
…e document

A PDF's catalog `/Lang` entry and its document properties (the `/Info`
dictionary and the XMP packet) never reached the `DoclingDocument`:
docling-parse exposes them in its document annotations, but the pipeline
read those only for the outline.

Add `PdfDocumentBackend.get_document_metadata()` with an empty default and
implement it for both PDF backends: pypdfium2 reads `/Lang` and `/Info`,
docling-parse reads `/Lang` and the XMP packet. `/Info` keys are mapped to
the XMP names ISO 32000-2 pairs them with, so both backends report the
same vocabulary, and PDF date strings are converted to ISO 8601.

The new `PdfPipelineOptions.attach_document_metadata` option (CLI:
`--document-metadata`) records the result on `DoclingDocument.body.meta`,
the document-level meta slot (docling-core#544): the language as
`BaseMeta.language` with the primary subtag and `created_by="pdf:/Lang"`,
the full BCP 47 tag as the `pdf__lang` custom field, and each property as
a namespaced custom field such as `dc__title` or `pdf__producer`.

The option is off by default because docling-core's Markdown and HTML
serializers still render body meta fields as content (docling-core#745).

Closes docling-project#4741

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Jeff Witt <1848307+wittjeff@users.noreply.github.com>
… stubs

_build_document now reads the option to decide whether to fetch the
document metadata, so the SimpleNamespace stubs must define it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Jeff Witt <1848307+wittjeff@users.noreply.github.com>
@wittjeff
wittjeff force-pushed the feat/pdf-document-metadata branch from 32245ec to 13cd676 Compare October 20, 2026 00:47
@github-actions github-actions Bot added ai:ci-safe AI triage found no CI-safety concern (advisory) and removed ai:ci-safe AI triage found no CI-safety concern (advisory) ai:review-lgtm AI first review found no blocker or major issue (advisory) labels Oct 20, 2026

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

AI first review (advisory): blocker found

This review covers the full diff at 13cd6768a8. It read the code and did not run it.

An XMP packet that declares an entity or DOCTYPE makes the whole conversion fail when attach_document_metadata is on. The new extractor catches only ParseError. Check this error path first.

A maintainer makes the merge decision. This review never approves the pull request.

without a text value, are skipped. An unparsable packet yields no properties.
"""
try:
root = _defused_fromstring(xml)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

blocker: Hostile XMP packet fails the whole conversion

Trigger: the option is on, the backend is docling-parse, and the PDF has an XMP packet with an <!ENTITY ...> declaration, as in a billion-laughs payload. defusedxml forbids entities by default (forbid_entities=True). It raises EntitiesForbidden. DTDForbidden and ExternalReferenceForbidden exist too. All three derive from DefusedXmlException, which is a ValueError (defusedxml/common.py:18). They are not ElementTree.ParseError. The except ElementTree.ParseError block does not catch them.

Result: the error leaves get_document_metadata() and _build_document(). docling/pipeline/base_pipeline.py:110 catches it and sets ConversionStatus.FAILURE. The PDF's text and layout are lost because of a metadata property. The docstring says an unparsable packet yields no properties, and this case breaks that rule. Metadata is optional, and the input is untrusted PDF content.

Fix: also catch DefusedXmlException (from defusedxml.common) in parse_xmp_properties, log it, and return {}. Add a test with an XMP packet that has an entity declaration. The existing malformed-packet test covers only ParseError.

@github-actions github-actions Bot added the ai:review-changes AI first review suggests changes label Oct 20, 2026
… the conversion

parse_xmp_properties caught only ElementTree.ParseError. defusedxml raises
EntitiesForbidden, DTDForbidden and ExternalReferenceForbidden (all
DefusedXmlException, a ValueError) for a packet that declares entities or
external references, so a hostile XMP packet escaped get_document_metadata()
and the pipeline marked the whole conversion FAILURE when
attach_document_metadata was on. Catch DefusedXmlException too, log a warning
and return no properties, as the docstring already promised for an unusable
packet. Adds a test with an entity-declaring packet.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Jeff Witt <1848307+wittjeff@users.noreply.github.com>
@github-actions github-actions Bot removed the ai:ci-safe AI triage found no CI-safety concern (advisory) label Oct 20, 2026
@github-actions github-actions Bot added ai:ci-safe AI triage found no CI-safety concern (advisory) and removed ai:review-changes AI first review suggests changes labels Oct 20, 2026

This branch has not been deployed

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

Labels

ai:ci-safe AI triage found no CI-safety concern (advisory) CLI issues related to the command line interface (CLI) docling-document enhancement New feature or request language support pdf parsing PDF issue related to docling-parse pdf PDF issue (except docling-parse)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

PDF: document language (/Lang) and XMP metadata are not attached

1 participant