// 采集可用字体 → _design-test/font-specimens.json // 来源:① 本地素材库 OFL 字体(可商用) ② 本机已安装系统字体(仅预览) // 说明:本机 Node / 浏览器均无外网,Google Fonts 无法拉取,故不含 Web 字体。 // 用法: node _design-test/harvest-fonts.js const fs = require('fs'); const path = require('path'); const ROOT = path.resolve(__dirname, '..'); // ── ① 本地素材库字体(js/data/posters/*.js,全部 OFL-1.1,可商用)────── const LOCAL = [ { id: 'league-spartan', family: 'League Spartan', cn: 'League Spartan · 几何无衬线', cat: '几何/无衬线', w: '700' }, { id: 'fontsource-montserrat', family: 'Montserrat', cn: 'Montserrat · 几何无衬线', cat: '几何/无衬线', w: '800' }, { id: 'fontsource-poppins', family: 'Poppins', cn: 'Poppins · 圆几何', cat: '几何/无衬线', w: '700' }, { id: 'fontsource-inter', family: 'Inter', cn: 'Inter · 界面无衬线', cat: '几何/无衬线', w: '700' }, { id: 'fontsource-space-grotesk', family: 'Space Grotesk', cn: 'Space Grotesk · 科技无衬线', cat: '科技/等宽', w: '700' }, { id: 'fontsource-cinzel', family: 'Cinzel', cn: 'Cinzel · 罗马大写碑体', cat: '衬线', w: '700' }, { id: 'fontsource-playfair', family: 'Playfair Display', cn: 'Playfair Display · 高对比衬线', cat: '衬线', w: '700' }, { id: 'fontsource-lora', family: 'Lora', cn: 'Lora · 书卷衬线', cat: '衬线', w: '700' }, { id: 'fontsource-oswald', family: 'Oswald', cn: 'Oswald · 窄体', cat: '窄体/粗黑', w: '500' }, { id: 'fontsource-bebas-neue', family: 'Bebas Neue', cn: 'Bebas Neue · 高瘦大写字', cat: '窄体/粗黑', w: '400' }, { id: 'fontsource-jetbrains-mono', family: 'JetBrains Mono', cn: 'JetBrains Mono · 等宽', cat: '科技/等宽', w: '700' }, { id: 'fontsource-caveat', family: 'Caveat', cn: 'Caveat · 手写', cat: '手写/笔刷', w: '700' } ]; // ── ② 本机已安装字体(浏览器实测存在;仅作预览,商用需自行确认授权)──── // [字体名, 分类, 一句话特点] const SYSTEM = [ // 几何 / 无衬线 ['Helvetica', '几何/无衬线', '经典瑞士无衬线,Z 的中性标准'], ['Arial', '几何/无衬线', '最通用的无衬线'], ['Roboto', '几何/无衬线', 'Google 系统无衬线'], ['Segoe UI', '几何/无衬线', 'Windows 系统无衬线'], ['Tahoma', '几何/无衬线', '小字清晰的无衬线'], ['Verdana', '几何/无衬线', '宽字距、易读'], ['Trebuchet MS', '几何/无衬线', '带性格的旧金山体'], ['Calibri', '几何/无衬线', '圆角现代无衬线'], ['Candara', '几何/无衬线', '优雅的人文无衬线'], ['Corbel', '几何/无衬线', '开阔的人文无衬线'], ['Century Gothic', '几何/无衬线', '纯几何圆体'], ['Gill Sans MT', '几何/无衬线', '英式人文无衬线'], ['Franklin Gothic Book', '几何/无衬线', '美式哥特体(细)'], ['Franklin Gothic Medium', '几何/无衬线', '美式哥特体(中)'], ['Franklin Gothic Demi', '几何/无衬线', '美式哥特体(半粗)'], ['Microsoft Sans Serif', '几何/无衬线', 'Windows 经典 UI 体'], ['Tw Cen MT', '几何/无衬线', '二十世纪几何体'], ['Berlin Sans FB', '几何/无衬线', '有切角性格的展示无衬线'], ['Bahnschrift', '几何/无衬线', 'DIN 血统可变窄体'], ['Bahnschrift SemiBold', '几何/无衬线', 'DIN 血统(半粗)'], ['Arial Narrow', '窄体/粗黑', '压缩无衬线'], ['Arial Black', '窄体/粗黑', '超重无衬线'], ['Lucida Sans', '几何/无衬线', '宽圆无衬线'], ['Lucida Sans Unicode', '几何/无衬线', '宽圆无衬线(大字符集)'], ['Microsoft YaHei', '几何/无衬线', '微软雅黑(拉丁部分)'], // 窄体 / 粗黑 / 展示 ['Impact', '窄体/粗黑', '极致粗窄,Z 一坨重量'], ['Haettenschweiler', '窄体/粗黑', '超窄粗体'], ['Britannic Bold', '窄体/粗黑', '英式粗展示体'], ['Agency FB', '窄体/粗黑', '宽方现代展示体'], ['Wide Latin', '窄体/粗黑', '超宽衬线大字'], ['Goudy Stout', '窄体/粗黑', '极重衬线展示体'], // 衬线 ['Times New Roman', '衬线', '最经典衬线'], ['Georgia', '衬线', '屏幕衬线,笔画厚重'], ['Garamond', '衬线', '法式古典衬线'], ['Book Antiqua', '衬线', 'Palatino 血统'], ['Bookman Old Style', '衬线', '厚重书本衬线'], ['Cambria', '衬线', '屏幕阅读衬线'], ['Constantia', '衬线', '人文衬线'], ['Palatino Linotype', '衬线', '人文主义衬线'], ['Century Schoolbook', '衬线', '教科书衬线'], ['Century', '衬线', '古典几何衬线'], ['Calisto MT', '衬线', '过渡期衬线'], ['Bodoni MT', '衬线', '高对比 Didone,Z 斜画极细'], ['Elephant', '衬线', '超重衬线'], ['Engravers MT', '衬线', '刻版罗马大写'], ['Goudy Old Style', '衬线', '老式衬线'], ['High Tower Text', '衬线', '古籍感衬线'], ['Modern No. 20', '衬线', 'Didone 复古'], ['Perpetua', '衬线', ' Gill 的衬线体'], ['Perpetua Titling MT', '衬线', '大写标题衬线'], ['Poor Richard', '衬线', '美式老式衬线'], ['Castellar', '衬线', '碑体大写(带衬脚)'], ['Colonna MT', '衬线', '大写碑体'], ['Imprint MT Shadow', '衬线', '带阴影的碑体'], ['Niagara Engraved', '衬线', '镂空雕刻体'], ['Sylfaen', '衬线', '乔治亚衬线'], ['MS Gothic', '衬线', '日式黑体(拉丁窄)'], ['Yu Gothic', '几何/无衬线', '日式无衬线'], // slab / 方正 ['Rockwell', 'slab/方正', '几何板衬线,Z 方脚'], ['Rockwell Extra Bold', 'slab/方正', '超重板衬线'], ['Cooper Black', 'slab/方正', '圆胖粗衬线'], // 装饰 / ArtDeco / 趣味 ['Broadway', '装饰/趣味', 'ArtDeco 舞台体'], ['Showcard Gothic', '装饰/趣味', '手绘招牌体'], ['Stencil', '装饰/趣味', '模板喷字'], ['Onyx', '装饰/趣味', '超窄高对比'], ['Playbill', '装饰/趣味', '西部木牌体'], ['Algerian', '装饰/趣味', '异域装饰体'], ['Snap ITC', '装饰/趣味', '弹跳趣味体'], ['Juice ITC', '装饰/趣味', '手绘卡通体'], ['Magneto', '装饰/趣味', 'ArtDeco 霓虹体'], ['Jokerman', '装饰/趣味', '花哨趣味体'], ['Curlz MT', '装饰/趣味', '卷曲花体'], ['Chiller', '装饰/趣味', '惊悚手写体'], ['Pristina', '装饰/趣味', '细笔装饰体'], ['Harrington', '装饰/趣味', 'ArtDeco 装饰体'], ['Forte', '装饰/趣味', '笔刷装饰体'], ['Gigi', '装饰/趣味', '少女感装饰体'], ['Maiandra GD', '装饰/趣味', '圆角人文展示体'], ['Tempus Sans ITC', '装饰/趣味', '轻快展示体'], ['Papyrus', '装饰/趣味', '莎草纸手写体'], ['Old English Text MT', '装饰/趣味', '哥特黑体'], ['Viner Hand ITC', '手写/笔刷', '硬笔手写'], // 手写 / 笔刷 ['Brush Script MT', '手写/笔刷', '经典笔刷体'], ['Segoe Script', '手写/笔刷', '系统手写体'], ['Segoe Print', '手写/笔刷', '系统手写印刷体'], ['Freestyle Script', '手写/笔刷', '随性笔刷'], ['French Script MT', '手写/笔刷', '法式连笔'], ['Edwardian Script ITC', '手写/笔刷', '爱德华花体'], ['Palace Script MT', '手写/笔刷', '宫廷花体'], ['Script MT Bold', '手写/笔刷', '粗花体'], ['Vladimir Script', '手写/笔刷', '自由花体'], ['Bradley Hand ITC', '手写/笔刷', '手写便签体'], ['Kristen ITC', '手写/笔刷', '儿童手写体'], ['Lucida Handwriting', '手写/笔刷', '手写连笔'], ['Kunstler Script', '手写/笔刷', '德式装饰花体'], ['Ink Free', '手写/笔刷', '钢笔随手写'], ['Parchment', '手写/笔刷', '羊皮纸手写'], ['Vivaldi', '手写/笔刷', '斜体花体'], ['Comic Sans MS', '手写/笔刷', '手绘马克笔'], // 等宽 / 科技 ['Consolas', '科技/等宽', '编程等宽'], ['Cascadia Code', '科技/等宽', '微软编程等宽'], ['Cascadia Mono', '科技/等宽', '微软等宽'], ['Lucida Console', '科技/等宽', '经典等宽'], ['Courier New', '科技/等宽', '打字机等宽'] ]; function pickLocalFile(files, want) { const has = (re) => files.find(f => re.test(f)); return has(new RegExp('-latin-' + want + '-normal\\.woff2$')) || has(/-latin-400-normal\.woff2$/) || has(/-latin-\d+-normal\.woff2$/) || has(/-latin-.*normal\.woff2$/) || files.find(function (x) { return /\.woff2$/.test(x); }) || files[0]; } const out = []; let missing = 0; for (const f of LOCAL) { const p = path.join(ROOT, 'js', 'data', 'posters', f.id + '.js'); if (!fs.existsSync(p)) { console.log(' ✗ 缺源文件', f.id); missing++; continue; } const t = fs.readFileSync(p, 'utf8'); const files = [...t.matchAll(/"file":"posters\/[^"]+\/([^"]+\.(?:woff2|woff|ttf|otf))"/g)].map(m => m[1]); const latin = files.filter(x => /-latin-/.test(x)); const pick = pickLocalFile(latin.length ? latin : files, f.w); if (!pick) { console.log(' ✗ 无 woff2', f.id); missing++; continue; } out.push({ slug: f.id, family: f.family, cn: f.cn, cat: f.cat, weight: f.w, kind: 'ofl', license: 'OFL-1.1 · 可商用', fmt: (pick.split('.').pop() || 'woff2').toLowerCase(), src: '../js/data/posters/' + f.id + '/files/' + pick }); console.log(' ✓', f.family, '->', pick); } for (const [family, cat, cn] of SYSTEM) { out.push({ slug: 'sys-' + family.toLowerCase().replace(/[^a-z0-9]+/g, '-'), family, cn, cat, weight: '', kind: 'system', license: '系统字体 · 商用需自行授权', src: '' }); } console.log('本地 OFL 字体:', LOCAL.length - missing, '| 系统字体:', SYSTEM.length); const cats = {}; out.forEach(f => { cats[f.cat] = (cats[f.cat] || 0) + 1; }); fs.writeFileSync(path.join(__dirname, 'font-specimens.json'), JSON.stringify({ total: out.length, cats, fonts: out }, null, 0)); console.log('合计', out.length, '款 ·', JSON.stringify(cats));