Merge branch 'master' into patch-1

This commit is contained in:
Hugo van Kemenade 2019-09-20 22:59:29 +03:00 committed by GitHub
commit f5aed1a254
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
59 changed files with 940 additions and 212 deletions

29
.github/workflows/lint.yml vendored Normal file
View File

@ -0,0 +1,29 @@
name: Lint
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
python: [3.7]
name: Python ${{ matrix.python }}
steps:
- uses: actions/checkout@v1
- name: Set up Python ${{ matrix.python }}
uses: actions/setup-python@v1
with:
python-version: ${{ matrix.python }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install --upgrade tox
- name: Lint
run: tox -e lint

View File

@ -7,13 +7,37 @@ Changelog (Pillow)
- This is the last Pillow release to support Python 2.7 #3642
- Lazily use ImageFileDirectory_v1 values from Exif #4031
[radarhere]
- Improved HSV conversion #4004
[radarhere]
- Added text stroking #3978
[radarhere, hugovk]
- No more deprecated bdist_wininst .exe installers #4029
[hugovk]
- Do not allow floodfill to extend into negative coordinates #4017
[radarhere]
- Fixed arc drawing bug for a non-whole number of degrees #4014
[radarhere]
- Fix bug when merging identical images to GIF with a list of durations #4003
[djy0, radarhere]
- Fix bug in TIFF loading of BufferedReader #3998
[chadawagner]
- Added fallback for finding ld on MinGW Cygwin #4019
[radarhere]
- Remove indirect dependencies from requirements.txt #3976
[hugovk]
- Depends: Update libwebp to 1.0.3 #3983, libimagequant to 2.12.5 #3993
- Depends: Update libwebp to 1.0.3 #3983, libimagequant to 2.12.5 #3993, freetype to 2.10.1 #3991
[radarhere]
- Change overflow check to use PY_SSIZE_T_MAX #3964
@ -64,7 +88,7 @@ Changelog (Pillow)
- Updated TIFF tile descriptors to match current decoding functionality #3795
[dmnisson]
- Added an `image.entropy()` method (second revision) #3608
- Added an ``image.entropy()`` method (second revision) #3608
[fish2000]
- Pass the correct types to PyArg_ParseTuple #3880
@ -700,7 +724,7 @@ Changelog (Pillow)
- Enable background colour parameter on rotate #3057
[storesource]
- Remove unnecessary `#if 1` directive #3072
- Remove unnecessary ``#if 1`` directive #3072
[jdufresne]
- Remove unused Python class, Path #3070
@ -1237,7 +1261,7 @@ Changelog (Pillow)
- Add decompression bomb check to Image.crop #2410
[wiredfool]
- ImageFile: Ensure that the `err_code` variable is initialized in case of exception. #2363
- ImageFile: Ensure that the ``err_code`` variable is initialized in case of exception. #2363
[alexkiro]
- Tiff: Support append_images for saving multipage TIFFs #2406
@ -1474,7 +1498,7 @@ Changelog (Pillow)
- Removed PIL 1.0 era TK readme that concerns Windows 95/NT #2360
[wiredfool]
- Prevent `nose -v` printing docstrings #2369
- Prevent ``nose -v`` printing docstrings #2369
[hugovk]
- Replaced absolute PIL imports with relative imports #2349
@ -1919,7 +1943,7 @@ Changelog (Pillow)
- Changed depends/install_*.sh urls to point to github pillow-depends repo #1983
[wiredfool]
- Allow ICC profile from `encoderinfo` while saving PNGs #1909
- Allow ICC profile from ``encoderinfo`` while saving PNGs #1909
[homm]
- Fix integer overflow on ILP32 systems (32-bit Linux). #1975
@ -2362,7 +2386,7 @@ Changelog (Pillow)
- Added PDF multipage saving #1445
[radarhere]
- Removed deprecated code, Image.tostring, Image.fromstring, Image.offset, ImageDraw.setink, ImageDraw.setfill, ImageFileIO, ImageFont.FreeTypeFont and ImageFont.truetype `file` kwarg, ImagePalette private _make functions, ImageWin.fromstring and ImageWin.tostring #1343
- Removed deprecated code, Image.tostring, Image.fromstring, Image.offset, ImageDraw.setink, ImageDraw.setfill, ImageFileIO, ImageFont.FreeTypeFont and ImageFont.truetype ``file`` kwarg, ImagePalette private _make functions, ImageWin.fromstring and ImageWin.tostring #1343
[radarhere]
- Load more broken images #1428
@ -2854,7 +2878,7 @@ Changelog (Pillow)
- Doc cleanup
[wiredfool]
- Fix `ImageStat` docs #796
- Fix ``ImageStat`` docs #796
[akx]
- Added docs for ExifTags #794
@ -3291,7 +3315,7 @@ Changelog (Pillow)
- Add RGBA support to ImageColor #309
[yoavweiss]
- Test for `str`, not `"utf-8"` #306 (fixes #304)
- Test for ``str``, not ``"utf-8"`` #306 (fixes #304)
[mjpieters]
- Fix missing import os in _util.py #303
@ -3397,7 +3421,7 @@ Changelog (Pillow)
- Partial work to add a wrapper for WebPGetFeatures to correctly support #220 (fixes #204)
- Significant performance improvement of `alpha_composite` function #156
- Significant performance improvement of ``alpha_composite`` function #156
[homm]
- Support explicitly disabling features via --disable-* options #240

View File

@ -67,7 +67,7 @@ To report a security vulnerability, please follow the procedure described in the
.. |zenodo| image:: https://zenodo.org/badge/17549/python-pillow/Pillow.svg
:target: https://zenodo.org/badge/latestdoi/17549/python-pillow/Pillow
.. |tidelift| image:: https://tidelift.com/badges/github/python-pillow/Pillow?style=flat
.. |tidelift| image:: https://tidelift.com/badges/package/pypi/Pillow?style=flat
:target: https://tidelift.com/subscription/pkg/pypi-pillow?utm_source=pypi-pillow&utm_medium=referral&utm_campaign=readme
.. |version| image:: https://img.shields.io/pypi/v/pillow.svg

BIN
Tests/images/g4_orientation_1.tif Executable file

Binary file not shown.

BIN
Tests/images/g4_orientation_2.tif Executable file

Binary file not shown.

BIN
Tests/images/g4_orientation_3.tif Executable file

Binary file not shown.

BIN
Tests/images/g4_orientation_4.tif Executable file

Binary file not shown.

BIN
Tests/images/g4_orientation_5.tif Executable file

Binary file not shown.

BIN
Tests/images/g4_orientation_6.tif Executable file

Binary file not shown.

BIN
Tests/images/g4_orientation_7.tif Executable file

Binary file not shown.

BIN
Tests/images/g4_orientation_8.tif Executable file

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 439 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 214 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

@ -495,6 +495,26 @@ class TestFileGif(PillowTestCase):
# Assert that the new duration is the total of the identical frames
self.assertEqual(reread.info["duration"], 4500)
def test_identical_frames_to_single_frame(self):
for duration in ([1000, 1500, 2000, 4000], (1000, 1500, 2000, 4000), 8500):
out = self.tempfile("temp.gif")
im_list = [
Image.new("L", (100, 100), "#000"),
Image.new("L", (100, 100), "#000"),
Image.new("L", (100, 100), "#000"),
]
im_list[0].save(
out, save_all=True, append_images=im_list[1:], duration=duration
)
reread = Image.open(out)
# Assert that all frames were combined
self.assertEqual(reread.n_frames, 1)
# Assert that the new duration is the total of the identical frames
self.assertEqual(reread.info["duration"], 8500)
def test_number_of_loops(self):
number_of_loops = 2

View File

@ -369,6 +369,10 @@ class TestFileJpeg(PillowTestCase):
with self.assertRaises(IOError):
im.load()
# Test that the error is raised if loaded a second time
with self.assertRaises(IOError):
im.load()
def _n_qtables_helper(self, n, test_file):
im = Image.open(test_file)
f = self.tempfile("temp.jpg")

View File

@ -91,6 +91,12 @@ class TestFileJpeg2k(PillowTestCase):
)
self.assert_image_equal(im, test_card)
def test_tiled_offset_too_small(self):
with self.assertRaises(ValueError):
self.roundtrip(
test_card, tile_size=(128, 128), tile_offset=(0, 0), offset=(128, 32)
)
def test_irreversible_rt(self):
im = self.roundtrip(test_card, irreversible=True, quality_layers=[20])
self.assert_image_similar(im, test_card, 2.0)

View File

