@zzclub/z-cli
Version:
all-in-one 工具箱,专为提升日常及工作效率而生
329 lines (315 loc) • 11 kB
JavaScript
import {
writeFileContent,
getLocalConfig,
setHighLightStr,
} from "../utils/common.js";
import { getFormatedFileSize, replaceFileContent } from "../utils/file.js";
import chalk from "chalk";
import ora from "ora";
import path from "node:path";
import { dirname } from "node:path";
import { fileURLToPath } from "node:url";
import fs from "node:fs";
import sharp from "sharp";
import { picgoCmd } from "./picgo.js";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const supportFileTypes = ["png", "jpg", "jpeg", "gif", "webp"];
export const tinyCmd = {
name: "tiny",
description: "压缩图片",
options: [
{
flags: "-t, --type <fileType>",
description: "转换后的图片类型",
defaultValue: null,
},
{
flags: "-f, --file <file>",
description: "要压缩的图片文件",
defaultValue: null,
},
{
flags: "-d, --dir <dir>",
description: "压缩文件夹内所有文件",
defaultValue: null,
},
{
flags: "-co, --condition <condition>",
description: "压缩文件夹内所有名称包含[--condition]的图片文件",
defaultValue: null,
},
{
flags: "-q, --quality <quality>",
description: "压缩质量(1-100)",
defaultValue: 75,
},
{
flags: "-c, --colours <colours>",
description: "GIF色彩保留(2-256)",
defaultValue: 128,
},
{
flags: "-n, --name <name>",
description: "指定文件名输出",
defaultValue: "",
},
{
flags: "-m, --max <max>",
description: "限制要上传的文件大小(仅当开启 --picgo 时会用到)",
defaultValue: 60,
},
{
flags: "--picgo [type]",
description: "是否调用picgo",
defaultValue: null,
},
{
flags: "--no-picgo [type]",
description: "是否调用picgo",
defaultValue: null,
},
{
flags: "-ref, --replace-file <replaceFile>",
description: "要替换内容的文件",
defaultValue: "",
},
{
flags: "--no-replace [type]",
description:
"是否替换指定文件的内容, 替换规则斤仅针对Obsidian, 请看readme.md",
defaultValue: null,
},
{
flags: "--replace [type]",
description:
"是否替换指定文件的内容, 替换规则斤仅针对Obsidian, 请看readme.md",
defaultValue: null,
},
],
action: async (option) => {
let spinner = ora();
let config = await getLocalConfig();
// console.log(`OPTION`, option);
let { type, file, dir, condition, quality, colours } = option;
if (!file && !dir && !condition) {
spinner.fail("请指定要压缩的文件(--file=xxx)或文件夹(--dir=/a/b)");
process.exit(1);
}
// 是否需要替换内容, true时 需要收集list<Map>
let isNeedUploadAndReplace = !!(
// option.picgo &&
(option.replace && option.replaceFile)
);
let replaceMaps = [];
function collectReplaceMaps(replaceMaps, { text, newText }) {
if (isNeedUploadAndReplace) {
let map = {
text, // obsidian md 文件中 粘贴后的 值
newText, // 此时是压缩后的文件名, 在picgo里再替换成上传后的
};
replaceMaps.push(map);
}
}
async function tinyFile(file, statisticsCount, options = {}) {
try {
let stats = fs.statSync(file);
let isFile = stats.isFile();
if (isFile) {
// 文件类型
let extname = path.extname(file);
// 带后缀的文件名
let fileName = path.basename(file)?.split(".")[0];
// 文件路径
let dirPath = path.dirname(file);
// 文件类型
let filetype = extname.slice(1);
let fileSize = stats.size;
let beforeSize = getFormatedFileSize(fileSize);
// 是否在支持的格式里
// 如果正在批量压缩, 并且要求指定名称输出, 则加一个index后缀, 避免重名
let customFileName;
if (options.count && options.count > 1 && option.name) {
customFileName = `${option.name}${options.order + 1}`;
} else {
customFileName = option.name || `${fileName}-tiny`;
}
let outputPath =
path.resolve(process.cwd(), dirPath) +
"/" +
customFileName +
extname;
let inputPath = path.resolve(process.cwd(), file);
if (extname.slice(1) === "gif") {
// console.log(`option.colours`, option.colours);
await sharp(inputPath, {
animated: true,
limitInputPixels: false,
})
.gif({
colours: +option.colours,
})
.toFile(outputPath);
let afterStats = fs.statSync(outputPath);
let afterSize = getFormatedFileSize(afterStats.size);
let offPercent = ((1 - afterStats.size / fileSize) * 100).toFixed(
2
);
spinner.succeed(
`${chalk.red(beforeSize)} ${chalk.yellowBright(
"=>"
)} ${chalk.green(afterSize)} (${
offPercent >= 0 ? chalk.green("↓") : chalk.red("↑")
}${Math.abs(offPercent)}%)【 ${customFileName}${extname} 】`
);
// 收集压缩前后的文件名映射关系
collectReplaceMaps(replaceMaps, {
text: `${path.basename(file)}`,
newText: `${customFileName}${extname}`,
});
statisticsCount.ok++;
statisticsCount.okFiles.push(outputPath);
} else {
if (sharp(inputPath)[filetype]) {
await sharp(path.resolve(process.cwd(), file))
[filetype]({ quality: +quality })
.toFile(outputPath);
let afterStats = fs.statSync(outputPath);
let afterSize = getFormatedFileSize(afterStats.size);
let offPercent = ((1 - afterStats.size / fileSize) * 100).toFixed(
2
);
spinner.succeed(
`${chalk.red(beforeSize)} ${chalk.yellowBright(
"=>"
)} ${chalk.green(afterSize)} (${
offPercent >= 0 ? chalk.green("↓") : chalk.red("↑")
}${Math.abs(offPercent)}%)【 ${customFileName}${extname} 】`
);
// 收集压缩前后的文件名映射关系
collectReplaceMaps(replaceMaps, {
text: `${path.basename(file)}`,
newText: `${customFileName}${extname}`,
});
statisticsCount.ok++;
statisticsCount.okFiles.push(outputPath);
} else {
spinner.fail(
`不支持此文件类型[${filetype || fileName || file}]!`
);
statisticsCount.error++;
statisticsCount.okFiles.push(outputPath);
}
}
}
} catch (err) {
// console.log(`err`, err);
spinner.fail("出错啦, 文件不存在或不支持此类型 \n" + err);
statisticsCount.error++;
statisticsCount.errFiles.push(outputPath);
}
}
let statisticsCount = {
ok: 0,
okFiles: [],
err: 0,
errFiles: [],
};
// 指定了文件, 压缩
if (file) {
await tinyFile(file, statisticsCount);
}
// 指定文件夹, 翻译文件夹内所有文件
if (dir || condition) {
let inputDirPath = dir || "./";
let files;
try {
// 读取所有文件
files = fs.readdirSync(inputDirPath);
// spinner.succeed(`共有${files.length}个文件`);
} catch (err) {
spinner.fail("出错啦, 文件夹似乎不存在");
process.exit(1);
}
for (let i = 0; i < files.length; i++) {
let file = files[i];
let filePath = path.join(inputDirPath, file);
let stats = fs.statSync(filePath);
if (stats.isFile()) {
if (condition) {
let fileName = path.basename(file);
let index = fileName.indexOf(condition);
if (index > -1) {
let tip = setHighLightStr(fileName, condition);
spinner.succeed(`${tip}正在压缩`);
await tinyFile(filePath, statisticsCount, {
count: files.length,
order: i,
});
}
} else {
spinner.succeed(`正在压缩:${file}`);
await tinyFile(filePath, statisticsCount, {
count: files.length,
order: i,
});
}
} else {
spinner.fail(`${filePath}不是文件`);
}
}
spinner.succeed(`压缩成功 ${chalk.green(statisticsCount.ok)} 个`);
}
// 自动替换wiki链接
// 因为obsidian里已经有了一个插件, 可以自动上传到picgo并替换链接, 功能重复度很高
// 后续考虑给作者提个pr, 加入压缩功能
// console.log(`fileMaps`, replaceMaps);
if (option.replace) {
// 上传成功改的图片文件
let tinyFiles = statisticsCount.okFiles;
if (tinyFiles && tinyFiles.length) {
// await batchUploadByPicGo(tinyFiles, option);
if (!option.replace) process.exit(1);
spinner.start("开始替换文件内容, 替换后请仔细检查!");
console.log(`replaceMaps`, replaceMaps);
const fileContent = await replaceFileContent(
option.replaceFile,
replaceMaps
);
console.log(`fileContent`, fileContent);
if (!fileContent) {
spinner.fail("替换文件内容失败! 请检查要替换的文件是否存在!");
process.exit(1);
}
try {
fs.writeFileSync(option.replaceFile, fileContent, "utf-8");
spinner.succeed(
`[${chalk.red(
path.basename(option.replaceFile)
)}]图片链接替换完成!请前往检查!`
);
} catch (err) {
spinner.fail("图片链接替换失败!");
}
}
// 处理完后不再上传picgo
process.exit(1);
}
// 自动上传至picgo
if (option.picgo) {
spinner.start(`正在连接picgo`);
await picgoCmd.action({
tinyFiles: statisticsCount.okFiles,
max: option.max,
replace: isNeedUploadAndReplace,
replaceFile: option.replaceFile,
replaceMaps,
});
spinner.stop();
// 只有开启了picgo后时 才会使用替换功能 后续替换功能会独立出去 目前仅为满足自己所需
// if (option.replace && option.replaceFile) {
// spinner.start(`开始替换内容, 替换后请仔细检查!`);
// }
}
},
};