From 481af097b48eb928bc094e0ad3bdfeaeed21c522 Mon Sep 17 00:00:00 2001
From: Jonas Jenwald <jonas.jenwald@gmail.com>
Date: Sat, 17 Jul 2021 16:26:28 +0200
Subject: [PATCH] Convert `PDFFunction` to a standard class with `static`
 methods

For e.g. `gulp mozcentral`, the *built* `pdf.worker.js` file decreases from `1 837 608` to `1 834 907` bytes with this patch-series.
The improvement comes first of all from less overall indentation in `PDFFunction`, and secondly from the removal of (now) unnecessary indirection in the code.
---
 src/core/function.js | 720 +++++++++++++++++++++----------------------
 1 file changed, 352 insertions(+), 368 deletions(-)

diff --git a/src/core/function.js b/src/core/function.js
index 8dc6be408..075a9970a 100644
--- a/src/core/function.js
+++ b/src/core/function.js
@@ -131,393 +131,377 @@ function toNumberArray(arr) {
   return arr;
 }
 
-const PDFFunction = (function PDFFunctionClosure() {
-  return {
-    getSampleArray(size, outputSize, bps, stream) {
-      let i, ii;
-      let length = 1;
-      for (i = 0, ii = size.length; i < ii; i++) {
-        length *= size[i];
+class PDFFunction {
+  static getSampleArray(size, outputSize, bps, stream) {
+    let i, ii;
+    let length = 1;
+    for (i = 0, ii = size.length; i < ii; i++) {
+      length *= size[i];
+    }
+    length *= outputSize;
+
+    const array = new Array(length);
+    let codeSize = 0;
+    let codeBuf = 0;
+    // 32 is a valid bps so shifting won't work
+    const sampleMul = 1.0 / (2.0 ** bps - 1);
+
+    const strBytes = stream.getBytes((length * bps + 7) / 8);
+    let strIdx = 0;
+    for (i = 0; i < length; i++) {
+      while (codeSize < bps) {
+        codeBuf <<= 8;
+        codeBuf |= strBytes[strIdx++];
+        codeSize += 8;
       }
-      length *= outputSize;
+      codeSize -= bps;
+      array[i] = (codeBuf >> codeSize) * sampleMul;
+      codeBuf &= (1 << codeSize) - 1;
+    }
+    return array;
+  }
 
-      const array = new Array(length);
-      let codeSize = 0;
-      let codeBuf = 0;
-      // 32 is a valid bps so shifting won't work
-      const sampleMul = 1.0 / (2.0 ** bps - 1);
+  static parse({ xref, isEvalSupported, fn }) {
+    const dict = fn.dict || fn;
+    const typeNum = dict.get("FunctionType");
 
-      const strBytes = stream.getBytes((length * bps + 7) / 8);
-      let strIdx = 0;
-      for (i = 0; i < length; i++) {
-        while (codeSize < bps) {
-          codeBuf <<= 8;
-          codeBuf |= strBytes[strIdx++];
-          codeSize += 8;
-        }
-        codeSize -= bps;
-        array[i] = (codeBuf >> codeSize) * sampleMul;
-        codeBuf &= (1 << codeSize) - 1;
+    switch (typeNum) {
+      case 0:
+        return this.constructSampled({ xref, isEvalSupported, fn, dict });
+      case 1:
+        break;
+      case 2:
+        return this.constructInterpolated({ xref, isEvalSupported, dict });
+      case 3:
+        return this.constructStiched({ xref, isEvalSupported, dict });
+      case 4:
+        return this.constructPostScript({ xref, isEvalSupported, fn, dict });
+    }
+    throw new FormatError("Unknown type of function");
+  }
+
+  static parseArray({ xref, isEvalSupported, fnObj }) {
+    if (!Array.isArray(fnObj)) {
+      // not an array -- parsing as regular function
+      return this.parse({ xref, isEvalSupported, fn: fnObj });
+    }
+
+    const fnArray = [];
+    for (let j = 0, jj = fnObj.length; j < jj; j++) {
+      fnArray.push(
+        this.parse({ xref, isEvalSupported, fn: xref.fetchIfRef(fnObj[j]) })
+      );
+    }
+    return function (src, srcOffset, dest, destOffset) {
+      for (let i = 0, ii = fnArray.length; i < ii; i++) {
+        fnArray[i](src, srcOffset, dest, destOffset + i);
       }
-      return array;
-    },
+    };
+  }
 
-    parse({ xref, isEvalSupported, fn }) {
-      const dict = fn.dict || fn;
-      const typeNum = dict.get("FunctionType");
-
-      switch (typeNum) {
-        case 0:
-          return this.constructSampled({ xref, isEvalSupported, fn, dict });
-        case 1:
-          break;
-        case 2:
-          return this.constructInterpolated({ xref, isEvalSupported, dict });
-        case 3:
-          return this.constructStiched({ xref, isEvalSupported, dict });
-        case 4:
-          return this.constructPostScript({ xref, isEvalSupported, fn, dict });
+  static constructSampled({ xref, isEvalSupported, fn, dict }) {
+    function toMultiArray(arr) {
+      const inputLength = arr.length;
+      const out = [];
+      let index = 0;
+      for (let i = 0; i < inputLength; i += 2) {
+        out[index++] = [arr[i], arr[i + 1]];
       }
-      throw new FormatError("Unknown type of function");
-    },
+      return out;
+    }
+    // See chapter 3, page 109 of the PDF reference
+    function interpolate(x, xmin, xmax, ymin, ymax) {
+      return ymin + (x - xmin) * ((ymax - ymin) / (xmax - xmin));
+    }
 
-    parseArray({ xref, isEvalSupported, fnObj }) {
-      if (!Array.isArray(fnObj)) {
-        // not an array -- parsing as regular function
-        return this.parse({ xref, isEvalSupported, fn: fnObj });
+    let domain = toNumberArray(dict.getArray("Domain"));
+    let range = toNumberArray(dict.getArray("Range"));
+
+    if (!domain || !range) {
+      throw new FormatError("No domain or range");
+    }
+
+    const inputSize = domain.length / 2;
+    const outputSize = range.length / 2;
+
+    domain = toMultiArray(domain);
+    range = toMultiArray(range);
+
+    const size = toNumberArray(dict.getArray("Size"));
+    const bps = dict.get("BitsPerSample");
+    const order = dict.get("Order") || 1;
+    if (order !== 1) {
+      // No description how cubic spline interpolation works in PDF32000:2008
+      // As in poppler, ignoring order, linear interpolation may work as good
+      info("No support for cubic spline interpolation: " + order);
+    }
+
+    let encode = toNumberArray(dict.getArray("Encode"));
+    if (!encode) {
+      encode = [];
+      for (let i = 0; i < inputSize; ++i) {
+        encode.push([0, size[i] - 1]);
+      }
+    } else {
+      encode = toMultiArray(encode);
+    }
+
+    let decode = toNumberArray(dict.getArray("Decode"));
+    if (!decode) {
+      decode = range;
+    } else {
+      decode = toMultiArray(decode);
+    }
+
+    const samples = this.getSampleArray(size, outputSize, bps, fn);
+    // const mask = 2 ** bps - 1;
+
+    return function constructSampledFn(src, srcOffset, dest, destOffset) {
+      // See chapter 3, page 110 of the PDF reference.
+
+      // Building the cube vertices: its part and sample index
+      // http://rjwagner49.com/Mathematics/Interpolation.pdf
+      const cubeVertices = 1 << inputSize;
+      const cubeN = new Float64Array(cubeVertices);
+      const cubeVertex = new Uint32Array(cubeVertices);
+      let i, j;
+      for (j = 0; j < cubeVertices; j++) {
+        cubeN[j] = 1;
       }
 
-      const fnArray = [];
-      for (let j = 0, jj = fnObj.length; j < jj; j++) {
-        fnArray.push(
-          this.parse({ xref, isEvalSupported, fn: xref.fetchIfRef(fnObj[j]) })
+      let k = outputSize,
+        pos = 1;
+      // Map x_i to y_j for 0 <= i < m using the sampled function.
+      for (i = 0; i < inputSize; ++i) {
+        // x_i' = min(max(x_i, Domain_2i), Domain_2i+1)
+        const domain_2i = domain[i][0];
+        const domain_2i_1 = domain[i][1];
+        const xi = Math.min(
+          Math.max(src[srcOffset + i], domain_2i),
+          domain_2i_1
         );
-      }
-      return function (src, srcOffset, dest, destOffset) {
-        for (let i = 0, ii = fnArray.length; i < ii; i++) {
-          fnArray[i](src, srcOffset, dest, destOffset + i);
-        }
-      };
-    },
 
-    constructSampled({ xref, isEvalSupported, fn, dict }) {
-      function toMultiArray(arr) {
-        const inputLength = arr.length;
-        const out = [];
-        let index = 0;
-        for (let i = 0; i < inputLength; i += 2) {
-          out[index++] = [arr[i], arr[i + 1]];
-        }
-        return out;
-      }
-      // See chapter 3, page 109 of the PDF reference
-      function interpolate(x, xmin, xmax, ymin, ymax) {
-        return ymin + (x - xmin) * ((ymax - ymin) / (xmax - xmin));
-      }
+        // e_i = Interpolate(x_i', Domain_2i, Domain_2i+1,
+        //                   Encode_2i, Encode_2i+1)
+        let e = interpolate(
+          xi,
+          domain_2i,
+          domain_2i_1,
+          encode[i][0],
+          encode[i][1]
+        );
 
-      let domain = toNumberArray(dict.getArray("Domain"));
-      let range = toNumberArray(dict.getArray("Range"));
+        // e_i' = min(max(e_i, 0), Size_i - 1)
+        const size_i = size[i];
+        e = Math.min(Math.max(e, 0), size_i - 1);
 
-      if (!domain || !range) {
-        throw new FormatError("No domain or range");
-      }
-
-      const inputSize = domain.length / 2;
-      const outputSize = range.length / 2;
-
-      domain = toMultiArray(domain);
-      range = toMultiArray(range);
-
-      const size = toNumberArray(dict.getArray("Size"));
-      const bps = dict.get("BitsPerSample");
-      const order = dict.get("Order") || 1;
-      if (order !== 1) {
-        // No description how cubic spline interpolation works in PDF32000:2008
-        // As in poppler, ignoring order, linear interpolation may work as good
-        info("No support for cubic spline interpolation: " + order);
-      }
-
-      let encode = toNumberArray(dict.getArray("Encode"));
-      if (!encode) {
-        encode = [];
-        for (let i = 0; i < inputSize; ++i) {
-          encode.push([0, size[i] - 1]);
-        }
-      } else {
-        encode = toMultiArray(encode);
-      }
-
-      let decode = toNumberArray(dict.getArray("Decode"));
-      if (!decode) {
-        decode = range;
-      } else {
-        decode = toMultiArray(decode);
-      }
-
-      const samples = this.getSampleArray(size, outputSize, bps, fn);
-      // const mask = 2 ** bps - 1;
-
-      return function constructSampledFn(src, srcOffset, dest, destOffset) {
-        // See chapter 3, page 110 of the PDF reference.
-
-        // Building the cube vertices: its part and sample index
-        // http://rjwagner49.com/Mathematics/Interpolation.pdf
-        const cubeVertices = 1 << inputSize;
-        const cubeN = new Float64Array(cubeVertices);
-        const cubeVertex = new Uint32Array(cubeVertices);
-        let i, j;
+        // Adjusting the cube: N and vertex sample index
+        const e0 = e < size_i - 1 ? Math.floor(e) : e - 1; // e1 = e0 + 1;
+        const n0 = e0 + 1 - e; // (e1 - e) / (e1 - e0);
+        const n1 = e - e0; // (e - e0) / (e1 - e0);
+        const offset0 = e0 * k;
+        const offset1 = offset0 + k; // e1 * k
         for (j = 0; j < cubeVertices; j++) {
-          cubeN[j] = 1;
-        }
-
-        let k = outputSize,
-          pos = 1;
-        // Map x_i to y_j for 0 <= i < m using the sampled function.
-        for (i = 0; i < inputSize; ++i) {
-          // x_i' = min(max(x_i, Domain_2i), Domain_2i+1)
-          const domain_2i = domain[i][0];
-          const domain_2i_1 = domain[i][1];
-          const xi = Math.min(
-            Math.max(src[srcOffset + i], domain_2i),
-            domain_2i_1
-          );
-
-          // e_i = Interpolate(x_i', Domain_2i, Domain_2i+1,
-          //                   Encode_2i, Encode_2i+1)
-          let e = interpolate(
-            xi,
-            domain_2i,
-            domain_2i_1,
-            encode[i][0],
-            encode[i][1]
-          );
-
-          // e_i' = min(max(e_i, 0), Size_i - 1)
-          const size_i = size[i];
-          e = Math.min(Math.max(e, 0), size_i - 1);
-
-          // Adjusting the cube: N and vertex sample index
-          const e0 = e < size_i - 1 ? Math.floor(e) : e - 1; // e1 = e0 + 1;
-          const n0 = e0 + 1 - e; // (e1 - e) / (e1 - e0);
-          const n1 = e - e0; // (e - e0) / (e1 - e0);
-          const offset0 = e0 * k;
-          const offset1 = offset0 + k; // e1 * k
-          for (j = 0; j < cubeVertices; j++) {
-            if (j & pos) {
-              cubeN[j] *= n1;
-              cubeVertex[j] += offset1;
-            } else {
-              cubeN[j] *= n0;
-              cubeVertex[j] += offset0;
-            }
-          }
-
-          k *= size_i;
-          pos <<= 1;
-        }
-
-        for (j = 0; j < outputSize; ++j) {
-          // Sum all cube vertices' samples portions
-          let rj = 0;
-          for (i = 0; i < cubeVertices; i++) {
-            rj += samples[cubeVertex[i] + j] * cubeN[i];
-          }
-
-          // r_j' = Interpolate(r_j, 0, 2^BitsPerSample - 1,
-          //                    Decode_2j, Decode_2j+1)
-          rj = interpolate(rj, 0, 1, decode[j][0], decode[j][1]);
-
-          // y_j = min(max(r_j, range_2j), range_2j+1)
-          dest[destOffset + j] = Math.min(
-            Math.max(rj, range[j][0]),
-            range[j][1]
-          );
-        }
-      };
-    },
-
-    constructInterpolated({ xref, isEvalSupported, dict }) {
-      const c0 = toNumberArray(dict.getArray("C0")) || [0];
-      const c1 = toNumberArray(dict.getArray("C1")) || [1];
-      const n = dict.get("N");
-
-      const diff = [];
-      for (let i = 0, ii = c0.length; i < ii; ++i) {
-        diff.push(c1[i] - c0[i]);
-      }
-      const length = diff.length;
-
-      return function constructInterpolatedFn(
-        src,
-        srcOffset,
-        dest,
-        destOffset
-      ) {
-        const x = n === 1 ? src[srcOffset] : src[srcOffset] ** n;
-
-        for (let j = 0; j < length; ++j) {
-          dest[destOffset + j] = c0[j] + x * diff[j];
-        }
-      };
-    },
-
-    constructStiched({ xref, isEvalSupported, dict }) {
-      const domain = toNumberArray(dict.getArray("Domain"));
-
-      if (!domain) {
-        throw new FormatError("No domain");
-      }
-
-      const inputSize = domain.length / 2;
-      if (inputSize !== 1) {
-        throw new FormatError("Bad domain for stiched function");
-      }
-
-      const fnRefs = dict.get("Functions");
-      const fns = [];
-      for (let i = 0, ii = fnRefs.length; i < ii; ++i) {
-        fns.push(
-          this.parse({ xref, isEvalSupported, fn: xref.fetchIfRef(fnRefs[i]) })
-        );
-      }
-
-      const bounds = toNumberArray(dict.getArray("Bounds"));
-      const encode = toNumberArray(dict.getArray("Encode"));
-      const tmpBuf = new Float32Array(1);
-
-      return function constructStichedFn(src, srcOffset, dest, destOffset) {
-        const clip = function constructStichedFromIRClip(v, min, max) {
-          if (v > max) {
-            v = max;
-          } else if (v < min) {
-            v = min;
-          }
-          return v;
-        };
-
-        // clip to domain
-        const v = clip(src[srcOffset], domain[0], domain[1]);
-        // calculate which bound the value is in
-        const length = bounds.length;
-        let i;
-        for (i = 0; i < length; ++i) {
-          if (v < bounds[i]) {
-            break;
-          }
-        }
-
-        // encode value into domain of function
-        let dmin = domain[0];
-        if (i > 0) {
-          dmin = bounds[i - 1];
-        }
-        let dmax = domain[1];
-        if (i < bounds.length) {
-          dmax = bounds[i];
-        }
-
-        const rmin = encode[2 * i];
-        const rmax = encode[2 * i + 1];
-
-        // Prevent the value from becoming NaN as a result
-        // of division by zero (fixes issue6113.pdf).
-        tmpBuf[0] =
-          dmin === dmax
-            ? rmin
-            : rmin + ((v - dmin) * (rmax - rmin)) / (dmax - dmin);
-
-        // call the appropriate function
-        fns[i](tmpBuf, 0, dest, destOffset);
-      };
-    },
-
-    constructPostScript({ xref, isEvalSupported, fn, dict }) {
-      const domain = toNumberArray(dict.getArray("Domain"));
-      const range = toNumberArray(dict.getArray("Range"));
-
-      if (!domain) {
-        throw new FormatError("No domain.");
-      }
-
-      if (!range) {
-        throw new FormatError("No range.");
-      }
-
-      const lexer = new PostScriptLexer(fn);
-      const parser = new PostScriptParser(lexer);
-      const code = parser.parse();
-
-      if (isEvalSupported && IsEvalSupportedCached.value) {
-        const compiled = new PostScriptCompiler().compile(code, domain, range);
-        if (compiled) {
-          // Compiled function consists of simple expressions such as addition,
-          // subtraction, Math.max, and also contains 'var' and 'return'
-          // statements. See the generation in the PostScriptCompiler below.
-          // eslint-disable-next-line no-new-func
-          return new Function(
-            "src",
-            "srcOffset",
-            "dest",
-            "destOffset",
-            compiled
-          );
-        }
-      }
-      info("Unable to compile PS function");
-
-      const numOutputs = range.length >> 1;
-      const numInputs = domain.length >> 1;
-      const evaluator = new PostScriptEvaluator(code);
-      // Cache the values for a big speed up, the cache size is limited though
-      // since the number of possible values can be huge from a PS function.
-      const cache = Object.create(null);
-      // The MAX_CACHE_SIZE is set to ~4x the maximum number of distinct values
-      // seen in our tests.
-      const MAX_CACHE_SIZE = 2048 * 4;
-      let cache_available = MAX_CACHE_SIZE;
-      const tmpBuf = new Float32Array(numInputs);
-
-      return function constructPostScriptFn(src, srcOffset, dest, destOffset) {
-        let i, value;
-        let key = "";
-        const input = tmpBuf;
-        for (i = 0; i < numInputs; i++) {
-          value = src[srcOffset + i];
-          input[i] = value;
-          key += value + "_";
-        }
-
-        const cachedValue = cache[key];
-        if (cachedValue !== undefined) {
-          dest.set(cachedValue, destOffset);
-          return;
-        }
-
-        const output = new Float32Array(numOutputs);
-        const stack = evaluator.execute(input);
-        const stackIndex = stack.length - numOutputs;
-        for (i = 0; i < numOutputs; i++) {
-          value = stack[stackIndex + i];
-          let bound = range[i * 2];
-          if (value < bound) {
-            value = bound;
+          if (j & pos) {
+            cubeN[j] *= n1;
+            cubeVertex[j] += offset1;
           } else {
-            bound = range[i * 2 + 1];
-            if (value > bound) {
-              value = bound;
-            }
+            cubeN[j] *= n0;
+            cubeVertex[j] += offset0;
           }
-          output[i] = value;
         }
-        if (cache_available > 0) {
-          cache_available--;
-          cache[key] = output;
+
+        k *= size_i;
+        pos <<= 1;
+      }
+
+      for (j = 0; j < outputSize; ++j) {
+        // Sum all cube vertices' samples portions
+        let rj = 0;
+        for (i = 0; i < cubeVertices; i++) {
+          rj += samples[cubeVertex[i] + j] * cubeN[i];
         }
-        dest.set(output, destOffset);
+
+        // r_j' = Interpolate(r_j, 0, 2^BitsPerSample - 1,
+        //                    Decode_2j, Decode_2j+1)
+        rj = interpolate(rj, 0, 1, decode[j][0], decode[j][1]);
+
+        // y_j = min(max(r_j, range_2j), range_2j+1)
+        dest[destOffset + j] = Math.min(Math.max(rj, range[j][0]), range[j][1]);
+      }
+    };
+  }
+
+  static constructInterpolated({ xref, isEvalSupported, dict }) {
+    const c0 = toNumberArray(dict.getArray("C0")) || [0];
+    const c1 = toNumberArray(dict.getArray("C1")) || [1];
+    const n = dict.get("N");
+
+    const diff = [];
+    for (let i = 0, ii = c0.length; i < ii; ++i) {
+      diff.push(c1[i] - c0[i]);
+    }
+    const length = diff.length;
+
+    return function constructInterpolatedFn(src, srcOffset, dest, destOffset) {
+      const x = n === 1 ? src[srcOffset] : src[srcOffset] ** n;
+
+      for (let j = 0; j < length; ++j) {
+        dest[destOffset + j] = c0[j] + x * diff[j];
+      }
+    };
+  }
+
+  static constructStiched({ xref, isEvalSupported, dict }) {
+    const domain = toNumberArray(dict.getArray("Domain"));
+
+    if (!domain) {
+      throw new FormatError("No domain");
+    }
+
+    const inputSize = domain.length / 2;
+    if (inputSize !== 1) {
+      throw new FormatError("Bad domain for stiched function");
+    }
+
+    const fnRefs = dict.get("Functions");
+    const fns = [];
+    for (let i = 0, ii = fnRefs.length; i < ii; ++i) {
+      fns.push(
+        this.parse({ xref, isEvalSupported, fn: xref.fetchIfRef(fnRefs[i]) })
+      );
+    }
+
+    const bounds = toNumberArray(dict.getArray("Bounds"));
+    const encode = toNumberArray(dict.getArray("Encode"));
+    const tmpBuf = new Float32Array(1);
+
+    return function constructStichedFn(src, srcOffset, dest, destOffset) {
+      const clip = function constructStichedFromIRClip(v, min, max) {
+        if (v > max) {
+          v = max;
+        } else if (v < min) {
+          v = min;
+        }
+        return v;
       };
-    },
-  };
-})();
+
+      // clip to domain
+      const v = clip(src[srcOffset], domain[0], domain[1]);
+      // calculate which bound the value is in
+      const length = bounds.length;
+      let i;
+      for (i = 0; i < length; ++i) {
+        if (v < bounds[i]) {
+          break;
+        }
+      }
+
+      // encode value into domain of function
+      let dmin = domain[0];
+      if (i > 0) {
+        dmin = bounds[i - 1];
+      }
+      let dmax = domain[1];
+      if (i < bounds.length) {
+        dmax = bounds[i];
+      }
+
+      const rmin = encode[2 * i];
+      const rmax = encode[2 * i + 1];
+
+      // Prevent the value from becoming NaN as a result
+      // of division by zero (fixes issue6113.pdf).
+      tmpBuf[0] =
+        dmin === dmax
+          ? rmin
+          : rmin + ((v - dmin) * (rmax - rmin)) / (dmax - dmin);
+
+      // call the appropriate function
+      fns[i](tmpBuf, 0, dest, destOffset);
+    };
+  }
+
+  static constructPostScript({ xref, isEvalSupported, fn, dict }) {
+    const domain = toNumberArray(dict.getArray("Domain"));
+    const range = toNumberArray(dict.getArray("Range"));
+
+    if (!domain) {
+      throw new FormatError("No domain.");
+    }
+
+    if (!range) {
+      throw new FormatError("No range.");
+    }
+
+    const lexer = new PostScriptLexer(fn);
+    const parser = new PostScriptParser(lexer);
+    const code = parser.parse();
+
+    if (isEvalSupported && IsEvalSupportedCached.value) {
+      const compiled = new PostScriptCompiler().compile(code, domain, range);
+      if (compiled) {
+        // Compiled function consists of simple expressions such as addition,
+        // subtraction, Math.max, and also contains 'var' and 'return'
+        // statements. See the generation in the PostScriptCompiler below.
+        // eslint-disable-next-line no-new-func
+        return new Function("src", "srcOffset", "dest", "destOffset", compiled);
+      }
+    }
+    info("Unable to compile PS function");
+
+    const numOutputs = range.length >> 1;
+    const numInputs = domain.length >> 1;
+    const evaluator = new PostScriptEvaluator(code);
+    // Cache the values for a big speed up, the cache size is limited though
+    // since the number of possible values can be huge from a PS function.
+    const cache = Object.create(null);
+    // The MAX_CACHE_SIZE is set to ~4x the maximum number of distinct values
+    // seen in our tests.
+    const MAX_CACHE_SIZE = 2048 * 4;
+    let cache_available = MAX_CACHE_SIZE;
+    const tmpBuf = new Float32Array(numInputs);
+
+    return function constructPostScriptFn(src, srcOffset, dest, destOffset) {
+      let i, value;
+      let key = "";
+      const input = tmpBuf;
+      for (i = 0; i < numInputs; i++) {
+        value = src[srcOffset + i];
+        input[i] = value;
+        key += value + "_";
+      }
+
+      const cachedValue = cache[key];
+      if (cachedValue !== undefined) {
+        dest.set(cachedValue, destOffset);
+        return;
+      }
+
+      const output = new Float32Array(numOutputs);
+      const stack = evaluator.execute(input);
+      const stackIndex = stack.length - numOutputs;
+      for (i = 0; i < numOutputs; i++) {
+        value = stack[stackIndex + i];
+        let bound = range[i * 2];
+        if (value < bound) {
+          value = bound;
+        } else {
+          bound = range[i * 2 + 1];
+          if (value > bound) {
+            value = bound;
+          }
+        }
+        output[i] = value;
+      }
+      if (cache_available > 0) {
+        cache_available--;
+        cache[key] = output;
+      }
+      dest.set(output, destOffset);
+    };
+  }
+}
 
 function isPDFFunction(v) {
   let fnDict;