mirror of
				https://github.com/excalidraw/excalidraw.git
				synced 2025-11-04 04:44:31 +01:00 
			
		
		
		
	feat: multiple fonts fallbacks (#8286)
This commit is contained in:
		@@ -95,6 +95,11 @@
 | 
			
		||||
        color: #fff;
 | 
			
		||||
      }
 | 
			
		||||
    </style>
 | 
			
		||||
 | 
			
		||||
    <!-- Warmup the connection for Google fonts -->
 | 
			
		||||
    <link rel="preconnect" href="https://fonts.googleapis.com" />
 | 
			
		||||
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
 | 
			
		||||
 | 
			
		||||
    <!------------------------------------------------------------------------->
 | 
			
		||||
    <% if (typeof PROD != 'undefined' && PROD == true) { %>
 | 
			
		||||
    <script>
 | 
			
		||||
@@ -114,52 +119,16 @@
 | 
			
		||||
      ) {
 | 
			
		||||
        window.location.href = "https://app.excalidraw.com";
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // point into our CDN in prod
 | 
			
		||||
      window.EXCALIDRAW_ASSET_PATH =
 | 
			
		||||
        "https://excalidraw.nyc3.cdn.digitaloceanspaces.com/fonts/oss/";
 | 
			
		||||
    </script>
 | 
			
		||||
 | 
			
		||||
    <!-- Following placeholder is replaced during the build step -->
 | 
			
		||||
    <!-- PLACEHOLDER:EXCALIDRAW_APP_FONTS -->
 | 
			
		||||
 | 
			
		||||
    <% } else { %>
 | 
			
		||||
    <script>
 | 
			
		||||
      window.EXCALIDRAW_ASSET_PATH = window.origin;
 | 
			
		||||
    </script>
 | 
			
		||||
    <% } %>
 | 
			
		||||
 | 
			
		||||
    <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
 | 
			
		||||
    <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
 | 
			
		||||
    <link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
 | 
			
		||||
 | 
			
		||||
    <!-- Excalidraw version -->
 | 
			
		||||
    <meta name="version" content="{version}" />
 | 
			
		||||
 | 
			
		||||
    <!-- Warmup the connection for Google fonts -->
 | 
			
		||||
    <link rel="preconnect" href="https://fonts.googleapis.com" />
 | 
			
		||||
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
 | 
			
		||||
 | 
			
		||||
    <!-- Preload all default fonts and Virgil for backwards compatibility to avoid swap on init -->
 | 
			
		||||
    <% if (typeof PROD != 'undefined' && PROD == true) { %>
 | 
			
		||||
    <link
 | 
			
		||||
      rel="preload"
 | 
			
		||||
      href="https://excalidraw.nyc3.cdn.digitaloceanspaces.com/fonts/oss/Excalifont-Regular-C9eKQy_N.woff2"
 | 
			
		||||
      as="font"
 | 
			
		||||
      type="font/woff2"
 | 
			
		||||
      crossorigin="anonymous"
 | 
			
		||||
    />
 | 
			
		||||
    <link
 | 
			
		||||
      rel="preload"
 | 
			
		||||
      href="https://excalidraw.nyc3.cdn.digitaloceanspaces.com/fonts/oss/Virgil-Regular-hO16qHwV.woff2"
 | 
			
		||||
      as="font"
 | 
			
		||||
      type="font/woff2"
 | 
			
		||||
      crossorigin="anonymous"
 | 
			
		||||
    />
 | 
			
		||||
    <link
 | 
			
		||||
      rel="preload"
 | 
			
		||||
      href="https://excalidraw.nyc3.cdn.digitaloceanspaces.com/fonts/oss/ComicShanns-Regular-D0c8wzsC.woff2"
 | 
			
		||||
      as="font"
 | 
			
		||||
      type="font/woff2"
 | 
			
		||||
      crossorigin="anonymous"
 | 
			
		||||
    />
 | 
			
		||||
    <% } else { %>
 | 
			
		||||
    <!-- in DEV we need to preload from the local server and without the hash -->
 | 
			
		||||
    <link
 | 
			
		||||
      rel="preload"
 | 
			
		||||
@@ -184,7 +153,7 @@
 | 
			
		||||
    />
 | 
			
		||||
    <% } %>
 | 
			
		||||
 | 
			
		||||
    <!-- For Nunito only preload the latin range, which should be enough for now -->
 | 
			
		||||
    <!-- For Nunito only preload the latin range, which should be good enough for now -->
 | 
			
		||||
    <link
 | 
			
		||||
      rel="preload"
 | 
			
		||||
      href="https://fonts.gstatic.com/s/nunito/v26/XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTQ3j6zbXWjgeg.woff2"
 | 
			
		||||
