ss-tools-duck 1.0.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 +317 -0
- package/duck-cli.js +333 -0
- package/duck_decode.js +161 -0
- package/duck_encode.js +219 -0
- package/duck_payload_exporter.js +591 -0
- package/duck_video.js +201 -0
- package/index.d.ts +196 -0
- package/index.js +88 -0
- package/package.json +22 -0
package/duck_video.js
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* duck_video.js
|
|
3
|
+
*
|
|
4
|
+
* 视频合成 + 解帧工具(对应 Python _images_to_video 功能)
|
|
5
|
+
* 依赖:fluent-ffmpeg(需要系统安装 ffmpeg)
|
|
6
|
+
*
|
|
7
|
+
* 提供:
|
|
8
|
+
* imagesToMp4({ frames, fps, audioPath, outputPath }) → Promise<Buffer>
|
|
9
|
+
* mp4ToFrames({ mp4Input, outputDir }) → Promise<Buffer[]>
|
|
10
|
+
* encodeVideoFrames({ frames, fps, audioPath, password, title, compress, outputPath })
|
|
11
|
+
* 帧序列 → 鸭子图(一步封装)
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
'use strict';
|
|
15
|
+
|
|
16
|
+
const fs = require('fs');
|
|
17
|
+
const path = require('path');
|
|
18
|
+
const os = require('os');
|
|
19
|
+
const crypto = require('crypto');
|
|
20
|
+
const ffmpeg = require('fluent-ffmpeg');
|
|
21
|
+
const sharp = require('sharp');
|
|
22
|
+
const { encodeMp4 } = require('./duck_encode');
|
|
23
|
+
|
|
24
|
+
// ─── 工具:创建临时目录 ───────────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
function makeTempDir() {
|
|
27
|
+
const dir = path.join(os.tmpdir(), 'duck_ffmpeg_' + crypto.randomBytes(6).toString('hex'));
|
|
28
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
29
|
+
return dir;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function removeTempDir(dir) {
|
|
33
|
+
try {
|
|
34
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
35
|
+
} catch { /* ignore */ }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ─── 判断 ffmpeg 是否可用 ─────────────────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
async function checkFfmpegAvailable() {
|
|
41
|
+
return new Promise((resolve) => {
|
|
42
|
+
ffmpeg.getAvailableFormats((err) => resolve(!err));
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ─── 图片帧 → MP4 Buffer ──────────────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* 将图片帧数组合成 MP4(H.264, yuv420p,与 Python MoviePy 输出兼容)
|
|
50
|
+
*
|
|
51
|
+
* @param {object} opts
|
|
52
|
+
* @param {Buffer[]} opts.frames PNG / JPEG Buffer 数组
|
|
53
|
+
* @param {number} opts.fps 帧率
|
|
54
|
+
* @param {string} [opts.audioPath] 音频文件路径(WAV/MP3/AAC)
|
|
55
|
+
* @param {string} [opts.outputPath] 若提供则同时写文件
|
|
56
|
+
* @returns {Promise<Buffer>} MP4 字节
|
|
57
|
+
*/
|
|
58
|
+
async function imagesToMp4({ frames, fps, audioPath = null, outputPath = null }) {
|
|
59
|
+
if (!frames || frames.length === 0) throw new Error('frames 数组不能为空');
|
|
60
|
+
|
|
61
|
+
const tmpDir = makeTempDir();
|
|
62
|
+
const frameDir = path.join(tmpDir, 'frames');
|
|
63
|
+
fs.mkdirSync(frameDir);
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
// ── 写出帧图片 ──────────────────────────────────────────────────────────
|
|
67
|
+
for (let i = 0; i < frames.length; i++) {
|
|
68
|
+
const framePath = path.join(frameDir, `frame_${String(i).padStart(6, '0')}.png`);
|
|
69
|
+
// 确保是 PNG 格式(sharp 自动转换)
|
|
70
|
+
const pngBuf = await sharp(frames[i]).png().toBuffer();
|
|
71
|
+
fs.writeFileSync(framePath, pngBuf);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ── 合成 MP4 ────────────────────────────────────────────────────────────
|
|
75
|
+
const tmpMp4 = path.join(tmpDir, 'output.mp4');
|
|
76
|
+
|
|
77
|
+
await new Promise((resolve, reject) => {
|
|
78
|
+
let cmd = ffmpeg()
|
|
79
|
+
.input(path.join(frameDir, 'frame_%06d.png'))
|
|
80
|
+
.inputFPS(fps)
|
|
81
|
+
.videoCodec('libx264')
|
|
82
|
+
.outputOptions([
|
|
83
|
+
'-pix_fmt yuv420p',
|
|
84
|
+
'-crf 16',
|
|
85
|
+
'-preset medium',
|
|
86
|
+
'-profile:v high',
|
|
87
|
+
'-movflags +faststart',
|
|
88
|
+
]);
|
|
89
|
+
|
|
90
|
+
if (audioPath && fs.existsSync(audioPath)) {
|
|
91
|
+
cmd = cmd
|
|
92
|
+
.input(audioPath)
|
|
93
|
+
.audioCodec('aac')
|
|
94
|
+
.outputOptions(['-shortest']); // 音频/视频取最短
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
cmd
|
|
98
|
+
.output(tmpMp4)
|
|
99
|
+
.on('end', resolve)
|
|
100
|
+
.on('error', reject)
|
|
101
|
+
.run();
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const mp4Buffer = fs.readFileSync(tmpMp4);
|
|
105
|
+
|
|
106
|
+
if (outputPath) {
|
|
107
|
+
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
|
108
|
+
fs.writeFileSync(outputPath, mp4Buffer);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return mp4Buffer;
|
|
112
|
+
|
|
113
|
+
} finally {
|
|
114
|
+
removeTempDir(tmpDir);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ─── MP4 → 帧数组 ─────────────────────────────────────────────────────────────
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* 将 MP4 解帧为 PNG Buffer 数组
|
|
122
|
+
*
|
|
123
|
+
* @param {object} opts
|
|
124
|
+
* @param {Buffer|string} opts.mp4Input MP4 Buffer 或文件路径
|
|
125
|
+
* @param {number} [opts.fps] 抽帧帧率(默认:原始帧率全提取)
|
|
126
|
+
* @returns {Promise<{ frames: Buffer[], fps: number }>}
|
|
127
|
+
*/
|
|
128
|
+
async function mp4ToFrames({ mp4Input, fps = null }) {
|
|
129
|
+
const tmpDir = makeTempDir();
|
|
130
|
+
let tmpMp4 = null;
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
// 若是 Buffer,先写临时文件
|
|
134
|
+
if (Buffer.isBuffer(mp4Input)) {
|
|
135
|
+
tmpMp4 = path.join(tmpDir, 'input.mp4');
|
|
136
|
+
fs.writeFileSync(tmpMp4, mp4Input);
|
|
137
|
+
} else {
|
|
138
|
+
tmpMp4 = mp4Input;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const frameDir = path.join(tmpDir, 'frames');
|
|
142
|
+
fs.mkdirSync(frameDir);
|
|
143
|
+
|
|
144
|
+
// 读取原始帧率
|
|
145
|
+
const meta = await new Promise((resolve, reject) => {
|
|
146
|
+
ffmpeg.ffprobe(tmpMp4, (err, data) => err ? reject(err) : resolve(data));
|
|
147
|
+
});
|
|
148
|
+
const videoStream = meta.streams.find(s => s.codec_type === 'video');
|
|
149
|
+
const srcFps = videoStream
|
|
150
|
+
? eval(videoStream.r_frame_rate) // "30/1" → 30
|
|
151
|
+
: 30;
|
|
152
|
+
const outFps = fps || srcFps;
|
|
153
|
+
|
|
154
|
+
// 提取帧
|
|
155
|
+
await new Promise((resolve, reject) => {
|
|
156
|
+
ffmpeg(tmpMp4)
|
|
157
|
+
.outputOptions([`-vf fps=${outFps}`])
|
|
158
|
+
.output(path.join(frameDir, 'frame_%06d.png'))
|
|
159
|
+
.on('end', resolve)
|
|
160
|
+
.on('error', reject)
|
|
161
|
+
.run();
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
const frameFiles = fs.readdirSync(frameDir)
|
|
165
|
+
.filter(f => f.endsWith('.png'))
|
|
166
|
+
.sort();
|
|
167
|
+
const frames = frameFiles.map(f => fs.readFileSync(path.join(frameDir, f)));
|
|
168
|
+
|
|
169
|
+
return { frames, fps: outFps };
|
|
170
|
+
|
|
171
|
+
} finally {
|
|
172
|
+
removeTempDir(tmpDir);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ─── 一步封装:帧序列 → 鸭子图 ──────────────────────────────────────────────
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* 视频帧序列 → 先合成 MP4 → 再编码为鸭子图
|
|
180
|
+
*
|
|
181
|
+
* @param {object} opts
|
|
182
|
+
* @param {Buffer[]} opts.frames 图片帧 Buffer 数组
|
|
183
|
+
* @param {number} opts.fps 帧率
|
|
184
|
+
* @param {string} [opts.audioPath] 音频文件路径
|
|
185
|
+
* @param {string} [opts.password]
|
|
186
|
+
* @param {string} [opts.title]
|
|
187
|
+
* @param {number} [opts.compress]
|
|
188
|
+
* @param {string} [opts.outputPath]
|
|
189
|
+
* @returns {Promise<{ imageBuffer: Buffer, filePath?: string }>}
|
|
190
|
+
*/
|
|
191
|
+
async function encodeVideoFrames({ frames, fps, audioPath = null, password = '', title = '', compress = 2, outputPath = null }) {
|
|
192
|
+
const mp4Buffer = await imagesToMp4({ frames, fps, audioPath });
|
|
193
|
+
return encodeMp4({ mp4Input: mp4Buffer, password, title, compress, outputPath });
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
module.exports = {
|
|
197
|
+
checkFfmpegAvailable,
|
|
198
|
+
imagesToMp4,
|
|
199
|
+
mp4ToFrames,
|
|
200
|
+
encodeVideoFrames,
|
|
201
|
+
};
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SS_tools Duck 隐写工具库 — TypeScript 类型声明
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
// ─── 公共选项类型 ─────────────────────────────────────────────────────────────
|
|
6
|
+
|
|
7
|
+
/** 压缩比(影响每像素隐藏的 bit 数):2 = 最低信息量,8 = 最高 */
|
|
8
|
+
export type CompressLevel = 2 | 6 | 8;
|
|
9
|
+
|
|
10
|
+
/** 编码共有选项 */
|
|
11
|
+
export interface EncodeOptions {
|
|
12
|
+
/** 加密密码;留空或省略则不加密 */
|
|
13
|
+
password?: string;
|
|
14
|
+
/** 显示在鸭子图右下角的标题 */
|
|
15
|
+
title?: string;
|
|
16
|
+
/** 压缩比,默认 2 */
|
|
17
|
+
compress?: CompressLevel;
|
|
18
|
+
/** 输出文件路径;省略则返回 Buffer 不写文件 */
|
|
19
|
+
outputPath?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** 编码结果 */
|
|
23
|
+
export interface EncodeResult {
|
|
24
|
+
/** 生成的鸭子图 PNG 字节 */
|
|
25
|
+
imageBuffer: Buffer;
|
|
26
|
+
/** 写出的文件路径(仅当传入 outputPath 时存在) */
|
|
27
|
+
filePath?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// ─── 解码相关 ─────────────────────────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
/** 解码结果 */
|
|
33
|
+
export interface DecodeResult {
|
|
34
|
+
/** 还原的原始字节(文本为 UTF-8 编码的 bytes) */
|
|
35
|
+
data: Buffer;
|
|
36
|
+
/** 文件扩展名,如 "txt" / "png" / "mp4" / "bin" */
|
|
37
|
+
ext: string;
|
|
38
|
+
/** 若 ext === "txt",已解码为字符串 */
|
|
39
|
+
text?: string;
|
|
40
|
+
/** 若 ext === "png",PNG 图片 Buffer */
|
|
41
|
+
imageBuffer?: Buffer;
|
|
42
|
+
/** 若 ext === "mp4" 或 "mp4.binpng",MP4 视频 Buffer */
|
|
43
|
+
mp4Buffer?: Buffer;
|
|
44
|
+
/** 若传入 outputDir,还原文件的输出路径 */
|
|
45
|
+
filePath?: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ─── 编码 API ─────────────────────────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
/** 单张图片 → 鸭子图 */
|
|
51
|
+
export function encodeImage(opts: EncodeOptions & { image: Buffer }): Promise<EncodeResult>;
|
|
52
|
+
|
|
53
|
+
/** 文本字符串 → 鸭子图 */
|
|
54
|
+
export function encodeText(opts: EncodeOptions & { text: string }): Promise<EncodeResult>;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* 任意二进制数据 → 鸭子图(通用接口)
|
|
58
|
+
* @param opts.rawBytes 原始字节
|
|
59
|
+
* @param opts.ext 扩展名(如 "pdf"、"zip")
|
|
60
|
+
*/
|
|
61
|
+
export function encodeBytes(opts: EncodeOptions & { rawBytes: Buffer; ext: string }): Promise<EncodeResult>;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* 多帧图片序列 → 独立鸭子图序列(每帧一个文件)
|
|
65
|
+
* @param opts.outputDir 输出目录;文件命名为 duck_000.png, duck_001.png …
|
|
66
|
+
*/
|
|
67
|
+
export function encodeImageSequence(opts: EncodeOptions & {
|
|
68
|
+
images: Buffer[];
|
|
69
|
+
outputDir?: string;
|
|
70
|
+
}): Promise<{ imageBuffers: Buffer[]; filePaths?: string[] }>;
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* MP4 视频(Buffer 或文件路径)→ 鸭子图(binpng 格式)
|
|
74
|
+
* @param opts.mp4Input MP4 字节 Buffer 或文件路径字符串
|
|
75
|
+
*/
|
|
76
|
+
export function encodeMp4(opts: EncodeOptions & { mp4Input: Buffer | string }): Promise<EncodeResult>;
|
|
77
|
+
|
|
78
|
+
// ─── 解码 API ─────────────────────────────────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
/** 从鸭子图 Buffer 或文件路径中还原隐藏数据 */
|
|
81
|
+
export function decodeDuckImage(opts: {
|
|
82
|
+
duckImage: Buffer | string;
|
|
83
|
+
/** 解密密码(无密码可省略) */
|
|
84
|
+
password?: string;
|
|
85
|
+
/** 还原文件的输出目录 */
|
|
86
|
+
outputDir?: string;
|
|
87
|
+
}): Promise<DecodeResult>;
|
|
88
|
+
|
|
89
|
+
/** 便捷接口:从文件路径解码 */
|
|
90
|
+
export function decodeFromFile(
|
|
91
|
+
duckFilePath: string,
|
|
92
|
+
password?: string,
|
|
93
|
+
outputDir?: string
|
|
94
|
+
): Promise<DecodeResult>;
|
|
95
|
+
|
|
96
|
+
/** 便捷接口:从 Buffer 解码 */
|
|
97
|
+
export function decodeFromBuffer(
|
|
98
|
+
duckBuffer: Buffer,
|
|
99
|
+
password?: string,
|
|
100
|
+
outputDir?: string
|
|
101
|
+
): Promise<DecodeResult>;
|
|
102
|
+
|
|
103
|
+
// ─── 视频合成 API ─────────────────────────────────────────────────────────────
|
|
104
|
+
|
|
105
|
+
/** 检查系统 ffmpeg 是否可用 */
|
|
106
|
+
export function checkFfmpegAvailable(): Promise<boolean>;
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* 图片帧数组 → MP4 Buffer
|
|
110
|
+
* @param opts.frames PNG/JPEG Buffer 数组(按顺序)
|
|
111
|
+
* @param opts.fps 帧率
|
|
112
|
+
* @param opts.audioPath 混音文件路径(WAV/MP3/AAC,可选)
|
|
113
|
+
* @param opts.outputPath 若提供则同时写出文件
|
|
114
|
+
*/
|
|
115
|
+
export function imagesToMp4(opts: {
|
|
116
|
+
frames: Buffer[];
|
|
117
|
+
fps: number;
|
|
118
|
+
audioPath?: string | null;
|
|
119
|
+
outputPath?: string | null;
|
|
120
|
+
}): Promise<Buffer>;
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* MP4 解帧为 PNG Buffer 数组
|
|
124
|
+
* @param opts.mp4Input MP4 Buffer 或文件路径
|
|
125
|
+
* @param opts.fps 抽帧帧率(省略则按原始帧率全提取)
|
|
126
|
+
*/
|
|
127
|
+
export function mp4ToFrames(opts: {
|
|
128
|
+
mp4Input: Buffer | string;
|
|
129
|
+
fps?: number | null;
|
|
130
|
+
}): Promise<{ frames: Buffer[]; fps: number }>;
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* 帧序列 → 合成 MP4 → 编码为鸭子图(一步封装)
|
|
134
|
+
*/
|
|
135
|
+
export function encodeVideoFrames(opts: EncodeOptions & {
|
|
136
|
+
frames: Buffer[];
|
|
137
|
+
fps: number;
|
|
138
|
+
audioPath?: string | null;
|
|
139
|
+
}): Promise<EncodeResult>;
|
|
140
|
+
|
|
141
|
+
// ─── 底层工具 API ─────────────────────────────────────────────────────────────
|
|
142
|
+
|
|
143
|
+
/** 计算指定 payload 长度所需的画布像素尺寸 */
|
|
144
|
+
export function requiredCanvasSize(payloadBits: number, compress?: CompressLevel): number;
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* 将字节数组转为二进制图 PNG(3 bytes = 1 pixel,用于存储 MP4 原始字节)
|
|
148
|
+
* @param bytes 原始字节
|
|
149
|
+
* @param size 目标图像尺寸(正方形,默认 512)
|
|
150
|
+
*/
|
|
151
|
+
export function bytesToBinaryImage(bytes: Buffer, size?: number): Promise<Buffer>;
|
|
152
|
+
|
|
153
|
+
/** 从二进制图 PNG 中还原字节(去除末尾零填充) */
|
|
154
|
+
export function binaryImageToBytes(pngBuffer: Buffer): Promise<Buffer>;
|
|
155
|
+
|
|
156
|
+
/** 构造文件头二进制数据 */
|
|
157
|
+
export function buildFileHeader(opts: {
|
|
158
|
+
ext: string;
|
|
159
|
+
dataLength: number;
|
|
160
|
+
password?: string;
|
|
161
|
+
}): Buffer;
|
|
162
|
+
|
|
163
|
+
/** 解析文件头 */
|
|
164
|
+
export function parseHeader(buf: Buffer, password?: string): {
|
|
165
|
+
ext: string;
|
|
166
|
+
dataStart: number;
|
|
167
|
+
dataLength: number;
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
/** 将 payload 隐写到 PNG 图像中,返回新的 PNG Buffer */
|
|
171
|
+
export function embedPayloadLSB(
|
|
172
|
+
pngBuffer: Buffer,
|
|
173
|
+
payloadBits: number[],
|
|
174
|
+
compress?: CompressLevel
|
|
175
|
+
): Promise<Buffer>;
|
|
176
|
+
|
|
177
|
+
/** 从 PNG 图像中提取隐写数据(尝试指定的 k 值) */
|
|
178
|
+
export function extractPayloadWithK(
|
|
179
|
+
pngBuffer: Buffer,
|
|
180
|
+
k: CompressLevel
|
|
181
|
+
): Promise<{ bits: number[]; bytesFromBits: (bits: number[]) => Buffer }>;
|
|
182
|
+
|
|
183
|
+
/** 完整流程:构造 header → embed → 返回鸭子图 Buffer */
|
|
184
|
+
export function exportDuckPayload(opts: {
|
|
185
|
+
rawBytes: Buffer;
|
|
186
|
+
ext: string;
|
|
187
|
+
password?: string;
|
|
188
|
+
title?: string;
|
|
189
|
+
compress?: CompressLevel;
|
|
190
|
+
}): Promise<Buffer>;
|
|
191
|
+
|
|
192
|
+
/** 用 SVG 生成鸭子图底图(返回 PNG Buffer) */
|
|
193
|
+
export function buildDuckImageBuffer(size: number, title?: string): Promise<Buffer>;
|
|
194
|
+
|
|
195
|
+
/** 生成鸭子 SVG 字符串 */
|
|
196
|
+
export function buildDuckImageSVG(size: number, title?: string): string;
|
package/index.js
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* index.js — SS_tools Duck 隐写工具库(Node.js 版)
|
|
3
|
+
*
|
|
4
|
+
* 公开 API:
|
|
5
|
+
*
|
|
6
|
+
* 编码(隐藏数据)
|
|
7
|
+
* ─────────────────────────────────────────────
|
|
8
|
+
* encodeImage({ image, password, title, compress, outputPath })
|
|
9
|
+
* 单张图片 → 鸭子图
|
|
10
|
+
*
|
|
11
|
+
* encodeText({ text, password, title, compress, outputPath })
|
|
12
|
+
* 文本字符串 → 鸭子图
|
|
13
|
+
*
|
|
14
|
+
* encodeBytes({ rawBytes, ext, password, title, compress, outputPath })
|
|
15
|
+
* 任意二进制数据 → 鸭子图(通用接口)
|
|
16
|
+
*
|
|
17
|
+
* encodeImageSequence({ images, password, title, compress, outputDir })
|
|
18
|
+
* 多帧图片 → 独立鸭子图序列
|
|
19
|
+
*
|
|
20
|
+
* encodeMp4({ mp4Input, password, title, compress, outputPath })
|
|
21
|
+
* MP4 文件 → 鸭子图
|
|
22
|
+
*
|
|
23
|
+
* 解码(还原数据)
|
|
24
|
+
* ─────────────────────────────────────────────
|
|
25
|
+
* decodeDuckImage({ duckImage, password, outputDir })
|
|
26
|
+
* 鸭子图(Buffer 或路径) → { data, ext, text?, imageBuffer?, mp4Buffer?, filePath? }
|
|
27
|
+
*
|
|
28
|
+
* decodeFromFile(duckFilePath, password?, outputDir?)
|
|
29
|
+
* 便捷:文件路径解码
|
|
30
|
+
*
|
|
31
|
+
* decodeFromBuffer(duckBuffer, password?, outputDir?)
|
|
32
|
+
* 便捷:Buffer 解码
|
|
33
|
+
*
|
|
34
|
+
* 视频合成(来自 duck_video,需系统安装 ffmpeg)
|
|
35
|
+
* ─────────────────────────────────────────────
|
|
36
|
+
* imagesToMp4({ frames, fps, audioPath, outputPath })
|
|
37
|
+
* 图片帧 Buffer 数组 → MP4 Buffer
|
|
38
|
+
*
|
|
39
|
+
* mp4ToFrames({ mp4Input, fps })
|
|
40
|
+
* MP4 → 帧 Buffer 数组
|
|
41
|
+
*
|
|
42
|
+
* encodeVideoFrames({ frames, fps, audioPath, password, title, compress, outputPath })
|
|
43
|
+
* 帧序列 → 鸭子图(一步封装)
|
|
44
|
+
*
|
|
45
|
+
* checkFfmpegAvailable()
|
|
46
|
+
* 检查系统 ffmpeg 是否可用
|
|
47
|
+
*
|
|
48
|
+
* 底层工具(来自 duck_payload_exporter)
|
|
49
|
+
* ─────────────────────────────────────────────
|
|
50
|
+
* buildFileHeader / parseHeader
|
|
51
|
+
* requiredCanvasSize
|
|
52
|
+
* embedPayloadLSB / extractPayloadWithK
|
|
53
|
+
* bytesToBinaryImage / binaryImageToBytes
|
|
54
|
+
* exportDuckPayload
|
|
55
|
+
*/
|
|
56
|
+
|
|
57
|
+
'use strict';
|
|
58
|
+
|
|
59
|
+
const encode = require('./duck_encode');
|
|
60
|
+
const decode = require('./duck_decode');
|
|
61
|
+
const exporter = require('./duck_payload_exporter');
|
|
62
|
+
const video = require('./duck_video');
|
|
63
|
+
|
|
64
|
+
module.exports = {
|
|
65
|
+
// ── 编码 ──────────────────────────────────────────────────────────────────
|
|
66
|
+
...encode,
|
|
67
|
+
|
|
68
|
+
// ── 解码 ──────────────────────────────────────────────────────────────────
|
|
69
|
+
...decode,
|
|
70
|
+
|
|
71
|
+
// ── 视频合成 ───────────────────────────────────────────────────────────────
|
|
72
|
+
checkFfmpegAvailable: video.checkFfmpegAvailable,
|
|
73
|
+
imagesToMp4: video.imagesToMp4,
|
|
74
|
+
mp4ToFrames: video.mp4ToFrames,
|
|
75
|
+
encodeVideoFrames: video.encodeVideoFrames,
|
|
76
|
+
|
|
77
|
+
// ── 底层工具(按需使用) ───────────────────────────────────────────────────
|
|
78
|
+
buildFileHeader: exporter.buildFileHeader,
|
|
79
|
+
parseHeader: exporter.parseHeader,
|
|
80
|
+
requiredCanvasSize: exporter.requiredCanvasSize,
|
|
81
|
+
embedPayloadLSB: exporter.embedPayloadLSB,
|
|
82
|
+
extractPayloadWithK: exporter.extractPayloadWithK,
|
|
83
|
+
bytesToBinaryImage: exporter.bytesToBinaryImage,
|
|
84
|
+
binaryImageToBytes: exporter.binaryImageToBytes,
|
|
85
|
+
exportDuckPayload: exporter.exportDuckPayload,
|
|
86
|
+
buildDuckImageBuffer: exporter.buildDuckImageBuffer,
|
|
87
|
+
buildDuckImageSVG: exporter.buildDuckImageSVG,
|
|
88
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ss-tools-duck",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Node.js implementation of SS_tools duck steganography encoder/decoder",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"types": "index.d.ts",
|
|
7
|
+
"bin": {
|
|
8
|
+
"duck-encode": "./duck-cli.js",
|
|
9
|
+
"duck-decode": "./duck-cli.js",
|
|
10
|
+
"duck-cli": "./duck-cli.js"
|
|
11
|
+
},
|
|
12
|
+
"scripts": {
|
|
13
|
+
"test": "node test.js",
|
|
14
|
+
"cli": "node duck-cli.js"
|
|
15
|
+
},
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"fluent-ffmpeg": "^2.1.3",
|
|
18
|
+
"glob": "^11.0.0",
|
|
19
|
+
"sharp": "^0.33.0"
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|