From b9a69a168cf5b2576d6987724c4ae36af7dbe3f8 Mon Sep 17 00:00:00 2001 From: Just van Rossum Date: Mon, 18 Nov 2024 12:20:01 +0100 Subject: [PATCH 1/4] Add pillow as dependency --- pyproject.toml | 1 + requirements.txt | 1 + 2 files changed, 2 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 681fc6a62e..85dd3ddb9c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,6 +22,7 @@ dependencies = [ "ufomerge>=1.8.0", "unicodedata2>=15.1.0", "skia-pathops>=0.8.0.post1", + "pillow>=11.0.0", ] dynamic = ["version"] requires-python = ">=3.10" diff --git a/requirements.txt b/requirements.txt index 3523cc3fff..4ef58bc1b3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,6 +2,7 @@ aiohttp==3.11.2 cattrs==24.1.2 fonttools[ufo,unicode]==4.55.0 glyphsLib==6.9.5 +pillow==11.0.0 pyyaml==6.0.2 ufomerge==1.8.2 unicodedata2==15.1.0 From 919f290da2a2b3f6e7fab7c04857a2a916eb16c5 Mon Sep 17 00:00:00 2001 From: Just van Rossum Date: Mon, 18 Nov 2024 13:25:58 +0100 Subject: [PATCH 2/4] Convert image to PNG when writing bg image to UFO --- src/fontra/backends/designspace.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/fontra/backends/designspace.py b/src/fontra/backends/designspace.py index 6043136840..7396e402a4 100644 --- a/src/fontra/backends/designspace.py +++ b/src/fontra/backends/designspace.py @@ -1188,7 +1188,7 @@ async def getBackgroundImage(self, imageIdentifier: str) -> ImageData | None: async def putBackgroundImage(self, imageIdentifier: str, data: ImageData) -> None: if data.type != ImageType.PNG: - raise NotImplementedError("convert image to PNG") + data = convertImageData(data, ImageType.PNG) imageInfo = self._imageMapping.reverse.get(imageIdentifier) if imageInfo is None: @@ -2240,3 +2240,14 @@ def mergeKernGroups( mergedGroups[groupName] = gA + [n for n in gB if n not in gASet] return mergedGroups + + +def convertImageData(data, type): + import io + + from PIL import Image + + image = Image.open(io.BytesIO(data.data)) + outFile = io.BytesIO() + image.save(outFile, type) + return ImageData(type=type, data=outFile.getvalue()) From 72237eb124bc3358f25d9097d23b1242a6ebd3fa Mon Sep 17 00:00:00 2001 From: Just van Rossum Date: Mon, 18 Nov 2024 13:46:16 +0100 Subject: [PATCH 3/4] For our test case, allow RGBA to be downgraded to RGB when writing JPEG --- src/fontra/backends/designspace.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/fontra/backends/designspace.py b/src/fontra/backends/designspace.py index 7396e402a4..497d0a4e7b 100644 --- a/src/fontra/backends/designspace.py +++ b/src/fontra/backends/designspace.py @@ -2248,6 +2248,13 @@ def convertImageData(data, type): from PIL import Image image = Image.open(io.BytesIO(data.data)) + if image.mode == "RGBA" and type == ImageType.JPEG: + # from https://stackoverflow.com/questions/9166400/convert-rgba-png-to-rgb-with-pil + image.load() # required for image.split() + imageJPEG = Image.new("RGB", image.size, (255, 255, 255)) + imageJPEG.paste(image, mask=image.split()[3]) # 3 is the alpha channel + image = imageJPEG + outFile = io.BytesIO() image.save(outFile, type) return ImageData(type=type, data=outFile.getvalue()) From d7b98df18a5ff514f8c191343c52ac5c10588482 Mon Sep 17 00:00:00 2001 From: Just van Rossum Date: Mon, 18 Nov 2024 13:46:31 +0100 Subject: [PATCH 4/4] Add test case for converting PNG to JPEG --- test-py/test_backends_designspace.py | 31 +++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/test-py/test_backends_designspace.py b/test-py/test_backends_designspace.py index 8265e28479..ac3c5fcef4 100644 --- a/test-py/test_backends_designspace.py +++ b/test-py/test_backends_designspace.py @@ -10,11 +10,12 @@ from fontra.backends import getFileSystemBackend, newFileSystemBackend from fontra.backends.copy import copyFont -from fontra.backends.designspace import DesignspaceBackend, UFOBackend +from fontra.backends.designspace import DesignspaceBackend, UFOBackend, convertImageData from fontra.backends.null import NullBackend from fontra.core.classes import ( Anchor, Axes, + BackgroundImage, CrossAxisMapping, FontAxis, FontInfo, @@ -22,6 +23,7 @@ GlyphAxis, GlyphSource, Guideline, + ImageType, Layer, LineMetric, OpenTypeFeatures, @@ -431,6 +433,33 @@ async def test_putGlyph_with_backgroundImage_new_font(testFont, tmpdir): assert 0, "expected backgroundImage" +async def test_putBackgroundImage_JPEG(writableTestFont): + glyph = await writableTestFont.getGlyph("C") + for layerName, layer in glyph.layers.items(): + bgImage = layer.glyph.backgroundImage + if bgImage is not None: + break + + imageDataPNG = await writableTestFont.getBackgroundImage(bgImage.identifier) + imageDataJPEG = convertImageData(imageDataPNG, ImageType.JPEG) + assert imageDataJPEG.type == ImageType.JPEG + + glyphName = "D" + imageIdentifier = str(uuid.uuid4()) + bgImage = BackgroundImage(identifier=imageIdentifier) + + glyphD = await writableTestFont.getGlyph(glyphName) + firstLayerName = list(glyphD.layers.keys())[0] + layer = glyphD.layers[firstLayerName] + layer.glyph.backgroundImage = bgImage + + await writableTestFont.putGlyph(glyphName, glyphD, [ord("D")]) + await writableTestFont.putBackgroundImage(imageIdentifier, imageDataJPEG) + + imageRoundtripped = await writableTestFont.getBackgroundImage(imageIdentifier) + assert imageRoundtripped.type == ImageType.PNG + + async def test_putAxes(writableTestFont): axes = await writableTestFont.getAxes() axes.axes.append(