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 logo + +

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 @@ + + OpenHCS + + + 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 '' 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 @@