diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
new file mode 100644
index 0000000..298ff3d
--- /dev/null
+++ b/.github/workflows/build.yml
@@ -0,0 +1,55 @@
+name: Build
+
+on:
+ push:
+ branches: [main]
+ paths:
+ - "inbox/**"
+ - "scripts/**"
+ - ".github/workflows/build.yml"
+ workflow_dispatch:
+
+permissions:
+ contents: write
+
+concurrency:
+ group: build
+ cancel-in-progress: false
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ steps:
+ - name: 检出仓库
+ uses: actions/checkout@v4
+
+ - name: 安装 Python
+ uses: actions/setup-python@v5
+ with:
+ python-version: "3.12"
+
+ - name: 安装依赖
+ run: |
+ pip install Pillow
+ if ! command -v rsvg-convert >/dev/null 2>&1; then
+ sudo apt-get update -qq
+ sudo apt-get install -y -qq librsvg2-bin
+ fi
+
+ - name: 构建
+ id: build
+ env:
+ TINYPNG_KEY: ${{ secrets.TINYPNG_KEY }}
+ run: python scripts/build.py --repo .
+
+ - name: 提交并推送
+ run: |
+ git config user.name "github-actions[bot]"
+ git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
+ git add -A
+ if git diff --cached --quiet; then
+ echo "没有需要提交的变更"
+ else
+ git commit -m "ci: 自动构建(新增 ${{ steps.build.outputs.added }} 个图标)"
+ git push
+ fi
diff --git a/README.md b/README.md
index 7fe080e..69f6ad0 100644
--- a/README.md
+++ b/README.md
@@ -1,72 +1,120 @@
-

-
HD-Icons:高清仪表盘图标
-
1024×1024 超清分辨率 · 无损压缩优化 · 多种样式适配
+

