@xiping/node-utils 1.0.38 → 1.0.39
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/lib/index.d.ts +2 -0
- package/lib/index.js +4 -0
- package/lib/src/ffmpeg/cutVideo.d.ts +29 -0
- package/lib/src/ffmpeg/cutVideo.js +241 -0
- package/lib/src/ffmpeg/index.d.ts +1 -0
- package/lib/src/ffmpeg/index.js +1 -0
- package/lib/src/file/getFileConfig.d.ts +30 -0
- package/lib/src/file/getFileConfig.js +208 -0
- package/lib/src/image/convert.d.ts +89 -0
- package/lib/src/image/convert.js +164 -0
- package/lib/src/image/example.d.ts +29 -0
- package/lib/src/image/example.js +116 -0
- package/lib/src/image/index.d.ts +6 -0
- package/lib/src/image/index.js +6 -0
- package/package.json +9 -7
package/lib/index.d.ts
CHANGED
package/lib/index.js
CHANGED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 视频截取器
|
|
3
|
+
* 使用 ffmpeg 截取视频的一部分,保持原视频编码和质量
|
|
4
|
+
*/
|
|
5
|
+
interface CutVideoOptions {
|
|
6
|
+
startTime?: string;
|
|
7
|
+
duration?: string;
|
|
8
|
+
endTime?: string;
|
|
9
|
+
outputFileName?: string;
|
|
10
|
+
outputFormat?: string;
|
|
11
|
+
tempDir?: string;
|
|
12
|
+
overwrite?: boolean;
|
|
13
|
+
}
|
|
14
|
+
interface CutVideoResult {
|
|
15
|
+
outputPath: string;
|
|
16
|
+
metadata: {
|
|
17
|
+
originalDuration: number;
|
|
18
|
+
cutDuration: number;
|
|
19
|
+
startTime: string;
|
|
20
|
+
endTime: string;
|
|
21
|
+
fileSize: number;
|
|
22
|
+
processingTime: number;
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
export declare const cutVideo: (videoPath: string, options?: CutVideoOptions) => Promise<CutVideoResult>;
|
|
26
|
+
export declare const cutVideoByTimeRange: (videoPath: string, startTime: string, endTime: string, options?: Omit<CutVideoOptions, "startTime" | "endTime">) => Promise<CutVideoResult>;
|
|
27
|
+
export declare const cutVideoByDuration: (videoPath: string, startTime: string, duration: string, options?: Omit<CutVideoOptions, "startTime" | "duration">) => Promise<CutVideoResult>;
|
|
28
|
+
export declare const cutVideoFromStart: (videoPath: string, durationSeconds: number, options?: Omit<CutVideoOptions, "startTime" | "duration">) => Promise<CutVideoResult>;
|
|
29
|
+
export {};
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import shell from "shelljs";
|
|
2
|
+
import * as fs from "fs";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import * as os from "node:os";
|
|
5
|
+
import { checkFFmpegAvailability } from "./check.js";
|
|
6
|
+
// 日志函数
|
|
7
|
+
const log = (message, data) => {
|
|
8
|
+
console.log(`[VideoCutter] ${message}`, data ? JSON.stringify(data, null, 2) : '');
|
|
9
|
+
};
|
|
10
|
+
// 验证和清理路径
|
|
11
|
+
function validateAndSanitizePath(inputPath) {
|
|
12
|
+
if (!inputPath || typeof inputPath !== 'string') {
|
|
13
|
+
throw new Error('Invalid path provided');
|
|
14
|
+
}
|
|
15
|
+
// 确保是绝对路径
|
|
16
|
+
if (!path.isAbsolute(inputPath)) {
|
|
17
|
+
throw new Error('Path must be absolute');
|
|
18
|
+
}
|
|
19
|
+
return inputPath;
|
|
20
|
+
}
|
|
21
|
+
// 时间格式转换
|
|
22
|
+
function normalizeTimeFormat(time) {
|
|
23
|
+
if (typeof time === 'number') {
|
|
24
|
+
const hours = Math.floor(time / 3600);
|
|
25
|
+
const minutes = Math.floor((time % 3600) / 60);
|
|
26
|
+
const seconds = Math.floor(time % 60);
|
|
27
|
+
return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
|
|
28
|
+
}
|
|
29
|
+
// 如果已经是 HH:MM:SS 格式,直接返回
|
|
30
|
+
if (/^\d{2}:\d{2}:\d{2}$/.test(time)) {
|
|
31
|
+
return time;
|
|
32
|
+
}
|
|
33
|
+
// 如果是秒数,转换为 HH:MM:SS
|
|
34
|
+
const seconds = parseFloat(time);
|
|
35
|
+
if (!isNaN(seconds)) {
|
|
36
|
+
return normalizeTimeFormat(seconds);
|
|
37
|
+
}
|
|
38
|
+
throw new Error(`Invalid time format: ${time}`);
|
|
39
|
+
}
|
|
40
|
+
// 获取视频时长
|
|
41
|
+
function getDurationInSeconds(videoPath) {
|
|
42
|
+
log('Getting video duration', { videoPath });
|
|
43
|
+
const result = shell.exec(`ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "${videoPath}"`, { silent: true });
|
|
44
|
+
if (result.code !== 0) {
|
|
45
|
+
throw new Error(`FFprobe failed to get duration: ${result.stderr}`);
|
|
46
|
+
}
|
|
47
|
+
const duration = parseFloat(result.stdout);
|
|
48
|
+
if (isNaN(duration) || duration <= 0) {
|
|
49
|
+
throw new Error('Invalid video duration');
|
|
50
|
+
}
|
|
51
|
+
return duration;
|
|
52
|
+
}
|
|
53
|
+
// 验证时间参数
|
|
54
|
+
function validateTimeParameters(startTime, duration, endTime, videoDuration) {
|
|
55
|
+
const startSeconds = timeToSeconds(startTime);
|
|
56
|
+
if (startSeconds >= videoDuration) {
|
|
57
|
+
throw new Error(`Start time (${startTime}) is greater than or equal to video duration (${videoDuration}s)`);
|
|
58
|
+
}
|
|
59
|
+
let endSeconds;
|
|
60
|
+
let durationStr;
|
|
61
|
+
if (endTime) {
|
|
62
|
+
endSeconds = timeToSeconds(endTime);
|
|
63
|
+
if (endSeconds <= startSeconds) {
|
|
64
|
+
throw new Error(`End time (${endTime}) must be greater than start time (${startTime})`);
|
|
65
|
+
}
|
|
66
|
+
if (endSeconds > videoDuration) {
|
|
67
|
+
endSeconds = videoDuration;
|
|
68
|
+
}
|
|
69
|
+
durationStr = normalizeTimeFormat(endSeconds - startSeconds);
|
|
70
|
+
}
|
|
71
|
+
else if (duration) {
|
|
72
|
+
durationStr = duration;
|
|
73
|
+
endSeconds = startSeconds + timeToSeconds(duration);
|
|
74
|
+
if (endSeconds > videoDuration) {
|
|
75
|
+
endSeconds = videoDuration;
|
|
76
|
+
durationStr = normalizeTimeFormat(videoDuration - startSeconds);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
// 默认截取到视频结束
|
|
81
|
+
endSeconds = videoDuration;
|
|
82
|
+
durationStr = normalizeTimeFormat(videoDuration - startSeconds);
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
startTime,
|
|
86
|
+
endTime: normalizeTimeFormat(endSeconds),
|
|
87
|
+
duration: durationStr
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
// 时间字符串转换为秒数
|
|
91
|
+
function timeToSeconds(timeStr) {
|
|
92
|
+
const parts = timeStr.split(':').map(Number);
|
|
93
|
+
if (parts.length === 3) {
|
|
94
|
+
// HH:MM:SS 格式
|
|
95
|
+
return parts[0] * 3600 + parts[1] * 60 + parts[2];
|
|
96
|
+
}
|
|
97
|
+
else if (parts.length === 2) {
|
|
98
|
+
// MM:SS 格式
|
|
99
|
+
return parts[0] * 60 + parts[1];
|
|
100
|
+
}
|
|
101
|
+
else if (parts.length === 1) {
|
|
102
|
+
// 秒数格式
|
|
103
|
+
return parts[0];
|
|
104
|
+
}
|
|
105
|
+
throw new Error(`Invalid time format: ${timeStr}`);
|
|
106
|
+
}
|
|
107
|
+
// 构建 FFmpeg 命令
|
|
108
|
+
function buildFFmpegCommand(inputPath, outputPath, startTime, duration, options) {
|
|
109
|
+
const { overwrite = false } = options;
|
|
110
|
+
// 使用 copy 模式,保持原视频编码和质量
|
|
111
|
+
let command = `ffmpeg -ss ${startTime} -i "${inputPath}" -t ${duration}`;
|
|
112
|
+
// 复制视频和音频流,不重新编码
|
|
113
|
+
command += ' -c copy';
|
|
114
|
+
// 其他设置
|
|
115
|
+
command += ' -avoid_negative_ts make_zero';
|
|
116
|
+
command += ' -y'; // 自动覆盖输出文件
|
|
117
|
+
// 输出文件
|
|
118
|
+
command += ` "${outputPath}"`;
|
|
119
|
+
return command;
|
|
120
|
+
}
|
|
121
|
+
// 临时目录管理
|
|
122
|
+
async function withTempDir(fn, customTempDir) {
|
|
123
|
+
const tempDir = customTempDir || fs.mkdtempSync(path.join(os.tmpdir(), "video-cut-"));
|
|
124
|
+
try {
|
|
125
|
+
return await fn(tempDir);
|
|
126
|
+
}
|
|
127
|
+
finally {
|
|
128
|
+
if (fs.existsSync(tempDir) && !customTempDir) {
|
|
129
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
130
|
+
log('Cleaned up temporary directory', { tempDir });
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
// 主函数
|
|
135
|
+
export const cutVideo = async (videoPath, options = {}) => {
|
|
136
|
+
const startTime = Date.now();
|
|
137
|
+
try {
|
|
138
|
+
// 输入验证
|
|
139
|
+
const validatedPath = validateAndSanitizePath(videoPath);
|
|
140
|
+
if (!fs.existsSync(validatedPath)) {
|
|
141
|
+
throw new Error(`Video file not found: ${validatedPath}`);
|
|
142
|
+
}
|
|
143
|
+
// 检查 ffmpeg 可用性
|
|
144
|
+
if (!checkFFmpegAvailability()) {
|
|
145
|
+
throw new Error('FFmpeg is not available. Please install FFmpeg first.');
|
|
146
|
+
}
|
|
147
|
+
// 参数设置和验证
|
|
148
|
+
const { startTime: startTimeParam = "00:00:00", duration, endTime, outputFileName, outputFormat = "mp4", tempDir: customTempDir, overwrite = false } = options;
|
|
149
|
+
log('Starting video cutting', {
|
|
150
|
+
videoPath: validatedPath,
|
|
151
|
+
options: { startTime: startTimeParam, duration, endTime, outputFormat }
|
|
152
|
+
});
|
|
153
|
+
// 获取视频信息
|
|
154
|
+
const videoDuration = getDurationInSeconds(validatedPath);
|
|
155
|
+
log('Video analysis complete', { duration: videoDuration });
|
|
156
|
+
// 验证和标准化时间参数
|
|
157
|
+
const timeParams = validateTimeParameters(startTimeParam, duration, endTime, videoDuration);
|
|
158
|
+
log('Time parameters validated', timeParams);
|
|
159
|
+
// 准备输出路径
|
|
160
|
+
const videoDir = path.dirname(validatedPath);
|
|
161
|
+
const fileNameWithoutExt = path.basename(validatedPath, path.extname(validatedPath));
|
|
162
|
+
const defaultOutputName = `${fileNameWithoutExt}_cut_${timeParams.startTime.replace(/:/g, '-')}_to_${timeParams.endTime.replace(/:/g, '-')}.${outputFormat}`;
|
|
163
|
+
const outputFileNameFinal = outputFileName || defaultOutputName;
|
|
164
|
+
const outputPath = path.join(videoDir, outputFileNameFinal);
|
|
165
|
+
// 检查输出文件是否已存在
|
|
166
|
+
if (fs.existsSync(outputPath) && !overwrite) {
|
|
167
|
+
throw new Error(`Output file already exists: ${outputPath}. Use overwrite option to force overwrite.`);
|
|
168
|
+
}
|
|
169
|
+
// 使用临时目录处理
|
|
170
|
+
await withTempDir(async (tempDir) => {
|
|
171
|
+
const tempOutputPath = path.join(tempDir, `temp_output.${outputFormat}`);
|
|
172
|
+
// 构建 FFmpeg 命令
|
|
173
|
+
const command = buildFFmpegCommand(validatedPath, tempOutputPath, timeParams.startTime, timeParams.duration, options);
|
|
174
|
+
log('Executing FFmpeg command', { command });
|
|
175
|
+
// 执行 FFmpeg 命令
|
|
176
|
+
const result = shell.exec(command, { silent: true });
|
|
177
|
+
if (result.code !== 0) {
|
|
178
|
+
throw new Error(`FFmpeg failed: ${result.stderr}`);
|
|
179
|
+
}
|
|
180
|
+
// 移动文件到最终位置
|
|
181
|
+
fs.copyFileSync(tempOutputPath, outputPath);
|
|
182
|
+
log('Video cutting complete', { outputPath });
|
|
183
|
+
}, customTempDir);
|
|
184
|
+
// 获取输出文件信息
|
|
185
|
+
const outputStats = fs.statSync(outputPath);
|
|
186
|
+
const processingTime = Date.now() - startTime;
|
|
187
|
+
log('Video cutting successful', {
|
|
188
|
+
outputPath,
|
|
189
|
+
processingTime: `${processingTime}ms`,
|
|
190
|
+
fileSize: `${(outputStats.size / 1024 / 1024).toFixed(2)}MB`
|
|
191
|
+
});
|
|
192
|
+
return {
|
|
193
|
+
outputPath,
|
|
194
|
+
metadata: {
|
|
195
|
+
originalDuration: videoDuration,
|
|
196
|
+
cutDuration: timeToSeconds(timeParams.duration),
|
|
197
|
+
startTime: timeParams.startTime,
|
|
198
|
+
endTime: timeParams.endTime,
|
|
199
|
+
fileSize: outputStats.size,
|
|
200
|
+
processingTime
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
catch (error) {
|
|
205
|
+
log('Video cutting failed', {
|
|
206
|
+
error: error instanceof Error ? error.message : String(error),
|
|
207
|
+
videoPath
|
|
208
|
+
});
|
|
209
|
+
throw error;
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
// 便捷函数:按时间范围截取
|
|
213
|
+
export const cutVideoByTimeRange = async (videoPath, startTime, endTime, options = {}) => {
|
|
214
|
+
return cutVideo(videoPath, {
|
|
215
|
+
...options,
|
|
216
|
+
startTime,
|
|
217
|
+
endTime
|
|
218
|
+
});
|
|
219
|
+
};
|
|
220
|
+
// 便捷函数:按持续时间截取
|
|
221
|
+
export const cutVideoByDuration = async (videoPath, startTime, duration, options = {}) => {
|
|
222
|
+
return cutVideo(videoPath, {
|
|
223
|
+
...options,
|
|
224
|
+
startTime,
|
|
225
|
+
duration
|
|
226
|
+
});
|
|
227
|
+
};
|
|
228
|
+
// 便捷函数:从头开始截取指定秒数的视频
|
|
229
|
+
export const cutVideoFromStart = async (videoPath, durationSeconds, options = {}) => {
|
|
230
|
+
// 验证持续时间参数
|
|
231
|
+
if (durationSeconds <= 0) {
|
|
232
|
+
throw new Error('Duration must be greater than 0 seconds');
|
|
233
|
+
}
|
|
234
|
+
// 将秒数转换为时间格式
|
|
235
|
+
const duration = normalizeTimeFormat(durationSeconds);
|
|
236
|
+
return cutVideo(videoPath, {
|
|
237
|
+
...options,
|
|
238
|
+
startTime: "00:00:00", // 从头开始
|
|
239
|
+
duration
|
|
240
|
+
});
|
|
241
|
+
};
|
package/lib/src/ffmpeg/index.js
CHANGED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { VideoInfo } from '../ffmpeg/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* 文件配置信息接口
|
|
4
|
+
*/
|
|
5
|
+
export interface FileConfig {
|
|
6
|
+
/** 文件大小(带单位) */
|
|
7
|
+
size: string;
|
|
8
|
+
/** 文件大小(字节) */
|
|
9
|
+
sizeInBytes: number;
|
|
10
|
+
/** 文件编码格式 */
|
|
11
|
+
encoding: string;
|
|
12
|
+
/** 文件扩展名 */
|
|
13
|
+
extension: string;
|
|
14
|
+
/** 文件是否存在 */
|
|
15
|
+
exists: boolean;
|
|
16
|
+
/** 视频信息(仅当文件是视频时存在) */
|
|
17
|
+
videoInfo?: VideoInfo;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* 获取文件配置信息
|
|
21
|
+
* @param filePath 文件绝对路径
|
|
22
|
+
* @returns 文件配置信息
|
|
23
|
+
*/
|
|
24
|
+
export declare function getFileConfig(filePath: string): FileConfig;
|
|
25
|
+
/**
|
|
26
|
+
* 获取文件配置信息(异步版本)
|
|
27
|
+
* @param filePath 文件绝对路径
|
|
28
|
+
* @returns Promise<FileConfig>
|
|
29
|
+
*/
|
|
30
|
+
export declare function getFileConfigAsync(filePath: string): Promise<FileConfig>;
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { statSync, readFileSync } from 'fs';
|
|
2
|
+
import { extname } from 'path';
|
|
3
|
+
import { getVideoInfo } from '../ffmpeg/index.js';
|
|
4
|
+
/**
|
|
5
|
+
* 检测文件是否为视频文件
|
|
6
|
+
* @param filePath 文件路径
|
|
7
|
+
* @returns 是否为视频文件
|
|
8
|
+
*/
|
|
9
|
+
function isVideoFile(filePath) {
|
|
10
|
+
const videoExtensions = [
|
|
11
|
+
'.mp4', '.avi', '.mov', '.wmv', '.flv', '.webm', '.mkv',
|
|
12
|
+
'.m4v', '.3gp', '.ogv', '.ts', '.mts', '.m2ts', '.vob',
|
|
13
|
+
'.asf', '.rm', '.rmvb', '.divx', '.xvid', '.mpg', '.mpeg',
|
|
14
|
+
'.m2v', '.mpe', '.mpv', '.m1v', '.m2p', '.m2t', '.mxf'
|
|
15
|
+
];
|
|
16
|
+
const extension = extname(filePath).toLowerCase();
|
|
17
|
+
return videoExtensions.includes(extension);
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* 格式化文件大小
|
|
21
|
+
* @param bytes 字节数
|
|
22
|
+
* @returns 格式化后的大小字符串
|
|
23
|
+
*/
|
|
24
|
+
function formatFileSize(bytes) {
|
|
25
|
+
if (bytes === 0)
|
|
26
|
+
return '0 B';
|
|
27
|
+
const k = 1024;
|
|
28
|
+
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
29
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
30
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* 检测文件编码格式
|
|
34
|
+
* @param filePath 文件路径
|
|
35
|
+
* @returns 编码格式
|
|
36
|
+
*/
|
|
37
|
+
function detectEncoding(filePath) {
|
|
38
|
+
try {
|
|
39
|
+
// 读取文件的前几个字节来检测编码
|
|
40
|
+
const buffer = readFileSync(filePath);
|
|
41
|
+
// 检查BOM(字节顺序标记)
|
|
42
|
+
if (buffer.length >= 3 && buffer[0] === 0xEF && buffer[1] === 0xBB && buffer[2] === 0xBF) {
|
|
43
|
+
return 'UTF-8';
|
|
44
|
+
}
|
|
45
|
+
if (buffer.length >= 2 && buffer[0] === 0xFF && buffer[1] === 0xFE) {
|
|
46
|
+
return 'UTF-16LE';
|
|
47
|
+
}
|
|
48
|
+
if (buffer.length >= 2 && buffer[0] === 0xFE && buffer[1] === 0xFF) {
|
|
49
|
+
return 'UTF-16BE';
|
|
50
|
+
}
|
|
51
|
+
// 简单的UTF-8检测
|
|
52
|
+
try {
|
|
53
|
+
const text = buffer.toString('utf8');
|
|
54
|
+
// 如果能够正确解码为UTF-8,则认为是UTF-8
|
|
55
|
+
if (Buffer.from(text, 'utf8').equals(buffer)) {
|
|
56
|
+
return 'UTF-8';
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
// 如果UTF-8解码失败,继续其他检测
|
|
61
|
+
}
|
|
62
|
+
// 检查是否为二进制文件
|
|
63
|
+
const textContent = buffer.toString('utf8', 0, Math.min(1024, buffer.length));
|
|
64
|
+
const hasNullBytes = buffer.includes(0);
|
|
65
|
+
const hasControlChars = /[\x00-\x08\x0E-\x1F\x7F]/.test(textContent);
|
|
66
|
+
if (hasNullBytes || hasControlChars) {
|
|
67
|
+
return 'Binary';
|
|
68
|
+
}
|
|
69
|
+
// 默认返回UTF-8
|
|
70
|
+
return 'UTF-8';
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
return 'Unknown';
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* 获取文件配置信息
|
|
78
|
+
* @param filePath 文件绝对路径
|
|
79
|
+
* @returns 文件配置信息
|
|
80
|
+
*/
|
|
81
|
+
export function getFileConfig(filePath) {
|
|
82
|
+
try {
|
|
83
|
+
// 检查文件是否存在
|
|
84
|
+
const stats = statSync(filePath);
|
|
85
|
+
if (!stats.isFile()) {
|
|
86
|
+
throw new Error('路径不是文件');
|
|
87
|
+
}
|
|
88
|
+
const sizeInBytes = stats.size;
|
|
89
|
+
const size = formatFileSize(sizeInBytes);
|
|
90
|
+
const encoding = detectEncoding(filePath);
|
|
91
|
+
const extension = extname(filePath);
|
|
92
|
+
const config = {
|
|
93
|
+
size,
|
|
94
|
+
sizeInBytes,
|
|
95
|
+
encoding,
|
|
96
|
+
extension,
|
|
97
|
+
exists: true
|
|
98
|
+
};
|
|
99
|
+
// 如果是视频文件,获取视频信息
|
|
100
|
+
if (isVideoFile(filePath)) {
|
|
101
|
+
try {
|
|
102
|
+
config.videoInfo = getVideoInfo(filePath);
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
// 如果获取视频信息失败,不抛出错误,只是不包含视频信息
|
|
106
|
+
console.warn(`获取视频信息失败: ${error}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return config;
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
// 文件不存在或无法访问
|
|
113
|
+
return {
|
|
114
|
+
size: '0 B',
|
|
115
|
+
sizeInBytes: 0,
|
|
116
|
+
encoding: 'Unknown',
|
|
117
|
+
extension: extname(filePath),
|
|
118
|
+
exists: false
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* 获取文件配置信息(异步版本)
|
|
124
|
+
* @param filePath 文件绝对路径
|
|
125
|
+
* @returns Promise<FileConfig>
|
|
126
|
+
*/
|
|
127
|
+
export async function getFileConfigAsync(filePath) {
|
|
128
|
+
const { stat, readFile } = await import('fs/promises');
|
|
129
|
+
try {
|
|
130
|
+
const stats = await stat(filePath);
|
|
131
|
+
if (!stats.isFile()) {
|
|
132
|
+
throw new Error('路径不是文件');
|
|
133
|
+
}
|
|
134
|
+
const sizeInBytes = stats.size;
|
|
135
|
+
const size = formatFileSize(sizeInBytes);
|
|
136
|
+
const encoding = await detectEncodingAsync(filePath);
|
|
137
|
+
const extension = extname(filePath);
|
|
138
|
+
const config = {
|
|
139
|
+
size,
|
|
140
|
+
sizeInBytes,
|
|
141
|
+
encoding,
|
|
142
|
+
extension,
|
|
143
|
+
exists: true
|
|
144
|
+
};
|
|
145
|
+
// 如果是视频文件,获取视频信息
|
|
146
|
+
if (isVideoFile(filePath)) {
|
|
147
|
+
try {
|
|
148
|
+
config.videoInfo = getVideoInfo(filePath);
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
// 如果获取视频信息失败,不抛出错误,只是不包含视频信息
|
|
152
|
+
console.warn(`获取视频信息失败: ${error}`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return config;
|
|
156
|
+
}
|
|
157
|
+
catch (error) {
|
|
158
|
+
return {
|
|
159
|
+
size: '0 B',
|
|
160
|
+
sizeInBytes: 0,
|
|
161
|
+
encoding: 'Unknown',
|
|
162
|
+
extension: extname(filePath),
|
|
163
|
+
exists: false
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* 异步检测文件编码格式
|
|
169
|
+
* @param filePath 文件路径
|
|
170
|
+
* @returns Promise<string>
|
|
171
|
+
*/
|
|
172
|
+
async function detectEncodingAsync(filePath) {
|
|
173
|
+
try {
|
|
174
|
+
const { readFile } = await import('fs/promises');
|
|
175
|
+
const buffer = await readFile(filePath);
|
|
176
|
+
// 检查BOM
|
|
177
|
+
if (buffer.length >= 3 && buffer[0] === 0xEF && buffer[1] === 0xBB && buffer[2] === 0xBF) {
|
|
178
|
+
return 'UTF-8';
|
|
179
|
+
}
|
|
180
|
+
if (buffer.length >= 2 && buffer[0] === 0xFF && buffer[1] === 0xFE) {
|
|
181
|
+
return 'UTF-16LE';
|
|
182
|
+
}
|
|
183
|
+
if (buffer.length >= 2 && buffer[0] === 0xFE && buffer[1] === 0xFF) {
|
|
184
|
+
return 'UTF-16BE';
|
|
185
|
+
}
|
|
186
|
+
// 简单的UTF-8检测
|
|
187
|
+
try {
|
|
188
|
+
const text = buffer.toString('utf8');
|
|
189
|
+
if (Buffer.from(text, 'utf8').equals(buffer)) {
|
|
190
|
+
return 'UTF-8';
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
// 继续其他检测
|
|
195
|
+
}
|
|
196
|
+
// 检查是否为二进制文件
|
|
197
|
+
const textContent = buffer.toString('utf8', 0, Math.min(1024, buffer.length));
|
|
198
|
+
const hasNullBytes = buffer.includes(0);
|
|
199
|
+
const hasControlChars = /[\x00-\x08\x0E-\x1F\x7F]/.test(textContent);
|
|
200
|
+
if (hasNullBytes || hasControlChars) {
|
|
201
|
+
return 'Binary';
|
|
202
|
+
}
|
|
203
|
+
return 'UTF-8';
|
|
204
|
+
}
|
|
205
|
+
catch (error) {
|
|
206
|
+
return 'Unknown';
|
|
207
|
+
}
|
|
208
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import sharp from 'sharp';
|
|
2
|
+
/**
|
|
3
|
+
* 支持的图片格式
|
|
4
|
+
*/
|
|
5
|
+
export type SupportedFormat = 'jpeg' | 'jpg' | 'png' | 'webp' | 'avif' | 'tiff' | 'gif';
|
|
6
|
+
/**
|
|
7
|
+
* 图片转换选项
|
|
8
|
+
*/
|
|
9
|
+
export interface ConvertOptions {
|
|
10
|
+
/** 输出质量 (1-100),仅对 JPEG、WebP、AVIF 有效 */
|
|
11
|
+
quality?: number;
|
|
12
|
+
/** 是否保持透明度,仅对 PNG、WebP 有效 */
|
|
13
|
+
keepTransparency?: boolean;
|
|
14
|
+
/** 输出宽度,保持宽高比 */
|
|
15
|
+
width?: number;
|
|
16
|
+
/** 输出高度,保持宽高比 */
|
|
17
|
+
height?: number;
|
|
18
|
+
/** 是否强制调整尺寸(不保持宽高比) */
|
|
19
|
+
forceResize?: boolean;
|
|
20
|
+
/** 压缩级别 (0-9),仅对 PNG 有效 */
|
|
21
|
+
compressionLevel?: number;
|
|
22
|
+
/** 是否渐进式编码,仅对 JPEG 有效 */
|
|
23
|
+
progressive?: boolean;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* 图片转换结果
|
|
27
|
+
*/
|
|
28
|
+
export interface ConvertResult {
|
|
29
|
+
/** 输入文件路径 */
|
|
30
|
+
inputPath: string;
|
|
31
|
+
/** 输出文件路径 */
|
|
32
|
+
outputPath: string;
|
|
33
|
+
/** 原始文件大小(字节) */
|
|
34
|
+
originalSize: number;
|
|
35
|
+
/** 转换后文件大小(字节) */
|
|
36
|
+
convertedSize: number;
|
|
37
|
+
/** 压缩率 */
|
|
38
|
+
compressionRatio: number;
|
|
39
|
+
/** 转换耗时(毫秒) */
|
|
40
|
+
processingTime: number;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* 将图片转换为指定格式
|
|
44
|
+
* @param inputPath 输入图片路径
|
|
45
|
+
* @param outputPath 输出图片路径
|
|
46
|
+
* @param format 目标格式
|
|
47
|
+
* @param options 转换选项
|
|
48
|
+
* @returns 转换结果
|
|
49
|
+
*/
|
|
50
|
+
export declare function convertImage(inputPath: string, outputPath: string, format: SupportedFormat, options?: ConvertOptions): Promise<ConvertResult>;
|
|
51
|
+
/**
|
|
52
|
+
* 批量转换图片格式
|
|
53
|
+
* @param files 文件列表,每个文件包含 inputPath, outputPath, format
|
|
54
|
+
* @param options 转换选项
|
|
55
|
+
* @returns 转换结果列表
|
|
56
|
+
*/
|
|
57
|
+
export declare function batchConvert(files: Array<{
|
|
58
|
+
inputPath: string;
|
|
59
|
+
outputPath: string;
|
|
60
|
+
format: SupportedFormat;
|
|
61
|
+
}>, options?: ConvertOptions): Promise<ConvertResult[]>;
|
|
62
|
+
/**
|
|
63
|
+
* 获取图片信息
|
|
64
|
+
* @param imagePath 图片路径
|
|
65
|
+
* @returns 图片信息
|
|
66
|
+
*/
|
|
67
|
+
export declare function getImageInfo(imagePath: string): Promise<{
|
|
68
|
+
path: string;
|
|
69
|
+
format: keyof sharp.FormatEnum;
|
|
70
|
+
width: number;
|
|
71
|
+
height: number;
|
|
72
|
+
size: number;
|
|
73
|
+
hasAlpha: boolean;
|
|
74
|
+
density: number;
|
|
75
|
+
space: keyof sharp.ColourspaceEnum;
|
|
76
|
+
channels: sharp.Channels;
|
|
77
|
+
depth: keyof sharp.DepthEnum;
|
|
78
|
+
}>;
|
|
79
|
+
/**
|
|
80
|
+
* 创建缩略图
|
|
81
|
+
* @param inputPath 输入图片路径
|
|
82
|
+
* @param outputPath 输出缩略图路径
|
|
83
|
+
* @param width 缩略图宽度
|
|
84
|
+
* @param height 缩略图高度
|
|
85
|
+
* @param format 输出格式
|
|
86
|
+
* @param options 转换选项
|
|
87
|
+
* @returns 转换结果
|
|
88
|
+
*/
|
|
89
|
+
export declare function createThumbnail(inputPath: string, outputPath: string, width: number, height: number, format?: SupportedFormat, options?: ConvertOptions): Promise<ConvertResult>;
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import sharp from 'sharp';
|
|
2
|
+
import { promises as fs } from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
/**
|
|
5
|
+
* 将图片转换为指定格式
|
|
6
|
+
* @param inputPath 输入图片路径
|
|
7
|
+
* @param outputPath 输出图片路径
|
|
8
|
+
* @param format 目标格式
|
|
9
|
+
* @param options 转换选项
|
|
10
|
+
* @returns 转换结果
|
|
11
|
+
*/
|
|
12
|
+
export async function convertImage(inputPath, outputPath, format, options = {}) {
|
|
13
|
+
const startTime = Date.now();
|
|
14
|
+
// 验证输入文件是否存在
|
|
15
|
+
try {
|
|
16
|
+
await fs.access(inputPath);
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
throw new Error(`输入文件不存在: ${inputPath}`);
|
|
20
|
+
}
|
|
21
|
+
// 获取原始文件大小
|
|
22
|
+
const originalStats = await fs.stat(inputPath);
|
|
23
|
+
const originalSize = originalStats.size;
|
|
24
|
+
// 确保输出目录存在
|
|
25
|
+
const outputDir = path.dirname(outputPath);
|
|
26
|
+
await fs.mkdir(outputDir, { recursive: true });
|
|
27
|
+
// 创建 Sharp 实例
|
|
28
|
+
let sharpInstance = sharp(inputPath);
|
|
29
|
+
// 处理尺寸调整
|
|
30
|
+
if (options.width || options.height) {
|
|
31
|
+
if (options.forceResize) {
|
|
32
|
+
sharpInstance = sharpInstance.resize(options.width, options.height, {
|
|
33
|
+
fit: 'fill'
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
sharpInstance = sharpInstance.resize(options.width, options.height, {
|
|
38
|
+
fit: 'inside',
|
|
39
|
+
withoutEnlargement: true
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
// 根据格式设置转换选项
|
|
44
|
+
switch (format.toLowerCase()) {
|
|
45
|
+
case 'jpeg':
|
|
46
|
+
case 'jpg':
|
|
47
|
+
sharpInstance = sharpInstance.jpeg({
|
|
48
|
+
quality: options.quality || 80,
|
|
49
|
+
progressive: options.progressive || false
|
|
50
|
+
});
|
|
51
|
+
break;
|
|
52
|
+
case 'png':
|
|
53
|
+
sharpInstance = sharpInstance.png({
|
|
54
|
+
quality: options.quality || 80,
|
|
55
|
+
compressionLevel: options.compressionLevel || 6,
|
|
56
|
+
progressive: options.progressive || false
|
|
57
|
+
});
|
|
58
|
+
break;
|
|
59
|
+
case 'webp':
|
|
60
|
+
sharpInstance = sharpInstance.webp({
|
|
61
|
+
quality: options.quality || 80,
|
|
62
|
+
lossless: false,
|
|
63
|
+
nearLossless: false,
|
|
64
|
+
smartSubsample: true
|
|
65
|
+
});
|
|
66
|
+
break;
|
|
67
|
+
case 'avif':
|
|
68
|
+
sharpInstance = sharpInstance.avif({
|
|
69
|
+
quality: options.quality || 80,
|
|
70
|
+
lossless: false
|
|
71
|
+
});
|
|
72
|
+
break;
|
|
73
|
+
case 'tiff':
|
|
74
|
+
sharpInstance = sharpInstance.tiff({
|
|
75
|
+
quality: options.quality || 80,
|
|
76
|
+
compression: 'lzw'
|
|
77
|
+
});
|
|
78
|
+
break;
|
|
79
|
+
case 'gif':
|
|
80
|
+
// GIF 转换需要特殊处理,因为 Sharp 不直接支持 GIF 输出
|
|
81
|
+
throw new Error('GIF 格式暂不支持输出,请使用其他格式');
|
|
82
|
+
default:
|
|
83
|
+
throw new Error(`不支持的格式: ${format}`);
|
|
84
|
+
}
|
|
85
|
+
// 执行转换
|
|
86
|
+
await sharpInstance.toFile(outputPath);
|
|
87
|
+
// 获取转换后文件大小
|
|
88
|
+
const convertedStats = await fs.stat(outputPath);
|
|
89
|
+
const convertedSize = convertedStats.size;
|
|
90
|
+
const processingTime = Date.now() - startTime;
|
|
91
|
+
return {
|
|
92
|
+
inputPath,
|
|
93
|
+
outputPath,
|
|
94
|
+
originalSize,
|
|
95
|
+
convertedSize,
|
|
96
|
+
compressionRatio: Math.round((1 - convertedSize / originalSize) * 100),
|
|
97
|
+
processingTime
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* 批量转换图片格式
|
|
102
|
+
* @param files 文件列表,每个文件包含 inputPath, outputPath, format
|
|
103
|
+
* @param options 转换选项
|
|
104
|
+
* @returns 转换结果列表
|
|
105
|
+
*/
|
|
106
|
+
export async function batchConvert(files, options = {}) {
|
|
107
|
+
const results = [];
|
|
108
|
+
for (const file of files) {
|
|
109
|
+
try {
|
|
110
|
+
const result = await convertImage(file.inputPath, file.outputPath, file.format, options);
|
|
111
|
+
results.push(result);
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
console.error(`转换失败 ${file.inputPath}:`, error);
|
|
115
|
+
// 继续处理其他文件
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return results;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* 获取图片信息
|
|
122
|
+
* @param imagePath 图片路径
|
|
123
|
+
* @returns 图片信息
|
|
124
|
+
*/
|
|
125
|
+
export async function getImageInfo(imagePath) {
|
|
126
|
+
try {
|
|
127
|
+
await fs.access(imagePath);
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
throw new Error(`图片文件不存在: ${imagePath}`);
|
|
131
|
+
}
|
|
132
|
+
const metadata = await sharp(imagePath).metadata();
|
|
133
|
+
const stats = await fs.stat(imagePath);
|
|
134
|
+
return {
|
|
135
|
+
path: imagePath,
|
|
136
|
+
format: metadata.format,
|
|
137
|
+
width: metadata.width,
|
|
138
|
+
height: metadata.height,
|
|
139
|
+
size: stats.size,
|
|
140
|
+
hasAlpha: metadata.hasAlpha,
|
|
141
|
+
density: metadata.density,
|
|
142
|
+
space: metadata.space,
|
|
143
|
+
channels: metadata.channels,
|
|
144
|
+
depth: metadata.depth
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* 创建缩略图
|
|
149
|
+
* @param inputPath 输入图片路径
|
|
150
|
+
* @param outputPath 输出缩略图路径
|
|
151
|
+
* @param width 缩略图宽度
|
|
152
|
+
* @param height 缩略图高度
|
|
153
|
+
* @param format 输出格式
|
|
154
|
+
* @param options 转换选项
|
|
155
|
+
* @returns 转换结果
|
|
156
|
+
*/
|
|
157
|
+
export async function createThumbnail(inputPath, outputPath, width, height, format = 'jpeg', options = {}) {
|
|
158
|
+
return convertImage(inputPath, outputPath, format, {
|
|
159
|
+
...options,
|
|
160
|
+
width,
|
|
161
|
+
height,
|
|
162
|
+
quality: options.quality || 85
|
|
163
|
+
});
|
|
164
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 常用转换场景示例
|
|
3
|
+
*/
|
|
4
|
+
export declare class ImageConverter {
|
|
5
|
+
/**
|
|
6
|
+
* 将图片转换为 WebP 格式(推荐用于 Web)
|
|
7
|
+
*/
|
|
8
|
+
static toWebP(inputPath: string, outputPath: string, quality?: number): Promise<import("./convert.js").ConvertResult>;
|
|
9
|
+
/**
|
|
10
|
+
* 将图片转换为 AVIF 格式(最新格式,压缩率更高)
|
|
11
|
+
*/
|
|
12
|
+
static toAVIF(inputPath: string, outputPath: string, quality?: number): Promise<import("./convert.js").ConvertResult>;
|
|
13
|
+
/**
|
|
14
|
+
* 将图片转换为 PNG 格式(保持透明度)
|
|
15
|
+
*/
|
|
16
|
+
static toPNG(inputPath: string, outputPath: string, keepTransparency?: boolean): Promise<import("./convert.js").ConvertResult>;
|
|
17
|
+
/**
|
|
18
|
+
* 将图片转换为 JPEG 格式(通用格式)
|
|
19
|
+
*/
|
|
20
|
+
static toJPEG(inputPath: string, outputPath: string, quality?: number): Promise<import("./convert.js").ConvertResult>;
|
|
21
|
+
/**
|
|
22
|
+
* 创建多种格式的图片(用于响应式 Web 设计)
|
|
23
|
+
*/
|
|
24
|
+
static createResponsiveImages(inputPath: string, outputDir: string, baseName: string, sizes: Array<{
|
|
25
|
+
width: number;
|
|
26
|
+
height: number;
|
|
27
|
+
suffix: string;
|
|
28
|
+
}>): Promise<any[]>;
|
|
29
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { convertImage, batchConvert, getImageInfo, createThumbnail, } from "./convert.js";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
/**
|
|
4
|
+
* 图片转换使用示例
|
|
5
|
+
*/
|
|
6
|
+
async function examples() {
|
|
7
|
+
try {
|
|
8
|
+
// 示例 1: 将 JPG 转换为 WebP
|
|
9
|
+
console.log("=== 示例 1: JPG 转 WebP ===");
|
|
10
|
+
const result1 = await convertImage("./input/sample.jpg", "./output/sample.webp", "webp", {
|
|
11
|
+
quality: 85,
|
|
12
|
+
width: 800,
|
|
13
|
+
height: 600,
|
|
14
|
+
});
|
|
15
|
+
console.log("转换结果:", result1);
|
|
16
|
+
// 示例 2: 将 PNG 转换为 AVIF
|
|
17
|
+
console.log("\n=== 示例 2: PNG 转 AVIF ===");
|
|
18
|
+
const result2 = await convertImage("./input/sample.png", "./output/sample.avif", "avif", {
|
|
19
|
+
quality: 90,
|
|
20
|
+
keepTransparency: true,
|
|
21
|
+
});
|
|
22
|
+
console.log("转换结果:", result2);
|
|
23
|
+
// 示例 3: 批量转换
|
|
24
|
+
console.log("\n=== 示例 3: 批量转换 ===");
|
|
25
|
+
const batchFiles = [
|
|
26
|
+
{
|
|
27
|
+
inputPath: "./input/image1.jpg",
|
|
28
|
+
outputPath: "./output/image1.webp",
|
|
29
|
+
format: "webp",
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
inputPath: "./input/image2.png",
|
|
33
|
+
outputPath: "./output/image2.avif",
|
|
34
|
+
format: "avif",
|
|
35
|
+
},
|
|
36
|
+
];
|
|
37
|
+
const batchResults = await batchConvert(batchFiles, {
|
|
38
|
+
quality: 80,
|
|
39
|
+
width: 1200,
|
|
40
|
+
});
|
|
41
|
+
console.log("批量转换结果:", batchResults);
|
|
42
|
+
// 示例 4: 获取图片信息
|
|
43
|
+
console.log("\n=== 示例 4: 获取图片信息 ===");
|
|
44
|
+
const imageInfo = await getImageInfo("./input/sample.jpg");
|
|
45
|
+
console.log("图片信息:", imageInfo);
|
|
46
|
+
// 示例 5: 创建缩略图
|
|
47
|
+
console.log("\n=== 示例 5: 创建缩略图 ===");
|
|
48
|
+
const thumbnailResult = await createThumbnail("./input/sample.jpg", "./output/thumbnail.jpg", 200, 200, "jpeg", { quality: 90 });
|
|
49
|
+
console.log("缩略图创建结果:", thumbnailResult);
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
console.error("转换过程中出现错误:", error);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* 常用转换场景示例
|
|
57
|
+
*/
|
|
58
|
+
export class ImageConverter {
|
|
59
|
+
/**
|
|
60
|
+
* 将图片转换为 WebP 格式(推荐用于 Web)
|
|
61
|
+
*/
|
|
62
|
+
static async toWebP(inputPath, outputPath, quality = 85) {
|
|
63
|
+
return convertImage(inputPath, outputPath, "webp", { quality });
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* 将图片转换为 AVIF 格式(最新格式,压缩率更高)
|
|
67
|
+
*/
|
|
68
|
+
static async toAVIF(inputPath, outputPath, quality = 80) {
|
|
69
|
+
return convertImage(inputPath, outputPath, "avif", { quality });
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* 将图片转换为 PNG 格式(保持透明度)
|
|
73
|
+
*/
|
|
74
|
+
static async toPNG(inputPath, outputPath, keepTransparency = true) {
|
|
75
|
+
return convertImage(inputPath, outputPath, "png", {
|
|
76
|
+
keepTransparency,
|
|
77
|
+
quality: 95,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* 将图片转换为 JPEG 格式(通用格式)
|
|
82
|
+
*/
|
|
83
|
+
static async toJPEG(inputPath, outputPath, quality = 90) {
|
|
84
|
+
return convertImage(inputPath, outputPath, "jpeg", {
|
|
85
|
+
quality,
|
|
86
|
+
progressive: true,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* 创建多种格式的图片(用于响应式 Web 设计)
|
|
91
|
+
*/
|
|
92
|
+
static async createResponsiveImages(inputPath, outputDir, baseName, sizes) {
|
|
93
|
+
const formats = [
|
|
94
|
+
{ format: "webp", quality: 85 },
|
|
95
|
+
{ format: "avif", quality: 80 },
|
|
96
|
+
{ format: "jpeg", quality: 90 },
|
|
97
|
+
];
|
|
98
|
+
const results = [];
|
|
99
|
+
for (const size of sizes) {
|
|
100
|
+
for (const { format, quality } of formats) {
|
|
101
|
+
const outputPath = path.join(outputDir, `${baseName}-${size.suffix}.${format}`);
|
|
102
|
+
const result = await convertImage(inputPath, outputPath, format, {
|
|
103
|
+
width: size.width,
|
|
104
|
+
height: size.height,
|
|
105
|
+
quality,
|
|
106
|
+
});
|
|
107
|
+
results.push(result);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return results;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
// // 如果直接运行此文件,执行示例
|
|
114
|
+
// if (import.meta.url === `file://${process.argv[1]}`) {
|
|
115
|
+
// examples();
|
|
116
|
+
// }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xiping/node-utils",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.39",
|
|
4
4
|
"description": "node-utils",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"author": "The-End-Hero <527409987@qq.com>",
|
|
@@ -25,20 +25,22 @@
|
|
|
25
25
|
"bugs": {
|
|
26
26
|
"url": "https://github.com/The-End-Hero/wang-ping/issues"
|
|
27
27
|
},
|
|
28
|
-
"gitHead": "
|
|
28
|
+
"gitHead": "189a73a39a274b78c9385c52cba91a3841198cfa",
|
|
29
29
|
"publishConfig": {
|
|
30
30
|
"access": "public",
|
|
31
31
|
"registry": "https://registry.npmjs.org/"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
34
|
"@xiping/srt-to-vtt": "1.0.37",
|
|
35
|
-
"chalk": "^5.
|
|
36
|
-
"
|
|
37
|
-
"
|
|
35
|
+
"chalk": "^5.6.2",
|
|
36
|
+
"fs-extra": "^11.3.2",
|
|
37
|
+
"sharp": "^0.34.4",
|
|
38
|
+
"shelljs": "0.10.0",
|
|
39
|
+
"srt-parser-2": "^1.2.3"
|
|
38
40
|
},
|
|
39
41
|
"devDependencies": {
|
|
40
|
-
"@types/node": "^22.
|
|
42
|
+
"@types/node": "^22.18.6",
|
|
41
43
|
"tslib": "^2.8.1",
|
|
42
|
-
"typescript": "^5.
|
|
44
|
+
"typescript": "^5.9.2"
|
|
43
45
|
}
|
|
44
46
|
}
|