Merge pull request #8691 from timvandermeij/square-circle-annotations

Implement support for square and circle annotations
This commit is contained in:
Tim van der Meij 2017-09-09 22:56:54 +02:00 committed by GitHub
commit 320779e6ed
9 changed files with 267 additions and 55 deletions

View File

@ -81,6 +81,12 @@ class AnnotationFactory {
case 'Line':
return new LineAnnotation(parameters);
case 'Square':
return new SquareAnnotation(parameters);
case 'Circle':
return new CircleAnnotation(parameters);
case 'Highlight':
return new HighlightAnnotation(parameters);
@ -886,6 +892,24 @@ class LineAnnotation extends Annotation {
}
}
class SquareAnnotation extends Annotation {
constructor(parameters) {
super(parameters);
this.data.annotationType = AnnotationType.SQUARE;
this._preparePopup(parameters.dict);
}
}
class CircleAnnotation extends Annotation {
constructor(parameters) {
super(parameters);
this.data.annotationType = AnnotationType.CIRCLE;
this._preparePopup(parameters.dict);
}
}
class HighlightAnnotation extends Annotation {
constructor(parameters) {
super(parameters);

View File

@ -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 {
@ -73,6 +74,12 @@ class AnnotationElementFactory {
case AnnotationType.LINE:
return new LineAnnotationElement(parameters);
case AnnotationType.SQUARE:
return new SquareAnnotationElement(parameters);
case AnnotationType.CIRCLE:
return new CircleAnnotationElement(parameters);
case AnnotationType.HIGHLIGHT:
return new HighlightAnnotationElement(parameters);
@ -105,6 +112,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);
@ -590,7 +598,7 @@ 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'];
const IGNORE_TYPES = ['Line', 'Square', 'Circle'];
this.container.className = 'popupAnnotation';
@ -771,8 +779,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
@ -781,30 +787,122 @@ 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;
}
}
class SquareAnnotationElement extends AnnotationElement {
constructor(parameters) {
let isRenderable = !!(parameters.data.hasPopup ||
parameters.data.title || parameters.data.contents);
super(parameters, isRenderable, /* ignoreBorder = */ true);
}
/**
* Render the square annotation's HTML element in the empty container.
*
* @public
* @memberof SquareAnnotationElement
* @returns {HTMLSectionElement}
*/
render() {
this.container.className = 'squareAnnotation';
// Create an invisible square with the same rectangle that acts as the
// trigger for the popup. Only the square itself should trigger the
// popup, not the entire container.
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);
// The browser draws half of the borders inside the square and half of
// the borders outside the square by default. This behavior cannot be
// changed programmatically, so correct for that here.
let borderWidth = data.borderStyle.width;
let square = this.svgFactory.createElement('svg:rect');
square.setAttribute('x', borderWidth / 2);
square.setAttribute('y', borderWidth / 2);
square.setAttribute('width', width - borderWidth);
square.setAttribute('height', height - borderWidth);
square.setAttribute('stroke-width', borderWidth);
square.setAttribute('stroke', 'transparent');
square.setAttribute('fill', 'none');
svg.appendChild(square);
this.container.append(svg);
// Create the popup ourselves so that we can bind it to the square instead
// of to the entire container (which is the default).
this._createPopup(this.container, square, data);
return this.container;
}
}
class CircleAnnotationElement extends AnnotationElement {
constructor(parameters) {
let isRenderable = !!(parameters.data.hasPopup ||
parameters.data.title || parameters.data.contents);
super(parameters, isRenderable, /* ignoreBorder = */ true);
}
/**
* Render the circle annotation's HTML element in the empty container.
*
* @public
* @memberof CircleAnnotationElement
* @returns {HTMLSectionElement}
*/
render() {
this.container.className = 'circleAnnotation';
// Create an invisible circle with the same ellipse that acts as the
// trigger for the popup. Only the circle itself should trigger the
// popup, not the entire container.
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);
// The browser draws half of the borders inside the circle and half of
// the borders outside the circle by default. This behavior cannot be
// changed programmatically, so correct for that here.
let borderWidth = data.borderStyle.width;
let circle = this.svgFactory.createElement('svg:ellipse');
circle.setAttribute('cx', width / 2);
circle.setAttribute('cy', height / 2);
circle.setAttribute('rx', (width / 2) - (borderWidth / 2));
circle.setAttribute('ry', (height / 2) - (borderWidth / 2));
circle.setAttribute('stroke-width', borderWidth);
circle.setAttribute('stroke', 'transparent');
circle.setAttribute('fill', 'none');
svg.appendChild(circle);
this.container.append(svg);
// Create the popup ourselves so that we can bind it to the circle instead
// of to the entire container (which is the default).
this._createPopup(this.container, circle, data);
return this.container;
}
@ -995,6 +1093,7 @@ class AnnotationLayer {
imageResourcesPath: parameters.imageResourcesPath ||
getDefaultSetting('imageResourcesPath'),
renderInteractiveForms: parameters.renderInteractiveForms || false,
svgFactory: new DOMSVGFactory(),
});
if (element.isRenderable) {
parameters.div.appendChild(element.render());

View File

@ -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,
};

View File

@ -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);

View File

@ -284,6 +284,7 @@
!annotation-squiggly.pdf
!annotation-highlight.pdf
!annotation-line.pdf
!annotation-square-circle.pdf
!annotation-fileattachment.pdf
!annotation-text-widget.pdf
!annotation-choice-widget.pdf

Binary file not shown.

View File

@ -3560,6 +3560,13 @@
"type": "eq",
"annotations": true
},
{ "id": "annotation-square-circle",
"file": "pdfs/annotation-square-circle.pdf",
"md5": "cfd3c302f68d61e1d55ed9c7896046c3",
"rounds": 1,
"type": "eq",
"annotations": true
},
{ "id": "annotation-fileattachment",
"file": "pdfs/annotation-fileattachment.pdf",
"md5": "d20ecee4b53c81b2dd44c8715a1b4a83",

View File

@ -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';

View File

@ -181,6 +181,8 @@
.annotationLayer .squigglyAnnotation,
.annotationLayer .strikeoutAnnotation,
.annotationLayer .lineAnnotation svg line,
.annotationLayer .squareAnnotation svg rect,
.annotationLayer .circleAnnotation svg ellipse,
.annotationLayer .fileAttachmentAnnotation {
cursor: pointer;
}