Skip to content

Instantly share code, notes, and snippets.

@OnCloud125252
Last active February 6, 2024 20:35
Show Gist options
  • Save OnCloud125252/7842f61aa11980a16f275ba9980117bf to your computer and use it in GitHub Desktop.
Save OnCloud125252/7842f61aa11980a16f275ba9980117bf to your computer and use it in GitHub Desktop.
Subset font using opentype.js
import { loadSync, Font } from "opentype.js";
import { readFileSync, readdirSync } from "fs";
import { join } from "path";
const rootDir = process.cwd();
const fontDir = join(rootDir, "fonts");
export default async function handler(req, res) {
if (req.method !== "GET") {
return res.status(405).send({ message: "Method Not Allowed" });
}
try {
const query = req.query;
const { family, text } = query;
const decodedFamily = decodeURIComponent(family);
const decodedText = decodeURIComponent(text);
if (!query || !decodedFamily) {
return res.status(400).send({ message: "Bad Request" });
}
else if (!readdirSync(fontDir).includes(`${decodedFamily}.otf`)) {
return res.status(404).send({ message: "Font Family Not Found" });
}
if (!decodedText) {
res.setHeader("Content-Type", "application/octet-stream");
res.setHeader(`Content-Disposition", "attachment; filename=${decodedFamily}.otf`);
return res.status(200).send(readFileSync(join(rootDir, "fonts", `${decodedFamily}.otf`)));
}
else if (decodedText) {
const subseted = await subsetFont(decodedFamily, decodedText);
res.setHeader("Content-Type", "application/octet-stream");
res.setHeader("Content-Disposition", `attachment; filename=${decodedFamily}.otf`);
return res.status(200).send(subseted.fontBuffer);
}
else {
return res.status(422).send({ message: "Unprocessable Entity" });
}
} catch (error) {
console.error(error);
return res.status(500).send({ message: "Internal Server Error" });
}
}
async function subsetFont(family, requireTexts) {
const fontPath = join(fontDir, `${family}.otf`);
const glyphs = [...new Set(requireTexts.split(""))].join("");
const font = loadSync(fontPath);
const postScriptName = font.getEnglishName("postScriptName");
const [familyName, styleName] = postScriptName.split("-");
const notdefGlyph = font.glyphs.get(0);
notdefGlyph.name = ".notdef";
const subGlyphs = [notdefGlyph].concat(font.stringToGlyphs(glyphs));
const subsetFont = new Font({
familyName: familyName,
styleName: styleName,
unitsPerEm: font.unitsPerEm,
ascender: font.ascender,
descender: font.descender,
designer: font.getEnglishName("designer"),
designerURL: font.getEnglishName("designerURL"),
manufacturer: font.getEnglishName("manufacturer"),
manufacturerURL: font.getEnglishName("manufacturerURL"),
license: font.getEnglishName("license"),
licenseURL: font.getEnglishName("licenseURL"),
version: font.getEnglishName("version"),
description: font.getEnglishName("description"),
copyright: "This is a subset font of " + postScriptName + ". " + font.getEnglishName("copyright"),
trademark: font.getEnglishName("trademark"),
glyphs: subGlyphs
});
return {
fontName: postScriptName,
fontBuffer: arrayBufferToNodeBuffer(subsetFont.toArrayBuffer())
};
}
function arrayBufferToNodeBuffer(arrayBuffer) {
const buffer = Buffer.alloc(arrayBuffer.byteLength);
const view = new Uint8Array(arrayBuffer);
for (let i = 0; i < buffer.length; ++i) {
buffer[i] = view[i];
}
return buffer;
}
/* Original code by Ashung: https://gist.github.com/Ashung/7cbed68bdaab918fd01ff73ea1c2ab75#file-main-js */
import { writeFileSync } from "fs";
import { loadSync, Font } from "opentype.js";
import { join } from "path";
// Define the configs
const fontPath = ["./src/NotoSerifSC-Bold.otf"];
const outputDir = "dist";
const requireTexts = " 0123456789:年月日时分秒公元农历腊零初一二三四五六七八九十廿甲乙丙丁戊己庚辛壬癸子丑寅卯辰巳午未申酉戌亥立春雨水惊蛰春分清明谷雨立夏小满芒种夏至小暑大暑立秋处暑白露秋分寒露霜降立冬小雪大雪冬至小寒大寒";
const glyphs = [...new Set(requireTexts.split(""))].join("");
fontPath.forEach(item => {
const font = loadSync(item);
const postScriptName = font.getEnglishName("postScriptName");
const [familyName, styleName] = postScriptName.split("-");
const notdefGlyph = font.glyphs.get(0);
notdefGlyph.name = ".notdef";
const subGlyphs = [notdefGlyph].concat(font.stringToGlyphs(glyphs));
const subsetFont = new Font({
familyName: familyName,
styleName: styleName,
unitsPerEm: font.unitsPerEm,
ascender: font.ascender,
descender: font.descender,
designer: font.getEnglishName("designer"),
designerURL: font.getEnglishName("designerURL"),
manufacturer: font.getEnglishName("manufacturer"),
manufacturerURL: font.getEnglishName("manufacturerURL"),
license: font.getEnglishName("license"),
licenseURL: font.getEnglishName("licenseURL"),
version: font.getEnglishName("version"),
description: font.getEnglishName("description"),
copyright: "This is a subset font of " + postScriptName + ". " + font.getEnglishName("copyright"),
trademark: font.getEnglishName("trademark"),
glyphs: subGlyphs
});
// Configure the output file
const dist = join(
outputDir,
postScriptName.replace(/-/g, "_").toLowerCase() + "_subset.otf"
);
// Found this method by digging into openfont.js npm package
const buffer = arrayBufferToNodeBuffer(subsetFont.toArrayBuffer());
writeFileSync(dist, buffer);
});
// Credit: https://stackoverflow.com/a/12101012
function arrayBufferToNodeBuffer(arrayBuffer) {
const buffer = Buffer.alloc(arrayBuffer.byteLength);
const view = new Uint8Array(arrayBuffer);
for (let i = 0; i < buffer.length; ++i) {
buffer[i] = view[i];
}
return buffer;
}
@OnCloud125252
Copy link
Author

OnCloud125252 commented Feb 22, 2023

font.js is an api for next.js that can provide real-time font subsetting.

You can specify words you need, the api will respond a subseted font.
By subsetting the font, you can decrease the network usage loading font files.
The effect will be more obvious expecially when using Chinese content.

Requirements

Create a font folder in the root directory which contain .otf font files.
The font files should be renamed to the font family name.
For example for a font file named Example.otf, the result will be font/Example.otf in root folder.

Usage

The request methos is "GET" and the format is /api/font?family=Example&text=requiredString.
Example will be the font family name, requiredString will be the words you want to specify, both querys are required.
Note that requiredString should always be URL encoded.

This is an example of loading a font which family name is Example and specify the required words to be ABC123哈囉.
Start by create a style tag:

<style>{`
    @font-face {
        font-family: Example;
        src: url("/api/font?family=Example&text=${encodeURIComponent("ABC123哈囉")}") format("opentype");
    }
`}</style>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment