summaryrefslogtreecommitdiffstats
path: root/packages/excalidraw/subset/subset-main.ts
blob: afccf0d2097dd7c3af715eb7f45a55fd9cee88f0 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
import { WorkerPool } from "../workers";
import { isServerEnv, promiseTry } from "../utils";
import { WorkerInTheMainChunkError, WorkerUrlNotDefinedError } from "../errors";

import type { Commands } from "./subset-shared.chunk";

let shouldUseWorkers = typeof Worker !== "undefined";

/**
 * Tries to subset glyphs in a font based on the used codepoints, returning the font as dataurl.
 * Under the hood utilizes worker threads (Web Workers, if available), otherwise fallbacks to the main thread.
 *
 * Check the following diagram for details: link.excalidraw.com/readonly/MbbnWPSWXgadXdtmzgeO
 *
 * @param arrayBuffer font data buffer in the woff2 format
 * @param codePoints codepoints used to subset the glyphs
 *
 * @returns font with subsetted glyphs (all glyphs in case of errors) converted into a dataurl
 */
export const subsetWoff2GlyphsByCodepoints = async (
  arrayBuffer: ArrayBuffer,
  codePoints: Array<number>,
): Promise<string> => {
  const { Commands, subsetToBase64, toBase64 } =
    await lazyLoadSharedSubsetChunk();

  if (!shouldUseWorkers) {
    return subsetToBase64(arrayBuffer, codePoints);
  }

  return promiseTry(async () => {
    try {
      const workerPool = await getOrCreateWorkerPool();
      // copy the buffer to avoid working on top of the detached array buffer in the fallback
      // i.e. in case the worker throws, the array buffer does not get automatically detached, even if the worker is terminated
      const arrayBufferCopy = arrayBuffer.slice(0);
      const result = await workerPool.postMessage(
        {
          command: Commands.Subset,
          arrayBuffer: arrayBufferCopy,
          codePoints,
        } as const,
        { transfer: [arrayBufferCopy] },
      );

      // encode on the main thread to avoid copying large binary strings (as dataurl) between threads
      return toBase64(result);
    } catch (e) {
      // don't use workers if they are failing
      shouldUseWorkers = false;

      if (
        // don't log the expected errors server-side
        !(
          isServerEnv() &&
          (e instanceof WorkerUrlNotDefinedError ||
            e instanceof WorkerInTheMainChunkError)
        )
      ) {
        // eslint-disable-next-line no-console
        console.error(
          "Failed to use workers for subsetting, falling back to the main thread.",
          e,
        );
      }

      // fallback to the main thread
      return subsetToBase64(arrayBuffer, codePoints);
    }
  });
};

// lazy-loaded and cached chunks
let subsetWorker: Promise<typeof import("./subset-worker.chunk")> | null = null;
let subsetShared: Promise<typeof import("./subset-shared.chunk")> | null = null;

const lazyLoadWorkerSubsetChunk = async () => {
  if (!subsetWorker) {
    subsetWorker = import("./subset-worker.chunk");
  }

  return subsetWorker;
};

const lazyLoadSharedSubsetChunk = async () => {
  if (!subsetShared) {
    // load dynamically to force create a shared chunk reused between main thread and the worker thread
    subsetShared = import("./subset-shared.chunk");
  }

  return subsetShared;
};

// could be extended with multiple commands in the future
type SubsetWorkerData = {
  command: typeof Commands.Subset;
  arrayBuffer: ArrayBuffer;
  codePoints: Array<number>;
};

type SubsetWorkerResult<T extends SubsetWorkerData["command"]> =
  T extends typeof Commands.Subset ? ArrayBuffer : never;

let workerPool: Promise<
  WorkerPool<SubsetWorkerData, SubsetWorkerResult<SubsetWorkerData["command"]>>
> | null = null;

/**
 * Lazy initialize or get the worker pool singleton.
 *
 * @throws implicitly if anything goes wrong - worker pool creation, loading wasm, initializing worker, etc.
 */
const getOrCreateWorkerPool = () => {
  if (!workerPool) {
    // immediate concurrent-friendly return, to ensure we have only one pool instance
    workerPool = promiseTry(async () => {
      const { WorkerUrl } = await lazyLoadWorkerSubsetChunk();

      const pool = WorkerPool.create<
        SubsetWorkerData,
        SubsetWorkerResult<SubsetWorkerData["command"]>
      >(WorkerUrl);

      return pool;
    });
  }

  return workerPool;
};