Skip to content

Commit 7780fd5

Browse files
committed
Re-factor PDFDocumentProperties such that it's properly reset when a new PDF file is opened (issue 8371)
This patch contains the following improvements: - Only fetch the various document properties *once* per PDF file opened, and cache the result (in a frozen object). - Always update the *entire* dialog at once, to prevent inconsistent UI state (issue 8371). - Ensure that the dialog, and all its internal properties, are reset when `PDFViewerApplication.close` is called. - Inline, and re-factor, the `getProperties` method in `open`, since that's the only call-site. - Always overwrite the fileSize with the value obtained from `pdfDocument.getDownloadInfo`, to ensure that it's correct. - ES6-ify the code that's touched in this patch. Fixes 8371.
1 parent 50d026f commit 7780fd5

File tree

4 files changed

+121
-85
lines changed

4 files changed

+121
-85
lines changed

web/app.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -561,6 +561,7 @@ var PDFViewerApplication = {
561561
this.pdfThumbnailViewer.setDocument(null);
562562
this.pdfViewer.setDocument(null);
563563
this.pdfLinkService.setDocument(null, null);
564+
this.pdfDocumentProperties.setDocument(null, null);
564565
}
565566
this.store = null;
566567
this.isInitialViewSet = false;
@@ -847,8 +848,6 @@ var PDFViewerApplication = {
847848

848849
this.pdfDocument = pdfDocument;
849850

850-
this.pdfDocumentProperties.setDocumentAndUrl(pdfDocument, this.url);
851-
852851
var downloadedPromise = pdfDocument.getDownloadInfo().then(function() {
853852
self.downloadComplete = true;
854853
self.loadingBar.hide();
@@ -869,6 +868,7 @@ var PDFViewerApplication = {
869868
baseDocumentUrl = location.href.split('#')[0];
870869
}
871870
this.pdfLinkService.setDocument(pdfDocument, baseDocumentUrl);
871+
this.pdfDocumentProperties.setDocument(pdfDocument, this.url);
872872

873873
var pdfViewer = this.pdfViewer;
874874
pdfViewer.currentScale = scale;

web/pdf_document_properties.js

Lines changed: 106 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,12 @@
1313
* limitations under the License.
1414
*/
1515

16-
import { getPDFFileNameFromURL, mozL10n } from './ui_utils';
16+
import { cloneObj, getPDFFileNameFromURL, mozL10n } from './ui_utils';
1717
import { createPromiseCapability } from './pdfjs';
1818
import { OverlayManager } from './overlay_manager';
1919

20+
const DEFAULT_FIELD_CONTENT = '-';
21+
2022
/**
2123
* @typedef {Object} PDFDocumentPropertiesOptions
2224
* @property {string} overlayName - Name/identifier for the overlay.
@@ -29,21 +31,16 @@ class PDFDocumentProperties {
2931
/**
3032
* @param {PDFDocumentPropertiesOptions} options
3133
*/
32-
constructor(options) {
33-
this.overlayName = options.overlayName;
34-
this.fields = options.fields;
35-
this.container = options.container;
34+
constructor({ overlayName, fields, container, closeButton, }) {
35+
this.overlayName = overlayName;
36+
this.fields = fields;
37+
this.container = container;
3638

37-
this.rawFileSize = 0;
38-
this.url = null;
39-
this.pdfDocument = null;
39+
this._reset();
4040

41-
// Bind the event listener for the Close button.
42-
if (options.closeButton) {
43-
options.closeButton.addEventListener('click', this.close.bind(this));
41+
if (closeButton) { // Bind the event listener for the Close button.
42+
closeButton.addEventListener('click', this.close.bind(this));
4443
}
45-
this._dataAvailableCapability = createPromiseCapability();
46-
4744
OverlayManager.register(this.overlayName, this.container,
4845
this.close.bind(this));
4946
}
@@ -52,9 +49,51 @@ class PDFDocumentProperties {
5249
* Open the document properties overlay.
5350
*/
5451
open() {
52+
let freezeFieldData = (data) => {
53+
Object.defineProperty(this, 'fieldData', {
54+
value: Object.freeze(data),
55+
writable: false,
56+
enumerable: true,
57+
configurable: true,
58+
});
59+
};
60+
5561
Promise.all([OverlayManager.open(this.overlayName),
5662
this._dataAvailableCapability.promise]).then(() => {
57-
this._getProperties();
63+
// If the document properties were previously fetched (for this PDF file),
64+
// just update the dialog immediately to avoid redundant lookups.
65+
if (this.fieldData) {
66+
this._updateUI();
67+
return;
68+
}
69+
// Get the document properties.
70+
this.pdfDocument.getMetadata().then(({ info, metadata, }) => {
71+
freezeFieldData({
72+
'fileName': getPDFFileNameFromURL(this.url),
73+
'fileSize': this._parseFileSize(this.maybeFileSize),
74+
'title': info.Title,
75+
'author': info.Author,
76+
'subject': info.Subject,
77+
'keywords': info.Keywords,
78+
'creationDate': this._parseDate(info.CreationDate),
79+
'modificationDate': this._parseDate(info.ModDate),
80+
'creator': info.Creator,
81+
'producer': info.Producer,
82+
'version': info.PDFFormatVersion,
83+
'pageCount': this.pdfDocument.numPages,
84+
});
85+
this._updateUI();
86+
87+
// Get the correct fileSize, since it may not have been set (if
88+
// `this.setFileSize` wasn't called) or may be incorrectly set.
89+
return this.pdfDocument.getDownloadInfo();
90+
}).then(({ length, }) => {
91+
let data = cloneObj(this.fieldData);
92+
data['fileSize'] = this._parseFileSize(length);
93+
94+
freezeFieldData(data);
95+
this._updateUI();
96+
});
5897
});
5998
}
6099

@@ -65,19 +104,6 @@ class PDFDocumentProperties {
65104
OverlayManager.close(this.overlayName);
66105
}
67106

68-
/**
69-
* Set the file size of the PDF document. This method is used to
70-
* update the file size in the document properties overlay once it
71-
* is known so we do not have to wait until the entire file is loaded.
72-
*
73-
* @param {number} fileSize - The file size of the PDF document.
74-
*/
75-
setFileSize(fileSize) {
76-
if (fileSize > 0) {
77-
this.rawFileSize = fileSize;
78-
}
79-
}
80-
81107
/**
82108
* Set a reference to the PDF document and the URL in order
83109
* to populate the overlay fields with the document properties.
@@ -87,68 +113,75 @@ class PDFDocumentProperties {
87113
* @param {Object} pdfDocument - A reference to the PDF document.
88114
* @param {string} url - The URL of the document.
89115
*/
90-
setDocumentAndUrl(pdfDocument, url) {
116+
setDocument(pdfDocument, url) {
117+
if (this.pdfDocument) {
118+
this._reset();
119+
this._updateUI(true);
120+
}
121+
if (!pdfDocument) {
122+
return;
123+
}
91124
this.pdfDocument = pdfDocument;
92125
this.url = url;
126+
93127
this._dataAvailableCapability.resolve();
94128
}
95129

96130
/**
97-
* @private
131+
* Set the file size of the PDF document. This method is used to
132+
* update the file size in the document properties overlay once it
133+
* is known so we do not have to wait until the entire file is loaded.
134+
*
135+
* @param {number} fileSize - The file size of the PDF document.
98136
*/
99-
_getProperties() {
100-
if (!OverlayManager.active) {
101-
// If the dialog was closed before `_dataAvailableCapability` was
102-
// resolved, don't bother updating the properties.
103-
return;
137+
setFileSize(fileSize) {
138+
if (typeof fileSize === 'number' && fileSize > 0) {
139+
this.maybeFileSize = fileSize;
104140
}
105-
// Get the file size (if it hasn't already been set).
106-
this.pdfDocument.getDownloadInfo().then((data) => {
107-
if (data.length === this.rawFileSize) {
108-
return;
109-
}
110-
this.setFileSize(data.length);
111-
this._updateUI(this.fields['fileSize'], this._parseFileSize());
112-
});
141+
}
113142

114-
// Get the document properties.
115-
this.pdfDocument.getMetadata().then((data) => {
116-
var content = {
117-
'fileName': getPDFFileNameFromURL(this.url),
118-
'fileSize': this._parseFileSize(),
119-
'title': data.info.Title,
120-
'author': data.info.Author,
121-
'subject': data.info.Subject,
122-
'keywords': data.info.Keywords,
123-
'creationDate': this._parseDate(data.info.CreationDate),
124-
'modificationDate': this._parseDate(data.info.ModDate),
125-
'creator': data.info.Creator,
126-
'producer': data.info.Producer,
127-
'version': data.info.PDFFormatVersion,
128-
'pageCount': this.pdfDocument.numPages
129-
};
130-
131-
// Show the properties in the dialog.
132-
for (var identifier in content) {
133-
this._updateUI(this.fields[identifier], content[identifier]);
134-
}
135-
});
143+
/**
144+
* @private
145+
*/
146+
_reset() {
147+
this.pdfDocument = null;
148+
this.url = null;
149+
150+
this.maybeFileSize = 0;
151+
delete this.fieldData;
152+
this._dataAvailableCapability = createPromiseCapability();
136153
}
137154

138155
/**
156+
* Always updates all of the dialog fields, to prevent inconsistent UI state.
157+
* NOTE: If the contents of a particular field is neither a non-empty string,
158+
* nor a number, it will fall back to `DEFAULT_FIELD_CONTENT`.
139159
* @private
140160
*/
141-
_updateUI(field, content) {
142-
if (field && content !== undefined && content !== '') {
143-
field.textContent = content;
161+
_updateUI(reset = false) {
162+
if (reset || !this.fieldData) {
163+
for (let id in this.fields) {
164+
this.fields[id].textContent = DEFAULT_FIELD_CONTENT;
165+
}
166+
return;
167+
}
168+
if (OverlayManager.active !== this.overlayName) {
169+
// Don't bother updating the dialog if has already been closed,
170+
// since it will be updated the next time `this.open` is called.
171+
return;
172+
}
173+
for (let id in this.fields) {
174+
let content = this.fieldData[id];
175+
this.fields[id].textContent = (content || content === 0) ?
176+
content : DEFAULT_FIELD_CONTENT;
144177
}
145178
}
146179

147180
/**
148181
* @private
149182
*/
150-
_parseFileSize() {
151-
var fileSize = this.rawFileSize, kb = fileSize / 1024;
183+
_parseFileSize(fileSize = 0) {
184+
let kb = fileSize / 1024;
152185
if (!kb) {
153186
return;
154187
} else if (kb < 1024) {
@@ -167,14 +200,14 @@ class PDFDocumentProperties {
167200
* @private
168201
*/
169202
_parseDate(inputDate) {
203+
if (!inputDate) {
204+
return;
205+
}
170206
// This is implemented according to the PDF specification, but note that
171207
// Adobe Reader doesn't handle changing the date to universal time
172208
// and doesn't use the user's time zone (they're effectively ignoring
173209
// the HH' and mm' parts of the date string).
174-
var dateToParse = inputDate;
175-
if (dateToParse === undefined) {
176-
return '';
177-
}
210+
let dateToParse = inputDate;
178211

179212
// Remove the D: prefix if it is available.
180213
if (dateToParse.substring(0, 2) === 'D:') {

web/preferences.js

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
* limitations under the License.
1414
*/
1515

16+
import { cloneObj } from './ui_utils';
17+
1618
var defaultPreferences = null;
1719
function getDefaultPreferences() {
1820
if (!defaultPreferences) {
@@ -38,16 +40,6 @@ function getDefaultPreferences() {
3840
return defaultPreferences;
3941
}
4042

41-
function cloneObj(obj) {
42-
var result = {};
43-
for (var i in obj) {
44-
if (Object.prototype.hasOwnProperty.call(obj, i)) {
45-
result[i] = obj[i];
46-
}
47-
}
48-
return result;
49-
}
50-
5143
/**
5244
* BasePreferences - Abstract base class for storing persistent settings.
5345
* Used for settings that should be applied to all opened documents,

web/ui_utils.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -422,6 +422,16 @@ function normalizeWheelEventDelta(evt) {
422422
return delta;
423423
}
424424

425+
function cloneObj(obj) {
426+
var result = {};
427+
for (var i in obj) {
428+
if (Object.prototype.hasOwnProperty.call(obj, i)) {
429+
result[i] = obj[i];
430+
}
431+
}
432+
return result;
433+
}
434+
425435
/**
426436
* Promise that is resolved when DOM window becomes visible.
427437
*/
@@ -582,6 +592,7 @@ export {
582592
MAX_AUTO_SCALE,
583593
SCROLLBAR_PADDING,
584594
VERTICAL_PADDING,
595+
cloneObj,
585596
RendererType,
586597
mozL10n,
587598
EventBus,

0 commit comments

Comments
 (0)