# modules/png_support.js

    /**
     * @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.
     * ====================================================================
     */

    import { jsPDF } from "../jspdf.js";
    import { zlibSync } from "../libs/fflate.js";
    import { PNG } from "../libs/png.js";

    /**
     * jsPDF PNG PlugIn
     * @name png_support
     * @module
     */
    (function(jsPDFAPI) {
      "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 canCompress = function(value) {
        return value !== jsPDFAPI.image_compression.NONE && hasCompressionJS();
      };

      var hasCompressionJS = function() {
        return typeof zlibSync === "function";
      };
      var compressBytes = function(bytes, lineLength, colorsPerPixel, compression) {
        var level = 4;
        var filter_method = filterUp;

        switch (compression) {
          case jsPDFAPI.image_compression.FAST:
            level = 1;
            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 dat = zlibSync(bytes, { level: level });
        return jsPDFAPI.__addimage__.arrayBufferToBinaryString(dat);
      };

      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 = arrays.map(function(value) {
          return value.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)) {
          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);