@semba-ryuichiro/webpify 1.0.9 → 1.2.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/README.md +16 -10
- package/dist/adapters/sharp-adapter/index.js +4 -1
- package/dist/cli/argument-parser/index.js +8 -0
- package/dist/cli/main/index.js +18 -9
- package/dist/cli/reporter/index.d.ts +2 -1
- package/dist/cli/reporter/index.js +19 -6
- package/dist/core/converter/index.js +1 -0
- package/dist/ports/image-processor.d.ts +1 -0
- package/dist/types/index.d.ts +7 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -26,6 +26,7 @@ webpify は画像ファイル(PNG/JPEG/GIF)を WebP 形式に変換する CL
|
|
|
26
26
|
- 単一ファイルまたはディレクトリ一括変換
|
|
27
27
|
- 再帰的なディレクトリ走査
|
|
28
28
|
- 品質パラメータの指定(1-100)
|
|
29
|
+
- lossless(可逆圧縮)モード対応
|
|
29
30
|
- 出力先ディレクトリの指定
|
|
30
31
|
- 既存ファイルの上書き制御
|
|
31
32
|
- WebP ファイル一覧表示
|
|
@@ -53,16 +54,18 @@ webpify ./images -r
|
|
|
53
54
|
|
|
54
55
|
### オプション
|
|
55
56
|
|
|
56
|
-
| オプション | 説明
|
|
57
|
-
| -------------------- |
|
|
58
|
-
| `-o, --output <dir>` | 出力先ディレクトリ
|
|
59
|
-
| `-q, --quality <n>` | 品質(1-100)
|
|
60
|
-
| `-r, --recursive` | 再帰的に処理
|
|
61
|
-
| `-f, --force` | 既存ファイルを上書き
|
|
62
|
-
| `--
|
|
63
|
-
| `--
|
|
64
|
-
|
|
|
65
|
-
|
|
|
57
|
+
| オプション | 説明 | デフォルト |
|
|
58
|
+
| -------------------- | --------------- | ----- |
|
|
59
|
+
| `-o, --output <dir>` | 出力先ディレクトリ | 入力と同じ |
|
|
60
|
+
| `-q, --quality <n>` | 品質(1-100) | 100 |
|
|
61
|
+
| `-r, --recursive` | 再帰的に処理 | false |
|
|
62
|
+
| `-f, --force` | 既存ファイルを上書き | false |
|
|
63
|
+
| `--lossless` | 可逆圧縮モードで変換 | false |
|
|
64
|
+
| `--list` | WebP ファイル一覧表示 | - |
|
|
65
|
+
| `--absolute` | 一覧表示時に絶対パスで表示 | false |
|
|
66
|
+
| `--quiet` | 統計情報を非表示 | false |
|
|
67
|
+
| `-v, --version` | バージョン表示 | - |
|
|
68
|
+
| `-h, --help` | ヘルプ表示 | - |
|
|
66
69
|
|
|
67
70
|
### 例
|
|
68
71
|
|
|
@@ -73,6 +76,9 @@ webpify image.png -q 90
|
|
|
73
76
|
# 別ディレクトリに出力
|
|
74
77
|
webpify ./images -o ./webp-images
|
|
75
78
|
|
|
79
|
+
# lossless(可逆圧縮)モードで変換
|
|
80
|
+
webpify image.png --lossless
|
|
81
|
+
|
|
76
82
|
# 強制上書き + 再帰 + 静音
|
|
77
83
|
webpify ./images -r -f --quiet
|
|
78
84
|
```
|
|
@@ -6,7 +6,10 @@ import sharp from 'sharp';
|
|
|
6
6
|
export function createSharpAdapter() {
|
|
7
7
|
return {
|
|
8
8
|
async convertToWebP(inputPath, outputPath, options) {
|
|
9
|
-
|
|
9
|
+
// lossless モードの場合は lossless: true を設定(quality は無視される)
|
|
10
|
+
// lossy モードの場合は quality を設定
|
|
11
|
+
const webpOptions = options.lossless ? { lossless: true } : { quality: options.quality };
|
|
12
|
+
const result = await sharp(inputPath).webp(webpOptions).toFile(outputPath);
|
|
10
13
|
return { size: result.size };
|
|
11
14
|
},
|
|
12
15
|
async getMetadata(filePath) {
|
|
@@ -44,8 +44,10 @@ export function createArgumentParser() {
|
|
|
44
44
|
.option('-q, --quality <number>', '品質レベル (1-100)', parseQuality, DEFAULT_QUALITY)
|
|
45
45
|
.option('-r, --recursive', 'ディレクトリを再帰的に処理', false)
|
|
46
46
|
.option('-f, --force', '既存ファイルを上書き', false)
|
|
47
|
+
.option('--lossless', 'lossless(可逆圧縮)モードで変換', false)
|
|
47
48
|
.option('--quiet', 'サイレントモード(出力なし)', false)
|
|
48
49
|
.option('--list', 'WebP ファイルをサイズ情報付きで一覧表示', false)
|
|
50
|
+
.option('--absolute', '--list オプション使用時に絶対パスで表示', false)
|
|
49
51
|
.configureOutput({
|
|
50
52
|
writeErr: (str) => process.stderr.write(str),
|
|
51
53
|
writeOut: (str) => process.stdout.write(str),
|
|
@@ -57,9 +59,11 @@ export function createArgumentParser() {
|
|
|
57
59
|
if (argv.length <= 2) {
|
|
58
60
|
program.outputHelp();
|
|
59
61
|
return {
|
|
62
|
+
absolutePath: false,
|
|
60
63
|
force: false,
|
|
61
64
|
input: '',
|
|
62
65
|
list: false,
|
|
66
|
+
lossless: false,
|
|
63
67
|
quality: DEFAULT_QUALITY,
|
|
64
68
|
quiet: false,
|
|
65
69
|
recursive: false,
|
|
@@ -75,9 +79,11 @@ export function createArgumentParser() {
|
|
|
75
79
|
const code = err.code;
|
|
76
80
|
if (code === 'commander.helpDisplayed' || code === 'commander.version') {
|
|
77
81
|
return {
|
|
82
|
+
absolutePath: false,
|
|
78
83
|
force: false,
|
|
79
84
|
input: '',
|
|
80
85
|
list: false,
|
|
86
|
+
lossless: false,
|
|
81
87
|
quality: DEFAULT_QUALITY,
|
|
82
88
|
quiet: false,
|
|
83
89
|
recursive: false,
|
|
@@ -89,9 +95,11 @@ export function createArgumentParser() {
|
|
|
89
95
|
const options = program.opts();
|
|
90
96
|
const args = program.args;
|
|
91
97
|
return {
|
|
98
|
+
absolutePath: options['absolute'],
|
|
92
99
|
force: options['force'],
|
|
93
100
|
input: args[0] || '',
|
|
94
101
|
list: options['list'],
|
|
102
|
+
lossless: options['lossless'],
|
|
95
103
|
output: options['output'],
|
|
96
104
|
quality: options['quality'],
|
|
97
105
|
quiet: options['quiet'],
|
package/dist/cli/main/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
1
2
|
/**
|
|
2
3
|
* サポートされる変換対象の拡張子
|
|
3
4
|
*/
|
|
@@ -16,7 +17,7 @@ export function createMain(deps) {
|
|
|
16
17
|
/**
|
|
17
18
|
* 一覧表示モードを実行する
|
|
18
19
|
*/
|
|
19
|
-
async function executeListMode(inputPath, recursive) {
|
|
20
|
+
async function executeListMode(inputPath, recursive, absolutePath) {
|
|
20
21
|
const files = await fileScanner.scan(inputPath, {
|
|
21
22
|
extensions: WEBP_EXTENSIONS,
|
|
22
23
|
recursive,
|
|
@@ -28,15 +29,18 @@ export function createMain(deps) {
|
|
|
28
29
|
size: info.size,
|
|
29
30
|
width: info.width,
|
|
30
31
|
}));
|
|
31
|
-
|
|
32
|
+
// absolutePath が false の場合は basePath を渡して相対パスを表示
|
|
33
|
+
const basePath = absolutePath ? undefined : path.resolve(inputPath);
|
|
34
|
+
reporter.reportImageList(items, basePath);
|
|
32
35
|
return 0;
|
|
33
36
|
}
|
|
34
37
|
/**
|
|
35
38
|
* 単一ファイル変換モードを実行する
|
|
36
39
|
*/
|
|
37
|
-
async function executeSingleFileMode(inputPath, quality, force, output, quiet) {
|
|
40
|
+
async function executeSingleFileMode(inputPath, quality, force, output, quiet, lossless) {
|
|
38
41
|
const result = await converter.convert(inputPath, {
|
|
39
42
|
force,
|
|
43
|
+
lossless,
|
|
40
44
|
output,
|
|
41
45
|
quality,
|
|
42
46
|
});
|
|
@@ -46,7 +50,7 @@ export function createMain(deps) {
|
|
|
46
50
|
/**
|
|
47
51
|
* ディレクトリ変換モードを実行する
|
|
48
52
|
*/
|
|
49
|
-
async function executeDirectoryMode(inputPath, quality, force, output, recursive, quiet) {
|
|
53
|
+
async function executeDirectoryMode(inputPath, quality, force, output, recursive, quiet, lossless) {
|
|
50
54
|
const files = await fileScanner.scan(inputPath, {
|
|
51
55
|
extensions: CONVERT_EXTENSIONS,
|
|
52
56
|
recursive,
|
|
@@ -63,7 +67,7 @@ export function createMain(deps) {
|
|
|
63
67
|
reporter.reportConversion(result, quiet);
|
|
64
68
|
}
|
|
65
69
|
};
|
|
66
|
-
const stats = await converter.convertBatch(files, { force, output, quality }, onProgress);
|
|
70
|
+
const stats = await converter.convertBatch(files, { force, lossless, output, quality }, onProgress);
|
|
67
71
|
if (!quiet) {
|
|
68
72
|
reporter.reportStats(stats);
|
|
69
73
|
}
|
|
@@ -83,12 +87,17 @@ export function createMain(deps) {
|
|
|
83
87
|
process.stderr.write(`Error: File not found: ${options.input}\n`);
|
|
84
88
|
return 1;
|
|
85
89
|
}
|
|
90
|
+
// lossless と quality の同時指定時に警告を出す(quality がデフォルト値でない場合)
|
|
91
|
+
if (options.lossless && options.quality !== 100 && !options.quiet) {
|
|
92
|
+
process.stderr.write('警告: --lossless と --quality が同時に指定されています。lossless モードでは quality は無視されます。\n');
|
|
93
|
+
}
|
|
86
94
|
// 一覧表示モード(--list オプション)
|
|
87
95
|
if (options.list) {
|
|
88
96
|
const isDir = await fileScanner.isDirectory(options.input);
|
|
89
97
|
if (!isDir) {
|
|
90
98
|
// 単一ファイルの場合も一覧表示
|
|
91
99
|
const info = await imageInspector.getInfo(options.input);
|
|
100
|
+
const basePath = options.absolutePath ? undefined : path.dirname(path.resolve(options.input));
|
|
92
101
|
reporter.reportImageList([
|
|
93
102
|
{
|
|
94
103
|
height: info.height,
|
|
@@ -96,19 +105,19 @@ export function createMain(deps) {
|
|
|
96
105
|
size: info.size,
|
|
97
106
|
width: info.width,
|
|
98
107
|
},
|
|
99
|
-
]);
|
|
108
|
+
], basePath);
|
|
100
109
|
return 0;
|
|
101
110
|
}
|
|
102
|
-
return executeListMode(options.input, options.recursive);
|
|
111
|
+
return executeListMode(options.input, options.recursive, options.absolutePath);
|
|
103
112
|
}
|
|
104
113
|
// ファイル or ディレクトリの判定
|
|
105
114
|
const isDirectory = await fileScanner.isDirectory(options.input);
|
|
106
115
|
if (isDirectory) {
|
|
107
116
|
// ディレクトリ変換モード
|
|
108
|
-
return executeDirectoryMode(options.input, options.quality, options.force, options.output, options.recursive, options.quiet);
|
|
117
|
+
return executeDirectoryMode(options.input, options.quality, options.force, options.output, options.recursive, options.quiet, options.lossless);
|
|
109
118
|
}
|
|
110
119
|
// 単一ファイル変換モード
|
|
111
|
-
return executeSingleFileMode(options.input, options.quality, options.force, options.output, options.quiet);
|
|
120
|
+
return executeSingleFileMode(options.input, options.quality, options.force, options.output, options.quiet, options.lossless);
|
|
112
121
|
},
|
|
113
122
|
};
|
|
114
123
|
}
|
|
@@ -24,8 +24,9 @@ export interface ReporterService {
|
|
|
24
24
|
/**
|
|
25
25
|
* WebP ファイル一覧を表示する
|
|
26
26
|
* @param items - 画像アイテムのリスト
|
|
27
|
+
* @param basePath - 相対パス計算の基準パス(省略時は絶対パスを表示)
|
|
27
28
|
*/
|
|
28
|
-
reportImageList(items: ImageListItem[]): void;
|
|
29
|
+
reportImageList(items: ImageListItem[], basePath?: string): void;
|
|
29
30
|
}
|
|
30
31
|
/**
|
|
31
32
|
* Reporter のファクトリ関数
|
|
@@ -53,18 +53,31 @@ export function createReporter() {
|
|
|
53
53
|
const reduction = formatReduction(result.inputSize, result.outputSize);
|
|
54
54
|
process.stdout.write(`Converted: ${fileName} (${inputSizeStr} -> ${outputSizeStr}, ${reduction})\n`);
|
|
55
55
|
},
|
|
56
|
-
reportImageList(items) {
|
|
56
|
+
reportImageList(items, basePath) {
|
|
57
57
|
if (items.length === 0) {
|
|
58
58
|
process.stdout.write('WebP ファイルが見つかりません\n');
|
|
59
59
|
return;
|
|
60
60
|
}
|
|
61
|
+
// 表示用パスを計算(basePath が指定されていれば相対パス、なければ絶対パス)
|
|
62
|
+
const displayPaths = items.map((item) => {
|
|
63
|
+
if (basePath) {
|
|
64
|
+
const relativePath = path.relative(basePath, item.path);
|
|
65
|
+
return relativePath || path.basename(item.path);
|
|
66
|
+
}
|
|
67
|
+
return item.path;
|
|
68
|
+
});
|
|
69
|
+
// 動的なカラム幅を計算(最小20、最大60文字)
|
|
70
|
+
const maxPathLength = Math.max(...displayPaths.map((p) => p.length));
|
|
71
|
+
const pathColumnWidth = Math.min(Math.max(maxPathLength + 2, 20), 60);
|
|
61
72
|
process.stdout.write('\n--- WebP File List ---\n');
|
|
62
|
-
process.stdout.write(`${'File'.padEnd(
|
|
63
|
-
process.stdout.write(`${'-'.repeat(
|
|
64
|
-
for (const item of items) {
|
|
65
|
-
const
|
|
73
|
+
process.stdout.write(`${'File'.padEnd(pathColumnWidth)} ${'Size'.padEnd(12)} ${'Width'.padEnd(8)} ${'Height'.padEnd(8)}\n`);
|
|
74
|
+
process.stdout.write(`${'-'.repeat(pathColumnWidth + 30)}\n`);
|
|
75
|
+
for (const [index, item] of items.entries()) {
|
|
76
|
+
const displayPath = displayPaths[index] ?? '';
|
|
66
77
|
const sizeStr = formatSize(item.size);
|
|
67
|
-
|
|
78
|
+
// 長いパスは省略表示
|
|
79
|
+
const truncatedPath = displayPath.length > pathColumnWidth - 2 ? `...${displayPath.slice(-(pathColumnWidth - 5))}` : displayPath;
|
|
80
|
+
process.stdout.write(`${truncatedPath.padEnd(pathColumnWidth)} ${sizeStr.padEnd(12)} ${String(item.width).padEnd(8)} ${String(item.height).padEnd(8)}\n`);
|
|
68
81
|
}
|
|
69
82
|
},
|
|
70
83
|
reportProgress(current, total, fileName) {
|
package/dist/types/index.d.ts
CHANGED
|
@@ -8,10 +8,12 @@ export type SupportedFormat = 'png' | 'jpeg' | 'jpg' | 'gif';
|
|
|
8
8
|
export interface ConvertOptions {
|
|
9
9
|
/** 出力先パス(省略時は入力ファイルと同じディレクトリ) */
|
|
10
10
|
output?: string | undefined;
|
|
11
|
-
/** 変換品質(1-100、デフォルト:
|
|
11
|
+
/** 変換品質(1-100、デフォルト: 100) */
|
|
12
12
|
quality: number;
|
|
13
13
|
/** 既存ファイルを上書きするかどうか */
|
|
14
14
|
force: boolean;
|
|
15
|
+
/** lossless(可逆圧縮)モードで変換するかどうか */
|
|
16
|
+
lossless: boolean;
|
|
15
17
|
}
|
|
16
18
|
/**
|
|
17
19
|
* 単一ファイルの変換結果
|
|
@@ -89,6 +91,10 @@ export interface ParsedOptions {
|
|
|
89
91
|
quiet: boolean;
|
|
90
92
|
/** WebP ファイル一覧表示モード */
|
|
91
93
|
list: boolean;
|
|
94
|
+
/** 一覧表示時に絶対パスで表示するか */
|
|
95
|
+
absolutePath: boolean;
|
|
96
|
+
/** lossless(可逆圧縮)モードで変換するか */
|
|
97
|
+
lossless: boolean;
|
|
92
98
|
}
|
|
93
99
|
/**
|
|
94
100
|
* 一覧表示用の画像アイテム
|