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"""
{text_html_content}
""" media_html_with_style = f"""
{media_html_content}
""" print("HTML 내용 준비 완료") # 텍스트 HTML과 미디어 HTML 반환 return text_html_with_style, media_html_with_style except Exception as e: import traceback error_details = traceback.format_exc() print(f"오류 발생: {str(e)}\n{error_details}") error_html = 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), "

PDF 파일을 업로드해주세요

", "

PDF 파일을 업로드해주세요

", "PDF 파일이 업로드되지 않았습니다.", ) try: # 변환 함수 호출 text_html, media_html = convert_pdf_to_html(pdf_file) # HTML 내용 디버깅 print(f"텍스트 HTML 길이: {len(text_html)} 바이트") print(f"미디어 HTML 길이: {len(media_html)} 바이트") # HTML 표시 영역 보이기 및 내용 업데이트 return gr.update(visible=True), text_html, media_html, "변환 완료!" except Exception as e: import traceback error_details = traceback.format_exc() print(f"처리 중 오류: {str(e)}\n{error_details}") error_html = f"

오류 발생

{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()