Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 69 additions & 61 deletions packages/webcanvas/src/core/Font.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@

import { getModule } from '../interop/module';
import { checkResult } from '../common/errors';
import { FontRegistry } from '../utils/FontRegistry';
import type { FontProvider } from './FontProvider';
import { FontsourceProvider } from '../providers/FontsourceProvider';

/**
* Supported font file types.
Expand All @@ -22,34 +23,9 @@ export interface LoadFontOptions {
type?: FontType;
}

/**
* Options for loading a font from fontsource CDN.
* @category Font
*/
export interface FontsourceOptions {
/**
* Font weight to load.
* @default 400
*/
weight?: 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900;
/**
* Font style to load.
* @default 'normal'
*/
style?: 'normal' | 'italic';
/**
* Unicode subset to load.
* @default 'latin'
*/
subset?: string;
}

/** @internal */
const FONTSOURCE_CDN = 'https://cdn.jsdelivr.net/fontsource/fonts';

/**
* Font loader class for managing custom fonts.
* Fonts are loaded globally and can be referenced by name in Text objects.
* Fonts are loaded globally and can be referenced by name in {@link Text} objects.
* @category Font
*
* @example
Expand All @@ -64,32 +40,71 @@ const FONTSOURCE_CDN = 'https://cdn.jsdelivr.net/fontsource/fonts';
*
* @example
* ```typescript
* // Auto-load from fontsource CDN (no manual fetch needed)
* // Auto-load from the configured font provider (fontsource CDN by default)
* await TVG.Font.load('poppins');
* await TVG.Font.load('roboto', { weight: 700, style: 'italic' });
*
* const text = new TVG.Text();
* text.font('poppins').fontSize(48).text('Hello!').fill(50, 50, 50);
* ```
*
* @example
* ```typescript
* // Use a custom font provider
* TVG.Font.provider({
* fetch: async (name) => {
* const res = await fetch(`/my-fonts/${name}.ttf`);
* return { data: new Uint8Array(await res.arrayBuffer()), type: 'ttf' };
* }
* });
*
* await TVG.Font.load('my-font');
* ```
*/
export class Font {
private static _provider: FontProvider = new FontsourceProvider();
private static readonly _loaded = new Set<string>();

/**
* Load font from raw data (Uint8Array)
* Set the font provider used when calling `Font.load()` without raw data.
*
* The default provider fetches from the [fontsource](https://fontsource.org) CDN.
* Replace it to load fonts from your own CDN or any other source.
*
* @param provider - A {@link FontProvider} implementation
* @beta
*
* @example
* ```typescript
* TVG.Font.provider({
* fetch: async (name) => {
* const res = await fetch(`https://my-cdn.com/fonts/${name}.ttf`);
* return { data: new Uint8Array(await res.arrayBuffer()), type: 'ttf' };
* }
* });
* ```
*/
public static provider(provider: FontProvider): void {
Font._provider = provider;
}


/**
* Load font from raw data.
* @param name - Unique name to identify this font
* @param data - Raw font data
* @param data - Raw font binary data
* @param options - Load options
*/
public static load(name: string, data: Uint8Array, options?: LoadFontOptions): void;

/**
* Auto-load a font from the fontsource CDN
* Auto-load a font using the configured font provider.
*
* The font name must match a package on fontsource (e.g. `'poppins'`, `'roboto'`).
* Calling this multiple times with the same arguments is safe — the CDN is only
* fetched once regardless of how many times you call it.
* With the default {@link FontsourceProvider}, the name must match a fontsource
* package slug (e.g. `'poppins'`, `'open-sans'`).
*
* @param name - Font slug as it appears on fontsource (e.g. `'poppins'`, `'open-sans'`)
* @param options - Weight, style, and subset options
* @param name - Font name passed to the provider
* @param options - Provider-specific options (see {@link FontsourceOptions} for defaults)
*
* @example
* ```typescript
Expand All @@ -98,48 +113,41 @@ export class Font {
* await TVG.Font.load('noto-sans', { subset: 'latin-ext' });
* ```
*/
public static load(name: string, options?: FontsourceOptions): Promise<void>;
public static load(name: string, options?: Record<string, unknown>): Promise<void>;

public static load(
name: string,
dataOrOptions?: Uint8Array | FontsourceOptions,
dataOrOptions?: Uint8Array | Record<string, unknown>,
loadOptions?: LoadFontOptions,
): void | Promise<void> {
if (dataOrOptions instanceof Uint8Array) {
Font._loadData(name, dataOrOptions, loadOptions);
return;
}

const opts = {
weight: 400,
style: 'normal' as const,
subset: 'latin',
...dataOrOptions,
};

const key = `${name.toLowerCase()}:${opts.subset}:${opts.weight}:${opts.style}`;

return FontRegistry.ensure(key, async () => {
const slug = name.toLowerCase().replace(/\s+/g, '-');
const url = `${FONTSOURCE_CDN}/${slug}@latest/${opts.subset}-${opts.weight}-${opts.style}.ttf`;

const res = await fetch(url);
if (!res.ok) {
throw new Error(
`Font "${name}" could not be loaded from fontsource (HTTP ${res.status}). ` +
`Check that the font exists at https://fontsource.org/fonts/${slug}`,
);
}

Font._loadData(name, new Uint8Array(await res.arrayBuffer()), { type: 'ttf' });
});
if (Font._loaded.has(name)) {
return Promise.resolve();
}

Font._loaded.add(name);

return Font._provider
.fetch(name, dataOrOptions)
.then((result) => {
Font._loadData(name, result.data, { type: result.type });
})
.catch((err) => {
Font._loaded.delete(name);
throw err;
});
}

/**
* Unload a previously loaded font.
* @param name - Font name to unload
*/
public static unload(name: string): void {
Font._loaded.delete(name);
const Module = getModule();

const namePtr = Module._malloc(name.length + 1);
Expand Down
49 changes: 49 additions & 0 deletions packages/webcanvas/src/core/FontProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/**
* Font provider abstraction for pluggable font sources.
* @category Font
*/

import type { FontType } from './Font';

/**
* Result returned by a {@link FontProvider} after fetching font data.
* @category Font
*/
export interface FontProviderResult {
/** Raw font binary data */
data: Uint8Array;
/** Font format */
type: FontType;
}

/**
* Interface for pluggable font sources.
*
* A font provider resolves a font name into binary font data.
* Implement this interface to load fonts from any source — a self-hosted
* CDN, a local server, or any custom storage.
*
* @category Font
* @beta
*
* @example
* ```typescript
* TVG.Font.provider({
* fetch: async (name) => {
* const res = await fetch(`/my-fonts/${name}.ttf`);
* return { data: new Uint8Array(await res.arrayBuffer()), type: 'ttf' };
* }
* });
*
* await TVG.Font.load('my-font');
* ```
*/
export interface FontProvider {
/**
* Fetch font data by name.
* @param name - Font name as provided by the caller
* @param options - Provider-specific options
* @returns Font binary data and format
*/
fetch(name: string, options?: Record<string, unknown>): Promise<FontProviderResult>;
}
6 changes: 3 additions & 3 deletions packages/webcanvas/src/core/Text.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,9 +87,9 @@ export class Text extends Paint {
}

/**
* Set the font to use for this text
* @param name - Font name (previously loaded via Font.load()) or "default"
*/
* Set the font to use for this text.
* @param name - Font name
*/
public font(name: string): this {
const Module = getModule();

Expand Down
7 changes: 5 additions & 2 deletions packages/webcanvas/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ import { LinearGradient } from './core/LinearGradient';
import { RadialGradient } from './core/RadialGradient';
import { Font } from './core/Font';
import { Accessor } from './core/Accessor';
import { FontsourceProvider } from './providers/FontsourceProvider';
import { ThorVGResultCode, ThorVGError, setGlobalErrorHandler, handleError, type ErrorHandler } from './common/errors';
import * as constants from './common/constants';
import type { RendererType } from './common/constants';
Expand Down Expand Up @@ -286,7 +287,7 @@ const ThorVG = {
export default ThorVG;

// Named exports for advanced usage
export { init, Canvas, Shape, Scene, Picture, Text, Animation, LinearGradient, RadialGradient, Font, Accessor, constants, ThorVGResultCode, ThorVGError };
export { init, Canvas, Shape, Scene, Picture, Text, Animation, LinearGradient, RadialGradient, Font, FontsourceProvider, Accessor, constants, ThorVGResultCode, ThorVGError };

// Re-export types
export type { CanvasOptions } from './core/Canvas';
Expand All @@ -296,7 +297,9 @@ export type { RectOptions, StrokeOptions } from './core/Shape';
export type { LoadDataOptions, PictureSize } from './core/Picture';
export type { TextLayout, TextOutline } from './core/Text';
export type { AnimationInfo, AnimationSegment } from './core/Animation';
export type { LoadFontOptions, FontType, FontsourceOptions } from './core/Font';
export type { LoadFontOptions, FontType } from './core/Font';
export type { FontsourceOptions } from './providers/FontsourceProvider';
export type { FontProvider, FontProviderResult } from './core/FontProvider';
export type { ColorStop } from './core/Fill';
/** @category Canvas */
export type { RendererType } from './common/constants';
Expand Down
71 changes: 71 additions & 0 deletions packages/webcanvas/src/providers/FontsourceProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/**
* Fontsource CDN font provider.
* @category Font
*/

import type { FontProvider, FontProviderResult } from '../core/FontProvider';

const FONTSOURCE_CDN = 'https://cdn.jsdelivr.net/fontsource/fonts';

/**
* Options for loading a font from the fontsource CDN.
* @category Font
*/
export interface FontsourceOptions {
/**
* Font weight to load.
* @default 400
*/
weight?: 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900;
/**
* Font style to load.
* @default 'normal'
*/
style?: 'normal' | 'italic';
/**
* Unicode subset to load.
* @default 'latin'
*/
subset?: string;
}

/**
* Font provider that fetches fonts from the [fontsource](https://fontsource.org) CDN.
*
* This is the default provider used by {@link Font.load} when no raw data is supplied.
*
* @category Font
* @beta
*
* @example
* ```typescript
* // Restore to default (if you previously swapped it out)
* TVG.Font.provider(new FontsourceProvider());
* ```
*/
export class FontsourceProvider implements FontProvider {
async fetch(name: string, options?: FontsourceOptions): Promise<FontProviderResult> {
const opts = {
weight: 400 as const,
style: 'normal' as const,
subset: 'latin',
...options,
};

const slug = name.toLowerCase().replace(/\s+/g, '-');
const url = `${FONTSOURCE_CDN}/${slug}@latest/${opts.subset}-${opts.weight}-${opts.style}.ttf`;

const res = await fetch(url);
if (!res.ok) {
throw new Error(
`Font "${name}" could not be loaded from fontsource (HTTP ${res.status}). ` +
`Check that the font exists at https://fontsource.org/fonts/${slug}`,
);
}

return {
data: new Uint8Array(await res.arrayBuffer()),
type: 'ttf',
};
}
}
35 changes: 0 additions & 35 deletions packages/webcanvas/src/utils/FontRegistry.ts

This file was deleted.

Loading
Loading