From 3cfa316d40e86c1d5a403c80c57d26b882450b3c Mon Sep 17 00:00:00 2001
From: Jonas Jenwald <jonas.jenwald@gmail.com>
Date: Sun, 16 May 2021 13:39:54 +0200
Subject: [PATCH] Convert `src/core/operator_list.js` to use standard classes

With modern JavaScript modules, where only *explicitly* exported properties are visible to the outside, the `QueueOptimizerClosure` should no longer be necessary.

Furthermore, to reduce the possibility of `NullOptimizer` and `QueueOptimizer` getting out of sync (note e.g. the inconsistency fixed in PR 10784), we now let the latter extend the former one.
---
 src/core/operator_list.js | 1289 ++++++++++++++++++-------------------
 1 file changed, 636 insertions(+), 653 deletions(-)

diff --git a/src/core/operator_list.js b/src/core/operator_list.js
index c58a53e86..3dfc2b0de 100644
--- a/src/core/operator_list.js
+++ b/src/core/operator_list.js
@@ -13,487 +13,498 @@
  * limitations under the License.
  */
 
-import { assert, ImageKind, OPS, warn } from "../shared/util.js";
+import { assert, ImageKind, OPS, shadow, warn } from "../shared/util.js";
 
-const QueueOptimizer = (function QueueOptimizerClosure() {
-  function addState(parentState, pattern, checkFn, iterateFn, processFn) {
-    let state = parentState;
-    for (let i = 0, ii = pattern.length - 1; i < ii; i++) {
-      const item = pattern[i];
-      state = state[item] || (state[item] = []);
-    }
-    state[pattern[pattern.length - 1]] = {
-      checkFn,
-      iterateFn,
-      processFn,
-    };
+function addState(parentState, pattern, checkFn, iterateFn, processFn) {
+  let state = parentState;
+  for (let i = 0, ii = pattern.length - 1; i < ii; i++) {
+    const item = pattern[i];
+    state = state[item] || (state[item] = []);
   }
+  state[pattern[pattern.length - 1]] = {
+    checkFn,
+    iterateFn,
+    processFn,
+  };
+}
 
-  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.
-    const iFirstPIMXO = iFirstSave + 2;
-    let i;
-    for (i = 0; i < count; i++) {
-      const arg = argsArray[iFirstPIMXO + 4 * i];
-      const 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;
+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.
+  const iFirstPIMXO = iFirstSave + 2;
+  let i;
+  for (i = 0; i < count; i++) {
+    const arg = argsArray[iFirstPIMXO + 4 * i];
+    const 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;
     }
-    return count - i;
+    break;
   }
+  return count - i;
+}
 
-  const InitialState = [];
+const InitialState = [];
 
-  // This replaces (save, transform, paintInlineImageXObject, restore)+
-  // sequences with one |paintInlineImageXObjectGroup| operation.
-  addState(
-    InitialState,
-    [OPS.save, OPS.transform, OPS.paintInlineImageXObject, OPS.restore],
-    null,
-    function iterateInlineImageGroup(context, i) {
-      const fnArray = context.fnArray;
-      const iFirstSave = context.iCurr - 3;
-      const pos = (i - iFirstSave) % 4;
-      switch (pos) {
-        case 0:
-          return fnArray[i] === OPS.save;
-        case 1:
-          return fnArray[i] === OPS.transform;
-        case 2:
-          return fnArray[i] === OPS.paintInlineImageXObject;
-        case 3:
-          return fnArray[i] === OPS.restore;
-      }
-      throw new Error(`iterateInlineImageGroup - invalid pos: ${pos}`);
-    },
-    function foundInlineImageGroup(context, i) {
-      const MIN_IMAGES_IN_INLINE_IMAGES_BLOCK = 10;
-      const MAX_IMAGES_IN_INLINE_IMAGES_BLOCK = 200;
-      const MAX_WIDTH = 1000;
-      const IMAGE_PADDING = 1;
-
-      const fnArray = context.fnArray,
-        argsArray = context.argsArray;
-      const curr = context.iCurr;
-      const iFirstSave = curr - 3;
-      const iFirstTransform = curr - 2;
-      const iFirstPIIXO = curr - 1;
-
-      const count = Math.min(
-        Math.floor((i - iFirstSave) / 4),
-        MAX_IMAGES_IN_INLINE_IMAGES_BLOCK
-      );
-      if (count < MIN_IMAGES_IN_INLINE_IMAGES_BLOCK) {
-        return i - ((i - iFirstSave) % 4);
-      }
-
-      // assuming that heights of those image is too small (~1 pixel)
-      // packing as much as possible by lines
-      let maxX = 0;
-      const map = [];
-      let maxLineHeight = 0;
-      let currentX = IMAGE_PADDING,
-        currentY = IMAGE_PADDING;
-      for (let q = 0; q < count; q++) {
-        const transform = argsArray[iFirstTransform + (q << 2)];
-        const 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);
-      }
-      const imgWidth = Math.max(maxX, currentX) + IMAGE_PADDING;
-      const imgHeight = currentY + maxLineHeight + IMAGE_PADDING;
-      const imgData = new Uint8ClampedArray(imgWidth * imgHeight * 4);
-      const imgRowSize = imgWidth << 2;
-      for (let q = 0; q < count; q++) {
-        const data = argsArray[iFirstPIIXO + (q << 2)][0].data;
-        // Copy image by lines and extends pixels into padding.
-        const rowSize = map[q].w << 2;
-        let dataOffset = 0;
-        let offset = (map[q].x + map[q].y * imgWidth) << 2;
-        imgData.set(data.subarray(0, rowSize), offset - imgRowSize);
-        for (let 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, paintInlineImageXObject, restore)+
+// sequences with one |paintInlineImageXObjectGroup| operation.
+addState(
+  InitialState,
+  [OPS.save, OPS.transform, OPS.paintInlineImageXObject, OPS.restore],
+  null,
+  function iterateInlineImageGroup(context, i) {
+    const fnArray = context.fnArray;
+    const iFirstSave = context.iCurr - 3;
+    const pos = (i - iFirstSave) % 4;
+    switch (pos) {
+      case 0:
+        return fnArray[i] === OPS.save;
+      case 1:
+        return fnArray[i] === OPS.transform;
+      case 2:
+        return fnArray[i] === OPS.paintInlineImageXObject;
+      case 3:
+        return fnArray[i] === OPS.restore;
     }
-  );
+    throw new Error(`iterateInlineImageGroup - invalid pos: ${pos}`);
+  },
+  function foundInlineImageGroup(context, i) {
+    const MIN_IMAGES_IN_INLINE_IMAGES_BLOCK = 10;
+    const MAX_IMAGES_IN_INLINE_IMAGES_BLOCK = 200;
+    const MAX_WIDTH = 1000;
+    const IMAGE_PADDING = 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],
-    null,
-    function iterateImageMaskGroup(context, i) {
-      const fnArray = context.fnArray;
-      const iFirstSave = context.iCurr - 3;
-      const pos = (i - iFirstSave) % 4;
-      switch (pos) {
-        case 0:
-          return fnArray[i] === OPS.save;
-        case 1:
-          return fnArray[i] === OPS.transform;
-        case 2:
-          return fnArray[i] === OPS.paintImageMaskXObject;
-        case 3:
-          return fnArray[i] === OPS.restore;
-      }
-      throw new Error(`iterateImageMaskGroup - invalid pos: ${pos}`);
-    },
-    function foundImageMaskGroup(context, i) {
-      const MIN_IMAGES_IN_MASKS_BLOCK = 10;
-      const MAX_IMAGES_IN_MASKS_BLOCK = 100;
-      const MAX_SAME_IMAGES_IN_MASKS_BLOCK = 1000;
+    const fnArray = context.fnArray,
+      argsArray = context.argsArray;
+    const curr = context.iCurr;
+    const iFirstSave = curr - 3;
+    const iFirstTransform = curr - 2;
+    const iFirstPIIXO = curr - 1;
 
-      const fnArray = context.fnArray,
-        argsArray = context.argsArray;
-      const curr = context.iCurr;
-      const iFirstSave = curr - 3;
-      const iFirstTransform = curr - 2;
-      const iFirstPIMXO = curr - 1;
-
-      // At this point, i is the index of the first op past the last valid
-      // quartet.
-      let count = Math.floor((i - iFirstSave) / 4);
-      count = handlePaintSolidColorImageMask(
-        iFirstSave,
-        count,
-        fnArray,
-        argsArray
-      );
-      if (count < MIN_IMAGES_IN_MASKS_BLOCK) {
-        return i - ((i - iFirstSave) % 4);
-      }
-
-      let isSameImage = false;
-      let iTransform, transformArgs;
-      const firstPIMXOArg0 = argsArray[iFirstPIMXO][0];
-      const firstTransformArg0 = argsArray[iFirstTransform][0],
-        firstTransformArg1 = argsArray[iFirstTransform][1],
-        firstTransformArg2 = argsArray[iFirstTransform][2],
-        firstTransformArg3 = argsArray[iFirstTransform][3];
-
-      if (firstTransformArg1 === firstTransformArg2) {
-        isSameImage = true;
-        iTransform = iFirstTransform + 4;
-        let iPIMXO = iFirstPIMXO + 4;
-        for (let q = 1; q < count; q++, iTransform += 4, iPIMXO += 4) {
-          transformArgs = argsArray[iTransform];
-          if (
-            argsArray[iPIMXO][0] !== firstPIMXOArg0 ||
-            transformArgs[0] !== firstTransformArg0 ||
-            transformArgs[1] !== firstTransformArg1 ||
-            transformArgs[2] !== firstTransformArg2 ||
-            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);
-        const positions = new Float32Array(count * 2);
-        iTransform = iFirstTransform;
-        for (let 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,
-          firstTransformArg1,
-          firstTransformArg2,
-          firstTransformArg3,
-          positions,
-        ]);
-      } else {
-        count = Math.min(count, MAX_IMAGES_IN_MASKS_BLOCK);
-        const images = [];
-        for (let q = 0; q < count; q++) {
-          transformArgs = argsArray[iFirstTransform + (q << 2)];
-          const 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;
+    const count = Math.min(
+      Math.floor((i - iFirstSave) / 4),
+      MAX_IMAGES_IN_INLINE_IMAGES_BLOCK
+    );
+    if (count < MIN_IMAGES_IN_INLINE_IMAGES_BLOCK) {
+      return i - ((i - iFirstSave) % 4);
     }
-  );
 
-  // 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) {
-      const argsArray = context.argsArray;
-      const iFirstTransform = context.iCurr - 2;
-      return (
-        argsArray[iFirstTransform][1] === 0 &&
-        argsArray[iFirstTransform][2] === 0
-      );
-    },
-    function iterateImageGroup(context, i) {
-      const fnArray = context.fnArray,
-        argsArray = context.argsArray;
-      const iFirstSave = context.iCurr - 3;
-      const pos = (i - iFirstSave) % 4;
-      switch (pos) {
-        case 0:
-          return fnArray[i] === OPS.save;
-        case 1:
-          if (fnArray[i] !== OPS.transform) {
-            return false;
-          }
-          const iFirstTransform = context.iCurr - 2;
-          const firstTransformArg0 = argsArray[iFirstTransform][0];
-          const firstTransformArg3 = argsArray[iFirstTransform][3];
-          if (
-            argsArray[i][0] !== firstTransformArg0 ||
-            argsArray[i][1] !== 0 ||
-            argsArray[i][2] !== 0 ||
-            argsArray[i][3] !== firstTransformArg3
-          ) {
-            return false; // transforms don't match
-          }
-          return true;
-        case 2:
-          if (fnArray[i] !== OPS.paintImageXObject) {
-            return false;
-          }
-          const iFirstPIXO = context.iCurr - 1;
-          const firstPIXOArg0 = argsArray[iFirstPIXO][0];
-          if (argsArray[i][0] !== firstPIXOArg0) {
-            return false; // images don't match
-          }
-          return true;
-        case 3:
-          return fnArray[i] === OPS.restore;
+    // assuming that heights of those image is too small (~1 pixel)
+    // packing as much as possible by lines
+    let maxX = 0;
+    const map = [];
+    let maxLineHeight = 0;
+    let currentX = IMAGE_PADDING,
+      currentY = IMAGE_PADDING;
+    for (let q = 0; q < count; q++) {
+      const transform = argsArray[iFirstTransform + (q << 2)];
+      const 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;
       }
-      throw new Error(`iterateImageGroup - invalid pos: ${pos}`);
-    },
-    function (context, i) {
-      const MIN_IMAGES_IN_BLOCK = 3;
-      const MAX_IMAGES_IN_BLOCK = 1000;
-
-      const fnArray = context.fnArray,
-        argsArray = context.argsArray;
-      const curr = context.iCurr;
-      const iFirstSave = curr - 3;
-      const iFirstTransform = curr - 2;
-      const iFirstPIXO = curr - 1;
-      const firstPIXOArg0 = argsArray[iFirstPIXO][0];
-      const firstTransformArg0 = argsArray[iFirstTransform][0];
-      const firstTransformArg3 = argsArray[iFirstTransform][3];
-
-      // At this point, i is the index of the first op past the last valid
-      // quartet.
-      const count = Math.min(
-        Math.floor((i - iFirstSave) / 4),
-        MAX_IMAGES_IN_BLOCK
-      );
-      if (count < MIN_IMAGES_IN_BLOCK) {
-        return i - ((i - iFirstSave) % 4);
+      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);
+    }
+    const imgWidth = Math.max(maxX, currentX) + IMAGE_PADDING;
+    const imgHeight = currentY + maxLineHeight + IMAGE_PADDING;
+    const imgData = new Uint8ClampedArray(imgWidth * imgHeight * 4);
+    const imgRowSize = imgWidth << 2;
+    for (let q = 0; q < count; q++) {
+      const data = argsArray[iFirstPIIXO + (q << 2)][0].data;
+      // Copy image by lines and extends pixels into padding.
+      const rowSize = map[q].w << 2;
+      let dataOffset = 0;
+      let offset = (map[q].x + map[q].y * imgWidth) << 2;
+      imgData.set(data.subarray(0, rowSize), offset - imgRowSize);
+      for (let 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;
+      }
+    }
 
-      // Extract the (x,y) positions from all of the matching transforms.
+    // 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],
+  null,
+  function iterateImageMaskGroup(context, i) {
+    const fnArray = context.fnArray;
+    const iFirstSave = context.iCurr - 3;
+    const pos = (i - iFirstSave) % 4;
+    switch (pos) {
+      case 0:
+        return fnArray[i] === OPS.save;
+      case 1:
+        return fnArray[i] === OPS.transform;
+      case 2:
+        return fnArray[i] === OPS.paintImageMaskXObject;
+      case 3:
+        return fnArray[i] === OPS.restore;
+    }
+    throw new Error(`iterateImageMaskGroup - invalid pos: ${pos}`);
+  },
+  function foundImageMaskGroup(context, i) {
+    const MIN_IMAGES_IN_MASKS_BLOCK = 10;
+    const MAX_IMAGES_IN_MASKS_BLOCK = 100;
+    const MAX_SAME_IMAGES_IN_MASKS_BLOCK = 1000;
+
+    const fnArray = context.fnArray,
+      argsArray = context.argsArray;
+    const curr = context.iCurr;
+    const iFirstSave = curr - 3;
+    const iFirstTransform = curr - 2;
+    const iFirstPIMXO = curr - 1;
+
+    // At this point, i is the index of the first op past the last valid
+    // quartet.
+    let count = Math.floor((i - iFirstSave) / 4);
+    count = handlePaintSolidColorImageMask(
+      iFirstSave,
+      count,
+      fnArray,
+      argsArray
+    );
+    if (count < MIN_IMAGES_IN_MASKS_BLOCK) {
+      return i - ((i - iFirstSave) % 4);
+    }
+
+    let isSameImage = false;
+    let iTransform, transformArgs;
+    const firstPIMXOArg0 = argsArray[iFirstPIMXO][0];
+    const firstTransformArg0 = argsArray[iFirstTransform][0],
+      firstTransformArg1 = argsArray[iFirstTransform][1],
+      firstTransformArg2 = argsArray[iFirstTransform][2],
+      firstTransformArg3 = argsArray[iFirstTransform][3];
+
+    if (firstTransformArg1 === firstTransformArg2) {
+      isSameImage = true;
+      iTransform = iFirstTransform + 4;
+      let iPIMXO = iFirstPIMXO + 4;
+      for (let q = 1; q < count; q++, iTransform += 4, iPIMXO += 4) {
+        transformArgs = argsArray[iTransform];
+        if (
+          argsArray[iPIMXO][0] !== firstPIMXOArg0 ||
+          transformArgs[0] !== firstTransformArg0 ||
+          transformArgs[1] !== firstTransformArg1 ||
+          transformArgs[2] !== firstTransformArg2 ||
+          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);
       const positions = new Float32Array(count * 2);
-      let iTransform = iFirstTransform;
+      iTransform = iFirstTransform;
       for (let q = 0; q < count; q++, iTransform += 4) {
-        const transformArgs = argsArray[iTransform];
+        transformArgs = argsArray[iTransform];
         positions[q << 1] = transformArgs[4];
         positions[(q << 1) + 1] = transformArgs[5];
       }
 
       // Replace queue items.
-      const args = [
-        firstPIXOArg0,
+      fnArray.splice(iFirstSave, count * 4, OPS.paintImageMaskXObjectRepeat);
+      argsArray.splice(iFirstSave, count * 4, [
+        firstPIMXOArg0,
         firstTransformArg0,
+        firstTransformArg1,
+        firstTransformArg2,
         firstTransformArg3,
         positions,
-      ];
-      fnArray.splice(iFirstSave, count * 4, OPS.paintImageXObjectRepeat);
-      argsArray.splice(iFirstSave, count * 4, args);
+      ]);
+    } else {
+      count = Math.min(count, MAX_IMAGES_IN_MASKS_BLOCK);
+      const images = [];
+      for (let q = 0; q < count; q++) {
+        transformArgs = argsArray[iFirstTransform + (q << 2)];
+        const maskParams = argsArray[iFirstPIMXO + (q << 2)][0];
+        images.push({
+          data: maskParams.data,
+          width: maskParams.width,
+          height: maskParams.height,
+          transform: transformArgs,
+        });
+      }
 
-      return iFirstSave + 1;
+      // Replace queue items.
+      fnArray.splice(iFirstSave, count * 4, OPS.paintImageMaskXObjectGroup);
+      argsArray.splice(iFirstSave, count * 4, [images]);
     }
-  );
 
-  // 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],
-    null,
-    function iterateShowTextGroup(context, i) {
-      const fnArray = context.fnArray,
-        argsArray = context.argsArray;
-      const iFirstSave = context.iCurr - 4;
-      const pos = (i - iFirstSave) % 5;
-      switch (pos) {
-        case 0:
-          return fnArray[i] === OPS.beginText;
-        case 1:
-          return fnArray[i] === OPS.setFont;
-        case 2:
-          return fnArray[i] === OPS.setTextMatrix;
-        case 3:
-          if (fnArray[i] !== OPS.showText) {
-            return false;
-          }
-          const iFirstSetFont = context.iCurr - 3;
-          const firstSetFontArg0 = argsArray[iFirstSetFont][0];
-          const firstSetFontArg1 = argsArray[iFirstSetFont][1];
-          if (
-            argsArray[i][0] !== firstSetFontArg0 ||
-            argsArray[i][1] !== firstSetFontArg1
-          ) {
-            return false; // fonts don't match
-          }
-          return true;
-        case 4:
-          return fnArray[i] === OPS.endText;
-      }
-      throw new Error(`iterateShowTextGroup - invalid pos: ${pos}`);
-    },
-    function (context, i) {
-      const MIN_CHARS_IN_BLOCK = 3;
-      const MAX_CHARS_IN_BLOCK = 1000;
+    return iFirstSave + 1;
+  }
+);
 
-      const fnArray = context.fnArray,
-        argsArray = context.argsArray;
-      const curr = context.iCurr;
-      const iFirstBeginText = curr - 4;
-      const iFirstSetFont = curr - 3;
-      const iFirstSetTextMatrix = curr - 2;
-      const iFirstShowText = curr - 1;
-      const iFirstEndText = curr;
-      const firstSetFontArg0 = argsArray[iFirstSetFont][0];
-      const firstSetFontArg1 = argsArray[iFirstSetFont][1];
-
-      // At this point, i is the index of the first op past the last valid
-      // quintet.
-      let count = Math.min(
-        Math.floor((i - iFirstBeginText) / 5),
-        MAX_CHARS_IN_BLOCK
-      );
-      if (count < MIN_CHARS_IN_BLOCK) {
-        return i - ((i - iFirstBeginText) % 5);
-      }
-
-      // If the preceding quintet is (<something>, setFont, setTextMatrix,
-      // showText, endText), include that as well. (E.g. <something> might be
-      // |dependency|.)
-      let 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.
-      let iEndText = iFirst + 4;
-      for (let q = 1; q < count; q++) {
-        fnArray.splice(iEndText, 3);
-        argsArray.splice(iEndText, 3);
-        iEndText += 2;
-      }
-
-      return iEndText + 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) {
+    const argsArray = context.argsArray;
+    const iFirstTransform = context.iCurr - 2;
+    return (
+      argsArray[iFirstTransform][1] === 0 && argsArray[iFirstTransform][2] === 0
+    );
+  },
+  function iterateImageGroup(context, i) {
+    const fnArray = context.fnArray,
+      argsArray = context.argsArray;
+    const iFirstSave = context.iCurr - 3;
+    const pos = (i - iFirstSave) % 4;
+    switch (pos) {
+      case 0:
+        return fnArray[i] === OPS.save;
+      case 1:
+        if (fnArray[i] !== OPS.transform) {
+          return false;
+        }
+        const iFirstTransform = context.iCurr - 2;
+        const firstTransformArg0 = argsArray[iFirstTransform][0];
+        const firstTransformArg3 = argsArray[iFirstTransform][3];
+        if (
+          argsArray[i][0] !== firstTransformArg0 ||
+          argsArray[i][1] !== 0 ||
+          argsArray[i][2] !== 0 ||
+          argsArray[i][3] !== firstTransformArg3
+        ) {
+          return false; // transforms don't match
+        }
+        return true;
+      case 2:
+        if (fnArray[i] !== OPS.paintImageXObject) {
+          return false;
+        }
+        const iFirstPIXO = context.iCurr - 1;
+        const firstPIXOArg0 = argsArray[iFirstPIXO][0];
+        if (argsArray[i][0] !== firstPIXOArg0) {
+          return false; // images don't match
+        }
+        return true;
+      case 3:
+        return fnArray[i] === OPS.restore;
     }
-  );
+    throw new Error(`iterateImageGroup - invalid pos: ${pos}`);
+  },
+  function (context, i) {
+    const MIN_IMAGES_IN_BLOCK = 3;
+    const MAX_IMAGES_IN_BLOCK = 1000;
 
-  // eslint-disable-next-line no-shadow
-  function QueueOptimizer(queue) {
+    const fnArray = context.fnArray,
+      argsArray = context.argsArray;
+    const curr = context.iCurr;
+    const iFirstSave = curr - 3;
+    const iFirstTransform = curr - 2;
+    const iFirstPIXO = curr - 1;
+    const firstPIXOArg0 = argsArray[iFirstPIXO][0];
+    const firstTransformArg0 = argsArray[iFirstTransform][0];
+    const firstTransformArg3 = argsArray[iFirstTransform][3];
+
+    // At this point, i is the index of the first op past the last valid
+    // quartet.
+    const count = Math.min(
+      Math.floor((i - iFirstSave) / 4),
+      MAX_IMAGES_IN_BLOCK
+    );
+    if (count < MIN_IMAGES_IN_BLOCK) {
+      return i - ((i - iFirstSave) % 4);
+    }
+
+    // Extract the (x,y) positions from all of the matching transforms.
+    const positions = new Float32Array(count * 2);
+    let iTransform = iFirstTransform;
+    for (let q = 0; q < count; q++, iTransform += 4) {
+      const transformArgs = argsArray[iTransform];
+      positions[q << 1] = transformArgs[4];
+      positions[(q << 1) + 1] = transformArgs[5];
+    }
+
+    // Replace queue items.
+    const 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],
+  null,
+  function iterateShowTextGroup(context, i) {
+    const fnArray = context.fnArray,
+      argsArray = context.argsArray;
+    const iFirstSave = context.iCurr - 4;
+    const pos = (i - iFirstSave) % 5;
+    switch (pos) {
+      case 0:
+        return fnArray[i] === OPS.beginText;
+      case 1:
+        return fnArray[i] === OPS.setFont;
+      case 2:
+        return fnArray[i] === OPS.setTextMatrix;
+      case 3:
+        if (fnArray[i] !== OPS.showText) {
+          return false;
+        }
+        const iFirstSetFont = context.iCurr - 3;
+        const firstSetFontArg0 = argsArray[iFirstSetFont][0];
+        const firstSetFontArg1 = argsArray[iFirstSetFont][1];
+        if (
+          argsArray[i][0] !== firstSetFontArg0 ||
+          argsArray[i][1] !== firstSetFontArg1
+        ) {
+          return false; // fonts don't match
+        }
+        return true;
+      case 4:
+        return fnArray[i] === OPS.endText;
+    }
+    throw new Error(`iterateShowTextGroup - invalid pos: ${pos}`);
+  },
+  function (context, i) {
+    const MIN_CHARS_IN_BLOCK = 3;
+    const MAX_CHARS_IN_BLOCK = 1000;
+
+    const fnArray = context.fnArray,
+      argsArray = context.argsArray;
+    const curr = context.iCurr;
+    const iFirstBeginText = curr - 4;
+    const iFirstSetFont = curr - 3;
+    const iFirstSetTextMatrix = curr - 2;
+    const iFirstShowText = curr - 1;
+    const iFirstEndText = curr;
+    const firstSetFontArg0 = argsArray[iFirstSetFont][0];
+    const firstSetFontArg1 = argsArray[iFirstSetFont][1];
+
+    // At this point, i is the index of the first op past the last valid
+    // quintet.
+    let count = Math.min(
+      Math.floor((i - iFirstBeginText) / 5),
+      MAX_CHARS_IN_BLOCK
+    );
+    if (count < MIN_CHARS_IN_BLOCK) {
+      return i - ((i - iFirstBeginText) % 5);
+    }
+
+    // If the preceding quintet is (<something>, setFont, setTextMatrix,
+    // showText, endText), include that as well. (E.g. <something> might be
+    // |dependency|.)
+    let 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.
+    let iEndText = iFirst + 4;
+    for (let q = 1; q < count; q++) {
+      fnArray.splice(iEndText, 3);
+      argsArray.splice(iEndText, 3);
+      iEndText += 2;
+    }
+
+    return iEndText + 1;
+  }
+);
+
+class NullOptimizer {
+  constructor(queue) {
     this.queue = queue;
+  }
+
+  _optimize() {}
+
+  push(fn, args) {
+    this.queue.fnArray.push(fn);
+    this.queue.argsArray.push(args);
+    this._optimize();
+  }
+
+  flush() {}
+
+  reset() {}
+}
+
+class QueueOptimizer extends NullOptimizer {
+  constructor(queue) {
+    super(queue);
     this.state = null;
     this.context = {
       iCurr: 0,
@@ -504,113 +515,89 @@ const QueueOptimizer = (function QueueOptimizerClosure() {
     this.lastProcessed = 0;
   }
 
-  QueueOptimizer.prototype = {
-    _optimize() {
-      // Process new fnArray item(s) chunk.
-      const fnArray = this.queue.fnArray;
-      let i = this.lastProcessed,
-        ii = fnArray.length;
-      let state = this.state;
-      let match = this.match;
-      if (!state && !match && i + 1 === ii && !InitialState[fnArray[i]]) {
-        // Micro-optimization for the common case: last item is not
-        // optimizable, just skipping it.
-        this.lastProcessed = ii;
-        return;
-      }
+  _optimize() {
+    // Process new fnArray item(s) chunk.
+    const fnArray = this.queue.fnArray;
+    let i = this.lastProcessed,
+      ii = fnArray.length;
+    let state = this.state;
+    let match = this.match;
+    if (!state && !match && i + 1 === ii && !InitialState[fnArray[i]]) {
+      // Micro-optimization for the common case: last item is not
+      // optimizable, just skipping it.
+      this.lastProcessed = ii;
+      return;
+    }
 
-      const context = this.context;
-      while (i < ii) {
-        if (match) {
-          // Already find a block of potentially optimizable items, iterating...
-          const iterate = (0, match.iterateFn)(context, i);
-          if (iterate) {
-            i++;
-            continue;
-          }
-          // Found last items for the block, processing...
-          i = (0, match.processFn)(context, i + 1);
-          ii = fnArray.length;
-          match = null;
-          state = null;
-          if (i >= ii) {
-            break;
-          }
-        }
-        // Find the potentially optimizable items.
-        state = (state || InitialState)[fnArray[i]];
-        if (!state || Array.isArray(state)) {
+    const context = this.context;
+    while (i < ii) {
+      if (match) {
+        // Already find a block of potentially optimizable items, iterating...
+        const iterate = (0, match.iterateFn)(context, i);
+        if (iterate) {
           i++;
           continue;
         }
-        // Found a start of the block based on addState rules.
-        context.iCurr = i;
-        i++;
-        if (state.checkFn && !(0, state.checkFn)(context)) {
-          // Check failed, continue search...
-          state = null;
-          continue;
-        }
-        match = state;
+        // Found last items for the block, processing...
+        i = (0, match.processFn)(context, i + 1);
+        ii = fnArray.length;
+        match = null;
         state = null;
+        if (i >= ii) {
+          break;
+        }
       }
-      this.state = state;
-      this.match = match;
-      this.lastProcessed = i;
-    },
-
-    push(fn, args) {
-      this.queue.fnArray.push(fn);
-      this.queue.argsArray.push(args);
-      this._optimize();
-    },
-
-    flush() {
-      while (this.match) {
-        const length = this.queue.fnArray.length;
-        this.lastProcessed = (0, this.match.processFn)(this.context, length);
-        this.match = null;
-        this.state = null;
-        // Repeat optimization until all chunks are exhausted.
-        this._optimize();
+      // Find the potentially optimizable items.
+      state = (state || InitialState)[fnArray[i]];
+      if (!state || Array.isArray(state)) {
+        i++;
+        continue;
       }
-    },
-
-    reset() {
-      this.state = null;
-      this.match = null;
-      this.lastProcessed = 0;
-    },
-  };
-  return QueueOptimizer;
-})();
-
-const NullOptimizer = (function NullOptimizerClosure() {
-  // eslint-disable-next-line no-shadow
-  function NullOptimizer(queue) {
-    this.queue = queue;
+      // Found a start of the block based on addState rules.
+      context.iCurr = i;
+      i++;
+      if (state.checkFn && !(0, state.checkFn)(context)) {
+        // Check failed, continue search...
+        state = null;
+        continue;
+      }
+      match = state;
+      state = null;
+    }
+    this.state = state;
+    this.match = match;
+    this.lastProcessed = i;
   }
 
-  NullOptimizer.prototype = {
-    push(fn, args) {
-      this.queue.fnArray.push(fn);
-      this.queue.argsArray.push(args);
-    },
+  flush() {
+    while (this.match) {
+      const length = this.queue.fnArray.length;
+      this.lastProcessed = (0, this.match.processFn)(this.context, length);
+      this.match = null;
+      this.state = null;
+      // Repeat optimization until all chunks are exhausted.
+      this._optimize();
+    }
+  }
 
-    flush() {},
+  reset() {
+    this.state = null;
+    this.match = null;
+    this.lastProcessed = 0;
+  }
+}
 
-    reset() {},
-  };
+class OperatorList {
+  static get CHUNK_SIZE() {
+    return shadow(this, "CHUNK_SIZE", 1000);
+  }
 
-  return NullOptimizer;
-})();
+  // Close to chunk size.
+  static get CHUNK_SIZE_ABOUT() {
+    return shadow(this, "CHUNK_SIZE_ABOUT", OperatorList.CHUNK_SIZE - 5);
+  }
 
-const OperatorList = (function OperatorListClosure() {
-  const CHUNK_SIZE = 1000;
-  const CHUNK_SIZE_ABOUT = CHUNK_SIZE - 5; // close to chunk size
-
-  // eslint-disable-next-line no-shadow
-  function OperatorList(intent, streamSink) {
+  constructor(intent, streamSink) {
     this._streamSink = streamSink;
     this.fnArray = [];
     this.argsArray = [];
@@ -625,127 +612,123 @@ const OperatorList = (function OperatorListClosure() {
     this._resolved = streamSink ? null : Promise.resolve();
   }
 
-  OperatorList.prototype = {
-    get length() {
-      return this.argsArray.length;
-    },
+  get length() {
+    return this.argsArray.length;
+  }
 
-    get ready() {
-      return this._resolved || this._streamSink.ready;
-    },
+  get ready() {
+    return this._resolved || this._streamSink.ready;
+  }
 
-    /**
-     * @type {number} The total length of the entire operator list, since
-     *                `this.length === 0` after flushing.
-     */
-    get totalLength() {
-      return this._totalLength + this.length;
-    },
+  /**
+   * @type {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.optimizer.push(fn, args);
-      this.weight++;
-      if (this._streamSink) {
-        if (this.weight >= CHUNK_SIZE) {
-          this.flush();
-        } else if (
-          this.weight >= CHUNK_SIZE_ABOUT &&
-          (fn === OPS.restore || fn === OPS.endText)
-        ) {
-          // heuristic to flush on boundary of restore or endText
-          this.flush();
-        }
+  addOp(fn, args) {
+    this.optimizer.push(fn, args);
+    this.weight++;
+    if (this._streamSink) {
+      if (this.weight >= OperatorList.CHUNK_SIZE) {
+        this.flush();
+      } else if (
+        this.weight >= OperatorList.CHUNK_SIZE_ABOUT &&
+        (fn === OPS.restore || fn === OPS.endText)
+      ) {
+        // Heuristic to flush on boundary of restore or endText.
+        this.flush();
       }
-    },
+    }
+  }
 
-    addDependency(dependency) {
-      if (this.dependencies.has(dependency)) {
-        return;
-      }
+  addDependency(dependency) {
+    if (this.dependencies.has(dependency)) {
+      return;
+    }
+    this.dependencies.add(dependency);
+    this.addOp(OPS.dependency, [dependency]);
+  }
+
+  addDependencies(dependencies) {
+    for (const dependency of dependencies) {
+      this.addDependency(dependency);
+    }
+  }
+
+  addOpList(opList) {
+    if (!(opList instanceof OperatorList)) {
+      warn('addOpList - ignoring invalid "opList" parameter.');
+      return;
+    }
+    for (const dependency of opList.dependencies) {
       this.dependencies.add(dependency);
-      this.addOp(OPS.dependency, [dependency]);
-    },
+    }
+    for (let i = 0, ii = opList.length; i < ii; i++) {
+      this.addOp(opList.fnArray[i], opList.argsArray[i]);
+    }
+  }
 
-    addDependencies(dependencies) {
-      for (const dependency of dependencies) {
-        this.addDependency(dependency);
-      }
-    },
+  getIR() {
+    return {
+      fnArray: this.fnArray,
+      argsArray: this.argsArray,
+      length: this.length,
+    };
+  }
 
-    addOpList(opList) {
-      if (!(opList instanceof OperatorList)) {
-        warn('addOpList - ignoring invalid "opList" parameter.');
-        return;
-      }
-      for (const dependency of opList.dependencies) {
-        this.dependencies.add(dependency);
-      }
-      for (let i = 0, ii = opList.length; i < ii; i++) {
-        this.addOp(opList.fnArray[i], opList.argsArray[i]);
-      }
-    },
+  get _transfers() {
+    const transfers = [];
+    const { fnArray, argsArray, length } = this;
+    for (let i = 0; i < length; i++) {
+      switch (fnArray[i]) {
+        case OPS.paintInlineImageXObject:
+        case OPS.paintInlineImageXObjectGroup:
+        case OPS.paintImageMaskXObject:
+          const arg = argsArray[i][0]; // First parameter in imgData.
 
-    getIR() {
-      return {
+          if (
+            typeof PDFJSDev === "undefined" ||
+            PDFJSDev.test("!PRODUCTION || TESTING")
+          ) {
+            assert(
+              arg.data instanceof Uint8ClampedArray,
+              'OperatorList._transfers: Unsupported "arg.data" type.'
+            );
+          }
+          if (!arg.cached) {
+            transfers.push(arg.data.buffer);
+          }
+          break;
+      }
+    }
+    return transfers;
+  }
+
+  flush(lastChunk = false) {
+    this.optimizer.flush();
+    const length = this.length;
+    this._totalLength += length;
+
+    this._streamSink.enqueue(
+      {
         fnArray: this.fnArray,
         argsArray: this.argsArray,
-        length: this.length,
-      };
-    },
+        lastChunk,
+        length,
+      },
+      1,
+      this._transfers
+    );
 
-    get _transfers() {
-      const transfers = [];
-      const { fnArray, argsArray, length } = this;
-      for (let i = 0; i < length; i++) {
-        switch (fnArray[i]) {
-          case OPS.paintInlineImageXObject:
-          case OPS.paintInlineImageXObjectGroup:
-          case OPS.paintImageMaskXObject:
-            const arg = argsArray[i][0]; // first param in imgData
-
-            if (
-              typeof PDFJSDev === "undefined" ||
-              PDFJSDev.test("!PRODUCTION || TESTING")
-            ) {
-              assert(
-                arg.data instanceof Uint8ClampedArray,
-                'OperatorList._transfers: Unsupported "arg.data" type.'
-              );
-            }
-            if (!arg.cached) {
-              transfers.push(arg.data.buffer);
-            }
-            break;
-        }
-      }
-      return transfers;
-    },
-
-    flush(lastChunk = false) {
-      this.optimizer.flush();
-      const length = this.length;
-      this._totalLength += length;
-
-      this._streamSink.enqueue(
-        {
-          fnArray: this.fnArray,
-          argsArray: this.argsArray,
-          lastChunk,
-          length,
-        },
-        1,
-        this._transfers
-      );
-
-      this.dependencies.clear();
-      this.fnArray.length = 0;
-      this.argsArray.length = 0;
-      this.weight = 0;
-      this.optimizer.reset();
-    },
-  };
-
-  return OperatorList;
-})();
+    this.dependencies.clear();
+    this.fnArray.length = 0;
+    this.argsArray.length = 0;
+    this.weight = 0;
+    this.optimizer.reset();
+  }
+}
 
 export { OperatorList };