@flighthq/font 0.3.0-next.906.07cea63 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,6 @@
1
+ import type { FontUrl } from '@flighthq/types/contract';
2
+ export declare function _loadFontFaceFromBytes(family: string, bytes: Uint8Array): Promise<FontFace>;
3
+ export declare function _loadFontFaceFromUrl(family: string, url: string): Promise<FontFace>;
4
+ export declare function _loadFontFaceFromUrls(family: string, sources: readonly FontUrl[]): Promise<FontFace>;
5
+ export declare function _loadFontFacesFromName(family: string): Promise<FontFace[]>;
6
+ //# sourceMappingURL=_fontFaceLoad.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"_fontFaceLoad.d.ts","sourceRoot":"","sources":["../src/_fontFaceLoad.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,0BAA0B,CAAC;AAKxD,wBAAsB,sBAAsB,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,QAAQ,CAAC,CAGjG;AAED,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC,CAEnF;AAED,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,SAAS,OAAO,EAAE,GAAG,OAAO,CAAC,QAAQ,CAAC,CAQpG;AAED,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC,CAE1E"}
@@ -0,0 +1,28 @@
1
+ import { inferFontFormatFromUrl } from './fontFormat';
2
+ import { getFontShorthand } from './fontShorthand';
3
+ export async function _loadFontFaceFromBytes(family, bytes) {
4
+ const source = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
5
+ return loadAndRegisterFontFace(family, source);
6
+ }
7
+ export function _loadFontFaceFromUrl(family, url) {
8
+ return loadAndRegisterFontFace(family, `url(${url})`);
9
+ }
10
+ export function _loadFontFaceFromUrls(family, sources) {
11
+ const source = sources
12
+ .map(({ url, format }) => {
13
+ const resolvedFormat = format ?? inferFontFormatFromUrl(url);
14
+ return resolvedFormat !== null ? `url(${url}) format('${resolvedFormat}')` : `url(${url})`;
15
+ })
16
+ .join(', ');
17
+ return loadAndRegisterFontFace(family, source);
18
+ }
19
+ export function _loadFontFacesFromName(family) {
20
+ return document.fonts.load(getFontShorthand(family));
21
+ }
22
+ async function loadAndRegisterFontFace(family, source) {
23
+ const face = new FontFace(family, source);
24
+ await face.load();
25
+ document.fonts.add(face);
26
+ return face;
27
+ }
28
+ //# sourceMappingURL=_fontFaceLoad.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"_fontFaceLoad.js","sourceRoot":"","sources":["../src/_fontFaceLoad.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,sBAAsB,EAAE,MAAM,cAAc,CAAC;AACtD,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAEnD,MAAM,CAAC,KAAK,UAAU,sBAAsB,CAAC,MAAc,EAAE,KAAiB;IAC5E,MAAM,MAAM,GAAI,KAAK,CAAC,MAAsB,CAAC,KAAK,CAAC,KAAK,CAAC,UAAU,EAAE,KAAK,CAAC,UAAU,GAAG,KAAK,CAAC,UAAU,CAAC,CAAC;IAC1G,OAAO,uBAAuB,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AACjD,CAAC;AAED,MAAM,UAAU,oBAAoB,CAAC,MAAc,EAAE,GAAW;IAC9D,OAAO,uBAAuB,CAAC,MAAM,EAAE,OAAO,GAAG,GAAG,CAAC,CAAC;AACxD,CAAC;AAED,MAAM,UAAU,qBAAqB,CAAC,MAAc,EAAE,OAA2B;IAC/E,MAAM,MAAM,GAAG,OAAO;SACnB,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,EAAE,EAAE;QACvB,MAAM,cAAc,GAAG,MAAM,IAAI,sBAAsB,CAAC,GAAG,CAAC,CAAC;QAC7D,OAAO,cAAc,KAAK,IAAI,CAAC,CAAC,CAAC,OAAO,GAAG,aAAa,cAAc,IAAI,CAAC,CAAC,CAAC,OAAO,GAAG,GAAG,CAAC;IAC7F,CAAC,CAAC;SACD,IAAI,CAAC,IAAI,CAAC,CAAC;IACd,OAAO,uBAAuB,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AACjD,CAAC;AAED,MAAM,UAAU,sBAAsB,CAAC,MAAc;IACnD,OAAO,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,gBAAgB,CAAC,MAAM,CAAC,CAAC,CAAC;AACvD,CAAC;AAED,KAAK,UAAU,uBAAuB,CAAC,MAAc,EAAE,MAA4B;IACjF,MAAM,IAAI,GAAG,IAAI,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC1C,MAAM,IAAI,CAAC,IAAI,EAAE,CAAC;IAClB,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IACzB,OAAO,IAAI,CAAC;AACd,CAAC"}
@@ -5,4 +5,5 @@ export * from './fontResource';
5
5
  export * from './fontResourceFrom';
6
6
  export * from './fontShorthand';
7
7
  export * from './fontStatus';
8
+ export * from './glyphOutlineSource';
8
9
  //# sourceMappingURL=contract.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"contract.d.ts","sourceRoot":"","sources":["../src/contract.ts"],"names":[],"mappings":"AAAA,cAAc,QAAQ,CAAC;AACvB,cAAc,cAAc,CAAC;AAC7B,cAAc,YAAY,CAAC;AAC3B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,oBAAoB,CAAC;AACnC,cAAc,iBAAiB,CAAC;AAChC,cAAc,cAAc,CAAC"}
1
+ {"version":3,"file":"contract.d.ts","sourceRoot":"","sources":["../src/contract.ts"],"names":[],"mappings":"AAAA,cAAc,QAAQ,CAAC;AACvB,cAAc,cAAc,CAAC;AAC7B,cAAc,YAAY,CAAC;AAC3B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,oBAAoB,CAAC;AACnC,cAAc,iBAAiB,CAAC;AAChC,cAAc,cAAc,CAAC;AAC7B,cAAc,sBAAsB,CAAC"}
package/dist/contract.js CHANGED
@@ -5,4 +5,5 @@ export * from './fontResource';
5
5
  export * from './fontResourceFrom';
6
6
  export * from './fontShorthand';
7
7
  export * from './fontStatus';
8
+ export * from './glyphOutlineSource';
8
9
  //# sourceMappingURL=contract.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"contract.js","sourceRoot":"","sources":["../src/contract.ts"],"names":[],"mappings":"AAAA,cAAc,QAAQ,CAAC;AACvB,cAAc,cAAc,CAAC;AAC7B,cAAc,YAAY,CAAC;AAC3B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,oBAAoB,CAAC;AACnC,cAAc,iBAAiB,CAAC;AAChC,cAAc,cAAc,CAAC"}
1
+ {"version":3,"file":"contract.js","sourceRoot":"","sources":["../src/contract.ts"],"names":[],"mappings":"AAAA,cAAc,QAAQ,CAAC;AACvB,cAAc,cAAc,CAAC;AAC7B,cAAc,YAAY,CAAC;AAC3B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,oBAAoB,CAAC;AACnC,cAAc,iBAAiB,CAAC;AAChC,cAAc,cAAc,CAAC;AAC7B,cAAc,sBAAsB,CAAC"}
@@ -1,6 +1,19 @@
1
1
  import type { Font, FontUrl } from '@flighthq/types/contract';
2
+ /**
3
+ * Loads a font face from bytes and returns its family handle. If loading rejects, the face is not
4
+ * registered and no `Font` is returned.
5
+ */
2
6
  export declare function loadFontFromBytes(bytes: Uint8Array, family: string): Promise<Font>;
7
+ /** Looks up a registered family and returns its handle. If the lookup rejects, no `Font` is returned. */
3
8
  export declare function loadFontFromName(name: string): Promise<Font>;
9
+ /**
10
+ * Loads a font face from one URL and returns its family handle. If loading rejects, the face is not
11
+ * registered and no `Font` is returned.
12
+ */
4
13
  export declare function loadFontFromUrl(url: string, family: string): Promise<Font>;
14
+ /**
15
+ * Loads a font face from fallback URLs and returns its family handle. If loading rejects, the face is
16
+ * not registered and no `Font` is returned.
17
+ */
5
18
  export declare function loadFontFromUrls(sources: FontUrl[], family: string): Promise<Font>;
6
19
  //# sourceMappingURL=fontFrom.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"fontFrom.d.ts","sourceRoot":"","sources":["../src/fontFrom.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,0BAA0B,CAAC;AAM9D,wBAAsB,iBAAiB,CAAC,KAAK,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAQxF;AAED,wBAAsB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAGlE;AAED,wBAAsB,eAAe,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAKhF;AAED,wBAAsB,gBAAgB,CAAC,OAAO,EAAE,OAAO,EAAE,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAWxF"}
1
+ {"version":3,"file":"fontFrom.d.ts","sourceRoot":"","sources":["../src/fontFrom.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,0BAA0B,CAAC;AAU9D;;;GAGG;AACH,wBAAsB,iBAAiB,CAAC,KAAK,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAGxF;AAED,yGAAyG;AACzG,wBAAsB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAGlE;AAED;;;GAGG;AACH,wBAAsB,eAAe,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAGhF;AAED;;;GAGG;AACH,wBAAsB,gBAAgB,CAAC,OAAO,EAAE,OAAO,EAAE,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAGxF"}
package/dist/fontFrom.js CHANGED
@@ -1,32 +1,32 @@
1
+ import { _loadFontFaceFromBytes, _loadFontFaceFromUrl, _loadFontFaceFromUrls, _loadFontFacesFromName, } from './_fontFaceLoad';
1
2
  import { createFont } from './font';
2
- import { inferFontFormatFromUrl } from './fontFormat';
3
- import { getFontShorthand } from './fontShorthand';
3
+ /**
4
+ * Loads a font face from bytes and returns its family handle. If loading rejects, the face is not
5
+ * registered and no `Font` is returned.
6
+ */
4
7
  export async function loadFontFromBytes(bytes, family) {
5
- const face = new FontFace(family, bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength));
6
- await face.load();
7
- document.fonts.add(face);
8
+ await _loadFontFaceFromBytes(family, bytes);
8
9
  return createFont(family);
9
10
  }
11
+ /** Looks up a registered family and returns its handle. If the lookup rejects, no `Font` is returned. */
10
12
  export async function loadFontFromName(name) {
11
- await document.fonts.load(getFontShorthand(name));
13
+ await _loadFontFacesFromName(name);
12
14
  return createFont(name);
13
15
  }
16
+ /**
17
+ * Loads a font face from one URL and returns its family handle. If loading rejects, the face is not
18
+ * registered and no `Font` is returned.
19
+ */
14
20
  export async function loadFontFromUrl(url, family) {
15
- const face = new FontFace(family, `url(${url})`);
16
- await face.load();
17
- document.fonts.add(face);
21
+ await _loadFontFaceFromUrl(family, url);
18
22
  return createFont(family);
19
23
  }
24
+ /**
25
+ * Loads a font face from fallback URLs and returns its family handle. If loading rejects, the face is
26
+ * not registered and no `Font` is returned.
27
+ */
20
28
  export async function loadFontFromUrls(sources, family) {
21
- const src = sources
22
- .map(({ url, format }) => {
23
- const fmt = format ?? inferFontFormatFromUrl(url);
24
- return fmt !== null ? `url(${url}) format('${fmt}')` : `url(${url})`;
25
- })
26
- .join(', ');
27
- const face = new FontFace(family, src);
28
- await face.load();
29
- document.fonts.add(face);
29
+ await _loadFontFaceFromUrls(family, sources);
30
30
  return createFont(family);
31
31
  }
32
32
  //# sourceMappingURL=fontFrom.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"fontFrom.js","sourceRoot":"","sources":["../src/fontFrom.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAC;AACpC,OAAO,EAAE,sBAAsB,EAAE,MAAM,cAAc,CAAC;AACtD,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAEnD,MAAM,CAAC,KAAK,UAAU,iBAAiB,CAAC,KAAiB,EAAE,MAAc;IACvE,MAAM,IAAI,GAAG,IAAI,QAAQ,CACvB,MAAM,EACL,KAAK,CAAC,MAAsB,CAAC,KAAK,CAAC,KAAK,CAAC,UAAU,EAAE,KAAK,CAAC,UAAU,GAAG,KAAK,CAAC,UAAU,CAAC,CAC3F,CAAC;IACF,MAAM,IAAI,CAAC,IAAI,EAAE,CAAC;IAClB,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IACzB,OAAO,UAAU,CAAC,MAAM,CAAC,CAAC;AAC5B,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,gBAAgB,CAAC,IAAY;IACjD,MAAM,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,gBAAgB,CAAC,IAAI,CAAC,CAAC,CAAC;IAClD,OAAO,UAAU,CAAC,IAAI,CAAC,CAAC;AAC1B,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,GAAW,EAAE,MAAc;IAC/D,MAAM,IAAI,GAAG,IAAI,QAAQ,CAAC,MAAM,EAAE,OAAO,GAAG,GAAG,CAAC,CAAC;IACjD,MAAM,IAAI,CAAC,IAAI,EAAE,CAAC;IAClB,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IACzB,OAAO,UAAU,CAAC,MAAM,CAAC,CAAC;AAC5B,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,gBAAgB,CAAC,OAAkB,EAAE,MAAc;IACvE,MAAM,GAAG,GAAG,OAAO;SAChB,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,EAAE,EAAE;QACvB,MAAM,GAAG,GAAG,MAAM,IAAI,sBAAsB,CAAC,GAAG,CAAC,CAAC;QAClD,OAAO,GAAG,KAAK,IAAI,CAAC,CAAC,CAAC,OAAO,GAAG,aAAa,GAAG,IAAI,CAAC,CAAC,CAAC,OAAO,GAAG,GAAG,CAAC;IACvE,CAAC,CAAC;SACD,IAAI,CAAC,IAAI,CAAC,CAAC;IACd,MAAM,IAAI,GAAG,IAAI,QAAQ,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IACvC,MAAM,IAAI,CAAC,IAAI,EAAE,CAAC;IAClB,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IACzB,OAAO,UAAU,CAAC,MAAM,CAAC,CAAC;AAC5B,CAAC"}
