lazywebp 1.3.0
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/LICENSE +21 -0
- package/README.md +150 -0
- package/dist/__tests__/cli.test.d.ts +2 -0
- package/dist/__tests__/cli.test.d.ts.map +1 -0
- package/dist/__tests__/cli.test.js +98 -0
- package/dist/__tests__/cli.test.js.map +1 -0
- package/dist/__tests__/converter.test.d.ts +2 -0
- package/dist/__tests__/converter.test.d.ts.map +1 -0
- package/dist/__tests__/converter.test.js +188 -0
- package/dist/__tests__/converter.test.js.map +1 -0
- package/dist/converter.d.ts +21 -0
- package/dist/converter.d.ts.map +1 -0
- package/dist/converter.js +278 -0
- package/dist/converter.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +117 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +46 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/utils.d.ts +7 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +35 -0
- package/dist/utils.js.map +1 -0
- package/package.json +61 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Keiver Hernandez
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# towebp - Lazy webp converter
|
|
2
|
+
|
|
3
|
+
<p align="center">
|
|
4
|
+
<img src="assets/lazywebp-towebp-start.webp" alt="Lazy Webp — drop zone" width="420" />
|
|
5
|
+
</p>
|
|
6
|
+
|
|
7
|
+
CLI tool and native macOS app to batch convert images to WebP format. Handles single files, multiple files, and entire directories with concurrent processing, atomic writes, and mtime-based skip logic.
|
|
8
|
+
|
|
9
|
+
## Quick Start
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npx towebp photo.png
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Converts `photo.png` to `photo.webp` in the same directory.
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
**Global** (adds `towebp` to your PATH):
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm install -g towebp
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
**Run without installing** (via npx):
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npx towebp photo.png
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
**From source:**
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
git clone https://github.com/keiver/towebp.git
|
|
35
|
+
cd towebp
|
|
36
|
+
npm install && npm run build && npm link
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Usage
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
# Convert a single file (output next to source)
|
|
43
|
+
towebp photo.png
|
|
44
|
+
|
|
45
|
+
# Convert multiple files at once
|
|
46
|
+
towebp photo.png banner.jpg logo.gif
|
|
47
|
+
|
|
48
|
+
# Convert all images in a directory
|
|
49
|
+
towebp images/
|
|
50
|
+
|
|
51
|
+
# Convert to a separate output directory
|
|
52
|
+
towebp -o output/ images/
|
|
53
|
+
|
|
54
|
+
# Custom quality (1-100, default: 90)
|
|
55
|
+
towebp -q 80 photo.png
|
|
56
|
+
|
|
57
|
+
# Recursive subdirectory processing
|
|
58
|
+
towebp -r images/
|
|
59
|
+
|
|
60
|
+
# Combine flags
|
|
61
|
+
towebp -q 75 -r -o dist/ src/assets/
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Options
|
|
65
|
+
|
|
66
|
+
| Flag | Description |
|
|
67
|
+
|---|---|
|
|
68
|
+
| `-q, --quality <n>` | WebP quality 1-100 (default: 90) |
|
|
69
|
+
| `-o, --output <dir>` | Output directory (default: next to source) |
|
|
70
|
+
| `-r, --recursive` | Process subdirectories recursively |
|
|
71
|
+
| `-h, --help` | Show help message |
|
|
72
|
+
| `-v, --version` | Show version number |
|
|
73
|
+
|
|
74
|
+
## Features
|
|
75
|
+
|
|
76
|
+
- Accepts multiple input files and directories in a single command
|
|
77
|
+
- Skips files that haven't changed (compares mtime)
|
|
78
|
+
- Gracefully skips non-image files with a warning
|
|
79
|
+
- Atomic writes via temp file + rename
|
|
80
|
+
- Concurrent processing (up to 4 workers)
|
|
81
|
+
- Color space conversion (display-p3 / RGB to sRGB)
|
|
82
|
+
- Auto-rotation based on EXIF data
|
|
83
|
+
- Recursive subdirectory support with mirrored output structure
|
|
84
|
+
|
|
85
|
+
## Supported Formats
|
|
86
|
+
|
|
87
|
+
JPG, JPEG, PNG, GIF, BMP, TIFF, WebP
|
|
88
|
+
|
|
89
|
+
## macOS App — Lazy Webp
|
|
90
|
+
|
|
91
|
+
Native SwiftUI app that wraps the CLI. Drag and drop images or folders to convert them — output is generated next to each source file.
|
|
92
|
+
|
|
93
|
+
<p align="center">
|
|
94
|
+
<img src="assets/lazywebp-towebp-folder.png.webp" alt="Lazy Webp — folder batch conversion" width="400" />
|
|
95
|
+
</p>
|
|
96
|
+
|
|
97
|
+
### Requirements
|
|
98
|
+
|
|
99
|
+
- macOS 14+
|
|
100
|
+
- Swift 6.2+
|
|
101
|
+
- The `towebp` CLI must be installed and available in your PATH
|
|
102
|
+
|
|
103
|
+
### Run in development
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
cd app
|
|
107
|
+
swift run
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Install to /Applications
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
cd app
|
|
114
|
+
./install-app.sh
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
This builds a release binary, creates an app bundle at `/Applications/Lazy Webp.app` (`dev.keiver.lazywebp`), and signs it locally.
|
|
118
|
+
|
|
119
|
+
### App Features
|
|
120
|
+
|
|
121
|
+
- Drag-and-drop files and folders
|
|
122
|
+
- File picker dialog
|
|
123
|
+
- Quality slider (1-100)
|
|
124
|
+
- Per-file size and savings display
|
|
125
|
+
- Live progress tracking with cancel support
|
|
126
|
+
- Always-on-top floating window
|
|
127
|
+
- Menu bar icon with quick access
|
|
128
|
+
- Launch at login option
|
|
129
|
+
- Install to /Applications from the menu bar
|
|
130
|
+
|
|
131
|
+
## Requirements
|
|
132
|
+
|
|
133
|
+
| Component | Requires |
|
|
134
|
+
|---|---|
|
|
135
|
+
| CLI | Node.js 18+, Sharp |
|
|
136
|
+
| macOS App | macOS 14+, Swift 6.2+ |
|
|
137
|
+
|
|
138
|
+
## npm Scripts
|
|
139
|
+
|
|
140
|
+
| Script | Description |
|
|
141
|
+
|---|---|
|
|
142
|
+
| `npm run dev` | Run directly from TypeScript source |
|
|
143
|
+
| `npm run build` | Compile to JavaScript in `dist/` |
|
|
144
|
+
| `npm start` | Run compiled build |
|
|
145
|
+
| `npm test` | Run tests |
|
|
146
|
+
| `npm run test:watch` | Run tests in watch mode |
|
|
147
|
+
|
|
148
|
+
## License
|
|
149
|
+
|
|
150
|
+
MIT
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/cli.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { execFile } from "node:child_process";
|
|
3
|
+
import { promisify } from "node:util";
|
|
4
|
+
import fs from "node:fs/promises";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import os from "node:os";
|
|
7
|
+
import sharp from "sharp";
|
|
8
|
+
const exec = promisify(execFile);
|
|
9
|
+
// Use tsx to run the TypeScript source directly
|
|
10
|
+
const CLI = path.resolve("src/index.ts");
|
|
11
|
+
const TSX = path.resolve("node_modules/.bin/tsx");
|
|
12
|
+
function tmpDir() {
|
|
13
|
+
return path.join(os.tmpdir(), `towebp-cli-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
14
|
+
}
|
|
15
|
+
async function createTestPng(filePath) {
|
|
16
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
17
|
+
await sharp({
|
|
18
|
+
create: { width: 10, height: 10, channels: 3, background: { r: 255, g: 0, b: 0 } },
|
|
19
|
+
})
|
|
20
|
+
.png()
|
|
21
|
+
.toFile(filePath);
|
|
22
|
+
}
|
|
23
|
+
async function cleanup(dir) {
|
|
24
|
+
try {
|
|
25
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
// ignore
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
describe("CLI", () => {
|
|
32
|
+
let workDir;
|
|
33
|
+
beforeEach(async () => {
|
|
34
|
+
workDir = tmpDir();
|
|
35
|
+
await fs.mkdir(workDir, { recursive: true });
|
|
36
|
+
});
|
|
37
|
+
afterEach(async () => {
|
|
38
|
+
await cleanup(workDir);
|
|
39
|
+
});
|
|
40
|
+
it("prints help with --help", async () => {
|
|
41
|
+
const { stdout } = await exec(TSX, [CLI, "--help"]);
|
|
42
|
+
expect(stdout).toContain("towebp");
|
|
43
|
+
expect(stdout).toContain("Usage:");
|
|
44
|
+
expect(stdout).toContain("--quality");
|
|
45
|
+
});
|
|
46
|
+
it("prints version with --version", async () => {
|
|
47
|
+
const { stdout } = await exec(TSX, [CLI, "--version"]);
|
|
48
|
+
expect(stdout.trim()).toMatch(/^\d+\.\d+\.\d+$/);
|
|
49
|
+
});
|
|
50
|
+
it("exits with error on no arguments", async () => {
|
|
51
|
+
try {
|
|
52
|
+
await exec(TSX, [CLI]);
|
|
53
|
+
expect.fail("should have thrown");
|
|
54
|
+
}
|
|
55
|
+
catch (err) {
|
|
56
|
+
const error = err;
|
|
57
|
+
expect(error.code).toBe(1);
|
|
58
|
+
expect(error.stderr).toContain("no input");
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
it("converts a single file via CLI", async () => {
|
|
62
|
+
const inputPath = path.join(workDir, "cli-test.png");
|
|
63
|
+
await createTestPng(inputPath);
|
|
64
|
+
const { stdout } = await exec(TSX, [CLI, inputPath]);
|
|
65
|
+
expect(stdout).toContain("Processed:");
|
|
66
|
+
const stat = await fs.stat(path.join(workDir, "cli-test.webp"));
|
|
67
|
+
expect(stat.size).toBeGreaterThan(0);
|
|
68
|
+
});
|
|
69
|
+
it("converts a directory via CLI", async () => {
|
|
70
|
+
await createTestPng(path.join(workDir, "a.png"));
|
|
71
|
+
await createTestPng(path.join(workDir, "b.png"));
|
|
72
|
+
const { stdout } = await exec(TSX, [CLI, workDir]);
|
|
73
|
+
expect(stdout).toContain("Total files:");
|
|
74
|
+
expect(stdout).toContain("2");
|
|
75
|
+
});
|
|
76
|
+
it("exits with error on invalid input", async () => {
|
|
77
|
+
try {
|
|
78
|
+
await exec(TSX, [CLI, "/nonexistent/path"]);
|
|
79
|
+
expect.fail("should have thrown");
|
|
80
|
+
}
|
|
81
|
+
catch (err) {
|
|
82
|
+
const error = err;
|
|
83
|
+
expect(error.code).toBe(1);
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
it("exits with error on unknown flag", async () => {
|
|
87
|
+
try {
|
|
88
|
+
await exec(TSX, [CLI, "--badopt"]);
|
|
89
|
+
expect.fail("should have thrown");
|
|
90
|
+
}
|
|
91
|
+
catch (err) {
|
|
92
|
+
const error = err;
|
|
93
|
+
expect(error.code).toBe(1);
|
|
94
|
+
expect(error.stderr).toContain("unknown option");
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
//# sourceMappingURL=cli.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.test.js","sourceRoot":"","sources":["../../src/__tests__/cli.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"converter.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/converter.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { ImageConverter } from "../converter.js";
|
|
3
|
+
import sharp from "sharp";
|
|
4
|
+
import fs from "node:fs/promises";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import os from "node:os";
|
|
7
|
+
function tmpDir() {
|
|
8
|
+
return path.join(os.tmpdir(), `towebp-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
9
|
+
}
|
|
10
|
+
async function createTestImage(filePath, format = "png", width = 10, height = 10) {
|
|
11
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
12
|
+
const background = format === "png" ? { r: 255, g: 0, b: 0 } : { r: 0, g: 255, b: 0 };
|
|
13
|
+
await sharp({
|
|
14
|
+
create: { width, height, channels: 3, background },
|
|
15
|
+
})
|
|
16
|
+
.toFormat(format)
|
|
17
|
+
.toFile(filePath);
|
|
18
|
+
}
|
|
19
|
+
async function cleanup(dir) {
|
|
20
|
+
try {
|
|
21
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
// ignore
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
describe("ImageConverter", () => {
|
|
28
|
+
let workDir;
|
|
29
|
+
beforeEach(async () => {
|
|
30
|
+
workDir = tmpDir();
|
|
31
|
+
await fs.mkdir(workDir, { recursive: true });
|
|
32
|
+
});
|
|
33
|
+
afterEach(async () => {
|
|
34
|
+
await cleanup(workDir);
|
|
35
|
+
});
|
|
36
|
+
describe("single file conversion", () => {
|
|
37
|
+
it("converts a PNG to WebP next to the source", async () => {
|
|
38
|
+
const inputPath = path.join(workDir, "test.png");
|
|
39
|
+
await createTestImage(inputPath);
|
|
40
|
+
const converter = new ImageConverter();
|
|
41
|
+
const result = await converter.run(inputPath);
|
|
42
|
+
expect(result.processed).toBe(1);
|
|
43
|
+
expect(result.skipped).toBe(0);
|
|
44
|
+
expect(result.failed.length).toBe(0);
|
|
45
|
+
const outputPath = path.join(workDir, "test.webp");
|
|
46
|
+
const stat = await fs.stat(outputPath);
|
|
47
|
+
expect(stat.size).toBeGreaterThan(0);
|
|
48
|
+
});
|
|
49
|
+
it("converts a JPG to WebP in a separate output directory", async () => {
|
|
50
|
+
const inputPath = path.join(workDir, "photo.jpg");
|
|
51
|
+
const outDir = path.join(workDir, "out");
|
|
52
|
+
await createTestImage(inputPath, "jpeg");
|
|
53
|
+
const converter = new ImageConverter();
|
|
54
|
+
const result = await converter.run(inputPath, outDir);
|
|
55
|
+
expect(result.processed).toBe(1);
|
|
56
|
+
const outputPath = path.join(outDir, "photo.webp");
|
|
57
|
+
const stat = await fs.stat(outputPath);
|
|
58
|
+
expect(stat.size).toBeGreaterThan(0);
|
|
59
|
+
});
|
|
60
|
+
it("skips when source is .webp and output would be same path", async () => {
|
|
61
|
+
const inputPath = path.join(workDir, "already.webp");
|
|
62
|
+
// Create a small webp
|
|
63
|
+
await sharp({
|
|
64
|
+
create: { width: 10, height: 10, channels: 3, background: { r: 0, g: 0, b: 255 } },
|
|
65
|
+
})
|
|
66
|
+
.webp()
|
|
67
|
+
.toFile(inputPath);
|
|
68
|
+
const converter = new ImageConverter();
|
|
69
|
+
const result = await converter.run(inputPath);
|
|
70
|
+
expect(result.skipped).toBe(1);
|
|
71
|
+
expect(result.processed).toBe(0);
|
|
72
|
+
});
|
|
73
|
+
it("skips non-image files", async () => {
|
|
74
|
+
const inputPath = path.join(workDir, "readme.txt");
|
|
75
|
+
await fs.writeFile(inputPath, "hello");
|
|
76
|
+
const converter = new ImageConverter();
|
|
77
|
+
const result = await converter.run(inputPath);
|
|
78
|
+
expect(result.totalFiles).toBe(1);
|
|
79
|
+
expect(result.skipped).toBe(1);
|
|
80
|
+
expect(result.processed).toBe(0);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
describe("directory conversion", () => {
|
|
84
|
+
it("converts all images in a directory (same-dir output)", async () => {
|
|
85
|
+
await createTestImage(path.join(workDir, "a.png"));
|
|
86
|
+
await createTestImage(path.join(workDir, "b.jpg"), "jpeg");
|
|
87
|
+
const converter = new ImageConverter();
|
|
88
|
+
const result = await converter.run(workDir);
|
|
89
|
+
expect(result.totalFiles).toBe(2);
|
|
90
|
+
expect(result.processed).toBe(2);
|
|
91
|
+
const aWebp = await fs.stat(path.join(workDir, "a.webp"));
|
|
92
|
+
const bWebp = await fs.stat(path.join(workDir, "b.webp"));
|
|
93
|
+
expect(aWebp.size).toBeGreaterThan(0);
|
|
94
|
+
expect(bWebp.size).toBeGreaterThan(0);
|
|
95
|
+
});
|
|
96
|
+
it("converts directory to separate output directory", async () => {
|
|
97
|
+
await createTestImage(path.join(workDir, "c.png"));
|
|
98
|
+
const outDir = path.join(workDir, "output");
|
|
99
|
+
const converter = new ImageConverter();
|
|
100
|
+
const result = await converter.run(workDir, outDir);
|
|
101
|
+
expect(result.processed).toBe(1);
|
|
102
|
+
const stat = await fs.stat(path.join(outDir, "c.webp"));
|
|
103
|
+
expect(stat.size).toBeGreaterThan(0);
|
|
104
|
+
});
|
|
105
|
+
it("skips unchanged files on second run", async () => {
|
|
106
|
+
await createTestImage(path.join(workDir, "d.png"));
|
|
107
|
+
const outDir = path.join(workDir, "output");
|
|
108
|
+
const converter1 = new ImageConverter();
|
|
109
|
+
await converter1.run(workDir, outDir);
|
|
110
|
+
const converter2 = new ImageConverter();
|
|
111
|
+
const result = await converter2.run(workDir, outDir);
|
|
112
|
+
expect(result.skipped).toBe(1);
|
|
113
|
+
expect(result.processed).toBe(0);
|
|
114
|
+
});
|
|
115
|
+
it("throws on empty directory", async () => {
|
|
116
|
+
const emptyDir = path.join(workDir, "empty");
|
|
117
|
+
await fs.mkdir(emptyDir, { recursive: true });
|
|
118
|
+
const converter = new ImageConverter();
|
|
119
|
+
await expect(converter.run(emptyDir)).rejects.toThrow("No valid image files found");
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
describe("recursive mode", () => {
|
|
123
|
+
it("processes images in subdirectories", async () => {
|
|
124
|
+
await createTestImage(path.join(workDir, "top.png"));
|
|
125
|
+
await createTestImage(path.join(workDir, "sub", "nested.png"));
|
|
126
|
+
const converter = new ImageConverter();
|
|
127
|
+
const result = await converter.run(workDir, undefined, true);
|
|
128
|
+
expect(result.totalFiles).toBe(2);
|
|
129
|
+
expect(result.processed).toBe(2);
|
|
130
|
+
const topWebp = await fs.stat(path.join(workDir, "top.webp"));
|
|
131
|
+
const nestedWebp = await fs.stat(path.join(workDir, "sub", "nested.webp"));
|
|
132
|
+
expect(topWebp.size).toBeGreaterThan(0);
|
|
133
|
+
expect(nestedWebp.size).toBeGreaterThan(0);
|
|
134
|
+
});
|
|
135
|
+
it("mirrors subdirectory structure in separate output", async () => {
|
|
136
|
+
await createTestImage(path.join(workDir, "a", "deep.png"));
|
|
137
|
+
const outDir = path.join(workDir, "output");
|
|
138
|
+
const converter = new ImageConverter();
|
|
139
|
+
const result = await converter.run(workDir, outDir, true);
|
|
140
|
+
expect(result.processed).toBe(1);
|
|
141
|
+
const stat = await fs.stat(path.join(outDir, "a", "deep.webp"));
|
|
142
|
+
expect(stat.size).toBeGreaterThan(0);
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
describe("quality option", () => {
|
|
146
|
+
it("uses custom quality", async () => {
|
|
147
|
+
const inputPath = path.join(workDir, "q.png");
|
|
148
|
+
// Use a large noisy image so quality difference is measurable
|
|
149
|
+
const size = 500;
|
|
150
|
+
const channels = 3;
|
|
151
|
+
const noise = Buffer.alloc(size * size * channels);
|
|
152
|
+
for (let i = 0; i < noise.length; i++) {
|
|
153
|
+
noise[i] = Math.floor(Math.random() * 256);
|
|
154
|
+
}
|
|
155
|
+
await fs.mkdir(path.dirname(inputPath), { recursive: true });
|
|
156
|
+
await sharp(noise, { raw: { width: size, height: size, channels } })
|
|
157
|
+
.png()
|
|
158
|
+
.toFile(inputPath);
|
|
159
|
+
const converterHigh = new ImageConverter(100);
|
|
160
|
+
await converterHigh.run(inputPath);
|
|
161
|
+
const highSize = (await fs.stat(path.join(workDir, "q.webp"))).size;
|
|
162
|
+
// Remove output for second run
|
|
163
|
+
await fs.unlink(path.join(workDir, "q.webp"));
|
|
164
|
+
const converterLow = new ImageConverter(1);
|
|
165
|
+
await converterLow.run(inputPath);
|
|
166
|
+
const lowSize = (await fs.stat(path.join(workDir, "q.webp"))).size;
|
|
167
|
+
// Lower quality should produce smaller file with noisy content
|
|
168
|
+
expect(lowSize).toBeLessThan(highSize);
|
|
169
|
+
});
|
|
170
|
+
it("clamps quality to valid range", () => {
|
|
171
|
+
const converterLow = new ImageConverter(0);
|
|
172
|
+
expect(converterLow.config.quality).toBe(1);
|
|
173
|
+
const converterHigh = new ImageConverter(200);
|
|
174
|
+
expect(converterHigh.config.quality).toBe(100);
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
describe("error handling", () => {
|
|
178
|
+
it("records failed files without crashing", async () => {
|
|
179
|
+
const inputPath = path.join(workDir, "corrupt.png");
|
|
180
|
+
await fs.writeFile(inputPath, "not a real image");
|
|
181
|
+
const converter = new ImageConverter();
|
|
182
|
+
const result = await converter.run(workDir);
|
|
183
|
+
expect(result.failed.length).toBe(1);
|
|
184
|
+
expect(result.failed[0].file).toBe(path.join(workDir, "corrupt.png"));
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
//# sourceMappingURL=converter.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"converter.test.js","sourceRoot":"","sources":["../../src/__tests__/converter.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAC;AACrE,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AACjD,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAClC,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,MAAM,SAAS,CAAC;AAEzB,SAAS,MAAM;IACb,OAAO,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,MAAM,EAAE,EAAE,eAAe,IAAI,CAAC,GAAG,EAAE,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;AACpG,CAAC;AAED,KAAK,UAAU,eAAe,CAC5B,QAAgB,EAChB,SAAyB,KAAK,EAC9B,KAAK,GAAG,EAAE,EACV,MAAM,GAAG,EAAE;IAEX,MAAM,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC5D,MAAM,UAAU,GAAG,MAAM,KAAK,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC;IACtF,MAAM,KAAK,CAAC;QACV,MAAM,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,EAAE,UAAU,EAAE;KACnD,CAAC;SACC,QAAQ,CAAC,MAAM,CAAC;SAChB,MAAM,CAAC,QAAQ,CAAC,CAAC;AACtB,CAAC;AAED,KAAK,UAAU,OAAO,CAAC,GAAW;IAChC,IAAI,CAAC;QACH,MAAM,EAAE,CAAC,EAAE,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;IACrD,CAAC;IAAC,MAAM,CAAC;QACP,SAAS;IACX,CAAC;AACH,CAAC;AAED,QAAQ,CAAC,gBAAgB,EAAE,GAAG,EAAE;IAC9B,IAAI,OAAe,CAAC;IAEpB,UAAU,CAAC,KAAK,IAAI,EAAE;QACpB,OAAO,GAAG,MAAM,EAAE,CAAC;QACnB,MAAM,EAAE,CAAC,KAAK,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC/C,CAAC,CAAC,CAAC;IAEH,SAAS,CAAC,KAAK,IAAI,EAAE;QACnB,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC;IACzB,CAAC,CAAC,CAAC;IAEH,QAAQ,CAAC,wBAAwB,EAAE,GAAG,EAAE;QACtC,EAAE,CAAC,2CAA2C,EAAE,KAAK,IAAI,EAAE;YACzD,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,UAAU,CAAC,CAAC;YACjD,MAAM,eAAe,CAAC,SAAS,CAAC,CAAC;YAEjC,MAAM,SAAS,GAAG,IAAI,cAAc,EAAE,CAAC;YACvC,MAAM,MAAM,GAAG,MAAM,SAAS,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;YAE9C,MAAM,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;YACjC,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;YAC/B,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;YAErC,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,WAAW,CAAC,CAAC;YACnD,MAAM,IAAI,GAAG,MAAM,EAAE,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;YACvC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC;QACvC,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,uDAAuD,EAAE,KAAK,IAAI,EAAE;YACrE,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,WAAW,CAAC,CAAC;YAClD,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;YACzC,MAAM,eAAe,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;YAEzC,MAAM,SAAS,GAAG,IAAI,cAAc,EAAE,CAAC;YACvC,MAAM,MAAM,GAAG,MAAM,SAAS,CAAC,GAAG,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;YAEtD,MAAM,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;YAEjC,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;YACnD,MAAM,IAAI,GAAG,MAAM,EAAE,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;YACvC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC;QACvC,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,0DAA0D,EAAE,KAAK,IAAI,EAAE;YACxE,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,cAAc,CAAC,CAAC;YACrD,sBAAsB;YACtB,MAAM,KAAK,CAAC;gBACV,MAAM,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,QAAQ,EAAE,CAAC,EAAE,UAAU,EAAE,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,GAAG,EAAE,EAAE;aACnF,CAAC;iBACC,IAAI,EAAE;iBACN,MAAM,CAAC,SAAS,CAAC,CAAC;YAErB,MAAM,SAAS,GAAG,IAAI,cAAc,EAAE,CAAC;YACvC,MAAM,MAAM,GAAG,MAAM,SAAS,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;YAE9C,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;YAC/B,MAAM,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QACnC,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,uBAAuB,EAAE,KAAK,IAAI,EAAE;YACrC,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,YAAY,CAAC,CAAC;YACnD,MAAM,EAAE,CAAC,SAAS,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;YAEvC,MAAM,SAAS,GAAG,IAAI,cAAc,EAAE,CAAC;YACvC,MAAM,MAAM,GAAG,MAAM,SAAS,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;YAC9C,MAAM,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;YAClC,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;YAC/B,MAAM,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QACnC,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,QAAQ,CAAC,sBAAsB,EAAE,GAAG,EAAE;QACpC,EAAE,CAAC,sDAAsD,EAAE,KAAK,IAAI,EAAE;YACpE,MAAM,eAAe,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC;YACnD,MAAM,eAAe,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,CAAC;YAE3D,MAAM,SAAS,GAAG,IAAI,cAAc,EAAE,CAAC;YACvC,MAAM,MAAM,GAAG,MAAM,SAAS,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;YAE5C,MAAM,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;YAClC,MAAM,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;YAEjC,MAAM,KAAK,GAAG,MAAM,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC,CAAC;YAC1D,MAAM,KAAK,GAAG,MAAM,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC,CAAC;YAC1D,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC;YACtC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC;QACxC,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,iDAAiD,EAAE,KAAK,IAAI,EAAE;YAC/D,MAAM,eAAe,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC;YACnD,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;YAE5C,MAAM,SAAS,GAAG,IAAI,cAAc,EAAE,CAAC;YACvC,MAAM,MAAM,GAAG,MAAM,SAAS,CAAC,GAAG,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;YAEpD,MAAM,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;YACjC,MAAM,IAAI,GAAG,MAAM,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC,CAAC;YACxD,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC;QACvC,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,qCAAqC,EAAE,KAAK,IAAI,EAAE;YACnD,MAAM,eAAe,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC;YACnD,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;YAE5C,MAAM,UAAU,GAAG,IAAI,cAAc,EAAE,CAAC;YACxC,MAAM,UAAU,CAAC,GAAG,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;YAEtC,MAAM,UAAU,GAAG,IAAI,cAAc,EAAE,CAAC;YACxC,MAAM,MAAM,GAAG,MAAM,UAAU,CAAC,GAAG,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;YAErD,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;YAC/B,MAAM,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QACnC,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,2BAA2B,EAAE,KAAK,IAAI,EAAE;YACzC,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;YAC7C,MAAM,EAAE,CAAC,KAAK,CAAC,QAAQ,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;YAE9C,MAAM,SAAS,GAAG,IAAI,cAAc,EAAE,CAAC;YACvC,MAAM,MAAM,CAAC,SAAS,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,4BAA4B,CAAC,CAAC;QACtF,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,QAAQ,CAAC,gBAAgB,EAAE,GAAG,EAAE;QAC9B,EAAE,CAAC,oCAAoC,EAAE,KAAK,IAAI,EAAE;YAClD,MAAM,eAAe,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,SAAS,CAAC,CAAC,CAAC;YACrD,MAAM,eAAe,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,KAAK,EAAE,YAAY,CAAC,CAAC,CAAC;YAE/D,MAAM,SAAS,GAAG,IAAI,cAAc,EAAE,CAAC;YACvC,MAAM,MAAM,GAAG,MAAM,SAAS,CAAC,GAAG,CAAC,OAAO,EAAE,SAAS,EAAE,IAAI,CAAC,CAAC;YAE7D,MAAM,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;YAClC,MAAM,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;YAEjC,MAAM,OAAO,GAAG,MAAM,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,UAAU,CAAC,CAAC,CAAC;YAC9D,MAAM,UAAU,GAAG,MAAM,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,KAAK,EAAE,aAAa,CAAC,CAAC,CAAC;YAC3E,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC;YACxC,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC;QAC7C,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,mDAAmD,EAAE,KAAK,IAAI,EAAE;YACjE,MAAM,eAAe,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,GAAG,EAAE,UAAU,CAAC,CAAC,CAAC;YAC3D,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;YAE5C,MAAM,SAAS,GAAG,IAAI,cAAc,EAAE,CAAC;YACvC,MAAM,MAAM,GAAG,MAAM,SAAS,CAAC,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,CAAC;YAE1D,MAAM,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;YACjC,MAAM,IAAI,GAAG,MAAM,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,EAAE,WAAW,CAAC,CAAC,CAAC;YAChE,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC;QACvC,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,QAAQ,CAAC,gBAAgB,EAAE,GAAG,EAAE;QAC9B,EAAE,CAAC,qBAAqB,EAAE,KAAK,IAAI,EAAE;YACnC,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;YAC9C,8DAA8D;YAC9D,MAAM,IAAI,GAAG,GAAG,CAAC;YACjB,MAAM,QAAQ,GAAG,CAAC,CAAC;YACnB,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,IAAI,GAAG,IAAI,GAAG,QAAQ,CAAC,CAAC;YACnD,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;gBACtC,KAAK,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,GAAG,CAAC,CAAC;YAC7C,CAAC;YACD,MAAM,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;YAC7D,MAAM,KAAK,CAAC,KAAK,EAAE,EAAE,GAAG,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE,CAAC;iBACjE,GAAG,EAAE;iBACL,MAAM,CAAC,SAAS,CAAC,CAAC;YAErB,MAAM,aAAa,GAAG,IAAI,cAAc,CAAC,GAAG,CAAC,CAAC;YAC9C,MAAM,aAAa,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;YACnC,MAAM,QAAQ,GAAG,CAAC,MAAM,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;YAEpE,+BAA+B;YAC/B,MAAM,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC,CAAC;YAE9C,MAAM,YAAY,GAAG,IAAI,cAAc,CAAC,CAAC,CAAC,CAAC;YAC3C,MAAM,YAAY,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;YAClC,MAAM,OAAO,GAAG,CAAC,MAAM,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;YAEnE,+DAA+D;YAC/D,MAAM,CAAC,OAAO,CAAC,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC;QACzC,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,+BAA+B,EAAE,GAAG,EAAE;YACvC,MAAM,YAAY,GAAG,IAAI,cAAc,CAAC,CAAC,CAAC,CAAC;YAC3C,MAAM,CAAC,YAAY,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;YAE5C,MAAM,aAAa,GAAG,IAAI,cAAc,CAAC,GAAG,CAAC,CAAC;YAC9C,MAAM,CAAC,aAAa,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACjD,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,QAAQ,CAAC,gBAAgB,EAAE,GAAG,EAAE;QAC9B,EAAE,CAAC,uCAAuC,EAAE,KAAK,IAAI,EAAE;YACrD,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,aAAa,CAAC,CAAC;YACpD,MAAM,EAAE,CAAC,SAAS,CAAC,SAAS,EAAE,kBAAkB,CAAC,CAAC;YAElD,MAAM,SAAS,GAAG,IAAI,cAAc,EAAE,CAAC;YACvC,MAAM,MAAM,GAAG,MAAM,SAAS,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;YAE5C,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;YACrC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,aAAa,CAAC,CAAC,CAAC;QACxE,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ConversionConfig, ConversionStats, ConversionResult, FileConversionResult } from "./types.js";
|
|
2
|
+
export declare class ImageConverter {
|
|
3
|
+
config: ConversionConfig;
|
|
4
|
+
stats: ConversionStats;
|
|
5
|
+
constructor(quality?: number);
|
|
6
|
+
private resetStats;
|
|
7
|
+
run(input: string, outputDir?: string, recursive?: boolean): Promise<ConversionResult>;
|
|
8
|
+
runAll(inputs: string[], outputDir?: string, recursive?: boolean): Promise<ConversionResult>;
|
|
9
|
+
private collectFileTask;
|
|
10
|
+
private collectDirectoryTasks;
|
|
11
|
+
private skipFile;
|
|
12
|
+
private isSameFile;
|
|
13
|
+
private resolveOutputPath;
|
|
14
|
+
private validatePaths;
|
|
15
|
+
private getDirectorySize;
|
|
16
|
+
private shouldProcessImage;
|
|
17
|
+
convertImage(inputPath: string, outputPath: string): Promise<FileConversionResult>;
|
|
18
|
+
private processInBatches;
|
|
19
|
+
private buildResult;
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=converter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"converter.d.ts","sourceRoot":"","sources":["../src/converter.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,gBAAgB,EAAE,eAAe,EAAE,gBAAgB,EAAkB,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAE5H,qBAAa,cAAc;IACzB,MAAM,EAAE,gBAAgB,CAAC;IACzB,KAAK,EAAE,eAAe,CAAC;gBAEX,OAAO,SAAK;IAoBxB,OAAO,CAAC,UAAU;IAaZ,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,EAAE,SAAS,UAAQ,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAIpF,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE,SAAS,CAAC,EAAE,MAAM,EAAE,SAAS,UAAQ,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAgChG,OAAO,CAAC,eAAe;YAgBT,qBAAqB;IAkCnC,OAAO,CAAC,QAAQ;IAMhB,OAAO,CAAC,UAAU;IAQlB,OAAO,CAAC,iBAAiB;YAQX,aAAa;YAkBb,gBAAgB;YAehB,kBAAkB;IAoB1B,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,oBAAoB,CAAC;YAuF1E,gBAAgB;IAwB9B,OAAO,CAAC,WAAW;CAoBpB"}
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import sharp from "sharp";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import fs from "node:fs/promises";
|
|
4
|
+
import fsSync from "node:fs";
|
|
5
|
+
import crypto from "node:crypto";
|
|
6
|
+
import os from "node:os";
|
|
7
|
+
import { formatBytes, formatDuration, getDiskSpace, isImageFile } from "./utils.js";
|
|
8
|
+
export class ImageConverter {
|
|
9
|
+
config;
|
|
10
|
+
stats;
|
|
11
|
+
constructor(quality = 90) {
|
|
12
|
+
this.config = {
|
|
13
|
+
quality: Math.max(1, Math.min(quality, 100)),
|
|
14
|
+
maxConcurrent: Math.max(1, Math.min(os.cpus().length - 1, 4)),
|
|
15
|
+
};
|
|
16
|
+
this.stats = {
|
|
17
|
+
processed: 0,
|
|
18
|
+
skipped: 0,
|
|
19
|
+
failed: [],
|
|
20
|
+
totalFiles: 0,
|
|
21
|
+
totalBytes: 0,
|
|
22
|
+
savedBytes: 0,
|
|
23
|
+
startTime: null,
|
|
24
|
+
endTime: null,
|
|
25
|
+
};
|
|
26
|
+
sharp.cache({ memory: 512 });
|
|
27
|
+
}
|
|
28
|
+
resetStats() {
|
|
29
|
+
this.stats = {
|
|
30
|
+
processed: 0,
|
|
31
|
+
skipped: 0,
|
|
32
|
+
failed: [],
|
|
33
|
+
totalFiles: 0,
|
|
34
|
+
totalBytes: 0,
|
|
35
|
+
savedBytes: 0,
|
|
36
|
+
startTime: null,
|
|
37
|
+
endTime: null,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
async run(input, outputDir, recursive = false) {
|
|
41
|
+
return this.runAll([input], outputDir, recursive);
|
|
42
|
+
}
|
|
43
|
+
async runAll(inputs, outputDir, recursive = false) {
|
|
44
|
+
this.resetStats();
|
|
45
|
+
this.stats.startTime = Date.now();
|
|
46
|
+
if (outputDir) {
|
|
47
|
+
await fs.mkdir(outputDir, { recursive: true });
|
|
48
|
+
}
|
|
49
|
+
// Collect all tasks from files and directories
|
|
50
|
+
const tasks = [];
|
|
51
|
+
for (const input of inputs) {
|
|
52
|
+
const stat = await fs.stat(input);
|
|
53
|
+
if (stat.isFile()) {
|
|
54
|
+
this.collectFileTask(input, outputDir, tasks);
|
|
55
|
+
}
|
|
56
|
+
else if (stat.isDirectory()) {
|
|
57
|
+
await this.collectDirectoryTasks(input, outputDir, recursive, tasks);
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
throw new Error(`Input is neither a file nor a directory: ${input}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (this.stats.totalFiles === 0) {
|
|
64
|
+
throw new Error("No valid image files found");
|
|
65
|
+
}
|
|
66
|
+
await this.processInBatches(tasks);
|
|
67
|
+
this.stats.endTime = Date.now();
|
|
68
|
+
return this.buildResult();
|
|
69
|
+
}
|
|
70
|
+
collectFileTask(inputPath, outputDir, tasks) {
|
|
71
|
+
if (!isImageFile(inputPath)) {
|
|
72
|
+
this.skipFile(`Skipping: not a supported image file: ${inputPath}`);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const outputPath = this.resolveOutputPath(inputPath, outputDir);
|
|
76
|
+
if (this.isSameFile(inputPath, outputPath)) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
this.stats.totalFiles++;
|
|
80
|
+
tasks.push({ inputPath, outputPath });
|
|
81
|
+
}
|
|
82
|
+
async collectDirectoryTasks(inputDir, outputDir, recursive, tasks) {
|
|
83
|
+
const sameDir = !outputDir;
|
|
84
|
+
const resolvedOutput = outputDir ?? inputDir;
|
|
85
|
+
if (!sameDir) {
|
|
86
|
+
await this.validatePaths(inputDir, resolvedOutput);
|
|
87
|
+
}
|
|
88
|
+
const entries = recursive
|
|
89
|
+
? await fs.readdir(inputDir, { recursive: true })
|
|
90
|
+
: await fs.readdir(inputDir);
|
|
91
|
+
const imageFiles = entries.filter((entry) => isImageFile(entry));
|
|
92
|
+
for (const file of imageFiles) {
|
|
93
|
+
const inputPath = path.join(inputDir, file);
|
|
94
|
+
let outputPath;
|
|
95
|
+
if (sameDir) {
|
|
96
|
+
outputPath = path.join(path.dirname(inputPath), `${path.parse(file).name}.webp`);
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
const relDir = path.dirname(file);
|
|
100
|
+
outputPath = path.join(resolvedOutput, relDir, `${path.parse(file).name}.webp`);
|
|
101
|
+
}
|
|
102
|
+
if (this.isSameFile(inputPath, outputPath)) {
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
this.stats.totalFiles++;
|
|
106
|
+
tasks.push({ inputPath, outputPath });
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
skipFile(message) {
|
|
110
|
+
console.warn(message);
|
|
111
|
+
this.stats.totalFiles++;
|
|
112
|
+
this.stats.skipped++;
|
|
113
|
+
}
|
|
114
|
+
isSameFile(inputPath, outputPath) {
|
|
115
|
+
if (path.resolve(inputPath) === path.resolve(outputPath)) {
|
|
116
|
+
this.skipFile(`Skipping: source and output are the same file: ${inputPath}`);
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
resolveOutputPath(inputPath, outputDir) {
|
|
122
|
+
const name = path.parse(inputPath).name;
|
|
123
|
+
if (outputDir) {
|
|
124
|
+
return path.join(outputDir, `${name}.webp`);
|
|
125
|
+
}
|
|
126
|
+
return path.join(path.dirname(inputPath), `${name}.webp`);
|
|
127
|
+
}
|
|
128
|
+
async validatePaths(inputDir, outputDir) {
|
|
129
|
+
const inputStats = await fs.stat(inputDir);
|
|
130
|
+
if (!inputStats.isDirectory()) {
|
|
131
|
+
throw new Error("Input path is not a directory");
|
|
132
|
+
}
|
|
133
|
+
await fs.access(inputDir, fsSync.constants.R_OK);
|
|
134
|
+
await fs.mkdir(outputDir, { recursive: true });
|
|
135
|
+
await fs.access(outputDir, fsSync.constants.W_OK);
|
|
136
|
+
const { available } = await getDiskSpace(outputDir);
|
|
137
|
+
const requiredSpace = await this.getDirectorySize(inputDir);
|
|
138
|
+
if (available < requiredSpace * 1.2) {
|
|
139
|
+
throw new Error("Insufficient disk space");
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
async getDirectorySize(dir) {
|
|
143
|
+
const files = await fs.readdir(dir, { recursive: true });
|
|
144
|
+
const sizes = await Promise.all(files.map(async (file) => {
|
|
145
|
+
try {
|
|
146
|
+
const stats = await fs.stat(path.join(dir, file));
|
|
147
|
+
return stats.isFile() ? stats.size : 0;
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
return 0;
|
|
151
|
+
}
|
|
152
|
+
}));
|
|
153
|
+
return sizes.reduce((acc, size) => acc + size, 0);
|
|
154
|
+
}
|
|
155
|
+
async shouldProcessImage(inputPath, outputPath) {
|
|
156
|
+
try {
|
|
157
|
+
const outputExists = await fs
|
|
158
|
+
.access(outputPath)
|
|
159
|
+
.then(() => true)
|
|
160
|
+
.catch(() => false);
|
|
161
|
+
if (!outputExists)
|
|
162
|
+
return true;
|
|
163
|
+
const [inputStats, outputStats] = await Promise.all([
|
|
164
|
+
fs.stat(inputPath),
|
|
165
|
+
fs.stat(outputPath),
|
|
166
|
+
]);
|
|
167
|
+
return inputStats.mtime > outputStats.mtime || outputStats.size === 0;
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
async convertImage(inputPath, outputPath) {
|
|
174
|
+
let tempOutput;
|
|
175
|
+
try {
|
|
176
|
+
const needsProcessing = await this.shouldProcessImage(inputPath, outputPath);
|
|
177
|
+
if (!needsProcessing) {
|
|
178
|
+
this.stats.skipped++;
|
|
179
|
+
return { success: true, skipped: true };
|
|
180
|
+
}
|
|
181
|
+
const inputSize = (await fs.stat(inputPath)).size;
|
|
182
|
+
tempOutput = path.join(path.dirname(outputPath), `.towebp-${crypto.randomBytes(8).toString("hex")}.webp`);
|
|
183
|
+
// Ensure output directory exists (for recursive mode with subdirs)
|
|
184
|
+
await fs.mkdir(path.dirname(outputPath), { recursive: true });
|
|
185
|
+
const pipeline = sharp(inputPath, {
|
|
186
|
+
failOnError: true,
|
|
187
|
+
limitInputPixels: 268402689, // 16384 x 16384
|
|
188
|
+
sequentialRead: true,
|
|
189
|
+
});
|
|
190
|
+
const metadata = await pipeline.metadata();
|
|
191
|
+
let sharpInstance = pipeline.rotate(); // Auto-rotate based on EXIF
|
|
192
|
+
const space = metadata.space;
|
|
193
|
+
if (space === "rgb" || space === "display-p3") {
|
|
194
|
+
sharpInstance = sharpInstance.toColorspace("srgb");
|
|
195
|
+
}
|
|
196
|
+
await sharpInstance
|
|
197
|
+
.toFormat("webp", {
|
|
198
|
+
quality: this.config.quality,
|
|
199
|
+
effort: 6,
|
|
200
|
+
alphaQuality: 100,
|
|
201
|
+
lossless: false,
|
|
202
|
+
smartSubsample: true,
|
|
203
|
+
nearLossless: false,
|
|
204
|
+
})
|
|
205
|
+
.toFile(tempOutput);
|
|
206
|
+
const tempStats = await fs.stat(tempOutput);
|
|
207
|
+
if (tempStats.size === 0) {
|
|
208
|
+
throw new Error("Generated file is empty");
|
|
209
|
+
}
|
|
210
|
+
// Guard against symlink at output path
|
|
211
|
+
try {
|
|
212
|
+
const outputLstat = await fs.lstat(outputPath);
|
|
213
|
+
if (outputLstat.isSymbolicLink()) {
|
|
214
|
+
throw new Error("Output path is a symbolic link — refusing to overwrite");
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
catch (e) {
|
|
218
|
+
if (e.code !== "ENOENT")
|
|
219
|
+
throw e;
|
|
220
|
+
}
|
|
221
|
+
await fs.rename(tempOutput, outputPath);
|
|
222
|
+
this.stats.processed++;
|
|
223
|
+
this.stats.totalBytes += inputSize;
|
|
224
|
+
this.stats.savedBytes += inputSize - tempStats.size;
|
|
225
|
+
return { success: true, skipped: false };
|
|
226
|
+
}
|
|
227
|
+
catch (err) {
|
|
228
|
+
if (tempOutput) {
|
|
229
|
+
try {
|
|
230
|
+
await fs.unlink(tempOutput);
|
|
231
|
+
}
|
|
232
|
+
catch {
|
|
233
|
+
// ignore cleanup errors
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
237
|
+
this.stats.failed.push({
|
|
238
|
+
file: inputPath,
|
|
239
|
+
error: message,
|
|
240
|
+
});
|
|
241
|
+
return { success: false, error: message };
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
async processInBatches(tasks) {
|
|
245
|
+
const results = [];
|
|
246
|
+
for (let i = 0; i < tasks.length; i += this.config.maxConcurrent) {
|
|
247
|
+
const batch = tasks.slice(i, i + this.config.maxConcurrent);
|
|
248
|
+
const batchResults = await Promise.all(batch.map((task) => this.convertImage(task.inputPath, task.outputPath)));
|
|
249
|
+
results.push(...batchResults);
|
|
250
|
+
const progress = (((i + batch.length) / tasks.length) * 100).toFixed(1);
|
|
251
|
+
const savedMB = (this.stats.savedBytes / 1024 / 1024).toFixed(2);
|
|
252
|
+
process.stdout.write(`\rProgress: ${progress}% (${i + batch.length}/${tasks.length}) | Saved: ${savedMB}MB`);
|
|
253
|
+
}
|
|
254
|
+
if (tasks.length > 0) {
|
|
255
|
+
process.stdout.write("\n");
|
|
256
|
+
}
|
|
257
|
+
return results;
|
|
258
|
+
}
|
|
259
|
+
buildResult() {
|
|
260
|
+
const duration = this.stats.startTime && this.stats.endTime
|
|
261
|
+
? formatDuration(this.stats.endTime - this.stats.startTime)
|
|
262
|
+
: "0s";
|
|
263
|
+
const ratio = this.stats.totalBytes > 0
|
|
264
|
+
? ((this.stats.savedBytes / this.stats.totalBytes) * 100).toFixed(2) + "%"
|
|
265
|
+
: "0%";
|
|
266
|
+
return {
|
|
267
|
+
totalFiles: this.stats.totalFiles,
|
|
268
|
+
processed: this.stats.processed,
|
|
269
|
+
skipped: this.stats.skipped,
|
|
270
|
+
failed: this.stats.failed,
|
|
271
|
+
duration,
|
|
272
|
+
totalSize: formatBytes(this.stats.totalBytes),
|
|
273
|
+
savedSize: formatBytes(this.stats.savedBytes),
|
|
274
|
+
compressionRatio: ratio,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
//# sourceMappingURL=converter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"converter.js","sourceRoot":"","sources":["../src/converter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAClC,OAAO,MAAM,MAAM,SAAS,CAAC;AAC7B,OAAO,MAAM,MAAM,aAAa,CAAC;AACjC,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,EAAE,WAAW,EAAE,cAAc,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAGpF,MAAM,OAAO,cAAc;IACzB,MAAM,CAAmB;IACzB,KAAK,CAAkB;IAEvB,YAAY,OAAO,GAAG,EAAE;QACtB,IAAI,CAAC,MAAM,GAAG;YACZ,OAAO,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;YAC5C,aAAa,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC;SAC9D,CAAC;QAEF,IAAI,CAAC,KAAK,GAAG;YACX,SAAS,EAAE,CAAC;YACZ,OAAO,EAAE,CAAC;YACV,MAAM,EAAE,EAAE;YACV,UAAU,EAAE,CAAC;YACb,UAAU,EAAE,CAAC;YACb,UAAU,EAAE,CAAC;YACb,SAAS,EAAE,IAAI;YACf,OAAO,EAAE,IAAI;SACd,CAAC;QAEF,KAAK,CAAC,KAAK,CAAC,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAC;IAC/B,CAAC;IAEO,UAAU;QAChB,IAAI,CAAC,KAAK,GAAG;YACX,SAAS,EAAE,CAAC;YACZ,OAAO,EAAE,CAAC;YACV,MAAM,EAAE,EAAE;YACV,UAAU,EAAE,CAAC;YACb,UAAU,EAAE,CAAC;YACb,UAAU,EAAE,CAAC;YACb,SAAS,EAAE,IAAI;YACf,OAAO,EAAE,IAAI;SACd,CAAC;IACJ,CAAC;IAED,KAAK,CAAC,GAAG,CAAC,KAAa,EAAE,SAAkB,EAAE,SAAS,GAAG,KAAK;QAC5D,OAAO,IAAI,CAAC,MAAM,CAAC,CAAC,KAAK,CAAC,EAAE,SAAS,EAAE,SAAS,CAAC,CAAC;IACpD,CAAC;IAED,KAAK,CAAC,MAAM,CAAC,MAAgB,EAAE,SAAkB,EAAE,SAAS,GAAG,KAAK;QAClE,IAAI,CAAC,UAAU,EAAE,CAAC;QAClB,IAAI,CAAC,KAAK,CAAC,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QAElC,IAAI,SAAS,EAAE,CAAC;YACd,MAAM,EAAE,CAAC,KAAK,CAAC,SAAS,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QACjD,CAAC;QAED,+CAA+C;QAC/C,MAAM,KAAK,GAAqB,EAAE,CAAC;QACnC,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;YAC3B,MAAM,IAAI,GAAG,MAAM,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YAElC,IAAI,IAAI,CAAC,MAAM,EAAE,EAAE,CAAC;gBAClB,IAAI,CAAC,eAAe,CAAC,KAAK,EAAE,SAAS,EAAE,KAAK,CAAC,CAAC;YAChD,CAAC;iBAAM,IAAI,IAAI,CAAC,WAAW,EAAE,EAAE,CAAC;gBAC9B,MAAM,IAAI,CAAC,qBAAqB,CAAC,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,KAAK,CAAC,CAAC;YACvE,CAAC;iBAAM,CAAC;gBACN,MAAM,IAAI,KAAK,CAAC,4CAA4C,KAAK,EAAE,CAAC,CAAC;YACvE,CAAC;QACH,CAAC;QAED,IAAI,IAAI,CAAC,KAAK,CAAC,UAAU,KAAK,CAAC,EAAE,CAAC;YAChC,MAAM,IAAI,KAAK,CAAC,4BAA4B,CAAC,CAAC;QAChD,CAAC;QAED,MAAM,IAAI,CAAC,gBAAgB,CAAC,KAAK,CAAC,CAAC;QAEnC,IAAI,CAAC,KAAK,CAAC,OAAO,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QAChC,OAAO,IAAI,CAAC,WAAW,EAAE,CAAC;IAC5B,CAAC;IAEO,eAAe,CAAC,SAAiB,EAAE,SAA6B,EAAE,KAAuB;QAC/F,IAAI,CAAC,WAAW,CAAC,SAAS,CAAC,EAAE,CAAC;YAC5B,IAAI,CAAC,QAAQ,CAAC,yCAAyC,SAAS,EAAE,CAAC,CAAC;YACpE,OAAO;QACT,CAAC;QAED,MAAM,UAAU,GAAG,IAAI,CAAC,iBAAiB,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC;QAEhE,IAAI,IAAI,CAAC,UAAU,CAAC,SAAS,EAAE,UAAU,CAAC,EAAE,CAAC;YAC3C,OAAO;QACT,CAAC;QAED,IAAI,CAAC,KAAK,CAAC,UAAU,EAAE,CAAC;QACxB,KAAK,CAAC,IAAI,CAAC,EAAE,SAAS,EAAE,UAAU,EAAE,CAAC,CAAC;IACxC,CAAC;IAEO,KAAK,CAAC,qBAAqB,CAAC,QAAgB,EAAE,SAA6B,EAAE,SAAkB,EAAE,KAAuB;QAC9H,MAAM,OAAO,GAAG,CAAC,SAAS,CAAC;QAC3B,MAAM,cAAc,GAAG,SAAS,IAAI,QAAQ,CAAC;QAE7C,IAAI,CAAC,OAAO,EAAE,CAAC;YACb,MAAM,IAAI,CAAC,aAAa,CAAC,QAAQ,EAAE,cAAc,CAAC,CAAC;QACrD,CAAC;QAED,MAAM,OAAO,GAAG,SAAS;YACvB,CAAC,CAAC,MAAM,EAAE,CAAC,OAAO,CAAC,QAAQ,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC;YACjD,CAAC,CAAC,MAAM,EAAE,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;QAE/B,MAAM,UAAU,GAAI,OAAoB,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC,CAAC;QAE/E,KAAK,MAAM,IAAI,IAAI,UAAU,EAAE,CAAC;YAC9B,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;YAE5C,IAAI,UAAkB,CAAC;YACvB,IAAI,OAAO,EAAE,CAAC;gBACZ,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,OAAO,CAAC,CAAC;YACnF,CAAC;iBAAM,CAAC;gBACN,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;gBAClC,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,cAAc,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,OAAO,CAAC,CAAC;YAClF,CAAC;YAED,IAAI,IAAI,CAAC,UAAU,CAAC,SAAS,EAAE,UAAU,CAAC,EAAE,CAAC;gBAC3C,SAAS;YACX,CAAC;YAED,IAAI,CAAC,KAAK,CAAC,UAAU,EAAE,CAAC;YACxB,KAAK,CAAC,IAAI,CAAC,EAAE,SAAS,EAAE,UAAU,EAAE,CAAC,CAAC;QACxC,CAAC;IACH,CAAC;IAEO,QAAQ,CAAC,OAAe;QAC9B,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QACtB,IAAI,CAAC,KAAK,CAAC,UAAU,EAAE,CAAC;QACxB,IAAI,CAAC,KAAK,CAAC,OAAO,EAAE,CAAC;IACvB,CAAC;IAEO,UAAU,CAAC,SAAiB,EAAE,UAAkB;QACtD,IAAI,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,KAAK,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,EAAE,CAAC;YACzD,IAAI,CAAC,QAAQ,CAAC,kDAAkD,SAAS,EAAE,CAAC,CAAC;YAC7E,OAAO,IAAI,CAAC;QACd,CAAC;QACD,OAAO,KAAK,CAAC;IACf,CAAC;IAEO,iBAAiB,CAAC,SAAiB,EAAE,SAAkB;QAC7D,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC;QACxC,IAAI,SAAS,EAAE,CAAC;YACd,OAAO,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,GAAG,IAAI,OAAO,CAAC,CAAC;QAC9C,CAAC;QACD,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,EAAE,GAAG,IAAI,OAAO,CAAC,CAAC;IAC5D,CAAC;IAEO,KAAK,CAAC,aAAa,CAAC,QAAgB,EAAE,SAAiB;QAC7D,MAAM,UAAU,GAAG,MAAM,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QAC3C,IAAI,CAAC,UAAU,CAAC,WAAW,EAAE,EAAE,CAAC;YAC9B,MAAM,IAAI,KAAK,CAAC,+BAA+B,CAAC,CAAC;QACnD,CAAC;QAED,MAAM,EAAE,CAAC,MAAM,CAAC,QAAQ,EAAE,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;QACjD,MAAM,EAAE,CAAC,KAAK,CAAC,SAAS,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAC/C,MAAM,EAAE,CAAC,MAAM,CAAC,SAAS,EAAE,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;QAElD,MAAM,EAAE,SAAS,EAAE,GAAG,MAAM,YAAY,CAAC,SAAS,CAAC,CAAC;QACpD,MAAM,aAAa,GAAG,MAAM,IAAI,CAAC,gBAAgB,CAAC,QAAQ,CAAC,CAAC;QAE5D,IAAI,SAAS,GAAG,aAAa,GAAG,GAAG,EAAE,CAAC;YACpC,MAAM,IAAI,KAAK,CAAC,yBAAyB,CAAC,CAAC;QAC7C,CAAC;IACH,CAAC;IAEO,KAAK,CAAC,gBAAgB,CAAC,GAAW;QACxC,MAAM,KAAK,GAAG,MAAM,EAAE,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QACzD,MAAM,KAAK,GAAG,MAAM,OAAO,CAAC,GAAG,CAC5B,KAAkB,CAAC,GAAG,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE;YACrC,IAAI,CAAC;gBACH,MAAM,KAAK,GAAG,MAAM,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC,CAAC;gBAClD,OAAO,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;YACzC,CAAC;YAAC,MAAM,CAAC;gBACP,OAAO,CAAC,CAAC;YACX,CAAC;QACH,CAAC,CAAC,CACH,CAAC;QACF,OAAO,KAAK,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE,CAAC,GAAG,GAAG,IAAI,EAAE,CAAC,CAAC,CAAC;IACpD,CAAC;IAEO,KAAK,CAAC,kBAAkB,CAAC,SAAiB,EAAE,UAAkB;QACpE,IAAI,CAAC;YACH,MAAM,YAAY,GAAG,MAAM,EAAE;iBAC1B,MAAM,CAAC,UAAU,CAAC;iBAClB,IAAI,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC;iBAChB,KAAK,CAAC,GAAG,EAAE,CAAC,KAAK,CAAC,CAAC;YAEtB,IAAI,CAAC,YAAY;gBAAE,OAAO,IAAI,CAAC;YAE/B,MAAM,CAAC,UAAU,EAAE,WAAW,CAAC,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC;gBAClD,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC;gBAClB,EAAE,CAAC,IAAI,CAAC,UAAU,CAAC;aACpB,CAAC,CAAC;YAEH,OAAO,UAAU,CAAC,KAAK,GAAG,WAAW,CAAC,KAAK,IAAI,WAAW,CAAC,IAAI,KAAK,CAAC,CAAC;QACxE,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC;IAED,KAAK,CAAC,YAAY,CAAC,SAAiB,EAAE,UAAkB;QACtD,IAAI,UAA8B,CAAC;QAEnC,IAAI,CAAC;YACH,MAAM,eAAe,GAAG,MAAM,IAAI,CAAC,kBAAkB,CAAC,SAAS,EAAE,UAAU,CAAC,CAAC;YAE7E,IAAI,CAAC,eAAe,EAAE,CAAC;gBACrB,IAAI,CAAC,KAAK,CAAC,OAAO,EAAE,CAAC;gBACrB,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;YAC1C,CAAC;YAED,MAAM,SAAS,GAAG,CAAC,MAAM,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC;YAElD,UAAU,GAAG,IAAI,CAAC,IAAI,CACpB,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,EACxB,WAAW,MAAM,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,OAAO,CACxD,CAAC;YAEF,mEAAmE;YACnE,MAAM,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;YAE9D,MAAM,QAAQ,GAAG,KAAK,CAAC,SAAS,EAAE;gBAChC,WAAW,EAAE,IAAI;gBACjB,gBAAgB,EAAE,SAAS,EAAE,gBAAgB;gBAC7C,cAAc,EAAE,IAAI;aACrB,CAAC,CAAC;YAEH,MAAM,QAAQ,GAAG,MAAM,QAAQ,CAAC,QAAQ,EAAE,CAAC;YAE3C,IAAI,aAAa,GAAG,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC,4BAA4B;YAEnE,MAAM,KAAK,GAAG,QAAQ,CAAC,KAA2B,CAAC;YACnD,IAAI,KAAK,KAAK,KAAK,IAAI,KAAK,KAAK,YAAY,EAAE,CAAC;gBAC9C,aAAa,GAAG,aAAa,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC;YACrD,CAAC;YAED,MAAM,aAAa;iBAChB,QAAQ,CAAC,MAAM,EAAE;gBAChB,OAAO,EAAE,IAAI,CAAC,MAAM,CAAC,OAAO;gBAC5B,MAAM,EAAE,CAAC;gBACT,YAAY,EAAE,GAAG;gBACjB,QAAQ,EAAE,KAAK;gBACf,cAAc,EAAE,IAAI;gBACpB,YAAY,EAAE,KAAK;aACpB,CAAC;iBACD,MAAM,CAAC,UAAU,CAAC,CAAC;YAEtB,MAAM,SAAS,GAAG,MAAM,EAAE,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;YAC5C,IAAI,SAAS,CAAC,IAAI,KAAK,CAAC,EAAE,CAAC;gBACzB,MAAM,IAAI,KAAK,CAAC,yBAAyB,CAAC,CAAC;YAC7C,CAAC;YAED,uCAAuC;YACvC,IAAI,CAAC;gBACH,MAAM,WAAW,GAAG,MAAM,EAAE,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC;gBAC/C,IAAI,WAAW,CAAC,cAAc,EAAE,EAAE,CAAC;oBACjC,MAAM,IAAI,KAAK,CAAC,wDAAwD,CAAC,CAAC;gBAC5E,CAAC;YACH,CAAC;YAAC,OAAO,CAAC,EAAE,CAAC;gBACX,IAAK,CAA2B,CAAC,IAAI,KAAK,QAAQ;oBAAE,MAAM,CAAC,CAAC;YAC9D,CAAC;YAED,MAAM,EAAE,CAAC,MAAM,CAAC,UAAU,EAAE,UAAU,CAAC,CAAC;YAExC,IAAI,CAAC,KAAK,CAAC,SAAS,EAAE,CAAC;YACvB,IAAI,CAAC,KAAK,CAAC,UAAU,IAAI,SAAS,CAAC;YACnC,IAAI,CAAC,KAAK,CAAC,UAAU,IAAI,SAAS,GAAG,SAAS,CAAC,IAAI,CAAC;YAEpD,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;QAC3C,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAI,UAAU,EAAE,CAAC;gBACf,IAAI,CAAC;oBACH,MAAM,EAAE,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;gBAC9B,CAAC;gBAAC,MAAM,CAAC;oBACP,wBAAwB;gBAC1B,CAAC;YACH,CAAC;YAED,MAAM,OAAO,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YACjE,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC;gBACrB,IAAI,EAAE,SAAS;gBACf,KAAK,EAAE,OAAO;aACf,CAAC,CAAC;YACH,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC;QAC5C,CAAC;IACH,CAAC;IAEO,KAAK,CAAC,gBAAgB,CAAC,KAAuB;QACpD,MAAM,OAAO,GAA2B,EAAE,CAAC;QAE3C,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,IAAI,IAAI,CAAC,MAAM,CAAC,aAAa,EAAE,CAAC;YACjE,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC;YAC5D,MAAM,YAAY,GAAG,MAAM,OAAO,CAAC,GAAG,CACpC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,UAAU,CAAC,CAAC,CACxE,CAAC;YACF,OAAO,CAAC,IAAI,CAAC,GAAG,YAAY,CAAC,CAAC;YAE9B,MAAM,QAAQ,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,MAAM,CAAC,GAAG,KAAK,CAAC,MAAM,CAAC,GAAG,GAAG,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;YACxE,MAAM,OAAO,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,UAAU,GAAG,IAAI,GAAG,IAAI,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;YACjE,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,eAAe,QAAQ,MAAM,CAAC,GAAG,KAAK,CAAC,MAAM,IAAI,KAAK,CAAC,MAAM,cAAc,OAAO,IAAI,CACvF,CAAC;QACJ,CAAC;QAED,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACrB,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QAC7B,CAAC;QAED,OAAO,OAAO,CAAC;IACjB,CAAC;IAEO,WAAW;QACjB,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,SAAS,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO;YACzD,CAAC,CAAC,cAAc,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC;YAC3D,CAAC,CAAC,IAAI,CAAC;QAET,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,UAAU,GAAG,CAAC;YACrC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,UAAU,GAAG,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,GAAG,GAAG,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,GAAG;YAC1E,CAAC,CAAC,IAAI,CAAC;QAET,OAAO;YACL,UAAU,EAAE,IAAI,CAAC,KAAK,CAAC,UAAU;YACjC,SAAS,EAAE,IAAI,CAAC,KAAK,CAAC,SAAS;YAC/B,OAAO,EAAE,IAAI,CAAC,KAAK,CAAC,OAAO;YAC3B,MAAM,EAAE,IAAI,CAAC,KAAK,CAAC,MAAM;YACzB,QAAQ;YACR,SAAS,EAAE,WAAW,CAAC,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC;YAC7C,SAAS,EAAE,WAAW,CAAC,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC;YAC7C,gBAAgB,EAAE,KAAK;SACxB,CAAC;IACJ,CAAC;CACF"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":""}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import { ImageConverter } from "./converter.js";
|
|
4
|
+
const require = createRequire(import.meta.url);
|
|
5
|
+
const { version: VERSION } = require("../package.json");
|
|
6
|
+
const HELP = `
|
|
7
|
+
towebp v${VERSION} — Convert images to WebP format
|
|
8
|
+
|
|
9
|
+
Usage:
|
|
10
|
+
towebp <file...> Convert file(s), output next to source
|
|
11
|
+
towebp <dir> Convert all images in dir, output next to sources
|
|
12
|
+
towebp -o <outputDir> <input...> Convert to separate output directory
|
|
13
|
+
towebp -q 80 <file> Custom quality (default: 90)
|
|
14
|
+
towebp -r <dir> Recursive subdirectory processing
|
|
15
|
+
|
|
16
|
+
Options:
|
|
17
|
+
-q, --quality <n> WebP quality 1-100 (default: 90)
|
|
18
|
+
-o, --output <dir> Output directory (default: next to source)
|
|
19
|
+
-r, --recursive Process subdirectories recursively
|
|
20
|
+
-h, --help Show this help message
|
|
21
|
+
-v, --version Show version number
|
|
22
|
+
|
|
23
|
+
Supported formats: jpg, jpeg, png, gif, bmp, tiff, webp
|
|
24
|
+
`.trim();
|
|
25
|
+
function parseArgs(argv) {
|
|
26
|
+
const args = argv.slice(2);
|
|
27
|
+
const result = {
|
|
28
|
+
inputs: [],
|
|
29
|
+
quality: 90,
|
|
30
|
+
recursive: false,
|
|
31
|
+
help: false,
|
|
32
|
+
version: false,
|
|
33
|
+
};
|
|
34
|
+
for (let i = 0; i < args.length; i++) {
|
|
35
|
+
const arg = args[i];
|
|
36
|
+
if (arg === "-h" || arg === "--help") {
|
|
37
|
+
result.help = true;
|
|
38
|
+
return result;
|
|
39
|
+
}
|
|
40
|
+
if (arg === "-v" || arg === "--version") {
|
|
41
|
+
result.version = true;
|
|
42
|
+
return result;
|
|
43
|
+
}
|
|
44
|
+
if (arg === "-r" || arg === "--recursive") {
|
|
45
|
+
result.recursive = true;
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (arg === "-q" || arg === "--quality") {
|
|
49
|
+
const next = args[++i];
|
|
50
|
+
if (next === undefined) {
|
|
51
|
+
console.error("Error: --quality requires a numeric argument");
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
const val = parseInt(next, 10);
|
|
55
|
+
if (isNaN(val)) {
|
|
56
|
+
console.error(`Error: invalid quality value: ${next}`);
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
// Clamp to 1-100
|
|
60
|
+
result.quality = Math.max(1, Math.min(val, 100));
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (arg === "-o" || arg === "--output") {
|
|
64
|
+
const next = args[++i];
|
|
65
|
+
if (next === undefined) {
|
|
66
|
+
console.error("Error: --output requires a directory argument");
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
result.outputDir = next;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (arg.startsWith("-")) {
|
|
73
|
+
console.error(`Error: unknown option: ${arg}`);
|
|
74
|
+
console.error("Run towebp --help for usage");
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
result.inputs.push(arg);
|
|
78
|
+
}
|
|
79
|
+
return result;
|
|
80
|
+
}
|
|
81
|
+
async function main() {
|
|
82
|
+
const parsed = parseArgs(process.argv);
|
|
83
|
+
if (parsed.help) {
|
|
84
|
+
console.log(HELP);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
if (parsed.version) {
|
|
88
|
+
console.log(VERSION);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (parsed.inputs.length === 0) {
|
|
92
|
+
console.error("Error: no input file or directory specified");
|
|
93
|
+
console.error("Run towebp --help for usage");
|
|
94
|
+
process.exit(1);
|
|
95
|
+
}
|
|
96
|
+
const converter = new ImageConverter(parsed.quality);
|
|
97
|
+
const results = await converter.runAll(parsed.inputs, parsed.outputDir, parsed.recursive);
|
|
98
|
+
console.log("\nConversion completed:");
|
|
99
|
+
console.log(` Total files: ${results.totalFiles}`);
|
|
100
|
+
console.log(` Processed: ${results.processed}`);
|
|
101
|
+
console.log(` Skipped: ${results.skipped}`);
|
|
102
|
+
console.log(` Failed: ${results.failed.length}`);
|
|
103
|
+
console.log(` Duration: ${results.duration}`);
|
|
104
|
+
console.log(` Total size: ${results.totalSize}`);
|
|
105
|
+
console.log(` Saved: ${results.savedSize}`);
|
|
106
|
+
console.log(` Compression: ${results.compressionRatio}`);
|
|
107
|
+
if (results.failed.length > 0) {
|
|
108
|
+
console.log("\nFailed conversions:");
|
|
109
|
+
results.failed.forEach((f) => console.log(` - ${f.file}: ${f.error}`));
|
|
110
|
+
process.exit(1);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
main().catch((err) => {
|
|
114
|
+
console.error("Error:", err instanceof Error ? err.message : String(err));
|
|
115
|
+
process.exit(1);
|
|
116
|
+
});
|
|
117
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export interface ConversionConfig {
|
|
2
|
+
quality: number;
|
|
3
|
+
maxConcurrent: number;
|
|
4
|
+
}
|
|
5
|
+
export interface ConversionStats {
|
|
6
|
+
processed: number;
|
|
7
|
+
skipped: number;
|
|
8
|
+
failed: FailedFile[];
|
|
9
|
+
totalFiles: number;
|
|
10
|
+
totalBytes: number;
|
|
11
|
+
savedBytes: number;
|
|
12
|
+
startTime: number | null;
|
|
13
|
+
endTime: number | null;
|
|
14
|
+
}
|
|
15
|
+
export interface ConversionResult {
|
|
16
|
+
totalFiles: number;
|
|
17
|
+
processed: number;
|
|
18
|
+
skipped: number;
|
|
19
|
+
failed: FailedFile[];
|
|
20
|
+
duration: string;
|
|
21
|
+
totalSize: string;
|
|
22
|
+
savedSize: string;
|
|
23
|
+
compressionRatio: string;
|
|
24
|
+
}
|
|
25
|
+
export interface FailedFile {
|
|
26
|
+
file: string;
|
|
27
|
+
error: string;
|
|
28
|
+
}
|
|
29
|
+
export interface FileConversionResult {
|
|
30
|
+
success: boolean;
|
|
31
|
+
skipped?: boolean;
|
|
32
|
+
error?: string;
|
|
33
|
+
}
|
|
34
|
+
export interface ConversionTask {
|
|
35
|
+
inputPath: string;
|
|
36
|
+
outputPath: string;
|
|
37
|
+
}
|
|
38
|
+
export interface ParsedArgs {
|
|
39
|
+
inputs: string[];
|
|
40
|
+
outputDir?: string;
|
|
41
|
+
quality: number;
|
|
42
|
+
recursive: boolean;
|
|
43
|
+
help: boolean;
|
|
44
|
+
version: boolean;
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,UAAU,EAAE,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CACxB;AAED,MAAM,WAAW,gBAAgB;IAC/B,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,UAAU,EAAE,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,gBAAgB,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,oBAAoB;IACnC,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,cAAc;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,OAAO,CAAC;IACnB,IAAI,EAAE,OAAO,CAAC;IACd,OAAO,EAAE,OAAO,CAAC;CAClB"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
|
package/dist/utils.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare function isImageFile(filePath: string): boolean;
|
|
2
|
+
export declare function formatBytes(bytes: number): string;
|
|
3
|
+
export declare function formatDuration(ms: number): string;
|
|
4
|
+
export declare function getDiskSpace(dir: string): Promise<{
|
|
5
|
+
available: number;
|
|
6
|
+
}>;
|
|
7
|
+
//# sourceMappingURL=utils.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAKA,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAGrD;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAWjD;AAED,wBAAgB,cAAc,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAIjD;AAED,wBAAsB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;IAAE,SAAS,EAAE,MAAM,CAAA;CAAE,CAAC,CAU9E"}
|
package/dist/utils.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
const INPUT_FORMATS = ["jpg", "jpeg", "png", "gif", "bmp", "tiff", "webp"];
|
|
4
|
+
export function isImageFile(filePath) {
|
|
5
|
+
const ext = path.extname(filePath).toLowerCase().slice(1);
|
|
6
|
+
return INPUT_FORMATS.includes(ext);
|
|
7
|
+
}
|
|
8
|
+
export function formatBytes(bytes) {
|
|
9
|
+
const units = ["B", "KB", "MB", "GB"];
|
|
10
|
+
let size = bytes;
|
|
11
|
+
let unit = 0;
|
|
12
|
+
while (size >= 1024 && unit < units.length - 1) {
|
|
13
|
+
size /= 1024;
|
|
14
|
+
unit++;
|
|
15
|
+
}
|
|
16
|
+
return `${size.toFixed(2)} ${units[unit]}`;
|
|
17
|
+
}
|
|
18
|
+
export function formatDuration(ms) {
|
|
19
|
+
const seconds = Math.floor(ms / 1000);
|
|
20
|
+
const minutes = Math.floor(seconds / 60);
|
|
21
|
+
return minutes > 0 ? `${minutes}m ${seconds % 60}s` : `${seconds}s`;
|
|
22
|
+
}
|
|
23
|
+
export async function getDiskSpace(dir) {
|
|
24
|
+
const target = process.platform === "win32" ? path.parse(dir).root : dir;
|
|
25
|
+
try {
|
|
26
|
+
const stats = await fs.statfs(target);
|
|
27
|
+
return { available: stats.bavail * stats.bsize };
|
|
28
|
+
}
|
|
29
|
+
catch (err) {
|
|
30
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
31
|
+
console.warn(`Warning: could not check disk space for ${dir}: ${message}`);
|
|
32
|
+
return { available: Infinity };
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=utils.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils.js","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAElC,MAAM,aAAa,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;AAE3E,MAAM,UAAU,WAAW,CAAC,QAAgB;IAC1C,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IAC1D,OAAO,aAAa,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;AACrC,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,KAAa;IACvC,MAAM,KAAK,GAAG,CAAC,GAAG,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC;IACtC,IAAI,IAAI,GAAG,KAAK,CAAC;IACjB,IAAI,IAAI,GAAG,CAAC,CAAC;IAEb,OAAO,IAAI,IAAI,IAAI,IAAI,IAAI,GAAG,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC/C,IAAI,IAAI,IAAI,CAAC;QACb,IAAI,EAAE,CAAC;IACT,CAAC;IAED,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;AAC7C,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,EAAU;IACvC,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,GAAG,IAAI,CAAC,CAAC;IACtC,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,GAAG,EAAE,CAAC,CAAC;IACzC,OAAO,OAAO,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,OAAO,KAAK,OAAO,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC,GAAG,OAAO,GAAG,CAAC;AACtE,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,GAAW;IAC5C,MAAM,MAAM,GAAG,OAAO,CAAC,QAAQ,KAAK,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC;IACzE,IAAI,CAAC;QACH,MAAM,KAAK,GAAG,MAAM,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;QACtC,OAAO,EAAE,SAAS,EAAE,KAAK,CAAC,MAAM,GAAG,KAAK,CAAC,KAAK,EAAE,CAAC;IACnD,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,OAAO,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QACjE,OAAO,CAAC,IAAI,CAAC,2CAA2C,GAAG,KAAK,OAAO,EAAE,CAAC,CAAC;QAC3E,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,CAAC;IACjC,CAAC;AACH,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "lazywebp",
|
|
3
|
+
"version": "1.3.0",
|
|
4
|
+
"description": "CLI tool to batch convert images to WebP format using Sharp",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"bin": {
|
|
15
|
+
"towebp": "dist/index.js"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist",
|
|
19
|
+
"LICENSE",
|
|
20
|
+
"README.md"
|
|
21
|
+
],
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=18"
|
|
24
|
+
},
|
|
25
|
+
"scripts": {
|
|
26
|
+
"build": "tsc",
|
|
27
|
+
"dev": "tsx src/index.ts",
|
|
28
|
+
"start": "node dist/index.js",
|
|
29
|
+
"test": "vitest run",
|
|
30
|
+
"test:watch": "vitest",
|
|
31
|
+
"prepublishOnly": "npm run build"
|
|
32
|
+
},
|
|
33
|
+
"keywords": [
|
|
34
|
+
"webp",
|
|
35
|
+
"image",
|
|
36
|
+
"converter",
|
|
37
|
+
"sharp",
|
|
38
|
+
"batch",
|
|
39
|
+
"cli",
|
|
40
|
+
"image-processing"
|
|
41
|
+
],
|
|
42
|
+
"author": "Keiver Hernandez (https://keiver.dev)",
|
|
43
|
+
"license": "MIT",
|
|
44
|
+
"repository": {
|
|
45
|
+
"type": "git",
|
|
46
|
+
"url": "git+https://github.com/keiver/towebp.git"
|
|
47
|
+
},
|
|
48
|
+
"bugs": {
|
|
49
|
+
"url": "https://github.com/keiver/towebp/issues"
|
|
50
|
+
},
|
|
51
|
+
"homepage": "https://keiver.dev/towebp",
|
|
52
|
+
"dependencies": {
|
|
53
|
+
"sharp": "^0.33.5"
|
|
54
|
+
},
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"@types/node": "^22.10.0",
|
|
57
|
+
"tsx": "^4.19.0",
|
|
58
|
+
"typescript": "^5.7.0",
|
|
59
|
+
"vitest": "^4.0.18"
|
|
60
|
+
}
|
|
61
|
+
}
|