Skip to content

Add support for woff2 via an addon #7693

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Apr 7, 2025
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
6 changes: 6 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"version": "2.0.0-beta.5",
"dependencies": {
"@davepagurek/bezier-path": "^0.0.2",
"@japont/unicode-range": "^1.0.0",
"acorn": "^8.12.1",
"acorn-walk": "^8.3.4",
"colorjs.io": "^0.5.2",
Expand Down
156 changes: 120 additions & 36 deletions src/type/p5.Font.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

import { textCoreConstants } from './textCore';
import * as constants from '../core/constants';
import { UnicodeRange } from '@japont/unicode-range';
import { unicodeRanges } from './unicodeRanges';

/*
API:
Expand Down Expand Up @@ -789,7 +791,7 @@ function parseCreateArgs(...args/*path, name, onSuccess, onError*/) {
}

// get the callbacks/descriptors if any
let success, error, descriptors;
let success, error, options;
for (let i = 0; i < args.length; i++) {
const arg = args[i];
if (typeof arg === 'function') {
Expand All @@ -800,11 +802,11 @@ function parseCreateArgs(...args/*path, name, onSuccess, onError*/) {
}
}
else if (typeof arg === 'object') {
descriptors = arg;
options = arg;
}
}

return { path, name, success, error, descriptors };
return { path, name, success, error, options };
}

