feat: implement image path resolution utility to streamline image handling in FastAPI endpoints and PPTX presentation creator

This commit is contained in:
sudipnext 2026-03-08 20:55:20 +05:45
parent e0d219e5fe
commit d185d10462
3 changed files with 98 additions and 58 deletions

View file

@ -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()

View file

@ -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

View file

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