import { Injectable, Logger } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import * as fs from 'fs/promises'; import * as path from 'path'; import { PDFDocument } from 'pdf-lib'; import { exec } from 'child_process'; import { promisify } from 'util'; import { Pdf2ImageOptions, ImageInfo, ConversionResult, } from './pdf2image.interface'; import { I18nService } from '../i18n/i18n.service'; const execAsync = promisify(exec); @Injectable() export class Pdf2ImageService { private readonly logger = new Logger(Pdf2ImageService.name); private tempDir: string; constructor( private configService: ConfigService, private i18nService: I18nService, ) { this.tempDir = this.configService.get('TEMP_DIR', './temp'); } /** * Convert PDF to list of images * Uses ImageMagick's convert command */ async convertToImages( pdfPath: string, options: Pdf2ImageOptions = {}, ): Promise { const { density = 300, quality = 85, format = 'jpeg', outDir = this.tempDir, } = options; // Validate PDF file try { await fs.access(pdfPath); } catch { throw new Error(`PDF file not found: ${pdfPath}`); } // Create output directory const timestamp = Date.now(); const outputDir = path.join(outDir, `pdf2img_${timestamp}`); await fs.mkdir(outputDir, { recursive: true }); this.logger.log(`Converting PDF to images: ${pdfPath}`); this.logger.log(`Output directory: ${outputDir}`); try { // Get total page count using pdf-lib instead of pdfinfo const pdfBytes = await fs.readFile(pdfPath); const pdfDoc = await PDFDocument.load(pdfBytes, { ignoreEncryption: true, }); const totalPages = pdfDoc.getPageCount(); if (totalPages === 0) { throw new Error(this.i18nService.getMessage('pdfPageCountError')); } this.logger.log( `Starting PDF conversion: ${path.basename(pdfPath)} (${totalPages} pages)`, ); this.logger.log(`Output directory: ${outputDir}`); this.logger.log( `Conversion parameters: density=${density}dpi, quality=${quality}%, format=${format}`, ); // Convert using Python script const zoom = (density / 72).toFixed(2); const pythonScript = path.join(process.cwd(), 'pdf_to_images.py'); const cmd = `python "${pythonScript}" "${pdfPath}" "${outputDir}" ${zoom} ${quality}`; this.logger.log(`Executing conversion command: ${cmd}`); const { stdout } = await execAsync(cmd); const result = JSON.parse(stdout); if (!result.success) { throw new Error(`Python conversion failed: ${result.error}`); } const images: ImageInfo[] = result.images; const successCount = result.images.length; const failedCount = totalPages - successCount; this.logger.log( `🎉 PDF conversion completed! ✅ Success: ${successCount} pages, ❌ Failed: ${failedCount} pages, 📊 Total pages: ${totalPages}`, ); return { images, totalPages, successCount, failedCount, }; } catch (error) { // Cleanup temp directory await this.cleanupDirectory(outputDir); throw new Error(`PDF to image conversion failed: ${error.message}`); } } /** * Batch convert multiple PDFs */ async batchConvert( pdfPaths: string[], options?: Pdf2ImageOptions, ): Promise { const results: ConversionResult[] = []; for (const pdfPath of pdfPaths) { try { const result = await this.convertToImages(pdfPath, options); results.push(result); } catch (error) { this.logger.error(`Failed to convert ${pdfPath}: ${error.message}`); throw error; } } return results; } /** * Cleanup image files */ async cleanupImages(images: ImageInfo[]): Promise { for (const image of images) { try { await fs.unlink(image.path); this.logger.log(`Deleted: ${image.path}`); } catch (error) { this.logger.warn(`Failed to delete ${image.path}: ${error.message}`); } } // Try to cleanup empty directory if (images.length > 0) { const dir = path.dirname(images[0].path); await this.cleanupDirectory(dir); } } /** * Cleanup directory */ async cleanupDirectory(dir: string): Promise { try { const files = await fs.readdir(dir); if (files.length === 0) { await fs.rmdir(dir); this.logger.log(`Cleaned up empty directory: ${dir}`); } } catch (error) { this.logger.warn(`Failed to cleanup directory ${dir}: ${error.message}`); } } /** * Check if image quality is acceptable */ isImageQualityGood(imageInfo: ImageInfo, minSizeKB: number = 10): boolean { const sizeKB = imageInfo.size / 1024; if (sizeKB < minSizeKB) { this.logger.warn( `Image too small: ${sizeKB.toFixed(2)}KB < ${minSizeKB}KB`, ); return false; } return true; } }