Add (basic) support for transfer functions to Images (issue 6931, bug 1149713)

This is *similar* to the existing transfer function support for SMasks, but extended to simple image data.
Please note that the extra amount of data now being sent to the worker-thread, for affected /ExtGState entries, is limited to *at most* 4 `Uint8Array`s each with a length of 256 elements.

Refer to https://www.adobe.com/content/dam/acom/en/devnet/acrobat/pdfs/PDF32000_2008.pdf#G9.1658137 for additional details.
This commit is contained in:
Jonas Jenwald 2020-08-17 08:49:19 +02:00
parent 9d3e046a4f
commit 1058f16605
5 changed files with 143 additions and 6 deletions

View File

@ -668,6 +668,51 @@ class PartialEvaluator {
);
}
handleTransferFunction(tr) {
let transferArray;
if (Array.isArray(tr)) {
transferArray = tr;
} else if (isPDFFunction(tr)) {
transferArray = [tr];
} else {
return null; // Not a valid transfer function entry.
}
const transferMaps = [];
let numFns = 0,
numEffectfulFns = 0;
for (const entry of transferArray) {
const transferObj = this.xref.fetchIfRef(entry);
numFns++;
if (isName(transferObj, "Identity")) {
transferMaps.push(null);
continue;
} else if (!isPDFFunction(transferObj)) {
return null; // Not a valid transfer function object.
}
const transferFn = this._pdfFunctionFactory.create(transferObj);
const transferMap = new Uint8Array(256),
tmp = new Float32Array(1);
for (let j = 0; j < 256; j++) {
tmp[0] = j / 255;
transferFn(tmp, 0, tmp, 0);
transferMap[j] = (tmp[0] * 255) | 0;
}
transferMaps.push(transferMap);
numEffectfulFns++;
}
if (!(numFns === 1 || numFns === 4)) {
return null; // Only 1 or 4 functions are supported, by the specification.
}
if (numEffectfulFns === 0) {
return null; // Only /Identity transfer functions found, which are no-ops.
}
return transferMaps;
}
handleTilingType(
fn,
args,
@ -887,7 +932,10 @@ class PartialEvaluator {
} else {
warn("Unsupported SMask type");
}
break;
case "TR":
const transferMaps = this.handleTransferFunction(value);
gStateObj.push([key, transferMaps]);
break;
// Only generate info log messages for the following since
// they are unlikely to have a big impact on the rendering.
@ -898,7 +946,6 @@ class PartialEvaluator {
case "BG2":
case "UCR":
case "UCR2":
case "TR":
case "TR2":
case "HT":
case "SM":

View File

@ -416,6 +416,7 @@ var CanvasExtraState = (function CanvasExtraStateClosure() {
this.lineWidth = 1;
this.activeSMask = null;
this.resumeSMaskCtx = null; // nonclonable field (see the save method below)
this.transferMaps = null;
}
CanvasExtraState.prototype = {
@ -484,7 +485,7 @@ var CanvasGraphics = (function CanvasGraphicsClosure() {
this._cachedGetSinglePixelWidth = null;
}
function putBinaryImageData(ctx, imgData) {
function putBinaryImageData(ctx, imgData, transferMaps = null) {
if (typeof ImageData !== "undefined" && imgData instanceof ImageData) {
ctx.putImageData(imgData, 0, 0);
return;
@ -514,6 +515,24 @@ var CanvasGraphics = (function CanvasGraphicsClosure() {
var dest = chunkImgData.data;
var i, j, thisChunkHeight, elemsInThisChunk;
let transferMapRed, transferMapGreen, transferMapBlue, transferMapGray;
if (transferMaps) {
switch (transferMaps.length) {
case 1:
transferMapRed = transferMaps[0];
transferMapGreen = transferMaps[0];
transferMapBlue = transferMaps[0];
transferMapGray = transferMaps[0];
break;
case 4:
transferMapRed = transferMaps[0];
transferMapGreen = transferMaps[1];
transferMapBlue = transferMaps[2];
transferMapGray = transferMaps[3];
break;
}
}
// There are multiple forms in which the pixel data can be passed, and
// imgData.kind tells us which one this is.
if (imgData.kind === ImageKind.GRAYSCALE_1BPP) {
@ -524,13 +543,20 @@ var CanvasGraphics = (function CanvasGraphicsClosure() {
var fullSrcDiff = (width + 7) >> 3;
var white = 0xffffffff;
var black = IsLittleEndianCached.value ? 0xff000000 : 0x000000ff;
if (transferMapGray) {
if (transferMapGray[0] === 0xff && transferMapGray[0xff] === 0) {
[white, black] = [black, white];
}
}
for (i = 0; i < totalChunks; i++) {
thisChunkHeight =
i < fullChunks ? FULL_CHUNK_HEIGHT : partialChunkHeight;
destPos = 0;
for (j = 0; j < thisChunkHeight; j++) {
var srcDiff = srcLength - srcPos;
var k = 0;
let k = 0;
var kEnd = srcDiff > fullSrcDiff ? width : srcDiff * 8 - 7;
var kEndUnrolled = kEnd & ~7;
var mask = 0;
@ -565,6 +591,11 @@ var CanvasGraphics = (function CanvasGraphicsClosure() {
}
} else if (imgData.kind === ImageKind.RGBA_32BPP) {
// RGBA, 32-bits per pixel.
const hasTransferMaps = !!(
transferMapRed ||
transferMapGreen ||
transferMapBlue
);
j = 0;
elemsInThisChunk = width * FULL_CHUNK_HEIGHT * 4;
@ -572,16 +603,51 @@ var CanvasGraphics = (function CanvasGraphicsClosure() {
dest.set(src.subarray(srcPos, srcPos + elemsInThisChunk));
srcPos += elemsInThisChunk;
if (hasTransferMaps) {
for (let k = 0; k < elemsInThisChunk; k += 4) {
if (transferMapRed) {
dest[k + 0] = transferMapRed[dest[k + 0]];
}
if (transferMapGreen) {
dest[k + 1] = transferMapGreen[dest[k + 1]];
}
if (transferMapBlue) {
dest[k + 2] = transferMapBlue[dest[k + 2]];
}
}
}
ctx.putImageData(chunkImgData, 0, j);
j += FULL_CHUNK_HEIGHT;
}
if (i < totalChunks) {
elemsInThisChunk = width * partialChunkHeight * 4;
dest.set(src.subarray(srcPos, srcPos + elemsInThisChunk));
if (hasTransferMaps) {
for (let k = 0; k < elemsInThisChunk; k += 4) {
if (transferMapRed) {
dest[k + 0] = transferMapRed[dest[k + 0]];
}
if (transferMapGreen) {
dest[k + 1] = transferMapGreen[dest[k + 1]];
}
if (transferMapBlue) {
dest[k + 2] = transferMapBlue[dest[k + 2]];
}
}
}
ctx.putImageData(chunkImgData, 0, j);
}
} else if (imgData.kind === ImageKind.RGB_24BPP) {
// RGB, 24-bits per pixel.
const hasTransferMaps = !!(
transferMapRed ||
transferMapGreen ||
transferMapBlue
);
thisChunkHeight = FULL_CHUNK_HEIGHT;
elemsInThisChunk = width * thisChunkHeight;
for (i = 0; i < totalChunks; i++) {
@ -597,6 +663,21 @@ var CanvasGraphics = (function CanvasGraphicsClosure() {
dest[destPos++] = src[srcPos++];
dest[destPos++] = 255;
}
if (hasTransferMaps) {
for (let k = 0; k < destPos; k += 4) {
if (transferMapRed) {
dest[k + 0] = transferMapRed[dest[k + 0]];
}
if (transferMapGreen) {
dest[k + 1] = transferMapGreen[dest[k + 1]];
}
if (transferMapBlue) {
dest[k + 2] = transferMapBlue[dest[k + 2]];
}
}
}
ctx.putImageData(chunkImgData, 0, i * FULL_CHUNK_HEIGHT);
}
} else {
@ -1040,6 +1121,8 @@ var CanvasGraphics = (function CanvasGraphicsClosure() {
}
this.tempSMask = null;
break;
case "TR":
this.current.transferMaps = value;
}
}
},
@ -2362,7 +2445,7 @@ var CanvasGraphics = (function CanvasGraphicsClosure() {
} else {
tmpCanvas = this.cachedCanvases.getCanvas("inlineImage", width, height);
var tmpCtx = tmpCanvas.context;
putBinaryImageData(tmpCtx, imgData);
putBinaryImageData(tmpCtx, imgData, this.current.transferMaps);
imgToPaint = tmpCanvas.canvas;
}
@ -2447,7 +2530,7 @@ var CanvasGraphics = (function CanvasGraphicsClosure() {
var tmpCanvas = this.cachedCanvases.getCanvas("inlineImage", w, h);
var tmpCtx = tmpCanvas.context;
putBinaryImageData(tmpCtx, imgData);
putBinaryImageData(tmpCtx, imgData, this.current.transferMaps);
for (var i = 0, ii = map.length; i < ii; i++) {
var entry = map[i];

View File

@ -45,6 +45,7 @@
!issue7492.pdf
!issue7544.pdf
!issue7507.pdf
!issue6931_reduced.pdf
!issue7580.pdf
!issue7598.pdf
!issue7665.pdf

Binary file not shown.

View File

@ -4468,6 +4468,12 @@
"rounds": 1,
"type": "eq"
},
{ "id": "issue6931",
"file": "pdfs/issue6931_reduced.pdf",
"md5": "e61388913821a5e044bf85a5846d6d9a",
"rounds": 1,
"type": "eq"
},
{ "id": "annotation-button-widget-annotations",
"file": "pdfs/annotation-button-widget.pdf",
"md5": "5cf23adfff84256d9cfe261bea96dade",