From 0f5a7f0d85ab2e79cf626cbf1cf1bd30d8ae7db2 Mon Sep 17 00:00:00 2001 From: douboer Date: Sun, 8 Feb 2026 18:28:39 +0800 Subject: [PATCH] update at 2026-02-08 18:28:39 --- .env.production | 6 + .gitignore | 8 + CDN-DEPLOYMENT.md | 151 + CDN-TEST-GUIDE.md | 168 + PLAN.md | 1134 +- README.md | 14 +- TROUBLESHOOTING-ICONS.md | 181 + USAGE.md | 47 +- apiserver/README.md | 89 + apiserver/__init__.py | 1 + .../__pycache__/renderer.cpython-312.pyc | Bin 0 -> 10091 bytes apiserver/renderer.py | 275 + apiserver/server.py | 332 + convert-icons.js | 59 + fonts.conf | 104 + miniprogram/ICON_FIX.md | 103 + miniprogram/README.md | 54 + miniprogram/UPDATE_LOG.md | 180 + miniprogram/app.js | 9 + miniprogram/app.json | 15 + miniprogram/app.miniapp.json | 5 + miniprogram/app.wxss | 58 + miniprogram/assets/fonts.js | 170 + miniprogram/assets/fonts.json | 170 + miniprogram/assets/icons/Button.svg | 5 + miniprogram/assets/icons/checkbox.png | Bin 0 -> 2758 bytes miniprogram/assets/icons/checkbox.svg | 10 + miniprogram/assets/icons/choose-color.png | Bin 0 -> 7684 bytes miniprogram/assets/icons/choose-color.svg | 21 + miniprogram/assets/icons/expand.png | Bin 0 -> 1456 bytes miniprogram/assets/icons/expand.svg | 3 + miniprogram/assets/icons/export-png.png | Bin 0 -> 4203 bytes miniprogram/assets/icons/export-png.svg | 5 + miniprogram/assets/icons/export-svg.png | Bin 0 -> 4529 bytes miniprogram/assets/icons/export-svg.svg | 5 + miniprogram/assets/icons/export.png | Bin 0 -> 1549 bytes miniprogram/assets/icons/export.svg | 3 + miniprogram/assets/icons/font-icon.png | Bin 0 -> 4189 bytes miniprogram/assets/icons/font-icon.svg | 4 + .../assets/icons/font-size-decrease.png | Bin 0 -> 3657 bytes .../assets/icons/font-size-decrease.svg | 3 + .../assets/icons/font-size-increase.png | Bin 0 -> 1805 bytes .../assets/icons/font-size-increase.svg | 3 + miniprogram/assets/icons/icons_idx _12.svg | 3 + miniprogram/assets/icons/icons_idx _18.svg | 4 + miniprogram/assets/icons/icons_idx _19.svg | 3 + miniprogram/assets/icons/icons_idx _29.svg | 3 + miniprogram/assets/icons/icons_idx _32.svg | 4 + miniprogram/assets/icons/icons_idx _33.svg | 3 + miniprogram/assets/icons/icons_idx _34.svg | 3 + miniprogram/assets/icons/icons_idx _35.svg | 3 + miniprogram/assets/icons/icons_idx _36.svg | 3 + miniprogram/assets/icons/icons_idx _37.svg | 3 + miniprogram/assets/icons/icons_idx _38.svg | 3 + miniprogram/assets/icons/search.svg | 4 + miniprogram/assets/icons/selectall.png | Bin 0 -> 6177 bytes miniprogram/assets/icons/selectall.svg | 6 + miniprogram/assets/icons/unselectall.png | Bin 0 -> 7144 bytes miniprogram/assets/icons/unselectall.svg | 6 + miniprogram/assets/icons/webicon.png | Bin 0 -> 9237 bytes miniprogram/assets/icons/zhedie.svg | 4 + miniprogram/assets/icons/星程字体转换.svg | 10 + miniprogram/config/cdn.js | 40 + miniprogram/i18n/base.json | 11 + miniprogram/pages/font-picker/index.js | 144 + miniprogram/pages/font-picker/index.json | 3 + miniprogram/pages/font-picker/index.wxml | 53 + miniprogram/pages/font-picker/index.wxss | 102 + miniprogram/pages/index/index.js | 605 + miniprogram/pages/index/index.json | 3 + miniprogram/pages/index/index.wxml | 217 + miniprogram/pages/index/index.wxss | 426 + miniprogram/project.config.json | 45 + miniprogram/project.miniapp.json | 68 + miniprogram/sitemap.json | 9 + miniprogram/utils/core/svg-builder.js | 192 + miniprogram/utils/core/text-layout.js | 36 + miniprogram/utils/mp/canvas-export.js | 131 + miniprogram/utils/mp/file-export.js | 49 + miniprogram/utils/mp/font-loader.js | 130 + miniprogram/utils/mp/render-api.js | 67 + miniprogram/utils/mp/storage.js | 60 + miniprogram/utils/mp/worker-manager.js | 93 + miniprogram/utils/mp/wx-promisify.js | 110 + miniprogram/workers/svg-generator/index.js | 105 + .../workers/svg-generator/svg-builder.js | 188 + .../workers/svg-generator/text-layout.js | 34 + .../workers/svg-generator/vendor/opentype.js | 14477 ++++++++++++++++ package-lock.json | 562 + package.json | 9 +- scripts/check-miniprogram-core-test.js | 60 + scripts/check-miniprogram-lint.js | 65 + scripts/deploy-assets.sh | 112 + scripts/deploy-fonts.sh | 170 + scripts/deploy-icons.sh | 56 + scripts/generate-font-list.py | 36 +- scripts/generate-fonts-json.py | 160 + 97 files changed, 22029 insertions(+), 59 deletions(-) create mode 100644 CDN-DEPLOYMENT.md create mode 100644 CDN-TEST-GUIDE.md create mode 100644 TROUBLESHOOTING-ICONS.md create mode 100644 apiserver/README.md create mode 100644 apiserver/__init__.py create mode 100644 apiserver/__pycache__/renderer.cpython-312.pyc create mode 100644 apiserver/renderer.py create mode 100644 apiserver/server.py create mode 100644 convert-icons.js create mode 100644 fonts.conf create mode 100644 miniprogram/ICON_FIX.md create mode 100644 miniprogram/README.md create mode 100644 miniprogram/UPDATE_LOG.md create mode 100644 miniprogram/app.js create mode 100644 miniprogram/app.json create mode 100644 miniprogram/app.miniapp.json create mode 100644 miniprogram/app.wxss create mode 100644 miniprogram/assets/fonts.js create mode 100644 miniprogram/assets/fonts.json create mode 100644 miniprogram/assets/icons/Button.svg create mode 100644 miniprogram/assets/icons/checkbox.png create mode 100644 miniprogram/assets/icons/checkbox.svg create mode 100644 miniprogram/assets/icons/choose-color.png create mode 100644 miniprogram/assets/icons/choose-color.svg create mode 100644 miniprogram/assets/icons/expand.png create mode 100644 miniprogram/assets/icons/expand.svg create mode 100644 miniprogram/assets/icons/export-png.png create mode 100644 miniprogram/assets/icons/export-png.svg create mode 100644 miniprogram/assets/icons/export-svg.png create mode 100644 miniprogram/assets/icons/export-svg.svg create mode 100644 miniprogram/assets/icons/export.png create mode 100644 miniprogram/assets/icons/export.svg create mode 100644 miniprogram/assets/icons/font-icon.png create mode 100644 miniprogram/assets/icons/font-icon.svg create mode 100644 miniprogram/assets/icons/font-size-decrease.png create mode 100644 miniprogram/assets/icons/font-size-decrease.svg create mode 100644 miniprogram/assets/icons/font-size-increase.png create mode 100644 miniprogram/assets/icons/font-size-increase.svg create mode 100644 miniprogram/assets/icons/icons_idx _12.svg create mode 100644 miniprogram/assets/icons/icons_idx _18.svg create mode 100644 miniprogram/assets/icons/icons_idx _19.svg create mode 100644 miniprogram/assets/icons/icons_idx _29.svg create mode 100644 miniprogram/assets/icons/icons_idx _32.svg create mode 100644 miniprogram/assets/icons/icons_idx _33.svg create mode 100644 miniprogram/assets/icons/icons_idx _34.svg create mode 100644 miniprogram/assets/icons/icons_idx _35.svg create mode 100644 miniprogram/assets/icons/icons_idx _36.svg create mode 100644 miniprogram/assets/icons/icons_idx _37.svg create mode 100644 miniprogram/assets/icons/icons_idx _38.svg create mode 100644 miniprogram/assets/icons/search.svg create mode 100644 miniprogram/assets/icons/selectall.png create mode 100644 miniprogram/assets/icons/selectall.svg create mode 100644 miniprogram/assets/icons/unselectall.png create mode 100644 miniprogram/assets/icons/unselectall.svg create mode 100644 miniprogram/assets/icons/webicon.png create mode 100644 miniprogram/assets/icons/zhedie.svg create mode 100644 miniprogram/assets/icons/星程字体转换.svg create mode 100644 miniprogram/config/cdn.js create mode 100644 miniprogram/i18n/base.json create mode 100644 miniprogram/pages/font-picker/index.js create mode 100644 miniprogram/pages/font-picker/index.json create mode 100644 miniprogram/pages/font-picker/index.wxml create mode 100644 miniprogram/pages/font-picker/index.wxss create mode 100644 miniprogram/pages/index/index.js create mode 100644 miniprogram/pages/index/index.json create mode 100644 miniprogram/pages/index/index.wxml create mode 100644 miniprogram/pages/index/index.wxss create mode 100644 miniprogram/project.config.json create mode 100644 miniprogram/project.miniapp.json create mode 100644 miniprogram/sitemap.json create mode 100644 miniprogram/utils/core/svg-builder.js create mode 100644 miniprogram/utils/core/text-layout.js create mode 100644 miniprogram/utils/mp/canvas-export.js create mode 100644 miniprogram/utils/mp/file-export.js create mode 100644 miniprogram/utils/mp/font-loader.js create mode 100644 miniprogram/utils/mp/render-api.js create mode 100644 miniprogram/utils/mp/storage.js create mode 100644 miniprogram/utils/mp/worker-manager.js create mode 100644 miniprogram/utils/mp/wx-promisify.js create mode 100644 miniprogram/workers/svg-generator/index.js create mode 100644 miniprogram/workers/svg-generator/svg-builder.js create mode 100644 miniprogram/workers/svg-generator/text-layout.js create mode 100644 miniprogram/workers/svg-generator/vendor/opentype.js create mode 100644 package-lock.json create mode 100755 scripts/check-miniprogram-core-test.js create mode 100755 scripts/check-miniprogram-lint.js create mode 100755 scripts/deploy-assets.sh create mode 100755 scripts/deploy-fonts.sh create mode 100644 scripts/deploy-icons.sh create mode 100755 scripts/generate-fonts-json.py diff --git a/.env.production b/.env.production index da51fa6..e754790 100644 --- a/.env.production +++ b/.env.production @@ -7,3 +7,9 @@ SUPABASE_SECRET_KEY=sb_secret_8uVHDbhMxE4HOlMBxMWjjw_vsHoZVI8 ALIBABA_CLOUD_ACCESS_KEY_ID=LTAI5tDM8CzT8ABCdYKrfzH8 ALIBABA_CLOUD_ACCESS_KEY_SECRET=hCejmpYoaCGehw2I4jcJo2qd2TwB62 +# 微信小程序 appID和apprSecret +mpAppID=wxeda897f274ff33cf +mpAppSecret=a2240b3b6bbc3f7757ad689a89d334cc + + + diff --git a/.gitignore b/.gitignore index 402788a..8e1baa2 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,11 @@ dist-ssr *.ttf frontend/vite.config.ts frontend/public/fonts.json + +# secrets +.env +.env.* +!.env.example +private.*.key +miniprogram/private.*.key +project.private.config.json diff --git a/CDN-DEPLOYMENT.md b/CDN-DEPLOYMENT.md new file mode 100644 index 0000000..fa68a17 --- /dev/null +++ b/CDN-DEPLOYMENT.md @@ -0,0 +1,151 @@ +# CDN 图标部署指南 + +## 📦 部署流程 + +### 1. 部署图标到服务器 + +```bash +cd /Users/gavin/font2svg +./scripts/deploy-assets.sh +``` + +部署脚本会自动: +- 上传所有 SVG 图标到 `fonts.biboer.cn/assets/icons/` +- 上传 PNG logo 到 `fonts.biboer.cn/assets/` +- 设置正确的文件权限 +- 验证部署结果 + +### 2. 验证部署 + +部署完成后,可以通过浏览器访问以下地址验证: + +``` +https://fonts.biboer.cn/assets/webicon.png +https://fonts.biboer.cn/assets/icons/export.svg +https://fonts.biboer.cn/assets/icons/font-icon.svg +``` + +### 3. 测试小程序 + +在微信开发者工具中: +1. 点击「详情」- 「本地设置」 +2. ✅ 勾选「不校验合法域名、web-view(业务域名)、TLS 版本以及 HTTPS 证书」(开发阶段) +3. 点击「编译」重新加载页面 +4. 检查图标是否正常显示 + +### 4. 配置服务器域名(生产环境) + +在微信小程序后台配置: +1. 登录 [微信公众平台](https://mp.weixin.qq.com/) +2. 开发管理 → 开发设置 → 服务器域名 +3. 将 `https://fonts.biboer.cn` 添加到: + - **request 合法域名** + - **downloadFile 合法域名** +4. 保存配置,等待生效(可能需要几分钟) + +## 🎯 优势对比 + +### 使用 CDN 方案 +✅ **小程序包体积减小** - 图标不占用包大小 +✅ **SVG 高清显示** - 网络地址支持 SVG 格式 +✅ **便于更新维护** - 更新图标无需重新发布小程序 +✅ **统一资源管理** - 字体和图标同一个 CDN + +### 本地资源方案 +❌ 占用包大小(约 50KB 图标) +❌ 需要使用 PNG 格式(SVG 不支持本地) +❌ 更新需要重新发布 +✅ 离线可用 + +## 📝 配置文件说明 + +### `/miniprogram/config/cdn.js` +统一管理所有 CDN 资源路径: + +```javascript +const ICON_PATHS = { + logo: 'https://fonts.biboer.cn/assets/webicon.png', + fontSizeDecrease: 'https://fonts.biboer.cn/assets/icons/font-size-decrease.svg', + // ... 其他图标 +} +``` + +### `/miniprogram/pages/index/index.js` +引入配置并添加到 data: + +```javascript +const { ICON_PATHS } = require('../../config/cdn') + +Page({ + data: { + icons: ICON_PATHS, + // ... + } +}) +``` + +### `/miniprogram/pages/index/index.wxml` +使用数据绑定引用图标: + +```xml + + +``` + +## 🔄 更新图标流程 + +1. 修改本地 `assets/icons/` 中的 SVG 文件 +2. 运行部署脚本:`./scripts/deploy-assets.sh` +3. 小程序无需重新发布,刷新即可看到新图标 + +## 🚨 注意事项 + +1. **HTTPS 要求**:小程序只能加载 HTTPS 资源 +2. **域名配置**:生产环境必须在小程序后台配置合法域名 +3. **缓存策略**:CDN 资源可能有缓存,更新后清除浏览器缓存 +4. **文件大小**:建议 SVG 图标控制在 10KB 以内,确保快速加载 +5. **兼容性**:保留本地 PNG 作为备用方案(可选) + +## 📊 性能对比 + +| 项目 | 本地 PNG | CDN SVG | +|------|---------|---------| +| 包大小影响 | +50KB | 0KB | +| 首次加载 | 立即显示 | ~100ms(网络) | +| 图标清晰度 | 一般 | 高清 | +| 更新成本 | 重新发布 | 无需发布 | + +## 🛠️ 故障排查 + +### 图标不显示 +1. 检查网络请求:开发者工具 → Network +2. 验证 CDN 地址:浏览器直接访问 +3. 检查域名配置:后台是否添加合法域名 +4. 清除缓存:微信开发者工具「清除缓存」 + +### 部署失败 +1. 检查 SSH 连接:`ssh gavin@fonts.biboer.cn` +2. 检查目录权限:服务器上 `/home/gavin/font2svg/assets/` +3. 检查本地文件:`assets/icons/` 是否存在 + +## 📦 服务器目录结构 + +``` +/home/gavin/font2svg/ +├── assets/ +│ ├── webicon.png # 小程序 logo +│ └── icons/ +│ ├── export.svg +│ ├── export-svg.svg +│ ├── export-png.svg +│ ├── font-icon.svg +│ ├── expand.svg +│ ├── font-size-decrease.svg +│ ├── font-size-increase.svg +│ ├── choose-color.svg +│ ├── selectall.svg +│ ├── unselectall.svg +│ └── checkbox.svg +├── fonts/ # 字体文件 +└── fonts.json # 字体列表 +``` diff --git a/CDN-TEST-GUIDE.md b/CDN-TEST-GUIDE.md new file mode 100644 index 0000000..eacd355 --- /dev/null +++ b/CDN-TEST-GUIDE.md @@ -0,0 +1,168 @@ +# ✅ CDN 部署完成 - 测试清单 + +## 📋 已完成项目 + +✅ **Logo 部署** +- https://fonts.biboer.cn/assets/webicon.png (127KB) + +✅ **图标部署** (全部返回 HTTP 200) +- font-icon.svg +- font-size-decrease.svg +- font-size-increase.svg +- choose-color.svg +- checkbox.svg +- expand.svg +- selectall.svg +- unselectall.svg +- export.svg +- export-svg.svg +- export-png.svg + +✅ **代码更新** +- `/miniprogram/config/cdn.js` - CDN 配置文件 +- `/miniprogram/pages/index/index.js` - 引入 CDN 配置 +- `/miniprogram/pages/index/index.wxml` - 使用动态图标路径 + +## 🧪 测试步骤 + +### 1. 开发环境测试 + +在微信开发者工具中: + +**Step 1**: 打开项目设置 +- 点击右上角「详情」 +- 选择「本地设置」标签页 + +**Step 2**: 关闭域名校验(开发阶段) +- ✅ 勾选「不校验合法域名、web-view(业务域名)、TLS 版本以及 HTTPS 证书」 + +**Step 3**: 重新编译 +- 点击「编译」按钮重新加载页面 +- 或按 `Cmd+B` (Mac) / `Ctrl+B` (Windows) + +**Step 4**: 检查显示效果 +``` +检查项: +□ 顶部 logo 显示正常 +□ 字体大小增减图标显示正常 +□ 颜色选择器图标显示正常 +□ 导出按钮图标(3个)显示正常 +□ 字体树折叠图标显示正常 +□ 字体图标显示正常 +□ 复选框图标显示正常 +□ 收藏图标显示正常 +``` + +**Step 5**: 检查网络请求 +- 打开「Network」面板 +- 筛选「image」类型 +- 确认所有请求指向 `fonts.biboer.cn` +- 确认所有请求返回 200 状态码 + +### 2. 如果图标不显示 + +**检查 1**: Console 错误信息 +``` +打开「Console」面板,查看是否有: +- 跨域错误 (CORS) +- 网络请求失败 +- 其他 JavaScript 错误 +``` + +**检查 2**: 网络请求详情 +``` +在 Network 面板中点击失败的请求,查看: +- Request URL: 是否正确 +- Status Code: 是否为 200 +- Response Headers: 是否包含正确的 Content-Type +``` + +**检查 3**: 代码路径 +```javascript +// 在 pages/index/index.js 的 onLoad 中打印 +console.log('图标配置:', this.data.icons) + +// 应该输出: +// { +// logo: 'https://fonts.biboer.cn/assets/webicon.png', +// fontIcon: 'https://fonts.biboer.cn/assets/icons/font-icon.svg', +// ... +// } +``` + +### 3. 生产环境配置(发布前必做) + +**配置服务器域名白名单**: + +1. 登录 [微信公众平台](https://mp.weixin.qq.com/) +2. 进入「开发管理」→「开发设置」→「服务器域名」 +3. 在以下两个位置添加 `https://fonts.biboer.cn`: + - **request 合法域名** + - **downloadFile 合法域名** +4. 点击「保存并提交」 +5. 等待生效(通常 5-10 分钟) + +**验证域名配置**: +- 在微信开发者工具中「取消勾选」域名校验 +- 点击「编译」 +- 如果仍能正常显示,说明域名配置成功 + +## 📊 性能对比 + +### 使用 CDN 后的收益 + +| 指标 | 之前(本地 PNG) | 现在(CDN SVG) | 改善 | +|------|-----------------|----------------|------| +| 小程序包大小 | ~ 50KB | ~ 0KB | ✅ -50KB | +| 图标清晰度 | PNG 栅格化 | SVG 矢量 | ✅ 高清 | +| 更新维护 | 需重新发布 | 无需发布 | ✅ 便捷 | +| 首次加载速度 | 立即 | ~100ms | ⚠️ 略慢 | +| 离线可用性 | ✅ 可用 | ❌ 需网络 | ⚠️ 牺牲 | + +### 资源加载时间 (平均) + +- **Logo (PNG, 127KB)**: ~150ms +- **每个图标 (SVG, ~2KB)**: ~50ms +- **总计首次加载**: ~700ms (并行请求) +- **后续加载**: ~0ms (浏览器缓存) + +## 🔍 故障排查快速索引 + +**问题 1**: 所有图标都不显示 +``` +原因:data.icons 未正确初始化 +解决:检查 index.js 中是否正确引入 cdn.js +``` + +**问题 2**: 部分图标显示,部分不显示 +``` +原因:服务器上文件缺失或路径错误 +解决:验证 CDN URL 能否在浏览器中直接访问 +``` + +**问题 3**: 开发环境正常,真机预览不显示 +``` +原因:未配置服务器域名白名单 +解决:在小程序后台添加 fonts.biboer.cn 到合法域名 +``` + +**问题 4**: 图标加载很慢 +``` +原因:CDN 未开启 GZIP 压缩或缓存策略不当 +解决:检查 nginx 配置,开启 GZIP 和浏览器缓存 +``` + +## 🎯 下一步行动 + +1. ⚡ **立即测试** - 在微信开发者工具中验证显示效果 +2. 📱 **真机预览** - 使用手机微信扫码预览 +3. 🔧 **配置域名** - 在小程序后台添加合法域名(生产必需) +4. 🚀 **准备发布** - 确认所有功能正常后提交审核 + +## 📞 需要帮助? + +如果遇到问题,请提供以下信息: +- 微信开发者工具 Console 截图 +- Network 面板请求详情截图 +- 具体报错信息 +- 是否在真机还是模拟器测试 diff --git a/PLAN.md b/PLAN.md index e58d08f..e644055 100644 --- a/PLAN.md +++ b/PLAN.md @@ -1,70 +1,1116 @@ -# 项目计划(2026-02-07) +# 项目计划(2026-02-08) ## 1. 当前状态 -### 1.1 已完成 +### 1.1 已完成(保留能力) - Web 应用主链路可用:字体选择、预览、收藏、导出(SVG/PNG) -- 字体元数据自动生成:`scripts/generate-font-list.py` -- 预览性能策略已落地:防抖、批处理、并发、懒加载、几何缓存 -- Python CLI 两条脚本可独立运行:`font2svg.py`、`pic2svg.py` +- Python CLI 可独立运行:`font2svg.py`、`pic2svg.py` +- 字体清单生成脚本可用:`scripts/generate-font-list.py` -### 1.2 主要风险 +### 1.2 新增目标 -- 预览仍以 `opentype.js` 直出为主,HarfBuzz 高级模式未接入主流程 -- `frontend/src/App.test.vue` 非标准测试框架用例,自动化覆盖不足 -- 前端未配置统一 lint 脚本,质量门禁不完整 +- 在不影响现有 Web/CLI 的前提下,新增微信小程序版本(新目录:`miniprogram/`) +- 小程序版本以移动端体验为主,不做 Web 页面 1:1 迁移 -## 2. 迭代目标 +## 2. 小程序专项范围 -## M1:工程质量门禁补齐(优先级 P0) +### 2.1 技术选型 -- 增加 `lint`、`typecheck`、`test` 统一脚本 -- 引入标准测试框架(Vitest + Vue Test Utils) -- 产出最小可用测试集(store / utils / 关键组件) +**开发方式**:微信小程序原生开发(TypeScript + WXML + WXSS) +- 理由:充分利用 Worker、Canvas 2D 等原生能力,避免框架转换损耗 +- 放弃 Taro/uni-app:复杂业务逻辑转换可能引入额外兼容问题 -验收标准: +**核心算法复用**:直接复制 Web 端 `utils/` 核心模块 +- `svg-builder.ts`:字形路径生成与 SVG 构建(纯函数,无浏览器 API 依赖) +- `text-layout.ts`:文本换行逻辑 +- `font-loader.ts`:适配为小程序 API(`wx.downloadFile` + `wx.getFileSystemManager`) -- `pnpm -C frontend run lint` -- `pnpm -C frontend run typecheck` -- `pnpm -C frontend run test` -- 三条命令均稳定通过 +**字体资源托管**:云存储(微信云开发 / 第三方 OSS) +- 字体文件上传到云端,生成 CDN URL +- `fonts.json` 清单文件托管(含字体元数据) +- 按需下载,突破小程序 2MB 主包限制 -## M2:预览与导出一致性加强(优先级 P0) +### 2.2 范围内(MVP) -- 统一预览与导出的文本 shaping 策略 -- 对比 `font2svg.py` 输出,建立回归样例 -- 处理复杂脚本、连字、组合字符边界情况 +- 文本输入 + 字体选择(搜索/分类/收藏)+ 实时预览 +- 字号、颜色调节 +- 导出 SVG(文件系统写入 + 分享) +- 导出 PNG(Canvas 2D 渲染 + 保存相册) +- 本地状态持久化(文本、字体、参数) -验收标准: +### 2.3 范围外(后续迭代) -- 关键样例(中英文混排、多行、字距)偏差可控 -- 导出结果与预览视觉一致 +- HarfBuzz/WASM 高级 shaping(首版关闭,使用 opentype.js 基础能力) +- 多字体对比预览(Web 版特性,小程序屏幕受限) +- 批量导出与 ZIP 打包(小程序 API 限制) +- 字体树分类折叠展开(简化为扁平列表 + 搜索) -## M3:大字体库性能优化(优先级 P1) +## 3. 移动端差异与技术策略 -- 增加字体对象 LRU 缓存与内存上限策略 -- 优化 `fonts.json` 增量生成能力 -- 导出阶段增加并发与失败重试上限 +### 3.1 交互策略 -验收标准: +- 单列流程:输入 -> 预览 -> 导出 +- 大按钮、触控优先、避免多栏复杂布局 -- 100+ 字体时首屏可交互时间维持稳定 -- 批量导出失败率下降 +### 3.2 性能策略 -## M4:文档与运维完善(优先级 P1) +- 字形生成放入 `Worker`,主线程专注渲染 +- 长文本分批处理,支持任务取消 +- 字体对象缓存与内存上限控制 -- 保持 `README.md` / `USAGE.md` / `DETAIL-DESIGN.md` 同步 -- 补充发布流程与版本变更记录模板 +### 3.3 平台适配策略 -## 3. 执行原则 +- 文件系统:`wx.getFileSystemManager()` + `writeFile/saveFile` +- PNG 导出:`wx.canvasToTempFilePath` +- 文件分享:`wx.shareFileMessage` +- 图片保存:`wx.saveImageToPhotosAlbum`(带授权处理) +- 预览:优先 `image` 组件渲染 SVG,必要时回退 PNG -- 小步提交:每次改动聚焦单一目标 -- 先验证再交付:测试、类型检查、lint 必跑 -- 文档与代码同步更新 +### 3.4 包体策略 -## 4. 里程碑建议 +- 字体与 Worker 支持分包/按需加载 +- 首包仅保留必要资源 + +## 4. 目录结构设计 + +``` +miniprogram/ + ├── pages/ # 页面 + │ ├── index/ # 主页(输入+预览+导出) + │ │ ├── index.wxml + │ │ ├── index.ts + │ │ ├── index.wxss + │ │ └── index.json + │ └── font-picker/ # 字体选择页 + │ ├── font-picker.wxml + │ ├── font-picker.ts + │ ├── font-picker.wxss + │ └── font-picker.json + ├── components/ # 组件 + │ ├── text-input/ # 文本输入组件 + │ ├── font-item/ # 字体列表项 + │ ├── preview-card/ # 预览卡片 + │ └── export-panel/ # 导出面板 + ├── workers/ # Worker 线程 + │ └── svg-generator/ # SVG 生成 Worker + │ └── index.ts + ├── utils/ # 工具函数 + │ ├── core/ # 核心算法(复用 Web) + │ │ ├── svg-builder.ts # SVG 生成核心 + │ │ ├── text-layout.ts # 文本换行 + │ │ └── glyph-path.ts # 字形路径 + │ ├── mp/ # 小程序专用 + │ │ ├── font-loader-mp.ts # 字体加载适配 + │ │ ├── storage-mp.ts # 存储适配 + │ │ ├── canvas-export.ts # Canvas 导出 + │ │ └── share-mp.ts # 分享/保存 + │ └── worker-manager.ts # Worker 管理器 + ├── typings/ # 类型定义(复用 Web types/) + │ └── font.d.ts + ├── assets/ # 静态资源 + │ └── icons/ # 图标(复用 Web) + ├── styles/ # 全局样式 + │ └── variables.wxss # 设计 token(基于 Figma) + ├── miniprogram_npm/ # npm 构建产物 + ├── app.ts # 小程序入口 + ├── app.json # 全局配置 + ├── app.wxss # 全局样式 + ├── package.json # npm 依赖 + ├── tsconfig.json # TypeScript 配置 + └── project.config.json # 微信开发者工具配置 +``` + +## 5. 里程碑与验收 + +### M1:工程骨架(P0) + +**实施步骤**: +1. 创建 `miniprogram/` 目录,微信开发者工具初始化原生项目 +2. 配置 `project.config.json`: + - 设置 `miniprogramRoot`、基础库版本 ≥ 2.9.0 + - 启用 TypeScript 编译插件 +3. 创建 `package.json`,安装依赖: + ```json + { + "dependencies": { + "opentype.js": "^1.3.4" + }, + "devDependencies": { + "miniprogram-api-typings": "latest", + "typescript": "~5.9.3" + } + } + ``` +4. 配置 `tsconfig.json`(参考 `frontend/tsconfig.json`) +5. 创建 `app.ts`、`app.json`、`app.wxss` +6. 创建首页 `pages/index/`(空白页面) +7. 微信开发者工具"构建 npm" + +**验收标准**: +- 开发者工具可启动并进入首页(显示空白页面) +- TypeScript 编译无错误 +- npm 包构建成功(`miniprogram_npm/` 生成) + +### M2:核心算法迁移(P0) + +**实施步骤**: +1. **复制纯算法模块**: + - `frontend/src/utils/svg-builder.ts` → `miniprogram/utils/core/svg-builder.ts` + - 保留 `generateSvg()` 函数(移除 `generateSvgWithHarfbuzz()`) + - `frontend/src/utils/text-layout.ts` → `miniprogram/utils/core/text-layout.ts` + - 提取字形路径逻辑到 `miniprogram/utils/core/glyph-path.ts` + +2. **复制类型定义**: + - `frontend/src/types/font.d.ts` → `miniprogram/typings/font.d.ts` + +3. **平台适配层开发**: + - `miniprogram/utils/mp/font-loader-mp.ts`: + - `loadFontFromUrl()`:使用 `wx.downloadFile()` + `wx.getFileSystemManager()` + - `miniprogram/utils/mp/storage-mp.ts`: + - 封装 `wx.getStorageSync/setStorageSync` + - 提供类型安全的 `getStorage`、`setStorage` 方法 + +4. **编写单元测试**: + - `miniprogram/utils/core/__tests__/svg-builder.test.ts` + - 测试用例:单字符、多字符、空字符串、特殊字符 + +**验收标准**: +- 输入文本 + Font 对象可生成有效 SVG 字符串 +- SVG 结构正确(包含 ``、``、`viewBox`) +- 核心算法单测通过(覆盖率 > 80%) + +### M3:预览链路(P0) + +**实施步骤**: +1. **主页布局**(`pages/index/`): + - 文本输入区(` diff --git a/miniprogram/assets/icons/choose-color.png b/miniprogram/assets/icons/choose-color.png new file mode 100644 index 0000000000000000000000000000000000000000..600f74a6c06dcd0017d777d534ac0030ba8a0f46 GIT binary patch literal 7684 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 + + + + + + + + + + + + + + + + + + + + diff --git a/miniprogram/assets/icons/expand.png b/miniprogram/assets/icons/expand.png new file mode 100644 index 0000000000000000000000000000000000000000..29f40e0c65ae8d751dc874a72653f909c439309b GIT binary patch literal 1456 zcmZ{k`9Bj30LEuyX71hz8xDF>fUl&3aBg$J1g}iD}@A;v~gC@K7p4mQ-@OAAIgM;OobHu(x3fR42 z)wUNR%me0D?Mj!T)8aCiBu>w9X3ijtuS-M7fe&^my`U@5g+58Tr*J~`Gj|LmfIkFW z%XgRU{RL<;=po^%X3X?qgaZInoiXs-f7gH$WX5i`ovxcH2n{YZyVJ1UAaJ)CqXQ0d z5`4?Gz2fh%s&RjvMZt^ivM-DmNE&2(x6 zx6_U7rszFkK)326`9($X+TcesCq7)mW)QLdAppD<=csEvUg+{k$nIW`i5Js=>agV* zOWa#u$9?0!B5s+NB@m+9Y1X5$W;G`O=5?BzZ!C**9g? zB#3&NX!e@ov6~BZ9f7G41ujjQeYhQG(|!_rlVoM1KsX?$3>wbUSxp9V-4swk>IRdz zfXiI!O@IfQpy`eq#kE?2?;tt_{tC;oiPhzMnR6rCXbq63QtDd0^R^!cN%@_J zj}yrcIyN<0j3{Th%JA|@(|+sOJ9U$taj6}ab4ZABP05NPUsm4H0^qDDs0ixqQw4aN z0BaE_MveLSp(A}`EyJg*WJ{g?P9XV%Yq^X`MC8P$b{`Jy)c%P1BX}Gdnz)Esp-U=Exsa zD@=jZQb(H;&c{r+>W2qKk7Ny~whqnfi?VoE{L(a=I()XP3RjIMyI|jQ?_bD5#p4>z zM9*OmF=K?)YXk4z&~E9EEsQrkJ7Sa&nH01AKw80A{@`_4#3yQ;gh^MXg5e%@lxM)r_!xH}o D7x1Re literal 0 HcmV?d00001 diff --git a/miniprogram/assets/icons/expand.svg b/miniprogram/assets/icons/expand.svg new file mode 100644 index 0000000..d8b652a --- /dev/null +++ b/miniprogram/assets/icons/expand.svg @@ -0,0 +1,3 @@ + + + diff --git a/miniprogram/assets/icons/export-png.png b/miniprogram/assets/icons/export-png.png new file mode 100644 index 0000000000000000000000000000000000000000..8a68af4a275426d83bb0edb76861c999f1b7b792 GIT binary patch literal 4203 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 + + + + diff --git a/miniprogram/assets/icons/export-svg.png b/miniprogram/assets/icons/export-svg.png new file mode 100644 index 0000000000000000000000000000000000000000..6343e04a6d2a7db0f775ac134373b349f381e6b8 GIT binary patch literal 4529 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 P00000NkvXXu0mjf&k%z- literal 0 HcmV?d00001 diff --git a/miniprogram/assets/icons/export-svg.svg b/miniprogram/assets/icons/export-svg.svg new file mode 100644 index 0000000..1e581d2 --- /dev/null +++ b/miniprogram/assets/icons/export-svg.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/miniprogram/assets/icons/export.png b/miniprogram/assets/icons/export.png new file mode 100644 index 0000000000000000000000000000000000000000..1d6d5c13d56bd67c3583f555dd064c596f7caac0 GIT binary patch literal 1549 zcma)6dpOez82*jPC1Vw>oTZ`Kq%)MFHjI#4o6TjYwbEsa5SmL3n{G#);WNGV*^sGF9CQj0ewIzaV{*WEG* z?Y5<53#;z>@83TqbHk-O&p8Z9dXjVHJvqlJFfk`yEvkPx`QZ_5Y@tp5QZVn}vx$$z zZ*|8T5FH1|k<4&;@cq=juA-fFdFz?ESrd2OzdfQm0PC}E@N^MaKNISlvl&b884ufI zm`d3&#R9~dng!ntIokIIgKsf8X1IiVKsaMDW?-epWpxBA;s=tf*%P+6v2WgIev1H)d7Lz(XpJ6Gv{-TC;(`+WE!0{?ZTmQPj?}!vPbOW@ zI$L8hCG0_nf(bW9<^_>cZL>O@`->BY<>^uH%9#42reIIYb{L^;-CT0c<4J{rn>Z3{ zBg~r6F{eyUi`{vJ5MVICXAC(&SZFzF%?y*D!O85D`K45ls%nk&Agg2e{Ymy+FJ;w! z)(LYHizG)@ji)>S?>68PS+|8G*TGKPV-yPOD8e0@ldw67WKP{HL95#`In4~c)Q;GJ zrTYh9nKZnPfs|o9legT#6(dHVDY3A<_n#T=3jp#|-J58F5l%#J7oSw5%vv+zv_NTUU<;jJ(B7Z6VPBw2 zpWK6Ty>MeetWDg55|hEy&f}(Flp#m#mf+5lgcNhnt@XX)2y~&q_c=d>?h4eJLy0T{+NOwPVs`#q%ttRMUMV3#` zxQF=NY=hbtVC}c6@UFYl3vKc96?z4qt5au>PffoV@rFbEE!v)orZ)S}ZP$Oo^;3_W zftO!ZWAs3C7bYWas%+v`vQ6n2((>F^T?gO=4(R)zNCuBTiZiEUYCfuB*w|eP#e8|6 zg(1RTrQb+T)e!--0LYQT{86;tx;44oVF*dl?EFg9QJ>Q%E^^WQK*(Kt-&=4E890nU zzLM|QfPQpdJzagr`HQNcpLhcSxx`RE@v=dbpu}(6NO6C{7Y8?jkW<@UT>v=j%?V+> z$704KNC~!#L!ZD_urt@XC#!bCR8ZmCIgF5D8IHAZzBE%Jx{`Lidx4(SgTBl?j4%Z7OB8+$$FC0F3$XJvX)+@@k0jW)^QyNT4^SL+_16PM z!qY6-P~Z%-oIi%7=zZ3DlJ{CE5Vs1G4k|N#h36>X-gI;O2~X5;`7E`VSw#z25); literal 0 HcmV?d00001 diff --git a/miniprogram/assets/icons/export.svg b/miniprogram/assets/icons/export.svg new file mode 100644 index 0000000..1d20331 --- /dev/null +++ b/miniprogram/assets/icons/export.svg @@ -0,0 +1,3 @@ + + + diff --git a/miniprogram/assets/icons/font-icon.png b/miniprogram/assets/icons/font-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..50652efe0bc7c56536efc3b04d58cae8e47de6c7 GIT binary patch literal 4189 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 literal 0 HcmV?d00001 diff --git a/miniprogram/assets/icons/font-icon.svg b/miniprogram/assets/icons/font-icon.svg new file mode 100644 index 0000000..a24279a --- /dev/null +++ b/miniprogram/assets/icons/font-icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/miniprogram/assets/icons/font-size-decrease.png b/miniprogram/assets/icons/font-size-decrease.png new file mode 100644 index 0000000000000000000000000000000000000000..427f6ab33d81c19ab0701f8447f0fcaacf6e9dae GIT binary patch literal 3657 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 YQtinieeNqKnGgUhu2{n>jXfUy5AWdo8~^|S literal 0 HcmV?d00001 diff --git a/miniprogram/assets/icons/font-size-decrease.svg b/miniprogram/assets/icons/font-size-decrease.svg new file mode 100644 index 0000000..d86d60e --- /dev/null +++ b/miniprogram/assets/icons/font-size-decrease.svg @@ -0,0 +1,3 @@ + + + diff --git a/miniprogram/assets/icons/font-size-increase.png b/miniprogram/assets/icons/font-size-increase.png new file mode 100644 index 0000000000000000000000000000000000000000..6b056418f84fbe0eda98009afe985aca472693c0 GIT binary patch literal 1805 zcmYjSdo-Kb7XLy#qm6h(A6gO3q6iIDQ&iG4T2Uol_Sxrm_SwI+_da{AolEgMwncx3 zJ^%n)$ljztwPtRLuC{vb@r)i)E4>u&@KgYBu-X*Jg}eB40MPCxlMV-8xcx`@368m! z3G=MVD`0Xx8?Vbk+p-`F??_4f**i>I74B6VD<; z?J&A5)IS%Lnv$Zd_Mpwib2r?B&51x?fBz^{+=*Cd4dA1eb>38%A(kw9bL~u=2}V3ql8TLi_+IqEY3_k6QjhkSm(%<-@v-6QRaB_A z#_&8UBijyA7>VXn@&Ph4yeUz!9cZ~!2_;mwnOYHyqKZ8lgKu@_elrMK^N zOxCO?q!9Dy$R-p+&wZR3ar;ZU%5=OLsL6$FUIpM!cYfC4O|6hA-QY3JPuUd^9;Zc^}AVh&z&Ku`B6cEbn9XC6^m&ePpA*Jj4(yP@@S0veLeq4fCtA z=G)rehMnB%xL-cuQgTPE0r4uhqp67FM`N7o9+HTwcu)0vMV@InU}2#=#D4Nj!4R-E zUwm}uQh+xb``+P=*A?K zE?Tv%W1*=3{R1w0X=%wAc>AZs;NIw>BWR+qyY&_;5}@S4v+RC7Iv7c_o9j6 zw(HTEna3W9mfkpsxn%TeWo1Z7`bRpcqaqIry^ndQAsH2p$V>W~)r;*(MIPonady&gloHbIJIvAj31Ti&!Qxg&I*|xF$vx0_5Z> z7V@M--}=qB)L7NJ2Jf`}IW9K1SP|5U`%=Y=E6g;Z!k+w~T4zyViXP=yPEJn0uqpzmI3XLqzQhGh+C?QNpm`XWh@XY6G|Q{2G&6S4HHo)TF-+WLAs`zznVSs>WP zbZVotE-IH?QK_(vFnk7|3RUODZ)YgC49*hELHy!<@JW9D*sTE{HEB>A*V0C42v`4+ zAy{K9&ynmE84S!4)m>3l`fCJEJ1)8Mo+8`3JHjZclR8o}jD00kdkJkJ`xNbLbBX_h zc1EpJn_+bnto@C0%-I&9y^DrE{up`wH$}|Pf)Krb?-k|d-%%_azgmS2HYsrbE&?wi zoDmu_WoHuJA9nT=7(s)Y*s~INQef`2yRoqLdW&A=`J4Mnx!7KHKVbAg8&h>~pM?}# zb;;|KDz)k>k}6~B^j$_$VXrTqe17Tc*r2#EWDcpp>o&A@q!?7JZycx8ztFeFsC1ecM?`r^RmC@q*CdI+J*)SKp_57>~(7EFW-Z7OEe#c tOmP}N@{@91pV{Xk`thF=Lw&9pUpJf4y5Jkw8MQgz0J4`KNkEKZ{S8HdM?wGq literal 0 HcmV?d00001 diff --git a/miniprogram/assets/icons/font-size-increase.svg b/miniprogram/assets/icons/font-size-increase.svg new file mode 100644 index 0000000..104d16b --- /dev/null +++ b/miniprogram/assets/icons/font-size-increase.svg @@ -0,0 +1,3 @@ + + + diff --git a/miniprogram/assets/icons/icons_idx _12.svg b/miniprogram/assets/icons/icons_idx _12.svg new file mode 100644 index 0000000..1eb83c1 --- /dev/null +++ b/miniprogram/assets/icons/icons_idx _12.svg @@ -0,0 +1,3 @@ + + + diff --git a/miniprogram/assets/icons/icons_idx _18.svg b/miniprogram/assets/icons/icons_idx _18.svg new file mode 100644 index 0000000..a9d2261 --- /dev/null +++ b/miniprogram/assets/icons/icons_idx _18.svg @@ -0,0 +1,4 @@ + + + + diff --git a/miniprogram/assets/icons/icons_idx _19.svg b/miniprogram/assets/icons/icons_idx _19.svg new file mode 100644 index 0000000..10a312a --- /dev/null +++ b/miniprogram/assets/icons/icons_idx _19.svg @@ -0,0 +1,3 @@ + + + diff --git a/miniprogram/assets/icons/icons_idx _29.svg b/miniprogram/assets/icons/icons_idx _29.svg new file mode 100644 index 0000000..1eb83c1 --- /dev/null +++ b/miniprogram/assets/icons/icons_idx _29.svg @@ -0,0 +1,3 @@ + + + diff --git a/miniprogram/assets/icons/icons_idx _32.svg b/miniprogram/assets/icons/icons_idx _32.svg new file mode 100644 index 0000000..a24279a --- /dev/null +++ b/miniprogram/assets/icons/icons_idx _32.svg @@ -0,0 +1,4 @@ + + + + diff --git a/miniprogram/assets/icons/icons_idx _33.svg b/miniprogram/assets/icons/icons_idx _33.svg new file mode 100644 index 0000000..0d38a96 --- /dev/null +++ b/miniprogram/assets/icons/icons_idx _33.svg @@ -0,0 +1,3 @@ + + + diff --git a/miniprogram/assets/icons/icons_idx _34.svg b/miniprogram/assets/icons/icons_idx _34.svg new file mode 100644 index 0000000..b760809 --- /dev/null +++ b/miniprogram/assets/icons/icons_idx _34.svg @@ -0,0 +1,3 @@ + + + diff --git a/miniprogram/assets/icons/icons_idx _35.svg b/miniprogram/assets/icons/icons_idx _35.svg new file mode 100644 index 0000000..b0168a5 --- /dev/null +++ b/miniprogram/assets/icons/icons_idx _35.svg @@ -0,0 +1,3 @@ + + + diff --git a/miniprogram/assets/icons/icons_idx _36.svg b/miniprogram/assets/icons/icons_idx _36.svg new file mode 100644 index 0000000..b760809 --- /dev/null +++ b/miniprogram/assets/icons/icons_idx _36.svg @@ -0,0 +1,3 @@ + + + diff --git a/miniprogram/assets/icons/icons_idx _37.svg b/miniprogram/assets/icons/icons_idx _37.svg new file mode 100644 index 0000000..b760809 --- /dev/null +++ b/miniprogram/assets/icons/icons_idx _37.svg @@ -0,0 +1,3 @@ + + + diff --git a/miniprogram/assets/icons/icons_idx _38.svg b/miniprogram/assets/icons/icons_idx _38.svg new file mode 100644 index 0000000..700c186 --- /dev/null +++ b/miniprogram/assets/icons/icons_idx _38.svg @@ -0,0 +1,3 @@ + + + diff --git a/miniprogram/assets/icons/search.svg b/miniprogram/assets/icons/search.svg new file mode 100644 index 0000000..b6d1550 --- /dev/null +++ b/miniprogram/assets/icons/search.svg @@ -0,0 +1,4 @@ + + + + diff --git a/miniprogram/assets/icons/selectall.png b/miniprogram/assets/icons/selectall.png new file mode 100644 index 0000000000000000000000000000000000000000..433ee7aef751ebd2526c805af48c9f942ef04a3c GIT binary patch literal 6177 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 literal 0 HcmV?d00001 diff --git a/miniprogram/assets/icons/selectall.svg b/miniprogram/assets/icons/selectall.svg new file mode 100644 index 0000000..ef85dac --- /dev/null +++ b/miniprogram/assets/icons/selectall.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/miniprogram/assets/icons/unselectall.png b/miniprogram/assets/icons/unselectall.png new file mode 100644 index 0000000000000000000000000000000000000000..9f0291e636a3f6a5a3a0a602b8a36bb479242d7d GIT binary patch literal 7144 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 e1uIw$w*DW;i0@x(gHd|`0000 + + + + + diff --git a/miniprogram/assets/icons/webicon.png b/miniprogram/assets/icons/webicon.png new file mode 100644 index 0000000000000000000000000000000000000000..05220b446b4acb95afcbce9d166567ce399b6aba GIT binary patch literal 9237 zcmZ{K1ymf{vi9I^gF6iF?(XgqG{M~m8InM7cXxLufgr&doZueZ6M}09^2j;&zxVui zz1O{VSM^u*)mOWFuhrdab)=@c0tPAxDgXe$P*RlDezAgo1~THyu_8=_;>Ez*N~uW! z0FCiz59SCj=Tw%8+G+s6TY3N>AOrxoe}MuH0RS&f0N{ri03e(J01&$6v}=jJ1R&N1 zN;Yb00Ol7T82}Fh1i-y`FfTg*h6M1Z+KUGehavs9FCT6Xfd3b+{$l@ZiZA0&-rp9k z0Omhc3Sj@8Em#2eAO8<+dt`9;#h|z;8iD};G~7P}29T9Q1OULM*y|d28mOrXTe>>4 zn_IbBShGW%-Ttruq7dO1*V)?BoC@OXwj5kSLP8vz z+#KB8Y%dvXU|$zca|oLYnC9;y|F4d$HQ3U_-p$kA)rIO$U2_XpFHbQL=#S99x4--J zw72<>k_-4>(|Va8$DarX7dt1%|3N$;r|2w)8F3&ga3{2Pw>A98Xor6FPG#m1+ISy{1f{}{+~XDHSHnRP6o2}&eksA zKV5P2@r!c&uaW;|%D6hYdg!>BTUv|12>nI*2lU_Bf9V+hpF01D{EI2d@#nt%<39eq zNd7`!R*5*OD9687oH%Ok;K(}wfXY@$R!Y|n!TPOjhRJ;9zOffecwOCuRBozFFaRYG zphOVcs5IDs&blOGlhJcxCVDg!>@50a>9XJK-TwK{rJcs`-rz(on}*O{U@yE9VW$~n%1XkFiN1+Omxi?59LDXBAUpO$<` z`r_L%+qsjhp!zO$ID4v$t|L!%P?2c?nwV6=jqNjHg5E620!-Fna&5|}rIJP# z85B5XI=l=Q=k0&%Tu>=upiWEw1VuDf9loX*N9WFQ>24xZ9(_A%@ z+J-;I)AGX22p07Gem=~d8DE5!0_r-Rhz=(>~IPp%cx2qUwv~Nouf6|8jz3k(lcVhL&LO+f~ozA2^_?{OiW=7#TH=oK{;|q z?q-SGZtK&#v#E>%XvJoS~K<=2$z-puIHo6zEI-LJS2TFDL-3UPa_1DF0;Va!ajf{1pc)l*ydgN=|lePZp%XqlByF$4%GT&&!6% z7&QX@LR)!6vvtVP=4;uO!rDXqOjd|izp0rIRqOHfU{eC^?21|{jvR-?qr-@;G3?<( zYNuj~&yr!(S8Op!m6qn?K2>wi~gprl$}9qBe;6iU@b24N`*NoUNmj&u)>Z8~A3HeH}0 zvdGR=13EpX#78-aav&y-ZUYPsdnxx>=%d@a?}&(TmKtlOt(6^kMCu;oRUR@3(q!r7 z0;1ySfz$Kylmua7!E|N|N#BE*Z0lcTuYt(S2hS4q3ys7wy1b@uANI8i=U;$u%lV zl4z_t_fRE`Q7;+=-np8!s3LBTTW~d`PH5N0FcFeQHmNBW?1|j$tDL#+6;dTK$TDAG z)+ke^Xm1Sjc7wn{8b1;(ktqgDr#zFLsZe82_nLXG*kmzg0qwwT=%70SV}xwNt(I*^ zrq$a8{n%67upI566MvZZ^Je!(CkcakY(aBdkWt=HPp{9}=GUQVwPXL&9wRH3AJ~*R zLFqrfE#?p93c$Bfoc(@7+fXnR4$n87_?*pWOyr*TyLxkq|s4DpJEcQGRs( zCanp?&lUT8zubK1&K=nw|HRPsK|??}*}i0$k&utRqk;k-L#m1OtNF+{eBFmDg}a$_ z<-6#i;cVidAL{6O#}0#LrzO5EzP{*>JpH5MH~ zRfrX&Vse}ztzmyMf@DNJiVsSP&}orku#ZTWjpf-=T&}gyi$$AIC#=Em@NUzpsO_0D zA){317%a=XhFpkr%hL13Bb~INP*RFx99OM>+L&z}FBRTv@9>oj$_UXGT4y9rA*XK+9j+^~cw|8X-i7rT} z2g!;GzRnm_&(roL>8w4?(beZiKFMzEWSqn+pFS{d87yMhO&m1t(28-+d*r3?YW4g3 zW=ztSUNcLSReY|}G>Nmo0h76ThzIyBqoSvaddhkdJ?t~(ieBc(eaAaNW|;oYRwNsj zqJO{TWQ&uUa$BkvX2Li=dMVz~!4`7c^2;4uj`g7nxne_eQ0Mnh!USQXjTe&8 z8SVX~SRB47+Z{7EBe__-`r9o&maMJ33#x@ZDgJ%Qvrya$L*r->k{{=zPbi|^JH$D+ z5;1h9(OiUMJCqb}LS-1}&-##XE9x!^@?z2!)Di?aA}yhTm$#SH529M(={n63vpvRZ zG6tLN5pQqt2=2nB_egtXm!tD1rl~na1uNk`rfLT?)~+kmA6lBwx|iYsasA&PzbVRi zXa!2NE=EhVc>vXZ6*tKLo_e}B&-p<*$R-||-Epx-y%EO(JkuK(uFTW^IK8DaHk1RtJQT)fta|CJ@|L{8Ql9>*&D zt$JGAuqW+Rc;ojsuDu#>NXyw~jFX4QDsw$7_3-09taerAj5Kfet$Gn<8AdEyVYh+i z6D;)>S~D%0^>X{G_R}-s%ap9}ay!_Ih2<5OAZ393Zq(l!X#KZJ#BaK~5lb_D@icxy z$F0ak39F``wNNEG8xiu~{hYmOM_6@Vj8%QSh;bhE%rM47J!`dZr4Yz_cdq92`_tli zEGa*zU%EAiv>){LXl2mB0Hb+0Apu=yOrX3IlDBfbEK)y|o-Y!;0;tAPo>Uq{bM@2J zYkfN@5mqUS$MYS~;Zn+P{nj)p1GNSyH*(u}E;qbOF*r|dta(7*YgZUpIPK^$5YZB# zH;XUWRvlyOQi?Fnpp};|^PV2|NS02%SiPq7zU%Q?WEyB$oj9c};f5XvAjWkdb4_F1c$B1ui*=*K@j2+nsj#-MI%BY!uBiaD;7Ig!MPucttgSEm|y;Qic^NWmBK+~`P9IJCI4@q8+x8YHA9P^j=az+Gc>vVs{0XO=^EejL9;Xi>C%f}{d}KO?fEQtNTB$qD|9hW z1c7p=CI4X*r>w|;+%FO~*g@`9&*Thks@xOEay(NU?><~B?`vZJm16W!Am6M0<<%gn zV|Qlafx0fFDLp^FDi*{98=}SwbMz|=bH6>o>QKR`rt-ExlDk<zKM)+5ChkbX&pE5F4s5Wo6&==dz#*OdB zhA0LTyNDk)e_5)cJzi0&Ln~ld`=$nK91Z`n#%gG6#Iqd&@D-QL^_s8%WfBSSZQoGds zENGIWWA!0Wd-BuVs7}BM3^7T2hV zu;~b^@tm~R42*m7cpf$Ke#D_n!q6L@JzQ%e>(lwyWoK;w8M?*W(Hhl z$FSkRZhi=*a$qGSxX%bRp^7c0Hs~Vr-gR_WYbEhrtY}2gzJ2y%6-Yjc6cpw0ec1lh zEw2a0plBb-lHwqXRMX z!&OKBx)tn4y!x3spu2-ThVZSlg0;;x6y*t)KIP>PH`>gxq%g)&uvMVR%C6F~B5D+?HSLTLkqg7GJch&aL&Ug# zcfLco4t37|#d~8KV`;%3YUHx|HWSJ5!@;Iu7%xHB#3$BAq%0CH@~8sEo!dlfiYkqO z#br&RPhYQ9M-IuXnqpyMoR0_Y`-q)3E?AmeHiFV$nM#7+BU^@7W9NG9n7u_5OZ;XK z$ssF;@3Y*B?{4m?eMbzU!|}Uj!mXo`!Y5Oq=E*aYxHQ#2ea)>KVzU0kV;Pw&r!?A6 zpl$Ee?nm?V&}A*?eF)zLF=hCCl4+5T_Mw?Q>X#>u2;zmVj*D<9Uu2-ZVGF;DiBrl| zEAM-F;n!LtZ`qNa&+uyK30gcCZ53t@mmzF7;zA)SLRcMph!a~A#Co@5*S8;kw4(Mx z;ki?0q~Xt=CS>mBR$Rge>ZoNS-zVN|nY>1@t^F)8Y@eoxdCchM_vu&xqFzbgSu7nL zp)y|1H7U33U{#_+3g#mF;pGMT@o7n89Q_3CYDxW!KAucVd*82 zS+bjfqstRKGCORDVbN5E*jC1vv}12Fzc)a$ae5$LLP(m|MOk0}I`sEG+iHFX?4(fL zKwlMRA4}mvDJ}+KnyX37dACFm+|Y!ZUKna0t9A&1v#~L#Gz>t7Cas)}UicZJM3$JX zQ8EspQQ~&Ap%1GUJty-R!IP$@^iZv0=_5#nMA2@Cc|7ORj5lQba_^550bAvmURVBL z+{Hob0+o4H#gj<^T{OBsL7Z} zI)AX?3NOn;u)<`~?Iu$zh}y-Tz%H8)a{mp>B8{Ti(hw<)-D$8FmkTJZFpM`LLqC7l zJgfH3=CE;Ca%^$>Qa2DyH$jGrK=amgM?Qe*n_QayDC#Pi)dQSR7IJ6;!-2`q+s2eC zt3cm4lM4`^$7~`3$i$%_HcAeo8N-}Lur+Rr<@r&`2?k{m46fIfp3IXuEiu(bY#kpyM{r(> zJNLBHj|4->O)j8^Z)X2aR{;un=-^ zn(v$?f<1rn64RkWpRPBYhHhWy_A%QS8E;4+epnhK=a&cU&66UFXHRAmYakraf+Htt{ z#FFFW*rQvNxKezdcE6(MZms)5F7n|s;fZ6Pf$tvBq_H(qWLYia;3P0Q>D%K8j_7%= z>)^Q&sa#w<#vJH(cgU$e0k73vhmod*!(@ZZ2NI`@o@I5Q zy=3BOTp9`DlKHCSZwF@#^NbK^4$R4WwXNVpck~06X=QiGN@_5ks3vIdBI*$P4s@<-l^tjN*q(VvU*}G?jf-;so zwdq3E>NqN53+awqUVXNol6vBymk4O?^h>6Z?q%T140PSXyw8^u0$|csoNqnrOH}dG z!aGZ?E}`e1QL!$j?U|kddcStnTH>q|;!8w-7rj?ATEXg}G0hg7V)W&ut27Wa%!%sB`=132xrB zi_1}j3lTF2Y_#GLBDGF`iTKTa4dXcn&-E34dslT7GQ2;bJg0}$y780uI$yoaiyuVd zv<)9i8!fp#rekgXfe8c1q8Rkcnl!VpFia;YD~~tlAa!trtW_`?>ItLv;RcW=^h}x~ zD50Kxv8Db7^~jef*bPp9(x$=c0(<%0Y9UG-Z}KeKR!r^id9PHpRw7 zFKjq@VYvX^#GEDYq6qW#SHN$>9jkQZcCTF3RL)`@#;7-|c(@dFglrqLd? ziXoY($#7U$CQ{eqKspAQ7+z)t1-ZvvM<9U>lbka?iMbs+9TPVmoLT{cS6X-H0h4$( zqSTc1C?4uVSVsFVA-kmPdLIr30kE7&1mW}`+^`_7sggD-eI3FhMP4nT+u<&4^%?9* zyEnP|JS=557@NFy3};f+l6Dw_l_jPpvW1IRG!nud$tdQoVp*CP01J+@Ul!L#wVd@7 z4rIj4*(pnny9gnDrR=84vjgUczxJjq_M5?Mmg9J%Johy{ib|MHthad#7e9GWWd>8$ zDq=xMzAx?a>iahz*~Y5*eo?*v2L7<=avm9WTF$}ADCsC(<+v8GMtOQTh%%qJ|Ju=I z8x8r&O}F-Ll}sKh4&>&7rT^ZANh4JB?rGL_`{v6tf{3`Fv?|1`zwe!gaM<(Rt(cv~ zldw?#c(Bp-D-64ag~Bp89@ZI0UsFvP4do7e8^uBsB=?rw1?zIf??4ey0jr!9Y4#?c z49HY;qR#wm@}u|fiO)p$>>o$Vglj9o+&JZIQPvMc*zl~eBxcIq2lxC=2KuoQ9p@x^ zDNM$vQdb8YX0v=us~(dcbL`y|wnrlF)|2$`V&+cm*TkaTdu%7^`wg~}<{ck;0u9!K z1=PWok^Jgx{6-c8JGtv8j30kQ@H9oV$eXltVYMe{^OINUkqZ)*JMeqw9Kwl>}5Xpjh!!%l6jOg-FHf{8uSPYaL5a zymzOaE<#?BU?%M74Wbm|!I%xq(&xtThI~z)F4!ZS1?fW6z05 zj@LNQ8GeqgU)y+3o_@$IBCvPUUp^7-Rx`|(#Eg!HI-6x3&`+V9j5|9-T3CbeU*4A3 zFV4;%RDq|l-vbI2xW!I?Y z?$yuR@3NbvZarSkuk3N{;~4@|N%IRIfdPh^BCFhXS$K_kswwV1EtIv*>~mcEUmL6z zFc+iqX*s0^^&)g~80waLvwWE&t$*3Se%=gi4AEHewCGj#+l|EE$SN@X?u^Q{(0glb zrBLAGZ2Qbgp0XaANPs_15Y~tM9FeRzC)3&Etsz?)j$wc$)(ajPMi`tfk+kb|{_l8;NR#WIEHzBC^oL2mmUwMEiVi!ko?#^X#-=VwSl~ zTNC}cSt2VSM=B}W2kJmlsT;f2A8vT=0s% zxrC@3WxK}znqwrfS2m6D`Tt|+ItP^lo&!-FbgIUy* zyUbJ?7MHM@vpWfX9*Ukc?7Y`-D?H59j7@@KZ`G#rYKWF8 zrE_?dzJSYk*-y<+0|m@h`I1qQDN()UFDrDH|LDX}7-k=0jr;XdibkrJ^Xv|Y z6 zBbluy%peGa!cT*;m{wDmvQji~+-yaD0e7ErSmOkR@4vwaa>gAF_2hOCEK-%{v5PKR zL3C7^IPAD@3Md(28KFv&l~%kgmX)lsyekY-iQcRHmTvkbAsr5T%OWTO*!!^cOc#Oy zl+Ky-#{EJ|hv3t^oMn?8o>ABK85J#Uof2QLi=q%SOuOA+rxX1`R$H%4`Cj4^&OrdoYzbs>x1d?P)+fB@aCDrvQw{Bp`)Pd&yO!ka_X{m(q=*b10`YDIRF3v literal 0 HcmV?d00001 diff --git a/miniprogram/assets/icons/zhedie.svg b/miniprogram/assets/icons/zhedie.svg new file mode 100644 index 0000000..161ef97 --- /dev/null +++ b/miniprogram/assets/icons/zhedie.svg @@ -0,0 +1,4 @@ + + + + diff --git a/miniprogram/assets/icons/星程字体转换.svg b/miniprogram/assets/icons/星程字体转换.svg new file mode 100644 index 0000000..49b7f9a --- /dev/null +++ b/miniprogram/assets/icons/星程字体转换.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/miniprogram/config/cdn.js b/miniprogram/config/cdn.js new file mode 100644 index 0000000..6c00f7f --- /dev/null +++ b/miniprogram/config/cdn.js @@ -0,0 +1,40 @@ +// CDN 配置文件 +// 管理所有静态资源的 CDN 地址 + +const CDN_BASE_URL = 'https://fonts.biboer.cn'; + +// 图标路径配置 +const ICON_PATHS = { + // Logo + logo: `${CDN_BASE_URL}/assets/webicon.png`, + + // 字体大小控制图标 + fontSizeDecrease: `${CDN_BASE_URL}/assets/icons/font-size-decrease.svg`, + fontSizeIncrease: `${CDN_BASE_URL}/assets/icons/font-size-increase.svg`, + + // 颜色选择器图标 + chooseColor: `${CDN_BASE_URL}/assets/icons/choose-color.svg`, + + // 导出按钮图标 + export: `${CDN_BASE_URL}/assets/icons/export.svg`, + exportSvg: `${CDN_BASE_URL}/assets/icons/export-svg.svg`, + exportPng: `${CDN_BASE_URL}/assets/icons/export-png.svg`, + + // 字体树图标 + fontIcon: `${CDN_BASE_URL}/assets/icons/font-icon.svg`, + expand: `${CDN_BASE_URL}/assets/icons/expand.svg`, + selectAll: `${CDN_BASE_URL}/assets/icons/selectall.svg`, + unselectAll: `${CDN_BASE_URL}/assets/icons/unselectall.svg`, + checkbox: `${CDN_BASE_URL}/assets/icons/checkbox.svg` +}; + +// 字体资源路径 +const FONT_BASE_URL = `${CDN_BASE_URL}/fonts`; +const FONTS_JSON_URL = `${CDN_BASE_URL}/fonts.json`; + +module.exports = { + CDN_BASE_URL, + ICON_PATHS, + FONT_BASE_URL, + FONTS_JSON_URL +}; diff --git a/miniprogram/i18n/base.json b/miniprogram/i18n/base.json new file mode 100644 index 0000000..19444b4 --- /dev/null +++ b/miniprogram/i18n/base.json @@ -0,0 +1,11 @@ +{ + "ios": { + "name": "星程社" + }, + "android": { + "name": "星程社" + }, + "common": { + "name": "星程社" + } +} diff --git a/miniprogram/pages/font-picker/index.js b/miniprogram/pages/font-picker/index.js new file mode 100644 index 0000000..af99ce4 --- /dev/null +++ b/miniprogram/pages/font-picker/index.js @@ -0,0 +1,144 @@ +const { loadFontsManifest, listCategories } = require('../../utils/mp/font-loader') +const { loadFavorites, saveFavorites } = require('../../utils/mp/storage') + +Page({ + data: { + fonts: [], + filteredFonts: [], + categories: ['全部'], + categoryIndex: 0, + favoriteOnly: false, + favorites: [], + searchText: '', + selectedFontId: '', + }, + + async onLoad(options) { + const selectedFontId = options && options.selected ? decodeURIComponent(options.selected) : '' + this.setData({ selectedFontId }) + + wx.showLoading({ title: '加载字体中', mask: true }) + try { + const favorites = loadFavorites() + const fonts = await loadFontsManifest() + const categories = listCategories(fonts) + + this.fontMap = new Map(fonts.map((font) => [font.id, font])) + + this.setData({ + fonts, + categories, + favorites, + }) + + this.applyFilter() + } catch (error) { + wx.showToast({ title: '字体加载失败', icon: 'none' }) + } finally { + wx.hideLoading() + } + }, + + applyFilter() { + const { + fonts, + favorites, + searchText, + categories, + categoryIndex, + favoriteOnly, + } = this.data + + const keyword = String(searchText || '').trim().toLowerCase() + const selectedCategory = categories[categoryIndex] || '全部' + const favoriteSet = new Set(favorites) + + const filteredFonts = fonts + .filter((font) => { + if (favoriteOnly && !favoriteSet.has(font.id)) { + return false + } + if (selectedCategory !== '全部' && selectedCategory !== '收藏' && font.category !== selectedCategory) { + return false + } + if (selectedCategory === '收藏' && !favoriteSet.has(font.id)) { + return false + } + if (!keyword) { + return true + } + return ( + String(font.name || '').toLowerCase().includes(keyword) || + String(font.category || '').toLowerCase().includes(keyword) + ) + }) + .map((font) => ({ + ...font, + isFavorite: favoriteSet.has(font.id), + })) + + this.setData({ filteredFonts }) + }, + + onSearchInput(event) { + this.setData({ searchText: event.detail.value || '' }) + this.applyFilter() + }, + + onCategoryChange(event) { + this.setData({ categoryIndex: Number(event.detail.value) || 0 }) + this.applyFilter() + }, + + onToggleFavoriteOnly() { + this.setData({ favoriteOnly: !this.data.favoriteOnly }) + this.applyFilter() + }, + + onToggleFavorite(event) { + const fontId = event.currentTarget.dataset.fontId + if (!fontId) { + return + } + + const next = new Set(this.data.favorites) + if (next.has(fontId)) { + next.delete(fontId) + } else { + next.add(fontId) + } + + const favorites = saveFavorites(Array.from(next)) + this.setData({ favorites }) + this.applyFilter() + }, + + onSelectFont(event) { + const fontId = event.currentTarget.dataset.fontId + if (!fontId) { + return + } + this.setData({ selectedFontId: fontId }) + }, + + onCancel() { + wx.navigateBack() + }, + + onConfirm() { + const { selectedFontId } = this.data + if (!selectedFontId) { + wx.showToast({ title: '请选择字体', icon: 'none' }) + return + } + + const font = this.fontMap ? this.fontMap.get(selectedFontId) : null + const eventChannel = this.getOpenerEventChannel() + eventChannel.emit('fontSelected', { + fontId: selectedFontId, + font, + }) + + wx.navigateBack() + }, +}) diff --git a/miniprogram/pages/font-picker/index.json b/miniprogram/pages/font-picker/index.json new file mode 100644 index 0000000..6a4e474 --- /dev/null +++ b/miniprogram/pages/font-picker/index.json @@ -0,0 +1,3 @@ +{ + "navigationBarTitleText": "选择字体" +} diff --git a/miniprogram/pages/font-picker/index.wxml b/miniprogram/pages/font-picker/index.wxml new file mode 100644 index 0000000..9379fea --- /dev/null +++ b/miniprogram/pages/font-picker/index.wxml @@ -0,0 +1,53 @@ + + + + + + + 分类:{{categories[categoryIndex]}} + + + + + + + 共 {{filteredFonts.length}} 个字体 + + + + {{item.name}} + {{item.category}} + + + + {{item.isFavorite ? '★' : '☆'}} + + 已选 + + + 没有匹配字体 + + + + + + + + diff --git a/miniprogram/pages/font-picker/index.wxss b/miniprogram/pages/font-picker/index.wxss new file mode 100644 index 0000000..46c2159 --- /dev/null +++ b/miniprogram/pages/font-picker/index.wxss @@ -0,0 +1,102 @@ +.search-input { + background: #f6f8fc; + border-radius: 12rpx; + padding: 18rpx 20rpx; + font-size: 28rpx; +} + +.toolbar { + margin-top: 16rpx; +} + +.picker-btn { + background: #f6f8fc; + border-radius: 12rpx; + padding: 14rpx 16rpx; + font-size: 26rpx; +} + +.mini-btn { + margin: 0; + height: 56rpx; + line-height: 56rpx; + background: #edf2ff; + color: #274c95; +} + +.list-card { + padding: 0; +} + +.summary { + padding: 18rpx 22rpx; + color: #6b7280; + font-size: 24rpx; + border-bottom: 1rpx solid #f0f2f7; +} + +.font-list { + height: calc(100vh - 410rpx); +} + +.font-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 18rpx 22rpx; + border-bottom: 1rpx solid #f2f3f8; +} + +.font-item.selected { + background: #eef5ff; +} + +.font-name { + font-size: 30rpx; + font-weight: 500; +} + +.font-meta { + margin-top: 6rpx; + font-size: 22rpx; + color: #6b7280; +} + +.actions { + align-items: center; +} + +.star { + font-size: 36rpx; + color: #f59e0b; + width: 56rpx; + text-align: center; +} + +.selected-tag { + background: #1677ff; + color: #fff; + border-radius: 999rpx; + font-size: 20rpx; + padding: 6rpx 14rpx; +} + +.empty { + color: #9ca3af; + text-align: center; + padding: 60rpx 0; +} + +.footer { + position: fixed; + left: 24rpx; + right: 24rpx; + bottom: 24rpx; + gap: 16rpx; +} + +.footer button { + flex: 1; + height: 84rpx; + line-height: 84rpx; +} diff --git a/miniprogram/pages/index/index.js b/miniprogram/pages/index/index.js new file mode 100644 index 0000000..872fcef --- /dev/null +++ b/miniprogram/pages/index/index.js @@ -0,0 +1,605 @@ +const { loadFontsManifest } = require('../../utils/mp/font-loader') +const { loadAppState, saveAppState, loadFavorites, saveFavorites } = require('../../utils/mp/storage') +const { saveSvgToUserPath, shareLocalFile, buildFilename } = require('../../utils/mp/file-export') +const { exportSvgToPngByCanvas, savePngToAlbum } = require('../../utils/mp/canvas-export') +const { renderSvgByApi } = require('../../utils/mp/render-api') +// const { ICON_PATHS } = require('../../config/cdn') // CDN 方案暂时注释 + +const COLOR_PALETTE = ['#000000', '#1d4ed8', '#047857', '#b45309', '#dc2626', '#7c3aed'] + +// 临时使用本地图标 +const LOCAL_ICON_PATHS = { + logo: '/assets/icons/webicon.png', + fontSizeDecrease: '/assets/icons/font-size-decrease.png', + fontSizeIncrease: '/assets/icons/font-size-increase.png', + chooseColor: '/assets/icons/choose-color.png', + export: '/assets/icons/export.png', + exportSvg: '/assets/icons/export-svg.png', + exportPng: '/assets/icons/export-png.png', + // 字体树图标(参考web项目) + fontIcon: 'https://fonts.biboer.cn/assets/icons/icons_idx%20_18.svg', // 字体item图标 + expandIcon: 'https://fonts.biboer.cn/assets/icons/icons_idx%20_12.svg', // 展开分类图标 + collapseIcon: 'https://fonts.biboer.cn/assets/icons/zhedie.svg', // 折叠分类图标 + favoriteIcon: 'https://fonts.biboer.cn/assets/icons/icons_idx%20_19.svg', // 收藏图标 + checkbox: '/assets/icons/checkbox.png', // 预览checkbox + search: 'https://fonts.biboer.cn/assets/icons/search.svg' // 搜索图标 +} + +function toSvgDataUri(svg) { + return `data:image/svg+xml;utf8,${encodeURIComponent(svg)}` +} + +function normalizeHexColor(input) { + const fallback = '#000000' + const value = String(input || '').trim() + if (/^#[0-9a-fA-F]{6}$/.test(value)) { + return value + } + return fallback +} + +Page({ + data: { + inputText: '星程字体转换', + fontSize: 120, + letterSpacingInput: '0', + textColor: '#000000', + colorPalette: COLOR_PALETTE, + selectedFonts: [], // 当前已选中的字体列表 + fontCategories: [], // 字体分类树 + favoriteCategories: [], // 已收藏字体分类树 + showColorPicker: false, + favorites: [], + // 使用本地图标路径 + icons: LOCAL_ICON_PATHS, + // 搜索功能 + searchKeyword: '', + showSearch: false, + }, + + async onLoad() { + // 调试:打印图标配置 + console.log('========== 图标配置 ==========') + console.log('icons:', this.data.icons) + console.log('logo URL:', this.data.icons.logo) + console.log('============================') + + this.fontMap = new Map() + this.generateTimer = null + + await this.bootstrap() + }, + + onShow() { + const favorites = loadFavorites() + this.setData({ favorites }) + this.updateFontTrees() + }, + + onUnload() { + if (this.generateTimer) { + clearTimeout(this.generateTimer) + this.generateTimer = null + } + }, + + async bootstrap() { + wx.showLoading({ title: '加载中', mask: true }) + try { + const state = loadAppState() + const fonts = await loadFontsManifest() + const favorites = loadFavorites() + + for (const font of fonts) { + this.fontMap.set(font.id, font) + } + + this.setData({ + inputText: state.inputText || this.data.inputText, + fontSize: Number(state.fontSize) > 0 ? Number(state.fontSize) : this.data.fontSize, + letterSpacingInput: + typeof state.letterSpacing === 'number' ? String(state.letterSpacing) : this.data.letterSpacingInput, + textColor: normalizeHexColor(state.textColor || this.data.textColor), + favorites, + }) + + // 构建字体树 + this.updateFontTrees() + + // 如果有保存的选中字体,恢复它们 + if (state.selectedFontIds && state.selectedFontIds.length > 0) { + const selectedFonts = state.selectedFontIds + .map(id => this.fontMap.get(id)) + .filter(font => font) + .map(font => ({ + id: font.id, + name: font.name, + category: font.category, + showInPreview: true, + previewSrc: '', + })) + + this.setData({ selectedFonts }) + await this.generateAllPreviews() + } + } catch (error) { + const message = error && error.message ? error.message : '初始化失败' + wx.showToast({ title: message, icon: 'none', duration: 2200 }) + } finally { + wx.hideLoading() + } + }, + + // 构建字体分类树 + updateFontTrees() { + const categoryMap = new Map() + const favoriteCategoryMap = new Map() + const favorites = this.data.favorites + const keyword = (this.data.searchKeyword || '').trim().toLowerCase() + + this.fontMap.forEach(font => { + const category = font.category || '其他' + const isFavorite = favorites.includes(font.id) + + // 应用搜索过滤 + if (keyword) { + const matchesSearch = font.name.toLowerCase().includes(keyword) || + category.toLowerCase().includes(keyword) + if (!matchesSearch) return + } + + // 所有字体树 + if (!categoryMap.has(category)) { + categoryMap.set(category, { + category, + expanded: true, + fonts: [], + }) + } + const selectedIds = this.data.selectedFonts.map(f => f.id) + categoryMap.get(category).fonts.push({ + ...font, + selected: selectedIds.includes(font.id), + isFavorite, + }) + + // 已收藏字体树 + if (isFavorite) { + if (!favoriteCategoryMap.has(category)) { + favoriteCategoryMap.set(category, { + category, + expanded: true, + fonts: [], + }) + } + favoriteCategoryMap.get(category).fonts.push({ + ...font, + selected: selectedIds.includes(font.id), + isFavorite: true, + }) + } + }) + + const fontCategories = Array.from(categoryMap.values()) + const favoriteCategories = Array.from(favoriteCategoryMap.values()) + + this.setData({ + fontCategories, + favoriteCategories, + }) + }, + + // 切换字体选择 + async onToggleFont(e) { + const fontId = e.currentTarget.dataset.fontId + if (!fontId) return + + const font = this.fontMap.get(fontId) + if (!font) return + + const selectedFonts = this.data.selectedFonts + const index = selectedFonts.findIndex(f => f.id === fontId) + + if (index >= 0) { + // 取消选择 + selectedFonts.splice(index, 1) + } else { + // 添加选择 + selectedFonts.push({ + id: font.id, + name: font.name, + category: font.category, + showInPreview: true, + previewSrc: '', + }) + } + + this.setData({ selectedFonts }) + this.updateFontTrees() + + // 保存状态 + saveAppState({ + inputText: this.data.inputText, + selectedFontIds: selectedFonts.map(f => f.id), + fontSize: Number(this.data.fontSize), + letterSpacing: Number(this.data.letterSpacingInput || 0), + textColor: this.data.textColor, + }) + + // 如果是新选中的字体,生成预览 + if (index < 0) { + await this.generatePreviewForFont(fontId) + } + }, + + // 切换字体在预览中的显示 + onTogglePreviewFont(e) { + const fontId = e.currentTarget.dataset.fontId + if (!fontId) return + + const selectedFonts = this.data.selectedFonts + const index = selectedFonts.findIndex(f => f.id === fontId) + + if (index >= 0) { + selectedFonts[index].showInPreview = !selectedFonts[index].showInPreview + this.setData({ selectedFonts }) + } + }, + + // 切换收藏 + onToggleFavorite(e) { + const fontId = e.currentTarget.dataset.fontId + if (!fontId) return + + let favorites = [...this.data.favorites] + const index = favorites.indexOf(fontId) + + if (index >= 0) { + favorites.splice(index, 1) + } else { + favorites.push(fontId) + } + + this.setData({ favorites }) + saveFavorites(favorites) + this.updateFontTrees() + }, + + // 切换分类展开/收起 + onToggleCategory(e) { + const category = e.currentTarget.dataset.category + if (!category) return + + const fontCategories = this.data.fontCategories + const index = fontCategories.findIndex(c => c.category === category) + + if (index >= 0) { + fontCategories[index].expanded = !fontCategories[index].expanded + this.setData({ fontCategories }) + } + }, + + // 切换收藏分类展开/收起 + onToggleFavoriteCategory(e) { + const category = e.currentTarget.dataset.category + if (!category) return + + const favoriteCategories = this.data.favoriteCategories + const index = favoriteCategories.findIndex(c => c.category === category) + + if (index >= 0) { + favoriteCategories[index].expanded = !favoriteCategories[index].expanded + this.setData({ favoriteCategories }) + } + }, + + // 生成单个字体的预览 + async generatePreviewForFont(fontId) { + const text = String(this.data.inputText || '') + if (!text.trim()) { + return + } + + try { + const letterSpacing = Number(this.data.letterSpacingInput || 0) + const fillColor = normalizeHexColor(this.data.textColor) + + const result = await renderSvgByApi({ + fontId, + text, + fontSize: Number(this.data.fontSize), + fillColor, + letterSpacing, + maxCharsPerLine: 45, + }) + + const previewImageSrc = toSvgDataUri(result.svg) + + // 更新对应字体的预览 + const selectedFonts = this.data.selectedFonts + const index = selectedFonts.findIndex(f => f.id === fontId) + + if (index >= 0) { + selectedFonts[index].previewSrc = previewImageSrc + selectedFonts[index].svg = result.svg + selectedFonts[index].width = result.width + selectedFonts[index].height = result.height + this.setData({ selectedFonts }) + } + } catch (error) { + console.error('生成预览失败', error) + } + }, + + // 生成所有选中字体的预览 + async generateAllPreviews() { + const text = String(this.data.inputText || '') + if (!text.trim()) { + return + } + + wx.showLoading({ title: '生成预览中', mask: true }) + + try { + for (const font of this.data.selectedFonts) { + await this.generatePreviewForFont(font.id) + } + } finally { + wx.hideLoading() + } + }, + + scheduleGenerate() { + if (this.generateTimer) { + clearTimeout(this.generateTimer) + } + + this.generateTimer = setTimeout(() => { + this.generateAllPreviews() + }, 260) + }, + + onInputText(event) { + this.setData({ inputText: event.detail.value || '' }) + saveAppState({ + inputText: event.detail.value || '', + selectedFontIds: this.data.selectedFonts.map(f => f.id), + fontSize: Number(this.data.fontSize), + letterSpacing: Number(this.data.letterSpacingInput || 0), + textColor: this.data.textColor, + }) + this.scheduleGenerate() + }, + + onFontSizeChanging(event) { + this.setData({ fontSize: Number(event.detail.value) || this.data.fontSize }) + }, + + onFontSizeChange(event) { + this.setData({ fontSize: Number(event.detail.value) || this.data.fontSize }) + saveAppState({ + inputText: this.data.inputText, + selectedFontIds: this.data.selectedFonts.map(f => f.id), + fontSize: Number(event.detail.value), + letterSpacing: Number(this.data.letterSpacingInput || 0), + textColor: this.data.textColor, + }) + this.scheduleGenerate() + }, + + onDecreaseFontSize() { + const newSize = Math.max(24, this.data.fontSize - 10) + this.setData({ fontSize: newSize }) + saveAppState({ + inputText: this.data.inputText, + selectedFontIds: this.data.selectedFonts.map(f => f.id), + fontSize: newSize, + letterSpacing: Number(this.data.letterSpacingInput || 0), + textColor: this.data.textColor, + }) + this.scheduleGenerate() + }, + + onIncreaseFontSize() { + const newSize = Math.min(320, this.data.fontSize + 10) + this.setData({ fontSize: newSize }) + saveAppState({ + inputText: this.data.inputText, + selectedFontIds: this.data.selectedFonts.map(f => f.id), + fontSize: newSize, + letterSpacing: Number(this.data.letterSpacingInput || 0), + textColor: this.data.textColor, + }) + this.scheduleGenerate() + }, + + onShowColorPicker() { + this.setData({ showColorPicker: true }) + }, + + onHideColorPicker() { + this.setData({ showColorPicker: false }) + }, + + onStopPropagation() { + // 阻止事件冒泡 + }, + + onColorInput(event) { + this.setData({ textColor: event.detail.value || '' }) + this.scheduleGenerate() + }, + + onPickColor(event) { + const color = event.currentTarget.dataset.color + if (!color) { + return + } + this.setData({ textColor: color }) + saveAppState({ + inputText: this.data.inputText, + selectedFontIds: this.data.selectedFonts.map(f => f.id), + fontSize: Number(this.data.fontSize), + letterSpacing: Number(this.data.letterSpacingInput || 0), + textColor: color, + }) + this.scheduleGenerate() + }, + + onRegenerate() { + this.generateAllPreviews() + }, + + onShowExportOptions() { + wx.showActionSheet({ + itemList: ['导出全部为 SVG', '导出全部为 PNG'], + success: (res) => { + if (res.tapIndex === 0) { + this.exportAllSvg() + } else if (res.tapIndex === 1) { + this.exportAllPng() + } + }, + }) + }, + + async exportAllSvg() { + const selectedFonts = this.data.selectedFonts.filter(f => f.showInPreview && f.svg) + + if (selectedFonts.length === 0) { + wx.showToast({ title: '请先生成预览', icon: 'none' }) + return + } + + wx.showLoading({ title: '导出 SVG 中', mask: true }) + + try { + for (const font of selectedFonts) { + const filePath = await saveSvgToUserPath(font.svg, font.name, this.data.inputText) + const filename = buildFilename(font.name, this.data.inputText, 'svg') + await shareLocalFile(filePath, filename) + } + wx.showToast({ title: 'SVG 导出完成', icon: 'success' }) + } catch (error) { + const message = error && error.errMsg ? error.errMsg : error.message + wx.showToast({ title: message || '导出 SVG 失败', icon: 'none', duration: 2400 }) + } finally { + wx.hideLoading() + } + }, + + async exportAllPng() { + const selectedFonts = this.data.selectedFonts.filter(f => f.showInPreview && f.svg) + + if (selectedFonts.length === 0) { + wx.showToast({ title: '请先生成预览', icon: 'none' }) + return + } + + wx.showLoading({ title: '导出 PNG 中', mask: true }) + + try { + for (const font of selectedFonts) { + const width = Math.max(64, Math.round(font.width || 1024)) + const height = Math.max(64, Math.round(font.height || 1024)) + + const pngPath = await exportSvgToPngByCanvas(this, { + svgString: font.svg, + width, + height, + }) + + await savePngToAlbum(pngPath) + } + wx.showToast({ title: 'PNG 已保存到相册', icon: 'success' }) + } catch (error) { + const message = error && error.errMsg ? error.errMsg : error.message + wx.showToast({ title: message || '导出 PNG 失败', icon: 'none', duration: 2400 }) + } finally { + wx.hideLoading() + } + }, + + async onExportSvg() { + const selectedFonts = this.data.selectedFonts.filter(f => f.showInPreview && f.svg) + + if (selectedFonts.length === 0) { + wx.showToast({ title: '请先生成预览', icon: 'none' }) + return + } + + // 如果只有一个字体,直接导出 + if (selectedFonts.length === 1) { + const font = selectedFonts[0] + wx.showLoading({ title: '导出 SVG 中', mask: true }) + try { + const filePath = await saveSvgToUserPath(font.svg, font.name, this.data.inputText) + const filename = buildFilename(font.name, this.data.inputText, 'svg') + await shareLocalFile(filePath, filename) + wx.showToast({ title: 'SVG 已分享', icon: 'success' }) + } catch (error) { + const message = error && error.errMsg ? error.errMsg : error.message + wx.showToast({ title: message || '导出 SVG 失败', icon: 'none', duration: 2400 }) + } finally { + wx.hideLoading() + } + } else { + this.exportAllSvg() + } + }, + + async onExportPng() { + const selectedFonts = this.data.selectedFonts.filter(f => f.showInPreview && f.svg) + + if (selectedFonts.length === 0) { + wx.showToast({ title: '请先生成预览', icon: 'none' }) + return + } + + // 如果只有一个字体,直接导出 + if (selectedFonts.length === 1) { + const font = selectedFonts[0] + wx.showLoading({ title: '导出 PNG 中', mask: true }) + + try { + const width = Math.max(64, Math.round(font.width || 1024)) + const height = Math.max(64, Math.round(font.height || 1024)) + + const pngPath = await exportSvgToPngByCanvas(this, { + svgString: font.svg, + width, + height, + }) + + const saveResult = await savePngToAlbum(pngPath) + if (saveResult.success) { + wx.showToast({ title: 'PNG 已保存到相册', icon: 'success' }) + } else { + wx.showToast({ title: '保存失败,请重试', icon: 'none' }) + } + } catch (error) { + const message = error && error.errMsg ? error.errMsg : error.message + wx.showToast({ title: message || '导出 PNG 失败', icon: 'none', duration: 2400 }) + } finally { + wx.hideLoading() + } + } else { + this.exportAllPng() + } + }, + + // 搜索功能 + onToggleSearch() { + this.setData({ showSearch: !this.data.showSearch }) + if (!this.data.showSearch) { + this.setData({ searchKeyword: '' }) + this.updateFontTrees() + } + }, + + onSearchInput(e) { + const keyword = e.detail.value || '' + this.setData({ searchKeyword: keyword }) + this.updateFontTrees() + }, +}) diff --git a/miniprogram/pages/index/index.json b/miniprogram/pages/index/index.json new file mode 100644 index 0000000..abc5247 --- /dev/null +++ b/miniprogram/pages/index/index.json @@ -0,0 +1,3 @@ +{ + "navigationBarTitleText": "Font2SVG" +} diff --git a/miniprogram/pages/index/index.wxml b/miniprogram/pages/index/index.wxml new file mode 100644 index 0000000..d777289 --- /dev/null +++ b/miniprogram/pages/index/index.wxml @@ -0,0 +1,217 @@ + + + + + + + TextToSVG + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 效果预览 + + + + + {{item.name}} + + + + + + + + + 生成中... + + + 请从下方选择字体 + + + + + + + + + 选择 + + + + + + + + + + + + + {{item.category}} + + + + + {{font.name}} + + + + + + + + + + + + + + + + + + + 已收藏 + + + + + {{item.category}} + + + + + {{font.name}} + + + + + + + + + + + + + + 暂无收藏字体 + + + + + + + + + + + + 自定义颜色: + + + + + + + diff --git a/miniprogram/pages/index/index.wxss b/miniprogram/pages/index/index.wxss new file mode 100644 index 0000000..ae079a2 --- /dev/null +++ b/miniprogram/pages/index/index.wxss @@ -0,0 +1,426 @@ +/* 顶部导航栏 */ +.header-row { + display: flex; + align-items: center; + gap: 16rpx; + height: 96rpx; + padding: 0 16rpx; + background: #fff; +} + +.logo-container { + width: 96rpx; + height: 96rpx; + border-radius: 24rpx; + overflow: hidden; + flex-shrink: 0; +} + +.logo { + width: 100%; + height: 100%; +} + +.app-title { + font-size: 32rpx; + font-weight: 600; + color: #8552A1; + flex-shrink: 0; +} + +.font-size-control { + display: flex; + align-items: center; + flex: 1; + gap: 4rpx; /* 改为 2rpx 的 gap,小程序 rpx = 物理像素*2 */ + padding: 0 12rpx; +} + +.font-size-icon { + width: 24rpx; + height: 24rpx; + flex-shrink: 0; +} + +.font-slider { + flex: 1; + height: 4rpx; +} + +.color-picker-btn { + width: 48rpx; + height: 48rpx; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} + +.color-icon { + width: 48rpx; + height: 48rpx; +} + +/* 输入栏和导出按钮 */ +.input-row { + display: flex; + align-items: center; + gap: 6rpx; + height: 78rpx; + padding: 0 16rpx; + margin-top: 16rpx; +} + +.text-input-container { + flex: 1; + height: 100%; + background: #F7F8FA; + border-radius: 12rpx; + padding: 0 6rpx; +} + +.text-input { + width: 100%; + height: 100%; + font-size: 20rpx; + color: #4E5969; +} + +.export-buttons { + display: flex; + align-items: center; + gap: 12rpx; + height: 100%; + background: #fff; + border: 1rpx solid #E5E6EB; + border-radius: 10rpx; + padding: 5rpx 10rpx; +} + +.export-btn { + width: 62rpx; + height: 62rpx; + display: flex; + align-items: center; + justify-content: center; +} + +.export-icon { + width: 100%; + height: 100%; +} + +/* 效果预览区域 */ +.preview-section { + flex: 1; + display: flex; + flex-direction: column; + margin-top: 16rpx; + padding: 0 16rpx; + border: 1rpx solid #f7e0e0; + border-radius: 12rpx; + background: #fff; + overflow: hidden; +} + +.section-title { + padding: 12rpx 0; + font-size: 28rpx; + font-weight: 400; + color: #000; +} + +.preview-list { + flex: 1; + min-height: 0; +} + +.preview-item { + display: flex; + flex-direction: column; + gap: 6rpx; + margin-bottom: 6rpx; +} + +.preview-header { + display: flex; + align-items: center; + gap: 4rpx; + padding-bottom: 4rpx; + border-bottom: 0.5rpx solid #C9CDD4; +} + +.font-icon { + width: 16rpx; + height: 16rpx; + flex-shrink: 0; +} + +.font-name-text { + flex: 1; + font-size: 20rpx; + color: #86909C; +} + +.preview-checkbox { + width: 14rpx; + height: 14rpx; +} + +.checkbox-wrapper { + width: 100%; + height: 100%; + border: 1rpx solid #C9CDD4; + border-radius: 3rpx; + display: flex; + align-items: center; + justify-content: center; + box-sizing: border-box; +} + +.checkbox-wrapper.checked { + border-color: #9B6BC2; + background: #9B6BC2; +} + +.checkbox-icon { + width: 100%; + height: 100%; +} + +.preview-content { + background: transparent; + padding: 4rpx 0; + min-height: 80rpx; + display: flex; + align-items: center; + justify-content: center; +} + +.preview-image { + width: 100%; +} + +.preview-loading { + font-size: 24rpx; + color: #86909C; +} + +.preview-empty { + text-align: center; + padding: 80rpx 0; + font-size: 24rpx; + color: #C9CDD4; +} + +/* 字体选择和已收藏字体 */ +.bottom-section { + display: flex; + gap: 16rpx; + height: 600rpx; + margin-top: 16rpx; + padding: 0 16rpx; +} + +.font-selection, +.favorite-selection { + flex: 1; + display: flex; + flex-direction: column; + border: 1rpx solid #f7e0e0; + border-radius: 16rpx; + background: #fff; + padding: 9rpx; + overflow: hidden; +} + +/* 搜索相关样式 */ +.selection-header { + display: flex; + align-items: center; + gap: 8rpx; + padding: 4rpx 0; +} + +.search-container { + flex: 1; + display: flex; + align-items: center; + gap: 6rpx; + background: #F7F8FA; + border-radius: 8rpx; + padding: 4rpx 12rpx; + height: 56rpx; +} + +.search-icon { + width: 28rpx; + height: 28rpx; + flex-shrink: 0; + opacity: 0.5; +} + +.search-input { + flex: 1; + font-size: 22rpx; + color: #4E5969; +} + +.search-toggle { + width: 56rpx; + height: 56rpx; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + background: #F7F8FA; + border-radius: 8rpx; +} + +.font-tree { + flex: 1; + min-height: 0; +} + +.font-category { + margin-bottom: 14rpx; +} + +.category-header { + display: flex; + align-items: center; + gap: 8rpx; + margin-bottom: 14rpx; +} + +.expand-icon { + width: 20rpx; + height: 20rpx; + transition: transform 0.3s; + flex-shrink: 0; +} + +.category-name { + font-size: 21rpx; + font-weight: 500; + color: #000; +} + +.font-list { + display: flex; + flex-direction: column; + gap: 0; +} + +.font-item { + display: flex; + align-items: center; + gap: 9rpx; + padding-bottom: 9rpx; + border-bottom: 1rpx solid #C9CDD4; + margin-bottom: 9rpx; +} + +.font-item-icon { + width: 18rpx; + height: 18rpx; + flex-shrink: 0; +} + +.font-item-name { + flex: 1; + font-size: 20rpx; + color: #86909C; +} + +.font-item-actions { + display: flex; + align-items: center; + gap: 9rpx; +} + +.font-checkbox { + width: 21rpx; + height: 21rpx; +} + +.checkbox-icon-sm { + width: 100%; + height: 100%; +} + +.favorite-btn { + width: 21rpx; + height: 21rpx; +} + +.favorite-icon { + width: 100%; + height: 100%; +} + +.empty-favorites { + text-align: center; + padding: 60rpx 0; + font-size: 24rpx; + color: #C9CDD4; +} + +/* 颜色选择器弹窗 */ +.color-picker-modal { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.5); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; +} + +.color-picker-content { + background: #fff; + border-radius: 20rpx; + padding: 40rpx; + width: 80%; +} + +.color-palette { + display: flex; + flex-wrap: wrap; + gap: 20rpx; + margin-bottom: 30rpx; +} + +.color-dot { + width: 60rpx; + height: 60rpx; + border-radius: 50%; + box-sizing: border-box; +} + +.color-input-row { + display: flex; + align-items: center; + gap: 16rpx; +} + +.color-input { + flex: 1; + height: 60rpx; + background: #F7F8FA; + border-radius: 10rpx; + padding: 0 16rpx; + font-size: 28rpx; +} + +/* 画布 */ +.hidden-canvas { + position: fixed; + width: 1px; + height: 1px; + left: -9999px; + top: -9999px; +} diff --git a/miniprogram/project.config.json b/miniprogram/project.config.json new file mode 100644 index 0000000..70fe1a8 --- /dev/null +++ b/miniprogram/project.config.json @@ -0,0 +1,45 @@ +{ + "description": "font2svg miniprogram", + "packOptions": { + "ignore": [], + "include": [] + }, + "setting": { + "es6": true, + "enhance": true, + "postcss": true, + "minified": true, + "coverView": true, + "showShadowRootInWxmlPanel": true, + "packNpmManually": false, + "compileHotReLoad": true, + "ignoreUploadUnusedFiles": true, + "compileWorklet": false, + "uglifyFileName": false, + "uploadWithSourceMap": true, + "packNpmRelationList": [], + "minifyWXSS": true, + "minifyWXML": true, + "localPlugins": false, + "disableUseStrict": false, + "useCompilerPlugins": false, + "condition": true, + "swc": false, + "disableSWC": true, + "babelSetting": { + "ignore": [], + "disablePlugins": [], + "outputPath": "" + } + }, + "compileType": "miniprogram", + "libVersion": "2.32.3", + "appid": "wxeda897f274ff33cf", + "projectname": "font2svg-miniprogram", + "condition": {}, + "simulatorPluginLibVersion": { + "wxext14566970e7e9f62": "2.27.3" + }, + "editorSetting": {}, + "projectArchitecture": "multiPlatform" +} \ No newline at end of file diff --git a/miniprogram/project.miniapp.json b/miniprogram/project.miniapp.json new file mode 100644 index 0000000..0aa2cbd --- /dev/null +++ b/miniprogram/project.miniapp.json @@ -0,0 +1,68 @@ +{ + "miniVersion": "v2", + "name": "%name%", + "version": "0.0.1", + "versionCode": 100, + "i18nFilePath": "i18n", + "mini-ohos": { + "sdkVersion": "0.5.1" + }, + "mini-android": { + "resourcePath": "miniapp/android/nativeResources", + "sdkVersion": "1.6.24", + "toolkitVersion": "0.11.0", + "useExtendedSdk": { + "media": false, + "bluetooth": false, + "network": false, + "scanner": false, + "xweb": false + }, + "icons": { + "hdpi": "", + "xhdpi": "", + "xxhdpi": "", + "xxxhdpi": "" + }, + "splashscreen": { + "hdpi": "", + "xhdpi": "", + "xxhdpi": "" + }, + "enableVConsole": "open", + "privacy": { + "enable": true + } + }, + "mini-ios": { + "sdkVersion": "1.6.28", + "toolkitVersion": "0.0.9", + "useExtendedSdk": { + "WeAppOpenFuns": true, + "WeAppNetwork": false, + "WeAppBluetooth": false, + "WeAppMedia": false, + "WeAppLBS": false, + "WeAppOthers": false + }, + "enableVConsole": "open", + "icons": { + "mainIcon120": "", + "mainIcon180": "", + "spotlightIcon80": "", + "spotlightIcon120": "", + "settingsIcon58": "", + "settingsIcon87": "", + "notificationIcon40": "", + "notificationIcon60": "", + "appStore1024": "" + }, + "splashScreen": { + "customImage": "" + }, + "privacy": { + "enable": false + }, + "enableOpenUrlNavigate": true + } +} \ No newline at end of file diff --git a/miniprogram/sitemap.json b/miniprogram/sitemap.json new file mode 100644 index 0000000..24591c9 --- /dev/null +++ b/miniprogram/sitemap.json @@ -0,0 +1,9 @@ +{ + "desc": "Font2SVG 小程序 sitemap", + "rules": [ + { + "action": "allow", + "page": "*" + } + ] +} diff --git a/miniprogram/utils/core/svg-builder.js b/miniprogram/utils/core/svg-builder.js new file mode 100644 index 0000000..d0439c8 --- /dev/null +++ b/miniprogram/utils/core/svg-builder.js @@ -0,0 +1,192 @@ +const { wrapTextByChars } = require('./text-layout') + +function formatNumber(value) { + const text = Number(value).toFixed(2).replace(/\.?0+$/, '') + return text || '0' +} + +function getGlyphPath(glyph) { + const path = glyph && glyph.path + if (!path || !Array.isArray(path.commands) || !path.commands.length) { + return '' + } + + const commands = [] + for (const cmd of path.commands) { + switch (cmd.type) { + case 'M': + commands.push(`M${formatNumber(cmd.x)} ${formatNumber(cmd.y)}`) + break + case 'L': + commands.push(`L${formatNumber(cmd.x)} ${formatNumber(cmd.y)}`) + break + case 'Q': + commands.push(`Q${formatNumber(cmd.x1)} ${formatNumber(cmd.y1)} ${formatNumber(cmd.x)} ${formatNumber(cmd.y)}`) + break + case 'C': + commands.push(`C${formatNumber(cmd.x1)} ${formatNumber(cmd.y1)} ${formatNumber(cmd.x2)} ${formatNumber(cmd.y2)} ${formatNumber(cmd.x)} ${formatNumber(cmd.y)}`) + break + case 'Z': + commands.push('Z') + break + default: + break + } + } + + return commands.join(' ') +} + +function getGlyphBounds(glyph) { + const path = glyph && glyph.path + if (!path || !Array.isArray(path.commands) || !path.commands.length) { + return null + } + + let xMin = Infinity + let yMin = Infinity + let xMax = -Infinity + let yMax = -Infinity + + for (const cmd of path.commands) { + if (typeof cmd.x === 'number') { + xMin = Math.min(xMin, cmd.x) + xMax = Math.max(xMax, cmd.x) + yMin = Math.min(yMin, cmd.y) + yMax = Math.max(yMax, cmd.y) + } + if (typeof cmd.x1 === 'number') { + xMin = Math.min(xMin, cmd.x1) + xMax = Math.max(xMax, cmd.x1) + yMin = Math.min(yMin, cmd.y1) + yMax = Math.max(yMax, cmd.y1) + } + if (typeof cmd.x2 === 'number') { + xMin = Math.min(xMin, cmd.x2) + xMax = Math.max(xMax, cmd.x2) + yMin = Math.min(yMin, cmd.y2) + yMax = Math.max(yMax, cmd.y2) + } + } + + if (xMin === Infinity) { + return null + } + + return { xMin, yMin, xMax, yMax } +} + +function generateSvgFromFont(options) { + const { + text, + font, + fontSize = 100, + fillColor = '#000000', + letterSpacing = 0, + maxCharsPerLine, + } = options + + if (!text || !String(text).trim()) { + throw new Error('文本内容不能为空') + } + + if (!font) { + throw new Error('字体对象不能为空') + } + + const normalizedText = wrapTextByChars(text, maxCharsPerLine) + + const scale = fontSize / font.unitsPerEm + const letterSpacingRaw = letterSpacing * font.unitsPerEm + + const glyphRuns = [] + let minX = null + let minY = null + let maxX = null + let maxY = null + let maxLineAdvance = 0 + + const ascender = Number.isFinite(font.ascender) ? font.ascender : font.unitsPerEm * 0.8 + const descender = Number.isFinite(font.descender) ? font.descender : -font.unitsPerEm * 0.2 + const lineAdvance = Math.max(font.unitsPerEm * 1.2, ascender - descender) + const lines = normalizedText.split('\n') + + for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) { + const line = lines[lineIndex] || '' + const yPos = -lineIndex * lineAdvance + let xPos = 0 + + for (const char of Array.from(line)) { + const glyph = font.charToGlyph(char) + glyphRuns.push({ glyph, xPos, yPos }) + + const bounds = getGlyphBounds(glyph) + if (bounds) { + const adjustedXMin = bounds.xMin + xPos + const adjustedYMin = bounds.yMin + yPos + const adjustedXMax = bounds.xMax + xPos + const adjustedYMax = bounds.yMax + yPos + + minX = minX === null ? adjustedXMin : Math.min(minX, adjustedXMin) + minY = minY === null ? adjustedYMin : Math.min(minY, adjustedYMin) + maxX = maxX === null ? adjustedXMax : Math.max(maxX, adjustedXMax) + maxY = maxY === null ? adjustedYMax : Math.max(maxY, adjustedYMax) + } + + xPos += (glyph.advanceWidth || 0) + letterSpacingRaw + } + + maxLineAdvance = Math.max(maxLineAdvance, xPos) + } + + if (minX === null || maxX === null) { + minX = 0 + maxX = maxLineAdvance + } + + if (minX === null || minY === null || maxX === null || maxY === null) { + throw new Error('未生成有效字形轮廓') + } + + const width = (maxX - minX) * scale + const height = (maxY - minY) * scale + + if (width <= 0 || height <= 0) { + throw new Error('计算得到的 SVG 尺寸无效') + } + + const paths = [] + for (const run of glyphRuns) { + const d = getGlyphPath(run.glyph) + if (!d) { + continue + } + const transform = `translate(${formatNumber(run.xPos)} ${formatNumber(run.yPos)})` + paths.push(` `) + } + + if (!paths.length) { + throw new Error('未生成任何路径') + } + + const viewBox = `${formatNumber(minX)} 0 ${formatNumber(maxX - minX)} ${formatNumber(maxY - minY)}` + const groupTransform = `translate(0 ${formatNumber(maxY)}) scale(1 -1)` + + const svg = `\n\n \n${paths.join('\n')}\n \n` + + const fontName = + (font.names && font.names.fontFamily && (font.names.fontFamily.en || font.names.fontFamily.zh)) || + (font.names && font.names.fullName && (font.names.fullName.en || font.names.fullName.zh)) || + 'Unknown' + + return { + svg, + width, + height, + fontName, + } +} + +module.exports = { + generateSvgFromFont, +} diff --git a/miniprogram/utils/core/text-layout.js b/miniprogram/utils/core/text-layout.js new file mode 100644 index 0000000..0c5214e --- /dev/null +++ b/miniprogram/utils/core/text-layout.js @@ -0,0 +1,36 @@ +const MAX_CHARS_PER_LINE = 45 + +function normalizeLineBreaks(text) { + return String(text || '').replace(/\r\n?/g, '\n') +} + +function wrapTextByChars(text, maxCharsPerLine = MAX_CHARS_PER_LINE) { + if (maxCharsPerLine <= 0) { + return normalizeLineBreaks(text) + } + + const normalized = normalizeLineBreaks(text) + const lines = normalized.split('\n') + const wrappedLines = [] + + for (const line of lines) { + const chars = Array.from(line) + + if (!chars.length) { + wrappedLines.push('') + continue + } + + for (let i = 0; i < chars.length; i += maxCharsPerLine) { + wrappedLines.push(chars.slice(i, i + maxCharsPerLine).join('')) + } + } + + return wrappedLines.join('\n') +} + +module.exports = { + MAX_CHARS_PER_LINE, + normalizeLineBreaks, + wrapTextByChars, +} diff --git a/miniprogram/utils/mp/canvas-export.js b/miniprogram/utils/mp/canvas-export.js new file mode 100644 index 0000000..ee86402 --- /dev/null +++ b/miniprogram/utils/mp/canvas-export.js @@ -0,0 +1,131 @@ +const { + canvasToTempFilePath, + saveImageToPhotosAlbum, + writeFile, + openSetting, + showModal, +} = require('./wx-promisify') + +function getWindowDpr() { + if (typeof wx.getWindowInfo === 'function') { + return wx.getWindowInfo().pixelRatio || 1 + } + const info = wx.getSystemInfoSync() + return info.pixelRatio || 1 +} + +function queryCanvasNode(page, selector) { + return new Promise((resolve, reject) => { + const query = wx.createSelectorQuery().in(page) + query + .select(selector) + .fields({ node: true, size: true }) + .exec((result) => { + const target = result && result[0] + if (!target || !target.node) { + reject(new Error('未找到导出画布节点')) + return + } + resolve(target) + }) + }) +} + +async function writeSvgTempFile(svgString) { + const path = `${wx.env.USER_DATA_PATH}/font2svg_preview_${Date.now()}.svg` + await writeFile(path, svgString, 'utf8') + return path +} + +async function exportSvgToPngByCanvas(page, options) { + const { + svgString, + width, + height, + selector = '#exportCanvas', + backgroundColor = '#ffffff', + } = options + + if (!svgString) { + throw new Error('缺少 SVG 内容') + } + + const canvasNode = await queryCanvasNode(page, selector) + const canvas = canvasNode.node + const ctx = canvas.getContext('2d') + const dpr = getWindowDpr() + + const renderWidth = Math.max(1, Math.min(2048, Math.round(width || canvasNode.width || 1024))) + const renderHeight = Math.max(1, Math.min(2048, Math.round(height || canvasNode.height || 1024))) + + canvas.width = renderWidth * dpr + canvas.height = renderHeight * dpr + + if (typeof ctx.setTransform === 'function') { + ctx.setTransform(dpr, 0, 0, dpr, 0, 0) + } else { + ctx.scale(dpr, dpr) + } + + ctx.fillStyle = backgroundColor + ctx.fillRect(0, 0, renderWidth, renderHeight) + + const svgPath = await writeSvgTempFile(svgString) + const image = canvas.createImage() + + await new Promise((resolve, reject) => { + image.onload = resolve + image.onerror = () => reject(new Error('加载 SVG 到画布失败')) + image.src = svgPath + }) + + ctx.drawImage(image, 0, 0, renderWidth, renderHeight) + + const fileRes = await canvasToTempFilePath( + { + canvas, + x: 0, + y: 0, + width: renderWidth, + height: renderHeight, + destWidth: renderWidth, + destHeight: renderHeight, + fileType: 'png', + }, + page + ) + + return fileRes.tempFilePath +} + +async function savePngToAlbum(filePath) { + try { + await saveImageToPhotosAlbum(filePath) + return { success: true } + } catch (error) { + const errMsg = String(error && error.errMsg ? error.errMsg : error) + const needAuth = errMsg.includes('auth deny') || errMsg.includes('authorize') + + if (needAuth) { + const modalRes = await showModal({ + title: '需要相册权限', + content: '请在设置中开启“保存到相册”权限后重试。', + confirmText: '去设置', + }) + if (modalRes.confirm) { + await openSetting() + } + } + + return { + success: false, + needAuth, + error, + } + } +} + +module.exports = { + exportSvgToPngByCanvas, + savePngToAlbum, +} diff --git a/miniprogram/utils/mp/file-export.js b/miniprogram/utils/mp/file-export.js new file mode 100644 index 0000000..46ba55f --- /dev/null +++ b/miniprogram/utils/mp/file-export.js @@ -0,0 +1,49 @@ +const { writeFile } = require('./wx-promisify') + +function sanitizeFilename(filename) { + return String(filename || 'font2svg') + .replace(/[<>:"/\\|?*\x00-\x1F]/g, '_') + .replace(/\s+/g, '_') + .slice(0, 80) +} + +function buildFilename(fontName, text, ext) { + const safeFont = sanitizeFilename(fontName || 'font') + const safeText = sanitizeFilename(Array.from(text || '').slice(0, 8).join('') || 'text') + return `${safeFont}_${safeText}.${ext}` +} + +async function writeTextToUserPath(text, ext, preferredName) { + const filename = preferredName || `font2svg_${Date.now()}.${ext}` + const filePath = `${wx.env.USER_DATA_PATH}/${filename}` + await writeFile(filePath, text, 'utf8') + return filePath +} + +async function saveSvgToUserPath(svgText, fontName, text) { + const filename = buildFilename(fontName, text, 'svg') + return writeTextToUserPath(svgText, 'svg', filename) +} + +async function shareLocalFile(filePath, fileName) { + if (typeof wx.shareFileMessage !== 'function') { + throw new Error('当前微信版本不支持文件分享') + } + + return new Promise((resolve, reject) => { + wx.shareFileMessage({ + filePath, + fileName, + success: resolve, + fail: reject, + }) + }) +} + +module.exports = { + sanitizeFilename, + buildFilename, + writeTextToUserPath, + saveSvgToUserPath, + shareLocalFile, +} diff --git a/miniprogram/utils/mp/font-loader.js b/miniprogram/utils/mp/font-loader.js new file mode 100644 index 0000000..4fcdffa --- /dev/null +++ b/miniprogram/utils/mp/font-loader.js @@ -0,0 +1,130 @@ +const { request, downloadFile, readFile } = require('./wx-promisify') + +const localFonts = require('../../assets/fonts') + +const fontBufferCache = new Map() +const MAX_FONT_CACHE = 4 + +function normalizePath(path, baseUrl) { + if (!path) { + return '' + } + if (/^https?:\/\//i.test(path)) { + return path + } + if (path.startsWith('//')) { + return `https:${path}` + } + if (path.startsWith('/')) { + return `${baseUrl}${path}` + } + return `${baseUrl}/${path}` +} + +function normalizeFontItem(item, baseUrl) { + const path = item.path || item.url || '' + const normalizedPath = normalizePath(path, baseUrl) + const filename = item.filename || normalizedPath.split('/').pop() || `${item.name || 'font'}.ttf` + return { + id: item.id || `${item.category || '默认'}/${item.name || filename}`, + name: item.name || filename.replace(/\.[^.]+$/, ''), + category: item.category || '默认', + filename, + path, + url: normalizedPath, + } +} + +function normalizeManifest(fonts, baseUrl) { + if (!Array.isArray(fonts)) { + return [] + } + return fonts + .map((item) => normalizeFontItem(item, baseUrl)) + .filter((item) => item.url) +} + +async function loadFontsManifest(options = {}) { + const app = getApp() + const manifestUrl = options.manifestUrl || app.globalData.fontsManifestUrl + const baseUrl = options.baseUrl || app.globalData.fontsBaseUrl + + if (Array.isArray(app.globalData.fonts) && app.globalData.fonts.length > 0) { + return app.globalData.fonts + } + + try { + const response = await request({ + url: manifestUrl, + method: 'GET', + timeout: 10000, + }) + + if (response.statusCode < 200 || response.statusCode >= 300) { + throw new Error(`获取字体清单失败,状态码: ${response.statusCode}`) + } + + const fonts = normalizeManifest(response.data, baseUrl) + if (!fonts.length) { + throw new Error('字体清单为空') + } + + app.globalData.fonts = fonts + return fonts + } catch (error) { + console.warn('远程字体清单加载失败,回退到本地清单:', error) + const fallbackFonts = normalizeManifest(localFonts, baseUrl) + app.globalData.fonts = fallbackFonts + return fallbackFonts + } +} + +function setLruCache(key, value) { + if (fontBufferCache.has(key)) { + fontBufferCache.delete(key) + } + fontBufferCache.set(key, value) + + while (fontBufferCache.size > MAX_FONT_CACHE) { + const firstKey = fontBufferCache.keys().next().value + fontBufferCache.delete(firstKey) + } +} + +async function loadFontBuffer(fontItem) { + const cacheKey = fontItem.id + if (fontBufferCache.has(cacheKey)) { + const cached = fontBufferCache.get(cacheKey) + setLruCache(cacheKey, cached) + return cached + } + + if (!fontItem.url) { + throw new Error('字体地址为空') + } + + const downloadRes = await downloadFile({ url: fontItem.url }) + if (downloadRes.statusCode < 200 || downloadRes.statusCode >= 300) { + throw new Error(`字体下载失败,状态码: ${downloadRes.statusCode}`) + } + + const readRes = await readFile(downloadRes.tempFilePath) + const result = { + tempFilePath: downloadRes.tempFilePath, + buffer: readRes.data, + } + + setLruCache(cacheKey, result) + return result +} + +function listCategories(fonts) { + const set = new Set(fonts.map((font) => font.category || '默认')) + return ['全部', '收藏', ...Array.from(set)] +} + +module.exports = { + loadFontsManifest, + loadFontBuffer, + listCategories, +} diff --git a/miniprogram/utils/mp/render-api.js b/miniprogram/utils/mp/render-api.js new file mode 100644 index 0000000..12ebb5a --- /dev/null +++ b/miniprogram/utils/mp/render-api.js @@ -0,0 +1,67 @@ +const { request } = require('./wx-promisify') + +function buildApiUrl() { + const app = getApp() + const apiUrl = app && app.globalData ? app.globalData.svgRenderApiUrl : '' + if (!apiUrl) { + throw new Error('未配置渲染 API 地址') + } + return apiUrl +} + +function normalizeResult(data) { + if (!data || typeof data !== 'object') { + throw new Error('渲染服务返回格式无效') + } + + if (typeof data.svg !== 'string' || !data.svg.trim()) { + throw new Error('渲染服务未返回有效 SVG') + } + + return { + svg: data.svg, + width: Number(data.width) || 0, + height: Number(data.height) || 0, + fontName: data.fontName || 'Unknown', + fontId: data.fontId || '', + } +} + +async function renderSvgByApi(payload) { + const app = getApp() + const timeout = app && app.globalData && app.globalData.apiTimeoutMs + ? Number(app.globalData.apiTimeoutMs) + : 30000 + + const response = await request({ + url: buildApiUrl(), + method: 'POST', + timeout, + header: { + 'content-type': 'application/json', + }, + data: { + fontId: payload.fontId, + text: payload.text, + fontSize: payload.fontSize, + fillColor: payload.fillColor, + letterSpacing: payload.letterSpacing, + maxCharsPerLine: payload.maxCharsPerLine, + }, + }) + + if (!response || response.statusCode < 200 || response.statusCode >= 300) { + throw new Error(`渲染服务请求失败,状态码: ${response && response.statusCode}`) + } + + const body = response.data || {} + if (!body.ok) { + throw new Error(body.error || '渲染服务返回错误') + } + + return normalizeResult(body.data) +} + +module.exports = { + renderSvgByApi, +} diff --git a/miniprogram/utils/mp/storage.js b/miniprogram/utils/mp/storage.js new file mode 100644 index 0000000..c330f29 --- /dev/null +++ b/miniprogram/utils/mp/storage.js @@ -0,0 +1,60 @@ +const STORAGE_KEYS = { + APP_STATE: 'font2svg:app-state', + FAVORITES: 'font2svg:favorites', +} + +function getStorage(key, fallbackValue) { + try { + const value = wx.getStorageSync(key) + if (value === '' || value === undefined || value === null) { + return fallbackValue + } + return value + } catch (error) { + console.warn('读取本地存储失败:', key, error) + return fallbackValue + } +} + +function setStorage(key, value) { + try { + wx.setStorageSync(key, value) + } catch (error) { + console.warn('写入本地存储失败:', key, error) + } +} + +function loadAppState() { + return getStorage(STORAGE_KEYS.APP_STATE, {}) +} + +function saveAppState(partialState) { + const current = loadAppState() + const next = { + ...current, + ...partialState, + updatedAt: Date.now(), + } + setStorage(STORAGE_KEYS.APP_STATE, next) + return next +} + +function loadFavorites() { + return getStorage(STORAGE_KEYS.FAVORITES, []) +} + +function saveFavorites(favorites) { + const unique = Array.from(new Set(favorites)) + setStorage(STORAGE_KEYS.FAVORITES, unique) + return unique +} + +module.exports = { + STORAGE_KEYS, + getStorage, + setStorage, + loadAppState, + saveAppState, + loadFavorites, + saveFavorites, +} diff --git a/miniprogram/utils/mp/worker-manager.js b/miniprogram/utils/mp/worker-manager.js new file mode 100644 index 0000000..e2e00d6 --- /dev/null +++ b/miniprogram/utils/mp/worker-manager.js @@ -0,0 +1,93 @@ +let singleton = null + +class SvgWorkerManager { + constructor() { + this.worker = wx.createWorker('workers/svg-generator/index.js') + this.pending = new Map() + this.timeoutMs = 30000 + + this.worker.onMessage((message) => { + const { requestId, success, data, error } = message || {} + const pendingTask = this.pending.get(requestId) + if (!pendingTask) { + return + } + + clearTimeout(pendingTask.timer) + this.pending.delete(requestId) + + if (success) { + pendingTask.resolve(data) + } else { + pendingTask.reject(new Error(error || 'Worker 执行失败')) + } + }) + + this.worker.onError((error) => { + this.rejectAll(error) + }) + + if (typeof this.worker.onProcessKilled === 'function') { + this.worker.onProcessKilled(() => { + this.rejectAll(new Error('Worker 进程被系统回收')) + }) + } + } + + rejectAll(error) { + for (const [requestId, pendingTask] of this.pending.entries()) { + clearTimeout(pendingTask.timer) + pendingTask.reject(error) + this.pending.delete(requestId) + } + } + + request(type, payload, timeoutMs) { + const requestId = `${Date.now()}-${Math.random().toString(16).slice(2)}` + return new Promise((resolve, reject) => { + const timer = setTimeout(() => { + this.pending.delete(requestId) + reject(new Error(`Worker 超时: ${type}`)) + }, timeoutMs || this.timeoutMs) + + this.pending.set(requestId, { resolve, reject, timer }) + + this.worker.postMessage({ + requestId, + type, + payload, + }) + }) + } + + loadFont(fontId, fontBuffer) { + return this.request('load-font', { + fontId, + fontBuffer, + }, 45000) + } + + generateSvg(params) { + return this.request('generate-svg', params, 45000) + } + + clearCache() { + return this.request('clear-cache', {}) + } + + terminate() { + this.rejectAll(new Error('Worker 已终止')) + this.worker.terminate() + } +} + +function getSvgWorkerManager() { + if (!singleton) { + singleton = new SvgWorkerManager() + } + return singleton +} + +module.exports = { + getSvgWorkerManager, +} diff --git a/miniprogram/utils/mp/wx-promisify.js b/miniprogram/utils/mp/wx-promisify.js new file mode 100644 index 0000000..6adc3f4 --- /dev/null +++ b/miniprogram/utils/mp/wx-promisify.js @@ -0,0 +1,110 @@ +function request(options) { + return new Promise((resolve, reject) => { + wx.request({ + ...options, + success: resolve, + fail: reject, + }) + }) +} + +function downloadFile(options) { + return new Promise((resolve, reject) => { + wx.downloadFile({ + ...options, + success: resolve, + fail: reject, + }) + }) +} + +function saveImageToPhotosAlbum(filePath) { + return new Promise((resolve, reject) => { + wx.saveImageToPhotosAlbum({ + filePath, + success: resolve, + fail: reject, + }) + }) +} + +function canvasToTempFilePath(options, component) { + return new Promise((resolve, reject) => { + wx.canvasToTempFilePath( + { + ...options, + success: resolve, + fail: reject, + }, + component + ) + }) +} + +function readFile(filePath, encoding) { + return new Promise((resolve, reject) => { + const fs = wx.getFileSystemManager() + fs.readFile({ + filePath, + encoding, + success: resolve, + fail: reject, + }) + }) +} + +function writeFile(filePath, data, encoding) { + return new Promise((resolve, reject) => { + const fs = wx.getFileSystemManager() + fs.writeFile({ + filePath, + data, + encoding, + success: resolve, + fail: reject, + }) + }) +} + +function saveFile(tempFilePath, filePath) { + return new Promise((resolve, reject) => { + const fs = wx.getFileSystemManager() + fs.saveFile({ + tempFilePath, + filePath, + success: resolve, + fail: reject, + }) + }) +} + +function openSetting() { + return new Promise((resolve, reject) => { + wx.openSetting({ + success: resolve, + fail: reject, + }) + }) +} + +function showModal(options) { + return new Promise((resolve, reject) => { + wx.showModal({ + ...options, + success: resolve, + fail: reject, + }) + }) +} + +module.exports = { + request, + downloadFile, + saveImageToPhotosAlbum, + canvasToTempFilePath, + readFile, + writeFile, + saveFile, + openSetting, + showModal, +} diff --git a/miniprogram/workers/svg-generator/index.js b/miniprogram/workers/svg-generator/index.js new file mode 100644 index 0000000..32ed48b --- /dev/null +++ b/miniprogram/workers/svg-generator/index.js @@ -0,0 +1,105 @@ +const opentype = require('./vendor/opentype.js') +const { generateSvgFromFont } = require('./svg-builder') + +const MAX_FONT_CACHE = 4 +const fontCache = new Map() + +function touchCache(key, value) { + if (fontCache.has(key)) { + fontCache.delete(key) + } + fontCache.set(key, value) + + while (fontCache.size > MAX_FONT_CACHE) { + const firstKey = fontCache.keys().next().value + fontCache.delete(firstKey) + } +} + +function sendResult(requestId, success, data, error) { + worker.postMessage({ + requestId, + success, + data, + error, + }) +} + +function handleLoadFont(requestId, payload) { + const { fontId, fontBuffer } = payload || {} + if (!fontId || !fontBuffer) { + throw new Error('加载字体参数无效') + } + + const font = opentype.parse(fontBuffer) + touchCache(fontId, { + font, + loadedAt: Date.now(), + }) + + return { fontId } +} + +function handleGenerateSvg(payload) { + const { + fontId, + text, + fontSize, + fillColor, + letterSpacing, + maxCharsPerLine, + } = payload || {} + + if (!fontId) { + throw new Error('缺少 fontId') + } + + const cached = fontCache.get(fontId) + if (!cached || !cached.font) { + throw new Error('字体未加载,请先加载字体') + } + + touchCache(fontId, cached) + + return generateSvgFromFont({ + text, + font: cached.font, + fontSize, + fillColor, + letterSpacing, + maxCharsPerLine, + }) +} + +worker.onMessage((message) => { + const { requestId, type, payload } = message || {} + + try { + if (!requestId) { + throw new Error('缺少 requestId') + } + + if (type === 'load-font') { + const data = handleLoadFont(requestId, payload) + sendResult(requestId, true, data) + return + } + + if (type === 'generate-svg') { + const data = handleGenerateSvg(payload) + sendResult(requestId, true, data) + return + } + + if (type === 'clear-cache') { + fontCache.clear() + sendResult(requestId, true, { ok: true }) + return + } + + throw new Error(`未知的任务类型: ${type}`) + } catch (error) { + const messageText = error && error.message ? error.message : String(error) + sendResult(requestId, false, null, messageText) + } +}) diff --git a/miniprogram/workers/svg-generator/svg-builder.js b/miniprogram/workers/svg-generator/svg-builder.js new file mode 100644 index 0000000..0c88b5c --- /dev/null +++ b/miniprogram/workers/svg-generator/svg-builder.js @@ -0,0 +1,188 @@ +const { wrapTextByChars } = require('./text-layout') + +function formatNumber(value) { + const text = Number(value).toFixed(2).replace(/\.?0+$/, '') + return text || '0' +} + +function getGlyphPath(glyph) { + const path = glyph && glyph.path + if (!path || !Array.isArray(path.commands) || !path.commands.length) { + return '' + } + + const commands = [] + for (const cmd of path.commands) { + switch (cmd.type) { + case 'M': + commands.push(`M${formatNumber(cmd.x)} ${formatNumber(cmd.y)}`) + break + case 'L': + commands.push(`L${formatNumber(cmd.x)} ${formatNumber(cmd.y)}`) + break + case 'Q': + commands.push(`Q${formatNumber(cmd.x1)} ${formatNumber(cmd.y1)} ${formatNumber(cmd.x)} ${formatNumber(cmd.y)}`) + break + case 'C': + commands.push(`C${formatNumber(cmd.x1)} ${formatNumber(cmd.y1)} ${formatNumber(cmd.x2)} ${formatNumber(cmd.y2)} ${formatNumber(cmd.x)} ${formatNumber(cmd.y)}`) + break + case 'Z': + commands.push('Z') + break + default: + break + } + } + + return commands.join(' ') +} + +function getGlyphBounds(glyph) { + const path = glyph && glyph.path + if (!path || !Array.isArray(path.commands) || !path.commands.length) { + return null + } + + let xMin = Infinity + let yMin = Infinity + let xMax = -Infinity + let yMax = -Infinity + + for (const cmd of path.commands) { + if (typeof cmd.x === 'number') { + xMin = Math.min(xMin, cmd.x) + xMax = Math.max(xMax, cmd.x) + yMin = Math.min(yMin, cmd.y) + yMax = Math.max(yMax, cmd.y) + } + if (typeof cmd.x1 === 'number') { + xMin = Math.min(xMin, cmd.x1) + xMax = Math.max(xMax, cmd.x1) + yMin = Math.min(yMin, cmd.y1) + yMax = Math.max(yMax, cmd.y1) + } + if (typeof cmd.x2 === 'number') { + xMin = Math.min(xMin, cmd.x2) + xMax = Math.max(xMax, cmd.x2) + yMin = Math.min(yMin, cmd.y2) + yMax = Math.max(yMax, cmd.y2) + } + } + + if (xMin === Infinity) { + return null + } + + return { xMin, yMin, xMax, yMax } +} + +function generateSvgFromFont(options) { + const { + text, + font, + fontSize = 100, + fillColor = '#000000', + letterSpacing = 0, + maxCharsPerLine, + } = options + + if (!text || !String(text).trim()) { + throw new Error('文本内容不能为空') + } + + const normalizedText = wrapTextByChars(text, maxCharsPerLine) + + const scale = fontSize / font.unitsPerEm + const letterSpacingRaw = letterSpacing * font.unitsPerEm + + const glyphRuns = [] + let minX = null + let minY = null + let maxX = null + let maxY = null + let maxLineAdvance = 0 + + const ascender = Number.isFinite(font.ascender) ? font.ascender : font.unitsPerEm * 0.8 + const descender = Number.isFinite(font.descender) ? font.descender : -font.unitsPerEm * 0.2 + const lineAdvance = Math.max(font.unitsPerEm * 1.2, ascender - descender) + const lines = normalizedText.split('\n') + + for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) { + const line = lines[lineIndex] || '' + const yPos = -lineIndex * lineAdvance + let xPos = 0 + + for (const char of Array.from(line)) { + const glyph = font.charToGlyph(char) + glyphRuns.push({ glyph, xPos, yPos }) + + const bounds = getGlyphBounds(glyph) + if (bounds) { + const adjustedXMin = bounds.xMin + xPos + const adjustedYMin = bounds.yMin + yPos + const adjustedXMax = bounds.xMax + xPos + const adjustedYMax = bounds.yMax + yPos + + minX = minX === null ? adjustedXMin : Math.min(minX, adjustedXMin) + minY = minY === null ? adjustedYMin : Math.min(minY, adjustedYMin) + maxX = maxX === null ? adjustedXMax : Math.max(maxX, adjustedXMax) + maxY = maxY === null ? adjustedYMax : Math.max(maxY, adjustedYMax) + } + + xPos += (glyph.advanceWidth || 0) + letterSpacingRaw + } + + maxLineAdvance = Math.max(maxLineAdvance, xPos) + } + + if (minX === null || maxX === null) { + minX = 0 + maxX = maxLineAdvance + } + + if (minX === null || minY === null || maxX === null || maxY === null) { + throw new Error('未生成有效字形轮廓') + } + + const width = (maxX - minX) * scale + const height = (maxY - minY) * scale + + if (width <= 0 || height <= 0) { + throw new Error('计算得到的 SVG 尺寸无效') + } + + const paths = [] + for (const run of glyphRuns) { + const d = getGlyphPath(run.glyph) + if (!d) { + continue + } + const transform = `translate(${formatNumber(run.xPos)} ${formatNumber(run.yPos)})` + paths.push(` `) + } + + if (!paths.length) { + throw new Error('未生成任何路径') + } + + const viewBox = `${formatNumber(minX)} 0 ${formatNumber(maxX - minX)} ${formatNumber(maxY - minY)}` + const groupTransform = `translate(0 ${formatNumber(maxY)}) scale(1 -1)` + + const svg = `\n\n \n${paths.join('\n')}\n \n` + + const fontName = + (font.names && font.names.fontFamily && (font.names.fontFamily.en || font.names.fontFamily.zh)) || + (font.names && font.names.fullName && (font.names.fullName.en || font.names.fullName.zh)) || + 'Unknown' + + return { + svg, + width, + height, + fontName, + } +} + +module.exports = { + generateSvgFromFont, +} diff --git a/miniprogram/workers/svg-generator/text-layout.js b/miniprogram/workers/svg-generator/text-layout.js new file mode 100644 index 0000000..19dbc06 --- /dev/null +++ b/miniprogram/workers/svg-generator/text-layout.js @@ -0,0 +1,34 @@ +const MAX_CHARS_PER_LINE = 45 + +function normalizeLineBreaks(text) { + return String(text || '').replace(/\r\n?/g, '\n') +} + +function wrapTextByChars(text, maxCharsPerLine = MAX_CHARS_PER_LINE) { + if (maxCharsPerLine <= 0) { + return normalizeLineBreaks(text) + } + + const normalized = normalizeLineBreaks(text) + const lines = normalized.split('\n') + const wrappedLines = [] + + for (const line of lines) { + const chars = Array.from(line) + if (!chars.length) { + wrappedLines.push('') + continue + } + + for (let i = 0; i < chars.length; i += maxCharsPerLine) { + wrappedLines.push(chars.slice(i, i + maxCharsPerLine).join('')) + } + } + + return wrappedLines.join('\n') +} + +module.exports = { + MAX_CHARS_PER_LINE, + wrapTextByChars, +} diff --git a/miniprogram/workers/svg-generator/vendor/opentype.js b/miniprogram/workers/svg-generator/vendor/opentype.js new file mode 100644 index 0000000..0499d3f --- /dev/null +++ b/miniprogram/workers/svg-generator/vendor/opentype.js @@ -0,0 +1,14477 @@ +/** + * https://opentype.js.org v1.3.4 | (c) Frederik De Bleser and other contributors | MIT License | Uses tiny-inflate by Devon Govett and string.prototype.codepointat polyfill by Mathias Bynens + */ + +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : + typeof define === 'function' && define.amd ? define(['exports'], factory) : + (global = global || self, factory(global.opentype = {})); +}(this, (function (exports) { 'use strict'; + + /*! https://mths.be/codepointat v0.2.0 by @mathias */ + if (!String.prototype.codePointAt) { + (function() { + var defineProperty = (function() { + // IE 8 only supports `Object.defineProperty` on DOM elements + try { + var object = {}; + var $defineProperty = Object.defineProperty; + var result = $defineProperty(object, object, object) && $defineProperty; + } catch(error) {} + return result; + }()); + var codePointAt = function(position) { + if (this == null) { + throw TypeError(); + } + var string = String(this); + var size = string.length; + // `ToInteger` + var index = position ? Number(position) : 0; + if (index != index) { // better `isNaN` + index = 0; + } + // Account for out-of-bounds indices: + if (index < 0 || index >= size) { + return undefined; + } + // Get the first code unit + var first = string.charCodeAt(index); + var second; + if ( // check if it’s the start of a surrogate pair + first >= 0xD800 && first <= 0xDBFF && // high surrogate + size > index + 1 // there is a next code unit + ) { + second = string.charCodeAt(index + 1); + if (second >= 0xDC00 && second <= 0xDFFF) { // low surrogate + // https://mathiasbynens.be/notes/javascript-encoding#surrogate-formulae + return (first - 0xD800) * 0x400 + second - 0xDC00 + 0x10000; + } + } + return first; + }; + if (defineProperty) { + defineProperty(String.prototype, 'codePointAt', { + 'value': codePointAt, + 'configurable': true, + 'writable': true + }); + } else { + String.prototype.codePointAt = codePointAt; + } + }()); + } + + var TINF_OK = 0; + var TINF_DATA_ERROR = -3; + + function Tree() { + this.table = new Uint16Array(16); /* table of code length counts */ + this.trans = new Uint16Array(288); /* code -> symbol translation table */ + } + + function Data(source, dest) { + this.source = source; + this.sourceIndex = 0; + this.tag = 0; + this.bitcount = 0; + + this.dest = dest; + this.destLen = 0; + + this.ltree = new Tree(); /* dynamic length/symbol tree */ + this.dtree = new Tree(); /* dynamic distance tree */ + } + + /* --------------------------------------------------- * + * -- uninitialized global data (static structures) -- * + * --------------------------------------------------- */ + + var sltree = new Tree(); + var sdtree = new Tree(); + + /* extra bits and base tables for length codes */ + var length_bits = new Uint8Array(30); + var length_base = new Uint16Array(30); + + /* extra bits and base tables for distance codes */ + var dist_bits = new Uint8Array(30); + var dist_base = new Uint16Array(30); + + /* special ordering of code length codes */ + var clcidx = new Uint8Array([ + 16, 17, 18, 0, 8, 7, 9, 6, + 10, 5, 11, 4, 12, 3, 13, 2, + 14, 1, 15 + ]); + + /* used by tinf_decode_trees, avoids allocations every call */ + var code_tree = new Tree(); + var lengths = new Uint8Array(288 + 32); + + /* ----------------------- * + * -- utility functions -- * + * ----------------------- */ + + /* build extra bits and base tables */ + function tinf_build_bits_base(bits, base, delta, first) { + var i, sum; + + /* build bits table */ + for (i = 0; i < delta; ++i) { bits[i] = 0; } + for (i = 0; i < 30 - delta; ++i) { bits[i + delta] = i / delta | 0; } + + /* build base table */ + for (sum = first, i = 0; i < 30; ++i) { + base[i] = sum; + sum += 1 << bits[i]; + } + } + + /* build the fixed huffman trees */ + function tinf_build_fixed_trees(lt, dt) { + var i; + + /* build fixed length tree */ + for (i = 0; i < 7; ++i) { lt.table[i] = 0; } + + lt.table[7] = 24; + lt.table[8] = 152; + lt.table[9] = 112; + + for (i = 0; i < 24; ++i) { lt.trans[i] = 256 + i; } + for (i = 0; i < 144; ++i) { lt.trans[24 + i] = i; } + for (i = 0; i < 8; ++i) { lt.trans[24 + 144 + i] = 280 + i; } + for (i = 0; i < 112; ++i) { lt.trans[24 + 144 + 8 + i] = 144 + i; } + + /* build fixed distance tree */ + for (i = 0; i < 5; ++i) { dt.table[i] = 0; } + + dt.table[5] = 32; + + for (i = 0; i < 32; ++i) { dt.trans[i] = i; } + } + + /* given an array of code lengths, build a tree */ + var offs = new Uint16Array(16); + + function tinf_build_tree(t, lengths, off, num) { + var i, sum; + + /* clear code length count table */ + for (i = 0; i < 16; ++i) { t.table[i] = 0; } + + /* scan symbol lengths, and sum code length counts */ + for (i = 0; i < num; ++i) { t.table[lengths[off + i]]++; } + + t.table[0] = 0; + + /* compute offset table for distribution sort */ + for (sum = 0, i = 0; i < 16; ++i) { + offs[i] = sum; + sum += t.table[i]; + } + + /* create code->symbol translation table (symbols sorted by code) */ + for (i = 0; i < num; ++i) { + if (lengths[off + i]) { t.trans[offs[lengths[off + i]]++] = i; } + } + } + + /* ---------------------- * + * -- decode functions -- * + * ---------------------- */ + + /* get one bit from source stream */ + function tinf_getbit(d) { + /* check if tag is empty */ + if (!d.bitcount--) { + /* load next tag */ + d.tag = d.source[d.sourceIndex++]; + d.bitcount = 7; + } + + /* shift bit out of tag */ + var bit = d.tag & 1; + d.tag >>>= 1; + + return bit; + } + + /* read a num bit value from a stream and add base */ + function tinf_read_bits(d, num, base) { + if (!num) + { return base; } + + while (d.bitcount < 24) { + d.tag |= d.source[d.sourceIndex++] << d.bitcount; + d.bitcount += 8; + } + + var val = d.tag & (0xffff >>> (16 - num)); + d.tag >>>= num; + d.bitcount -= num; + return val + base; + } + + /* given a data stream and a tree, decode a symbol */ + function tinf_decode_symbol(d, t) { + while (d.bitcount < 24) { + d.tag |= d.source[d.sourceIndex++] << d.bitcount; + d.bitcount += 8; + } + + var sum = 0, cur = 0, len = 0; + var tag = d.tag; + + /* get more bits while code value is above sum */ + do { + cur = 2 * cur + (tag & 1); + tag >>>= 1; + ++len; + + sum += t.table[len]; + cur -= t.table[len]; + } while (cur >= 0); + + d.tag = tag; + d.bitcount -= len; + + return t.trans[sum + cur]; + } + + /* given a data stream, decode dynamic trees from it */ + function tinf_decode_trees(d, lt, dt) { + var hlit, hdist, hclen; + var i, num, length; + + /* get 5 bits HLIT (257-286) */ + hlit = tinf_read_bits(d, 5, 257); + + /* get 5 bits HDIST (1-32) */ + hdist = tinf_read_bits(d, 5, 1); + + /* get 4 bits HCLEN (4-19) */ + hclen = tinf_read_bits(d, 4, 4); + + for (i = 0; i < 19; ++i) { lengths[i] = 0; } + + /* read code lengths for code length alphabet */ + for (i = 0; i < hclen; ++i) { + /* get 3 bits code length (0-7) */ + var clen = tinf_read_bits(d, 3, 0); + lengths[clcidx[i]] = clen; + } + + /* build code length tree */ + tinf_build_tree(code_tree, lengths, 0, 19); + + /* decode code lengths for the dynamic trees */ + for (num = 0; num < hlit + hdist;) { + var sym = tinf_decode_symbol(d, code_tree); + + switch (sym) { + case 16: + /* copy previous code length 3-6 times (read 2 bits) */ + var prev = lengths[num - 1]; + for (length = tinf_read_bits(d, 2, 3); length; --length) { + lengths[num++] = prev; + } + break; + case 17: + /* repeat code length 0 for 3-10 times (read 3 bits) */ + for (length = tinf_read_bits(d, 3, 3); length; --length) { + lengths[num++] = 0; + } + break; + case 18: + /* repeat code length 0 for 11-138 times (read 7 bits) */ + for (length = tinf_read_bits(d, 7, 11); length; --length) { + lengths[num++] = 0; + } + break; + default: + /* values 0-15 represent the actual code lengths */ + lengths[num++] = sym; + break; + } + } + + /* build dynamic trees */ + tinf_build_tree(lt, lengths, 0, hlit); + tinf_build_tree(dt, lengths, hlit, hdist); + } + + /* ----------------------------- * + * -- block inflate functions -- * + * ----------------------------- */ + + /* given a stream and two trees, inflate a block of data */ + function tinf_inflate_block_data(d, lt, dt) { + while (1) { + var sym = tinf_decode_symbol(d, lt); + + /* check for end of block */ + if (sym === 256) { + return TINF_OK; + } + + if (sym < 256) { + d.dest[d.destLen++] = sym; + } else { + var length, dist, offs; + var i; + + sym -= 257; + + /* possibly get more bits from length code */ + length = tinf_read_bits(d, length_bits[sym], length_base[sym]); + + dist = tinf_decode_symbol(d, dt); + + /* possibly get more bits from distance code */ + offs = d.destLen - tinf_read_bits(d, dist_bits[dist], dist_base[dist]); + + /* copy match */ + for (i = offs; i < offs + length; ++i) { + d.dest[d.destLen++] = d.dest[i]; + } + } + } + } + + /* inflate an uncompressed block of data */ + function tinf_inflate_uncompressed_block(d) { + var length, invlength; + var i; + + /* unread from bitbuffer */ + while (d.bitcount > 8) { + d.sourceIndex--; + d.bitcount -= 8; + } + + /* get length */ + length = d.source[d.sourceIndex + 1]; + length = 256 * length + d.source[d.sourceIndex]; + + /* get one's complement of length */ + invlength = d.source[d.sourceIndex + 3]; + invlength = 256 * invlength + d.source[d.sourceIndex + 2]; + + /* check length */ + if (length !== (~invlength & 0x0000ffff)) + { return TINF_DATA_ERROR; } + + d.sourceIndex += 4; + + /* copy block */ + for (i = length; i; --i) + { d.dest[d.destLen++] = d.source[d.sourceIndex++]; } + + /* make sure we start next block on a byte boundary */ + d.bitcount = 0; + + return TINF_OK; + } + + /* inflate stream from source to dest */ + function tinf_uncompress(source, dest) { + var d = new Data(source, dest); + var bfinal, btype, res; + + do { + /* read final block flag */ + bfinal = tinf_getbit(d); + + /* read block type (2 bits) */ + btype = tinf_read_bits(d, 2, 0); + + /* decompress block */ + switch (btype) { + case 0: + /* decompress uncompressed block */ + res = tinf_inflate_uncompressed_block(d); + break; + case 1: + /* decompress block with fixed huffman trees */ + res = tinf_inflate_block_data(d, sltree, sdtree); + break; + case 2: + /* decompress block with dynamic huffman trees */ + tinf_decode_trees(d, d.ltree, d.dtree); + res = tinf_inflate_block_data(d, d.ltree, d.dtree); + break; + default: + res = TINF_DATA_ERROR; + } + + if (res !== TINF_OK) + { throw new Error('Data error'); } + + } while (!bfinal); + + if (d.destLen < d.dest.length) { + if (typeof d.dest.slice === 'function') + { return d.dest.slice(0, d.destLen); } + else + { return d.dest.subarray(0, d.destLen); } + } + + return d.dest; + } + + /* -------------------- * + * -- initialization -- * + * -------------------- */ + + /* build fixed huffman trees */ + tinf_build_fixed_trees(sltree, sdtree); + + /* build extra bits and base tables */ + tinf_build_bits_base(length_bits, length_base, 4, 3); + tinf_build_bits_base(dist_bits, dist_base, 2, 1); + + /* fix a special case */ + length_bits[28] = 0; + length_base[28] = 258; + + var tinyInflate = tinf_uncompress; + + // The Bounding Box object + + function derive(v0, v1, v2, v3, t) { + return Math.pow(1 - t, 3) * v0 + + 3 * Math.pow(1 - t, 2) * t * v1 + + 3 * (1 - t) * Math.pow(t, 2) * v2 + + Math.pow(t, 3) * v3; + } + /** + * A bounding box is an enclosing box that describes the smallest measure within which all the points lie. + * It is used to calculate the bounding box of a glyph or text path. + * + * On initialization, x1/y1/x2/y2 will be NaN. Check if the bounding box is empty using `isEmpty()`. + * + * @exports opentype.BoundingBox + * @class + * @constructor + */ + function BoundingBox() { + this.x1 = Number.NaN; + this.y1 = Number.NaN; + this.x2 = Number.NaN; + this.y2 = Number.NaN; + } + + /** + * Returns true if the bounding box is empty, that is, no points have been added to the box yet. + */ + BoundingBox.prototype.isEmpty = function() { + return isNaN(this.x1) || isNaN(this.y1) || isNaN(this.x2) || isNaN(this.y2); + }; + + /** + * Add the point to the bounding box. + * The x1/y1/x2/y2 coordinates of the bounding box will now encompass the given point. + * @param {number} x - The X coordinate of the point. + * @param {number} y - The Y coordinate of the point. + */ + BoundingBox.prototype.addPoint = function(x, y) { + if (typeof x === 'number') { + if (isNaN(this.x1) || isNaN(this.x2)) { + this.x1 = x; + this.x2 = x; + } + if (x < this.x1) { + this.x1 = x; + } + if (x > this.x2) { + this.x2 = x; + } + } + if (typeof y === 'number') { + if (isNaN(this.y1) || isNaN(this.y2)) { + this.y1 = y; + this.y2 = y; + } + if (y < this.y1) { + this.y1 = y; + } + if (y > this.y2) { + this.y2 = y; + } + } + }; + + /** + * Add a X coordinate to the bounding box. + * This extends the bounding box to include the X coordinate. + * This function is used internally inside of addBezier. + * @param {number} x - The X coordinate of the point. + */ + BoundingBox.prototype.addX = function(x) { + this.addPoint(x, null); + }; + + /** + * Add a Y coordinate to the bounding box. + * This extends the bounding box to include the Y coordinate. + * This function is used internally inside of addBezier. + * @param {number} y - The Y coordinate of the point. + */ + BoundingBox.prototype.addY = function(y) { + this.addPoint(null, y); + }; + + /** + * Add a Bézier curve to the bounding box. + * This extends the bounding box to include the entire Bézier. + * @param {number} x0 - The starting X coordinate. + * @param {number} y0 - The starting Y coordinate. + * @param {number} x1 - The X coordinate of the first control point. + * @param {number} y1 - The Y coordinate of the first control point. + * @param {number} x2 - The X coordinate of the second control point. + * @param {number} y2 - The Y coordinate of the second control point. + * @param {number} x - The ending X coordinate. + * @param {number} y - The ending Y coordinate. + */ + BoundingBox.prototype.addBezier = function(x0, y0, x1, y1, x2, y2, x, y) { + // This code is based on http://nishiohirokazu.blogspot.com/2009/06/how-to-calculate-bezier-curves-bounding.html + // and https://github.com/icons8/svg-path-bounding-box + + var p0 = [x0, y0]; + var p1 = [x1, y1]; + var p2 = [x2, y2]; + var p3 = [x, y]; + + this.addPoint(x0, y0); + this.addPoint(x, y); + + for (var i = 0; i <= 1; i++) { + var b = 6 * p0[i] - 12 * p1[i] + 6 * p2[i]; + var a = -3 * p0[i] + 9 * p1[i] - 9 * p2[i] + 3 * p3[i]; + var c = 3 * p1[i] - 3 * p0[i]; + + if (a === 0) { + if (b === 0) { continue; } + var t = -c / b; + if (0 < t && t < 1) { + if (i === 0) { this.addX(derive(p0[i], p1[i], p2[i], p3[i], t)); } + if (i === 1) { this.addY(derive(p0[i], p1[i], p2[i], p3[i], t)); } + } + continue; + } + + var b2ac = Math.pow(b, 2) - 4 * c * a; + if (b2ac < 0) { continue; } + var t1 = (-b + Math.sqrt(b2ac)) / (2 * a); + if (0 < t1 && t1 < 1) { + if (i === 0) { this.addX(derive(p0[i], p1[i], p2[i], p3[i], t1)); } + if (i === 1) { this.addY(derive(p0[i], p1[i], p2[i], p3[i], t1)); } + } + var t2 = (-b - Math.sqrt(b2ac)) / (2 * a); + if (0 < t2 && t2 < 1) { + if (i === 0) { this.addX(derive(p0[i], p1[i], p2[i], p3[i], t2)); } + if (i === 1) { this.addY(derive(p0[i], p1[i], p2[i], p3[i], t2)); } + } + } + }; + + /** + * Add a quadratic curve to the bounding box. + * This extends the bounding box to include the entire quadratic curve. + * @param {number} x0 - The starting X coordinate. + * @param {number} y0 - The starting Y coordinate. + * @param {number} x1 - The X coordinate of the control point. + * @param {number} y1 - The Y coordinate of the control point. + * @param {number} x - The ending X coordinate. + * @param {number} y - The ending Y coordinate. + */ + BoundingBox.prototype.addQuad = function(x0, y0, x1, y1, x, y) { + var cp1x = x0 + 2 / 3 * (x1 - x0); + var cp1y = y0 + 2 / 3 * (y1 - y0); + var cp2x = cp1x + 1 / 3 * (x - x0); + var cp2y = cp1y + 1 / 3 * (y - y0); + this.addBezier(x0, y0, cp1x, cp1y, cp2x, cp2y, x, y); + }; + + // Geometric objects + + /** + * A bézier path containing a set of path commands similar to a SVG path. + * Paths can be drawn on a context using `draw`. + * @exports opentype.Path + * @class + * @constructor + */ + function Path() { + this.commands = []; + this.fill = 'black'; + this.stroke = null; + this.strokeWidth = 1; + } + + /** + * @param {number} x + * @param {number} y + */ + Path.prototype.moveTo = function(x, y) { + this.commands.push({ + type: 'M', + x: x, + y: y + }); + }; + + /** + * @param {number} x + * @param {number} y + */ + Path.prototype.lineTo = function(x, y) { + this.commands.push({ + type: 'L', + x: x, + y: y + }); + }; + + /** + * Draws cubic curve + * @function + * curveTo + * @memberof opentype.Path.prototype + * @param {number} x1 - x of control 1 + * @param {number} y1 - y of control 1 + * @param {number} x2 - x of control 2 + * @param {number} y2 - y of control 2 + * @param {number} x - x of path point + * @param {number} y - y of path point + */ + + /** + * Draws cubic curve + * @function + * bezierCurveTo + * @memberof opentype.Path.prototype + * @param {number} x1 - x of control 1 + * @param {number} y1 - y of control 1 + * @param {number} x2 - x of control 2 + * @param {number} y2 - y of control 2 + * @param {number} x - x of path point + * @param {number} y - y of path point + * @see curveTo + */ + Path.prototype.curveTo = Path.prototype.bezierCurveTo = function(x1, y1, x2, y2, x, y) { + this.commands.push({ + type: 'C', + x1: x1, + y1: y1, + x2: x2, + y2: y2, + x: x, + y: y + }); + }; + + /** + * Draws quadratic curve + * @function + * quadraticCurveTo + * @memberof opentype.Path.prototype + * @param {number} x1 - x of control + * @param {number} y1 - y of control + * @param {number} x - x of path point + * @param {number} y - y of path point + */ + + /** + * Draws quadratic curve + * @function + * quadTo + * @memberof opentype.Path.prototype + * @param {number} x1 - x of control + * @param {number} y1 - y of control + * @param {number} x - x of path point + * @param {number} y - y of path point + */ + Path.prototype.quadTo = Path.prototype.quadraticCurveTo = function(x1, y1, x, y) { + this.commands.push({ + type: 'Q', + x1: x1, + y1: y1, + x: x, + y: y + }); + }; + + /** + * Closes the path + * @function closePath + * @memberof opentype.Path.prototype + */ + + /** + * Close the path + * @function close + * @memberof opentype.Path.prototype + */ + Path.prototype.close = Path.prototype.closePath = function() { + this.commands.push({ + type: 'Z' + }); + }; + + /** + * Add the given path or list of commands to the commands of this path. + * @param {Array} pathOrCommands - another opentype.Path, an opentype.BoundingBox, or an array of commands. + */ + Path.prototype.extend = function(pathOrCommands) { + if (pathOrCommands.commands) { + pathOrCommands = pathOrCommands.commands; + } else if (pathOrCommands instanceof BoundingBox) { + var box = pathOrCommands; + this.moveTo(box.x1, box.y1); + this.lineTo(box.x2, box.y1); + this.lineTo(box.x2, box.y2); + this.lineTo(box.x1, box.y2); + this.close(); + return; + } + + Array.prototype.push.apply(this.commands, pathOrCommands); + }; + + /** + * Calculate the bounding box of the path. + * @returns {opentype.BoundingBox} + */ + Path.prototype.getBoundingBox = function() { + var box = new BoundingBox(); + + var startX = 0; + var startY = 0; + var prevX = 0; + var prevY = 0; + for (var i = 0; i < this.commands.length; i++) { + var cmd = this.commands[i]; + switch (cmd.type) { + case 'M': + box.addPoint(cmd.x, cmd.y); + startX = prevX = cmd.x; + startY = prevY = cmd.y; + break; + case 'L': + box.addPoint(cmd.x, cmd.y); + prevX = cmd.x; + prevY = cmd.y; + break; + case 'Q': + box.addQuad(prevX, prevY, cmd.x1, cmd.y1, cmd.x, cmd.y); + prevX = cmd.x; + prevY = cmd.y; + break; + case 'C': + box.addBezier(prevX, prevY, cmd.x1, cmd.y1, cmd.x2, cmd.y2, cmd.x, cmd.y); + prevX = cmd.x; + prevY = cmd.y; + break; + case 'Z': + prevX = startX; + prevY = startY; + break; + default: + throw new Error('Unexpected path command ' + cmd.type); + } + } + if (box.isEmpty()) { + box.addPoint(0, 0); + } + return box; + }; + + /** + * Draw the path to a 2D context. + * @param {CanvasRenderingContext2D} ctx - A 2D drawing context. + */ + Path.prototype.draw = function(ctx) { + ctx.beginPath(); + for (var i = 0; i < this.commands.length; i += 1) { + var cmd = this.commands[i]; + if (cmd.type === 'M') { + ctx.moveTo(cmd.x, cmd.y); + } else if (cmd.type === 'L') { + ctx.lineTo(cmd.x, cmd.y); + } else if (cmd.type === 'C') { + ctx.bezierCurveTo(cmd.x1, cmd.y1, cmd.x2, cmd.y2, cmd.x, cmd.y); + } else if (cmd.type === 'Q') { + ctx.quadraticCurveTo(cmd.x1, cmd.y1, cmd.x, cmd.y); + } else if (cmd.type === 'Z') { + ctx.closePath(); + } + } + + if (this.fill) { + ctx.fillStyle = this.fill; + ctx.fill(); + } + + if (this.stroke) { + ctx.strokeStyle = this.stroke; + ctx.lineWidth = this.strokeWidth; + ctx.stroke(); + } + }; + + /** + * Convert the Path to a string of path data instructions + * See http://www.w3.org/TR/SVG/paths.html#PathData + * @param {number} [decimalPlaces=2] - The amount of decimal places for floating-point values + * @return {string} + */ + Path.prototype.toPathData = function(decimalPlaces) { + decimalPlaces = decimalPlaces !== undefined ? decimalPlaces : 2; + + function floatToString(v) { + if (Math.round(v) === v) { + return '' + Math.round(v); + } else { + return v.toFixed(decimalPlaces); + } + } + + function packValues() { + var arguments$1 = arguments; + + var s = ''; + for (var i = 0; i < arguments.length; i += 1) { + var v = arguments$1[i]; + if (v >= 0 && i > 0) { + s += ' '; + } + + s += floatToString(v); + } + + return s; + } + + var d = ''; + for (var i = 0; i < this.commands.length; i += 1) { + var cmd = this.commands[i]; + if (cmd.type === 'M') { + d += 'M' + packValues(cmd.x, cmd.y); + } else if (cmd.type === 'L') { + d += 'L' + packValues(cmd.x, cmd.y); + } else if (cmd.type === 'C') { + d += 'C' + packValues(cmd.x1, cmd.y1, cmd.x2, cmd.y2, cmd.x, cmd.y); + } else if (cmd.type === 'Q') { + d += 'Q' + packValues(cmd.x1, cmd.y1, cmd.x, cmd.y); + } else if (cmd.type === 'Z') { + d += 'Z'; + } + } + + return d; + }; + + /** + * Convert the path to an SVG element, as a string. + * @param {number} [decimalPlaces=2] - The amount of decimal places for floating-point values + * @return {string} + */ + Path.prototype.toSVG = function(decimalPlaces) { + var svg = '= 0 && v <= 255, 'Byte value should be between 0 and 255.'); + return [v]; + }; + /** + * @constant + * @type {number} + */ + sizeOf.BYTE = constant(1); + + /** + * Convert a 8-bit signed integer to a list of 1 byte. + * @param {string} + * @returns {Array} + */ + encode.CHAR = function(v) { + return [v.charCodeAt(0)]; + }; + + /** + * @constant + * @type {number} + */ + sizeOf.CHAR = constant(1); + + /** + * Convert an ASCII string to a list of bytes. + * @param {string} + * @returns {Array} + */ + encode.CHARARRAY = function(v) { + if (typeof v === 'undefined') { + v = ''; + console.warn('Undefined CHARARRAY encountered and treated as an empty string. This is probably caused by a missing glyph name.'); + } + var b = []; + for (var i = 0; i < v.length; i += 1) { + b[i] = v.charCodeAt(i); + } + + return b; + }; + + /** + * @param {Array} + * @returns {number} + */ + sizeOf.CHARARRAY = function(v) { + if (typeof v === 'undefined') { + return 0; + } + return v.length; + }; + + /** + * Convert a 16-bit unsigned integer to a list of 2 bytes. + * @param {number} + * @returns {Array} + */ + encode.USHORT = function(v) { + return [(v >> 8) & 0xFF, v & 0xFF]; + }; + + /** + * @constant + * @type {number} + */ + sizeOf.USHORT = constant(2); + + /** + * Convert a 16-bit signed integer to a list of 2 bytes. + * @param {number} + * @returns {Array} + */ + encode.SHORT = function(v) { + // Two's complement + if (v >= LIMIT16) { + v = -(2 * LIMIT16 - v); + } + + return [(v >> 8) & 0xFF, v & 0xFF]; + }; + + /** + * @constant + * @type {number} + */ + sizeOf.SHORT = constant(2); + + /** + * Convert a 24-bit unsigned integer to a list of 3 bytes. + * @param {number} + * @returns {Array} + */ + encode.UINT24 = function(v) { + return [(v >> 16) & 0xFF, (v >> 8) & 0xFF, v & 0xFF]; + }; + + /** + * @constant + * @type {number} + */ + sizeOf.UINT24 = constant(3); + + /** + * Convert a 32-bit unsigned integer to a list of 4 bytes. + * @param {number} + * @returns {Array} + */ + encode.ULONG = function(v) { + return [(v >> 24) & 0xFF, (v >> 16) & 0xFF, (v >> 8) & 0xFF, v & 0xFF]; + }; + + /** + * @constant + * @type {number} + */ + sizeOf.ULONG = constant(4); + + /** + * Convert a 32-bit unsigned integer to a list of 4 bytes. + * @param {number} + * @returns {Array} + */ + encode.LONG = function(v) { + // Two's complement + if (v >= LIMIT32) { + v = -(2 * LIMIT32 - v); + } + + return [(v >> 24) & 0xFF, (v >> 16) & 0xFF, (v >> 8) & 0xFF, v & 0xFF]; + }; + + /** + * @constant + * @type {number} + */ + sizeOf.LONG = constant(4); + + encode.FIXED = encode.ULONG; + sizeOf.FIXED = sizeOf.ULONG; + + encode.FWORD = encode.SHORT; + sizeOf.FWORD = sizeOf.SHORT; + + encode.UFWORD = encode.USHORT; + sizeOf.UFWORD = sizeOf.USHORT; + + /** + * Convert a 32-bit Apple Mac timestamp integer to a list of 8 bytes, 64-bit timestamp. + * @param {number} + * @returns {Array} + */ + encode.LONGDATETIME = function(v) { + return [0, 0, 0, 0, (v >> 24) & 0xFF, (v >> 16) & 0xFF, (v >> 8) & 0xFF, v & 0xFF]; + }; + + /** + * @constant + * @type {number} + */ + sizeOf.LONGDATETIME = constant(8); + + /** + * Convert a 4-char tag to a list of 4 bytes. + * @param {string} + * @returns {Array} + */ + encode.TAG = function(v) { + check.argument(v.length === 4, 'Tag should be exactly 4 ASCII characters.'); + return [v.charCodeAt(0), + v.charCodeAt(1), + v.charCodeAt(2), + v.charCodeAt(3)]; + }; + + /** + * @constant + * @type {number} + */ + sizeOf.TAG = constant(4); + + // CFF data types /////////////////////////////////////////////////////////// + + encode.Card8 = encode.BYTE; + sizeOf.Card8 = sizeOf.BYTE; + + encode.Card16 = encode.USHORT; + sizeOf.Card16 = sizeOf.USHORT; + + encode.OffSize = encode.BYTE; + sizeOf.OffSize = sizeOf.BYTE; + + encode.SID = encode.USHORT; + sizeOf.SID = sizeOf.USHORT; + + // Convert a numeric operand or charstring number to a variable-size list of bytes. + /** + * Convert a numeric operand or charstring number to a variable-size list of bytes. + * @param {number} + * @returns {Array} + */ + encode.NUMBER = function(v) { + if (v >= -107 && v <= 107) { + return [v + 139]; + } else if (v >= 108 && v <= 1131) { + v = v - 108; + return [(v >> 8) + 247, v & 0xFF]; + } else if (v >= -1131 && v <= -108) { + v = -v - 108; + return [(v >> 8) + 251, v & 0xFF]; + } else if (v >= -32768 && v <= 32767) { + return encode.NUMBER16(v); + } else { + return encode.NUMBER32(v); + } + }; + + /** + * @param {number} + * @returns {number} + */ + sizeOf.NUMBER = function(v) { + return encode.NUMBER(v).length; + }; + + /** + * Convert a signed number between -32768 and +32767 to a three-byte value. + * This ensures we always use three bytes, but is not the most compact format. + * @param {number} + * @returns {Array} + */ + encode.NUMBER16 = function(v) { + return [28, (v >> 8) & 0xFF, v & 0xFF]; + }; + + /** + * @constant + * @type {number} + */ + sizeOf.NUMBER16 = constant(3); + + /** + * Convert a signed number between -(2^31) and +(2^31-1) to a five-byte value. + * This is useful if you want to be sure you always use four bytes, + * at the expense of wasting a few bytes for smaller numbers. + * @param {number} + * @returns {Array} + */ + encode.NUMBER32 = function(v) { + return [29, (v >> 24) & 0xFF, (v >> 16) & 0xFF, (v >> 8) & 0xFF, v & 0xFF]; + }; + + /** + * @constant + * @type {number} + */ + sizeOf.NUMBER32 = constant(5); + + /** + * @param {number} + * @returns {Array} + */ + encode.REAL = function(v) { + var value = v.toString(); + + // Some numbers use an epsilon to encode the value. (e.g. JavaScript will store 0.0000001 as 1e-7) + // This code converts it back to a number without the epsilon. + var m = /\.(\d*?)(?:9{5,20}|0{5,20})\d{0,2}(?:e(.+)|$)/.exec(value); + if (m) { + var epsilon = parseFloat('1e' + ((m[2] ? +m[2] : 0) + m[1].length)); + value = (Math.round(v * epsilon) / epsilon).toString(); + } + + var nibbles = ''; + for (var i = 0, ii = value.length; i < ii; i += 1) { + var c = value[i]; + if (c === 'e') { + nibbles += value[++i] === '-' ? 'c' : 'b'; + } else if (c === '.') { + nibbles += 'a'; + } else if (c === '-') { + nibbles += 'e'; + } else { + nibbles += c; + } + } + + nibbles += (nibbles.length & 1) ? 'f' : 'ff'; + var out = [30]; + for (var i$1 = 0, ii$1 = nibbles.length; i$1 < ii$1; i$1 += 2) { + out.push(parseInt(nibbles.substr(i$1, 2), 16)); + } + + return out; + }; + + /** + * @param {number} + * @returns {number} + */ + sizeOf.REAL = function(v) { + return encode.REAL(v).length; + }; + + encode.NAME = encode.CHARARRAY; + sizeOf.NAME = sizeOf.CHARARRAY; + + encode.STRING = encode.CHARARRAY; + sizeOf.STRING = sizeOf.CHARARRAY; + + /** + * @param {DataView} data + * @param {number} offset + * @param {number} numBytes + * @returns {string} + */ + decode.UTF8 = function(data, offset, numBytes) { + var codePoints = []; + var numChars = numBytes; + for (var j = 0; j < numChars; j++, offset += 1) { + codePoints[j] = data.getUint8(offset); + } + + return String.fromCharCode.apply(null, codePoints); + }; + + /** + * @param {DataView} data + * @param {number} offset + * @param {number} numBytes + * @returns {string} + */ + decode.UTF16 = function(data, offset, numBytes) { + var codePoints = []; + var numChars = numBytes / 2; + for (var j = 0; j < numChars; j++, offset += 2) { + codePoints[j] = data.getUint16(offset); + } + + return String.fromCharCode.apply(null, codePoints); + }; + + /** + * Convert a JavaScript string to UTF16-BE. + * @param {string} + * @returns {Array} + */ + encode.UTF16 = function(v) { + var b = []; + for (var i = 0; i < v.length; i += 1) { + var codepoint = v.charCodeAt(i); + b[b.length] = (codepoint >> 8) & 0xFF; + b[b.length] = codepoint & 0xFF; + } + + return b; + }; + + /** + * @param {string} + * @returns {number} + */ + sizeOf.UTF16 = function(v) { + return v.length * 2; + }; + + // Data for converting old eight-bit Macintosh encodings to Unicode. + // This representation is optimized for decoding; encoding is slower + // and needs more memory. The assumption is that all opentype.js users + // want to open fonts, but saving a font will be comparatively rare + // so it can be more expensive. Keyed by IANA character set name. + // + // Python script for generating these strings: + // + // s = u''.join([chr(c).decode('mac_greek') for c in range(128, 256)]) + // print(s.encode('utf-8')) + /** + * @private + */ + var eightBitMacEncodings = { + 'x-mac-croatian': // Python: 'mac_croatian' + 'ÄÅÇÉÑÖÜáàâäãåçéèêëíìîïñóòôöõúùûü†°¢£§•¶ß®Š™´¨≠ŽØ∞±≤≥∆µ∂∑∏š∫ªºΩžø' + + '¿¡¬√ƒ≈ƫȅ ÀÃÕŒœĐ—“”‘’÷◊©⁄€‹›Æ»–·‚„‰ÂćÁčÈÍÎÏÌÓÔđÒÚÛÙıˆ˜¯πË˚¸Êæˇ', + 'x-mac-cyrillic': // Python: 'mac_cyrillic' + 'АБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯ†°Ґ£§•¶І®©™Ђђ≠Ѓѓ∞±≤≥іµґЈЄєЇїЉљЊњ' + + 'јЅ¬√ƒ≈∆«»… ЋћЌќѕ–—“”‘’÷„ЎўЏџ№Ёёяабвгдежзийклмнопрстуфхцчшщъыьэю', + 'x-mac-gaelic': // http://unicode.org/Public/MAPPINGS/VENDORS/APPLE/GAELIC.TXT + 'ÄÅÇÉÑÖÜáàâäãåçéèêëíìîïñóòôöõúùûü†°¢£§•¶ß®©™´¨≠ÆØḂ±≤≥ḃĊċḊḋḞḟĠġṀæø' + + 'ṁṖṗɼƒſṠ«»… ÀÃÕŒœ–—“”‘’ṡẛÿŸṪ€‹›Ŷŷṫ·Ỳỳ⁊ÂÊÁËÈÍÎÏÌÓÔ♣ÒÚÛÙıÝýŴŵẄẅẀẁẂẃ', + 'x-mac-greek': // Python: 'mac_greek' + 'Ĺ²É³ÖÜ΅àâä΄¨çéèê룙î‰ôö¦€ùûü†ΓΔΘΛΞΠß®©ΣΪ§≠°·Α±≤≥¥ΒΕΖΗΙΚΜΦΫΨΩ' + + 'άΝ¬ΟΡ≈Τ«»… ΥΧΆΈœ–―“”‘’÷ΉΊΌΎέήίόΏύαβψδεφγηιξκλμνοπώρστθωςχυζϊϋΐΰ\u00AD', + 'x-mac-icelandic': // Python: 'mac_iceland' + 'ÄÅÇÉÑÖÜáàâäãåçéèêëíìîïñóòôöõúùûüݰ¢£§•¶ß®©™´¨≠ÆØ∞±≤≥¥µ∂∑∏π∫ªºΩæø' + + '¿¡¬√ƒ≈∆«»… ÀÃÕŒœ–—“”‘’÷◊ÿŸ⁄€ÐðÞþý·‚„‰ÂÊÁËÈÍÎÏÌÓÔÒÚÛÙıˆ˜¯˘˙˚¸˝˛ˇ', + 'x-mac-inuit': // http://unicode.org/Public/MAPPINGS/VENDORS/APPLE/INUIT.TXT + 'ᐃᐄᐅᐆᐊᐋᐱᐲᐳᐴᐸᐹᑉᑎᑏᑐᑑᑕᑖᑦᑭᑮᑯᑰᑲᑳᒃᒋᒌᒍᒎᒐᒑ°ᒡᒥᒦ•¶ᒧ®©™ᒨᒪᒫᒻᓂᓃᓄᓅᓇᓈᓐᓯᓰᓱᓲᓴᓵᔅᓕᓖᓗ' + + 'ᓘᓚᓛᓪᔨᔩᔪᔫᔭ… ᔮᔾᕕᕖᕗ–—“”‘’ᕘᕙᕚᕝᕆᕇᕈᕉᕋᕌᕐᕿᖀᖁᖂᖃᖄᖅᖏᖐᖑᖒᖓᖔᖕᙱᙲᙳᙴᙵᙶᖖᖠᖡᖢᖣᖤᖥᖦᕼŁł', + 'x-mac-ce': // Python: 'mac_latin2' + 'ÄĀāÉĄÖÜáąČäčĆć鏟ĎíďĒēĖóėôöõúĚěü†°Ę£§•¶ß®©™ę¨≠ģĮįĪ≤≥īĶ∂∑łĻļĽľĹĺŅ' + + 'ņѬ√ńŇ∆«»… ňŐÕőŌ–—“”‘’÷◊ōŔŕŘ‹›řŖŗŠ‚„šŚśÁŤťÍŽžŪÓÔūŮÚůŰűŲųÝýķŻŁżĢˇ', + macintosh: // Python: 'mac_roman' + 'ÄÅÇÉÑÖÜáàâäãåçéèêëíìîïñóòôöõúùûü†°¢£§•¶ß®©™´¨≠ÆØ∞±≤≥¥µ∂∑∏π∫ªºΩæø' + + '¿¡¬√ƒ≈∆«»… ÀÃÕŒœ–—“”‘’÷◊ÿŸ⁄€‹›fifl‡·‚„‰ÂÊÁËÈÍÎÏÌÓÔÒÚÛÙıˆ˜¯˘˙˚¸˝˛ˇ', + 'x-mac-romanian': // Python: 'mac_romanian' + 'ÄÅÇÉÑÖÜáàâäãåçéèêëíìîïñóòôöõúùûü†°¢£§•¶ß®©™´¨≠ĂȘ∞±≤≥¥µ∂∑∏π∫ªºΩăș' + + '¿¡¬√ƒ≈∆«»… ÀÃÕŒœ–—“”‘’÷◊ÿŸ⁄€‹›Țț‡·‚„‰ÂÊÁËÈÍÎÏÌÓÔÒÚÛÙıˆ˜¯˘˙˚¸˝˛ˇ', + 'x-mac-turkish': // Python: 'mac_turkish' + 'ÄÅÇÉÑÖÜáàâäãåçéèêëíìîïñóòôöõúùûü†°¢£§•¶ß®©™´¨≠ÆØ∞±≤≥¥µ∂∑∏π∫ªºΩæø' + + '¿¡¬√ƒ≈∆«»… ÀÃÕŒœ–—“”‘’÷◊ÿŸĞğİıŞş‡·‚„‰ÂÊÁËÈÍÎÏÌÓÔÒÚÛÙˆ˜¯˘˙˚¸˝˛ˇ' + }; + + /** + * Decodes an old-style Macintosh string. Returns either a Unicode JavaScript + * string, or 'undefined' if the encoding is unsupported. For example, we do + * not support Chinese, Japanese or Korean because these would need large + * mapping tables. + * @param {DataView} dataView + * @param {number} offset + * @param {number} dataLength + * @param {string} encoding + * @returns {string} + */ + decode.MACSTRING = function(dataView, offset, dataLength, encoding) { + var table = eightBitMacEncodings[encoding]; + if (table === undefined) { + return undefined; + } + + var result = ''; + for (var i = 0; i < dataLength; i++) { + var c = dataView.getUint8(offset + i); + // In all eight-bit Mac encodings, the characters 0x00..0x7F are + // mapped to U+0000..U+007F; we only need to look up the others. + if (c <= 0x7F) { + result += String.fromCharCode(c); + } else { + result += table[c & 0x7F]; + } + } + + return result; + }; + + // Helper function for encode.MACSTRING. Returns a dictionary for mapping + // Unicode character codes to their 8-bit MacOS equivalent. This table + // is not exactly a super cheap data structure, but we do not care because + // encoding Macintosh strings is only rarely needed in typical applications. + var macEncodingTableCache = typeof WeakMap === 'function' && new WeakMap(); + var macEncodingCacheKeys; + var getMacEncodingTable = function (encoding) { + // Since we use encoding as a cache key for WeakMap, it has to be + // a String object and not a literal. And at least on NodeJS 2.10.1, + // WeakMap requires that the same String instance is passed for cache hits. + if (!macEncodingCacheKeys) { + macEncodingCacheKeys = {}; + for (var e in eightBitMacEncodings) { + /*jshint -W053 */ // Suppress "Do not use String as a constructor." + macEncodingCacheKeys[e] = new String(e); + } + } + + var cacheKey = macEncodingCacheKeys[encoding]; + if (cacheKey === undefined) { + return undefined; + } + + // We can't do "if (cache.has(key)) {return cache.get(key)}" here: + // since garbage collection may run at any time, it could also kick in + // between the calls to cache.has() and cache.get(). In that case, + // we would return 'undefined' even though we do support the encoding. + if (macEncodingTableCache) { + var cachedTable = macEncodingTableCache.get(cacheKey); + if (cachedTable !== undefined) { + return cachedTable; + } + } + + var decodingTable = eightBitMacEncodings[encoding]; + if (decodingTable === undefined) { + return undefined; + } + + var encodingTable = {}; + for (var i = 0; i < decodingTable.length; i++) { + encodingTable[decodingTable.charCodeAt(i)] = i + 0x80; + } + + if (macEncodingTableCache) { + macEncodingTableCache.set(cacheKey, encodingTable); + } + + return encodingTable; + }; + + /** + * Encodes an old-style Macintosh string. Returns a byte array upon success. + * If the requested encoding is unsupported, or if the input string contains + * a character that cannot be expressed in the encoding, the function returns + * 'undefined'. + * @param {string} str + * @param {string} encoding + * @returns {Array} + */ + encode.MACSTRING = function(str, encoding) { + var table = getMacEncodingTable(encoding); + if (table === undefined) { + return undefined; + } + + var result = []; + for (var i = 0; i < str.length; i++) { + var c = str.charCodeAt(i); + + // In all eight-bit Mac encodings, the characters 0x00..0x7F are + // mapped to U+0000..U+007F; we only need to look up the others. + if (c >= 0x80) { + c = table[c]; + if (c === undefined) { + // str contains a Unicode character that cannot be encoded + // in the requested encoding. + return undefined; + } + } + result[i] = c; + // result.push(c); + } + + return result; + }; + + /** + * @param {string} str + * @param {string} encoding + * @returns {number} + */ + sizeOf.MACSTRING = function(str, encoding) { + var b = encode.MACSTRING(str, encoding); + if (b !== undefined) { + return b.length; + } else { + return 0; + } + }; + + // Helper for encode.VARDELTAS + function isByteEncodable(value) { + return value >= -128 && value <= 127; + } + + // Helper for encode.VARDELTAS + function encodeVarDeltaRunAsZeroes(deltas, pos, result) { + var runLength = 0; + var numDeltas = deltas.length; + while (pos < numDeltas && runLength < 64 && deltas[pos] === 0) { + ++pos; + ++runLength; + } + result.push(0x80 | (runLength - 1)); + return pos; + } + + // Helper for encode.VARDELTAS + function encodeVarDeltaRunAsBytes(deltas, offset, result) { + var runLength = 0; + var numDeltas = deltas.length; + var pos = offset; + while (pos < numDeltas && runLength < 64) { + var value = deltas[pos]; + if (!isByteEncodable(value)) { + break; + } + + // Within a byte-encoded run of deltas, a single zero is best + // stored literally as 0x00 value. However, if we have two or + // more zeroes in a sequence, it is better to start a new run. + // Fore example, the sequence of deltas [15, 15, 0, 15, 15] + // becomes 6 bytes (04 0F 0F 00 0F 0F) when storing the zero + // within the current run, but 7 bytes (01 0F 0F 80 01 0F 0F) + // when starting a new run. + if (value === 0 && pos + 1 < numDeltas && deltas[pos + 1] === 0) { + break; + } + + ++pos; + ++runLength; + } + result.push(runLength - 1); + for (var i = offset; i < pos; ++i) { + result.push((deltas[i] + 256) & 0xff); + } + return pos; + } + + // Helper for encode.VARDELTAS + function encodeVarDeltaRunAsWords(deltas, offset, result) { + var runLength = 0; + var numDeltas = deltas.length; + var pos = offset; + while (pos < numDeltas && runLength < 64) { + var value = deltas[pos]; + + // Within a word-encoded run of deltas, it is easiest to start + // a new run (with a different encoding) whenever we encounter + // a zero value. For example, the sequence [0x6666, 0, 0x7777] + // needs 7 bytes when storing the zero inside the current run + // (42 66 66 00 00 77 77), and equally 7 bytes when starting a + // new run (40 66 66 80 40 77 77). + if (value === 0) { + break; + } + + // Within a word-encoded run of deltas, a single value in the + // range (-128..127) should be encoded within the current run + // because it is more compact. For example, the sequence + // [0x6666, 2, 0x7777] becomes 7 bytes when storing the value + // literally (42 66 66 00 02 77 77), but 8 bytes when starting + // a new run (40 66 66 00 02 40 77 77). + if (isByteEncodable(value) && pos + 1 < numDeltas && isByteEncodable(deltas[pos + 1])) { + break; + } + + ++pos; + ++runLength; + } + result.push(0x40 | (runLength - 1)); + for (var i = offset; i < pos; ++i) { + var val = deltas[i]; + result.push(((val + 0x10000) >> 8) & 0xff, (val + 0x100) & 0xff); + } + return pos; + } + + /** + * Encode a list of variation adjustment deltas. + * + * Variation adjustment deltas are used in ‘gvar’ and ‘cvar’ tables. + * They indicate how points (in ‘gvar’) or values (in ‘cvar’) get adjusted + * when generating instances of variation fonts. + * + * @see https://www.microsoft.com/typography/otspec/gvar.htm + * @see https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6gvar.html + * @param {Array} + * @return {Array} + */ + encode.VARDELTAS = function(deltas) { + var pos = 0; + var result = []; + while (pos < deltas.length) { + var value = deltas[pos]; + if (value === 0) { + pos = encodeVarDeltaRunAsZeroes(deltas, pos, result); + } else if (value >= -128 && value <= 127) { + pos = encodeVarDeltaRunAsBytes(deltas, pos, result); + } else { + pos = encodeVarDeltaRunAsWords(deltas, pos, result); + } + } + return result; + }; + + // Convert a list of values to a CFF INDEX structure. + // The values should be objects containing name / type / value. + /** + * @param {Array} l + * @returns {Array} + */ + encode.INDEX = function(l) { + //var offset, offsets, offsetEncoder, encodedOffsets, encodedOffset, data, + // i, v; + // Because we have to know which data type to use to encode the offsets, + // we have to go through the values twice: once to encode the data and + // calculate the offsets, then again to encode the offsets using the fitting data type. + var offset = 1; // First offset is always 1. + var offsets = [offset]; + var data = []; + for (var i = 0; i < l.length; i += 1) { + var v = encode.OBJECT(l[i]); + Array.prototype.push.apply(data, v); + offset += v.length; + offsets.push(offset); + } + + if (data.length === 0) { + return [0, 0]; + } + + var encodedOffsets = []; + var offSize = (1 + Math.floor(Math.log(offset) / Math.log(2)) / 8) | 0; + var offsetEncoder = [undefined, encode.BYTE, encode.USHORT, encode.UINT24, encode.ULONG][offSize]; + for (var i$1 = 0; i$1 < offsets.length; i$1 += 1) { + var encodedOffset = offsetEncoder(offsets[i$1]); + Array.prototype.push.apply(encodedOffsets, encodedOffset); + } + + return Array.prototype.concat(encode.Card16(l.length), + encode.OffSize(offSize), + encodedOffsets, + data); + }; + + /** + * @param {Array} + * @returns {number} + */ + sizeOf.INDEX = function(v) { + return encode.INDEX(v).length; + }; + + /** + * Convert an object to a CFF DICT structure. + * The keys should be numeric. + * The values should be objects containing name / type / value. + * @param {Object} m + * @returns {Array} + */ + encode.DICT = function(m) { + var d = []; + var keys = Object.keys(m); + var length = keys.length; + + for (var i = 0; i < length; i += 1) { + // Object.keys() return string keys, but our keys are always numeric. + var k = parseInt(keys[i], 0); + var v = m[k]; + // Value comes before the key. + d = d.concat(encode.OPERAND(v.value, v.type)); + d = d.concat(encode.OPERATOR(k)); + } + + return d; + }; + + /** + * @param {Object} + * @returns {number} + */ + sizeOf.DICT = function(m) { + return encode.DICT(m).length; + }; + + /** + * @param {number} + * @returns {Array} + */ + encode.OPERATOR = function(v) { + if (v < 1200) { + return [v]; + } else { + return [12, v - 1200]; + } + }; + + /** + * @param {Array} v + * @param {string} + * @returns {Array} + */ + encode.OPERAND = function(v, type) { + var d = []; + if (Array.isArray(type)) { + for (var i = 0; i < type.length; i += 1) { + check.argument(v.length === type.length, 'Not enough arguments given for type' + type); + d = d.concat(encode.OPERAND(v[i], type[i])); + } + } else { + if (type === 'SID') { + d = d.concat(encode.NUMBER(v)); + } else if (type === 'offset') { + // We make it easy for ourselves and always encode offsets as + // 4 bytes. This makes offset calculation for the top dict easier. + d = d.concat(encode.NUMBER32(v)); + } else if (type === 'number') { + d = d.concat(encode.NUMBER(v)); + } else if (type === 'real') { + d = d.concat(encode.REAL(v)); + } else { + throw new Error('Unknown operand type ' + type); + // FIXME Add support for booleans + } + } + + return d; + }; + + encode.OP = encode.BYTE; + sizeOf.OP = sizeOf.BYTE; + + // memoize charstring encoding using WeakMap if available + var wmm = typeof WeakMap === 'function' && new WeakMap(); + + /** + * Convert a list of CharString operations to bytes. + * @param {Array} + * @returns {Array} + */ + encode.CHARSTRING = function(ops) { + // See encode.MACSTRING for why we don't do "if (wmm && wmm.has(ops))". + if (wmm) { + var cachedValue = wmm.get(ops); + if (cachedValue !== undefined) { + return cachedValue; + } + } + + var d = []; + var length = ops.length; + + for (var i = 0; i < length; i += 1) { + var op = ops[i]; + d = d.concat(encode[op.type](op.value)); + } + + if (wmm) { + wmm.set(ops, d); + } + + return d; + }; + + /** + * @param {Array} + * @returns {number} + */ + sizeOf.CHARSTRING = function(ops) { + return encode.CHARSTRING(ops).length; + }; + + // Utility functions //////////////////////////////////////////////////////// + + /** + * Convert an object containing name / type / value to bytes. + * @param {Object} + * @returns {Array} + */ + encode.OBJECT = function(v) { + var encodingFunction = encode[v.type]; + check.argument(encodingFunction !== undefined, 'No encoding function for type ' + v.type); + return encodingFunction(v.value); + }; + + /** + * @param {Object} + * @returns {number} + */ + sizeOf.OBJECT = function(v) { + var sizeOfFunction = sizeOf[v.type]; + check.argument(sizeOfFunction !== undefined, 'No sizeOf function for type ' + v.type); + return sizeOfFunction(v.value); + }; + + /** + * Convert a table object to bytes. + * A table contains a list of fields containing the metadata (name, type and default value). + * The table itself has the field values set as attributes. + * @param {opentype.Table} + * @returns {Array} + */ + encode.TABLE = function(table) { + var d = []; + var length = table.fields.length; + var subtables = []; + var subtableOffsets = []; + + for (var i = 0; i < length; i += 1) { + var field = table.fields[i]; + var encodingFunction = encode[field.type]; + check.argument(encodingFunction !== undefined, 'No encoding function for field type ' + field.type + ' (' + field.name + ')'); + var value = table[field.name]; + if (value === undefined) { + value = field.value; + } + + var bytes = encodingFunction(value); + + if (field.type === 'TABLE') { + subtableOffsets.push(d.length); + d = d.concat([0, 0]); + subtables.push(bytes); + } else { + d = d.concat(bytes); + } + } + + for (var i$1 = 0; i$1 < subtables.length; i$1 += 1) { + var o = subtableOffsets[i$1]; + var offset = d.length; + check.argument(offset < 65536, 'Table ' + table.tableName + ' too big.'); + d[o] = offset >> 8; + d[o + 1] = offset & 0xff; + d = d.concat(subtables[i$1]); + } + + return d; + }; + + /** + * @param {opentype.Table} + * @returns {number} + */ + sizeOf.TABLE = function(table) { + var numBytes = 0; + var length = table.fields.length; + + for (var i = 0; i < length; i += 1) { + var field = table.fields[i]; + var sizeOfFunction = sizeOf[field.type]; + check.argument(sizeOfFunction !== undefined, 'No sizeOf function for field type ' + field.type + ' (' + field.name + ')'); + var value = table[field.name]; + if (value === undefined) { + value = field.value; + } + + numBytes += sizeOfFunction(value); + + // Subtables take 2 more bytes for offsets. + if (field.type === 'TABLE') { + numBytes += 2; + } + } + + return numBytes; + }; + + encode.RECORD = encode.TABLE; + sizeOf.RECORD = sizeOf.TABLE; + + // Merge in a list of bytes. + encode.LITERAL = function(v) { + return v; + }; + + sizeOf.LITERAL = function(v) { + return v.length; + }; + + // Table metadata + + /** + * @exports opentype.Table + * @class + * @param {string} tableName + * @param {Array} fields + * @param {Object} options + * @constructor + */ + function Table(tableName, fields, options) { + // For coverage tables with coverage format 2, we do not want to add the coverage data directly to the table object, + // as this will result in wrong encoding order of the coverage data on serialization to bytes. + // The fallback of using the field values directly when not present on the table is handled in types.encode.TABLE() already. + if (fields.length && (fields[0].name !== 'coverageFormat' || fields[0].value === 1)) { + for (var i = 0; i < fields.length; i += 1) { + var field = fields[i]; + this[field.name] = field.value; + } + } + + this.tableName = tableName; + this.fields = fields; + if (options) { + var optionKeys = Object.keys(options); + for (var i$1 = 0; i$1 < optionKeys.length; i$1 += 1) { + var k = optionKeys[i$1]; + var v = options[k]; + if (this[k] !== undefined) { + this[k] = v; + } + } + } + } + + /** + * Encodes the table and returns an array of bytes + * @return {Array} + */ + Table.prototype.encode = function() { + return encode.TABLE(this); + }; + + /** + * Get the size of the table. + * @return {number} + */ + Table.prototype.sizeOf = function() { + return sizeOf.TABLE(this); + }; + + /** + * @private + */ + function ushortList(itemName, list, count) { + if (count === undefined) { + count = list.length; + } + var fields = new Array(list.length + 1); + fields[0] = {name: itemName + 'Count', type: 'USHORT', value: count}; + for (var i = 0; i < list.length; i++) { + fields[i + 1] = {name: itemName + i, type: 'USHORT', value: list[i]}; + } + return fields; + } + + /** + * @private + */ + function tableList(itemName, records, itemCallback) { + var count = records.length; + var fields = new Array(count + 1); + fields[0] = {name: itemName + 'Count', type: 'USHORT', value: count}; + for (var i = 0; i < count; i++) { + fields[i + 1] = {name: itemName + i, type: 'TABLE', value: itemCallback(records[i], i)}; + } + return fields; + } + + /** + * @private + */ + function recordList(itemName, records, itemCallback) { + var count = records.length; + var fields = []; + fields[0] = {name: itemName + 'Count', type: 'USHORT', value: count}; + for (var i = 0; i < count; i++) { + fields = fields.concat(itemCallback(records[i], i)); + } + return fields; + } + + // Common Layout Tables + + /** + * @exports opentype.Coverage + * @class + * @param {opentype.Table} + * @constructor + * @extends opentype.Table + */ + function Coverage(coverageTable) { + if (coverageTable.format === 1) { + Table.call(this, 'coverageTable', + [{name: 'coverageFormat', type: 'USHORT', value: 1}] + .concat(ushortList('glyph', coverageTable.glyphs)) + ); + } else if (coverageTable.format === 2) { + Table.call(this, 'coverageTable', + [{name: 'coverageFormat', type: 'USHORT', value: 2}] + .concat(recordList('rangeRecord', coverageTable.ranges, function(RangeRecord) { + return [ + {name: 'startGlyphID', type: 'USHORT', value: RangeRecord.start}, + {name: 'endGlyphID', type: 'USHORT', value: RangeRecord.end}, + {name: 'startCoverageIndex', type: 'USHORT', value: RangeRecord.index} ]; + })) + ); + } else { + check.assert(false, 'Coverage format must be 1 or 2.'); + } + } + Coverage.prototype = Object.create(Table.prototype); + Coverage.prototype.constructor = Coverage; + + function ScriptList(scriptListTable) { + Table.call(this, 'scriptListTable', + recordList('scriptRecord', scriptListTable, function(scriptRecord, i) { + var script = scriptRecord.script; + var defaultLangSys = script.defaultLangSys; + check.assert(!!defaultLangSys, 'Unable to write GSUB: script ' + scriptRecord.tag + ' has no default language system.'); + return [ + {name: 'scriptTag' + i, type: 'TAG', value: scriptRecord.tag}, + {name: 'script' + i, type: 'TABLE', value: new Table('scriptTable', [ + {name: 'defaultLangSys', type: 'TABLE', value: new Table('defaultLangSys', [ + {name: 'lookupOrder', type: 'USHORT', value: 0}, + {name: 'reqFeatureIndex', type: 'USHORT', value: defaultLangSys.reqFeatureIndex}] + .concat(ushortList('featureIndex', defaultLangSys.featureIndexes)))} + ].concat(recordList('langSys', script.langSysRecords, function(langSysRecord, i) { + var langSys = langSysRecord.langSys; + return [ + {name: 'langSysTag' + i, type: 'TAG', value: langSysRecord.tag}, + {name: 'langSys' + i, type: 'TABLE', value: new Table('langSys', [ + {name: 'lookupOrder', type: 'USHORT', value: 0}, + {name: 'reqFeatureIndex', type: 'USHORT', value: langSys.reqFeatureIndex} + ].concat(ushortList('featureIndex', langSys.featureIndexes)))} + ]; + })))} + ]; + }) + ); + } + ScriptList.prototype = Object.create(Table.prototype); + ScriptList.prototype.constructor = ScriptList; + + /** + * @exports opentype.FeatureList + * @class + * @param {opentype.Table} + * @constructor + * @extends opentype.Table + */ + function FeatureList(featureListTable) { + Table.call(this, 'featureListTable', + recordList('featureRecord', featureListTable, function(featureRecord, i) { + var feature = featureRecord.feature; + return [ + {name: 'featureTag' + i, type: 'TAG', value: featureRecord.tag}, + {name: 'feature' + i, type: 'TABLE', value: new Table('featureTable', [ + {name: 'featureParams', type: 'USHORT', value: feature.featureParams} ].concat(ushortList('lookupListIndex', feature.lookupListIndexes)))} + ]; + }) + ); + } + FeatureList.prototype = Object.create(Table.prototype); + FeatureList.prototype.constructor = FeatureList; + + /** + * @exports opentype.LookupList + * @class + * @param {opentype.Table} + * @param {Object} + * @constructor + * @extends opentype.Table + */ + function LookupList(lookupListTable, subtableMakers) { + Table.call(this, 'lookupListTable', tableList('lookup', lookupListTable, function(lookupTable) { + var subtableCallback = subtableMakers[lookupTable.lookupType]; + check.assert(!!subtableCallback, 'Unable to write GSUB lookup type ' + lookupTable.lookupType + ' tables.'); + return new Table('lookupTable', [ + {name: 'lookupType', type: 'USHORT', value: lookupTable.lookupType}, + {name: 'lookupFlag', type: 'USHORT', value: lookupTable.lookupFlag} + ].concat(tableList('subtable', lookupTable.subtables, subtableCallback))); + })); + } + LookupList.prototype = Object.create(Table.prototype); + LookupList.prototype.constructor = LookupList; + + // Record = same as Table, but inlined (a Table has an offset and its data is further in the stream) + // Don't use offsets inside Records (probable bug), only in Tables. + var table = { + Table: Table, + Record: Table, + Coverage: Coverage, + ScriptList: ScriptList, + FeatureList: FeatureList, + LookupList: LookupList, + ushortList: ushortList, + tableList: tableList, + recordList: recordList, + }; + + // Parsing utility functions + + // Retrieve an unsigned byte from the DataView. + function getByte(dataView, offset) { + return dataView.getUint8(offset); + } + + // Retrieve an unsigned 16-bit short from the DataView. + // The value is stored in big endian. + function getUShort(dataView, offset) { + return dataView.getUint16(offset, false); + } + + // Retrieve a signed 16-bit short from the DataView. + // The value is stored in big endian. + function getShort(dataView, offset) { + return dataView.getInt16(offset, false); + } + + // Retrieve an unsigned 32-bit long from the DataView. + // The value is stored in big endian. + function getULong(dataView, offset) { + return dataView.getUint32(offset, false); + } + + // Retrieve a 32-bit signed fixed-point number (16.16) from the DataView. + // The value is stored in big endian. + function getFixed(dataView, offset) { + var decimal = dataView.getInt16(offset, false); + var fraction = dataView.getUint16(offset + 2, false); + return decimal + fraction / 65535; + } + + // Retrieve a 4-character tag from the DataView. + // Tags are used to identify tables. + function getTag(dataView, offset) { + var tag = ''; + for (var i = offset; i < offset + 4; i += 1) { + tag += String.fromCharCode(dataView.getInt8(i)); + } + + return tag; + } + + // Retrieve an offset from the DataView. + // Offsets are 1 to 4 bytes in length, depending on the offSize argument. + function getOffset(dataView, offset, offSize) { + var v = 0; + for (var i = 0; i < offSize; i += 1) { + v <<= 8; + v += dataView.getUint8(offset + i); + } + + return v; + } + + // Retrieve a number of bytes from start offset to the end offset from the DataView. + function getBytes(dataView, startOffset, endOffset) { + var bytes = []; + for (var i = startOffset; i < endOffset; i += 1) { + bytes.push(dataView.getUint8(i)); + } + + return bytes; + } + + // Convert the list of bytes to a string. + function bytesToString(bytes) { + var s = ''; + for (var i = 0; i < bytes.length; i += 1) { + s += String.fromCharCode(bytes[i]); + } + + return s; + } + + var typeOffsets = { + byte: 1, + uShort: 2, + short: 2, + uLong: 4, + fixed: 4, + longDateTime: 8, + tag: 4 + }; + + // A stateful parser that changes the offset whenever a value is retrieved. + // The data is a DataView. + function Parser(data, offset) { + this.data = data; + this.offset = offset; + this.relativeOffset = 0; + } + + Parser.prototype.parseByte = function() { + var v = this.data.getUint8(this.offset + this.relativeOffset); + this.relativeOffset += 1; + return v; + }; + + Parser.prototype.parseChar = function() { + var v = this.data.getInt8(this.offset + this.relativeOffset); + this.relativeOffset += 1; + return v; + }; + + Parser.prototype.parseCard8 = Parser.prototype.parseByte; + + Parser.prototype.parseUShort = function() { + var v = this.data.getUint16(this.offset + this.relativeOffset); + this.relativeOffset += 2; + return v; + }; + + Parser.prototype.parseCard16 = Parser.prototype.parseUShort; + Parser.prototype.parseSID = Parser.prototype.parseUShort; + Parser.prototype.parseOffset16 = Parser.prototype.parseUShort; + + Parser.prototype.parseShort = function() { + var v = this.data.getInt16(this.offset + this.relativeOffset); + this.relativeOffset += 2; + return v; + }; + + Parser.prototype.parseF2Dot14 = function() { + var v = this.data.getInt16(this.offset + this.relativeOffset) / 16384; + this.relativeOffset += 2; + return v; + }; + + Parser.prototype.parseULong = function() { + var v = getULong(this.data, this.offset + this.relativeOffset); + this.relativeOffset += 4; + return v; + }; + + Parser.prototype.parseOffset32 = Parser.prototype.parseULong; + + Parser.prototype.parseFixed = function() { + var v = getFixed(this.data, this.offset + this.relativeOffset); + this.relativeOffset += 4; + return v; + }; + + Parser.prototype.parseString = function(length) { + var dataView = this.data; + var offset = this.offset + this.relativeOffset; + var string = ''; + this.relativeOffset += length; + for (var i = 0; i < length; i++) { + string += String.fromCharCode(dataView.getUint8(offset + i)); + } + + return string; + }; + + Parser.prototype.parseTag = function() { + return this.parseString(4); + }; + + // LONGDATETIME is a 64-bit integer. + // JavaScript and unix timestamps traditionally use 32 bits, so we + // only take the last 32 bits. + // + Since until 2038 those bits will be filled by zeros we can ignore them. + Parser.prototype.parseLongDateTime = function() { + var v = getULong(this.data, this.offset + this.relativeOffset + 4); + // Subtract seconds between 01/01/1904 and 01/01/1970 + // to convert Apple Mac timestamp to Standard Unix timestamp + v -= 2082844800; + this.relativeOffset += 8; + return v; + }; + + Parser.prototype.parseVersion = function(minorBase) { + var major = getUShort(this.data, this.offset + this.relativeOffset); + + // How to interpret the minor version is very vague in the spec. 0x5000 is 5, 0x1000 is 1 + // Default returns the correct number if minor = 0xN000 where N is 0-9 + // Set minorBase to 1 for tables that use minor = N where N is 0-9 + var minor = getUShort(this.data, this.offset + this.relativeOffset + 2); + this.relativeOffset += 4; + if (minorBase === undefined) { minorBase = 0x1000; } + return major + minor / minorBase / 10; + }; + + Parser.prototype.skip = function(type, amount) { + if (amount === undefined) { + amount = 1; + } + + this.relativeOffset += typeOffsets[type] * amount; + }; + + ///// Parsing lists and records /////////////////////////////// + + // Parse a list of 32 bit unsigned integers. + Parser.prototype.parseULongList = function(count) { + if (count === undefined) { count = this.parseULong(); } + var offsets = new Array(count); + var dataView = this.data; + var offset = this.offset + this.relativeOffset; + for (var i = 0; i < count; i++) { + offsets[i] = dataView.getUint32(offset); + offset += 4; + } + + this.relativeOffset += count * 4; + return offsets; + }; + + // Parse a list of 16 bit unsigned integers. The length of the list can be read on the stream + // or provided as an argument. + Parser.prototype.parseOffset16List = + Parser.prototype.parseUShortList = function(count) { + if (count === undefined) { count = this.parseUShort(); } + var offsets = new Array(count); + var dataView = this.data; + var offset = this.offset + this.relativeOffset; + for (var i = 0; i < count; i++) { + offsets[i] = dataView.getUint16(offset); + offset += 2; + } + + this.relativeOffset += count * 2; + return offsets; + }; + + // Parses a list of 16 bit signed integers. + Parser.prototype.parseShortList = function(count) { + var list = new Array(count); + var dataView = this.data; + var offset = this.offset + this.relativeOffset; + for (var i = 0; i < count; i++) { + list[i] = dataView.getInt16(offset); + offset += 2; + } + + this.relativeOffset += count * 2; + return list; + }; + + // Parses a list of bytes. + Parser.prototype.parseByteList = function(count) { + var list = new Array(count); + var dataView = this.data; + var offset = this.offset + this.relativeOffset; + for (var i = 0; i < count; i++) { + list[i] = dataView.getUint8(offset++); + } + + this.relativeOffset += count; + return list; + }; + + /** + * Parse a list of items. + * Record count is optional, if omitted it is read from the stream. + * itemCallback is one of the Parser methods. + */ + Parser.prototype.parseList = function(count, itemCallback) { + if (!itemCallback) { + itemCallback = count; + count = this.parseUShort(); + } + var list = new Array(count); + for (var i = 0; i < count; i++) { + list[i] = itemCallback.call(this); + } + return list; + }; + + Parser.prototype.parseList32 = function(count, itemCallback) { + if (!itemCallback) { + itemCallback = count; + count = this.parseULong(); + } + var list = new Array(count); + for (var i = 0; i < count; i++) { + list[i] = itemCallback.call(this); + } + return list; + }; + + /** + * Parse a list of records. + * Record count is optional, if omitted it is read from the stream. + * Example of recordDescription: { sequenceIndex: Parser.uShort, lookupListIndex: Parser.uShort } + */ + Parser.prototype.parseRecordList = function(count, recordDescription) { + // If the count argument is absent, read it in the stream. + if (!recordDescription) { + recordDescription = count; + count = this.parseUShort(); + } + var records = new Array(count); + var fields = Object.keys(recordDescription); + for (var i = 0; i < count; i++) { + var rec = {}; + for (var j = 0; j < fields.length; j++) { + var fieldName = fields[j]; + var fieldType = recordDescription[fieldName]; + rec[fieldName] = fieldType.call(this); + } + records[i] = rec; + } + return records; + }; + + Parser.prototype.parseRecordList32 = function(count, recordDescription) { + // If the count argument is absent, read it in the stream. + if (!recordDescription) { + recordDescription = count; + count = this.parseULong(); + } + var records = new Array(count); + var fields = Object.keys(recordDescription); + for (var i = 0; i < count; i++) { + var rec = {}; + for (var j = 0; j < fields.length; j++) { + var fieldName = fields[j]; + var fieldType = recordDescription[fieldName]; + rec[fieldName] = fieldType.call(this); + } + records[i] = rec; + } + return records; + }; + + // Parse a data structure into an object + // Example of description: { sequenceIndex: Parser.uShort, lookupListIndex: Parser.uShort } + Parser.prototype.parseStruct = function(description) { + if (typeof description === 'function') { + return description.call(this); + } else { + var fields = Object.keys(description); + var struct = {}; + for (var j = 0; j < fields.length; j++) { + var fieldName = fields[j]; + var fieldType = description[fieldName]; + struct[fieldName] = fieldType.call(this); + } + return struct; + } + }; + + /** + * Parse a GPOS valueRecord + * https://docs.microsoft.com/en-us/typography/opentype/spec/gpos#value-record + * valueFormat is optional, if omitted it is read from the stream. + */ + Parser.prototype.parseValueRecord = function(valueFormat) { + if (valueFormat === undefined) { + valueFormat = this.parseUShort(); + } + if (valueFormat === 0) { + // valueFormat2 in kerning pairs is most often 0 + // in this case return undefined instead of an empty object, to save space + return; + } + var valueRecord = {}; + + if (valueFormat & 0x0001) { valueRecord.xPlacement = this.parseShort(); } + if (valueFormat & 0x0002) { valueRecord.yPlacement = this.parseShort(); } + if (valueFormat & 0x0004) { valueRecord.xAdvance = this.parseShort(); } + if (valueFormat & 0x0008) { valueRecord.yAdvance = this.parseShort(); } + + // Device table (non-variable font) / VariationIndex table (variable font) not supported + // https://docs.microsoft.com/fr-fr/typography/opentype/spec/chapter2#devVarIdxTbls + if (valueFormat & 0x0010) { valueRecord.xPlaDevice = undefined; this.parseShort(); } + if (valueFormat & 0x0020) { valueRecord.yPlaDevice = undefined; this.parseShort(); } + if (valueFormat & 0x0040) { valueRecord.xAdvDevice = undefined; this.parseShort(); } + if (valueFormat & 0x0080) { valueRecord.yAdvDevice = undefined; this.parseShort(); } + + return valueRecord; + }; + + /** + * Parse a list of GPOS valueRecords + * https://docs.microsoft.com/en-us/typography/opentype/spec/gpos#value-record + * valueFormat and valueCount are read from the stream. + */ + Parser.prototype.parseValueRecordList = function() { + var valueFormat = this.parseUShort(); + var valueCount = this.parseUShort(); + var values = new Array(valueCount); + for (var i = 0; i < valueCount; i++) { + values[i] = this.parseValueRecord(valueFormat); + } + return values; + }; + + Parser.prototype.parsePointer = function(description) { + var structOffset = this.parseOffset16(); + if (structOffset > 0) { + // NULL offset => return undefined + return new Parser(this.data, this.offset + structOffset).parseStruct(description); + } + return undefined; + }; + + Parser.prototype.parsePointer32 = function(description) { + var structOffset = this.parseOffset32(); + if (structOffset > 0) { + // NULL offset => return undefined + return new Parser(this.data, this.offset + structOffset).parseStruct(description); + } + return undefined; + }; + + /** + * Parse a list of offsets to lists of 16-bit integers, + * or a list of offsets to lists of offsets to any kind of items. + * If itemCallback is not provided, a list of list of UShort is assumed. + * If provided, itemCallback is called on each item and must parse the item. + * See examples in tables/gsub.js + */ + Parser.prototype.parseListOfLists = function(itemCallback) { + var offsets = this.parseOffset16List(); + var count = offsets.length; + var relativeOffset = this.relativeOffset; + var list = new Array(count); + for (var i = 0; i < count; i++) { + var start = offsets[i]; + if (start === 0) { + // NULL offset + // Add i as owned property to list. Convenient with assert. + list[i] = undefined; + continue; + } + this.relativeOffset = start; + if (itemCallback) { + var subOffsets = this.parseOffset16List(); + var subList = new Array(subOffsets.length); + for (var j = 0; j < subOffsets.length; j++) { + this.relativeOffset = start + subOffsets[j]; + subList[j] = itemCallback.call(this); + } + list[i] = subList; + } else { + list[i] = this.parseUShortList(); + } + } + this.relativeOffset = relativeOffset; + return list; + }; + + ///// Complex tables parsing ////////////////////////////////// + + // Parse a coverage table in a GSUB, GPOS or GDEF table. + // https://www.microsoft.com/typography/OTSPEC/chapter2.htm + // parser.offset must point to the start of the table containing the coverage. + Parser.prototype.parseCoverage = function() { + var startOffset = this.offset + this.relativeOffset; + var format = this.parseUShort(); + var count = this.parseUShort(); + if (format === 1) { + return { + format: 1, + glyphs: this.parseUShortList(count) + }; + } else if (format === 2) { + var ranges = new Array(count); + for (var i = 0; i < count; i++) { + ranges[i] = { + start: this.parseUShort(), + end: this.parseUShort(), + index: this.parseUShort() + }; + } + return { + format: 2, + ranges: ranges + }; + } + throw new Error('0x' + startOffset.toString(16) + ': Coverage format must be 1 or 2.'); + }; + + // Parse a Class Definition Table in a GSUB, GPOS or GDEF table. + // https://www.microsoft.com/typography/OTSPEC/chapter2.htm + Parser.prototype.parseClassDef = function() { + var startOffset = this.offset + this.relativeOffset; + var format = this.parseUShort(); + if (format === 1) { + return { + format: 1, + startGlyph: this.parseUShort(), + classes: this.parseUShortList() + }; + } else if (format === 2) { + return { + format: 2, + ranges: this.parseRecordList({ + start: Parser.uShort, + end: Parser.uShort, + classId: Parser.uShort + }) + }; + } + throw new Error('0x' + startOffset.toString(16) + ': ClassDef format must be 1 or 2.'); + }; + + ///// Static methods /////////////////////////////////// + // These convenience methods can be used as callbacks and should be called with "this" context set to a Parser instance. + + Parser.list = function(count, itemCallback) { + return function() { + return this.parseList(count, itemCallback); + }; + }; + + Parser.list32 = function(count, itemCallback) { + return function() { + return this.parseList32(count, itemCallback); + }; + }; + + Parser.recordList = function(count, recordDescription) { + return function() { + return this.parseRecordList(count, recordDescription); + }; + }; + + Parser.recordList32 = function(count, recordDescription) { + return function() { + return this.parseRecordList32(count, recordDescription); + }; + }; + + Parser.pointer = function(description) { + return function() { + return this.parsePointer(description); + }; + }; + + Parser.pointer32 = function(description) { + return function() { + return this.parsePointer32(description); + }; + }; + + Parser.tag = Parser.prototype.parseTag; + Parser.byte = Parser.prototype.parseByte; + Parser.uShort = Parser.offset16 = Parser.prototype.parseUShort; + Parser.uShortList = Parser.prototype.parseUShortList; + Parser.uLong = Parser.offset32 = Parser.prototype.parseULong; + Parser.uLongList = Parser.prototype.parseULongList; + Parser.struct = Parser.prototype.parseStruct; + Parser.coverage = Parser.prototype.parseCoverage; + Parser.classDef = Parser.prototype.parseClassDef; + + ///// Script, Feature, Lookup lists /////////////////////////////////////////////// + // https://www.microsoft.com/typography/OTSPEC/chapter2.htm + + var langSysTable = { + reserved: Parser.uShort, + reqFeatureIndex: Parser.uShort, + featureIndexes: Parser.uShortList + }; + + Parser.prototype.parseScriptList = function() { + return this.parsePointer(Parser.recordList({ + tag: Parser.tag, + script: Parser.pointer({ + defaultLangSys: Parser.pointer(langSysTable), + langSysRecords: Parser.recordList({ + tag: Parser.tag, + langSys: Parser.pointer(langSysTable) + }) + }) + })) || []; + }; + + Parser.prototype.parseFeatureList = function() { + return this.parsePointer(Parser.recordList({ + tag: Parser.tag, + feature: Parser.pointer({ + featureParams: Parser.offset16, + lookupListIndexes: Parser.uShortList + }) + })) || []; + }; + + Parser.prototype.parseLookupList = function(lookupTableParsers) { + return this.parsePointer(Parser.list(Parser.pointer(function() { + var lookupType = this.parseUShort(); + check.argument(1 <= lookupType && lookupType <= 9, 'GPOS/GSUB lookup type ' + lookupType + ' unknown.'); + var lookupFlag = this.parseUShort(); + var useMarkFilteringSet = lookupFlag & 0x10; + return { + lookupType: lookupType, + lookupFlag: lookupFlag, + subtables: this.parseList(Parser.pointer(lookupTableParsers[lookupType])), + markFilteringSet: useMarkFilteringSet ? this.parseUShort() : undefined + }; + }))) || []; + }; + + Parser.prototype.parseFeatureVariationsList = function() { + return this.parsePointer32(function() { + var majorVersion = this.parseUShort(); + var minorVersion = this.parseUShort(); + check.argument(majorVersion === 1 && minorVersion < 1, 'GPOS/GSUB feature variations table unknown.'); + var featureVariations = this.parseRecordList32({ + conditionSetOffset: Parser.offset32, + featureTableSubstitutionOffset: Parser.offset32 + }); + return featureVariations; + }) || []; + }; + + var parse = { + getByte: getByte, + getCard8: getByte, + getUShort: getUShort, + getCard16: getUShort, + getShort: getShort, + getULong: getULong, + getFixed: getFixed, + getTag: getTag, + getOffset: getOffset, + getBytes: getBytes, + bytesToString: bytesToString, + Parser: Parser, + }; + + // The `cmap` table stores the mappings from characters to glyphs. + + function parseCmapTableFormat12(cmap, p) { + //Skip reserved. + p.parseUShort(); + + // Length in bytes of the sub-tables. + cmap.length = p.parseULong(); + cmap.language = p.parseULong(); + + var groupCount; + cmap.groupCount = groupCount = p.parseULong(); + cmap.glyphIndexMap = {}; + + for (var i = 0; i < groupCount; i += 1) { + var startCharCode = p.parseULong(); + var endCharCode = p.parseULong(); + var startGlyphId = p.parseULong(); + + for (var c = startCharCode; c <= endCharCode; c += 1) { + cmap.glyphIndexMap[c] = startGlyphId; + startGlyphId++; + } + } + } + + function parseCmapTableFormat4(cmap, p, data, start, offset) { + // Length in bytes of the sub-tables. + cmap.length = p.parseUShort(); + cmap.language = p.parseUShort(); + + // segCount is stored x 2. + var segCount; + cmap.segCount = segCount = p.parseUShort() >> 1; + + // Skip searchRange, entrySelector, rangeShift. + p.skip('uShort', 3); + + // The "unrolled" mapping from character codes to glyph indices. + cmap.glyphIndexMap = {}; + var endCountParser = new parse.Parser(data, start + offset + 14); + var startCountParser = new parse.Parser(data, start + offset + 16 + segCount * 2); + var idDeltaParser = new parse.Parser(data, start + offset + 16 + segCount * 4); + var idRangeOffsetParser = new parse.Parser(data, start + offset + 16 + segCount * 6); + var glyphIndexOffset = start + offset + 16 + segCount * 8; + for (var i = 0; i < segCount - 1; i += 1) { + var glyphIndex = (void 0); + var endCount = endCountParser.parseUShort(); + var startCount = startCountParser.parseUShort(); + var idDelta = idDeltaParser.parseShort(); + var idRangeOffset = idRangeOffsetParser.parseUShort(); + for (var c = startCount; c <= endCount; c += 1) { + if (idRangeOffset !== 0) { + // The idRangeOffset is relative to the current position in the idRangeOffset array. + // Take the current offset in the idRangeOffset array. + glyphIndexOffset = (idRangeOffsetParser.offset + idRangeOffsetParser.relativeOffset - 2); + + // Add the value of the idRangeOffset, which will move us into the glyphIndex array. + glyphIndexOffset += idRangeOffset; + + // Then add the character index of the current segment, multiplied by 2 for USHORTs. + glyphIndexOffset += (c - startCount) * 2; + glyphIndex = parse.getUShort(data, glyphIndexOffset); + if (glyphIndex !== 0) { + glyphIndex = (glyphIndex + idDelta) & 0xFFFF; + } + } else { + glyphIndex = (c + idDelta) & 0xFFFF; + } + + cmap.glyphIndexMap[c] = glyphIndex; + } + } + } + + // Parse the `cmap` table. This table stores the mappings from characters to glyphs. + // There are many available formats, but we only support the Windows format 4 and 12. + // This function returns a `CmapEncoding` object or null if no supported format could be found. + function parseCmapTable(data, start) { + var cmap = {}; + cmap.version = parse.getUShort(data, start); + check.argument(cmap.version === 0, 'cmap table version should be 0.'); + + // The cmap table can contain many sub-tables, each with their own format. + // We're only interested in a "platform 0" (Unicode format) and "platform 3" (Windows format) table. + cmap.numTables = parse.getUShort(data, start + 2); + var offset = -1; + for (var i = cmap.numTables - 1; i >= 0; i -= 1) { + var platformId = parse.getUShort(data, start + 4 + (i * 8)); + var encodingId = parse.getUShort(data, start + 4 + (i * 8) + 2); + if ((platformId === 3 && (encodingId === 0 || encodingId === 1 || encodingId === 10)) || + (platformId === 0 && (encodingId === 0 || encodingId === 1 || encodingId === 2 || encodingId === 3 || encodingId === 4))) { + offset = parse.getULong(data, start + 4 + (i * 8) + 4); + break; + } + } + + if (offset === -1) { + // There is no cmap table in the font that we support. + throw new Error('No valid cmap sub-tables found.'); + } + + var p = new parse.Parser(data, start + offset); + cmap.format = p.parseUShort(); + + if (cmap.format === 12) { + parseCmapTableFormat12(cmap, p); + } else if (cmap.format === 4) { + parseCmapTableFormat4(cmap, p, data, start, offset); + } else { + throw new Error('Only format 4 and 12 cmap tables are supported (found format ' + cmap.format + ').'); + } + + return cmap; + } + + function addSegment(t, code, glyphIndex) { + t.segments.push({ + end: code, + start: code, + delta: -(code - glyphIndex), + offset: 0, + glyphIndex: glyphIndex + }); + } + + function addTerminatorSegment(t) { + t.segments.push({ + end: 0xFFFF, + start: 0xFFFF, + delta: 1, + offset: 0 + }); + } + + // Make cmap table, format 4 by default, 12 if needed only + function makeCmapTable(glyphs) { + // Plan 0 is the base Unicode Plan but emojis, for example are on another plan, and needs cmap 12 format (with 32bit) + var isPlan0Only = true; + var i; + + // Check if we need to add cmap format 12 or if format 4 only is fine + for (i = glyphs.length - 1; i > 0; i -= 1) { + var g = glyphs.get(i); + if (g.unicode > 65535) { + console.log('Adding CMAP format 12 (needed!)'); + isPlan0Only = false; + break; + } + } + + var cmapTable = [ + {name: 'version', type: 'USHORT', value: 0}, + {name: 'numTables', type: 'USHORT', value: isPlan0Only ? 1 : 2}, + + // CMAP 4 header + {name: 'platformID', type: 'USHORT', value: 3}, + {name: 'encodingID', type: 'USHORT', value: 1}, + {name: 'offset', type: 'ULONG', value: isPlan0Only ? 12 : (12 + 8)} + ]; + + if (!isPlan0Only) + { cmapTable = cmapTable.concat([ + // CMAP 12 header + {name: 'cmap12PlatformID', type: 'USHORT', value: 3}, // We encode only for PlatformID = 3 (Windows) because it is supported everywhere + {name: 'cmap12EncodingID', type: 'USHORT', value: 10}, + {name: 'cmap12Offset', type: 'ULONG', value: 0} + ]); } + + cmapTable = cmapTable.concat([ + // CMAP 4 Subtable + {name: 'format', type: 'USHORT', value: 4}, + {name: 'cmap4Length', type: 'USHORT', value: 0}, + {name: 'language', type: 'USHORT', value: 0}, + {name: 'segCountX2', type: 'USHORT', value: 0}, + {name: 'searchRange', type: 'USHORT', value: 0}, + {name: 'entrySelector', type: 'USHORT', value: 0}, + {name: 'rangeShift', type: 'USHORT', value: 0} + ]); + + var t = new table.Table('cmap', cmapTable); + + t.segments = []; + for (i = 0; i < glyphs.length; i += 1) { + var glyph = glyphs.get(i); + for (var j = 0; j < glyph.unicodes.length; j += 1) { + addSegment(t, glyph.unicodes[j], i); + } + + t.segments = t.segments.sort(function (a, b) { + return a.start - b.start; + }); + } + + addTerminatorSegment(t); + + var segCount = t.segments.length; + var segCountToRemove = 0; + + // CMAP 4 + // Set up parallel segment arrays. + var endCounts = []; + var startCounts = []; + var idDeltas = []; + var idRangeOffsets = []; + var glyphIds = []; + + // CMAP 12 + var cmap12Groups = []; + + // Reminder this loop is not following the specification at 100% + // The specification -> find suites of characters and make a group + // Here we're doing one group for each letter + // Doing as the spec can save 8 times (or more) space + for (i = 0; i < segCount; i += 1) { + var segment = t.segments[i]; + + // CMAP 4 + if (segment.end <= 65535 && segment.start <= 65535) { + endCounts = endCounts.concat({name: 'end_' + i, type: 'USHORT', value: segment.end}); + startCounts = startCounts.concat({name: 'start_' + i, type: 'USHORT', value: segment.start}); + idDeltas = idDeltas.concat({name: 'idDelta_' + i, type: 'SHORT', value: segment.delta}); + idRangeOffsets = idRangeOffsets.concat({name: 'idRangeOffset_' + i, type: 'USHORT', value: segment.offset}); + if (segment.glyphId !== undefined) { + glyphIds = glyphIds.concat({name: 'glyph_' + i, type: 'USHORT', value: segment.glyphId}); + } + } else { + // Skip Unicode > 65535 (16bit unsigned max) for CMAP 4, will be added in CMAP 12 + segCountToRemove += 1; + } + + // CMAP 12 + // Skip Terminator Segment + if (!isPlan0Only && segment.glyphIndex !== undefined) { + cmap12Groups = cmap12Groups.concat({name: 'cmap12Start_' + i, type: 'ULONG', value: segment.start}); + cmap12Groups = cmap12Groups.concat({name: 'cmap12End_' + i, type: 'ULONG', value: segment.end}); + cmap12Groups = cmap12Groups.concat({name: 'cmap12Glyph_' + i, type: 'ULONG', value: segment.glyphIndex}); + } + } + + // CMAP 4 Subtable + t.segCountX2 = (segCount - segCountToRemove) * 2; + t.searchRange = Math.pow(2, Math.floor(Math.log((segCount - segCountToRemove)) / Math.log(2))) * 2; + t.entrySelector = Math.log(t.searchRange / 2) / Math.log(2); + t.rangeShift = t.segCountX2 - t.searchRange; + + t.fields = t.fields.concat(endCounts); + t.fields.push({name: 'reservedPad', type: 'USHORT', value: 0}); + t.fields = t.fields.concat(startCounts); + t.fields = t.fields.concat(idDeltas); + t.fields = t.fields.concat(idRangeOffsets); + t.fields = t.fields.concat(glyphIds); + + t.cmap4Length = 14 + // Subtable header + endCounts.length * 2 + + 2 + // reservedPad + startCounts.length * 2 + + idDeltas.length * 2 + + idRangeOffsets.length * 2 + + glyphIds.length * 2; + + if (!isPlan0Only) { + // CMAP 12 Subtable + var cmap12Length = 16 + // Subtable header + cmap12Groups.length * 4; + + t.cmap12Offset = 12 + (2 * 2) + 4 + t.cmap4Length; + t.fields = t.fields.concat([ + {name: 'cmap12Format', type: 'USHORT', value: 12}, + {name: 'cmap12Reserved', type: 'USHORT', value: 0}, + {name: 'cmap12Length', type: 'ULONG', value: cmap12Length}, + {name: 'cmap12Language', type: 'ULONG', value: 0}, + {name: 'cmap12nGroups', type: 'ULONG', value: cmap12Groups.length / 3} + ]); + + t.fields = t.fields.concat(cmap12Groups); + } + + return t; + } + + var cmap = { parse: parseCmapTable, make: makeCmapTable }; + + // Glyph encoding + + var cffStandardStrings = [ + '.notdef', 'space', 'exclam', 'quotedbl', 'numbersign', 'dollar', 'percent', 'ampersand', 'quoteright', + 'parenleft', 'parenright', 'asterisk', 'plus', 'comma', 'hyphen', 'period', 'slash', 'zero', 'one', 'two', + 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine', 'colon', 'semicolon', 'less', 'equal', 'greater', + 'question', 'at', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', + 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'bracketleft', 'backslash', 'bracketright', 'asciicircum', 'underscore', + 'quoteleft', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', + 'u', 'v', 'w', 'x', 'y', 'z', 'braceleft', 'bar', 'braceright', 'asciitilde', 'exclamdown', 'cent', 'sterling', + 'fraction', 'yen', 'florin', 'section', 'currency', 'quotesingle', 'quotedblleft', 'guillemotleft', + 'guilsinglleft', 'guilsinglright', 'fi', 'fl', 'endash', 'dagger', 'daggerdbl', 'periodcentered', 'paragraph', + 'bullet', 'quotesinglbase', 'quotedblbase', 'quotedblright', 'guillemotright', 'ellipsis', 'perthousand', + 'questiondown', 'grave', 'acute', 'circumflex', 'tilde', 'macron', 'breve', 'dotaccent', 'dieresis', 'ring', + 'cedilla', 'hungarumlaut', 'ogonek', 'caron', 'emdash', 'AE', 'ordfeminine', 'Lslash', 'Oslash', 'OE', + 'ordmasculine', 'ae', 'dotlessi', 'lslash', 'oslash', 'oe', 'germandbls', 'onesuperior', 'logicalnot', 'mu', + 'trademark', 'Eth', 'onehalf', 'plusminus', 'Thorn', 'onequarter', 'divide', 'brokenbar', 'degree', 'thorn', + 'threequarters', 'twosuperior', 'registered', 'minus', 'eth', 'multiply', 'threesuperior', 'copyright', + 'Aacute', 'Acircumflex', 'Adieresis', 'Agrave', 'Aring', 'Atilde', 'Ccedilla', 'Eacute', 'Ecircumflex', + 'Edieresis', 'Egrave', 'Iacute', 'Icircumflex', 'Idieresis', 'Igrave', 'Ntilde', 'Oacute', 'Ocircumflex', + 'Odieresis', 'Ograve', 'Otilde', 'Scaron', 'Uacute', 'Ucircumflex', 'Udieresis', 'Ugrave', 'Yacute', + 'Ydieresis', 'Zcaron', 'aacute', 'acircumflex', 'adieresis', 'agrave', 'aring', 'atilde', 'ccedilla', 'eacute', + 'ecircumflex', 'edieresis', 'egrave', 'iacute', 'icircumflex', 'idieresis', 'igrave', 'ntilde', 'oacute', + 'ocircumflex', 'odieresis', 'ograve', 'otilde', 'scaron', 'uacute', 'ucircumflex', 'udieresis', 'ugrave', + 'yacute', 'ydieresis', 'zcaron', 'exclamsmall', 'Hungarumlautsmall', 'dollaroldstyle', 'dollarsuperior', + 'ampersandsmall', 'Acutesmall', 'parenleftsuperior', 'parenrightsuperior', '266 ff', 'onedotenleader', + 'zerooldstyle', 'oneoldstyle', 'twooldstyle', 'threeoldstyle', 'fouroldstyle', 'fiveoldstyle', 'sixoldstyle', + 'sevenoldstyle', 'eightoldstyle', 'nineoldstyle', 'commasuperior', 'threequartersemdash', 'periodsuperior', + 'questionsmall', 'asuperior', 'bsuperior', 'centsuperior', 'dsuperior', 'esuperior', 'isuperior', 'lsuperior', + 'msuperior', 'nsuperior', 'osuperior', 'rsuperior', 'ssuperior', 'tsuperior', 'ff', 'ffi', 'ffl', + 'parenleftinferior', 'parenrightinferior', 'Circumflexsmall', 'hyphensuperior', 'Gravesmall', 'Asmall', + 'Bsmall', 'Csmall', 'Dsmall', 'Esmall', 'Fsmall', 'Gsmall', 'Hsmall', 'Ismall', 'Jsmall', 'Ksmall', 'Lsmall', + 'Msmall', 'Nsmall', 'Osmall', 'Psmall', 'Qsmall', 'Rsmall', 'Ssmall', 'Tsmall', 'Usmall', 'Vsmall', 'Wsmall', + 'Xsmall', 'Ysmall', 'Zsmall', 'colonmonetary', 'onefitted', 'rupiah', 'Tildesmall', 'exclamdownsmall', + 'centoldstyle', 'Lslashsmall', 'Scaronsmall', 'Zcaronsmall', 'Dieresissmall', 'Brevesmall', 'Caronsmall', + 'Dotaccentsmall', 'Macronsmall', 'figuredash', 'hypheninferior', 'Ogoneksmall', 'Ringsmall', 'Cedillasmall', + 'questiondownsmall', 'oneeighth', 'threeeighths', 'fiveeighths', 'seveneighths', 'onethird', 'twothirds', + 'zerosuperior', 'foursuperior', 'fivesuperior', 'sixsuperior', 'sevensuperior', 'eightsuperior', 'ninesuperior', + 'zeroinferior', 'oneinferior', 'twoinferior', 'threeinferior', 'fourinferior', 'fiveinferior', 'sixinferior', + 'seveninferior', 'eightinferior', 'nineinferior', 'centinferior', 'dollarinferior', 'periodinferior', + 'commainferior', 'Agravesmall', 'Aacutesmall', 'Acircumflexsmall', 'Atildesmall', 'Adieresissmall', + 'Aringsmall', 'AEsmall', 'Ccedillasmall', 'Egravesmall', 'Eacutesmall', 'Ecircumflexsmall', 'Edieresissmall', + 'Igravesmall', 'Iacutesmall', 'Icircumflexsmall', 'Idieresissmall', 'Ethsmall', 'Ntildesmall', 'Ogravesmall', + 'Oacutesmall', 'Ocircumflexsmall', 'Otildesmall', 'Odieresissmall', 'OEsmall', 'Oslashsmall', 'Ugravesmall', + 'Uacutesmall', 'Ucircumflexsmall', 'Udieresissmall', 'Yacutesmall', 'Thornsmall', 'Ydieresissmall', '001.000', + '001.001', '001.002', '001.003', 'Black', 'Bold', 'Book', 'Light', 'Medium', 'Regular', 'Roman', 'Semibold']; + + var cffStandardEncoding = [ + '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', + '', '', '', '', 'space', 'exclam', 'quotedbl', 'numbersign', 'dollar', 'percent', 'ampersand', 'quoteright', + 'parenleft', 'parenright', 'asterisk', 'plus', 'comma', 'hyphen', 'period', 'slash', 'zero', 'one', 'two', + 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine', 'colon', 'semicolon', 'less', 'equal', 'greater', + 'question', 'at', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', + 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'bracketleft', 'backslash', 'bracketright', 'asciicircum', 'underscore', + 'quoteleft', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', + 'u', 'v', 'w', 'x', 'y', 'z', 'braceleft', 'bar', 'braceright', 'asciitilde', '', '', '', '', '', '', '', '', + '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', + 'exclamdown', 'cent', 'sterling', 'fraction', 'yen', 'florin', 'section', 'currency', 'quotesingle', + 'quotedblleft', 'guillemotleft', 'guilsinglleft', 'guilsinglright', 'fi', 'fl', '', 'endash', 'dagger', + 'daggerdbl', 'periodcentered', '', 'paragraph', 'bullet', 'quotesinglbase', 'quotedblbase', 'quotedblright', + 'guillemotright', 'ellipsis', 'perthousand', '', 'questiondown', '', 'grave', 'acute', 'circumflex', 'tilde', + 'macron', 'breve', 'dotaccent', 'dieresis', '', 'ring', 'cedilla', '', 'hungarumlaut', 'ogonek', 'caron', + 'emdash', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', 'AE', '', 'ordfeminine', '', '', '', + '', 'Lslash', 'Oslash', 'OE', 'ordmasculine', '', '', '', '', '', 'ae', '', '', '', 'dotlessi', '', '', + 'lslash', 'oslash', 'oe', 'germandbls']; + + var cffExpertEncoding = [ + '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', + '', '', '', '', 'space', 'exclamsmall', 'Hungarumlautsmall', '', 'dollaroldstyle', 'dollarsuperior', + 'ampersandsmall', 'Acutesmall', 'parenleftsuperior', 'parenrightsuperior', 'twodotenleader', 'onedotenleader', + 'comma', 'hyphen', 'period', 'fraction', 'zerooldstyle', 'oneoldstyle', 'twooldstyle', 'threeoldstyle', + 'fouroldstyle', 'fiveoldstyle', 'sixoldstyle', 'sevenoldstyle', 'eightoldstyle', 'nineoldstyle', 'colon', + 'semicolon', 'commasuperior', 'threequartersemdash', 'periodsuperior', 'questionsmall', '', 'asuperior', + 'bsuperior', 'centsuperior', 'dsuperior', 'esuperior', '', '', 'isuperior', '', '', 'lsuperior', 'msuperior', + 'nsuperior', 'osuperior', '', '', 'rsuperior', 'ssuperior', 'tsuperior', '', 'ff', 'fi', 'fl', 'ffi', 'ffl', + 'parenleftinferior', '', 'parenrightinferior', 'Circumflexsmall', 'hyphensuperior', 'Gravesmall', 'Asmall', + 'Bsmall', 'Csmall', 'Dsmall', 'Esmall', 'Fsmall', 'Gsmall', 'Hsmall', 'Ismall', 'Jsmall', 'Ksmall', 'Lsmall', + 'Msmall', 'Nsmall', 'Osmall', 'Psmall', 'Qsmall', 'Rsmall', 'Ssmall', 'Tsmall', 'Usmall', 'Vsmall', 'Wsmall', + 'Xsmall', 'Ysmall', 'Zsmall', 'colonmonetary', 'onefitted', 'rupiah', 'Tildesmall', '', '', '', '', '', '', '', + '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', + 'exclamdownsmall', 'centoldstyle', 'Lslashsmall', '', '', 'Scaronsmall', 'Zcaronsmall', 'Dieresissmall', + 'Brevesmall', 'Caronsmall', '', 'Dotaccentsmall', '', '', 'Macronsmall', '', '', 'figuredash', 'hypheninferior', + '', '', 'Ogoneksmall', 'Ringsmall', 'Cedillasmall', '', '', '', 'onequarter', 'onehalf', 'threequarters', + 'questiondownsmall', 'oneeighth', 'threeeighths', 'fiveeighths', 'seveneighths', 'onethird', 'twothirds', '', + '', 'zerosuperior', 'onesuperior', 'twosuperior', 'threesuperior', 'foursuperior', 'fivesuperior', + 'sixsuperior', 'sevensuperior', 'eightsuperior', 'ninesuperior', 'zeroinferior', 'oneinferior', 'twoinferior', + 'threeinferior', 'fourinferior', 'fiveinferior', 'sixinferior', 'seveninferior', 'eightinferior', + 'nineinferior', 'centinferior', 'dollarinferior', 'periodinferior', 'commainferior', 'Agravesmall', + 'Aacutesmall', 'Acircumflexsmall', 'Atildesmall', 'Adieresissmall', 'Aringsmall', 'AEsmall', 'Ccedillasmall', + 'Egravesmall', 'Eacutesmall', 'Ecircumflexsmall', 'Edieresissmall', 'Igravesmall', 'Iacutesmall', + 'Icircumflexsmall', 'Idieresissmall', 'Ethsmall', 'Ntildesmall', 'Ogravesmall', 'Oacutesmall', + 'Ocircumflexsmall', 'Otildesmall', 'Odieresissmall', 'OEsmall', 'Oslashsmall', 'Ugravesmall', 'Uacutesmall', + 'Ucircumflexsmall', 'Udieresissmall', 'Yacutesmall', 'Thornsmall', 'Ydieresissmall']; + + var standardNames = [ + '.notdef', '.null', 'nonmarkingreturn', 'space', 'exclam', 'quotedbl', 'numbersign', 'dollar', 'percent', + 'ampersand', 'quotesingle', 'parenleft', 'parenright', 'asterisk', 'plus', 'comma', 'hyphen', 'period', 'slash', + 'zero', 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine', 'colon', 'semicolon', 'less', + 'equal', 'greater', 'question', 'at', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', + 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'bracketleft', 'backslash', 'bracketright', + 'asciicircum', 'underscore', 'grave', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', + 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'braceleft', 'bar', 'braceright', 'asciitilde', + 'Adieresis', 'Aring', 'Ccedilla', 'Eacute', 'Ntilde', 'Odieresis', 'Udieresis', 'aacute', 'agrave', + 'acircumflex', 'adieresis', 'atilde', 'aring', 'ccedilla', 'eacute', 'egrave', 'ecircumflex', 'edieresis', + 'iacute', 'igrave', 'icircumflex', 'idieresis', 'ntilde', 'oacute', 'ograve', 'ocircumflex', 'odieresis', + 'otilde', 'uacute', 'ugrave', 'ucircumflex', 'udieresis', 'dagger', 'degree', 'cent', 'sterling', 'section', + 'bullet', 'paragraph', 'germandbls', 'registered', 'copyright', 'trademark', 'acute', 'dieresis', 'notequal', + 'AE', 'Oslash', 'infinity', 'plusminus', 'lessequal', 'greaterequal', 'yen', 'mu', 'partialdiff', 'summation', + 'product', 'pi', 'integral', 'ordfeminine', 'ordmasculine', 'Omega', 'ae', 'oslash', 'questiondown', + 'exclamdown', 'logicalnot', 'radical', 'florin', 'approxequal', 'Delta', 'guillemotleft', 'guillemotright', + 'ellipsis', 'nonbreakingspace', 'Agrave', 'Atilde', 'Otilde', 'OE', 'oe', 'endash', 'emdash', 'quotedblleft', + 'quotedblright', 'quoteleft', 'quoteright', 'divide', 'lozenge', 'ydieresis', 'Ydieresis', 'fraction', + 'currency', 'guilsinglleft', 'guilsinglright', 'fi', 'fl', 'daggerdbl', 'periodcentered', 'quotesinglbase', + 'quotedblbase', 'perthousand', 'Acircumflex', 'Ecircumflex', 'Aacute', 'Edieresis', 'Egrave', 'Iacute', + 'Icircumflex', 'Idieresis', 'Igrave', 'Oacute', 'Ocircumflex', 'apple', 'Ograve', 'Uacute', 'Ucircumflex', + 'Ugrave', 'dotlessi', 'circumflex', 'tilde', 'macron', 'breve', 'dotaccent', 'ring', 'cedilla', 'hungarumlaut', + 'ogonek', 'caron', 'Lslash', 'lslash', 'Scaron', 'scaron', 'Zcaron', 'zcaron', 'brokenbar', 'Eth', 'eth', + 'Yacute', 'yacute', 'Thorn', 'thorn', 'minus', 'multiply', 'onesuperior', 'twosuperior', 'threesuperior', + 'onehalf', 'onequarter', 'threequarters', 'franc', 'Gbreve', 'gbreve', 'Idotaccent', 'Scedilla', 'scedilla', + 'Cacute', 'cacute', 'Ccaron', 'ccaron', 'dcroat']; + + /** + * This is the encoding used for fonts created from scratch. + * It loops through all glyphs and finds the appropriate unicode value. + * Since it's linear time, other encodings will be faster. + * @exports opentype.DefaultEncoding + * @class + * @constructor + * @param {opentype.Font} + */ + function DefaultEncoding(font) { + this.font = font; + } + + DefaultEncoding.prototype.charToGlyphIndex = function(c) { + var code = c.codePointAt(0); + var glyphs = this.font.glyphs; + if (glyphs) { + for (var i = 0; i < glyphs.length; i += 1) { + var glyph = glyphs.get(i); + for (var j = 0; j < glyph.unicodes.length; j += 1) { + if (glyph.unicodes[j] === code) { + return i; + } + } + } + } + return null; + }; + + /** + * @exports opentype.CmapEncoding + * @class + * @constructor + * @param {Object} cmap - a object with the cmap encoded data + */ + function CmapEncoding(cmap) { + this.cmap = cmap; + } + + /** + * @param {string} c - the character + * @return {number} The glyph index. + */ + CmapEncoding.prototype.charToGlyphIndex = function(c) { + return this.cmap.glyphIndexMap[c.codePointAt(0)] || 0; + }; + + /** + * @exports opentype.CffEncoding + * @class + * @constructor + * @param {string} encoding - The encoding + * @param {Array} charset - The character set. + */ + function CffEncoding(encoding, charset) { + this.encoding = encoding; + this.charset = charset; + } + + /** + * @param {string} s - The character + * @return {number} The index. + */ + CffEncoding.prototype.charToGlyphIndex = function(s) { + var code = s.codePointAt(0); + var charName = this.encoding[code]; + return this.charset.indexOf(charName); + }; + + /** + * @exports opentype.GlyphNames + * @class + * @constructor + * @param {Object} post + */ + function GlyphNames(post) { + switch (post.version) { + case 1: + this.names = standardNames.slice(); + break; + case 2: + this.names = new Array(post.numberOfGlyphs); + for (var i = 0; i < post.numberOfGlyphs; i++) { + if (post.glyphNameIndex[i] < standardNames.length) { + this.names[i] = standardNames[post.glyphNameIndex[i]]; + } else { + this.names[i] = post.names[post.glyphNameIndex[i] - standardNames.length]; + } + } + + break; + case 2.5: + this.names = new Array(post.numberOfGlyphs); + for (var i$1 = 0; i$1 < post.numberOfGlyphs; i$1++) { + this.names[i$1] = standardNames[i$1 + post.glyphNameIndex[i$1]]; + } + + break; + case 3: + this.names = []; + break; + default: + this.names = []; + break; + } + } + + /** + * Gets the index of a glyph by name. + * @param {string} name - The glyph name + * @return {number} The index + */ + GlyphNames.prototype.nameToGlyphIndex = function(name) { + return this.names.indexOf(name); + }; + + /** + * @param {number} gid + * @return {string} + */ + GlyphNames.prototype.glyphIndexToName = function(gid) { + return this.names[gid]; + }; + + function addGlyphNamesAll(font) { + var glyph; + var glyphIndexMap = font.tables.cmap.glyphIndexMap; + var charCodes = Object.keys(glyphIndexMap); + + for (var i = 0; i < charCodes.length; i += 1) { + var c = charCodes[i]; + var glyphIndex = glyphIndexMap[c]; + glyph = font.glyphs.get(glyphIndex); + glyph.addUnicode(parseInt(c)); + } + + for (var i$1 = 0; i$1 < font.glyphs.length; i$1 += 1) { + glyph = font.glyphs.get(i$1); + if (font.cffEncoding) { + if (font.isCIDFont) { + glyph.name = 'gid' + i$1; + } else { + glyph.name = font.cffEncoding.charset[i$1]; + } + } else if (font.glyphNames.names) { + glyph.name = font.glyphNames.glyphIndexToName(i$1); + } + } + } + + function addGlyphNamesToUnicodeMap(font) { + font._IndexToUnicodeMap = {}; + + var glyphIndexMap = font.tables.cmap.glyphIndexMap; + var charCodes = Object.keys(glyphIndexMap); + + for (var i = 0; i < charCodes.length; i += 1) { + var c = charCodes[i]; + var glyphIndex = glyphIndexMap[c]; + if (font._IndexToUnicodeMap[glyphIndex] === undefined) { + font._IndexToUnicodeMap[glyphIndex] = { + unicodes: [parseInt(c)] + }; + } else { + font._IndexToUnicodeMap[glyphIndex].unicodes.push(parseInt(c)); + } + } + } + + /** + * @alias opentype.addGlyphNames + * @param {opentype.Font} + * @param {Object} + */ + function addGlyphNames(font, opt) { + if (opt.lowMemory) { + addGlyphNamesToUnicodeMap(font); + } else { + addGlyphNamesAll(font); + } + } + + // Drawing utility functions. + + // Draw a line on the given context from point `x1,y1` to point `x2,y2`. + function line(ctx, x1, y1, x2, y2) { + ctx.beginPath(); + ctx.moveTo(x1, y1); + ctx.lineTo(x2, y2); + ctx.stroke(); + } + + var draw = { line: line }; + + // The Glyph object + // import glyf from './tables/glyf' Can't be imported here, because it's a circular dependency + + function getPathDefinition(glyph, path) { + var _path = path || new Path(); + return { + configurable: true, + + get: function() { + if (typeof _path === 'function') { + _path = _path(); + } + + return _path; + }, + + set: function(p) { + _path = p; + } + }; + } + /** + * @typedef GlyphOptions + * @type Object + * @property {string} [name] - The glyph name + * @property {number} [unicode] + * @property {Array} [unicodes] + * @property {number} [xMin] + * @property {number} [yMin] + * @property {number} [xMax] + * @property {number} [yMax] + * @property {number} [advanceWidth] + */ + + // A Glyph is an individual mark that often corresponds to a character. + // Some glyphs, such as ligatures, are a combination of many characters. + // Glyphs are the basic building blocks of a font. + // + // The `Glyph` class contains utility methods for drawing the path and its points. + /** + * @exports opentype.Glyph + * @class + * @param {GlyphOptions} + * @constructor + */ + function Glyph(options) { + // By putting all the code on a prototype function (which is only declared once) + // we reduce the memory requirements for larger fonts by some 2% + this.bindConstructorValues(options); + } + + /** + * @param {GlyphOptions} + */ + Glyph.prototype.bindConstructorValues = function(options) { + this.index = options.index || 0; + + // These three values cannot be deferred for memory optimization: + this.name = options.name || null; + this.unicode = options.unicode || undefined; + this.unicodes = options.unicodes || options.unicode !== undefined ? [options.unicode] : []; + + // But by binding these values only when necessary, we reduce can + // the memory requirements by almost 3% for larger fonts. + if ('xMin' in options) { + this.xMin = options.xMin; + } + + if ('yMin' in options) { + this.yMin = options.yMin; + } + + if ('xMax' in options) { + this.xMax = options.xMax; + } + + if ('yMax' in options) { + this.yMax = options.yMax; + } + + if ('advanceWidth' in options) { + this.advanceWidth = options.advanceWidth; + } + + // The path for a glyph is the most memory intensive, and is bound as a value + // with a getter/setter to ensure we actually do path parsing only once the + // path is actually needed by anything. + Object.defineProperty(this, 'path', getPathDefinition(this, options.path)); + }; + + /** + * @param {number} + */ + Glyph.prototype.addUnicode = function(unicode) { + if (this.unicodes.length === 0) { + this.unicode = unicode; + } + + this.unicodes.push(unicode); + }; + + /** + * Calculate the minimum bounding box for this glyph. + * @return {opentype.BoundingBox} + */ + Glyph.prototype.getBoundingBox = function() { + return this.path.getBoundingBox(); + }; + + /** + * Convert the glyph to a Path we can draw on a drawing context. + * @param {number} [x=0] - Horizontal position of the beginning of the text. + * @param {number} [y=0] - Vertical position of the *baseline* of the text. + * @param {number} [fontSize=72] - Font size in pixels. We scale the glyph units by `1 / unitsPerEm * fontSize`. + * @param {Object=} options - xScale, yScale to stretch the glyph. + * @param {opentype.Font} if hinting is to be used, the font + * @return {opentype.Path} + */ + Glyph.prototype.getPath = function(x, y, fontSize, options, font) { + x = x !== undefined ? x : 0; + y = y !== undefined ? y : 0; + fontSize = fontSize !== undefined ? fontSize : 72; + var commands; + var hPoints; + if (!options) { options = { }; } + var xScale = options.xScale; + var yScale = options.yScale; + + if (options.hinting && font && font.hinting) { + // in case of hinting, the hinting engine takes care + // of scaling the points (not the path) before hinting. + hPoints = this.path && font.hinting.exec(this, fontSize); + // in case the hinting engine failed hPoints is undefined + // and thus reverts to plain rending + } + + if (hPoints) { + // Call font.hinting.getCommands instead of `glyf.getPath(hPoints).commands` to avoid a circular dependency + commands = font.hinting.getCommands(hPoints); + x = Math.round(x); + y = Math.round(y); + // TODO in case of hinting xyScaling is not yet supported + xScale = yScale = 1; + } else { + commands = this.path.commands; + var scale = 1 / (this.path.unitsPerEm || 1000) * fontSize; + if (xScale === undefined) { xScale = scale; } + if (yScale === undefined) { yScale = scale; } + } + + var p = new Path(); + for (var i = 0; i < commands.length; i += 1) { + var cmd = commands[i]; + if (cmd.type === 'M') { + p.moveTo(x + (cmd.x * xScale), y + (-cmd.y * yScale)); + } else if (cmd.type === 'L') { + p.lineTo(x + (cmd.x * xScale), y + (-cmd.y * yScale)); + } else if (cmd.type === 'Q') { + p.quadraticCurveTo(x + (cmd.x1 * xScale), y + (-cmd.y1 * yScale), + x + (cmd.x * xScale), y + (-cmd.y * yScale)); + } else if (cmd.type === 'C') { + p.curveTo(x + (cmd.x1 * xScale), y + (-cmd.y1 * yScale), + x + (cmd.x2 * xScale), y + (-cmd.y2 * yScale), + x + (cmd.x * xScale), y + (-cmd.y * yScale)); + } else if (cmd.type === 'Z') { + p.closePath(); + } + } + + return p; + }; + + /** + * Split the glyph into contours. + * This function is here for backwards compatibility, and to + * provide raw access to the TrueType glyph outlines. + * @return {Array} + */ + Glyph.prototype.getContours = function() { + if (this.points === undefined) { + return []; + } + + var contours = []; + var currentContour = []; + for (var i = 0; i < this.points.length; i += 1) { + var pt = this.points[i]; + currentContour.push(pt); + if (pt.lastPointOfContour) { + contours.push(currentContour); + currentContour = []; + } + } + + check.argument(currentContour.length === 0, 'There are still points left in the current contour.'); + return contours; + }; + + /** + * Calculate the xMin/yMin/xMax/yMax/lsb/rsb for a Glyph. + * @return {Object} + */ + Glyph.prototype.getMetrics = function() { + var commands = this.path.commands; + var xCoords = []; + var yCoords = []; + for (var i = 0; i < commands.length; i += 1) { + var cmd = commands[i]; + if (cmd.type !== 'Z') { + xCoords.push(cmd.x); + yCoords.push(cmd.y); + } + + if (cmd.type === 'Q' || cmd.type === 'C') { + xCoords.push(cmd.x1); + yCoords.push(cmd.y1); + } + + if (cmd.type === 'C') { + xCoords.push(cmd.x2); + yCoords.push(cmd.y2); + } + } + + var metrics = { + xMin: Math.min.apply(null, xCoords), + yMin: Math.min.apply(null, yCoords), + xMax: Math.max.apply(null, xCoords), + yMax: Math.max.apply(null, yCoords), + leftSideBearing: this.leftSideBearing + }; + + if (!isFinite(metrics.xMin)) { + metrics.xMin = 0; + } + + if (!isFinite(metrics.xMax)) { + metrics.xMax = this.advanceWidth; + } + + if (!isFinite(metrics.yMin)) { + metrics.yMin = 0; + } + + if (!isFinite(metrics.yMax)) { + metrics.yMax = 0; + } + + metrics.rightSideBearing = this.advanceWidth - metrics.leftSideBearing - (metrics.xMax - metrics.xMin); + return metrics; + }; + + /** + * Draw the glyph on the given context. + * @param {CanvasRenderingContext2D} ctx - A 2D drawing context, like Canvas. + * @param {number} [x=0] - Horizontal position of the beginning of the text. + * @param {number} [y=0] - Vertical position of the *baseline* of the text. + * @param {number} [fontSize=72] - Font size in pixels. We scale the glyph units by `1 / unitsPerEm * fontSize`. + * @param {Object=} options - xScale, yScale to stretch the glyph. + */ + Glyph.prototype.draw = function(ctx, x, y, fontSize, options) { + this.getPath(x, y, fontSize, options).draw(ctx); + }; + + /** + * Draw the points of the glyph. + * On-curve points will be drawn in blue, off-curve points will be drawn in red. + * @param {CanvasRenderingContext2D} ctx - A 2D drawing context, like Canvas. + * @param {number} [x=0] - Horizontal position of the beginning of the text. + * @param {number} [y=0] - Vertical position of the *baseline* of the text. + * @param {number} [fontSize=72] - Font size in pixels. We scale the glyph units by `1 / unitsPerEm * fontSize`. + */ + Glyph.prototype.drawPoints = function(ctx, x, y, fontSize) { + function drawCircles(l, x, y, scale) { + ctx.beginPath(); + for (var j = 0; j < l.length; j += 1) { + ctx.moveTo(x + (l[j].x * scale), y + (l[j].y * scale)); + ctx.arc(x + (l[j].x * scale), y + (l[j].y * scale), 2, 0, Math.PI * 2, false); + } + + ctx.closePath(); + ctx.fill(); + } + + x = x !== undefined ? x : 0; + y = y !== undefined ? y : 0; + fontSize = fontSize !== undefined ? fontSize : 24; + var scale = 1 / this.path.unitsPerEm * fontSize; + + var blueCircles = []; + var redCircles = []; + var path = this.path; + for (var i = 0; i < path.commands.length; i += 1) { + var cmd = path.commands[i]; + if (cmd.x !== undefined) { + blueCircles.push({x: cmd.x, y: -cmd.y}); + } + + if (cmd.x1 !== undefined) { + redCircles.push({x: cmd.x1, y: -cmd.y1}); + } + + if (cmd.x2 !== undefined) { + redCircles.push({x: cmd.x2, y: -cmd.y2}); + } + } + + ctx.fillStyle = 'blue'; + drawCircles(blueCircles, x, y, scale); + ctx.fillStyle = 'red'; + drawCircles(redCircles, x, y, scale); + }; + + /** + * Draw lines indicating important font measurements. + * Black lines indicate the origin of the coordinate system (point 0,0). + * Blue lines indicate the glyph bounding box. + * Green line indicates the advance width of the glyph. + * @param {CanvasRenderingContext2D} ctx - A 2D drawing context, like Canvas. + * @param {number} [x=0] - Horizontal position of the beginning of the text. + * @param {number} [y=0] - Vertical position of the *baseline* of the text. + * @param {number} [fontSize=72] - Font size in pixels. We scale the glyph units by `1 / unitsPerEm * fontSize`. + */ + Glyph.prototype.drawMetrics = function(ctx, x, y, fontSize) { + var scale; + x = x !== undefined ? x : 0; + y = y !== undefined ? y : 0; + fontSize = fontSize !== undefined ? fontSize : 24; + scale = 1 / this.path.unitsPerEm * fontSize; + ctx.lineWidth = 1; + + // Draw the origin + ctx.strokeStyle = 'black'; + draw.line(ctx, x, -10000, x, 10000); + draw.line(ctx, -10000, y, 10000, y); + + // This code is here due to memory optimization: by not using + // defaults in the constructor, we save a notable amount of memory. + var xMin = this.xMin || 0; + var yMin = this.yMin || 0; + var xMax = this.xMax || 0; + var yMax = this.yMax || 0; + var advanceWidth = this.advanceWidth || 0; + + // Draw the glyph box + ctx.strokeStyle = 'blue'; + draw.line(ctx, x + (xMin * scale), -10000, x + (xMin * scale), 10000); + draw.line(ctx, x + (xMax * scale), -10000, x + (xMax * scale), 10000); + draw.line(ctx, -10000, y + (-yMin * scale), 10000, y + (-yMin * scale)); + draw.line(ctx, -10000, y + (-yMax * scale), 10000, y + (-yMax * scale)); + + // Draw the advance width + ctx.strokeStyle = 'green'; + draw.line(ctx, x + (advanceWidth * scale), -10000, x + (advanceWidth * scale), 10000); + }; + + // The GlyphSet object + + // Define a property on the glyph that depends on the path being loaded. + function defineDependentProperty(glyph, externalName, internalName) { + Object.defineProperty(glyph, externalName, { + get: function() { + // Request the path property to make sure the path is loaded. + glyph.path; // jshint ignore:line + return glyph[internalName]; + }, + set: function(newValue) { + glyph[internalName] = newValue; + }, + enumerable: true, + configurable: true + }); + } + + /** + * A GlyphSet represents all glyphs available in the font, but modelled using + * a deferred glyph loader, for retrieving glyphs only once they are absolutely + * necessary, to keep the memory footprint down. + * @exports opentype.GlyphSet + * @class + * @param {opentype.Font} + * @param {Array} + */ + function GlyphSet(font, glyphs) { + this.font = font; + this.glyphs = {}; + if (Array.isArray(glyphs)) { + for (var i = 0; i < glyphs.length; i++) { + var glyph = glyphs[i]; + glyph.path.unitsPerEm = font.unitsPerEm; + this.glyphs[i] = glyph; + } + } + + this.length = (glyphs && glyphs.length) || 0; + } + + /** + * @param {number} index + * @return {opentype.Glyph} + */ + GlyphSet.prototype.get = function(index) { + // this.glyphs[index] is 'undefined' when low memory mode is on. glyph is pushed on request only. + if (this.glyphs[index] === undefined) { + this.font._push(index); + if (typeof this.glyphs[index] === 'function') { + this.glyphs[index] = this.glyphs[index](); + } + + var glyph = this.glyphs[index]; + var unicodeObj = this.font._IndexToUnicodeMap[index]; + + if (unicodeObj) { + for (var j = 0; j < unicodeObj.unicodes.length; j++) + { glyph.addUnicode(unicodeObj.unicodes[j]); } + } + + if (this.font.cffEncoding) { + if (this.font.isCIDFont) { + glyph.name = 'gid' + index; + } else { + glyph.name = this.font.cffEncoding.charset[index]; + } + } else if (this.font.glyphNames.names) { + glyph.name = this.font.glyphNames.glyphIndexToName(index); + } + + this.glyphs[index].advanceWidth = this.font._hmtxTableData[index].advanceWidth; + this.glyphs[index].leftSideBearing = this.font._hmtxTableData[index].leftSideBearing; + } else { + if (typeof this.glyphs[index] === 'function') { + this.glyphs[index] = this.glyphs[index](); + } + } + + return this.glyphs[index]; + }; + + /** + * @param {number} index + * @param {Object} + */ + GlyphSet.prototype.push = function(index, loader) { + this.glyphs[index] = loader; + this.length++; + }; + + /** + * @alias opentype.glyphLoader + * @param {opentype.Font} font + * @param {number} index + * @return {opentype.Glyph} + */ + function glyphLoader(font, index) { + return new Glyph({index: index, font: font}); + } + + /** + * Generate a stub glyph that can be filled with all metadata *except* + * the "points" and "path" properties, which must be loaded only once + * the glyph's path is actually requested for text shaping. + * @alias opentype.ttfGlyphLoader + * @param {opentype.Font} font + * @param {number} index + * @param {Function} parseGlyph + * @param {Object} data + * @param {number} position + * @param {Function} buildPath + * @return {opentype.Glyph} + */ + function ttfGlyphLoader(font, index, parseGlyph, data, position, buildPath) { + return function() { + var glyph = new Glyph({index: index, font: font}); + + glyph.path = function() { + parseGlyph(glyph, data, position); + var path = buildPath(font.glyphs, glyph); + path.unitsPerEm = font.unitsPerEm; + return path; + }; + + defineDependentProperty(glyph, 'xMin', '_xMin'); + defineDependentProperty(glyph, 'xMax', '_xMax'); + defineDependentProperty(glyph, 'yMin', '_yMin'); + defineDependentProperty(glyph, 'yMax', '_yMax'); + + return glyph; + }; + } + /** + * @alias opentype.cffGlyphLoader + * @param {opentype.Font} font + * @param {number} index + * @param {Function} parseCFFCharstring + * @param {string} charstring + * @return {opentype.Glyph} + */ + function cffGlyphLoader(font, index, parseCFFCharstring, charstring) { + return function() { + var glyph = new Glyph({index: index, font: font}); + + glyph.path = function() { + var path = parseCFFCharstring(font, glyph, charstring); + path.unitsPerEm = font.unitsPerEm; + return path; + }; + + return glyph; + }; + } + + var glyphset = { GlyphSet: GlyphSet, glyphLoader: glyphLoader, ttfGlyphLoader: ttfGlyphLoader, cffGlyphLoader: cffGlyphLoader }; + + // The `CFF` table contains the glyph outlines in PostScript format. + + // Custom equals function that can also check lists. + function equals(a, b) { + if (a === b) { + return true; + } else if (Array.isArray(a) && Array.isArray(b)) { + if (a.length !== b.length) { + return false; + } + + for (var i = 0; i < a.length; i += 1) { + if (!equals(a[i], b[i])) { + return false; + } + } + + return true; + } else { + return false; + } + } + + // Subroutines are encoded using the negative half of the number space. + // See type 2 chapter 4.7 "Subroutine operators". + function calcCFFSubroutineBias(subrs) { + var bias; + if (subrs.length < 1240) { + bias = 107; + } else if (subrs.length < 33900) { + bias = 1131; + } else { + bias = 32768; + } + + return bias; + } + + // Parse a `CFF` INDEX array. + // An index array consists of a list of offsets, then a list of objects at those offsets. + function parseCFFIndex(data, start, conversionFn) { + var offsets = []; + var objects = []; + var count = parse.getCard16(data, start); + var objectOffset; + var endOffset; + if (count !== 0) { + var offsetSize = parse.getByte(data, start + 2); + objectOffset = start + ((count + 1) * offsetSize) + 2; + var pos = start + 3; + for (var i = 0; i < count + 1; i += 1) { + offsets.push(parse.getOffset(data, pos, offsetSize)); + pos += offsetSize; + } + + // The total size of the index array is 4 header bytes + the value of the last offset. + endOffset = objectOffset + offsets[count]; + } else { + endOffset = start + 2; + } + + for (var i$1 = 0; i$1 < offsets.length - 1; i$1 += 1) { + var value = parse.getBytes(data, objectOffset + offsets[i$1], objectOffset + offsets[i$1 + 1]); + if (conversionFn) { + value = conversionFn(value); + } + + objects.push(value); + } + + return {objects: objects, startOffset: start, endOffset: endOffset}; + } + + function parseCFFIndexLowMemory(data, start) { + var offsets = []; + var count = parse.getCard16(data, start); + var objectOffset; + var endOffset; + if (count !== 0) { + var offsetSize = parse.getByte(data, start + 2); + objectOffset = start + ((count + 1) * offsetSize) + 2; + var pos = start + 3; + for (var i = 0; i < count + 1; i += 1) { + offsets.push(parse.getOffset(data, pos, offsetSize)); + pos += offsetSize; + } + + // The total size of the index array is 4 header bytes + the value of the last offset. + endOffset = objectOffset + offsets[count]; + } else { + endOffset = start + 2; + } + + return {offsets: offsets, startOffset: start, endOffset: endOffset}; + } + function getCffIndexObject(i, offsets, data, start, conversionFn) { + var count = parse.getCard16(data, start); + var objectOffset = 0; + if (count !== 0) { + var offsetSize = parse.getByte(data, start + 2); + objectOffset = start + ((count + 1) * offsetSize) + 2; + } + + var value = parse.getBytes(data, objectOffset + offsets[i], objectOffset + offsets[i + 1]); + if (conversionFn) { + value = conversionFn(value); + } + return value; + } + + // Parse a `CFF` DICT real value. + function parseFloatOperand(parser) { + var s = ''; + var eof = 15; + var lookup = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '.', 'E', 'E-', null, '-']; + while (true) { + var b = parser.parseByte(); + var n1 = b >> 4; + var n2 = b & 15; + + if (n1 === eof) { + break; + } + + s += lookup[n1]; + + if (n2 === eof) { + break; + } + + s += lookup[n2]; + } + + return parseFloat(s); + } + + // Parse a `CFF` DICT operand. + function parseOperand(parser, b0) { + var b1; + var b2; + var b3; + var b4; + if (b0 === 28) { + b1 = parser.parseByte(); + b2 = parser.parseByte(); + return b1 << 8 | b2; + } + + if (b0 === 29) { + b1 = parser.parseByte(); + b2 = parser.parseByte(); + b3 = parser.parseByte(); + b4 = parser.parseByte(); + return b1 << 24 | b2 << 16 | b3 << 8 | b4; + } + + if (b0 === 30) { + return parseFloatOperand(parser); + } + + if (b0 >= 32 && b0 <= 246) { + return b0 - 139; + } + + if (b0 >= 247 && b0 <= 250) { + b1 = parser.parseByte(); + return (b0 - 247) * 256 + b1 + 108; + } + + if (b0 >= 251 && b0 <= 254) { + b1 = parser.parseByte(); + return -(b0 - 251) * 256 - b1 - 108; + } + + throw new Error('Invalid b0 ' + b0); + } + + // Convert the entries returned by `parseDict` to a proper dictionary. + // If a value is a list of one, it is unpacked. + function entriesToObject(entries) { + var o = {}; + for (var i = 0; i < entries.length; i += 1) { + var key = entries[i][0]; + var values = entries[i][1]; + var value = (void 0); + if (values.length === 1) { + value = values[0]; + } else { + value = values; + } + + if (o.hasOwnProperty(key) && !isNaN(o[key])) { + throw new Error('Object ' + o + ' already has key ' + key); + } + + o[key] = value; + } + + return o; + } + + // Parse a `CFF` DICT object. + // A dictionary contains key-value pairs in a compact tokenized format. + function parseCFFDict(data, start, size) { + start = start !== undefined ? start : 0; + var parser = new parse.Parser(data, start); + var entries = []; + var operands = []; + size = size !== undefined ? size : data.length; + + while (parser.relativeOffset < size) { + var op = parser.parseByte(); + + // The first byte for each dict item distinguishes between operator (key) and operand (value). + // Values <= 21 are operators. + if (op <= 21) { + // Two-byte operators have an initial escape byte of 12. + if (op === 12) { + op = 1200 + parser.parseByte(); + } + + entries.push([op, operands]); + operands = []; + } else { + // Since the operands (values) come before the operators (keys), we store all operands in a list + // until we encounter an operator. + operands.push(parseOperand(parser, op)); + } + } + + return entriesToObject(entries); + } + + // Given a String Index (SID), return the value of the string. + // Strings below index 392 are standard CFF strings and are not encoded in the font. + function getCFFString(strings, index) { + if (index <= 390) { + index = cffStandardStrings[index]; + } else { + index = strings[index - 391]; + } + + return index; + } + + // Interpret a dictionary and return a new dictionary with readable keys and values for missing entries. + // This function takes `meta` which is a list of objects containing `operand`, `name` and `default`. + function interpretDict(dict, meta, strings) { + var newDict = {}; + var value; + + // Because we also want to include missing values, we start out from the meta list + // and lookup values in the dict. + for (var i = 0; i < meta.length; i += 1) { + var m = meta[i]; + + if (Array.isArray(m.type)) { + var values = []; + values.length = m.type.length; + for (var j = 0; j < m.type.length; j++) { + value = dict[m.op] !== undefined ? dict[m.op][j] : undefined; + if (value === undefined) { + value = m.value !== undefined && m.value[j] !== undefined ? m.value[j] : null; + } + if (m.type[j] === 'SID') { + value = getCFFString(strings, value); + } + values[j] = value; + } + newDict[m.name] = values; + } else { + value = dict[m.op]; + if (value === undefined) { + value = m.value !== undefined ? m.value : null; + } + + if (m.type === 'SID') { + value = getCFFString(strings, value); + } + newDict[m.name] = value; + } + } + + return newDict; + } + + // Parse the CFF header. + function parseCFFHeader(data, start) { + var header = {}; + header.formatMajor = parse.getCard8(data, start); + header.formatMinor = parse.getCard8(data, start + 1); + header.size = parse.getCard8(data, start + 2); + header.offsetSize = parse.getCard8(data, start + 3); + header.startOffset = start; + header.endOffset = start + 4; + return header; + } + + var TOP_DICT_META = [ + {name: 'version', op: 0, type: 'SID'}, + {name: 'notice', op: 1, type: 'SID'}, + {name: 'copyright', op: 1200, type: 'SID'}, + {name: 'fullName', op: 2, type: 'SID'}, + {name: 'familyName', op: 3, type: 'SID'}, + {name: 'weight', op: 4, type: 'SID'}, + {name: 'isFixedPitch', op: 1201, type: 'number', value: 0}, + {name: 'italicAngle', op: 1202, type: 'number', value: 0}, + {name: 'underlinePosition', op: 1203, type: 'number', value: -100}, + {name: 'underlineThickness', op: 1204, type: 'number', value: 50}, + {name: 'paintType', op: 1205, type: 'number', value: 0}, + {name: 'charstringType', op: 1206, type: 'number', value: 2}, + { + name: 'fontMatrix', + op: 1207, + type: ['real', 'real', 'real', 'real', 'real', 'real'], + value: [0.001, 0, 0, 0.001, 0, 0] + }, + {name: 'uniqueId', op: 13, type: 'number'}, + {name: 'fontBBox', op: 5, type: ['number', 'number', 'number', 'number'], value: [0, 0, 0, 0]}, + {name: 'strokeWidth', op: 1208, type: 'number', value: 0}, + {name: 'xuid', op: 14, type: [], value: null}, + {name: 'charset', op: 15, type: 'offset', value: 0}, + {name: 'encoding', op: 16, type: 'offset', value: 0}, + {name: 'charStrings', op: 17, type: 'offset', value: 0}, + {name: 'private', op: 18, type: ['number', 'offset'], value: [0, 0]}, + {name: 'ros', op: 1230, type: ['SID', 'SID', 'number']}, + {name: 'cidFontVersion', op: 1231, type: 'number', value: 0}, + {name: 'cidFontRevision', op: 1232, type: 'number', value: 0}, + {name: 'cidFontType', op: 1233, type: 'number', value: 0}, + {name: 'cidCount', op: 1234, type: 'number', value: 8720}, + {name: 'uidBase', op: 1235, type: 'number'}, + {name: 'fdArray', op: 1236, type: 'offset'}, + {name: 'fdSelect', op: 1237, type: 'offset'}, + {name: 'fontName', op: 1238, type: 'SID'} + ]; + + var PRIVATE_DICT_META = [ + {name: 'subrs', op: 19, type: 'offset', value: 0}, + {name: 'defaultWidthX', op: 20, type: 'number', value: 0}, + {name: 'nominalWidthX', op: 21, type: 'number', value: 0} + ]; + + // Parse the CFF top dictionary. A CFF table can contain multiple fonts, each with their own top dictionary. + // The top dictionary contains the essential metadata for the font, together with the private dictionary. + function parseCFFTopDict(data, strings) { + var dict = parseCFFDict(data, 0, data.byteLength); + return interpretDict(dict, TOP_DICT_META, strings); + } + + // Parse the CFF private dictionary. We don't fully parse out all the values, only the ones we need. + function parseCFFPrivateDict(data, start, size, strings) { + var dict = parseCFFDict(data, start, size); + return interpretDict(dict, PRIVATE_DICT_META, strings); + } + + // Returns a list of "Top DICT"s found using an INDEX list. + // Used to read both the usual high-level Top DICTs and also the FDArray + // discovered inside CID-keyed fonts. When a Top DICT has a reference to + // a Private DICT that is read and saved into the Top DICT. + // + // In addition to the expected/optional values as outlined in TOP_DICT_META + // the following values might be saved into the Top DICT. + // + // _subrs [] array of local CFF subroutines from Private DICT + // _subrsBias bias value computed from number of subroutines + // (see calcCFFSubroutineBias() and parseCFFCharstring()) + // _defaultWidthX default widths for CFF characters + // _nominalWidthX bias added to width embedded within glyph description + // + // _privateDict saved copy of parsed Private DICT from Top DICT + function gatherCFFTopDicts(data, start, cffIndex, strings) { + var topDictArray = []; + for (var iTopDict = 0; iTopDict < cffIndex.length; iTopDict += 1) { + var topDictData = new DataView(new Uint8Array(cffIndex[iTopDict]).buffer); + var topDict = parseCFFTopDict(topDictData, strings); + topDict._subrs = []; + topDict._subrsBias = 0; + topDict._defaultWidthX = 0; + topDict._nominalWidthX = 0; + var privateSize = topDict.private[0]; + var privateOffset = topDict.private[1]; + if (privateSize !== 0 && privateOffset !== 0) { + var privateDict = parseCFFPrivateDict(data, privateOffset + start, privateSize, strings); + topDict._defaultWidthX = privateDict.defaultWidthX; + topDict._nominalWidthX = privateDict.nominalWidthX; + if (privateDict.subrs !== 0) { + var subrOffset = privateOffset + privateDict.subrs; + var subrIndex = parseCFFIndex(data, subrOffset + start); + topDict._subrs = subrIndex.objects; + topDict._subrsBias = calcCFFSubroutineBias(topDict._subrs); + } + topDict._privateDict = privateDict; + } + topDictArray.push(topDict); + } + return topDictArray; + } + + // Parse the CFF charset table, which contains internal names for all the glyphs. + // This function will return a list of glyph names. + // See Adobe TN #5176 chapter 13, "Charsets". + function parseCFFCharset(data, start, nGlyphs, strings) { + var sid; + var count; + var parser = new parse.Parser(data, start); + + // The .notdef glyph is not included, so subtract 1. + nGlyphs -= 1; + var charset = ['.notdef']; + + var format = parser.parseCard8(); + if (format === 0) { + for (var i = 0; i < nGlyphs; i += 1) { + sid = parser.parseSID(); + charset.push(getCFFString(strings, sid)); + } + } else if (format === 1) { + while (charset.length <= nGlyphs) { + sid = parser.parseSID(); + count = parser.parseCard8(); + for (var i$1 = 0; i$1 <= count; i$1 += 1) { + charset.push(getCFFString(strings, sid)); + sid += 1; + } + } + } else if (format === 2) { + while (charset.length <= nGlyphs) { + sid = parser.parseSID(); + count = parser.parseCard16(); + for (var i$2 = 0; i$2 <= count; i$2 += 1) { + charset.push(getCFFString(strings, sid)); + sid += 1; + } + } + } else { + throw new Error('Unknown charset format ' + format); + } + + return charset; + } + + // Parse the CFF encoding data. Only one encoding can be specified per font. + // See Adobe TN #5176 chapter 12, "Encodings". + function parseCFFEncoding(data, start, charset) { + var code; + var enc = {}; + var parser = new parse.Parser(data, start); + var format = parser.parseCard8(); + if (format === 0) { + var nCodes = parser.parseCard8(); + for (var i = 0; i < nCodes; i += 1) { + code = parser.parseCard8(); + enc[code] = i; + } + } else if (format === 1) { + var nRanges = parser.parseCard8(); + code = 1; + for (var i$1 = 0; i$1 < nRanges; i$1 += 1) { + var first = parser.parseCard8(); + var nLeft = parser.parseCard8(); + for (var j = first; j <= first + nLeft; j += 1) { + enc[j] = code; + code += 1; + } + } + } else { + throw new Error('Unknown encoding format ' + format); + } + + return new CffEncoding(enc, charset); + } + + // Take in charstring code and return a Glyph object. + // The encoding is described in the Type 2 Charstring Format + // https://www.microsoft.com/typography/OTSPEC/charstr2.htm + function parseCFFCharstring(font, glyph, code) { + var c1x; + var c1y; + var c2x; + var c2y; + var p = new Path(); + var stack = []; + var nStems = 0; + var haveWidth = false; + var open = false; + var x = 0; + var y = 0; + var subrs; + var subrsBias; + var defaultWidthX; + var nominalWidthX; + if (font.isCIDFont) { + var fdIndex = font.tables.cff.topDict._fdSelect[glyph.index]; + var fdDict = font.tables.cff.topDict._fdArray[fdIndex]; + subrs = fdDict._subrs; + subrsBias = fdDict._subrsBias; + defaultWidthX = fdDict._defaultWidthX; + nominalWidthX = fdDict._nominalWidthX; + } else { + subrs = font.tables.cff.topDict._subrs; + subrsBias = font.tables.cff.topDict._subrsBias; + defaultWidthX = font.tables.cff.topDict._defaultWidthX; + nominalWidthX = font.tables.cff.topDict._nominalWidthX; + } + var width = defaultWidthX; + + function newContour(x, y) { + if (open) { + p.closePath(); + } + + p.moveTo(x, y); + open = true; + } + + function parseStems() { + var hasWidthArg; + + // The number of stem operators on the stack is always even. + // If the value is uneven, that means a width is specified. + hasWidthArg = stack.length % 2 !== 0; + if (hasWidthArg && !haveWidth) { + width = stack.shift() + nominalWidthX; + } + + nStems += stack.length >> 1; + stack.length = 0; + haveWidth = true; + } + + function parse(code) { + var b1; + var b2; + var b3; + var b4; + var codeIndex; + var subrCode; + var jpx; + var jpy; + var c3x; + var c3y; + var c4x; + var c4y; + + var i = 0; + while (i < code.length) { + var v = code[i]; + i += 1; + switch (v) { + case 1: // hstem + parseStems(); + break; + case 3: // vstem + parseStems(); + break; + case 4: // vmoveto + if (stack.length > 1 && !haveWidth) { + width = stack.shift() + nominalWidthX; + haveWidth = true; + } + + y += stack.pop(); + newContour(x, y); + break; + case 5: // rlineto + while (stack.length > 0) { + x += stack.shift(); + y += stack.shift(); + p.lineTo(x, y); + } + + break; + case 6: // hlineto + while (stack.length > 0) { + x += stack.shift(); + p.lineTo(x, y); + if (stack.length === 0) { + break; + } + + y += stack.shift(); + p.lineTo(x, y); + } + + break; + case 7: // vlineto + while (stack.length > 0) { + y += stack.shift(); + p.lineTo(x, y); + if (stack.length === 0) { + break; + } + + x += stack.shift(); + p.lineTo(x, y); + } + + break; + case 8: // rrcurveto + while (stack.length > 0) { + c1x = x + stack.shift(); + c1y = y + stack.shift(); + c2x = c1x + stack.shift(); + c2y = c1y + stack.shift(); + x = c2x + stack.shift(); + y = c2y + stack.shift(); + p.curveTo(c1x, c1y, c2x, c2y, x, y); + } + + break; + case 10: // callsubr + codeIndex = stack.pop() + subrsBias; + subrCode = subrs[codeIndex]; + if (subrCode) { + parse(subrCode); + } + + break; + case 11: // return + return; + case 12: // flex operators + v = code[i]; + i += 1; + switch (v) { + case 35: // flex + // |- dx1 dy1 dx2 dy2 dx3 dy3 dx4 dy4 dx5 dy5 dx6 dy6 fd flex (12 35) |- + c1x = x + stack.shift(); // dx1 + c1y = y + stack.shift(); // dy1 + c2x = c1x + stack.shift(); // dx2 + c2y = c1y + stack.shift(); // dy2 + jpx = c2x + stack.shift(); // dx3 + jpy = c2y + stack.shift(); // dy3 + c3x = jpx + stack.shift(); // dx4 + c3y = jpy + stack.shift(); // dy4 + c4x = c3x + stack.shift(); // dx5 + c4y = c3y + stack.shift(); // dy5 + x = c4x + stack.shift(); // dx6 + y = c4y + stack.shift(); // dy6 + stack.shift(); // flex depth + p.curveTo(c1x, c1y, c2x, c2y, jpx, jpy); + p.curveTo(c3x, c3y, c4x, c4y, x, y); + break; + case 34: // hflex + // |- dx1 dx2 dy2 dx3 dx4 dx5 dx6 hflex (12 34) |- + c1x = x + stack.shift(); // dx1 + c1y = y; // dy1 + c2x = c1x + stack.shift(); // dx2 + c2y = c1y + stack.shift(); // dy2 + jpx = c2x + stack.shift(); // dx3 + jpy = c2y; // dy3 + c3x = jpx + stack.shift(); // dx4 + c3y = c2y; // dy4 + c4x = c3x + stack.shift(); // dx5 + c4y = y; // dy5 + x = c4x + stack.shift(); // dx6 + p.curveTo(c1x, c1y, c2x, c2y, jpx, jpy); + p.curveTo(c3x, c3y, c4x, c4y, x, y); + break; + case 36: // hflex1 + // |- dx1 dy1 dx2 dy2 dx3 dx4 dx5 dy5 dx6 hflex1 (12 36) |- + c1x = x + stack.shift(); // dx1 + c1y = y + stack.shift(); // dy1 + c2x = c1x + stack.shift(); // dx2 + c2y = c1y + stack.shift(); // dy2 + jpx = c2x + stack.shift(); // dx3 + jpy = c2y; // dy3 + c3x = jpx + stack.shift(); // dx4 + c3y = c2y; // dy4 + c4x = c3x + stack.shift(); // dx5 + c4y = c3y + stack.shift(); // dy5 + x = c4x + stack.shift(); // dx6 + p.curveTo(c1x, c1y, c2x, c2y, jpx, jpy); + p.curveTo(c3x, c3y, c4x, c4y, x, y); + break; + case 37: // flex1 + // |- dx1 dy1 dx2 dy2 dx3 dy3 dx4 dy4 dx5 dy5 d6 flex1 (12 37) |- + c1x = x + stack.shift(); // dx1 + c1y = y + stack.shift(); // dy1 + c2x = c1x + stack.shift(); // dx2 + c2y = c1y + stack.shift(); // dy2 + jpx = c2x + stack.shift(); // dx3 + jpy = c2y + stack.shift(); // dy3 + c3x = jpx + stack.shift(); // dx4 + c3y = jpy + stack.shift(); // dy4 + c4x = c3x + stack.shift(); // dx5 + c4y = c3y + stack.shift(); // dy5 + if (Math.abs(c4x - x) > Math.abs(c4y - y)) { + x = c4x + stack.shift(); + } else { + y = c4y + stack.shift(); + } + + p.curveTo(c1x, c1y, c2x, c2y, jpx, jpy); + p.curveTo(c3x, c3y, c4x, c4y, x, y); + break; + default: + console.log('Glyph ' + glyph.index + ': unknown operator ' + 1200 + v); + stack.length = 0; + } + break; + case 14: // endchar + if (stack.length > 0 && !haveWidth) { + width = stack.shift() + nominalWidthX; + haveWidth = true; + } + + if (open) { + p.closePath(); + open = false; + } + + break; + case 18: // hstemhm + parseStems(); + break; + case 19: // hintmask + case 20: // cntrmask + parseStems(); + i += (nStems + 7) >> 3; + break; + case 21: // rmoveto + if (stack.length > 2 && !haveWidth) { + width = stack.shift() + nominalWidthX; + haveWidth = true; + } + + y += stack.pop(); + x += stack.pop(); + newContour(x, y); + break; + case 22: // hmoveto + if (stack.length > 1 && !haveWidth) { + width = stack.shift() + nominalWidthX; + haveWidth = true; + } + + x += stack.pop(); + newContour(x, y); + break; + case 23: // vstemhm + parseStems(); + break; + case 24: // rcurveline + while (stack.length > 2) { + c1x = x + stack.shift(); + c1y = y + stack.shift(); + c2x = c1x + stack.shift(); + c2y = c1y + stack.shift(); + x = c2x + stack.shift(); + y = c2y + stack.shift(); + p.curveTo(c1x, c1y, c2x, c2y, x, y); + } + + x += stack.shift(); + y += stack.shift(); + p.lineTo(x, y); + break; + case 25: // rlinecurve + while (stack.length > 6) { + x += stack.shift(); + y += stack.shift(); + p.lineTo(x, y); + } + + c1x = x + stack.shift(); + c1y = y + stack.shift(); + c2x = c1x + stack.shift(); + c2y = c1y + stack.shift(); + x = c2x + stack.shift(); + y = c2y + stack.shift(); + p.curveTo(c1x, c1y, c2x, c2y, x, y); + break; + case 26: // vvcurveto + if (stack.length % 2) { + x += stack.shift(); + } + + while (stack.length > 0) { + c1x = x; + c1y = y + stack.shift(); + c2x = c1x + stack.shift(); + c2y = c1y + stack.shift(); + x = c2x; + y = c2y + stack.shift(); + p.curveTo(c1x, c1y, c2x, c2y, x, y); + } + + break; + case 27: // hhcurveto + if (stack.length % 2) { + y += stack.shift(); + } + + while (stack.length > 0) { + c1x = x + stack.shift(); + c1y = y; + c2x = c1x + stack.shift(); + c2y = c1y + stack.shift(); + x = c2x + stack.shift(); + y = c2y; + p.curveTo(c1x, c1y, c2x, c2y, x, y); + } + + break; + case 28: // shortint + b1 = code[i]; + b2 = code[i + 1]; + stack.push(((b1 << 24) | (b2 << 16)) >> 16); + i += 2; + break; + case 29: // callgsubr + codeIndex = stack.pop() + font.gsubrsBias; + subrCode = font.gsubrs[codeIndex]; + if (subrCode) { + parse(subrCode); + } + + break; + case 30: // vhcurveto + while (stack.length > 0) { + c1x = x; + c1y = y + stack.shift(); + c2x = c1x + stack.shift(); + c2y = c1y + stack.shift(); + x = c2x + stack.shift(); + y = c2y + (stack.length === 1 ? stack.shift() : 0); + p.curveTo(c1x, c1y, c2x, c2y, x, y); + if (stack.length === 0) { + break; + } + + c1x = x + stack.shift(); + c1y = y; + c2x = c1x + stack.shift(); + c2y = c1y + stack.shift(); + y = c2y + stack.shift(); + x = c2x + (stack.length === 1 ? stack.shift() : 0); + p.curveTo(c1x, c1y, c2x, c2y, x, y); + } + + break; + case 31: // hvcurveto + while (stack.length > 0) { + c1x = x + stack.shift(); + c1y = y; + c2x = c1x + stack.shift(); + c2y = c1y + stack.shift(); + y = c2y + stack.shift(); + x = c2x + (stack.length === 1 ? stack.shift() : 0); + p.curveTo(c1x, c1y, c2x, c2y, x, y); + if (stack.length === 0) { + break; + } + + c1x = x; + c1y = y + stack.shift(); + c2x = c1x + stack.shift(); + c2y = c1y + stack.shift(); + x = c2x + stack.shift(); + y = c2y + (stack.length === 1 ? stack.shift() : 0); + p.curveTo(c1x, c1y, c2x, c2y, x, y); + } + + break; + default: + if (v < 32) { + console.log('Glyph ' + glyph.index + ': unknown operator ' + v); + } else if (v < 247) { + stack.push(v - 139); + } else if (v < 251) { + b1 = code[i]; + i += 1; + stack.push((v - 247) * 256 + b1 + 108); + } else if (v < 255) { + b1 = code[i]; + i += 1; + stack.push(-(v - 251) * 256 - b1 - 108); + } else { + b1 = code[i]; + b2 = code[i + 1]; + b3 = code[i + 2]; + b4 = code[i + 3]; + i += 4; + stack.push(((b1 << 24) | (b2 << 16) | (b3 << 8) | b4) / 65536); + } + } + } + } + + parse(code); + + glyph.advanceWidth = width; + return p; + } + + function parseCFFFDSelect(data, start, nGlyphs, fdArrayCount) { + var fdSelect = []; + var fdIndex; + var parser = new parse.Parser(data, start); + var format = parser.parseCard8(); + if (format === 0) { + // Simple list of nGlyphs elements + for (var iGid = 0; iGid < nGlyphs; iGid++) { + fdIndex = parser.parseCard8(); + if (fdIndex >= fdArrayCount) { + throw new Error('CFF table CID Font FDSelect has bad FD index value ' + fdIndex + ' (FD count ' + fdArrayCount + ')'); + } + fdSelect.push(fdIndex); + } + } else if (format === 3) { + // Ranges + var nRanges = parser.parseCard16(); + var first = parser.parseCard16(); + if (first !== 0) { + throw new Error('CFF Table CID Font FDSelect format 3 range has bad initial GID ' + first); + } + var next; + for (var iRange = 0; iRange < nRanges; iRange++) { + fdIndex = parser.parseCard8(); + next = parser.parseCard16(); + if (fdIndex >= fdArrayCount) { + throw new Error('CFF table CID Font FDSelect has bad FD index value ' + fdIndex + ' (FD count ' + fdArrayCount + ')'); + } + if (next > nGlyphs) { + throw new Error('CFF Table CID Font FDSelect format 3 range has bad GID ' + next); + } + for (; first < next; first++) { + fdSelect.push(fdIndex); + } + first = next; + } + if (next !== nGlyphs) { + throw new Error('CFF Table CID Font FDSelect format 3 range has bad final GID ' + next); + } + } else { + throw new Error('CFF Table CID Font FDSelect table has unsupported format ' + format); + } + return fdSelect; + } + + // Parse the `CFF` table, which contains the glyph outlines in PostScript format. + function parseCFFTable(data, start, font, opt) { + font.tables.cff = {}; + var header = parseCFFHeader(data, start); + var nameIndex = parseCFFIndex(data, header.endOffset, parse.bytesToString); + var topDictIndex = parseCFFIndex(data, nameIndex.endOffset); + var stringIndex = parseCFFIndex(data, topDictIndex.endOffset, parse.bytesToString); + var globalSubrIndex = parseCFFIndex(data, stringIndex.endOffset); + font.gsubrs = globalSubrIndex.objects; + font.gsubrsBias = calcCFFSubroutineBias(font.gsubrs); + + var topDictArray = gatherCFFTopDicts(data, start, topDictIndex.objects, stringIndex.objects); + if (topDictArray.length !== 1) { + throw new Error('CFF table has too many fonts in \'FontSet\' - count of fonts NameIndex.length = ' + topDictArray.length); + } + + var topDict = topDictArray[0]; + font.tables.cff.topDict = topDict; + + if (topDict._privateDict) { + font.defaultWidthX = topDict._privateDict.defaultWidthX; + font.nominalWidthX = topDict._privateDict.nominalWidthX; + } + + if (topDict.ros[0] !== undefined && topDict.ros[1] !== undefined) { + font.isCIDFont = true; + } + + if (font.isCIDFont) { + var fdArrayOffset = topDict.fdArray; + var fdSelectOffset = topDict.fdSelect; + if (fdArrayOffset === 0 || fdSelectOffset === 0) { + throw new Error('Font is marked as a CID font, but FDArray and/or FDSelect information is missing'); + } + fdArrayOffset += start; + var fdArrayIndex = parseCFFIndex(data, fdArrayOffset); + var fdArray = gatherCFFTopDicts(data, start, fdArrayIndex.objects, stringIndex.objects); + topDict._fdArray = fdArray; + fdSelectOffset += start; + topDict._fdSelect = parseCFFFDSelect(data, fdSelectOffset, font.numGlyphs, fdArray.length); + } + + var privateDictOffset = start + topDict.private[1]; + var privateDict = parseCFFPrivateDict(data, privateDictOffset, topDict.private[0], stringIndex.objects); + font.defaultWidthX = privateDict.defaultWidthX; + font.nominalWidthX = privateDict.nominalWidthX; + + if (privateDict.subrs !== 0) { + var subrOffset = privateDictOffset + privateDict.subrs; + var subrIndex = parseCFFIndex(data, subrOffset); + font.subrs = subrIndex.objects; + font.subrsBias = calcCFFSubroutineBias(font.subrs); + } else { + font.subrs = []; + font.subrsBias = 0; + } + + // Offsets in the top dict are relative to the beginning of the CFF data, so add the CFF start offset. + var charStringsIndex; + if (opt.lowMemory) { + charStringsIndex = parseCFFIndexLowMemory(data, start + topDict.charStrings); + font.nGlyphs = charStringsIndex.offsets.length; + } else { + charStringsIndex = parseCFFIndex(data, start + topDict.charStrings); + font.nGlyphs = charStringsIndex.objects.length; + } + + var charset = parseCFFCharset(data, start + topDict.charset, font.nGlyphs, stringIndex.objects); + if (topDict.encoding === 0) { + // Standard encoding + font.cffEncoding = new CffEncoding(cffStandardEncoding, charset); + } else if (topDict.encoding === 1) { + // Expert encoding + font.cffEncoding = new CffEncoding(cffExpertEncoding, charset); + } else { + font.cffEncoding = parseCFFEncoding(data, start + topDict.encoding, charset); + } + + // Prefer the CMAP encoding to the CFF encoding. + font.encoding = font.encoding || font.cffEncoding; + + font.glyphs = new glyphset.GlyphSet(font); + if (opt.lowMemory) { + font._push = function(i) { + var charString = getCffIndexObject(i, charStringsIndex.offsets, data, start + topDict.charStrings); + font.glyphs.push(i, glyphset.cffGlyphLoader(font, i, parseCFFCharstring, charString)); + }; + } else { + for (var i = 0; i < font.nGlyphs; i += 1) { + var charString = charStringsIndex.objects[i]; + font.glyphs.push(i, glyphset.cffGlyphLoader(font, i, parseCFFCharstring, charString)); + } + } + } + + // Convert a string to a String ID (SID). + // The list of strings is modified in place. + function encodeString(s, strings) { + var sid; + + // Is the string in the CFF standard strings? + var i = cffStandardStrings.indexOf(s); + if (i >= 0) { + sid = i; + } + + // Is the string already in the string index? + i = strings.indexOf(s); + if (i >= 0) { + sid = i + cffStandardStrings.length; + } else { + sid = cffStandardStrings.length + strings.length; + strings.push(s); + } + + return sid; + } + + function makeHeader() { + return new table.Record('Header', [ + {name: 'major', type: 'Card8', value: 1}, + {name: 'minor', type: 'Card8', value: 0}, + {name: 'hdrSize', type: 'Card8', value: 4}, + {name: 'major', type: 'Card8', value: 1} + ]); + } + + function makeNameIndex(fontNames) { + var t = new table.Record('Name INDEX', [ + {name: 'names', type: 'INDEX', value: []} + ]); + t.names = []; + for (var i = 0; i < fontNames.length; i += 1) { + t.names.push({name: 'name_' + i, type: 'NAME', value: fontNames[i]}); + } + + return t; + } + + // Given a dictionary's metadata, create a DICT structure. + function makeDict(meta, attrs, strings) { + var m = {}; + for (var i = 0; i < meta.length; i += 1) { + var entry = meta[i]; + var value = attrs[entry.name]; + if (value !== undefined && !equals(value, entry.value)) { + if (entry.type === 'SID') { + value = encodeString(value, strings); + } + + m[entry.op] = {name: entry.name, type: entry.type, value: value}; + } + } + + return m; + } + + // The Top DICT houses the global font attributes. + function makeTopDict(attrs, strings) { + var t = new table.Record('Top DICT', [ + {name: 'dict', type: 'DICT', value: {}} + ]); + t.dict = makeDict(TOP_DICT_META, attrs, strings); + return t; + } + + function makeTopDictIndex(topDict) { + var t = new table.Record('Top DICT INDEX', [ + {name: 'topDicts', type: 'INDEX', value: []} + ]); + t.topDicts = [{name: 'topDict_0', type: 'TABLE', value: topDict}]; + return t; + } + + function makeStringIndex(strings) { + var t = new table.Record('String INDEX', [ + {name: 'strings', type: 'INDEX', value: []} + ]); + t.strings = []; + for (var i = 0; i < strings.length; i += 1) { + t.strings.push({name: 'string_' + i, type: 'STRING', value: strings[i]}); + } + + return t; + } + + function makeGlobalSubrIndex() { + // Currently we don't use subroutines. + return new table.Record('Global Subr INDEX', [ + {name: 'subrs', type: 'INDEX', value: []} + ]); + } + + function makeCharsets(glyphNames, strings) { + var t = new table.Record('Charsets', [ + {name: 'format', type: 'Card8', value: 0} + ]); + for (var i = 0; i < glyphNames.length; i += 1) { + var glyphName = glyphNames[i]; + var glyphSID = encodeString(glyphName, strings); + t.fields.push({name: 'glyph_' + i, type: 'SID', value: glyphSID}); + } + + return t; + } + + function glyphToOps(glyph) { + var ops = []; + var path = glyph.path; + ops.push({name: 'width', type: 'NUMBER', value: glyph.advanceWidth}); + var x = 0; + var y = 0; + for (var i = 0; i < path.commands.length; i += 1) { + var dx = (void 0); + var dy = (void 0); + var cmd = path.commands[i]; + if (cmd.type === 'Q') { + // CFF only supports bézier curves, so convert the quad to a bézier. + var _13 = 1 / 3; + var _23 = 2 / 3; + + // We're going to create a new command so we don't change the original path. + // Since all coordinates are relative, we round() them ASAP to avoid propagating errors. + cmd = { + type: 'C', + x: cmd.x, + y: cmd.y, + x1: Math.round(_13 * x + _23 * cmd.x1), + y1: Math.round(_13 * y + _23 * cmd.y1), + x2: Math.round(_13 * cmd.x + _23 * cmd.x1), + y2: Math.round(_13 * cmd.y + _23 * cmd.y1) + }; + } + + if (cmd.type === 'M') { + dx = Math.round(cmd.x - x); + dy = Math.round(cmd.y - y); + ops.push({name: 'dx', type: 'NUMBER', value: dx}); + ops.push({name: 'dy', type: 'NUMBER', value: dy}); + ops.push({name: 'rmoveto', type: 'OP', value: 21}); + x = Math.round(cmd.x); + y = Math.round(cmd.y); + } else if (cmd.type === 'L') { + dx = Math.round(cmd.x - x); + dy = Math.round(cmd.y - y); + ops.push({name: 'dx', type: 'NUMBER', value: dx}); + ops.push({name: 'dy', type: 'NUMBER', value: dy}); + ops.push({name: 'rlineto', type: 'OP', value: 5}); + x = Math.round(cmd.x); + y = Math.round(cmd.y); + } else if (cmd.type === 'C') { + var dx1 = Math.round(cmd.x1 - x); + var dy1 = Math.round(cmd.y1 - y); + var dx2 = Math.round(cmd.x2 - cmd.x1); + var dy2 = Math.round(cmd.y2 - cmd.y1); + dx = Math.round(cmd.x - cmd.x2); + dy = Math.round(cmd.y - cmd.y2); + ops.push({name: 'dx1', type: 'NUMBER', value: dx1}); + ops.push({name: 'dy1', type: 'NUMBER', value: dy1}); + ops.push({name: 'dx2', type: 'NUMBER', value: dx2}); + ops.push({name: 'dy2', type: 'NUMBER', value: dy2}); + ops.push({name: 'dx', type: 'NUMBER', value: dx}); + ops.push({name: 'dy', type: 'NUMBER', value: dy}); + ops.push({name: 'rrcurveto', type: 'OP', value: 8}); + x = Math.round(cmd.x); + y = Math.round(cmd.y); + } + + // Contours are closed automatically. + } + + ops.push({name: 'endchar', type: 'OP', value: 14}); + return ops; + } + + function makeCharStringsIndex(glyphs) { + var t = new table.Record('CharStrings INDEX', [ + {name: 'charStrings', type: 'INDEX', value: []} + ]); + + for (var i = 0; i < glyphs.length; i += 1) { + var glyph = glyphs.get(i); + var ops = glyphToOps(glyph); + t.charStrings.push({name: glyph.name, type: 'CHARSTRING', value: ops}); + } + + return t; + } + + function makePrivateDict(attrs, strings) { + var t = new table.Record('Private DICT', [ + {name: 'dict', type: 'DICT', value: {}} + ]); + t.dict = makeDict(PRIVATE_DICT_META, attrs, strings); + return t; + } + + function makeCFFTable(glyphs, options) { + var t = new table.Table('CFF ', [ + {name: 'header', type: 'RECORD'}, + {name: 'nameIndex', type: 'RECORD'}, + {name: 'topDictIndex', type: 'RECORD'}, + {name: 'stringIndex', type: 'RECORD'}, + {name: 'globalSubrIndex', type: 'RECORD'}, + {name: 'charsets', type: 'RECORD'}, + {name: 'charStringsIndex', type: 'RECORD'}, + {name: 'privateDict', type: 'RECORD'} + ]); + + var fontScale = 1 / options.unitsPerEm; + // We use non-zero values for the offsets so that the DICT encodes them. + // This is important because the size of the Top DICT plays a role in offset calculation, + // and the size shouldn't change after we've written correct offsets. + var attrs = { + version: options.version, + fullName: options.fullName, + familyName: options.familyName, + weight: options.weightName, + fontBBox: options.fontBBox || [0, 0, 0, 0], + fontMatrix: [fontScale, 0, 0, fontScale, 0, 0], + charset: 999, + encoding: 0, + charStrings: 999, + private: [0, 999] + }; + + var privateAttrs = {}; + + var glyphNames = []; + var glyph; + + // Skip first glyph (.notdef) + for (var i = 1; i < glyphs.length; i += 1) { + glyph = glyphs.get(i); + glyphNames.push(glyph.name); + } + + var strings = []; + + t.header = makeHeader(); + t.nameIndex = makeNameIndex([options.postScriptName]); + var topDict = makeTopDict(attrs, strings); + t.topDictIndex = makeTopDictIndex(topDict); + t.globalSubrIndex = makeGlobalSubrIndex(); + t.charsets = makeCharsets(glyphNames, strings); + t.charStringsIndex = makeCharStringsIndex(glyphs); + t.privateDict = makePrivateDict(privateAttrs, strings); + + // Needs to come at the end, to encode all custom strings used in the font. + t.stringIndex = makeStringIndex(strings); + + var startOffset = t.header.sizeOf() + + t.nameIndex.sizeOf() + + t.topDictIndex.sizeOf() + + t.stringIndex.sizeOf() + + t.globalSubrIndex.sizeOf(); + attrs.charset = startOffset; + + // We use the CFF standard encoding; proper encoding will be handled in cmap. + attrs.encoding = 0; + attrs.charStrings = attrs.charset + t.charsets.sizeOf(); + attrs.private[1] = attrs.charStrings + t.charStringsIndex.sizeOf(); + + // Recreate the Top DICT INDEX with the correct offsets. + topDict = makeTopDict(attrs, strings); + t.topDictIndex = makeTopDictIndex(topDict); + + return t; + } + + var cff = { parse: parseCFFTable, make: makeCFFTable }; + + // The `head` table contains global information about the font. + + // Parse the header `head` table + function parseHeadTable(data, start) { + var head = {}; + var p = new parse.Parser(data, start); + head.version = p.parseVersion(); + head.fontRevision = Math.round(p.parseFixed() * 1000) / 1000; + head.checkSumAdjustment = p.parseULong(); + head.magicNumber = p.parseULong(); + check.argument(head.magicNumber === 0x5F0F3CF5, 'Font header has wrong magic number.'); + head.flags = p.parseUShort(); + head.unitsPerEm = p.parseUShort(); + head.created = p.parseLongDateTime(); + head.modified = p.parseLongDateTime(); + head.xMin = p.parseShort(); + head.yMin = p.parseShort(); + head.xMax = p.parseShort(); + head.yMax = p.parseShort(); + head.macStyle = p.parseUShort(); + head.lowestRecPPEM = p.parseUShort(); + head.fontDirectionHint = p.parseShort(); + head.indexToLocFormat = p.parseShort(); + head.glyphDataFormat = p.parseShort(); + return head; + } + + function makeHeadTable(options) { + // Apple Mac timestamp epoch is 01/01/1904 not 01/01/1970 + var timestamp = Math.round(new Date().getTime() / 1000) + 2082844800; + var createdTimestamp = timestamp; + + if (options.createdTimestamp) { + createdTimestamp = options.createdTimestamp + 2082844800; + } + + return new table.Table('head', [ + {name: 'version', type: 'FIXED', value: 0x00010000}, + {name: 'fontRevision', type: 'FIXED', value: 0x00010000}, + {name: 'checkSumAdjustment', type: 'ULONG', value: 0}, + {name: 'magicNumber', type: 'ULONG', value: 0x5F0F3CF5}, + {name: 'flags', type: 'USHORT', value: 0}, + {name: 'unitsPerEm', type: 'USHORT', value: 1000}, + {name: 'created', type: 'LONGDATETIME', value: createdTimestamp}, + {name: 'modified', type: 'LONGDATETIME', value: timestamp}, + {name: 'xMin', type: 'SHORT', value: 0}, + {name: 'yMin', type: 'SHORT', value: 0}, + {name: 'xMax', type: 'SHORT', value: 0}, + {name: 'yMax', type: 'SHORT', value: 0}, + {name: 'macStyle', type: 'USHORT', value: 0}, + {name: 'lowestRecPPEM', type: 'USHORT', value: 0}, + {name: 'fontDirectionHint', type: 'SHORT', value: 2}, + {name: 'indexToLocFormat', type: 'SHORT', value: 0}, + {name: 'glyphDataFormat', type: 'SHORT', value: 0} + ], options); + } + + var head = { parse: parseHeadTable, make: makeHeadTable }; + + // The `hhea` table contains information for horizontal layout. + + // Parse the horizontal header `hhea` table + function parseHheaTable(data, start) { + var hhea = {}; + var p = new parse.Parser(data, start); + hhea.version = p.parseVersion(); + hhea.ascender = p.parseShort(); + hhea.descender = p.parseShort(); + hhea.lineGap = p.parseShort(); + hhea.advanceWidthMax = p.parseUShort(); + hhea.minLeftSideBearing = p.parseShort(); + hhea.minRightSideBearing = p.parseShort(); + hhea.xMaxExtent = p.parseShort(); + hhea.caretSlopeRise = p.parseShort(); + hhea.caretSlopeRun = p.parseShort(); + hhea.caretOffset = p.parseShort(); + p.relativeOffset += 8; + hhea.metricDataFormat = p.parseShort(); + hhea.numberOfHMetrics = p.parseUShort(); + return hhea; + } + + function makeHheaTable(options) { + return new table.Table('hhea', [ + {name: 'version', type: 'FIXED', value: 0x00010000}, + {name: 'ascender', type: 'FWORD', value: 0}, + {name: 'descender', type: 'FWORD', value: 0}, + {name: 'lineGap', type: 'FWORD', value: 0}, + {name: 'advanceWidthMax', type: 'UFWORD', value: 0}, + {name: 'minLeftSideBearing', type: 'FWORD', value: 0}, + {name: 'minRightSideBearing', type: 'FWORD', value: 0}, + {name: 'xMaxExtent', type: 'FWORD', value: 0}, + {name: 'caretSlopeRise', type: 'SHORT', value: 1}, + {name: 'caretSlopeRun', type: 'SHORT', value: 0}, + {name: 'caretOffset', type: 'SHORT', value: 0}, + {name: 'reserved1', type: 'SHORT', value: 0}, + {name: 'reserved2', type: 'SHORT', value: 0}, + {name: 'reserved3', type: 'SHORT', value: 0}, + {name: 'reserved4', type: 'SHORT', value: 0}, + {name: 'metricDataFormat', type: 'SHORT', value: 0}, + {name: 'numberOfHMetrics', type: 'USHORT', value: 0} + ], options); + } + + var hhea = { parse: parseHheaTable, make: makeHheaTable }; + + // The `hmtx` table contains the horizontal metrics for all glyphs. + + function parseHmtxTableAll(data, start, numMetrics, numGlyphs, glyphs) { + var advanceWidth; + var leftSideBearing; + var p = new parse.Parser(data, start); + for (var i = 0; i < numGlyphs; i += 1) { + // If the font is monospaced, only one entry is needed. This last entry applies to all subsequent glyphs. + if (i < numMetrics) { + advanceWidth = p.parseUShort(); + leftSideBearing = p.parseShort(); + } + + var glyph = glyphs.get(i); + glyph.advanceWidth = advanceWidth; + glyph.leftSideBearing = leftSideBearing; + } + } + + function parseHmtxTableOnLowMemory(font, data, start, numMetrics, numGlyphs) { + font._hmtxTableData = {}; + + var advanceWidth; + var leftSideBearing; + var p = new parse.Parser(data, start); + for (var i = 0; i < numGlyphs; i += 1) { + // If the font is monospaced, only one entry is needed. This last entry applies to all subsequent glyphs. + if (i < numMetrics) { + advanceWidth = p.parseUShort(); + leftSideBearing = p.parseShort(); + } + + font._hmtxTableData[i] = { + advanceWidth: advanceWidth, + leftSideBearing: leftSideBearing + }; + } + } + + // Parse the `hmtx` table, which contains the horizontal metrics for all glyphs. + // This function augments the glyph array, adding the advanceWidth and leftSideBearing to each glyph. + function parseHmtxTable(font, data, start, numMetrics, numGlyphs, glyphs, opt) { + if (opt.lowMemory) + { parseHmtxTableOnLowMemory(font, data, start, numMetrics, numGlyphs); } + else + { parseHmtxTableAll(data, start, numMetrics, numGlyphs, glyphs); } + } + + function makeHmtxTable(glyphs) { + var t = new table.Table('hmtx', []); + for (var i = 0; i < glyphs.length; i += 1) { + var glyph = glyphs.get(i); + var advanceWidth = glyph.advanceWidth || 0; + var leftSideBearing = glyph.leftSideBearing || 0; + t.fields.push({name: 'advanceWidth_' + i, type: 'USHORT', value: advanceWidth}); + t.fields.push({name: 'leftSideBearing_' + i, type: 'SHORT', value: leftSideBearing}); + } + + return t; + } + + var hmtx = { parse: parseHmtxTable, make: makeHmtxTable }; + + // The `ltag` table stores IETF BCP-47 language tags. It allows supporting + + function makeLtagTable(tags) { + var result = new table.Table('ltag', [ + {name: 'version', type: 'ULONG', value: 1}, + {name: 'flags', type: 'ULONG', value: 0}, + {name: 'numTags', type: 'ULONG', value: tags.length} + ]); + + var stringPool = ''; + var stringPoolOffset = 12 + tags.length * 4; + for (var i = 0; i < tags.length; ++i) { + var pos = stringPool.indexOf(tags[i]); + if (pos < 0) { + pos = stringPool.length; + stringPool += tags[i]; + } + + result.fields.push({name: 'offset ' + i, type: 'USHORT', value: stringPoolOffset + pos}); + result.fields.push({name: 'length ' + i, type: 'USHORT', value: tags[i].length}); + } + + result.fields.push({name: 'stringPool', type: 'CHARARRAY', value: stringPool}); + return result; + } + + function parseLtagTable(data, start) { + var p = new parse.Parser(data, start); + var tableVersion = p.parseULong(); + check.argument(tableVersion === 1, 'Unsupported ltag table version.'); + // The 'ltag' specification does not define any flags; skip the field. + p.skip('uLong', 1); + var numTags = p.parseULong(); + + var tags = []; + for (var i = 0; i < numTags; i++) { + var tag = ''; + var offset = start + p.parseUShort(); + var length = p.parseUShort(); + for (var j = offset; j < offset + length; ++j) { + tag += String.fromCharCode(data.getInt8(j)); + } + + tags.push(tag); + } + + return tags; + } + + var ltag = { make: makeLtagTable, parse: parseLtagTable }; + + // The `maxp` table establishes the memory requirements for the font. + + // Parse the maximum profile `maxp` table. + function parseMaxpTable(data, start) { + var maxp = {}; + var p = new parse.Parser(data, start); + maxp.version = p.parseVersion(); + maxp.numGlyphs = p.parseUShort(); + if (maxp.version === 1.0) { + maxp.maxPoints = p.parseUShort(); + maxp.maxContours = p.parseUShort(); + maxp.maxCompositePoints = p.parseUShort(); + maxp.maxCompositeContours = p.parseUShort(); + maxp.maxZones = p.parseUShort(); + maxp.maxTwilightPoints = p.parseUShort(); + maxp.maxStorage = p.parseUShort(); + maxp.maxFunctionDefs = p.parseUShort(); + maxp.maxInstructionDefs = p.parseUShort(); + maxp.maxStackElements = p.parseUShort(); + maxp.maxSizeOfInstructions = p.parseUShort(); + maxp.maxComponentElements = p.parseUShort(); + maxp.maxComponentDepth = p.parseUShort(); + } + + return maxp; + } + + function makeMaxpTable(numGlyphs) { + return new table.Table('maxp', [ + {name: 'version', type: 'FIXED', value: 0x00005000}, + {name: 'numGlyphs', type: 'USHORT', value: numGlyphs} + ]); + } + + var maxp = { parse: parseMaxpTable, make: makeMaxpTable }; + + // The `name` naming table. + + // NameIDs for the name table. + var nameTableNames = [ + 'copyright', // 0 + 'fontFamily', // 1 + 'fontSubfamily', // 2 + 'uniqueID', // 3 + 'fullName', // 4 + 'version', // 5 + 'postScriptName', // 6 + 'trademark', // 7 + 'manufacturer', // 8 + 'designer', // 9 + 'description', // 10 + 'manufacturerURL', // 11 + 'designerURL', // 12 + 'license', // 13 + 'licenseURL', // 14 + 'reserved', // 15 + 'preferredFamily', // 16 + 'preferredSubfamily', // 17 + 'compatibleFullName', // 18 + 'sampleText', // 19 + 'postScriptFindFontName', // 20 + 'wwsFamily', // 21 + 'wwsSubfamily' // 22 + ]; + + var macLanguages = { + 0: 'en', + 1: 'fr', + 2: 'de', + 3: 'it', + 4: 'nl', + 5: 'sv', + 6: 'es', + 7: 'da', + 8: 'pt', + 9: 'no', + 10: 'he', + 11: 'ja', + 12: 'ar', + 13: 'fi', + 14: 'el', + 15: 'is', + 16: 'mt', + 17: 'tr', + 18: 'hr', + 19: 'zh-Hant', + 20: 'ur', + 21: 'hi', + 22: 'th', + 23: 'ko', + 24: 'lt', + 25: 'pl', + 26: 'hu', + 27: 'es', + 28: 'lv', + 29: 'se', + 30: 'fo', + 31: 'fa', + 32: 'ru', + 33: 'zh', + 34: 'nl-BE', + 35: 'ga', + 36: 'sq', + 37: 'ro', + 38: 'cz', + 39: 'sk', + 40: 'si', + 41: 'yi', + 42: 'sr', + 43: 'mk', + 44: 'bg', + 45: 'uk', + 46: 'be', + 47: 'uz', + 48: 'kk', + 49: 'az-Cyrl', + 50: 'az-Arab', + 51: 'hy', + 52: 'ka', + 53: 'mo', + 54: 'ky', + 55: 'tg', + 56: 'tk', + 57: 'mn-CN', + 58: 'mn', + 59: 'ps', + 60: 'ks', + 61: 'ku', + 62: 'sd', + 63: 'bo', + 64: 'ne', + 65: 'sa', + 66: 'mr', + 67: 'bn', + 68: 'as', + 69: 'gu', + 70: 'pa', + 71: 'or', + 72: 'ml', + 73: 'kn', + 74: 'ta', + 75: 'te', + 76: 'si', + 77: 'my', + 78: 'km', + 79: 'lo', + 80: 'vi', + 81: 'id', + 82: 'tl', + 83: 'ms', + 84: 'ms-Arab', + 85: 'am', + 86: 'ti', + 87: 'om', + 88: 'so', + 89: 'sw', + 90: 'rw', + 91: 'rn', + 92: 'ny', + 93: 'mg', + 94: 'eo', + 128: 'cy', + 129: 'eu', + 130: 'ca', + 131: 'la', + 132: 'qu', + 133: 'gn', + 134: 'ay', + 135: 'tt', + 136: 'ug', + 137: 'dz', + 138: 'jv', + 139: 'su', + 140: 'gl', + 141: 'af', + 142: 'br', + 143: 'iu', + 144: 'gd', + 145: 'gv', + 146: 'ga', + 147: 'to', + 148: 'el-polyton', + 149: 'kl', + 150: 'az', + 151: 'nn' + }; + + // MacOS language ID → MacOS script ID + // + // Note that the script ID is not sufficient to determine what encoding + // to use in TrueType files. For some languages, MacOS used a modification + // of a mainstream script. For example, an Icelandic name would be stored + // with smRoman in the TrueType naming table, but the actual encoding + // is a special Icelandic version of the normal Macintosh Roman encoding. + // As another example, Inuktitut uses an 8-bit encoding for Canadian Aboriginal + // Syllables but MacOS had run out of available script codes, so this was + // done as a (pretty radical) "modification" of Ethiopic. + // + // http://unicode.org/Public/MAPPINGS/VENDORS/APPLE/Readme.txt + var macLanguageToScript = { + 0: 0, // langEnglish → smRoman + 1: 0, // langFrench → smRoman + 2: 0, // langGerman → smRoman + 3: 0, // langItalian → smRoman + 4: 0, // langDutch → smRoman + 5: 0, // langSwedish → smRoman + 6: 0, // langSpanish → smRoman + 7: 0, // langDanish → smRoman + 8: 0, // langPortuguese → smRoman + 9: 0, // langNorwegian → smRoman + 10: 5, // langHebrew → smHebrew + 11: 1, // langJapanese → smJapanese + 12: 4, // langArabic → smArabic + 13: 0, // langFinnish → smRoman + 14: 6, // langGreek → smGreek + 15: 0, // langIcelandic → smRoman (modified) + 16: 0, // langMaltese → smRoman + 17: 0, // langTurkish → smRoman (modified) + 18: 0, // langCroatian → smRoman (modified) + 19: 2, // langTradChinese → smTradChinese + 20: 4, // langUrdu → smArabic + 21: 9, // langHindi → smDevanagari + 22: 21, // langThai → smThai + 23: 3, // langKorean → smKorean + 24: 29, // langLithuanian → smCentralEuroRoman + 25: 29, // langPolish → smCentralEuroRoman + 26: 29, // langHungarian → smCentralEuroRoman + 27: 29, // langEstonian → smCentralEuroRoman + 28: 29, // langLatvian → smCentralEuroRoman + 29: 0, // langSami → smRoman + 30: 0, // langFaroese → smRoman (modified) + 31: 4, // langFarsi → smArabic (modified) + 32: 7, // langRussian → smCyrillic + 33: 25, // langSimpChinese → smSimpChinese + 34: 0, // langFlemish → smRoman + 35: 0, // langIrishGaelic → smRoman (modified) + 36: 0, // langAlbanian → smRoman + 37: 0, // langRomanian → smRoman (modified) + 38: 29, // langCzech → smCentralEuroRoman + 39: 29, // langSlovak → smCentralEuroRoman + 40: 0, // langSlovenian → smRoman (modified) + 41: 5, // langYiddish → smHebrew + 42: 7, // langSerbian → smCyrillic + 43: 7, // langMacedonian → smCyrillic + 44: 7, // langBulgarian → smCyrillic + 45: 7, // langUkrainian → smCyrillic (modified) + 46: 7, // langByelorussian → smCyrillic + 47: 7, // langUzbek → smCyrillic + 48: 7, // langKazakh → smCyrillic + 49: 7, // langAzerbaijani → smCyrillic + 50: 4, // langAzerbaijanAr → smArabic + 51: 24, // langArmenian → smArmenian + 52: 23, // langGeorgian → smGeorgian + 53: 7, // langMoldavian → smCyrillic + 54: 7, // langKirghiz → smCyrillic + 55: 7, // langTajiki → smCyrillic + 56: 7, // langTurkmen → smCyrillic + 57: 27, // langMongolian → smMongolian + 58: 7, // langMongolianCyr → smCyrillic + 59: 4, // langPashto → smArabic + 60: 4, // langKurdish → smArabic + 61: 4, // langKashmiri → smArabic + 62: 4, // langSindhi → smArabic + 63: 26, // langTibetan → smTibetan + 64: 9, // langNepali → smDevanagari + 65: 9, // langSanskrit → smDevanagari + 66: 9, // langMarathi → smDevanagari + 67: 13, // langBengali → smBengali + 68: 13, // langAssamese → smBengali + 69: 11, // langGujarati → smGujarati + 70: 10, // langPunjabi → smGurmukhi + 71: 12, // langOriya → smOriya + 72: 17, // langMalayalam → smMalayalam + 73: 16, // langKannada → smKannada + 74: 14, // langTamil → smTamil + 75: 15, // langTelugu → smTelugu + 76: 18, // langSinhalese → smSinhalese + 77: 19, // langBurmese → smBurmese + 78: 20, // langKhmer → smKhmer + 79: 22, // langLao → smLao + 80: 30, // langVietnamese → smVietnamese + 81: 0, // langIndonesian → smRoman + 82: 0, // langTagalog → smRoman + 83: 0, // langMalayRoman → smRoman + 84: 4, // langMalayArabic → smArabic + 85: 28, // langAmharic → smEthiopic + 86: 28, // langTigrinya → smEthiopic + 87: 28, // langOromo → smEthiopic + 88: 0, // langSomali → smRoman + 89: 0, // langSwahili → smRoman + 90: 0, // langKinyarwanda → smRoman + 91: 0, // langRundi → smRoman + 92: 0, // langNyanja → smRoman + 93: 0, // langMalagasy → smRoman + 94: 0, // langEsperanto → smRoman + 128: 0, // langWelsh → smRoman (modified) + 129: 0, // langBasque → smRoman + 130: 0, // langCatalan → smRoman + 131: 0, // langLatin → smRoman + 132: 0, // langQuechua → smRoman + 133: 0, // langGuarani → smRoman + 134: 0, // langAymara → smRoman + 135: 7, // langTatar → smCyrillic + 136: 4, // langUighur → smArabic + 137: 26, // langDzongkha → smTibetan + 138: 0, // langJavaneseRom → smRoman + 139: 0, // langSundaneseRom → smRoman + 140: 0, // langGalician → smRoman + 141: 0, // langAfrikaans → smRoman + 142: 0, // langBreton → smRoman (modified) + 143: 28, // langInuktitut → smEthiopic (modified) + 144: 0, // langScottishGaelic → smRoman (modified) + 145: 0, // langManxGaelic → smRoman (modified) + 146: 0, // langIrishGaelicScript → smRoman (modified) + 147: 0, // langTongan → smRoman + 148: 6, // langGreekAncient → smRoman + 149: 0, // langGreenlandic → smRoman + 150: 0, // langAzerbaijanRoman → smRoman + 151: 0 // langNynorsk → smRoman + }; + + // While Microsoft indicates a region/country for all its language + // IDs, we omit the region code if it's equal to the "most likely + // region subtag" according to Unicode CLDR. For scripts, we omit + // the subtag if it is equal to the Suppress-Script entry in the + // IANA language subtag registry for IETF BCP 47. + // + // For example, Microsoft states that its language code 0x041A is + // Croatian in Croatia. We transform this to the BCP 47 language code 'hr' + // and not 'hr-HR' because Croatia is the default country for Croatian, + // according to Unicode CLDR. As another example, Microsoft states + // that 0x101A is Croatian (Latin) in Bosnia-Herzegovina. We transform + // this to 'hr-BA' and not 'hr-Latn-BA' because Latin is the default script + // for the Croatian language, according to IANA. + // + // http://www.unicode.org/cldr/charts/latest/supplemental/likely_subtags.html + // http://www.iana.org/assignments/language-subtag-registry/language-subtag-registry + var windowsLanguages = { + 0x0436: 'af', + 0x041C: 'sq', + 0x0484: 'gsw', + 0x045E: 'am', + 0x1401: 'ar-DZ', + 0x3C01: 'ar-BH', + 0x0C01: 'ar', + 0x0801: 'ar-IQ', + 0x2C01: 'ar-JO', + 0x3401: 'ar-KW', + 0x3001: 'ar-LB', + 0x1001: 'ar-LY', + 0x1801: 'ary', + 0x2001: 'ar-OM', + 0x4001: 'ar-QA', + 0x0401: 'ar-SA', + 0x2801: 'ar-SY', + 0x1C01: 'aeb', + 0x3801: 'ar-AE', + 0x2401: 'ar-YE', + 0x042B: 'hy', + 0x044D: 'as', + 0x082C: 'az-Cyrl', + 0x042C: 'az', + 0x046D: 'ba', + 0x042D: 'eu', + 0x0423: 'be', + 0x0845: 'bn', + 0x0445: 'bn-IN', + 0x201A: 'bs-Cyrl', + 0x141A: 'bs', + 0x047E: 'br', + 0x0402: 'bg', + 0x0403: 'ca', + 0x0C04: 'zh-HK', + 0x1404: 'zh-MO', + 0x0804: 'zh', + 0x1004: 'zh-SG', + 0x0404: 'zh-TW', + 0x0483: 'co', + 0x041A: 'hr', + 0x101A: 'hr-BA', + 0x0405: 'cs', + 0x0406: 'da', + 0x048C: 'prs', + 0x0465: 'dv', + 0x0813: 'nl-BE', + 0x0413: 'nl', + 0x0C09: 'en-AU', + 0x2809: 'en-BZ', + 0x1009: 'en-CA', + 0x2409: 'en-029', + 0x4009: 'en-IN', + 0x1809: 'en-IE', + 0x2009: 'en-JM', + 0x4409: 'en-MY', + 0x1409: 'en-NZ', + 0x3409: 'en-PH', + 0x4809: 'en-SG', + 0x1C09: 'en-ZA', + 0x2C09: 'en-TT', + 0x0809: 'en-GB', + 0x0409: 'en', + 0x3009: 'en-ZW', + 0x0425: 'et', + 0x0438: 'fo', + 0x0464: 'fil', + 0x040B: 'fi', + 0x080C: 'fr-BE', + 0x0C0C: 'fr-CA', + 0x040C: 'fr', + 0x140C: 'fr-LU', + 0x180C: 'fr-MC', + 0x100C: 'fr-CH', + 0x0462: 'fy', + 0x0456: 'gl', + 0x0437: 'ka', + 0x0C07: 'de-AT', + 0x0407: 'de', + 0x1407: 'de-LI', + 0x1007: 'de-LU', + 0x0807: 'de-CH', + 0x0408: 'el', + 0x046F: 'kl', + 0x0447: 'gu', + 0x0468: 'ha', + 0x040D: 'he', + 0x0439: 'hi', + 0x040E: 'hu', + 0x040F: 'is', + 0x0470: 'ig', + 0x0421: 'id', + 0x045D: 'iu', + 0x085D: 'iu-Latn', + 0x083C: 'ga', + 0x0434: 'xh', + 0x0435: 'zu', + 0x0410: 'it', + 0x0810: 'it-CH', + 0x0411: 'ja', + 0x044B: 'kn', + 0x043F: 'kk', + 0x0453: 'km', + 0x0486: 'quc', + 0x0487: 'rw', + 0x0441: 'sw', + 0x0457: 'kok', + 0x0412: 'ko', + 0x0440: 'ky', + 0x0454: 'lo', + 0x0426: 'lv', + 0x0427: 'lt', + 0x082E: 'dsb', + 0x046E: 'lb', + 0x042F: 'mk', + 0x083E: 'ms-BN', + 0x043E: 'ms', + 0x044C: 'ml', + 0x043A: 'mt', + 0x0481: 'mi', + 0x047A: 'arn', + 0x044E: 'mr', + 0x047C: 'moh', + 0x0450: 'mn', + 0x0850: 'mn-CN', + 0x0461: 'ne', + 0x0414: 'nb', + 0x0814: 'nn', + 0x0482: 'oc', + 0x0448: 'or', + 0x0463: 'ps', + 0x0415: 'pl', + 0x0416: 'pt', + 0x0816: 'pt-PT', + 0x0446: 'pa', + 0x046B: 'qu-BO', + 0x086B: 'qu-EC', + 0x0C6B: 'qu', + 0x0418: 'ro', + 0x0417: 'rm', + 0x0419: 'ru', + 0x243B: 'smn', + 0x103B: 'smj-NO', + 0x143B: 'smj', + 0x0C3B: 'se-FI', + 0x043B: 'se', + 0x083B: 'se-SE', + 0x203B: 'sms', + 0x183B: 'sma-NO', + 0x1C3B: 'sms', + 0x044F: 'sa', + 0x1C1A: 'sr-Cyrl-BA', + 0x0C1A: 'sr', + 0x181A: 'sr-Latn-BA', + 0x081A: 'sr-Latn', + 0x046C: 'nso', + 0x0432: 'tn', + 0x045B: 'si', + 0x041B: 'sk', + 0x0424: 'sl', + 0x2C0A: 'es-AR', + 0x400A: 'es-BO', + 0x340A: 'es-CL', + 0x240A: 'es-CO', + 0x140A: 'es-CR', + 0x1C0A: 'es-DO', + 0x300A: 'es-EC', + 0x440A: 'es-SV', + 0x100A: 'es-GT', + 0x480A: 'es-HN', + 0x080A: 'es-MX', + 0x4C0A: 'es-NI', + 0x180A: 'es-PA', + 0x3C0A: 'es-PY', + 0x280A: 'es-PE', + 0x500A: 'es-PR', + + // Microsoft has defined two different language codes for + // “Spanish with modern sorting” and “Spanish with traditional + // sorting”. This makes sense for collation APIs, and it would be + // possible to express this in BCP 47 language tags via Unicode + // extensions (eg., es-u-co-trad is Spanish with traditional + // sorting). However, for storing names in fonts, the distinction + // does not make sense, so we give “es” in both cases. + 0x0C0A: 'es', + 0x040A: 'es', + + 0x540A: 'es-US', + 0x380A: 'es-UY', + 0x200A: 'es-VE', + 0x081D: 'sv-FI', + 0x041D: 'sv', + 0x045A: 'syr', + 0x0428: 'tg', + 0x085F: 'tzm', + 0x0449: 'ta', + 0x0444: 'tt', + 0x044A: 'te', + 0x041E: 'th', + 0x0451: 'bo', + 0x041F: 'tr', + 0x0442: 'tk', + 0x0480: 'ug', + 0x0422: 'uk', + 0x042E: 'hsb', + 0x0420: 'ur', + 0x0843: 'uz-Cyrl', + 0x0443: 'uz', + 0x042A: 'vi', + 0x0452: 'cy', + 0x0488: 'wo', + 0x0485: 'sah', + 0x0478: 'ii', + 0x046A: 'yo' + }; + + // Returns a IETF BCP 47 language code, for example 'zh-Hant' + // for 'Chinese in the traditional script'. + function getLanguageCode(platformID, languageID, ltag) { + switch (platformID) { + case 0: // Unicode + if (languageID === 0xFFFF) { + return 'und'; + } else if (ltag) { + return ltag[languageID]; + } + + break; + + case 1: // Macintosh + return macLanguages[languageID]; + + case 3: // Windows + return windowsLanguages[languageID]; + } + + return undefined; + } + + var utf16 = 'utf-16'; + + // MacOS script ID → encoding. This table stores the default case, + // which can be overridden by macLanguageEncodings. + var macScriptEncodings = { + 0: 'macintosh', // smRoman + 1: 'x-mac-japanese', // smJapanese + 2: 'x-mac-chinesetrad', // smTradChinese + 3: 'x-mac-korean', // smKorean + 6: 'x-mac-greek', // smGreek + 7: 'x-mac-cyrillic', // smCyrillic + 9: 'x-mac-devanagai', // smDevanagari + 10: 'x-mac-gurmukhi', // smGurmukhi + 11: 'x-mac-gujarati', // smGujarati + 12: 'x-mac-oriya', // smOriya + 13: 'x-mac-bengali', // smBengali + 14: 'x-mac-tamil', // smTamil + 15: 'x-mac-telugu', // smTelugu + 16: 'x-mac-kannada', // smKannada + 17: 'x-mac-malayalam', // smMalayalam + 18: 'x-mac-sinhalese', // smSinhalese + 19: 'x-mac-burmese', // smBurmese + 20: 'x-mac-khmer', // smKhmer + 21: 'x-mac-thai', // smThai + 22: 'x-mac-lao', // smLao + 23: 'x-mac-georgian', // smGeorgian + 24: 'x-mac-armenian', // smArmenian + 25: 'x-mac-chinesesimp', // smSimpChinese + 26: 'x-mac-tibetan', // smTibetan + 27: 'x-mac-mongolian', // smMongolian + 28: 'x-mac-ethiopic', // smEthiopic + 29: 'x-mac-ce', // smCentralEuroRoman + 30: 'x-mac-vietnamese', // smVietnamese + 31: 'x-mac-extarabic' // smExtArabic + }; + + // MacOS language ID → encoding. This table stores the exceptional + // cases, which override macScriptEncodings. For writing MacOS naming + // tables, we need to emit a MacOS script ID. Therefore, we cannot + // merge macScriptEncodings into macLanguageEncodings. + // + // http://unicode.org/Public/MAPPINGS/VENDORS/APPLE/Readme.txt + var macLanguageEncodings = { + 15: 'x-mac-icelandic', // langIcelandic + 17: 'x-mac-turkish', // langTurkish + 18: 'x-mac-croatian', // langCroatian + 24: 'x-mac-ce', // langLithuanian + 25: 'x-mac-ce', // langPolish + 26: 'x-mac-ce', // langHungarian + 27: 'x-mac-ce', // langEstonian + 28: 'x-mac-ce', // langLatvian + 30: 'x-mac-icelandic', // langFaroese + 37: 'x-mac-romanian', // langRomanian + 38: 'x-mac-ce', // langCzech + 39: 'x-mac-ce', // langSlovak + 40: 'x-mac-ce', // langSlovenian + 143: 'x-mac-inuit', // langInuktitut + 146: 'x-mac-gaelic' // langIrishGaelicScript + }; + + function getEncoding(platformID, encodingID, languageID) { + switch (platformID) { + case 0: // Unicode + return utf16; + + case 1: // Apple Macintosh + return macLanguageEncodings[languageID] || macScriptEncodings[encodingID]; + + case 3: // Microsoft Windows + if (encodingID === 1 || encodingID === 10) { + return utf16; + } + + break; + } + + return undefined; + } + + // Parse the naming `name` table. + // FIXME: Format 1 additional fields are not supported yet. + // ltag is the content of the `ltag' table, such as ['en', 'zh-Hans', 'de-CH-1904']. + function parseNameTable(data, start, ltag) { + var name = {}; + var p = new parse.Parser(data, start); + var format = p.parseUShort(); + var count = p.parseUShort(); + var stringOffset = p.offset + p.parseUShort(); + for (var i = 0; i < count; i++) { + var platformID = p.parseUShort(); + var encodingID = p.parseUShort(); + var languageID = p.parseUShort(); + var nameID = p.parseUShort(); + var property = nameTableNames[nameID] || nameID; + var byteLength = p.parseUShort(); + var offset = p.parseUShort(); + var language = getLanguageCode(platformID, languageID, ltag); + var encoding = getEncoding(platformID, encodingID, languageID); + if (encoding !== undefined && language !== undefined) { + var text = (void 0); + if (encoding === utf16) { + text = decode.UTF16(data, stringOffset + offset, byteLength); + } else { + text = decode.MACSTRING(data, stringOffset + offset, byteLength, encoding); + } + + if (text) { + var translations = name[property]; + if (translations === undefined) { + translations = name[property] = {}; + } + + translations[language] = text; + } + } + } + + var langTagCount = 0; + if (format === 1) { + // FIXME: Also handle Microsoft's 'name' table 1. + langTagCount = p.parseUShort(); + } + + return name; + } + + // {23: 'foo'} → {'foo': 23} + // ['bar', 'baz'] → {'bar': 0, 'baz': 1} + function reverseDict(dict) { + var result = {}; + for (var key in dict) { + result[dict[key]] = parseInt(key); + } + + return result; + } + + function makeNameRecord(platformID, encodingID, languageID, nameID, length, offset) { + return new table.Record('NameRecord', [ + {name: 'platformID', type: 'USHORT', value: platformID}, + {name: 'encodingID', type: 'USHORT', value: encodingID}, + {name: 'languageID', type: 'USHORT', value: languageID}, + {name: 'nameID', type: 'USHORT', value: nameID}, + {name: 'length', type: 'USHORT', value: length}, + {name: 'offset', type: 'USHORT', value: offset} + ]); + } + + // Finds the position of needle in haystack, or -1 if not there. + // Like String.indexOf(), but for arrays. + function findSubArray(needle, haystack) { + var needleLength = needle.length; + var limit = haystack.length - needleLength + 1; + + loop: + for (var pos = 0; pos < limit; pos++) { + for (; pos < limit; pos++) { + for (var k = 0; k < needleLength; k++) { + if (haystack[pos + k] !== needle[k]) { + continue loop; + } + } + + return pos; + } + } + + return -1; + } + + function addStringToPool(s, pool) { + var offset = findSubArray(s, pool); + if (offset < 0) { + offset = pool.length; + var i = 0; + var len = s.length; + for (; i < len; ++i) { + pool.push(s[i]); + } + + } + + return offset; + } + + function makeNameTable(names, ltag) { + var nameID; + var nameIDs = []; + + var namesWithNumericKeys = {}; + var nameTableIds = reverseDict(nameTableNames); + for (var key in names) { + var id = nameTableIds[key]; + if (id === undefined) { + id = key; + } + + nameID = parseInt(id); + + if (isNaN(nameID)) { + throw new Error('Name table entry "' + key + '" does not exist, see nameTableNames for complete list.'); + } + + namesWithNumericKeys[nameID] = names[key]; + nameIDs.push(nameID); + } + + var macLanguageIds = reverseDict(macLanguages); + var windowsLanguageIds = reverseDict(windowsLanguages); + + var nameRecords = []; + var stringPool = []; + + for (var i = 0; i < nameIDs.length; i++) { + nameID = nameIDs[i]; + var translations = namesWithNumericKeys[nameID]; + for (var lang in translations) { + var text = translations[lang]; + + // For MacOS, we try to emit the name in the form that was introduced + // in the initial version of the TrueType spec (in the late 1980s). + // However, this can fail for various reasons: the requested BCP 47 + // language code might not have an old-style Mac equivalent; + // we might not have a codec for the needed character encoding; + // or the name might contain characters that cannot be expressed + // in the old-style Macintosh encoding. In case of failure, we emit + // the name in a more modern fashion (Unicode encoding with BCP 47 + // language tags) that is recognized by MacOS 10.5, released in 2009. + // If fonts were only read by operating systems, we could simply + // emit all names in the modern form; this would be much easier. + // However, there are many applications and libraries that read + // 'name' tables directly, and these will usually only recognize + // the ancient form (silently skipping the unrecognized names). + var macPlatform = 1; // Macintosh + var macLanguage = macLanguageIds[lang]; + var macScript = macLanguageToScript[macLanguage]; + var macEncoding = getEncoding(macPlatform, macScript, macLanguage); + var macName = encode.MACSTRING(text, macEncoding); + if (macName === undefined) { + macPlatform = 0; // Unicode + macLanguage = ltag.indexOf(lang); + if (macLanguage < 0) { + macLanguage = ltag.length; + ltag.push(lang); + } + + macScript = 4; // Unicode 2.0 and later + macName = encode.UTF16(text); + } + + var macNameOffset = addStringToPool(macName, stringPool); + nameRecords.push(makeNameRecord(macPlatform, macScript, macLanguage, + nameID, macName.length, macNameOffset)); + + var winLanguage = windowsLanguageIds[lang]; + if (winLanguage !== undefined) { + var winName = encode.UTF16(text); + var winNameOffset = addStringToPool(winName, stringPool); + nameRecords.push(makeNameRecord(3, 1, winLanguage, + nameID, winName.length, winNameOffset)); + } + } + } + + nameRecords.sort(function(a, b) { + return ((a.platformID - b.platformID) || + (a.encodingID - b.encodingID) || + (a.languageID - b.languageID) || + (a.nameID - b.nameID)); + }); + + var t = new table.Table('name', [ + {name: 'format', type: 'USHORT', value: 0}, + {name: 'count', type: 'USHORT', value: nameRecords.length}, + {name: 'stringOffset', type: 'USHORT', value: 6 + nameRecords.length * 12} + ]); + + for (var r = 0; r < nameRecords.length; r++) { + t.fields.push({name: 'record_' + r, type: 'RECORD', value: nameRecords[r]}); + } + + t.fields.push({name: 'strings', type: 'LITERAL', value: stringPool}); + return t; + } + + var _name = { parse: parseNameTable, make: makeNameTable }; + + // The `OS/2` table contains metrics required in OpenType fonts. + + var unicodeRanges = [ + {begin: 0x0000, end: 0x007F}, // Basic Latin + {begin: 0x0080, end: 0x00FF}, // Latin-1 Supplement + {begin: 0x0100, end: 0x017F}, // Latin Extended-A + {begin: 0x0180, end: 0x024F}, // Latin Extended-B + {begin: 0x0250, end: 0x02AF}, // IPA Extensions + {begin: 0x02B0, end: 0x02FF}, // Spacing Modifier Letters + {begin: 0x0300, end: 0x036F}, // Combining Diacritical Marks + {begin: 0x0370, end: 0x03FF}, // Greek and Coptic + {begin: 0x2C80, end: 0x2CFF}, // Coptic + {begin: 0x0400, end: 0x04FF}, // Cyrillic + {begin: 0x0530, end: 0x058F}, // Armenian + {begin: 0x0590, end: 0x05FF}, // Hebrew + {begin: 0xA500, end: 0xA63F}, // Vai + {begin: 0x0600, end: 0x06FF}, // Arabic + {begin: 0x07C0, end: 0x07FF}, // NKo + {begin: 0x0900, end: 0x097F}, // Devanagari + {begin: 0x0980, end: 0x09FF}, // Bengali + {begin: 0x0A00, end: 0x0A7F}, // Gurmukhi + {begin: 0x0A80, end: 0x0AFF}, // Gujarati + {begin: 0x0B00, end: 0x0B7F}, // Oriya + {begin: 0x0B80, end: 0x0BFF}, // Tamil + {begin: 0x0C00, end: 0x0C7F}, // Telugu + {begin: 0x0C80, end: 0x0CFF}, // Kannada + {begin: 0x0D00, end: 0x0D7F}, // Malayalam + {begin: 0x0E00, end: 0x0E7F}, // Thai + {begin: 0x0E80, end: 0x0EFF}, // Lao + {begin: 0x10A0, end: 0x10FF}, // Georgian + {begin: 0x1B00, end: 0x1B7F}, // Balinese + {begin: 0x1100, end: 0x11FF}, // Hangul Jamo + {begin: 0x1E00, end: 0x1EFF}, // Latin Extended Additional + {begin: 0x1F00, end: 0x1FFF}, // Greek Extended + {begin: 0x2000, end: 0x206F}, // General Punctuation + {begin: 0x2070, end: 0x209F}, // Superscripts And Subscripts + {begin: 0x20A0, end: 0x20CF}, // Currency Symbol + {begin: 0x20D0, end: 0x20FF}, // Combining Diacritical Marks For Symbols + {begin: 0x2100, end: 0x214F}, // Letterlike Symbols + {begin: 0x2150, end: 0x218F}, // Number Forms + {begin: 0x2190, end: 0x21FF}, // Arrows + {begin: 0x2200, end: 0x22FF}, // Mathematical Operators + {begin: 0x2300, end: 0x23FF}, // Miscellaneous Technical + {begin: 0x2400, end: 0x243F}, // Control Pictures + {begin: 0x2440, end: 0x245F}, // Optical Character Recognition + {begin: 0x2460, end: 0x24FF}, // Enclosed Alphanumerics + {begin: 0x2500, end: 0x257F}, // Box Drawing + {begin: 0x2580, end: 0x259F}, // Block Elements + {begin: 0x25A0, end: 0x25FF}, // Geometric Shapes + {begin: 0x2600, end: 0x26FF}, // Miscellaneous Symbols + {begin: 0x2700, end: 0x27BF}, // Dingbats + {begin: 0x3000, end: 0x303F}, // CJK Symbols And Punctuation + {begin: 0x3040, end: 0x309F}, // Hiragana + {begin: 0x30A0, end: 0x30FF}, // Katakana + {begin: 0x3100, end: 0x312F}, // Bopomofo + {begin: 0x3130, end: 0x318F}, // Hangul Compatibility Jamo + {begin: 0xA840, end: 0xA87F}, // Phags-pa + {begin: 0x3200, end: 0x32FF}, // Enclosed CJK Letters And Months + {begin: 0x3300, end: 0x33FF}, // CJK Compatibility + {begin: 0xAC00, end: 0xD7AF}, // Hangul Syllables + {begin: 0xD800, end: 0xDFFF}, // Non-Plane 0 * + {begin: 0x10900, end: 0x1091F}, // Phoenicia + {begin: 0x4E00, end: 0x9FFF}, // CJK Unified Ideographs + {begin: 0xE000, end: 0xF8FF}, // Private Use Area (plane 0) + {begin: 0x31C0, end: 0x31EF}, // CJK Strokes + {begin: 0xFB00, end: 0xFB4F}, // Alphabetic Presentation Forms + {begin: 0xFB50, end: 0xFDFF}, // Arabic Presentation Forms-A + {begin: 0xFE20, end: 0xFE2F}, // Combining Half Marks + {begin: 0xFE10, end: 0xFE1F}, // Vertical Forms + {begin: 0xFE50, end: 0xFE6F}, // Small Form Variants + {begin: 0xFE70, end: 0xFEFF}, // Arabic Presentation Forms-B + {begin: 0xFF00, end: 0xFFEF}, // Halfwidth And Fullwidth Forms + {begin: 0xFFF0, end: 0xFFFF}, // Specials + {begin: 0x0F00, end: 0x0FFF}, // Tibetan + {begin: 0x0700, end: 0x074F}, // Syriac + {begin: 0x0780, end: 0x07BF}, // Thaana + {begin: 0x0D80, end: 0x0DFF}, // Sinhala + {begin: 0x1000, end: 0x109F}, // Myanmar + {begin: 0x1200, end: 0x137F}, // Ethiopic + {begin: 0x13A0, end: 0x13FF}, // Cherokee + {begin: 0x1400, end: 0x167F}, // Unified Canadian Aboriginal Syllabics + {begin: 0x1680, end: 0x169F}, // Ogham + {begin: 0x16A0, end: 0x16FF}, // Runic + {begin: 0x1780, end: 0x17FF}, // Khmer + {begin: 0x1800, end: 0x18AF}, // Mongolian + {begin: 0x2800, end: 0x28FF}, // Braille Patterns + {begin: 0xA000, end: 0xA48F}, // Yi Syllables + {begin: 0x1700, end: 0x171F}, // Tagalog + {begin: 0x10300, end: 0x1032F}, // Old Italic + {begin: 0x10330, end: 0x1034F}, // Gothic + {begin: 0x10400, end: 0x1044F}, // Deseret + {begin: 0x1D000, end: 0x1D0FF}, // Byzantine Musical Symbols + {begin: 0x1D400, end: 0x1D7FF}, // Mathematical Alphanumeric Symbols + {begin: 0xFF000, end: 0xFFFFD}, // Private Use (plane 15) + {begin: 0xFE00, end: 0xFE0F}, // Variation Selectors + {begin: 0xE0000, end: 0xE007F}, // Tags + {begin: 0x1900, end: 0x194F}, // Limbu + {begin: 0x1950, end: 0x197F}, // Tai Le + {begin: 0x1980, end: 0x19DF}, // New Tai Lue + {begin: 0x1A00, end: 0x1A1F}, // Buginese + {begin: 0x2C00, end: 0x2C5F}, // Glagolitic + {begin: 0x2D30, end: 0x2D7F}, // Tifinagh + {begin: 0x4DC0, end: 0x4DFF}, // Yijing Hexagram Symbols + {begin: 0xA800, end: 0xA82F}, // Syloti Nagri + {begin: 0x10000, end: 0x1007F}, // Linear B Syllabary + {begin: 0x10140, end: 0x1018F}, // Ancient Greek Numbers + {begin: 0x10380, end: 0x1039F}, // Ugaritic + {begin: 0x103A0, end: 0x103DF}, // Old Persian + {begin: 0x10450, end: 0x1047F}, // Shavian + {begin: 0x10480, end: 0x104AF}, // Osmanya + {begin: 0x10800, end: 0x1083F}, // Cypriot Syllabary + {begin: 0x10A00, end: 0x10A5F}, // Kharoshthi + {begin: 0x1D300, end: 0x1D35F}, // Tai Xuan Jing Symbols + {begin: 0x12000, end: 0x123FF}, // Cuneiform + {begin: 0x1D360, end: 0x1D37F}, // Counting Rod Numerals + {begin: 0x1B80, end: 0x1BBF}, // Sundanese + {begin: 0x1C00, end: 0x1C4F}, // Lepcha + {begin: 0x1C50, end: 0x1C7F}, // Ol Chiki + {begin: 0xA880, end: 0xA8DF}, // Saurashtra + {begin: 0xA900, end: 0xA92F}, // Kayah Li + {begin: 0xA930, end: 0xA95F}, // Rejang + {begin: 0xAA00, end: 0xAA5F}, // Cham + {begin: 0x10190, end: 0x101CF}, // Ancient Symbols + {begin: 0x101D0, end: 0x101FF}, // Phaistos Disc + {begin: 0x102A0, end: 0x102DF}, // Carian + {begin: 0x1F030, end: 0x1F09F} // Domino Tiles + ]; + + function getUnicodeRange(unicode) { + for (var i = 0; i < unicodeRanges.length; i += 1) { + var range = unicodeRanges[i]; + if (unicode >= range.begin && unicode < range.end) { + return i; + } + } + + return -1; + } + + // Parse the OS/2 and Windows metrics `OS/2` table + function parseOS2Table(data, start) { + var os2 = {}; + var p = new parse.Parser(data, start); + os2.version = p.parseUShort(); + os2.xAvgCharWidth = p.parseShort(); + os2.usWeightClass = p.parseUShort(); + os2.usWidthClass = p.parseUShort(); + os2.fsType = p.parseUShort(); + os2.ySubscriptXSize = p.parseShort(); + os2.ySubscriptYSize = p.parseShort(); + os2.ySubscriptXOffset = p.parseShort(); + os2.ySubscriptYOffset = p.parseShort(); + os2.ySuperscriptXSize = p.parseShort(); + os2.ySuperscriptYSize = p.parseShort(); + os2.ySuperscriptXOffset = p.parseShort(); + os2.ySuperscriptYOffset = p.parseShort(); + os2.yStrikeoutSize = p.parseShort(); + os2.yStrikeoutPosition = p.parseShort(); + os2.sFamilyClass = p.parseShort(); + os2.panose = []; + for (var i = 0; i < 10; i++) { + os2.panose[i] = p.parseByte(); + } + + os2.ulUnicodeRange1 = p.parseULong(); + os2.ulUnicodeRange2 = p.parseULong(); + os2.ulUnicodeRange3 = p.parseULong(); + os2.ulUnicodeRange4 = p.parseULong(); + os2.achVendID = String.fromCharCode(p.parseByte(), p.parseByte(), p.parseByte(), p.parseByte()); + os2.fsSelection = p.parseUShort(); + os2.usFirstCharIndex = p.parseUShort(); + os2.usLastCharIndex = p.parseUShort(); + os2.sTypoAscender = p.parseShort(); + os2.sTypoDescender = p.parseShort(); + os2.sTypoLineGap = p.parseShort(); + os2.usWinAscent = p.parseUShort(); + os2.usWinDescent = p.parseUShort(); + if (os2.version >= 1) { + os2.ulCodePageRange1 = p.parseULong(); + os2.ulCodePageRange2 = p.parseULong(); + } + + if (os2.version >= 2) { + os2.sxHeight = p.parseShort(); + os2.sCapHeight = p.parseShort(); + os2.usDefaultChar = p.parseUShort(); + os2.usBreakChar = p.parseUShort(); + os2.usMaxContent = p.parseUShort(); + } + + return os2; + } + + function makeOS2Table(options) { + return new table.Table('OS/2', [ + {name: 'version', type: 'USHORT', value: 0x0003}, + {name: 'xAvgCharWidth', type: 'SHORT', value: 0}, + {name: 'usWeightClass', type: 'USHORT', value: 0}, + {name: 'usWidthClass', type: 'USHORT', value: 0}, + {name: 'fsType', type: 'USHORT', value: 0}, + {name: 'ySubscriptXSize', type: 'SHORT', value: 650}, + {name: 'ySubscriptYSize', type: 'SHORT', value: 699}, + {name: 'ySubscriptXOffset', type: 'SHORT', value: 0}, + {name: 'ySubscriptYOffset', type: 'SHORT', value: 140}, + {name: 'ySuperscriptXSize', type: 'SHORT', value: 650}, + {name: 'ySuperscriptYSize', type: 'SHORT', value: 699}, + {name: 'ySuperscriptXOffset', type: 'SHORT', value: 0}, + {name: 'ySuperscriptYOffset', type: 'SHORT', value: 479}, + {name: 'yStrikeoutSize', type: 'SHORT', value: 49}, + {name: 'yStrikeoutPosition', type: 'SHORT', value: 258}, + {name: 'sFamilyClass', type: 'SHORT', value: 0}, + {name: 'bFamilyType', type: 'BYTE', value: 0}, + {name: 'bSerifStyle', type: 'BYTE', value: 0}, + {name: 'bWeight', type: 'BYTE', value: 0}, + {name: 'bProportion', type: 'BYTE', value: 0}, + {name: 'bContrast', type: 'BYTE', value: 0}, + {name: 'bStrokeVariation', type: 'BYTE', value: 0}, + {name: 'bArmStyle', type: 'BYTE', value: 0}, + {name: 'bLetterform', type: 'BYTE', value: 0}, + {name: 'bMidline', type: 'BYTE', value: 0}, + {name: 'bXHeight', type: 'BYTE', value: 0}, + {name: 'ulUnicodeRange1', type: 'ULONG', value: 0}, + {name: 'ulUnicodeRange2', type: 'ULONG', value: 0}, + {name: 'ulUnicodeRange3', type: 'ULONG', value: 0}, + {name: 'ulUnicodeRange4', type: 'ULONG', value: 0}, + {name: 'achVendID', type: 'CHARARRAY', value: 'XXXX'}, + {name: 'fsSelection', type: 'USHORT', value: 0}, + {name: 'usFirstCharIndex', type: 'USHORT', value: 0}, + {name: 'usLastCharIndex', type: 'USHORT', value: 0}, + {name: 'sTypoAscender', type: 'SHORT', value: 0}, + {name: 'sTypoDescender', type: 'SHORT', value: 0}, + {name: 'sTypoLineGap', type: 'SHORT', value: 0}, + {name: 'usWinAscent', type: 'USHORT', value: 0}, + {name: 'usWinDescent', type: 'USHORT', value: 0}, + {name: 'ulCodePageRange1', type: 'ULONG', value: 0}, + {name: 'ulCodePageRange2', type: 'ULONG', value: 0}, + {name: 'sxHeight', type: 'SHORT', value: 0}, + {name: 'sCapHeight', type: 'SHORT', value: 0}, + {name: 'usDefaultChar', type: 'USHORT', value: 0}, + {name: 'usBreakChar', type: 'USHORT', value: 0}, + {name: 'usMaxContext', type: 'USHORT', value: 0} + ], options); + } + + var os2 = { parse: parseOS2Table, make: makeOS2Table, unicodeRanges: unicodeRanges, getUnicodeRange: getUnicodeRange }; + + // The `post` table stores additional PostScript information, such as glyph names. + + // Parse the PostScript `post` table + function parsePostTable(data, start) { + var post = {}; + var p = new parse.Parser(data, start); + post.version = p.parseVersion(); + post.italicAngle = p.parseFixed(); + post.underlinePosition = p.parseShort(); + post.underlineThickness = p.parseShort(); + post.isFixedPitch = p.parseULong(); + post.minMemType42 = p.parseULong(); + post.maxMemType42 = p.parseULong(); + post.minMemType1 = p.parseULong(); + post.maxMemType1 = p.parseULong(); + switch (post.version) { + case 1: + post.names = standardNames.slice(); + break; + case 2: + post.numberOfGlyphs = p.parseUShort(); + post.glyphNameIndex = new Array(post.numberOfGlyphs); + for (var i = 0; i < post.numberOfGlyphs; i++) { + post.glyphNameIndex[i] = p.parseUShort(); + } + + post.names = []; + for (var i$1 = 0; i$1 < post.numberOfGlyphs; i$1++) { + if (post.glyphNameIndex[i$1] >= standardNames.length) { + var nameLength = p.parseChar(); + post.names.push(p.parseString(nameLength)); + } + } + + break; + case 2.5: + post.numberOfGlyphs = p.parseUShort(); + post.offset = new Array(post.numberOfGlyphs); + for (var i$2 = 0; i$2 < post.numberOfGlyphs; i$2++) { + post.offset[i$2] = p.parseChar(); + } + + break; + } + return post; + } + + function makePostTable() { + return new table.Table('post', [ + {name: 'version', type: 'FIXED', value: 0x00030000}, + {name: 'italicAngle', type: 'FIXED', value: 0}, + {name: 'underlinePosition', type: 'FWORD', value: 0}, + {name: 'underlineThickness', type: 'FWORD', value: 0}, + {name: 'isFixedPitch', type: 'ULONG', value: 0}, + {name: 'minMemType42', type: 'ULONG', value: 0}, + {name: 'maxMemType42', type: 'ULONG', value: 0}, + {name: 'minMemType1', type: 'ULONG', value: 0}, + {name: 'maxMemType1', type: 'ULONG', value: 0} + ]); + } + + var post = { parse: parsePostTable, make: makePostTable }; + + // The `GSUB` table contains ligatures, among other things. + + var subtableParsers = new Array(9); // subtableParsers[0] is unused + + // https://www.microsoft.com/typography/OTSPEC/GSUB.htm#SS + subtableParsers[1] = function parseLookup1() { + var start = this.offset + this.relativeOffset; + var substFormat = this.parseUShort(); + if (substFormat === 1) { + return { + substFormat: 1, + coverage: this.parsePointer(Parser.coverage), + deltaGlyphId: this.parseUShort() + }; + } else if (substFormat === 2) { + return { + substFormat: 2, + coverage: this.parsePointer(Parser.coverage), + substitute: this.parseOffset16List() + }; + } + check.assert(false, '0x' + start.toString(16) + ': lookup type 1 format must be 1 or 2.'); + }; + + // https://www.microsoft.com/typography/OTSPEC/GSUB.htm#MS + subtableParsers[2] = function parseLookup2() { + var substFormat = this.parseUShort(); + check.argument(substFormat === 1, 'GSUB Multiple Substitution Subtable identifier-format must be 1'); + return { + substFormat: substFormat, + coverage: this.parsePointer(Parser.coverage), + sequences: this.parseListOfLists() + }; + }; + + // https://www.microsoft.com/typography/OTSPEC/GSUB.htm#AS + subtableParsers[3] = function parseLookup3() { + var substFormat = this.parseUShort(); + check.argument(substFormat === 1, 'GSUB Alternate Substitution Subtable identifier-format must be 1'); + return { + substFormat: substFormat, + coverage: this.parsePointer(Parser.coverage), + alternateSets: this.parseListOfLists() + }; + }; + + // https://www.microsoft.com/typography/OTSPEC/GSUB.htm#LS + subtableParsers[4] = function parseLookup4() { + var substFormat = this.parseUShort(); + check.argument(substFormat === 1, 'GSUB ligature table identifier-format must be 1'); + return { + substFormat: substFormat, + coverage: this.parsePointer(Parser.coverage), + ligatureSets: this.parseListOfLists(function() { + return { + ligGlyph: this.parseUShort(), + components: this.parseUShortList(this.parseUShort() - 1) + }; + }) + }; + }; + + var lookupRecordDesc = { + sequenceIndex: Parser.uShort, + lookupListIndex: Parser.uShort + }; + + // https://www.microsoft.com/typography/OTSPEC/GSUB.htm#CSF + subtableParsers[5] = function parseLookup5() { + var start = this.offset + this.relativeOffset; + var substFormat = this.parseUShort(); + + if (substFormat === 1) { + return { + substFormat: substFormat, + coverage: this.parsePointer(Parser.coverage), + ruleSets: this.parseListOfLists(function() { + var glyphCount = this.parseUShort(); + var substCount = this.parseUShort(); + return { + input: this.parseUShortList(glyphCount - 1), + lookupRecords: this.parseRecordList(substCount, lookupRecordDesc) + }; + }) + }; + } else if (substFormat === 2) { + return { + substFormat: substFormat, + coverage: this.parsePointer(Parser.coverage), + classDef: this.parsePointer(Parser.classDef), + classSets: this.parseListOfLists(function() { + var glyphCount = this.parseUShort(); + var substCount = this.parseUShort(); + return { + classes: this.parseUShortList(glyphCount - 1), + lookupRecords: this.parseRecordList(substCount, lookupRecordDesc) + }; + }) + }; + } else if (substFormat === 3) { + var glyphCount = this.parseUShort(); + var substCount = this.parseUShort(); + return { + substFormat: substFormat, + coverages: this.parseList(glyphCount, Parser.pointer(Parser.coverage)), + lookupRecords: this.parseRecordList(substCount, lookupRecordDesc) + }; + } + check.assert(false, '0x' + start.toString(16) + ': lookup type 5 format must be 1, 2 or 3.'); + }; + + // https://www.microsoft.com/typography/OTSPEC/GSUB.htm#CC + subtableParsers[6] = function parseLookup6() { + var start = this.offset + this.relativeOffset; + var substFormat = this.parseUShort(); + if (substFormat === 1) { + return { + substFormat: 1, + coverage: this.parsePointer(Parser.coverage), + chainRuleSets: this.parseListOfLists(function() { + return { + backtrack: this.parseUShortList(), + input: this.parseUShortList(this.parseShort() - 1), + lookahead: this.parseUShortList(), + lookupRecords: this.parseRecordList(lookupRecordDesc) + }; + }) + }; + } else if (substFormat === 2) { + return { + substFormat: 2, + coverage: this.parsePointer(Parser.coverage), + backtrackClassDef: this.parsePointer(Parser.classDef), + inputClassDef: this.parsePointer(Parser.classDef), + lookaheadClassDef: this.parsePointer(Parser.classDef), + chainClassSet: this.parseListOfLists(function() { + return { + backtrack: this.parseUShortList(), + input: this.parseUShortList(this.parseShort() - 1), + lookahead: this.parseUShortList(), + lookupRecords: this.parseRecordList(lookupRecordDesc) + }; + }) + }; + } else if (substFormat === 3) { + return { + substFormat: 3, + backtrackCoverage: this.parseList(Parser.pointer(Parser.coverage)), + inputCoverage: this.parseList(Parser.pointer(Parser.coverage)), + lookaheadCoverage: this.parseList(Parser.pointer(Parser.coverage)), + lookupRecords: this.parseRecordList(lookupRecordDesc) + }; + } + check.assert(false, '0x' + start.toString(16) + ': lookup type 6 format must be 1, 2 or 3.'); + }; + + // https://www.microsoft.com/typography/OTSPEC/GSUB.htm#ES + subtableParsers[7] = function parseLookup7() { + // Extension Substitution subtable + var substFormat = this.parseUShort(); + check.argument(substFormat === 1, 'GSUB Extension Substitution subtable identifier-format must be 1'); + var extensionLookupType = this.parseUShort(); + var extensionParser = new Parser(this.data, this.offset + this.parseULong()); + return { + substFormat: 1, + lookupType: extensionLookupType, + extension: subtableParsers[extensionLookupType].call(extensionParser) + }; + }; + + // https://www.microsoft.com/typography/OTSPEC/GSUB.htm#RCCS + subtableParsers[8] = function parseLookup8() { + var substFormat = this.parseUShort(); + check.argument(substFormat === 1, 'GSUB Reverse Chaining Contextual Single Substitution Subtable identifier-format must be 1'); + return { + substFormat: substFormat, + coverage: this.parsePointer(Parser.coverage), + backtrackCoverage: this.parseList(Parser.pointer(Parser.coverage)), + lookaheadCoverage: this.parseList(Parser.pointer(Parser.coverage)), + substitutes: this.parseUShortList() + }; + }; + + // https://www.microsoft.com/typography/OTSPEC/gsub.htm + function parseGsubTable(data, start) { + start = start || 0; + var p = new Parser(data, start); + var tableVersion = p.parseVersion(1); + check.argument(tableVersion === 1 || tableVersion === 1.1, 'Unsupported GSUB table version.'); + if (tableVersion === 1) { + return { + version: tableVersion, + scripts: p.parseScriptList(), + features: p.parseFeatureList(), + lookups: p.parseLookupList(subtableParsers) + }; + } else { + return { + version: tableVersion, + scripts: p.parseScriptList(), + features: p.parseFeatureList(), + lookups: p.parseLookupList(subtableParsers), + variations: p.parseFeatureVariationsList() + }; + } + + } + + // GSUB Writing ////////////////////////////////////////////// + var subtableMakers = new Array(9); + + subtableMakers[1] = function makeLookup1(subtable) { + if (subtable.substFormat === 1) { + return new table.Table('substitutionTable', [ + {name: 'substFormat', type: 'USHORT', value: 1}, + {name: 'coverage', type: 'TABLE', value: new table.Coverage(subtable.coverage)}, + {name: 'deltaGlyphID', type: 'USHORT', value: subtable.deltaGlyphId} + ]); + } else { + return new table.Table('substitutionTable', [ + {name: 'substFormat', type: 'USHORT', value: 2}, + {name: 'coverage', type: 'TABLE', value: new table.Coverage(subtable.coverage)} + ].concat(table.ushortList('substitute', subtable.substitute))); + } + }; + + subtableMakers[2] = function makeLookup2(subtable) { + check.assert(subtable.substFormat === 1, 'Lookup type 2 substFormat must be 1.'); + return new table.Table('substitutionTable', [ + {name: 'substFormat', type: 'USHORT', value: 1}, + {name: 'coverage', type: 'TABLE', value: new table.Coverage(subtable.coverage)} + ].concat(table.tableList('seqSet', subtable.sequences, function(sequenceSet) { + return new table.Table('sequenceSetTable', table.ushortList('sequence', sequenceSet)); + }))); + }; + + subtableMakers[3] = function makeLookup3(subtable) { + check.assert(subtable.substFormat === 1, 'Lookup type 3 substFormat must be 1.'); + return new table.Table('substitutionTable', [ + {name: 'substFormat', type: 'USHORT', value: 1}, + {name: 'coverage', type: 'TABLE', value: new table.Coverage(subtable.coverage)} + ].concat(table.tableList('altSet', subtable.alternateSets, function(alternateSet) { + return new table.Table('alternateSetTable', table.ushortList('alternate', alternateSet)); + }))); + }; + + subtableMakers[4] = function makeLookup4(subtable) { + check.assert(subtable.substFormat === 1, 'Lookup type 4 substFormat must be 1.'); + return new table.Table('substitutionTable', [ + {name: 'substFormat', type: 'USHORT', value: 1}, + {name: 'coverage', type: 'TABLE', value: new table.Coverage(subtable.coverage)} + ].concat(table.tableList('ligSet', subtable.ligatureSets, function(ligatureSet) { + return new table.Table('ligatureSetTable', table.tableList('ligature', ligatureSet, function(ligature) { + return new table.Table('ligatureTable', + [{name: 'ligGlyph', type: 'USHORT', value: ligature.ligGlyph}] + .concat(table.ushortList('component', ligature.components, ligature.components.length + 1)) + ); + })); + }))); + }; + + subtableMakers[6] = function makeLookup6(subtable) { + if (subtable.substFormat === 1) { + var returnTable = new table.Table('chainContextTable', [ + {name: 'substFormat', type: 'USHORT', value: subtable.substFormat}, + {name: 'coverage', type: 'TABLE', value: new table.Coverage(subtable.coverage)} + ].concat(table.tableList('chainRuleSet', subtable.chainRuleSets, function(chainRuleSet) { + return new table.Table('chainRuleSetTable', table.tableList('chainRule', chainRuleSet, function(chainRule) { + var tableData = table.ushortList('backtrackGlyph', chainRule.backtrack, chainRule.backtrack.length) + .concat(table.ushortList('inputGlyph', chainRule.input, chainRule.input.length + 1)) + .concat(table.ushortList('lookaheadGlyph', chainRule.lookahead, chainRule.lookahead.length)) + .concat(table.ushortList('substitution', [], chainRule.lookupRecords.length)); + + chainRule.lookupRecords.forEach(function (record, i) { + tableData = tableData + .concat({name: 'sequenceIndex' + i, type: 'USHORT', value: record.sequenceIndex}) + .concat({name: 'lookupListIndex' + i, type: 'USHORT', value: record.lookupListIndex}); + }); + return new table.Table('chainRuleTable', tableData); + })); + }))); + return returnTable; + } else if (subtable.substFormat === 2) { + check.assert(false, 'lookup type 6 format 2 is not yet supported.'); + } else if (subtable.substFormat === 3) { + var tableData = [ + {name: 'substFormat', type: 'USHORT', value: subtable.substFormat} ]; + + tableData.push({name: 'backtrackGlyphCount', type: 'USHORT', value: subtable.backtrackCoverage.length}); + subtable.backtrackCoverage.forEach(function (coverage, i) { + tableData.push({name: 'backtrackCoverage' + i, type: 'TABLE', value: new table.Coverage(coverage)}); + }); + tableData.push({name: 'inputGlyphCount', type: 'USHORT', value: subtable.inputCoverage.length}); + subtable.inputCoverage.forEach(function (coverage, i) { + tableData.push({name: 'inputCoverage' + i, type: 'TABLE', value: new table.Coverage(coverage)}); + }); + tableData.push({name: 'lookaheadGlyphCount', type: 'USHORT', value: subtable.lookaheadCoverage.length}); + subtable.lookaheadCoverage.forEach(function (coverage, i) { + tableData.push({name: 'lookaheadCoverage' + i, type: 'TABLE', value: new table.Coverage(coverage)}); + }); + + tableData.push({name: 'substitutionCount', type: 'USHORT', value: subtable.lookupRecords.length}); + subtable.lookupRecords.forEach(function (record, i) { + tableData = tableData + .concat({name: 'sequenceIndex' + i, type: 'USHORT', value: record.sequenceIndex}) + .concat({name: 'lookupListIndex' + i, type: 'USHORT', value: record.lookupListIndex}); + }); + + var returnTable$1 = new table.Table('chainContextTable', tableData); + + return returnTable$1; + } + + check.assert(false, 'lookup type 6 format must be 1, 2 or 3.'); + }; + + function makeGsubTable(gsub) { + return new table.Table('GSUB', [ + {name: 'version', type: 'ULONG', value: 0x10000}, + {name: 'scripts', type: 'TABLE', value: new table.ScriptList(gsub.scripts)}, + {name: 'features', type: 'TABLE', value: new table.FeatureList(gsub.features)}, + {name: 'lookups', type: 'TABLE', value: new table.LookupList(gsub.lookups, subtableMakers)} + ]); + } + + var gsub = { parse: parseGsubTable, make: makeGsubTable }; + + // The `GPOS` table contains kerning pairs, among other things. + + // Parse the metadata `meta` table. + // https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6meta.html + function parseMetaTable(data, start) { + var p = new parse.Parser(data, start); + var tableVersion = p.parseULong(); + check.argument(tableVersion === 1, 'Unsupported META table version.'); + p.parseULong(); // flags - currently unused and set to 0 + p.parseULong(); // tableOffset + var numDataMaps = p.parseULong(); + + var tags = {}; + for (var i = 0; i < numDataMaps; i++) { + var tag = p.parseTag(); + var dataOffset = p.parseULong(); + var dataLength = p.parseULong(); + var text = decode.UTF8(data, start + dataOffset, dataLength); + + tags[tag] = text; + } + return tags; + } + + function makeMetaTable(tags) { + var numTags = Object.keys(tags).length; + var stringPool = ''; + var stringPoolOffset = 16 + numTags * 12; + + var result = new table.Table('meta', [ + {name: 'version', type: 'ULONG', value: 1}, + {name: 'flags', type: 'ULONG', value: 0}, + {name: 'offset', type: 'ULONG', value: stringPoolOffset}, + {name: 'numTags', type: 'ULONG', value: numTags} + ]); + + for (var tag in tags) { + var pos = stringPool.length; + stringPool += tags[tag]; + + result.fields.push({name: 'tag ' + tag, type: 'TAG', value: tag}); + result.fields.push({name: 'offset ' + tag, type: 'ULONG', value: stringPoolOffset + pos}); + result.fields.push({name: 'length ' + tag, type: 'ULONG', value: tags[tag].length}); + } + + result.fields.push({name: 'stringPool', type: 'CHARARRAY', value: stringPool}); + + return result; + } + + var meta = { parse: parseMetaTable, make: makeMetaTable }; + + // The `sfnt` wrapper provides organization for the tables in the font. + + function log2(v) { + return Math.log(v) / Math.log(2) | 0; + } + + function computeCheckSum(bytes) { + while (bytes.length % 4 !== 0) { + bytes.push(0); + } + + var sum = 0; + for (var i = 0; i < bytes.length; i += 4) { + sum += (bytes[i] << 24) + + (bytes[i + 1] << 16) + + (bytes[i + 2] << 8) + + (bytes[i + 3]); + } + + sum %= Math.pow(2, 32); + return sum; + } + + function makeTableRecord(tag, checkSum, offset, length) { + return new table.Record('Table Record', [ + {name: 'tag', type: 'TAG', value: tag !== undefined ? tag : ''}, + {name: 'checkSum', type: 'ULONG', value: checkSum !== undefined ? checkSum : 0}, + {name: 'offset', type: 'ULONG', value: offset !== undefined ? offset : 0}, + {name: 'length', type: 'ULONG', value: length !== undefined ? length : 0} + ]); + } + + function makeSfntTable(tables) { + var sfnt = new table.Table('sfnt', [ + {name: 'version', type: 'TAG', value: 'OTTO'}, + {name: 'numTables', type: 'USHORT', value: 0}, + {name: 'searchRange', type: 'USHORT', value: 0}, + {name: 'entrySelector', type: 'USHORT', value: 0}, + {name: 'rangeShift', type: 'USHORT', value: 0} + ]); + sfnt.tables = tables; + sfnt.numTables = tables.length; + var highestPowerOf2 = Math.pow(2, log2(sfnt.numTables)); + sfnt.searchRange = 16 * highestPowerOf2; + sfnt.entrySelector = log2(highestPowerOf2); + sfnt.rangeShift = sfnt.numTables * 16 - sfnt.searchRange; + + var recordFields = []; + var tableFields = []; + + var offset = sfnt.sizeOf() + (makeTableRecord().sizeOf() * sfnt.numTables); + while (offset % 4 !== 0) { + offset += 1; + tableFields.push({name: 'padding', type: 'BYTE', value: 0}); + } + + for (var i = 0; i < tables.length; i += 1) { + var t = tables[i]; + check.argument(t.tableName.length === 4, 'Table name' + t.tableName + ' is invalid.'); + var tableLength = t.sizeOf(); + var tableRecord = makeTableRecord(t.tableName, computeCheckSum(t.encode()), offset, tableLength); + recordFields.push({name: tableRecord.tag + ' Table Record', type: 'RECORD', value: tableRecord}); + tableFields.push({name: t.tableName + ' table', type: 'RECORD', value: t}); + offset += tableLength; + check.argument(!isNaN(offset), 'Something went wrong calculating the offset.'); + while (offset % 4 !== 0) { + offset += 1; + tableFields.push({name: 'padding', type: 'BYTE', value: 0}); + } + } + + // Table records need to be sorted alphabetically. + recordFields.sort(function(r1, r2) { + if (r1.value.tag > r2.value.tag) { + return 1; + } else { + return -1; + } + }); + + sfnt.fields = sfnt.fields.concat(recordFields); + sfnt.fields = sfnt.fields.concat(tableFields); + return sfnt; + } + + // Get the metrics for a character. If the string has more than one character + // this function returns metrics for the first available character. + // You can provide optional fallback metrics if no characters are available. + function metricsForChar(font, chars, notFoundMetrics) { + for (var i = 0; i < chars.length; i += 1) { + var glyphIndex = font.charToGlyphIndex(chars[i]); + if (glyphIndex > 0) { + var glyph = font.glyphs.get(glyphIndex); + return glyph.getMetrics(); + } + } + + return notFoundMetrics; + } + + function average(vs) { + var sum = 0; + for (var i = 0; i < vs.length; i += 1) { + sum += vs[i]; + } + + return sum / vs.length; + } + + // Convert the font object to a SFNT data structure. + // This structure contains all the necessary tables and metadata to create a binary OTF file. + function fontToSfntTable(font) { + var xMins = []; + var yMins = []; + var xMaxs = []; + var yMaxs = []; + var advanceWidths = []; + var leftSideBearings = []; + var rightSideBearings = []; + var firstCharIndex; + var lastCharIndex = 0; + var ulUnicodeRange1 = 0; + var ulUnicodeRange2 = 0; + var ulUnicodeRange3 = 0; + var ulUnicodeRange4 = 0; + + for (var i = 0; i < font.glyphs.length; i += 1) { + var glyph = font.glyphs.get(i); + var unicode = glyph.unicode | 0; + + if (isNaN(glyph.advanceWidth)) { + throw new Error('Glyph ' + glyph.name + ' (' + i + '): advanceWidth is not a number.'); + } + + if (firstCharIndex > unicode || firstCharIndex === undefined) { + // ignore .notdef char + if (unicode > 0) { + firstCharIndex = unicode; + } + } + + if (lastCharIndex < unicode) { + lastCharIndex = unicode; + } + + var position = os2.getUnicodeRange(unicode); + if (position < 32) { + ulUnicodeRange1 |= 1 << position; + } else if (position < 64) { + ulUnicodeRange2 |= 1 << position - 32; + } else if (position < 96) { + ulUnicodeRange3 |= 1 << position - 64; + } else if (position < 123) { + ulUnicodeRange4 |= 1 << position - 96; + } else { + throw new Error('Unicode ranges bits > 123 are reserved for internal usage'); + } + // Skip non-important characters. + if (glyph.name === '.notdef') { continue; } + var metrics = glyph.getMetrics(); + xMins.push(metrics.xMin); + yMins.push(metrics.yMin); + xMaxs.push(metrics.xMax); + yMaxs.push(metrics.yMax); + leftSideBearings.push(metrics.leftSideBearing); + rightSideBearings.push(metrics.rightSideBearing); + advanceWidths.push(glyph.advanceWidth); + } + + var globals = { + xMin: Math.min.apply(null, xMins), + yMin: Math.min.apply(null, yMins), + xMax: Math.max.apply(null, xMaxs), + yMax: Math.max.apply(null, yMaxs), + advanceWidthMax: Math.max.apply(null, advanceWidths), + advanceWidthAvg: average(advanceWidths), + minLeftSideBearing: Math.min.apply(null, leftSideBearings), + maxLeftSideBearing: Math.max.apply(null, leftSideBearings), + minRightSideBearing: Math.min.apply(null, rightSideBearings) + }; + globals.ascender = font.ascender; + globals.descender = font.descender; + + var headTable = head.make({ + flags: 3, // 00000011 (baseline for font at y=0; left sidebearing point at x=0) + unitsPerEm: font.unitsPerEm, + xMin: globals.xMin, + yMin: globals.yMin, + xMax: globals.xMax, + yMax: globals.yMax, + lowestRecPPEM: 3, + createdTimestamp: font.createdTimestamp + }); + + var hheaTable = hhea.make({ + ascender: globals.ascender, + descender: globals.descender, + advanceWidthMax: globals.advanceWidthMax, + minLeftSideBearing: globals.minLeftSideBearing, + minRightSideBearing: globals.minRightSideBearing, + xMaxExtent: globals.maxLeftSideBearing + (globals.xMax - globals.xMin), + numberOfHMetrics: font.glyphs.length + }); + + var maxpTable = maxp.make(font.glyphs.length); + + var os2Table = os2.make(Object.assign({ + xAvgCharWidth: Math.round(globals.advanceWidthAvg), + usFirstCharIndex: firstCharIndex, + usLastCharIndex: lastCharIndex, + ulUnicodeRange1: ulUnicodeRange1, + ulUnicodeRange2: ulUnicodeRange2, + ulUnicodeRange3: ulUnicodeRange3, + ulUnicodeRange4: ulUnicodeRange4, + // See http://typophile.com/node/13081 for more info on vertical metrics. + // We get metrics for typical characters (such as "x" for xHeight). + // We provide some fallback characters if characters are unavailable: their + // ordering was chosen experimentally. + sTypoAscender: globals.ascender, + sTypoDescender: globals.descender, + sTypoLineGap: 0, + usWinAscent: globals.yMax, + usWinDescent: Math.abs(globals.yMin), + ulCodePageRange1: 1, // FIXME: hard-code Latin 1 support for now + sxHeight: metricsForChar(font, 'xyvw', {yMax: Math.round(globals.ascender / 2)}).yMax, + sCapHeight: metricsForChar(font, 'HIKLEFJMNTZBDPRAGOQSUVWXY', globals).yMax, + usDefaultChar: font.hasChar(' ') ? 32 : 0, // Use space as the default character, if available. + usBreakChar: font.hasChar(' ') ? 32 : 0, // Use space as the break character, if available. + }, font.tables.os2)); + + var hmtxTable = hmtx.make(font.glyphs); + var cmapTable = cmap.make(font.glyphs); + + var englishFamilyName = font.getEnglishName('fontFamily'); + var englishStyleName = font.getEnglishName('fontSubfamily'); + var englishFullName = englishFamilyName + ' ' + englishStyleName; + var postScriptName = font.getEnglishName('postScriptName'); + if (!postScriptName) { + postScriptName = englishFamilyName.replace(/\s/g, '') + '-' + englishStyleName; + } + + var names = {}; + for (var n in font.names) { + names[n] = font.names[n]; + } + + if (!names.uniqueID) { + names.uniqueID = {en: font.getEnglishName('manufacturer') + ':' + englishFullName}; + } + + if (!names.postScriptName) { + names.postScriptName = {en: postScriptName}; + } + + if (!names.preferredFamily) { + names.preferredFamily = font.names.fontFamily; + } + + if (!names.preferredSubfamily) { + names.preferredSubfamily = font.names.fontSubfamily; + } + + var languageTags = []; + var nameTable = _name.make(names, languageTags); + var ltagTable = (languageTags.length > 0 ? ltag.make(languageTags) : undefined); + + var postTable = post.make(); + var cffTable = cff.make(font.glyphs, { + version: font.getEnglishName('version'), + fullName: englishFullName, + familyName: englishFamilyName, + weightName: englishStyleName, + postScriptName: postScriptName, + unitsPerEm: font.unitsPerEm, + fontBBox: [0, globals.yMin, globals.ascender, globals.advanceWidthMax] + }); + + var metaTable = (font.metas && Object.keys(font.metas).length > 0) ? meta.make(font.metas) : undefined; + + // The order does not matter because makeSfntTable() will sort them. + var tables = [headTable, hheaTable, maxpTable, os2Table, nameTable, cmapTable, postTable, cffTable, hmtxTable]; + if (ltagTable) { + tables.push(ltagTable); + } + // Optional tables + if (font.tables.gsub) { + tables.push(gsub.make(font.tables.gsub)); + } + if (metaTable) { + tables.push(metaTable); + } + + var sfntTable = makeSfntTable(tables); + + // Compute the font's checkSum and store it in head.checkSumAdjustment. + var bytes = sfntTable.encode(); + var checkSum = computeCheckSum(bytes); + var tableFields = sfntTable.fields; + var checkSumAdjusted = false; + for (var i$1 = 0; i$1 < tableFields.length; i$1 += 1) { + if (tableFields[i$1].name === 'head table') { + tableFields[i$1].value.checkSumAdjustment = 0xB1B0AFBA - checkSum; + checkSumAdjusted = true; + break; + } + } + + if (!checkSumAdjusted) { + throw new Error('Could not find head table with checkSum to adjust.'); + } + + return sfntTable; + } + + var sfnt = { make: makeSfntTable, fontToTable: fontToSfntTable, computeCheckSum: computeCheckSum }; + + // The Layout object is the prototype of Substitution objects, and provides + + function searchTag(arr, tag) { + /* jshint bitwise: false */ + var imin = 0; + var imax = arr.length - 1; + while (imin <= imax) { + var imid = (imin + imax) >>> 1; + var val = arr[imid].tag; + if (val === tag) { + return imid; + } else if (val < tag) { + imin = imid + 1; + } else { imax = imid - 1; } + } + // Not found: return -1-insertion point + return -imin - 1; + } + + function binSearch(arr, value) { + /* jshint bitwise: false */ + var imin = 0; + var imax = arr.length - 1; + while (imin <= imax) { + var imid = (imin + imax) >>> 1; + var val = arr[imid]; + if (val === value) { + return imid; + } else if (val < value) { + imin = imid + 1; + } else { imax = imid - 1; } + } + // Not found: return -1-insertion point + return -imin - 1; + } + + // binary search in a list of ranges (coverage, class definition) + function searchRange(ranges, value) { + // jshint bitwise: false + var range; + var imin = 0; + var imax = ranges.length - 1; + while (imin <= imax) { + var imid = (imin + imax) >>> 1; + range = ranges[imid]; + var start = range.start; + if (start === value) { + return range; + } else if (start < value) { + imin = imid + 1; + } else { imax = imid - 1; } + } + if (imin > 0) { + range = ranges[imin - 1]; + if (value > range.end) { return 0; } + return range; + } + } + + /** + * @exports opentype.Layout + * @class + */ + function Layout(font, tableName) { + this.font = font; + this.tableName = tableName; + } + + Layout.prototype = { + + /** + * Binary search an object by "tag" property + * @instance + * @function searchTag + * @memberof opentype.Layout + * @param {Array} arr + * @param {string} tag + * @return {number} + */ + searchTag: searchTag, + + /** + * Binary search in a list of numbers + * @instance + * @function binSearch + * @memberof opentype.Layout + * @param {Array} arr + * @param {number} value + * @return {number} + */ + binSearch: binSearch, + + /** + * Get or create the Layout table (GSUB, GPOS etc). + * @param {boolean} create - Whether to create a new one. + * @return {Object} The GSUB or GPOS table. + */ + getTable: function(create) { + var layout = this.font.tables[this.tableName]; + if (!layout && create) { + layout = this.font.tables[this.tableName] = this.createDefaultTable(); + } + return layout; + }, + + /** + * Returns all scripts in the substitution table. + * @instance + * @return {Array} + */ + getScriptNames: function() { + var layout = this.getTable(); + if (!layout) { return []; } + return layout.scripts.map(function(script) { + return script.tag; + }); + }, + + /** + * Returns the best bet for a script name. + * Returns 'DFLT' if it exists. + * If not, returns 'latn' if it exists. + * If neither exist, returns undefined. + */ + getDefaultScriptName: function() { + var layout = this.getTable(); + if (!layout) { return; } + var hasLatn = false; + for (var i = 0; i < layout.scripts.length; i++) { + var name = layout.scripts[i].tag; + if (name === 'DFLT') { return name; } + if (name === 'latn') { hasLatn = true; } + } + if (hasLatn) { return 'latn'; } + }, + + /** + * Returns all LangSysRecords in the given script. + * @instance + * @param {string} [script='DFLT'] + * @param {boolean} create - forces the creation of this script table if it doesn't exist. + * @return {Object} An object with tag and script properties. + */ + getScriptTable: function(script, create) { + var layout = this.getTable(create); + if (layout) { + script = script || 'DFLT'; + var scripts = layout.scripts; + var pos = searchTag(layout.scripts, script); + if (pos >= 0) { + return scripts[pos].script; + } else if (create) { + var scr = { + tag: script, + script: { + defaultLangSys: {reserved: 0, reqFeatureIndex: 0xffff, featureIndexes: []}, + langSysRecords: [] + } + }; + scripts.splice(-1 - pos, 0, scr); + return scr.script; + } + } + }, + + /** + * Returns a language system table + * @instance + * @param {string} [script='DFLT'] + * @param {string} [language='dlft'] + * @param {boolean} create - forces the creation of this langSysTable if it doesn't exist. + * @return {Object} + */ + getLangSysTable: function(script, language, create) { + var scriptTable = this.getScriptTable(script, create); + if (scriptTable) { + if (!language || language === 'dflt' || language === 'DFLT') { + return scriptTable.defaultLangSys; + } + var pos = searchTag(scriptTable.langSysRecords, language); + if (pos >= 0) { + return scriptTable.langSysRecords[pos].langSys; + } else if (create) { + var langSysRecord = { + tag: language, + langSys: {reserved: 0, reqFeatureIndex: 0xffff, featureIndexes: []} + }; + scriptTable.langSysRecords.splice(-1 - pos, 0, langSysRecord); + return langSysRecord.langSys; + } + } + }, + + /** + * Get a specific feature table. + * @instance + * @param {string} [script='DFLT'] + * @param {string} [language='dlft'] + * @param {string} feature - One of the codes listed at https://www.microsoft.com/typography/OTSPEC/featurelist.htm + * @param {boolean} create - forces the creation of the feature table if it doesn't exist. + * @return {Object} + */ + getFeatureTable: function(script, language, feature, create) { + var langSysTable = this.getLangSysTable(script, language, create); + if (langSysTable) { + var featureRecord; + var featIndexes = langSysTable.featureIndexes; + var allFeatures = this.font.tables[this.tableName].features; + // The FeatureIndex array of indices is in arbitrary order, + // even if allFeatures is sorted alphabetically by feature tag. + for (var i = 0; i < featIndexes.length; i++) { + featureRecord = allFeatures[featIndexes[i]]; + if (featureRecord.tag === feature) { + return featureRecord.feature; + } + } + if (create) { + var index = allFeatures.length; + // Automatic ordering of features would require to shift feature indexes in the script list. + check.assert(index === 0 || feature >= allFeatures[index - 1].tag, 'Features must be added in alphabetical order.'); + featureRecord = { + tag: feature, + feature: { params: 0, lookupListIndexes: [] } + }; + allFeatures.push(featureRecord); + featIndexes.push(index); + return featureRecord.feature; + } + } + }, + + /** + * Get the lookup tables of a given type for a script/language/feature. + * @instance + * @param {string} [script='DFLT'] + * @param {string} [language='dlft'] + * @param {string} feature - 4-letter feature code + * @param {number} lookupType - 1 to 9 + * @param {boolean} create - forces the creation of the lookup table if it doesn't exist, with no subtables. + * @return {Object[]} + */ + getLookupTables: function(script, language, feature, lookupType, create) { + var featureTable = this.getFeatureTable(script, language, feature, create); + var tables = []; + if (featureTable) { + var lookupTable; + var lookupListIndexes = featureTable.lookupListIndexes; + var allLookups = this.font.tables[this.tableName].lookups; + // lookupListIndexes are in no particular order, so use naive search. + for (var i = 0; i < lookupListIndexes.length; i++) { + lookupTable = allLookups[lookupListIndexes[i]]; + if (lookupTable.lookupType === lookupType) { + tables.push(lookupTable); + } + } + if (tables.length === 0 && create) { + lookupTable = { + lookupType: lookupType, + lookupFlag: 0, + subtables: [], + markFilteringSet: undefined + }; + var index = allLookups.length; + allLookups.push(lookupTable); + lookupListIndexes.push(index); + return [lookupTable]; + } + } + return tables; + }, + + /** + * Find a glyph in a class definition table + * https://docs.microsoft.com/en-us/typography/opentype/spec/chapter2#class-definition-table + * @param {object} classDefTable - an OpenType Layout class definition table + * @param {number} glyphIndex - the index of the glyph to find + * @returns {number} -1 if not found + */ + getGlyphClass: function(classDefTable, glyphIndex) { + switch (classDefTable.format) { + case 1: + if (classDefTable.startGlyph <= glyphIndex && glyphIndex < classDefTable.startGlyph + classDefTable.classes.length) { + return classDefTable.classes[glyphIndex - classDefTable.startGlyph]; + } + return 0; + case 2: + var range = searchRange(classDefTable.ranges, glyphIndex); + return range ? range.classId : 0; + } + }, + + /** + * Find a glyph in a coverage table + * https://docs.microsoft.com/en-us/typography/opentype/spec/chapter2#coverage-table + * @param {object} coverageTable - an OpenType Layout coverage table + * @param {number} glyphIndex - the index of the glyph to find + * @returns {number} -1 if not found + */ + getCoverageIndex: function(coverageTable, glyphIndex) { + switch (coverageTable.format) { + case 1: + var index = binSearch(coverageTable.glyphs, glyphIndex); + return index >= 0 ? index : -1; + case 2: + var range = searchRange(coverageTable.ranges, glyphIndex); + return range ? range.index + glyphIndex - range.start : -1; + } + }, + + /** + * Returns the list of glyph indexes of a coverage table. + * Format 1: the list is stored raw + * Format 2: compact list as range records. + * @instance + * @param {Object} coverageTable + * @return {Array} + */ + expandCoverage: function(coverageTable) { + if (coverageTable.format === 1) { + return coverageTable.glyphs; + } else { + var glyphs = []; + var ranges = coverageTable.ranges; + for (var i = 0; i < ranges.length; i++) { + var range = ranges[i]; + var start = range.start; + var end = range.end; + for (var j = start; j <= end; j++) { + glyphs.push(j); + } + } + return glyphs; + } + } + + }; + + // The Position object provides utility methods to manipulate + + /** + * @exports opentype.Position + * @class + * @extends opentype.Layout + * @param {opentype.Font} + * @constructor + */ + function Position(font) { + Layout.call(this, font, 'gpos'); + } + + Position.prototype = Layout.prototype; + + /** + * Init some data for faster and easier access later. + */ + Position.prototype.init = function() { + var script = this.getDefaultScriptName(); + this.defaultKerningTables = this.getKerningTables(script); + }; + + /** + * Find a glyph pair in a list of lookup tables of type 2 and retrieve the xAdvance kerning value. + * + * @param {integer} leftIndex - left glyph index + * @param {integer} rightIndex - right glyph index + * @returns {integer} + */ + Position.prototype.getKerningValue = function(kerningLookups, leftIndex, rightIndex) { + for (var i = 0; i < kerningLookups.length; i++) { + var subtables = kerningLookups[i].subtables; + for (var j = 0; j < subtables.length; j++) { + var subtable = subtables[j]; + var covIndex = this.getCoverageIndex(subtable.coverage, leftIndex); + if (covIndex < 0) { continue; } + switch (subtable.posFormat) { + case 1: + // Search Pair Adjustment Positioning Format 1 + var pairSet = subtable.pairSets[covIndex]; + for (var k = 0; k < pairSet.length; k++) { + var pair = pairSet[k]; + if (pair.secondGlyph === rightIndex) { + return pair.value1 && pair.value1.xAdvance || 0; + } + } + break; // left glyph found, not right glyph - try next subtable + case 2: + // Search Pair Adjustment Positioning Format 2 + var class1 = this.getGlyphClass(subtable.classDef1, leftIndex); + var class2 = this.getGlyphClass(subtable.classDef2, rightIndex); + var pair$1 = subtable.classRecords[class1][class2]; + return pair$1.value1 && pair$1.value1.xAdvance || 0; + } + } + } + return 0; + }; + + /** + * List all kerning lookup tables. + * + * @param {string} [script='DFLT'] - use font.position.getDefaultScriptName() for a better default value + * @param {string} [language='dflt'] + * @return {object[]} The list of kerning lookup tables (may be empty), or undefined if there is no GPOS table (and we should use the kern table) + */ + Position.prototype.getKerningTables = function(script, language) { + if (this.font.tables.gpos) { + return this.getLookupTables(script, language, 'kern', 2); + } + }; + + // The Substitution object provides utility methods to manipulate + + /** + * @exports opentype.Substitution + * @class + * @extends opentype.Layout + * @param {opentype.Font} + * @constructor + */ + function Substitution(font) { + Layout.call(this, font, 'gsub'); + } + + // Check if 2 arrays of primitives are equal. + function arraysEqual(ar1, ar2) { + var n = ar1.length; + if (n !== ar2.length) { return false; } + for (var i = 0; i < n; i++) { + if (ar1[i] !== ar2[i]) { return false; } + } + return true; + } + + // Find the first subtable of a lookup table in a particular format. + function getSubstFormat(lookupTable, format, defaultSubtable) { + var subtables = lookupTable.subtables; + for (var i = 0; i < subtables.length; i++) { + var subtable = subtables[i]; + if (subtable.substFormat === format) { + return subtable; + } + } + if (defaultSubtable) { + subtables.push(defaultSubtable); + return defaultSubtable; + } + return undefined; + } + + Substitution.prototype = Layout.prototype; + + /** + * Create a default GSUB table. + * @return {Object} gsub - The GSUB table. + */ + Substitution.prototype.createDefaultTable = function() { + // Generate a default empty GSUB table with just a DFLT script and dflt lang sys. + return { + version: 1, + scripts: [{ + tag: 'DFLT', + script: { + defaultLangSys: { reserved: 0, reqFeatureIndex: 0xffff, featureIndexes: [] }, + langSysRecords: [] + } + }], + features: [], + lookups: [] + }; + }; + + /** + * List all single substitutions (lookup type 1) for a given script, language, and feature. + * @param {string} [script='DFLT'] + * @param {string} [language='dflt'] + * @param {string} feature - 4-character feature name ('aalt', 'salt', 'ss01'...) + * @return {Array} substitutions - The list of substitutions. + */ + Substitution.prototype.getSingle = function(feature, script, language) { + var substitutions = []; + var lookupTables = this.getLookupTables(script, language, feature, 1); + for (var idx = 0; idx < lookupTables.length; idx++) { + var subtables = lookupTables[idx].subtables; + for (var i = 0; i < subtables.length; i++) { + var subtable = subtables[i]; + var glyphs = this.expandCoverage(subtable.coverage); + var j = (void 0); + if (subtable.substFormat === 1) { + var delta = subtable.deltaGlyphId; + for (j = 0; j < glyphs.length; j++) { + var glyph = glyphs[j]; + substitutions.push({ sub: glyph, by: glyph + delta }); + } + } else { + var substitute = subtable.substitute; + for (j = 0; j < glyphs.length; j++) { + substitutions.push({ sub: glyphs[j], by: substitute[j] }); + } + } + } + } + return substitutions; + }; + + /** + * List all multiple substitutions (lookup type 2) for a given script, language, and feature. + * @param {string} [script='DFLT'] + * @param {string} [language='dflt'] + * @param {string} feature - 4-character feature name ('ccmp', 'stch') + * @return {Array} substitutions - The list of substitutions. + */ + Substitution.prototype.getMultiple = function(feature, script, language) { + var substitutions = []; + var lookupTables = this.getLookupTables(script, language, feature, 2); + for (var idx = 0; idx < lookupTables.length; idx++) { + var subtables = lookupTables[idx].subtables; + for (var i = 0; i < subtables.length; i++) { + var subtable = subtables[i]; + var glyphs = this.expandCoverage(subtable.coverage); + var j = (void 0); + + for (j = 0; j < glyphs.length; j++) { + var glyph = glyphs[j]; + var replacements = subtable.sequences[j]; + substitutions.push({ sub: glyph, by: replacements }); + } + } + } + return substitutions; + }; + + /** + * List all alternates (lookup type 3) for a given script, language, and feature. + * @param {string} [script='DFLT'] + * @param {string} [language='dflt'] + * @param {string} feature - 4-character feature name ('aalt', 'salt'...) + * @return {Array} alternates - The list of alternates + */ + Substitution.prototype.getAlternates = function(feature, script, language) { + var alternates = []; + var lookupTables = this.getLookupTables(script, language, feature, 3); + for (var idx = 0; idx < lookupTables.length; idx++) { + var subtables = lookupTables[idx].subtables; + for (var i = 0; i < subtables.length; i++) { + var subtable = subtables[i]; + var glyphs = this.expandCoverage(subtable.coverage); + var alternateSets = subtable.alternateSets; + for (var j = 0; j < glyphs.length; j++) { + alternates.push({ sub: glyphs[j], by: alternateSets[j] }); + } + } + } + return alternates; + }; + + /** + * List all ligatures (lookup type 4) for a given script, language, and feature. + * The result is an array of ligature objects like { sub: [ids], by: id } + * @param {string} feature - 4-letter feature name ('liga', 'rlig', 'dlig'...) + * @param {string} [script='DFLT'] + * @param {string} [language='dflt'] + * @return {Array} ligatures - The list of ligatures. + */ + Substitution.prototype.getLigatures = function(feature, script, language) { + var ligatures = []; + var lookupTables = this.getLookupTables(script, language, feature, 4); + for (var idx = 0; idx < lookupTables.length; idx++) { + var subtables = lookupTables[idx].subtables; + for (var i = 0; i < subtables.length; i++) { + var subtable = subtables[i]; + var glyphs = this.expandCoverage(subtable.coverage); + var ligatureSets = subtable.ligatureSets; + for (var j = 0; j < glyphs.length; j++) { + var startGlyph = glyphs[j]; + var ligSet = ligatureSets[j]; + for (var k = 0; k < ligSet.length; k++) { + var lig = ligSet[k]; + ligatures.push({ + sub: [startGlyph].concat(lig.components), + by: lig.ligGlyph + }); + } + } + } + } + return ligatures; + }; + + /** + * Add or modify a single substitution (lookup type 1) + * Format 2, more flexible, is always used. + * @param {string} feature - 4-letter feature name ('liga', 'rlig', 'dlig'...) + * @param {Object} substitution - { sub: id, by: id } (format 1 is not supported) + * @param {string} [script='DFLT'] + * @param {string} [language='dflt'] + */ + Substitution.prototype.addSingle = function(feature, substitution, script, language) { + var lookupTable = this.getLookupTables(script, language, feature, 1, true)[0]; + var subtable = getSubstFormat(lookupTable, 2, { // lookup type 1 subtable, format 2, coverage format 1 + substFormat: 2, + coverage: {format: 1, glyphs: []}, + substitute: [] + }); + check.assert(subtable.coverage.format === 1, 'Single: unable to modify coverage table format ' + subtable.coverage.format); + var coverageGlyph = substitution.sub; + var pos = this.binSearch(subtable.coverage.glyphs, coverageGlyph); + if (pos < 0) { + pos = -1 - pos; + subtable.coverage.glyphs.splice(pos, 0, coverageGlyph); + subtable.substitute.splice(pos, 0, 0); + } + subtable.substitute[pos] = substitution.by; + }; + + /** + * Add or modify a multiple substitution (lookup type 2) + * @param {string} feature - 4-letter feature name ('ccmp', 'stch') + * @param {Object} substitution - { sub: id, by: [id] } for format 2. + * @param {string} [script='DFLT'] + * @param {string} [language='dflt'] + */ + Substitution.prototype.addMultiple = function(feature, substitution, script, language) { + check.assert(substitution.by instanceof Array && substitution.by.length > 1, 'Multiple: "by" must be an array of two or more ids'); + var lookupTable = this.getLookupTables(script, language, feature, 2, true)[0]; + var subtable = getSubstFormat(lookupTable, 1, { // lookup type 2 subtable, format 1, coverage format 1 + substFormat: 1, + coverage: {format: 1, glyphs: []}, + sequences: [] + }); + check.assert(subtable.coverage.format === 1, 'Multiple: unable to modify coverage table format ' + subtable.coverage.format); + var coverageGlyph = substitution.sub; + var pos = this.binSearch(subtable.coverage.glyphs, coverageGlyph); + if (pos < 0) { + pos = -1 - pos; + subtable.coverage.glyphs.splice(pos, 0, coverageGlyph); + subtable.sequences.splice(pos, 0, 0); + } + subtable.sequences[pos] = substitution.by; + }; + + /** + * Add or modify an alternate substitution (lookup type 3) + * @param {string} feature - 4-letter feature name ('liga', 'rlig', 'dlig'...) + * @param {Object} substitution - { sub: id, by: [ids] } + * @param {string} [script='DFLT'] + * @param {string} [language='dflt'] + */ + Substitution.prototype.addAlternate = function(feature, substitution, script, language) { + var lookupTable = this.getLookupTables(script, language, feature, 3, true)[0]; + var subtable = getSubstFormat(lookupTable, 1, { // lookup type 3 subtable, format 1, coverage format 1 + substFormat: 1, + coverage: {format: 1, glyphs: []}, + alternateSets: [] + }); + check.assert(subtable.coverage.format === 1, 'Alternate: unable to modify coverage table format ' + subtable.coverage.format); + var coverageGlyph = substitution.sub; + var pos = this.binSearch(subtable.coverage.glyphs, coverageGlyph); + if (pos < 0) { + pos = -1 - pos; + subtable.coverage.glyphs.splice(pos, 0, coverageGlyph); + subtable.alternateSets.splice(pos, 0, 0); + } + subtable.alternateSets[pos] = substitution.by; + }; + + /** + * Add a ligature (lookup type 4) + * Ligatures with more components must be stored ahead of those with fewer components in order to be found + * @param {string} feature - 4-letter feature name ('liga', 'rlig', 'dlig'...) + * @param {Object} ligature - { sub: [ids], by: id } + * @param {string} [script='DFLT'] + * @param {string} [language='dflt'] + */ + Substitution.prototype.addLigature = function(feature, ligature, script, language) { + var lookupTable = this.getLookupTables(script, language, feature, 4, true)[0]; + var subtable = lookupTable.subtables[0]; + if (!subtable) { + subtable = { // lookup type 4 subtable, format 1, coverage format 1 + substFormat: 1, + coverage: { format: 1, glyphs: [] }, + ligatureSets: [] + }; + lookupTable.subtables[0] = subtable; + } + check.assert(subtable.coverage.format === 1, 'Ligature: unable to modify coverage table format ' + subtable.coverage.format); + var coverageGlyph = ligature.sub[0]; + var ligComponents = ligature.sub.slice(1); + var ligatureTable = { + ligGlyph: ligature.by, + components: ligComponents + }; + var pos = this.binSearch(subtable.coverage.glyphs, coverageGlyph); + if (pos >= 0) { + // ligatureSet already exists + var ligatureSet = subtable.ligatureSets[pos]; + for (var i = 0; i < ligatureSet.length; i++) { + // If ligature already exists, return. + if (arraysEqual(ligatureSet[i].components, ligComponents)) { + return; + } + } + // ligature does not exist: add it. + ligatureSet.push(ligatureTable); + } else { + // Create a new ligatureSet and add coverage for the first glyph. + pos = -1 - pos; + subtable.coverage.glyphs.splice(pos, 0, coverageGlyph); + subtable.ligatureSets.splice(pos, 0, [ligatureTable]); + } + }; + + /** + * List all feature data for a given script and language. + * @param {string} feature - 4-letter feature name + * @param {string} [script='DFLT'] + * @param {string} [language='dflt'] + * @return {Array} substitutions - The list of substitutions. + */ + Substitution.prototype.getFeature = function(feature, script, language) { + if (/ss\d\d/.test(feature)) { + // ss01 - ss20 + return this.getSingle(feature, script, language); + } + switch (feature) { + case 'aalt': + case 'salt': + return this.getSingle(feature, script, language) + .concat(this.getAlternates(feature, script, language)); + case 'dlig': + case 'liga': + case 'rlig': + return this.getLigatures(feature, script, language); + case 'ccmp': + return this.getMultiple(feature, script, language) + .concat(this.getLigatures(feature, script, language)); + case 'stch': + return this.getMultiple(feature, script, language); + } + return undefined; + }; + + /** + * Add a substitution to a feature for a given script and language. + * @param {string} feature - 4-letter feature name + * @param {Object} sub - the substitution to add (an object like { sub: id or [ids], by: id or [ids] }) + * @param {string} [script='DFLT'] + * @param {string} [language='dflt'] + */ + Substitution.prototype.add = function(feature, sub, script, language) { + if (/ss\d\d/.test(feature)) { + // ss01 - ss20 + return this.addSingle(feature, sub, script, language); + } + switch (feature) { + case 'aalt': + case 'salt': + if (typeof sub.by === 'number') { + return this.addSingle(feature, sub, script, language); + } + return this.addAlternate(feature, sub, script, language); + case 'dlig': + case 'liga': + case 'rlig': + return this.addLigature(feature, sub, script, language); + case 'ccmp': + if (sub.by instanceof Array) { + return this.addMultiple(feature, sub, script, language); + } + return this.addLigature(feature, sub, script, language); + } + return undefined; + }; + + function isBrowser() { + return typeof window !== 'undefined'; + } + + function nodeBufferToArrayBuffer(buffer) { + var ab = new ArrayBuffer(buffer.length); + var view = new Uint8Array(ab); + for (var i = 0; i < buffer.length; ++i) { + view[i] = buffer[i]; + } + + return ab; + } + + function arrayBufferToNodeBuffer(ab) { + var buffer = new Buffer(ab.byteLength); + var view = new Uint8Array(ab); + for (var i = 0; i < buffer.length; ++i) { + buffer[i] = view[i]; + } + + return buffer; + } + + function checkArgument(expression, message) { + if (!expression) { + throw message; + } + } + + // The `glyf` table describes the glyphs in TrueType outline format. + + // Parse the coordinate data for a glyph. + function parseGlyphCoordinate(p, flag, previousValue, shortVectorBitMask, sameBitMask) { + var v; + if ((flag & shortVectorBitMask) > 0) { + // The coordinate is 1 byte long. + v = p.parseByte(); + // The `same` bit is re-used for short values to signify the sign of the value. + if ((flag & sameBitMask) === 0) { + v = -v; + } + + v = previousValue + v; + } else { + // The coordinate is 2 bytes long. + // If the `same` bit is set, the coordinate is the same as the previous coordinate. + if ((flag & sameBitMask) > 0) { + v = previousValue; + } else { + // Parse the coordinate as a signed 16-bit delta value. + v = previousValue + p.parseShort(); + } + } + + return v; + } + + // Parse a TrueType glyph. + function parseGlyph(glyph, data, start) { + var p = new parse.Parser(data, start); + glyph.numberOfContours = p.parseShort(); + glyph._xMin = p.parseShort(); + glyph._yMin = p.parseShort(); + glyph._xMax = p.parseShort(); + glyph._yMax = p.parseShort(); + var flags; + var flag; + + if (glyph.numberOfContours > 0) { + // This glyph is not a composite. + var endPointIndices = glyph.endPointIndices = []; + for (var i = 0; i < glyph.numberOfContours; i += 1) { + endPointIndices.push(p.parseUShort()); + } + + glyph.instructionLength = p.parseUShort(); + glyph.instructions = []; + for (var i$1 = 0; i$1 < glyph.instructionLength; i$1 += 1) { + glyph.instructions.push(p.parseByte()); + } + + var numberOfCoordinates = endPointIndices[endPointIndices.length - 1] + 1; + flags = []; + for (var i$2 = 0; i$2 < numberOfCoordinates; i$2 += 1) { + flag = p.parseByte(); + flags.push(flag); + // If bit 3 is set, we repeat this flag n times, where n is the next byte. + if ((flag & 8) > 0) { + var repeatCount = p.parseByte(); + for (var j = 0; j < repeatCount; j += 1) { + flags.push(flag); + i$2 += 1; + } + } + } + + check.argument(flags.length === numberOfCoordinates, 'Bad flags.'); + + if (endPointIndices.length > 0) { + var points = []; + var point; + // X/Y coordinates are relative to the previous point, except for the first point which is relative to 0,0. + if (numberOfCoordinates > 0) { + for (var i$3 = 0; i$3 < numberOfCoordinates; i$3 += 1) { + flag = flags[i$3]; + point = {}; + point.onCurve = !!(flag & 1); + point.lastPointOfContour = endPointIndices.indexOf(i$3) >= 0; + points.push(point); + } + + var px = 0; + for (var i$4 = 0; i$4 < numberOfCoordinates; i$4 += 1) { + flag = flags[i$4]; + point = points[i$4]; + point.x = parseGlyphCoordinate(p, flag, px, 2, 16); + px = point.x; + } + + var py = 0; + for (var i$5 = 0; i$5 < numberOfCoordinates; i$5 += 1) { + flag = flags[i$5]; + point = points[i$5]; + point.y = parseGlyphCoordinate(p, flag, py, 4, 32); + py = point.y; + } + } + + glyph.points = points; + } else { + glyph.points = []; + } + } else if (glyph.numberOfContours === 0) { + glyph.points = []; + } else { + glyph.isComposite = true; + glyph.points = []; + glyph.components = []; + var moreComponents = true; + while (moreComponents) { + flags = p.parseUShort(); + var component = { + glyphIndex: p.parseUShort(), + xScale: 1, + scale01: 0, + scale10: 0, + yScale: 1, + dx: 0, + dy: 0 + }; + if ((flags & 1) > 0) { + // The arguments are words + if ((flags & 2) > 0) { + // values are offset + component.dx = p.parseShort(); + component.dy = p.parseShort(); + } else { + // values are matched points + component.matchedPoints = [p.parseUShort(), p.parseUShort()]; + } + + } else { + // The arguments are bytes + if ((flags & 2) > 0) { + // values are offset + component.dx = p.parseChar(); + component.dy = p.parseChar(); + } else { + // values are matched points + component.matchedPoints = [p.parseByte(), p.parseByte()]; + } + } + + if ((flags & 8) > 0) { + // We have a scale + component.xScale = component.yScale = p.parseF2Dot14(); + } else if ((flags & 64) > 0) { + // We have an X / Y scale + component.xScale = p.parseF2Dot14(); + component.yScale = p.parseF2Dot14(); + } else if ((flags & 128) > 0) { + // We have a 2x2 transformation + component.xScale = p.parseF2Dot14(); + component.scale01 = p.parseF2Dot14(); + component.scale10 = p.parseF2Dot14(); + component.yScale = p.parseF2Dot14(); + } + + glyph.components.push(component); + moreComponents = !!(flags & 32); + } + if (flags & 0x100) { + // We have instructions + glyph.instructionLength = p.parseUShort(); + glyph.instructions = []; + for (var i$6 = 0; i$6 < glyph.instructionLength; i$6 += 1) { + glyph.instructions.push(p.parseByte()); + } + } + } + } + + // Transform an array of points and return a new array. + function transformPoints(points, transform) { + var newPoints = []; + for (var i = 0; i < points.length; i += 1) { + var pt = points[i]; + var newPt = { + x: transform.xScale * pt.x + transform.scale01 * pt.y + transform.dx, + y: transform.scale10 * pt.x + transform.yScale * pt.y + transform.dy, + onCurve: pt.onCurve, + lastPointOfContour: pt.lastPointOfContour + }; + newPoints.push(newPt); + } + + return newPoints; + } + + function getContours(points) { + var contours = []; + var currentContour = []; + for (var i = 0; i < points.length; i += 1) { + var pt = points[i]; + currentContour.push(pt); + if (pt.lastPointOfContour) { + contours.push(currentContour); + currentContour = []; + } + } + + check.argument(currentContour.length === 0, 'There are still points left in the current contour.'); + return contours; + } + + // Convert the TrueType glyph outline to a Path. + function getPath(points) { + var p = new Path(); + if (!points) { + return p; + } + + var contours = getContours(points); + + for (var contourIndex = 0; contourIndex < contours.length; ++contourIndex) { + var contour = contours[contourIndex]; + + var prev = null; + var curr = contour[contour.length - 1]; + var next = contour[0]; + + if (curr.onCurve) { + p.moveTo(curr.x, curr.y); + } else { + if (next.onCurve) { + p.moveTo(next.x, next.y); + } else { + // If both first and last points are off-curve, start at their middle. + var start = {x: (curr.x + next.x) * 0.5, y: (curr.y + next.y) * 0.5}; + p.moveTo(start.x, start.y); + } + } + + for (var i = 0; i < contour.length; ++i) { + prev = curr; + curr = next; + next = contour[(i + 1) % contour.length]; + + if (curr.onCurve) { + // This is a straight line. + p.lineTo(curr.x, curr.y); + } else { + var prev2 = prev; + var next2 = next; + + if (!prev.onCurve) { + prev2 = { x: (curr.x + prev.x) * 0.5, y: (curr.y + prev.y) * 0.5 }; + } + + if (!next.onCurve) { + next2 = { x: (curr.x + next.x) * 0.5, y: (curr.y + next.y) * 0.5 }; + } + + p.quadraticCurveTo(curr.x, curr.y, next2.x, next2.y); + } + } + + p.closePath(); + } + return p; + } + + function buildPath(glyphs, glyph) { + if (glyph.isComposite) { + for (var j = 0; j < glyph.components.length; j += 1) { + var component = glyph.components[j]; + var componentGlyph = glyphs.get(component.glyphIndex); + // Force the ttfGlyphLoader to parse the glyph. + componentGlyph.getPath(); + if (componentGlyph.points) { + var transformedPoints = (void 0); + if (component.matchedPoints === undefined) { + // component positioned by offset + transformedPoints = transformPoints(componentGlyph.points, component); + } else { + // component positioned by matched points + if ((component.matchedPoints[0] > glyph.points.length - 1) || + (component.matchedPoints[1] > componentGlyph.points.length - 1)) { + throw Error('Matched points out of range in ' + glyph.name); + } + var firstPt = glyph.points[component.matchedPoints[0]]; + var secondPt = componentGlyph.points[component.matchedPoints[1]]; + var transform = { + xScale: component.xScale, scale01: component.scale01, + scale10: component.scale10, yScale: component.yScale, + dx: 0, dy: 0 + }; + secondPt = transformPoints([secondPt], transform)[0]; + transform.dx = firstPt.x - secondPt.x; + transform.dy = firstPt.y - secondPt.y; + transformedPoints = transformPoints(componentGlyph.points, transform); + } + glyph.points = glyph.points.concat(transformedPoints); + } + } + } + + return getPath(glyph.points); + } + + function parseGlyfTableAll(data, start, loca, font) { + var glyphs = new glyphset.GlyphSet(font); + + // The last element of the loca table is invalid. + for (var i = 0; i < loca.length - 1; i += 1) { + var offset = loca[i]; + var nextOffset = loca[i + 1]; + if (offset !== nextOffset) { + glyphs.push(i, glyphset.ttfGlyphLoader(font, i, parseGlyph, data, start + offset, buildPath)); + } else { + glyphs.push(i, glyphset.glyphLoader(font, i)); + } + } + + return glyphs; + } + + function parseGlyfTableOnLowMemory(data, start, loca, font) { + var glyphs = new glyphset.GlyphSet(font); + + font._push = function(i) { + var offset = loca[i]; + var nextOffset = loca[i + 1]; + if (offset !== nextOffset) { + glyphs.push(i, glyphset.ttfGlyphLoader(font, i, parseGlyph, data, start + offset, buildPath)); + } else { + glyphs.push(i, glyphset.glyphLoader(font, i)); + } + }; + + return glyphs; + } + + // Parse all the glyphs according to the offsets from the `loca` table. + function parseGlyfTable(data, start, loca, font, opt) { + if (opt.lowMemory) + { return parseGlyfTableOnLowMemory(data, start, loca, font); } + else + { return parseGlyfTableAll(data, start, loca, font); } + } + + var glyf = { getPath: getPath, parse: parseGlyfTable}; + + /* A TrueType font hinting interpreter. + * + * (c) 2017 Axel Kittenberger + * + * This interpreter has been implemented according to this documentation: + * https://developer.apple.com/fonts/TrueType-Reference-Manual/RM05/Chap5.html + * + * According to the documentation F24DOT6 values are used for pixels. + * That means calculation is 1/64 pixel accurate and uses integer operations. + * However, Javascript has floating point operations by default and only + * those are available. One could make a case to simulate the 1/64 accuracy + * exactly by truncating after every division operation + * (for example with << 0) to get pixel exactly results as other TrueType + * implementations. It may make sense since some fonts are pixel optimized + * by hand using DELTAP instructions. The current implementation doesn't + * and rather uses full floating point precision. + * + * xScale, yScale and rotation is currently ignored. + * + * A few non-trivial instructions are missing as I didn't encounter yet + * a font that used them to test a possible implementation. + * + * Some fonts seem to use undocumented features regarding the twilight zone. + * Only some of them are implemented as they were encountered. + * + * The exports.DEBUG statements are removed on the minified distribution file. + */ + + var instructionTable; + var exec; + var execGlyph; + var execComponent; + + /* + * Creates a hinting object. + * + * There ought to be exactly one + * for each truetype font that is used for hinting. + */ + function Hinting(font) { + // the font this hinting object is for + this.font = font; + + this.getCommands = function (hPoints) { + return glyf.getPath(hPoints).commands; + }; + + // cached states + this._fpgmState = + this._prepState = + undefined; + + // errorState + // 0 ... all okay + // 1 ... had an error in a glyf, + // continue working but stop spamming + // the console + // 2 ... error at prep, stop hinting at this ppem + // 3 ... error at fpeg, stop hinting for this font at all + this._errorState = 0; + } + + /* + * Not rounding. + */ + function roundOff(v) { + return v; + } + + /* + * Rounding to grid. + */ + function roundToGrid(v) { + //Rounding in TT is supposed to "symmetrical around zero" + return Math.sign(v) * Math.round(Math.abs(v)); + } + + /* + * Rounding to double grid. + */ + function roundToDoubleGrid(v) { + return Math.sign(v) * Math.round(Math.abs(v * 2)) / 2; + } + + /* + * Rounding to half grid. + */ + function roundToHalfGrid(v) { + return Math.sign(v) * (Math.round(Math.abs(v) + 0.5) - 0.5); + } + + /* + * Rounding to up to grid. + */ + function roundUpToGrid(v) { + return Math.sign(v) * Math.ceil(Math.abs(v)); + } + + /* + * Rounding to down to grid. + */ + function roundDownToGrid(v) { + return Math.sign(v) * Math.floor(Math.abs(v)); + } + + /* + * Super rounding. + */ + var roundSuper = function (v) { + var period = this.srPeriod; + var phase = this.srPhase; + var threshold = this.srThreshold; + var sign = 1; + + if (v < 0) { + v = -v; + sign = -1; + } + + v += threshold - phase; + + v = Math.trunc(v / period) * period; + + v += phase; + + // according to http://xgridfit.sourceforge.net/round.html + if (v < 0) { return phase * sign; } + + return v * sign; + }; + + /* + * Unit vector of x-axis. + */ + var xUnitVector = { + x: 1, + + y: 0, + + axis: 'x', + + // Gets the projected distance between two points. + // o1/o2 ... if true, respective original position is used. + distance: function (p1, p2, o1, o2) { + return (o1 ? p1.xo : p1.x) - (o2 ? p2.xo : p2.x); + }, + + // Moves point p so the moved position has the same relative + // position to the moved positions of rp1 and rp2 than the + // original positions had. + // + // See APPENDIX on INTERPOLATE at the bottom of this file. + interpolate: function (p, rp1, rp2, pv) { + var do1; + var do2; + var doa1; + var doa2; + var dm1; + var dm2; + var dt; + + if (!pv || pv === this) { + do1 = p.xo - rp1.xo; + do2 = p.xo - rp2.xo; + dm1 = rp1.x - rp1.xo; + dm2 = rp2.x - rp2.xo; + doa1 = Math.abs(do1); + doa2 = Math.abs(do2); + dt = doa1 + doa2; + + if (dt === 0) { + p.x = p.xo + (dm1 + dm2) / 2; + return; + } + + p.x = p.xo + (dm1 * doa2 + dm2 * doa1) / dt; + return; + } + + do1 = pv.distance(p, rp1, true, true); + do2 = pv.distance(p, rp2, true, true); + dm1 = pv.distance(rp1, rp1, false, true); + dm2 = pv.distance(rp2, rp2, false, true); + doa1 = Math.abs(do1); + doa2 = Math.abs(do2); + dt = doa1 + doa2; + + if (dt === 0) { + xUnitVector.setRelative(p, p, (dm1 + dm2) / 2, pv, true); + return; + } + + xUnitVector.setRelative(p, p, (dm1 * doa2 + dm2 * doa1) / dt, pv, true); + }, + + // Slope of line normal to this + normalSlope: Number.NEGATIVE_INFINITY, + + // Sets the point 'p' relative to point 'rp' + // by the distance 'd'. + // + // See APPENDIX on SETRELATIVE at the bottom of this file. + // + // p ... point to set + // rp ... reference point + // d ... distance on projection vector + // pv ... projection vector (undefined = this) + // org ... if true, uses the original position of rp as reference. + setRelative: function (p, rp, d, pv, org) { + if (!pv || pv === this) { + p.x = (org ? rp.xo : rp.x) + d; + return; + } + + var rpx = org ? rp.xo : rp.x; + var rpy = org ? rp.yo : rp.y; + var rpdx = rpx + d * pv.x; + var rpdy = rpy + d * pv.y; + + p.x = rpdx + (p.y - rpdy) / pv.normalSlope; + }, + + // Slope of vector line. + slope: 0, + + // Touches the point p. + touch: function (p) { + p.xTouched = true; + }, + + // Tests if a point p is touched. + touched: function (p) { + return p.xTouched; + }, + + // Untouches the point p. + untouch: function (p) { + p.xTouched = false; + } + }; + + /* + * Unit vector of y-axis. + */ + var yUnitVector = { + x: 0, + + y: 1, + + axis: 'y', + + // Gets the projected distance between two points. + // o1/o2 ... if true, respective original position is used. + distance: function (p1, p2, o1, o2) { + return (o1 ? p1.yo : p1.y) - (o2 ? p2.yo : p2.y); + }, + + // Moves point p so the moved position has the same relative + // position to the moved positions of rp1 and rp2 than the + // original positions had. + // + // See APPENDIX on INTERPOLATE at the bottom of this file. + interpolate: function (p, rp1, rp2, pv) { + var do1; + var do2; + var doa1; + var doa2; + var dm1; + var dm2; + var dt; + + if (!pv || pv === this) { + do1 = p.yo - rp1.yo; + do2 = p.yo - rp2.yo; + dm1 = rp1.y - rp1.yo; + dm2 = rp2.y - rp2.yo; + doa1 = Math.abs(do1); + doa2 = Math.abs(do2); + dt = doa1 + doa2; + + if (dt === 0) { + p.y = p.yo + (dm1 + dm2) / 2; + return; + } + + p.y = p.yo + (dm1 * doa2 + dm2 * doa1) / dt; + return; + } + + do1 = pv.distance(p, rp1, true, true); + do2 = pv.distance(p, rp2, true, true); + dm1 = pv.distance(rp1, rp1, false, true); + dm2 = pv.distance(rp2, rp2, false, true); + doa1 = Math.abs(do1); + doa2 = Math.abs(do2); + dt = doa1 + doa2; + + if (dt === 0) { + yUnitVector.setRelative(p, p, (dm1 + dm2) / 2, pv, true); + return; + } + + yUnitVector.setRelative(p, p, (dm1 * doa2 + dm2 * doa1) / dt, pv, true); + }, + + // Slope of line normal to this. + normalSlope: 0, + + // Sets the point 'p' relative to point 'rp' + // by the distance 'd' + // + // See APPENDIX on SETRELATIVE at the bottom of this file. + // + // p ... point to set + // rp ... reference point + // d ... distance on projection vector + // pv ... projection vector (undefined = this) + // org ... if true, uses the original position of rp as reference. + setRelative: function (p, rp, d, pv, org) { + if (!pv || pv === this) { + p.y = (org ? rp.yo : rp.y) + d; + return; + } + + var rpx = org ? rp.xo : rp.x; + var rpy = org ? rp.yo : rp.y; + var rpdx = rpx + d * pv.x; + var rpdy = rpy + d * pv.y; + + p.y = rpdy + pv.normalSlope * (p.x - rpdx); + }, + + // Slope of vector line. + slope: Number.POSITIVE_INFINITY, + + // Touches the point p. + touch: function (p) { + p.yTouched = true; + }, + + // Tests if a point p is touched. + touched: function (p) { + return p.yTouched; + }, + + // Untouches the point p. + untouch: function (p) { + p.yTouched = false; + } + }; + + Object.freeze(xUnitVector); + Object.freeze(yUnitVector); + + /* + * Creates a unit vector that is not x- or y-axis. + */ + function UnitVector(x, y) { + this.x = x; + this.y = y; + this.axis = undefined; + this.slope = y / x; + this.normalSlope = -x / y; + Object.freeze(this); + } + + /* + * Gets the projected distance between two points. + * o1/o2 ... if true, respective original position is used. + */ + UnitVector.prototype.distance = function(p1, p2, o1, o2) { + return ( + this.x * xUnitVector.distance(p1, p2, o1, o2) + + this.y * yUnitVector.distance(p1, p2, o1, o2) + ); + }; + + /* + * Moves point p so the moved position has the same relative + * position to the moved positions of rp1 and rp2 than the + * original positions had. + * + * See APPENDIX on INTERPOLATE at the bottom of this file. + */ + UnitVector.prototype.interpolate = function(p, rp1, rp2, pv) { + var dm1; + var dm2; + var do1; + var do2; + var doa1; + var doa2; + var dt; + + do1 = pv.distance(p, rp1, true, true); + do2 = pv.distance(p, rp2, true, true); + dm1 = pv.distance(rp1, rp1, false, true); + dm2 = pv.distance(rp2, rp2, false, true); + doa1 = Math.abs(do1); + doa2 = Math.abs(do2); + dt = doa1 + doa2; + + if (dt === 0) { + this.setRelative(p, p, (dm1 + dm2) / 2, pv, true); + return; + } + + this.setRelative(p, p, (dm1 * doa2 + dm2 * doa1) / dt, pv, true); + }; + + /* + * Sets the point 'p' relative to point 'rp' + * by the distance 'd' + * + * See APPENDIX on SETRELATIVE at the bottom of this file. + * + * p ... point to set + * rp ... reference point + * d ... distance on projection vector + * pv ... projection vector (undefined = this) + * org ... if true, uses the original position of rp as reference. + */ + UnitVector.prototype.setRelative = function(p, rp, d, pv, org) { + pv = pv || this; + + var rpx = org ? rp.xo : rp.x; + var rpy = org ? rp.yo : rp.y; + var rpdx = rpx + d * pv.x; + var rpdy = rpy + d * pv.y; + + var pvns = pv.normalSlope; + var fvs = this.slope; + + var px = p.x; + var py = p.y; + + p.x = (fvs * px - pvns * rpdx + rpdy - py) / (fvs - pvns); + p.y = fvs * (p.x - px) + py; + }; + + /* + * Touches the point p. + */ + UnitVector.prototype.touch = function(p) { + p.xTouched = true; + p.yTouched = true; + }; + + /* + * Returns a unit vector with x/y coordinates. + */ + function getUnitVector(x, y) { + var d = Math.sqrt(x * x + y * y); + + x /= d; + y /= d; + + if (x === 1 && y === 0) { return xUnitVector; } + else if (x === 0 && y === 1) { return yUnitVector; } + else { return new UnitVector(x, y); } + } + + /* + * Creates a point in the hinting engine. + */ + function HPoint( + x, + y, + lastPointOfContour, + onCurve + ) { + this.x = this.xo = Math.round(x * 64) / 64; // hinted x value and original x-value + this.y = this.yo = Math.round(y * 64) / 64; // hinted y value and original y-value + + this.lastPointOfContour = lastPointOfContour; + this.onCurve = onCurve; + this.prevPointOnContour = undefined; + this.nextPointOnContour = undefined; + this.xTouched = false; + this.yTouched = false; + + Object.preventExtensions(this); + } + + /* + * Returns the next touched point on the contour. + * + * v ... unit vector to test touch axis. + */ + HPoint.prototype.nextTouched = function(v) { + var p = this.nextPointOnContour; + + while (!v.touched(p) && p !== this) { p = p.nextPointOnContour; } + + return p; + }; + + /* + * Returns the previous touched point on the contour + * + * v ... unit vector to test touch axis. + */ + HPoint.prototype.prevTouched = function(v) { + var p = this.prevPointOnContour; + + while (!v.touched(p) && p !== this) { p = p.prevPointOnContour; } + + return p; + }; + + /* + * The zero point. + */ + var HPZero = Object.freeze(new HPoint(0, 0)); + + /* + * The default state of the interpreter. + * + * Note: Freezing the defaultState and then deriving from it + * makes the V8 Javascript engine going awkward, + * so this is avoided, albeit the defaultState shouldn't + * ever change. + */ + var defaultState = { + cvCutIn: 17 / 16, // control value cut in + deltaBase: 9, + deltaShift: 0.125, + loop: 1, // loops some instructions + minDis: 1, // minimum distance + autoFlip: true + }; + + /* + * The current state of the interpreter. + * + * env ... 'fpgm' or 'prep' or 'glyf' + * prog ... the program + */ + function State(env, prog) { + this.env = env; + this.stack = []; + this.prog = prog; + + switch (env) { + case 'glyf' : + this.zp0 = this.zp1 = this.zp2 = 1; + this.rp0 = this.rp1 = this.rp2 = 0; + /* fall through */ + case 'prep' : + this.fv = this.pv = this.dpv = xUnitVector; + this.round = roundToGrid; + } + } + + /* + * Executes a glyph program. + * + * This does the hinting for each glyph. + * + * Returns an array of moved points. + * + * glyph: the glyph to hint + * ppem: the size the glyph is rendered for + */ + Hinting.prototype.exec = function(glyph, ppem) { + if (typeof ppem !== 'number') { + throw new Error('Point size is not a number!'); + } + + // Received a fatal error, don't do any hinting anymore. + if (this._errorState > 2) { return; } + + var font = this.font; + var prepState = this._prepState; + + if (!prepState || prepState.ppem !== ppem) { + var fpgmState = this._fpgmState; + + if (!fpgmState) { + // Executes the fpgm state. + // This is used by fonts to define functions. + State.prototype = defaultState; + + fpgmState = + this._fpgmState = + new State('fpgm', font.tables.fpgm); + + fpgmState.funcs = [ ]; + fpgmState.font = font; + + if (exports.DEBUG) { + console.log('---EXEC FPGM---'); + fpgmState.step = -1; + } + + try { + exec(fpgmState); + } catch (e) { + console.log('Hinting error in FPGM:' + e); + this._errorState = 3; + return; + } + } + + // Executes the prep program for this ppem setting. + // This is used by fonts to set cvt values + // depending on to be rendered font size. + + State.prototype = fpgmState; + prepState = + this._prepState = + new State('prep', font.tables.prep); + + prepState.ppem = ppem; + + // Creates a copy of the cvt table + // and scales it to the current ppem setting. + var oCvt = font.tables.cvt; + if (oCvt) { + var cvt = prepState.cvt = new Array(oCvt.length); + var scale = ppem / font.unitsPerEm; + for (var c = 0; c < oCvt.length; c++) { + cvt[c] = oCvt[c] * scale; + } + } else { + prepState.cvt = []; + } + + if (exports.DEBUG) { + console.log('---EXEC PREP---'); + prepState.step = -1; + } + + try { + exec(prepState); + } catch (e) { + if (this._errorState < 2) { + console.log('Hinting error in PREP:' + e); + } + this._errorState = 2; + } + } + + if (this._errorState > 1) { return; } + + try { + return execGlyph(glyph, prepState); + } catch (e) { + if (this._errorState < 1) { + console.log('Hinting error:' + e); + console.log('Note: further hinting errors are silenced'); + } + this._errorState = 1; + return undefined; + } + }; + + /* + * Executes the hinting program for a glyph. + */ + execGlyph = function(glyph, prepState) { + // original point positions + var xScale = prepState.ppem / prepState.font.unitsPerEm; + var yScale = xScale; + var components = glyph.components; + var contours; + var gZone; + var state; + + State.prototype = prepState; + if (!components) { + state = new State('glyf', glyph.instructions); + if (exports.DEBUG) { + console.log('---EXEC GLYPH---'); + state.step = -1; + } + execComponent(glyph, state, xScale, yScale); + gZone = state.gZone; + } else { + var font = prepState.font; + gZone = []; + contours = []; + for (var i = 0; i < components.length; i++) { + var c = components[i]; + var cg = font.glyphs.get(c.glyphIndex); + + state = new State('glyf', cg.instructions); + + if (exports.DEBUG) { + console.log('---EXEC COMP ' + i + '---'); + state.step = -1; + } + + execComponent(cg, state, xScale, yScale); + // appends the computed points to the result array + // post processes the component points + var dx = Math.round(c.dx * xScale); + var dy = Math.round(c.dy * yScale); + var gz = state.gZone; + var cc = state.contours; + for (var pi = 0; pi < gz.length; pi++) { + var p = gz[pi]; + p.xTouched = p.yTouched = false; + p.xo = p.x = p.x + dx; + p.yo = p.y = p.y + dy; + } + + var gLen = gZone.length; + gZone.push.apply(gZone, gz); + for (var j = 0; j < cc.length; j++) { + contours.push(cc[j] + gLen); + } + } + + if (glyph.instructions && !state.inhibitGridFit) { + // the composite has instructions on its own + state = new State('glyf', glyph.instructions); + + state.gZone = state.z0 = state.z1 = state.z2 = gZone; + + state.contours = contours; + + // note: HPZero cannot be used here, since + // the point might be modified + gZone.push( + new HPoint(0, 0), + new HPoint(Math.round(glyph.advanceWidth * xScale), 0) + ); + + if (exports.DEBUG) { + console.log('---EXEC COMPOSITE---'); + state.step = -1; + } + + exec(state); + + gZone.length -= 2; + } + } + + return gZone; + }; + + /* + * Executes the hinting program for a component of a multi-component glyph + * or of the glyph itself for a non-component glyph. + */ + execComponent = function(glyph, state, xScale, yScale) + { + var points = glyph.points || []; + var pLen = points.length; + var gZone = state.gZone = state.z0 = state.z1 = state.z2 = []; + var contours = state.contours = []; + + // Scales the original points and + // makes copies for the hinted points. + var cp; // current point + for (var i = 0; i < pLen; i++) { + cp = points[i]; + + gZone[i] = new HPoint( + cp.x * xScale, + cp.y * yScale, + cp.lastPointOfContour, + cp.onCurve + ); + } + + // Chain links the contours. + var sp; // start point + var np; // next point + + for (var i$1 = 0; i$1 < pLen; i$1++) { + cp = gZone[i$1]; + + if (!sp) { + sp = cp; + contours.push(i$1); + } + + if (cp.lastPointOfContour) { + cp.nextPointOnContour = sp; + sp.prevPointOnContour = cp; + sp = undefined; + } else { + np = gZone[i$1 + 1]; + cp.nextPointOnContour = np; + np.prevPointOnContour = cp; + } + } + + if (state.inhibitGridFit) { return; } + + if (exports.DEBUG) { + console.log('PROCESSING GLYPH', state.stack); + for (var i$2 = 0; i$2 < pLen; i$2++) { + console.log(i$2, gZone[i$2].x, gZone[i$2].y); + } + } + + gZone.push( + new HPoint(0, 0), + new HPoint(Math.round(glyph.advanceWidth * xScale), 0) + ); + + exec(state); + + // Removes the extra points. + gZone.length -= 2; + + if (exports.DEBUG) { + console.log('FINISHED GLYPH', state.stack); + for (var i$3 = 0; i$3 < pLen; i$3++) { + console.log(i$3, gZone[i$3].x, gZone[i$3].y); + } + } + }; + + /* + * Executes the program loaded in state. + */ + exec = function(state) { + var prog = state.prog; + + if (!prog) { return; } + + var pLen = prog.length; + var ins; + + for (state.ip = 0; state.ip < pLen; state.ip++) { + if (exports.DEBUG) { state.step++; } + ins = instructionTable[prog[state.ip]]; + + if (!ins) { + throw new Error( + 'unknown instruction: 0x' + + Number(prog[state.ip]).toString(16) + ); + } + + ins(state); + + // very extensive debugging for each step + /* + if (exports.DEBUG) { + var da; + if (state.gZone) { + da = []; + for (let i = 0; i < state.gZone.length; i++) + { + da.push(i + ' ' + + state.gZone[i].x * 64 + ' ' + + state.gZone[i].y * 64 + ' ' + + (state.gZone[i].xTouched ? 'x' : '') + + (state.gZone[i].yTouched ? 'y' : '') + ); + } + console.log('GZ', da); + } + + if (state.tZone) { + da = []; + for (let i = 0; i < state.tZone.length; i++) { + da.push(i + ' ' + + state.tZone[i].x * 64 + ' ' + + state.tZone[i].y * 64 + ' ' + + (state.tZone[i].xTouched ? 'x' : '') + + (state.tZone[i].yTouched ? 'y' : '') + ); + } + console.log('TZ', da); + } + + if (state.stack.length > 10) { + console.log( + state.stack.length, + '...', state.stack.slice(state.stack.length - 10) + ); + } else { + console.log(state.stack.length, state.stack); + } + } + */ + } + }; + + /* + * Initializes the twilight zone. + * + * This is only done if a SZPx instruction + * refers to the twilight zone. + */ + function initTZone(state) + { + var tZone = state.tZone = new Array(state.gZone.length); + + // no idea if this is actually correct... + for (var i = 0; i < tZone.length; i++) + { + tZone[i] = new HPoint(0, 0); + } + } + + /* + * Skips the instruction pointer ahead over an IF/ELSE block. + * handleElse .. if true breaks on matching ELSE + */ + function skip(state, handleElse) + { + var prog = state.prog; + var ip = state.ip; + var nesting = 1; + var ins; + + do { + ins = prog[++ip]; + if (ins === 0x58) // IF + { nesting++; } + else if (ins === 0x59) // EIF + { nesting--; } + else if (ins === 0x40) // NPUSHB + { ip += prog[ip + 1] + 1; } + else if (ins === 0x41) // NPUSHW + { ip += 2 * prog[ip + 1] + 1; } + else if (ins >= 0xB0 && ins <= 0xB7) // PUSHB + { ip += ins - 0xB0 + 1; } + else if (ins >= 0xB8 && ins <= 0xBF) // PUSHW + { ip += (ins - 0xB8 + 1) * 2; } + else if (handleElse && nesting === 1 && ins === 0x1B) // ELSE + { break; } + } while (nesting > 0); + + state.ip = ip; + } + + /*----------------------------------------------------------* + * And then a lot of instructions... * + *----------------------------------------------------------*/ + + // SVTCA[a] Set freedom and projection Vectors To Coordinate Axis + // 0x00-0x01 + function SVTCA(v, state) { + if (exports.DEBUG) { console.log(state.step, 'SVTCA[' + v.axis + ']'); } + + state.fv = state.pv = state.dpv = v; + } + + // SPVTCA[a] Set Projection Vector to Coordinate Axis + // 0x02-0x03 + function SPVTCA(v, state) { + if (exports.DEBUG) { console.log(state.step, 'SPVTCA[' + v.axis + ']'); } + + state.pv = state.dpv = v; + } + + // SFVTCA[a] Set Freedom Vector to Coordinate Axis + // 0x04-0x05 + function SFVTCA(v, state) { + if (exports.DEBUG) { console.log(state.step, 'SFVTCA[' + v.axis + ']'); } + + state.fv = v; + } + + // SPVTL[a] Set Projection Vector To Line + // 0x06-0x07 + function SPVTL(a, state) { + var stack = state.stack; + var p2i = stack.pop(); + var p1i = stack.pop(); + var p2 = state.z2[p2i]; + var p1 = state.z1[p1i]; + + if (exports.DEBUG) { console.log('SPVTL[' + a + ']', p2i, p1i); } + + var dx; + var dy; + + if (!a) { + dx = p1.x - p2.x; + dy = p1.y - p2.y; + } else { + dx = p2.y - p1.y; + dy = p1.x - p2.x; + } + + state.pv = state.dpv = getUnitVector(dx, dy); + } + + // SFVTL[a] Set Freedom Vector To Line + // 0x08-0x09 + function SFVTL(a, state) { + var stack = state.stack; + var p2i = stack.pop(); + var p1i = stack.pop(); + var p2 = state.z2[p2i]; + var p1 = state.z1[p1i]; + + if (exports.DEBUG) { console.log('SFVTL[' + a + ']', p2i, p1i); } + + var dx; + var dy; + + if (!a) { + dx = p1.x - p2.x; + dy = p1.y - p2.y; + } else { + dx = p2.y - p1.y; + dy = p1.x - p2.x; + } + + state.fv = getUnitVector(dx, dy); + } + + // SPVFS[] Set Projection Vector From Stack + // 0x0A + function SPVFS(state) { + var stack = state.stack; + var y = stack.pop(); + var x = stack.pop(); + + if (exports.DEBUG) { console.log(state.step, 'SPVFS[]', y, x); } + + state.pv = state.dpv = getUnitVector(x, y); + } + + // SFVFS[] Set Freedom Vector From Stack + // 0x0B + function SFVFS(state) { + var stack = state.stack; + var y = stack.pop(); + var x = stack.pop(); + + if (exports.DEBUG) { console.log(state.step, 'SPVFS[]', y, x); } + + state.fv = getUnitVector(x, y); + } + + // GPV[] Get Projection Vector + // 0x0C + function GPV(state) { + var stack = state.stack; + var pv = state.pv; + + if (exports.DEBUG) { console.log(state.step, 'GPV[]'); } + + stack.push(pv.x * 0x4000); + stack.push(pv.y * 0x4000); + } + + // GFV[] Get Freedom Vector + // 0x0C + function GFV(state) { + var stack = state.stack; + var fv = state.fv; + + if (exports.DEBUG) { console.log(state.step, 'GFV[]'); } + + stack.push(fv.x * 0x4000); + stack.push(fv.y * 0x4000); + } + + // SFVTPV[] Set Freedom Vector To Projection Vector + // 0x0E + function SFVTPV(state) { + state.fv = state.pv; + + if (exports.DEBUG) { console.log(state.step, 'SFVTPV[]'); } + } + + // ISECT[] moves point p to the InterSECTion of two lines + // 0x0F + function ISECT(state) + { + var stack = state.stack; + var pa0i = stack.pop(); + var pa1i = stack.pop(); + var pb0i = stack.pop(); + var pb1i = stack.pop(); + var pi = stack.pop(); + var z0 = state.z0; + var z1 = state.z1; + var pa0 = z0[pa0i]; + var pa1 = z0[pa1i]; + var pb0 = z1[pb0i]; + var pb1 = z1[pb1i]; + var p = state.z2[pi]; + + if (exports.DEBUG) { console.log('ISECT[], ', pa0i, pa1i, pb0i, pb1i, pi); } + + // math from + // en.wikipedia.org/wiki/Line%E2%80%93line_intersection#Given_two_points_on_each_line + + var x1 = pa0.x; + var y1 = pa0.y; + var x2 = pa1.x; + var y2 = pa1.y; + var x3 = pb0.x; + var y3 = pb0.y; + var x4 = pb1.x; + var y4 = pb1.y; + + var div = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4); + var f1 = x1 * y2 - y1 * x2; + var f2 = x3 * y4 - y3 * x4; + + p.x = (f1 * (x3 - x4) - f2 * (x1 - x2)) / div; + p.y = (f1 * (y3 - y4) - f2 * (y1 - y2)) / div; + } + + // SRP0[] Set Reference Point 0 + // 0x10 + function SRP0(state) { + state.rp0 = state.stack.pop(); + + if (exports.DEBUG) { console.log(state.step, 'SRP0[]', state.rp0); } + } + + // SRP1[] Set Reference Point 1 + // 0x11 + function SRP1(state) { + state.rp1 = state.stack.pop(); + + if (exports.DEBUG) { console.log(state.step, 'SRP1[]', state.rp1); } + } + + // SRP1[] Set Reference Point 2 + // 0x12 + function SRP2(state) { + state.rp2 = state.stack.pop(); + + if (exports.DEBUG) { console.log(state.step, 'SRP2[]', state.rp2); } + } + + // SZP0[] Set Zone Pointer 0 + // 0x13 + function SZP0(state) { + var n = state.stack.pop(); + + if (exports.DEBUG) { console.log(state.step, 'SZP0[]', n); } + + state.zp0 = n; + + switch (n) { + case 0: + if (!state.tZone) { initTZone(state); } + state.z0 = state.tZone; + break; + case 1 : + state.z0 = state.gZone; + break; + default : + throw new Error('Invalid zone pointer'); + } + } + + // SZP1[] Set Zone Pointer 1 + // 0x14 + function SZP1(state) { + var n = state.stack.pop(); + + if (exports.DEBUG) { console.log(state.step, 'SZP1[]', n); } + + state.zp1 = n; + + switch (n) { + case 0: + if (!state.tZone) { initTZone(state); } + state.z1 = state.tZone; + break; + case 1 : + state.z1 = state.gZone; + break; + default : + throw new Error('Invalid zone pointer'); + } + } + + // SZP2[] Set Zone Pointer 2 + // 0x15 + function SZP2(state) { + var n = state.stack.pop(); + + if (exports.DEBUG) { console.log(state.step, 'SZP2[]', n); } + + state.zp2 = n; + + switch (n) { + case 0: + if (!state.tZone) { initTZone(state); } + state.z2 = state.tZone; + break; + case 1 : + state.z2 = state.gZone; + break; + default : + throw new Error('Invalid zone pointer'); + } + } + + // SZPS[] Set Zone PointerS + // 0x16 + function SZPS(state) { + var n = state.stack.pop(); + + if (exports.DEBUG) { console.log(state.step, 'SZPS[]', n); } + + state.zp0 = state.zp1 = state.zp2 = n; + + switch (n) { + case 0: + if (!state.tZone) { initTZone(state); } + state.z0 = state.z1 = state.z2 = state.tZone; + break; + case 1 : + state.z0 = state.z1 = state.z2 = state.gZone; + break; + default : + throw new Error('Invalid zone pointer'); + } + } + + // SLOOP[] Set LOOP variable + // 0x17 + function SLOOP(state) { + state.loop = state.stack.pop(); + + if (exports.DEBUG) { console.log(state.step, 'SLOOP[]', state.loop); } + } + + // RTG[] Round To Grid + // 0x18 + function RTG(state) { + if (exports.DEBUG) { console.log(state.step, 'RTG[]'); } + + state.round = roundToGrid; + } + + // RTHG[] Round To Half Grid + // 0x19 + function RTHG(state) { + if (exports.DEBUG) { console.log(state.step, 'RTHG[]'); } + + state.round = roundToHalfGrid; + } + + // SMD[] Set Minimum Distance + // 0x1A + function SMD(state) { + var d = state.stack.pop(); + + if (exports.DEBUG) { console.log(state.step, 'SMD[]', d); } + + state.minDis = d / 0x40; + } + + // ELSE[] ELSE clause + // 0x1B + function ELSE(state) { + // This instruction has been reached by executing a then branch + // so it just skips ahead until matching EIF. + // + // In case the IF was negative the IF[] instruction already + // skipped forward over the ELSE[] + + if (exports.DEBUG) { console.log(state.step, 'ELSE[]'); } + + skip(state, false); + } + + // JMPR[] JuMP Relative + // 0x1C + function JMPR(state) { + var o = state.stack.pop(); + + if (exports.DEBUG) { console.log(state.step, 'JMPR[]', o); } + + // A jump by 1 would do nothing. + state.ip += o - 1; + } + + // SCVTCI[] Set Control Value Table Cut-In + // 0x1D + function SCVTCI(state) { + var n = state.stack.pop(); + + if (exports.DEBUG) { console.log(state.step, 'SCVTCI[]', n); } + + state.cvCutIn = n / 0x40; + } + + // DUP[] DUPlicate top stack element + // 0x20 + function DUP(state) { + var stack = state.stack; + + if (exports.DEBUG) { console.log(state.step, 'DUP[]'); } + + stack.push(stack[stack.length - 1]); + } + + // POP[] POP top stack element + // 0x21 + function POP(state) { + if (exports.DEBUG) { console.log(state.step, 'POP[]'); } + + state.stack.pop(); + } + + // CLEAR[] CLEAR the stack + // 0x22 + function CLEAR(state) { + if (exports.DEBUG) { console.log(state.step, 'CLEAR[]'); } + + state.stack.length = 0; + } + + // SWAP[] SWAP the top two elements on the stack + // 0x23 + function SWAP(state) { + var stack = state.stack; + + var a = stack.pop(); + var b = stack.pop(); + + if (exports.DEBUG) { console.log(state.step, 'SWAP[]'); } + + stack.push(a); + stack.push(b); + } + + // DEPTH[] DEPTH of the stack + // 0x24 + function DEPTH(state) { + var stack = state.stack; + + if (exports.DEBUG) { console.log(state.step, 'DEPTH[]'); } + + stack.push(stack.length); + } + + // LOOPCALL[] LOOPCALL function + // 0x2A + function LOOPCALL(state) { + var stack = state.stack; + var fn = stack.pop(); + var c = stack.pop(); + + if (exports.DEBUG) { console.log(state.step, 'LOOPCALL[]', fn, c); } + + // saves callers program + var cip = state.ip; + var cprog = state.prog; + + state.prog = state.funcs[fn]; + + // executes the function + for (var i = 0; i < c; i++) { + exec(state); + + if (exports.DEBUG) { console.log( + ++state.step, + i + 1 < c ? 'next loopcall' : 'done loopcall', + i + ); } + } + + // restores the callers program + state.ip = cip; + state.prog = cprog; + } + + // CALL[] CALL function + // 0x2B + function CALL(state) { + var fn = state.stack.pop(); + + if (exports.DEBUG) { console.log(state.step, 'CALL[]', fn); } + + // saves callers program + var cip = state.ip; + var cprog = state.prog; + + state.prog = state.funcs[fn]; + + // executes the function + exec(state); + + // restores the callers program + state.ip = cip; + state.prog = cprog; + + if (exports.DEBUG) { console.log(++state.step, 'returning from', fn); } + } + + // CINDEX[] Copy the INDEXed element to the top of the stack + // 0x25 + function CINDEX(state) { + var stack = state.stack; + var k = stack.pop(); + + if (exports.DEBUG) { console.log(state.step, 'CINDEX[]', k); } + + // In case of k == 1, it copies the last element after popping + // thus stack.length - k. + stack.push(stack[stack.length - k]); + } + + // MINDEX[] Move the INDEXed element to the top of the stack + // 0x26 + function MINDEX(state) { + var stack = state.stack; + var k = stack.pop(); + + if (exports.DEBUG) { console.log(state.step, 'MINDEX[]', k); } + + stack.push(stack.splice(stack.length - k, 1)[0]); + } + + // FDEF[] Function DEFinition + // 0x2C + function FDEF(state) { + if (state.env !== 'fpgm') { throw new Error('FDEF not allowed here'); } + var stack = state.stack; + var prog = state.prog; + var ip = state.ip; + + var fn = stack.pop(); + var ipBegin = ip; + + if (exports.DEBUG) { console.log(state.step, 'FDEF[]', fn); } + + while (prog[++ip] !== 0x2D){ } + + state.ip = ip; + state.funcs[fn] = prog.slice(ipBegin + 1, ip); + } + + // MDAP[a] Move Direct Absolute Point + // 0x2E-0x2F + function MDAP(round, state) { + var pi = state.stack.pop(); + var p = state.z0[pi]; + var fv = state.fv; + var pv = state.pv; + + if (exports.DEBUG) { console.log(state.step, 'MDAP[' + round + ']', pi); } + + var d = pv.distance(p, HPZero); + + if (round) { d = state.round(d); } + + fv.setRelative(p, HPZero, d, pv); + fv.touch(p); + + state.rp0 = state.rp1 = pi; + } + + // IUP[a] Interpolate Untouched Points through the outline + // 0x30 + function IUP(v, state) { + var z2 = state.z2; + var pLen = z2.length - 2; + var cp; + var pp; + var np; + + if (exports.DEBUG) { console.log(state.step, 'IUP[' + v.axis + ']'); } + + for (var i = 0; i < pLen; i++) { + cp = z2[i]; // current point + + // if this point has been touched go on + if (v.touched(cp)) { continue; } + + pp = cp.prevTouched(v); + + // no point on the contour has been touched? + if (pp === cp) { continue; } + + np = cp.nextTouched(v); + + if (pp === np) { + // only one point on the contour has been touched + // so simply moves the point like that + + v.setRelative(cp, cp, v.distance(pp, pp, false, true), v, true); + } + + v.interpolate(cp, pp, np, v); + } + } + + // SHP[] SHift Point using reference point + // 0x32-0x33 + function SHP(a, state) { + var stack = state.stack; + var rpi = a ? state.rp1 : state.rp2; + var rp = (a ? state.z0 : state.z1)[rpi]; + var fv = state.fv; + var pv = state.pv; + var loop = state.loop; + var z2 = state.z2; + + while (loop--) + { + var pi = stack.pop(); + var p = z2[pi]; + + var d = pv.distance(rp, rp, false, true); + fv.setRelative(p, p, d, pv); + fv.touch(p); + + if (exports.DEBUG) { + console.log( + state.step, + (state.loop > 1 ? + 'loop ' + (state.loop - loop) + ': ' : + '' + ) + + 'SHP[' + (a ? 'rp1' : 'rp2') + ']', pi + ); + } + } + + state.loop = 1; + } + + // SHC[] SHift Contour using reference point + // 0x36-0x37 + function SHC(a, state) { + var stack = state.stack; + var rpi = a ? state.rp1 : state.rp2; + var rp = (a ? state.z0 : state.z1)[rpi]; + var fv = state.fv; + var pv = state.pv; + var ci = stack.pop(); + var sp = state.z2[state.contours[ci]]; + var p = sp; + + if (exports.DEBUG) { console.log(state.step, 'SHC[' + a + ']', ci); } + + var d = pv.distance(rp, rp, false, true); + + do { + if (p !== rp) { fv.setRelative(p, p, d, pv); } + p = p.nextPointOnContour; + } while (p !== sp); + } + + // SHZ[] SHift Zone using reference point + // 0x36-0x37 + function SHZ(a, state) { + var stack = state.stack; + var rpi = a ? state.rp1 : state.rp2; + var rp = (a ? state.z0 : state.z1)[rpi]; + var fv = state.fv; + var pv = state.pv; + + var e = stack.pop(); + + if (exports.DEBUG) { console.log(state.step, 'SHZ[' + a + ']', e); } + + var z; + switch (e) { + case 0 : z = state.tZone; break; + case 1 : z = state.gZone; break; + default : throw new Error('Invalid zone'); + } + + var p; + var d = pv.distance(rp, rp, false, true); + var pLen = z.length - 2; + for (var i = 0; i < pLen; i++) + { + p = z[i]; + fv.setRelative(p, p, d, pv); + //if (p !== rp) fv.setRelative(p, p, d, pv); + } + } + + // SHPIX[] SHift point by a PIXel amount + // 0x38 + function SHPIX(state) { + var stack = state.stack; + var loop = state.loop; + var fv = state.fv; + var d = stack.pop() / 0x40; + var z2 = state.z2; + + while (loop--) { + var pi = stack.pop(); + var p = z2[pi]; + + if (exports.DEBUG) { + console.log( + state.step, + (state.loop > 1 ? 'loop ' + (state.loop - loop) + ': ' : '') + + 'SHPIX[]', pi, d + ); + } + + fv.setRelative(p, p, d); + fv.touch(p); + } + + state.loop = 1; + } + + // IP[] Interpolate Point + // 0x39 + function IP(state) { + var stack = state.stack; + var rp1i = state.rp1; + var rp2i = state.rp2; + var loop = state.loop; + var rp1 = state.z0[rp1i]; + var rp2 = state.z1[rp2i]; + var fv = state.fv; + var pv = state.dpv; + var z2 = state.z2; + + while (loop--) { + var pi = stack.pop(); + var p = z2[pi]; + + if (exports.DEBUG) { + console.log( + state.step, + (state.loop > 1 ? 'loop ' + (state.loop - loop) + ': ' : '') + + 'IP[]', pi, rp1i, '<->', rp2i + ); + } + + fv.interpolate(p, rp1, rp2, pv); + + fv.touch(p); + } + + state.loop = 1; + } + + // MSIRP[a] Move Stack Indirect Relative Point + // 0x3A-0x3B + function MSIRP(a, state) { + var stack = state.stack; + var d = stack.pop() / 64; + var pi = stack.pop(); + var p = state.z1[pi]; + var rp0 = state.z0[state.rp0]; + var fv = state.fv; + var pv = state.pv; + + fv.setRelative(p, rp0, d, pv); + fv.touch(p); + + if (exports.DEBUG) { console.log(state.step, 'MSIRP[' + a + ']', d, pi); } + + state.rp1 = state.rp0; + state.rp2 = pi; + if (a) { state.rp0 = pi; } + } + + // ALIGNRP[] Align to reference point. + // 0x3C + function ALIGNRP(state) { + var stack = state.stack; + var rp0i = state.rp0; + var rp0 = state.z0[rp0i]; + var loop = state.loop; + var fv = state.fv; + var pv = state.pv; + var z1 = state.z1; + + while (loop--) { + var pi = stack.pop(); + var p = z1[pi]; + + if (exports.DEBUG) { + console.log( + state.step, + (state.loop > 1 ? 'loop ' + (state.loop - loop) + ': ' : '') + + 'ALIGNRP[]', pi + ); + } + + fv.setRelative(p, rp0, 0, pv); + fv.touch(p); + } + + state.loop = 1; + } + + // RTG[] Round To Double Grid + // 0x3D + function RTDG(state) { + if (exports.DEBUG) { console.log(state.step, 'RTDG[]'); } + + state.round = roundToDoubleGrid; + } + + // MIAP[a] Move Indirect Absolute Point + // 0x3E-0x3F + function MIAP(round, state) { + var stack = state.stack; + var n = stack.pop(); + var pi = stack.pop(); + var p = state.z0[pi]; + var fv = state.fv; + var pv = state.pv; + var cv = state.cvt[n]; + + if (exports.DEBUG) { + console.log( + state.step, + 'MIAP[' + round + ']', + n, '(', cv, ')', pi + ); + } + + var d = pv.distance(p, HPZero); + + if (round) { + if (Math.abs(d - cv) < state.cvCutIn) { d = cv; } + + d = state.round(d); + } + + fv.setRelative(p, HPZero, d, pv); + + if (state.zp0 === 0) { + p.xo = p.x; + p.yo = p.y; + } + + fv.touch(p); + + state.rp0 = state.rp1 = pi; + } + + // NPUSB[] PUSH N Bytes + // 0x40 + function NPUSHB(state) { + var prog = state.prog; + var ip = state.ip; + var stack = state.stack; + + var n = prog[++ip]; + + if (exports.DEBUG) { console.log(state.step, 'NPUSHB[]', n); } + + for (var i = 0; i < n; i++) { stack.push(prog[++ip]); } + + state.ip = ip; + } + + // NPUSHW[] PUSH N Words + // 0x41 + function NPUSHW(state) { + var ip = state.ip; + var prog = state.prog; + var stack = state.stack; + var n = prog[++ip]; + + if (exports.DEBUG) { console.log(state.step, 'NPUSHW[]', n); } + + for (var i = 0; i < n; i++) { + var w = (prog[++ip] << 8) | prog[++ip]; + if (w & 0x8000) { w = -((w ^ 0xffff) + 1); } + stack.push(w); + } + + state.ip = ip; + } + + // WS[] Write Store + // 0x42 + function WS(state) { + var stack = state.stack; + var store = state.store; + + if (!store) { store = state.store = []; } + + var v = stack.pop(); + var l = stack.pop(); + + if (exports.DEBUG) { console.log(state.step, 'WS', v, l); } + + store[l] = v; + } + + // RS[] Read Store + // 0x43 + function RS(state) { + var stack = state.stack; + var store = state.store; + + var l = stack.pop(); + + if (exports.DEBUG) { console.log(state.step, 'RS', l); } + + var v = (store && store[l]) || 0; + + stack.push(v); + } + + // WCVTP[] Write Control Value Table in Pixel units + // 0x44 + function WCVTP(state) { + var stack = state.stack; + + var v = stack.pop(); + var l = stack.pop(); + + if (exports.DEBUG) { console.log(state.step, 'WCVTP', v, l); } + + state.cvt[l] = v / 0x40; + } + + // RCVT[] Read Control Value Table entry + // 0x45 + function RCVT(state) { + var stack = state.stack; + var cvte = stack.pop(); + + if (exports.DEBUG) { console.log(state.step, 'RCVT', cvte); } + + stack.push(state.cvt[cvte] * 0x40); + } + + // GC[] Get Coordinate projected onto the projection vector + // 0x46-0x47 + function GC(a, state) { + var stack = state.stack; + var pi = stack.pop(); + var p = state.z2[pi]; + + if (exports.DEBUG) { console.log(state.step, 'GC[' + a + ']', pi); } + + stack.push(state.dpv.distance(p, HPZero, a, false) * 0x40); + } + + // MD[a] Measure Distance + // 0x49-0x4A + function MD(a, state) { + var stack = state.stack; + var pi2 = stack.pop(); + var pi1 = stack.pop(); + var p2 = state.z1[pi2]; + var p1 = state.z0[pi1]; + var d = state.dpv.distance(p1, p2, a, a); + + if (exports.DEBUG) { console.log(state.step, 'MD[' + a + ']', pi2, pi1, '->', d); } + + state.stack.push(Math.round(d * 64)); + } + + // MPPEM[] Measure Pixels Per EM + // 0x4B + function MPPEM(state) { + if (exports.DEBUG) { console.log(state.step, 'MPPEM[]'); } + state.stack.push(state.ppem); + } + + // FLIPON[] set the auto FLIP Boolean to ON + // 0x4D + function FLIPON(state) { + if (exports.DEBUG) { console.log(state.step, 'FLIPON[]'); } + state.autoFlip = true; + } + + // LT[] Less Than + // 0x50 + function LT(state) { + var stack = state.stack; + var e2 = stack.pop(); + var e1 = stack.pop(); + + if (exports.DEBUG) { console.log(state.step, 'LT[]', e2, e1); } + + stack.push(e1 < e2 ? 1 : 0); + } + + // LTEQ[] Less Than or EQual + // 0x53 + function LTEQ(state) { + var stack = state.stack; + var e2 = stack.pop(); + var e1 = stack.pop(); + + if (exports.DEBUG) { console.log(state.step, 'LTEQ[]', e2, e1); } + + stack.push(e1 <= e2 ? 1 : 0); + } + + // GTEQ[] Greater Than + // 0x52 + function GT(state) { + var stack = state.stack; + var e2 = stack.pop(); + var e1 = stack.pop(); + + if (exports.DEBUG) { console.log(state.step, 'GT[]', e2, e1); } + + stack.push(e1 > e2 ? 1 : 0); + } + + // GTEQ[] Greater Than or EQual + // 0x53 + function GTEQ(state) { + var stack = state.stack; + var e2 = stack.pop(); + var e1 = stack.pop(); + + if (exports.DEBUG) { console.log(state.step, 'GTEQ[]', e2, e1); } + + stack.push(e1 >= e2 ? 1 : 0); + } + + // EQ[] EQual + // 0x54 + function EQ(state) { + var stack = state.stack; + var e2 = stack.pop(); + var e1 = stack.pop(); + + if (exports.DEBUG) { console.log(state.step, 'EQ[]', e2, e1); } + + stack.push(e2 === e1 ? 1 : 0); + } + + // NEQ[] Not EQual + // 0x55 + function NEQ(state) { + var stack = state.stack; + var e2 = stack.pop(); + var e1 = stack.pop(); + + if (exports.DEBUG) { console.log(state.step, 'NEQ[]', e2, e1); } + + stack.push(e2 !== e1 ? 1 : 0); + } + + // ODD[] ODD + // 0x56 + function ODD(state) { + var stack = state.stack; + var n = stack.pop(); + + if (exports.DEBUG) { console.log(state.step, 'ODD[]', n); } + + stack.push(Math.trunc(n) % 2 ? 1 : 0); + } + + // EVEN[] EVEN + // 0x57 + function EVEN(state) { + var stack = state.stack; + var n = stack.pop(); + + if (exports.DEBUG) { console.log(state.step, 'EVEN[]', n); } + + stack.push(Math.trunc(n) % 2 ? 0 : 1); + } + + // IF[] IF test + // 0x58 + function IF(state) { + var test = state.stack.pop(); + + if (exports.DEBUG) { console.log(state.step, 'IF[]', test); } + + // if test is true it just continues + // if not the ip is skipped until matching ELSE or EIF + if (!test) { + skip(state, true); + + if (exports.DEBUG) { console.log(state.step, 'EIF[]'); } + } + } + + // EIF[] End IF + // 0x59 + function EIF(state) { + // this can be reached normally when + // executing an else branch. + // -> just ignore it + + if (exports.DEBUG) { console.log(state.step, 'EIF[]'); } + } + + // AND[] logical AND + // 0x5A + function AND(state) { + var stack = state.stack; + var e2 = stack.pop(); + var e1 = stack.pop(); + + if (exports.DEBUG) { console.log(state.step, 'AND[]', e2, e1); } + + stack.push(e2 && e1 ? 1 : 0); + } + + // OR[] logical OR + // 0x5B + function OR(state) { + var stack = state.stack; + var e2 = stack.pop(); + var e1 = stack.pop(); + + if (exports.DEBUG) { console.log(state.step, 'OR[]', e2, e1); } + + stack.push(e2 || e1 ? 1 : 0); + } + + // NOT[] logical NOT + // 0x5C + function NOT(state) { + var stack = state.stack; + var e = stack.pop(); + + if (exports.DEBUG) { console.log(state.step, 'NOT[]', e); } + + stack.push(e ? 0 : 1); + } + + // DELTAP1[] DELTA exception P1 + // DELTAP2[] DELTA exception P2 + // DELTAP3[] DELTA exception P3 + // 0x5D, 0x71, 0x72 + function DELTAP123(b, state) { + var stack = state.stack; + var n = stack.pop(); + var fv = state.fv; + var pv = state.pv; + var ppem = state.ppem; + var base = state.deltaBase + (b - 1) * 16; + var ds = state.deltaShift; + var z0 = state.z0; + + if (exports.DEBUG) { console.log(state.step, 'DELTAP[' + b + ']', n, stack); } + + for (var i = 0; i < n; i++) { + var pi = stack.pop(); + var arg = stack.pop(); + var appem = base + ((arg & 0xF0) >> 4); + if (appem !== ppem) { continue; } + + var mag = (arg & 0x0F) - 8; + if (mag >= 0) { mag++; } + if (exports.DEBUG) { console.log(state.step, 'DELTAPFIX', pi, 'by', mag * ds); } + + var p = z0[pi]; + fv.setRelative(p, p, mag * ds, pv); + } + } + + // SDB[] Set Delta Base in the graphics state + // 0x5E + function SDB(state) { + var stack = state.stack; + var n = stack.pop(); + + if (exports.DEBUG) { console.log(state.step, 'SDB[]', n); } + + state.deltaBase = n; + } + + // SDS[] Set Delta Shift in the graphics state + // 0x5F + function SDS(state) { + var stack = state.stack; + var n = stack.pop(); + + if (exports.DEBUG) { console.log(state.step, 'SDS[]', n); } + + state.deltaShift = Math.pow(0.5, n); + } + + // ADD[] ADD + // 0x60 + function ADD(state) { + var stack = state.stack; + var n2 = stack.pop(); + var n1 = stack.pop(); + + if (exports.DEBUG) { console.log(state.step, 'ADD[]', n2, n1); } + + stack.push(n1 + n2); + } + + // SUB[] SUB + // 0x61 + function SUB(state) { + var stack = state.stack; + var n2 = stack.pop(); + var n1 = stack.pop(); + + if (exports.DEBUG) { console.log(state.step, 'SUB[]', n2, n1); } + + stack.push(n1 - n2); + } + + // DIV[] DIV + // 0x62 + function DIV(state) { + var stack = state.stack; + var n2 = stack.pop(); + var n1 = stack.pop(); + + if (exports.DEBUG) { console.log(state.step, 'DIV[]', n2, n1); } + + stack.push(n1 * 64 / n2); + } + + // MUL[] MUL + // 0x63 + function MUL(state) { + var stack = state.stack; + var n2 = stack.pop(); + var n1 = stack.pop(); + + if (exports.DEBUG) { console.log(state.step, 'MUL[]', n2, n1); } + + stack.push(n1 * n2 / 64); + } + + // ABS[] ABSolute value + // 0x64 + function ABS(state) { + var stack = state.stack; + var n = stack.pop(); + + if (exports.DEBUG) { console.log(state.step, 'ABS[]', n); } + + stack.push(Math.abs(n)); + } + + // NEG[] NEGate + // 0x65 + function NEG(state) { + var stack = state.stack; + var n = stack.pop(); + + if (exports.DEBUG) { console.log(state.step, 'NEG[]', n); } + + stack.push(-n); + } + + // FLOOR[] FLOOR + // 0x66 + function FLOOR(state) { + var stack = state.stack; + var n = stack.pop(); + + if (exports.DEBUG) { console.log(state.step, 'FLOOR[]', n); } + + stack.push(Math.floor(n / 0x40) * 0x40); + } + + // CEILING[] CEILING + // 0x67 + function CEILING(state) { + var stack = state.stack; + var n = stack.pop(); + + if (exports.DEBUG) { console.log(state.step, 'CEILING[]', n); } + + stack.push(Math.ceil(n / 0x40) * 0x40); + } + + // ROUND[ab] ROUND value + // 0x68-0x6B + function ROUND(dt, state) { + var stack = state.stack; + var n = stack.pop(); + + if (exports.DEBUG) { console.log(state.step, 'ROUND[]'); } + + stack.push(state.round(n / 0x40) * 0x40); + } + + // WCVTF[] Write Control Value Table in Funits + // 0x70 + function WCVTF(state) { + var stack = state.stack; + var v = stack.pop(); + var l = stack.pop(); + + if (exports.DEBUG) { console.log(state.step, 'WCVTF[]', v, l); } + + state.cvt[l] = v * state.ppem / state.font.unitsPerEm; + } + + // DELTAC1[] DELTA exception C1 + // DELTAC2[] DELTA exception C2 + // DELTAC3[] DELTA exception C3 + // 0x73, 0x74, 0x75 + function DELTAC123(b, state) { + var stack = state.stack; + var n = stack.pop(); + var ppem = state.ppem; + var base = state.deltaBase + (b - 1) * 16; + var ds = state.deltaShift; + + if (exports.DEBUG) { console.log(state.step, 'DELTAC[' + b + ']', n, stack); } + + for (var i = 0; i < n; i++) { + var c = stack.pop(); + var arg = stack.pop(); + var appem = base + ((arg & 0xF0) >> 4); + if (appem !== ppem) { continue; } + + var mag = (arg & 0x0F) - 8; + if (mag >= 0) { mag++; } + + var delta = mag * ds; + + if (exports.DEBUG) { console.log(state.step, 'DELTACFIX', c, 'by', delta); } + + state.cvt[c] += delta; + } + } + + // SROUND[] Super ROUND + // 0x76 + function SROUND(state) { + var n = state.stack.pop(); + + if (exports.DEBUG) { console.log(state.step, 'SROUND[]', n); } + + state.round = roundSuper; + + var period; + + switch (n & 0xC0) { + case 0x00: + period = 0.5; + break; + case 0x40: + period = 1; + break; + case 0x80: + period = 2; + break; + default: + throw new Error('invalid SROUND value'); + } + + state.srPeriod = period; + + switch (n & 0x30) { + case 0x00: + state.srPhase = 0; + break; + case 0x10: + state.srPhase = 0.25 * period; + break; + case 0x20: + state.srPhase = 0.5 * period; + break; + case 0x30: + state.srPhase = 0.75 * period; + break; + default: throw new Error('invalid SROUND value'); + } + + n &= 0x0F; + + if (n === 0) { state.srThreshold = 0; } + else { state.srThreshold = (n / 8 - 0.5) * period; } + } + + // S45ROUND[] Super ROUND 45 degrees + // 0x77 + function S45ROUND(state) { + var n = state.stack.pop(); + + if (exports.DEBUG) { console.log(state.step, 'S45ROUND[]', n); } + + state.round = roundSuper; + + var period; + + switch (n & 0xC0) { + case 0x00: + period = Math.sqrt(2) / 2; + break; + case 0x40: + period = Math.sqrt(2); + break; + case 0x80: + period = 2 * Math.sqrt(2); + break; + default: + throw new Error('invalid S45ROUND value'); + } + + state.srPeriod = period; + + switch (n & 0x30) { + case 0x00: + state.srPhase = 0; + break; + case 0x10: + state.srPhase = 0.25 * period; + break; + case 0x20: + state.srPhase = 0.5 * period; + break; + case 0x30: + state.srPhase = 0.75 * period; + break; + default: + throw new Error('invalid S45ROUND value'); + } + + n &= 0x0F; + + if (n === 0) { state.srThreshold = 0; } + else { state.srThreshold = (n / 8 - 0.5) * period; } + } + + // ROFF[] Round Off + // 0x7A + function ROFF(state) { + if (exports.DEBUG) { console.log(state.step, 'ROFF[]'); } + + state.round = roundOff; + } + + // RUTG[] Round Up To Grid + // 0x7C + function RUTG(state) { + if (exports.DEBUG) { console.log(state.step, 'RUTG[]'); } + + state.round = roundUpToGrid; + } + + // RDTG[] Round Down To Grid + // 0x7D + function RDTG(state) { + if (exports.DEBUG) { console.log(state.step, 'RDTG[]'); } + + state.round = roundDownToGrid; + } + + // SCANCTRL[] SCAN conversion ConTRoL + // 0x85 + function SCANCTRL(state) { + var n = state.stack.pop(); + + // ignored by opentype.js + + if (exports.DEBUG) { console.log(state.step, 'SCANCTRL[]', n); } + } + + // SDPVTL[a] Set Dual Projection Vector To Line + // 0x86-0x87 + function SDPVTL(a, state) { + var stack = state.stack; + var p2i = stack.pop(); + var p1i = stack.pop(); + var p2 = state.z2[p2i]; + var p1 = state.z1[p1i]; + + if (exports.DEBUG) { console.log(state.step, 'SDPVTL[' + a + ']', p2i, p1i); } + + var dx; + var dy; + + if (!a) { + dx = p1.x - p2.x; + dy = p1.y - p2.y; + } else { + dx = p2.y - p1.y; + dy = p1.x - p2.x; + } + + state.dpv = getUnitVector(dx, dy); + } + + // GETINFO[] GET INFOrmation + // 0x88 + function GETINFO(state) { + var stack = state.stack; + var sel = stack.pop(); + var r = 0; + + if (exports.DEBUG) { console.log(state.step, 'GETINFO[]', sel); } + + // v35 as in no subpixel hinting + if (sel & 0x01) { r = 35; } + + // TODO rotation and stretch currently not supported + // and thus those GETINFO are always 0. + + // opentype.js is always gray scaling + if (sel & 0x20) { r |= 0x1000; } + + stack.push(r); + } + + // ROLL[] ROLL the top three stack elements + // 0x8A + function ROLL(state) { + var stack = state.stack; + var a = stack.pop(); + var b = stack.pop(); + var c = stack.pop(); + + if (exports.DEBUG) { console.log(state.step, 'ROLL[]'); } + + stack.push(b); + stack.push(a); + stack.push(c); + } + + // MAX[] MAXimum of top two stack elements + // 0x8B + function MAX(state) { + var stack = state.stack; + var e2 = stack.pop(); + var e1 = stack.pop(); + + if (exports.DEBUG) { console.log(state.step, 'MAX[]', e2, e1); } + + stack.push(Math.max(e1, e2)); + } + + // MIN[] MINimum of top two stack elements + // 0x8C + function MIN(state) { + var stack = state.stack; + var e2 = stack.pop(); + var e1 = stack.pop(); + + if (exports.DEBUG) { console.log(state.step, 'MIN[]', e2, e1); } + + stack.push(Math.min(e1, e2)); + } + + // SCANTYPE[] SCANTYPE + // 0x8D + function SCANTYPE(state) { + var n = state.stack.pop(); + // ignored by opentype.js + if (exports.DEBUG) { console.log(state.step, 'SCANTYPE[]', n); } + } + + // INSTCTRL[] INSTCTRL + // 0x8D + function INSTCTRL(state) { + var s = state.stack.pop(); + var v = state.stack.pop(); + + if (exports.DEBUG) { console.log(state.step, 'INSTCTRL[]', s, v); } + + switch (s) { + case 1 : state.inhibitGridFit = !!v; return; + case 2 : state.ignoreCvt = !!v; return; + default: throw new Error('invalid INSTCTRL[] selector'); + } + } + + // PUSHB[abc] PUSH Bytes + // 0xB0-0xB7 + function PUSHB(n, state) { + var stack = state.stack; + var prog = state.prog; + var ip = state.ip; + + if (exports.DEBUG) { console.log(state.step, 'PUSHB[' + n + ']'); } + + for (var i = 0; i < n; i++) { stack.push(prog[++ip]); } + + state.ip = ip; + } + + // PUSHW[abc] PUSH Words + // 0xB8-0xBF + function PUSHW(n, state) { + var ip = state.ip; + var prog = state.prog; + var stack = state.stack; + + if (exports.DEBUG) { console.log(state.ip, 'PUSHW[' + n + ']'); } + + for (var i = 0; i < n; i++) { + var w = (prog[++ip] << 8) | prog[++ip]; + if (w & 0x8000) { w = -((w ^ 0xffff) + 1); } + stack.push(w); + } + + state.ip = ip; + } + + // MDRP[abcde] Move Direct Relative Point + // 0xD0-0xEF + // (if indirect is 0) + // + // and + // + // MIRP[abcde] Move Indirect Relative Point + // 0xE0-0xFF + // (if indirect is 1) + + function MDRP_MIRP(indirect, setRp0, keepD, ro, dt, state) { + var stack = state.stack; + var cvte = indirect && stack.pop(); + var pi = stack.pop(); + var rp0i = state.rp0; + var rp = state.z0[rp0i]; + var p = state.z1[pi]; + + var md = state.minDis; + var fv = state.fv; + var pv = state.dpv; + var od; // original distance + var d; // moving distance + var sign; // sign of distance + var cv; + + d = od = pv.distance(p, rp, true, true); + sign = d >= 0 ? 1 : -1; // Math.sign would be 0 in case of 0 + + // TODO consider autoFlip + d = Math.abs(d); + + if (indirect) { + cv = state.cvt[cvte]; + + if (ro && Math.abs(d - cv) < state.cvCutIn) { d = cv; } + } + + if (keepD && d < md) { d = md; } + + if (ro) { d = state.round(d); } + + fv.setRelative(p, rp, sign * d, pv); + fv.touch(p); + + if (exports.DEBUG) { + console.log( + state.step, + (indirect ? 'MIRP[' : 'MDRP[') + + (setRp0 ? 'M' : 'm') + + (keepD ? '>' : '_') + + (ro ? 'R' : '_') + + (dt === 0 ? 'Gr' : (dt === 1 ? 'Bl' : (dt === 2 ? 'Wh' : ''))) + + ']', + indirect ? + cvte + '(' + state.cvt[cvte] + ',' + cv + ')' : + '', + pi, + '(d =', od, '->', sign * d, ')' + ); + } + + state.rp1 = state.rp0; + state.rp2 = pi; + if (setRp0) { state.rp0 = pi; } + } + + /* + * The instruction table. + */ + instructionTable = [ + /* 0x00 */ SVTCA.bind(undefined, yUnitVector), + /* 0x01 */ SVTCA.bind(undefined, xUnitVector), + /* 0x02 */ SPVTCA.bind(undefined, yUnitVector), + /* 0x03 */ SPVTCA.bind(undefined, xUnitVector), + /* 0x04 */ SFVTCA.bind(undefined, yUnitVector), + /* 0x05 */ SFVTCA.bind(undefined, xUnitVector), + /* 0x06 */ SPVTL.bind(undefined, 0), + /* 0x07 */ SPVTL.bind(undefined, 1), + /* 0x08 */ SFVTL.bind(undefined, 0), + /* 0x09 */ SFVTL.bind(undefined, 1), + /* 0x0A */ SPVFS, + /* 0x0B */ SFVFS, + /* 0x0C */ GPV, + /* 0x0D */ GFV, + /* 0x0E */ SFVTPV, + /* 0x0F */ ISECT, + /* 0x10 */ SRP0, + /* 0x11 */ SRP1, + /* 0x12 */ SRP2, + /* 0x13 */ SZP0, + /* 0x14 */ SZP1, + /* 0x15 */ SZP2, + /* 0x16 */ SZPS, + /* 0x17 */ SLOOP, + /* 0x18 */ RTG, + /* 0x19 */ RTHG, + /* 0x1A */ SMD, + /* 0x1B */ ELSE, + /* 0x1C */ JMPR, + /* 0x1D */ SCVTCI, + /* 0x1E */ undefined, // TODO SSWCI + /* 0x1F */ undefined, // TODO SSW + /* 0x20 */ DUP, + /* 0x21 */ POP, + /* 0x22 */ CLEAR, + /* 0x23 */ SWAP, + /* 0x24 */ DEPTH, + /* 0x25 */ CINDEX, + /* 0x26 */ MINDEX, + /* 0x27 */ undefined, // TODO ALIGNPTS + /* 0x28 */ undefined, + /* 0x29 */ undefined, // TODO UTP + /* 0x2A */ LOOPCALL, + /* 0x2B */ CALL, + /* 0x2C */ FDEF, + /* 0x2D */ undefined, // ENDF (eaten by FDEF) + /* 0x2E */ MDAP.bind(undefined, 0), + /* 0x2F */ MDAP.bind(undefined, 1), + /* 0x30 */ IUP.bind(undefined, yUnitVector), + /* 0x31 */ IUP.bind(undefined, xUnitVector), + /* 0x32 */ SHP.bind(undefined, 0), + /* 0x33 */ SHP.bind(undefined, 1), + /* 0x34 */ SHC.bind(undefined, 0), + /* 0x35 */ SHC.bind(undefined, 1), + /* 0x36 */ SHZ.bind(undefined, 0), + /* 0x37 */ SHZ.bind(undefined, 1), + /* 0x38 */ SHPIX, + /* 0x39 */ IP, + /* 0x3A */ MSIRP.bind(undefined, 0), + /* 0x3B */ MSIRP.bind(undefined, 1), + /* 0x3C */ ALIGNRP, + /* 0x3D */ RTDG, + /* 0x3E */ MIAP.bind(undefined, 0), + /* 0x3F */ MIAP.bind(undefined, 1), + /* 0x40 */ NPUSHB, + /* 0x41 */ NPUSHW, + /* 0x42 */ WS, + /* 0x43 */ RS, + /* 0x44 */ WCVTP, + /* 0x45 */ RCVT, + /* 0x46 */ GC.bind(undefined, 0), + /* 0x47 */ GC.bind(undefined, 1), + /* 0x48 */ undefined, // TODO SCFS + /* 0x49 */ MD.bind(undefined, 0), + /* 0x4A */ MD.bind(undefined, 1), + /* 0x4B */ MPPEM, + /* 0x4C */ undefined, // TODO MPS + /* 0x4D */ FLIPON, + /* 0x4E */ undefined, // TODO FLIPOFF + /* 0x4F */ undefined, // TODO DEBUG + /* 0x50 */ LT, + /* 0x51 */ LTEQ, + /* 0x52 */ GT, + /* 0x53 */ GTEQ, + /* 0x54 */ EQ, + /* 0x55 */ NEQ, + /* 0x56 */ ODD, + /* 0x57 */ EVEN, + /* 0x58 */ IF, + /* 0x59 */ EIF, + /* 0x5A */ AND, + /* 0x5B */ OR, + /* 0x5C */ NOT, + /* 0x5D */ DELTAP123.bind(undefined, 1), + /* 0x5E */ SDB, + /* 0x5F */ SDS, + /* 0x60 */ ADD, + /* 0x61 */ SUB, + /* 0x62 */ DIV, + /* 0x63 */ MUL, + /* 0x64 */ ABS, + /* 0x65 */ NEG, + /* 0x66 */ FLOOR, + /* 0x67 */ CEILING, + /* 0x68 */ ROUND.bind(undefined, 0), + /* 0x69 */ ROUND.bind(undefined, 1), + /* 0x6A */ ROUND.bind(undefined, 2), + /* 0x6B */ ROUND.bind(undefined, 3), + /* 0x6C */ undefined, // TODO NROUND[ab] + /* 0x6D */ undefined, // TODO NROUND[ab] + /* 0x6E */ undefined, // TODO NROUND[ab] + /* 0x6F */ undefined, // TODO NROUND[ab] + /* 0x70 */ WCVTF, + /* 0x71 */ DELTAP123.bind(undefined, 2), + /* 0x72 */ DELTAP123.bind(undefined, 3), + /* 0x73 */ DELTAC123.bind(undefined, 1), + /* 0x74 */ DELTAC123.bind(undefined, 2), + /* 0x75 */ DELTAC123.bind(undefined, 3), + /* 0x76 */ SROUND, + /* 0x77 */ S45ROUND, + /* 0x78 */ undefined, // TODO JROT[] + /* 0x79 */ undefined, // TODO JROF[] + /* 0x7A */ ROFF, + /* 0x7B */ undefined, + /* 0x7C */ RUTG, + /* 0x7D */ RDTG, + /* 0x7E */ POP, // actually SANGW, supposed to do only a pop though + /* 0x7F */ POP, // actually AA, supposed to do only a pop though + /* 0x80 */ undefined, // TODO FLIPPT + /* 0x81 */ undefined, // TODO FLIPRGON + /* 0x82 */ undefined, // TODO FLIPRGOFF + /* 0x83 */ undefined, + /* 0x84 */ undefined, + /* 0x85 */ SCANCTRL, + /* 0x86 */ SDPVTL.bind(undefined, 0), + /* 0x87 */ SDPVTL.bind(undefined, 1), + /* 0x88 */ GETINFO, + /* 0x89 */ undefined, // TODO IDEF + /* 0x8A */ ROLL, + /* 0x8B */ MAX, + /* 0x8C */ MIN, + /* 0x8D */ SCANTYPE, + /* 0x8E */ INSTCTRL, + /* 0x8F */ undefined, + /* 0x90 */ undefined, + /* 0x91 */ undefined, + /* 0x92 */ undefined, + /* 0x93 */ undefined, + /* 0x94 */ undefined, + /* 0x95 */ undefined, + /* 0x96 */ undefined, + /* 0x97 */ undefined, + /* 0x98 */ undefined, + /* 0x99 */ undefined, + /* 0x9A */ undefined, + /* 0x9B */ undefined, + /* 0x9C */ undefined, + /* 0x9D */ undefined, + /* 0x9E */ undefined, + /* 0x9F */ undefined, + /* 0xA0 */ undefined, + /* 0xA1 */ undefined, + /* 0xA2 */ undefined, + /* 0xA3 */ undefined, + /* 0xA4 */ undefined, + /* 0xA5 */ undefined, + /* 0xA6 */ undefined, + /* 0xA7 */ undefined, + /* 0xA8 */ undefined, + /* 0xA9 */ undefined, + /* 0xAA */ undefined, + /* 0xAB */ undefined, + /* 0xAC */ undefined, + /* 0xAD */ undefined, + /* 0xAE */ undefined, + /* 0xAF */ undefined, + /* 0xB0 */ PUSHB.bind(undefined, 1), + /* 0xB1 */ PUSHB.bind(undefined, 2), + /* 0xB2 */ PUSHB.bind(undefined, 3), + /* 0xB3 */ PUSHB.bind(undefined, 4), + /* 0xB4 */ PUSHB.bind(undefined, 5), + /* 0xB5 */ PUSHB.bind(undefined, 6), + /* 0xB6 */ PUSHB.bind(undefined, 7), + /* 0xB7 */ PUSHB.bind(undefined, 8), + /* 0xB8 */ PUSHW.bind(undefined, 1), + /* 0xB9 */ PUSHW.bind(undefined, 2), + /* 0xBA */ PUSHW.bind(undefined, 3), + /* 0xBB */ PUSHW.bind(undefined, 4), + /* 0xBC */ PUSHW.bind(undefined, 5), + /* 0xBD */ PUSHW.bind(undefined, 6), + /* 0xBE */ PUSHW.bind(undefined, 7), + /* 0xBF */ PUSHW.bind(undefined, 8), + /* 0xC0 */ MDRP_MIRP.bind(undefined, 0, 0, 0, 0, 0), + /* 0xC1 */ MDRP_MIRP.bind(undefined, 0, 0, 0, 0, 1), + /* 0xC2 */ MDRP_MIRP.bind(undefined, 0, 0, 0, 0, 2), + /* 0xC3 */ MDRP_MIRP.bind(undefined, 0, 0, 0, 0, 3), + /* 0xC4 */ MDRP_MIRP.bind(undefined, 0, 0, 0, 1, 0), + /* 0xC5 */ MDRP_MIRP.bind(undefined, 0, 0, 0, 1, 1), + /* 0xC6 */ MDRP_MIRP.bind(undefined, 0, 0, 0, 1, 2), + /* 0xC7 */ MDRP_MIRP.bind(undefined, 0, 0, 0, 1, 3), + /* 0xC8 */ MDRP_MIRP.bind(undefined, 0, 0, 1, 0, 0), + /* 0xC9 */ MDRP_MIRP.bind(undefined, 0, 0, 1, 0, 1), + /* 0xCA */ MDRP_MIRP.bind(undefined, 0, 0, 1, 0, 2), + /* 0xCB */ MDRP_MIRP.bind(undefined, 0, 0, 1, 0, 3), + /* 0xCC */ MDRP_MIRP.bind(undefined, 0, 0, 1, 1, 0), + /* 0xCD */ MDRP_MIRP.bind(undefined, 0, 0, 1, 1, 1), + /* 0xCE */ MDRP_MIRP.bind(undefined, 0, 0, 1, 1, 2), + /* 0xCF */ MDRP_MIRP.bind(undefined, 0, 0, 1, 1, 3), + /* 0xD0 */ MDRP_MIRP.bind(undefined, 0, 1, 0, 0, 0), + /* 0xD1 */ MDRP_MIRP.bind(undefined, 0, 1, 0, 0, 1), + /* 0xD2 */ MDRP_MIRP.bind(undefined, 0, 1, 0, 0, 2), + /* 0xD3 */ MDRP_MIRP.bind(undefined, 0, 1, 0, 0, 3), + /* 0xD4 */ MDRP_MIRP.bind(undefined, 0, 1, 0, 1, 0), + /* 0xD5 */ MDRP_MIRP.bind(undefined, 0, 1, 0, 1, 1), + /* 0xD6 */ MDRP_MIRP.bind(undefined, 0, 1, 0, 1, 2), + /* 0xD7 */ MDRP_MIRP.bind(undefined, 0, 1, 0, 1, 3), + /* 0xD8 */ MDRP_MIRP.bind(undefined, 0, 1, 1, 0, 0), + /* 0xD9 */ MDRP_MIRP.bind(undefined, 0, 1, 1, 0, 1), + /* 0xDA */ MDRP_MIRP.bind(undefined, 0, 1, 1, 0, 2), + /* 0xDB */ MDRP_MIRP.bind(undefined, 0, 1, 1, 0, 3), + /* 0xDC */ MDRP_MIRP.bind(undefined, 0, 1, 1, 1, 0), + /* 0xDD */ MDRP_MIRP.bind(undefined, 0, 1, 1, 1, 1), + /* 0xDE */ MDRP_MIRP.bind(undefined, 0, 1, 1, 1, 2), + /* 0xDF */ MDRP_MIRP.bind(undefined, 0, 1, 1, 1, 3), + /* 0xE0 */ MDRP_MIRP.bind(undefined, 1, 0, 0, 0, 0), + /* 0xE1 */ MDRP_MIRP.bind(undefined, 1, 0, 0, 0, 1), + /* 0xE2 */ MDRP_MIRP.bind(undefined, 1, 0, 0, 0, 2), + /* 0xE3 */ MDRP_MIRP.bind(undefined, 1, 0, 0, 0, 3), + /* 0xE4 */ MDRP_MIRP.bind(undefined, 1, 0, 0, 1, 0), + /* 0xE5 */ MDRP_MIRP.bind(undefined, 1, 0, 0, 1, 1), + /* 0xE6 */ MDRP_MIRP.bind(undefined, 1, 0, 0, 1, 2), + /* 0xE7 */ MDRP_MIRP.bind(undefined, 1, 0, 0, 1, 3), + /* 0xE8 */ MDRP_MIRP.bind(undefined, 1, 0, 1, 0, 0), + /* 0xE9 */ MDRP_MIRP.bind(undefined, 1, 0, 1, 0, 1), + /* 0xEA */ MDRP_MIRP.bind(undefined, 1, 0, 1, 0, 2), + /* 0xEB */ MDRP_MIRP.bind(undefined, 1, 0, 1, 0, 3), + /* 0xEC */ MDRP_MIRP.bind(undefined, 1, 0, 1, 1, 0), + /* 0xED */ MDRP_MIRP.bind(undefined, 1, 0, 1, 1, 1), + /* 0xEE */ MDRP_MIRP.bind(undefined, 1, 0, 1, 1, 2), + /* 0xEF */ MDRP_MIRP.bind(undefined, 1, 0, 1, 1, 3), + /* 0xF0 */ MDRP_MIRP.bind(undefined, 1, 1, 0, 0, 0), + /* 0xF1 */ MDRP_MIRP.bind(undefined, 1, 1, 0, 0, 1), + /* 0xF2 */ MDRP_MIRP.bind(undefined, 1, 1, 0, 0, 2), + /* 0xF3 */ MDRP_MIRP.bind(undefined, 1, 1, 0, 0, 3), + /* 0xF4 */ MDRP_MIRP.bind(undefined, 1, 1, 0, 1, 0), + /* 0xF5 */ MDRP_MIRP.bind(undefined, 1, 1, 0, 1, 1), + /* 0xF6 */ MDRP_MIRP.bind(undefined, 1, 1, 0, 1, 2), + /* 0xF7 */ MDRP_MIRP.bind(undefined, 1, 1, 0, 1, 3), + /* 0xF8 */ MDRP_MIRP.bind(undefined, 1, 1, 1, 0, 0), + /* 0xF9 */ MDRP_MIRP.bind(undefined, 1, 1, 1, 0, 1), + /* 0xFA */ MDRP_MIRP.bind(undefined, 1, 1, 1, 0, 2), + /* 0xFB */ MDRP_MIRP.bind(undefined, 1, 1, 1, 0, 3), + /* 0xFC */ MDRP_MIRP.bind(undefined, 1, 1, 1, 1, 0), + /* 0xFD */ MDRP_MIRP.bind(undefined, 1, 1, 1, 1, 1), + /* 0xFE */ MDRP_MIRP.bind(undefined, 1, 1, 1, 1, 2), + /* 0xFF */ MDRP_MIRP.bind(undefined, 1, 1, 1, 1, 3) + ]; + + /***************************** + Mathematical Considerations + ****************************** + + fv ... refers to freedom vector + pv ... refers to projection vector + rp ... refers to reference point + p ... refers to to point being operated on + d ... refers to distance + + SETRELATIVE: + ============ + + case freedom vector == x-axis: + ------------------------------ + + (pv) + .-' + rpd .-' + .-* + d .-'90°' + .-' ' + .-' ' + *-' ' b + rp ' + ' + ' + p *----------*-------------- (fv) + pm + + rpdx = rpx + d * pv.x + rpdy = rpy + d * pv.y + + equation of line b + + y - rpdy = pvns * (x- rpdx) + + y = p.y + + x = rpdx + ( p.y - rpdy ) / pvns + + + case freedom vector == y-axis: + ------------------------------ + + * pm + |\ + | \ + | \ + | \ + | \ + | \ + | \ + | \ + | \ + | \ b + | \ + | \ + | \ .-' (pv) + | 90° \.-' + | .-'* rpd + | .-' + * *-' d + p rp + + rpdx = rpx + d * pv.x + rpdy = rpy + d * pv.y + + equation of line b: + pvns ... normal slope to pv + + y - rpdy = pvns * (x - rpdx) + + x = p.x + + y = rpdy + pvns * (p.x - rpdx) + + + + generic case: + ------------- + + + .'(fv) + .' + .* pm + .' ! + .' . + .' ! + .' . b + .' ! + * . + p ! + 90° . ... (pv) + ...-*-''' + ...---''' rpd + ...---''' d + *--''' + rp + + rpdx = rpx + d * pv.x + rpdy = rpy + d * pv.y + + equation of line b: + pvns... normal slope to pv + + y - rpdy = pvns * (x - rpdx) + + equation of freedom vector line: + fvs ... slope of freedom vector (=fy/fx) + + y - py = fvs * (x - px) + + + on pm both equations are true for same x/y + + y - rpdy = pvns * (x - rpdx) + + y - py = fvs * (x - px) + + form to y and set equal: + + pvns * (x - rpdx) + rpdy = fvs * (x - px) + py + + expand: + + pvns * x - pvns * rpdx + rpdy = fvs * x - fvs * px + py + + switch: + + fvs * x - fvs * px + py = pvns * x - pvns * rpdx + rpdy + + solve for x: + + fvs * x - pvns * x = fvs * px - pvns * rpdx - py + rpdy + + + + fvs * px - pvns * rpdx + rpdy - py + x = ----------------------------------- + fvs - pvns + + and: + + y = fvs * (x - px) + py + + + + INTERPOLATE: + ============ + + Examples of point interpolation. + + The weight of the movement of the reference point gets bigger + the further the other reference point is away, thus the safest + option (that is avoiding 0/0 divisions) is to weight the + original distance of the other point by the sum of both distances. + + If the sum of both distances is 0, then move the point by the + arithmetic average of the movement of both reference points. + + + + + (+6) + rp1o *---->*rp1 + . . (+12) + . . rp2o *---------->* rp2 + . . . . + . . . . + . 10 20 . . + |.........|...................| . + . . . + . . (+8) . + po *------>*p . + . . . + . 12 . 24 . + |...........|.......................| + 36 + + + ------- + + + + (+10) + rp1o *-------->*rp1 + . . (-10) + . . rp2 *<---------* rpo2 + . . . . + . . . . + . 10 . 30 . . + |.........|.............................| + . . + . (+5) . + po *--->* p . + . . . + . . 20 . + |....|..............| + 5 15 + + + ------- + + + (+10) + rp1o *-------->*rp1 + . . + . . + rp2o *-------->*rp2 + + + (+10) + po *-------->* p + + ------- + + + (+10) + rp1o *-------->*rp1 + . . + . .(+30) + rp2o *---------------------------->*rp2 + + + (+25) + po *----------------------->* p + + + + vim: set ts=4 sw=4 expandtab: + *****/ + + /** + * Converts a string into a list of tokens. + */ + + /** + * Create a new token + * @param {string} char a single char + */ + function Token(char) { + this.char = char; + this.state = {}; + this.activeState = null; + } + + /** + * Create a new context range + * @param {number} startIndex range start index + * @param {number} endOffset range end index offset + * @param {string} contextName owner context name + */ + function ContextRange(startIndex, endOffset, contextName) { + this.contextName = contextName; + this.startIndex = startIndex; + this.endOffset = endOffset; + } + + /** + * Check context start and end + * @param {string} contextName a unique context name + * @param {function} checkStart a predicate function the indicates a context's start + * @param {function} checkEnd a predicate function the indicates a context's end + */ + function ContextChecker(contextName, checkStart, checkEnd) { + this.contextName = contextName; + this.openRange = null; + this.ranges = []; + this.checkStart = checkStart; + this.checkEnd = checkEnd; + } + + /** + * @typedef ContextParams + * @type Object + * @property {array} context context items + * @property {number} currentIndex current item index + */ + + /** + * Create a context params + * @param {array} context a list of items + * @param {number} currentIndex current item index + */ + function ContextParams(context, currentIndex) { + this.context = context; + this.index = currentIndex; + this.length = context.length; + this.current = context[currentIndex]; + this.backtrack = context.slice(0, currentIndex); + this.lookahead = context.slice(currentIndex + 1); + } + + /** + * Create an event instance + * @param {string} eventId event unique id + */ + function Event(eventId) { + this.eventId = eventId; + this.subscribers = []; + } + + /** + * Initialize a core events and auto subscribe required event handlers + * @param {any} events an object that enlists core events handlers + */ + function initializeCoreEvents(events) { + var this$1 = this; + + var coreEvents = [ + 'start', 'end', 'next', 'newToken', 'contextStart', + 'contextEnd', 'insertToken', 'removeToken', 'removeRange', + 'replaceToken', 'replaceRange', 'composeRUD', 'updateContextsRanges' + ]; + + coreEvents.forEach(function (eventId) { + Object.defineProperty(this$1.events, eventId, { + value: new Event(eventId) + }); + }); + + if (!!events) { + coreEvents.forEach(function (eventId) { + var event = events[eventId]; + if (typeof event === 'function') { + this$1.events[eventId].subscribe(event); + } + }); + } + var requiresContextUpdate = [ + 'insertToken', 'removeToken', 'removeRange', + 'replaceToken', 'replaceRange', 'composeRUD' + ]; + requiresContextUpdate.forEach(function (eventId) { + this$1.events[eventId].subscribe( + this$1.updateContextsRanges + ); + }); + } + + /** + * Converts a string into a list of tokens + * @param {any} events tokenizer core events + */ + function Tokenizer(events) { + this.tokens = []; + this.registeredContexts = {}; + this.contextCheckers = []; + this.events = {}; + this.registeredModifiers = []; + + initializeCoreEvents.call(this, events); + } + + /** + * Sets the state of a token, usually called by a state modifier. + * @param {string} key state item key + * @param {any} value state item value + */ + Token.prototype.setState = function(key, value) { + this.state[key] = value; + this.activeState = { key: key, value: this.state[key] }; + return this.activeState; + }; + + Token.prototype.getState = function (stateId) { + return this.state[stateId] || null; + }; + + /** + * Checks if an index exists in the tokens list. + * @param {number} index token index + */ + Tokenizer.prototype.inboundIndex = function(index) { + return index >= 0 && index < this.tokens.length; + }; + + /** + * Compose and apply a list of operations (replace, update, delete) + * @param {array} RUDs replace, update and delete operations + * TODO: Perf. Optimization (lengthBefore === lengthAfter ? dispatch once) + */ + Tokenizer.prototype.composeRUD = function (RUDs) { + var this$1 = this; + + var silent = true; + var state = RUDs.map(function (RUD) { return ( + this$1[RUD[0]].apply(this$1, RUD.slice(1).concat(silent)) + ); }); + var hasFAILObject = function (obj) { return ( + typeof obj === 'object' && + obj.hasOwnProperty('FAIL') + ); }; + if (state.every(hasFAILObject)) { + return { + FAIL: "composeRUD: one or more operations hasn't completed successfully", + report: state.filter(hasFAILObject) + }; + } + this.dispatch('composeRUD', [state.filter(function (op) { return !hasFAILObject(op); })]); + }; + + /** + * Replace a range of tokens with a list of tokens + * @param {number} startIndex range start index + * @param {number} offset range offset + * @param {token} tokens a list of tokens to replace + * @param {boolean} silent dispatch events and update context ranges + */ + Tokenizer.prototype.replaceRange = function (startIndex, offset, tokens, silent) { + offset = offset !== null ? offset : this.tokens.length; + var isTokenType = tokens.every(function (token) { return token instanceof Token; }); + if (!isNaN(startIndex) && this.inboundIndex(startIndex) && isTokenType) { + var replaced = this.tokens.splice.apply( + this.tokens, [startIndex, offset].concat(tokens) + ); + if (!silent) { this.dispatch('replaceToken', [startIndex, offset, tokens]); } + return [replaced, tokens]; + } else { + return { FAIL: 'replaceRange: invalid tokens or startIndex.' }; + } + }; + + /** + * Replace a token with another token + * @param {number} index token index + * @param {token} token a token to replace + * @param {boolean} silent dispatch events and update context ranges + */ + Tokenizer.prototype.replaceToken = function (index, token, silent) { + if (!isNaN(index) && this.inboundIndex(index) && token instanceof Token) { + var replaced = this.tokens.splice(index, 1, token); + if (!silent) { this.dispatch('replaceToken', [index, token]); } + return [replaced[0], token]; + } else { + return { FAIL: 'replaceToken: invalid token or index.' }; + } + }; + + /** + * Removes a range of tokens + * @param {number} startIndex range start index + * @param {number} offset range offset + * @param {boolean} silent dispatch events and update context ranges + */ + Tokenizer.prototype.removeRange = function(startIndex, offset, silent) { + offset = !isNaN(offset) ? offset : this.tokens.length; + var tokens = this.tokens.splice(startIndex, offset); + if (!silent) { this.dispatch('removeRange', [tokens, startIndex, offset]); } + return tokens; + }; + + /** + * Remove a token at a certain index + * @param {number} index token index + * @param {boolean} silent dispatch events and update context ranges + */ + Tokenizer.prototype.removeToken = function(index, silent) { + if (!isNaN(index) && this.inboundIndex(index)) { + var token = this.tokens.splice(index, 1); + if (!silent) { this.dispatch('removeToken', [token, index]); } + return token; + } else { + return { FAIL: 'removeToken: invalid token index.' }; + } + }; + + /** + * Insert a list of tokens at a certain index + * @param {array} tokens a list of tokens to insert + * @param {number} index insert the list of tokens at index + * @param {boolean} silent dispatch events and update context ranges + */ + Tokenizer.prototype.insertToken = function (tokens, index, silent) { + var tokenType = tokens.every( + function (token) { return token instanceof Token; } + ); + if (tokenType) { + this.tokens.splice.apply( + this.tokens, [index, 0].concat(tokens) + ); + if (!silent) { this.dispatch('insertToken', [tokens, index]); } + return tokens; + } else { + return { FAIL: 'insertToken: invalid token(s).' }; + } + }; + + /** + * A state modifier that is called on 'newToken' event + * @param {string} modifierId state modifier id + * @param {function} condition a predicate function that returns true or false + * @param {function} modifier a function to update token state + */ + Tokenizer.prototype.registerModifier = function(modifierId, condition, modifier) { + this.events.newToken.subscribe(function(token, contextParams) { + var conditionParams = [token, contextParams]; + var canApplyModifier = ( + condition === null || + condition.apply(this, conditionParams) === true + ); + var modifierParams = [token, contextParams]; + if (canApplyModifier) { + var newStateValue = modifier.apply(this, modifierParams); + token.setState(modifierId, newStateValue); + } + }); + this.registeredModifiers.push(modifierId); + }; + + /** + * Subscribe a handler to an event + * @param {function} eventHandler an event handler function + */ + Event.prototype.subscribe = function (eventHandler) { + if (typeof eventHandler === 'function') { + return ((this.subscribers.push(eventHandler)) - 1); + } else { + return { FAIL: ("invalid '" + (this.eventId) + "' event handler")}; + } + }; + + /** + * Unsubscribe an event handler + * @param {string} subsId subscription id + */ + Event.prototype.unsubscribe = function (subsId) { + this.subscribers.splice(subsId, 1); + }; + + /** + * Sets context params current value index + * @param {number} index context params current value index + */ + ContextParams.prototype.setCurrentIndex = function(index) { + this.index = index; + this.current = this.context[index]; + this.backtrack = this.context.slice(0, index); + this.lookahead = this.context.slice(index + 1); + }; + + /** + * Get an item at an offset from the current value + * example (current value is 3): + * 1 2 [3] 4 5 | items values + * -2 -1 0 1 2 | offset values + * @param {number} offset an offset from current value index + */ + ContextParams.prototype.get = function (offset) { + switch (true) { + case (offset === 0): + return this.current; + case (offset < 0 && Math.abs(offset) <= this.backtrack.length): + return this.backtrack.slice(offset)[0]; + case (offset > 0 && offset <= this.lookahead.length): + return this.lookahead[offset - 1]; + default: + return null; + } + }; + + /** + * Converts a context range into a string value + * @param {contextRange} range a context range + */ + Tokenizer.prototype.rangeToText = function (range) { + if (range instanceof ContextRange) { + return ( + this.getRangeTokens(range) + .map(function (token) { return token.char; }).join('') + ); + } + }; + + /** + * Converts all tokens into a string + */ + Tokenizer.prototype.getText = function () { + return this.tokens.map(function (token) { return token.char; }).join(''); + }; + + /** + * Get a context by name + * @param {string} contextName context name to get + */ + Tokenizer.prototype.getContext = function (contextName) { + var context = this.registeredContexts[contextName]; + return !!context ? context : null; + }; + + /** + * Subscribes a new event handler to an event + * @param {string} eventName event name to subscribe to + * @param {function} eventHandler a function to be invoked on event + */ + Tokenizer.prototype.on = function(eventName, eventHandler) { + var event = this.events[eventName]; + if (!!event) { + return event.subscribe(eventHandler); + } else { + return null; + } + }; + + /** + * Dispatches an event + * @param {string} eventName event name + * @param {any} args event handler arguments + */ + Tokenizer.prototype.dispatch = function(eventName, args) { + var this$1 = this; + + var event = this.events[eventName]; + if (event instanceof Event) { + event.subscribers.forEach(function (subscriber) { + subscriber.apply(this$1, args || []); + }); + } + }; + + /** + * Register a new context checker + * @param {string} contextName a unique context name + * @param {function} contextStartCheck a predicate function that returns true on context start + * @param {function} contextEndCheck a predicate function that returns true on context end + * TODO: call tokenize on registration to update context ranges with the new context. + */ + Tokenizer.prototype.registerContextChecker = function(contextName, contextStartCheck, contextEndCheck) { + if (!!this.getContext(contextName)) { return { + FAIL: + ("context name '" + contextName + "' is already registered.") + }; } + if (typeof contextStartCheck !== 'function') { return { + FAIL: + "missing context start check." + }; } + if (typeof contextEndCheck !== 'function') { return { + FAIL: + "missing context end check." + }; } + var contextCheckers = new ContextChecker( + contextName, contextStartCheck, contextEndCheck + ); + this.registeredContexts[contextName] = contextCheckers; + this.contextCheckers.push(contextCheckers); + return contextCheckers; + }; + + /** + * Gets a context range tokens + * @param {contextRange} range a context range + */ + Tokenizer.prototype.getRangeTokens = function(range) { + var endIndex = range.startIndex + range.endOffset; + return [].concat( + this.tokens + .slice(range.startIndex, endIndex) + ); + }; + + /** + * Gets the ranges of a context + * @param {string} contextName context name + */ + Tokenizer.prototype.getContextRanges = function(contextName) { + var context = this.getContext(contextName); + if (!!context) { + return context.ranges; + } else { + return { FAIL: ("context checker '" + contextName + "' is not registered.") }; + } + }; + + /** + * Resets context ranges to run context update + */ + Tokenizer.prototype.resetContextsRanges = function () { + var registeredContexts = this.registeredContexts; + for (var contextName in registeredContexts) { + if (registeredContexts.hasOwnProperty(contextName)) { + var context = registeredContexts[contextName]; + context.ranges = []; + } + } + }; + + /** + * Updates context ranges + */ + Tokenizer.prototype.updateContextsRanges = function () { + this.resetContextsRanges(); + var chars = this.tokens.map(function (token) { return token.char; }); + for (var i = 0; i < chars.length; i++) { + var contextParams = new ContextParams(chars, i); + this.runContextCheck(contextParams); + } + this.dispatch('updateContextsRanges', [this.registeredContexts]); + }; + + /** + * Sets the end offset of an open range + * @param {number} offset range end offset + * @param {string} contextName context name + */ + Tokenizer.prototype.setEndOffset = function (offset, contextName) { + var startIndex = this.getContext(contextName).openRange.startIndex; + var range = new ContextRange(startIndex, offset, contextName); + var ranges = this.getContext(contextName).ranges; + range.rangeId = contextName + "." + (ranges.length); + ranges.push(range); + this.getContext(contextName).openRange = null; + return range; + }; + + /** + * Runs a context check on the current context + * @param {contextParams} contextParams current context params + */ + Tokenizer.prototype.runContextCheck = function(contextParams) { + var this$1 = this; + + var index = contextParams.index; + this.contextCheckers.forEach(function (contextChecker) { + var contextName = contextChecker.contextName; + var openRange = this$1.getContext(contextName).openRange; + if (!openRange && contextChecker.checkStart(contextParams)) { + openRange = new ContextRange(index, null, contextName); + this$1.getContext(contextName).openRange = openRange; + this$1.dispatch('contextStart', [contextName, index]); + } + if (!!openRange && contextChecker.checkEnd(contextParams)) { + var offset = (index - openRange.startIndex) + 1; + var range = this$1.setEndOffset(offset, contextName); + this$1.dispatch('contextEnd', [contextName, range]); + } + }); + }; + + /** + * Converts a text into a list of tokens + * @param {string} text a text to tokenize + */ + Tokenizer.prototype.tokenize = function (text) { + this.tokens = []; + this.resetContextsRanges(); + var chars = Array.from(text); + this.dispatch('start'); + for (var i = 0; i < chars.length; i++) { + var char = chars[i]; + var contextParams = new ContextParams(chars, i); + this.dispatch('next', [contextParams]); + this.runContextCheck(contextParams); + var token = new Token(char); + this.tokens.push(token); + this.dispatch('newToken', [token, contextParams]); + } + this.dispatch('end', [this.tokens]); + return this.tokens; + }; + + // ╭─┄┄┄────────────────────────┄─────────────────────────────────────────────╮ + // ┊ Character Class Assertions ┊ Checks if a char belongs to a certain class ┊ + // ╰─╾──────────────────────────┄─────────────────────────────────────────────╯ + // jscs:disable maximumLineLength + /** + * Check if a char is Arabic + * @param {string} c a single char + */ + function isArabicChar(c) { + return /[\u0600-\u065F\u066A-\u06D2\u06FA-\u06FF]/.test(c); + } + + /** + * Check if a char is an isolated arabic char + * @param {string} c a single char + */ + function isIsolatedArabicChar(char) { + return /[\u0630\u0690\u0621\u0631\u0661\u0671\u0622\u0632\u0672\u0692\u06C2\u0623\u0673\u0693\u06C3\u0624\u0694\u06C4\u0625\u0675\u0695\u06C5\u06E5\u0676\u0696\u06C6\u0627\u0677\u0697\u06C7\u0648\u0688\u0698\u06C8\u0689\u0699\u06C9\u068A\u06CA\u066B\u068B\u06CB\u068C\u068D\u06CD\u06FD\u068E\u06EE\u06FE\u062F\u068F\u06CF\u06EF]/.test(char); + } + + /** + * Check if a char is an Arabic Tashkeel char + * @param {string} c a single char + */ + function isTashkeelArabicChar(char) { + return /[\u0600-\u0605\u060C-\u060E\u0610-\u061B\u061E\u064B-\u065F\u0670\u06D6-\u06DC\u06DF-\u06E4\u06E7\u06E8\u06EA-\u06ED]/.test(char); + } + + /** + * Check if a char is Latin + * @param {string} c a single char + */ + function isLatinChar(c) { + return /[A-z]/.test(c); + } + + /** + * Check if a char is whitespace char + * @param {string} c a single char + */ + function isWhiteSpace(c) { + return /\s/.test(c); + } + + /** + * Query a feature by some of it's properties to lookup a glyph substitution. + */ + + /** + * Create feature query instance + * @param {Font} font opentype font instance + */ + function FeatureQuery(font) { + this.font = font; + this.features = {}; + } + + /** + * @typedef SubstitutionAction + * @type Object + * @property {number} id substitution type + * @property {string} tag feature tag + * @property {any} substitution substitution value(s) + */ + + /** + * Create a substitution action instance + * @param {SubstitutionAction} action + */ + function SubstitutionAction(action) { + this.id = action.id; + this.tag = action.tag; + this.substitution = action.substitution; + } + + /** + * Lookup a coverage table + * @param {number} glyphIndex glyph index + * @param {CoverageTable} coverage coverage table + */ + function lookupCoverage(glyphIndex, coverage) { + if (!glyphIndex) { return -1; } + switch (coverage.format) { + case 1: + return coverage.glyphs.indexOf(glyphIndex); + + case 2: + var ranges = coverage.ranges; + for (var i = 0; i < ranges.length; i++) { + var range = ranges[i]; + if (glyphIndex >= range.start && glyphIndex <= range.end) { + var offset = glyphIndex - range.start; + return range.index + offset; + } + } + break; + default: + return -1; // not found + } + return -1; + } + + /** + * Handle a single substitution - format 1 + * @param {ContextParams} contextParams context params to lookup + */ + function singleSubstitutionFormat1(glyphIndex, subtable) { + var substituteIndex = lookupCoverage(glyphIndex, subtable.coverage); + if (substituteIndex === -1) { return null; } + return glyphIndex + subtable.deltaGlyphId; + } + + /** + * Handle a single substitution - format 2 + * @param {ContextParams} contextParams context params to lookup + */ + function singleSubstitutionFormat2(glyphIndex, subtable) { + var substituteIndex = lookupCoverage(glyphIndex, subtable.coverage); + if (substituteIndex === -1) { return null; } + return subtable.substitute[substituteIndex]; + } + + /** + * Lookup a list of coverage tables + * @param {any} coverageList a list of coverage tables + * @param {ContextParams} contextParams context params to lookup + */ + function lookupCoverageList(coverageList, contextParams) { + var lookupList = []; + for (var i = 0; i < coverageList.length; i++) { + var coverage = coverageList[i]; + var glyphIndex = contextParams.current; + glyphIndex = Array.isArray(glyphIndex) ? glyphIndex[0] : glyphIndex; + var lookupIndex = lookupCoverage(glyphIndex, coverage); + if (lookupIndex !== -1) { + lookupList.push(lookupIndex); + } + } + if (lookupList.length !== coverageList.length) { return -1; } + return lookupList; + } + + /** + * Handle chaining context substitution - format 3 + * @param {ContextParams} contextParams context params to lookup + */ + function chainingSubstitutionFormat3(contextParams, subtable) { + var lookupsCount = ( + subtable.inputCoverage.length + + subtable.lookaheadCoverage.length + + subtable.backtrackCoverage.length + ); + if (contextParams.context.length < lookupsCount) { return []; } + // INPUT LOOKUP // + var inputLookups = lookupCoverageList( + subtable.inputCoverage, contextParams + ); + if (inputLookups === -1) { return []; } + // LOOKAHEAD LOOKUP // + var lookaheadOffset = subtable.inputCoverage.length - 1; + if (contextParams.lookahead.length < subtable.lookaheadCoverage.length) { return []; } + var lookaheadContext = contextParams.lookahead.slice(lookaheadOffset); + while (lookaheadContext.length && isTashkeelArabicChar(lookaheadContext[0].char)) { + lookaheadContext.shift(); + } + var lookaheadParams = new ContextParams(lookaheadContext, 0); + var lookaheadLookups = lookupCoverageList( + subtable.lookaheadCoverage, lookaheadParams + ); + // BACKTRACK LOOKUP // + var backtrackContext = [].concat(contextParams.backtrack); + backtrackContext.reverse(); + while (backtrackContext.length && isTashkeelArabicChar(backtrackContext[0].char)) { + backtrackContext.shift(); + } + if (backtrackContext.length < subtable.backtrackCoverage.length) { return []; } + var backtrackParams = new ContextParams(backtrackContext, 0); + var backtrackLookups = lookupCoverageList( + subtable.backtrackCoverage, backtrackParams + ); + var contextRulesMatch = ( + inputLookups.length === subtable.inputCoverage.length && + lookaheadLookups.length === subtable.lookaheadCoverage.length && + backtrackLookups.length === subtable.backtrackCoverage.length + ); + var substitutions = []; + if (contextRulesMatch) { + for (var i = 0; i < subtable.lookupRecords.length; i++) { + var lookupRecord = subtable.lookupRecords[i]; + var lookupListIndex = lookupRecord.lookupListIndex; + var lookupTable = this.getLookupByIndex(lookupListIndex); + for (var s = 0; s < lookupTable.subtables.length; s++) { + var subtable$1 = lookupTable.subtables[s]; + var lookup = this.getLookupMethod(lookupTable, subtable$1); + var substitutionType = this.getSubstitutionType(lookupTable, subtable$1); + if (substitutionType === '12') { + for (var n = 0; n < inputLookups.length; n++) { + var glyphIndex = contextParams.get(n); + var substitution = lookup(glyphIndex); + if (substitution) { substitutions.push(substitution); } + } + } + } + } + } + return substitutions; + } + + /** + * Handle ligature substitution - format 1 + * @param {ContextParams} contextParams context params to lookup + */ + function ligatureSubstitutionFormat1(contextParams, subtable) { + // COVERAGE LOOKUP // + var glyphIndex = contextParams.current; + var ligSetIndex = lookupCoverage(glyphIndex, subtable.coverage); + if (ligSetIndex === -1) { return null; } + // COMPONENTS LOOKUP + // (!) note, components are ordered in the written direction. + var ligature; + var ligatureSet = subtable.ligatureSets[ligSetIndex]; + for (var s = 0; s < ligatureSet.length; s++) { + ligature = ligatureSet[s]; + for (var l = 0; l < ligature.components.length; l++) { + var lookaheadItem = contextParams.lookahead[l]; + var component = ligature.components[l]; + if (lookaheadItem !== component) { break; } + if (l === ligature.components.length - 1) { return ligature; } + } + } + return null; + } + + /** + * Handle decomposition substitution - format 1 + * @param {number} glyphIndex glyph index + * @param {any} subtable subtable + */ + function decompositionSubstitutionFormat1(glyphIndex, subtable) { + var substituteIndex = lookupCoverage(glyphIndex, subtable.coverage); + if (substituteIndex === -1) { return null; } + return subtable.sequences[substituteIndex]; + } + + /** + * Get default script features indexes + */ + FeatureQuery.prototype.getDefaultScriptFeaturesIndexes = function () { + var scripts = this.font.tables.gsub.scripts; + for (var s = 0; s < scripts.length; s++) { + var script = scripts[s]; + if (script.tag === 'DFLT') { return ( + script.script.defaultLangSys.featureIndexes + ); } + } + return []; + }; + + /** + * Get feature indexes of a specific script + * @param {string} scriptTag script tag + */ + FeatureQuery.prototype.getScriptFeaturesIndexes = function(scriptTag) { + var tables = this.font.tables; + if (!tables.gsub) { return []; } + if (!scriptTag) { return this.getDefaultScriptFeaturesIndexes(); } + var scripts = this.font.tables.gsub.scripts; + for (var i = 0; i < scripts.length; i++) { + var script = scripts[i]; + if (script.tag === scriptTag && script.script.defaultLangSys) { + return script.script.defaultLangSys.featureIndexes; + } else { + var langSysRecords = script.langSysRecords; + if (!!langSysRecords) { + for (var j = 0; j < langSysRecords.length; j++) { + var langSysRecord = langSysRecords[j]; + if (langSysRecord.tag === scriptTag) { + var langSys = langSysRecord.langSys; + return langSys.featureIndexes; + } + } + } + } + } + return this.getDefaultScriptFeaturesIndexes(); + }; + + /** + * Map a feature tag to a gsub feature + * @param {any} features gsub features + * @param {string} scriptTag script tag + */ + FeatureQuery.prototype.mapTagsToFeatures = function (features, scriptTag) { + var tags = {}; + for (var i = 0; i < features.length; i++) { + var tag = features[i].tag; + var feature = features[i].feature; + tags[tag] = feature; + } + this.features[scriptTag].tags = tags; + }; + + /** + * Get features of a specific script + * @param {string} scriptTag script tag + */ + FeatureQuery.prototype.getScriptFeatures = function (scriptTag) { + var features = this.features[scriptTag]; + if (this.features.hasOwnProperty(scriptTag)) { return features; } + var featuresIndexes = this.getScriptFeaturesIndexes(scriptTag); + if (!featuresIndexes) { return null; } + var gsub = this.font.tables.gsub; + features = featuresIndexes.map(function (index) { return gsub.features[index]; }); + this.features[scriptTag] = features; + this.mapTagsToFeatures(features, scriptTag); + return features; + }; + + /** + * Get substitution type + * @param {any} lookupTable lookup table + * @param {any} subtable subtable + */ + FeatureQuery.prototype.getSubstitutionType = function(lookupTable, subtable) { + var lookupType = lookupTable.lookupType.toString(); + var substFormat = subtable.substFormat.toString(); + return lookupType + substFormat; + }; + + /** + * Get lookup method + * @param {any} lookupTable lookup table + * @param {any} subtable subtable + */ + FeatureQuery.prototype.getLookupMethod = function(lookupTable, subtable) { + var this$1 = this; + + var substitutionType = this.getSubstitutionType(lookupTable, subtable); + switch (substitutionType) { + case '11': + return function (glyphIndex) { return singleSubstitutionFormat1.apply( + this$1, [glyphIndex, subtable] + ); }; + case '12': + return function (glyphIndex) { return singleSubstitutionFormat2.apply( + this$1, [glyphIndex, subtable] + ); }; + case '63': + return function (contextParams) { return chainingSubstitutionFormat3.apply( + this$1, [contextParams, subtable] + ); }; + case '41': + return function (contextParams) { return ligatureSubstitutionFormat1.apply( + this$1, [contextParams, subtable] + ); }; + case '21': + return function (glyphIndex) { return decompositionSubstitutionFormat1.apply( + this$1, [glyphIndex, subtable] + ); }; + default: + throw new Error( + "lookupType: " + (lookupTable.lookupType) + " - " + + "substFormat: " + (subtable.substFormat) + " " + + "is not yet supported" + ); + } + }; + + /** + * [ LOOKUP TYPES ] + * ------------------------------- + * Single 1; + * Multiple 2; + * Alternate 3; + * Ligature 4; + * Context 5; + * ChainingContext 6; + * ExtensionSubstitution 7; + * ReverseChainingContext 8; + * ------------------------------- + * + */ + + /** + * @typedef FQuery + * @type Object + * @param {string} tag feature tag + * @param {string} script feature script + * @param {ContextParams} contextParams context params + */ + + /** + * Lookup a feature using a query parameters + * @param {FQuery} query feature query + */ + FeatureQuery.prototype.lookupFeature = function (query) { + var contextParams = query.contextParams; + var currentIndex = contextParams.index; + var feature = this.getFeature({ + tag: query.tag, script: query.script + }); + if (!feature) { return new Error( + "font '" + (this.font.names.fullName.en) + "' " + + "doesn't support feature '" + (query.tag) + "' " + + "for script '" + (query.script) + "'." + ); } + var lookups = this.getFeatureLookups(feature); + var substitutions = [].concat(contextParams.context); + for (var l = 0; l < lookups.length; l++) { + var lookupTable = lookups[l]; + var subtables = this.getLookupSubtables(lookupTable); + for (var s = 0; s < subtables.length; s++) { + var subtable = subtables[s]; + var substType = this.getSubstitutionType(lookupTable, subtable); + var lookup = this.getLookupMethod(lookupTable, subtable); + var substitution = (void 0); + switch (substType) { + case '11': + substitution = lookup(contextParams.current); + if (substitution) { + substitutions.splice(currentIndex, 1, new SubstitutionAction({ + id: 11, tag: query.tag, substitution: substitution + })); + } + break; + case '12': + substitution = lookup(contextParams.current); + if (substitution) { + substitutions.splice(currentIndex, 1, new SubstitutionAction({ + id: 12, tag: query.tag, substitution: substitution + })); + } + break; + case '63': + substitution = lookup(contextParams); + if (Array.isArray(substitution) && substitution.length) { + substitutions.splice(currentIndex, 1, new SubstitutionAction({ + id: 63, tag: query.tag, substitution: substitution + })); + } + break; + case '41': + substitution = lookup(contextParams); + if (substitution) { + substitutions.splice(currentIndex, 1, new SubstitutionAction({ + id: 41, tag: query.tag, substitution: substitution + })); + } + break; + case '21': + substitution = lookup(contextParams.current); + if (substitution) { + substitutions.splice(currentIndex, 1, new SubstitutionAction({ + id: 21, tag: query.tag, substitution: substitution + })); + } + break; + } + contextParams = new ContextParams(substitutions, currentIndex); + if (Array.isArray(substitution) && !substitution.length) { continue; } + substitution = null; + } + } + return substitutions.length ? substitutions : null; + }; + + /** + * Checks if a font supports a specific features + * @param {FQuery} query feature query object + */ + FeatureQuery.prototype.supports = function (query) { + if (!query.script) { return false; } + this.getScriptFeatures(query.script); + var supportedScript = this.features.hasOwnProperty(query.script); + if (!query.tag) { return supportedScript; } + var supportedFeature = ( + this.features[query.script].some(function (feature) { return feature.tag === query.tag; }) + ); + return supportedScript && supportedFeature; + }; + + /** + * Get lookup table subtables + * @param {any} lookupTable lookup table + */ + FeatureQuery.prototype.getLookupSubtables = function (lookupTable) { + return lookupTable.subtables || null; + }; + + /** + * Get lookup table by index + * @param {number} index lookup table index + */ + FeatureQuery.prototype.getLookupByIndex = function (index) { + var lookups = this.font.tables.gsub.lookups; + return lookups[index] || null; + }; + + /** + * Get lookup tables for a feature + * @param {string} feature + */ + FeatureQuery.prototype.getFeatureLookups = function (feature) { + // TODO: memoize + return feature.lookupListIndexes.map(this.getLookupByIndex.bind(this)); + }; + + /** + * Query a feature by it's properties + * @param {any} query an object that describes the properties of a query + */ + FeatureQuery.prototype.getFeature = function getFeature(query) { + if (!this.font) { return { FAIL: "No font was found"}; } + if (!this.features.hasOwnProperty(query.script)) { + this.getScriptFeatures(query.script); + } + var scriptFeatures = this.features[query.script]; + if (!scriptFeatures) { return ( + { FAIL: ("No feature for script " + (query.script))} + ); } + if (!scriptFeatures.tags[query.tag]) { return null; } + return this.features[query.script].tags[query.tag]; + }; + + /** + * Arabic word context checkers + */ + + function arabicWordStartCheck(contextParams) { + var char = contextParams.current; + var prevChar = contextParams.get(-1); + return ( + // ? arabic first char + (prevChar === null && isArabicChar(char)) || + // ? arabic char preceded with a non arabic char + (!isArabicChar(prevChar) && isArabicChar(char)) + ); + } + + function arabicWordEndCheck(contextParams) { + var nextChar = contextParams.get(1); + return ( + // ? last arabic char + (nextChar === null) || + // ? next char is not arabic + (!isArabicChar(nextChar)) + ); + } + + var arabicWordCheck = { + startCheck: arabicWordStartCheck, + endCheck: arabicWordEndCheck + }; + + /** + * Arabic sentence context checkers + */ + + function arabicSentenceStartCheck(contextParams) { + var char = contextParams.current; + var prevChar = contextParams.get(-1); + return ( + // ? an arabic char preceded with a non arabic char + (isArabicChar(char) || isTashkeelArabicChar(char)) && + !isArabicChar(prevChar) + ); + } + + function arabicSentenceEndCheck(contextParams) { + var nextChar = contextParams.get(1); + switch (true) { + case nextChar === null: + return true; + case (!isArabicChar(nextChar) && !isTashkeelArabicChar(nextChar)): + var nextIsWhitespace = isWhiteSpace(nextChar); + if (!nextIsWhitespace) { return true; } + if (nextIsWhitespace) { + var arabicCharAhead = false; + arabicCharAhead = ( + contextParams.lookahead.some( + function (c) { return isArabicChar(c) || isTashkeelArabicChar(c); } + ) + ); + if (!arabicCharAhead) { return true; } + } + break; + default: + return false; + } + } + + var arabicSentenceCheck = { + startCheck: arabicSentenceStartCheck, + endCheck: arabicSentenceEndCheck + }; + + /** + * Apply single substitution format 1 + * @param {Array} substitutions substitutions + * @param {any} tokens a list of tokens + * @param {number} index token index + */ + function singleSubstitutionFormat1$1(action, tokens, index) { + tokens[index].setState(action.tag, action.substitution); + } + + /** + * Apply single substitution format 2 + * @param {Array} substitutions substitutions + * @param {any} tokens a list of tokens + * @param {number} index token index + */ + function singleSubstitutionFormat2$1(action, tokens, index) { + tokens[index].setState(action.tag, action.substitution); + } + + /** + * Apply chaining context substitution format 3 + * @param {Array} substitutions substitutions + * @param {any} tokens a list of tokens + * @param {number} index token index + */ + function chainingSubstitutionFormat3$1(action, tokens, index) { + action.substitution.forEach(function (subst, offset) { + var token = tokens[index + offset]; + token.setState(action.tag, subst); + }); + } + + /** + * Apply ligature substitution format 1 + * @param {Array} substitutions substitutions + * @param {any} tokens a list of tokens + * @param {number} index token index + */ + function ligatureSubstitutionFormat1$1(action, tokens, index) { + var token = tokens[index]; + token.setState(action.tag, action.substitution.ligGlyph); + var compsCount = action.substitution.components.length; + for (var i = 0; i < compsCount; i++) { + token = tokens[index + i + 1]; + token.setState('deleted', true); + } + } + + /** + * Supported substitutions + */ + var SUBSTITUTIONS = { + 11: singleSubstitutionFormat1$1, + 12: singleSubstitutionFormat2$1, + 63: chainingSubstitutionFormat3$1, + 41: ligatureSubstitutionFormat1$1 + }; + + /** + * Apply substitutions to a list of tokens + * @param {Array} substitutions substitutions + * @param {any} tokens a list of tokens + * @param {number} index token index + */ + function applySubstitution(action, tokens, index) { + if (action instanceof SubstitutionAction && SUBSTITUTIONS[action.id]) { + SUBSTITUTIONS[action.id](action, tokens, index); + } + } + + /** + * Apply Arabic presentation forms to a range of tokens + */ + + /** + * Check if a char can be connected to it's preceding char + * @param {ContextParams} charContextParams context params of a char + */ + function willConnectPrev(charContextParams) { + var backtrack = [].concat(charContextParams.backtrack); + for (var i = backtrack.length - 1; i >= 0; i--) { + var prevChar = backtrack[i]; + var isolated = isIsolatedArabicChar(prevChar); + var tashkeel = isTashkeelArabicChar(prevChar); + if (!isolated && !tashkeel) { return true; } + if (isolated) { return false; } + } + return false; + } + + /** + * Check if a char can be connected to it's proceeding char + * @param {ContextParams} charContextParams context params of a char + */ + function willConnectNext(charContextParams) { + if (isIsolatedArabicChar(charContextParams.current)) { return false; } + for (var i = 0; i < charContextParams.lookahead.length; i++) { + var nextChar = charContextParams.lookahead[i]; + var tashkeel = isTashkeelArabicChar(nextChar); + if (!tashkeel) { return true; } + } + return false; + } + + /** + * Apply arabic presentation forms to a list of tokens + * @param {ContextRange} range a range of tokens + */ + function arabicPresentationForms(range) { + var this$1 = this; + + var script = 'arab'; + var tags = this.featuresTags[script]; + var tokens = this.tokenizer.getRangeTokens(range); + if (tokens.length === 1) { return; } + var contextParams = new ContextParams( + tokens.map(function (token) { return token.getState('glyphIndex'); } + ), 0); + var charContextParams = new ContextParams( + tokens.map(function (token) { return token.char; } + ), 0); + tokens.forEach(function (token, index) { + if (isTashkeelArabicChar(token.char)) { return; } + contextParams.setCurrentIndex(index); + charContextParams.setCurrentIndex(index); + var CONNECT = 0; // 2 bits 00 (10: can connect next) (01: can connect prev) + if (willConnectPrev(charContextParams)) { CONNECT |= 1; } + if (willConnectNext(charContextParams)) { CONNECT |= 2; } + var tag; + switch (CONNECT) { + case 1: (tag = 'fina'); break; + case 2: (tag = 'init'); break; + case 3: (tag = 'medi'); break; + } + if (tags.indexOf(tag) === -1) { return; } + var substitutions = this$1.query.lookupFeature({ + tag: tag, script: script, contextParams: contextParams + }); + if (substitutions instanceof Error) { return console.info(substitutions.message); } + substitutions.forEach(function (action, index) { + if (action instanceof SubstitutionAction) { + applySubstitution(action, tokens, index); + contextParams.context[index] = action.substitution; + } + }); + }); + } + + /** + * Apply Arabic required ligatures feature to a range of tokens + */ + + /** + * Update context params + * @param {any} tokens a list of tokens + * @param {number} index current item index + */ + function getContextParams(tokens, index) { + var context = tokens.map(function (token) { return token.activeState.value; }); + return new ContextParams(context, index || 0); + } + + /** + * Apply Arabic required ligatures to a context range + * @param {ContextRange} range a range of tokens + */ + function arabicRequiredLigatures(range) { + var this$1 = this; + + var script = 'arab'; + var tokens = this.tokenizer.getRangeTokens(range); + var contextParams = getContextParams(tokens); + contextParams.context.forEach(function (glyphIndex, index) { + contextParams.setCurrentIndex(index); + var substitutions = this$1.query.lookupFeature({ + tag: 'rlig', script: script, contextParams: contextParams + }); + if (substitutions.length) { + substitutions.forEach( + function (action) { return applySubstitution(action, tokens, index); } + ); + contextParams = getContextParams(tokens); + } + }); + } + + /** + * Latin word context checkers + */ + + function latinWordStartCheck(contextParams) { + var char = contextParams.current; + var prevChar = contextParams.get(-1); + return ( + // ? latin first char + (prevChar === null && isLatinChar(char)) || + // ? latin char preceded with a non latin char + (!isLatinChar(prevChar) && isLatinChar(char)) + ); + } + + function latinWordEndCheck(contextParams) { + var nextChar = contextParams.get(1); + return ( + // ? last latin char + (nextChar === null) || + // ? next char is not latin + (!isLatinChar(nextChar)) + ); + } + + var latinWordCheck = { + startCheck: latinWordStartCheck, + endCheck: latinWordEndCheck + }; + + /** + * Apply Latin ligature feature to a range of tokens + */ + + /** + * Update context params + * @param {any} tokens a list of tokens + * @param {number} index current item index + */ + function getContextParams$1(tokens, index) { + var context = tokens.map(function (token) { return token.activeState.value; }); + return new ContextParams(context, index || 0); + } + + /** + * Apply Arabic required ligatures to a context range + * @param {ContextRange} range a range of tokens + */ + function latinLigature(range) { + var this$1 = this; + + var script = 'latn'; + var tokens = this.tokenizer.getRangeTokens(range); + var contextParams = getContextParams$1(tokens); + contextParams.context.forEach(function (glyphIndex, index) { + contextParams.setCurrentIndex(index); + var substitutions = this$1.query.lookupFeature({ + tag: 'liga', script: script, contextParams: contextParams + }); + if (substitutions.length) { + substitutions.forEach( + function (action) { return applySubstitution(action, tokens, index); } + ); + contextParams = getContextParams$1(tokens); + } + }); + } + + /** + * Infer bidirectional properties for a given text and apply + * the corresponding layout rules. + */ + + /** + * Create Bidi. features + * @param {string} baseDir text base direction. value either 'ltr' or 'rtl' + */ + function Bidi(baseDir) { + this.baseDir = baseDir || 'ltr'; + this.tokenizer = new Tokenizer(); + this.featuresTags = {}; + } + + /** + * Sets Bidi text + * @param {string} text a text input + */ + Bidi.prototype.setText = function (text) { + this.text = text; + }; + + /** + * Store essential context checks: + * arabic word check for applying gsub features + * arabic sentence check for adjusting arabic layout + */ + Bidi.prototype.contextChecks = ({ + latinWordCheck: latinWordCheck, + arabicWordCheck: arabicWordCheck, + arabicSentenceCheck: arabicSentenceCheck + }); + + /** + * Register arabic word check + */ + function registerContextChecker(checkId) { + var check = this.contextChecks[(checkId + "Check")]; + return this.tokenizer.registerContextChecker( + checkId, check.startCheck, check.endCheck + ); + } + + /** + * Perform pre tokenization procedure then + * tokenize text input + */ + function tokenizeText() { + registerContextChecker.call(this, 'latinWord'); + registerContextChecker.call(this, 'arabicWord'); + registerContextChecker.call(this, 'arabicSentence'); + return this.tokenizer.tokenize(this.text); + } + + /** + * Reverse arabic sentence layout + * TODO: check base dir before applying adjustments - priority low + */ + function reverseArabicSentences() { + var this$1 = this; + + var ranges = this.tokenizer.getContextRanges('arabicSentence'); + ranges.forEach(function (range) { + var rangeTokens = this$1.tokenizer.getRangeTokens(range); + this$1.tokenizer.replaceRange( + range.startIndex, + range.endOffset, + rangeTokens.reverse() + ); + }); + } + + /** + * Register supported features tags + * @param {script} script script tag + * @param {Array} tags features tags list + */ + Bidi.prototype.registerFeatures = function (script, tags) { + var this$1 = this; + + var supportedTags = tags.filter( + function (tag) { return this$1.query.supports({script: script, tag: tag}); } + ); + if (!this.featuresTags.hasOwnProperty(script)) { + this.featuresTags[script] = supportedTags; + } else { + this.featuresTags[script] = + this.featuresTags[script].concat(supportedTags); + } + }; + + /** + * Apply GSUB features + * @param {Array} tagsList a list of features tags + * @param {string} script a script tag + * @param {Font} font opentype font instance + */ + Bidi.prototype.applyFeatures = function (font, features) { + if (!font) { throw new Error( + 'No valid font was provided to apply features' + ); } + if (!this.query) { this.query = new FeatureQuery(font); } + for (var f = 0; f < features.length; f++) { + var feature = features[f]; + if (!this.query.supports({script: feature.script})) { continue; } + this.registerFeatures(feature.script, feature.tags); + } + }; + + /** + * Register a state modifier + * @param {string} modifierId state modifier id + * @param {function} condition a predicate function that returns true or false + * @param {function} modifier a modifier function to set token state + */ + Bidi.prototype.registerModifier = function (modifierId, condition, modifier) { + this.tokenizer.registerModifier(modifierId, condition, modifier); + }; + + /** + * Check if 'glyphIndex' is registered + */ + function checkGlyphIndexStatus() { + if (this.tokenizer.registeredModifiers.indexOf('glyphIndex') === -1) { + throw new Error( + 'glyphIndex modifier is required to apply ' + + 'arabic presentation features.' + ); + } + } + + /** + * Apply arabic presentation forms features + */ + function applyArabicPresentationForms() { + var this$1 = this; + + var script = 'arab'; + if (!this.featuresTags.hasOwnProperty(script)) { return; } + checkGlyphIndexStatus.call(this); + var ranges = this.tokenizer.getContextRanges('arabicWord'); + ranges.forEach(function (range) { + arabicPresentationForms.call(this$1, range); + }); + } + + /** + * Apply required arabic ligatures + */ + function applyArabicRequireLigatures() { + var this$1 = this; + + var script = 'arab'; + if (!this.featuresTags.hasOwnProperty(script)) { return; } + var tags = this.featuresTags[script]; + if (tags.indexOf('rlig') === -1) { return; } + checkGlyphIndexStatus.call(this); + var ranges = this.tokenizer.getContextRanges('arabicWord'); + ranges.forEach(function (range) { + arabicRequiredLigatures.call(this$1, range); + }); + } + + /** + * Apply required arabic ligatures + */ + function applyLatinLigatures() { + var this$1 = this; + + var script = 'latn'; + if (!this.featuresTags.hasOwnProperty(script)) { return; } + var tags = this.featuresTags[script]; + if (tags.indexOf('liga') === -1) { return; } + checkGlyphIndexStatus.call(this); + var ranges = this.tokenizer.getContextRanges('latinWord'); + ranges.forEach(function (range) { + latinLigature.call(this$1, range); + }); + } + + /** + * Check if a context is registered + * @param {string} contextId context id + */ + Bidi.prototype.checkContextReady = function (contextId) { + return !!this.tokenizer.getContext(contextId); + }; + + /** + * Apply features to registered contexts + */ + Bidi.prototype.applyFeaturesToContexts = function () { + if (this.checkContextReady('arabicWord')) { + applyArabicPresentationForms.call(this); + applyArabicRequireLigatures.call(this); + } + if (this.checkContextReady('latinWord')) { + applyLatinLigatures.call(this); + } + if (this.checkContextReady('arabicSentence')) { + reverseArabicSentences.call(this); + } + }; + + /** + * process text input + * @param {string} text an input text + */ + Bidi.prototype.processText = function(text) { + if (!this.text || this.text !== text) { + this.setText(text); + tokenizeText.call(this); + this.applyFeaturesToContexts(); + } + }; + + /** + * Process a string of text to identify and adjust + * bidirectional text entities. + * @param {string} text input text + */ + Bidi.prototype.getBidiText = function (text) { + this.processText(text); + return this.tokenizer.getText(); + }; + + /** + * Get the current state index of each token + * @param {text} text an input text + */ + Bidi.prototype.getTextGlyphs = function (text) { + this.processText(text); + var indexes = []; + for (var i = 0; i < this.tokenizer.tokens.length; i++) { + var token = this.tokenizer.tokens[i]; + if (token.state.deleted) { continue; } + var index = token.activeState.value; + indexes.push(Array.isArray(index) ? index[0] : index); + } + return indexes; + }; + + // The Font object + + /** + * @typedef FontOptions + * @type Object + * @property {Boolean} empty - whether to create a new empty font + * @property {string} familyName + * @property {string} styleName + * @property {string=} fullName + * @property {string=} postScriptName + * @property {string=} designer + * @property {string=} designerURL + * @property {string=} manufacturer + * @property {string=} manufacturerURL + * @property {string=} license + * @property {string=} licenseURL + * @property {string=} version + * @property {string=} description + * @property {string=} copyright + * @property {string=} trademark + * @property {Number} unitsPerEm + * @property {Number} ascender + * @property {Number} descender + * @property {Number} createdTimestamp + * @property {string=} weightClass + * @property {string=} widthClass + * @property {string=} fsSelection + */ + + /** + * A Font represents a loaded OpenType font file. + * It contains a set of glyphs and methods to draw text on a drawing context, + * or to get a path representing the text. + * @exports opentype.Font + * @class + * @param {FontOptions} + * @constructor + */ + function Font(options) { + options = options || {}; + options.tables = options.tables || {}; + + if (!options.empty) { + // Check that we've provided the minimum set of names. + checkArgument(options.familyName, 'When creating a new Font object, familyName is required.'); + checkArgument(options.styleName, 'When creating a new Font object, styleName is required.'); + checkArgument(options.unitsPerEm, 'When creating a new Font object, unitsPerEm is required.'); + checkArgument(options.ascender, 'When creating a new Font object, ascender is required.'); + checkArgument(options.descender <= 0, 'When creating a new Font object, negative descender value is required.'); + + // OS X will complain if the names are empty, so we put a single space everywhere by default. + this.names = { + fontFamily: {en: options.familyName || ' '}, + fontSubfamily: {en: options.styleName || ' '}, + fullName: {en: options.fullName || options.familyName + ' ' + options.styleName}, + // postScriptName may not contain any whitespace + postScriptName: {en: options.postScriptName || (options.familyName + options.styleName).replace(/\s/g, '')}, + designer: {en: options.designer || ' '}, + designerURL: {en: options.designerURL || ' '}, + manufacturer: {en: options.manufacturer || ' '}, + manufacturerURL: {en: options.manufacturerURL || ' '}, + license: {en: options.license || ' '}, + licenseURL: {en: options.licenseURL || ' '}, + version: {en: options.version || 'Version 0.1'}, + description: {en: options.description || ' '}, + copyright: {en: options.copyright || ' '}, + trademark: {en: options.trademark || ' '} + }; + this.unitsPerEm = options.unitsPerEm || 1000; + this.ascender = options.ascender; + this.descender = options.descender; + this.createdTimestamp = options.createdTimestamp; + this.tables = Object.assign(options.tables, { + os2: Object.assign({ + usWeightClass: options.weightClass || this.usWeightClasses.MEDIUM, + usWidthClass: options.widthClass || this.usWidthClasses.MEDIUM, + fsSelection: options.fsSelection || this.fsSelectionValues.REGULAR, + }, options.tables.os2) + }); + } + + this.supported = true; // Deprecated: parseBuffer will throw an error if font is not supported. + this.glyphs = new glyphset.GlyphSet(this, options.glyphs || []); + this.encoding = new DefaultEncoding(this); + this.position = new Position(this); + this.substitution = new Substitution(this); + this.tables = this.tables || {}; + + // needed for low memory mode only. + this._push = null; + this._hmtxTableData = {}; + + Object.defineProperty(this, 'hinting', { + get: function() { + if (this._hinting) { return this._hinting; } + if (this.outlinesFormat === 'truetype') { + return (this._hinting = new Hinting(this)); + } + } + }); + } + + /** + * Check if the font has a glyph for the given character. + * @param {string} + * @return {Boolean} + */ + Font.prototype.hasChar = function(c) { + return this.encoding.charToGlyphIndex(c) !== null; + }; + + /** + * Convert the given character to a single glyph index. + * Note that this function assumes that there is a one-to-one mapping between + * the given character and a glyph; for complex scripts this might not be the case. + * @param {string} + * @return {Number} + */ + Font.prototype.charToGlyphIndex = function(s) { + return this.encoding.charToGlyphIndex(s); + }; + + /** + * Convert the given character to a single Glyph object. + * Note that this function assumes that there is a one-to-one mapping between + * the given character and a glyph; for complex scripts this might not be the case. + * @param {string} + * @return {opentype.Glyph} + */ + Font.prototype.charToGlyph = function(c) { + var glyphIndex = this.charToGlyphIndex(c); + var glyph = this.glyphs.get(glyphIndex); + if (!glyph) { + // .notdef + glyph = this.glyphs.get(0); + } + + return glyph; + }; + + /** + * Update features + * @param {any} options features options + */ + Font.prototype.updateFeatures = function (options) { + // TODO: update all features options not only 'latn'. + return this.defaultRenderOptions.features.map(function (feature) { + if (feature.script === 'latn') { + return { + script: 'latn', + tags: feature.tags.filter(function (tag) { return options[tag]; }) + }; + } else { + return feature; + } + }); + }; + + /** + * Convert the given text to a list of Glyph objects. + * Note that there is no strict one-to-one mapping between characters and + * glyphs, so the list of returned glyphs can be larger or smaller than the + * length of the given string. + * @param {string} + * @param {GlyphRenderOptions} [options] + * @return {opentype.Glyph[]} + */ + Font.prototype.stringToGlyphs = function(s, options) { + var this$1 = this; + + + var bidi = new Bidi(); + + // Create and register 'glyphIndex' state modifier + var charToGlyphIndexMod = function (token) { return this$1.charToGlyphIndex(token.char); }; + bidi.registerModifier('glyphIndex', null, charToGlyphIndexMod); + + // roll-back to default features + var features = options ? + this.updateFeatures(options.features) : + this.defaultRenderOptions.features; + + bidi.applyFeatures(this, features); + + var indexes = bidi.getTextGlyphs(s); + + var length = indexes.length; + + // convert glyph indexes to glyph objects + var glyphs = new Array(length); + var notdef = this.glyphs.get(0); + for (var i = 0; i < length; i += 1) { + glyphs[i] = this.glyphs.get(indexes[i]) || notdef; + } + return glyphs; + }; + + /** + * @param {string} + * @return {Number} + */ + Font.prototype.nameToGlyphIndex = function(name) { + return this.glyphNames.nameToGlyphIndex(name); + }; + + /** + * @param {string} + * @return {opentype.Glyph} + */ + Font.prototype.nameToGlyph = function(name) { + var glyphIndex = this.nameToGlyphIndex(name); + var glyph = this.glyphs.get(glyphIndex); + if (!glyph) { + // .notdef + glyph = this.glyphs.get(0); + } + + return glyph; + }; + + /** + * @param {Number} + * @return {String} + */ + Font.prototype.glyphIndexToName = function(gid) { + if (!this.glyphNames.glyphIndexToName) { + return ''; + } + + return this.glyphNames.glyphIndexToName(gid); + }; + + /** + * Retrieve the value of the kerning pair between the left glyph (or its index) + * and the right glyph (or its index). If no kerning pair is found, return 0. + * The kerning value gets added to the advance width when calculating the spacing + * between glyphs. + * For GPOS kerning, this method uses the default script and language, which covers + * most use cases. To have greater control, use font.position.getKerningValue . + * @param {opentype.Glyph} leftGlyph + * @param {opentype.Glyph} rightGlyph + * @return {Number} + */ + Font.prototype.getKerningValue = function(leftGlyph, rightGlyph) { + leftGlyph = leftGlyph.index || leftGlyph; + rightGlyph = rightGlyph.index || rightGlyph; + var gposKerning = this.position.defaultKerningTables; + if (gposKerning) { + return this.position.getKerningValue(gposKerning, leftGlyph, rightGlyph); + } + // "kern" table + return this.kerningPairs[leftGlyph + ',' + rightGlyph] || 0; + }; + + /** + * @typedef GlyphRenderOptions + * @type Object + * @property {string} [script] - script used to determine which features to apply. By default, 'DFLT' or 'latn' is used. + * See https://www.microsoft.com/typography/otspec/scripttags.htm + * @property {string} [language='dflt'] - language system used to determine which features to apply. + * See https://www.microsoft.com/typography/developers/opentype/languagetags.aspx + * @property {boolean} [kerning=true] - whether to include kerning values + * @property {object} [features] - OpenType Layout feature tags. Used to enable or disable the features of the given script/language system. + * See https://www.microsoft.com/typography/otspec/featuretags.htm + */ + Font.prototype.defaultRenderOptions = { + kerning: true, + features: [ + /** + * these 4 features are required to render Arabic text properly + * and shouldn't be turned off when rendering arabic text. + */ + { script: 'arab', tags: ['init', 'medi', 'fina', 'rlig'] }, + { script: 'latn', tags: ['liga', 'rlig'] } + ] + }; + + /** + * Helper function that invokes the given callback for each glyph in the given text. + * The callback gets `(glyph, x, y, fontSize, options)`.* @param {string} text + * @param {string} text - The text to apply. + * @param {number} [x=0] - Horizontal position of the beginning of the text. + * @param {number} [y=0] - Vertical position of the *baseline* of the text. + * @param {number} [fontSize=72] - Font size in pixels. We scale the glyph units by `1 / unitsPerEm * fontSize`. + * @param {GlyphRenderOptions=} options + * @param {Function} callback + */ + Font.prototype.forEachGlyph = function(text, x, y, fontSize, options, callback) { + x = x !== undefined ? x : 0; + y = y !== undefined ? y : 0; + fontSize = fontSize !== undefined ? fontSize : 72; + options = Object.assign({}, this.defaultRenderOptions, options); + var fontScale = 1 / this.unitsPerEm * fontSize; + var glyphs = this.stringToGlyphs(text, options); + var kerningLookups; + if (options.kerning) { + var script = options.script || this.position.getDefaultScriptName(); + kerningLookups = this.position.getKerningTables(script, options.language); + } + for (var i = 0; i < glyphs.length; i += 1) { + var glyph = glyphs[i]; + callback.call(this, glyph, x, y, fontSize, options); + if (glyph.advanceWidth) { + x += glyph.advanceWidth * fontScale; + } + + if (options.kerning && i < glyphs.length - 1) { + // We should apply position adjustment lookups in a more generic way. + // Here we only use the xAdvance value. + var kerningValue = kerningLookups ? + this.position.getKerningValue(kerningLookups, glyph.index, glyphs[i + 1].index) : + this.getKerningValue(glyph, glyphs[i + 1]); + x += kerningValue * fontScale; + } + + if (options.letterSpacing) { + x += options.letterSpacing * fontSize; + } else if (options.tracking) { + x += (options.tracking / 1000) * fontSize; + } + } + return x; + }; + + /** + * Create a Path object that represents the given text. + * @param {string} text - The text to create. + * @param {number} [x=0] - Horizontal position of the beginning of the text. + * @param {number} [y=0] - Vertical position of the *baseline* of the text. + * @param {number} [fontSize=72] - Font size in pixels. We scale the glyph units by `1 / unitsPerEm * fontSize`. + * @param {GlyphRenderOptions=} options + * @return {opentype.Path} + */ + Font.prototype.getPath = function(text, x, y, fontSize, options) { + var fullPath = new Path(); + this.forEachGlyph(text, x, y, fontSize, options, function(glyph, gX, gY, gFontSize) { + var glyphPath = glyph.getPath(gX, gY, gFontSize, options, this); + fullPath.extend(glyphPath); + }); + return fullPath; + }; + + /** + * Create an array of Path objects that represent the glyphs of a given text. + * @param {string} text - The text to create. + * @param {number} [x=0] - Horizontal position of the beginning of the text. + * @param {number} [y=0] - Vertical position of the *baseline* of the text. + * @param {number} [fontSize=72] - Font size in pixels. We scale the glyph units by `1 / unitsPerEm * fontSize`. + * @param {GlyphRenderOptions=} options + * @return {opentype.Path[]} + */ + Font.prototype.getPaths = function(text, x, y, fontSize, options) { + var glyphPaths = []; + this.forEachGlyph(text, x, y, fontSize, options, function(glyph, gX, gY, gFontSize) { + var glyphPath = glyph.getPath(gX, gY, gFontSize, options, this); + glyphPaths.push(glyphPath); + }); + + return glyphPaths; + }; + + /** + * Returns the advance width of a text. + * + * This is something different than Path.getBoundingBox() as for example a + * suffixed whitespace increases the advanceWidth but not the bounding box + * or an overhanging letter like a calligraphic 'f' might have a quite larger + * bounding box than its advance width. + * + * This corresponds to canvas2dContext.measureText(text).width + * + * @param {string} text - The text to create. + * @param {number} [fontSize=72] - Font size in pixels. We scale the glyph units by `1 / unitsPerEm * fontSize`. + * @param {GlyphRenderOptions=} options + * @return advance width + */ + Font.prototype.getAdvanceWidth = function(text, fontSize, options) { + return this.forEachGlyph(text, 0, 0, fontSize, options, function() {}); + }; + + /** + * Draw the text on the given drawing context. + * @param {CanvasRenderingContext2D} ctx - A 2D drawing context, like Canvas. + * @param {string} text - The text to create. + * @param {number} [x=0] - Horizontal position of the beginning of the text. + * @param {number} [y=0] - Vertical position of the *baseline* of the text. + * @param {number} [fontSize=72] - Font size in pixels. We scale the glyph units by `1 / unitsPerEm * fontSize`. + * @param {GlyphRenderOptions=} options + */ + Font.prototype.draw = function(ctx, text, x, y, fontSize, options) { + this.getPath(text, x, y, fontSize, options).draw(ctx); + }; + + /** + * Draw the points of all glyphs in the text. + * On-curve points will be drawn in blue, off-curve points will be drawn in red. + * @param {CanvasRenderingContext2D} ctx - A 2D drawing context, like Canvas. + * @param {string} text - The text to create. + * @param {number} [x=0] - Horizontal position of the beginning of the text. + * @param {number} [y=0] - Vertical position of the *baseline* of the text. + * @param {number} [fontSize=72] - Font size in pixels. We scale the glyph units by `1 / unitsPerEm * fontSize`. + * @param {GlyphRenderOptions=} options + */ + Font.prototype.drawPoints = function(ctx, text, x, y, fontSize, options) { + this.forEachGlyph(text, x, y, fontSize, options, function(glyph, gX, gY, gFontSize) { + glyph.drawPoints(ctx, gX, gY, gFontSize); + }); + }; + + /** + * Draw lines indicating important font measurements for all glyphs in the text. + * Black lines indicate the origin of the coordinate system (point 0,0). + * Blue lines indicate the glyph bounding box. + * Green line indicates the advance width of the glyph. + * @param {CanvasRenderingContext2D} ctx - A 2D drawing context, like Canvas. + * @param {string} text - The text to create. + * @param {number} [x=0] - Horizontal position of the beginning of the text. + * @param {number} [y=0] - Vertical position of the *baseline* of the text. + * @param {number} [fontSize=72] - Font size in pixels. We scale the glyph units by `1 / unitsPerEm * fontSize`. + * @param {GlyphRenderOptions=} options + */ + Font.prototype.drawMetrics = function(ctx, text, x, y, fontSize, options) { + this.forEachGlyph(text, x, y, fontSize, options, function(glyph, gX, gY, gFontSize) { + glyph.drawMetrics(ctx, gX, gY, gFontSize); + }); + }; + + /** + * @param {string} + * @return {string} + */ + Font.prototype.getEnglishName = function(name) { + var translations = this.names[name]; + if (translations) { + return translations.en; + } + }; + + /** + * Validate + */ + Font.prototype.validate = function() { + var _this = this; + + function assert(predicate, message) { + } + + function assertNamePresent(name) { + var englishName = _this.getEnglishName(name); + assert(englishName && englishName.trim().length > 0); + } + + // Identification information + assertNamePresent('fontFamily'); + assertNamePresent('weightName'); + assertNamePresent('manufacturer'); + assertNamePresent('copyright'); + assertNamePresent('version'); + + // Dimension information + assert(this.unitsPerEm > 0); + }; + + /** + * Convert the font object to a SFNT data structure. + * This structure contains all the necessary tables and metadata to create a binary OTF file. + * @return {opentype.Table} + */ + Font.prototype.toTables = function() { + return sfnt.fontToTable(this); + }; + /** + * @deprecated Font.toBuffer is deprecated. Use Font.toArrayBuffer instead. + */ + Font.prototype.toBuffer = function() { + console.warn('Font.toBuffer is deprecated. Use Font.toArrayBuffer instead.'); + return this.toArrayBuffer(); + }; + /** + * Converts a `opentype.Font` into an `ArrayBuffer` + * @return {ArrayBuffer} + */ + Font.prototype.toArrayBuffer = function() { + var sfntTable = this.toTables(); + var bytes = sfntTable.encode(); + var buffer = new ArrayBuffer(bytes.length); + var intArray = new Uint8Array(buffer); + for (var i = 0; i < bytes.length; i++) { + intArray[i] = bytes[i]; + } + + return buffer; + }; + + /** + * Initiate a download of the OpenType font. + */ + Font.prototype.download = function(fileName) { + var familyName = this.getEnglishName('fontFamily'); + var styleName = this.getEnglishName('fontSubfamily'); + fileName = fileName || familyName.replace(/\s/g, '') + '-' + styleName + '.otf'; + var arrayBuffer = this.toArrayBuffer(); + + if (isBrowser()) { + window.URL = window.URL || window.webkitURL; + + if (window.URL) { + var dataView = new DataView(arrayBuffer); + var blob = new Blob([dataView], {type: 'font/opentype'}); + + var link = document.createElement('a'); + link.href = window.URL.createObjectURL(blob); + link.download = fileName; + + var event = document.createEvent('MouseEvents'); + event.initEvent('click', true, false); + link.dispatchEvent(event); + } else { + console.warn('Font file could not be downloaded. Try using a different browser.'); + } + } else { + var fs = require('fs'); + var buffer = arrayBufferToNodeBuffer(arrayBuffer); + fs.writeFileSync(fileName, buffer); + } + }; + /** + * @private + */ + Font.prototype.fsSelectionValues = { + ITALIC: 0x001, //1 + UNDERSCORE: 0x002, //2 + NEGATIVE: 0x004, //4 + OUTLINED: 0x008, //8 + STRIKEOUT: 0x010, //16 + BOLD: 0x020, //32 + REGULAR: 0x040, //64 + USER_TYPO_METRICS: 0x080, //128 + WWS: 0x100, //256 + OBLIQUE: 0x200 //512 + }; + + /** + * @private + */ + Font.prototype.usWidthClasses = { + ULTRA_CONDENSED: 1, + EXTRA_CONDENSED: 2, + CONDENSED: 3, + SEMI_CONDENSED: 4, + MEDIUM: 5, + SEMI_EXPANDED: 6, + EXPANDED: 7, + EXTRA_EXPANDED: 8, + ULTRA_EXPANDED: 9 + }; + + /** + * @private + */ + Font.prototype.usWeightClasses = { + THIN: 100, + EXTRA_LIGHT: 200, + LIGHT: 300, + NORMAL: 400, + MEDIUM: 500, + SEMI_BOLD: 600, + BOLD: 700, + EXTRA_BOLD: 800, + BLACK: 900 + }; + + // The `fvar` table stores font variation axes and instances. + + function addName(name, names) { + var nameString = JSON.stringify(name); + var nameID = 256; + for (var nameKey in names) { + var n = parseInt(nameKey); + if (!n || n < 256) { + continue; + } + + if (JSON.stringify(names[nameKey]) === nameString) { + return n; + } + + if (nameID <= n) { + nameID = n + 1; + } + } + + names[nameID] = name; + return nameID; + } + + function makeFvarAxis(n, axis, names) { + var nameID = addName(axis.name, names); + return [ + {name: 'tag_' + n, type: 'TAG', value: axis.tag}, + {name: 'minValue_' + n, type: 'FIXED', value: axis.minValue << 16}, + {name: 'defaultValue_' + n, type: 'FIXED', value: axis.defaultValue << 16}, + {name: 'maxValue_' + n, type: 'FIXED', value: axis.maxValue << 16}, + {name: 'flags_' + n, type: 'USHORT', value: 0}, + {name: 'nameID_' + n, type: 'USHORT', value: nameID} + ]; + } + + function parseFvarAxis(data, start, names) { + var axis = {}; + var p = new parse.Parser(data, start); + axis.tag = p.parseTag(); + axis.minValue = p.parseFixed(); + axis.defaultValue = p.parseFixed(); + axis.maxValue = p.parseFixed(); + p.skip('uShort', 1); // reserved for flags; no values defined + axis.name = names[p.parseUShort()] || {}; + return axis; + } + + function makeFvarInstance(n, inst, axes, names) { + var nameID = addName(inst.name, names); + var fields = [ + {name: 'nameID_' + n, type: 'USHORT', value: nameID}, + {name: 'flags_' + n, type: 'USHORT', value: 0} + ]; + + for (var i = 0; i < axes.length; ++i) { + var axisTag = axes[i].tag; + fields.push({ + name: 'axis_' + n + ' ' + axisTag, + type: 'FIXED', + value: inst.coordinates[axisTag] << 16 + }); + } + + return fields; + } + + function parseFvarInstance(data, start, axes, names) { + var inst = {}; + var p = new parse.Parser(data, start); + inst.name = names[p.parseUShort()] || {}; + p.skip('uShort', 1); // reserved for flags; no values defined + + inst.coordinates = {}; + for (var i = 0; i < axes.length; ++i) { + inst.coordinates[axes[i].tag] = p.parseFixed(); + } + + return inst; + } + + function makeFvarTable(fvar, names) { + var result = new table.Table('fvar', [ + {name: 'version', type: 'ULONG', value: 0x10000}, + {name: 'offsetToData', type: 'USHORT', value: 0}, + {name: 'countSizePairs', type: 'USHORT', value: 2}, + {name: 'axisCount', type: 'USHORT', value: fvar.axes.length}, + {name: 'axisSize', type: 'USHORT', value: 20}, + {name: 'instanceCount', type: 'USHORT', value: fvar.instances.length}, + {name: 'instanceSize', type: 'USHORT', value: 4 + fvar.axes.length * 4} + ]); + result.offsetToData = result.sizeOf(); + + for (var i = 0; i < fvar.axes.length; i++) { + result.fields = result.fields.concat(makeFvarAxis(i, fvar.axes[i], names)); + } + + for (var j = 0; j < fvar.instances.length; j++) { + result.fields = result.fields.concat(makeFvarInstance(j, fvar.instances[j], fvar.axes, names)); + } + + return result; + } + + function parseFvarTable(data, start, names) { + var p = new parse.Parser(data, start); + var tableVersion = p.parseULong(); + check.argument(tableVersion === 0x00010000, 'Unsupported fvar table version.'); + var offsetToData = p.parseOffset16(); + // Skip countSizePairs. + p.skip('uShort', 1); + var axisCount = p.parseUShort(); + var axisSize = p.parseUShort(); + var instanceCount = p.parseUShort(); + var instanceSize = p.parseUShort(); + + var axes = []; + for (var i = 0; i < axisCount; i++) { + axes.push(parseFvarAxis(data, start + offsetToData + i * axisSize, names)); + } + + var instances = []; + var instanceStart = start + offsetToData + axisCount * axisSize; + for (var j = 0; j < instanceCount; j++) { + instances.push(parseFvarInstance(data, instanceStart + j * instanceSize, axes, names)); + } + + return {axes: axes, instances: instances}; + } + + var fvar = { make: makeFvarTable, parse: parseFvarTable }; + + // The `GDEF` table contains various glyph properties + + var attachList = function() { + return { + coverage: this.parsePointer(Parser.coverage), + attachPoints: this.parseList(Parser.pointer(Parser.uShortList)) + }; + }; + + var caretValue = function() { + var format = this.parseUShort(); + check.argument(format === 1 || format === 2 || format === 3, + 'Unsupported CaretValue table version.'); + if (format === 1) { + return { coordinate: this.parseShort() }; + } else if (format === 2) { + return { pointindex: this.parseShort() }; + } else if (format === 3) { + // Device / Variation Index tables unsupported + return { coordinate: this.parseShort() }; + } + }; + + var ligGlyph = function() { + return this.parseList(Parser.pointer(caretValue)); + }; + + var ligCaretList = function() { + return { + coverage: this.parsePointer(Parser.coverage), + ligGlyphs: this.parseList(Parser.pointer(ligGlyph)) + }; + }; + + var markGlyphSets = function() { + this.parseUShort(); // Version + return this.parseList(Parser.pointer(Parser.coverage)); + }; + + function parseGDEFTable(data, start) { + start = start || 0; + var p = new Parser(data, start); + var tableVersion = p.parseVersion(1); + check.argument(tableVersion === 1 || tableVersion === 1.2 || tableVersion === 1.3, + 'Unsupported GDEF table version.'); + var gdef = { + version: tableVersion, + classDef: p.parsePointer(Parser.classDef), + attachList: p.parsePointer(attachList), + ligCaretList: p.parsePointer(ligCaretList), + markAttachClassDef: p.parsePointer(Parser.classDef) + }; + if (tableVersion >= 1.2) { + gdef.markGlyphSets = p.parsePointer(markGlyphSets); + } + return gdef; + } + var gdef = { parse: parseGDEFTable }; + + // The `GPOS` table contains kerning pairs, among other things. + + var subtableParsers$1 = new Array(10); // subtableParsers[0] is unused + + // https://docs.microsoft.com/en-us/typography/opentype/spec/gpos#lookup-type-1-single-adjustment-positioning-subtable + // this = Parser instance + subtableParsers$1[1] = function parseLookup1() { + var start = this.offset + this.relativeOffset; + var posformat = this.parseUShort(); + if (posformat === 1) { + return { + posFormat: 1, + coverage: this.parsePointer(Parser.coverage), + value: this.parseValueRecord() + }; + } else if (posformat === 2) { + return { + posFormat: 2, + coverage: this.parsePointer(Parser.coverage), + values: this.parseValueRecordList() + }; + } + check.assert(false, '0x' + start.toString(16) + ': GPOS lookup type 1 format must be 1 or 2.'); + }; + + // https://docs.microsoft.com/en-us/typography/opentype/spec/gpos#lookup-type-2-pair-adjustment-positioning-subtable + subtableParsers$1[2] = function parseLookup2() { + var start = this.offset + this.relativeOffset; + var posFormat = this.parseUShort(); + check.assert(posFormat === 1 || posFormat === 2, '0x' + start.toString(16) + ': GPOS lookup type 2 format must be 1 or 2.'); + var coverage = this.parsePointer(Parser.coverage); + var valueFormat1 = this.parseUShort(); + var valueFormat2 = this.parseUShort(); + if (posFormat === 1) { + // Adjustments for Glyph Pairs + return { + posFormat: posFormat, + coverage: coverage, + valueFormat1: valueFormat1, + valueFormat2: valueFormat2, + pairSets: this.parseList(Parser.pointer(Parser.list(function() { + return { // pairValueRecord + secondGlyph: this.parseUShort(), + value1: this.parseValueRecord(valueFormat1), + value2: this.parseValueRecord(valueFormat2) + }; + }))) + }; + } else if (posFormat === 2) { + var classDef1 = this.parsePointer(Parser.classDef); + var classDef2 = this.parsePointer(Parser.classDef); + var class1Count = this.parseUShort(); + var class2Count = this.parseUShort(); + return { + // Class Pair Adjustment + posFormat: posFormat, + coverage: coverage, + valueFormat1: valueFormat1, + valueFormat2: valueFormat2, + classDef1: classDef1, + classDef2: classDef2, + class1Count: class1Count, + class2Count: class2Count, + classRecords: this.parseList(class1Count, Parser.list(class2Count, function() { + return { + value1: this.parseValueRecord(valueFormat1), + value2: this.parseValueRecord(valueFormat2) + }; + })) + }; + } + }; + + subtableParsers$1[3] = function parseLookup3() { return { error: 'GPOS Lookup 3 not supported' }; }; + subtableParsers$1[4] = function parseLookup4() { return { error: 'GPOS Lookup 4 not supported' }; }; + subtableParsers$1[5] = function parseLookup5() { return { error: 'GPOS Lookup 5 not supported' }; }; + subtableParsers$1[6] = function parseLookup6() { return { error: 'GPOS Lookup 6 not supported' }; }; + subtableParsers$1[7] = function parseLookup7() { return { error: 'GPOS Lookup 7 not supported' }; }; + subtableParsers$1[8] = function parseLookup8() { return { error: 'GPOS Lookup 8 not supported' }; }; + subtableParsers$1[9] = function parseLookup9() { return { error: 'GPOS Lookup 9 not supported' }; }; + + // https://docs.microsoft.com/en-us/typography/opentype/spec/gpos + function parseGposTable(data, start) { + start = start || 0; + var p = new Parser(data, start); + var tableVersion = p.parseVersion(1); + check.argument(tableVersion === 1 || tableVersion === 1.1, 'Unsupported GPOS table version ' + tableVersion); + + if (tableVersion === 1) { + return { + version: tableVersion, + scripts: p.parseScriptList(), + features: p.parseFeatureList(), + lookups: p.parseLookupList(subtableParsers$1) + }; + } else { + return { + version: tableVersion, + scripts: p.parseScriptList(), + features: p.parseFeatureList(), + lookups: p.parseLookupList(subtableParsers$1), + variations: p.parseFeatureVariationsList() + }; + } + + } + + // GPOS Writing ////////////////////////////////////////////// + // NOT SUPPORTED + var subtableMakers$1 = new Array(10); + + function makeGposTable(gpos) { + return new table.Table('GPOS', [ + {name: 'version', type: 'ULONG', value: 0x10000}, + {name: 'scripts', type: 'TABLE', value: new table.ScriptList(gpos.scripts)}, + {name: 'features', type: 'TABLE', value: new table.FeatureList(gpos.features)}, + {name: 'lookups', type: 'TABLE', value: new table.LookupList(gpos.lookups, subtableMakers$1)} + ]); + } + + var gpos = { parse: parseGposTable, make: makeGposTable }; + + // The `kern` table contains kerning pairs. + + function parseWindowsKernTable(p) { + var pairs = {}; + // Skip nTables. + p.skip('uShort'); + var subtableVersion = p.parseUShort(); + check.argument(subtableVersion === 0, 'Unsupported kern sub-table version.'); + // Skip subtableLength, subtableCoverage + p.skip('uShort', 2); + var nPairs = p.parseUShort(); + // Skip searchRange, entrySelector, rangeShift. + p.skip('uShort', 3); + for (var i = 0; i < nPairs; i += 1) { + var leftIndex = p.parseUShort(); + var rightIndex = p.parseUShort(); + var value = p.parseShort(); + pairs[leftIndex + ',' + rightIndex] = value; + } + return pairs; + } + + function parseMacKernTable(p) { + var pairs = {}; + // The Mac kern table stores the version as a fixed (32 bits) but we only loaded the first 16 bits. + // Skip the rest. + p.skip('uShort'); + var nTables = p.parseULong(); + //check.argument(nTables === 1, 'Only 1 subtable is supported (got ' + nTables + ').'); + if (nTables > 1) { + console.warn('Only the first kern subtable is supported.'); + } + p.skip('uLong'); + var coverage = p.parseUShort(); + var subtableVersion = coverage & 0xFF; + p.skip('uShort'); + if (subtableVersion === 0) { + var nPairs = p.parseUShort(); + // Skip searchRange, entrySelector, rangeShift. + p.skip('uShort', 3); + for (var i = 0; i < nPairs; i += 1) { + var leftIndex = p.parseUShort(); + var rightIndex = p.parseUShort(); + var value = p.parseShort(); + pairs[leftIndex + ',' + rightIndex] = value; + } + } + return pairs; + } + + // Parse the `kern` table which contains kerning pairs. + function parseKernTable(data, start) { + var p = new parse.Parser(data, start); + var tableVersion = p.parseUShort(); + if (tableVersion === 0) { + return parseWindowsKernTable(p); + } else if (tableVersion === 1) { + return parseMacKernTable(p); + } else { + throw new Error('Unsupported kern table version (' + tableVersion + ').'); + } + } + + var kern = { parse: parseKernTable }; + + // The `loca` table stores the offsets to the locations of the glyphs in the font. + + // Parse the `loca` table. This table stores the offsets to the locations of the glyphs in the font, + // relative to the beginning of the glyphData table. + // The number of glyphs stored in the `loca` table is specified in the `maxp` table (under numGlyphs) + // The loca table has two versions: a short version where offsets are stored as uShorts, and a long + // version where offsets are stored as uLongs. The `head` table specifies which version to use + // (under indexToLocFormat). + function parseLocaTable(data, start, numGlyphs, shortVersion) { + var p = new parse.Parser(data, start); + var parseFn = shortVersion ? p.parseUShort : p.parseULong; + // There is an extra entry after the last index element to compute the length of the last glyph. + // That's why we use numGlyphs + 1. + var glyphOffsets = []; + for (var i = 0; i < numGlyphs + 1; i += 1) { + var glyphOffset = parseFn.call(p); + if (shortVersion) { + // The short table version stores the actual offset divided by 2. + glyphOffset *= 2; + } + + glyphOffsets.push(glyphOffset); + } + + return glyphOffsets; + } + + var loca = { parse: parseLocaTable }; + + // opentype.js + + /** + * The opentype library. + * @namespace opentype + */ + + // File loaders ///////////////////////////////////////////////////////// + /** + * Loads a font from a file. The callback throws an error message as the first parameter if it fails + * and the font as an ArrayBuffer in the second parameter if it succeeds. + * @param {string} path - The path of the file + * @param {Function} callback - The function to call when the font load completes + */ + function loadFromFile(path, callback) { + var fs = require('fs'); + fs.readFile(path, function(err, buffer) { + if (err) { + return callback(err.message); + } + + callback(null, nodeBufferToArrayBuffer(buffer)); + }); + } + /** + * Loads a font from a URL. The callback throws an error message as the first parameter if it fails + * and the font as an ArrayBuffer in the second parameter if it succeeds. + * @param {string} url - The URL of the font file. + * @param {Function} callback - The function to call when the font load completes + */ + function loadFromUrl(url, callback) { + var request = new XMLHttpRequest(); + request.open('get', url, true); + request.responseType = 'arraybuffer'; + request.onload = function() { + if (request.response) { + return callback(null, request.response); + } else { + return callback('Font could not be loaded: ' + request.statusText); + } + }; + + request.onerror = function () { + callback('Font could not be loaded'); + }; + + request.send(); + } + + // Table Directory Entries ////////////////////////////////////////////// + /** + * Parses OpenType table entries. + * @param {DataView} + * @param {Number} + * @return {Object[]} + */ + function parseOpenTypeTableEntries(data, numTables) { + var tableEntries = []; + var p = 12; + for (var i = 0; i < numTables; i += 1) { + var tag = parse.getTag(data, p); + var checksum = parse.getULong(data, p + 4); + var offset = parse.getULong(data, p + 8); + var length = parse.getULong(data, p + 12); + tableEntries.push({tag: tag, checksum: checksum, offset: offset, length: length, compression: false}); + p += 16; + } + + return tableEntries; + } + + /** + * Parses WOFF table entries. + * @param {DataView} + * @param {Number} + * @return {Object[]} + */ + function parseWOFFTableEntries(data, numTables) { + var tableEntries = []; + var p = 44; // offset to the first table directory entry. + for (var i = 0; i < numTables; i += 1) { + var tag = parse.getTag(data, p); + var offset = parse.getULong(data, p + 4); + var compLength = parse.getULong(data, p + 8); + var origLength = parse.getULong(data, p + 12); + var compression = (void 0); + if (compLength < origLength) { + compression = 'WOFF'; + } else { + compression = false; + } + + tableEntries.push({tag: tag, offset: offset, compression: compression, + compressedLength: compLength, length: origLength}); + p += 20; + } + + return tableEntries; + } + + /** + * @typedef TableData + * @type Object + * @property {DataView} data - The DataView + * @property {number} offset - The data offset. + */ + + /** + * @param {DataView} + * @param {Object} + * @return {TableData} + */ + function uncompressTable(data, tableEntry) { + if (tableEntry.compression === 'WOFF') { + var inBuffer = new Uint8Array(data.buffer, tableEntry.offset + 2, tableEntry.compressedLength - 2); + var outBuffer = new Uint8Array(tableEntry.length); + tinyInflate(inBuffer, outBuffer); + if (outBuffer.byteLength !== tableEntry.length) { + throw new Error('Decompression error: ' + tableEntry.tag + ' decompressed length doesn\'t match recorded length'); + } + + var view = new DataView(outBuffer.buffer, 0); + return {data: view, offset: 0}; + } else { + return {data: data, offset: tableEntry.offset}; + } + } + + // Public API /////////////////////////////////////////////////////////// + + /** + * Parse the OpenType file data (as an ArrayBuffer) and return a Font object. + * Throws an error if the font could not be parsed. + * @param {ArrayBuffer} + * @param {Object} opt - options for parsing + * @return {opentype.Font} + */ + function parseBuffer(buffer, opt) { + opt = (opt === undefined || opt === null) ? {} : opt; + + var indexToLocFormat; + var ltagTable; + + // Since the constructor can also be called to create new fonts from scratch, we indicate this + // should be an empty font that we'll fill with our own data. + var font = new Font({empty: true}); + + // OpenType fonts use big endian byte ordering. + // We can't rely on typed array view types, because they operate with the endianness of the host computer. + // Instead we use DataViews where we can specify endianness. + var data = new DataView(buffer, 0); + var numTables; + var tableEntries = []; + var signature = parse.getTag(data, 0); + if (signature === String.fromCharCode(0, 1, 0, 0) || signature === 'true' || signature === 'typ1') { + font.outlinesFormat = 'truetype'; + numTables = parse.getUShort(data, 4); + tableEntries = parseOpenTypeTableEntries(data, numTables); + } else if (signature === 'OTTO') { + font.outlinesFormat = 'cff'; + numTables = parse.getUShort(data, 4); + tableEntries = parseOpenTypeTableEntries(data, numTables); + } else if (signature === 'wOFF') { + var flavor = parse.getTag(data, 4); + if (flavor === String.fromCharCode(0, 1, 0, 0)) { + font.outlinesFormat = 'truetype'; + } else if (flavor === 'OTTO') { + font.outlinesFormat = 'cff'; + } else { + throw new Error('Unsupported OpenType flavor ' + signature); + } + + numTables = parse.getUShort(data, 12); + tableEntries = parseWOFFTableEntries(data, numTables); + } else { + throw new Error('Unsupported OpenType signature ' + signature); + } + + var cffTableEntry; + var fvarTableEntry; + var glyfTableEntry; + var gdefTableEntry; + var gposTableEntry; + var gsubTableEntry; + var hmtxTableEntry; + var kernTableEntry; + var locaTableEntry; + var nameTableEntry; + var metaTableEntry; + var p; + + for (var i = 0; i < numTables; i += 1) { + var tableEntry = tableEntries[i]; + var table = (void 0); + switch (tableEntry.tag) { + case 'cmap': + table = uncompressTable(data, tableEntry); + font.tables.cmap = cmap.parse(table.data, table.offset); + font.encoding = new CmapEncoding(font.tables.cmap); + break; + case 'cvt ' : + table = uncompressTable(data, tableEntry); + p = new parse.Parser(table.data, table.offset); + font.tables.cvt = p.parseShortList(tableEntry.length / 2); + break; + case 'fvar': + fvarTableEntry = tableEntry; + break; + case 'fpgm' : + table = uncompressTable(data, tableEntry); + p = new parse.Parser(table.data, table.offset); + font.tables.fpgm = p.parseByteList(tableEntry.length); + break; + case 'head': + table = uncompressTable(data, tableEntry); + font.tables.head = head.parse(table.data, table.offset); + font.unitsPerEm = font.tables.head.unitsPerEm; + indexToLocFormat = font.tables.head.indexToLocFormat; + break; + case 'hhea': + table = uncompressTable(data, tableEntry); + font.tables.hhea = hhea.parse(table.data, table.offset); + font.ascender = font.tables.hhea.ascender; + font.descender = font.tables.hhea.descender; + font.numberOfHMetrics = font.tables.hhea.numberOfHMetrics; + break; + case 'hmtx': + hmtxTableEntry = tableEntry; + break; + case 'ltag': + table = uncompressTable(data, tableEntry); + ltagTable = ltag.parse(table.data, table.offset); + break; + case 'maxp': + table = uncompressTable(data, tableEntry); + font.tables.maxp = maxp.parse(table.data, table.offset); + font.numGlyphs = font.tables.maxp.numGlyphs; + break; + case 'name': + nameTableEntry = tableEntry; + break; + case 'OS/2': + table = uncompressTable(data, tableEntry); + font.tables.os2 = os2.parse(table.data, table.offset); + break; + case 'post': + table = uncompressTable(data, tableEntry); + font.tables.post = post.parse(table.data, table.offset); + font.glyphNames = new GlyphNames(font.tables.post); + break; + case 'prep' : + table = uncompressTable(data, tableEntry); + p = new parse.Parser(table.data, table.offset); + font.tables.prep = p.parseByteList(tableEntry.length); + break; + case 'glyf': + glyfTableEntry = tableEntry; + break; + case 'loca': + locaTableEntry = tableEntry; + break; + case 'CFF ': + cffTableEntry = tableEntry; + break; + case 'kern': + kernTableEntry = tableEntry; + break; + case 'GDEF': + gdefTableEntry = tableEntry; + break; + case 'GPOS': + gposTableEntry = tableEntry; + break; + case 'GSUB': + gsubTableEntry = tableEntry; + break; + case 'meta': + metaTableEntry = tableEntry; + break; + } + } + + var nameTable = uncompressTable(data, nameTableEntry); + font.tables.name = _name.parse(nameTable.data, nameTable.offset, ltagTable); + font.names = font.tables.name; + + if (glyfTableEntry && locaTableEntry) { + var shortVersion = indexToLocFormat === 0; + var locaTable = uncompressTable(data, locaTableEntry); + var locaOffsets = loca.parse(locaTable.data, locaTable.offset, font.numGlyphs, shortVersion); + var glyfTable = uncompressTable(data, glyfTableEntry); + font.glyphs = glyf.parse(glyfTable.data, glyfTable.offset, locaOffsets, font, opt); + } else if (cffTableEntry) { + var cffTable = uncompressTable(data, cffTableEntry); + cff.parse(cffTable.data, cffTable.offset, font, opt); + } else { + throw new Error('Font doesn\'t contain TrueType or CFF outlines.'); + } + + var hmtxTable = uncompressTable(data, hmtxTableEntry); + hmtx.parse(font, hmtxTable.data, hmtxTable.offset, font.numberOfHMetrics, font.numGlyphs, font.glyphs, opt); + addGlyphNames(font, opt); + + if (kernTableEntry) { + var kernTable = uncompressTable(data, kernTableEntry); + font.kerningPairs = kern.parse(kernTable.data, kernTable.offset); + } else { + font.kerningPairs = {}; + } + + if (gdefTableEntry) { + var gdefTable = uncompressTable(data, gdefTableEntry); + font.tables.gdef = gdef.parse(gdefTable.data, gdefTable.offset); + } + + if (gposTableEntry) { + var gposTable = uncompressTable(data, gposTableEntry); + font.tables.gpos = gpos.parse(gposTable.data, gposTable.offset); + font.position.init(); + } + + if (gsubTableEntry) { + var gsubTable = uncompressTable(data, gsubTableEntry); + font.tables.gsub = gsub.parse(gsubTable.data, gsubTable.offset); + } + + if (fvarTableEntry) { + var fvarTable = uncompressTable(data, fvarTableEntry); + font.tables.fvar = fvar.parse(fvarTable.data, fvarTable.offset, font.names); + } + + if (metaTableEntry) { + var metaTable = uncompressTable(data, metaTableEntry); + font.tables.meta = meta.parse(metaTable.data, metaTable.offset); + font.metas = font.tables.meta; + } + + return font; + } + + /** + * Asynchronously load the font from a URL or a filesystem. When done, call the callback + * with two arguments `(err, font)`. The `err` will be null on success, + * the `font` is a Font object. + * We use the node.js callback convention so that + * opentype.js can integrate with frameworks like async.js. + * @alias opentype.load + * @param {string} url - The URL of the font to load. + * @param {Function} callback - The callback. + */ + function load(url, callback, opt) { + opt = (opt === undefined || opt === null) ? {} : opt; + var isNode = typeof window === 'undefined'; + var loadFn = isNode && !opt.isUrl ? loadFromFile : loadFromUrl; + + return new Promise(function (resolve, reject) { + loadFn(url, function(err, arrayBuffer) { + if (err) { + if (callback) { + return callback(err); + } else { + reject(err); + } + } + var font; + try { + font = parseBuffer(arrayBuffer, opt); + } catch (e) { + if (callback) { + return callback(e, null); + } else { + reject(e); + } + } + if (callback) { + return callback(null, font); + } else { + resolve(font); + } + }); + }); + } + + /** + * Synchronously load the font from a URL or file. + * When done, returns the font object or throws an error. + * @alias opentype.loadSync + * @param {string} url - The URL of the font to load. + * @param {Object} opt - opt.lowMemory + * @return {opentype.Font} + */ + function loadSync(url, opt) { + var fs = require('fs'); + var buffer = fs.readFileSync(url); + return parseBuffer(nodeBufferToArrayBuffer(buffer), opt); + } + + var opentype = /*#__PURE__*/Object.freeze({ + __proto__: null, + Font: Font, + Glyph: Glyph, + Path: Path, + BoundingBox: BoundingBox, + _parse: parse, + parse: parseBuffer, + load: load, + loadSync: loadSync + }); + + exports.BoundingBox = BoundingBox; + exports.Font = Font; + exports.Glyph = Glyph; + exports.Path = Path; + exports._parse = parse; + exports.default = opentype; + exports.load = load; + exports.loadSync = loadSync; + exports.parse = parseBuffer; + + Object.defineProperty(exports, '__esModule', { value: true }); + +}))); +//# sourceMappingURL=opentype.js.map diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..4c7b4cf --- /dev/null +++ b/package-lock.json @@ -0,0 +1,562 @@ +{ + "name": "font2svg", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "font2svg", + "version": "1.0.0", + "dependencies": { + "sharp": "^0.34.5" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz", + "integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@img/colour": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz", + "integrity": "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + } + } +} diff --git a/package.json b/package.json index 05ea520..596b727 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,13 @@ "dev": "cd frontend && pnpm run dev", "build": "cd frontend && pnpm run build", "preview": "cd frontend && pnpm run preview", - "prepare-fonts": "python3 scripts/generate-font-list.py" + "api:serve": "if [ -x .venv/bin/python ]; then .venv/bin/python apiserver/server.py; else python3 apiserver/server.py; fi", + "prepare-fonts": "python3 scripts/generate-font-list.py", + "mp:syntax": "find miniprogram -name '*.js' -print0 | xargs -0 -n1 node --check", + "mp:lint": "node scripts/check-miniprogram-lint.js", + "mp:test": "node scripts/check-miniprogram-core-test.js" + }, + "dependencies": { + "sharp": "^0.34.5" } } diff --git a/scripts/check-miniprogram-core-test.js b/scripts/check-miniprogram-core-test.js new file mode 100755 index 0000000..65e6200 --- /dev/null +++ b/scripts/check-miniprogram-core-test.js @@ -0,0 +1,60 @@ +#!/usr/bin/env node + +const fs = require('fs') +const path = require('path') + +const { wrapTextByChars } = require('../miniprogram/utils/core/text-layout') +const { generateSvgFromFont } = require('../miniprogram/utils/core/svg-builder') +const opentype = require('../frontend/node_modules/opentype.js/dist/opentype.js') + +function findFirstFontFile(rootDir) { + const entries = fs.readdirSync(rootDir, { withFileTypes: true }) + for (const entry of entries) { + const fullPath = path.join(rootDir, entry.name) + if (entry.isDirectory()) { + const nested = findFirstFontFile(fullPath) + if (nested) return nested + continue + } + + if (/\.(ttf|otf)$/i.test(entry.name)) { + return fullPath + } + } + return null +} + +function assert(condition, message) { + if (!condition) { + throw new Error(message) + } +} + +function run() { + console.log('开始执行小程序核心模块测试...') + + const wrapped = wrapTextByChars('123456', 2) + assert(wrapped === '12\n34\n56', 'wrapTextByChars 结果不符合预期') + + const fontFile = findFirstFontFile(path.join(__dirname, '..', 'frontend', 'public', 'fonts')) + assert(fontFile, '未找到可用字体文件') + + const buffer = fs.readFileSync(fontFile) + const font = opentype.parse(buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength)) + + const result = generateSvgFromFont({ + text: '星程字体转换', + font, + fontSize: 120, + fillColor: '#000000', + letterSpacing: 0, + }) + + assert(typeof result.svg === 'string' && result.svg.includes(' 0 && result.height > 0, 'SVG 尺寸计算无效') + + console.log('小程序核心模块测试通过。') +} + +run() diff --git a/scripts/check-miniprogram-lint.js b/scripts/check-miniprogram-lint.js new file mode 100755 index 0000000..8d9731d --- /dev/null +++ b/scripts/check-miniprogram-lint.js @@ -0,0 +1,65 @@ +#!/usr/bin/env node + +const fs = require('fs') +const path = require('path') + +const ROOT = path.join(__dirname, '..', 'miniprogram') +const TARGET_EXTENSIONS = new Set(['.js', '.json', '.wxml', '.wxss']) + +function walk(dir, files = []) { + const entries = fs.readdirSync(dir, { withFileTypes: true }) + for (const entry of entries) { + const fullPath = path.join(dir, entry.name) + if (fullPath.includes(`${path.sep}workers${path.sep}svg-generator${path.sep}vendor${path.sep}`)) { + continue + } + if (fullPath.includes(`${path.sep}i18n${path.sep}`)) { + continue + } + if (fullPath.endsWith('.miniapp.json')) { + continue + } + if (entry.isDirectory()) { + walk(fullPath, files) + continue + } + const ext = path.extname(entry.name) + if (TARGET_EXTENSIONS.has(ext)) { + files.push(fullPath) + } + } + return files +} + +function main() { + console.log('开始执行小程序代码规范检查...') + + const files = walk(ROOT) + const violations = [] + + for (const file of files) { + const content = fs.readFileSync(file, 'utf8') + const lines = content.split(/\r?\n/) + for (let i = 0; i < lines.length; i += 1) { + const line = lines[i] + if (/\t/.test(line)) { + violations.push(`${file}:${i + 1} 使用了 Tab 缩进`) + } + if (/\s+$/.test(line)) { + violations.push(`${file}:${i + 1} 存在行尾空格`) + } + } + } + + if (violations.length > 0) { + console.error('发现代码规范问题:') + for (const violation of violations) { + console.error(`- ${violation}`) + } + process.exit(1) + } + + console.log('小程序代码规范检查通过。') +} + +main() diff --git a/scripts/deploy-assets.sh b/scripts/deploy-assets.sh new file mode 100755 index 0000000..a14787d --- /dev/null +++ b/scripts/deploy-assets.sh @@ -0,0 +1,112 @@ +#!/bin/bash + +# Font2SVG 静态资源部署脚本 +# 用于将图标和 logo 上传到 fonts.biboer.cn 服务器 + +set -e # 遇到错误立即退出 + +# ===== 配置区域 ===== +SERVER="gavin@fonts.biboer.cn" +REMOTE_DIR="/home/gavin/font2svg" +LOCAL_ASSETS_DIR="assets" + +# 颜色输出 +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' + +# ===== 函数定义 ===== +log_info() { + echo -e "${GREEN}[INFO]${NC} $1" +} + +log_warn() { + echo -e "${YELLOW}[WARN]${NC} $1" +} + +log_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +check_files() { + log_info "检查本地文件..." + + if [ ! -d "$LOCAL_ASSETS_DIR/icons" ]; then + log_error "图标目录不存在: $LOCAL_ASSETS_DIR/icons" + exit 1 + fi + + SVG_COUNT=$(find "$LOCAL_ASSETS_DIR/icons" -name "*.svg" | wc -l | tr -d ' ') + PNG_COUNT=$(find "$LOCAL_ASSETS_DIR/icons" -name "*.png" | wc -l | tr -d ' ') + log_info "发现 $SVG_COUNT 个 SVG 图标, $PNG_COUNT 个 PNG 图标" +} + +create_remote_dirs() { + log_info "创建远程目录..." + ssh $SERVER "mkdir -p $REMOTE_DIR/assets/icons" +} + +upload_assets() { + log_info "上传静态资源到 $SERVER..." + + # 上传图标(SVG 和 PNG) + rsync -avz --progress \ + --include="*.svg" \ + --include="*.png" \ + --include="*/" \ + --exclude="*" \ + "$LOCAL_ASSETS_DIR/" "$SERVER:$REMOTE_DIR/assets/" + + log_info "静态资源上传完成" +} + +set_permissions() { + log_info "设置文件权限..." + ssh $SERVER "chmod -R 755 $REMOTE_DIR/assets" + log_info "权限设置完成" +} + +verify_deployment() { + log_info "验证部署..." + + # 检查目录是否存在 + if ssh $SERVER "[ -d $REMOTE_DIR/assets/icons ]"; then + log_info "✓ assets/icons 目录存在" + else + log_error "✗ assets/icons 目录不存在" + exit 1 + fi + + # 统计远程文件数量 + REMOTE_SVG_COUNT=$(ssh $SERVER "find $REMOTE_DIR/assets/icons -name '*.svg' | wc -l" | tr -d ' ') + REMOTE_PNG_COUNT=$(ssh $SERVER "find $REMOTE_DIR/assets/icons -name '*.png' | wc -l" | tr -d ' ') + log_info "远程服务器上有 $REMOTE_SVG_COUNT 个 SVG, $REMOTE_PNG_COUNT 个 PNG" +} + +show_urls() { + log_info "==========================================" + log_info "部署完成!资源 URL 示例:" + log_info " Logo: https://fonts.biboer.cn/assets/webicon.png" + log_info " 图标: https://fonts.biboer.cn/assets/icons/[图标名].svg" + log_info "==========================================" +} + +# ===== 主流程 ===== +main() { + log_info "开始部署 Font2SVG 静态资源..." + echo "" + + check_files + create_remote_dirs + upload_assets + set_permissions + verify_deployment + + echo "" + show_urls + log_info "部署完成!" +} + +# 运行主流程 +main diff --git a/scripts/deploy-fonts.sh b/scripts/deploy-fonts.sh new file mode 100755 index 0000000..fef35a7 --- /dev/null +++ b/scripts/deploy-fonts.sh @@ -0,0 +1,170 @@ +#!/bin/bash + +# Font2SVG 字体资源部署脚本 +# 用于将字体文件上传到 fonts.biboer.cn 服务器 + +set -e # 遇到错误立即退出 + +# ===== 配置区域 ===== +SERVER="user@fonts.biboer.cn" # 请替换为你的 SSH 用户名 +REMOTE_DIR="/home/gavin/font2svg" +LOCAL_FONTS_DIR="frontend/public/fonts" +LOCAL_FONTS_JSON="frontend/public/fonts.json" + +# 颜色输出 +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# ===== 函数定义 ===== +log_info() { + echo -e "${GREEN}[INFO]${NC} $1" +} + +log_warn() { + echo -e "${YELLOW}[WARN]${NC} $1" +} + +log_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +check_files() { + log_info "检查本地文件..." + + if [ ! -d "$LOCAL_FONTS_DIR" ]; then + log_error "字体目录不存在: $LOCAL_FONTS_DIR" + exit 1 + fi + + if [ ! -f "$LOCAL_FONTS_JSON" ]; then + log_error "fonts.json 文件不存在: $LOCAL_FONTS_JSON" + exit 1 + fi + + FONT_COUNT=$(find "$LOCAL_FONTS_DIR" -name "*.ttf" -o -name "*.otf" | wc -l) + log_info "发现 $FONT_COUNT 个字体文件" +} + +create_remote_dirs() { + log_info "创建远程目录..." + ssh $SERVER "mkdir -p $REMOTE_DIR/fonts" +} + +upload_fonts() { + log_info "上传字体文件到 $SERVER..." + log_warn "这可能需要几分钟,取决于字体文件大小..." + + # 使用 rsync 进行增量上传(只上传修改过的文件) + rsync -avz --progress \ + --exclude=".DS_Store" \ + --exclude="Thumbs.db" \ + "$LOCAL_FONTS_DIR/" "$SERVER:$REMOTE_DIR/fonts/" + + log_info "字体文件上传完成" +} + +upload_fonts_json() { + log_info "上传 fonts.json..." + scp "$LOCAL_FONTS_JSON" "$SERVER:$REMOTE_DIR/" + log_info "fonts.json 上传完成" +} + +set_permissions() { + log_info "设置文件权限..." + ssh $SERVER "chmod -R 755 $REMOTE_DIR" + log_info "权限设置完成" +} + +verify_deployment() { + log_info "验证部署结果..." + + # 检查 fonts.json + HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" "https://fonts.biboer.cn/fonts.json") + + if [ "$HTTP_CODE" = "200" ]; then + log_info "fonts.json 可访问 ✓" + else + log_error "fonts.json 访问失败 (HTTP $HTTP_CODE)" + log_warn "请检查 Cloudflare DNS 配置和 Nginx 配置" + exit 1 + fi + + # 检查 CORS 头 + CORS_HEADER=$(curl -s -I "https://fonts.biboer.cn/fonts.json" | grep -i "access-control-allow-origin") + + if [ -n "$CORS_HEADER" ]; then + log_info "CORS 配置正确 ✓" + else + log_warn "未检测到 CORS 头,请检查 Nginx 配置" + fi +} + +restart_nginx() { + log_info "重启 Nginx..." + ssh $SERVER "sudo systemctl restart nginx" || { + log_warn "Nginx 重启失败,可能需要手动执行" + return 1 + } + log_info "Nginx 重启成功" +} + +show_summary() { + echo "" + echo "=======================================" + log_info "部署完成!" + echo "=======================================" + echo "" + echo "下一步操作:" + echo "1. 在小程序后台配置域名:" + echo " https://mp.weixin.qq.com/ → 开发 → 服务器域名" + echo " 添加 downloadFile 合法域名: https://fonts.biboer.cn" + echo "" + echo "2. 测试字体加载(在小程序开发者工具控制台):" + echo " wx.request({" + echo " url: 'https://fonts.biboer.cn/fonts.json'," + echo " success: (res) => console.log(res.data)" + echo " })" + echo "" + echo "3. 验证 CDN 缓存状态:" + echo " curl -I https://fonts.biboer.cn/fonts.json | grep cf-cache-status" + echo "" +} + +# ===== 主流程 ===== +main() { + echo "" + echo "=========================================" + echo " Font2SVG 字体资源部署脚本" + echo " 目标服务器: fonts.biboer.cn" + echo "=========================================" + echo "" + + # 检查是否在项目根目录 + if [ ! -f "package.json" ]; then + log_error "请在项目根目录执行此脚本" + exit 1 + fi + + # 执行部署步骤 + check_files + create_remote_dirs + upload_fonts + upload_fonts_json + set_permissions + + # 可选:重启 Nginx(需要 sudo 权限) + read -p "是否重启 Nginx?(y/N) " -n 1 -r + echo + if [[ $REPLY =~ ^[Yy]$ ]]; then + restart_nginx + fi + + # 验证部署 + verify_deployment + show_summary +} + +# 执行主函数 +main "$@" diff --git a/scripts/deploy-icons.sh b/scripts/deploy-icons.sh new file mode 100644 index 0000000..75ce65e --- /dev/null +++ b/scripts/deploy-icons.sh @@ -0,0 +1,56 @@ +#!/bin/bash + +# 部署图标到 CDN (fonts.biboer.cn) +# 使用方法: ./deploy-icons.sh + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ICONS_DIR="$SCRIPT_DIR/frontend/src/assets/icons" +REMOTE_USER="root" +REMOTE_HOST="fonts.biboer.cn" +REMOTE_PATH="/var/www/fonts" +ICONS_REMOTE_PATH="$REMOTE_PATH/icons" + +echo "🚀 开始部署图标到 CDN..." +echo "源目录: $ICONS_DIR" +echo "目标: $REMOTE_HOST:$ICONS_REMOTE_PATH" +echo "" + +# 检查源目录 +if [ ! -d "$ICONS_DIR" ]; then + echo "❌ 错误: 图标目录不存在 $ICONS_DIR" + exit 1 +fi + +# 统计文件数量 +ICON_COUNT=$(find "$ICONS_DIR" -type f \( -name "*.svg" -o -name "*.png" \) | wc -l) +echo "📦 找到 $ICON_COUNT 个图标文件" +echo "" + +# 创建远程目录 +echo "📁 创建远程目录..." +ssh "$REMOTE_USER@$REMOTE_HOST" "mkdir -p $ICONS_REMOTE_PATH" + +# 上传图标文件(SVG 和 PNG) +echo "📤 上传图标文件..." +rsync -avz --progress \ + --include="*.svg" \ + --include="*.png" \ + --exclude="*" \ + "$ICONS_DIR/" \ + "$REMOTE_USER@$REMOTE_HOST:$ICONS_REMOTE_PATH/" + +# 设置权限 +echo "🔐 设置文件权限..." +ssh "$REMOTE_USER@$REMOTE_HOST" "chmod -R 644 $ICONS_REMOTE_PATH/*" + +echo "" +echo "✅ 部署完成!" +echo "" +echo "📝 图标访问地址示例:" +echo " https://fonts.biboer.cn/icons/webicon.png" +echo " https://fonts.biboer.cn/icons/font-icon.svg" +echo "" +echo "🔗 测试访问:" +curl -I "https://fonts.biboer.cn/icons/webicon.png" 2>/dev/null | head -n 1 || echo "⚠️ 无法访问,请检查服务器配置" diff --git a/scripts/generate-font-list.py b/scripts/generate-font-list.py index 20689fd..2ca1997 100644 --- a/scripts/generate-font-list.py +++ b/scripts/generate-font-list.py @@ -1,7 +1,10 @@ #!/usr/bin/env python3 """ -生成字体清单 JSON 文件 -扫描 frontend/public/fonts/ 目录下的所有字体文件,生成 frontend/public/fonts.json +生成字体清单文件 +扫描 frontend/public/fonts/ 目录下的所有字体文件,同时生成: +1. frontend/public/fonts.json +2. miniprogram/assets/fonts.json +3. miniprogram/assets/fonts.js """ import os @@ -44,6 +47,23 @@ def scan_fonts(font_dir='frontend/public/fonts'): return fonts +def write_fonts_json(fonts, output_file): + """写入字体清单 JSON 文件""" + os.makedirs(os.path.dirname(output_file), exist_ok=True) + with open(output_file, 'w', encoding='utf-8') as f: + json.dump(fonts, f, ensure_ascii=False, indent=2) + print(f"字体清单已保存到: {output_file}") + + +def write_fonts_js(fonts, output_file): + """写入小程序可 require 的 JS 清单文件""" + os.makedirs(os.path.dirname(output_file), exist_ok=True) + content = "module.exports = " + json.dumps(fonts, ensure_ascii=False, indent=2) + "\n" + with open(output_file, 'w', encoding='utf-8') as f: + f.write(content) + print(f"字体清单已保存到: {output_file}") + + def main(): """主函数""" # 扫描字体(唯一来源:frontend/public/fonts) @@ -51,14 +71,10 @@ def main(): print(f"找到 {len(fonts)} 个字体文件") - # 保存到 JSON 文件 - output_file = 'frontend/public/fonts.json' - os.makedirs(os.path.dirname(output_file), exist_ok=True) - - with open(output_file, 'w', encoding='utf-8') as f: - json.dump(fonts, f, ensure_ascii=False, indent=2) - - print(f"字体清单已保存到: {output_file}") + # 同步写入 Web 与小程序清单 + write_fonts_json(fonts, 'frontend/public/fonts.json') + write_fonts_json(fonts, 'miniprogram/assets/fonts.json') + write_fonts_js(fonts, 'miniprogram/assets/fonts.js') # 统计信息 categories = {} diff --git a/scripts/generate-fonts-json.py b/scripts/generate-fonts-json.py new file mode 100755 index 0000000..854eb04 --- /dev/null +++ b/scripts/generate-fonts-json.py @@ -0,0 +1,160 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +Font2SVG - fonts.json 生成脚本 +扫描 frontend/public/fonts/ 目录,生成小程序所需的 fonts.json +URL 格式:https://fonts.biboer.cn/fonts/{category}/{fontname}.ttf +""" + +import os +import json +from pathlib import Path +from urllib.parse import quote + +# 配置 +BASE_URL = "https://fonts.biboer.cn/fonts" +FONTS_DIR = Path(__file__).parent.parent / "frontend" / "public" / "fonts" +OUTPUT_FILE = Path(__file__).parent.parent / "frontend" / "public" / "fonts.json" + +def scan_fonts(fonts_dir: Path) -> list: + """ + 扫描字体目录,生成字体列表 + + Args: + fonts_dir: 字体根目录 + + Returns: + 字体信息列表 + """ + fonts = [] + + # 遍历所有分类目录 + for category_dir in fonts_dir.iterdir(): + if not category_dir.is_dir(): + continue + + category = category_dir.name + + # 跳过隐藏目录 + if category.startswith('.'): + continue + + # 遍历分类下的所有字体文件 + for font_file in category_dir.glob("*.ttf"): + # 获取文件信息 + font_name = font_file.stem # 不含扩展名的文件名 + file_size = font_file.stat().st_size + + # 构建 URL(需要编码中文路径) + encoded_category = quote(category) + encoded_filename = quote(font_file.name) + url = f"{BASE_URL}/{encoded_category}/{encoded_filename}" + + # 创建字体信息对象 + font_info = { + "id": f"{category}/{font_name}", + "name": font_name, + "category": category, + "path": url, + "size": file_size + } + + fonts.append(font_info) + print(f"✓ {category}/{font_name} ({file_size} bytes)") + + return fonts + +def sort_fonts(fonts: list) -> list: + """ + 对字体列表排序 + 1. 按分类排序 + 2. 同分类内按名称排序 + """ + return sorted(fonts, key=lambda x: (x["category"], x["name"])) + +def save_fonts_json(fonts: list, output_file: Path): + """ + 保存 fonts.json + """ + # 确保输出目录存在 + output_file.parent.mkdir(parents=True, exist_ok=True) + + # 写入 JSON + with output_file.open("w", encoding="utf-8") as f: + json.dump(fonts, f, ensure_ascii=False, indent=2) + + print(f"\n✓ fonts.json 已保存到: {output_file}") + print(f"✓ 共 {len(fonts)} 个字体") + +def print_summary(fonts: list): + """ + 打印统计信息 + """ + # 按分类统计 + categories = {} + total_size = 0 + + for font in fonts: + category = font["category"] + size = font["size"] + + if category not in categories: + categories[category] = {"count": 0, "size": 0} + + categories[category]["count"] += 1 + categories[category]["size"] += size + total_size += size + + print("\n" + "="*50) + print("字体统计信息") + print("="*50) + + for category, stats in sorted(categories.items()): + size_mb = stats["size"] / 1024 / 1024 + print(f"{category:20s} {stats['count']:3d} 个字体 {size_mb:8.2f} MB") + + print("-"*50) + total_mb = total_size / 1024 / 1024 + print(f"{'总计':20s} {len(fonts):3d} 个字体 {total_mb:8.2f} MB") + print("="*50) + +def main(): + print("="*50) + print("Font2SVG - fonts.json 生成工具") + print("目标域名: fonts.biboer.cn") + print("="*50) + print() + + # 检查字体目录是否存在 + if not FONTS_DIR.exists(): + print(f"❌ 错误:字体目录不存在: {FONTS_DIR}") + return + + print(f"扫描目录: {FONTS_DIR}") + print() + + # 扫描字体 + fonts = scan_fonts(FONTS_DIR) + + if not fonts: + print("❌ 未找到任何字体文件") + return + + # 排序 + fonts = sort_fonts(fonts) + + # 保存 + save_fonts_json(fonts, OUTPUT_FILE) + + # 统计信息 + print_summary(fonts) + + print() + print("下一步操作:") + print("1. 检查生成的 fonts.json 内容") + print("2. 运行部署脚本: bash scripts/deploy-fonts.sh") + print("3. 验证访问: curl https://fonts.biboer.cn/fonts.json") + +if __name__ == "__main__": + main()