@ -1,10 +1,9 @@
import { stringToBase64 , toByteString } from "../data/encode" ;
import { LOCAL_FONT_PROTOCOL } from "./metadata" ;
export interface Font {
urls : URL [ ] ;
fontFace : FontFace ;
getContent ( ) : Promise < string > ;
getContent ( codePoints : ReadonlySet < number > ) : Promise < string > ;
}
export const UNPKG_PROD_URL = ` https://unpkg.com/ ${
import . meta . env . VITE_PKG_NAME
@ -12,6 +11,10 @@ export const UNPKG_PROD_URL = `https://unpkg.com/${
: "@excalidraw/excalidraw" // fallback to latest package version (i.e. for app)
} / dist / prod / ` ;
/** caches for lazy loaded chunks, reused across concurrent calls and separate editor instances */
let fontEditorCache : Promise < typeof import ( " fonteditor - core " ) > | null = null ;
let brotliCache : Promise < typeof import ( " fonteditor - core " ) .woff2 > | null = null ;
export class ExcalidrawFont implements Font {
public readonly urls : URL [ ] ;
public readonly fontFace : FontFace ;
@ -33,20 +36,31 @@ export class ExcalidrawFont implements Font {
/ * *
* Tries to fetch woff2 content , based on the registered urls .
* Returns last defined url in case of errors .
*
* Note : uses browser APIs for base64 encoding - use dataurl outside the browser environment .
* NOTE : assumes usage of ` dataurl ` outside the browser environment
*
* @returns base64 with subsetted glyphs based on the passed codepoint , last defined url otherwise
* /
public async getContent ( ) : Promise < string > {
public async getContent ( codePoints : ReadonlySet < number > ) : Promise < string > {
let i = 0 ;
const errorMessages = [ ] ;
while ( i < this . urls . length ) {
const url = this . urls [ i ] ;
// it's dataurl, the font is inlined as base64, no need to fetch
if ( url . protocol === "data:" ) {
// it's dataurl, the font is inlined as base64, no need to fetch
return url . toString ( ) ;
const arrayBuffer = Buffer . from (
url . toString ( ) . split ( "," ) [ 1 ] ,
"base64" ,
) . buffer ;
const base64 = await ExcalidrawFont . subsetGlyphsByCodePoints (
arrayBuffer ,
codePoints ,
) ;
return base64 ;
}
try {
@ -57,13 +71,12 @@ export class ExcalidrawFont implements Font {
} ) ;
if ( response . ok ) {
const mimeType = await response . headers . get ( "Content-Type" ) ;
const buffer = await response . arrayBuffer ( ) ;
return ` data: ${ mimeType } ;base64, ${ await stringToBase64 (
await toByteString ( buffer ) ,
true ,
) } ` ;
const arrayBuffer = await response . arrayBuffer ( ) ;
const base64 = await ExcalidrawFont . subsetGlyphsByCodePoints (
arrayBuffer ,
codePoints ,
) ;
return base64 ;
}
// response not ok, try to continue
@ -89,6 +102,45 @@ export class ExcalidrawFont implements Font {
return this . urls . length ? this . urls [ this . urls . length - 1 ] . toString ( ) : "" ;
}
/ * *
* Converts a font data as arraybuffer into a dataurl ( base64 ) with subsetted glyphs based on the specified ` codePoints ` .
*
* NOTE : only glyphs are subsetted , other metadata as GPOS tables stay , consider filtering those as well in the future
*
* @param arrayBuffer font data buffer , preferrably in the woff2 format , though others should work as well
* @param codePoints codepoints used to subset the glyphs
*
* @returns font with subsetted glyphs converted into a dataurl
* /
private static async subsetGlyphsByCodePoints (
arrayBuffer : ArrayBuffer ,
codePoints : ReadonlySet < number > ,
) : Promise < string > {
// checks for the cache first to avoid triggering the import multiple times in case of concurrent calls
if ( ! fontEditorCache ) {
fontEditorCache = import ( "fonteditor-core" ) ;
}
const { Font , woff2 } = await fontEditorCache ;
// checks for the cache first to avoid triggering the init multiple times in case of concurrent calls
if ( ! brotliCache ) {
brotliCache = woff2 . init ( "/wasm/woff2.wasm" ) ;
}
await brotliCache ;
const font = Font . create ( arrayBuffer , {
type : "woff2" ,
kerning : true ,
hinting : true ,
// subset the glyhs based on the specified codepoints!
subset : [ . . . codePoints ] ,
} ) ;
return font . toBase64 ( { type : "woff2" , hinting : true } ) ;
}
private static createUrls ( uri : string ) : URL [ ] {
if ( uri . startsWith ( LOCAL_FONT_PROTOCOL ) ) {
// no url for local fonts