|
| 1 | +#!/usr/bin/env bash |
| 2 | +# SVG Logo Validation Script |
| 3 | +# Checks generated SVG against the skill's technical requirements. |
| 4 | +# Usage: ./validate-svg.sh <path-to-svg> |
| 5 | + |
| 6 | +set -euo pipefail |
| 7 | + |
| 8 | +if [[ $# -lt 1 ]]; then |
| 9 | + echo "Usage: $0 <svg-file>" |
| 10 | + exit 1 |
| 11 | +fi |
| 12 | + |
| 13 | +SVG_FILE="$1" |
| 14 | +if [[ ! -f "$SVG_FILE" ]]; then |
| 15 | + echo "Error: File not found: $SVG_FILE" |
| 16 | + exit 1 |
| 17 | +fi |
| 18 | + |
| 19 | +PASS=0 |
| 20 | +FAIL=0 |
| 21 | +WARN=0 |
| 22 | + |
| 23 | +pass() { echo " ✓ PASS: $1"; ((PASS++)); } |
| 24 | +fail() { echo " ✗ FAIL: $1"; ((FAIL++)); } |
| 25 | +warn() { echo " ⚠ WARN: $1"; ((WARN++)); } |
| 26 | + |
| 27 | +echo "=== SVG Logo Validation: $SVG_FILE ===" |
| 28 | +echo "" |
| 29 | + |
| 30 | +# --- Banned Elements --- |
| 31 | +echo "Banned Elements:" |
| 32 | +for elem in "<text" "<tspan" "<filter" "<feGaussianBlur" "<feDropShadow" "<image" "<style" "<script" "<animate" "<foreignObject"; do |
| 33 | + if grep -q "$elem" "$SVG_FILE"; then |
| 34 | + fail "Contains banned element: $elem" |
| 35 | + fi |
| 36 | +done |
| 37 | +# Check for base64 embedded data |
| 38 | +if grep -q "base64," "$SVG_FILE"; then |
| 39 | + fail "Contains embedded base64 data" |
| 40 | +fi |
| 41 | +# Check for external use references |
| 42 | +if grep -qE 'href="https?://' "$SVG_FILE"; then |
| 43 | + fail "Contains external URL reference" |
| 44 | +fi |
| 45 | +if [[ $FAIL -eq 0 ]]; then |
| 46 | + pass "No banned elements found" |
| 47 | +fi |
| 48 | + |
| 49 | +echo "" |
| 50 | + |
| 51 | +# --- Accessibility --- |
| 52 | +echo "Accessibility:" |
| 53 | +if grep -q "<title" "$SVG_FILE"; then |
| 54 | + pass "<title> element present" |
| 55 | +else |
| 56 | + fail "Missing <title> element" |
| 57 | +fi |
| 58 | + |
| 59 | +if grep -q "<desc" "$SVG_FILE"; then |
| 60 | + pass "<desc> element present" |
| 61 | +else |
| 62 | + fail "Missing <desc> element" |
| 63 | +fi |
| 64 | + |
| 65 | +if grep -q 'role="img"' "$SVG_FILE"; then |
| 66 | + pass 'role="img" present' |
| 67 | +else |
| 68 | + fail 'Missing role="img" on root <svg>' |
| 69 | +fi |
| 70 | + |
| 71 | +if grep -q "aria-labelledby" "$SVG_FILE"; then |
| 72 | + pass "aria-labelledby present" |
| 73 | +else |
| 74 | + fail "Missing aria-labelledby on root <svg>" |
| 75 | +fi |
| 76 | + |
| 77 | +echo "" |
| 78 | + |
| 79 | +# --- ViewBox & Dimensions --- |
| 80 | +echo "ViewBox & Dimensions:" |
| 81 | +if grep -q "viewBox" "$SVG_FILE"; then |
| 82 | + pass "viewBox attribute present" |
| 83 | +else |
| 84 | + fail "Missing viewBox attribute" |
| 85 | +fi |
| 86 | + |
| 87 | +# Check for hardcoded pixel dimensions (width="123px" or height="123px") |
| 88 | +if grep -qE '(width|height)="[0-9]+(px)?"' "$SVG_FILE" | head -1 && grep -qE '<svg[^>]*(width|height)="[0-9]+(px)?"' "$SVG_FILE"; then |
| 89 | + warn "Root <svg> has hardcoded width/height — prefer viewBox-only for scalability" |
| 90 | +fi |
| 91 | + |
| 92 | +if grep -q 'xmlns="http://www.w3.org/2000/svg"' "$SVG_FILE"; then |
| 93 | + pass "xmlns namespace present" |
| 94 | +else |
| 95 | + fail "Missing xmlns namespace" |
| 96 | +fi |
| 97 | + |
| 98 | +echo "" |
| 99 | + |
| 100 | +# --- Coordinate Precision --- |
| 101 | +echo "Coordinate Precision:" |
| 102 | +# Find coordinates with more than 2 decimal places in path data |
| 103 | +EXCESS_DECIMALS=$(grep -oE '[0-9]+\.[0-9]{3,}' "$SVG_FILE" | wc -l) |
| 104 | +if [[ $EXCESS_DECIMALS -gt 0 ]]; then |
| 105 | + fail "Found $EXCESS_DECIMALS coordinates with >2 decimal places" |
| 106 | +else |
| 107 | + pass "All coordinates within 2 decimal places" |
| 108 | +fi |
| 109 | + |
| 110 | +echo "" |
| 111 | + |
| 112 | +# --- Path Closure --- |
| 113 | +echo "Path Closure:" |
| 114 | +# Extract all d="..." attributes and check each subpath ends with Z/z |
| 115 | +# This is a heuristic: count M/m commands vs Z/z commands in path data |
| 116 | +M_COUNT=$(grep -oE '[Mm]' "$SVG_FILE" | wc -l) |
| 117 | +Z_COUNT=$(grep -oE '[Zz]' "$SVG_FILE" | wc -l) |
| 118 | +if [[ $M_COUNT -gt 0 && $Z_COUNT -lt $M_COUNT ]]; then |
| 119 | + warn "Found $M_COUNT path subpaths but only $Z_COUNT closures (Z) — some paths may be unclosed" |
| 120 | +else |
| 121 | + pass "Path closure count looks correct ($M_COUNT subpaths, $Z_COUNT closures)" |
| 122 | +fi |
| 123 | + |
| 124 | +echo "" |
| 125 | + |
| 126 | +# --- File Size --- |
| 127 | +echo "File Size:" |
| 128 | +FILE_SIZE=$(wc -c < "$SVG_FILE") |
| 129 | +if [[ $FILE_SIZE -gt 5120 ]]; then |
| 130 | + fail "File size ${FILE_SIZE} bytes exceeds 5KB wordmark budget" |
| 131 | +elif [[ $FILE_SIZE -gt 3072 ]]; then |
| 132 | + warn "File size ${FILE_SIZE} bytes — OK for wordmarks, over budget for simple marks (<1.5KB) and moderate marks (<3KB)" |
| 133 | +elif [[ $FILE_SIZE -gt 1536 ]]; then |
| 134 | + warn "File size ${FILE_SIZE} bytes — OK for moderate marks, over budget for simple marks (<1.5KB)" |
| 135 | +else |
| 136 | + pass "File size ${FILE_SIZE} bytes — within simple mark budget" |
| 137 | +fi |
| 138 | + |
| 139 | +echo "" |
| 140 | + |
| 141 | +# --- Shape Count --- |
| 142 | +echo "Shape Count:" |
| 143 | +# Count visible shape elements (rect, circle, ellipse, polygon, polyline, line, path) |
| 144 | +SHAPE_COUNT=$(grep -coE '<(rect|circle|ellipse|polygon|polyline|line|path)[\s/>]' "$SVG_FILE" || echo 0) |
| 145 | +# Subtract shapes inside <defs> (they're templates, not visible) |
| 146 | +DEFS_SHAPES=$(sed -n '/<defs>/,/<\/defs>/p' "$SVG_FILE" | grep -coE '<(rect|circle|ellipse|polygon|polyline|line|path)[\s/>]' || echo 0) |
| 147 | +VISIBLE_SHAPES=$((SHAPE_COUNT - DEFS_SHAPES)) |
| 148 | +# Add <use> elements (they instantiate shapes) |
| 149 | +USE_COUNT=$(grep -coE '<use[\s/>]' "$SVG_FILE" || echo 0) |
| 150 | +TOTAL=$((VISIBLE_SHAPES + USE_COUNT)) |
| 151 | + |
| 152 | +if [[ $TOTAL -gt 7 ]]; then |
| 153 | + warn "Shape count: $TOTAL visible elements (budget: ≤7 for marks, wordmark letters exempt)" |
| 154 | +else |
| 155 | + pass "Shape count: $TOTAL visible elements (within budget)" |
| 156 | +fi |
| 157 | + |
| 158 | +echo "" |
| 159 | + |
| 160 | +# --- Unused Defs --- |
| 161 | +echo "Unused Defs:" |
| 162 | +if grep -q "<defs>" "$SVG_FILE"; then |
| 163 | + # Extract IDs defined in defs |
| 164 | + DEFS_IDS=$(sed -n '/<defs>/,/<\/defs>/p' "$SVG_FILE" | grep -oE 'id="[^"]*"' | sed 's/id="//;s/"//' || true) |
| 165 | + UNUSED=0 |
| 166 | + for id in $DEFS_IDS; do |
| 167 | + # Check if this ID is referenced elsewhere (href, url(), aria-labelledby, etc.) |
| 168 | + REF_COUNT=$(grep -c "$id" "$SVG_FILE" || echo 0) |
| 169 | + if [[ $REF_COUNT -le 1 ]]; then |
| 170 | + warn "Potentially unused def: id=\"$id\"" |
| 171 | + ((UNUSED++)) |
| 172 | + fi |
| 173 | + done |
| 174 | + if [[ $UNUSED -eq 0 ]]; then |
| 175 | + pass "All defs appear to be referenced" |
| 176 | + fi |
| 177 | +else |
| 178 | + pass "No <defs> block (nothing to check)" |
| 179 | +fi |
| 180 | + |
| 181 | +echo "" |
| 182 | + |
| 183 | +# --- Editor Metadata --- |
| 184 | +echo "Editor Metadata:" |
| 185 | +METADATA_FOUND=0 |
| 186 | +for pattern in "data-name" "xml:space" "inkscape:" "sodipodi:" "illustrator" "sketch:"; do |
| 187 | + if grep -qi "$pattern" "$SVG_FILE"; then |
| 188 | + fail "Contains editor metadata: $pattern" |
| 189 | + ((METADATA_FOUND++)) |
| 190 | + fi |
| 191 | +done |
| 192 | +if [[ $METADATA_FOUND -eq 0 ]]; then |
| 193 | + pass "No editor metadata found" |
| 194 | +fi |
| 195 | + |
| 196 | +echo "" |
| 197 | + |
| 198 | +# --- Summary --- |
| 199 | +echo "=== Summary ===" |
| 200 | +echo " Passed: $PASS" |
| 201 | +echo " Failed: $FAIL" |
| 202 | +echo " Warnings: $WARN" |
| 203 | +echo "" |
| 204 | + |
| 205 | +if [[ $FAIL -gt 0 ]]; then |
| 206 | + echo "RESULT: FAIL ($FAIL issues must be fixed)" |
| 207 | + exit 1 |
| 208 | +else |
| 209 | + echo "RESULT: PASS (with $WARN warnings)" |
| 210 | + exit 0 |
| 211 | +fi |
0 commit comments