@edmund32/edx-cli 0.1.2
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 +372 -0
- package/bin/edx-alias.js +5 -0
- package/bin/edx-app.js +5 -0
- package/bin/edx-hw.js +5 -0
- package/bin/edx-image.js +5 -0
- package/bin/edx-md.js +5 -0
- package/bin/edx.js +5 -0
- package/package.json +27 -0
- package/src/cli/edx.mjs +30 -0
- package/src/cli/run.mjs +17 -0
- package/src/commands/alias.mjs +301 -0
- package/src/commands/app.mjs +318 -0
- package/src/commands/hw.mjs +84 -0
- package/src/commands/image.mjs +217 -0
- package/src/commands/md.mjs +91 -0
- package/src/image/merge.mjs +233 -0
- package/src/image/split.mjs +271 -0
- package/src/platform/platform.mjs +13 -0
- package/src/utils/paths.mjs +8 -0
- package/src/utils/process.mjs +26 -0
- package/src/utils/result.mjs +17 -0
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import sharp from 'sharp';
|
|
4
|
+
|
|
5
|
+
const IMAGE_EXTS = new Set(['.jpg', '.jpeg', '.png', '.webp']);
|
|
6
|
+
|
|
7
|
+
export function isImageFile(filePath) {
|
|
8
|
+
return IMAGE_EXTS.has(path.extname(filePath).toLowerCase());
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function parsePositiveInteger(value, name) {
|
|
12
|
+
const parsed = Number(value);
|
|
13
|
+
|
|
14
|
+
if (!Number.isInteger(parsed) || parsed < 1) {
|
|
15
|
+
throw new Error(`${name} 必须是正整数`);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return parsed;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function parsePoint(value, name) {
|
|
22
|
+
if (!value) {
|
|
23
|
+
throw new Error(`${name} 缺少坐标值`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const parts = value.split(',').map((part) => Number(part.trim()));
|
|
27
|
+
|
|
28
|
+
if (parts.length !== 2 || parts.some((part) => !Number.isFinite(part) || part < 0 || part > 1)) {
|
|
29
|
+
throw new Error(`${name} 必须是 0-1 比例坐标,格式如 0.5,0`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
x: parts[0],
|
|
34
|
+
y: parts[1],
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function buildOutputRoot(targetPath) {
|
|
39
|
+
const absoluteTarget = path.resolve(targetPath);
|
|
40
|
+
const parent = path.dirname(absoluteTarget);
|
|
41
|
+
const name = path.basename(absoluteTarget);
|
|
42
|
+
|
|
43
|
+
return path.join(parent, `${name}_output`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function discoverImages(targetPath) {
|
|
47
|
+
const absoluteTarget = path.resolve(targetPath);
|
|
48
|
+
|
|
49
|
+
if (!fs.existsSync(absoluteTarget)) {
|
|
50
|
+
throw new Error(`找不到 target: ${targetPath}`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const stat = fs.statSync(absoluteTarget);
|
|
54
|
+
|
|
55
|
+
if (stat.isFile()) {
|
|
56
|
+
if (!isImageFile(absoluteTarget)) {
|
|
57
|
+
return [];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return [
|
|
61
|
+
{
|
|
62
|
+
filePath: absoluteTarget,
|
|
63
|
+
relativeDir: '',
|
|
64
|
+
targetRoot: path.dirname(absoluteTarget),
|
|
65
|
+
},
|
|
66
|
+
];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (!stat.isDirectory()) {
|
|
70
|
+
throw new Error(`target 不是文件或目录: ${targetPath}`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const images = [];
|
|
74
|
+
|
|
75
|
+
function walk(currentDir) {
|
|
76
|
+
const entries = fs.readdirSync(currentDir).sort((a, b) => a.localeCompare(b, 'zh-Hans-CN'));
|
|
77
|
+
|
|
78
|
+
for (const entry of entries) {
|
|
79
|
+
const fullPath = path.join(currentDir, entry);
|
|
80
|
+
const entryStat = fs.statSync(fullPath);
|
|
81
|
+
|
|
82
|
+
if (entryStat.isDirectory()) {
|
|
83
|
+
walk(fullPath);
|
|
84
|
+
} else if (entryStat.isFile() && isImageFile(fullPath)) {
|
|
85
|
+
images.push({
|
|
86
|
+
filePath: fullPath,
|
|
87
|
+
relativeDir: path.relative(absoluteTarget, path.dirname(fullPath)),
|
|
88
|
+
targetRoot: absoluteTarget,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
walk(absoluteTarget);
|
|
95
|
+
return images;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function outputBaseDir(image, outputRoot) {
|
|
99
|
+
if (!image.relativeDir) {
|
|
100
|
+
return outputRoot;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return path.join(
|
|
104
|
+
outputRoot,
|
|
105
|
+
...image.relativeDir.split(path.sep).filter(Boolean).map((segment) => `${segment}_output`),
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function imageStem(filePath) {
|
|
110
|
+
return path.basename(filePath, path.extname(filePath));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function cellFileName(stem, index) {
|
|
114
|
+
return `${stem}_${String(index).padStart(3, '0')}.png`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function clampExtractBox(box, meta) {
|
|
118
|
+
const left = Math.max(0, Math.min(box.left, meta.width - 1));
|
|
119
|
+
const top = Math.max(0, Math.min(box.top, meta.height - 1));
|
|
120
|
+
const maxWidth = meta.width - left;
|
|
121
|
+
const maxHeight = meta.height - top;
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
left,
|
|
125
|
+
top,
|
|
126
|
+
width: Math.max(1, Math.min(box.width, maxWidth)),
|
|
127
|
+
height: Math.max(1, Math.min(box.height, maxHeight)),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function regionToExtractBox(meta, from, to) {
|
|
132
|
+
if (!meta.width || !meta.height) {
|
|
133
|
+
throw new Error('无法读取图片尺寸');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (from.x >= to.x || from.y >= to.y) {
|
|
137
|
+
throw new Error('--from 必须位于 --to 的左下方');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const left = Math.round(from.x * meta.width);
|
|
141
|
+
const right = Math.round(to.x * meta.width);
|
|
142
|
+
const bottom = Math.round(from.y * meta.height);
|
|
143
|
+
const topFromBottom = Math.round(to.y * meta.height);
|
|
144
|
+
|
|
145
|
+
return clampExtractBox(
|
|
146
|
+
{
|
|
147
|
+
left,
|
|
148
|
+
top: meta.height - topFromBottom,
|
|
149
|
+
width: right - left,
|
|
150
|
+
height: topFromBottom - bottom,
|
|
151
|
+
},
|
|
152
|
+
meta,
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async function writeGridImage(image, outputRoot, rows, cols) {
|
|
157
|
+
const stem = imageStem(image.filePath);
|
|
158
|
+
const outputDir = path.join(outputBaseDir(image, outputRoot), stem);
|
|
159
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
160
|
+
|
|
161
|
+
const source = sharp(image.filePath);
|
|
162
|
+
const meta = await source.metadata();
|
|
163
|
+
|
|
164
|
+
if (!meta.width || !meta.height) {
|
|
165
|
+
throw new Error('无法读取图片尺寸');
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const cellWidth = Math.floor(meta.width / cols);
|
|
169
|
+
const cellHeight = Math.floor(meta.height / rows);
|
|
170
|
+
let index = 1;
|
|
171
|
+
const outputs = [];
|
|
172
|
+
|
|
173
|
+
for (let row = 0; row < rows; row++) {
|
|
174
|
+
for (let col = 0; col < cols; col++) {
|
|
175
|
+
const left = col * cellWidth;
|
|
176
|
+
const top = row * cellHeight;
|
|
177
|
+
const width = col === cols - 1 ? meta.width - left : cellWidth;
|
|
178
|
+
const height = row === rows - 1 ? meta.height - top : cellHeight;
|
|
179
|
+
const outputPath = path.join(outputDir, cellFileName(stem, index));
|
|
180
|
+
|
|
181
|
+
await sharp(image.filePath)
|
|
182
|
+
.extract(clampExtractBox({ left, top, width, height }, meta))
|
|
183
|
+
.png()
|
|
184
|
+
.toFile(outputPath);
|
|
185
|
+
|
|
186
|
+
outputs.push(outputPath);
|
|
187
|
+
index += 1;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return outputs;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async function writeRegionImage(image, outputRoot, from, to) {
|
|
195
|
+
const outputDir = outputBaseDir(image, outputRoot);
|
|
196
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
197
|
+
|
|
198
|
+
const meta = await sharp(image.filePath).metadata();
|
|
199
|
+
const box = regionToExtractBox(meta, from, to);
|
|
200
|
+
const outputPath = path.join(outputDir, `${imageStem(image.filePath)}.png`);
|
|
201
|
+
|
|
202
|
+
await sharp(image.filePath).extract(box).png().toFile(outputPath);
|
|
203
|
+
|
|
204
|
+
return [outputPath];
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function displayPath(filePath) {
|
|
208
|
+
return path.basename(filePath);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async function processImages(images, handler) {
|
|
212
|
+
const outputs = [];
|
|
213
|
+
const errors = [];
|
|
214
|
+
const logs = [];
|
|
215
|
+
const total = images.length;
|
|
216
|
+
|
|
217
|
+
if (total === 0) {
|
|
218
|
+
logs.push('未找到可处理图片');
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
let current = 0;
|
|
222
|
+
|
|
223
|
+
for (const image of images) {
|
|
224
|
+
current += 1;
|
|
225
|
+
const label = displayPath(image.filePath);
|
|
226
|
+
logs.push(`处理 ${current}/${total}: ${label}`);
|
|
227
|
+
|
|
228
|
+
try {
|
|
229
|
+
const imageOutputs = await handler(image);
|
|
230
|
+
outputs.push(...imageOutputs);
|
|
231
|
+
logs.push(`完成 ${current}/${total}: ${label} -> ${imageOutputs.length} 个文件`);
|
|
232
|
+
} catch (error) {
|
|
233
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
234
|
+
errors.push({
|
|
235
|
+
filePath: image.filePath,
|
|
236
|
+
message,
|
|
237
|
+
});
|
|
238
|
+
logs.push(`失败 ${current}/${total}: ${label} -> ${message}`);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return { outputs, errors, logs };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export async function splitGrid(targetPath, options) {
|
|
246
|
+
const rows = parsePositiveInteger(options.rows, '--rows');
|
|
247
|
+
const cols = parsePositiveInteger(options.cols, '--cols');
|
|
248
|
+
const images = discoverImages(targetPath);
|
|
249
|
+
const outputRoot = buildOutputRoot(targetPath);
|
|
250
|
+
const result = await processImages(images, (image) => writeGridImage(image, outputRoot, rows, cols));
|
|
251
|
+
|
|
252
|
+
return {
|
|
253
|
+
outputRoot,
|
|
254
|
+
modeLabel: 'grid 分割',
|
|
255
|
+
...result,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export async function splitRegion(targetPath, options) {
|
|
260
|
+
const from = parsePoint(options.from, '--from');
|
|
261
|
+
const to = parsePoint(options.to, '--to');
|
|
262
|
+
const images = discoverImages(targetPath);
|
|
263
|
+
const outputRoot = buildOutputRoot(targetPath);
|
|
264
|
+
const result = await processImages(images, (image) => writeRegionImage(image, outputRoot, from, to));
|
|
265
|
+
|
|
266
|
+
return {
|
|
267
|
+
outputRoot,
|
|
268
|
+
modeLabel: 'region 裁切',
|
|
269
|
+
...result,
|
|
270
|
+
};
|
|
271
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
|
|
4
|
+
export const PROJECT_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
|
|
5
|
+
|
|
6
|
+
export function fromRoot(...segments) {
|
|
7
|
+
return path.join(PROJECT_ROOT, ...segments);
|
|
8
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
|
|
3
|
+
export function commandExists(command) {
|
|
4
|
+
const probe = process.platform === 'win32' ? 'where' : 'command';
|
|
5
|
+
const args = process.platform === 'win32' ? [command] : ['-v', command];
|
|
6
|
+
const result = spawnSync(probe, args, {
|
|
7
|
+
shell: process.platform !== 'win32',
|
|
8
|
+
stdio: 'ignore',
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
return result.status === 0;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function runCommand(command, args = [], options = {}) {
|
|
15
|
+
const result = spawnSync(command, args, {
|
|
16
|
+
cwd: options.cwd ?? process.cwd(),
|
|
17
|
+
encoding: 'utf8',
|
|
18
|
+
shell: options.shell ?? false,
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
code: result.status ?? 1,
|
|
23
|
+
stdout: result.stdout ?? '',
|
|
24
|
+
stderr: result.stderr ?? '',
|
|
25
|
+
};
|
|
26
|
+
}
|