You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
351 lines
9.6 KiB
351 lines
9.6 KiB
const Fs = require('fs');
|
|
const Path = require('path');
|
|
const Os = require('os');
|
|
const { exec } = require('child_process');
|
|
const ConfigManager = require('./config-manager');
|
|
const FileUtils = require('./utils/file-utils');
|
|
|
|
/** 包名 */
|
|
const PACKAGE_NAME = require('./package.json').name;
|
|
|
|
/**
|
|
* i18n
|
|
* @param {string} key
|
|
* @returns {string}
|
|
*/
|
|
const translate = (key) => Editor.T(`${PACKAGE_NAME}.${key}`);
|
|
|
|
/** 扩展名 */
|
|
const EXTENSION_NAME = translate('name');
|
|
|
|
/** 内置资源目录 */
|
|
const internalPath = Path.normalize('assets/internal/');
|
|
|
|
module.exports = {
|
|
|
|
/**
|
|
* 项目路径
|
|
* @type {string}
|
|
*/
|
|
projectPath: null,
|
|
|
|
/**
|
|
* 资源根目录路径
|
|
* @type {string}
|
|
*/
|
|
assetsPath: null,
|
|
|
|
/**
|
|
* 压缩引擎路径
|
|
* @type {string}
|
|
*/
|
|
pngquantPath: null,
|
|
|
|
/**
|
|
* 日志
|
|
* @type {{ successCount: number, failedCount: number, successInfo: string, failedInfo: string }}
|
|
*/
|
|
logger: null,
|
|
|
|
/**
|
|
* 需要排除的文件夹
|
|
* @type {string[]}
|
|
*/
|
|
excludeFolders: null,
|
|
|
|
/**
|
|
* 需要排除的文件
|
|
* @type {string[]}
|
|
*/
|
|
excludeFiles: null,
|
|
|
|
/**
|
|
* 扩展消息
|
|
* @type {{ [key: string]: Function }}
|
|
*/
|
|
messages: {
|
|
|
|
/**
|
|
* 打开设置面板
|
|
*/
|
|
'open-setting-panel'() {
|
|
Editor.Panel.open(`${PACKAGE_NAME}.setting`);
|
|
},
|
|
|
|
/**
|
|
* 读取配置
|
|
* @param {any} event
|
|
*/
|
|
'read-config'(event) {
|
|
const config = ConfigManager.get();
|
|
event.reply(null, config);
|
|
},
|
|
|
|
/**
|
|
* 保存配置
|
|
* @param {any} event
|
|
* @param {any} config
|
|
*/
|
|
'save-config'(event, config) {
|
|
const configFilePath = ConfigManager.set(config);
|
|
Editor.log(`[${EXTENSION_NAME}]`, translate('configSaved'), configFilePath);
|
|
event.reply(null, true);
|
|
},
|
|
|
|
},
|
|
|
|
/**
|
|
* 生命周期:加载
|
|
*/
|
|
load() {
|
|
// 绑定 this
|
|
this.onBuildStart = this.onBuildStart.bind(this);
|
|
this.onBuildFinished = this.onBuildFinished.bind(this);
|
|
// 监听事件
|
|
Editor.Builder.on('build-start', this.onBuildStart);
|
|
Editor.Builder.on('build-finished', this.onBuildFinished);
|
|
},
|
|
|
|
/**
|
|
* 生命周期:加载
|
|
*/
|
|
unload() {
|
|
// 取消事件监听
|
|
Editor.Builder.removeListener('build-start', this.onBuildStart);
|
|
Editor.Builder.removeListener('build-finished', this.onBuildFinished);
|
|
},
|
|
|
|
/**
|
|
* 构建开始回调
|
|
* @param {BuildOptions} options
|
|
* @param {Function} callback
|
|
*/
|
|
onBuildStart(options, callback) {
|
|
const config = ConfigManager.get();
|
|
if (config && config.enabled) {
|
|
Editor.log(`[${EXTENSION_NAME}]`, translate('willCompress'));
|
|
// 取消编辑器资源选中(解除占用)
|
|
Editor.Selection.clear('asset');
|
|
}
|
|
// Done
|
|
callback();
|
|
},
|
|
|
|
/**
|
|
* 构建完成回调
|
|
* @param {BuildOptions} options
|
|
* @param {Function} callback
|
|
*/
|
|
async onBuildFinished(options, callback) {
|
|
const config = ConfigManager.get();
|
|
|
|
// 未开启直接跳过
|
|
if (!config || !config.enabled) {
|
|
callback();
|
|
return;
|
|
}
|
|
|
|
// 获取项目路径
|
|
this.projectPath = Editor.Project.path || Editor.projectPath;
|
|
this.assetsPath = Path.join(this.projectPath, 'assets');
|
|
|
|
// 获取压缩引擎路径
|
|
const platform = Os.platform(),
|
|
pngquantPath = this.pngquantPath = Path.join(__dirname, enginePathMap[platform]);
|
|
// 设置引擎文件的执行权限(仅 macOS)
|
|
if (pngquantPath && platform === 'darwin') {
|
|
if (Fs.statSync(pngquantPath).mode != 33261) {
|
|
// 默认为 33188
|
|
Fs.chmodSync(pngquantPath, 33261);
|
|
}
|
|
}
|
|
|
|
// 压缩引擎路径
|
|
if (!pngquantPath) {
|
|
Editor.log(`[${EXTENSION_NAME}]`, translate('notSupport'), platform);
|
|
callback();
|
|
return;
|
|
}
|
|
|
|
// 准备
|
|
Editor.log(`[${EXTENSION_NAME}]`, translate('prepareCompress'));
|
|
|
|
// 组装压缩命令
|
|
const qualityParam = `--quality ${config.minQuality}-${config.maxQuality}`,
|
|
speedParam = `--speed ${config.speed}`,
|
|
skipParam = '--skip-if-larger',
|
|
outputParam = '--ext=.png',
|
|
writeParam = '--force',
|
|
// colorsParam = config.colors,
|
|
// compressOptions = `${qualityParam} ${speedParam} ${skipParam} ${outputParam} ${writeParam} ${colorsParam}`;
|
|
compressOptions = `${qualityParam} ${speedParam} ${skipParam} ${outputParam} ${writeParam}`;
|
|
|
|
// 需要排除的文件夹
|
|
this.excludeFolders = config.excludeFolders ? config.excludeFolders.map(value => Path.normalize(value)) : [];
|
|
// 需要排除的文件
|
|
this.excludeFiles = config.excludeFiles ? config.excludeFiles.map(value => Path.normalize(value)) : [];
|
|
|
|
// 重置日志
|
|
this.logger = {
|
|
successCount: 0,
|
|
failedCount: 0,
|
|
successInfo: '',
|
|
failedInfo: ''
|
|
};
|
|
|
|
// 开始压缩
|
|
Editor.log(`[${EXTENSION_NAME}]`, translate('startCompress'));
|
|
|
|
// 遍历项目资源
|
|
const dest = options.dest,
|
|
dirs = ['res', 'assets', 'subpackages', 'remote'];
|
|
for (let i = 0; i < dirs.length; i++) {
|
|
const dirPath = Path.join(dest, dirs[i]);
|
|
if (!Fs.existsSync(dirPath)) {
|
|
continue;
|
|
}
|
|
Editor.log(`[${EXTENSION_NAME}]`, translate('compressDir'), dirPath);
|
|
// 压缩并记录结果
|
|
await this.compress(dirPath, compressOptions);
|
|
}
|
|
|
|
// 打印压缩结果
|
|
this.printResults();
|
|
|
|
// Done
|
|
callback();
|
|
},
|
|
|
|
/**
|
|
* 压缩
|
|
* @param {string} srcPath 文件路径
|
|
* @param {object} options 压缩参数
|
|
*/
|
|
async compress(srcPath, options) {
|
|
const pngquantPath = this.pngquantPath,
|
|
tasks = [];
|
|
const handler = (filePath, stats) => {
|
|
// 过滤文件
|
|
if (!this.filter(filePath)) {
|
|
return;
|
|
}
|
|
// 加入压缩队列
|
|
tasks.push(new Promise(res => {
|
|
const sizeBefore = stats.size / 1024,
|
|
command = `"${pngquantPath}" ${options} -- "${filePath}"`;
|
|
// pngquant $OPTIONS -- "$FILE"
|
|
exec(command, (error, stdout, stderr) => {
|
|
this.recordResult(error, sizeBefore, filePath);
|
|
res();
|
|
});
|
|
}));
|
|
};
|
|
FileUtils.map(srcPath, handler);
|
|
await Promise.all(tasks);
|
|
},
|
|
|
|
/**
|
|
* 判断资源是否可以进行压缩
|
|
* @param {string} path 路径
|
|
*/
|
|
filter(path) {
|
|
// 排除非 png 资源和内置资源
|
|
if (!path.endsWith('.png') || path.includes(internalPath)) {
|
|
return false;
|
|
}
|
|
// 排除指定文件夹和文件
|
|
const assetPath = this.getAssetPath(path);
|
|
if (assetPath) {
|
|
const excludeFolders = this.excludeFolders,
|
|
excludeFiles = this.excludeFiles;
|
|
// 文件夹
|
|
for (let i = 0, l = excludeFolders.length; i < l; i++) {
|
|
if (assetPath.startsWith(excludeFolders[i])) {
|
|
return false;
|
|
}
|
|
}
|
|
// 文件
|
|
for (let i = 0, l = excludeFiles.length; i < l; i++) {
|
|
if (assetPath.startsWith(excludeFiles[i])) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
// 测试通过
|
|
return true;
|
|
},
|
|
|
|
/**
|
|
* 获取资源源路径
|
|
* @param {string} filePath
|
|
* @return {string}
|
|
*/
|
|
getAssetPath(filePath) {
|
|
// 获取源路径(图像在项目中的实际路径)
|
|
const basename = Path.basename(filePath),
|
|
uuid = basename.slice(0, basename.indexOf('.')),
|
|
sourcePath = Editor.assetdb.uuidToFspath(uuid);
|
|
if (!sourcePath) {
|
|
// 图集资源
|
|
// 暂时还没有找到办法处理
|
|
return null;
|
|
}
|
|
return Path.relative(this.assetsPath, sourcePath);
|
|
},
|
|
|
|
/**
|
|
* 记录结果
|
|
* @param {object} error 错误
|
|
* @param {number} sizeBefore 压缩前尺寸
|
|
* @param {string} filePath 文件路径
|
|
*/
|
|
recordResult(error, sizeBefore, filePath) {
|
|
const log = this.logger;
|
|
if (!error) {
|
|
// 成功
|
|
const fileName = Path.basename(filePath),
|
|
sizeAfter = Fs.statSync(filePath).size / 1024,
|
|
savedSize = sizeBefore - sizeAfter,
|
|
savedRatio = savedSize / sizeBefore * 100;
|
|
log.successCount++;
|
|
log.successInfo += `\n + ${'Successful'.padEnd(13, ' ')} | ${fileName.padEnd(50, ' ')} | ${(sizeBefore.toFixed(2) + ' KB').padEnd(13, ' ')} -> ${(sizeAfter.toFixed(2) + ' KB').padEnd(13, ' ')} | ${(savedSize.toFixed(2) + ' KB').padEnd(13, ' ')} | ${(savedRatio.toFixed(2) + '%').padEnd(20, ' ')}`;
|
|
} else {
|
|
// 失败
|
|
log.failedCount++;
|
|
log.failedInfo += `\n - ${'Failed'.padEnd(13, ' ')} | ${filePath.replace(this.projectPath, '')}`;
|
|
switch (error.code) {
|
|
case 98:
|
|
log.failedInfo += `\n ${' '.repeat(10)} - 失败原因:压缩后体积增大(已经不能再压缩了)`;
|
|
break;
|
|
case 99:
|
|
log.failedInfo += `\n ${' '.repeat(10)} - 失败原因:压缩后质量低于已配置最低质量`;
|
|
break;
|
|
case 127:
|
|
log.failedInfo += `\n ${' '.repeat(10)} - 失败原因:压缩引擎没有执行权限`;
|
|
break;
|
|
default:
|
|
log.failedInfo += `\n ${' '.repeat(10)} - 失败原因:code ${error.code}`;
|
|
break;
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* 打印结果
|
|
*/
|
|
printResults() {
|
|
const log = this.logger,
|
|
header = `\n # ${'Result'.padEnd(13, ' ')} | ${'Name / Path'.padEnd(50, ' ')} | ${'Size Before'.padEnd(13, ' ')} -> ${'Size After'.padEnd(13, ' ')} | ${'Saved Size'.padEnd(13, ' ')} | ${'Compressibility'.padEnd(20, ' ')}`;
|
|
Editor.log('[PAC]', `压缩完成(${log.successCount} 张成功 | ${log.failedCount} 张失败)`);
|
|
Editor.log('[PAC]', '压缩日志 >>>' + header + log.successInfo + log.failedInfo);
|
|
},
|
|
|
|
}
|
|
|
|
/** 压缩引擎路径表 */
|
|
const enginePathMap = {
|
|
/** macOS */
|
|
'darwin': 'pngquant/macos/pngquant',
|
|
/** Windows */
|
|
'win32': 'pngquant/windows/pngquant'
|
|
}
|
|
|