diff --git a/src/core/annotation.js b/src/core/annotation.js index 4012868c8..b0bbf8e1f 100644 --- a/src/core/annotation.js +++ b/src/core/annotation.js @@ -33,9 +33,14 @@ import { Util, warn, } from "../shared/util.js"; -import { collectActions, getInheritableProperty } from "./core_utils.js"; +import { + collectActions, + getInheritableProperty, + numberToString, +} from "./core_utils.js"; import { createDefaultAppearance, + getPdfColor, parseDefaultAppearance, } from "./default_appearance.js"; import { Dict, isName, Name, Ref, RefSet } from "./primitives.js"; @@ -663,6 +668,27 @@ class Annotation { } } + getBorderAndBackgroundAppearances() { + if (!this.backgroundColor && !this.borderColor) { + return ""; + } + const width = this.data.rect[2] - this.data.rect[0]; + const height = this.data.rect[3] - this.data.rect[1]; + const rect = `0 0 ${width} ${height} re`; + + let str = ""; + if (this.backgroundColor) { + str = `${getPdfColor(this.backgroundColor)} ${rect} f `; + } + + if (this.borderColor) { + const borderWidth = this.borderStyle.width || 1; + str += `${borderWidth} w ${getPdfColor(this.borderColor)} ${rect} S `; + } + + return str; + } + /** * Set the border style (as AnnotationBorderStyle object). * @@ -1609,7 +1635,13 @@ class WidgetAnnotation extends Annotation { descent = 0; } - const vPadding = defaultPadding + Math.abs(descent) * fontSize; + // Take into account the space we have to compute the default vertical + // padding. + const defaultVPadding = Math.min( + Math.floor((totalHeight - fontSize) / 2), + defaultPadding + ); + const vPadding = defaultVPadding + Math.abs(descent) * fontSize; const alignment = this.data.textAlignment; if (this.data.multiLine) { @@ -1640,10 +1672,13 @@ class WidgetAnnotation extends Annotation { ); } + // Empty or it has a trailing whitespace. + const colors = this.getBorderAndBackgroundAppearances(); + if (alignment === 0 || alignment > 2) { // Left alignment: nothing to do return ( - "/Tx BMC q BT " + + `/Tx BMC q ${colors}BT ` + defaultAppearance + ` 1 0 0 1 ${hPadding} ${vPadding} Tm (${escapeString( encodedString @@ -1662,7 +1697,7 @@ class WidgetAnnotation extends Annotation { vPadding ); return ( - "/Tx BMC q BT " + + `/Tx BMC q ${colors}BT ` + defaultAppearance + ` 1 0 0 1 0 0 Tm ${renderedText}` + " ET Q EMC" @@ -1790,8 +1825,8 @@ class WidgetAnnotation extends Annotation { } else { shift = hPadding; } - shift = shift.toFixed(2); - vPadding = vPadding.toFixed(2); + shift = numberToString(shift); + vPadding = numberToString(vPadding); return `${shift} ${vPadding} Td (${escapeString(text)}) Tj`; } @@ -1889,16 +1924,18 @@ class TextWidgetAnnotation extends WidgetAnnotation { } _getCombAppearance(defaultAppearance, font, text, width, hPadding, vPadding) { - const combWidth = (width / this.data.maxLen).toFixed(2); + const combWidth = numberToString(width / this.data.maxLen); const buf = []; const positions = font.getCharPositions(text); for (const [start, end] of positions) { buf.push(`(${escapeString(text.substring(start, end))}) Tj`); } + // Empty or it has a trailing whitespace. + const colors = this.getBorderAndBackgroundAppearances(); const renderedComb = buf.join(` ${combWidth} 0 Td `); return ( - "/Tx BMC q BT " + + `/Tx BMC q ${colors}BT ` + defaultAppearance + ` 1 0 0 1 ${hPadding} ${vPadding} Tm ${renderedComb}` + " ET Q EMC" @@ -1938,8 +1975,12 @@ class TextWidgetAnnotation extends WidgetAnnotation { } const renderedText = buf.join("\n"); + + // Empty or it has a trailing whitespace. + const colors = this.getBorderAndBackgroundAppearances(); + return ( - "/Tx BMC q BT " + + `/Tx BMC q ${colors}BT ` + defaultAppearance + ` 1 0 0 1 0 ${height} Tm ${renderedText}` + " ET Q EMC" @@ -2295,8 +2336,8 @@ class ButtonWidgetAnnotation extends WidgetAnnotation { } // Values to center the glyph in the bbox. - const xShift = (width - metrics.width) / 2; - const yShift = (height - metrics.height) / 2; + const xShift = numberToString((width - metrics.width) / 2); + const yShift = numberToString((height - metrics.height) / 2); const appearance = `q BT /PdfJsZaDb ${fontSize} Tf 0 g ${xShift} ${yShift} Td (${char}) Tj ET Q`; diff --git a/src/core/core_utils.js b/src/core/core_utils.js index 8366b03fc..3e258c964 100644 --- a/src/core/core_utils.js +++ b/src/core/core_utils.js @@ -531,6 +531,23 @@ function recoverJsURL(str) { return null; } +function numberToString(value) { + if (Number.isInteger(value)) { + return value.toString(); + } + + const roundedValue = Math.round(value * 100); + if (roundedValue % 100 === 0) { + return (roundedValue / 100).toString(); + } + + if (roundedValue % 10 === 0) { + return value.toFixed(1); + } + + return value.toFixed(2); +} + export { collectActions, DocStats, @@ -542,6 +559,7 @@ export { isWhiteSpace, log2, MissingDataException, + numberToString, ParserEOFException, parseXFAPath, readInt8, diff --git a/src/core/default_appearance.js b/src/core/default_appearance.js index 4b2d9eb6e..82cc91b92 100644 --- a/src/core/default_appearance.js +++ b/src/core/default_appearance.js @@ -13,9 +13,9 @@ * limitations under the License. */ +import { escapePDFName, numberToString } from "./core_utils.js"; import { OPS, warn } from "../shared/util.js"; import { ColorSpace } from "./colorspace.js"; -import { escapePDFName } from "./core_utils.js"; import { EvaluatorPreprocessor } from "./evaluator.js"; import { Name } from "./primitives.js"; import { StringStream } from "./stream.js"; @@ -82,18 +82,21 @@ function parseDefaultAppearance(str) { return new DefaultAppearanceEvaluator(str).parse(); } -// Create default appearance string from some information. -function createDefaultAppearance({ fontSize, fontName, fontColor }) { - let colorCmd; - if (fontColor.every(c => c === 0)) { - colorCmd = "0 g"; - } else { - colorCmd = - Array.from(fontColor) - .map(c => (c / 255).toFixed(2)) - .join(" ") + " rg"; +function getPdfColor(color) { + if (color[0] === color[1] && color[1] === color[2]) { + const gray = color[0] / 255; + return `${numberToString(gray)} g`; } - return `/${escapePDFName(fontName)} ${fontSize} Tf ${colorCmd}`; + return ( + Array.from(color) + .map(c => numberToString(c / 255)) + .join(" ") + " rg" + ); } -export { createDefaultAppearance, parseDefaultAppearance }; +// Create default appearance string from some information. +function createDefaultAppearance({ fontSize, fontName, fontColor }) { + return `/${escapePDFName(fontName)} ${fontSize} Tf ${getPdfColor(fontColor)}`; +} + +export { createDefaultAppearance, getPdfColor, parseDefaultAppearance }; diff --git a/src/core/writer.js b/src/core/writer.js index 406d8304d..a30c7791d 100644 --- a/src/core/writer.js +++ b/src/core/writer.js @@ -15,7 +15,7 @@ import { bytesToString, escapeString, warn } from "../shared/util.js"; import { Dict, Name, Ref } from "./primitives.js"; -import { escapePDFName, parseXFAPath } from "./core_utils.js"; +import { escapePDFName, numberToString, parseXFAPath } from "./core_utils.js"; import { SimpleDOMNode, SimpleXMLParser } from "./xml_parser.js"; import { BaseStream } from "./base_stream.js"; import { calculateMD5 } from "./crypto.js"; @@ -53,23 +53,6 @@ function writeArray(array, buffer, transform) { buffer.push("]"); } -function numberToString(value) { - if (Number.isInteger(value)) { - return value.toString(); - } - - const roundedValue = Math.round(value * 100); - if (roundedValue % 100 === 0) { - return (roundedValue / 100).toString(); - } - - if (roundedValue % 10 === 0) { - return value.toFixed(1); - } - - return value.toFixed(2); -} - function writeValue(value, buffer, transform) { if (value instanceof Name) { buffer.push(`/${escapePDFName(value.name)}`); diff --git a/test/pdfs/issue14928.pdf.link b/test/pdfs/issue14928.pdf.link new file mode 100644 index 000000000..5e1a91b4e --- /dev/null +++ b/test/pdfs/issue14928.pdf.link @@ -0,0 +1,2 @@ +https://github.com/mozilla/pdf.js/files/8710789/cerfa_15615-01-2.pdf + diff --git a/test/test_manifest.json b/test/test_manifest.json index f6a79dcec..0bca13fe1 100644 --- a/test/test_manifest.json +++ b/test/test_manifest.json @@ -6480,5 +6480,19 @@ "link": true, "lastPage": 1, "type": "eq" + }, + { "id": "issue14928", + "file": "pdfs/issue14928.pdf", + "md5": "9687feb927eeb1b2a6f0794ea00f5c60", + "rounds": 1, + "link": true, + "lastPage": 1, + "type": "eq", + "print": true, + "annotationStorage": { + "1483R": { + "value": "05/17/2022" + } + } } ] diff --git a/test/unit/annotation_spec.js b/test/unit/annotation_spec.js index a3accfca4..58c28a19c 100644 --- a/test/unit/annotation_spec.js +++ b/test/unit/annotation_spec.js @@ -1614,7 +1614,7 @@ describe("annotation", function () { ); expect(appearance).toEqual( "/Tx BMC q BT /Helv 5 Tf 1 0 0 1 0 0 Tm" + - " 2.00 3.04 Td (test\\\\print) Tj ET Q EMC" + " 2 3.04 Td (test\\\\print) Tj ET Q EMC" ); }); @@ -1650,7 +1650,7 @@ describe("annotation", function () { "\x30\x53\x30\x93\x30\x6b\x30\x61\x30\x6f\x4e\x16\x75\x4c\x30\x6e"; expect(appearance).toEqual( "/Tx BMC q BT /Goth 5 Tf 1 0 0 1 0 0 Tm" + - ` 2.00 2.00 Td (${utf16String}) Tj ET Q EMC` + ` 2 2 Td (${utf16String}) Tj ET Q EMC` ); }); @@ -1733,7 +1733,7 @@ describe("annotation", function () { ); expect(appearance).toEqual( "/Tx BMC q BT /Helv 5.92 Tf 0 g 1 0 0 1 0 0 Tm" + - " 2.00 3.23 Td (test \\(print\\)) Tj ET Q EMC" + " 2 3.23 Td (test \\(print\\)) Tj ET Q EMC" ); }); @@ -1769,7 +1769,7 @@ describe("annotation", function () { "\x30\x53\x30\x93\x30\x6b\x30\x61\x30\x6f\x4e\x16\x75\x4c\x30\x6e"; expect(appearance).toEqual( "/Tx BMC q BT /Goth 3.5 Tf 0 g 1 0 0 1 0 0 Tm" + - ` 2.00 2.00 Td (${utf16String}) Tj ET Q EMC` + ` 2 2 Td (${utf16String}) Tj ET Q EMC` ); }); @@ -1832,13 +1832,13 @@ describe("annotation", function () { ); expect(appearance).toEqual( "/Tx BMC q BT /Helv 5 Tf 1 0 0 1 0 10 Tm " + - "2.00 -5.00 Td (a aa aaa ) Tj\n" + - "0.00 -5.00 Td (aaaa aaaaa ) Tj\n" + - "0.00 -5.00 Td (aaaaaa ) Tj\n" + - "0.00 -5.00 Td (pneumonoultr) Tj\n" + - "0.00 -5.00 Td (amicroscopi) Tj\n" + - "0.00 -5.00 Td (csilicovolca) Tj\n" + - "0.00 -5.00 Td (noconiosis) Tj ET Q EMC" + "2 -5 Td (a aa aaa ) Tj\n" + + "0 -5 Td (aaaa aaaaa ) Tj\n" + + "0 -5 Td (aaaaaa ) Tj\n" + + "0 -5 Td (pneumonoultr) Tj\n" + + "0 -5 Td (amicroscopi) Tj\n" + + "0 -5 Td (csilicovolca) Tj\n" + + "0 -5 Td (noconiosis) Tj ET Q EMC" ); }); @@ -1873,8 +1873,8 @@ describe("annotation", function () { ); expect(appearance).toEqual( "/Tx BMC q BT /Goth 5 Tf 1 0 0 1 0 10 Tm " + - "2.00 -5.00 Td (\x30\x53\x30\x93\x30\x6b\x30\x61\x30\x6f) Tj\n" + - "0.00 -5.00 Td (\x4e\x16\x75\x4c\x30\x6e) Tj ET Q EMC" + "2 -5 Td (\x30\x53\x30\x93\x30\x6b\x30\x61\x30\x6f) Tj\n" + + "0 -5 Td (\x4e\x16\x75\x4c\x30\x6e) Tj ET Q EMC" ); }); @@ -1891,25 +1891,25 @@ describe("annotation", function () { partialEvaluator.xref = xref; const expectedAppearance = "/Tx BMC q BT /Helv 5 Tf 1 0 0 1 0 10 Tm " + - "2.00 -5.00 Td " + + "2 -5 Td " + "(Lorem ipsum dolor sit amet, consectetur adipiscing elit.) Tj\n" + - "0.00 -5.00 Td " + + "0 -5 Td " + "(Aliquam vitae felis ac lectus bibendum ultricies quis non) Tj\n" + - "0.00 -5.00 Td " + + "0 -5 Td " + "( diam.) Tj\n" + - "0.00 -5.00 Td " + + "0 -5 Td " + "(Morbi id porttitor quam, a iaculis dui.) Tj\n" + - "0.00 -5.00 Td " + + "0 -5 Td " + "(Pellentesque habitant morbi tristique senectus et netus ) Tj\n" + - "0.00 -5.00 Td " + + "0 -5 Td " + "(et malesuada fames ac turpis egestas.) Tj\n" + - "0.00 -5.00 Td () Tj\n" + - "0.00 -5.00 Td () Tj\n" + - "0.00 -5.00 Td " + + "0 -5 Td () Tj\n" + + "0 -5 Td () Tj\n" + + "0 -5 Td " + "(Nulla consectetur, ligula in tincidunt placerat, velit ) Tj\n" + - "0.00 -5.00 Td " + + "0 -5 Td " + "(augue consectetur orci, sed mattis libero nunc ut massa.) Tj\n" + - "0.00 -5.00 Td " + + "0 -5 Td " + "(Etiam facilisis tempus interdum.) Tj ET Q EMC"; const annotation = await AnnotationFactory.create( @@ -1967,10 +1967,10 @@ describe("annotation", function () { ); expect(appearance).toEqual( "/Tx BMC q BT /Helv 5 Tf 1 0 0 1 2 3.035 Tm" + - " (a) Tj 8.00 0 Td (a) Tj 8.00 0 Td (\\() Tj" + - " 8.00 0 Td (a) Tj 8.00 0 Td (a) Tj" + - " 8.00 0 Td (\\)) Tj 8.00 0 Td (a) Tj" + - " 8.00 0 Td (\\\\) Tj ET Q EMC" + " (a) Tj 8 0 Td (a) Tj 8 0 Td (\\() Tj" + + " 8 0 Td (a) Tj 8 0 Td (a) Tj" + + " 8 0 Td (\\)) Tj 8 0 Td (a) Tj" + + " 8 0 Td (\\\\) Tj ET Q EMC" ); }); @@ -2007,10 +2007,10 @@ describe("annotation", function () { ); expect(appearance).toEqual( "/Tx BMC q BT /Goth 5 Tf 1 0 0 1 2 2 Tm" + - " (\x30\x53) Tj 8.00 0 Td (\x30\x93) Tj 8.00 0 Td (\x30\x6b) Tj" + - " 8.00 0 Td (\x30\x61) Tj 8.00 0 Td (\x30\x6f) Tj" + - " 8.00 0 Td (\x4e\x16) Tj 8.00 0 Td (\x75\x4c) Tj" + - " 8.00 0 Td (\x30\x6e) Tj ET Q EMC" + " (\x30\x53) Tj 8 0 Td (\x30\x93) Tj 8 0 Td (\x30\x6b) Tj" + + " 8 0 Td (\x30\x61) Tj 8 0 Td (\x30\x6f) Tj" + + " 8 0 Td (\x4e\x16) Tj 8 0 Td (\x75\x4c) Tj" + + " 8 0 Td (\x30\x6e) Tj ET Q EMC" ); }); @@ -2050,9 +2050,9 @@ describe("annotation", function () { "/V (hello world) /AP << /N 2 0 R>> /M (date)>>\nendobj\n" ); expect(newData.data).toEqual( - "2 0 obj\n<< /Length 77 /Subtype /Form /Resources " + + "2 0 obj\n<< /Length 74 /Subtype /Form /Resources " + "<< /Font << /Helv 314 0 R>>>> /BBox [0 0 32 10]>> stream\n" + - "/Tx BMC q BT /Helv 5 Tf 1 0 0 1 0 0 Tm 2.00 3.04 Td (hello world) Tj " + + "/Tx BMC q BT /Helv 5 Tf 1 0 0 1 0 0 Tm 2 3.04 Td (hello world) Tj " + "ET Q EMC\nendstream\nendobj\n" ); }); @@ -2181,9 +2181,9 @@ describe("annotation", function () { `/V (\xfe\xff${utf16String}) /AP << /N 2 0 R>> /M (date)>>\nendobj\n` ); expect(newData.data).toEqual( - "2 0 obj\n<< /Length 82 /Subtype /Form /Resources " + + "2 0 obj\n<< /Length 76 /Subtype /Form /Resources " + "<< /Font << /Helv 314 0 R /Goth 159 0 R>>>> /BBox [0 0 32 10]>> stream\n" + - `/Tx BMC q BT /Goth 5 Tf 1 0 0 1 0 0 Tm 2.00 2.00 Td (${utf16String}) Tj ` + + `/Tx BMC q BT /Goth 5 Tf 1 0 0 1 0 0 Tm 2 2 Td (${utf16String}) Tj ` + "ET Q EMC\nendstream\nendobj\n" ); }); @@ -3428,8 +3428,8 @@ describe("annotation", function () { "BT", "/Helv 5 Tf", "1 0 0 1 0 10 Tm", - "2.00 -5.88 Td (a) Tj", - "0.00 -6.75 Td (b) Tj", + "2 -5.88 Td (a) Tj", + "0 -6.75 Td (b) Tj", "ET Q EMC", ].join("\n") ); @@ -3477,8 +3477,8 @@ describe("annotation", function () { "BT", "/Helv 5 Tf", "1 0 0 1 0 10 Tm", - "2.00 -5.88 Td (b) Tj", - "0.00 -6.75 Td (c) Tj", + "2 -5.88 Td (b) Tj", + "0 -6.75 Td (c) Tj", "ET Q EMC", ].join("\n") ); @@ -3526,7 +3526,7 @@ describe("annotation", function () { expect(newData.data).toEqual( [ "2 0 obj", - "<< /Length 136 /Subtype /Form /Resources << /Font << /Helv 314 0 R>>>> " + + "<< /Length 133 /Subtype /Form /Resources << /Font << /Helv 314 0 R>>>> " + "/BBox [0 0 32 10]>> stream", "/Tx BMC q", "1 1 32 10 re W n", @@ -3535,7 +3535,7 @@ describe("annotation", function () { "BT", "/Helv 5 Tf", "1 0 0 1 0 10 Tm", - "2.00 -5.88 Td (C) Tj", + "2 -5.88 Td (C) Tj", "ET Q EMC", "endstream", "endobj\n", @@ -3592,7 +3592,7 @@ describe("annotation", function () { expect(newData.data).toEqual( [ "2 0 obj", - "<< /Length 177 /Subtype /Form /Resources << /Font << /Helv 314 0 R>>>> " + + "<< /Length 171 /Subtype /Form /Resources << /Font << /Helv 314 0 R>>>> " + "/BBox [0 0 32 10]>> stream", "/Tx BMC q", "1 1 32 10 re W n", @@ -3602,8 +3602,8 @@ describe("annotation", function () { "BT", "/Helv 5 Tf", "1 0 0 1 0 10 Tm", - "2.00 -5.88 Td (b) Tj", - "0.00 -6.75 Td (c) Tj", + "2 -5.88 Td (b) Tj", + "0 -6.75 Td (c) Tj", "ET Q EMC", "endstream", "endobj\n", diff --git a/test/unit/default_appearance_spec.js b/test/unit/default_appearance_spec.js index 1a73db468..7bd422736 100644 --- a/test/unit/default_appearance_spec.js +++ b/test/unit/default_appearance_spec.js @@ -21,7 +21,7 @@ import { describe("Default appearance", function () { describe("parseDefaultAppearance and createDefaultAppearance", function () { it("should parse and create default appearance", function () { - const da = "/F1 12 Tf 0.10 0.20 0.30 rg"; + const da = "/F1 12 Tf 0.1 0.2 0.3 rg"; const result = { fontSize: 12, fontName: "F1", @@ -42,8 +42,7 @@ describe("Default appearance", function () { }); it("should parse default appearance with save/restore", function () { - const da = - "q Q 0.10 0.20 0.30 rg /F1 12 Tf q 0.30 0.20 0.10 rg /F2 13 Tf Q"; + const da = "q Q 0.1 0.2 0.3 rg /F1 12 Tf q 0.3 0.2 0.1 rg /F2 13 Tf Q"; expect(parseDefaultAppearance(da)).toEqual({ fontSize: 12, fontName: "F1",