Merge branch 'master' into eps

This commit is contained in:
Andrew Murray 2021-08-06 22:03:12 +10:00
commit 929c561937
35 changed files with 391 additions and 102 deletions

View File

@ -5,6 +5,39 @@ Changelog (Pillow)
8.4.0 (unreleased) 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 - Ensure TIFF RowsPerStrip is multiple of 8 for JPEG compression #5588
[kmilos, radarhere] [kmilos, radarhere]

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
Tests/images/zero_dpi.jp2 Normal file

Binary file not shown.

View File

@ -718,6 +718,15 @@ class TestFileJpeg:
# This should return the default, and not raise a ZeroDivisionError # This should return the default, and not raise a ZeroDivisionError
assert im.info.get("dpi") == (72, 72) 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): def test_no_dpi_in_exif(self):
# Arrange # Arrange
# This is photoshop-200dpi.jpg with resolution removed from EXIF: # This is photoshop-200dpi.jpg with resolution removed from EXIF:

View File

@ -4,7 +4,7 @@ from io import BytesIO
import pytest import pytest
from PIL import Image, ImageFile, Jpeg2KImagePlugin, features from PIL import Image, ImageFile, Jpeg2KImagePlugin, UnidentifiedImageError, features
from .helper import ( from .helper import (
assert_image_equal, assert_image_equal,
@ -151,6 +151,28 @@ def test_reduce():
assert im.size == (40, 30) 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): def test_layers_type(tmp_path):
outfile = str(tmp_path / "temp_layers.jp2") outfile = str(tmp_path / "temp_layers.jp2")
for quality_layers in [[100, 50, 10], (100, 50, 10), None]: for quality_layers in [[100, 50, 10], (100, 50, 10), None]:

View File

@ -670,6 +670,15 @@ class TestFileLibTiff(LibTiffTestCase):
TiffImagePlugin.WRITE_LIBTIFF = False TiffImagePlugin.WRITE_LIBTIFF = False
TiffImagePlugin.READ_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): def test_crashing_metadata(self, tmp_path):
# issue 1597 # issue 1597
with Image.open("Tests/images/rdf.tif") as im: with Image.open("Tests/images/rdf.tif") as im:

View File

@ -57,9 +57,10 @@ def test_n_frames():
assert im.n_frames == 1 assert im.n_frames == 1
assert not im.is_animated assert not im.is_animated
with Image.open(test_file) as im: for path in [test_file, "Tests/images/negative_layer_count.psd"]:
assert im.n_frames == 2 with Image.open(path) as im:
assert im.is_animated assert im.n_frames == 2
assert im.is_animated
def test_eoferror(): def test_eoferror():

View File

@ -73,6 +73,13 @@ def test_write(tmp_path):
img.save(out, format="sgi") img.save(out, format="sgi")
assert_image_equal_tofile(img, out) 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"): for mode in ("L", "RGB", "RGBA"):
roundtrip(hopper(mode)) roundtrip(hopper(mode))

View File

@ -104,6 +104,13 @@ class TestFileWebp:
hopper().save(buffer_method, format="WEBP", method=6) hopper().save(buffer_method, format="WEBP", method=6)
assert buffer_no_args.getbuffer() != buffer_method.getbuffer() 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): def test_write_unsupported_mode_L(self, tmp_path):
""" """
Saving a black-and-white file to WebP format should work, and be Saving a black-and-white file to WebP format should work, and be

View File

@ -42,10 +42,14 @@ def test_default():
im = hopper("P") im = hopper("P")
assert_image(im, "P", im.size) assert_image(im, "P", im.size)
im = im.convert() converted_im = im.convert()
assert_image(im, "RGB", im.size) assert_image(converted_im, "RGB", im.size)
im = im.convert() converted_im = im.convert()
assert_image(im, "RGB", im.size) 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 # ref https://github.com/python-pillow/Pillow/issues/274

View File

@ -33,6 +33,9 @@ def test_angle():
with Image.open("Tests/images/test-card.png") as im: with Image.open("Tests/images/test-card.png") as im:
rotate(im, im.mode, angle) rotate(im, im.mode, angle)
im = hopper()
assert_image_equal(im.rotate(angle), im.rotate(angle, expand=1))
def test_zero(): def test_zero():
for angle in (0, 45, 90, 180, 270): for angle in (0, 45, 90, 180, 270):

View File

