Search code examples
javascripthtmlimage-processingcompressionpngcrush

Client PNG compression using javascript like pngcrush?


I'm developing HTML5 apps.

When user uploads image from their mobile, the size was too large.

I want to compress the image as PNG like the pngcrush way.

Is there any good way to choice on the frontend (like a javascript library)?

Or is it possible to port the pngcrush library to javascript?


Solution

  • There are a few projects out there which seem to be based around the idea of using emscripten (a LLVM-to-JavaScript compiler) to actually compile the source code from pngcrush to working JavaScript for the browser.

    The version for pngcrush-1.7.27 is currently the only one that doesn't seem to produce corrupted images for me. I put together an example which uses promises here: http://plnkr.co/edit/iLpbOjlYiacR04oGdXSI?p=preview

    Here's a basic usage example:

    var instance = new pngcrush();
    
    instance.exec(inputFile, function (stdoutEvent) {
      console.log(stdoutEvent.data.line);
    }).then(function (doneEvent) {
      var outputFile = new Blob([doneEvent.data.data], { type: 'image/png' });
      // do something with the outputFile
    });
    

    Here are the contents of the pngcrush-class.js file from the above plunker for reference:

    (function(exports) {
    
      var noop = function () {};
    
      function pngcrush () {
        this.callbacks = {
          'error':  [],
          'done':   [],
          'start':  [],
          'stdout': []
        };
      }
    
      pngcrush.prototype.exec = function (file, notify) {
        var self = this;
    
        if (this.execPromise) {
          return this.execPromise.catch(noop).then(function () {
            return self.exec(file, notify);
          });
        }
    
        if (file.type !== 'image/png') {
          return Promise.reject(file);
        }
    
        var promise = this.execPromise = this.readAsArrayBuffer(file).then(function (event) {
          var arrayBuffer = event.target.result;
          return self.initWorker().then(function (worker) {
            var done = new Promise(function (resolve, reject) {
              var offDone, offError, offStdout;
              offDone = self.once('done', function (event) {
                offError();
                offStdout();
                resolve(event);
              });
              offError = self.once('error', function (event) {
                offDone();
                offStdout();
                reject(event);
              });
              offStdout = self.on('stdout', function (event) {
                if (typeof notify === 'function') {
                  notify.call(self, event);
                }
              });
              worker.postMessage({
                'type': 'file',
                'data': new Uint8Array(arrayBuffer)
              });
              worker.postMessage({
                'type': 'command',
                'command': 'go'
              });
            });
            done.catch(noop).then(function () {
              worker.terminate();
            });
            return done;
          });
        });
    
        promise.catch(noop).then(function () {
          if (promise === self.execPromise) {
            delete self.execPromise;
          }
        });
    
        return promise;
      };
    
      pngcrush.prototype.initWorker = function () {
        var self = this;
    
        if (this.workerPromise) {
          return this.workerPromise;
        }
    
        var promise = this.workerPromise = new Promise(function (resolve, reject) {
          var worker = new Worker('worker.js');
          worker.onerror = function (event) {
            var callbacks = [];
            reject(event);
            Array.prototype.push.apply(callbacks, self.callbacks.error);
            while (callbacks.length) {
              callbacks.shift().call(self, event);
            }
          };
          worker.onmessage = function (event) {
            if (event.data.type === 'ready') {
              worker.onmessage = function (event) {
                var name = event.data.type;
                if (typeof self.callbacks[name] !== 'undefined') {
                  var callbacks = [];
                  Array.prototype.push.apply(callbacks, self.callbacks[name]);
                  while (callbacks.length) {
                    callbacks.shift().call(self, event);
                  }
                }
              };
              resolve(worker);
            }
          };
        });
    
        promise.catch(noop).then(function () {
          if (promise === self.workerPromise) {
            delete self.workerPromise;
          }
        });
    
        return promise;
      };
    
      pngcrush.prototype.on = function (name, callback) {
        var self = this;
        if (typeof this.callbacks[name] !== 'undefined' && typeof callback === 'function') {
          this.callbacks[name].push(callback);
          var off = (function () {
            var ran = false;
            return function () {
              if (ran === true) {
                return;
              }
              ran = true;
              var idx = self.callbacks[name].lastIndexOf(callback);
              if (idx !== -1) {
                self.callbacks[name].splice(idx - 1, 1);
              }
            };
          })();
          return off;
        }
        return noop;
      };
    
      pngcrush.prototype.once = function (name, callback) {
        var off = this.on(name, function () {
          off();
          callback.apply(this, arguments);
        });
        return off;
      };
    
      pngcrush.prototype.readAsArrayBuffer = function (file) {
        var fileReader = new FileReader();
        return new Promise(function (resolve, reject) {
          fileReader.onerror = reject;
          fileReader.onload  = resolve;
          fileReader.readAsArrayBuffer(file);
        });
      };
    
      pngcrush.prototype.readAsDataURL = function (file) {
        var fileReader = new FileReader();
        return new Promise(function (resolve, reject) {
          fileReader.onerror = reject;
          fileReader.onload  = resolve;
          fileReader.readAsDataURL(file);
        });
      };
    
      exports.pngcrush = pngcrush;
    
    })(this);