diff --git a/src/core/annotation.js b/src/core/annotation.js index ab31be32f4b71..0013618b26fe8 100644 --- a/src/core/annotation.js +++ b/src/core/annotation.js @@ -23,7 +23,6 @@ import { assert, isString, OPS, - stringToBytes, stringToPDFString, Util, warn, @@ -31,9 +30,10 @@ import { 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 { /** @@ -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; @@ -78,6 +81,7 @@ class AnnotationFactory { subtype, id, pdfManager, + acroForm: acroForm instanceof Dict ? acroForm : Dict.empty, }; switch (subtype) { @@ -471,6 +475,7 @@ class Annotation { */ setAppearance(dict) { this.appearance = null; + this.checkedAppearance = null; const appearanceStates = dict.get("AP"); if (!isDict(appearanceStates)) { @@ -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", @@ -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; }); }); @@ -795,11 +801,16 @@ class WidgetAnnotation extends Annotation { getArray: true, }); data.alternativeText = stringToPDFString(dict.get("TU") || ""); - data.defaultAppearance = getInheritableProperty({ dict, key: "DA" }) || ""; + data.defaultAppearance = + getInheritableProperty({ dict, key: "DA" }) || + params.acroForm.get("DA") || + ""; 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) { @@ -877,17 +888,315 @@ 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 + ); + } +} + +class WidgetWithTextAnnotation extends WidgetAnnotation { + 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 it's a password textfield then no rendering to avoid to leak it. + // see 12.7.4.3, table 228 + if (!annotationStorage || this.data.isPassword) { + return null; + } + const value = annotationStorage[this.data.id] || ""; + if (value === "") { + return null; + } + + // Magic value + const defaultPadding = 2; + + // Default horizontal padding: can we have an heuristic to guess it? + const hPadding = defaultPadding; + const totalHeight = this.data.rect[3] - this.data.rect[1]; + const totalWidth = this.data.rect[2] - this.data.rect[0]; + + const fontInfo = await this.getFontData(evaluator, task); + const [font, fontName] = fontInfo; + let [, , fontSize] = fontInfo; + + fontSize = this.computeAutoSizedFont( + fontName, + fontSize, + totalHeight, + 2 * defaultPadding + ); + + let descent = font.descent; + if (isNaN(descent)) { + descent = 0; + } + + const vPadding = defaultPadding + Math.abs(descent) * fontSize; + const defaultAppearance = this.data.defaultAppearance; + + 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; + } + + if (this.data.multiLine) { + const renderedText = this.handleMultiline( + value, + font, + fontSize, + totalWidth, + alignment, + hPadding, + vPadding + ); + return `/Tx BMC q BT ${defaultAppearance} 1 0 0 1 0 ${totalHeight} Tm ${renderedText} ET Q EMC`; + } + + 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`; + } + + const renderedText = this.renderPDFText( + value, + font, + fontSize, + totalWidth, + alignment, + hPadding, + vPadding + ); + return `/Tx BMC q BT ${defaultAppearance} 1 0 0 1 0 0 Tm ${renderedText} 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, + }); + + return [initialState.font, initialState.fontName, initialState.fontSize]; + } + + computeAutoSizedFont(fontName, fontSize, height, spaceAround) { + if (fontSize === null || fontSize === 0) { + // fontSize should be computed as a function of totalHeight + if (spaceAround >= height) { + fontSize = Math.floor(0.8 * height); + } else { + fontSize = 0.8 * (height - spaceAround); + } + fontSize = fontSize.toFixed(2); + + let re = new RegExp(`/${fontName}\\s+[0-9\.]+\\s+Tf`); + if (this.data.defaultAppearance.search(re) === -1) { + // The font size is missing + re = new RegExp(`/${fontName}\\s+Tf`); + } + this.data.defaultAppearance = this.data.defaultAppearance.replace( + re, + `/${fontName} ${fontSize} Tf` + ); + } + return fontSize; + } + + renderPDFText( + text, + font, + fontSize, + totalWidth, + alignment, + hPadding, + vPadding + ) { + // We need to get the width of the text in order to align it correctly + const glyphs = font.charsToGlyphs(text); + 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 if (alignment === 2) { + // Right + shift = totalWidth - width - hPadding; + } else { + shift = hPadding; + } + shift = shift.toFixed(2); + vPadding = vPadding.toFixed(2); + + return `${shift} ${vPadding} Td (${text}) Tj`; + } + + handleMultiline(text, font, fontSize, width, alignment, hPadding, vPadding) { + const lines = text.replace("\r\n", "\n").split("\n"); + let buf = ""; + const totalWidth = alignment === 1 ? width : width - hPadding; + for (const line of lines) { + const chunks = this.splitLine(line, font, fontSize, totalWidth); + for (const chunk of chunks) { + if (buf === "") { + buf = this.renderPDFText( + chunk, + font, + fontSize, + width, + alignment, + hPadding, + -fontSize + ); + } else { + buf += + "\n" + + this.renderPDFText( + chunk, + font, + fontSize, + width, + alignment, + 0, + -fontSize + ); + } + } + } + + return buf; + } + + splitLine(line, font, fontSize, width) { + const scale = fontSize / 1000; + const white = font.charsToGlyphs(" ", true)[0].width * scale; + const chunks = []; + + let lastSpacePos = -1, + startChunk = 0, + currentWidth = 0; + + for (let i = 0; i < line.length; i++) { + const character = line.charAt(i); + if (character === " ") { + if (currentWidth + white > width) { + // We can break here + chunks.push(line.substring(startChunk, i)); + startChunk = i; + currentWidth = white; + lastSpacePos = -1; + } else { + currentWidth += white; + lastSpacePos = i; + } + } else { + const charWidth = font.charsToGlyphs(character, false)[0].width * scale; + if (currentWidth + charWidth > width) { + // We must break to the last white position (if one) + if (lastSpacePos !== -1) { + chunks.push(line.substring(startChunk, lastSpacePos + 1)); + startChunk = i = lastSpacePos + 1; + lastSpacePos = -1; + currentWidth = 0; + } else { + // Just break in the middle of the word + chunks.push(line.substring(startChunk, i)); + currentWidth = charWidth; + } + } else { + currentWidth += charWidth; + } + } + } + + if (startChunk < line.length) { + chunks.push(line.substring(startChunk, line.length)); + } + + return chunks; } } -class TextWidgetAnnotation extends WidgetAnnotation { +class TextWidgetAnnotation extends WidgetWithTextAnnotation { constructor(params) { super(params); @@ -899,7 +1208,8 @@ class TextWidgetAnnotation extends WidgetAnnotation { // Determine the alignment of text in the field. let alignment = getInheritableProperty({ dict, key: "Q" }); if (!Number.isInteger(alignment) || alignment < 0 || alignment > 2) { - alignment = null; + // By default text is left aligned + alignment = 0; } this.data.textAlignment = alignment; @@ -909,6 +1219,7 @@ class TextWidgetAnnotation extends WidgetAnnotation { maximumLength = null; } this.data.maxLen = maximumLength; + this.isPassword = this.hasFieldFlag(AnnotationFieldFlag.PASSWORD); // Process field flags for the display layer. this.data.multiLine = this.hasFieldFlag(AnnotationFieldFlag.MULTILINE); @@ -919,38 +1230,11 @@ class TextWidgetAnnotation extends WidgetAnnotation { !this.hasFieldFlag(AnnotationFieldFlag.FILESELECT) && this.data.maxLen !== null; } - - getOperatorList(evaluator, task, renderForms) { - if (renderForms || this.appearance) { - return super.getOperatorList(evaluator, task, renderForms); - } - - 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) { - 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; - }); - } } class ButtonWidgetAnnotation extends WidgetAnnotation { constructor(params) { super(params); - this.data.checkBox = !this.hasFieldFlag(AnnotationFieldFlag.RADIO) && !this.hasFieldFlag(AnnotationFieldFlag.PUSHBUTTON); @@ -970,6 +1254,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; @@ -993,6 +1302,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) { @@ -1023,6 +1339,7 @@ class ButtonWidgetAnnotation extends WidgetAnnotation { break; } } + this.checkedAppearance = normalAppearanceState.get(this.data.buttonValue); } _processPushButton(params) { @@ -1039,7 +1356,7 @@ class ButtonWidgetAnnotation extends WidgetAnnotation { } } -class ChoiceWidgetAnnotation extends WidgetAnnotation { +class ChoiceWidgetAnnotation extends WidgetWithTextAnnotation { constructor(params) { super(params); diff --git a/src/core/document.js b/src/core/document.js index c0d343f053b1d..359fc27c851b0 100644 --- a/src/core/document.js +++ b/src/core/document.js @@ -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" @@ -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 " + diff --git a/src/core/evaluator.js b/src/core/evaluator.js index 81a05a736022a..bf0b24bb99136 100644 --- a/src/core/evaluator.js +++ b/src/core/evaluator.js @@ -751,10 +751,12 @@ class PartialEvaluator { handleSetFont(resources, fontArgs, fontRef, operatorList, task, state) { // TODO(mack): Not needed? - var fontName; + var fontName, + fontSize = 0; if (fontArgs) { fontArgs = fontArgs.slice(); fontName = fontArgs[0].name; + fontSize = fontArgs[1]; } return this.loadFont(fontName, fontRef, resources) @@ -783,6 +785,8 @@ class PartialEvaluator { }) .then(translated => { state.font = translated.font; + state.fontSize = fontSize; + state.fontName = fontName; translated.send(this.handler); return translated.loadedName; }); @@ -3518,6 +3522,8 @@ class EvalState { constructor() { this.ctm = new Float32Array(IDENTITY_MATRIX); this.font = null; + this.fontSize = 0; + this.fontName = null; this.textRenderingMode = TextRenderingMode.FILL; this.fillColorSpace = ColorSpace.singletons.gray; this.strokeColorSpace = ColorSpace.singletons.gray; @@ -3801,4 +3807,4 @@ class EvaluatorPreprocessor { } } -export { PartialEvaluator }; +export { EvalState, PartialEvaluator }; diff --git a/src/core/worker.js b/src/core/worker.js index 31bd828de52cf..ece58dfa4d9b2 100644 --- a/src/core/worker.js +++ b/src/core/worker.js @@ -517,6 +517,7 @@ class WorkerMessageHandler { "GetOperatorList", function wphSetupRenderPage(data, sink) { var pageIndex = data.pageIndex; + const annotationStorage = data.annotationStorage; pdfManager.getPage(pageIndex).then(function (page) { var task = new WorkerTask(`GetOperatorList: page ${pageIndex}`); startWorkerTask(task); @@ -532,6 +533,7 @@ class WorkerMessageHandler { task, intent: data.intent, renderInteractiveForms: data.renderInteractiveForms, + annotationStorage, }) .then( function (operatorListInfo) { diff --git a/src/display/annotation_layer.js b/src/display/annotation_layer.js index cee8ee77d690b..28bc8891af1da 100644 --- a/src/display/annotation_layer.js +++ b/src/display/annotation_layer.js @@ -140,6 +140,7 @@ class AnnotationElement { this.imageResourcesPath = parameters.imageResourcesPath; this.renderInteractiveForms = parameters.renderInteractiveForms; this.svgFactory = parameters.svgFactory; + this.annotationStorage = parameters.annotationStorage; if (isRenderable) { this.container = this._createContainer(ignoreBorder); @@ -438,6 +439,8 @@ class TextWidgetAnnotationElement extends WidgetAnnotationElement { */ render() { const TEXT_ALIGNMENT = ["left", "center", "right"]; + const storage = this.annotationStorage; + const id = this.data.id; this.container.className = "textWidgetAnnotation"; @@ -446,15 +449,21 @@ class TextWidgetAnnotationElement extends WidgetAnnotationElement { // NOTE: We cannot set the values using `element.value` below, since it // prevents the AnnotationLayer rasterizer in `test/driver.js` // from parsing the elements correctly for the reference tests. + const textContent = storage.getOrCreate(id, this.data.fieldValue); + if (this.data.multiLine) { element = document.createElement("textarea"); - element.textContent = this.data.fieldValue; + element.textContent = textContent; } else { element = document.createElement("input"); element.type = "text"; - element.setAttribute("value", this.data.fieldValue); + element.setAttribute("value", textContent); } + element.addEventListener("change", function (event) { + storage.setValue(id, event.target.value); + }); + element.disabled = this.data.readOnly; element.name = this.data.fieldName; @@ -542,15 +551,26 @@ class CheckboxWidgetAnnotationElement extends WidgetAnnotationElement { */ render() { this.container.className = "buttonWidgetAnnotation checkBox"; + const storage = this.annotationStorage; + const data = this.data; + const id = data.id; + const value = storage.getOrCreate( + id, + data.fieldValue && data.fieldValue !== "Off" + ); const element = document.createElement("input"); - element.disabled = this.data.readOnly; + element.disabled = data.readOnly; element.type = "checkbox"; - element.name = this.data.fieldName; - if (this.data.fieldValue && this.data.fieldValue !== "Off") { - element.setAttribute("checked", true); + element.name = data.fieldName; + if (value) { + element.setAttribute("checked", value); } + element.addEventListener("change", function (event) { + storage.setValue(id, event.target.checked); + }); + this.container.appendChild(element); return this.container; } @@ -571,15 +591,32 @@ class RadioButtonWidgetAnnotationElement extends WidgetAnnotationElement { */ render() { this.container.className = "buttonWidgetAnnotation radioButton"; + const storage = this.annotationStorage; + const data = this.data; + const id = data.id; + const value = storage.getOrCreate(id, data.fieldValue === data.buttonValue); const element = document.createElement("input"); element.disabled = this.data.readOnly; element.type = "radio"; element.name = this.data.fieldName; - if (this.data.fieldValue === this.data.buttonValue) { + if (value) { element.setAttribute("checked", true); } + element.addEventListener("change", function (e) { + const name = e.target.name; + for (const el of document.getElementsByName(name)) { + if (el !== e.target) { + storage.setValue( + el.parentNode.getAttribute("data-annotation-id"), + false + ); + } + } + storage.setValue(id, e.target.checked); + }); + this.container.appendChild(element); return this.container; } @@ -623,6 +660,8 @@ class ChoiceWidgetAnnotationElement extends WidgetAnnotationElement { const selectElement = document.createElement("select"); selectElement.disabled = this.data.readOnly; selectElement.name = this.data.fieldName; + const storage = this.annotationStorage; + const id = this.data.id; if (!this.data.combo) { // List boxes have a size and (optionally) multiple selection. @@ -643,6 +682,13 @@ class ChoiceWidgetAnnotationElement extends WidgetAnnotationElement { selectElement.appendChild(optionElement); } + selectElement.addEventListener("change", function (event) { + const options = event.target.options; + const value = options[options.selectedIndex].text; + + storage.setValue(id, value); + }); + this.container.appendChild(selectElement); return this.container; } @@ -1450,6 +1496,7 @@ class AnnotationLayer { imageResourcesPath: parameters.imageResourcesPath || "", renderInteractiveForms: parameters.renderInteractiveForms || false, svgFactory: new DOMSVGFactory(), + annotationStorage: parameters.annotationStorage, }); if (element.isRenderable) { parameters.div.appendChild(element.render()); diff --git a/src/display/annotation_storage.js b/src/display/annotation_storage.js new file mode 100644 index 0000000000000..e9d9b7d1b0e80 --- /dev/null +++ b/src/display/annotation_storage.js @@ -0,0 +1,57 @@ +/* Copyright 2014 Mozilla Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +class AnnotationStorage { + constructor(storage = {}) { + this._storage = storage; + } + + /** + * Get the value for a given key if it doesn't exist + * or store and return the defaultValue + * + * @public + * @memberof AnnotationStorage + * @param {String} key + * @param {Object} defaultValue + * @returns {Object} + */ + getOrCreate(key, defaultValue) { + if (key in this._storage) { + return this._storage[key]; + } + + this._storage[key] = defaultValue; + return defaultValue; + } + + /** + * Set the value for a given key + * + * @public + * @memberof AnnotationStorage + * @param {String} key + * @param {Object} value + */ + setValue(key, value) { + this._storage[key] = value; + } + + getDict() { + return this._storage; + } +} + +export { AnnotationStorage }; diff --git a/src/display/api.js b/src/display/api.js index e88618a32f4fc..29e659648274a 100644 --- a/src/display/api.js +++ b/src/display/api.js @@ -48,6 +48,7 @@ import { } from "./display_utils.js"; import { FontFaceObject, FontLoader } from "./font_loader.js"; import { NodeCanvasFactory, NodeCMapReaderFactory } from "./node_utils.js"; +import { AnnotationStorage } from "./annotation_storage.js"; import { apiCompatibilityParams } from "./api_compatibility.js"; import { CanvasGraphics } from "./canvas.js"; import { GlobalWorkerOptions } from "./worker_options.js"; @@ -576,6 +577,14 @@ class PDFDocumentProxy { constructor(pdfInfo, transport) { this._pdfInfo = pdfInfo; this._transport = transport; + this._annotationStorage = new AnnotationStorage(); + } + + /** + * @type {AnnotationStorage} storage for annotations. + */ + get annotationStorage() { + return this._annotationStorage; } /** @@ -1004,6 +1013,7 @@ class PDFPageProxy { imageLayer = null, canvasFactory = null, background = null, + annotationStorage = null, }) { if (this._stats) { this._stats.time("Overall"); @@ -1044,10 +1054,12 @@ class PDFPageProxy { if (this._stats) { this._stats.time("Page Request"); } + this._pumpOperatorList({ pageIndex: this._pageIndex, intent: renderingIntent, renderInteractiveForms: renderInteractiveForms === true, + annotationStorage, }); } diff --git a/test/unit/annotation_spec.js b/test/unit/annotation_spec.js index 100ca9dbbeca1..6bc73ea05d517 100644 --- a/test/unit/annotation_spec.js +++ b/test/unit/annotation_spec.js @@ -37,6 +37,9 @@ describe("annotation", function () { class PDFManagerMock { constructor(params) { this.docBaseUrl = params.docBaseUrl || null; + this.pdfDocument = { + acroForm: new Dict(), + }; } ensure(obj, prop, args) { @@ -49,6 +52,10 @@ describe("annotation", function () { } }); } + + ensureDoc(prop, args) { + return this.ensure(this.pdfDocument, prop, args); + } } let pdfManagerMock, idFactoryMock; @@ -1384,7 +1391,7 @@ describe("annotation", function () { idFactoryMock ).then(({ data }) => { expect(data.annotationType).toEqual(AnnotationType.WIDGET); - expect(data.textAlignment).toEqual(null); + expect(data.textAlignment).toEqual(0); expect(data.maxLen).toEqual(null); expect(data.readOnly).toEqual(false); expect(data.multiLine).toEqual(false); @@ -1408,7 +1415,7 @@ describe("annotation", function () { idFactoryMock ).then(({ data }) => { expect(data.annotationType).toEqual(AnnotationType.WIDGET); - expect(data.textAlignment).toEqual(null); + expect(data.textAlignment).toEqual(0); expect(data.maxLen).toEqual(null); expect(data.readOnly).toEqual(false); expect(data.multiLine).toEqual(false); diff --git a/web/annotation_layer_builder.js b/web/annotation_layer_builder.js index 44691ea6caa45..fb532e1d8c4c3 100644 --- a/web/annotation_layer_builder.js +++ b/web/annotation_layer_builder.js @@ -38,6 +38,7 @@ class AnnotationLayerBuilder { pdfPage, linkService, downloadManager, + annotationStorage, imageResourcesPath = "", renderInteractiveForms = false, l10n = NullL10n, @@ -49,6 +50,7 @@ class AnnotationLayerBuilder { this.imageResourcesPath = imageResourcesPath; this.renderInteractiveForms = renderInteractiveForms; this.l10n = l10n; + this.annotationStorage = annotationStorage; this.div = null; this._cancelled = false; @@ -73,6 +75,7 @@ class AnnotationLayerBuilder { renderInteractiveForms: this.renderInteractiveForms, linkService: this.linkService, downloadManager: this.downloadManager, + annotationStorage: this.annotationStorage, }; if (this.div) { @@ -124,6 +127,7 @@ class DefaultAnnotationLayerFactory { createAnnotationLayerBuilder( pageDiv, pdfPage, + annotationStorage, imageResourcesPath = "", renderInteractiveForms = false, l10n = NullL10n @@ -135,6 +139,7 @@ class DefaultAnnotationLayerFactory { renderInteractiveForms, linkService: new SimpleLinkService(), l10n, + annotationStorage, }); } } diff --git a/web/base_viewer.js b/web/base_viewer.js index 1dae1399f1a71..9e48c50a7be25 100644 --- a/web/base_viewer.js +++ b/web/base_viewer.js @@ -1165,6 +1165,7 @@ class BaseViewer { renderInteractiveForms, linkService: this.linkService, downloadManager: this.downloadManager, + annotationStorage: this.pdfDocument.annotationStorage, l10n, }); } diff --git a/web/firefox_print_service.js b/web/firefox_print_service.js index 312a054279e2c..7b5491cecfaf7 100644 --- a/web/firefox_print_service.js +++ b/web/firefox_print_service.js @@ -53,6 +53,7 @@ function composePage(pdfDocument, pageNumber, size, printContainer) { transform: [PRINT_UNITS, 0, 0, PRINT_UNITS, 0, 0], viewport: pdfPage.getViewport({ scale: 1, rotation: size.rotation }), intent: "print", + annotationStorage: pdfDocument.annotationStorage.getDict(), }; return pdfPage.render(renderContext).promise; }) @@ -109,7 +110,7 @@ PDFPrintServiceFactory.instance = { return shadow(this, "supportsPrinting", value); }, - createPrintService(pdfDocument, pagesOverview, printContainer) { + createPrintService(pdfDocument, pagesOverview, printContainer, l10n) { return new FirefoxPrintService(pdfDocument, pagesOverview, printContainer); }, }; diff --git a/web/pdf_print_service.js b/web/pdf_print_service.js index d16792c7fac49..d4a00a24d7d0e 100644 --- a/web/pdf_print_service.js +++ b/web/pdf_print_service.js @@ -49,6 +49,7 @@ function renderPage(activeServiceOnEntry, pdfDocument, pageNumber, size) { transform: [PRINT_UNITS, 0, 0, PRINT_UNITS, 0, 0], viewport: pdfPage.getViewport({ scale: 1, rotation: size.rotation }), intent: "print", + annotationStorage: pdfDocument.annotationStorage.getDict(), }; return pdfPage.render(renderContext).promise; })