diff --git a/.github/workflows/website-pages.yml b/.github/workflows/website-pages.yml index 1dc8b1028..170f5f90d 100644 --- a/.github/workflows/website-pages.yml +++ b/.github/workflows/website-pages.yml @@ -4,7 +4,7 @@ on: pull_request: paths: - "website/**" - - "openhcs/resources/assets/openhcs-mark.svg" + - "openhcs/resources/assets/openhcs-*.svg" - "docs/source/_static/ui.png" - "scripts/build_website.py" - "tests/unit/test_build_website.py" @@ -14,7 +14,7 @@ on: branches: [main] paths: - "website/**" - - "openhcs/resources/assets/openhcs-mark.svg" + - "openhcs/resources/assets/openhcs-*.svg" - "docs/source/_static/ui.png" - "scripts/build_website.py" - "tests/unit/test_build_website.py" diff --git a/README.md b/README.md index 8d0d72795..c0765a00e 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,7 @@
-OpenHCS logo - -

OpenHCS

+

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 82ec8aeb3..570746f45 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.2" +__version__ = "0.7.3" # Configure polystore defaults for OpenHCS integration os.environ.setdefault("POLYSTORE_METADATA_FILENAME", "openhcs_metadata.json") diff --git a/openhcs/pyqt_gui/branding.py b/openhcs/pyqt_gui/branding.py index 2dfd0316a..33530d048 100644 --- a/openhcs/pyqt_gui/branding.py +++ b/openhcs/pyqt_gui/branding.py @@ -11,6 +11,6 @@ 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)): + if not pixmap.loadFromData(brand_asset_bytes(BrandAsset.ICON_RASTER)): raise RuntimeError("Packaged OpenHCS application icon could not be decoded.") return QIcon(pixmap) diff --git a/openhcs/resources/assets/README.md b/openhcs/resources/assets/README.md index 883ffd492..0347d536e 100644 --- a/openhcs/resources/assets/README.md +++ b/openhcs/resources/assets/README.md @@ -1,10 +1,15 @@ # OpenHCS brand assets -`openhcs-mark.svg` is the canonical OpenHCS project mark. It preserves the -geometry and colors of the OpenHCSDev organization avatar. +The official source family contains: -`openhcs-mark.png`, `openhcs.ico`, and `openhcs.icns` are mechanically rendered -platform encodings of that SVG. Regenerate them with: +- `openhcs-mark.svg` and `openhcs-mark-mono.svg` for symbol-only use. +- `openhcs-lockup-horizontal.svg` for wide headers. +- `openhcs-lockup-stacked.svg` for compact full-logo placement. +- `openhcs-icon-square.svg` for application and plugin icons. +- `openhcs-favicon.svg` for browser chrome. + +`openhcs-icon-square.png`, `openhcs.ico`, and `openhcs.icns` are mechanically +rendered platform encodings of `openhcs-icon-square.svg`. Regenerate them with: ```bash scripts/render_brand_assets.sh diff --git a/openhcs/resources/assets/openhcs-favicon.svg b/openhcs/resources/assets/openhcs-favicon.svg new file mode 100644 index 000000000..72c0b569f --- /dev/null +++ b/openhcs/resources/assets/openhcs-favicon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/openhcs/resources/assets/openhcs-icon-square.png b/openhcs/resources/assets/openhcs-icon-square.png new file mode 100644 index 000000000..8eb703da3 Binary files /dev/null and b/openhcs/resources/assets/openhcs-icon-square.png differ diff --git a/openhcs/resources/assets/openhcs-icon-square.svg b/openhcs/resources/assets/openhcs-icon-square.svg new file mode 100644 index 000000000..0570aa203 --- /dev/null +++ b/openhcs/resources/assets/openhcs-icon-square.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/openhcs/resources/assets/openhcs-lockup-horizontal.svg b/openhcs/resources/assets/openhcs-lockup-horizontal.svg new file mode 100644 index 000000000..d000bef92 --- /dev/null +++ b/openhcs/resources/assets/openhcs-lockup-horizontal.svg @@ -0,0 +1,8 @@ + + + + + + + OpenHCS + diff --git a/openhcs/resources/assets/openhcs-lockup-stacked.svg b/openhcs/resources/assets/openhcs-lockup-stacked.svg new file mode 100644 index 000000000..64c3d6b8e --- /dev/null +++ b/openhcs/resources/assets/openhcs-lockup-stacked.svg @@ -0,0 +1,8 @@ + + OpenHCS + + + + + + diff --git a/openhcs/resources/assets/openhcs-mark-mono.svg b/openhcs/resources/assets/openhcs-mark-mono.svg new file mode 100644 index 000000000..82f0c45b2 --- /dev/null +++ b/openhcs/resources/assets/openhcs-mark-mono.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/openhcs/resources/assets/openhcs-mark.png b/openhcs/resources/assets/openhcs-mark.png deleted file mode 100644 index 18d3f2618..000000000 Binary files a/openhcs/resources/assets/openhcs-mark.png and /dev/null differ diff --git a/openhcs/resources/assets/openhcs-mark.svg b/openhcs/resources/assets/openhcs-mark.svg index a545b14d0..b409b83df 100644 --- a/openhcs/resources/assets/openhcs-mark.svg +++ b/openhcs/resources/assets/openhcs-mark.svg @@ -1,8 +1,5 @@ - - OpenHCS - - + + + + diff --git a/openhcs/resources/assets/openhcs.icns b/openhcs/resources/assets/openhcs.icns index b9cf481c3..381a4be76 100644 Binary files a/openhcs/resources/assets/openhcs.icns and b/openhcs/resources/assets/openhcs.icns differ diff --git a/openhcs/resources/assets/openhcs.ico b/openhcs/resources/assets/openhcs.ico index 9810f1557..a55e2c1b6 100644 Binary files a/openhcs/resources/assets/openhcs.ico and b/openhcs/resources/assets/openhcs.ico differ diff --git a/openhcs/resources/brand.py b/openhcs/resources/brand.py index ee8d3d828..d99dcdde9 100644 --- a/openhcs/resources/brand.py +++ b/openhcs/resources/brand.py @@ -8,11 +8,19 @@ from pathlib import Path -class BrandAsset(str, Enum): - """Closed set of mechanically equivalent OpenHCS mark encodings.""" +BRAND_PRIMARY_COLOR = "#1D9E75" + - SCALABLE = "openhcs-mark.svg" - RASTER = "openhcs-mark.png" +class BrandAsset(str, Enum): + """Closed family of official OpenHCS logo variants and native encodings.""" + + MARK = "openhcs-mark.svg" + MARK_MONO = "openhcs-mark-mono.svg" + LOCKUP_HORIZONTAL = "openhcs-lockup-horizontal.svg" + LOCKUP_STACKED = "openhcs-lockup-stacked.svg" + ICON_SQUARE = "openhcs-icon-square.svg" + FAVICON = "openhcs-favicon.svg" + ICON_RASTER = "openhcs-icon-square.png" WINDOWS_ICON = "openhcs.ico" MACOS_ICON = "openhcs.icns" diff --git a/packaging/chatgpt/openhcs/assets/openhcs.svg b/packaging/chatgpt/openhcs/assets/openhcs.svg deleted file mode 100644 index 766f6e4fa..000000000 --- a/packaging/chatgpt/openhcs/assets/openhcs.svg +++ /dev/null @@ -1,21 +0,0 @@ - - OpenHCS - A microscopy plate with connected image-analysis wells. - - - - - - - - - - - - - - - - - - diff --git a/packaging/codex/openhcs/.codex-plugin/plugin.json b/packaging/codex/openhcs/.codex-plugin/plugin.json index a4a7610d6..19126a87f 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.2", + "version": "0.7.3", "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 315856685..d6770bea5 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.2", + "openhcs[gui,mcp,viz]==0.7.3", "openhcs-mcp" ] } diff --git a/packaging/mcpb/openhcs/manifest.json b/packaging/mcpb/openhcs/manifest.json index 08cbdfba8..ac62b01a3 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.2", + "version": "0.7.3", "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 3456576a5..822b00809 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.2" +version = "0.7.3" description = "Managed runtime for the OpenHCS MCP Bundle" requires-python = ">=3.11,<3.14" dependencies = [ - "openhcs[gui,mcp,viz]==0.7.2", + "openhcs[gui,mcp,viz]==0.7.3", ] diff --git a/scripts/build_hosted_mcp_plugin.py b/scripts/build_hosted_mcp_plugin.py index a8e9c5a4d..9f054ced8 100644 --- a/scripts/build_hosted_mcp_plugin.py +++ b/scripts/build_hosted_mcp_plugin.py @@ -15,13 +15,14 @@ from openhcs.mcp.http import hosted_capability_registry from openhcs.mcp.http_auth import McpHttpAuthenticationMode +from openhcs.resources.brand import BRAND_PRIMARY_COLOR REPO_ROOT = Path(__file__).resolve().parents[1] LOCAL_PLUGIN_ROOT = REPO_ROOT / "packaging" / "codex" / "openhcs" LOCAL_PLUGIN_MANIFEST = LOCAL_PLUGIN_ROOT / ".codex-plugin" / "plugin.json" CHATGPT_PLUGIN_ASSET = ( - REPO_ROOT / "packaging" / "chatgpt" / "openhcs" / "assets" / "openhcs.svg" + REPO_ROOT / "openhcs" / "resources" / "assets" / "openhcs-icon-square.svg" ) APP_MANIFEST_PATH = "./.app.json" @@ -328,7 +329,7 @@ def _plugin_manifest( "privacyPolicyURL": privacy_policy_url, "termsOfServiceURL": terms_of_service_url, "defaultPrompt": list(STARTER_PROMPTS), - "brandColor": "#00A86B", + "brandColor": BRAND_PRIMARY_COLOR, "composerIcon": validate_plugin_relative_path( BRAND_ASSET_PATH, label="Plugin composer-icon path", diff --git a/scripts/build_website.py b/scripts/build_website.py index 3e644527d..c8fb64061 100644 --- a/scripts/build_website.py +++ b/scripts/build_website.py @@ -32,7 +32,15 @@ "assets/logos/tensorflow.svg", ) ASSET_SOURCES = { - "assets/logos/openhcs.svg": "openhcs/resources/assets/openhcs-mark.svg", + "assets/logos/openhcs-favicon.svg": ( + "openhcs/resources/assets/openhcs-favicon.svg" + ), + "assets/logos/openhcs-horizontal.svg": ( + "openhcs/resources/assets/openhcs-lockup-horizontal.svg" + ), + "assets/logos/openhcs-stacked.svg": ( + "openhcs/resources/assets/openhcs-lockup-stacked.svg" + ), "assets/ui.png": "docs/source/_static/ui.png", } REQUIRED_COPY = ( diff --git a/scripts/render_brand_assets.sh b/scripts/render_brand_assets.sh index 011b98cf2..5a2c1c867 100755 --- a/scripts/render_brand_assets.sh +++ b/scripts/render_brand_assets.sh @@ -4,8 +4,8 @@ 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" +source_svg="$asset_directory/openhcs-icon-square.svg" +raster_png="$asset_directory/openhcs-icon-square.png" windows_icon="$asset_directory/openhcs.ico" macos_icon="$asset_directory/openhcs.icns" diff --git a/server.json b/server.json index 1a224c983..101b0a28f 100644 --- a/server.json +++ b/server.json @@ -8,19 +8,19 @@ "url": "https://github.com/OpenHCSDev/OpenHCS", "source": "github" }, - "version": "0.7.2", + "version": "0.7.3", "packages": [ { "registryType": "pypi", "registryBaseUrl": "https://pypi.org", "identifier": "openhcs", - "version": "0.7.2", + "version": "0.7.3", "runtimeHint": "uvx", "runtimeArguments": [ { "type": "named", "name": "--with", - "value": "openhcs[gui,mcp,viz]==0.7.2", + "value": "openhcs[gui,mcp,viz]==0.7.3", "description": "Install the matching OpenHCS GUI, MCP, Napari, and Fiji dependencies." } ], diff --git a/tests/unit/test_brand_assets.py b/tests/unit/test_brand_assets.py index 48fac35de..030f7a740 100644 --- a/tests/unit/test_brand_assets.py +++ b/tests/unit/test_brand_assets.py @@ -1,19 +1,30 @@ from __future__ import annotations +import re import struct from xml.etree import ElementTree from openhcs.resources.brand import ( + BRAND_PRIMARY_COLOR, BrandAsset, brand_asset_bytes, brand_asset_path, ) +def test_primary_brand_color_matches_official_mark(): + assert BRAND_PRIMARY_COLOR == "#1D9E75" + + def test_brand_assets_are_one_complete_packaged_family() -> None: assert {asset.name for asset in BrandAsset} == { - "SCALABLE", - "RASTER", + "MARK", + "MARK_MONO", + "LOCKUP_HORIZONTAL", + "LOCKUP_STACKED", + "ICON_SQUARE", + "FAVICON", + "ICON_RASTER", "WINDOWS_ICON", "MACOS_ICON", } @@ -23,26 +34,57 @@ def test_brand_assets_are_one_complete_packaged_family() -> None: 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)) +def test_official_logo_family_preserves_declared_geometry_and_colors() -> None: + root = ElementTree.fromstring(brand_asset_bytes(BrandAsset.MARK)) 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 root.attrib["viewBox"] == "0 0 90 32" + elements = list(root) + assert len(elements) == 3 + assert elements[0].attrib["stroke"] == "#1D9E75" + assert elements[1].attrib["stroke"] == "#5DCAA5" + assert elements[2].attrib["fill"] == "#1D9E75" + + square = ElementTree.fromstring(brand_asset_bytes(BrandAsset.ICON_SQUARE)) + assert square.attrib["viewBox"] == "0 0 512 512" + background = square.find("svg:rect", namespace) assert background is not None + assert background.attrib["fill"] == "#0A0D16" + + for asset in ( + BrandAsset.MARK, + BrandAsset.LOCKUP_HORIZONTAL, + BrandAsset.LOCKUP_STACKED, + BrandAsset.ICON_SQUARE, + BrandAsset.FAVICON, + ): + assert b"#dda98b" not in brand_asset_bytes(asset).lower() + + +def test_stacked_wordmark_and_symbol_share_flush_width() -> None: + root = ElementTree.fromstring(brand_asset_bytes(BrandAsset.LOCKUP_STACKED)) + namespace = {"svg": "http://www.w3.org/2000/svg"} + text = root.find("svg:text", namespace) + mark = root.find("svg:g", namespace) + + assert text 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") + assert text.attrib["x"] == "7" + assert text.attrib["textLength"] == "140" + assert text.attrib["lengthAdjust"] == "spacingAndGlyphs" + + match = re.fullmatch( + r"translate\((?P[0-9.]+) (?P[0-9.]+)\) " + r"scale\((?P[0-9.]+)\)", + mark.attrib["transform"], + ) + assert match is not None + assert float(match.group("x")) == float(text.attrib["x"]) + assert abs(90 * float(match.group("scale")) - 140) < 0.01 def test_platform_brand_encodings_have_native_container_headers() -> None: - png = brand_asset_bytes(BrandAsset.RASTER) + png = brand_asset_bytes(BrandAsset.ICON_RASTER) assert png[:8] == b"\x89PNG\r\n\x1a\n" assert struct.unpack(">II", png[16:24]) == (1024, 1024) diff --git a/tests/unit/test_build_hosted_mcp_plugin.py b/tests/unit/test_build_hosted_mcp_plugin.py index 07aec89f1..8617de34e 100644 --- a/tests/unit/test_build_hosted_mcp_plugin.py +++ b/tests/unit/test_build_hosted_mcp_plugin.py @@ -68,6 +68,16 @@ def _source_asset(tmp_path): return source +def test_default_hosted_plugin_asset_uses_package_brand_authority(): + assert plugin_builder.CHATGPT_PLUGIN_ASSET == ( + plugin_builder.REPO_ROOT + / "openhcs" + / "resources" + / "assets" + / "openhcs-icon-square.svg" + ) + + def _build(tmp_path, *, output_name="plugin", **overrides): arguments = { **PUBLIC_URLS, @@ -114,7 +124,7 @@ def test_hosted_plugin_is_registered_read_only_projection(tmp_path): "Find OpenHCS functions for normalizing an image stack.", "Show me how to configure an OpenHCS pipeline for my microscopy assay.", ], - "brandColor": "#00A86B", + "brandColor": "#1D9E75", "composerIcon": "./assets/openhcs.svg", "logo": "./assets/openhcs.svg", } diff --git a/tests/unit/test_build_website.py b/tests/unit/test_build_website.py index 6e03b8c75..4ef3660a4 100644 --- a/tests/unit/test_build_website.py +++ b/tests/unit/test_build_website.py @@ -6,6 +6,7 @@ import pytest from scripts.build_website import ( + ASSET_SOURCES, CONTACT_EMAIL_TOKEN, RELEASE_VERSION_TOKEN, build_site, @@ -29,7 +30,6 @@ 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", @@ -40,15 +40,14 @@ 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 + authority = REPO_ROOT / "website/assets/logos" / logo_name assert (site_dir / "assets/logos" / logo_name).read_bytes() == ( authority.read_bytes() ) + for output_name, source_name in ASSET_SOURCES.items(): + assert (site_dir / output_name).read_bytes() == ( + REPO_ROOT / source_name + ).read_bytes() assert local_targets == ( "assets/logos/bioformats.svg", "assets/logos/cellprofiler.png", @@ -56,7 +55,9 @@ 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/openhcs-favicon.svg", + "assets/logos/openhcs-horizontal.svg", + "assets/logos/openhcs-stacked.svg", "assets/logos/pyclesperanto.png", "assets/logos/pytorch.svg", "assets/logos/tensorflow.svg", @@ -185,8 +186,10 @@ 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 html.count('src="assets/logos/openhcs-horizontal.svg"') == 1 + assert html.count('src="assets/logos/openhcs-stacked.svg"') == 1 + assert 'href="assets/logos/openhcs-favicon.svg"' in html + assert 'OpenHCS' not in html assert '' not in html assert 'class="hero-grid"' in html assert 'class="release-summary"' in html @@ -238,8 +241,9 @@ 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 document.count('src="assets/logos/openhcs-horizontal.svg"') == 1 + assert document.count('src="assets/logos/openhcs-stacked.svg"') == 1 + assert 'href="assets/logos/openhcs-favicon.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 @@ -286,7 +290,7 @@ def test_website_source_and_workflow_follow_package_metadata_authorities(): assert workflow.count(' - "openhcs/__init__.py"') == 2 assert ( workflow.count( - ' - "openhcs/resources/assets/openhcs-mark.svg"' + ' - "openhcs/resources/assets/openhcs-*.svg"' ) == 2 ) @@ -317,7 +321,9 @@ 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 + assert ( + 'src="openhcs/resources/assets/openhcs-lockup-stacked.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 66f24c0c4..15231f85d 100644 --- a/website/index.html +++ b/website/index.html @@ -20,7 +20,7 @@ > OpenHCS | Open-source high-content image analysis - + @@ -32,8 +32,7 @@