diff --git a/src/core/annotation.js b/src/core/annotation.js index 13aa20477..4539db43d 100644 --- a/src/core/annotation.js +++ b/src/core/annotation.js @@ -106,6 +106,8 @@ AnnotationFactory.prototype = /** @lends AnnotationFactory.prototype */ { switch (fieldType) { case 'Tx': return new TextWidgetAnnotation(parameters); + case 'Ch': + return new ChoiceWidgetAnnotation(parameters); } warn('Unimplemented widget field type "' + fieldType + '", ' + 'falling back to base field type.'); @@ -619,8 +621,8 @@ var WidgetAnnotation = (function WidgetAnnotationClosure() { var data = this.data; data.annotationType = AnnotationType.WIDGET; - data.fieldValue = stringToPDFString( - Util.getInheritableProperty(dict, 'V') || ''); + data.fieldValue = Util.getInheritableProperty(dict, 'V', + /* getArray = */ true); data.alternativeText = stringToPDFString(dict.get('TU') || ''); data.defaultAppearance = Util.getInheritableProperty(dict, 'DA') || ''; var fieldType = Util.getInheritableProperty(dict, 'FT'); @@ -632,6 +634,8 @@ var WidgetAnnotation = (function WidgetAnnotationClosure() { data.fieldFlags = 0; } + data.readOnly = this.hasFieldFlag(AnnotationFieldFlag.READONLY); + // Hide signatures because we cannot validate them. if (data.fieldType === 'Sig') { this.setFlags(AnnotationFlag.HIDDEN); @@ -693,6 +697,9 @@ var TextWidgetAnnotation = (function TextWidgetAnnotationClosure() { function TextWidgetAnnotation(params) { WidgetAnnotation.call(this, params); + // The field value is always a string. + this.data.fieldValue = stringToPDFString(this.data.fieldValue || ''); + // Determine the alignment of text in the field. var alignment = Util.getInheritableProperty(params.dict, 'Q'); if (!isInt(alignment) || alignment < 0 || alignment > 2) { @@ -708,7 +715,6 @@ var TextWidgetAnnotation = (function TextWidgetAnnotationClosure() { 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); this.data.comb = this.hasFieldFlag(AnnotationFieldFlag.COMB) && !this.hasFieldFlag(AnnotationFieldFlag.MULTILINE) && @@ -752,6 +758,62 @@ var TextWidgetAnnotation = (function TextWidgetAnnotationClosure() { return TextWidgetAnnotation; })(); +var ChoiceWidgetAnnotation = (function ChoiceWidgetAnnotationClosure() { + function ChoiceWidgetAnnotation(params) { + WidgetAnnotation.call(this, params); + + // Determine the options. The options array may consist of strings or + // arrays. If the array consists of arrays, then the first element of + // each array is the export value and the second element of each array is + // the display value. If the array consists of strings, then these + // represent both the export and display value. In this case, we convert + // it to an array of arrays as well for convenience in the display layer. + this.data.options = []; + + var options = params.dict.getArray('Opt'); + if (isArray(options)) { + for (var i = 0, ii = options.length; i < ii; i++) { + var option = options[i]; + + this.data.options[i] = { + exportValue: isArray(option) ? option[0] : option, + displayValue: isArray(option) ? option[1] : option, + }; + } + } + + // Determine the field value. In this case, it may be a string or an + // array of strings. For convenience in the display layer, convert the + // string to an array of one string as well. + if (!isArray(this.data.fieldValue)) { + this.data.fieldValue = [this.data.fieldValue]; + } + + // Process field flags for the display layer. + this.data.combo = this.hasFieldFlag(AnnotationFieldFlag.COMBO); + this.data.multiSelect = this.hasFieldFlag(AnnotationFieldFlag.MULTISELECT); + } + + Util.inherit(ChoiceWidgetAnnotation, WidgetAnnotation, { + getOperatorList: + function ChoiceWidgetAnnotation_getOperatorList(evaluator, task, + renderForms) { + 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 (renderForms) { + return Promise.resolve(operatorList); + } + + return Annotation.prototype.getOperatorList.call(this, evaluator, task, + renderForms); + } + }); + + return ChoiceWidgetAnnotation; +})(); + var TextAnnotation = (function TextAnnotationClosure() { var DEFAULT_ICON_SIZE = 22; // px diff --git a/src/display/annotation_layer.js b/src/display/annotation_layer.js index 29ed79a70..88c0c369e 100644 --- a/src/display/annotation_layer.js +++ b/src/display/annotation_layer.js @@ -76,6 +76,8 @@ AnnotationElementFactory.prototype = switch (fieldType) { case 'Tx': return new TextWidgetAnnotationElement(parameters); + case 'Ch': + return new ChoiceWidgetAnnotationElement(parameters); } return new WidgetAnnotationElement(parameters); @@ -400,9 +402,7 @@ var TextAnnotationElement = (function TextAnnotationElementClosure() { * @alias WidgetAnnotationElement */ var WidgetAnnotationElement = (function WidgetAnnotationElementClosure() { - function WidgetAnnotationElement(parameters) { - var isRenderable = parameters.renderInteractiveForms || - (!parameters.data.hasAppearance && !!parameters.data.fieldValue); + function WidgetAnnotationElement(parameters, isRenderable) { AnnotationElement.call(this, parameters, isRenderable); } @@ -432,7 +432,9 @@ var TextWidgetAnnotationElement = ( var TEXT_ALIGNMENT = ['left', 'center', 'right']; function TextWidgetAnnotationElement(parameters) { - WidgetAnnotationElement.call(this, parameters); + var isRenderable = parameters.renderInteractiveForms || + (!parameters.data.hasAppearance && !!parameters.data.fieldValue); + WidgetAnnotationElement.call(this, parameters, isRenderable); } Util.inherit(TextWidgetAnnotationElement, WidgetAnnotationElement, { @@ -528,6 +530,64 @@ var TextWidgetAnnotationElement = ( return TextWidgetAnnotationElement; })(); +/** + * @class + * @alias ChoiceWidgetAnnotationElement + */ +var ChoiceWidgetAnnotationElement = ( + function ChoiceWidgetAnnotationElementClosure() { + function ChoiceWidgetAnnotationElement(parameters) { + WidgetAnnotationElement.call(this, parameters, + parameters.renderInteractiveForms); + } + + Util.inherit(ChoiceWidgetAnnotationElement, WidgetAnnotationElement, { + /** + * Render the choice widget annotation's HTML element in the empty + * container. + * + * @public + * @memberof ChoiceWidgetAnnotationElement + * @returns {HTMLSectionElement} + */ + render: function ChoiceWidgetAnnotationElement_render() { + this.container.className = 'choiceWidgetAnnotation'; + + var selectElement = document.createElement('select'); + selectElement.disabled = this.data.readOnly; + + if (!this.data.combo) { + // List boxes have a size and (optionally) multiple selection. + selectElement.size = this.data.options.length; + + if (this.data.multiSelect) { + selectElement.multiple = true; + } + } + + // Insert the options into the choice field. + for (var i = 0, ii = this.data.options.length; i < ii; i++) { + var option = this.data.options[i]; + + var optionElement = document.createElement('option'); + optionElement.textContent = option.displayValue; + optionElement.value = option.exportValue; + + if (this.data.fieldValue.indexOf(option.displayValue) >= 0) { + optionElement.setAttribute('selected', true); + } + + selectElement.appendChild(optionElement); + } + + this.container.appendChild(selectElement); + return this.container; + } + }); + + return ChoiceWidgetAnnotationElement; +})(); + /** * @class * @alias PopupAnnotationElement diff --git a/src/shared/util.js b/src/shared/util.js index 48876dad3..fe829729a 100644 --- a/src/shared/util.js +++ b/src/shared/util.js @@ -873,15 +873,15 @@ var Util = (function UtilClosure() { } }; - Util.getInheritableProperty = function Util_getInheritableProperty(dict, - name) { + Util.getInheritableProperty = + function Util_getInheritableProperty(dict, name, getArray) { while (dict && !dict.has(name)) { dict = dict.get('Parent'); } if (!dict) { return null; } - return dict.get(name); + return getArray ? dict.getArray(name) : dict.get(name); }; Util.inherit = function Util_inherit(sub, base, prototype) { diff --git a/test/annotation_layer_test.css b/test/annotation_layer_test.css index df5dea06c..5c6a87be8 100644 --- a/test/annotation_layer_test.css +++ b/test/annotation_layer_test.css @@ -44,7 +44,8 @@ } .annotationLayer .textWidgetAnnotation input, -.annotationLayer .textWidgetAnnotation textarea { +.annotationLayer .textWidgetAnnotation textarea, +.annotationLayer .choiceWidgetAnnotation select { background-color: rgba(0, 54, 255, 0.13); border: 1px solid transparent; box-sizing: border-box; @@ -62,7 +63,8 @@ } .annotationLayer .textWidgetAnnotation input[disabled], -.annotationLayer .textWidgetAnnotation textarea[disabled] { +.annotationLayer .textWidgetAnnotation textarea[disabled], +.annotationLayer .choiceWidgetAnnotation select[disabled] { background: none; border: 1px solid transparent; } diff --git a/test/pdfs/.gitignore b/test/pdfs/.gitignore index 53730d89d..173e86f1f 100644 --- a/test/pdfs/.gitignore +++ b/test/pdfs/.gitignore @@ -256,4 +256,5 @@ !annotation-highlight.pdf !annotation-fileattachment.pdf !annotation-text-widget.pdf +!annotation-choice-widget.pdf !zero_descent.pdf diff --git a/test/pdfs/annotation-choice-widget.pdf b/test/pdfs/annotation-choice-widget.pdf new file mode 100644 index 000000000..b557d9a85 Binary files /dev/null and b/test/pdfs/annotation-choice-widget.pdf differ diff --git a/test/test_manifest.json b/test/test_manifest.json index d4a40e056..231921876 100644 --- a/test/test_manifest.json +++ b/test/test_manifest.json @@ -3240,6 +3240,20 @@ "type": "eq", "forms": true }, + { "id": "annotation-choice-widget-annotations", + "file": "pdfs/annotation-choice-widget.pdf", + "md5": "7dfb0d743a0da0f4a71b209ab43b0be5", + "rounds": 1, + "type": "eq", + "annotations": true + }, + { "id": "annotation-choice-widget-forms", + "file": "pdfs/annotation-choice-widget.pdf", + "md5": "7dfb0d743a0da0f4a71b209ab43b0be5", + "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 ad3ba8a42..89b02fe73 100644 --- a/test/unit/annotation_layer_spec.js +++ b/test/unit/annotation_layer_spec.js @@ -614,6 +614,175 @@ describe('Annotation layer', function() { }); }); + describe('ChoiceWidgetAnnotation', function() { + var choiceWidgetDict; + + beforeEach(function (done) { + choiceWidgetDict = new Dict(); + choiceWidgetDict.set('Type', Name.get('Annot')); + choiceWidgetDict.set('Subtype', Name.get('Widget')); + choiceWidgetDict.set('FT', Name.get('Ch')); + + done(); + }); + + afterEach(function () { + choiceWidgetDict = null; + }); + + it('should handle missing option arrays', function() { + var choiceWidgetRef = new Ref(122, 0); + var xref = new XRefMock([ + { ref: choiceWidgetRef, data: choiceWidgetDict, } + ]); + + var choiceWidgetAnnotation = annotationFactory.create(xref, + choiceWidgetRef); + var data = choiceWidgetAnnotation.data; + expect(data.annotationType).toEqual(AnnotationType.WIDGET); + expect(data.options).toEqual([]); + }); + + it('should handle option arrays with array elements', function() { + var options = [['foo_export', 'Foo'], ['bar_export', 'Bar']]; + var expected = [ + { + exportValue: 'foo_export', + displayValue: 'Foo' + }, + { + exportValue: 'bar_export', + displayValue: 'Bar' + } + ]; + + choiceWidgetDict.set('Opt', options); + + var choiceWidgetRef = new Ref(123, 0); + var xref = new XRefMock([ + { ref: choiceWidgetRef, data: choiceWidgetDict, } + ]); + + var choiceWidgetAnnotation = annotationFactory.create(xref, + choiceWidgetRef); + var data = choiceWidgetAnnotation.data; + expect(data.annotationType).toEqual(AnnotationType.WIDGET); + expect(data.options).toEqual(expected); + }); + + it('should handle option arrays with string elements', function() { + var options = ['Foo', 'Bar']; + var expected = [ + { + exportValue: 'Foo', + displayValue: 'Foo' + }, + { + exportValue: 'Bar', + displayValue: 'Bar' + } + ]; + + choiceWidgetDict.set('Opt', options); + + var choiceWidgetRef = new Ref(981, 0); + var xref = new XRefMock([ + { ref: choiceWidgetRef, data: choiceWidgetDict, } + ]); + + var choiceWidgetAnnotation = annotationFactory.create(xref, + choiceWidgetRef); + var data = choiceWidgetAnnotation.data; + expect(data.annotationType).toEqual(AnnotationType.WIDGET); + expect(data.options).toEqual(expected); + }); + + it('should handle array field values', function() { + var fieldValue = ['Foo', 'Bar']; + + choiceWidgetDict.set('V', fieldValue); + + var choiceWidgetRef = new Ref(968, 0); + var xref = new XRefMock([ + { ref: choiceWidgetRef, data: choiceWidgetDict, } + ]); + + var choiceWidgetAnnotation = annotationFactory.create(xref, + choiceWidgetRef); + var data = choiceWidgetAnnotation.data; + expect(data.annotationType).toEqual(AnnotationType.WIDGET); + expect(data.fieldValue).toEqual(fieldValue); + }); + + it('should handle string field values', function() { + var fieldValue = 'Foo'; + + choiceWidgetDict.set('V', fieldValue); + + var choiceWidgetRef = new Ref(978, 0); + var xref = new XRefMock([ + { ref: choiceWidgetRef, data: choiceWidgetDict, } + ]); + + var choiceWidgetAnnotation = annotationFactory.create(xref, + choiceWidgetRef); + var data = choiceWidgetAnnotation.data; + expect(data.annotationType).toEqual(AnnotationType.WIDGET); + expect(data.fieldValue).toEqual([fieldValue]); + }); + + it('should handle unknown flags', function() { + var choiceWidgetRef = new Ref(166, 0); + var xref = new XRefMock([ + { ref: choiceWidgetRef, data: choiceWidgetDict, } + ]); + + var choiceWidgetAnnotation = annotationFactory.create(xref, + choiceWidgetRef); + var data = choiceWidgetAnnotation.data; + expect(data.annotationType).toEqual(AnnotationType.WIDGET); + expect(data.readOnly).toEqual(false); + expect(data.combo).toEqual(false); + expect(data.multiSelect).toEqual(false); + }); + + it('should not set invalid flags', function() { + choiceWidgetDict.set('Ff', 'readonly'); + + var choiceWidgetRef = new Ref(165, 0); + var xref = new XRefMock([ + { ref: choiceWidgetRef, data: choiceWidgetDict, } + ]); + + var choiceWidgetAnnotation = annotationFactory.create(xref, + choiceWidgetRef); + var data = choiceWidgetAnnotation.data; + expect(data.annotationType).toEqual(AnnotationType.WIDGET); + expect(data.readOnly).toEqual(false); + expect(data.combo).toEqual(false); + expect(data.multiSelect).toEqual(false); + }); + + it('should set valid flags', function() { + choiceWidgetDict.set('Ff', AnnotationFieldFlag.READONLY + + AnnotationFieldFlag.COMBO + + AnnotationFieldFlag.MULTISELECT); + + var choiceWidgetRef = new Ref(512, 0); + var xref = new XRefMock([ + { ref: choiceWidgetRef, data: choiceWidgetDict, } + ]); + + var choiceWidgetAnnotation = annotationFactory.create(xref, + choiceWidgetRef); + var data = choiceWidgetAnnotation.data; + expect(data.annotationType).toEqual(AnnotationType.WIDGET); + expect(data.readOnly).toEqual(true); + expect(data.combo).toEqual(true); + expect(data.multiSelect).toEqual(true); + }); + }); + describe('FileAttachmentAnnotation', function() { var loadingTask; var annotations; diff --git a/web/annotation_layer_builder.css b/web/annotation_layer_builder.css index 0d46e7284..8cff630f9 100644 --- a/web/annotation_layer_builder.css +++ b/web/annotation_layer_builder.css @@ -42,7 +42,8 @@ } .annotationLayer .textWidgetAnnotation input, -.annotationLayer .textWidgetAnnotation textarea { +.annotationLayer .textWidgetAnnotation textarea, +.annotationLayer .choiceWidgetAnnotation select { background-color: rgba(0, 54, 255, 0.13); border: 1px solid transparent; box-sizing: border-box; @@ -60,19 +61,22 @@ } .annotationLayer .textWidgetAnnotation input[disabled], -.annotationLayer .textWidgetAnnotation textarea[disabled] { +.annotationLayer .textWidgetAnnotation textarea[disabled], +.annotationLayer .choiceWidgetAnnotation select[disabled] { background: none; border: 1px solid transparent; cursor: not-allowed; } .annotationLayer .textWidgetAnnotation input:hover, -.annotationLayer .textWidgetAnnotation textarea:hover { +.annotationLayer .textWidgetAnnotation textarea:hover, +.annotationLayer .choiceWidgetAnnotation select:hover { border: 1px solid #000; } .annotationLayer .textWidgetAnnotation input:focus, -.annotationLayer .textWidgetAnnotation textarea:focus { +.annotationLayer .textWidgetAnnotation textarea:focus, +.annotationLayer .choiceWidgetAnnotation select:focus { background: none; border: 1px solid transparent; }