/* -*- 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 assert, ColorSpace, DecodeStream, Dict, Encodings,
           error, ErrorFont, Font, FONT_IDENTITY_MATRIX, fontCharsToUnicode,
           FontFlags, ImageKind, info, isArray, isCmd, isDict, isEOF, isName,
           isNum, isStream, isString, JpegStream, Lexer, Metrics,
           MurmurHash3_64, Name, Parser, Pattern, PDFImage, PDFJS, serifFonts,
           stdFontMap, symbolsFonts, getTilingPatternIR, warn, Util, Promise,
           RefSetCache, isRef, TextRenderingMode, CMapFactory, OPS,
           UNSUPPORTED_FEATURES, UnsupportedManager, NormalizedUnicodes,
           IDENTITY_MATRIX, reverseIfRtl */

'use strict';

var PartialEvaluator = (function PartialEvaluatorClosure() {
  function PartialEvaluator(pdfManager, xref, handler, pageIndex,
                            uniquePrefix, idCounters, fontCache) {
    this.pdfManager = pdfManager;
    this.xref = xref;
    this.handler = handler;
    this.pageIndex = pageIndex;
    this.uniquePrefix = uniquePrefix;
    this.idCounters = idCounters;
    this.fontCache = fontCache;
  }

  var TILING_PATTERN = 1, SHADING_PATTERN = 2;

  PartialEvaluator.prototype = {
    hasBlendModes: function PartialEvaluator_hasBlendModes(resources) {
      if (!isDict(resources)) {
        return false;
      }

      var processed = Object.create(null);
      if (resources.objId) {
        processed[resources.objId] = true;
      }

      var nodes = [resources];
      while (nodes.length) {
        var key;
        var node = nodes.shift();
        // First check the current resources for blend modes.
        var graphicStates = node.get('ExtGState');
        if (isDict(graphicStates)) {
          graphicStates = graphicStates.getAll();
          for (key in graphicStates) {
            var graphicState = graphicStates[key];
            var bm = graphicState['BM'];
            if (isName(bm) && bm.name !== 'Normal') {
              return true;
            }
          }
        }
        // Descend into the XObjects to look for more resources and blend modes.
        var xObjects = node.get('XObject');
        if (!isDict(xObjects)) {
          continue;
        }
        xObjects = xObjects.getAll();
        for (key in xObjects) {
          var xObject = xObjects[key];
          if (!isStream(xObject)) {
            continue;
          }
          var xResources = xObject.dict.get('Resources');
          // Checking objId to detect an infinite loop.
          if (isDict(xResources) &&
              (!xResources.objId || !processed[xResources.objId])) {
            nodes.push(xResources);
            if (xResources.objId) {
              processed[xResources.objId] = true;
            }
          }
        }
      }
      return false;
    },

    buildFormXObject: function PartialEvaluator_buildFormXObject(resources,
                                                                 xobj, smask,
                                                                 operatorList,
                                                                 initialState) {
      var matrix = xobj.dict.get('Matrix');
      var bbox = xobj.dict.get('BBox');
      var group = xobj.dict.get('Group');
      if (group) {
        var groupOptions = {
          matrix: matrix,
          bbox: bbox,
          smask: smask,
          isolated: false,
          knockout: false
        };

        var groupSubtype = group.get('S');
        if (isName(groupSubtype) && groupSubtype.name === 'Transparency') {
          groupOptions.isolated = (group.get('I') || false);
          groupOptions.knockout = (group.get('K') || false);
          var colorSpace = group.get('CS');
          groupOptions.colorSpace = (colorSpace ?
            ColorSpace.parseToIR(colorSpace, this.xref, resources) : null);
        }
        operatorList.addOp(OPS.beginGroup, [groupOptions]);
      }

      operatorList.addOp(OPS.paintFormXObjectBegin, [matrix, bbox]);

      this.getOperatorList(xobj, (xobj.dict.get('Resources') || resources),
                           operatorList, initialState);
      operatorList.addOp(OPS.paintFormXObjectEnd, []);

      if (group) {
        operatorList.addOp(OPS.endGroup, [groupOptions]);
      }
    },

    buildPaintImageXObject:
        function PartialEvaluator_buildPaintImageXObject(resources, image,
                                                         inline, operatorList,
                                                         cacheKey, cache) {
      var self = this;
      var dict = image.dict;
      var w = dict.get('Width', 'W');
      var h = dict.get('Height', 'H');

      if (!(w && isNum(w)) || !(h && isNum(h))) {
        warn('Image dimensions are missing, or not numbers.');
        return;
      }
      if (PDFJS.maxImageSize !== -1 && w * h > PDFJS.maxImageSize) {
        warn('Image exceeded maximum allowed size and was removed.');
        return;
      }

      var imageMask = (dict.get('ImageMask', 'IM') || false);
      var imgData, args;
      if (imageMask) {
        // This depends on a tmpCanvas being filled with the
        // current fillStyle, such that processing the pixel
        // data can't be done here. Instead of creating a
        // complete PDFImage, only read the information needed
        // for later.

        var width = dict.get('Width', 'W');
        var height = dict.get('Height', 'H');
        var bitStrideLength = (width + 7) >> 3;
        var imgArray = image.getBytes(bitStrideLength * height);
        var decode = dict.get('Decode', 'D');
        var canTransfer = image instanceof DecodeStream;
        var inverseDecode = (!!decode && decode[0] > 0);

        imgData = PDFImage.createMask(imgArray, width, height,
                                      canTransfer, inverseDecode);
        imgData.cached = true;
        args = [imgData];
        operatorList.addOp(OPS.paintImageMaskXObject, args);
        if (cacheKey) {
          cache.key = cacheKey;
          cache.fn = OPS.paintImageMaskXObject;
          cache.args = args;
        }
        return;
      }

      var softMask = (dict.get('SMask', 'SM') || false);
      var mask = (dict.get('Mask') || false);

      var SMALL_IMAGE_DIMENSIONS = 200;
      // Inlining small images into the queue as RGB data
      if (inline && !softMask && !mask && !(image instanceof JpegStream) &&
          (w + h) < SMALL_IMAGE_DIMENSIONS) {
        var imageObj = new PDFImage(this.xref, resources, image,
                                    inline, null, null);
        // We force the use of RGBA_32BPP images here, because we can't handle
        // any other kind.
        imgData = imageObj.createImageData(/* forceRGBA = */ true);
        operatorList.addOp(OPS.paintInlineImageXObject, [imgData]);
        return;
      }

      // If there is no imageMask, create the PDFImage and a lot
      // of image processing can be done here.
      var uniquePrefix = (this.uniquePrefix || '');
      var objId = 'img_' + uniquePrefix + (++this.idCounters.obj);
      operatorList.addDependency(objId);
      args = [objId, w, h];

      if (!softMask && !mask && image instanceof JpegStream &&
          image.isNativelySupported(this.xref, resources)) {
        // These JPEGs don't need any more processing so we can just send it.
        operatorList.addOp(OPS.paintJpegXObject, args);
        this.handler.send('obj',
          [objId, this.pageIndex, 'JpegStream', image.getIR()]);
        return;
      }

      PDFImage.buildImage(self.handler, self.xref, resources, image, inline).
        then(function(imageObj) {
          var imgData = imageObj.createImageData(/* forceRGBA = */ false);
          self.handler.send('obj', [objId, self.pageIndex, 'Image', imgData],
            null, [imgData.data.buffer]);
        }).then(null, function (reason) {
          warn('Unable to decode image: ' + reason);
          self.handler.send('obj', [objId, self.pageIndex, 'Image', null]);
        });

      operatorList.addOp(OPS.paintImageXObject, args);
      if (cacheKey) {
        cache.key = cacheKey;
        cache.fn = OPS.paintImageXObject;
        cache.args = args;
      }
    },

    handleSMask: function PartialEvaluator_handleSmask(smask, resources,
                                                       operatorList,
                                                       stateManager) {
      var smaskContent = smask.get('G');
      var smaskOptions = {
        subtype: smask.get('S').name,
        backdrop: smask.get('BC')
      };
      this.buildFormXObject(resources, smaskContent, smaskOptions,
                            operatorList, stateManager.state.clone());
    },

    handleTilingType:
        function PartialEvaluator_handleTilingType(fn, args, resources,
                                                   pattern, patternDict,
                                                   operatorList) {
      // Create an IR of the pattern code.
      var tilingOpList = this.getOperatorList(pattern,
        (patternDict.get('Resources') || resources));
      // Add the dependencies to the parent operator list so they are resolved
      // before sub operator list is executed synchronously.
      operatorList.addDependencies(tilingOpList.dependencies);
      operatorList.addOp(fn, getTilingPatternIR({
                               fnArray: tilingOpList.fnArray,
                               argsArray: tilingOpList.argsArray
                             }, patternDict, args));
    },

    handleSetFont:
        function PartialEvaluator_handleSetFont(resources, fontArgs, fontRef,
                                                operatorList, state) {
      // TODO(mack): Not needed?
      var fontName;
      if (fontArgs) {
        fontArgs = fontArgs.slice();
        fontName = fontArgs[0].name;
      }
      var self = this;
      var font = this.loadFont(fontName, fontRef, this.xref, resources,
                               operatorList);
      state.font = font;
      var loadedName = font.loadedName;
      if (!font.sent) {
        var fontData = font.translated.exportData();

        self.handler.send('commonobj', [
          loadedName,
          'Font',
          fontData
        ]);
        font.sent = true;
      }

      return loadedName;
    },

    handleText: function PartialEvaluator_handleText(chars, state) {
      var font = state.font.translated;
      var glyphs = font.charsToGlyphs(chars);
      var isAddToPathSet = !!(state.textRenderingMode &
                              TextRenderingMode.ADD_TO_PATH_FLAG);
      if (font.data && (isAddToPathSet || PDFJS.disableFontFace)) {
        for (var i = 0; i < glyphs.length; i++) {
          if (glyphs[i] === null) {
            continue;
          }
          var fontChar = glyphs[i].fontChar;
          if (!font.renderer.hasBuiltPath(fontChar)) {
            var path = font.renderer.getPathJs(fontChar);
            this.handler.send('commonobj', [
              font.loadedName + '_path_' + fontChar,
              'FontPath',
              path
            ]);
          }
        }
      }

      return glyphs;
    },

    setGState: function PartialEvaluator_setGState(resources, gState,
                                                   operatorList, xref,
                                                   stateManager) {

      var self = this;
      // TODO(mack): This should be rewritten so that this function returns
      // what should be added to the queue during each iteration
      function setGStateForKey(gStateObj, key, value) {
        switch (key) {
          case 'Type':
            break;
          case 'LW':
          case 'LC':
          case 'LJ':
          case 'ML':
          case 'D':
          case 'RI':
          case 'FL':
          case 'CA':
          case 'ca':
            gStateObj.push([key, value]);
            break;
          case 'Font':
            var loadedName = self.handleSetFont(resources, null, value[0],
                                                operatorList,
                                                stateManager.state);
            operatorList.addDependency(loadedName);
            gStateObj.push([key, [loadedName, value[1]]]);
            break;
          case 'BM':
            gStateObj.push([key, value]);
            break;
          case 'SMask':
            if (isName(value) && value.name === 'None') {
              gStateObj.push([key, false]);
              break;
            }
            var dict = xref.fetchIfRef(value);
            if (isDict(dict)) {
              self.handleSMask(dict, resources, operatorList, stateManager);
              gStateObj.push([key, true]);
            } else {
              warn('Unsupported SMask type');
            }

            break;
          // Only generate info log messages for the following since
          // they are unlikey to have a big impact on the rendering.
          case 'OP':
          case 'op':
          case 'OPM':
          case 'BG':
          case 'BG2':
          case 'UCR':
          case 'UCR2':
          case 'TR':
          case 'TR2':
          case 'HT':
          case 'SM':
          case 'SA':
          case 'AIS':
          case 'TK':
            // TODO implement these operators.
            info('graphic state operator ' + key);
            break;
          default:
            info('Unknown graphic state operator ' + key);
            break;
        }
      }

      // This array holds the converted/processed state data.
      var gStateObj = [];
      var gStateMap = gState.map;
      for (var key in gStateMap) {
        var value = gStateMap[key];
        setGStateForKey(gStateObj, key, value);
      }

      operatorList.addOp(OPS.setGState, [gStateObj]);
    },

    loadFont: function PartialEvaluator_loadFont(fontName, font, xref,
                                                 resources,
                                                 parentOperatorList) {

      function errorFont() {
        return {
          translated: new ErrorFont('Font ' + fontName + ' is not available'),
          loadedName: 'g_font_error'
        };
      }

      var fontRef;
      if (font) { // Loading by ref.
        assert(isRef(font));
        fontRef = font;
      } else { // Loading by name.
        var fontRes = resources.get('Font');
        if (fontRes) {
          fontRef = fontRes.getRaw(fontName);
        } else {
          warn('fontRes not available');
          return errorFont();
        }
      }
      if (this.fontCache.has(fontRef)) {
        return this.fontCache.get(fontRef);
      }

      font = xref.fetchIfRef(fontRef);
      if (!isDict(font)) {
        return errorFont();
      }

      var preEvaluatedFont = this.preEvaluateFont(font, xref);
      var descriptor = preEvaluatedFont.descriptor;
      var fontID = fontRef.num + '_' + fontRef.gen;
      if (isDict(descriptor)) {
        if (!descriptor.fontAliases) {
          descriptor.fontAliases = Object.create(null);
        }

        var fontAliases = descriptor.fontAliases;
        var hash = preEvaluatedFont.hash;
        if (fontAliases[hash]) {
          var aliasFontRef = fontAliases[hash].aliasRef;
          if (aliasFontRef && this.fontCache.has(aliasFontRef)) {
            this.fontCache.putAlias(fontRef, aliasFontRef);
            var cachedFont = this.fontCache.get(fontRef);
            return cachedFont;
          }
        }

        if (!fontAliases[hash]) {
          fontAliases[hash] = {
            fontID: Font.getFontID()
          };
        }

        fontAliases[hash].aliasRef = fontRef;
        fontID = fontAliases[hash].fontID;
      }

      // Workaround for bad PDF generators that don't reference fonts
      // properly, i.e. by not using an object identifier.
      // Check if the fontRef is a Dict (as opposed to a standard object),
      // in which case we don't cache the font and instead reference it by
      // fontName in font.loadedName below.
      var fontRefIsDict = isDict(fontRef);
      if (!fontRefIsDict) {
        this.fontCache.put(fontRef, font);
      }

      // Keep track of each font we translated so the caller can
      // load them asynchronously before calling display on a page.
      font.loadedName = 'g_font_' + (fontRefIsDict ?
        fontName.replace(/\W/g, '') : fontID);

      if (!font.translated) {
        var translated;
        try {
          translated = this.translateFont(preEvaluatedFont, xref);
        } catch (e) {
          UnsupportedManager.notify(UNSUPPORTED_FEATURES.font);
          translated = new ErrorFont(e instanceof Error ? e.message : e);
        }
        font.translated = translated;
      }

      if (font.translated.loadCharProcs) {
        var charProcs = font.get('CharProcs').getAll();
        var fontResources = (font.get('Resources') || resources);
        var charProcKeys = Object.keys(charProcs);
        var charProcOperatorList = {};
        for (var i = 0, n = charProcKeys.length; i < n; ++i) {
          var key = charProcKeys[i];
          var glyphStream = charProcs[key];
          var operatorList = this.getOperatorList(glyphStream, fontResources);
          charProcOperatorList[key] = operatorList.getIR();
          if (!parentOperatorList) {
            continue;
          }
          // Add the dependencies to the parent operator list so they are
          // resolved before sub operator list is executed synchronously.
          parentOperatorList.addDependencies(charProcOperatorList.dependencies);
        }
        font.translated.charProcOperatorList = charProcOperatorList;
      }
      font.loaded = true;
      return font;
    },

    buildPath: function PartialEvaluator_buildPath(operatorList, fn, args) {
      var lastIndex = operatorList.length - 1;
      if (lastIndex < 0 ||
          operatorList.fnArray[lastIndex] !== OPS.constructPath) {
        operatorList.addOp(OPS.constructPath, [[fn], args]);
      } else {
        var opArgs = operatorList.argsArray[lastIndex];
        opArgs[0].push(fn);
        Array.prototype.push.apply(opArgs[1], args);
      }
    },

    getOperatorList: function PartialEvaluator_getOperatorList(stream,
                                                               resources,
                                                               operatorList,
                                                               initialState) {

      var self = this;
      var xref = this.xref;
      var imageCache = {};

      operatorList = (operatorList || new OperatorList());

      resources = (resources || Dict.empty);
      var xobjs = (resources.get('XObject') || Dict.empty);
      var patterns = (resources.get('Pattern') || Dict.empty);
      var stateManager = new StateManager(initialState || new EvalState());
      var preprocessor = new EvaluatorPreprocessor(stream, xref, stateManager);

      var operation, i, ii;
      while ((operation = preprocessor.read())) {
        var args = operation.args;
        var fn = operation.fn;
        var shading;

        switch (fn | 0) {
          case OPS.setStrokeColorN:
          case OPS.setFillColorN:
            if (args[args.length - 1].code) {
              break;
            }
            // compile tiling patterns
            var patternName = args[args.length - 1];
            // SCN/scn applies patterns along with normal colors
            var pattern;
            if (isName(patternName) &&
                (pattern = patterns.get(patternName.name))) {
              var dict = (isStream(pattern) ? pattern.dict : pattern);
              var typeNum = dict.get('PatternType');

              if (typeNum == TILING_PATTERN) {
                self.handleTilingType(fn, args, resources, pattern, dict,
                                      operatorList);
                args = [];
                continue;
              } else if (typeNum == SHADING_PATTERN) {
                shading = dict.get('Shading');
                var matrix = dict.get('Matrix');
                pattern = Pattern.parseShading(shading, matrix, xref,
                                               resources);
                args = pattern.getIR();
              } else {
                error('Unkown PatternType ' + typeNum);
              }
            }
            break;
          case OPS.paintXObject:
            if (args[0].code) {
              break;
            }
            // eagerly compile XForm objects
            var name = args[0].name;
            if (imageCache.key === name) {
              operatorList.addOp(imageCache.fn, imageCache.args);
              args = [];
              continue;
            }

            var xobj = xobjs.get(name);
            if (xobj) {
              assert(isStream(xobj), 'XObject should be a stream');

              var type = xobj.dict.get('Subtype');
              assert(isName(type),
                'XObject should have a Name subtype');

              if ('Form' == type.name) {
                stateManager.save();
                self.buildFormXObject(resources, xobj, null, operatorList,
                                      stateManager.state.clone());
                args = [];
                stateManager.restore();
                continue;
              } else if ('Image' == type.name) {
                self.buildPaintImageXObject(resources, xobj, false,
                                            operatorList, name, imageCache);
                args = [];
                continue;
              } else {
                error('Unhandled XObject subtype ' + type.name);
              }
            }
            break;
          case OPS.setFont:
            // eagerly collect all fonts
            var loadedName = self.handleSetFont(resources, args, null,
                                                operatorList,
                                                stateManager.state);
            operatorList.addDependency(loadedName);
            args[0] = loadedName;
            break;
          case OPS.endInlineImage:
            var cacheKey = args[0].cacheKey;
            if (cacheKey && imageCache.key === cacheKey) {
              operatorList.addOp(imageCache.fn, imageCache.args);
              args = [];
              continue;
            }
            self.buildPaintImageXObject(resources, args[0], true,
                                        operatorList, cacheKey, imageCache);
            args = [];
            continue;
          case OPS.showText:
            args[0] = this.handleText(args[0], stateManager.state);
            break;
          case OPS.showSpacedText:
            var arr = args[0];
            var arrLength = arr.length;
            for (i = 0; i < arrLength; ++i) {
              if (isString(arr[i])) {
                arr[i] = this.handleText(arr[i], stateManager.state);
              }
            }
            break;
          case OPS.nextLineShowText:
            args[0] = this.handleText(args[0], stateManager.state);
            break;
          case OPS.nextLineSetSpacingShowText:
            args[2] = this.handleText(args[2], stateManager.state);
            break;
          case OPS.setTextRenderingMode:
            stateManager.state.textRenderingMode = args[0];
            break;
          // Parse the ColorSpace data to a raw format.
          case OPS.setFillColorSpace:
          case OPS.setStrokeColorSpace:
            args = [ColorSpace.parseToIR(args[0], xref, resources)];
            break;
          case OPS.shadingFill:
            var shadingRes = resources.get('Shading');
            if (!shadingRes) {
              error('No shading resource found');
            }

            shading = shadingRes.get(args[0].name);
            if (!shading) {
              error('No shading object found');
            }

            var shadingFill = Pattern.parseShading(shading, null, xref,
                                                   resources);
            var patternIR = shadingFill.getIR();
            args = [patternIR];
            fn = OPS.shadingFill;
            break;
          case OPS.setGState:
            var dictName = args[0];
            var extGState = resources.get('ExtGState');

            if (!isDict(extGState) || !extGState.has(dictName.name)) {
              break;
            }

            var gState = extGState.get(dictName.name);
            self.setGState(resources, gState, operatorList, xref,
                           stateManager);
            args = [];
            continue;
          case OPS.moveTo:
          case OPS.lineTo:
          case OPS.curveTo:
          case OPS.curveTo2:
          case OPS.curveTo3:
          case OPS.closePath:
            self.buildPath(operatorList, fn, args);
            continue;
        }
        operatorList.addOp(fn, args);
      }

      // Some PDFs don't close all restores inside object/form.
      // Closing those for them.
      for (i = 0, ii = preprocessor.savedStatesDepth; i < ii; i++) {
        operatorList.addOp(OPS.restore, []);
      }

      return operatorList;
    },

    getTextContent: function PartialEvaluator_getTextContent(stream, resources,
                                                             stateManager) {

      stateManager = (stateManager || new StateManager(new TextState()));

      var textContent = {
        items: [],
        styles: Object.create(null)
      };
      var bidiTexts = textContent.items;
      var SPACE_FACTOR = 0.35;
      var MULTI_SPACE_FACTOR = 1.5;

      var self = this;
      var xref = this.xref;

      resources = (xref.fetchIfRef(resources) || Dict.empty);

      // The xobj is parsed iff it's needed, e.g. if there is a `DO` cmd.
      var xobjs = null;
      var xobjsCache = {};

      var preprocessor = new EvaluatorPreprocessor(stream, xref, stateManager);

      var operation;
      var textState;

      function newTextChunk() {
        var font = textState.font;
        if (!(font.loadedName in textContent.styles)) {
          textContent.styles[font.loadedName] = {
            fontFamily: font.fallbackName,
            ascent: font.ascent,
            descent: font.descent,
            vertical: font.vertical
          };
        }
        return {
          str: '',
          dir: null,
          width: 0,
          height: 0,
          transform: null,
          fontName: font.loadedName
        };
      }

      function runBidi(textChunk) {
        var bidiResult = PDFJS.bidi(textChunk.str, -1, textState.font.vertical);
        textChunk.str = bidiResult.str;
        textChunk.dir = bidiResult.dir;
        return textChunk;
      }

      function handleSetFont(fontName, fontRef) {
        var font = textState.font = self.loadFont(fontName, fontRef, xref,
                                                  resources, null).translated;
        textState.fontMatrix = font.fontMatrix ? font.fontMatrix :
                                 FONT_IDENTITY_MATRIX;
      }

      function buildTextGeometry(chars, textChunk) {
        var font = textState.font;
        textChunk = textChunk || newTextChunk();
        if (!textChunk.transform) {
          // 9.4.4 Text Space Details
          var tsm = [textState.fontSize * textState.textHScale, 0,
                     0, textState.fontSize,
                     0, textState.textRise];
          var trm = textChunk.transform = Util.transform(textState.ctm,
                                    Util.transform(textState.textMatrix, tsm));
          if (!font.vertical) {
            textChunk.height = Math.sqrt(trm[2] * trm[2] + trm[3] * trm[3]);
          } else {
            textChunk.width = Math.sqrt(trm[0] * trm[0] + trm[1] * trm[1]);
          }
        }
        var width = 0;
        var height = 0;
        var glyphs = font.charsToGlyphs(chars);
        var defaultVMetrics = font.defaultVMetrics;
        for (var i = 0; i < glyphs.length; i++) {
          var glyph = glyphs[i];
          if (!glyph) { // Previous glyph was a space.
            width += textState.wordSpacing * textState.textHScale;
            continue;
          }
          var vMetricX = null;
          var vMetricY = null;
          var glyphWidth = null;
          if (font.vertical) {
            if (glyph.vmetric) {
              glyphWidth = glyph.vmetric[0];
              vMetricX = glyph.vmetric[1];
              vMetricY = glyph.vmetric[2];
            } else {
              glyphWidth = glyph.width;
              vMetricX = glyph.width * 0.5;
              vMetricY = defaultVMetrics[2];
            }
          } else {
            glyphWidth = glyph.width;
          }

          var glyphUnicode = glyph.unicode;
          if (glyphUnicode in NormalizedUnicodes) {
            glyphUnicode = NormalizedUnicodes[glyphUnicode];
          }
          glyphUnicode = reverseIfRtl(glyphUnicode);

          // The following will calculate the x and y of the individual glyphs.
          // if (font.vertical) {
          //   tsm[4] -= vMetricX * Math.abs(textState.fontSize) *
          //             textState.fontMatrix[0];
          //   tsm[5] -= vMetricY * textState.fontSize *
          //             textState.fontMatrix[0];
          // }
          // var trm = Util.transform(textState.textMatrix, tsm);
          // var pt = Util.applyTransform([trm[4], trm[5]], textState.ctm);
          // var x = pt[0];
          // var y = pt[1];

          var tx = 0;
          var ty = 0;
          if (!font.vertical) {
            var w0 = glyphWidth * textState.fontMatrix[0];
            tx = (w0 * textState.fontSize + textState.charSpacing) *
                 textState.textHScale;
            width += tx;
          } else {
            var w1 = glyphWidth * textState.fontMatrix[0];
            ty = w1 * textState.fontSize + textState.charSpacing;
            height += ty;
          }
          textState.translateTextMatrix(tx, ty);

          textChunk.str += glyphUnicode;
        }

        var a = textState.textLineMatrix[0];
        var b = textState.textLineMatrix[1];
        var scaleLineX = Math.sqrt(a * a + b * b);
        a = textState.ctm[0];
        b = textState.ctm[1];
        var scaleCtmX = Math.sqrt(a * a + b * b);
        if (!font.vertical) {
          textChunk.width += width * scaleCtmX * scaleLineX;
        } else {
          textChunk.height += Math.abs(height * scaleCtmX * scaleLineX);
        }
        return textChunk;
      }

      while ((operation = preprocessor.read())) {
        textState = stateManager.state;
        var fn = operation.fn;
        var args = operation.args;
        switch (fn | 0) {
          case OPS.setFont:
            handleSetFont(args[0].name);
            textState.fontSize = args[1];
            break;
          case OPS.setTextRise:
            textState.textRise = args[0];
            break;
          case OPS.setHScale:
            textState.textHScale = args[0] / 100;
            break;
          case OPS.setLeading:
            textState.leading = args[0];
            break;
          case OPS.moveText:
            textState.translateTextLineMatrix(args[0], args[1]);
            textState.textMatrix = textState.textLineMatrix.slice();
            break;
          case OPS.setLeadingMoveText:
            textState.leading = -args[1];
            textState.translateTextLineMatrix(args[0], args[1]);
            textState.textMatrix = textState.textLineMatrix.slice();
            break;
          case OPS.nextLine:
            textState.carriageReturn();
            break;
          case OPS.setTextMatrix:
            textState.setTextMatrix(args[0], args[1], args[2], args[3],
                                    args[4], args[5]);
            textState.setTextLineMatrix(args[0], args[1], args[2], args[3],
                                        args[4], args[5]);
            break;
          case OPS.setCharSpacing:
            textState.charSpacing = args[0];
            break;
          case OPS.setWordSpacing:
            textState.wordSpacing = args[0];
            break;
          case OPS.beginText:
            textState.textMatrix = IDENTITY_MATRIX.slice();
            textState.textLineMatrix = IDENTITY_MATRIX.slice();
            break;
          case OPS.showSpacedText:
            var items = args[0];
            var textChunk = newTextChunk();
            var offset;
            for (var j = 0, jj = items.length; j < jj; j++) {
              if (typeof items[j] === 'string') {
                buildTextGeometry(items[j], textChunk);
              } else {
                var val = items[j] / 1000;
                if (!textState.font.vertical) {
                  offset = -val * textState.fontSize * textState.textHScale *
                           textState.textMatrix[0];
                  textState.translateTextMatrix(offset, 0);
                  textChunk.width += offset;
                } else {
                  offset = -val * textState.fontSize * textState.textMatrix[3];
                  textState.translateTextMatrix(0, offset);
                  textChunk.height += offset;
                }
                if (items[j] < 0 && textState.font.spaceWidth > 0) {
                  var fakeSpaces = -items[j] / textState.font.spaceWidth;
                  if (fakeSpaces > MULTI_SPACE_FACTOR) {
                    fakeSpaces = Math.round(fakeSpaces);
                    while (fakeSpaces--) {
                      textChunk.str += ' ';
                    }
                  } else if (fakeSpaces > SPACE_FACTOR) {
                    textChunk.str += ' ';
                  }
                }
              }
            }
            bidiTexts.push(runBidi(textChunk));
            break;
          case OPS.showText:
            bidiTexts.push(runBidi(buildTextGeometry(args[0])));
            break;
          case OPS.nextLineShowText:
            textState.carriageReturn();
            bidiTexts.push(runBidi(buildTextGeometry(args[0])));
            break;
          case OPS.nextLineSetSpacingShowText:
            textState.wordSpacing = args[0];
            textState.charSpacing = args[1];
            textState.carriageReturn();
            bidiTexts.push(runBidi(buildTextGeometry(args[2])));
            break;
          case OPS.paintXObject:
            if (args[0].code) {
              break;
            }

            if (!xobjs) {
              xobjs = (resources.get('XObject') || Dict.empty);
            }

            var name = args[0].name;
            if (xobjsCache.key === name) {
              if (xobjsCache.texts) {
                Util.concatenateToArray(bidiTexts, xobjsCache.texts.items);
                Util.extendObj(textContent.styles, xobjsCache.texts.styles);
              }
              break;
            }

            var xobj = xobjs.get(name);
            if (!xobj) {
              break;
            }
            assert(isStream(xobj), 'XObject should be a stream');

            var type = xobj.dict.get('Subtype');
            assert(isName(type),
              'XObject should have a Name subtype');

            if ('Form' !== type.name) {
              xobjsCache.key = name;
              xobjsCache.texts = null;
              break;
            }

            stateManager.save();
            var matrix = xobj.dict.get('Matrix');
            if (isArray(matrix) && matrix.length === 6) {
              stateManager.transform(matrix);
            }

            var formTextContent = this.getTextContent(
              xobj,
              xobj.dict.get('Resources') || resources,
              stateManager
            );
            Util.concatenateToArray(bidiTexts, formTextContent.items);
            Util.extendObj(textContent.styles, formTextContent.styles);
            stateManager.restore();

            xobjsCache.key = name;
            xobjsCache.texts = formTextContent;
            break;
          case OPS.setGState:
            var dictName = args[0];
            var extGState = resources.get('ExtGState');

            if (!isDict(extGState) || !extGState.has(dictName.name)) {
              break;
            }

            var gsState = extGState.get(dictName.name);

            for (var i = 0; i < gsState.length; i++) {
              if (gsState[i] === 'Font') {
                handleSetFont(args[0].name);
              }
            }
            break;
        } // switch
      } // while

      return textContent;
    },

    extractDataStructures: function
      partialEvaluatorExtractDataStructures(dict, baseDict,
                                            xref, properties) {
      // 9.10.2
      var toUnicode = (dict.get('ToUnicode') || baseDict.get('ToUnicode'));
      if (toUnicode) {
        properties.toUnicode = this.readToUnicode(toUnicode, xref, properties);
      }
      if (properties.composite) {
        // CIDSystemInfo helps to match CID to glyphs
        var cidSystemInfo = dict.get('CIDSystemInfo');
        if (isDict(cidSystemInfo)) {
          properties.cidSystemInfo = {
            registry: cidSystemInfo.get('Registry'),
            ordering: cidSystemInfo.get('Ordering'),
            supplement: cidSystemInfo.get('Supplement')
          };
        }

        var cidToGidMap = dict.get('CIDToGIDMap');
        if (isStream(cidToGidMap)) {
          properties.cidToGidMap = this.readCidToGidMap(cidToGidMap);
        }
      }

      // Based on 9.6.6 of the spec the encoding can come from multiple places
      // and depends on the font type. The base encoding and differences are
      // read here, but the encoding that is actually used is chosen during
      // glyph mapping in the font.
      // TODO: Loading the built in encoding in the font would allow the
      // differences to be merged in here not require us to hold on to it.
      var differences = [];
      var baseEncodingName = null;
      var encoding;
      if (dict.has('Encoding')) {
        encoding = dict.get('Encoding');
        if (isDict(encoding)) {
          baseEncodingName = encoding.get('BaseEncoding');
          baseEncodingName = (isName(baseEncodingName) ?
                              baseEncodingName.name : null);
          // Load the differences between the base and original
          if (encoding.has('Differences')) {
            var diffEncoding = encoding.get('Differences');
            var index = 0;
            for (var j = 0, jj = diffEncoding.length; j < jj; j++) {
              var data = diffEncoding[j];
              if (isNum(data)) {
                index = data;
              } else {
                differences[index++] = data.name;
              }
            }
          }
        } else if (isName(encoding)) {
          baseEncodingName = encoding.name;
        } else {
          error('Encoding is not a Name nor a Dict');
        }
        // According to table 114 if the encoding is a named encoding it must be
        // one of these predefined encodings.
        if ((baseEncodingName !== 'MacRomanEncoding' &&
             baseEncodingName !== 'MacExpertEncoding' &&
             baseEncodingName !== 'WinAnsiEncoding')) {
          baseEncodingName = null;
        }
      }

      if (baseEncodingName) {
        properties.defaultEncoding = Encodings[baseEncodingName].slice();
      } else {
        encoding = (properties.type === 'TrueType' ?
                    Encodings.WinAnsiEncoding : Encodings.StandardEncoding);
        // The Symbolic attribute can be misused for regular fonts
        // Heuristic: we have to check if the font is a standard one also
        if (!!(properties.flags & FontFlags.Symbolic)) {
          encoding = (!properties.file && /Symbol/i.test(properties.name) ?
                      Encodings.SymbolSetEncoding :
                      Encodings.MacRomanEncoding);
        }
        properties.defaultEncoding = encoding;
      }

      properties.differences = differences;
      properties.baseEncodingName = baseEncodingName;
      properties.dict = dict;
    },

    readToUnicode: function PartialEvaluator_readToUnicode(toUnicode) {
      var cmapObj = toUnicode;
      if (isName(cmapObj)) {
        return CMapFactory.create(cmapObj).map;
      } else if (isStream(cmapObj)) {
        var cmap = CMapFactory.create(cmapObj).map;
        // Convert UTF-16BE
        // NOTE: cmap can be a sparse array, so use forEach instead of for(;;)
        // to iterate over all keys.
        cmap.forEach(function(token, i) {
          var str = [];
          for (var k = 0; k < token.length; k += 2) {
            var w1 = (token.charCodeAt(k) << 8) | token.charCodeAt(k + 1);
            if ((w1 & 0xF800) !== 0xD800) { // w1 < 0xD800 || w1 > 0xDFFF
              str.push(w1);
              continue;
            }
            k += 2;
            var w2 = (token.charCodeAt(k) << 8) | token.charCodeAt(k + 1);
            str.push(((w1 & 0x3ff) << 10) + (w2 & 0x3ff) + 0x10000);
          }
          cmap[i] = String.fromCharCode.apply(String, str);
        });
        return cmap;
      }
      return null;
    },

    readCidToGidMap: function PartialEvaluator_readCidToGidMap(cidToGidStream) {
      // Extract the encoding from the CIDToGIDMap
      var glyphsData = cidToGidStream.getBytes();

      // Set encoding 0 to later verify the font has an encoding
      var result = [];
      for (var j = 0, jj = glyphsData.length; j < jj; j++) {
        var glyphID = (glyphsData[j++] << 8) | glyphsData[j];
        if (glyphID === 0) {
          continue;
        }
        var code = j >> 1;
        result[code] = glyphID;
      }
      return result;
    },

    extractWidths: function PartialEvaluator_extractWidths(dict, xref,
                                                           descriptor,
                                                           properties) {
      var glyphsWidths = [];
      var defaultWidth = 0;
      var glyphsVMetrics = [];
      var defaultVMetrics;
      var i, ii, j, jj, start, code, widths;
      if (properties.composite) {
        defaultWidth = dict.get('DW') || 1000;

        widths = dict.get('W');
        if (widths) {
          for (i = 0, ii = widths.length; i < ii; i++) {
            start = widths[i++];
            code = xref.fetchIfRef(widths[i]);
            if (isArray(code)) {
              for (j = 0, jj = code.length; j < jj; j++) {
                glyphsWidths[start++] = code[j];
              }
            } else {
              var width = widths[++i];
              for (j = start; j <= code; j++) {
                glyphsWidths[j] = width;
              }
            }
          }
        }

        if (properties.vertical) {
          var vmetrics = (dict.get('DW2') || [880, -1000]);
          defaultVMetrics = [vmetrics[1], defaultWidth * 0.5, vmetrics[0]];
          vmetrics = dict.get('W2');
          if (vmetrics) {
            for (i = 0, ii = vmetrics.length; i < ii; i++) {
              start = vmetrics[i++];
              code = xref.fetchIfRef(vmetrics[i]);
              if (isArray(code)) {
                for (j = 0, jj = code.length; j < jj; j++) {
                  glyphsVMetrics[start++] = [code[j++], code[j++], code[j]];
                }
              } else {
                var vmetric = [vmetrics[++i], vmetrics[++i], vmetrics[++i]];
                for (j = start; j <= code; j++) {
                  glyphsVMetrics[j] = vmetric;
                }
              }
            }
          }
        }
      } else {
        var firstChar = properties.firstChar;
        widths = dict.get('Widths');
        if (widths) {
          j = firstChar;
          for (i = 0, ii = widths.length; i < ii; i++) {
            glyphsWidths[j++] = widths[i];
          }
          defaultWidth = (parseFloat(descriptor.get('MissingWidth')) || 0);
        } else {
          // Trying get the BaseFont metrics (see comment above).
          var baseFontName = dict.get('BaseFont');
          if (isName(baseFontName)) {
            var metrics = this.getBaseFontMetrics(baseFontName.name);

            glyphsWidths = this.buildCharCodeToWidth(metrics.widths,
                                                     properties);
            defaultWidth = metrics.defaultWidth;
          }
        }
      }

      // Heuristic: detection of monospace font by checking all non-zero widths
      var isMonospace = true;
      var firstWidth = defaultWidth;
      for (var glyph in glyphsWidths) {
        var glyphWidth = glyphsWidths[glyph];
        if (!glyphWidth) {
          continue;
        }
        if (!firstWidth) {
          firstWidth = glyphWidth;
          continue;
        }
        if (firstWidth != glyphWidth) {
          isMonospace = false;
          break;
        }
      }
      if (isMonospace) {
        properties.flags |= FontFlags.FixedPitch;
      }

      properties.defaultWidth = defaultWidth;
      properties.widths = glyphsWidths;
      properties.defaultVMetrics = defaultVMetrics;
      properties.vmetrics = glyphsVMetrics;
    },

    isSerifFont: function PartialEvaluator_isSerifFont(baseFontName) {
      // Simulating descriptor flags attribute
      var fontNameWoStyle = baseFontName.split('-')[0];
      return (fontNameWoStyle in serifFonts) ||
              (fontNameWoStyle.search(/serif/gi) !== -1);
    },

    getBaseFontMetrics: function PartialEvaluator_getBaseFontMetrics(name) {
      var defaultWidth = 0;
      var widths = [];
      var monospace = false;
      var lookupName = (stdFontMap[name] || name);

      if (!(lookupName in Metrics)) {
        // Use default fonts for looking up font metrics if the passed
        // font is not a base font
        if (this.isSerifFont(name)) {
          lookupName = 'Times-Roman';
        } else {
          lookupName = 'Helvetica';
        }
      }
      var glyphWidths = Metrics[lookupName];

      if (isNum(glyphWidths)) {
        defaultWidth = glyphWidths;
        monospace = true;
      } else {
        widths = glyphWidths;
      }

      return {
        defaultWidth: defaultWidth,
        monospace: monospace,
        widths: widths
      };
    },

    buildCharCodeToWidth:
        function PartialEvaluator_bulildCharCodeToWidth(widthsByGlyphName,
                                                        properties) {
      var widths = Object.create(null);
      var differences = properties.differences;
      var encoding = properties.defaultEncoding;
      for (var charCode = 0; charCode < 256; charCode++) {
        if (charCode in differences &&
            widthsByGlyphName[differences[charCode]]) {
          widths[charCode] = widthsByGlyphName[differences[charCode]];
          continue;
        }
        if (charCode in encoding && widthsByGlyphName[encoding[charCode]]) {
          widths[charCode] = widthsByGlyphName[encoding[charCode]];
          continue;
        }
      }
      return widths;
    },

    preEvaluateFont: function PartialEvaluator_preEvaluateFont(dict, xref) {
      var baseDict = dict;
      var type = dict.get('Subtype');
      assert(isName(type), 'invalid font Subtype');

      var composite = false;
      var uint8array;
      if (type.name == 'Type0') {
        // If font is a composite
        //  - get the descendant font
        //  - set the type according to the descendant font
        //  - get the FontDescriptor from the descendant font
        var df = dict.get('DescendantFonts');
        if (!df) {
          error('Descendant fonts are not specified');
        }
        dict = (isArray(df) ? xref.fetchIfRef(df[0]) : df);

        type = dict.get('Subtype');
        assert(isName(type), 'invalid font Subtype');
        composite = true;
      }

      var descriptor = dict.get('FontDescriptor');
      if (descriptor) {
        var hash = new MurmurHash3_64();
        var encoding = baseDict.getRaw('Encoding');
        if (isName(encoding)) {
          hash.update(encoding.name);
        } else if (isRef(encoding)) {
          hash.update(encoding.num + '_' + encoding.gen);
        }

        var toUnicode = dict.get('ToUnicode') || baseDict.get('ToUnicode');
        if (isStream(toUnicode)) {
          var stream = toUnicode.str || toUnicode;
          uint8array = stream.buffer ?
            new Uint8Array(stream.buffer.buffer, 0, stream.bufferLength) :
            new Uint8Array(stream.bytes.buffer,
                           stream.start, stream.end - stream.start);
          hash.update(uint8array);

        } else if (isName(toUnicode)) {
          hash.update(toUnicode.name);
        }

        var widths = dict.get('Widths') || baseDict.get('Widths');
        if (widths) {
          uint8array = new Uint8Array(new Uint32Array(widths).buffer);
          hash.update(uint8array);
        }
      }

      return {
        descriptor: descriptor,
        dict: dict,
        baseDict: baseDict,
        composite: composite,
        hash: hash ? hash.hexdigest() : ''
      };
    },

    translateFont: function PartialEvaluator_translateFont(preEvaluatedFont,
                                                           xref) {
      var baseDict = preEvaluatedFont.baseDict;
      var dict = preEvaluatedFont.dict;
      var composite = preEvaluatedFont.composite;
      var descriptor = preEvaluatedFont.descriptor;
      var type = dict.get('Subtype');
      var maxCharIndex = (composite ? 0xFFFF : 0xFF);
      var properties;

      if (!descriptor) {
        if (type.name == 'Type3') {
          // FontDescriptor is only required for Type3 fonts when the document
          // is a tagged pdf. Create a barbebones one to get by.
          descriptor = new Dict(null);
          descriptor.set('FontName', Name.get(type.name));
        } else {
          // Before PDF 1.5 if the font was one of the base 14 fonts, having a
          // FontDescriptor was not required.
          // This case is here for compatibility.
          var baseFontName = dict.get('BaseFont');
          if (!isName(baseFontName)) {
            error('Base font is not specified');
          }

          // Using base font name as a font name.
          baseFontName = baseFontName.name.replace(/[,_]/g, '-');
          var metrics = this.getBaseFontMetrics(baseFontName);

          // Simulating descriptor flags attribute
          var fontNameWoStyle = baseFontName.split('-')[0];
          var flags =
            (this.isSerifFont(fontNameWoStyle) ? FontFlags.Serif : 0) |
            (metrics.monospace ? FontFlags.FixedPitch : 0) |
            (symbolsFonts[fontNameWoStyle] ? FontFlags.Symbolic :
                                             FontFlags.Nonsymbolic);

          properties = {
            type: type.name,
            name: baseFontName,
            widths: metrics.widths,
            defaultWidth: metrics.defaultWidth,
            flags: flags,
            firstChar: 0,
            lastChar: maxCharIndex
          };
          this.extractDataStructures(dict, dict, xref, properties);
          properties.widths = this.buildCharCodeToWidth(metrics.widths,
                                                        properties);
          return new Font(baseFontName, null, properties);
        }
      }

      // According to the spec if 'FontDescriptor' is declared, 'FirstChar',
      // 'LastChar' and 'Widths' should exist too, but some PDF encoders seem
      // to ignore this rule when a variant of a standart font is used.
      // TODO Fill the width array depending on which of the base font this is
      // a variant.
      var firstChar = (dict.get('FirstChar') || 0);
      var lastChar = (dict.get('LastChar') || maxCharIndex);

      var fontName = descriptor.get('FontName');
      var baseFont = dict.get('BaseFont');
      // Some bad PDFs have a string as the font name.
      if (isString(fontName)) {
        fontName = Name.get(fontName);
      }
      if (isString(baseFont)) {
        baseFont = Name.get(baseFont);
      }

      if (type.name !== 'Type3') {
        var fontNameStr = fontName && fontName.name;
        var baseFontStr = baseFont && baseFont.name;
        if (fontNameStr !== baseFontStr) {
          info('The FontDescriptor\'s FontName is "' + fontNameStr +
               '" but should be the same as the Font\'s BaseFont "' +
               baseFontStr + '"');
          // Workaround for cases where e.g. fontNameStr = 'Arial' and
          // baseFontStr = 'Arial,Bold' (needed when no font file is embedded).
          if (fontNameStr && baseFontStr &&
              baseFontStr.search(fontNameStr) === 0) {
            fontName = baseFont;
          }
        }
      }
      fontName = (fontName || baseFont);

      assert(isName(fontName), 'invalid font name');

      var fontFile = descriptor.get('FontFile', 'FontFile2', 'FontFile3');
      if (fontFile) {
        if (fontFile.dict) {
          var subtype = fontFile.dict.get('Subtype');
          if (subtype) {
            subtype = subtype.name;
          }
          var length1 = fontFile.dict.get('Length1');
          var length2 = fontFile.dict.get('Length2');
        }
      }

      properties = {
        type: type.name,
        name: fontName.name,
        subtype: subtype,
        file: fontFile,
        length1: length1,
        length2: length2,
        loadedName: baseDict.loadedName,
        composite: composite,
        wideChars: composite,
        fixedPitch: false,
        fontMatrix: (dict.get('FontMatrix') || FONT_IDENTITY_MATRIX),
        firstChar: firstChar || 0,
        lastChar: (lastChar || maxCharIndex),
        bbox: descriptor.get('FontBBox'),
        ascent: descriptor.get('Ascent'),
        descent: descriptor.get('Descent'),
        xHeight: descriptor.get('XHeight'),
        capHeight: descriptor.get('CapHeight'),
        flags: descriptor.get('Flags'),
        italicAngle: descriptor.get('ItalicAngle'),
        coded: false
      };

      if (composite) {
        var cidEncoding = baseDict.get('Encoding');
        if (isName(cidEncoding)) {
          properties.cidEncoding = cidEncoding.name;
        }
        properties.cMap = CMapFactory.create(cidEncoding,
          { url: PDFJS.cMapUrl, packed: PDFJS.cMapPacked }, null);
        properties.vertical = properties.cMap.vertical;
      }
      this.extractDataStructures(dict, baseDict, xref, properties);
      this.extractWidths(dict, xref, descriptor, properties);

      if (type.name === 'Type3') {
        properties.coded = true;
      }

      return new Font(fontName.name, fontFile, properties);
    }
  };

  return PartialEvaluator;
})();

