mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-05-03 10:00:07 -04:00
fix: load font faces in Safari manually (#8693)
This commit is contained in:
parent
79b181bcdc
commit
03028eaa8c
4 changed files with 269 additions and 207 deletions
|
@ -9,14 +9,7 @@ import type {
|
|||
import type { Bounds } from "../element/bounds";
|
||||
import { getCommonBounds, getElementAbsoluteCoords } from "../element/bounds";
|
||||
import { renderSceneToSvg } from "../renderer/staticSvgScene";
|
||||
import {
|
||||
arrayToMap,
|
||||
distance,
|
||||
getFontString,
|
||||
PromisePool,
|
||||
promiseTry,
|
||||
toBrandedType,
|
||||
} from "../utils";
|
||||
import { arrayToMap, distance, getFontString, toBrandedType } from "../utils";
|
||||
import type { AppState, BinaryFiles } from "../types";
|
||||
import {
|
||||
DEFAULT_EXPORT_PADDING,
|
||||
|
@ -25,9 +18,6 @@ import {
|
|||
SVG_NS,
|
||||
THEME,
|
||||
THEME_FILTER,
|
||||
FONT_FAMILY_FALLBACKS,
|
||||
getFontFamilyFallbacks,
|
||||
CJK_HAND_DRAWN_FALLBACK_FONT,
|
||||
} from "../constants";
|
||||
import { getDefaultAppState } from "../appState";
|
||||
import { serializeAsJSON } from "../data/json";
|
||||
|
@ -44,12 +34,11 @@ import {
|
|||
import { newTextElement } from "../element";
|
||||
import { type Mutable } from "../utility-types";
|
||||
import { newElementWith } from "../element/mutateElement";
|
||||
import { isFrameLikeElement, isTextElement } from "../element/typeChecks";
|
||||
import { isFrameLikeElement } from "../element/typeChecks";
|
||||
import type { RenderableElementsMap } from "./types";
|
||||
import { syncInvalidIndices } from "../fractionalIndex";
|
||||
import { renderStaticScene } from "../renderer/staticScene";
|
||||
import { Fonts } from "../fonts";
|
||||
import { containsCJK } from "../element/textElement";
|
||||
|
||||
const SVG_EXPORT_TAG = `<!-- svg-source:excalidraw -->`;
|
||||
|
||||
|
@ -375,7 +364,10 @@ export const exportToSvg = async (
|
|||
</clipPath>`;
|
||||
}
|
||||
|
||||
const fontFaces = opts?.skipInliningFonts ? [] : await getFontFaces(elements);
|
||||
const fontFaces = !opts?.skipInliningFonts
|
||||
? await Fonts.generateFontFaceDeclarations(elements)
|
||||
: [];
|
||||
|
||||
const delimiter = "\n "; // 6 spaces
|
||||
|
||||
svgRoot.innerHTML = `
|
||||
|
@ -454,111 +446,3 @@ export const getExportSize = (
|
|||
|
||||
return [width, height];
|
||||
};
|
||||
|
||||
const getFontFaces = async (
|
||||
elements: readonly ExcalidrawElement[],
|
||||
): Promise<string[]> => {
|
||||
const fontFamilies = new Set<number>();
|
||||
const charsPerFamily: Record<number, Set<string>> = {};
|
||||
|
||||
for (const element of elements) {
|
||||
if (!isTextElement(element)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
fontFamilies.add(element.fontFamily);
|
||||
|
||||
// gather unique codepoints only when inlining fonts
|
||||
for (const char of element.originalText) {
|
||||
if (!charsPerFamily[element.fontFamily]) {
|
||||
charsPerFamily[element.fontFamily] = new Set();
|
||||
}
|
||||
|
||||
charsPerFamily[element.fontFamily].add(char);
|
||||
}
|
||||
}
|
||||
|
||||
const orderedFamilies = Array.from(fontFamilies);
|
||||
|
||||
// for simplicity, assuming we have just one family with the CJK handdrawn fallback
|
||||
const familyWithCJK = orderedFamilies.find((x) =>
|
||||
getFontFamilyFallbacks(x).includes(CJK_HAND_DRAWN_FALLBACK_FONT),
|
||||
);
|
||||
|
||||
if (familyWithCJK) {
|
||||
const characters = getChars(charsPerFamily[familyWithCJK]);
|
||||
|
||||
if (containsCJK(characters)) {
|
||||
const family = FONT_FAMILY_FALLBACKS[CJK_HAND_DRAWN_FALLBACK_FONT];
|
||||
|
||||
// adding the same characters to the CJK handrawn family
|
||||
charsPerFamily[family] = new Set(characters);
|
||||
|
||||
// the order between the families and fallbacks is important, as fallbacks need to be defined first and in the reversed order
|
||||
// so that they get overriden with the later defined font faces, i.e. in case they share some codepoints
|
||||
orderedFamilies.unshift(
|
||||
FONT_FAMILY_FALLBACKS[CJK_HAND_DRAWN_FALLBACK_FONT],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const iterator = fontFacesIterator(orderedFamilies, charsPerFamily);
|
||||
|
||||
// don't trigger hundreds of concurrent requests (each performing fetch, creating a worker, etc.),
|
||||
// instead go three requests at a time, in a controlled manner, without completely blocking the main thread
|
||||
// and avoiding potential issues such as rate limits
|
||||
const concurrency = 3;
|
||||
const fontFaces = await new PromisePool(iterator, concurrency).all();
|
||||
|
||||
// dedup just in case (i.e. could be the same font faces with 0 glyphs)
|
||||
return Array.from(new Set(fontFaces));
|
||||
};
|
||||
|
||||
function* fontFacesIterator(
|
||||
families: Array<number>,
|
||||
charsPerFamily: Record<number, Set<string>>,
|
||||
): Generator<Promise<void | readonly [number, string]>> {
|
||||
for (const [familyIndex, family] of families.entries()) {
|
||||
const { fontFaces, metadata } = Fonts.registered.get(family) ?? {};
|
||||
|
||||
if (!Array.isArray(fontFaces)) {
|
||||
console.error(
|
||||
`Couldn't find registered fonts for font-family "${family}"`,
|
||||
Fonts.registered,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (metadata?.local) {
|
||||
// don't inline local fonts
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const [fontFaceIndex, fontFace] of fontFaces.entries()) {
|
||||
yield promiseTry(async () => {
|
||||
try {
|
||||
const characters = getChars(charsPerFamily[family]);
|
||||
const fontFaceCSS = await fontFace.toCSS(characters);
|
||||
|
||||
if (!fontFaceCSS) {
|
||||
return;
|
||||
}
|
||||
|
||||
// giving a buffer of 10K font faces per family
|
||||
const fontFaceOrder = familyIndex * 10_000 + fontFaceIndex;
|
||||
const fontFaceTuple = [fontFaceOrder, fontFaceCSS] as const;
|
||||
|
||||
return fontFaceTuple;
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`Couldn't transform font-face to css for family "${fontFace.fontFace.family}"`,
|
||||
error,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const getChars = (characterSet: Set<string>) =>
|
||||
Array.from(characterSet).join("");
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue