Merge branch 'main' into progress
6
.git-blame-ignore-revs
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
# Flake8
|
||||||
|
8de95676e0fd89f2326b3953488ab66ff29cd2d0
|
||||||
|
# Format with Black
|
||||||
|
53a7e3500437a9fd5826bc04758f7116bd7e52dc
|
||||||
|
# Format the C code with ClangFormat
|
||||||
|
46b7e86bab79450ec0a2866c6c0c679afb659d17
|
52
.github/workflows/wheels.yml
vendored
|
@ -75,9 +75,9 @@ jobs:
|
||||||
CIBW_TEST_SKIP: "*-macosx_arm64"
|
CIBW_TEST_SKIP: "*-macosx_arm64"
|
||||||
MACOSX_DEPLOYMENT_TARGET: ${{ matrix.macosx_deployment_target }}
|
MACOSX_DEPLOYMENT_TARGET: ${{ matrix.macosx_deployment_target }}
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v3
|
- uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: dist
|
name: dist-${{ matrix.os }}-${{ matrix.archs }}${{ matrix.manylinux && format('-{0}', matrix.manylinux) }}
|
||||||
path: ./wheelhouse/*.whl
|
path: ./wheelhouse/*.whl
|
||||||
|
|
||||||
windows:
|
windows:
|
||||||
|
@ -116,10 +116,7 @@ jobs:
|
||||||
|
|
||||||
& python.exe -m pip install -r .ci/requirements-cibw.txt
|
& python.exe -m pip install -r .ci/requirements-cibw.txt
|
||||||
|
|
||||||
# Cannot cross-compile FriBiDi (only used for tests)
|
& python.exe winbuild\build_prepare.py -v --no-imagequant --architecture=${{ matrix.arch }}
|
||||||
$FLAGS = ("--no-imagequant", "--architecture=${{ matrix.arch }}")
|
|
||||||
if ('${{ matrix.arch }}' -eq 'ARM64') { $FLAGS += "--no-fribidi" }
|
|
||||||
& python.exe winbuild\build_prepare.py -v @FLAGS
|
|
||||||
shell: pwsh
|
shell: pwsh
|
||||||
|
|
||||||
- name: Build wheels
|
- name: Build wheels
|
||||||
|
@ -157,24 +154,16 @@ jobs:
|
||||||
shell: cmd
|
shell: cmd
|
||||||
|
|
||||||
- name: Upload wheels
|
- name: Upload wheels
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: dist
|
name: dist-windows-${{ matrix.arch }}
|
||||||
path: ./wheelhouse/*.whl
|
path: ./wheelhouse/*.whl
|
||||||
|
|
||||||
- name: Prepare to upload FriBiDi
|
|
||||||
if: "matrix.arch != 'ARM64'"
|
|
||||||
run: |
|
|
||||||
mkdir fribidi\${{ matrix.arch }}
|
|
||||||
copy winbuild\build\bin\fribidi* fribidi\${{ matrix.arch }}
|
|
||||||
shell: cmd
|
|
||||||
|
|
||||||
- name: Upload fribidi.dll
|
- name: Upload fribidi.dll
|
||||||
if: "matrix.arch != 'ARM64'"
|
uses: actions/upload-artifact@v4
|
||||||
uses: actions/upload-artifact@v3
|
|
||||||
with:
|
with:
|
||||||
name: fribidi
|
name: fribidi-windows-${{ matrix.arch }}
|
||||||
path: fribidi\*
|
path: winbuild\build\bin\fribidi*
|
||||||
|
|
||||||
sdist:
|
sdist:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
@ -190,17 +179,26 @@ jobs:
|
||||||
|
|
||||||
- run: make sdist
|
- run: make sdist
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v3
|
- uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: dist
|
name: dist-sdist
|
||||||
path: dist/*.tar.gz
|
path: dist/*.tar.gz
|
||||||
|
|
||||||
success:
|
pypi-publish:
|
||||||
permissions:
|
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
|
||||||
contents: none
|
|
||||||
needs: [build, windows, sdist]
|
needs: [build, windows, sdist]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
name: Wheels Successful
|
name: Upload release to PyPI
|
||||||
|
environment:
|
||||||
|
name: release-pypi
|
||||||
|
url: https://pypi.org/p/Pillow
|
||||||
|
permissions:
|
||||||
|
id-token: write
|
||||||
steps:
|
steps:
|
||||||
- name: Success
|
- uses: actions/download-artifact@v4
|
||||||
run: echo Wheels Successful
|
with:
|
||||||
|
pattern: dist-*
|
||||||
|
path: dist
|
||||||
|
merge-multiple: true
|
||||||
|
- name: Publish to PyPI
|
||||||
|
uses: pypa/gh-action-pypi-publish@release/v1
|
||||||
|
|
|
@ -5,6 +5,9 @@ Changelog (Pillow)
|
||||||
10.2.0 (unreleased)
|
10.2.0 (unreleased)
|
||||||
-------------------
|
-------------------
|
||||||
|
|
||||||
|
- Fix incorrect color blending for overlapping glyphs #7497
|
||||||
|
[ZachNagengast, nulano, radarhere]
|
||||||
|
|
||||||
- Attempt memory mapping when tile args is a string #7565
|
- Attempt memory mapping when tile args is a string #7565
|
||||||
[radarhere]
|
[radarhere]
|
||||||
|
|
||||||
|
|
BIN
Tests/fonts/CBDTTestFont.ttf
Normal file
BIN
Tests/fonts/EBDTTestFont.ttf
Normal file
|
@ -2,7 +2,6 @@
|
||||||
NotoNastaliqUrdu-Regular.ttf and NotoSansSymbols-Regular.ttf, from https://github.com/googlei18n/noto-fonts
|
NotoNastaliqUrdu-Regular.ttf and NotoSansSymbols-Regular.ttf, from https://github.com/googlei18n/noto-fonts
|
||||||
NotoSans-Regular.ttf, from https://www.google.com/get/noto/
|
NotoSans-Regular.ttf, from https://www.google.com/get/noto/
|
||||||
NotoSansJP-Thin.otf, from https://www.google.com/get/noto/help/cjk/
|
NotoSansJP-Thin.otf, from https://www.google.com/get/noto/help/cjk/
|
||||||
NotoColorEmoji.ttf, from https://github.com/googlefonts/noto-emoji
|
|
||||||
AdobeVFPrototype.ttf, from https://github.com/adobe-fonts/adobe-variable-font-prototype
|
AdobeVFPrototype.ttf, from https://github.com/adobe-fonts/adobe-variable-font-prototype
|
||||||
TINY5x3GX.ttf, from http://velvetyne.fr/fonts/tiny
|
TINY5x3GX.ttf, from http://velvetyne.fr/fonts/tiny
|
||||||
ArefRuqaa-Regular.ttf, from https://github.com/google/fonts/tree/master/ofl/arefruqaa
|
ArefRuqaa-Regular.ttf, from https://github.com/google/fonts/tree/master/ofl/arefruqaa
|
||||||
|
@ -25,3 +24,5 @@ FreeMono.ttf is licensed under GPLv3.
|
||||||
10x20-ISO8859-1.pcf, from https://packages.ubuntu.com/xenial/xfonts-base
|
10x20-ISO8859-1.pcf, from https://packages.ubuntu.com/xenial/xfonts-base
|
||||||
|
|
||||||
"Public domain font. Share and enjoy."
|
"Public domain font. Share and enjoy."
|
||||||
|
|
||||||
|
CBDTTestFont.ttf and EBDTTestFont.ttf from https://github.com/nulano/font-tests are public domain.
|
||||||
|
|
BIN
Tests/images/bitmap_font_blend.png
Normal file
After Width: | Height: | Size: 387 B |
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.7 KiB |
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.7 KiB |
BIN
Tests/images/cbdt.png
Normal file
After Width: | Height: | Size: 348 B |
BIN
Tests/images/cbdt_mask.png
Normal file
After Width: | Height: | Size: 367 B |
Before Width: | Height: | Size: 3.6 KiB |
Before Width: | Height: | Size: 3.5 KiB |
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 3.6 KiB |
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 4.2 KiB |
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 4.2 KiB |
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 4.9 KiB |
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 4.9 KiB |
|
@ -1,5 +1,7 @@
|
||||||
#!/usr/bin/python3
|
#!/usr/bin/python3
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
# Copyright 2020 Google LLC
|
# Copyright 2020 Google LLC
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
@ -13,7 +15,6 @@
|
||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
|
|
||||||
import atheris
|
import atheris
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
#!/usr/bin/python3
|
#!/usr/bin/python3
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
# Copyright 2020 Google LLC
|
# Copyright 2020 Google LLC
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
@ -13,7 +15,6 @@
|
||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
|
|
||||||
import atheris
|
import atheris
|
||||||
|
|
|
@ -859,6 +859,19 @@ def test_bitmap_font_stroke(layout_engine):
|
||||||
assert_image_similar_tofile(im, target, 0.03)
|
assert_image_similar_tofile(im, target, 0.03)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("embedded_color", (False, True))
|
||||||
|
def test_bitmap_blend(layout_engine, embedded_color):
|
||||||
|
font = ImageFont.truetype(
|
||||||
|
"Tests/fonts/EBDTTestFont.ttf", size=64, layout_engine=layout_engine
|
||||||
|
)
|
||||||
|
|
||||||
|
im = Image.new("RGBA", (128, 96), "white")
|
||||||
|
d = ImageDraw.Draw(im)
|
||||||
|
d.text((16, 16), "AA", font=font, fill="#8E2F52", embedded_color=embedded_color)
|
||||||
|
|
||||||
|
assert_image_equal_tofile(im, "Tests/images/bitmap_font_blend.png")
|
||||||
|
|
||||||
|
|
||||||
def test_standard_embedded_color(layout_engine):
|
def test_standard_embedded_color(layout_engine):
|
||||||
txt = "Hello World!"
|
txt = "Hello World!"
|
||||||
ttf = ImageFont.truetype(FONT_PATH, 40, layout_engine=layout_engine)
|
ttf = ImageFont.truetype(FONT_PATH, 40, layout_engine=layout_engine)
|
||||||
|
@ -897,15 +910,15 @@ def test_float_coord(layout_engine, fontmode):
|
||||||
def test_cbdt(layout_engine):
|
def test_cbdt(layout_engine):
|
||||||
try:
|
try:
|
||||||
font = ImageFont.truetype(
|
font = ImageFont.truetype(
|
||||||
"Tests/fonts/NotoColorEmoji.ttf", size=109, layout_engine=layout_engine
|
"Tests/fonts/CBDTTestFont.ttf", size=64, layout_engine=layout_engine
|
||||||
)
|
)
|
||||||
|
|
||||||
im = Image.new("RGB", (150, 150), "white")
|
im = Image.new("RGB", (128, 96), "white")
|
||||||
d = ImageDraw.Draw(im)
|
d = ImageDraw.Draw(im)
|
||||||
|
|
||||||
d.text((10, 10), "\U0001f469", font=font, embedded_color=True)
|
d.text((16, 16), "AB", font=font, embedded_color=True)
|
||||||
|
|
||||||
assert_image_similar_tofile(im, "Tests/images/cbdt_notocoloremoji.png", 6.2)
|
assert_image_equal_tofile(im, "Tests/images/cbdt.png")
|
||||||
except OSError as e: # pragma: no cover
|
except OSError as e: # pragma: no cover
|
||||||
assert str(e) in ("unimplemented feature", "unknown file format")
|
assert str(e) in ("unimplemented feature", "unknown file format")
|
||||||
pytest.skip("freetype compiled without libpng or CBDT support")
|
pytest.skip("freetype compiled without libpng or CBDT support")
|
||||||
|
@ -914,17 +927,15 @@ def test_cbdt(layout_engine):
|
||||||
def test_cbdt_mask(layout_engine):
|
def test_cbdt_mask(layout_engine):
|
||||||
try:
|
try:
|
||||||
font = ImageFont.truetype(
|
font = ImageFont.truetype(
|
||||||
"Tests/fonts/NotoColorEmoji.ttf", size=109, layout_engine=layout_engine
|
"Tests/fonts/CBDTTestFont.ttf", size=64, layout_engine=layout_engine
|
||||||
)
|
)
|
||||||
|
|
||||||
im = Image.new("RGB", (150, 150), "white")
|
im = Image.new("RGB", (128, 96), "white")
|
||||||
d = ImageDraw.Draw(im)
|
d = ImageDraw.Draw(im)
|
||||||
|
|
||||||
d.text((10, 10), "\U0001f469", "black", font=font)
|
d.text((16, 16), "AB", "green", font=font)
|
||||||
|
|
||||||
assert_image_similar_tofile(
|
assert_image_equal_tofile(im, "Tests/images/cbdt_mask.png")
|
||||||
im, "Tests/images/cbdt_notocoloremoji_mask.png", 6.2
|
|
||||||
)
|
|
||||||
except OSError as e: # pragma: no cover
|
except OSError as e: # pragma: no cover
|
||||||
assert str(e) in ("unimplemented feature", "unknown file format")
|
assert str(e) in ("unimplemented feature", "unknown file format")
|
||||||
pytest.skip("freetype compiled without libpng or CBDT support")
|
pytest.skip("freetype compiled without libpng or CBDT support")
|
||||||
|
|
|
@ -233,7 +233,7 @@ htmlhelp_basename = "PillowPILForkdoc"
|
||||||
|
|
||||||
# -- Options for LaTeX output ---------------------------------------------
|
# -- Options for LaTeX output ---------------------------------------------
|
||||||
|
|
||||||
latex_elements = {
|
latex_elements: dict[str, str] = {
|
||||||
# The paper size ('letterpaper' or 'a4paper').
|
# The paper size ('letterpaper' or 'a4paper').
|
||||||
# 'papersize': 'letterpaper',
|
# 'papersize': 'letterpaper',
|
||||||
# The font size ('10pt', '11pt' or '12pt').
|
# The font size ('10pt', '11pt' or '12pt').
|
||||||
|
|
|
@ -5,7 +5,7 @@ from PIL import Image, ImageDraw, ImageFont
|
||||||
font = ImageFont.truetype("Tests/fonts/NotoSans-Regular.ttf", 16)
|
font = ImageFont.truetype("Tests/fonts/NotoSans-Regular.ttf", 16)
|
||||||
|
|
||||||
|
|
||||||
def test(anchor):
|
def test(anchor: str) -> Image.Image:
|
||||||
im = Image.new("RGBA", (200, 100), "white")
|
im = Image.new("RGBA", (200, 100), "white")
|
||||||
d = ImageDraw.Draw(im)
|
d = ImageDraw.Draw(im)
|
||||||
d.line(((100, 0), (100, 100)), "gray")
|
d.line(((100, 0), (100, 100)), "gray")
|
||||||
|
|
|
@ -15,7 +15,7 @@ except AttributeError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def testimage():
|
def testimage() -> None:
|
||||||
"""
|
"""
|
||||||
PIL lets you create in-memory images with various pixel types:
|
PIL lets you create in-memory images with various pixel types:
|
||||||
|
|
||||||
|
|
|
@ -24,7 +24,7 @@ class ContainerIO:
|
||||||
file (for example a TAR file).
|
file (for example a TAR file).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, file, offset, length):
|
def __init__(self, file, offset, length) -> None:
|
||||||
"""
|
"""
|
||||||
Create file object.
|
Create file object.
|
||||||
|
|
||||||
|
|
|
@ -1181,7 +1181,7 @@ class Image:
|
||||||
|
|
||||||
return im
|
return im
|
||||||
|
|
||||||
def copy(self):
|
def copy(self) -> Image:
|
||||||
"""
|
"""
|
||||||
Copies this image. Use this method if you wish to paste things
|
Copies this image. Use this method if you wish to paste things
|
||||||
into an image, but still retain the original.
|
into an image, but still retain the original.
|
||||||
|
@ -2467,7 +2467,7 @@ class Image:
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
def seek(self, frame):
|
def seek(self, frame) -> Image:
|
||||||
"""
|
"""
|
||||||
Seeks to the given frame in this sequence file. If you seek
|
Seeks to the given frame in this sequence file. If you seek
|
||||||
beyond the end of the sequence, the method raises an
|
beyond the end of the sequence, the method raises an
|
||||||
|
@ -2554,7 +2554,7 @@ class Image:
|
||||||
|
|
||||||
return self._new(self.im.getband(channel))
|
return self._new(self.im.getband(channel))
|
||||||
|
|
||||||
def tell(self):
|
def tell(self) -> int:
|
||||||
"""
|
"""
|
||||||
Returns the current frame number. See :py:meth:`~PIL.Image.Image.seek`.
|
Returns the current frame number. See :py:meth:`~PIL.Image.Image.seek`.
|
||||||
|
|
||||||
|
|
|
@ -33,6 +33,7 @@ from __future__ import annotations
|
||||||
|
|
||||||
import math
|
import math
|
||||||
import numbers
|
import numbers
|
||||||
|
import struct
|
||||||
|
|
||||||
from . import Image, ImageColor
|
from . import Image, ImageColor
|
||||||
|
|
||||||
|
@ -543,7 +544,8 @@ class ImageDraw:
|
||||||
# font.getmask2(mode="RGBA") returns color in RGB bands and mask in A
|
# font.getmask2(mode="RGBA") returns color in RGB bands and mask in A
|
||||||
# extract mask and set text alpha
|
# extract mask and set text alpha
|
||||||
color, mask = mask, mask.getband(3)
|
color, mask = mask, mask.getband(3)
|
||||||
color.fillband(3, (ink >> 24) & 0xFF)
|
ink_alpha = struct.pack("i", ink)[3]
|
||||||
|
color.fillband(3, ink_alpha)
|
||||||
x, y = coord
|
x, y = coord
|
||||||
self.im.paste(color, (x, y, x + mask.size[0], y + mask.size[1]), mask)
|
self.im.paste(color, (x, y, x + mask.size[0], y + mask.size[1]), mask)
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -15,33 +15,37 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
|
from functools import lru_cache
|
||||||
# mode descriptor cache
|
|
||||||
_modes = None
|
|
||||||
|
|
||||||
|
|
||||||
class ModeDescriptor:
|
class ModeDescriptor:
|
||||||
"""Wrapper for mode strings."""
|
"""Wrapper for mode strings."""
|
||||||
|
|
||||||
def __init__(self, mode, bands, basemode, basetype, typestr):
|
def __init__(
|
||||||
|
self,
|
||||||
|
mode: str,
|
||||||
|
bands: tuple[str, ...],
|
||||||
|
basemode: str,
|
||||||
|
basetype: str,
|
||||||
|
typestr: str,
|
||||||
|
) -> None:
|
||||||
self.mode = mode
|
self.mode = mode
|
||||||
self.bands = bands
|
self.bands = bands
|
||||||
self.basemode = basemode
|
self.basemode = basemode
|
||||||
self.basetype = basetype
|
self.basetype = basetype
|
||||||
self.typestr = typestr
|
self.typestr = typestr
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self) -> str:
|
||||||
return self.mode
|
return self.mode
|
||||||
|
|
||||||
|
|
||||||
def getmode(mode):
|
@lru_cache
|
||||||
|
def getmode(mode: str) -> ModeDescriptor:
|
||||||
"""Gets a mode descriptor for the given mode."""
|
"""Gets a mode descriptor for the given mode."""
|
||||||
global _modes
|
|
||||||
if not _modes:
|
|
||||||
# initialize mode cache
|
# initialize mode cache
|
||||||
modes = {}
|
|
||||||
endian = "<" if sys.byteorder == "little" else ">"
|
endian = "<" if sys.byteorder == "little" else ">"
|
||||||
for m, (basemode, basetype, bands, typestr) in {
|
|
||||||
|
modes = {
|
||||||
# core modes
|
# core modes
|
||||||
# Bits need to be extended to bytes
|
# Bits need to be extended to bytes
|
||||||
"1": ("L", "L", ("1",), "|b1"),
|
"1": ("L", "L", ("1",), "|b1"),
|
||||||
|
@ -65,10 +69,12 @@ def getmode(mode):
|
||||||
"LA": ("L", "L", ("L", "A"), "|u1"),
|
"LA": ("L", "L", ("L", "A"), "|u1"),
|
||||||
"La": ("L", "L", ("L", "a"), "|u1"),
|
"La": ("L", "L", ("L", "a"), "|u1"),
|
||||||
"PA": ("RGB", "L", ("P", "A"), "|u1"),
|
"PA": ("RGB", "L", ("P", "A"), "|u1"),
|
||||||
}.items():
|
}
|
||||||
modes[m] = ModeDescriptor(m, bands, basemode, basetype, typestr)
|
if mode in modes:
|
||||||
# mapping modes
|
base_mode, base_type, bands, type_str = modes[mode]
|
||||||
for i16mode, typestr in {
|
return ModeDescriptor(mode, bands, base_mode, base_type, type_str)
|
||||||
|
|
||||||
|
mapping_modes = {
|
||||||
# I;16 == I;16L, and I;32 == I;32L
|
# I;16 == I;16L, and I;32 == I;32L
|
||||||
"I;16": "<u2",
|
"I;16": "<u2",
|
||||||
"I;16S": "<i2",
|
"I;16S": "<i2",
|
||||||
|
@ -84,8 +90,7 @@ def getmode(mode):
|
||||||
"I;32S": "<i4",
|
"I;32S": "<i4",
|
||||||
"I;32BS": ">i4",
|
"I;32BS": ">i4",
|
||||||
"I;32LS": "<i4",
|
"I;32LS": "<i4",
|
||||||
}.items():
|
}
|
||||||
modes[i16mode] = ModeDescriptor(i16mode, ("I",), "L", "L", typestr)
|
|
||||||
# set global mode cache atomically
|
type_str = mapping_modes[mode]
|
||||||
_modes = modes
|
return ModeDescriptor(mode, ("I",), "L", "L", type_str)
|
||||||
return _modes[mode]
|
|
||||||
|
|
|
@ -16,6 +16,10 @@
|
||||||
##
|
##
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Callable
|
||||||
|
|
||||||
|
from . import Image
|
||||||
|
|
||||||
|
|
||||||
class Iterator:
|
class Iterator:
|
||||||
"""
|
"""
|
||||||
|
@ -29,14 +33,14 @@ class Iterator:
|
||||||
:param im: An image object.
|
:param im: An image object.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, im):
|
def __init__(self, im: Image.Image):
|
||||||
if not hasattr(im, "seek"):
|
if not hasattr(im, "seek"):
|
||||||
msg = "im must have seek method"
|
msg = "im must have seek method"
|
||||||
raise AttributeError(msg)
|
raise AttributeError(msg)
|
||||||
self.im = im
|
self.im = im
|
||||||
self.position = getattr(self.im, "_min_frame", 0)
|
self.position = getattr(self.im, "_min_frame", 0)
|
||||||
|
|
||||||
def __getitem__(self, ix):
|
def __getitem__(self, ix: int) -> Image.Image:
|
||||||
try:
|
try:
|
||||||
self.im.seek(ix)
|
self.im.seek(ix)
|
||||||
return self.im
|
return self.im
|
||||||
|
@ -44,10 +48,10 @@ class Iterator:
|
||||||
msg = "end of sequence"
|
msg = "end of sequence"
|
||||||
raise IndexError(msg) from e
|
raise IndexError(msg) from e
|
||||||
|
|
||||||
def __iter__(self):
|
def __iter__(self) -> Iterator:
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def __next__(self):
|
def __next__(self) -> Image.Image:
|
||||||
try:
|
try:
|
||||||
self.im.seek(self.position)
|
self.im.seek(self.position)
|
||||||
self.position += 1
|
self.position += 1
|
||||||
|
@ -57,7 +61,10 @@ class Iterator:
|
||||||
raise StopIteration(msg) from e
|
raise StopIteration(msg) from e
|
||||||
|
|
||||||
|
|
||||||
def all_frames(im, func=None):
|
def all_frames(
|
||||||
|
im: Image.Image | list[Image.Image],
|
||||||
|
func: Callable[[Image.Image], Image.Image] | None = None,
|
||||||
|
) -> list[Image.Image]:
|
||||||
"""
|
"""
|
||||||
Applies a given function to all frames in an image or a list of images.
|
Applies a given function to all frames in an image or a list of images.
|
||||||
The frames are returned as a list of separate images.
|
The frames are returned as a list of separate images.
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import io
|
import io
|
||||||
|
from types import TracebackType
|
||||||
|
|
||||||
from . import ContainerIO
|
from . import ContainerIO
|
||||||
|
|
||||||
|
@ -23,7 +24,7 @@ from . import ContainerIO
|
||||||
class TarIO(ContainerIO.ContainerIO):
|
class TarIO(ContainerIO.ContainerIO):
|
||||||
"""A file object that provides read access to a given member of a TAR file."""
|
"""A file object that provides read access to a given member of a TAR file."""
|
||||||
|
|
||||||
def __init__(self, tarfile, file):
|
def __init__(self, tarfile: str, file: str) -> None:
|
||||||
"""
|
"""
|
||||||
Create file object.
|
Create file object.
|
||||||
|
|
||||||
|
@ -57,11 +58,16 @@ class TarIO(ContainerIO.ContainerIO):
|
||||||
super().__init__(self.fh, self.fh.tell(), size)
|
super().__init__(self.fh, self.fh.tell(), size)
|
||||||
|
|
||||||
# Context manager support
|
# Context manager support
|
||||||
def __enter__(self):
|
def __enter__(self) -> TarIO:
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def __exit__(self, *args):
|
def __exit__(
|
||||||
|
self,
|
||||||
|
exc_type: type[BaseException] | None,
|
||||||
|
exc_val: BaseException | None,
|
||||||
|
exc_tb: TracebackType | None,
|
||||||
|
) -> None:
|
||||||
self.close()
|
self.close()
|
||||||
|
|
||||||
def close(self):
|
def close(self) -> None:
|
||||||
self.fh.close()
|
self.fh.close()
|
||||||
|
|
|
@ -1049,8 +1049,8 @@ font_render(FontObject *self, PyObject *args) {
|
||||||
if (yy >= 0 && yy < im->ysize) {
|
if (yy >= 0 && yy < im->ysize) {
|
||||||
/* blend this glyph into the buffer */
|
/* blend this glyph into the buffer */
|
||||||
int k;
|
int k;
|
||||||
unsigned char v;
|
|
||||||
unsigned char *target;
|
unsigned char *target;
|
||||||
|
unsigned int tmp;
|
||||||
if (color) {
|
if (color) {
|
||||||
/* target[RGB] returns the color, target[A] returns the mask */
|
/* target[RGB] returns the color, target[A] returns the mask */
|
||||||
/* target bands get split again in ImageDraw.text */
|
/* target bands get split again in ImageDraw.text */
|
||||||
|
@ -1061,34 +1061,55 @@ font_render(FontObject *self, PyObject *args) {
|
||||||
if (color && bitmap.pixel_mode == FT_PIXEL_MODE_BGRA) {
|
if (color && bitmap.pixel_mode == FT_PIXEL_MODE_BGRA) {
|
||||||
/* paste color glyph */
|
/* paste color glyph */
|
||||||
for (k = x0; k < x1; k++) {
|
for (k = x0; k < x1; k++) {
|
||||||
if (target[k * 4 + 3] < source[k * 4 + 3]) {
|
unsigned int src_alpha = source[k * 4 + 3];
|
||||||
/* unpremultiply BGRa to RGBA */
|
|
||||||
target[k * 4 + 0] = CLIP8(
|
/* paste only if source has data */
|
||||||
(255 * (int)source[k * 4 + 2]) / source[k * 4 + 3]);
|
if (src_alpha > 0) {
|
||||||
target[k * 4 + 1] = CLIP8(
|
/* unpremultiply BGRa */
|
||||||
(255 * (int)source[k * 4 + 1]) / source[k * 4 + 3]);
|
int src_red = CLIP8((255 * (int)source[k * 4 + 2]) / src_alpha);
|
||||||
target[k * 4 + 2] = CLIP8(
|
int src_green = CLIP8((255 * (int)source[k * 4 + 1]) / src_alpha);
|
||||||
(255 * (int)source[k * 4 + 0]) / source[k * 4 + 3]);
|
int src_blue = CLIP8((255 * (int)source[k * 4 + 0]) / src_alpha);
|
||||||
target[k * 4 + 3] = source[k * 4 + 3];
|
|
||||||
|
/* blend required if target has data */
|
||||||
|
if (target[k * 4 + 3] > 0) {
|
||||||
|
/* blend RGBA colors */
|
||||||
|
target[k * 4 + 0] = BLEND(src_alpha, target[k * 4 + 0], src_red, tmp);
|
||||||
|
target[k * 4 + 1] = BLEND(src_alpha, target[k * 4 + 1], src_green, tmp);
|
||||||
|
target[k * 4 + 2] = BLEND(src_alpha, target[k * 4 + 2], src_blue, tmp);
|
||||||
|
target[k * 4 + 3] = CLIP8(src_alpha + MULDIV255(target[k * 4 + 3], (255 - src_alpha), tmp));
|
||||||
|
} else {
|
||||||
|
/* paste unpremultiplied RGBA values */
|
||||||
|
target[k * 4 + 0] = src_red;
|
||||||
|
target[k * 4 + 1] = src_green;
|
||||||
|
target[k * 4 + 2] = src_blue;
|
||||||
|
target[k * 4 + 3] = src_alpha;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (bitmap.pixel_mode == FT_PIXEL_MODE_GRAY) {
|
} else if (bitmap.pixel_mode == FT_PIXEL_MODE_GRAY) {
|
||||||
if (color) {
|
if (color) {
|
||||||
unsigned char *ink = (unsigned char *)&foreground_ink;
|
unsigned char *ink = (unsigned char *)&foreground_ink;
|
||||||
for (k = x0; k < x1; k++) {
|
for (k = x0; k < x1; k++) {
|
||||||
v = source[k] * convert_scale;
|
unsigned int src_alpha = source[k] * convert_scale;
|
||||||
if (target[k * 4 + 3] < v) {
|
if (src_alpha > 0) {
|
||||||
|
if (target[k * 4 + 3] > 0) {
|
||||||
|
target[k * 4 + 0] = BLEND(src_alpha, target[k * 4 + 0], ink[0], tmp);
|
||||||
|
target[k * 4 + 1] = BLEND(src_alpha, target[k * 4 + 1], ink[1], tmp);
|
||||||
|
target[k * 4 + 2] = BLEND(src_alpha, target[k * 4 + 2], ink[2], tmp);
|
||||||
|
target[k * 4 + 3] = CLIP8(src_alpha + MULDIV255(target[k * 4 + 3], (255 - src_alpha), tmp));
|
||||||
|
} else {
|
||||||
target[k * 4 + 0] = ink[0];
|
target[k * 4 + 0] = ink[0];
|
||||||
target[k * 4 + 1] = ink[1];
|
target[k * 4 + 1] = ink[1];
|
||||||
target[k * 4 + 2] = ink[2];
|
target[k * 4 + 2] = ink[2];
|
||||||
target[k * 4 + 3] = v;
|
target[k * 4 + 3] = src_alpha;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
for (k = x0; k < x1; k++) {
|
for (k = x0; k < x1; k++) {
|
||||||
v = source[k] * convert_scale;
|
unsigned int src_alpha = source[k] * convert_scale;
|
||||||
if (target[k] < v) {
|
if (src_alpha > 0) {
|
||||||
target[k] = v;
|
target[k] = target[k] > 0 ? CLIP8(src_alpha + MULDIV255(target[k], (255 - src_alpha), tmp)) : src_alpha;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -56,7 +56,9 @@ def cmd_nmake(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def cmds_cmake(target: str | tuple[str, ...] | list[str], *params) -> list[str]:
|
def cmds_cmake(
|
||||||
|
target: str | tuple[str, ...] | list[str], *params, build_dir: str = "."
|
||||||
|
) -> list[str]:
|
||||||
if not isinstance(target, str):
|
if not isinstance(target, str):
|
||||||
target = " ".join(target)
|
target = " ".join(target)
|
||||||
|
|
||||||
|
@ -73,10 +75,11 @@ def cmds_cmake(target: str | tuple[str, ...] | list[str], *params) -> list[str]:
|
||||||
"-DCMAKE_CXX_FLAGS=-nologo",
|
"-DCMAKE_CXX_FLAGS=-nologo",
|
||||||
*params,
|
*params,
|
||||||
'-G "{cmake_generator}"',
|
'-G "{cmake_generator}"',
|
||||||
".",
|
f'-B "{build_dir}"',
|
||||||
|
"-S .",
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
f"{{cmake}} --build . --clean-first --parallel --target {target}",
|
f'{{cmake}} --build "{build_dir}" --clean-first --parallel --target {target}',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -367,7 +370,14 @@ DEPS = {
|
||||||
"build": [
|
"build": [
|
||||||
cmd_copy(r"COPYING", r"{bin_dir}\fribidi-1.0.13-COPYING"),
|
cmd_copy(r"COPYING", r"{bin_dir}\fribidi-1.0.13-COPYING"),
|
||||||
cmd_copy(r"{winbuild_dir}\fribidi.cmake", r"CMakeLists.txt"),
|
cmd_copy(r"{winbuild_dir}\fribidi.cmake", r"CMakeLists.txt"),
|
||||||
*cmds_cmake("fribidi"),
|
# generated tab.i files cannot be cross-compiled
|
||||||
|
" ^&^& ".join(
|
||||||
|
[
|
||||||
|
"if {architecture}==ARM64 cmd /c call {vcvarsall} x86",
|
||||||
|
*cmds_cmake("fribidi-gen", "-DARCH=x86", build_dir="build_x86"),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
*cmds_cmake("fribidi", "-DARCH={architecture}"),
|
||||||
],
|
],
|
||||||
"bins": [r"*.dll"],
|
"bins": [r"*.dll"],
|
||||||
},
|
},
|
||||||
|
@ -381,10 +391,9 @@ def find_msvs(architecture: str) -> dict[str, str] | None:
|
||||||
print("Program Files not found")
|
print("Program Files not found")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
requires = ["-requires", "Microsoft.VisualStudio.Component.VC.Tools.x86.x64"]
|
||||||
if architecture == "ARM64":
|
if architecture == "ARM64":
|
||||||
tools = "Microsoft.VisualStudio.Component.VC.Tools.ARM64"
|
requires += ["-requires", "Microsoft.VisualStudio.Component.VC.Tools.ARM64"]
|
||||||
else:
|
|
||||||
tools = "Microsoft.VisualStudio.Component.VC.Tools.x86.x64"
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
vspath = (
|
vspath = (
|
||||||
|
@ -395,8 +404,7 @@ def find_msvs(architecture: str) -> dict[str, str] | None:
|
||||||
),
|
),
|
||||||
"-latest",
|
"-latest",
|
||||||
"-prerelease",
|
"-prerelease",
|
||||||
"-requires",
|
*requires,
|
||||||
tools,
|
|
||||||
"-property",
|
"-property",
|
||||||
"installationPath",
|
"installationPath",
|
||||||
"-products",
|
"-products",
|
||||||
|
@ -707,11 +715,6 @@ if __name__ == "__main__":
|
||||||
disabled += ["libimagequant"]
|
disabled += ["libimagequant"]
|
||||||
if args.no_fribidi:
|
if args.no_fribidi:
|
||||||
disabled += ["fribidi"]
|
disabled += ["fribidi"]
|
||||||
elif args.architecture == "ARM64" and platform.machine() != "ARM64":
|
|
||||||
import warnings
|
|
||||||
|
|
||||||
warnings.warn("Cross-compiling FriBiDi is currently not supported, disabling")
|
|
||||||
disabled += ["fribidi"]
|
|
||||||
|
|
||||||
prefs = {
|
prefs = {
|
||||||
"architecture": args.architecture,
|
"architecture": args.architecture,
|
||||||
|
|
|
@ -2,9 +2,9 @@ cmake_minimum_required(VERSION 3.12)
|
||||||
|
|
||||||
project(fribidi)
|
project(fribidi)
|
||||||
|
|
||||||
|
|
||||||
add_definitions(-D_CRT_SECURE_NO_WARNINGS)
|
add_definitions(-D_CRT_SECURE_NO_WARNINGS)
|
||||||
|
|
||||||
include_directories(${CMAKE_CURRENT_BINARY_DIR})
|
|
||||||
include_directories(lib)
|
include_directories(lib)
|
||||||
|
|
||||||
function(extract_regex_1 var text regex)
|
function(extract_regex_1 var text regex)
|
||||||
|
@ -27,12 +27,20 @@ function(fribidi_conf)
|
||||||
set(PACKAGE_BUGREPORT "https://github.com/fribidi/fribidi/issues/new")
|
set(PACKAGE_BUGREPORT "https://github.com/fribidi/fribidi/issues/new")
|
||||||
set(SIZEOF_INT 4)
|
set(SIZEOF_INT 4)
|
||||||
set(FRIBIDI_MSVC_BUILD_PLACEHOLDER "#define FRIBIDI_BUILT_WITH_MSVC")
|
set(FRIBIDI_MSVC_BUILD_PLACEHOLDER "#define FRIBIDI_BUILT_WITH_MSVC")
|
||||||
message("detected ${PACKAGE_NAME} version ${FRIBIDI_VERSION}")
|
message("Detected ${PACKAGE_NAME} version ${FRIBIDI_VERSION}")
|
||||||
configure_file(lib/fribidi-config.h.in lib/fribidi-config.h @ONLY)
|
configure_file(lib/fribidi-config.h.in ${CMAKE_CURRENT_SOURCE_DIR}/lib/fribidi-config.h @ONLY)
|
||||||
endfunction()
|
endfunction()
|
||||||
fribidi_conf()
|
fribidi_conf()
|
||||||
|
|
||||||
|
|
||||||
|
option(ARCH "Target architecture")
|
||||||
|
if(${ARCH} STREQUAL ARM64)
|
||||||
|
set(GEN FALSE)
|
||||||
|
else()
|
||||||
|
set(GEN TRUE)
|
||||||
|
endif()
|
||||||
|
message("Generate tab.i files: " ${GEN})
|
||||||
|
|
||||||
function(prepend var prefix)
|
function(prepend var prefix)
|
||||||
set(out "")
|
set(out "")
|
||||||
foreach(f ${ARGN})
|
foreach(f ${ARGN})
|
||||||
|
@ -56,7 +64,8 @@ macro(fribidi_definitions _TGT)
|
||||||
endmacro()
|
endmacro()
|
||||||
|
|
||||||
function(fribidi_gen _NAME _OUTNAME _PARAM)
|
function(fribidi_gen _NAME _OUTNAME _PARAM)
|
||||||
set(_OUT lib/${_OUTNAME})
|
set(_OUT ${CMAKE_CURRENT_SOURCE_DIR}/lib/${_OUTNAME})
|
||||||
|
if(GEN)
|
||||||
prepend(_DEP "${CMAKE_CURRENT_SOURCE_DIR}/gen.tab/" ${ARGN})
|
prepend(_DEP "${CMAKE_CURRENT_SOURCE_DIR}/gen.tab/" ${ARGN})
|
||||||
add_executable(gen-${_NAME}
|
add_executable(gen-${_NAME}
|
||||||
gen.tab/gen-${_NAME}.c
|
gen.tab/gen-${_NAME}.c
|
||||||
|
@ -68,6 +77,7 @@ function(fribidi_gen _NAME _OUTNAME _PARAM)
|
||||||
COMMAND gen-${_NAME} ${_PARAM} ${_DEP} > ${_OUT}
|
COMMAND gen-${_NAME} ${_PARAM} ${_DEP} > ${_OUT}
|
||||||
DEPENDS ${_DEP}
|
DEPENDS ${_DEP}
|
||||||
OUTPUT ${_OUT})
|
OUTPUT ${_OUT})
|
||||||
|
endif(GEN)
|
||||||
list(APPEND FRIBIDI_SOURCES_GENERATED "${_OUT}")
|
list(APPEND FRIBIDI_SOURCES_GENERATED "${_OUT}")
|
||||||
set(FRIBIDI_SOURCES_GENERATED ${FRIBIDI_SOURCES_GENERATED} PARENT_SCOPE)
|
set(FRIBIDI_SOURCES_GENERATED ${FRIBIDI_SOURCES_GENERATED} PARENT_SCOPE)
|
||||||
endfunction()
|
endfunction()
|
||||||
|
@ -78,8 +88,10 @@ fribidi_gen(unicode-version fribidi-unicode-version.h ""
|
||||||
|
|
||||||
macro(fribidi_tab _NAME)
|
macro(fribidi_tab _NAME)
|
||||||
fribidi_gen(${_NAME}-tab ${_NAME}.tab.i 2 ${ARGN})
|
fribidi_gen(${_NAME}-tab ${_NAME}.tab.i 2 ${ARGN})
|
||||||
|
if(GEN)
|
||||||
target_sources(gen-${_NAME}-tab
|
target_sources(gen-${_NAME}-tab
|
||||||
PRIVATE lib/fribidi-unicode-version.h)
|
PRIVATE lib/fribidi-unicode-version.h)
|
||||||
|
endif(GEN)
|
||||||
endmacro()
|
endmacro()
|
||||||
|
|
||||||
fribidi_tab(bidi-type unidata/UnicodeData.txt)
|
fribidi_tab(bidi-type unidata/UnicodeData.txt)
|
||||||
|
@ -89,6 +101,8 @@ fribidi_tab(mirroring unidata/BidiMirroring.txt)
|
||||||
fribidi_tab(brackets unidata/BidiBrackets.txt unidata/UnicodeData.txt)
|
fribidi_tab(brackets unidata/BidiBrackets.txt unidata/UnicodeData.txt)
|
||||||
fribidi_tab(brackets-type unidata/BidiBrackets.txt)
|
fribidi_tab(brackets-type unidata/BidiBrackets.txt)
|
||||||
|
|
||||||
|
add_custom_target(fribidi-gen DEPENDS ${FRIBIDI_SOURCES_GENERATED})
|
||||||
|
|
||||||
|
|
||||||
file(GLOB FRIBIDI_SOURCES lib/*.c)
|
file(GLOB FRIBIDI_SOURCES lib/*.c)
|
||||||
file(GLOB FRIBIDI_HEADERS lib/*.h)
|
file(GLOB FRIBIDI_HEADERS lib/*.h)
|
||||||
|
|