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
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:
176
test_styled_text.py
Normal file
176
test_styled_text.py
Normal file
@@ -0,0 +1,176 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test styled TextStrip rendering against PIL.
|
||||
"""
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
|
||||
def render_pil(text, x, y, font_size=36, frame_size=(400, 100),
|
||||
stroke_width=0, stroke_fill=None, anchor="la",
|
||||
multiline=False, line_spacing=4, align="left"):
|
||||
"""Render with PIL directly."""
|
||||
frame = np.zeros((frame_size[1], frame_size[0], 3), dtype=np.uint8)
|
||||
img = Image.fromarray(frame)
|
||||
draw = ImageDraw.Draw(img)
|
||||
|
||||
font = _load_font(None, font_size)
|
||||
|
||||
# Default stroke fill
|
||||
if stroke_fill is None:
|
||||
stroke_fill = (0, 0, 0)
|
||||
|
||||
if multiline:
|
||||
draw.multiline_text((x, y), text, fill=(255, 255, 255), font=font,
|
||||
stroke_width=stroke_width, stroke_fill=stroke_fill,
|
||||
spacing=line_spacing, align=align, anchor=anchor)
|
||||
else:
|
||||
draw.text((x, y), text, fill=(255, 255, 255), font=font,
|
||||
stroke_width=stroke_width, stroke_fill=stroke_fill, anchor=anchor)
|
||||
|
||||
return np.array(img)
|
||||
|
||||
|
||||
def render_strip(text, x, y, font_size=36, frame_size=(400, 100),
|
||||
stroke_width=0, stroke_fill=None, anchor="la",
|
||||
multiline=False, line_spacing=4, align="left"):
|
||||
"""Render with TextStrip."""
|
||||
frame = jnp.zeros((frame_size[1], frame_size[0], 3), dtype=jnp.uint8)
|
||||
|
||||
strip = render_text_strip(
|
||||
text, None, 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([255, 255, 255], dtype=jnp.float32)
|
||||
|
||||
result = place_text_strip_jax(
|
||||
frame, strip_img, x, y,
|
||||
strip.baseline_y, strip.bearing_x,
|
||||
color, 1.0,
|
||||
anchor_x=strip.anchor_x, anchor_y=strip.anchor_y,
|
||||
stroke_width=strip.stroke_width
|
||||
)
|
||||
|
||||
return np.array(result)
|
||||
|
||||
|
||||
def compare(name, text, x, y, font_size=36, frame_size=(400, 100),
|
||||
tolerance=0, **kwargs):
|
||||
"""Compare PIL and TextStrip rendering.
|
||||
|
||||
tolerance=0: exact pixel match required
|
||||
tolerance=1: allow 1-pixel position shift (for sub-pixel rendering differences
|
||||
in center-aligned multiline text where the strip is pre-rendered
|
||||
at a different base position than the final placement)
|
||||
"""
|
||||
pil = render_pil(text, x, y, font_size, frame_size, **kwargs)
|
||||
strip = render_strip(text, x, y, font_size, frame_size, **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}")
|
||||
print(f" Max diff: 0, Pixels different: 0")
|
||||
return True
|
||||
|
||||
if tolerance > 0:
|
||||
# Check if the difference is just a sub-pixel position shift:
|
||||
# for each shifted version, compute the minimum diff
|
||||
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)
|
||||
max_shift_diff = best_diff.max()
|
||||
pixels_shift_diff = (best_diff > 0).any(axis=2).sum()
|
||||
if max_shift_diff == 0:
|
||||
print(f"PASS: {name} (within {tolerance}px position tolerance)")
|
||||
print(f" Raw diff: {max_diff}, After shift tolerance: 0")
|
||||
return True
|
||||
|
||||
status = "FAIL"
|
||||
print(f"{status}: {name}")
|
||||
print(f" Max diff: {max_diff}, Pixels different: {pixels_diff}")
|
||||
|
||||
# Save debug images
|
||||
Image.fromarray(pil).save(f"/tmp/pil_{name}.png")
|
||||
Image.fromarray(strip).save(f"/tmp/strip_{name}.png")
|
||||
diff_scaled = np.clip(diff * 10, 0, 255).astype(np.uint8)
|
||||
Image.fromarray(diff_scaled).save(f"/tmp/diff_{name}.png")
|
||||
print(f" Saved: /tmp/pil_{name}.png, /tmp/strip_{name}.png, /tmp/diff_{name}.png")
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def main():
|
||||
print("=" * 60)
|
||||
print("Styled TextStrip vs PIL Comparison")
|
||||
print("=" * 60)
|
||||
|
||||
results = []
|
||||
|
||||
# Basic text
|
||||
results.append(compare("basic", "Hello World", 20, 50))
|
||||
|
||||
# Stroke/outline
|
||||
results.append(compare("stroke_2", "Outlined", 20, 50,
|
||||
stroke_width=2, stroke_fill=(255, 0, 0)))
|
||||
|
||||
results.append(compare("stroke_5", "Big Outline", 30, 60, font_size=48,
|
||||
frame_size=(500, 120),
|
||||
stroke_width=5, stroke_fill=(0, 0, 0)))
|
||||
|
||||
# Anchors - center
|
||||
results.append(compare("anchor_mm", "Center", 200, 50, frame_size=(400, 100),
|
||||
anchor="mm"))
|
||||
|
||||
# Anchors - right
|
||||
results.append(compare("anchor_rm", "Right", 380, 50, frame_size=(400, 100),
|
||||
anchor="rm"))
|
||||
|
||||
# Multiline
|
||||
results.append(compare("multiline", "Line 1\nLine 2\nLine 3", 20, 20,
|
||||
frame_size=(400, 150),
|
||||
multiline=True, line_spacing=8))
|
||||
|
||||
# Multiline centered (1px tolerance: sub-pixel rendering differs because
|
||||
# the strip is pre-rendered at an integer position while PIL's center
|
||||
# alignment uses fractional getlength values for the 'm' anchor shift)
|
||||
results.append(compare("multiline_center", "Short\nMedium Length\nX", 200, 20,
|
||||
frame_size=(400, 150),
|
||||
multiline=True, anchor="ma", align="center",
|
||||
tolerance=1))
|
||||
|
||||
# Stroke + multiline
|
||||
results.append(compare("stroke_multiline", "Line A\nLine B", 20, 20,
|
||||
frame_size=(400, 120),
|
||||
stroke_width=2, stroke_fill=(0, 0, 255),
|
||||
multiline=True))
|
||||
|
||||
print("=" * 60)
|
||||
passed = sum(results)
|
||||
total = len(results)
|
||||
print(f"Results: {passed}/{total} passed")
|
||||
|
||||
if passed == total:
|
||||
print("ALL TESTS PASSED!")
|
||||
else:
|
||||
print(f"FAILED: {total - passed} tests")
|
||||
print("=" * 60)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user