diff --git a/src/core/annotation.js b/src/core/annotation.js index 985cf1c8a..4d817365d 100644 --- a/src/core/annotation.js +++ b/src/core/annotation.js @@ -16,8 +16,8 @@ import { AnnotationBorderStyleType, AnnotationFieldFlag, AnnotationFlag, - AnnotationType, assert, isString, OPS, stringToBytes, stringToPDFString, Util, - warn + AnnotationReplyType, AnnotationType, assert, isString, OPS, stringToBytes, + stringToPDFString, Util, warn } from '../shared/util'; import { Catalog, FileSpec, ObjectLoader } from './obj'; import { Dict, isDict, isName, isRef, isStream } from './primitives'; @@ -643,16 +643,61 @@ class MarkupAnnotation extends Annotation { super(parameters); const dict = parameters.dict; - if (!dict.has('C')) { - // Fall back to the default background color. - this.data.color = null; + + if (dict.has('IRT')) { + const rawIRT = dict.getRaw('IRT'); + this.data.inReplyTo = isRef(rawIRT) ? rawIRT.toString() : null; + + const rt = dict.get('RT'); + this.data.replyType = isName(rt) ? rt.name : AnnotationReplyType.REPLY; } - this.setCreationDate(dict.get('CreationDate')); - this.data.creationDate = this.creationDate; + if (this.data.replyType === AnnotationReplyType.GROUP) { + // Subordinate annotations in a group should inherit + // the group attributes from the primary annotation. + const parent = dict.get('IRT'); - this.data.hasPopup = dict.has('Popup'); - this.data.title = stringToPDFString(dict.get('T') || ''); + this.data.title = stringToPDFString(parent.get('T') || ''); + + this.setContents(parent.get('Contents')); + this.data.contents = this.contents; + + if (!parent.has('CreationDate')) { + this.data.creationDate = null; + } else { + this.setCreationDate(parent.get('CreationDate')); + this.data.creationDate = this.creationDate; + } + + if (!parent.has('M')) { + this.data.modificationDate = null; + } else { + this.setModificationDate(parent.get('M')); + this.data.modificationDate = this.modificationDate; + } + + this.data.hasPopup = parent.has('Popup'); + + if (!parent.has('C')) { + // Fall back to the default background color. + this.data.color = null; + } else { + this.setColor(parent.getArray('C')); + this.data.color = this.color; + } + } else { + this.data.title = stringToPDFString(dict.get('T') || ''); + + this.setCreationDate(dict.get('CreationDate')); + this.data.creationDate = this.creationDate; + + this.data.hasPopup = dict.has('Popup'); + + if (!dict.has('C')) { + // Fall back to the default background color. + this.data.color = null; + } + } } /** @@ -969,6 +1014,7 @@ class TextAnnotation extends MarkupAnnotation { super(parameters); + const dict = parameters.dict; this.data.annotationType = AnnotationType.TEXT; if (this.data.hasAppearance) { @@ -976,10 +1022,17 @@ class TextAnnotation extends MarkupAnnotation { } else { this.data.rect[1] = this.data.rect[3] - DEFAULT_ICON_SIZE; this.data.rect[2] = this.data.rect[0] + DEFAULT_ICON_SIZE; - this.data.name = parameters.dict.has('Name') ? - parameters.dict.get('Name').name : 'Note'; + this.data.name = dict.has('Name') ? + dict.get('Name').name : 'Note'; } + if (dict.has('State')) { + this.data.state = dict.get('State') || null; + this.data.stateModel = dict.get('StateModel') || null; + } else { + this.data.state = null; + this.data.stateModel = null; + } } } @@ -1012,9 +1065,15 @@ class PopupAnnotation extends Annotation { let parentSubtype = parentItem.get('Subtype'); this.data.parentType = isName(parentSubtype) ? parentSubtype.name : null; - this.data.parentId = dict.getRaw('Parent').toString(); - this.data.title = stringToPDFString(parentItem.get('T') || ''); - this.data.contents = stringToPDFString(parentItem.get('Contents') || ''); + const rawParent = dict.getRaw('Parent'); + this.data.parentId = isRef(rawParent) ? rawParent.toString() : null; + + const rt = parentItem.get('RT'); + if (isName(rt, AnnotationReplyType.GROUP)) { + // Subordinate annotations in a group should inherit + // the group attributes from the primary annotation. + parentItem = parentItem.get('IRT'); + } if (!parentItem.has('M')) { this.data.modificationDate = null; @@ -1040,6 +1099,9 @@ class PopupAnnotation extends Annotation { this.setFlags(parentFlags); } } + + this.data.title = stringToPDFString(parentItem.get('T') || ''); + this.data.contents = stringToPDFString(parentItem.get('Contents') || ''); } } diff --git a/src/shared/util.js b/src/shared/util.js index 35685921f..d83550a35 100644 --- a/src/shared/util.js +++ b/src/shared/util.js @@ -86,6 +86,29 @@ const AnnotationType = { REDACT: 26, }; +const AnnotationStateModelType = { + MARKED: 'Marked', + REVIEW: 'Review', +}; + +const AnnotationMarkedState = { + MARKED: 'Marked', + UNMARKED: 'Unmarked', +}; + +const AnnotationReviewState = { + ACCEPTED: 'Accepted', + REJECTED: 'Rejected', + CANCELLED: 'Cancelled', + COMPLETED: 'Completed', + NONE: 'None', +}; + +const AnnotationReplyType = { + GROUP: 'Group', + REPLY: 'R', +}; + const AnnotationFlag = { INVISIBLE: 0x01, HIDDEN: 0x02, @@ -910,6 +933,10 @@ export { AnnotationBorderStyleType, AnnotationFieldFlag, AnnotationFlag, + AnnotationMarkedState, + AnnotationReplyType, + AnnotationReviewState, + AnnotationStateModelType, AnnotationType, FontType, ImageKind, diff --git a/test/unit/annotation_spec.js b/test/unit/annotation_spec.js index 8c17cf6fd..4b634632c 100644 --- a/test/unit/annotation_spec.js +++ b/test/unit/annotation_spec.js @@ -343,6 +343,220 @@ describe('annotation', function() { expect(markupAnnotation.creationDate).toEqual(null); }); + + it('should not parse IRT/RT when not defined', function (done) { + dict.set('Type', Name.get('Annot')); + dict.set('Subtype', Name.get('Text')); + + const xref = new XRefMock([ + { ref, data: dict, }, + ]); + + AnnotationFactory.create(xref, ref, pdfManagerMock, + idFactoryMock).then(({ data, }) => { + expect(data.inReplyTo).toBeUndefined(); + expect(data.replyType).toBeUndefined(); + done(); + }, done.fail); + }); + + it('should parse IRT and set default RT when not defined.', + function (done) { + const annotationRef = new Ref(819, 0); + const annotationDict = new Dict(); + annotationDict.set('Type', Name.get('Annot')); + annotationDict.set('Subtype', Name.get('Text')); + + const replyRef = new Ref(820, 0); + const replyDict = new Dict(); + replyDict.set('Type', Name.get('Annot')); + replyDict.set('Subtype', Name.get('Text')); + replyDict.set('IRT', annotationRef); + + const xref = new XRefMock([ + { ref: annotationRef, data: annotationDict, }, + { ref: replyRef, data: replyDict, } + ]); + annotationDict.assignXref(xref); + replyDict.assignXref(xref); + + AnnotationFactory.create(xref, replyRef, pdfManagerMock, + idFactoryMock).then(({ data, }) => { + expect(data.inReplyTo).toEqual(annotationRef.toString()); + expect(data.replyType).toEqual('R'); + done(); + }, done.fail); + }); + + it('should parse IRT/RT for a group type', function (done) { + const annotationRef = new Ref(819, 0); + const annotationDict = new Dict(); + annotationDict.set('Type', Name.get('Annot')); + annotationDict.set('Subtype', Name.get('Text')); + annotationDict.set('T', 'ParentTitle'); + annotationDict.set('Contents', 'ParentText'); + annotationDict.set('CreationDate', 'D:20180423'); + annotationDict.set('M', 'D:20190423'); + annotationDict.set('C', [0, 0, 1]); + + const popupRef = new Ref(820, 0); + const popupDict = new Dict(); + popupDict.set('Type', Name.get('Annot')); + popupDict.set('Subtype', Name.get('Popup')); + popupDict.set('Parent', annotationRef); + annotationDict.set('Popup', popupRef); + + const replyRef = new Ref(821, 0); + const replyDict = new Dict(); + replyDict.set('Type', Name.get('Annot')); + replyDict.set('Subtype', Name.get('Text')); + replyDict.set('IRT', annotationRef); + replyDict.set('RT', Name.get('Group')); + replyDict.set('T', 'ReplyTitle'); + replyDict.set('Contents', 'ReplyText'); + replyDict.set('CreationDate', 'D:20180523'); + replyDict.set('M', 'D:20190523'); + replyDict.set('C', [0.4]); + + const xref = new XRefMock([ + { ref: annotationRef, data: annotationDict, }, + { ref: popupRef, data: popupDict, }, + { ref: replyRef, data: replyDict, } + ]); + annotationDict.assignXref(xref); + popupDict.assignXref(xref); + replyDict.assignXref(xref); + + AnnotationFactory.create(xref, replyRef, pdfManagerMock, + idFactoryMock).then(({ data, }) => { + expect(data.inReplyTo).toEqual(annotationRef.toString()); + expect(data.replyType).toEqual('Group'); + expect(data.title).toEqual('ParentTitle'); + expect(data.contents).toEqual('ParentText'); + expect(data.creationDate).toEqual('D:20180423'); + expect(data.modificationDate).toEqual('D:20190423'); + expect(data.color).toEqual(new Uint8ClampedArray([0, 0, 255])); + expect(data.hasPopup).toEqual(true); + done(); + }, done.fail); + }); + + it('should parse IRT/RT for a reply type', function (done) { + const annotationRef = new Ref(819, 0); + const annotationDict = new Dict(); + annotationDict.set('Type', Name.get('Annot')); + annotationDict.set('Subtype', Name.get('Text')); + annotationDict.set('T', 'ParentTitle'); + annotationDict.set('Contents', 'ParentText'); + annotationDict.set('CreationDate', 'D:20180423'); + annotationDict.set('M', 'D:20190423'); + annotationDict.set('C', [0, 0, 1]); + + const popupRef = new Ref(820, 0); + const popupDict = new Dict(); + popupDict.set('Type', Name.get('Annot')); + popupDict.set('Subtype', Name.get('Popup')); + popupDict.set('Parent', annotationRef); + annotationDict.set('Popup', popupRef); + + const replyRef = new Ref(821, 0); + const replyDict = new Dict(); + replyDict.set('Type', Name.get('Annot')); + replyDict.set('Subtype', Name.get('Text')); + replyDict.set('IRT', annotationRef); + replyDict.set('RT', Name.get('R')); + replyDict.set('T', 'ReplyTitle'); + replyDict.set('Contents', 'ReplyText'); + replyDict.set('CreationDate', 'D:20180523'); + replyDict.set('M', 'D:20190523'); + replyDict.set('C', [0.4]); + + const xref = new XRefMock([ + { ref: annotationRef, data: annotationDict, }, + { ref: popupRef, data: popupDict, }, + { ref: replyRef, data: replyDict, } + ]); + annotationDict.assignXref(xref); + popupDict.assignXref(xref); + replyDict.assignXref(xref); + + AnnotationFactory.create(xref, replyRef, pdfManagerMock, + idFactoryMock).then(({ data, }) => { + expect(data.inReplyTo).toEqual(annotationRef.toString()); + expect(data.replyType).toEqual('R'); + expect(data.title).toEqual('ReplyTitle'); + expect(data.contents).toEqual('ReplyText'); + expect(data.creationDate).toEqual('D:20180523'); + expect(data.modificationDate).toEqual('D:20190523'); + expect(data.color).toEqual(new Uint8ClampedArray([102, 102, 102])); + expect(data.hasPopup).toEqual(false); + done(); + }, done.fail); + }); + }); + + describe('TextAnnotation', function() { + it('should not parse state model and state when not defined', + function (done) { + const annotationRef = new Ref(819, 0); + const annotationDict = new Dict(); + annotationDict.set('Type', Name.get('Annot')); + annotationDict.set('Subtype', Name.get('Text')); + annotationDict.set('Contents', 'TestText'); + + const replyRef = new Ref(820, 0); + const replyDict = new Dict(); + replyDict.set('Type', Name.get('Annot')); + replyDict.set('Subtype', Name.get('Text')); + replyDict.set('IRT', annotationRef); + replyDict.set('RT', Name.get('R')); + replyDict.set('Contents', 'ReplyText'); + + const xref = new XRefMock([ + { ref: annotationRef, data: annotationDict, }, + { ref: replyRef, data: replyDict, } + ]); + annotationDict.assignXref(xref); + replyDict.assignXref(xref); + + AnnotationFactory.create(xref, replyRef, pdfManagerMock, + idFactoryMock).then(({ data, }) => { + expect(data.stateModel).toBeNull(); + expect(data.state).toBeNull(); + done(); + }, done.fail); + }); + + it('should correctly parse state model and state when defined', + function (done) { + const annotationRef = new Ref(819, 0); + const annotationDict = new Dict(); + annotationDict.set('Type', Name.get('Annot')); + annotationDict.set('Subtype', Name.get('Text')); + + const replyRef = new Ref(820, 0); + const replyDict = new Dict(); + replyDict.set('Type', Name.get('Annot')); + replyDict.set('Subtype', Name.get('Text')); + replyDict.set('IRT', annotationRef); + replyDict.set('RT', Name.get('R')); + replyDict.set('StateModel', 'Review'); + replyDict.set('State', 'Rejected'); + + const xref = new XRefMock([ + { ref: annotationRef, data: annotationDict, }, + { ref: replyRef, data: replyDict, } + ]); + annotationDict.assignXref(xref); + replyDict.assignXref(xref); + + AnnotationFactory.create(xref, replyRef, pdfManagerMock, + idFactoryMock).then(({ data, }) => { + expect(data.stateModel).toEqual('Review'); + expect(data.state).toEqual('Rejected'); + done(); + }, done.fail); + }); }); describe('LinkAnnotation', function() { @@ -1540,6 +1754,58 @@ describe('annotation', function() { done(); }, done.fail); }); + + it('should correctly inherit Contents from group-master annotation ' + + 'if parent has ReplyType == Group', function (done) { + const annotationRef = new Ref(819, 0); + const annotationDict = new Dict(); + annotationDict.set('Type', Name.get('Annot')); + annotationDict.set('Subtype', Name.get('Text')); + annotationDict.set('T', 'Correct Title'); + annotationDict.set('Contents', 'Correct Text'); + annotationDict.set('M', 'D:20190423'); + annotationDict.set('C', [0, 0, 1]); + + const replyRef = new Ref(820, 0); + const replyDict = new Dict(); + replyDict.set('Type', Name.get('Annot')); + replyDict.set('Subtype', Name.get('Text')); + replyDict.set('IRT', annotationRef); + replyDict.set('RT', Name.get('Group')); + replyDict.set('T', 'Reply Title'); + replyDict.set('Contents', 'Reply Text'); + replyDict.set('M', 'D:20190523'); + replyDict.set('C', [0.4]); + + const popupRef = new Ref(821, 0); + const popupDict = new Dict(); + popupDict.set('Type', Name.get('Annot')); + popupDict.set('Subtype', Name.get('Popup')); + popupDict.set('T', 'Wrong Title'); + popupDict.set('Contents', 'Wrong Text'); + popupDict.set('Parent', replyRef); + popupDict.set('M', 'D:20190623'); + popupDict.set('C', [0.8]); + replyDict.set('Popup', popupRef); + + const xref = new XRefMock([ + { ref: annotationRef, data: annotationDict, }, + { ref: replyRef, data: replyDict, }, + { ref: popupRef, data: popupDict, } + ]); + annotationDict.assignXref(xref); + popupDict.assignXref(xref); + replyDict.assignXref(xref); + + AnnotationFactory.create(xref, popupRef, pdfManagerMock, + idFactoryMock).then(({ data, }) => { + expect(data.title).toEqual('Correct Title'); + expect(data.contents).toEqual('Correct Text'); + expect(data.modificationDate).toEqual('D:20190423'); + expect(data.color).toEqual(new Uint8ClampedArray([0, 0, 255])); + done(); + }, done.fail); + }); }); describe('InkAnnotation', function() {