diff --git a/src/ansys/dynamicreporting/core/serverless/adr.py b/src/ansys/dynamicreporting/core/serverless/adr.py
index 220ebdde3..1517a4c4a 100644
--- a/src/ansys/dynamicreporting/core/serverless/adr.py
+++ b/src/ansys/dynamicreporting/core/serverless/adr.py
@@ -138,8 +138,8 @@ def __init__(
databases: dict | None = None,
media_directory: str | None = None,
static_directory: str | None = None,
- media_url: str | None = None,
- static_url: str | None = None,
+ media_url: str = "/media/",
+ static_url: str = "/static/",
debug: bool | None = None,
opts: dict | None = None,
request: HttpRequest | None = None,
@@ -686,15 +686,11 @@ def static_directory(self) -> str:
@property
def static_url(self) -> str:
- from django.conf import settings
-
- return settings.STATIC_URL
+ return self._static_url
@property
def media_url(self) -> str:
- from django.conf import settings
-
- return settings.MEDIA_URL
+ return self._media_url
@property
def session(self) -> Session:
@@ -1038,8 +1034,10 @@ def export_report_as_html(
exporter = ServerlessReportExporter(
html_content=html_content,
output_dir=output_dir,
- static_dir=self._static_directory,
media_dir=self._media_directory,
+ static_dir=self._static_directory,
+ media_url=self._media_url,
+ static_url=self._static_url,
filename=filename,
ansys_version=str(self._ansys_version),
dark_mode=dark_mode,
diff --git a/src/ansys/dynamicreporting/core/serverless/html_exporter.py b/src/ansys/dynamicreporting/core/serverless/html_exporter.py
index 45590109c..09a2dc2cc 100644
--- a/src/ansys/dynamicreporting/core/serverless/html_exporter.py
+++ b/src/ansys/dynamicreporting/core/serverless/html_exporter.py
@@ -1,6 +1,7 @@
import base64
import os
from pathlib import Path
+import re
from typing import Any
from ..adr_utils import get_logger
@@ -16,7 +17,6 @@
VIEWER_JS,
VIEWER_UTILS,
)
-from ..utils.report_download_html import ReportDownloadHTML
class ServerlessReportExporter:
@@ -34,8 +34,10 @@ def __init__(
self,
html_content: str,
output_dir: Path,
- static_dir: Path,
media_dir: Path,
+ static_dir: Path,
+ media_url: str,
+ static_url: str,
*,
filename: str = "index.html",
no_inline_files: bool = False,
@@ -49,8 +51,10 @@ def __init__(
"""
self._html_content = html_content
self._output_dir = output_dir
- self._static_dir = static_dir
self._media_dir = media_dir
+ self._static_dir = static_dir
+ self._media_url = media_url
+ self._static_url = static_url
self._filename = filename
self._debug = debug
self._logger = logger or get_logger()
@@ -58,7 +62,7 @@ def __init__(
self._ansys_version = ansys_version
self._dark_mode = dark_mode
- # State tracking properties, functionally identical to ReportDownloadHTML
+ # State tracking properties
self._filemap: dict[str, str] = {}
self._replaced_file_ext: str | None = None
self._collision_count = 0
@@ -96,7 +100,8 @@ def export(self) -> None:
html = self._replace_blocks(html, "")
html = self._replace_blocks(html, "
")
+ html = self._replace_blocks(html, "") # any-order scripts (MathJax)
html = self._replace_blocks(html, "")
html = self._replace_blocks(html, "
")
html = self._replace_blocks(html, "")
@@ -161,19 +166,24 @@ def _replace_files(self, text: str, inline: bool = False, size_check: bool = Fal
current = 0
ver = str(self._ansys_version) if self._ansys_version is not None else ""
- patterns = (
- f"/static/ansys{ver}/",
- "/static/",
- "/media/",
- f"/ansys{ver}/",
- )
+ patterns = []
+ if ver:
+ patterns.append(f"{self._static_url}ansys{ver}/") # custom
+ patterns.append(f"/static/ansys{ver}/") # legacy literal
+
+ # custom first, then legacy literals
+ patterns.extend([self._static_url, self._media_url, "/static/", "/media/"])
+
+ if ver:
+ patterns.append(f"/ansys{ver}/") # server-root style
while True:
# Find the next match using the legacy priority order
idx1 = -1
for pat in patterns:
- idx1 = text.find(pat, current)
- if idx1 != -1:
+ pos = text.find(pat, current)
+ if pos != -1:
+ idx1 = pos
break
if idx1 == -1:
return text # nothing more to replace
@@ -192,7 +202,6 @@ def _replace_files(self, text: str, inline: bool = False, size_check: bool = Fal
self._replaced_file_ext = ext
new_path = self._process_file(path_in_html, simple_path, inline=inline)
-
if size_check and self._inline_size_exception:
new_path = "__SIZE_EXCEPTION__"
@@ -309,9 +318,7 @@ def _copy_static_file(self, source_rel_path: str, target_rel_path: str):
target_file.parent.mkdir(parents=True, exist_ok=True)
content = source_file.read_bytes()
# Patch some viewer JS internals (loader/paths) if needed
- content = ReportDownloadHTML.fix_viewer_component_paths(
- str(target_file), content, self._ansys_version
- )
+ content = self._fix_viewer_component_paths(str(target_file), content)
target_file.write_bytes(content)
else:
self._logger.warning(f"Warning: Static source file not found: {source_file}")
@@ -322,7 +329,7 @@ def _copy_static_files(self, files: list[str], source_prefix: str, target_prefix
self._copy_static_file(source_prefix.lstrip("/") + f, target_prefix + f)
def _make_unique_basename(self, name: str) -> str:
- """Ensures a unique filename in the target media directory to avoid collisions."""
+ """Ensures a unique filename in the target media directory to avoid collisions (legacy)."""
if not self._no_inline:
return name
target_path = self._output_dir / "media" / name
@@ -331,6 +338,10 @@ def _make_unique_basename(self, name: str) -> str:
self._collision_count += 1
return f"{self._collision_count}_{name}"
+ @staticmethod
+ def is_scene_file(name: str) -> bool:
+ return name.upper().endswith((".AVZ", ".SCDOC", ".SCDOCX", ".GLB"))
+
def _process_file(self, path_in_html: str, pathname: str, inline: bool = False) -> str:
"""
Reads a file from local disk and either inlines it or copies it.
@@ -348,12 +359,18 @@ def _process_file(self, path_in_html: str, pathname: str, inline: bool = False)
return self._filemap[pathname]
# Resolve source file location based on the raw pathname (no normalization)
- if pathname.startswith("/media/"):
+ ver = str(self._ansys_version) if self._ansys_version is not None else ""
+
+ # Source resolution: custom first, then legacy literals
+ if pathname.startswith(self._media_url):
+ source_file = self._media_dir / pathname.replace(self._media_url, "", 1)
+ elif pathname.startswith(self._static_url):
+ source_file = self._static_dir / pathname.replace(self._static_url, "", 1)
+ elif pathname.startswith("/media/"): # legacy literal
source_file = self._media_dir / pathname.replace("/media/", "", 1)
- elif pathname.startswith("/static/"):
+ elif pathname.startswith("/static/"): # legacy literal
source_file = self._static_dir / pathname.replace("/static/", "", 1)
- elif pathname.startswith(f"/ansys{self._ansys_version}/"):
- # Legacy downloads these from the server root; serverless reads them from static dir
+ elif ver and pathname.startswith(f"/ansys{ver}/"):
source_file = self._static_dir / pathname.lstrip("/")
else:
source_file = None
@@ -375,7 +392,7 @@ def _process_file(self, path_in_html: str, pathname: str, inline: bool = False)
# 4/3 is roughly the expansion factor of base64 encoding (3 bytes -> 4 chars)
estimated_inline_size = int(len(content) * (4.0 / 3.0))
- if (inline or ReportDownloadHTML.is_scene_file(pathname)) and self._should_use_data_uri(
+ if (inline or self.is_scene_file(pathname)) and self._should_use_data_uri(
estimated_inline_size
):
# Inline as data URI
@@ -398,39 +415,73 @@ def _process_file(self, path_in_html: str, pathname: str, inline: bool = False)
# prefix with parent folder (GUID) like legacy
basename = f"{source_file.parent.name}_{basename}"
else:
- content = ReportDownloadHTML.fix_viewer_component_paths(
- basename, content, self._ansys_version
- )
+ content = self._fix_viewer_component_paths(basename, content)
- # Output path (exact legacy behavior):
- # - If /static/ansys{ver}/ -> keep ansys tree, remove '/static/' -> './ansys{ver}/.../'
- # - Else -> './media/'
- if pathname.startswith(f"/static/ansys{self._ansys_version}/"):
- local_pathname = os.path.dirname(pathname).replace("/static/", "./", 1)
- result = f"{local_pathname}/{basename}"
+ # Output path:
+ # keep ansys tree if the input path came from /static/ansys{ver}/ (custom OR legacy literal)
+ if ver and (
+ pathname.startswith(f"{self._static_url}ansys{ver}/")
+ or pathname.startswith(f"/static/ansys{ver}/")
+ ):
+ local_pathname = os.path.dirname(pathname)
+ # normalize either custom or legacy /static/ to './'
+ if local_pathname.startswith(self._static_url):
+ local_pathname = local_pathname.replace(self._static_url, "./", 1)
+ elif local_pathname.startswith("/static/"):
+ local_pathname = local_pathname.replace("/static/", "./", 1)
+ result = f"{local_pathname}/{basename}"
target_file = self._output_dir / local_pathname.lstrip("./") / basename
- target_file.parent.mkdir(parents=True, exist_ok=True)
- target_file.write_bytes(content)
else:
result = f"./media/{basename}"
-
target_file = self._output_dir / "media" / basename
- target_file.parent.mkdir(parents=True, exist_ok=True)
- target_file.write_bytes(content)
+
+ target_file.parent.mkdir(parents=True, exist_ok=True)
+ target_file.write_bytes(content)
self._filemap[pathname] = result
return result
+ def _find_block(self, text: str, start: int, prefix: str, suffix: str) -> tuple[int, int, str]:
+ """
+ Legacy-compatible: return the next [prefix ... suffix] block that contains at least
+ one asset-like reference. Accept both the literal legacy prefixes and the configured
+ custom prefixes. Also accept any '/ansys/' or generic '/ansys/'.
+ """
+ # Normalize known prefixes (custom URLs may differ from /static/ and /media/)
+ custom_static = (self._static_url or "").strip()
+ custom_media = (self._media_url or "").strip()
+
+ while True:
+ try:
+ idx1 = text.index(prefix, start)
+ except ValueError:
+ return -1, -1, ""
+ try:
+ idx2 = text.index(suffix, idx1 + len(prefix))
+ except ValueError:
+ return -1, -1, ""
+ idx2 += len(suffix)
+ block = text[idx1:idx2]
+
+ if (
+ ("/static/" in block)
+ or ("/media/" in block)
+ or (custom_static and custom_static in block)
+ or (custom_media and custom_media in block)
+ or re.search(r"/ansys\d+/", block) is not None
+ ):
+ return idx1, idx2, block
+
+ start = idx2
+
def _replace_blocks(
self, html: str, prefix: str, suffix: str, inline: bool = False, size_check: bool = False
) -> str:
"""Iteratively finds and replaces all asset references within matching blocks."""
current_pos = 0
while True:
- start, end, text_block = ReportDownloadHTML.find_block(
- html, current_pos, prefix, suffix
- )
+ start, end, text_block = self._find_block(html, current_pos, prefix, suffix)
if start < 0:
break
processed_text = self._replace_files(text_block, inline=inline, size_check=size_check)
@@ -438,28 +489,52 @@ def _replace_blocks(
current_pos = start + len(processed_text)
return html
+ def _fix_viewer_component_paths(self, filename: str, data: bytes) -> bytes:
+ """
+ Adjust hard-coded viewer paths for offline export, honoring custom self._static_url.
+ Mirrors legacy behavior but replaces the '/static/' prefix with self._static_url.
+ """
+ ver = str(self._ansys_version) if self._ansys_version is not None else ""
+ if filename.endswith("ANSYSViewer_min.js"):
+ s = data.decode("utf-8")
+ # Replace "/website/images/" with dynamic base to ./media/
+ s = s.replace(
+ f'"{self._static_url}website/images/"',
+ r'document.URL.replace(/\\/g, "/").replace("index.html", "media/")',
+ )
+ # Point ansys images to local ./ansys{ver}//nexus/images/
+ if ver:
+ s = s.replace(f'"/ansys{ver}/nexus/images/', f'"./ansys{ver}//nexus/images/')
+ # Allow file:// loads in offline mode (legacy behavior)
+ s = s.replace('"FILE",delegate', '"arraybuffer",delegate')
+ return s.encode("utf-8")
+
+ if filename.endswith("viewer-loader.js"):
+ s = data.decode("utf-8")
+ if ver:
+ s = s.replace(f'"/ansys{ver}/nexus/images/', f'"./ansys{ver}//nexus/images/')
+ return s.encode("utf-8")
+
+ return data
+
def _inline_ansys_viewer(self, html: str) -> str:
"""Handles the special case of inlining assets for the component."""
current_pos = 0
while True:
- start, end, text_block = ReportDownloadHTML.find_block(
+ start, end, text_block = self._find_block(
html, current_pos, ""
)
if start < 0:
break
-
# Legacy parity: always inline viewer attributes
text = self._replace_blocks(text_block, 'proxy_img="', '"', inline=True)
text = self._replace_blocks(text, 'src="', '"', inline=True, size_check=True)
-
if "__SIZE_EXCEPTION__" in text:
msg = "3D geometry too large for stand-alone HTML file"
text = text.replace('src="__SIZE_EXCEPTION__"', f'src="" proxy_only="{msg}"')
-
if self._replaced_file_ext:
ext = self._replaced_file_ext.replace(".", "").upper()
text = text.replace("