@ -134,6 +134,17 @@ class TestImageFont:
target = "Tests/images/transparent_background_text_L.png" target = "Tests/images/transparent_background_text_L.png"
assert_image_similar_tofile(im.convert("L"), target, 0.01) 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): def test_textsize_equal(self):
im = Image.new(mode="RGB", size=(300, 100)) im = Image.new(mode="RGB", size=(300, 100))
draw = ImageDraw.Draw(im) draw = ImageDraw.Draw(im)

View File

@ -156,23 +156,31 @@ def test_scale():
assert newimg.size == (25, 25) assert newimg.size == (25, 25)
def test_expand_palette(): @pytest.mark.parametrize("border", (10, (1, 2, 3, 4)))
im = Image.open("Tests/images/p_16.tga") def test_expand_palette(border):
im_expanded = ImageOps.expand(im, 10, (255, 0, 0)) 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() if isinstance(border, int):
for b in range(10): left = top = right = bottom = border
else:
left, top, right, bottom = border
px = im_expanded.convert("RGB").load()
for x in range(im_expanded.width): for x in range(im_expanded.width):
assert px[x, b] == (255, 0, 0) for b in range(top):
assert px[x, im_expanded.height - 1 - b] == (255, 0, 0) 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): for y in range(im_expanded.height):
assert px[b, x] == (255, 0, 0) for b in range(left):
assert px[b, im_expanded.width - 1 - b] == (255, 0, 0) 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( im_cropped = im_expanded.crop(
(10, 10, im_expanded.width - 10, im_expanded.height - 10) (left, top, im_expanded.width - right, im_expanded.height - bottom)
) )
assert_image_equal(im_cropped, im) assert_image_equal(im_cropped, im)
def test_colorize_2color(): def test_colorize_2color():

View File

@ -10,8 +10,9 @@ def test_sanity():
palette = ImagePalette.ImagePalette("RGB", list(range(256)) * 3) palette = ImagePalette.ImagePalette("RGB", list(range(256)) * 3)
assert len(palette.colors) == 256 assert len(palette.colors) == 256
with pytest.raises(ValueError): with pytest.warns(DeprecationWarning):
ImagePalette.ImagePalette("RGB", list(range(256)) * 3, 10) with pytest.raises(ValueError):
ImagePalette.ImagePalette("RGB", list(range(256)) * 3, 10)
def test_reload(): def test_reload():

View File

@ -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 performs any operations on the data given to it, has been deprecated and will be
removed in Pillow 10.0.0 (2023-01-02). 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 Removed features
---------------- ----------------

View File

@ -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 | | 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 | | | | 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 | | Operating system | | Tested Python | | Latest tested | | Tested |
| | | versions | | Pillow version | | processors | | | | 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 | | | | 3.5 | 7.2.0 | |
+----------------------------------+---------------------------+------------------+--------------+ +----------------------------------+---------------------------+------------------+--------------+

View 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.

View File

@ -14,6 +14,7 @@ expected to be backported to earlier versions.
.. toctree:: .. toctree::
:maxdepth: 2 :maxdepth: 2
8.4.0
8.3.1 8.3.1
8.3.0 8.3.0
8.2.0 8.2.0

View File

@ -533,14 +533,16 @@ class pil_build_ext(build_ext):
_add_directory(include_dirs, "/usr/X11/include") _add_directory(include_dirs, "/usr/X11/include")
# SDK install path # SDK install path
try: sdk_path = "/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk"
sdk_path = ( if not os.path.exists(sdk_path):
subprocess.check_output(["xcrun", "--show-sdk-path"]) try:
.strip() sdk_path = (
.decode("latin1") subprocess.check_output(["xcrun", "--show-sdk-path"])
) .strip()
except Exception: .decode("latin1")
sdk_path = None )
except Exception:
sdk_path = None
if sdk_path: if sdk_path:
_add_directory(library_dirs, os.path.join(sdk_path, "usr", "lib")) _add_directory(library_dirs, os.path.join(sdk_path, "usr", "lib"))
_add_directory(include_dirs, os.path.join(sdk_path, "usr", "include")) _add_directory(include_dirs, os.path.join(sdk_path, "usr", "include"))

View File

