// app/api/ocr/utils/imageRotation.ts // Sharp을 사용한 서버 사이드 이미지 회전 import sharp from 'sharp'; /** * 서버 사이드에서 이미지를 회전시킵니다 * @param base64 - base64 인코딩된 이미지 데이터 * @param degrees - 회전 각도 (0, 90, 180, 270) * @returns Promise - 회전된 이미지의 base64 데이터 */ export async function rotateImageBase64(base64: string, degrees: number): Promise { try { console.log(`🔄 Rotating image by ${degrees} degrees...`); // base64를 Buffer로 변환 const inputBuffer = Buffer.from(base64, 'base64'); // 회전 각도에 따른 처리 let rotatedBuffer: Buffer; switch (degrees) { case 0: // 회전 없음 rotatedBuffer = inputBuffer; break; case 90: rotatedBuffer = await sharp(inputBuffer) .rotate(90) .jpeg({ quality: 90, progressive: true }) .toBuffer(); break; case 180: rotatedBuffer = await sharp(inputBuffer) .rotate(180) .jpeg({ quality: 90, progressive: true }) .toBuffer(); break; case 270: rotatedBuffer = await sharp(inputBuffer) .rotate(270) .jpeg({ quality: 90, progressive: true }) .toBuffer(); break; default: console.warn(`⚠️ Unsupported rotation angle: ${degrees}°. Using original image.`); rotatedBuffer = inputBuffer; } // Buffer를 다시 base64로 변환 const rotatedBase64 = rotatedBuffer.toString('base64'); console.log(`✅ Image rotated successfully (${degrees}°)`); return rotatedBase64; } catch (error) { console.error(`❌ Error rotating image by ${degrees}°:`, error); console.warn('Using original image due to rotation error'); return base64; // 실패시 원본 반환 } } /** * 이미지 품질을 개선합니다 * @param base64 - base64 인코딩된 이미지 데이터 * @returns Promise - 개선된 이미지의 base64 데이터 */ export async function enhanceImageQuality(base64: string): Promise { try { console.log('🎨 Enhancing image quality...'); const inputBuffer = Buffer.from(base64, 'base64'); const enhancedBuffer = await sharp(inputBuffer) .resize(2000, 2000, { fit: 'inside', withoutEnlargement: true }) // 개별 매개변수 방식으로 수정 .sharpen(1, 1, 2) // sigma, m1(flat), m2(jagged) .normalize() // 히스토그램 정규화 .gamma(1.1) // 약간의 감마 보정 .jpeg({ quality: 95, progressive: true, mozjpeg: true }) .toBuffer(); const enhancedBase64 = enhancedBuffer.toString('base64'); console.log('✅ Image quality enhanced'); return enhancedBase64; } catch (error) { console.error('❌ Error enhancing image:', error); return base64; } } /** * PDF를 고품질 이미지로 변환합니다 * @param pdfBuffer - PDF Buffer 데이터 * @param pageIndex - 변환할 페이지 인덱스 (0부터 시작) * @returns Promise - 변환된 이미지의 base64 데이터 */ export async function convertPDFToImage(pdfBuffer: Buffer, pageIndex: number = 0): Promise { try { console.log(`📄 Converting PDF page ${pageIndex + 1} to image...`); // pdf2pic 라이브러리 사용 const pdf2pic = require('pdf2pic'); const convert = pdf2pic.fromBuffer(pdfBuffer, { density: 300, // 300 DPI for high quality saveFilename: "page", savePath: "/tmp", // 임시 경로 format: "jpeg", width: 2480, // A4 크기 @ 300 DPI height: 3508, quality: 100 }); const result = await convert(pageIndex + 1, { responseType: "buffer" }); const base64 = result.buffer.toString('base64'); console.log('✅ PDF converted to image successfully'); return base64; } catch (error) { console.error('❌ Error converting PDF to image:', error); throw new Error('Failed to convert PDF to image'); } } /** * 이미지에서 텍스트 방향을 감지합니다 * @param base64 - base64 인코딩된 이미지 데이터 * @returns Promise - 감지된 올바른 회전 각도 */ export async function detectTextOrientation(base64: string): Promise { // 이 함수는 간단한 방향 감지를 시뮬레이션합니다 // 실제로는 더 정교한 알고리즘이 필요할 수 있습니다 console.log('🧭 Detecting text orientation...'); const rotations = [0, 90, 180, 270]; const scores: { rotation: number; score: number }[] = []; for (const rotation of rotations) { try { const rotatedBase64 = await rotateImageBase64(base64, rotation); // 간단한 품질 측정 (실제로는 OCR API 호출이나 다른 방법 사용) const score = await estimateTextQuality(rotatedBase64); scores.push({ rotation, score }); console.log(` ${rotation}°: quality score = ${score.toFixed(3)}`); } catch (error) { console.warn(` ${rotation}°: Failed to test orientation`); scores.push({ rotation, score: 0 }); } } const bestOrientation = scores.reduce((best, current) => current.score > best.score ? current : best ); console.log(`🎯 Best orientation detected: ${bestOrientation.rotation}°`); return bestOrientation.rotation; } /** * 이미지의 텍스트 품질을 추정합니다 (간단한 버전) */ async function estimateTextQuality(base64: string): Promise { try { const buffer = Buffer.from(base64, 'base64'); // Sharp을 사용해 이미지 통계 분석 const stats = await sharp(buffer) .greyscale() .stats(); // 간단한 품질 지표 계산 // 실제로는 더 복잡한 알고리즘이 필요합니다 const contrast = stats.channels[0].max - stats.channels[0].min; const sharpness = stats.channels[0].stdev; return (contrast + sharpness) / 510; // 0-1 범위로 정규화 } catch (error) { return 0; } } /** * 이미지가 회전이 필요한지 빠르게 체크합니다 * @param base64 - base64 인코딩된 이미지 데이터 * @returns Promise - 회전이 필요하면 true */ export async function needsRotation(base64: string): Promise { try { const buffer = Buffer.from(base64, 'base64'); // 이미지 메타데이터 확인 const metadata = await sharp(buffer).metadata(); // EXIF 방향 정보가 있으면 회전 필요 if (metadata.orientation && metadata.orientation > 1) { console.log(`📐 EXIF orientation detected: ${metadata.orientation}`); return true; } // 가로/세로 비율 체크 (일반적으로 문서는 세로가 더 긺) if (metadata.width && metadata.height) { const aspectRatio = metadata.width / metadata.height; if (aspectRatio > 1.5) { console.log(`📐 Wide aspect ratio detected: ${aspectRatio.toFixed(2)}`); return true; // 너무 가로로 긴 이미지는 회전 필요할 가능성 } } return false; } catch (error) { console.warn('Error checking if rotation needed:', error); return false; } }