File name
Commit message
Commit date
File name
Commit message
Commit date
import fitz # PyMuPDF
from PIL import Image, ImageTk
import io
import os
import tkinter as tk
from tkinter import ttk, messagebox
import platform
import subprocess
class PDFPreviewManager:
"""PDF 미리보기 관리 클래스"""
def __init__(self):
self.preview_cache = {} # 미리보기 이미지 캐싱
def get_pdf_info(self, pdf_path: str) -> dict:
"""PDF 기본 정보 추출"""
try:
if not pdf_path or not os.path.exists(pdf_path):
return {
'error': 'PDF 파일을 찾을 수 없습니다.',
'page_count': 0,
'file_size': 0,
'is_encrypted': False,
'title': '',
'author': '',
'creation_date': '',
'file_name': os.path.basename(pdf_path) if pdf_path else '',
'file_path': pdf_path or ''
}
doc = fitz.open(pdf_path)
metadata = doc.metadata or {}
info = {
'page_count': doc.page_count,
'file_size': os.path.getsize(pdf_path),
'is_encrypted': doc.is_encrypted,
'title': metadata.get('title', '') or '',
'author': metadata.get('author', '') or '',
'creation_date': metadata.get('creationDate', '') or '',
'file_name': os.path.basename(pdf_path),
'file_path': pdf_path
}
doc.close()
return info
except Exception as e:
return {
'error': str(e),
'page_count': 0,
'file_size': 0,
'is_encrypted': False,
'title': '',
'author': '',
'creation_date': '',
'file_name': os.path.basename(pdf_path) if pdf_path else '',
'file_path': pdf_path or ''
}
def create_preview_thumbnail(self, pdf_path: str, target_size=(250, 350)) -> Image.Image:
"""PDF 첫 페이지 썸네일 생성"""
cache_key = f"{pdf_path}_{target_size}"
# 캐시에 있으면 반환
if cache_key in self.preview_cache:
return self.preview_cache[cache_key]
try:
doc = fitz.open(pdf_path)
if doc.page_count == 0:
doc.close()
return None
page = doc[0] # 첫 페이지
# 미리보기 크기에 맞게 해상도 계산
zoom_x = target_size[0] / page.rect.width
zoom_y = target_size[1] / page.rect.height
zoom = min(zoom_x, zoom_y) * 1.5 # 약간 높은 해상도
mat = fitz.Matrix(zoom, zoom)
pix = page.get_pixmap(matrix=mat)
img_data = pix.tobytes("png")
# PIL Image로 변환
image = Image.open(io.BytesIO(img_data))
image.thumbnail(target_size, Image.Resampling.LANCZOS)
doc.close()
# 캐시에 저장
self.preview_cache[cache_key] = image
return image
except Exception as e:
print(f"미리보기 생성 실패 ({pdf_path}): {e}")
return None
def format_file_size(self, size_bytes: int) -> str:
"""파일 크기를 읽기 쉬운 형태로 변환"""
if size_bytes < 1024:
return f"{size_bytes}B"
elif size_bytes < 1024**2:
return f"{size_bytes/1024:.1f}KB"
else:
return f"{size_bytes/(1024**2):.1f}MB"
def clear_cache(self):
"""캐시 클리어"""
self.preview_cache.clear()
def open_file_with_system(self, pdf_path: str) -> bool:
"""시스템 기본 프로그램으로 파일 열기"""
try:
if platform.system() == 'Windows':
os.startfile(pdf_path)
elif platform.system() == 'Darwin': # macOS
subprocess.run(['open', pdf_path])
else: # Linux
subprocess.run(['xdg-open', pdf_path])
return True
except Exception as e:
print(f"파일 열기 실패 ({pdf_path}): {e}")
return False
class PDFPreviewDialog:
"""PDF 미리보기 다이얼로그"""
def __init__(self, parent, pdf_path: str, employee_name: str, preview_manager: PDFPreviewManager):
self.parent = parent
self.pdf_path = pdf_path
self.employee_name = employee_name
self.preview_manager = preview_manager
# PDF 정보 가져오기
self.pdf_info = preview_manager.get_pdf_info(pdf_path)
# 다이얼로그 창 생성
self.window = tk.Toplevel(parent)
self.window.title(f"미리보기 - {employee_name}")
self.window.geometry("450x650")
self.window.resizable(True, True)
# 창을 모달로 설정
self.window.transient(parent)
self.window.grab_set()
# 창을 화면 중앙에 위치
self.center_window()
self.setup_ui()
def center_window(self):
"""창을 화면 중앙에 위치시키기"""
self.window.update_idletasks()
width = self.window.winfo_width()
height = self.window.winfo_height()
pos_x = (self.window.winfo_screenwidth() // 2) - (width // 2)
pos_y = (self.window.winfo_screenheight() // 2) - (height // 2)
self.window.geometry(f'{width}x{height}+{pos_x}+{pos_y}')
def setup_ui(self):
"""UI 구성"""
main_frame = ttk.Frame(self.window, padding=10)
main_frame.pack(fill='both', expand=True)
# 상단: 파일 정보
info_frame = ttk.LabelFrame(main_frame, text="📋 파일 정보", padding=10)
info_frame.pack(fill='x', pady=(0, 10))
# 정보 표시
info_grid = ttk.Frame(info_frame)
info_grid.pack(fill='x')
# 파일명
ttk.Label(info_grid, text="파일명:", font=('맑은 고딕', 9, 'bold')).grid(
row=0, column=0, sticky='w', padx=(0, 10)
)
ttk.Label(info_grid, text=self.pdf_info.get('file_name', '알 수 없음')).grid(
row=0, column=1, sticky='w'
)
# 직원명
ttk.Label(info_grid, text="직원명:", font=('맑은 고딕', 9, 'bold')).grid(
row=1, column=0, sticky='w', padx=(0, 10), pady=2
)
ttk.Label(info_grid, text=self.employee_name).grid(
row=1, column=1, sticky='w', pady=2
)
# 페이지 수
page_count = self.pdf_info.get('page_count', 0)
ttk.Label(info_grid, text="페이지 수:", font=('맑은 고딕', 9, 'bold')).grid(
row=2, column=0, sticky='w', padx=(0, 10), pady=2
)
ttk.Label(info_grid, text=f"{page_count}페이지").grid(
row=2, column=1, sticky='w', pady=2
)
# 파일 크기
file_size = self.preview_manager.format_file_size(self.pdf_info.get('file_size', 0))
ttk.Label(info_grid, text="파일 크기:", font=('맑은 고딕', 9, 'bold')).grid(
row=3, column=0, sticky='w', padx=(0, 10), pady=2
)
ttk.Label(info_grid, text=file_size).grid(
row=3, column=1, sticky='w', pady=2
)
# 암호화 상태
if self.pdf_info.get('is_encrypted'):
ttk.Label(info_grid, text="보안:", font=('맑은 고딕', 9, 'bold')).grid(
row=4, column=0, sticky='w', padx=(0, 10), pady=2
)
security_label = ttk.Label(info_grid, text="🔒 비밀번호 보호됨", foreground='orange')
security_label.grid(row=4, column=1, sticky='w', pady=2)
# 중앙: 미리보기 이미지
preview_frame = ttk.LabelFrame(main_frame, text="📖 첫 페이지 미리보기", padding=10)
preview_frame.pack(fill='both', expand=True, pady=(0, 10))
# 스크롤 가능한 캔버스 생성
canvas_frame = ttk.Frame(preview_frame)
canvas_frame.pack(fill='both', expand=True)
self.canvas = tk.Canvas(canvas_frame, bg='white', relief='sunken', bd=2)
scrollbar_v = ttk.Scrollbar(canvas_frame, orient='vertical', command=self.canvas.yview)
scrollbar_h = ttk.Scrollbar(canvas_frame, orient='horizontal', command=self.canvas.xview)
self.canvas.configure(yscrollcommand=scrollbar_v.set, xscrollcommand=scrollbar_h.set)
# 미리보기 이미지 로드 및 표시
self.load_preview_image()
# 그리드 배치
self.canvas.grid(row=0, column=0, sticky='nsew')
scrollbar_v.grid(row=0, column=1, sticky='ns')
scrollbar_h.grid(row=1, column=0, sticky='ew')
canvas_frame.grid_rowconfigure(0, weight=1)
canvas_frame.grid_columnconfigure(0, weight=1)
# 하단: 버튼들
button_frame = ttk.Frame(main_frame)
button_frame.pack(fill='x', pady=(10, 0))
# 버튼 배치
ttk.Button(button_frame, text="📂 시스템으로 열기",
command=self.open_with_system).pack(side='left', padx=(0, 5))
ttk.Button(button_frame, text="📁 폴더 열기",
command=self.open_folder).pack(side='left', padx=5)
ttk.Button(button_frame, text="🔄 새로고침",
command=self.refresh_preview).pack(side='left', padx=5)
# 오른쪽에 닫기 버튼
ttk.Button(button_frame, text="닫기",
command=self.close_dialog).pack(side='right')
def load_preview_image(self):
"""미리보기 이미지 로드"""
try:
if 'error' in self.pdf_info:
# 오류 메시지 표시
self.canvas.create_text(200, 100,
text=f"미리보기를 생성할 수 없습니다.\n{self.pdf_info['error']}",
fill='red', font=('맑은 고딕', 12), anchor='center')
return
# 미리보기 이미지 생성
thumbnail = self.preview_manager.create_preview_thumbnail(self.pdf_path, (350, 500))
if thumbnail:
# PIL Image를 PhotoImage로 변환
self.photo = ImageTk.PhotoImage(thumbnail)
# 캔버스에 이미지 표시
self.canvas.delete("all") # 기존 내용 제거
self.canvas.create_image(thumbnail.width//2, thumbnail.height//2,
image=self.photo, anchor='center')
# 스크롤 영역 설정
self.canvas.configure(scrollregion=self.canvas.bbox("all"))
else:
# 미리보기 실패 메시지
self.canvas.create_text(200, 100,
text="미리보기를 생성할 수 없습니다.\nPDF 파일이 손상되었거나\n지원하지 않는 형식일 수 있습니다.",
fill='red', font=('맑은 고딕', 12), anchor='center')
except Exception as e:
# 예외 발생시 오류 메시지 표시
self.canvas.create_text(200, 100,
text=f"미리보기 로드 실패:\n{str(e)}",
fill='red', font=('맑은 고딕', 12), anchor='center')
def open_with_system(self):
"""시스템 기본 프로그램으로 열기"""
success = self.preview_manager.open_file_with_system(self.pdf_path)
if not success:
messagebox.showerror("오류", "파일을 열 수 없습니다.\n기본 PDF 뷰어가 설치되어 있는지 확인해주세요.")
def open_folder(self):
"""파일이 있는 폴더 열기"""
try:
folder_path = os.path.dirname(self.pdf_path)
if platform.system() == 'Windows':
subprocess.run(['explorer', '/select,', self.pdf_path])
elif platform.system() == 'Darwin': # macOS
subprocess.run(['open', '-R', self.pdf_path])
else: # Linux
subprocess.run(['xdg-open', folder_path])
except Exception as e:
messagebox.showerror("오류", f"폴더를 열 수 없습니다:\n{str(e)}")
def refresh_preview(self):
"""미리보기 새로고침"""
# 캐시에서 제거
cache_key = f"{self.pdf_path}_(350, 500)"
if cache_key in self.preview_manager.preview_cache:
del self.preview_manager.preview_cache[cache_key]
# PDF 정보 다시 로드
self.pdf_info = self.preview_manager.get_pdf_info(self.pdf_path)
# 미리보기 이미지 다시 로드
self.load_preview_image()
messagebox.showinfo("새로고침", "미리보기가 새로고침되었습니다.")
def close_dialog(self):
"""다이얼로그 닫기"""
self.window.grab_release() # 모달 해제
self.window.destroy()