Skip to content

Commit

Permalink
Get rid of the customData hack with base64 image
Browse files Browse the repository at this point in the history
  • Loading branch information
ollimeier committed Nov 6, 2024
1 parent 81f646c commit c6da347
Show file tree
Hide file tree
Showing 8 changed files with 104 additions and 23 deletions.
52 changes: 39 additions & 13 deletions src/fontra/backends/designspace.py
Original file line number Diff line number Diff line change
Expand Up @@ -1076,6 +1076,40 @@ def _writeDesignSpaceDocument(self):
self.dsDoc.write(self.dsDoc.path)
self.dsDocModTime = os.stat(self.dsDoc.path).st_mtime

async def getBinaryData(self) -> dict[str, bytes]:
binaryData = {}

for ufoLayer in self.ufoLayers:
folderPath = pathlib.Path(ufoLayer.path) / "images"
if not folderPath.is_dir():
continue
for filePath in folderPath.iterdir():
if filePath.is_file():
binaryData[filePath.name] = base64.b64encode(
filePath.read_bytes()
).decode("utf-8")

return binaryData

async def putBinaryData(
self, name: str, fontraLayerName: str | None, binaryData: bytes
) -> None:
if fontraLayerName is not None:
ufoDir = self.ufoLayers.findItem(fontraLayerName=fontraLayerName).path
filePath = pathlib.Path(ufoDir) / "images" / name
else:
# loop through all layers and return the first found image
for ufoLayer in self.ufoLayers:
ufoDir = ufoLayer.path
filePath = pathlib.Path(ufoDir) / "images" / name
if filePath.is_file():
break

if not filePath.is_file():
filePath = pathlib.Path(self.defaultUFOLayer.path) / "images" / name

filePath.write_bytes(binaryData)

async def watchExternalChanges(
self, callback: Callable[[Any], Awaitable[None]]
) -> None:
Expand Down Expand Up @@ -1590,7 +1624,7 @@ def ufoLayerToStaticGlyph(
verticalOrigin=verticalOrigin,
anchors=unpackAnchors(glyph.anchors),
guidelines=unpackGuidelines(glyph.guidelines),
image=unpackImage(glyph.image, ufoDir),
image=unpackImage(glyph.image),
)

return staticGlyph, glyph
Expand All @@ -1613,32 +1647,24 @@ def unpackAnchors(anchors):
return [Anchor(name=a.get("name"), x=a["x"], y=a["y"]) for a in anchors]


def unpackImage(image, ufoDir=None):
def unpackImage(image):
if image is None:
return None

if ufoDir is None:
customData = image.get("customData", {})
else:
pathToImage = os.path.join(ufoDir, "images/", image["fileName"])
with open(pathToImage, "rb") as image_file:
base64Data = base64.b64encode(image_file.read()).decode("utf-8")
customData = image.get("customData", {"base64": base64Data})

xx = image.get("xScale", 1)
xy = image.get("xyScale", 0)
yx = image.get("yxScale", 0)
yy = image.get("yScale", 1)
dx = image.get("xOffset", 0)
dy = image.get("yOffset", 0)
transformation = Transform(xx, xy, yx, yy, dx, dy)
transformation = DecomposedTransform.fromTransform(transformation)
decomposedTransform = DecomposedTransform.fromTransform(transformation)

return Image(
fileName=image["fileName"],
transformation=transformation,
transformation=decomposedTransform,
color=image.get("color", None),
customData=customData,
customData=image.get("customData", {}),
)


Expand Down
23 changes: 23 additions & 0 deletions src/fontra/backends/fontra.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import asyncio
import base64
import csv
import json
import logging
Expand Down Expand Up @@ -92,6 +93,13 @@ def glyphInfoPath(self):
def glyphsDir(self):
return self.path / self.glyphsDirName

@property
def binaryDataPath(self):
binaryDataPath = self.path / "binaryData"
if not binaryDataPath.is_dir():
binaryDataPath.mkdir()
return binaryDataPath

async def aclose(self):
self.flush()

Expand Down Expand Up @@ -188,6 +196,21 @@ async def putCustomData(self, customData: dict[str, Any]) -> None:
self.fontData.customData = deepcopy(customData)
self._scheduler.schedule(self._writeFontData)

async def getBinaryData(self) -> bytes | None:
binaryData = {}
if not self.binaryDataPath.is_dir():
return binaryData
for filePath in self.binaryDataPath.iterdir():
if filePath.is_file():
binaryData[filePath.name] = base64.b64encode(
filePath.read_bytes()
).decode("utf-8")
return binaryData

async def putBinaryData(self, name: str, binaryData: bytes) -> None:
filePath = self.binaryDataPath / name
filePath.write_bytes(binaryData)

