From 049848ba009014d5056dd79676028cec91bd2a5c Mon Sep 17 00:00:00 2001 From: Jonas Jenwald Date: Sun, 21 Apr 2024 11:29:03 +0200 Subject: [PATCH] Unify the `ReadableStream` and `TextContent` code-paths in `src/display/text_layer.js` The only reason that this code still accepts `TextContent` is for backward-compatibility purposes, so we can simplify the implementation by always using a `ReadableStream` internally. --- src/display/text_layer.js | 83 ++++++++++++++++++------------------ test/driver.js | 14 +++--- test/unit/text_layer_spec.js | 42 ++++++++++++++++++ 3 files changed, 88 insertions(+), 51 deletions(-) diff --git a/src/display/text_layer.js b/src/display/text_layer.js index 5394f0dc2..03a139cdf 100644 --- a/src/display/text_layer.js +++ b/src/display/text_layer.js @@ -250,9 +250,7 @@ function appendText(task, geom, styles) { textDivProperties.canvasWidth = style.vertical ? geom.height : geom.width; } task._textDivProperties.set(textDiv, textDivProperties); - if (task._isReadableStream) { - task._layoutText(textDiv); - } + task._layoutText(textDiv); } function layout(params) { @@ -298,16 +296,14 @@ function render(task) { capability.resolve(); return; } - - if (!task._isReadableStream) { - for (const textDiv of textDivs) { - task._layoutText(textDiv); - } - } capability.resolve(); } class TextLayerRenderTask { + #reader = null; + + #textContentSource = null; + constructor({ textContentSource, container, @@ -316,14 +312,26 @@ class TextLayerRenderTask { textDivProperties, textContentItemsStr, }) { - this._textContentSource = textContentSource; - this._isReadableStream = textContentSource instanceof ReadableStream; + if (textContentSource instanceof ReadableStream) { + this.#textContentSource = textContentSource; + } else if ( + (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) && + typeof textContentSource === "object" + ) { + this.#textContentSource = new ReadableStream({ + start(controller) { + controller.enqueue(textContentSource); + controller.close(); + }, + }); + } else { + throw new Error('No "textContentSource" parameter specified.'); + } this._container = this._rootContainer = container; this._textDivs = textDivs || []; this._textContentItemsStr = textContentItemsStr || []; this._fontInspectorEnabled = !!globalThis.FontInspector?.enabled; - this._reader = null; this._textDivProperties = textDivProperties || new WeakMap(); this._canceled = false; this._capability = Promise.withResolvers(); @@ -365,15 +373,14 @@ class TextLayerRenderTask { */ cancel() { this._canceled = true; - if (this._reader) { - this._reader - .cancel(new AbortException("TextLayer task cancelled.")) - .catch(() => { - // Avoid "Uncaught promise" messages in the console. - }); - this._reader = null; - } - this._capability.reject(new AbortException("TextLayer task cancelled.")); + const abortEx = new AbortException("TextLayer task cancelled."); + + this.#reader?.cancel(abortEx).catch(() => { + // Avoid "Uncaught promise" messages in the console. + }); + this.#reader = null; + + this._capability.reject(abortEx); } /** @@ -429,29 +436,21 @@ class TextLayerRenderTask { const { promise, resolve, reject } = Promise.withResolvers(); let styleCache = Object.create(null); - if (this._isReadableStream) { - const pump = () => { - this._reader.read().then(({ value, done }) => { - if (done) { - resolve(); - return; - } + const pump = () => { + this.#reader.read().then(({ value, done }) => { + if (done) { + resolve(); + return; + } - Object.assign(styleCache, value.styles); - this._processItems(value.items, styleCache); - pump(); - }, reject); - }; + Object.assign(styleCache, value.styles); + this._processItems(value.items, styleCache); + pump(); + }, reject); + }; - this._reader = this._textContentSource.getReader(); - pump(); - } else if (this._textContentSource) { - const { items, styles } = this._textContentSource; - this._processItems(items, styles); - resolve(); - } else { - throw new Error('No "textContentSource" parameter specified.'); - } + this.#reader = this.#textContentSource.getReader(); + pump(); promise.then(() => { styleCache = null; diff --git a/test/driver.js b/test/driver.js index e47a48a5d..d0a91ce48 100644 --- a/test/driver.js +++ b/test/driver.js @@ -335,15 +335,11 @@ class Rasterize { await task.promise; - const { _pageWidth, _pageHeight, _textContentSource, _textDivs } = task; + const { _pageWidth, _pageHeight, _textDivs } = task; const boxes = []; - let posRegex; - for ( - let i = 0, j = 0, ii = _textContentSource.items.length; - i < ii; - i++ - ) { - const { width, height, type } = _textContentSource.items[i]; + let j = 0, + posRegex; + for (const { width, height, type } of textContent.items) { if (type) { continue; } @@ -396,7 +392,7 @@ class Rasterize { drawLayer.destroy(); } catch (reason) { - throw new Error(`Rasterize.textLayer: "${reason?.message}".`); + throw new Error(`Rasterize.highlightLayer: "${reason?.message}".`); } } diff --git a/test/unit/text_layer_spec.js b/test/unit/text_layer_spec.js index ca4a400e9..b6d93eab7 100644 --- a/test/unit/text_layer_spec.js +++ b/test/unit/text_layer_spec.js @@ -58,5 +58,47 @@ describe("textLayer", function () { "", "page 1 / 3", ]); + + await loadingTask.destroy(); + }); + + it("creates textLayer from TextContent", async function () { + if (isNodeJS) { + pending("document.createElement is not supported in Node.js."); + } + const loadingTask = getDocument(buildGetDocumentParams("basicapi.pdf")); + const pdfDocument = await loadingTask.promise; + const page = await pdfDocument.getPage(1); + + const textContentItemsStr = []; + + const textLayerRenderTask = renderTextLayer({ + textContentSource: await page.getTextContent(), + container: document.createElement("div"), + viewport: page.getViewport({ scale: 1 }), + textContentItemsStr, + }); + expect(textLayerRenderTask instanceof TextLayerRenderTask).toEqual(true); + + await textLayerRenderTask.promise; + expect(textContentItemsStr).toEqual([ + "Table Of Content", + "", + "Chapter 1", + " ", + "..........................................................", + " ", + "2", + "", + "Paragraph 1.1", + " ", + "......................................................", + " ", + "3", + "", + "page 1 / 3", + ]); + + await loadingTask.destroy(); }); });