modules/png_support.js

/* global jsPDF, Deflater, PNG */
/**
 * @license
 * 
 * Copyright (c) 2014 James Robb, https://github.com/jamesbrobb
 *
 * Permission is hereby granted, free of charge, to any person obtaining
 * a copy of this software and associated documentation files (the
 * "Software"), to deal in the Software without restriction, including
 * without limitation the rights to use, copy, modify, merge, publish,
 * distribute, sublicense, and/or sell copies of the Software, and to
 * permit persons to whom the Software is furnished to do so, subject to
 * the following conditions:
 *
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
 * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
 * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
 * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 * ====================================================================
 */

/**
* jsPDF PNG PlugIn
* @name png_support
* @module
*/
(function (jsPDFAPI, global) {
  'use strict'

  /*
   * @see http://www.w3.org/TR/PNG-Chunks.html
   *
   Color    Allowed      Interpretation
   Type     Bit Depths

     0       1,2,4,8,16  Each pixel is a grayscale sample.

     2       8,16        Each pixel is an R,G,B triple.

     3       1,2,4,8     Each pixel is a palette index;
                         a PLTE chunk must appear.

     4       8,16        Each pixel is a grayscale sample,
                         followed by an alpha sample.

     6       8,16        Each pixel is an R,G,B triple,
                         followed by an alpha sample.
  */

  /*
   * PNG filter method types
   *
   * @see http://www.w3.org/TR/PNG-Filters.html
   * @see http://www.libpng.org/pub/png/book/chapter09.html
   *
   * This is what the value 'Predictor' in decode params relates to
   *
   * 15 is "optimal prediction", which means the prediction algorithm can change from line to line.
   * In that case, you actually have to read the first byte off each line for the prediction algorthim (which should be 0-4, corresponding to PDF 10-14) and select the appropriate unprediction algorithm based on that byte.
   *
     0       None
     1       Sub
     2       Up
     3       Average
     4       Paeth
   */

  var doesNotHavePngJS = function () {
    return typeof global.PNG !== 'function' || typeof global.FlateStream !== 'function';
  };

  var canCompress = function (value) {
    return value !== jsPDFAPI.image_compression.NONE && hasCompressionJS();
  };

  var hasCompressionJS = function () {
    return (typeof Deflater === 'function');
  };

  var compressBytes = function (bytes, lineLength, colorsPerPixel, compression) {
    var level = 5;
    var filter_method = filterUp;

    switch (compression) {
      case jsPDFAPI.image_compression.FAST:
        level = 3;
        filter_method = filterSub;
        break;

      case jsPDFAPI.image_compression.MEDIUM:
        level = 6;
        filter_method = filterAverage;
        break;

      case jsPDFAPI.image_compression.SLOW:
        level = 9;
        filter_method = filterPaeth;
        break;
    }

    bytes = applyPngFilterMethod(bytes, lineLength, colorsPerPixel, filter_method);

    var header = new Uint8Array(createZlibHeader(level));
    var checksum = jsPDF.API.adler32cs.fromBuffer(bytes.buffer);

    var deflate = new Deflater(level);
    var a = deflate.append(bytes);
    var cBytes = deflate.flush();

    var len = header.length + a.length + cBytes.length;

    var cmpd = new Uint8Array(len + 4);
    cmpd.set(header);
    cmpd.set(a, header.length);
    cmpd.set(cBytes, header.length + a.length);

    cmpd[len++] = (checksum >>> 24) & 0xff;
    cmpd[len++] = (checksum >>> 16) & 0xff;
    cmpd[len++] = (checksum >>> 8) & 0xff;
    cmpd[len++] = checksum & 0xff;

    return jsPDFAPI.__addimage__.arrayBufferToBinaryString(cmpd);
  };

  var createZlibHeader = function (level) {
    /*
     * @see http://www.ietf.org/rfc/rfc1950.txt for zlib header
     */
    var hdr = 30720;
    var flevel = Math.min(3, ((level - 1) & 0xff) >> 1);

    hdr |= (flevel << 6);
    hdr |= 0;//FDICT
    hdr += 31 - (hdr % 31);

    return [120, (hdr & 0xff) & 0xff];
  };

  var applyPngFilterMethod = function (bytes, lineLength, colorsPerPixel, filter_method) {
    var lines = bytes.length / lineLength,
      result = new Uint8Array(bytes.length + lines),
      filter_methods = getFilterMethods(),
      line, prevLine, offset;

    for (var i = 0; i < lines; i += 1) {
      offset = i * lineLength;
      line = bytes.subarray(offset, offset + lineLength);

      if (filter_method) {
        result.set(filter_method(line, colorsPerPixel, prevLine), offset + i);
      } else {
        var len = filter_methods.length,
          results = [];

        for (var j; j < len; j += 1) {
          results[j] = filter_methods[j](line, colorsPerPixel, prevLine);
        }

        var ind = getIndexOfSmallestSum(results.concat());

        result.set(results[ind], offset + i);
      }

      prevLine = line;
    }

    return result;
  };

  var filterNone = function (line) {
    /*var result = new Uint8Array(line.length + 1);
    result[0] = 0;
    result.set(line, 1);*/

    var result = Array.apply([], line);
    result.unshift(0);

    return result;
  };

  var filterSub = function (line, colorsPerPixel) {
    var result = [],
      len = line.length,
      left;

    result[0] = 1;

    for (var i = 0; i < len; i += 1) {
      left = line[i - colorsPerPixel] || 0;
      result[i + 1] = (line[i] - left + 0x0100) & 0xff;
    }

    return result;
  };

  var filterUp = function (line, colorsPerPixel, prevLine) {
    var result = [],
      len = line.length,
      up;

    result[0] = 2;

    for (var i = 0; i < len; i += 1) {
      up = prevLine && prevLine[i] || 0;
      result[i + 1] = (line[i] - up + 0x0100) & 0xff;
    }

    return result;
  };

  var filterAverage = function (line, colorsPerPixel, prevLine) {
    var result = [],
      len = line.length,
      left,
      up;

    result[0] = 3;

    for (var i = 0; i < len; i += 1) {
      left = line[i - colorsPerPixel] || 0;
      up = prevLine && prevLine[i] || 0;
      result[i + 1] = (line[i] + 0x0100 - ((left + up) >>> 1)) & 0xff;
    }

    return result;
  };

  var filterPaeth = function (line, colorsPerPixel, prevLine) {
    var result = [],
      len = line.length,
      left,
      up,
      upLeft,
      paeth;

    result[0] = 4;

    for (var i = 0; i < len; i += 1) {
      left = line[i - colorsPerPixel] || 0;
      up = prevLine && prevLine[i] || 0;
      upLeft = prevLine && prevLine[i - colorsPerPixel] || 0;
      paeth = paethPredictor(left, up, upLeft);
      result[i + 1] = (line[i] - paeth + 0x0100) & 0xff;
    }

    return result;
  };

  var paethPredictor = function (left, up, upLeft) {
    if (left === up && up === upLeft) {
      return left;
    }
    var pLeft = Math.abs(up - upLeft),
    pUp = Math.abs(left - upLeft),
    pUpLeft = Math.abs(left + up - upLeft - upLeft);
    return (pLeft <= pUp && pLeft <= pUpLeft) ? left : (pUp <= pUpLeft) ? up : upLeft;
  };

  var getFilterMethods = function () {
    return [filterNone, filterSub, filterUp, filterAverage, filterPaeth];
  };

  var getIndexOfSmallestSum = function (arrays) {
    var sum = [];

    for (var i = 0; i < arrays.length; i += 1) {
      sum.push(arrays[i].reduce(function (pv, cv) {return pv + Math.abs(cv);}, 0));
    }

    return sum.indexOf(Math.min.apply(null, sum));
  };

  var getPredictorFromCompression = function (compression) {
    var predictor;
    switch (compression) {
      case jsPDFAPI.image_compression.FAST:
        predictor = 11;
        break;

      case jsPDFAPI.image_compression.MEDIUM:
        predictor = 13;
        break;

      case jsPDFAPI.image_compression.SLOW:
        predictor = 14;
        break;

      default:
        predictor = 12;
        break;
    }
    return predictor;
  };

  /**
  * @name processPNG
  * @function
  * @ignore
  */
  jsPDFAPI.processPNG = function (imageData, index, alias, compression) {
    'use strict';

    var colorSpace,
      filter = this.decode.FLATE_DECODE,
      bitsPerComponent,
      image, decodeParameters = '', trns,
      colors, pal, smask,
      pixels, len, alphaData, imgData, hasColors, pixel,
      i, n;

    if (this.__addimage__.isArrayBuffer(imageData))
      imageData = new Uint8Array(imageData);

    if (this.__addimage__.isArrayBufferView(imageData)) {

      if (doesNotHavePngJS()) {
        throw new Error("PNG support requires png.js and zlib.js");
      }

      image = new PNG(imageData);
      imageData = image.imgData;
      bitsPerComponent = image.bits;
      colorSpace = image.colorSpace;
      colors = image.colors;

      /*
       * colorType 6 - Each pixel is an R,G,B triple, followed by an alpha sample.
       *
       * colorType 4 - Each pixel is a grayscale sample, followed by an alpha sample.
       *
       * Extract alpha to create two separate images, using the alpha as a sMask
       */
      if ([4, 6].indexOf(image.colorType) !== -1) {

        /*
         * processes 8 bit RGBA and grayscale + alpha images
         */
        if (image.bits === 8) {

          pixels = image.pixelBitlength == 32 ? new Uint32Array(image.decodePixels().buffer) : image.pixelBitlength == 16 ? new Uint16Array(image.decodePixels().buffer) : new Uint8Array(image.decodePixels().buffer);
          len = pixels.length;
          imgData = new Uint8Array(len * image.colors);
          alphaData = new Uint8Array(len);
          var pDiff = image.pixelBitlength - image.bits;
          i = 0;
          n = 0;
          var pbl;

          for (; i < len; i++) {
            pixel = pixels[i];
            pbl = 0;

            while (pbl < pDiff) {

              imgData[n++] = (pixel >>> pbl) & 0xff;
              pbl = pbl + image.bits;
            }

            alphaData[i] = (pixel >>> pbl) & 0xff;
          }
        }

        /*
         * processes 16 bit RGBA and grayscale + alpha images
         */
        if (image.bits === 16) {
          pixels = new Uint32Array(image.decodePixels().buffer);
          len = pixels.length;
          imgData = new Uint8Array((len * (32 / image.pixelBitlength)) * image.colors);
          alphaData = new Uint8Array(len * (32 / image.pixelBitlength));
          hasColors = image.colors > 1;
          i = 0;
          n = 0;
          var a = 0;

          while (i < len) {
            pixel = pixels[i++];

            imgData[n++] = (pixel >>> 0) & 0xFF;

            if (hasColors) {
              imgData[n++] = (pixel >>> 16) & 0xFF;

              pixel = pixels[i++];
              imgData[n++] = (pixel >>> 0) & 0xFF;
            }

            alphaData[a++] = (pixel >>> 16) & 0xFF;
          }
          bitsPerComponent = 8;
        }

        if (canCompress(compression)) {
          imageData = compressBytes(imgData, image.width * image.colors, image.colors, compression);
          smask = compressBytes(alphaData, image.width, 1, compression);
        } else {
          imageData = imgData;
          smask = alphaData;
          filter = undefined;
        }
      }

      /*
       * Indexed png. Each pixel is a palette index.
       */
      if (image.colorType === 3) {

        colorSpace = this.color_spaces.INDEXED;
        pal = image.palette;

        if (image.transparency.indexed) {

          var trans = image.transparency.indexed;
          var total = 0;
          i = 0;
          len = trans.length;

          for (; i < len; ++i) {
            total += trans[i];
          }

          total = total / 255;

          /*
           * a single color is specified as 100% transparent (0),
           * so we set trns to use a /Mask with that index
           */
          if (total === len - 1 && trans.indexOf(0) !== -1) {
            trns = [trans.indexOf(0)];

            /*
             * there's more than one colour within the palette that specifies
             * a transparency value less than 255, so we unroll the pixels to create an image sMask
             */
          } else if (total !== len) {
            pixels = image.decodePixels();
            alphaData = new Uint8Array(pixels.length);
            i = 0;
            len = pixels.length;

            for (; i < len; i++) {
              alphaData[i] = trans[pixels[i]];
            }

            smask = compressBytes(alphaData, image.width, 1);
          }
        }
      }

      var predictor = getPredictorFromCompression(compression);

      if (filter === this.decode.FLATE_DECODE) {
        decodeParameters = '/Predictor ' + predictor + ' ';
      }
      decodeParameters += '/Colors ' + colors + ' /BitsPerComponent ' + bitsPerComponent + ' /Columns ' + image.width;

      if (this.__addimage__.isArrayBuffer(imageData) || this.__addimage__.isArrayBufferView(imageData)) {
        imageData = this.__addimage__.arrayBufferToBinaryString(imageData);
      }

      if (smask && this.__addimage__.isArrayBuffer(smask) || this.__addimage__.isArrayBufferView(smask)) {
        smask = this.__addimage__.arrayBufferToBinaryString(smask);
      }

      return { alias: alias, data: imageData, index: index, filter: filter, decodeParameters: decodeParameters, transparency: trns, palette: pal, sMask: smask, predictor: predictor, width: image.width, height: image.height, bitsPerComponent: bitsPerComponent, colorSpace: colorSpace };
    }
  }

})(jsPDF.API, typeof self !== "undefined" && self || typeof window !== "undefined" && window || typeof global !== "undefined" && global || Function('return typeof this === "object" && this.content')() || Function('return this')());
// `self` is undefined in Firefox for Android content script context
// while `this` is nsIContentFrameMessageManager
// with an attribute `content` that corresponds to the window