Files
fletimageanalysis/pdf_processor.py
2025-07-16 17:33:20 +09:00

323 lines
11 KiB
Python

"""
PDF 처리 모듈
PDF 파일을 이미지로 변환하고 base64로 인코딩하는 기능을 제공합니다.
"""
import base64
import io
import fitz # PyMuPDF
from PIL import Image
from typing import List, Optional, Tuple, Dict, Any
import logging
from pathlib import Path
# 로깅 설정
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
class PDFProcessor:
"""PDF 파일 처리 클래스"""
def __init__(self):
self.supported_formats = ['pdf']
def validate_pdf_file(self, file_path: str) -> bool:
"""PDF 파일 유효성 검사"""
try:
path = Path(file_path)
# 파일 존재 여부 확인
if not path.exists():
logger.error(f"파일이 존재하지 않습니다: {file_path}")
return False
# 파일 확장자 확인
if path.suffix.lower() != '.pdf':
logger.error(f"지원하지 않는 파일 형식입니다: {path.suffix}")
return False
# PDF 파일 열기 테스트
doc = fitz.open(file_path)
page_count = len(doc)
doc.close()
if page_count == 0:
logger.error("PDF 파일에 페이지가 없습니다.")
return False
logger.info(f"PDF 검증 완료: {page_count}페이지")
return True
except Exception as e:
logger.error(f"PDF 파일 검증 중 오류 발생: {e}")
return False
def get_pdf_info(self, file_path: str) -> Optional[dict]:
"""PDF 파일 정보 조회"""
try:
doc = fitz.open(file_path)
info = {
'page_count': len(doc),
'metadata': doc.metadata,
'file_size': Path(file_path).stat().st_size,
'filename': Path(file_path).name
}
doc.close()
return info
except Exception as e:
logger.error(f"PDF 정보 조회 중 오류 발생: {e}")
return None
def convert_pdf_page_to_image(
self,
file_path: str,
page_number: int = 0,
zoom: float = 2.0,
image_format: str = "PNG"
) -> Optional[Image.Image]:
"""PDF 페이지를 PIL Image로 변환"""
try:
doc = fitz.open(file_path)
if page_number >= len(doc):
logger.error(f"페이지 번호가 범위를 벗어남: {page_number}")
doc.close()
return None
# 페이지 로드
page = doc.load_page(page_number)
# 이미지 변환을 위한 매트릭스 설정 (확대/축소)
mat = fitz.Matrix(zoom, zoom)
pix = page.get_pixmap(matrix=mat)
# PIL Image로 변환
img = Image.frombytes("RGB", [pix.width, pix.height], pix.samples)
doc.close()
logger.info(f"페이지 {page_number + 1} 이미지 변환 완료: {img.size}")
return img
except Exception as e:
logger.error(f"PDF 페이지 이미지 변환 중 오류 발생: {e}")
return None
def convert_pdf_to_images(
self,
file_path: str,
max_pages: Optional[int] = None,
zoom: float = 2.0
) -> List[Image.Image]:
"""PDF의 모든 페이지를 이미지로 변환"""
images = []
try:
doc = fitz.open(file_path)
total_pages = len(doc)
# 최대 페이지 수 제한
if max_pages:
total_pages = min(total_pages, max_pages)
for page_num in range(total_pages):
img = self.convert_pdf_page_to_image(file_path, page_num, zoom)
if img:
images.append(img)
doc.close()
logger.info(f"{len(images)}개 페이지 이미지 변환 완료")
except Exception as e:
logger.error(f"PDF 전체 페이지 변환 중 오류 발생: {e}")
return images
def image_to_base64(
self,
image: Image.Image,
format: str = "PNG",
quality: int = 95
) -> Optional[str]:
"""PIL Image를 base64 문자열로 변환"""
try:
buffer = io.BytesIO()
# JPEG 형식인 경우 품질 설정
if format.upper() == "JPEG":
image.save(buffer, format=format, quality=quality)
else:
image.save(buffer, format=format)
buffer.seek(0)
base64_string = base64.b64encode(buffer.getvalue()).decode('utf-8')
logger.info(f"이미지를 base64로 변환 완료 (크기: {len(base64_string)} 문자)")
return base64_string
except Exception as e:
logger.error(f"이미지 base64 변환 중 오류 발생: {e}")
return None
def pdf_page_to_base64(
self,
file_path: str,
page_number: int = 0,
zoom: float = 2.0,
format: str = "PNG"
) -> Optional[str]:
"""PDF 페이지를 직접 base64로 변환"""
img = self.convert_pdf_page_to_image(file_path, page_number, zoom)
if img:
return self.image_to_base64(img, format)
return None
def pdf_page_to_image_bytes(
self,
file_path: str,
page_number: int = 0,
zoom: float = 2.0,
format: str = "PNG"
) -> Optional[bytes]:
"""PDF 페이지를 이미지 바이트로 변환 (Flet 이미지 표시용)"""
try:
img = self.convert_pdf_page_to_image(file_path, page_number, zoom)
if img:
buffer = io.BytesIO()
img.save(buffer, format=format)
buffer.seek(0)
image_bytes = buffer.getvalue()
logger.info(f"페이지 {page_number + 1} 이미지 바이트 변환 완료 (크기: {len(image_bytes)} 바이트)")
return image_bytes
return None
except Exception as e:
logger.error(f"PDF 페이지 이미지 바이트 변환 중 오류 발생: {e}")
return None
def get_optimal_zoom_for_size(self, target_size: Tuple[int, int]) -> float:
"""목표 크기에 맞는 최적 줌 비율 계산"""
# 기본 PDF 페이지 크기 (A4: 595x842 points)
default_width, default_height = 595, 842
target_width, target_height = target_size
# 비율 계산
width_ratio = target_width / default_width
height_ratio = target_height / default_height
# 작은 비율을 선택하여 전체 페이지가 들어가도록 함
zoom = min(width_ratio, height_ratio)
logger.info(f"최적 줌 비율 계산: {zoom:.2f}")
return zoom
def extract_text_with_coordinates(self, file_path: str, page_number: int = 0) -> List[Dict[str, Any]]:
"""PDF 페이지에서 텍스트와 좌표를 추출합니다."""
text_blocks = []
try:
doc = fitz.open(file_path)
if page_number >= len(doc):
logger.error(f"페이지 번호가 범위를 벗어남: {page_number}")
doc.close()
return []
page = doc.load_page(page_number)
# 'dict' 옵션은 블록, 라인, 스팬에 대한 상세 정보를 제공합니다.
blocks = page.get_text("dict")["blocks"]
for b in blocks: # 블록 반복
if b['type'] == 0: # 텍스트 블록
for l in b["lines"]: # 라인 반복
for s in l["spans"]: # 스팬(텍스트 조각) 반복
text_blocks.append({
"text": s["text"],
"bbox": s["bbox"], # (x0, y0, x1, y1)
"font": s["font"],
"size": s["size"]
})
doc.close()
logger.info(f"페이지 {page_number + 1}에서 {len(text_blocks)}개의 텍스트 블록 추출 완료")
return text_blocks
except Exception as e:
logger.error(f"PDF 텍스트 및 좌표 추출 중 오류 발생: {e}")
return []
def convert_to_images(
self,
file_path: str,
zoom: float = 2.0,
max_pages: int = 10
) -> List[Image.Image]:
"""PDF의 모든 페이지(또는 지정된 수까지)를 PIL Image 리스트로 변환"""
images = []
try:
doc = fitz.open(file_path)
page_count = min(len(doc), max_pages) # 최대 페이지 수 제한
logger.info(f"PDF 변환 시작: {page_count}페이지")
for page_num in range(page_count):
page = doc.load_page(page_num)
# 이미지 변환을 위한 매트릭스 설정
mat = fitz.Matrix(zoom, zoom)
pix = page.get_pixmap(matrix=mat)
# PIL Image로 변환
img = Image.frombytes("RGB", [pix.width, pix.height], pix.samples)
images.append(img)
logger.info(f"페이지 {page_num + 1}/{page_count} 변환 완료: {img.size}")
doc.close()
logger.info(f"PDF 전체 변환 완료: {len(images)}개 이미지")
return images
except Exception as e:
logger.error(f"PDF 다중 페이지 변환 중 오류 발생: {e}")
return []
def image_to_bytes(self, image: Image.Image, format: str = 'PNG') -> bytes:
"""
PIL Image를 바이트 데이터로 변환합니다.
Args:
image: PIL Image 객체
format: 이미지 포맷 ('PNG', 'JPEG' 등)
Returns:
이미지 바이트 데이터
"""
try:
buffer = io.BytesIO()
image.save(buffer, format=format)
image_bytes = buffer.getvalue()
buffer.close()
logger.info(f"이미지를 {format} 바이트로 변환: {len(image_bytes)} bytes")
return image_bytes
except Exception as e:
logger.error(f"이미지 바이트 변환 중 오류 발생: {e}")
return b''
# 사용 예시
if __name__ == "__main__":
processor = PDFProcessor()
# 테스트용 코드 (실제 PDF 파일 경로로 변경 필요)
test_pdf = "test.pdf"
if processor.validate_pdf_file(test_pdf):
info = processor.get_pdf_info(test_pdf)
print(f"PDF 정보: {info}")
# 첫 번째 페이지를 base64로 변환
base64_data = processor.pdf_page_to_base64(test_pdf, 0)
if base64_data:
print(f"Base64 변환 성공: {len(base64_data)} 문자")
else:
print("PDF 파일 검증 실패")