Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
155 changes: 84 additions & 71 deletions plugins/imgbatch/README.md
Original file line number Diff line number Diff line change
@@ -1,17 +1,23 @@
# Imgbatch
# 图片批量处理

## 🤖 关于本项目

本项目的**所有代码均由 AI(Artificial Intelligence)生成**,项目作者本人不具备相关编程知识,对代码内容不作任何技术层面的解释或保证。如果你遇到问题或有改进想法,欢迎自行修改、Fork 或提交 Issue。

Imgbatch 是一个面向 ZTools 的图片批量处理插件,提供本地离线的多种图片处理能力,适合快速处理一批图片并统一输出结果。
图片批量处理是一个面向 ZTools 的图片批量处理插件,提供本地离线的多种图片处理能力,适合快速处理一批图片并统一输出结果。

当前版本:`0.1.5`
当前版本:`0.1.6`

## 0.1.6 更新

- 修正水印页点击锚点位置会触发页面回顶的问题,局部重刷时会保留工具面板滚动位置
- 旋转页将常用角度编辑改为单独弹框,支持添加、删除、排序和恢复默认角度
- 收紧常用角度编辑行为,避免仅修改 UI 偏好时把现有预览结果误标为需要重新预览
- 图标改为内置本地 SVG 方案,不再依赖外网图标资源,离线首次打开也能正常显示
- 修正“修改尺寸 -> 对齐最大 / 对齐最小”在锁定比例开启时只会塞进参考框、无法真正统一输出尺寸的问题

## 0.1.5 更新

- 补齐插件运行时依赖,修正从官方插件仓库安装后缺少 `sharp` 导致本地图片处理无法执行的问题
- 修正合并为 PDF 时缺少 `fork` 引入导致的执行失败问题
- 调整插件包元数据,补齐 `platform` 与 `unpack` 配置,提升官方仓库分发包的运行兼容性
- 收紧 `plugin.json` 为正式安装包可用格式,移除会导致已安装插件空白页的开发入口配置
- 统一插件标题显示为 `Imgbatch`
Expand Down Expand Up @@ -41,73 +47,9 @@ Imgbatch 是一个面向 ZTools 的图片批量处理插件,提供本地离线
- 设置页改为全屏工作区模式,支持滚动和 `Esc` 退出
- 修正结果页、继续处理、确认框/预设框 `Esc` 等一批交互回归问题

## 界面预览

### 图片压缩

![图片批量处理 图片压缩](./docs/screenshots/compression.png)

### 格式转换

![图片批量处理 格式转换](./docs/screenshots/format.png)

### 修改尺寸

![图片批量处理 修改尺寸](./docs/screenshots/resize.png)

### 添加水印

![图片批量处理 添加水印](./docs/screenshots/watermark.png)

### 添加圆角

![图片批量处理 添加圆角](./docs/screenshots/corners.png)

### 补边留白

![图片批量处理 补边留白](./docs/screenshots/padding.png)

### 裁剪

![图片批量处理 裁剪](./docs/screenshots/crop.png)

### 旋转

![图片批量处理 旋转](./docs/screenshots/rotate.png)

### 翻转

![图片批量处理 翻转](./docs/screenshots/flip.png)

### 合并为 PDF

![图片批量处理 合并为 PDF](./docs/screenshots/merge-pdf.png)

### 合并为图片

![图片批量处理 合并为图片](./docs/screenshots/merge-image.png)

### 合并为 GIF

![图片批量处理 合并为 GIF](./docs/screenshots/merge-gif.png)

### 手动裁剪

![图片批量处理 手动裁剪](./docs/screenshots/manual-crop.png)

### 预览效果

![图片批量处理 预览双栏对比图](./docs/screenshots/preview1.png)

![图片批量处理 预览前后对比图](./docs/screenshots/preview2.png)

### 设置页

![图片批量处理 设置页](./docs/screenshots/settings.png)

### 处理结果对比
## 界面说明