@ -81,6 +81,19 @@ class TestFileLibTiff(LibTiffTestCase):
self.assertEqual(im.size, (500, 500))
self._assert_noerr(im)
def test_g4_non_disk_file_object(self):
"""Testing loading from non-disk non-BytesIO file object"""
test_file = "Tests/images/hopper_g4_500.tif"
s = io.BytesIO()
with open(test_file, "rb") as f:
s.write(f.read())
s.seek(0)
r = io.BufferedReader(s)
im = Image.open(r)
self.assertEqual(im.size, (500, 500))
self._assert_noerr(im)
def test_g4_eq_png(self):
""" Checking that we're actually getting the data that we expect"""
png = Image.open("Tests/images/hopper_bw_500.png")
@ -825,3 +838,12 @@ class TestFileLibTiff(LibTiffTestCase):
im = Image.open(infile)
im.load()
self.assertEqual(im.size, (950, 975))
def test_orientation(self):
base_im = Image.open("Tests/images/g4_orientation_1.tif")
for i in range(2, 9):
im = Image.open("Tests/images/g4_orientation_" + str(i) + ".tif")
im.load()
self.assert_image_similar(base_im, im, 0.7)

View File

@ -222,7 +222,7 @@ class TestFileTiffMetadata(PillowTestCase):
self.assertEqual(0, reloaded.tag_v2[41988].numerator)
self.assertEqual(0, reloaded.tag_v2[41988].denominator)
def test_expty_values(self):
def test_empty_values(self):
data = io.BytesIO(
b"II*\x00\x08\x00\x00\x00\x03\x00\x1a\x01\x05\x00\x00\x00\x00\x00"
b"\x00\x00\x00\x00\x1b\x01\x05\x00\x00\x00\x00\x00\x00\x00\x00\x00"
@ -239,11 +239,13 @@ class TestFileTiffMetadata(PillowTestCase):
def test_PhotoshopInfo(self):
im = Image.open("Tests/images/issue_2278.tif")
self.assertIsInstance(im.tag_v2[34377], bytes)
self.assertEqual(len(im.tag_v2[34377]), 1)
self.assertIsInstance(im.tag_v2[34377][0], bytes)
out = self.tempfile("temp.tiff")
im.save(out)
reloaded = Image.open(out)
self.assertIsInstance(reloaded.tag_v2[34377], bytes)
self.assertEqual(len(reloaded.tag_v2[34377]), 1)
self.assertIsInstance(reloaded.tag_v2[34377][0], bytes)
def test_too_many_entries(self):
ifd = TiffImagePlugin.ImageFileDirectory_v2()

View File

@ -23,6 +23,7 @@ class TestImageConvert(PillowTestCase):
"RGBX",
"CMYK",
"YCbCr",
"HSV",
)
for mode in modes:

View File

@ -1,8 +1,8 @@
import os.path
from PIL import Image, ImageColor, ImageDraw
from PIL import Image, ImageColor, ImageDraw, ImageFont, features
from .helper import PillowTestCase, hopper
from .helper import PillowTestCase, hopper, unittest
BLACK = (0, 0, 0)
WHITE = (255, 255, 255)
@ -29,6 +29,8 @@ POINTS2 = [10, 10, 20, 40, 30, 30]
KITE_POINTS = [(10, 50), (70, 10), (90, 50), (70, 90), (10, 50)]
HAS_FREETYPE = features.check("freetype2")
class TestImageDraw(PillowTestCase):
def test_sanity(self):
@ -140,6 +142,18 @@ class TestImageDraw(PillowTestCase):
# Assert
self.assert_image_similar(im, Image.open(expected), 1)
def test_arc_width_non_whole_angle(self):
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
expected = "Tests/images/imagedraw_arc_width_non_whole_angle.png"
# Act
draw.arc(BBOX1, 10, 259.5, width=5)
# Assert
self.assert_image_similar(im, Image.open(expected), 1)
def test_bitmap(self):
# Arrange
small = Image.open("Tests/images/pil123rgba.png").resize((50, 50))
@ -559,6 +573,24 @@ class TestImageDraw(PillowTestCase):
# Assert
self.assert_image_equal(im, Image.open("Tests/images/imagedraw_floodfill2.png"))
def test_floodfill_not_negative(self):
# floodfill() is experimental
# Test that floodfill does not extend into negative coordinates
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
draw.line((W / 2, 0, W / 2, H / 2), fill="green")
draw.line((0, H / 2, W / 2, H / 2), fill="green")
# Act
ImageDraw.floodfill(im, (int(W / 4), int(H / 4)), ImageColor.getrgb("red"))
# Assert
self.assert_image_equal(
im, Image.open("Tests/images/imagedraw_floodfill_not_negative.png")
)
def create_base_image_draw(
self, size, mode=DEFAULT_MODE, background1=WHITE, background2=GRAY
):
@ -771,6 +803,54 @@ class TestImageDraw(PillowTestCase):
draw.textsize("\n")
draw.textsize("test\n")
@unittest.skipUnless(HAS_FREETYPE, "ImageFont not available")
def test_textsize_stroke(self):
# Arrange
im = Image.new("RGB", (W, H))
draw = ImageDraw.Draw(im)
font = ImageFont.truetype("Tests/fonts/FreeMono.ttf", 20)
# Act / Assert
self.assertEqual(draw.textsize("A", font, stroke_width=2), (16, 20))
self.assertEqual(
draw.multiline_textsize("ABC\nAaaa", font, stroke_width=2), (52, 44)
)
@unittest.skipUnless(HAS_FREETYPE, "ImageFont not available")
def test_stroke(self):
for suffix, stroke_fill in {"same": None, "different": "#0f0"}.items():
# Arrange
im = Image.new("RGB", (120, 130))
draw = ImageDraw.Draw(im)
font = ImageFont.truetype("Tests/fonts/FreeMono.ttf", 120)
# Act
draw.text(
(10, 10), "A", "#f00", font, stroke_width=2, stroke_fill=stroke_fill
)
# Assert
self.assert_image_similar(
im, Image.open("Tests/images/imagedraw_stroke_" + suffix + ".png"), 3.1
)
@unittest.skipUnless(HAS_FREETYPE, "ImageFont not available")
def test_stroke_multiline(self):
# Arrange
im = Image.new("RGB", (100, 250))
draw = ImageDraw.Draw(im)
font = ImageFont.truetype("Tests/fonts/FreeMono.ttf", 120)
# Act
draw.multiline_text(
(10, 10), "A\nB", "#f00", font, stroke_width=2, stroke_fill="#0f0"
)
# Assert
self.assert_image_similar(
im, Image.open("Tests/images/imagedraw_stroke_multiline.png"), 3.3
)
def test_same_color_outline(self):
# Prepare shape
x0, y0 = 5, 5

View File

@ -111,6 +111,10 @@ class TestImageFile(PillowTestCase):
with self.assertRaises(IOError):
im.load()
# Test that the error is raised if loaded a second time
with self.assertRaises(IOError):
im.load()
def test_truncated_without_errors(self):
if "zip_encoder" not in codecs:
self.skipTest("PNG (zlib) encoder not available")
@ -321,3 +325,13 @@ class TestPyDecoder(PillowTestCase):
self.assertEqual(
exif.get_ifd(0xA005), {1: "R98", 2: b"0100", 4097: 2272, 4098: 1704}
)
def test_exif_shared(self):
im = Image.open("Tests/images/exif.png")
exif = im.getexif()
self.assertIs(im.getexif(), exif)
def test_exif_str(self):
im = Image.open("Tests/images/exif.png")
exif = im.getexif()
self.assertEqual(str(exif), "{274: 1}")

View File

@ -605,6 +605,21 @@ class TestImageFont(PillowTestCase):
self.assertEqual(t.getsize_multiline("ABC\nA"), (36, 36))
self.assertEqual(t.getsize_multiline("ABC\nAaaa"), (48, 36))
def test_getsize_stroke(self):
# Arrange
t = self.get_font()
# Act / Assert
for stroke_width in [0, 2]:
self.assertEqual(
t.getsize("A", stroke_width=stroke_width),
(12 + stroke_width * 2, 16 + stroke_width * 2),
)
self.assertEqual(
t.getsize_multiline("ABC\nAaaa", stroke_width=stroke_width),
(48 + stroke_width * 2, 36 + stroke_width * 4),
)
def test_complex_font_settings(self):
# Arrange
t = self.get_font()

View File

@ -115,6 +115,30 @@ class TestImagecomplextext(PillowTestCase):
self.assert_image_similar(im, target_img, 1.15)
def test_text_direction_ttb_stroke(self):
ttf = ImageFont.truetype("Tests/fonts/NotoSansJP-Regular.otf", 50)
im = Image.new(mode="RGB", size=(100, 300))
draw = ImageDraw.Draw(im)
try:
draw.text(
(25, 25),
"あい",
font=ttf,
fill=500,
direction="ttb",
stroke_width=2,
stroke_fill="#0f0",
)
except ValueError as ex:
if str(ex) == "libraqm 0.7 or greater required for 'ttb' direction":
self.skipTest("libraqm 0.7 or greater not available")
target = "Tests/images/test_direction_ttb_stroke.png"
target_img = Image.open(target)
self.assert_image_similar(im, target_img, 12.4)
def test_ligature_features(self):
ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE)

View File

