Merge remote-tracking branch 'upstream/main' into add-cygwin-to-ci

This commit is contained in:
DWesl 2022-02-06 11:03:11 -05:00
commit b582806887
33 changed files with 602 additions and 305 deletions

View File

@ -19,7 +19,6 @@ jobs:
amazon-2-amd64, amazon-2-amd64,
arch, arch,
centos-7-amd64, centos-7-amd64,
centos-8-amd64,
centos-stream-8-amd64, centos-stream-8-amd64,
debian-10-buster-x86, debian-10-buster-x86,
debian-11-bullseye-x86, debian-11-bullseye-x86,

View File

@ -4,7 +4,7 @@ on: [push, pull_request, workflow_dispatch]
jobs: jobs:
build: build:
runs-on: windows-2019 runs-on: windows-latest
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:

View File

@ -4,7 +4,7 @@ on: [push, pull_request, workflow_dispatch]
jobs: jobs:
build: build:
runs-on: windows-2019 runs-on: windows-latest
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:

View File

@ -5,6 +5,12 @@ Changelog (Pillow)
9.1.0 (unreleased) 9.1.0 (unreleased)
------------------ ------------------
- Enable arm64 for MSVC on Windows #5811
[gaborkertesz-linaro, gaborkertesz]
- Keep IPython/Jupyter text/plain output stable #5891
[shamrin, radarhere]
- Raise an error when performing a negative crop #5972 - Raise an error when performing a negative crop #5972
[radarhere, hugovk] [radarhere, hugovk]
@ -26,6 +32,15 @@ Changelog (Pillow)
- Remove readonly from Image.__eq__ #5930 - Remove readonly from Image.__eq__ #5930
[hugovk] [hugovk]
9.0.1 (2022-02-03)
------------------
- In show_file, use os.remove to remove temporary images. CVE-2022-24303 #6010
[radarhere, hugovk]
- Restrict builtins within lambdas for ImageMath.eval. CVE-2022-22817 #6009
[radarhere]
9.0.0 (2022-01-02) 9.0.0 (2022-01-02)
------------------ ------------------

View File

@ -58,6 +58,15 @@ def test_sanity():
assert image2_scale2.format == "EPS" assert image2_scale2.format == "EPS"
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
def test_load():
with Image.open(FILE1) as im:
assert im.load()[0, 0] == (255, 255, 255)
# Test again now that it has already been loaded once
assert im.load()[0, 0] == (255, 255, 255)
def test_invalid_file(): def test_invalid_file():
invalid_file = "Tests/images/flower.jpg" invalid_file = "Tests/images/flower.jpg"

View File

@ -5,20 +5,28 @@ from PIL import GbrImagePlugin, Image
from .helper import assert_image_equal_tofile from .helper import assert_image_equal_tofile
def test_invalid_file():
invalid_file = "Tests/images/flower.jpg"
with pytest.raises(SyntaxError):
GbrImagePlugin.GbrImageFile(invalid_file)
def test_gbr_file(): def test_gbr_file():
with Image.open("Tests/images/gbr.gbr") as im: with Image.open("Tests/images/gbr.gbr") as im:
assert_image_equal_tofile(im, "Tests/images/gbr.png") assert_image_equal_tofile(im, "Tests/images/gbr.png")
def test_load():
with Image.open("Tests/images/gbr.gbr") as im:
assert im.load()[0, 0] == (0, 0, 0, 0)
# Test again now that it has already been loaded once
assert im.load()[0, 0] == (0, 0, 0, 0)
def test_multiple_load_operations(): def test_multiple_load_operations():
with Image.open("Tests/images/gbr.gbr") as im: with Image.open("Tests/images/gbr.gbr") as im:
im.load() im.load()
im.load() im.load()
assert_image_equal_tofile(im, "Tests/images/gbr.png") assert_image_equal_tofile(im, "Tests/images/gbr.png")
def test_invalid_file():
invalid_file = "Tests/images/flower.jpg"
with pytest.raises(SyntaxError):
GbrImagePlugin.GbrImageFile(invalid_file)

View File

@ -28,6 +28,14 @@ def test_sanity():
assert im.format == "ICNS" assert im.format == "ICNS"
def test_load():
with Image.open(TEST_FILE) as im:
assert im.load()[0, 0] == (0, 0, 0, 0)
# Test again now that it has already been loaded once
assert im.load()[0, 0] == (0, 0, 0, 0)
def test_save(tmp_path): def test_save(tmp_path):
temp_file = str(tmp_path / "temp.icns") temp_file = str(tmp_path / "temp.icns")

View File

@ -18,6 +18,11 @@ def test_sanity():
assert im.get_format_mimetype() == "image/x-icon" assert im.get_format_mimetype() == "image/x-icon"
def test_load():
with Image.open(TEST_ICO_FILE) as im:
assert im.load()[0, 0] == (1, 1, 9, 255)
def test_mask(): def test_mask():
with Image.open("Tests/images/hopper_mask.ico") as im: with Image.open("Tests/images/hopper_mask.ico") as im:
assert_image_equal_tofile(im, "Tests/images/hopper_mask.png") assert_image_equal_tofile(im, "Tests/images/hopper_mask.png")

View File

@ -2,15 +2,11 @@ from PIL import WalImageFile
from .helper import assert_image_equal_tofile from .helper import assert_image_equal_tofile
TEST_FILE = "Tests/images/hopper.wal"
def test_open(): def test_open():
# Arrange
TEST_FILE = "Tests/images/hopper.wal"
# Act
with WalImageFile.open(TEST_FILE) as im: with WalImageFile.open(TEST_FILE) as im:
# Assert
assert im.format == "WAL" assert im.format == "WAL"
assert im.format_description == "Quake2 Texture" assert im.format_description == "Quake2 Texture"
assert im.mode == "P" assert im.mode == "P"
@ -19,3 +15,11 @@ def test_open():
assert isinstance(im, WalImageFile.WalImageFile) assert isinstance(im, WalImageFile.WalImageFile)
assert_image_equal_tofile(im, "Tests/images/hopper_wal.png") assert_image_equal_tofile(im, "Tests/images/hopper_wal.png")
def test_load():
with WalImageFile.open(TEST_FILE) as im:
assert im.load()[0, 0] == 122
# Test again now that it has already been loaded once
assert im.load()[0, 0] == 122

View File

@ -24,6 +24,12 @@ def test_load_raw():
assert_image_similar_tofile(im, "Tests/images/drawing_wmf_ref.png", 2.0) assert_image_similar_tofile(im, "Tests/images/drawing_wmf_ref.png", 2.0)
def test_load():
with Image.open("Tests/images/drawing.emf") as im:
if hasattr(Image.core, "drawwmf"):
assert im.load()[0, 0] == (255, 255, 255)
def test_register_handler(tmp_path): def test_register_handler(tmp_path):
class TestHandler: class TestHandler:
methodCalled = False methodCalled = False

View File

@ -52,9 +52,17 @@ def test_ops():
assert pixel(ImageMath.eval("float(B)**33", images)) == "F 8589934592.0" assert pixel(ImageMath.eval("float(B)**33", images)) == "F 8589934592.0"
def test_prevent_exec(): @pytest.mark.parametrize(
"expression",
(
"exec('pass')",
"(lambda: exec('pass'))()",
"(lambda: (lambda: exec('pass'))())()",
),
)
def test_prevent_exec(expression):
with pytest.raises(ValueError): with pytest.raises(ValueError):
ImageMath.eval("exec('pass')") ImageMath.eval(expression)
def test_logical(): def test_logical():

View File

@ -85,11 +85,13 @@ def test_ipythonviewer():
not on_ci() or is_win32(), not on_ci() or is_win32(),
reason="Only run on CIs; hangs on Windows CIs", reason="Only run on CIs; hangs on Windows CIs",
) )
def test_file_deprecated(): def test_file_deprecated(tmp_path):
f = str(tmp_path / "temp.jpg")
for viewer in ImageShow._viewers: for viewer in ImageShow._viewers:
hopper().save(f)
with pytest.warns(DeprecationWarning): with pytest.warns(DeprecationWarning):
try: try:
viewer.show_file(file="test.jpg") viewer.show_file(file=f)
except NotImplementedError: except NotImplementedError:
pass pass
with pytest.raises(TypeError): with pytest.raises(TypeError):

View File

