123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351 |
- 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'
- }
|