pdf_document_properties.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358
  1. /* Copyright 2012 Mozilla Foundation
  2. *
  3. * Licensed under the Apache License, Version 2.0 (the "License");
  4. * you may not use this file except in compliance with the License.
  5. * You may obtain a copy of the License at
  6. *
  7. * http://www.apache.org/licenses/LICENSE-2.0
  8. *
  9. * Unless required by applicable law or agreed to in writing, software
  10. * distributed under the License is distributed on an "AS IS" BASIS,
  11. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. * See the License for the specific language governing permissions and
  13. * limitations under the License.
  14. */
  15. import { createPromiseCapability, PDFDateString } from "pdfjs-lib";
  16. import { getPageSizeInches, isPortraitOrientation } from "./ui_utils.js";
  17. const DEFAULT_FIELD_CONTENT = "-";
  18. // See https://en.wikibooks.org/wiki/Lentis/Conversion_to_the_Metric_Standard_in_the_United_States
  19. const NON_METRIC_LOCALES = ["en-us", "en-lr", "my"];
  20. // Should use the format: `width x height`, in portrait orientation.
  21. // See https://en.wikipedia.org/wiki/Paper_size
  22. const US_PAGE_NAMES = {
  23. "8.5x11": "Letter",
  24. "8.5x14": "Legal",
  25. };
  26. const METRIC_PAGE_NAMES = {
  27. "297x420": "A3",
  28. "210x297": "A4",
  29. };
  30. function getPageName(size, isPortrait, pageNames) {
  31. const width = isPortrait ? size.width : size.height;
  32. const height = isPortrait ? size.height : size.width;
  33. return pageNames[`${width}x${height}`];
  34. }
  35. /**
  36. * @typedef {Object} PDFDocumentPropertiesOptions
  37. * @property {HTMLDialogElement} dialog - The overlay's DOM element.
  38. * @property {Object} fields - Names and elements of the overlay's fields.
  39. * @property {HTMLButtonElement} closeButton - Button for closing the overlay.
  40. */
  41. class PDFDocumentProperties {
  42. #fieldData = null;
  43. /**
  44. * @param {PDFDocumentPropertiesOptions} options
  45. * @param {OverlayManager} overlayManager - Manager for the viewer overlays.
  46. * @param {EventBus} eventBus - The application event bus.
  47. * @param {IL10n} l10n - Localization service.
  48. * @param {function} fileNameLookup - The function that is used to lookup
  49. * the document fileName.
  50. */
  51. constructor(
  52. { dialog, fields, closeButton },
  53. overlayManager,
  54. eventBus,
  55. l10n,
  56. fileNameLookup
  57. ) {
  58. this.dialog = dialog;
  59. this.fields = fields;
  60. this.overlayManager = overlayManager;
  61. this.l10n = l10n;
  62. this._fileNameLookup = fileNameLookup;
  63. this.#reset();
  64. // Bind the event listener for the Close button.
  65. closeButton.addEventListener("click", this.close.bind(this));
  66. this.overlayManager.register(this.dialog);
  67. eventBus._on("pagechanging", evt => {
  68. this._currentPageNumber = evt.pageNumber;
  69. });
  70. eventBus._on("rotationchanging", evt => {
  71. this._pagesRotation = evt.pagesRotation;
  72. });
  73. this._isNonMetricLocale = true; // The default viewer locale is 'en-us'.
  74. l10n.getLanguage().then(locale => {
  75. this._isNonMetricLocale = NON_METRIC_LOCALES.includes(locale);
  76. });
  77. }
  78. /**
  79. * Open the document properties overlay.
  80. */
  81. async open() {
  82. await Promise.all([
  83. this.overlayManager.open(this.dialog),
  84. this._dataAvailableCapability.promise,
  85. ]);
  86. const currentPageNumber = this._currentPageNumber;
  87. const pagesRotation = this._pagesRotation;
  88. // If the document properties were previously fetched (for this PDF file),
  89. // just update the dialog immediately to avoid redundant lookups.
  90. if (
  91. this.#fieldData &&
  92. currentPageNumber === this.#fieldData._currentPageNumber &&
  93. pagesRotation === this.#fieldData._pagesRotation
  94. ) {
  95. this.#updateUI();
  96. return;
  97. }
  98. // Get the document properties.
  99. const {
  100. info,
  101. /* metadata, */
  102. /* contentDispositionFilename, */
  103. contentLength,
  104. } = await this.pdfDocument.getMetadata();
  105. const [
  106. fileName,
  107. fileSize,
  108. creationDate,
  109. modificationDate,
  110. pageSize,
  111. isLinearized,
  112. ] = await Promise.all([
  113. this._fileNameLookup(),
  114. this.#parseFileSize(contentLength),
  115. this.#parseDate(info.CreationDate),
  116. this.#parseDate(info.ModDate),
  117. this.pdfDocument.getPage(currentPageNumber).then(pdfPage => {
  118. return this.#parsePageSize(getPageSizeInches(pdfPage), pagesRotation);
  119. }),
  120. this.#parseLinearization(info.IsLinearized),
  121. ]);
  122. this.#fieldData = Object.freeze({
  123. fileName,
  124. fileSize,
  125. title: info.Title,
  126. author: info.Author,
  127. subject: info.Subject,
  128. keywords: info.Keywords,
  129. creationDate,
  130. modificationDate,
  131. creator: info.Creator,
  132. producer: info.Producer,
  133. version: info.PDFFormatVersion,
  134. pageCount: this.pdfDocument.numPages,
  135. pageSize,
  136. linearized: isLinearized,
  137. _currentPageNumber: currentPageNumber,
  138. _pagesRotation: pagesRotation,
  139. });
  140. this.#updateUI();
  141. // Get the correct fileSize, since it may not have been available
  142. // or could potentially be wrong.
  143. const { length } = await this.pdfDocument.getDownloadInfo();
  144. if (contentLength === length) {
  145. return; // The fileSize has already been correctly set.
  146. }
  147. const data = Object.assign(Object.create(null), this.#fieldData);
  148. data.fileSize = await this.#parseFileSize(length);
  149. this.#fieldData = Object.freeze(data);
  150. this.#updateUI();
  151. }
  152. /**
  153. * Close the document properties overlay.
  154. */
  155. async close() {
  156. this.overlayManager.close(this.dialog);
  157. }
  158. /**
  159. * Set a reference to the PDF document in order to populate the dialog fields
  160. * with the document properties. Note that the dialog will contain no
  161. * information if this method is not called.
  162. *
  163. * @param {PDFDocumentProxy} pdfDocument - A reference to the PDF document.
  164. */
  165. setDocument(pdfDocument) {
  166. if (this.pdfDocument) {
  167. this.#reset();
  168. this.#updateUI(true);
  169. }
  170. if (!pdfDocument) {
  171. return;
  172. }
  173. this.pdfDocument = pdfDocument;
  174. this._dataAvailableCapability.resolve();
  175. }
  176. #reset() {
  177. this.pdfDocument = null;
  178. this.#fieldData = null;
  179. this._dataAvailableCapability = createPromiseCapability();
  180. this._currentPageNumber = 1;
  181. this._pagesRotation = 0;
  182. }
  183. /**
  184. * Always updates all of the dialog fields, to prevent inconsistent UI state.
  185. * NOTE: If the contents of a particular field is neither a non-empty string,
  186. * nor a number, it will fall back to `DEFAULT_FIELD_CONTENT`.
  187. */
  188. #updateUI(reset = false) {
  189. if (reset || !this.#fieldData) {
  190. for (const id in this.fields) {
  191. this.fields[id].textContent = DEFAULT_FIELD_CONTENT;
  192. }
  193. return;
  194. }
  195. if (this.overlayManager.active !== this.dialog) {
  196. // Don't bother updating the dialog if has already been closed,
  197. // since it will be updated the next time `this.open` is called.
  198. return;
  199. }
  200. for (const id in this.fields) {
  201. const content = this.#fieldData[id];
  202. this.fields[id].textContent =
  203. content || content === 0 ? content : DEFAULT_FIELD_CONTENT;
  204. }
  205. }
  206. async #parseFileSize(fileSize = 0) {
  207. const kb = fileSize / 1024,
  208. mb = kb / 1024;
  209. if (!kb) {
  210. return undefined;
  211. }
  212. return this.l10n.get(`document_properties_${mb >= 1 ? "mb" : "kb"}`, {
  213. size_mb: mb >= 1 && (+mb.toPrecision(3)).toLocaleString(),
  214. size_kb: mb < 1 && (+kb.toPrecision(3)).toLocaleString(),
  215. size_b: fileSize.toLocaleString(),
  216. });
  217. }
  218. async #parsePageSize(pageSizeInches, pagesRotation) {
  219. if (!pageSizeInches) {
  220. return undefined;
  221. }
  222. // Take the viewer rotation into account as well; compare with Adobe Reader.
  223. if (pagesRotation % 180 !== 0) {
  224. pageSizeInches = {
  225. width: pageSizeInches.height,
  226. height: pageSizeInches.width,
  227. };
  228. }
  229. const isPortrait = isPortraitOrientation(pageSizeInches);
  230. let sizeInches = {
  231. width: Math.round(pageSizeInches.width * 100) / 100,
  232. height: Math.round(pageSizeInches.height * 100) / 100,
  233. };
  234. // 1in == 25.4mm; no need to round to 2 decimals for millimeters.
  235. let sizeMillimeters = {
  236. width: Math.round(pageSizeInches.width * 25.4 * 10) / 10,
  237. height: Math.round(pageSizeInches.height * 25.4 * 10) / 10,
  238. };
  239. let rawName =
  240. getPageName(sizeInches, isPortrait, US_PAGE_NAMES) ||
  241. getPageName(sizeMillimeters, isPortrait, METRIC_PAGE_NAMES);
  242. if (
  243. !rawName &&
  244. !(
  245. Number.isInteger(sizeMillimeters.width) &&
  246. Number.isInteger(sizeMillimeters.height)
  247. )
  248. ) {
  249. // Attempt to improve the page name detection by falling back to fuzzy
  250. // matching of the metric dimensions, to account for e.g. rounding errors
  251. // and/or PDF files that define the page sizes in an imprecise manner.
  252. const exactMillimeters = {
  253. width: pageSizeInches.width * 25.4,
  254. height: pageSizeInches.height * 25.4,
  255. };
  256. const intMillimeters = {
  257. width: Math.round(sizeMillimeters.width),
  258. height: Math.round(sizeMillimeters.height),
  259. };
  260. // Try to avoid false positives, by only considering "small" differences.
  261. if (
  262. Math.abs(exactMillimeters.width - intMillimeters.width) < 0.1 &&
  263. Math.abs(exactMillimeters.height - intMillimeters.height) < 0.1
  264. ) {
  265. rawName = getPageName(intMillimeters, isPortrait, METRIC_PAGE_NAMES);
  266. if (rawName) {
  267. // Update *both* sizes, computed above, to ensure that the displayed
  268. // dimensions always correspond to the detected page name.
  269. sizeInches = {
  270. width: Math.round((intMillimeters.width / 25.4) * 100) / 100,
  271. height: Math.round((intMillimeters.height / 25.4) * 100) / 100,
  272. };
  273. sizeMillimeters = intMillimeters;
  274. }
  275. }
  276. }
  277. const [{ width, height }, unit, name, orientation] = await Promise.all([
  278. this._isNonMetricLocale ? sizeInches : sizeMillimeters,
  279. this.l10n.get(
  280. `document_properties_page_size_unit_${
  281. this._isNonMetricLocale ? "inches" : "millimeters"
  282. }`
  283. ),
  284. rawName &&
  285. this.l10n.get(
  286. `document_properties_page_size_name_${rawName.toLowerCase()}`
  287. ),
  288. this.l10n.get(
  289. `document_properties_page_size_orientation_${
  290. isPortrait ? "portrait" : "landscape"
  291. }`
  292. ),
  293. ]);
  294. return this.l10n.get(
  295. `document_properties_page_size_dimension_${name ? "name_" : ""}string`,
  296. {
  297. width: width.toLocaleString(),
  298. height: height.toLocaleString(),
  299. unit,
  300. name,
  301. orientation,
  302. }
  303. );
  304. }
  305. async #parseDate(inputDate) {
  306. const dateObject = PDFDateString.toDateObject(inputDate);
  307. if (!dateObject) {
  308. return undefined;
  309. }
  310. return this.l10n.get("document_properties_date_string", {
  311. date: dateObject.toLocaleDateString(),
  312. time: dateObject.toLocaleTimeString(),
  313. });
  314. }
  315. #parseLinearization(isLinearized) {
  316. return this.l10n.get(
  317. `document_properties_linearized_${isLinearized ? "yes" : "no"}`
  318. );
  319. }
  320. }
  321. export { PDFDocumentProperties };