@ -914,16 +914,18 @@ class Image:
self.load() self.load()
has_transparency = self.info.get("transparency") is not None
if not mode and self.mode == "P": if not mode and self.mode == "P":
# determine default mode # determine default mode
if self.palette: if self.palette:
mode = self.palette.mode mode = self.palette.mode
else: else:
mode = "RGB" mode = "RGB"
if mode == "RGB" and has_transparency:
mode = "RGBA"
if not mode or (mode == self.mode and not matrix): if not mode or (mode == self.mode and not matrix):
return self.copy() return self.copy()
has_transparency = self.info.get("transparency") is not None
if matrix: if matrix:
# matrix conversion # matrix conversion
if mode not in ("L", "RGB"): if mode not in ("L", "RGB"):
@ -2074,10 +2076,8 @@ class Image:
return self.copy() return self.copy()
if angle == 180: if angle == 180:
return self.transpose(ROTATE_180) return self.transpose(ROTATE_180)
if angle == 90 and expand: if angle in (90, 270) and (expand or self.width == self.height):
return self.transpose(ROTATE_90) return self.transpose(ROTATE_90 if angle == 90 else ROTATE_270)
if angle == 270 and expand:
return self.transpose(ROTATE_270)
# Calculate the affine matrix. Note that this is the reverse # Calculate the affine matrix. Note that this is the reverse
# transformation (from destination image to source) because we # transformation (from destination image to source) because we

View File

@ -21,7 +21,7 @@ import functools
import operator import operator
import re import re
from . import Image, ImageDraw from . import Image
# #
# helpers # helpers
@ -395,15 +395,16 @@ def expand(image, border=0, fill=0):
height = top + image.size[1] + bottom height = top + image.size[1] + bottom
color = _color(fill, image.mode) color = _color(fill, image.mode)
if image.mode == "P" and image.palette: if image.mode == "P" and image.palette:
out = Image.new(image.mode, (width, height)) image.load()
out.putpalette(image.palette) palette = image.palette.copy()
out.paste(image, (left, top)) if isinstance(color, tuple):
color = palette.getcolor(color)
draw = ImageDraw.Draw(out)
draw.rectangle((0, 0, width - 1, height - 1), outline=color, width=border)
else: else:
out = Image.new(image.mode, (width, height), color) palette = None
out.paste(image, (left, top)) out = Image.new(image.mode, (width, height), color)
if palette:
out.putpalette(palette.palette)
out.paste(image, (left, top))
return out return out

View File

@ -17,6 +17,7 @@
# #
import array import array
import warnings
from . import GimpGradientFile, GimpPaletteFile, ImageColor, PaletteFile from . import GimpGradientFile, GimpPaletteFile, ImageColor, PaletteFile
@ -28,12 +29,11 @@ class ImagePalette:
:param mode: The mode to use for the Palette. See: :param mode: The mode to use for the Palette. See:
:ref:`concept-modes`. Defaults to "RGB" :ref:`concept-modes`. Defaults to "RGB"
:param palette: An optional palette. If given, it must be a bytearray, :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`` an array or a list of ints between 0-255. The list must be aligned
times the number of colors in ``mode``. The list must be aligned
by channel (All R values must be contiguous in the list before G by channel (All R values must be contiguous in the list before G
and B values.) Defaults to 0 through 255 per channel. and B values.) Defaults to 0 through 255 per channel.
:param size: An optional palette size. If given, it cannot be equal to :param size: An optional palette size. If given, an error is raised
or greater than 256. Defaults to 0. if ``palette`` is not of equal length.
""" """
def __init__(self, mode="RGB", palette=None, size=0): 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.rawmode = None # if set, palette contains raw data
self.palette = palette or bytearray() self.palette = palette or bytearray()
self.dirty = None self.dirty = None
if size != 0 and size != len(self.palette): if size != 0:
raise ValueError("wrong palette size") 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 @property
def palette(self): def palette(self):

View File

