text: /* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
  /* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
  /* Copyright 2012 Mozilla Foundation
   *
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   *     http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  /* globals ColorSpace, DecodeStream, error, info, isArray, ImageKind, isStream,
             JpegStream, JpxImage, Name, Promise, Stream, warn */
  
  'use strict';
  
  var PDFImage = (function PDFImageClosure() {
    /**
     * Decode the image in the main thread if it supported. Resovles the promise
     * when the image data is ready.
     */
    function handleImageData(handler, xref, res, image) {
      if (image instanceof JpegStream && image.isNativelyDecodable(xref, res)) {
        // For natively supported jpegs send them to the main thread for decoding.
        var dict = image.dict;
        var colorSpace = dict.get('ColorSpace', 'CS');
        colorSpace = ColorSpace.parse(colorSpace, xref, res);
        var numComps = colorSpace.numComps;
        var resolvePromise;
        handler.send('JpegDecode', [image.getIR(), numComps], function(message) {
          var data = message.data;
          var stream = new Stream(data, 0, data.length, image.dict);
          resolvePromise(stream);
        });
        return new Promise(function (resolve) {
          resolvePromise = resolve;
        });
      } else {
        return Promise.resolve(image);
      }
    }
    /**
     * Decode and clamp a value. The formula is different from the spec because we
     * don't decode to float range [0,1], we decode it in the [0,max] range.
     */
    function decodeAndClamp(value, addend, coefficient, max) {
      value = addend + value * coefficient;
      // Clamp the value to the range
      return (value < 0 ? 0 : (value > max ? max : value));
    }
    function PDFImage(xref, res, image, inline, smask, mask, isMask) {
      this.image = image;
      var dict = image.dict;
      if (dict.has('Filter')) {
        var filter = dict.get('Filter').name;
        if (filter === 'JPXDecode') {
          var jpxImage = new JpxImage();
          jpxImage.parseImageProperties(image.stream);
          image.stream.reset();
          image.bitsPerComponent = jpxImage.bitsPerComponent;
          image.numComps = jpxImage.componentsCount;
        } else if (filter === 'JBIG2Decode') {
          image.bitsPerComponent = 1;
          image.numComps = 1;
        }
      }
      // TODO cache rendered images?
  
      this.width = dict.get('Width', 'W');
      this.height = dict.get('Height', 'H');
  
      if (this.width < 1 || this.height < 1) {
        error('Invalid image width: ' + this.width + ' or height: ' +
              this.height);
      }
  
      this.interpolate = dict.get('Interpolate', 'I') || false;
      this.imageMask = dict.get('ImageMask', 'IM') || false;
      this.matte = dict.get('Matte') || false;
  
      var bitsPerComponent = image.bitsPerComponent;
      if (!bitsPerComponent) {
        bitsPerComponent = dict.get('BitsPerComponent', 'BPC');
        if (!bitsPerComponent) {
          if (this.imageMask) {
            bitsPerComponent = 1;
          } else {
            error('Bits per component missing in image: ' + this.imageMask);
          }
        }
      }
      this.bpc = bitsPerComponent;
  
      if (!this.imageMask) {
        var colorSpace = dict.get('ColorSpace', 'CS');
        if (!colorSpace) {
          info('JPX images (which do not require color spaces)');
          switch (image.numComps) {
            case 1:
              colorSpace = Name.get('DeviceGray');
              break;
            case 3:
              colorSpace = Name.get('DeviceRGB');
              break;
            case 4:
              colorSpace = Name.get('DeviceCMYK');
              break;
            default:
              error('JPX images with ' + this.numComps +
                    ' color components not supported.');
          }
        }
        this.colorSpace = ColorSpace.parse(colorSpace, xref, res);
        this.numComps = this.colorSpace.numComps;
      }
  
      this.decode = dict.get('Decode', 'D');
      this.needsDecode = false;
      if (this.decode &&
          ((this.colorSpace && !this.colorSpace.isDefaultDecode(this.decode)) ||
           (isMask && !ColorSpace.isDefaultDecode(this.decode, 1)))) {
        this.needsDecode = true;
        // Do some preprocessing to avoid more math.
        var max = (1 << bitsPerComponent) - 1;
        this.decodeCoefficients = [];
        this.decodeAddends = [];
        for (var i = 0, j = 0; i < this.decode.length; i += 2, ++j) {
          var dmin = this.decode[i];
          var dmax = this.decode[i + 1];
          this.decodeCoefficients[j] = dmax - dmin;
          this.decodeAddends[j] = max * dmin;
        }
      }
  
      if (smask) {
        this.smask = new PDFImage(xref, res, smask, false);
      } else if (mask) {
        if (isStream(mask)) {
          this.mask = new PDFImage(xref, res, mask, false, null, null, true);
        } else {
          // Color key mask (just an array).
          this.mask = mask;
        }
      }
    }
    /**
     * Handles processing of image data and returns the Promise that is resolved
     * with a PDFImage when the image is ready to be used.
     */
    PDFImage.buildImage = function PDFImage_buildImage(handler, xref,
                                                       res, image, inline) {
      var imagePromise = handleImageData(handler, xref, res, image);
      var smaskPromise;
      var maskPromise;
  
      var smask = image.dict.get('SMask');
      var mask = image.dict.get('Mask');
  
      if (smask) {
        smaskPromise = handleImageData(handler, xref, res, smask);
        maskPromise = Promise.resolve(null);
      } else {
        smaskPromise = Promise.resolve(null);
        if (mask) {
          if (isStream(mask)) {
            maskPromise = handleImageData(handler, xref, res, mask);
          } else if (isArray(mask)) {
            maskPromise = Promise.resolve(mask);
          } else {
            warn('Unsupported mask format.');
            maskPromise = Promise.resolve(null);
          }
        } else {
          maskPromise = Promise.resolve(null);
        }
      }
      return Promise.all([imagePromise, smaskPromise, maskPromise]).then(
        function(results) {
          var imageData = results[0];
          var smaskData = results[1];
          var maskData = results[2];
          return new PDFImage(xref, res, imageData, inline, smaskData, maskData);
        });
    };
  
    /**
     * Resize an image using the nearest neighbor algorithm. Currently only
     * supports one and three component images.
     * @param {TypedArray} pixels The original image with one component.
     * @param {Number} bpc Number of bits per component.
     * @param {Number} components Number of color components, 1 or 3 is supported.
     * @param {Number} w1 Original width.
     * @param {Number} h1 Original height.
     * @param {Number} w2 New width.
     * @param {Number} h2 New height.
     * @param {TypedArray} dest (Optional) The destination buffer.
     * @param {Number} alpha01 (Optional) Size reserved for the alpha channel.
     * @return {TypedArray} Resized image data.
     */
    PDFImage.resize = function PDFImage_resize(pixels, bpc, components,
                                               w1, h1, w2, h2, dest, alpha01) {
  
      if (components !== 1 && components !== 3) {
        error('Unsupported component count for resizing.');
      }
  
      var length = w2 * h2 * components;
      var temp = dest ? dest : (bpc <= 8 ? new Uint8Array(length) :
          (bpc <= 16 ? new Uint16Array(length) : new Uint32Array(length)));
      var xRatio = w1 / w2;
      var yRatio = h1 / h2;
      var i, j, py, newIndex = 0, oldIndex;
      var xScaled = new Uint16Array(w2);
      var w1Scanline = w1 * components;
      if (alpha01 !== 1) {
        alpha01 = 0;
      }
  
      for (j = 0; j < w2; j++) {
        xScaled[j] = Math.floor(j * xRatio) * components;
      }
  
      if (components === 1) {
        for (i = 0; i < h2; i++) {
          py = Math.floor(i * yRatio) * w1Scanline;
          for (j = 0; j < w2; j++) {
            oldIndex = py + xScaled[j];
            temp[newIndex++] = pixels[oldIndex];
          }
        }
      } else if (components === 3) {
        for (i = 0; i < h2; i++) {
          py = Math.floor(i * yRatio) * w1Scanline;
          for (j = 0; j < w2; j++) {
            oldIndex = py + xScaled[j];
            temp[newIndex++] = pixels[oldIndex++];
            temp[newIndex++] = pixels[oldIndex++];
            temp[newIndex++] = pixels[oldIndex++];
            newIndex += alpha01;
          }
        }
      }
      return temp;
    };
  
    PDFImage.createMask =
        function PDFImage_createMask(imgArray, width, height, canTransfer,
                                     inverseDecode) {
      // If imgArray came from a DecodeStream, we're safe to transfer it.
      // Otherwise, copy it.
      var actualLength = imgArray.byteLength;
      var data;
      if (canTransfer) {
        data = imgArray;
      } else {
        data = new Uint8Array(actualLength);
        data.set(imgArray);
      }
      // Invert if necessary. It's safe to modify the array -- whether it's the
      // original or a copy, we're about to transfer it anyway, so nothing else
      // in this thread can be relying on its contents.
      if (inverseDecode) {
        for (var i = 0; i < actualLength; i++) {
          data[i] = ~data[i];
        }
      }
  
      return {data: data, width: width, height: height};
    };
  
    PDFImage.prototype = {
      get drawWidth() {
        return Math.max(this.width,
                        this.smask && this.smask.width || 0,
                        this.mask && this.mask.width || 0);
      },
      get drawHeight() {
        return Math.max(this.height,
                        this.smask && this.smask.height || 0,
                        this.mask && this.mask.height || 0);
      },
      decodeBuffer: function PDFImage_decodeBuffer(buffer) {
        var bpc = this.bpc;
        var numComps = this.numComps;
  
        var decodeAddends = this.decodeAddends;
        var decodeCoefficients = this.decodeCoefficients;
        var max = (1 << bpc) - 1;
        var i, ii;
  
        if (bpc === 1) {
          // If the buffer needed decode that means it just needs to be inverted.
          for (i = 0, ii = buffer.length; i < ii; i++) {
            buffer[i] = +!(buffer[i]);
          }
          return;
        }
        var index = 0;
        for (i = 0, ii = this.width * this.height; i < ii; i++) {
          for (var j = 0; j < numComps; j++) {
            buffer[index] = decodeAndClamp(buffer[index], decodeAddends[j],
                                           decodeCoefficients[j], max);
            index++;
          }
        }
      },
      getComponents: function PDFImage_getComponents(buffer) {
        var bpc = this.bpc;
  
        // This image doesn't require any extra work.
        if (bpc === 8) {
          return buffer;
        }
  
        var width = this.width;
        var height = this.height;
        var numComps = this.numComps;
  
        var length = width * height * numComps;
        var bufferPos = 0;
        var output = (bpc <= 8 ? new Uint8Array(length) :
          (bpc <= 16 ? new Uint16Array(length) : new Uint32Array(length)));
        var rowComps = width * numComps;
  
        var max = (1 << bpc) - 1;
        var i = 0, ii, buf;
  
        if (bpc === 1) {
          // Optimization for reading 1 bpc images.
          var mask, loop1End, loop2End;
          for (var j = 0; j < height; j++) {
            loop1End = i + (rowComps & ~7);
            loop2End = i + rowComps;
  
            // unroll loop for all full bytes
            while (i < loop1End) {
              buf = buffer[bufferPos++];
              output[i] = (buf >> 7) & 1;
              output[i + 1] = (buf >> 6) & 1;
              output[i + 2] = (buf >> 5) & 1;
              output[i + 3] = (buf >> 4) & 1;
              output[i + 4] = (buf >> 3) & 1;
              output[i + 5] = (buf >> 2) & 1;
              output[i + 6] = (buf >> 1) & 1;
              output[i + 7] = buf & 1;
              i += 8;
            }
  
            // handle remaing bits
            if (i < loop2End) {
              buf = buffer[bufferPos++];
              mask = 128;
              while (i < loop2End) {
                output[i++] = +!!(buf & mask);
                mask >>= 1;
              }
            }
          }
        } else {
          // The general case that handles all other bpc values.
          var bits = 0;
          buf = 0;
          for (i = 0, ii = length; i < ii; ++i) {
            if (i % rowComps === 0) {
              buf = 0;
              bits = 0;
            }
  
            while (bits < bpc) {
              buf = (buf << 8) | buffer[bufferPos++];
              bits += 8;
            }
  
            var remainingBits = bits - bpc;
            var value = buf >> remainingBits;
            output[i] = (value < 0 ? 0 : (value > max ? max : value));
            buf = buf & ((1 << remainingBits) - 1);
            bits = remainingBits;
          }
        }
        return output;
      },
      fillOpacity: function PDFImage_fillOpacity(rgbaBuf, width, height,
                                                 actualHeight, image) {
        var smask = this.smask;
        var mask = this.mask;
        var alphaBuf, sw, sh, i, ii, j;
  
        if (smask) {
          sw = smask.width;
          sh = smask.height;
          alphaBuf = new Uint8Array(sw * sh);
          smask.fillGrayBuffer(alphaBuf);
          if (sw != width || sh != height) {
            alphaBuf = PDFImage.resize(alphaBuf, smask.bpc, 1, sw, sh, width,
                                       height);
          }
        } else if (mask) {
          if (mask instanceof PDFImage) {
            sw = mask.width;
            sh = mask.height;
            alphaBuf = new Uint8Array(sw * sh);
            mask.numComps = 1;
            mask.fillGrayBuffer(alphaBuf);
  
            // Need to invert values in rgbaBuf
            for (i = 0, ii = sw * sh; i < ii; ++i) {
              alphaBuf[i] = 255 - alphaBuf[i];
            }
  
            if (sw != width || sh != height) {
              alphaBuf = PDFImage.resize(alphaBuf, mask.bpc, 1, sw, sh, width,
                                         height);
            }
          } else if (isArray(mask)) {
            // Color key mask: if any of the compontents are outside the range
            // then they should be painted.
            alphaBuf = new Uint8Array(width * height);
            var numComps = this.numComps;
            for (i = 0, ii = width * height; i < ii; ++i) {
              var opacity = 0;
              var imageOffset = i * numComps;
              for (j = 0; j < numComps; ++j) {
                var color = image[imageOffset + j];
                var maskOffset = j * 2;
                if (color < mask[maskOffset] || color > mask[maskOffset + 1]) {
                  opacity = 255;
                  break;
                }
              }
              alphaBuf[i] = opacity;
            }
          } else {
            error('Unknown mask format.');
          }
        }
  
        if (alphaBuf) {
          for (i = 0, j = 3, ii = width * actualHeight; i < ii; ++i, j += 4) {
            rgbaBuf[j] = alphaBuf[i];
          }
        } else {
          // No mask.
          for (i = 0, j = 3, ii = width * actualHeight; i < ii; ++i, j += 4) {
            rgbaBuf[j] = 255;
          }
        }
      },
      undoPreblend: function PDFImage_undoPreblend(buffer, width, height) {
        var matte = this.smask && this.smask.matte;
        if (!matte) {
          return;
        }
  
        function clamp(value) {
          return (value < 0 ? 0 : (value > 255 ? 255 : value)) | 0;
        }
  
        var matteRgb = this.colorSpace.getRgb(matte, 0);
        var length = width * height * 4;
        for (var i = 0; i < length; i += 4) {
          var alpha = buffer[i + 3];
          if (alpha === 0) {
            // according formula we have to get Infinity in all components
            // making it as white (tipical paper color) should be okay
            buffer[i] = 255;
            buffer[i + 1] = 255;
            buffer[i + 2] = 255;
            continue;
          }
          var k = 255 / alpha;
          buffer[i] = clamp((buffer[i] - matteRgb[0]) * k + matteRgb[0]);
          buffer[i + 1] = clamp((buffer[i + 1] - matteRgb[1]) * k + matteRgb[1]);
          buffer[i + 2] = clamp((buffer[i + 2] - matteRgb[2]) * k + matteRgb[2]);
        }
      },
      createImageData: function PDFImage_createImageData(forceRGBA) {
        var drawWidth = this.drawWidth;
        var drawHeight = this.drawHeight;
        var imgData = { // other fields are filled in below
          width: drawWidth,
          height: drawHeight
        };
  
        var numComps = this.numComps;
        var originalWidth = this.width;
        var originalHeight = this.height;
        var bpc = this.bpc;
  
        // Rows start at byte boundary.
        var rowBytes = (originalWidth * numComps * bpc + 7) >> 3;
        var imgArray = this.getImageBytes(originalHeight * rowBytes);
  
        if (!forceRGBA) {
          // If it is a 1-bit-per-pixel grayscale (i.e. black-and-white) image
          // without any complications, we pass a same-sized copy to the main
          // thread rather than expanding by 32x to RGBA form. This saves *lots*
          // of memory for many scanned documents. It's also much faster.
          //
          // Similarly, if it is a 24-bit-per pixel RGB image without any
          // complications, we avoid expanding by 1.333x to RGBA form.
          var kind;
          if (this.colorSpace.name === 'DeviceGray' && bpc === 1) {
            kind = ImageKind.GRAYSCALE_1BPP;
          } else if (this.colorSpace.name === 'DeviceRGB' && bpc === 8) {
            kind = ImageKind.RGB_24BPP;
          }
          if (kind && !this.smask && !this.mask && !this.needsDecode &&
              drawWidth === originalWidth && drawHeight === originalHeight) {
            imgData.kind = kind;
  
            // If imgArray came from a DecodeStream, we're safe to transfer it
            // (and thus neuter it) because it will constitute the entire
            // DecodeStream's data.  But if it came from a Stream, we need to
            // copy it because it'll only be a portion of the Stream's data, and
            // the rest will be read later on.
            if (this.image instanceof DecodeStream) {
              imgData.data = imgArray;
            } else {
              var newArray = new Uint8Array(imgArray.length);
              newArray.set(imgArray);
              imgData.data = newArray;
            }
            return imgData;
          }
        }
  
        // imgArray can be incomplete (e.g. after CCITT fax encoding).
        var actualHeight = 0 | (imgArray.length / rowBytes *
                           drawHeight / originalHeight);
  
        var comps = this.getComponents(imgArray);
  
        // If opacity data is present, use RGBA_32BPP form. Otherwise, use the
        // more compact RGB_24BPP form if allowable.
        var alpha01, maybeUndoPreblend;
        if (!forceRGBA && !this.smask && !this.mask) {
          imgData.kind = ImageKind.RGB_24BPP;
          imgData.data = new Uint8Array(drawWidth * drawHeight * 3);
          alpha01 = 0;
          maybeUndoPreblend = false;
        } else {
          imgData.kind = ImageKind.RGBA_32BPP;
          imgData.data = new Uint8Array(drawWidth * drawHeight * 4);
          alpha01 = 1;
          maybeUndoPreblend = true;
  
          // Color key masking (opacity) must be performed before decoding.
          this.fillOpacity(imgData.data, drawWidth, drawHeight, actualHeight,
                           comps);
        }
  
        if (this.needsDecode) {
          this.decodeBuffer(comps);
        }
        this.colorSpace.fillRgb(imgData.data, originalWidth, originalHeight,
                                drawWidth, drawHeight, actualHeight, bpc, comps,
                                alpha01);
        if (maybeUndoPreblend) {
          this.undoPreblend(imgData.data, drawWidth, actualHeight);
        }
  
        return imgData;
      },
      fillGrayBuffer: function PDFImage_fillGrayBuffer(buffer) {
        var numComps = this.numComps;
        if (numComps != 1) {
          error('Reading gray scale from a color image: ' + numComps);
        }
  
        var width = this.width;
        var height = this.height;
        var bpc = this.bpc;
  
        // rows start at byte boundary
        var rowBytes = (width * numComps * bpc + 7) >> 3;
        var imgArray = this.getImageBytes(height * rowBytes);
  
        var comps = this.getComponents(imgArray);
        var i, length;
  
        if (bpc === 1) {
          // inline decoding (= inversion) for 1 bpc images
          length = width * height;
          if (this.needsDecode) {
            // invert and scale to {0, 255}
            for (i = 0; i < length; ++i) {
              buffer[i] = (comps[i] - 1) & 255;
            }
          } else {
            // scale to {0, 255}
            for (i = 0; i < length; ++i) {
              buffer[i] = (-comps[i]) & 255;
            }
          }
          return;
        }
  
        if (this.needsDecode) {
          this.decodeBuffer(comps);
        }
        length = width * height;
        // we aren't using a colorspace so we need to scale the value
        var scale = 255 / ((1 << bpc) - 1);
        for (i = 0; i < length; ++i) {
          buffer[i] = (scale * comps[i]) | 0;
        }
      },
      getImageBytes: function PDFImage_getImageBytes(length) {
        this.image.reset();
        return this.image.getBytes(length);
      }
    };
    return PDFImage;
  })();
  