diff --git a/src/core/annotation.js b/src/core/annotation.js index 0ca76bf6a..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,6 +988,22 @@ 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); diff --git a/src/display/annotation_layer.js b/src/display/annotation_layer.js index 65fd7cacd..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); @@ -681,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); @@ -697,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) { @@ -866,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 diff --git a/test/pdfs/.gitignore b/test/pdfs/.gitignore index 0f7eabc30..35aef99f1 100644 --- a/test/pdfs/.gitignore +++ b/test/pdfs/.gitignore @@ -268,6 +268,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 bf7aee0bd..ac3f4ac95 100644 --- a/test/test_manifest.json +++ b/test/test_manifest.json @@ -3357,6 +3357,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; }