|
|
| """
|
| DOCX to PDF Converter with Perfect Formatting Preservation
|
| Optimized for FastAPI with LibreOffice headless mode
|
| Supports Arabic RTL text and preserves all original formatting
|
| """
|
|
|
| import subprocess
|
| import tempfile
|
| import shutil
|
| import os
|
| from pathlib import Path
|
| import zipfile
|
| import re
|
| import json
|
| import threading
|
| import time
|
| from typing import Optional, List
|
| import logging
|
|
|
|
|
| os.environ['SAL_DISABLE_JAVA'] = '1'
|
| os.environ['SAL_DISABLE_JAVA_SECURITY'] = '1'
|
| os.environ['LIBO_DISABLE_JAVA'] = '1'
|
|
|
| from fastapi import FastAPI, File, UploadFile, HTTPException, BackgroundTasks
|
| from fastapi.responses import FileResponse, JSONResponse
|
| from fastapi.staticfiles import StaticFiles
|
| from fastapi.middleware.cors import CORSMiddleware
|
| from pydantic import BaseModel
|
|
|
|
|
| from app import (
|
| setup_libreoffice,
|
| setup_font_environment,
|
| create_fontconfig,
|
| validate_docx_structure,
|
| preprocess_docx_for_perfect_conversion,
|
| create_libreoffice_config,
|
| convert_docx_to_pdf,
|
| analyze_conversion_error,
|
| validate_pdf_output,
|
| post_process_pdf_for_perfect_formatting,
|
| generate_comprehensive_quality_report,
|
| calculate_quality_score
|
| )
|
|
|
|
|
| logging.basicConfig(level=logging.INFO)
|
| logger = logging.getLogger(__name__)
|
|
|
|
|
| app = FastAPI(
|
| title="محول DOCX إلى PDF المتقدم",
|
| description="""
|
| # محول DOCX إلى PDF المتقدم - دقة 99%+ للتنسيق العربي
|
|
|
| ## الميزات الرئيسية:
|
| - **دقة 99%+**: مطابقة بكسل بكسل مع Word الأصلي
|
| - **العربية RTL**: دعم كامل لاتجاه النص من اليمين إلى اليسار
|
| - **معالجة مسبقة**: إزالة العناصر المشكلة تلقائياً
|
| - **جداول مثالية**: الحفاظ على تنسيق الجداول والأبعاد
|
| - **خطوط عربية**: دعم كامل للخطوط العربية (Amiri, Noto, Scheherazade)
|
| - **Placeholders**: حفظ مواقع القوالب الديناميكية
|
| - **جودة عالية**: 600 DPI بدون ضغط مدمر
|
|
|
| ## التقنيات المستخدمة:
|
| - **LibreOffice**: محرك التحويل الأساسي مع إعدادات محسنة
|
| - **PyMuPDF**: مراقبة لاحقة للتحقق من الجودة
|
| - **FontConfig**: نظام خطوط متقدم مع استبدال الخطوط
|
| - **Post-processing**: تحليل شامل بعد التحويل
|
|
|
| ## استخدام API:
|
| 1. استخدم نقطة النهاية `/convert` لتحويل ملف DOCX إلى PDF
|
| 2. استلم تقرير الجودة مع نتيجة التحويل
|
| 3. قم بتنزيل الملف المحول باستخدام الرابط المقدم
|
| """,
|
| version="1.0.0",
|
| contact={
|
| "name": "فريق التطوير",
|
| "url": "https://huggingface.co",
|
| },
|
| license_info={
|
| "name": "MIT License",
|
| "url": "https://opensource.org/licenses/MIT",
|
| }
|
| )
|
|
|
|
|
| app.add_middleware(
|
| CORSMiddleware,
|
| allow_origins=["*"],
|
| allow_credentials=True,
|
| allow_methods=["*"],
|
| allow_headers=["*"],
|
| )
|
|
|
|
|
| class ConversionResponse(BaseModel):
|
| success: bool
|
| message: str
|
| pdf_url: Optional[str] = None
|
| quality_report: Optional[str] = None
|
|
|
| class HealthResponse(BaseModel):
|
| status: str
|
| libreoffice_available: bool
|
| java_disabled: bool
|
|
|
|
|
| app.mount("/static", StaticFiles(directory="static"), name="static")
|
|
|
| @app.on_event("startup")
|
| async def startup_event():
|
| """Initialize the application on startup"""
|
| logger.info("Starting DOCX to PDF Converter API")
|
|
|
|
|
| libreoffice_available = setup_libreoffice()
|
|
|
|
|
| try:
|
| setup_font_environment()
|
| except Exception as e:
|
| logger.warning(f"Font environment setup failed: {e}")
|
| logger.warning("Continuing with default system fonts...")
|
|
|
|
|
| Path("static").mkdir(exist_ok=True)
|
|
|
| if not libreoffice_available:
|
| logger.warning("LibreOffice is not available on this system. DOCX to PDF conversion will not work until LibreOffice is installed.")
|
| logger.warning("Please install LibreOffice from: https://www.libreoffice.org/download/download-libreoffice/")
|
| else:
|
| logger.info("LibreOffice is available and ready for DOCX to PDF conversion")
|
|
|
| logger.info("Application initialized successfully")
|
|
|
| @app.get("/", tags=["UI"])
|
| async def root():
|
| """
|
| عرض واجهة المستخدم الرئيسية
|
|
|
| تقدم واجهة HTML التفاعلية لتحويل ملفات DOCX إلى PDF
|
| تتضمن إرشادات الاستخدام والميزات المتقدمة
|
| """
|
| return FileResponse("static/index.html")
|
|
|
| @app.get("/health", response_model=HealthResponse, tags=["Health"])
|
| async def health_check():
|
| """
|
| التحقق من صحة التطبيق
|
|
|
| تحقق من توفر LibreOffice وتشغيل الخدمة بشكل صحيح
|
|
|
| ## الاستجابة:
|
| - `status`: حالة التطبيق (healthy/degraded)
|
| - `libreoffice_available`: ما إذا كان LibreOffice متوفرًا أم لا
|
| - `java_disabled`: ما إذا كان Java معطلًا أم لا
|
| """
|
| libreoffice_available = False
|
| libreoffice_version = None
|
| java_disabled = True
|
|
|
| try:
|
|
|
| result = subprocess.run(
|
| ["libreoffice", "--version"],
|
| capture_output=True,
|
| text=True,
|
| timeout=10
|
| )
|
| libreoffice_available = result.returncode == 0
|
| if libreoffice_available:
|
| libreoffice_version = result.stdout.strip()
|
| except Exception:
|
| libreoffice_available = False
|
|
|
|
|
| try:
|
|
|
| test_result = subprocess.run(
|
| ["libreoffice", "--headless", "--disable-java", "--version"],
|
| capture_output=True,
|
| text=True,
|
| timeout=5
|
| )
|
|
|
| java_disabled = test_result.returncode == 0
|
| except Exception:
|
|
|
| java_disabled = True
|
|
|
| status = "healthy" if libreoffice_available and java_disabled else "degraded"
|
|
|
| if not libreoffice_available:
|
| logger.warning("LibreOffice is not available. Please install LibreOffice for DOCX to PDF conversion.")
|
|
|
| return HealthResponse(
|
| status=status,
|
| libreoffice_available=libreoffice_available,
|
| java_disabled=java_disabled
|
| )
|
|
|
| @app.post("/convert", response_model=ConversionResponse, tags=["Conversion"])
|
| async def convert_docx(file: UploadFile = File(..., description="ملف DOCX للتحويل إلى PDF")):
|
| """
|
| تحويل ملف DOCX إلى PDF مع الحفاظ على التنسيق الأصلي بدقة 99%+
|
|
|
| ## الميزات:
|
| - دعم كامل للنصوص العربية والاتجاه من اليمين إلى اليسار
|
| - الحفاظ على تنسيق الجداول والصور والأبعاد
|
| - معالجة مسبقة للعناصر المشكلة
|
| - تقرير جودة شامل بعد التحويل
|
|
|
| ## الاستجابة:
|
| - `success`: ما إذا كان التحويل ناجحًا أم لا
|
| - `message`: رسالة الحالة التفصيلية
|
| - `pdf_url`: رابط تنزيل ملف PDF المحول
|
| - `quality_report`: تقرير الجودة مع نقاط الدقة
|
|
|
| ## الأخطاء المحتملة:
|
| - 400: ملف غير مدعوم (ليس DOCX)
|
| - 500: خطأ في التحويل (مشكلة في LibreOffice)
|
| """
|
| if not file.filename.endswith('.docx'):
|
| raise HTTPException(status_code=400, detail="فقط ملفات DOCX مسموحة")
|
|
|
| try:
|
|
|
| try:
|
| subprocess.run(
|
| ["libreoffice", "--version"],
|
| capture_output=True,
|
| text=True,
|
| timeout=10
|
| )
|
| except (subprocess.TimeoutExpired, FileNotFoundError):
|
| raise HTTPException(
|
| status_code=500,
|
| detail="لم يتم العثور على LibreOffice. يرجى تثبيت LibreOffice وإعادة تشغيل الخادم."
|
| )
|
|
|
|
|
| with tempfile.NamedTemporaryFile(suffix=".docx", delete=False) as tmp_file:
|
| content = await file.read()
|
| if isinstance(content, str):
|
| content = content.encode('utf-8')
|
| tmp_file.write(content)
|
| tmp_file_path = tmp_file.name
|
|
|
|
|
|
|
| class FileObj:
|
| def __init__(self, name):
|
| self.name = name
|
|
|
| file_obj = FileObj(tmp_file_path)
|
| pdf_path, status_message = convert_docx_to_pdf(file_obj)
|
|
|
|
|
| try:
|
| os.unlink(tmp_file_path)
|
| except Exception as e:
|
| logger.warning(f"Failed to delete temporary file {tmp_file_path}: {e}")
|
|
|
| if pdf_path is None:
|
| raise HTTPException(status_code=500, detail=f"فشل التحويل: {status_message}")
|
|
|
|
|
| Path("static").mkdir(exist_ok=True)
|
|
|
|
|
| pdf_filename = f"converted_{int(time.time())}.pdf"
|
| final_pdf_path = f"static/{pdf_filename}"
|
|
|
|
|
| try:
|
| shutil.move(pdf_path, final_pdf_path)
|
| except Exception as e:
|
| logger.error(f"Failed to move PDF file: {e}")
|
| raise HTTPException(status_code=500, detail=f"فشل في حفظ ملف PDF: {str(e)}")
|
|
|
| return ConversionResponse(
|
| success=True,
|
| message="Conversion completed successfully",
|
| pdf_url=f"/static/{pdf_filename}",
|
| quality_report=status_message
|
| )
|
|
|
| except HTTPException:
|
|
|
| raise
|
| except Exception as e:
|
| logger.error(f"Conversion error: {str(e)}", exc_info=True)
|
|
|
| error_message = str(e)
|
|
|
|
|
| if "LibreOffice" in error_message or "The system cannot find the file specified" in error_message:
|
| error_detail = "لم يتم العثور على LibreOffice. يرجى التأكد من تثبيت LibreOffice وضبط مسار النظام بشكل صحيح."
|
| elif "permission" in error_message.lower() or "access" in error_message.lower():
|
| error_detail = "خطأ في الوصول إلى الملف. يرجى التأكد من صلاحيات الملف."
|
| else:
|
| error_detail = f"حدث خطأ أثناء التحويل: {error_message}"
|
|
|
| raise HTTPException(status_code=500, detail=error_detail)
|
|
|
| @app.get("/download/{filename}", tags=["Download"])
|
| async def download_pdf(filename: str):
|
| """
|
| تنزيل ملف PDF المحول
|
|
|
| ## المعلمات:
|
| - `filename`: اسم ملف PDF للتنزيل
|
|
|
| ## الاستجابة:
|
| - ملف PDF للتنزيل المباشر
|
|
|
| ## الأخطاء:
|
| - 404: الملف غير موجود
|
| """
|
| file_path = f"static/{filename}"
|
| if not os.path.exists(file_path):
|
| raise HTTPException(status_code=404, detail="الملف غير موجود")
|
|
|
| return FileResponse(
|
| path=file_path,
|
| filename=filename,
|
| media_type='application/pdf'
|
| )
|
|
|
| if __name__ == "__main__":
|
| import uvicorn
|
| uvicorn.run(
|
| "main:app",
|
| host="0.0.0.0",
|
| port=7860,
|
| reload=True,
|
| log_level="info"
|
| ) |