Skip to content

Commit 2b25809

Browse files
committed
Fix #3515 by preserving images quality by default
1 parent 85b53d8 commit 2b25809

File tree

4 files changed

+24
-7
lines changed

4 files changed

+24
-7
lines changed

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,4 @@ mypy:
2626
mypy pypdf --ignore-missing-imports --check-untyped --strict
2727

2828
ruff:
29-
ruff check pypdf tests make_release.py
29+
ruff check --fix pypdf tests make_release.py

pypdf/_page.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -387,6 +387,8 @@ def replace(self, new_image: Image, **kwargs: Any) -> None:
387387
if not isinstance(new_image, Image):
388388
raise TypeError("new_image shall be a PIL Image")
389389
b = BytesIO()
390+
if "quality" not in kwargs:
391+
kwargs["quality"] = "keep"
390392
new_image.save(b, "PDF", **kwargs)
391393
reader = PdfReader(b)
392394
assert reader.pages[0].images[0].indirect_reference is not None
@@ -398,7 +400,8 @@ def replace(self, new_image: Image, **kwargs: Any) -> None:
398400
).indirect_reference = self.indirect_reference
399401
# change the object attributes
400402
extension, byte_stream, img = _xobj_to_image(
401-
cast(DictionaryObject, self.indirect_reference.get_object())
403+
cast(DictionaryObject, self.indirect_reference.get_object()),
404+
pil_params=kwargs,
402405
)
403406
assert extension is not None
404407
self.name = self.name[: self.name.rfind(".")] + extension

pypdf/filters.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -791,15 +791,20 @@ def decode_stream_data(stream: Any) -> bytes:
791791
return data
792792

793793

794-
def _xobj_to_image(x_object: dict[str, Any]) -> tuple[Optional[str], bytes, Any]:
794+
def _xobj_to_image(
795+
x_object: dict[str, Any],
796+
pil_params: Union[dict[str, Any], None] = None
797+
) -> tuple[Optional[str], bytes, Any]:
795798
"""
796799
Users need to have the pillow package installed.
797800
798801
It's unclear if pypdf will keep this function here, hence it's private.
799802
It might get removed at any point.
800803
801804
Args:
802-
x_object:
805+
x_object:
806+
pil_params: parameters provided to Pillow Image.save() method,
807+
cf. <https://pillow.readthedocs.io/en/stable/reference/Image.html#PIL.Image.Image.save>
803808
804809
Returns:
805810
Tuple[file extension, bytes, PIL.Image.Image]
@@ -846,6 +851,9 @@ def _apply_alpha(
846851
extension = ".png"
847852
return img, extension, image_format
848853

854+
if pil_params is None:
855+
pil_params = {}
856+
849857
# For error reporting
850858
obj_as_text = (
851859
x_object.indirect_reference.__repr__()
@@ -905,6 +913,8 @@ def _apply_alpha(
905913
except UnidentifiedImageError:
906914
img = _extended_image_frombytes(mode, size, data)
907915
elif lfilters == FT.DCT_DECODE:
916+
if "quality" not in pil_params:
917+
pil_params["quality"] = "keep"
908918
img, image_format, extension = Image.open(BytesIO(data)), "JPEG", ".jpg"
909919
# invert_color kept unchanged
910920
elif lfilters == FT.JPX_DECODE:
@@ -950,7 +960,7 @@ def _apply_alpha(
950960
# Save image to bytes
951961
img_byte_arr = BytesIO()
952962
try:
953-
img.save(img_byte_arr, format=image_format)
963+
img.save(img_byte_arr, format=image_format, **pil_params)
954964
except OSError: # pragma: no cover # covered with pillow 10.3
955965
# in case of we convert to RGBA and then to PNG
956966
img1 = img.convert("RGBA")

pypdf/generic/_data_structures.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1046,10 +1046,14 @@ def flate_encode(self, level: int = -1) -> "EncodedStreamObject":
10461046
retval._data = FlateDecode.encode(self._data, level)
10471047
return retval
10481048

1049-
def decode_as_image(self) -> Any:
1049+
def decode_as_image(self, pil_params: Union[dict[str, Any], None] = None) -> Any:
10501050
"""
10511051
Try to decode the stream object as an image
10521052
1053+
Args:
1054+
pil_params: parameters provided to Pillow Image.save() method,
1055+
cf. <https://pillow.readthedocs.io/en/stable/reference/Image.html#PIL.Image.Image.save>
1056+
10531057
Returns:
10541058
a PIL image if proper decoding has been found
10551059
Raises:
@@ -1066,7 +1070,7 @@ def decode_as_image(self) -> Any:
10661070
except AttributeError:
10671071
msg = f"{self.__repr__()} object does not seem to be an Image" # pragma: no cover
10681072
logger_warning(msg, __name__)
1069-
extension, _, img = _xobj_to_image(self)
1073+
extension, _, img = _xobj_to_image(self, pil_params)
10701074
if extension is None:
10711075
return None # pragma: no cover
10721076
return img

0 commit comments

Comments
 (0)