Skip to content

Commit 1747d25

Browse files
committed
Support textfield and choice widgets for printing
1 parent 63e33a5 commit 1747d25

File tree

7 files changed

+409
-46
lines changed

7 files changed

+409
-46
lines changed

src/core/annotation.js

+188-38
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,9 @@ import {
2121
AnnotationReplyType,
2222
AnnotationType,
2323
assert,
24+
escapeString,
2425
isString,
2526
OPS,
26-
stringToBytes,
2727
stringToPDFString,
2828
Util,
2929
warn,
@@ -33,7 +33,7 @@ import { Dict, isDict, isName, isRef, isStream } from "./primitives.js";
3333
import { ColorSpace } from "./colorspace.js";
3434
import { getInheritableProperty } from "./core_utils.js";
3535
import { OperatorList } from "./operator_list.js";
36-
import { Stream } from "./stream.js";
36+
import { StringStream } from "./stream.js";
3737

3838
class AnnotationFactory {
3939
/**
@@ -893,19 +893,199 @@ class WidgetAnnotation extends Annotation {
893893
if (renderForms) {
894894
return Promise.resolve(new OperatorList());
895895
}
896-
return super.getOperatorList(
897-
evaluator,
898-
task,
899-
renderForms,
900-
annotationStorage
896+
897+
if (!this._hasText) {
898+
return super.getOperatorList(
899+
evaluator,
900+
task,
901+
renderForms,
902+
annotationStorage
903+
);
904+
}
905+
906+
return this._getAppearance(evaluator, task, annotationStorage).then(
907+
content => {
908+
if (this.appearance && content === null) {
909+
return super.getOperatorList(
910+
evaluator,
911+
task,
912+
renderForms,
913+
annotationStorage
914+
);
915+
}
916+
917+
const operatorList = new OperatorList();
918+
919+
// Even if there is an appearance stream, ignore it. This is the
920+
// behaviour used by Adobe Reader.
921+
if (!this.data.defaultAppearance || content === null) {
922+
return operatorList;
923+
}
924+
925+
const matrix = [1, 0, 0, 1, 0, 0];
926+
const bbox = [
927+
0,
928+
0,
929+
this.data.rect[2] - this.data.rect[0],
930+
this.data.rect[3] - this.data.rect[1],
931+
];
932+
933+
const transform = getTransformMatrix(this.data.rect, bbox, matrix);
934+
operatorList.addOp(OPS.beginAnnotation, [
935+
this.data.rect,
936+
transform,
937+
matrix,
938+
]);
939+
940+
const stream = new StringStream(content);
941+
return evaluator
942+
.getOperatorList({
943+
stream,
944+
task,
945+
resources: this.fieldResources,
946+
operatorList,
947+
})
948+
.then(function () {
949+
operatorList.addOp(OPS.endAnnotation, []);
950+
return operatorList;
951+
});
952+
}
953+
);
954+
}
955+
956+
async _getAppearance(evaluator, task, annotationStorage) {
957+
const isPassword = this.hasFieldFlag(AnnotationFieldFlag.PASSWORD);
958+
if (!annotationStorage || isPassword) {
959+
return null;
960+
}
961+
let value = annotationStorage[this.data.id] || "";
962+
if (value === "") {
963+
return null;
964+
}
965+
value = escapeString(value);
966+
967+
const defaultPadding = 2;
968+
const hPadding = defaultPadding;
969+
const totalHeight = this.data.rect[3] - this.data.rect[1];
970+
const totalWidth = this.data.rect[2] - this.data.rect[0];
971+
972+
const fontInfo = await this._getFontData(evaluator, task);
973+
const [font, fontName] = fontInfo;
974+
let fontSize = fontInfo[2];
975+
976+
fontSize = this._computeFontSize(font, fontName, fontSize, totalHeight);
977+
978+
let descent = font.descent;
979+
if (isNaN(descent)) {
980+
descent = 0;
981+
}
982+
983+
const vPadding = defaultPadding + Math.abs(descent) * fontSize;
984+
const defaultAppearance = this.data.defaultAppearance;
985+
const alignment = this.data.textAlignment;
986+
if (alignment === 0 || alignment > 2) {
987+
// Left alignment: nothing to do
988+
return (
989+
"/Tx BMC q BT " +
990+
defaultAppearance +
991+
` 1 0 0 1 ${hPadding} ${vPadding} Tm (${value}) Tj` +
992+
" ET Q EMC"
993+
);
994+
}
995+
996+
const renderedText = this._renderText(
997+
value,
998+
font,
999+
fontSize,
1000+
totalWidth,
1001+
alignment,
1002+
hPadding,
1003+
vPadding
1004+
);
1005+
return (
1006+
"/Tx BMC q BT " +
1007+
defaultAppearance +
1008+
` 1 0 0 1 0 0 Tm ${renderedText}` +
1009+
" ET Q EMC"
9011010
);
9021011
}
1012+
1013+
async _getFontData(evaluator, task) {
1014+
const operatorList = new OperatorList();
1015+
const initialState = {
1016+
fontSize: 0,
1017+
font: null,
1018+
fontName: null,
1019+
clone() {
1020+
return this;
1021+
},
1022+
};
1023+
1024+
await evaluator.getOperatorList({
1025+
stream: new StringStream(this.data.defaultAppearance),
1026+
task,
1027+
resources: this.fieldResources,
1028+
operatorList,
1029+
initialState,
1030+
});
1031+
1032+
return [initialState.font, initialState.fontName, initialState.fontSize];
1033+
}
1034+
1035+
_computeFontSize(font, fontName, fontSize, height) {
1036+
if (fontSize === null || fontSize === 0) {
1037+
const em = font.charsToGlyphs("M", true)[0].width / 1000;
1038+
// According to https://en.wikipedia.org/wiki/Em_(typography)
1039+
// an average cap height should be 70% of 1em
1040+
const capHeight = 0.7 * em;
1041+
// 1.5 * capHeight * fontSize seems to be a good value for lineHeight
1042+
fontSize = Math.max(1, Math.floor(height / (1.5 * capHeight)));
1043+
1044+
let fontRegex = new RegExp(`/${fontName}\\s+[0-9\.]+\\s+Tf`);
1045+
if (this.data.defaultAppearance.search(fontRegex) === -1) {
1046+
// The font size is missing
1047+
fontRegex = new RegExp(`/${fontName}\\s+Tf`);
1048+
}
1049+
this.data.defaultAppearance = this.data.defaultAppearance.replace(
1050+
fontRegex,
1051+
`/${fontName} ${fontSize} Tf`
1052+
);
1053+
}
1054+
return fontSize;
1055+
}
1056+
1057+
_renderText(text, font, fontSize, totalWidth, alignment, hPadding, vPadding) {
1058+
// We need to get the width of the text in order to align it correctly
1059+
const glyphs = font.charsToGlyphs(text);
1060+
const scale = fontSize / 1000;
1061+
let width = 0;
1062+
for (const glyph of glyphs) {
1063+
width += glyph.width * scale;
1064+
}
1065+
1066+
let shift;
1067+
if (alignment === 1) {
1068+
// Center
1069+
shift = (totalWidth - width) / 2;
1070+
} else if (alignment === 2) {
1071+
// Right
1072+
shift = totalWidth - width - hPadding;
1073+
} else {
1074+
shift = hPadding;
1075+
}
1076+
shift = shift.toFixed(2);
1077+
vPadding = vPadding.toFixed(2);
1078+
1079+
return `${shift} ${vPadding} Td (${text}) Tj`;
1080+
}
9031081
}
9041082

9051083
class TextWidgetAnnotation extends WidgetAnnotation {
9061084
constructor(params) {
9071085
super(params);
9081086

1087+
this._hasText = true;
1088+
9091089
const dict = params.dict;
9101090

9111091
// The field value is always a string.
@@ -934,37 +1114,6 @@ class TextWidgetAnnotation extends WidgetAnnotation {
9341114
!this.hasFieldFlag(AnnotationFieldFlag.FILESELECT) &&
9351115
this.data.maxLen !== null;
9361116
}
937-
938-
getOperatorList(evaluator, task, renderForms, annotationStorage) {
939-
if (renderForms || this.appearance) {
940-
return super.getOperatorList(
941-
evaluator,
942-
task,
943-
renderForms,
944-
annotationStorage
945-
);
946-
}
947-
948-
const operatorList = new OperatorList();
949-
950-
// Even if there is an appearance stream, ignore it. This is the
951-
// behaviour used by Adobe Reader.
952-
if (!this.data.defaultAppearance) {
953-
return Promise.resolve(operatorList);
954-
}
955-
956-
const stream = new Stream(stringToBytes(this.data.defaultAppearance));
957-
return evaluator
958-
.getOperatorList({
959-
stream,
960-
task,
961-
resources: this.fieldResources,
962-
operatorList,
963-
})
964-
.then(function () {
965-
return operatorList;
966-
});
967-
}
9681117
}
9691118

9701119
class ButtonWidgetAnnotation extends WidgetAnnotation {
@@ -1148,6 +1297,7 @@ class ChoiceWidgetAnnotation extends WidgetAnnotation {
11481297
// Process field flags for the display layer.
11491298
this.data.combo = this.hasFieldFlag(AnnotationFieldFlag.COMBO);
11501299
this.data.multiSelect = this.hasFieldFlag(AnnotationFieldFlag.MULTISELECT);
1300+
this._hasText = true;
11511301
}
11521302
}
11531303

src/core/evaluator.js

+5-1
Original file line numberDiff line numberDiff line change
@@ -727,10 +727,12 @@ class PartialEvaluator {
727727

728728
handleSetFont(resources, fontArgs, fontRef, operatorList, task, state) {
729729
// TODO(mack): Not needed?
730-
var fontName;
730+
var fontName,
731+
fontSize = 0;
731732
if (fontArgs) {
732733
fontArgs = fontArgs.slice();
733734
fontName = fontArgs[0].name;
735+
fontSize = fontArgs[1];
734736
}
735737

736738
return this.loadFont(fontName, fontRef, resources)
@@ -763,6 +765,8 @@ class PartialEvaluator {
763765
})
764766
.then(translated => {
765767
state.font = translated.font;
768+
state.fontSize = fontSize;
769+
state.fontName = fontName;
766770
translated.send(this.handler);
767771
return translated.loadedName;
768772
});

src/display/annotation_layer.js

+19-2
Original file line numberDiff line numberDiff line change
@@ -441,6 +441,8 @@ class TextWidgetAnnotationElement extends WidgetAnnotationElement {
441441
*/
442442
render() {
443443
const TEXT_ALIGNMENT = ["left", "center", "right"];
444+
const storage = this.annotationStorage;
445+
const id = this.data.id;
444446

445447
this.container.className = "textWidgetAnnotation";
446448

@@ -449,15 +451,21 @@ class TextWidgetAnnotationElement extends WidgetAnnotationElement {
449451
// NOTE: We cannot set the values using `element.value` below, since it
450452
// prevents the AnnotationLayer rasterizer in `test/driver.js`
451453
// from parsing the elements correctly for the reference tests.
454+
const textContent = storage.getOrCreateValue(id, this.data.fieldValue);
455+
452456
if (this.data.multiLine) {
453457
element = document.createElement("textarea");
454-
element.textContent = this.data.fieldValue;
458+
element.textContent = textContent;
455459
} else {
456460
element = document.createElement("input");
457461
element.type = "text";
458-
element.setAttribute("value", this.data.fieldValue);
462+
element.setAttribute("value", textContent);
459463
}
460464

465+
element.addEventListener("change", function (event) {
466+
storage.setValue(id, event.target.value);
467+
});
468+
461469
element.disabled = this.data.readOnly;
462470
element.name = this.data.fieldName;
463471

@@ -654,6 +662,8 @@ class ChoiceWidgetAnnotationElement extends WidgetAnnotationElement {
654662
*/
655663
render() {
656664
this.container.className = "choiceWidgetAnnotation";
665+
const storage = this.annotationStorage;
666+
const id = this.data.id;
657667

658668
const selectElement = document.createElement("select");
659669
selectElement.disabled = this.data.readOnly;
@@ -674,10 +684,17 @@ class ChoiceWidgetAnnotationElement extends WidgetAnnotationElement {
674684
optionElement.value = option.exportValue;
675685
if (this.data.fieldValue.includes(option.displayValue)) {
676686
optionElement.setAttribute("selected", true);
687+
storage.setValue(id, option.displayValue);
677688
}
678689
selectElement.appendChild(optionElement);
679690
}
680691

692+
selectElement.addEventListener("change", function (event) {
693+
const options = event.target.options;
694+
const value = options[options.selectedIndex].text;
695+
storage.setValue(id, value);
696+
});
697+
681698
this.container.appendChild(selectElement);
682699
return this.container;
683700
}

src/shared/util.js

+7
Original file line numberDiff line numberDiff line change
@@ -793,6 +793,12 @@ function stringToPDFString(str) {
793793
return strBuf.join("");
794794
}
795795

796+
function escapeString(str) {
797+
// replace "(", ")" and "\" by "\(", "\)" and "\\"
798+
// in order to write it in a PDF file.
799+
return str.replace(/([\(\)\\])/g, "\\$1");
800+
}
801+
796802
function stringToUTF8String(str) {
797803
return decodeURIComponent(escape(str));
798804
}
@@ -927,6 +933,7 @@ export {
927933
bytesToString,
928934
createPromiseCapability,
929935
createObjectURL,
936+
escapeString,
930937
getVerbosityLevel,
931938
info,
932939
isArrayBuffer,

0 commit comments

Comments
 (0)