|
|
|
|
|
"""
|
|
|
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"
|
|
|
) |