Skip to content

feat: add .cv resume file support - #4891

Open
ilanoh wants to merge 2 commits into
docling-project:mainfrom
ilanoh:feat/cv-input-format
Open

ilanoh wants to merge 2 commits into
docling-project:mainfrom
ilanoh:feat/cv-input-format

Conversation

@ilanoh

@ilanoh ilanoh commented Oct 19, 2026

Copy link
Copy Markdown

Adds .cv resume files as an input format, as agreed in #3567.

A .cv file (https://cvfile.org/spec/) is a PDF/A-3u that embeds its canonical Markdown or HTML as PDF Associated Files (ISO 32000-2, 14.13) and names the primary payload in a cv: XMP packet. Docling treated these files as ordinary PDFs and ran layout and OCR over the rendered pages. With this change Docling reads the embedded payload and converts it with the existing Markdown or HTML backend, so the text is kept exactly and no model runs.

Changes

  • InputFormat.CV with the cv extension and the application/vnd.cv+pdf MIME type.
  • CvDocumentBackend (docling/backend/cv_backend.py) reads cv:primaryPayload from the XMP packet with defusedxml, finds that file in the catalog /AF array with pypdf, and delegates to MarkdownDocumentBackend or HTMLDocumentBackend according to the payload /Subtype. It verifies the sha-256 digest from cv:integrity, applies the 16 MiB payload cap of the specification (section 7.3), and logs a warning for a newer major version (section 8.3).
  • CvFormatOption (SimplePipeline + CvDocumentBackend), registered as the default option for InputFormat.CV. StandardPdfPipeline is unchanged.
  • Detection in _DocumentConversionInput:
    • The .cv extension maps the PDF MIME type reported by filetype to application/vnd.cv+pdf, next to the OLE2 extension rule.
    • In docs(), a PDF whose XMP declares the http://ns.cvfile.org/cv/1.0/ namespace converts as CV when InputFormat.CV is in the format options. A .cv file converts as PDF when only InputFormat.PDF is allowed. Callers that pass allowed_formats=[InputFormat.PDF] therefore see no change.
    • The namespace search memory-maps the file. On a 200 MB PDF it takes 40 to 64 ms.
  • New format-cv extra (format-web, pypdf, defusedxml), included in standard. The imports are guarded like the email backend, and test_backend_optional_dependencies.py covers the missing dependency case.
  • Docs: supported formats table, CvFormatOption in the API reference, and the format-cv extra in the packaging skill reference.

Dependency on docling-core

DocumentOrigin rejects application/vnd.cv+pdf until docling-project/docling-core#898 is released. I will raise the docling-core floor in pyproject.toml and uv.lock once that release is out, as was done for AFP. Until then, the two end-to-end tests in tests/test_backend_cv.py fail on the origin MIME type check.

Questions for reviewers

  1. Including format-cv in standard adds pypdf to the default docling install. pypdfium2 cannot read the catalog /AF array, so I did not find a way to use it. I can move the extra out of standard if you prefer. In that case a slim install without the extra raises the install hint when it receives a PDF that declares the cv: namespace, unless the caller removes InputFormat.CV from allowed_formats.
  2. The content check for a PDF without the .cv extension relies on the XMP stream being unfiltered, which is the case for the reference producers (Python, TypeScript and Go SDKs). A .cv file with a compressed XMP stream and a .pdf name converts as a PDF.

Issue resolved by this Pull Request:
Resolves #3567

Checklist:

  • Documentation has been updated, if necessary.
  • Examples have been added, if necessary.
  • Tests have been added, if necessary.

Review evidence:

Before: a .cv file, or the same file named .pdf, ran through StandardPdfPipeline. The fixtures have a blank page, so the result was empty.
After: both convert through SimplePipeline from the embedded payload in about 0.03 s with the CLI (docling marie_curie.cv resume.pdf --to md). The Markdown output is identical to the ground truth. docling tests/data/pdf/bookmark_sample.pdf still runs StandardPdfPipeline.

Reference data: new files only, in tests/data/cv/. The two fixtures were built with the cvfile Python SDK from public facts about Marie Curie on a blank page, so any text in the output comes from the embedded payload. One has a Markdown primary payload with an HTML alternate, the other has only HTML. Ground truth was generated with DOCLING_GEN_TEST_DATA=1.

Commands and results, with docling-core#898 installed in editable mode:

  • make validate: all hooks pass.
  • uv run pytest tests/test_backend_cv.py tests/test_input_doc.py tests/test_invalid_input.py tests/test_backend_markdown.py tests/test_backend_html.py tests/test_backend_email.py tests/test_backend_afp.py tests/test_backend_optional_dependencies.py tests/test_video_format_routing.py: 324 passed, 1 skipped, 1 failed. The failure is test_backend_html.py::test_e2e_html_conversions. It also fails on main with docling-core main, and it passes with the locked docling-core 2.100.0.
  • All the specification test vectors (https://github.com/cvfile/cv/tree/main/spec/test-vectors) were converted with allowed_formats=[InputFormat.CV]. The valid files convert. The integrity mismatch, oversized payload, missing XMP and encrypted files fail with a DocumentLoadError that names the problem. The future major version file converts with a warning.

Not run: the full test suite, because I installed only the extras that the touched code needs.

A .cv file (https://cvfile.org/spec/) is a PDF/A-3u that embeds its
canonical Markdown or HTML as PDF Associated Files and names the primary
payload in a cv: XMP packet. Convert it from that payload with the
Markdown or HTML backend instead of parsing the rendered pages.

Add InputFormat.CV, detected from the .cv extension, the
application/vnd.cv+pdf MIME type, or the cv: XMP namespace in a PDF.
A PDF converts as CV only when InputFormat.CV is allowed, so callers
that allow only InputFormat.PDF keep the current behavior.

The backend verifies the sha-256 digest from cv:integrity, applies the
16 MiB payload cap of the specification, and warns on a newer major
version. pypdf and defusedxml come with the new format-cv extra, which
the standard bundle includes.

Resolves docling-project#3567

Signed-off-by: Ilan <ilan.ohayon3@gmail.com>
@github-actions

Copy link
Copy Markdown
Contributor

✅ DCO Check Passed

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

@mergify

mergify Bot commented Oct 19, 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-needs-care AI triage: check CI-safety findings before you approve CI enhancement New feature or request mimetype pdf PDF issue (except docling-parse) labels Oct 19, 2026
@github-actions

github-actions Bot commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

AI triage (advisory)

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

The PR adds InputFormat.CV and a CvDocumentBackend for .cv resume files. The backend reads the embedded Markdown or HTML payload from the PDF and converts it with the existing Markdown or HTML backend. It adds format detection by extension and XMP namespace, a format-cv extra, tests, and docs.

CI safety: needs maintainer care before you approve CI.

  • pyproject.toml: changes build, dependency, or tool configuration
  • uv.lock: changes locked dependencies
2 related pull request(s) in the same code
PR State Signal Assessment
#4880 open changes the same code related: This PR adds a different input format (iCalendar). It edits the same registration points, such as InputFormat, FormatToExtensions, and _get_default_option, but the two features are independent.
#4865 open changes the same code related: This PR adds ZIP member expansion in docs() and changes convert(). It touches the same function as this PR but solves a different problem.

Reference data changes

Change Files
added 4
  • added tests/data/cv/groundtruth/marie_curie.cv.json
  • added tests/data/cv/groundtruth/marie_curie.cv.md
  • added tests/data/cv/groundtruth/marie_curie_html.cv.json
  • added tests/data/cv/groundtruth/marie_curie_html.cv.md

Assessment: expected. The PR adds a new .cv input format. The four new reference files are the first outputs for the two new .cv sources, so no existing reference data changes.

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 3f772af807. It read the code and did not run it.

The change adds a PDF-to-CV reroute that runs for every PDF in the default converter, including installs that lack pypdf. Check what a slim install does with a PDF that declares the cv: namespace.

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

obj = item
backend_input = item
format = self._guess_format(obj)
format = self._resolve_cv_format(

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.

question: A .cv file may now fail on installs without format-cv, where it used to convert as a PDF

DocumentConverter() uses list(InputFormat) as allowed_formats by default, and format_to_options gets a CvFormatOption for each of them. So InputFormat.CV is always in format_options, even when pypdf and defusedxml are missing. _resolve_cv_format then maps the file to InputFormat.CV in two cases. In the first, the name ends in .cv. In the second, the XMP packet declares the cv: namespace. CvDocumentBackend.__init__ raises a plain ImportError when the dependencies are missing. InputDocument._init_doc re-raises every error that is not a DocumentLoadError. The conversion therefore aborts instead of returning a FAILURE result. Before this PR, these files converted as ordinary PDFs on a docling-slim[format-pdf] install.

Please confirm that this is intended. If it is not, fall back to InputFormat.PDF in _resolve_cv_format when the CV dependencies are unavailable. The new test test_backend_reports_missing_dependency_with_install_hint only covers the backend in isolation. It does not cover the default converter on a PDF.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Not intended, thanks. Fixed in e60e66c. AbstractDocumentBackend now has a dependencies_available() classmethod, which returns True by default. The CV, Markdown and HTML backends implement it. _resolve_cv_format resolves to InputFormat.PDF when the CV backend cannot run, so a slim install converts these files as PDFs, as before. test_cv_converts_as_pdf_without_cv_dependencies covers the routing through docs(). I also checked it in a fresh interpreter with pypdf blocked: both the .cv file and the renamed .pdf resolve to pdf.

stream = stream.get_object() if stream is not None else None
if not isinstance(stream, StreamObject):
continue
data = stream.get_data()

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.

suggestion: The 16 MiB cap is checked after the stream is fully decoded

stream.get_data() decodes the whole embedded stream. Only after that does len(data) > _MAX_PAYLOAD_BYTES run. A small compressed payload can still expand to a large buffer in memory before it is rejected. If the stream has a /Length or a /Params /Size entry, check it before decoding. You can also bound the decoder output. I could not read the installed pypdf source, so I did not check its own decompression limits.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in e60e66c. The backend now reads /Params /Size, which the .cv specification requires on every payload stream. It rejects a declared size above the cap before it decodes. The check after decoding stays in place for a stream that declares a wrong size. pypdf also bounds the output of its decoders, through pypdf.filters.ZLIB_MAX_OUTPUT_LENGTH (75 MB in 6.21), so memory stays bounded even then.

@github-actions github-actions Bot added the ai:review-lgtm AI first review found no blocker or major issue (advisory) label Oct 19, 2026
A default DocumentConverter allows InputFormat.CV, so on an install
without the format-cv extra a .cv file, or a PDF that declares the cv:
namespace, raised an ImportError where it used to convert as a PDF.
Add AbstractDocumentBackend.dependencies_available(), implement it for
the CV, Markdown and HTML backends, and resolve to InputFormat.PDF when
the CV backend cannot run.

Also reject a payload whose declared /Params /Size exceeds the 16 MiB
cap before decoding it.

Signed-off-by: Ilan <ilan.ohayon3@gmail.com>
@github-actions github-actions Bot added ai:ci-needs-care AI triage: check CI-safety findings before you approve CI and removed ai:ci-needs-care AI triage: check CI-safety findings before you approve CI ai:review-lgtm AI first review found no blocker or major issue (advisory) labels Oct 19, 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): no blocker found

This review covers the changes since 3f772af807 at e60e66c7a7. It read the code and did not run it.

I found nothing important in the new changes. dependencies_available() routes a .cv file or a CV-tagged PDF to the PDF format when the CV, Markdown or HTML packages are missing. The /Params /Size check now runs before decoding the payload.

Earlier findings

  • fixed (question): A .cv file may now fail on installs without format-cv, where it used to convert as a PDF
  • fixed (suggestion): The 16 MiB cap is checked after the stream is fully decoded

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

@codecov

codecov Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 45.45455% with 108 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
docling/backend/cv_backend.py 36.60% 97 Missing ⚠️
docling/datamodel/document.py 66.66% 6 Missing and 4 partials ⚠️
docling/backend/abstract_backend.py 66.66% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

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-needs-care AI triage: check CI-safety findings before you approve CI ai:review-lgtm AI first review found no blocker or major issue (advisory) enhancement New feature or request mimetype pdf PDF issue (except docling-parse)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support .cv resume files (PDF/A-3u with embedded canonical Markdown) as a first-class input format

1 participant