![图片批量处理 处理结果对比](./docs/screenshots/result.png)
仓库内截图资源已移除,避免发布目录和官方插件仓库持续携带大体积预览图片。插件界面与交互以本地 ZTools 客户端中的实际表现为准。

## 功能

Expand Down Expand Up @@ -173,6 +115,8 @@ MIT
图片批量处理/
├── assets/ # 前端页面、状态、样式与组件
├── lib/ # preload 侧共享运行逻辑
├── scripts/ # 发布和测试辅助脚本
├── runtime-packages/ # CLI 发布目录中的运行时依赖(仅发布产物中出现)
├── index.html # 插件页面入口
├── plugin.json # ZTools 插件配置
├── preload.js # 本地处理与 ZTools API 桥接
Expand All @@ -196,3 +140,72 @@ npm install

如果你想直接体验插件,请前往仓库的 Releases 页面下载对应版本的打包文件,并按 ZTools 的插件安装方式导入使用。

## CLI 发布流程

当前官方推荐使用 `@ztools-center/plugin-cli` 发布到 `ZTools-plugins`。

### 当前项目的特殊点

Imgbatch 不是纯前端插件,`preload.js` 运行时依赖:

- `sharp`
- `pdf-lib`
- `gifenc`

其中 `sharp` 包含原生模块与动态库。`plugin-cli` 默认会忽略:

- `node_modules/`
- `dist/`
- `build/`

因此 **不能直接拿源码根目录执行 `ztools publish`**,否则官方插件仓库里的分发包会丢失运行时依赖。

### 当前可用做法

先生成 CLI 兼容发布目录:

```bash
npm run build:cli-package
```

生成一个用于 CLI 发布的相对目录:

- 目录名由构建脚本生成
- 该目录位于仓库根目录下

这份目录具有这些特点:

- 不包含 `node_modules/`
- 运行时依赖被整理到 `runtime-packages/`
- `preload.js` 启动时会自动把 `runtime-packages/` 加入模块搜索路径
- `plugin.json` 已补齐 `runtime-packages/**/*.node` 和 `runtime-packages/**/*.dll` 的 `unpack` 规则

### 发布步骤

1. 在源码仓库完成修改并验证功能。
2. 执行:

```bash
npm run build:cli-package
```

3. 进入生成好的 CLI 发布目录,初始化独立 git 仓库并提交一次:

```bash
git init
git add .
git commit -m "Release Imgbatch x.y.z"
```

4. 在该目录执行:

```bash
ztools publish
```

### 注意事项

- 不要直接在源码仓库根目录执行 `ztools publish`,否则会把本地资料目录、文档和不适合发布的文件一起带进插件仓库。
- `plugin-cli` 会要求当前目录是干净 git 仓库,发布目录里不能留未提交改动。
- `runtime-packages/` 是当前适配 `plugin-cli` 的关键目录,不要随意改回 `node_modules/`。
- 若后续官方 `plugin-cli` 调整忽略规则或支持原生依赖发布,再重新评估是否继续保留这套兼容目录。
92 changes: 83 additions & 9 deletions plugins/imgbatch/assets/app/components/AppShell.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { renderSideNav } from './SideNav.js'
import { renderTopBar } from './TopBar.js'
import { renderImageQueue } from './ImageQueueList.js'
import { renderToolPage } from '../pages/index.js'
import { renderIcon } from './icons.js'

