Skip to content

Commit e0a3357

Browse files
authored
Merge pull request #7693 from processing/woff2
Add support for woff2 via an addon
2 parents 1782cb5 + 59e11a0 commit e0a3357

File tree

4 files changed

+1448
-36
lines changed

4 files changed

+1448
-36
lines changed

package-lock.json

+6
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
"version": "2.0.0-beta.5",
2727
"dependencies": {
2828
"@davepagurek/bezier-path": "^0.0.2",
29+
"@japont/unicode-range": "^1.0.0",
2930
"acorn": "^8.12.1",
3031
"acorn-walk": "^8.3.4",
3132
"colorjs.io": "^0.5.2",

src/type/p5.Font.js

+120-36
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
import { textCoreConstants } from './textCore';
66
import * as constants from '../core/constants';
7+
import { UnicodeRange } from '@japont/unicode-range';
8+
import { unicodeRanges } from './unicodeRanges';
79

810
/*
911
API:
@@ -1068,7 +1070,7 @@ function parseCreateArgs(...args/*path, name, onSuccess, onError*/) {
10681070
}
10691071

10701072
// get the callbacks/descriptors if any
1071-
let success, error, descriptors;
1073+
let success, error, options;
10721074
for (let i = 0; i < args.length; i++) {
10731075
const arg = args[i];
10741076
if (typeof arg === 'function') {
@@ -1079,11 +1081,11 @@ function parseCreateArgs(...args/*path, name, onSuccess, onError*/) {
10791081
}
10801082
}
10811083
else if (typeof arg === 'object') {
1082-
descriptors = arg;
1084+
options = arg;
10831085
}
10841086
}
10851087

1086-
return { path, name, success, error, descriptors };
1088+
return { path, name, success, error, options };
10871089
}
10881090

