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)
------------------
- 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]

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
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:

View File

@ -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]:

View File

@ -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:

View File

@ -57,7 +57,8 @@ def test_n_frames():
assert im.n_frames == 1
assert not im.is_animated
with Image.open(test_file) as im:
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

View File

@ -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))

View File

@ -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

View File

@ -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

View File

@ -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):

View File

@ -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)

View File

@ -156,21 +156,29 @@ 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))
if isinstance(border, int):
left = top = right = bottom = border
else:
left, top, right, bottom = border
px = im_expanded.convert("RGB").load()
for b in range(10):
for x in range(im_expanded.width):
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)
(left, top, im_expanded.width - right, im_expanded.height - bottom)
)
assert_image_equal(im_cropped, im)

View File

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

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

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 |
+======================+=====+=====+=====+=====+=====+=====+=====+=====+
| 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 | |
+----------------------------------+---------------------------+------------------+--------------+

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::
:maxdepth: 2
8.4.0
8.3.1
8.3.0
8.2.0

View File

@ -533,6 +533,8 @@ class pil_build_ext(build_ext):
_add_directory(include_dirs, "/usr/X11/include")
# SDK install path
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"])

View File

@ -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

View File

@ -21,7 +21,7 @@ import functools
import operator
import re
from . import Image, ImageDraw
from . import Image
#
# helpers
@ -395,14 +395,15 @@ 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:
palette = None
out = Image.new(image.mode, (width, height), color)
if palette:
out.putpalette(palette.palette)
out.paste(image, (left, top))
return out

View File

@ -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,7 +41,13 @@ 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):
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

View File

@ -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")

View 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

View File

@ -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):

View File

@ -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):

View File

@ -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"]

View File

@ -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()

View File

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

View File

@ -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 */

View File

@ -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++;
}
}