init
This commit is contained in:
commit
be88621c9e
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
test/
|
||||
output/
|
||||
node_modules/
|
61
bin/cli.js
Executable file
61
bin/cli.js
Executable 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
162
index.js
Normal 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
31
package.json
Normal 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
78
readme.md
Normal 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
0
readme_en.md
Normal file
BIN
wasm/convert_image_to_webp.wasm
Executable file
BIN
wasm/convert_image_to_webp.wasm
Executable file
Binary file not shown.
8
yarn.lock
Normal file
8
yarn.lock
Normal 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==
|
Loading…
Reference in New Issue
Block a user