- 프로그램명 : XLS2PDF.py
- 제작기간 : '25. 7. 14.(1일)
- 제작자 : REDUCTO
- 사용언어 : Python 3.13
- 사용라이브러리 : 하단 requirements.txt 참조
- 버전 : v1.0
소개
기술사공부를 하다가 평소 gemini AI와의 대화(Live)로 연습을 하는데 얘한테 input source로 제가 공부하고 있는 파일을 주고 싶어서 만들어 보았습니다. 사실 지난번에 JSON으로 빼는걸 만들었는데, 계층이 너무 깊어지기도 하고 PDF로 빼면 이번에 출시된 NoteBook LLM에 이용할 수 있지 않을까 해서 구채여 만들어 보았습니다
* 무단배포는 금지합니다.(댓글달아주세용)
* 기능에 커스터마이징이 필요하시다면 댓글달아주세용
사용법
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
import os
import sys
import threading
import queue
import time
from tkinterdnd2 import TkinterDnD, DND_FILES
from ttkthemes import ThemedStyle
def convert_by_text_extraction(xlsx_path, pdf_path, sheet_to_convert, progress_queue):
try:
from openpyxl import load_workbook
from reportlab.pdfgen import canvas
from reportlab.lib.pagesizes import letter
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
except ImportError:
progress_queue.put(('error', '텍스트 변환에 필요한 라이브러리(openpyxl, reportlab)가 없습니다.'))
return
progress_queue.put(('status', '폰트 설정 및 파일 분석 중...'))
FONT_PATH = "c:/Windows/Fonts/malgun.ttf"
FONT_NAME = "MalgunGothic"
if os.path.exists(FONT_PATH):
pdfmetrics.registerFont(TTFont(FONT_NAME, FONT_PATH))
else:
progress_queue.put(('error', f'한글 폰트를 찾을 수 없습니다:\n{FONT_PATH}'))
return
try:
workbook = load_workbook(xlsx_path, read_only=True)
c = canvas.Canvas(pdf_path, pagesize=letter)
width, height = letter
sheets_to_process = []
if sheet_to_convert == "모든 시트":
sheets_to_process = workbook.sheetnames
elif sheet_to_convert in workbook.sheetnames:
sheets_to_process.append(sheet_to_convert)
else:
progress_queue.put(('error', f"'{sheet_to_convert}' 시트를 찾을 수 없습니다."))
return
total_sheets = len(sheets_to_process)
for i, sheet_name in enumerate(sheets_to_process):
progress_queue.put(('status', f"'{sheet_name}' 시트 변환 중 ({i+1}/{total_sheets})..."))
progress_queue.put(('progress', int((i / total_sheets) * 100)))
sheet = workbook[sheet_name]
if i > 0: c.showPage()
y_pos = height - 50
c.setFont(FONT_NAME, 14)
c.drawString(50, y_pos, f"--- {sheet.title} ---")
y_pos -= 30
c.setFont(FONT_NAME, 9)
for row in sheet.iter_rows():
if y_pos < 50:
c.showPage()
c.setFont(FONT_NAME, 9)
y_pos = height - 50
line = ' '.join(str(cell.value if cell.value is not None else '') for cell in row)
c.drawString(50, y_pos, line)
y_pos -= 14
c.save()
progress_queue.put(('done', f"성공적으로 변환했습니다:\n{pdf_path}"))
except Exception as e:
progress_queue.put(('error', f"텍스트 변환 중 오류 발생:\n{e}"))
def convert_by_excel_export(xlsx_path, pdf_path, sheet_to_convert, progress_queue):
try:
import win32com.client
except ImportError:
progress_queue.put(('error', "'엑셀로 인쇄' 방식을 사용하려면 pywin32 라이브러리가 필요합니다."))
return
excel = None
workbook = None
try:
progress_queue.put(('status', 'Excel 프로그램 실행 중...'))
progress_queue.put(('progress', 10))
excel = win32com.client.Dispatch("Excel.Application")
excel.Visible = False
excel.DisplayAlerts = False
progress_queue.put(('status', f"'{os.path.basename(xlsx_path)}' 파일 여는 중..."))
progress_queue.put(('progress', 30))
workbook = excel.Workbooks.Open(xlsx_path)
progress_queue.put(('status', 'PDF로 내보내는 중...'))
progress_queue.put(('progress', 70))
if sheet_to_convert == "모든 시트":
workbook.ExportAsFixedFormat(0, pdf_path, 0, True, False)
elif sheet_to_convert in [s.Name for s in workbook.Sheets]:
ws = workbook.Worksheets[sheet_to_convert]
ws.ExportAsFixedFormat(0, pdf_path, 0, True, False)
else:
progress_queue.put(('error', f"'{sheet_to_convert}' 시트를 찾을 수 없습니다."))
return
progress_queue.put(('done', f"성공적으로 변환했습니다:\n{pdf_path}"))
except Exception as e:
progress_queue.put(('error', f"엑셀 변환 중 오류 발생:\n{e}\n\nExcel이 설치되어 있는지 확인하세요."))
finally:
if workbook:
workbook.Close(SaveChanges=False)
if excel:
excel.Quit()
class App(ttk.Frame):
def __init__(self, master):
super().__init__(master, padding="10")
self.master = master
self.progress_queue = queue.Queue()
self.create_widgets()
self.master.after(100, self.process_queue)
def create_widgets(self):
drop_zone = ttk.Label(self, text="이곳에 XLSX 파일을 드래그하세요", relief="solid", padding="20", anchor=tk.CENTER)
drop_zone.pack(fill=tk.X, pady=(0, 10))
drop_zone.drop_target_register(DND_FILES)
drop_zone.dnd_bind('<<Drop>>', self.handle_drop)
file_frame = ttk.Frame(self)
file_frame.pack(fill=tk.X, expand=True, pady=5)
ttk.Label(file_frame, text="엑셀 파일:").pack(side=tk.LEFT, padx=(0, 5))
self.entry_file_path = ttk.Entry(file_frame)
self.entry_file_path.pack(side=tk.LEFT, expand=True, fill=tk.X, padx=(0, 10))
self.btn_select_file = ttk.Button(file_frame, text="파일 찾기", command=self.select_file)
self.btn_select_file.pack(side=tk.LEFT)
options_frame = ttk.Frame(self)
options_frame.pack(fill=tk.X, expand=True, pady=5)
ttk.Label(options_frame, text="시트 선택:").pack(side=tk.LEFT, padx=(0, 10))
self.sheet_combobox = ttk.Combobox(options_frame, state="readonly")
self.sheet_combobox.pack(side=tk.LEFT, expand=True, fill=tk.X, padx=(0, 20))
self.conversion_method = tk.StringVar(value="excel")
ttk.Radiobutton(options_frame, text="엑셀로 인쇄", variable=self.conversion_method, value="excel").pack(side=tk.LEFT)
ttk.Radiobutton(options_frame, text="텍스트 추출", variable=self.conversion_method, value="text").pack(side=tk.LEFT, padx=(5,0))
self.btn_convert = ttk.Button(self, text="PDF로 변환", command=self.start_conversion, style='Accent.TButton')
self.btn_convert.pack(fill=tk.X, ipady=5, pady=10)
status_frame = ttk.Frame(self, padding="10 0 0 0")
status_frame.pack(fill=tk.X, expand=True)
self.status_label = ttk.Label(status_frame, text="준비 완료")
self.status_label.pack(fill=tk.X)
self.progress_bar = ttk.Progressbar(status_frame, mode='determinate')
self.progress_bar.pack(fill=tk.X, pady=5)
def handle_drop(self, event):
file_path = self.master.tk.splitlist(event.data)[0]
if file_path.lower().endswith('.xlsx'):
self.entry_file_path.delete(0, tk.END)
self.entry_file_path.insert(0, os.path.abspath(file_path))
self.update_sheet_list()
else:
messagebox.showwarning("파일 오류", "XLSX 파일만 드롭할 수 있습니다.")
def select_file(self):
file_path = filedialog.askopenfilename(title="변환할 XLSX 파일 선택", filetypes=[("XLSX files", "*.xlsx")])
if file_path:
self.entry_file_path.delete(0, tk.END)
self.entry_file_path.insert(0, os.path.abspath(file_path))
self.update_sheet_list()
def update_sheet_list(self):
from openpyxl import load_workbook
xlsx_path = self.entry_file_path.get()
if os.path.exists(xlsx_path):
try:
workbook = load_workbook(xlsx_path, read_only=True)
sheet_names = workbook.sheetnames
self.sheet_combobox['values'] = ["모든 시트"] + sheet_names
self.sheet_combobox.current(0)
workbook.close()
except Exception as e:
self.sheet_combobox['values'] = []
messagebox.showerror("파일 오류", f"엑셀 파일을 분석할 수 없습니다:\n{e}")
else:
self.sheet_combobox['values'] = []
def start_conversion(self):
xlsx_path = self.entry_file_path.get()
if not xlsx_path:
messagebox.showwarning("입력 오류", "먼저 XLSX 파일을 선택해주세요.")
return
selected_sheet = self.sheet_combobox.get()
if not selected_sheet:
messagebox.showwarning("입력 오류", "변환할 시트를 선택해주세요.")
return
self.btn_convert.config(state=tk.DISABLED)
self.progress_bar['value'] = 0
base_name = os.path.splitext(os.path.basename(xlsx_path))[0]
dir_name = os.path.dirname(xlsx_path)
sheet_name_for_file = "전체" if selected_sheet == "모든 시트" else selected_sheet
pdf_path = os.path.join(dir_name, f"{base_name}_{sheet_name_for_file}.pdf")
method = self.conversion_method.get()
target_func = convert_by_excel_export if method == "excel" else convert_by_text_extraction
thread = threading.Thread(
target=target_func,
args=(xlsx_path, pdf_path, selected_sheet, self.progress_queue)
)
thread.daemon = True
thread.start()
def process_queue(self):
try:
message = self.progress_queue.get_nowait()
msg_type, msg_data = message
if msg_type == 'status':
self.status_label.config(text=msg_data)
elif msg_type == 'progress':
self.progress_bar['value'] = msg_data
elif msg_type == 'done':
self.status_label.config(text="변환 완료!")
self.progress_bar['value'] = 100
messagebox.showinfo("성공", msg_data)
self.btn_convert.config(state=tk.NORMAL)
elif msg_type == 'error':
self.status_label.config(text="오류 발생")
self.progress_bar['value'] = 0
messagebox.showerror("오류", msg_data)
self.btn_convert.config(state=tk.NORMAL)
except queue.Empty:
pass
finally:
self.master.after(100, self.process_queue)
if __name__ == "__main__":
root = TkinterDnD.Tk()
root.title("XLSX to PDF 변환기")
root.geometry("600x400")
style = ThemedStyle(root)
style.set_theme("arc")
style.configure('Accent.TButton', font=('Helvetica', 10, 'bold'))
app = App(root)
app.pack(fill="both", expand=True)
root.mainloop()
필요 라이브러리
* 사전준비
- python이 설치되어있어야합니다. 코드를 실행하기 위해서 해당 requirement.txt를 pip install 해주세요
# PIP 라이브러리 설치하기
pip install -r requirement.txt
# XLS2PDF 실행하식
python .\XlS2PDF.py
- 필자의 환경은 python 3.13.0입니다. 가급적 맞추어주시거나 상위버전을 사용해 주시는게 좋습니다.
단순하게 생겼죠? Drag N Drop을 지원하고 시트를 선택해서 엑셀로 출력할 수 있습니다. 텍스트만 뽑을 수도 있습니다. 주의사항으로 확장자가 .xlsx만 받아들여지니 참고해주세용
* 동작이 converter.log에 기록됩니다 참고해주세요
'프로그램 > Python Project' 카테고리의 다른 글
[Python Project] TRIZ와 SCAMPER, 그런데 이제 Gemini가 조금 들어간 -TRIZSolver- (0) | 2025.05.17 |
---|---|
[Python Project] (코드) 소스코드 주석삭제 -CommentKiller- (0) | 2025.05.15 |
[Python Project] (코드) 엑셀JSON변환 -XSL2JSON- (0) | 2025.05.13 |
[Python Project] 사진이어붙이기 -PhotoCollage- (0) | 2025.04.17 |
[Python] comcbt 중복문제 제거하기 (0) | 2023.03.05 |