@ -6,6 +6,7 @@
# #
# History: # History:
# 2014-03-12 ajh Created # 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 Coriolis Systems Limited
# Copyright (c) 2014 Alastair Houghton # Copyright (c) 2014 Alastair Houghton
@ -19,6 +20,79 @@ import struct
from . import Image, ImageFile 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): def _parse_codestream(fp):
"""Parse the JPEG 2000 codestream to extract the size and component """Parse the JPEG 2000 codestream to extract the size and component
count from the SIZ marker segment, returning a PIL (size, mode) tuple.""" count from the SIZ marker segment, returning a PIL (size, mode) tuple."""
@ -53,55 +127,45 @@ def _parse_codestream(fp):
return (size, mode) 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): def _parse_jp2_header(fp):
"""Parse the JP2 header box to extract size, component count and """Parse the JP2 header box to extract size, component count,
color space information, returning a (size, mode, mimetype) tuple.""" color space information, and optionally DPI information,
returning a (size, mode, mimetype, dpi) tuple."""
# Find the JP2 header box # Find the JP2 header box
reader = BoxReader(fp)
header = None header = None
mimetype = None mimetype = None
while True: while reader.has_next_box():
lbox, tbox = struct.unpack(">I4s", fp.read(8)) tbox = reader.next_box_type()
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")
if tbox == b"jp2h": if tbox == b"jp2h":
header = fp.read(lbox - hlen) header = reader.read_boxes()
break break
elif tbox == b"ftyp": elif tbox == b"ftyp":
if fp.read(4) == b"jpx ": if reader.read_fields(">4s")[0] == b"jpx ":
mimetype = "image/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 size = None
mode = None mode = None
bpc = None bpc = None
nc = None nc = None
dpi = None # 2-tuple of DPI info, or None
unkc = 0 # Colorspace information unknown
hio = io.BytesIO(header) while header.has_next_box():
while True: tbox = header.next_box_type()
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)
if tbox == b"ihdr": 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) size = (width, height)
if unkc: if unkc:
if nc == 1 and (bpc & 0x7F) > 8: if nc == 1 and (bpc & 0x7F) > 8:
@ -114,11 +178,10 @@ def _parse_jp2_header(fp):
mode = "RGB" mode = "RGB"
elif nc == 4: elif nc == 4:
mode = "RGBA" mode = "RGBA"
break
elif tbox == b"colr": elif tbox == b"colr":
meth, prec, approx = struct.unpack_from(">BBB", content) meth, prec, approx = header.read_fields(">BBB")
if meth == 1: if meth == 1 and unkc == 0:
cs = struct.unpack_from(">I", content, 3)[0] cs = header.read_fields(">I")[0]
if cs == 16: # sRGB if cs == 16: # sRGB
if nc == 1 and (bpc & 0x7F) > 8: if nc == 1 and (bpc & 0x7F) > 8:
mode = "I;16" mode = "I;16"
@ -128,7 +191,6 @@ def _parse_jp2_header(fp):
mode = "RGB" mode = "RGB"
elif nc == 4: elif nc == 4:
mode = "RGBA" mode = "RGBA"
break
elif cs == 17: # grayscale elif cs == 17: # grayscale
if nc == 1 and (bpc & 0x7F) > 8: if nc == 1 and (bpc & 0x7F) > 8:
mode = "I;16" mode = "I;16"
@ -136,18 +198,27 @@ def _parse_jp2_header(fp):
mode = "L" mode = "L"
elif nc == 2: elif nc == 2:
mode = "LA" mode = "LA"
break
elif cs == 18: # sYCC elif cs == 18: # sYCC
if nc == 3: if nc == 3:
mode = "RGB" mode = "RGB"
elif nc == 4: elif nc == 4:
mode = "RGBA" 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 break
if size is None or mode is None: 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": if sig == b"\x00\x00\x00\x0cjP \x0d\x0a\x87\x0a":
self.codec = "jp2" self.codec = "jp2"
header = _parse_jp2_header(self.fp) 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: else:
raise SyntaxError("not a JPEG 2000 file") raise SyntaxError("not a JPEG 2000 file")

View File

@ -168,11 +168,11 @@ def APP(self, marker):
# 1 dpcm = 2.54 dpi # 1 dpcm = 2.54 dpi
dpi *= 2.54 dpi *= 2.54
self.info["dpi"] = dpi, dpi self.info["dpi"] = dpi, dpi
except (KeyError, SyntaxError, ValueError, ZeroDivisionError): except (TypeError, KeyError, SyntaxError, ValueError, ZeroDivisionError):
# SyntaxError for invalid/unreadable EXIF # SyntaxError for invalid/unreadable EXIF
# KeyError for dpi not included # KeyError for dpi not included
# ZeroDivisionError for invalid dpi rational value # 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 self.info["dpi"] = 72, 72

View File

