// src/lib/pdf/compress.ts — from tinytinytools.com // Served verbatim from the working tree. Method: https://tinytinytools.com/guides import { PDFDocument } from 'pdf-lib'; import { openRenderDocument, type RenderAdapters } from './render'; export const COMPRESSOR_DPIS = Object.freeze([72, 96, 150] as const); /** * The three presets the UI offers, ordered least to most aggressive. * * `detail` states the DPI and JPEG quality the preset actually applies rather than a * predicted size reduction. Reduction depends on the source — page count, dimensions, * how much of the page is photographic, and how well the original was already * compressed — so a "~60-80% smaller" badge would be a number we cannot stand behind. * The settings are verifiable; a forecast is not. */ export const COMPRESSION_LEVELS = Object.freeze([ { id: 'low', label: 'Low', summary: 'Best quality, largest file', detail: '150 DPI · quality 90', dpi: 150, quality: 90, }, { id: 'recommended', label: 'Recommended', summary: 'Balanced size and legibility', detail: '96 DPI · quality 75', dpi: 96, quality: 75, }, { id: 'high', label: 'High', summary: 'Smallest file, softest detail', detail: '72 DPI · quality 50', dpi: 72, quality: 50, }, ] as const); export type CompressionLevel = (typeof COMPRESSION_LEVELS)[number]; export type CompressionLevelId = CompressionLevel['id']; export const DEFAULT_COMPRESSION_LEVEL_ID: CompressionLevelId = 'recommended'; /** Resolve a level id to its DPI/quality pair, falling back to the default preset. */ export function resolveCompressionLevel(id: unknown): CompressionLevel { return ( COMPRESSION_LEVELS.find(level => level.id === id) ?? COMPRESSION_LEVELS.find(level => level.id === DEFAULT_COMPRESSION_LEVEL_ID)! ); } export async function compressPdfBytes( bytes: Uint8Array, options: { dpi: number; quality: number }, renderAdapters?: RenderAdapters, onProgress?: (pageNumber: number, pageCount: number) => void, ): Promise { if (!(COMPRESSOR_DPIS as readonly number[]).includes(options.dpi)) { throw new Error('Choose a DPI preset.'); } if (!Number.isFinite(options.quality) || options.quality < 40 || options.quality > 95) { throw new Error('Choose a quality between 40 and 95.'); } const renderDoc = await openRenderDocument(bytes, renderAdapters); try { const out = await PDFDocument.create(); for (const pageInfo of renderDoc.pages) { onProgress?.(pageInfo.pageNumber, renderDoc.pageCount); const image = await renderDoc.renderPage(pageInfo.pageNumber, { scale: options.dpi / 72, format: 'jpeg', quality: options.quality, }); const embedded = await out.embedJpg(new Uint8Array(await image.blob.arrayBuffer())); const page = out.addPage([pageInfo.widthPt, pageInfo.heightPt]); page.drawImage(embedded, { x: 0, y: 0, width: pageInfo.widthPt, height: pageInfo.heightPt }); } return out.save(); } finally { await renderDoc.destroy(); } }