diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml
index 3ae61dd9a..672aaa3f5 100644
--- a/.github/workflows/integration-tests.yml
+++ b/.github/workflows/integration-tests.yml
@@ -653,6 +653,26 @@ jobs:
if ($shortcut.WorkingDirectory -ne $installRoot) {
throw "Desktop shortcut working directory is not the install root."
}
+ $environmentPython = Join-Path `
+ $environment[0].FullName `
+ "Scripts\python.exe"
+ $expectedIconPath = (
+ & $environmentPython `
+ -I -m openhcs.resources.brand windows_icon
+ ).Trim()
+ if ($LASTEXITCODE -ne 0) {
+ throw "Installed package could not resolve its Windows brand icon."
+ }
+ if (-not (Test-Path -LiteralPath $expectedIconPath -PathType Leaf)) {
+ throw "Installed Windows brand icon is missing."
+ }
+ $expectedIconLocation = "$expectedIconPath,0"
+ if ($shortcut.IconLocation -ne $expectedIconLocation) {
+ throw (
+ "Desktop shortcut icon '$($shortcut.IconLocation)' does not " +
+ "match '$expectedIconLocation'."
+ )
+ }
$guiSubsystem = python -c "import struct,sys; from pathlib import Path; data=Path(sys.argv[1]).read_bytes(); pe=struct.unpack_from('
-
- ___ _ _ ___ _____
- / _ \ _ __ ___ _ ___ | | | |/ __\/ ___/
-| | | | '_ \/ _ \| '_ \| |_| | | \___ \
-| |_| | |_||| __/| | | || _ | |__ __/ |
- \___/| .__/\___||_| |_||_| |_|\___/\____/
- |_| High-Content Screening
-
+
+
+OpenHCS
**Bioimage analysis platform for high-content screening**\
**Compile-time validation · Bidirectional GUI↔Code · Multi-GPU · LLM pipeline generation · Extensible function registry**
diff --git a/openhcs/__init__.py b/openhcs/__init__.py
index b5a7161c4..82ec8aeb3 100644
--- a/openhcs/__init__.py
+++ b/openhcs/__init__.py
@@ -14,7 +14,7 @@
from openhcs._source_dependencies import ensure_source_checkout_external_paths
-__version__ = "0.7.1"
+__version__ = "0.7.2"
# Configure polystore defaults for OpenHCS integration
os.environ.setdefault("POLYSTORE_METADATA_FILENAME", "openhcs_metadata.json")
diff --git a/openhcs/gui_startup.py b/openhcs/gui_startup.py
index 8ba16421b..8ef5f8a07 100644
--- a/openhcs/gui_startup.py
+++ b/openhcs/gui_startup.py
@@ -309,6 +309,7 @@ def _run_startup_window_child() -> int:
QPushButton,
QVBoxLayout,
)
+ from openhcs.pyqt_gui.branding import openhcs_application_icon
from pyqt_reactive.theming import ColorScheme, ThemeManager
class _EventBridge(QObject):
@@ -435,10 +436,13 @@ def _append_detail(self, message: str) -> None:
app = QApplication([sys.argv[0]])
app.setApplicationName("OpenHCS Startup")
+ application_icon = openhcs_application_icon()
+ app.setWindowIcon(application_icon)
color_scheme = ColorScheme()
theme_manager = ThemeManager(color_scheme)
theme_manager.apply_color_scheme(color_scheme)
window = _StartupWindow(color_scheme, theme_manager.style_generator)
+ window.setWindowIcon(application_icon)
bridge = _EventBridge()
bridge.event_received.connect(window.apply_event)
diff --git a/openhcs/pyqt_gui/app.py b/openhcs/pyqt_gui/app.py
index 829c69eb3..0eb79fbd0 100644
--- a/openhcs/pyqt_gui/app.py
+++ b/openhcs/pyqt_gui/app.py
@@ -8,13 +8,12 @@
import sys
import logging
from typing import Callable, Optional, TYPE_CHECKING
-from pathlib import Path
from PyQt6.QtWidgets import QApplication, QMessageBox
from PyQt6.QtCore import qInstallMessageHandler
-from PyQt6.QtGui import QIcon
from openhcs.core.config import GlobalPipelineConfig
+from openhcs.pyqt_gui.branding import openhcs_application_icon
from polystore.base import storage_registry
from polystore.filemanager import FileManager
from objectstate import spawn_thread_with_context
@@ -168,10 +167,7 @@ def init_function_registry_background():
register_reactor_providers(lambda: self.runtime_context.ui_config.zmq)
- # Set application icon (if available)
- icon_path = Path(__file__).parent / "resources" / "openhcs_icon.png"
- if icon_path.exists():
- self.setWindowIcon(QIcon(str(icon_path)))
+ self.setWindowIcon(openhcs_application_icon())
# Setup exception handling
sys.excepthook = self.handle_exception
diff --git a/openhcs/pyqt_gui/branding.py b/openhcs/pyqt_gui/branding.py
new file mode 100644
index 000000000..2dfd0316a
--- /dev/null
+++ b/openhcs/pyqt_gui/branding.py
@@ -0,0 +1,16 @@
+"""Qt projection of the package-owned OpenHCS brand mark."""
+
+from __future__ import annotations
+
+from PyQt6.QtGui import QIcon, QPixmap
+
+from openhcs.resources.brand import BrandAsset, brand_asset_bytes
+
+
+def openhcs_application_icon() -> QIcon:
+ """Build the OpenHCS application icon from its packaged raster asset."""
+
+ pixmap = QPixmap()
+ if not pixmap.loadFromData(brand_asset_bytes(BrandAsset.RASTER)):
+ raise RuntimeError("Packaged OpenHCS application icon could not be decoded.")
+ return QIcon(pixmap)
diff --git a/openhcs/resources/__init__.py b/openhcs/resources/__init__.py
new file mode 100644
index 000000000..b718f1ecb
--- /dev/null
+++ b/openhcs/resources/__init__.py
@@ -0,0 +1 @@
+"""Packaged, implementation-independent OpenHCS resources."""
diff --git a/openhcs/resources/assets/README.md b/openhcs/resources/assets/README.md
new file mode 100644
index 000000000..883ffd492
--- /dev/null
+++ b/openhcs/resources/assets/README.md
@@ -0,0 +1,11 @@
+# OpenHCS brand assets
+
+`openhcs-mark.svg` is the canonical OpenHCS project mark. It preserves the
+geometry and colors of the OpenHCSDev organization avatar.
+
+`openhcs-mark.png`, `openhcs.ico`, and `openhcs.icns` are mechanically rendered
+platform encodings of that SVG. Regenerate them with:
+
+```bash
+scripts/render_brand_assets.sh
+```
diff --git a/openhcs/resources/assets/openhcs-mark.png b/openhcs/resources/assets/openhcs-mark.png
new file mode 100644
index 000000000..18d3f2618
Binary files /dev/null and b/openhcs/resources/assets/openhcs-mark.png differ
diff --git a/openhcs/resources/assets/openhcs-mark.svg b/openhcs/resources/assets/openhcs-mark.svg
new file mode 100644
index 000000000..a545b14d0
--- /dev/null
+++ b/openhcs/resources/assets/openhcs-mark.svg
@@ -0,0 +1,8 @@
+
diff --git a/openhcs/resources/assets/openhcs.icns b/openhcs/resources/assets/openhcs.icns
new file mode 100644
index 000000000..b9cf481c3
Binary files /dev/null and b/openhcs/resources/assets/openhcs.icns differ
diff --git a/openhcs/resources/assets/openhcs.ico b/openhcs/resources/assets/openhcs.ico
new file mode 100644
index 000000000..9810f1557
Binary files /dev/null and b/openhcs/resources/assets/openhcs.ico differ
diff --git a/openhcs/resources/brand.py b/openhcs/resources/brand.py
new file mode 100644
index 000000000..ee8d3d828
--- /dev/null
+++ b/openhcs/resources/brand.py
@@ -0,0 +1,45 @@
+"""Authoritative access to the packaged OpenHCS brand assets."""
+
+from __future__ import annotations
+
+import argparse
+from enum import Enum
+from importlib.resources import files
+from pathlib import Path
+
+
+class BrandAsset(str, Enum):
+ """Closed set of mechanically equivalent OpenHCS mark encodings."""
+
+ SCALABLE = "openhcs-mark.svg"
+ RASTER = "openhcs-mark.png"
+ WINDOWS_ICON = "openhcs.ico"
+ MACOS_ICON = "openhcs.icns"
+
+
+def brand_asset_path(asset: BrandAsset) -> Path:
+ """Return the filesystem path for one installed OpenHCS brand asset."""
+
+ return Path(str(files("openhcs.resources") / "assets" / asset.value))
+
+
+def brand_asset_bytes(asset: BrandAsset) -> bytes:
+ """Read one packaged OpenHCS brand asset."""
+
+ return brand_asset_path(asset).read_bytes()
+
+
+def main() -> int:
+ """Print an installed brand asset path for native launcher integration."""
+
+ parser = argparse.ArgumentParser(description=__doc__)
+ choices = tuple(asset.name.lower() for asset in BrandAsset)
+ parser.add_argument("asset", choices=choices)
+ arguments = parser.parse_args()
+ asset = BrandAsset[arguments.asset.upper()]
+ print(brand_asset_path(asset))
+ return 0
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
diff --git a/packaging/codex/openhcs/.codex-plugin/plugin.json b/packaging/codex/openhcs/.codex-plugin/plugin.json
index 9aba0f256..a4a7610d6 100644
--- a/packaging/codex/openhcs/.codex-plugin/plugin.json
+++ b/packaging/codex/openhcs/.codex-plugin/plugin.json
@@ -1,6 +1,6 @@
{
"name": "openhcs",
- "version": "0.7.1",
+ "version": "0.7.2",
"description": "Inspect, author, validate, and run OpenHCS microscopy workflows through the local OpenHCS MCP server.",
"author": {
"name": "OpenHCSDev",
diff --git a/packaging/codex/openhcs/.mcp.json b/packaging/codex/openhcs/.mcp.json
index 6253fcff5..315856685 100644
--- a/packaging/codex/openhcs/.mcp.json
+++ b/packaging/codex/openhcs/.mcp.json
@@ -4,7 +4,7 @@
"command": "uvx",
"args": [
"--from",
- "openhcs[gui,mcp,viz]==0.7.1",
+ "openhcs[gui,mcp,viz]==0.7.2",
"openhcs-mcp"
]
}
diff --git a/packaging/installers/macos/build-installer.sh b/packaging/installers/macos/build-installer.sh
index 70eceb6c8..01c6b054a 100755
--- a/packaging/installers/macos/build-installer.sh
+++ b/packaging/installers/macos/build-installer.sh
@@ -5,11 +5,16 @@ set -euo pipefail
script_directory=$(cd "$(dirname "$0")" && pwd)
contract_path=${1:-"$script_directory/../installer_contract.json"}
output_path=${2:-"$script_directory/dist/OpenHCS Installer.app"}
+brand_icon_path="$script_directory/../../../openhcs/resources/assets/openhcs.icns"
if [[ ! -f "$contract_path" ]]; then
printf 'Installer contract not found: %s\n' "$contract_path" >&2
exit 2
fi
+if [[ ! -f "$brand_icon_path" ]]; then
+ printf 'OpenHCS brand icon not found: %s\n' "$brand_icon_path" >&2
+ exit 2
+fi
if ! command -v xcrun >/dev/null 2>&1; then
printf 'Xcode command-line tools are required to build the macOS installer app.\n' >&2
exit 2
@@ -57,6 +62,7 @@ done
CFBundleDisplayNameOpenHCS Installer
CFBundleExecutableOpenHCSInstaller
CFBundleIdentifierorg.openhcs.installer
+ CFBundleIconFileOpenHCS
CFBundleNameOpenHCS Installer
CFBundlePackageTypeAPPL
LSMinimumSystemVersion12.0
@@ -69,6 +75,8 @@ PLIST
"$temporary_app/Contents/Resources/install-openhcs.sh"
/bin/cp "$contract_path" \
"$temporary_app/Contents/Resources/installer_contract.json"
+/bin/cp "$brand_icon_path" \
+ "$temporary_app/Contents/Resources/OpenHCS.icns"
/bin/chmod 755 "$temporary_app/Contents/MacOS/OpenHCSInstaller"
/bin/chmod 755 "$temporary_app/Contents/Resources/install-openhcs.sh"
/usr/bin/plutil -lint "$temporary_app/Contents/Info.plist"
diff --git a/packaging/installers/macos/install-openhcs.sh b/packaging/installers/macos/install-openhcs.sh
index 8e06fed05..9b6966dc5 100755
--- a/packaging/installers/macos/install-openhcs.sh
+++ b/packaging/installers/macos/install-openhcs.sh
@@ -114,9 +114,10 @@ temporary_uv_installer=$(
/usr/bin/mktemp "${TMPDIR:-/tmp}/openhcs-uv-installer.XXXXXX"
)
new_launcher_app="$applications_root/.$product_name.app.new.$$"
+launcher_backup="$applications_root/.$product_name.app.backup.$$"
agent_registration_report="$application_root/agent-registration.json"
agent_registration_candidate="$application_root/.agent-registration.new.$$"
-launcher_created=false
+launcher_published=false
install_succeeded=false
active_child_pid=
@@ -143,13 +144,17 @@ cleanup() {
install_succeeded=true
fi
if [[ "$install_succeeded" == true ]]; then
+ /bin/rm -rf "$launcher_backup"
write_installer_state launcher-path "$launcher_app"
fi
if [[ "$install_succeeded" != true ]]; then
/bin/rm -rf "$new_environment"
- if [[ "$launcher_created" == true ]]; then
+ if [[ "$launcher_published" == true ]]; then
/bin/rm -rf "$launcher_app"
fi
+ if [[ -d "$launcher_backup" ]]; then
+ /bin/mv "$launcher_backup" "$launcher_app"
+ fi
fi
}
@@ -281,10 +286,21 @@ if [[ -e "$launcher_app" && ! -d "$launcher_app" ]]; then
fi
report_progress 'Preparing Applications and Desktop shortcuts…'
-if [[ ! -e "$launcher_app" ]]; then
- /bin/rm -rf "$new_launcher_app"
- /bin/mkdir -p "$new_launcher_app/Contents/MacOS"
- /bin/cat >"$new_launcher_app/Contents/Info.plist" <&2
+ exit 1
+fi
+/bin/cp "$brand_icon_path" \
+ "$new_launcher_app/Contents/Resources/OpenHCS.icns"
+/bin/cat >"$new_launcher_app/Contents/Info.plist" <
@@ -292,21 +308,24 @@ if [[ ! -e "$launcher_app" ]]; then
CFBundleDisplayName$product_name
CFBundleExecutablelaunch-openhcs
CFBundleIdentifierorg.openhcs.desktop
+ CFBundleIconFileOpenHCS
CFBundleName$product_name
CFBundlePackageTypeAPPL
CFBundleVersion1
PLIST
- /bin/cat >"$new_launcher_app/Contents/MacOS/launch-openhcs" <"$new_launcher_app/Contents/MacOS/launch-openhcs" <net48
OpenHCS-Windows-Installer
OpenHCS.Installer
+ OpenHCS.ico
latest
false
diff --git a/packaging/mcpb/openhcs/manifest.json b/packaging/mcpb/openhcs/manifest.json
index daa923667..08cbdfba8 100644
--- a/packaging/mcpb/openhcs/manifest.json
+++ b/packaging/mcpb/openhcs/manifest.json
@@ -2,7 +2,7 @@
"manifest_version": "0.4",
"name": "openhcs",
"display_name": "OpenHCS",
- "version": "0.7.1",
+ "version": "0.7.2",
"description": "Operate local OpenHCS microscopy workflows through MCP.",
"long_description": "Installs a pinned OpenHCS environment with the MCP server, PyQt user interface, and Napari/Fiji viewer runtimes. The MCP process runs over stdio and attaches to an authenticated, separately running OpenHCS UI when GUI tools are requested.",
"author": {
diff --git a/packaging/mcpb/openhcs/pyproject.toml b/packaging/mcpb/openhcs/pyproject.toml
index 74ba59fcc..3456576a5 100644
--- a/packaging/mcpb/openhcs/pyproject.toml
+++ b/packaging/mcpb/openhcs/pyproject.toml
@@ -1,8 +1,8 @@
[project]
name = "openhcs-mcpb-runtime"
-version = "0.7.1"
+version = "0.7.2"
description = "Managed runtime for the OpenHCS MCP Bundle"
requires-python = ">=3.11,<3.14"
dependencies = [
- "openhcs[gui,mcp,viz]==0.7.1",
+ "openhcs[gui,mcp,viz]==0.7.2",
]
diff --git a/pyproject.toml b/pyproject.toml
index 8eb1cc377..4eef1c362 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -323,6 +323,10 @@ openhcs = [
"**/*.yaml",
"**/*.yml",
"**/*.json",
+ "resources/assets/*.svg",
+ "resources/assets/*.png",
+ "resources/assets/*.ico",
+ "resources/assets/*.icns",
"agent/resources/knowledge/**/*.md",
"agent/resources/knowledge/**/*.rst",
]
diff --git a/scripts/build_website.py b/scripts/build_website.py
index 9b51cc17c..3e644527d 100644
--- a/scripts/build_website.py
+++ b/scripts/build_website.py
@@ -31,7 +31,10 @@
"assets/logos/pytorch.svg",
"assets/logos/tensorflow.svg",
)
-ASSET_SOURCES = {"assets/ui.png": "docs/source/_static/ui.png"}
+ASSET_SOURCES = {
+ "assets/logos/openhcs.svg": "openhcs/resources/assets/openhcs-mark.svg",
+ "assets/ui.png": "docs/source/_static/ui.png",
+}
REQUIRED_COPY = (
"PyPI",
".cppipe",
diff --git a/scripts/render_brand_assets.sh b/scripts/render_brand_assets.sh
new file mode 100755
index 000000000..011b98cf2
--- /dev/null
+++ b/scripts/render_brand_assets.sh
@@ -0,0 +1,36 @@
+#!/bin/bash
+
+set -euo pipefail
+
+repository_root=$(cd "$(dirname "$0")/.." && pwd)
+asset_directory="$repository_root/openhcs/resources/assets"
+source_svg="$asset_directory/openhcs-mark.svg"
+raster_png="$asset_directory/openhcs-mark.png"
+windows_icon="$asset_directory/openhcs.ico"
+macos_icon="$asset_directory/openhcs.icns"
+
+for executable in rsvg-convert python; do
+ if ! command -v "$executable" >/dev/null 2>&1; then
+ printf 'Required brand renderer is unavailable: %s\n' "$executable" >&2
+ exit 2
+ fi
+done
+
+rsvg-convert --width 1024 --height 1024 "$source_svg" --output "$raster_png"
+python - "$raster_png" "$windows_icon" "$macos_icon" <<'PY'
+from pathlib import Path
+import sys
+
+from PIL import Image
+
+source = Path(sys.argv[1])
+windows_destination = Path(sys.argv[2])
+macos_destination = Path(sys.argv[3])
+with Image.open(source) as image:
+ image.save(
+ windows_destination,
+ format="ICO",
+ sizes=((16, 16), (24, 24), (32, 32), (48, 48), (64, 64), (128, 128), (256, 256)),
+ )
+ image.save(macos_destination, format="ICNS")
+PY
diff --git a/server.json b/server.json
index 747eaeb0f..1a224c983 100644
--- a/server.json
+++ b/server.json
@@ -8,19 +8,19 @@
"url": "https://github.com/OpenHCSDev/OpenHCS",
"source": "github"
},
- "version": "0.7.1",
+ "version": "0.7.2",
"packages": [
{
"registryType": "pypi",
"registryBaseUrl": "https://pypi.org",
"identifier": "openhcs",
- "version": "0.7.1",
+ "version": "0.7.2",
"runtimeHint": "uvx",
"runtimeArguments": [
{
"type": "named",
"name": "--with",
- "value": "openhcs[gui,mcp,viz]==0.7.1",
+ "value": "openhcs[gui,mcp,viz]==0.7.2",
"description": "Install the matching OpenHCS GUI, MCP, Napari, and Fiji dependencies."
}
],
diff --git a/tests/installer/test_macos_simple_installer.py b/tests/installer/test_macos_simple_installer.py
index 6acb87dd7..88bfa8e08 100644
--- a/tests/installer/test_macos_simple_installer.py
+++ b/tests/installer/test_macos_simple_installer.py
@@ -181,6 +181,8 @@ def test_macos_installer_builds_a_universal_native_app_with_embedded_contract()
assert "CFBundleVersion" not in build
assert "Contents/Resources/installer_contract.json" in build
assert "Contents/Resources/install-openhcs.sh" in build
+ assert "Contents/Resources/OpenHCS.icns" in build
+ assert "CFBundleIconFileOpenHCS" in build
def test_macos_release_is_one_verified_disk_image() -> None:
@@ -245,6 +247,11 @@ def test_macos_shell_owns_live_progress_log_and_launcher_projection() -> None:
touch_position < regular_file_position < projection_position < redirect_position
)
assert 'if [[ -L "$log_path" ]]' in source
+ assert '"$environment_python" -m openhcs.resources.brand macos_icon' in source
+ assert '"$new_launcher_app/Contents/Resources/OpenHCS.icns"' in source
+ assert "CFBundleIconFileOpenHCS" in source
+ assert 'mv "$launcher_app" "$launcher_backup"' in source
+ assert 'mv "$launcher_backup" "$launcher_app"' in source
app_source = APP_SOURCE_PATH.read_text(encoding="utf-8")
assert 'installerStateValue(named: "progress")' in app_source
diff --git a/tests/installer/test_windows_simple_installer.py b/tests/installer/test_windows_simple_installer.py
index af6f1d13f..faf0be773 100644
--- a/tests/installer/test_windows_simple_installer.py
+++ b/tests/installer/test_windows_simple_installer.py
@@ -47,6 +47,7 @@ def test_windows_installer_has_stable_double_click_entrypoint() -> None:
assert "" not in project
assert "" not in project
assert "OpenHCS-Windows-Installer" in project
+ assert "OpenHCS.ico" in project
assert 'EmbeddedResource Include="Install-OpenHCS.ps1"' in project
assert 'EmbeddedResource Include="..\\installer_contract.json"' in project
assert "OpenHCS.Installer.Install-OpenHCS.ps1" in project
@@ -57,6 +58,8 @@ def test_windows_installer_has_stable_double_click_entrypoint() -> None:
assert "[string]$ContractPath" in build
assert '"OpenHCS-Windows-Installer.exe"' in build
assert '"installer_contract.json"' in build
+ assert '"openhcs.ico"' in build
+ assert '"OpenHCS.ico"' in build
assert "Assembly.GetExecutingAssembly()" in launcher
assert "Environment.Is64BitOperatingSystem" in launcher
@@ -182,6 +185,12 @@ def test_windows_installer_delegates_runtime_to_declared_entrypoint() -> None:
assert "CreateShortcut" in source
assert "$shortcut.TargetPath = $guiExecutable" in source
assert '$shortcut.Arguments = ""' in source
+ assert (
+ "& $environmentPython -I -m openhcs.resources.brand windows_icon"
+ in source
+ )
+ assert '$shortcut.IconLocation = "$applicationIconPath,0"' in source
+ assert '$shortcut.IconLocation = "$powerShellExecutable,0"' not in source
assert '$env:OPENHCS_CPU_ONLY = "true"' in source
assert (
'"environments\\{0}\\Scripts\\{1}.exe") @args' in source
@@ -501,6 +510,9 @@ def test_windows_installer_ci_has_an_absolute_safety_ceiling() -> None:
assert "$contract.gui_entry_point" in smoke_step
assert "$shortcut.TargetPath -ne $expectedGuiExecutable" in smoke_step
assert "Desktop shortcut target is not a GUI-subsystem executable." in smoke_step
+ assert "-I -m openhcs.resources.brand windows_icon" in smoke_step
+ assert "$shortcut.IconLocation -ne" in smoke_step
+ assert "match '$expectedIconLocation'." in smoke_step
assert '$env:CODEX_HOME = Join-Path $env:RUNNER_TEMP "codex-home"' in smoke_step
assert "Windows installer did not register the stable OpenHCS MCP launcher." in (
smoke_step
diff --git a/tests/unit/pyqt_gui/test_branding.py b/tests/unit/pyqt_gui/test_branding.py
new file mode 100644
index 000000000..5ab49b79c
--- /dev/null
+++ b/tests/unit/pyqt_gui/test_branding.py
@@ -0,0 +1,10 @@
+from __future__ import annotations
+
+from openhcs.pyqt_gui.branding import openhcs_application_icon
+
+
+def test_packaged_application_icon_decodes_for_qt(qapp) -> None:
+ icon = openhcs_application_icon()
+
+ assert not icon.isNull()
+ assert icon.pixmap(128, 128).size().width() == 128
diff --git a/tests/unit/test_brand_assets.py b/tests/unit/test_brand_assets.py
new file mode 100644
index 000000000..48fac35de
--- /dev/null
+++ b/tests/unit/test_brand_assets.py
@@ -0,0 +1,55 @@
+from __future__ import annotations
+
+import struct
+from xml.etree import ElementTree
+
+from openhcs.resources.brand import (
+ BrandAsset,
+ brand_asset_bytes,
+ brand_asset_path,
+)
+
+
+def test_brand_assets_are_one_complete_packaged_family() -> None:
+ assert {asset.name for asset in BrandAsset} == {
+ "SCALABLE",
+ "RASTER",
+ "WINDOWS_ICON",
+ "MACOS_ICON",
+ }
+ for asset in BrandAsset:
+ path = brand_asset_path(asset)
+ assert path.is_file()
+ assert path.read_bytes() == brand_asset_bytes(asset)
+
+
+def test_canonical_brand_svg_preserves_official_geometry_and_colors() -> None:
+ root = ElementTree.fromstring(brand_asset_bytes(BrandAsset.SCALABLE))
+ namespace = {"svg": "http://www.w3.org/2000/svg"}
+
+ assert root.attrib["viewBox"] == "0 0 420 420"
+ background = root.find("svg:rect", namespace)
+ mark = root.find("svg:path", namespace)
+ assert background is not None
+ assert mark is not None
+ assert background.attrib == {
+ "width": "420",
+ "height": "420",
+ "fill": "#f0f0f0",
+ }
+ assert mark.attrib["fill"] == "#dda98b"
+ assert mark.attrib["d"].startswith("M35 35h141")
+
+
+def test_platform_brand_encodings_have_native_container_headers() -> None:
+ png = brand_asset_bytes(BrandAsset.RASTER)
+ assert png[:8] == b"\x89PNG\r\n\x1a\n"
+ assert struct.unpack(">II", png[16:24]) == (1024, 1024)
+
+ windows_icon = brand_asset_bytes(BrandAsset.WINDOWS_ICON)
+ reserved, image_type, image_count = struct.unpack("I", macos_icon[4:8])[0] == len(macos_icon)
diff --git a/tests/unit/test_build_website.py b/tests/unit/test_build_website.py
index c37c6491d..6e03b8c75 100644
--- a/tests/unit/test_build_website.py
+++ b/tests/unit/test_build_website.py
@@ -29,6 +29,7 @@ def test_build_site_stages_authoritative_screenshot_and_valid_references(
REPO_ROOT / "docs/source/_static/ui.png"
).read_bytes()
for logo_name in (
+ "openhcs.svg",
"bioformats.svg",
"cellprofiler.png",
"cupy.svg",
@@ -39,9 +40,15 @@ def test_build_site_stages_authoritative_screenshot_and_valid_references(
"pytorch.svg",
"tensorflow.svg",
):
+ if logo_name == "openhcs.svg":
+ authority = (
+ REPO_ROOT / "openhcs/resources/assets/openhcs-mark.svg"
+ )
+ else:
+ authority = REPO_ROOT / "website/assets/logos" / logo_name
assert (site_dir / "assets/logos" / logo_name).read_bytes() == (
- REPO_ROOT / "website/assets/logos" / logo_name
- ).read_bytes()
+ authority.read_bytes()
+ )
assert local_targets == (
"assets/logos/bioformats.svg",
"assets/logos/cellprofiler.png",
@@ -49,6 +56,7 @@ def test_build_site_stages_authoritative_screenshot_and_valid_references(
"assets/logos/fiji.svg",
"assets/logos/jax.png",
"assets/logos/napari.svg",
+ "assets/logos/openhcs.svg",
"assets/logos/pyclesperanto.png",
"assets/logos/pytorch.svg",
"assets/logos/tensorflow.svg",
@@ -115,8 +123,9 @@ def test_shipping_copy_projects_current_release_and_keeps_boundaries_explicit(
assert html.index("Download for Windows") < html.index(
'python -m pip install "openhcs[gui,viz,bioformats,mcp,cellprofiler-compat]"'
)
- assert "The OpenHCS 0.6 beta includes supported CellProfiler" in html
- assert "Available in the 0.6 beta" in html
+ assert "The current OpenHCS beta includes supported CellProfiler" in html
+ assert "Available in the current beta" in html
+ assert "0.6 beta" not in html
assert f"New in {package_version}" not in html
assert re.search(r"official registry\s+metadata", html)
assert "https://openhcs.readthedocs.io/en/latest/" in html
@@ -176,6 +185,9 @@ def test_landing_page_uses_factual_copy_and_readable_proportions():
styles = (REPO_ROOT / "website/styles.css").read_text(encoding="utf-8")
assert "OpenHCS defines and runs microscopy workflows." in html
+ assert html.count('src="assets/logos/openhcs.svg"') == 2
+ assert 'href="assets/logos/openhcs.svg"' in html
+ assert 'H' not in html
assert 'class="hero-grid"' in html
assert 'class="release-summary"' in html
assert "Plate, pipeline, and result management." in html
@@ -226,6 +238,8 @@ def test_public_policy_pages_are_staged_with_truthful_hosted_boundaries(
assert 'href="privacy.html"' in document
assert 'href="support.html"' in document
assert 'href="terms.html"' in document
+ assert document.count('src="assets/logos/openhcs.svg"') == 2
+ assert 'href="assets/logos/openhcs.svg"' in document
assert "does not currently operate a public hosted MCP endpoint" in privacy_copy
assert "does not record bearer tokens or tool arguments" in privacy_copy
@@ -270,6 +284,12 @@ def test_website_source_and_workflow_follow_package_metadata_authorities():
assert "0.5.21" not in source_html
assert "0.5.22" not in source_html
assert workflow.count(' - "openhcs/__init__.py"') == 2
+ assert (
+ workflow.count(
+ ' - "openhcs/resources/assets/openhcs-mark.svg"'
+ )
+ == 2
+ )
for page_name in ("privacy.html", "support.html", "terms.html"):
page_source = (REPO_ROOT / "website" / page_name).read_text(encoding="utf-8")
assert CONTACT_EMAIL_TOKEN in page_source
@@ -297,6 +317,7 @@ def test_readme_does_not_link_unpublished_coverage_site():
readme = (REPO_ROOT / "README.md").read_text(encoding="utf-8")
assert "trissim.github.io/openhcs/coverage" not in readme
+ assert 'src="openhcs/resources/assets/openhcs-mark.svg"' in readme
def test_build_site_refuses_to_replace_source_or_repository_root():
diff --git a/website/index.html b/website/index.html
index 2af5a5b42..66f24c0c4 100644
--- a/website/index.html
+++ b/website/index.html
@@ -20,7 +20,7 @@
>
OpenHCS | Open-source high-content image analysis
-
+
@@ -32,7 +32,7 @@
@@ -81,7 +81,7 @@
OpenHCS defines and runs microscopy workflows.
Pipeline inputsOpenHCS Python and supported .cppipe files
InterfacesDesktop, headless, and local MCP
ReviewNapari and Fiji
-
StatusAvailable in the 0.6 beta
+
StatusAvailable in the current beta
@@ -352,7 +352,7 @@ Installation