import os import gradio as gr from pathlib import Path import base64 # PDF to HTML 변환기 클래스 임포트 - 수정된 버전 사용 from convert import PDFToHTMLConverter def convert_pdf_to_html(pdf_file): """PDF 파일을 HTML로 변환하고 결과 반환""" try: # 현재 작업 디렉토리 확인 current_dir = Path.cwd() temp_dir = current_dir / ".temp" # PDF 데이터 준비 if hasattr(pdf_file, "name"): # Gradio 파일 객체인 경우 with open(pdf_file.name, "rb") as f: pdf_data = f.read() else: # 이미 바이너리 데이터인 경우 pdf_data = pdf_file # 고정 경로에 PDF 저장 pdf_input_dir = temp_dir / "temp_input_pdf" pdf_input_dir.mkdir(exist_ok=True, parents=True) pdf_path = pdf_input_dir / "current.pdf" # PDF 저장 with open(pdf_path, "wb") as f: f.write(pdf_data) print(f"PDF 저장 완료: {pdf_path}") # PDF 변환 - 텍스트 HTML과 미디어 HTML로 분리 converter = PDFToHTMLConverter(str(pdf_path)) text_html_path, media_html_path = converter.convert() print(f"HTML 변환 완료: {text_html_path}, {media_html_path}") # HTML 파일 읽기 with open(text_html_path, "r", encoding="utf-8") as f: text_html_content = f.read() with open(media_html_path, "r", encoding="utf-8") as f: media_html_content = f.read() # 이미지를 Base64로 인코딩하여 HTML에 직접 포함 img_dir_path = temp_dir / "temp_output_html" / "images" if img_dir_path.exists(): print(f"이미지 디렉토리 확인: {img_dir_path}") for img_file in img_dir_path.glob("*.*"): try: rel_path = f"images/{img_file.name}" print(f"이미지 처리 중: {img_file}") # 이미지 파일 읽기 with open(img_file, "rb") as f: encoded_string = base64.b64encode(f.read()).decode("utf-8") # 이미지 타입에 따라 MIME 타입 설정 ext = img_file.suffix.lower()[1:] # .png -> png mime_type = { "png": "image/png", "jpg": "image/jpeg", "jpeg": "image/jpeg", "gif": "image/gif", "svg": "image/svg+xml", }.get(ext, "image/png") # Base64 이미지 URL 생성 data_url = f"data:{mime_type};base64,{encoded_string}" # 미디어 HTML 내용에서 이미지 경로 교체 original_pattern = f'src="{rel_path}"' replacement = f'src="{data_url}"' if original_pattern in media_html_content: media_html_content = media_html_content.replace( original_pattern, replacement ) print(f"이미지 {img_file.name} Base64 인코딩 완료") else: print( f"경고: 이미지 경로 '{rel_path}'를 HTML에서 찾을 수 없습니다" ) except Exception as e: print(f"이미지 {img_file.name} 처리 중 오류: {str(e)}") else: print(f"이미지 디렉토리가 존재하지 않음: {img_dir_path}") # 스크롤 가능한 컨테이너로 HTML 컨텐츠 래핑 text_html_with_style = f"""
{str(e)}
{error_details}"
return error_html, error_html
def launch_web_interface():
"""Gradio 웹 인터페이스 실행"""
# CSS 스타일
css = """
/* 전체 레이아웃 */
body, .gradio-container {
margin: 0 !important;
padding: 0 !important;
width: 100% !important;
max-width: none !important;
background-color: #1f1f1f;
}
/* 헤더 영역 */
.header-area {
background-color: #2a2a2a;
padding: 1rem;
border-bottom: 1px solid #444;
margin-bottom: 1rem;
}
/* 업로드 영역 */
.upload-area {
background-color: #2a2a2a;
padding: 1rem;
border-radius: 5px;
margin-bottom: 1rem;
}
/* HTML 뷰어 컨테이너 */
.html-columns {
display: flex;
gap: 20px;
}
.html-column {
flex: 1;
min-width: 0;
}
/* HTML 뷰어 */
.html-display {
min-height: 800px !important;
width: 100% !important;
background-color: #2a2a2a !important;
}
/* HTML 내용의 텍스트 색상 */
.html-display * {
color: #ffffff !important;
}
/* HTML 내의 표 스타일 */
.html-display table {
background-color: #333 !important;
border: 1px solid #555 !important;
}
.html-display td,
.html-display th {
border: 1px solid #555 !important;
color: #fff !important;
}
/* 버튼 스타일 */
.convert-button {
background-color: #E67E22 !important;
border: none !important;
}
/* 타이틀 텍스트 */
.title-text {
color: white !important;
margin: 0 !important;
padding: 0 !important;
}
/* 설명 텍스트 */
.description-text {
color: #aaa !important;
margin-top: 0.5rem !important;
}
/* 컬럼 제목 */
.column-title {
color: white !important;
margin-bottom: 0.5rem !important;
}
/* 푸터 */
.footer-area {
margin-top: 2rem;
text-align: center;
color: #888;
padding: 1rem;
}
"""
# Gradio 인터페이스
with gr.Blocks(css=css, theme=gr.themes.Default()) as demo:
# 헤더 섹션
with gr.Column(elem_classes="header-area"):
gr.Markdown("# PDF to HTML 변환기", elem_classes="title-text")
gr.Markdown(
"PDF 파일을 업로드하여 텍스트와 미디어로 분리된 HTML을 생성합니다.",
elem_classes="description-text",
)
# 업로드 섹션
with gr.Column(elem_classes="upload-area"):
# 파일 업로드 및 변환 버튼
with gr.Row():
pdf_input = gr.File(
label="PDF 파일 업로드", type="binary", elem_id="pdf-upload"
)
convert_btn = gr.Button(
"변환하기", variant="primary", elem_classes="convert-button"
)
# 상태 표시
status_output = gr.Textbox(
label="상태", value="대기 중...", interactive=False
)
# HTML 뷰어 영역 (두 열로 구성)
with gr.Column(visible=False) as html_output_area:
with gr.Row(elem_classes="html-columns"):
# 왼쪽 열 - 텍스트 HTML
with gr.Column(elem_classes="html-column"):
gr.Markdown("### 텍스트 내용", elem_classes="column-title")
text_html_viewer = gr.HTML(
label="텍스트 HTML",
elem_id="text-html-viewer",
elem_classes="html-display",
)
# 오른쪽 열 - 미디어 HTML
with gr.Column(elem_classes="html-column"):
gr.Markdown("### 표 및 이미지", elem_classes="column-title")
media_html_viewer = gr.HTML(
label="미디어 HTML",
elem_id="media-html-viewer",
elem_classes="html-display",
)
# 푸터
with gr.Column(elem_classes="footer-area"):
gr.Markdown("© 2025 pdf2html")
# 변환 처리 함수
def process_conversion(pdf_file):
if pdf_file is None:
return (
gr.update(visible=False),
"{str(e)}
" return ( gr.update(visible=False), error_html, error_html, f"오류: {str(e)}", ) # 변환 버튼 클릭 이벤트 convert_btn.click( fn=process_conversion, inputs=pdf_input, outputs=[ html_output_area, text_html_viewer, media_html_viewer, status_output, ], ) # 레이아웃 문제 해결을 위한 JavaScript demo.load( js=""" function fixLayout() { // HTML 뷰어 컨테이너 확인 const htmlColumns = document.querySelector('.html-columns'); if (htmlColumns) { // 컨테이너의 너비 균등하게 맞추기 const columns = htmlColumns.querySelectorAll('.html-column'); columns.forEach(column => { column.style.flex = '1'; column.style.minWidth = '0'; }); } // 텍스트 색상 강제 설정 const textViewer = document.getElementById('text-html-viewer'); const mediaViewer = document.getElementById('media-html-viewer'); function forceTextColor(element) { if (!element) return; // iframe 내부 문서에 접근 try { const iframes = element.querySelectorAll('iframe'); iframes.forEach(iframe => { if (iframe.contentDocument) { const allElements = iframe.contentDocument.querySelectorAll('*'); allElements.forEach(el => { if (el.tagName !== 'IMG') { el.style.color = '#ffffff'; } }); // 배경색 설정 const body = iframe.contentDocument.body; if (body) { body.style.backgroundColor = '#2a2a2a'; } } }); } catch (e) { console.error('iframe 접근 중 오류:', e); } // 직접 문서 내 요소에 색상 설정 const allTextElements = element.querySelectorAll('p, h1, h2, h3, h4, h5, h6, span, div, a, li, td, th'); allTextElements.forEach(el => { el.style.color = '#ffffff'; }); } forceTextColor(textViewer); forceTextColor(mediaViewer); // 이미지 표시 확인 if (mediaViewer) { const images = mediaViewer.querySelectorAll('img'); console.log(`미디어 뷰어에서 이미지 ${images.length}개 발견`); images.forEach((img, index) => { // 이미지 로드 상태 확인 console.log(`이미지 ${index + 1} 로드 상태: ${img.complete ? '완료' : '로딩 중'}`); if (img.complete && img.naturalWidth === 0) { console.log(`이미지 ${index + 1} 로드 실패`); } }); } } // 페이지 로드 시 레이아웃 조정 window.addEventListener('load', function() { setTimeout(fixLayout, 1000); setTimeout(fixLayout, 3000); setTimeout(fixLayout, 5000); // 더 긴 시간 후에도 한 번 더 실행 }); // MutationObserver로 DOM 변경 감지 const observer = new MutationObserver(mutations => { setTimeout(fixLayout, 500); }); // 페이지 로드 후 Observer 시작 window.addEventListener('load', () => { observer.observe(document.body, { childList: true, subtree: true, attributes: true }); // 스타일 요소 직접 추가 const style = document.createElement('style'); style.textContent = ` .html-display * { color: #ffffff !important; } .html-display { background-color: #2a2a2a !important; } `; document.head.appendChild(style); }); """ ) # 인터페이스 실행 demo.launch(share=False, inbrowser=True, show_api=False) if __name__ == "__main__": launch_web_interface()