export function getAppShellMode(state) {
const tool = TOOL_MAP[state.activeTool]
Expand Down Expand Up @@ -110,7 +111,7 @@ function renderSettingsWorkspace(dialog) {
<div class="select-shell settings-select ${dialog.settingsSelectOpen ? 'is-open' : ''}">
<button type="button" class="select-shell__value" data-action="toggle-config-select" aria-haspopup="listbox" aria-expanded="${dialog.settingsSelectOpen ? 'true' : 'false'}">
<span class="select-shell__text">${escapeHtml((options.find(([value]) => value === mode) || options[0])[1])}</span>
<span class="material-symbols-outlined select-shell__icon">expand_more</span>
${renderIcon('expand_more', 'select-shell__icon')}
</button>
<div class="select-shell__menu" role="listbox">
${options.map(([value, label]) => `
Expand Down Expand Up @@ -138,7 +139,7 @@ function renderSettingsWorkspace(dialog) {
<div class="select-shell settings-select ${dialog.performanceSelectOpen ? 'is-open' : ''}">
<button type="button" class="select-shell__value" data-action="toggle-config-select" aria-haspopup="listbox" aria-expanded="${dialog.performanceSelectOpen ? 'true' : 'false'}">
<span class="select-shell__text">${escapeHtml((performanceOptions.find(([value]) => value === performanceMode) || performanceOptions[1])[1])}</span>
<span class="material-symbols-outlined select-shell__icon">expand_more</span>
${renderIcon('expand_more', 'select-shell__icon')}
</button>
<div class="select-shell__menu" role="listbox">
${performanceOptions.map(([value, label]) => `
Expand All @@ -158,7 +159,7 @@ function renderSettingsWorkspace(dialog) {
<div class="select-shell settings-select">
<button type="button" class="select-shell__value" data-action="toggle-config-select" aria-haspopup="listbox" aria-expanded="false">
<span class="select-shell__text">${escapeHtml((queueThumbnailOptions.find(([value]) => value === queueThumbnailSize) || queueThumbnailOptions[2])[1])}</span>
<span class="material-symbols-outlined select-shell__icon">expand_more</span>
${renderIcon('expand_more', 'select-shell__icon')}
</button>
<div class="select-shell__menu" role="listbox">
${queueThumbnailOptions.map(([value, label]) => `
Expand Down Expand Up @@ -196,6 +197,9 @@ function renderSettingsWorkspace(dialog) {
function renderPresetModal(state) {
const dialog = state.presetDialog
if (!dialog?.visible) return ''
if (dialog.mode === 'rotate-presets') {
return renderRotatePresetModal(dialog)
}

const presets = state.presetsByTool?.[dialog.toolId] || []
const toolLabel = TOOL_MAP[dialog.toolId]?.label || dialog.toolId
Expand All @@ -205,7 +209,7 @@ function renderPresetModal(state) {
<div class="app-modal" data-action="close-preset-dialog">
<div class="app-modal__dialog app-modal__dialog--preset">
<button class="app-modal__close" data-action="close-preset-dialog" data-tooltip="关闭" aria-label="关闭">
<span class="material-symbols-outlined">close</span>
${renderIcon('close')}
</button>
<div class="app-modal__header">
<div class="app-modal__title">${modeTitle}</div>
Expand Down Expand Up @@ -267,14 +271,84 @@ function renderPresetModal(state) {
`
}

function renderRotatePresetModal(dialog) {
const draftAngles = Array.isArray(dialog.presetAnglesDraft) ? dialog.presetAnglesDraft : []

return `
<div class="app-modal" data-action="close-preset-dialog">
<div class="app-modal__dialog app-modal__dialog--preset app-modal__dialog--rotate-presets">
<button class="app-modal__close" data-action="close-preset-dialog" data-tooltip="关闭" aria-label="关闭">
${renderIcon('close')}
</button>
<div class="app-modal__header">
<div class="app-modal__title">常用角度调整</div>
<div class="app-modal__subtitle">旋转</div>
</div>
<div class="rotate-preset-manager">
<div class="rotate-preset-manager__input-row">
<label class="setting-row setting-row--stack">
<span class="setting-row__header">
<span class="setting-row__label">添加角度</span>
</span>
<span class="input-shell">
<input
class="text-input"
type="number"
inputmode="numeric"
min="-360"
max="360"
step="1"
value="${escapeHtml(dialog.angleInput || '')}"
placeholder="例如 30"
data-action="change-rotate-preset-input"
aria-label="输入要加入常用角度的数值"
/>
</span>
</label>
<div class="rotate-preset-manager__toolbar">
<button type="button" class="secondary-button secondary-button--compact watermark-picker-button" data-action="add-rotate-preset-dialog-item">添加角度</button>
<button type="button" class="secondary-button secondary-button--compact watermark-picker-button" data-action="sort-rotate-preset-dialog-items">按角度排序</button>
<button type="button" class="secondary-button secondary-button--compact watermark-picker-button" data-action="reset-rotate-preset-dialog-items">恢复默认</button>
</div>
</div>
<div class="rotate-preset-manager__list">
${draftAngles.length
? draftAngles.map((angle, index) => `
<div class="rotate-preset-manager__item">
<div class="rotate-preset-manager__value">${escapeHtml(`${angle}°`)}</div>
<div class="rotate-preset-manager__item-actions">
<button type="button" class="icon-button" data-action="move-rotate-preset-dialog-item" data-direction="up" data-index="${index}" aria-label="上移" ${index === 0 ? 'disabled' : ''}>
${renderIcon('keyboard_arrow_up')}
</button>
<button type="button" class="icon-button" data-action="move-rotate-preset-dialog-item" data-direction="down" data-index="${index}" aria-label="下移" ${index === draftAngles.length - 1 ? 'disabled' : ''}>
${renderIcon('keyboard_arrow_down')}
</button>
<button type="button" class="icon-button" data-action="remove-rotate-preset-dialog-item" data-index="${index}" aria-label="删除">
${renderIcon('delete')}
</button>
</div>
</div>
`).join('')
: '<div class="preset-empty">当前还没有常用角度,添加后会显示在这里。</div>'}
</div>
</div>
<div class="app-modal__footer">
<button type="button" class="secondary-button" data-action="close-preset-dialog">取消</button>
<button type="button" class="primary-button" data-action="confirm-rotate-preset-dialog">保存调整</button>
</div>
</div>
</div>
`
}

function renderConfirmModal(dialog) {
if (!dialog?.visible) return ''

return `
<div class="app-modal" data-action="close-confirm-dialog">
<div class="app-modal__dialog app-modal__dialog--preset">
<button class="app-modal__close" data-action="close-confirm-dialog" data-tooltip="关闭" aria-label="关闭">
<span class="material-symbols-outlined">close</span>
${renderIcon('close')}
</button>
<div class="app-modal__header">
<div class="app-modal__title">${escapeHtml(dialog.title || '请确认')}</div>
Expand Down Expand Up @@ -476,23 +550,23 @@ function renderPreviewActions({ mode = 'slider', isExpanded = false, canSave = f
const fullscreenButton = mode === 'split'
? `
<button class="preview-modal__close" data-action="toggle-preview-compare-fullscreen" data-tooltip="${isExpanded ? '\u7f29\u5c0f\u663e\u793a' : '\u5168\u5c4f\u663e\u793a'}" aria-label="${isExpanded ? '\u7f29\u5c0f\u663e\u793a' : '\u5168\u5c4f\u663e\u793a'}">
<span class="material-symbols-outlined">${isExpanded ? 'fullscreen_exit' : 'fullscreen'}</span>
${renderIcon(isExpanded ? 'fullscreen_exit' : 'fullscreen')}
</button>`
: ''
const saveButton = canSave
? `
<button class="preview-modal__close" data-action="save-preview-result" data-tooltip="保存当前预览" aria-label="保存当前预览">
<span class="material-symbols-outlined">save</span>
${renderIcon('save')}
</button>`
: ''
return `
<div class="preview-modal__actions">
${saveButton}
<button class="preview-modal__close" data-action="toggle-preview-help" data-tooltip="查看操作说明" aria-label="查看操作说明">
<span class="material-symbols-outlined">help</span>
${renderIcon('help')}
</button>${fullscreenButton}
<button class="preview-modal__close" data-action="close-preview-modal" data-tooltip="\u5173\u95ed" aria-label="\u5173\u95ed">
<span class="material-symbols-outlined">close</span>
${renderIcon('close')}
</button>
</div>`
}
Expand Down
Loading