Skip to content

Commit cd8bb72

Browse files
committed
Support multiline textfields for printing
1 parent 8c162f5 commit cd8bb72

File tree

3 files changed

+225
-7
lines changed

3 files changed

+225
-7
lines changed

src/core/annotation.js

+115-5
Original file line numberDiff line numberDiff line change
@@ -958,11 +958,10 @@ class WidgetAnnotation extends Annotation {
958958
if (!annotationStorage || isPassword) {
959959
return null;
960960
}
961-
let value = annotationStorage[this.data.id] || "";
961+
const value = annotationStorage[this.data.id];
962962
if (value === "") {
963-
return null;
963+
return "";
964964
}
965-
value = escapeString(value);
966965

967966
const defaultPadding = 2;
968967
const hPadding = defaultPadding;
@@ -983,12 +982,27 @@ class WidgetAnnotation extends Annotation {
983982
const vPadding = defaultPadding + Math.abs(descent) * fontSize;
984983
const defaultAppearance = this.data.defaultAppearance;
985984
const alignment = this.data.textAlignment;
985+
986+
if (this.data.multiLine) {
987+
return this._getMultilineAppearance(
988+
defaultAppearance,
989+
value,
990+
font,
991+
fontSize,
992+
totalWidth,
993+
totalHeight,
994+
alignment,
995+
hPadding,
996+
vPadding
997+
);
998+
}
999+
9861000
if (alignment === 0 || alignment > 2) {
9871001
// Left alignment: nothing to do
9881002
return (
9891003
"/Tx BMC q BT " +
9901004
defaultAppearance +
991-
` 1 0 0 1 ${hPadding} ${vPadding} Tm (${value}) Tj` +
1005+
` 1 0 0 1 ${hPadding} ${vPadding} Tm (${escapeString(value)}) Tj` +
9921006
" ET Q EMC"
9931007
);
9941008
}
@@ -1076,7 +1090,7 @@ class WidgetAnnotation extends Annotation {
10761090
shift = shift.toFixed(2);
10771091
vPadding = vPadding.toFixed(2);
10781092

1079-
return `${shift} ${vPadding} Td (${text}) Tj`;
1093+
return `${shift} ${vPadding} Td (${escapeString(text)}) Tj`;
10801094
}
10811095
}
10821096

@@ -1114,6 +1128,102 @@ class TextWidgetAnnotation extends WidgetAnnotation {
11141128
!this.hasFieldFlag(AnnotationFieldFlag.FILESELECT) &&
11151129
this.data.maxLen !== null;
11161130
}
1131+
1132+
_getMultilineAppearance(
1133+
defaultAppearance,
1134+
text,
1135+
font,
1136+
fontSize,
1137+
width,
1138+
height,
1139+
alignment,
1140+
hPadding,
1141+
vPadding
1142+
) {
1143+
const lines = text.split(/\r\n|\r|\n/);
1144+
const buf = [];
1145+
const totalWidth = width - 2 * hPadding;
1146+
for (const line of lines) {
1147+
const chunks = this._splitLine(line, font, fontSize, totalWidth);
1148+
for (const chunk of chunks) {
1149+
const padding = buf.length === 0 ? hPadding : 0;
1150+
buf.push(
1151+
this._renderText(
1152+
chunk,
1153+
font,
1154+
fontSize,
1155+
width,
1156+
alignment,
1157+
padding,
1158+
-fontSize // <0 because a line is below the previous one
1159+
)
1160+
);
1161+
}
1162+
}
1163+
1164+
const renderedText = buf.join("\n");
1165+
return (
1166+
"/Tx BMC q BT " +
1167+
defaultAppearance +
1168+
` 1 0 0 1 0 ${height} Tm ${renderedText}` +
1169+
" ET Q EMC"
1170+
);
1171+
}
1172+
1173+
_splitLine(line, font, fontSize, width) {
1174+
if (line.length <= 1) {
1175+
// Nothing to split
1176+
return [line];
1177+
}
1178+
1179+
const scale = fontSize / 1000;
1180+
const whitespace = font.charsToGlyphs(" ", true)[0].width * scale;
1181+
const chunks = [];
1182+
1183+
let lastSpacePos = -1,
1184+
startChunk = 0,
1185+
currentWidth = 0;
1186+
1187+
for (let i = 0, ii = line.length; i < ii; i++) {
1188+
const character = line.charAt(i);
1189+
if (character === " ") {
1190+
if (currentWidth + whitespace > width) {
1191+
// We can break here
1192+
chunks.push(line.substring(startChunk, i));
1193+
startChunk = i;
1194+
currentWidth = whitespace;
1195+
lastSpacePos = -1;
1196+
} else {
1197+
currentWidth += whitespace;
1198+
lastSpacePos = i;
1199+
}
1200+
} else {
1201+
const charWidth = font.charsToGlyphs(character, false)[0].width * scale;
1202+
if (currentWidth + charWidth > width) {
1203+
// We must break to the last white position (if available)
1204+
if (lastSpacePos !== -1) {
1205+
chunks.push(line.substring(startChunk, lastSpacePos + 1));
1206+
startChunk = i = lastSpacePos + 1;
1207+
lastSpacePos = -1;
1208+
currentWidth = 0;
1209+
} else {
1210+
// Just break in the middle of the word
1211+
chunks.push(line.substring(startChunk, i));
1212+
startChunk = i;
1213+
currentWidth = charWidth;
1214+
}
1215+
} else {
1216+
currentWidth += charWidth;
1217+
}
1218+
}
1219+
}
1220+
1221+
if (startChunk < line.length) {
1222+
chunks.push(line.substring(startChunk, line.length));
1223+
}
1224+
1225+
return chunks;
1226+
}
11171227
}
11181228