var OperatorList = (function OperatorListClosure() {
  var CHUNK_SIZE = 1000;
  var CHUNK_SIZE_ABOUT = CHUNK_SIZE - 5; // close to chunk size

  function getTransfers(queue) {
    var transfers = [];
    var fnArray = queue.fnArray, argsArray = queue.argsArray;
    for (var i = 0, ii = queue.length; i < ii; i++) {
      switch (fnArray[i]) {
        case OPS.paintInlineImageXObject:
        case OPS.paintInlineImageXObjectGroup:
        case OPS.paintImageMaskXObject:
          var arg = argsArray[i][0]; // first param in imgData
          if (!arg.cached) {
            transfers.push(arg.data.buffer);
          }
          break;
      }
    }
    return transfers;
  }

  function OperatorList(intent, messageHandler, pageIndex) {
    this.messageHandler = messageHandler;
    this.fnArray = [];
    this.argsArray = [];
    this.dependencies = {};
    this.pageIndex = pageIndex;
    this.intent = intent;
  }

  OperatorList.prototype = {
    get length() {
      return this.argsArray.length;
    },

    addOp: function(fn, args) {
      this.fnArray.push(fn);
      this.argsArray.push(args);
      if (this.messageHandler) {
        if (this.fnArray.length >= CHUNK_SIZE) {
          this.flush();
        } else if (this.fnArray.length >= CHUNK_SIZE_ABOUT &&
                   (fn === OPS.restore || fn === OPS.endText)) {
          // heuristic to flush on boundary of restore or endText
          this.flush();
        }
      }
    },

    addDependency: function(dependency) {
      if (dependency in this.dependencies) {
        return;
      }
      this.dependencies[dependency] = true;
      this.addOp(OPS.dependency, [dependency]);
    },

    addDependencies: function(dependencies) {
      for (var key in dependencies) {
        this.addDependency(key);
      }
    },

    addOpList: function(opList) {
      Util.extendObj(this.dependencies, opList.dependencies);
      for (var i = 0, ii = opList.length; i < ii; i++) {
        this.addOp(opList.fnArray[i], opList.argsArray[i]);
      }
    },

    getIR: function() {
      return {
        fnArray: this.fnArray,
        argsArray: this.argsArray,
        length: this.length
      };
    },

    flush: function(lastChunk) {
      new QueueOptimizer().optimize(this);
      var transfers = getTransfers(this);
      this.messageHandler.send('RenderPageChunk', {
        operatorList: {
          fnArray: this.fnArray,
          argsArray: this.argsArray,
          lastChunk: lastChunk,
          length: this.length
        },
        pageIndex: this.pageIndex,
        intent: this.intent
      }, null, transfers);
      this.dependencies = {};
      this.fnArray.length = 0;
      this.argsArray.length = 0;
    }
  };

  return OperatorList;
})();

