diff --git a/src/docx/document.py b/src/docx/document.py index 73757b46d..8cafecd3e 100644 --- a/src/docx/document.py +++ b/src/docx/document.py @@ -5,6 +5,7 @@ from __future__ import annotations +import os from typing import IO, TYPE_CHECKING, Iterator, List, Sequence from docx.blkcntnr import BlockItemContainer @@ -120,19 +121,20 @@ def add_paragraph(self, text: str = "", style: str | ParagraphStyle | None = Non def add_picture( self, - image_path_or_stream: str | IO[bytes], + image_path_or_stream: str | os.PathLike[str] | IO[bytes], width: int | Length | None = None, height: int | Length | None = None, ): """Return new picture shape added in its own paragraph at end of the document. - The picture contains the image at `image_path_or_stream`, scaled based on - `width` and `height`. If neither width nor height is specified, the picture - appears at its native size. If only one is specified, it is used to compute a - scaling factor that is then applied to the unspecified dimension, preserving the - 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. + The picture contains the image at `image_path_or_stream`, which is a path (a + string or path-like object) or a file-like object containing a binary image. The + picture is scaled based on `width` and `height`. If neither width nor height is + specified, the picture appears at its native size. If only one is specified, it + is used to compute a scaling factor that is then applied to the unspecified + dimension, preserving the 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. """ run = self.add_paragraph().add_run() return run.add_picture(image_path_or_stream, width, height) diff --git a/src/docx/image/image.py b/src/docx/image/image.py index e5e7f8a13..26e93284c 100644 --- a/src/docx/image/image.py +++ b/src/docx/image/image.py @@ -33,15 +33,15 @@ def from_blob(cls, blob: bytes) -> Image: return cls._from_stream(stream, blob) @classmethod - def from_file(cls, image_descriptor: str | IO[bytes]): + def from_file(cls, image_descriptor: str | os.PathLike[str] | IO[bytes]): """Return a new |Image| subclass instance loaded from the image file identified by `image_descriptor`, a path or file-like object.""" - if isinstance(image_descriptor, str): - path = image_descriptor + if isinstance(image_descriptor, (str, os.PathLike)): + path = os.fspath(image_descriptor) with open(path, "rb") as f: blob = f.read() stream = io.BytesIO(blob) - filename = os.path.basename(path) + filename = os.path.basename(os.fsdecode(path)) else: stream = image_descriptor stream.seek(0) diff --git a/src/docx/package.py b/src/docx/package.py index 7ea47e6e1..ef233ea1a 100644 --- a/src/docx/package.py +++ b/src/docx/package.py @@ -2,6 +2,7 @@ from __future__ import annotations +import os from typing import IO, cast from docx.image.image import Image @@ -22,7 +23,9 @@ def after_unmarshal(self): """ self._gather_image_parts() - def get_or_add_image_part(self, image_descriptor: str | IO[bytes]) -> ImagePart: + def get_or_add_image_part( + self, image_descriptor: str | os.PathLike[str] | IO[bytes] + ) -> ImagePart: """Return |ImagePart| containing image specified by `image_descriptor`. The image-part is newly created if a matching one is not already present in the @@ -65,7 +68,9 @@ def __len__(self): def append(self, item: ImagePart): self._image_parts.append(item) - def get_or_add_image_part(self, image_descriptor: str | IO[bytes]) -> ImagePart: + def get_or_add_image_part( + self, image_descriptor: str | os.PathLike[str] | IO[bytes] + ) -> ImagePart: """Return |ImagePart| object containing image identified by `image_descriptor`. The image-part is newly created if a matching one is not present in the diff --git a/src/docx/parts/story.py b/src/docx/parts/story.py index 7482c91a8..68440809a 100644 --- a/src/docx/parts/story.py +++ b/src/docx/parts/story.py @@ -2,6 +2,7 @@ from __future__ import annotations +import os from typing import IO, TYPE_CHECKING, Tuple, cast from docx.opc.constants import RELATIONSHIP_TYPE as RT @@ -24,7 +25,9 @@ class StoryPart(XmlPart): `.add_paragraph()`, `.add_table()` etc. """ - def get_or_add_image(self, image_descriptor: str | IO[bytes]) -> Tuple[str, Image]: + def get_or_add_image( + self, image_descriptor: str | os.PathLike[str] | IO[bytes] + ) -> Tuple[str, Image]: """Return (rId, image) pair for image identified by `image_descriptor`. `rId` is the str key (often like "rId7") for the relationship between this story @@ -59,7 +62,7 @@ def get_style_id( def new_pic_inline( self, - image_descriptor: str | IO[bytes], + image_descriptor: str | os.PathLike[str] | IO[bytes], width: int | Length | None = None, height: int | Length | None = None, ) -> CT_Inline: diff --git a/src/docx/text/run.py b/src/docx/text/run.py index 57ea31fa4..17a7fca59 100644 --- a/src/docx/text/run.py +++ b/src/docx/text/run.py @@ -2,6 +2,7 @@ from __future__ import annotations +import os from typing import IO, TYPE_CHECKING, Iterator, cast from docx.drawing import Drawing @@ -58,7 +59,7 @@ def add_break(self, break_type: WD_BREAK = WD_BREAK.LINE): def add_picture( self, - image_path_or_stream: str | IO[bytes], + image_path_or_stream: str | os.PathLike[str] | IO[bytes], width: int | Length | None = None, height: int | Length | None = None, ) -> InlineShape: @@ -66,8 +67,8 @@ def add_picture( The picture is added to the end of this run. - `image_path_or_stream` can be a path (a string) or a file-like object containing - a binary image. + `image_path_or_stream` can be a path (a string or path-like object) or a + file-like object containing a binary image. If neither width nor height is specified, the picture appears at its native size. If only one is specified, it is used to compute a scaling diff --git a/tests/image/test_image.py b/tests/image/test_image.py index c13e87305..e4eaed8e9 100644 --- a/tests/image/test_image.py +++ b/tests/image/test_image.py @@ -1,6 +1,7 @@ """Unit test suite for docx.image package""" import io +from pathlib import Path import pytest @@ -40,6 +41,12 @@ def it_can_construct_from_an_image_path(self, from_path_fixture): _from_stream_.assert_called_once_with(stream_, blob, filename) assert image is image_ + def it_can_construct_from_a_pathlib_path(self, from_path_fixture): + image_path, _from_stream_, stream_, blob, filename, image_ = from_path_fixture + image = Image.from_file(Path(image_path)) + _from_stream_.assert_called_once_with(stream_, blob, filename) + assert image is image_ + def it_can_construct_from_an_image_file_like(self, from_filelike_fixture): image_stream, _from_stream_, blob, image_ = from_filelike_fixture image = Image.from_file(image_stream)