diff --git a/src/core/annotation.js b/src/core/annotation.js index 20b7b7aa9..a0fefdfc3 100644 --- a/src/core/annotation.js +++ b/src/core/annotation.js @@ -115,6 +115,9 @@ AnnotationFactory.prototype = /** @lends AnnotationFactory.prototype */ { case 'Popup': return new PopupAnnotation(parameters); + case 'Line': + return new LineAnnotation(parameters); + case 'Highlight': return new HighlightAnnotation(parameters); @@ -955,6 +958,8 @@ var PopupAnnotation = (function PopupAnnotationClosure() { return; } + var parentSubtype = parentItem.get('Subtype'); + this.data.parentType = isName(parentSubtype) ? parentSubtype.name : null; this.data.parentId = dict.getRaw('Parent').toString(); this.data.title = stringToPDFString(parentItem.get('T') || ''); this.data.contents = stringToPDFString(parentItem.get('Contents') || ''); @@ -983,15 +988,28 @@ var PopupAnnotation = (function PopupAnnotationClosure() { return PopupAnnotation; })(); +var LineAnnotation = (function LineAnnotationClosure() { + function LineAnnotation(parameters) { + Annotation.call(this, parameters); + + this.data.annotationType = AnnotationType.LINE; + + var dict = parameters.dict; + this.data.lineCoordinates = Util.normalizeRect(dict.getArray('L')); + this._preparePopup(dict); + } + + Util.inherit(LineAnnotation, Annotation, {}); + + return LineAnnotation; +})(); + var HighlightAnnotation = (function HighlightAnnotationClosure() { function HighlightAnnotation(parameters) { Annotation.call(this, parameters); this.data.annotationType = AnnotationType.HIGHLIGHT; this._preparePopup(parameters.dict); - - // PDF viewers completely ignore any border styles. - this.data.borderStyle.setWidth(0); } Util.inherit(HighlightAnnotation, Annotation, {}); @@ -1005,9 +1023,6 @@ var UnderlineAnnotation = (function UnderlineAnnotationClosure() { this.data.annotationType = AnnotationType.UNDERLINE; this._preparePopup(parameters.dict); - - // PDF viewers completely ignore any border styles. - this.data.borderStyle.setWidth(0); } Util.inherit(UnderlineAnnotation, Annotation, {}); @@ -1021,9 +1036,6 @@ var SquigglyAnnotation = (function SquigglyAnnotationClosure() { this.data.annotationType = AnnotationType.SQUIGGLY; this._preparePopup(parameters.dict); - - // PDF viewers completely ignore any border styles. - this.data.borderStyle.setWidth(0); } Util.inherit(SquigglyAnnotation, Annotation, {}); @@ -1037,9 +1049,6 @@ var StrikeOutAnnotation = (function StrikeOutAnnotationClosure() { this.data.annotationType = AnnotationType.STRIKEOUT; this._preparePopup(parameters.dict); - - // PDF viewers completely ignore any border styles. - this.data.borderStyle.setWidth(0); } Util.inherit(StrikeOutAnnotation, Annotation, {}); diff --git a/src/display/annotation_layer.js b/src/display/annotation_layer.js index fded967d5..e807d7275 100644 --- a/src/display/annotation_layer.js +++ b/src/display/annotation_layer.js @@ -93,6 +93,9 @@ AnnotationElementFactory.prototype = case AnnotationType.POPUP: return new PopupAnnotationElement(parameters); + case AnnotationType.LINE: + return new LineAnnotationElement(parameters); + case AnnotationType.HIGHLIGHT: return new HighlightAnnotationElement(parameters); @@ -119,7 +122,7 @@ AnnotationElementFactory.prototype = * @alias AnnotationElement */ var AnnotationElement = (function AnnotationElementClosure() { - function AnnotationElement(parameters, isRenderable) { + function AnnotationElement(parameters, isRenderable, ignoreBorder) { this.isRenderable = isRenderable || false; this.data = parameters.data; this.layer = parameters.layer; @@ -131,7 +134,7 @@ var AnnotationElement = (function AnnotationElementClosure() { this.renderInteractiveForms = parameters.renderInteractiveForms; if (isRenderable) { - this.container = this._createContainer(); + this.container = this._createContainer(ignoreBorder); } } @@ -140,10 +143,12 @@ var AnnotationElement = (function AnnotationElementClosure() { * Create an empty container for the annotation's HTML element. * * @private + * @param {boolean} ignoreBorder * @memberof AnnotationElement * @returns {HTMLSectionElement} */ - _createContainer: function AnnotationElement_createContainer() { + _createContainer: + function AnnotationElement_createContainer(ignoreBorder) { var data = this.data, page = this.page, viewport = this.viewport; var container = document.createElement('section'); var width = data.rect[2] - data.rect[0]; @@ -165,7 +170,7 @@ var AnnotationElement = (function AnnotationElementClosure() { CustomStyle.setProp('transformOrigin', container, -rect[0] + 'px ' + -rect[1] + 'px'); - if (data.borderStyle.width > 0) { + if (!ignoreBorder && data.borderStyle.width > 0) { container.style.borderWidth = data.borderStyle.width + 'px'; if (data.borderStyle.style !== AnnotationBorderStyleType.UNDERLINE) { // Underline styles only have a bottom border, so we do not need @@ -679,6 +684,10 @@ var ChoiceWidgetAnnotationElement = ( * @alias PopupAnnotationElement */ var PopupAnnotationElement = (function PopupAnnotationElementClosure() { + // Do not render popup annotations for parent elements with these types as + // they create the popups themselves (because of custom trigger divs). + var IGNORE_TYPES = ['Line']; + function PopupAnnotationElement(parameters) { var isRenderable = !!(parameters.data.title || parameters.data.contents); AnnotationElement.call(this, parameters, isRenderable); @@ -695,6 +704,10 @@ var PopupAnnotationElement = (function PopupAnnotationElementClosure() { render: function PopupAnnotationElement_render() { this.container.className = 'popupAnnotation'; + if (IGNORE_TYPES.indexOf(this.data.parentType) >= 0) { + return this.container; + } + var selector = '[data-annotation-id="' + this.data.parentId + '"]'; var parentElement = this.layer.querySelector(selector); if (!parentElement) { @@ -864,6 +877,69 @@ var PopupElement = (function PopupElementClosure() { return PopupElement; })(); +/** + * @class + * @alias LineAnnotationElement + */ +var LineAnnotationElement = (function LineAnnotationElementClosure() { + var SVG_NS = 'http://www.w3.org/2000/svg'; + + function LineAnnotationElement(parameters) { + var isRenderable = !!(parameters.data.hasPopup || + parameters.data.title || parameters.data.contents); + AnnotationElement.call(this, parameters, isRenderable, + /* ignoreBorder = */ true); + } + + Util.inherit(LineAnnotationElement, AnnotationElement, { + /** + * Render the line annotation's HTML element in the empty container. + * + * @public + * @memberof LineAnnotationElement + * @returns {HTMLSectionElement} + */ + render: function LineAnnotationElement_render() { + this.container.className = 'lineAnnotation'; + + // Create an invisible line with the same starting and ending coordinates + // that acts as the trigger for the popup. Only the line itself should + // trigger the popup, not the entire container. + var data = this.data; + var width = data.rect[2] - data.rect[0]; + var height = data.rect[3] - data.rect[1]; + + var svg = document.createElementNS(SVG_NS, 'svg:svg'); + svg.setAttributeNS(null, 'version', '1.1'); + svg.setAttributeNS(null, 'width', width + 'px'); + svg.setAttributeNS(null, 'height', height + 'px'); + svg.setAttributeNS(null, 'preserveAspectRatio', 'none'); + svg.setAttributeNS(null, 'viewBox', '0 0 ' + width + ' ' + height); + + // PDF coordinates are calculated from a bottom left origin, so transform + // the line coordinates to a top left origin for the SVG element. + var line = document.createElementNS(SVG_NS, 'svg:line'); + line.setAttributeNS(null, 'x1', data.rect[2] - data.lineCoordinates[0]); + line.setAttributeNS(null, 'y1', data.rect[3] - data.lineCoordinates[1]); + line.setAttributeNS(null, 'x2', data.rect[2] - data.lineCoordinates[2]); + line.setAttributeNS(null, 'y2', data.rect[3] - data.lineCoordinates[3]); + line.setAttributeNS(null, 'stroke-width', data.borderStyle.width); + line.setAttributeNS(null, 'stroke', 'transparent'); + + svg.appendChild(line); + this.container.append(svg); + + // Create the popup ourselves so that we can bind it to the line instead + // of to the entire container (which is the default). + this._createPopup(this.container, line, this.data); + + return this.container; + } + }); + + return LineAnnotationElement; +})(); + /** * @class * @alias HighlightAnnotationElement @@ -873,7 +949,8 @@ var HighlightAnnotationElement = ( function HighlightAnnotationElement(parameters) { var isRenderable = !!(parameters.data.hasPopup || parameters.data.title || parameters.data.contents); - AnnotationElement.call(this, parameters, isRenderable); + AnnotationElement.call(this, parameters, isRenderable, + /* ignoreBorder = */ true); } Util.inherit(HighlightAnnotationElement, AnnotationElement, { @@ -890,7 +967,6 @@ var HighlightAnnotationElement = ( if (!this.data.hasPopup) { this._createPopup(this.container, null, this.data); } - return this.container; } }); @@ -907,7 +983,8 @@ var UnderlineAnnotationElement = ( function UnderlineAnnotationElement(parameters) { var isRenderable = !!(parameters.data.hasPopup || parameters.data.title || parameters.data.contents); - AnnotationElement.call(this, parameters, isRenderable); + AnnotationElement.call(this, parameters, isRenderable, + /* ignoreBorder = */ true); } Util.inherit(UnderlineAnnotationElement, AnnotationElement, { @@ -924,7 +1001,6 @@ var UnderlineAnnotationElement = ( if (!this.data.hasPopup) { this._createPopup(this.container, null, this.data); } - return this.container; } }); @@ -940,7 +1016,8 @@ var SquigglyAnnotationElement = (function SquigglyAnnotationElementClosure() { function SquigglyAnnotationElement(parameters) { var isRenderable = !!(parameters.data.hasPopup || parameters.data.title || parameters.data.contents); - AnnotationElement.call(this, parameters, isRenderable); + AnnotationElement.call(this, parameters, isRenderable, + /* ignoreBorder = */ true); } Util.inherit(SquigglyAnnotationElement, AnnotationElement, { @@ -957,7 +1034,6 @@ var SquigglyAnnotationElement = (function SquigglyAnnotationElementClosure() { if (!this.data.hasPopup) { this._createPopup(this.container, null, this.data); } - return this.container; } }); @@ -974,7 +1050,8 @@ var StrikeOutAnnotationElement = ( function StrikeOutAnnotationElement(parameters) { var isRenderable = !!(parameters.data.hasPopup || parameters.data.title || parameters.data.contents); - AnnotationElement.call(this, parameters, isRenderable); + AnnotationElement.call(this, parameters, isRenderable, + /* ignoreBorder = */ true); } Util.inherit(StrikeOutAnnotationElement, AnnotationElement, { @@ -991,7 +1068,6 @@ var StrikeOutAnnotationElement = ( if (!this.data.hasPopup) { this._createPopup(this.container, null, this.data); } - return this.container; } }); diff --git a/test/pdfs/.gitignore b/test/pdfs/.gitignore index 037c4f5ab..94fb31e7e 100644 --- a/test/pdfs/.gitignore +++ b/test/pdfs/.gitignore @@ -269,6 +269,7 @@ !annotation-strikeout.pdf !annotation-squiggly.pdf !annotation-highlight.pdf +!annotation-line.pdf !annotation-fileattachment.pdf !annotation-text-widget.pdf !annotation-choice-widget.pdf diff --git a/test/pdfs/annotation-line.pdf b/test/pdfs/annotation-line.pdf new file mode 100755 index 000000000..2a81992e7 Binary files /dev/null and b/test/pdfs/annotation-line.pdf differ diff --git a/test/test_manifest.json b/test/test_manifest.json index e512cad94..033a658ba 100644 --- a/test/test_manifest.json +++ b/test/test_manifest.json @@ -3370,6 +3370,13 @@ "type": "eq", "annotations": true }, + { "id": "annotation-line", + "file": "pdfs/annotation-line.pdf", + "md5": "fde60608be2748f10fb6522cba425ca1", + "rounds": 1, + "type": "eq", + "annotations": true + }, { "id": "annotation-fileattachment", "file": "pdfs/annotation-fileattachment.pdf", "md5": "d20ecee4b53c81b2dd44c8715a1b4a83", diff --git a/test/unit/annotation_spec.js b/test/unit/annotation_spec.js index 9693b4e58..7b7af73b4 100644 --- a/test/unit/annotation_spec.js +++ b/test/unit/annotation_spec.js @@ -1287,6 +1287,27 @@ describe('annotation', function() { }); }); + describe('LineAnnotation', function() { + it('should set the line coordinates', function() { + var lineDict = new Dict(); + lineDict.set('Type', Name.get('Annot')); + lineDict.set('Subtype', Name.get('Line')); + lineDict.set('L', [1, 2, 3, 4]); + + var lineRef = new Ref(122, 0); + var xref = new XRefMock([ + { ref: lineRef, data: lineDict, } + ]); + + var annotation = annotationFactory.create(xref, lineRef, pdfManagerMock, + idFactoryMock); + var data = annotation.data; + expect(data.annotationType).toEqual(AnnotationType.LINE); + + expect(data.lineCoordinates).toEqual([1, 2, 3, 4]); + }); + }); + describe('FileAttachmentAnnotation', function() { it('should correctly parse a file attachment', function() { var fileStream = new StringStream( diff --git a/web/annotation_layer_builder.css b/web/annotation_layer_builder.css index 982f66134..234d3d70e 100644 --- a/web/annotation_layer_builder.css +++ b/web/annotation_layer_builder.css @@ -143,6 +143,7 @@ .annotationLayer .underlineAnnotation, .annotationLayer .squigglyAnnotation, .annotationLayer .strikeoutAnnotation, +.annotationLayer .lineAnnotation svg line, .annotationLayer .fileAttachmentAnnotation { cursor: pointer; }