Add JAX typography, xector primitives, deferred effect chains, and GPU streaming
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m28s

- Add JAX text rendering with font atlas, styled text placement, and typography primitives
- Add xector (element-wise/reduction) operations library and sexp effects
- Add deferred effect chain fusion for JIT-compiled effect pipelines
- Expand drawing primitives with font management, alignment, shadow, and outline
- Add interpreter support for function-style define and require
- Add GPU persistence mode and hardware decode support to streaming
- Add new sexp effects: cell_pattern, halftone, mosaic, and derived definitions
- Add path registry for asset resolution
- Add integration, primitives, and xector tests

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
gilesb
2026-02-06 15:12:54 +00:00
parent dbc4ece2cc
commit fc9597456f
30 changed files with 7749 additions and 165 deletions

542
test_funky_text.py Normal file
View File

@@ -0,0 +1,542 @@
#!/usr/bin/env python3
"""
Funky comparison tests: PIL vs TextStrip system.
Tests colors, opacity, fonts, sizes, edge positions, clipping, overlaps, etc.
"""
import numpy as np
import jax.numpy as jnp
from PIL import Image, ImageDraw, ImageFont
from streaming.jax_typography import (
render_text_strip, place_text_strip_jax, _load_font
)
FONTS = {
'sans': '/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf',
'bold': '/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf',
'serif': '/usr/share/fonts/truetype/dejavu/DejaVuSerif.ttf',
'serif_bold': '/usr/share/fonts/truetype/dejavu/DejaVuSerif-Bold.ttf',
'mono': '/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf',
'mono_bold': '/usr/share/fonts/truetype/dejavu/DejaVuSansMono-Bold.ttf',
'narrow': '/usr/share/fonts/truetype/liberation/LiberationSansNarrow-Regular.ttf',
'italic': '/usr/share/fonts/truetype/freefont/FreeSansOblique.ttf',
}
def render_pil(text, x, y, font_path=None, font_size=36, frame_size=(400, 100),
fill=(255, 255, 255), bg=(0, 0, 0), opacity=1.0,
stroke_width=0, stroke_fill=None, anchor="la",
multiline=False, line_spacing=4, align="left"):
"""Render with PIL directly, including color/opacity."""
frame = np.full((frame_size[1], frame_size[0], 3), bg, dtype=np.uint8)
# For opacity, render to RGBA then composite
txt_layer = Image.new('RGBA', frame_size, (0, 0, 0, 0))
draw = ImageDraw.Draw(txt_layer)
font = _load_font(font_path, font_size)
if stroke_fill is None:
stroke_fill = (0, 0, 0)
# PIL fill with alpha for opacity
alpha_int = int(round(opacity * 255))
fill_rgba = (*fill, alpha_int)
stroke_rgba = (*stroke_fill, alpha_int) if stroke_width > 0 else None
if multiline:
draw.multiline_text((x, y), text, fill=fill_rgba, font=font,
stroke_width=stroke_width, stroke_fill=stroke_rgba,
spacing=line_spacing, align=align, anchor=anchor)
else:
draw.text((x, y), text, fill=fill_rgba, font=font,
stroke_width=stroke_width, stroke_fill=stroke_rgba, anchor=anchor)
# Composite onto background
bg_img = Image.fromarray(frame).convert('RGBA')
result = Image.alpha_composite(bg_img, txt_layer)
return np.array(result.convert('RGB'))
def render_strip(text, x, y, font_path=None, font_size=36, frame_size=(400, 100),
fill=(255, 255, 255), bg=(0, 0, 0), opacity=1.0,
stroke_width=0, stroke_fill=None, anchor="la",
multiline=False, line_spacing=4, align="left"):
"""Render with TextStrip system."""
frame = jnp.full((frame_size[1], frame_size[0], 3), jnp.array(bg, dtype=jnp.uint8), dtype=jnp.uint8)
strip = render_text_strip(
text, font_path, font_size,
stroke_width=stroke_width, stroke_fill=stroke_fill,
anchor=anchor, multiline=multiline, line_spacing=line_spacing, align=align
)
strip_img = jnp.asarray(strip.image)
color = jnp.array(fill, dtype=jnp.float32)
result = place_text_strip_jax(
frame, strip_img, x, y,
strip.baseline_y, strip.bearing_x,
color, opacity,
anchor_x=strip.anchor_x, anchor_y=strip.anchor_y,
stroke_width=strip.stroke_width
)
return np.array(result)
def compare(name, tolerance=0, **kwargs):
"""Compare PIL and TextStrip rendering."""
pil = render_pil(**kwargs)
strip = render_strip(**kwargs)
diff = np.abs(pil.astype(np.int16) - strip.astype(np.int16))
max_diff = diff.max()
pixels_diff = (diff > 0).any(axis=2).sum()
if max_diff == 0:
print(f" PASS: {name}")
return True
if tolerance > 0:
best_diff = diff.copy()
for dy in range(-tolerance, tolerance + 1):
for dx in range(-tolerance, tolerance + 1):
if dy == 0 and dx == 0:
continue
shifted = np.roll(np.roll(strip, dy, axis=0), dx, axis=1)
sdiff = np.abs(pil.astype(np.int16) - shifted.astype(np.int16))
best_diff = np.minimum(best_diff, sdiff)
if best_diff.max() == 0:
print(f" PASS: {name} (within {tolerance}px tolerance)")
return True
print(f" FAIL: {name}")
print(f" Max diff: {max_diff}, Pixels different: {pixels_diff}")
Image.fromarray(pil).save(f"/tmp/pil_{name}.png")
Image.fromarray(strip).save(f"/tmp/strip_{name}.png")
diff_vis = np.clip(diff * 10, 0, 255).astype(np.uint8)
Image.fromarray(diff_vis).save(f"/tmp/diff_{name}.png")
print(f" Saved: /tmp/pil_{name}.png /tmp/strip_{name}.png /tmp/diff_{name}.png")
return False
def test_colors():
"""Test various text colors on various backgrounds."""
print("\n--- Colors ---")
results = []
# White on black (baseline)
results.append(compare("color_white_on_black",
text="Hello", x=20, y=30, fill=(255, 255, 255), bg=(0, 0, 0)))
# Red on black
results.append(compare("color_red",
text="Red Text", x=20, y=30, fill=(255, 0, 0), bg=(0, 0, 0)))
# Green on black
results.append(compare("color_green",
text="Green!", x=20, y=30, fill=(0, 255, 0), bg=(0, 0, 0)))
# Blue on black
results.append(compare("color_blue",
text="Blue sky", x=20, y=30, fill=(0, 100, 255), bg=(0, 0, 0)))
# Yellow on dark gray
results.append(compare("color_yellow_on_gray",
text="Yellow", x=20, y=30, fill=(255, 255, 0), bg=(40, 40, 40)))
# Magenta on white
results.append(compare("color_magenta_on_white",
text="Magenta", x=20, y=30, fill=(255, 0, 255), bg=(255, 255, 255)))
# Subtle: gray text on slightly lighter gray
results.append(compare("color_subtle_gray",
text="Subtle", x=20, y=30, fill=(128, 128, 128), bg=(64, 64, 64)))
# Orange on deep blue
results.append(compare("color_orange_on_blue",
text="Warm", x=20, y=30, fill=(255, 165, 0), bg=(0, 0, 80)))
return results
def test_opacity():
"""Test different opacity levels."""
print("\n--- Opacity ---")
results = []
results.append(compare("opacity_100",
text="Full", x=20, y=30, opacity=1.0))
results.append(compare("opacity_75",
text="75%", x=20, y=30, opacity=0.75))
results.append(compare("opacity_50",
text="Half", x=20, y=30, opacity=0.5))
results.append(compare("opacity_25",
text="Quarter", x=20, y=30, opacity=0.25))
results.append(compare("opacity_10",
text="Ghost", x=20, y=30, opacity=0.1))
# Opacity on colored background
results.append(compare("opacity_on_colored_bg",
text="Overlay", x=20, y=30, fill=(255, 255, 255), bg=(100, 0, 0),
opacity=0.5))
# Color + opacity combo
results.append(compare("opacity_red_on_green",
text="Blend", x=20, y=30, fill=(255, 0, 0), bg=(0, 100, 0),
opacity=0.6))
return results
def test_fonts():
"""Test different fonts and sizes."""
print("\n--- Fonts & Sizes ---")
results = []
for label, path in FONTS.items():
results.append(compare(f"font_{label}",
text="Quick Fox", x=20, y=30, font_path=path, font_size=28,
frame_size=(300, 80)))
# Tiny text
results.append(compare("size_tiny",
text="Tiny text at 12px", x=10, y=15, font_size=12,
frame_size=(200, 40)))
# Big text
results.append(compare("size_big",
text="BIG", x=20, y=10, font_size=72,
frame_size=(300, 100)))
# Huge text
results.append(compare("size_huge",
text="XL", x=10, y=10, font_size=120,
frame_size=(300, 160)))
return results
def test_anchors():
"""Test all anchor combinations."""
print("\n--- Anchors ---")
results = []
# All horizontal x vertical combos
for h in ['l', 'm', 'r']:
for v in ['a', 'm', 's', 'd']:
anchor = h + v
results.append(compare(f"anchor_{anchor}",
text="Anchor", x=200, y=50, anchor=anchor,
frame_size=(400, 100)))
return results
def test_strokes():
"""Test various stroke widths and colors."""
print("\n--- Strokes ---")
results = []
for sw in [1, 2, 3, 4, 6, 8]:
results.append(compare(f"stroke_w{sw}",
text="Stroke", x=30, y=20, font_size=40,
stroke_width=sw, stroke_fill=(0, 0, 0),
frame_size=(300, 80)))
# Colored strokes
results.append(compare("stroke_red",
text="Red outline", x=20, y=20, font_size=36,
stroke_width=3, stroke_fill=(255, 0, 0),
frame_size=(350, 80)))
results.append(compare("stroke_white_on_black",
text="Glow", x=20, y=20, font_size=40,
fill=(255, 255, 255), stroke_width=4, stroke_fill=(0, 0, 255),
frame_size=(250, 80)))
# Stroke with bold font
results.append(compare("stroke_bold",
text="Bold+Stroke", x=20, y=20,
font_path=FONTS['bold'], font_size=36,
stroke_width=3, stroke_fill=(0, 0, 0),
frame_size=(400, 80)))
# Stroke + colored text on colored bg
results.append(compare("stroke_colored_on_bg",
text="Party", x=20, y=20, font_size=48,
fill=(255, 255, 0), bg=(50, 0, 80),
stroke_width=3, stroke_fill=(255, 0, 0),
frame_size=(300, 80)))
return results
def test_edge_clipping():
"""Test text at frame edges - clipping behavior."""
print("\n--- Edge Clipping ---")
results = []
# Text at very left edge
results.append(compare("clip_left_edge",
text="LEFT", x=0, y=30, frame_size=(200, 80)))
# Text partially off right edge
results.append(compare("clip_right_edge",
text="RIGHT SIDE", x=150, y=30, frame_size=(200, 80)))
# Text at top edge
results.append(compare("clip_top",
text="TOP", x=20, y=0, frame_size=(200, 80)))
# Text at bottom edge - partially clipped
results.append(compare("clip_bottom",
text="BOTTOM", x=20, y=55, font_size=40,
frame_size=(200, 80)))
# Large text overflowing all sides from center
results.append(compare("clip_overflow_center",
text="HUGE", x=75, y=40, font_size=100, anchor="mm",
frame_size=(150, 80)))
# Corner placement
results.append(compare("clip_corner_br",
text="Corner", x=350, y=70, font_size=36,
frame_size=(400, 100)))
return results
def test_multiline_fancy():
"""Test multiline with various styles."""
print("\n--- Multiline Fancy ---")
results = []
# Right-aligned (1px tolerance: same sub-pixel issue as center alignment)
results.append(compare("multi_right",
text="Right\nAligned\nText", x=380, y=20,
frame_size=(400, 150),
multiline=True, anchor="ra", align="right",
tolerance=1))
# Center + stroke
results.append(compare("multi_center_stroke",
text="Title\nSubtitle", x=200, y=20,
font_size=32, frame_size=(400, 120),
multiline=True, anchor="ma", align="center",
stroke_width=2, stroke_fill=(0, 0, 0),
tolerance=1))
# Wide line spacing
results.append(compare("multi_wide_spacing",
text="Line A\nLine B\nLine C", x=20, y=10,
frame_size=(300, 200),
multiline=True, line_spacing=20))
# Zero extra spacing
results.append(compare("multi_tight_spacing",
text="Tight\nPacked\nLines", x=20, y=10,
frame_size=(300, 150),
multiline=True, line_spacing=0))
# Many lines
results.append(compare("multi_many_lines",
text="One\nTwo\nThree\nFour\nFive\nSix", x=20, y=5,
font_size=20, frame_size=(200, 200),
multiline=True, line_spacing=4))
# Bold multiline with stroke
results.append(compare("multi_bold_stroke",
text="BOLD\nSTROKE", x=20, y=10,
font_path=FONTS['bold'], font_size=48,
stroke_width=3, stroke_fill=(200, 0, 0),
frame_size=(350, 150), multiline=True))
# Multiline on colored bg with opacity
results.append(compare("multi_opacity_on_bg",
text="Semi\nTransparent", x=20, y=10,
fill=(255, 255, 0), bg=(0, 50, 100), opacity=0.7,
frame_size=(300, 120), multiline=True))
return results
def test_special_chars():
"""Test special characters and edge cases."""
print("\n--- Special Characters ---")
results = []
# Numbers and symbols
results.append(compare("chars_numbers",
text="0123456789", x=20, y=30, frame_size=(300, 80)))
results.append(compare("chars_punctuation",
text="Hello, World! (v2.0)", x=10, y=30, frame_size=(350, 80)))
results.append(compare("chars_symbols",
text="@#$%^&*+=", x=20, y=30, frame_size=(300, 80)))
# Single character
results.append(compare("chars_single",
text="X", x=50, y=30, font_size=48, frame_size=(100, 80)))
# Very long text (clipped)
results.append(compare("chars_long",
text="The quick brown fox jumps over the lazy dog", x=10, y=30,
font_size=24, frame_size=(400, 80)))
# Mixed case
results.append(compare("chars_mixed_case",
text="AaBbCcDdEeFf", x=10, y=30, frame_size=(350, 80)))
return results
def test_combos():
"""Complex combinations of features."""
print("\n--- Combos ---")
results = []
# Big bold stroke + color + opacity
results.append(compare("combo_all_features",
text="EPIC", x=20, y=10,
font_path=FONTS['bold'], font_size=64,
fill=(255, 200, 0), bg=(20, 0, 40), opacity=0.85,
stroke_width=4, stroke_fill=(180, 0, 0),
frame_size=(350, 100)))
# Small mono on busy background
results.append(compare("combo_mono_code",
text="fn main() {}", x=10, y=15,
font_path=FONTS['mono'], font_size=16,
fill=(0, 255, 100), bg=(30, 30, 30),
frame_size=(250, 50)))
# Serif italic multiline with stroke
results.append(compare("combo_serif_italic_multi",
text="Once upon\na time...", x=20, y=10,
font_path=FONTS['italic'], font_size=28,
stroke_width=1, stroke_fill=(80, 80, 80),
frame_size=(300, 120), multiline=True))
# Narrow font, big stroke, center anchored
results.append(compare("combo_narrow_stroke_center",
text="NARROW", x=150, y=40,
font_path=FONTS['narrow'], font_size=44,
stroke_width=5, stroke_fill=(0, 0, 0),
anchor="mm", frame_size=(300, 80)))
# Multiple strips on same frame (simulated by sequential placement)
results.append(compare("combo_opacity_blend",
text="Layered", x=20, y=30,
fill=(255, 0, 0), bg=(0, 0, 255), opacity=0.5,
font_size=48, frame_size=(300, 80)))
return results
def test_multi_strip_overlay():
"""Test placing multiple strips on the same frame."""
print("\n--- Multi-Strip Overlay ---")
results = []
frame_size = (400, 150)
bg = (20, 20, 40)
# PIL version - multiple draw calls
font1 = _load_font(FONTS['bold'], 48)
font2 = _load_font(None, 24)
font3 = _load_font(FONTS['mono'], 18)
pil_frame = np.full((frame_size[1], frame_size[0], 3), bg, dtype=np.uint8)
txt_layer = Image.new('RGBA', frame_size, (0, 0, 0, 0))
draw = ImageDraw.Draw(txt_layer)
draw.text((20, 10), "TITLE", fill=(255, 255, 0, 255), font=font1,
stroke_width=2, stroke_fill=(0, 0, 0, 255))
draw.text((20, 70), "Subtitle here", fill=(200, 200, 200, 255), font=font2)
draw.text((20, 110), "code_snippet()", fill=(0, 255, 128, 200), font=font3)
bg_img = Image.fromarray(pil_frame).convert('RGBA')
pil_result = np.array(Image.alpha_composite(bg_img, txt_layer).convert('RGB'))
# Strip version - multiple placements
frame = jnp.full((frame_size[1], frame_size[0], 3), jnp.array(bg, dtype=jnp.uint8), dtype=jnp.uint8)
s1 = render_text_strip("TITLE", FONTS['bold'], 48, stroke_width=2, stroke_fill=(0, 0, 0))
s2 = render_text_strip("Subtitle here", None, 24)
s3 = render_text_strip("code_snippet()", FONTS['mono'], 18)
frame = place_text_strip_jax(
frame, jnp.asarray(s1.image), 20, 10,
s1.baseline_y, s1.bearing_x,
jnp.array([255, 255, 0], dtype=jnp.float32), 1.0,
anchor_x=s1.anchor_x, anchor_y=s1.anchor_y,
stroke_width=s1.stroke_width)
frame = place_text_strip_jax(
frame, jnp.asarray(s2.image), 20, 70,
s2.baseline_y, s2.bearing_x,
jnp.array([200, 200, 200], dtype=jnp.float32), 1.0,
anchor_x=s2.anchor_x, anchor_y=s2.anchor_y,
stroke_width=s2.stroke_width)
frame = place_text_strip_jax(
frame, jnp.asarray(s3.image), 20, 110,
s3.baseline_y, s3.bearing_x,
jnp.array([0, 255, 128], dtype=jnp.float32), 200/255,
anchor_x=s3.anchor_x, anchor_y=s3.anchor_y,
stroke_width=s3.stroke_width)
strip_result = np.array(frame)
diff = np.abs(pil_result.astype(np.int16) - strip_result.astype(np.int16))
max_diff = diff.max()
pixels_diff = (diff > 0).any(axis=2).sum()
if max_diff <= 1:
print(f" PASS: multi_overlay (max_diff={max_diff})")
results.append(True)
else:
print(f" FAIL: multi_overlay")
print(f" Max diff: {max_diff}, Pixels different: {pixels_diff}")
Image.fromarray(pil_result).save("/tmp/pil_multi_overlay.png")
Image.fromarray(strip_result).save("/tmp/strip_multi_overlay.png")
diff_vis = np.clip(diff * 10, 0, 255).astype(np.uint8)
Image.fromarray(diff_vis).save("/tmp/diff_multi_overlay.png")
print(f" Saved: /tmp/pil_multi_overlay.png /tmp/strip_multi_overlay.png /tmp/diff_multi_overlay.png")
results.append(False)
return results
def main():
print("=" * 60)
print("Funky TextStrip vs PIL Comparison")
print("=" * 60)
all_results = []
all_results.extend(test_colors())
all_results.extend(test_opacity())
all_results.extend(test_fonts())
all_results.extend(test_anchors())
all_results.extend(test_strokes())
all_results.extend(test_edge_clipping())
all_results.extend(test_multiline_fancy())
all_results.extend(test_special_chars())
all_results.extend(test_combos())
all_results.extend(test_multi_strip_overlay())
print("\n" + "=" * 60)
passed = sum(all_results)
total = len(all_results)
print(f"Results: {passed}/{total} passed")
if passed == total:
print("ALL TESTS PASSED!")
else:
failed = [i for i, r in enumerate(all_results) if not r]
print(f"FAILED: {total - passed} tests (indices: {failed})")
print("=" * 60)
if __name__ == "__main__":
main()