mirror of
https://github.com/python-pillow/Pillow.git
synced 2024-12-26 01:46:18 +03:00
Merge branch 'master' into dpi_fix
This commit is contained in:
commit
ee5c134b33
|
@ -30,6 +30,10 @@ environment:
|
|||
PIP_DIR: bin
|
||||
TEST_OPTIONS: --processes=0
|
||||
DEPLOY: NO
|
||||
- PYTHON: C:/vp/pypy3
|
||||
EXECUTABLE: bin/pypy.exe
|
||||
PIP_DIR: bin
|
||||
VENV: YES
|
||||
|
||||
|
||||
install:
|
||||
|
@ -43,7 +47,12 @@ install:
|
|||
- ps: |
|
||||
if ($env:PYTHON -eq "c:/vp/pypy2")
|
||||
{
|
||||
c:\pillow\winbuild\appveyor_install_pypy.cmd
|
||||
c:\pillow\winbuild\appveyor_install_pypy2.cmd
|
||||
}
|
||||
- ps: |
|
||||
if ($env:PYTHON -eq "c:/vp/pypy3")
|
||||
{
|
||||
c:\pillow\winbuild\appveyor_install_pypy3.cmd
|
||||
}
|
||||
- ps: |
|
||||
if ($env:PYTHON -eq "c:/msys64/mingw32")
|
||||
|
|
29
.github/workflows/lint.yml
vendored
Normal file
29
.github/workflows/lint.yml
vendored
Normal 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
|
62
CHANGES.rst
62
CHANGES.rst
|
@ -7,12 +7,54 @@ Changelog (Pillow)
|
|||
|
||||
- This is the last Pillow release to support Python 2.7 #3642
|
||||
|
||||
- Depends: Update libwebp to 1.0.3 #3983
|
||||
- Changed WindowsViewer format to PNG #4080
|
||||
[radarhere]
|
||||
|
||||
- Use TIFF orientation #4063
|
||||
[radarhere]
|
||||
|
||||
- Raise the same error if a truncated image is loaded a second time #3965
|
||||
[radarhere]
|
||||
|
||||
- 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, freetype to 2.10.1 #3991
|
||||
[radarhere]
|
||||
|
||||
- Change overflow check to use PY_SSIZE_T_MAX #3964
|
||||
[radarhere]
|
||||
|
||||
- Report reason for pytest skips #3942
|
||||
[hugovk]
|
||||
|
||||
6.1.0 (2019-07-01)
|
||||
------------------
|
||||
|
||||
|
@ -55,7 +97,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
|
||||
|
@ -691,7 +733,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
|
||||
|
@ -1228,7 +1270,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
|
||||
|
@ -1465,7 +1507,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
|
||||
|
@ -1910,7 +1952,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
|
||||
|
@ -2353,7 +2395,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
|
||||
|
@ -2845,7 +2887,7 @@ Changelog (Pillow)
|
|||
- Doc cleanup
|
||||
[wiredfool]
|
||||
|
||||
- Fix `ImageStat` docs #796
|
||||
- Fix ``ImageStat`` docs #796
|
||||
[akx]
|
||||
|
||||
- Added docs for ExifTags #794
|
||||
|
@ -3282,7 +3324,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
|
||||
|
@ -3388,7 +3430,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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -4,7 +4,7 @@ Pillow Tests
|
|||
Test scripts are named ``test_xxx.py`` and use the ``unittest`` module. A base class and helper functions can be found in ``helper.py``.
|
||||
|
||||
Dependencies
|
||||
-----------
|
||||
------------
|
||||
|
||||
Install::
|
||||
|
||||
|
|
|
@ -355,6 +355,12 @@ def on_appveyor():
|
|||
return "APPVEYOR" in os.environ
|
||||
|
||||
|
||||
def on_ci():
|
||||
# Travis and AppVeyor have "CI"
|
||||
# Azure Pipelines has "TF_BUILD"
|
||||
return "CI" in os.environ or "TF_BUILD" in os.environ
|
||||
|
||||
|
||||
if sys.platform == "win32":
|
||||
IMCONVERT = os.environ.get("MAGICK_HOME", "")
|
||||
if IMCONVERT:
|
||||
|
|
BIN
Tests/images/g4_orientation_1.tif
Executable file
BIN
Tests/images/g4_orientation_1.tif
Executable file
Binary file not shown.
BIN
Tests/images/g4_orientation_2.tif
Executable file
BIN
Tests/images/g4_orientation_2.tif
Executable file
Binary file not shown.
BIN
Tests/images/g4_orientation_3.tif
Executable file
BIN
Tests/images/g4_orientation_3.tif
Executable file
Binary file not shown.
BIN
Tests/images/g4_orientation_4.tif
Executable file
BIN
Tests/images/g4_orientation_4.tif
Executable file
Binary file not shown.
BIN
Tests/images/g4_orientation_5.tif
Executable file
BIN
Tests/images/g4_orientation_5.tif
Executable file
Binary file not shown.
BIN
Tests/images/g4_orientation_6.tif
Executable file
BIN
Tests/images/g4_orientation_6.tif
Executable file
Binary file not shown.
BIN
Tests/images/g4_orientation_7.tif
Executable file
BIN
Tests/images/g4_orientation_7.tif
Executable file
Binary file not shown.
BIN
Tests/images/g4_orientation_8.tif
Executable file
BIN
Tests/images/g4_orientation_8.tif
Executable file
Binary file not shown.
BIN
Tests/images/imagedraw_arc_width_non_whole_angle.png
Normal file
BIN
Tests/images/imagedraw_arc_width_non_whole_angle.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 439 B |
BIN
Tests/images/imagedraw_floodfill_not_negative.png
Normal file
BIN
Tests/images/imagedraw_floodfill_not_negative.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 214 B |
BIN
Tests/images/imagedraw_stroke_different.png
Normal file
BIN
Tests/images/imagedraw_stroke_different.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.2 KiB |
BIN
Tests/images/imagedraw_stroke_multiline.png
Normal file
BIN
Tests/images/imagedraw_stroke_multiline.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.0 KiB |
BIN
Tests/images/imagedraw_stroke_same.png
Normal file
BIN
Tests/images/imagedraw_stroke_same.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.3 KiB |
BIN
Tests/images/test_direction_ttb_stroke.png
Normal file
BIN
Tests/images/test_direction_ttb_stroke.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.7 KiB |
|
@ -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
|
||||
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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")
|
||||
|
@ -818,3 +831,12 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
self.assert_image_equal_tofile(
|
||||
im, "Tests/images/old-style-jpeg-compression.png"
|
||||
)
|
||||
|
||||
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)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -23,6 +23,7 @@ class TestImageConvert(PillowTestCase):
|
|||
"RGBX",
|
||||
"CMYK",
|
||||
"YCbCr",
|
||||
"HSV",
|
||||
)
|
||||
|
||||
for mode in modes:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}")
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -8,7 +8,11 @@ try:
|
|||
|
||||
class TestImageGrab(PillowTestCase):
|
||||
def test_grab(self):
|
||||
for im in [ImageGrab.grab(), ImageGrab.grab(include_layered_windows=True)]:
|
||||
for im in [
|
||||
ImageGrab.grab(),
|
||||
ImageGrab.grab(include_layered_windows=True),
|
||||
ImageGrab.grab(all_screens=True),
|
||||
]:
|
||||
self.assert_image(im, im.mode, im.size)
|
||||
|
||||
def test_grabclipboard(self):
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
from PIL import Image, ImageShow
|
||||
|
||||
from .helper import PillowTestCase, hopper
|
||||
from .helper import PillowTestCase, hopper, on_ci, unittest
|
||||
|
||||
|
||||
class TestImageShow(PillowTestCase):
|
||||
|
@ -15,7 +15,7 @@ class TestImageShow(PillowTestCase):
|
|||
# Restore original state
|
||||
ImageShow._viewers.pop()
|
||||
|
||||
def test_show(self):
|
||||
def test_viewer_show(self):
|
||||
class TestViewer(ImageShow.Viewer):
|
||||
methodCalled = False
|
||||
|
||||
|
@ -34,6 +34,12 @@ class TestImageShow(PillowTestCase):
|
|||
# Restore original state
|
||||
ImageShow._viewers.pop(0)
|
||||
|
||||
@unittest.skipUnless(on_ci(), "Only run on CIs")
|
||||
def test_show(self):
|
||||
for mode in ("1", "I;16", "LA", "RGB", "RGBA"):
|
||||
im = hopper(mode)
|
||||
self.assertTrue(ImageShow.show(im))
|
||||
|
||||
def test_viewer(self):
|
||||
viewer = ImageShow.Viewer()
|
||||
|
||||
|
|
|
@ -31,4 +31,8 @@ class TestLocale(PillowTestCase):
|
|||
locale.setlocale(locale.LC_ALL, "polish")
|
||||
except locale.Error:
|
||||
unittest.skip("Polish locale not available")
|
||||
Image.open(path)
|
||||
|
||||
try:
|
||||
Image.open(path)
|
||||
finally:
|
||||
locale.setlocale(locale.LC_ALL, (None, None))
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
#!/bin/bash
|
||||
# install libimagequant
|
||||
|
||||
archive=libimagequant-2.12.3
|
||||
archive=libimagequant-2.12.5
|
||||
|
||||
./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/master/$archive.tar.gz
|
||||
|
||||
|
|
|
@ -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 doesn’t 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 doesn’t 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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -169,7 +169,7 @@ Many of Pillow's features require external libraries:
|
|||
|
||||
* **libimagequant** provides improved color quantization
|
||||
|
||||
* Pillow has been tested with libimagequant **2.6-2.12.3**
|
||||
* Pillow has been tested with libimagequant **2.6-2.12.5**
|
||||
* Libimagequant is licensed GPLv3, which is more restrictive than
|
||||
the Pillow license, therefore we will not be distributing binaries
|
||||
with libimagequant support enabled.
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -11,7 +11,7 @@ or the clipboard to a PIL image memory.
|
|||
|
||||
.. versionadded:: 1.1.3
|
||||
|
||||
.. py:function:: PIL.ImageGrab.grab(bbox=None, include_layered_windows=False)
|
||||
.. py:function:: PIL.ImageGrab.grab(bbox=None, include_layered_windows=False, all_screens=False)
|
||||
|
||||
Take a snapshot of the screen. The pixels inside the bounding box are
|
||||
returned as an "RGB" image on Windows or "RGBA" on macOS.
|
||||
|
@ -20,7 +20,13 @@ or the clipboard to a PIL image memory.
|
|||
.. versionadded:: 1.1.3 (Windows), 3.0.0 (macOS)
|
||||
|
||||
:param bbox: What region to copy. Default is the entire screen.
|
||||
Note that on Windows OS, the top-left point may be negative if ``all_screens=True`` is used.
|
||||
:param include_layered_windows: Includes layered windows. Windows OS only.
|
||||
|
||||
.. versionadded:: 6.1.0
|
||||
:param all_screens: Capture all monitors. Windows OS only.
|
||||
|
||||
.. versionadded:: 6.2.0
|
||||
:return: An image
|
||||
|
||||
.. py:function:: PIL.ImageGrab.grabclipboard()
|
||||
|
|
|
@ -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**.
|
||||
|
||||
|
|
|
@ -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`.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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.
|
||||
|
|
90
docs/releasenotes/6.2.0.rst
Normal file
90
docs/releasenotes/6.2.0.rst
Normal file
|
@ -0,0 +1,90 @@
|
|||
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.
|
||||
|
||||
Image.frombuffer
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
There has been a longstanding warning that the defaults of ``Image.frombuffer``
|
||||
may change in the future for the "raw" decoder. The change will now take place
|
||||
in Pillow 7.0.
|
||||
|
||||
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.
|
|
@ -6,6 +6,7 @@ Release Notes
|
|||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
6.2.0
|
||||
6.1.0
|
||||
6.0.0
|
||||
5.4.1
|
||||
|
|
4
setup.py
4
setup.py
|
@ -432,7 +432,9 @@ class pil_build_ext(build_ext):
|
|||
# pythonX.Y.dll.a is in the /usr/lib/pythonX.Y/config directory
|
||||
_add_directory(
|
||||
library_dirs,
|
||||
os.path.join("/usr/lib", "python%s" % sys.version[:3], "config"),
|
||||
os.path.join(
|
||||
"/usr/lib", "python{}.{}".format(*sys.version_info), "config"
|
||||
),
|
||||
)
|
||||
|
||||
elif sys.platform == "darwin":
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
116
src/PIL/Image.py
116
src/PIL/Image.py
|
@ -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:
|
||||
|
@ -2109,15 +2110,15 @@ class Image(object):
|
|||
Displays this image. This method is mainly intended for
|
||||
debugging purposes.
|
||||
|
||||
On Unix platforms, this method saves the image to a temporary
|
||||
PPM file, and calls the **display**, **eog** or **xv**
|
||||
utility, depending on which one can be found.
|
||||
The image is first saved to a temporary file. By default, it will be in
|
||||
PNG format.
|
||||
|
||||
On macOS, this method saves the image to a temporary PNG file, and
|
||||
opens it with the native Preview application.
|
||||
On Unix, the image is then opened using the **display**, **eog** or
|
||||
**xv** utility, depending on which one can be found.
|
||||
|
||||
On Windows, it saves the image to a temporary BMP file, and uses
|
||||
the standard BMP display utility to show it (usually Paint).
|
||||
On macOS, the image is opened with the native Preview application.
|
||||
|
||||
On Windows, the image is opened with the standard PNG display utility.
|
||||
|
||||
:param title: Optional title to use for the image window,
|
||||
where possible.
|
||||
|
@ -2592,7 +2593,7 @@ def frombuffer(mode, size, data, decoder_name="raw", *args):
|
|||
if decoder_name == "raw":
|
||||
if args == ():
|
||||
warnings.warn(
|
||||
"the frombuffer defaults may change in a future release; "
|
||||
"the frombuffer defaults will change in Pillow 7.0.0; "
|
||||
"for portability, change the call to read:\n"
|
||||
" frombuffer(mode, size, data, 'raw', mode, 0, 1)",
|
||||
RuntimeWarning,
|
||||
|
@ -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)
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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(
|
||||
|
@ -546,18 +592,41 @@ def truetype(font=None, size=10, index=0, encoding="", layout_engine=None):
|
|||
This function loads a font object from the given file or file-like
|
||||
object, and creates a font object for a font of the given size.
|
||||
|
||||
Pillow uses FreeType to open font files. If you are opening many fonts
|
||||
simultaneously on Windows, be aware that Windows limits the number of files
|
||||
that can be open in C at once to 512. If you approach that limit, an
|
||||
``OSError`` may be thrown, reporting that FreeType "cannot open resource".
|
||||
|
||||
This function requires the _imagingft service.
|
||||
|
||||
:param font: A filename or file-like object containing a TrueType font.
|
||||
Under Windows, if the file is not found in this filename,
|
||||
the loader also looks in Windows :file:`fonts/` directory.
|
||||
If the file is not found in this filename, the loader may also
|
||||
search in other directories, such as the :file:`fonts/`
|
||||
directory on Windows or :file:`/Library/Fonts/`,
|
||||
:file:`/System/Library/Fonts/` and :file:`~/Library/Fonts/` on
|
||||
macOS.
|
||||
|
||||
: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.
|
||||
|
|
|
@ -29,7 +29,7 @@ else:
|
|||
raise ImportError("ImageGrab is macOS and Windows only")
|
||||
|
||||
|
||||
def grab(bbox=None, include_layered_windows=False):
|
||||
def grab(bbox=None, include_layered_windows=False, all_screens=False):
|
||||
if sys.platform == "darwin":
|
||||
fh, filepath = tempfile.mkstemp(".png")
|
||||
os.close(fh)
|
||||
|
@ -37,8 +37,10 @@ def grab(bbox=None, include_layered_windows=False):
|
|||
im = Image.open(filepath)
|
||||
im.load()
|
||||
os.unlink(filepath)
|
||||
if bbox:
|
||||
im = im.crop(bbox)
|
||||
else:
|
||||
size, data = grabber(include_layered_windows)
|
||||
offset, size, data = grabber(include_layered_windows, all_screens)
|
||||
im = Image.frombytes(
|
||||
"RGB",
|
||||
size,
|
||||
|
@ -49,8 +51,10 @@ def grab(bbox=None, include_layered_windows=False):
|
|||
(size[0] * 3 + 3) & -4,
|
||||
-1,
|
||||
)
|
||||
if bbox:
|
||||
im = im.crop(bbox)
|
||||
if bbox:
|
||||
x0, y0 = offset
|
||||
left, top, right, bottom = bbox
|
||||
im = im.crop((left - x0, top - y0, right - x0, bottom - y0))
|
||||
return im
|
||||
|
||||
|
||||
|
|
|
@ -105,7 +105,8 @@ class Viewer(object):
|
|||
if sys.platform == "win32":
|
||||
|
||||
class WindowsViewer(Viewer):
|
||||
format = "BMP"
|
||||
format = "PNG"
|
||||
options = {"compress_level": 1}
|
||||
|
||||
def get_command(self, file, **options):
|
||||
return (
|
||||
|
@ -179,7 +180,7 @@ else:
|
|||
with open(path, "r") as f:
|
||||
command = self.get_command_ex(file, **options)[0]
|
||||
subprocess.Popen(
|
||||
["im=$(cat);" + command + " $im;" "rm -f $im"], shell=True, stdin=f
|
||||
["im=$(cat);" + command + " $im; rm -f $im"], shell=True, stdin=f
|
||||
)
|
||||
os.remove(path)
|
||||
return 1
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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:
|
||||
|
@ -1151,6 +1165,7 @@ class TiffImageFile(ImageFile.ImageFile):
|
|||
except ValueError:
|
||||
raise IOError("Couldn't set the image")
|
||||
|
||||
close_self_fp = self._exclusive_fp and not self._is_animated
|
||||
if hasattr(self.fp, "getvalue"):
|
||||
# We've got a stringio like thing passed in. Yay for all in memory.
|
||||
# The decoder needs the entire file in one shot, so there's not
|
||||
|
@ -1164,24 +1179,29 @@ 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.")
|
||||
self.fp.seek(0)
|
||||
if not close_self_fp:
|
||||
self.fp.seek(0)
|
||||
# 4 bytes, otherwise the trace might error out
|
||||
n, err = decoder.decode(b"fpfp")
|
||||
else:
|
||||
# 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:
|
||||
if close_self_fp:
|
||||
self.fp.close()
|
||||
self.fp = None # might be shared
|
||||
|
||||
|
@ -1387,6 +1407,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:
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -324,8 +324,8 @@ typedef HANDLE(__stdcall* Func_SetThreadDpiAwarenessContext)(HANDLE);
|
|||
PyObject*
|
||||
PyImaging_GrabScreenWin32(PyObject* self, PyObject* args)
|
||||
{
|
||||
int width, height;
|
||||
int includeLayeredWindows = 0;
|
||||
int x = 0, y = 0, width, height;
|
||||
int includeLayeredWindows = 0, all_screens = 0;
|
||||
HBITMAP bitmap;
|
||||
BITMAPCOREHEADER core;
|
||||
HDC screen, screen_copy;
|
||||
|
@ -335,7 +335,7 @@ PyImaging_GrabScreenWin32(PyObject* self, PyObject* args)
|
|||
HMODULE user32;
|
||||
Func_SetThreadDpiAwarenessContext SetThreadDpiAwarenessContext_function;
|
||||
|
||||
if (!PyArg_ParseTuple(args, "|i", &includeLayeredWindows))
|
||||
if (!PyArg_ParseTuple(args, "|ii", &includeLayeredWindows, &all_screens))
|
||||
return NULL;
|
||||
|
||||
/* step 1: create a memory DC large enough to hold the
|
||||
|
@ -355,8 +355,15 @@ PyImaging_GrabScreenWin32(PyObject* self, PyObject* args)
|
|||
dpiAwareness = SetThreadDpiAwarenessContext_function((HANDLE) -3);
|
||||
}
|
||||
|
||||
width = GetDeviceCaps(screen, HORZRES);
|
||||
height = GetDeviceCaps(screen, VERTRES);
|
||||
if (all_screens) {
|
||||
x = GetSystemMetrics(SM_XVIRTUALSCREEN);
|
||||
y = GetSystemMetrics(SM_YVIRTUALSCREEN);
|
||||
width = GetSystemMetrics(SM_CXVIRTUALSCREEN);
|
||||
height = GetSystemMetrics(SM_CYVIRTUALSCREEN);
|
||||
} else {
|
||||
width = GetDeviceCaps(screen, HORZRES);
|
||||
height = GetDeviceCaps(screen, VERTRES);
|
||||
}
|
||||
|
||||
if (SetThreadDpiAwarenessContext_function != NULL) {
|
||||
SetThreadDpiAwarenessContext_function(dpiAwareness);
|
||||
|
@ -376,7 +383,7 @@ PyImaging_GrabScreenWin32(PyObject* self, PyObject* args)
|
|||
rop = SRCCOPY;
|
||||
if (includeLayeredWindows)
|
||||
rop |= CAPTUREBLT;
|
||||
if (!BitBlt(screen_copy, 0, 0, width, height, screen, 0, 0, rop))
|
||||
if (!BitBlt(screen_copy, 0, 0, width, height, screen, x, y, rop))
|
||||
goto error;
|
||||
|
||||
/* step 3: extract bits from bitmap */
|
||||
|
@ -398,7 +405,7 @@ PyImaging_GrabScreenWin32(PyObject* self, PyObject* args)
|
|||
DeleteDC(screen_copy);
|
||||
DeleteDC(screen);
|
||||
|
||||
return Py_BuildValue("(ii)N", width, height, buffer);
|
||||
return Py_BuildValue("(ii)(ii)N", x, y, width, height, buffer);
|
||||
|
||||
error:
|
||||
PyErr_SetString(PyExc_IOError, "screen grab failed");
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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");
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
3
winbuild/appveyor_install_pypy3.cmd
Normal file
3
winbuild/appveyor_install_pypy3.cmd
Normal file
|
@ -0,0 +1,3 @@
|
|||
curl -fsSL -o pypy3.zip http://buildbot.pypy.org/nightly/py3.6/pypy-c-jit-97588-7392d01b93d0-win32.zip
|
||||
7z x pypy3.zip -oc:\
|
||||
c:\Python37\Scripts\virtualenv.exe -p c:\pypy-c-jit-97588-7392d01b93d0-win32\pypy3.exe c:\vp\pypy3
|
|
@ -84,7 +84,8 @@ def vc_setup(compiler, bit):
|
|||
arch = "x86" if bit == 32 else "x86_amd64"
|
||||
script = (
|
||||
r"""
|
||||
call "C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat" %s"""
|
||||
call "C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat" %s
|
||||
echo on"""
|
||||
% arch
|
||||
)
|
||||
return script
|
||||
|
@ -191,16 +192,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:
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -67,7 +67,6 @@ def extract_openjpeg(compiler):
|
|||
r"""
|
||||
rem build openjpeg
|
||||
setlocal
|
||||
@echo on
|
||||
cd %%BUILD%%
|
||||
mkdir %%INCLIB%%\openjpeg-2.0
|
||||
copy /Y /B openjpeg-2.0.0-win32-x86\include\openjpeg-2.0 %%INCLIB%%\openjpeg-2.0
|
||||
|
@ -114,6 +113,7 @@ def setup_compiler(compiler):
|
|||
return (
|
||||
r"""setlocal EnableDelayedExpansion
|
||||
call "%%ProgramFiles%%\Microsoft SDKs\Windows\%(env_version)s\Bin\SetEnv.Cmd" /Release %(env_flags)s
|
||||
echo on
|
||||
set INCLIB=%%INCLIB%%\%(inc_dir)s
|
||||
""" # noqa: E501
|
||||
% compiler
|
||||
|
@ -139,12 +139,11 @@ setlocal
|
|||
"""
|
||||
+ vc_setup(compiler, bit)
|
||||
+ r"""
|
||||
@echo on
|
||||
cd /D %%OPENJPEG%%%(inc_dir)s
|
||||
|
||||
%%CMAKE%% -DBUILD_THIRDPARTY:BOOL=OFF -DBUILD_SHARED_LIBS:BOOL=OFF -G "NMake Makefiles" .
|
||||
nmake -f Makefile clean
|
||||
nmake -f Makefile
|
||||
%%CMAKE%% -DBUILD_THIRDPARTY:BOOL=OFF -DBUILD_SHARED_LIBS:BOOL=OFF -DCMAKE_BUILD_TYPE=Release -G "NMake Makefiles" .
|
||||
nmake -nologo -f Makefile clean
|
||||
nmake -nologo -f Makefile
|
||||
copy /Y /B bin\* %%INCLIB%%
|
||||
mkdir %%INCLIB%%\openjpeg-%(op_ver)s
|
||||
copy /Y /B src\lib\openjp2\*.h %%INCLIB%%\openjpeg-%(op_ver)s
|
||||
|
@ -164,9 +163,9 @@ setlocal
|
|||
+ vc_setup(compiler, bit)
|
||||
+ r"""
|
||||
cd /D %%JPEG%%
|
||||
nmake -f makefile.vc setup-vc6
|
||||
nmake -f makefile.vc clean
|
||||
nmake -f makefile.vc libjpeg.lib
|
||||
nmake -nologo -f makefile.vc setup-vc6
|
||||
nmake -nologo -f makefile.vc clean
|
||||
nmake -nologo -f makefile.vc nodebug=1 libjpeg.lib
|
||||
copy /Y /B *.dll %%INCLIB%%
|
||||
copy /Y /B *.lib %%INCLIB%%
|
||||
copy /Y /B j*.h %%INCLIB%%
|
||||
|
@ -175,8 +174,8 @@ endlocal
|
|||
rem Build zlib
|
||||
setlocal
|
||||
cd /D %%ZLIB%%
|
||||
nmake -f win32\Makefile.msc clean
|
||||
nmake -f win32\Makefile.msc zlib.lib
|
||||
nmake -nologo -f win32\Makefile.msc clean
|
||||
nmake -nologo -f win32\Makefile.msc zlib.lib
|
||||
copy /Y /B *.dll %%INCLIB%%
|
||||
copy /Y /B *.lib %%INCLIB%%
|
||||
copy /Y /B zlib.lib %%INCLIB%%\z.lib
|
||||
|
@ -191,7 +190,7 @@ setlocal
|
|||
+ r"""
|
||||
cd /D %%WEBP%%
|
||||
rd /S /Q %%WEBP%%\output\release-static
|
||||
nmake -f Makefile.vc CFG=release-static RTLIBCFG=static OBJDIR=output all
|
||||
nmake -nologo -f Makefile.vc CFG=release-static RTLIBCFG=static OBJDIR=output all
|
||||
copy /Y /B output\release-static\%(webp_platform)s\lib\* %%INCLIB%%
|
||||
mkdir %%INCLIB%%\webp
|
||||
copy /Y /B src\webp\*.h %%INCLIB%%\\webp
|
||||
|
@ -206,8 +205,8 @@ rem do after building jpeg and zlib
|
|||
copy %%~dp0\nmake.opt %%TIFF%%
|
||||
|
||||
cd /D %%TIFF%%
|
||||
nmake -f makefile.vc clean
|
||||
nmake -f makefile.vc lib
|
||||
nmake -nologo -f makefile.vc clean
|
||||
nmake -nologo -f makefile.vc lib
|
||||
copy /Y /B libtiff\*.dll %%INCLIB%%
|
||||
copy /Y /B libtiff\*.lib %%INCLIB%%
|
||||
copy /Y /B libtiff\tiff*.h %%INCLIB%%
|
||||
|
@ -320,7 +319,7 @@ cd /D %%GHOSTSCRIPT%%
|
|||
set WIN64=""
|
||||
"""
|
||||
script += r"""
|
||||
nmake -f psi/msvc.mak
|
||||
nmake -nologo -f psi/msvc.mak
|
||||
copy /Y /B bin\ C:\Python27\
|
||||
endlocal
|
||||
"""
|
||||
|
|
|
@ -8,6 +8,7 @@ pythons = {
|
|||
"pypy2": {"compiler": 7, "vc": 2010},
|
||||
"35": {"compiler": 7.1, "vc": 2015},
|
||||
"36": {"compiler": 7.1, "vc": 2015},
|
||||
"pypy3": {"compiler": 7.1, "vc": 2015},
|
||||
"37": {"compiler": 7.1, "vc": 2015},
|
||||
}
|
||||
|
||||
|
@ -35,9 +36,9 @@ libs = {
|
|||
"dir": "tiff-4.0.10",
|
||||
},
|
||||
"freetype": {
|
||||
"url": "https://download.savannah.gnu.org/releases/freetype/freetype-2.10.0.tar.gz", # noqa: E501
|
||||
"filename": PILLOW_DEPENDS_DIR + "freetype-2.10.0.tar.gz",
|
||||
"dir": "freetype-2.10.0",
|
||||
"url": "https://download.savannah.gnu.org/releases/freetype/freetype-2.10.1.tar.gz", # noqa: E501
|
||||
"filename": PILLOW_DEPENDS_DIR + "freetype-2.10.1.tar.gz",
|
||||
"dir": "freetype-2.10.1",
|
||||
},
|
||||
"lcms": {
|
||||
"url": SF_MIRROR + "/project/lcms/lcms/2.7/lcms2-2.7.zip",
|
||||
|
|
Loading…
Reference in New Issue
Block a user