diff --git a/src/core/image_utils.js b/src/core/image_utils.js index 8cef91dc0..1dfb5b740 100644 --- a/src/core/image_utils.js +++ b/src/core/image_utils.js @@ -13,7 +13,13 @@ * limitations under the License. */ -import { assert, shadow, unreachable, warn } from "../shared/util.js"; +import { + assert, + MAX_IMAGE_SIZE_TO_CACHE, + shadow, + unreachable, + warn, +} from "../shared/util.js"; import { RefSetCache } from "./primitives.js"; class BaseLocalCache { @@ -160,7 +166,7 @@ class GlobalImageCache { } static get MAX_BYTE_SIZE() { - return shadow(this, "MAX_BYTE_SIZE", /* Forty megabytes = */ 40e6); + return shadow(this, "MAX_BYTE_SIZE", 5 * MAX_IMAGE_SIZE_TO_CACHE); } constructor() { diff --git a/src/display/api.js b/src/display/api.js index 9f0cc7caf..20c78009e 100644 --- a/src/display/api.js +++ b/src/display/api.js @@ -26,6 +26,7 @@ import { info, InvalidPDFException, isArrayBuffer, + MAX_IMAGE_SIZE_TO_CACHE, MissingPDFException, PasswordException, RenderingIntentFlag, @@ -66,6 +67,7 @@ import { XfaText } from "./xfa_text.js"; const DEFAULT_RANGE_CHUNK_SIZE = 65536; // 2^16 = 65536 const RENDERING_CANCELLED_TIMEOUT = 100; // ms +const DELAYED_CLEANUP_TIMEOUT = 5000; // ms let DefaultCanvasFactory = DOMCanvasFactory; let DefaultCMapReaderFactory = DOMCMapReaderFactory; @@ -1262,6 +1264,10 @@ class PDFDocumentProxy { * Proxy to a `PDFPage` in the worker thread. */ class PDFPageProxy { + #delayedCleanupTimeout = null; + + #pendingCleanup = false; + constructor(pageIndex, pageInfo, transport, pdfBug = false) { this._pageIndex = pageIndex; this._pageInfo = pageInfo; @@ -1272,8 +1278,7 @@ class PDFPageProxy { this.commonObjs = transport.commonObjs; this.objs = new PDFObjects(); - this.cleanupAfterRender = false; - this.pendingCleanup = false; + this._maybeCleanupAfterRender = false; this._intentStates = new Map(); this.destroyed = false; } @@ -1413,8 +1418,10 @@ class PDFPageProxy { printAnnotationStorage ); // If there was a pending destroy, cancel it so no cleanup happens during - // this call to render. - this.pendingCleanup = false; + // this call to render... + this.#pendingCleanup = false; + // ... and ensure that a delayed cleanup is always aborted. + this.#abortDelayedCleanup(); if (!optionalContentConfigPromise) { optionalContentConfigPromise = this._transport.getOptionalContentConfig(); @@ -1455,11 +1462,11 @@ class PDFPageProxy { intentState.renderTasks.delete(internalRenderTask); // Attempt to reduce memory usage during *printing*, by always running - // cleanup once rendering has finished (regardless of cleanupAfterRender). - if (this.cleanupAfterRender || intentPrint) { - this.pendingCleanup = true; + // cleanup immediately once rendering has finished. + if (this._maybeCleanupAfterRender || intentPrint) { + this.#pendingCleanup = true; } - this._tryCleanup(); + this.#tryCleanup(/* delayed = */ !intentPrint); if (error) { internalRenderTask.capability.reject(error); @@ -1509,7 +1516,7 @@ class PDFPageProxy { { transparency, isOffscreenCanvasSupported }, optionalContentConfig, ]) => { - if (this.pendingCleanup) { + if (this.#pendingCleanup) { complete(); return; } @@ -1681,7 +1688,9 @@ class PDFPageProxy { } } this.objs.clear(); - this.pendingCleanup = false; + this.#pendingCleanup = false; + this.#abortDelayedCleanup(); + return Promise.all(waitOn); } @@ -1693,16 +1702,34 @@ class PDFPageProxy { * @returns {boolean} Indicates if clean-up was successfully run. */ cleanup(resetStats = false) { - this.pendingCleanup = true; - return this._tryCleanup(resetStats); + this.#pendingCleanup = true; + const success = this.#tryCleanup(/* delayed = */ false); + + if (resetStats && success) { + this._stats &&= new StatTimer(); + } + return success; } /** * Attempts to clean up if rendering is in a state where that's possible. - * @private + * @param {boolean} [delayed] - Delay the cleanup, to e.g. improve zooming + * performance in documents with large images. + * The default value is `false`. + * @returns {boolean} Indicates if clean-up was successfully run. */ - _tryCleanup(resetStats = false) { - if (!this.pendingCleanup) { + #tryCleanup(delayed = false) { + this.#abortDelayedCleanup(); + + if (!this.#pendingCleanup) { + return false; + } + if (delayed) { + this.#delayedCleanupTimeout = setTimeout(() => { + this.#delayedCleanupTimeout = null; + this.#tryCleanup(/* delayed = */ false); + }, DELAYED_CLEANUP_TIMEOUT); + return false; } for (const { renderTasks, operatorList } of this._intentStates.values()) { @@ -1710,16 +1737,19 @@ class PDFPageProxy { return false; } } - this._intentStates.clear(); this.objs.clear(); - if (resetStats && this._stats) { - this._stats = new StatTimer(); - } - this.pendingCleanup = false; + this.#pendingCleanup = false; return true; } + #abortDelayedCleanup() { + if (this.#delayedCleanupTimeout) { + clearTimeout(this.#delayedCleanupTimeout); + this.#delayedCleanupTimeout = null; + } + } + /** * @private */ @@ -1756,7 +1786,7 @@ class PDFPageProxy { } if (operatorListChunk.lastChunk) { - this._tryCleanup(); + this.#tryCleanup(/* delayed = */ true); } } @@ -1814,7 +1844,7 @@ class PDFPageProxy { for (const internalRenderTask of intentState.renderTasks) { internalRenderTask.operatorListChanged(); } - this._tryCleanup(); + this.#tryCleanup(/* delayed = */ true); } if (intentState.displayReadyCapability) { @@ -2782,7 +2812,6 @@ class WorkerTransport { pageProxy.objs.resolve(id, imageData); // Heuristic that will allow us not to store large data. - const MAX_IMAGE_SIZE_TO_STORE = 8000000; if (imageData) { let length; if (imageData.bitmap) { @@ -2792,8 +2821,8 @@ class WorkerTransport { length = imageData.data?.length || 0; } - if (length > MAX_IMAGE_SIZE_TO_STORE) { - pageProxy.cleanupAfterRender = true; + if (length > MAX_IMAGE_SIZE_TO_CACHE) { + pageProxy._maybeCleanupAfterRender = true; } } break; diff --git a/src/shared/util.js b/src/shared/util.js index d500aa9a7..873e26e0c 100644 --- a/src/shared/util.js +++ b/src/shared/util.js @@ -26,6 +26,8 @@ if ( const IDENTITY_MATRIX = [1, 0, 0, 1, 0, 0]; const FONT_IDENTITY_MATRIX = [0.001, 0, 0, 0.001, 0, 0]; +const MAX_IMAGE_SIZE_TO_CACHE = 10e6; // Ten megabytes. + // Represent the percentage of the height of a single-line field over // the font size. Acrobat seems to use this value. const LINE_FACTOR = 1.35; @@ -1060,6 +1062,7 @@ export { isArrayEqual, LINE_DESCENT_FACTOR, LINE_FACTOR, + MAX_IMAGE_SIZE_TO_CACHE, MissingPDFException, objectFromMap, objectSize,