Merge pull request #10771 from timvandermeij/annotation-dates

[api-minor] Implement creation/modification date for annotations
This commit is contained in:
Tim van der Meij 2019-05-07 00:32:52 +02:00 committed by GitHub
commit 83f6de3cf8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 343 additions and 59 deletions

View File

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

View File

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

View File

@ -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';
@ -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'));
@ -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,
};
@ -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.
*
@ -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;

View File

@ -14,7 +14,8 @@
*/
import {
addLinkAttributes, DOMSVGFactory, getFilenameFromUrl, LinkTarget
addLinkAttributes, DOMSVGFactory, getFilenameFromUrl, LinkTarget,
PDFDateString
} from './display_utils';
import {
AnnotationBorderStyleType, AnnotationType, stringToPDFString, unreachable,
@ -251,6 +252,7 @@ class AnnotationElement {
trigger,
color: data.color,
title: data.title,
modificationDate: data.modificationDate,
contents: data.contents,
hideWrapper: true,
});
@ -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,
});
@ -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;
@ -724,9 +728,27 @@ 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));
@ -734,8 +756,6 @@ class PopupElement {
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;
}

View File

@ -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';
@ -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,
@ -508,4 +593,5 @@ export {
loadScript,
deprecated,
releaseImageResources,
PDFDateString,
};

View File

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

View File

@ -35,3 +35,9 @@
.annotationLayer .popupWrapper {
display: block;
}
.annotationLayer .popup h1,
.annotationLayer .popup p {
margin: 0;
padding: 0;
}

View File

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

View File

@ -15,7 +15,8 @@
/* eslint no-var: error */
import {
DOMCanvasFactory, DOMSVGFactory, getFilenameFromUrl, isValidFetchUrl
DOMCanvasFactory, DOMSVGFactory, getFilenameFromUrl, isValidFetchUrl,
PDFDateString
} from '../../src/display/display_utils';
import isNodeJS from '../../src/shared/is_node';
@ -220,4 +221,70 @@ describe('display_utils', function() {
expect(isValidFetchUrl('https://www.example.com')).toEqual(true);
});
});
describe('PDFDateString', function() {
describe('toDateObject', function() {
it('converts PDF date strings to JavaScript `Date` objects', function() {
const expectations = {
undefined: null,
null: null,
42: null,
'2019': null,
'D2019': null,
'D:': null,
'D:201': null,
'D:2019': new Date(Date.UTC(2019, 0, 1, 0, 0, 0)),
'D:20190': new Date(Date.UTC(2019, 0, 1, 0, 0, 0)),
'D:201900': new Date(Date.UTC(2019, 0, 1, 0, 0, 0)),
'D:201913': new Date(Date.UTC(2019, 0, 1, 0, 0, 0)),
'D:201902': new Date(Date.UTC(2019, 1, 1, 0, 0, 0)),
'D:2019020': new Date(Date.UTC(2019, 1, 1, 0, 0, 0)),
'D:20190200': new Date(Date.UTC(2019, 1, 1, 0, 0, 0)),
'D:20190232': new Date(Date.UTC(2019, 1, 1, 0, 0, 0)),
'D:20190203': new Date(Date.UTC(2019, 1, 3, 0, 0, 0)),
// Invalid dates like the 31th of April are handled by JavaScript:
'D:20190431': new Date(Date.UTC(2019, 4, 1, 0, 0, 0)),
'D:201902030': new Date(Date.UTC(2019, 1, 3, 0, 0, 0)),
'D:2019020300': new Date(Date.UTC(2019, 1, 3, 0, 0, 0)),
'D:2019020324': new Date(Date.UTC(2019, 1, 3, 0, 0, 0)),
'D:2019020304': new Date(Date.UTC(2019, 1, 3, 4, 0, 0)),
'D:20190203040': new Date(Date.UTC(2019, 1, 3, 4, 0, 0)),
'D:201902030400': new Date(Date.UTC(2019, 1, 3, 4, 0, 0)),
'D:201902030460': new Date(Date.UTC(2019, 1, 3, 4, 0, 0)),
'D:201902030405': new Date(Date.UTC(2019, 1, 3, 4, 5, 0)),
'D:2019020304050': new Date(Date.UTC(2019, 1, 3, 4, 5, 0)),
'D:20190203040500': new Date(Date.UTC(2019, 1, 3, 4, 5, 0)),
'D:20190203040560': new Date(Date.UTC(2019, 1, 3, 4, 5, 0)),
'D:20190203040506': new Date(Date.UTC(2019, 1, 3, 4, 5, 6)),
'D:20190203040506F': new Date(Date.UTC(2019, 1, 3, 4, 5, 6)),
'D:20190203040506Z': new Date(Date.UTC(2019, 1, 3, 4, 5, 6)),
'D:20190203040506-': new Date(Date.UTC(2019, 1, 3, 4, 5, 6)),
'D:20190203040506+': new Date(Date.UTC(2019, 1, 3, 4, 5, 6)),
'D:20190203040506+\'': new Date(Date.UTC(2019, 1, 3, 4, 5, 6)),
'D:20190203040506+0': new Date(Date.UTC(2019, 1, 3, 4, 5, 6)),
'D:20190203040506+01': new Date(Date.UTC(2019, 1, 3, 3, 5, 6)),
'D:20190203040506+00\'': new Date(Date.UTC(2019, 1, 3, 4, 5, 6)),
'D:20190203040506+24\'': new Date(Date.UTC(2019, 1, 3, 4, 5, 6)),
'D:20190203040506+01\'': new Date(Date.UTC(2019, 1, 3, 3, 5, 6)),
'D:20190203040506+01\'0': new Date(Date.UTC(2019, 1, 3, 3, 5, 6)),
'D:20190203040506+01\'00': new Date(Date.UTC(2019, 1, 3, 3, 5, 6)),
'D:20190203040506+01\'60': new Date(Date.UTC(2019, 1, 3, 3, 5, 6)),
'D:20190203040506+0102': new Date(Date.UTC(2019, 1, 3, 3, 3, 6)),
'D:20190203040506+01\'02': new Date(Date.UTC(2019, 1, 3, 3, 3, 6)),
'D:20190203040506+01\'02\'': new Date(Date.UTC(2019, 1, 3, 3, 3, 6)),
// Offset hour and minute that result in a day change:
'D:20190203040506+05\'07': new Date(Date.UTC(2019, 1, 2, 22, 58, 6)),
};
for (const [input, expectation] of Object.entries(expectations)) {
const result = PDFDateString.toDateObject(input);
if (result) {
expect(result.getTime()).toEqual(expectation.getTime());
} else {
expect(result).toEqual(expectation);
}
}
});
});
});
});

