From de6c92a96dace1a47dbfda44c9fa7ab6370a7f5e Mon Sep 17 00:00:00 2001 From: Tim van der Meij Date: Wed, 12 Oct 2016 21:13:37 +0200 Subject: [PATCH 1/3] Examples: improve SVG viewer This patch: - resolves a warning in the console about missing character encoding; - makes the viewer use the same background color and PDF file as the regular viewer; - simplifies the example to bring it more in line with the other examples. --- examples/svgviewer/index.html | 17 ++++++++++------- examples/svgviewer/viewer.js | 27 ++++++++------------------- 2 files changed, 18 insertions(+), 26 deletions(-) diff --git a/examples/svgviewer/index.html b/examples/svgviewer/index.html index 931b595a3..70bf357ed 100644 --- a/examples/svgviewer/index.html +++ b/examples/svgviewer/index.html @@ -2,21 +2,24 @@ + + + PDF.js SVG viewer example + + - - SVG Viewer Example + diff --git a/examples/svgviewer/viewer.js b/examples/svgviewer/viewer.js index 51f5ad2dd..7d330419e 100644 --- a/examples/svgviewer/viewer.js +++ b/examples/svgviewer/viewer.js @@ -1,40 +1,29 @@ 'use strict'; +var DEFAULT_SCALE = 1.5; + // Parse query string to extract some parameters (it can fail for some input) var query = document.location.href.replace(/^[^?]*(\?([^#]*))?(#.*)?/, '$2'); var queryParams = query ? JSON.parse('{' + query.split('&').map(function (a) { return a.split('=').map(decodeURIComponent).map(JSON.stringify).join(': '); }).join(',') + '}') : {}; -var url = queryParams.file || '../../test/pdfs/liveprogramming.pdf'; -var scale = +queryParams.scale || 1.5; +var url = queryParams.file || '../../web/compressed.tracemonkey-pldi-09.pdf'; function renderDocument(pdf, svgLib) { - var numPages = pdf.numPages; - // Using promise to fetch the page - - // For testing only. - var MAX_NUM_PAGES = 50; - var ii = Math.min(MAX_NUM_PAGES, numPages); - var promise = Promise.resolve(); - for (var i = 1; i <= ii; i++) { - var anchor = document.createElement('a'); - anchor.setAttribute('name', 'page=' + i); - anchor.setAttribute('title', 'Page ' + i); - document.body.appendChild(anchor); - + for (var i = 1; i <= pdf.numPages; i++) { // Using promise to fetch and render the next page - promise = promise.then(function (pageNum, anchor) { + promise = promise.then(function (pageNum) { return pdf.getPage(pageNum).then(function (page) { - var viewport = page.getViewport(scale); + var viewport = page.getViewport(DEFAULT_SCALE); var container = document.createElement('div'); container.id = 'pageContainer' + pageNum; container.className = 'pageContainer'; container.style.width = viewport.width + 'px'; container.style.height = viewport.height + 'px'; - anchor.appendChild(container); + document.body.appendChild(container); return page.getOperatorList().then(function (opList) { var svgGfx = new svgLib.SVGGraphics(page.commonObjs, page.objs); @@ -43,7 +32,7 @@ function renderDocument(pdf, svgLib) { }); }); }); - }.bind(null, i, anchor)); + }.bind(null, i)); } } From fa90573c4b41eb9c145c82705647e28581497357 Mon Sep 17 00:00:00 2001 From: Tim van der Meij Date: Wed, 12 Oct 2016 21:22:35 +0200 Subject: [PATCH 2/3] SVG: optimize transform group creation This patch ensures that we only create transformation groups when it is actually required and that we re-use transform groups as much as possible. It reduces the number of transform groups for the Tracemonkey paper from 2790 to 1271, thereby making the DOM much lighter and rendering/scrolling smoother. Moreover, it simplifies the code and prevents duplication. Finally, we issue a warning when an unimplemented graphic state is encountered. Before, this was ignored silently, making debugging harder. --- src/display/svg.js | 118 +++++++++++++++------------------------------ 1 file changed, 38 insertions(+), 80 deletions(-) diff --git a/src/display/svg.js b/src/display/svg.js index 2d210f9fc..3fa536cf7 100644 --- a/src/display/svg.js +++ b/src/display/svg.js @@ -409,9 +409,7 @@ var SVGGraphics = (function SVGGraphicsClosure() { this.transformMatrix = this.transformStack.pop(); this.current = this.extraStack.pop(); - this.tgrp = document.createElementNS(NS, 'svg:g'); - this.tgrp.setAttributeNS(null, 'transform', pm(this.transformMatrix)); - this.pgrp.appendChild(this.tgrp); + this.tgrp = null; }, group: function SVGGraphics_group(items) { @@ -453,9 +451,7 @@ var SVGGraphics = (function SVGGraphicsClosure() { var transformMatrix = [a, b, c, d, e, f]; this.transformMatrix = Util.transform(this.transformMatrix, transformMatrix); - - this.tgrp = document.createElementNS(NS, 'svg:g'); - this.tgrp.setAttributeNS(null, 'transform', pm(this.transformMatrix)); + this.tgrp = null; }, getSVG: function SVGGraphics_getSVG(operatorList, viewport) { @@ -464,14 +460,9 @@ var SVGGraphics = (function SVGGraphicsClosure() { return this.loadDependencies(operatorList).then(function () { this.transformMatrix = IDENTITY_MATRIX; - this.pgrp = document.createElementNS(NS, 'svg:g'); // Parent group - this.pgrp.setAttributeNS(null, 'transform', pm(viewport.transform)); - this.tgrp = document.createElementNS(NS, 'svg:g'); // Transform group - this.tgrp.setAttributeNS(null, 'transform', pm(this.transformMatrix)); this.defs = document.createElementNS(NS, 'svg:defs'); - this.pgrp.appendChild(this.defs); - this.pgrp.appendChild(this.tgrp); - this.svg.appendChild(this.pgrp); + this.svg.setAttributeNS(null, 'transform', pm(viewport.transform)); + this.svg.appendChild(this.defs); var opTree = this.convertOpList(operatorList); this.executeOpTree(opTree); return this.svg; @@ -633,7 +624,7 @@ var SVGGraphics = (function SVGGraphicsClosure() { this.group(opTree[x].items); break; default: - warn('Unimplemented method '+ fn); + warn('Unimplemented operator ' + fn); break; } } @@ -759,8 +750,7 @@ var SVGGraphics = (function SVGGraphicsClosure() { current.txtElement.appendChild(current.tspan); current.txtgrp.appendChild(current.txtElement); - this.tgrp.appendChild(current.txtElement); - + this._ensureTransformGroup().appendChild(current.txtElement); }, setLeadingMoveText: function SVGGraphics_setLeadingMoveText(x, y) { @@ -817,16 +807,7 @@ var SVGGraphics = (function SVGGraphicsClosure() { current.xcoords = []; }, - endText: function SVGGraphics_endText() { - if (this.current.pendingClip) { - this.cgrp.appendChild(this.tgrp); - this.pgrp.appendChild(this.cgrp); - } else { - this.pgrp.appendChild(this.tgrp); - } - this.tgrp = document.createElementNS(NS, 'svg:g'); - this.tgrp.setAttributeNS(null, 'transform', pm(this.transformMatrix)); - }, + endText: function SVGGraphics_endText() {}, // Path properties setLineWidth: function SVGGraphics_setLineWidth(width) { @@ -924,30 +905,15 @@ var SVGGraphics = (function SVGGraphicsClosure() { pf(current.dashPhase) + 'px'); current.path.setAttributeNS(null, 'fill', 'none'); - this.tgrp.appendChild(current.path); - if (current.pendingClip) { - this.cgrp.appendChild(this.tgrp); - this.pgrp.appendChild(this.cgrp); - } else { - this.pgrp.appendChild(this.tgrp); - } + this._ensureTransformGroup().appendChild(current.path); + // Saving a reference in current.element so that it can be addressed // in 'fill' and 'stroke' current.element = current.path; current.setCurrentPoint(x, y); }, - endPath: function SVGGraphics_endPath() { - var current = this.current; - if (current.pendingClip) { - this.cgrp.appendChild(this.tgrp); - this.pgrp.appendChild(this.cgrp); - } else { - this.pgrp.appendChild(this.tgrp); - } - this.tgrp = document.createElementNS(NS, 'svg:g'); - this.tgrp.setAttributeNS(null, 'transform', pm(this.transformMatrix)); - }, + endPath: function SVGGraphics_endPath() {}, clip: function SVGGraphics_clip(type) { var current = this.current; @@ -966,12 +932,12 @@ var SVGGraphics = (function SVGGraphicsClosure() { this.clippath.appendChild(clipElement); this.defs.appendChild(this.clippath); - // Create a new group with that attribute + // Create a clipping group that references the clipping path. current.pendingClip = true; this.cgrp = document.createElementNS(NS, 'svg:g'); this.cgrp.setAttributeNS(null, 'clip-path', 'url(#' + current.clipId + ')'); - this.pgrp.appendChild(this.cgrp); + this.svg.appendChild(this.cgrp); }, closePath: function SVGGraphics_closePath() { @@ -1015,20 +981,11 @@ var SVGGraphics = (function SVGGraphicsClosure() { case 'D': this.setDash(value[0], value[1]); break; - case 'RI': - break; - case 'FL': - break; case 'Font': this.setFont(value); break; - case 'CA': - break; - case 'ca': - break; - case 'BM': - break; - case 'SMask': + default: + warn('Unimplemented graphic state ' + key); break; } } @@ -1082,11 +1039,11 @@ var SVGGraphics = (function SVGGraphicsClosure() { rect.setAttributeNS(null, 'width', '1px'); rect.setAttributeNS(null, 'height', '1px'); rect.setAttributeNS(null, 'fill', current.fillColor); - this.tgrp.appendChild(rect); + + this._ensureTransformGroup().appendChild(rect); }, paintJpegXObject: function SVGGraphics_paintJpegXObject(objId, w, h) { - var current = this.current; var imgObj = this.objs.get(objId); var imgEl = document.createElementNS(NS, 'svg:image'); imgEl.setAttributeNS(XLINK_NS, 'xlink:href', imgObj.src); @@ -1097,13 +1054,7 @@ var SVGGraphics = (function SVGGraphicsClosure() { imgEl.setAttributeNS(null, 'transform', 'scale(' + pf(1 / w) + ' ' + pf(-1 / h) + ')'); - this.tgrp.appendChild(imgEl); - if (current.pendingClip) { - this.cgrp.appendChild(this.tgrp); - this.pgrp.appendChild(this.cgrp); - } else { - this.pgrp.appendChild(this.tgrp); - } + this._ensureTransformGroup().appendChild(imgEl); }, paintImageXObject: function SVGGraphics_paintImageXObject(objId) { @@ -1117,7 +1068,6 @@ var SVGGraphics = (function SVGGraphicsClosure() { paintInlineImageXObject: function SVGGraphics_paintInlineImageXObject(imgData, mask) { - var current = this.current; var width = imgData.width; var height = imgData.height; @@ -1127,7 +1077,7 @@ var SVGGraphics = (function SVGGraphicsClosure() { cliprect.setAttributeNS(null, 'y', '0'); cliprect.setAttributeNS(null, 'width', pf(width)); cliprect.setAttributeNS(null, 'height', pf(height)); - current.element = cliprect; + this.current.element = cliprect; this.clip('nonzero'); var imgEl = document.createElementNS(NS, 'svg:image'); imgEl.setAttributeNS(XLINK_NS, 'xlink:href', imgSrc); @@ -1141,13 +1091,7 @@ var SVGGraphics = (function SVGGraphicsClosure() { if (mask) { mask.appendChild(imgEl); } else { - this.tgrp.appendChild(imgEl); - } - if (current.pendingClip) { - this.cgrp.appendChild(this.tgrp); - this.pgrp.appendChild(this.cgrp); - } else { - this.pgrp.appendChild(this.tgrp); + this._ensureTransformGroup().appendChild(imgEl); } }, @@ -1170,15 +1114,14 @@ var SVGGraphics = (function SVGGraphicsClosure() { rect.setAttributeNS(null, 'fill', fillColor); rect.setAttributeNS(null, 'mask', 'url(#' + current.maskId +')'); this.defs.appendChild(mask); - this.tgrp.appendChild(rect); + + this._ensureTransformGroup().appendChild(rect); this.paintInlineImageXObject(imgData, mask); }, paintFormXObjectBegin: function SVGGraphics_paintFormXObjectBegin(matrix, bbox) { - this.save(); - if (isArray(matrix) && matrix.length === 6) { this.transform(matrix[0], matrix[1], matrix[2], matrix[3], matrix[4], matrix[5]); @@ -1200,8 +1143,23 @@ var SVGGraphics = (function SVGGraphicsClosure() { }, paintFormXObjectEnd: - function SVGGraphics_paintFormXObjectEnd() { - this.restore(); + function SVGGraphics_paintFormXObjectEnd() {}, + + /** + * @private + */ + _ensureTransformGroup: function SVGGraphics_ensureTransformGroup() { + if (!this.tgrp) { + this.tgrp = document.createElementNS(NS, 'svg:g'); + this.tgrp.setAttributeNS(null, 'transform', pm(this.transformMatrix)); + + if (this.current.pendingClip) { + this.cgrp.appendChild(this.tgrp); + } else { + this.svg.appendChild(this.tgrp); + } + } + return this.tgrp; } }; return SVGGraphics; From 426fc454de146730afb7d4c57452930027bfe228 Mon Sep 17 00:00:00 2001 From: Tim van der Meij Date: Sat, 15 Oct 2016 21:05:57 +0200 Subject: [PATCH 3/3] SVG: factor out initialization code into a private method Each well-formed SVG image has the following structure: SVG element - Definitions element - Root group - Other group 1 - ... - Other group n This patch factors out initialization code into a private method in such as way that the creation of this structure is clear from the code. The root group is the replacement for the parent group from before. We need this group as we cannot apply the viewport transform on the SVG element itself (this caused issues in Chrome). If other code appends groups to the SVG image, in reality it is appending those groups to the root group, but this detail is abstracted away by this patch. --- src/display/svg.js | 48 +++++++++++++++++++++++++++++++--------------- 1 file changed, 33 insertions(+), 15 deletions(-) diff --git a/src/display/svg.js b/src/display/svg.js index 3fa536cf7..ddb5caef9 100644 --- a/src/display/svg.js +++ b/src/display/svg.js @@ -293,16 +293,6 @@ var SVGExtraState = (function SVGExtraStateClosure() { })(); var SVGGraphics = (function SVGGraphicsClosure() { - function createScratchSVG(width, height) { - var NS = 'http://www.w3.org/2000/svg'; - var svg = document.createElementNS(NS, 'svg:svg'); - svg.setAttributeNS(null, 'version', '1.1'); - svg.setAttributeNS(null, 'width', width + 'px'); - svg.setAttributeNS(null, 'height', height + 'px'); - svg.setAttributeNS(null, 'viewBox', '0 0 ' + width + ' ' + height); - return svg; - } - function opListToTree(opList) { var opTree = []; var tmp = []; @@ -455,17 +445,14 @@ var SVGGraphics = (function SVGGraphicsClosure() { }, getSVG: function SVGGraphics_getSVG(operatorList, viewport) { - this.svg = createScratchSVG(viewport.width, viewport.height); this.viewport = viewport; + var svgElement = this._initialize(viewport); return this.loadDependencies(operatorList).then(function () { this.transformMatrix = IDENTITY_MATRIX; - this.defs = document.createElementNS(NS, 'svg:defs'); - this.svg.setAttributeNS(null, 'transform', pm(viewport.transform)); - this.svg.appendChild(this.defs); var opTree = this.convertOpList(operatorList); this.executeOpTree(opTree); - return this.svg; + return svgElement; }.bind(this)); }, @@ -1145,6 +1132,37 @@ var SVGGraphics = (function SVGGraphicsClosure() { paintFormXObjectEnd: function SVGGraphics_paintFormXObjectEnd() {}, + /** + * @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, 'viewBox', '0 0 ' + viewport.width + + ' ' + viewport.height); + + // Create the definitions element. + var definitions = document.createElementNS(NS, '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'); + rootGroup.setAttributeNS(null, 'transform', pm(viewport.transform)); + svg.appendChild(rootGroup); + + // For the construction of the SVG image we are only interested in the + // root group, so we expose it as the entry point of the SVG image for + // the other code in this class. + this.svg = rootGroup; + + return svg; + }, + /** * @private */