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