@taloswork/ose-shared 0.1.10 → 0.1.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -3
- package/skills/cozy-static-svg/SKILL.md +0 -76
- package/skills/cozy-static-svg/references/approved-preview.png +0 -0
- package/skills/cozy-static-svg/references/cross-platform.md +0 -73
- package/skills/cozy-static-svg/references/moon_keeps_watch.svg +0 -229
- package/skills/cozy-static-svg/references/style-anchor.md +0 -271
- package/skills/cozy-static-svg/scripts/artkit.py +0 -321
- package/skills/cozy-static-svg/scripts/to_png.py +0 -333
|
@@ -1,333 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""SVG → PNG(跨平台:macOS / Windows / Linux 同一条命令,纯标准库 + 系统已有工具)。
|
|
3
|
-
|
|
4
|
-
仅当用户明确要 PNG 时才用,默认工作流仍是「直出原生 SVG,不转 PNG」。
|
|
5
|
-
|
|
6
|
-
用法(两个平台写法一致):
|
|
7
|
-
macOS / Linux python3 scripts/to_png.py 作品.svg
|
|
8
|
-
Windows py -3 scripts\\to_png.py 作品.svg
|
|
9
|
-
python3 scripts/to_png.py --scale 2 . # 目录下全部 SVG,2 倍图
|
|
10
|
-
py -3 scripts\\to_png.py -o out --width 2400 作品目录
|
|
11
|
-
python3 scripts/to_png.py --backend inkscape 作品.svg
|
|
12
|
-
|
|
13
|
-
后端自动探测,找到哪个用哪个(也可用 --backend 指定):
|
|
14
|
-
1. rsvg-convert 与手绘流程同源,最推荐
|
|
15
|
-
macOS: brew install librsvg | Windows: MSYS2 / scoop 自行安装
|
|
16
|
-
2. inkscape Inkscape 1.x,装了图形界面就自带命令行
|
|
17
|
-
3. cairosvg pip install cairosvg(纯 Python 依赖)
|
|
18
|
-
4. chrome / edge 系统自带浏览器无头截图,零安装兜底
|
|
19
|
-
|
|
20
|
-
其它参数:
|
|
21
|
-
-o DIR 输出目录(默认与源文件同目录)
|
|
22
|
-
--scale N 相对 SVG 原始尺寸的倍数(默认 1)
|
|
23
|
-
--width / --height 指定像素尺寸(只给一边则按比例算另一边)
|
|
24
|
-
--background RRGGBB 铺不透明底色(默认保留透明,圆角外仍是透明的)
|
|
25
|
-
"""
|
|
26
|
-
from __future__ import annotations
|
|
27
|
-
|
|
28
|
-
import argparse
|
|
29
|
-
import glob
|
|
30
|
-
import os
|
|
31
|
-
import re
|
|
32
|
-
import shutil
|
|
33
|
-
import subprocess
|
|
34
|
-
import sys
|
|
35
|
-
import tempfile
|
|
36
|
-
import xml.etree.ElementTree as ET
|
|
37
|
-
from pathlib import Path
|
|
38
|
-
|
|
39
|
-
if sys.version_info < (3, 9):
|
|
40
|
-
sys.exit(
|
|
41
|
-
"需要 Python 3.9 或更新版本(当前 %d.%d);"
|
|
42
|
-
"Windows 请用 py -3 运行,或到 python.org 安装新版。" % sys.version_info[:2]
|
|
43
|
-
)
|
|
44
|
-
|
|
45
|
-
if hasattr(sys.stdout, "reconfigure"): # Windows 控制台默认 GBK,强制 UTF-8 输出
|
|
46
|
-
try:
|
|
47
|
-
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
|
48
|
-
except (AttributeError, ValueError):
|
|
49
|
-
pass
|
|
50
|
-
|
|
51
|
-
CHROME_CANDIDATES = {
|
|
52
|
-
"darwin": [
|
|
53
|
-
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
|
54
|
-
"/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
|
|
55
|
-
"/Applications/Chromium.app/Contents/MacOS/Chromium",
|
|
56
|
-
],
|
|
57
|
-
"win32": [
|
|
58
|
-
r"%PROGRAMFILES%\Google\Chrome\Application\chrome.exe",
|
|
59
|
-
r"%PROGRAMFILES(X86)%\Google\Chrome\Application\chrome.exe",
|
|
60
|
-
r"%LOCALAPPDATA%\Google\Chrome\Application\chrome.exe",
|
|
61
|
-
r"%PROGRAMFILES(X86)%\Microsoft\Edge\Application\msedge.exe",
|
|
62
|
-
r"%PROGRAMFILES%\Microsoft\Edge\Application\msedge.exe",
|
|
63
|
-
],
|
|
64
|
-
}
|
|
65
|
-
CHROME_ON_PATH = ["chrome", "google-chrome", "chromium", "chromium-browser", "msedge"]
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
# ---------- 输入展开(与 artkit.py 同规则:文件 / 目录 / 通配符) ----------
|
|
69
|
-
|
|
70
|
-
def collect_sources(items: list[str]) -> list[Path]:
|
|
71
|
-
found: list[Path] = []
|
|
72
|
-
for item in items:
|
|
73
|
-
text = str(item)
|
|
74
|
-
path = Path(text)
|
|
75
|
-
if path.is_dir():
|
|
76
|
-
found.extend(
|
|
77
|
-
sorted(
|
|
78
|
-
p
|
|
79
|
-
for p in path.rglob("*")
|
|
80
|
-
if p.is_file() and p.suffix.lower() == ".svg"
|
|
81
|
-
)
|
|
82
|
-
)
|
|
83
|
-
continue
|
|
84
|
-
if any(ch in text for ch in "*?["):
|
|
85
|
-
found.extend(
|
|
86
|
-
sorted(
|
|
87
|
-
Path(m)
|
|
88
|
-
for m in glob.glob(text, recursive=True)
|
|
89
|
-
if Path(m).is_file() and Path(m).suffix.lower() == ".svg"
|
|
90
|
-
)
|
|
91
|
-
)
|
|
92
|
-
continue
|
|
93
|
-
found.append(path)
|
|
94
|
-
|
|
95
|
-
unique: list[Path] = []
|
|
96
|
-
seen: set[str] = set()
|
|
97
|
-
for p in found:
|
|
98
|
-
try:
|
|
99
|
-
key = str(p.resolve())
|
|
100
|
-
except OSError:
|
|
101
|
-
key = str(p)
|
|
102
|
-
if key not in seen:
|
|
103
|
-
seen.add(key)
|
|
104
|
-
unique.append(p)
|
|
105
|
-
return unique
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
# ---------- 后端探测 ----------
|
|
109
|
-
|
|
110
|
-
def _which(*names: str) -> str | None:
|
|
111
|
-
for name in names:
|
|
112
|
-
hit = shutil.which(name)
|
|
113
|
-
if hit:
|
|
114
|
-
return hit
|
|
115
|
-
return None
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
def find_chrome() -> str | None:
|
|
119
|
-
for raw in CHROME_CANDIDATES.get(sys.platform, []):
|
|
120
|
-
path = os.path.expandvars(raw)
|
|
121
|
-
if Path(path).is_file():
|
|
122
|
-
return path
|
|
123
|
-
return _which(*CHROME_ON_PATH)
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
def find_backends() -> dict[str, str]:
|
|
127
|
-
"""返回 {后端名: 可执行路径或说明};cairosvg 用 'python' 表示模块可用。"""
|
|
128
|
-
backends: dict[str, str] = {}
|
|
129
|
-
if (rsvg := _which("rsvg-convert")):
|
|
130
|
-
backends["rsvg"] = rsvg
|
|
131
|
-
if (ink := _which("inkscape")) or Path(
|
|
132
|
-
"/Applications/Inkscape.app/Contents/MacOS/inkscape"
|
|
133
|
-
).is_file():
|
|
134
|
-
backends["inkscape"] = ink or "/Applications/Inkscape.app/Contents/MacOS/inkscape"
|
|
135
|
-
try:
|
|
136
|
-
import cairosvg # noqa: F401
|
|
137
|
-
|
|
138
|
-
backends["cairosvg"] = "python-module"
|
|
139
|
-
except ImportError:
|
|
140
|
-
pass
|
|
141
|
-
if (chrome := find_chrome()):
|
|
142
|
-
backends["chrome"] = chrome
|
|
143
|
-
return backends
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
# ---------- 尺寸 ----------
|
|
147
|
-
|
|
148
|
-
def svg_size(src: Path) -> tuple[float, float] | None:
|
|
149
|
-
try:
|
|
150
|
-
root = ET.parse(src).getroot()
|
|
151
|
-
except (ET.ParseError, OSError):
|
|
152
|
-
return None
|
|
153
|
-
size = re.compile(r"^\s*(\d+(?:\.\d+)?)")
|
|
154
|
-
out = []
|
|
155
|
-
for attr in ("width", "height"):
|
|
156
|
-
match = size.match(root.get(attr) or "")
|
|
157
|
-
if not match:
|
|
158
|
-
return None
|
|
159
|
-
out.append(float(match.group(1)))
|
|
160
|
-
return out[0], out[1]
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
def target_size(src: Path, scale: float, width: float | None, height: float | None):
|
|
164
|
-
base = svg_size(src) or (1200.0, 900.0)
|
|
165
|
-
w, h = base[0] * scale, base[1] * scale
|
|
166
|
-
if width and height:
|
|
167
|
-
return width, height
|
|
168
|
-
if width:
|
|
169
|
-
return width, round(w and h * width / w)
|
|
170
|
-
if height:
|
|
171
|
-
return round(w * height / h), height
|
|
172
|
-
return round(w), round(h)
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
# ---------- 各后端转换 ----------
|
|
176
|
-
|
|
177
|
-
def convert_rsvg(exe: str, src: Path, out: Path, size, background: str | None) -> None:
|
|
178
|
-
cmd = [exe, "-w", str(size[0]), "-h", str(size[1])]
|
|
179
|
-
if background:
|
|
180
|
-
cmd += ["-b", f"#{background.lstrip('#')}"]
|
|
181
|
-
cmd += [str(src), "-o", str(out)]
|
|
182
|
-
subprocess.run(cmd, check=True, capture_output=True)
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
def convert_inkscape(exe: str, src: Path, out: Path, size, background: str | None) -> None:
|
|
186
|
-
cmd = [
|
|
187
|
-
exe,
|
|
188
|
-
"--export-type=png",
|
|
189
|
-
f"--export-filename={out}",
|
|
190
|
-
f"--export-width={size[0]}",
|
|
191
|
-
f"--export-height={size[1]}",
|
|
192
|
-
]
|
|
193
|
-
if background:
|
|
194
|
-
cmd += [f"--export-background=#{background.lstrip('#')}", "--export-background-opacity=255"]
|
|
195
|
-
cmd.append(str(src))
|
|
196
|
-
subprocess.run(cmd, check=True, capture_output=True)
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
def convert_cairosvg(src: Path, out: Path, size, background: str | None) -> None:
|
|
200
|
-
import cairosvg
|
|
201
|
-
|
|
202
|
-
cairosvg.svg2png(
|
|
203
|
-
url=str(src),
|
|
204
|
-
write_to=str(out),
|
|
205
|
-
output_width=size[0],
|
|
206
|
-
output_height=size[1],
|
|
207
|
-
background_color=f"#{background.lstrip('#')}" if background else None,
|
|
208
|
-
)
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
def convert_chrome(exe: str, src: Path, out: Path, size, background: str | None) -> None:
|
|
212
|
-
markup = src.read_text(encoding="utf-8")
|
|
213
|
-
markup = re.sub(r"<\?xml[^>]*\?>", "", markup, count=1).strip()
|
|
214
|
-
if background:
|
|
215
|
-
markup = markup.replace(
|
|
216
|
-
"<svg ", f'<svg style="background:#{background.lstrip("#")}" ', 1
|
|
217
|
-
)
|
|
218
|
-
html = (
|
|
219
|
-
"<!doctype html><html><head><meta charset=\"utf-8\"><style>"
|
|
220
|
-
"html,body{margin:0;padding:0;overflow:hidden;background:transparent}"
|
|
221
|
-
"svg{display:block;width:100vw;height:100vh}</style></head><body>"
|
|
222
|
-
+ markup
|
|
223
|
-
+ "</body></html>"
|
|
224
|
-
)
|
|
225
|
-
with tempfile.TemporaryDirectory() as tmp:
|
|
226
|
-
page = Path(tmp) / "page.html"
|
|
227
|
-
page.write_text(html, encoding="utf-8")
|
|
228
|
-
common = [
|
|
229
|
-
f"--screenshot={out}",
|
|
230
|
-
f"--window-size={size[0]},{size[1]}",
|
|
231
|
-
"--hide-scrollbars",
|
|
232
|
-
"--force-device-scale-factor=1",
|
|
233
|
-
"--virtual-time-budget=2000",
|
|
234
|
-
"--default-background-color=00000000",
|
|
235
|
-
page.resolve().as_uri(),
|
|
236
|
-
]
|
|
237
|
-
for headless in ("--headless=new", "--headless"):
|
|
238
|
-
try:
|
|
239
|
-
subprocess.run(
|
|
240
|
-
[exe, headless, *common], check=True, capture_output=True, timeout=120
|
|
241
|
-
)
|
|
242
|
-
if out.is_file():
|
|
243
|
-
return
|
|
244
|
-
except (subprocess.CalledProcessError, subprocess.TimeoutExpired):
|
|
245
|
-
continue
|
|
246
|
-
raise RuntimeError("无头浏览器没有产出截图(可换 --backend inkscape 或 rsvg)")
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
# ---------- 主流程 ----------
|
|
250
|
-
|
|
251
|
-
def main() -> int:
|
|
252
|
-
parser = argparse.ArgumentParser(
|
|
253
|
-
description="SVG → PNG(跨平台,自动选择可用后端)",
|
|
254
|
-
epilog="参数可以是文件、目录或通配符。默认保留透明底,输出与源文件同目录。",
|
|
255
|
-
)
|
|
256
|
-
parser.add_argument("svgs", nargs="+", help="待转换的 SVG(文件 / 目录 / 通配符)")
|
|
257
|
-
parser.add_argument("-o", "--outdir", type=Path, help="输出目录")
|
|
258
|
-
parser.add_argument("--scale", type=float, default=1.0, help="相对原始尺寸的倍数(默认 1)")
|
|
259
|
-
parser.add_argument("--width", type=float, help="输出宽度(像素)")
|
|
260
|
-
parser.add_argument("--height", type=float, help="输出高度(像素)")
|
|
261
|
-
parser.add_argument("--background", help="铺底色,如 FFFFFF(默认透明)")
|
|
262
|
-
parser.add_argument(
|
|
263
|
-
"--backend",
|
|
264
|
-
choices=["auto", "rsvg", "inkscape", "cairosvg", "chrome"],
|
|
265
|
-
default="auto",
|
|
266
|
-
help="指定后端(默认 auto:rsvg → inkscape → cairosvg → chrome)",
|
|
267
|
-
)
|
|
268
|
-
args = parser.parse_args()
|
|
269
|
-
|
|
270
|
-
if args.outdir:
|
|
271
|
-
args.outdir.mkdir(parents=True, exist_ok=True)
|
|
272
|
-
|
|
273
|
-
sources = collect_sources(args.svgs)
|
|
274
|
-
if not sources:
|
|
275
|
-
print("没有找到任何 .svg —— 检查路径,或用目录 / 通配符批量指定。")
|
|
276
|
-
return 1
|
|
277
|
-
|
|
278
|
-
backends = find_backends()
|
|
279
|
-
order = ["rsvg", "inkscape", "cairosvg", "chrome"]
|
|
280
|
-
if args.backend != "auto":
|
|
281
|
-
if args.backend not in backends:
|
|
282
|
-
print(f"指定的后端 {args.backend} 在本机没找到。可用:{', '.join(backends) or '无'}")
|
|
283
|
-
print(_install_hint())
|
|
284
|
-
return 1
|
|
285
|
-
chosen = args.backend
|
|
286
|
-
else:
|
|
287
|
-
chosen = next((name for name in order if name in backends), None)
|
|
288
|
-
if not chosen:
|
|
289
|
-
print("没有找到任何可用的转换后端。")
|
|
290
|
-
print(_install_hint())
|
|
291
|
-
return 1
|
|
292
|
-
|
|
293
|
-
print(f"后端:{chosen}({backends[chosen]})")
|
|
294
|
-
failed = 0
|
|
295
|
-
for index, src in enumerate(sources, 1):
|
|
296
|
-
name = src.name
|
|
297
|
-
if not src.is_file():
|
|
298
|
-
print(f"[{index}/{len(sources)}] {name} ✗ 文件不存在")
|
|
299
|
-
failed += 1
|
|
300
|
-
continue
|
|
301
|
-
size = target_size(src, args.scale, args.width, args.height)
|
|
302
|
-
out = (args.outdir or src.parent) / (src.stem + ".png")
|
|
303
|
-
try:
|
|
304
|
-
if chosen == "rsvg":
|
|
305
|
-
convert_rsvg(backends["rsvg"], src, out, size, args.background)
|
|
306
|
-
elif chosen == "inkscape":
|
|
307
|
-
convert_inkscape(backends["inkscape"], src, out, size, args.background)
|
|
308
|
-
elif chosen == "cairosvg":
|
|
309
|
-
convert_cairosvg(src, out, size, args.background)
|
|
310
|
-
else:
|
|
311
|
-
convert_chrome(backends["chrome"], src, out, size, args.background)
|
|
312
|
-
print(f"[{index}/{len(sources)}] {name} → {out.name}({size[0]:g}×{size[1]:g})")
|
|
313
|
-
except (subprocess.CalledProcessError, RuntimeError, OSError, ValueError) as exc:
|
|
314
|
-
print(f"[{index}/{len(sources)}] {name} ✗ 转换失败:{exc}")
|
|
315
|
-
failed += 1
|
|
316
|
-
|
|
317
|
-
ok = len(sources) - failed
|
|
318
|
-
print(f"总计:{ok}/{len(sources)} 张转成 PNG。")
|
|
319
|
-
return 1 if failed else 0
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
def _install_hint() -> str:
|
|
323
|
-
return (
|
|
324
|
-
"装一个就行:\n"
|
|
325
|
-
" macOS brew install librsvg (或 brew install --cask inkscape)\n"
|
|
326
|
-
" Windows choco/scoop 装 librsvg,或装 Inkscape,或 pip install cairosvg\n"
|
|
327
|
-
" —— 其实系统自带的 Edge 就能当后端,装不了工具时用 --backend chrome\n"
|
|
328
|
-
" Linux apt install librsvg2-bin / dnf install librsvg2-tools"
|
|
329
|
-
)
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
if __name__ == "__main__":
|
|
333
|
-
raise SystemExit(main())
|