Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

is it possible to use imagemin-cli and keep the same folder structure of compressing files?

I'm trying to create imagemin script with npm scripts and using imagemin-cli for it. First, I copy files to dist (or .tmp for development) folder and then compress images with this scripts:

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/",
  ...
},

So, when I run these scripts, after compression all images are put inside the folder images/.

Is there a way to compress images and keep the folder structure? Maybe with another plugin or something else.

like image 844
lalexa Avatar asked Mar 01 '17 08:03

lalexa


2 Answers

Is it a way to compress images with keeping folder structure?

The short answer is no, not with 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 in the projects github repo.


Solution

A cross platform way to achieve your requirements is to write a custom node.js utility script that utilizes the imagemin API directly. So effectively... build your own CLI tool that can be run via npm-scripts.

The following gists show how this can be achieved...


imagemin.js

The utility node script is as follows:

#!/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);
});

Note: The code above uses version 1.0.5 of the imagemin API and not the latest version - Why? See point 1 under the Additional Notes section below.)


Uninstall and Install new packages

  1. Firstly uninstall imagemin-cli as it's no longer necessary:

$ npm un -D imagemin-cli

  1. Next install imagemin version 1.0.5 (This is an older package so may take npm longer to install than usual)

$ npm i -D [email protected]

  1. Then install cli-glob. This will be used to specify the glob pattern to match the images for optimizing.

$ npm i -D cli-glob


npm-scripts

Update your npm-scripts as follows:

...
"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",
    ...
},
...

Note: To optimize images using the gists shown above it's not necessary to use the two scripts named copy:prod and copy:dev shown in your original post/question)

  1. The glob \"app/src/... part of the script above uses cli-glob to match the necessary image source files.

  2. The paths are then piped to the imagemin.js utility node script.

  3. When the -v (verbose) argument/flag is included then each processed image is logged to the console. To omit logging simply remove the -v flag.

  4. The -o (output) argument/flag is used to specify the destination folder name. E.g. dist or .tmp. When the value for -o is omitted the resultant images are output to the project root directory.


Additional notes:

  1. The reason for using imagemin version 1.0.5 is because this API allows the src value to be specified as a single filepath. In versions greater than 2.0.0 the API expects the src value to be a glob pattern as shown in the latest version 5.2.2.

  2. The gists above assume imagemin.js is saved to a folder named bin which exists in the same folder as package.json. It can be changed to a preferred name, or an invisible folder by prefixing it with a dot [.] e.g. .scripts or .bin. Whatever you choose, you'll need to update the path to the script in npm-scripts accordingly.

like image 158
RobC Avatar answered Sep 21 '22 21:09

RobC


Update 2020

There's an unmerged (as of mid-june 2020) pull request by Gijs Rogé that enables preserving directory structure in the output directory.

You can install npm modules not yet listed in the registry by installing directly from Github, referencing a repo and even specific commit:
npm install https://github.com/<username>/<repository>#<commit> --save-dev

To install imagemin with Gijs Rogé’s fix, run...
npm install https://github.com/imagemin/imagemin#bfd7c547045f68ed92243c6a772f6265a08a687f --save-dev

...and enable the new option in your script by setting 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( ... )
        ]
});

A .jpg found in input_dir/some/sub/dir/image.jpg will now be processed and written to output_dir/input_dir/some/sub/dir/image.jpg.

Use destination: '.' to overwrite original files in place.

like image 21
robro Avatar answered Sep 24 '22 21:09

robro