Skip to content

PPTX: a hyperlink on a whole shape (p:cNvPr/a:hlinkClick) is dropped #4782

Closed

Description

@Steve-Allison

Bug

A hyperlink attached to a whole shape, written as a:hlinkClick under the shape's p:cNvPr (python-pptx: shape.click_action.hyperlink.address), does not reach the document: the shape's text item has hyperlink=None.

#4442 covers the run-level form (a:rPr/a:hlinkClick) and PR #4443 fixes that form only; this is the other place PowerPoint stores a link, and it is lost on 2.138.0 and on main.

Steps to reproduce

"""Reproduce: a PPTX hyperlink on a whole shape (p:cNvPr/a:hlinkClick) is lost; so is one on text runs (#4442)."""

import sys
from io import BytesIO

import docling
import pptx
from docling.datamodel.base_models import InputFormat
from docling.document_converter import DocumentConverter
from docling_core.types.doc import ContentLayer, TextItem
from docling_core.types.io import DocumentStream
from lxml import etree
from pptx import Presentation
from pptx.util import Inches


def build() -> BytesIO:
    prs = Presentation()
    slide = prs.slides.add_slide(prs.slide_layouts[6])
    paragraph = slide.shapes.add_textbox(Inches(1), Inches(1), Inches(6), Inches(1)).text_frame.paragraphs[0]
    paragraph.add_run().text = "Read "
    for word in ("the", " story"):
        run = paragraph.add_run()
        run.text = word
        run.hyperlink.address = "https://example.com/story"
    paragraph.add_run().text = " tonight."
    button = slide.shapes.add_textbox(Inches(1), Inches(3), Inches(4), Inches(1))
    button.text_frame.text = "Watch the talk"
    button.click_action.hyperlink.address = "https://example.com/talk"
    print(etree.tostring(button._element.nvSpPr.cNvPr, pretty_print=True).decode().strip())
    buf = BytesIO()
    prs.save(buf)
    buf.seek(0)
    return buf


result = DocumentConverter(allowed_formats=[InputFormat.PPTX]).convert(
    DocumentStream(name="repro.pptx", stream=build())
)
print(f"docling {docling.__version__}, python-pptx {pptx.__version__}, python {sys.version.split()[0]}")
for item, _ in result.document.iterate_items(included_content_layers=set(ContentLayer)):
    if isinstance(item, TextItem):
        print(f"{item.label.value}: {item.text!r} hyperlink={item.hyperlink!r}")

Output (the cNvPr print shows the link is in the file):

<p:cNvPr ... id="3" name="TextBox 2">
  <a:hlinkClick r:id="rId3"/>
</p:cNvPr>
docling 2.138.0, python-pptx 1.0.2, python 3.14.8
paragraph: 'Read the story tonight.' hyperlink=None
paragraph: 'Watch the talk' hyperlink=None

(The first paragraph is the run-level loss of #4442; the second is this issue.)

Expected

The shape's text item carries the shape's link as its hyperlink, the way the DOCX backend carries a hyperlink run. Where a run inside the shape has its own link, the run's link wins for that run.

Docling version

2.138.0

Python version

3.14.8

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions