mirror of
https://github.com/python-pillow/Pillow.git
synced 2025-02-06 22:50:55 +03:00
Merge branch 'main' into context_manager
This commit is contained in:
commit
6af0425283
|
@ -21,7 +21,7 @@ set -e
|
||||||
|
|
||||||
if [[ $(uname) != CYGWIN* ]]; then
|
if [[ $(uname) != CYGWIN* ]]; then
|
||||||
sudo apt-get -qq install libfreetype6-dev liblcms2-dev python3-tk\
|
sudo apt-get -qq install libfreetype6-dev liblcms2-dev python3-tk\
|
||||||
ghostscript libffi-dev libjpeg-turbo-progs libopenjp2-7-dev\
|
ghostscript libjpeg-turbo-progs libopenjp2-7-dev\
|
||||||
cmake meson imagemagick libharfbuzz-dev libfribidi-dev\
|
cmake meson imagemagick libharfbuzz-dev libfribidi-dev\
|
||||||
sway wl-clipboard libopenblas-dev
|
sway wl-clipboard libopenblas-dev
|
||||||
fi
|
fi
|
||||||
|
@ -38,12 +38,7 @@ python3 -m pip install -U pytest-timeout
|
||||||
python3 -m pip install pyroma
|
python3 -m pip install pyroma
|
||||||
|
|
||||||
if [[ $(uname) != CYGWIN* ]]; then
|
if [[ $(uname) != CYGWIN* ]]; then
|
||||||
# TODO Update condition when NumPy supports free-threading
|
python3 -m pip install numpy
|
||||||
if [[ "$PYTHON_GIL" == "0" ]]; then
|
|
||||||
python3 -m pip install numpy --index-url https://pypi.anaconda.org/scientific-python-nightly-wheels/simple
|
|
||||||
else
|
|
||||||
python3 -m pip install numpy
|
|
||||||
fi
|
|
||||||
|
|
||||||
# PyQt6 doesn't support PyPy3
|
# PyQt6 doesn't support PyPy3
|
||||||
if [[ $GHA_PYTHON_VERSION == 3.* ]]; then
|
if [[ $GHA_PYTHON_VERSION == 3.* ]]; then
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
cibuildwheel==2.20.0
|
cibuildwheel==2.21.1
|
||||||
|
|
4
.github/workflows/test.yml
vendored
4
.github/workflows/test.yml
vendored
|
@ -37,7 +37,7 @@ jobs:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
os: [
|
os: [
|
||||||
"macos-14",
|
"macos-latest",
|
||||||
"ubuntu-latest",
|
"ubuntu-latest",
|
||||||
]
|
]
|
||||||
python-version: [
|
python-version: [
|
||||||
|
@ -56,7 +56,7 @@ jobs:
|
||||||
# M1 only available for 3.10+
|
# M1 only available for 3.10+
|
||||||
- { os: "macos-13", python-version: "3.9" }
|
- { os: "macos-13", python-version: "3.9" }
|
||||||
exclude:
|
exclude:
|
||||||
- { os: "macos-14", python-version: "3.9" }
|
- { os: "macos-latest", python-version: "3.9" }
|
||||||
|
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
name: ${{ matrix.os }} Python ${{ matrix.python-version }} ${{ matrix.disable-gil && 'free-threaded' || '' }}
|
name: ${{ matrix.os }} Python ${{ matrix.python-version }} ${{ matrix.disable-gil && 'free-threaded' || '' }}
|
||||||
|
|
6
.github/workflows/wheels-dependencies.sh
vendored
6
.github/workflows/wheels-dependencies.sh
vendored
|
@ -21,10 +21,10 @@ if [[ "$MB_ML_VER" != 2014 ]]; then
|
||||||
else
|
else
|
||||||
HARFBUZZ_VERSION=8.5.0
|
HARFBUZZ_VERSION=8.5.0
|
||||||
fi
|
fi
|
||||||
LIBPNG_VERSION=1.6.43
|
LIBPNG_VERSION=1.6.44
|
||||||
JPEGTURBO_VERSION=3.0.3
|
JPEGTURBO_VERSION=3.0.4
|
||||||
OPENJPEG_VERSION=2.5.2
|
OPENJPEG_VERSION=2.5.2
|
||||||
XZ_VERSION=5.4.5
|
XZ_VERSION=5.6.2
|
||||||
TIFF_VERSION=4.6.0
|
TIFF_VERSION=4.6.0
|
||||||
LCMS2_VERSION=2.16
|
LCMS2_VERSION=2.16
|
||||||
if [[ -n "$IS_MACOS" ]]; then
|
if [[ -n "$IS_MACOS" ]]; then
|
||||||
|
|
9
.github/workflows/wheels-test.sh
vendored
9
.github/workflows/wheels-test.sh
vendored
|
@ -13,14 +13,7 @@ else
|
||||||
yum install -y fribidi
|
yum install -y fribidi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ "${AUDITWHEEL_POLICY::9}" != "musllinux" ]; then
|
python3 -m pip install numpy
|
||||||
# TODO Update condition when NumPy supports free-threading
|
|
||||||
if [ $(python3 -c "import sysconfig;print(sysconfig.get_config_var('Py_GIL_DISABLED'))") == "1" ]; then
|
|
||||||
python3 -m pip install numpy --index-url https://pypi.anaconda.org/scientific-python-nightly-wheels/simple
|
|
||||||
else
|
|
||||||
python3 -m pip install numpy
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ ! -d "test-images-main" ]; then
|
if [ ! -d "test-images-main" ]; then
|
||||||
curl -fsSL -o pillow-test-images.zip https://github.com/python-pillow/test-images/archive/main.zip
|
curl -fsSL -o pillow-test-images.zip https://github.com/python-pillow/test-images/archive/main.zip
|
||||||
|
|
12
.github/workflows/wheels.yml
vendored
12
.github/workflows/wheels.yml
vendored
|
@ -102,12 +102,18 @@ jobs:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- name: "macOS x86_64"
|
- name: "macOS 10.10 x86_64"
|
||||||
os: macos-13
|
os: macos-13
|
||||||
cibw_arch: x86_64
|
cibw_arch: x86_64
|
||||||
|
build: "pp310* cp3{9,10,11}*"
|
||||||
macosx_deployment_target: "10.10"
|
macosx_deployment_target: "10.10"
|
||||||
|
- name: "macOS 10.13 x86_64"
|
||||||
|
os: macos-13
|
||||||
|
cibw_arch: x86_64
|
||||||
|
build: "cp3{12,13}*"
|
||||||
|
macosx_deployment_target: "10.13"
|
||||||
- name: "macOS arm64"
|
- name: "macOS arm64"
|
||||||
os: macos-14
|
os: macos-latest
|
||||||
cibw_arch: arm64
|
cibw_arch: arm64
|
||||||
macosx_deployment_target: "11.0"
|
macosx_deployment_target: "11.0"
|
||||||
- name: "manylinux2014 and musllinux x86_64"
|
- name: "manylinux2014 and musllinux x86_64"
|
||||||
|
@ -145,7 +151,7 @@ jobs:
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v4
|
- uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: dist-${{ matrix.os }}-${{ matrix.cibw_arch }}${{ matrix.manylinux && format('-{0}', matrix.manylinux) }}
|
name: dist-${{ matrix.os }}${{ matrix.macosx_deployment_target && format('-{0}', matrix.macosx_deployment_target) }}-${{ matrix.cibw_arch }}${{ matrix.manylinux && format('-{0}', matrix.manylinux) }}
|
||||||
path: ./wheelhouse/*.whl
|
path: ./wheelhouse/*.whl
|
||||||
|
|
||||||
windows:
|
windows:
|
||||||
|
|
|
@ -5,6 +5,15 @@ Changelog (Pillow)
|
||||||
11.0.0 (unreleased)
|
11.0.0 (unreleased)
|
||||||
-------------------
|
-------------------
|
||||||
|
|
||||||
|
- Accept float stroke widths #8369
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Deprecate ICNS (width, height, scale) sizes in favour of load(scale) #8352
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
|
- Improved handling of RGBA palettes when saving GIF images #8366
|
||||||
|
[radarhere]
|
||||||
|
|
||||||
- Deprecate isImageType #8364
|
- Deprecate isImageType #8364
|
||||||
[radarhere]
|
[radarhere]
|
||||||
|
|
||||||
|
|
BIN
Tests/images/imagedraw_stroke_float.png
Normal file
BIN
Tests/images/imagedraw_stroke_float.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.5 KiB |
|
@ -1088,22 +1088,17 @@ class TestImage:
|
||||||
valgrind pytest -qq Tests/test_image.py::TestImage::test_overrun | grep decode.c
|
valgrind pytest -qq Tests/test_image.py::TestImage::test_overrun | grep decode.c
|
||||||
"""
|
"""
|
||||||
with Image.open(os.path.join("Tests/images", path)) as im:
|
with Image.open(os.path.join("Tests/images", path)) as im:
|
||||||
try:
|
with pytest.raises(OSError) as e:
|
||||||
im.load()
|
im.load()
|
||||||
pytest.fail()
|
buffer_overrun = str(e.value) == "buffer overrun when reading image file"
|
||||||
except OSError as e:
|
truncated = "image file is truncated" in str(e.value)
|
||||||
buffer_overrun = str(e) == "buffer overrun when reading image file"
|
|
||||||
truncated = "image file is truncated" in str(e)
|
|
||||||
|
|
||||||
assert buffer_overrun or truncated
|
assert buffer_overrun or truncated
|
||||||
|
|
||||||
def test_fli_overrun2(self) -> None:
|
def test_fli_overrun2(self) -> None:
|
||||||
with Image.open("Tests/images/fli_overrun2.bin") as im:
|
with Image.open("Tests/images/fli_overrun2.bin") as im:
|
||||||
try:
|
with pytest.raises(OSError, match="buffer overrun when reading image file"):
|
||||||
im.seek(1)
|
im.seek(1)
|
||||||
pytest.fail()
|
|
||||||
except OSError as e:
|
|
||||||
assert str(e) == "buffer overrun when reading image file"
|
|
||||||
|
|
||||||
def test_exit_fp(self) -> None:
|
def test_exit_fp(self) -> None:
|
||||||
with Image.new("L", (1, 1)) as im:
|
with Image.new("L", (1, 1)) as im:
|
||||||
|
|
|
@ -49,5 +49,7 @@ def test_copy_zero() -> None:
|
||||||
@skip_unless_feature("libtiff")
|
@skip_unless_feature("libtiff")
|
||||||
def test_deepcopy() -> None:
|
def test_deepcopy() -> None:
|
||||||
with Image.open("Tests/images/g4_orientation_5.tif") as im:
|
with Image.open("Tests/images/g4_orientation_5.tif") as im:
|
||||||
|
assert im.size == (590, 88)
|
||||||
|
|
||||||
out = copy.deepcopy(im)
|
out = copy.deepcopy(im)
|
||||||
assert out.size == (590, 88)
|
assert out.size == (590, 88)
|
||||||
|
|
|
@ -300,9 +300,7 @@ class TestImageResize:
|
||||||
im.resize((10, 10), -1)
|
im.resize((10, 10), -1)
|
||||||
|
|
||||||
@skip_unless_feature("libtiff")
|
@skip_unless_feature("libtiff")
|
||||||
def test_load_first(self) -> None:
|
def test_transposed(self) -> None:
|
||||||
# load() may change the size of the image
|
|
||||||
# Test that resize() is calling it before getting the size
|
|
||||||
with Image.open("Tests/images/g4_orientation_5.tif") as img:
|
with Image.open("Tests/images/g4_orientation_5.tif") as img:
|
||||||
im = img.resize((64, 64))
|
im = img.resize((64, 64))
|
||||||
assert im.size == (64, 64)
|
assert im.size == (64, 64)
|
||||||
|
|
|
@ -92,15 +92,13 @@ def test_no_resize() -> None:
|
||||||
|
|
||||||
|
|
||||||
@skip_unless_feature("libtiff")
|
@skip_unless_feature("libtiff")
|
||||||
def test_load_first() -> None:
|
def test_transposed() -> None:
|
||||||
# load() may change the size of the image
|
|
||||||
# Test that thumbnail() is calling it before performing size calculations
|
|
||||||
with Image.open("Tests/images/g4_orientation_5.tif") as im:
|
with Image.open("Tests/images/g4_orientation_5.tif") as im:
|
||||||
|
assert im.size == (590, 88)
|
||||||
|
|
||||||
im.thumbnail((64, 64))
|
im.thumbnail((64, 64))
|
||||||
assert im.size == (64, 10)
|
assert im.size == (64, 10)
|
||||||
|
|
||||||
# Test thumbnail(), without draft(),
|
|
||||||
# on an image that is large enough once load() has changed the size
|
|
||||||
with Image.open("Tests/images/g4_orientation_5.tif") as im:
|
with Image.open("Tests/images/g4_orientation_5.tif") as im:
|
||||||
im.thumbnail((590, 88), reducing_gap=None)
|
im.thumbnail((590, 88), reducing_gap=None)
|
||||||
assert im.size == (590, 88)
|
assert im.size == (590, 88)
|
||||||
|
|
|
@ -1369,6 +1369,20 @@ def test_stroke() -> None:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@skip_unless_feature("freetype2")
|
||||||
|
def test_stroke_float() -> None:
|
||||||
|
# Arrange
|
||||||
|
im = Image.new("RGB", (120, 130))
|
||||||
|
draw = ImageDraw.Draw(im)
|
||||||
|
font = ImageFont.truetype("Tests/fonts/FreeMono.ttf", 120)
|
||||||
|
|
||||||
|
# Act
|
||||||
|
draw.text((12, 12), "A", "#f00", font, stroke_width=0.5)
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
assert_image_similar_tofile(im, "Tests/images/imagedraw_stroke_float.png", 3.1)
|
||||||
|
|
||||||
|
|
||||||
@skip_unless_feature("freetype2")
|
@skip_unless_feature("freetype2")
|
||||||
def test_stroke_descender() -> None:
|
def test_stroke_descender() -> None:
|
||||||
# Arrange
|
# Arrange
|
||||||
|
|
|
@ -5,6 +5,7 @@ import os
|
||||||
import re
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
import sys
|
import sys
|
||||||
|
import tempfile
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, BinaryIO
|
from typing import Any, BinaryIO
|
||||||
|
@ -460,17 +461,43 @@ def test_free_type_font_get_mask(font: ImageFont.FreeTypeFont) -> None:
|
||||||
assert mask.size == (108, 13)
|
assert mask.size == (108, 13)
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_when_image_not_found() -> None:
|
||||||
|
with tempfile.NamedTemporaryFile(delete=False) as tmp:
|
||||||
|
pass
|
||||||
|
with pytest.raises(OSError) as e:
|
||||||
|
ImageFont.load(tmp.name)
|
||||||
|
|
||||||
|
os.unlink(tmp.name)
|
||||||
|
|
||||||
|
root = os.path.splitext(tmp.name)[0]
|
||||||
|
assert str(e.value) == f"cannot find glyph data file {root}.{{gif|pbm|png}}"
|
||||||
|
|
||||||
|
|
||||||
def test_load_path_not_found() -> None:
|
def test_load_path_not_found() -> None:
|
||||||
# Arrange
|
# Arrange
|
||||||
filename = "somefilenamethatdoesntexist.ttf"
|
filename = "somefilenamethatdoesntexist.ttf"
|
||||||
|
|
||||||
# Act/Assert
|
# Act/Assert
|
||||||
with pytest.raises(OSError):
|
with pytest.raises(OSError) as e:
|
||||||
ImageFont.load_path(filename)
|
ImageFont.load_path(filename)
|
||||||
|
|
||||||
|
# The file doesn't exist, so don't suggest `load`
|
||||||
|
assert filename in str(e.value)
|
||||||
|
assert "did you mean" not in str(e.value)
|
||||||
with pytest.raises(OSError):
|
with pytest.raises(OSError):
|
||||||
ImageFont.truetype(filename)
|
ImageFont.truetype(filename)
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_path_existing_path() -> None:
|
||||||
|
with tempfile.NamedTemporaryFile() as tmp:
|
||||||
|
with pytest.raises(OSError) as e:
|
||||||
|
ImageFont.load_path(tmp.name)
|
||||||
|
|
||||||
|
# The file exists, so the error message suggests to use `load` instead
|
||||||
|
assert tmp.name in str(e.value)
|
||||||
|
assert " did you mean" in str(e.value)
|
||||||
|
|
||||||
|
|
||||||
def test_load_non_font_bytes() -> None:
|
def test_load_non_font_bytes() -> None:
|
||||||
with open("Tests/images/hopper.jpg", "rb") as f:
|
with open("Tests/images/hopper.jpg", "rb") as f:
|
||||||
with pytest.raises(OSError):
|
with pytest.raises(OSError):
|
||||||
|
|
|
@ -115,7 +115,7 @@ def test_ipythonviewer() -> None:
|
||||||
test_viewer = viewer
|
test_viewer = viewer
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
pytest.fail()
|
pytest.fail("IPythonViewer not found")
|
||||||
|
|
||||||
im = hopper()
|
im = hopper()
|
||||||
assert test_viewer.show(im) == 1
|
assert test_viewer.show(im) == 1
|
||||||
|
|
|
@ -60,6 +60,18 @@ class TestImageWinDib:
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
ImageWin.Dib(mode)
|
ImageWin.Dib(mode)
|
||||||
|
|
||||||
|
def test_dib_hwnd(self) -> None:
|
||||||
|
mode = "RGBA"
|
||||||
|
size = (128, 128)
|
||||||
|
wnd = 0
|
||||||
|
|
||||||
|
dib = ImageWin.Dib(mode, size)
|
||||||
|
hwnd = ImageWin.HWND(wnd)
|
||||||
|
|
||||||
|
dib.expose(hwnd)
|
||||||
|
dib.draw(hwnd, (0, 0) + size)
|
||||||
|
assert isinstance(dib.query_palette(hwnd), int)
|
||||||
|
|
||||||
def test_dib_paste(self) -> None:
|
def test_dib_paste(self) -> None:
|
||||||
# Arrange
|
# Arrange
|
||||||
im = hopper()
|
im = hopper()
|
||||||
|
|
|
@ -238,8 +238,10 @@ def test_zero_size() -> None:
|
||||||
|
|
||||||
|
|
||||||
@skip_unless_feature("libtiff")
|
@skip_unless_feature("libtiff")
|
||||||
def test_load_first() -> None:
|
def test_transposed() -> None:
|
||||||
with Image.open("Tests/images/g4_orientation_5.tif") as im:
|
with Image.open("Tests/images/g4_orientation_5.tif") as im:
|
||||||
|
assert im.size == (590, 88)
|
||||||
|
|
||||||
a = numpy.array(im)
|
a = numpy.array(im)
|
||||||
assert a.shape == (88, 590)
|
assert a.shape == (88, 590)
|
||||||
|
|
||||||
|
|
|
@ -23,6 +23,13 @@ Python 3.8
|
||||||
Pillow has dropped support for Python 3.8,
|
Pillow has dropped support for Python 3.8,
|
||||||
which reached end-of-life in October 2024.
|
which reached end-of-life in October 2024.
|
||||||
|
|
||||||
|
Python 3.12 on macOS <= 10.12
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
The latest version of Python 3.12 only supports macOS versions 10.13 and later,
|
||||||
|
and so Pillow has also updated the deployment target for its prebuilt Python 3.12
|
||||||
|
wheels.
|
||||||
|
|
||||||
PSFile
|
PSFile
|
||||||
^^^^^^
|
^^^^^^
|
||||||
|
|
||||||
|
@ -45,6 +52,11 @@ TiffImagePlugin IFD_LEGACY_API
|
||||||
|
|
||||||
An unused setting, ``TiffImagePlugin.IFD_LEGACY_API``, has been removed.
|
An unused setting, ``TiffImagePlugin.IFD_LEGACY_API``, has been removed.
|
||||||
|
|
||||||
|
WebP 0.4
|
||||||
|
^^^^^^^^
|
||||||
|
|
||||||
|
Support for WebP 0.4 and earlier has been removed; WebP 0.5 is the minimum supported.
|
||||||
|
|
||||||
Deprecations
|
Deprecations
|
||||||
============
|
============
|
||||||
|
|
||||||
|
@ -115,10 +127,18 @@ TODO
|
||||||
API Additions
|
API Additions
|
||||||
=============
|
=============
|
||||||
|
|
||||||
TODO
|
Writing XMP bytes to JPEG and MPO
|
||||||
^^^^
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
TODO
|
XMP data can now be saved to JPEG files using an ``xmp`` argument::
|
||||||
|
|
||||||
|
im.save("out.jpg", xmp=b"test")
|
||||||
|
|
||||||
|
The data can also be set through :py:attr:`~PIL.Image.Image.info`, for use when saving
|
||||||
|
either JPEG or MPO images::
|
||||||
|
|
||||||
|
im.info["xmp"] = b"test"
|
||||||
|
im.save("out.jpg")
|
||||||
|
|
||||||
Other Changes
|
Other Changes
|
||||||
=============
|
=============
|
||||||
|
@ -132,6 +152,10 @@ of 3.13.0 final (2024-10-01, :pep:`719`).
|
||||||
|
|
||||||
Pillow 11.0.0 now officially supports Python 3.13.
|
Pillow 11.0.0 now officially supports Python 3.13.
|
||||||
|
|
||||||
|
Support has also been added for the experimental free-threaded mode of :pep:`703`.
|
||||||
|
|
||||||
|
Python 3.13 only supports macOS versions 10.13 and later.
|
||||||
|
|
||||||
C-level Flags
|
C-level Flags
|
||||||
^^^^^^^^^^^^^
|
^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
|
|
@ -125,7 +125,6 @@ lint.ignore = [
|
||||||
"PT007", # pytest-parametrize-values-wrong-type
|
"PT007", # pytest-parametrize-values-wrong-type
|
||||||
"PT011", # pytest-raises-too-broad
|
"PT011", # pytest-raises-too-broad
|
||||||
"PT012", # pytest-raises-with-multiple-statements
|
"PT012", # pytest-raises-with-multiple-statements
|
||||||
"PT016", # pytest-fail-without-message
|
|
||||||
"PT017", # pytest-assert-in-except
|
"PT017", # pytest-assert-in-except
|
||||||
"PYI026", # flake8-pyi: typing.TypeAlias added in Python 3.10
|
"PYI026", # flake8-pyi: typing.TypeAlias added in Python 3.10
|
||||||
"PYI034", # flake8-pyi: typing.Self added in Python 3.11
|
"PYI034", # flake8-pyi: typing.Self added in Python 3.11
|
||||||
|
|
|
@ -2272,7 +2272,6 @@ class Image:
|
||||||
msg = "reducing_gap must be 1.0 or greater"
|
msg = "reducing_gap must be 1.0 or greater"
|
||||||
raise ValueError(msg)
|
raise ValueError(msg)
|
||||||
|
|
||||||
self.load()
|
|
||||||
if box is None:
|
if box is None:
|
||||||
box = (0, 0) + self.size
|
box = (0, 0) + self.size
|
||||||
|
|
||||||
|
@ -2724,27 +2723,18 @@ class Image:
|
||||||
)
|
)
|
||||||
return x, y
|
return x, y
|
||||||
|
|
||||||
box = None
|
preserved_size = preserve_aspect_ratio()
|
||||||
final_size: tuple[int, int]
|
if preserved_size is None:
|
||||||
if reducing_gap is not None:
|
return
|
||||||
preserved_size = preserve_aspect_ratio()
|
final_size = preserved_size
|
||||||
if preserved_size is None:
|
|
||||||
return
|
|
||||||
final_size = preserved_size
|
|
||||||
|
|
||||||
|
box = None
|
||||||
|
if reducing_gap is not None:
|
||||||
res = self.draft(
|
res = self.draft(
|
||||||
None, (int(size[0] * reducing_gap), int(size[1] * reducing_gap))
|
None, (int(size[0] * reducing_gap), int(size[1] * reducing_gap))
|
||||||
)
|
)
|
||||||
if res is not None:
|
if res is not None:
|
||||||
box = res[1]
|
box = res[1]
|
||||||
if box is None:
|
|
||||||
self.load()
|
|
||||||
|
|
||||||
# load() may have changed the size of the image
|
|
||||||
preserved_size = preserve_aspect_ratio()
|
|
||||||
if preserved_size is None:
|
|
||||||
return
|
|
||||||
final_size = preserved_size
|
|
||||||
|
|
||||||
if self.size != final_size:
|
if self.size != final_size:
|
||||||
im = self.resize(final_size, resample, box=box, reducing_gap=reducing_gap)
|
im = self.resize(final_size, resample, box=box, reducing_gap=reducing_gap)
|
||||||
|
|
|
@ -415,7 +415,7 @@ class ImageFile(Image.Image):
|
||||||
|
|
||||||
def load_prepare(self) -> None:
|
def load_prepare(self) -> None:
|
||||||
# create image memory if necessary
|
# create image memory if necessary
|
||||||
if self._im is None or self.im.mode != self.mode or self.im.size != self.size:
|
if self._im is None:
|
||||||
self.im = Image.core.new(self.mode, self.size)
|
self.im = Image.core.new(self.mode, self.size)
|
||||||
# create palette (optional)
|
# create palette (optional)
|
||||||
if self.mode == "P":
|
if self.mode == "P":
|
||||||
|
|
|
@ -98,11 +98,13 @@ class ImageFont:
|
||||||
def _load_pilfont(self, filename: str) -> None:
|
def _load_pilfont(self, filename: str) -> None:
|
||||||
with open(filename, "rb") as fp:
|
with open(filename, "rb") as fp:
|
||||||
image: ImageFile.ImageFile | None = None
|
image: ImageFile.ImageFile | None = None
|
||||||
|
root = os.path.splitext(filename)[0]
|
||||||
|
|
||||||
for ext in (".png", ".gif", ".pbm"):
|
for ext in (".png", ".gif", ".pbm"):
|
||||||
if image:
|
if image:
|
||||||
image.close()
|
image.close()
|
||||||
try:
|
try:
|
||||||
fullname = os.path.splitext(filename)[0] + ext
|
fullname = root + ext
|
||||||
image = Image.open(fullname)
|
image = Image.open(fullname)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
@ -112,7 +114,8 @@ class ImageFont:
|
||||||
else:
|
else:
|
||||||
if image:
|
if image:
|
||||||
image.close()
|
image.close()
|
||||||
msg = "cannot find glyph data file"
|
|
||||||
|
msg = f"cannot find glyph data file {root}.{{gif|pbm|png}}"
|
||||||
raise OSError(msg)
|
raise OSError(msg)
|
||||||
|
|
||||||
self.file = fullname
|
self.file = fullname
|
||||||
|
@ -224,7 +227,7 @@ class FreeTypeFont:
|
||||||
raise core.ex
|
raise core.ex
|
||||||
|
|
||||||
if size <= 0:
|
if size <= 0:
|
||||||
msg = "font size must be greater than 0"
|
msg = f"font size must be greater than 0, not {size}"
|
||||||
raise ValueError(msg)
|
raise ValueError(msg)
|
||||||
|
|
||||||
self.path = font
|
self.path = font
|
||||||
|
@ -783,8 +786,9 @@ class TransposedFont:
|
||||||
|
|
||||||
def load(filename: str) -> ImageFont:
|
def load(filename: str) -> ImageFont:
|
||||||
"""
|
"""
|
||||||
Load a font file. This function loads a font object from the given
|
Load a font file. This function loads a font object from the given
|
||||||
bitmap font file, and returns the corresponding font object.
|
bitmap font file, and returns the corresponding font object. For loading TrueType
|
||||||
|
or OpenType fonts instead, see :py:func:`~PIL.ImageFont.truetype`.
|
||||||
|
|
||||||
:param filename: Name of font file.
|
:param filename: Name of font file.
|
||||||
:return: A font object.
|
:return: A font object.
|
||||||
|
@ -804,9 +808,10 @@ def truetype(
|
||||||
) -> FreeTypeFont:
|
) -> FreeTypeFont:
|
||||||
"""
|
"""
|
||||||
Load a TrueType or OpenType font from a file or file-like object,
|
Load a TrueType or OpenType font from a file or file-like object,
|
||||||
and create a font object.
|
and create a font object. This function loads a font object from the given
|
||||||
This function loads a font object from the given file or file-like
|
file or file-like object, and creates a font object for a font of the given
|
||||||
object, and creates a font object for a font of the given size.
|
size. For loading bitmap fonts instead, see :py:func:`~PIL.ImageFont.load`
|
||||||
|
and :py:func:`~PIL.ImageFont.load_path`.
|
||||||
|
|
||||||
Pillow uses FreeType to open font files. On Windows, be aware that FreeType
|
Pillow uses FreeType to open font files. On Windows, be aware that FreeType
|
||||||
will keep the file open as long as the FreeTypeFont object exists. Windows
|
will keep the file open as long as the FreeTypeFont object exists. Windows
|
||||||
|
@ -942,7 +947,10 @@ def load_path(filename: str | bytes) -> ImageFont:
|
||||||
return load(os.path.join(directory, filename))
|
return load(os.path.join(directory, filename))
|
||||||
except OSError:
|
except OSError:
|
||||||
pass
|
pass
|
||||||
msg = "cannot find font file"
|
msg = f'cannot find font file "{filename}" in sys.path'
|
||||||
|
if os.path.exists(filename):
|
||||||
|
msg += f', did you mean ImageFont.load("{filename}") instead?'
|
||||||
|
|
||||||
raise OSError(msg)
|
raise OSError(msg)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -98,14 +98,15 @@ class Dib:
|
||||||
HDC or HWND instance. In PythonWin, you can use
|
HDC or HWND instance. In PythonWin, you can use
|
||||||
``CDC.GetHandleAttrib()`` to get a suitable handle.
|
``CDC.GetHandleAttrib()`` to get a suitable handle.
|
||||||
"""
|
"""
|
||||||
|
handle_int = int(handle)
|
||||||
if isinstance(handle, HWND):
|
if isinstance(handle, HWND):
|
||||||
dc = self.image.getdc(handle)
|
dc = self.image.getdc(handle_int)
|
||||||
try:
|
try:
|
||||||
self.image.expose(dc)
|
self.image.expose(dc)
|
||||||
finally:
|
finally:
|
||||||
self.image.releasedc(handle, dc)
|
self.image.releasedc(handle_int, dc)
|
||||||
else:
|
else:
|
||||||
self.image.expose(handle)
|
self.image.expose(handle_int)
|
||||||
|
|
||||||
def draw(
|
def draw(
|
||||||
self,
|
self,
|
||||||
|
@ -124,14 +125,15 @@ class Dib:
|
||||||
"""
|
"""
|
||||||
if src is None:
|
if src is None:
|
||||||
src = (0, 0) + self.size
|
src = (0, 0) + self.size
|
||||||
|
handle_int = int(handle)
|
||||||
if isinstance(handle, HWND):
|
if isinstance(handle, HWND):
|
||||||
dc = self.image.getdc(handle)
|
dc = self.image.getdc(handle_int)
|
||||||
try:
|
try:
|
||||||
self.image.draw(dc, dst, src)
|
self.image.draw(dc, dst, src)
|
||||||
finally:
|
finally:
|
||||||
self.image.releasedc(handle, dc)
|
self.image.releasedc(handle_int, dc)
|
||||||
else:
|
else:
|
||||||
self.image.draw(handle, dst, src)
|
self.image.draw(handle_int, dst, src)
|
||||||
|
|
||||||
def query_palette(self, handle: int | HDC | HWND) -> int:
|
def query_palette(self, handle: int | HDC | HWND) -> int:
|
||||||
"""
|
"""
|
||||||
|
@ -148,14 +150,15 @@ class Dib:
|
||||||
:return: The number of entries that were changed (if one or more entries,
|
:return: The number of entries that were changed (if one or more entries,
|
||||||
this indicates that the image should be redrawn).
|
this indicates that the image should be redrawn).
|
||||||
"""
|
"""
|
||||||
|
handle_int = int(handle)
|
||||||
if isinstance(handle, HWND):
|
if isinstance(handle, HWND):
|
||||||
handle = self.image.getdc(handle)
|
handle = self.image.getdc(handle_int)
|
||||||
try:
|
try:
|
||||||
result = self.image.query_palette(handle)
|
result = self.image.query_palette(handle)
|
||||||
finally:
|
finally:
|
||||||
self.image.releasedc(handle, handle)
|
self.image.releasedc(handle, handle)
|
||||||
else:
|
else:
|
||||||
result = self.image.query_palette(handle)
|
result = self.image.query_palette(handle_int)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def paste(
|
def paste(
|
||||||
|
|
|
@ -141,7 +141,7 @@ def _safe_zlib_decompress(s: bytes) -> bytes:
|
||||||
dobj = zlib.decompressobj()
|
dobj = zlib.decompressobj()
|
||||||
plaintext = dobj.decompress(s, MAX_TEXT_CHUNK)
|
plaintext = dobj.decompress(s, MAX_TEXT_CHUNK)
|
||||||
if dobj.unconsumed_tail:
|
if dobj.unconsumed_tail:
|
||||||
msg = "Decompressed Data Too Large"
|
msg = "Decompressed data too large for PngImagePlugin.MAX_TEXT_CHUNK"
|
||||||
raise ValueError(msg)
|
raise ValueError(msg)
|
||||||
return plaintext
|
return plaintext
|
||||||
|
|
||||||
|
|
|
@ -1195,8 +1195,8 @@ class TiffImageFile(ImageFile.ImageFile):
|
||||||
# Create a new core image object on second and
|
# Create a new core image object on second and
|
||||||
# subsequent frames in the image. Image may be
|
# subsequent frames in the image. Image may be
|
||||||
# different size/mode.
|
# different size/mode.
|
||||||
Image._decompression_bomb_check(self.size)
|
Image._decompression_bomb_check(self._tile_size)
|
||||||
self.im = Image.core.new(self.mode, self.size)
|
self.im = Image.core.new(self.mode, self._tile_size)
|
||||||
|
|
||||||
def _seek(self, frame: int) -> None:
|
def _seek(self, frame: int) -> None:
|
||||||
if isinstance(self._fp, DeferredError):
|
if isinstance(self._fp, DeferredError):
|
||||||
|
@ -1278,6 +1278,11 @@ class TiffImageFile(ImageFile.ImageFile):
|
||||||
return self._load_libtiff()
|
return self._load_libtiff()
|
||||||
return super().load()
|
return super().load()
|
||||||
|
|
||||||
|
def load_prepare(self) -> None:
|
||||||
|
if self._im is None:
|
||||||
|
self.im = Image.core.new(self.mode, self._tile_size)
|
||||||
|
ImageFile.ImageFile.load_prepare(self)
|
||||||
|
|
||||||
def load_end(self) -> None:
|
def load_end(self) -> None:
|
||||||
# allow closing if we're on the first frame, there's no next
|
# allow closing if we're on the first frame, there's no next
|
||||||
# This is the ImageFile.load path only, libtiff specific below.
|
# This is the ImageFile.load path only, libtiff specific below.
|
||||||
|
@ -1421,7 +1426,12 @@ class TiffImageFile(ImageFile.ImageFile):
|
||||||
if not isinstance(xsize, int) or not isinstance(ysize, int):
|
if not isinstance(xsize, int) or not isinstance(ysize, int):
|
||||||
msg = "Invalid dimensions"
|
msg = "Invalid dimensions"
|
||||||
raise ValueError(msg)
|
raise ValueError(msg)
|
||||||
self._size = xsize, ysize
|
self._tile_size = xsize, ysize
|
||||||
|
orientation = self.tag_v2.get(ExifTags.Base.Orientation)
|
||||||
|
if orientation in (5, 6, 7, 8):
|
||||||
|
self._size = ysize, xsize
|
||||||
|
else:
|
||||||
|
self._size = xsize, ysize
|
||||||
|
|
||||||
logger.debug("- size: %s", self.size)
|
logger.debug("- size: %s", self.size)
|
||||||
|
|
||||||
|
@ -1564,7 +1574,7 @@ class TiffImageFile(ImageFile.ImageFile):
|
||||||
if STRIPOFFSETS in self.tag_v2:
|
if STRIPOFFSETS in self.tag_v2:
|
||||||
offsets = self.tag_v2[STRIPOFFSETS]
|
offsets = self.tag_v2[STRIPOFFSETS]
|
||||||
h = self.tag_v2.get(ROWSPERSTRIP, ysize)
|
h = self.tag_v2.get(ROWSPERSTRIP, ysize)
|
||||||
w = self.size[0]
|
w = xsize
|
||||||
else:
|
else:
|
||||||
# tiled image
|
# tiled image
|
||||||
offsets = self.tag_v2[TILEOFFSETS]
|
offsets = self.tag_v2[TILEOFFSETS]
|
||||||
|
@ -1598,9 +1608,9 @@ class TiffImageFile(ImageFile.ImageFile):
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
x = x + w
|
x = x + w
|
||||||
if x >= self.size[0]:
|
if x >= xsize:
|
||||||
x, y = 0, y + h
|
x, y = 0, y + h
|
||||||
if y >= self.size[1]:
|
if y >= ysize:
|
||||||
x = y = 0
|
x = y = 0
|
||||||
layer += 1
|
layer += 1
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -833,7 +833,7 @@ font_render(FontObject *self, PyObject *args) {
|
||||||
Py_ssize_t id;
|
Py_ssize_t id;
|
||||||
int mask = 0; /* is FT_LOAD_TARGET_MONO enabled? */
|
int mask = 0; /* is FT_LOAD_TARGET_MONO enabled? */
|
||||||
int color = 0; /* is FT_LOAD_COLOR enabled? */
|
int color = 0; /* is FT_LOAD_COLOR enabled? */
|
||||||
int stroke_width = 0;
|
float stroke_width = 0;
|
||||||
PY_LONG_LONG foreground_ink_long = 0;
|
PY_LONG_LONG foreground_ink_long = 0;
|
||||||
unsigned int foreground_ink;
|
unsigned int foreground_ink;
|
||||||
const char *mode = NULL;
|
const char *mode = NULL;
|
||||||
|
@ -853,7 +853,7 @@ font_render(FontObject *self, PyObject *args) {
|
||||||
|
|
||||||
if (!PyArg_ParseTuple(
|
if (!PyArg_ParseTuple(
|
||||||
args,
|
args,
|
||||||
"OO|zzOzizLffO:render",
|
"OO|zzOzfzLffO:render",
|
||||||
&string,
|
&string,
|
||||||
&fill,
|
&fill,
|
||||||
&mode,
|
&mode,
|
||||||
|
@ -919,8 +919,8 @@ font_render(FontObject *self, PyObject *args) {
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
width += stroke_width * 2 + ceil(x_start);
|
width += ceil(stroke_width * 2 + x_start);
|
||||||
height += stroke_width * 2 + ceil(y_start);
|
height += ceil(stroke_width * 2 + y_start);
|
||||||
image = PyObject_CallFunction(fill, "ii", width, height);
|
image = PyObject_CallFunction(fill, "ii", width, height);
|
||||||
if (image == Py_None) {
|
if (image == Py_None) {
|
||||||
PyMem_Del(glyph_info);
|
PyMem_Del(glyph_info);
|
||||||
|
@ -934,8 +934,8 @@ font_render(FontObject *self, PyObject *args) {
|
||||||
Py_XDECREF(imageId);
|
Py_XDECREF(imageId);
|
||||||
im = (Imaging)id;
|
im = (Imaging)id;
|
||||||
|
|
||||||
x_offset -= stroke_width;
|
x_offset = round(x_offset - stroke_width);
|
||||||
y_offset -= stroke_width;
|
y_offset = round(y_offset - stroke_width);
|
||||||
if (count == 0 || width == 0 || height == 0) {
|
if (count == 0 || width == 0 || height == 0) {
|
||||||
PyMem_Del(glyph_info);
|
PyMem_Del(glyph_info);
|
||||||
return Py_BuildValue("N(ii)", image, x_offset, y_offset);
|
return Py_BuildValue("N(ii)", image, x_offset, y_offset);
|
||||||
|
@ -950,7 +950,7 @@ font_render(FontObject *self, PyObject *args) {
|
||||||
|
|
||||||
FT_Stroker_Set(
|
FT_Stroker_Set(
|
||||||
stroker,
|
stroker,
|
||||||
(FT_Fixed)stroke_width * 64,
|
(FT_Fixed)round(stroke_width * 64),
|
||||||
FT_STROKER_LINECAP_ROUND,
|
FT_STROKER_LINECAP_ROUND,
|
||||||
FT_STROKER_LINEJOIN_ROUND,
|
FT_STROKER_LINEJOIN_ROUND,
|
||||||
0
|
0
|
||||||
|
@ -988,8 +988,8 @@ font_render(FontObject *self, PyObject *args) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/* set pen position to text origin */
|
/* set pen position to text origin */
|
||||||
x = (-x_min + stroke_width + x_start) * 64;
|
x = round((-x_min + stroke_width + x_start) * 64);
|
||||||
y = (-y_max + (-stroke_width) - y_start) * 64;
|
y = round((-y_max + (-stroke_width) - y_start) * 64);
|
||||||
|
|
||||||
if (stroker == NULL) {
|
if (stroker == NULL) {
|
||||||
load_flags |= FT_LOAD_RENDER;
|
load_flags |= FT_LOAD_RENDER;
|
||||||
|
|
|
@ -1411,10 +1411,3 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) {
|
||||||
}
|
}
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
/*
|
|
||||||
* Local Variables:
|
|
||||||
* c-basic-offset: 4
|
|
||||||
* End:
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
|
@ -104,10 +104,3 @@ typedef struct {
|
||||||
int plt;
|
int plt;
|
||||||
|
|
||||||
} JPEG2KENCODESTATE;
|
} JPEG2KENCODESTATE;
|
||||||
|
|
||||||
/*
|
|
||||||
* Local Variables:
|
|
||||||
* c-basic-offset: 4
|
|
||||||
* End:
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
|
@ -979,10 +979,3 @@ ImagingJpeg2KVersion(void) {
|
||||||
}
|
}
|
||||||
|
|
||||||
#endif /* HAVE_OPENJPEG */
|
#endif /* HAVE_OPENJPEG */
|
||||||
|
|
||||||
/*
|
|
||||||
* Local Variables:
|
|
||||||
* c-basic-offset: 4
|
|
||||||
* End:
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
|
@ -652,10 +652,3 @@ ImagingJpeg2KEncodeCleanup(ImagingCodecState state) {
|
||||||
}
|
}
|
||||||
|
|
||||||
#endif /* HAVE_OPENJPEG */
|
#endif /* HAVE_OPENJPEG */
|
||||||
|
|
||||||
/*
|
|
||||||
* Local Variables:
|
|
||||||
* c-basic-offset: 4
|
|
||||||
* End:
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
|
@ -44,7 +44,6 @@ PyImaging_GetBuffer(PyObject *buffer, Py_buffer *view);
|
||||||
typedef struct {
|
typedef struct {
|
||||||
PyObject_HEAD Py_ssize_t count;
|
PyObject_HEAD Py_ssize_t count;
|
||||||
double *xy;
|
double *xy;
|
||||||
int index; /* temporary use, e.g. in decimate */
|
|
||||||
} PyPathObject;
|
} PyPathObject;
|
||||||
|
|
||||||
static PyTypeObject PyPathType;
|
static PyTypeObject PyPathType;
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
README
|
README
|
||||||
------
|
------
|
||||||
|
|
||||||
[cibuildwheel](https://github.com/pypa/cibuildwheel) is used to build macOS and Linux
|
[cibuildwheel](https://github.com/pypa/cibuildwheel) is used to build wheels for tagged
|
||||||
wheels for tagged versions of Pillow.
|
versions of Pillow.
|
||||||
|
|
||||||
This directory contains [multibuild](https://github.com/multi-build/multibuild) to
|
This directory contains [multibuild](https://github.com/multi-build/multibuild) to
|
||||||
build dependencies for the wheels, and dependency licenses to be included.
|
build dependencies for macOS and Linux wheels, and dependency licenses to be included.
|
||||||
|
|
||||||
Archives
|
Archives
|
||||||
--------
|
--------
|
||||||
|
@ -30,6 +30,3 @@ Wheels
|
||||||
|
|
||||||
Wheels are
|
Wheels are
|
||||||
[GitHub Actions artifacts created for tags, relevant changes or manual builds](https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml).
|
[GitHub Actions artifacts created for tags, relevant changes or manual builds](https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml).
|
||||||
|
|
||||||
Windows wheels are created separately. They are
|
|
||||||
[GitHub Actions artifacts created on each run of the Windows workflow](https://github.com/python-pillow/Pillow/actions/workflows/test-windows.yml?query=branch%3Amain).
|
|
||||||
|
|
|
@ -113,13 +113,13 @@ V = {
|
||||||
"FREETYPE": "2.13.3",
|
"FREETYPE": "2.13.3",
|
||||||
"FRIBIDI": "1.0.15",
|
"FRIBIDI": "1.0.15",
|
||||||
"HARFBUZZ": "9.0.0",
|
"HARFBUZZ": "9.0.0",
|
||||||
"JPEGTURBO": "3.0.3",
|
"JPEGTURBO": "3.0.4",
|
||||||
"LCMS2": "2.16",
|
"LCMS2": "2.16",
|
||||||
"LIBPNG": "1.6.43",
|
"LIBPNG": "1.6.44",
|
||||||
"LIBWEBP": "1.4.0",
|
"LIBWEBP": "1.4.0",
|
||||||
"OPENJPEG": "2.5.2",
|
"OPENJPEG": "2.5.2",
|
||||||
"TIFF": "4.6.0",
|
"TIFF": "4.6.0",
|
||||||
"XZ": "5.4.5",
|
"XZ": "5.6.2",
|
||||||
"ZLIB": "1.3.1",
|
"ZLIB": "1.3.1",
|
||||||
}
|
}
|
||||||
V["LIBPNG_DOTLESS"] = V["LIBPNG"].replace(".", "")
|
V["LIBPNG_DOTLESS"] = V["LIBPNG"].replace(".", "")
|
||||||
|
@ -175,7 +175,7 @@ DEPS: dict[str, dict[str, Any]] = {
|
||||||
"libs": [r"*.lib"],
|
"libs": [r"*.lib"],
|
||||||
},
|
},
|
||||||
"xz": {
|
"xz": {
|
||||||
"url": f"{SF_PROJECTS}/lzmautils/files/xz-{V['XZ']}.tar.gz/download",
|
"url": f"https://github.com/tukaani-project/xz/releases/download/v{V['XZ']}/xz-{V['XZ']}.tar.gz",
|
||||||
"filename": f"xz-{V['XZ']}.tar.gz",
|
"filename": f"xz-{V['XZ']}.tar.gz",
|
||||||
"dir": f"xz-{V['XZ']}",
|
"dir": f"xz-{V['XZ']}",
|
||||||
"license": "COPYING",
|
"license": "COPYING",
|
||||||
|
|
Loading…
Reference in New Issue
Block a user