@ironsoftware/ironpdf
Version:
IronPDF for Node
338 lines (304 loc) • 10.3 kB
text/typescript
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();
}
);
}