From be1d6626a7d3f97a8fcd79712622982deb32c6f9 Mon Sep 17 00:00:00 2001 From: Tim van der Meij Date: Sun, 21 Apr 2019 21:21:01 +0200 Subject: [PATCH] Implement creation/modification date for annotations 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. --- l10n/en-US/viewer.properties | 4 + l10n/nl/viewer.properties | 4 + src/core/annotation.js | 45 ++++++++++- src/display/annotation_layer.js | 28 ++++++- src/display/display_utils.js | 88 ++++++++++++++++++++- src/pdf.js | 1 + test/annotation_layer_builder_overrides.css | 6 ++ test/unit/annotation_spec.js | 81 +++++++++++++++++++ test/unit/display_utils_spec.js | 69 +++++++++++++++- web/annotation_layer_builder.css | 24 ++++-- web/pdf_document_properties.js | 52 ++---------- 11 files changed, 343 insertions(+), 59 deletions(-) diff --git a/l10n/en-US/viewer.properties b/l10n/en-US/viewer.properties index 22045e111..2dd7751aa 100644 --- a/l10n/en-US/viewer.properties +++ b/l10n/en-US/viewer.properties @@ -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). diff --git a/l10n/nl/viewer.properties b/l10n/nl/viewer.properties index 7422f8492..c62110a46 100644 --- a/l10n/nl/viewer.properties +++ b/l10n/nl/viewer.properties @@ -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). diff --git a/src/core/annotation.js b/src/core/annotation.js index 742adaf43..a1491afe8 100644 --- a/src/core/annotation.js +++ b/src/core/annotation.js @@ -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; diff --git a/src/display/annotation_layer.js b/src/display/annotation_layer.js index ad6f14e73..6997a51df 100644 --- a/src/display/annotation_layer.js +++ b/src/display/annotation_layer.js @@ -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; } diff --git a/src/display/display_utils.js b/src/display/display_utils.js index 2d1655681..d4def30e8 100644 --- a/src/display/display_utils.js +++ b/src/display/display_utils.js @@ -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, }; diff --git a/src/pdf.js b/src/pdf.js index 32895bae3..4ccf2c1a7 100644 --- a/src/pdf.js +++ b/src/pdf.js @@ -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; diff --git a/test/annotation_layer_builder_overrides.css b/test/annotation_layer_builder_overrides.css index c2d13407a..b1ca12395 100644 --- a/test/annotation_layer_builder_overrides.css +++ b/test/annotation_layer_builder_overrides.css @@ -35,3 +35,9 @@ .annotationLayer .popupWrapper { display: block; } + +.annotationLayer .popup h1, +.annotationLayer .popup p { + margin: 0; + padding: 0; +} diff --git a/test/unit/annotation_spec.js b/test/unit/annotation_spec.js index 9e1af94a2..b26b40de6 100644 --- a/test/unit/annotation_spec.js +++ b/test/unit/annotation_spec.js @@ -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(); diff --git a/test/unit/display_utils_spec.js b/test/unit/display_utils_spec.js index 5dbbb4093..19a090559 100644 --- a/test/unit/display_utils_spec.js +++ b/test/unit/display_utils_spec.js @@ -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); + } + } + }); + }); + }); }); diff --git a/web/annotation_layer_builder.css b/web/annotation_layer_builder.css index 191360f7e..7384f4115 100644 --- a/web/annotation_layer_builder.css +++ b/web/annotation_layer_builder.css @@ -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, diff --git a/web/pdf_document_properties.js b/web/pdf_document_properties.js index 168ac14f2..a73400b2c 100644 --- a/web/pdf_document_properties.js +++ b/web/pdf_document_properties.js @@ -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}}'); } /**