Skip to content

Commit

Permalink
Implement creation/modification date for annotations
Browse files Browse the repository at this point in the history
This includes the information in the core and display layers. The
date parsing logic from the document properties is rewritten according
to the specification and now includes unit tests.

Moreover, missing unit tests for the color of a popup annotation have
been added.

Finally the styling of the popup is changed slightly to make the text a
bit smaller (it's currently quite large in comparison to other viewers)
and to make the drop shadow a bit more subtle. The former is done to be
able to easily include the modification date in the popup similar to how
other viewers do this.
  • Loading branch information
timvandermeij committed May 5, 2019
1 parent 6cfb1e1 commit be1d662
Show file tree
Hide file tree
Showing 11 changed files with 343 additions and 59 deletions.
4 changes: 4 additions & 0 deletions l10n/en-US/viewer.properties
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,10 @@ invalid_file_error=Invalid or corrupted PDF file.
missing_file_error=Missing PDF file.
unexpected_response_error=Unexpected server response.

# LOCALIZATION NOTE (annotation_date_string): "{{date}}" and "{{time}}" will be
# replaced by the modification date, and time, of the annotation.
annotation_date_string={{date}}, {{time}}

# LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip.
# "{{type}}" will be replaced with an annotation type from a list defined in
# the PDF spec (32000-1:2008 Table 169 – Annotation types).
Expand Down
4 changes: 4 additions & 0 deletions l10n/nl/viewer.properties
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,10 @@ invalid_file_error=Ongeldig of beschadigd PDF-bestand.
missing_file_error=PDF-bestand ontbreekt.
unexpected_response_error=Onverwacht serverantwoord.

# LOCALIZATION NOTE (annotation_date_string): "{{date}}" and "{{time}}" will be
# replaced by the modification date, and time, of the annotation.
annotation_date_string={{date}}, {{time}}

# LOCALIZATION NOTE (text_annotation_type.alt): This is used as a tooltip.
# "{{type}}" will be replaced with an annotation type from a list defined in
# the PDF spec (32000-1:2008 Table 169 – Annotation types).
Expand Down
45 changes: 44 additions & 1 deletion src/core/annotation.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

import {
AnnotationBorderStyleType, AnnotationFieldFlag, AnnotationFlag,
AnnotationType, OPS, stringToBytes, stringToPDFString, Util, warn
AnnotationType, isString, OPS, stringToBytes, stringToPDFString, Util, warn
} from '../shared/util';
import { Catalog, FileSpec, ObjectLoader } from './obj';
import { Dict, isDict, isName, isRef, isStream } from './primitives';
Expand Down Expand Up @@ -176,6 +176,8 @@ class Annotation {
constructor(params) {
let dict = params.dict;

this.setCreationDate(dict.get('CreationDate'));
this.setModificationDate(dict.get('M'));
this.setFlags(dict.get('F'));
this.setRectangle(dict.getArray('Rect'));
this.setColor(dict.getArray('C'));
Expand All @@ -187,8 +189,10 @@ class Annotation {
annotationFlags: this.flags,
borderStyle: this.borderStyle,
color: this.color,
creationDate: this.creationDate,
hasAppearance: !!this.appearance,
id: params.id,
modificationDate: this.modificationDate,
rect: this.rectangle,
subtype: params.subtype,
};
Expand Down Expand Up @@ -239,6 +243,31 @@ class Annotation {
return this._isPrintable(this.flags);
}

/**
* Set the creation date.
*
* @public
* @memberof Annotation
* @param {string} creationDate - PDF date string that indicates when the
* annotation was originally created
*/
setCreationDate(creationDate) {
this.creationDate = isString(creationDate) ? creationDate : null;
}

/**
* Set the modification date.
*
* @public
* @memberof Annotation
* @param {string} modificationDate - PDF date string that indicates when the
* annotation was last modified
*/
setModificationDate(modificationDate) {
this.modificationDate = isString(modificationDate) ?
modificationDate : null;
}

/**
* Set the flags.
*
Expand Down Expand Up @@ -947,6 +976,20 @@ class PopupAnnotation extends Annotation {
this.data.title = stringToPDFString(parentItem.get('T') || '');
this.data.contents = stringToPDFString(parentItem.get('Contents') || '');

if (!parentItem.has('CreationDate')) {
this.data.creationDate = null;
} else {
this.setCreationDate(parentItem.get('CreationDate'));
this.data.creationDate = this.creationDate;
}

if (!parentItem.has('M')) {
this.data.modificationDate = null;
} else {
this.setModificationDate(parentItem.get('M'));
this.data.modificationDate = this.modificationDate;
}

if (!parentItem.has('C')) {
// Fall back to the default background color.
this.data.color = null;
Expand Down
28 changes: 24 additions & 4 deletions src/display/annotation_layer.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
*/

import {
addLinkAttributes, DOMSVGFactory, getFilenameFromUrl, LinkTarget
addLinkAttributes, DOMSVGFactory, getFilenameFromUrl, LinkTarget,
PDFDateString
} from './display_utils';
import {
AnnotationBorderStyleType, AnnotationType, stringToPDFString, unreachable,
Expand Down Expand Up @@ -251,6 +252,7 @@ class AnnotationElement {
trigger,
color: data.color,
title: data.title,
modificationDate: data.modificationDate,
contents: data.contents,
hideWrapper: true,
});
Expand Down Expand Up @@ -664,6 +666,7 @@ class PopupAnnotationElement extends AnnotationElement {
trigger: parentElement,
color: this.data.color,
title: this.data.title,
modificationDate: this.data.modificationDate,
contents: this.data.contents,
});

Expand All @@ -686,6 +689,7 @@ class PopupElement {
this.trigger = parameters.trigger;
this.color = parameters.color;
this.title = parameters.title;
this.modificationDate = parameters.modificationDate;
this.contents = parameters.contents;
this.hideWrapper = parameters.hideWrapper || false;

Expand Down Expand Up @@ -724,18 +728,34 @@ class PopupElement {
popup.style.backgroundColor = Util.makeCssRgb(r | 0, g | 0, b | 0);
}

let contents = this._formatContents(this.contents);
let title = document.createElement('h1');
title.textContent = this.title;
popup.appendChild(title);

// The modification date is shown in the popup instead of the creation
// date if it is available and can be parsed correctly, which is
// consistent with other viewers such as Adobe Acrobat.
const dateObject = PDFDateString.toDateObject(this.modificationDate);
if (dateObject) {
const modificationDate = document.createElement('span');
modificationDate.textContent = '{{date}}, {{time}}';
modificationDate.dataset.l10nId = 'annotation_date_string';
modificationDate.dataset.l10nArgs = JSON.stringify({
date: dateObject.toLocaleDateString(),
time: dateObject.toLocaleTimeString(),
});
popup.appendChild(modificationDate);
}

let contents = this._formatContents(this.contents);
popup.appendChild(contents);

// Attach the event listeners to the trigger element.
this.trigger.addEventListener('click', this._toggle.bind(this));
this.trigger.addEventListener('mouseover', this._show.bind(this, false));
this.trigger.addEventListener('mouseout', this._hide.bind(this, false));
popup.addEventListener('click', this._hide.bind(this, true));

popup.appendChild(title);
popup.appendChild(contents);
wrapper.appendChild(popup);
return wrapper;
}
Expand Down
88 changes: 87 additions & 1 deletion src/display/display_utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
/* eslint no-var: error */

import {
assert, CMapCompressionType, removeNullCharacters, stringToBytes,
assert, CMapCompressionType, isString, removeNullCharacters, stringToBytes,
unreachable, URL, Util, warn
} from '../shared/util';

Expand Down Expand Up @@ -491,6 +491,91 @@ function releaseImageResources(img) {
img.removeAttribute('src');
}

let pdfDateStringRegex;

class PDFDateString {
/**
* Convert a PDF date string to a JavaScript `Date` object.
*
* The PDF date string format is described in section 7.9.4 of the official
* PDF 32000-1:2008 specification. However, in the PDF 1.7 reference (sixth
* edition) Adobe describes the same format including a trailing apostrophe.
* This syntax in incorrect, but Adobe Acrobat creates PDF files that contain
* them. We ignore all apostrophes as they are not necessary for date parsing.
*
* Moreover, Adobe Acrobat doesn't handle changing the date to universal time
* and doesn't use the user's time zone (effectively ignoring the HH' and mm'
* parts of the date string).
*
* @param {string} input
* @return {Date|null}
*/
static toDateObject(input) {
if (!input || !isString(input)) {
return null;
}

// Lazily initialize the regular expression.
if (!pdfDateStringRegex) {
pdfDateStringRegex = new RegExp(
'^D:' + // Prefix (required)
'(\\d{4})' + // Year (required)
'(\\d{2})?' + // Month (optional)
'(\\d{2})?' + // Day (optional)
'(\\d{2})?' + // Hour (optional)
'(\\d{2})?' + // Minute (optional)
'(\\d{2})?' + // Second (optional)
'([Z|+|-])?' + // Universal time relation (optional)
'(\\d{2})?' + // Offset hour (optional)
'\'?' + // Splitting apostrophe (optional)
'(\\d{2})?' + // Offset minute (optional)
'\'?' // Trailing apostrophe (optional)
);
}

// Optional fields that don't satisfy the requirements from the regular
// expression (such as incorrect digit counts or numbers that are out of
// range) will fall back the defaults from the specification.
const matches = pdfDateStringRegex.exec(input);
if (!matches) {
return null;
}

// JavaScript's `Date` object expects the month to be between 0 and 11
// instead of 1 and 12, so we have to correct for that.
const year = parseInt(matches[1], 10);
let month = parseInt(matches[2], 10);
month = (month >= 1 && month <= 12) ? month - 1 : 0;
let day = parseInt(matches[3], 10);
day = (day >= 1 && day <= 31) ? day : 1;
let hour = parseInt(matches[4], 10);
hour = (hour >= 0 && hour <= 23) ? hour : 0;
let minute = parseInt(matches[5], 10);
minute = (minute >= 0 && minute <= 59) ? minute : 0;
let second = parseInt(matches[6], 10);
second = (second >= 0 && second <= 59) ? second : 0;
const universalTimeRelation = matches[7] || 'Z';
let offsetHour = parseInt(matches[8], 10);
offsetHour = (offsetHour >= 0 && offsetHour <= 23) ? offsetHour : 0;
let offsetMinute = parseInt(matches[9], 10) || 0;
offsetMinute = (offsetMinute >= 0 && offsetMinute <= 59) ? offsetMinute : 0;

// Universal time relation 'Z' means that the local time is equal to the
// universal time, whereas the relations '+'/'-' indicate that the local
// time is later respectively earlier than the universal time. Every date
// is normalized to universal time.
if (universalTimeRelation === '-') {
hour += offsetHour;
minute += offsetMinute;
} else if (universalTimeRelation === '+') {
hour -= offsetHour;
minute -= offsetMinute;
}

return new Date(Date.UTC(year, month, day, hour, minute, second));
}
}

export {
PageViewport,
RenderingCancelledException,
Expand All @@ -508,4 +593,5 @@ export {
loadScript,
deprecated,
releaseImageResources,
PDFDateString,
};
1 change: 1 addition & 0 deletions src/pdf.js
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ exports.getFilenameFromUrl = pdfjsDisplayDisplayUtils.getFilenameFromUrl;
exports.LinkTarget = pdfjsDisplayDisplayUtils.LinkTarget;
exports.addLinkAttributes = pdfjsDisplayDisplayUtils.addLinkAttributes;
exports.loadScript = pdfjsDisplayDisplayUtils.loadScript;
exports.PDFDateString = pdfjsDisplayDisplayUtils.PDFDateString;
exports.GlobalWorkerOptions = pdfjsDisplayWorkerOptions.GlobalWorkerOptions;
exports.apiCompatibilityParams =
pdfjsDisplayAPICompatibility.apiCompatibilityParams;
6 changes: 6 additions & 0 deletions test/annotation_layer_builder_overrides.css
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,9 @@
.annotationLayer .popupWrapper {
display: block;
}

.annotationLayer .popup h1,
.annotationLayer .popup p {
margin: 0;
padding: 0;
}
81 changes: 81 additions & 0 deletions test/unit/annotation_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,34 @@ describe('annotation', function() {
dict = ref = null;
});

it('should set and get a valid creation date', function() {
const annotation = new Annotation({ dict, ref, });
annotation.setCreationDate('D:20190422');

expect(annotation.creationDate).toEqual('D:20190422');
});

it('should set and get an invalid creation date', function() {
const annotation = new Annotation({ dict, ref, });
annotation.setCreationDate(undefined);

expect(annotation.creationDate).toEqual(null);
});

it('should set and get a valid modification date', function() {
const annotation = new Annotation({ dict, ref, });
annotation.setModificationDate('D:20190422');

expect(annotation.modificationDate).toEqual('D:20190422');
});

it('should set and get an invalid modification date', function() {
const annotation = new Annotation({ dict, ref, });
annotation.setModificationDate(undefined);

expect(annotation.modificationDate).toEqual(null);
});

it('should set and get flags', function() {
const annotation = new Annotation({ dict, ref, });
annotation.setFlags(13);
Expand Down Expand Up @@ -1400,6 +1428,59 @@ describe('annotation', function() {
});

describe('PopupAnnotation', function() {
it('should inherit properties from its parent', function(done) {
const parentDict = new Dict();
parentDict.set('Type', Name.get('Annot'));
parentDict.set('Subtype', Name.get('Text'));
parentDict.set('CreationDate', 'D:20190422');
parentDict.set('M', 'D:20190423');
parentDict.set('C', [0, 0, 1]);

const popupDict = new Dict();
popupDict.set('Type', Name.get('Annot'));
popupDict.set('Subtype', Name.get('Popup'));
popupDict.set('Parent', parentDict);

const popupRef = new Ref(13, 0);
const xref = new XRefMock([
{ ref: popupRef, data: popupDict, }
]);

AnnotationFactory.create(xref, popupRef, pdfManagerMock,
idFactoryMock).then(({ data, viewable, }) => {
expect(data.annotationType).toEqual(AnnotationType.POPUP);
expect(data.creationDate).toEqual('D:20190422');
expect(data.modificationDate).toEqual('D:20190423');
expect(data.color).toEqual(new Uint8ClampedArray([0, 0, 255]));
done();
}, done.fail);
});

it('should handle missing parent properties', function(done) {
const parentDict = new Dict();
parentDict.set('Type', Name.get('Annot'));
parentDict.set('Subtype', Name.get('Text'));

const popupDict = new Dict();
popupDict.set('Type', Name.get('Annot'));
popupDict.set('Subtype', Name.get('Popup'));
popupDict.set('Parent', parentDict);

const popupRef = new Ref(13, 0);
const xref = new XRefMock([
{ ref: popupRef, data: popupDict, }
]);

AnnotationFactory.create(xref, popupRef, pdfManagerMock,
idFactoryMock).then(({ data, viewable, }) => {
expect(data.annotationType).toEqual(AnnotationType.POPUP);
expect(data.creationDate).toEqual(null);
expect(data.modificationDate).toEqual(null);
expect(data.color).toEqual(null);
done();
}, done.fail);
});

it('should inherit the parent flags when the Popup is not viewable, ' +
'but the parent is (PR 7352)', function(done) {
const parentDict = new Dict();
Expand Down
Loading

0 comments on commit be1d662

Please sign in to comment.