1
+ {"version":3,"file":"fontFrom.js","sourceRoot":"","sources":["../src/fontFrom.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,sBAAsB,EACtB,oBAAoB,EACpB,qBAAqB,EACrB,sBAAsB,GACvB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAC;AAEpC;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CAAC,KAAiB,EAAE,MAAc;IACvE,MAAM,sBAAsB,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;IAC5C,OAAO,UAAU,CAAC,MAAM,CAAC,CAAC;AAC5B,CAAC;AAED,yGAAyG;AACzG,MAAM,CAAC,KAAK,UAAU,gBAAgB,CAAC,IAAY;IACjD,MAAM,sBAAsB,CAAC,IAAI,CAAC,CAAC;IACnC,OAAO,UAAU,CAAC,IAAI,CAAC,CAAC;AAC1B,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,GAAW,EAAE,MAAc;IAC/D,MAAM,oBAAoB,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IACxC,OAAO,UAAU,CAAC,MAAM,CAAC,CAAC;AAC5B,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CAAC,OAAkB,EAAE,MAAc;IACvE,MAAM,qBAAqB,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC7C,OAAO,UAAU,CAAC,MAAM,CAAC,CAAC;AAC5B,CAAC"}
@@ -1,6 +1,22 @@
1
1
  import type { FontResource, FontUrl } from '@flighthq/types/contract';
2
+ /**
3
+ * Loads a face from bytes into `out`. If loading rejects, no face is registered and `out.face` retains
4
+ * its previous value, so a failed reload does not blank a working resource.
5
+ */
2
6
  export declare function loadFontResourceFromBytes(out: FontResource, bytes: Uint8Array): Promise<FontResource>;
7
+ /**
8
+ * Looks up an existing family and attaches its first face to `out`. If the lookup rejects or returns no
9
+ * faces, `out.face` retains its previous value.
10
+ */
3
11
  export declare function loadFontResourceFromName(out: FontResource): Promise<FontResource>;
12
+ /**
13
+ * Loads a face from one URL into `out`. If loading rejects, no face is registered and `out.face`
14
+ * retains its previous value, so a failed reload does not blank a working resource.
15
+ */
4
16
  export declare function loadFontResourceFromUrl(out: FontResource, url: string): Promise<FontResource>;
17
+ /**
18
+ * Loads a face from fallback URLs into `out`. If loading rejects, no face is registered and `out.face`
19
+ * retains its previous value, so a failed reload does not blank a working resource.
20
+ */
5
21
  export declare function loadFontResourceFromUrls(out: FontResource, sources: FontUrl[]): Promise<FontResource>;
6
22
  //# sourceMappingURL=fontResourceFrom.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"fontResourceFrom.d.ts","sourceRoot":"","sources":["../src/fontResourceFrom.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,OAAO,EAAE,MAAM,0BAA0B,CAAC;AAKtE,wBAAsB,yBAAyB,CAAC,GAAG,EAAE,YAAY,EAAE,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,YAAY,CAAC,CAS3G;AAED,wBAAsB,wBAAwB,CAAC,GAAG,EAAE,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC,CAIvF;AAED,wBAAsB,uBAAuB,CAAC,GAAG,EAAE,YAAY,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CAMnG;AAED,wBAAsB,wBAAwB,CAAC,GAAG,EAAE,YAAY,EAAE,OAAO,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,YAAY,CAAC,CAY3G"}
1
+ {"version":3,"file":"fontResourceFrom.d.ts","sourceRoot":"","sources":["../src/fontResourceFrom.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,OAAO,EAAE,MAAM,0BAA0B,CAAC;AAStE;;;GAGG;AACH,wBAAsB,yBAAyB,CAAC,GAAG,EAAE,YAAY,EAAE,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,YAAY,CAAC,CAI3G;AAED;;;GAGG;AACH,wBAAsB,wBAAwB,CAAC,GAAG,EAAE,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC,CAIvF;AAED;;;GAGG;AACH,wBAAsB,uBAAuB,CAAC,GAAG,EAAE,YAAY,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CAInG;AAED;;;GAGG;AACH,wBAAsB,wBAAwB,CAAC,GAAG,EAAE,YAAY,EAAE,OAAO,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,YAAY,CAAC,CAI3G"}
@@ -1,35 +1,38 @@
1
- import { inferFontFormatFromUrl } from './fontFormat';
2
- import { getFontShorthand } from './fontShorthand';
1
+ import { _loadFontFaceFromBytes, _loadFontFaceFromUrl, _loadFontFaceFromUrls, _loadFontFacesFromName, } from './_fontFaceLoad';
2
+ /**
3
+ * Loads a face from bytes into `out`. If loading rejects, no face is registered and `out.face` retains
4
+ * its previous value, so a failed reload does not blank a working resource.
5
+ */
3
6
  export async function loadFontResourceFromBytes(out, bytes) {
4
- const face = new FontFace(out.family, bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength));
5
- await face.load();
6
- document.fonts.add(face);
7
+ const face = await _loadFontFaceFromBytes(out.family, bytes);
7
8
  out.face = face;
8
9
  return out;
9
10
  }
11
+ /**
12
+ * Looks up an existing family and attaches its first face to `out`. If the lookup rejects or returns no
13
+ * faces, `out.face` retains its previous value.
14
+ */
10
15
  export async function loadFontResourceFromName(out) {
11
- const faces = await document.fonts.load(getFontShorthand(out.family));
16
+ const faces = await _loadFontFacesFromName(out.family);
12
17
  if (faces.length > 0)
13
18
  out.face = faces[0];
14
19
  return out;
15
20
  }
21
+ /**
22
+ * Loads a face from one URL into `out`. If loading rejects, no face is registered and `out.face`
23
+ * retains its previous value, so a failed reload does not blank a working resource.
24
+ */
16
25
  export async function loadFontResourceFromUrl(out, url) {
17
- const face = new FontFace(out.family, `url(${url})`);
18
- await face.load();
19
- document.fonts.add(face);
26
+ const face = await _loadFontFaceFromUrl(out.family, url);
20
27
  out.face = face;
21
28
  return out;
22
29
  }
30
+ /**
31
+ * Loads a face from fallback URLs into `out`. If loading rejects, no face is registered and `out.face`
32
+ * retains its previous value, so a failed reload does not blank a working resource.
33
+ */
23
34
  export async function loadFontResourceFromUrls(out, sources) {
24
- const src = sources
25
- .map(({ url, format }) => {
26
- const fmt = format ?? inferFontFormatFromUrl(url);
27
- return fmt !== null ? `url(${url}) format('${fmt}')` : `url(${url})`;
28
- })
29
- .join(', ');
30
- const face = new FontFace(out.family, src);
31
- await face.load();
32
- document.fonts.add(face);
35
+ const face = await _loadFontFaceFromUrls(out.family, sources);
33
36
  out.face = face;
34
37
  return out;
35
38
  }
@@ -1 +1 @@
1
- {"version":3,"file":"fontResourceFrom.js","sourceRoot":"","sources":["../src/fontResourceFrom.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,sBAAsB,EAAE,MAAM,cAAc,CAAC;AACtD,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAEnD,MAAM,CAAC,KAAK,UAAU,yBAAyB,CAAC,GAAiB,EAAE,KAAiB;IAClF,MAAM,IAAI,GAAG,IAAI,QAAQ,CACvB,GAAG,CAAC,MAAM,EACT,KAAK,CAAC,MAAsB,CAAC,KAAK,CAAC,KAAK,CAAC,UAAU,EAAE,KAAK,CAAC,UAAU,GAAG,KAAK,CAAC,UAAU,CAAC,CAC3F,CAAC;IACF,MAAM,IAAI,CAAC,IAAI,EAAE,CAAC;IAClB,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IACzB,GAAG,CAAC,IAAI,GAAG,IAAI,CAAC;IAChB,OAAO,GAAG,CAAC;AACb,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,wBAAwB,CAAC,GAAiB;IAC9D,MAAM,KAAK,GAAG,MAAM,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,gBAAgB,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC;IACtE,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC;QAAE,GAAG,CAAC,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;IAC1C,OAAO,GAAG,CAAC;AACb,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,uBAAuB,CAAC,GAAiB,EAAE,GAAW;IAC1E,MAAM,IAAI,GAAG,IAAI,QAAQ,CAAC,GAAG,CAAC,MAAM,EAAE,OAAO,GAAG,GAAG,CAAC,CAAC;IACrD,MAAM,IAAI,CAAC,IAAI,EAAE,CAAC;IAClB,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IACzB,GAAG,CAAC,IAAI,GAAG,IAAI,CAAC;IAChB,OAAO,GAAG,CAAC;AACb,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,wBAAwB,CAAC,GAAiB,EAAE,OAAkB;IAClF,MAAM,GAAG,GAAG,OAAO;SAChB,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,EAAE,EAAE;QACvB,MAAM,GAAG,GAAG,MAAM,IAAI,sBAAsB,CAAC,GAAG,CAAC,CAAC;QAClD,OAAO,GAAG,KAAK,IAAI,CAAC,CAAC,CAAC,OAAO,GAAG,aAAa,GAAG,IAAI,CAAC,CAAC,CAAC,OAAO,GAAG,GAAG,CAAC;IACvE,CAAC,CAAC;SACD,IAAI,CAAC,IAAI,CAAC,CAAC;IACd,MAAM,IAAI,GAAG,IAAI,QAAQ,CAAC,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAC3C,MAAM,IAAI,CAAC,IAAI,EAAE,CAAC;IAClB,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IACzB,GAAG,CAAC,IAAI,GAAG,IAAI,CAAC;IAChB,OAAO,GAAG,CAAC;AACb,CAAC"}
1
+ {"version":3,"file":"fontResourceFrom.js","sourceRoot":"","sources":["../src/fontResourceFrom.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,sBAAsB,EACtB,oBAAoB,EACpB,qBAAqB,EACrB,sBAAsB,GACvB,MAAM,iBAAiB,CAAC;AAEzB;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,yBAAyB,CAAC,GAAiB,EAAE,KAAiB;IAClF,MAAM,IAAI,GAAG,MAAM,sBAAsB,CAAC,GAAG,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;IAC7D,GAAG,CAAC,IAAI,GAAG,IAAI,CAAC;IAChB,OAAO,GAAG,CAAC;AACb,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,wBAAwB,CAAC,GAAiB;IAC9D,MAAM,KAAK,GAAG,MAAM,sBAAsB,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;IACvD,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC;QAAE,GAAG,CAAC,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;IAC1C,OAAO,GAAG,CAAC;AACb,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,uBAAuB,CAAC,GAAiB,EAAE,GAAW;IAC1E,MAAM,IAAI,GAAG,MAAM,oBAAoB,CAAC,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IACzD,GAAG,CAAC,IAAI,GAAG,IAAI,CAAC;IAChB,OAAO,GAAG,CAAC;AACb,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,wBAAwB,CAAC,GAAiB,EAAE,OAAkB;IAClF,MAAM,IAAI,GAAG,MAAM,qBAAqB,CAAC,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC9D,GAAG,CAAC,IAAI,GAAG,IAAI,CAAC;IAChB,OAAO,GAAG,CAAC;AACb,CAAC"}
@@ -0,0 +1,3 @@
1
+ import type { GlyphOutlineSource, GlyphRasterizerBackend } from '@flighthq/types/contract';
2
+ export declare function createGlyphRasterizerBackendFromGlyphOutlineSource(source: GlyphOutlineSource): GlyphRasterizerBackend;
3
+ //# sourceMappingURL=glyphOutlineSource.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"glyphOutlineSource.d.ts","sourceRoot":"","sources":["../src/glyphOutlineSource.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAEV,kBAAkB,EAGlB,sBAAsB,EAEvB,MAAM,0BAA0B,CAAC;AAOlC,wBAAgB,kDAAkD,CAAC,MAAM,EAAE,kBAAkB,GAAG,sBAAsB,CAgBrH"}
@@ -0,0 +1,107 @@
1
+ import { createPath, flattenPath, getPathBounds } from '@flighthq/path/contract';
2
+ // Adapts an index-keyed vector font into the codepoint-keyed rasterizer consumed by glyphatlas. The
3
+ // adapter is bound to one source, so callers can install it on one GlyphAtlas through
4
+ // `GlyphAtlasOptions.rasterizerBackend` without changing the process-wide backend. Rasterization is a
5
+ // portable 4x4 coverage scan over flattened contours: it needs no DOM/canvas and therefore works for
6
+ // imported fonts in browser, worker, native-host, and headless environments alike.
7
+ export function createGlyphRasterizerBackendFromGlyphOutlineSource(source) {
8
+ return {
9
+ measureMetrics(options) {
10
+ const metrics = source.getGlyphOutlineMetrics();
11
+ const scale = resolveGlyphOutlineScale(metrics.unitsPerEm, options.fontSize);
12
+ if (scale === null)
13
+ return null;
14
+ return {
15
+ ascent: metrics.ascent * scale,
16
+ descent: metrics.descent * scale,
17
+ lineGap: metrics.lineGap * scale,
18
+ };
19
+ },
20
+ rasterize(codePoint, options) {
21
+ return rasterizeGlyphOutlineSource(source, codePoint, options);
22
+ },
23
+ };
24
+ }
25
+ function rasterizeGlyphOutlineSource(source, codePoint, options) {
26
+ const glyphIndex = source.getGlyphOutlineIndexForCodePoint(codePoint);
27
+ if (glyphIndex < 0)
28
+ return null;
29
+ const metrics = source.getGlyphOutlineMetrics();
30
+ const scale = resolveGlyphOutlineScale(metrics.unitsPerEm, options.fontSize);
31
+ if (scale === null)
32
+ return null;
33
+ const path = createPath();
34
+ if (!source.getGlyphOutline(path, glyphIndex))
35
+ return null;
36
+ const advance = source.getGlyphOutlineAdvance(glyphIndex) * scale;
37
+ if (!Number.isFinite(advance))
38
+ return null;
39
+ const bounds = { height: 0, width: 0, x: 0, y: 0 };
40
+ if (!getPathBounds(path, bounds) || bounds.width === 0 || bounds.height === 0) {
41
+ return { advance, bearingX: 0, bearingY: 0, height: 0, pixels: new Uint8ClampedArray(), width: 0 };
42
+ }
43
+ const left = Math.floor(bounds.x * scale) - RASTER_GUARD;
44
+ const top = Math.floor(bounds.y * scale) - RASTER_GUARD;
45
+ const right = Math.ceil((bounds.x + bounds.width) * scale) + RASTER_GUARD;
46
+ const bottom = Math.ceil((bounds.y + bounds.height) * scale) + RASTER_GUARD;
47
+ const width = right - left;
48
+ const height = bottom - top;
49
+ if (width <= 0 || height <= 0)
50
+ return null;
51
+ // Flatten once at quarter-pixel precision in design units. Sampling the Path helper directly would
52
+ // flatten every curve again for every subpixel and turn raster cost into pixels × path decoding.
53
+ const contours = flattenPath(path, 0.25 / scale);
54
+ const pixels = new Uint8ClampedArray(width * height * 4);
55
+ for (let pixelY = 0; pixelY < height; pixelY++) {
56
+ for (let pixelX = 0; pixelX < width; pixelX++) {
57
+ let covered = 0;
58
+ for (let sampleY = 0; sampleY < RASTER_SAMPLE_AXIS; sampleY++) {
59
+ const y = (top + pixelY + (sampleY + 0.5) / RASTER_SAMPLE_AXIS) / scale;
60
+ for (let sampleX = 0; sampleX < RASTER_SAMPLE_AXIS; sampleX++) {
61
+ const x = (left + pixelX + (sampleX + 0.5) / RASTER_SAMPLE_AXIS) / scale;
62
+ if (containsFlattenedGlyphOutlinePoint(contours, path.winding, x, y))
63
+ covered++;
64
+ }
65
+ }
66
+ if (covered === 0)
67
+ continue;
68
+ const offset = (pixelY * width + pixelX) * 4;
69
+ pixels[offset] = 0xff;
70
+ pixels[offset + 1] = 0xff;
71
+ pixels[offset + 2] = 0xff;
72
+ pixels[offset + 3] = Math.round((covered * 0xff) / RASTER_SAMPLE_COUNT);
73
+ }
74
+ }
75
+ return { advance, bearingX: left, bearingY: -top, height, pixels, width };
76
+ }
77
+ function containsFlattenedGlyphOutlinePoint(contours, winding, x, y) {
78
+ let crossings = 0;
79
+ for (const contour of contours) {
80
+ const pointCount = (contour.length / 2) | 0;
81
+ if (pointCount < 2)
82
+ continue;
83
+ let fromX = contour[(pointCount - 1) * 2];
84
+ let fromY = contour[(pointCount - 1) * 2 + 1];
85
+ for (let pointIndex = 0; pointIndex < pointCount; pointIndex++) {
86
+ const toX = contour[pointIndex * 2];
87
+ const toY = contour[pointIndex * 2 + 1];
88
+ if ((fromY <= y && toY > y) || (toY <= y && fromY > y)) {
89
+ const crossingX = fromX + ((y - fromY) * (toX - fromX)) / (toY - fromY);
90
+ if (x < crossingX)
91
+ crossings += toY > fromY ? 1 : -1;
92
+ }
93
+ fromX = toX;
94
+ fromY = toY;
95
+ }
96
+ }
97
+ return winding === 'evenOdd' ? (Math.abs(crossings) & 1) !== 0 : crossings !== 0;
98
+ }
99
+ function resolveGlyphOutlineScale(unitsPerEm, fontSize) {
100
+ if (!(unitsPerEm > 0) || !(fontSize > 0) || !Number.isFinite(unitsPerEm) || !Number.isFinite(fontSize))
101
+ return null;
102
+ return fontSize / unitsPerEm;
103
+ }
104
+ const RASTER_GUARD = 1;
105
+ const RASTER_SAMPLE_AXIS = 4;
106
+ const RASTER_SAMPLE_COUNT = RASTER_SAMPLE_AXIS * RASTER_SAMPLE_AXIS;
107
+ //# sourceMappingURL=glyphOutlineSource.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"glyphOutlineSource.js","sourceRoot":"","sources":["../src/glyphOutlineSource.ts"],"names":[],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export { createFont, createFontResource, detectFontFormat, getFontShorthand, inferFontFormatFromUrl, isFontLoaded, loadFontFromBytes, loadFontFromName, loadFontFromUrl, loadFontFromUrls, loadFontResourceFromBytes, loadFontResourceFromName, loadFontResourceFromUrl, loadFontResourceFromUrls, whenFontsReady, } from './contract';
1
+ export { createFont, createFontResource, createGlyphRasterizerBackendFromGlyphOutlineSource, detectFontFormat, getFontShorthand, inferFontFormatFromUrl, isFontLoaded, loadFontFromBytes, loadFontFromName, loadFontFromUrl, loadFontFromUrls, loadFontResourceFromBytes, loadFontResourceFromName, loadFontResourceFromUrl, loadFontResourceFromUrls, whenFontsReady, } from './contract';
2
2
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,UAAU,EACV,kBAAkB,EAClB,gBAAgB,EAChB,gBAAgB,EAChB,sBAAsB,EACtB,YAAY,EACZ,iBAAiB,EACjB,gBAAgB,EAChB,eAAe,EACf,gBAAgB,EAChB,yBAAyB,EACzB,wBAAwB,EACxB,uBAAuB,EACvB,wBAAwB,EACxB,cAAc,GACf,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,UAAU,EACV,kBAAkB,EAClB,kDAAkD,EAClD,gBAAgB,EAChB,gBAAgB,EAChB,sBAAsB,EACtB,YAAY,EACZ,iBAAiB,EACjB,gBAAgB,EAChB,eAAe,EACf,gBAAgB,EAChB,yBAAyB,EACzB,wBAAwB,EACxB,uBAAuB,EACvB,wBAAwB,EACxB,cAAc,GACf,MAAM,YAAY,CAAC"}
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
- export { createFont, createFontResource, detectFontFormat, getFontShorthand, inferFontFormatFromUrl, isFontLoaded, loadFontFromBytes, loadFontFromName, loadFontFromUrl, loadFontFromUrls, loadFontResourceFromBytes, loadFontResourceFromName, loadFontResourceFromUrl, loadFontResourceFromUrls, whenFontsReady, } from './contract';
1
+ export { createFont, createFontResource, createGlyphRasterizerBackendFromGlyphOutlineSource, detectFontFormat, getFontShorthand, inferFontFormatFromUrl, isFontLoaded, loadFontFromBytes, loadFontFromName, loadFontFromUrl, loadFontFromUrls, loadFontResourceFromBytes, loadFontResourceFromName, loadFontResourceFromUrl, loadFontResourceFromUrls, whenFontsReady, } from './contract';
2
2
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,UAAU,EACV,kBAAkB,EAClB,gBAAgB,EAChB,gBAAgB,EAChB,sBAAsB,EACtB,YAAY,EACZ,iBAAiB,EACjB,gBAAgB,EAChB,eAAe,EACf,gBAAgB,EAChB,yBAAyB,EACzB,wBAAwB,EACxB,uBAAuB,EACvB,wBAAwB,EACxB,cAAc,GACf,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,UAAU,EACV,kBAAkB,EAClB,kDAAkD,EAClD,gBAAgB,EAChB,gBAAgB,EAChB,sBAAsB,EACtB,YAAY,EACZ,iBAAiB,EACjB,gBAAgB,EAChB,eAAe,EACf,gBAAgB,EAChB,yBAAyB,EACzB,wBAAwB,EACxB,uBAAuB,EACvB,wBAAwB,EACxB,cAAc,GACf,MAAM,YAAY,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,8 @@
1
1
  {
2
2
  "name": "@flighthq/font",
3
- "version": "0.3.0-next.906.07cea63",
3
+ "version": "0.3.0",
4
+ "author": "Joshua Granick and other contributors",
5
+ "license": "MIT",
4
6
  "repository": {
5
7
  "type": "git",
6
8
  "url": "https://github.com/flighthq/flight.git",
@@ -36,8 +38,9 @@
36
38
  "clean:dist": "tsx ../../scripts/clean-package-dist.ts"
37
39
  },
38
40
  "dependencies": {
39
- "@flighthq/entity": "0.3.0-next.906.07cea63",
40
- "@flighthq/types": "0.3.0-next.906.07cea63"
41
+ "@flighthq/entity": "0.3.0",
42
+ "@flighthq/path": "0.3.0",
43
+ "@flighthq/types": "0.3.0"
41
44
  },
42
45
  "devDependencies": {
43
46
  "typescript": "^5.3.0"
@@ -0,0 +1,98 @@
1
+ import {
2
+ _loadFontFaceFromBytes,
3
+ _loadFontFaceFromUrl,
4
+ _loadFontFaceFromUrls,
5
+ _loadFontFacesFromName,
6
+ } from './_fontFaceLoad';
7
+
8
+ interface FontFaceConstruction {
9
+ family: string;
10
+ source: string | ArrayBuffer;
11
+ instance: MockFontFace;
12
+ }
13
+
14
+ let constructions: FontFaceConstruction[];
15
+ let addMock: ReturnType<typeof vi.fn>;
16
+ let loadMock: ReturnType<typeof vi.fn>;
17
+
18
+ class MockFontFace {
19
+ load = vi.fn().mockResolvedValue(undefined);
20
+ constructor(family: string, source: string | ArrayBuffer) {
21
+ constructions.push({ family, source, instance: this });
22
+ }
23
+ }
24
+
25
+ let originalFonts: PropertyDescriptor | undefined;
26
+ beforeEach(() => {
27
+ constructions = [];
28
+ addMock = vi.fn();
29
+ loadMock = vi.fn().mockResolvedValue([]);
30
+ vi.stubGlobal('FontFace', MockFontFace);
31
+ originalFonts = Object.getOwnPropertyDescriptor(document, 'fonts');
32
+ Object.defineProperty(document, 'fonts', {
33
+ value: { add: addMock, load: loadMock },
34
+ configurable: true,
35
+ });
36
+ });
37
+
38
+ afterEach(() => {
39
+ if (originalFonts) {
40
+ Object.defineProperty(document, 'fonts', originalFonts);
41
+ } else {
42
+ delete (document as unknown as { fonts?: unknown }).fonts;
43
+ }
44
+ vi.unstubAllGlobals();
45
+ vi.restoreAllMocks();
46
+ });
47
+
48
+ describe('_loadFontFaceFromBytes', () => {
49
+ it('loads and registers a face using only the bytes inside the supplied view', async () => {
50
+ const backing = new Uint8Array([1, 2, 3, 4, 5, 6]);
51
+ const face = await _loadFontFaceFromBytes('Family', backing.subarray(1, 5));
52
+
53
+ expect(face).toBe(constructions[0].instance);
54
+ expect(new Uint8Array(constructions[0].source as ArrayBuffer)).toEqual(new Uint8Array([2, 3, 4, 5]));
55
+ expect(constructions[0].instance.load).toHaveBeenCalledOnce();
56
+ expect(addMock).toHaveBeenCalledWith(face);
57
+ });
58
+ });
59
+
60
+ describe('_loadFontFaceFromUrl', () => {
61
+ it('loads and registers the single URL source', async () => {
62
+ await _loadFontFaceFromUrl('Family', 'font.woff2');
63
+ expect(constructions[0].source).toBe('url(font.woff2)');
64
+ expect(addMock).toHaveBeenCalledWith(constructions[0].instance);
65
+ });
66
+ });
67
+
68
+ describe('_loadFontFaceFromUrls', () => {
69
+ it('composes explicit, inferred, and absent format hints once for both public loader families', async () => {
70
+ await _loadFontFaceFromUrls('Family', [{ url: 'a.woff2', format: 'woff2' }, { url: 'b.ttf' }, { url: 'c.bin' }]);
71
+
72
+ expect(constructions[0].source).toBe("url(a.woff2) format('woff2'), url(b.ttf) format('truetype'), url(c.bin)");
73
+ });
74
+
75
+ it('does not register a face when loading rejects', async () => {
76
+ const failing = new MockFontFace('Family', '');
77
+ failing.load.mockRejectedValue(new Error('bad font'));
78
+ vi.stubGlobal(
79
+ 'FontFace',
80
+ vi.fn(function () {
81
+ return failing;
82
+ }),
83
+ );
84
+
85
+ await expect(_loadFontFaceFromUrls('Family', [{ url: 'font.woff2' }])).rejects.toThrow('bad font');
86
+ expect(addMock).not.toHaveBeenCalled();
87
+ });
88
+ });
89
+
90
+ describe('_loadFontFacesFromName', () => {
91
+ it('uses the escaped shorthand and returns the document faces', async () => {
92
+ const faces = [{ family: 'Family' } as unknown as FontFace];
93
+ loadMock.mockResolvedValue(faces);
94
+
95
+ await expect(_loadFontFacesFromName("Family's Name")).resolves.toBe(faces);
96
+ expect(loadMock).toHaveBeenCalledWith("1em 'Family\\'s Name'");
97
+ });
98
+ });
@@ -6,86 +6,231 @@ import {
6
6
  loadFontResourceFromUrls,
7
7
  } from './fontResourceFrom';
8
8
 
9
- let mockFace: { load: ReturnType<typeof vi.fn> };
9
+ interface FontFaceConstruction {
10
+ family: string;
11
+ source: string | ArrayBuffer;
12
+ instance: MockFontFace;
13
+ }
10
14
 
15
+ let constructions: FontFaceConstruction[];
16
+ let addMock: ReturnType<typeof vi.fn>;
17
+ let loadMock: ReturnType<typeof vi.fn>;
18
+
19
+ class MockFontFace {
20
+ load = vi.fn().mockResolvedValue(undefined);
21
+ constructor(family: string, source: string | ArrayBuffer) {
22
+ constructions.push({ family, source, instance: this });
23
+ }
24
+ }
25
+
26
+ // Mirrors the harness in fontFrom.test.ts, including its restore discipline: the document.fonts
27
+ // descriptor is saved and put back per test rather than left overwritten, so under a shared
28
+ // (isolate:false) worker the patch cannot leak into a sibling file.
29
+ let originalFonts: PropertyDescriptor | undefined;
11
30
  beforeEach(() => {
12
- mockFace = { load: vi.fn().mockResolvedValue(undefined) };
13
- vi.stubGlobal(
14
- 'FontFace',
15
- vi.fn(function () {
16
- return mockFace;
17
- }),
18
- );
31
+ constructions = [];
32
+ addMock = vi.fn();
33
+ loadMock = vi.fn().mockResolvedValue([]);
34
+ vi.stubGlobal('FontFace', MockFontFace);
35
+ originalFonts = Object.getOwnPropertyDescriptor(document, 'fonts');
19
36
  Object.defineProperty(document, 'fonts', {
20
- value: { add: vi.fn(), load: vi.fn() },
37
+ value: { add: addMock, load: loadMock },
21
38
  configurable: true,
22
- writable: true,
23
39
  });
24
- vi.spyOn(document.fonts, 'add').mockImplementation(() => document.fonts);
25
- vi.spyOn(document.fonts, 'load').mockResolvedValue([mockFace as unknown as FontFace]);
26
40
  });
27
41
 
28
42
  afterEach(() => {
43
+ if (originalFonts) {
44
+ Object.defineProperty(document, 'fonts', originalFonts);
45
+ } else {
46
+ delete (document as unknown as { fonts?: unknown }).fonts;
47
+ }
29
48
  vi.unstubAllGlobals();
30
49
  vi.restoreAllMocks();
31
50
  });
32
51
 
33
52
  describe('loadFontResourceFromBytes', () => {
34
- it('loads the face and attaches it to the resource', async () => {
53
+ it('constructs the face under the resource family, loads it, and registers it', async () => {
35
54
  const resource = createFontResource('TestFont');
36
55
  const result = await loadFontResourceFromBytes(resource, new Uint8Array(8));
37
56
  expect(result).toBe(resource);
38
- expect(resource.face).toBe(mockFace);
39
- expect(mockFace.load).toHaveBeenCalledOnce();
40
- expect(document.fonts.add).toHaveBeenCalledWith(mockFace);
57
+ expect(constructions).toHaveLength(1);
58
+ expect(constructions[0].family).toBe('TestFont');
59
+ expect(constructions[0].instance.load).toHaveBeenCalledOnce();
60
+ expect(addMock).toHaveBeenCalledWith(constructions[0].instance);
61
+ expect(resource.face).toBe(constructions[0].instance);
62
+ });
63
+
64
+ it('slices only the view bytes out of a larger backing buffer', async () => {
65
+ // This integration assertion pins that the resource wrapper forwards its view to the canonical
66
+ // byte loader rather than widening it back to the whole buffer.
67
+ const backing = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]);
68
+ await loadFontResourceFromBytes(createFontResource('TestFont'), backing.subarray(2, 6));
69
+ expect(new Uint8Array(constructions[0].source as ArrayBuffer)).toEqual(new Uint8Array([3, 4, 5, 6]));
70
+ });
71
+
72
+ it('propagates a load failure and does not register the face', async () => {
73
+ const resource = createFontResource('TestFont');
74
+ const failing = new MockFontFace('TestFont', new ArrayBuffer(0));
75
+ failing.load.mockRejectedValue(new Error('bad font'));
76
+ vi.stubGlobal(
77
+ 'FontFace',
78
+ vi.fn(function () {
79
+ return failing;
80
+ }),
81
+ );
82
+ await expect(loadFontResourceFromBytes(resource, new Uint8Array(4))).rejects.toThrow('bad font');
83
+ expect(addMock).not.toHaveBeenCalled();
84
+ expect(resource.face).toBeNull();
85
+ });
86
+
87
+ it('leaves a previously loaded face attached when a reload fails', async () => {
88
+ // The contract this pins: `face` is assigned only after the load resolves, so a failed reload
89
+ // rejects and leaves the resource on the font it already had rather than blanking it. A caller
90
+ // that ignores the rejection keeps rendering with a working font instead of losing one.
91
+ const resource = createFontResource('TestFont');
92
+ await loadFontResourceFromBytes(resource, new Uint8Array(4));
93
+ const loaded = resource.face;
94
+ expect(loaded).not.toBeNull();
95
+
96
+ const failing = new MockFontFace('TestFont', new ArrayBuffer(0));
97
+ failing.load.mockRejectedValue(new Error('boom'));
98
+ vi.stubGlobal(
99
+ 'FontFace',
100
+ vi.fn(function () {
101
+ return failing;
102
+ }),
103
+ );
104
+ await expect(loadFontResourceFromBytes(resource, new Uint8Array(4))).rejects.toThrow('boom');
105
+ expect(resource.face).toBe(loaded);
106
+ expect(addMock).toHaveBeenCalledTimes(1);
41
107
  });
42
108
  });
43
109
 
44
110
  describe('loadFontResourceFromName', () => {
45
- it('resolves a face already registered with the document', async () => {
111
+ it('queries the family via the escaped font shorthand', async () => {
112
+ await loadFontResourceFromName(createFontResource('MyFont'));
113
+ expect(loadMock).toHaveBeenCalledWith("1em 'MyFont'");
114
+ });
115
+
116
+ it('escapes a single quote in the family name so the shorthand stays valid', async () => {
117
+ await loadFontResourceFromName(createFontResource("Josh's Font"));
118
+ expect(loadMock).toHaveBeenCalledWith("1em 'Josh\\'s Font'");
119
+ });
120
+
121
+ it('attaches the first face when the document returns several', async () => {
122
+ const first = { family: 'first' } as unknown as FontFace;
123
+ const second = { family: 'second' } as unknown as FontFace;
124
+ loadMock.mockResolvedValue([first, second]);
46
125
  const resource = createFontResource('TestFont');
47
126
  const result = await loadFontResourceFromName(resource);
48
127
  expect(result).toBe(resource);
49
- expect(resource.face).toBe(mockFace);
128
+ expect(resource.face).toBe(first);
50
129
  });
51
130
 
52
131
  it('leaves face null when no registered faces are found', async () => {
53
- vi.spyOn(document.fonts, 'load').mockResolvedValue([]);
54
132
  const resource = createFontResource('UnknownFont');
55
133
  await loadFontResourceFromName(resource);
56
134
  expect(resource.face).toBeNull();
57
135
  });
136
+
137
+ it('registers nothing — a name lookup finds an existing face rather than adding one', async () => {
138
+ loadMock.mockResolvedValue([{ family: 'x' } as unknown as FontFace]);
139
+ await loadFontResourceFromName(createFontResource('TestFont'));
140
+ expect(addMock).not.toHaveBeenCalled();
141
+ expect(constructions).toHaveLength(0);
142
+ });
143
+
144
+ it('propagates a lookup failure', async () => {
145
+ loadMock.mockRejectedValue(new Error('no such font'));
146
+ await expect(loadFontResourceFromName(createFontResource('MyFont'))).rejects.toThrow('no such font');
147
+ });
58
148
  });
59
149
 
60
150
  describe('loadFontResourceFromUrl', () => {
61
- it('loads the face from a URL and attaches it to the resource', async () => {
62
- const resource = createFontResource('TestFont');
151
+ it('builds a url() source under the resource family and registers the face', async () => {
152
+ const resource = createFontResource('MyFont');
63
153
  const result = await loadFontResourceFromUrl(resource, 'https://example.com/font.woff2');
64
154
  expect(result).toBe(resource);
65
- expect(resource.face).toBe(mockFace);
66
- expect(document.fonts.add).toHaveBeenCalledWith(mockFace);
155
+ expect(constructions[0].family).toBe('MyFont');
156
+ expect(constructions[0].source).toBe('url(https://example.com/font.woff2)');
157
+ expect(addMock).toHaveBeenCalledWith(constructions[0].instance);
158
+ expect(resource.face).toBe(constructions[0].instance);
159
+ });
160
+
161
+ it('adds no format() hint — a single url carries none, unlike the multi-source form', async () => {
162
+ await loadFontResourceFromUrl(createFontResource('MyFont'), 'font.woff2');
163
+ expect(constructions[0].source).toBe('url(font.woff2)');
164
+ });
165
+
166
+ it('propagates a load failure and does not register the face', async () => {
167
+ const resource = createFontResource('MyFont');
168
+ const failing = new MockFontFace('MyFont', '');
169
+ failing.load.mockRejectedValue(new Error('bad url'));
170
+ vi.stubGlobal(
171
+ 'FontFace',
172
+ vi.fn(function () {
173
+ return failing;
174
+ }),
175
+ );
176
+ await expect(loadFontResourceFromUrl(resource, 'font.woff2')).rejects.toThrow('bad url');
177
+ expect(addMock).not.toHaveBeenCalled();
178
+ expect(resource.face).toBeNull();
67
179
  });
68
180
  });
69
181
 
70
182
  describe('loadFontResourceFromUrls', () => {
71
- it('builds a multi-source src string and loads the face', async () => {
72
- const resource = createFontResource('TestFont');
183
+ it('composes a comma-joined src with explicit and inferred format() hints', async () => {
184
+ // The block this replaces was titled "builds a multi-source src string" and asserted nothing
185
+ // about the string — only that a face was attached. The composed src is the whole point of this
186
+ // function, and it was the one thing not checked.
187
+ const resource = createFontResource('MyFont');
73
188
  const result = await loadFontResourceFromUrls(resource, [
74
189
  { url: 'font.woff2', format: 'woff2' },
75
190
  { url: 'font.ttf' },
76
191
  ]);
77
192
  expect(result).toBe(resource);
78
- expect(resource.face).toBe(mockFace);
79
- expect(document.fonts.add).toHaveBeenCalledWith(mockFace);
193
+ expect(constructions[0].family).toBe('MyFont');
194
+ expect(constructions[0].source).toBe("url(font.woff2) format('woff2'), url(font.ttf) format('truetype')");
195
+ expect(addMock).toHaveBeenCalledWith(constructions[0].instance);
80
196
  });
81
197
 
82
- it('infers format from file extension when no format is provided', async () => {
83
- const MockFontFace = vi.fn(function () {
84
- return mockFace;
85
- });
86
- vi.stubGlobal('FontFace', MockFontFace);
87
- const resource = createFontResource('TestFont');
88
- await loadFontResourceFromUrls(resource, [{ url: 'font.otf' }]);
89
- expect(MockFontFace).toHaveBeenCalledWith('TestFont', expect.stringContaining("format('opentype')"));
198
+ it('infers the format from the file extension when none is given', async () => {
199
+ await loadFontResourceFromUrls(createFontResource('MyFont'), [{ url: 'font.otf' }]);
200
+ expect(constructions[0].source).toBe("url(font.otf) format('opentype')");
201
+ });
202
+
203
+ it('omits the format() hint when it cannot be inferred and none is given', async () => {
204
+ await loadFontResourceFromUrls(createFontResource('MyFont'), [{ url: 'font.bin' }]);
205
+ expect(constructions[0].source).toBe('url(font.bin)');
206
+ });
207
+
208
+ it('mixes inferred, explicit, and absent hints across one src list', async () => {
209
+ await loadFontResourceFromUrls(createFontResource('MyFont'), [
210
+ { url: 'a.woff2', format: 'woff2' },
211
+ { url: 'b.ttf' },
212
+ { url: 'c.bin' },
213
+ ]);
214
+ expect(constructions[0].source).toBe("url(a.woff2) format('woff2'), url(b.ttf) format('truetype'), url(c.bin)");
215
+ });
216
+
217
+ it('builds an empty src for an empty source list rather than throwing', async () => {
218
+ await loadFontResourceFromUrls(createFontResource('MyFont'), []);
219
+ expect(constructions[0].source).toBe('');
220
+ });
221
+
222
+ it('propagates a load failure and does not register the face', async () => {
223
+ const resource = createFontResource('MyFont');
224
+ const failing = new MockFontFace('MyFont', '');
225
+ failing.load.mockRejectedValue(new Error('bad url'));
226
+ vi.stubGlobal(
227
+ 'FontFace',
228
+ vi.fn(function () {
229
+ return failing;
230
+ }),
231
+ );
232
+ await expect(loadFontResourceFromUrls(resource, [{ url: 'font.woff2' }])).rejects.toThrow('bad url');
233
+ expect(addMock).not.toHaveBeenCalled();
234
+ expect(resource.face).toBeNull();
90
235
  });
91
236
  });
@@ -0,0 +1,80 @@
1
+ import { appendPathClose, appendPathLineTo, appendPathMoveTo } from '@flighthq/path/contract';
2
+ import type { GlyphOutlineSource, Path } from '@flighthq/types/contract';
3
+ import { describe, expect, it } from 'vitest';
4
+
5
+ import { createGlyphRasterizerBackendFromGlyphOutlineSource } from './glyphOutlineSource';
6
+
7
+ describe('createGlyphRasterizerBackendFromGlyphOutlineSource', () => {
8
+ it('maps codepoints to glyph indices and rasterizes design-unit outlines at the requested em size', () => {
9
+ const backend = createGlyphRasterizerBackendFromGlyphOutlineSource(createTestGlyphOutlineSource());
10
+ const raster = backend.rasterize(0x41, { fontFamily: 'embedded', fontSize: 20 });
11
+
12
+ expect(raster).not.toBeNull();
13
+ expect(raster!.advance).toBe(12);
14
+ expect(raster!.bearingX).toBe(-3);
15
+ expect(raster!.bearingY).toBe(17);
16
+ expect(raster!.width).toBe(14);
17
+ expect(raster!.height).toBe(22);
18
+ expect(raster!.pixels.some((value, index) => index % 4 === 3 && value === 0xff)).toBe(true);
19
+ expect(raster!.pixels[3]).toBe(0);
20
+ });
21
+
22
+ it('returns scaled outline metrics to glyphatlas', () => {
23
+ const backend = createGlyphRasterizerBackendFromGlyphOutlineSource(createTestGlyphOutlineSource());
24
+
25
+ expect(backend.measureMetrics?.({ fontFamily: 'embedded', fontSize: 20 })).toEqual({
26
+ ascent: 16,
27
+ descent: 4,
28
+ lineGap: 2,
29
+ });
30
+ });
31
+
32
+ it('returns a zero-area raster with an advance for an empty glyph', () => {
33
+ const backend = createGlyphRasterizerBackendFromGlyphOutlineSource(createTestGlyphOutlineSource());
34
+
35
+ expect(backend.rasterize(0x20, { fontFamily: 'embedded', fontSize: 20 })).toEqual({
36
+ advance: 5,
37
+ bearingX: 0,
38
+ bearingY: 0,
39
+ height: 0,
40
+ pixels: new Uint8ClampedArray(),
41
+ width: 0,
42
+ });
43
+ });
44
+
45
+ it('sentinels for an unmapped codepoint or invalid em scale', () => {
46
+ const backend = createGlyphRasterizerBackendFromGlyphOutlineSource(createTestGlyphOutlineSource());
47
+
48
+ expect(backend.rasterize(0x42, { fontFamily: 'embedded', fontSize: 20 })).toBeNull();
49
+ expect(backend.rasterize(0x41, { fontFamily: 'embedded', fontSize: 0 })).toBeNull();
50
+ });
51
+ });
52
+
53
+ function createTestGlyphOutlineSource(): GlyphOutlineSource {
54
+ return {
55
+ getGlyphOutline(out: Path, glyphIndex: number): boolean {
56
+ out.commands.length = 0;
57
+ out.data.length = 0;
58
+ out.winding = 'nonZero';
59
+ if (glyphIndex === 1) return true;
60
+ if (glyphIndex !== 0) return false;
61
+ appendPathMoveTo(out, -100, -800);
62
+ appendPathLineTo(out, 500, -800);
63
+ appendPathLineTo(out, 500, 200);
64
+ appendPathLineTo(out, -100, 200);
65
+ appendPathClose(out);
66
+ return true;
67
+ },
68
+ getGlyphOutlineAdvance(glyphIndex: number): number {
69
+ return glyphIndex === 1 ? 250 : 600;
70
+ },
71
+ getGlyphOutlineIndexForCodePoint(codePoint: number): number {
72
+ if (codePoint === 0x41) return 0;
73
+ if (codePoint === 0x20) return 1;
74
+ return -1;
75
+ },
76
+ getGlyphOutlineMetrics() {
77
+ return { ascent: 800, descent: 200, lineGap: 100, unitsPerEm: 1000 };
78
+ },
79
+ };
80
+ }