mirror of
https://github.com/python-pillow/Pillow.git
synced 2024-12-26 18:06:18 +03:00
Merge branch 'master' into eps
This commit is contained in:
commit
929c561937
33
CHANGES.rst
33
CHANGES.rst
|
@ -5,6 +5,39 @@ Changelog (Pillow)
|
|||
8.4.0 (unreleased)
|
||||
------------------
|
||||
|
||||
- Consider I;16 pixel size when drawing text #5598
|
||||
[radarhere]
|
||||
|
||||
- If default conversion from P is RGB with transparency, convert to RGBA #5594
|
||||
[radarhere]
|
||||
|
||||
- Speed up rotating square images by 90 or 270 degrees #5646
|
||||
[radarhere]
|
||||
|
||||
- Add support for reading DPI information from JPEG2000 images
|
||||
[rogermb, radarhere]
|
||||
|
||||
- Catch TypeError from corrupted DPI value in EXIF #5639
|
||||
[homm, radarhere]
|
||||
|
||||
- Do not close file pointer when saving SGI images #5645
|
||||
[farizrahman4u, radarhere]
|
||||
|
||||
- Deprecate ImagePalette size parameter #5641
|
||||
[radarhere, hugovk]
|
||||
|
||||
- Prefer command line tools SDK on macOS #5624
|
||||
[radarhere]
|
||||
|
||||
- Added tags when saving YCbCr TIFF #5597
|
||||
[radarhere]
|
||||
|
||||
- PSD layer count may be negative #5613
|
||||
[radarhere]
|
||||
|
||||
- Fixed ImageOps expand with tuple border on P image #5615
|
||||
[radarhere]
|
||||
|
||||
- Ensure TIFF RowsPerStrip is multiple of 8 for JPEG compression #5588
|
||||
[kmilos, radarhere]
|
||||
|
||||
|
|
BIN
Tests/images/broken_exif_dpi.jpg
Normal file
BIN
Tests/images/broken_exif_dpi.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 7.6 KiB |
BIN
Tests/images/expected_to_read.jp2
Normal file
BIN
Tests/images/expected_to_read.jp2
Normal file
Binary file not shown.
BIN
Tests/images/invalid_header_length.jp2
Normal file
BIN
Tests/images/invalid_header_length.jp2
Normal file
Binary file not shown.
BIN
Tests/images/negative_layer_count.psd
Normal file
BIN
Tests/images/negative_layer_count.psd
Normal file
Binary file not shown.
BIN
Tests/images/not_enough_data.jp2
Normal file
BIN
Tests/images/not_enough_data.jp2
Normal file
Binary file not shown.
BIN
Tests/images/zero_dpi.jp2
Normal file
BIN
Tests/images/zero_dpi.jp2
Normal file
Binary file not shown.
|
@ -718,6 +718,15 @@ class TestFileJpeg:
|
|||
# This should return the default, and not raise a ZeroDivisionError
|
||||
assert im.info.get("dpi") == (72, 72)
|
||||
|
||||
def test_dpi_exif_string(self):
|
||||
# Arrange
|
||||
# 0x011A tag in this exif contains string '300300\x02'
|
||||
with Image.open("Tests/images/broken_exif_dpi.jpg") as im:
|
||||
|
||||
# Act / Assert
|
||||
# This should return the default
|
||||
assert im.info.get("dpi") == (72, 72)
|
||||
|
||||
def test_no_dpi_in_exif(self):
|
||||
# Arrange
|
||||
# This is photoshop-200dpi.jpg with resolution removed from EXIF:
|
||||
|
|
|
@ -4,7 +4,7 @@ from io import BytesIO
|
|||
|
||||
import pytest
|
||||
|
||||
from PIL import Image, ImageFile, Jpeg2KImagePlugin, features
|
||||
from PIL import Image, ImageFile, Jpeg2KImagePlugin, UnidentifiedImageError, features
|
||||
|
||||
from .helper import (
|
||||
assert_image_equal,
|
||||
|
@ -151,6 +151,28 @@ def test_reduce():
|
|||
assert im.size == (40, 30)
|
||||
|
||||
|
||||
def test_load_dpi():
|
||||
with Image.open("Tests/images/test-card-lossless.jp2") as im:
|
||||
assert im.info["dpi"] == (71.9836, 71.9836)
|
||||
|
||||
with Image.open("Tests/images/zero_dpi.jp2") as im:
|
||||
assert "dpi" not in im.info
|
||||
|
||||
|
||||
def test_header_errors():
|
||||
for path in (
|
||||
"Tests/images/invalid_header_length.jp2",
|
||||
"Tests/images/not_enough_data.jp2",
|
||||
):
|
||||
with pytest.raises(UnidentifiedImageError):
|
||||
with Image.open(path):
|
||||
pass
|
||||
|
||||
with pytest.raises(OSError):
|
||||
with Image.open("Tests/images/expected_to_read.jp2"):
|
||||
pass
|
||||
|
||||
|
||||
def test_layers_type(tmp_path):
|
||||
outfile = str(tmp_path / "temp_layers.jp2")
|
||||
for quality_layers in [[100, 50, 10], (100, 50, 10), None]:
|
||||
|
|
|
@ -670,6 +670,15 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
TiffImagePlugin.WRITE_LIBTIFF = False
|
||||
TiffImagePlugin.READ_LIBTIFF = False
|
||||
|
||||
def test_save_ycbcr(self, tmp_path):
|
||||
im = hopper("YCbCr")
|
||||
outfile = str(tmp_path / "temp.tif")
|
||||
im.save(outfile, compression="jpeg")
|
||||
|
||||
with Image.open(outfile) as reloaded:
|
||||
assert reloaded.tag_v2[530] == (1, 1)
|
||||
assert reloaded.tag_v2[532] == (0, 255, 128, 255, 128, 255)
|
||||
|
||||
def test_crashing_metadata(self, tmp_path):
|
||||
# issue 1597
|
||||
with Image.open("Tests/images/rdf.tif") as im:
|
||||
|
|
|
@ -57,9 +57,10 @@ def test_n_frames():
|
|||
assert im.n_frames == 1
|
||||
assert not im.is_animated
|
||||
|
||||
with Image.open(test_file) as im:
|
||||
assert im.n_frames == 2
|
||||
assert im.is_animated
|
||||
for path in [test_file, "Tests/images/negative_layer_count.psd"]:
|
||||
with Image.open(path) as im:
|
||||
assert im.n_frames == 2
|
||||
assert im.is_animated
|
||||
|
||||
|
||||
def test_eoferror():
|
||||
|
|
|
@ -73,6 +73,13 @@ def test_write(tmp_path):
|
|||
img.save(out, format="sgi")
|
||||
assert_image_equal_tofile(img, out)
|
||||
|
||||
out = str(tmp_path / "fp.sgi")
|
||||
with open(out, "wb") as fp:
|
||||
img.save(fp)
|
||||
assert_image_equal_tofile(img, out)
|
||||
|
||||
assert not fp.closed
|
||||
|
||||
for mode in ("L", "RGB", "RGBA"):
|
||||
roundtrip(hopper(mode))
|
||||
|
||||
|
|
|
@ -104,6 +104,13 @@ class TestFileWebp:
|
|||
hopper().save(buffer_method, format="WEBP", method=6)
|
||||
assert buffer_no_args.getbuffer() != buffer_method.getbuffer()
|
||||
|
||||
def test_icc_profile(self, tmp_path):
|
||||
self._roundtrip(tmp_path, self.rgb_mode, 12.5, {"icc_profile": None})
|
||||
if _webp.HAVE_WEBPANIM:
|
||||
self._roundtrip(
|
||||
tmp_path, self.rgb_mode, 12.5, {"icc_profile": None, "save_all": True}
|
||||
)
|
||||
|
||||
def test_write_unsupported_mode_L(self, tmp_path):
|
||||
"""
|
||||
Saving a black-and-white file to WebP format should work, and be
|
||||
|
|
|
@ -42,10 +42,14 @@ def test_default():
|
|||
|
||||
im = hopper("P")
|
||||
assert_image(im, "P", im.size)
|
||||
im = im.convert()
|
||||
assert_image(im, "RGB", im.size)
|
||||
im = im.convert()
|
||||
assert_image(im, "RGB", im.size)
|
||||
converted_im = im.convert()
|
||||
assert_image(converted_im, "RGB", im.size)
|
||||
converted_im = im.convert()
|
||||
assert_image(converted_im, "RGB", im.size)
|
||||
|
||||
im.info["transparency"] = 0
|
||||
converted_im = im.convert()
|
||||
assert_image(converted_im, "RGBA", im.size)
|
||||
|
||||
|
||||
# ref https://github.com/python-pillow/Pillow/issues/274
|
||||
|
|
|
@ -33,6 +33,9 @@ def test_angle():
|
|||
with Image.open("Tests/images/test-card.png") as im:
|
||||
rotate(im, im.mode, angle)
|
||||
|
||||
im = hopper()
|
||||
assert_image_equal(im.rotate(angle), im.rotate(angle, expand=1))
|
||||
|
||||
|
||||
def test_zero():
|
||||
for angle in (0, 45, 90, 180, 270):
|
||||
|
|
|
@ -134,6 +134,17 @@ class TestImageFont:
|
|||
target = "Tests/images/transparent_background_text_L.png"
|
||||
assert_image_similar_tofile(im.convert("L"), target, 0.01)
|
||||
|
||||
def test_I16(self):
|
||||
im = Image.new(mode="I;16", size=(300, 100))
|
||||
draw = ImageDraw.Draw(im)
|
||||
ttf = self.get_font()
|
||||
|
||||
txt = "Hello World!"
|
||||
draw.text((10, 10), txt, font=ttf)
|
||||
|
||||
target = "Tests/images/transparent_background_text_L.png"
|
||||
assert_image_similar_tofile(im.convert("L"), target, 0.01)
|
||||
|
||||
def test_textsize_equal(self):
|
||||
im = Image.new(mode="RGB", size=(300, 100))
|
||||
draw = ImageDraw.Draw(im)
|
||||
|
|
|
@ -156,23 +156,31 @@ def test_scale():
|
|||
assert newimg.size == (25, 25)
|
||||
|
||||
|
||||
def test_expand_palette():
|
||||
im = Image.open("Tests/images/p_16.tga")
|
||||
im_expanded = ImageOps.expand(im, 10, (255, 0, 0))
|
||||
@pytest.mark.parametrize("border", (10, (1, 2, 3, 4)))
|
||||
def test_expand_palette(border):
|
||||
with Image.open("Tests/images/p_16.tga") as im:
|
||||
im_expanded = ImageOps.expand(im, border, (255, 0, 0))
|
||||
|
||||
px = im_expanded.convert("RGB").load()
|
||||
for b in range(10):
|
||||
if isinstance(border, int):
|
||||
left = top = right = bottom = border
|
||||
else:
|
||||
left, top, right, bottom = border
|
||||
px = im_expanded.convert("RGB").load()
|
||||
for x in range(im_expanded.width):
|
||||
assert px[x, b] == (255, 0, 0)
|
||||
assert px[x, im_expanded.height - 1 - b] == (255, 0, 0)
|
||||
for b in range(top):
|
||||
assert px[x, b] == (255, 0, 0)
|
||||
for b in range(bottom):
|
||||
assert px[x, im_expanded.height - 1 - b] == (255, 0, 0)
|
||||
for y in range(im_expanded.height):
|
||||
assert px[b, x] == (255, 0, 0)
|
||||
assert px[b, im_expanded.width - 1 - b] == (255, 0, 0)
|
||||
for b in range(left):
|
||||
assert px[b, y] == (255, 0, 0)
|
||||
for b in range(right):
|
||||
assert px[im_expanded.width - 1 - b, y] == (255, 0, 0)
|
||||
|
||||
im_cropped = im_expanded.crop(
|
||||
(10, 10, im_expanded.width - 10, im_expanded.height - 10)
|
||||
)
|
||||
assert_image_equal(im_cropped, im)
|
||||
im_cropped = im_expanded.crop(
|
||||
(left, top, im_expanded.width - right, im_expanded.height - bottom)
|
||||
)
|
||||
assert_image_equal(im_cropped, im)
|
||||
|
||||
|
||||
def test_colorize_2color():
|
||||
|
|
|
@ -10,8 +10,9 @@ def test_sanity():
|
|||
palette = ImagePalette.ImagePalette("RGB", list(range(256)) * 3)
|
||||
assert len(palette.colors) == 256
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
ImagePalette.ImagePalette("RGB", list(range(256)) * 3, 10)
|
||||
with pytest.warns(DeprecationWarning):
|
||||
with pytest.raises(ValueError):
|
||||
ImagePalette.ImagePalette("RGB", list(range(256)) * 3, 10)
|
||||
|
||||
|
||||
def test_reload():
|
||||
|
|
|
@ -92,6 +92,17 @@ dictionary. The :py:attr:`~PIL.JpegImagePlugin.convert_dict_qtables` method no l
|
|||
performs any operations on the data given to it, has been deprecated and will be
|
||||
removed in Pillow 10.0.0 (2023-01-02).
|
||||
|
||||
ImagePalette size parameter
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. deprecated:: 8.4.0
|
||||
|
||||
The ``size`` parameter will be removed in Pillow 10.0.0 (2023-01-02).
|
||||
|
||||
Before Pillow 8.3.0, ``ImagePalette`` required palette data of particular lengths by
|
||||
default, and the size parameter could be used to override that. Pillow 8.3.0 removed
|
||||
the default required length, also removing the need for the size parameter.
|
||||
|
||||
Removed features
|
||||
----------------
|
||||
|
||||
|
|
|
@ -18,9 +18,9 @@ Pillow supports these Python versions.
|
|||
+----------------------+-----+-----+-----+-----+-----+-----+-----+-----+
|
||||
| Python |3.10 | 3.9 | 3.8 | 3.7 | 3.6 | 3.5 | 3.4 | 2.7 |
|
||||
+======================+=====+=====+=====+=====+=====+=====+=====+=====+
|
||||
| Pillow >= 8.3 | Yes | Yes | Yes | Yes | Yes | | | |
|
||||
| Pillow >= 8.4 | Yes | Yes | Yes | Yes | Yes | | | |
|
||||
+----------------------+-----+-----+-----+-----+-----+-----+-----+-----+
|
||||
| Pillow 8.0 - 8.2 | | Yes | Yes | Yes | Yes | | | |
|
||||
| Pillow 8.0 - 8.3 | | Yes | Yes | Yes | Yes | | | |
|
||||
+----------------------+-----+-----+-----+-----+-----+-----+-----+-----+
|
||||
| Pillow 7.0 - 7.2 | | | Yes | Yes | Yes | Yes | | |
|
||||
+----------------------+-----+-----+-----+-----+-----+-----+-----+-----+
|
||||
|
@ -494,11 +494,11 @@ These platforms have been reported to work at the versions mentioned.
|
|||
| Operating system | | Tested Python | | Latest tested | | Tested |
|
||||
| | | versions | | Pillow version | | processors |
|
||||
+==================================+===========================+==================+==============+
|
||||
| macOS 11.0 Big Sur | 3.7, 3.8, 3.9 | 8.2.0 |arm |
|
||||
| macOS 11.0 Big Sur | 3.7, 3.8, 3.9 | 8.3.1 |arm |
|
||||
| +---------------------------+------------------+--------------+
|
||||
| | 3.6, 3.7, 3.8, 3.9 | 8.2.0 |x86-64 |
|
||||
| | 3.6, 3.7, 3.8, 3.9 | 8.3.1 |x86-64 |
|
||||
+----------------------------------+---------------------------+------------------+--------------+
|
||||
| macOS 10.15 Catalina | 3.6, 3.7, 3.8, 3.9 | 8.0.1 |x86-64 |
|
||||
| macOS 10.15 Catalina | 3.6, 3.7, 3.8, 3.9 | 8.3.1 |x86-64 |
|
||||
| +---------------------------+------------------+ |
|
||||
| | 3.5 | 7.2.0 | |
|
||||
+----------------------------------+---------------------------+------------------+--------------+
|
||||
|
|
47
docs/releasenotes/8.4.0.rst
Normal file
47
docs/releasenotes/8.4.0.rst
Normal file
|
@ -0,0 +1,47 @@
|
|||
8.4.0
|
||||
-----
|
||||
|
||||
API Changes
|
||||
===========
|
||||
|
||||
Deprecations
|
||||
^^^^^^^^^^^^
|
||||
|
||||
ImagePalette size parameter
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The ``size`` parameter will be removed in Pillow 10.0.0 (2023-01-02).
|
||||
|
||||
Before Pillow 8.3.0, ``ImagePalette`` required palette data of particular lengths by
|
||||
default, and the size parameter could be used to override that. Pillow 8.3.0 removed
|
||||
the default required length, also removing the need for the size parameter.
|
||||
|
||||
API Additions
|
||||
=============
|
||||
|
||||
TODO
|
||||
^^^^
|
||||
|
||||
TODO
|
||||
|
||||
Security
|
||||
========
|
||||
|
||||
TODO
|
||||
^^^^
|
||||
|
||||
TODO
|
||||
|
||||
Other Changes
|
||||
=============
|
||||
|
||||
Speed improvement when rotating square images
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Starting with Pillow 3.3.0, the speed of rotating images by 90 or 270 degrees was
|
||||
improved by quickly returning :py:meth:`~PIL.Image.Image.transpose` instead, if the
|
||||
rotate operation allowed for expansion and did not specify a center or post-rotate
|
||||
translation.
|
||||
|
||||
Since the ``expand`` flag makes no difference for square images though, Pillow now
|
||||
uses this faster method for square images without the ``expand`` flag as well.
|
|
@ -14,6 +14,7 @@ expected to be backported to earlier versions.
|
|||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
8.4.0
|
||||
8.3.1
|
||||
8.3.0
|
||||
8.2.0
|
||||
|
|
18
setup.py
18
setup.py
|
@ -533,14 +533,16 @@ class pil_build_ext(build_ext):
|
|||
_add_directory(include_dirs, "/usr/X11/include")
|
||||
|
||||
# SDK install path
|
||||
try:
|
||||
sdk_path = (
|
||||
subprocess.check_output(["xcrun", "--show-sdk-path"])
|
||||
.strip()
|
||||
.decode("latin1")
|
||||
)
|
||||
except Exception:
|
||||
sdk_path = None
|
||||
sdk_path = "/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk"
|
||||
if not os.path.exists(sdk_path):
|
||||
try:
|
||||
sdk_path = (
|
||||
subprocess.check_output(["xcrun", "--show-sdk-path"])
|
||||
.strip()
|
||||
.decode("latin1")
|
||||
)
|
||||
except Exception:
|
||||
sdk_path = None
|
||||
if sdk_path:
|
||||
_add_directory(library_dirs, os.path.join(sdk_path, "usr", "lib"))
|
||||
_add_directory(include_dirs, os.path.join(sdk_path, "usr", "include"))
|
||||
|
|
|
@ -914,16 +914,18 @@ class Image:
|
|||
|
||||
self.load()
|
||||
|
||||
has_transparency = self.info.get("transparency") is not None
|
||||
if not mode and self.mode == "P":
|
||||
# determine default mode
|
||||
if self.palette:
|
||||
mode = self.palette.mode
|
||||
else:
|
||||
mode = "RGB"
|
||||
if mode == "RGB" and has_transparency:
|
||||
mode = "RGBA"
|
||||
if not mode or (mode == self.mode and not matrix):
|
||||
return self.copy()
|
||||
|
||||
has_transparency = self.info.get("transparency") is not None
|
||||
if matrix:
|
||||
# matrix conversion
|
||||
if mode not in ("L", "RGB"):
|
||||
|
@ -2074,10 +2076,8 @@ class Image:
|
|||
return self.copy()
|
||||
if angle == 180:
|
||||
return self.transpose(ROTATE_180)
|
||||
if angle == 90 and expand:
|
||||
return self.transpose(ROTATE_90)
|
||||
if angle == 270 and expand:
|
||||
return self.transpose(ROTATE_270)
|
||||
if angle in (90, 270) and (expand or self.width == self.height):
|
||||
return self.transpose(ROTATE_90 if angle == 90 else ROTATE_270)
|
||||
|
||||
# Calculate the affine matrix. Note that this is the reverse
|
||||
# transformation (from destination image to source) because we
|
||||
|
|
|
@ -21,7 +21,7 @@ import functools
|
|||
import operator
|
||||
import re
|
||||
|
||||
from . import Image, ImageDraw
|
||||
from . import Image
|
||||
|
||||
#
|
||||
# helpers
|
||||
|
@ -395,15 +395,16 @@ def expand(image, border=0, fill=0):
|
|||
height = top + image.size[1] + bottom
|
||||
color = _color(fill, image.mode)
|
||||
if image.mode == "P" and image.palette:
|
||||
out = Image.new(image.mode, (width, height))
|
||||
out.putpalette(image.palette)
|
||||
out.paste(image, (left, top))
|
||||
|
||||
draw = ImageDraw.Draw(out)
|
||||
draw.rectangle((0, 0, width - 1, height - 1), outline=color, width=border)
|
||||
image.load()
|
||||
palette = image.palette.copy()
|
||||
if isinstance(color, tuple):
|
||||
color = palette.getcolor(color)
|
||||
else:
|
||||
out = Image.new(image.mode, (width, height), color)
|
||||
out.paste(image, (left, top))
|
||||
palette = None
|
||||
out = Image.new(image.mode, (width, height), color)
|
||||
if palette:
|
||||
out.putpalette(palette.palette)
|
||||
out.paste(image, (left, top))
|
||||
return out
|
||||
|
||||
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
#
|
||||
|
||||
import array
|
||||
import warnings
|
||||
|
||||
from . import GimpGradientFile, GimpPaletteFile, ImageColor, PaletteFile
|
||||
|
||||
|
@ -28,12 +29,11 @@ class ImagePalette:
|
|||
:param mode: The mode to use for the Palette. See:
|
||||
:ref:`concept-modes`. Defaults to "RGB"
|
||||
:param palette: An optional palette. If given, it must be a bytearray,
|
||||
an array or a list of ints between 0-255 and of length ``size``
|
||||
times the number of colors in ``mode``. The list must be aligned
|
||||
an array or a list of ints between 0-255. The list must be aligned
|
||||
by channel (All R values must be contiguous in the list before G
|
||||
and B values.) Defaults to 0 through 255 per channel.
|
||||
:param size: An optional palette size. If given, it cannot be equal to
|
||||
or greater than 256. Defaults to 0.
|
||||
:param size: An optional palette size. If given, an error is raised
|
||||
if ``palette`` is not of equal length.
|
||||
"""
|
||||
|
||||
def __init__(self, mode="RGB", palette=None, size=0):
|
||||
|
@ -41,8 +41,14 @@ class ImagePalette:
|
|||
self.rawmode = None # if set, palette contains raw data
|
||||
self.palette = palette or bytearray()
|
||||
self.dirty = None
|
||||
if size != 0 and size != len(self.palette):
|
||||
raise ValueError("wrong palette size")
|
||||
if size != 0:
|
||||
warnings.warn(
|
||||
"The size parameter is deprecated and will be removed in Pillow 10 "
|
||||
"(2023-01-02).",
|
||||
DeprecationWarning,
|
||||
)
|
||||
if size != len(self.palette):
|
||||
raise ValueError("wrong palette size")
|
||||
|
||||
@property
|
||||
def palette(self):
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
#
|
||||
# History:
|
||||
# 2014-03-12 ajh Created
|
||||
# 2021-06-30 rogermb Extract dpi information from the 'resc' header box
|
||||
#
|
||||
# Copyright (c) 2014 Coriolis Systems Limited
|
||||
# Copyright (c) 2014 Alastair Houghton
|
||||
|
@ -19,6 +20,79 @@ import struct
|
|||
from . import Image, ImageFile
|
||||
|
||||
|
||||
class BoxReader:
|
||||
"""
|
||||
A small helper class to read fields stored in JPEG2000 header boxes
|
||||
and to easily step into and read sub-boxes.
|
||||
"""
|
||||
|
||||
def __init__(self, fp, length=-1):
|
||||
self.fp = fp
|
||||
self.has_length = length >= 0
|
||||
self.length = length
|
||||
self.remaining_in_box = -1
|
||||
|
||||
def _can_read(self, num_bytes):
|
||||
if self.has_length and self.fp.tell() + num_bytes > self.length:
|
||||
# Outside box: ensure we don't read past the known file length
|
||||
return False
|
||||
if self.remaining_in_box >= 0:
|
||||
# Inside box contents: ensure read does not go past box boundaries
|
||||
return num_bytes <= self.remaining_in_box
|
||||
else:
|
||||
return True # No length known, just read
|
||||
|
||||
def _read_bytes(self, num_bytes):
|
||||
if not self._can_read(num_bytes):
|
||||
raise SyntaxError("Not enough data in header")
|
||||
|
||||
data = self.fp.read(num_bytes)
|
||||
if len(data) < num_bytes:
|
||||
raise OSError(
|
||||
f"Expected to read {num_bytes} bytes but only got {len(data)}."
|
||||
)
|
||||
|
||||
if self.remaining_in_box > 0:
|
||||
self.remaining_in_box -= num_bytes
|
||||
return data
|
||||
|
||||
def read_fields(self, field_format):
|
||||
size = struct.calcsize(field_format)
|
||||
data = self._read_bytes(size)
|
||||
return struct.unpack(field_format, data)
|
||||
|
||||
def read_boxes(self):
|
||||
size = self.remaining_in_box
|
||||
data = self._read_bytes(size)
|
||||
return BoxReader(io.BytesIO(data), size)
|
||||
|
||||
def has_next_box(self):
|
||||
if self.has_length:
|
||||
return self.fp.tell() + self.remaining_in_box < self.length
|
||||
else:
|
||||
return True
|
||||
|
||||
def next_box_type(self):
|
||||
# Skip the rest of the box if it has not been read
|
||||
if self.remaining_in_box > 0:
|
||||
self.fp.seek(self.remaining_in_box, os.SEEK_CUR)
|
||||
self.remaining_in_box = -1
|
||||
|
||||
# Read the length and type of the next box
|
||||
lbox, tbox = self.read_fields(">I4s")
|
||||
if lbox == 1:
|
||||
lbox = self.read_fields(">Q")[0]
|
||||
hlen = 16
|
||||
else:
|
||||
hlen = 8
|
||||
|
||||
if lbox < hlen or not self._can_read(lbox - hlen):
|
||||
raise SyntaxError("Invalid header length")
|
||||
|
||||
self.remaining_in_box = lbox - hlen
|
||||
return tbox
|
||||
|
||||
|
||||
def _parse_codestream(fp):
|
||||
"""Parse the JPEG 2000 codestream to extract the size and component
|
||||
count from the SIZ marker segment, returning a PIL (size, mode) tuple."""
|
||||
|
@ -53,55 +127,45 @@ def _parse_codestream(fp):
|
|||
return (size, mode)
|
||||
|
||||
|
||||
def _res_to_dpi(num, denom, exp):
|
||||
"""Convert JPEG2000's (numerator, denominator, exponent-base-10) resolution,
|
||||
calculated as (num / denom) * 10^exp and stored in dots per meter,
|
||||
to floating-point dots per inch."""
|
||||
if denom != 0:
|
||||
return (254 * num * (10 ** exp)) / (10000 * denom)
|
||||
|
||||
|
||||
def _parse_jp2_header(fp):
|
||||
"""Parse the JP2 header box to extract size, component count and
|
||||
color space information, returning a (size, mode, mimetype) tuple."""
|
||||
"""Parse the JP2 header box to extract size, component count,
|
||||
color space information, and optionally DPI information,
|
||||
returning a (size, mode, mimetype, dpi) tuple."""
|
||||
|
||||
# Find the JP2 header box
|
||||
reader = BoxReader(fp)
|
||||
header = None
|
||||
mimetype = None
|
||||
while True:
|
||||
lbox, tbox = struct.unpack(">I4s", fp.read(8))
|
||||
if lbox == 1:
|
||||
lbox = struct.unpack(">Q", fp.read(8))[0]
|
||||
hlen = 16
|
||||
else:
|
||||
hlen = 8
|
||||
|
||||
if lbox < hlen:
|
||||
raise SyntaxError("Invalid JP2 header length")
|
||||
while reader.has_next_box():
|
||||
tbox = reader.next_box_type()
|
||||
|
||||
if tbox == b"jp2h":
|
||||
header = fp.read(lbox - hlen)
|
||||
header = reader.read_boxes()
|
||||
break
|
||||
elif tbox == b"ftyp":
|
||||
if fp.read(4) == b"jpx ":
|
||||
if reader.read_fields(">4s")[0] == b"jpx ":
|
||||
mimetype = "image/jpx"
|
||||
fp.seek(lbox - hlen - 4, os.SEEK_CUR)
|
||||
else:
|
||||
fp.seek(lbox - hlen, os.SEEK_CUR)
|
||||
|
||||
if header is None:
|
||||
raise SyntaxError("could not find JP2 header")
|
||||
|
||||
size = None
|
||||
mode = None
|
||||
bpc = None
|
||||
nc = None
|
||||
dpi = None # 2-tuple of DPI info, or None
|
||||
unkc = 0 # Colorspace information unknown
|
||||
|
||||
hio = io.BytesIO(header)
|
||||
while True:
|
||||
lbox, tbox = struct.unpack(">I4s", hio.read(8))
|
||||
if lbox == 1:
|
||||
lbox = struct.unpack(">Q", hio.read(8))[0]
|
||||
hlen = 16
|
||||
else:
|
||||
hlen = 8
|
||||
|
||||
content = hio.read(lbox - hlen)
|
||||
while header.has_next_box():
|
||||
tbox = header.next_box_type()
|
||||
|
||||
if tbox == b"ihdr":
|
||||
height, width, nc, bpc, c, unkc, ipr = struct.unpack(">IIHBBBB", content)
|
||||
height, width, nc, bpc, c, unkc, ipr = header.read_fields(">IIHBBBB")
|
||||
size = (width, height)
|
||||
if unkc:
|
||||
if nc == 1 and (bpc & 0x7F) > 8:
|
||||
|
@ -114,11 +178,10 @@ def _parse_jp2_header(fp):
|
|||
mode = "RGB"
|
||||
elif nc == 4:
|
||||
mode = "RGBA"
|
||||
break
|
||||
elif tbox == b"colr":
|
||||
meth, prec, approx = struct.unpack_from(">BBB", content)
|
||||
if meth == 1:
|
||||
cs = struct.unpack_from(">I", content, 3)[0]
|
||||
meth, prec, approx = header.read_fields(">BBB")
|
||||
if meth == 1 and unkc == 0:
|
||||
cs = header.read_fields(">I")[0]
|
||||
if cs == 16: # sRGB
|
||||
if nc == 1 and (bpc & 0x7F) > 8:
|
||||
mode = "I;16"
|
||||
|
@ -128,7 +191,6 @@ def _parse_jp2_header(fp):
|
|||
mode = "RGB"
|
||||
elif nc == 4:
|
||||
mode = "RGBA"
|
||||
break
|
||||
elif cs == 17: # grayscale
|
||||
if nc == 1 and (bpc & 0x7F) > 8:
|
||||
mode = "I;16"
|
||||
|
@ -136,18 +198,27 @@ def _parse_jp2_header(fp):
|
|||
mode = "L"
|
||||
elif nc == 2:
|
||||
mode = "LA"
|
||||
break
|
||||
elif cs == 18: # sYCC
|
||||
if nc == 3:
|
||||
mode = "RGB"
|
||||
elif nc == 4:
|
||||
mode = "RGBA"
|
||||
elif tbox == b"res ":
|
||||
res = header.read_boxes()
|
||||
while res.has_next_box():
|
||||
tres = res.next_box_type()
|
||||
if tres == b"resc":
|
||||
vrcn, vrcd, hrcn, hrcd, vrce, hrce = res.read_fields(">HHHHBB")
|
||||
hres = _res_to_dpi(hrcn, hrcd, hrce)
|
||||
vres = _res_to_dpi(vrcn, vrcd, vrce)
|
||||
if hres is not None and vres is not None:
|
||||
dpi = (hres, vres)
|
||||
break
|
||||
|
||||
if size is None or mode is None:
|
||||
raise SyntaxError("Malformed jp2 header")
|
||||
raise SyntaxError("Malformed JP2 header")
|
||||
|
||||
return (size, mode, mimetype)
|
||||
return (size, mode, mimetype, dpi)
|
||||
|
||||
|
||||
##
|
||||
|
@ -169,7 +240,9 @@ class Jpeg2KImageFile(ImageFile.ImageFile):
|
|||
if sig == b"\x00\x00\x00\x0cjP \x0d\x0a\x87\x0a":
|
||||
self.codec = "jp2"
|
||||
header = _parse_jp2_header(self.fp)
|
||||
self._size, self.mode, self.custom_mimetype = header
|
||||
self._size, self.mode, self.custom_mimetype, dpi = header
|
||||
if dpi is not None:
|
||||
self.info["dpi"] = dpi
|
||||
else:
|
||||
raise SyntaxError("not a JPEG 2000 file")
|
||||
|
||||
|
|
|
@ -168,11 +168,11 @@ def APP(self, marker):
|
|||
# 1 dpcm = 2.54 dpi
|
||||
dpi *= 2.54
|
||||
self.info["dpi"] = dpi, dpi
|
||||
except (KeyError, SyntaxError, ValueError, ZeroDivisionError):
|
||||
except (TypeError, KeyError, SyntaxError, ValueError, ZeroDivisionError):
|
||||
# SyntaxError for invalid/unreadable EXIF
|
||||
# KeyError for dpi not included
|
||||
# ZeroDivisionError for invalid dpi rational value
|
||||
# ValueError for dpi being an invalid float
|
||||
# ValueError or TypeError for dpi being an invalid float
|
||||
self.info["dpi"] = 72, 72
|
||||
|
||||
|
||||
|
|
|
@ -22,6 +22,7 @@ from . import Image, ImageFile, ImagePalette
|
|||
from ._binary import i8
|
||||
from ._binary import i16be as i16
|
||||
from ._binary import i32be as i32
|
||||
from ._binary import si16be as si16
|
||||
|
||||
MODES = {
|
||||
# (photoshop mode, bits) -> (pil mode, required channels)
|
||||
|
@ -179,7 +180,7 @@ def _layerinfo(fp, ct_bytes):
|
|||
def read(size):
|
||||
return ImageFile._safe_read(fp, size)
|
||||
|
||||
ct = i16(read(2))
|
||||
ct = si16(read(2))
|
||||
|
||||
# sanity check
|
||||
if ct_bytes < (abs(ct) * 20):
|
||||
|
|
|
@ -193,7 +193,8 @@ def _save(im, fp, filename):
|
|||
for channel in im.split():
|
||||
fp.write(channel.tobytes("raw", rawmode, 0, orientation))
|
||||
|
||||
fp.close()
|
||||
if hasattr(fp, "flush"):
|
||||
fp.flush()
|
||||
|
||||
|
||||
class SGI16Decoder(ImageFile.PyDecoder):
|
||||
|
|
|
@ -93,6 +93,7 @@ SUBIFD = 330
|
|||
EXTRASAMPLES = 338
|
||||
SAMPLEFORMAT = 339
|
||||
JPEGTABLES = 347
|
||||
YCBCRSUBSAMPLING = 530
|
||||
REFERENCEBLACKWHITE = 532
|
||||
COPYRIGHT = 33432
|
||||
IPTC_NAA_CHUNK = 33723 # newsphoto properties
|
||||
|
@ -1596,6 +1597,13 @@ def _save(im, fp, filename):
|
|||
# no compression by default:
|
||||
ifd[COMPRESSION] = COMPRESSION_INFO_REV.get(compression, 1)
|
||||
|
||||
if im.mode == "YCbCr":
|
||||
for tag, value in {
|
||||
YCBCRSUBSAMPLING: (1, 1),
|
||||
REFERENCEBLACKWHITE: (0, 255, 128, 255, 128, 255),
|
||||
}.items():
|
||||
ifd.setdefault(tag, value)
|
||||
|
||||
if libtiff:
|
||||
if "quality" in im.encoderinfo:
|
||||
quality = im.encoderinfo["quality"]
|
||||
|
|
|
@ -202,7 +202,7 @@ def _save_all(im, fp, filename):
|
|||
lossless = im.encoderinfo.get("lossless", False)
|
||||
quality = im.encoderinfo.get("quality", 80)
|
||||
method = im.encoderinfo.get("method", 0)
|
||||
icc_profile = im.encoderinfo.get("icc_profile", "")
|
||||
icc_profile = im.encoderinfo.get("icc_profile") or ""
|
||||
exif = im.encoderinfo.get("exif", "")
|
||||
if isinstance(exif, Image.Exif):
|
||||
exif = exif.tobytes()
|
||||
|
@ -309,7 +309,7 @@ def _save_all(im, fp, filename):
|
|||
def _save(im, fp, filename):
|
||||
lossless = im.encoderinfo.get("lossless", False)
|
||||
quality = im.encoderinfo.get("quality", 80)
|
||||
icc_profile = im.encoderinfo.get("icc_profile", "")
|
||||
icc_profile = im.encoderinfo.get("icc_profile") or ""
|
||||
exif = im.encoderinfo.get("exif", "")
|
||||
if isinstance(exif, Image.Exif):
|
||||
exif = exif.tobytes()
|
||||
|
|
|
@ -47,6 +47,16 @@ def si16le(c, o=0):
|
|||
return unpack_from("<h", c, o)[0]
|
||||
|
||||
|
||||
def si16be(c, o=0):
|
||||
"""
|
||||
Converts a 2-bytes (16 bits) string to a signed integer, big endian.
|
||||
|
||||
:param c: string containing bytes to convert
|
||||
:param o: offset of bytes to convert in string
|
||||
"""
|
||||
return unpack_from(">h", c, o)[0]
|
||||
|
||||
|
||||
def i32le(c, o=0):
|
||||
"""
|
||||
Converts a 4-bytes (32 bits) string to an unsigned integer.
|
||||
|
|
|
@ -808,6 +808,12 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) {
|
|||
av + stride * 2);
|
||||
free(av);
|
||||
}
|
||||
} else if (key_int == TIFFTAG_YCBCRSUBSAMPLING) {
|
||||
status = ImagingLibTiffSetField(
|
||||
&encoder->state,
|
||||
(ttag_t)key_int,
|
||||
(UINT16)PyLong_AsLong(PyTuple_GetItem(value, 0)),
|
||||
(UINT16)PyLong_AsLong(PyTuple_GetItem(value, 1)));
|
||||
} else if (type == TIFF_SHORT) {
|
||||
UINT16 *av;
|
||||
/* malloc check ok, calloc checks for overflow */
|
||||
|
|
|
@ -417,9 +417,16 @@ fill_mask_L(
|
|||
if (imOut->image8) {
|
||||
for (y = 0; y < ysize; y++) {
|
||||
UINT8 *out = imOut->image8[y + dy] + dx;
|
||||
if (strncmp(imOut->mode, "I;16", 4) == 0) {
|
||||
out += dx;
|
||||
}
|
||||
UINT8 *mask = imMask->image8[y + sy] + sx;
|
||||
for (x = 0; x < xsize; x++) {
|
||||
*out = BLEND(*mask, *out, ink[0], tmp1);
|
||||
if (strncmp(imOut->mode, "I;16", 4) == 0) {
|
||||
out++;
|
||||
*out = BLEND(*mask, *out, ink[0], tmp1);
|
||||
}
|
||||
out++, mask++;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user