@ -44,11 +44,24 @@ supports the following standard modes:
* ``I`` (32-bit signed integer pixels)
* ``F`` (32-bit floating point pixels)
PIL also provides limited support for a few special modes, including ``LA`` (L
with alpha), ``RGBX`` (true color with padding) and ``RGBa`` (true color with
premultiplied alpha). However, PIL doesnt support user-defined modes; if you
need to handle band combinations that are not listed above, use a sequence of
Image objects.
Pillow also provides limited support for a few special modes, including:
* ``LA`` (L with alpha)
* ``PA`` (P with alpha)
* ``RGBX`` (true color with padding)
* ``RGBa`` (true color with premultiplied alpha)
* ``La`` (L with premultiplied alpha)
* ``I;16`` (16-bit unsigned integer pixels)
* ``I;16L`` (16-bit little endian unsigned integer pixels)
* ``I;16B`` (16-bit big endian unsigned integer pixels)
* ``I;16N`` (16-bit native endian unsigned integer pixels)
* ``BGR;15`` (15-bit reversed true colour)
* ``BGR;16`` (16-bit reversed true colour)
* ``BGR;24`` (24-bit reversed true colour)
* ``BGR;32`` (32-bit reversed true colour)
However, Pillow doesnt support user-defined modes; if you need to handle band
combinations that are not listed above, use a sequence of Image objects.
You can read the mode of an image through the :py:attr:`~PIL.Image.Image.mode`
attribute. This is a string containing one of the above values.

View File

@ -389,12 +389,12 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options:
image will be saved without tiling.
**quality_mode**
Either `"rates"` or `"dB"` depending on the units you want to use to
Either ``"rates"`` or ``"dB"`` depending on the units you want to use to
specify image quality.
**quality_layers**
A sequence of numbers, each of which represents either an approximate size
reduction (if quality mode is `"rates"`) or a signal to noise ratio value
reduction (if quality mode is ``"rates"``) or a signal to noise ratio value
in decibels. If not specified, defaults to a single layer of full quality.
**num_resolutions**
@ -811,10 +811,10 @@ Saving sequences
Support for animated WebP files will only be enabled if the system WebP
library is v0.5.0 or later. You can check webp animation support at
runtime by calling `features.check("webp_anim")`.
runtime by calling ``features.check("webp_anim")``.
When calling :py:meth:`~PIL.Image.Image.save`, the following options
are available when the `save_all` argument is present and true.
are available when the ``save_all`` argument is present and true.
**append_images**
A list of images to append as additional frames. Each of the

View File

@ -247,7 +247,7 @@ Transposing an image
out = im.transpose(Image.ROTATE_270)
``transpose(ROTATE)`` operations can also be performed identically with
:py:meth:`~PIL.Image.Image.rotate` operations, provided the `expand` flag is
:py:meth:`~PIL.Image.Image.rotate` operations, provided the ``expand`` flag is
true, to provide for the same changes to the image's size.
A more general form of image transformations can be carried out via the

View File

@ -3,6 +3,8 @@ Pillow
Pillow is the friendly PIL fork by `Alex Clark and Contributors <https://github.com/python-pillow/Pillow/graphs/contributors>`_. PIL is the Python Imaging Library by Fredrik Lundh and Contributors.
Pillow for enterprise is available via the Tidelift Subscription. `Learn more <https://tidelift.com/subscription/pkg/pypi-pillow?utm_source=pillow&utm_medium=referral&utm_campaign=docs>`_.
.. image:: https://zenodo.org/badge/17549/python-pillow/Pillow.svg
:target: https://zenodo.org/badge/latestdoi/17549/python-pillow/Pillow

View File

