From 360cb4652e73458588234217f4c756f304e71f0b Mon Sep 17 00:00:00 2001 From: Tim van der Meij Date: Wed, 20 Aug 2014 14:06:43 +0200 Subject: [PATCH 1/8] Implements classes Annotation, AnnotationBorderStyle, AnnotationLayer, AnnotationHandler and LinkAnnotation --- src/core/annotation.js | 1187 +++++++++++++++++++++++++--------------- 1 file changed, 756 insertions(+), 431 deletions(-) diff --git a/src/core/annotation.js b/src/core/annotation.js index 3bf8a8249cecb..2dd249292846e 100644 --- a/src/core/annotation.js +++ b/src/core/annotation.js @@ -1,6 +1,6 @@ /* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ -/* Copyright 2012 Mozilla Foundation +/* Copyright 2014 Mozilla Foundation * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,521 +14,846 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -/* globals PDFJS, Util, isDict, isName, stringToPDFString, warn, Dict, Stream, - stringToBytes, assert, Promise, isArray, ObjectLoader, OperatorList, - isValidUrl, OPS, createPromiseCapability, AnnotationType */ +/* globals AnnotationType, AnnotationFlag, Util, Promise, OperatorList, + AnnotationBorderStyleType, isDict, warn, isArray, DeviceGrayCS, + DeviceRgbCS, DeviceCmykCS, isName, createPromiseCapability, OPS */ 'use strict'; -var DEFAULT_ICON_SIZE = 22; // px -var SUPPORTED_TYPES = ['Link', 'Text', 'Widget']; +var DEFAULT_RECTANGLE = [0, 0, 0, 0]; +var DEFAULT_DASH_ARRAY = [3]; +/** + * Contains basic data for each annotation type. Each annotation + * type extends this class to obtain basic functionality. + * + * @class + */ var Annotation = (function AnnotationClosure() { - // 12.5.5: Algorithm: Appearance streams - function getTransformMatrix(rect, bbox, matrix) { - var bounds = Util.getAxialAlignedBoundingBox(bbox, matrix); - var minX = bounds[0]; - var minY = bounds[1]; - var maxX = bounds[2]; - var maxY = bounds[3]; - - if (minX === maxX || minY === maxY) { - // From real-life file, bbox was [0, 0, 0, 0]. In this case, - // just apply the transform for rect - return [1, 0, 0, 1, rect[0], rect[1]]; - } - - var xRatio = (rect[2] - rect[0]) / (maxX - minX); - var yRatio = (rect[3] - rect[1]) / (maxY - minY); - return [ - xRatio, - 0, - 0, - yRatio, - rect[0] - minX * xRatio, - rect[1] - minY * yRatio - ]; + /* + * @constructor + * @private + */ + function Annotation() { + this.id = null; + this.type = null; + this.contents = null; + this.name = null; + this.date = null; + this.rectangle = null; + this.borderStyle = new AnnotationBorderStyle(); + this.color = null; + this.flags = 0; } - function getDefaultAppearance(dict) { - var appearanceState = dict.get('AP'); - if (!isDict(appearanceState)) { - return; - } - - var appearance; - var appearances = appearanceState.get('N'); - if (isDict(appearances)) { - var as = dict.get('AS'); - if (as && appearances.has(as.name)) { - appearance = appearances.get(as.name); + Annotation.prototype = { + /** + * Set the ID. + * + * @public + * @memberof Annotation + * @param id {integer} The ID + */ + setId: function Annotation_setId(id) { + if (id === (id | 0)) { + this.id = id; } - } else { - appearance = appearances; - } - return appearance; - } + }, - function Annotation(params) { - var dict = params.dict; - var data = this.data = {}; - - data.subtype = dict.get('Subtype').name; - var rect = dict.get('Rect') || [0, 0, 0, 0]; - data.rect = Util.normalizeRect(rect); - data.annotationFlags = dict.get('F'); - - var color = dict.get('C'); - if (isArray(color) && color.length === 3) { - // TODO(mack): currently only supporting rgb; need support different - // colorspaces - data.color = color; - } else { - data.color = [0, 0, 0]; - } + /** + * Get the ID. + * + * @public + * @memberof Annotation + * @default null + * @return {integer|null} The ID, either a valid integer or null if + * no (valid) ID has been set. + */ + getId: function Annotation_getId() { + return this.id; + }, - // Some types of annotations have border style dict which has more - // info than the border array - if (dict.has('BS')) { - var borderStyle = dict.get('BS'); - data.borderWidth = borderStyle.has('W') ? borderStyle.get('W') : 1; - } else { - var borderArray = dict.get('Border') || [0, 0, 1]; - data.borderWidth = borderArray[2] || 0; - - // TODO: implement proper support for annotations with line dash patterns. - var dashArray = borderArray[3]; - if (data.borderWidth > 0 && dashArray) { - if (!isArray(dashArray)) { - // Ignore the border if dashArray is not actually an array, - // this is consistent with the behaviour in Adobe Reader. - data.borderWidth = 0; - } else { - var dashArrayLength = dashArray.length; - if (dashArrayLength > 0) { - // According to the PDF specification: the elements in a dashArray - // shall be numbers that are nonnegative and not all equal to zero. - var isInvalid = false; - var numPositive = 0; - for (var i = 0; i < dashArrayLength; i++) { - var validNumber = (+dashArray[i] >= 0); - if (!validNumber) { - isInvalid = true; - break; - } else if (dashArray[i] > 0) { - numPositive++; - } - } - if (isInvalid || numPositive === 0) { - data.borderWidth = 0; - } - } + /** + * Set the type. + * + * @public + * @memberof Annotation + * @param type {integer} The type + * @see {@link shared/util.js} + */ + setType: function Annotation_setType(type) { + if (type === (type | 0)) { + if (type >= 1 && type <= Object.keys(AnnotationType).length) { + this.type = type; } } - } + }, - this.appearance = getDefaultAppearance(dict); - data.hasAppearance = !!this.appearance; - data.id = params.ref.num; - } + /** + * Get the type. + * + * @public + * @memberof Annotation + * @default null + * @return {integer|null} The type, either a valid integer or null if + * no (valid) type has been set. + * @see {@link shared/util.js} + */ + getType: function Annotation_getType() { + return this.type; + }, - Annotation.prototype = { + /** + * Set the contents. + * + * @public + * @memberof Annotation + * @param contents {string} The contents + */ + setContents: function Annotation_setContents(contents) { + this.contents = contents || null; + }, - getData: function Annotation_getData() { - return this.data; + /** + * Get the contents. + * + * @public + * @memberof Annotation + * @default null + * @return {string|null} The contents, either a valid string or null if + * no (valid) contents have been set. + */ + getContents: function Annotation_getContents() { + return this.contents; }, - isInvisible: function Annotation_isInvisible() { - var data = this.data; - if (data && SUPPORTED_TYPES.indexOf(data.subtype) !== -1) { - return false; - } else { - return !!(data && - data.annotationFlags && // Default: not invisible - data.annotationFlags & 0x1); // Invisible - } + /** + * Set the name. + * + * @public + * @memberof Annotation + * @param name {string} The name + */ + setName: function Annotation_setName(name) { + this.name = name || null; }, - isViewable: function Annotation_isViewable() { - var data = this.data; - return !!(!this.isInvisible() && - data && - (!data.annotationFlags || - !(data.annotationFlags & 0x22)) && // Hidden or NoView - data.rect); // rectangle is necessary + /** + * Get the name. + * + * @public + * @memberof Annotation + * @default null + * @return {string|null} The name, either a valid string or null if + * no (valid) name has been set. + */ + getName: function Annotation_getName() { + return this.name; }, - isPrintable: function Annotation_isPrintable() { - var data = this.data; - return !!(!this.isInvisible() && - data && - data.annotationFlags && // Default: not printable - data.annotationFlags & 0x4 && // Print - !(data.annotationFlags & 0x2) && // Hidden - data.rect); // rectangle is necessary - }, - - loadResources: function Annotation_loadResources(keys) { - return new Promise(function (resolve, reject) { - this.appearance.dict.getAsync('Resources').then(function (resources) { - if (!resources) { - resolve(); - return; - } - var objectLoader = new ObjectLoader(resources.map, - keys, - resources.xref); - objectLoader.load().then(function() { - resolve(resources); - }, reject); - }, reject); - }.bind(this)); + /** + * Set the (modification) date. Note that this is a PDF date string + * that needs conversion before being used. + * + * @public + * @memberof Annotation + * @param date {string} The (modification) date + */ + setDate: function Annotation_setDate(date) { + this.date = date || null; }, - getOperatorList: function Annotation_getOperatorList(evaluator) { + /** + * Get the (modification) date. + * + * @public + * @memberof Annotation + * @default null + * @return {string|null} The (modification) date, either a valid PDF date + * string or null if no (valid) date has been set. + */ + getDate: function Annotation_getDate() { + return this.date; + }, - if (!this.appearance) { - return Promise.resolve(new OperatorList()); + /** + * Set the display rectangle. + * + * @public + * @memberof Annotation + * @param rectangle {Array} The display rectangle + */ + setRectangle: function Annotation_setRectangle(rectangle) { + if (isArray(rectangle) && rectangle.length === 4) { + this.rectangle = Util.normalizeRect(rectangle); } + }, - var data = this.data; - - var appearanceDict = this.appearance.dict; - var resourcesPromise = this.loadResources([ - 'ExtGState', - 'ColorSpace', - 'Pattern', - 'Shading', - 'XObject', - 'Font' - // ProcSet - // Properties - ]); - var bbox = appearanceDict.get('BBox') || [0, 0, 1, 1]; - var matrix = appearanceDict.get('Matrix') || [1, 0, 0, 1, 0 ,0]; - var transform = getTransformMatrix(data.rect, bbox, matrix); - var self = this; - - return resourcesPromise.then(function(resources) { - var opList = new OperatorList(); - opList.addOp(OPS.beginAnnotation, [data.rect, transform, matrix]); - return evaluator.getOperatorList(self.appearance, resources, opList). - then(function () { - opList.addOp(OPS.endAnnotation, []); - self.appearance.reset(); - return opList; - }); - }); - } - }; + /** + * Get the display rectangle. + * + * @public + * @memberof Annotation + * @default [0, 0, 0, 0] + * @return {Array} The display rectangle + */ + getRectangle: function Annotation_getRectangle() { + return this.rectangle || DEFAULT_RECTANGLE; + }, - Annotation.getConstructor = - function Annotation_getConstructor(subtype, fieldType) { + /** + * Set the border style (as AnnotationBorderStyle object). + * + * @public + * @memberof Annotation + * @param borderStyle {Dict} The border style dictionary + */ + setBorderStyle: function Annotation_setBorderStyle(borderStyle) { + if (isDict(borderStyle)) { + if (borderStyle.has('BS')) { + var dict = borderStyle.get('BS'); + + if (!dict.has('Type') || (isName(dict.get('Type')) && + dict.get('Type').name === 'Border')) { + this.borderStyle.setWidth(dict.get('W')); + this.borderStyle.setStyle(dict.get('S').name); + this.borderStyle.setDashArray(dict.get('D')); + } + } else if (borderStyle.has('Border')) { + var array = borderStyle.get('Border'); + if (isArray(array) && array.length >= 3) { + this.borderStyle.setHorizontalCornerRadius(array[0]); + this.borderStyle.setVerticalCornerRadius(array[1]); + this.borderStyle.setWidth(array[2]); + this.borderStyle.setStyle('S'); + + if (array.length === 4) { // Dash array available + this.borderStyle.setDashArray(array[3]); + } + } + } + } + }, - if (!subtype) { - return; - } + /** + * Get the border style (as AnnotationBorderStyle object). + * + * @public + * @memberof Annotation + * @default defaults of AnnotationBorderStyle + * @return {AnnotationBorderStyle} The border style object + */ + getBorderStyle: function Annotation_getBorderStyle() { + return this.borderStyle; + }, - // TODO(mack): Implement FreeText annotations - if (subtype === 'Link') { - return LinkAnnotation; - } else if (subtype === 'Text') { - return TextAnnotation; - } else if (subtype === 'Widget') { - if (!fieldType) { - return; - } + /** + * Set the color and take care of color space conversion. + * + * @public + * @memberof Annotation + * @param color {Array} The color of a particular color space + */ + setColor: function Annotation_setColorSpace(color) { + if (isArray(color)) { + var rgbColor = new Uint8Array(3); + + switch (color.length) { + case 0: // Transparent border, so we do not need to draw it at all + this.borderStyle.setWidth(0); + break; - if (fieldType === 'Tx') { - return TextWidgetAnnotation; - } else { - return WidgetAnnotation; - } - } else { - return Annotation; - } - }; + case 1: // Convert gray to RGB + var grayCS = new DeviceGrayCS(); + grayCS.getRgbItem(color, 0, rgbColor, 0); + this.color = rgbColor; + break; - Annotation.fromRef = function Annotation_fromRef(xref, ref) { + case 3: // Convert RGB percentages to RGB + var rgbCS = new DeviceRgbCS(); + rgbCS.getRgbItem(color, 0, rgbColor, 0); + this.color = rgbColor; + break; - var dict = xref.fetchIfRef(ref); - if (!isDict(dict)) { - return; - } + case 4: // Convert CMYK to RGB + var cmykCS = new DeviceCmykCS(); + cmykCS.getRgbItem(color, 0, rgbColor, 0); + this.color = rgbColor; + break; - var subtype = dict.get('Subtype'); - subtype = isName(subtype) ? subtype.name : ''; - if (!subtype) { - return; - } + default: + break; + } + } + }, - var fieldType = Util.getInheritableProperty(dict, 'FT'); - fieldType = isName(fieldType) ? fieldType.name : ''; + /** + * Get the color in RGB color space. + * + * @public + * @memberof Annotation + * @default null + * @return {Array} The color in RGB color space + */ + getColor: function Annotation_getColor() { + return this.color; + }, - var Constructor = Annotation.getConstructor(subtype, fieldType); - if (!Constructor) { - return; - } + /** + * Set the flags. + * + * @public + * @memberof Annotation + * @param flags {integer} The flags + * @see {@link shared/util.js} + */ + setFlags: function Annotation_setFlag(flags) { + if (flags === (flags | 0)) { + this.flags = flags; + } + }, - var params = { - dict: dict, - ref: ref, - }; + /** + * Check if a provided flag is set. + * + * @public + * @memberof Annotation + * @default false + * @return {boolean} Whether or not the provided flag is set + */ + hasFlag: function Annotation_hasFlag(bitMask) { + if (this.flags) { + return (this.flags & bitMask) > 0; + } + return false; + }, - var annotation = new Constructor(params); + /** + * Check if the annotation is viewable. + * + * @public + * @memberof Annotation + * @default true + * @return {boolean} Whether or not the annotation is viewable + */ + isViewable: function Annotation_isViewable() { + if (this.flags) { + return !this.hasFlag(AnnotationFlag.INVISIBLE) && + !this.hasFlag(AnnotationFlag.HIDDEN) && + !this.hasFlag(AnnotationFlag.NOVIEW); + } + return true; + }, - if (annotation.isViewable() || annotation.isPrintable()) { - return annotation; - } else { - if (SUPPORTED_TYPES.indexOf(subtype) === -1) { - warn('unimplemented annotation type: ' + subtype); + /** + * Check if the annotation is printable. + * + * @public + * @memberof Annotation + * @default false + * @return {boolean} Whether or not the annotation is printable + */ + isPrintable: function Annotation_isPrintable() { + if (this.flags) { + return this.hasFlag(AnnotationFlag.PRINT); } + return false; } }; - Annotation.appendToOperatorList = function Annotation_appendToOperatorList( - annotations, opList, pdfManager, partialEvaluator, intent) { - - function reject(e) { - annotationsReadyCapability.reject(e); - } + return Annotation; +})(); - var annotationsReadyCapability = createPromiseCapability(); +/** + * Contains all data regarding an annotation's border style. + * + * @class + */ +var AnnotationBorderStyle = (function AnnotationBorderStyleClosure() { + /** + * @constructor + * @private + */ + function AnnotationBorderStyle() { + this.width = 1; + this.style = AnnotationBorderStyleType.SOLID; + this.dashArray = null; + this.horizontalCornerRadius = 0; + this.verticalCornerRadius = 0; + } - var annotationPromises = []; - for (var i = 0, n = annotations.length; i < n; ++i) { - if (intent === 'display' && annotations[i].isViewable() || - intent === 'print' && annotations[i].isPrintable()) { - annotationPromises.push( - annotations[i].getOperatorList(partialEvaluator)); + AnnotationBorderStyle.prototype = { + /** + * Set the width. + * + * @public + * @memberof AnnotationBorderStyle + * @param width {integer} The width + */ + setWidth: function Annotation_setWidth(width) { + if (width === (width | 0)) { + this.width = width; } - } - Promise.all(annotationPromises).then(function(datas) { - opList.addOp(OPS.beginAnnotations, []); - for (var i = 0, n = datas.length; i < n; ++i) { - var annotOpList = datas[i]; - opList.addOpList(annotOpList); - } - opList.addOp(OPS.endAnnotations, []); - annotationsReadyCapability.resolve(); - }, reject); + }, - return annotationsReadyCapability.promise; - }; + /** + * Get the width. + * + * @public + * @memberof AnnotationBorderStyle + * @default 1 + * @return {integer} The width + */ + getWidth: function Annotation_getWidth() { + return this.width; + }, - return Annotation; -})(); + /** + * Set the style. + * + * @public + * @memberof AnnotationBorderStyle + * @param style {string} The style + * @see {@link shared/util.js} + */ + setStyle: function Annotation_setStyle(style) { + switch (style) { + case 'S': + this.style = AnnotationBorderStyleType.SOLID; + break; + + case 'D': + this.style = AnnotationBorderStyleType.DASHED; + break; + + case 'B': + this.style = AnnotationBorderStyleType.BEVELED; + break; + + case 'I': + this.style = AnnotationBorderStyleType.INSET; + break; + + case 'U': + this.style = AnnotationBorderStyleType.UNDERLINE; + break; + + default: + break; + } + }, + + /** + * Get the style. + * + * @public + * @memberof AnnotationBorderStyle + * @default AnnotationBorderStyleType.SOLID + * @return {integer} The style + * @see {@link shared/util.js} + */ + getStyle: function Annotation_getStyle() { + return this.style; + }, -var WidgetAnnotation = (function WidgetAnnotationClosure() { - - function WidgetAnnotation(params) { - Annotation.call(this, params); - - var dict = params.dict; - var data = this.data; - - data.fieldValue = stringToPDFString( - Util.getInheritableProperty(dict, 'V') || ''); - data.alternativeText = stringToPDFString(dict.get('TU') || ''); - data.defaultAppearance = Util.getInheritableProperty(dict, 'DA') || ''; - var fieldType = Util.getInheritableProperty(dict, 'FT'); - data.fieldType = isName(fieldType) ? fieldType.name : ''; - data.fieldFlags = Util.getInheritableProperty(dict, 'Ff') || 0; - this.fieldResources = Util.getInheritableProperty(dict, 'DR') || Dict.empty; - - // Building the full field name by collecting the field and - // its ancestors 'T' data and joining them using '.'. - var fieldName = []; - var namedItem = dict; - var ref = params.ref; - while (namedItem) { - var parent = namedItem.get('Parent'); - var parentRef = namedItem.getRaw('Parent'); - var name = namedItem.get('T'); - if (name) { - fieldName.unshift(stringToPDFString(name)); - } else { - // The field name is absent, that means more than one field - // with the same name may exist. Replacing the empty name - // with the '`' plus index in the parent's 'Kids' array. - // This is not in the PDF spec but necessary to id the - // the input controls. - var kids = parent.get('Kids'); - var j, jj; - for (j = 0, jj = kids.length; j < jj; j++) { - var kidRef = kids[j]; - if (kidRef.num === ref.num && kidRef.gen === ref.gen) { + /** + * Set the dash array. + * + * @public + * @memberof AnnotationBorderStyle + * @param dashArray {Array} The dash array with at least one element + */ + setDashArray: function Annotation_setDashArray(dashArray) { + if (isArray(dashArray) && dashArray.length > 0) { + // According to the PDF specification: the elements in a dashArray + // shall be numbers that are nonnegative and not all equal to zero. + var isInvalid = false; + var numPositive = 0; + for (var i = 0, len = dashArray.length; i < len; i++) { + var validNumber = (+dashArray[i] >= 0); + if (!validNumber) { + isInvalid = true; break; + } else if (dashArray[i] > 0) { + numPositive++; } } - fieldName.unshift('`' + j); + if (!isInvalid && numPositive > 0) { + this.dashArray = dashArray; + } else { + this.width = 0; // Adobe behavior when the array is invalid. + } + } else if(dashArray) { + this.width = 0; // Adobe behavior when the array is invalid. } - namedItem = parent; - ref = parentRef; - } - data.fullName = fieldName.join('.'); - } + }, + + /** + * Get the dash array. + * + * @public + * @memberof AnnotationBorderStyle + * @default [3] + * @return {Array} The dash array + */ + getDashArray: function Annotation_getDashArray() { + return this.dashArray || DEFAULT_DASH_ARRAY; + }, + + /** + * Set the horizontal corner radius (from a Border dictionary). + * + * @public + * @memberof AnnotationBorderStyle + * @param radius {integer} The horizontal corner radius + */ + setHorizontalCornerRadius: + function Annotation_setHorizontalCornerRadius(radius) { + if (radius === (radius | 0)) { + this.horizontalCornerRadius = radius; + } + }, + + /** + * Get the horizontal corner radius. + * + * @public + * @memberof AnnotationBorderStyle + * @default 0 + * @return {integer} The horizontal corner radius + */ + getHorizontalCornerRadius: + function Annotation_getHorizontalCornerRadius() { + return this.horizontalCornerRadius; + }, - var parent = Annotation.prototype; - Util.inherit(WidgetAnnotation, Annotation, { - isViewable: function WidgetAnnotation_isViewable() { - if (this.data.fieldType === 'Sig') { - warn('unimplemented annotation type: Widget signature'); - return false; + /** + * Set the vertical corner radius (from a Border dictionary). + * + * @public + * @memberof AnnotationBorderStyle + * @param radius {integer} The vertical corner radius + */ + setVerticalCornerRadius: + function Annotation_setVerticalCornerRadius(radius) { + if (radius === (radius | 0)) { + this.verticalCornerRadius = radius; } + }, - return parent.isViewable.call(this); + /** + * Get the vertical corner radius. + * + * @public + * @memberof AnnotationBorderStyle + * @default 0 + * @return {integer} The vertical corner radius + */ + getVerticalCornerRadius: + function Annotation_getVerticalCornerRadius() { + return this.verticalCornerRadius; } - }); + }; - return WidgetAnnotation; + return AnnotationBorderStyle; })(); -var TextWidgetAnnotation = (function TextWidgetAnnotationClosure() { - function TextWidgetAnnotation(params) { - WidgetAnnotation.call(this, params); - - this.data.textAlignment = Util.getInheritableProperty(params.dict, 'Q'); - this.data.annotationType = AnnotationType.WIDGET; - this.data.hasHtml = !this.data.hasAppearance && !!this.data.fieldValue; +/** + * Provides an interface to annotations (used by + * src/core/core.js). + * + * @class + */ +var AnnotationLayer = (function AnnotationLayerClosure() { + /** + * @constructor + * @private + */ + function AnnotationLayer() { + this.annotationHandler = new AnnotationHandler(); } - Util.inherit(TextWidgetAnnotation, WidgetAnnotation, { - getOperatorList: function TextWidgetAnnotation_getOperatorList(evaluator) { - if (this.appearance) { - return Annotation.prototype.getOperatorList.call(this, evaluator); + AnnotationLayer.prototype = { + /** + * Creates a single annotation of a requested type. + * + * @public + * @memberof AnnotationLayer + * @param params {Object} Parameters (such as ID) for creating + * the annotation. + * @return {Annotation|null} Either an annotation object of a supported + * type or null if the requested type is not + * supported or the annotation is not viewable + * or printable. + */ + getAnnotation: function AnnotationLayer_getAnnotation(dict, ref) { + // Make sure we are dealing with a valid annotation dictionary. + if (!isDict(dict) || (isDict(dict) && isName(dict.get('Type')) && + dict.get('Type').name !== 'Annot')) { + warn('Invalid annotation dictionary'); + return; } - var opList = new OperatorList(); - var data = this.data; - - // Even if there is an appearance stream, ignore it. This is the - // behaviour used by Adobe Reader. - if (!data.defaultAppearance) { - return Promise.resolve(opList); + var subType = dict.get('Subtype'); + if (!isName(subType)) { + return; + } + subType = subType.name; + + // Create and configure the annotation object. + var annotation = null; + var params = { + dict: dict, + ref: ref + }; + + switch (subType) { + case 'Link': + annotation = this.annotationHandler.createLinkAnnotation(params); + break; + + default: + warn('Unimplemented annotation type: ' + subType); + return null; } - var stream = new Stream(stringToBytes(data.defaultAppearance)); - return evaluator.getOperatorList(stream, this.fieldResources, opList). - then(function () { - return opList; - }); - } - }); + if (annotation.isViewable() || annotation.isPrintable()) { + return annotation; + } + return null; + }, - return TextWidgetAnnotation; -})(); + /** + * Appends annotations to the operator list. + * + * @public + * @memberof AnnotationLayer + * @param annotations {Array} All available annotations + * @param opList {OperatorList} The current operator list + * @param partialEvaluator {PartialEvaluator} The partial evaluator + * @param intent {string} The rendering intent (display or print) + * @return {Promise} Promise indicating that the annotations are ready. + */ + appendToOperatorList: + function AnnotationLayer_appendToOperatorList(annotations, opList, + partialEvaluator, + intent) { + function reject(e) { + annotationsReadyCapability.reject(e); + } -var InteractiveAnnotation = (function InteractiveAnnotationClosure() { - function InteractiveAnnotation(params) { - Annotation.call(this, params); + var annotationsReadyCapability = createPromiseCapability(); - this.data.hasHtml = true; - } + var annotationPromises = []; + for (var i = 0, n = annotations.length; i < n; ++i) { + if (intent === 'display' && annotations[i].isViewable() || + intent === 'print' && annotations[i].isPrintable()) { + annotationPromises.push(new OperatorList()); + } + } + Promise.all(annotationPromises).then(function(data) { + opList.addOp(OPS.beginAnnotations, []); + for (var i = 0, n = data.length; i < n; ++i) { + var annotOpList = data[i]; + opList.addOpList(annotOpList); + } + opList.addOp(OPS.endAnnotations, []); + annotationsReadyCapability.resolve(); + }, reject); - Util.inherit(InteractiveAnnotation, Annotation, { }); + return annotationsReadyCapability.promise; + } + }; - return InteractiveAnnotation; + return AnnotationLayer; })(); -var TextAnnotation = (function TextAnnotationClosure() { - function TextAnnotation(params) { - InteractiveAnnotation.call(this, params); - - var dict = params.dict; - var data = this.data; - - var content = dict.get('Contents'); - var title = dict.get('T'); - data.annotationType = AnnotationType.TEXT; - data.content = stringToPDFString(content || ''); - data.title = stringToPDFString(title || ''); - - if (data.hasAppearance) { - data.name = 'NoIcon'; - } else { - data.rect[1] = data.rect[3] - DEFAULT_ICON_SIZE; - data.rect[2] = data.rect[0] + DEFAULT_ICON_SIZE; - data.name = dict.has('Name') ? dict.get('Name').name : 'Note'; - } +/** + * Handles creation and configuration of annotation + * objects of all types. + * + * @class + */ +var AnnotationHandler = (function AnnotationHandlerClosure() { + /** + * @constructor + * @private + */ + function AnnotationHandler() {} + + AnnotationHandler.prototype = { + /** + * Creates and configures a link annotation. + * + * @public + * @memberof AnnotationHandler + * @param params {Object} Parameters (such as ID) for creating + * the link annotation. + * @return {LinkAnnotation} Configured link annotation object. + */ + createLinkAnnotation: + function AnnotationLayer_createLinkAnnotation(params) { + var dict = params.dict; + var annotation = new LinkAnnotation(); + var action = null; + + annotation.setId(params.ref.num); + annotation.setType(AnnotationType.LINK); + annotation.setContents(dict.get('Contents')); + annotation.setName(dict.get('NM')); + annotation.setDate(dict.get('M')); + annotation.setRectangle(dict.get('Rect')); + annotation.setBorderStyle(dict); + annotation.setColor(dict.get('C')); + annotation.setFlags(dict.get('F')); + if (dict.has('A')) { + annotation.setAction(dict.get('A')); + } else if (dict.has('Dest')) { + annotation.setDestination(dict.get('Dest')); + } - if (dict.has('C')) { - data.hasBgColor = true; + return annotation; } - } - - Util.inherit(TextAnnotation, InteractiveAnnotation, { }); + }; - return TextAnnotation; + return AnnotationHandler; })(); +/** + * Contains all data specific to Link annotations as + * described in section 8.4.5 of the PDF specification. + * + * @class + * @extends Annotation + */ var LinkAnnotation = (function LinkAnnotationClosure() { - function LinkAnnotation(params) { - InteractiveAnnotation.call(this, params); - - var dict = params.dict; - var data = this.data; - data.annotationType = AnnotationType.LINK; - - var action = dict.get('A'); - if (action) { - var linkType = action.get('S').name; - if (linkType === 'URI') { - var url = action.get('URI'); - if (isName(url)) { - // Some bad PDFs do not put parentheses around relative URLs. - url = '/' + url.name; - } else if (url) { - url = addDefaultProtocolToUrl(url); - } - // TODO: pdf spec mentions urls can be relative to a Base - // entry in the dictionary. - if (!isValidUrl(url, false)) { - url = ''; - } - data.url = url; - } else if (linkType === 'GoTo') { - data.dest = action.get('D'); - } else if (linkType === 'GoToR') { - var urlDict = action.get('F'); - if (isDict(urlDict)) { - // We assume that the 'url' is a Filspec dictionary - // and fetch the url without checking any further - url = urlDict.get('F') || ''; + /** + * @constructor + * @private + */ + function LinkAnnotation() { + Annotation.call(this); + + this.action = null; + this.destination = null; + } + + LinkAnnotation.prototype = { + /** + * Set the action. + * + * @public + * @memberof LinkAnnotation + * @param action {Dict} Object containing details for + * the action to be set + */ + setAction: function LinkAnnotation_setAction(action) { + if (isDict(action)) { + if (isName(action.get('Type')) && + action.get('Type').name !== 'Action') { + return; } - // TODO: pdf reference says that GoToR - // can also have 'NewWindow' attribute - if (!isValidUrl(url, false)) { - url = ''; + var linkType = action.get('S').name; + var destination = action.get('D'); + + switch (linkType) { + case 'URI': + var url = action.get('URI') || ''; + + if (isName(url)) { + // Some bad PDFs do not put parentheses around relative URLs. + url = '/' + url.name; + } + + // Add default protocol to URL. + if (url && url.indexOf('www.') === 0) { + url = 'http://' + url; + } + + this.action = { + type: 'URI', + url: url + }; + break; + + case 'GoTo': + this.action = { + type: 'GoTo', + destination: (isName(destination) ? destination.name : + (!isArray(destination) ? destination : '')) + }; + break; + + case 'GoToR': + this.action = { + type: 'GoToR', + destination: (isName(destination) ? destination.name : + (!isArray(destination) ? destination : '')), + url: action.get('F'), + newWindow: (action.get('NewWindow') ? true : false) + }; + break; + + case 'Named': + this.action = { + type: 'Named', + action: action.get('N').name + }; + break; + + default: + warn('Unrecognized link type: ' + linkType); + break; } - data.url = url; - data.dest = action.get('D'); - } else if (linkType === 'Named') { - data.action = action.get('N').name; - } else { - warn('unrecognized link type: ' + linkType); } - } else if (dict.has('Dest')) { - // simple destination link - var dest = dict.get('Dest'); - data.dest = isName(dest) ? dest.name : dest; - } - } + }, - // Lets URLs beginning with 'www.' default to using the 'http://' protocol. - function addDefaultProtocolToUrl(url) { - if (url && url.indexOf('www.') === 0) { - return ('http://' + url); - } - return url; - } + /** + * Get the action. + * + * @public + * @memberof LinkAnnotation + * @default null + * @return {Object|null} The annotation's action details, either an object + * (that can contain type, url, destination, + * newWindow or action fields depending on the + * link type) or null if no (valid) action has been + * set. + */ + getAction: function Annotation_getAction() { + return this.action; + }, - Util.inherit(LinkAnnotation, InteractiveAnnotation, { - hasOperatorList: function LinkAnnotation_hasOperatorList() { - return false; + /** + * Set the destination. + * + * @public + * @memberof LinkAnnotation + * @param destination {Name|string} The annotation's destination + */ + setDestination: function LinkAnnotation_setDestination(destination) { + if (destination) { + this.destination = (isName(destination) ? destination.name : + (!isArray(destination) ? destination : '')); + } + }, + + /** + * Get the destination. + * + * @public + * @memberof LinkAnnotation + * @default null + * @return {string|null} The annotation's destination, either a string or + * null if no (valid) destination has been set. + */ + getDestination: function Annotation_getDestination() { + return this.destination; } - }); + }; + + Util.inherit(LinkAnnotation, Annotation, LinkAnnotation.prototype); return LinkAnnotation; })(); From a66adaf7ce641f6d03a4e17b61b94b3e269abfd7 Mon Sep 17 00:00:00 2001 From: Tim van der Meij Date: Wed, 20 Aug 2014 14:18:16 +0200 Subject: [PATCH 2/8] Creates unit tests for the annotation layer --- test/unit/annotation_layer_spec.js | 198 +++++++++++++++++++++++++++++ test/unit/unit_test.html | 1 + 2 files changed, 199 insertions(+) create mode 100644 test/unit/annotation_layer_spec.js diff --git a/test/unit/annotation_layer_spec.js b/test/unit/annotation_layer_spec.js new file mode 100644 index 0000000000000..0c3cf8e461951 --- /dev/null +++ b/test/unit/annotation_layer_spec.js @@ -0,0 +1,198 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ +/* globals expect, it, describe, isDict, AnnotationBorderStyleType, + AnnotationFlag, AnnotationType, Annotation, LinkAnnotation, + Dict, Name */ + +'use strict'; + +describe('Annotation layer', function() { + describe('Annotation and AnnotationBorderStyle', function() { + it('should set and get its ID', function() { + var annotation = new Annotation(); + annotation.setId(123); + + expect(annotation.getId()).toEqual(123); + }); + + it('should not set an invalid ID', function() { + var annotation = new Annotation(); + annotation.setId('id'); + + expect(annotation.getId()).toEqual(null); + }); + + it('should set and get its type', function() { + var annotation = new Annotation(); + annotation.setType(2); + + expect(annotation.getType()).toEqual(AnnotationType.LINK); + }); + + it('should not set an invalid type (1)', function() { + var annotation = new Annotation(); + annotation.setType(89); + + expect(annotation.getType()).toEqual(null); + }); + + it('should not set an invalid type (2)', function() { + var annotation = new Annotation(); + annotation.setType('link'); + + expect(annotation.getType()).toEqual(null); + }); + + it('should set and get its contents', function() { + var annotation = new Annotation(); + annotation.setContents('Annotation contents'); + + expect(annotation.getContents()).toEqual('Annotation contents'); + }); + + it('should set and get its date', function() { + var annotation = new Annotation(); + annotation.setDate('D:20100325090038-04\'00\''); + + expect(annotation.getDate()).toEqual('D:20100325090038-04\'00\''); + }); + + it('should set and get its rectangle', function() { + var annotation = new Annotation(); + annotation.setRectangle([117, 694, 164.298, 720]); + + expect(annotation.getRectangle()).toEqual([117, 694, 164.298, 720]); + }); + + it('should not set an invalid rectangle', function() { + var annotation = new Annotation(); + annotation.setRectangle([117, 694, 164.298]); + + expect(annotation.getRectangle()).toEqual([0, 0, 0, 0]); + }); + + it('should set and get its border style from a BS dictionary', function() { + var dict = new Dict(); + var bs = new Dict(); + bs.set('Type', new Name('Border')); + bs.set('S', { name: 'U' }); + bs.set('W', 2); + bs.set('D', [2, 3]); + dict.set('BS', bs); + + var annotation = new Annotation(); + annotation.setBorderStyle(dict); + + expect(annotation.getBorderStyle().getStyle()). + toEqual(AnnotationBorderStyleType.UNDERLINE); + + expect(annotation.getBorderStyle().getWidth()).toEqual(2); + + expect(annotation.getBorderStyle().getDashArray()).toEqual([2, 3]); + }); + + it('should set and get its border style from a Border array', function() { + var dict = new Dict(); + dict.set('Border', [5, 2, 2, [2, 3]]); + + var annotation = new Annotation(); + annotation.setBorderStyle(dict); + + var borderStyle = annotation.getBorderStyle(); + + expect(borderStyle.getStyle()).toEqual(AnnotationBorderStyleType.SOLID); + expect(borderStyle.getWidth()).toEqual(2); + expect(borderStyle.getDashArray()).toEqual([2, 3]); + expect(borderStyle.getHorizontalCornerRadius()).toEqual(5); + expect(borderStyle.getVerticalCornerRadius()).toEqual(2); + }); + + it('should not set an invalid border style', function() { + var annotation = new Annotation(); + annotation.setBorderStyle('not a dict'); + var borderStyle = annotation.getBorderStyle(); + + expect(borderStyle.getStyle()).toEqual(AnnotationBorderStyleType.SOLID); + expect(borderStyle.getWidth()).toEqual(1); + expect(borderStyle.getDashArray()).toEqual([3]); + expect(borderStyle.getHorizontalCornerRadius()).toEqual(0); + expect(borderStyle.getVerticalCornerRadius()).toEqual(0); + }); + + it('should set and get its RGB color', function() { + var annotation = new Annotation(); + annotation.setColor([0, 0, 1]); + + expect(annotation.getColor()).toEqual([0, 0, 255]); + }); + + it('should set and get its CMYK color', function() { + var annotation = new Annotation(); + annotation.setColor([0.1, 0.92, 0.84, 0.02]); + + expect(annotation.getColor()).toEqual([233, 59, 47]); + }); + + it('should set and get its grayscale color', function() { + var annotation = new Annotation(); + annotation.setColor([0.4]); + + expect(annotation.getColor()).toEqual([102, 102, 102]); + }); + + it('should not set an invalid color', function() { + var annotation = new Annotation(); + annotation.setColor([0.4, 7]); + + expect(annotation.getColor()).toEqual(null); + }); + + it('should set and get flags', function() { + var annotation = new Annotation(); + annotation.setFlags(13); + + expect(annotation.hasFlag(AnnotationFlag.INVISIBLE)).toEqual(true); + expect(annotation.hasFlag(AnnotationFlag.NOZOOM)).toEqual(true); + expect(annotation.hasFlag(AnnotationFlag.PRINT)).toEqual(true); + expect(annotation.hasFlag(AnnotationFlag.READONLY)).toEqual(false); + }); + + it('should be viewable and not printable by default', function() { + var annotation = new Annotation(); + + expect(annotation.isViewable()).toEqual(true); + expect(annotation.isPrintable()).toEqual(false); + }); + }); + + describe('LinkAnnotation', function() { + it('should set and get its action', function() { + var dict = new Dict(); + dict.set('S', new Name('URI')); + dict.set('URI', 'www.mozilla.org'); + + var annotation = new LinkAnnotation(); + annotation.setAction(dict); + + expect(annotation.getAction()).toEqual({ + type: 'URI', + url: 'http://www.mozilla.org' + }); + }); + + it('should not set an invalid action', function() { + var annotation = new LinkAnnotation(); + annotation.setAction('no object'); + + expect(annotation.getAction()).toEqual(null); + }); + + it('should set and get its destination', function() { + var annotation = new LinkAnnotation(); + annotation.setDestination('EN'); + + expect(annotation.getDestination()).toEqual('EN'); + }); + }); +}); + diff --git a/test/unit/unit_test.html b/test/unit/unit_test.html index 94d27eb90a5fa..491555f65b0cf 100644 --- a/test/unit/unit_test.html +++ b/test/unit/unit_test.html @@ -54,6 +54,7 @@ +