mirror of
https://github.com/python-pillow/Pillow.git
synced 2025-08-11 07:44:46 +03:00
Merge branch 'main' into image_mode_size_delegation
This commit is contained in:
commit
06aa04aaf5
|
@ -1,6 +1,6 @@
|
||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/psf/black
|
- repo: https://github.com/psf/black
|
||||||
rev: 23.3.0
|
rev: 23.7.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: black
|
- id: black
|
||||||
args: [--target-version=py38]
|
args: [--target-version=py38]
|
||||||
|
@ -23,13 +23,13 @@ repos:
|
||||||
- id: yesqa
|
- id: yesqa
|
||||||
|
|
||||||
- repo: https://github.com/Lucas-C/pre-commit-hooks
|
- repo: https://github.com/Lucas-C/pre-commit-hooks
|
||||||
rev: v1.5.1
|
rev: v1.5.3
|
||||||
hooks:
|
hooks:
|
||||||
- id: remove-tabs
|
- id: remove-tabs
|
||||||
exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$)
|
exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$)
|
||||||
|
|
||||||
- repo: https://github.com/PyCQA/flake8
|
- repo: https://github.com/PyCQA/flake8
|
||||||
rev: 6.0.0
|
rev: 6.1.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: flake8
|
- id: flake8
|
||||||
additional_dependencies:
|
additional_dependencies:
|
||||||
|
@ -55,7 +55,7 @@ repos:
|
||||||
- id: sphinx-lint
|
- id: sphinx-lint
|
||||||
|
|
||||||
- repo: https://github.com/tox-dev/pyproject-fmt
|
- repo: https://github.com/tox-dev/pyproject-fmt
|
||||||
rev: 0.12.1
|
rev: 0.13.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: pyproject-fmt
|
- id: pyproject-fmt
|
||||||
|
|
||||||
|
|
12
CHANGES.rst
12
CHANGES.rst
|
@ -5,6 +5,18 @@ Changelog (Pillow)
|
||||||
10.1.0 (unreleased)
|
10.1.0 (unreleased)
|
||||||
-------------------
|
-------------------
|
||||||
|
|
||||||
|
- Allow "loop=None" when saving GIF images #7329
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Fixed transparency when saving P mode images to PDF #7323
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Added saving LA images as PDFs #7299
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Set SMaskInData to 1 for PDFs with alpha #7316, #7317
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
- Changed Image mode property to be read-only by default #7307
|
- Changed Image mode property to be read-only by default #7307
|
||||||
[radarhere]
|
[radarhere]
|
||||||
|
|
||||||
|
|
|
@ -875,6 +875,14 @@ def test_identical_frames_to_single_frame(duration, tmp_path):
|
||||||
assert reread.info["duration"] == 8500
|
assert reread.info["duration"] == 8500
|
||||||
|
|
||||||
|
|
||||||
|
def test_loop_none(tmp_path):
|
||||||
|
out = str(tmp_path / "temp.gif")
|
||||||
|
im = Image.new("L", (100, 100), "#000")
|
||||||
|
im.save(out, loop=None)
|
||||||
|
with Image.open(out) as reread:
|
||||||
|
assert "loop" not in reread.info
|
||||||
|
|
||||||
|
|
||||||
def test_number_of_loops(tmp_path):
|
def test_number_of_loops(tmp_path):
|
||||||
number_of_loops = 2
|
number_of_loops = 2
|
||||||
|
|
||||||
|
|
|
@ -48,6 +48,22 @@ def test_save_alpha(tmp_path, mode):
|
||||||
helper_save_as_pdf(tmp_path, mode)
|
helper_save_as_pdf(tmp_path, mode)
|
||||||
|
|
||||||
|
|
||||||
|
def test_p_alpha(tmp_path):
|
||||||
|
# Arrange
|
||||||
|
outfile = str(tmp_path / "temp.pdf")
|
||||||
|
with Image.open("Tests/images/pil123p.png") as im:
|
||||||
|
assert im.mode == "P"
|
||||||
|
assert isinstance(im.info["transparency"], bytes)
|
||||||
|
|
||||||
|
# Act
|
||||||
|
im.save(outfile)
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
with open(outfile, "rb") as fp:
|
||||||
|
contents = fp.read()
|
||||||
|
assert b"\n/SMask " in contents
|
||||||
|
|
||||||
|
|
||||||
def test_monochrome(tmp_path):
|
def test_monochrome(tmp_path):
|
||||||
# Arrange
|
# Arrange
|
||||||
mode = "1"
|
mode = "1"
|
||||||
|
|
|
@ -661,15 +661,15 @@ class TestImage:
|
||||||
blank_p.palette = None
|
blank_p.palette = None
|
||||||
blank_pa.palette = None
|
blank_pa.palette = None
|
||||||
|
|
||||||
def _make_new(base_image, im, palette_result=None):
|
def _make_new(base_image, image, palette_result=None):
|
||||||
new_im = base_image._new(im.im)
|
new_image = base_image._new(image.im)
|
||||||
assert new_im.mode == im.mode
|
assert new_image.mode == image.mode
|
||||||
assert new_im.size == im.size
|
assert new_image.size == image.size
|
||||||
assert new_im.info == base_image.info
|
assert new_image.info == base_image.info
|
||||||
if palette_result is not None:
|
if palette_result is not None:
|
||||||
assert new_im.palette.tobytes() == palette_result.tobytes()
|
assert new_image.palette.tobytes() == palette_result.tobytes()
|
||||||
else:
|
else:
|
||||||
assert new_im.palette is None
|
assert new_image.palette is None
|
||||||
|
|
||||||
_make_new(im, im_p, ImagePalette.ImagePalette(list(range(256)) * 3))
|
_make_new(im, im_p, ImagePalette.ImagePalette(list(range(256)) * 3))
|
||||||
_make_new(im_p, im, None)
|
_make_new(im_p, im, None)
|
||||||
|
|
|
@ -253,7 +253,7 @@ their :py:attr:`~PIL.Image.Image.info` values.
|
||||||
|
|
||||||
**loop**
|
**loop**
|
||||||
Integer number of times the GIF should loop. 0 means that it will loop
|
Integer number of times the GIF should loop. 0 means that it will loop
|
||||||
forever. By default, the image will not loop.
|
forever. If omitted or ``None``, the image will not loop.
|
||||||
|
|
||||||
**comment**
|
**comment**
|
||||||
A comment about the image.
|
A comment about the image.
|
||||||
|
|
|
@ -912,7 +912,7 @@ def _get_global_header(im, info):
|
||||||
info
|
info
|
||||||
and (
|
and (
|
||||||
"transparency" in info
|
"transparency" in info
|
||||||
or "loop" in info
|
or info.get("loop") is not None
|
||||||
or info.get("duration")
|
or info.get("duration")
|
||||||
or info.get("comment")
|
or info.get("comment")
|
||||||
)
|
)
|
||||||
|
@ -937,7 +937,7 @@ def _get_global_header(im, info):
|
||||||
# Global Color Table
|
# Global Color Table
|
||||||
_get_header_palette(palette_bytes),
|
_get_header_palette(palette_bytes),
|
||||||
]
|
]
|
||||||
if "loop" in info:
|
if info.get("loop") is not None:
|
||||||
header.append(
|
header.append(
|
||||||
b"!"
|
b"!"
|
||||||
+ o8(255) # extension intro
|
+ o8(255) # extension intro
|
||||||
|
|
|
@ -247,7 +247,7 @@ def eval(expression, _dict={}, **kw):
|
||||||
|
|
||||||
def scan(code):
|
def scan(code):
|
||||||
for const in code.co_consts:
|
for const in code.co_consts:
|
||||||
if type(const) == type(compiled_code):
|
if type(const) is type(compiled_code):
|
||||||
scan(const)
|
scan(const)
|
||||||
|
|
||||||
for name in code.co_names:
|
for name in code.co_names:
|
||||||
|
|
|
@ -46,6 +46,132 @@ def _save_all(im, fp, filename):
|
||||||
# (Internal) Image save plugin for the PDF format.
|
# (Internal) Image save plugin for the PDF format.
|
||||||
|
|
||||||
|
|
||||||
|
def _write_image(im, filename, existing_pdf, image_refs):
|
||||||
|
# FIXME: Should replace ASCIIHexDecode with RunLengthDecode
|
||||||
|
# (packbits) or LZWDecode (tiff/lzw compression). Note that
|
||||||
|
# PDF 1.2 also supports Flatedecode (zip compression).
|
||||||
|
|
||||||
|
params = None
|
||||||
|
decode = None
|
||||||
|
|
||||||
|
#
|
||||||
|
# Get image characteristics
|
||||||
|
|
||||||
|
width, height = im.size
|
||||||
|
|
||||||
|
dict_obj = {"BitsPerComponent": 8}
|
||||||
|
if im.mode == "1":
|
||||||
|
if features.check("libtiff"):
|
||||||
|
filter = "CCITTFaxDecode"
|
||||||
|
dict_obj["BitsPerComponent"] = 1
|
||||||
|
params = PdfParser.PdfArray(
|
||||||
|
[
|
||||||
|
PdfParser.PdfDict(
|
||||||
|
{
|
||||||
|
"K": -1,
|
||||||
|
"BlackIs1": True,
|
||||||
|
"Columns": width,
|
||||||
|
"Rows": height,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
filter = "DCTDecode"
|
||||||
|
dict_obj["ColorSpace"] = PdfParser.PdfName("DeviceGray")
|
||||||
|
procset = "ImageB" # grayscale
|
||||||
|
elif im.mode == "L":
|
||||||
|
filter = "DCTDecode"
|
||||||
|
# params = f"<< /Predictor 15 /Columns {width-2} >>"
|
||||||
|
dict_obj["ColorSpace"] = PdfParser.PdfName("DeviceGray")
|
||||||
|
procset = "ImageB" # grayscale
|
||||||
|
elif im.mode == "LA":
|
||||||
|
filter = "JPXDecode"
|
||||||
|
# params = f"<< /Predictor 15 /Columns {width-2} >>"
|
||||||
|
procset = "ImageB" # grayscale
|
||||||
|
dict_obj["SMaskInData"] = 1
|
||||||
|
elif im.mode == "P":
|
||||||
|
filter = "ASCIIHexDecode"
|
||||||
|
palette = im.getpalette()
|
||||||
|
dict_obj["ColorSpace"] = [
|
||||||
|
PdfParser.PdfName("Indexed"),
|
||||||
|
PdfParser.PdfName("DeviceRGB"),
|
||||||
|
255,
|
||||||
|
PdfParser.PdfBinary(palette),
|
||||||
|
]
|
||||||
|
procset = "ImageI" # indexed color
|
||||||
|
|
||||||
|
if "transparency" in im.info:
|
||||||
|
smask = im.convert("LA").getchannel("A")
|
||||||
|
smask.encoderinfo = {}
|
||||||
|
|
||||||
|
image_ref = _write_image(smask, filename, existing_pdf, image_refs)[0]
|
||||||
|
dict_obj["SMask"] = image_ref
|
||||||
|
elif im.mode == "RGB":
|
||||||
|
filter = "DCTDecode"
|
||||||
|
dict_obj["ColorSpace"] = PdfParser.PdfName("DeviceRGB")
|
||||||
|
procset = "ImageC" # color images
|
||||||
|
elif im.mode == "RGBA":
|
||||||
|
filter = "JPXDecode"
|
||||||
|
procset = "ImageC" # color images
|
||||||
|
dict_obj["SMaskInData"] = 1
|
||||||
|
elif im.mode == "CMYK":
|
||||||
|
filter = "DCTDecode"
|
||||||
|
dict_obj["ColorSpace"] = PdfParser.PdfName("DeviceCMYK")
|
||||||
|
procset = "ImageC" # color images
|
||||||
|
decode = [1, 0, 1, 0, 1, 0, 1, 0]
|
||||||
|
else:
|
||||||
|
msg = f"cannot save mode {im.mode}"
|
||||||
|
raise ValueError(msg)
|
||||||
|
|
||||||
|
#
|
||||||
|
# image
|
||||||
|
|
||||||
|
op = io.BytesIO()
|
||||||
|
|
||||||
|
if filter == "ASCIIHexDecode":
|
||||||
|
ImageFile._save(im, op, [("hex", (0, 0) + im.size, 0, im.mode)])
|
||||||
|
elif filter == "CCITTFaxDecode":
|
||||||
|
im.save(
|
||||||
|
op,
|
||||||
|
"TIFF",
|
||||||
|
compression="group4",
|
||||||
|
# use a single strip
|
||||||
|
strip_size=math.ceil(width / 8) * height,
|
||||||
|
)
|
||||||
|
elif filter == "DCTDecode":
|
||||||
|
Image.SAVE["JPEG"](im, op, filename)
|
||||||
|
elif filter == "JPXDecode":
|
||||||
|
del dict_obj["BitsPerComponent"]
|
||||||
|
Image.SAVE["JPEG2000"](im, op, filename)
|
||||||
|
else:
|
||||||
|
msg = f"unsupported PDF filter ({filter})"
|
||||||
|
raise ValueError(msg)
|
||||||
|
|
||||||
|
stream = op.getvalue()
|
||||||
|
if filter == "CCITTFaxDecode":
|
||||||
|
stream = stream[8:]
|
||||||
|
filter = PdfParser.PdfArray([PdfParser.PdfName(filter)])
|
||||||
|
else:
|
||||||
|
filter = PdfParser.PdfName(filter)
|
||||||
|
|
||||||
|
image_ref = image_refs.pop(0)
|
||||||
|
existing_pdf.write_obj(
|
||||||
|
image_ref,
|
||||||
|
stream=stream,
|
||||||
|
Type=PdfParser.PdfName("XObject"),
|
||||||
|
Subtype=PdfParser.PdfName("Image"),
|
||||||
|
Width=width, # * 72.0 / x_resolution,
|
||||||
|
Height=height, # * 72.0 / y_resolution,
|
||||||
|
Filter=filter,
|
||||||
|
Decode=decode,
|
||||||
|
DecodeParms=params,
|
||||||
|
**dict_obj,
|
||||||
|
)
|
||||||
|
|
||||||
|
return image_ref, procset
|
||||||
|
|
||||||
|
|
||||||
def _save(im, fp, filename, save_all=False):
|
def _save(im, fp, filename, save_all=False):
|
||||||
is_appending = im.encoderinfo.get("append", False)
|
is_appending = im.encoderinfo.get("append", False)
|
||||||
if is_appending:
|
if is_appending:
|
||||||
|
@ -109,6 +235,9 @@ def _save(im, fp, filename, save_all=False):
|
||||||
number_of_pages += im_number_of_pages
|
number_of_pages += im_number_of_pages
|
||||||
for i in range(im_number_of_pages):
|
for i in range(im_number_of_pages):
|
||||||
image_refs.append(existing_pdf.next_object_id(0))
|
image_refs.append(existing_pdf.next_object_id(0))
|
||||||
|
if im.mode == "P" and "transparency" in im.info:
|
||||||
|
image_refs.append(existing_pdf.next_object_id(0))
|
||||||
|
|
||||||
page_refs.append(existing_pdf.next_object_id(0))
|
page_refs.append(existing_pdf.next_object_id(0))
|
||||||
contents_refs.append(existing_pdf.next_object_id(0))
|
contents_refs.append(existing_pdf.next_object_id(0))
|
||||||
existing_pdf.pages.append(page_refs[-1])
|
existing_pdf.pages.append(page_refs[-1])
|
||||||
|
@ -121,123 +250,7 @@ def _save(im, fp, filename, save_all=False):
|
||||||
for im_sequence in ims:
|
for im_sequence in ims:
|
||||||
im_pages = ImageSequence.Iterator(im_sequence) if save_all else [im_sequence]
|
im_pages = ImageSequence.Iterator(im_sequence) if save_all else [im_sequence]
|
||||||
for im in im_pages:
|
for im in im_pages:
|
||||||
# FIXME: Should replace ASCIIHexDecode with RunLengthDecode
|
image_ref, procset = _write_image(im, filename, existing_pdf, image_refs)
|
||||||
# (packbits) or LZWDecode (tiff/lzw compression). Note that
|
|
||||||
# PDF 1.2 also supports Flatedecode (zip compression).
|
|
||||||
|
|
||||||
params = None
|
|
||||||
decode = None
|
|
||||||
|
|
||||||
#
|
|
||||||
# Get image characteristics
|
|
||||||
|
|
||||||
width, height = im.size
|
|
||||||
|
|
||||||
dict_obj = {"BitsPerComponent": 8}
|
|
||||||
if im.mode == "1":
|
|
||||||
if features.check("libtiff"):
|
|
||||||
filter = "CCITTFaxDecode"
|
|
||||||
dict_obj["BitsPerComponent"] = 1
|
|
||||||
params = PdfParser.PdfArray(
|
|
||||||
[
|
|
||||||
PdfParser.PdfDict(
|
|
||||||
{
|
|
||||||
"K": -1,
|
|
||||||
"BlackIs1": True,
|
|
||||||
"Columns": width,
|
|
||||||
"Rows": height,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
]
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
filter = "DCTDecode"
|
|
||||||
dict_obj["ColorSpace"] = PdfParser.PdfName("DeviceGray")
|
|
||||||
procset = "ImageB" # grayscale
|
|
||||||
elif im.mode == "L":
|
|
||||||
filter = "DCTDecode"
|
|
||||||
# params = f"<< /Predictor 15 /Columns {width-2} >>"
|
|
||||||
dict_obj["ColorSpace"] = PdfParser.PdfName("DeviceGray")
|
|
||||||
procset = "ImageB" # grayscale
|
|
||||||
elif im.mode == "LA":
|
|
||||||
filter = "JPXDecode"
|
|
||||||
# params = f"<< /Predictor 15 /Columns {width-2} >>"
|
|
||||||
colorspace = PdfParser.PdfName("DeviceGray")
|
|
||||||
procset = "ImageB" # grayscale
|
|
||||||
elif im.mode == "P":
|
|
||||||
filter = "ASCIIHexDecode"
|
|
||||||
palette = im.getpalette()
|
|
||||||
dict_obj["ColorSpace"] = [
|
|
||||||
PdfParser.PdfName("Indexed"),
|
|
||||||
PdfParser.PdfName("DeviceRGB"),
|
|
||||||
255,
|
|
||||||
PdfParser.PdfBinary(palette),
|
|
||||||
]
|
|
||||||
procset = "ImageI" # indexed color
|
|
||||||
elif im.mode == "RGB":
|
|
||||||
filter = "DCTDecode"
|
|
||||||
dict_obj["ColorSpace"] = PdfParser.PdfName("DeviceRGB")
|
|
||||||
procset = "ImageC" # color images
|
|
||||||
elif im.mode == "RGBA":
|
|
||||||
filter = "JPXDecode"
|
|
||||||
procset = "ImageC" # color images
|
|
||||||
dict_obj["SMaskInData"] = 1
|
|
||||||
elif im.mode == "CMYK":
|
|
||||||
filter = "DCTDecode"
|
|
||||||
dict_obj["ColorSpace"] = PdfParser.PdfName("DeviceCMYK")
|
|
||||||
procset = "ImageC" # color images
|
|
||||||
decode = [1, 0, 1, 0, 1, 0, 1, 0]
|
|
||||||
else:
|
|
||||||
msg = f"cannot save mode {im.mode}"
|
|
||||||
raise ValueError(msg)
|
|
||||||
|
|
||||||
#
|
|
||||||
# image
|
|
||||||
|
|
||||||
op = io.BytesIO()
|
|
||||||
|
|
||||||
if filter == "ASCIIHexDecode":
|
|
||||||
ImageFile._save(im, op, [("hex", (0, 0) + im.size, 0, im.mode)])
|
|
||||||
elif filter == "CCITTFaxDecode":
|
|
||||||
im.save(
|
|
||||||
op,
|
|
||||||
"TIFF",
|
|
||||||
compression="group4",
|
|
||||||
# use a single strip
|
|
||||||
strip_size=math.ceil(im.width / 8) * im.height,
|
|
||||||
)
|
|
||||||
elif filter == "DCTDecode":
|
|
||||||
Image.SAVE["JPEG"](im, op, filename)
|
|
||||||
elif filter == "JPXDecode":
|
|
||||||
del dict_obj["BitsPerComponent"]
|
|
||||||
Image.SAVE["JPEG2000"](im, op, filename)
|
|
||||||
elif filter == "FlateDecode":
|
|
||||||
ImageFile._save(im, op, [("zip", (0, 0) + im.size, 0, im.mode)])
|
|
||||||
elif filter == "RunLengthDecode":
|
|
||||||
ImageFile._save(im, op, [("packbits", (0, 0) + im.size, 0, im.mode)])
|
|
||||||
else:
|
|
||||||
msg = f"unsupported PDF filter ({filter})"
|
|
||||||
raise ValueError(msg)
|
|
||||||
|
|
||||||
stream = op.getvalue()
|
|
||||||
if filter == "CCITTFaxDecode":
|
|
||||||
stream = stream[8:]
|
|
||||||
filter = PdfParser.PdfArray([PdfParser.PdfName(filter)])
|
|
||||||
else:
|
|
||||||
filter = PdfParser.PdfName(filter)
|
|
||||||
|
|
||||||
existing_pdf.write_obj(
|
|
||||||
image_refs[page_number],
|
|
||||||
stream=stream,
|
|
||||||
Type=PdfParser.PdfName("XObject"),
|
|
||||||
Subtype=PdfParser.PdfName("Image"),
|
|
||||||
Width=width, # * 72.0 / x_resolution,
|
|
||||||
Height=height, # * 72.0 / y_resolution,
|
|
||||||
Filter=filter,
|
|
||||||
Decode=decode,
|
|
||||||
DecodeParms=params,
|
|
||||||
**dict_obj,
|
|
||||||
)
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# page
|
# page
|
||||||
|
@ -246,13 +259,13 @@ def _save(im, fp, filename, save_all=False):
|
||||||
page_refs[page_number],
|
page_refs[page_number],
|
||||||
Resources=PdfParser.PdfDict(
|
Resources=PdfParser.PdfDict(
|
||||||
ProcSet=[PdfParser.PdfName("PDF"), PdfParser.PdfName(procset)],
|
ProcSet=[PdfParser.PdfName("PDF"), PdfParser.PdfName(procset)],
|
||||||
XObject=PdfParser.PdfDict(image=image_refs[page_number]),
|
XObject=PdfParser.PdfDict(image=image_ref),
|
||||||
),
|
),
|
||||||
MediaBox=[
|
MediaBox=[
|
||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
width * 72.0 / x_resolution,
|
im.width * 72.0 / x_resolution,
|
||||||
height * 72.0 / y_resolution,
|
im.height * 72.0 / y_resolution,
|
||||||
],
|
],
|
||||||
Contents=contents_refs[page_number],
|
Contents=contents_refs[page_number],
|
||||||
)
|
)
|
||||||
|
@ -261,8 +274,8 @@ def _save(im, fp, filename, save_all=False):
|
||||||
# page contents
|
# page contents
|
||||||
|
|
||||||
page_contents = b"q %f 0 0 %f 0 0 cm /image Do Q\n" % (
|
page_contents = b"q %f 0 0 %f 0 0 cm /image Do Q\n" % (
|
||||||
width * 72.0 / x_resolution,
|
im.width * 72.0 / x_resolution,
|
||||||
height * 72.0 / y_resolution,
|
im.height * 72.0 / y_resolution,
|
||||||
)
|
)
|
||||||
|
|
||||||
existing_pdf.write_obj(contents_refs[page_number], stream=page_contents)
|
existing_pdf.write_obj(contents_refs[page_number], stream=page_contents)
|
||||||
|
|
Loading…
Reference in New Issue
Block a user