@ -52,7 +52,7 @@ Functions
.. warning::
To protect against potential DOS attacks caused by "`decompression bombs`_" (i.e. malicious files
which decompress into a huge amount of data and are designed to crash or cause disruption by using up
a lot of memory), Pillow will issue a `DecompressionBombWarning` if the image is over a certain
a lot of memory), Pillow will issue a ``DecompressionBombWarning`` if the image is over a certain
limit. If desired, the warning can be turned into an error with
``warnings.simplefilter('error', Image.DecompressionBombWarning)`` or suppressed entirely with
``warnings.simplefilter('ignore', Image.DecompressionBombWarning)``. See also `the logging
@ -262,10 +262,10 @@ Instances of the :py:class:`Image` class have the following attributes:
.. py:attribute:: filename
The filename or path of the source file. Only images created with the
factory function `open` have a filename attribute. If the input is a
factory function ``open`` have a filename attribute. If the input is a
file like object, the filename attribute is set to an empty string.
:type: :py:class: `string`
:type: :py:class:`string`
.. py:attribute:: format

View File

@ -33,13 +33,13 @@ can be easily displayed in a chromaticity diagram, for example).
.. py:attribute:: version
The version number of the ICC standard that this profile follows
(e.g. `2.0`).
(e.g. ``2.0``).
:type: :py:class:`float`
.. py:attribute:: icc_version
Same as `version`, but in encoded format (see 7.2.4 of ICC.1:2010).
Same as ``version``, but in encoded format (see 7.2.4 of ICC.1:2010).
.. py:attribute:: device_class

View File

@ -255,7 +255,7 @@ Methods
Draw a shape.
.. py:method:: PIL.ImageDraw.ImageDraw.text(xy, text, fill=None, font=None, anchor=None, spacing=0, align="left", direction=None, features=None, language=None)
.. py:method:: PIL.ImageDraw.ImageDraw.text(xy, text, fill=None, font=None, anchor=None, spacing=0, align="left", direction=None, features=None, language=None, stroke_width=0, stroke_fill=None)
Draws the string at the given position.
@ -297,6 +297,15 @@ Methods
.. versionadded:: 6.0.0
:param stroke_width: The width of the text stroke.
.. versionadded:: 6.2.0
:param stroke_fill: Color to use for the text stroke. If not given, will default to
the ``fill`` parameter.
.. versionadded:: 6.2.0
.. py:method:: PIL.ImageDraw.ImageDraw.multiline_text(xy, text, fill=None, font=None, anchor=None, spacing=0, align="left", direction=None, features=None, language=None)
Draws the string at the given position.
@ -336,7 +345,7 @@ Methods
.. versionadded:: 6.0.0
.. py:method:: PIL.ImageDraw.ImageDraw.textsize(text, font=None, spacing=4, direction=None, features=None, language=None)
.. py:method:: PIL.ImageDraw.ImageDraw.textsize(text, font=None, spacing=4, direction=None, features=None, language=None, stroke_width=0)
Return the size of the given string, in pixels.
@ -372,7 +381,11 @@ Methods
.. versionadded:: 6.0.0
.. py:method:: PIL.ImageDraw.ImageDraw.multiline_textsize(text, font=None, spacing=4, direction=None, features=None, language=None)
:param stroke_width: The width of the text stroke.
.. versionadded:: 6.2.0
.. py:method:: PIL.ImageDraw.ImageDraw.multiline_textsize(text, font=None, spacing=4, direction=None, features=None, language=None, stroke_width=0)
Return the size of the given string, in pixels.
@ -408,6 +421,10 @@ Methods
.. versionadded:: 6.0.0
:param stroke_width: The width of the text stroke.
.. versionadded:: 6.2.0
.. py:method:: PIL.ImageDraw.getdraw(im=None, hints=None)
.. warning:: This method is experimental.

View File

@ -53,7 +53,7 @@ vector data. Path objects can be passed to the methods on the
Converts the path to a Python list [(x, y), …].
:param flat: By default, this function returns a list of 2-tuples
[(x, y), ...]. If this argument is `True`, it
[(x, y), ...]. If this argument is ``True``, it
returns a flat list [x, y, ...] instead.
:return: A list of coordinates. See **flat**.

View File

@ -27,7 +27,7 @@ Image resizing filters
----------------------
Image resizing methods :py:meth:`~PIL.Image.Image.resize` and
:py:meth:`~PIL.Image.Image.thumbnail` take a `resample` argument, which tells
:py:meth:`~PIL.Image.Image.thumbnail` take a ``resample`` argument, which tells
which filter should be used for resampling. Possible values are:
:py:attr:`PIL.Image.NEAREST`, :py:attr:`PIL.Image.BILINEAR`,
:py:attr:`PIL.Image.BICUBIC` and :py:attr:`PIL.Image.ANTIALIAS`.

View File

@ -4,18 +4,28 @@
Open HTTP response objects with Image.open
------------------------------------------
HTTP response objects returned from `urllib2.urlopen(url)` or `requests.get(url, stream=True).raw` are 'file-like' but do not support `.seek()` operations. As a result PIL was unable to open them as images, requiring a wrap in `cStringIO` or `BytesIO`.
HTTP response objects returned from ``urllib2.urlopen(url)`` or
``requests.get(url, stream=True).raw`` are 'file-like' but do not support ``.seek()``
operations. As a result PIL was unable to open them as images, requiring a wrap in
``cStringIO`` or ``BytesIO``.
Now new functionality has been added to `Image.open()` by way of an `.seek(0)` check and catch on exception `AttributeError` or `io.UnsupportedOperation`. If this is caught we attempt to wrap the object using `io.BytesIO` (which will only work on buffer-file-like objects).
Now new functionality has been added to ``Image.open()`` by way of an ``.seek(0)`` check and
catch on exception ``AttributeError`` or ``io.UnsupportedOperation``. If this is caught we
attempt to wrap the object using ``io.BytesIO`` (which will only work on buffer-file-like
objects).
This allows opening of files using both `urllib2` and `requests`, e.g.::
This allows opening of files using both ``urllib2`` and ``requests``, e.g.::
Image.open(urllib2.urlopen(url))
Image.open(requests.get(url, stream=True).raw)
If the response uses content-encoding (compression, either gzip or deflate) then this will fail as both the urllib2 and requests raw file object will produce compressed data in that case. Using Content-Encoding on images is rather non-sensical as most images are already compressed, but it can still happen.
If the response uses content-encoding (compression, either gzip or deflate) then this
will fail as both the urllib2 and requests raw file object will produce compressed data
in that case. Using Content-Encoding on images is rather non-sensical as most images are
already compressed, but it can still happen.
For requests the work-around is to set the decode_content attribute on the raw object to True::
For requests the work-around is to set the decode_content attribute on the raw object to
True::
response = requests.get(url, stream=True)
response.raw.decode_content = True

View File

@ -5,8 +5,8 @@
Saving Multipage Images
-----------------------
There is now support for saving multipage images in the `GIF` and
`PDF` formats. To enable this functionality, pass in `save_all=True`
There is now support for saving multipage images in the ``GIF`` and
``PDF`` formats. To enable this functionality, pass in ``save_all=True``
as a keyword argument to the save::
im.save('test.pdf', save_all=True)
@ -37,7 +37,7 @@ have been removed in this release::
ImageDraw.setink()
ImageDraw.setfill()
The ImageFileIO module
The ImageFont.FreeTypeFont and ImageFont.truetype `file` keyword arg
The ImageFont.FreeTypeFont and ImageFont.truetype ``file`` keyword arg
The ImagePalette private _make functions
ImageWin.fromstring()
ImageWin.tostring()

View File

@ -5,8 +5,8 @@
ImageDraw arc, chord and pieslice can now use floats
----------------------------------------------------
There is no longer a need to ensure that the start and end arguments for `arc`,
`chord` and `pieslice` are integers.
There is no longer a need to ensure that the start and end arguments for ``arc``,
``chord`` and ``pieslice`` are integers.
Note that these numbers are not simply rounded internally, but are actually
utilised in the drawing process.

View File

@ -0,0 +1,83 @@
6.2.0
-----
API Additions
=============
Text stroking
^^^^^^^^^^^^^
``stroke_width`` and ``stroke_fill`` arguments have been added to text drawing
operations. They allow text to be outlined, setting the width of the stroke and
and the color respectively. If not provided, ``stroke_fill`` will default to
the ``fill`` parameter.
.. code-block:: python
from PIL import Image, ImageDraw, ImageFont
font = ImageFont.truetype("Tests/fonts/FreeMono.ttf", 40)
font.getsize_multiline("A", stroke_width=2)
font.getsize("ABC\nAaaa", stroke_width=2)
im = Image.new("RGB", (100, 100))
draw = ImageDraw.Draw(im)
draw.textsize("A", font, stroke_width=2)
draw.multiline_textsize("ABC\nAaaa", font, stroke_width=2)
draw.text((10, 10), "A", "#f00", font, stroke_width=2, stroke_fill="#0f0")
draw.multiline_text((10, 10), "A\nB", "#f00", font,
stroke_width=2, stroke_fill="#0f0")
For example,
.. code-block:: python
from PIL import Image, ImageDraw, ImageFont
im = Image.new("RGB", (120, 130))
draw = ImageDraw.Draw(im)
font = ImageFont.truetype("Tests/fonts/FreeMono.ttf", 120)
draw.text((10, 10), "A", "#f00", font, stroke_width=2, stroke_fill="#0f0")
creates the following image:
.. image:: ../../Tests/images/imagedraw_stroke_different.png
API Changes
===========
Image.getexif
^^^^^^^^^^^^^
To allow for lazy loading of Exif data, ``Image.getexif()`` now returns a
shared instance of ``Image.Exif``.
Deprecations
^^^^^^^^^^^^
Python 2.7
~~~~~~~~~~
Python 2.7 reaches end-of-life on 2020-01-01.
Pillow 7.0.0 will be released on 2020-01-01 and will drop support for Python
2.7, making Pillow 6.2.x the last release series to support Python 2.
Other Changes
=============
Removed bdist_wininst .exe installers
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.exe installers fell out of favour with PEP 527, and will be deprecated in
Python 3.8. Pillow will no longer be distributing them. Wheels should be used
instead.
Flags for libwebp in wheels
^^^^^^^^^^^^^^^^^^^^^^^^^^^
When building libwebp for inclusion in wheels, Pillow now adds the -O3 and
-DNDEBUG CFLAGS. These flags would be used by default if building libwebp
without debugging, and using them fixes a significant decrease in speed when
a wheel-installed copy of Pillow performs libwebp operations.

View File

@ -6,6 +6,7 @@ Release Notes
.. toctree::
:maxdepth: 2
6.2.0
6.1.0
6.0.0
5.4.1

View File

@ -489,6 +489,11 @@ def _write_multiple_frames(im, fp, palette):
offset = frame_data["bbox"][:2]
_write_frame_data(fp, im_frame, offset, frame_data["encoderinfo"])
return True
elif "duration" in im.encoderinfo and isinstance(
im.encoderinfo["duration"], (list, tuple)
):
# Since multiple frames will not be written, add together the frame durations
im.encoderinfo["duration"] = sum(im.encoderinfo["duration"])
def _save_all(im, fp, filename):

View File

@ -263,6 +263,9 @@ class IcoImageFile(ImageFile.ImageFile):
Handles classic, XP and Vista icon formats.
When saving, PNG compression is used. Support for this was only added in
Windows Vista.
This plugin is a refactored version of Win32IconImagePlugin by Bryan Davis
<casadebender@gmail.com>.
https://code.google.com/archive/p/casadebender/wikis/Win32IconImagePlugin.wiki

View File

@ -555,6 +555,7 @@ class Image(object):
self.category = NORMAL
self.readonly = 0
self.pyaccess = None
self._exif = None
@property
def width(self):
@ -1324,10 +1325,10 @@ class Image(object):
return self.im.getextrema()
def getexif(self):
exif = Exif()
if "exif" in self.info:
exif.load(self.info["exif"])
return exif
if self._exif is None:
self._exif = Exif()
self._exif.load(self.info.get("exif"))
return self._exif
def getim(self):
"""
@ -2073,10 +2074,10 @@ class Image(object):
if open_fp:
if params.get("append", False):
fp = builtins.open(filename, "r+b")
else:
# Open also for reading ("+"), because TIFF save_all
# writer needs to go back and edit the written data.
fp = builtins.open(filename, "r+b")
else:
fp = builtins.open(filename, "w+b")
try:
@ -3137,25 +3138,27 @@ class Exif(MutableMapping):
def __init__(self):
self._data = {}
self._ifds = {}
self._info = None
self._loaded_exif = None
def _fixup(self, value):
try:
if len(value) == 1 and not isinstance(value, dict):
return value[0]
except Exception:
pass
return value
def _fixup_dict(self, src_dict):
# Helper function for _getexif()
# returns a dict with any single item tuples/lists as individual values
def _fixup(value):
try:
if len(value) == 1 and not isinstance(value, dict):
return value[0]
except Exception:
pass
return value
return {k: _fixup(v) for k, v in src_dict.items()}
return {k: self._fixup(v) for k, v in src_dict.items()}
def _get_ifd_dict(self, tag):
try:
# an offset pointer to the location of the nested embedded IFD.
# It should be a long, but may be corrupted.
self.fp.seek(self._data[tag])
self.fp.seek(self[tag])
except (KeyError, TypeError):
pass
else:
@ -3172,16 +3175,24 @@ class Exif(MutableMapping):
# The EXIF record consists of a TIFF file embedded in a JPEG
# application marker (!).
if data == self._loaded_exif:
return
self._loaded_exif = data
self._data.clear()
self._ifds.clear()
self._info = None
if not data:
return
self.fp = io.BytesIO(data[6:])
self.head = self.fp.read(8)
# process dictionary
from . import TiffImagePlugin
info = TiffImagePlugin.ImageFileDirectory_v1(self.head)
self.endian = info._endian
self.fp.seek(info.next)
info.load(self.fp)
self._data = dict(self._fixup_dict(info))
self._info = TiffImagePlugin.ImageFileDirectory_v1(self.head)
self.endian = self._info._endian
self.fp.seek(self._info.next)
self._info.load(self.fp)
# get EXIF extension
ifd = self._get_ifd_dict(0x8769)
@ -3189,12 +3200,6 @@ class Exif(MutableMapping):
self._data.update(ifd)
self._ifds[0x8769] = ifd
# get gpsinfo extension
ifd = self._get_ifd_dict(0x8825)
if ifd:
self._data[0x8825] = ifd
self._ifds[0x8825] = ifd
def tobytes(self, offset=0):
from . import TiffImagePlugin
@ -3203,19 +3208,20 @@ class Exif(MutableMapping):
else:
head = b"MM\x00\x2A\x00\x00\x00\x08"
ifd = TiffImagePlugin.ImageFileDirectory_v2(ifh=head)
for tag, value in self._data.items():
for tag, value in self.items():
ifd[tag] = value
return b"Exif\x00\x00" + head + ifd.tobytes(offset)
def get_ifd(self, tag):
if tag not in self._ifds and tag in self._data:
if tag == 0xA005: # interop
if tag not in self._ifds and tag in self:
if tag in [0x8825, 0xA005]:
# gpsinfo, interop
self._ifds[tag] = self._get_ifd_dict(tag)
elif tag == 0x927C: # makernote
from .TiffImagePlugin import ImageFileDirectory_v2
if self._data[0x927C][:8] == b"FUJIFILM":
exif_data = self._data[0x927C]
if self[0x927C][:8] == b"FUJIFILM":
exif_data = self[0x927C]
ifd_offset = i32le(exif_data[8:12])
ifd_data = exif_data[ifd_offset:]
@ -3252,8 +3258,8 @@ class Exif(MutableMapping):
ImageFileDirectory_v2(), data, False
)
self._ifds[0x927C] = dict(self._fixup_dict(makernote))
elif self._data.get(0x010F) == "Nintendo":
ifd_data = self._data[0x927C]
elif self.get(0x010F) == "Nintendo":
ifd_data = self[0x927C]
makernote = {}
for i in range(0, struct.unpack(">H", ifd_data[:2])[0]):
@ -3291,16 +3297,29 @@ class Exif(MutableMapping):
return self._ifds.get(tag, {})
def __str__(self):
if self._info is not None:
# Load all keys into self._data
for tag in self._info.keys():
self[tag]
return str(self._data)
def __len__(self):
return len(self._data)
keys = set(self._data)
if self._info is not None:
keys.update(self._info)
return len(keys)
def __getitem__(self, tag):
if self._info is not None and tag not in self._data and tag in self._info:
self._data[tag] = self._fixup(self._info[tag])
if tag == 0x8825:
self._data[tag] = self.get_ifd(tag)
del self._info[tag]
return self._data[tag]
def __contains__(self, tag):
return tag in self._data
return tag in self._data or (self._info is not None and tag in self._info)
if not py3:
@ -3308,10 +3327,17 @@ class Exif(MutableMapping):
return tag in self
def __setitem__(self, tag, value):
if self._info is not None and tag in self._info:
del self._info[tag]
self._data[tag] = value
def __delitem__(self, tag):
if self._info is not None and tag in self._info:
del self._info[tag]
del self._data[tag]
def __iter__(self):
return iter(set(self._data))
keys = set(self._data)
if self._info is not None:
keys.update(self._info)
return iter(keys)

View File

@ -261,24 +261,95 @@ class ImageDraw(object):
return text.split(split_character)
def text(self, xy, text, fill=None, font=None, anchor=None, *args, **kwargs):
def text(
self,
xy,
text,
fill=None,
font=None,
anchor=None,
spacing=4,
align="left",
direction=None,
features=None,
language=None,
stroke_width=0,
stroke_fill=None,
*args,
**kwargs
):
if self._multiline_check(text):
return self.multiline_text(xy, text, fill, font, anchor, *args, **kwargs)
ink, fill = self._getink(fill)
return self.multiline_text(
xy,
text,
fill,
font,
anchor,
spacing,
align,
direction,
features,
language,
stroke_width,
stroke_fill,
)
if font is None:
font = self.getfont()
if ink is None:
ink = fill
if ink is not None:
def getink(fill):
ink, fill = self._getink(fill)
if ink is None:
return fill
return ink
def draw_text(ink, stroke_width=0, stroke_offset=None):
coord = xy
try:
mask, offset = font.getmask2(text, self.fontmode, *args, **kwargs)
xy = xy[0] + offset[0], xy[1] + offset[1]
mask, offset = font.getmask2(
text,
self.fontmode,
direction=direction,
features=features,
language=language,
stroke_width=stroke_width,
*args,
**kwargs
)
coord = coord[0] + offset[0], coord[1] + offset[1]
except AttributeError:
try:
mask = font.getmask(text, self.fontmode, *args, **kwargs)
mask = font.getmask(
text,
self.fontmode,
direction,
features,
language,
stroke_width,
*args,
**kwargs
)
except TypeError:
mask = font.getmask(text)
self.draw.draw_bitmap(xy, mask, ink)
if stroke_offset:
coord = coord[0] + stroke_offset[0], coord[1] + stroke_offset[1]
self.draw.draw_bitmap(coord, mask, ink)
ink = getink(fill)
if ink is not None:
stroke_ink = None
if stroke_width:
stroke_ink = getink(stroke_fill) if stroke_fill is not None else ink
if stroke_ink is not None:
# Draw stroked text
draw_text(stroke_ink, stroke_width)
# Draw normal text
draw_text(ink, 0, (stroke_width, stroke_width))
else:
# Only draw normal text
draw_text(ink)
def multiline_text(
self,
@ -292,14 +363,23 @@ class ImageDraw(object):
direction=None,
features=None,
language=None,
stroke_width=0,
stroke_fill=None,
):
widths = []
max_width = 0
lines = self._multiline_split(text)
line_spacing = self.textsize("A", font=font)[1] + spacing
line_spacing = (
self.textsize("A", font=font, stroke_width=stroke_width)[1] + spacing
)
for line in lines:
line_width, line_height = self.textsize(
line, font, direction=direction, features=features, language=language
line,
font,
direction=direction,
features=features,
language=language,
stroke_width=stroke_width,
)
widths.append(line_width)
max_width = max(max_width, line_width)
@ -322,32 +402,50 @@ class ImageDraw(object):
direction=direction,
features=features,
language=language,
stroke_width=stroke_width,
stroke_fill=stroke_fill,
)
top += line_spacing
left = xy[0]
def textsize(
self, text, font=None, spacing=4, direction=None, features=None, language=None
self,
text,
font=None,
spacing=4,
direction=None,
features=None,
language=None,
stroke_width=0,
):
"""Get the size of a given string, in pixels."""
if self._multiline_check(text):
return self.multiline_textsize(
text, font, spacing, direction, features, language
text, font, spacing, direction, features, language, stroke_width
)
if font is None:
font = self.getfont()
return font.getsize(text, direction, features, language)
return font.getsize(text, direction, features, language, stroke_width)
def multiline_textsize(
self, text, font=None, spacing=4, direction=None, features=None, language=None
self,
text,
font=None,
spacing=4,
direction=None,
features=None,
language=None,
stroke_width=0,
):
max_width = 0
lines = self._multiline_split(text)
line_spacing = self.textsize("A", font=font)[1] + spacing
line_spacing = (
self.textsize("A", font=font, stroke_width=stroke_width)[1] + spacing
)
for line in lines:
line_width, line_height = self.textsize(
line, font, spacing, direction, features, language
line, font, spacing, direction, features, language, stroke_width
)
max_width = max(max_width, line_width)
return max_width, len(lines) * line_spacing - spacing
@ -437,8 +535,9 @@ def floodfill(image, xy, value, border=None, thresh=0):
new_edge = set()
for (x, y) in edge: # 4 adjacent method
for (s, t) in ((x + 1, y), (x - 1, y), (x, y + 1), (x, y - 1)):
if (s, t) in full_edge:
continue # if already processed, skip
# If already processed, or if a coordinate is negative, skip
if (s, t) in full_edge or s < 0 or t < 0:
continue
try:
p = pixel[s, t]
except (ValueError, IndexError):

View File

@ -244,7 +244,6 @@ class ImageFile(Image.Image):
if LOAD_TRUNCATED_IMAGES:
break
else:
self.tile = []
raise IOError(
"image file is truncated "
"(%d bytes not processed)" % len(b)

View File

@ -207,7 +207,9 @@ class FreeTypeFont(object):
"""
return self.font.ascent, self.font.descent
def getsize(self, text, direction=None, features=None, language=None):
def getsize(
self, text, direction=None, features=None, language=None, stroke_width=0
):
"""
Returns width and height (in pixels) of given text if rendered in font with
provided direction, features, and language.
@ -243,13 +245,26 @@ class FreeTypeFont(object):
.. versionadded:: 6.0.0
:param stroke_width: The width of the text stroke.
.. versionadded:: 6.2.0
:return: (width, height)
"""
size, offset = self.font.getsize(text, direction, features, language)
return (size[0] + offset[0], size[1] + offset[1])
return (
size[0] + stroke_width * 2 + offset[0],
size[1] + stroke_width * 2 + offset[1],
)
def getsize_multiline(
self, text, direction=None, spacing=4, features=None, language=None
self,
text,
direction=None,
spacing=4,
features=None,
language=None,
stroke_width=0,
):
"""
Returns width and height (in pixels) of given text if rendered in font
@ -285,13 +300,19 @@ class FreeTypeFont(object):
.. versionadded:: 6.0.0
:param stroke_width: The width of the text stroke.
.. versionadded:: 6.2.0
:return: (width, height)
"""
max_width = 0
lines = self._multiline_split(text)
line_spacing = self.getsize("A")[1] + spacing
line_spacing = self.getsize("A", stroke_width=stroke_width)[1] + spacing
for line in lines:
line_width, line_height = self.getsize(line, direction, features, language)
line_width, line_height = self.getsize(
line, direction, features, language, stroke_width
)
max_width = max(max_width, line_width)
return max_width, len(lines) * line_spacing - spacing
@ -308,7 +329,15 @@ class FreeTypeFont(object):
"""
return self.font.getsize(text)[1]
def getmask(self, text, mode="", direction=None, features=None, language=None):
def getmask(
self,
text,
mode="",
direction=None,
features=None,
language=None,
stroke_width=0,
):
"""
Create a bitmap for the text.
@ -352,11 +381,20 @@ class FreeTypeFont(object):
.. versionadded:: 6.0.0
:param stroke_width: The width of the text stroke.
.. versionadded:: 6.2.0
:return: An internal PIL storage memory instance as defined by the
:py:mod:`PIL.Image.core` interface module.
"""
return self.getmask2(
text, mode, direction=direction, features=features, language=language
text,
mode,
direction=direction,
features=features,
language=language,
stroke_width=stroke_width,
)[0]
def getmask2(
@ -367,6 +405,7 @@ class FreeTypeFont(object):
direction=None,
features=None,
language=None,
stroke_width=0,
*args,
**kwargs
):
@ -413,13 +452,20 @@ class FreeTypeFont(object):
.. versionadded:: 6.0.0
:param stroke_width: The width of the text stroke.
.. versionadded:: 6.2.0
:return: A tuple of an internal PIL storage memory instance as defined by the
:py:mod:`PIL.Image.core` interface module, and the text offset, the
gap between the starting coordinate and the first marking
"""
size, offset = self.font.getsize(text, direction, features, language)
size = size[0] + stroke_width * 2, size[1] + stroke_width * 2
im = fill("L", size, 0)
self.font.render(text, im.id, mode == "1", direction, features, language)
self.font.render(
text, im.id, mode == "1", direction, features, language, stroke_width
)
return im, offset
def font_variant(
@ -562,11 +608,25 @@ def truetype(font=None, size=10, index=0, encoding="", layout_engine=None):
:param size: The requested size, in points.
:param index: Which font face to load (default is first available face).
:param encoding: Which font encoding to use (default is Unicode). Common
encodings are "unic" (Unicode), "symb" (Microsoft
Symbol), "ADOB" (Adobe Standard), "ADBE" (Adobe Expert),
and "armn" (Apple Roman). See the FreeType documentation
for more information.
:param encoding: Which font encoding to use (default is Unicode). Possible
encodings include (see the FreeType documentation for more
information):
* "unic" (Unicode)
* "symb" (Microsoft Symbol)
* "ADOB" (Adobe Standard)
* "ADBE" (Adobe Expert)
* "ADBC" (Adobe Custom)
* "armn" (Apple Roman)
* "sjis" (Shift JIS)
* "gb " (PRC)
* "big5"
* "wans" (Extended Wansung)
* "joha" (Johab)
* "lat1" (Latin-1)
This specifies the character set to use. It does not alter the
encoding of any text provided in subsequent operations.
:param layout_engine: Which layout engine to use, if available:
`ImageFont.LAYOUT_BASIC` or `ImageFont.LAYOUT_RAQM`.
:return: A font object.

View File

@ -158,7 +158,7 @@ def APP(self, marker):
# If DPI isn't in JPEG header, fetch from EXIF
if "dpi" not in self.info and "exif" in self.info:
try:
exif = self._getexif()
exif = self.getexif()
resolution_unit = exif[0x0128]
x_resolution = exif[0x011A]
try:
@ -485,19 +485,9 @@ def _fixup_dict(src_dict):
def _getexif(self):
# Use the cached version if possible
try:
return self.info["parsed_exif"]
except KeyError:
pass
if "exif" not in self.info:
return None
exif = dict(self.getexif())
# Cache the result for future use
self.info["parsed_exif"] = exif
return exif
return dict(self.getexif())
def _getmp(self):

View File

@ -86,13 +86,11 @@ class MpoImageFile(JpegImagePlugin.JpegImageFile):
self.offset = self.__mpoffsets[frame]
self.fp.seek(self.offset + 2) # skip SOI marker
if "parsed_exif" in self.info:
del self.info["parsed_exif"]
if i16(self.fp.read(2)) == 0xFFE1: # APP1
n = i16(self.fp.read(2)) - 2
self.info["exif"] = ImageFile._safe_read(self.fp, n)
exif = self._getexif()
exif = self.getexif()
if 40962 in exif and 40963 in exif:
self._size = (exif[40962], exif[40963])
elif "exif" in self.info:

View File

@ -612,7 +612,7 @@ class PngImageFile(ImageFile.ImageFile):
rawmode, data = self.png.im_palette
self.palette = ImagePalette.raw(rawmode, data)
self.__idat = length # used by load_read()
self.__prepare_idat = length # used by load_prepare()
@property
def text(self):
@ -645,6 +645,7 @@ class PngImageFile(ImageFile.ImageFile):
if self.info.get("interlace"):
self.decoderconfig = self.decoderconfig + (1,)
self.__idat = self.__prepare_idat # used by load_read()
ImageFile.ImageFile.load_prepare(self)
def load_read(self, read_bytes):

View File

@ -1098,6 +1098,20 @@ class TiffImageFile(ImageFile.ImageFile):
return super(TiffImageFile, self).load()
def load_end(self):
if self._tile_orientation:
method = {
2: Image.FLIP_LEFT_RIGHT,
3: Image.ROTATE_180,
4: Image.FLIP_TOP_BOTTOM,
5: Image.TRANSPOSE,
6: Image.ROTATE_270,
7: Image.TRANSVERSE,
8: Image.ROTATE_90,
}.get(self._tile_orientation)
if method is not None:
self.im = self.im.transpose(method)
self._size = self.im.size
# allow closing if we're on the first frame, there's no next
# This is the ImageFile.load path only, libtiff specific below.
if not self._is_animated:
@ -1164,7 +1178,7 @@ class TiffImageFile(ImageFile.ImageFile):
if DEBUG:
print("have getvalue. just sending in a string from getvalue")
n, err = decoder.decode(self.fp.getvalue())
elif hasattr(self.fp, "fileno"):
elif fp:
# we've got a actual file on disk, pass in the fp.
if DEBUG:
print("have fileno, calling fileno version of the decoder.")
@ -1175,11 +1189,15 @@ class TiffImageFile(ImageFile.ImageFile):
# we have something else.
if DEBUG:
print("don't have fileno or getvalue. just reading")
self.fp.seek(0)
# UNDONE -- so much for that buffer size thing.
n, err = decoder.decode(self.fp.read())
self.tile = []
self.readonly = 0
self.load_end()
# libtiff closed the fp in a, we need to close self.fp, if possible
if self._exclusive_fp and not self._is_animated:
self.fp.close()
@ -1387,6 +1405,8 @@ class TiffImageFile(ImageFile.ImageFile):
palette = [o8(b // 256) for b in self.tag_v2[COLORMAP]]
self.palette = ImagePalette.raw("RGB;L", b"".join(palette))
self._tile_orientation = self.tag_v2.get(0x0112)
def _close__fp(self):
try:
if self.__fp != self.fp:

View File

@ -175,9 +175,10 @@ TAGS_V2 = {
530: ("YCbCrSubSampling", SHORT, 2),
531: ("YCbCrPositioning", SHORT, 1),
532: ("ReferenceBlackWhite", RATIONAL, 6),
700: ("XMP", BYTE, 1),
700: ("XMP", BYTE, 0),
33432: ("Copyright", ASCII, 1),
34377: ("PhotoshopInfo", BYTE, 1),
33723: ("IptcNaaInfo", UNDEFINED, 0),
34377: ("PhotoshopInfo", BYTE, 0),
# FIXME add more tags here
34665: ("ExifIFD", LONG, 1),
34675: ("ICCProfile", UNDEFINED, 1),

View File

@ -25,6 +25,7 @@
#include <ft2build.h>
#include FT_FREETYPE_H
#include FT_GLYPH_H
#include FT_STROKER_H
#include FT_MULTIPLE_MASTERS_H
#include FT_SFNT_NAMES_H
@ -790,7 +791,13 @@ font_render(FontObject* self, PyObject* args)
int index, error, ascender, horizontal_dir;
int load_flags;
unsigned char *source;
FT_GlyphSlot glyph;
FT_Glyph glyph;
FT_GlyphSlot glyph_slot;
FT_Bitmap bitmap;
FT_BitmapGlyph bitmap_glyph;
int stroke_width = 0;
FT_Stroker stroker = NULL;
FT_Int left;
/* render string into given buffer (the buffer *must* have
the right size, or this will crash) */
PyObject* string;
@ -806,7 +813,8 @@ font_render(FontObject* self, PyObject* args)
GlyphInfo *glyph_info;
PyObject *features = NULL;
if (!PyArg_ParseTuple(args, "On|izOz:render", &string, &id, &mask, &dir, &features, &lang)) {
if (!PyArg_ParseTuple(args, "On|izOzi:render", &string, &id, &mask, &dir, &features, &lang,
&stroke_width)) {
return NULL;
}
@ -819,21 +827,37 @@ font_render(FontObject* self, PyObject* args)
Py_RETURN_NONE;
}
if (stroke_width) {
error = FT_Stroker_New(library, &stroker);
if (error) {
return geterror(error);
}
FT_Stroker_Set(stroker, (FT_Fixed)stroke_width*64, FT_STROKER_LINECAP_ROUND, FT_STROKER_LINEJOIN_ROUND, 0);
}
im = (Imaging) id;
/* Note: bitmap fonts within ttf fonts do not work, see #891/pr#960 */
load_flags = FT_LOAD_RENDER|FT_LOAD_NO_BITMAP;
if (mask)
load_flags = FT_LOAD_NO_BITMAP;
if (stroker == NULL) {
load_flags |= FT_LOAD_RENDER;
}
if (mask) {
load_flags |= FT_LOAD_TARGET_MONO;
}
ascender = 0;
for (i = 0; i < count; i++) {
index = glyph_info[i].index;
error = FT_Load_Glyph(self->face, index, load_flags);
if (error)
if (error) {
return geterror(error);
}
glyph = self->face->glyph;
temp = glyph->bitmap.rows - glyph->bitmap_top;
glyph_slot = self->face->glyph;
bitmap = glyph_slot->bitmap;
temp = bitmap.rows - glyph_slot->bitmap_top;
temp -= PIXEL(glyph_info[i].y_offset);
if (temp > ascender)
ascender = temp;
@ -844,37 +868,62 @@ font_render(FontObject* self, PyObject* args)
for (i = 0; i < count; i++) {
index = glyph_info[i].index;
error = FT_Load_Glyph(self->face, index, load_flags);
if (error)
if (error) {
return geterror(error);
}
glyph = self->face->glyph;
if (horizontal_dir) {
if (i == 0 && self->face->glyph->metrics.horiBearingX < 0) {
x = -self->face->glyph->metrics.horiBearingX;
glyph_slot = self->face->glyph;
if (stroker != NULL) {
error = FT_Get_Glyph(glyph_slot, &glyph);
if (!error) {
error = FT_Glyph_Stroke(&glyph, stroker, 1);
}
xx = PIXEL(x) + glyph->bitmap_left;
xx += PIXEL(glyph_info[i].x_offset);
if (!error) {
FT_Vector origin = {0, 0};
error = FT_Glyph_To_Bitmap(&glyph, FT_RENDER_MODE_NORMAL, &origin, 1);
}
if (error) {
return geterror(error);
}
bitmap_glyph = (FT_BitmapGlyph)glyph;
bitmap = bitmap_glyph->bitmap;
left = bitmap_glyph->left;
FT_Done_Glyph(glyph);
} else {
if (self->face->glyph->metrics.vertBearingX < 0) {
x = -self->face->glyph->metrics.vertBearingX;
bitmap = glyph_slot->bitmap;
left = glyph_slot->bitmap_left;
}
if (horizontal_dir) {
if (i == 0 && glyph_slot->metrics.horiBearingX < 0) {
x = -glyph_slot->metrics.horiBearingX;
}
xx = im->xsize / 2 - glyph->bitmap.width / 2;
xx = PIXEL(x) + left;
xx += PIXEL(glyph_info[i].x_offset) + stroke_width;
} else {
if (glyph_slot->metrics.vertBearingX < 0) {
x = -glyph_slot->metrics.vertBearingX;
}
xx = im->xsize / 2 - bitmap.width / 2;
}
x0 = 0;
x1 = glyph->bitmap.width;
x1 = bitmap.width;
if (xx < 0)
x0 = -xx;
if (xx + x1 > im->xsize)
x1 = im->xsize - xx;
source = (unsigned char*) glyph->bitmap.buffer;
for (bitmap_y = 0; bitmap_y < glyph->bitmap.rows; bitmap_y++) {
source = (unsigned char*) bitmap.buffer;
for (bitmap_y = 0; bitmap_y < bitmap.rows; bitmap_y++) {
if (horizontal_dir) {
yy = bitmap_y + im->ysize - (PIXEL(glyph->metrics.horiBearingY) + ascender);
yy -= PIXEL(glyph_info[i].y_offset);
yy = bitmap_y + im->ysize - (PIXEL(glyph_slot->metrics.horiBearingY) + ascender);
yy -= PIXEL(glyph_info[i].y_offset) + stroke_width * 2;
} else {
yy = bitmap_y + PIXEL(y + glyph->metrics.vertBearingY) + ascender;
yy = bitmap_y + PIXEL(y + glyph_slot->metrics.vertBearingY) + ascender;
yy += PIXEL(glyph_info[i].y_offset);
}
if (yy >= 0 && yy < im->ysize) {
@ -900,12 +949,13 @@ font_render(FontObject* self, PyObject* args)
}
}
}
source += glyph->bitmap.pitch;
source += bitmap.pitch;
}
x += glyph_info[i].x_advance;
y -= glyph_info[i].y_advance;
}
FT_Stroker_Done(stroker);
PyMem_Del(glyph_info);
Py_RETURN_NONE;
}

View File

@ -1211,6 +1211,8 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args)
PyErr_SetString(PyExc_ValueError,
"JPEG 2000 tile offset too small; top left tile must "
"intersect image area");
Py_DECREF(encoder);
return NULL;
}
if (context->tile_offset_x > context->offset_x

View File

@ -101,6 +101,19 @@ bit2ycbcr(UINT8* out, const UINT8* in, int xsize)
}
}
static void
bit2hsv(UINT8* out, const UINT8* in, int xsize)
{
int x;
for (x = 0; x < xsize; x++, out += 4) {
UINT8 v = (*in++ != 0) ? 255 : 0;
out[0] = 0;
out[1] = 0;
out[2] = v;
out[3] = 255;
}
}
/* ----------------- */
/* RGB/L conversions */
/* ----------------- */
@ -175,6 +188,19 @@ l2rgb(UINT8* out, const UINT8* in, int xsize)
}
}
static void
l2hsv(UINT8* out, const UINT8* in, int xsize)
{
int x;
for (x = 0; x < xsize; x++, out += 4) {
UINT8 v = *in++;
out[0] = 0;
out[1] = 0;
out[2] = v;
out[3] = 255;
}
}
static void
la2l(UINT8* out, const UINT8* in, int xsize)
{
@ -196,6 +222,19 @@ la2rgb(UINT8* out, const UINT8* in, int xsize)
}
}
static void
la2hsv(UINT8* out, const UINT8* in, int xsize)
{
int x;
for (x = 0; x < xsize; x++, in += 4, out += 4) {
UINT8 v = in[0];
out[0] = 0;
out[1] = 0;
out[2] = v;
out[3] = in[3];
}
}
static void
rgb2bit(UINT8* out, const UINT8* in, int xsize)
{
@ -283,54 +322,58 @@ rgb2bgr24(UINT8* out, const UINT8* in, int xsize)
}
static void
rgb2hsv(UINT8* out, const UINT8* in, int xsize)
rgb2hsv_row(UINT8* out, const UINT8* in)
{ // following colorsys.py
float h,s,rc,gc,bc,cr;
UINT8 maxc,minc;
UINT8 r, g, b;
UINT8 uh,us,uv;
int x;
for (x = 0; x < xsize; x++, in += 4) {
r = in[0];
g = in[1];
b = in[2];
maxc = MAX(r,MAX(g,b));
minc = MIN(r,MIN(g,b));
uv = maxc;
if (minc == maxc){
*out++ = 0;
*out++ = 0;
*out++ = uv;
r = in[0];
g = in[1];
b = in[2];
maxc = MAX(r,MAX(g,b));
minc = MIN(r,MIN(g,b));
uv = maxc;
if (minc == maxc){
uh = 0;
us = 0;
} else {
cr = (float)(maxc-minc);
s = cr/(float)maxc;
rc = ((float)(maxc-r))/cr;
gc = ((float)(maxc-g))/cr;
bc = ((float)(maxc-b))/cr;
if (r == maxc) {
h = bc-gc;
} else if (g == maxc) {
h = 2.0 + rc-bc;
} else {
cr = (float)(maxc-minc);
s = cr/(float)maxc;
rc = ((float)(maxc-r))/cr;
gc = ((float)(maxc-g))/cr;
bc = ((float)(maxc-b))/cr;
if (r == maxc) {
h = bc-gc;
} else if (g == maxc) {
h = 2.0 + rc-bc;
} else {
h = 4.0 + gc-rc;
}
// incorrect hue happens if h/6 is negative.
h = fmod((h/6.0 + 1.0), 1.0);
uh = (UINT8)CLIP8((int)(h*255.0));
us = (UINT8)CLIP8((int)(s*255.0));
*out++ = uh;
*out++ = us;
*out++ = uv;
h = 4.0 + gc-rc;
}
*out++ = in[3];
// incorrect hue happens if h/6 is negative.
h = fmod((h/6.0 + 1.0), 1.0);
uh = (UINT8)CLIP8((int)(h*255.0));
us = (UINT8)CLIP8((int)(s*255.0));
}
out[0] = uh;
out[1] = us;
out[2] = uv;
}
static void
rgb2hsv(UINT8* out, const UINT8* in, int xsize)
{
int x;
for (x = 0; x < xsize; x++, in += 4, out += 4) {
rgb2hsv_row(out, in);
out[3] = in[3];
}
}
static void
hsv2rgb(UINT8* out, const UINT8* in, int xsize)
{ // following colorsys.py
@ -562,6 +605,22 @@ cmyk2rgb(UINT8* out, const UINT8* in, int xsize)
}
}
static void
cmyk2hsv(UINT8* out, const UINT8* in, int xsize)
{
int x, nk, tmp;
for (x = 0; x < xsize; x++) {
nk = 255 - in[3];
out[0] = CLIP8(nk - MULDIV255(in[0], nk, tmp));
out[1] = CLIP8(nk - MULDIV255(in[1], nk, tmp));
out[2] = CLIP8(nk - MULDIV255(in[2], nk, tmp));
rgb2hsv_row(out, out);
out[3] = 255;
out += 4;
in += 4;
}
}
/* ------------- */
/* I conversions */
/* ------------- */
@ -631,6 +690,25 @@ i2rgb(UINT8* out, const UINT8* in_, int xsize)
}
}
static void
i2hsv(UINT8* out, const UINT8* in_, int xsize)
{
int x;
INT32* in = (INT32*) in_;
for (x = 0; x < xsize; x++, in++, out+=4) {
out[0] = 0;
out[1] = 0;
if (*in <= 0) {
out[2] = 0;
} else if (*in >= 255) {
out[2] = 255;
} else {
out[2] = (UINT8) *in;
}
out[3] = 255;
}
}
/* ------------- */
/* F conversions */
/* ------------- */
@ -861,6 +939,7 @@ static struct {
{ "1", "RGBX", bit2rgb },
{ "1", "CMYK", bit2cmyk },
{ "1", "YCbCr", bit2ycbcr },
{ "1", "HSV", bit2hsv },
{ "L", "1", l2bit },
{ "L", "LA", l2la },
@ -871,6 +950,7 @@ static struct {
{ "L", "RGBX", l2rgb },
{ "L", "CMYK", l2cmyk },
{ "L", "YCbCr", l2ycbcr },
{ "L", "HSV", l2hsv },
{ "LA", "L", la2l },
{ "LA", "La", lA2la },
@ -879,6 +959,7 @@ static struct {
{ "LA", "RGBX", la2rgb },
{ "LA", "CMYK", la2cmyk },
{ "LA", "YCbCr", la2ycbcr },
{ "LA", "HSV", la2hsv },
{ "La", "LA", la2lA },
@ -887,6 +968,7 @@ static struct {
{ "I", "RGB", i2rgb },
{ "I", "RGBA", i2rgb },
{ "I", "RGBX", i2rgb },
{ "I", "HSV", i2hsv },
{ "F", "L", f2l },
{ "F", "I", f2i },
@ -915,6 +997,7 @@ static struct {
{ "RGBA", "RGBX", rgb2rgba },
{ "RGBA", "CMYK", rgb2cmyk },
{ "RGBA", "YCbCr", ImagingConvertRGB2YCbCr },
{ "RGBA", "HSV", rgb2hsv },
{ "RGBa", "RGBA", rgba2rgbA },
@ -926,10 +1009,12 @@ static struct {
{ "RGBX", "RGB", rgba2rgb },
{ "RGBX", "CMYK", rgb2cmyk },
{ "RGBX", "YCbCr", ImagingConvertRGB2YCbCr },
{ "RGBX", "HSV", rgb2hsv },
{ "CMYK", "RGB", cmyk2rgb },
{ "CMYK", "RGBA", cmyk2rgb },
{ "CMYK", "RGBX", cmyk2rgb },
{ "CMYK", "HSV", cmyk2hsv },
{ "YCbCr", "L", ycbcr2l },
{ "YCbCr", "LA", ycbcr2la },
@ -1101,6 +1186,28 @@ pa2rgb(UINT8* out, const UINT8* in, int xsize, const UINT8* palette)
}
}
static void
p2hsv(UINT8* out, const UINT8* in, int xsize, const UINT8* palette)
{
int x;
for (x = 0; x < xsize; x++, out += 4) {
const UINT8* rgb = &palette[*in++ * 4];
rgb2hsv_row(out, rgb);
out[3] = 255;
}
}
static void
pa2hsv(UINT8* out, const UINT8* in, int xsize, const UINT8* palette)
{
int x;
for (x = 0; x < xsize; x++, in += 4, out += 4) {
const UINT8* rgb = &palette[in[0] * 4];
rgb2hsv_row(out, rgb);
out[3] = 255;
}
}
static void
p2rgba(UINT8* out, const UINT8* in, int xsize, const UINT8* palette)
{
@ -1192,6 +1299,8 @@ frompalette(Imaging imOut, Imaging imIn, const char *mode)
convert = alpha ? pa2cmyk : p2cmyk;
else if (strcmp(mode, "YCbCr") == 0)
convert = alpha ? pa2ycbcr : p2ycbcr;
else if (strcmp(mode, "HSV") == 0)
convert = alpha ? pa2hsv : p2hsv;
else
return (Imaging) ImagingError_ValueError("conversion not supported");

View File

@ -834,7 +834,7 @@ ellipse(Imaging im, int x0, int y0, int x1, int y1,
// Build edge list
// malloc check UNDONE, FLOAT?
maxEdgeCount = end - start;
maxEdgeCount = ceil(end - start);
if (inner) {
maxEdgeCount *= 2;
}

View File

@ -132,7 +132,7 @@ ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size)
} else if (strcmp(mode, "BGR;15") == 0) {
/* EXPERIMENTAL */
/* 15-bit true colour */
/* 15-bit reversed true colour */
im->bands = 1;
im->pixelsize = 2;
im->linesize = (xsize*2 + 3) & -4;

View File

@ -191,16 +191,14 @@ def run_one(op):
if __name__ == "__main__":
opts, args = getopt.getopt(sys.argv[1:], "", ["clean", "dist", "wheel"])
opts, args = getopt.getopt(sys.argv[1:], "", ["clean", "wheel"])
opts = dict(opts)
if "--clean" in opts:
clean()
op = "install"
if "--dist" in opts:
op = "bdist_wininst --user-access-control=auto"
elif "--wheel" in opts:
if "--wheel" in opts:
op = "bdist_wheel"
if "PYTHON" in os.environ:

View File

@ -37,8 +37,8 @@ virtualenv as well, reducing the number of packages that we need to
install.)
Download the rest of the Pythons by opening a command window, changing
to the `winbuild` directory, and running `python
get_pythons.py`.
to the ``winbuild`` directory, and running ``python
get_pythons.py``.
UNDONE -- gpg verify the signatures (note that we can download from
https)
@ -65,8 +65,8 @@ Dependencies
------------
The script 'build_dep.py' downloads and builds the dependencies. Open
a command window, change directory into `winbuild` and run `python
build_dep.py`.
a command window, change directory into ``winbuild`` and run ``python
build_dep.py``.
This will download libjpeg, libtiff, libz, and freetype. It will then
compile 32 and 64-bit versions of the libraries, with both versions of
@ -78,9 +78,9 @@ UNDONE -- webp, jpeg2k not recognized
Building Pillow
---------------
Once the dependencies are built, run `python build.py --clean` to
build and install Pillow in virtualenvs for each python
build. `build.py --dist` will build Windows installers instead of
Once the dependencies are built, run ``python build.py --clean`` to
build and install Pillow in virtualenvs for each Python
build. ``build.py --wheel`` will build wheels instead of
installing into virtualenvs.
UNDONE -- suppressed output, what about failures.
@ -88,6 +88,6 @@ UNDONE -- suppressed output, what about failures.
Testing Pillow
--------------
Build and install Pillow, then run `python test.py` from the
`winbuild` directory.
Build and install Pillow, then run ``python test.py`` from the
``winbuild`` directory.