Merge pull request #16062 from calixteman/create_image_in_worker

[api-minor] Generate images in the worker instead of the main thread.
This commit is contained in:
calixteman 2023-03-02 13:34:50 +01:00 committed by GitHub
commit d4216264e8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 700 additions and 89 deletions

View File

@ -425,6 +425,8 @@ class Page {
this.resources,
this.nonBlendModesSet
),
isOffscreenCanvasSupported:
this.evaluatorOptions.isOffscreenCanvasSupported,
pageIndex: this.pageIndex,
cacheKey,
});

View File

@ -716,7 +716,12 @@ class PartialEvaluator {
});
// We force the use of RGBA_32BPP images here, because we can't handle
// any other kind.
imgData = imageObj.createImageData(/* forceRGBA = */ true);
imgData = imageObj.createImageData(
/* forceRGBA = */ true,
/* isOffscreenCanvasSupported = */ false
);
operatorList.isOffscreenCanvasSupported =
this.options.isOffscreenCanvasSupported;
operatorList.addImageOps(
OPS.paintInlineImageXObject,
[imgData],
@ -756,11 +761,22 @@ class PartialEvaluator {
localColorSpaceCache,
})
.then(imageObj => {
imgData = imageObj.createImageData(/* forceRGBA = */ false);
imgData = imageObj.createImageData(
/* forceRGBA = */ false,
/* isOffscreenCanvasSupported = */ this.options
.isOffscreenCanvasSupported
);
if (cacheKey && imageRef && cacheGlobally) {
this.globalImageCache.addByteSize(imageRef, imgData.data.length);
let length = 0;
if (imgData.bitmap) {
length = imgData.width * imgData.height * 4;
} else {
length = imgData.data.length;
}
this.globalImageCache.addByteSize(imageRef, length);
}
return this._sendImgData(objId, imgData, cacheGlobally);
})
.catch(reason => {

View File

@ -13,8 +13,18 @@
* limitations under the License.
*/
import { assert, FormatError, ImageKind, info, warn } from "../shared/util.js";
import { applyMaskImageData } from "../shared/image_utils.js";
import {
assert,
FeatureTest,
FormatError,
ImageKind,
info,
warn,
} from "../shared/util.js";
import {
convertBlackAndWhiteToRGBA,
convertToRGBA,
} from "../shared/image_utils.js";
import { BaseStream } from "./base_stream.js";
import { ColorSpace } from "./colorspace.js";
import { DecodeStream } from "./decode_stream.js";
@ -364,11 +374,12 @@ class PDFImage {
const canvas = new OffscreenCanvas(width, height);
const ctx = canvas.getContext("2d");
const imgData = ctx.createImageData(width, height);
applyMaskImageData({
convertBlackAndWhiteToRGBA({
src: imgArray,
dest: imgData.data,
width,
height,
nonBlackColor: 0,
inverseDecode,
});
@ -641,7 +652,7 @@ class PDFImage {
}
}
createImageData(forceRGBA = false) {
createImageData(forceRGBA = false, isOffscreenCanvasSupported = false) {
const drawWidth = this.drawWidth;
const drawHeight = this.drawHeight;
const imgData = {
@ -686,8 +697,12 @@ class PDFImage {
drawWidth === originalWidth &&
drawHeight === originalHeight
) {
const data = this.getImageBytes(originalHeight * rowBytes, {});
if (isOffscreenCanvasSupported) {
return this.createBitmap(kind, originalWidth, originalHeight, data);
}
imgData.kind = kind;
imgData.data = this.getImageBytes(originalHeight * rowBytes, {});
imgData.data = data;
if (this.needsDecode) {
// Invert the buffer (which must be grayscale if we reached here).
@ -704,21 +719,52 @@ class PDFImage {
}
if (this.image instanceof JpegStream && !this.smask && !this.mask) {
let imageLength = originalHeight * rowBytes;
switch (this.colorSpace.name) {
case "DeviceGray":
// Avoid truncating the image, since `JpegImage.getData`
// will expand the image data when `forceRGB === true`.
imageLength *= 3;
/* falls through */
case "DeviceRGB":
case "DeviceCMYK":
imgData.kind = ImageKind.RGB_24BPP;
imgData.data = this.getImageBytes(imageLength, {
if (isOffscreenCanvasSupported) {
let isHandled = false;
switch (this.colorSpace.name) {
case "DeviceGray":
// Avoid truncating the image, since `JpegImage.getData`
// will expand the image data when `forceRGB === true`.
imageLength *= 4;
isHandled = true;
break;
case "DeviceRGB":
imageLength = (imageLength / 3) * 4;
isHandled = true;
break;
case "DeviceCMYK":
isHandled = true;
break;
}
if (isHandled) {
const rgba = this.getImageBytes(imageLength, {
drawWidth,
drawHeight,
forceRGB: true,
forceRGBA: true,
});
return imgData;
return this.createBitmap(
ImageKind.RGBA_32BPP,
drawWidth,
drawHeight,
rgba
);
}
} else {
switch (this.colorSpace.name) {
case "DeviceGray":
imageLength *= 3;
/* falls through */
case "DeviceRGB":
case "DeviceCMYK":
imgData.kind = ImageKind.RGB_24BPP;
imgData.data = this.getImageBytes(imageLength, {
drawWidth,
drawHeight,
forceRGB: true,
});
return imgData;
}
}
}
}
@ -735,32 +781,45 @@ class PDFImage {
// If opacity data is present, use RGBA_32BPP form. Otherwise, use the
// more compact RGB_24BPP form if allowable.
let alpha01, maybeUndoPreblend;
let canvas, ctx, canvasImgData, data;
if (isOffscreenCanvasSupported) {
canvas = new OffscreenCanvas(drawWidth, drawHeight);
ctx = canvas.getContext("2d");
canvasImgData = ctx.createImageData(drawWidth, drawHeight);
data = canvasImgData.data;
}
imgData.kind = ImageKind.RGBA_32BPP;
if (!forceRGBA && !this.smask && !this.mask) {
imgData.kind = ImageKind.RGB_24BPP;
imgData.data = new Uint8ClampedArray(drawWidth * drawHeight * 3);
alpha01 = 0;
if (!isOffscreenCanvasSupported) {
imgData.kind = ImageKind.RGB_24BPP;
data = new Uint8ClampedArray(drawWidth * drawHeight * 3);
alpha01 = 0;
} else {
const arr = new Uint32Array(data.buffer);
arr.fill(FeatureTest.isLittleEndian ? 0xff000000 : 0x000000ff);
alpha01 = 1;
}
maybeUndoPreblend = false;
} else {
imgData.kind = ImageKind.RGBA_32BPP;
imgData.data = new Uint8ClampedArray(drawWidth * drawHeight * 4);
if (!isOffscreenCanvasSupported) {
data = new Uint8ClampedArray(drawWidth * drawHeight * 4);
}
alpha01 = 1;
maybeUndoPreblend = true;
// Color key masking (opacity) must be performed before decoding.
this.fillOpacity(
imgData.data,
drawWidth,
drawHeight,
actualHeight,
comps
);
this.fillOpacity(data, drawWidth, drawHeight, actualHeight, comps);
}
if (this.needsDecode) {
this.decodeBuffer(comps);
}
this.colorSpace.fillRgb(
imgData.data,
data,
originalWidth,
originalHeight,
drawWidth,
@ -771,9 +830,23 @@ class PDFImage {
alpha01
);
if (maybeUndoPreblend) {
this.undoPreblend(imgData.data, drawWidth, actualHeight);
this.undoPreblend(data, drawWidth, actualHeight);
}
if (isOffscreenCanvasSupported) {
ctx.putImageData(canvasImgData, 0, 0);
const bitmap = canvas.transferToImageBitmap();
return {
data: null,
width: drawWidth,
height: drawHeight,
bitmap,
interpolate: this.interpolate,
};
}
imgData.data = data;
return imgData;
}
@ -833,13 +906,49 @@ class PDFImage {
}
}
createBitmap(kind, width, height, src) {
const canvas = new OffscreenCanvas(width, height);
const ctx = canvas.getContext("2d");
let imgData;
if (kind === ImageKind.RGBA_32BPP) {
imgData = new ImageData(src, width, height);
} else {
imgData = ctx.createImageData(width, height);
convertToRGBA({
kind,
src,
dest: new Uint32Array(imgData.data.buffer),
width,
height,
inverseDecode: this.needsDecode,
});
}
ctx.putImageData(imgData, 0, 0);
const bitmap = canvas.transferToImageBitmap();
return {
data: null,
width,
height,
bitmap,
interpolate: this.interpolate,
};
}
getImageBytes(
length,
{ drawWidth, drawHeight, forceRGB = false, internal = false }
{
drawWidth,
drawHeight,
forceRGBA = false,
forceRGB = false,
internal = false,
}
) {
this.image.reset();
this.image.drawWidth = drawWidth || this.width;
this.image.drawHeight = drawHeight || this.height;
this.image.forceRGBA = !!forceRGBA;
this.image.forceRGB = !!forceRGB;
const imageBytes = this.image.getBytes(length);

View File

@ -63,7 +63,7 @@ class JpegStream extends DecodeStream {
// Checking if values need to be transformed before conversion.
const decodeArr = this.dict.getArray("D", "Decode");
if (this.forceRGB && Array.isArray(decodeArr)) {
if ((this.forceRGBA || this.forceRGB) && Array.isArray(decodeArr)) {
const bitsPerComponent = this.dict.get("BPC", "BitsPerComponent") || 8;
const decodeArrLength = decodeArr.length;
const transform = new Int32Array(decodeArrLength);
@ -93,6 +93,7 @@ class JpegStream extends DecodeStream {
const data = jpegImage.getData({
width: this.drawWidth,
height: this.drawHeight,
forceRGBA: this.forceRGBA,
forceRGB: this.forceRGB,
isSourcePDF: true,
});

View File

@ -14,6 +14,7 @@
*/
import { assert, BaseException, warn } from "../shared/util.js";
import { grayToRGBA } from "../shared/image_utils.js";
import { readUint16 } from "./core_utils.js";
class JpegError extends BaseException {
@ -1217,6 +1218,19 @@ class JpegImage {
return data;
}
_convertYccToRgba(data, out) {
for (let i = 0, j = 0, length = data.length; i < length; i += 3, j += 4) {
const Y = data[i];
const Cb = data[i + 1];
const Cr = data[i + 2];
out[j] = Y - 179.456 + 1.402 * Cr;
out[j + 1] = Y + 135.459 - 0.344 * Cb - 0.714 * Cr;
out[j + 2] = Y - 226.816 + 1.772 * Cb;
out[j + 3] = 255;
}
return out;
}
_convertYcckToRgb(data) {
let Y, Cb, Cr, k;
let offset = 0;
@ -1287,6 +1301,74 @@ class JpegImage {
return data.subarray(0, offset);
}
_convertYcckToRgba(data) {
for (let i = 0, length = data.length; i < length; i += 4) {
const Y = data[i];
const Cb = data[i + 1];
const Cr = data[i + 2];
const k = data[i + 3];
data[i] =
-122.67195406894 +
Cb *
(-6.60635669420364e-5 * Cb +
0.000437130475926232 * Cr -
5.4080610064599e-5 * Y +
0.00048449797120281 * k -
0.154362151871126) +
Cr *
(-0.000957964378445773 * Cr +
0.000817076911346625 * Y -
0.00477271405408747 * k +
1.53380253221734) +
Y *
(0.000961250184130688 * Y -
0.00266257332283933 * k +
0.48357088451265) +
k * (-0.000336197177618394 * k + 0.484791561490776);
data[i + 1] =
107.268039397724 +
Cb *
(2.19927104525741e-5 * Cb -
0.000640992018297945 * Cr +
0.000659397001245577 * Y +
0.000426105652938837 * k -
0.176491792462875) +
Cr *
(-0.000778269941513683 * Cr +
0.00130872261408275 * Y +
0.000770482631801132 * k -
0.151051492775562) +
Y *
(0.00126935368114843 * Y -
0.00265090189010898 * k +
0.25802910206845) +
k * (-0.000318913117588328 * k - 0.213742400323665);
data[i + 2] =
-20.810012546947 +
Cb *
(-0.000570115196973677 * Cb -
2.63409051004589e-5 * Cr +
0.0020741088115012 * Y -
0.00288260236853442 * k +
0.814272968359295) +
Cr *
(-1.53496057440975e-5 * Cr -
0.000132689043961446 * Y +
0.000560833691242812 * k -
0.195152027534049) +
Y *
(0.00174418132927582 * Y -
0.00255243321439347 * k +
0.116935020465145) +
k * (-0.000343531996510555 * k + 0.24165260232407);
data[i + 3] = 255;
}
return data;
}
_convertYcckToCmyk(data) {
let Y, Cb, Cr;
for (let i = 0, length = data.length; i < length; i += 4) {
@ -1371,7 +1453,81 @@ class JpegImage {
return data.subarray(0, offset);
}
getData({ width, height, forceRGB = false, isSourcePDF = false }) {
_convertCmykToRgba(data) {
for (let i = 0, length = data.length; i < length; i += 4) {
const c = data[i];
const m = data[i + 1];
const y = data[i + 2];
const k = data[i + 3];
data[i] =
255 +
c *
(-0.00006747147073602441 * c +
0.0008379262121013727 * m +
0.0002894718188643294 * y +
0.003264231057537806 * k -
1.1185611867203937) +
m *
(0.000026374107616089405 * m -
0.00008626949158638572 * y -
0.0002748769067499491 * k -
0.02155688794978967) +
y *
(-0.00003878099212869363 * y -
0.0003267808279485286 * k +
0.0686742238595345) -
k * (0.0003361971776183937 * k + 0.7430659151342254);
data[i + 1] =
255 +
c *
(0.00013596372813588848 * c +
0.000924537132573585 * m +
0.00010567359618683593 * y +
0.0004791864687436512 * k -
0.3109689587515875) +
m *
(-0.00023545346108370344 * m +
0.0002702845253534714 * y +
0.0020200308977307156 * k -
0.7488052167015494) +
y *
(0.00006834815998235662 * y +
0.00015168452363460973 * k -
0.09751927774728933) -
k * (0.0003189131175883281 * k + 0.7364883807733168);
data[i + 2] =
255 +
c *
(0.000013598650411385307 * c +
0.00012423956175490851 * m +
0.0004751985097583589 * y -
0.0000036729317476630422 * k -
0.05562186980264034) +
m *
(0.00016141380598724676 * m +
0.0009692239130725186 * y +
0.0007782692450036253 * k -
0.44015232367526463) +
y *
(5.068882914068769e-7 * y +
0.0017778369011375071 * k -
0.7591454649749609) -
k * (0.0003435319965105553 * k + 0.7063770186160144);
data[i + 3] = 255;
}
return data;
}
getData({
width,
height,
forceRGBA = false,
forceRGB = false,
isSourcePDF = false,
}) {
if (
typeof PDFJSDev === "undefined" ||
PDFJSDev.test("!PRODUCTION || TESTING")
@ -1387,23 +1543,37 @@ class JpegImage {
// Type of data: Uint8ClampedArray(width * height * numComponents)
const data = this._getLinearizedBlockData(width, height, isSourcePDF);
if (this.numComponents === 1 && forceRGB) {
const rgbData = new Uint8ClampedArray(data.length * 3);
if (this.numComponents === 1 && (forceRGBA || forceRGB)) {
const len = data.length * (forceRGBA ? 4 : 3);
const rgbaData = new Uint8ClampedArray(len);
let offset = 0;
for (const grayColor of data) {
rgbData[offset++] = grayColor;
rgbData[offset++] = grayColor;
rgbData[offset++] = grayColor;
if (forceRGBA) {
grayToRGBA(data, new Uint32Array(rgbaData.buffer));
} else {
for (const grayColor of data) {
rgbaData[offset++] = grayColor;
rgbaData[offset++] = grayColor;
rgbaData[offset++] = grayColor;
}
}
return rgbData;
return rgbaData;
} else if (this.numComponents === 3 && this._isColorConversionNeeded) {
if (forceRGBA) {
const rgbaData = new Uint8ClampedArray((data.length / 3) * 4);
return this._convertYccToRgba(data, rgbaData);
}
return this._convertYccToRgb(data);
} else if (this.numComponents === 4) {
if (this._isColorConversionNeeded) {
if (forceRGBA) {
return this._convertYcckToRgba(data);
}
if (forceRGB) {
return this._convertYcckToRgb(data);
}
return this._convertYcckToCmyk(data);
} else if (forceRGBA) {
return this._convertCmykToRgba(data);
} else if (forceRGB) {
return this._convertCmykToRgb(data);
}

View File

@ -136,17 +136,32 @@ addState(
}
}
const img = {
width: imgWidth,
height: imgHeight,
};
if (context.isOffscreenCanvasSupported) {
const canvas = new OffscreenCanvas(imgWidth, imgHeight);
const ctx = canvas.getContext("2d");
ctx.putImageData(
new ImageData(
new Uint8ClampedArray(imgData.buffer),
imgWidth,
imgHeight
),
0,
0
);
img.bitmap = canvas.transferToImageBitmap();
img.data = null;
} else {
img.kind = ImageKind.RGBA_32BPP;
img.data = imgData;
}
// 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,
]);
argsArray.splice(iFirstSave, count * 4, [img, map]);
return iFirstSave + 1;
}
@ -487,11 +502,17 @@ class QueueOptimizer extends NullOptimizer {
iCurr: 0,
fnArray: queue.fnArray,
argsArray: queue.argsArray,
isOffscreenCanvasSupported: false,
};
this.match = null;
this.lastProcessed = 0;
}
// eslint-disable-next-line accessor-pairs
set isOffscreenCanvasSupported(value) {
this.context.isOffscreenCanvasSupported = value;
}
_optimize() {
// Process new fnArray item(s) chunk.
const fnArray = this.queue.fnArray;
@ -589,6 +610,11 @@ class OperatorList {
this._resolved = streamSink ? null : Promise.resolve();
}
// eslint-disable-next-line accessor-pairs
set isOffscreenCanvasSupported(value) {
this.optimizer.isOffscreenCanvasSupported = value;
}
get length() {
return this.argsArray.length;
}

View File

@ -46,6 +46,7 @@ import {
DOMCanvasFactory,
DOMCMapReaderFactory,
DOMStandardFontDataFactory,
FilterFactory,
isDataScheme,
isValidFetchUrl,
loadScript,
@ -232,6 +233,8 @@ if (typeof PDFJSDev === "undefined" || !PDFJSDev.test("PRODUCTION")) {
* (see `web/debugger.js`). The default value is `false`.
* @property {Object} [canvasFactory] - The factory instance that will be used
* when creating canvases. The default value is {new DOMCanvasFactory()}.
* @property {Object} [filterFactory] - A factory instance that will be used
* to create SVG filters when rendering some images on the main canvas.
*/
/**
@ -341,6 +344,8 @@ function getDocument(src) {
isValidFetchUrl(standardFontDataUrl, document.baseURI));
const canvasFactory =
src.canvasFactory || new DefaultCanvasFactory({ ownerDocument });
const filterFactory =
src.filterFactory || new FilterFactory({ ownerDocument });
// Parameters only intended for development/testing purposes.
const styleElement =
@ -355,6 +360,7 @@ function getDocument(src) {
// since the user may provide *custom* ones.
const transportFactory = {
canvasFactory,
filterFactory,
};
if (!useWorkerFetch) {
transportFactory.cMapReaderFactory = new CMapReaderFactory({
@ -1514,6 +1520,7 @@ class PDFPageProxy {
operatorList: intentState.operatorList,
pageIndex: this._pageIndex,
canvasFactory: canvasFactory || this._transport.canvasFactory,
filterFactory: this._transport.filterFactory,
useRequestAnimationFrame: !intentPrint,
pdfBug: this._pdfBug,
pageColors,
@ -1526,19 +1533,25 @@ class PDFPageProxy {
intentState.displayReadyCapability.promise,
optionalContentConfigPromise,
])
.then(([transparency, optionalContentConfig]) => {
if (this.pendingCleanup) {
complete();
return;
}
this._stats?.time("Rendering");
internalRenderTask.initializeGraphics({
transparency,
.then(
([
{ transparency, isOffscreenCanvasSupported },
optionalContentConfig,
});
internalRenderTask.operatorListChanged();
})
]) => {
if (this.pendingCleanup) {
complete();
return;
}
this._stats?.time("Rendering");
internalRenderTask.initializeGraphics({
transparency,
isOffscreenCanvasSupported,
optionalContentConfig,
});
internalRenderTask.operatorListChanged();
}
)
.catch(complete);
return renderTask;
@ -1739,7 +1752,7 @@ class PDFPageProxy {
/**
* @private
*/
_startRenderPage(transparency, cacheKey) {
_startRenderPage(transparency, isOffscreenCanvasSupported, cacheKey) {
const intentState = this._intentStates.get(cacheKey);
if (!intentState) {
return; // Rendering was cancelled.
@ -1748,7 +1761,10 @@ class PDFPageProxy {
// TODO Refactor RenderPageRequest to separate rendering
// and operator list logic
intentState.displayReadyCapability?.resolve(transparency);
intentState.displayReadyCapability?.resolve({
transparency,
isOffscreenCanvasSupported,
});
}
/**
@ -2357,6 +2373,7 @@ class WorkerTransport {
this._params = params;
this.canvasFactory = factory.canvasFactory;
this.filterFactory = factory.filterFactory;
this.cMapReaderFactory = factory.cMapReaderFactory;
this.standardFontDataFactory = factory.standardFontDataFactory;
@ -2489,6 +2506,7 @@ class WorkerTransport {
this.commonObjs.clear();
this.fontLoader.clear();
this.#methodPromises.clear();
this.filterFactory.destroy();
if (this._networkStream) {
this._networkStream.cancelAllRequests(
@ -2709,7 +2727,11 @@ class WorkerTransport {
}
const page = this.#pageCache.get(data.pageIndex);
page._startRenderPage(data.transparency, data.cacheKey);
page._startRenderPage(
data.transparency,
data.isOffscreenCanvasSupported,
data.cacheKey
);
});
messageHandler.on("commonobj", ([id, type, exportedData]) => {
@ -3079,6 +3101,7 @@ class WorkerTransport {
this.fontLoader.clear();
}
this.#methodPromises.clear();
this.filterFactory.destroy();
}
get loadingParams() {
@ -3246,6 +3269,7 @@ class InternalRenderTask {
operatorList,
pageIndex,
canvasFactory,
filterFactory,
useRequestAnimationFrame = false,
pdfBug = false,
pageColors = null,
@ -3259,6 +3283,7 @@ class InternalRenderTask {
this.operatorList = operatorList;
this._pageIndex = pageIndex;
this.canvasFactory = canvasFactory;
this.filterFactory = filterFactory;
this._pdfBug = pdfBug;
this.pageColors = pageColors;
@ -3285,7 +3310,11 @@ class InternalRenderTask {
});
}
initializeGraphics({ transparency = false, optionalContentConfig }) {
initializeGraphics({
transparency = false,
isOffscreenCanvasSupported = false,
optionalContentConfig,
}) {
if (this.cancelled) {
return;
}
@ -3312,6 +3341,7 @@ class InternalRenderTask {
this.commonObjs,
this.objs,
this.canvasFactory,
isOffscreenCanvasSupported ? this.filterFactory : null,
{ optionalContentConfig },
this.annotationCanvasMap,
this.pageColors

View File

@ -37,7 +37,7 @@ import {
PathType,
TilingPattern,
} from "./pattern_helper.js";
import { applyMaskImageData } from "../shared/image_utils.js";
import { convertBlackAndWhiteToRGBA } from "../shared/image_utils.js";
// <canvas> contexts store most of the state we need natively.
// However, PDF needs a bit more state, which we store here.
@ -812,12 +812,13 @@ function putBinaryImageMask(ctx, imgData) {
// Expand the mask so it can be used by the canvas. Any required
// inversion has already been handled.
({ srcPos } = applyMaskImageData({
({ srcPos } = convertBlackAndWhiteToRGBA({
src,
srcPos,
dest,
width,
height: thisChunkHeight,
nonBlackColor: 0,
}));
ctx.putImageData(chunkImgData, 0, i * FULL_CHUNK_HEIGHT);
@ -1015,6 +1016,7 @@ class CanvasGraphics {
commonObjs,
objs,
canvasFactory,
filterFactory,
{ optionalContentConfig, markedContentStack = null },
annotationCanvasMap,
pageColors
@ -1032,6 +1034,7 @@ class CanvasGraphics {
this.commonObjs = commonObjs;
this.objs = objs;
this.canvasFactory = canvasFactory;
this.filterFactory = filterFactory;
this.groupStack = [];
this.processingType3 = null;
// Patterns are painted relative to the initial page/form transform, see
@ -1573,7 +1576,10 @@ class CanvasGraphics {
this.checkSMaskState();
break;
case "TR":
this.current.transferMaps = value;
this.current.transferMaps = this.filterFactory
? this.filterFactory.addFilter(value)
: value;
break;
}
}
}
@ -2463,6 +2469,7 @@ class CanvasGraphics {
this.commonObjs,
this.objs,
this.canvasFactory,
this.filterFactory,
{
optionalContentConfig: this.optionalContentConfig,
markedContentStack: this.markedContentStack,
@ -3017,6 +3024,24 @@ class CanvasGraphics {
this.paintInlineImageXObjectGroup(imgData, map);
}
applyTransferMapsToBitmap(imgData) {
if (!this.current.transferMaps) {
return imgData.bitmap;
}
const { bitmap, width, height } = imgData;
const tmpCanvas = this.cachedCanvases.getCanvas(
"inlineImage",
width,
height
);
const tmpCtx = tmpCanvas.context;
tmpCtx.filter = this.current.transferMaps;
tmpCtx.drawImage(bitmap, 0, 0);
tmpCtx.filter = "";
return tmpCanvas.canvas;
}
paintInlineImageXObject(imgData) {
if (!this.contentVisible) {
return;
@ -3030,11 +3055,13 @@ class CanvasGraphics {
ctx.scale(1 / width, -1 / height);
let imgToPaint;
// typeof check is needed due to node.js support, see issue #8489
if (
if (imgData.bitmap) {
imgToPaint = this.applyTransferMapsToBitmap(imgData);
} else if (
(typeof HTMLElement === "function" && imgData instanceof HTMLElement) ||
!imgData.data
) {
// typeof check is needed due to node.js support, see issue #8489
imgToPaint = imgData;
} else {
const tmpCanvas = this.cachedCanvases.getCanvas(
@ -3077,12 +3104,18 @@ class CanvasGraphics {
return;
}
const ctx = this.ctx;
const w = imgData.width;
const h = imgData.height;
let imgToPaint;
if (imgData.bitmap) {
imgToPaint = this.applyTransferMapsToBitmap(imgData);
} else {
const w = imgData.width;
const h = imgData.height;
const tmpCanvas = this.cachedCanvases.getCanvas("inlineImage", w, h);
const tmpCtx = tmpCanvas.context;
putBinaryImageData(tmpCtx, imgData, this.current.transferMaps);
const tmpCanvas = this.cachedCanvases.getCanvas("inlineImage", w, h);
const tmpCtx = tmpCanvas.context;
putBinaryImageData(tmpCtx, imgData, this.current.transferMaps);
imgToPaint = tmpCanvas.canvas;
}
for (const entry of map) {
ctx.save();
@ -3090,7 +3123,7 @@ class CanvasGraphics {
ctx.scale(1, -1);
drawImageAtIntegerCoords(
ctx,
tmpCanvas.canvas,
imgToPaint,
entry.x,
entry.y,
entry.w,

View File

@ -39,6 +39,139 @@ class PixelsPerInch {
static PDF_TO_CSS_UNITS = this.CSS / this.PDF;
}
/**
* FilterFactory aims to create some SVG filters we can use when drawing an
* image (or whatever) on a canvas.
* Filters aren't applied with ctx.putImageData because it just overwrites the
* underlying pixels.
* With these filters, it's possible for example to apply some transfer maps on
* an image without the need to apply them on the pixel arrays: the renderer
* does the magic for us.
*/
class FilterFactory {
#_cache;
#_defs;
#document;
#id = 0;
constructor({ ownerDocument = globalThis.document } = {}) {
this.#document = ownerDocument;
}
get #cache() {
return (this.#_cache ||= new Map());
}
get #defs() {
if (!this.#_defs) {
const svg = this.#document.createElementNS(SVG_NS, "svg");
svg.setAttribute("width", 0);
svg.setAttribute("height", 0);
svg.style.visibility = "hidden";
svg.style.contain = "strict";
this.#_defs = this.#document.createElementNS(SVG_NS, "defs");
svg.append(this.#_defs);
this.#document.body.append(svg);
}
return this.#_defs;
}
addFilter(maps) {
if (!maps) {
return "";
}
// When a page is zoomed the page is re-drawn but the maps are likely
// the same.
let value = this.#cache.get(maps);
if (value) {
return value;
}
let tableR, tableG, tableB, key;
if (maps.length === 1) {
const mapR = maps[0];
const buffer = new Array(256);
for (let i = 0; i < 256; i++) {
buffer[i] = mapR[i] / 255;
}
key = tableR = tableG = tableB = buffer.join(",");
} else {
const [mapR, mapG, mapB] = maps;
const bufferR = new Array(256);
const bufferG = new Array(256);
const bufferB = new Array(256);
for (let i = 0; i < 256; i++) {
bufferR[i] = mapR[i] / 255;
bufferG[i] = mapG[i] / 255;
bufferB[i] = mapB[i] / 255;
}
tableR = bufferR.join(",");
tableG = bufferG.join(",");
tableB = bufferB.join(",");
key = `${tableR}${tableG}${tableB}`;
}
value = this.#cache.get(key);
if (value) {
this.#cache.set(maps, value);
return value;
}
// We create a SVG filter: feComponentTransferElement
// https://www.w3.org/TR/SVG11/filters.html#feComponentTransferElement
const id = `transfer_map_${this.#id++}`;
const url = `url(#${id})`;
this.#cache.set(maps, url);
this.#cache.set(key, url);
const filter = this.#document.createElementNS(SVG_NS, "filter", SVG_NS);
filter.setAttribute("id", id);
filter.setAttribute("color-interpolation-filters", "sRGB");
const feComponentTransfer = this.#document.createElementNS(
SVG_NS,
"feComponentTransfer"
);
filter.append(feComponentTransfer);
const type = "discrete";
const feFuncR = this.#document.createElementNS(SVG_NS, "feFuncR");
feFuncR.setAttribute("type", type);
feFuncR.setAttribute("tableValues", tableR);
feComponentTransfer.append(feFuncR);
const feFuncG = this.#document.createElementNS(SVG_NS, "feFuncG");
feFuncG.setAttribute("type", type);
feFuncG.setAttribute("tableValues", tableG);
feComponentTransfer.append(feFuncG);
const feFuncB = this.#document.createElementNS(SVG_NS, "feFuncB");
feFuncB.setAttribute("type", type);
feFuncB.setAttribute("tableValues", tableB);
feComponentTransfer.append(feFuncB);
this.#defs.append(filter);
return url;
}
destroy() {
if (this.#_defs) {
this.#_defs.parentNode.remove();
this.#_defs = null;
}
if (this.#_cache) {
this.#_cache.clear();
this.#_cache = null;
}
this.#id = 0;
}
}
class DOMCanvasFactory extends BaseCanvasFactory {
constructor({ ownerDocument = globalThis.document } = {}) {
super();
@ -681,6 +814,7 @@ export {
DOMCMapReaderFactory,
DOMStandardFontDataFactory,
DOMSVGFactory,
FilterFactory,
getColorValues,
getCurrentTransform,
getCurrentTransformInverse,

View File

@ -52,6 +52,7 @@ import {
version,
} from "./display/api.js";
import {
FilterFactory,
getFilenameFromUrl,
getPdfFilenameFromUrl,
getXfaPageViewport,
@ -91,6 +92,7 @@ export {
createPromiseCapability,
createValidAbsoluteUrl,
FeatureTest,
FilterFactory,
getDocument,
getFilenameFromUrl,
getPdfFilenameFromUrl,

View File

@ -13,23 +13,37 @@
* limitations under the License.
*/
import { FeatureTest } from "./util.js";
import { FeatureTest, ImageKind } from "./util.js";
function applyMaskImageData({
function convertToRGBA(params) {
switch (params.kind) {
case ImageKind.GRAYSCALE_1BPP:
return convertBlackAndWhiteToRGBA(params);
case ImageKind.RGB_24BPP:
return convertRGBToRGBA(params);
}
return null;
}
function convertBlackAndWhiteToRGBA({
src,
srcPos = 0,
dest,
destPos = 0,
width,
height,
nonBlackColor = 0xffffffff,
inverseDecode = false,
}) {
const opaque = FeatureTest.isLittleEndian ? 0xff000000 : 0x000000ff;
const [zeroMapping, oneMapping] = !inverseDecode ? [opaque, 0] : [0, opaque];
const black = FeatureTest.isLittleEndian ? 0xff000000 : 0x000000ff;
const [zeroMapping, oneMapping] = inverseDecode
? [nonBlackColor, black]
: [black, nonBlackColor];
const widthInSource = width >> 3;
const widthRemainder = width & 7;
const srcLength = src.length;
dest = new Uint32Array(dest.buffer);
let destPos = 0;
for (let i = 0; i < height; i++) {
for (const max = srcPos + widthInSource; srcPos < max; srcPos++) {
@ -51,8 +65,70 @@ function applyMaskImageData({
dest[destPos++] = elem & (1 << (7 - j)) ? oneMapping : zeroMapping;
}
}
return { srcPos, destPos };
}
function convertRGBToRGBA({
src,
srcPos = 0,
dest,
destPos = 0,
width,
height,
}) {
let i = 0;
const len32 = src.length >> 2;
const src32 = new Uint32Array(src.buffer, srcPos, len32);
if (FeatureTest.isLittleEndian) {
// It's a way faster to do the shuffle manually instead of working
// component by component with some Uint8 arrays.
for (; i < len32 - 2; i += 3, destPos += 4) {
const s1 = src32[i]; // R2B1G1R1
const s2 = src32[i + 1]; // G3R3B2G2
const s3 = src32[i + 2]; // B4G4R4B3
dest[destPos] = s1 | 0xff000000;
dest[destPos + 1] = (s1 >>> 24) | (s2 << 8) | 0xff000000;
dest[destPos + 2] = (s2 >>> 16) | (s3 << 16) | 0xff000000;
dest[destPos + 3] = (s3 >>> 8) | 0xff000000;
}
for (let j = i * 4, jj = src.length; j < jj; j += 3) {
dest[destPos++] =
src[j] | (src[j + 1] << 8) | (src[j + 2] << 16) | 0xff000000;
}
} else {
for (; i < len32 - 2; i += 3, destPos += 4) {
const s1 = src32[i]; // R1G1B1R2
const s2 = src32[i + 1]; // G2B2R3G3
const s3 = src32[i + 2]; // B3R4G4B4
dest[destPos] = s1 | 0xff;
dest[destPos + 1] = (s1 << 24) | (s2 >>> 8) | 0xff;
dest[destPos + 2] = (s2 << 16) | (s3 >>> 16) | 0xff;
dest[destPos + 3] = (s3 << 8) | 0xff;
}
for (let j = i * 4, jj = src.length; j < jj; j += 3) {
dest[destPos++] =
(src[j] << 24) | (src[j + 1] << 16) | (src[j + 2] << 8) | 0xff;
}
}
return { srcPos, destPos };
}
export { applyMaskImageData };
function grayToRGBA(src, dest) {
if (FeatureTest.isLittleEndian) {
for (let i = 0, ii = src.length; i < ii; i++) {
dest[i] = (src[i] * 0x10101) | 0xff000000;
}
} else {
for (let i = 0, ii = src.length; i < ii; i++) {
dest[i] = (src[i] * 0x1010100) | 0x000000ff;
}
}
}
export { convertBlackAndWhiteToRGBA, convertToRGBA, grayToRGBA };

View File

@ -2655,7 +2655,11 @@ Caron Broadcasting, Inc., an Ohio corporation (“Lessee”).`)
});
it("gets operatorList with JPEG image (issue 4888)", async function () {
const loadingTask = getDocument(buildGetDocumentParams("cmykjpeg.pdf"));
const loadingTask = getDocument(
buildGetDocumentParams("cmykjpeg.pdf", {
isOffscreenCanvasSupported: false,
})
);
const pdfDoc = await loadingTask.promise;
const pdfPage = await pdfDoc.getPage(1);
@ -3089,7 +3093,11 @@ Caron Broadcasting, Inc., an Ohio corporation (“Lessee”).`)
EXPECTED_WIDTH = 2550,
EXPECTED_HEIGHT = 3300;
const loadingTask = getDocument(buildGetDocumentParams("issue11878.pdf"));
const loadingTask = getDocument(
buildGetDocumentParams("issue11878.pdf", {
isOffscreenCanvasSupported: false,
})
);
const pdfDoc = await loadingTask.promise;
let firstImgData = null;

View File

@ -61,7 +61,11 @@ describe("SVGGraphics", function () {
let page;
beforeAll(async function () {
loadingTask = getDocument(buildGetDocumentParams("xobject-image.pdf"));
loadingTask = getDocument(
buildGetDocumentParams("xobject-image.pdf", {
isOffscreenCanvasSupported: false,
})
);
const doc = await loadingTask.promise;
page = await doc.getPage(1);
});