Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add form printing support #12076

Closed
wants to merge 14 commits into from
224 changes: 187 additions & 37 deletions src/core/annotation.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,17 @@ import {
assert,
isString,
OPS,
stringToBytes,
stringToPDFString,
Util,
warn,
} from "../shared/util.js";
import { Catalog, FileSpec, ObjectLoader } from "./obj.js";
import { Dict, isDict, isName, isRef, isStream } from "./primitives.js";
import { ColorSpace } from "./colorspace.js";
import { EvalState } from "./evaluator.js";
import { getInheritableProperty } from "./core_utils.js";
import { OperatorList } from "./operator_list.js";
import { Stream } from "./stream.js";
import { StringStream } from "./stream.js";

class AnnotationFactory {
/**
Expand All @@ -49,18 +49,21 @@ class AnnotationFactory {
* instance.
*/
static create(xref, ref, pdfManager, idFactory) {
return pdfManager.ensure(this, "_create", [
xref,
ref,
pdfManager,
idFactory,
]);
return pdfManager.ensureDoc("acroForm").then(acroForm => {
return pdfManager.ensure(this, "_create", [
xref,
ref,
pdfManager,
idFactory,
acroForm,
]);
});
}

/**
* @private
*/
static _create(xref, ref, pdfManager, idFactory) {
static _create(xref, ref, pdfManager, idFactory, acroForm) {
const dict = xref.fetchIfRef(ref);
if (!isDict(dict)) {
return undefined;
Expand All @@ -78,6 +81,7 @@ class AnnotationFactory {
subtype,
id,
pdfManager,
acroForm: acroForm instanceof Dict ? acroForm : Dict.empty,
};

switch (subtype) {
Expand Down Expand Up @@ -471,6 +475,7 @@ class Annotation {
*/
setAppearance(dict) {
this.appearance = null;
this.checkedAppearance = null;

const appearanceStates = dict.get("AP");
if (!isDict(appearanceStates)) {
Expand Down Expand Up @@ -509,13 +514,14 @@ class Annotation {
});
}

getOperatorList(evaluator, task, renderForms) {
getOperatorList(evaluator, task, renderForms, annotationStorage) {
if (!this.appearance) {
return Promise.resolve(new OperatorList());
}

const data = this.data;
const appearanceDict = this.appearance.dict;
const appearance = this.appearance;
const appearanceDict = appearance.dict;
const resourcesPromise = this.loadResources([
"ExtGState",
"ColorSpace",
Expand All @@ -533,14 +539,14 @@ class Annotation {
opList.addOp(OPS.beginAnnotation, [data.rect, transform, matrix]);
return evaluator
.getOperatorList({
stream: this.appearance,
stream: appearance,
task,
resources,
operatorList: opList,
})
.then(() => {
opList.addOp(OPS.endAnnotation, []);
this.appearance.reset();
appearance.reset();
return opList;
});
});
Expand Down Expand Up @@ -799,7 +805,9 @@ class WidgetAnnotation extends Annotation {
const fieldType = getInheritableProperty({ dict, key: "FT" });
data.fieldType = isName(fieldType) ? fieldType.name : null;
this.fieldResources =
getInheritableProperty({ dict, key: "DR" }) || Dict.empty;
getInheritableProperty({ dict, key: "DR" }) ||
params.acroForm.get("DR") ||
Dict.empty;

data.fieldFlags = getInheritableProperty({ dict, key: "Ff" });
if (!Number.isInteger(data.fieldFlags) || data.fieldFlags < 0) {
Expand Down Expand Up @@ -877,13 +885,18 @@ class WidgetAnnotation extends Annotation {
return !!(this.data.fieldFlags & flag);
}

getOperatorList(evaluator, task, renderForms) {
getOperatorList(evaluator, task, 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) {
return Promise.resolve(new OperatorList());
}
return super.getOperatorList(evaluator, task, renderForms);
return super.getOperatorList(
evaluator,
task,
renderForms,
annotationStorage
);
}
}

Expand Down Expand Up @@ -920,37 +933,141 @@ class TextWidgetAnnotation extends WidgetAnnotation {
this.data.maxLen !== null;
}

getOperatorList(evaluator, task, renderForms) {
if (renderForms || this.appearance) {
return super.getOperatorList(evaluator, task, renderForms);
getOperatorList(evaluator, task, renderForms, annotationStorage) {
return this.getAppearance(evaluator, task, annotationStorage).then(
content => {
if (renderForms || (this.appearance && content === null)) {
return super.getOperatorList(
evaluator,
task,
renderForms,
annotationStorage
);
}

const operatorList = new OperatorList();

// Even if there is an appearance stream, ignore it. This is the
// behaviour used by Adobe Reader.
if (!this.data.defaultAppearance || content === null) {
return operatorList;
}

const matrix = [1, 0, 0, 1, 0, 0];
const bbox = [
0,
0,
this.data.rect[2] - this.data.rect[0],
this.data.rect[3] - this.data.rect[1],
];

const transform = getTransformMatrix(this.data.rect, bbox, matrix);
operatorList.addOp(OPS.beginAnnotation, [
this.data.rect,
transform,
matrix,
]);

const stream = new StringStream(content);
return evaluator
.getOperatorList({
stream,
task,
resources: this.fieldResources,
operatorList,
})
.then(function () {
operatorList.addOp(OPS.endAnnotation, []);
return operatorList;
});
}
);
}

async getAppearance(evaluator, task, annotationStorage) {
if (!annotationStorage) {
return null;
}
const value = annotationStorage[this.data.id] || "";
if (value === "") {
return null;
}

const borderWidth = this.data.borderStyle.width;

// Default horizontal padding: can we have an heuristic to guess it?
const hPadding = borderWidth + 2;

// TODO: Handle the case where the font is not defined:
// fallback on helv maybe?
// TODO: Handle the case where fontSize is null
const [font, fontSize] = await this.getFontData(evaluator, task);

const vPadding = borderWidth + Math.ceil(Math.abs(font.descent) * fontSize);
const defaultAppearance = this.data.defaultAppearance;
const totalWidth = this.data.rect[2] - this.data.rect[0];

if (this.data.comb) {
const combWidth = (totalWidth / this.data.maxLen).toFixed(2);
let buf = `/Tx BMC q BT ${defaultAppearance} 1 0 0 1 ${hPadding} ${vPadding} Tm`;
let first = true;
for (const character of value) {
if (first) {
buf += ` (${character}) Tj`;
first = false;
} else {
buf += ` ${combWidth} 0 Td (${character}) Tj`;
}
}
buf += " ET Q EMC";
return buf;
}

const alignment = this.data.textAlignment;
if (alignment === 0 || alignment > 2) {
// Left alignment: nothing to do
return `/Tx BMC q BT ${defaultAppearance} 1 0 0 1 ${hPadding} ${vPadding} Tm (${value}) Tj ET Q EMC`;
}

// We need to get the width of the text in order to align it correctly
const glyphs = font.charsToGlyphs(value);

const scale = fontSize / 1000;
let width = 0;
for (const glyph of glyphs) {
width += glyph.width * scale;
}

let shift;
if (alignment === 1) {
// Center
shift = (totalWidth - width) / 2;
} else {
// Right
shift = totalWidth - width - hPadding;
}

return `/Tx BMC q BT ${defaultAppearance} 1 0 0 1 ${shift} ${vPadding} Tm (${value}) Tj ET Q EMC`;
}

async getFontData(evaluator, task) {
const operatorList = new OperatorList();
const initialState = new EvalState();
await evaluator.getOperatorList({
stream: new StringStream(this.data.defaultAppearance),
task,
resources: this.fieldResources,
operatorList,
initialState,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please note the default value in

pdf.js/src/core/evaluator.js

Lines 1224 to 1235 in 72d71ba

getOperatorList({
stream,
task,
resources,
operatorList,
initialState = null,
}) {
// Ensure that `resources`/`initialState` is correctly initialized,
// even if the provided parameter is e.g. `null`.
resources = resources || Dict.empty;
initialState = initialState || new EvalState();
hence you shouldn't need to pass the initialState here (nor export EvalState in the other file).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed EvalState export but I want to pass an initialState to be able to get the font and the font size from it in order to compute text width (and split it for multiline textfields).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you want to chat about that or anything else, then I'm on riot in the pdf.js channel, my nick is "calixte" (in CEST timezone).

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I completely overlooked that you actually accessed the state afterwards; in that case exporting/using it like you previously did is probably OK.

(Although it does feel slightly "hacky" to do things in that way, given how the getOperatorList method in currently implemented/used, however at the top of my head I don't really have a much better suggestion.)

});

// Even if there is an appearance stream, ignore it. This is the
// behaviour used by Adobe Reader.
if (!this.data.defaultAppearance) {
return Promise.resolve(operatorList);
}

const stream = new Stream(stringToBytes(this.data.defaultAppearance));
return evaluator
.getOperatorList({
stream,
task,
resources: this.fieldResources,
operatorList,
})
.then(function () {
return operatorList;
});
return [initialState.font, initialState.fontSize];
}
}

class ButtonWidgetAnnotation extends WidgetAnnotation {
constructor(params) {
super(params);

this.data.checkBox =
!this.hasFieldFlag(AnnotationFieldFlag.RADIO) &&
!this.hasFieldFlag(AnnotationFieldFlag.PUSHBUTTON);
Expand All @@ -970,6 +1087,31 @@ class ButtonWidgetAnnotation extends WidgetAnnotation {
}
}

getOperatorList(evaluator, task, renderForms, annotationStorage) {
if (annotationStorage) {
const value = annotationStorage[this.data.id] || false;
if (value && this.checkedAppearance) {
const savedAppearance = this.appearance;
this.appearance = this.checkedAppearance;
const operatorList = super.getOperatorList(
evaluator,
task,
renderForms,
annotationStorage
);
this.appearance = savedAppearance;
return operatorList;
}
return Promise.resolve(new OperatorList());
}
return super.getOperatorList(
evaluator,
task,
renderForms,
annotationStorage
);
}

_processCheckBox(params) {
if (isName(this.data.fieldValue)) {
this.data.fieldValue = this.data.fieldValue.name;
Expand All @@ -993,6 +1135,13 @@ class ButtonWidgetAnnotation extends WidgetAnnotation {

this.data.exportValue =
exportValues[0] === "Off" ? exportValues[1] : exportValues[0];

const normalAppearanceState = customAppearance.get("N");
if (!isDict(normalAppearanceState)) {
return;
}

this.checkedAppearance = normalAppearanceState.get(this.data.exportValue);
}

_processRadioButton(params) {
Expand Down Expand Up @@ -1023,6 +1172,7 @@ class ButtonWidgetAnnotation extends WidgetAnnotation {
break;
}
}
this.checkedAppearance = normalAppearanceState.get(this.data.buttonValue);
}

_processPushButton(params) {
Expand Down
16 changes: 14 additions & 2 deletions src/core/document.js
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,14 @@ class Page {
});
}

getOperatorList({ handler, sink, task, intent, renderInteractiveForms }) {
getOperatorList({
handler,
sink,
task,
intent,
renderInteractiveForms,
annotationStorage,
}) {
const contentStreamPromise = this.pdfManager.ensure(
this,
"getContentStream"
Expand Down Expand Up @@ -302,7 +309,12 @@ class Page {
if (isAnnotationRenderable(annotation, intent)) {
opListPromises.push(
annotation
.getOperatorList(partialEvaluator, task, renderInteractiveForms)
.getOperatorList(
partialEvaluator,
task,
renderInteractiveForms,
annotationStorage
)
.catch(function (reason) {
warn(
"getOperatorList - ignoring annotation data during " +
Expand Down
4 changes: 3 additions & 1 deletion src/core/evaluator.js
Original file line number Diff line number Diff line change
Expand Up @@ -783,6 +783,7 @@ class PartialEvaluator {
})
.then(translated => {
state.font = translated.font;
state.fontSize = fontArgs[1];
calixteman marked this conversation as resolved.
Show resolved Hide resolved
translated.send(this.handler);
return translated.loadedName;
});
Expand Down Expand Up @@ -3518,6 +3519,7 @@ class EvalState {
constructor() {
this.ctm = new Float32Array(IDENTITY_MATRIX);
this.font = null;
this.fontSize = 0;
this.textRenderingMode = TextRenderingMode.FILL;
this.fillColorSpace = ColorSpace.singletons.gray;
this.strokeColorSpace = ColorSpace.singletons.gray;
Expand Down Expand Up @@ -3801,4 +3803,4 @@ class EvaluatorPreprocessor {
}
}

export { PartialEvaluator };
export { EvalState, PartialEvaluator };
Loading