11191229
class ButtonWidgetAnnotation extends WidgetAnnotation {

src/display/annotation_layer.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -462,7 +462,7 @@ class TextWidgetAnnotationElement extends WidgetAnnotationElement {
462462
element.setAttribute("value", textContent);
463463
}
464464

465-
element.addEventListener("change", function (event) {
465+
element.addEventListener("input", function (event) {
466466
storage.setValue(id, event.target.value);
467467
});
468468

@@ -689,7 +689,7 @@ class ChoiceWidgetAnnotationElement extends WidgetAnnotationElement {
689689
selectElement.appendChild(optionElement);
690690
}
691691

692-
selectElement.addEventListener("change", function (event) {
692+
selectElement.addEventListener("input", function (event) {
693693
const options = event.target.options;
694694
const value = options[options.selectedIndex].text;
695695
storage.setValue(id, value);

test/unit/annotation_spec.js

+108
Original file line numberDiff line numberDiff line change
@@ -1673,6 +1673,114 @@ describe("annotation", function () {
16731673
done();
16741674
}, done.fail);
16751675
});
1676+
1677+
it("should render multiline text for printing", function (done) {
1678+
textWidgetDict.set("Ff", AnnotationFieldFlag.MULTILINE);
1679+
1680+
const textWidgetRef = Ref.get(271, 0);
1681+
const xref = new XRefMock([
1682+
{ ref: textWidgetRef, data: textWidgetDict },
1683+
fontRefObj,
1684+
]);
1685+
const task = new WorkerTask("test print");
1686+
partialEvaluator.xref = xref;
1687+
1688+
AnnotationFactory.create(
1689+
xref,
1690+
textWidgetRef,
1691+
pdfManagerMock,
1692+
idFactoryMock
1693+
)
1694+
.then(annotation => {
1695+
const id = annotation.data.id;
1696+
const annotationStorage = {};
1697+
annotationStorage[id] =
1698+
"a aa aaa aaaa aaaaa aaaaaa " +
1699+
"pneumonoultramicroscopicsilicovolcanoconiosis";
1700+
return annotation._getAppearance(
1701+
partialEvaluator,
1702+
task,
1703+
annotationStorage
1704+
);
1705+
}, done.fail)
1706+
.then(appearance => {
1707+
expect(appearance).toEqual(
1708+
"/Tx BMC q BT /Helv 5 Tf 1 0 0 1 0 10 Tm " +
1709+
"2.00 -5.00 Td (a aa aaa ) Tj\n" +
1710+
"0.00 -5.00 Td (aaaa aaaaa ) Tj\n" +
1711+
"0.00 -5.00 Td (aaaaaa ) Tj\n" +
1712+
"0.00 -5.00 Td (pneumonoultr) Tj\n" +
1713+
"0.00 -5.00 Td (amicroscopi) Tj\n" +
1714+
"0.00 -5.00 Td (csilicovolca) Tj\n" +
1715+
"0.00 -5.00 Td (noconiosis) Tj ET Q EMC"
1716+
);
1717+
done();
1718+
}, done.fail);
1719+
});
1720+
1721+
it("should render multiline text with various EOL for printing", function (done) {
1722+
textWidgetDict.set("Ff", AnnotationFieldFlag.MULTILINE);
1723+
textWidgetDict.set("Rect", [0, 0, 128, 10]);
1724+
1725+
const textWidgetRef = Ref.get(271, 0);
1726+
const xref = new XRefMock([
1727+
{ ref: textWidgetRef, data: textWidgetDict },
1728+
fontRefObj,
1729+
]);
1730+
const task = new WorkerTask("test print");
1731+
partialEvaluator.xref = xref;
1732+
const expectedAppearance =
1733+
"/Tx BMC q BT /Helv 5 Tf 1 0 0 1 0 10 Tm " +
1734+
"2.00 -5.00 Td " +
1735+
"(Lorem ipsum dolor sit amet, consectetur adipiscing elit.) Tj\n" +
1736+
"0.00 -5.00 Td " +
1737+
"(Aliquam vitae felis ac lectus bibendum ultricies quis non) Tj\n" +
1738+
"0.00 -5.00 Td " +
1739+
"( diam.) Tj\n" +
1740+
"0.00 -5.00 Td " +
1741+
"(Morbi id porttitor quam, a iaculis dui.) Tj\n" +
1742+
"0.00 -5.00 Td " +
1743+
"(Pellentesque habitant morbi tristique senectus et netus ) Tj\n" +
1744+
"0.00 -5.00 Td " +
1745+
"(et malesuada fames ac turpis egestas.) Tj\n" +
1746+
"0.00 -5.00 Td () Tj\n" +
1747+
"0.00 -5.00 Td () Tj\n" +
1748+
"0.00 -5.00 Td " +
1749+
"(Nulla consectetur, ligula in tincidunt placerat, velit ) Tj\n" +
1750+
"0.00 -5.00 Td " +
1751+
"(augue consectetur orci, sed mattis libero nunc ut massa.) Tj\n" +
1752+
"0.00 -5.00 Td " +
1753+
"(Etiam facilisis tempus interdum.) Tj ET Q EMC";
1754+
1755+
AnnotationFactory.create(
1756+
xref,
1757+
textWidgetRef,
1758+
pdfManagerMock,
1759+
idFactoryMock
1760+
)
1761+
.then(annotation => {
1762+
const id = annotation.data.id;
1763+
const annotationStorage = {};
1764+
annotationStorage[id] =
1765+
"Lorem ipsum dolor sit amet, consectetur adipiscing elit.\r" +
1766+
"Aliquam vitae felis ac lectus bibendum ultricies quis non diam.\n" +
1767+
"Morbi id porttitor quam, a iaculis dui.\r\n" +
1768+
"Pellentesque habitant morbi tristique senectus et " +
1769+
"netus et malesuada fames ac turpis egestas.\n\r\n\r" +
1770+
"Nulla consectetur, ligula in tincidunt placerat, " +
1771+
"velit augue consectetur orci, sed mattis libero nunc ut massa.\r" +
1772+
"Etiam facilisis tempus interdum.";
1773+
return annotation._getAppearance(
1774+
partialEvaluator,
1775+
task,
1776+
annotationStorage
1777+
);
1778+
}, done.fail)
1779+
.then(appearance => {
1780+
expect(appearance).toEqual(expectedAppearance);
1781+
done();
1782+
}, done.fail);
1783+
});
16761784
});
16771785

16781786
describe("ButtonWidgetAnnotation", function () {

0 commit comments

Comments
 (0)