Skip to content

Commit f0f0429

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

File tree

4 files changed

+21
-7
lines changed

4 files changed

+21
-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+
quality=kwargs["quality"],
402405
)
403406
assert extension is not None
404407
self.name = self.name[: self.name.rfind(".")] + extension

pypdf/filters.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -791,15 +791,17 @@ 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(x_object: dict[str, Any], pil_params: Union[dict, None] = None) -> tuple[Optional[str], bytes, Any]:
795795
"""
796796
Users need to have the pillow package installed.
797797
798798
It's unclear if pypdf will keep this function here, hence it's private.
799799
It might get removed at any point.
800800
801801
Args:
802-
x_object:
802+
x_object:
803+
pil_params: parameters provided to Pillow Image.save() method,
804+
cf. <https://pillow.readthedocs.io/en/stable/reference/Image.html#PIL.Image.Image.save>
803805
804806
Returns:
805807
Tuple[file extension, bytes, PIL.Image.Image]
@@ -846,6 +848,9 @@ def _apply_alpha(
846848
extension = ".png"
847849
return img, extension, image_format
848850

851+
if pil_params is None:
852+
pil_params = {}
853+
849854
# For error reporting
850855
obj_as_text = (
851856
x_object.indirect_reference.__repr__()
@@ -905,6 +910,8 @@ def _apply_alpha(
905910
except UnidentifiedImageError:
906911
img = _extended_image_frombytes(mode, size, data)
907912
elif lfilters == FT.DCT_DECODE:
913+
if "quality" not in pil_params:
914+
pil_params["quality"] = "keep"
908915
img, image_format, extension = Image.open(BytesIO(data)), "JPEG", ".jpg"
909916
# invert_color kept unchanged
910917
elif lfilters == FT.JPX_DECODE:
@@ -950,7 +957,7 @@ def _apply_alpha(
950957
# Save image to bytes
951958
img_byte_arr = BytesIO()
952959
try:
953-
img.save(img_byte_arr, format=image_format)
960+
img.save(img_byte_arr, format=image_format, **pil_params)
954961
except OSError: # pragma: no cover # covered with pillow 10.3
955962
# in case of we convert to RGBA and then to PNG
956963
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, 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)