alphapil 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- alphapil/__init__.py +24 -0
- alphapil/engine.py +259 -0
- alphapil/interpreter.py +342 -0
- alphapil/modules/__init__.py +15 -0
- alphapil/modules/base.py +278 -0
- alphapil/modules/effects.py +170 -0
- alphapil/modules/images.py +323 -0
- alphapil/modules/masking.py +214 -0
- alphapil/modules/shapes.py +250 -0
- alphapil/modules/text.py +527 -0
- alphapil/modules/utils.py +304 -0
- alphapil-0.1.0.dist-info/METADATA +115 -0
- alphapil-0.1.0.dist-info/RECORD +15 -0
- alphapil-0.1.0.dist-info/WHEEL +5 -0
- alphapil-0.1.0.dist-info/top_level.txt +1 -0
alphapil/__init__.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AlphaPIL - An asynchronous, template-based image generation engine.
|
|
3
|
+
|
|
4
|
+
This package provides a powerful recursive parser for handling nested functions
|
|
5
|
+
in image generation templates, with support for asynchronous operations.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
__version__ = "0.1.0"
|
|
9
|
+
__author__ = "AlphaPIL Team"
|
|
10
|
+
|
|
11
|
+
from .engine import CanvasEngine
|
|
12
|
+
from .interpreter import CanvasInterpreter
|
|
13
|
+
from .modules import AlphaMixin, ShapesMixin, TextMixin, ImagesMixin, UtilsMixin, MaskingMixin
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"CanvasEngine",
|
|
17
|
+
"CanvasInterpreter",
|
|
18
|
+
"AlphaMixin",
|
|
19
|
+
"ShapesMixin",
|
|
20
|
+
"TextMixin",
|
|
21
|
+
"ImagesMixin",
|
|
22
|
+
"UtilsMixin",
|
|
23
|
+
"MaskingMixin"
|
|
24
|
+
]
|
alphapil/engine.py
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CanvasEngine - High-level canvas operations for AlphaPIL.
|
|
3
|
+
|
|
4
|
+
This module provides the CanvasEngine class that inherits from CanvasInterpreter
|
|
5
|
+
and all module mixins to provide comprehensive image generation capabilities.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import io
|
|
9
|
+
from typing import Tuple, Union, Optional
|
|
10
|
+
from PIL import Image, ImageDraw, ImageFont
|
|
11
|
+
from .interpreter import CanvasInterpreter
|
|
12
|
+
from .modules import AlphaMixin, ShapesMixin, TextMixin, ImagesMixin, UtilsMixin, MaskingMixin
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class CanvasEngine(CanvasInterpreter, AlphaMixin, ShapesMixin, TextMixin, ImagesMixin, UtilsMixin, MaskingMixin):
|
|
16
|
+
"""
|
|
17
|
+
High-level canvas engine that extends CanvasInterpreter with Pillow-based
|
|
18
|
+
image generation capabilities and all module mixins.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(self):
|
|
22
|
+
"""Initialize the canvas engine and register built-in functions."""
|
|
23
|
+
# Initialize all parent classes properly
|
|
24
|
+
CanvasInterpreter.__init__(self)
|
|
25
|
+
AlphaMixin.__init__(self)
|
|
26
|
+
ShapesMixin.__init__(self)
|
|
27
|
+
TextMixin.__init__(self)
|
|
28
|
+
ImagesMixin.__init__(self)
|
|
29
|
+
UtilsMixin.__init__(self)
|
|
30
|
+
MaskingMixin.__init__(self)
|
|
31
|
+
|
|
32
|
+
# Initialize mixin states
|
|
33
|
+
self._init_state()
|
|
34
|
+
self._init_text()
|
|
35
|
+
|
|
36
|
+
# Initialize canvas-specific attributes
|
|
37
|
+
self.canvas: Optional[Image.Image] = None
|
|
38
|
+
self.draw: Optional[ImageDraw.Draw] = None
|
|
39
|
+
self.canvas_size: Tuple[int, int] = (0, 0)
|
|
40
|
+
|
|
41
|
+
# Register all built-in functions from modules
|
|
42
|
+
self._register_builtin_functions()
|
|
43
|
+
|
|
44
|
+
async def render_template(self, template_text: str, data: dict = None) -> bytes:
|
|
45
|
+
"""
|
|
46
|
+
Render a template with optional data injection (Async).
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
template_text: Template content as string
|
|
50
|
+
data: Optional dictionary of variables to set
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
Canvas image as bytes
|
|
54
|
+
|
|
55
|
+
Raises:
|
|
56
|
+
RuntimeError: If template rendering fails
|
|
57
|
+
"""
|
|
58
|
+
try:
|
|
59
|
+
# Reset engine state
|
|
60
|
+
self.reset()
|
|
61
|
+
|
|
62
|
+
# Inject data if provided
|
|
63
|
+
if data:
|
|
64
|
+
for key, value in data.items():
|
|
65
|
+
self.set_variable(key, str(value))
|
|
66
|
+
|
|
67
|
+
# Parse template line by line
|
|
68
|
+
lines = template_text.strip().split('\n')
|
|
69
|
+
for line_num, line in enumerate(lines, 1):
|
|
70
|
+
line = line.strip()
|
|
71
|
+
|
|
72
|
+
# Skip comments and empty lines
|
|
73
|
+
if not line or line.startswith('#'):
|
|
74
|
+
continue
|
|
75
|
+
|
|
76
|
+
# Parse and execute the line (awaiting async parse)
|
|
77
|
+
result = await self.parse(line)
|
|
78
|
+
|
|
79
|
+
# Return canvas as bytes
|
|
80
|
+
return self.get_canvas_bytes()
|
|
81
|
+
|
|
82
|
+
except Exception as e:
|
|
83
|
+
raise RuntimeError(f"Template rendering failed: {e}")
|
|
84
|
+
|
|
85
|
+
async def render_template_file(self, template_path: str, data: dict = None) -> bytes:
|
|
86
|
+
"""
|
|
87
|
+
Render a template from file with optional data injection (Async).
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
template_path: Path to template file
|
|
91
|
+
data: Optional dictionary of variables to set
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
Canvas image as bytes
|
|
95
|
+
|
|
96
|
+
Raises:
|
|
97
|
+
FileNotFoundError: If template file doesn't exist
|
|
98
|
+
RuntimeError: If template rendering fails
|
|
99
|
+
"""
|
|
100
|
+
try:
|
|
101
|
+
# Use async file reading if possible, but standard open is fine for text files
|
|
102
|
+
# or could use aiofiles if dependency added. For now sync read is okay.
|
|
103
|
+
with open(template_path, 'r', encoding='utf-8') as f:
|
|
104
|
+
template_text = f.read()
|
|
105
|
+
return await self.render_template(template_text, data)
|
|
106
|
+
except FileNotFoundError:
|
|
107
|
+
raise FileNotFoundError(f"Template file not found: {template_path}")
|
|
108
|
+
except Exception as e:
|
|
109
|
+
raise RuntimeError(f"Template rendering failed: {e}")
|
|
110
|
+
|
|
111
|
+
def _register_builtin_functions(self) -> None:
|
|
112
|
+
"""Register all built-in canvas manipulation functions from all modules."""
|
|
113
|
+
# Core canvas functions
|
|
114
|
+
self.register_function("createCanvas", self._create_canvas)
|
|
115
|
+
self.register_function("save", self._save_canvas)
|
|
116
|
+
self.register_function("setVar", self._set_var)
|
|
117
|
+
|
|
118
|
+
# State management functions
|
|
119
|
+
# State management functions - Registered at end to use new implementations
|
|
120
|
+
# self.register_function("setFont", self._set_font)
|
|
121
|
+
# self.register_function("setColor", self._set_color)
|
|
122
|
+
# self.register_function("setStroke", self._set_stroke)
|
|
123
|
+
|
|
124
|
+
# Shape functions from ShapesMixin
|
|
125
|
+
self.register_function("drawRect", self._draw_rect)
|
|
126
|
+
self.register_function("drawCircle", self._draw_circle)
|
|
127
|
+
self.register_function("drawRoundedRect", self._draw_rounded_rect)
|
|
128
|
+
self.register_function("drawLine", self._draw_line)
|
|
129
|
+
|
|
130
|
+
# Text functions from TextMixin
|
|
131
|
+
self.register_function("drawText", self._draw_text)
|
|
132
|
+
self.register_function("drawTextStroke", self._draw_text_stroke)
|
|
133
|
+
self.register_function("drawTextGradient", self._draw_text_gradient)
|
|
134
|
+
self.register_function("toUpper", self._to_upper)
|
|
135
|
+
self.register_function("toLower", self._to_lower)
|
|
136
|
+
self.register_function("toTitle", self._to_title)
|
|
137
|
+
self.register_function("measureText", self._measure_text)
|
|
138
|
+
self.register_function("wrapText", self._wrap_text)
|
|
139
|
+
self.register_function("autoSizeText", self._auto_size_text)
|
|
140
|
+
self.register_function("truncateText", self._truncate_text)
|
|
141
|
+
self.register_function("drawTextMid", self._draw_text_mid)
|
|
142
|
+
self.register_function("drawTextIn", self._draw_text_in)
|
|
143
|
+
|
|
144
|
+
# Image functions from ImagesMixin
|
|
145
|
+
self.register_function("drawImage", self._draw_image)
|
|
146
|
+
self.register_function("useImageAsCanvas", self._use_image_as_canvas)
|
|
147
|
+
self.register_function("imageFilter", self._image_filter)
|
|
148
|
+
self.register_function("clearImageCache", self.clear_image_cache)
|
|
149
|
+
|
|
150
|
+
# Utility functions from UtilsMixin
|
|
151
|
+
self.register_function("math", self._math)
|
|
152
|
+
self.register_function("if", self._if)
|
|
153
|
+
self.register_function("random", self._random)
|
|
154
|
+
self.register_function("getHex", self._get_hex)
|
|
155
|
+
self.register_function("replace", self._replace)
|
|
156
|
+
self.register_function("length", self._length)
|
|
157
|
+
self.register_function("substring", self._substring)
|
|
158
|
+
self.register_function("join", self._join)
|
|
159
|
+
self.register_function("split", self._split)
|
|
160
|
+
|
|
161
|
+
# Masking functions from MaskingMixin
|
|
162
|
+
self.register_function("createLayer", self._create_layer)
|
|
163
|
+
self.register_function("switchLayer", self._switch_layer)
|
|
164
|
+
self.register_function("mergeLayer", self._merge_layer)
|
|
165
|
+
self.register_function("applyMask", self._apply_mask)
|
|
166
|
+
|
|
167
|
+
# State management commands
|
|
168
|
+
self.register_function("setFont", self._cmd_set_font)
|
|
169
|
+
self.register_function("loadFont", self._load_font)
|
|
170
|
+
self.register_function("setColor", self._cmd_set_color)
|
|
171
|
+
self.register_function("setStroke", self._cmd_set_stroke)
|
|
172
|
+
|
|
173
|
+
def _create_canvas(self, width: str, height: str, color: str = "white") -> str:
|
|
174
|
+
"""
|
|
175
|
+
Create a new canvas with specified dimensions and background color.
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
width: Canvas width as string
|
|
179
|
+
height: Canvas height as string
|
|
180
|
+
color: Background color (default: "white")
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
Confirmation message
|
|
184
|
+
"""
|
|
185
|
+
try:
|
|
186
|
+
w = int(width)
|
|
187
|
+
h = int(height)
|
|
188
|
+
self.canvas_size = (w, h)
|
|
189
|
+
|
|
190
|
+
# Parse color using the helper from AlphaMixin
|
|
191
|
+
bg_color = self._get_color(color) or (255, 255, 255)
|
|
192
|
+
|
|
193
|
+
self.canvas = Image.new("RGB", (w, h), bg_color)
|
|
194
|
+
self.draw = ImageDraw.Draw(self.canvas)
|
|
195
|
+
return f"Canvas created: {w}x{h}"
|
|
196
|
+
except ValueError as e:
|
|
197
|
+
raise ValueError(f"Invalid canvas dimensions: {e}")
|
|
198
|
+
|
|
199
|
+
def _set_var(self, name: str, value: str) -> str:
|
|
200
|
+
"""
|
|
201
|
+
Set a variable value.
|
|
202
|
+
|
|
203
|
+
Args:
|
|
204
|
+
name: Variable name (without {} wrapper)
|
|
205
|
+
value: Variable value
|
|
206
|
+
|
|
207
|
+
Returns:
|
|
208
|
+
Confirmation message
|
|
209
|
+
"""
|
|
210
|
+
self.set_variable(name, value)
|
|
211
|
+
return f"Variable {name} set to {value}"
|
|
212
|
+
|
|
213
|
+
def _save_canvas(self, filename: str = "output.png") -> str:
|
|
214
|
+
"""
|
|
215
|
+
Save the current canvas to a file with maximum quality.
|
|
216
|
+
"""
|
|
217
|
+
if not self.canvas:
|
|
218
|
+
raise RuntimeError("No canvas to save. Call $createCanvas first.")
|
|
219
|
+
|
|
220
|
+
try:
|
|
221
|
+
# Set high quality parameters for various formats
|
|
222
|
+
save_params = {"optimize": True}
|
|
223
|
+
if filename.lower().endswith(('.jpg', '.jpeg')):
|
|
224
|
+
save_params.update({"quality": 100, "subsampling": 0})
|
|
225
|
+
|
|
226
|
+
self.canvas.save(filename, **save_params)
|
|
227
|
+
return f"Canvas saved as {filename}"
|
|
228
|
+
except Exception as e:
|
|
229
|
+
raise RuntimeError(f"Failed to save canvas: {e}")
|
|
230
|
+
|
|
231
|
+
def get_canvas_bytes(self, format: str = "PNG") -> bytes:
|
|
232
|
+
"""
|
|
233
|
+
Get the canvas as bytes with maximum quality.
|
|
234
|
+
"""
|
|
235
|
+
if not self.canvas:
|
|
236
|
+
raise RuntimeError("No canvas available. Call $createCanvas first.")
|
|
237
|
+
|
|
238
|
+
img_bytes = io.BytesIO()
|
|
239
|
+
|
|
240
|
+
# Set high quality parameters
|
|
241
|
+
save_params = {"format": format, "optimize": True}
|
|
242
|
+
if format.upper() in ["JPEG", "JPG"]:
|
|
243
|
+
save_params.update({"quality": 100, "subsampling": 0})
|
|
244
|
+
|
|
245
|
+
self.canvas.save(img_bytes, **save_params)
|
|
246
|
+
img_bytes.seek(0)
|
|
247
|
+
return img_bytes.getvalue()
|
|
248
|
+
|
|
249
|
+
def reset(self) -> None:
|
|
250
|
+
"""Reset the canvas, drawing context, and state."""
|
|
251
|
+
self.canvas = None
|
|
252
|
+
self.draw = None
|
|
253
|
+
self.canvas_size = (0, 0)
|
|
254
|
+
self._init_state()
|
|
255
|
+
self._init_text()
|
|
256
|
+
if hasattr(self, '_image_cache'):
|
|
257
|
+
self._image_cache.clear()
|
|
258
|
+
|
|
259
|
+
# State management is now handled via AlphaMixin _cmd_* methods
|
alphapil/interpreter.py
ADDED
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CanvasInterpreter - Core recursive parser for AlphaPIL.
|
|
3
|
+
|
|
4
|
+
This module provides the fundamental parsing logic for handling nested functions
|
|
5
|
+
using a while loop and re.search pattern matching.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import re
|
|
9
|
+
import asyncio
|
|
10
|
+
import inspect
|
|
11
|
+
from typing import Any, Dict, List, Optional, Callable
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class CanvasInterpreter:
|
|
15
|
+
"""
|
|
16
|
+
Core interpreter class that handles recursive parsing of nested functions.
|
|
17
|
+
|
|
18
|
+
The parser resolves functions from inside-out using a while loop with re.search
|
|
19
|
+
to find and replace nested function calls until no more patterns remain.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def __init__(self):
|
|
23
|
+
"""Initialize the interpreter with an empty function registry."""
|
|
24
|
+
self.functions: Dict[str, Callable] = {}
|
|
25
|
+
self.variables: Dict[str, Any] = {}
|
|
26
|
+
|
|
27
|
+
def register_function(self, name: str, func: Callable) -> None:
|
|
28
|
+
"""
|
|
29
|
+
Register a function that can be called in templates.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
name: Function name (without $ prefix)
|
|
33
|
+
func: Callable that will be executed when the function is encountered
|
|
34
|
+
"""
|
|
35
|
+
self.functions[name] = func
|
|
36
|
+
|
|
37
|
+
def set_variable(self, name: str, value: Any) -> None:
|
|
38
|
+
"""
|
|
39
|
+
Set a variable that can be used in templates.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
name: Variable name (without {} wrapper)
|
|
43
|
+
value: Value to assign to the variable
|
|
44
|
+
"""
|
|
45
|
+
self.variables[name] = value
|
|
46
|
+
|
|
47
|
+
def _find_innermost_function(self, text: str) -> Optional[re.Match]:
|
|
48
|
+
"""
|
|
49
|
+
Find the innermost function call in the text.
|
|
50
|
+
|
|
51
|
+
This regex pattern matches function calls like $functionName[...;...]
|
|
52
|
+
and ensures we find the innermost nested calls first.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
text: Text to search for function calls
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
Match object if function found, None otherwise
|
|
59
|
+
"""
|
|
60
|
+
# Pattern matches: $functionName[arguments]
|
|
61
|
+
# Uses negative lookahead to avoid matching nested brackets incorrectly
|
|
62
|
+
pattern = r'\$(\w+)\[([^\[\]]*(?:\[[^\[\]]*\][^\[\]]*)*)\]'
|
|
63
|
+
return re.search(pattern, text)
|
|
64
|
+
|
|
65
|
+
def _parse_arguments(self, args_str: str) -> List[str]:
|
|
66
|
+
"""
|
|
67
|
+
Parse function arguments, handling quoted strings and semicolon separators.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
args_str: String containing function arguments
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
List of parsed arguments
|
|
74
|
+
"""
|
|
75
|
+
if not args_str.strip():
|
|
76
|
+
return []
|
|
77
|
+
|
|
78
|
+
args = []
|
|
79
|
+
current_arg = ""
|
|
80
|
+
in_quotes = False
|
|
81
|
+
quote_char = None
|
|
82
|
+
nesting_depth = 0
|
|
83
|
+
|
|
84
|
+
i = 0
|
|
85
|
+
while i < len(args_str):
|
|
86
|
+
char = args_str[i]
|
|
87
|
+
|
|
88
|
+
if char in ('"', "'") and not in_quotes:
|
|
89
|
+
in_quotes = True
|
|
90
|
+
quote_char = char
|
|
91
|
+
current_arg += char
|
|
92
|
+
elif char == quote_char and in_quotes:
|
|
93
|
+
in_quotes = False
|
|
94
|
+
quote_char = None
|
|
95
|
+
current_arg += char
|
|
96
|
+
elif not in_quotes:
|
|
97
|
+
if char in ('[', '('):
|
|
98
|
+
nesting_depth += 1
|
|
99
|
+
current_arg += char
|
|
100
|
+
elif char in (']', ')'):
|
|
101
|
+
nesting_depth -= 1
|
|
102
|
+
current_arg += char
|
|
103
|
+
elif char == ';' and nesting_depth == 0:
|
|
104
|
+
args.append(current_arg.strip())
|
|
105
|
+
current_arg = ""
|
|
106
|
+
else:
|
|
107
|
+
current_arg += char
|
|
108
|
+
else:
|
|
109
|
+
current_arg += char
|
|
110
|
+
|
|
111
|
+
i += 1
|
|
112
|
+
|
|
113
|
+
# Always append the last argument, even if empty, to ensure correct argument count
|
|
114
|
+
args.append(current_arg.strip())
|
|
115
|
+
|
|
116
|
+
return args
|
|
117
|
+
|
|
118
|
+
def _resolve_variables(self, text: str) -> str:
|
|
119
|
+
"""
|
|
120
|
+
Resolve variable placeholders like {variable_name} in the text.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
text: Text containing variable placeholders
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
Text with variables replaced by their values
|
|
127
|
+
"""
|
|
128
|
+
def replace_var(match):
|
|
129
|
+
var_name = match.group(1)
|
|
130
|
+
# Support default values: {var|default}
|
|
131
|
+
default_val = ""
|
|
132
|
+
if '|' in var_name:
|
|
133
|
+
var_name, default_val = var_name.split('|', 1)
|
|
134
|
+
|
|
135
|
+
# Use get() with default_val (or empty string if not provided)
|
|
136
|
+
# Note: variables dict values should be strings ideally
|
|
137
|
+
val = self.variables.get(var_name, default_val)
|
|
138
|
+
return str(val) if val is not None else default_val
|
|
139
|
+
|
|
140
|
+
# Match variables with optional |default part: {name} or {name|default}
|
|
141
|
+
# Allow any characters except closing brace inside
|
|
142
|
+
return re.sub(r'\{([^\}]+)\}', replace_var, text)
|
|
143
|
+
|
|
144
|
+
async def _preprocess_argument(self, arg: str) -> str:
|
|
145
|
+
"""
|
|
146
|
+
Recursively resolve all nested functions and variables in an argument.
|
|
147
|
+
|
|
148
|
+
Execution order:
|
|
149
|
+
1. Variable replacement: {var} → value
|
|
150
|
+
2. Nested function evaluation: $func[...] → result (recursively)
|
|
151
|
+
|
|
152
|
+
This ensures that expressions like $drawRect[10;10;$math[100 * 2];50;red]
|
|
153
|
+
work correctly by evaluating $math[100 * 2] → "200" before passing to _drawRect.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
arg: Argument string that may contain nested functions and variables
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
Fully resolved argument value
|
|
160
|
+
"""
|
|
161
|
+
# First pass: resolve variables
|
|
162
|
+
arg = self._resolve_variables(arg)
|
|
163
|
+
|
|
164
|
+
# Second pass: resolve nested functions recursively
|
|
165
|
+
max_iterations = 100 # Prevent infinite loops in nested functions
|
|
166
|
+
iteration = 0
|
|
167
|
+
|
|
168
|
+
while iteration < max_iterations:
|
|
169
|
+
iteration += 1
|
|
170
|
+
match = self._find_innermost_function(arg)
|
|
171
|
+
|
|
172
|
+
if not match:
|
|
173
|
+
break # No more functions to resolve
|
|
174
|
+
|
|
175
|
+
func_name = match.group(1)
|
|
176
|
+
args_str = match.group(2)
|
|
177
|
+
|
|
178
|
+
# Recursively preprocess nested arguments
|
|
179
|
+
nested_args = self._parse_arguments(args_str)
|
|
180
|
+
|
|
181
|
+
# Process nested arguments async
|
|
182
|
+
processed_nested_args = []
|
|
183
|
+
for a in nested_args:
|
|
184
|
+
processed_nested_args.append(await self._preprocess_argument(a))
|
|
185
|
+
|
|
186
|
+
# Execute the function
|
|
187
|
+
if func_name not in self.functions:
|
|
188
|
+
raise ValueError(f"Unknown function: ${func_name}")
|
|
189
|
+
|
|
190
|
+
try:
|
|
191
|
+
result = await self._execute_function(func_name, processed_nested_args)
|
|
192
|
+
# Clean up numeric results
|
|
193
|
+
if func_name in ['math', 'random', 'length', 'getHex']:
|
|
194
|
+
result = str(result).strip()
|
|
195
|
+
else:
|
|
196
|
+
result = str(result)
|
|
197
|
+
except Exception as e:
|
|
198
|
+
raise RuntimeError(f"Error in nested function ${func_name}: {e}")
|
|
199
|
+
|
|
200
|
+
# Replace in original string
|
|
201
|
+
arg = arg[:match.start()] + result + arg[match.end():]
|
|
202
|
+
|
|
203
|
+
if iteration >= max_iterations:
|
|
204
|
+
raise RuntimeError(f"Argument preprocessing exceeded maximum iterations: {arg}")
|
|
205
|
+
|
|
206
|
+
return arg
|
|
207
|
+
|
|
208
|
+
async def _execute_function(self, func_name: str, args: List[str]) -> str:
|
|
209
|
+
"""
|
|
210
|
+
Execute a registered function with parsed arguments (Async).
|
|
211
|
+
Supports both sync and async functions, and flexible argument mapping.
|
|
212
|
+
|
|
213
|
+
Args:
|
|
214
|
+
func_name: Name of the function to execute
|
|
215
|
+
args: List of argument strings
|
|
216
|
+
|
|
217
|
+
Returns:
|
|
218
|
+
String result of the function execution
|
|
219
|
+
"""
|
|
220
|
+
if func_name not in self.functions:
|
|
221
|
+
raise ValueError(f"Unknown function: ${func_name}")
|
|
222
|
+
|
|
223
|
+
try:
|
|
224
|
+
func = self.functions[func_name]
|
|
225
|
+
|
|
226
|
+
# Split into named and unnamed arguments
|
|
227
|
+
named_args = {}
|
|
228
|
+
unnamed_args = []
|
|
229
|
+
|
|
230
|
+
for arg in args:
|
|
231
|
+
if '=' in arg and not arg.startswith(('http://', 'https://')):
|
|
232
|
+
parts = arg.split('=', 1)
|
|
233
|
+
key = parts[0].strip()
|
|
234
|
+
val = parts[1].strip()
|
|
235
|
+
|
|
236
|
+
# Only treat as named arg if:
|
|
237
|
+
# 1. key is a valid identifier
|
|
238
|
+
# 2. value doesn't start with '=' (avoids '==')
|
|
239
|
+
# 3. key doesn't end with common comparison symbols (avoids '!=', '<=', '>=')
|
|
240
|
+
if key.isidentifier() and not val.startswith('=') and not key.endswith(('!', '<', '>')):
|
|
241
|
+
named_args[key] = val
|
|
242
|
+
continue
|
|
243
|
+
unnamed_args.append(arg)
|
|
244
|
+
|
|
245
|
+
# Use inspect to map arguments to the function signature
|
|
246
|
+
sig = inspect.signature(func)
|
|
247
|
+
params = list(sig.parameters.values())
|
|
248
|
+
|
|
249
|
+
# Build the argument set
|
|
250
|
+
final_kwargs = {}
|
|
251
|
+
|
|
252
|
+
# 1. Apply named arguments
|
|
253
|
+
final_kwargs.update(named_args)
|
|
254
|
+
|
|
255
|
+
# 2. Map unnamed arguments to remaining parameters
|
|
256
|
+
unnamed_idx = 0
|
|
257
|
+
for param in params:
|
|
258
|
+
if param.name not in final_kwargs and unnamed_idx < len(unnamed_args):
|
|
259
|
+
final_kwargs[param.name] = unnamed_args[unnamed_idx]
|
|
260
|
+
unnamed_idx += 1
|
|
261
|
+
|
|
262
|
+
# Execute the function - only pass arguments that exist in the function signature
|
|
263
|
+
safe_kwargs = {k: v for k, v in final_kwargs.items() if k in sig.parameters}
|
|
264
|
+
result = func(**safe_kwargs)
|
|
265
|
+
|
|
266
|
+
# Handle async functions
|
|
267
|
+
if inspect.iscoroutine(result):
|
|
268
|
+
result = await result
|
|
269
|
+
|
|
270
|
+
# Strip whitespace from result for numeric functions
|
|
271
|
+
if func_name in ['math', 'random', 'length', 'getHex']:
|
|
272
|
+
result = str(result).strip()
|
|
273
|
+
|
|
274
|
+
return str(result)
|
|
275
|
+
except Exception as e:
|
|
276
|
+
# Provide more detailed error info for debugging
|
|
277
|
+
raise RuntimeError(f"Error executing function ${func_name} with args {args}: {e}")
|
|
278
|
+
|
|
279
|
+
async def parse(self, template: str) -> str:
|
|
280
|
+
"""
|
|
281
|
+
Parse a template string with correct execution order (Async).
|
|
282
|
+
|
|
283
|
+
Execution order:
|
|
284
|
+
1. Resolve variables: {var} → value
|
|
285
|
+
2. Find innermost function
|
|
286
|
+
3. Preprocess arguments (recursively resolve variables + nested functions)
|
|
287
|
+
4. Execute function with fully resolved arguments
|
|
288
|
+
5. Replace function call with result
|
|
289
|
+
6. Repeat until no functions remain
|
|
290
|
+
|
|
291
|
+
This ensures expressions like $drawRect[10;10;$math[100 * 2];50;red]
|
|
292
|
+
work correctly by evaluating nested $math before passing to _drawRect.
|
|
293
|
+
|
|
294
|
+
Args:
|
|
295
|
+
template: Template string containing function calls and variables
|
|
296
|
+
|
|
297
|
+
Returns:
|
|
298
|
+
Fully resolved template string
|
|
299
|
+
"""
|
|
300
|
+
result = template
|
|
301
|
+
|
|
302
|
+
# Main parsing loop with iteration limit to prevent infinite loops
|
|
303
|
+
max_iterations = 1000
|
|
304
|
+
iteration = 0
|
|
305
|
+
|
|
306
|
+
while iteration < max_iterations:
|
|
307
|
+
iteration += 1
|
|
308
|
+
|
|
309
|
+
# Step 1: Resolve variables first
|
|
310
|
+
result = self._resolve_variables(result)
|
|
311
|
+
|
|
312
|
+
# Step 2: Find the innermost function call
|
|
313
|
+
match = self._find_innermost_function(result)
|
|
314
|
+
|
|
315
|
+
if not match:
|
|
316
|
+
break # No more function calls to resolve
|
|
317
|
+
|
|
318
|
+
func_name = match.group(1)
|
|
319
|
+
args_str = match.group(2)
|
|
320
|
+
|
|
321
|
+
# Step 3: Parse and preprocess arguments
|
|
322
|
+
# This recursively resolves all variables and nested functions in each argument
|
|
323
|
+
args = self._parse_arguments(args_str)
|
|
324
|
+
|
|
325
|
+
# Process arguments async
|
|
326
|
+
processed_args = []
|
|
327
|
+
for arg in args:
|
|
328
|
+
processed_args.append(await self._preprocess_argument(arg))
|
|
329
|
+
|
|
330
|
+
# Step 4: Execute function with fully resolved arguments
|
|
331
|
+
try:
|
|
332
|
+
func_result = await self._execute_function(func_name, processed_args)
|
|
333
|
+
except Exception as e:
|
|
334
|
+
raise RuntimeError(f"Failed to execute ${func_name}: {e}")
|
|
335
|
+
|
|
336
|
+
# Step 5: Replace the function call with its result
|
|
337
|
+
result = result[:match.start()] + func_result + result[match.end():]
|
|
338
|
+
|
|
339
|
+
if iteration >= max_iterations:
|
|
340
|
+
raise RuntimeError("Parser exceeded maximum iterations - possible infinite loop in template")
|
|
341
|
+
|
|
342
|
+
return result
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AlphaPIL Modules - Modular functionality for image generation.
|
|
3
|
+
|
|
4
|
+
This package contains modular components that can be easily extended
|
|
5
|
+
by adding new Python files to this directory.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .base import AlphaMixin
|
|
9
|
+
from .shapes import ShapesMixin
|
|
10
|
+
from .text import TextMixin
|
|
11
|
+
from .images import ImagesMixin
|
|
12
|
+
from .utils import UtilsMixin
|
|
13
|
+
from .masking import MaskingMixin
|
|
14
|
+
|
|
15
|
+
__all__ = ["AlphaMixin", "ShapesMixin", "TextMixin", "ImagesMixin", "UtilsMixin", "MaskingMixin"]
|