1
0
Fork 0
mirror of https://github.com/mozilla/pdf.js.git synced 2025-04-22 16:18:08 +02:00

[api-minor] Render pushbuttons on their own canvas (bug 1737260)

- First step to fix https://bugzilla.mozilla.org/show_bug.cgi?id=1737260;
 - several interactive pdfs use the possibility to hide/show buttons to show different icons;
 - render pushbuttons on their own canvas and then insert it the annotation_layer;
 - update test/driver.js in order to convert canvases for pushbuttons into images.
This commit is contained in:
Calixte Denizet 2021-11-06 18:36:49 +01:00
parent 891f21fba6
commit 33ea817b20
13 changed files with 333 additions and 86 deletions

View file

@ -26,6 +26,7 @@ import {
isAscii,
isString,
OPS,
RenderingIntentFlag,
shadow,
stringToPDFString,
stringToUTF16BEString,
@ -386,6 +387,7 @@ class Annotation {
modificationDate: this.modificationDate,
rect: this.rectangle,
subtype: params.subtype,
hasOwnCanvas: false,
};
if (params.collectFields) {
@ -708,8 +710,8 @@ class Annotation {
this.appearance = normalAppearanceState.get(as.name);
}
loadResources(keys) {
return this.appearance.dict.getAsync("Resources").then(resources => {
loadResources(keys, appearance) {
return appearance.dict.getAsync("Resources").then(resources => {
if (!resources) {
return undefined;
}
@ -721,22 +723,24 @@ class Annotation {
});
}
getOperatorList(evaluator, task, renderForms, annotationStorage) {
if (!this.appearance) {
return Promise.resolve(new OperatorList());
getOperatorList(evaluator, task, intent, renderForms, annotationStorage) {
const data = this.data;
let appearance = this.appearance;
const isUsingOwnCanvas =
data.hasOwnCanvas && intent & RenderingIntentFlag.DISPLAY;
if (!appearance) {
if (!isUsingOwnCanvas) {
return Promise.resolve(new OperatorList());
}
appearance = new StringStream("");
appearance.dict = new Dict();
}
const appearance = this.appearance;
const data = this.data;
const appearanceDict = appearance.dict;
const resourcesPromise = this.loadResources([
"ExtGState",
"ColorSpace",
"Pattern",
"Shading",
"XObject",
"Font",
]);
const resourcesPromise = this.loadResources(
["ExtGState", "ColorSpace", "Pattern", "Shading", "XObject", "Font"],
appearance
);
const bbox = appearanceDict.getArray("BBox") || [0, 0, 1, 1];
const matrix = appearanceDict.getArray("Matrix") || [1, 0, 0, 1, 0, 0];
const transform = getTransformMatrix(data.rect, bbox, matrix);
@ -748,6 +752,7 @@ class Annotation {
data.rect,
transform,
matrix,
isUsingOwnCanvas,
]);
return evaluator
@ -1329,7 +1334,7 @@ class WidgetAnnotation extends Annotation {
return !!(this.data.fieldFlags & flag);
}
getOperatorList(evaluator, task, renderForms, annotationStorage) {
getOperatorList(evaluator, task, intent, renderForms, annotationStorage) {
// Do not render form elements on the canvas when interactive forms are
// enabled. The display layer is responsible for rendering them instead.
if (renderForms && !(this instanceof SignatureWidgetAnnotation)) {
@ -1340,6 +1345,7 @@ class WidgetAnnotation extends Annotation {
return super.getOperatorList(
evaluator,
task,
intent,
renderForms,
annotationStorage
);
@ -1351,6 +1357,7 @@ class WidgetAnnotation extends Annotation {
return super.getOperatorList(
evaluator,
task,
intent,
renderForms,
annotationStorage
);
@ -1936,17 +1943,25 @@ class ButtonWidgetAnnotation extends WidgetAnnotation {
} else if (this.data.radioButton) {
this._processRadioButton(params);
} else if (this.data.pushButton) {
this.data.hasOwnCanvas = true;
this._processPushButton(params);
} else {
warn("Invalid field flags for button widget annotation");
}
}
async getOperatorList(evaluator, task, renderForms, annotationStorage) {
async getOperatorList(
evaluator,
task,
intent,
renderForms,
annotationStorage
) {
if (this.data.pushButton) {
return super.getOperatorList(
evaluator,
task,
intent,
false, // we use normalAppearance to render the button
annotationStorage
);
@ -1965,6 +1980,7 @@ class ButtonWidgetAnnotation extends WidgetAnnotation {
return super.getOperatorList(
evaluator,
task,
intent,
renderForms,
annotationStorage
);
@ -1988,6 +2004,7 @@ class ButtonWidgetAnnotation extends WidgetAnnotation {
const operatorList = super.getOperatorList(
evaluator,
task,
intent,
renderForms,
annotationStorage
);

View file

@ -407,6 +407,7 @@ class Page {
.getOperatorList(
partialEvaluator,
task,
intent,
renderForms,
annotationStorage
)

View file

@ -198,7 +198,25 @@ class AnnotationElement {
page.view[3] - data.rect[3] + page.view[1],
]);
container.style.transform = `matrix(${viewport.transform.join(",")})`;
if (data.hasOwnCanvas) {
const transform = viewport.transform.slice();
const [scaleX, scaleY] = Util.singularValueDecompose2dScale(transform);
width = Math.ceil(width * scaleX);
height = Math.ceil(height * scaleY);
rect[0] *= scaleX;
rect[1] *= scaleY;
// Reset the scale part of the transform matrix (which must be diagonal
// or anti-diagonal) in order to avoid to rescale the canvas.
// The canvas for the annotation is correctly scaled when it is drawn
// (see `beginAnnotation` in canvas.js).
for (let i = 0; i < 4; i++) {
transform[i] = Math.sign(transform[i]);
}
container.style.transform = `matrix(${transform.join(",")})`;
} else {
container.style.transform = `matrix(${viewport.transform.join(",")})`;
}
container.style.transformOrigin = `${-rect[0]}px ${-rect[1]}px`;
if (!ignoreBorder && data.borderStyle.width > 0) {
@ -258,8 +276,13 @@ class AnnotationElement {
container.style.left = `${rect[0]}px`;
container.style.top = `${rect[1]}px`;
container.style.width = `${width}px`;
container.style.height = `${height}px`;
if (data.hasOwnCanvas) {
container.style.width = container.style.height = "auto";
} else {
container.style.width = `${width}px`;
container.style.height = `${height}px`;
}
return container;
}
@ -2318,10 +2341,12 @@ class AnnotationLayer {
sortedAnnotations.push(...popupAnnotations);
}
const div = parameters.div;
for (const data of sortedAnnotations) {
const element = AnnotationElementFactory.create({
data,
layer: parameters.div,
layer: div,
page: parameters.page,
viewport: parameters.viewport,
linkService: parameters.linkService,
@ -2343,19 +2368,21 @@ class AnnotationLayer {
}
if (Array.isArray(rendered)) {
for (const renderedElement of rendered) {
parameters.div.appendChild(renderedElement);
div.appendChild(renderedElement);
}
} else {
if (element instanceof PopupAnnotationElement) {
// Popup annotation elements should not be on top of other
// annotation elements to prevent interfering with mouse events.
parameters.div.prepend(rendered);
div.prepend(rendered);
} else {
parameters.div.appendChild(rendered);
div.appendChild(rendered);
}
}
}
}
this.#setAnnotationCanvasMap(div, parameters.annotationCanvasMap);
}
/**
@ -2366,18 +2393,73 @@ class AnnotationLayer {
* @memberof AnnotationLayer
*/
static update(parameters) {
const transform = `matrix(${parameters.viewport.transform.join(",")})`;
for (const data of parameters.annotations) {
const elements = parameters.div.querySelectorAll(
const { page, viewport, annotations, annotationCanvasMap, div } =
parameters;
const transform = viewport.transform;
const matrix = `matrix(${transform.join(",")})`;
let scale, ownMatrix;
for (const data of annotations) {
const elements = div.querySelectorAll(
`[data-annotation-id="${data.id}"]`
);
if (elements) {
for (const element of elements) {
element.style.transform = transform;
if (data.hasOwnCanvas) {
const rect = Util.normalizeRect([
data.rect[0],
page.view[3] - data.rect[1] + page.view[1],
data.rect[2],
page.view[3] - data.rect[3] + page.view[1],
]);
if (!ownMatrix) {
// When an annotation has its own canvas, then
// the scale has been already applied to the canvas,
// so we musn't scale it twice.
scale = Math.abs(transform[0] || transform[1]);
const ownTransform = transform.slice();
for (let i = 0; i < 4; i++) {
ownTransform[i] = Math.sign(ownTransform[i]);
}
ownMatrix = `matrix(${ownTransform.join(",")})`;
}
const left = rect[0] * scale;
const top = rect[1] * scale;
element.style.left = `${left}px`;
element.style.top = `${top}px`;
element.style.transformOrigin = `${-left}px ${-top}px`;
element.style.transform = ownMatrix;
} else {
element.style.transform = matrix;
}
}
}
}
parameters.div.hidden = false;
this.#setAnnotationCanvasMap(div, annotationCanvasMap);
div.hidden = false;
}
static #setAnnotationCanvasMap(div, annotationCanvasMap) {
if (!annotationCanvasMap) {
return;
}
for (const [id, canvas] of annotationCanvasMap) {
const element = div.querySelector(`[data-annotation-id="${id}"]`);
if (!element) {
continue;
}
const { firstChild } = element;
if (firstChild.nodeName === "CANVAS") {
element.replaceChild(canvas, firstChild);
} else {
element.insertBefore(canvas, firstChild);
}
}
annotationCanvasMap.clear();
}
}

View file

@ -1161,6 +1161,8 @@ class PDFDocumentProxy {
* created from `PDFDocumentProxy.getOptionalContentConfig`. If `null`,
* the configuration will be fetched automatically with the default visibility
* states set.
* @property {Map<string, Canvas>} [annotationCanvasMap] - Map some annotation
* ids with canvases used to render them.
*/
/**
@ -1374,6 +1376,7 @@ class PDFPageProxy {
canvasFactory = null,
background = null,
optionalContentConfigPromise = null,
annotationCanvasMap = null,
}) {
if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("GENERIC")) {
if (arguments[0]?.renderInteractiveForms !== undefined) {
@ -1491,6 +1494,7 @@ class PDFPageProxy {
},
objs: this.objs,
commonObjs: this.commonObjs,
annotationCanvasMap,
operatorList: intentState.operatorList,
pageIndex: this._pageIndex,
canvasFactory: canvasFactoryInstance,
@ -3216,6 +3220,7 @@ class InternalRenderTask {
params,
objs,
commonObjs,
annotationCanvasMap,
operatorList,
pageIndex,
canvasFactory,
@ -3226,6 +3231,7 @@ class InternalRenderTask {
this.params = params;
this.objs = objs;
this.commonObjs = commonObjs;
this.annotationCanvasMap = annotationCanvasMap;
this.operatorListIdx = null;
this.operatorList = operatorList;
this._pageIndex = pageIndex;
@ -3284,7 +3290,8 @@ class InternalRenderTask {
this.objs,
this.canvasFactory,
imageLayer,
optionalContentConfig
optionalContentConfig,
this.annotationCanvasMap
);
this.gfx.beginDrawing({
transform,

View file

@ -1068,7 +1068,8 @@ class CanvasGraphics {
objs,
canvasFactory,
imageLayer,
optionalContentConfig
optionalContentConfig,
annotationCanvasMap
) {
this.ctx = canvasCtx;
this.current = new CanvasExtraState(
@ -1100,6 +1101,10 @@ class CanvasGraphics {
this.optionalContentConfig = optionalContentConfig;
this.cachedCanvases = new CachedCanvases(this.canvasFactory);
this.cachedPatterns = new Map();
this.annotationCanvasMap = annotationCanvasMap;
this.viewportScale = 1;
this.outputScaleX = 1;
this.outputScaleY = 1;
if (canvasCtx) {
// NOTE: if mozCurrentTransform is polyfilled, then the current state of
// the transformation must already be set in canvasCtx._transformMatrix.
@ -1147,8 +1152,11 @@ class CanvasGraphics {
resetCtxToDefault(this.ctx);
if (transform) {
this.ctx.transform.apply(this.ctx, transform);
this.outputScaleX = transform[0];
this.outputScaleY = transform[0];
}
this.ctx.transform.apply(this.ctx, viewport.transform);
this.viewportScale = viewport.scale;
this.baseTransform = this.ctx.mozCurrentTransform.slice();
this._combinedScaleFactor = Math.hypot(
@ -2691,27 +2699,72 @@ class CanvasGraphics {
this.restore();
}
beginAnnotation(id, rect, transform, matrix) {
beginAnnotation(id, rect, transform, matrix, hasOwnCanvas) {
this.save();
resetCtxToDefault(this.ctx);
this.current = new CanvasExtraState(
this.ctx.canvas.width,
this.ctx.canvas.height
);
if (Array.isArray(rect) && rect.length === 4) {
const width = rect[2] - rect[0];
const height = rect[3] - rect[1];
this.ctx.rect(rect[0], rect[1], width, height);
this.clip();
this.endPath();
if (hasOwnCanvas && this.annotationCanvasMap) {
transform = transform.slice();
transform[4] -= rect[0];
transform[5] -= rect[1];
rect = rect.slice();
rect[0] = rect[1] = 0;
rect[2] = width;
rect[3] = height;
const [scaleX, scaleY] = Util.singularValueDecompose2dScale(
this.ctx.mozCurrentTransform
);
const { viewportScale } = this;
const canvasWidth = Math.ceil(
width * this.outputScaleX * viewportScale
);
const canvasHeight = Math.ceil(
height * this.outputScaleY * viewportScale
);
this.annotationCanvas = this.canvasFactory.create(
canvasWidth,
canvasHeight
);
const { canvas, context } = this.annotationCanvas;
canvas.style.width = `calc(${width}px * var(--viewport-scale-factor))`;
canvas.style.height = `calc(${height}px * var(--viewport-scale-factor))`;
this.annotationCanvasMap.set(id, canvas);
this.annotationCanvas.savedCtx = this.ctx;
this.ctx = context;
this.ctx.setTransform(scaleX, 0, 0, -scaleY, 0, height * scaleY);
addContextCurrentTransform(this.ctx);
resetCtxToDefault(this.ctx);
} else {
resetCtxToDefault(this.ctx);
this.ctx.rect(rect[0], rect[1], width, height);
this.clip();
this.endPath();
}
}
this.current = new CanvasExtraState(
this.ctx.canvas.width,
this.ctx.canvas.height
);
this.transform.apply(this, transform);
this.transform.apply(this, matrix);
}
endAnnotation() {
if (this.annotationCanvas) {
this.ctx = this.annotationCanvas.savedCtx;
delete this.annotationCanvas.savedCtx;
delete this.annotationCanvas;
}
this.restore();
}