@ -1,14 +1,15 @@
#!/bin/bash #!/bin/bash
# install libimagequant # install libimagequant
archive=libimagequant-2.17.0 archive=libimagequant-4.0.0
./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
pushd $archive pushd $archive/imagequant-sys
make shared cargo install cargo-c
sudo cp libimagequant.so* /usr/lib/ cargo cinstall --prefix=/usr --destdir=.
sudo cp libimagequant.h /usr/include/ sudo cp usr/lib/libimagequant.so* /usr/lib/
sudo cp usr/include/libimagequant.h /usr/include/
popd popd

View File

@ -2,7 +2,7 @@
# install raqm # install raqm
archive=libraqm-0.8.0 archive=libraqm-0.9.0
./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

@ -169,7 +169,7 @@ Many of Pillow's features require external libraries:
* **littlecms** provides color management * **littlecms** provides color management
* Pillow version 2.2.1 and below uses liblcms1, Pillow 2.3.0 and * Pillow version 2.2.1 and below uses liblcms1, Pillow 2.3.0 and
above uses liblcms2. Tested with **1.19** and **2.7-2.12**. above uses liblcms2. Tested with **1.19** and **2.7-2.13**.
* **libwebp** provides the WebP format. * **libwebp** provides the WebP format.
@ -187,7 +187,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-2.17.0** * Pillow has been tested with libimagequant **2.6-4.0**
* 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.
@ -453,8 +453,6 @@ These platforms are built and tested for every change.
+----------------------------------+----------------------------+---------------------+ +----------------------------------+----------------------------+---------------------+
| CentOS 7 | 3.9 | x86-64 | | CentOS 7 | 3.9 | x86-64 |
+----------------------------------+----------------------------+---------------------+ +----------------------------------+----------------------------+---------------------+
| CentOS 8 | 3.9 | x86-64 |
+----------------------------------+----------------------------+---------------------+
| CentOS Stream 8 | 3.9 | x86-64 | | CentOS Stream 8 | 3.9 | x86-64 |
+----------------------------------+----------------------------+---------------------+ +----------------------------------+----------------------------+---------------------+
| Debian 10 Buster | 3.7 | x86 | | Debian 10 Buster | 3.7 | x86 |
@ -532,6 +530,8 @@ These platforms have been reported to work at the versions mentioned.
+----------------------------------+---------------------------+------------------+--------------+ +----------------------------------+---------------------------+------------------+--------------+
| CentOS 6.3 | 2.7, 3.3 | |x86 | | CentOS 6.3 | 2.7, 3.3 | |x86 |
+----------------------------------+---------------------------+------------------+--------------+ +----------------------------------+---------------------------+------------------+--------------+
| CentOS 8 | 3.9 | 9.0.0 |x86-64 |
+----------------------------------+---------------------------+------------------+--------------+
| Fedora 23 | 2.7, 3.4 | 3.1.0 |x86-64 | | Fedora 23 | 2.7, 3.4 | 3.1.0 |x86-64 |
+----------------------------------+---------------------------+------------------+--------------+ +----------------------------------+---------------------------+------------------+--------------+
| Ubuntu Linux 12.04 LTS (Precise) | | 2.6, 3.2, 3.3, 3.4, 3.5 | 3.4.1 |x86,x86-64 | | Ubuntu Linux 12.04 LTS (Precise) | | 2.6, 3.2, 3.3, 3.4, 3.5 | 3.4.1 |x86,x86-64 |

View File

@ -0,0 +1,23 @@
9.0.1
-----
Security
========
This release addresses several security problems.
:cve:`CVE-2022-24303`: If the path to the temporary directory on Linux or macOS
contained a space, this would break removal of the temporary image file after
``im.show()`` (and related actions), and potentially remove an unrelated file. This
has been present since PIL.
:cve:`CVE-2022-22817`: While Pillow 9.0 restricted top-level builtins available to
:py:meth:`PIL.ImageMath.eval`, it did not prevent builtins available to lambda
expressions. These are now also restricted.
Other Changes
=============
Pillow 9.0 added support for ``xdg-open`` as an image viewer, but there have been
reports that the temporary image file was removed too quickly to be loaded into the
final application. A delay has been added.

View File

@ -14,6 +14,7 @@ expected to be backported to earlier versions.
.. toctree:: .. toctree::
:maxdepth: 2 :maxdepth: 2
9.0.1
9.0.0 9.0.0
8.4.0 8.4.0
8.3.2 8.3.2

View File

@ -329,12 +329,12 @@ class EpsImageFile(ImageFile.ImageFile):
def load(self, scale=1, transparency=False): def load(self, scale=1, transparency=False):
# Load EPS via Ghostscript # Load EPS via Ghostscript
if not self.tile: if self.tile:
return
self.im = Ghostscript(self.tile, self.size, self.fp, scale, transparency) self.im = Ghostscript(self.tile, self.size, self.fp, scale, transparency)
self.mode = self.im.mode self.mode = self.im.mode
self._size = self.im.size self._size = self.im.size
self.tile = [] self.tile = []
return Image.Image.load(self)
def load_seek(self, *args, **kwargs): def load_seek(self, *args, **kwargs):
# we can't incrementally load, so force ImageFile.parser to # we can't incrementally load, so force ImageFile.parser to

View File

@ -84,12 +84,10 @@ class GbrImageFile(ImageFile.ImageFile):
self._data_size = width * height * color_depth self._data_size = width * height * color_depth
def load(self): def load(self):
if self.im: if not self.im:
# Already loaded
return
self.im = Image.core.new(self.mode, self.size) self.im = Image.core.new(self.mode, self.size)
self.frombytes(self.fp.read(self._data_size)) self.frombytes(self.fp.read(self._data_size))
return Image.Image.load(self)
# #

View File

@ -286,21 +286,22 @@ class IcnsImageFile(ImageFile.ImageFile):
self.best_size[1] * self.best_size[2], self.best_size[1] * self.best_size[2],
) )
Image.Image.load(self) px = Image.Image.load(self)
if self.im and self.im.size == self.size: if self.im and self.im.size == self.size:
# Already loaded # Already loaded
return return px
self.load_prepare() self.load_prepare()
# This is likely NOT the best way to do it, but whatever. # This is likely NOT the best way to do it, but whatever.
im = self.icns.getimage(self.best_size) im = self.icns.getimage(self.best_size)
# If this is a PNG or JPEG 2000, it won't be loaded yet # If this is a PNG or JPEG 2000, it won't be loaded yet
im.load() px = im.load()
self.im = im.im self.im = im.im
self.mode = im.mode self.mode = im.mode
self.size = im.size self.size = im.size
self.load_end()
return px
def _save(im, fp, filename): def _save(im, fp, filename):

View File

@ -306,7 +306,7 @@ class IcoImageFile(ImageFile.ImageFile):
def load(self): def load(self):
if self.im and self.im.size == self.size: if self.im and self.im.size == self.size:
# Already loaded # Already loaded
return return Image.Image.load(self)
im = self.ico.getimage(self.size) im = self.ico.getimage(self.size)
# if tile is PNG, it won't really be loaded yet # if tile is PNG, it won't really be loaded yet
im.load() im.load()

View File

@ -328,6 +328,7 @@ class StubImageFile(ImageFile):
# become the other object (!) # become the other object (!)
self.__class__ = image.__class__ self.__class__ = image.__class__
self.__dict__ = image.__dict__ self.__dict__ = image.__dict__
return image.load()
def _load(self): def _load(self):
"""(Hook) Find actual image loader.""" """(Hook) Find actual image loader."""

View File

@ -240,11 +240,18 @@ def eval(expression, _dict={}, **kw):
if hasattr(v, "im"): if hasattr(v, "im"):
args[k] = _Operand(v) args[k] = _Operand(v)
code = compile(expression, "<string>", "eval") compiled_code = compile(expression, "<string>", "eval")
def scan(code):
for const in code.co_consts:
if type(const) == type(compiled_code):
scan(const)
for name in code.co_names: for name in code.co_names:
if name not in args and name != "abs": if name not in args and name != "abs":
raise ValueError(f"'{name}' not allowed") raise ValueError(f"'{name}' not allowed")
scan(compiled_code)
out = builtins.eval(expression, {"__builtins": {"abs": abs}}, args) out = builtins.eval(expression, {"__builtins": {"abs": abs}}, args)
try: try:
return out.im return out.im

View File

