@@ -83,6 +83,9 @@ class CustomLayer:
8383
8484class Canvas :
8585 _custom_layer_registry : dict [str , Callable [..., Image .Image | None ]] = {}
86+ _template_registry : dict [str , str ] = {}
87+
88+ _BUILTIN_TEMPLATES_DIR = os .path .join (os .path .dirname (__file__ ), "templates" )
8689
8790 @classmethod
8891 def register_layer_fn (cls , name : str , fn : Callable [..., Image .Image | None ]) -> None :
@@ -92,6 +95,14 @@ def register_layer_fn(cls, name: str, fn: Callable[..., Image.Image | None]) ->
9295 def unregister_layer_fn (cls , name : str ) -> None :
9396 cls ._custom_layer_registry .pop (name , None )
9497
98+ @classmethod
99+ def register_template (cls , name : str , path : str ) -> None :
100+ cls ._template_registry [name ] = path
101+
102+ @classmethod
103+ def unregister_template (cls , name : str ) -> None :
104+ cls ._template_registry .pop (name , None )
105+
95106 def __init__ (self , width : int , height : int , layers : list [RenderableLayer ] | None = None ):
96107 if width <= 0 :
97108 raise ValidationError ("width must be > 0" )
@@ -352,6 +363,49 @@ def from_json(cls, data: str) -> Self:
352363
353364 return cls (width = raw ["width" ], height = raw ["height" ], layers = renderable_layers )
354365
366+ @classmethod
367+ def _read_template_file (cls , path : str ) -> str :
368+ try :
369+ with open (path ) as f :
370+ return f .read ()
371+ except OSError as e :
372+ raise ValidationError (f"Cannot read template file '{ path } ': { e } " ) from e
373+
374+ @classmethod
375+ def _resolve_template_string (cls , spec_or_path : str ) -> str :
376+ if spec_or_path .lstrip ().startswith ("{" ):
377+ return spec_or_path
378+ if spec_or_path in cls ._template_registry :
379+ return cls ._read_template_file (cls ._template_registry [spec_or_path ])
380+ builtin_path = os .path .join (cls ._BUILTIN_TEMPLATES_DIR , f"{ spec_or_path } .json" )
381+ if os .path .exists (builtin_path ):
382+ return cls ._read_template_file (builtin_path )
383+ if os .path .exists (spec_or_path ):
384+ return cls ._read_template_file (spec_or_path )
385+ raise ValidationError (
386+ f"Template '{ spec_or_path } ' is not a registered template name, "
387+ f"built-in template name, or valid file path."
388+ )
389+
390+ @classmethod
391+ def from_template (cls , spec_or_path : str , variables : dict [str , str ] | None = None ) -> Self :
392+ import json as _json
393+ import re
394+
395+ variables = variables or {}
396+ raw_spec = cls ._resolve_template_string (spec_or_path )
397+
398+ def substitute (match : re .Match ) -> str :
399+ key = match .group (1 ) or match .group (2 )
400+ if key not in variables :
401+ raise ValidationError (
402+ f"Template placeholder '${ key } ' has no matching variable. "
403+ f"Provide variables={{'{ key } ': ...}} to Canvas.from_template()."
404+ )
405+ return _json .dumps (variables [key ])[1 :- 1 ]
406+
407+ return cls .from_json (re .sub (r"\$\{(\w+)\}|\$(\w+)" , substitute , raw_spec ))
408+
355409 def to_base64 (self , format : FileFormat = "PNG" , quality : int | None = None ) -> str :
356410 import base64
357411
0 commit comments