diff --git a/DETAIL-DESIGN.md b/DETAIL-DESIGN.md new file mode 100644 index 0000000..2e9af83 --- /dev/null +++ b/DETAIL-DESIGN.md @@ -0,0 +1,242 @@ +# DETAIL-DESIGN + +更新时间:2026-02-07 + +## 1. 文档目标 + +本文定义 `font2svg` 的详细设计,覆盖以下范围: + +- Web 前端(`frontend/`)的模块职责、数据结构、处理流程 +- Python CLI(`font2svg.py` / `pic2svg.py`)的职责边界 +- 字体资源组织规范与导出策略 +- 性能、错误处理、可维护性约束 + +## 2. 系统边界 + +## 2.1 子系统划分 + +1. `frontend/`:交互式预览与导出(主用户入口) +2. `scripts/generate-font-list.py`:字体清单构建 +3. `font2svg.py`:命令行字体文本转 SVG +4. `pic2svg.py`:命令行图片转 SVG + +## 2.2 非目标 + +- 不提供后端 API +- 不做字体版权管理系统 +- 不做云端存储与用户账号 + +## 3. 前端架构设计 + +## 3.1 技术选型 + +- 框架:Vue 3 + Composition API +- 构建:Vite + `vite-plugin-wasm` +- 状态:Pinia +- 样式:UnoCSS + 手写样式 +- 字体解析:`opentype.js` +- 字形 shaping:`harfbuzzjs`(已封装,主链路暂未强依赖) + +## 3.2 目录与模块映射 + +- `src/App.vue`:页面总编排、交互入口、导出触发 +- `src/stores/fontStore.ts`:字体域状态 +- `src/stores/uiStore.ts`:UI 域状态与导出选择 +- `src/components/FontSelector.vue`:字体搜索与树渲染入口 +- `src/components/FontTree.vue`:分类树、收藏、预览勾选 +- `src/components/FavoritesList.vue`:收藏列表 +- `src/components/SvgPreview.vue`:预览生成调度 +- `src/utils/svg-builder.ts`:SVG 生成核心 +- `src/utils/download.ts`:下载与打包 +- `src/utils/font-loader.ts`:字体加载(含进度) +- `src/utils/text-layout.ts`:文本换行标准化 + +## 3.3 状态模型 + +## FontInfo + +```ts +interface FontInfo { + id: string + name: string + path: string + category: string + isFavorite: boolean + font?: Font + loaded: boolean + progress: number +} +``` + +## UI 持久化 Key + +- `font.favoriteFontIds` +- `font.previewFontIds` +- `font.expandedCategories` +- `ui.fontSize` +- `ui.inputText` +- `ui.textColor` +- `ui.selectedExportItems` + +## 4. 关键流程设计 + +## 4.1 字体清单加载 + +1. `useFontLoader()` 在 `App.vue` 初始化阶段触发。 +2. 请求 `/fonts.json`。 +3. 每条记录映射为 `FontInfo`,加入 `fontStore.fonts`。 +4. 调用 `updateFontTree()` 生成分组树。 + +异常策略:请求失败弹窗提示,并记录控制台错误。 + +## 4.2 字体按需加载 + +入口:`fontStore.loadFont(fontInfo)` + +- 若 `loaded=true` 或无路径则直接返回 +- 同字体并发请求通过 `loadingFontTasks` 去重 +- 使用 `loadFontWithProgress` 拉取字体并实时写入 `progress` +- 解析成功后写入 `fontInfo.font` 与 `loaded=true` + +设计意图:避免初始一次性加载全部字体导致内存抖动。 + +## 4.3 预览生成调度 + +入口:`SvgPreview.vue` + +1. 监听 `previewFonts/inputText/fontSize/fillColor` 变化。 +2. 采用 `240ms` 防抖触发重算。 +3. 每批最多处理 `20` 个字体,批内并发 `4`。 +4. 借助 `IntersectionObserver` 懒加载后续批次。 +5. 使用 `previewGeometryCache` 缓存几何结果,颜色切换直接替换 token。 + +核心常量: + +- `PREVIEW_DEBOUNCE_MS = 240` +- `PREVIEW_BATCH_SIZE = 20` +- `PREVIEW_CONCURRENCY = 4` +- `PREVIEW_GEOMETRY_CACHE_LIMIT = 600` + +取消策略:用 `previewGenerationToken` 判定过期任务,避免旧结果污染。 + +## 4.4 文本布局 + +`wrapTextByChars(text, 45)` 策略: + +- 统一换行符 `\r\n/\r -> \n` +- 保留手动换行 +- 每行按字符数上限切分(默认 45) + +说明:该策略简单稳定,但不进行词边界或 East Asian 宽度感知换行。 + +## 4.5 SVG 生成 + +当前主链路:`generateSvg(options)` + +- 基于 `opentype.js` 的 `charToGlyph` + path 指令拼装 +- 计算 glyph 边界盒,汇总 viewBox 与 width/height +- 输出 `` 的坐标翻转结构 + +高级链路:`generateSvgWithHarfbuzz(options, fontBuffer)` + +- 已封装 HarfBuzz shaping 与定位缩放逻辑 +- 目前未接到主预览流程 + +## 4.6 导出流程 + +入口:`App.vue -> handleExport('svg' | 'png')` + +1. 先清理失效导出项(不在当前预览集合中的项) +2. 单项导出:直接下载 +3. 多项导出:聚合后 ZIP 下载 +4. PNG 导出:先 SVG,再 `canvas` 渲染为 Blob + +命名规则: + +- SVG:`{fontPart}_{textPart}.svg` +- PNG:`{fontPart}_{textPart}.png` +- `textPart` 截取前 8 字符 +- 非法字符统一替换 `_` + +## 5. 字体资源规范 + +## 5.1 目录约束 + +字体唯一来源:`frontend/public/fonts/` + +- 支持多级目录 +- 分类名来自相对目录路径 +- 根目录字体分类记为 `未分类` + +## 5.2 fonts.json 结构 + +由 `scripts/generate-font-list.py` 生成: + +```json +{ + "id": "分类/字体名", + "name": "字体名", + "filename": "字体文件名.ttf", + "category": "分类", + "path": "/fonts/分类/字体文件名.ttf" +} +``` + +## 6. Python CLI 设计 + +## 6.1 font2svg.py + +职责:把给定字体与文本转换为 SVG。 + +- 使用 `uharfbuzz` 做 shaping +- 使用 `fonttools` 读取 glyph 与路径 +- 支持单字体或目录批量转换 +- 支持字距参数 `--letter-spacing` + +## 6.2 pic2svg.py + +职责:将二值化后的图片轮廓矢量化。 + +- OpenCV 预处理灰度与阈值 +- 可选圆拟合快速路径 +- 默认依赖 potrace 做高保真描边 + +## 7. 性能设计 + +- 字体元数据与字体文件解耦(先列表后按需) +- 预览批处理 + 并发上限 +- 预览项懒加载 +- 几何缓存与颜色 token 替换,减少重复 path 生成 + +## 8. 错误处理设计 + +- 用户可恢复错误:`alert` 提示(如空文本、未选导出项) +- 任务级错误:控制台 `console.error/warn`,不中断整个批次 +- 资源加载失败:单字体失败不阻断其他字体渲染 + +## 9. 测试与质量门禁 + +当前仓库实际情况: + +- 有 TypeScript 编译检查(`pnpm -C frontend run build` 中包含 `vue-tsc`) +- 暂无标准 `lint` 脚本 +- 暂无标准单元测试脚本 + +建议目标: + +1. 增加 `lint`(ESLint) +2. 增加 `test`(Vitest) +3. 在 CI 中强制执行 `lint + typecheck + test` + +## 10. 已知限制 + +- 主预览链路尚未默认启用 HarfBuzz shaping +- 自动换行仅按固定字符数,不考虑语义断句 +- 浏览器内批量 PNG 导出在极大尺寸时可能触发内存压力 + +## 11. 后续演进建议 + +1. 将 `generateSvgWithHarfbuzz` 接入主链路并提供回退机制 +2. 为预览与导出建立一致性回归样例 +3. 增加导出并发队列与失败重试(有限次数) +4. 统一日志级别,移除生产环境调试日志 diff --git a/PLAN.md b/PLAN.md index d23ecea..e58d08f 100644 --- a/PLAN.md +++ b/PLAN.md @@ -1,1353 +1,70 @@ -# Font2SVG Web 应用开发计划 +# 项目计划(2026-02-07) -## 项目概述 +## 1. 当前状态 -基于 Figma 设计实现一个交互式字体转 SVG 工具的 Web 应用。采用纯前端架构,使用 TypeScript + Vue3 + Vite 技术栈,在浏览器中通过 opentype.js 和 harfbuzz.js (WASM) 实现字体到 SVG 的实时转换。 +### 1.1 已完成 -**设计来源**: [Figma 设计稿](https://www.figma.com/design/S7WVUzg3Z0DMWjYUC6dJzN/font2svg?node-id=3-5&m=dev) +- Web 应用主链路可用:字体选择、预览、收藏、导出(SVG/PNG) +- 字体元数据自动生成:`scripts/generate-font-list.py` +- 预览性能策略已落地:防抖、批处理、并发、懒加载、几何缓存 +- Python CLI 两条脚本可独立运行:`font2svg.py`、`pic2svg.py` -## 🔄 最新机制更新(2026-02-07) +### 1.2 主要风险 -- 字体唯一来源目录调整为 `frontend/public/fonts/` -- 删除独立 `font/` 源目录流程 -- `pnpm run prepare-fonts` 仅执行字体扫描并重建 `frontend/public/fonts.json` -- 如本文后续历史段落仍出现 `font/` 或复制流程,请以上述机制为准 +- 预览仍以 `opentype.js` 直出为主,HarfBuzz 高级模式未接入主流程 +- `frontend/src/App.test.vue` 非标准测试框架用例,自动化覆盖不足 +- 前端未配置统一 lint 脚本,质量门禁不完整 -## 📋 Figma Annotations 功能需求清单 +## 2. 迭代目标 -以下所有功能需求来自 Figma 设计中各组件的 annotation 标注: +## M1:工程质量门禁补齐(优先级 P0) -### 1. 文本输入与预览控制 (节点 8:94, 18:146) -- ✅ **输入框**: "此处输入内容" -- ✅ **触发方式**: 输入内容后,**回车**或者点击**"预览"按钮**生效 -- ✅ **预览按钮** (节点 18:157): 点击预览 +- 增加 `lint`、`typecheck`、`test` 统一脚本 +- 引入标准测试框架(Vitest + Vue Test Utils) +- 产出最小可用测试集(store / utils / 关键组件) -### 2. 字体大小调整 (节点 7:2720) -- ✅ **滑块控件**: 文字预览大小调整 -- ✅ **实时更新**: 调整时,效果预览窗口文字大小**动态变化** +验收标准: -### 3. 导出功能 (节点 23:77) -- ✅ **导出按钮**: 导出选中的预览文字的 svg 图 -- ✅ **保存位置**: 弹出保存框,默认保存在 **~/Download** -- ✅ **批量导出**: 选中多个字体时批量导出 +- `pnpm -C frontend run lint` +- `pnpm -C frontend run typecheck` +- `pnpm -C frontend run test` +- 三条命令均稳定通过 -### 4. 字体选择区 (节点 21:185) -- ✅ **数据源**: 从 `frontend/public/fonts/` 目录读取所有字体 -- ✅ **树状结构**: 字体按**目录树状分组** -- ✅ **展开/收拢**: 支持展开和收拢 -- ✅ **单选**: 字体支持单个选择 -- ✅ **批量框选**: 支持**鼠标批量框选**(Shift/Ctrl 多选) +## M2:预览与导出一致性加强(优先级 P0) -### 5. 收藏功能 -- ✅ **未收藏图标** (节点 21:200): 点击收藏该字体。**无填充**表示当前未收藏 -- ✅ **已收藏图标** (节点 21:206, 21:241): 点击取消收藏该字体。**红色填充**表示当前已收藏 -- ✅ **预览勾选** (节点 21:205, I21:205): 勾选表示在**效果预览窗口中展示** +- 统一预览与导出的文本 shaping 策略 +- 对比 `font2svg.py` 输出,建立回归样例 +- 处理复杂脚本、连字、组合字符边界情况 -### 6. 已收藏字体区 (节点 8:75) -- ✅ **展示方式**: 已收藏字体展示,字体按目录树状分组,支持展开和收拢 -- ✅ **操作方式**: 字体支持单个选择,或者鼠标批量框选 -- ✅ **取消收藏**: 点击可取消收藏 -- ✅ **动态分类** (节点 21:258): 如该分类下没有选中字体,该分类删除 +验收标准: -### 7. 效果预览区 (节点 8:95) -- ✅ **预览内容**: 预览窗口,内容、大小依据设置 -- ✅ **字体依据**: 字体依据字体选择勾选 -- ✅ **多字体对比**: 如勾选 2 个字体,显示内容的 2 个字体预览 +- 关键样例(中英文混排、多行、字距)偏差可控 +- 导出结果与预览视觉一致 -### 8. 预览项细节 (节点 8:130-8:135) -- ✅ **字体名称** (节点 8:134): 预览字体名,取**文件名** -- ✅ **预览窗口** (节点 8:135): 某一字体预览窗口 -- ✅ **选中交互**: 点击任意区域选中或取消选中 -- ✅ **导出标记**: 选中的可以导出 -- ✅ **选择框** (节点 I23:55): 选中/未选中 +## M3:大字体库性能优化(优先级 P1) -### 9. 版权说明 (节点 5:15) -- ✅ 底部显示:@版权说明:所有字体来源互联网分享,仅供效果预览,不做下载传播,如有侵权,请告知douboer@gmail.com +- 增加字体对象 LRU 缓存与内存上限策略 +- 优化 `fonts.json` 增量生成能力 +- 导出阶段增加并发与失败重试上限 -## ⚠️ 核心技术决策 +验收标准: -**字体到 SVG 转换实现方式:TypeScript 重新实现** +- 100+ 字体时首屏可交互时间维持稳定 +- 批量导出失败率下降 -**决策内容**: -- ✅ 使用 TypeScript + opentype.js + harfbuzz.js 重新实现核心转换逻辑 -- ✅ `font2svg.py` 保持不变,仅作为算法参考 -- ❌ 不调用 Python 代码(不使用 FastAPI 后端、Pyodide 或 Electron) +## M4:文档与运维完善(优先级 P1) -**决策理由**: -1. **纯前端架构** - 无需后端服务器,部署成本为零 -2. **性能更优** - 浏览器直接处理,无网络延迟 -3. **用户体验佳** - 实时预览响应快,交互流畅 -4. **可离线使用** - 可打包为 PWA,完全离线工作 -5. **技术可行** - opentype.js + harfbuzz.js 完全覆盖 font2svg.py 的功能 +- 保持 `README.md` / `USAGE.md` / `DETAIL-DESIGN.md` 同步 +- 补充发布流程与版本变更记录模板 -**技术对比**: -```typescript -// font2svg.py 核心逻辑(~100 行) -// 1. 加载字体 → opentype.parse(buffer) -// 2. Text shaping → harfbuzz.js WASM -// 3. 获取字形 → glyph.getPath() -// 4. 坐标变换 → 简单数学计算 -// 5. 生成 SVG → 字符串拼接 -``` +## 3. 执行原则 -## 技术架构 +- 小步提交:每次改动聚焦单一目标 +- 先验证再交付:测试、类型检查、lint 必跑 +- 文档与代码同步更新 -### 核心技术栈 -- **前端框架**: Vue 3 + TypeScript -- **构建工具**: Vite -- **样式方案**: UnoCSS (原子化 CSS,与 Tailwind 兼容) -- **状态管理**: Pinia -- **字体处理**: opentype.js + harfbuzz.js (WASM) +## 4. 里程碑建议 -### 关键约束 -- ✅ 保持 `font2svg.py` 不变,仅作为参考 -- ✅ 纯前端实现,无需后端服务器 -- ✅ 字体文件放在 `frontend/public/fonts/` 目录 -- ✅ SVG 图标放在 `src/assets/icons/` -- ✅ 应用图标为 `src/assets/webicon.png` -- ✅ 使用 fonttools.subset 预处理字体(构建时) -- ❌ 暂不实现运行时缓存策略 -- ❌ 暂不需要许可证管理 - -## 项目结构 - -``` -font2svg/ -├── frontend/ -│ ├── public/ -│ │ ├── fonts/ # 字体唯一来源目录(支持分类子目录) -│ │ └── fonts.json # 由脚本重建的字体清单 -│ └── src/ -│ ├── assets/ -│ │ ├── icons/ # SVG UI 图标 -│ │ └── webicon.png # 应用图标 -│ ├── components/ -│ ├── composables/ -│ ├── stores/ -│ ├── utils/ -│ ├── types/ -│ ├── App.vue -│ └── main.ts -├── scripts/ -│ └── generate-font-list.py # 扫描 frontend/public/fonts 并重建 fonts.json -├── frontend/vite.config.ts -├── frontend/tsconfig.json -├── frontend/uno.config.ts -└── package.json -``` - -## 开发阶段 - -### 阶段 1: 项目初始化与环境配置 (1-2 天) - -**目标**: 搭建开发环境和基础架构 - -**任务**: -1. 初始化 Vite + Vue3 + TypeScript 项目 - ```bash - pnpm create vite font2svg-web --template vue-ts - ``` - -2. 安装核心依赖 - ```bash - pnpm add opentype.js harfbuzzjs pinia - pnpm add -D unocss @unocss/preset-wind vite-plugin-wasm - ``` - -3. 配置 Vite 支持 WASM - ```typescript - // vite.config.ts - import wasm from 'vite-plugin-wasm'; - export default defineConfig({ - plugins: [vue(), UnoCSS(), wasm()], - optimizeDeps: { exclude: ['harfbuzzjs'] } - }); - ``` - -4. 配置 UnoCSS - ```typescript - // uno.config.ts - import { defineConfig, presetWind } from 'unocss'; - export default defineConfig({ - presets: [presetWind()] - }); - ``` - -5. 创建目录结构 - - 创建 `src/components/`、`src/composables/`、`src/stores/`、`src/utils/`、`src/types/` - - 复制 UI 图标到 `src/assets/icons/` - - 复制应用图标到 `src/assets/webicon.png` - -**验收标准**: -- ✅ 项目可成功启动并显示 Hello World -- ✅ UnoCSS 样式正常工作 -- ✅ TypeScript 类型检查无错误 - ---- - -### 阶段 2: 核心转换模块开发 (3-5 天) - -**目标**: 实现字体到 SVG 的核心转换逻辑 - -**参考**: `font2svg.py` 的实现算法 - -**任务**: - -#### 2.1 HarfBuzz WASM 封装 -```typescript -// src/utils/harfbuzz.ts -import hbjs from 'harfbuzzjs'; - -let hb: any = null; - -export async function initHarfbuzz() { - if (!hb) { - hb = await hbjs(); - } - return hb; -} - -export interface ShapedGlyph { - glyphIndex: number; - xAdvance: number; - yAdvance: number; - xOffset: number; - yOffset: number; -} - -export async function shapeText( - fontBuffer: ArrayBuffer, - text: string -): Promise { - const hb = await initHarfbuzz(); - - const blob = hb.createBlob(fontBuffer); - const face = hb.createFace(blob, 0); - const font = hb.createFont(face); - - const buffer = hb.createBuffer(); - buffer.addText(text); - buffer.guessSegmentProperties(); - hb.shape(font, buffer); - - const result = buffer.json(); - - // 清理资源 - buffer.destroy(); - font.destroy(); - face.destroy(); - blob.destroy(); - - return result.map((item: any) => ({ - glyphIndex: item.g, - xAdvance: item.ax, - yAdvance: item.ay, - xOffset: item.dx, - yOffset: item.dy - })); -} -``` - -#### 2.2 字体加载模块 -```typescript -// src/utils/font-loader.ts -import opentype from 'opentype.js'; - -export async function loadFont( - source: File | string | ArrayBuffer -): Promise { - if (source instanceof File) { - const buffer = await source.arrayBuffer(); - return opentype.parse(buffer); - } else if (typeof source === 'string') { - return opentype.load(source); - } else { - return opentype.parse(source); - } -} - -export async function loadFontWithProgress( - url: string, - onProgress?: (percent: number) => void -): Promise { - const response = await fetch(url); - const contentLength = response.headers.get('content-length'); - const total = contentLength ? parseInt(contentLength) : 0; - - const reader = response.body!.getReader(); - const chunks: Uint8Array[] = []; - let loaded = 0; - - while (true) { - const { done, value } = await reader.read(); - if (done) break; - - chunks.push(value); - loaded += value.length; - - if (onProgress && total > 0) { - onProgress(Math.round((loaded / total) * 100)); - } - } - - const buffer = new Uint8Array(loaded); - let offset = 0; - for (const chunk of chunks) { - buffer.set(chunk, offset); - offset += chunk.length; - } - - return opentype.parse(buffer.buffer); -} -``` - -#### 2.3 SVG 生成核心逻辑 - -**⚠️ 关键实现:参考 `font2svg.py` 用 TypeScript 重写** - -这个模块是核心转换逻辑,完全对应 `font2svg.py` 的算法流程: - -| Python 实现 | TypeScript 等价实现 | -|------------|-------------------| -| `TTFont(font_path)` | `opentype.parse(buffer)` | -| `hb.shape()` | `harfbuzz.js WASM shapeText()` | -| `font.getGlyphSet()[gid]` | `font.glyphs.get(glyphIndex)` | -| `glyph.draw(pen)` | `glyph.getPath().toPathData()` | -| 坐标变换(26.6 定点数) | `item.xAdvance / 64` | -| SVG 模板拼接 | 字符串拼接 | - -```typescript -// src/composables/useSvgGenerate.ts -import { ref } from 'vue'; -import type { Font } from 'opentype.js'; -import { shapeText, initHarfbuzz } from '@/utils/harfbuzz'; - -export function useSvgGenerate() { - const svgContent = ref(''); - const isGenerating = ref(false); - const error = ref(null); - - async function generate( - font: Font, - text: string, - letterSpacing: number = 0, - fontSize: number = 72 - ) { - isGenerating.value = true; - error.value = null; - - try { - // 1. 初始化 HarfBuzz - await initHarfbuzz(); - - // 2. 获取字体 buffer - const fontBuffer = font.toArrayBuffer(); - - // 3. Text shaping - const shaped = await shapeText(fontBuffer, text); - - // 4. 提取字形路径和位置 - const scale = fontSize / font.unitsPerEm; - let currentX = 0; - const paths: Array<{ d: string; x: number; y: number }> = []; - - for (const item of shaped) { - const glyph = font.glyphs.get(item.glyphIndex); - const glyphPath = glyph.getPath(0, 0, fontSize); - - const x = currentX + (item.xOffset / 64) * scale; - const y = (item.yOffset / 64) * scale; - - paths.push({ - d: glyphPath.toPathData(), - x, - y - }); - - // HarfBuzz 使用 26.6 定点数(除以 64) - currentX += (item.xAdvance / 64) * scale + letterSpacing; - } - - // 5. 计算边界框 - const fullPath = font.getPath(text, 0, 0, fontSize); - const bbox = fullPath.getBoundingBox(); - - const width = Math.ceil(bbox.x2 - bbox.x1 + letterSpacing * (text.length - 1)); - const height = Math.ceil(bbox.y2 - bbox.y1); - const offsetX = -bbox.x1; - const offsetY = -bbox.y1; - - // 6. 生成 SVG 文档 - const pathElements = paths.map(({ d, x, y }) => { - return ` `; - }).join('\n'); - - svgContent.value = ` - - -${pathElements} - -`; - - } catch (err) { - error.value = err instanceof Error ? err.message : '生成失败'; - console.error('SVG 生成错误:', err); - } finally { - isGenerating.value = false; - } - } - - return { svgContent, isGenerating, error, generate }; -} -``` - -#### 2.4 下载工具 -```typescript -// src/utils/download.ts -export function downloadSvg(content: string, filename: string) { - const blob = new Blob([content], { type: 'image/svg+xml;charset=utf-8' }); - const url = URL.createObjectURL(blob); - const link = document.createElement('a'); - link.href = url; - link.download = filename.endsWith('.svg') ? filename : `${filename}.svg`; - link.click(); - URL.revokeObjectURL(url); -} - -export async function downloadMultipleSvgs( - svgList: Array<{ content: string; filename: string }> -) { - // 使用 JSZip 打包多个 SVG - const JSZip = (await import('jszip')).default; - const zip = new JSZip(); - - svgList.forEach(({ content, filename }) => { - zip.file(filename, content); - }); - - const blob = await zip.generateAsync({ type: 'blob' }); - const url = URL.createObjectURL(blob); - const link = document.createElement('a'); - link.href = url; - link.download = 'font-svgs.zip'; - link.click(); - URL.revokeObjectURL(url); -} -``` - -**验收标准**: -- ✅ 成功加载 TTF/OTF 字体文件 -- ✅ HarfBuzz WASM 正常初始化和工作 -- ✅ 能够将文本转换为 SVG 格式 -- ✅ SVG 输出与 `font2svg.py` 结果一致 -- ✅ 支持中文字符和复杂排版 - ---- - -### 阶段 3: UI 组件开发 (5-7 天) - -**目标**: 实现 Figma 设计的所有界面组件 - -**Figma 设计参考**: 节点 3:5 - -#### 3.1 状态管理 - -**⚠️ 重要性能策略:按需加载字体** - -如果 `font/` 目录有 100+ 个字体文件(总计 1-2GB),**绝对不能**在初始化时全部加载到内存。 - -**正确的加载策略**: -1. ✅ **初始化时**:只加载字体元数据(JSON,~10KB) - - 字体名称、路径、分类 - - 不加载字体文件本身 -2. ✅ **按需加载**:用户选择字体时才加载该字体文件 - - 点击预览 → 加载字体 → 生成 SVG -3. ✅ **内存缓存**:已加载的字体保存在内存中 - - 避免重复加载同一字体 -4. ✅ **可选优化**:LRU 策略限制缓存数量 - - 最多缓存 20 个字体(~200-400MB) - - 超出后清除最久未使用的字体 - -```typescript -// src/stores/fontStore.ts -import { defineStore } from 'pinia'; -import type { Font } from 'opentype.js'; - -export interface FontInfo { - id: string; - name: string; - category: string; - path: string; - font?: Font; // ⚠️ 注意:初始时为 undefined,按需加载 -} - -export const useFontStore = defineStore('font', { - state: () => ({ - fontList: [] as FontInfo[], - selectedFonts: [] as string[], // font IDs for preview - currentFont: null as Font | null, - text: '星程紫微', - letterSpacing: 0, - fontSize: 72 - }), - - actions: { - async loadFontList() { - // ⚠️ 只加载元数据 JSON,不加载字体文件 - // 这个 JSON 只有 ~10KB,包含 100+ 字体的基本信息 - const response = await fetch('/fonts.json'); - this.fontList = await response.json(); - }, - - async loadFont(fontId: string) { - const fontInfo = this.fontList.find(f => f.id === fontId); - if (!fontInfo) return; - - // ⚠️ 关键:只有当字体未加载时才加载 - if (!fontInfo.font) { - const { loadFont } = await import('@/utils/font-loader'); - // 这里才真正加载 10-20MB 的字体文件 - fontInfo.font = await loadFont(fontInfo.path); - } - - this.currentFont = fontInfo.font; - }, - - toggleFontSelection(fontId: string) { - const index = this.selectedFonts.indexOf(fontId); - if (index === -1) { - this.selectedFonts.push(fontId); - } else { - this.selectedFonts.splice(index, 1); - } - } - } -}); -``` - -```typescript -// src/composables/useFavorites.ts -import { ref, computed } from 'vue'; - -const STORAGE_KEY = 'font2svg-favorites'; - -export function useFavorites() { - const favorites = ref([]); - - // 从 localStorage 加载 - function load() { - const stored = localStorage.getItem(STORAGE_KEY); - if (stored) { - favorites.value = JSON.parse(stored); - } - } - - // 保存到 localStorage - function save() { - localStorage.setItem(STORAGE_KEY, JSON.stringify(favorites.value)); - } - - function toggle(fontId: string) { - const index = favorites.value.indexOf(fontId); - if (index === -1) { - favorites.value.push(fontId); - } else { - favorites.value.splice(index, 1); - } - save(); - } - - function isFavorite(fontId: string) { - return favorites.value.includes(fontId); - } - - // 初始化时加载 - load(); - - return { - favorites: computed(() => favorites.value), - toggle, - isFavorite - }; -} -``` - -#### 3.2 主布局组件 - -**参考**: Figma 节点 3:5 主界面布局 + annotations - -```vue - - - - -``` - -#### 3.3 字体选择器组件 -```vue - - - - -``` - -#### 3.4 字体树组件(支持展开/收拢) - -**参考**: Figma 节点 21:188-21:226,支持批量框选(Shift/Ctrl) - -```vue - - - - -``` - -#### 3.5 SVG 预览组件 -```vue - - - - -``` - -```vue - - - - -``` - -**验收标准**: -- ✅ 界面布局与 Figma 设计一致 -- ✅ 字体树支持展开/收拢 -- ✅ 收藏功能正常(LocalStorage 持久化) -- ✅ 多字体预览正常显示 -- ✅ 输入防抖生效(300ms) -- ✅ 响应式布局适配不同屏幕 -- ✅ **关键**:100+ 字体初始化时不卡顿(只加载元数据) -- ✅ **关键**:字体按需加载,已加载字体缓存生效 - ---- - -### 阶段 4: 字体资源管理 (2-3 天) - -**目标**: 配置字体文件的加载和优化 - -#### 4.1 字体元数据生成 - -**⚠️ 重要**:这个脚本生成的 JSON 文件是唯一在初始化时加载的数据,必须保持轻量(~10KB)。 - -```python -# scripts/scan-fonts.py -import os -import json -from pathlib import Path - -def scan_fonts(font_dir: str): - fonts = [] - - for root, dirs, files in os.walk(font_dir): - category = Path(root).name - if category == Path(font_dir).name: - category = "其他字体" - - for file in files: - if file.endswith(('.ttf', '.otf')): - font_path = os.path.join(root, file) - rel_path = os.path.relpath(font_path, start=os.path.dirname(font_dir)) - - fonts.append({ - 'id': file.replace('.', '_').replace(' ', '_'), - 'name': Path(file).stem, - 'category': category, - 'path': f'/fonts/{rel_path}' - }) - - return fonts - -if __name__ == '__main__': - fonts = scan_fonts('./font') - - with open('public/fonts.json', 'w', encoding='utf-8') as f: - json.dump(fonts, f, ensure_ascii=False, indent=2) - - print(f'扫描完成,共找到 {len(fonts)} 个字体') -``` - -#### 4.2 字体子集化脚本 -```python -# scripts/prepare-fonts.py -import os -import sys -from pathlib import Path -from fontTools.subset import Subsetter, Options -from fontTools.ttLib import TTFont - -# 常用汉字(3500 字) -COMMON_CHARS = """ -的一是在不了有和人这中大为上个国我以要他时来用们生到作地于出就分对成会可主发年动同工也能下过子说产种面而方后多定行学法所民得经十三之进着等部度家电力里如水化高自二理起小物现实加量都两体制机当使点从业本去把性好应开它合还因由其些然前外天政四日那社义事平形相全表间样与关各重新线内数正心反你明看原又么利比或但质气第向道命此变条只没结解问意建月公无系军很情者最立代想已通并提直题党程展五果料象员革位入常文总次品式活设及管特件长求老头基资边流路级少图山统接知较将组见计别她手角期根论运农指几九区强放决西被干做必战先回则任取据处队南给色光门即保治北造百规热领七海口东导器压志世金增争济阶油思术极交受联什认六共权收证改清己美再采转更单风切打白教速花带安场身车例真务具万每目至达走积示议声报斗完类八离华名确才科张信马节话米整空元况今集温传土许步群广石记需段研界拉林律叫且究观越织装影算低持音众书布复容儿须际商非验连断深难近矿千周委素技备半办青省列习响约支般史感劳便团往酸历市克何除消构府称太准精值号率族维划选标写存候毛亲快效斯院查江型眼王按格养易置派层片始却专状育厂京识适属圆包火住调满县局照参红细引听该铁价严 -""" - -def subset_font(input_path: str, output_path: str, text: str): - """对字体进行子集化""" - try: - font = TTFont(input_path) - - # 配置选项 - options = Options() - options.drop_tables.add('GSUB') # 移除连字表 - options.drop_tables.add('GPOS') # 移除位置表 - - # 创建子集器 - subsetter = Subsetter(options=options) - subsetter.populate(text=text) - subsetter.subset(font) - - # 保存 - os.makedirs(os.path.dirname(output_path), exist_ok=True) - font.save(output_path) - - # 计算压缩率 - original_size = os.path.getsize(input_path) - subset_size = os.path.getsize(output_path) - ratio = (1 - subset_size / original_size) * 100 - - print(f'✓ {Path(input_path).name}') - print(f' 原始: {original_size / 1024 / 1024:.2f} MB') - print(f' 子集: {subset_size / 1024 / 1024:.2f} MB') - print(f' 压缩: {ratio:.1f}%') - - return True - except Exception as e: - print(f'✗ {Path(input_path).name}: {e}') - return False - -def main(): - font_dir = Path('./font') - output_dir = Path('./public/fonts') - - if not font_dir.exists(): - print('错误: font/ 目录不存在') - sys.exit(1) - - # 清空输出目录 - if output_dir.exists(): - import shutil - shutil.rmtree(output_dir) - - # 扫描并处理所有字体 - font_files = list(font_dir.rglob('*.ttf')) + list(font_dir.rglob('*.otf')) - - print(f'找到 {len(font_files)} 个字体文件') - print(f'开始子集化(保留 {len(COMMON_CHARS)} 个常用汉字)...\n') - - success = 0 - for font_path in font_files: - rel_path = font_path.relative_to(font_dir) - output_path = output_dir / rel_path - - if subset_font(str(font_path), str(output_path), COMMON_CHARS): - success += 1 - print() - - print(f'完成!成功处理 {success}/{len(font_files)} 个字体') - -if __name__ == '__main__': - main() -``` - -#### 4.3 package.json 脚本 -```json -{ - "scripts": { - "dev": "vite", - "build": "vite build", - "preview": "vite preview", - "prepare-fonts": "python scripts/prepare-fonts.py && python scripts/scan-fonts.py" - } -} -``` - -**验收标准**: -- ✅ 字体元数据 JSON 正确生成 -- ✅ 字体子集化成功,文件大小减少 60-80% -- ✅ 前端可正常加载优化后的字体 -- ✅ 子集字体包含所有测试文本字符 - ---- - -### 阶段 5: 导出功能与优化 (2-3 天) - -**目标**: 完成 SVG 导出和性能优化 - -#### 5.1 导出功能 -```typescript -// src/composables/useExport.ts -import { downloadSvg, downloadMultipleSvgs } from '@/utils/download'; -import { useFontStore } from '@/stores/fontStore'; -import { useSvgGenerate } from './useSvgGenerate'; - -export function useExport() { - const fontStore = useFontStore(); - const { generate } = useSvgGenerate(); - - async function exportSelected() { - const selectedFonts = fontStore.fontList.filter(f => - fontStore.selectedFonts.includes(f.id) - ); - - if (selectedFonts.length === 0) { - alert('请先选择要导出的字体'); - return; - } - - if (selectedFonts.length === 1) { - // 单个导出 - const font = selectedFonts[0]; - await fontStore.loadFont(font.id); - - if (fontStore.currentFont) { - const { svgContent } = await generate( - fontStore.currentFont, - fontStore.text, - fontStore.letterSpacing, - fontStore.fontSize - ); - - const filename = `${font.name}_${fontStore.text}.svg`; - downloadSvg(svgContent, filename); - } - } else { - // 批量导出为 ZIP - const svgList = []; - - for (const font of selectedFonts) { - await fontStore.loadFont(font.id); - - if (fontStore.currentFont) { - const { svgContent } = await generate( - fontStore.currentFont, - fontStore.text, - fontStore.letterSpacing, - fontStore.fontSize - ); - - svgList.push({ - content: svgContent, - filename: `${font.name}_${fontStore.text}.svg` - }); - } - } - - await downloadMultipleSvgs(svgList); - } - } - - return { exportSelected }; -} -``` - -#### 5.2 Web Worker 优化 -```typescript -// src/workers/font-processor.worker.ts -import opentype from 'opentype.js'; -import { shapeText } from '@/utils/harfbuzz'; - -self.onmessage = async (e) => { - const { type, data } = e.data; - - try { - switch (type) { - case 'load-font': { - const font = opentype.parse(data.buffer); - self.postMessage({ - type: 'font-loaded', - data: { fontName: font.names.fullName.en } - }); - break; - } - - case 'generate-svg': { - const { fontBuffer, text, letterSpacing, fontSize } = data; - const font = opentype.parse(fontBuffer); - - // ... SVG 生成逻辑 ... - - self.postMessage({ - type: 'svg-generated', - data: { svgContent } - }); - break; - } - } - } catch (error) { - self.postMessage({ - type: 'error', - error: error.message - }); - } -}; -``` - -#### 5.3 性能优化 -- 添加 Service Worker 缓存 harfbuzz.js WASM 文件 -- 实现虚拟滚动优化字体列表渲染 -- 添加图片懒加载 -- 优化防抖时间和逻辑 - -**验收标准**: -- ✅ 单个 SVG 导出正常 -- ✅ 批量导出 ZIP 正常 -- ✅ 文件命名格式正确 -- ✅ 大字体文件加载不卡顿 -- ✅ 输入防抖生效,性能流畅 - ---- - -### 阶段 6: 测试与部署 (1-2 天) - -**目标**: 全面测试和准备部署 - -#### 6.1 功能测试清单 -- [ ] 字体上传功能 -- [ ] 预置字体加载 -- [ ] 字体树展开/收拢 -- [ ] 收藏功能(刷新后保持) -- [ ] 文本输入和实时预览 -- [ ] 字号调整滑块 -- [ ] 字间距调整 -- [ ] 多字体对比预览 -- [ ] 单个 SVG 导出 -- [ ] 批量 ZIP 导出 -- [ ] 错误提示 -- [ ] 响应式布局 - -#### 6.2 浏览器兼容性测试 -- [ ] Chrome 最新版 -- [ ] Firefox 最新版 -- [ ] Safari 最新版 -- [ ] Edge 最新版 - -#### 6.3 部署配置 -```yaml -# vercel.json -{ - "headers": [ - { - "source": "/fonts/(.*)", - "headers": [ - { - "key": "Cache-Control", - "value": "public, max-age=31536000, immutable" - } - ] - } - ] -} -``` - -**验收标准**: -- ✅ 所有功能测试通过 -- ✅ 主流浏览器兼容 -- ✅ 成功部署到 Vercel -- ✅ 文档齐全 - ---- - -## 技术难点与解决方案 - -### 1. HarfBuzz WASM 体积大(~1-2MB) -**解决方案**: -- 代码分割,动态 import -- CDN 加速 -- Service Worker 缓存 - -### 2. 中文字体文件大(10-20MB) -**解决方案**: -- 构建时子集化(保留常用 3500 字) -- 流式加载 + 进度条 -- Web Worker 处理避免 UI 阻塞 - -### 3. opentype.js + harfbuzz.js 集成复杂 -**解决方案**: -- 先用 opentype.js 实现简单版本 -- 逐步集成 harfbuzz.js -- 参考 font2svg.py 的算法 - -### 4. 性能优化 -**解决方案**: -- 输入防抖(300ms) -- 虚拟滚动(字体列表) -- Canvas 快速预览 + SVG 精确输出 -- 字形路径缓存 - ---- - -## 依赖包清单 - -```json -{ - "dependencies": { - "vue": "^3.4.0", - "pinia": "^2.1.7", - "opentype.js": "^1.3.4", - "harfbuzzjs": "^0.3.3", - "lodash-es": "^4.17.21", - "jszip": "^3.10.1" - }, - "devDependencies": { - "@vitejs/plugin-vue": "^5.0.0", - "typescript": "^5.3.0", - "vite": "^5.0.0", - "unocss": "^0.58.0", - "@unocss/preset-wind": "^0.58.0", - "vite-plugin-wasm": "^3.3.0" - } -} -``` - ---- - -## 开发时间估算 - -| 阶段 | 预计时间 | 累计时间 | -|------|---------|---------| -| 阶段 1: 项目初始化 | 1-2 天 | 1-2 天 | -| 阶段 2: 核心转换模块 | 3-5 天 | 4-7 天 | -| 阶段 3: UI 组件开发 | 5-7 天 | 9-14 天 | -| 阶段 4: 字体资源管理 | 2-3 天 | 11-17 天 | -| 阶段 5: 导出与优化 | 2-3 天 | 13-20 天 | -| 阶段 6: 测试与部署 | 1-2 天 | 14-22 天 | - -**总计: 14-22 天** - ---- - -## 成功标准 - -1. ✅ 所有 Figma 设计功能完整实现 -2. ✅ SVG 输出与 `font2svg.py` 一致 -3. ✅ 支持中文字体和复杂排版 -4. ✅ 性能流畅,无明显卡顿 -5. ✅ 主流浏览器兼容 -6. ✅ 代码规范,类型安全 -7. ✅ 成功部署并可访问 - ---- - -## 备注 - -- 本计划基于纯前端实现,无需后端服务器 -- `font2svg.py` 保持不变,仅作为参考 -- 使用 fonttools.subset 在构建时优化字体 -- 暂不实现运行时缓存策略 -- 暂不需要许可证管理功能 +1. 第 1 周:完成 M1 +2. 第 2 周:完成 M2 +3. 第 3 周:完成 M3、M4 diff --git a/README.md b/README.md index 62e448a..d94144a 100644 --- a/README.md +++ b/README.md @@ -1,125 +1,83 @@ # font2svg -一个基于 Vue 3 + TypeScript 的本地字体预览与导出工具。 +本仓库提供两条能力链路: -核心目标:从本地字体库中选择字体,实时生成文本预览,并导出为 `SVG` 或 `PNG`。 +- Web 应用(`frontend/`):本地字体预览、多字体对比、导出 `SVG/PNG` +- Python CLI(根目录脚本):图片转 SVG、字体文本转 SVG -## 界面快照 +## 文档导航 -![snapshot](frontend/src/assets/snapshot.png) +- 项目总览:`README.md` +- 详细设计:`DETAIL-DESIGN.md` +- 使用说明:`USAGE.md` +- 迭代计划:`PLAN.md` +- 前端子项目说明:`frontend/README.md` -## 当前功能 +## 核心特性 -- 字体库加载 - - 从 `frontend/public/fonts.json` 读取字体清单 - - 支持按分类展示字体树 - - 支持收藏字体列表 -- 预览控制 - - 勾选字体进入预览 - - 多字体并行预览 - - 预览项可单独勾选用于导出 -- 文本输入 - - 输入框支持手动回车换行 - - 自动按每行 45 字换行(保留手动换行) -- 样式控制 - - 字号滑块(10 ~ 500) - - 文字颜色选择 -- 导出 - - 支持导出 `SVG` - - 支持导出 `PNG`(由 SVG 转换) - - 多项导出自动打包 zip - - 导出文件名规则:`字体名_内容前8字.扩展名` +- 字体树 + 分类折叠 + 搜索 + 收藏 +- 预览勾选与导出勾选分离,支持全选/全不选 +- 文本自动按每行 45 字换行(保留手动换行) +- 预览采用分批加载与并发渲染,降低大字体库卡顿 +- 导出 `SVG` 或 `PNG`,多项自动打包 ZIP +- 字体来源统一为 `frontend/public/fonts/` -## 技术栈 +## 目录结构 -- `Vue 3` -- `TypeScript` -- `Vite` -- `Pinia` -- `UnoCSS` -- `opentype.js` -- `harfbuzzjs` -- `jszip` - -## 目录说明 - -- `frontend/`: 前端应用源码 -- `frontend/public/fonts/`: 字体目录(唯一字体来源,支持分类子目录) -- `frontend/public/fonts.json`: 字体清单(由脚本重建) -- `scripts/generate-font-list.py`: 扫描 `frontend/public/fonts` 并生成 `fonts.json` +```text +font2svg/ +├── frontend/ # Vue3 + TS Web 应用 +│ ├── public/ +│ │ ├── fonts/ # 字体唯一来源目录 +│ │ └── fonts.json # 字体清单(脚本生成) +│ └── src/ +├── scripts/ +│ └── generate-font-list.py # 生成 frontend/public/fonts.json +├── font2svg.py # 字体文本转 SVG(Python CLI) +├── pic2svg.py # 图片转 SVG(Python CLI) +└── DETAIL-DESIGN.md # 详细设计 +``` ## 环境要求 -- Node.js 18+ -- pnpm 8+ -- Python 3(用于字体准备脚本) +- Node.js `>=18` +- pnpm `>=8` +- Python `>=3.9` +- (可选)potrace:`pic2svg.py` 需要 -## 快速开始 - -### 1. 安装依赖 +## 快速开始(Web) ```bash pnpm -C frontend install -``` - -### 2. 准备字体 - -将字体放入如下结构: - -```text -frontend/public/fonts/ - 手写/ - 字体A.ttf - 黑体/ - 字体B.otf -``` - -然后执行: - -```bash pnpm run prepare-fonts -``` - -该命令会扫描 `frontend/public/fonts/` 并重新生成 `frontend/public/fonts.json`。 - -### 3. 启动开发环境 - -```bash pnpm run dev ``` -默认由 Vite 启动前端开发服务。 +默认访问地址:`http://localhost:5174` ## 常用命令 ```bash -# 启动开发 +# 根目录 pnpm run dev - -# 构建(调用 frontend build) pnpm run build - -# 本地预览构建产物 pnpm run preview - -# 重新生成字体清单 pnpm run prepare-fonts + +# 前端子项目 +pnpm -C frontend run dev +pnpm -C frontend run build +pnpm -C frontend run preview ``` -## 导出行为说明 +## Python CLI 概览 -- 单个选中项:直接下载对应文件(`svg` 或 `png`) -- 多个选中项:打包 zip 下载 - - SVG: `font2svg-svg-export.zip` - - PNG: `font2svg-png-export.zip` -- 导出前会自动清理失效选中项,避免导出已不在当前预览列表中的字体 +```bash +# 字体转 SVG +python font2svg.py --font path/to/font.ttf --text "你好" -## 文本布局说明 +# 图片转 SVG +python pic2svg.py images/your_image.png --outdir output +``` -- 输入文本会先做换行标准化(`\r\n` -> `\n`) -- 每行超过 45 字会自动分行 -- SVG 高度按字形真实边界计算,不额外添加内置 padding - -## 旧脚本说明 - -仓库仍保留 `pic2svg.py` / `font2svg.py` 等 Python 脚本,用于历史流程或离线转换;当前主交互流程以上述 `frontend/` Web 应用为准。 +详细参数请看 `USAGE.md`。 diff --git a/USAGE.md b/USAGE.md index c74d922..8b9e0dd 100644 --- a/USAGE.md +++ b/USAGE.md @@ -1,44 +1,77 @@ -# 使用示例与说明 +# 使用说明 -## 🚀 快速开始 +本文覆盖两类使用方式: -### 1. 安装依赖 +- Web 应用(推荐):`frontend/` 图形界面 +- Python CLI:`font2svg.py` / `pic2svg.py` + +## 1. Web 应用 + +### 1.1 安装依赖 + +```bash +pnpm -C frontend install +``` + +### 1.2 准备字体 + +将字体放入:`frontend/public/fonts/`,支持多级目录,例如: + +```text +frontend/public/fonts/ + 书法/ + 字体A.ttf + 黑体/ + 字体B.otf +``` + +执行清单生成: + +```bash +pnpm run prepare-fonts +``` + +该命令会重建:`frontend/public/fonts.json` + +### 1.3 启动与构建 + +```bash +pnpm run dev +pnpm run build +pnpm run preview +``` + +默认开发端口:`5174`。 + +### 1.4 页面操作 + +1. 左侧字体区搜索/勾选字体进入预览。 +2. 可点击收藏按钮加入“已收藏字体”。 +3. 顶部设置字号、颜色、输入内容。 +4. 右侧预览区点击条目可切换导出选择。 +5. 点击导出 `SVG` 或 `PNG`。 + +导出规则: + +- 单个条目:直接下载 +- 多个条目:自动打包 ZIP +- 文件名:`字体名_文本前8字符.扩展名` + +## 2. Python CLI + +### 2.1 安装依赖 ```bash pip install -r requirements.txt ``` -### 2. 安装 potrace(必需) +`pic2svg.py` 还需要 `potrace`: ```bash brew install potrace ``` -### 2.1 字体转SVG依赖 - -```bash -pip install fonttools uharfbuzz -``` - -### 3. 转换单个文件 - -```bash -python pic2svg.py images/your_image.png -``` - -### 3.1 指定输出目录 - -```bash -python pic2svg.py images/your_image.png --outdir output/ -``` - -### 4. 批量转换 - -```bash -python pic2svg.py --indir images --outdir output -``` - -### 5. 字体转SVG +### 2.2 字体转 SVG(font2svg.py) ```bash python font2svg.py --font path/to/font.ttf --text "Hello" @@ -47,56 +80,53 @@ python font2svg.py --font path/to/font.ttf --text "Hello" --letter-spacing 20 python font2svg.py --fontdir font --text "星程紫微" --outdir svg ``` -说明:单字体输出文件名根据 `--text` 自动生成;使用 `--fontdir` 时会加上字体名作为前缀。 +参数: -## ⚙️ 参数说明 +- `--font`:单个字体文件(ttf/otf) +- `--fontdir`:字体目录(批量处理) +- `--text`:必填,渲染文字 +- `--outdir`:输出目录,不传则输出到当前目录 +- `--letter-spacing`:字距(字体单位) -- `--threshold`:固定阈值(0-255),默认使用 Otsu 自动阈值。 -- `--indir`:输入目录(批量转换)。 -- `--outdir`:输出目录(自动创建,使用输入文件名.svg)。 -- `--turdsize`:抑制噪点面积阈值,越小保留细节越多。 -- `--opttolerance`:曲线优化容差,越大文件越小但可能失真。 -- `--unit`:坐标量化单位,`1` 表示不量化。 -- `--optimize-curves`:开启曲线优化(更小但可能略失真)。 -- `--circle-fit`:圆拟合误差阈值(相对半径),>0 启用圆替代。 -- `--font`:字体文件路径(ttf/otf)。 -- `--fontdir`:字体目录(遍历ttf/otf)。 -- `--text`:文字内容。 -- `--letter-spacing`:字距(字体单位),默认 0。 - -## 🧩 常用配置示例 +### 2.3 图片转 SVG(pic2svg.py) ```bash -# 保真优先(默认参数) -python pic2svg.py images/your_image.png --turdsize 0 --opttolerance 0 --unit 1 - -# 文件更小(可能略失真) -python pic2svg.py images/your_image.png --optimize-curves --opttolerance 0.2 - -# 需要固定阈值时 -python pic2svg.py images/your_image.png --threshold 128 - -# 圆拟合简化(仅当轮廓接近圆时生效) -python pic2svg.py images/your_image.png --circle-fit 0.02 - -# 批量转换 +python pic2svg.py images/your_image.png +python pic2svg.py images/your_image.png --outdir output/ python pic2svg.py --indir images --outdir output ``` -## 🐛 常见问题 +常用参数: -**Q: 细节丢失或断裂?** -A: 降低 `--turdsize`,关闭 `--optimize-curves`,必要时指定 `--threshold`。 +- `--threshold`:固定阈值(0-255),默认 Otsu +- `--turdsize`:噪点抑制阈值 +- `--opttolerance`:曲线优化容差 +- `--unit`:坐标量化单位 +- `--optimize-curves`:启用曲线优化 +- `--circle-fit`:圆拟合误差阈值 -**Q: SVG 太大?** -A: 开启 `--optimize-curves`,或适当增大 `--opttolerance`。 +## 3. 常见问题 -**Q: 能否处理彩色图?** -A: 当前流程会转为灰度并二值化,只保留黑色区域。 +### Q1:前端看不到字体? -**Q: 圆拟合过于粗糙?** -A: 减小 `--circle-fit` 或关闭圆拟合。 +按顺序检查: -## 📄 License +1. 字体是否放在 `frontend/public/fonts/` +2. 是否执行了 `pnpm run prepare-fonts` +3. `frontend/public/fonts.json` 是否包含对应条目 -MIT License - 自由使用和修改 +### Q2:导出 PNG 失败? + +通常是浏览器内存或 SVG 内容异常导致,建议: + +1. 降低字号后重试 +2. 先导出 SVG 验证生成是否正常 +3. 分批导出而不是一次全选过多字体 + +### Q3:`pic2svg.py` 提示找不到 potrace? + +安装后确认: + +```bash +which potrace +``` diff --git a/frontend/README.md b/frontend/README.md index 33895ab..8002d82 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -1,5 +1,74 @@ -# Vue 3 + TypeScript + Vite +# frontend -This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `