mirror of
https://github.com/python-pillow/Pillow.git
synced 2024-11-10 19:56:47 +03:00
Merge branch 'main' into iptc
This commit is contained in:
commit
dcfce9487e
|
@ -1,6 +1,6 @@
|
|||
repos:
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 23.3.0
|
||||
rev: 23.7.0
|
||||
hooks:
|
||||
- id: black
|
||||
args: [--target-version=py38]
|
||||
|
@ -23,13 +23,13 @@ repos:
|
|||
- id: yesqa
|
||||
|
||||
- repo: https://github.com/Lucas-C/pre-commit-hooks
|
||||
rev: v1.5.1
|
||||
rev: v1.5.3
|
||||
hooks:
|
||||
- id: remove-tabs
|
||||
exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$)
|
||||
|
||||
- repo: https://github.com/PyCQA/flake8
|
||||
rev: 6.0.0
|
||||
rev: 6.1.0
|
||||
hooks:
|
||||
- id: flake8
|
||||
additional_dependencies:
|
||||
|
@ -55,7 +55,7 @@ repos:
|
|||
- id: sphinx-lint
|
||||
|
||||
- repo: https://github.com/tox-dev/pyproject-fmt
|
||||
rev: 0.12.1
|
||||
rev: 0.13.0
|
||||
hooks:
|
||||
- id: pyproject-fmt
|
||||
|
||||
|
|
15
CHANGES.rst
15
CHANGES.rst
|
@ -5,6 +5,21 @@ Changelog (Pillow)
|
|||
10.1.0 (unreleased)
|
||||
-------------------
|
||||
|
||||
- Read WebP duration after opening #7311
|
||||
[k128, radarhere]
|
||||
|
||||
- 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
|
||||
[radarhere]
|
||||
|
||||
|
|
|
@ -875,6 +875,14 @@ def test_identical_frames_to_single_frame(duration, tmp_path):
|
|||
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):
|
||||
number_of_loops = 2
|
||||
|
||||
|
|
|
@ -48,6 +48,22 @@ def test_save_alpha(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):
|
||||
# Arrange
|
||||
mode = "1"
|
||||
|
|
|
@ -79,7 +79,7 @@ class TestFilePng:
|
|||
|
||||
def test_sanity(self, tmp_path):
|
||||
# internal version number
|
||||
assert re.search(r"\d+\.\d+\.\d+(\.\d+)?$", features.version_codec("zlib"))
|
||||
assert re.search(r"\d+(\.\d+){1,3}$", features.version_codec("zlib"))
|
||||
|
||||
test_file = str(tmp_path / "temp.png")
|
||||
|
||||
|
|
|
@ -233,5 +233,4 @@ class TestFileWebp:
|
|||
im.save(out_webp, save_all=True)
|
||||
|
||||
with Image.open(out_webp) as reloaded:
|
||||
reloaded.load()
|
||||
assert reloaded.info["duration"] == 1000
|
||||
|
|
|
@ -661,15 +661,15 @@ class TestImage:
|
|||
blank_p.palette = None
|
||||
blank_pa.palette = None
|
||||
|
||||
def _make_new(base_image, im, palette_result=None):
|
||||
new_im = base_image._new(im)
|
||||
assert new_im.mode == im.mode
|
||||
assert new_im.size == im.size
|
||||
assert new_im.info == base_image.info
|
||||
def _make_new(base_image, image, palette_result=None):
|
||||
new_image = base_image._new(image.im)
|
||||
assert new_image.mode == image.mode
|
||||
assert new_image.size == image.size
|
||||
assert new_image.info == base_image.info
|
||||
if palette_result is not None:
|
||||
assert new_im.palette.tobytes() == palette_result.tobytes()
|
||||
assert new_image.palette.tobytes() == palette_result.tobytes()
|
||||
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_p, im, None)
|
||||
|
|
|
@ -253,7 +253,7 @@ their :py:attr:`~PIL.Image.Image.info` values.
|
|||
|
||||
**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**
|
||||
A comment about the image.
|
||||
|
|
|
@ -83,10 +83,9 @@ Install Pillow with :command:`pip`::
|
|||
.. tab:: Windows
|
||||
|
||||
We provide Pillow binaries for Windows compiled for the matrix of
|
||||
supported Pythons in both 32 and 64-bit versions in the wheel format.
|
||||
These binaries include support for all optional libraries except
|
||||
libimagequant and libxcb. Raqm support requires
|
||||
FriBiDi to be installed separately::
|
||||
supported Pythons in 64-bit versions in the wheel format. These binaries include
|
||||
support for all optional libraries except libimagequant and libxcb. Raqm support
|
||||
requires FriBiDi to be installed separately::
|
||||
|
||||
python3 -m pip install --upgrade pip
|
||||
python3 -m pip install --upgrade Pillow
|
||||
|
|
|
@ -206,4 +206,4 @@ Support reading signed 8-bit TIFF images
|
|||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
TIFF images with signed integer data, 8 bits per sample and a photometric
|
||||
interpretaton of BlackIsZero can now be read.
|
||||
interpretation of BlackIsZero can now be read.
|
||||
|
|
|
@ -912,7 +912,7 @@ def _get_global_header(im, info):
|
|||
info
|
||||
and (
|
||||
"transparency" in info
|
||||
or "loop" in info
|
||||
or info.get("loop") is not None
|
||||
or info.get("duration")
|
||||
or info.get("comment")
|
||||
)
|
||||
|
@ -937,7 +937,7 @@ def _get_global_header(im, info):
|
|||
# Global Color Table
|
||||
_get_header_palette(palette_bytes),
|
||||
]
|
||||
if "loop" in info:
|
||||
if info.get("loop") is not None:
|
||||
header.append(
|
||||
b"!"
|
||||
+ o8(255) # extension intro
|
||||
|
|
|
@ -247,7 +247,7 @@ def eval(expression, _dict={}, **kw):
|
|||
|
||||
def scan(code):
|
||||
for const in code.co_consts:
|
||||
if type(const) == type(compiled_code):
|
||||
if type(const) is type(compiled_code):
|
||||
scan(const)
|
||||
|
||||
for name in code.co_names:
|
||||
|
|
|
@ -46,6 +46,132 @@ def _save_all(im, fp, filename):
|
|||
# (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):
|
||||
is_appending = im.encoderinfo.get("append", False)
|
||||
if is_appending:
|
||||
|
@ -109,6 +235,9 @@ def _save(im, fp, filename, save_all=False):
|
|||
number_of_pages += im_number_of_pages
|
||||
for i in range(im_number_of_pages):
|
||||
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))
|
||||
contents_refs.append(existing_pdf.next_object_id(0))
|
||||
existing_pdf.pages.append(page_refs[-1])
|
||||
|
@ -121,123 +250,7 @@ def _save(im, fp, filename, save_all=False):
|
|||
for im_sequence in ims:
|
||||
im_pages = ImageSequence.Iterator(im_sequence) if save_all else [im_sequence]
|
||||
for im in im_pages:
|
||||
# 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
|
||||
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,
|
||||
)
|
||||
image_ref, procset = _write_image(im, filename, existing_pdf, image_refs)
|
||||
|
||||
#
|
||||
# page
|
||||
|
@ -246,13 +259,13 @@ def _save(im, fp, filename, save_all=False):
|
|||
page_refs[page_number],
|
||||
Resources=PdfParser.PdfDict(
|
||||
ProcSet=[PdfParser.PdfName("PDF"), PdfParser.PdfName(procset)],
|
||||
XObject=PdfParser.PdfDict(image=image_refs[page_number]),
|
||||
XObject=PdfParser.PdfDict(image=image_ref),
|
||||
),
|
||||
MediaBox=[
|
||||
0,
|
||||
0,
|
||||
width * 72.0 / x_resolution,
|
||||
height * 72.0 / y_resolution,
|
||||
im.width * 72.0 / x_resolution,
|
||||
im.height * 72.0 / y_resolution,
|
||||
],
|
||||
Contents=contents_refs[page_number],
|
||||
)
|
||||
|
@ -261,8 +274,8 @@ def _save(im, fp, filename, save_all=False):
|
|||
# page contents
|
||||
|
||||
page_contents = b"q %f 0 0 %f 0 0 cm /image Do Q\n" % (
|
||||
width * 72.0 / x_resolution,
|
||||
height * 72.0 / y_resolution,
|
||||
im.width * 72.0 / x_resolution,
|
||||
im.height * 72.0 / y_resolution,
|
||||
)
|
||||
|
||||
existing_pdf.write_obj(contents_refs[page_number], stream=page_contents)
|
||||
|
|
|
@ -74,6 +74,9 @@ class WebPImageFile(ImageFile.ImageFile):
|
|||
self.info["background"] = (bg_r, bg_g, bg_b, bg_a)
|
||||
self.n_frames = frame_count
|
||||
self.is_animated = self.n_frames > 1
|
||||
ret = self._decoder.get_next()
|
||||
if ret is not None:
|
||||
self.info["duration"] = ret[1]
|
||||
self._mode = "RGB" if mode == "RGBX" else mode
|
||||
self.rawmode = mode
|
||||
self.tile = []
|
||||
|
@ -90,7 +93,7 @@ class WebPImageFile(ImageFile.ImageFile):
|
|||
self.info["xmp"] = xmp
|
||||
|
||||
# Initialize seek state
|
||||
self._reset(reset=False)
|
||||
self._reset()
|
||||
|
||||
def _getexif(self):
|
||||
if "exif" not in self.info:
|
||||
|
@ -113,9 +116,8 @@ class WebPImageFile(ImageFile.ImageFile):
|
|||
# Set logical frame to requested position
|
||||
self.__logical_frame = frame
|
||||
|
||||
def _reset(self, reset=True):
|
||||
if reset:
|
||||
self._decoder.reset()
|
||||
def _reset(self):
|
||||
self._decoder.reset()
|
||||
self.__physical_frame = 0
|
||||
self.__loaded = -1
|
||||
self.__timestamp = 0
|
||||
|
|
|
@ -130,9 +130,9 @@ deps = {
|
|||
"bins": ["cjpeg.exe", "djpeg.exe"],
|
||||
},
|
||||
"zlib": {
|
||||
"url": "https://zlib.net/zlib1213.zip",
|
||||
"filename": "zlib1213.zip",
|
||||
"dir": "zlib-1.2.13",
|
||||
"url": "https://zlib.net/zlib13.zip",
|
||||
"filename": "zlib13.zip",
|
||||
"dir": "zlib-1.3",
|
||||
"license": "README",
|
||||
"license_pattern": "Copyright notice:\n\n(.+)$",
|
||||
"build": [
|
||||
|
@ -335,9 +335,9 @@ deps = {
|
|||
"libs": [r"imagequant.lib"],
|
||||
},
|
||||
"harfbuzz": {
|
||||
"url": "https://github.com/harfbuzz/harfbuzz/archive/8.0.0.zip",
|
||||
"filename": "harfbuzz-8.0.0.zip",
|
||||
"dir": "harfbuzz-8.0.0",
|
||||
"url": "https://github.com/harfbuzz/harfbuzz/archive/8.1.1.zip",
|
||||
"filename": "harfbuzz-8.1.1.zip",
|
||||
"dir": "harfbuzz-8.1.1",
|
||||
"license": "COPYING",
|
||||
"build": [
|
||||
*cmds_cmake(
|
||||
|
|
Loading…
Reference in New Issue
Block a user