diff --git a/src/core/annotation.js b/src/core/annotation.js index c2688cd46..6246671ad 100644 --- a/src/core/annotation.js +++ b/src/core/annotation.js @@ -106,6 +106,9 @@ class AnnotationFactory { case 'Polygon': return new PolygonAnnotation(parameters); + case 'Ink': + return new InkAnnotation(parameters); + case 'Highlight': return new HighlightAnnotation(parameters); @@ -1013,6 +1016,34 @@ class PolygonAnnotation extends PolylineAnnotation { } } +class InkAnnotation extends Annotation { + constructor(parameters) { + super(parameters); + + this.data.annotationType = AnnotationType.INK; + + let dict = parameters.dict; + const xref = parameters.xref; + + let originalInkLists = dict.getArray('InkList'); + this.data.inkLists = []; + for (let i = 0, ii = originalInkLists.length; i < ii; ++i) { + // The raw ink lists array contains arrays of numbers representing + // the alternating horizontal and vertical coordinates, respectively, + // of each vertex. Convert this to an array of objects with x and y + // coordinates. + this.data.inkLists.push([]); + for (let j = 0, jj = originalInkLists[i].length; j < jj; j += 2) { + this.data.inkLists[i].push({ + x: xref.fetchIfRef(originalInkLists[i][j]), + y: xref.fetchIfRef(originalInkLists[i][j + 1]), + }); + } + } + this._preparePopup(dict); + } +} + class HighlightAnnotation extends Annotation { constructor(parameters) { super(parameters); diff --git a/src/display/annotation_layer.js b/src/display/annotation_layer.js index a756b51f7..27113f818 100644 --- a/src/display/annotation_layer.js +++ b/src/display/annotation_layer.js @@ -83,6 +83,9 @@ class AnnotationElementFactory { case AnnotationType.POLYLINE: return new PolylineAnnotationElement(parameters); + case AnnotationType.INK: + return new InkAnnotationElement(parameters); + case AnnotationType.POLYGON: return new PolygonAnnotationElement(parameters); @@ -628,7 +631,14 @@ class PopupAnnotationElement extends AnnotationElement { render() { // Do not render popup annotations for parent elements with these types as // they create the popups themselves (because of custom trigger divs). - const IGNORE_TYPES = ['Line', 'Square', 'Circle', 'PolyLine', 'Polygon']; + const IGNORE_TYPES = [ + 'Line', + 'Square', + 'Circle', + 'PolyLine', + 'Polygon', + 'Ink', + ]; this.container.className = 'popupAnnotation'; @@ -1006,6 +1016,73 @@ class PolygonAnnotationElement extends PolylineAnnotationElement { } } +class InkAnnotationElement extends AnnotationElement { + constructor(parameters) { + let isRenderable = !!(parameters.data.hasPopup || + parameters.data.title || parameters.data.contents); + super(parameters, isRenderable, /* ignoreBorder = */ true); + + this.containerClassName = 'inkAnnotation'; + + // Use the polyline SVG element since it allows us to use coordinates + // directly and to draw both straight lines and curves. + this.svgElementName = 'svg:polyline'; + } + + /** + * Render the ink annotation's HTML element in the empty container. + * + * @public + * @memberof InkAnnotationElement + * @returns {HTMLSectionElement} + */ + render() { + this.container.className = this.containerClassName; + + // Create an invisible polyline with the same points that acts as the + // trigger for the popup. + let data = this.data; + let width = data.rect[2] - data.rect[0]; + let height = data.rect[3] - data.rect[1]; + let svg = this.svgFactory.create(width, height); + + let inkLists = data.inkLists; + for (let i = 0, ii = inkLists.length; i < ii; i++) { + let inkList = inkLists[i]; + let points = []; + + // Convert the ink list to a single points string that the SVG + // polyline element expects ("x1,y1 x2,y2 ..."). PDF coordinates are + // calculated from a bottom left origin, so transform the polyline + // coordinates to a top left origin for the SVG element. + for (let j = 0, jj = inkList.length; j < jj; j++) { + let x = inkList[j].x - data.rect[0]; + let y = data.rect[3] - inkList[j].y; + points.push(x + ',' + y); + } + + points = points.join(' '); + + let borderWidth = data.borderStyle.width; + let polyline = this.svgFactory.createElement(this.svgElementName); + polyline.setAttribute('points', points); + polyline.setAttribute('stroke-width', borderWidth); + polyline.setAttribute('stroke', 'transparent'); + polyline.setAttribute('fill', 'none'); + + // Create the popup ourselves so that we can bind it to the polyline + // instead of to the entire container (which is the default). + this._createPopup(this.container, polyline, data); + + svg.appendChild(polyline); + } + + this.container.append(svg); + + return this.container; + } +} + class HighlightAnnotationElement extends AnnotationElement { constructor(parameters) { let isRenderable = !!(parameters.data.hasPopup || diff --git a/test/pdfs/.gitignore b/test/pdfs/.gitignore index deae14fc0..709e141f2 100644 --- a/test/pdfs/.gitignore +++ b/test/pdfs/.gitignore @@ -272,6 +272,7 @@ !text_clip_cff_cid.pdf !issue4801.pdf !issue5334.pdf +!annotation-caret-ink.pdf !bug1186827.pdf !issue215.pdf !issue5044.pdf diff --git a/test/pdfs/annotation-caret-ink.pdf b/test/pdfs/annotation-caret-ink.pdf new file mode 100644 index 000000000..6e8679fa5 Binary files /dev/null and b/test/pdfs/annotation-caret-ink.pdf differ diff --git a/test/test_manifest.json b/test/test_manifest.json index 432ab2054..31000b690 100644 --- a/test/test_manifest.json +++ b/test/test_manifest.json @@ -1776,6 +1776,13 @@ "lastPage": 1, "type": "load" }, + { "id": "annotation-caret-ink", + "file": "pdfs/annotation-caret-ink.pdf", + "md5": "6218ca235580d1975474c979e0128c2d", + "rounds": 1, + "type": "eq", + "annotations": true + }, { "id": "bug1130815-eq", "file": "pdfs/bug1130815.pdf", "md5": "3ff3b550c3af766991b2a1b11d00de85", diff --git a/test/unit/annotation_spec.js b/test/unit/annotation_spec.js index 27ddb14b6..49c9b7c3a 100644 --- a/test/unit/annotation_spec.js +++ b/test/unit/annotation_spec.js @@ -1435,4 +1435,59 @@ describe('annotation', function() { }, done.fail); }); }); + + describe('InkAnnotation', function() { + it('should handle a single ink list', function(done) { + const inkDict = new Dict(); + inkDict.set('Type', Name.get('Annot')); + inkDict.set('Subtype', Name.get('Ink')); + inkDict.set('InkList', [[1, 1, 1, 2, 2, 2, 3, 3]]); + + const inkRef = new Ref(142, 0); + const xref = new XRefMock([ + { ref: inkRef, data: inkDict, } + ]); + + AnnotationFactory.create(xref, inkRef, pdfManagerMock, + idFactoryMock).then(({ data, }) => { + expect(data.annotationType).toEqual(AnnotationType.INK); + expect(data.inkLists.length).toEqual(1); + expect(data.inkLists[0]).toEqual([ + { x: 1, y: 1, }, + { x: 1, y: 2, }, + { x: 2, y: 2, }, + { x: 3, y: 3, }, + ]); + done(); + }, done.fail); + }); + + it('should handle multiple ink lists', function(done) { + const inkDict = new Dict(); + inkDict.set('Type', Name.get('Annot')); + inkDict.set('Subtype', Name.get('Ink')); + inkDict.set('InkList', [ + [1, 1, 1, 2], + [3, 3, 4, 5], + ]); + + const inkRef = new Ref(143, 0); + const xref = new XRefMock([ + { ref: inkRef, data: inkDict, } + ]); + + AnnotationFactory.create(xref, inkRef, pdfManagerMock, + idFactoryMock).then(({ data, }) => { + expect(data.annotationType).toEqual(AnnotationType.INK); + expect(data.inkLists.length).toEqual(2); + expect(data.inkLists[0]).toEqual([ + { x: 1, y: 1, }, { x: 1, y: 2, } + ]); + expect(data.inkLists[1]).toEqual([ + { x: 3, y: 3, }, { x: 4, y: 5, } + ]); + done(); + }, done.fail); + }); + }); }); diff --git a/web/annotation_layer_builder.css b/web/annotation_layer_builder.css index 016e186c7..8e9fd486e 100644 --- a/web/annotation_layer_builder.css +++ b/web/annotation_layer_builder.css @@ -188,6 +188,7 @@ .annotationLayer .circleAnnotation svg ellipse, .annotationLayer .polylineAnnotation svg polyline, .annotationLayer .polygonAnnotation svg polygon, +.annotationLayer .inkAnnotation svg polyline, .annotationLayer .stampAnnotation, .annotationLayer .fileAttachmentAnnotation { cursor: pointer;