text: /* 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.
   */
  
  import {
    AbortException, assert, CMapCompressionType, createPromiseCapability,
    FONT_IDENTITY_MATRIX, FormatError, getLookupTableFactory, IDENTITY_MATRIX,
    ImageKind, info, isNum, isString, NativeImageDecoding, OPS, TextRenderingMode,
    UNSUPPORTED_FEATURES, Util, warn
  } from '../shared/util';
  import { CMapFactory, IdentityCMap } from './cmap';
  import { DecodeStream, JpegStream, Stream } from './stream';
  import {
    Dict, isCmd, isDict, isEOF, isName, isRef, isStream, Name
  } from './primitives';
  import {
    ErrorFont, Font, FontFlags, getFontType, IdentityToUnicodeMap, ToUnicodeMap
  } from './fonts';
  import {
    getEncoding, MacRomanEncoding, StandardEncoding, SymbolSetEncoding,
    WinAnsiEncoding, ZapfDingbatsEncoding
  } from './encodings';
  import {
    getNormalizedUnicodes, getUnicodeForGlyph, reverseIfRtl
  } from './unicode';
  import {
    getSerifFonts, getStdFontMap, getSymbolsFonts
  } from './standard_fonts';
  import { getTilingPatternIR, Pattern } from './pattern';
  import { Lexer, Parser } from './parser';
  import { bidi } from './bidi';
  import { ColorSpace } from './colorspace';
  import { getGlyphsUnicode } from './glyphlist';
  import { getMetrics } from './metrics';
  import { isPDFFunction } from './function';
  import { MurmurHash3_64 } from './murmurhash3';
  import { PDFImage } from './image';
  
  var PartialEvaluator = (function PartialEvaluatorClosure() {
    const DefaultPartialEvaluatorOptions = {
      forceDataSchema: false,
      maxImageSize: -1,
      disableFontFace: false,
      nativeImageDecoderSupport: NativeImageDecoding.DECODE,
      ignoreErrors: false,
      isEvalSupported: true,
    };
  
    function NativeImageDecoder({ xref, resources, handler,
                                  forceDataSchema = false,
                                  pdfFunctionFactory, }) {
      this.xref = xref;
      this.resources = resources;
      this.handler = handler;
      this.forceDataSchema = forceDataSchema;
      this.pdfFunctionFactory = pdfFunctionFactory;
    }
    NativeImageDecoder.prototype = {
      canDecode(image) {
        return image instanceof JpegStream &&
               NativeImageDecoder.isDecodable(image, this.xref, this.resources,
                                              this.pdfFunctionFactory);
      },
      decode(image) {
        // 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, this.xref, this.resources,
                                      this.pdfFunctionFactory);
        var numComps = colorSpace.numComps;
        var decodePromise = this.handler.sendWithPromise('JpegDecode',
          [image.getIR(this.forceDataSchema), numComps]);
        return decodePromise.then(function (message) {
          var data = message.data;
          return new Stream(data, 0, data.length, image.dict);
        });
      },
    };
    /**
     * Checks if the image can be decoded and displayed by the browser without any
     * further processing such as color space conversions.
     */
    NativeImageDecoder.isSupported = function(image, xref, res,
                                              pdfFunctionFactory) {
      var dict = image.dict;
      if (dict.has('DecodeParms') || dict.has('DP')) {
        return false;
      }
      var cs = ColorSpace.parse(dict.get('ColorSpace', 'CS'), xref, res,
                                pdfFunctionFactory);
      return (cs.name === 'DeviceGray' || cs.name === 'DeviceRGB') &&
             cs.isDefaultDecode(dict.getArray('Decode', 'D'));
    };
    /**
     * Checks if the image can be decoded by the browser.
     */
    NativeImageDecoder.isDecodable = function(image, xref, res,
                                              pdfFunctionFactory) {
      var dict = image.dict;
      if (dict.has('DecodeParms') || dict.has('DP')) {
        return false;
      }
      var cs = ColorSpace.parse(dict.get('ColorSpace', 'CS'), xref, res,
                                pdfFunctionFactory);
      return (cs.numComps === 1 || cs.numComps === 3) &&
             cs.isDefaultDecode(dict.getArray('Decode', 'D'));
    };
  
    function PartialEvaluator({ pdfManager, xref, handler, pageIndex, idFactory,
                                fontCache, builtInCMapCache, options = null,
                                pdfFunctionFactory, }) {
      this.pdfManager = pdfManager;
      this.xref = xref;
      this.handler = handler;
      this.pageIndex = pageIndex;
      this.idFactory = idFactory;
      this.fontCache = fontCache;
      this.builtInCMapCache = builtInCMapCache;
      this.options = options || DefaultPartialEvaluatorOptions;
      this.pdfFunctionFactory = pdfFunctionFactory;
  
      this.fetchBuiltInCMap = (name) => {
        var cachedCMap = this.builtInCMapCache[name];
        if (cachedCMap) {
          return Promise.resolve(cachedCMap);
        }
        return this.handler.sendWithPromise('FetchBuiltInCMap', {
          name,
        }).then((data) => {
          if (data.compressionType !== CMapCompressionType.NONE) {
            // Given the size of uncompressed CMaps, only cache compressed ones.
            this.builtInCMapCache[name] = data;
          }
          return data;
        });
      };
    }
  
    // Trying to minimize Date.now() usage and check every 100 time
    var TIME_SLOT_DURATION_MS = 20;
    var CHECK_TIME_EVERY = 100;
    function TimeSlotManager() {
      this.reset();
    }
    TimeSlotManager.prototype = {
      check: function TimeSlotManager_check() {
        if (++this.checked < CHECK_TIME_EVERY) {
          return false;
        }
        this.checked = 0;
        return this.endTime <= Date.now();
      },
      reset: function TimeSlotManager_reset() {
        this.endTime = Date.now() + TIME_SLOT_DURATION_MS;
        this.checked = 0;
      },
    };
  
    // Convert PDF blend mode names to HTML5 blend mode names.
    function normalizeBlendMode(value) {
      if (!isName(value)) {
        return 'source-over';
      }
      switch (value.name) {
        case 'Normal':
        case 'Compatible':
          return 'source-over';
        case 'Multiply':
          return 'multiply';
        case 'Screen':
          return 'screen';
        case 'Overlay':
          return 'overlay';
        case 'Darken':
          return 'darken';
        case 'Lighten':
          return 'lighten';
        case 'ColorDodge':
          return 'color-dodge';
        case 'ColorBurn':
          return 'color-burn';
        case 'HardLight':
          return 'hard-light';
        case 'SoftLight':
          return 'soft-light';
        case 'Difference':
          return 'difference';
        case 'Exclusion':
          return 'exclusion';
        case 'Hue':
          return 'hue';
        case 'Saturation':
          return 'saturation';
        case 'Color':
          return 'color';
        case 'Luminosity':
          return 'luminosity';
      }
      warn('Unsupported blend mode: ' + value.name);
      return 'source-over';
    }
  
    var deferred = Promise.resolve();
  
    var TILING_PATTERN = 1, SHADING_PATTERN = 2;
  
    PartialEvaluator.prototype = {
      clone(newOptions = DefaultPartialEvaluatorOptions) {
        var newEvaluator = Object.create(this);
        newEvaluator.options = newOptions;
        return newEvaluator;
      },
  
      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], xref = this.xref;
        while (nodes.length) {
          var key, i, ii;
          var node = nodes.shift();
          // First check the current resources for blend modes.
          var graphicStates = node.get('ExtGState');
          if (isDict(graphicStates)) {
            var graphicStatesKeys = graphicStates.getKeys();
            for (i = 0, ii = graphicStatesKeys.length; i < ii; i++) {
              key = graphicStatesKeys[i];
  
              var graphicState = graphicStates.get(key);
              var bm = graphicState.get('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;
          }
          var xObjectsKeys = xObjects.getKeys();
          for (i = 0, ii = xObjectsKeys.length; i < ii; i++) {
            key = xObjectsKeys[i];
  
            var xObject = xObjects.getRaw(key);
            if (isRef(xObject)) {
              if (processed[xObject.toString()]) {
                // The XObject has already been processed, and by avoiding a
                // redundant `xref.fetch` we can *significantly* reduce the load
                // time for badly generated PDF files (fixes issue6961.pdf).
                continue;
              }
              xObject = xref.fetch(xObject);
            }
            if (!isStream(xObject)) {
              continue;
            }
            if (xObject.dict.objId) {
              if (processed[xObject.dict.objId]) {
                // stream has objId and is processed already
                continue;
              }
              processed[xObject.dict.objId] = true;
            }
            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,
                                                                   task,
                                                                   initialState) {
        var dict = xobj.dict;
        var matrix = dict.getArray('Matrix');
        var bbox = dict.getArray('BBox');
        var group = dict.get('Group');
        if (group) {
          var groupOptions = {
            matrix,
            bbox,
            smask,
            isolated: false,
            knockout: false,
          };
  
          var groupSubtype = group.get('S');
          var colorSpace = null;
          if (isName(groupSubtype, 'Transparency')) {
            groupOptions.isolated = (group.get('I') || false);
            groupOptions.knockout = (group.get('K') || false);
            if (group.has('CS')) {
              colorSpace = ColorSpace.parse(group.get('CS'), this.xref, resources,
                                            this.pdfFunctionFactory);
            }
          }
  
          if (smask && smask.backdrop) {
            colorSpace = colorSpace || ColorSpace.singletons.rgb;
            smask.backdrop = colorSpace.getRgb(smask.backdrop, 0);
          }
  
          operatorList.addOp(OPS.beginGroup, [groupOptions]);
        }
  
        operatorList.addOp(OPS.paintFormXObjectBegin, [matrix, bbox]);
  
        return this.getOperatorList({
          stream: xobj,
          task,
          resources: dict.get('Resources') || resources,
          operatorList,
          initialState,
        }).then(function () {
          operatorList.addOp(OPS.paintFormXObjectEnd, []);
  
          if (group) {
            operatorList.addOp(OPS.endGroup, [groupOptions]);
          }
        });
      },
  
      buildPaintImageXObject:
          function PartialEvaluator_buildPaintImageXObject(resources, image,
                                                           inline, operatorList,
                                                           cacheKey, imageCache) {
        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;
        }
        var maxImageSize = this.options.maxImageSize;
        if (maxImageSize !== -1 && w * h > 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.getArray('Decode', 'D');
  
          imgData = PDFImage.createMask({
            imgArray,
            width,
            height,
            imageIsFromDecodeStream: image instanceof DecodeStream,
            inverseDecode: (!!decode && decode[0] > 0),
          });
          imgData.cached = true;
          args = [imgData];
          operatorList.addOp(OPS.paintImageMaskXObject, args);
          if (cacheKey) {
            imageCache[cacheKey] = {
              fn: OPS.paintImageMaskXObject,
              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) {
          let imageObj = new PDFImage({
            xref: this.xref,
            res: resources,
            image,
            pdfFunctionFactory: this.pdfFunctionFactory,
          });
          // 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;
        }
  
        var nativeImageDecoderSupport = this.options.nativeImageDecoderSupport;
        // If there is no imageMask, create the PDFImage and a lot
        // of image processing can be done here.
        var objId = 'img_' + this.idFactory.createObjId();
        operatorList.addDependency(objId);
        args = [objId, w, h];
  
        if (nativeImageDecoderSupport !== NativeImageDecoding.NONE &&
            !softMask && !mask && image instanceof JpegStream &&
            NativeImageDecoder.isSupported(image, this.xref, resources,
                                           this.pdfFunctionFactory)) {
          // 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(this.options.forceDataSchema)]);
          if (cacheKey) {
            imageCache[cacheKey] = {
              fn: OPS.paintJpegXObject,
              args,
            };
          }
          return;
        }
  
        // Creates native image decoder only if a JPEG image or mask is present.
        var nativeImageDecoder = null;
        if (nativeImageDecoderSupport === NativeImageDecoding.DECODE &&
            (image instanceof JpegStream || mask instanceof JpegStream ||
             softMask instanceof JpegStream)) {
          nativeImageDecoder = new NativeImageDecoder({
            xref: this.xref,
            resources,
            handler: this.handler,
            forceDataSchema: this.options.forceDataSchema,
            pdfFunctionFactory: this.pdfFunctionFactory,
          });
        }
  
        PDFImage.buildImage({
          handler: this.handler,
          xref: this.xref,
          res: resources,
          image,
          nativeDecoder: nativeImageDecoder,
          pdfFunctionFactory: this.pdfFunctionFactory,
        }).then((imageObj) => {
          var imgData = imageObj.createImageData(/* forceRGBA = */ false);
          this.handler.send('obj', [objId, this.pageIndex, 'Image', imgData],
            [imgData.data.buffer]);
        }).catch((reason) => {
          warn('Unable to decode image: ' + reason);
          this.handler.send('obj', [objId, this.pageIndex, 'Image', null]);
        });
  
        operatorList.addOp(OPS.paintImageXObject, args);
        if (cacheKey) {
          imageCache[cacheKey] = {
            fn: OPS.paintImageXObject,
            args,
          };
        }
      },
  
      handleSMask: function PartialEvaluator_handleSmask(smask, resources,
                                                         operatorList, task,
                                                         stateManager) {
        var smaskContent = smask.get('G');
        var smaskOptions = {
          subtype: smask.get('S').name,
          backdrop: smask.get('BC'),
        };
  
        // The SMask might have a alpha/luminosity value transfer function --
        // we will build a map of integer values in range 0..255 to be fast.
        var transferObj = smask.get('TR');
        if (isPDFFunction(transferObj)) {
          let transferFn = this.pdfFunctionFactory.create(transferObj);
          var transferMap = new Uint8Array(256);
          var tmp = new Float32Array(1);
          for (var i = 0; i < 256; i++) {
            tmp[0] = i / 255;
            transferFn(tmp, 0, tmp, 0);
            transferMap[i] = (tmp[0] * 255) | 0;
          }
          smaskOptions.transferMap = transferMap;
        }
  
        return this.buildFormXObject(resources, smaskContent, smaskOptions,
                                     operatorList, task,
                                     stateManager.state.clone());
      },
  
      handleTilingType(fn, args, resources, pattern, patternDict, operatorList,
                       task) {
        // Create an IR of the pattern code.
        let tilingOpList = new OperatorList();
        // Merge the available resources, to prevent issues when the patternDict
        // is missing some /Resources entries (fixes issue6541.pdf).
        let resourcesArray = [patternDict.get('Resources'), resources];
        let patternResources = Dict.merge(this.xref, resourcesArray);
  
        return this.getOperatorList({
          stream: pattern,
          task,
          resources: patternResources,
          operatorList: tilingOpList,
        }).then(function() {
          return getTilingPatternIR({
            fnArray: tilingOpList.fnArray,
            argsArray: tilingOpList.argsArray,
          }, patternDict, args);
        }).then(function(tilingPatternIR) {
          // Add the dependencies to the parent operator list so they are
          // resolved before the sub operator list is executed synchronously.
          operatorList.addDependencies(tilingOpList.dependencies);
          operatorList.addOp(fn, tilingPatternIR);
        }, (reason) => {
          if (this.options.ignoreErrors) {
            // Error(s) in the TilingPattern -- sending unsupported feature
            // notification and allow rendering to continue.
            this.handler.send('UnsupportedFeature',
                              { featureId: UNSUPPORTED_FEATURES.unknown, });
            warn(`handleTilingType - ignoring pattern: "${reason}".`);
            return;
          }
          throw reason;
        });
      },
  
      handleSetFont:
          function PartialEvaluator_handleSetFont(resources, fontArgs, fontRef,
                                                  operatorList, task, state) {
        // TODO(mack): Not needed?
        var fontName;
        if (fontArgs) {
          fontArgs = fontArgs.slice();
          fontName = fontArgs[0].name;
        }
  
        return this.loadFont(fontName, fontRef, resources).then((translated) => {
          if (!translated.font.isType3Font) {
            return translated;
          }
          return translated.loadType3Data(this, resources, operatorList, task).
            then(function () {
            return translated;
          }).catch((reason) => {
            // Error in the font data -- sending unsupported feature notification.
            this.handler.send('UnsupportedFeature',
                              { featureId: UNSUPPORTED_FEATURES.font, });
            return new TranslatedFont('g_font_error',
              new ErrorFont('Type3 font load error: ' + reason), translated.font);
          });
        }).then((translated) => {
          state.font = translated.font;
          translated.send(this.handler);
          return translated.loadedName;
        });
      },
  
      handleText: function PartialEvaluator_handleText(chars, state) {
        var font = state.font;
        var glyphs = font.charsToGlyphs(chars);
        var isAddToPathSet = !!(state.textRenderingMode &
                                TextRenderingMode.ADD_TO_PATH_FLAG);
        if (font.data && (isAddToPathSet || this.options.disableFontFace)) {
          var buildPath = (fontChar) => {
            if (!font.renderer.hasBuiltPath(fontChar)) {
              var path = font.renderer.getPathJs(fontChar);
              this.handler.send('commonobj', [
                font.loadedName + '_path_' + fontChar,
                'FontPath',
                path
              ]);
            }
          };
  
          for (var i = 0, ii = glyphs.length; i < ii; i++) {
            var glyph = glyphs[i];
            buildPath(glyph.fontChar);
  
            // If the glyph has an accent we need to build a path for its
            // fontChar too, otherwise CanvasGraphics_paintChar will fail.
            var accent = glyph.accent;
            if (accent && accent.fontChar) {
              buildPath(accent.fontChar);
            }
          }
        }
  
        return glyphs;
      },
  
      setGState: function PartialEvaluator_setGState(resources, gState,
                                                     operatorList, task,
                                                     stateManager) {
        // This array holds the converted/processed state data.
        var gStateObj = [];
        var gStateKeys = gState.getKeys();
        var promise = Promise.resolve();
        for (var i = 0, ii = gStateKeys.length; i < ii; i++) {
          let key = gStateKeys[i];
          let value = gState.get(key);
          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':
              promise = promise.then(() => {
                return this.handleSetFont(resources, null, value[0], operatorList,
                                          task, stateManager.state).
                  then(function (loadedName) {
                    operatorList.addDependency(loadedName);
                    gStateObj.push([key, [loadedName, value[1]]]);
                  });
              });
              break;
            case 'BM':
              gStateObj.push([key, normalizeBlendMode(value)]);
              break;
            case 'SMask':
              if (isName(value, 'None')) {
                gStateObj.push([key, false]);
                break;
              }
              if (isDict(value)) {
                promise = promise.then(() => {
                  return this.handleSMask(value, resources, operatorList,
                                          task, stateManager);
                });
                gStateObj.push([key, true]);
              } else {
                warn('Unsupported SMask type');
              }
  
              break;
            // Only generate info log messages for the following since
            // they are unlikely 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;
          }
        }
        return promise.then(function () {
          if (gStateObj.length > 0) {
            operatorList.addOp(OPS.setGState, [gStateObj]);
          }
        });
      },
  
      loadFont: function PartialEvaluator_loadFont(fontName, font, resources) {
        function errorFont() {
          return Promise.resolve(new TranslatedFont('g_font_error',
            new ErrorFont('Font ' + fontName + ' is not available'), font));
        }
  
        var fontRef, xref = this.xref;
        if (font) { // Loading by ref.
          if (!isRef(font)) {
            throw new Error('The "font" object should be a reference.');
          }
          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 (!fontRef) {
          warn('fontRef not available');
          return errorFont();
        }
  
        if (this.fontCache.has(fontRef)) {
          return this.fontCache.get(fontRef);
        }
  
        font = xref.fetchIfRef(fontRef);
        if (!isDict(font)) {
          return errorFont();
        }
  
        // We are holding `font.translated` references just for `fontRef`s that
        // are not actually `Ref`s, but rather `Dict`s. See explanation below.
        if (font.translated) {
          return font.translated;
        }
  
        var fontCapability = createPromiseCapability();
  
        var preEvaluatedFont = this.preEvaluateFont(font);
        var descriptor = preEvaluatedFont.descriptor;
  
        var fontRefIsRef = isRef(fontRef), fontID;
        if (fontRefIsRef) {
          fontID = fontRef.toString();
        }
  
        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 (fontRefIsRef && aliasFontRef &&
                this.fontCache.has(aliasFontRef)) {
              this.fontCache.putAlias(fontRef, aliasFontRef);
              return this.fontCache.get(fontRef);
            }
          } else {
            fontAliases[hash] = {
              fontID: Font.getFontID(),
            };
          }
  
          if (fontRefIsRef) {
            fontAliases[hash].aliasRef = fontRef;
          }
          fontID = fontAliases[hash].fontID;
        }
  
        // Workaround for bad PDF generators that reference fonts incorrectly,
        // where `fontRef` is a `Dict` rather than a `Ref` (fixes bug946506.pdf).
        // In this case we should not put the font into `this.fontCache` (which is
        // a `RefSetCache`), since it's not meaningful to use a `Dict` as a key.
        //
        // However, if we don't cache the font it's not possible to remove it
        // when `cleanup` is triggered from the API, which causes issues on
        // subsequent rendering operations (see issue7403.pdf).
        // A simple workaround would be to just not hold `font.translated`
        // references in this case, but this would force us to unnecessarily load
        // the same fonts over and over.
        //
        // Instead, we cheat a bit by attempting to use a modified `fontID` as a
        // key in `this.fontCache`, to allow the font to be cached.
        // NOTE: This works because `RefSetCache` calls `toString()` on provided
        //       keys. Also, since `fontRef` is used when getting cached fonts,
        //       we'll not accidentally match fonts cached with the `fontID`.
        if (fontRefIsRef) {
          this.fontCache.put(fontRef, fontCapability.promise);
        } else {
          if (!fontID) {
            fontID = this.idFactory.createObjId();
          }
          this.fontCache.put('id_' + fontID, fontCapability.promise);
        }
        assert(fontID, 'The "fontID" must be defined.');
  
        // Keep track of each font we translated so the caller can
        // load them asynchronously before calling display on a page.
        font.loadedName = 'g_' + this.pdfManager.docId + '_f' + fontID;
  
        font.translated = fontCapability.promise;
  
        // TODO move promises into translate font
        var translatedPromise;
        try {
          translatedPromise = this.translateFont(preEvaluatedFont);
        } catch (e) {
          translatedPromise = Promise.reject(e);
        }
  
        translatedPromise.then(function (translatedFont) {
          if (translatedFont.fontType !== undefined) {
            var xrefFontStats = xref.stats.fontTypes;
            xrefFontStats[translatedFont.fontType] = true;
          }
  
          fontCapability.resolve(new TranslatedFont(font.loadedName,
            translatedFont, font));
        }).catch((reason) => {
          // TODO fontCapability.reject?
          // Error in the font data -- sending unsupported feature notification.
          this.handler.send('UnsupportedFeature',
                            { featureId: UNSUPPORTED_FEATURES.font, });
  
          try {
            // error, but it's still nice to have font type reported
            var descriptor = preEvaluatedFont.descriptor;
            var fontFile3 = descriptor && descriptor.get('FontFile3');
            var subtype = fontFile3 && fontFile3.get('Subtype');
            var fontType = getFontType(preEvaluatedFont.type,
                                       subtype && subtype.name);
            var xrefFontStats = xref.stats.fontTypes;
            xrefFontStats[fontType] = true;
          } catch (ex) { }
  
          fontCapability.resolve(new TranslatedFont(font.loadedName,
            new ErrorFont(reason instanceof Error ? reason.message : reason),
            font));
        });
        return fontCapability.promise;
      },
  
      buildPath: function PartialEvaluator_buildPath(operatorList, fn, args) {
        var lastIndex = operatorList.length - 1;
        if (!args) {
          args = [];
        }
        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);
        }
      },
  
      handleColorN: function PartialEvaluator_handleColorN(operatorList, fn, args,
                                                           cs, patterns,
                                                           resources, task) {
        // 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) {
            var color = cs.base ? cs.base.getRgb(args, 0) : null;
            return this.handleTilingType(fn, color, resources, pattern,
                                         dict, operatorList, task);
          } else if (typeNum === SHADING_PATTERN) {
            var shading = dict.get('Shading');
            var matrix = dict.getArray('Matrix');
            pattern = Pattern.parseShading(shading, matrix, this.xref, resources,
                                           this.handler, this.pdfFunctionFactory);
            operatorList.addOp(fn, pattern.getIR());
            return Promise.resolve();
          }
          return Promise.reject(new Error('Unknown PatternType: ' + typeNum));
        }
        // TODO shall we fail here?
        operatorList.addOp(fn, args);
        return Promise.resolve();
      },
  
      getOperatorList({ stream, task, resources, operatorList,
                        initialState = null, }) {
        // Ensure that `resources`/`initialState` is correctly initialized,
        // even if the provided parameter is e.g. `null`.
        resources = resources || Dict.empty;
        initialState = initialState || new EvalState();
  
        if (!operatorList) {
          throw new Error('getOperatorList: missing "operatorList" parameter');
        }
  
        var self = this;
        var xref = this.xref;
        var imageCache = Object.create(null);
  
        var xobjs = (resources.get('XObject') || Dict.empty);
        var patterns = (resources.get('Pattern') || Dict.empty);
        var stateManager = new StateManager(initialState);
        var preprocessor = new EvaluatorPreprocessor(stream, xref, stateManager);
        var timeSlotManager = new TimeSlotManager();
  
        function closePendingRestoreOPS(argument) {
          for (var i = 0, ii = preprocessor.savedStatesDepth; i < ii; i++) {
            operatorList.addOp(OPS.restore, []);
          }
        }
  
        return new Promise(function promiseBody(resolve, reject) {
          var next = function (promise) {
            promise.then(function () {
              try {
                promiseBody(resolve, reject);
              } catch (ex) {
                reject(ex);
              }
            }, reject);
          };
          task.ensureNotTerminated();
          timeSlotManager.reset();
          var stop, operation = {}, i, ii, cs;
          while (!(stop = timeSlotManager.check())) {
            // The arguments parsed by read() are used beyond this loop, so we
            // cannot reuse the same array on each iteration. Therefore we pass
            // in |null| as the initial value (see the comment on
            // EvaluatorPreprocessor_read() for why).
            operation.args = null;
            if (!(preprocessor.read(operation))) {
              break;
            }
            var args = operation.args;
            var fn = operation.fn;
  
            switch (fn | 0) {
              case OPS.paintXObject:
                // eagerly compile XForm objects
                var name = args[0].name;
                if (name && imageCache[name] !== undefined) {
                  operatorList.addOp(imageCache[name].fn, imageCache[name].args);
                  args = null;
                  continue;
                }
  
                next(new Promise(function(resolveXObject, rejectXObject) {
                  if (!name) {
                    throw new FormatError('XObject must be referred to by name.');
                  }
  
                  let xobj = xobjs.get(name);
                  if (!xobj) {
                    operatorList.addOp(fn, args);
                    resolveXObject();
                    return;
                  }
                  if (!isStream(xobj)) {
                    throw new FormatError('XObject should be a stream');
                  }
  
                  let type = xobj.dict.get('Subtype');
                  if (!isName(type)) {
                    throw new FormatError('XObject should have a Name subtype');
                  }
  
                  if (type.name === 'Form') {
                    stateManager.save();
                    self.buildFormXObject(resources, xobj, null, operatorList,
                                          task, stateManager.state.clone()).
                      then(function() {
                        stateManager.restore();
                        resolveXObject();
                      }, rejectXObject);
                    return;
                  } else if (type.name === 'Image') {
                    self.buildPaintImageXObject(resources, xobj, false,
                                                operatorList, name, imageCache);
                  } else if (type.name === 'PS') {
                    // PostScript XObjects are unused when viewing documents.
                    // See section 4.7.1 of Adobe's PDF reference.
                    info('Ignored XObject subtype PS');
                  } else {
                    throw new FormatError(
                      `Unhandled XObject subtype ${type.name}`);
                  }
                  resolveXObject();
                }).catch(function(reason) {
                  if (self.options.ignoreErrors) {
                    // Error(s) in the XObject -- sending unsupported feature
                    // notification and allow rendering to continue.
                    self.handler.send('UnsupportedFeature',
                      { featureId: UNSUPPORTED_FEATURES.unknown, });
                    warn(`getOperatorList - ignoring XObject: "${reason}".`);
                    return;
                  }
                  throw reason;
                }));
                return;
              case OPS.setFont:
                var fontSize = args[1];
                // eagerly collect all fonts
                next(self.handleSetFont(resources, args, null, operatorList,
                                        task, stateManager.state).
                  then(function (loadedName) {
                    operatorList.addDependency(loadedName);
                    operatorList.addOp(OPS.setFont, [loadedName, fontSize]);
                  }));
                return;
              case OPS.endInlineImage:
                var cacheKey = args[0].cacheKey;
                if (cacheKey) {
                  var cacheEntry = imageCache[cacheKey];
                  if (cacheEntry !== undefined) {
                    operatorList.addOp(cacheEntry.fn, cacheEntry.args);
                    args = null;
                    continue;
                  }
                }
                self.buildPaintImageXObject(resources, args[0], true,
                  operatorList, cacheKey, imageCache);
                args = null;
                continue;
              case OPS.showText:
                args[0] = self.handleText(args[0], stateManager.state);
                break;
              case OPS.showSpacedText:
                var arr = args[0];
                var combinedGlyphs = [];
                var arrLength = arr.length;
                var state = stateManager.state;
                for (i = 0; i < arrLength; ++i) {
                  var arrItem = arr[i];
                  if (isString(arrItem)) {
                    Array.prototype.push.apply(combinedGlyphs,
                      self.handleText(arrItem, state));
                  } else if (isNum(arrItem)) {
                    combinedGlyphs.push(arrItem);
                  }
                }
                args[0] = combinedGlyphs;
                fn = OPS.showText;
                break;
              case OPS.nextLineShowText:
                operatorList.addOp(OPS.nextLine);
                args[0] = self.handleText(args[0], stateManager.state);
                fn = OPS.showText;
                break;
              case OPS.nextLineSetSpacingShowText:
                operatorList.addOp(OPS.nextLine);
                operatorList.addOp(OPS.setWordSpacing, [args.shift()]);
                operatorList.addOp(OPS.setCharSpacing, [args.shift()]);
                args[0] = self.handleText(args[0], stateManager.state);
                fn = OPS.showText;
                break;
              case OPS.setTextRenderingMode:
                stateManager.state.textRenderingMode = args[0];
                break;
  
              case OPS.setFillColorSpace:
                stateManager.state.fillColorSpace =
                  ColorSpace.parse(args[0], xref, resources,
                                   self.pdfFunctionFactory);
                continue;
              case OPS.setStrokeColorSpace:
                stateManager.state.strokeColorSpace =
                  ColorSpace.parse(args[0], xref, resources,
                                   self.pdfFunctionFactory);
                continue;
              case OPS.setFillColor:
                cs = stateManager.state.fillColorSpace;
                args = cs.getRgb(args, 0);
                fn = OPS.setFillRGBColor;
                break;
              case OPS.setStrokeColor:
                cs = stateManager.state.strokeColorSpace;
                args = cs.getRgb(args, 0);
                fn = OPS.setStrokeRGBColor;
                break;
              case OPS.setFillGray:
                stateManager.state.fillColorSpace = ColorSpace.singletons.gray;
                args = ColorSpace.singletons.gray.getRgb(args, 0);
                fn = OPS.setFillRGBColor;
                break;
              case OPS.setStrokeGray:
                stateManager.state.strokeColorSpace = ColorSpace.singletons.gray;
                args = ColorSpace.singletons.gray.getRgb(args, 0);
                fn = OPS.setStrokeRGBColor;
                break;
              case OPS.setFillCMYKColor:
                stateManager.state.fillColorSpace = ColorSpace.singletons.cmyk;
                args = ColorSpace.singletons.cmyk.getRgb(args, 0);
                fn = OPS.setFillRGBColor;
                break;
              case OPS.setStrokeCMYKColor:
                stateManager.state.strokeColorSpace = ColorSpace.singletons.cmyk;
                args = ColorSpace.singletons.cmyk.getRgb(args, 0);
                fn = OPS.setStrokeRGBColor;
                break;
              case OPS.setFillRGBColor:
                stateManager.state.fillColorSpace = ColorSpace.singletons.rgb;
                args = ColorSpace.singletons.rgb.getRgb(args, 0);
                break;
              case OPS.setStrokeRGBColor:
                stateManager.state.strokeColorSpace = ColorSpace.singletons.rgb;
                args = ColorSpace.singletons.rgb.getRgb(args, 0);
                break;
              case OPS.setFillColorN:
                cs = stateManager.state.fillColorSpace;
                if (cs.name === 'Pattern') {
                  next(self.handleColorN(operatorList, OPS.setFillColorN, args,
                                         cs, patterns, resources, task));
                  return;
                }
                args = cs.getRgb(args, 0);
                fn = OPS.setFillRGBColor;
                break;
              case OPS.setStrokeColorN:
                cs = stateManager.state.strokeColorSpace;
                if (cs.name === 'Pattern') {
                  next(self.handleColorN(operatorList, OPS.setStrokeColorN, args,
                                         cs, patterns, resources, task));
                  return;
                }
                args = cs.getRgb(args, 0);
                fn = OPS.setStrokeRGBColor;
                break;
  
              case OPS.shadingFill:
                var shadingRes = resources.get('Shading');
                if (!shadingRes) {
                  throw new FormatError('No shading resource found');
                }
  
                var shading = shadingRes.get(args[0].name);
                if (!shading) {
                  throw new FormatError('No shading object found');
                }
  
                var shadingFill = Pattern.parseShading(shading, null, xref,
                  resources, self.handler, self.pdfFunctionFactory);
                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);
                next(self.setGState(resources, gState, operatorList, task,
                                    stateManager));
                return;
              case OPS.moveTo:
              case OPS.lineTo:
              case OPS.curveTo:
              case OPS.curveTo2:
              case OPS.curveTo3:
              case OPS.closePath:
                self.buildPath(operatorList, fn, args);
                continue;
              case OPS.rectangle:
                self.buildPath(operatorList, fn, args);
                continue;
              case OPS.markPoint:
              case OPS.markPointProps:
              case OPS.beginMarkedContent:
              case OPS.beginMarkedContentProps:
              case OPS.endMarkedContent:
              case OPS.beginCompat:
              case OPS.endCompat:
                // Ignore operators where the corresponding handlers are known to
                // be no-op in CanvasGraphics (display/canvas.js). This prevents
                // serialization errors and is also a bit more efficient.
                // We could also try to serialize all objects in a general way,
                // e.g. as done in https://github.com/mozilla/pdf.js/pull/6266,
                // but doing so is meaningless without knowing the semantics.
                continue;
              default:
                // Note: Ignore the operator if it has `Dict` arguments, since
                // those are non-serializable, otherwise postMessage will throw
                // "An object could not be cloned.".
                if (args !== null) {
                  for (i = 0, ii = args.length; i < ii; i++) {
                    if (args[i] instanceof Dict) {
                      break;
                    }
                  }
                  if (i < ii) {
                    warn('getOperatorList - ignoring operator: ' + fn);
                    continue;
                  }
                }
            }
            operatorList.addOp(fn, args);
          }
          if (stop) {
            next(deferred);
            return;
          }
          // Some PDFs don't close all restores inside object/form.
          // Closing those for them.
          closePendingRestoreOPS();
          resolve();
        }).catch((reason) => {
          if (this.options.ignoreErrors) {
            // Error(s) in the OperatorList -- sending unsupported feature
            // notification and allow rendering to continue.
            this.handler.send('UnsupportedFeature',
                              { featureId: UNSUPPORTED_FEATURES.unknown, });
            warn('getOperatorList - ignoring errors during task: ' + task.name);
  
            closePendingRestoreOPS();
            return;
          }
          throw reason;
        });
      },
  
      getTextContent({ stream, task, resources, stateManager = null,
                       normalizeWhitespace = false, combineTextItems = false,
                       sink, seenStyles = Object.create(null), }) {
        // Ensure that `resources`/`stateManager` is correctly initialized,
        // even if the provided parameter is e.g. `null`.
        resources = resources || Dict.empty;
        stateManager = stateManager || new StateManager(new TextState());
  
        var WhitespaceRegexp = /\s/g;
  
        var textContent = {
          items: [],
          styles: Object.create(null),
        };
        var textContentItem = {
          initialized: false,
          str: [],
          width: 0,
          height: 0,
          vertical: false,
          lastAdvanceWidth: 0,
          lastAdvanceHeight: 0,
          textAdvanceScale: 0,
          spaceWidth: 0,
          fakeSpaceMin: Infinity,
          fakeMultiSpaceMin: Infinity,
          fakeMultiSpaceMax: -0,
          textRunBreakAllowed: false,
          transform: null,
          fontName: null,
        };
        var SPACE_FACTOR = 0.3;
        var MULTI_SPACE_FACTOR = 1.5;
        var MULTI_SPACE_FACTOR_MAX = 4;
  
        var self = this;
        var xref = this.xref;
  
        // The xobj is parsed iff it's needed, e.g. if there is a `DO` cmd.
        var xobjs = null;
        var skipEmptyXObjs = Object.create(null);
  
        var preprocessor = new EvaluatorPreprocessor(stream, xref, stateManager);
  
        var textState;
  
        function ensureTextContentItem() {
          if (textContentItem.initialized) {
            return textContentItem;
          }
          var font = textState.font;
          if (!(font.loadedName in seenStyles)) {
            seenStyles[font.loadedName] = true;
            textContent.styles[font.loadedName] = {
              fontFamily: font.fallbackName,
              ascent: font.ascent,
              descent: font.descent,
              vertical: font.vertical,
            };
          }
          textContentItem.fontName = font.loadedName;
  
          // 9.4.4 Text Space Details
          var tsm = [textState.fontSize * textState.textHScale, 0,
                     0, textState.fontSize,
                     0, textState.textRise];
  
          if (font.isType3Font &&
              textState.fontMatrix !== FONT_IDENTITY_MATRIX &&
              textState.fontSize === 1) {
            var glyphHeight = font.bbox[3] - font.bbox[1];
            if (glyphHeight > 0) {
              glyphHeight = glyphHeight * textState.fontMatrix[3];
              tsm[3] *= glyphHeight;
            }
          }
  
          var trm = Util.transform(textState.ctm,
                                   Util.transform(textState.textMatrix, tsm));
          textContentItem.transform = trm;
          if (!font.vertical) {
            textContentItem.width = 0;
            textContentItem.height = Math.sqrt(trm[2] * trm[2] + trm[3] * trm[3]);
            textContentItem.vertical = false;
          } else {
            textContentItem.width = Math.sqrt(trm[0] * trm[0] + trm[1] * trm[1]);
            textContentItem.height = 0;
            textContentItem.vertical = true;
          }
  
          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);
          textContentItem.textAdvanceScale = scaleCtmX * scaleLineX;
          textContentItem.lastAdvanceWidth = 0;
          textContentItem.lastAdvanceHeight = 0;
  
          var spaceWidth = font.spaceWidth / 1000 * textState.fontSize;
          if (spaceWidth) {
            textContentItem.spaceWidth = spaceWidth;
            textContentItem.fakeSpaceMin = spaceWidth * SPACE_FACTOR;
            textContentItem.fakeMultiSpaceMin = spaceWidth * MULTI_SPACE_FACTOR;
            textContentItem.fakeMultiSpaceMax =
              spaceWidth * MULTI_SPACE_FACTOR_MAX;
            // It's okay for monospace fonts to fake as much space as needed.
            textContentItem.textRunBreakAllowed = !font.isMonospace;
          } else {
            textContentItem.spaceWidth = 0;
            textContentItem.fakeSpaceMin = Infinity;
            textContentItem.fakeMultiSpaceMin = Infinity;
            textContentItem.fakeMultiSpaceMax = 0;
            textContentItem.textRunBreakAllowed = false;
          }
  
  
          textContentItem.initialized = true;
          return textContentItem;
        }
  
        function replaceWhitespace(str) {
          // Replaces all whitespaces with standard spaces (0x20), to avoid
          // alignment issues between the textLayer and the canvas if the text
          // contains e.g. tabs (fixes issue6612.pdf).
          var i = 0, ii = str.length, code;
          while (i < ii && (code = str.charCodeAt(i)) >= 0x20 && code <= 0x7F) {
            i++;
          }
          return (i < ii ? str.replace(WhitespaceRegexp, ' ') : str);
        }
  
        function runBidiTransform(textChunk) {
          var str = textChunk.str.join('');
          var bidiResult = bidi(str, -1, textChunk.vertical);
          return {
            str: (normalizeWhitespace ? replaceWhitespace(bidiResult.str) :
                                        bidiResult.str),
            dir: bidiResult.dir,
            width: textChunk.width,
            height: textChunk.height,
            transform: textChunk.transform,
            fontName: textChunk.fontName,
          };
        }
  
        function handleSetFont(fontName, fontRef) {
          return self.loadFont(fontName, fontRef, resources).
            then(function (translated) {
              textState.font = translated.font;
              textState.fontMatrix = translated.font.fontMatrix ||
                FONT_IDENTITY_MATRIX;
            });
        }
  
        function buildTextContentItem(chars) {
          var font = textState.font;
          var textChunk = ensureTextContentItem();
          var width = 0;
          var height = 0;
          var glyphs = font.charsToGlyphs(chars);
          for (var i = 0; i < glyphs.length; i++) {
            var glyph = glyphs[i];
            var glyphWidth = null;
            if (font.vertical && glyph.vmetric) {
              glyphWidth = glyph.vmetric[0];
            } else {
              glyphWidth = glyph.width;
            }
  
            var glyphUnicode = glyph.unicode;
            var NormalizedUnicodes = getNormalizedUnicodes();
            if (NormalizedUnicodes[glyphUnicode] !== undefined) {
              glyphUnicode = NormalizedUnicodes[glyphUnicode];
            }
            glyphUnicode = reverseIfRtl(glyphUnicode);
  
            var charSpacing = textState.charSpacing;
            if (glyph.isSpace) {
              var wordSpacing = textState.wordSpacing;
              charSpacing += wordSpacing;
              if (wordSpacing > 0) {
                addFakeSpaces(wordSpacing, textChunk.str);
              }
            }
  
            var tx = 0;
            var ty = 0;
            if (!font.vertical) {
              var w0 = glyphWidth * textState.fontMatrix[0];
              tx = (w0 * textState.fontSize + charSpacing) *
                   textState.textHScale;
              width += tx;
            } else {
              var w1 = glyphWidth * textState.fontMatrix[0];
              ty = w1 * textState.fontSize + charSpacing;
              height += ty;
            }
            textState.translateTextMatrix(tx, ty);
  
            textChunk.str.push(glyphUnicode);
          }
  
          if (!font.vertical) {
            textChunk.lastAdvanceWidth = width;
            textChunk.width += width;
          } else {
            textChunk.lastAdvanceHeight = height;
            textChunk.height += Math.abs(height);
          }
  
          return textChunk;
        }
  
        function addFakeSpaces(width, strBuf) {
          if (width < textContentItem.fakeSpaceMin) {
            return;
          }
          if (width < textContentItem.fakeMultiSpaceMin) {
            strBuf.push(' ');
            return;
          }
          var fakeSpaces = Math.round(width / textContentItem.spaceWidth);
          while (fakeSpaces-- > 0) {
            strBuf.push(' ');
          }
        }
  
        function flushTextContentItem() {
          if (!textContentItem.initialized) {
            return;
          }
  
          // Do final text scaling
          textContentItem.width *= textContentItem.textAdvanceScale;
          textContentItem.height *= textContentItem.textAdvanceScale;
          textContent.items.push(runBidiTransform(textContentItem));
  
          textContentItem.initialized = false;
          textContentItem.str.length = 0;
        }
  
        function enqueueChunk() {
          let length = textContent.items.length;
          if (length > 0) {
            sink.enqueue(textContent, length);
            textContent.items = [];
            textContent.styles = Object.create(null);
          }
        }
  
        var timeSlotManager = new TimeSlotManager();
  
        return new Promise(function promiseBody(resolve, reject) {
          let next = function (promise) {
            enqueueChunk();
            Promise.all([promise, sink.ready]).then(function () {
              try {
                promiseBody(resolve, reject);
              } catch (ex) {
                reject(ex);
              }
            }, reject);
          };
          task.ensureNotTerminated();
          timeSlotManager.reset();
          var stop, operation = {}, args = [];
          while (!(stop = timeSlotManager.check())) {
            // The arguments parsed by read() are not used beyond this loop, so
            // we can reuse the same array on every iteration, thus avoiding
            // unnecessary allocations.
            args.length = 0;
            operation.args = args;
            if (!(preprocessor.read(operation))) {
              break;
            }
            textState = stateManager.state;
            var fn = operation.fn;
            args = operation.args;
            var advance, diff;
  
            switch (fn | 0) {
              case OPS.setFont:
                // Optimization to ignore multiple identical Tf commands.
                var fontNameArg = args[0].name, fontSizeArg = args[1];
                if (textState.font && fontNameArg === textState.fontName &&
                    fontSizeArg === textState.fontSize) {
                  break;
                }
  
                flushTextContentItem();
                textState.fontName = fontNameArg;
                textState.fontSize = fontSizeArg;
                next(handleSetFont(fontNameArg, null));
                return;
              case OPS.setTextRise:
                flushTextContentItem();
                textState.textRise = args[0];
                break;
              case OPS.setHScale:
                flushTextContentItem();
                textState.textHScale = args[0] / 100;
                break;
              case OPS.setLeading:
                flushTextContentItem();
                textState.leading = args[0];
                break;
              case OPS.moveText:
                // Optimization to treat same line movement as advance
                var isSameTextLine = !textState.font ? false :
                  ((textState.font.vertical ? args[0] : args[1]) === 0);
                advance = args[0] - args[1];
                if (combineTextItems &&
                    isSameTextLine && textContentItem.initialized &&
                    advance > 0 &&
                    advance <= textContentItem.fakeMultiSpaceMax) {
                  textState.translateTextLineMatrix(args[0], args[1]);
                  textContentItem.width +=
                    (args[0] - textContentItem.lastAdvanceWidth);
                  textContentItem.height +=
                    (args[1] - textContentItem.lastAdvanceHeight);
                  diff = (args[0] - textContentItem.lastAdvanceWidth) -
                         (args[1] - textContentItem.lastAdvanceHeight);
                  addFakeSpaces(diff, textContentItem.str);
                  break;
                }
  
                flushTextContentItem();
                textState.translateTextLineMatrix(args[0], args[1]);
                textState.textMatrix = textState.textLineMatrix.slice();
                break;
              case OPS.setLeadingMoveText:
                flushTextContentItem();
                textState.leading = -args[1];
                textState.translateTextLineMatrix(args[0], args[1]);
                textState.textMatrix = textState.textLineMatrix.slice();
                break;
              case OPS.nextLine:
                flushTextContentItem();
                textState.carriageReturn();
                break;
              case OPS.setTextMatrix:
                // Optimization to treat same line movement as advance.
                advance = textState.calcTextLineMatrixAdvance(
                  args[0], args[1], args[2], args[3], args[4], args[5]);
                if (combineTextItems &&
                    advance !== null && textContentItem.initialized &&
                    advance.value > 0 &&
                    advance.value <= textContentItem.fakeMultiSpaceMax) {
                  textState.translateTextLineMatrix(advance.width,
                                                    advance.height);
                  textContentItem.width +=
                    (advance.width - textContentItem.lastAdvanceWidth);
                  textContentItem.height +=
                    (advance.height - textContentItem.lastAdvanceHeight);
                  diff = (advance.width - textContentItem.lastAdvanceWidth) -
                         (advance.height - textContentItem.lastAdvanceHeight);
                  addFakeSpaces(diff, textContentItem.str);
                  break;
                }
  
                flushTextContentItem();
                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:
                flushTextContentItem();
                textState.textMatrix = IDENTITY_MATRIX.slice();
                textState.textLineMatrix = IDENTITY_MATRIX.slice();
                break;
              case OPS.showSpacedText:
                var items = args[0];
                var offset;
                for (var j = 0, jj = items.length; j < jj; j++) {
                  if (typeof items[j] === 'string') {
                    buildTextContentItem(items[j]);
                  } else if (isNum(items[j])) {
                    ensureTextContentItem();
  
                    // PDF Specification 5.3.2 states:
                    // The number is expressed in thousandths of a unit of text
                    // space.
                    // This amount is subtracted from the current horizontal or
                    // vertical coordinate, depending on the writing mode.
                    // In the default coordinate system, a positive adjustment
                    // has the effect of moving the next glyph painted either to
                    // the left or down by the given amount.
                    advance = items[j] * textState.fontSize / 1000;
                    var breakTextRun = false;
                    if (textState.font.vertical) {
                      offset = advance;
                      textState.translateTextMatrix(0, offset);
                      breakTextRun = textContentItem.textRunBreakAllowed &&
                                     advance > textContentItem.fakeMultiSpaceMax;
                      if (!breakTextRun) {
                        // Value needs to be added to height to paint down.
                        textContentItem.height += offset;
                      }
                    } else {
                      advance = -advance;
                      offset = advance * textState.textHScale;
                      textState.translateTextMatrix(offset, 0);
                      breakTextRun = textContentItem.textRunBreakAllowed &&
                                     advance > textContentItem.fakeMultiSpaceMax;
                      if (!breakTextRun) {
                        // Value needs to be subtracted from width to paint left.
                        textContentItem.width += offset;
                      }
                    }
                    if (breakTextRun) {
                      flushTextContentItem();
                    } else if (advance > 0) {
                      addFakeSpaces(advance, textContentItem.str);
                    }
                  }
                }
                break;
              case OPS.showText:
                buildTextContentItem(args[0]);
                break;
              case OPS.nextLineShowText:
                flushTextContentItem();
                textState.carriageReturn();
                buildTextContentItem(args[0]);
                break;
              case OPS.nextLineSetSpacingShowText:
                flushTextContentItem();
                textState.wordSpacing = args[0];
                textState.charSpacing = args[1];
                textState.carriageReturn();
                buildTextContentItem(args[2]);
                break;
              case OPS.paintXObject:
                flushTextContentItem();
                if (!xobjs) {
                  xobjs = (resources.get('XObject') || Dict.empty);
                }
  
                var name = args[0].name;
                if (name && skipEmptyXObjs[name] !== undefined) {
                  break;
                }
  
                next(new Promise(function(resolveXObject, rejectXObject) {
                  if (!name) {
                    throw new FormatError('XObject must be referred to by name.');
                  }
  
                  let xobj = xobjs.get(name);
                  if (!xobj) {
                    resolveXObject();
                    return;
                  }
                  if (!isStream(xobj)) {
                    throw new FormatError('XObject should be a stream');
                  }
  
                  let type = xobj.dict.get('Subtype');
                  if (!isName(type)) {
                    throw new FormatError('XObject should have a Name subtype');
                  }
  
                  if (type.name !== 'Form') {
                    skipEmptyXObjs[name] = true;
                    resolveXObject();
                    return;
                  }
  
                  // Use a new `StateManager` to prevent incorrect positioning of
                  // textItems *after* the Form XObject, since errors in the data
                  // can otherwise prevent `restore` operators from executing.
                  // NOTE: Only an issue when `options.ignoreErrors === true`.
                  let currentState = stateManager.state.clone();
                  let xObjStateManager = new StateManager(currentState);
  
                  let matrix = xobj.dict.getArray('Matrix');
                  if (Array.isArray(matrix) && matrix.length === 6) {
                    xObjStateManager.transform(matrix);
                  }
  
                  // Enqueue the `textContent` chunk before parsing the /Form
                  // XObject.
                  enqueueChunk();
                  let sinkWrapper = {
                    enqueueInvoked: false,
  
                    enqueue(chunk, size) {
                      this.enqueueInvoked = true;
                      sink.enqueue(chunk, size);
                    },
  
                    get desiredSize() {
                      return sink.desiredSize;
                    },
  
                    get ready() {
                      return sink.ready;
                    },
                  };
  
                  self.getTextContent({
                    stream: xobj,
                    task,
                    resources: xobj.dict.get('Resources') || resources,
                    stateManager: xObjStateManager,
                    normalizeWhitespace,
                    combineTextItems,
                    sink: sinkWrapper,
                    seenStyles,
                  }).then(function() {
                    if (!sinkWrapper.enqueueInvoked) {
                      skipEmptyXObjs[name] = true;
                    }
                    resolveXObject();
                  }, rejectXObject);
                }).catch(function(reason) {
                  if (reason instanceof AbortException) {
                    return;
                  }
                  if (self.options.ignoreErrors) {
                    // Error(s) in the XObject -- allow text-extraction to
                    // continue.
                    warn(`getTextContent - ignoring XObject: "${reason}".`);
                    return;
                  }
                  throw reason;
                }));
                return;
              case OPS.setGState:
                flushTextContentItem();
                var dictName = args[0];
                var extGState = resources.get('ExtGState');
  
                if (!isDict(extGState) || !isName(dictName)) {
                  break;
                }
                var gState = extGState.get(dictName.name);
                if (!isDict(gState)) {
                  break;
                }
                var gStateFont = gState.get('Font');
                if (gStateFont) {
                  textState.fontName = null;
                  textState.fontSize = gStateFont[1];
                  next(handleSetFont(null, gStateFont[0]));
                  return;
                }
                break;
            } // switch
            if (textContent.items.length >= sink.desiredSize) {
              // Wait for ready, if we reach highWaterMark.
              stop = true;
              break;
            }
          } // while
          if (stop) {
            next(deferred);
            return;
          }
          flushTextContentItem();
          enqueueChunk();
          resolve();
        }).catch((reason) => {
          if (reason instanceof AbortException) {
            return;
          }
          if (this.options.ignoreErrors) {
            // Error(s) in the TextContent -- allow text-extraction to continue.
            warn('getTextContent - ignoring errors during task: ' + task.name);
  
            flushTextContentItem();
            enqueueChunk();
            return;
          }
          throw reason;
        });
      },
  
      extractDataStructures:
          function PartialEvaluator_extractDataStructures(dict, baseDict,
                                                          properties) {
        var xref = this.xref;
        // 9.10.2
        var toUnicode = (dict.get('ToUnicode') || baseDict.get('ToUnicode'));
        var toUnicodePromise = toUnicode ?
          this.readToUnicode(toUnicode) : Promise.resolve(undefined);
  
        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 = xref.fetchIfRef(diffEncoding[j]);
                if (isNum(data)) {
                  index = data;
                } else if (isName(data)) {
                  differences[index++] = data.name;
                } else {
                  throw new FormatError(
                    `Invalid entry in 'Differences' array: ${data}`);
                }
              }
            }
          } else if (isName(encoding)) {
            baseEncodingName = encoding.name;
          } else {
            throw new FormatError('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 = getEncoding(baseEncodingName).slice();
        } else {
          var isSymbolicFont = !!(properties.flags & FontFlags.Symbolic);
          var isNonsymbolicFont = !!(properties.flags & FontFlags.Nonsymbolic);
          // According to "Table 114" in section "9.6.6.1 General" (under
          // "9.6.6 Character Encoding") of the PDF specification, a Nonsymbolic
          // font should use the `StandardEncoding` if no encoding is specified.
          encoding = StandardEncoding;
          if (properties.type === 'TrueType' && !isNonsymbolicFont) {
            encoding = WinAnsiEncoding;
          }
          // The Symbolic attribute can be misused for regular fonts
          // Heuristic: we have to check if the font is a standard one also
          if (isSymbolicFont) {
            encoding = MacRomanEncoding;
            if (!properties.file) {
              if (/Symbol/i.test(properties.name)) {
                encoding = SymbolSetEncoding;
              } else if (/Dingbats/i.test(properties.name)) {
                encoding = ZapfDingbatsEncoding;
              }
            }
          }
          properties.defaultEncoding = encoding;
        }
  
        properties.differences = differences;
        properties.baseEncodingName = baseEncodingName;
        properties.hasEncoding = !!baseEncodingName || differences.length > 0;
        properties.dict = dict;
        return toUnicodePromise.then((toUnicode) => {
          properties.toUnicode = toUnicode;
          return this.buildToUnicode(properties);
        }).then(function (toUnicode) {
          properties.toUnicode = toUnicode;
          return properties;
        });
      },
  
      /**
       * Builds a char code to unicode map based on section 9.10 of the spec.
       * @param {Object} properties Font properties object.
       * @return {Promise} A Promise that is resolved with a
       *   {ToUnicodeMap|IdentityToUnicodeMap} object.
       */
      buildToUnicode: function PartialEvaluator_buildToUnicode(properties) {
        properties.hasIncludedToUnicodeMap =
          !!properties.toUnicode && properties.toUnicode.length > 0;
        // Section 9.10.2 Mapping Character Codes to Unicode Values
        if (properties.hasIncludedToUnicodeMap) {
          return Promise.resolve(properties.toUnicode);
        }
        // According to the spec if the font is a simple font we should only map
        // to unicode if the base encoding is MacRoman, MacExpert, or WinAnsi or
        // the differences array only contains adobe standard or symbol set names,
        // in pratice it seems better to always try to create a toUnicode
        // map based of the default encoding.
        var toUnicode, charcode, glyphName;
        if (!properties.composite /* is simple font */) {
          toUnicode = [];
          var encoding = properties.defaultEncoding.slice();
          var baseEncodingName = properties.baseEncodingName;
          // Merge in the differences array.
          var differences = properties.differences;
          for (charcode in differences) {
            glyphName = differences[charcode];
            if (glyphName === '.notdef') {
              // Skip .notdef to prevent rendering errors, e.g. boxes appearing
              // where there should be spaces (fixes issue5256.pdf).
              continue;
            }
            encoding[charcode] = glyphName;
          }
          var glyphsUnicodeMap = getGlyphsUnicode();
          for (charcode in encoding) {
            // a) Map the character code to a character name.
            glyphName = encoding[charcode];
            // b) Look up the character name in the Adobe Glyph List (see the
            //    Bibliography) to obtain the corresponding Unicode value.
            if (glyphName === '') {
              continue;
            } else if (glyphsUnicodeMap[glyphName] === undefined) {
              // (undocumented) c) Few heuristics to recognize unknown glyphs
              // NOTE: Adobe Reader does not do this step, but OSX Preview does
              var code = 0;
              switch (glyphName[0]) {
                case 'G': // Gxx glyph
                  if (glyphName.length === 3) {
                    code = parseInt(glyphName.substr(1), 16);
                  }
                  break;
                case 'g': // g00xx glyph
                  if (glyphName.length === 5) {
                    code = parseInt(glyphName.substr(1), 16);
                  }
                  break;
                case 'C': // Cddd glyph
                case 'c': // cddd glyph
                  if (glyphName.length >= 3) {
                    code = +glyphName.substr(1);
                  }
                  break;
                default:
                  // 'uniXXXX'/'uXXXX{XX}' glyphs
                  var unicode = getUnicodeForGlyph(glyphName, glyphsUnicodeMap);
                  if (unicode !== -1) {
                    code = unicode;
                  }
              }
              if (code) {
                // If |baseEncodingName| is one the predefined encodings,
                // and |code| equals |charcode|, using the glyph defined in the
                // baseEncoding seems to yield a better |toUnicode| mapping
                // (fixes issue 5070).
                if (baseEncodingName && code === +charcode) {
                  var baseEncoding = getEncoding(baseEncodingName);
                  if (baseEncoding && (glyphName = baseEncoding[charcode])) {
                    toUnicode[charcode] =
                      String.fromCharCode(glyphsUnicodeMap[glyphName]);
                    continue;
                  }
                }
                toUnicode[charcode] = String.fromCharCode(code);
              }
              continue;
            }
            toUnicode[charcode] =
              String.fromCharCode(glyphsUnicodeMap[glyphName]);
          }
          return Promise.resolve(new ToUnicodeMap(toUnicode));
        }
        // If the font is a composite font that uses one of the predefined CMaps
        // listed in Table 118 (except Identity–H and Identity–V) or whose
        // descendant CIDFont uses the Adobe-GB1, Adobe-CNS1, Adobe-Japan1, or
        // Adobe-Korea1 character collection:
        if (properties.composite && (
             (properties.cMap.builtInCMap &&
              !(properties.cMap instanceof IdentityCMap)) ||
             (properties.cidSystemInfo.registry === 'Adobe' &&
               (properties.cidSystemInfo.ordering === 'GB1' ||
                properties.cidSystemInfo.ordering === 'CNS1' ||
                properties.cidSystemInfo.ordering === 'Japan1' ||
                properties.cidSystemInfo.ordering === 'Korea1')))) {
          // Then:
          // a) Map the character code to a character identifier (CID) according
          // to the font’s CMap.
          // b) Obtain the registry and ordering of the character collection used
          // by the font’s CMap (for example, Adobe and Japan1) from its
          // CIDSystemInfo dictionary.
          var registry = properties.cidSystemInfo.registry;
          var ordering = properties.cidSystemInfo.ordering;
          // c) Construct a second CMap name by concatenating the registry and
          // ordering obtained in step (b) in the format registry–ordering–UCS2
          // (for example, Adobe–Japan1–UCS2).
          var ucs2CMapName = Name.get(registry + '-' + ordering + '-UCS2');
          // d) Obtain the CMap with the name constructed in step (c) (available
          // from the ASN Web site; see the Bibliography).
          return CMapFactory.create({
            encoding: ucs2CMapName,
            fetchBuiltInCMap: this.fetchBuiltInCMap,
            useCMap: null,
          }).then(function (ucs2CMap) {
            var cMap = properties.cMap;
            toUnicode = [];
            cMap.forEach(function(charcode, cid) {
              if (cid > 0xffff) {
                throw new FormatError('Max size of CID is 65,535');
              }
              // e) Map the CID obtained in step (a) according to the CMap
              // obtained in step (d), producing a Unicode value.
              var ucs2 = ucs2CMap.lookup(cid);
              if (ucs2) {
                toUnicode[charcode] =
                  String.fromCharCode((ucs2.charCodeAt(0) << 8) +
                                      ucs2.charCodeAt(1));
              }
            });
            return new ToUnicodeMap(toUnicode);
          });
        }
  
        // The viewer's choice, just use an identity map.
        return Promise.resolve(new IdentityToUnicodeMap(properties.firstChar,
                                                        properties.lastChar));
      },
  
      readToUnicode: function PartialEvaluator_readToUnicode(toUnicode) {
        var cmapObj = toUnicode;
        if (isName(cmapObj)) {
          return CMapFactory.create({
            encoding: cmapObj,
            fetchBuiltInCMap: this.fetchBuiltInCMap,
            useCMap: null,
          }).then(function (cmap) {
            if (cmap instanceof IdentityCMap) {
              return new IdentityToUnicodeMap(0, 0xFFFF);
            }
            return new ToUnicodeMap(cmap.getMap());
          });
        } else if (isStream(cmapObj)) {
          return CMapFactory.create({
            encoding: cmapObj,
            fetchBuiltInCMap: this.fetchBuiltInCMap,
            useCMap: null,
          }).then(function (cmap) {
            if (cmap instanceof IdentityCMap) {
              return new IdentityToUnicodeMap(0, 0xFFFF);
            }
            var map = new Array(cmap.length);
            // Convert UTF-16BE
            // NOTE: cmap can be a sparse array, so use forEach instead of for(;;)
            // to iterate over all keys.
            cmap.forEach(function(charCode, token) {
              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);
              }
              map[charCode] = String.fromCharCode.apply(String, str);
            });
            return new ToUnicodeMap(map);
          });
        }
        return Promise.resolve(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, descriptor,
                                                             properties) {
        var xref = this.xref;
        var glyphsWidths = [];
        var defaultWidth = 0;
        var glyphsVMetrics = [];
        var defaultVMetrics;
        var i, ii, j, jj, start, code, widths;
        if (properties.composite) {
          defaultWidth = dict.has('DW') ? dict.get('DW') : 1000;
  
          widths = dict.get('W');
          if (widths) {
            for (i = 0, ii = widths.length; i < ii; i++) {
              start = xref.fetchIfRef(widths[i++]);
              code = xref.fetchIfRef(widths[i]);
              if (Array.isArray(code)) {
                for (j = 0, jj = code.length; j < jj; j++) {
                  glyphsWidths[start++] = xref.fetchIfRef(code[j]);
                }
              } else {
                var width = xref.fetchIfRef(widths[++i]);
                for (j = start; j <= code; j++) {
                  glyphsWidths[j] = width;
                }
              }
            }
          }
  
          if (properties.vertical) {
            var vmetrics = dict.getArray('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 = xref.fetchIfRef(vmetrics[i++]);
                code = xref.fetchIfRef(vmetrics[i]);
                if (Array.isArray(code)) {
                  for (j = 0, jj = code.length; j < jj; j++) {
                    glyphsVMetrics[start++] = [
                      xref.fetchIfRef(code[j++]),
                      xref.fetchIfRef(code[j++]),
                      xref.fetchIfRef(code[j])
                    ];
                  }
                } else {
                  var vmetric = [
                    xref.fetchIfRef(vmetrics[++i]),
                    xref.fetchIfRef(vmetrics[++i]),
                    xref.fetchIfRef(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++] = xref.fetchIfRef(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 getSerifFonts()) ||
                (fontNameWoStyle.search(/serif/gi) !== -1);
      },
  
      getBaseFontMetrics: function PartialEvaluator_getBaseFontMetrics(name) {
        var defaultWidth = 0;
        var widths = [];
        var monospace = false;
        var stdFontMap = getStdFontMap();
        var lookupName = (stdFontMap[name] || name);
        var Metrics = getMetrics();
  
        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(); // expand lazy widths array
        }
  
        return {
          defaultWidth,
          monospace,
          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) {
        var baseDict = dict;
        var type = dict.get('Subtype');
        if (!isName(type)) {
          throw new FormatError('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) {
            throw new FormatError('Descendant fonts are not specified');
          }
          dict = (Array.isArray(df) ? this.xref.fetchIfRef(df[0]) : df);
  
          type = dict.get('Subtype');
          if (!isName(type)) {
            throw new FormatError('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.toString());
          } else if (isDict(encoding)) {
            var keys = encoding.getKeys();
            for (var i = 0, ii = keys.length; i < ii; i++) {
              var entry = encoding.getRaw(keys[i]);
              if (isName(entry)) {
                hash.update(entry.name);
              } else if (isRef(entry)) {
                hash.update(entry.toString());
              } else if (Array.isArray(entry)) {
                // 'Differences' array (fixes bug1157493.pdf).
                var diffLength = entry.length, diffBuf = new Array(diffLength);
  
                for (var j = 0; j < diffLength; j++) {
                  var diffEntry = entry[j];
                  if (isName(diffEntry)) {
                    diffBuf[j] = diffEntry.name;
                  } else if (isNum(diffEntry) || isRef(diffEntry)) {
                    diffBuf[j] = diffEntry.toString();
                  }
                }
                hash.update(diffBuf.join());
              }
            }
          }
  
          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,
          dict,
          baseDict,
          composite,
          type: type.name,
          hash: hash ? hash.hexdigest() : '',
        };
      },
  
      translateFont: function PartialEvaluator_translateFont(preEvaluatedFont) {
        var baseDict = preEvaluatedFont.baseDict;
        var dict = preEvaluatedFont.dict;
        var composite = preEvaluatedFont.composite;
        var descriptor = preEvaluatedFont.descriptor;
        var type = preEvaluatedFont.type;
        var maxCharIndex = (composite ? 0xFFFF : 0xFF);
        var properties;
  
        if (!descriptor) {
          if (type === '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));
            descriptor.set('FontBBox', dict.getArray('FontBBox'));
          } 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)) {
              throw new FormatError('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) |
              (getSymbolsFonts()[fontNameWoStyle] ? FontFlags.Symbolic :
                                                    FontFlags.Nonsymbolic);
  
            properties = {
              type,
              name: baseFontName,
              widths: metrics.widths,
              defaultWidth: metrics.defaultWidth,
              flags,
              firstChar: 0,
              lastChar: maxCharIndex,
            };
            return this.extractDataStructures(dict, dict, properties).
              then((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 standard 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 !== '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.indexOf(fontNameStr) === 0) {
              fontName = baseFont;
            }
          }
        }
        fontName = (fontName || baseFont);
  
        if (!isName(fontName)) {
          throw new FormatError('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');
            var length3 = fontFile.dict.get('Length3');
          }
        }
  
        properties = {
          type,
          name: fontName.name,
          subtype,
          file: fontFile,
          length1,
          length2,
          length3,
          loadedName: baseDict.loadedName,
          composite,
          wideChars: composite,
          fixedPitch: false,
          fontMatrix: (dict.getArray('FontMatrix') || FONT_IDENTITY_MATRIX),
          firstChar: firstChar || 0,
          lastChar: (lastChar || maxCharIndex),
          bbox: descriptor.getArray('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'),
          isType3Font: false,
        };
  
        var cMapPromise;
        if (composite) {
          var cidEncoding = baseDict.get('Encoding');
          if (isName(cidEncoding)) {
            properties.cidEncoding = cidEncoding.name;
          }
          cMapPromise = CMapFactory.create({
            encoding: cidEncoding,
            fetchBuiltInCMap: this.fetchBuiltInCMap,
            useCMap: null,
          }).then(function (cMap) {
            properties.cMap = cMap;
            properties.vertical = properties.cMap.vertical;
          });
        } else {
          cMapPromise = Promise.resolve(undefined);
        }
  
        return cMapPromise.then(() => {
          return this.extractDataStructures(dict, baseDict, properties);
        }).then((properties) => {
          this.extractWidths(dict, descriptor, properties);
  
          if (type === 'Type3') {
            properties.isType3Font = true;
          }
          return new Font(fontName.name, fontFile, properties);
        });
      },
    };
  
    return PartialEvaluator;
  })();
  
  var TranslatedFont = (function TranslatedFontClosure() {
    function TranslatedFont(loadedName, font, dict) {
      this.loadedName = loadedName;
      this.font = font;
      this.dict = dict;
      this.type3Loaded = null;
      this.sent = false;
    }
    TranslatedFont.prototype = {
      send(handler) {
        if (this.sent) {
          return;
        }
        var fontData = this.font.exportData();
        handler.send('commonobj', [
          this.loadedName,
          'Font',
          fontData
        ]);
        this.sent = true;
      },
      loadType3Data(evaluator, resources, parentOperatorList, task) {
        if (!this.font.isType3Font) {
          throw new Error('Must be a Type3 font.');
        }
  
        if (this.type3Loaded) {
          return this.type3Loaded;
        }
        // When parsing Type3 glyphs, always ignore them if there are errors.
        // Compared to the parsing of e.g. an entire page, it doesn't really
        // make sense to only be able to render a Type3 glyph partially.
        var type3Options = Object.create(evaluator.options);
        type3Options.ignoreErrors = false;
        var type3Evaluator = evaluator.clone(type3Options);
  
        var translatedFont = this.font;
        var loadCharProcsPromise = Promise.resolve();
        var charProcs = this.dict.get('CharProcs');
        var fontResources = this.dict.get('Resources') || resources;
        var charProcKeys = charProcs.getKeys();
        var charProcOperatorList = Object.create(null);
  
        for (var i = 0, n = charProcKeys.length; i < n; ++i) {
          let key = charProcKeys[i];
          loadCharProcsPromise = loadCharProcsPromise.then(function () {
            var glyphStream = charProcs.get(key);
            var operatorList = new OperatorList();
            return type3Evaluator.getOperatorList({
              stream: glyphStream,
              task,
              resources: fontResources,
              operatorList,
            }).then(function () {
              charProcOperatorList[key] = operatorList.getIR();
  
              // Add the dependencies to the parent operator list so they are
              // resolved before sub operator list is executed synchronously.
              parentOperatorList.addDependencies(operatorList.dependencies);
            }).catch(function(reason) {
              warn(`Type3 font resource "${key}" is not available.`);
              var operatorList = new OperatorList();
              charProcOperatorList[key] = operatorList.getIR();
            });
          });
        }
        this.type3Loaded = loadCharProcsPromise.then(function () {
          translatedFont.charProcOperatorList = charProcOperatorList;
        });
        return this.type3Loaded;
      },
    };
    return TranslatedFont;
  })();
  
  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 = Object.create(null);
      this._totalLength = 0;
      this.pageIndex = pageIndex;
      this.intent = intent;
    }
  
    OperatorList.prototype = {
      get length() {
        return this.argsArray.length;
      },
  
      /**
       * @returns {number} The total length of the entire operator list,
       *                   since `this.length === 0` after flushing.
       */
      get totalLength() {
        return (this._totalLength + this.length);
      },
  
      addOp(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(dependency) {
        if (dependency in this.dependencies) {
          return;
        }
        this.dependencies[dependency] = true;
        this.addOp(OPS.dependency, [dependency]);
      },
  
      addDependencies(dependencies) {
        for (var key in dependencies) {
          this.addDependency(key);
        }
      },
  
      addOpList(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() {
        return {
          fnArray: this.fnArray,
          argsArray: this.argsArray,
          length: this.length,
        };
      },
  
      flush(lastChunk) {
        if (this.intent !== 'oplist') {
          new QueueOptimizer().optimize(this);
        }
        var transfers = getTransfers(this);
        var length = this.length;
        this._totalLength += length;
  
        this.messageHandler.send('RenderPageChunk', {
          operatorList: {
            fnArray: this.fnArray,
            argsArray: this.argsArray,
            lastChunk,
            length,
          },
          pageIndex: this.pageIndex,
          intent: this.intent,
        }, transfers);
        this.dependencies = Object.create(null);
        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() {
        var old = this.state;
        this.stateStack.push(this.state);
        this.state = old.clone();
      },
      restore() {
        var prev = this.stateStack.pop();
        if (prev) {
          this.state = prev;
        }
      },
      transform(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.fontName = null;
      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];
      },
      calcTextLineMatrixAdvance:
          function TextState_calcTextLineMatrixAdvance(a, b, c, d, e, f) {
        var font = this.font;
        if (!font) {
          return null;
        }
        var m = this.textLineMatrix;
        if (!(a === m[0] && b === m[1] && c === m[2] && d === m[3])) {
          return null;
        }
        var txDiff = e - m[4], tyDiff = f - m[5];
        if ((font.vertical && txDiff !== 0) || (!font.vertical && tyDiff !== 0)) {
          return null;
        }
        var tx, ty, denominator = a * d - b * c;
        if (font.vertical) {
          tx = -tyDiff * c / denominator;
          ty = tyDiff * a / denominator;
        } else {
          tx = txDiff * d / denominator;
          ty = -txDiff * b / denominator;
        }
        return { width: tx, height: ty, value: (font.vertical ? ty : tx), };
      },
      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;
      this.fillColorSpace = ColorSpace.singletons.gray;
      this.strokeColorSpace = ColorSpace.singletons.gray;
    }
    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 getOPMap = getLookupTableFactory(function (t) {
      // Graphic state
      t['w'] = { id: OPS.setLineWidth, numArgs: 1, variableArgs: false, };
      t['J'] = { id: OPS.setLineCap, numArgs: 1, variableArgs: false, };
      t['j'] = { id: OPS.setLineJoin, numArgs: 1, variableArgs: false, };
      t['M'] = { id: OPS.setMiterLimit, numArgs: 1, variableArgs: false, };
      t['d'] = { id: OPS.setDash, numArgs: 2, variableArgs: false, };
      t['ri'] = { id: OPS.setRenderingIntent, numArgs: 1, variableArgs: false, };
      t['i'] = { id: OPS.setFlatness, numArgs: 1, variableArgs: false, };
      t['gs'] = { id: OPS.setGState, numArgs: 1, variableArgs: false, };
      t['q'] = { id: OPS.save, numArgs: 0, variableArgs: false, };
      t['Q'] = { id: OPS.restore, numArgs: 0, variableArgs: false, };
      t['cm'] = { id: OPS.transform, numArgs: 6, variableArgs: false, };
  
      // Path
      t['m'] = { id: OPS.moveTo, numArgs: 2, variableArgs: false, };
      t['l'] = { id: OPS.lineTo, numArgs: 2, variableArgs: false, };
      t['c'] = { id: OPS.curveTo, numArgs: 6, variableArgs: false, };
      t['v'] = { id: OPS.curveTo2, numArgs: 4, variableArgs: false, };
      t['y'] = { id: OPS.curveTo3, numArgs: 4, variableArgs: false, };
      t['h'] = { id: OPS.closePath, numArgs: 0, variableArgs: false, };
      t['re'] = { id: OPS.rectangle, numArgs: 4, variableArgs: false, };
      t['S'] = { id: OPS.stroke, numArgs: 0, variableArgs: false, };
      t['s'] = { id: OPS.closeStroke, numArgs: 0, variableArgs: false, };
      t['f'] = { id: OPS.fill, numArgs: 0, variableArgs: false, };
      t['F'] = { id: OPS.fill, numArgs: 0, variableArgs: false, };
      t['f*'] = { id: OPS.eoFill, numArgs: 0, variableArgs: false, };
      t['B'] = { id: OPS.fillStroke, numArgs: 0, variableArgs: false, };
      t['B*'] = { id: OPS.eoFillStroke, numArgs: 0, variableArgs: false, };
      t['b'] = { id: OPS.closeFillStroke, numArgs: 0, variableArgs: false, };
      t['b*'] = { id: OPS.closeEOFillStroke, numArgs: 0, variableArgs: false, };
      t['n'] = { id: OPS.endPath, numArgs: 0, variableArgs: false, };
  
      // Clipping
      t['W'] = { id: OPS.clip, numArgs: 0, variableArgs: false, };
      t['W*'] = { id: OPS.eoClip, numArgs: 0, variableArgs: false, };
  
      // Text
      t['BT'] = { id: OPS.beginText, numArgs: 0, variableArgs: false, };
      t['ET'] = { id: OPS.endText, numArgs: 0, variableArgs: false, };
      t['Tc'] = { id: OPS.setCharSpacing, numArgs: 1, variableArgs: false, };
      t['Tw'] = { id: OPS.setWordSpacing, numArgs: 1, variableArgs: false, };
      t['Tz'] = { id: OPS.setHScale, numArgs: 1, variableArgs: false, };
      t['TL'] = { id: OPS.setLeading, numArgs: 1, variableArgs: false, };
      t['Tf'] = { id: OPS.setFont, numArgs: 2, variableArgs: false, };
      t['Tr'] = { id: OPS.setTextRenderingMode, numArgs: 1,
                  variableArgs: false, };
      t['Ts'] = { id: OPS.setTextRise, numArgs: 1, variableArgs: false, };
      t['Td'] = { id: OPS.moveText, numArgs: 2, variableArgs: false, };
      t['TD'] = { id: OPS.setLeadingMoveText, numArgs: 2, variableArgs: false, };
      t['Tm'] = { id: OPS.setTextMatrix, numArgs: 6, variableArgs: false, };
      t['T*'] = { id: OPS.nextLine, numArgs: 0, variableArgs: false, };
      t['Tj'] = { id: OPS.showText, numArgs: 1, variableArgs: false, };
      t['TJ'] = { id: OPS.showSpacedText, numArgs: 1, variableArgs: false, };
      t['\''] = { id: OPS.nextLineShowText, numArgs: 1, variableArgs: false, };
      t['"'] = { id: OPS.nextLineSetSpacingShowText, numArgs: 3,
                 variableArgs: false, };
  
      // Type3 fonts
      t['d0'] = { id: OPS.setCharWidth, numArgs: 2, variableArgs: false, };
      t['d1'] = { id: OPS.setCharWidthAndBounds, numArgs: 6,
                  variableArgs: false, };
  
      // Color
      t['CS'] = { id: OPS.setStrokeColorSpace, numArgs: 1, variableArgs: false, };
      t['cs'] = { id: OPS.setFillColorSpace, numArgs: 1, variableArgs: false, };
      t['SC'] = { id: OPS.setStrokeColor, numArgs: 4, variableArgs: true, };
      t['SCN'] = { id: OPS.setStrokeColorN, numArgs: 33, variableArgs: true, };
      t['sc'] = { id: OPS.setFillColor, numArgs: 4, variableArgs: true, };
      t['scn'] = { id: OPS.setFillColorN, numArgs: 33, variableArgs: true, };
      t['G'] = { id: OPS.setStrokeGray, numArgs: 1, variableArgs: false, };
      t['g'] = { id: OPS.setFillGray, numArgs: 1, variableArgs: false, };
      t['RG'] = { id: OPS.setStrokeRGBColor, numArgs: 3, variableArgs: false, };
      t['rg'] = { id: OPS.setFillRGBColor, numArgs: 3, variableArgs: false, };
      t['K'] = { id: OPS.setStrokeCMYKColor, numArgs: 4, variableArgs: false, };
      t['k'] = { id: OPS.setFillCMYKColor, numArgs: 4, variableArgs: false, };
  
      // Shading
      t['sh'] = { id: OPS.shadingFill, numArgs: 1, variableArgs: false, };
  
      // Images
      t['BI'] = { id: OPS.beginInlineImage, numArgs: 0, variableArgs: false, };
      t['ID'] = { id: OPS.beginImageData, numArgs: 0, variableArgs: false, };
      t['EI'] = { id: OPS.endInlineImage, numArgs: 1, variableArgs: false, };
  
      // XObjects
      t['Do'] = { id: OPS.paintXObject, numArgs: 1, variableArgs: false, };
      t['MP'] = { id: OPS.markPoint, numArgs: 1, variableArgs: false, };
      t['DP'] = { id: OPS.markPointProps, numArgs: 2, variableArgs: false, };
      t['BMC'] = { id: OPS.beginMarkedContent, numArgs: 1, variableArgs: false, };
      t['BDC'] = { id: OPS.beginMarkedContentProps, numArgs: 2,
                   variableArgs: false, };
      t['EMC'] = { id: OPS.endMarkedContent, numArgs: 0, variableArgs: false, };
  
      // Compatibility
      t['BX'] = { id: OPS.beginCompat, numArgs: 0, variableArgs: false, };
      t['EX'] = { id: OPS.endCompat, numArgs: 0, variableArgs: false, };
  
      // (reserved partial commands for the lexer)
      t['BM'] = null;
      t['BD'] = null;
      t['true'] = null;
      t['fa'] = null;
      t['fal'] = null;
      t['fals'] = null;
      t['false'] = null;
      t['nu'] = null;
      t['nul'] = null;
      t['null'] = null;
    });
  
    function EvaluatorPreprocessor(stream, xref, stateManager) {
      this.opMap = getOPMap();
      // TODO(mduan): pass array of knownCommands rather than this.opMap
      // dictionary
      this.parser = new Parser(new Lexer(stream, this.opMap), false, xref);
      this.stateManager = stateManager;
      this.nonProcessedArgs = [];
    }
  
    EvaluatorPreprocessor.prototype = {
      get savedStatesDepth() {
        return this.stateManager.stateStack.length;
      },
  
      // |operation| is an object with two fields:
      //
      // - |fn| is an out param.
      //
      // - |args| is an inout param. On entry, it should have one of two values.
      //
      //   - An empty array. This indicates that the caller is providing the
      //     array in which the args will be stored in. The caller should use
      //     this value if it can reuse a single array for each call to read().
      //
      //   - |null|. This indicates that the caller needs this function to create
      //     the array in which any args are stored in. If there are zero args,
      //     this function will leave |operation.args| as |null| (thus avoiding
      //     allocations that would occur if we used an empty array to represent
      //     zero arguments). Otherwise, it will replace |null| with a new array
      //     containing the arguments. The caller should use this value if it
      //     cannot reuse an array for each call to read().
      //
      // These two modes are present because this function is very hot and so
      // avoiding allocations where possible is worthwhile.
      //
      read: function EvaluatorPreprocessor_read(operation) {
        var args = operation.args;
        while (true) {
          var obj = this.parser.getObj();
          if (isCmd(obj)) {
            var cmd = obj.cmd;
            // Check that the command is valid
            var opSpec = this.opMap[cmd];
            if (!opSpec) {
              warn('Unknown command "' + cmd + '"');
              continue;
            }
  
            var fn = opSpec.id;
            var numArgs = opSpec.numArgs;
            var argsLength = args !== null ? args.length : 0;
  
            if (!opSpec.variableArgs) {
              // Postscript commands can be nested, e.g. /F2 /GS2 gs 5.711 Tf
              if (argsLength !== numArgs) {
                var nonProcessedArgs = this.nonProcessedArgs;
                while (argsLength > numArgs) {
                  nonProcessedArgs.push(args.shift());
                  argsLength--;
                }
                while (argsLength < numArgs && nonProcessedArgs.length !== 0) {
                  if (args === null) {
                    args = [];
                  }
                  args.unshift(nonProcessedArgs.pop());
                  argsLength++;
                }
              }
  
              if (argsLength < numArgs) {
                // If we receive too few arguments, it's not possible to execute
                // the command, hence we skip the command.
                warn('Skipping command ' + fn + ': expected ' + numArgs +
                     ' args, but received ' + argsLength + ' args.');
                if (args !== null) {
                  args.length = 0;
                }
                continue;
              }
            } else if (argsLength > numArgs) {
              info('Command ' + fn + ': expected [0,' + numArgs +
                   '] args, but received ' + argsLength + ' args.');
            }
  
            // TODO figure out how to type-check vararg functions
            this.preprocessCommand(fn, args);
  
            operation.fn = fn;
            operation.args = args;
            return true;
          }
          if (isEOF(obj)) {
            return false; // no more commands
          }
          // argument
          if (obj !== null) {
            if (args === null) {
              args = [];
            }
            args.push(obj);
            if (args.length > 33) {
              throw new FormatError('Too many arguments');
            }
          }
        }
      },
  
      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(iFirstSave, 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 iFirstSave.
      var iFirstPIMXO = iFirstSave + 2;
      for (var i = 0; i < count; i++) {
        var arg = argsArray[iFirstPIMXO + 4 * i];
        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[iFirstPIMXO + 4 * i] = OPS.paintSolidColorImageMask;
          continue;
        }
        break;
      }
      return count - i;
    }
  
    var InitialState = [];
  
    // This replaces (save, transform, paintInlineImageXObject, restore)+
    // sequences with one |paintInlineImageXObjectGroup| operation.
    addState(InitialState,
      [OPS.save, OPS.transform, OPS.paintInlineImageXObject, OPS.restore],
      function foundInlineImageGroup(context) {
        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 curr = context.iCurr;
        var iFirstSave = curr - 3;
        var iFirstTransform = curr - 2;
        var iFirstPIIXO = curr - 1;
  
        // Look for the quartets.
        var i = iFirstSave + 4;
        var ii = fnArray.length;
        while (i + 3 < ii) {
          if (fnArray[i] !== OPS.save ||
              fnArray[i + 1] !== OPS.transform ||
              fnArray[i + 2] !== OPS.paintInlineImageXObject ||
              fnArray[i + 3] !== OPS.restore) {
            break;    // ops don't match
          }
          i += 4;
        }
  
        // At this point, i is the index of the first op past the last valid
        // quartet.
        var count = Math.min((i - iFirstSave) / 4,
                             MAX_IMAGES_IN_INLINE_IMAGES_BLOCK);
        if (count < MIN_IMAGES_IN_INLINE_IMAGES_BLOCK) {
          return i;
        }
  
        // 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[iFirstTransform + (q << 2)];
          var img = argsArray[iFirstPIIXO + (q << 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,
            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[iFirstPIIXO + (q << 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;
          }
        }
  
        // Replace queue items.
        fnArray.splice(iFirstSave, count * 4, OPS.paintInlineImageXObjectGroup);
        argsArray.splice(iFirstSave, count * 4,
          [{ width: imgWidth, height: imgHeight, kind: ImageKind.RGBA_32BPP,
             data: imgData, }, map]);
  
        return iFirstSave + 1;
      });
  
    // This replaces (save, transform, paintImageMaskXObject, restore)+
    // sequences with one |paintImageMaskXObjectGroup| or one
    // |paintImageMaskXObjectRepeat| operation.
    addState(InitialState,
      [OPS.save, OPS.transform, OPS.paintImageMaskXObject, OPS.restore],
      function foundImageMaskGroup(context) {
        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 curr = context.iCurr;
        var iFirstSave = curr - 3;
        var iFirstTransform = curr - 2;
        var iFirstPIMXO = curr - 1;
  
        // Look for the quartets.
        var i = iFirstSave + 4;
        var ii = fnArray.length;
        while (i + 3 < ii) {
          if (fnArray[i] !== OPS.save ||
              fnArray[i + 1] !== OPS.transform ||
              fnArray[i + 2] !== OPS.paintImageMaskXObject ||
              fnArray[i + 3] !== OPS.restore) {
            break;    // ops don't match
          }
          i += 4;
        }
  
        // At this point, i is the index of the first op past the last valid
        // quartet.
        var count = (i - iFirstSave) / 4;
        count = handlePaintSolidColorImageMask(iFirstSave, count, fnArray,
                                               argsArray);
        if (count < MIN_IMAGES_IN_MASKS_BLOCK) {
          return i;
        }
  
        var q;
        var isSameImage = false;
        var iTransform, transformArgs;
        var firstPIMXOArg0 = argsArray[iFirstPIMXO][0];
        if (argsArray[iFirstTransform][1] === 0 &&
            argsArray[iFirstTransform][2] === 0) {
          isSameImage = true;
          var firstTransformArg0 = argsArray[iFirstTransform][0];
          var firstTransformArg3 = argsArray[iFirstTransform][3];
          iTransform = iFirstTransform + 4;
          var iPIMXO = iFirstPIMXO + 4;
          for (q = 1; q < count; q++, iTransform += 4, iPIMXO += 4) {
            transformArgs = argsArray[iTransform];
            if (argsArray[iPIMXO][0] !== firstPIMXOArg0 ||
                transformArgs[0] !== firstTransformArg0 ||
                transformArgs[1] !== 0 ||
                transformArgs[2] !== 0 ||
                transformArgs[3] !== firstTransformArg3) {
              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);
          iTransform = iFirstTransform;
          for (q = 0; q < count; q++, iTransform += 4) {
            transformArgs = argsArray[iTransform];
            positions[(q << 1)] = transformArgs[4];
            positions[(q << 1) + 1] = transformArgs[5];
          }
  
          // Replace queue items.
          fnArray.splice(iFirstSave, count * 4, OPS.paintImageMaskXObjectRepeat);
          argsArray.splice(iFirstSave, count * 4,
            [firstPIMXOArg0, firstTransformArg0, firstTransformArg3, positions]);
        } else {
          count = Math.min(count, MAX_IMAGES_IN_MASKS_BLOCK);
          var images = [];
          for (q = 0; q < count; q++) {
            transformArgs = argsArray[iFirstTransform + (q << 2)];
            var maskParams = argsArray[iFirstPIMXO + (q << 2)][0];
            images.push({ data: maskParams.data, width: maskParams.width,
                          height: maskParams.height,
                          transform: transformArgs, });
          }
  
          // Replace queue items.
          fnArray.splice(iFirstSave, count * 4, OPS.paintImageMaskXObjectGroup);
          argsArray.splice(iFirstSave, count * 4, [images]);
        }
  
        return iFirstSave + 1;
      });
  
    // This replaces (save, transform, paintImageXObject, restore)+ sequences
    // with one paintImageXObjectRepeat operation, if the |transform| and
    // |paintImageXObjectRepeat| ops are appropriate.
    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 curr = context.iCurr;
        var iFirstSave = curr - 3;
        var iFirstTransform = curr - 2;
        var iFirstPIXO = curr - 1;
        var iFirstRestore = curr;
  
        if (argsArray[iFirstTransform][1] !== 0 ||
            argsArray[iFirstTransform][2] !== 0) {
          return iFirstRestore + 1;   // transform has the wrong form
        }
  
        // Look for the quartets.
        var firstPIXOArg0 = argsArray[iFirstPIXO][0];
        var firstTransformArg0 = argsArray[iFirstTransform][0];
        var firstTransformArg3 = argsArray[iFirstTransform][3];
        var i = iFirstSave + 4;
        var ii = fnArray.length;
        while (i + 3 < ii) {
          if (fnArray[i] !== OPS.save ||
              fnArray[i + 1] !== OPS.transform ||
              fnArray[i + 2] !== OPS.paintImageXObject ||
              fnArray[i + 3] !== OPS.restore) {
            break;    // ops don't match
          }
          if (argsArray[i + 1][0] !== firstTransformArg0 ||
              argsArray[i + 1][1] !== 0 ||
              argsArray[i + 1][2] !== 0 ||
              argsArray[i + 1][3] !== firstTransformArg3) {
            break;    // transforms don't match
          }
          if (argsArray[i + 2][0] !== firstPIXOArg0) {
            break;    // images don't match
          }
          i += 4;
        }
  
        // At this point, i is the index of the first op past the last valid
        // quartet.
        var count = Math.min((i - iFirstSave) / 4, MAX_IMAGES_IN_BLOCK);
        if (count < MIN_IMAGES_IN_BLOCK) {
          return i;
        }
  
        // Extract the (x,y) positions from all of the matching transforms.
        var positions = new Float32Array(count * 2);
        var iTransform = iFirstTransform;
        for (var q = 0; q < count; q++, iTransform += 4) {
          var transformArgs = argsArray[iTransform];
          positions[(q << 1)] = transformArgs[4];
          positions[(q << 1) + 1] = transformArgs[5];
        }
  
        // Replace queue items.
        var args = [firstPIXOArg0, firstTransformArg0, firstTransformArg3,
                    positions];
        fnArray.splice(iFirstSave, count * 4, OPS.paintImageXObjectRepeat);
        argsArray.splice(iFirstSave, count * 4, args);
  
        return iFirstSave + 1;
      });
  
    // This replaces (beginText, setFont, setTextMatrix, showText, endText)+
    // sequences with (beginText, setFont, (setTextMatrix, showText)+, endText)+
    // sequences, if the font for each one is the same.
    addState(InitialState,
      [OPS.beginText, OPS.setFont, OPS.setTextMatrix, OPS.showText, OPS.endText],
      function (context) {
        var MIN_CHARS_IN_BLOCK = 3;
        var MAX_CHARS_IN_BLOCK = 1000;
  
        var fnArray = context.fnArray, argsArray = context.argsArray;
        var curr = context.iCurr;
        var iFirstBeginText = curr - 4;
        var iFirstSetFont = curr - 3;
        var iFirstSetTextMatrix = curr - 2;
        var iFirstShowText = curr - 1;
        var iFirstEndText = curr;
  
        // Look for the quintets.
        var firstSetFontArg0 = argsArray[iFirstSetFont][0];
        var firstSetFontArg1 = argsArray[iFirstSetFont][1];
        var i = iFirstBeginText + 5;
        var ii = fnArray.length;
        while (i + 4 < ii) {
          if (fnArray[i] !== OPS.beginText ||
              fnArray[i + 1] !== OPS.setFont ||
              fnArray[i + 2] !== OPS.setTextMatrix ||
              fnArray[i + 3] !== OPS.showText ||
              fnArray[i + 4] !== OPS.endText) {
            break;    // ops don't match
          }
          if (argsArray[i + 1][0] !== firstSetFontArg0 ||
              argsArray[i + 1][1] !== firstSetFontArg1) {
            break;    // fonts don't match
          }
          i += 5;
        }
  
        // At this point, i is the index of the first op past the last valid
        // quintet.
        var count = Math.min(((i - iFirstBeginText) / 5), MAX_CHARS_IN_BLOCK);
        if (count < MIN_CHARS_IN_BLOCK) {
          return i;
        }
  
        // If the preceding quintet is (<something>, setFont, setTextMatrix,
        // showText, endText), include that as well. (E.g. <something> might be
        // |dependency|.)
        var iFirst = iFirstBeginText;
        if (iFirstBeginText >= 4 &&
            fnArray[iFirstBeginText - 4] === fnArray[iFirstSetFont] &&
            fnArray[iFirstBeginText - 3] === fnArray[iFirstSetTextMatrix] &&
            fnArray[iFirstBeginText - 2] === fnArray[iFirstShowText] &&
            fnArray[iFirstBeginText - 1] === fnArray[iFirstEndText] &&
            argsArray[iFirstBeginText - 4][0] === firstSetFontArg0 &&
            argsArray[iFirstBeginText - 4][1] === firstSetFontArg1) {
          count++;
          iFirst -= 5;
        }
  
        // Remove (endText, beginText, setFont) trios.
        var iEndText = iFirst + 4;
        for (var q = 1; q < count; q++) {
          fnArray.splice(iEndText, 3);
          argsArray.splice(iEndText, 3);
          iEndText += 2;
        }
  
        return iEndText + 1;
      });
  
    function QueueOptimizer() {}
  
    QueueOptimizer.prototype = {
      optimize: function QueueOptimizer_optimize(queue) {
        var fnArray = queue.fnArray, argsArray = queue.argsArray;
        var context = {
          iCurr: 0,
          fnArray,
          argsArray,
        };
        var state;
        var i = 0, ii = fnArray.length;
        while (i < ii) {
          state = (state || InitialState)[fnArray[i]];
          if (typeof state === 'function') { // we found some handler
            context.iCurr = i;
            // state() returns the index of the first non-matching op (if we
            // didn't match) or the first op past the modified ops (if we did
            // match and replace).
            i = state(context);
            state = undefined;    // reset the state machine
            ii = context.fnArray.length;
          } else {
            i++;
          }
        }
      },
    };
    return QueueOptimizer;
  })();
  
  export {
    OperatorList,
    PartialEvaluator,
  };
  