first commit
This commit is contained in:
322
pdf_processor.py
Normal file
322
pdf_processor.py
Normal file
@@ -0,0 +1,322 @@
|
||||
"""
|
||||
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 파일 검증 실패")
|
||||
Reference in New Issue
Block a user