Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Background image for tracing #1707

Closed
wants to merge 13 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ ufomerge==1.8.2
unicodedata2==15.1.0
watchfiles==0.24.0
skia-pathops==0.8.0.post2
pybase64==1.4.0
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this for? The base64 module comes with Python.

69 changes: 66 additions & 3 deletions src/fontra/backends/designspace.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

import asyncio
import base64
import logging
import os
import pathlib
Expand All @@ -22,7 +23,7 @@
DiscreteAxisDescriptor,
SourceDescriptor,
)
from fontTools.misc.transform import DecomposedTransform
from fontTools.misc.transform import DecomposedTransform, Transform
from fontTools.pens.pointPen import AbstractPointPen
from fontTools.pens.recordingPen import RecordingPointPen
from fontTools.ufoLib import UFOReaderWriter
Expand All @@ -42,6 +43,7 @@
GlyphAxis,
GlyphSource,
Guideline,
Image,
Kerning,
Layer,
LineMetric,
Expand Down Expand Up @@ -344,7 +346,9 @@ async def getGlyph(self, glyphName: str) -> VariableGlyph | None:
if glyphName not in ufoLayer.glyphSet:
continue

staticGlyph, ufoGlyph = ufoLayerToStaticGlyph(ufoLayer.glyphSet, glyphName)
staticGlyph, ufoGlyph = ufoLayerToStaticGlyph(
ufoLayer.glyphSet, glyphName, ufoDir=ufoLayer.path
)
if ufoLayer == self.defaultUFOLayer:
localDS = ufoGlyph.lib.get(GLYPH_DESIGNSPACE_LIB_KEY)
if localDS is not None:
Expand Down Expand Up @@ -1072,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]:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So this would return all binary data for all layers. That's insanely inefficient: we always load "big things" on demand. But see the general comment, you ignored the method signature that I prescribed.

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 @@ -1397,6 +1435,7 @@ class UFOGlyph:
height: float | None = None
anchors: list = []
guidelines: list = []
image: Image | None = None
note: str | None = None
lib: dict

Expand Down Expand Up @@ -1566,7 +1605,9 @@ def iterAttrs(self, attrName):
yield getattr(item, attrName)


def ufoLayerToStaticGlyph(glyphSet, glyphName, penClass=PackedPathPointPen):
def ufoLayerToStaticGlyph(
glyphSet, glyphName, penClass=PackedPathPointPen, ufoDir=None
):
glyph = UFOGlyph()
glyph.lib = {}
pen = penClass()
Expand All @@ -1583,6 +1624,7 @@ def ufoLayerToStaticGlyph(glyphSet, glyphName, penClass=PackedPathPointPen):
verticalOrigin=verticalOrigin,
anchors=unpackAnchors(glyph.anchors),
guidelines=unpackGuidelines(glyph.guidelines),
image=unpackImage(glyph.image),
)

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


def unpackImage(image):
if image is None:
return None

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)
decomposedTransform = DecomposedTransform.fromTransform(transformation)

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


def unpackGuidelines(guidelines):
return [
Guideline(
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):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please do not work on the Fontra backlend until we've actually designed how that data should be stored in there.

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
20 changes: 20 additions & 0 deletions src/fontra/client/core/classes.json
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,10 @@
"guidelines": {
"type": "list",
"subtype": "Guideline"
},
"image": {
"type": "Image",
"optional": true
}
},
"PackedPath": {
Expand Down Expand Up @@ -300,6 +304,22 @@
"subtype": "Any"
}
},
"Image": {
"fileName": {
"type": "str"
},
"transformation": {
"type": "DecomposedTransform"
},
"color": {
"type": "str",
"optional": true
},
"customData": {
"type": "dict",
"subtype": "Any"
}
},
"Axes": {
"axes": {
"type": "list",
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
28 changes: 27 additions & 1 deletion src/fontra/client/core/glyph-controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
import { VariationError } from "./errors.js";
import { filterPathByPointIndices } from "./path-functions.js";
import { PathHitTester } from "./path-hit-tester.js";
import { centeredRect, sectRect, unionRect } from "./rectangle.js";
import { centeredRect, rectFromArray, sectRect, unionRect } from "./rectangle.js";
import {
getRepresentation,
registerRepresentationFactory,
Expand Down Expand Up @@ -558,6 +558,10 @@ export class StaticGlyphController {
return this.instance.guidelines;
}

get image() {
return this.instance.image;
}

get path() {
return this.instance.path;
}
Expand Down Expand Up @@ -615,11 +619,13 @@ export class StaticGlyphController {
point: pointIndices,
component: componentIndices,
anchor: anchorIndices,
image: imageIndices,
} = parseSelection(selection);

pointIndices = pointIndices || [];
componentIndices = componentIndices || [];
anchorIndices = anchorIndices || [];
imageIndices = imageIndices || [];

const selectionRects = [];
if (pointIndices.length) {
Expand Down Expand Up @@ -647,6 +653,26 @@ export class StaticGlyphController {
}
}

for (const imageIndex of imageIndices) {
// currently it clear it's just one image
const image = this.image;
if (image) {
const sx = image.transformation.translateX;
const sy = image.transformation.translateY;
const xScale = image.transformation.scaleX;
const yScale = image.transformation.scaleY;

const img = new Image();
// 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;
selectionRects.push(rectFromArray([sx, sy, sx + w, sy + h]));
}
}

return unionRect(...selectionRects);
}
}
Expand Down
11 changes: 11 additions & 0 deletions src/fontra/client/core/var-glyph.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ export class StaticGlyph {
source.guidelines = noCopy
? obj.guidelines || []
: normalizeGuidelines(obj.guidelines || []);
//source.image = noCopy ? obj.image || undefined : copyCustomData(obj.image || undefined);
source.image = obj.image;
return source;
}

Expand All @@ -76,6 +78,15 @@ export function copyComponent(component) {
};
}

export function copyImage(image) {
return {
fileName: image.fileName,
transformation: { ...getDecomposedIdentity(), ...image.transformation },
color: image.color,
customData: { ...image.customData },
};
}

function copyCustomData(data) {
return JSON.parse(JSON.stringify(data));
}
10 changes: 10 additions & 0 deletions src/fontra/core/classes.py
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,7 @@ class StaticGlyph:
verticalOrigin: Optional[float] = None
anchors: list[Anchor] = field(default_factory=list)
guidelines: list[Guideline] = field(default_factory=list)
image: Optional[Image] = None

def convertToPackedPaths(self):
return replace(self, path=self.path.asPackedPath())
Expand All @@ -228,6 +229,14 @@ class Anchor:
customData: CustomData = field(default_factory=dict)


@dataclass(kw_only=True)
class Image:
fileName: str
transformation: DecomposedTransform = field(default_factory=DecomposedTransform)
color: Optional[str] = None
customData: CustomData = field(default_factory=dict)


Location = dict[str, float]
CustomData = dict[str, Any]

Expand Down Expand Up @@ -410,6 +419,7 @@ def registerHook(cls, omitIfDefault=True, **fieldHooks):
registerHook(GlyphAxis, customData=_unstructureDictSortedRecursively)
registerHook(Anchor, customData=_unstructureDictSortedRecursively)
registerHook(Guideline, customData=_unstructureDictSortedRecursively)
registerHook(Image, customData=_unstructureDictSortedRecursively)
registerHook(StaticGlyph, customData=_unstructureDictSortedRecursively)
registerHook(
GlyphSource,
Expand Down
2 changes: 2 additions & 0 deletions src/fontra/core/clipboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
readGlyphOrCreate,
unpackAnchors,
unpackGuidelines,
unpackImage,
)
from .classes import StaticGlyph
from .path import PackedPathPointPen
Expand Down Expand Up @@ -75,6 +76,7 @@ def parseGLIF(data: str) -> StaticGlyph | None:
xAdvance=ufoGlyph.width,
anchors=unpackAnchors(ufoGlyph.anchors),
guidelines=unpackGuidelines(ufoGlyph.guidelines),
image=unpackImage(ufoGlyph.image),
)


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":
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, "binaryData" is not a font attribute like "unitsPerEm" is. See general comment.

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
Loading
Loading