| """ |
| PDF to HTML Converter - Hugging Face Space |
| แปลง PDF เป็น HTML พร้อมรักษา layout และ text ที่เลือกได้ |
| |
| ใช้ PyMuPDF (fitz) - ไม่ต้องติดตั้ง pdf2htmlEX |
| """ |
|
|
| import base64 |
| import io |
| import os |
| import tempfile |
| import time |
| from pathlib import Path |
|
|
| import fitz |
| import gradio as gr |
|
|
| |
|
|
| TITLE = "📄 PDF to HTML Converter" |
| DESCRIPTION = """ |
| แปลง PDF เป็น HTML ที่รักษา layout เหมือนต้นฉบับ พร้อม: |
| - ✅ รักษาตำแหน่งข้อความตาม PDF |
| - ✅ ข้อความเลือก/copy ได้ |
| - ✅ เปิดได้ในทุก browser |
| - ✅ รองรับภาษาไทย |
| |
| **วิธีใช้:** อัปโหลด PDF → ปรับตั้งค่า → กดแปลง → ดาวน์โหลด HTML |
| """ |
|
|
| |
|
|
|
|
| def get_file_size(size_bytes): |
| """รับขนาดไฟล์แบบ human readable""" |
| for unit in ["B", "KB", "MB", "GB"]: |
| if size_bytes < 1024: |
| return f"{size_bytes:.1f} {unit}" |
| size_bytes /= 1024 |
| return f"{size_bytes:.1f} TB" |
|
|
|
|
| def extract_text_with_positions(page, scale=1.5): |
| """ดึงข้อความพร้อมตำแหน่งจากหน้า PDF""" |
| blocks = [] |
|
|
| |
| text_dict = page.get_text("dict", flags=fitz.TEXT_PRESERVE_WHITESPACE) |
|
|
| for block in text_dict.get("blocks", []): |
| if block.get("type") == 0: |
| for line in block.get("lines", []): |
| for span in line.get("spans", []): |
| text = span.get("text", "").strip() |
| if not text: |
| continue |
|
|
| bbox = span.get("bbox", [0, 0, 0, 0]) |
| font_size = span.get("size", 12) |
| font_name = span.get("font", "sans-serif") |
| color = span.get("color", 0) |
|
|
| |
| if isinstance(color, int): |
| hex_color = f"#{color:06x}" |
| else: |
| hex_color = "#000000" |
|
|
| blocks.append( |
| { |
| "text": text, |
| "x": bbox[0] * scale, |
| "y": bbox[1] * scale, |
| "width": (bbox[2] - bbox[0]) * scale, |
| "height": (bbox[3] - bbox[1]) * scale, |
| "font_size": font_size * scale, |
| "font_name": font_name, |
| "color": hex_color, |
| } |
| ) |
|
|
| return blocks |
|
|
|
|
| def render_page_as_image(page, scale=1.5, image_format="png"): |
| """Render หน้า PDF เป็นรูปภาพ""" |
| mat = fitz.Matrix(scale, scale) |
| pix = page.get_pixmap(matrix=mat, alpha=False) |
|
|
| if image_format == "png": |
| img_data = pix.tobytes("png") |
| else: |
| img_data = pix.tobytes("jpeg") |
|
|
| return base64.b64encode(img_data).decode("utf-8") |
|
|
|
|
| def generate_html(pages_data, title="PDF Document", include_background=True): |
| """สร้าง HTML จากข้อมูลหน้า PDF""" |
|
|
| html_pages = [] |
|
|
| for i, page_data in enumerate(pages_data): |
| text_elements = [] |
|
|
| for block in page_data["texts"]: |
| |
| text = ( |
| block["text"] |
| .replace("&", "&") |
| .replace("<", "<") |
| .replace(">", ">") |
| ) |
|
|
| style = ( |
| f""" |
| position: absolute; |
| left: {block["x"]:.1f}px; |
| top: {block["y"]:.1f}px; |
| font-size: {block["font_size"]:.1f}px; |
| color: {block["color"]}; |
| white-space: pre; |
| pointer-events: auto; |
| cursor: text; |
| user-select: text; |
| """.strip() |
| .replace("\n", "") |
| .replace(" ", " ") |
| ) |
|
|
| text_elements.append(f'<span style="{style}">{text}</span>') |
|
|
| |
| if include_background and page_data.get("image"): |
| bg_style = f"background-image: url('data:image/png;base64,{page_data['image']}'); background-size: cover;" |
| else: |
| bg_style = "background: white;" |
|
|
| page_html = f""" |
| <div class="page" style="width: {page_data["width"]:.0f}px; height: {page_data["height"]:.0f}px; {bg_style}"> |
| <div class="text-layer"> |
| {"".join(text_elements)} |
| </div> |
| </div> |
| """ |
| html_pages.append(page_html) |
|
|
| |
| html = f"""<!DOCTYPE html> |
| <html lang="th"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>{title}</title> |
| <style> |
| * {{ |
| box-sizing: border-box; |
| margin: 0; |
| padding: 0; |
| }} |
| body {{ |
| font-family: 'Sarabun', 'Noto Sans Thai', sans-serif; |
| background: #e2e8f0; |
| padding: 20px; |
| display: flex; |
| flex-direction: column; |
| align-items: center; |
| gap: 20px; |
| }} |
| .page {{ |
| position: relative; |
| background: white; |
| box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15); |
| overflow: hidden; |
| }} |
| .text-layer {{ |
| position: absolute; |
| inset: 0; |
| overflow: hidden; |
| }} |
| .text-layer span {{ |
| position: absolute; |
| }} |
| .text-layer span::selection {{ |
| background: rgba(37, 99, 235, 0.3); |
| }} |
| @media print {{ |
| body {{ |
| background: white; |
| padding: 0; |
| gap: 0; |
| }} |
| .page {{ |
| box-shadow: none; |
| page-break-after: always; |
| }} |
| }} |
| </style> |
| <link href="https://fonts.googleapis.com/css2?family=Sarabun:wght@400;600;700&display=swap" rel="stylesheet"> |
| </head> |
| <body> |
| {"".join(html_pages)} |
| </body> |
| </html> |
| """ |
| return html |
|
|
|
|
| def convert_pdf_to_html( |
| pdf_file, scale, include_background, selected_pages, progress=gr.Progress() |
| ): |
| """ |
| แปลง PDF เป็น HTML โดยใช้ PyMuPDF |
| """ |
| if pdf_file is None: |
| return None, "❌ กรุณาอัปโหลดไฟล์ PDF", "", None |
|
|
| progress(0.1, desc="กำลังเปิดไฟล์ PDF...") |
|
|
| try: |
| |
| input_path = pdf_file.name if hasattr(pdf_file, "name") else pdf_file |
| doc = fitz.open(input_path) |
|
|
| total_pages = len(doc) |
| pdf_name = os.path.basename(input_path) |
| pdf_base = os.path.splitext(pdf_name)[0] |
|
|
| |
| if selected_pages.strip(): |
| try: |
| page_indices = [] |
| for part in selected_pages.split(","): |
| part = part.strip() |
| if "-" in part: |
| start, end = map(int, part.split("-")) |
| page_indices.extend(range(start - 1, min(end, total_pages))) |
| else: |
| idx = int(part) - 1 |
| if 0 <= idx < total_pages: |
| page_indices.append(idx) |
| page_indices = sorted(set(page_indices)) |
| except: |
| page_indices = list(range(total_pages)) |
| else: |
| page_indices = list(range(total_pages)) |
|
|
| if not page_indices: |
| page_indices = list(range(total_pages)) |
|
|
| progress(0.2, desc=f"กำลังประมวลผล {len(page_indices)} หน้า...") |
|
|
| pages_data = [] |
| start_time = time.time() |
|
|
| for i, page_idx in enumerate(page_indices): |
| progress( |
| 0.2 + (0.7 * (i + 1) / len(page_indices)), |
| desc=f"กำลังแปลงหน้า {page_idx + 1}/{total_pages}...", |
| ) |
|
|
| page = doc[page_idx] |
| rect = page.rect |
|
|
| |
| texts = extract_text_with_positions(page, scale) |
|
|
| |
| image = None |
| if include_background: |
| image = render_page_as_image(page, scale) |
|
|
| pages_data.append( |
| { |
| "page_num": page_idx + 1, |
| "width": rect.width * scale, |
| "height": rect.height * scale, |
| "texts": texts, |
| "image": image, |
| } |
| ) |
|
|
| doc.close() |
|
|
| progress(0.9, desc="กำลังสร้าง HTML...") |
|
|
| |
| html_content = generate_html( |
| pages_data, title=pdf_base, include_background=include_background |
| ) |
|
|
| |
| temp_dir = tempfile.mkdtemp() |
| output_path = os.path.join(temp_dir, f"{pdf_base}.html") |
|
|
| with open(output_path, "w", encoding="utf-8") as f: |
| f.write(html_content) |
|
|
| elapsed = time.time() - start_time |
| input_size = os.path.getsize(input_path) |
| output_size = os.path.getsize(output_path) |
|
|
| |
| total_texts = sum(len(p["texts"]) for p in pages_data) |
|
|
| progress(1.0, desc="เสร็จสิ้น!") |
|
|
| |
| status = f"""✅ **แปลงสำเร็จ!** |
| |
| 📊 **สถิติ:** |
| | รายการ | ค่า | |
| |--------|-----| |
| | หน้าที่แปลง | {len(page_indices)} / {total_pages} หน้า | |
| | ข้อความที่พบ | {total_texts} รายการ | |
| | ไฟล์ต้นฉบับ | {get_file_size(input_size)} | |
| | ไฟล์ HTML | {get_file_size(output_size)} | |
| | เวลาที่ใช้ | {elapsed:.1f} วินาที | |
| | Scale | {scale}x | |
| | รวม Background | {"✅" if include_background else "❌"} | |
| """ |
|
|
| |
| preview = html_content[:50000] |
| if len(html_content) > 50000: |
| preview += "\n\n... (truncated for preview)" |
|
|
| return output_path, status, preview, html_content |
|
|
| except Exception as e: |
| import traceback |
|
|
| error_detail = traceback.format_exc() |
| return None, f"❌ เกิดข้อผิดพลาด: {str(e)}\n\n```\n{error_detail}\n```", "", None |
|
|
|
|
| def extract_text_only(pdf_file, progress=gr.Progress()): |
| """ดึงเฉพาะข้อความจาก PDF""" |
| if pdf_file is None: |
| return "❌ กรุณาอัปโหลดไฟล์ PDF" |
|
|
| progress(0.2, desc="กำลังเปิดไฟล์...") |
|
|
| try: |
| input_path = pdf_file.name if hasattr(pdf_file, "name") else pdf_file |
| doc = fitz.open(input_path) |
|
|
| all_text = [] |
| total_pages = len(doc) |
|
|
| for i, page in enumerate(doc): |
| progress( |
| 0.2 + (0.7 * (i + 1) / total_pages), desc=f"หน้า {i + 1}/{total_pages}" |
| ) |
| text = page.get_text("text") |
| if text.strip(): |
| all_text.append(f"--- หน้า {i + 1} ---\n{text}") |
|
|
| doc.close() |
|
|
| progress(1.0, desc="เสร็จสิ้น!") |
|
|
| if all_text: |
| return "\n\n".join(all_text) |
| else: |
| return "❌ ไม่พบข้อความใน PDF (อาจเป็นไฟล์ที่ scan มา)" |
|
|
| except Exception as e: |
| return f"❌ เกิดข้อผิดพลาด: {str(e)}" |
|
|
|
|
| def extract_as_json(pdf_file, scale, progress=gr.Progress()): |
| """ดึงข้อมูลเป็น JSON พร้อมพิกัด""" |
| if pdf_file is None: |
| return "❌ กรุณาอัปโหลดไฟล์ PDF" |
|
|
| progress(0.2, desc="กำลังเปิดไฟล์...") |
|
|
| try: |
| import json |
|
|
| input_path = pdf_file.name if hasattr(pdf_file, "name") else pdf_file |
| doc = fitz.open(input_path) |
|
|
| result = { |
| "filename": os.path.basename(input_path), |
| "total_pages": len(doc), |
| "pages": [], |
| } |
|
|
| for i, page in enumerate(doc): |
| progress(0.2 + (0.7 * (i + 1) / len(doc)), desc=f"หน้า {i + 1}/{len(doc)}") |
|
|
| rect = page.rect |
| texts = extract_text_with_positions(page, scale) |
|
|
| result["pages"].append( |
| { |
| "page_num": i + 1, |
| "width": rect.width * scale, |
| "height": rect.height * scale, |
| "text_count": len(texts), |
| "texts": texts, |
| } |
| ) |
|
|
| doc.close() |
|
|
| progress(1.0, desc="เสร็จสิ้น!") |
|
|
| return json.dumps(result, ensure_ascii=False, indent=2) |
|
|
| except Exception as e: |
| return f"❌ เกิดข้อผิดพลาด: {str(e)}" |
|
|
|
|
| |
|
|
| with gr.Blocks( |
| title=TITLE, |
| theme=gr.themes.Soft(), |
| css=""" |
| .output-html { max-height: 400px; overflow: auto; font-family: monospace; font-size: 12px; } |
| .status-box { font-size: 14px; } |
| footer { display: none !important; } |
| """, |
| ) as demo: |
| gr.Markdown(f"# {TITLE}") |
| gr.Markdown(DESCRIPTION) |
|
|
| with gr.Tabs(): |
| |
| with gr.TabItem("📄 PDF → HTML", id="tab-html"): |
| with gr.Row(): |
| with gr.Column(scale=1): |
| pdf_input = gr.File( |
| label="📁 อัปโหลด PDF", file_types=[".pdf"], type="filepath" |
| ) |
|
|
| with gr.Accordion("⚙️ ตั้งค่า", open=True): |
| scale_slider = gr.Slider( |
| minimum=0.5, |
| maximum=3.0, |
| value=1.5, |
| step=0.1, |
| label="Scale (ความคมชัด)", |
| info="1.5 = 150%, 2.0 = 200%", |
| ) |
|
|
| include_bg = gr.Checkbox( |
| value=True, |
| label="รวม Background (ภาพหน้า PDF)", |
| info="ปิดเพื่อได้ไฟล์เล็กลง แต่จะเห็นเฉพาะข้อความ", |
| ) |
|
|
| pages_input = gr.Textbox( |
| label="เลือกหน้า (ว่าง = ทุกหน้า)", |
| placeholder="เช่น 1,3,5-10", |
| info="ระบุหน้าที่ต้องการ เช่น 1,2,3 หรือ 1-5,8,10-12", |
| ) |
|
|
| convert_btn = gr.Button( |
| "🚀 แปลงเป็น HTML", variant="primary", size="lg" |
| ) |
|
|
| with gr.Column(scale=2): |
| html_output = gr.File(label="📥 ดาวน์โหลด HTML") |
|
|
| status_output = gr.Markdown( |
| label="สถานะ", elem_classes=["status-box"] |
| ) |
|
|
| with gr.Accordion("👁️ Preview HTML Code", open=False): |
| preview_output = gr.Code( |
| label="HTML Preview", language="html", elem_classes=["output-html"] |
| ) |
|
|
| |
| html_state = gr.State() |
|
|
| convert_btn.click( |
| fn=convert_pdf_to_html, |
| inputs=[pdf_input, scale_slider, include_bg, pages_input], |
| outputs=[html_output, status_output, preview_output, html_state], |
| ) |
|
|
| |
| with gr.TabItem("📝 ดึงข้อความ", id="tab-text"): |
| gr.Markdown("ดึงเฉพาะข้อความจาก PDF (เรียงตามหน้า)") |
|
|
| with gr.Row(): |
| with gr.Column(scale=1): |
| pdf_text_input = gr.File( |
| label="📁 อัปโหลด PDF", file_types=[".pdf"], type="filepath" |
| ) |
| extract_btn = gr.Button("📝 ดึงข้อความ", variant="primary") |
|
|
| with gr.Column(scale=2): |
| text_output = gr.Textbox( |
| label="ข้อความที่ดึงได้", |
| lines=20, |
| max_lines=50, |
| show_copy_button=True, |
| ) |
|
|
| extract_btn.click( |
| fn=extract_text_only, inputs=[pdf_text_input], outputs=[text_output] |
| ) |
|
|
| |
| with gr.TabItem("📊 Export JSON", id="tab-json"): |
| gr.Markdown("ดึงข้อมูลเป็น JSON พร้อมพิกัด (x, y, width, height, font_size)") |
|
|
| with gr.Row(): |
| with gr.Column(scale=1): |
| pdf_json_input = gr.File( |
| label="📁 อัปโหลด PDF", file_types=[".pdf"], type="filepath" |
| ) |
| json_scale = gr.Slider( |
| minimum=0.5, maximum=3.0, value=1.0, step=0.1, label="Scale" |
| ) |
| json_btn = gr.Button("📊 Export JSON", variant="primary") |
|
|
| with gr.Column(scale=2): |
| json_output = gr.Code( |
| label="JSON Output", language="json", show_label=True |
| ) |
|
|
| json_btn.click( |
| fn=extract_as_json, |
| inputs=[pdf_json_input, json_scale], |
| outputs=[json_output], |
| ) |
|
|
| |
| with gr.TabItem("ℹ️ เกี่ยวกับ", id="tab-about"): |
| gr.Markdown(""" |
| ## 🔧 เทคโนโลยีที่ใช้ |
| |
| - **[PyMuPDF (fitz)](https://pymupdf.readthedocs.io/)** - อ่านและประมวลผล PDF |
| - **[Gradio](https://gradio.app)** - สร้าง Web UI |
| - **[Hugging Face Spaces](https://huggingface.co/spaces)** - Hosting ฟรี |
| |
| ## 📋 Features |
| |
| | Feature | Description | |
| |---------|-------------| |
| | **PDF → HTML** | แปลง PDF เป็น HTML ที่รักษา layout | |
| | **ดึงข้อความ** | ดึงเฉพาะ text จาก PDF | |
| | **Export JSON** | ดึงข้อมูลพร้อมพิกัด (x, y, size) | |
| | **เลือกหน้า** | เลือกแปลงเฉพาะหน้าที่ต้องการ | |
| | **ปรับ Scale** | ปรับความคมชัด 0.5x - 3.0x | |
| |
| ## 💡 Tips |
| |
| 1. **ไฟล์เล็กลง**: ปิด "รวม Background" จะได้ไฟล์ HTML เล็กลงมาก |
| 2. **เลือกหน้า**: ใส่ `1-5` หรือ `1,3,5,7-10` เพื่อแปลงเฉพาะบางหน้า |
| 3. **JSON**: ใช้สำหรับ import ข้อมูลไปใช้ในแอปอื่น |
| |
| ## ⚠️ ข้อจำกัด |
| |
| - PDF ที่เป็นรูปภาพ (scanned) จะไม่มีข้อความให้ดึง |
| - ไฟล์ขนาดใหญ่มากอาจใช้เวลานานหรือ timeout |
| - บาง fonts พิเศษอาจแสดงผลไม่ถูกต้อง |
| |
| ## 📄 License |
| |
| MIT License - ใช้งานได้ฟรี |
| """) |
|
|
| gr.Markdown(""" |
| --- |
| <center>Made with ❤️ using PyMuPDF & Gradio</center> |
| """) |
|
|
|
|
| |
|
|
| if __name__ == "__main__": |
| demo.launch(server_name="0.0.0.0", server_port=7860, share=False) |
|
|