Skip to content

Commit

Permalink
Merge branch 'main' into location-base-source-ui-issue-1640
Browse files Browse the repository at this point in the history
  • Loading branch information
justvanrossum committed Nov 25, 2024
2 parents b3a3274 + 1a6e987 commit 2ff633b
Show file tree
Hide file tree
Showing 53 changed files with 3,635 additions and 798 deletions.
39 changes: 39 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,44 @@
# Changelog for Fontra

## 2024-11-20

- Add support for background image colorization [PR 1815](https://github.com/googlefonts/fontra/pull/1815)

## 2024-11-18

New feature: background images.

A background image can be added to a glyph in three ways:

- Paste image data
- Drop an image file onto the canvas
- Choose an image file from the user's hard drive, with the "Glyph" -> "Add background image..." menu.

The image file or data can be in PNG or JPEG format.

The glyph needs to be in edit mode, and at a selected source (not at an interpolation).

Fontra's background image feature is mostly compatible with UFO background images, although it doesn't implement UFO's colorization feature yet. Fontra does allow the opacity of the image to be set.

Background images are locked by default, and can be unlocked with the "Unlock background images" context menu item.

Selected background images can be moved around by dragging, and they participate in the Selection Transformation panel's operations.

The Selection Info panel shows the settings for a selected background image: the Opacity can be edited there and the Transformation settings can be edited numerically there.

Caveat: support for background images is limited to the `.designspace`/`.ufo` and `.fontra` backends. It is currently not supported in the `rcjk` backend.

[Issue 1660](https://github.com/googlefonts/fontra/issues/1660), [Issue 1777](https://github.com/googlefonts/fontra/issues/1777) (There were too many PRs to mention individually here.)

## 2024-11-13

- Improved UI translations [PR 1764](https://github.com/googlefonts/fontra/pull/1764)
- Added "Selecte previous/next glyph" menu items [PR 1706](https://github.com/googlefonts/fontra/pull/1706)
- Partial support for background images (more to come) [PR 1775](https://github.com/googlefonts/fontra/pull/1775)
- Add support for many UFO font info fields, so they won't get lost during round-tripping [PR 1770](https://github.com/googlefonts/fontra/pull/1770)
- Fixed cosmetic issue with scrollbars on Windows [PR 1767](https://github.com/googlefonts/fontra/pull/1767)
- Fixed bug with Copy/Paste menu items [PR 1756](https://github.com/googlefonts/fontra/pull/1756)

## 2024-10-24

- Various improvements to the font sources panel [PR 1739](https://github.com/googlefonts/fontra/pull/1739)
Expand Down
302 changes: 151 additions & 151 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"mocha": "^10.8.2",
"prettier-plugin-organize-imports": "^4.1.0",
"prettier-plugin-sort-json": "^4.0.0",
"rollup": "^4.24.4"
"rollup": "^4.27.0"
},
"dependencies": {
"bezier-js": "^6.1.4",
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
7 changes: 4 additions & 3 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
aiohttp==3.10.10
aiohttp==3.11.2
cattrs==24.1.2
fonttools[ufo,unicode]==4.54.1
glyphsLib==6.9.4
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
Expand Down
20 changes: 9 additions & 11 deletions src/fontra/backends/copy.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,15 +87,13 @@ async def _copyFont(
raise e

if isinstance(destBackend, WriteBackgroundImage):
backgroundImageInfos = [info for t in done for info in t.result()]
if backgroundImageInfos:
backgroundImageIdentifiers = [info for t in done for info in t.result()]
if backgroundImageIdentifiers:
assert isinstance(sourceBackend, ReadBackgroundImage), type(sourceBackend)
for glyphName, layerName, imageIdentifier in backgroundImageInfos:
for imageIdentifier in backgroundImageIdentifiers:
imageData = await sourceBackend.getBackgroundImage(imageIdentifier)
if imageData is not None:
await destBackend.putBackgroundImage(
imageIdentifier, glyphName, layerName, imageData
)
await destBackend.putBackgroundImage(imageIdentifier, imageData)

await destBackend.putKerning(await sourceBackend.getKerning())
await destBackend.putFeatures(await sourceBackend.getFeatures())
Expand All @@ -110,7 +108,7 @@ async def copyGlyphs(
progressInterval: int,
continueOnError: bool,
) -> list:
backgroundImageInfos = []
backgroundImageIdentifiers = []

while glyphNamesToCopy:
if progressInterval and not (len(glyphNamesToCopy) % progressInterval):
Expand Down Expand Up @@ -140,15 +138,15 @@ async def copyGlyphs(
}
glyphNamesToCopy.extend(sorted(componentNames - glyphNamesCopied))

for layerName, layer in glyph.layers.items():
for layer in glyph.layers.values():
if layer.glyph.backgroundImage is not None:
backgroundImageInfos.append(
(glyphName, layerName, layer.glyph.backgroundImage.identifier)
backgroundImageIdentifiers.append(
layer.glyph.backgroundImage.identifier
)

await destBackend.putGlyph(glyphName, glyph, glyphMap[glyphName])

return backgroundImageInfos
return backgroundImageIdentifiers


async def mainAsync() -> None:
Expand Down
90 changes: 61 additions & 29 deletions src/fontra/backends/designspace.py
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,7 @@ def __init__(self, dsDoc: DesignSpaceDocument) -> None:
self._glyphDependencies: GlyphDependencies | None = None
self._backgroundTasksTask: asyncio.Task | None = None
self._imageMapping = DoubleDict()
self._imageDataToWrite: dict[str, ImageData] = {}
# Set this to true to set "public.truetype.overlap" in each writte .glif's lib:
self.setOverlapSimpleFlag = False
self._familyName: str | None = None
Expand Down Expand Up @@ -651,11 +652,13 @@ async def putGlyph(
if imageInfo is not None:
_, imageFileName = imageInfo
else:
imageFileName = f"{layer.glyph.backgroundImage.identifier}.png"
imageIdentifier = layer.glyph.backgroundImage.identifier
imageFileName = f"{imageIdentifier}.png"
imageInfo = (ufoLayer.path, imageFileName)
self._imageMapping[imageInfo] = (
layer.glyph.backgroundImage.identifier
)
self._imageMapping[imageInfo] = imageIdentifier
imageData = self._imageDataToWrite.pop(imageIdentifier, None)
if imageData is not None:
await self.putBackgroundImage(imageIdentifier, imageData)

drawPointsFunc = populateUFOLayerGlyph(
layerGlyph,
Expand Down Expand Up @@ -1183,30 +1186,19 @@ async def getBackgroundImage(self, imageIdentifier: str) -> ImageData | None:

return ImageData(type=ImageType.PNG, data=data)

async def putBackgroundImage(
self, imageIdentifier: str, glyphName: str, layerName: str, data: ImageData
) -> None:
if glyphName not in self.glyphMap:
raise KeyError(glyphName)

async def putBackgroundImage(self, imageIdentifier: str, data: ImageData) -> None:
if data.type != ImageType.PNG:
raise NotImplementedError("convert image to PNG")

defaultStaticGlyph, defaultUFOGlyph = ufoLayerToStaticGlyph(
self.defaultUFOLayer.glyphSet, glyphName
)

layerNameMapping = defaultUFOGlyph.lib.get(LAYER_NAME_MAPPING_LIB_KEY, {})
revLayerNameMapping = {v: k for k, v in layerNameMapping.items()}
layerName = revLayerNameMapping.get(layerName, layerName)
ufoLayer = self.ufoLayers.findItem(fontraLayerName=layerName)

imageFileName = f"{imageIdentifier}.{data.type.lower()}"
data = convertImageData(data, ImageType.PNG)

ufoLayer.reader.writeImage(imageFileName, data.data, validate=True)

key = (ufoLayer.path, imageFileName)
self._imageMapping[key] = imageIdentifier
imageInfo = self._imageMapping.reverse.get(imageIdentifier)
if imageInfo is None:
# We don't yet know in which layer to write this image, let's postpone
# until putGlyph() comes across it.
self._imageDataToWrite[imageIdentifier] = data
else:
ufoPath, imageFileName = self._imageMapping.reverse[imageIdentifier]
reader = self.ufoManager.getReader(ufoPath)
reader.writeImage(imageFileName, data.data, validate=True)

def _getImageIdentifier(self, ufoPath: str, imageFileName: str) -> str:
key = (ufoPath, imageFileName)
Expand Down Expand Up @@ -1828,12 +1820,29 @@ def unpackBackgroundImage(imageDict: dict | None) -> BackgroundImage | None:
return None

t = Transform(*(imageDict.get(k, dv) for k, dv in imageTransformFields))
colorChannels = [float(ch.strip()) for ch in imageDict.get("color", "").split(",")]
colorChannels = (
[float(ch.strip()) for ch in imageDict["color"].split(",")]
if "color" in imageDict
else None
)

opacity = 1.0

if colorChannels:
if len(colorChannels) == 4:
opacity = colorChannels[3]
if colorChannels[:3] != [0, 0, 0]:
colorChannels[3] = 1.0
else:
colorChannels = None
else:
colorChannels = None

return BackgroundImage(
identifier=imageDict["fileName"],
transformation=DecomposedTransform.fromTransform(t),
color=RGBAColor(*colorChannels) if len(colorChannels) == 4 else None,
opacity=opacity,
color=RGBAColor(*colorChannels) if colorChannels else None,
)


Expand All @@ -1848,8 +1857,11 @@ def packBackgroundImage(backgroundImage, imageFileName) -> dict:
if backgroundImage.color is not None:
c = backgroundImage.color
imageDict["color"] = ",".join(
_formatChannelValue(ch) for ch in [c.red, c.green, c.blue, c.alpha]
_formatChannelValue(ch)
for ch in [c.red, c.green, c.blue, backgroundImage.opacity]
)
elif backgroundImage.opacity != 1.0:
imageDict["color"] = f"0,0,0,{_formatChannelValue(backgroundImage.opacity)}"

return imageDict

Expand Down Expand Up @@ -1915,6 +1927,8 @@ def populateUFOLayerGlyph(
layerGlyph.image = packBackgroundImage(
staticGlyph.backgroundImage, imageFileName
)
else:
layerGlyph.image = None

for component in staticGlyph.components:
if component.location or forceVariableComponents:
Expand Down Expand Up @@ -2226,3 +2240,21 @@ 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))
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())
4 changes: 1 addition & 3 deletions src/fontra/backends/fontra.py
Original file line number Diff line number Diff line change
Expand Up @@ -197,9 +197,7 @@ async def getBackgroundImage(self, imageIdentifier: str) -> ImageData | None:

return None # Image not found

async def putBackgroundImage(
self, imageIdentifier: str, glyphName: str, layerName: str, data: ImageData
) -> None:
async def putBackgroundImage(self, imageIdentifier: str, data: ImageData) -> None:
fileName = f"{imageIdentifier}.{data.type.lower()}"
self.backgroundImagesDir.mkdir(exist_ok=True)
path = self.backgroundImagesDir / fileName
Expand Down
3 changes: 3 additions & 0 deletions src/fontra/client/core/classes.json
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,9 @@
"transformation": {
"type": "DecomposedTransform"
},
"opacity": {
"type": "float"
},
"color": {
"type": "RGBAColor",
"optional": true
Expand Down
Loading

0 comments on commit 2ff633b

Please sign in to comment.