This commit is contained in:
kura 2024-12-20 23:23:13 +08:00
commit be88621c9e
9 changed files with 343 additions and 0 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
test/
output/
node_modules/

61
bin/cli.js Executable file
View File

@ -0,0 +1,61 @@
#!/usr/bin/env node
import { processImages } from '../index.js';
import { program } from 'commander'; // 引入 commander
import path from 'path';
// 添加命令行参数
program
//中文
// .option('-i, --input <path>', '指定输入目录', './')
// .option('-o, --output <path>', '指定输出目录', './output')
// .option('-q, --quality <number>', '指定压缩质量 1-100', 60) // 默认质量为60
// .option('-r, --recursive <number>', '是否递归处理子目录,递归几层', 0)
// .option('-a, --alpha <number>', '是否保留透明通道', 1)
// .option('-h, --help', '显示帮助信息')
// .parse(process.argv);
//english
.option('-i, --input <path>', 'Specify the input directory', './')
.option('-o, --output <path>', 'Specify the output directory', './')
.option('-q, --quality <number>', 'Specify the compression quality 1-100', 60) // 默认质量为60
.option('-r, --recursive <number>', 'Whether to recursively process subdirectories, how many layers to recurse', 0)
.option('-a, --alpha <number>', 'Whether to keep the transparent channel', 1)
.option('-h, --help', 'Display help information')
.parse(process.argv);
const options = program.opts(); // 获取命令行选项
if (options.help) {
program.help();
}
//判断参数是否正确
if (options.quality < 1 || options.quality > 100) {
console.error('Quality parameter must be between 1 and 100');
process.exit(1);
}
if (options.recursive < 0) {
console.error('Recursive parameter must be greater than 0');
process.exit(1);
}
if (options.input === '') {
options.input = './';
}
if (options.output === '') {
options.output = './output';
}
if (options.alpha === '') {
options.alpha = 1;
}
if (options.recursive === '') {
options.recursive = 0;
}
//recursive最深3层 避免递归过深 出现意外
if (options.recursive > 5) {
options.recursive = 5;
console.error('depth of recursive is too deep, automatically set to 5');
}
if (options.alpha != 1 && options.alpha != 0) {
options.alpha = 1;
console.error('alpha parameter error, automatically set to 1, keep alpha');
}
// 输出目录 加上output
options.output = path.join(options.output, 'output');
processImages(options);

162
index.js Normal file
View File

@ -0,0 +1,162 @@
import { readdir, existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
import { join } from 'path';
import { fileURLToPath } from 'url';
import { dirname, resolve } from 'path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const wasmImports = {
/** @export */
__assert_fail: (condition, filename, line, func) => { console.log(condition, filename, line, func) },
/** @export */
emscripten_resize_heap: (size, old_size) => { console.log(size, old_size) },
/** @export */
fd_close: (fd) => { console.log(fd) },
/** @export */
fd_seek: (fd, offset, whence) => { console.log(fd, offset, whence) },
/** @export */
fd_write: (fd, buf, len, pos) => { console.log(fd, buf, len, pos) },
emscripten_memcpy_js: (dest, src, len) => { Module.HEAPU8.copyWithin(dest, src, src + len); },
}
var Module;
// 初始化 WASM 模块
async function initWasm() {
if (Module) {
return;
}
const wasmBinary = readFileSync(join(__dirname, './wasm/convert_image_to_webp.wasm'));
const info = {
'env': wasmImports,
'wasi_snapshot_preview1': wasmImports,
};
await WebAssembly.instantiate(wasmBinary, info).then(result => {
// console.log('wasmModule ok');
Module = {
...result.instance.exports,
HEAPU8: new Uint8Array(result.instance.exports.memory.buffer),
getValue: (ptr) => { return Module.HEAPU8[ptr] },
};
// processImages();
});
}
export function processImages(options) {
initWasm().then(async () => {
convertCount = 0;
failCount = 0;
await processDirectory(options.input, options.output, options);
//打印结束
console.log('Compression completed.');
console.log(`Converted: ${convertCount} Failed: ${failCount}`);
});
}
//转换个数
let convertCount = 0;
//失败个数
let failCount = 0;
// 新增处理目录的递归函数
function processDirectory(inputDir, outputDir, options, currentDepth = 0) {
return new Promise((resolve, reject) => {
readdir(inputDir, { withFileTypes: true }, async (err, entries) => {
if (err) {
console.log('Failed to scan directory: ' + err);
reject(err);
return;
}
try {
// 处理子目录
if (options.recursive && currentDepth < options.recursive) {
const dirPromises = entries
.filter(entry => entry.isDirectory())
.map(entry => {
const newInputPath = join(inputDir, entry.name);
if (resolve(newInputPath) === resolve(options.output)) {
return Promise.resolve();
}
const newOutputPath = join(outputDir, entry.name);
return processDirectory(newInputPath, newOutputPath, options, currentDepth + 1);
});
await Promise.all(dirPromises);
}
// 处理图片文件
const imageFiles = entries
.filter(entry => entry.isFile() && /\.(jpg|jpeg|png|gif|tga|bmp|psd|gif|hdr|pic)$/i.test(entry.name))
.map(entry => entry.name);
let completedFiles = 0;
let totalFiles = imageFiles.length;
// 压缩图片文件
imageFiles.forEach(file => {
const inputPath = join(inputDir, file);
const outputPath = join(outputDir, `${file.split('.')[0]}.webp`);
try {
// 读取图片文件
const inputBuffer = readFileSync(inputPath);
// 转换为 Uint8Array
const inputData = new Uint8Array(inputBuffer);
// 分配内存
const outputSizePtr = Module.malloc(4);
const inputDataPtr = Module.malloc(inputData.length);
Module.HEAPU8.set(inputData, inputDataPtr);
// 调用 WASM 函数进行转换(使用命令行指定的质量)
const webpPtr = Module.convert_image_to_webp(
inputDataPtr,
inputData.length,
0,
0,
options.quality, // 使用命令行指定的质量
outputSizePtr,
options.alpha ? 1 : 0
);
// 获取输出大小
const outputSize = Module.HEAPU8[outputSizePtr] |
(Module.HEAPU8[outputSizePtr + 1] << 8) |
(Module.HEAPU8[outputSizePtr + 2] << 16) |
(Module.HEAPU8[outputSizePtr + 3] << 24);;
// 获取 WebP 数据
const webpData = Buffer.from(
Module.HEAPU8.subarray(webpPtr, webpPtr + outputSize)
);
// 释放内存
Module.free(outputSizePtr);
Module.free(webpPtr);
Module.free(inputDataPtr);
// 保存文件
if (!existsSync(outputDir)) {
mkdirSync(outputDir, { recursive: true });
}
writeFileSync(outputPath, webpData);
// 更新进度
completedFiles++;
const originalSize = (inputBuffer.length / 1024).toFixed(2);
const compressedSize = (outputSize / 1024).toFixed(2);
console.log(`Compressed: ${file} ( - ${completedFiles}/${totalFiles})`);
console.log(`Compression ratio: ${originalSize}KB => ${compressedSize}KB =>${((compressedSize / originalSize) * 100).toFixed(2)}%`);
convertCount++;
} catch (error) {
console.error(`Error processing file ${file}:`, error);
failCount++;
}
});
resolve();
} catch (error) {
reject(error);
}
});
});
}