@ -15,7 +15,6 @@ import os
import shutil import shutil
import subprocess import subprocess
import sys import sys
import tempfile
import warnings import warnings
from shlex import quote from shlex import quote
@ -127,6 +126,16 @@ class Viewer:
os.system(self.get_command(path, **options)) os.system(self.get_command(path, **options))
return 1 return 1
def _remove_path_after_delay(self, path):
subprocess.Popen(
[
sys.executable,
"-c",
"import os, sys, time; time.sleep(20); os.remove(sys.argv[1])",
path,
]
)
# -------------------------------------------------------------------- # --------------------------------------------------------------------
@ -180,16 +189,8 @@ class MacViewer(Viewer):
path = options.pop("file") path = options.pop("file")
else: else:
raise TypeError("Missing required argument: 'path'") raise TypeError("Missing required argument: 'path'")
fd, temp_path = tempfile.mkstemp() subprocess.call(["open", "-a", "Preview.app", path])
with os.fdopen(fd, "w") as f: self._remove_path_after_delay(path)
f.write(path)
with open(temp_path) as f:
subprocess.Popen(
["im=$(cat); open -a Preview.app $im; sleep 20; rm -f $im"],
shell=True,
stdin=f,
)
os.remove(temp_path)
return 1 return 1
@ -205,6 +206,16 @@ class UnixViewer(Viewer):
command = self.get_command_ex(file, **options)[0] command = self.get_command_ex(file, **options)[0]
return f"({command} {quote(file)}; rm -f {quote(file)})&" return f"({command} {quote(file)}; rm -f {quote(file)})&"
class XDGViewer(UnixViewer):
"""
The freedesktop.org ``xdg-open`` command.
"""
def get_command_ex(self, file, **options):
command = executable = "xdg-open"
return command, executable
def show_file(self, path=None, **options): def show_file(self, path=None, **options):
""" """
Display given file. Display given file.
@ -223,28 +234,11 @@ class UnixViewer(Viewer):
path = options.pop("file") path = options.pop("file")
else: else:
raise TypeError("Missing required argument: 'path'") raise TypeError("Missing required argument: 'path'")
fd, temp_path = tempfile.mkstemp() subprocess.Popen(["xdg-open", path])
with os.fdopen(fd, "w") as f: self._remove_path_after_delay(path)
f.write(path)
with open(temp_path) as f:
command = self.get_command_ex(path, **options)[0]
subprocess.Popen(
["im=$(cat);" + command + " $im; rm -f $im"], shell=True, stdin=f
)
os.remove(temp_path)
return 1 return 1
class XDGViewer(UnixViewer):
"""
The freedesktop.org ``xdg-open`` command.
"""
def get_command_ex(self, file, **options):
command = executable = "xdg-open"
return command, executable
class DisplayViewer(UnixViewer): class DisplayViewer(UnixViewer):
""" """
The ImageMagick ``display`` command. The ImageMagick ``display`` command.
@ -257,6 +251,32 @@ class DisplayViewer(UnixViewer):
command += f" -name {quote(title)}" command += f" -name {quote(title)}"
return command, executable return command, executable
def show_file(self, path=None, **options):
"""
Display given file.
Before Pillow 9.1.0, the first argument was ``file``. This is now deprecated,
and ``path`` should be used instead.
"""
if path is None:
if "file" in options:
warnings.warn(
"The 'file' argument is deprecated and will be removed in Pillow "
"10 (2023-07-01). Use 'path' instead.",
DeprecationWarning,
)
path = options.pop("file")
else:
raise TypeError("Missing required argument: 'path'")
args = ["display"]
if "title" in options:
args += ["-name", options["title"]]
args.append(path)
subprocess.Popen(args)
os.remove(path)
return 1
class GmDisplayViewer(UnixViewer): class GmDisplayViewer(UnixViewer):
"""The GraphicsMagick ``gm display`` command.""" """The GraphicsMagick ``gm display`` command."""
@ -266,6 +286,27 @@ class GmDisplayViewer(UnixViewer):
command = "gm display" command = "gm display"
return command, executable return command, executable
def show_file(self, path=None, **options):
"""
Display given file.
Before Pillow 9.1.0, the first argument was ``file``. This is now deprecated,
and ``path`` should be used instead.
"""
if path is None:
if "file" in options:
warnings.warn(
"The 'file' argument is deprecated and will be removed in Pillow "
"10 (2023-07-01). Use 'path' instead.",
DeprecationWarning,
)
path = options.pop("file")
else:
raise TypeError("Missing required argument: 'path'")
subprocess.Popen(["gm", "display", path])
os.remove(path)
return 1
class EogViewer(UnixViewer): class EogViewer(UnixViewer):
"""The GNOME Image Viewer ``eog`` command.""" """The GNOME Image Viewer ``eog`` command."""
@ -275,6 +316,27 @@ class EogViewer(UnixViewer):
command = "eog -n" command = "eog -n"
return command, executable return command, executable
def show_file(self, path=None, **options):
"""
Display given file.
Before Pillow 9.1.0, the first argument was ``file``. This is now deprecated,
and ``path`` should be used instead.
"""
if path is None:
if "file" in options:
warnings.warn(
"The 'file' argument is deprecated and will be removed in Pillow "
"10 (2023-07-01). Use 'path' instead.",
DeprecationWarning,
)
path = options.pop("file")
else:
raise TypeError("Missing required argument: 'path'")
subprocess.Popen(["eog", "-n", path])
os.remove(path)
return 1
class XVViewer(UnixViewer): class XVViewer(UnixViewer):
""" """
@ -290,6 +352,32 @@ class XVViewer(UnixViewer):
command += f" -name {quote(title)}" command += f" -name {quote(title)}"
return command, executable return command, executable
def show_file(self, path=None, **options):
"""
Display given file.
Before Pillow 9.1.0, the first argument was ``file``. This is now deprecated,
and ``path`` should be used instead.
"""
if path is None:
if "file" in options:
warnings.warn(
"The 'file' argument is deprecated and will be removed in Pillow "
"10 (2023-07-01). Use 'path' instead.",
DeprecationWarning,
)
path = options.pop("file")
else:
raise TypeError("Missing required argument: 'path'")
args = ["xv"]
if "title" in options:
args += ["-name", options["title"]]
args.append(path)
subprocess.Popen(args)
os.remove(path)
return 1
if sys.platform not in ("win32", "darwin"): # unixoids if sys.platform not in ("win32", "darwin"): # unixoids
if shutil.which("xdg-open"): if shutil.which("xdg-open"):

View File

@ -51,14 +51,11 @@ class WalImageFile(ImageFile.ImageFile):
self.info["next_name"] = next_name self.info["next_name"] = next_name
def load(self): def load(self):
if self.im: if not self.im:
# Already loaded
return
self.im = Image.core.new(self.mode, self.size) self.im = Image.core.new(self.mode, self.size)
self.frombytes(self.fp.read(self.size[0] * self.size[1])) self.frombytes(self.fp.read(self.size[0] * self.size[1]))
self.putpalette(quake2palette) self.putpalette(quake2palette)
Image.Image.load(self) return Image.Image.load(self)
def open(filename): def open(filename):

View File

@ -158,7 +158,7 @@ class WmfStubImageFile(ImageFile.StubImageFile):
(x1 - x0) * self.info["dpi"] // self._inch, (x1 - x0) * self.info["dpi"] // self._inch,
(y1 - y0) * self.info["dpi"] // self._inch, (y1 - y0) * self.info["dpi"] // self._inch,
) )
super().load() return super().load()
def _save(im, fp, filename): def _save(im, fp, filename):

View File

@ -1,7 +1,7 @@
The MIT License (MIT) The MIT License (MIT)
Copyright © 2015 Information Technology Authority (ITA) <foss@ita.gov.om> Copyright © 2015 Information Technology Authority (ITA) <foss@ita.gov.om>
Copyright © 2016-2021 Khaled Hosny <khaled@aliftype.com> Copyright © 2016-2022 Khaled Hosny <khaled@aliftype.com>
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View File