@ -22,6 +22,7 @@ from . import Image, ImageFile, ImagePalette
from ._binary import i8 from ._binary import i8
from ._binary import i16be as i16 from ._binary import i16be as i16
from ._binary import i32be as i32 from ._binary import i32be as i32
from ._binary import si16be as si16
MODES = { MODES = {
# (photoshop mode, bits) -> (pil mode, required channels) # (photoshop mode, bits) -> (pil mode, required channels)
@ -179,7 +180,7 @@ def _layerinfo(fp, ct_bytes):
def read(size): def read(size):
return ImageFile._safe_read(fp, size) return ImageFile._safe_read(fp, size)
ct = i16(read(2)) ct = si16(read(2))
# sanity check # sanity check
if ct_bytes < (abs(ct) * 20): if ct_bytes < (abs(ct) * 20):

View File

@ -193,7 +193,8 @@ def _save(im, fp, filename):
for channel in im.split(): for channel in im.split():
fp.write(channel.tobytes("raw", rawmode, 0, orientation)) fp.write(channel.tobytes("raw", rawmode, 0, orientation))
fp.close() if hasattr(fp, "flush"):
fp.flush()
class SGI16Decoder(ImageFile.PyDecoder): class SGI16Decoder(ImageFile.PyDecoder):

View File

@ -93,6 +93,7 @@ SUBIFD = 330
EXTRASAMPLES = 338 EXTRASAMPLES = 338
SAMPLEFORMAT = 339 SAMPLEFORMAT = 339
JPEGTABLES = 347 JPEGTABLES = 347
YCBCRSUBSAMPLING = 530
REFERENCEBLACKWHITE = 532 REFERENCEBLACKWHITE = 532
COPYRIGHT = 33432 COPYRIGHT = 33432
IPTC_NAA_CHUNK = 33723 # newsphoto properties IPTC_NAA_CHUNK = 33723 # newsphoto properties
@ -1596,6 +1597,13 @@ def _save(im, fp, filename):
# no compression by default: # no compression by default:
ifd[COMPRESSION] = COMPRESSION_INFO_REV.get(compression, 1) 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 libtiff:
if "quality" in im.encoderinfo: if "quality" in im.encoderinfo:
quality = im.encoderinfo["quality"] quality = im.encoderinfo["quality"]

View File

@ -202,7 +202,7 @@ def _save_all(im, fp, filename):
lossless = im.encoderinfo.get("lossless", False) lossless = im.encoderinfo.get("lossless", False)
quality = im.encoderinfo.get("quality", 80) quality = im.encoderinfo.get("quality", 80)
method = im.encoderinfo.get("method", 0) 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", "") exif = im.encoderinfo.get("exif", "")
if isinstance(exif, Image.Exif): if isinstance(exif, Image.Exif):
exif = exif.tobytes() exif = exif.tobytes()
@ -309,7 +309,7 @@ def _save_all(im, fp, filename):
def _save(im, fp, filename): def _save(im, fp, filename):
lossless = im.encoderinfo.get("lossless", False) lossless = im.encoderinfo.get("lossless", False)
quality = im.encoderinfo.get("quality", 80) 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", "") exif = im.encoderinfo.get("exif", "")
if isinstance(exif, Image.Exif): if isinstance(exif, Image.Exif):
exif = exif.tobytes() exif = exif.tobytes()

View File

@ -47,6 +47,16 @@ def si16le(c, o=0):
return unpack_from("<h", 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): def i32le(c, o=0):
""" """
Converts a 4-bytes (32 bits) string to an unsigned integer. Converts a 4-bytes (32 bits) string to an unsigned integer.

View File

@ -808,6 +808,12 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) {
av + stride * 2); av + stride * 2);
free(av); 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) { } else if (type == TIFF_SHORT) {
UINT16 *av; UINT16 *av;
/* malloc check ok, calloc checks for overflow */ /* malloc check ok, calloc checks for overflow */

View File

@ -417,9 +417,16 @@ fill_mask_L(
if (imOut->image8) { if (imOut->image8) {
for (y = 0; y < ysize; y++) { for (y = 0; y < ysize; y++) {
UINT8 *out = imOut->image8[y + dy] + dx; UINT8 *out = imOut->image8[y + dy] + dx;
if (strncmp(imOut->mode, "I;16", 4) == 0) {
out += dx;
}
UINT8 *mask = imMask->image8[y + sy] + sx; UINT8 *mask = imMask->image8[y + sy] + sx;
for (x = 0; x < xsize; x++) { for (x = 0; x < xsize; x++) {
*out = BLEND(*mask, *out, ink[0], tmp1); *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++; out++, mask++;
} }
} }