From c0e9108d00fd9ed3054bfc6a6b274c33ee279a65 Mon Sep 17 00:00:00 2001 From: Calixte Denizet Date: Fri, 17 Sep 2021 15:31:22 +0200 Subject: [PATCH] Annotation - Some checkboxes have an empty N dictionary - it aims to fix #14021; - the N dict is empty here so just create a default one; - it implies that the checked checkbox has no appearance so create a default one too in order to print it; - in the pdf in the issue, a checked box is not printed because it has no default appearance so we need to guess its appearance from its state. --- src/core/annotation.js | 172 ++++++++++++++++++++++++-------- src/display/annotation_layer.js | 5 +- test/pdfs/issue14021.pdf.link | 1 + test/test_manifest.json | 31 ++++++ 4 files changed, 162 insertions(+), 47 deletions(-) create mode 100644 test/pdfs/issue14021.pdf.link diff --git a/src/core/annotation.js b/src/core/annotation.js index 83520ca16..ecd5fa77c 100644 --- a/src/core/annotation.js +++ b/src/core/annotation.js @@ -1906,7 +1906,7 @@ class ButtonWidgetAnnotation extends WidgetAnnotation { } } - getOperatorList(evaluator, task, renderForms, annotationStorage) { + async getOperatorList(evaluator, task, renderForms, annotationStorage) { if (this.data.pushButton) { return super.getOperatorList( evaluator, @@ -1916,10 +1916,16 @@ class ButtonWidgetAnnotation extends WidgetAnnotation { ); } + let value = null; if (annotationStorage) { const storageEntry = annotationStorage.get(this.data.id); - const value = storageEntry && storageEntry.value; - if (value === undefined) { + value = storageEntry ? storageEntry.value : null; + } + + if (value === null) { + // Nothing in the annotationStorage. + if (this.appearance) { + // But we've a default appearance so use it. return super.getOperatorList( evaluator, task, @@ -1928,35 +1934,33 @@ class ButtonWidgetAnnotation extends WidgetAnnotation { ); } - let appearance; - if (value) { - appearance = this.checkedAppearance; + // There is no default appearance so use the one derived + // from the field value. + if (this.data.checkBox) { + value = this.data.fieldValue === this.data.exportValue; } else { - appearance = this.uncheckedAppearance; + value = this.data.fieldValue === this.data.buttonValue; } - - if (appearance) { - const savedAppearance = this.appearance; - this.appearance = appearance; - const operatorList = super.getOperatorList( - evaluator, - task, - renderForms, - annotationStorage - ); - this.appearance = savedAppearance; - return operatorList; - } - - // No appearance - return Promise.resolve(new OperatorList()); } - return super.getOperatorList( - evaluator, - task, - renderForms, - annotationStorage - ); + + const appearance = value + ? this.checkedAppearance + : this.uncheckedAppearance; + if (appearance) { + const savedAppearance = this.appearance; + this.appearance = appearance; + const operatorList = super.getOperatorList( + evaluator, + task, + renderForms, + annotationStorage + ); + this.appearance = savedAppearance; + return operatorList; + } + + // No appearance + return new OperatorList(); } async save(evaluator, task, annotationStorage) { @@ -1982,7 +1986,7 @@ class ButtonWidgetAnnotation extends WidgetAnnotation { return null; } - const defaultValue = this.data.fieldValue && this.data.fieldValue !== "Off"; + const defaultValue = this.data.fieldValue === this.data.exportValue; if (defaultValue === value) { return null; } @@ -2093,6 +2097,64 @@ class ButtonWidgetAnnotation extends WidgetAnnotation { return newRefs; } + _getDefaultCheckedAppearance(params, type) { + const width = this.data.rect[2] - this.data.rect[0]; + const height = this.data.rect[3] - this.data.rect[1]; + const bbox = [0, 0, width, height]; + + // Ratio used to have a mark slightly smaller than the bbox. + const FONT_RATIO = 0.8; + const fontSize = Math.min(width, height) * FONT_RATIO; + + // Char Metrics + // Widths came from widths for ZapfDingbats. + // Heights are guessed with Fontforge and FoxitDingbats.pfb. + let metrics, char; + if (type === "check") { + // Char 33 (2713 in unicode) + metrics = { + width: 0.755 * fontSize, + height: 0.705 * fontSize, + }; + char = "\x33"; + } else if (type === "disc") { + // Char 6C (25CF in unicode) + metrics = { + width: 0.791 * fontSize, + height: 0.705 * fontSize, + }; + char = "\x6C"; + } else { + unreachable(`_getDefaultCheckedAppearance - unsupported type: ${type}`); + } + + // Values to center the glyph in the bbox. + const xShift = (width - metrics.width) / 2; + const yShift = (height - metrics.height) / 2; + + const appearance = `q BT /PdfJsZaDb ${fontSize} Tf 0 g ${xShift} ${yShift} Td (${char}) Tj ET Q`; + + const appearanceStreamDict = new Dict(params.xref); + appearanceStreamDict.set("FormType", 1); + appearanceStreamDict.set("Subtype", Name.get("Form")); + appearanceStreamDict.set("Type", Name.get("XObject")); + appearanceStreamDict.set("BBox", bbox); + appearanceStreamDict.set("Matrix", [1, 0, 0, 1, 0, 0]); + appearanceStreamDict.set("Length", appearance.length); + + const resources = new Dict(params.xref); + const font = new Dict(params.xref); + font.set("PdfJsZaDb", this.fallbackFontDict); + resources.set("Font", font); + + appearanceStreamDict.set("Resources", resources); + + this.checkedAppearance = new StringStream(appearance); + this.checkedAppearance.dict = appearanceStreamDict; + + this._streams.push(this.checkedAppearance); + } + _processCheckBox(params) { const customAppearance = params.dict.get("AP"); if (!isDict(customAppearance)) { @@ -2111,27 +2173,46 @@ class ButtonWidgetAnnotation extends WidgetAnnotation { this.data.fieldValue = asValue; } + const yes = + this.data.fieldValue !== null && this.data.fieldValue !== "Off" + ? this.data.fieldValue + : "Yes"; + const exportValues = normalAppearance.getKeys(); - if (!exportValues.includes("Off")) { - // The /Off appearance is optional. - exportValues.push("Off"); + if (exportValues.length === 0) { + exportValues.push("Off", yes); + } else if (exportValues.length === 1) { + if (exportValues[0] === "Off") { + exportValues.push(yes); + } else { + exportValues.unshift("Off"); + } + } else if (exportValues.includes(yes)) { + exportValues.length = 0; + exportValues.push("Off", yes); + } else { + const otherYes = exportValues.find(v => v !== "Off"); + exportValues.length = 0; + exportValues.push("Off", otherYes); } + // Don't use a "V" entry pointing to a non-existent appearance state, // see e.g. bug1720411.pdf where it's an *empty* Name-instance. if (!exportValues.includes(this.data.fieldValue)) { - this.data.fieldValue = null; - } - if (exportValues.length !== 2) { - return; + this.data.fieldValue = "Off"; } - this.data.exportValue = - exportValues[0] === "Off" ? exportValues[1] : exportValues[0]; + this.data.exportValue = exportValues[1]; - this.checkedAppearance = normalAppearance.get(this.data.exportValue); + this.checkedAppearance = + normalAppearance.get(this.data.exportValue) || null; this.uncheckedAppearance = normalAppearance.get("Off") || null; - this._streams.push(this.checkedAppearance); + if (this.checkedAppearance) { + this._streams.push(this.checkedAppearance); + } else { + this._getDefaultCheckedAppearance(params, "check"); + } if (this.uncheckedAppearance) { this._streams.push(this.uncheckedAppearance); } @@ -2168,10 +2249,15 @@ class ButtonWidgetAnnotation extends WidgetAnnotation { } } - this.checkedAppearance = normalAppearance.get(this.data.buttonValue); + this.checkedAppearance = + normalAppearance.get(this.data.buttonValue) || null; this.uncheckedAppearance = normalAppearance.get("Off") || null; - this._streams.push(this.checkedAppearance); + if (this.checkedAppearance) { + this._streams.push(this.checkedAppearance); + } else { + this._getDefaultCheckedAppearance(params, "disc"); + } if (this.uncheckedAppearance) { this._streams.push(this.uncheckedAppearance); } diff --git a/src/display/annotation_layer.js b/src/display/annotation_layer.js index 2cc05914a..087c05a0b 100644 --- a/src/display/annotation_layer.js +++ b/src/display/annotation_layer.js @@ -1013,10 +1013,7 @@ class CheckboxWidgetAnnotationElement extends WidgetAnnotationElement { const data = this.data; const id = data.id; let value = storage.getValue(id, { - value: - data.fieldValue && - ((data.exportValue && data.exportValue === data.fieldValue) || - (!data.exportValue && data.fieldValue !== "Off")), + value: data.exportValue === data.fieldValue, }).value; if (typeof value === "string") { // The value has been changed through js and set in annotationStorage. diff --git a/test/pdfs/issue14021.pdf.link b/test/pdfs/issue14021.pdf.link new file mode 100644 index 000000000..8e157a18b --- /dev/null +++ b/test/pdfs/issue14021.pdf.link @@ -0,0 +1 @@ +https://github.com/mozilla/pdf.js/files/7159923/docOficialPDF.php.pdf diff --git a/test/test_manifest.json b/test/test_manifest.json index 7f9c94d1e..9343cbb19 100644 --- a/test/test_manifest.json +++ b/test/test_manifest.json @@ -5944,5 +5944,36 @@ "rounds": 1, "type": "eq", "annotations": true + }, + { "id": "issue14021", + "file": "pdfs/issue14021.pdf", + "md5": "d18aa84135ce985c70a8f56306ecb95f", + "link": true, + "rounds": 1, + "firstPage": 2, + "lastPage": 2, + "type": "eq", + "forms": true + }, + { "id": "issue14021-storage", + "file": "pdfs/issue14021.pdf", + "md5": "d18aa84135ce985c70a8f56306ecb95f", + "link": true, + "rounds": 1, + "firstPage": 2, + "lastPage": 3, + "type": "eq", + "print": true, + "annotationStorage": { + "148R": { + "value": true + }, + "138R": { + "value": true + }, + "139R": { + "value": true + } + } } ]