10891091
function font(p5, fn) {
@@ -1095,6 +1097,32 @@ function font(p5, fn) {
10951097
*/
10961098
p5.Font = Font;
10971099

1100+
/**
1101+
* @private
1102+
*/
1103+
fn.parseFontData = async function(pathOrData) {
1104+
// load the raw font bytes
1105+
let result = pathOrData instanceof Uint8Array
1106+
? pathOrData
1107+
: await fn.loadBytes(pathOrData);
1108+
//console.log('result:', result);
1109+
1110+
if (!result) {
1111+
throw Error('Failed to load font data');
1112+
}
1113+
1114+
// parse the font data
1115+
let fonts = Typr.parse(result);
1116+
1117+
// TODO: generate descriptors from font in the future
1118+
1119+
if (fonts.length === 0 || fonts[0].cmap === undefined) {
1120+
throw Error('parsing font data');
1121+
}
1122+
1123+
return fonts[0];
1124+
};
1125+
10981126
/**
10991127
* Loads a font and creates a <a href="#/p5.Font">p5.Font</a> object.
11001128
* `loadFont()` can load fonts in either .otf or .ttf format. Loaded fonts can
@@ -1111,8 +1139,7 @@ function font(p5, fn) {
11111139
*
11121140
* In 2D mode, `path` can take on a few other forms. It could be a path to a CSS file,
11131141
* such as one from <a href="https://fonts.google.com/">Google Fonts.</a> It could also
1114-
* 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
1115-
* properties that you would find in an `@font-face` block.
1142+
* be a string with a <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face">CSS `@font-face` declaration.</a>
11161143
*
11171144
* The second parameter, `successCallback`, is optional. If a function is
11181145
* passed, it will be called once the font has loaded. The callback function
@@ -1129,8 +1156,10 @@ function font(p5, fn) {
11291156
*
11301157
* @method loadFont
11311158
* @for p5
1132-
* @param {String|Object} path path of the font to be loaded, a CSS `@font-face` string, or an object with font face properties.
1159+
* @param {String} path path of the font or CSS file to be loaded, or a CSS `@font-face` string.
11331160
* @param {String} [name] An alias that can be used for this font in `textFont()`. Defaults to the name in the font's metadata.
1161+
* @param {Object} [options] An optional object with extra CSS font face descriptors, or p5.js font settings.
1162+
* @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.
11341163
* @param {Function} [successCallback] function called with the
11351164
* <a href="#/p5.Font">p5.Font</a> object after it
11361165
* loads.
@@ -1219,13 +1248,6 @@ function font(p5, fn) {
12191248
* // Some other forms of loading fonts:
12201249
* loadFont("https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,[email protected],200..800&display=swap");
12211250
* loadFont(`@font-face { font-family: "Bricolage Grotesque", serif; font-optical-sizing: auto; font-weight: 400; font-style: normal; font-variation-settings: "wdth" 100; }`);
1222-
* loadFont({
1223-
* fontFamily: '"Bricolage Grotesque", serif',
1224-
* fontOpticalSizing: 'auto',
1225-
* fontWeight: 400,
1226-
* fontStyle: 'normal',
1227-
* fontVariationSettings: '"wdth" 100',
1228-
* });
12291251
* </code>
12301252
* </div>
12311253
*/
@@ -1243,7 +1265,7 @@ function font(p5, fn) {
12431265
*/
12441266
fn.loadFont = async function (...args/*path, name, onSuccess, onError, descriptors*/) {
12451267

1246-
let { path, name, success, error, descriptors } = parseCreateArgs(...args);
1268+
let { path, name, success, error, options: { sets, ...descriptors } = {} } = parseCreateArgs(...args);
12471269

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

@@ -1259,7 +1281,7 @@ function font(p5, fn) {
12591281
if (isCSS) {
12601282
const stylesheet = new CSSStyleSheet();
12611283
await stylesheet.replace(path);
1262-
const fontPromises = [];
1284+
const possibleFonts = [];
12631285
for (const rule of stylesheet.cssRules) {
12641286
if (rule instanceof CSSFontFaceRule) {
12651287
const style = rule.style;
@@ -1275,37 +1297,99 @@ function font(p5, fn) {
12751297
.join('');
12761298
fontDescriptors[camelCaseKey] = style.getPropertyValue(key);
12771299
}
1278-
fontPromises.push(create(this, name, src, fontDescriptors));
1300+
possibleFonts.push({
1301+
name,
1302+
src,
1303+
fontDescriptors,
1304+
loadWithData: async () => {
1305+
let fontData;
1306+
try {
1307+
const urlMatch = /url\(([^\)]+)\)/.exec(src);
1308+
if (urlMatch) {
1309+
let url = urlMatch[1];
1310+
if (/^['"]/.exec(url) && url.at(0) === url.at(-1)) {
1311+
url = url.slice(1, -1)
1312+
}
1313+
fontData = await fn.parseFontData(url);
1314+
}
1315+
} catch (_e) {}
1316+
return create(this, name, src, fontDescriptors, fontData)
1317+
},
1318+
loadWithoutData: () => create(this, name, src, fontDescriptors)
1319+
});
12791320
}
12801321
}
1281-
const fonts = await Promise.all(fontPromises);
1282-
return fonts[0]; // TODO: handle multiple faces?
1322+
1323+
// TODO: handle multiple font faces?
1324+
sets = sets || ['latin']; // Default to latin for now if omitted
1325+
const requestedGroups = (sets instanceof Array ? sets : [sets])
1326+
.map(s => s.toLowerCase());
1327+
// Grab thr named groups with names that include the requested keywords
1328+
const requestedCategories = unicodeRanges
1329+
.filter((r) => requestedGroups.some(
1330+
g => r.category.includes(g) &&
1331+
// Only include extended character sets if specifically requested
1332+
r.category.includes('ext') === g.includes('ext')
1333+
));
1334+
const requestedRanges = new Set(
1335+
UnicodeRange.parse(
1336+
requestedCategories.map((c) => `U+${c.hexrange[0]}-${c.hexrange[1]}`)
1337+
)
1338+
);
1339+
let closestRangeOverlap = 0;
1340+
let closestDescriptorOverlap = 0;
1341+
let closestMatch = undefined;
1342+
for (const font of possibleFonts) {
1343+
if (!font.fontDescriptors.unicodeRange) continue;
1344+
const fontRange = new Set(
1345+
UnicodeRange.parse(
1346+
font.fontDescriptors.unicodeRange.split(/,\s*/g)
1347+
)
1348+
);
1349+
const rangeOverlap = [...fontRange.values()]
1350+
.filter(v => requestedRanges.has(v))
1351+
.length;
1352+
1353+
const targetDescriptors = {
1354+
// Default to normal style at regular weight
1355+
style: 'normal',
1356+
weight: 400,
1357+
// Override from anything else passed in
1358+
...descriptors
1359+
};
1360+
const descriptorOverlap = Object.keys(font.fontDescriptors)
1361+
.filter(k => font.fontDescriptors[k] === targetDescriptors[k])
1362+
.length;
1363+
1364+
if (
1365+
descriptorOverlap > closestDescriptorOverlap ||
1366+
(descriptorOverlap === closestDescriptorOverlap && rangeOverlap >= closestRangeOverlap)
1367+
) {
1368+
closestDescriptorOverlap = descriptorOverlap
1369+
closestRangeOverlap = rangeOverlap;
1370+
closestMatch = font;
1371+
}
1372+
}
1373+
const picked = (closestMatch || possibleFonts.at(-1));
1374+
for (const font of possibleFonts) {
1375+
if (font !== picked) {
1376+
// Load without parsing data with Typr so that it still can be accessed
1377+
// via regular CSS by name
1378+
font.loadWithoutData();
1379+
}
1380+
}
1381+
return picked?.loadWithData();
12831382
}
12841383

12851384
let pfont;
12861385
try {
1287-
// load the raw font bytes
1288-
let result = await fn.loadBytes(path);
1289-
//console.log('result:', result);
1290-
1291-
if (!result) {
1292-
throw Error('Failed to load font data');
1293-
}
1294-
1295-
// parse the font data
1296-
let fonts = Typr.parse(result);
1297-
1298-
// TODO: generate descriptors from font in the future
1299-
1300-
if (fonts.length === 0 || fonts[0].cmap === undefined) {
1301-
throw Error('parsing font data');
1302-
}
1386+
const fontData = await fn.parseFontData(path);
13031387

13041388
// make sure we have a valid name
1305-
name = name || extractFontName(fonts[0], path);
1389+
name = name || extractFontName(fontData, path);
13061390

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

13101394
} catch (err) {
13111395
// failed to parse the font, load it as a simple FontFace

0 commit comments

Comments
 (0)