Merge branch 'python-pillow-main'

This commit is contained in:
Andrew Murray 2022-08-02 15:55:21 +10:00
commit c3cc621c67
28 changed files with 273 additions and 65 deletions

View File

@ -24,7 +24,7 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
docker: [ docker: [
ubuntu-20.04-focal-amd64-valgrind, ubuntu-22.04-jammy-amd64-valgrind,
] ]
dockerTag: [main] dockerTag: [main]

View File

@ -19,13 +19,13 @@ repos:
- id: yesqa - id: yesqa
- repo: https://github.com/Lucas-C/pre-commit-hooks - repo: https://github.com/Lucas-C/pre-commit-hooks
rev: v1.2.0 rev: v1.3.0
hooks: hooks:
- id: remove-tabs - id: remove-tabs
exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.opt$) exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.opt$)
- repo: https://github.com/PyCQA/flake8 - repo: https://github.com/PyCQA/flake8
rev: 4.0.1 rev: 5.0.2
hooks: hooks:
- id: flake8 - id: flake8
additional_dependencies: [flake8-2020, flake8-implicit-str-concat] additional_dependencies: [flake8-2020, flake8-implicit-str-concat]

View File

@ -5,6 +5,21 @@ Changelog (Pillow)
9.3.0 (unreleased) 9.3.0 (unreleased)
------------------ ------------------
- Parse orientation from XMP tag contents #6463
[bigcat88, radarhere]
- Added support for reading ATI1/ATI2 (BC4/BC5) DDS images #6457
[REDxEYE, radarhere]
- Do not clear GIF tile when checking number of frames #6455
[radarhere]
- Support saving multiple MPO frames #6444
[radarhere]
- Do not double quote Pillow version for setuptools >= 60 #6450
[radarhere]
- Added ABGR BMP mask mode #6436 - Added ABGR BMP mask mode #6436
[radarhere] [radarhere]

View File