@@ -200,6 +169,13 @@
 | 
			
		||||
      type="text/css"
 | 
			
		||||
    />
 | 
			
		||||
 | 
			
		||||
    <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
 | 
			
		||||
    <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
 | 
			
		||||
    <link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
 | 
			
		||||
 | 
			
		||||
    <!-- Excalidraw version -->
 | 
			
		||||
    <meta name="version" content="{version}" />
 | 
			
		||||
 | 
			
		||||
    <% if (typeof VITE_APP_DEV_DISABLE_LIVE_RELOAD != 'undefined' &&
 | 
			
		||||
    VITE_APP_DEV_DISABLE_LIVE_RELOAD == true) { %>
 | 
			
		||||
    <script>
 | 
			
		||||
 
 | 
			
		||||
@@ -63,15 +63,15 @@ export const FontPickerList = React.memo(
 | 
			
		||||
      () =>
 | 
			
		||||
        Array.from(Fonts.registered.entries())
 | 
			
		||||
          .filter(([_, { metadata }]) => !metadata.serverSide)
 | 
			
		||||
          .map(([familyId, { metadata, fontFaces }]) => {
 | 
			
		||||
            const font = {
 | 
			
		||||
          .map(([familyId, { metadata, fonts }]) => {
 | 
			
		||||
            const fontDescriptor = {
 | 
			
		||||
              value: familyId,
 | 
			
		||||
              icon: metadata.icon,
 | 
			
		||||
              text: fontFaces[0].fontFace.family,
 | 
			
		||||
              text: fonts[0].fontFace.family,
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            if (metadata.deprecated) {
 | 
			
		||||
              Object.assign(font, {
 | 
			
		||||
              Object.assign(fontDescriptor, {
 | 
			
		||||
                deprecated: metadata.deprecated,
 | 
			
		||||
                badge: {
 | 
			
		||||
                  type: DropDownMenuItemBadgeType.RED,
 | 
			
		||||
@@ -80,7 +80,7 @@ export const FontPickerList = React.memo(
 | 
			
		||||
              });
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return font as FontDescriptor;
 | 
			
		||||
            return fontDescriptor as FontDescriptor;
 | 
			
		||||
          })
 | 
			
		||||
          .sort((a, b) =>
 | 
			
		||||
            a.text.toLowerCase() > b.text.toLowerCase() ? 1 : -1,
 | 
			
		||||
 
 | 
			
		||||
@@ -1,41 +1,29 @@
 | 
			
		||||
import { stringToBase64, toByteString } from "../data/encode";
 | 
			
		||||
import { LOCAL_FONT_PROTOCOL } from "./metadata";
 | 
			
		||||
 | 
			
		||||
export interface Font {
 | 
			
		||||
  url: URL;
 | 
			
		||||
  urls: URL[];
 | 
			
		||||
  fontFace: FontFace;
 | 
			
		||||
  getContent(): Promise<string>;
 | 
			
		||||
}
 | 
			
		||||
export const UNPKG_PROD_URL = `https://unpkg.com/${
 | 
			
		||||
  import.meta.env.VITE_PKG_NAME
 | 
			
		||||
}@${import.meta.env.PKG_VERSION}/dist/prod/`;
 | 
			
		||||
    ? `${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 class ExcalidrawFont implements Font {
 | 
			
		||||
  public readonly url: URL;
 | 
			
		||||
  public readonly urls: URL[];
 | 
			
		||||
  public readonly fontFace: FontFace;
 | 
			
		||||
 | 
			
		||||
  constructor(family: string, uri: string, descriptors?: FontFaceDescriptors) {
 | 
			
		||||
    // absolute assets paths, which are found in tests and excalidraw-app build, won't work with base url, so we are stripping initial slash away
 | 
			
		||||
    const assetUrl: string = uri.replace(/^\/+/, "");
 | 
			
		||||
    let baseUrl: string | undefined = undefined;
 | 
			
		||||
    this.urls = ExcalidrawFont.createUrls(uri);
 | 
			
		||||
 | 
			
		||||
    // fallback to unpkg to form a valid URL in case of a passed relative assetUrl
 | 
			
		||||
    let baseUrlBuilder = window.EXCALIDRAW_ASSET_PATH || UNPKG_PROD_URL;
 | 
			
		||||
    const sources = this.urls
 | 
			
		||||
      .map((url) => `url(${url}) ${ExcalidrawFont.getFormat(url)}`)
 | 
			
		||||
      .join(", ");
 | 
			
		||||
 | 
			
		||||
    // in case user passed a root-relative url (~absolute path),
 | 
			
		||||
    // like "/" or "/some/path", or relative (starts with "./"),
 | 
			
		||||
    // prepend it with `location.origin`
 | 
			
		||||
    if (/^\.?\//.test(baseUrlBuilder)) {
 | 
			
		||||
      baseUrlBuilder = new URL(
 | 
			
		||||
        baseUrlBuilder.replace(/^\.?\/+/, ""),
 | 
			
		||||
        window?.location?.origin,
 | 
			
		||||
      ).toString();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // ensure there is a trailing slash, otherwise url won't be correctly concatenated
 | 
			
		||||
    baseUrl = `${baseUrlBuilder.replace(/\/+$/, "")}/`;
 | 
			
		||||
 | 
			
		||||
    this.url = new URL(assetUrl, baseUrl);
 | 
			
		||||
    this.fontFace = new FontFace(family, `url(${this.url})`, {
 | 
			
		||||
    this.fontFace = new FontFace(family, sources, {
 | 
			
		||||
      display: "swap",
 | 
			
		||||
      style: "normal",
 | 
			
		||||
      weight: "400",
 | 
			
		||||
@@ -44,35 +32,128 @@ export class ExcalidrawFont implements Font {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Fetches woff2 content based on the registered url (browser).
 | 
			
		||||
   * Tries to fetch woff2 content, based on the registered urls.
 | 
			
		||||
   * Returns last defined url in case of errors.
 | 
			
		||||
   *
 | 
			
		||||
   * Use dataurl outside the browser environment.
 | 
			
		||||
   * Note: uses browser APIs for base64 encoding - use dataurl outside the browser environment.
 | 
			
		||||
   */
 | 
			
		||||
  public async getContent(): Promise<string> {
 | 
			
		||||
    if (this.url.protocol === "data:") {
 | 
			
		||||
      // it's dataurl, the font is inlined as base64, no need to fetch
 | 
			
		||||
      return this.url.toString();
 | 
			
		||||
    let i = 0;
 | 
			
		||||
    const errorMessages = [];
 | 
			
		||||
 | 
			
		||||
    while (i < this.urls.length) {
 | 
			
		||||
      const url = this.urls[i];
 | 
			
		||||
 | 
			
		||||
      if (url.protocol === "data:") {
 | 
			
		||||
        // it's dataurl, the font is inlined as base64, no need to fetch
 | 
			
		||||
        return url.toString();
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      try {
 | 
			
		||||
        const response = await fetch(url, {
 | 
			
		||||
          headers: {
 | 
			
		||||
            Accept: "font/woff2",
 | 
			
		||||
          },
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        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,
 | 
			
		||||
          )}`;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // response not ok, try to continue
 | 
			
		||||
        errorMessages.push(
 | 
			
		||||
          `"${url.toString()}" returned status "${response.status}"`,
 | 
			
		||||
        );
 | 
			
		||||
      } catch (e) {
 | 
			
		||||
        errorMessages.push(`"${url.toString()}" returned error "${e}"`);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      i++;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const response = await fetch(this.url, {
 | 
			
		||||
      headers: {
 | 
			
		||||
        Accept: "font/woff2",
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
    console.error(
 | 
			
		||||
      `Failed to fetch font "${
 | 
			
		||||
        this.fontFace.family
 | 
			
		||||
      }" from urls "${this.urls.toString()}`,
 | 
			
		||||
      JSON.stringify(errorMessages, undefined, 2),
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    if (!response.ok) {
 | 
			
		||||
      console.error(
 | 
			
		||||
        `Couldn't fetch font-family "${this.fontFace.family}" from url "${this.url}"`,
 | 
			
		||||
        response,
 | 
			
		||||
    // in case of issues, at least return the last url as a content
 | 
			
		||||
    // defaults to unpkg for bundled fonts (so that we don't have to host them forever) and http url for others
 | 
			
		||||
    return this.urls.length ? this.urls[this.urls.length - 1].toString() : "";
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private static createUrls(uri: string): URL[] {
 | 
			
		||||
    if (uri.startsWith(LOCAL_FONT_PROTOCOL)) {
 | 
			
		||||
      // no url for local fonts
 | 
			
		||||
      return [];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (uri.startsWith("http") || uri.startsWith("data")) {
 | 
			
		||||
      // one url for http imports or data url
 | 
			
		||||
      return [new URL(uri)];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // absolute assets paths, which are found in tests and excalidraw-app build, won't work with base url, so we are stripping initial slash away
 | 
			
		||||
    const assetUrl: string = uri.replace(/^\/+/, "");
 | 
			
		||||
    const urls: URL[] = [];
 | 
			
		||||
 | 
			
		||||
    if (typeof window.EXCALIDRAW_ASSET_PATH === "string") {
 | 
			
		||||
      const normalizedBaseUrl = this.normalizeBaseUrl(
 | 
			
		||||
        window.EXCALIDRAW_ASSET_PATH,
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      urls.push(new URL(assetUrl, normalizedBaseUrl));
 | 
			
		||||
    } else if (Array.isArray(window.EXCALIDRAW_ASSET_PATH)) {
 | 
			
		||||
      window.EXCALIDRAW_ASSET_PATH.forEach((path) => {
 | 
			
		||||
        const normalizedBaseUrl = this.normalizeBaseUrl(path);
 | 
			
		||||
        urls.push(new URL(assetUrl, normalizedBaseUrl));
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const mimeType = await response.headers.get("Content-Type");
 | 
			
		||||
    const buffer = await response.arrayBuffer();
 | 
			
		||||
    // fallback url for bundled fonts
 | 
			
		||||
    urls.push(new URL(assetUrl, UNPKG_PROD_URL));
 | 
			
		||||
 | 
			
		||||
    return `data:${mimeType};base64,${await stringToBase64(
 | 
			
		||||
      await toByteString(buffer),
 | 
			
		||||
      true,
 | 
			
		||||
    )}`;
 | 
			
		||||
    return urls;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private static getFormat(url: URL) {
 | 
			
		||||
    try {
 | 
			
		||||
      const pathname = new URL(url).pathname;
 | 
			
		||||
      const parts = pathname.split(".");
 | 
			
		||||
 | 
			
		||||
      if (parts.length === 1) {
 | 
			
		||||
        return "";
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      return `format('${parts.pop()}')`;
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      return "";
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private static normalizeBaseUrl(baseUrl: string) {
 | 
			
		||||
    let result = baseUrl;
 | 
			
		||||
 | 
			
		||||
    // in case user passed a root-relative url (~absolute path),
 | 
			
		||||
    // like "/" or "/some/path", or relative (starts with "./"),
 | 
			
		||||
    // prepend it with `location.origin`
 | 
			
		||||
    if (/^\.?\//.test(result)) {
 | 
			
		||||
      result = new URL(
 | 
			
		||||
        result.replace(/^\.?\/+/, ""),
 | 
			
		||||
        window?.location?.origin,
 | 
			
		||||
      ).toString();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // ensure there is a trailing slash, otherwise url won't be correctly concatenated
 | 
			
		||||
    result = `${result.replace(/\/+$/, "")}/`;
 | 
			
		||||
 | 
			
		||||
    return result;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,6 @@
 | 
			
		||||
/* Only UI fonts here, which are needed before the editor initializes. */
 | 
			
		||||
/* These also cannot be preprended with `EXCALIDRAW_ASSET_PATH`. */
 | 
			
		||||
/* These cannot be dynamically prepended with `EXCALIDRAW_ASSET_PATH`. */
 | 
			
		||||
/* WARN: The following content is replaced during excalidraw-app build  */
 | 
			
		||||
 | 
			
		||||
@font-face {
 | 
			
		||||
  font-family: "Assistant";
 | 
			
		||||
 
 | 
			
		||||
@@ -39,7 +39,7 @@ export class Fonts {
 | 
			
		||||
        number,
 | 
			
		||||
        {
 | 
			
		||||
          metadata: FontMetadata;
 | 
			
		||||
          fontFaces: Font[];
 | 
			
		||||
          fonts: Font[];
 | 
			
		||||
        }
 | 
			
		||||
      >
 | 
			
		||||
    | undefined;
 | 
			
		||||
@@ -121,12 +121,9 @@ export class Fonts {
 | 
			
		||||
 | 
			
		||||
  public load = async () => {
 | 
			
		||||
    // Add all registered font faces into the `document.fonts` (if not added already)
 | 
			
		||||
    for (const { fontFaces } of Fonts.registered.values()) {
 | 
			
		||||
      for (const { fontFace, url } of fontFaces) {
 | 
			
		||||
        if (
 | 
			
		||||
          url.protocol !== LOCAL_FONT_PROTOCOL &&
 | 
			
		||||
          !window.document.fonts.has(fontFace)
 | 
			
		||||
        ) {
 | 
			
		||||
    for (const { fonts } of Fonts.registered.values()) {
 | 
			
		||||
      for (const { fontFace } of fonts) {
 | 
			
		||||
        if (!window.document.fonts.has(fontFace)) {
 | 
			
		||||
          window.document.fonts.add(fontFace);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
@@ -148,8 +145,10 @@ export class Fonts {
 | 
			
		||||
          } catch (e) {
 | 
			
		||||
            // don't let it all fail if just one font fails to load
 | 
			
		||||
            console.error(
 | 
			
		||||
              `Failed to load font: "${fontString}" with error "${e}", given the following registered font:`,
 | 
			
		||||
              JSON.stringify(Fonts.registered.get(fontFamily), undefined, 2),
 | 
			
		||||
              `Failed to load font "${fontString}" from urls "${Fonts.registered
 | 
			
		||||
                .get(fontFamily)
 | 
			
		||||
                ?.fonts.map((x) => x.urls)}"`,
 | 
			
		||||
              e,
 | 
			
		||||
            );
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
@@ -168,7 +167,7 @@ export class Fonts {
 | 
			
		||||
    const fonts = {
 | 
			
		||||
      registered: new Map<
 | 
			
		||||
        ValueOf<typeof FONT_FAMILY>,
 | 
			
		||||
        { metadata: FontMetadata; fontFaces: Font[] }
 | 
			
		||||
        { metadata: FontMetadata; fonts: Font[] }
 | 
			
		||||
      >(),
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
@@ -253,7 +252,7 @@ function register(
 | 
			
		||||
    | {
 | 
			
		||||
        registered: Map<
 | 
			
		||||
          ValueOf<typeof FONT_FAMILY>,
 | 
			
		||||
          { metadata: FontMetadata; fontFaces: Font[] }
 | 
			
		||||
          { metadata: FontMetadata; fonts: Font[] }
 | 
			
		||||
        >;
 | 
			
		||||
      },
 | 
			
		||||
  family: string,
 | 
			
		||||
@@ -267,7 +266,7 @@ function register(
 | 
			
		||||
  if (!registeredFamily) {
 | 
			
		||||
    this.registered.set(familyId, {
 | 
			
		||||
      metadata,
 | 
			
		||||
      fontFaces: params.map(
 | 
			
		||||
      fonts: params.map(
 | 
			
		||||
        ({ uri, descriptors }) => new ExcalidrawFont(family, uri, descriptors),
 | 
			
		||||
      ),
 | 
			
		||||
    });
 | 
			
		||||
 
 | 
			
		||||
@@ -27,6 +27,8 @@ export interface FontMetadata {
 | 
			
		||||
  deprecated?: true;
 | 
			
		||||
  /** flag to indicate a server-side only font */
 | 
			
		||||
  serverSide?: true;
 | 
			
		||||
  /** flag to indiccate a local-only font */
 | 
			
		||||
  local?: true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const FONT_METADATA: Record<number, FontMetadata> = {
 | 
			
		||||
@@ -85,6 +87,7 @@ export const FONT_METADATA: Record<number, FontMetadata> = {
 | 
			
		||||
    },
 | 
			
		||||
    icon: FontFamilyNormalIcon,
 | 
			
		||||
    deprecated: true,
 | 
			
		||||
    local: true,
 | 
			
		||||
  },
 | 
			
		||||
  [FONT_FAMILY.Cascadia]: {
 | 
			
		||||
    metrics: {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								packages/excalidraw/global.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								packages/excalidraw/global.d.ts
									
									
									
									
										vendored
									
									
								
							@@ -1,7 +1,7 @@
 | 
			
		||||
interface Window {
 | 
			
		||||
  ClipboardItem: any;
 | 
			
		||||
  __EXCALIDRAW_SHA__: string | undefined;
 | 
			
		||||
  EXCALIDRAW_ASSET_PATH: string | undefined;
 | 
			
		||||
  EXCALIDRAW_ASSET_PATH: string | string[] | undefined;
 | 
			
		||||
  EXCALIDRAW_EXPORT_SOURCE: string;
 | 
			
		||||
  EXCALIDRAW_THROTTLE_RENDER: boolean | undefined;
 | 
			
		||||
  DEBUG_FRACTIONAL_INDICES: boolean | undefined;
 | 
			
		||||
 
 | 
			
		||||
@@ -43,7 +43,6 @@ import type { RenderableElementsMap } from "./types";
 | 
			
		||||
import { syncInvalidIndices } from "../fractionalIndex";
 | 
			
		||||
import { renderStaticScene } from "../renderer/staticScene";
 | 
			
		||||
import { Fonts } from "../fonts";
 | 
			
		||||
import { LOCAL_FONT_PROTOCOL } from "../fonts/metadata";
 | 
			
		||||
 | 
			
		||||
const SVG_EXPORT_TAG = `<!-- svg-source:excalidraw -->`;
 | 
			
		||||
 | 
			
		||||
@@ -375,35 +374,28 @@ export const exportToSvg = async (
 | 
			
		||||
    ? []
 | 
			
		||||
    : await Promise.all(
 | 
			
		||||
        Array.from(fontFamilies).map(async (x) => {
 | 
			
		||||
          const { fontFaces } = Fonts.registered.get(x) ?? {};
 | 
			
		||||
          const { fonts, metadata } = Fonts.registered.get(x) ?? {};
 | 
			
		||||
 | 
			
		||||
          if (!Array.isArray(fontFaces)) {
 | 
			
		||||
          if (!Array.isArray(fonts)) {
 | 
			
		||||
            console.error(
 | 
			
		||||
              `Couldn't find registered font-faces for font-family "${x}"`,
 | 
			
		||||
              `Couldn't find registered fonts for font-family "${x}"`,
 | 
			
		||||
              Fonts.registered,
 | 
			
		||||
            );
 | 
			
		||||
            return;
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          return Promise.all(
 | 
			
		||||
            fontFaces
 | 
			
		||||
              .filter((font) => font.url.protocol !== LOCAL_FONT_PROTOCOL)
 | 
			
		||||
              .map(async (font) => {
 | 
			
		||||
                try {
 | 
			
		||||
                  const content = await font.getContent();
 | 
			
		||||
          if (metadata?.local) {
 | 
			
		||||
            // don't inline local fonts
 | 
			
		||||
            return;
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
                  return `@font-face {
 | 
			
		||||
          return Promise.all(
 | 
			
		||||
            fonts.map(
 | 
			
		||||
              async (font) => `@font-face {
 | 
			
		||||
        font-family: ${font.fontFace.family};
 | 
			
		||||
        src: url(${content});
 | 
			
		||||
          }`;
 | 
			
		||||
                } catch (e) {
 | 
			
		||||
                  console.error(
 | 
			
		||||
                    `Skipped inlining font with URL "${font.url.toString()}"`,
 | 
			
		||||
                    e,
 | 
			
		||||
                  );
 | 
			
		||||
                  return "";
 | 
			
		||||
                }
 | 
			
		||||
              }),
 | 
			
		||||
        src: url(${await font.getContent()});
 | 
			
		||||
          }`,
 | 
			
		||||
            ),
 | 
			
		||||
          );
 | 
			
		||||
        }),
 | 
			
		||||
      );
 | 
			
		||||
 
 | 
			
		||||
@@ -1,3 +1,6 @@
 | 
			
		||||
const OSS_FONTS_CDN =
 | 
			
		||||
  "https://excalidraw.nyc3.cdn.digitaloceanspaces.com/fonts/oss/";
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Custom vite plugin to convert url woff2 imports into a text.
 | 
			
		||||
 * Other woff2 imports are automatically served and resolved as a file uri.
 | 
			
		||||
@@ -41,6 +44,89 @@ module.exports.woff2BrowserPlugin = () => {
 | 
			
		||||
          `const $1 = $2`,
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // use CDN for Assistant
 | 
			
		||||
      if (!isDev && id.endsWith("/excalidraw/fonts/assets/fonts.css")) {
 | 
			
		||||
        return `/* WARN: The following content is generated during excalidraw-app build */
 | 
			
		||||
 | 
			
		||||
      @font-face {
 | 
			
		||||
        font-family: "Assistant";
 | 
			
		||||
        src: url(${OSS_FONTS_CDN}Assistant-Regular-DVxZuzxb.woff2)
 | 
			
		||||
            format("woff2"),
 | 
			
		||||
          url(./Assistant-Regular.woff2) format("woff2");
 | 
			
		||||
        font-weight: 400;
 | 
			
		||||
        style: normal;
 | 
			
		||||
        display: swap;
 | 
			
		||||
      }
 | 
			
		||||
      
 | 
			
		||||
      @font-face {
 | 
			
		||||
        font-family: "Assistant";
 | 
			
		||||
        src: url(${OSS_FONTS_CDN}Assistant-Medium-DrcxCXg3.woff2)
 | 
			
		||||
            format("woff2"),
 | 
			
		||||
          url(./Assistant-Medium.woff2) format("woff2");
 | 
			
		||||
        font-weight: 500;
 | 
			
		||||
        style: normal;
 | 
			
		||||
        display: swap;
 | 
			
		||||
      }
 | 
			
		||||
      
 | 
			
		||||
      @font-face {
 | 
			
		||||
        font-family: "Assistant";
 | 
			
		||||
        src: url(${OSS_FONTS_CDN}Assistant-SemiBold-SCI4bEL9.woff2)
 | 
			
		||||
            format("woff2"),
 | 
			
		||||
          url(./Assistant-SemiBold.woff2) format("woff2");
 | 
			
		||||
        font-weight: 600;
 | 
			
		||||
        style: normal;
 | 
			
		||||
        display: swap;
 | 
			
		||||
      }
 | 
			
		||||
      
 | 
			
		||||
      @font-face {
 | 
			
		||||
        font-family: "Assistant";
 | 
			
		||||
        src: url(${OSS_FONTS_CDN}Assistant-Bold-gm-uSS1B.woff2)
 | 
			
		||||
            format("woff2"),
 | 
			
		||||
          url(./Assistant-Bold.woff2) format("woff2");
 | 
			
		||||
        font-weight: 700;
 | 
			
		||||
        style: normal;
 | 
			
		||||
        display: swap;
 | 
			
		||||
      }`;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // using EXCALIDRAW_ASSET_PATH as a SSOT
 | 
			
		||||
      if (!isDev && id.endsWith("excalidraw-app/index.html")) {
 | 
			
		||||
        return code.replace(
 | 
			
		||||
          "<!-- PLACEHOLDER:EXCALIDRAW_APP_FONTS -->",
 | 
			
		||||
          `<script>
 | 
			
		||||
        // point into our CDN in prod, fallback to root (excalidraw.com) domain in case of issues
 | 
			
		||||
        window.EXCALIDRAW_ASSET_PATH = [
 | 
			
		||||
          "${OSS_FONTS_CDN}",
 | 
			
		||||
          "/",
 | 
			
		||||
        ];
 | 
			
		||||
      </script>
 | 
			
		||||
 | 
			
		||||
      <!-- Preload all default fonts and Virgil for backwards compatibility to avoid swap on init -->
 | 
			
		||||
      <link
 | 
			
		||||
        rel="preload"
 | 
			
		||||
        href="${OSS_FONTS_CDN}Excalifont-Regular-C9eKQy_N.woff2"
 | 
			
		||||
        as="font"
 | 
			
		||||
        type="font/woff2"
 | 
			
		||||
        crossorigin="anonymous"
 | 
			
		||||
      />
 | 
			
		||||
      <link
 | 
			
		||||
        rel="preload"
 | 
			
		||||
        href="${OSS_FONTS_CDN}Virgil-Regular-hO16qHwV.woff2"
 | 
			
		||||
        as="font"
 | 
			
		||||
        type="font/woff2"
 | 
			
		||||
        crossorigin="anonymous"
 | 
			
		||||
      />
 | 
			
		||||
      <link
 | 
			
		||||
        rel="preload"
 | 
			
		||||
        href="${OSS_FONTS_CDN}ComicShanns-Regular-D0c8wzsC.woff2"
 | 
			
		||||
        as="font"
 | 
			
		||||
        type="font/woff2"
 | 
			
		||||
        crossorigin="anonymous"
 | 
			
		||||
      />
 | 
			
		||||
    `,
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
  };
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -72,12 +72,14 @@ vi.mock(
 | 
			
		||||
      ...mod,
 | 
			
		||||
      ExcalidrawFont: class extends ExcalidrawFontImpl {
 | 
			
		||||
        public async getContent(): Promise<string> {
 | 
			
		||||
          if (this.url.protocol !== "file:") {
 | 
			
		||||
          const url = this.urls[0];
 | 
			
		||||
 | 
			
		||||
          if (url.protocol !== "file:") {
 | 
			
		||||
            return super.getContent();
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          // read local assets directly, without running a server
 | 
			
		||||
          const content = await fs.promises.readFile(this.url);
 | 
			
		||||
          const content = await fs.promises.readFile(url);
 | 
			
		||||
          return `data:font/woff2;base64,${content.toString("base64")}`;
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										28
									
								
								vercel.json
									
									
									
									
									
								
							
							
						
						
									
										28
									
								
								vercel.json
									
									
									
									
									
								
							@@ -6,7 +6,7 @@
 | 
			
		||||
      "headers": [
 | 
			
		||||
        {
 | 
			
		||||
          "key": "Access-Control-Allow-Origin",
 | 
			
		||||
          "value": "*"
 | 
			
		||||
          "value": "https://excalidraw.com"
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          "key": "X-Content-Type-Options",
 | 
			
		||||
@@ -21,6 +21,32 @@
 | 
			
		||||
          "value": "origin"
 | 
			
		||||
        }
 | 
			
		||||
      ]
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "source": "/:file*.woff2",
 | 
			
		||||
      "headers": [
 | 
			
		||||
        {
 | 
			
		||||
          "key": "Cache-Control",
 | 
			
		||||
          "value": "public, max-age=31536000"
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          "key": "Access-Control-Allow-Origin",
 | 
			
		||||
          "value": "https://excalidraw.com"
 | 
			
		||||
        }
 | 
			
		||||
      ]
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "source": "/(Virgil|Cascadia|Assistant-Regular).woff2",
 | 
			
		||||
      "headers": [
 | 
			
		||||
        {
 | 
			
		||||
          "key": "Cache-Control",
 | 
			
		||||
          "value": "public, max-age=31536000"
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          "key": "Access-Control-Allow-Origin",
 | 
			
		||||
          "value": "*"
 | 
			
		||||
        }
 | 
			
		||||
      ]
 | 
			
		||||
    }
 | 
			
		||||
  ],
 | 
			
		||||
  "redirects": [
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user