diff --git a/src/display/annotation_layer.js b/src/display/annotation_layer.js index 1014458eb..8011ed05d 100644 --- a/src/display/annotation_layer.js +++ b/src/display/annotation_layer.js @@ -14,8 +14,8 @@ */ import { - addLinkAttributes, CustomStyle, getDefaultSetting, getFilenameFromUrl, - LinkTarget + addLinkAttributes, CustomStyle, DOMSVGFactory, getDefaultSetting, + getFilenameFromUrl, LinkTarget } from './dom_utils'; import { AnnotationBorderStyleType, AnnotationType, stringToPDFString, Util, warn @@ -31,6 +31,7 @@ import { * @property {DownloadManager} downloadManager * @property {string} imageResourcesPath * @property {boolean} renderInteractiveForms + * @property {Object} svgFactory */ class AnnotationElementFactory { @@ -105,6 +106,7 @@ class AnnotationElement { this.downloadManager = parameters.downloadManager; this.imageResourcesPath = parameters.imageResourcesPath; this.renderInteractiveForms = parameters.renderInteractiveForms; + this.svgFactory = parameters.svgFactory; if (isRenderable) { this.container = this._createContainer(ignoreBorder); @@ -769,8 +771,6 @@ class LineAnnotationElement extends AnnotationElement { * @returns {HTMLSectionElement} */ render() { - const SVG_NS = 'http://www.w3.org/2000/svg'; - this.container.className = 'lineAnnotation'; // Create an invisible line with the same starting and ending coordinates @@ -779,30 +779,24 @@ class LineAnnotationElement extends AnnotationElement { let data = this.data; let width = data.rect[2] - data.rect[0]; let height = data.rect[3] - data.rect[1]; - - let 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); + let svg = this.svgFactory.create(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. - let 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'); + let line = this.svgFactory.createElement('svg:line'); + line.setAttribute('x1', data.rect[2] - data.lineCoordinates[0]); + line.setAttribute('y1', data.rect[3] - data.lineCoordinates[1]); + line.setAttribute('x2', data.rect[2] - data.lineCoordinates[2]); + line.setAttribute('y2', data.rect[3] - data.lineCoordinates[3]); + line.setAttribute('stroke-width', data.borderStyle.width); + line.setAttribute('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); + this._createPopup(this.container, line, data); return this.container; } @@ -993,6 +987,7 @@ class AnnotationLayer { imageResourcesPath: parameters.imageResourcesPath || getDefaultSetting('imageResourcesPath'), renderInteractiveForms: parameters.renderInteractiveForms || false, + svgFactory: new DOMSVGFactory(), }); if (element.isRenderable) { parameters.div.appendChild(element.render()); diff --git a/src/display/dom_utils.js b/src/display/dom_utils.js index 6fca2ae2e..4d0116849 100644 --- a/src/display/dom_utils.js +++ b/src/display/dom_utils.js @@ -14,12 +14,13 @@ */ import { - CMapCompressionType, createValidAbsoluteUrl, deprecated, + assert, CMapCompressionType, createValidAbsoluteUrl, deprecated, removeNullCharacters, stringToBytes, warn } from '../shared/util'; import globalScope from '../shared/global_scope'; -var DEFAULT_LINK_REL = 'noopener noreferrer nofollow'; +const DEFAULT_LINK_REL = 'noopener noreferrer nofollow'; +const SVG_NS = 'http://www.w3.org/2000/svg'; class DOMCanvasFactory { create(width, height) { @@ -109,6 +110,27 @@ class DOMCMapReaderFactory { } } +class DOMSVGFactory { + create(width, height) { + assert(width > 0 && height > 0, 'Invalid SVG dimensions'); + + let svg = document.createElementNS(SVG_NS, 'svg:svg'); + svg.setAttribute('version', '1.1'); + svg.setAttribute('width', width + 'px'); + svg.setAttribute('height', height + 'px'); + svg.setAttribute('preserveAspectRatio', 'none'); + svg.setAttribute('viewBox', '0 0 ' + width + ' ' + height); + + return svg; + } + + createElement(type) { + assert(typeof type === 'string', 'Invalid SVG element type'); + + return document.createElementNS(SVG_NS, type); + } +} + /** * Optimised CSS custom property getter/setter. * @class @@ -330,4 +352,5 @@ export { DEFAULT_LINK_REL, DOMCanvasFactory, DOMCMapReaderFactory, + DOMSVGFactory, }; diff --git a/src/display/svg.js b/src/display/svg.js index 1ce15e383..a9367ea01 100644 --- a/src/display/svg.js +++ b/src/display/svg.js @@ -18,6 +18,7 @@ import { createObjectURL, FONT_IDENTITY_MATRIX, IDENTITY_MATRIX, ImageKind, isNodeJS, isNum, OPS, Util, warn } from '../shared/util'; +import { DOMSVGFactory } from './dom_utils'; var SVGGraphics = function() { throw new Error('Not implemented: SVGGraphics'); @@ -403,6 +404,8 @@ SVGGraphics = (function SVGGraphicsClosure() { } function SVGGraphics(commonObjs, objs, forceDataSchema) { + this.svgFactory = new DOMSVGFactory(); + this.current = new SVGExtraState(); this.transformMatrix = IDENTITY_MATRIX; // Graphics state matrix this.transformStack = []; @@ -418,7 +421,6 @@ SVGGraphics = (function SVGGraphicsClosure() { this.forceDataSchema = !!forceDataSchema; } - var NS = 'http://www.w3.org/2000/svg'; var XML_NS = 'http://www.w3.org/XML/1998/namespace'; var XLINK_NS = 'http://www.w3.org/1999/xlink'; var LINE_CAP_STYLES = ['butt', 'round', 'square']; @@ -683,13 +685,13 @@ SVGGraphics = (function SVGGraphicsClosure() { this.current.y = this.current.lineY = 0; current.xcoords = []; - current.tspan = document.createElementNS(NS, 'svg:tspan'); + current.tspan = this.svgFactory.createElement('svg:tspan'); current.tspan.setAttributeNS(null, 'font-family', current.fontFamily); current.tspan.setAttributeNS(null, 'font-size', pf(current.fontSize) + 'px'); current.tspan.setAttributeNS(null, 'y', pf(-current.y)); - current.txtElement = document.createElementNS(NS, 'svg:text'); + current.txtElement = this.svgFactory.createElement('svg:text'); current.txtElement.appendChild(current.tspan); }, @@ -698,9 +700,9 @@ SVGGraphics = (function SVGGraphicsClosure() { this.current.y = this.current.lineY = 0; this.current.textMatrix = IDENTITY_MATRIX; this.current.lineMatrix = IDENTITY_MATRIX; - this.current.tspan = document.createElementNS(NS, 'svg:tspan'); - this.current.txtElement = document.createElementNS(NS, 'svg:text'); - this.current.txtgrp = document.createElementNS(NS, 'svg:g'); + this.current.tspan = this.svgFactory.createElement('svg:tspan'); + this.current.txtElement = this.svgFactory.createElement('svg:text'); + this.current.txtgrp = this.svgFactory.createElement('svg:g'); this.current.xcoords = []; }, @@ -710,7 +712,7 @@ SVGGraphics = (function SVGGraphicsClosure() { this.current.y = this.current.lineY += y; current.xcoords = []; - current.tspan = document.createElementNS(NS, 'svg:tspan'); + current.tspan = this.svgFactory.createElement('svg:tspan'); current.tspan.setAttributeNS(null, 'font-family', current.fontFamily); current.tspan.setAttributeNS(null, 'font-size', pf(current.fontSize) + 'px'); @@ -810,7 +812,7 @@ SVGGraphics = (function SVGGraphicsClosure() { addFontStyle: function SVGGraphics_addFontStyle(fontObj) { if (!this.cssStyle) { - this.cssStyle = document.createElementNS(NS, 'svg:style'); + this.cssStyle = this.svgFactory.createElement('svg:style'); this.cssStyle.setAttributeNS(null, 'type', 'text/css'); this.defs.appendChild(this.cssStyle); } @@ -852,7 +854,7 @@ SVGGraphics = (function SVGGraphicsClosure() { current.fontWeight = bold; current.fontStyle = italic; - current.tspan = document.createElementNS(NS, 'svg:tspan'); + current.tspan = this.svgFactory.createElement('svg:tspan'); current.tspan.setAttributeNS(null, 'y', pf(-current.y)); current.xcoords = []; }, @@ -885,7 +887,7 @@ SVGGraphics = (function SVGGraphicsClosure() { setFillRGBColor: function SVGGraphics_setFillRGBColor(r, g, b) { var color = Util.makeCssRgb(r, g, b); this.current.fillColor = color; - this.current.tspan = document.createElementNS(NS, 'svg:tspan'); + this.current.tspan = this.svgFactory.createElement('svg:tspan'); this.current.xcoords = []; }, setDash: function SVGGraphics_setDash(dashArray, dashPhase) { @@ -896,7 +898,7 @@ SVGGraphics = (function SVGGraphicsClosure() { constructPath: function SVGGraphics_constructPath(ops, args) { var current = this.current; var x = current.x, y = current.y; - current.path = document.createElementNS(NS, 'svg:path'); + current.path = this.svgFactory.createElement('svg:path'); var d = []; var opLength = ops.length; @@ -967,7 +969,7 @@ SVGGraphics = (function SVGGraphicsClosure() { // Add current path to clipping path var clipId = 'clippath' + clipCount; clipCount++; - var clipPath = document.createElementNS(NS, 'svg:clipPath'); + var clipPath = this.svgFactory.createElement('svg:clipPath'); clipPath.setAttributeNS(null, 'id', clipId); clipPath.setAttributeNS(null, 'transform', pm(this.transformMatrix)); var clipElement = current.element.cloneNode(); @@ -1110,7 +1112,7 @@ SVGGraphics = (function SVGGraphicsClosure() { paintSolidColorImageMask: function SVGGraphics_paintSolidColorImageMask() { var current = this.current; - var rect = document.createElementNS(NS, 'svg:rect'); + var rect = this.svgFactory.createElement('svg:rect'); rect.setAttributeNS(null, 'x', '0'); rect.setAttributeNS(null, 'y', '0'); rect.setAttributeNS(null, 'width', '1px'); @@ -1122,7 +1124,7 @@ SVGGraphics = (function SVGGraphicsClosure() { paintJpegXObject: function SVGGraphics_paintJpegXObject(objId, w, h) { var imgObj = this.objs.get(objId); - var imgEl = document.createElementNS(NS, 'svg:image'); + var imgEl = this.svgFactory.createElement('svg:image'); imgEl.setAttributeNS(XLINK_NS, 'xlink:href', imgObj.src); imgEl.setAttributeNS(null, 'width', pf(w)); imgEl.setAttributeNS(null, 'height', pf(h)); @@ -1149,14 +1151,14 @@ SVGGraphics = (function SVGGraphicsClosure() { var height = imgData.height; var imgSrc = convertImgDataToPng(imgData, this.forceDataSchema); - var cliprect = document.createElementNS(NS, 'svg:rect'); + var cliprect = this.svgFactory.createElement('svg:rect'); cliprect.setAttributeNS(null, 'x', '0'); cliprect.setAttributeNS(null, 'y', '0'); cliprect.setAttributeNS(null, 'width', pf(width)); cliprect.setAttributeNS(null, 'height', pf(height)); this.current.element = cliprect; this.clip('nonzero'); - var imgEl = document.createElementNS(NS, 'svg:image'); + var imgEl = this.svgFactory.createElement('svg:image'); imgEl.setAttributeNS(XLINK_NS, 'xlink:href', imgSrc); imgEl.setAttributeNS(null, 'x', '0'); imgEl.setAttributeNS(null, 'y', pf(-height)); @@ -1180,10 +1182,10 @@ SVGGraphics = (function SVGGraphicsClosure() { var fillColor = current.fillColor; current.maskId = 'mask' + maskCount++; - var mask = document.createElementNS(NS, 'svg:mask'); + var mask = this.svgFactory.createElement('svg:mask'); mask.setAttributeNS(null, 'id', current.maskId); - var rect = document.createElementNS(NS, 'svg:rect'); + var rect = this.svgFactory.createElement('svg:rect'); rect.setAttributeNS(null, 'x', '0'); rect.setAttributeNS(null, 'y', '0'); rect.setAttributeNS(null, 'width', pf(width)); @@ -1208,7 +1210,7 @@ SVGGraphics = (function SVGGraphicsClosure() { var width = bbox[2] - bbox[0]; var height = bbox[3] - bbox[1]; - var cliprect = document.createElementNS(NS, 'svg:rect'); + var cliprect = this.svgFactory.createElement('svg:rect'); cliprect.setAttributeNS(null, 'x', bbox[0]); cliprect.setAttributeNS(null, 'y', bbox[1]); cliprect.setAttributeNS(null, 'width', pf(width)); @@ -1225,24 +1227,17 @@ SVGGraphics = (function SVGGraphicsClosure() { /** * @private */ - _initialize: function SVGGraphics_initialize(viewport) { - // Create the SVG element. - var svg = document.createElementNS(NS, 'svg:svg'); - svg.setAttributeNS(null, 'version', '1.1'); - svg.setAttributeNS(null, 'width', viewport.width + 'px'); - svg.setAttributeNS(null, 'height', viewport.height + 'px'); - svg.setAttributeNS(null, 'preserveAspectRatio', 'none'); - svg.setAttributeNS(null, 'viewBox', '0 0 ' + viewport.width + - ' ' + viewport.height); + _initialize(viewport) { + let svg = this.svgFactory.create(viewport.width, viewport.height); // Create the definitions element. - var definitions = document.createElementNS(NS, 'svg:defs'); + let definitions = this.svgFactory.createElement('svg:defs'); svg.appendChild(definitions); this.defs = definitions; // Create the root group element, which acts a container for all other // groups and applies the viewport transform. - var rootGroup = document.createElementNS(NS, 'svg:g'); + let rootGroup = this.svgFactory.createElement('svg:g'); rootGroup.setAttributeNS(null, 'transform', pm(viewport.transform)); svg.appendChild(rootGroup); @@ -1259,7 +1254,7 @@ SVGGraphics = (function SVGGraphicsClosure() { */ _ensureClipGroup: function SVGGraphics_ensureClipGroup() { if (!this.current.clipGroup) { - var clipGroup = document.createElementNS(NS, 'svg:g'); + var clipGroup = this.svgFactory.createElement('svg:g'); clipGroup.setAttributeNS(null, 'clip-path', this.current.activeClipUrl); this.svg.appendChild(clipGroup); @@ -1273,7 +1268,7 @@ SVGGraphics = (function SVGGraphicsClosure() { */ _ensureTransformGroup: function SVGGraphics_ensureTransformGroup() { if (!this.tgrp) { - this.tgrp = document.createElementNS(NS, 'svg:g'); + this.tgrp = this.svgFactory.createElement('svg:g'); this.tgrp.setAttributeNS(null, 'transform', pm(this.transformMatrix)); if (this.current.activeClipUrl) { this._ensureClipGroup().appendChild(this.tgrp); diff --git a/test/unit/dom_utils_spec.js b/test/unit/dom_utils_spec.js index 6457131c4..ab1f82d4f 100644 --- a/test/unit/dom_utils_spec.js +++ b/test/unit/dom_utils_spec.js @@ -14,11 +14,72 @@ */ import { - getFilenameFromUrl, isExternalLinkTargetSet, LinkTarget + DOMSVGFactory, getFilenameFromUrl, isExternalLinkTargetSet, LinkTarget } from '../../src/display/dom_utils'; +import { isNodeJS } from '../../src/shared/util'; import { PDFJS } from '../../src/display/global'; describe('dom_utils', function() { + describe('DOMSVGFactory', function() { + let svgFactory; + + beforeAll(function (done) { + svgFactory = new DOMSVGFactory(); + done(); + }); + + afterAll(function () { + svgFactory = null; + }); + + it('`create` should throw an error if the dimensions are invalid', + function() { + // Invalid width. + expect(function() { + return svgFactory.create(-1, 0); + }).toThrow(new Error('Invalid SVG dimensions')); + + // Invalid height. + expect(function() { + return svgFactory.create(0, -1); + }).toThrow(new Error('Invalid SVG dimensions')); + }); + + it('`create` should return an SVG element if the dimensions are valid', + function() { + if (isNodeJS()) { + pending('Document is not supported in Node.js.'); + } + + let svg = svgFactory.create(20, 40); + + expect(svg instanceof SVGSVGElement).toBe(true); + expect(svg.getAttribute('version')).toBe('1.1'); + expect(svg.getAttribute('width')).toBe('20px'); + expect(svg.getAttribute('height')).toBe('40px'); + expect(svg.getAttribute('preserveAspectRatio')).toBe('none'); + expect(svg.getAttribute('viewBox')).toBe('0 0 20 40'); + }); + + it('`createElement` should throw an error if the type is not a string', + function() { + expect(function() { + return svgFactory.createElement(true); + }).toThrow(new Error('Invalid SVG element type')); + }); + + it('`createElement` should return an SVG element if the type is valid', + function() { + if (isNodeJS()) { + pending('Document is not supported in Node.js.'); + } + + let svg = svgFactory.createElement('svg:rect'); + + expect(svg instanceof SVGRectElement).toBe(true); + }); + }); + describe('getFilenameFromUrl', function() { it('should get the filename from an absolute URL', function() { var url = 'http://server.org/filename.pdf';