From 794474e425f450b90ee8014c92b63e63e3d8c1f2 Mon Sep 17 00:00:00 2001 From: Ewout ter Hoeven <15776622+EwoutH@users.noreply.github.com> Date: Mon, 10 Nov 2025 13:39:51 +0100 Subject: [PATCH 1/3] oxml: add optional `title` and `descr` attributes to CT_NonVisualDrawingProps Expose the `` attributes used for image title and alt text in Word. These correspond to the "Alt Text (Title)" and "Alt Text (Description)" fields shown in the Word UI. Based on the approach from PR #227 ("Add support for image title and alt text"), limited here to adding OptionalAttribute definitions in the OXML layer. --- src/docx/oxml/shape.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/docx/oxml/shape.py b/src/docx/oxml/shape.py index c6df8e7b8..6754694bd 100644 --- a/src/docx/oxml/shape.py +++ b/src/docx/oxml/shape.py @@ -77,7 +77,7 @@ class CT_Inline(BaseOxmlElement): ) @classmethod - def new(cls, cx: Length, cy: Length, shape_id: int, pic: CT_Picture) -> CT_Inline: + def new(cls, cx: Length, cy: Length, shape_id: int, pic: CT_Picture, title=None, descr=None) -> CT_Inline: """Return a new ```` element populated with the values passed as parameters.""" inline = cast(CT_Inline, parse_xml(cls._inline_xml())) @@ -85,13 +85,15 @@ def new(cls, cx: Length, cy: Length, shape_id: int, pic: CT_Picture) -> CT_Inlin inline.extent.cy = cy inline.docPr.id = shape_id inline.docPr.name = "Picture %d" % shape_id + if title: inline.docPr.title = title + if descr: inline.docPr.descr = descr inline.graphic.graphicData.uri = "http://schemas.openxmlformats.org/drawingml/2006/picture" inline.graphic.graphicData._insert_pic(pic) return inline @classmethod def new_pic_inline( - cls, shape_id: int, rId: str, filename: str, cx: Length, cy: Length + cls, shape_id: int, rId: str, filename: str, cx: Length, cy: Length, title=None, descr=None ) -> CT_Inline: """Create `wp:inline` element containing a `pic:pic` element. @@ -99,7 +101,7 @@ def new_pic_inline( """ pic_id = 0 # Word doesn't seem to use this, but does not omit it pic = CT_Picture.new(pic_id, filename, rId, cx, cy) - inline = cls.new(cx, cy, shape_id, pic) + inline = cls.new(cx, cy, shape_id, pic, title=title, descr=descr) return inline @classmethod @@ -126,6 +128,8 @@ class CT_NonVisualDrawingProps(BaseOxmlElement): id = RequiredAttribute("id", ST_DrawingElementId) name = RequiredAttribute("name", XsdString) + title = OptionalAttribute('title', XsdString) + descr = OptionalAttribute('descr', XsdString) class CT_NonVisualPictureProperties(BaseOxmlElement): From d46a19814fe103d7635870e510dedc394b8ff9ba Mon Sep 17 00:00:00 2001 From: Ewout ter Hoeven <15776622+EwoutH@users.noreply.github.com> Date: Mon, 10 Nov 2025 13:50:33 +0100 Subject: [PATCH 2/3] feat: add alt-text support for images in StoryPart and Run - Extend StoryPart.new_pic_inline() with optional `title` and `descr` parameters. - Pass these through to CT_Inline.new_pic_inline() for setting attributes. - Update Run.add_picture() to accept and forward `title` and `descr` to StoryPart. Part of the effort to enable setting image alternative text (accessibility title/description) when adding pictures. --- src/docx/document.py | 7 ++++++- src/docx/parts/story.py | 4 +++- src/docx/text/run.py | 7 ++++++- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/docx/document.py b/src/docx/document.py index 73757b46d..3c94de4ee 100644 --- a/src/docx/document.py +++ b/src/docx/document.py @@ -123,6 +123,8 @@ def add_picture( image_path_or_stream: str | IO[bytes], width: int | Length | None = None, height: int | Length | None = None, + title: str | None = None, + descr: str | None = None, ): """Return new picture shape added in its own paragraph at end of the document. @@ -133,9 +135,12 @@ def add_picture( aspect ratio of the image. The native size of the picture is calculated using the dots-per-inch (dpi) value specified in the image file, defaulting to 72 dpi if no value is specified, as is often the case. + + `title` sets the image's alternative-text title. + `descr` sets the alternative-text description shown in Word's Alt Text pane. """ run = self.add_paragraph().add_run() - return run.add_picture(image_path_or_stream, width, height) + return run.add_picture(image_path_or_stream, width, height, title, descr) def add_section(self, start_type: WD_SECTION = WD_SECTION.NEW_PAGE): """Return a |Section| object newly added at the end of the document. diff --git a/src/docx/parts/story.py b/src/docx/parts/story.py index 7482c91a8..186529126 100644 --- a/src/docx/parts/story.py +++ b/src/docx/parts/story.py @@ -62,6 +62,8 @@ def new_pic_inline( image_descriptor: str | IO[bytes], width: int | Length | None = None, height: int | Length | None = None, + title: str | None = None, + descr: str | None = None, ) -> CT_Inline: """Return a newly-created `w:inline` element. @@ -71,7 +73,7 @@ def new_pic_inline( rId, image = self.get_or_add_image(image_descriptor) cx, cy = image.scaled_dimensions(width, height) shape_id, filename = self.next_id, image.filename - return CT_Inline.new_pic_inline(shape_id, rId, filename, cx, cy) + return CT_Inline.new_pic_inline(shape_id, rId, filename, cx, cy, title, descr) @property def next_id(self) -> int: diff --git a/src/docx/text/run.py b/src/docx/text/run.py index 57ea31fa4..fc699deb4 100644 --- a/src/docx/text/run.py +++ b/src/docx/text/run.py @@ -61,6 +61,8 @@ def add_picture( image_path_or_stream: str | IO[bytes], width: int | Length | None = None, height: int | Length | None = None, + title: str | None = None, + descr: str | None = None, ) -> InlineShape: """Return |InlineShape| containing image identified by `image_path_or_stream`. @@ -75,8 +77,11 @@ def add_picture( ratio of the image. The native size of the picture is calculated using the dots- per-inch (dpi) value specified in the image file, defaulting to 72 dpi if no value is specified, as is often the case. + + `title` sets the image's alternative-text title. `descr` sets the alternative-text + description shown in Word's Alt Text pane. """ - inline = self.part.new_pic_inline(image_path_or_stream, width, height) + inline = self.part.new_pic_inline(image_path_or_stream, width, height, title, descr) self._r.add_drawing(inline) return InlineShape(inline) From 74bebf65bb0549df9388dc84a5ff622133875861 Mon Sep 17 00:00:00 2001 From: Ewout ter Hoeven <15776622+EwoutH@users.noreply.github.com> Date: Mon, 10 Nov 2025 13:56:44 +0100 Subject: [PATCH 3/3] feat(shape): add alt_text and alt_title properties to InlineShape Expose read/write access to image alternative text stored in via new `InlineShape.alt_text` (description) and `InlineShape.alt_title` (title) properties. --- src/docx/shape.py | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/src/docx/shape.py b/src/docx/shape.py index cd35deb35..6435e142f 100644 --- a/src/docx/shape.py +++ b/src/docx/shape.py @@ -101,3 +101,42 @@ def width(self): def width(self, cx: Length): self._inline.extent.cx = cx self._inline.graphic.graphicData.pic.spPr.cx = cx + + @property + def alt_text(self) -> str | None: + """Read/write alternative-text description for this inline shape. + + This corresponds to the 'Description' field in Word's Alt Text pane and is + stored on the ``wp:docPr`` ``descr`` attribute. + """ + docPr = self._inline.docPr + if docPr is None: + return None + return docPr.descr + + @alt_text.setter + def alt_text(self, value: str | None) -> None: + docPr = self._inline.docPr + if docPr is None: + return + # Setting to empty/None clears the attribute. + docPr.descr = value if value else None + + @property + def alt_title(self) -> str | None: + """Read/write alternative-text title for this inline shape. + + This corresponds to the 'Title' field in Word's Alt Text pane and is + stored on the ``wp:docPr`` ``title`` attribute. + """ + docPr = self._inline.docPr + if docPr is None: + return None + return docPr.title + + @alt_title.setter + def alt_title(self, value: str | None) -> None: + docPr = self._inline.docPr + if docPr is None: + return + docPr.title = value if value else None