31
package.json Normal file
View File

@ -0,0 +1,31 @@
{
"name": "img2webp-cli",
"version": "1.0.0",
"description": "Convert images to WebP format using WASM in cli",
"type": "module",
"bin": {
"cp2webp": "./bin/cli.js"
},
"scripts": {
"test": "node bin/cli.js -i ./test -o ./output",
"goGlobal": "npm link",
"testGlobal": "cp2webp -i /Users/kura/Downloads/PC端图标 -o /Users/kura/Downloads/PC端图标/output -r 2"
},
"files": [
"bin",
"wasm"
],
"keywords": [
"image",
"webp",
"compression",
"wasm",
"convert",
"cli"
],
"author": "kuraa",
"license": "MIT",
"dependencies": {
"commander": "^12.1.0"
}
}

78
readme.md Normal file
View File

@ -0,0 +1,78 @@
# img2webp-cli
用于将当前目录下所有图像转换为 WebP 格式的命令行工具,带有递归处理子目录与压缩质量选择的功能,使用 WASM 进行处理。
[English](./readme_en.md)
[中文](./readme.md)
## 安装
npm:
```bash
npm install -g img2webp-cli
```
yarn:
```bash
yarn global add img2webp-cli
```
## 使用
* 快捷使用:
在图片目录下执行:
```bash
cp2webp
```
* 自定义使用:
```bash
cp2webp -i <input_directory> -o <output_directory> -q <quality> -r <recursive> -a <alpha>
```
### 命令行选项
- `-i, --input <path>`: 指定输入目录,默认为当前目录 `./`
- `-o, --output <path>`: 指定输出目录,默认为 `./output`
- `-q, --quality <number>`: 指定压缩质量,范围为 1-100默认为 60
- `-r, --recursive <number>`: 是否递归处理子目录,默认为 0不递归
- `-a, --alpha <number>`: 是否保留透明通道1 表示保留0 表示不保留,默认为 1
- `-h, --help`: 显示帮助信息
## 示例
`/path/to/input` 目录中的图像转换为 WebP 格式,并将结果输出到 `/path/to/output` 目录:
1. 进入图片目录
```bash
cd /path/to/input
```
2. 执行命令
```bash
cp2webp
```
or
任意目录下执行
```bash
cp2webp -i /path/to/input -o /path/to/output -q 60 -r 1 -a 1
```
## 注意事项
- 该工具支持的图像格式包括 JPG, PNG, TGA, BMP, PSD, GIF, HDR, PIC 等常见图片格式(具体支持格式请查看[stb_image](https://github.com/nothings/stb))
- 默认情况下,输出目录会在指定的输出路径下创建一个名为 `output` 的子目录。
- 递归处理的最大深度限制为 5 层,避免递归过深,出现意外。
## 感谢
- [stb_image](https://github.com/nothings/stb)
- [libwebp](https://chromium.googlesource.com/webm/libwebp)
wasm打包了[stb_image](https://github.com/nothings/stb)与[libwebp](https://chromium.googlesource.com/webm/libwebp),感谢作者与团队。
## 许可证
MIT

0
readme_en.md Normal file
View File

BIN
wasm/convert_image_to_webp.wasm Executable file

Binary file not shown.

8
yarn.lock Normal file
View File

@ -0,0 +1,8 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
commander@^12.1.0:
version "12.1.0"
resolved "https://registry.npmmirror.com/commander/-/commander-12.1.0.tgz#01423b36f501259fdaac4d0e4d60c96c991585d3"
integrity sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==