Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion great_docs/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -718,9 +718,13 @@ def _generate_favicons(self, logo_src: Path, dest_dir: Path) -> dict[str, str]:
"""
import io

import cairosvg
from PIL import Image

try:
import cairosvg
except ImportError:
cairosvg = None # type: ignore[assignment]

result: dict[str, str] = {}
suffix = logo_src.suffix.lower()

Expand All @@ -733,6 +737,16 @@ def _generate_favicons(self, logo_src: Path, dest_dir: Path) -> dict[str, str]:
result["icon-svg"] = "favicon.svg"
result["icon"] = "favicon.svg"

if cairosvg is None:
print(
"Favicon: cairosvg is not installed; skipping raster favicon "
"generation from SVG. Install it with:\n"
" pip install 'great-docs[svg]'\n"
"On Linux you also need: apt install libcairo2-dev\n"
"On macOS: brew install cairo"
)
return result

# Rasterize SVG → PNG preserving aspect ratio, then resize
png_data = cairosvg.svg2png(url=str(logo_src), scale=4)
raw = Image.open(io.BytesIO(png_data))
Expand Down
4 changes: 3 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,13 @@ dependencies = [
"pygments>=2.0.0",
"requests>=2.25.0",
"Pillow>=9.0.0",
"cairosvg>=2.5.0",
"ruff>=0.9.9",
]

[project.optional-dependencies]
svg = [
"cairosvg>=2.5.0",
]
dev = [
"pytest>=6.0",
"pytest-cov>=3.0",
Expand Down
26 changes: 26 additions & 0 deletions tests/test_great_docs.py
Original file line number Diff line number Diff line change
Expand Up @@ -9477,9 +9477,30 @@ def test_downscale_preserves_aspect_ratio(self):
assert result.size == (100, 100)


def _make_mock_cairosvg():
"""Create a mock cairosvg module that returns valid PNG data from svg2png."""
import io as _io

mock_mod = types.ModuleType("cairosvg")

def svg2png(**kwargs):
img = PILImage.new("RGBA", (256, 256), (0, 0, 255, 255))
buf = _io.BytesIO()
img.save(buf, format="PNG")
return buf.getvalue()

mock_mod.svg2png = svg2png # type: ignore[attr-defined]
return mock_mod


class TestGenerateFaviconsSvg:
"""Tests for _generate_favicons with SVG source."""

@pytest.fixture(autouse=True)
def _mock_cairosvg(self):
with patch.dict("sys.modules", {"cairosvg": _make_mock_cairosvg()}):
yield

def test_svg_generates_all_files(self):
"""SVG source should produce ico, svg, 16px, 32px, and apple-touch-icon."""
with tempfile.TemporaryDirectory() as tmp_dir:
Expand Down Expand Up @@ -9725,6 +9746,11 @@ def test_favicon_invalid_type_returns_none(self):
class TestFaviconLinkInjection:
"""Tests for favicon <link> tag injection into _quarto.yml."""

@pytest.fixture(autouse=True)
def _mock_cairosvg(self):
with patch.dict("sys.modules", {"cairosvg": _make_mock_cairosvg()}):
yield

def _build_with_favicon(
self, tmp_dir: str, favicon_config: str | None = None, create_logo: bool = False
) -> dict:
Expand Down
Loading