function font(p5, fn) {
Expand All @@ -816,6 +818,32 @@ function font(p5, fn) {
*/
p5.Font = Font;

/**
* @private
*/
fn.parseFontData = async function(pathOrData) {
// load the raw font bytes
let result = pathOrData instanceof Uint8Array
? pathOrData
: await fn.loadBytes(pathOrData);
//console.log('result:', result);

if (!result) {
throw Error('Failed to load font data');
}

// parse the font data
let fonts = Typr.parse(result);

// TODO: generate descriptors from font in the future
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this also referring to the MultiFont concept, so it would be character faces? Or also style? I really like that idea, would be a great issue / feature addition to make.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Possibly things like a variant name if it exists, weight ranges included, variable ranges, and maybe character set?


if (fonts.length === 0 || fonts[0].cmap === undefined) {
throw Error('parsing font data');
}

return fonts[0];
};

/**
* Loads a font and creates a <a href="#/p5.Font">p5.Font</a> object.
* `loadFont()` can load fonts in either .otf or .ttf format. Loaded fonts can
Expand All @@ -832,8 +860,7 @@ function font(p5, fn) {
*
* In 2D mode, `path` can take on a few other forms. It could be a path to a CSS file,
* such as one from <a href="https://fonts.google.com/">Google Fonts.</a> It could also
* be a string with a <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face">CSS `@font-face` declaration.</a> It can also be an object containing key-value pairs with
* properties that you would find in an `@font-face` block.
* be a string with a <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face">CSS `@font-face` declaration.</a>
*
* The second parameter, `successCallback`, is optional. If a function is
* passed, it will be called once the font has loaded. The callback function
Expand All @@ -850,8 +877,10 @@ function font(p5, fn) {
*
* @method loadFont
* @for p5
* @param {String|Object} path path of the font to be loaded, a CSS `@font-face` string, or an object with font face properties.
* @param {String} path path of the font or CSS file to be loaded, or a CSS `@font-face` string.
* @param {String} [name] An alias that can be used for this font in `textFont()`. Defaults to the name in the font's metadata.
* @param {Object} [options] An optional object with extra CSS font face descriptors, or p5.js font settings.
* @param {String|Array<String>} [options.sets] (Experimental) An optional string of list of strings with Unicode character set names that should be included. When a CSS file is used as the font, it may contain multiple font files. The font best matching the requested character sets will be picked.
* @param {Function} [successCallback] function called with the
* <a href="#/p5.Font">p5.Font</a> object after it
* loads.
Expand Down Expand Up @@ -940,13 +969,6 @@ function font(p5, fn) {
* // Some other forms of loading fonts:
* loadFont("https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,[email protected],200..800&display=swap");
* loadFont(`@font-face { font-family: "Bricolage Grotesque", serif; font-optical-sizing: auto; font-weight: 400; font-style: normal; font-variation-settings: "wdth" 100; }`);
* loadFont({
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I took this out because I was copy-and-pasting this from an earlier comment, but looking at the code, I don't think this actually works on its own. We can flesh out this new usage pattern in examples later, since this current example was from me lazily showing just code and no canvas anyway.

* fontFamily: '"Bricolage Grotesque", serif',
* fontOpticalSizing: 'auto',
* fontWeight: 400,
* fontStyle: 'normal',
* fontVariationSettings: '"wdth" 100',
* });
* </code>
* </div>
*/
Expand All @@ -964,7 +986,7 @@ function font(p5, fn) {
*/
fn.loadFont = async function (...args/*path, name, onSuccess, onError, descriptors*/) {

let { path, name, success, error, descriptors } = parseCreateArgs(...args);
let { path, name, success, error, options: { sets, ...descriptors } = {} } = parseCreateArgs(...args);

let isCSS = path.includes('@font-face');

Expand All @@ -980,7 +1002,7 @@ function font(p5, fn) {
if (isCSS) {
const stylesheet = new CSSStyleSheet();
await stylesheet.replace(path);
const fontPromises = [];
const possibleFonts = [];
for (const rule of stylesheet.cssRules) {
if (rule instanceof CSSFontFaceRule) {
const style = rule.style;
Expand All @@ -996,37 +1018,99 @@ function font(p5, fn) {
.join('');
fontDescriptors[camelCaseKey] = style.getPropertyValue(key);
}
fontPromises.push(create(this, name, src, fontDescriptors));
possibleFonts.push({
name,
src,
fontDescriptors,
loadWithData: async () => {
let fontData;
try {
const urlMatch = /url\(([^\)]+)\)/.exec(src);
if (urlMatch) {
let url = urlMatch[1];
if (/^['"]/.exec(url) && url.at(0) === url.at(-1)) {
url = url.slice(1, -1)
}
fontData = await fn.parseFontData(url);
}
} catch (_e) {}
return create(this, name, src, fontDescriptors, fontData)
},
loadWithoutData: () => create(this, name, src, fontDescriptors)
});
}
}
const fonts = await Promise.all(fontPromises);
return fonts[0]; // TODO: handle multiple faces?

// TODO: handle multiple font faces?
sets = sets || ['latin']; // Default to latin for now if omitted
const requestedGroups = (sets instanceof Array ? sets : [sets])
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@davepagurek what if is array is empty?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If someone passes in sets: [] (as opposed to undefined, which gets the default latin) then it won't match anything, and will fall back to just the last font face included in the file. Probably fine for now?

.map(s => s.toLowerCase());
// Grab thr named groups with names that include the requested keywords
const requestedCategories = unicodeRanges
.filter((r) => requestedGroups.some(
g => r.category.includes(g) &&
// Only include extended character sets if specifically requested
r.category.includes('ext') === g.includes('ext')
));
const requestedRanges = new Set(
UnicodeRange.parse(
requestedCategories.map((c) => `U+${c.hexrange[0]}-${c.hexrange[1]}`)
)
);
let closestRangeOverlap = 0;
let closestDescriptorOverlap = 0;
let closestMatch = undefined;
for (const font of possibleFonts) {
if (!font.fontDescriptors.unicodeRange) continue;
const fontRange = new Set(
UnicodeRange.parse(
font.fontDescriptors.unicodeRange.split(/,\s*/g)
)
);
const rangeOverlap = [...fontRange.values()]
.filter(v => requestedRanges.has(v))
.length;

const targetDescriptors = {
// Default to normal style at regular weight
style: 'normal',
weight: 400,
// Override from anything else passed in
...descriptors
};
const descriptorOverlap = Object.keys(font.fontDescriptors)
.filter(k => font.fontDescriptors[k] === targetDescriptors[k])
.length;

if (
descriptorOverlap > closestDescriptorOverlap ||
(descriptorOverlap === closestDescriptorOverlap && rangeOverlap >= closestRangeOverlap)
) {
closestDescriptorOverlap = descriptorOverlap
closestRangeOverlap = rangeOverlap;
closestMatch = font;
}
}
const picked = (closestMatch || possibleFonts.at(-1));
for (const font of possibleFonts) {
if (font !== picked) {
// Load without parsing data with Typr so that it still can be accessed
// via regular CSS by name
font.loadWithoutData();
}
}
return picked?.loadWithData();
}

let pfont;
try {
// load the raw font bytes
let result = await fn.loadBytes(path);
//console.log('result:', result);

if (!result) {
throw Error('Failed to load font data');
}

// parse the font data
let fonts = Typr.parse(result);

// TODO: generate descriptors from font in the future

if (fonts.length === 0 || fonts[0].cmap === undefined) {
throw Error('parsing font data');
}
const fontData = await fn.parseFontData(path);

// make sure we have a valid name
name = name || extractFontName(fonts[0], path);
name = name || extractFontName(fontData, path);

// create a FontFace object and pass it to the p5.Font constructor
pfont = await create(this, name, path, descriptors, fonts[0]);
pfont = await create(this, name, path, descriptors, fontData);

} catch (err) {
// failed to parse the font, load it as a simple FontFace
Expand Down
Loading