var StateManager = (function StateManagerClosure() {
  function StateManager(initialState) {
    this.state = initialState;
    this.stateStack = [];
  }
  StateManager.prototype = {
    save: function () {
      var old = this.state;
      this.stateStack.push(this.state);
      this.state = old.clone();
    },
    restore: function () {
      var prev = this.stateStack.pop();
      if (prev) {
        this.state = prev;
      }
    },
    transform: function (args) {
      this.state.ctm = Util.transform(this.state.ctm, args);
    }
  };
  return StateManager;
})();

var TextState = (function TextStateClosure() {
  function TextState() {
    this.ctm = new Float32Array(IDENTITY_MATRIX);
    this.fontSize = 0;
    this.font = null;
    this.fontMatrix = FONT_IDENTITY_MATRIX;
    this.textMatrix = IDENTITY_MATRIX.slice();
    this.textLineMatrix = IDENTITY_MATRIX.slice();
    this.charSpacing = 0;
    this.wordSpacing = 0;
    this.leading = 0;
    this.textHScale = 1;
    this.textRise = 0;
  }

  TextState.prototype = {
    setTextMatrix: function TextState_setTextMatrix(a, b, c, d, e, f) {
      var m = this.textMatrix;
      m[0] = a; m[1] = b; m[2] = c; m[3] = d; m[4] = e; m[5] = f;
    },
    setTextLineMatrix: function TextState_setTextMatrix(a, b, c, d, e, f) {
      var m = this.textLineMatrix;
      m[0] = a; m[1] = b; m[2] = c; m[3] = d; m[4] = e; m[5] = f;
    },
    translateTextMatrix: function TextState_translateTextMatrix(x, y) {
      var m = this.textMatrix;
      m[4] = m[0] * x + m[2] * y + m[4];
      m[5] = m[1] * x + m[3] * y + m[5];
    },
    translateTextLineMatrix: function TextState_translateTextMatrix(x, y) {
      var m = this.textLineMatrix;
      m[4] = m[0] * x + m[2] * y + m[4];
      m[5] = m[1] * x + m[3] * y + m[5];
    },
    calcRenderMatrix: function TextState_calcRendeMatrix(ctm) {
      // 9.4.4 Text Space Details
      var tsm = [this.fontSize * this.textHScale, 0,
                0, this.fontSize,
                0, this.textRise];
      return Util.transform(ctm, Util.transform(this.textMatrix, tsm));
    },
    carriageReturn: function TextState_carriageReturn() {
      this.translateTextLineMatrix(0, -this.leading);
      this.textMatrix = this.textLineMatrix.slice();
    },
    clone: function TextState_clone() {
      var clone = Object.create(this);
      clone.textMatrix = this.textMatrix.slice();
      clone.textLineMatrix = this.textLineMatrix.slice();
      clone.fontMatrix = this.fontMatrix.slice();
      return clone;
    }
  };
  return TextState;
})();