@ -68,6 +68,7 @@ Projects using Raqm
3. [FontView](https://github.com/googlei18n/fontview) 3. [FontView](https://github.com/googlei18n/fontview)
4. [Pillow](https://github.com/python-pillow) 4. [Pillow](https://github.com/python-pillow)
5. [mplcairo](https://github.com/anntzer/mplcairo) 5. [mplcairo](https://github.com/anntzer/mplcairo)
6. [CEGUI](https://github.com/cegui/cegui)
The following projects have patches to support complex text layout using Raqm: The following projects have patches to support complex text layout using Raqm:
@ -77,8 +78,8 @@ The following projects have patches to support complex text layout using Raqm:
[1]: http://fribidi.org [1]: https://github.com/fribidi/fribidi
[2]: https://github.com/Tehreer/SheenBidi [2]: https://github.com/Tehreer/SheenBidi
[3]: http://harfbuzz.org [3]: https://github.com/harfbuzz/harfbuzz
[4]: https://www.freetype.org [4]: https://www.freetype.org
[5]: https://www.gtk.org/gtk-doc [5]: https://www.gtk.org/gtk-doc

View File

@ -32,10 +32,10 @@
#define _RAQM_VERSION_H_ #define _RAQM_VERSION_H_
#define RAQM_VERSION_MAJOR 0 #define RAQM_VERSION_MAJOR 0
#define RAQM_VERSION_MINOR 8 #define RAQM_VERSION_MINOR 9
#define RAQM_VERSION_MICRO 0 #define RAQM_VERSION_MICRO 0
#define RAQM_VERSION_STRING "0.8.0" #define RAQM_VERSION_STRING "0.9.0"
#define RAQM_VERSION_ATLEAST(major,minor,micro) \ #define RAQM_VERSION_ATLEAST(major,minor,micro) \
((major)*10000+(minor)*100+(micro) <= \ ((major)*10000+(minor)*100+(micro) <= \

View File

@ -1,6 +1,6 @@
/* /*
* Copyright © 2015 Information Technology Authority (ITA) <foss@ita.gov.om> * Copyright © 2015 Information Technology Authority (ITA) <foss@ita.gov.om>
* Copyright © 2016-2021 Khaled Hosny <khaled@aliftype.com> * Copyright © 2016-2022 Khaled Hosny <khaled@aliftype.com>
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy * Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to * of this software and associated documentation files (the "Software"), to
@ -24,7 +24,6 @@
#ifdef HAVE_CONFIG_H #ifdef HAVE_CONFIG_H
#include "config.h" #include "config.h"
#undef HAVE_CONFIG_H // Workaround for Fribidi 1.0.5 and earlier
#endif #endif
#include <assert.h> #include <assert.h>
@ -38,29 +37,11 @@
#else #else
#include "../fribidi-shim/fribidi.h" #include "../fribidi-shim/fribidi.h"
#endif #endif
#if FRIBIDI_MAJOR_VERSION >= 1
#define USE_FRIBIDI_EX_API
#endif
#endif #endif
#include <hb.h> #include <hb.h>
#include <hb-ft.h> #include <hb-ft.h>
#if FREETYPE_MAJOR > 2 || \
FREETYPE_MAJOR == 2 && FREETYPE_MINOR >= 11
#define HAVE_FT_GET_TRANSFORM
#endif
#if HB_VERSION_ATLEAST(2, 0, 0)
#define HAVE_HB_BUFFER_SET_INVISIBLE_GLYPH
#endif
#if HB_VERSION_ATLEAST(1, 8, 0)
#define HAVE_DECL_HB_BUFFER_FLAG_REMOVE_DEFAULT_IGNORABLES 1
#else
#define HAVE_DECL_HB_BUFFER_FLAG_REMOVE_DEFAULT_IGNORABLES 0
#endif
#include "raqm.h" #include "raqm.h"
/** /**
@ -190,13 +171,9 @@
typedef FriBidiLevel _raqm_bidi_level_t; typedef FriBidiLevel _raqm_bidi_level_t;
#endif #endif
typedef enum {
RAQM_FLAG_NONE = 0,
RAQM_FLAG_UTF8 = 1 << 0
} _raqm_flags_t;
typedef struct { typedef struct {
FT_Face ftface; FT_Face ftface;
int ftloadflags;
hb_language_t lang; hb_language_t lang;
hb_script_t script; hb_script_t script;
} _raqm_text_info; } _raqm_text_info;
@ -209,6 +186,7 @@ struct _raqm {
uint32_t *text; uint32_t *text;
char *text_utf8; char *text_utf8;
size_t text_len; size_t text_len;
size_t text_capacity_bytes;
_raqm_text_info *text_info; _raqm_text_info *text_info;
@ -219,17 +197,17 @@ struct _raqm {
size_t features_len; size_t features_len;
raqm_run_t *runs; raqm_run_t *runs;
raqm_run_t *runs_pool;
raqm_glyph_t *glyphs; raqm_glyph_t *glyphs;
size_t glyphs_capacity;
_raqm_flags_t flags;
int ft_loadflags;
int invisible_glyph; int invisible_glyph;
}; };
struct _raqm_run { struct _raqm_run {
int pos; uint32_t pos;
int len; uint32_t len;
hb_direction_t direction; hb_direction_t direction;
hb_script_t script; hb_script_t script;
@ -243,31 +221,21 @@ static uint32_t
_raqm_u8_to_u32_index (raqm_t *rq, _raqm_u8_to_u32_index (raqm_t *rq,
uint32_t index); uint32_t index);
static bool static void
_raqm_init_text_info (raqm_t *rq) _raqm_init_text_info (raqm_t *rq)
{ {
hb_language_t default_lang; hb_language_t default_lang = hb_language_get_default ();
if (rq->text_info)
return true;
rq->text_info = malloc (sizeof (_raqm_text_info) * rq->text_len);
if (!rq->text_info)
return false;
default_lang = hb_language_get_default ();
for (size_t i = 0; i < rq->text_len; i++) for (size_t i = 0; i < rq->text_len; i++)
{ {
rq->text_info[i].ftface = NULL; rq->text_info[i].ftface = NULL;
rq->text_info[i].ftloadflags = -1;
rq->text_info[i].lang = default_lang; rq->text_info[i].lang = default_lang;
rq->text_info[i].script = HB_SCRIPT_INVALID; rq->text_info[i].script = HB_SCRIPT_INVALID;
} }
return true;
} }
static void static void
_raqm_free_text_info (raqm_t *rq) _raqm_release_text_info (raqm_t *rq)
{ {
if (!rq->text_info) if (!rq->text_info)
return; return;
@ -277,9 +245,6 @@ _raqm_free_text_info (raqm_t *rq)
if (rq->text_info[i].ftface) if (rq->text_info[i].ftface)
FT_Done_Face (rq->text_info[i].ftface); FT_Done_Face (rq->text_info[i].ftface);
} }
free (rq->text_info);
rq->text_info = NULL;
} }
static bool static bool
@ -289,6 +254,9 @@ _raqm_compare_text_info (_raqm_text_info a,
if (a.ftface != b.ftface) if (a.ftface != b.ftface)
return false; return false;
if (a.ftloadflags != b.ftloadflags)
return false;
if (a.lang != b.lang) if (a.lang != b.lang)
return false; return false;
@ -298,6 +266,88 @@ _raqm_compare_text_info (_raqm_text_info a,
return true; return true;
} }
static void
_raqm_free_text(raqm_t* rq)
{
free (rq->text);
rq->text = NULL;
rq->text_info = NULL;
rq->text_utf8 = NULL;
rq->text_len = 0;
rq->text_capacity_bytes = 0;
}
static bool
_raqm_alloc_text(raqm_t *rq,
size_t len,
bool need_utf8)
{
/* Allocate contiguous memory block for texts and text_info */
size_t mem_size = (sizeof (uint32_t) + sizeof (_raqm_text_info)) * len;
if (need_utf8)
mem_size += sizeof (char) * len;
if (mem_size > rq->text_capacity_bytes)
{
void* new_mem = realloc (rq->text, mem_size);
if (!new_mem)
{
_raqm_free_text (rq);
return false;
}
rq->text_capacity_bytes = mem_size;
rq->text = new_mem;
}
rq->text_info = (_raqm_text_info*)(rq->text + len);
rq->text_utf8 = need_utf8 ? (char*)(rq->text_info + len) : NULL;
return true;
}
static raqm_run_t*
_raqm_alloc_run (raqm_t *rq)
{
raqm_run_t *run = rq->runs_pool;
if (run)
{
rq->runs_pool = run->next;
}
else
{
run = malloc (sizeof (raqm_run_t));
run->font = NULL;
run->buffer = NULL;
}
run->pos = 0;
run->len = 0;
run->direction = HB_DIRECTION_INVALID;
run->script = HB_SCRIPT_INVALID;
run->next = NULL;
return run;
}
static void
_raqm_free_runs (raqm_run_t *runs)
{
while (runs)
{
raqm_run_t *run = runs;
runs = runs->next;
if (run->buffer)
hb_buffer_destroy (run->buffer);
if (run->font)
hb_font_destroy (run->font);
free (run);
}
}
/** /**
* raqm_create: * raqm_create:
* *
@ -322,26 +372,26 @@ raqm_create (void)
rq->ref_count = 1; rq->ref_count = 1;
rq->text = NULL;
rq->text_utf8 = NULL;
rq->text_len = 0;
rq->text_info = NULL;
rq->base_dir = RAQM_DIRECTION_DEFAULT; rq->base_dir = RAQM_DIRECTION_DEFAULT;
rq->resolved_dir = RAQM_DIRECTION_DEFAULT; rq->resolved_dir = RAQM_DIRECTION_DEFAULT;
rq->features = NULL; rq->features = NULL;
rq->features_len = 0; rq->features_len = 0;
rq->runs = NULL;
rq->glyphs = NULL;
rq->flags = RAQM_FLAG_NONE;
rq->ft_loadflags = -1;
rq->invisible_glyph = 0; rq->invisible_glyph = 0;
rq->text = NULL;
rq->text_utf8 = NULL;
rq->text_info = NULL;
rq->text_capacity_bytes = 0;
rq->text_len = 0;
rq->runs = NULL;
rq->runs_pool = NULL;
rq->glyphs = NULL;
rq->glyphs_capacity = 0;
return rq; return rq;
} }
@ -366,28 +416,13 @@ raqm_reference (raqm_t *rq)
return rq; return rq;
} }
static void
_raqm_free_runs (raqm_t *rq)
{
raqm_run_t *runs = rq->runs;
while (runs)
{
raqm_run_t *run = runs;
runs = runs->next;
hb_buffer_destroy (run->buffer);
hb_font_destroy (run->font);
free (run);
}
}
/** /**
* raqm_destroy: * raqm_destroy:
* @rq: a #raqm_t. * @rq: a #raqm_t.
* *
* Decreases the reference count on @rq by one. If the result is zero, then @rq * Decreases the reference count on @rq by one. If the result is zero, then @rq
* and all associated resources are freed. * and all associated resources are freed.
* See cairo_reference(). * See raqm_reference().
* *
* Since: 0.1 * Since: 0.1
*/ */
@ -397,14 +432,60 @@ raqm_destroy (raqm_t *rq)
if (!rq || --rq->ref_count != 0) if (!rq || --rq->ref_count != 0)
return; return;
free (rq->text); _raqm_release_text_info (rq);
free (rq->text_utf8); _raqm_free_text (rq);
_raqm_free_text_info (rq); _raqm_free_runs (rq->runs);
_raqm_free_runs (rq); _raqm_free_runs (rq->runs_pool);
free (rq->glyphs); free (rq->glyphs);
free (rq->features);
free (rq); free (rq);
} }
/**
* raqm_clear_contents:
* @rq: a #raqm_t.
*
* Clears internal state of previously used raqm_t object, making it ready
* for reuse and keeping some of allocated memory to increase performance.
*
* Since: 0.9
*/
void
raqm_clear_contents (raqm_t *rq)
{
if (!rq)
return;
_raqm_release_text_info (rq);
/* Return allocated runs to the pool, keep hb buffers for reuse */
raqm_run_t *run = rq->runs;
while (run)
{
if (run->buffer)
hb_buffer_reset (run->buffer);
if (run->font)
{
hb_font_destroy (run->font);
run->font = NULL;
}
if (!run->next)
{
run->next = rq->runs_pool;
rq->runs_pool = rq->runs;
rq->runs = NULL;
break;
}
run = run->next;
}
rq->text_len = 0;
rq->resolved_dir = RAQM_DIRECTION_DEFAULT;
}
/** /**
* raqm_set_text: * raqm_set_text:
* @rq: a #raqm_t. * @rq: a #raqm_t.
@ -429,23 +510,20 @@ raqm_set_text (raqm_t *rq,
if (!rq || !text) if (!rq || !text)
return false; return false;
rq->text_len = len; /* Call raqm_clear_contents to reuse this raqm_t */
if (rq->text_len)
return false;
/* Empty string, dont fail but do nothing */ /* Empty string, dont fail but do nothing */
if (!len) if (!len)
return true; return true;
free (rq->text); if (!_raqm_alloc_text(rq, len, false))
rq->text = malloc (sizeof (uint32_t) * rq->text_len);
if (!rq->text)
return false; return false;
_raqm_free_text_info (rq); rq->text_len = len;
if (!_raqm_init_text_info (rq)) memcpy (rq->text, text, sizeof (uint32_t) * len);
return false; _raqm_init_text_info (rq);
memcpy (rq->text, text, sizeof (uint32_t) * rq->text_len);
return true; return true;
} }
@ -515,37 +593,25 @@ raqm_set_text_utf8 (raqm_t *rq,
const char *text, const char *text,
size_t len) size_t len)
{ {
uint32_t *unicode;
size_t ulen;
bool ok;
if (!rq || !text) if (!rq || !text)
return false; return false;
/* Call raqm_clear_contents to reuse this raqm_t */
if (rq->text_len)
return false;
/* Empty string, dont fail but do nothing */ /* Empty string, dont fail but do nothing */
if (!len) if (!len)
{
rq->text_len = len;
return true; return true;
}
rq->flags |= RAQM_FLAG_UTF8; if (!_raqm_alloc_text(rq, len, true))
rq->text_utf8 = malloc (sizeof (char) * len);
if (!rq->text_utf8)
return false;
unicode = malloc (sizeof (uint32_t) * len);
if (!unicode)
return false; return false;
rq->text_len = _raqm_u8_to_u32 (text, len, rq->text);
memcpy (rq->text_utf8, text, sizeof (char) * len); memcpy (rq->text_utf8, text, sizeof (char) * len);
_raqm_init_text_info (rq);
ulen = _raqm_u8_to_u32 (text, len, unicode); return true;
ok = raqm_set_text (rq, unicode, ulen);
free (unicode);
return ok;
} }
/** /**
@ -561,7 +627,7 @@ raqm_set_text_utf8 (raqm_t *rq,
* *
* The default is #RAQM_DIRECTION_DEFAULT, which determines the paragraph * The default is #RAQM_DIRECTION_DEFAULT, which determines the paragraph
* direction based on the first character with strong bidi type (see [rule * direction based on the first character with strong bidi type (see [rule
* P2](http://unicode.org/reports/tr9/#P2) in Unicode Bidirectional Algorithm), * P2](https://unicode.org/reports/tr9/#P2) in Unicode Bidirectional Algorithm),
* which can be good enough for many cases but has problems when a mainly * which can be good enough for many cases but has problems when a mainly
* right-to-left paragraph starts with a left-to-right character and vice versa * right-to-left paragraph starts with a left-to-right character and vice versa
* as the detected paragraph direction will be the wrong one, or when text does * as the detected paragraph direction will be the wrong one, or when text does
@ -629,7 +695,7 @@ raqm_set_language (raqm_t *rq,
if (!rq->text_len) if (!rq->text_len)
return true; return true;
if (rq->flags & RAQM_FLAG_UTF8) if (rq->text_utf8)
{ {
start = _raqm_u8_to_u32_index (rq, start); start = _raqm_u8_to_u32_index (rq, start);
end = _raqm_u8_to_u32_index (rq, end); end = _raqm_u8_to_u32_index (rq, end);
@ -686,13 +752,14 @@ raqm_add_font_feature (raqm_t *rq,
ok = hb_feature_from_string (feature, len, &fea); ok = hb_feature_from_string (feature, len, &fea);
if (ok) if (ok)
{ {
rq->features_len++; void* new_features = realloc (rq->features,
rq->features = realloc (rq->features, sizeof (hb_feature_t) * (rq->features_len + 1));
sizeof (hb_feature_t) * (rq->features_len)); if (!new_features)
if (!rq->features)
return false; return false;
rq->features[rq->features_len - 1] = fea; rq->features = new_features;
rq->features[rq->features_len] = fea;
rq->features_len++;
} }
return ok; return ok;
@ -700,12 +767,13 @@ raqm_add_font_feature (raqm_t *rq,
static hb_font_t * static hb_font_t *
_raqm_create_hb_font (raqm_t *rq, _raqm_create_hb_font (raqm_t *rq,
FT_Face face) FT_Face face,
int loadflags)
{ {
hb_font_t *font = hb_ft_font_create_referenced (face); hb_font_t *font = hb_ft_font_create_referenced (face);
if (rq->ft_loadflags >= 0) if (loadflags >= 0)
hb_ft_font_set_load_flags (font, rq->ft_loadflags); hb_ft_font_set_load_flags (font, loadflags);
return font; return font;
} }
@ -796,7 +864,7 @@ raqm_set_freetype_face_range (raqm_t *rq,
if (!rq->text_len) if (!rq->text_len)
return true; return true;
if (rq->flags & RAQM_FLAG_UTF8) if (rq->text_utf8)
{ {
start = _raqm_u8_to_u32_index (rq, start); start = _raqm_u8_to_u32_index (rq, start);
end = _raqm_u8_to_u32_index (rq, end); end = _raqm_u8_to_u32_index (rq, end);
@ -805,6 +873,30 @@ raqm_set_freetype_face_range (raqm_t *rq,
return _raqm_set_freetype_face (rq, face, start, end); return _raqm_set_freetype_face (rq, face, start, end);
} }
static bool
_raqm_set_freetype_load_flags (raqm_t *rq,
int flags,
size_t start,
size_t end)
{
if (!rq)
return false;
if (!rq->text_len)
return true;
if (start >= rq->text_len || end > rq->text_len)
return false;
if (!rq->text_info)
return false;
for (size_t i = start; i < end; i++)
rq->text_info[i].ftloadflags = flags;
return true;
}
/** /**
* raqm_set_freetype_load_flags: * raqm_set_freetype_load_flags:
* @rq: a #raqm_t. * @rq: a #raqm_t.
@ -825,12 +917,57 @@ bool
raqm_set_freetype_load_flags (raqm_t *rq, raqm_set_freetype_load_flags (raqm_t *rq,
int flags) int flags)
{ {
return _raqm_set_freetype_load_flags(rq, flags, 0, rq->text_len);
}
/**
* raqm_set_freetype_load_flags_range:
* @rq: a #raqm_t.
* @flags: FreeType load flags.
* @start: index of first character that should use @flags.
* @len: number of characters using @flags.
*
* Sets the load flags passed to FreeType when loading glyphs for @len-number
* of characters staring at @start. Flags should be the same as used by the
* client when rendering corresponding FreeType glyphs. The @start and @len
* are input string array indices (i.e. counting bytes in UTF-8 and scaler
* values in UTF-32).
*
* This method can be used repeatedly to set different flags for different
* parts of the text. It is the responsibility of the client to make sure that
* flag ranges cover the whole text.
*
* This requires version of HarfBuzz that has hb_ft_font_set_load_flags(), for
* older version the flags will be ignored.
*
* See also raqm_set_freetype_load_flags().
*
* Return value:
* %true if no errors happened, %false otherwise.
*
* Since: 0.9
*/
bool
raqm_set_freetype_load_flags_range (raqm_t *rq,
int flags,
size_t start,
size_t len)
{
size_t end = start + len;
if (!rq) if (!rq)
return false; return false;
rq->ft_loadflags = flags; if (!rq->text_len)
return true; return true;
if (rq->text_utf8)
{
start = _raqm_u8_to_u32_index (rq, start);
end = _raqm_u8_to_u32_index (rq, end);
}
return _raqm_set_freetype_load_flags (rq, flags, start, end);
} }
/** /**
@ -841,17 +978,10 @@ raqm_set_freetype_load_flags (raqm_t *rq,
* Sets the glyph id to be used for invisible glyhphs. * Sets the glyph id to be used for invisible glyhphs.
* *
* If @gid is negative, invisible glyphs will be suppressed from the output. * If @gid is negative, invisible glyphs will be suppressed from the output.
* This requires HarfBuzz 1.8.0 or later. If raqm is used with an earlier
* HarfBuzz version, the return value will be %false and the shaping behavior
* does not change.
* *
* If @gid is zero, invisible glyphs will be rendered as space. * If @gid is zero, invisible glyphs will be rendered as space.
* This works on all versions of HarfBuzz.
* *
* If @gid is a positive number, it will be used for invisible glyphs. * If @gid is a positive number, it will be used for invisible glyphs.
* This requires a version of HarfBuzz that has
* hb_buffer_set_invisible_glyph(). For older versions, the return value
* will be %false and the shaping behavior does not change.
* *
* Return value: * Return value:
* %true if no errors happened, %false otherwise. * %true if no errors happened, %false otherwise.
@ -865,17 +995,6 @@ raqm_set_invisible_glyph (raqm_t *rq,
if (!rq) if (!rq)
return false; return false;
#ifndef HAVE_HB_BUFFER_SET_INVISIBLE_GLYPH
if (gid > 0)
return false;
#endif
#if !defined(HAVE_DECL_HB_BUFFER_FLAG_REMOVE_DEFAULT_IGNORABLES) || \
!HAVE_DECL_HB_BUFFER_FLAG_REMOVE_DEFAULT_IGNORABLES
if (gid < 0)
return false;
#endif
rq->invisible_glyph = gid; rq->invisible_glyph = gid;
return true; return true;
} }
@ -961,18 +1080,21 @@ raqm_get_glyphs (raqm_t *rq,
for (raqm_run_t *run = rq->runs; run != NULL; run = run->next) for (raqm_run_t *run = rq->runs; run != NULL; run = run->next)
count += hb_buffer_get_length (run->buffer); count += hb_buffer_get_length (run->buffer);
*length = count; if (count > rq->glyphs_capacity)
{
if (rq->glyphs) void* new_mem = realloc (rq->glyphs, sizeof (raqm_glyph_t) * count);
free (rq->glyphs); if (!new_mem)
rq->glyphs = malloc (sizeof (raqm_glyph_t) * count);
if (!rq->glyphs)
{ {
*length = 0; *length = 0;
return NULL; return NULL;
} }
rq->glyphs = new_mem;
rq->glyphs_capacity = count;
}
*length = count;
RAQM_TEST ("Glyph information:\n"); RAQM_TEST ("Glyph information:\n");
count = 0; count = 0;
@ -1005,7 +1127,7 @@ raqm_get_glyphs (raqm_t *rq,
count += len; count += len;
} }
if (rq->flags & RAQM_FLAG_UTF8) if (rq->text_utf8)
{ {
#ifdef RAQM_TESTING #ifdef RAQM_TESTING
RAQM_TEST ("\nUTF-32 clusters:"); RAQM_TEST ("\nUTF-32 clusters:");
@ -1276,24 +1398,14 @@ _raqm_bidi_itemize (raqm_t *rq, size_t *run_count)
FriBidiCharType *types; FriBidiCharType *types;
_raqm_bidi_level_t *levels; _raqm_bidi_level_t *levels;
int max_level = 0; int max_level = 0;
#ifdef USE_FRIBIDI_EX_API
FriBidiBracketType *btypes; FriBidiBracketType *btypes;
#endif
types = calloc (rq->text_len, sizeof (FriBidiCharType)); types = calloc (rq->text_len, sizeof (FriBidiCharType));
#ifdef USE_FRIBIDI_EX_API
btypes = calloc (rq->text_len, sizeof (FriBidiBracketType)); btypes = calloc (rq->text_len, sizeof (FriBidiBracketType));
#endif
levels = calloc (rq->text_len, sizeof (_raqm_bidi_level_t)); levels = calloc (rq->text_len, sizeof (_raqm_bidi_level_t));
if (!types || !levels if (!types || !levels || !btypes)
#ifdef USE_FRIBIDI_EX_API
|| !btypes
#endif
)
{
goto done; goto done;
}
if (rq->base_dir == RAQM_DIRECTION_RTL) if (rq->base_dir == RAQM_DIRECTION_RTL)
par_type = FRIBIDI_PAR_RTL; par_type = FRIBIDI_PAR_RTL;
@ -1301,15 +1413,10 @@ _raqm_bidi_itemize (raqm_t *rq, size_t *run_count)
par_type = FRIBIDI_PAR_LTR; par_type = FRIBIDI_PAR_LTR;
fribidi_get_bidi_types (rq->text, rq->text_len, types); fribidi_get_bidi_types (rq->text, rq->text_len, types);
#ifdef USE_FRIBIDI_EX_API
fribidi_get_bracket_types (rq->text, rq->text_len, types, btypes); fribidi_get_bracket_types (rq->text, rq->text_len, types, btypes);
max_level = fribidi_get_par_embedding_levels_ex (types, btypes, max_level = fribidi_get_par_embedding_levels_ex (types, btypes,
rq->text_len, &par_type, rq->text_len, &par_type,
levels); levels);
#else
max_level = fribidi_get_par_embedding_levels (types, rq->text_len,
&par_type, levels);
#endif
if (par_type == FRIBIDI_PAR_LTR) if (par_type == FRIBIDI_PAR_LTR)
rq->resolved_dir = RAQM_DIRECTION_LTR; rq->resolved_dir = RAQM_DIRECTION_LTR;
@ -1325,9 +1432,7 @@ _raqm_bidi_itemize (raqm_t *rq, size_t *run_count)
done: done:
free (types); free (types);
free (levels); free (levels);
#ifdef USE_FRIBIDI_EX_API
free (btypes); free (btypes);
#endif
return runs; return runs;
} }
@ -1403,7 +1508,7 @@ _raqm_itemize (raqm_t *rq)
last = NULL; last = NULL;
for (size_t i = 0; i < run_count; i++) for (size_t i = 0; i < run_count; i++)
{ {
raqm_run_t *run = calloc (1, sizeof (raqm_run_t)); raqm_run_t *run = _raqm_alloc_run (rq);
if (!run) if (!run)
{ {
ok = false; ok = false;
@ -1422,13 +1527,14 @@ _raqm_itemize (raqm_t *rq)
{ {
run->pos = runs[i].pos + runs[i].len - 1; run->pos = runs[i].pos + runs[i].len - 1;
run->script = rq->text_info[run->pos].script; run->script = rq->text_info[run->pos].script;
run->font = _raqm_create_hb_font (rq, rq->text_info[run->pos].ftface); run->font = _raqm_create_hb_font (rq, rq->text_info[run->pos].ftface,
rq->text_info[run->pos].ftloadflags);
for (int j = runs[i].len - 1; j >= 0; j--) for (int j = runs[i].len - 1; j >= 0; j--)
{ {
_raqm_text_info info = rq->text_info[runs[i].pos + j]; _raqm_text_info info = rq->text_info[runs[i].pos + j];
if (!_raqm_compare_text_info (rq->text_info[run->pos], info)) if (!_raqm_compare_text_info (rq->text_info[run->pos], info))
{ {
raqm_run_t *newrun = calloc (1, sizeof (raqm_run_t)); raqm_run_t *newrun = _raqm_alloc_run (rq);
if (!newrun) if (!newrun)
{ {
ok = false; ok = false;
@ -1438,7 +1544,8 @@ _raqm_itemize (raqm_t *rq)
newrun->len = 1; newrun->len = 1;
newrun->direction = _raqm_hb_dir (rq, runs[i].level); newrun->direction = _raqm_hb_dir (rq, runs[i].level);
newrun->script = info.script; newrun->script = info.script;
newrun->font = _raqm_create_hb_font (rq, info.ftface); newrun->font = _raqm_create_hb_font (rq, info.ftface,
info.ftloadflags);
run->next = newrun; run->next = newrun;
run = newrun; run = newrun;
} }
@ -1453,13 +1560,14 @@ _raqm_itemize (raqm_t *rq)
{ {
run->pos = runs[i].pos; run->pos = runs[i].pos;
run->script = rq->text_info[run->pos].script; run->script = rq->text_info[run->pos].script;
run->font = _raqm_create_hb_font (rq, rq->text_info[run->pos].ftface); run->font = _raqm_create_hb_font (rq, rq->text_info[run->pos].ftface,
rq->text_info[run->pos].ftloadflags);
for (size_t j = 0; j < runs[i].len; j++) for (size_t j = 0; j < runs[i].len; j++)
{ {
_raqm_text_info info = rq->text_info[runs[i].pos + j]; _raqm_text_info info = rq->text_info[runs[i].pos + j];
if (!_raqm_compare_text_info (rq->text_info[run->pos], info)) if (!_raqm_compare_text_info (rq->text_info[run->pos], info))
{ {
raqm_run_t *newrun = calloc (1, sizeof (raqm_run_t)); raqm_run_t *newrun = _raqm_alloc_run (rq);
if (!newrun) if (!newrun)
{ {
ok = false; ok = false;
@ -1469,7 +1577,8 @@ _raqm_itemize (raqm_t *rq)
newrun->len = 1; newrun->len = 1;
newrun->direction = _raqm_hb_dir (rq, runs[i].level); newrun->direction = _raqm_hb_dir (rq, runs[i].level);
newrun->script = info.script; newrun->script = info.script;
newrun->font = _raqm_create_hb_font (rq, info.ftface); newrun->font = _raqm_create_hb_font (rq, info.ftface,
info.ftloadflags);
run->next = newrun; run->next = newrun;
run = newrun; run = newrun;
} }
@ -1758,7 +1867,6 @@ _raqm_resolve_scripts (raqm_t *rq)
return true; return true;
} }
#ifdef HAVE_FT_GET_TRANSFORM
static void static void
_raqm_ft_transform (int *x, _raqm_ft_transform (int *x,
int *y, int *y,
@ -1773,21 +1881,18 @@ _raqm_ft_transform (int *x,
*x = vector.x; *x = vector.x;
*y = vector.y; *y = vector.y;
} }
#endif
static bool static bool
_raqm_shape (raqm_t *rq) _raqm_shape (raqm_t *rq)
{ {
hb_buffer_flags_t hb_buffer_flags = HB_BUFFER_FLAG_BOT | HB_BUFFER_FLAG_EOT; hb_buffer_flags_t hb_buffer_flags = HB_BUFFER_FLAG_BOT | HB_BUFFER_FLAG_EOT;
#if defined(HAVE_DECL_HB_BUFFER_FLAG_REMOVE_DEFAULT_IGNORABLES) && \
HAVE_DECL_HB_BUFFER_FLAG_REMOVE_DEFAULT_IGNORABLES
if (rq->invisible_glyph < 0) if (rq->invisible_glyph < 0)
hb_buffer_flags |= HB_BUFFER_FLAG_REMOVE_DEFAULT_IGNORABLES; hb_buffer_flags |= HB_BUFFER_FLAG_REMOVE_DEFAULT_IGNORABLES;
#endif
for (raqm_run_t *run = rq->runs; run != NULL; run = run->next) for (raqm_run_t *run = rq->runs; run != NULL; run = run->next)
{ {
if (!run->buffer)
run->buffer = hb_buffer_create (); run->buffer = hb_buffer_create ();
hb_buffer_add_utf32 (run->buffer, rq->text, rq->text_len, hb_buffer_add_utf32 (run->buffer, rq->text, rq->text_len,
@ -1797,15 +1902,12 @@ _raqm_shape (raqm_t *rq)
hb_buffer_set_direction (run->buffer, run->direction); hb_buffer_set_direction (run->buffer, run->direction);
hb_buffer_set_flags (run->buffer, hb_buffer_flags); hb_buffer_set_flags (run->buffer, hb_buffer_flags);
#ifdef HAVE_HB_BUFFER_SET_INVISIBLE_GLYPH
if (rq->invisible_glyph > 0) if (rq->invisible_glyph > 0)
hb_buffer_set_invisible_glyph (run->buffer, rq->invisible_glyph); hb_buffer_set_invisible_glyph (run->buffer, rq->invisible_glyph);
#endif
hb_shape_full (run->font, run->buffer, rq->features, rq->features_len, hb_shape_full (run->font, run->buffer, rq->features, rq->features_len,
NULL); NULL);
#ifdef HAVE_FT_GET_TRANSFORM
{ {
FT_Matrix matrix; FT_Matrix matrix;
hb_glyph_position_t *pos; hb_glyph_position_t *pos;
@ -1819,7 +1921,6 @@ _raqm_shape (raqm_t *rq)
_raqm_ft_transform (&pos[i].x_offset, &pos[i].y_offset, matrix); _raqm_ft_transform (&pos[i].x_offset, &pos[i].y_offset, matrix);
} }
} }
#endif
} }
return true; return true;
@ -1917,7 +2018,7 @@ raqm_index_to_position (raqm_t *rq,
if (rq == NULL) if (rq == NULL)
return false; return false;
if (rq->flags & RAQM_FLAG_UTF8) if (rq->text_utf8)
*index = _raqm_u8_to_u32_index (rq, *index); *index = _raqm_u8_to_u32_index (rq, *index);
if (*index >= rq->text_len) if (*index >= rq->text_len)
@ -1974,7 +2075,7 @@ raqm_index_to_position (raqm_t *rq,
} }
found: found:
if (rq->flags & RAQM_FLAG_UTF8) if (rq->text_utf8)
*index = _raqm_u32_to_u8_index (rq, *index); *index = _raqm_u32_to_u8_index (rq, *index);
RAQM_TEST ("The position is %d at index %zu\n",*x ,*index); RAQM_TEST ("The position is %d at index %zu\n",*x ,*index);
return true; return true;

View File

@ -1,6 +1,6 @@
/* /*
* Copyright © 2015 Information Technology Authority (ITA) <foss@ita.gov.om> * Copyright © 2015 Information Technology Authority (ITA) <foss@ita.gov.om>
* Copyright © 2016-2021 Khaled Hosny <khaled@aliftype.com> * Copyright © 2016-2022 Khaled Hosny <khaled@aliftype.com>
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy * Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to * of this software and associated documentation files (the "Software"), to
@ -106,6 +106,9 @@ raqm_reference (raqm_t *rq);
RAQM_API void RAQM_API void
raqm_destroy (raqm_t *rq); raqm_destroy (raqm_t *rq);
RAQM_API void
raqm_clear_contents (raqm_t *rq);
RAQM_API bool RAQM_API bool
raqm_set_text (raqm_t *rq, raqm_set_text (raqm_t *rq,
const uint32_t *text, const uint32_t *text,
@ -145,6 +148,12 @@ RAQM_API bool
raqm_set_freetype_load_flags (raqm_t *rq, raqm_set_freetype_load_flags (raqm_t *rq,
int flags); int flags);
RAQM_API bool
raqm_set_freetype_load_flags_range (raqm_t *rq,
int flags,
size_t start,
size_t len);
RAQM_API bool RAQM_API bool
raqm_set_invisible_glyph (raqm_t *rq, raqm_set_invisible_glyph (raqm_t *rq,
int gid); int gid);

View File

@ -24,7 +24,7 @@ Download and install:
* `CMake 3.12 or newer <https://cmake.org/download/>`_ * `CMake 3.12 or newer <https://cmake.org/download/>`_
(also available as Visual Studio component C++ CMake tools for Windows) (also available as Visual Studio component C++ CMake tools for Windows)
* `NASM <https://www.nasm.us/pub/nasm/releasebuilds/?C=M;O=D>`_ * x86/x64: `NASM <https://www.nasm.us/pub/nasm/releasebuilds/?C=M;O=D>`_
Any version of Visual Studio 2017 or newer should be supported, Any version of Visual Studio 2017 or newer should be supported,
including Visual Studio 2017 Community, or Build Tools for Visual Studio 2019. including Visual Studio 2017 Community, or Build Tools for Visual Studio 2019.
@ -42,8 +42,8 @@ behaviour of ``build_prepare.py``:
If ``PYTHON`` is unset, the version of Python used to run If ``PYTHON`` is unset, the version of Python used to run
``build_prepare.py`` will be used. If only ``PYTHON`` is set, ``build_prepare.py`` will be used. If only ``PYTHON`` is set,
``EXECUTABLE`` defaults to ``python.exe``. ``EXECUTABLE`` defaults to ``python.exe``.
* ``ARCHITECTURE`` is used to select a ``x86`` or ``x64`` build. By default, * ``ARCHITECTURE`` is used to select a ``x86``, ``x64`` or ``ARM64``build.
uses same architecture as the version of Python used to run ``build_prepare.py``. By default, uses same architecture as the version of Python used to run ``build_prepare.py``.
is used. is used.
* ``PILLOW_BUILD`` can be used to override the ``winbuild\build`` directory * ``PILLOW_BUILD`` can be used to override the ``winbuild\build`` directory
path, used to store generated build scripts and compiled libraries. path, used to store generated build scripts and compiled libraries.

View File

@ -1,4 +1,5 @@
import os import os
import platform
import shutil import shutil
import struct import struct
import subprocess import subprocess
@ -93,6 +94,7 @@ SF_MIRROR = "http://iweb.dl.sourceforge.net"
architectures = { architectures = {
"x86": {"vcvars_arch": "x86", "msbuild_arch": "Win32"}, "x86": {"vcvars_arch": "x86", "msbuild_arch": "Win32"},
"x64": {"vcvars_arch": "x86_amd64", "msbuild_arch": "x64"}, "x64": {"vcvars_arch": "x86_amd64", "msbuild_arch": "x64"},
"ARM64": {"vcvars_arch": "x86_arm64", "msbuild_arch": "ARM64"},
} }
header = [ header = [
@ -219,25 +221,25 @@ deps = {
# "bins": [r"objs\{msbuild_arch}\Release\freetype.dll"], # "bins": [r"objs\{msbuild_arch}\Release\freetype.dll"],
}, },
"lcms2": { "lcms2": {
"url": SF_MIRROR + "/project/lcms/lcms/2.12/lcms2-2.12.tar.gz", "url": SF_MIRROR + "/project/lcms/lcms/2.13/lcms2-2.13.tar.gz",
"filename": "lcms2-2.12.tar.gz", "filename": "lcms2-2.13.tar.gz",
"dir": "lcms2-2.12", "dir": "lcms2-2.13",
"patch": { "patch": {
r"Projects\VC2017\lcms2_static\lcms2_static.vcxproj": { r"Projects\VC2019\lcms2_static\lcms2_static.vcxproj": {
# default is /MD for x86 and /MT for x64, we need /MD always # default is /MD for x86 and /MT for x64, we need /MD always
"<RuntimeLibrary>MultiThreaded</RuntimeLibrary>": "<RuntimeLibrary>MultiThreadedDLL</RuntimeLibrary>", # noqa: E501 "<RuntimeLibrary>MultiThreaded</RuntimeLibrary>": "<RuntimeLibrary>MultiThreadedDLL</RuntimeLibrary>", # noqa: E501
# retarget to default toolset (selected by vcvarsall.bat) # retarget to default toolset (selected by vcvarsall.bat)
"<PlatformToolset>v141</PlatformToolset>": "<PlatformToolset>$(DefaultPlatformToolset)</PlatformToolset>", # noqa: E501 "<PlatformToolset>v142</PlatformToolset>": "<PlatformToolset>$(DefaultPlatformToolset)</PlatformToolset>", # noqa: E501
# retarget to latest (selected by vcvarsall.bat) # retarget to latest (selected by vcvarsall.bat)
"<WindowsTargetPlatformVersion>10.0.17134.0</WindowsTargetPlatformVersion>": "<WindowsTargetPlatformVersion>$(WindowsSDKVersion)</WindowsTargetPlatformVersion>", # noqa: E501 "<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>": "<WindowsTargetPlatformVersion>$(WindowsSDKVersion)</WindowsTargetPlatformVersion>", # noqa: E501
} }
}, },
"build": [ "build": [
cmd_rmdir("Lib"), cmd_rmdir("Lib"),
cmd_rmdir(r"Projects\VC2017\Release"), cmd_rmdir(r"Projects\VC2019\Release"),
cmd_msbuild(r"Projects\VC2017\lcms2.sln", "Release", "Clean"), cmd_msbuild(r"Projects\VC2019\lcms2.sln", "Release", "Clean"),
cmd_msbuild( cmd_msbuild(
r"Projects\VC2017\lcms2.sln", "Release", "lcms2_static:Rebuild" r"Projects\VC2019\lcms2.sln", "Release", "lcms2_static:Rebuild"
), ),
cmd_xcopy("include", "{inc_dir}"), cmd_xcopy("include", "{inc_dir}"),
], ],
@ -278,9 +280,9 @@ deps = {
"libs": [r"imagequant.lib"], "libs": [r"imagequant.lib"],
}, },
"harfbuzz": { "harfbuzz": {
"url": "https://github.com/harfbuzz/harfbuzz/archive/3.2.0.zip", "url": "https://github.com/harfbuzz/harfbuzz/archive/3.3.1.zip",
"filename": "harfbuzz-3.2.0.zip", "filename": "harfbuzz-3.3.1.zip",
"dir": "harfbuzz-3.2.0", "dir": "harfbuzz-3.3.1",
"build": [ "build": [
cmd_cmake("-DHB_HAVE_FREETYPE:BOOL=TRUE"), cmd_cmake("-DHB_HAVE_FREETYPE:BOOL=TRUE"),
cmd_nmake(target="clean"), cmd_nmake(target="clean"),
@ -490,7 +492,10 @@ if __name__ == "__main__":
python_dir = os.environ.get("PYTHON") python_dir = os.environ.get("PYTHON")
python_exe = os.environ.get("EXECUTABLE", "python.exe") python_exe = os.environ.get("EXECUTABLE", "python.exe")
architecture = os.environ.get( architecture = os.environ.get(
"ARCHITECTURE", "x86" if struct.calcsize("P") == 4 else "x64" "ARCHITECTURE",
"ARM64"
if platform.machine() == "ARM64"
else ("x86" if struct.calcsize("P") == 4 else "x64"),
) )
build_dir = os.environ.get("PILLOW_BUILD", os.path.join(winbuild_dir, "build")) build_dir = os.environ.get("PILLOW_BUILD", os.path.join(winbuild_dir, "build"))
sources_dir = "" sources_dir = ""