diff --git a/electron/servers/fastapi/api/v1/ppt/endpoints/slide_to_html.py b/electron/servers/fastapi/api/v1/ppt/endpoints/slide_to_html.py index 5025ce4a..00db8019 100644 --- a/electron/servers/fastapi/api/v1/ppt/endpoints/slide_to_html.py +++ b/electron/servers/fastapi/api/v1/ppt/endpoints/slide_to_html.py @@ -9,7 +9,7 @@ from openai import OpenAI from openai import APIError from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select, delete, func -from utils.asset_directory_utils import get_images_directory +from utils.asset_directory_utils import get_images_directory, resolve_image_path_to_filesystem from services.database import get_async_session from models.sql.presentation_layout_code import PresentationLayoutCodeModel from .prompts import ( @@ -454,28 +454,10 @@ async def convert_slide_to_html(request: SlideToHtmlRequest): ) # Resolve image path to actual file system path - image_path = request.image - - # Handle different path formats - if image_path.startswith("/app_data/images/"): - # Remove the /app_data/images/ prefix and join with actual images directory - relative_path = image_path[len("/app_data/images/") :] - actual_image_path = os.path.join(get_images_directory(), relative_path) - elif image_path.startswith("/static/"): - # Handle static files - relative_path = image_path[len("/static/") :] - actual_image_path = os.path.join("static", relative_path) - else: - # Assume it's already a full path or relative to images directory - if os.path.isabs(image_path): - actual_image_path = image_path - else: - actual_image_path = os.path.join(get_images_directory(), image_path) - - # Check if image file exists - if not os.path.exists(actual_image_path): + actual_image_path = resolve_image_path_to_filesystem(request.image) + if not actual_image_path: raise HTTPException( - status_code=404, detail=f"Image file not found: {image_path}" + status_code=404, detail=f"Image file not found: {request.image}" ) # Read and encode image to base64 @@ -546,20 +528,8 @@ async def convert_html_to_react(request: HtmlToReactRequest): image_b64 = None media_type = None if request.image: - image_path = request.image - if image_path.startswith("/app_data/images/"): - relative_path = image_path[len("/app_data/images/") :] - actual_image_path = os.path.join(get_images_directory(), relative_path) - elif image_path.startswith("/static/"): - relative_path = image_path[len("/static/") :] - actual_image_path = os.path.join("static", relative_path) - else: - actual_image_path = ( - image_path - if os.path.isabs(image_path) - else os.path.join(get_images_directory(), image_path) - ) - if os.path.exists(actual_image_path): + actual_image_path = resolve_image_path_to_filesystem(request.image) + if actual_image_path: with open(actual_image_path, "rb") as f: image_b64 = base64.b64encode(f.read()).decode("utf-8") ext = os.path.splitext(actual_image_path)[1].lower() diff --git a/electron/servers/fastapi/services/pptx_presentation_creator.py b/electron/servers/fastapi/services/pptx_presentation_creator.py index b27c6145..d3e51a7f 100644 --- a/electron/servers/fastapi/services/pptx_presentation_creator.py +++ b/electron/servers/fastapi/services/pptx_presentation_creator.py @@ -36,7 +36,9 @@ from models.pptx_models import ( PptxTextBoxModel, PptxTextRunModel, ) +from utils.asset_directory_utils import get_images_directory, resolve_image_path_to_filesystem from utils.download_helpers import download_files +from utils.get_env import get_app_data_directory_env from utils.image_utils import ( clip_image, create_circle_image, @@ -206,35 +208,36 @@ class PptxPresentationCreator: image_urls = [] models_with_network_asset: List[PptxPictureBoxModel] = [] + def _process_image_path(each_shape, image_path): + if not image_path.startswith("http"): + return + if "app_data/" in image_path: + relative_path = image_path.split("app_data/")[1] + app_data_dir = get_app_data_directory_env() + if app_data_dir: + each_shape.picture.path = os.path.join(app_data_dir, relative_path) + else: + each_shape.picture.path = os.path.join("/app_data", relative_path) + each_shape.picture.is_network = False + return + # Resolve HTTP URLs that contain absolute filesystem paths (Mac/Electron) + local_path = resolve_image_path_to_filesystem(image_path) + if local_path: + each_shape.picture.path = local_path + each_shape.picture.is_network = False + return + image_urls.append(image_path) + models_with_network_asset.append(each_shape) + if self._ppt_model.shapes: for each_shape in self._ppt_model.shapes: if isinstance(each_shape, PptxPictureBoxModel): - image_path = each_shape.picture.path - if image_path.startswith("http"): - if "app_data/" in image_path: - relative_path = image_path.split("app_data/")[1] - each_shape.picture.path = os.path.join( - "/app_data", relative_path - ) - each_shape.picture.is_network = False - continue - image_urls.append(image_path) - models_with_network_asset.append(each_shape) + _process_image_path(each_shape, each_shape.picture.path) for each_slide in self._slide_models: for each_shape in each_slide.shapes: if isinstance(each_shape, PptxPictureBoxModel): - image_path = each_shape.picture.path - if image_path.startswith("http"): - if "app_data" in image_path: - relative_path = image_path.split("app_data/")[1] - each_shape.picture.path = os.path.join( - "/app_data", relative_path - ) - each_shape.picture.is_network = False - continue - image_urls.append(image_path) - models_with_network_asset.append(each_shape) + _process_image_path(each_shape, each_shape.picture.path) if image_urls: image_paths = await download_files(image_urls, self._temp_dir) @@ -312,6 +315,12 @@ class PptxPresentationCreator: def add_picture(self, slide: Slide, picture_model: PptxPictureBoxModel): image_path = picture_model.picture.path + # Resolve /app_data/... to actual filesystem path (Electron) + if image_path.startswith("/app_data/"): + app_data_dir = get_app_data_directory_env() + if app_data_dir: + relative = image_path[len("/app_data/"):] + image_path = os.path.join(app_data_dir, relative) if ( picture_model.clip or picture_model.border_radius diff --git a/electron/servers/fastapi/utils/asset_directory_utils.py b/electron/servers/fastapi/utils/asset_directory_utils.py index a88196c1..a3e44be7 100644 --- a/electron/servers/fastapi/utils/asset_directory_utils.py +++ b/electron/servers/fastapi/utils/asset_directory_utils.py @@ -1,7 +1,68 @@ import os +from typing import Optional +from urllib.parse import urlparse, unquote + from utils.get_env import get_app_data_directory_env +def resolve_image_path_to_filesystem(path_or_url: str) -> Optional[str]: + """ + Resolve an image path or URL to an actual filesystem path. + + Handles: + - Path strings: /app_data/images/..., /static/..., absolute paths, relative + - HTTP URLs whose path component is an absolute filesystem path (Mac/Electron): + When img src is /Users/.../images/xxx.png, browser resolves to + http://origin/Users/.../images/xxx.png. Next.js returns 404 for these. + + Returns the filesystem path if the file exists, else None. + """ + if not path_or_url: + return None + # Extract path from HTTP URL if needed + path = path_or_url + if path_or_url.startswith("http"): + try: + parsed = urlparse(path_or_url) + path = unquote(parsed.path) + except Exception: + return None + # Handle /app_data/images/ + if path.startswith("/app_data/images/"): + relative = path[len("/app_data/images/"):] + app_data = get_app_data_directory_env() + if app_data: + actual = os.path.join(app_data, "images", relative) + if os.path.isfile(actual): + return actual + # Fallback: get_images_directory() + relative + actual = os.path.join(get_images_directory(), relative) + return actual if os.path.isfile(actual) else None + # Handle /app_data/ (other subdirs) + if path.startswith("/app_data/"): + relative = path[len("/app_data/"):] + app_data = get_app_data_directory_env() + if app_data: + actual = os.path.join(app_data, relative) + return actual if os.path.isfile(actual) else None + # Handle absolute filesystem path (e.g. from HTTP URL path on Mac) + if path.startswith("/Users/") or path.startswith("/home/") or path.startswith("/var/"): + return path if os.path.isfile(path) else None + if "Application Support" in path or ("Library" in path and "images" in path): + return path if os.path.isfile(path) else None + # Handle /static/ + if path.startswith("/static/"): + relative = path[len("/static/"):] + actual = os.path.join("static", relative) + return actual if os.path.isfile(actual) else None + # Absolute path as-is + if os.path.isabs(path): + return path if os.path.isfile(path) else None + # Relative to images directory + actual = os.path.join(get_images_directory(), path) + return actual if os.path.isfile(actual) else None + + def get_images_directory(): images_directory = os.path.join(get_app_data_directory_env(), "images") os.makedirs(images_directory, exist_ok=True)