var EvalState = (function EvalStateClosure() {
  function EvalState() {
    this.ctm = new Float32Array(IDENTITY_MATRIX);
    this.font = null;
    this.textRenderingMode = TextRenderingMode.FILL;
  }
  EvalState.prototype = {
    clone: function CanvasExtraState_clone() {
      return Object.create(this);
    },
  };
  return EvalState;
})();

var EvaluatorPreprocessor = (function EvaluatorPreprocessorClosure() {
  // Specifies properties for each command
  //
  // If variableArgs === true: [0, `numArgs`] expected
  // If variableArgs === false: exactly `numArgs` expected
  var OP_MAP = {
    // Graphic state
    w: { id: OPS.setLineWidth, numArgs: 1, variableArgs: false },
    J: { id: OPS.setLineCap, numArgs: 1, variableArgs: false },
    j: { id: OPS.setLineJoin, numArgs: 1, variableArgs: false },
    M: { id: OPS.setMiterLimit, numArgs: 1, variableArgs: false },
    d: { id: OPS.setDash, numArgs: 2, variableArgs: false },
    ri: { id: OPS.setRenderingIntent, numArgs: 1, variableArgs: false },
    i: { id: OPS.setFlatness, numArgs: 1, variableArgs: false },
    gs: { id: OPS.setGState, numArgs: 1, variableArgs: false },
    q: { id: OPS.save, numArgs: 0, variableArgs: false },
    Q: { id: OPS.restore, numArgs: 0, variableArgs: false },
    cm: { id: OPS.transform, numArgs: 6, variableArgs: false },

    // Path
    m: { id: OPS.moveTo, numArgs: 2, variableArgs: false },
    l: { id: OPS.lineTo, numArgs: 2, variableArgs: false },
    c: { id: OPS.curveTo, numArgs: 6, variableArgs: false },
    v: { id: OPS.curveTo2, numArgs: 4, variableArgs: false },
    y: { id: OPS.curveTo3, numArgs: 4, variableArgs: false },
    h: { id: OPS.closePath, numArgs: 0, variableArgs: false },
    re: { id: OPS.rectangle, numArgs: 4, variableArgs: false },
    S: { id: OPS.stroke, numArgs: 0, variableArgs: false },
    s: { id: OPS.closeStroke, numArgs: 0, variableArgs: false },
    f: { id: OPS.fill, numArgs: 0, variableArgs: false },
    F: { id: OPS.fill, numArgs: 0, variableArgs: false },
    'f*': { id: OPS.eoFill, numArgs: 0, variableArgs: false },
    B: { id: OPS.fillStroke, numArgs: 0, variableArgs: false },
    'B*': { id: OPS.eoFillStroke, numArgs: 0, variableArgs: false },
    b: { id: OPS.closeFillStroke, numArgs: 0, variableArgs: false },
    'b*': { id: OPS.closeEOFillStroke, numArgs: 0, variableArgs: false },
    n: { id: OPS.endPath, numArgs: 0, variableArgs: false },

    // Clipping
    W: { id: OPS.clip, numArgs: 0, variableArgs: false },
    'W*': { id: OPS.eoClip, numArgs: 0, variableArgs: false },

    // Text
    BT: { id: OPS.beginText, numArgs: 0, variableArgs: false },
    ET: { id: OPS.endText, numArgs: 0, variableArgs: false },
    Tc: { id: OPS.setCharSpacing, numArgs: 1, variableArgs: false },
    Tw: { id: OPS.setWordSpacing, numArgs: 1, variableArgs: false },
    Tz: { id: OPS.setHScale, numArgs: 1, variableArgs: false },
    TL: { id: OPS.setLeading, numArgs: 1, variableArgs: false },
    Tf: { id: OPS.setFont, numArgs: 2, variableArgs: false },
    Tr: { id: OPS.setTextRenderingMode, numArgs: 1, variableArgs: false },
    Ts: { id: OPS.setTextRise, numArgs: 1, variableArgs: false },
    Td: { id: OPS.moveText, numArgs: 2, variableArgs: false },
    TD: { id: OPS.setLeadingMoveText, numArgs: 2, variableArgs: false },
    Tm: { id: OPS.setTextMatrix, numArgs: 6, variableArgs: false },
    'T*': { id: OPS.nextLine, numArgs: 0, variableArgs: false },
    Tj: { id: OPS.showText, numArgs: 1, variableArgs: false },
    TJ: { id: OPS.showSpacedText, numArgs: 1, variableArgs: false },
    '\'': { id: OPS.nextLineShowText, numArgs: 1, variableArgs: false },
    '"': { id: OPS.nextLineSetSpacingShowText, numArgs: 3,
           variableArgs: false },

    // Type3 fonts
    d0: { id: OPS.setCharWidth, numArgs: 2, variableArgs: false },
    d1: { id: OPS.setCharWidthAndBounds, numArgs: 6, variableArgs: false },

    // Color
    CS: { id: OPS.setStrokeColorSpace, numArgs: 1, variableArgs: false },
    cs: { id: OPS.setFillColorSpace, numArgs: 1, variableArgs: false },
    SC: { id: OPS.setStrokeColor, numArgs: 4, variableArgs: true },
    SCN: { id: OPS.setStrokeColorN, numArgs: 33, variableArgs: true },
    sc: { id: OPS.setFillColor, numArgs: 4, variableArgs: true },
    scn: { id: OPS.setFillColorN, numArgs: 33, variableArgs: true },
    G: { id: OPS.setStrokeGray, numArgs: 1, variableArgs: false },
    g: { id: OPS.setFillGray, numArgs: 1, variableArgs: false },
    RG: { id: OPS.setStrokeRGBColor, numArgs: 3, variableArgs: false },
    rg: { id: OPS.setFillRGBColor, numArgs: 3, variableArgs: false },
    K: { id: OPS.setStrokeCMYKColor, numArgs: 4, variableArgs: false },
    k: { id: OPS.setFillCMYKColor, numArgs: 4, variableArgs: false },

    // Shading
    sh: { id: OPS.shadingFill, numArgs: 1, variableArgs: false },

    // Images
    BI: { id: OPS.beginInlineImage, numArgs: 0, variableArgs: false },
    ID: { id: OPS.beginImageData, numArgs: 0, variableArgs: false },
    EI: { id: OPS.endInlineImage, numArgs: 1, variableArgs: false },

    // XObjects
    Do: { id: OPS.paintXObject, numArgs: 1, variableArgs: false },
    MP: { id: OPS.markPoint, numArgs: 1, variableArgs: false },
    DP: { id: OPS.markPointProps, numArgs: 2, variableArgs: false },
    BMC: { id: OPS.beginMarkedContent, numArgs: 1, variableArgs: false },
    BDC: { id: OPS.beginMarkedContentProps, numArgs: 2,
           variableArgs: false },
    EMC: { id: OPS.endMarkedContent, numArgs: 0, variableArgs: false },

    // Compatibility
    BX: { id: OPS.beginCompat, numArgs: 0, variableArgs: false },
    EX: { id: OPS.endCompat, numArgs: 0, variableArgs: false },

    // (reserved partial commands for the lexer)
    BM: null,
    BD: null,
    'true': null,
    fa: null,
    fal: null,
    fals: null,
    'false': null,
    nu: null,
    nul: null,
    'null': null
  };

  function EvaluatorPreprocessor(stream, xref, stateManager) {
    // TODO(mduan): pass array of knownCommands rather than OP_MAP
    // dictionary
    this.parser = new Parser(new Lexer(stream, OP_MAP), false, xref);
    this.stateManager = stateManager;
  }

  EvaluatorPreprocessor.prototype = {
    get savedStatesDepth() {
      return this.stateManager.stateStack.length;
    },

    read: function EvaluatorPreprocessor_read() {
      var args = [];
      while (true) {
        var obj = this.parser.getObj();
        if (isEOF(obj)) {
          return null; // no more commands
        }
        if (!isCmd(obj)) {
          // argument
          if (obj !== null && obj !== undefined) {
            args.push((obj instanceof Dict ? obj.getAll() : obj));
            assert(args.length <= 33, 'Too many arguments');
          }
          continue;
        }

        var cmd = obj.cmd;
        // Check that the command is valid
        var opSpec = OP_MAP[cmd];
        if (!opSpec) {
          warn('Unknown command "' + cmd + '"');
          continue;
        }

        var fn = opSpec.id;

        // Validate the number of arguments for the command
        if (opSpec.variableArgs) {
          if (args.length > opSpec.numArgs) {
            info('Command ' + fn + ': expected [0,' + opSpec.numArgs +
                 '] args, but received ' + args.length + ' args');
          }
        } else {
          if (args.length < opSpec.numArgs) {
            // If we receive too few args, it's not possible to possible
            // to execute the command, so skip the command
            info('Command ' + fn + ': because expected ' +
                 opSpec.numArgs + ' args, but received ' + args.length +
                 ' args; skipping');
            args = [];
            continue;
          } else if (args.length > opSpec.numArgs) {
            info('Command ' + fn + ': expected ' + opSpec.numArgs +
                 ' args, but received ' + args.length + ' args');
          }
        }

        // TODO figure out how to type-check vararg functions
        this.preprocessCommand(fn, args);

        return { fn: fn, args: args };
      }
    },

    preprocessCommand:
        function EvaluatorPreprocessor_preprocessCommand(fn, args) {
      switch (fn | 0) {
        case OPS.save:
          this.stateManager.save();
          break;
        case OPS.restore:
          this.stateManager.restore();
          break;
        case OPS.transform:
          this.stateManager.transform(args);
          break;
      }
    }
  };
  return EvaluatorPreprocessor;
})();

