SVG Utility Ecosystem¶
This document provides a comprehensive overview of the SVG utility ecosystem in ZooUI, explaining the content-addressable SVG cache, the five shape-type-specific detection and elongation utilities, their shared architecture patterns, and their integration with the scene widget and dialog system.
Overview¶
The SVG utility ecosystem is responsible for:
Storing and retrieving SVG content via a content-addressable disk cache
Detecting the shape type of loaded SVG content (arrow, stick, circle, square, triangle)
Elongating shapes interactively via mouse wheel with modifier keys
Managing the
svg_hash-prefix addressing scheme used across the applicationModifying SVG stroke color and line thickness via dialog controls
The ecosystem spans 6 files (~2,620 lines total) organized into a cache layer and five parallel shape utility modules. All utilities share a common architecture: XML namespace-aware ElementTree parsing, content-addressable cache integration, and non-destructive (hash-returning) transformation pipelines.
Architecture¶
┌─────────────────────────────────────────────────────────────────┐
│ QZUI Widget │
│ (wheelEvent: Ctrl+wheel on selected SVG triggers elongation) │
└─────────────┬───────────────────────────────────────────────────┘
│
│ Import from mediaobjectsutils.svg.utils
│
┌─────────────▼───────────────────────────────────────────────────┐
│ Shape Detection Cascade │
│ is_straight_arrow_svg? → elongate_straight_arrow() │
│ is_diagonal_arrow_svg? → elongate_diagonal_arrow() │
│ is_square_svg? → elongate_square() │
│ is_circle_svg? → elongate_circle() │
│ is_triangle_svg? → elongate_triangle() │
│ is_stick_svg? → elongate_stick() │
└─────────────┬───────────────────────────────────────────────────┘
│
│ All functions parse SVG via _load_svg_tree()
│ (resolves file path OR svg_ cache hash)
│
┌─────────────▼───────────────────────────────────────────────────┐
│ Shape Utility Modules │
│ svgarrowutils.py (663 lines) — arrow + diagonal │
│ svgstickutils.py (565 lines) — stick/line + diagonal │
│ svgcircleutils.py (391 lines) — circle/ellipse │
│ svgtriangleutils.py (398 lines) — triangle │
│ svgsquareutils.py (326 lines) — square/rectangle │
│ │
│ Shared patterns across all 5 modules: │
│ • _load_svg_tree() — file-or-cache → ElementTree │
│ • SVG_NS = {'svg': 'http://www.w3.org/2000/svg'} │
│ • Elongation → store in cache → return svg_ hash │
│ • viewBox recalculation with 10%/20px padding │
└─────────────┬───────────────────────────────────────────────────┘
│
│ Store / retrieve via cache hash
│
┌─────────────▼───────────────────────────────────────────────────┐
│ SVGCache │
│ • Storage: ~/.cache/zooui/svg/ (flat directory) │
│ • Addressing: svg_{8-char SHA1} content hash │
│ • store_svg() — deduplicate, validate, write │
│ • get_svg_content() — read by hash │
│ • cleanup_on_exit() — remove all files on exit │
└────────────────────────────┬────────────────────────────────────┘
│
│ Consumed by
│
┌────────────────────────────┼────────────────────────────────────┐
│ ▼ │
│ ┌──────────────────┐ ┌────────────────┐ ┌─────────────────┐ │
│ │ SVGMediaObject │ │ SVG Picker │ │ SVG Modifier │ │
│ │ • _get_svg_ │ │ Dialog │ │ Dialog │ │
│ │ load_path() │ │ • _modify_ │ │ • Uses cache │ │
│ │ • svg_ prefix │ │ svg_file() │ │ for modified │ │
│ │ detection │ │ • Direct ET │ │ SVGs │ │
│ └──────────────────┘ └────────────────┘ └─────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
SVGCache¶
SVGCache provides a content-addressable disk cache for SVG data.
All elongation results and picker-created SVGs are stored here.
Storage Layout:
~/.cache/zooui/svg/
├── svg_a1b2c3d4.svg
├── svg_e5f6a7b8.svg
└── ...
Content is stored in a flat directory with no subdirectory hierarchy.
Each file is named {hash}.svg where the hash is the first 8 characters
of a SHA1 digest of the UTF-8 SVG XML content.
Content Addressing:
import hashlib
def compute_svg_hash(svg_content: str) -> str:
return f"svg_{hashlib.sha1(svg_content.encode('utf-8')).hexdigest()[:8]}"
The svg_ prefix is the universal discriminator — every consumer
detects cache references by checking if media_id.startswith('svg_').
Singleton Access:
from zooui.objects.mediaobjects.mediaobjectsutils.svg.svgcache.svgcache import (
get_svg_cache,
)
cache = get_svg_cache()
# Always returns the same SVGCache instance
The singleton is lazily created on first access and stored at module level
in a private _svg_cache_instance variable.
Public API:
Method |
Signature |
Returns |
Description |
|---|---|---|---|
|
|
cache hash |
Validate, deduplicate, write to disk. Retries on write failure. |
|
|
file path |
Resolve hash to filesystem path |
|
|
boolean |
Check if hash exists on disk |
|
|
SVG string |
Read from cache; returns None on miss |
|
|
|
Time-based removal |
|
|
— |
Remove all cache files on program exit |
|
|
statistics |
File count, total size, MB |
Retry Logic:
On write failure, store_svg() appends <!-- retry_N --> to the SVG
content to produce a new hash, then retries up to max_retries times.
This handles rare hash collisions and transient filesystem errors.
XML Parsing and Namespace Handling¶
All shape utilities use Python’s xml.etree.ElementTree for SVG parsing.
The SVG namespace is defined as a module-level constant in every utility:
SVG_NS = {'svg': 'http://www.w3.org/2000/svg'}
Parsing Patterns:
Operation |
API Used |
|---|---|
Load from file |
|
Load from cache string |
|
Find single element |
|
Find all elements |
|
Read numeric attr |
|
Modify attr |
|
Serialize to string |
|
Tag Name Expansion:
When programmatically changing an element’s tag (e.g., circle to ellipse), the fully expanded namespace form is required:
circle.tag = '{http://www.w3.org/2000/svg}ellipse'
This is a subtlety of ElementTree — tags are stored internally in
{namespace}localname format.
Shape Detection¶
Each utility provides is_*_svg() detection functions that inspect
SVG element structure to determine whether a given SVG matches a known shape.
Shared Loader:
All detection functions use a common internal helper:
def _load_svg_tree(svg_input: str) -> ET.ElementTree:
if svg_input.startswith('svg_'):
content = get_svg_cache().get_svg_content(svg_input)
root = ET.fromstring(content)
return ET.ElementTree(root)
else:
return ET.parse(svg_input)
This adapter makes all detection functions transparently support both file paths and cache hashes.
Detection Matrix:
Shape |
Detection Function |
SVG Elements Checked |
Key Validation |
|---|---|---|---|
Straight Arrow |
|
1 |
Line must be horizontal (Δy < 0.1) or vertical (Δx < 0.1) |
Diagonal Arrow |
|
1 |
Dx ≈ Dy within 10% tolerance |
Any Arrow |
|
(composite) |
Straight OR diagonal |
Straight Stick |
|
1 |
No arrowhead present |
Diagonal Stick |
|
1 |
No arrowhead, 45° alignment |
Any Stick |
|
(composite) |
Straight OR diagonal |
Circle |
|
1 |
Validates cx, cy, r (circle); cx, cy, rx, ry (ellipse) |
Square |
|
1 |
Validates x, y, width, height are numeric and > 0 |
Triangle |
|
1 |
Distinguishes from arrows by requiring zero |
Arrow vs Stick Distinction:
The critical difference is element count: arrows have 1 <line>
plus 1 <polygon> (the arrowhead), while sticks have only 1 <line>
and zero polygons. The triangle utility additionally checks for zero
<line> elements to avoid misclassifying arrows as triangles.
Shape Elongation¶
Elongation transforms a shape’s dimensions by a scale factor, producing a modified copy stored in SVGCache. All elongation functions are non-destructive — the original file is never modified.
Entry Point — QZUI Wheel Event:
User scrolls wheel on selected SVG object
│
▼
QZUI.wheelEvent() checks modifier keys:
• Ctrl only → 1D elongation (arrows, sticks)
• Ctrl+Shift → 2D Y-only (squares, circles, triangles)
• Shift only → 2D X-only (squares, circles, triangles)
• Ctrl+Shift → 2D proportional
│
▼
Detection cascade (first match wins):
1. is_straight_arrow_svg()? → elongate_straight_arrow()
2. is_diagonal_arrow_svg()? → elongate_diagonal_arrow()
3. is_square_svg()? → elongate_square()
4. is_circle_svg()? → elongate_circle()
5. is_triangle_svg()? → elongate_triangle()
6. is_stick_svg()? → elongate_stick()
7. (fall through to normal zoom)
│
▼
Result: svg_ hash updated on SVGMediaObject._media_id
QSvgRenderer reloaded from cache path
Object dimensions updated and repainted
Scale Factor Calculation:
degrees = event.angleDelta().y()
elongation_delta = degrees / 360.0
current_factor = max(1.0 + elongation_delta, 0.2)
# 720° scroll = factor of 2.0 (doubling)
# Minimum factor clamped to 0.2
Arrow Elongation¶
Functions: elongate_straight_arrow(), elongate_diagonal_arrow()
Algorithm:
Load SVG via
_load_svg_tree()and detect directionScale the line endpoint:
new_x2 = x1 + (x2 - x1) * scale_factorTranslate the 3-point polygon (arrowhead) by the same
(dx, dy)offset — uniform translation, not scaling; the arrowhead maintains its original sizeRecompute
viewBoxto encompass all points with padding (10% of SVG dimensions or 20px, whichever is larger)Supports negative coordinates in viewBox for left/upward extensions
Serialize, store in cache, return cache hash
Stick Elongation¶
Functions: elongate_stick() → dispatches to elongate_straight_stick()
or elongate_diagonal_stick()
Algorithm:
Same as arrow elongation but without the arrowhead translation step.
Sticks have only a <line> element — the endpoint is scaled along the
direction of elongation.
Circle/ellipse Elongation¶
Function: elongate_circle(svg_path, scale_x, scale_y)
Algorithm:
Find
<circle>or<ellipse>elementFor proportional scaling: update
rattribute (circle) orrx/ry(ellipse)For non-proportional scaling of a circle: convert to ellipse by changing
tagto{ns}ellipse, settingrx/ry, and removingrCompute axis-aligned bounding box from
(cx - rx, cy - ry)to(cx + rx, cy + ry)Recompute viewBox with padding; serialize; cache; return hash
Triangle Elongation¶
Function: elongate_triangle(svg_path, scale_x, scale_y)
Algorithm:
Parse the 3-point
<polygon>using_parse_polygon_points()Calculate centroid via
_calculate_triangle_centroid()(arithmetic mean)Scale all 3 vertices outward from centroid using
_scale_points_from_centerwith separate X and Y factorsFormat points to 6 decimal precision
Recompute bounding box, apply padding, update viewBox
Serialize; cache; return hash
Square/Rectangle Elongation¶
Function: elongate_square(svg_path, scale_x, scale_y)
Algorithm:
Read current
x,y,width,heightfrom<rect>Compute center:
center_x = x + width/2,center_y = y + height/2Scale dimensions:
new_width = width * scale_x,new_height = height * scale_yRecompute position keeping center fixed:
new_x = center_x - new_width/2Update all four attributes on the
<rect>elementRecompute viewBox from corner points; serialize; cache; return hash
Direction Detection¶
Arrow and stick utilities provide direction-detection functions that classify the orientation of the shape:
Shape |
Function |
Directions |
|---|---|---|
Straight Arrow |
|
|
Diagonal Arrow |
|
|
Straight Stick |
|
|
Diagonal Stick |
|
|
Stick (composite) |
|
tries straight first, then diagonal |
Bounds Query Functions¶
Three shape utilities provide bounding-box query functions:
get_circle_bounds()→(cx, cy, rx, ry)— for circles,rx = ry = rget_triangle_bounds()→(min_x, min_y, width, height)get_rectangle_bounds()→(x, y, width, height)
Integration with Dialogs¶
OpenSVGPickerInputDialog¶
The SVG picker dialog (File > Open new SVG, Ctrl+G) creates new SVG
objects from a browser of zooui/data/SVG/ files. It performs inline XML
manipulation (not using the shape utility modules) for color/thickness:
1. User selects SVG file + color + thickness
2. _modify_svg_file() parses SVG with ET
3. Applies color to stroke/fill attributes
4. Applies thickness to stroke-width
5. Stores result in SVGCache via store_svg()
6. Returns cache hash as media_id
ModifySVGInputDialog¶
The SVG modifier dialog (right-click on SVG object) allows changing stroke color and line thickness of existing SVG shapes:
1. Loads current SVG content from cache or file
2. User selects new color and/or thickness
3. SVG XML is modified in place (stroke, stroke-width)
4. Result stored in SVGCache with new content hash
5. Object's media_id updated to new hash
6. is_modified flag set to True
Unlike the picker, the modifier dialog interacts with SVGCache but does not use the shape detection/elongation utilities — it operates on general SVG content rather than detecting specific shapes.
Export Chain¶
The utilities are exported through a layered module hierarchy:
mediaobjectsutils/__init__.py ← Top-level re-export (29 names)
├── svg.svgcache.svgcache → SVGCache, compute_svg_hash, get_svg_cache
└── svg.utils → All shape detection and elongation functions
├── svgarrowutils.py → 8 functions
├── svgstickutils.py → 9 functions
├── svgcircleutils.py → 3 functions
├── svgtriangleutils.py → 3 functions
└── svgsquareutils.py → 3 functions
Import Locations:
Consumer |
Imports From |
Used For |
|---|---|---|
|
|
Shape detection cascade + elongation in wheelEvent |
|
|
Resolve cache hashes to file paths for QSvgRenderer |
|
|
Create/cache colorized SVGs with inline XML manipulation |
|
|
Cache modified SVG content |
Usage Example¶
Programmatic Shape Detection and Elongation¶
from zooui.objects.mediaobjects.mediaobjectsutils.svg import (
is_arrow_svg,
is_circle_svg,
is_square_svg,
is_triangle_svg,
is_stick_svg,
elongate_square,
)
svg_path = "zooui/data/SVG/red_square.svg"
# Detect shape type
if is_square_svg(svg_path):
print("Detected: square")
# Elongate by factor 2.0 in X and 1.5 in Y
new_hash = elongate_square(svg_path, scale_x=2.0, scale_y=1.5)
print(f"Elongated SVG cached as: {new_hash}")
elif is_circle_svg(svg_path):
print("Detected: circle")
elif is_triangle_svg(svg_path):
print("Detected: triangle")
Working with SVGCache¶
from zooui.objects.mediaobjects.mediaobjectsutils.svg.svgcache.svgcache import (
get_svg_cache,
compute_svg_hash,
)
cache = get_svg_cache()
# Store SVG content
with open("my_custom.svg") as f:
content = f.read()
svg_hash = cache.store_svg(content)
print(f"Stored as: {svg_hash}") # e.g., svg_a1b2c3d4
# Retrieve content
cached = cache.get_svg_content(svg_hash)
print(f"Content length: {len(cached)} bytes")
# Get file path for QSvgRenderer
path = cache.get_cache_path(svg_hash)
print(f"On disk: {path}") # ~/.cache/zooui/svg/svg_a1b2c3d4.svg
# Compute hash without storing
hash_only = compute_svg_hash("<svg>...</svg>")
print(f"Would be stored as: {hash_only}")
# Cleanup
files_removed, bytes_freed = cache.cleanup_old_files(max_age_days=7)
print(f"Cleaned up {files_removed} files, freed {bytes_freed} bytes")
See Also¶
SVG Features — User-facing SVG features guide
Window System — SVG picker and modifier dialog details
Object System — SVGMediaObject and shape integration
../zooui/svgcache — SVGCache API reference
../zooui/svgarrowutils — Arrow utility API reference
../zooui/svgcircleutils — Circle utility API reference
../zooui/svgsquareutils — Square utility API reference
../zooui/svgtriangleutils — Triangle utility API reference