/** * @licstart The following is the entire license notice for the * JavaScript code in this page * * Copyright 2022 Mozilla Foundation * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * @licend The above is the entire license notice for the * JavaScript code in this page */ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.PDFImage = void 0; var _util = require("../shared/util.js"); var _image_utils = require("../shared/image_utils.js"); var _base_stream = require("./base_stream.js"); var _colorspace = require("./colorspace.js"); var _decode_stream = require("./decode_stream.js"); var _jpeg_stream = require("./jpeg_stream.js"); var _jpx = require("./jpx.js"); var _primitives = require("./primitives.js"); function decodeAndClamp(value, addend, coefficient, max) { value = addend + value * coefficient; if (value < 0) { value = 0; } else if (value > max) { value = max; } return value; } function resizeImageMask(src, bpc, w1, h1, w2, h2) { const length = w2 * h2; let dest; if (bpc <= 8) { dest = new Uint8Array(length); } else if (bpc <= 16) { dest = new Uint16Array(length); } else { dest = new Uint32Array(length); } const xRatio = w1 / w2; const yRatio = h1 / h2; let i, j, py, newIndex = 0, oldIndex; const xScaled = new Uint16Array(w2); const w1Scanline = w1; for (i = 0; i < w2; i++) { xScaled[i] = Math.floor(i * xRatio); } for (i = 0; i < h2; i++) { py = Math.floor(i * yRatio) * w1Scanline; for (j = 0; j < w2; j++) { oldIndex = py + xScaled[j]; dest[newIndex++] = src[oldIndex]; } } return dest; } class PDFImage { constructor({ xref, res, image, isInline = false, smask = null, mask = null, isMask = false, pdfFunctionFactory, localColorSpaceCache }) { this.image = image; const dict = image.dict; const filter = dict.get("F", "Filter"); let filterName; if (filter instanceof _primitives.Name) { filterName = filter.name; } else if (Array.isArray(filter)) { const filterZero = xref.fetchIfRef(filter[0]); if (filterZero instanceof _primitives.Name) { filterName = filterZero.name; } } switch (filterName) { case "JPXDecode": const jpxImage = new _jpx.JpxImage(); jpxImage.parseImageProperties(image.stream); image.stream.reset(); image.width = jpxImage.width; image.height = jpxImage.height; image.bitsPerComponent = jpxImage.bitsPerComponent; image.numComps = jpxImage.componentsCount; break; case "JBIG2Decode": image.bitsPerComponent = 1; image.numComps = 1; break; } let width = dict.get("W", "Width"); let height = dict.get("H", "Height"); if (Number.isInteger(image.width) && image.width > 0 && Number.isInteger(image.height) && image.height > 0 && (image.width !== width || image.height !== height)) { (0, _util.warn)("PDFImage - using the Width/Height of the image data, " + "rather than the image dictionary."); width = image.width; height = image.height; } if (width < 1 || height < 1) { throw new _util.FormatError(`Invalid image width: ${width} or height: ${height}`); } this.width = width; this.height = height; this.interpolate = dict.get("I", "Interpolate"); this.imageMask = dict.get("IM", "ImageMask") || false; this.matte = dict.get("Matte") || false; let bitsPerComponent = image.bitsPerComponent; if (!bitsPerComponent) { bitsPerComponent = dict.get("BPC", "BitsPerComponent"); if (!bitsPerComponent) { if (this.imageMask) { bitsPerComponent = 1; } else { throw new _util.FormatError(`Bits per component missing in image: ${this.imageMask}`); } } } this.bpc = bitsPerComponent; if (!this.imageMask) { let colorSpace = dict.getRaw("CS") || dict.getRaw("ColorSpace"); if (!colorSpace) { (0, _util.info)("JPX images (which do not require color spaces)"); switch (image.numComps) { case 1: colorSpace = _primitives.Name.get("DeviceGray"); break; case 3: colorSpace = _primitives.Name.get("DeviceRGB"); break; case 4: colorSpace = _primitives.Name.get("DeviceCMYK"); break; default: throw new Error(`JPX images with ${image.numComps} color components not supported.`); } } this.colorSpace = _colorspace.ColorSpace.parse({ cs: colorSpace, xref, resources: isInline ? res : null, pdfFunctionFactory, localColorSpaceCache }); this.numComps = this.colorSpace.numComps; } this.decode = dict.getArray("D", "Decode"); this.needsDecode = false; if (this.decode && (this.colorSpace && !this.colorSpace.isDefaultDecode(this.decode, bitsPerComponent) || isMask && !_colorspace.ColorSpace.isDefaultDecode(this.decode, 1))) { this.needsDecode = true; const max = (1 << bitsPerComponent) - 1; this.decodeCoefficients = []; this.decodeAddends = []; const isIndexed = this.colorSpace && this.colorSpace.name === "Indexed"; for (let i = 0, j = 0; i < this.decode.length; i += 2, ++j) { const dmin = this.decode[i]; const dmax = this.decode[i + 1]; this.decodeCoefficients[j] = isIndexed ? (dmax - dmin) / max : dmax - dmin; this.decodeAddends[j] = isIndexed ? dmin : max * dmin; } } if (smask) { this.smask = new PDFImage({ xref, res, image: smask, isInline, pdfFunctionFactory, localColorSpaceCache }); } else if (mask) { if (mask instanceof _base_stream.BaseStream) { const maskDict = mask.dict, imageMask = maskDict.get("IM", "ImageMask"); if (!imageMask) { (0, _util.warn)("Ignoring /Mask in image without /ImageMask."); } else { this.mask = new PDFImage({ xref, res, image: mask, isInline, isMask: true, pdfFunctionFactory, localColorSpaceCache }); } } else { this.mask = mask; } } } static async buildImage({ xref, res, image, isInline = false, pdfFunctionFactory, localColorSpaceCache }) { const imageData = image; let smaskData = null; let maskData = null; const smask = image.dict.get("SMask"); const mask = image.dict.get("Mask"); if (smask) { if (smask instanceof _base_stream.BaseStream) { smaskData = smask; } else { (0, _util.warn)("Unsupported /SMask format."); } } else if (mask) { if (mask instanceof _base_stream.BaseStream || Array.isArray(mask)) { maskData = mask; } else { (0, _util.warn)("Unsupported /Mask format."); } } return new PDFImage({ xref, res, image: imageData, isInline, smask: smaskData, mask: maskData, pdfFunctionFactory, localColorSpaceCache }); } static createRawMask({ imgArray, width, height, imageIsFromDecodeStream, inverseDecode, interpolate }) { const computedLength = (width + 7 >> 3) * height; const actualLength = imgArray.byteLength; const haveFullData = computedLength === actualLength; let data, i; if (imageIsFromDecodeStream && (!inverseDecode || haveFullData)) { data = imgArray; } else if (!inverseDecode) { data = new Uint8Array(imgArray); } else { data = new Uint8Array(computedLength); data.set(imgArray); data.fill(0xff, actualLength); } if (inverseDecode) { for (i = 0; i < actualLength; i++) { data[i] ^= 0xff; } } return { data, width, height, interpolate }; } static createMask({ imgArray, width, height, imageIsFromDecodeStream, inverseDecode, interpolate }) { const isSingleOpaquePixel = width === 1 && height === 1 && inverseDecode === (imgArray.length === 0 || !!(imgArray[0] & 128)); if (isSingleOpaquePixel) { return { isSingleOpaquePixel }; } if (_util.FeatureTest.isOffscreenCanvasSupported) { const canvas = new OffscreenCanvas(width, height); const ctx = canvas.getContext("2d"); const imgData = ctx.createImageData(width, height); (0, _image_utils.applyMaskImageData)({ src: imgArray, dest: imgData.data, width, height, inverseDecode }); ctx.putImageData(imgData, 0, 0); const bitmap = canvas.transferToImageBitmap(); return { data: null, width, height, interpolate, bitmap }; } return this.createRawMask({ imgArray, width, height, inverseDecode, imageIsFromDecodeStream, interpolate }); } get drawWidth() { return Math.max(this.width, this.smask && this.smask.width || 0, this.mask && this.mask.width || 0); } get drawHeight() { return Math.max(this.height, this.smask && this.smask.height || 0, this.mask && this.mask.height || 0); } decodeBuffer(buffer) { const bpc = this.bpc; const numComps = this.numComps; const decodeAddends = this.decodeAddends; const decodeCoefficients = this.decodeCoefficients; const max = (1 << bpc) - 1; let i, ii; if (bpc === 1) { for (i = 0, ii = buffer.length; i < ii; i++) { buffer[i] = +!buffer[i]; } return; } let index = 0; for (i = 0, ii = this.width * this.height; i < ii; i++) { for (let j = 0; j < numComps; j++) { buffer[index] = decodeAndClamp(buffer[index], decodeAddends[j], decodeCoefficients[j], max); index++; } } } getComponents(buffer) { const bpc = this.bpc; if (bpc === 8) { return buffer; } const width = this.width; const height = this.height; const numComps = this.numComps; const length = width * height * numComps; let bufferPos = 0; let output; if (bpc <= 8) { output = new Uint8Array(length); } else if (bpc <= 16) { output = new Uint16Array(length); } else { output = new Uint32Array(length); } const rowComps = width * numComps; const max = (1 << bpc) - 1; let i = 0, ii, buf; if (bpc === 1) { let mask, loop1End, loop2End; for (let j = 0; j < height; j++) { loop1End = i + (rowComps & ~7); loop2End = i + rowComps; while (i < loop1End) { buf = buffer[bufferPos++]; output[i] = buf >> 7 & 1; output[i + 1] = buf >> 6 & 1; output[i + 2] = buf >> 5 & 1; output[i + 3] = buf >> 4 & 1; output[i + 4] = buf >> 3 & 1; output[i + 5] = buf >> 2 & 1; output[i + 6] = buf >> 1 & 1; output[i + 7] = buf & 1; i += 8; } if (i < loop2End) { buf = buffer[bufferPos++]; mask = 128; while (i < loop2End) { output[i++] = +!!(buf & mask); mask >>= 1; } } } } else { let bits = 0; buf = 0; for (i = 0, ii = length; i < ii; ++i) { if (i % rowComps === 0) { buf = 0; bits = 0; } while (bits < bpc) { buf = buf << 8 | buffer[bufferPos++]; bits += 8; } const remainingBits = bits - bpc; let value = buf >> remainingBits; if (value < 0) { value = 0; } else if (value > max) { value = max; } output[i] = value; buf &= (1 << remainingBits) - 1; bits = remainingBits; } } return output; } fillOpacity(rgbaBuf, width, height, actualHeight, image) { const smask = this.smask; const mask = this.mask; let alphaBuf, sw, sh, i, ii, j; if (smask) { sw = smask.width; sh = smask.height; alphaBuf = new Uint8ClampedArray(sw * sh); smask.fillGrayBuffer(alphaBuf); if (sw !== width || sh !== height) { alphaBuf = resizeImageMask(alphaBuf, smask.bpc, sw, sh, width, height); } } else if (mask) { if (mask instanceof PDFImage) { sw = mask.width; sh = mask.height; alphaBuf = new Uint8ClampedArray(sw * sh); mask.numComps = 1; mask.fillGrayBuffer(alphaBuf); for (i = 0, ii = sw * sh; i < ii; ++i) { alphaBuf[i] = 255 - alphaBuf[i]; } if (sw !== width || sh !== height) { alphaBuf = resizeImageMask(alphaBuf, mask.bpc, sw, sh, width, height); } } else if (Array.isArray(mask)) { alphaBuf = new Uint8ClampedArray(width * height); const numComps = this.numComps; for (i = 0, ii = width * height; i < ii; ++i) { let opacity = 0; const imageOffset = i * numComps; for (j = 0; j < numComps; ++j) { const color = image[imageOffset + j]; const maskOffset = j * 2; if (color < mask[maskOffset] || color > mask[maskOffset + 1]) { opacity = 255; break; } } alphaBuf[i] = opacity; } } else { throw new _util.FormatError("Unknown mask format."); } } if (alphaBuf) { for (i = 0, j = 3, ii = width * actualHeight; i < ii; ++i, j += 4) { rgbaBuf[j] = alphaBuf[i]; } } else { for (i = 0, j = 3, ii = width * actualHeight; i < ii; ++i, j += 4) { rgbaBuf[j] = 255; } } } undoPreblend(buffer, width, height) { const matte = this.smask && this.smask.matte; if (!matte) { return; } const matteRgb = this.colorSpace.getRgb(matte, 0); const matteR = matteRgb[0]; const matteG = matteRgb[1]; const matteB = matteRgb[2]; const length = width * height * 4; for (let i = 0; i < length; i += 4) { const alpha = buffer[i + 3]; if (alpha === 0) { buffer[i] = 255; buffer[i + 1] = 255; buffer[i + 2] = 255; continue; } const k = 255 / alpha; buffer[i] = (buffer[i] - matteR) * k + matteR; buffer[i + 1] = (buffer[i + 1] - matteG) * k + matteG; buffer[i + 2] = (buffer[i + 2] - matteB) * k + matteB; } } createImageData(forceRGBA = false) { const drawWidth = this.drawWidth; const drawHeight = this.drawHeight; const imgData = { width: drawWidth, height: drawHeight, interpolate: this.interpolate, kind: 0, data: null }; const numComps = this.numComps; const originalWidth = this.width; const originalHeight = this.height; const bpc = this.bpc; const rowBytes = originalWidth * numComps * bpc + 7 >> 3; if (!forceRGBA) { let kind; if (this.colorSpace.name === "DeviceGray" && bpc === 1) { kind = _util.ImageKind.GRAYSCALE_1BPP; } else if (this.colorSpace.name === "DeviceRGB" && bpc === 8 && !this.needsDecode) { kind = _util.ImageKind.RGB_24BPP; } if (kind && !this.smask && !this.mask && drawWidth === originalWidth && drawHeight === originalHeight) { imgData.kind = kind; imgData.data = this.getImageBytes(originalHeight * rowBytes, {}); if (this.needsDecode) { (0, _util.assert)(kind === _util.ImageKind.GRAYSCALE_1BPP, "PDFImage.createImageData: The image must be grayscale."); const buffer = imgData.data; for (let i = 0, ii = buffer.length; i < ii; i++) { buffer[i] ^= 0xff; } } return imgData; } if (this.image instanceof _jpeg_stream.JpegStream && !this.smask && !this.mask) { let imageLength = originalHeight * rowBytes; switch (this.colorSpace.name) { case "DeviceGray": imageLength *= 3; case "DeviceRGB": case "DeviceCMYK": imgData.kind = _util.ImageKind.RGB_24BPP; imgData.data = this.getImageBytes(imageLength, { drawWidth, drawHeight, forceRGB: true }); return imgData; } } } const imgArray = this.getImageBytes(originalHeight * rowBytes, { internal: true }); const actualHeight = 0 | imgArray.length / rowBytes * drawHeight / originalHeight; const comps = this.getComponents(imgArray); let alpha01, maybeUndoPreblend; if (!forceRGBA && !this.smask && !this.mask) { imgData.kind = _util.ImageKind.RGB_24BPP; imgData.data = new Uint8ClampedArray(drawWidth * drawHeight * 3); alpha01 = 0; maybeUndoPreblend = false; } else { imgData.kind = _util.ImageKind.RGBA_32BPP; imgData.data = new Uint8ClampedArray(drawWidth * drawHeight * 4); alpha01 = 1; maybeUndoPreblend = true; this.fillOpacity(imgData.data, drawWidth, drawHeight, actualHeight, comps); } if (this.needsDecode) { this.decodeBuffer(comps); } this.colorSpace.fillRgb(imgData.data, originalWidth, originalHeight, drawWidth, drawHeight, actualHeight, bpc, comps, alpha01); if (maybeUndoPreblend) { this.undoPreblend(imgData.data, drawWidth, actualHeight); } return imgData; } fillGrayBuffer(buffer) { const numComps = this.numComps; if (numComps !== 1) { throw new _util.FormatError(`Reading gray scale from a color image: ${numComps}`); } const width = this.width; const height = this.height; const bpc = this.bpc; const rowBytes = width * numComps * bpc + 7 >> 3; const imgArray = this.getImageBytes(height * rowBytes, { internal: true }); const comps = this.getComponents(imgArray); let i, length; if (bpc === 1) { length = width * height; if (this.needsDecode) { for (i = 0; i < length; ++i) { buffer[i] = comps[i] - 1 & 255; } } else { for (i = 0; i < length; ++i) { buffer[i] = -comps[i] & 255; } } return; } if (this.needsDecode) { this.decodeBuffer(comps); } length = width * height; const scale = 255 / ((1 << bpc) - 1); for (i = 0; i < length; ++i) { buffer[i] = scale * comps[i]; } } getImageBytes(length, { drawWidth, drawHeight, forceRGB = false, internal = false }) { this.image.reset(); this.image.drawWidth = drawWidth || this.width; this.image.drawHeight = drawHeight || this.height; this.image.forceRGB = !!forceRGB; const imageBytes = this.image.getBytes(length); if (internal || this.image instanceof _decode_stream.DecodeStream) { return imageBytes; } (0, _util.assert)(imageBytes instanceof Uint8Array, 'PDFImage.getImageBytes: Unsupported "imageBytes" type.'); return new Uint8Array(imageBytes); } } exports.PDFImage = PDFImage;