| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190 |
- from fastapi import FastAPI, File, UploadFile, HTTPException
- from fastapi.responses import RedirectResponse, FileResponse
- from pydantic import BaseModel
- from typing import Optional
- import subprocess
- import os
- import time
- from PIL import Image # Pillowライブラリを追加
- import io
- # レスポンスモデル
- class ConvertResponse(BaseModel):
- pdf_path: str
- converted: bool
- original: Optional[str] = None
- file_size: Optional[int] = None
- error: Optional[str] = None
- class HealthResponse(BaseModel):
- status: str
- service: str
- version: str
- uptime: float
- # FastAPI アプリケーション
- app = FastAPI(
- title="LibreOffice ドキュメント変換サービス",
- description="Word/PPT/Excel/PDF を PDF に変換し、混合内容のドキュメント処理をサポートします",
- version="1.0.0",
- docs_url="/docs",
- redoc_url="/redoc"
- )
- start_time = time.time()
- @app.get("/", include_in_schema=False)
- async def root():
- """ドキュメントページへリダイレクト"""
- return RedirectResponse(url="/docs")
- @app.get("/health", response_model=HealthResponse)
- async def health():
- """ヘルスチェックインターフェース"""
- return HealthResponse(
- status="healthy",
- service="libreoffice-converter",
- version="1.0.0",
- uptime=time.time() - start_time
- )
- @app.post("/convert")
- async def convert(file: UploadFile = File(...)):
- """
- ドキュメント変換インターフェース
- 戻り値: PDF ファイルストリーム
- """
- try:
- # ファイル形式の検証
- allowed_extensions = [
- '.pdf', '.doc', '.docx', '.ppt', '.pptx', '.xls', '.xlsx',
- '.md', '.txt', '.rtf', '.odt', '.ods', '.odp',
- '.jpg', '.jpeg', '.png', '.bmp', '.gif', '.tiff', '.webp'
- ]
- file_ext = os.path.splitext(file.filename)[1].lower()
- if file_ext not in allowed_extensions:
- raise HTTPException(
- status_code=400,
- detail=f"サポートされていないファイル形式です: {file_ext}。サポート対象: {', '.join(allowed_extensions)}"
- )
- # uploads ディレクトリの存在を確認
- upload_dir = "/app/uploads" if os.path.exists("/app/uploads") else "./uploads"
- os.makedirs(upload_dir, exist_ok=True)
- # アップロードファイルの保存
- filepath = os.path.join(upload_dir, file.filename)
- with open(filepath, "wb") as buffer:
- content = await file.read()
- buffer.write(content)
- # PDF の場合はそのまま返却
- if file_ext == '.pdf':
- return FileResponse(filepath, filename=file.filename, media_type='application/pdf')
- if file_ext == '.md':
- # Node.js スクリプトを使用して Markdown を PDF にレンダリング
- expected_pdf = filepath.rsplit('.', 1)[0] + '.pdf'
- cmd = [
- 'node',
- '/app/md_to_pdf.js',
- filepath,
- expected_pdf
- ]
- elif file_ext in ['.jpg', '.jpeg', '.png', '.bmp', '.gif', '.tiff', '.webp']:
- # 画像ファイルの場合は Pillow を使用して PDF に変換
- expected_pdf = filepath.rsplit('.', 1)[0] + '.pdf'
- # 画像を開いてPDFとして保存
- with Image.open(filepath) as img:
- # RGBAモードの場合はRGBに変換(透明度がある画像対応)
- if img.mode in ('RGBA', 'LA', 'P'):
- # 白い背景に変換
- background = Image.new('RGB', img.size, (255, 255, 255))
- if img.mode == 'P':
- img = img.convert('RGBA')
- background.paste(img, mask=img.split()[-1] if img.mode in ('RGBA', 'LA') else None)
- img = background
- elif img.mode != 'RGB':
- img = img.convert('RGB')
- # PDFとして保存
- img.save(expected_pdf, 'PDF', resolution=100.0, save_all=False)
- # PDF生成が完了したことを確認
- if not os.path.exists(expected_pdf):
- raise HTTPException(
- status_code=500,
- detail="画像からPDFへの変換は成功しましたが、出力ファイルが見つかりません"
- )
-
- # 画像変換完了、PDFファイルを返却
- filename_base = os.path.splitext(file.filename)[0]
- return FileResponse(expected_pdf, filename=f"{filename_base}.pdf", media_type='application/pdf')
- else:
- # LibreOffice による変換
- cmd = [
- 'soffice',
- '--headless',
- '--convert-to', 'pdf',
- '--outdir', upload_dir,
- filepath
- ]
- result = subprocess.run(
- cmd,
- capture_output=True,
- text=True,
- timeout=600, # 複雑なMarkdown変換をサポートするために10分に延長
- )
- # Combine stdout and stderr for error reporting since capture_output uses PIPE
- combined_output = result.stdout if result.stdout else ""
- if result.stderr:
- combined_output += "\n" + result.stderr
- # Node.jsスクリプトの実際の出力を表示して、デバッグ
- print(f"Node.js script output: {combined_output}")
- if result.returncode != 0:
- print(f"Subprocess failed with return code: {result.returncode}")
- # Combine stdout and stderr for error reporting
- combined_output = result.stdout if result.stdout else ""
- if result.stderr:
- combined_output += "\n" + result.stderr
- print(f"Subprocess output: {combined_output}")
- raise HTTPException(
- status_code=500,
- detail=f"変換に失敗しました: {combined_output}"
- )
- # 出力ファイルの確認
- expected_pdf = filepath.rsplit('.', 1)[0] + '.pdf'
- if not os.path.exists(expected_pdf):
- raise HTTPException(
- status_code=500,
- detail="変換は成功しましたが、出力ファイルが見つかりません"
- )
- filename_base = os.path.splitext(file.filename)[0]
- return FileResponse(expected_pdf, filename=f"{filename_base}.pdf", media_type='application/pdf')
- except HTTPException:
- raise
- except subprocess.TimeoutExpired:
- raise HTTPException(status_code=504, detail="変換タイムアウト (300秒)")
- except Exception as e:
- raise HTTPException(status_code=500, detail=str(e))
- @app.get("/version")
- async def version():
- """バージョン情報"""
- return {
- "service": "libreoffice-converter",
- "version": "1.0.0",
- "framework": "FastAPI",
- "libreoffice": "7.x"
- }
|