var QueueOptimizer = (function QueueOptimizerClosure() {
  function addState(parentState, pattern, fn) {
    var state = parentState;
    for (var i = 0, ii = pattern.length - 1; i < ii; i++) {
      var item = pattern[i];
      state = (state[item] || (state[item] = []));
    }
    state[pattern[pattern.length - 1]] = fn;
  }

  function handlePaintSolidColorImageMask(index, count, fnArray, argsArray) {
    // Handles special case of mainly LaTeX documents which
    // use image masks to draw lines with the current fill style.
    // 'count' groups of (save, transform, paintImageMaskXObject, restore)+
    // have been found at index.
    for (var i = 0; i < count; i++) {
      var arg = argsArray[index + 4 * i + 2];
      var imageMask = arg.length == 1 && arg[0];
      if (imageMask && imageMask.width == 1 && imageMask.height == 1 &&
          (!imageMask.data.length || (imageMask.data.length == 1 &&
                                      imageMask.data[0] === 0))) {
        fnArray[index + 4 * i + 2] = OPS.paintSolidColorImageMask;
        continue;
      }
      break;
    }
    return count - i;
  }

  var InitialState = [];

  addState(InitialState,
    [OPS.save, OPS.transform, OPS.paintInlineImageXObject, OPS.restore],
    function foundInlineImageGroup(context) {
      // grouping paintInlineImageXObject's into paintInlineImageXObjectGroup
      // searching for (save, transform, paintInlineImageXObject, restore)+
      var MIN_IMAGES_IN_INLINE_IMAGES_BLOCK = 10;
      var MAX_IMAGES_IN_INLINE_IMAGES_BLOCK = 200;
      var MAX_WIDTH = 1000;
      var IMAGE_PADDING = 1;

      var fnArray = context.fnArray, argsArray = context.argsArray;
      var j = context.currentOperation - 3, i = j + 4;
      var ii = fnArray.length;

      for (; i < ii && fnArray[i - 4] === fnArray[i]; i++) {}
      var count = Math.min((i - j) >> 2, MAX_IMAGES_IN_INLINE_IMAGES_BLOCK);
      if (count < MIN_IMAGES_IN_INLINE_IMAGES_BLOCK) {
        context.currentOperation = i - 1;
        return;
      }
      // assuming that heights of those image is too small (~1 pixel)
      // packing as much as possible by lines
      var maxX = 0;
      var map = [], maxLineHeight = 0;
      var currentX = IMAGE_PADDING, currentY = IMAGE_PADDING;
      var q;
      for (q = 0; q < count; q++) {
        var transform = argsArray[j + (q << 2) + 1];
        var img = argsArray[j + (q << 2) + 2][0];
        if (currentX + img.width > MAX_WIDTH) {
          // starting new line
          maxX = Math.max(maxX, currentX);
          currentY += maxLineHeight + 2 * IMAGE_PADDING;
          currentX = 0;
          maxLineHeight = 0;
        }
        map.push({
          transform: transform,
          x: currentX, y: currentY,
          w: img.width, h: img.height
        });
        currentX += img.width + 2 * IMAGE_PADDING;
        maxLineHeight = Math.max(maxLineHeight, img.height);
      }
      var imgWidth = Math.max(maxX, currentX) + IMAGE_PADDING;
      var imgHeight = currentY + maxLineHeight + IMAGE_PADDING;
      var imgData = new Uint8Array(imgWidth * imgHeight * 4);
      var imgRowSize = imgWidth << 2;
      for (q = 0; q < count; q++) {
        var data = argsArray[j + (q << 2) + 2][0].data;
        // copy image by lines and extends pixels into padding
        var rowSize = map[q].w << 2;
        var dataOffset = 0;
        var offset = (map[q].x + map[q].y * imgWidth) << 2;
        imgData.set(data.subarray(0, rowSize), offset - imgRowSize);
        for (var k = 0, kk = map[q].h; k < kk; k++) {
          imgData.set(data.subarray(dataOffset, dataOffset + rowSize), offset);
          dataOffset += rowSize;
          offset += imgRowSize;
        }
        imgData.set(data.subarray(dataOffset - rowSize, dataOffset), offset);
        while (offset >= 0) {
          data[offset - 4] = data[offset];
          data[offset - 3] = data[offset + 1];
          data[offset - 2] = data[offset + 2];
          data[offset - 1] = data[offset + 3];
          data[offset + rowSize] = data[offset + rowSize - 4];
          data[offset + rowSize + 1] = data[offset + rowSize - 3];
          data[offset + rowSize + 2] = data[offset + rowSize - 2];
          data[offset + rowSize + 3] = data[offset + rowSize - 1];
          offset -= imgRowSize;
        }
      }
      // replacing queue items
      fnArray.splice(j, count * 4, OPS.paintInlineImageXObjectGroup);
      argsArray.splice(j, count * 4,
        [{ width: imgWidth, height: imgHeight, kind: ImageKind.RGBA_32BPP,
           data: imgData }, map]);
      context.currentOperation = j;
    });

  addState(InitialState,
    [OPS.save, OPS.transform, OPS.paintImageMaskXObject, OPS.restore],
    function foundImageMaskGroup(context) {
      // grouping paintImageMaskXObject's into paintImageMaskXObjectGroup
      // searching for (save, transform, paintImageMaskXObject, restore)+
      var MIN_IMAGES_IN_MASKS_BLOCK = 10;
      var MAX_IMAGES_IN_MASKS_BLOCK = 100;
      var MAX_SAME_IMAGES_IN_MASKS_BLOCK = 1000;

      var fnArray = context.fnArray, argsArray = context.argsArray;
      var j = context.currentOperation - 3, i = j + 4;
      var ii = fnArray.length, q;

      for (; i < ii && fnArray[i - 4] === fnArray[i]; i++) {}
      var count = (i - j) >> 2;
      count = handlePaintSolidColorImageMask(j, count, fnArray, argsArray);
      if (count < MIN_IMAGES_IN_MASKS_BLOCK) {
        context.currentOperation = i - 1;
        return;
      }

      var isSameImage = false;
      var transformArgs;
      if (argsArray[j + 1][1] === 0 && argsArray[j + 1][2] === 0) {
        i = j + 4;
        isSameImage = true;
        for (q = 1; q < count; q++, i += 4) {
          var prevTransformArgs = argsArray[i - 3];
          transformArgs = argsArray[i + 1];
          if (argsArray[i - 2][0] !== argsArray[i + 2][0] ||
              prevTransformArgs[0] !== transformArgs[0] ||
              prevTransformArgs[1] !== transformArgs[1] ||
              prevTransformArgs[2] !== transformArgs[2] ||
              prevTransformArgs[3] !== transformArgs[3]) {
            if (q < MIN_IMAGES_IN_MASKS_BLOCK) {
              isSameImage = false;
            } else {
              count = q;
            }
            break; // different image or transform
          }
        }
      }

      if (isSameImage) {
        count = Math.min(count, MAX_SAME_IMAGES_IN_MASKS_BLOCK);
        var positions = new Float32Array(count * 2);
        i = j + 1;
        for (q = 0; q < count; q++) {
          transformArgs = argsArray[i];
          positions[(q << 1)] = transformArgs[4];
          positions[(q << 1) + 1] = transformArgs[5];
          i += 4;
        }

        // replacing queue items
        fnArray.splice(j, count * 4, OPS.paintImageMaskXObjectRepeat);
        argsArray.splice(j, count * 4, [argsArray[j + 2][0],
                         argsArray[j + 1][0], argsArray[j + 1][3], positions]);

        context.currentOperation = j;
      } else {
        count = Math.min(count, MAX_IMAGES_IN_MASKS_BLOCK);
        var images = [];
        for (q = 0; q < count; q++) {
          transformArgs = argsArray[j + (q << 2) + 1];
          var maskParams = argsArray[j + (q << 2) + 2][0];
          images.push({ data: maskParams.data, width: maskParams.width,
                        height: maskParams.height,
                        transform: transformArgs });
        }

        // replacing queue items
        fnArray.splice(j, count * 4, OPS.paintImageMaskXObjectGroup);
        argsArray.splice(j, count * 4, [images]);

        context.currentOperation = j;
      }
    });

  addState(InitialState,
    [OPS.save, OPS.transform, OPS.paintImageXObject, OPS.restore],
    function (context) {
      var MIN_IMAGES_IN_BLOCK = 3;
      var MAX_IMAGES_IN_BLOCK = 1000;

      var fnArray = context.fnArray, argsArray = context.argsArray;
      var j = context.currentOperation - 3, i = j + 4;
      if (argsArray[j + 1][1] !== 0 || argsArray[j + 1][2] !== 0) {
        return;
      }
      var ii = fnArray.length;
      var transformArgs;
      for (; i + 3 < ii && fnArray[i - 4] === fnArray[i]; i += 4) {
        if (fnArray[i - 3] !== fnArray[i + 1] ||
            fnArray[i - 2] !== fnArray[i + 2] ||
            fnArray[i - 1] !== fnArray[i + 3]) {
          break;
        }
        if (argsArray[i - 2][0] !== argsArray[i + 2][0]) {
          break; // different image
        }
        var prevTransformArgs = argsArray[i - 3];
        transformArgs = argsArray[i + 1];
        if (prevTransformArgs[0] !== transformArgs[0] ||
            prevTransformArgs[1] !== transformArgs[1] ||
            prevTransformArgs[2] !== transformArgs[2] ||
            prevTransformArgs[3] !== transformArgs[3]) {
          break; // different transform
        }
      }
      var count = Math.min((i - j) >> 2, MAX_IMAGES_IN_BLOCK);
      if (count < MIN_IMAGES_IN_BLOCK) {
        context.currentOperation = i - 1;
        return;
      }

      var positions = new Float32Array(count * 2);
      i = j + 1;
      for (var q = 0; q < count; q++) {
        transformArgs = argsArray[i];
        positions[(q << 1)] = transformArgs[4];
        positions[(q << 1) + 1] = transformArgs[5];
        i += 4;
      }
      var args = [argsArray[j + 2][0], argsArray[j + 1][0],
                  argsArray[j + 1][3], positions];
      // replacing queue items
      fnArray.splice(j, count * 4, OPS.paintImageXObjectRepeat);
      argsArray.splice(j, count * 4, args);

      context.currentOperation = j;
    });

  addState(InitialState,
    [OPS.beginText, OPS.setFont, OPS.setTextMatrix, OPS.showText, OPS.endText],
    function (context) {
      // moving single chars with same font into beginText/endText groups
      // searching for (beginText, setFont, setTextMatrix, showText, endText)+
      var MIN_CHARS_IN_BLOCK = 3;
      var MAX_CHARS_IN_BLOCK = 1000;

      var fnArray = context.fnArray, argsArray = context.argsArray;
      var j = context.currentOperation - 4, i = j + 5;
      var ii = fnArray.length;

      for (; i < ii && fnArray[i - 5] === fnArray[i]; i++) {
        if (fnArray[i] === OPS.setFont) {
          if (argsArray[i - 5][0] !== argsArray[i][0] ||
              argsArray[i - 5][1] !== argsArray[i][1]) {
            break;
          }
        }
      }
      var count = Math.min(((i - j) / 5) | 0, MAX_CHARS_IN_BLOCK);
      if (count < MIN_CHARS_IN_BLOCK) {
        context.currentOperation = i - 1;
        return;
      }
      if (j >= 4 && fnArray[j - 4] === fnArray[j + 1] &&
          fnArray[j - 3] === fnArray[j + 2] &&
          fnArray[j - 2] === fnArray[j + 3] &&
          fnArray[j - 1] === fnArray[j + 4] &&
          argsArray[j - 4][0] === argsArray[j + 1][0] &&
          argsArray[j - 4][1] === argsArray[j + 1][1]) {
        // extending one block ahead (very first block might have 'dependency')
        count++;
        j -= 5;
      }
      var k = j + 7;
      i = j + 4;
      for (var q = 1; q < count; q++) {
        fnArray[i] = fnArray[k];
        argsArray[i] = argsArray[k];
        fnArray[i + 1] = fnArray[k + 1];
        argsArray[i + 1] = argsArray[k + 1];
        i += 2;
        k += 5;
      }
      var removed = (count - 1) * 3;
      fnArray.splice(i, removed);
      argsArray.splice(i, removed);

      context.currentOperation = i;
    });

  function QueueOptimizer() {}

  QueueOptimizer.prototype = {
    optimize: function QueueOptimizer_optimize(queue) {
      var fnArray = queue.fnArray, argsArray = queue.argsArray;
      var context = {
        currentOperation: 0,
        fnArray: fnArray,
        argsArray: argsArray
      };
      var i, ii = argsArray.length;
      var state;
      for (i = 0; i < ii; i++) {
        state = (state || InitialState)[fnArray[i]];
        if (typeof state === 'function') { // we found some handler
          context.currentOperation = i;
          state = state(context);
          i = context.currentOperation;
          ii = context.fnArray.length;
        }
      }
    }
  };
  return QueueOptimizer;
})();
