diff --git a/src/core/annotation.js b/src/core/annotation.js index 0527d17f4..42f49c2ed 100644 --- a/src/core/annotation.js +++ b/src/core/annotation.js @@ -33,6 +33,7 @@ coreColorSpace, coreObj, coreEvaluator) { var AnnotationBorderStyleType = sharedUtil.AnnotationBorderStyleType; +var AnnotationFieldFlag = sharedUtil.AnnotationFieldFlag; var AnnotationFlag = sharedUtil.AnnotationFlag; var AnnotationType = sharedUtil.AnnotationType; var OPS = sharedUtil.OPS; @@ -65,10 +66,14 @@ AnnotationFactory.prototype = /** @lends AnnotationFactory.prototype */ { /** * @param {XRef} xref * @param {Object} ref + * @param {string} uniquePrefix + * @param {Object} idCounters + * @param {boolean} renderInteractiveForms * @returns {Annotation} */ create: function AnnotationFactory_create(xref, ref, - uniquePrefix, idCounters) { + uniquePrefix, idCounters, + renderInteractiveForms) { var dict = xref.fetchIfRef(ref); if (!isDict(dict)) { return; @@ -87,6 +92,7 @@ AnnotationFactory.prototype = /** @lends AnnotationFactory.prototype */ { ref: isRef(ref) ? ref : null, subtype: subtype, id: id, + renderInteractiveForms: renderInteractiveForms, }; switch (subtype) { @@ -621,9 +627,13 @@ var WidgetAnnotation = (function WidgetAnnotationClosure() { data.defaultAppearance = Util.getInheritableProperty(dict, 'DA') || ''; var fieldType = Util.getInheritableProperty(dict, 'FT'); data.fieldType = isName(fieldType) ? fieldType.name : null; - data.fieldFlags = Util.getInheritableProperty(dict, 'Ff') || 0; this.fieldResources = Util.getInheritableProperty(dict, 'DR') || Dict.empty; + data.fieldFlags = Util.getInheritableProperty(dict, 'Ff'); + if (!isInt(data.fieldFlags) || data.fieldFlags < 0) { + data.fieldFlags = 0; + } + // Hide signatures because we cannot validate them. if (data.fieldType === 'Sig') { this.setFlags(AnnotationFlag.HIDDEN); @@ -662,7 +672,22 @@ var WidgetAnnotation = (function WidgetAnnotationClosure() { data.fullName = fieldName.join('.'); } - Util.inherit(WidgetAnnotation, Annotation, {}); + Util.inherit(WidgetAnnotation, Annotation, { + /** + * Check if a provided field flag is set. + * + * @public + * @memberof WidgetAnnotation + * @param {number} flag - Bit position, numbered from one instead of + * zero, to check + * @return {boolean} + * @see {@link shared/util.js} + */ + hasFieldFlag: function WidgetAnnotation_hasFieldFlag(flag) { + var mask = 1 << (flag - 1); + return !!(this.data.fieldFlags & mask); + }, + }); return WidgetAnnotation; })(); @@ -671,6 +696,8 @@ var TextWidgetAnnotation = (function TextWidgetAnnotationClosure() { function TextWidgetAnnotation(params) { WidgetAnnotation.call(this, params); + this.renderInteractiveForms = params.renderInteractiveForms; + // Determine the alignment of text in the field. var alignment = Util.getInheritableProperty(params.dict, 'Q'); if (!isInt(alignment) || alignment < 0 || alignment > 2) { @@ -684,29 +711,38 @@ var TextWidgetAnnotation = (function TextWidgetAnnotationClosure() { maximumLength = null; } this.data.maxLen = maximumLength; + + // Process field flags for the display layer. + this.data.readOnly = this.hasFieldFlag(AnnotationFieldFlag.READONLY); + this.data.multiLine = this.hasFieldFlag(AnnotationFieldFlag.MULTILINE); } Util.inherit(TextWidgetAnnotation, WidgetAnnotation, { getOperatorList: function TextWidgetAnnotation_getOperatorList(evaluator, task) { + var operatorList = new OperatorList(); + + // Do not render form elements on the canvas when interactive forms are + // enabled. The display layer is responsible for rendering them instead. + if (this.renderInteractiveForms) { + return Promise.resolve(operatorList); + } + if (this.appearance) { return Annotation.prototype.getOperatorList.call(this, evaluator, task); } - var opList = new OperatorList(); - var data = this.data; - // Even if there is an appearance stream, ignore it. This is the // behaviour used by Adobe Reader. - if (!data.defaultAppearance) { - return Promise.resolve(opList); + if (!this.data.defaultAppearance) { + return Promise.resolve(operatorList); } - var stream = new Stream(stringToBytes(data.defaultAppearance)); - return evaluator.getOperatorList(stream, task, - this.fieldResources, opList). + var stream = new Stream(stringToBytes(this.data.defaultAppearance)); + return evaluator.getOperatorList(stream, task, this.fieldResources, + operatorList). then(function () { - return opList; + return operatorList; }); } }); diff --git a/src/core/document.js b/src/core/document.js index 9e414dc90..0d5535385 100644 --- a/src/core/document.js +++ b/src/core/document.js @@ -205,7 +205,8 @@ var Page = (function PageClosure() { }.bind(this)); }, - getOperatorList: function Page_getOperatorList(handler, task, intent) { + getOperatorList: function Page_getOperatorList(handler, task, intent, + renderInteractiveForms) { var self = this; var pdfManager = this.pdfManager; @@ -245,6 +246,8 @@ var Page = (function PageClosure() { }); }); + this.renderInteractiveForms = renderInteractiveForms; + var annotationsPromise = pdfManager.ensure(this, 'annotations'); return Promise.all([pageListPromise, annotationsPromise]).then( function(datas) { @@ -328,7 +331,8 @@ var Page = (function PageClosure() { var annotationRef = annotationRefs[i]; var annotation = annotationFactory.create(this.xref, annotationRef, this.uniquePrefix, - this.idCounters); + this.idCounters, + this.renderInteractiveForms); if (annotation) { annotations.push(annotation); } diff --git a/src/core/worker.js b/src/core/worker.js index 40e5a5a0f..33959e7bd 100644 --- a/src/core/worker.js +++ b/src/core/worker.js @@ -839,7 +839,8 @@ var WorkerMessageHandler = { var pageNum = pageIndex + 1; var start = Date.now(); // Pre compile the pdf page and fetch the fonts/images. - page.getOperatorList(handler, task, data.intent).then( + page.getOperatorList(handler, task, data.intent, + data.renderInteractiveForms).then( function(operatorList) { finishWorkerTask(task); diff --git a/src/display/annotation_layer.js b/src/display/annotation_layer.js index 60c8f4618..c81eefc4d 100644 --- a/src/display/annotation_layer.js +++ b/src/display/annotation_layer.js @@ -446,9 +446,15 @@ var TextWidgetAnnotationElement = ( var element = null; if (this.renderInteractiveForms) { - element = document.createElement('input'); - element.type = 'text'; + if (this.data.multiLine) { + element = document.createElement('textarea'); + } else { + element = document.createElement('input'); + element.type = 'text'; + } + element.value = this.data.fieldValue; + element.disabled = this.data.readOnly; if (this.data.maxLen !== null) { element.maxLength = this.data.maxLen; diff --git a/src/display/api.js b/src/display/api.js index 22cabc63c..6ac9e8da5 100644 --- a/src/display/api.js +++ b/src/display/api.js @@ -656,6 +656,9 @@ var PDFDocumentProxy = (function PDFDocumentProxyClosure() { * calling of PDFPage.getViewport method. * @property {string} intent - Rendering intent, can be 'display' or 'print' * (default value is 'display'). + * @property {boolean} renderInteractiveForms - (optional) Whether or not + * interactive form elements are rendered in the display + * layer. If so, we do not render them on canvas as well. * @property {Array} transform - (optional) Additional transform, applied * just before viewport transform. * @property {Object} imageLayer - (optional) An object that has beginLayout, @@ -764,6 +767,8 @@ var PDFPageProxy = (function PDFPageProxyClosure() { this.pendingCleanup = false; var renderingIntent = (params.intent === 'print' ? 'print' : 'display'); + var renderInteractiveForms = (params.renderInteractiveForms === true ? + true : /* Default */ false); if (!this.intentStates[renderingIntent]) { this.intentStates[renderingIntent] = Object.create(null); @@ -784,7 +789,8 @@ var PDFPageProxy = (function PDFPageProxyClosure() { this.stats.time('Page Request'); this.transport.messageHandler.send('RenderPageRequest', { pageIndex: this.pageNumber - 1, - intent: renderingIntent + intent: renderingIntent, + renderInteractiveForms: renderInteractiveForms, }); } diff --git a/src/shared/util.js b/src/shared/util.js index 974e91004..853cc1a34 100644 --- a/src/shared/util.js +++ b/src/shared/util.js @@ -93,6 +93,28 @@ var AnnotationFlag = { LOCKEDCONTENTS: 0x200 }; +var AnnotationFieldFlag = { + READONLY: 1, + REQUIRED: 2, + NOEXPORT: 3, + MULTILINE: 13, + PASSWORD: 14, + NOTOGGLETOOFF: 15, + RADIO: 16, + PUSHBUTTON: 17, + COMBO: 18, + EDIT: 19, + SORT: 20, + FILESELECT: 21, + MULTISELECT: 22, + DONOTSPELLCHECK: 23, + DONOTSCROLL: 24, + COMB: 25, + RICHTEXT: 26, + RADIOSINUNISON: 26, + COMMITONSELCHANGE: 27, +}; + var AnnotationBorderStyleType = { SOLID: 1, DASHED: 2, @@ -2364,6 +2386,7 @@ exports.OPS = OPS; exports.VERBOSITY_LEVELS = VERBOSITY_LEVELS; exports.UNSUPPORTED_FEATURES = UNSUPPORTED_FEATURES; exports.AnnotationBorderStyleType = AnnotationBorderStyleType; +exports.AnnotationFieldFlag = AnnotationFieldFlag; exports.AnnotationFlag = AnnotationFlag; exports.AnnotationType = AnnotationType; exports.FontType = FontType; diff --git a/test/annotation_layer_test.css b/test/annotation_layer_test.css index f99ff1fad..ae5881c62 100644 --- a/test/annotation_layer_test.css +++ b/test/annotation_layer_test.css @@ -43,7 +43,8 @@ position: absolute; } -.annotationLayer .textWidgetAnnotation input { +.annotationLayer .textWidgetAnnotation input, +.annotationLayer .textWidgetAnnotation textarea { background-color: rgba(0, 54, 255, 0.13); border: 1px solid transparent; box-sizing: border-box; @@ -54,6 +55,18 @@ width: 100%; } +.annotationLayer .textWidgetAnnotation textarea { + font: message-box; + font-size: 9px; + resize: none; +} + +.annotationLayer .textWidgetAnnotation input[disabled], +.annotationLayer .textWidgetAnnotation textarea[disabled] { + background: none; + border: 1px solid transparent; +} + .annotationLayer .popupAnnotation { display: block !important; } diff --git a/test/pdfs/.gitignore b/test/pdfs/.gitignore index 0e2e4b5a1..8603aa58d 100644 --- a/test/pdfs/.gitignore +++ b/test/pdfs/.gitignore @@ -250,4 +250,5 @@ !annotation-squiggly.pdf !annotation-highlight.pdf !annotation-fileattachment.pdf +!annotation-text-widget.pdf !zero_descent.pdf diff --git a/test/pdfs/annotation-text-widget.pdf b/test/pdfs/annotation-text-widget.pdf new file mode 100644 index 000000000..d6c959c1a Binary files /dev/null and b/test/pdfs/annotation-text-widget.pdf differ diff --git a/test/test_manifest.json b/test/test_manifest.json index 6ffefb1a2..94cc7e68e 100644 --- a/test/test_manifest.json +++ b/test/test_manifest.json @@ -3151,6 +3151,13 @@ "type": "eq", "annotations": true }, + { "id": "annotation-text-widget-forms", + "file": "pdfs/annotation-text-widget.pdf", + "md5": "cc9672539ad5b837152a9c6961e5f106", + "rounds": 1, + "type": "eq", + "forms": true + }, { "id": "issue6108", "file": "pdfs/issue6108.pdf", "md5": "8961cb55149495989a80bf0487e0f076", diff --git a/test/unit/annotation_layer_spec.js b/test/unit/annotation_layer_spec.js index 6971e6fca..267b6e8a9 100644 --- a/test/unit/annotation_layer_spec.js +++ b/test/unit/annotation_layer_spec.js @@ -469,7 +469,8 @@ describe('Annotation layer', function() { textWidgetDict = null; }); - it('should handle unknown text alignment and maximum length', function() { + it('should handle unknown text alignment, maximum length and flags', + function() { var textWidgetRef = new Ref(124, 0); var xref = new XRefMock([ { ref: textWidgetRef, data: textWidgetDict, } @@ -478,11 +479,15 @@ describe('Annotation layer', function() { var textWidgetAnnotation = annotationFactory.create(xref, textWidgetRef); expect(textWidgetAnnotation.data.textAlignment).toEqual(null); expect(textWidgetAnnotation.data.maxLen).toEqual(null); + expect(textWidgetAnnotation.data.readOnly).toEqual(false); + expect(textWidgetAnnotation.data.multiLine).toEqual(false); }); - it('should not set invalid text alignment and maximum length', function() { + it('should not set invalid text alignment, maximum length and flags', + function() { textWidgetDict.set('Q', 'center'); textWidgetDict.set('MaxLen', 'five'); + textWidgetDict.set('Ff', 'readonly'); var textWidgetRef = new Ref(43, 0); var xref = new XRefMock([ @@ -492,11 +497,15 @@ describe('Annotation layer', function() { var textWidgetAnnotation = annotationFactory.create(xref, textWidgetRef); expect(textWidgetAnnotation.data.textAlignment).toEqual(null); expect(textWidgetAnnotation.data.maxLen).toEqual(null); + expect(textWidgetAnnotation.data.readOnly).toEqual(false); + expect(textWidgetAnnotation.data.multiLine).toEqual(false); }); - it('should set valid text alignment and maximum length', function() { + it('should set valid text alignment, maximum length and flags', + function() { textWidgetDict.set('Q', 1); textWidgetDict.set('MaxLen', 20); + textWidgetDict.set('Ff', 4097); var textWidgetRef = new Ref(84, 0); var xref = new XRefMock([ @@ -506,6 +515,8 @@ describe('Annotation layer', function() { var textWidgetAnnotation = annotationFactory.create(xref, textWidgetRef); expect(textWidgetAnnotation.data.textAlignment).toEqual(1); expect(textWidgetAnnotation.data.maxLen).toEqual(20); + expect(textWidgetAnnotation.data.readOnly).toEqual(true); + expect(textWidgetAnnotation.data.multiLine).toEqual(true); }); }); diff --git a/web/annotation_layer_builder.css b/web/annotation_layer_builder.css index 36b62b064..b7427a159 100644 --- a/web/annotation_layer_builder.css +++ b/web/annotation_layer_builder.css @@ -41,7 +41,8 @@ cursor: pointer; } -.annotationLayer .textWidgetAnnotation input { +.annotationLayer .textWidgetAnnotation input, +.annotationLayer .textWidgetAnnotation textarea { background-color: rgba(0, 54, 255, 0.13); border: 1px solid transparent; box-sizing: border-box; @@ -52,11 +53,26 @@ width: 100%; } -.annotationLayer .textWidgetAnnotation input:hover { +.annotationLayer .textWidgetAnnotation textarea { + font: message-box; + font-size: 9px; + resize: none; +} + +.annotationLayer .textWidgetAnnotation input[disabled], +.annotationLayer .textWidgetAnnotation textarea[disabled] { + background: none; + border: 1px solid transparent; + cursor: not-allowed; +} + +.annotationLayer .textWidgetAnnotation input:hover, +.annotationLayer .textWidgetAnnotation textarea:hover { border: 1px solid #000; } -.annotationLayer .textWidgetAnnotation input:focus { +.annotationLayer .textWidgetAnnotation input:focus, +.annotationLayer .textWidgetAnnotation textarea:focus { background: none; border: 1px solid transparent; } diff --git a/web/pdf_page_view.js b/web/pdf_page_view.js index 46ee7495c..bebe9e537 100644 --- a/web/pdf_page_view.js +++ b/web/pdf_page_view.js @@ -498,6 +498,7 @@ var PDFPageView = (function PDFPageViewClosure() { canvasContext: ctx, transform: transform, viewport: this.viewport, + renderInteractiveForms: pdfjsLib.PDFJS.renderInteractiveForms, // intent: 'default', // === 'display' }; var renderTask = this.renderTask = this.pdfPage.render(renderContext);