@ -96,8 +96,8 @@ Released as needed privately to individual vendors for critical security-related
## Binary Distributions ## Binary Distributions
### Windows ### Windows
* [ ] Contact `@cgohlke` for Windows binaries via release ticket e.g. https://github.com/python-pillow/Pillow/issues/1174. * [ ] Download the artifacts from the [GitHub Actions "Test Windows" workflow](https://github.com/python-pillow/Pillow/actions/workflows/test-windows.yml)
* [ ] Download and extract tarball from `@cgohlke` and copy into `dist/` and copy into `dist/`
### Mac and Linux ### Mac and Linux
* [ ] Use the [Pillow Wheel Builder](https://github.com/python-pillow/pillow-wheels): * [ ] Use the [Pillow Wheel Builder](https://github.com/python-pillow/pillow-wheels):

BIN
Tests/images/ati1.dds Normal file

Binary file not shown.

BIN
Tests/images/ati1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 969 B

BIN
Tests/images/ati2.dds Normal file

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -10,6 +10,8 @@ from .helper import assert_image_equal, assert_image_equal_tofile, hopper
TEST_FILE_DXT1 = "Tests/images/dxt1-rgb-4bbp-noalpha_MipMaps-1.dds" TEST_FILE_DXT1 = "Tests/images/dxt1-rgb-4bbp-noalpha_MipMaps-1.dds"
TEST_FILE_DXT3 = "Tests/images/dxt3-argb-8bbp-explicitalpha_MipMaps-1.dds" TEST_FILE_DXT3 = "Tests/images/dxt3-argb-8bbp-explicitalpha_MipMaps-1.dds"
TEST_FILE_DXT5 = "Tests/images/dxt5-argb-8bbp-interpolatedalpha_MipMaps-1.dds" TEST_FILE_DXT5 = "Tests/images/dxt5-argb-8bbp-interpolatedalpha_MipMaps-1.dds"
TEST_FILE_ATI1 = "Tests/images/ati1.dds"
TEST_FILE_ATI2 = "Tests/images/ati2.dds"
TEST_FILE_DX10_BC5_TYPELESS = "Tests/images/bc5_typeless.dds" TEST_FILE_DX10_BC5_TYPELESS = "Tests/images/bc5_typeless.dds"
TEST_FILE_DX10_BC5_UNORM = "Tests/images/bc5_unorm.dds" TEST_FILE_DX10_BC5_UNORM = "Tests/images/bc5_unorm.dds"
TEST_FILE_DX10_BC5_SNORM = "Tests/images/bc5_snorm.dds" TEST_FILE_DX10_BC5_SNORM = "Tests/images/bc5_snorm.dds"
@ -64,6 +66,32 @@ def test_sanity_dxt5():
assert_image_equal_tofile(im, TEST_FILE_DXT5.replace(".dds", ".png")) assert_image_equal_tofile(im, TEST_FILE_DXT5.replace(".dds", ".png"))
def test_sanity_ati1():
"""Check ATI1 images can be opened"""
with Image.open(TEST_FILE_ATI1) as im:
im.load()
assert im.format == "DDS"
assert im.mode == "L"
assert im.size == (64, 64)
assert_image_equal_tofile(im, TEST_FILE_ATI1.replace(".dds", ".png"))
def test_sanity_ati2():
"""Check ATI2 images can be opened"""
with Image.open(TEST_FILE_ATI2) as im:
im.load()
assert im.format == "DDS"
assert im.mode == "RGB"
assert im.size == (256, 256)
assert_image_equal_tofile(im, TEST_FILE_DX10_BC5_UNORM.replace(".dds", ".png"))
@pytest.mark.parametrize( @pytest.mark.parametrize(
("image_path", "expected_path"), ("image_path", "expected_path"),
( (

View File

@ -399,6 +399,11 @@ def test_no_change():
assert im.is_animated assert im.is_animated
assert_image_equal(im, expected) assert_image_equal(im, expected)
with Image.open("Tests/images/comment_after_only_frame.gif") as im:
expected = Image.new("P", (1, 1))
assert not im.is_animated
assert_image_equal(im, expected)
def test_eoferror(): def test_eoferror():
with Image.open(TEST_GIF) as im: with Image.open(TEST_GIF) as im:

View File

@ -5,15 +5,19 @@ import pytest
from PIL import Image from PIL import Image
from .helper import assert_image_similar, is_pypy, skip_unless_feature from .helper import (
assert_image_equal,
assert_image_similar,
is_pypy,
skip_unless_feature,
)
test_files = ["Tests/images/sugarshack.mpo", "Tests/images/frozenpond.mpo"] test_files = ["Tests/images/sugarshack.mpo", "Tests/images/frozenpond.mpo"]
pytestmark = skip_unless_feature("jpg") pytestmark = skip_unless_feature("jpg")
def frame_roundtrip(im, **options): def roundtrip(im, **options):
# Note that for now, there is no MPO saving functionality
out = BytesIO() out = BytesIO()
im.save(out, "MPO", **options) im.save(out, "MPO", **options)
test_bytes = out.tell() test_bytes = out.tell()
@ -237,13 +241,38 @@ def test_image_grab():
def test_save(): def test_save():
# Note that only individual frames can be saved at present
for test_file in test_files: for test_file in test_files:
with Image.open(test_file) as im: with Image.open(test_file) as im:
assert im.tell() == 0 assert im.tell() == 0
jpg0 = frame_roundtrip(im) jpg0 = roundtrip(im)
assert_image_similar(im, jpg0, 30) assert_image_similar(im, jpg0, 30)
im.seek(1) im.seek(1)
assert im.tell() == 1 assert im.tell() == 1
jpg1 = frame_roundtrip(im) jpg1 = roundtrip(im)
assert_image_similar(im, jpg1, 30) assert_image_similar(im, jpg1, 30)
def test_save_all():
for test_file in test_files:
with Image.open(test_file) as im:
im_reloaded = roundtrip(im, save_all=True)
im.seek(0)
assert_image_similar(im, im_reloaded, 30)
im.seek(1)
im_reloaded.seek(1)
assert_image_similar(im, im_reloaded, 30)
im = Image.new("RGB", (1, 1))
im2 = Image.new("RGB", (1, 1), "#f00")
im_reloaded = roundtrip(im, save_all=True, append_images=[im2])
assert_image_equal(im, im_reloaded)
im_reloaded.seek(1)
assert_image_similar(im2, im_reloaded, 1)
# Test that a single frame image will not be saved as an MPO
jpg = roundtrip(im, save_all=True)
assert "mp" not in jpg.info

View File

@ -345,11 +345,15 @@ def test_exif_transpose():
check(orientation_im) check(orientation_im)
# Orientation from "XML:com.adobe.xmp" info key # Orientation from "XML:com.adobe.xmp" info key
with Image.open("Tests/images/xmp_tags_orientation.png") as im: for suffix in ("", "_exiftool"):
assert im.getexif()[0x0112] == 3 with Image.open("Tests/images/xmp_tags_orientation" + suffix + ".png") as im:
assert im.getexif()[0x0112] == 3
transposed_im = ImageOps.exif_transpose(im) transposed_im = ImageOps.exif_transpose(im)
assert 0x0112 not in transposed_im.getexif() assert 0x0112 not in transposed_im.getexif()
transposed_im._reload_exif()
assert 0x0112 not in transposed_im.getexif()
# Orientation from "Raw profile type exif" info key # Orientation from "Raw profile type exif" info key
# This test image has been manually hexedited from exif_imagemagick.png # This test image has been manually hexedited from exif_imagemagick.png

View File

@ -1,7 +1,7 @@
#!/bin/bash #!/bin/bash
# install libimagequant # install libimagequant
archive=libimagequant-4.0.0 archive=libimagequant-4.0.1
./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz ./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz

View File

@ -1209,6 +1209,17 @@ image when first opened. The :py:meth:`~PIL.Image.Image.seek` and :py:meth:`~PIL
methods may be used to read other pictures from the file. The pictures are methods may be used to read other pictures from the file. The pictures are
zero-indexed and random access is supported. zero-indexed and random access is supported.
When calling :py:meth:`~PIL.Image.Image.save` to write an MPO file, by default
only the first frame of a multiframe image will be saved. If the ``save_all``
argument is present and true, then all frames will be saved, and the following
option will also be available.
**append_images**
A list of images to append as additional pictures. Each of the
images in the list can be single or multiframe images.
.. versionadded:: 9.3.0
PCD PCD
^^^ ^^^

View File

@ -15,35 +15,13 @@ Python Support
Pillow supports these Python versions. Pillow supports these Python versions.
+----------------------+-----+-----+-----+-----+-----+-----+-----+-----+ .. csv-table:: Newer versions
| Python |3.10 | 3.9 | 3.8 | 3.7 | 3.6 | 3.5 | 3.4 | 2.7 | :file: newer-versions.csv
+======================+=====+=====+=====+=====+=====+=====+=====+=====+ :header-rows: 1
| Pillow >= 9.0 | Yes | Yes | Yes | Yes | | | | |
+----------------------+-----+-----+-----+-----+-----+-----+-----+-----+
| Pillow 8.3.2 - 8.4 | Yes | Yes | Yes | Yes | Yes | | | |
+----------------------+-----+-----+-----+-----+-----+-----+-----+-----+
| Pillow 8.0 - 8.3.1 | | Yes | Yes | Yes | Yes | | | |
+----------------------+-----+-----+-----+-----+-----+-----+-----+-----+
| Pillow 7.0 - 7.2 | | | Yes | Yes | Yes | Yes | | |
+----------------------+-----+-----+-----+-----+-----+-----+-----+-----+
| Pillow 6.2.1 - 6.2.2 | | | Yes | Yes | Yes | Yes | | Yes |
+----------------------+-----+-----+-----+-----+-----+-----+-----+-----+
| Pillow 6.0 - 6.2.0 | | | | Yes | Yes | Yes | | Yes |
+----------------------+-----+-----+-----+-----+-----+-----+-----+-----+
| Pillow 5.2 - 5.4 | | | | Yes | Yes | Yes | Yes | Yes |
+----------------------+-----+-----+-----+-----+-----+-----+-----+-----+
+------------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+ .. csv-table:: Older versions
| Python | 3.6 | 3.5 | 3.4 | 3.3 | 3.2 | 2.7 | 2.6 | 2.5 | 2.4 | :file: older-versions.csv
+==================+=====+=====+=====+=====+=====+=====+=====+=====+=====+ :header-rows: 1
| Pillow 5.0 - 5.1 | Yes | Yes | Yes | | | Yes | | | |
+------------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+
| Pillow 4 | Yes | Yes | Yes | Yes | | Yes | | | |
+------------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+
| Pillow 2 - 3 | | Yes | Yes | Yes | Yes | Yes | Yes | | |
+------------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+
| Pillow < 2 | | | | | | Yes | Yes | Yes | Yes |
+------------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+
Basic Installation Basic Installation
------------------ ------------------
@ -188,7 +166,7 @@ Many of Pillow's features require external libraries:
* **libimagequant** provides improved color quantization * **libimagequant** provides improved color quantization
* Pillow has been tested with libimagequant **2.6-4.0** * Pillow has been tested with libimagequant **2.6-4.0.1**
* Libimagequant is licensed GPLv3, which is more restrictive than * Libimagequant is licensed GPLv3, which is more restrictive than
the Pillow license, therefore we will not be distributing binaries the Pillow license, therefore we will not be distributing binaries
with libimagequant support enabled. with libimagequant support enabled.

6
docs/newer-versions.csv Normal file
View File

@ -0,0 +1,6 @@
Python,3.11,3.10,3.9,3.8,3.7,3.6,3.5
Pillow >= 9.3,Yes,Yes,Yes,Yes,Yes,,
Pillow 9.0 - 9.2,,Yes,Yes,Yes,Yes,,
Pillow 8.3.2 - 8.4,,Yes,Yes,Yes,Yes,Yes,
Pillow 8.0 - 8.3.1,,,Yes,Yes,Yes,Yes,
Pillow 7.0 - 7.2,,,,Yes,Yes,Yes,Yes
1 Python 3.11 3.10 3.9 3.8 3.7 3.6 3.5
2 Pillow >= 9.3 Yes Yes Yes Yes Yes
3 Pillow 9.0 - 9.2 Yes Yes Yes Yes
4 Pillow 8.3.2 - 8.4 Yes Yes Yes Yes Yes
5 Pillow 8.0 - 8.3.1 Yes Yes Yes Yes
6 Pillow 7.0 - 7.2 Yes Yes Yes Yes

8
docs/older-versions.csv Normal file
View File

@ -0,0 +1,8 @@
Python,3.8,3.7,3.6,3.5,3.4,3.3,3.2,2.7,2.6,2.5,2.4
Pillow 6.2.1 - 6.2.2,Yes,Yes,Yes,Yes,,,,Yes,,,
Pillow 6.0 - 6.2.0,,Yes,Yes,Yes,,,,Yes,,,
Pillow 5.2 - 5.4,,Yes,Yes,Yes,Yes,,,Yes,,,
Pillow 5.0 - 5.1,,,Yes,Yes,Yes,,,Yes,,,
Pillow 4,,,Yes,Yes,Yes,Yes,,Yes,,,
Pillow 2 - 3,,,,Yes,Yes,Yes,Yes,Yes,Yes,,
Pillow < 2,,,,,,,,Yes,Yes,Yes,Yes
1 Python 3.8 3.7 3.6 3.5 3.4 3.3 3.2 2.7 2.6 2.5 2.4
2 Pillow 6.2.1 - 6.2.2 Yes Yes Yes Yes Yes
3 Pillow 6.0 - 6.2.0 Yes Yes Yes Yes
4 Pillow 5.2 - 5.4 Yes Yes Yes Yes Yes
5 Pillow 5.0 - 5.1 Yes Yes Yes Yes
6 Pillow 4 Yes Yes Yes Yes Yes
7 Pillow 2 - 3 Yes Yes Yes Yes Yes Yes
8 Pillow < 2 Yes Yes Yes Yes

View File

@ -0,0 +1,59 @@
9.3.0
-----
Backwards Incompatible Changes
==============================
TODO
^^^^
Deprecations
============
TODO
^^^^
TODO
API Changes
===========
TODO
^^^^
TODO
API Additions
=============
Saving multiple MPO frames
^^^^^^^^^^^^^^^^^^^^^^^^^^
Multiple MPO frames can now be saved. Using the ``save_all`` argument, all of
an image's frames will be saved to file::
from PIL import Image
im = Image.open("frozenpond.mpo")
im.save(out, save_all=True)
Additional images can also be appended when saving, by combining the
``save_all`` argument with the ``append_images`` argument::
im.save(out, save_all=True, append_images=[im1, im2, ...])
Security
========
TODO
^^^^
TODO
Other Changes
=============
Added DDS ATI1 and ATI2 reading
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Support has been added to read the ATI1 and ATI2 formats of DDS images.

View File

@ -14,6 +14,7 @@ expected to be backported to earlier versions.
.. toctree:: .. toctree::
:maxdepth: 2 :maxdepth: 2
9.3.0
9.2.0 9.2.0
9.1.1 9.1.1
9.1.0 9.1.0

View File

@ -158,6 +158,14 @@ class DdsImageFile(ImageFile.ImageFile):
elif fourcc == b"DXT5": elif fourcc == b"DXT5":
self.pixel_format = "DXT5" self.pixel_format = "DXT5"
n = 3 n = 3
elif fourcc == b"ATI1":
self.pixel_format = "BC4"
n = 4
self.mode = "L"
elif fourcc == b"ATI2":
self.pixel_format = "BC5"
n = 5
self.mode = "RGB"
elif fourcc == b"BC5S": elif fourcc == b"BC5S":
self.pixel_format = "BC5S" self.pixel_format = "BC5S"
n = 5 n = 5

View File

@ -185,8 +185,6 @@ class GifImageFile(ImageFile.ImageFile):
if not s or s == b";": if not s or s == b";":
raise EOFError raise EOFError
self.tile = []
palette = None palette = None
info = {} info = {}
@ -295,6 +293,8 @@ class GifImageFile(ImageFile.ImageFile):
if not update_image: if not update_image:
return return
self.tile = []
if self.dispose: if self.dispose:
self.im.paste(self.dispose, self.dispose_extent) self.im.paste(self.dispose, self.dispose_extent)

View File

@ -1404,9 +1404,9 @@ class Image:
if 0x0112 not in self._exif: if 0x0112 not in self._exif:
xmp_tags = self.info.get("XML:com.adobe.xmp") xmp_tags = self.info.get("XML:com.adobe.xmp")
if xmp_tags: if xmp_tags:
match = re.search(r'tiff:Orientation="([0-9])"', xmp_tags) match = re.search(r'tiff:Orientation(="|>)([0-9])', xmp_tags)
if match: if match:
self._exif[0x0112] = int(match[1]) self._exif[0x0112] = int(match[2])
return self._exif return self._exif

View File

@ -499,9 +499,14 @@ def _save(im, fp, tile, bufsize=0):
try: try:
fh = fp.fileno() fh = fp.fileno()
fp.flush() fp.flush()
exc = None _encode_tile(im, fp, tile, bufsize, fh)
except (AttributeError, io.UnsupportedOperation) as e: except (AttributeError, io.UnsupportedOperation) as exc:
exc = e _encode_tile(im, fp, tile, bufsize, None, exc)
if hasattr(fp, "flush"):
fp.flush()
def _encode_tile(im, fp, tile, bufsize, fh, exc=None):
for e, b, o, a in tile: for e, b, o, a in tile:
if o > 0: if o > 0:
fp.seek(o) fp.seek(o)
@ -526,8 +531,6 @@ def _save(im, fp, tile, bufsize=0):
raise OSError(f"encoder error {s} when writing image file") from exc raise OSError(f"encoder error {s} when writing image file") from exc
finally: finally:
encoder.cleanup() encoder.cleanup()
if hasattr(fp, "flush"):
fp.flush()
def _safe_read(fp, size): def _safe_read(fp, size):

View File

@ -601,10 +601,12 @@ def exif_transpose(image):
"Raw profile type exif" "Raw profile type exif"
] = transposed_exif.tobytes().hex() ] = transposed_exif.tobytes().hex()
elif "XML:com.adobe.xmp" in transposed_image.info: elif "XML:com.adobe.xmp" in transposed_image.info:
transposed_image.info["XML:com.adobe.xmp"] = re.sub( for pattern in (
r'tiff:Orientation="([0-9])"', r'tiff:Orientation="([0-9])"',
"", r"<tiff:Orientation>([0-9])</tiff:Orientation>",
transposed_image.info["XML:com.adobe.xmp"], ):
) transposed_image.info["XML:com.adobe.xmp"] = re.sub(
pattern, "", transposed_image.info["XML:com.adobe.xmp"]
)
return transposed_image return transposed_image
return image.copy() return image.copy()

View File

@ -711,7 +711,7 @@ def _save(im, fp, filename):
qtables = getattr(im, "quantization", None) qtables = getattr(im, "quantization", None)
qtables = validate_qtables(qtables) qtables = validate_qtables(qtables)
extra = b"" extra = info.get("extra", b"")
icc_profile = info.get("icc_profile") icc_profile = info.get("icc_profile")
if icc_profile: if icc_profile:

View File

@ -18,16 +18,66 @@
# See the README file for information on usage and redistribution. # See the README file for information on usage and redistribution.
# #
from . import Image, ImageFile, JpegImagePlugin import itertools
import os
import struct
from . import Image, ImageFile, ImageSequence, JpegImagePlugin, TiffImagePlugin
from ._binary import i16be as i16 from ._binary import i16be as i16
from ._binary import o32le
# def _accept(prefix): # def _accept(prefix):
# return JpegImagePlugin._accept(prefix) # return JpegImagePlugin._accept(prefix)
def _save(im, fp, filename): def _save(im, fp, filename):
# Note that we can only save the current frame at present JpegImagePlugin._save(im, fp, filename)
return JpegImagePlugin._save(im, fp, filename)
def _save_all(im, fp, filename):
append_images = im.encoderinfo.get("append_images", [])
if not append_images:
try:
animated = im.is_animated
except AttributeError:
animated = False
if not animated:
_save(im, fp, filename)
return
offsets = []
for imSequence in itertools.chain([im], append_images):
for im_frame in ImageSequence.Iterator(imSequence):
if not offsets:
# APP2 marker
im.encoderinfo["extra"] = (
b"\xFF\xE2" + struct.pack(">H", 6 + 70) + b"MPF\0" + b" " * 70
)
JpegImagePlugin._save(im_frame, fp, filename)
offsets.append(fp.tell())
else:
im_frame.save(fp, "JPEG")
offsets.append(fp.tell() - offsets[-1])
ifd = TiffImagePlugin.ImageFileDirectory_v2()
ifd[0xB001] = len(offsets)
mpentries = b""
data_offset = 0
for i, size in enumerate(offsets):
if i == 0:
mptype = 0x030000 # Baseline MP Primary Image
else:
mptype = 0x000000 # Undefined
mpentries += struct.pack("<LLLHH", mptype, size, data_offset, 0, 0)
if i == 0:
data_offset -= 28
data_offset += size
ifd[0xB002] = mpentries
fp.seek(28)
fp.write(b"II\x2A\x00" + o32le(8) + ifd.tobytes(8))
fp.seek(0, os.SEEK_END)
## ##
@ -124,6 +174,7 @@ class MpoImageFile(JpegImagePlugin.JpegImageFile):
# Image.register_open(MpoImageFile.format, # Image.register_open(MpoImageFile.format,
# JpegImagePlugin.jpeg_factory, _accept) # JpegImagePlugin.jpeg_factory, _accept)
Image.register_save(MpoImageFile.format, _save) Image.register_save(MpoImageFile.format, _save)
Image.register_save_all(MpoImageFile.format, _save_all)
Image.register_extension(MpoImageFile.format, ".mpo") Image.register_extension(MpoImageFile.format, ".mpo")

View File

@ -281,9 +281,9 @@ deps = {
"libs": [r"imagequant.lib"], "libs": [r"imagequant.lib"],
}, },
"harfbuzz": { "harfbuzz": {
"url": "https://github.com/harfbuzz/harfbuzz/archive/4.4.1.zip", "url": "https://github.com/harfbuzz/harfbuzz/archive/5.1.0.zip",
"filename": "harfbuzz-4.4.1.zip", "filename": "harfbuzz-5.1.0.zip",
"dir": "harfbuzz-4.4.1", "dir": "harfbuzz-5.1.0",
"build": [ "build": [
cmd_cmake("-DHB_HAVE_FREETYPE:BOOL=TRUE"), cmd_cmake("-DHB_HAVE_FREETYPE:BOOL=TRUE"),
cmd_nmake(target="clean"), cmd_nmake(target="clean"),