diff --git a/src/core/function.js b/src/core/function.js index b7e3e6ccf..075a9970a 100644 --- a/src/core/function.js +++ b/src/core/function.js @@ -131,492 +131,377 @@ function toNumberArray(arr) { return arr; } -const PDFFunction = (function PDFFunctionClosure() { - const CONSTRUCT_SAMPLED = 0; - const CONSTRUCT_INTERPOLATED = 2; - const CONSTRUCT_STICHED = 3; - const CONSTRUCT_POSTSCRIPT = 4; +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; - return { - getSampleArray(size, outputSize, bps, stream) { - let i, ii; - let length = 1; - for (i = 0, ii = size.length; i < ii; i++) { - length *= size[i]; + 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; - }, + }; + } - getIR({ xref, isEvalSupported, fn }) { - let dict = fn.dict; - if (!dict) { - dict = fn; + 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]]; + } + 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)); + } + + 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 types = [ - this.constructSampled, - null, - this.constructInterpolated, - this.constructStiched, - this.constructPostScript, - ]; - - const typeNum = dict.get("FunctionType"); - const typeFn = types[typeNum]; - if (!typeFn) { - throw new FormatError("Unknown type of function"); - } - - return typeFn.call(this, { xref, isEvalSupported, fn, dict }); - }, - - fromIR({ xref, isEvalSupported, IR }) { - const type = IR[0]; - switch (type) { - case CONSTRUCT_SAMPLED: - return this.constructSampledFromIR({ xref, isEvalSupported, IR }); - case CONSTRUCT_INTERPOLATED: - return this.constructInterpolatedFromIR({ - xref, - isEvalSupported, - IR, - }); - case CONSTRUCT_STICHED: - return this.constructStichedFromIR({ xref, isEvalSupported, IR }); - // case CONSTRUCT_POSTSCRIPT: - default: - return this.constructPostScriptFromIR({ xref, isEvalSupported, IR }); - } - }, - - parse({ xref, isEvalSupported, fn }) { - const IR = this.getIR({ xref, isEvalSupported, fn }); - return this.fromIR({ xref, isEvalSupported, IR }); - }, - - 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]) }) + 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]]; - ++index; - } - return out; - } - let domain = toNumberArray(dict.getArray("Domain")); - let range = toNumberArray(dict.getArray("Range")); + // 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] + ); - if (!domain || !range) { - throw new FormatError("No domain or 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); - 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); - - return [ - CONSTRUCT_SAMPLED, - inputSize, - domain, - encode, - decode, - samples, - size, - outputSize, - 2 ** bps - 1, - range, - ]; - }, - - constructSampledFromIR({ xref, isEvalSupported, IR }) { - // See chapter 3, page 109 of the PDF reference - function interpolate(x, xmin, xmax, ymin, ymax) { - return ymin + (x - xmin) * ((ymax - ymin) / (xmax - xmin)); - } - - return function constructSampledFromIRResult( - src, - srcOffset, - dest, - destOffset - ) { - // See chapter 3, page 110 of the PDF reference. - const m = IR[1]; - const domain = IR[2]; - const encode = IR[3]; - const decode = IR[4]; - const samples = IR[5]; - const size = IR[6]; - const n = IR[7]; - // var mask = IR[8]; - const range = IR[9]; - - // Building the cube vertices: its part and sample index - // http://rjwagner49.com/Mathematics/Interpolation.pdf - const cubeVertices = 1 << m; - 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 = n, - pos = 1; - // Map x_i to y_j for 0 <= i < m using the sampled function. - for (i = 0; i < m; ++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 < n; ++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, fn, dict }) { - const c0 = toNumberArray(dict.getArray("C0")) || [0]; - const c1 = toNumberArray(dict.getArray("C1")) || [1]; - const n = dict.get("N"); - - const length = c0.length; - const diff = []; - for (let i = 0; i < length; ++i) { - diff.push(c1[i] - c0[i]); - } - - return [CONSTRUCT_INTERPOLATED, c0, diff, n]; - }, - - constructInterpolatedFromIR({ xref, isEvalSupported, IR }) { - const c0 = IR[1]; - const diff = IR[2]; - const n = IR[3]; - - const length = diff.length; - - return function constructInterpolatedFromIRResult( - 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, fn, 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")); - - return [CONSTRUCT_STICHED, domain, bounds, encode, fns]; - }, - - constructStichedFromIR({ xref, isEvalSupported, IR }) { - const domain = IR[1]; - const bounds = IR[2]; - const encode = IR[3]; - const fns = IR[4]; - const tmpBuf = new Float32Array(1); - - return function constructStichedFromIRResult( - 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(); - - return [CONSTRUCT_POSTSCRIPT, domain, range, code]; - }, - - constructPostScriptFromIR({ xref, isEvalSupported, IR }) { - const domain = IR[1]; - const range = IR[2]; - const code = IR[3]; - - 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 constructPostScriptFromIRResult( - 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;