- New POST /api/generate-pptx route (Next.js) uses PptxGenJS to build PPTX directly from the Phase 8 JSON element model — no headless Chrome - export_utils.py queries DB for slides + layout codes, POSTs payload to Next.js, saves binary response to disk (removes python-pptx/Puppeteer) - Coordinate conversion: px / 96 → inches (1280×720 = 13.333×7.5 in) - CSS color/font-size parsing (hex, rgb/rgba, px→pt at 0.75pt/px) - Fallback renderer for slides without a JSON layout schema - PDF export (Puppeteer) unchanged Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
127 lines
4.4 KiB
Python
127 lines
4.4 KiB
Python
import os
|
|
import uuid
|
|
from typing import Literal, Optional
|
|
|
|
import aiohttp
|
|
from fastapi import HTTPException
|
|
from pathvalidate import sanitize_filename
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
from sqlmodel import select
|
|
|
|
from models.presentation_and_path import PresentationAndPath
|
|
from models.sql.presentation import PresentationModel
|
|
from models.sql.presentation_layout_code import PresentationLayoutCodeModel
|
|
from models.sql.slide import SlideModel
|
|
from services.database import async_session_maker
|
|
from utils.asset_directory_utils import get_exports_directory
|
|
|
|
|
|
async def export_presentation(
|
|
presentation_id: uuid.UUID,
|
|
title: str,
|
|
export_as: Literal["pptx", "pdf"],
|
|
client_id: Optional[uuid.UUID] = None,
|
|
session: Optional[AsyncSession] = None,
|
|
) -> PresentationAndPath:
|
|
next_url = os.environ.get("NEXT_INTERNAL_URL", "http://localhost:3000")
|
|
|
|
if export_as == "pptx":
|
|
# Query presentation, slides, and layout codes from the DB
|
|
async with async_session_maker() as db:
|
|
presentation = await db.get(PresentationModel, presentation_id)
|
|
|
|
slides_result = await db.scalars(
|
|
select(SlideModel)
|
|
.where(
|
|
SlideModel.presentation == presentation_id,
|
|
SlideModel.deleted_at.is_(None),
|
|
)
|
|
.order_by(SlideModel.index)
|
|
)
|
|
slides = list(slides_result.all())
|
|
|
|
# Fetch layout codes if the presentation uses a custom template
|
|
template_id_str = (
|
|
getattr(presentation, "template_name", None) if presentation else None
|
|
)
|
|
layouts = []
|
|
if template_id_str:
|
|
try:
|
|
template_uuid = uuid.UUID(template_id_str)
|
|
layouts_result = await db.scalars(
|
|
select(PresentationLayoutCodeModel).where(
|
|
PresentationLayoutCodeModel.presentation == template_uuid
|
|
)
|
|
)
|
|
layouts = list(layouts_result.all())
|
|
except (ValueError, TypeError):
|
|
pass
|
|
|
|
# Build JSON payload for the Next.js PptxGenJS endpoint
|
|
payload = {
|
|
"title": title,
|
|
"slides": [
|
|
{
|
|
"id": str(s.id),
|
|
"layout": s.layout,
|
|
"layout_group": s.layout_group,
|
|
"index": s.index,
|
|
"content": s.content or {},
|
|
"speaker_note": s.speaker_note,
|
|
}
|
|
for s in slides
|
|
],
|
|
"layouts": [
|
|
{
|
|
"layout_id": lay.layout_id,
|
|
"layout_name": lay.layout_name,
|
|
"layout_code": lay.layout_code,
|
|
}
|
|
for lay in layouts
|
|
],
|
|
}
|
|
|
|
export_directory = get_exports_directory()
|
|
pptx_path = os.path.join(
|
|
export_directory,
|
|
f"{sanitize_filename(title or str(uuid.uuid4()))}.pptx",
|
|
)
|
|
|
|
async with aiohttp.ClientSession() as http:
|
|
async with http.post(
|
|
f"{next_url}/api/generate-pptx",
|
|
json=payload,
|
|
) as response:
|
|
if response.status != 200:
|
|
error_text = await response.text()
|
|
print(f"[export_utils] generate-pptx failed ({response.status}): {error_text}")
|
|
raise HTTPException(
|
|
status_code=500,
|
|
detail="Failed to generate PPTX",
|
|
)
|
|
pptx_bytes = await response.read()
|
|
|
|
os.makedirs(export_directory, exist_ok=True)
|
|
with open(pptx_path, "wb") as f:
|
|
f.write(pptx_bytes)
|
|
|
|
return PresentationAndPath(
|
|
presentation_id=presentation_id,
|
|
path=pptx_path,
|
|
)
|
|
|
|
else:
|
|
async with aiohttp.ClientSession() as http:
|
|
async with http.post(
|
|
f"{next_url}/api/export-as-pdf",
|
|
json={
|
|
"id": str(presentation_id),
|
|
"title": sanitize_filename(title or str(uuid.uuid4())),
|
|
},
|
|
) as response:
|
|
response_json = await response.json()
|
|
|
|
return PresentationAndPath(
|
|
presentation_id=presentation_id,
|
|
path=response_json["path"],
|
|
)
|