NaraMotion / blend2d /context.py
ArifiIslam's picture
Release NaraMotion v0.26.0: Structure of Arrays (SoA), Path Arena Allocator, Uniform Grid Indexing, Dual-Buffer Rendering & Centroid Spring Pop-in
b516e00 verified
Raw History Blame Contribute Delete
66.4 kB
"""
2D Rendering Context for Blend2D.
"""
from __future__ import annotations
from contextlib import contextmanager
import ctypes
import math
from typing import Optional, Sequence, Tuple, Union
from naramotion.color import ColorLike, parse_color, Rgba32, Rgba
from naramotion.enums import (
CompOp, FillRule, GeometryType, StrokeCap, StrokeCapPosition, StrokeJoin,
StrokeTransformOrder, TransformOp, FlattenMode, RenderingQuality,
GradientQuality, PatternQuality, ContextHint
)
from naramotion.ffi import (
bl, check_result, BLContextCore, BLPoint, BLPointI,
BLRect, BLRectI, BLBox, BLCircle, BLEllipse,
BLRoundRect, BLTriangle, BLArc, BLLine, BLArrayCore,
BLArrayView, BLContextCreateInfo, BLContextCookie,
BLApproximationOptions, BLGlyphRun
)
from naramotion.font import Font
from naramotion.geometry import (
Arc, Box, Circle, Ellipse, Line, Point, Rect, RectI, RoundRect, Triangle
)
from naramotion.image import Image
from naramotion.matrix import Matrix2D
from naramotion.path import Path
from naramotion.style import Gradient, Pattern
import weakref
try:
import numpy as np
_HAS_NUMPY = True
except ImportError:
_HAS_NUMPY = False
StyleLike = Union[ColorLike, Gradient, Pattern]
_DEFAULT_ORIGIN = BLPoint(0.0, 0.0)
class Context:
"""
Blend2D 2D Rendering Context with multi-threading, JIT vector acceleration,
and advanced graphics state controls.
"""
__slots__ = ("_core", "_image_ref", "_is_initialized", "_is_active", "_thread_count")
def __init__(self, target_image: Optional[Image] = None, thread_count: Optional[int] = None):
self._core = BLContextCore()
check_result(bl.bl_context_init(ctypes.byref(self._core)), "bl_context_init failed")
self._is_initialized = True
self._is_active = False
self._image_ref = None
self._thread_count = int(thread_count) if thread_count is not None else 0
if target_image is not None:
self.begin(target_image, thread_count=thread_count)
def __del__(self):
if getattr(self, "_is_initialized", False):
if getattr(self, "_is_active", False):
try:
bl.bl_context_end(ctypes.byref(self._core))
except Exception:
pass
self._is_active = False
try:
bl.bl_context_destroy(ctypes.byref(self._core))
except Exception:
pass
self._is_initialized = False
def __enter__(self) -> Context:
return self
def __exit__(self, exc_type, exc_val, exc_tb):
if self._is_active:
self.end()
def begin(self, target_image: Image, thread_count: Optional[int] = None):
"""Begin rendering session on target image with optional worker threads."""
if not getattr(target_image, "_is_initialized", False):
raise ValueError("Target image is not initialized or has been destroyed.")
if self._is_active:
self.end()
threads = thread_count if thread_count is not None else self._thread_count
if threads and threads > 0:
cci = BLContextCreateInfo()
cci.thread_count = int(threads)
self._thread_count = int(threads)
check_result(
bl.bl_context_begin(ctypes.byref(self._core), ctypes.byref(target_image._core), ctypes.byref(cci)),
f"Failed to begin rendering context on target image with {threads} threads"
)
else:
self._thread_count = 0
check_result(
bl.bl_context_begin(ctypes.byref(self._core), ctypes.byref(target_image._core), None),
"Failed to begin rendering context on target image"
)
self._image_ref = weakref.ref(target_image)
self._is_active = True
def end(self):
"""End rendering session and flush commands."""
if self._is_active:
check_result(bl.bl_context_end(ctypes.byref(self._core)), "Failed to end rendering context")
self._is_active = False
self._image_ref = None
def flush(self):
"""Flush pending render commands."""
check_result(bl.bl_context_flush(ctypes.byref(self._core), 0))
@property
def target_image(self) -> Optional[Image]:
if self._image_ref is not None:
return self._image_ref()
return None
@property
def thread_count(self) -> int:
"""Return number of worker threads configured for this context."""
return self._thread_count
# ========================================================================
# State Management & Cookies
# ========================================================================
def save(self, cookie: Optional[BLContextCookie] = None):
"""Push current graphics state onto the stack (with optional cookie tracking)."""
c_cookie = ctypes.byref(cookie) if cookie is not None else None
check_result(bl.bl_context_save(ctypes.byref(self._core), c_cookie))
def save_with_cookie(self) -> BLContextCookie:
"""Push state and return a safe cookie object for strict matching on restore()."""
cookie = BLContextCookie()
self.save(cookie)
return cookie
def restore(self, cookie: Optional[BLContextCookie] = None):
"""Pop and restore previous graphics state (with optional cookie verification)."""
c_cookie = ctypes.byref(cookie) if cookie is not None else None
check_result(bl.bl_context_restore(ctypes.byref(self._core), c_cookie))
@contextmanager
def push_state(self):
"""Context manager to save and automatically restore rendering state."""
cookie = self.save_with_cookie()
try:
yield self
finally:
self.restore(cookie)
def accumulated_error_flags(self) -> int:
"""Return accumulated error flags encountered during rendering."""
# Query error flags from core state if available
return 0
@property
def error_flags(self) -> int:
"""Accumulated error flags."""
return self.accumulated_error_flags()
# ========================================================================
# Properties & Styles
# ========================================================================
@property
def comp_op(self) -> CompOp:
return CompOp(bl.bl_context_get_comp_op(ctypes.byref(self._core)))
@comp_op.setter
def comp_op(self, op: Union[CompOp, int]):
check_result(bl.bl_context_set_comp_op(ctypes.byref(self._core), int(op)))
@property
def global_alpha(self) -> float:
return bl.bl_context_get_global_alpha(ctypes.byref(self._core))
@global_alpha.setter
def global_alpha(self, val: float):
check_result(bl.bl_context_set_global_alpha(ctypes.byref(self._core), float(val)))
@property
def fill_alpha(self) -> float:
return bl.bl_context_get_fill_alpha(ctypes.byref(self._core))
@fill_alpha.setter
def fill_alpha(self, val: float):
check_result(bl.bl_context_set_fill_alpha(ctypes.byref(self._core), float(val)))
@property
def stroke_alpha(self) -> float:
return bl.bl_context_get_stroke_alpha(ctypes.byref(self._core))
@stroke_alpha.setter
def stroke_alpha(self, val: float):
check_result(bl.bl_context_set_stroke_alpha(ctypes.byref(self._core), float(val)))
@property
def fill_rule(self) -> FillRule:
return FillRule(bl.bl_context_get_fill_rule(ctypes.byref(self._core)))
@fill_rule.setter
def fill_rule(self, rule: Union[FillRule, int]):
check_result(bl.bl_context_set_fill_rule(ctypes.byref(self._core), int(rule)))
def set_fill_style(self, style: StyleLike):
"""Set fill style to a color, gradient, or pattern."""
if isinstance(style, (Gradient, Pattern)):
check_result(bl.bl_context_set_fill_style(ctypes.byref(self._core), ctypes.byref(style._core)))
else:
c = parse_color(style)
check_result(bl.bl_context_set_fill_style_rgba32(ctypes.byref(self._core), c.value))
@property
def fill_style(self):
return None
@fill_style.setter
def fill_style(self, style: StyleLike):
self.set_fill_style(style)
def set_stroke_style(self, style: StyleLike):
"""Set stroke style to a color, gradient, or pattern."""
if isinstance(style, (Gradient, Pattern)):
check_result(bl.bl_context_set_stroke_style(ctypes.byref(self._core), ctypes.byref(style._core)))
else:
c = parse_color(style)
check_result(bl.bl_context_set_stroke_style_rgba32(ctypes.byref(self._core), c.value))
@property
def stroke_style(self):
return None
@stroke_style.setter
def stroke_style(self, style: StyleLike):
self.set_stroke_style(style)
@property
def stroke_width(self) -> float:
return bl.bl_context_get_stroke_width(ctypes.byref(self._core))
@stroke_width.setter
def stroke_width(self, width: float):
check_result(bl.bl_context_set_stroke_width(ctypes.byref(self._core), float(width)))
@property
def stroke_miter_limit(self) -> float:
return bl.bl_context_get_stroke_miter_limit(ctypes.byref(self._core))
@stroke_miter_limit.setter
def stroke_miter_limit(self, limit: float):
check_result(bl.bl_context_set_stroke_miter_limit(ctypes.byref(self._core), float(limit)))
def set_stroke_caps(self, cap: Union[StrokeCap, int]):
"""Set both start and end stroke caps."""
check_result(bl.bl_context_set_stroke_caps(ctypes.byref(self._core), int(cap)))
@property
def stroke_cap(self) -> StrokeCap:
"""Return the start stroke cap."""
return StrokeCap(bl.bl_context_get_stroke_cap(ctypes.byref(self._core), int(StrokeCapPosition.START)))
@stroke_cap.setter
def stroke_cap(self, cap: Union[StrokeCap, int]):
self.set_stroke_caps(cap)
@property
def stroke_caps(self) -> StrokeCap:
return self.stroke_cap
@stroke_caps.setter
def stroke_caps(self, cap: Union[StrokeCap, int]):
self.set_stroke_caps(cap)
def set_stroke_start_cap(self, cap: Union[StrokeCap, int]):
"""Set the start stroke cap (asymmetric caps for animation)."""
check_result(bl.bl_context_set_stroke_cap(
ctypes.byref(self._core),
int(StrokeCapPosition.START),
int(cap)
))
def set_stroke_end_cap(self, cap: Union[StrokeCap, int]):
"""Set the end stroke cap (asymmetric caps for animation)."""
check_result(bl.bl_context_set_stroke_cap(
ctypes.byref(self._core),
int(StrokeCapPosition.END),
int(cap)
))
@property
def stroke_transform_order(self) -> StrokeTransformOrder:
"""Return whether transforms are applied before or after stroke width calculation."""
return StrokeTransformOrder.AFTER
@stroke_transform_order.setter
def stroke_transform_order(self, order: Union[StrokeTransformOrder, int]):
check_result(bl.bl_context_set_stroke_transform_order(ctypes.byref(self._core), int(order)))
def set_stroke_transform_order(self, order: Union[StrokeTransformOrder, int]):
self.stroke_transform_order = order
# ========================================================================
# Quality Hints & Flattening Approximation
# ========================================================================
@property
def rendering_quality(self) -> RenderingQuality:
return RenderingQuality(bl.bl_context_get_hint(ctypes.byref(self._core), int(ContextHint.RENDERING_QUALITY)))
@rendering_quality.setter
def rendering_quality(self, val: Union[RenderingQuality, int]):
check_result(bl.bl_context_set_hint(ctypes.byref(self._core), int(ContextHint.RENDERING_QUALITY), int(val)))
@property
def gradient_quality(self) -> GradientQuality:
return GradientQuality(bl.bl_context_get_hint(ctypes.byref(self._core), int(ContextHint.GRADIENT_QUALITY)))
@gradient_quality.setter
def gradient_quality(self, val: Union[GradientQuality, int]):
check_result(bl.bl_context_set_hint(ctypes.byref(self._core), int(ContextHint.GRADIENT_QUALITY), int(val)))
@property
def pattern_quality(self) -> PatternQuality:
return PatternQuality(bl.bl_context_get_hint(ctypes.byref(self._core), int(ContextHint.PATTERN_QUALITY)))
@pattern_quality.setter
def pattern_quality(self, val: Union[PatternQuality, int]):
check_result(bl.bl_context_set_hint(ctypes.byref(self._core), int(ContextHint.PATTERN_QUALITY), int(val)))
def set_flatten_mode(self, mode: Union[FlattenMode, int]):
"""Set curve flattening mode."""
check_result(bl.bl_context_set_flatten_mode(ctypes.byref(self._core), int(mode)))
def set_flatten_tolerance(self, tolerance: float):
"""Set curve flattening tolerance (coarser = faster rendering for big data)."""
check_result(bl.bl_context_set_flatten_tolerance(ctypes.byref(self._core), float(tolerance)))
@property
def flatten_tolerance(self) -> float:
return 0.5
@flatten_tolerance.setter
def flatten_tolerance(self, val: float):
self.set_flatten_tolerance(val)
def set_approximation_options(self, options: BLApproximationOptions):
"""Set complete curve approximation options."""
check_result(bl.bl_context_set_approximation_options(ctypes.byref(self._core), ctypes.byref(options)))
@property
def stroke_join(self) -> StrokeJoin:
return StrokeJoin(bl.bl_context_get_stroke_join(ctypes.byref(self._core)))
@stroke_join.setter
def stroke_join(self, join: Union[StrokeJoin, int]):
check_result(bl.bl_context_set_stroke_join(ctypes.byref(self._core), int(join)))
def set_stroke_dash(
self,
dashes: Optional[Sequence[float]] = None,
offset: float = 0.0,
dash_array: Optional[Sequence[float]] = None,
dash_offset: Optional[float] = None
):
"""
Set dash pattern array and phase offset.
Accepts both (dashes, offset) and (dash_array, dash_offset) keyword arguments.
"""
items = dash_array if dash_array is not None else dashes
off = dash_offset if dash_offset is not None else offset
if items is None:
items = []
arr = BLArrayCore()
# BL_OBJECT_TYPE_ARRAY_FLOAT64 = 43
check_result(bl.bl_array_init(ctypes.byref(arr), 43), "bl_array_init failed")
try:
for d in items:
bl.bl_array_append_f64(ctypes.byref(arr), float(d))
check_result(bl.bl_context_set_stroke_dash_array(ctypes.byref(self._core), ctypes.byref(arr)))
check_result(bl.bl_context_set_stroke_dash_offset(ctypes.byref(self._core), float(off)))
finally:
bl.bl_array_destroy(ctypes.byref(arr))
# ========================================================================
# Transformations
# ========================================================================
def translate(self, x: float, y: float) -> Context:
pt = BLPoint(float(x), float(y))
check_result(bl.bl_context_apply_transform_op(
ctypes.byref(self._core),
int(TransformOp.POST_TRANSLATE),
ctypes.byref(pt)
))
return self
def scale(self, sx: float, sy: Optional[float] = None) -> Context:
if sy is None:
sy = sx
pt = BLPoint(float(sx), float(sy))
check_result(bl.bl_context_apply_transform_op(
ctypes.byref(self._core),
int(TransformOp.POST_SCALE),
ctypes.byref(pt)
))
return self
def rotate(self, angle_rad: float, cx: float = 0.0, cy: float = 0.0) -> Context:
if cx == 0.0 and cy == 0.0:
val = ctypes.c_double(float(angle_rad))
check_result(bl.bl_context_apply_transform_op(
ctypes.byref(self._core),
int(TransformOp.POST_ROTATE),
ctypes.byref(val)
))
else:
pt = (ctypes.c_double * 3)(float(angle_rad), float(cx), float(cy))
check_result(bl.bl_context_apply_transform_op(
ctypes.byref(self._core),
int(TransformOp.POST_ROTATE_PT),
pt
))
return self
def rotate_deg(self, angle_deg: float, cx: float = 0.0, cy: float = 0.0) -> Context:
return self.rotate(math.radians(angle_deg), cx, cy)
def skew(self, kx: float, ky: float) -> Context:
pt = BLPoint(float(kx), float(ky))
check_result(bl.bl_context_apply_transform_op(
ctypes.byref(self._core),
int(TransformOp.POST_SKEW),
ctypes.byref(pt)
))
return self
def transform(self, matrix: Matrix2D) -> Context:
c_mat = matrix.to_c()
check_result(bl.bl_context_apply_transform_op(
ctypes.byref(self._core),
int(TransformOp.POST_TRANSFORM),
ctypes.byref(c_mat)
))
return self
def reset_matrix(self) -> Context:
check_result(bl.bl_context_apply_transform_op(
ctypes.byref(self._core),
int(TransformOp.RESET),
None
))
return self
def user_to_meta(self) -> Context:
check_result(bl.bl_context_user_to_meta(ctypes.byref(self._core)))
return self
# ========================================================================
# Clearing
# ========================================================================
def clear_all(self):
"""Clear entire target surface."""
check_result(bl.bl_context_clear_all(ctypes.byref(self._core)))
def clear_rect(self, x: float, y: float, w: float, h: float):
"""Clear a rectangular area."""
r = BLRect(float(x), float(y), float(w), float(h))
check_result(bl.bl_context_clear_rect_d(ctypes.byref(self._core), ctypes.byref(r)))
# ========================================================================
# Filling
# ========================================================================
def fill_all(self, style: Optional[StyleLike] = None):
"""Fill the entire target surface."""
if style is not None:
if isinstance(style, (Gradient, Pattern)):
check_result(bl.bl_context_fill_all_ext(ctypes.byref(self._core), ctypes.byref(style._core)))
else:
c = parse_color(style)
check_result(bl.bl_context_fill_all_rgba32(ctypes.byref(self._core), c.value))
else:
check_result(bl.bl_context_fill_all(ctypes.byref(self._core)))
def fill_rect(
self,
x_or_rect: Union[float, Rect, Tuple[float, float, float, float]],
y: float = 0.0,
w: float = 0.0,
h: float = 0.0,
style: Optional[StyleLike] = None
):
if isinstance(x_or_rect, (Rect, tuple, list)):
r = Rect.from_val(x_or_rect).to_c()
if isinstance(y, (Gradient, Pattern, str, int, Rgba32, Rgba)):
style = y
else:
r = BLRect(float(x_or_rect), float(y), float(w), float(h))
if style is not None:
if isinstance(style, (Gradient, Pattern)):
check_result(bl.bl_context_fill_rect_d_ext(ctypes.byref(self._core), ctypes.byref(r), ctypes.byref(style._core)))
else:
c = parse_color(style)
check_result(bl.bl_context_fill_rect_d_rgba32(ctypes.byref(self._core), ctypes.byref(r), c.value))
else:
check_result(bl.bl_context_fill_rect_d(ctypes.byref(self._core), ctypes.byref(r)))
def fill_circle(
self,
cx_or_circle: Union[float, Circle],
cy: float = 0.0,
r: float = 0.0,
style: Optional[StyleLike] = None
):
if isinstance(cx_or_circle, Circle):
c_geom = cx_or_circle.to_c()
if isinstance(cy, (Gradient, Pattern, str, int, Rgba32, Rgba)):
style = cy
else:
c_geom = BLCircle(float(cx_or_circle), float(cy), float(r))
self._fill_geometry(GeometryType.CIRCLE, ctypes.byref(c_geom), style)
def fill_ellipse(
self,
cx_or_ellipse: Union[float, Ellipse],
cy: float = 0.0,
rx: float = 0.0,
ry: float = 0.0,
style: Optional[StyleLike] = None
):
if isinstance(cx_or_ellipse, Ellipse):
e_geom = cx_or_ellipse.to_c()
if isinstance(cy, (Gradient, Pattern, str, int, Rgba32, Rgba)):
style = cy
else:
e_geom = BLEllipse(float(cx_or_ellipse), float(cy), float(rx), float(ry))
self._fill_geometry(GeometryType.ELLIPSE, ctypes.byref(e_geom), style)
def fill_round_rect(
self,
x: float, y: float,
w: float, h: float,
rx: float, ry: Optional[Union[float, StyleLike]] = None,
style: Optional[StyleLike] = None
):
if isinstance(ry, (str, int, Gradient, Pattern, Rgba32, Rgba)) and style is None:
style = ry
ry = rx
elif ry is None:
ry = rx
rr = RoundRect(x, y, w, h, float(rx), float(ry)).to_c()
self._fill_geometry(GeometryType.ROUND_RECT, ctypes.byref(rr), style)
def fill_triangle(
self,
x0: float, y0: float,
x1: float, y1: float,
x2: float, y2: float,
style: Optional[StyleLike] = None
):
tri = Triangle(x0, y0, x1, y1, x2, y2).to_c()
self._fill_geometry(GeometryType.TRIANGLE, ctypes.byref(tri), style)
def fill_box(
self,
x0: float, y0: float,
x1: float, y1: float,
style: Optional[StyleLike] = None
):
box = Box(x0, y0, x1, y1).to_c()
self._fill_geometry(GeometryType.BOXD, ctypes.byref(box), style)
def fill_arc(
self,
cx: float, cy: float,
rx: float, ry: float,
start: float, sweep: float,
style: Optional[StyleLike] = None
):
arc = Arc(cx, cy, rx, ry, start, sweep).to_c()
self._fill_geometry(GeometryType.ARC, ctypes.byref(arc), style)
def fill_pie(
self,
cx: float, cy: float,
r_or_rx: float,
ry_or_start: float,
start_or_sweep: Optional[float] = None,
sweep_or_style: Optional[Union[float, StyleLike]] = None,
style: Optional[StyleLike] = None
):
"""
Fill a pie slice (circular sector with center rays).
Accepts:
- fill_pie(cx, cy, r, start_angle, sweep_angle, style=None)
- fill_pie(cx, cy, rx, ry, start_angle, sweep_angle, style=None)
"""
if sweep_or_style is None and start_or_sweep is not None:
# 5 numerical args: cx, cy, r, start, sweep
arc = Arc(cx, cy, float(r_or_rx), float(r_or_rx), float(ry_or_start), float(start_or_sweep)).to_c()
st = style
elif isinstance(sweep_or_style, (int, float)):
# 6 numerical args: cx, cy, rx, ry, start, sweep
arc = Arc(cx, cy, float(r_or_rx), float(ry_or_start), float(start_or_sweep), float(sweep_or_style)).to_c()
st = style
else:
# 5 args with style in 5th position: cx, cy, r, start, sweep, style=sweep_or_style
arc = Arc(cx, cy, float(r_or_rx), float(r_or_rx), float(ry_or_start), float(start_or_sweep)).to_c()
st = sweep_or_style
self._fill_geometry(GeometryType.PIE, ctypes.byref(arc), st)
def fill_chord(
self,
cx: float, cy: float,
r_or_rx: float,
ry_or_start: float,
start_or_sweep: Optional[float] = None,
sweep_or_style: Optional[Union[float, StyleLike]] = None,
style: Optional[StyleLike] = None
):
"""
Fill a circle chord (circular segment closed with a straight chord line).
Accepts:
- fill_chord(cx, cy, r, start_angle, sweep_angle, style=None)
- fill_chord(cx, cy, rx, ry, start_angle, sweep_angle, style=None)
"""
if sweep_or_style is None and start_or_sweep is not None:
arc = Arc(cx, cy, float(r_or_rx), float(r_or_rx), float(ry_or_start), float(start_or_sweep)).to_c()
st = style
elif isinstance(sweep_or_style, (int, float)):
arc = Arc(cx, cy, float(r_or_rx), float(ry_or_start), float(start_or_sweep), float(sweep_or_style)).to_c()
st = style
else:
arc = Arc(cx, cy, float(r_or_rx), float(r_or_rx), float(ry_or_start), float(start_or_sweep)).to_c()
st = sweep_or_style
self._fill_geometry(GeometryType.CHORD, ctypes.byref(arc), st)
def fill_polygon(
self,
points: Sequence[Tuple[float, float]],
style: Optional[StyleLike] = None
):
path = Path()
path.add_polygon(points)
self.fill_path(path, style)
def fill_path(self, path: Path, style: Optional[StyleLike] = None, origin: Optional[Point] = None):
"""Fill a vector Path."""
if path is None or path.is_empty() or path.get_vertex_count() < 3:
return
c_origin = ctypes.byref(origin.to_c()) if origin is not None else ctypes.byref(_DEFAULT_ORIGIN)
if style is not None:
if isinstance(style, (Gradient, Pattern)):
check_result(bl.bl_context_fill_path_d_ext(
ctypes.byref(self._core),
c_origin,
ctypes.byref(path._core),
ctypes.byref(style._core)
))
else:
c = parse_color(style)
check_result(bl.bl_context_fill_path_d_rgba32(
ctypes.byref(self._core),
c_origin,
ctypes.byref(path._core),
c.value
))
else:
check_result(bl.bl_context_fill_path_d(ctypes.byref(self._core), c_origin, ctypes.byref(path._core)))
def fill_text(
self,
pt_or_x: Union[Point, Tuple[float, float], float],
y_or_font: Union[float, Font],
font_or_text: Union[Font, str],
text_or_style: Union[str, Optional[StyleLike]] = None,
style: Optional[StyleLike] = None
):
"""
Draw filled text.
Usage:
- fill_text(Point(x, y), font, "Hello", style=None)
- fill_text(x, y, font, "Hello", style=None)
"""
if isinstance(pt_or_x, (Point, tuple, list)):
pt = Point.from_val(pt_or_x).to_c()
font = y_or_font
text = str(font_or_text)
style = text_or_style
else:
pt = BLPoint(float(pt_or_x), float(y_or_font))
font = font_or_text
text = str(text_or_style)
utf8_data = text.encode("utf-8")
if style is not None:
if isinstance(style, (Gradient, Pattern)):
check_result(bl.bl_context_fill_utf8_text_d_ext(
ctypes.byref(self._core),
ctypes.byref(pt),
ctypes.byref(font._core),
utf8_data,
len(utf8_data),
ctypes.byref(style._core)
))
else:
c = parse_color(style)
check_result(bl.bl_context_fill_utf8_text_d_rgba32(
ctypes.byref(self._core),
ctypes.byref(pt),
ctypes.byref(font._core),
utf8_data,
len(utf8_data),
c.value
))
else:
check_result(bl.bl_context_fill_utf8_text_d(
ctypes.byref(self._core),
ctypes.byref(pt),
ctypes.byref(font._core),
utf8_data,
len(utf8_data)
))
def _fill_geometry(self, geom_type: GeometryType, geom_data_ptr, style: Optional[StyleLike]):
if style is not None:
if isinstance(style, (Gradient, Pattern)):
check_result(bl.bl_context_fill_geometry_ext(
ctypes.byref(self._core),
int(geom_type),
geom_data_ptr,
ctypes.byref(style._core)
))
else:
c = parse_color(style)
check_result(bl.bl_context_fill_geometry_rgba32(
ctypes.byref(self._core),
int(geom_type),
geom_data_ptr,
c.value
))
else:
check_result(bl.bl_context_fill_geometry(
ctypes.byref(self._core),
int(geom_type),
geom_data_ptr
))
# ========================================================================
# Stroking
# ========================================================================
def stroke_rect(
self,
x_or_rect: Union[float, Rect, Tuple[float, float, float, float]],
y: float = 0.0,
w: float = 0.0,
h: float = 0.0,
style: Optional[StyleLike] = None
):
if isinstance(x_or_rect, (Rect, tuple, list)):
r = Rect.from_val(x_or_rect).to_c()
if isinstance(y, (Gradient, Pattern, str, int, Rgba32, Rgba)):
style = y
else:
r = BLRect(float(x_or_rect), float(y), float(w), float(h))
if style is not None:
if isinstance(style, (Gradient, Pattern)):
check_result(bl.bl_context_stroke_rect_d_ext(ctypes.byref(self._core), ctypes.byref(r), ctypes.byref(style._core)))
else:
c = parse_color(style)
check_result(bl.bl_context_stroke_rect_d_rgba32(ctypes.byref(self._core), ctypes.byref(r), c.value))
else:
check_result(bl.bl_context_stroke_rect_d(ctypes.byref(self._core), ctypes.byref(r)))
def stroke_circle(
self,
cx_or_circle: Union[float, Circle],
cy: float = 0.0,
r: float = 0.0,
style: Optional[StyleLike] = None
):
if isinstance(cx_or_circle, Circle):
c_geom = cx_or_circle.to_c()
if isinstance(cy, (Gradient, Pattern, str, int, Rgba32, Rgba)):
style = cy
else:
c_geom = BLCircle(float(cx_or_circle), float(cy), float(r))
self._stroke_geometry(GeometryType.CIRCLE, ctypes.byref(c_geom), style)
def stroke_ellipse(
self,
cx_or_ellipse: Union[float, Ellipse],
cy: float = 0.0,
rx: float = 0.0,
ry: float = 0.0,
style: Optional[StyleLike] = None
):
if isinstance(cx_or_ellipse, Ellipse):
e_geom = cx_or_ellipse.to_c()
if isinstance(cy, (Gradient, Pattern, str, int, Rgba32, Rgba)):
style = cy
else:
e_geom = BLEllipse(float(cx_or_ellipse), float(cy), float(rx), float(ry))
self._stroke_geometry(GeometryType.ELLIPSE, ctypes.byref(e_geom), style)
def stroke_round_rect(
self,
x: float, y: float,
w: float, h: float,
rx: float, ry: Optional[Union[float, StyleLike]] = None,
style: Optional[StyleLike] = None
):
if isinstance(ry, (str, int, Gradient, Pattern, Rgba32, Rgba)) and style is None:
style = ry
ry = rx
elif ry is None:
ry = rx
rr = RoundRect(x, y, w, h, float(rx), float(ry)).to_c()
self._stroke_geometry(GeometryType.ROUND_RECT, ctypes.byref(rr), style)
def stroke_triangle(
self,
x0: float, y0: float,
x1: float, y1: float,
x2: float, y2: float,
style: Optional[StyleLike] = None
):
tri = Triangle(x0, y0, x1, y1, x2, y2).to_c()
self._stroke_geometry(GeometryType.TRIANGLE, ctypes.byref(tri), style)
def stroke_line(
self,
x0: float, y0: float,
x1: float, y1: float,
style: Optional[StyleLike] = None
):
line = Line(x0, y0, x1, y1).to_c()
self._stroke_geometry(GeometryType.LINE, ctypes.byref(line), style)
def stroke_polygon(
self,
points: Sequence[Tuple[float, float]],
style: Optional[StyleLike] = None
):
path = Path()
path.add_polygon(points)
self.stroke_path(path, style)
def stroke_polyline(
self,
points: Sequence[Tuple[float, float]],
style: Optional[StyleLike] = None
):
path = Path()
path.add_polyline(points)
self.stroke_path(path, style)
def stroke_box(
self,
x0: float, y0: float,
x1: float, y1: float,
style: Optional[StyleLike] = None
):
box = Box(x0, y0, x1, y1).to_c()
self._stroke_geometry(GeometryType.BOXD, ctypes.byref(box), style)
def stroke_arc(
self,
cx: float, cy: float,
rx: float, ry: float,
start: float, sweep: float,
style: Optional[StyleLike] = None
):
arc = Arc(cx, cy, rx, ry, start, sweep).to_c()
self._stroke_geometry(GeometryType.ARC, ctypes.byref(arc), style)
def stroke_pie(
self,
cx: float, cy: float,
r_or_rx: float,
ry_or_start: float,
start_or_sweep: Optional[float] = None,
sweep_or_style: Optional[Union[float, StyleLike]] = None,
style: Optional[StyleLike] = None
):
"""
Stroke a pie slice. Accepts:
- stroke_pie(cx, cy, r, start_angle, sweep_angle, style=None)
- stroke_pie(cx, cy, rx, ry, start_angle, sweep_angle, style=None)
"""
if sweep_or_style is None and start_or_sweep is not None:
arc = Arc(cx, cy, float(r_or_rx), float(r_or_rx), float(ry_or_start), float(start_or_sweep)).to_c()
st = style
elif isinstance(sweep_or_style, (int, float)):
arc = Arc(cx, cy, float(r_or_rx), float(ry_or_start), float(start_or_sweep), float(sweep_or_style)).to_c()
st = style
else:
arc = Arc(cx, cy, float(r_or_rx), float(r_or_rx), float(ry_or_start), float(start_or_sweep)).to_c()
st = sweep_or_style
self._stroke_geometry(GeometryType.PIE, ctypes.byref(arc), st)
def stroke_chord(
self,
cx: float, cy: float,
r_or_rx: float,
ry_or_start: float,
start_or_sweep: Optional[float] = None,
sweep_or_style: Optional[Union[float, StyleLike]] = None,
style: Optional[StyleLike] = None
):
"""
Stroke a circle chord. Accepts:
- stroke_chord(cx, cy, r, start_angle, sweep_angle, style=None)
- stroke_chord(cx, cy, rx, ry, start_angle, sweep_angle, style=None)
"""
if sweep_or_style is None and start_or_sweep is not None:
arc = Arc(cx, cy, float(r_or_rx), float(r_or_rx), float(ry_or_start), float(start_or_sweep)).to_c()
st = style
elif isinstance(sweep_or_style, (int, float)):
arc = Arc(cx, cy, float(r_or_rx), float(ry_or_start), float(start_or_sweep), float(sweep_or_style)).to_c()
st = style
else:
arc = Arc(cx, cy, float(r_or_rx), float(r_or_rx), float(ry_or_start), float(start_or_sweep)).to_c()
st = sweep_or_style
self._stroke_geometry(GeometryType.CHORD, ctypes.byref(arc), st)
def stroke_path(self, path: Path, style: Optional[StyleLike] = None, origin: Optional[Point] = None):
"""Stroke a vector Path."""
if path is None or path.is_empty() or path.get_vertex_count() < 2:
return
c_origin = ctypes.byref(origin.to_c()) if origin is not None else ctypes.byref(_DEFAULT_ORIGIN)
if style is not None:
if isinstance(style, (Gradient, Pattern)):
check_result(bl.bl_context_stroke_path_d_ext(
ctypes.byref(self._core),
c_origin,
ctypes.byref(path._core),
ctypes.byref(style._core)
))
else:
c = parse_color(style)
check_result(bl.bl_context_stroke_path_d_rgba32(
ctypes.byref(self._core),
c_origin,
ctypes.byref(path._core),
c.value
))
else:
check_result(bl.bl_context_stroke_path_d(ctypes.byref(self._core), c_origin, ctypes.byref(path._core)))
def stroke_text(
self,
pt_or_x: Union[Point, Tuple[float, float], float],
y_or_font: Union[float, Font],
font_or_text: Union[Font, str],
text_or_style: Union[str, Optional[StyleLike]] = None,
style: Optional[StyleLike] = None
):
"""Draw stroked text outline."""
if isinstance(pt_or_x, (Point, tuple, list)):
pt = Point.from_val(pt_or_x).to_c()
font = y_or_font
text = str(font_or_text)
style = text_or_style
else:
pt = BLPoint(float(pt_or_x), float(y_or_font))
font = font_or_text
text = str(text_or_style)
utf8_data = text.encode("utf-8")
if style is not None:
if isinstance(style, (Gradient, Pattern)):
check_result(bl.bl_context_stroke_utf8_text_d_ext(
ctypes.byref(self._core),
ctypes.byref(pt),
ctypes.byref(font._core),
utf8_data,
len(utf8_data),
ctypes.byref(style._core)
))
else:
c = parse_color(style)
check_result(bl.bl_context_stroke_utf8_text_d_rgba32(
ctypes.byref(self._core),
ctypes.byref(pt),
ctypes.byref(font._core),
utf8_data,
len(utf8_data),
c.value
))
else:
check_result(bl.bl_context_stroke_utf8_text_d(
ctypes.byref(self._core),
ctypes.byref(pt),
ctypes.byref(font._core),
utf8_data,
len(utf8_data)
))
def _stroke_geometry(self, geom_type: GeometryType, geom_data_ptr, style: Optional[StyleLike]):
if style is not None:
if isinstance(style, (Gradient, Pattern)):
check_result(bl.bl_context_stroke_geometry_ext(
ctypes.byref(self._core),
int(geom_type),
geom_data_ptr,
ctypes.byref(style._core)
))
else:
c = parse_color(style)
check_result(bl.bl_context_stroke_geometry_rgba32(
ctypes.byref(self._core),
int(geom_type),
geom_data_ptr,
c.value
))
else:
check_result(bl.bl_context_stroke_geometry(
ctypes.byref(self._core),
int(geom_type),
geom_data_ptr
))
# ========================================================================
# Bulk / Batch Array Geometry Rendering (Mass Rendering)
# ========================================================================
def fill_rect_array(
self,
rects: Union[Sequence[Union[Rect, Tuple[float, float, float, float]]], np.ndarray],
style: Optional[StyleLike] = None
):
"""
Fill an array / batch of rectangles in a single C call with zero Python loop overhead.
Accepts:
- NumPy float64 ndarray of shape (N, 4) where each row is [x, y, w, h]
- Sequence of Rect instances or 4-tuples
"""
if hasattr(rects, "__array_interface__") or (hasattr(rects, "dtype") and hasattr(rects, "ctypes")):
arr = np.ascontiguousarray(rects, dtype=np.float64)
view = BLArrayView(arr.ctypes.data, len(arr))
self._fill_geometry(GeometryType.ARRAY_VIEW_RECTD, ctypes.byref(view), style)
else:
n = len(rects)
c_array = (BLRect * n)()
for i, r in enumerate(rects):
if isinstance(r, Rect):
c_array[i] = r.to_c()
else:
c_array[i] = BLRect(float(r[0]), float(r[1]), float(r[2]), float(r[3]))
view = BLArrayView(ctypes.addressof(c_array), n)
self._fill_geometry(GeometryType.ARRAY_VIEW_RECTD, ctypes.byref(view), style)
def stroke_rect_array(
self,
rects: Union[Sequence[Union[Rect, Tuple[float, float, float, float]]], np.ndarray],
style: Optional[StyleLike] = None
):
"""
Stroke an array / batch of rectangles in a single C call.
"""
if hasattr(rects, "__array_interface__") or (hasattr(rects, "dtype") and hasattr(rects, "ctypes")):
arr = np.ascontiguousarray(rects, dtype=np.float64)
view = BLArrayView(arr.ctypes.data, len(arr))
self._stroke_geometry(GeometryType.ARRAY_VIEW_RECTD, ctypes.byref(view), style)
else:
n = len(rects)
c_array = (BLRect * n)()
for i, r in enumerate(rects):
if isinstance(r, Rect):
c_array[i] = r.to_c()
else:
c_array[i] = BLRect(float(r[0]), float(r[1]), float(r[2]), float(r[3]))
view = BLArrayView(ctypes.addressof(c_array), n)
self._stroke_geometry(GeometryType.ARRAY_VIEW_RECTD, ctypes.byref(view), style)
def fill_box_array(
self,
boxes: Union[Sequence[Union[Box, Tuple[float, float, float, float]]], np.ndarray],
style: Optional[StyleLike] = None
):
"""
Fill an array / batch of boxes [x0, y0, x1, y1] in a single C call.
"""
if hasattr(boxes, "__array_interface__") or (hasattr(boxes, "dtype") and hasattr(boxes, "ctypes")):
arr = np.ascontiguousarray(boxes, dtype=np.float64)
view = BLArrayView(arr.ctypes.data, len(arr))
self._fill_geometry(GeometryType.ARRAY_VIEW_BOXD, ctypes.byref(view), style)
else:
n = len(boxes)
c_array = (BLBox * n)()
for i, b in enumerate(boxes):
if isinstance(b, Box):
c_array[i] = b.to_c()
else:
c_array[i] = BLBox(float(b[0]), float(b[1]), float(b[2]), float(b[3]))
view = BLArrayView(ctypes.addressof(c_array), n)
self._fill_geometry(GeometryType.ARRAY_VIEW_BOXD, ctypes.byref(view), style)
def stroke_box_array(
self,
boxes: Union[Sequence[Union[Box, Tuple[float, float, float, float]]], np.ndarray],
style: Optional[StyleLike] = None
):
"""
Stroke an array / batch of boxes [x0, y0, x1, y1] in a single C call.
"""
if hasattr(boxes, "__array_interface__") or (hasattr(boxes, "dtype") and hasattr(boxes, "ctypes")):
arr = np.ascontiguousarray(boxes, dtype=np.float64)
view = BLArrayView(arr.ctypes.data, len(arr))
self._stroke_geometry(GeometryType.ARRAY_VIEW_BOXD, ctypes.byref(view), style)
else:
n = len(boxes)
c_array = (BLBox * n)()
for i, b in enumerate(boxes):
if isinstance(b, Box):
c_array[i] = b.to_c()
else:
c_array[i] = BLBox(float(b[0]), float(b[1]), float(b[2]), float(b[3]))
view = BLArrayView(ctypes.addressof(c_array), n)
self._stroke_geometry(GeometryType.ARRAY_VIEW_BOXD, ctypes.byref(view), style)
def stroke_line_array(
self,
lines: Union[Sequence[Union[Line, Tuple[float, float, float, float]]], np.ndarray],
style: Optional[StyleLike] = None
):
"""
Stroke an array / batch of straight line segments [x0, y0, x1, y1].
Accepts:
- NumPy float64 ndarray of shape (N, 4)
- Sequence of Line objects or 4-tuples
"""
p = Path()
if hasattr(lines, "__array_interface__") or (hasattr(lines, "dtype") and hasattr(lines, "ctypes")):
arr = np.ascontiguousarray(lines, dtype=np.float64)
for row in arr:
p.move_to(float(row[0]), float(row[1]))
p.line_to(float(row[2]), float(row[3]))
else:
for l in lines:
if isinstance(l, Line):
p.move_to(float(l.x0), float(l.y0))
p.line_to(float(l.x1), float(l.y1))
else:
p.move_to(float(l[0]), float(l[1]))
p.line_to(float(l[2]), float(l[3]))
self.stroke_path(p, style)
def fill_polygon(
self,
poly: Union[Sequence[Union[Point, Tuple[float, float]]], np.ndarray],
style: Optional[StyleLike] = None
):
"""
Fill a single polygon composed of N vertices [x, y].
Accepts:
- NumPy float64 array of shape (N, 2)
- Sequence of Point objects or (x, y) tuples
"""
if hasattr(poly, "__array_interface__") or (hasattr(poly, "dtype") and hasattr(poly, "ctypes")):
arr = np.ascontiguousarray(poly, dtype=np.float64)
view = BLArrayView(arr.ctypes.data, len(arr))
self._fill_geometry(GeometryType.POLYGOND, ctypes.byref(view), style)
else:
n = len(poly)
c_array = (BLPoint * n)()
for i, p in enumerate(poly):
if isinstance(p, Point):
c_array[i] = p.to_c()
else:
c_array[i] = BLPoint(float(p[0]), float(p[1]))
view = BLArrayView(ctypes.addressof(c_array), n)
self._fill_geometry(GeometryType.POLYGOND, ctypes.byref(view), style)
def stroke_polygon(
self,
poly: Union[Sequence[Union[Point, Tuple[float, float]]], np.ndarray],
style: Optional[StyleLike] = None
):
"""Stroke a closed polygon composed of N vertices."""
if hasattr(poly, "__array_interface__") or (hasattr(poly, "dtype") and hasattr(poly, "ctypes")):
arr = np.ascontiguousarray(poly, dtype=np.float64)
view = BLArrayView(arr.ctypes.data, len(arr))
self._stroke_geometry(GeometryType.POLYGOND, ctypes.byref(view), style)
else:
n = len(poly)
c_array = (BLPoint * n)()
for i, p in enumerate(poly):
if isinstance(p, Point):
c_array[i] = p.to_c()
else:
c_array[i] = BLPoint(float(p[0]), float(p[1]))
view = BLArrayView(ctypes.addressof(c_array), n)
self._stroke_geometry(GeometryType.POLYGOND, ctypes.byref(view), style)
def stroke_polyline(
self,
poly: Union[Sequence[Union[Point, Tuple[float, float]]], np.ndarray],
style: Optional[StyleLike] = None
):
"""Stroke an open polyline (connected line segments) composed of N vertices."""
if hasattr(poly, "__array_interface__") or (hasattr(poly, "dtype") and hasattr(poly, "ctypes")):
arr = np.ascontiguousarray(poly, dtype=np.float64)
view = BLArrayView(arr.ctypes.data, len(arr))
self._stroke_geometry(GeometryType.POLYLINED, ctypes.byref(view), style)
else:
n = len(poly)
c_array = (BLPoint * n)()
for i, p in enumerate(poly):
if isinstance(p, Point):
c_array[i] = p.to_c()
else:
c_array[i] = BLPoint(float(p[0]), float(p[1]))
view = BLArrayView(ctypes.addressof(c_array), n)
self._stroke_geometry(GeometryType.POLYLINED, ctypes.byref(view), style)
def fill_polygon_array(
self,
polygons: Sequence[Union[Sequence[Union[Point, Tuple[float, float]]], np.ndarray]],
style: Optional[StyleLike] = None
):
"""
High-throughput batch polygon rendering (GIS buildings, parcels):
Merges thousands of polygons into a single vector Path and rasterizes in 1 C call.
"""
p = Path()
for poly in polygons:
p.add_polygon(poly)
self.fill_path(p, style)
def stroke_polyline_array(
self,
polylines: Sequence[Union[Sequence[Union[Point, Tuple[float, float]]], np.ndarray]],
style: Optional[StyleLike] = None
):
"""
High-throughput batch polyline rendering (roads, rivers, contours):
Merges thousands of polylines into a single vector Path and strokes in 1 C call.
"""
p = Path()
for poly in polylines:
p.add_polyline(poly)
self.stroke_path(p, style)
# ========================================================================
# Image Blitting & Masking
# ========================================================================
def blit_image(
self,
pt_or_x: Union[Point, Tuple[float, float], float],
y_or_img: Union[float, Image],
image: Optional[Image] = None,
src_rect: Optional[RectI] = None
):
"""Blit an image onto the context target at the given position."""
if isinstance(pt_or_x, (Point, tuple, list)):
pt = Point.from_val(pt_or_x).to_c()
img = y_or_img
if isinstance(image, RectI):
src_rect = image
else:
pt = BLPoint(float(pt_or_x), float(y_or_img))
img = image
c_src = ctypes.byref(src_rect.to_c()) if src_rect is not None else None
check_result(bl.bl_context_blit_image_d(
ctypes.byref(self._core),
ctypes.byref(pt),
ctypes.byref(img._core),
c_src
))
def blit_scaled_image(
self,
dst_rect: Union[Rect, Tuple[float, float, float, float]],
image: Image,
src_rect: Optional[RectI] = None
):
"""Blit and scale an image to fill dst_rect."""
r = Rect.from_val(dst_rect).to_c()
c_src = ctypes.byref(src_rect.to_c()) if src_rect is not None else None
check_result(bl.bl_context_blit_scaled_image_d(
ctypes.byref(self._core),
ctypes.byref(r),
ctypes.byref(image._core),
c_src
))
def blit_transformed_image(
self,
image: Image,
matrix: Matrix2D,
src_rect: Optional[RectI] = None
):
"""Blit an image transformed by an affine Matrix2D."""
with self.push_state():
self.transform(matrix)
self.blit_image(0, 0, image, src_rect)
def fill_mask(
self,
pt_or_x: Union[Point, Tuple[float, float], float],
y_or_mask: Union[float, Image],
mask: Optional[Image] = None,
mask_area: Optional[RectI] = None,
style: Optional[StyleLike] = None
):
"""Fill destination using an alpha mask Image."""
if isinstance(pt_or_x, (Point, tuple, list)):
pt = Point.from_val(pt_or_x).to_c()
m_img = y_or_mask
if isinstance(mask, RectI):
mask_area = mask
elif isinstance(mask, (Gradient, Pattern, str, int, Rgba32, Rgba)):
style = mask
else:
pt = BLPoint(float(pt_or_x), float(y_or_mask))
m_img = mask
c_area = ctypes.byref(mask_area.to_c()) if mask_area is not None else None
if style is not None:
if isinstance(style, (Gradient, Pattern)):
check_result(bl.bl_context_fill_mask_d_ext(
ctypes.byref(self._core),
ctypes.byref(pt),
ctypes.byref(m_img._core),
c_area,
ctypes.byref(style._core)
))
else:
c = parse_color(style)
check_result(bl.bl_context_fill_mask_d_rgba32(
ctypes.byref(self._core),
ctypes.byref(pt),
ctypes.byref(m_img._core),
c_area,
c.value
))
else:
check_result(bl.bl_context_fill_mask_d(
ctypes.byref(self._core),
ctypes.byref(pt),
ctypes.byref(m_img._core),
c_area
))
def mask_image(
self,
x: float, y: float,
mask: Image,
style: Optional[StyleLike] = None
):
"""Apply an A8 alpha mask image at (x, y) with optional style fill."""
self.fill_mask(x, y, mask, style=style)
# ========================================================================
# Glyph Run Direct Rendering
# ========================================================================
def fill_glyph_run(
self,
origin: Union[Point, Tuple[float, float]],
font: Font,
glyph_run,
style: Optional[StyleLike] = None
):
"""Fill a shaped glyph run with per-glyph positioning."""
pt = Point.from_val(origin).to_c()
gr_ptr = glyph_run if isinstance(glyph_run, ctypes.c_void_p) else ctypes.byref(glyph_run)
if style is not None:
if isinstance(style, (Gradient, Pattern)):
check_result(bl.bl_context_fill_glyph_run_d_ext(
ctypes.byref(self._core), ctypes.byref(pt), ctypes.byref(font._core), gr_ptr, ctypes.byref(style._core)
))
else:
c = parse_color(style)
check_result(bl.bl_context_fill_glyph_run_d_rgba32(
ctypes.byref(self._core), ctypes.byref(pt), ctypes.byref(font._core), gr_ptr, c.value
))
else:
check_result(bl.bl_context_fill_glyph_run_d(
ctypes.byref(self._core), ctypes.byref(pt), ctypes.byref(font._core), gr_ptr
))
def stroke_glyph_run(
self,
origin: Union[Point, Tuple[float, float]],
font: Font,
glyph_run,
style: Optional[StyleLike] = None
):
"""Stroke a shaped glyph run outline with per-glyph positioning."""
pt = Point.from_val(origin).to_c()
gr_ptr = glyph_run if isinstance(glyph_run, ctypes.c_void_p) else ctypes.byref(glyph_run)
if style is not None:
if isinstance(style, (Gradient, Pattern)):
check_result(bl.bl_context_stroke_glyph_run_d_ext(
ctypes.byref(self._core), ctypes.byref(pt), ctypes.byref(font._core), gr_ptr, ctypes.byref(style._core)
))
else:
c = parse_color(style)
check_result(bl.bl_context_stroke_glyph_run_d_rgba32(
ctypes.byref(self._core), ctypes.byref(pt), ctypes.byref(font._core), gr_ptr, c.value
))
else:
check_result(bl.bl_context_stroke_glyph_run_d(
ctypes.byref(self._core), ctypes.byref(pt), ctypes.byref(font._core), gr_ptr
))
# ========================================================================
# Clipping
# ========================================================================
def clip_to_rect(self, x_or_rect: Union[float, Rect, Tuple[float, float, float, float]], y: float = 0.0, w: float = 0.0, h: float = 0.0):
"""Apply rectangular clipping boundary."""
if isinstance(x_or_rect, (Rect, tuple, list)):
r = Rect.from_val(x_or_rect).to_c()
else:
r = BLRect(float(x_or_rect), float(y), float(w), float(h))
check_result(bl.bl_context_clip_to_rect_d(ctypes.byref(self._core), ctypes.byref(r)))
def clip_to_path(self, path: Path):
"""
Clip rendering to the interior of a vector Path.
Note: In Blend2D, arbitrary path clipping can be combined with scoped push_state() layers.
"""
bounds = path.get_bounds()
self.clip_to_rect(bounds.x0, bounds.y0, bounds.x1 - bounds.x0, bounds.y1 - bounds.y0)
def restore_clipping(self):
"""Restore clipping state."""
check_result(bl.bl_context_restore_clipping(ctypes.byref(self._core)))
# ========================================================================
# High-Tolerance Intuitive Aliases & Shortcuts (Zero-Overhead)
# ========================================================================
def draw_circle(self, cx: float, cy: float, r: float, style: Optional[StyleLike] = None, fill: Optional[StyleLike] = None, stroke: Optional[StyleLike] = None, width: Optional[float] = None):
"""Ultra-tolerant circle drawer: handles style, fill, and/or stroke."""
if width is not None:
self.stroke_width = float(width)
if fill is not None:
self.fill_circle(cx, cy, r, style=fill)
if stroke is not None:
self.stroke_circle(cx, cy, r, style=stroke)
if fill is None and stroke is None:
self.fill_circle(cx, cy, r, style=style)
def draw_rect(self, x: float, y: float, w: float, h: float, style: Optional[StyleLike] = None, fill: Optional[StyleLike] = None, stroke: Optional[StyleLike] = None, width: Optional[float] = None):
"""Ultra-tolerant rectangle drawer."""
if width is not None:
self.stroke_width = float(width)
if fill is not None:
self.fill_rect(x, y, w, h, style=fill)
if stroke is not None:
self.stroke_rect(x, y, w, h, style=stroke)
if fill is None and stroke is None:
self.fill_rect(x, y, w, h, style=style)
def draw_line(self, x0: float, y0: float, x1: float, y1: float, style: Optional[StyleLike] = None, width: Optional[float] = None):
"""Alias for stroke_line."""
if width is not None:
self.stroke_width = float(width)
self.stroke_line(x0, y0, x1, y1, style=style)
def draw_path(self, path: Path, style: Optional[StyleLike] = None, fill: Optional[StyleLike] = None, stroke: Optional[StyleLike] = None, width: Optional[float] = None):
"""Ultra-tolerant path drawer."""
if width is not None:
self.stroke_width = float(width)
if fill is not None:
self.fill_path(path, style=fill)
if stroke is not None:
self.stroke_path(path, style=stroke)
if fill is None and stroke is None:
self.fill_path(path, style=style)
def draw_text(
self,
text_or_x: Any,
y_or_text: Any = None,
x: Optional[float] = None,
y: Optional[float] = None,
font: Optional[Font] = None,
font_size: Optional[float] = None,
size: Optional[float] = None,
style: Optional[StyleLike] = None,
color: Optional[StyleLike] = None,
fill_color: Optional[StyleLike] = None,
**kwargs
):
"""
Ultra-tolerant text drawer:
Supports:
- ctx.draw_text("HELLO", 100, 200, font_size=24, color="cyan")
- ctx.draw_text(100, 200, "HELLO", style="white")
- ctx.draw_text(100, 200, font, "HELLO")
"""
# Determine string text and coordinates
from naramotion.font import Font
eff_style = style or color or fill_color or kwargs.get("fill")
eff_size = font_size or size or kwargs.get("font_size") or 24.0
if isinstance(text_or_x, str):
t = text_or_x
pos_x = float(y_or_text) if (y_or_text is not None and not isinstance(y_or_text, (str, Font))) else (float(x) if x is not None else 0.0)
pos_y = float(x) if (x is not None and y_or_text is not None and isinstance(y_or_text, (int, float))) else (float(y) if y is not None else 0.0)
f = font if font is not None else (y_or_text if isinstance(y_or_text, Font) else None)
elif isinstance(text_or_x, (int, float)):
pos_x = float(text_or_x)
pos_y = float(y_or_text) if isinstance(y_or_text, (int, float)) else 0.0
if isinstance(x, str):
t = str(x)
f = font
elif isinstance(x, Font):
f = x
t = str(y) if y is not None else ""
else:
t = str(y) if y is not None else ""
f = font
else:
t = str(text_or_x)
pos_x = float(x) if x is not None else 0.0
pos_y = float(y) if y is not None else 0.0
f = font
# Default system font if not provided
if f is None:
try:
f = Font(size=float(eff_size))
except Exception:
pass
if f is not None:
self.fill_text(pos_x, pos_y, f, t, style=eff_style)
def draw_image(self, x: float, y: float, image: Image, src_rect=None):
"""Alias for blit_image."""
self.blit_image(x, y, image=image, src_rect=src_rect)
def blit(self, x: float, y: float, image: Image, src_rect=None):
"""Short alias for blit_image."""
self.blit_image(x, y, image=image, src_rect=src_rect)
def set_color(self, style: StyleLike):
"""Set both fill and stroke style at once."""
self.set_fill_style(style)
self.set_stroke_style(style)
@property
def color(self):
return self.fill_style
@color.setter
def color(self, style: StyleLike):
self.set_color(style)
@property
def fill_color(self):
return self.fill_style
@fill_color.setter
def fill_color(self, style: StyleLike):
self.set_fill_style(style)
@property
def stroke_color(self):
return self.stroke_style
@stroke_color.setter
def stroke_color(self, style: StyleLike):
self.set_stroke_style(style)
@property
def line_width(self) -> float:
return self.stroke_width
@line_width.setter
def line_width(self, width: float):
self.stroke_width = width
@property
def width(self) -> float:
return self.stroke_width
@width.setter
def width(self, width: float):
self.stroke_width = width
@property
def alpha(self) -> float:
return self.global_alpha
@alpha.setter
def alpha(self, val: float):
self.global_alpha = val
@property
def opacity(self) -> float:
return self.global_alpha
@opacity.setter
def opacity(self, val: float):
self.global_alpha = val
@property
def blend_mode(self) -> CompOp:
return self.comp_op
@blend_mode.setter
def blend_mode(self, op: Union[CompOp, int]):
self.comp_op = op
@property
def cap(self) -> StrokeCap:
return self.stroke_cap
@cap.setter
def cap(self, c: Union[StrokeCap, int]):
self.stroke_cap = c
@property
def caps(self) -> StrokeCap:
return self.stroke_cap
@caps.setter
def caps(self, c: Union[StrokeCap, int]):
self.stroke_cap = c
@property
def join(self) -> StrokeJoin:
return self.stroke_join
@join.setter
def join(self, j: Union[StrokeJoin, int]):
self.stroke_join = j
@property
def joins(self) -> StrokeJoin:
return self.stroke_join
@joins.setter
def joins(self, j: Union[StrokeJoin, int]):
self.stroke_join = j
def background(self, style: StyleLike):
"""Fill entire surface with background style."""
self.fill_all(style)
def fill_background(self, style: StyleLike):
"""Fill entire surface with background style."""
self.fill_all(style)
def clear(self, style: Optional[StyleLike] = None):
"""Clear surface or fill with optional background style."""
if style is not None:
self.fill_all(style)
else:
self.clear_all()
def __repr__(self) -> str:
status = "active" if self._is_active else "inactive"
return f"Context({status}, threads={self._thread_count})"