+
+# HD-Icons
+
+**为仪表盘而生的高清图标库**
+
+1024×1024 超清导出 · TinyPNG 无损压缩 · 圆角矩形 / 圆形 / SVG 三种风格
+
+[](https://github.com/xushier/HD-Icons/stargazers)
+[](https://github.com/xushier/HD-Icons/blob/master/LICENSE)
+[](https://github.com/xushier/HD-Icons/commits/main)
+[](https://www.jsdelivr.com/package/gh/xushier/HD-Icons)
+
+**共 1754 个图标,持续更新中**
+---
+## ✨ 特性
+
+- **超清分辨率** — 所有 PNG 均为 1024×1024,任意缩放依旧锐利
+- **无损压缩** — 每张图标经 TinyPNG 压缩,体积更小、画质不变
+- **三种风格** — 圆角矩形、圆形(PNG)与矢量(SVG),命名一一对应
+- **自动构建** — 新图标入库后,索引、预览图、数量统计全部自动更新
+
+## 📊 图标统计
+
+| 风格 | 目录 | 数量 |
+|------|------|------|
+| 📦 圆角矩形 | `border-radius/` | 1083 |
+| ⭕ 圆形 | `circle/` | 124 |
+| 🎨 SVG 矢量 | `svg/` | 547 |
+
+## 🚀 使用
+
+### 方式一:Docker 图标站(推荐)
+
+部署 [HD-Icons-docker](https://github.com/xushier/HD-Icons-docker),自动同步本仓库更新,支持一键复制、移动端适配与自定义上传。
+
+| 日间模式 | 夜间模式 |
+|----------|----------|
+|
|
|
+
+
+📱 移动端适配 / 一键复制 / 自定义上传
+
+| 移动端自适应 | 一键复制地址 | 自定义上传 |
+|----------|----------|----------|
+|
|
|
|
+
+
+
+### 方式二:直链引用
+
+GitHub Raw(可直连 GitHub 时使用):
+
+```
+https://raw.githubusercontent.com/xushier/HD-Icons/main/border-radius/图标名.png
+```
+
+jsDelivr CDN(无法直连 GitHub 或需要加速时使用):
+
+```
+https://cdn.jsdelivr.net/gh/xushier/HD-Icons/border-radius/图标名.png
+```
+
+把 `图标名` 替换为实际文件名(可在 [icons.json](icons.json) 或下方预览图中查找),目录按需替换为 `border-radius`、`circle` 或 `svg`。
+
+### 方式三:App 图标包
+
+Yamby、Hills 等安卓应用可直接订阅图标索引:
+
+```
+https://raw.githubusercontent.com/xushier/HD-Icons/main/icons.json
+```
+
+> 如无响应,请开启代理环境,或使用 [HD-Icons-docker](https://github.com/xushier/HD-Icons-docker) 自行部署。
+
+## 🖼️ 图标预览
+
+### 📦 圆角矩形(1083)
-
- [](https://github.com/xushier/HD-Icons/stargazers)
- [](https://github.com/xushier/HD-Icons/network)
- [](https://github.com/xushier/HD-Icons/issues)
- [](https://github.com/xushier/HD-Icons/blob/master/LICENSE)
-
-
- [](https://github.com/xushier/HD-Icons/releases)
- [](https://github.com/xushier/HD-Icons/commits/main)
- [](https://github.com/xushier/HD-Icons)
-
-
- [](https://www.jsdelivr.com/package/gh/xushier/HD-Icons)
-
+
+### ⭕ 圆形(124)
+
-
-
-
-
-
-
-

-
-
+
+### 🎨 SVG 矢量(547)
-
+
+点击展开 SVG 图标预览
+
+

+
+
-## 📝 项目简介
+## 🎯 请求与贡献图标
-> 🎯 **使命**:为 Unraid Docker 容器和各类仪表盘应用提供高质量、标准化的图标解决方案
-
-### ✨ 核心特性
-
-| 特性 | 描述 |
-|------|-----|
-| 🖼️ **超清分辨率** | 1024×1024 像素,确保在任何尺寸下都清晰锐利 |
-| 🗜️ **无损压缩** | 所有图标经过无损压缩,体积更小,质量不变 |
-| 🎨 **优化显示** | 图标主体适当放大,满足各种使用场景 |
-| 📁 **多样样式** | 提供 `border-radius`(圆角矩形)、 `circle`(圆形)和 `svg`(矢量)三种风格 |
-| 🔄 **持续更新** | 不定期添加新图标,满足不断增长的需求 |
-
-### 🎯 适用场景
-
-本项目最初为 `Unraid Docker` 容器图标展示而设计,现已扩展支持多种仪表盘应用、部分APP及其他用途:
+- 缺少你需要的图标?[提交 Issue](https://github.com/xushier/HD-Icons/issues/new/choose) 即可
+- 想贡献图标?Fork 本仓库,把做好的图标(1024×1024 PNG 或 SVG)放进 `inbox/` 对应子目录,提交 PR,压缩入库、索引更新、预览图生成全部自动完成
+- 命名规范:小写英文单词 + 连字符 + 序号,如 `google-map-1.png`
+
+📖 适用场景
+本项目最初为 Unraid Docker 容器图标展示而设计,现已扩展支持多种仪表盘应用、部分 App 及其他用途:
| 应用程序 | 描述 | 链接 | 备注 |
|----------|------|------|------|
| **FlatNas** | 新晋现代化仪表盘 | [🔗](https://github.com/Garry-QD/FlatNas) | 开源免费 |
-| **Sun-Panel** | 适合国人体质的现代化仪表盘 | [🔗](https://github.com/hslr-s/sun-panel) | 1.3.0及之前版本开源,新版Pro授权99永久 |
+| **Sun-Panel** | 适合国人体质的现代化仪表盘 | [🔗](https://github.com/hslr-s/sun-panel) | 1.3.0 及之前版本开源,新版 Pro 授权 99 永久 |
| **Dashy** | 现代化仪表盘 | [🔗](https://github.com/Lissy93/dashy) | 开源免费 |
| **Homer Dashboard** | 简洁的静态主页 | [🔗](https://github.com/bastienwirtz/homer) | 开源免费 |
| **Heimdall** | 应用程序仪表盘 | [🔗](https://github.com/linuxserver/Heimdall) | 开源免费 |
@@ -74,11 +122,10 @@
| **Flame** | 自托管仪表盘 | [🔗](https://github.com/pawelmalak/flame) | 开源免费 |
| **SUI** | 简单用户界面 | [🔗](https://github.com/jeroenpardon/sui) | 开源免费 |
+
-
-### 🌟 社区支持
-
-
+
+💬 社区
| 平台 | 账号/群组 | 链接 |
|------|-----------|------|
@@ -87,136 +134,41 @@
| **QQ群** | 647605169 | 💬 加入讨论 |
| **微信群** | dxyxddsbds(备注加群) | 📱 添加微信入群 |
-
-
-> 💡 **需要新图标?** [点击这里提交请求](https://github.com/xushier/HD-Icons/issues/new/choose) 🎯
-
-## 📖 使用说明(三种方式)
-
-### 🐳 Docker 部署图标展示容器使用(强烈推荐)
-
-> 🚀 **项目地址**:[HD-Icons-docker](https://github.com/xushier/HD-Icons-docker)
->
-> 💡 **优势**:自动同步更新、一键复制、移动端适配、自定义上传
-
-#### 🌟 功能展示
-
-
-
-| 功能 | 预览 |
-|------|------|
-| **☀️ 日间模式** |
|
-| **🌙 夜间模式** |
|
-| **📱 移动端适配** |
|
-| **📋 一键复制** |
|
-| **📤 自定义上传** |
|
-
-
-
-### 🔗 远程链接使用(自动同步更新)
-
-#### 📋 GitHub 原始链接
-
-> 🌐 **适用场景**:可直接访问 GitHub 的用户
->
-> 📝 **格式**:`https://raw.githubusercontent.com/xushier/HD-Icons/main/border-radius/图标名称.png`
->
-> 🔗 **项目地址**:[HD-Icons](https://github.com/xushier/HD-Icons)
-
-#### ⚡ CDN 加速链接
-
-> 🚀 **适用场景**:无法直接访问 GitHub 但需要使用 GitHub 图标源的用户
->
-> 📝 **格式**:`https://cdn.jsdelivr.net/gh/xushier/HD-Icons/border-radius/图标名称.png`
->
-> 💡 **优势**:全球 CDN 加速,访问速度更快
-
-> 📌 **使用提示**:将上述链接中的 `图标名称` 替换为实际图标文件名即可。[📋 查看所有图标](#-图标预览)
-
-### 📱 移动应用使用
-
-> 🎯 **支持应用**:Yamby、Hills 等安卓 APP
->
-> 📦 **图标包地址**:`https://raw.githubusercontent.com/xushier/HD-Icons/main/icons.json`
->
-> ⚠️ **注意**:如无响应,请开启代理环境或自行部署
->
-> 🚀 **自部署方案**:[HD-Icons-docker](https://github.com/xushier/HD-Icons-docker)
-
-## ✨ 图标预览
-
-> 🎨 **持续更新中**:当前共计 1754 个,数量不断增加,欢迎提交新图标请求
-### 📦 圆角矩形图标
-
-

-
-
-### ⭕ 圆形图标
-
-

-
-
-### 🎨 SVG 矢量图标
-
-
-
-
-
----
+
## 🤝 支持与赞助
-
!!!项目批量使用请告知!!!
-
💝 感谢您的支持,让这个项目持续发展!
-
-
-
-
- 支付宝
- |
-
-
- 微信支付
- |
-
-
+**!!!项目批量使用请告知!!!**
+
+**💝 感谢您的支持,让这个项目持续发展!**
+
+| 支付宝 | 微信支付 |
+|--------|----------|
+|

|

|
+
+
赞助时请备注「图标」,感谢您的慷慨支持!
-
赞助时请备注「图标」,感谢您的慷慨支持!
-## 📜 免责声明
+
+📜 免责声明
> ⚖️ **法律声明**:本仓库中图像的(几乎)所有产品名称、商标和注册商标均为其各自所有者的财产。仪表盘导航用户仅将本仓库中的所有图像用于识别目的。
->
+>
> 📝 **使用说明**:这些图像文件中出现的名称、商标和品牌的使用不表示认可。
----
+
+
+## ⭐ Star History
-### 🌟 项目统计
-
[](https://star-history.com/#xushier/HD-Icons&Date)
-### 📊 项目活跃度
+如果这个项目对您有帮助,请给个 ⭐ Star 支持一下!
-
-
+Made with ❤️ by xushier
-
----
-
-
-
🎯 如果这个项目对您有帮助,请给个 ⭐ Star 支持一下!
-
Made with ❤️ by xushier
-
-
diff --git a/_TEMPLATE.md b/_TEMPLATE.md
deleted file mode 100644
index 0f3fe57..0000000
--- a/_TEMPLATE.md
+++ /dev/null
@@ -1,222 +0,0 @@
-
-

-
HD-Icons:高清仪表盘图标
-
1024×1024 超清分辨率 · 无损压缩优化 · 多种样式适配
-
-
-
-
-
-
- [](https://github.com/xushier/HD-Icons/stargazers)
- [](https://github.com/xushier/HD-Icons/network)
- [](https://github.com/xushier/HD-Icons/issues)
- [](https://github.com/xushier/HD-Icons/blob/master/LICENSE)
-
-
- [](https://github.com/xushier/HD-Icons/releases)
- [](https://github.com/xushier/HD-Icons/commits/main)
- [](https://github.com/xushier/HD-Icons)
-
-
- [](https://www.jsdelivr.com/package/gh/xushier/HD-Icons)
-
-
-
-
-
-
-
-
-
-
-

-
-
-
-
-
-
-
-## 📝 项目简介
-
-> 🎯 **使命**:为 Unraid Docker 容器和各类仪表盘应用提供高质量、标准化的图标解决方案
-
-### ✨ 核心特性
-
-| 特性 | 描述 |
-|------|-----|
-| 🖼️ **超清分辨率** | 1024×1024 像素,确保在任何尺寸下都清晰锐利 |
-| 🗜️ **无损压缩** | 所有图标经过无损压缩,体积更小,质量不变 |
-| 🎨 **优化显示** | 图标主体适当放大,满足各种使用场景 |
-| 📁 **多样样式** | 提供 `border-radius`(圆角矩形)、 `circle`(圆形)和 `svg`(矢量)三种风格 |
-| 🔄 **持续更新** | 不定期添加新图标,满足不断增长的需求 |
-
-### 🎯 适用场景
-
-本项目最初为 `Unraid Docker` 容器图标展示而设计,现已扩展支持多种仪表盘应用、部分APP及其他用途:
-
-
-
-| 应用程序 | 描述 | 链接 | 备注 |
-|----------|------|------|------|
-| **FlatNas** | 新晋现代化仪表盘 | [🔗](https://github.com/Garry-QD/FlatNas) | 开源免费 |
-| **Sun-Panel** | 适合国人体质的现代化仪表盘 | [🔗](https://github.com/hslr-s/sun-panel) | 1.3.0及之前版本开源,新版Pro授权99永久 |
-| **Dashy** | 现代化仪表盘 | [🔗](https://github.com/Lissy93/dashy) | 开源免费 |
-| **Homer Dashboard** | 简洁的静态主页 | [🔗](https://github.com/bastienwirtz/homer) | 开源免费 |
-| **Heimdall** | 应用程序仪表盘 | [🔗](https://github.com/linuxserver/Heimdall) | 开源免费 |
-| **Organizr(v2)** | 组织管理工具 | [🔗](https://github.com/causefx/Organizr) | 开源免费 |
-| **Flame** | 自托管仪表盘 | [🔗](https://github.com/pawelmalak/flame) | 开源免费 |
-| **SUI** | 简单用户界面 | [🔗](https://github.com/jeroenpardon/sui) | 开源免费 |
-
-
-
-### 🌟 社区支持
-
-
-
-| 平台 | 账号/群组 | 链接 |
-|------|-----------|------|
-| **B站** | 小迪课代表 | [🎬 传送门](https://space.bilibili.com/32313260) |
-| **公众号** | 小迪同学 | 📱 微信搜索关注 |
-| **QQ群** | 647605169 | 💬 加入讨论 |
-| **微信群** | dxyxddsbds(备注加群) | 📱 添加微信入群 |
-
-
-
-> 💡 **需要新图标?** [点击这里提交请求](https://github.com/xushier/HD-Icons/issues/new/choose) 🎯
-
-## 📖 使用说明(三种方式)
-
-### 🐳 Docker 部署图标展示容器使用(强烈推荐)
-
-> 🚀 **项目地址**:[HD-Icons-docker](https://github.com/xushier/HD-Icons-docker)
->
-> 💡 **优势**:自动同步更新、一键复制、移动端适配、自定义上传
-
-#### 🌟 功能展示
-
-
-
-| 功能 | 预览 |
-|------|------|
-| **☀️ 日间模式** |
|
-| **🌙 夜间模式** |
|
-| **📱 移动端适配** |
|
-| **📋 一键复制** |
|
-| **📤 自定义上传** |
|
-
-
-
-### 🔗 远程链接使用(自动同步更新)
-
-#### 📋 GitHub 原始链接
-
-> 🌐 **适用场景**:可直接访问 GitHub 的用户
->
-> 📝 **格式**:`https://raw.githubusercontent.com/xushier/HD-Icons/main/border-radius/图标名称.png`
->
-> 🔗 **项目地址**:[HD-Icons](https://github.com/xushier/HD-Icons)
-
-#### ⚡ CDN 加速链接
-
-> 🚀 **适用场景**:无法直接访问 GitHub 但需要使用 GitHub 图标源的用户
->
-> 📝 **格式**:`https://cdn.jsdelivr.net/gh/xushier/HD-Icons/border-radius/图标名称.png`
->
-> 💡 **优势**:全球 CDN 加速,访问速度更快
-
-> 📌 **使用提示**:将上述链接中的 `图标名称` 替换为实际图标文件名即可。[📋 查看所有图标](#-图标预览)
-
-### 📱 移动应用使用
-
-> 🎯 **支持应用**:Yamby、Hills 等安卓 APP
->
-> 📦 **图标包地址**:`https://raw.githubusercontent.com/xushier/HD-Icons/main/icons.json`
->
-> ⚠️ **注意**:如无响应,请开启代理环境或自行部署
->
-> 🚀 **自部署方案**:[HD-Icons-docker](https://github.com/xushier/HD-Icons-docker)
-
-## ✨ 图标预览
-
-> 🎨 **持续更新中**:当前共计 0 个,数量不断增加,欢迎提交新图标请求
-
-### 📦 圆角矩形图标
-
-

-
-
-### ⭕ 圆形图标
-
-

-
-
-### 🎨 SVG 矢量图标
-
-
-
-
-
-
-
-
-
-
-
----
-
-## 🤝 支持与赞助
-
-
-
!!!项目批量使用请告知!!!
-
💝 感谢您的支持,让这个项目持续发展!
-
-
-
-
-
- 支付宝
- |
-
-
- 微信支付
- |
-
-
-
-
赞助时请备注「图标」,感谢您的慷慨支持!
-
-
-## 📜 免责声明
-
-> ⚖️ **法律声明**:本仓库中图像的(几乎)所有产品名称、商标和注册商标均为其各自所有者的财产。仪表盘导航用户仅将本仓库中的所有图像用于识别目的。
->
-> 📝 **使用说明**:这些图像文件中出现的名称、商标和品牌的使用不表示认可。
-
----
-
-
-
-### 🌟 项目统计
-
-[](https://star-history.com/#xushier/HD-Icons&Date)
-
-### 📊 项目活跃度
-
-
-
-
-
-
----
-
-
-
🎯 如果这个项目对您有帮助,请给个 ⭐ Star 支持一下!
-
Made with ❤️ by xushier
-
diff --git a/_generate.py b/_generate.py
deleted file mode 100644
index b12d21e..0000000
--- a/_generate.py
+++ /dev/null
@@ -1,53 +0,0 @@
-import pathlib
-from pathlib import Path
-
-root = pathlib.Path(__file__).parent.resolve()
-template_path = root / "_TEMPLATE.md"
-readme_path = root / "README.md"
-
-
-# def generate_img_tag(file,folder,radius):
- # return f'
{file.stem}
'
-
-def generate_img_tag(file,folder):
- return f'
'
-
-imgs = sorted(Path("./border-radius").glob("*.png"))
-img_tags = [generate_img_tag(x,"border-radius") for x in imgs]
-circle_imgs = sorted(Path("./circle").glob("*.png"))
-circle_img_tags = [generate_img_tag(x,"circle") for x in circle_imgs]
-svg_imgs = sorted(Path("./svg").glob("*.svg"))
-svg_img_tags = [generate_img_tag(x,"svg") for x in svg_imgs]
-all_nums = len(img_tags) + len(circle_img_tags) + len(svg_img_tags)
-
-# Read the template file
-with open(template_path, "r", encoding="UTF-8") as f:
- lines = f.readlines()
-
-# Find the line that starts with ""
-# for line in lines:
-# if line.startswith(""):
-# line_number = lines.index(line)
-# break
-# for line in lines:
-# if line.startswith(""):
-# circle_line_number = lines.index(line)
-# break
-for line in lines:
- if line.startswith(""):
- svg_line_number = lines.index(line)
- break
-
-# Insert the icons after the line
-# lines.insert(line_number + 1, " ".join(img_tags))
-# lines.insert(circle_line_number + 2, " ".join(circle_img_tags))
-lines.insert(svg_line_number + 3, " ".join(svg_img_tags))
-
-lines = ["> 🎨 **持续更新中**:当前共计 " + str(all_nums) + " 个,数量不断增加,欢迎提交新图标请求" if '> 🎨 **持续更新中**:当前共计 0 个,数量不断增加,欢迎提交新图标请求' in i else i for i in lines]
-
-# Write the new file
-with open(readme_path, "w", encoding="UTF-8") as f:
- f.write("".join(lines))
- f.write("\n")
-
-print("文件已更新!")
diff --git a/_icons-circle.jpg b/_icons-circle.jpg
index ab554c8..71f8dff 100644
Binary files a/_icons-circle.jpg and b/_icons-circle.jpg differ
diff --git a/_icons-radius.jpg b/_icons-radius.jpg
index af73dbd..12f32c0 100644
Binary files a/_icons-radius.jpg and b/_icons-radius.jpg differ
diff --git a/icons.json b/icons.json
index 4a76dc7..63d984b 100644
--- a/icons.json
+++ b/icons.json
@@ -1,8 +1,8 @@
{
"name": "小迪的HD-Icons",
- "description": "当前数量:1754,更新于:2026-02-01 21:43:36。不定期更新,有需求欢迎提交:github.com/xushier/HD-Icons",
+ "description": "当前数量:1754,更新于:2026-08-27 19:06:22。不定期更新,有需求欢迎提交:github.com/xushier/HD-Icons",
"total_count": 1754,
- "update_at": "2026-02-01 21:43:36",
+ "update_at": "2026-08-27 19:06:22",
"icons": [
{
"name": "115-drive-1",
@@ -7021,4 +7021,4 @@
"url": "https://raw.githubusercontent.com/xushier/HD-Icons/main/svg/zulip-2.svg"
}
]
-}
\ No newline at end of file
+}
diff --git a/inbox/border-radius/.gitkeep b/inbox/border-radius/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/inbox/circle/.gitkeep b/inbox/circle/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/inbox/svg/.gitkeep b/inbox/svg/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/publish.bat b/publish.bat
new file mode 100644
index 0000000..4888e28
--- /dev/null
+++ b/publish.bat
@@ -0,0 +1,4 @@
+@echo off
+chcp 65001 >nul
+powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0publish.ps1" %*
+pause
diff --git a/publish.ps1 b/publish.ps1
new file mode 100644
index 0000000..ac74ecb
--- /dev/null
+++ b/publish.ps1
@@ -0,0 +1,52 @@
+# HD-Icons 一键发布脚本
+# 用法:把新图标放入 inbox/ 对应子文件夹后,双击 publish.bat(或运行本脚本)
+# 可选参数:.\publish.ps1 -Message "自定义提交说明"
+
+param([string]$Message = "")
+
+$ErrorActionPreference = "Stop"
+Set-Location $PSScriptRoot
+
+# 定位 git(git 不在 PATH 时使用常见安装路径)
+$git = "git"
+if (-not (Get-Command git -ErrorAction SilentlyContinue)) {
+ $fallback = "C:\Program Files\Git\cmd\git.exe"
+ if (Test-Path $fallback) { $git = $fallback } else {
+ Write-Host "[错误] 未找到 git,请安装 Git 或将其加入 PATH" -ForegroundColor Red
+ exit 1
+ }
+}
+
+# 检查 inbox 是否有新文件
+$inboxStatus = & $git status --porcelain -- inbox
+if (-not $inboxStatus) {
+ Write-Host "inbox 中没有新图标,无需发布。" -ForegroundColor Yellow
+ exit 0
+}
+
+# 从文件名自动生成提交说明
+$names = @($inboxStatus | ForEach-Object {
+ $_.Substring(3).Trim() -replace '^inbox/', ''
+} | ForEach-Object { [System.IO.Path]::GetFileNameWithoutExtension($_) -replace '-\d+$', '' } | Sort-Object -Unique)
+$prefixes = ($names | Select-Object -First 8) -join ", "
+if ($names.Count -gt 8) { $prefixes += " 等" }
+$fileCount = ($inboxStatus | Measure-Object).Count
+if (-not $Message) {
+ $Message = "新增 $fileCount 个图标:$prefixes"
+}
+
+Write-Host "本次发布:$Message" -ForegroundColor Cyan
+& $git add -A
+& $git commit -m $Message
+if ($LASTEXITCODE -ne 0) {
+ Write-Host "[错误] 提交失败" -ForegroundColor Red
+ exit 1
+}
+& $git push
+if ($LASTEXITCODE -ne 0) {
+ Write-Host "[错误] 推送失败,请检查网络或凭据" -ForegroundColor Red
+ exit 1
+}
+Write-Host ""
+Write-Host "已推送到 GitHub,Actions 将自动完成压缩、入库、索引、README 与预览图更新。" -ForegroundColor Green
+Write-Host "进度查看:https://github.com/xushier/HD-Icons/actions"
diff --git a/scripts/build.py b/scripts/build.py
new file mode 100644
index 0000000..497d7cd
--- /dev/null
+++ b/scripts/build.py
@@ -0,0 +1,409 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+HD-Icons 一键构建脚本(本地 / GitHub Actions 通用)
+
+流程:
+ 1. 校验 inbox 新文件命名
+ 2. TinyPNG 无损压缩 inbox 中的 PNG(SVG 不压缩)
+ 3. 分发入库:inbox 文件移入对应目录,重名按"同前缀最大序号+1"自动改名
+ 4. 全量重建 icons.json(输出格式与历史版本完全一致)
+ 5. 更新 README.md 中的数量标记
+ 6. 重新生成三张合并预览图(_icons-radius.jpg / _icons-circle.jpg / _icons-svg.jpg)
+
+用法:
+ python scripts/build.py --repo .
+
+环境变量:
+ TINYPNG_KEY TinyPNG API 密钥(inbox 有 PNG 时必需;GitHub 上配置为 Secret)
+ HDICONS_FONT 预览图标签字体路径(可选,默认自动查找)
+"""
+
+import argparse
+import base64
+import json
+import os
+import re
+import shutil
+import subprocess
+import sys
+import tempfile
+import urllib.error
+import urllib.request
+from datetime import datetime
+from pathlib import Path
+
+FOLDERS = ["border-radius", "circle", "svg"]
+RAW_BASE = "https://raw.githubusercontent.com/xushier/HD-Icons/main"
+IMAGE_EXTS = {".png", ".jpg", ".jpeg", ".svg", ".gif"}
+
+NAME_RE = re.compile(r"^[a-z0-9]+(-[a-z0-9]+)*$")
+NUM_SUFFIX_RE = re.compile(r"-\d+$")
+README_MARKER_RE = re.compile(r"()(.*?)()")
+
+# 预览图排版参数
+PREVIEW = {
+ "icon": 128, # 单个图标缩略尺寸
+ "cols": 15, # 每行图标数
+ "gap_x": 30, # 水平间距
+ "gap_y": 62, # 垂直间距(含文件名文字区域)
+ "pad": 20, # 四周留白
+ "text_size": 17, # 文件名字号
+ "bg": (244, 246, 249),
+ "fg": (62, 66, 72),
+}
+
+
+def log(msg):
+ print(msg, flush=True)
+
+
+def warn(msg):
+ print(f"[警告] {msg}", file=sys.stderr, flush=True)
+
+
+def fail(msg):
+ print(f"[错误] {msg}", file=sys.stderr, flush=True)
+ sys.exit(1)
+
+
+# ---------------------------------------------------------------- 1. 校验 inbox
+
+def validate_inbox(repo):
+ """检查 inbox 内新文件的类型与命名,返回 (errors, warnings, files)。"""
+ errors, warnings, files = [], [], {}
+ for folder in FOLDERS:
+ inbox_dir = repo / "inbox" / folder
+ items = []
+ if not inbox_dir.is_dir():
+ files[folder] = items
+ continue
+ expected = ".svg" if folder == "svg" else ".png"
+ for f in sorted(inbox_dir.iterdir()):
+ if f.name == ".gitkeep" or not f.is_file():
+ continue
+ if f.suffix.lower() != expected:
+ errors.append(f"inbox/{folder}/ 只接受 {expected} 文件,发现:{f.name}")
+ continue
+ if not NAME_RE.match(f.stem):
+ errors.append(f"命名不合法(仅允许小写字母/数字/连字符):{f.name}")
+ continue
+ if not NUM_SUFFIX_RE.search(f.stem):
+ warnings.append(f"{f.name} 未以数字结尾,建议形如 name-1 的命名")
+ items.append(f)
+ files[folder] = items
+ return errors, warnings, files
+
+
+# ---------------------------------------------------------------- 2. TinyPNG 压缩
+
+def tinify(path, key):
+ """调用 TinyPNG API 压缩单个 PNG,返回 (压缩前字节数, 压缩后字节数, 已用额度)。"""
+ data = path.read_bytes()
+ req = urllib.request.Request("https://api.tinify.com/shrink", data=data)
+ req.add_header("Authorization",
+ "Basic " + base64.b64encode(f"api:{key}".encode()).decode())
+ with urllib.request.urlopen(req, timeout=180) as resp:
+ info = json.loads(resp.read().decode())
+ quota = (resp.headers.get("Compression-Count")
+ or resp.headers.get("X-Compression-Count"))
+ with urllib.request.urlopen(info["output"]["url"], timeout=180) as r:
+ out = r.read()
+ if len(out) < len(data):
+ path.write_bytes(out)
+ return len(data), path.stat().st_size, quota
+
+
+def compress_inbox(files, key):
+ saved = 0
+ quota = None
+ pngs = files["border-radius"] + files["circle"]
+ for f in pngs:
+ try:
+ before, after, quota = tinify(f, key)
+ except urllib.error.HTTPError as e:
+ if e.code == 429:
+ fail("TinyPNG 本月免费额度已用完(500 张/月),本次未做任何改动")
+ fail(f"压缩失败 {f.name}:HTTP {e.code} {e.reason}")
+ except (urllib.error.URLError, OSError) as e:
+ fail(f"压缩失败 {f.name}:网络错误 {e}")
+ saved += before - after
+ log(f" {f.name}: {before // 1024}KB -> {after // 1024}KB")
+ msg = f"共压缩 {len(pngs)} 张,节省 {saved // 1024}KB"
+ if quota:
+ msg += f"(本月已用 {quota}/500)"
+ log(msg)
+
+
+# ---------------------------------------------------------------- 3. 分发入库
+
+def available_name(target_dir, name):
+ """若目标已存在同名文件,按"同前缀最大序号+1"改名,否则保持原名。"""
+ if not (target_dir / name).exists():
+ return name
+ stem, ext = Path(name).stem, Path(name).suffix
+ prefix = NUM_SUFFIX_RE.sub("", stem)
+ max_num = 0
+ for f in target_dir.iterdir():
+ m = re.fullmatch(re.escape(prefix) + r"-(\d+)", f.stem)
+ if m and f.suffix.lower() == ext.lower():
+ max_num = max(max_num, int(m.group(1)))
+ n = max_num + 1
+ while (target_dir / f"{prefix}-{n}{ext}").exists():
+ n += 1
+ return f"{prefix}-{n}{ext}"
+
+
+def dispatch(repo, files):
+ moved = []
+ for folder, items in files.items():
+ if not items:
+ continue
+ target = repo / folder
+ for f in items:
+ new_name = available_name(target, f.name)
+ if new_name != f.name:
+ log(f" 重名处理:{folder}/{f.name} -> {new_name}")
+ shutil.move(str(f), str(target / new_name))
+ moved.append(f"{folder}/{new_name}")
+ return moved
+
+
+# ---------------------------------------------------------------- 4. icons.json
+
+def build_icons_json(repo):
+ """全量重建 icons.json,逻辑与历史脚本一致(含跨目录重名加后缀的去重行为)。"""
+ icons, used = [], set()
+ counts = {"radius": 0, "circle": 0, "svg": 0}
+ for folder in FOLDERS:
+ d = repo / folder
+ if not d.is_dir():
+ warn(f"文件夹不存在,跳过:{folder}")
+ continue
+ for file in sorted(d.iterdir()):
+ if not (file.is_file() and file.suffix.lower() in IMAGE_EXTS):
+ continue
+ name = file.stem
+ original = name
+ url = f"{RAW_BASE}/{folder}/{file.name}"
+ counter = 1
+ while name in used:
+ name = f"{original}-{counter}"
+ counter += 1
+ used.add(name)
+ icons.append({"name": name, "url": url})
+ counts[{"border-radius": "radius", "circle": "circle", "svg": "svg"}[folder]] += 1
+
+ update_at = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
+ data = {
+ "name": "小迪的HD-Icons",
+ "description": (f"当前数量:{len(icons)},更新于:{update_at}。"
+ "不定期更新,有需求欢迎提交:github.com/xushier/HD-Icons"),
+ "total_count": len(icons),
+ "update_at": update_at,
+ "icons": icons,
+ }
+ (repo / "icons.json").write_text(
+ json.dumps(data, ensure_ascii=False, indent=4) + "\n", encoding="utf-8")
+ counts["total"] = len(icons)
+ return counts
+
+
+# ---------------------------------------------------------------- 5. README 标记
+
+def update_readme(repo, counts):
+ readme = repo / "README.md"
+ if not readme.is_file():
+ warn("README.md 不存在,跳过数量更新")
+ return
+ text = readme.read_text(encoding="utf-8")
+
+ def repl(m):
+ key = m.group(2)
+ if key in counts:
+ return m.group(1) + str(counts[key]) + m.group(4)
+ warn(f"README 中存在未知标记 ICONS:{key}")
+ return m.group(0)
+
+ readme.write_text(README_MARKER_RE.sub(repl, text), encoding="utf-8")
+
+
+# ---------------------------------------------------------------- 6. 预览图
+
+def load_font(size):
+ from PIL import ImageFont
+ candidates = [
+ os.environ.get("HDICONS_FONT"),
+ str(Path(__file__).resolve().parent / "fonts" / "DejaVuSans.ttf"),
+ "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
+ "C:/Windows/Fonts/segoeui.ttf",
+ "C:/Windows/Fonts/arial.ttf",
+ ]
+ for c in candidates:
+ if c and Path(c).is_file():
+ try:
+ return ImageFont.truetype(c, size)
+ except OSError:
+ pass
+ warn("未找到可用字体,预览图将使用默认位图字体")
+ return ImageFont.load_default()
+
+
+def rasterize_svg(svg_path, size, cache_dir):
+ """把 SVG 光栅化为 PNG(依次尝试 rsvg-convert / resvg / cairosvg),失败返回 None。"""
+ out = cache_dir / (svg_path.stem + ".png")
+ if out.exists():
+ return out
+ cmds = []
+ if shutil.which("rsvg-convert"):
+ cmds.append(["rsvg-convert", "-w", str(size), "-h", str(size),
+ "-o", str(out), str(svg_path)])
+ if shutil.which("resvg"):
+ cmds.append(["resvg", "--width", str(size), "--height", str(size),
+ str(svg_path), str(out)])
+ for cmd in cmds:
+ try:
+ if subprocess.run(cmd, capture_output=True, timeout=60).returncode == 0 \
+ and out.exists():
+ return out
+ except (OSError, subprocess.TimeoutExpired):
+ pass
+ try:
+ import cairosvg
+ cairosvg.svg2png(url=str(svg_path), write_to=str(out),
+ output_width=size, output_height=size)
+ if out.exists():
+ return out
+ except Exception:
+ pass
+ return None
+
+
+def merge_preview(repo, folder, out_name):
+ """把目录内全部图标合并为一张带文件名标注的 JPEG 预览图。"""
+ from PIL import Image, ImageDraw
+ import textwrap
+
+ src = repo / folder
+ files = sorted(f for f in src.iterdir()
+ if f.is_file() and f.suffix.lower() in IMAGE_EXTS)
+ if not files:
+ warn(f"{folder}/ 为空,跳过预览图 {out_name}")
+ return
+
+ font = load_font(PREVIEW["text_size"])
+ cell_w = PREVIEW["icon"] + PREVIEW["gap_x"]
+ cell_h = PREVIEW["icon"] + PREVIEW["gap_y"]
+ rows = (len(files) + PREVIEW["cols"] - 1) // PREVIEW["cols"]
+ width = PREVIEW["cols"] * cell_w - PREVIEW["gap_x"] + 2 * PREVIEW["pad"]
+ height = rows * cell_h - PREVIEW["gap_y"] + 2 * PREVIEW["pad"]
+
+ sheet = Image.new("RGB", (width, height), PREVIEW["bg"])
+ draw = ImageDraw.Draw(sheet)
+
+ cache_dir = Path(tempfile.mkdtemp(prefix="hdicons_"))
+ skipped = 0
+ try:
+ for i, f in enumerate(files):
+ row, col = divmod(i, PREVIEW["cols"])
+ x = col * cell_w + PREVIEW["pad"]
+ y = row * cell_h + PREVIEW["pad"]
+
+ if f.suffix.lower() == ".svg":
+ raster = rasterize_svg(f, 512, cache_dir)
+ if raster is None:
+ skipped += 1
+ continue
+ f = raster
+ try:
+ icon = Image.open(f).convert("RGBA")
+ except OSError as e:
+ warn(f"无法读取 {f.name}:{e}")
+ skipped += 1
+ continue
+ icon = icon.resize((PREVIEW["icon"], PREVIEW["icon"]),
+ Image.Resampling.LANCZOS)
+ sheet.paste(icon, (x, y), icon)
+
+ wrapped = "\n".join(textwrap.wrap(f.stem, width=16))
+ bbox = draw.textbbox((0, 0), wrapped, font=font)
+ text_w = bbox[2] - bbox[0]
+ draw.text((x + (PREVIEW["icon"] - text_w) // 2,
+ y + PREVIEW["icon"] + 4),
+ wrapped, font=font, fill=PREVIEW["fg"])
+ finally:
+ shutil.rmtree(cache_dir, ignore_errors=True)
+
+ rendered = len(files) - skipped
+ if rendered < len(files):
+ # 预览图必须包含全部图标,有遗漏时不覆盖已有图片,避免生成残缺预览
+ warn(f"{folder} 有 {skipped} 个文件未能渲染(SVG 光栅化不可用或文件损坏),"
+ f"本次跳过写入 {out_name},保留现有版本")
+ return
+ out_path = repo / out_name
+ sheet.save(out_path, format="JPEG", quality=78, dpi=(128, 128))
+ log(f" {out_name}: {len(files)} 个图标,{sheet.width}x{sheet.height}")
+
+
+# ---------------------------------------------------------------- 主流程
+
+def main():
+ parser = argparse.ArgumentParser(description="HD-Icons 一键构建")
+ parser.add_argument("--repo", default=".", help="仓库根目录")
+ parser.add_argument("--skip-compress", action="store_true",
+ help="跳过 TinyPNG 压缩(仅调试用)")
+ args = parser.parse_args()
+ repo = Path(args.repo).resolve()
+
+ log("== 1/6 校验 inbox ==")
+ errors, warnings, inbox_files = validate_inbox(repo)
+ for w in warnings:
+ warn(w)
+ if errors:
+ for e in errors:
+ warn(e)
+ fail("inbox 存在问题,已中止(所有文件保持原样)")
+
+ png_count = len(inbox_files["border-radius"]) + len(inbox_files["circle"])
+ svg_count = len(inbox_files["svg"])
+ total_inbox = png_count + svg_count
+ log(f"inbox 共 {total_inbox} 个新文件(PNG {png_count},SVG {svg_count})")
+
+ if png_count and not args.skip_compress:
+ key = os.environ.get("TINYPNG_KEY", "").strip()
+ if not key:
+ fail("inbox 中有 PNG 但未设置 TINYPNG_KEY"
+ "(GitHub 上需在 Settings -> Secrets 中配置)")
+ log("== 2/6 TinyPNG 无损压缩 ==")
+ compress_inbox(inbox_files, key)
+ else:
+ log("== 2/6 跳过压缩(无 PNG 或指定 --skip-compress)==")
+
+ log("== 3/6 分发入库 ==")
+ moved = dispatch(repo, inbox_files)
+ for m in moved:
+ log(f" + {m}")
+
+ log("== 4/6 重建 icons.json ==")
+ counts = build_icons_json(repo)
+ log(f"总数 {counts['total']}(圆角矩形 {counts['radius']} / "
+ f"圆形 {counts['circle']} / SVG {counts['svg']})")
+
+ log("== 5/6 更新 README 数量标记 ==")
+ update_readme(repo, counts)
+
+ log("== 6/6 生成预览图 ==")
+ merge_preview(repo, "border-radius", "_icons-radius.jpg")
+ merge_preview(repo, "circle", "_icons-circle.jpg")
+ merge_preview(repo, "svg", "_icons-svg.jpg")
+
+ gh_output = os.environ.get("GITHUB_OUTPUT")
+ if gh_output:
+ with open(gh_output, "a", encoding="utf-8") as f:
+ f.write(f"added={len(moved)}\n")
+
+ log(f"完成!本次新增 {len(moved)} 个图标。")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/scripts/fonts/DejaVuSans.ttf b/scripts/fonts/DejaVuSans.ttf
new file mode 100644
index 0000000..e5f7eec
Binary files /dev/null and b/scripts/fonts/DejaVuSans.ttf differ