Repository navigation
fix(pptx): preserve run-level hyperlinks and character formatting - #4443
avanish-garg wants to merge 4 commits into
Conversation
|
✅ DCO Check Passed Thanks @avanish-garg, all your commits are properly signed off. 🎉 |
Merge Protections🔴 1 of 2 protections blocking · waiting on 👀 reviews
🔴 Require two reviewer for test updatesWaiting for
This rule is failing.When test data is updated, we require two reviewers
Show 1 satisfied protection🟢 Enforce conventional commitMake sure that we follow https://www.conventionalcommits.org/en/v1.0.0/
|
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
d13aba6 to
7e92359
Compare
ceberam
left a comment
There was a problem hiding this comment.
Thanks @avanish-garg for this PR, since indeed the PPTX backend was not yet keeping the formatting unlike the other Office backends.
The new code and tests are technically sound and functionally correct.
My only recommendation would be to clean up the excessive inline commentary and verbose private docstrings.
There are multiple lengthy inline comment blocks that explain why previous behavior was buggy, detail developer rationale, or repeat method-level documentation. The inline comments in Docling should also limit explanations on how third-party applications like PowerPoint work, since developers and maintainers can refer to the documentation of those applications directly, if necessary.
|
Thanks for the review @ceberam. Done with the changes! |
ceberam
left a comment
There was a problem hiding this comment.
Thanks @avanish-garg for addressing my comment.
All good from my side.
|
@avanish-garg do you mind having a last round fixing the conflict with |
0c25dba to
0d3aed1
Compare
AI triage (advisory)Checked commit This pull request preserves run-level character formatting and hyperlinks in the PowerPoint backend. It wraps multi-run paragraphs into inline groups. CI safety: no concern found. A maintainer must still approve CI. 1 related pull request(s) in the same code
Reference data changes
Assessment: expected. The reference data changes match the new paragraph run splitting and inline group structure in PowerPoint documents. The |
0d3aed1 to
2513684
Compare
ceberam
left a comment
There was a problem hiding this comment.
@avanish-garg I've just seen a blocking issue and added a couple of suggestions worth addressing.
|
|
||
| def _get_run_hyperlink_target(self, run: _Run) -> Optional[AnyUrl | Path]: | ||
| """Resolve a run's hyperlink to a URL or local path, or ``None``.""" | ||
| address = run.hyperlink.address |
There was a problem hiding this comment.
_get_run_hyperlink_target does not guard against KeyError from a dangling relationship. run.hyperlink.address calls part.target_ref(rId) internally, which raises KeyError if the relationship ID no longer exists in the file, a condition that occurs with sanitized or re-saved PPTX files. The DOCX backend's counterpart explicitly catches this. Without the guard, a single malformed hyperlink crashes the entire document conversion.
Fix:
| address = run.hyperlink.address | |
| try: | |
| address = run.hyperlink.address | |
| except KeyError: | |
| return None |
| _log.warning("Skipping malformed hyperlink address: %r", address) | ||
| return None | ||
|
|
||
| def _iter_paragraph_runs(self, paragraph) -> list[_ParagraphRun]: |
There was a problem hiding this comment.
- Minor performance comment:
_iter_paragraph_runsis called twice per shape: once in the early-return guard (result discarded) and once inside_handle_text_elements. The old_paragraph_fragmentswas cheap; the new method does font inspection and hyperlink resolution per run. - Add type annotation to
paragraph
The PPTX text-extraction loop built each paragraph's text via plain
string concatenation (p_text += e.text) and handed the whole thing to
a single add_text/add_list_item call, discarding every run's own
hyperlink and bold/italic/underline formatting - the same information
DOCX/HTML/ODF/MD backends already preserve per run.
Splits each paragraph into (text, formatting, hyperlink) tuples per
run, mirroring msword_backend.py's existing _iter_paragraph_content/
_get_format_from_run/_get_hyperlink_target pattern:
- Adjacent runs with identical formatting/hyperlink are coalesced back
into one unit first, since PowerPoint sometimes splits a single word
across runs with no actual formatting difference (e.g. spell-check
markup) - without this, wrapping each such run in its own item would
let the markdown serializer's inter-sibling space split real words.
- A paragraph with more than one run is wrapped in an inline group
(mirroring the DOCX backend's own _create_or_reuse_parent), so a
sentence with a bold word or a link in the middle stays one flowing
unit on export instead of splintering into separate blocks.
- Title/section-header paragraphs are kept as a single item as before:
a heading is one outline-level unit, not a sequence of independently
formatted spans.
- Each item's own provenance is now built from its own text rather
than the whole paragraph's, so charspan stays correct once a
paragraph can produce more than one item (python-pptx exposes no
finer bbox than the shape's, so the bbox itself stays shape-level).
New test: test_pptx_preserves_run_level_formatting_and_hyperlinks,
covering a bold run, a plain run, and a hyperlinked run in one
paragraph. Golden fixtures regenerated; diffs are exactly the fixed
behavior (a real markdown hyperlink where the URL was silently
dropped, formatting: {...} now present throughout) with no unrelated
changes.
Native Office Math equations (docling-project#4625) keep their own FORMULA items; the text
runs around them now carry their run-level formatting and hyperlinks too.
Signed-off-by: Avanish Garg <gargavanish@gmail.com>
Signed-off-by: Avanish Garg <gargavanish@gmail.com>
A run hyperlink pointing at a missing, empty or absent relationship id made python-pptx raise KeyError and failed the whole conversion. Treat such a link as absent and keep the run text. Split each paragraph into runs once per shape instead of once for the empty-shape check and again when building items, and annotate the paragraph parameter. Signed-off-by: Avanish Garg <gargavanish@gmail.com>
2513684 to
ed1a75b
Compare
|
@mittalpk please fix the linting |
|
@PeterStaar-IBM I think this one is for @avanish-garg, since #4443 is his branch; my #4787 only references it. The lint failure is the max-lines hook: tests/test_backend_pptx.py is at 1547 lines. Moving the new tests into their own file fixes it, as I did for the same failure in #4733. |
tests/test_backend_pptx.py went over the 1500-line limit enforced by the max-lines hook. Signed-off-by: Avanish Garg <gargavanish@gmail.com>
|
Done with lint changes |
|
Any update on this @ceberam ? |
ceberam
left a comment
There was a problem hiding this comment.
Thanks @avanish-garg ! Approved.
Just a non-blocking message though below if you can check it out.
| # ``(label, text, formatting, hyperlink)``; equations are FORMULA entries with | ||
| # neither formatting nor hyperlink. |
There was a problem hiding this comment.
What are these inline comments referring to? Or they should just be removed?
Issue resolved by this Pull Request:
Resolves #4442
Splits each paragraph into
(text, formatting, hyperlink)tuples per run, mirroringmsword_backend.py's existing_iter_paragraph_content/_get_format_from_run/_get_hyperlink_targetpattern (python-pptx's publicFontAPI only exposes bold/italic/underline,unlike python-docx's richer run API, so strikethrough/sub/superscript aren't in scope here):
since PowerPoint sometimes splits a single word across runs with no actual formatting difference
(e.g. spell-check markup) — without this, wrapping each such run in its own item would let the
markdown serializer's inter-sibling space (
InlineGroupalways joins children with" ") splitreal words apart.
own
_create_or_reuse_parent, so a sentence with a bold word or a link in the middle stays oneflowing unit on export instead of splintering into separate markdown blocks.
outline-level unit, not a sequence of independently formatted spans.
charspanstays correct once a paragraph can produce more than one item (python-pptx exposes nofiner bbox than the shape's, so the bbox itself stays shape-level for every item in it, same as
before).
Changes:
docling/backend/mspowerpoint_backend.pygets three new helpers(
_get_run_format/_get_run_hyperlink_target/_iter_paragraph_runs) plus a_add_list_item_with_formattinghelper for the list case (mirroringmsword_backend.py's_add_formatted_list_item), and_handle_text_elementsis rewired to use them.tests/test_backend_pptx.pygets a new regression test,test_pptx_preserves_run_level_formatting_and_hyperlinks, covering a bold run, a plain run, and ahyperlinked run in one paragraph — verified to fail on unmodified
main(all three collapse intoone plain-text item) and pass with the fix.
Golden fixtures regenerated (
DOCLING_GEN_TEST_DATA=1). Every diff was reviewed by hand and isexactly the fixed behavior: a real markdown hyperlink where the URL used to be silently dropped
(
powerpoint_comments.pptx.md), a real bold marker where formatting used to be silently dropped(
powerpoint_issue_2663.pptx.md,**Key Benefits:**), and an explicit"formatting": {"bold": false, ...}block now present on plain-text items where the field waspreviously just absent — no text content changed anywhere, no unrelated diffs. (One golden JSON's
embedded base64 image bytes also changed on first regeneration; confirmed via a clean regen on
unmodified
mainthat this is a pre-existing Pillow/PNG-encoder version difference in thisenvironment, unrelated to this fix, and reverted just that field back to its original value before
committing.)
Full
tests/test_backend_pptx.pysuite: 30 passed, 2 skipped (pre-existing, LibreOffice-gated), noregressions.
ruff check/ruff format --check: clean.ty check: same 7 pre-existing warnings asunmodified
main(confirmed via diff), no new ones, none on the changed lines.Checklist:
run-formatting handling to update