def _readGlyphInfo(self) -> None:
with self.glyphInfoPath.open("r", encoding="utf-8", newline="") as file:
reader = csv.reader(file, delimiter=";")
Expand Down
10 changes: 10 additions & 0 deletions src/fontra/client/core/font-controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export class FontController {
this._rootObject.sources = ensureDenseSources(await this.font.getSources());
this._rootObject.unitsPerEm = await this.font.getUnitsPerEm();
this._rootObject.customData = await this.font.getCustomData();
this._rootObject.binaryData = await this.font.getBinaryData();
this._rootClassDef = (await getClassSchema())["Font"];
this.backendInfo = await this.font.getBackEndInfo();
this.readOnly = await this.font.isReadOnly();
Expand Down Expand Up @@ -104,6 +105,15 @@ export class FontController {
return this._rootObject.customData;
}

get binaryData() {
return this._rootObject.binaryData;
}

getImage(name) {
// async messes up the visualization of the image
return this.binaryData[name];
}

async getData(key) {
if (!this._rootObject[key]) {
const methods = {
Expand Down
4 changes: 3 additions & 1 deletion src/fontra/client/core/glyph-controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -663,7 +663,9 @@ export class StaticGlyphController {
const yScale = image.transformation.scaleY;

const img = new Image();
img.src = "data:image/jpg;base64," + image.customData["base64"];
// TODO: We need the fontController to get the image data for calculating the size
// of the image to get the right bounding box.
// img.src = "data:image/jpg;base64," + fontController.getImage(image.fileName);

const w = img.width * xScale;
const h = img.height * yScale;
Expand Down
9 changes: 9 additions & 0 deletions src/fontra/core/fonthandler.py
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,8 @@ async def _getData(self, key: str) -> Any:
value = await self.backend.getCustomData()
case "unitsPerEm":
value = await self.backend.getUnitsPerEm()
case "binaryData":
value = await self.backend.getBinaryData()
case _:
raise KeyError(key)

Expand Down Expand Up @@ -268,6 +270,13 @@ async def getUnitsPerEm(self, *, connection):
async def getCustomData(self, *, connection):
return await self.getData("customData")

# Then add getBinaryData() on FontHandler as a "remotemethod"
@remoteMethod
async def getBinaryData(self, *, connection):
print("FontHandler getBinaryData")
# and do the base64 conversion there.
return await self.getData("binaryData")

def _getClientData(self, connection, key, default=None):
return self.clientData[connection.clientUUID].get(key, default)

Expand Down
3 changes: 2 additions & 1 deletion src/fontra/views/editor/scene-model.js
Original file line number Diff line number Diff line change
Expand Up @@ -546,6 +546,7 @@ export class SceneModel {
// return { selection: fontGuidelineSelection };
// }

// TODO: If images are locked, don't allow selection
const imageSelection = this.imageSelectionAtPoint(point);
if (imageSelection.size) {
return { selection: imageSelection };
Expand Down Expand Up @@ -741,7 +742,7 @@ export class SceneModel {
const y = point.y - positionedGlyph.y;

const img = new Image();
img.src = "data:image/jpg;base64," + image.customData["base64"];
img.src = "data:image/jpg;base64," + this.fontController.getImage(image.fileName);

const sx = image.transformation.translateX;
const sy = image.transformation.translateY;
Expand Down
2 changes: 1 addition & 1 deletion src/fontra/views/editor/visualization-layer-definitions.js
Original file line number Diff line number Diff line change
Expand Up @@ -428,7 +428,7 @@ registerVisualizationLayerDefinition({
}

const img = new Image();
img.src = "data:image/jpg;base64," + image.customData["base64"];
img.src = "data:image/jpg;base64," + model.fontController.getImage(image.fileName);

const sx = image.transformation.translateX;
const sy = image.transformation.translateY;
Expand Down
24 changes: 17 additions & 7 deletions test-py/test_backends_designspace.py
Original file line number Diff line number Diff line change
Expand Up @@ -288,16 +288,26 @@ async def test_addAnchor(writableTestFont):
)


async def test_getImage(writableTestFont):
async def test_getBinaryData(writableTestFont):
glyph = await writableTestFont.getGlyph("M")

layerName = "MutatorSansLightCondensed/foreground"
layer = glyph.layers[layerName]
binaryData = await writableTestFont.getBinaryData()

ufoDir = writableTestFont.ufoLayers.findItem(fontraLayerName=layerName).path
pathToImage = pathlib.Path(ufoDir) / "images" / "W_images.png"
with open(pathToImage, "rb") as image_file:
base64Data = base64.b64encode(image_file.read()).decode("utf-8")
filePath = pathlib.Path(ufoDir) / "images" / "W_images.png"

assert binaryData[layer.glyph.image.fileName] == base64.b64encode(
filePath.read_bytes()
).decode("utf-8")


async def test_getImage(writableTestFont):
glyph = await writableTestFont.getGlyph("M")

layerName = "MutatorSansLightCondensed/foreground"
layer = glyph.layers[layerName]

transformation = Transform(
-0.29948946703118456,
Expand All @@ -307,14 +317,14 @@ async def test_getImage(writableTestFont):
901.6748243052426,
789.4527729820308,
)
transformation = DecomposedTransform.fromTransform(transformation)
decomposedTransform = DecomposedTransform.fromTransform(transformation)

assert layer.glyph.image is not None
image = Image(
fileName="W_images.png",
transformation=transformation,
transformation=decomposedTransform,
color=None,
customData={"base64": base64Data},
customData={},
)
assert image.fileName == layer.glyph.image.fileName
assert image.transformation == layer.glyph.image.transformation
Expand Down

0 comments on commit c6da347

Please sign in to comment.