import * as grpc from "@grpc/grpc-js"; import {ServiceError} from "@grpc/grpc-js"; import {IronPdfServiceClient} from "../../generated_proto/ironpdfengineproto/IronPdfService"; import {Access} from "../../access"; import {EmptyResultP__Output} from "../../generated_proto/ironpdfengineproto/EmptyResultP"; import {BytesResultStreamP__Output} from "../../generated_proto/ironpdfengineproto/BytesResultStreamP"; import {QPdfCompressionFlagsP} from "../../generated_proto/ironpdfengineproto/QPdfCompressionFlagsP"; import { QPdfCompressAndSaveAsAdvancedFromBytesRequestStreamP } from "../../generated_proto/ironpdfengineproto/QPdfCompressAndSaveAsAdvancedFromBytesRequestStreamP"; import {chunkBuffer, handleEmptyResultP__Output, handleRemoteException} from "../util"; import {PassThrough, Readable} from "stream"; import fs from "fs"; import {Buffer} from "buffer"; import {AdvancedCompressionOptions, ObjectStreamMode} from "../../../public/compression"; export async function compressImage( id: string, imageQuality: number, scaleToVisibleSize = false ): Promise { const client: IronPdfServiceClient = await Access.ensureConnection(); return new Promise( (resolve: () => void, reject: (errorMsg: string) => void) => { client.Pdfium_Compress_CompressImages( { document: {documentId: id}, scaleToVisibleSize: scaleToVisibleSize, quality: imageQuality, }, ( err: ServiceError | null, value: EmptyResultP__Output | undefined ) => { if (err) { reject(`${err.name}/n${err.message}`); } else if (value) { handleEmptyResultP__Output(value, reject); resolve(); } } ); } ); } export async function compressStructTree( id: string ): Promise { const client: IronPdfServiceClient = await Access.ensureConnection(); return new Promise( (resolve: () => void, reject: (errorMsg: string) => void) => { client.Pdfium_Compress_RemoveStructTree( { document: {documentId: id} }, ( err: ServiceError | null, value: EmptyResultP__Output | undefined ) => { if (err) { reject(`${err.name}/n${err.message}`); } else if (value) { handleEmptyResultP__Output(value, reject); resolve(); } } ); } ); } export async function compressAndSaveAs( id: string, outputPath: string, imageQuality?: number ): Promise { const client: IronPdfServiceClient = await Access.ensureConnection(); return new Promise( (resolve: () => void, reject: (errorMsg: string) => void) => { const request = { document: {documentId: id}, outputPath: "", password: "", ...(imageQuality !== undefined ? {jpeg: imageQuality} : {}), }; const stream: grpc.ClientReadableStream = client.QPdf_Compression_CompressAndSaveAs(request); const buffers: Buffer[] = []; stream.on("data", (data: BytesResultStreamP__Output) => { if (data.exception) { reject( `${data.exception.message}/n${data.exception.remoteStackTrace}` ); } else if (data.resultChunk) { buffers.push(data.resultChunk); } }); stream.on("error", (err: Error) => { reject(`${err.name}/n${err.message}`); }); stream.on("end", () => { fs.writeFileSync(outputPath, Buffer.concat(buffers)); resolve(); }); } ); } export async function compressInMemory( id: string, imageQuality?: number ): Promise { const client: IronPdfServiceClient = await Access.ensureConnection(); return new Promise( (resolve: (_: Buffer) => void, reject: (errorMsg: string) => void) => { const request = { document: {documentId: id}, outputPath: "", password: "", ...(imageQuality !== undefined ? {jpeg: imageQuality} : {}), }; const stream: grpc.ClientReadableStream = client.QPdf_Compression_CompressAndSaveAs(request); const buffers: Buffer[] = []; stream.on("data", (data: BytesResultStreamP__Output) => { if (data.exception) { reject( `${data.exception.message}/n${data.exception.remoteStackTrace}` ); } 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)); }); } ); } /** * Map a user-facing {@link AdvancedCompressionOptions} onto the wire * {@link QPdfCompressionFlagsP}. */ export function toFlagsProto(options: AdvancedCompressionOptions): QPdfCompressionFlagsP { const pdfiumWillReEncode = options.targetImageDpi !== null && options.targetImageDpi !== undefined && options.targetImageDpi > 0; const optimizeImages = !pdfiumWillReEncode && options.jpegQuality != null; const flags: QPdfCompressionFlagsP = { compressStreams: options.compressStreams ?? true, coalesceContents: options.coalesceContents ?? true, removeUnreferencedResources: options.removeUnreferencedResources ?? true, recompressFlate: options.recompressFlate ?? true, compressionLevel: options.compressionLevel ?? 9, objectStreams: (options.objectStreams ?? ObjectStreamMode.Generate) as string, decodeGeneralizedStreams: options.decodeGeneralizedStreams ?? true, optimizeImagesMinWidth: options.optimizeImagesMinWidth ?? 0, optimizeImagesMinHeight: options.optimizeImagesMinHeight ?? 0, optimizeImagesMinArea: options.optimizeImagesMinArea ?? 0, optimizeImages: optimizeImages, }; if (!pdfiumWillReEncode && options.jpegQuality != null) { flags.jpegQuality = options.jpegQuality; } return flags; } /** * Apply the advanced compression pipeline to an open engine document and * save the result to disk. Mirrors .NET * {@code PdfDocument.CompressAndSaveAs(string, AdvancedCompressionOptions)}. * * Step 1 (optional) — pdfium image re-encoding when {@code targetImageDpi > 0} * and {@code jpegQuality != null}. * Step 2 (optional) — struct-tree removal when {@code removeStructureTree}. * Step 3 — qpdf {@code QPdf_Compression_CompressAndSaveAsAdvanced}. */ export async function compressAndSaveAsAdvanced( id: string, outputPath: string, options: AdvancedCompressionOptions, password = "" ): Promise { if (!options) { throw new Error("options must not be null"); } // Step 1 — pdfium image re-encoding (gated, matches .NET pdfiumWillReEncode). const pdfiumWillReEncode = options.targetImageDpi !== null && options.targetImageDpi !== undefined && options.targetImageDpi > 0; if (pdfiumWillReEncode && options.jpegQuality != null) { await compressImage( id, options.jpegQuality, false /* scaleToVisibleSize */ ); } // Step 2 — struct-tree removal. if (options.removeStructureTree) { await compressStructTree(id); } // Step 3 — qpdf advanced pass. const client: IronPdfServiceClient = await Access.ensureConnection(); return new Promise( (resolve: () => void, reject: (errorMsg: string) => void) => { const stream: grpc.ClientReadableStream = client.QPdf_Compression_CompressAndSaveAsAdvanced({ document: {documentId: id}, outputPath: "", password: password ?? "", flags: toFlagsProto(options), }); 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", () => { try { fs.writeFileSync(outputPath, Buffer.concat(buffers)); } catch (e) { reject( `Failed to write compressed PDF to ${outputPath}: ${(e as Error).message}` ); return; } resolve(); }); } ); } /** * Apply the advanced compression pipeline to byte[] input and stream the * result back. Mirrors .NET * {@code PdfDocument.CompressAndSaveAs(byte[], string, AdvancedCompressionOptions, ...)}. * * Returns the compressed bytes; when {@code outputPath} is non-empty also * writes them to disk. */ export async function compressAndSaveAsAdvancedFromBytes( pdfBytes: Buffer, outputPath: string, options: AdvancedCompressionOptions, password = "" ): Promise { if (!pdfBytes || pdfBytes.length === 0) { throw new Error("pdfBytes must not be null or empty"); } if (!options) { throw new Error("options must not be null"); } const client: IronPdfServiceClient = await Access.ensureConnection(); return new Promise( (resolve: (_: Buffer) => void, reject: (errorMsg: string) => void) => { const stream: grpc.ClientDuplexStream< QPdfCompressAndSaveAsAdvancedFromBytesRequestStreamP, BytesResultStreamP__Output > = client.QPdf_Compression_CompressAndSaveAsAdvancedFromBytes(); 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", () => { const result = Buffer.concat(buffers); if (outputPath) { try { fs.writeFileSync(outputPath, result); } catch (e) { reject( `Failed to write compressed PDF to ${outputPath}: ${(e as Error).message}` ); return; } } resolve(result); }); stream.write({ info: { outputPath: outputPath ?? "", password: password ?? "", flags: toFlagsProto(options), }, }); for (const chunk of chunkBuffer(pdfBytes)) { stream.write({pdfBytesChunk: chunk}); } stream.end(); } ); } export async function compressInMemoryStream( id: string, imageQuality?: number ): Promise { const client: IronPdfServiceClient = await Access.ensureConnection(); const passThrough = new PassThrough(); const request = { document: {documentId: id}, outputPath: "", password: "", ...(imageQuality !== undefined ? {jpeg: imageQuality} : {}), }; const stream: grpc.ClientReadableStream = client.QPdf_Compression_CompressAndSaveAs(request); 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; }