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.
This commit is contained in:
Jonas Jenwald 2024-04-21 11:29:03 +02:00
parent b6765403a1
commit 049848ba00
3 changed files with 88 additions and 51 deletions

View file

@ -250,9 +250,7 @@ function appendText(task, geom, styles) {
textDivProperties.canvasWidth = style.vertical ? geom.height : geom.width; textDivProperties.canvasWidth = style.vertical ? geom.height : geom.width;
} }
task._textDivProperties.set(textDiv, textDivProperties); task._textDivProperties.set(textDiv, textDivProperties);
if (task._isReadableStream) {
task._layoutText(textDiv); task._layoutText(textDiv);
}
} }
function layout(params) { function layout(params) {
@ -298,16 +296,14 @@ function render(task) {
capability.resolve(); capability.resolve();
return; return;
} }
if (!task._isReadableStream) {
for (const textDiv of textDivs) {
task._layoutText(textDiv);
}
}
capability.resolve(); capability.resolve();
} }
class TextLayerRenderTask { class TextLayerRenderTask {
#reader = null;
#textContentSource = null;
constructor({ constructor({
textContentSource, textContentSource,
container, container,
@ -316,14 +312,26 @@ class TextLayerRenderTask {
textDivProperties, textDivProperties,
textContentItemsStr, textContentItemsStr,
}) { }) {
this._textContentSource = textContentSource; if (textContentSource instanceof ReadableStream) {
this._isReadableStream = 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._container = this._rootContainer = container;
this._textDivs = textDivs || []; this._textDivs = textDivs || [];
this._textContentItemsStr = textContentItemsStr || []; this._textContentItemsStr = textContentItemsStr || [];
this._fontInspectorEnabled = !!globalThis.FontInspector?.enabled; this._fontInspectorEnabled = !!globalThis.FontInspector?.enabled;
this._reader = null;
this._textDivProperties = textDivProperties || new WeakMap(); this._textDivProperties = textDivProperties || new WeakMap();
this._canceled = false; this._canceled = false;
this._capability = Promise.withResolvers(); this._capability = Promise.withResolvers();
@ -365,15 +373,14 @@ class TextLayerRenderTask {
*/ */
cancel() { cancel() {
this._canceled = true; this._canceled = true;
if (this._reader) { const abortEx = new AbortException("TextLayer task cancelled.");
this._reader
.cancel(new AbortException("TextLayer task cancelled.")) this.#reader?.cancel(abortEx).catch(() => {
.catch(() => {
// Avoid "Uncaught promise" messages in the console. // Avoid "Uncaught promise" messages in the console.
}); });
this._reader = null; this.#reader = null;
}
this._capability.reject(new AbortException("TextLayer task cancelled.")); this._capability.reject(abortEx);
} }
/** /**
@ -429,9 +436,8 @@ class TextLayerRenderTask {
const { promise, resolve, reject } = Promise.withResolvers(); const { promise, resolve, reject } = Promise.withResolvers();
let styleCache = Object.create(null); let styleCache = Object.create(null);
if (this._isReadableStream) {
const pump = () => { const pump = () => {
this._reader.read().then(({ value, done }) => { this.#reader.read().then(({ value, done }) => {
if (done) { if (done) {
resolve(); resolve();
return; return;
@ -443,15 +449,8 @@ class TextLayerRenderTask {
}, reject); }, reject);
}; };
this._reader = this._textContentSource.getReader(); this.#reader = this.#textContentSource.getReader();
pump(); pump();
} else if (this._textContentSource) {
const { items, styles } = this._textContentSource;
this._processItems(items, styles);
resolve();
} else {
throw new Error('No "textContentSource" parameter specified.');
}
promise.then(() => { promise.then(() => {
styleCache = null; styleCache = null;

View file

@ -335,15 +335,11 @@ class Rasterize {
await task.promise; await task.promise;
const { _pageWidth, _pageHeight, _textContentSource, _textDivs } = task; const { _pageWidth, _pageHeight, _textDivs } = task;
const boxes = []; const boxes = [];
let posRegex; let j = 0,
for ( posRegex;
let i = 0, j = 0, ii = _textContentSource.items.length; for (const { width, height, type } of textContent.items) {
i < ii;
i++
) {
const { width, height, type } = _textContentSource.items[i];
if (type) { if (type) {
continue; continue;
} }
@ -396,7 +392,7 @@ class Rasterize {
drawLayer.destroy(); drawLayer.destroy();
} catch (reason) { } catch (reason) {
throw new Error(`Rasterize.textLayer: "${reason?.message}".`); throw new Error(`Rasterize.highlightLayer: "${reason?.message}".`);
} }
} }

View file

@ -58,5 +58,47 @@ describe("textLayer", function () {
"", "",
"page 1 / 3", "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();
}); });
}); });