mirror of
https://github.com/excalidraw/excalidraw.git
synced 2024-11-02 03:25:53 +01:00
Processing fallback fonts in the correct order
This commit is contained in:
parent
f40a8d0cab
commit
8ad59687bb
@ -66,11 +66,11 @@ export const FontPickerList = React.memo(
|
|||||||
.filter(
|
.filter(
|
||||||
([_, { metadata }]) => !metadata.serverSide && !metadata.fallback,
|
([_, { metadata }]) => !metadata.serverSide && !metadata.fallback,
|
||||||
)
|
)
|
||||||
.map(([familyId, { metadata, fonts }]) => {
|
.map(([familyId, { metadata, fontFaces }]) => {
|
||||||
const fontDescriptor = {
|
const fontDescriptor = {
|
||||||
value: familyId,
|
value: familyId,
|
||||||
icon: metadata.icon ?? FontFamilyNormalIcon,
|
icon: metadata.icon ?? FontFamilyNormalIcon,
|
||||||
text: fonts[0].fontFace.family,
|
text: fontFaces[0]?.fontFace?.family ?? "Unknown",
|
||||||
};
|
};
|
||||||
|
|
||||||
if (metadata.deprecated) {
|
if (metadata.deprecated) {
|
||||||
|
@ -114,6 +114,9 @@ export const CLASSES = {
|
|||||||
SHAPE_ACTIONS_MENU: "App-menu__left",
|
SHAPE_ACTIONS_MENU: "App-menu__left",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const CHINESE_HANDWRITTEN_FALLBACK_FONT = "Xiaolai";
|
||||||
|
export const WINDOWS_EMOJI_FALLBACK_FONT = "Segoe UI Emoji";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* // TODO: shouldn't be really `const`, likely neither have integers as values, due to value for the custom fonts, which should likely be some hash.
|
* // TODO: shouldn't be really `const`, likely neither have integers as values, due to value for the custom fonts, which should likely be some hash.
|
||||||
*
|
*
|
||||||
@ -133,7 +136,17 @@ export const FONT_FAMILY = {
|
|||||||
"Comic Shanns": 8,
|
"Comic Shanns": 8,
|
||||||
"Liberation Sans": 9,
|
"Liberation Sans": 9,
|
||||||
// from here on fallback fonts only
|
// from here on fallback fonts only
|
||||||
Xiaolai: 1000,
|
[CHINESE_HANDWRITTEN_FALLBACK_FONT]: 1000,
|
||||||
|
[WINDOWS_EMOJI_FALLBACK_FONT]: 9999,
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: perahaps could be specific per-family in the future
|
||||||
|
export const FONT_FAMILY_FALLBACKS = {
|
||||||
|
string: `${CHINESE_HANDWRITTEN_FALLBACK_FONT}, ${WINDOWS_EMOJI_FALLBACK_FONT}`,
|
||||||
|
ordered: [
|
||||||
|
FONT_FAMILY[CHINESE_HANDWRITTEN_FALLBACK_FONT],
|
||||||
|
FONT_FAMILY[WINDOWS_EMOJI_FALLBACK_FONT],
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
export const THEME = {
|
export const THEME = {
|
||||||
@ -157,10 +170,6 @@ export const FRAME_STYLE = {
|
|||||||
nameLineHeight: 1.25,
|
nameLineHeight: 1.25,
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO: consider adding a built-in fallback font for emojis
|
|
||||||
export const WINDOWS_EMOJI_FALLBACK_FONT = "Segoe UI Emoji";
|
|
||||||
export const CHINESE_HANDWRITTEN_FALLBACK_FONT = "Xiaolai";
|
|
||||||
|
|
||||||
export const MIN_FONT_SIZE = 1;
|
export const MIN_FONT_SIZE = 1;
|
||||||
export const DEFAULT_FONT_SIZE = 20;
|
export const DEFAULT_FONT_SIZE = 20;
|
||||||
export const DEFAULT_FONT_FAMILY: FontFamilyValues = FONT_FAMILY.Excalifont;
|
export const DEFAULT_FONT_FAMILY: FontFamilyValues = FONT_FAMILY.Excalifont;
|
||||||
|
@ -3,31 +3,30 @@ import { LOCAL_FONT_PROTOCOL } from "./metadata";
|
|||||||
import loadWoff2 from "./wasm/woff2.loader";
|
import loadWoff2 from "./wasm/woff2.loader";
|
||||||
import loadHbSubset from "./wasm/hb-subset.loader";
|
import loadHbSubset from "./wasm/hb-subset.loader";
|
||||||
|
|
||||||
export interface Font {
|
export interface IExcalidrawFontFace {
|
||||||
urls: URL[];
|
urls: URL[];
|
||||||
fontFace: FontFace;
|
fontFace: FontFace;
|
||||||
getContent(codePoints: ReadonlySet<number>): Promise<string>;
|
toCSS(
|
||||||
|
characters: string,
|
||||||
|
codePoints: Set<number>,
|
||||||
|
): Promise<string> | undefined;
|
||||||
}
|
}
|
||||||
export const UNPKG_FALLBACK_URL = `https://unpkg.com/${
|
|
||||||
import.meta.env.VITE_PKG_NAME
|
|
||||||
? `${import.meta.env.VITE_PKG_NAME}@${import.meta.env.PKG_VERSION}` // should be provided by vite during package build
|
|
||||||
: "@excalidraw/excalidraw" // fallback to latest package version (i.e. for app)
|
|
||||||
}/dist/prod/`;
|
|
||||||
|
|
||||||
export interface ExcalidrawFontFace {
|
export class ExcalidrawFontFace implements IExcalidrawFontFace {
|
||||||
uri: string;
|
|
||||||
descriptors?: FontFaceDescriptors;
|
|
||||||
};
|
|
||||||
|
|
||||||
export class ExcalidrawFont implements Font {
|
|
||||||
public readonly urls: URL[];
|
public readonly urls: URL[];
|
||||||
public readonly fontFace: FontFace;
|
public readonly fontFace: FontFace;
|
||||||
|
|
||||||
|
private static readonly UNPKG_FALLBACK_URL = `https://unpkg.com/${
|
||||||
|
import.meta.env.VITE_PKG_NAME
|
||||||
|
? `${import.meta.env.VITE_PKG_NAME}@${import.meta.env.PKG_VERSION}` // should be provided by vite during package build
|
||||||
|
: "@excalidraw/excalidraw" // fallback to latest package version (i.e. for app)
|
||||||
|
}/dist/prod/`;
|
||||||
|
|
||||||
constructor(family: string, uri: string, descriptors?: FontFaceDescriptors) {
|
constructor(family: string, uri: string, descriptors?: FontFaceDescriptors) {
|
||||||
this.urls = ExcalidrawFont.createUrls(uri);
|
this.urls = ExcalidrawFontFace.createUrls(uri);
|
||||||
|
|
||||||
const sources = this.urls
|
const sources = this.urls
|
||||||
.map((url) => `url(${url}) ${ExcalidrawFont.getFormat(url)}`)
|
.map((url) => `url(${url}) ${ExcalidrawFontFace.getFormat(url)}`)
|
||||||
.join(", ");
|
.join(", ");
|
||||||
|
|
||||||
this.fontFace = new FontFace(family, sources, {
|
this.fontFace = new FontFace(family, sources, {
|
||||||
@ -38,6 +37,41 @@ export class ExcalidrawFont implements Font {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates CSS `@font-face` definition with the (subsetted) font source as a data url for the characters within the unicode range.
|
||||||
|
*
|
||||||
|
* Retrieves `undefined` otherwise.
|
||||||
|
*/
|
||||||
|
public toCSS(
|
||||||
|
characters: string,
|
||||||
|
codePoints: Set<number>,
|
||||||
|
): Promise<string> | undefined {
|
||||||
|
const unicodeRangeRegex = this.fontFace.unicodeRange
|
||||||
|
.split(", ")
|
||||||
|
.map((range) => {
|
||||||
|
const [start, end] = range.replace("U+", "").split("-");
|
||||||
|
if (end) {
|
||||||
|
return `\\u${start}-\\u${end}`;
|
||||||
|
}
|
||||||
|
return `\\u${start}`;
|
||||||
|
})
|
||||||
|
.join("");
|
||||||
|
|
||||||
|
if (!new RegExp(`[${unicodeRangeRegex}]`).exec(characters)) {
|
||||||
|
// quick exit, so that this does not count as a pending promsie
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise<string>((resolve) => {
|
||||||
|
this.getContent(codePoints).then((content) => {
|
||||||
|
resolve(`@font-face {
|
||||||
|
font-family: ${this.fontFace.family};
|
||||||
|
src: url(${content});
|
||||||
|
}`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tries to fetch woff2 content, based on the registered urls (from first to last, treated as fallbacks).
|
* Tries to fetch woff2 content, based on the registered urls (from first to last, treated as fallbacks).
|
||||||
*
|
*
|
||||||
@ -45,29 +79,29 @@ export class ExcalidrawFont implements Font {
|
|||||||
*
|
*
|
||||||
* @returns base64 with subsetted glyphs based on the passed codepoint, last defined url otherwise
|
* @returns base64 with subsetted glyphs based on the passed codepoint, last defined url otherwise
|
||||||
*/
|
*/
|
||||||
public async getContent(codePoints: ReadonlySet<number>): Promise<string> {
|
private async getContent(codePoints: ReadonlySet<number>): Promise<string> {
|
||||||
let i = 0;
|
let i = 0;
|
||||||
const errorMessages = [];
|
const errorMessages = [];
|
||||||
|
|
||||||
while (i < this.urls.length) {
|
while (i < this.urls.length) {
|
||||||
const url = this.urls[i];
|
const url = this.urls[i];
|
||||||
|
|
||||||
// it's dataurl (server), the font is inlined as base64, no need to fetch
|
|
||||||
if (url.protocol === "data:") {
|
|
||||||
const arrayBuffer = Buffer.from(
|
|
||||||
url.toString().split(",")[1],
|
|
||||||
"base64",
|
|
||||||
).buffer;
|
|
||||||
|
|
||||||
const base64 = await ExcalidrawFont.subsetGlyphsByCodePoints(
|
|
||||||
arrayBuffer,
|
|
||||||
codePoints,
|
|
||||||
);
|
|
||||||
|
|
||||||
return base64;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// it's dataurl (server), the font is inlined as base64, no need to fetch
|
||||||
|
if (url.protocol === "data:") {
|
||||||
|
const arrayBuffer = Buffer.from(
|
||||||
|
url.toString().split(",")[1],
|
||||||
|
"base64",
|
||||||
|
).buffer;
|
||||||
|
|
||||||
|
const base64 = await ExcalidrawFontFace.subsetGlyphsByCodePoints(
|
||||||
|
arrayBuffer,
|
||||||
|
codePoints,
|
||||||
|
);
|
||||||
|
|
||||||
|
return base64;
|
||||||
|
}
|
||||||
|
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
headers: {
|
headers: {
|
||||||
Accept: "font/woff2",
|
Accept: "font/woff2",
|
||||||
@ -76,7 +110,7 @@ export class ExcalidrawFont implements Font {
|
|||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const arrayBuffer = await response.arrayBuffer();
|
const arrayBuffer = await response.arrayBuffer();
|
||||||
const base64 = await ExcalidrawFont.subsetGlyphsByCodePoints(
|
const base64 = await ExcalidrawFontFace.subsetGlyphsByCodePoints(
|
||||||
arrayBuffer,
|
arrayBuffer,
|
||||||
codePoints,
|
codePoints,
|
||||||
);
|
);
|
||||||
@ -128,11 +162,11 @@ export class ExcalidrawFont implements Font {
|
|||||||
const subsetSnft = subset(decompressedBinary, codePoints);
|
const subsetSnft = subset(decompressedBinary, codePoints);
|
||||||
const compressedBinary = compress(subsetSnft.buffer);
|
const compressedBinary = compress(subsetSnft.buffer);
|
||||||
|
|
||||||
return ExcalidrawFont.toBase64(compressedBinary.buffer);
|
return ExcalidrawFontFace.toBase64(compressedBinary.buffer);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Skipped glyph subsetting", e);
|
console.error("Skipped glyph subsetting", e);
|
||||||
// Fallback to encoding whole font in case of errors
|
// Fallback to encoding whole font in case of errors
|
||||||
return ExcalidrawFont.toBase64(arrayBuffer);
|
return ExcalidrawFontFace.toBase64(arrayBuffer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -178,7 +212,7 @@ export class ExcalidrawFont implements Font {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// fallback url for bundled fonts
|
// fallback url for bundled fonts
|
||||||
urls.push(new URL(assetUrl, UNPKG_FALLBACK_URL));
|
urls.push(new URL(assetUrl, ExcalidrawFontFace.UNPKG_FALLBACK_URL));
|
||||||
|
|
||||||
return urls;
|
return urls;
|
||||||
}
|
}
|
@ -10,12 +10,11 @@ import { isTextElement } from "../element";
|
|||||||
import { getFontString } from "../utils";
|
import { getFontString } from "../utils";
|
||||||
import { FONT_FAMILY } from "../constants";
|
import { FONT_FAMILY } from "../constants";
|
||||||
import { FONT_METADATA, type FontMetadata } from "./metadata";
|
import { FONT_METADATA, type FontMetadata } from "./metadata";
|
||||||
import {
|
|
||||||
ExcalidrawFont,
|
|
||||||
type ExcalidrawFontFace,
|
|
||||||
type Font,
|
|
||||||
} from "./ExcalidrawFont";
|
|
||||||
import { getContainerElement } from "../element/textElement";
|
import { getContainerElement } from "../element/textElement";
|
||||||
|
import {
|
||||||
|
ExcalidrawFontFace,
|
||||||
|
type IExcalidrawFontFace,
|
||||||
|
} from "./ExcalidrawFontFace";
|
||||||
import { CascadiaFontFaces } from "./woff2/Cascadia";
|
import { CascadiaFontFaces } from "./woff2/Cascadia";
|
||||||
import { ComicFontFaces } from "./woff2/Comic";
|
import { ComicFontFaces } from "./woff2/Comic";
|
||||||
import { ExcalifontFontFaces } from "./woff2/Excalifont";
|
import { ExcalifontFontFaces } from "./woff2/Excalifont";
|
||||||
@ -25,6 +24,7 @@ import { LilitaFontFaces } from "./woff2/Lilita";
|
|||||||
import { NunitoFontFaces } from "./woff2/Nunito";
|
import { NunitoFontFaces } from "./woff2/Nunito";
|
||||||
import { VirgilFontFaces } from "./woff2/Virgil";
|
import { VirgilFontFaces } from "./woff2/Virgil";
|
||||||
import { XiaolaiFontFaces } from "./woff2/Xiaolai";
|
import { XiaolaiFontFaces } from "./woff2/Xiaolai";
|
||||||
|
import { EmojiFontFaces } from "./woff2/Emoji";
|
||||||
|
|
||||||
export class Fonts {
|
export class Fonts {
|
||||||
// it's ok to track fonts across multiple instances only once, so let's use
|
// it's ok to track fonts across multiple instances only once, so let's use
|
||||||
@ -36,7 +36,7 @@ export class Fonts {
|
|||||||
number,
|
number,
|
||||||
{
|
{
|
||||||
metadata: FontMetadata;
|
metadata: FontMetadata;
|
||||||
fonts: Font[];
|
fontFaces: IExcalidrawFontFace[];
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
| undefined;
|
| undefined;
|
||||||
@ -148,13 +148,13 @@ export class Fonts {
|
|||||||
fontFamilies: Array<ExcalidrawTextElement["fontFamily"]>,
|
fontFamilies: Array<ExcalidrawTextElement["fontFamily"]>,
|
||||||
) {
|
) {
|
||||||
// add all registered font faces into the `document.fonts` (if not added already)
|
// add all registered font faces into the `document.fonts` (if not added already)
|
||||||
for (const { fonts, metadata } of Fonts.registered.values()) {
|
for (const { fontFaces, metadata } of Fonts.registered.values()) {
|
||||||
// skip registering font faces for local fonts (i.e. Helvetica)
|
// skip registering font faces for local fonts (i.e. Helvetica)
|
||||||
if (metadata.local) {
|
if (metadata.local) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const { fontFace } of fonts) {
|
for (const { fontFace } of fontFaces) {
|
||||||
if (!window.document.fonts.has(fontFace)) {
|
if (!window.document.fonts.has(fontFace)) {
|
||||||
window.document.fonts.add(fontFace);
|
window.document.fonts.add(fontFace);
|
||||||
}
|
}
|
||||||
@ -179,7 +179,7 @@ export class Fonts {
|
|||||||
console.error(
|
console.error(
|
||||||
`Failed to load font "${fontString}" from urls "${Fonts.registered
|
`Failed to load font "${fontString}" from urls "${Fonts.registered
|
||||||
.get(fontFamily)
|
.get(fontFamily)
|
||||||
?.fonts.map((x) => x.urls)}"`,
|
?.fontFaces.map((x) => x.urls)}"`,
|
||||||
e,
|
e,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -199,17 +199,17 @@ export class Fonts {
|
|||||||
const fonts = {
|
const fonts = {
|
||||||
registered: new Map<
|
registered: new Map<
|
||||||
ValueOf<typeof FONT_FAMILY>,
|
ValueOf<typeof FONT_FAMILY>,
|
||||||
{ metadata: FontMetadata; fonts: Font[] }
|
{ metadata: FontMetadata; fontFaces: IExcalidrawFontFace[] }
|
||||||
>(),
|
>(),
|
||||||
};
|
};
|
||||||
|
|
||||||
const init = (
|
const init = (
|
||||||
family: keyof typeof FONT_FAMILY,
|
family: keyof typeof FONT_FAMILY,
|
||||||
...fontFaces: ExcalidrawFontFace[]
|
...fontFacesDescriptors: ExcalidrawFontFaceDescriptor[]
|
||||||
) => {
|
) => {
|
||||||
const metadata = FONT_METADATA[FONT_FAMILY[family]];
|
const metadata = FONT_METADATA[FONT_FAMILY[family]];
|
||||||
|
|
||||||
register.call(fonts, family, metadata, ...fontFaces);
|
register.call(fonts, family, metadata, ...fontFacesDescriptors);
|
||||||
};
|
};
|
||||||
|
|
||||||
init("Cascadia", ...CascadiaFontFaces);
|
init("Cascadia", ...CascadiaFontFaces);
|
||||||
@ -222,7 +222,10 @@ export class Fonts {
|
|||||||
init("Lilita One", ...LilitaFontFaces);
|
init("Lilita One", ...LilitaFontFaces);
|
||||||
init("Nunito", ...NunitoFontFaces);
|
init("Nunito", ...NunitoFontFaces);
|
||||||
init("Virgil", ...VirgilFontFaces);
|
init("Virgil", ...VirgilFontFaces);
|
||||||
|
|
||||||
|
// fallback font faces
|
||||||
init("Xiaolai", ...XiaolaiFontFaces);
|
init("Xiaolai", ...XiaolaiFontFaces);
|
||||||
|
init("Segoe UI Emoji", ...EmojiFontFaces);
|
||||||
|
|
||||||
Fonts._initialized = true;
|
Fonts._initialized = true;
|
||||||
|
|
||||||
@ -248,7 +251,7 @@ export class Fonts {
|
|||||||
*
|
*
|
||||||
* @param family font family
|
* @param family font family
|
||||||
* @param metadata font metadata
|
* @param metadata font metadata
|
||||||
* @param faces font faces
|
* @param fontFacesDecriptors font faces descriptors
|
||||||
*/
|
*/
|
||||||
function register(
|
function register(
|
||||||
this:
|
this:
|
||||||
@ -256,12 +259,12 @@ function register(
|
|||||||
| {
|
| {
|
||||||
registered: Map<
|
registered: Map<
|
||||||
ValueOf<typeof FONT_FAMILY>,
|
ValueOf<typeof FONT_FAMILY>,
|
||||||
{ metadata: FontMetadata; fonts: Font[] }
|
{ metadata: FontMetadata; fontFaces: IExcalidrawFontFace[] }
|
||||||
>;
|
>;
|
||||||
},
|
},
|
||||||
family: string,
|
family: string,
|
||||||
metadata: FontMetadata,
|
metadata: FontMetadata,
|
||||||
...faces: ExcalidrawFontFace[]
|
...fontFacesDecriptors: ExcalidrawFontFaceDescriptor[]
|
||||||
) {
|
) {
|
||||||
// TODO: likely we will need to abandon number "id" in order to support custom fonts
|
// TODO: likely we will need to abandon number "id" in order to support custom fonts
|
||||||
const familyId = FONT_FAMILY[family as keyof typeof FONT_FAMILY];
|
const familyId = FONT_FAMILY[family as keyof typeof FONT_FAMILY];
|
||||||
@ -270,8 +273,9 @@ function register(
|
|||||||
if (!registeredFamily) {
|
if (!registeredFamily) {
|
||||||
this.registered.set(familyId, {
|
this.registered.set(familyId, {
|
||||||
metadata,
|
metadata,
|
||||||
fonts: faces.map(
|
fontFaces: fontFacesDecriptors.map(
|
||||||
({ uri, descriptors }) => new ExcalidrawFont(family, uri, descriptors),
|
({ uri, descriptors }) =>
|
||||||
|
new ExcalidrawFontFace(family, uri, descriptors),
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -309,3 +313,8 @@ export const getLineHeight = (fontFamily: FontFamilyValues) => {
|
|||||||
|
|
||||||
return lineHeight as ExcalidrawTextElement["lineHeight"];
|
return lineHeight as ExcalidrawTextElement["lineHeight"];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export interface ExcalidrawFontFaceDescriptor {
|
||||||
|
uri: string;
|
||||||
|
descriptors?: FontFaceDescriptors;
|
||||||
|
}
|
||||||
|
@ -119,6 +119,17 @@ export const FONT_METADATA: Record<number, FontMetadata> = {
|
|||||||
},
|
},
|
||||||
fallback: true,
|
fallback: true,
|
||||||
},
|
},
|
||||||
|
[FONT_FAMILY["Segoe UI Emoji"]]: {
|
||||||
|
metrics: {
|
||||||
|
// reusing Excalifont metrics
|
||||||
|
unitsPerEm: 1000,
|
||||||
|
ascender: 886,
|
||||||
|
descender: -374,
|
||||||
|
lineHeight: 1.25,
|
||||||
|
},
|
||||||
|
local: true,
|
||||||
|
fallback: true,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Unicode ranges defined by google fonts */
|
/** Unicode ranges defined by google fonts */
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import CascadiaCodeRegular from "./CascadiaCode-Regular.woff2";
|
import CascadiaCodeRegular from "./CascadiaCode-Regular.woff2";
|
||||||
import type { ExcalidrawFontFace } from "../../ExcalidrawFont";
|
import { type ExcalidrawFontFaceDescriptor } from "../..";
|
||||||
|
|
||||||
export const CascadiaFontFaces: ExcalidrawFontFace[] = [
|
export const CascadiaFontFaces: ExcalidrawFontFaceDescriptor[] = [
|
||||||
{
|
{
|
||||||
uri: CascadiaCodeRegular,
|
uri: CascadiaCodeRegular,
|
||||||
},
|
},
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import ComicShannsRegular from "./ComicShanns-Regular.woff2";
|
import ComicShannsRegular from "./ComicShanns-Regular.woff2";
|
||||||
import type { ExcalidrawFontFace } from "../../ExcalidrawFont";
|
import { type ExcalidrawFontFaceDescriptor } from "../..";
|
||||||
|
|
||||||
export const ComicFontFaces: ExcalidrawFontFace[] = [
|
export const ComicFontFaces: ExcalidrawFontFaceDescriptor[] = [
|
||||||
{
|
{
|
||||||
uri: ComicShannsRegular,
|
uri: ComicShannsRegular,
|
||||||
},
|
},
|
||||||
|
8
packages/excalidraw/fonts/woff2/Emoji/index.ts
Normal file
8
packages/excalidraw/fonts/woff2/Emoji/index.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { LOCAL_FONT_PROTOCOL } from "../../metadata";
|
||||||
|
import { type ExcalidrawFontFaceDescriptor } from "../..";
|
||||||
|
|
||||||
|
export const EmojiFontFaces: ExcalidrawFontFaceDescriptor[] = [
|
||||||
|
{
|
||||||
|
uri: LOCAL_FONT_PROTOCOL,
|
||||||
|
},
|
||||||
|
];
|
@ -1,7 +1,7 @@
|
|||||||
import Excalifont from "./Excalifont-Regular.woff2";
|
import Excalifont from "./Excalifont-Regular.woff2";
|
||||||
import type { ExcalidrawFontFace } from "../../ExcalidrawFont";
|
import { type ExcalidrawFontFaceDescriptor } from "../..";
|
||||||
|
|
||||||
export const ExcalifontFontFaces: ExcalidrawFontFace[] = [
|
export const ExcalifontFontFaces: ExcalidrawFontFaceDescriptor[] = [
|
||||||
{
|
{
|
||||||
uri: Excalifont,
|
uri: Excalifont,
|
||||||
},
|
},
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { LOCAL_FONT_PROTOCOL } from "../../metadata";
|
import { LOCAL_FONT_PROTOCOL } from "../../metadata";
|
||||||
import type { ExcalidrawFontFace } from "../../ExcalidrawFont";
|
import { type ExcalidrawFontFaceDescriptor } from "../..";
|
||||||
|
|
||||||
export const HelveticaFontFaces: ExcalidrawFontFace[] = [
|
export const HelveticaFontFaces: ExcalidrawFontFaceDescriptor[] = [
|
||||||
{
|
{
|
||||||
uri: LOCAL_FONT_PROTOCOL,
|
uri: LOCAL_FONT_PROTOCOL,
|
||||||
},
|
},
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import LiberationSansRegular from "./LiberationSans-Regular.woff2";
|
import LiberationSansRegular from "./LiberationSans-Regular.woff2";
|
||||||
import type { ExcalidrawFontFace } from "../../ExcalidrawFont";
|
import { type ExcalidrawFontFaceDescriptor } from "../..";
|
||||||
|
|
||||||
export const LiberationFontFaces: ExcalidrawFontFace[] = [
|
export const LiberationFontFaces: ExcalidrawFontFaceDescriptor[] = [
|
||||||
{
|
{
|
||||||
uri: LiberationSansRegular,
|
uri: LiberationSansRegular,
|
||||||
},
|
},
|
||||||
|
@ -2,9 +2,9 @@ import LilitaLatin from "https://fonts.gstatic.com/s/lilitaone/v15/i7dPIFZ9Zz-WB
|
|||||||
import LilitaLatinExt from "https://fonts.gstatic.com/s/lilitaone/v15/i7dPIFZ9Zz-WBtRtedDbYE98RXi4EwSsbg.woff2";
|
import LilitaLatinExt from "https://fonts.gstatic.com/s/lilitaone/v15/i7dPIFZ9Zz-WBtRtedDbYE98RXi4EwSsbg.woff2";
|
||||||
|
|
||||||
import { GOOGLE_FONTS_RANGES } from "../../metadata";
|
import { GOOGLE_FONTS_RANGES } from "../../metadata";
|
||||||
import type { ExcalidrawFontFace } from "../../ExcalidrawFont";
|
import { type ExcalidrawFontFaceDescriptor } from "../..";
|
||||||
|
|
||||||
export const LilitaFontFaces: ExcalidrawFontFace[] = [
|
export const LilitaFontFaces: ExcalidrawFontFaceDescriptor[] = [
|
||||||
{
|
{
|
||||||
uri: LilitaLatinExt,
|
uri: LilitaLatinExt,
|
||||||
descriptors: { unicodeRange: GOOGLE_FONTS_RANGES.LATIN_EXT },
|
descriptors: { unicodeRange: GOOGLE_FONTS_RANGES.LATIN_EXT },
|
||||||
|
@ -5,9 +5,9 @@ import CyrilicExt from "https://fonts.gstatic.com/s/nunito/v26/XRXI3I6Li01BKofiO
|
|||||||
import Vietnamese from "https://fonts.gstatic.com/s/nunito/v26/XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTs3j6zbXWjgevT5.woff2";
|
import Vietnamese from "https://fonts.gstatic.com/s/nunito/v26/XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTs3j6zbXWjgevT5.woff2";
|
||||||
|
|
||||||
import { GOOGLE_FONTS_RANGES } from "../../metadata";
|
import { GOOGLE_FONTS_RANGES } from "../../metadata";
|
||||||
import type { ExcalidrawFontFace } from "../../ExcalidrawFont";
|
import { type ExcalidrawFontFaceDescriptor } from "../..";
|
||||||
|
|
||||||
export const NunitoFontFaces: ExcalidrawFontFace[] = [
|
export const NunitoFontFaces: ExcalidrawFontFaceDescriptor[] = [
|
||||||
{
|
{
|
||||||
uri: CyrilicExt,
|
uri: CyrilicExt,
|
||||||
descriptors: {
|
descriptors: {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import Virgil from "./Virgil-Regular.woff2";
|
import Virgil from "./Virgil-Regular.woff2";
|
||||||
import type { ExcalidrawFontFace } from "../../ExcalidrawFont";
|
import { type ExcalidrawFontFaceDescriptor } from "../..";
|
||||||
|
|
||||||
export const VirgilFontFaces: ExcalidrawFontFace[] = [
|
export const VirgilFontFaces: ExcalidrawFontFaceDescriptor[] = [
|
||||||
{
|
{
|
||||||
uri: Virgil,
|
uri: Virgil,
|
||||||
},
|
},
|
||||||
|
@ -218,7 +218,7 @@ import _213 from "./31dc7ab58ea136b7dca23037305e023b.woff2";
|
|||||||
import _214 from "./d53f5bd45db0e96874d1174341701b59.woff2";
|
import _214 from "./d53f5bd45db0e96874d1174341701b59.woff2";
|
||||||
import _215 from "./6fe856ceea3ef870142b02ddecc1b006.woff2";
|
import _215 from "./6fe856ceea3ef870142b02ddecc1b006.woff2";
|
||||||
|
|
||||||
import type { ExcalidrawFontFace } from "../../ExcalidrawFont";
|
import { type ExcalidrawFontFaceDescriptor } from "../..";
|
||||||
|
|
||||||
/* Generated By cn-font-split@5.2.1 https://www.npmjs.com/package/cn-font-split
|
/* Generated By cn-font-split@5.2.1 https://www.npmjs.com/package/cn-font-split
|
||||||
CreateTime: Thu, 22 Aug 2024 10:58:35 GMT;
|
CreateTime: Thu, 22 Aug 2024 10:58:35 GMT;
|
||||||
@ -240,7 +240,7 @@ licenseURL: http://scripts.sil.org/OFL
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
// TODO_CHINESE: consider having import for each (to automate build step)
|
// TODO_CHINESE: consider having import for each (to automate build step)
|
||||||
export const XiaolaiFontFaces: ExcalidrawFontFace[] = [
|
export const XiaolaiFontFaces: ExcalidrawFontFaceDescriptor[] = [
|
||||||
{
|
{
|
||||||
uri: _0,
|
uri: _0,
|
||||||
descriptors: {
|
descriptors: {
|
||||||
|
@ -67,6 +67,7 @@
|
|||||||
"canvas-roundrect-polyfill": "0.0.1",
|
"canvas-roundrect-polyfill": "0.0.1",
|
||||||
"clsx": "1.1.1",
|
"clsx": "1.1.1",
|
||||||
"cross-env": "7.0.3",
|
"cross-env": "7.0.3",
|
||||||
|
"es6-promise-pool": "2.5.0",
|
||||||
"fractional-indexing": "3.2.0",
|
"fractional-indexing": "3.2.0",
|
||||||
"fuzzy": "0.1.3",
|
"fuzzy": "0.1.3",
|
||||||
"image-blob-reduce": "3.0.1",
|
"image-blob-reduce": "3.0.1",
|
||||||
|
@ -9,7 +9,13 @@ import type {
|
|||||||
import type { Bounds } from "../element/bounds";
|
import type { Bounds } from "../element/bounds";
|
||||||
import { getCommonBounds, getElementAbsoluteCoords } from "../element/bounds";
|
import { getCommonBounds, getElementAbsoluteCoords } from "../element/bounds";
|
||||||
import { renderSceneToSvg } from "../renderer/staticSvgScene";
|
import { renderSceneToSvg } from "../renderer/staticSvgScene";
|
||||||
import { arrayToMap, distance, getFontString, toBrandedType } from "../utils";
|
import {
|
||||||
|
arrayToMap,
|
||||||
|
distance,
|
||||||
|
getFontString,
|
||||||
|
PromisePool,
|
||||||
|
toBrandedType,
|
||||||
|
} from "../utils";
|
||||||
import type { AppState, BinaryFiles } from "../types";
|
import type { AppState, BinaryFiles } from "../types";
|
||||||
import {
|
import {
|
||||||
DEFAULT_EXPORT_PADDING,
|
DEFAULT_EXPORT_PADDING,
|
||||||
@ -18,6 +24,7 @@ import {
|
|||||||
SVG_NS,
|
SVG_NS,
|
||||||
THEME,
|
THEME,
|
||||||
THEME_FILTER,
|
THEME_FILTER,
|
||||||
|
FONT_FAMILY_FALLBACKS,
|
||||||
} from "../constants";
|
} from "../constants";
|
||||||
import { getDefaultAppState } from "../appState";
|
import { getDefaultAppState } from "../appState";
|
||||||
import { serializeAsJSON } from "../data/json";
|
import { serializeAsJSON } from "../data/json";
|
||||||
@ -39,7 +46,6 @@ import type { RenderableElementsMap } from "./types";
|
|||||||
import { syncInvalidIndices } from "../fractionalIndex";
|
import { syncInvalidIndices } from "../fractionalIndex";
|
||||||
import { renderStaticScene } from "../renderer/staticScene";
|
import { renderStaticScene } from "../renderer/staticScene";
|
||||||
import { Fonts } from "../fonts";
|
import { Fonts } from "../fonts";
|
||||||
import type { Font } from "../fonts/ExcalidrawFont";
|
|
||||||
|
|
||||||
const SVG_EXPORT_TAG = `<!-- svg-source:excalidraw -->`;
|
const SVG_EXPORT_TAG = `<!-- svg-source:excalidraw -->`;
|
||||||
|
|
||||||
@ -439,6 +445,7 @@ const getFontFaces = async (
|
|||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly ExcalidrawElement[],
|
||||||
): Promise<string[]> => {
|
): Promise<string[]> => {
|
||||||
const fontFamilies = new Set<number>();
|
const fontFamilies = new Set<number>();
|
||||||
|
const characters = new Set<string>();
|
||||||
const codePoints = new Set<number>();
|
const codePoints = new Set<number>();
|
||||||
|
|
||||||
for (const element of elements) {
|
for (const element of elements) {
|
||||||
@ -449,52 +456,85 @@ const getFontFaces = async (
|
|||||||
fontFamilies.add(element.fontFamily);
|
fontFamilies.add(element.fontFamily);
|
||||||
|
|
||||||
// gather unique codepoints only when inlining fonts
|
// gather unique codepoints only when inlining fonts
|
||||||
for (const codePoint of Array.from(element.originalText, (u) =>
|
for (const char of element.originalText) {
|
||||||
u.codePointAt(0),
|
if (!characters.has(char)) {
|
||||||
)) {
|
characters.add(char);
|
||||||
if (codePoint) {
|
codePoints.add(char.codePointAt(0)!);
|
||||||
codePoints.add(codePoint);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getSource = (font: Font) => {
|
const uniqueChars = Array.from(characters).join("");
|
||||||
try {
|
|
||||||
// retrieve font source as dataurl based on the used codepoints
|
|
||||||
return font.getContent(codePoints);
|
|
||||||
} catch {
|
|
||||||
// fallback to font source as a url
|
|
||||||
return font.urls[0].toString();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const fontFaces = await Promise.all(
|
// quick check for Han (might match a bit more, but that's fine)
|
||||||
Array.from(fontFamilies).map(async (x) => {
|
if (uniqueChars.match(/\p{Script=Han}/u)) {
|
||||||
const { fonts, metadata } = Fonts.registered.get(x) ?? {};
|
fontFamilies.add(FONT_FAMILY.Xiaolai);
|
||||||
|
}
|
||||||
|
|
||||||
if (!Array.isArray(fonts)) {
|
const iterator = fontFacesIterator(fontFamilies, uniqueChars, codePoints);
|
||||||
console.error(
|
|
||||||
`Couldn't find registered fonts for font-family "${x}"`,
|
|
||||||
Fonts.registered,
|
|
||||||
);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (metadata?.local) {
|
// don't trigger hundreds/thousands of concurrent requests, instead go 3 requests at a time, in a controlled manner
|
||||||
// don't inline local fonts
|
// in other words, does not block the main thread and avoids related issues, including potential rate limits
|
||||||
return [];
|
const concurrency = 3;
|
||||||
}
|
const fontFaces = await new PromisePool(iterator, concurrency).all();
|
||||||
|
|
||||||
return Promise.all(
|
// dedup just in case (i.e. could be the same font faces with 0 glyphs)
|
||||||
fonts.map(
|
return Array.from(new Set(fontFaces));
|
||||||
async (font) => `@font-face {
|
|
||||||
font-family: ${font.fontFace.family};
|
|
||||||
src: url(${await getSource(font)});
|
|
||||||
}`,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
return fontFaces.flat();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function* fontFacesIterator(
|
||||||
|
families: Set<number>,
|
||||||
|
characters: string,
|
||||||
|
codePoints: Set<number>,
|
||||||
|
): Generator<Promise<[number, string]>> {
|
||||||
|
// the order between the families 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
|
||||||
|
const reversedFallbacks = Array.from(FONT_FAMILY_FALLBACKS.ordered).reverse();
|
||||||
|
|
||||||
|
for (const [familyIndex, family] of Array.from(families).entries()) {
|
||||||
|
const fallbackIndex = reversedFallbacks.findIndex(
|
||||||
|
(fallback) => fallback === family,
|
||||||
|
);
|
||||||
|
|
||||||
|
let order: number;
|
||||||
|
|
||||||
|
if (fallbackIndex !== -1) {
|
||||||
|
// making sure the fallback fonts are defined first
|
||||||
|
order = fallbackIndex;
|
||||||
|
} else {
|
||||||
|
// making sure the built-in font faces are always defined after fallback fonts
|
||||||
|
order = FONT_FAMILY_FALLBACKS.ordered.length + familyIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
// making a safe buffer in between the families, assuming there won't be more than 10k font faces per family
|
||||||
|
order *= 10_000;
|
||||||
|
|
||||||
|
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()) {
|
||||||
|
const pendingFontFace = fontFace.toCSS(characters, codePoints);
|
||||||
|
|
||||||
|
// so that we don't add undefined to the pool
|
||||||
|
if (pendingFontFace) {
|
||||||
|
yield new Promise(async (resolve) => {
|
||||||
|
const promiseIndex = order + fontFaceIndex;
|
||||||
|
const fontFaceCSS = await pendingFontFace;
|
||||||
|
resolve([promiseIndex, fontFaceCSS]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
|
import Pool from "es6-promise-pool";
|
||||||
import { COLOR_PALETTE } from "./colors";
|
import { COLOR_PALETTE } from "./colors";
|
||||||
import type { EVENT } from "./constants";
|
import type { EVENT } from "./constants";
|
||||||
import {
|
import {
|
||||||
CHINESE_HANDWRITTEN_FALLBACK_FONT,
|
|
||||||
DEFAULT_VERSION,
|
DEFAULT_VERSION,
|
||||||
FONT_FAMILY,
|
FONT_FAMILY,
|
||||||
|
FONT_FAMILY_FALLBACKS,
|
||||||
isDarwin,
|
isDarwin,
|
||||||
WINDOWS_EMOJI_FALLBACK_FONT,
|
WINDOWS_EMOJI_FALLBACK_FONT,
|
||||||
} from "./constants";
|
} from "./constants";
|
||||||
@ -89,8 +90,8 @@ export const getFontFamilyString = ({
|
|||||||
}) => {
|
}) => {
|
||||||
for (const [fontFamilyString, id] of Object.entries(FONT_FAMILY)) {
|
for (const [fontFamilyString, id] of Object.entries(FONT_FAMILY)) {
|
||||||
if (id === fontFamily) {
|
if (id === fontFamily) {
|
||||||
// TODO: we should fallback first to generic family names first, rather than directly to the emoji font
|
// TODO: we should fallback first to generic family names first
|
||||||
return `${fontFamilyString}, ${CHINESE_HANDWRITTEN_FALLBACK_FONT}, ${WINDOWS_EMOJI_FALLBACK_FONT}`;
|
return `${fontFamilyString}, ${FONT_FAMILY_FALLBACKS.string}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return WINDOWS_EMOJI_FALLBACK_FONT;
|
return WINDOWS_EMOJI_FALLBACK_FONT;
|
||||||
@ -1167,3 +1168,43 @@ export const promiseTry = async <TValue, TArgs extends unknown[]>(
|
|||||||
|
|
||||||
export const isAnyTrue = (...args: boolean[]): boolean =>
|
export const isAnyTrue = (...args: boolean[]): boolean =>
|
||||||
Math.max(...args.map((arg) => (arg ? 1 : 0))) > 0;
|
Math.max(...args.map((arg) => (arg ? 1 : 0))) > 0;
|
||||||
|
|
||||||
|
// extending the missing types
|
||||||
|
// relying on the [Index, T] to keep a correct order
|
||||||
|
type TPromisePool<T, Index = number> = Pool<[Index, T][]> & {
|
||||||
|
addEventListener: (
|
||||||
|
type: "fulfilled",
|
||||||
|
listener: (event: { data: { result: [Index, T] } }) => void,
|
||||||
|
) => (event: { data: { result: [Index, T] } }) => void;
|
||||||
|
removeEventListener: (
|
||||||
|
type: "fulfilled",
|
||||||
|
listener: (event: { data: { result: [Index, T] } }) => void,
|
||||||
|
) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class PromisePool<T> {
|
||||||
|
private readonly pool: TPromisePool<T>;
|
||||||
|
private readonly entries: Record<number, T> = {};
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
source: IterableIterator<Promise<[number, T]>>,
|
||||||
|
concurrency: number,
|
||||||
|
) {
|
||||||
|
this.pool = new Pool(
|
||||||
|
source as unknown as () => void | PromiseLike<[number, T][]>,
|
||||||
|
concurrency,
|
||||||
|
) as TPromisePool<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
public all() {
|
||||||
|
const listener = this.pool.addEventListener("fulfilled", (event) => {
|
||||||
|
const [index, value] = event.data.result;
|
||||||
|
this.entries[index] = value;
|
||||||
|
});
|
||||||
|
|
||||||
|
return this.pool.start().then(() => {
|
||||||
|
setTimeout(() => this.pool.removeEventListener("fulfilled", listener));
|
||||||
|
return Object.values(this.entries);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -5521,6 +5521,11 @@ es-to-primitive@^1.2.1:
|
|||||||
is-date-object "^1.0.1"
|
is-date-object "^1.0.1"
|
||||||
is-symbol "^1.0.2"
|
is-symbol "^1.0.2"
|
||||||
|
|
||||||
|
es6-promise-pool@2.5.0:
|
||||||
|
version "2.5.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/es6-promise-pool/-/es6-promise-pool-2.5.0.tgz#147c612b36b47f105027f9d2bf54a598a99d9ccb"
|
||||||
|
integrity sha512-VHErXfzR/6r/+yyzPKeBvO0lgjfC5cbDCQWjWwMZWSb6YU39TGIl51OUmCfWCq4ylMdJSB8zkz2vIuIeIxXApA==
|
||||||
|
|
||||||
esbuild-plugin-external-global@1.0.1:
|
esbuild-plugin-external-global@1.0.1:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/esbuild-plugin-external-global/-/esbuild-plugin-external-global-1.0.1.tgz#e3bba0e3a561f61b395bec0984a90ed0de06c4ce"
|
resolved "https://registry.yarnpkg.com/esbuild-plugin-external-global/-/esbuild-plugin-external-global-1.0.1.tgz#e3bba0e3a561f61b395bec0984a90ed0de06c4ce"
|
||||||
|
Loading…
Reference in New Issue
Block a user