是否可以使用 imagemin-cli 并保持相同的压缩文件文件夹结构?
is it possible to use imagemin-cli and keep the same folder structure of compressing files?
我正在尝试使用 npm 脚本创建 imagemin
脚本并为其使用 imagemin-cli
。首先,我将文件复制到 dist
(或 .tmp
用于开发)文件夹,然后使用以下脚本压缩图像:
package.json
...
scripts {
"copy:dev": "cpx app/src/**/*.{html,png,jpg,mp4,webm} .tmp/",
"copy:prod": "cpx app/src/**/*.{html,png,jpg,mp4,webm} dist/",
"imagemin:dev": "imagemin app/src/images/**/* -o .tmp/images/",
"imagemin:prod": "imagemin app/src/images/**/* -o dist/images/",
...
},
因此,当我 运行 这些脚本时,压缩后所有图像都放在文件夹 images/
.
中
有没有办法压缩图片并保持文件夹结构?也许使用另一个插件或其他东西。
Is it a way to compress images with keeping folder structure?
简短的回答是否定的,不是 imagemin-cli
imagemin, (the API imagemin-cli is built upon), does not provide a mechanism to preserve the folder structure. See open issue/feature-request #191 在项目 github 仓库中。
解决方案
实现您的要求的跨平台方法是编写自定义 node.js 实用脚本,直接使用 imagemin API。如此有效...构建您自己的 CLI 工具,可以通过 npm-scripts
.
运行
以下要点展示了如何实现这一目标...
imagemin.js
实用程序node脚本如下:
#!/usr/bin/env node
'use strict';
var path = require('path');
var readline = require('readline');
var Imagemin = require('imagemin');
var outdir = process.env.PWD; // Default output folder.
var verbose = false; // Default no logging.
// The folder name specified MUST exist in the `glob` pattern of the npm-script.
var DEST_SUBROOT_FOLDER = 'images';
// Nice ticks for logging aren't supported via cmd.exe
var ticksymbol = process.env.npm_config_shell.indexOf('bash') !== -1 ? '✔' : '√';
var rl = readline.createInterface({
input: process.stdin,
output: null,
terminal: false
});
// Handle the optional `-o` argument for the destination folder.
if (process.argv.indexOf('-o') !== -1) {
outdir = process.argv[process.argv.indexOf('-o') + 1];
}
// Handle the optional `-v` argument for verbose logging.
if (process.argv.indexOf('-v') !== -1) {
verbose = true;
}
/**
* Utilizes the Imagemin API to create a new instance for optimizing each image.
* @param {String} srcpath - The filepath of the source image to optimize.
* @param {String} destpath - The destination path to save the resultant file.
* @param {Function} - The relevent `use` plugin (jpegtran|optipng|gifsicle).
*/
function imagemin(srcpath, destpath, plugin) {
var im = new Imagemin()
.src(srcpath)
.dest(destpath)
.use(plugin);
im.optimize(function (err, file) {
if (err) {
console.error('Error: ' + err);
process.exit(1);
}
if (file && verbose) {
console.log('\x1b[32m%s\x1b[0m', ticksymbol, destpath);
}
});
}
/**
* Obtains the destination path and file suffix from the original source path.
* @param {String} srcpath - The filepath for the image to optimize.
* @return {{dest: String, type: String}} dest path and ext (.jpg|.png|.gif).
*/
function getPathInfo(srcpath) {
var ext = path.extname(srcpath),
parts = srcpath.split(path.sep),
subpath = parts.slice(parts.indexOf(DEST_SUBROOT_FOLDER), parts.length);
subpath.unshift(outdir);
return {
dest: path.normalize(subpath.join(path.sep)),
ext: ext
};
}
/**
* Triggers the relevent imagemin process according to file suffix (jpg|png|gif).
* @param {String} srcpath - The filepath of the image to optimize.
*/
function optimizeImage(srcpath) {
var p = getPathInfo(srcpath);
switch (p.ext) {
case '.jpg':
imagemin(srcpath, p.dest, Imagemin.jpegtran({ progressive: true }));
break;
case '.png':
imagemin(srcpath, p.dest, Imagemin.optipng({ optimizationLevel: 5 }));
break;
case '.gif':
imagemin(srcpath, p.dest, Imagemin.gifsicle({ interlaced: true }));
break;
}
}
// Read each line from process.stdin (i.e. the filepath)
rl.on('line', function(srcpath) {
optimizeImage(srcpath);
});
注: 以上代码使用imagemin
1.0.5
版本API 而不是最新版本 - 为什么?请参阅下面“附加说明”部分下的第 1 点。)
卸载并安装新包
- 首先卸载
imagemin-cli
,因为它不再需要:
$ npm un -D imagemin-cli
- 下一步安装 imagemin 版本
1.0.5
(这是一个较旧的软件包,因此可能需要 npm
更长的时间来安装比平常)
$ npm i -D imagemin@1.0.5
- 然后安装cli-glob。这将用于指定 glob 模式以匹配要优化的图像。
$ npm i -D cli-glob
npm-scripts
更新您的 npm-scripts
如下:
...
"scripts": {
"imagemin:prod": "glob \"app/src/images/**/*.{png,jpg,gif}\" | node bin/imagemin -v -o dist",
"imagemin:dev": "glob \"app/src/images/**/*.{png,jpg,gif}\" | node bin/imagemin -v -o .tmp",
...
},
...
注意: 要使用上面显示的要点优化图像,不必使用名为 copy:prod
和 copy:dev
显示在你原来的 post/question)
上面脚本的glob \"app/src/...
部分使用cli-glob来匹配必要的图片源文件。
路径然后通过管道传输到 imagemin.js
实用程序节点脚本。
当包含 -v
(详细)argument/flag 时,每个处理过的图像都会记录到控制台。要省略日志记录,只需删除 -v
标志。
-o
(输出)argument/flag用于指定目标文件夹名称。例如。 dist
或 .tmp
。当省略 -o
的值时,生成的图像将输出到项目根目录。
补充说明:
我也有同样的问题,但我在节点模块中更改了 imagemin 的 index.js 文件。请将代码复制粘贴到节点模块
'use strict';
const fs = require('fs');
const path = require('path');
const fileType = require('file-type');
const globby = require('globby');
const makeDir = require('make-dir');
const pify = require('pify');
const pPipe = require('p-pipe');
const replaceExt = require('replace-ext');
const fsP = pify(fs);
const handleFile = (input, output, options) => fsP.readFile(input).then(data => {
const dest = output ? output : null;
if (options.plugins && !Array.isArray(options.plugins)) {
throw new TypeError('The `plugins` option should be an `Array`');
}
const pipe = options.plugins.length > 0 ? pPipe(options.plugins)(data) : Promise.resolve(data);
return pipe
.then(buffer => {
const ret = {
data: buffer,
path: (fileType(buffer) && fileType(buffer).ext === 'webp') ? replaceExt(dest, '.webp') : dest
};
if (!dest) {
return ret;
}
return fsP.writeFile(ret.path, ret.data)
.then(() => ret)
.then(function(result) {})
})
.catch(error => {
error.message = `Error in file: ${input}\n\n${error.message}`;
throw error;
});
});
module.exports = (input, output, options) => {
if (!Array.isArray(input)) {
return Promise.reject(new TypeError(`Expected an \`Array\`, got \`${typeof input}\``));
}
if (typeof output === 'object') {
options = output;
output = null;
}
options = Object.assign({plugins: []}, options);
options.plugins = options.use || options.plugins;
return globby(input, {onlyFiles: true}).then(paths => Promise.all(paths.map(x => handleFile(x, output, options))));
};
module.exports.buffer = (input, options) => {
if (!Buffer.isBuffer(input)) {
return Promise.reject(new TypeError(`Expected a \`Buffer\`, got \`${typeof input}\``));
}
options = Object.assign({plugins: []}, options);
options.plugins = options.use || options.plugins;
if (options.plugins.length === 0) {
return Promise.resolve(input);
}
return pPipe(options.plugins)(input);
};
2020 年更新
Gijs Rogé 有一个未合并的(截至 2020 年 6 月中旬)pull request,可以在输出目录中保留目录结构。
您可以通过直接从 Github 安装未在注册表中列出的 npm 模块,引用 repo 甚至特定提交:
npm install https://github.com/<username>/<repository>#<commit> --save-dev
要使用 Gijs Rogé 的修复安装 imagemin,运行...
npm install https://github.com/imagemin/imagemin#bfd7c547045f68ed92243c6a772f6265a08a687f --save-dev
...并通过设置 preserveDirectories: true
:
在脚本中启用新选项
// Note: imports and plugin configs have been omitted for brevity
const imagemin = require('imagemin');
const imageminMozjpeg = require('imagemin-mozjpeg');
...
(async () => {
const files = await imagemin(['input_dir/**/*.{jpg,jpeg,png,svg}'], {
destination: 'output_dir/',
✨preserveDirectories: true,
plugins: [
imageminMozjpeg( ... ),
imageminPngquant( ... ),
imageminSvgo( ... )
]
});
在 input_dir/some/sub/dir/image.jpg
中找到的 .jpg
现在将被处理并写入 output_dir/input_dir/some/sub/dir/image.jpg
。
使用destination: '.'
覆盖原来的文件。
以下脚本为每个文件夹运行一个单独的 imagemin
作业。
它解决了同样的问题。
const path = require('path');
const fs = require('fs');
const imagemin = require('imagemin');
const imageminWebp = require('imagemin-webp');
const COMPRESSED_FOLDER = '__compressed';
const TIMER_NAME = 'compressed';
(async () => {
console.time(TIMER_NAME);
const publicPath = path.resolve(__dirname, '../public');
const compressedFolderRegExp = new RegExp(COMPRESSED_FOLDER);
const publicPathRegExp = new RegExp(publicPath);
const folders = getAllDirectories(publicPath).filter(
(directoryName) => !directoryName.match(compressedFolderRegExp)
);
await Promise.all(
folders.map(async (folderPath) => {
const destination = folderPath.replace(
publicPathRegExp,
`${publicPath}/${COMPRESSED_FOLDER}`
);
console.log('compressing...', destination);
return imagemin([`${folderPath}/*.{jpg,png}`], {
destination,
plugins: [imageminWebp({ quality: 50 })],
});
})
);
console.timeEnd(TIMER_NAME);
process.exit();
})();
function getAllDirectories(filepath) {
const directoryPaths = fs
.readdirSync(filepath, { withFileTypes: true })
.filter((d) => d.isDirectory())
.map(({ name }) => `${filepath}/${name}`);
const childDirectories = directoryPaths.reduce(
(acc, directoryPath) => acc.concat(getAllDirectories(directoryPath)),
[]
);
return [filepath, ...childDirectories];
}
我正在尝试使用 npm 脚本创建 imagemin
脚本并为其使用 imagemin-cli
。首先,我将文件复制到 dist
(或 .tmp
用于开发)文件夹,然后使用以下脚本压缩图像:
package.json
...
scripts {
"copy:dev": "cpx app/src/**/*.{html,png,jpg,mp4,webm} .tmp/",
"copy:prod": "cpx app/src/**/*.{html,png,jpg,mp4,webm} dist/",
"imagemin:dev": "imagemin app/src/images/**/* -o .tmp/images/",
"imagemin:prod": "imagemin app/src/images/**/* -o dist/images/",
...
},
因此,当我 运行 这些脚本时,压缩后所有图像都放在文件夹 images/
.
有没有办法压缩图片并保持文件夹结构?也许使用另一个插件或其他东西。
Is it a way to compress images with keeping folder structure?
简短的回答是否定的,不是 imagemin-cli
imagemin, (the API imagemin-cli is built upon), does not provide a mechanism to preserve the folder structure. See open issue/feature-request #191 在项目 github 仓库中。
解决方案
实现您的要求的跨平台方法是编写自定义 node.js 实用脚本,直接使用 imagemin API。如此有效...构建您自己的 CLI 工具,可以通过 npm-scripts
.
以下要点展示了如何实现这一目标...
imagemin.js
实用程序node脚本如下:
#!/usr/bin/env node
'use strict';
var path = require('path');
var readline = require('readline');
var Imagemin = require('imagemin');
var outdir = process.env.PWD; // Default output folder.
var verbose = false; // Default no logging.
// The folder name specified MUST exist in the `glob` pattern of the npm-script.
var DEST_SUBROOT_FOLDER = 'images';
// Nice ticks for logging aren't supported via cmd.exe
var ticksymbol = process.env.npm_config_shell.indexOf('bash') !== -1 ? '✔' : '√';
var rl = readline.createInterface({
input: process.stdin,
output: null,
terminal: false
});
// Handle the optional `-o` argument for the destination folder.
if (process.argv.indexOf('-o') !== -1) {
outdir = process.argv[process.argv.indexOf('-o') + 1];
}
// Handle the optional `-v` argument for verbose logging.
if (process.argv.indexOf('-v') !== -1) {
verbose = true;
}
/**
* Utilizes the Imagemin API to create a new instance for optimizing each image.
* @param {String} srcpath - The filepath of the source image to optimize.
* @param {String} destpath - The destination path to save the resultant file.
* @param {Function} - The relevent `use` plugin (jpegtran|optipng|gifsicle).
*/
function imagemin(srcpath, destpath, plugin) {
var im = new Imagemin()
.src(srcpath)
.dest(destpath)
.use(plugin);
im.optimize(function (err, file) {
if (err) {
console.error('Error: ' + err);
process.exit(1);
}
if (file && verbose) {
console.log('\x1b[32m%s\x1b[0m', ticksymbol, destpath);
}
});
}
/**
* Obtains the destination path and file suffix from the original source path.
* @param {String} srcpath - The filepath for the image to optimize.
* @return {{dest: String, type: String}} dest path and ext (.jpg|.png|.gif).
*/
function getPathInfo(srcpath) {
var ext = path.extname(srcpath),
parts = srcpath.split(path.sep),
subpath = parts.slice(parts.indexOf(DEST_SUBROOT_FOLDER), parts.length);
subpath.unshift(outdir);
return {
dest: path.normalize(subpath.join(path.sep)),
ext: ext
};
}
/**
* Triggers the relevent imagemin process according to file suffix (jpg|png|gif).
* @param {String} srcpath - The filepath of the image to optimize.
*/
function optimizeImage(srcpath) {
var p = getPathInfo(srcpath);
switch (p.ext) {
case '.jpg':
imagemin(srcpath, p.dest, Imagemin.jpegtran({ progressive: true }));
break;
case '.png':
imagemin(srcpath, p.dest, Imagemin.optipng({ optimizationLevel: 5 }));
break;
case '.gif':
imagemin(srcpath, p.dest, Imagemin.gifsicle({ interlaced: true }));
break;
}
}
// Read each line from process.stdin (i.e. the filepath)
rl.on('line', function(srcpath) {
optimizeImage(srcpath);
});
注: 以上代码使用imagemin
1.0.5
版本API 而不是最新版本 - 为什么?请参阅下面“附加说明”部分下的第 1 点。)
卸载并安装新包
- 首先卸载
imagemin-cli
,因为它不再需要:
$ npm un -D imagemin-cli
- 下一步安装 imagemin 版本
1.0.5
(这是一个较旧的软件包,因此可能需要npm
更长的时间来安装比平常)
$ npm i -D imagemin@1.0.5
- 然后安装cli-glob。这将用于指定 glob 模式以匹配要优化的图像。
$ npm i -D cli-glob
npm-scripts
更新您的 npm-scripts
如下:
...
"scripts": {
"imagemin:prod": "glob \"app/src/images/**/*.{png,jpg,gif}\" | node bin/imagemin -v -o dist",
"imagemin:dev": "glob \"app/src/images/**/*.{png,jpg,gif}\" | node bin/imagemin -v -o .tmp",
...
},
...
注意: 要使用上面显示的要点优化图像,不必使用名为 copy:prod
和 copy:dev
显示在你原来的 post/question)
上面脚本的
glob \"app/src/...
部分使用cli-glob来匹配必要的图片源文件。路径然后通过管道传输到
imagemin.js
实用程序节点脚本。当包含
-v
(详细)argument/flag 时,每个处理过的图像都会记录到控制台。要省略日志记录,只需删除-v
标志。-o
(输出)argument/flag用于指定目标文件夹名称。例如。dist
或.tmp
。当省略-o
的值时,生成的图像将输出到项目根目录。
补充说明:
我也有同样的问题,但我在节点模块中更改了 imagemin 的 index.js 文件。请将代码复制粘贴到节点模块
'use strict';
const fs = require('fs');
const path = require('path');
const fileType = require('file-type');
const globby = require('globby');
const makeDir = require('make-dir');
const pify = require('pify');
const pPipe = require('p-pipe');
const replaceExt = require('replace-ext');
const fsP = pify(fs);
const handleFile = (input, output, options) => fsP.readFile(input).then(data => {
const dest = output ? output : null;
if (options.plugins && !Array.isArray(options.plugins)) {
throw new TypeError('The `plugins` option should be an `Array`');
}
const pipe = options.plugins.length > 0 ? pPipe(options.plugins)(data) : Promise.resolve(data);
return pipe
.then(buffer => {
const ret = {
data: buffer,
path: (fileType(buffer) && fileType(buffer).ext === 'webp') ? replaceExt(dest, '.webp') : dest
};
if (!dest) {
return ret;
}
return fsP.writeFile(ret.path, ret.data)
.then(() => ret)
.then(function(result) {})
})
.catch(error => {
error.message = `Error in file: ${input}\n\n${error.message}`;
throw error;
});
});
module.exports = (input, output, options) => {
if (!Array.isArray(input)) {
return Promise.reject(new TypeError(`Expected an \`Array\`, got \`${typeof input}\``));
}
if (typeof output === 'object') {
options = output;
output = null;
}
options = Object.assign({plugins: []}, options);
options.plugins = options.use || options.plugins;
return globby(input, {onlyFiles: true}).then(paths => Promise.all(paths.map(x => handleFile(x, output, options))));
};
module.exports.buffer = (input, options) => {
if (!Buffer.isBuffer(input)) {
return Promise.reject(new TypeError(`Expected a \`Buffer\`, got \`${typeof input}\``));
}
options = Object.assign({plugins: []}, options);
options.plugins = options.use || options.plugins;
if (options.plugins.length === 0) {
return Promise.resolve(input);
}
return pPipe(options.plugins)(input);
};
2020 年更新
Gijs Rogé 有一个未合并的(截至 2020 年 6 月中旬)pull request,可以在输出目录中保留目录结构。
您可以通过直接从 Github 安装未在注册表中列出的 npm 模块,引用 repo 甚至特定提交:
npm install https://github.com/<username>/<repository>#<commit> --save-dev
要使用 Gijs Rogé 的修复安装 imagemin,运行...
npm install https://github.com/imagemin/imagemin#bfd7c547045f68ed92243c6a772f6265a08a687f --save-dev
...并通过设置 preserveDirectories: true
:
// Note: imports and plugin configs have been omitted for brevity
const imagemin = require('imagemin');
const imageminMozjpeg = require('imagemin-mozjpeg');
...
(async () => {
const files = await imagemin(['input_dir/**/*.{jpg,jpeg,png,svg}'], {
destination: 'output_dir/',
✨preserveDirectories: true,
plugins: [
imageminMozjpeg( ... ),
imageminPngquant( ... ),
imageminSvgo( ... )
]
});
在 input_dir/some/sub/dir/image.jpg
中找到的 .jpg
现在将被处理并写入 output_dir/input_dir/some/sub/dir/image.jpg
。
使用destination: '.'
覆盖原来的文件。
以下脚本为每个文件夹运行一个单独的 imagemin
作业。
它解决了同样的问题。
const path = require('path');
const fs = require('fs');
const imagemin = require('imagemin');
const imageminWebp = require('imagemin-webp');
const COMPRESSED_FOLDER = '__compressed';
const TIMER_NAME = 'compressed';
(async () => {
console.time(TIMER_NAME);
const publicPath = path.resolve(__dirname, '../public');
const compressedFolderRegExp = new RegExp(COMPRESSED_FOLDER);
const publicPathRegExp = new RegExp(publicPath);
const folders = getAllDirectories(publicPath).filter(
(directoryName) => !directoryName.match(compressedFolderRegExp)
);
await Promise.all(
folders.map(async (folderPath) => {
const destination = folderPath.replace(
publicPathRegExp,
`${publicPath}/${COMPRESSED_FOLDER}`
);
console.log('compressing...', destination);
return imagemin([`${folderPath}/*.{jpg,png}`], {
destination,
plugins: [imageminWebp({ quality: 50 })],
});
})
);
console.timeEnd(TIMER_NAME);
process.exit();
})();
function getAllDirectories(filepath) {
const directoryPaths = fs
.readdirSync(filepath, { withFileTypes: true })
.filter((d) => d.isDirectory())
.map(({ name }) => `${filepath}/${name}`);
const childDirectories = directoryPaths.reduce(
(acc, directoryPath) => acc.concat(getAllDirectories(directoryPath)),
[]
);
return [filepath, ...childDirectories];
}