commit 3aed987c75b33d226d413ea306275af245b5c833 Author: douboer Date: Fri Feb 6 11:56:47 2026 +0800 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6a4b9fe --- /dev/null +++ b/.gitignore @@ -0,0 +1,27 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +.npm-cache +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +*.ttf diff --git a/README.md b/README.md new file mode 100644 index 0000000..df47d72 --- /dev/null +++ b/README.md @@ -0,0 +1,136 @@ +# 图片转SVG工具 + +将图片中的黑色部分提取并转换为高保真 SVG,基于 `potrace` 描边以保证准确还原。 + +## 功能特点 + +- 自动提取黑色区域(Otsu 自动阈值) +- 透明通道白底合成,避免边缘污染 +- `potrace` 高精度矢量化,支持孔洞 +- 默认保真优先(关闭曲线优化) +- 支持圆拟合简化(近似圆轮廓可替代为圆弧) + +## 安装依赖 + +```bash +pip install -r requirements.txt +``` + +### 安装 potrace(必需) + +```bash +brew install potrace +``` + +### 字体转SVG依赖 + +用于 `font2svg.py`(字形轮廓输出): + +```bash +pip install fonttools uharfbuzz +``` + +## 使用方法 + +### 基本用法 + +```bash +python pic2svg.py input.png +``` + +这将在同一目录生成 `input.svg` 文件。 + +### 指定输出目录 + +```bash +python pic2svg.py input.png --outdir svg +``` + +### 常用参数示例 + +```bash +# 使用固定阈值 +python pic2svg.py input.png --threshold 128 + +# 保真优先(默认参数) +python pic2svg.py input.png --turdsize 0 --opttolerance 0 --unit 1 + +# 文件更小(可能略失真) +python pic2svg.py input.png --optimize-curves --opttolerance 0.2 + +# 圆拟合简化(仅当轮廓接近圆时生效) +python pic2svg.py input.png --circle-fit 0.02 + +# 批量转换(输入目录) +python pic2svg.py --indir images --outdir svg +``` + +### 字体转SVG(新脚本) + +```bash +python font2svg.py --font path/to/font.ttf --text "Hello" +python font2svg.py --font font/XCDUANZHUANGSONGTI.ttf --text "星程紫微" --outdir svg +python font2svg.py --font path/to/font.ttf --text "Hello" --letter-spacing 20 +python font2svg.py --fontdir font --text "星程紫微" --outdir svg +``` + +说明:单字体输出文件名根据 `--text` 自动生成;使用 `--fontdir` 时会加上字体名作为前缀。 + +## 参数说明 + +| 参数 | 说明 | 默认值 | +|------|------|--------| +| `input` | 输入图片路径(必需) | - | +| `--indir` | 输入目录(批量转换) | - | +| `--outdir` | 输出目录(自动创建,使用输入文件名.svg) | - | +| `--threshold` | 固定阈值(0-255),未设置则使用Otsu | - | +| `--turdsize` | 抑制噪点面积阈值(potrace -t) | 0 | +| `--opttolerance` | 曲线优化容差(potrace -O) | 0.0 | +| `--unit` | 坐标量化单位(potrace -u) | 1 | +| `--optimize-curves` | 启用曲线优化(更小但可能略失真) | 关闭 | +| `--circle-fit` | 圆拟合误差阈值(相对半径),>0启用圆替代 | 0.0 | + +### font2svg 参数说明 + +| 参数 | 说明 | 默认值 | +|------|------|--------| +| `--font` | 字体文件路径(ttf/otf) | - | +| `--text` | 文字内容 | - | +| `--outdir` | 输出目录(自动创建) | - | +| `--letter-spacing` | 字距(字体单位) | 0 | +| `--fontdir` | 字体目录(遍历ttf/otf) | - | + +## 批量转换 + +```bash +python pic2svg.py --indir images --outdir output +``` + +## 工作原理 + +1. 读取图像,透明通道白底合成 +2. 灰度化 + Otsu 反色二值化 +3. 若启用 `--circle-fit`,先尝试圆拟合替代 +4. 写入 PBM 位图 +5. `potrace` 描边生成 SVG(圆拟合失败时回退) + +## 故障排除 + +**细节缺失或断裂**:降低 `turdsize`,关闭 `--optimize-curves`,必要时设置更合适的 `--threshold`。 + +**文件过大**:开启 `--optimize-curves` 或适当增大 `--opttolerance`。 + +**边缘毛刺**:尝试调整 `--threshold`,或先对原图做轻微去噪。 + +**圆拟合过于粗糙**:减小 `--circle-fit` 或关闭圆拟合。 + +## 技术栈 + +- Python 3.6+ +- OpenCV / NumPy +- potrace +- fonttools / uharfbuzz + +## License + +MIT diff --git a/USAGE.md b/USAGE.md new file mode 100644 index 0000000..c74d922 --- /dev/null +++ b/USAGE.md @@ -0,0 +1,102 @@ +# 使用示例与说明 + +## 🚀 快速开始 + +### 1. 安装依赖 + +```bash +pip install -r requirements.txt +``` + +### 2. 安装 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 + +```bash +python font2svg.py --font path/to/font.ttf --text "Hello" +python font2svg.py --font path/to/font.ttf --text "你好" --outdir output +python font2svg.py --font path/to/font.ttf --text "Hello" --letter-spacing 20 +python font2svg.py --fontdir font --text "星程紫微" --outdir svg +``` + +说明:单字体输出文件名根据 `--text` 自动生成;使用 `--fontdir` 时会加上字体名作为前缀。 + +## ⚙️ 参数说明 + +- `--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。 + +## 🧩 常用配置示例 + +```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 --indir images --outdir output +``` + +## 🐛 常见问题 + +**Q: 细节丢失或断裂?** +A: 降低 `--turdsize`,关闭 `--optimize-curves`,必要时指定 `--threshold`。 + +**Q: SVG 太大?** +A: 开启 `--optimize-curves`,或适当增大 `--opttolerance`。 + +**Q: 能否处理彩色图?** +A: 当前流程会转为灰度并二值化,只保留黑色区域。 + +**Q: 圆拟合过于粗糙?** +A: 减小 `--circle-fit` 或关闭圆拟合。 + +## 📄 License + +MIT License - 自由使用和修改 diff --git a/__pycache__/pic2svg.cpython-312.pyc b/__pycache__/pic2svg.cpython-312.pyc new file mode 100644 index 0000000..1f2da46 Binary files /dev/null and b/__pycache__/pic2svg.cpython-312.pyc differ diff --git a/font2svg.py b/font2svg.py new file mode 100644 index 0000000..f61f50d --- /dev/null +++ b/font2svg.py @@ -0,0 +1,282 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +字体转SVG工具 - 字形轮廓矢量化 +目标:从字体文件中提取字形轮廓并输出SVG +""" + +import argparse +import os +import re + +HB = None +BoundsPen = None +SVGPathPen = None +TTFont = None + + +def _ensure_deps(): + global HB, BoundsPen, SVGPathPen, TTFont + if HB is not None: + return + try: + import uharfbuzz as hb # type: ignore[import-not-found] + from fontTools.pens.boundsPen import BoundsPen as BP # type: ignore[import-not-found] + from fontTools.pens.svgPathPen import SVGPathPen as SP # type: ignore[import-not-found] + from fontTools.ttLib import TTFont as FT # type: ignore[import-not-found] + except ModuleNotFoundError as e: + raise RuntimeError( + "缺少依赖,请先执行: pip install fonttools uharfbuzz" + ) from e + + HB = hb + BoundsPen = BP + SVGPathPen = SP + TTFont = FT + + +def _format_number(value): + text = f"{value:.2f}".rstrip("0").rstrip(".") + return text if text else "0" + + +def _sanitize_name(text, fallback): + safe = re.sub(r"[\\/:*?\"<>|]", "", text.strip()) + safe = re.sub(r"\s+", "_", safe) + safe = safe[:40] or fallback + return safe + + +def _default_output_name(text, font_path=None): + text_part = _sanitize_name(text, "text") + if font_path: + font_base = os.path.splitext(os.path.basename(font_path))[0] + font_part = _sanitize_name(font_base, "font") + return f"{font_part}_{text_part}.svg" + return f"{text_part}.svg" + + +def _shape_text(font_path, text): + _ensure_deps() + ttfont = TTFont(font_path) + upem = ttfont["head"].unitsPerEm + + blob = HB.Blob.from_file_path(font_path) + face = HB.Face(blob, 0) + font = HB.Font(face) + font.scale = (upem, upem) + HB.ot_font_set_funcs(font) + + buf = HB.Buffer() + buf.add_str(text) + buf.guess_segment_properties() + HB.shape(font, buf) + + return ttfont, buf.glyph_infos, buf.glyph_positions, upem + + +def _positions_scale(positions, upem): + sample = 0 + for pos in positions: + if pos.x_advance: + sample = abs(pos.x_advance) + break + if sample > upem * 4: + return 1 / 64.0 + return 1.0 + + +def _glyph_runs(ttfont, infos, positions, letter_spacing): + glyph_set = ttfont.getGlyphSet() + upem = ttfont["head"].unitsPerEm + scale = _positions_scale(positions, upem) + spacing_raw = letter_spacing / scale + + x = 0.0 + y = 0.0 + runs = [] + for info, pos in zip(infos, positions): + glyph_name = ttfont.getGlyphName(info.codepoint) + x_pos = (x + pos.x_offset) * scale + y_pos = (y + pos.y_offset) * scale + runs.append((glyph_name, x_pos, y_pos)) + x += float(pos.x_advance) + spacing_raw + y += float(pos.y_advance) + + return glyph_set, runs + + +def _compute_bounds(glyph_set, runs): + _ensure_deps() + min_x = None + min_y = None + max_x = None + max_y = None + + for glyph_name, x_pos, y_pos in runs: + glyph = glyph_set[glyph_name] + pen = BoundsPen(glyph_set) + glyph.draw(pen) + if pen.bounds is None: + continue + xmin, ymin, xmax, ymax = pen.bounds + xmin += x_pos + xmax += x_pos + ymin += y_pos + ymax += y_pos + + min_x = xmin if min_x is None else min(min_x, xmin) + min_y = ymin if min_y is None else min(min_y, ymin) + max_x = xmax if max_x is None else max(max_x, xmax) + max_y = ymax if max_y is None else max(max_y, ymax) + + return min_x, min_y, max_x, max_y + + +def _build_svg(glyph_set, runs, bounds, output_path): + _ensure_deps() + min_x, min_y, max_x, max_y = bounds + if min_x is None or min_y is None or max_x is None or max_y is None: + raise ValueError("未生成有效字形轮廓。") + + width = max_x - min_x + height = max_y - min_y + if width <= 0 or height <= 0: + raise ValueError("计算得到的SVG尺寸无效。") + + paths = [] + for glyph_name, x_pos, y_pos in runs: + glyph = glyph_set[glyph_name] + pen = SVGPathPen(glyph_set) + glyph.draw(pen) + d = pen.getCommands() + if not d: + continue + transform = ( + f"translate({_format_number(x_pos)} {_format_number(y_pos)})" + ) + paths.append(f' ') + + if not paths: + raise ValueError("未生成任何路径。") + + view_box = ( + f"{_format_number(min_x)} 0 " + f"{_format_number(width)} {_format_number(height)}" + ) + group_transform = ( + f"translate(0 {_format_number(max_y)}) scale(1 -1)" + ) + + svg_content = ( + '\n' + '\n' + f' \n' + f"{chr(10).join(paths)}\n" + " \n" + "\n" + ) + + with open(output_path, "w", encoding="utf-8") as f: + f.write(svg_content) + + +def main(): + parser = argparse.ArgumentParser( + description="将字体字形轮廓转换为SVG", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +示例: + python font2svg.py --font font.ttf --text "Hello" + python font2svg.py --font font.ttf --text "你好" --outdir output + python font2svg.py --font font.ttf --text "Hello" --letter-spacing 20 + python font2svg.py --fontdir font --text "星程紫微" --outdir svg + """, + ) + + input_group = parser.add_mutually_exclusive_group(required=True) + input_group.add_argument("--font", help="字体文件路径(ttf/otf)") + input_group.add_argument("--fontdir", help="字体目录(遍历ttf/otf)") + parser.add_argument("--text", required=True, help="要渲染的文字内容") + parser.add_argument( + "--outdir", + help="输出目录(可选,自动创建,文件名自动生成)", + ) + parser.add_argument( + "--letter-spacing", + type=float, + default=0.0, + help="字距(字体单位),默认 0", + ) + + args = parser.parse_args() + + if args.text.strip() == "": + parser.error("文字内容不能为空") + if args.font and not os.path.isfile(args.font): + parser.error(f"字体文件不存在: {args.font}") + if args.fontdir and not os.path.isdir(args.fontdir): + parser.error(f"字体目录不存在: {args.fontdir}") + + try: + _ensure_deps() + + if args.outdir: + os.makedirs(args.outdir, exist_ok=True) + + if args.fontdir: + font_files = [ + os.path.join(args.fontdir, name) + for name in sorted(os.listdir(args.fontdir)) + if os.path.splitext(name)[1].lower() in {".ttf", ".otf"} + ] + if not font_files: + print("字体目录中未找到可处理的字体文件。") + return 1 + + success = 0 + failed = 0 + for font_path in font_files: + output_name = _default_output_name(args.text, font_path=font_path) + output_path = ( + os.path.join(args.outdir, output_name) + if args.outdir + else output_name + ) + try: + ttfont, infos, positions, _ = _shape_text(font_path, args.text) + glyph_set, runs = _glyph_runs( + ttfont, infos, positions, args.letter_spacing + ) + bounds = _compute_bounds(glyph_set, runs) + _build_svg(glyph_set, runs, bounds, output_path) + print(f"SVG文件已保存: {output_path}") + success += 1 + except Exception as e: + print(f"转换失败: {font_path} -> {e}") + failed += 1 + + print(f"批量转换完成: 成功 {success} 个,失败 {failed} 个") + return 0 if success > 0 and failed == 0 else 1 + + output_name = _default_output_name(args.text) + output_path = ( + os.path.join(args.outdir, output_name) if args.outdir else output_name + ) + ttfont, infos, positions, _ = _shape_text(args.font, args.text) + glyph_set, runs = _glyph_runs(ttfont, infos, positions, args.letter_spacing) + bounds = _compute_bounds(glyph_set, runs) + _build_svg(glyph_set, runs, bounds, output_path) + except Exception as e: + print(f"转换失败: {e}") + return 1 + + print(f"SVG文件已保存: {output_path}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/images/iShot_2026-02-05_16.38.29.png b/images/iShot_2026-02-05_16.38.29.png new file mode 100644 index 0000000..3651d5c Binary files /dev/null and b/images/iShot_2026-02-05_16.38.29.png differ diff --git a/images/iShot_2026-02-05_16.38.49.png b/images/iShot_2026-02-05_16.38.49.png new file mode 100644 index 0000000..da14564 Binary files /dev/null and b/images/iShot_2026-02-05_16.38.49.png differ diff --git a/images/iShot_2026-02-05_20.17.05.png b/images/iShot_2026-02-05_20.17.05.png new file mode 100644 index 0000000..d144908 Binary files /dev/null and b/images/iShot_2026-02-05_20.17.05.png differ diff --git a/images/iShot_2026-02-05_20.22.17.png b/images/iShot_2026-02-05_20.22.17.png new file mode 100644 index 0000000..37f39ed Binary files /dev/null and b/images/iShot_2026-02-05_20.22.17.png differ diff --git a/pic2svg.py b/pic2svg.py new file mode 100644 index 0000000..9820745 --- /dev/null +++ b/pic2svg.py @@ -0,0 +1,379 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +图片转SVG工具 - 基于 potrace 的高保真描边 +目标:准确还原图片中的黑色部分 +""" + +import argparse +import os +import shutil +import subprocess +import tempfile + +import cv2 +import numpy as np + + +class ImageToSVG: + """图片转SVG转换器 - 使用 potrace 进行高精度矢量化""" + + def __init__( + self, + threshold=None, + turdsize=0, + opttolerance=0, + unit=1, + optimize_curves=False, + circle_fit=0.0, + ): + """ + 初始化转换器 + + Args: + threshold: 固定阈值(0-255),None 表示使用 Otsu 自动阈值 + turdsize: 抑制噪点的面积阈值(potrace -t) + opttolerance: 曲线优化容差(potrace -O) + unit: 坐标量化单位(potrace -u,1 表示不量化) + optimize_curves: 是否开启曲线优化(文件更小但可能失真) + circle_fit: 圆拟合误差阈值(相对半径),>0启用圆替代 + """ + self.threshold = threshold + self.turdsize = turdsize + self.opttolerance = opttolerance + self.unit = unit + self.optimize_curves = optimize_curves + self.circle_fit = circle_fit + + @staticmethod + def _iter_input_files(input_dir): + exts = {".png", ".jpg", ".jpeg", ".bmp", ".tif", ".tiff"} + for name in sorted(os.listdir(input_dir)): + path = os.path.join(input_dir, name) + if not os.path.isfile(path): + continue + if os.path.splitext(name)[1].lower() in exts: + yield path + + @staticmethod + def _ensure_potrace(): + """确保 potrace 可用""" + if shutil.which("potrace") is None: + raise RuntimeError("未找到 potrace,请先安装:brew install potrace") + + @staticmethod + def _load_gray(image_path): + """加载图片并转换为灰度图(透明通道会按白底合成)""" + img = cv2.imread(image_path, cv2.IMREAD_UNCHANGED) + if img is None: + raise ValueError(f"无法读取图片: {image_path}") + + if img.ndim == 3 and img.shape[2] == 4: + bgr = img[:, :, :3].astype(np.float32) + alpha = img[:, :, 3:4].astype(np.float32) / 255.0 + bgr = bgr * alpha + 255.0 * (1.0 - alpha) + gray = cv2.cvtColor(bgr.astype(np.uint8), cv2.COLOR_BGR2GRAY) + elif img.ndim == 3: + gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) + else: + gray = img + + return gray + + def _binarize(self, gray): + """二值化:黑色为前景""" + if self.threshold is None: + _, binary = cv2.threshold( + gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU + ) + else: + _, binary = cv2.threshold(gray, self.threshold, 255, cv2.THRESH_BINARY_INV) + + return (binary > 0).astype(np.uint8) + + @staticmethod + def _write_pbm(mask, path): + """写入 ASCII PBM(P1)""" + height, width = mask.shape + with open(path, "w", encoding="ascii") as f: + f.write("P1\n") + f.write(f"{width} {height}\n") + for y in range(height): + row = mask[y] + f.write(" ".join("1" if v else "0" for v in row)) + f.write("\n") + + def _run_potrace(self, pbm_path, output_path): + """调用 potrace 生成 SVG""" + cmd = [ + "potrace", + str(pbm_path), + "-s", + "-o", + str(output_path), + "-u", + str(self.unit), + "-t", + str(self.turdsize), + "-O", + str(self.opttolerance), + ] + if not self.optimize_curves: + cmd.append("-n") + subprocess.run(cmd, check=True) + + @staticmethod + def _format_number(value): + text = f"{value:.2f}".rstrip("0").rstrip(".") + return text if text else "0" + + def _circle_path(self, cx, cy, radius): + """将圆转换为SVG路径(由两段圆弧组成)""" + cy_s = self._format_number(cy) + r_s = self._format_number(radius) + start_x = self._format_number(cx + radius) + end_x = self._format_number(cx - radius) + return ( + f"M {start_x} {cy_s} " + f"A {r_s} {r_s} 0 1 0 {end_x} {cy_s} " + f"A {r_s} {r_s} 0 1 0 {start_x} {cy_s} Z" + ) + + @staticmethod + def _fit_circle(contour): + """拟合圆并返回最大相对误差""" + (cx, cy), radius = cv2.minEnclosingCircle(contour) + if radius <= 0: + return None + points = contour.reshape(-1, 2).astype(np.float32) + dx = points[:, 0] - cx + dy = points[:, 1] - cy + dist = np.sqrt(dx * dx + dy * dy) + rel_error = np.abs(dist - radius) / radius + max_rel_error = float(rel_error.max()) + return cx, cy, radius, max_rel_error + + @staticmethod + def _collect_component_indices(hierarchy, start_idx): + indices = [] + stack = [start_idx] + while stack: + idx = stack.pop() + indices.append(idx) + child = hierarchy[idx][2] + while child != -1: + stack.append(child) + child = hierarchy[child][0] + return indices + + def _try_circle_svg(self, mask, output_path): + if self.circle_fit <= 0: + return False + + contours, hierarchy = cv2.findContours( + mask, cv2.RETR_TREE, cv2.CHAIN_APPROX_NONE + ) + if not contours or hierarchy is None: + return False + + hierarchy = hierarchy[0] + paths = [] + + for idx, info in enumerate(hierarchy): + if info[3] != -1: + continue + + parent_area = cv2.contourArea(contours[idx]) + if parent_area <= self.turdsize: + continue + + component_indices = self._collect_component_indices(hierarchy, idx) + component_paths = [] + + for contour_idx in component_indices: + contour = contours[contour_idx] + result = self._fit_circle(contour) + if result is None: + return False + cx, cy, radius, max_rel_error = result + if max_rel_error > self.circle_fit: + return False + component_paths.append(self._circle_path(cx, cy, radius)) + + if component_paths: + paths.append( + f' ' + ) + + if not paths: + return False + + height, width = mask.shape + svg_content = ( + '\n' + '\n' + f"{chr(10).join(paths)}\n" + "\n" + ) + + with open(output_path, "w", encoding="utf-8") as f: + f.write(svg_content) + + return True + + def convert_to_svg(self, image_path, output_path=None): + """ + 将图片转换为SVG(高保真) + + Args: + image_path: 输入图片路径 + output_path: 输出SVG路径 + """ + gray = self._load_gray(image_path) + mask = self._binarize(gray) + + if mask.sum() == 0: + print("未检测到黑色区域,跳过。") + return None + + if output_path is None: + base_name = os.path.splitext(image_path)[0] + output_path = base_name + ".svg" + else: + os.makedirs(os.path.dirname(output_path) or ".", exist_ok=True) + + if self._try_circle_svg(mask, output_path): + file_size = os.path.getsize(output_path) + print(f"SVG文件已保存: {output_path}") + print(f"文件大小: {file_size} 字节 ({file_size/1024:.1f} KB)") + return output_path + + self._ensure_potrace() + with tempfile.TemporaryDirectory() as tmp_dir: + pbm_path = os.path.join(tmp_dir, "input.pbm") + self._write_pbm(mask, pbm_path) + self._run_potrace(pbm_path, output_path) + + file_size = os.path.getsize(output_path) + print(f"SVG文件已保存: {output_path}") + print(f"文件大小: {file_size} 字节 ({file_size/1024:.1f} KB)") + return output_path + + +def main(): + """主函数""" + parser = argparse.ArgumentParser( + description="将图片转换为高保真SVG(使用potrace描边)", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +示例: + python pic2svg.py input.png + python pic2svg.py --indir images + python pic2svg.py --indir images --outdir output + python pic2svg.py input.png --outdir output + python pic2svg.py input.png --turdsize 0 --opttolerance 0 --unit 1 + """, + ) + + parser.add_argument("input", nargs="?", help="输入图片路径(可选,与 --indir 互斥)") + parser.add_argument("--indir", help="输入目录(批量转换,与 input 互斥)") + parser.add_argument( + "--outdir", + help="输出目录(自动创建,使用输入文件名.svg)", + ) + parser.add_argument( + "--threshold", + type=int, + default=None, + help="固定阈值(0-255),默认使用Otsu自动阈值", + ) + parser.add_argument( + "--turdsize", + type=int, + default=0, + help="抑制噪点面积阈值(potrace -t,默认 0)", + ) + parser.add_argument( + "--opttolerance", + type=float, + default=0.0, + help="曲线优化容差(potrace -O,默认 0)", + ) + parser.add_argument( + "--unit", + type=int, + default=1, + help="坐标量化单位(potrace -u,默认 1)", + ) + parser.add_argument( + "--optimize-curves", + action="store_true", + help="启用曲线优化(文件更小但可能略失真)", + ) + parser.add_argument( + "--circle-fit", + type=float, + default=0.0, + help="圆拟合误差阈值(相对半径),>0启用圆替代,如 0.02", + ) + + args = parser.parse_args() + + try: + if args.input is None and args.indir is None: + parser.error("必须提供输入文件或 --indir") + if args.input and args.indir: + parser.error("input 与 --indir 互斥,请只指定一个") + if args.indir and not os.path.isdir(args.indir): + parser.error(f"输入目录不存在: {args.indir}") + + converter = ImageToSVG( + threshold=args.threshold, + turdsize=args.turdsize, + opttolerance=args.opttolerance, + unit=args.unit, + optimize_curves=args.optimize_curves, + circle_fit=args.circle_fit, + ) + + if args.indir: + outdir = args.outdir + if outdir: + os.makedirs(outdir, exist_ok=True) + files = list(converter._iter_input_files(args.indir)) + if not files: + print("输入目录中未找到可处理的图片文件。") + return 1 + success = 0 + failed = 0 + for path in files: + output_path = None + if outdir: + base_name = os.path.splitext(os.path.basename(path))[0] + output_path = os.path.join(outdir, base_name + ".svg") + try: + converter.convert_to_svg(path, output_path) + success += 1 + except Exception as e: + failed += 1 + print(f"转换失败: {path} -> {e}") + print(f"批量转换完成: 成功 {success} 个,失败 {failed} 个") + else: + output_path = None + if args.outdir: + os.makedirs(args.outdir, exist_ok=True) + base_name = os.path.splitext(os.path.basename(args.input))[0] + output_path = os.path.join(args.outdir, base_name + ".svg") + converter.convert_to_svg(args.input, output_path) + except Exception as e: + print(f"转换失败: {e}") + return 1 + + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..6ceb4d1 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +opencv-python>=4.5.0 +numpy>=1.19.0 +fonttools>=4.0.0 +uharfbuzz>=0.34.0 diff --git a/run.sh b/run.sh new file mode 100755 index 0000000..29e3611 --- /dev/null +++ b/run.sh @@ -0,0 +1,4 @@ + +source .venv/bin/activate + +python font2svg.py --fontdir font --text "星程紫微" --outdir svg diff --git a/svg/AlimamaDaoLiTi_星程字体转换.svg b/svg/AlimamaDaoLiTi_星程字体转换.svg new file mode 100644 index 0000000..2a6f92d --- /dev/null +++ b/svg/AlimamaDaoLiTi_星程字体转换.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/svg/AlimamaDaoLiTi_星程紫微.svg b/svg/AlimamaDaoLiTi_星程紫微.svg new file mode 100644 index 0000000..21da67c --- /dev/null +++ b/svg/AlimamaDaoLiTi_星程紫微.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/svg/Hangeuljaemin4-Regular_星程字体转换.svg b/svg/Hangeuljaemin4-Regular_星程字体转换.svg new file mode 100644 index 0000000..c9c3a89 --- /dev/null +++ b/svg/Hangeuljaemin4-Regular_星程字体转换.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/svg/Hangeuljaemin4-Regular_星程紫微.svg b/svg/Hangeuljaemin4-Regular_星程紫微.svg new file mode 100644 index 0000000..531dae1 --- /dev/null +++ b/svg/Hangeuljaemin4-Regular_星程紫微.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/svg/I.顏體_星程字体转换.svg b/svg/I.顏體_星程字体转换.svg new file mode 100644 index 0000000..59acee8 --- /dev/null +++ b/svg/I.顏體_星程字体转换.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/svg/I.顏體_星程紫微.svg b/svg/I.顏體_星程紫微.svg new file mode 100644 index 0000000..c4f4b79 --- /dev/null +++ b/svg/I.顏體_星程紫微.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/svg/XCDUANZHUANGSONGTI_星程字体转换.svg b/svg/XCDUANZHUANGSONGTI_星程字体转换.svg new file mode 100644 index 0000000..8082cd2 --- /dev/null +++ b/svg/XCDUANZHUANGSONGTI_星程字体转换.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/svg/XCDUANZHUANGSONGTI_星程紫微.svg b/svg/XCDUANZHUANGSONGTI_星程紫微.svg new file mode 100644 index 0000000..3e864db --- /dev/null +++ b/svg/XCDUANZHUANGSONGTI_星程紫微.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/svg/aoyagireisyosimo_ttf_2_01_星程字体转换.svg b/svg/aoyagireisyosimo_ttf_2_01_星程字体转换.svg new file mode 100644 index 0000000..61ead1f --- /dev/null +++ b/svg/aoyagireisyosimo_ttf_2_01_星程字体转换.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/svg/aoyagireisyosimo_ttf_2_01_星程紫微.svg b/svg/aoyagireisyosimo_ttf_2_01_星程紫微.svg new file mode 100644 index 0000000..0f99114 --- /dev/null +++ b/svg/aoyagireisyosimo_ttf_2_01_星程紫微.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/svg/qiji-combo_星程字体转换.svg b/svg/qiji-combo_星程字体转换.svg new file mode 100644 index 0000000..18e15f0 --- /dev/null +++ b/svg/qiji-combo_星程字体转换.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/svg/qiji-combo_星程紫微.svg b/svg/qiji-combo_星程紫微.svg new file mode 100644 index 0000000..2ae6f65 --- /dev/null +++ b/svg/qiji-combo_星程紫微.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/svg/临海隶书_星程字体转换.svg b/svg/临海隶书_星程字体转换.svg new file mode 100644 index 0000000..df1b636 --- /dev/null +++ b/svg/临海隶书_星程字体转换.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/svg/临海隶书_星程紫微.svg b/svg/临海隶书_星程紫微.svg new file mode 100644 index 0000000..822c037 --- /dev/null +++ b/svg/临海隶书_星程紫微.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/svg/京華老宋体_KingHwa_OldSong_星程字体转换.svg b/svg/京華老宋体_KingHwa_OldSong_星程字体转换.svg new file mode 100644 index 0000000..4b131ab --- /dev/null +++ b/svg/京華老宋体_KingHwa_OldSong_星程字体转换.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/svg/京華老宋体_KingHwa_OldSong_星程紫微.svg b/svg/京華老宋体_KingHwa_OldSong_星程紫微.svg new file mode 100644 index 0000000..2871dac --- /dev/null +++ b/svg/京華老宋体_KingHwa_OldSong_星程紫微.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/svg/优设标题黑_星程字体转换.svg b/svg/优设标题黑_星程字体转换.svg new file mode 100644 index 0000000..d9efc0c --- /dev/null +++ b/svg/优设标题黑_星程字体转换.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/svg/优设标题黑_星程紫微.svg b/svg/优设标题黑_星程紫微.svg new file mode 100644 index 0000000..89d4bd0 --- /dev/null +++ b/svg/优设标题黑_星程紫微.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/svg/包图小白体_星程字体转换.svg b/svg/包图小白体_星程字体转换.svg new file mode 100644 index 0000000..3cb1a33 --- /dev/null +++ b/svg/包图小白体_星程字体转换.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/svg/包图小白体_星程紫微.svg b/svg/包图小白体_星程紫微.svg new file mode 100644 index 0000000..8fc1e4a --- /dev/null +++ b/svg/包图小白体_星程紫微.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/svg/庞门正道标题体_星程字体转换.svg b/svg/庞门正道标题体_星程字体转换.svg new file mode 100644 index 0000000..7333772 --- /dev/null +++ b/svg/庞门正道标题体_星程字体转换.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/svg/庞门正道标题体_星程紫微.svg b/svg/庞门正道标题体_星程紫微.svg new file mode 100644 index 0000000..8e9f61a --- /dev/null +++ b/svg/庞门正道标题体_星程紫微.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/svg/源界明朝_星程字体转换.svg b/svg/源界明朝_星程字体转换.svg new file mode 100644 index 0000000..12c23ff --- /dev/null +++ b/svg/源界明朝_星程字体转换.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/svg/源界明朝_星程紫微.svg b/svg/源界明朝_星程紫微.svg new file mode 100644 index 0000000..dadd290 --- /dev/null +++ b/svg/源界明朝_星程紫微.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/svg/演示佛系体_星程字体转换.svg b/svg/演示佛系体_星程字体转换.svg new file mode 100644 index 0000000..9e5f85a --- /dev/null +++ b/svg/演示佛系体_星程字体转换.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/svg/演示佛系体_星程紫微.svg b/svg/演示佛系体_星程紫微.svg new file mode 100644 index 0000000..ebba759 --- /dev/null +++ b/svg/演示佛系体_星程紫微.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/svg/王漢宗勘亭流繁_星程字体转换.svg b/svg/王漢宗勘亭流繁_星程字体转换.svg new file mode 100644 index 0000000..145c5a8 --- /dev/null +++ b/svg/王漢宗勘亭流繁_星程字体转换.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/svg/王漢宗勘亭流繁_星程紫微.svg b/svg/王漢宗勘亭流繁_星程紫微.svg new file mode 100644 index 0000000..85d0eb9 --- /dev/null +++ b/svg/王漢宗勘亭流繁_星程紫微.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/svg/王漢宗新潮體_星程字体转换.svg b/svg/王漢宗新潮體_星程字体转换.svg new file mode 100644 index 0000000..05b1732 --- /dev/null +++ b/svg/王漢宗新潮體_星程字体转换.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/svg/王漢宗新潮體_星程紫微.svg b/svg/王漢宗新潮體_星程紫微.svg new file mode 100644 index 0000000..fe380dc --- /dev/null +++ b/svg/王漢宗新潮體_星程紫微.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/svg/王漢宗波卡體空陰_星程字体转换.svg b/svg/王漢宗波卡體空陰_星程字体转换.svg new file mode 100644 index 0000000..195a307 --- /dev/null +++ b/svg/王漢宗波卡體空陰_星程字体转换.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/svg/王漢宗波卡體空陰_星程紫微.svg b/svg/王漢宗波卡體空陰_星程紫微.svg new file mode 100644 index 0000000..31c4d12 --- /dev/null +++ b/svg/王漢宗波卡體空陰_星程紫微.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/svg/王漢宗細黑體繁_星程字体转换.svg b/svg/王漢宗細黑體繁_星程字体转换.svg new file mode 100644 index 0000000..e5ec310 --- /dev/null +++ b/svg/王漢宗細黑體繁_星程字体转换.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/svg/王漢宗細黑體繁_星程紫微.svg b/svg/王漢宗細黑體繁_星程紫微.svg new file mode 100644 index 0000000..77eda79 --- /dev/null +++ b/svg/王漢宗細黑體繁_星程紫微.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/svg/王漢宗綜藝體雙空陰_星程字体转换.svg b/svg/王漢宗綜藝體雙空陰_星程字体转换.svg new file mode 100644 index 0000000..e3bdd8e --- /dev/null +++ b/svg/王漢宗綜藝體雙空陰_星程字体转换.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/svg/王漢宗綜藝體雙空陰_星程紫微.svg b/svg/王漢宗綜藝體雙空陰_星程紫微.svg new file mode 100644 index 0000000..9c8385d --- /dev/null +++ b/svg/王漢宗綜藝體雙空陰_星程紫微.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/svg/王漢宗超明體繁_星程字体转换.svg b/svg/王漢宗超明體繁_星程字体转换.svg new file mode 100644 index 0000000..3490e5d --- /dev/null +++ b/svg/王漢宗超明體繁_星程字体转换.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/svg/王漢宗超明體繁_星程紫微.svg b/svg/王漢宗超明體繁_星程紫微.svg new file mode 100644 index 0000000..b6b9a42 --- /dev/null +++ b/svg/王漢宗超明體繁_星程紫微.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/svg/王漢宗酷儷海報_星程字体转换.svg b/svg/王漢宗酷儷海報_星程字体转换.svg new file mode 100644 index 0000000..5665972 --- /dev/null +++ b/svg/王漢宗酷儷海報_星程字体转换.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/svg/王漢宗酷儷海報_星程紫微.svg b/svg/王漢宗酷儷海報_星程紫微.svg new file mode 100644 index 0000000..a8e1878 --- /dev/null +++ b/svg/王漢宗酷儷海報_星程紫微.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/svg/王漢宗顏楷體繁_星程字体转换.svg b/svg/王漢宗顏楷體繁_星程字体转换.svg new file mode 100644 index 0000000..59acee8 --- /dev/null +++ b/svg/王漢宗顏楷體繁_星程字体转换.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/svg/王漢宗顏楷體繁_星程紫微.svg b/svg/王漢宗顏楷體繁_星程紫微.svg new file mode 100644 index 0000000..c4f4b79 --- /dev/null +++ b/svg/王漢宗顏楷體繁_星程紫微.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/svg/站酷快乐体_星程字体转换.svg b/svg/站酷快乐体_星程字体转换.svg new file mode 100644 index 0000000..65720fc --- /dev/null +++ b/svg/站酷快乐体_星程字体转换.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/svg/站酷快乐体_星程紫微.svg b/svg/站酷快乐体_星程紫微.svg new file mode 100644 index 0000000..dfcb93c --- /dev/null +++ b/svg/站酷快乐体_星程紫微.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/svg/问藏书房_星程字体转换.svg b/svg/问藏书房_星程字体转换.svg new file mode 100644 index 0000000..c21d3c9 --- /dev/null +++ b/svg/问藏书房_星程字体转换.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/svg/问藏书房_星程紫微.svg b/svg/问藏书房_星程紫微.svg new file mode 100644 index 0000000..0803748 --- /dev/null +++ b/svg/问藏书房_星程紫微.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/svg/霞鹜臻楷_星程字体转换.svg b/svg/霞鹜臻楷_星程字体转换.svg new file mode 100644 index 0000000..37cdaec --- /dev/null +++ b/svg/霞鹜臻楷_星程字体转换.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/svg/霞鹜臻楷_星程紫微.svg b/svg/霞鹜臻楷_星程紫微.svg new file mode 100644 index 0000000..46b9a73 --- /dev/null +++ b/svg/霞鹜臻楷_星程紫微.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/t/10款高级感中文字体/procteate安装字体.mp4 b/t/10款高级感中文字体/procteate安装字体.mp4 new file mode 100644 index 0000000..4b3bbb0 Binary files /dev/null and b/t/10款高级感中文字体/procteate安装字体.mp4 differ diff --git a/t/10款高级感中文字体/不可居無竹/韩契在民体 한글재민체/Hangeuljaemin4-Regular.otf b/t/10款高级感中文字体/不可居無竹/韩契在民体 한글재민체/Hangeuljaemin4-Regular.otf new file mode 100644 index 0000000..c1ad721 Binary files /dev/null and b/t/10款高级感中文字体/不可居無竹/韩契在民体 한글재민체/Hangeuljaemin4-Regular.otf differ diff --git a/t/10款高级感中文字体/安装后字体显示名称预览/安装后字体显示名称预览.jpg b/t/10款高级感中文字体/安装后字体显示名称预览/安装后字体显示名称预览.jpg new file mode 100644 index 0000000..0f7e7e9 Binary files /dev/null and b/t/10款高级感中文字体/安装后字体显示名称预览/安装后字体显示名称预览.jpg differ diff --git a/t/10款高级感中文字体/忆从前/Nom Na Tong 喃那宋/NomNaTong-Regular.otf b/t/10款高级感中文字体/忆从前/Nom Na Tong 喃那宋/NomNaTong-Regular.otf new file mode 100644 index 0000000..f8f57ed Binary files /dev/null and b/t/10款高级感中文字体/忆从前/Nom Na Tong 喃那宋/NomNaTong-Regular.otf differ diff --git a/t/10款高级感中文字体/电脑字体安装方法.png b/t/10款高级感中文字体/电脑字体安装方法.png new file mode 100644 index 0000000..d1862fd Binary files /dev/null and b/t/10款高级感中文字体/电脑字体安装方法.png differ diff --git a/t/10款高级感中文字体/造物/阿里妈妈刀隶体-v1.000/AlimamaDaoLiTi.otf b/t/10款高级感中文字体/造物/阿里妈妈刀隶体-v1.000/AlimamaDaoLiTi.otf new file mode 100644 index 0000000..207a47c Binary files /dev/null and b/t/10款高级感中文字体/造物/阿里妈妈刀隶体-v1.000/AlimamaDaoLiTi.otf differ