Skip to content

Commit

Permalink
Merge pull request #1795 from googlefonts/bg-image-data-issue1793
Browse files Browse the repository at this point in the history
[background image] Make image data participate in copy/paste
  • Loading branch information
justvanrossum authored Nov 16, 2024
2 parents bea3787 + 6054a78 commit 1d67e6c
Show file tree
Hide file tree
Showing 3 changed files with 174 additions and 31 deletions.
60 changes: 48 additions & 12 deletions src/fontra/client/core/font-controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { LRUCache } from "./lru-cache.js";
import { setPopFirst } from "./set-ops.js";
import { TaskPool } from "./task-pool.js";
import {
assert,
chain,
getCharFromCodePoint,
mapObjectValues,
Expand Down Expand Up @@ -134,10 +135,7 @@ export class FontController {
// This returns a promise for the requested background image
let cacheEntry = this._backgroundImageCache.get(imageIdentifier);
if (!cacheEntry) {
const imagePromise = this._getBackgroundImage(imageIdentifier);
cacheEntry = { imagePromise, image: null };
this._backgroundImageCache.put(imageIdentifier, cacheEntry);
imagePromise.then((image) => (cacheEntry.image = image));
cacheEntry = this._cacheBackgroundImageFromIdentifier(imageIdentifier);
}
return cacheEntry.imagePromise;
}
Expand All @@ -158,19 +156,39 @@ export class FontController {
return cacheEntry?.image;
}

async _getBackgroundImage(imageIdentifier) {
_cacheBackgroundImageFromIdentifier(imageIdentifier) {
return this._cacheBackgroundImageFromDataURLPromise(
imageIdentifier,
this._loadBackgroundImageData(imageIdentifier)
);
}

async _loadBackgroundImageData(imageIdentifier) {
const imageData = await this.font.getBackgroundImage(imageIdentifier);
if (!imageData) {
return null;
}
return imageData ? `data:image/${imageData.type};base64,${imageData.data}` : null;
}

const image = new Image();
_cacheBackgroundImageFromDataURLPromise(imageIdentifier, imageDataURLPromise) {
const imagePromise = new Promise((resolve, reject) => {
image.onload = (event) => resolve(image);
const image = new Image();
image.onload = (event) => {
cacheEntry.image = image;
resolve(image);
};
imageDataURLPromise.then((imageDataURL) => {
if (imageDataURL) {
image.src = imageDataURL;
} else {
resolve(null);
}
});
});
image.src = `data:image/${imageData.type};base64,${imageData.data}`;

return await imagePromise;
const cacheEntry = { imagePromise, image: null };

this._backgroundImageCache.put(imageIdentifier, cacheEntry);

return cacheEntry;
}

getBackgroundImageBounds(imageIdentifier) {
Expand All @@ -185,6 +203,24 @@ export class FontController {
return this.getBackgroundImageBounds.bind(this);
}

async putBackgroundImageData(imageIdentifier, imageDataURL) {
const [header, imageData] = imageDataURL.split(",");
const imageTypeRegex = /data:image\/(.+?);/g;
const match = imageTypeRegex.exec(header);
const imageType = match[1];
assert(imageType === "png" || imageType === "jpeg");

this._cacheBackgroundImageFromDataURLPromise(
imageIdentifier,
Promise.resolve(imageDataURL)
);

await this.font.putBackgroundImage(imageIdentifier, {
type: imageType,
data: imageData,
});
}

getCachedGlyphNames() {
return this._glyphsPromiseCache.keys();
}
Expand Down
17 changes: 15 additions & 2 deletions src/fontra/core/fonthandler.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
patternIntersect,
patternUnion,
)
from .classes import Font, FontInfo, FontSource, VariableGlyph
from .classes import Font, FontInfo, FontSource, ImageData, VariableGlyph
from .lrucache import LRUCache
from .protocols import (
ProjectManager,
Expand Down Expand Up @@ -165,7 +165,8 @@ async def isReadOnly(self, *, connection=None) -> bool:
async def getBackEndInfo(self, *, connection=None) -> dict:
features = {}
for key, methodName in [
("find-glyphs-that-use-glyph", "findGlyphsThatUseGlyph")
("find-glyphs-that-use-glyph", "findGlyphsThatUseGlyph"),
("background-image", "putBackgroundImage"),
]:
features[key] = hasattr(self.backend, methodName)
projectManagerFeatures = {}
Expand Down Expand Up @@ -288,6 +289,18 @@ def _getClientData(self, connection, key, default=None):
def _setClientData(self, connection, key, value):
self.clientData[connection.clientUUID][key] = value

@remoteMethod
async def putBackgroundImage(
self, imageIdentifier: str, data: dict, *, connection
) -> None:
if not hasattr(self.backend, "putBackgroundImage"):
logger.warning("Backend doesn't support writing of background images")
return
await self.backend.putBackgroundImage(
imageIdentifier,
ImageData(type=data["type"], data=base64.b64decode(data["data"])),
)

@remoteMethod
async def findGlyphsThatUseGlyph(self, glyphName: str, *, connection) -> list[str]:
if hasattr(self.backend, "findGlyphsThatUseGlyph"):
Expand Down
128 changes: 111 additions & 17 deletions src/fontra/views/editor/editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -1709,11 +1709,15 @@ export class EditorController {
// We *have* to do this first, as it won't work after any
// await (Safari insists on that). So we have to do a bit
// of redundant work by calling _prepareCopyOrCut twice.
const { layerGlyphs, flattenedPath } = this._prepareCopyOrCutLayers(
undefined,
false
const { layerGlyphs, flattenedPath, backgroundImageData } =
this._prepareCopyOrCutLayers(undefined, false);
await this._writeLayersToClipboard(
null,
layerGlyphs,
flattenedPath,
backgroundImageData,
event
);
await this._writeLayersToClipboard(null, layerGlyphs, flattenedPath, event);
}
let copyResult;
await this.sceneController.editGlyphAndRecordChanges(
Expand All @@ -1726,8 +1730,13 @@ export class EditorController {
true
);
if (copyResult && !event) {
const { layerGlyphs, flattenedPath } = copyResult;
await this._writeLayersToClipboard(null, layerGlyphs, flattenedPath);
const { layerGlyphs, flattenedPath, backgroundImageData } = copyResult;
await this._writeLayersToClipboard(
null,
layerGlyphs,
flattenedPath,
backgroundImageData
);
}
}

Expand All @@ -1741,25 +1750,51 @@ export class EditorController {
}

if (this.sceneSettings.selectedGlyph.isEditing) {
const { layerGlyphs, flattenedPath } = this._prepareCopyOrCutLayers(
undefined,
false
const { layerGlyphs, flattenedPath, backgroundImageData } =
this._prepareCopyOrCutLayers(undefined, false);
await this._writeLayersToClipboard(
null,
layerGlyphs,
flattenedPath,
backgroundImageData,
event
);
await this._writeLayersToClipboard(null, layerGlyphs, flattenedPath, event);
} else {
const positionedGlyph = this.sceneModel.getSelectedPositionedGlyph();
const varGlyph = positionedGlyph.varGlyph.glyph;
const backgroundImageData = await this._collectBackgroundImageData(varGlyph);
const glyphController = positionedGlyph.glyph;
await this._writeLayersToClipboard(
varGlyph,
[{ glyph: glyphController.instance }],
glyphController.flattenedPath,
backgroundImageData,
event
);
}
}

async _writeLayersToClipboard(varGlyph, layerGlyphs, flattenedPath, event) {
async _collectBackgroundImageData(varGlyph) {
const backgroundImageData = {};
for (const layer of Object.values(varGlyph.layers)) {
if (layer.glyph.backgroundImage) {
const imageIdentifier = layer.glyph.backgroundImage.identifier;
const bgImage = await this.fontController.getBackgroundImage(imageIdentifier);
if (bgImage) {
backgroundImageData[imageIdentifier] = bgImage.src;
}
}
}
return backgroundImageData;
}

async _writeLayersToClipboard(
varGlyph,
layerGlyphs,
flattenedPath,
backgroundImageData,
event
) {
if (!layerGlyphs?.length) {
// nothing to do
return;
Expand All @@ -1774,9 +1809,11 @@ export class EditorController {
const glyphName = this.sceneSettings.selectedGlyphName;
const codePoints = this.fontController.glyphMap[glyphName] || [];
const glifString = staticGlyphToGLIF(glyphName, layerGlyphs[0].glyph, codePoints);
const jsonString = JSON.stringify(
varGlyph ? { variableGlyph: varGlyph } : { layerGlyphs: layerGlyphs }
);
const jsonObject = varGlyph ? { variableGlyph: varGlyph } : { layerGlyphs };
if (backgroundImageData && !isObjectEmpty(backgroundImageData)) {
jsonObject.backgroundImageData = backgroundImageData;
}
const jsonString = JSON.stringify(jsonObject);

const mapping = { "svg": svgString, "glif": glifString, "fontra-json": jsonString };
const plainTextString =
Expand Down Expand Up @@ -1824,6 +1861,8 @@ export class EditorController {

const layerGlyphs = [];
let flattenedPath;
const backgroundImageData = {};

for (const [layerName, layerGlyph] of Object.entries(
this.sceneController.getEditingLayerFromGlyphLayers(varGlyph.layers)
)) {
Expand All @@ -1839,6 +1878,13 @@ export class EditorController {
location: layerLocations[layerName],
glyph: copyResult.instance,
});
if (copyResult.instance.backgroundImage) {
const imageIdentifier = copyResult.instance.backgroundImage.identifier;
const bgImage = this.fontController.getBackgroundImageCached(imageIdentifier);
if (bgImage) {
backgroundImageData[imageIdentifier] = bgImage.src;
}
}
}
if (!layerGlyphs.length && !doCut) {
const { instance, flattenedPath: instancePath } = this._prepareCopyOrCut(
Expand All @@ -1852,7 +1898,7 @@ export class EditorController {
}
layerGlyphs.push({ glyph: instance });
}
return { layerGlyphs, flattenedPath };
return { layerGlyphs, flattenedPath, backgroundImageData };
}

_prepareCopyOrCut(editInstance, doCut = false, wantFlattenedPath = false) {
Expand Down Expand Up @@ -1952,11 +1998,15 @@ export class EditorController {
}

async doPaste() {
let { pasteVarGlyph, pasteLayerGlyphs } = await this._unpackClipboard();
let { pasteVarGlyph, pasteLayerGlyphs, backgroundImageData } =
await this._unpackClipboard();
if (!pasteVarGlyph && !pasteLayerGlyphs?.length) {
return;
}

const backgroundImageIdentifierMapping =
this._makeBackgroundImageIdentifierMapping(backgroundImageData);

if (pasteVarGlyph && this.sceneSettings.selectedGlyph.isEditing) {
const result = await runDialogWholeGlyphPaste();
if (!result) {
Expand Down Expand Up @@ -2003,6 +2053,10 @@ export class EditorController {
}

if (pasteVarGlyph) {
this._remapBackgroundImageIdentifiers(
Object.values(pasteVarGlyph.layers).map((layerGlyph) => layerGlyph.glyph),
backgroundImageIdentifierMapping
);
const positionedGlyph = this.sceneModel.getSelectedPositionedGlyph();
if (positionedGlyph.isUndefined) {
await this.newGlyph(
Expand All @@ -2021,8 +2075,46 @@ export class EditorController {
};
this.sceneSettings.glyphLocation = { ...this.sceneSettings.glyphLocation };
} else {
this._remapBackgroundImageIdentifiers(
pasteLayerGlyphs.map((layerGlyph) => layerGlyph.glyph),
backgroundImageIdentifierMapping
);
await this._pasteLayerGlyphs(pasteLayerGlyphs);
}

await this._writeBackgroundImageData(
backgroundImageData,
backgroundImageIdentifierMapping
);
}

_makeBackgroundImageIdentifierMapping(backgroundImageData) {
if (!backgroundImageData || isObjectEmpty(backgroundImageData)) {
return null;
}
const mapping = {};
for (const originalImageIdentifier of Object.keys(backgroundImageData)) {
const newImageIdentifier = crypto.randomUUID();
mapping[originalImageIdentifier] = newImageIdentifier;
}
return mapping;
}

_remapBackgroundImageIdentifiers(glyphs, identifierMapping) {
for (const glyph of glyphs) {
if (glyph.backgroundImage) {
glyph.backgroundImage.identifier =
identifierMapping[glyph.backgroundImage.identifier] ||
glyph.backgroundImage.identifier;
}
}
}

async _writeBackgroundImageData(backgroundImageData, identifierMapping) {
for (const [imageIdentifier, imageData] of Object.entries(backgroundImageData)) {
const mappedIdentifier = identifierMapping[imageIdentifier] || imageIdentifier;
await this.fontController.putBackgroundImageData(mappedIdentifier, imageData);
}
}

async _unpackClipboard() {
Expand Down Expand Up @@ -2050,6 +2142,7 @@ export class EditorController {

let pasteLayerGlyphs;
let pasteVarGlyph;
let backgroundImageData;

if (customJSON) {
try {
Expand All @@ -2064,13 +2157,14 @@ export class EditorController {
if (clipboardObject.variableGlyph) {
pasteVarGlyph = VariableGlyph.fromObject(clipboardObject.variableGlyph);
}
backgroundImageData = clipboardObject.backgroundImageData;
} catch (error) {
console.log("couldn't paste from JSON:", error.toString());
}
} else {
pasteLayerGlyphs = [{ glyph: await this.parseClipboard(plainText) }];
}
return { pasteVarGlyph, pasteLayerGlyphs };
return { pasteVarGlyph, pasteLayerGlyphs, backgroundImageData };
}

async _pasteReplaceGlyph(varGlyph) {
Expand Down

0 comments on commit 1d67e6c

Please sign in to comment.