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
21 changes: 11 additions & 10 deletions .github/workflows/build-and-test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,18 +29,19 @@ jobs:
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
with:
fetch-depth: 1
- name: Set up Python
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6
- name: Setup Docker Buildx
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3
- name: Build BadgeSort container
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6
with:
python-version: '3.14'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -e .
pip install pytest pytest-cov
- name: Run tests
context: .
load: true
tags: badgesort:test
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Run tests in container
run: |
python -m pytest tests/ -v --cov=badgesort --cov-report=term-missing
docker run --rm badgesort:test sh -c ". \$VENV_PATH/bin/activate && pip install pytest pytest-cov && pytest tests/ -v --cov=badgesort --cov-report=term-missing"
image:
name: Build Image
needs: [test]
Expand Down
34 changes: 27 additions & 7 deletions badgesort/icons.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,19 +23,31 @@
# Cache for logo availability checks to avoid repeated requests
_logo_availability_cache = {}

# GitHub camo proxy constants
CAMO_URL_LIMIT = 8192
CAMO_OVERHEAD = 76 # base URL (35) + digest (40) + slash (1)

logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)

def svg_to_base64_data_uri(svg_content, fill_color='white', max_url_length=3700):
def _calculate_camo_url_length(badge_url):
"""Calculate the approximate camo URL length for a badge URL.

GitHub's camo proxy format: https://camo.githubusercontent.com/<digest>/<hex-encoded-url>
"""
return CAMO_OVERHEAD + (len(badge_url.encode('utf-8')) * 2)

def svg_to_base64_data_uri(svg_content, fill_color='white', max_url_length=3550):
"""Convert an SVG to a compressed base64-encoded data URI with specified fill color optimized for 14x14px badges.

Args:
svg_content: SVG content as string
fill_color: Fill color for the SVG paths ('white', 'black', or None)
max_url_length: Maximum data URI length before falling back to PNG rasterization.
Default 3700 chars ensures badge URLs stay under GitHub's camo proxy
Default 3550 chars ensures badge URLs stay under GitHub's camo proxy
8192 char limit (which hex-encodes URLs: 76 + url_len*2 <= 8192).
Calculation: safe_url = (8192 - 76) / 2 - overhead ≈ 3850, with 150 char margin = 3700
Calculation: (8192 - 76 camo_overhead) / 2 hex_encoding - ~109 badge_url_overhead = 3949, with 10% margin = 3550

Copilot AI Nov 13, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The calculation description states "with 10% margin = 3550" but this is slightly unclear. The actual calculation would be: 3949 × 0.9 ≈ 3554, but the code uses 3550 (which is even more conservative). Consider clarifying the description to either:

  • "with ~10% margin (rounded down) = 3550" or
  • "with 399 char safety margin (≈10%) = 3550"

This would make it clearer that 3550 is a rounded-down value for safety.

Suggested change
Calculation: (8192 - 76 camo_overhead) / 2 hex_encoding - ~109 badge_url_overhead = 3949, with 10% margin = 3550
Calculation: (8192 - 76 camo_overhead) / 2 hex_encoding - ~109 badge_url_overhead = 3949, with ~10% margin (rounded down) = 3550

Copilot uses AI. Check for mistakes.
(~109 char badge_url_overhead is the badge URL structure: domain, path, parameters, excluding the data URI payload)

Uses scour-based SVG compression for optimal file size. For very large SVGs that would exceed
URL length limits, falls back to PNG rasterization at 14x14px.
Expand Down Expand Up @@ -63,7 +75,8 @@ def svg_to_base64_data_uri(svg_content, fill_color='white', max_url_length=3700)
if png_data_uri:
return png_data_uri
else:
logger.debug('PNG fallback failed, using original SVG despite size')
logger.warning(f'PNG fallback failed for oversized SVG ({len(svg_data_uri)} chars > {max_url_length} limit). '
f'Using original SVG despite size. Badge may exceed GitHub camo URL limit.')
# For now, return the original SVG data URI even if it's too long
# This is better than skipping the icon entirely

Expand Down Expand Up @@ -424,8 +437,8 @@ def run(args):
if should_embed_svg:
logger.debug(f'Embedding SVG data URI for {icon.slug}')
# Convert SVG to base64 data URI for embedding
# Use 3700 char limit to stay under GitHub camo's 8192 char limit
icon_data_uri = svg_to_base64_data_uri(icon.svg, icon_hex_comp, max_url_length=3700)
# Use 3550 char limit to stay under GitHub camo's 8192 char limit
icon_data_uri = svg_to_base64_data_uri(icon.svg, icon_hex_comp, max_url_length=3550)
icon_data_uri_encoded = quote(icon_data_uri, safe='')
icon_url = f'{icon_base}/{icon_title_safe}-{badge_color}.svg' if icon_title_safe else f'{icon_base}/-{badge_color}.svg'
icon_url += f'?style={args.badge_style}&logo={icon_data_uri_encoded}'
Expand All @@ -449,7 +462,7 @@ def run(args):
background_color = badge_color

# Always use white icons for good contrast against any background
icon_data_uri = svg_to_base64_data_uri(icon.svg, 'white', max_url_length=3700)
icon_data_uri = svg_to_base64_data_uri(icon.svg, 'white', max_url_length=3550)
icon_data_uri_encoded = quote(icon_data_uri, safe='')
icon_url = f'{icon_base}/icon/{icon_title_safe}?icon={icon_data_uri_encoded}&label&color={background_color}&labelColor={background_color}' if icon_title_safe else f'{icon_base}/icon/?icon={icon_data_uri_encoded}&label&color={background_color}&labelColor={background_color}'
else:
Expand All @@ -458,6 +471,13 @@ def run(args):

# Store custom URL if provided
custom_url = custom_params.get('url', None)

# Check if badge URL would exceed GitHub's camo proxy limit and warn
camo_length = _calculate_camo_url_length(icon_url)
if camo_length > CAMO_URL_LIMIT:
logger.warning(f'Badge URL for {icon.slug} exceeds GitHub camo limit: {camo_length} > {CAMO_URL_LIMIT} chars. '
f'Badge may not render correctly on GitHub. Consider using a simpler icon or badge style.')

icon_list.append({
'rgb': icon_rgb,
'slug': icon.slug,
Expand Down
6 changes: 3 additions & 3 deletions tests/test_camo_url_limits.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,14 @@ def calculate_camo_url_length(badge_url):

def test_svg_data_uri_max_length_default():
"""Test that default max_url_length respects camo limits."""
# The default max_url_length should be 3700 to stay under camo's 8192 limit
# The default max_url_length should be 3550 to stay under camo's 8192 limit
# This test verifies the function signature has the correct default
import inspect
sig = inspect.signature(svg_to_base64_data_uri)
max_url_length_default = sig.parameters['max_url_length'].default

assert max_url_length_default == 3700, \
f"Default max_url_length should be 3700 (found {max_url_length_default})"
assert max_url_length_default == 3550, \
f"Default max_url_length should be 3550 (found {max_url_length_default})"


def test_shields_embedded_svg_urls_under_camo_limit():
Expand Down
Loading