diff --git a/src/core/annotation.js b/src/core/annotation.js index e5b60bd56..9463525c1 100644 --- a/src/core/annotation.js +++ b/src/core/annotation.js @@ -15,7 +15,8 @@ import { AnnotationBorderStyleType, AnnotationFieldFlag, AnnotationFlag, - AnnotationType, OPS, stringToBytes, stringToPDFString, Util, warn + AnnotationType, getInheritableProperty, OPS, stringToBytes, stringToPDFString, + Util, warn } from '../shared/util'; import { Catalog, FileSpec, ObjectLoader } from './obj'; import { Dict, isDict, isName, isRef, isStream } from './primitives'; @@ -60,7 +61,7 @@ class AnnotationFactory { return new TextAnnotation(parameters); case 'Widget': - let fieldType = Util.getInheritableProperty(dict, 'FT'); + let fieldType = getInheritableProperty({ dict, key: 'FT', }); fieldType = isName(fieldType) ? fieldType.name : null; switch (fieldType) { @@ -580,15 +581,16 @@ class WidgetAnnotation extends Annotation { data.annotationType = AnnotationType.WIDGET; data.fieldName = this._constructFieldName(dict); - data.fieldValue = Util.getInheritableProperty(dict, 'V', - /* getArray = */ true); + data.fieldValue = getInheritableProperty({ dict, key: 'V', + getArray: true, }); data.alternativeText = stringToPDFString(dict.get('TU') || ''); - data.defaultAppearance = Util.getInheritableProperty(dict, 'DA') || ''; - let fieldType = Util.getInheritableProperty(dict, 'FT'); + data.defaultAppearance = getInheritableProperty({ dict, key: 'DA', }) || ''; + let fieldType = getInheritableProperty({ dict, key: 'FT', }); data.fieldType = isName(fieldType) ? fieldType.name : null; - this.fieldResources = Util.getInheritableProperty(dict, 'DR') || Dict.empty; + this.fieldResources = getInheritableProperty({ dict, key: 'DR', }) || + Dict.empty; - data.fieldFlags = Util.getInheritableProperty(dict, 'Ff'); + data.fieldFlags = getInheritableProperty({ dict, key: 'Ff', }); if (!Number.isInteger(data.fieldFlags) || data.fieldFlags < 0) { data.fieldFlags = 0; } @@ -675,18 +677,20 @@ class TextWidgetAnnotation extends WidgetAnnotation { constructor(params) { super(params); + const dict = params.dict; + // The field value is always a string. this.data.fieldValue = stringToPDFString(this.data.fieldValue || ''); // Determine the alignment of text in the field. - let alignment = Util.getInheritableProperty(params.dict, 'Q'); + let alignment = getInheritableProperty({ dict, key: 'Q', }); if (!Number.isInteger(alignment) || alignment < 0 || alignment > 2) { alignment = null; } this.data.textAlignment = alignment; // Determine the maximum length of text in the field. - let maximumLength = Util.getInheritableProperty(params.dict, 'MaxLen'); + let maximumLength = getInheritableProperty({ dict, key: 'MaxLen', }); if (!Number.isInteger(maximumLength) || maximumLength < 0) { maximumLength = null; } @@ -814,7 +818,7 @@ class ChoiceWidgetAnnotation extends WidgetAnnotation { // inherit the options from a parent annotation (issue 8094). this.data.options = []; - let options = Util.getInheritableProperty(params.dict, 'Opt'); + let options = getInheritableProperty({ dict: params.dict, key: 'Opt', }); if (Array.isArray(options)) { let xref = params.xref; for (let i = 0, ii = options.length; i < ii; i++) { diff --git a/src/core/document.js b/src/core/document.js index 7ef67855c..675e9c1a6 100644 --- a/src/core/document.js +++ b/src/core/document.js @@ -16,8 +16,8 @@ import { Catalog, ObjectLoader, XRef } from './obj'; import { Dict, isDict, isName, isStream } from './primitives'; import { - info, isArrayBuffer, isNum, isSpace, isString, MissingDataException, OPS, - shadow, stringToBytes, stringToPDFString, Util, warn + getInheritableProperty, info, isArrayBuffer, isNum, isSpace, isString, + MissingDataException, OPS, shadow, stringToBytes, stringToPDFString, Util } from '../shared/util'; import { NullStream, Stream, StreamsSequenceStream } from './stream'; import { AnnotationFactory } from './annotation'; @@ -62,33 +62,19 @@ var Page = (function PageClosure() { } Page.prototype = { - getInheritedPageProp: function Page_getInheritedPageProp(key, getArray) { - var dict = this.pageDict, valueArray = null, loopCount = 0; - var MAX_LOOP_COUNT = 100; - getArray = getArray || false; - // Always walk up the entire parent chain, to be able to find - // e.g. \Resources placed on multiple levels of the tree. - while (dict) { - var value = getArray ? dict.getArray(key) : dict.get(key); - if (value !== undefined) { - if (!valueArray) { - valueArray = []; - } - valueArray.push(value); - } - if (++loopCount > MAX_LOOP_COUNT) { - warn('getInheritedPageProp: maximum loop count exceeded for ' + key); - return valueArray ? valueArray[0] : undefined; - } - dict = dict.get('Parent'); + /** + * @private + */ + _getInheritableProperty(key, getArray = false) { + let value = getInheritableProperty({ dict: this.pageDict, key, getArray, + stopWhenFound: false, }); + if (!Array.isArray(value)) { + return value; } - if (!valueArray) { - return undefined; + if (value.length === 1 || !isDict(value[0])) { + return value[0]; } - if (valueArray.length === 1 || !isDict(valueArray[0])) { - return valueArray[0]; - } - return Dict.merge(this.xref, valueArray); + return Dict.merge(this.xref, value); }, get content() { @@ -100,11 +86,12 @@ var Page = (function PageClosure() { // present, but can be empty. Some document omit it still, in this case // we return an empty dictionary. return shadow(this, 'resources', - this.getInheritedPageProp('Resources') || Dict.empty); + this._getInheritableProperty('Resources') || Dict.empty); }, get mediaBox() { - var mediaBox = this.getInheritedPageProp('MediaBox', true); + var mediaBox = this._getInheritableProperty('MediaBox', + /* getArray = */ true); // Reset invalid media box to letter size. if (!Array.isArray(mediaBox) || mediaBox.length !== 4) { return shadow(this, 'mediaBox', LETTER_SIZE_MEDIABOX); @@ -113,7 +100,8 @@ var Page = (function PageClosure() { }, get cropBox() { - var cropBox = this.getInheritedPageProp('CropBox', true); + var cropBox = this._getInheritableProperty('CropBox', + /* getArray = */ true); // Reset invalid crop box to media box. if (!Array.isArray(cropBox) || cropBox.length !== 4) { return shadow(this, 'cropBox', this.mediaBox); @@ -143,7 +131,7 @@ var Page = (function PageClosure() { }, get rotate() { - var rotate = this.getInheritedPageProp('Rotate') || 0; + var rotate = this._getInheritableProperty('Rotate') || 0; // Normalize rotation so it's a multiple of 90 and between 0 and 270 if (rotate % 90 !== 0) { rotate = 0; @@ -180,7 +168,7 @@ var Page = (function PageClosure() { loadResources: function Page_loadResources(keys) { if (!this.resourcesPromise) { - // TODO: add async getInheritedPageProp and remove this. + // TODO: add async `_getInheritableProperty` and remove this. this.resourcesPromise = this.pdfManager.ensure(this, 'resources'); } return this.resourcesPromise.then(() => { @@ -316,7 +304,7 @@ var Page = (function PageClosure() { get annotations() { var annotations = []; - var annotationRefs = this.getInheritedPageProp('Annots') || []; + var annotationRefs = this._getInheritableProperty('Annots') || []; for (var i = 0, n = annotationRefs.length; i < n; ++i) { var annotationRef = annotationRefs[i]; var annotation = AnnotationFactory.create(this.xref, annotationRef, diff --git a/src/shared/util.js b/src/shared/util.js index ec81372b5..a7b670ba8 100644 --- a/src/shared/util.js +++ b/src/shared/util.js @@ -644,6 +644,53 @@ function isEvalSupported() { } } +/** + * Get the value of an inheritable property. + * + * If the PDF specification explicitly lists a property in a dictionary as + * inheritable, then the value of the property may be present in the dictionary + * itself or in one or more parents of the dictionary. + * + * If the key is not found in the tree, `undefined` is returned. Otherwise, + * the value for the key is returned or, if `stopWhenFound` is `false`, a list + * of values is returned. To avoid infinite loops, the traversal is stopped when + * the loop limit is reached. + * + * @param {Dict} dict - Dictionary from where to start the traversal. + * @param {string} key - The key of the property to find the value for. + * @param {boolean} getArray - Whether or not the value should be fetched as an + * array. The default value is `false`. + * @param {boolean} stopWhenFound - Whether or not to stop the traversal when + * the key is found. If set to `false`, we always walk up the entire parent + * chain, for example to be able to find `\Resources` placed on multiple + * levels of the tree. The default value is `true`. + */ +function getInheritableProperty({ dict, key, getArray = false, + stopWhenFound = true, }) { + const LOOP_LIMIT = 100; + let loopCount = 0; + let values; + + while (dict) { + const value = getArray ? dict.getArray(key) : dict.get(key); + if (value !== undefined) { + if (stopWhenFound) { + return value; + } + if (!values) { + values = []; + } + values.push(value); + } + if (++loopCount > LOOP_LIMIT) { + warn(`getInheritableProperty: maximum loop count exceeded for "${key}"`); + break; + } + dict = dict.get('Parent'); + } + return values; +} + var IDENTITY_MATRIX = [1, 0, 0, 1, 0, 0]; var Util = (function UtilClosure() { @@ -853,17 +900,6 @@ var Util = (function UtilClosure() { } }; - Util.getInheritableProperty = - function Util_getInheritableProperty(dict, name, getArray) { - while (dict && !dict.has(name)) { - dict = dict.get('Parent'); - } - if (!dict) { - return null; - } - return getArray ? dict.getArray(name) : dict.get(name); - }; - Util.inherit = function Util_inherit(sub, base, prototype) { sub.prototype = Object.create(base.prototype); sub.prototype.constructor = sub; @@ -1609,6 +1645,7 @@ export { createPromiseCapability, createObjectURL, deprecated, + getInheritableProperty, getLookupTableFactory, getVerbosityLevel, info, diff --git a/test/unit/util_spec.js b/test/unit/util_spec.js index c5e18d564..47b8a037f 100644 --- a/test/unit/util_spec.js +++ b/test/unit/util_spec.js @@ -14,9 +14,12 @@ */ import { - bytesToString, isArrayBuffer, isBool, isEmptyObj, isNum, isSpace, isString, - log2, ReadableStream, removeNullCharacters, stringToBytes, stringToPDFString + bytesToString, getInheritableProperty, isArrayBuffer, isBool, isEmptyObj, + isNum, isSpace, isString, log2, ReadableStream, removeNullCharacters, + stringToBytes, stringToPDFString } from '../../src/shared/util'; +import { Dict, Ref } from '../../src/core/primitives'; +import { XRefMock } from './test_utils'; describe('util', function() { describe('bytesToString', function() { @@ -50,6 +53,106 @@ describe('util', function() { }); }); + describe('getInheritableProperty', function() { + it('handles non-dictionary arguments', function() { + expect(getInheritableProperty({ dict: null, key: 'foo', })) + .toEqual(undefined); + expect(getInheritableProperty({ dict: undefined, key: 'foo', })) + .toEqual(undefined); + }); + + it('handles dictionaries that do not contain the property', function() { + // Empty dictionary. + const emptyDict = new Dict(); + expect(getInheritableProperty({ dict: emptyDict, key: 'foo', })) + .toEqual(undefined); + + // Filled dictionary with a different property. + const filledDict = new Dict(); + filledDict.set('bar', 'baz'); + expect(getInheritableProperty({ dict: filledDict, key: 'foo', })) + .toEqual(undefined); + }); + + it('fetches the property if it is not inherited', function() { + const ref = new Ref(10, 0); + const xref = new XRefMock([{ ref, data: 'quux', }]); + const dict = new Dict(xref); + + // Regular values should be fetched. + dict.set('foo', 'bar'); + expect(getInheritableProperty({ dict, key: 'foo', })).toEqual('bar'); + + // Array value should be fetched (with references resolved). + dict.set('baz', ['qux', ref]); + expect(getInheritableProperty({ dict, key: 'baz', getArray: true, })) + .toEqual(['qux', 'quux']); + }); + + it('fetches the property if it is inherited and present on one level', + function() { + const ref = new Ref(10, 0); + const xref = new XRefMock([{ ref, data: 'quux', }]); + const firstDict = new Dict(xref); + const secondDict = new Dict(xref); + firstDict.set('Parent', secondDict); + + // Regular values should be fetched. + secondDict.set('foo', 'bar'); + expect(getInheritableProperty({ dict: firstDict, key: 'foo', })) + .toEqual('bar'); + + // Array value should be fetched (with references resolved). + secondDict.set('baz', ['qux', ref]); + expect(getInheritableProperty({ dict: firstDict, key: 'baz', + getArray: true, })) + .toEqual(['qux', 'quux']); + }); + + it('fetches the property if it is inherited and present on multiple levels', + function() { + const ref = new Ref(10, 0); + const xref = new XRefMock([{ ref, data: 'quux', }]); + const firstDict = new Dict(xref); + const secondDict = new Dict(xref); + firstDict.set('Parent', secondDict); + + // Regular values should be fetched. + firstDict.set('foo', 'bar1'); + secondDict.set('foo', 'bar2'); + expect(getInheritableProperty({ dict: firstDict, key: 'foo', })) + .toEqual('bar1'); + expect(getInheritableProperty({ dict: firstDict, key: 'foo', + getArray: false, stopWhenFound: false, })) + .toEqual(['bar1', 'bar2']); + + // Array value should be fetched (with references resolved). + firstDict.set('baz', ['qux1', ref]); + secondDict.set('baz', ['qux2', ref]); + expect(getInheritableProperty({ dict: firstDict, key: 'baz', + getArray: true, stopWhenFound: false, })) + .toEqual([['qux1', 'quux'], ['qux2', 'quux']]); + }); + + it('stops searching when the loop limit is reached', function() { + const dict = new Dict(); + let currentDict = dict; + let parentDict = null; + for (let i = 0; i < 150; i++) { // Exceeds the loop limit of 100. + parentDict = new Dict(); + currentDict.set('Parent', parentDict); + currentDict = parentDict; + } + parentDict.set('foo', 'bar'); // Never found because of loop limit. + expect(getInheritableProperty({ dict, key: 'foo', })).toEqual(undefined); + + dict.set('foo', 'baz'); + expect(getInheritableProperty({ dict, key: 'foo', getArray: false, + stopWhenFound: false, })) + .toEqual(['baz']); + }); + }); + describe('isArrayBuffer', function() { it('handles array buffer values', function() { expect(isArrayBuffer(new ArrayBuffer(0))).toEqual(true);