UNPKG

@ironsoftware/ironpdf

Version:

IronPDF for Node

338 lines (304 loc) 10.3 kB
import * as grpc from "@grpc/grpc-js"; import {Buffer} from "buffer"; import * as fs from "fs"; import * as os from "os"; import * as path from "path"; import {PassThrough, Readable} from "stream"; import {Access} from "../../access"; import {IronPdfServiceClient} from "../../generated_proto/ironpdfengineproto/IronPdfService"; import {BytesResultStreamP__Output} from "../../generated_proto/ironpdfengineproto/BytesResultStreamP"; import {BooleanResultP__Output} from "../../generated_proto/ironpdfengineproto/BooleanResultP"; import { QPdfIsLinearizedRequestStreamP } from "../../generated_proto/ironpdfengineproto/QPdfIsLinearizedRequestStreamP"; import { QPdfLinearizeInMemoryRequestStreamP } from "../../generated_proto/ironpdfengineproto/QPdfLinearizeInMemoryRequestStreamP"; import { QPdfSaveAsLinearizedFromBytesRequestStreamP } from "../../generated_proto/ironpdfengineproto/QPdfSaveAsLinearizedFromBytesRequestStreamP"; import {LinearizationMode} from "../../../public/render"; import {chunkBuffer, handleRemoteException} from "../util"; /** * Check if the given PDF bytes represent a linearized ("Fast Web View") PDF. */ export async function isLinearizedFromBytes( pdfBytes: Buffer, password = "" ): Promise<boolean> { const client: IronPdfServiceClient = await Access.ensureConnection(); return new Promise( (resolve: (_: boolean) => void, reject: (errorMsg: string) => void) => { const stream: grpc.ClientWritableStream<QPdfIsLinearizedRequestStreamP> = client.QPdf_Linearization_IsLinearized( (err: grpc.ServiceError | null, value: BooleanResultP__Output | undefined) => { if (err) { reject(`${err.name}/n${err.message}`); } else if (value) { if (value.exception) { handleRemoteException(value.exception, reject); return; } resolve(value.result ?? false); } else { reject("No response from IronPdfEngine for isLinearized"); } } ); stream.write({info: {password: password ?? ""}}); chunkBuffer(pdfBytes).forEach((chunk) => { stream.write({pdfBytesChunk: chunk}); }); stream.end(); } ); } /** * Linearize a PDF held by the engine (by document id) and return the linearized bytes * via the {@code QPdf_Linearization_LinearizeInMemoryFromId} unary-request/server-streaming RPC. */ export async function linearizeInMemoryFromId( id: string, password = "" ): Promise<Buffer> { const client: IronPdfServiceClient = await Access.ensureConnection(); return new Promise( (resolve: (_: Buffer) => void, reject: (errorMsg: string) => void) => { const stream: grpc.ClientReadableStream<BytesResultStreamP__Output> = client.QPdf_Linearization_LinearizeInMemoryFromId({ document: {documentId: id}, password: password ?? "", }); const buffers: Buffer[] = []; stream.on("data", (data: BytesResultStreamP__Output) => { if (data.exception) { handleRemoteException(data.exception, reject); } else if (data.resultChunk) { buffers.push(data.resultChunk); } }); stream.on("error", (err: Error) => { reject(`${err.name}/n${err.message}`); }); stream.on("end", () => { resolve(Buffer.concat(buffers)); }); } ); } /** * Linearize a PDF held by the engine (by document id) and stream the linearized bytes * as a {@link Readable}. Useful for piping to HTTP responses or file streams without * buffering the entire PDF in memory. */ export async function linearizeInMemoryFromIdStream( id: string, password = "" ): Promise<Readable> { const client: IronPdfServiceClient = await Access.ensureConnection(); const passThrough = new PassThrough(); const stream: grpc.ClientReadableStream<BytesResultStreamP__Output> = client.QPdf_Linearization_LinearizeInMemoryFromId({ document: {documentId: id}, password: password ?? "", }); stream.on("data", (data: BytesResultStreamP__Output) => { if (data.exception) { passThrough.destroy( new Error(`${data.exception.message}/n${data.exception.remoteStackTrace}`) ); } else if (data.resultChunk) { passThrough.write(data.resultChunk); } }); stream.on("error", (err: Error) => { passThrough.destroy(err); }); stream.on("end", () => { passThrough.end(); }); return passThrough; } /** * Linearize a PDF provided as raw bytes and return the linearized bytes via the * bidirectional streaming {@code QPdf_Linearization_LinearizeInMemory} RPC. */ export async function linearizeInMemoryFromBytes( pdfBytes: Buffer, password = "" ): Promise<Buffer> { const client: IronPdfServiceClient = await Access.ensureConnection(); return new Promise( (resolve: (_: Buffer) => void, reject: (errorMsg: string) => void) => { const stream: grpc.ClientDuplexStream< QPdfLinearizeInMemoryRequestStreamP, BytesResultStreamP__Output > = client.QPdf_Linearization_LinearizeInMemory(); const buffers: Buffer[] = []; stream.on("data", (data: BytesResultStreamP__Output) => { if (data.exception) { handleRemoteException(data.exception, reject); } else if (data.resultChunk) { buffers.push(data.resultChunk); } }); stream.on("error", (err: Error) => { reject(`${err.name}/n${err.message}`); }); stream.on("end", () => { resolve(Buffer.concat(buffers)); }); stream.write({info: {password: password ?? ""}}); chunkBuffer(pdfBytes).forEach((chunk) => { stream.write({pdfBytesChunk: chunk}); }); stream.end(); } ); } /** * Core linearization logic shared across all linearization entry points. Implements the * {@link LinearizationMode} strategy pattern. Mirrors {@code PdfDocument.LinearizePdfCore} * on the C# side. */ export async function linearizeCoreFromBytes( pdfBytes: Buffer, password = "", mode: LinearizationMode = LinearizationMode.Automatic ): Promise<Buffer> { if (!pdfBytes || pdfBytes.length === 0) { throw new Error("The PDF bytes cannot be null or empty."); } if (mode === LinearizationMode.InMemory) { return linearizeInMemoryFromBytes(pdfBytes, password); } if (mode === LinearizationMode.FileBased) { // Explicit FileBased — let any disk error bubble up. return linearizeViaTempFile(pdfBytes, password); } // Automatic mode if (canWriteToTemp()) { try { return await linearizeViaTempFile(pdfBytes, password); } catch (e) { console.warn( `Automatic Linearization: Disk attempt failed (${(e as Error).message}). ` + "Falling back to Memory linearization." ); return linearizeInMemoryFromBytes(pdfBytes, password); } } console.warn("Automatic Linearization: No write permission detected. Using Memory linearization."); return linearizeInMemoryFromBytes(pdfBytes, password); } /** * Variant of {@link linearizeCoreFromBytes} that starts from an open document on the engine. * For {@link LinearizationMode.InMemory} we use the cheap document-id RPC; for the disk-based * paths we have to fetch the bytes once and delegate to {@link linearizeCoreFromBytes}. */ export async function linearizeCoreFromId( id: string, getBytes: () => Promise<Buffer>, password = "", mode: LinearizationMode = LinearizationMode.Automatic ): Promise<Buffer> { if (mode === LinearizationMode.InMemory) { return linearizeInMemoryFromId(id, password); } const pdfBytes = await getBytes(); return linearizeCoreFromBytes(pdfBytes, password, mode); } /** * Linearize via the engine's file-based RPC and persist the result through a client-side * temporary file. The client-side disk write is the difference between this and * {@link linearizeInMemoryFromBytes} — when the client filesystem is read-only, * {@code FileBased} mode fails here so {@link LinearizationMode.Automatic} can fall back. * * Mirrors C# {@code PdfDocument.LinearizeViaTempFile}. */ async function linearizeViaTempFile(pdfBytes: Buffer, password: string): Promise<Buffer> { const linearized = await saveAsLinearizedFromBytes(pdfBytes, "", password); const tempPath = path.join( os.tmpdir(), `ironpdf-linearize-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 10)}.pdf` ); try { fs.writeFileSync(tempPath, linearized); return fs.readFileSync(tempPath); } finally { try { if (fs.existsSync(tempPath)) { fs.unlinkSync(tempPath); } } catch { // best-effort cleanup } } } /** * Probe whether the current process can create files in the system temp directory. */ function canWriteToTemp(): boolean { const probePath = path.join( os.tmpdir(), `ironpdf-probe-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 10)}.tmp` ); try { fs.writeFileSync(probePath, ""); return true; } catch { return false; } finally { try { if (fs.existsSync(probePath)) { fs.unlinkSync(probePath); } } catch { // best-effort cleanup } } } /** * Linearize a PDF provided as raw bytes and save the result to disk via the file-based * streaming {@code QPdf_Linearization_SaveAsLinearizedFromBytes} RPC. * * Mirrors the in-memory behavior used by {@link compressInMemory} in {@code compress.ts}: * the engine streams the linearized bytes back, and we concatenate and persist them * locally at {@code outputPath}. */ export async function saveAsLinearizedFromBytes( pdfBytes: Buffer, outputPath: string, password = "" ): Promise<Buffer> { const client: IronPdfServiceClient = await Access.ensureConnection(); return new Promise( (resolve: (_: Buffer) => void, reject: (errorMsg: string) => void) => { const stream: grpc.ClientDuplexStream< QPdfSaveAsLinearizedFromBytesRequestStreamP, BytesResultStreamP__Output > = client.QPdf_Linearization_SaveAsLinearizedFromBytes(); const buffers: Buffer[] = []; stream.on("data", (data: BytesResultStreamP__Output) => { if (data.exception) { handleRemoteException(data.exception, reject); } else if (data.resultChunk) { buffers.push(data.resultChunk); } }); stream.on("error", (err: Error) => { reject(`${err.name}/n${err.message}`); }); stream.on("end", () => { resolve(Buffer.concat(buffers)); }); stream.write({ info: {outputPath: outputPath, password: password ?? ""}, }); chunkBuffer(pdfBytes).forEach((chunk) => { stream.write({pdfBytesChunk: chunk}); }); stream.end(); } ); }