Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/docx/document.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

from __future__ import annotations

from os import PathLike
from typing import IO, TYPE_CHECKING, Iterator, List, Sequence

from docx.blkcntnr import BlockItemContainer
Expand Down Expand Up @@ -120,7 +121,7 @@ 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 | PathLike[str] | IO[bytes],
width: int | Length | None = None,
height: int | Length | None = None,
):
Expand Down
6 changes: 3 additions & 3 deletions src/docx/image/image.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,11 @@ 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)
Expand Down
5 changes: 3 additions & 2 deletions src/docx/package.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from __future__ import annotations

from os import PathLike
from typing import IO, cast

from docx.image.image import Image
Expand All @@ -22,7 +23,7 @@ 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 | 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
Expand Down Expand Up @@ -65,7 +66,7 @@ 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 | 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
Expand Down
7 changes: 5 additions & 2 deletions src/docx/parts/story.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from __future__ import annotations

from os import PathLike
from typing import IO, TYPE_CHECKING, Tuple, cast

from docx.opc.constants import RELATIONSHIP_TYPE as RT
Expand All @@ -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 | 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
Expand Down Expand Up @@ -59,7 +62,7 @@ def get_style_id(

def new_pic_inline(
self,
image_descriptor: str | IO[bytes],
image_descriptor: str | PathLike[str] | IO[bytes],
width: int | Length | None = None,
height: int | Length | None = None,
) -> CT_Inline:
Expand Down
7 changes: 4 additions & 3 deletions src/docx/text/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from __future__ import annotations

from os import PathLike
from typing import IO, TYPE_CHECKING, Iterator, cast

from docx.drawing import Drawing
Expand Down Expand Up @@ -58,16 +59,16 @@ 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 | PathLike[str] | IO[bytes],
width: int | Length | None = None,
height: int | Length | None = None,
) -> InlineShape:
"""Return |InlineShape| containing image identified by `image_path_or_stream`.

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 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
Expand Down
7 changes: 4 additions & 3 deletions tests/image/test_image.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Unit test suite for docx.image package"""

import io
from pathlib import Path

import pytest

Expand Down Expand Up @@ -150,13 +151,13 @@ def from_filelike_fixture(self, _from_stream_, image_):
image_stream = io.BytesIO(blob)
return image_stream, _from_stream_, blob, image_

@pytest.fixture
def from_path_fixture(self, _from_stream_, BytesIO_, stream_, image_):
@pytest.fixture(params=[str, Path])
def from_path_fixture(self, request, _from_stream_, BytesIO_, stream_, image_):
filename = "python-icon.png"
image_path = test_file(filename)
with open(image_path, "rb") as f:
blob = f.read()
return image_path, _from_stream_, stream_, blob, filename, image_
return request.param(image_path), _from_stream_, stream_, blob, filename, image_

@pytest.fixture(params=["foobar.png", None])
def from_stream_fixture(
Expand Down
24 changes: 24 additions & 0 deletions tests/test_api.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,41 @@
"""Test suite for the docx.api module."""

import io
from pathlib import Path

import pytest

from docx.api import Document as DocumentFactoryFn
from docx.document import Document as DocumentCls
from docx.opc.constants import CONTENT_TYPE as CT

from .unitutil.file import test_file
from .unitutil.mock import FixtureRequest, Mock, class_mock, function_mock, instance_mock


class DescribeDocument:
"""Unit-test suite for `docx.api.Document` factory function."""

@pytest.mark.parametrize("in_run", [False, True])
def it_can_add_a_picture_from_a_path(self, tmp_path: Path, in_run: bool):
image_path = tmp_path / "图片.png"
image_path.write_bytes(Path(test_file("python-icon.png")).read_bytes())
document = DocumentFactoryFn()
target = document.add_paragraph().add_run() if in_run else document

target.add_picture(image_path)

assert len(document.inline_shapes) == 1
stream = io.BytesIO()
document.save(stream)
stream.seek(0)
reloaded = DocumentFactoryFn(stream)
assert len(reloaded.inline_shapes) == 1
package = reloaded.part.package
assert package is not None
(image_part,) = package.image_parts
assert image_part.blob == image_path.read_bytes()

def it_opens_a_docx_file(self, Package_: Mock, document_: Mock):
document_part = Package_.open.return_value.main_document_part
document_part.document = document_
Expand Down