diff --git a/src/display/api.js b/src/display/api.js index 2ae61ea2904e1..ef0cb9ed53cde 100644 --- a/src/display/api.js +++ b/src/display/api.js @@ -419,7 +419,16 @@ function getDocument(src) { PDFJSDev.test("GENERIC") && isNodeJS ) { - return new PDFNodeStream(params); + const isFetchSupported = function () { + return ( + typeof fetch !== "undefined" && + typeof Response !== "undefined" && + "body" in Response.prototype + ); + }; + return isFetchSupported() && isValidFetchUrl(params.url) + ? new PDFFetchStream(params) + : new PDFNodeStream(params); } return isValidFetchUrl(params.url) ? new PDFFetchStream(params) @@ -762,6 +771,9 @@ class PDFDocumentProxy { if (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) { // For testing purposes. + Object.defineProperty(this, "getNetworkStreamName", { + value: () => this._transport.getNetworkStreamName(), + }); Object.defineProperty(this, "getXFADatasets", { value: () => this._transport.getXFADatasets(), }); @@ -2344,6 +2356,9 @@ class WorkerTransport { if (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) { // For testing purposes. + Object.defineProperty(this, "getNetworkStreamName", { + value: () => networkStream?.constructor?.name || null, + }); Object.defineProperty(this, "getXFADatasets", { value: () => this.messageHandler.sendWithPromise("GetXFADatasets", null), diff --git a/test/unit/api_spec.js b/test/unit/api_spec.js index 0035c0c6532ee..6870ddb8bf238 100644 --- a/test/unit/api_spec.js +++ b/test/unit/api_spec.js @@ -32,6 +32,7 @@ import { import { buildGetDocumentParams, CMAP_URL, + createTemporaryNodeServer, DefaultFileReaderFactory, TEST_PDFS_PATH, } from "./test_utils.js"; @@ -67,13 +68,27 @@ describe("api", function () { buildGetDocumentParams(tracemonkeyFileName); let CanvasFactory; + let tempServer = null; beforeAll(function () { CanvasFactory = new DefaultCanvasFactory(); + + if (isNodeJS) { + tempServer = createTemporaryNodeServer(); + } }); afterAll(function () { CanvasFactory = null; + + if (isNodeJS) { + // Close the server from accepting new connections after all test + // finishes. + const { server } = tempServer; + server.close(); + + tempServer = null; + } }); function waitSome(callback) { @@ -119,13 +134,10 @@ describe("api", function () { }); it("creates pdf doc from URL-object", async function () { - if (isNodeJS) { - pending("window.location is not supported in Node.js."); - } - const urlObj = new URL( - TEST_PDFS_PATH + basicApiFileName, - window.location - ); + const urlObj = isNodeJS + ? new URL(`http://127.0.0.1:${tempServer.port}/${basicApiFileName}`) + : new URL(TEST_PDFS_PATH + basicApiFileName, window.location); + const loadingTask = getDocument(urlObj); expect(loadingTask instanceof PDFDocumentLoadingTask).toEqual(true); const pdfDocument = await loadingTask.promise; @@ -134,6 +146,9 @@ describe("api", function () { expect(pdfDocument instanceof PDFDocumentProxy).toEqual(true); expect(pdfDocument.numPages).toEqual(3); + // Ensure that the Fetch API was used to load the PDF document. + expect(pdfDocument.getNetworkStreamName()).toEqual("PDFFetchStream"); + await loadingTask.destroy(); }); diff --git a/test/unit/clitests.json b/test/unit/clitests.json index 6ea14e1ed08b2..acf5e06e8d919 100644 --- a/test/unit/clitests.json +++ b/test/unit/clitests.json @@ -21,6 +21,7 @@ "encodings_spec.js", "evaluator_spec.js", "event_utils_spec.js", + "fetch_stream_spec.js", "font_substitutions_spec.js", "function_spec.js", "message_handler_spec.js", diff --git a/test/unit/fetch_stream_spec.js b/test/unit/fetch_stream_spec.js index 2ff1b41ec6735..75bda5d2caf4f 100644 --- a/test/unit/fetch_stream_spec.js +++ b/test/unit/fetch_stream_spec.js @@ -13,16 +13,40 @@ * limitations under the License. */ -import { AbortException } from "../../src/shared/util.js"; +import { AbortException, isNodeJS } from "../../src/shared/util.js"; +import { createTemporaryNodeServer } from "./test_utils.js"; import { PDFFetchStream } from "../../src/display/fetch_stream.js"; describe("fetch_stream", function () { - const pdfUrl = new URL("../pdfs/tracemonkey.pdf", window.location).href; + let tempServer = null; + + function getPdfUrl() { + return isNodeJS + ? `http://127.0.0.1:${tempServer.port}/tracemonkey.pdf` + : new URL("../pdfs/tracemonkey.pdf", window.location).href; + } const pdfLength = 1016315; + beforeAll(function () { + if (isNodeJS) { + tempServer = createTemporaryNodeServer(); + } + }); + + afterAll(function () { + if (isNodeJS) { + // Close the server from accepting new connections after all test + // finishes. + const { server } = tempServer; + server.close(); + + tempServer = null; + } + }); + it("read with streaming", async function () { const stream = new PDFFetchStream({ - url: pdfUrl, + url: getPdfUrl(), disableStream: false, disableRange: true, }); @@ -57,7 +81,7 @@ describe("fetch_stream", function () { it("read ranges with streaming", async function () { const rangeSize = 32768; const stream = new PDFFetchStream({ - url: pdfUrl, + url: getPdfUrl(), rangeChunkSize: rangeSize, disableStream: false, disableRange: false, diff --git a/test/unit/node_stream_spec.js b/test/unit/node_stream_spec.js index 6158aa9b73c53..f3a2daa754545 100644 --- a/test/unit/node_stream_spec.js +++ b/test/unit/node_stream_spec.js @@ -14,6 +14,7 @@ */ import { AbortException, isNodeJS } from "../../src/shared/util.js"; +import { createTemporaryNodeServer } from "./test_utils.js"; import { PDFNodeStream } from "../../src/display/node_stream.js"; // Ensure that these tests only run in Node.js environments. @@ -25,12 +26,10 @@ if (!isNodeJS) { const path = await __non_webpack_import__("path"); const url = await __non_webpack_import__("url"); -const http = await __non_webpack_import__("http"); -const fs = await __non_webpack_import__("fs"); describe("node_stream", function () { - let server = null; - let port = null; + let tempServer = null; + const pdf = url.parse( encodeURI( "file://" + path.join(process.cwd(), "./test/pdfs/tracemonkey.pdf") @@ -39,50 +38,20 @@ describe("node_stream", function () { const pdfLength = 1016315; beforeAll(function () { - // Create http server to serve pdf data for tests. - server = http - .createServer((request, response) => { - const filePath = process.cwd() + "/test/pdfs" + request.url; - fs.lstat(filePath, (error, stat) => { - if (error) { - response.writeHead(404); - response.end(`File ${request.url} not found!`); - return; - } - if (!request.headers.range) { - const contentLength = stat.size; - const stream = fs.createReadStream(filePath); - response.writeHead(200, { - "Content-Type": "application/pdf", - "Content-Length": contentLength, - "Accept-Ranges": "bytes", - }); - stream.pipe(response); - } else { - const [start, end] = request.headers.range - .split("=")[1] - .split("-") - .map(x => Number(x)); - const stream = fs.createReadStream(filePath, { start, end }); - response.writeHead(206, { - "Content-Type": "application/pdf", - }); - stream.pipe(response); - } - }); - }) - .listen(0); /* Listen on a random free port */ - port = server.address().port; + tempServer = createTemporaryNodeServer(); }); afterAll(function () { // Close the server from accepting new connections after all test finishes. + const { server } = tempServer; server.close(); + + tempServer = null; }); it("read both http(s) and filesystem pdf files", async function () { const stream1 = new PDFNodeStream({ - url: `http://127.0.0.1:${port}/tracemonkey.pdf`, + url: `http://127.0.0.1:${tempServer.port}/tracemonkey.pdf`, rangeChunkSize: 65536, disableStream: true, disableRange: true, @@ -144,7 +113,7 @@ describe("node_stream", function () { it("read custom ranges for both http(s) and filesystem urls", async function () { const rangeSize = 32768; const stream1 = new PDFNodeStream({ - url: `http://127.0.0.1:${port}/tracemonkey.pdf`, + url: `http://127.0.0.1:${tempServer.port}/tracemonkey.pdf`, length: pdfLength, rangeChunkSize: rangeSize, disableStream: true, diff --git a/test/unit/test_utils.js b/test/unit/test_utils.js index 111c050069c66..bc1d839d5eadd 100644 --- a/test/unit/test_utils.js +++ b/test/unit/test_utils.js @@ -13,15 +13,16 @@ * limitations under the License. */ +import { assert, isNodeJS } from "../../src/shared/util.js"; import { NullStream, StringStream } from "../../src/core/stream.js"; import { Page, PDFDocument } from "../../src/core/document.js"; -import { isNodeJS } from "../../src/shared/util.js"; import { Ref } from "../../src/core/primitives.js"; -let fs; +let fs, http; if (isNodeJS) { // Native packages. fs = await __non_webpack_import__("fs"); + http = await __non_webpack_import__("http"); } const TEST_PDFS_PATH = isNodeJS ? "./test/pdfs/" : "../pdfs/"; @@ -144,10 +145,54 @@ function createIdFactory(pageIndex) { return page._localIdFactory; } +function createTemporaryNodeServer() { + assert(isNodeJS, "Should only be used in Node.js environments."); + + // Create http server to serve pdf data for tests. + const server = http + .createServer((request, response) => { + const filePath = process.cwd() + "/test/pdfs" + request.url; + fs.lstat(filePath, (error, stat) => { + if (error) { + response.writeHead(404); + response.end(`File ${request.url} not found!`); + return; + } + if (!request.headers.range) { + const contentLength = stat.size; + const stream = fs.createReadStream(filePath); + response.writeHead(200, { + "Content-Type": "application/pdf", + "Content-Length": contentLength, + "Accept-Ranges": "bytes", + }); + stream.pipe(response); + } else { + const [start, end] = request.headers.range + .split("=")[1] + .split("-") + .map(x => Number(x)); + const stream = fs.createReadStream(filePath, { start, end }); + response.writeHead(206, { + "Content-Type": "application/pdf", + }); + stream.pipe(response); + } + }); + }) + .listen(0); /* Listen on a random free port */ + + return { + server, + port: server.address().port, + }; +} + export { buildGetDocumentParams, CMAP_URL, createIdFactory, + createTemporaryNodeServer, DefaultFileReaderFactory, STANDARD_FONT_DATA_URL, TEST_PDFS_PATH,