View File

@ -158,25 +158,33 @@
z-index: 200;
max-width: 20em;
background-color: #FFFF99;
box-shadow: 0px 2px 5px #333;
box-shadow: 0px 2px 5px #888;
border-radius: 2px;
padding: 0.6em;
padding: 6px;
margin-left: 5px;
cursor: pointer;
font: message-box;
font-size: 9px;
word-wrap: break-word;
}
.annotationLayer .popup > * {
font-size: 9px;
}
.annotationLayer .popup h1 {
font-size: 1em;
border-bottom: 1px solid #000000;
margin: 0;
padding-bottom: 0.2em;
display: inline-block;
}
.annotationLayer .popup span {
display: inline-block;
margin-left: 5px;
}
.annotationLayer .popup p {
margin: 0;
padding-top: 0.2em;
border-top: 1px solid #333;
margin-top: 2px;
padding-top: 2px;
}
.annotationLayer .highlightAnnotation,

View File

@ -13,10 +13,10 @@
* limitations under the License.
*/
import { createPromiseCapability, PDFDateString } from 'pdfjs-lib';
import {
getPageSizeInches, getPDFFileNameFromURL, isPortraitOrientation, NullL10n
} from './ui_utils';
import { createPromiseCapability } from 'pdfjs-lib';
const DEFAULT_FIELD_CONTENT = '-';
@ -363,50 +363,14 @@ class PDFDocumentProperties {
* @private
*/
_parseDate(inputDate) {
if (!inputDate) {
return;
const dateObject = PDFDateString.toDateObject(inputDate);
if (dateObject) {
const dateString = dateObject.toLocaleDateString();
const timeString = dateObject.toLocaleTimeString();
return this.l10n.get('document_properties_date_string',
{ date: dateString, time: timeString, },
'{{date}}, {{time}}');
}
// This is implemented according to the PDF specification, but note that
// Adobe Reader doesn't handle changing the date to universal time
// and doesn't use the user's time zone (they're effectively ignoring
// the HH' and mm' parts of the date string).
let dateToParse = inputDate;
// Remove the D: prefix if it is available.
if (dateToParse.substring(0, 2) === 'D:') {
dateToParse = dateToParse.substring(2);
}
// Get all elements from the PDF date string.
// JavaScript's `Date` object expects the month to be between
// 0 and 11 instead of 1 and 12, so we're correcting for this.
let year = parseInt(dateToParse.substring(0, 4), 10);
let month = parseInt(dateToParse.substring(4, 6), 10) - 1;
let day = parseInt(dateToParse.substring(6, 8), 10);
let hours = parseInt(dateToParse.substring(8, 10), 10);
let minutes = parseInt(dateToParse.substring(10, 12), 10);
let seconds = parseInt(dateToParse.substring(12, 14), 10);
let utRel = dateToParse.substring(14, 15);
let offsetHours = parseInt(dateToParse.substring(15, 17), 10);
let offsetMinutes = parseInt(dateToParse.substring(18, 20), 10);
// As per spec, utRel = 'Z' means equal to universal time.
// The other cases ('-' and '+') have to be handled here.
if (utRel === '-') {
hours += offsetHours;
minutes += offsetMinutes;
} else if (utRel === '+') {
hours -= offsetHours;
minutes -= offsetMinutes;
}
// Return the new date format from the user's locale.
let date = new Date(Date.UTC(year, month, day, hours, minutes, seconds));
let dateString = date.toLocaleDateString();
let timeString = date.toLocaleTimeString();
return this.l10n.get('document_properties_date_string',
{ date: dateString, time: timeString, },
'{{date}}, {{time}}');
}
/**