From a90075a66803b30cd02c0607d47c0d912913b6aa Mon Sep 17 00:00:00 2001 From: Frank Henigman Date: Wed, 4 Feb 2026 20:52:52 -0500 Subject: [PATCH 01/10] Add FontFile.to_imagefont(). --- Tests/test_font_pcf.py | 10 ++++++++++ src/PIL/FontFile.py | 42 ++++++++++++++++++++++++++++++++---------- 2 files changed, 42 insertions(+), 10 deletions(-) diff --git a/Tests/test_font_pcf.py b/Tests/test_font_pcf.py index 1be7a4d1e39..5b8832977db 100644 --- a/Tests/test_font_pcf.py +++ b/Tests/test_font_pcf.py @@ -76,6 +76,16 @@ def test_draw(request: pytest.FixtureRequest, tmp_path: Path) -> None: assert_image_similar_tofile(im, "Tests/images/test_draw_pbm_target.png", 0) +def test_to_imagefont(request: pytest.FixtureRequest, tmp_path: Path) -> None: + with open(fontname, "rb") as test_file: + pcffont = PcfFontFile.PcfFontFile(test_file) + imgfont = pcffont.to_imagefont() + im = Image.new("L", (130, 30), "white") + draw = ImageDraw.Draw(im) + draw.text((0, 0), message, "black", font=imgfont) + assert_image_similar_tofile(im, "Tests/images/test_draw_pbm_target.png", 0) + + def test_textsize(request: pytest.FixtureRequest, tmp_path: Path) -> None: tempname = save_font(request, tmp_path) font = ImageFont.load(tempname) diff --git a/src/PIL/FontFile.py b/src/PIL/FontFile.py index 1e0c1c166b5..b50385daac6 100644 --- a/src/PIL/FontFile.py +++ b/src/PIL/FontFile.py @@ -15,10 +15,11 @@ # from __future__ import annotations +import io import os from typing import BinaryIO -from . import Image, _binary +from . import Image, ImageFont, _binary WIDTH = 800 @@ -123,12 +124,33 @@ def save(self, filename: str) -> None: # font metrics with open(os.path.splitext(filename)[0] + ".pil", "wb") as fp: - fp.write(b"PILfont\n") - fp.write(f";;;;;;{self.ysize};\n".encode("ascii")) # HACK!!! - fp.write(b"DATA\n") - for id in range(256): - m = self.metrics[id] - if not m: - puti16(fp, (0,) * 10) - else: - puti16(fp, m[0] + m[1] + m[2]) + self.save_metrics(fp) + + def save_metrics(self, fp: BinaryIO) -> None: + """Save font metrics to a file-like object""" + fp.write(b"PILfont\n") + fp.write(f";;;;;;{self.ysize};\n".encode("ascii")) # HACK!!! + fp.write(b"DATA\n") + for id in range(256): + m = self.metrics[id] + if not m: + puti16(fp, (0,) * 10) + else: + puti16(fp, m[0] + m[1] + m[2]) + + def to_imagefont(self) -> ImageFont.ImageFont: + """Convert to ImageFont""" + + self.compile() + + # font data + if not self.bitmap: + msg = "No bitmap created" + raise ValueError(msg) + + buf = io.BytesIO() + self.save_metrics(buf) + buf.seek(0) + imgfont = ImageFont.ImageFont() + imgfont._load_pilfont_data(buf, self.bitmap) + return imgfont From 3e14bea593449d76c2b3e6e63aafa26e3c0aedcb Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Mon, 9 Feb 2026 22:18:01 +1100 Subject: [PATCH 02/10] Use `assert_image_equal_tofile` when similarity is zero --- Tests/test_font_pcf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/test_font_pcf.py b/Tests/test_font_pcf.py index ce5b36413cf..01d56dfd3e5 100644 --- a/Tests/test_font_pcf.py +++ b/Tests/test_font_pcf.py @@ -82,7 +82,7 @@ def test_to_imagefont(request: pytest.FixtureRequest, tmp_path: Path) -> None: im = Image.new("L", (130, 30), "white") draw = ImageDraw.Draw(im) draw.text((0, 0), message, "black", font=imgfont) - assert_image_similar_tofile(im, "Tests/images/test_draw_pbm_target.png", 0) + assert_image_equal_tofile(im, "Tests/images/test_draw_pbm_target.png") def test_textsize(request: pytest.FixtureRequest, tmp_path: Path) -> None: From 0604d6a2c9cd6ac0c86baeda436abed06638d374 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 6 Feb 2026 21:12:31 +1100 Subject: [PATCH 03/10] Remove unused argument --- Tests/test_font_pcf.py | 6 +++--- src/PIL/FontFile.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Tests/test_font_pcf.py b/Tests/test_font_pcf.py index 01d56dfd3e5..8ac63ea61ae 100644 --- a/Tests/test_font_pcf.py +++ b/Tests/test_font_pcf.py @@ -75,13 +75,13 @@ def test_draw(request: pytest.FixtureRequest, tmp_path: Path) -> None: assert_image_equal_tofile(im, "Tests/images/test_draw_pbm_target.png") -def test_to_imagefont(request: pytest.FixtureRequest, tmp_path: Path) -> None: +def test_to_imagefont(tmp_path: Path) -> None: with open(fontname, "rb") as test_file: pcffont = PcfFontFile.PcfFontFile(test_file) - imgfont = pcffont.to_imagefont() + imagefont = pcffont.to_imagefont() im = Image.new("L", (130, 30), "white") draw = ImageDraw.Draw(im) - draw.text((0, 0), message, "black", font=imgfont) + draw.text((0, 0), message, "black", font=imagefont) assert_image_equal_tofile(im, "Tests/images/test_draw_pbm_target.png") diff --git a/src/PIL/FontFile.py b/src/PIL/FontFile.py index b50385daac6..71a08b05e71 100644 --- a/src/PIL/FontFile.py +++ b/src/PIL/FontFile.py @@ -151,6 +151,6 @@ def to_imagefont(self) -> ImageFont.ImageFont: buf = io.BytesIO() self.save_metrics(buf) buf.seek(0) - imgfont = ImageFont.ImageFont() - imgfont._load_pilfont_data(buf, self.bitmap) - return imgfont + imagefont = ImageFont.ImageFont() + imagefont._load_pilfont_data(buf, self.bitmap) + return imagefont From 612e3c24a4f38837a6d915fe8eac15a7d1eeca17 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 6 Feb 2026 21:39:39 +1100 Subject: [PATCH 04/10] Remove temporary buffer --- src/PIL/FontFile.py | 39 +++++++++++++++++++++------------------ src/PIL/ImageFont.py | 3 +++ 2 files changed, 24 insertions(+), 18 deletions(-) diff --git a/src/PIL/FontFile.py b/src/PIL/FontFile.py index 71a08b05e71..c0c64fe6842 100644 --- a/src/PIL/FontFile.py +++ b/src/PIL/FontFile.py @@ -15,7 +15,6 @@ # from __future__ import annotations -import io import os from typing import BinaryIO @@ -111,6 +110,22 @@ def compile(self) -> None: self.bitmap.paste(im.crop(src), s) self.metrics[i] = d, dst, s + def _encode_metrics(self) -> bytes: + values: tuple[int, ...] = () + for id in range(256): + m = self.metrics[id] + if m: + values += m[0] + m[1] + m[2] + else: + values += (0,) * 10 + + metrics = b"" + for v in values: + if v < 0: + v += 65536 + metrics += _binary.o16be(v) + return metrics + def save(self, filename: str) -> None: """Save font""" @@ -124,19 +139,10 @@ def save(self, filename: str) -> None: # font metrics with open(os.path.splitext(filename)[0] + ".pil", "wb") as fp: - self.save_metrics(fp) - - def save_metrics(self, fp: BinaryIO) -> None: - """Save font metrics to a file-like object""" - fp.write(b"PILfont\n") - fp.write(f";;;;;;{self.ysize};\n".encode("ascii")) # HACK!!! - fp.write(b"DATA\n") - for id in range(256): - m = self.metrics[id] - if not m: - puti16(fp, (0,) * 10) - else: - puti16(fp, m[0] + m[1] + m[2]) + fp.write(b"PILfont\n") + fp.write(f";;;;;;{self.ysize};\n".encode("ascii")) # HACK!!! + fp.write(b"DATA\n") + fp.write(self._encode_metrics()) def to_imagefont(self) -> ImageFont.ImageFont: """Convert to ImageFont""" @@ -148,9 +154,6 @@ def to_imagefont(self) -> ImageFont.ImageFont: msg = "No bitmap created" raise ValueError(msg) - buf = io.BytesIO() - self.save_metrics(buf) - buf.seek(0) imagefont = ImageFont.ImageFont() - imagefont._load_pilfont_data(buf, self.bitmap) + imagefont._load(self.bitmap, self._encode_metrics()) return imagefont diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index ae003d139c9..ea7f4dc5477 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -149,6 +149,9 @@ def _load_pilfont_data(self, file: IO[bytes], image: Image.Image) -> None: # read PILfont metrics data = file.read(256 * 20) + self._load(image, data) + + def _load(self, image: Image.Image, data: bytes) -> None: image.load() self.font = Image.core.font(image.im, data) From 723e7648267b5205b349d3d5a482202e0372fed5 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 6 Feb 2026 21:27:22 +1100 Subject: [PATCH 05/10] Improved coverage --- Tests/test_fontfile.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/Tests/test_fontfile.py b/Tests/test_fontfile.py index 575dada86cb..1a9069fd892 100644 --- a/Tests/test_fontfile.py +++ b/Tests/test_fontfile.py @@ -1,5 +1,6 @@ from __future__ import annotations +from io import BytesIO from pathlib import Path import pytest @@ -7,6 +8,15 @@ from PIL import FontFile, Image +def test_puti16() -> None: + fp = BytesIO() + FontFile.puti16(fp, (0, 1, 2, 3, 4, 5, 6, 7, 8, 9)) + assert fp.getvalue() == ( + b"\x00\x00\x00\x01\x00\x02\x00\x03\x00\x04" + b"\x00\x05\x00\x06\x00\x07\x00\x08\x00\t" + ) + + def test_compile() -> None: font = FontFile.FontFile() font.glyph[0] = ((0, 0), (0, 0, 0, 0), (0, 0, 0, 1), Image.new("L", (0, 0))) @@ -24,5 +34,11 @@ def test_save(tmp_path: Path) -> None: tempname = str(tmp_path / "temp.pil") font = FontFile.FontFile() - with pytest.raises(ValueError): + with pytest.raises(ValueError, match="No bitmap created"): font.save(tempname) + + +def test_to_imagefont() -> None: + font = FontFile.FontFile() + with pytest.raises(ValueError, match="No bitmap created"): + font.to_imagefont() From 0ce21f98e7aab659e858475e728bb1cf98587f8a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 13 Feb 2026 18:06:29 +1100 Subject: [PATCH 06/10] Updated documentation --- docs/reference/ImageFont.rst | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/docs/reference/ImageFont.rst b/docs/reference/ImageFont.rst index aac55fe6b05..d4d66988b0c 100644 --- a/docs/reference/ImageFont.rst +++ b/docs/reference/ImageFont.rst @@ -8,10 +8,14 @@ The :py:mod:`~PIL.ImageFont` module defines a class with the same name. Instance this class store bitmap fonts, and are used with the :py:meth:`PIL.ImageDraw.ImageDraw.text` method. -PIL uses its own font file format to store bitmap fonts, limited to 256 characters. You can use -`pilfont.py `_ -from :pypi:`pillow-scripts` to convert BDF and -PCF font descriptors (X window font formats) to this format. +PIL uses its own font file format to store bitmap fonts, limited to 256 characters. You +can use :py:meth:`~PIL.FontFile.FontFile.to_imagefont` to convert BDF and PCF font +descriptors (X window font formats) to this format:: + + from PIL import PcfFontFile + with open("Tests/fonts/10x20-ISO8859-1.pcf", "rb") as fp: + font = PcfFontFile.PcfFontFile(fp) + imagefont = font.to_imagefont() Starting with version 1.1.4, PIL can be configured to support TrueType and OpenType fonts (as well as other font formats supported by the FreeType From 04470d5151069736f136210d3170bdab402baa47 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Tue, 3 Mar 2026 22:51:41 +1100 Subject: [PATCH 07/10] Removed unused argument Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- Tests/test_font_pcf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/test_font_pcf.py b/Tests/test_font_pcf.py index 8ac63ea61ae..321dd85603a 100644 --- a/Tests/test_font_pcf.py +++ b/Tests/test_font_pcf.py @@ -75,7 +75,7 @@ def test_draw(request: pytest.FixtureRequest, tmp_path: Path) -> None: assert_image_equal_tofile(im, "Tests/images/test_draw_pbm_target.png") -def test_to_imagefont(tmp_path: Path) -> None: +def test_to_imagefont() -> None: with open(fontname, "rb") as test_file: pcffont = PcfFontFile.PcfFontFile(test_file) imagefont = pcffont.to_imagefont() From f7ee26575ec7915c5d5d6b4b76bf1b3186383e1f Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Tue, 3 Mar 2026 22:57:34 +1100 Subject: [PATCH 08/10] Avoid shadowing built-in Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- src/PIL/FontFile.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PIL/FontFile.py b/src/PIL/FontFile.py index c0c64fe6842..9e12a8bfede 100644 --- a/src/PIL/FontFile.py +++ b/src/PIL/FontFile.py @@ -112,8 +112,8 @@ def compile(self) -> None: def _encode_metrics(self) -> bytes: values: tuple[int, ...] = () - for id in range(256): - m = self.metrics[id] + for i in range(256): + m = self.metrics[i] if m: values += m[0] + m[1] + m[2] else: From f7582b8d5828a52a4276c22dc05ab8a4823d1748 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Tue, 3 Mar 2026 23:04:00 +1100 Subject: [PATCH 09/10] Updated documentation terms Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- docs/reference/ImageFont.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/reference/ImageFont.rst b/docs/reference/ImageFont.rst index d4d66988b0c..920a05e65e9 100644 --- a/docs/reference/ImageFont.rst +++ b/docs/reference/ImageFont.rst @@ -8,9 +8,9 @@ The :py:mod:`~PIL.ImageFont` module defines a class with the same name. Instance this class store bitmap fonts, and are used with the :py:meth:`PIL.ImageDraw.ImageDraw.text` method. -PIL uses its own font file format to store bitmap fonts, limited to 256 characters. You +Pillow uses its own font file format to store bitmap fonts, limited to 256 characters. You can use :py:meth:`~PIL.FontFile.FontFile.to_imagefont` to convert BDF and PCF font -descriptors (X window font formats) to this format:: +descriptors (X Window font formats) to this format:: from PIL import PcfFontFile with open("Tests/fonts/10x20-ISO8859-1.pcf", "rb") as fp: From abbd515e9bf0f96c1f220d98742b581ce4a7d8d6 Mon Sep 17 00:00:00 2001 From: Frank Henigman Date: Fri, 6 Mar 2026 22:20:21 -0500 Subject: [PATCH 10/10] Improve efficiency of FontFile._encode_metrics() Build up mutable sequences instead of recreating mutable ones. --- src/PIL/FontFile.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/PIL/FontFile.py b/src/PIL/FontFile.py index 9e12a8bfede..341431d3f45 100644 --- a/src/PIL/FontFile.py +++ b/src/PIL/FontFile.py @@ -111,20 +111,20 @@ def compile(self) -> None: self.metrics[i] = d, dst, s def _encode_metrics(self) -> bytes: - values: tuple[int, ...] = () + values: list[int] = [] for i in range(256): m = self.metrics[i] if m: - values += m[0] + m[1] + m[2] + values.extend(m[0] + m[1] + m[2]) else: - values += (0,) * 10 + values.extend((0,) * 10) - metrics = b"" + data = bytearray() for v in values: if v < 0: v += 65536 - metrics += _binary.o16be(v) - return metrics + data += _binary.o16be(v) + return bytes(data) def save(self, filename: str) -> None: """Save font"""