diff --git a/more_math/Parser/MathExpr.g4 b/more_math/Parser/MathExpr.g4 index 44288a5..c7cd612 100644 --- a/more_math/Parser/MathExpr.g4 +++ b/more_math/Parser/MathExpr.g4 @@ -1383,7 +1383,7 @@ func3: /** text_image(text, font, size, [max_width], [weight], [rotation_angle], [line_spacing], [italic], [underline]) - renders text to a 2D tensor with values 0-1 - text: text to render; line breaks start new lines - - font: font name to look up in the system fonts, or a path to a font file + - font: font name to look up in the system fonts, or a path to a font file in a system font folder or the ComfyUI input folder - size: font size in pixels - max_width: wrap lines wider than this many pixels (default: 0, no wrapping) - weight: font weight 100-900 as in CSS (default: 400) diff --git a/more_math/Parser/UnifiedMathVisitor.py b/more_math/Parser/UnifiedMathVisitor.py index f014efe..344db8c 100644 --- a/more_math/Parser/UnifiedMathVisitor.py +++ b/more_math/Parser/UnifiedMathVisitor.py @@ -10,6 +10,7 @@ import math import inspect import weakref import torch.nn.functional as F +import folder_paths from comfy.nested_tensor import NestedTensor from comfy.model_management import throw_exception_if_processing_interrupted from comfy.k_diffusion import sampling as k_sampling @@ -190,9 +191,7 @@ def _font_axis_name(axis): return str(name).lower() -@functools.lru_cache(maxsize=1) -def _system_fonts(): - """Index of installed fonts, scanned once per process (fonts installed later need a restart).""" +def _font_dirs(): if sys.platform == "win32": search_dirs = [os.path.join(os.environ.get("WINDIR", "C:\\Windows"), "Fonts"), os.path.expandvars(r"%LOCALAPPDATA%\Microsoft\Windows\Fonts")] elif sys.platform == "darwin": @@ -205,9 +204,23 @@ def _system_fonts(): os.path.expanduser("~/.local/share/fonts"), os.path.expanduser("~/.fonts"), ] + return list(dict.fromkeys(search_dirs)) + +def _is_allowed_font_file(path): + """Font files given by path must be in a system font folder or in ComfyUI's input folder.""" + if not os.path.isfile(path): + return False + path = os.path.normcase(os.path.realpath(path)) + roots = _font_dirs() + [folder_paths.get_input_directory()] + return any(path.startswith(os.path.normcase(os.path.realpath(root)) + os.sep) for root in roots) + + +@functools.lru_cache(maxsize=1) +def _system_fonts(): + """Index of installed fonts, scanned once per process (fonts installed later need a restart).""" fonts = [] - for directory in dict.fromkeys(search_dirs): + for directory in _font_dirs(): for root, _, files in os.walk(directory): for fname in files: if not fname.lower().endswith((".ttf", ".otf", ".ttc", ".otc")): @@ -3536,7 +3549,7 @@ class UnifiedMathVisitor(MathExprVisitor): tier = _FONT_WEIGHT_TIERS[bisect.bisect_left((150, 250, 350, 450, 550, 650, 750, 850), weight_val)] synthetic_italic = synthetic_bold = False variation_weight, variation_italic = weight_val, is_italic - if os.path.isfile(font_name): + if _is_allowed_font_file(font_name): font_path = font_name else: font_path = _find_font(font_name, tier, weight_val, is_italic) diff --git a/more_math/Parser/inbuilt_symbols.py b/more_math/Parser/inbuilt_symbols.py index 8bec85f..71d1e3e 100644 --- a/more_math/Parser/inbuilt_symbols.py +++ b/more_math/Parser/inbuilt_symbols.py @@ -264,7 +264,7 @@ INBUILT_FUNCTION_META = { 'interpolate_area': {'min_args': 2, 'max_args': 2, 'signature': 'interpolate_area(x, scale)', 'forms': [['x', 'scale']], 'description': 'resizes x [N, C, ...] with area interpolation. A number scales every spatial dimension; a list is the target spatial size', 'details': '', 'params': {'x': 'tensor shaped [N, C, ...] with 1-3 spatial dimensions', 'scale': 'a number scales every spatial dimension; a list gives the target spatial size'}, 'aliases': []}, 'interpolate_nearest': {'min_args': 2, 'max_args': 2, 'signature': 'interpolate_nearest(x, scale)', 'forms': [['x', 'scale']], 'description': 'resizes x [N, C, ...] with nearest-neighbour interpolation. A number scales every spatial dimension; a list is the target spatial size', 'details': '', 'params': {'x': 'tensor shaped [N, C, ...] with 1-3 spatial dimensions', 'scale': 'a number scales every spatial dimension; a list gives the target spatial size'}, 'aliases': ['interpolate_nearest_exact']}, 'interpolate_nearest_exact': {'min_args': 2, 'max_args': 2, 'signature': 'interpolate_nearest_exact(x, scale)', 'forms': [['x', 'scale']], 'description': 'resizes x [N, C, ...] with nearest-neighbour interpolation. A number scales every spatial dimension; a list is the target spatial size', 'details': '', 'params': {'x': 'tensor shaped [N, C, ...] with 1-3 spatial dimensions', 'scale': 'a number scales every spatial dimension; a list gives the target spatial size'}, 'aliases': ['interpolate_nearest']}, - 'text_image': {'min_args': 3, 'max_args': 9, 'signature': 'text_image(text, font, size, [max_width], [weight], [rotation_angle], [line_spacing], [italic], [underline])', 'forms': [['text', 'font', 'size', '[max_width]', '[weight]', '[rotation_angle]', '[line_spacing]', '[italic]', '[underline]']], 'description': 'renders text to a 2D tensor with values 0-1', 'details': '', 'params': {'text': 'text to render; line breaks start new lines', 'font': 'font name to look up in the system fonts, or a path to a font file', 'size': 'font size in pixels', 'max_width': 'wrap lines wider than this many pixels (default: 0, no wrapping)', 'weight': 'font weight 100-900 as in CSS (default: 400)', 'rotation_angle': 'rotation in degrees, counter-clockwise (default: 0)', 'line_spacing': 'line distance as a multiple of the line height (default: 1)', 'italic': 'truthy for italic text (default: false)', 'underline': 'truthy to underline the text (default: false)'}, 'aliases': []}, + 'text_image': {'min_args': 3, 'max_args': 9, 'signature': 'text_image(text, font, size, [max_width], [weight], [rotation_angle], [line_spacing], [italic], [underline])', 'forms': [['text', 'font', 'size', '[max_width]', '[weight]', '[rotation_angle]', '[line_spacing]', '[italic]', '[underline]']], 'description': 'renders text to a 2D tensor with values 0-1', 'details': '', 'params': {'text': 'text to render; line breaks start new lines', 'font': 'font name to look up in the system fonts, or a path to a font file in a system font folder or the ComfyUI input folder', 'size': 'font size in pixels', 'max_width': 'wrap lines wider than this many pixels (default: 0, no wrapping)', 'weight': 'font weight 100-900 as in CSS (default: 400)', 'rotation_angle': 'rotation in degrees, counter-clockwise (default: 0)', 'line_spacing': 'line distance as a multiple of the line height (default: 1)', 'italic': 'truthy for italic text (default: false)', 'underline': 'truthy to underline the text (default: false)'}, 'aliases': []}, 'as_nested_tensor': {'min_args': 1, 'max_args': 1, 'signature': 'as_nested_tensor(values)', 'forms': [['values']], 'description': 'converts a list to a nested tensor (special object containing tensors of different shapes behaving like a tensor)', 'details': '', 'params': {'values': 'list of tensors; their shapes may differ'}, 'aliases': ['as_nested']}, 'as_nested': {'min_args': 1, 'max_args': 1, 'signature': 'as_nested(values)', 'forms': [['values']], 'description': 'converts a list to a nested tensor (special object containing tensors of different shapes behaving like a tensor)', 'details': '', 'params': {'values': 'list of tensors; their shapes may differ'}, 'aliases': ['as_nested_tensor']}, 'svd': {'min_args': 1, 'max_args': 2, 'signature': 'svd(x, [full_matrices])', 'forms': [['x', '[full_matrices]']], 'description': 'computes the singular value decomposition of x. Returns a list of 3 tensors U, S, Vh such that x = matmul(matmul(U, diagonal_matrix(S, shape(U))), Vh)', 'details': '', 'params': {'x': 'matrix, or a batch of matrices in the last two dimensions', 'full_matrices': 'truthy to return full-size U and Vh (default: false)'}, 'aliases': ['singular_value_decomposition']}, diff --git a/tests/test_unified_visitor_regressions.py b/tests/test_unified_visitor_regressions.py index c9a9832..0e9bc97 100644 --- a/tests/test_unified_visitor_regressions.py +++ b/tests/test_unified_visitor_regressions.py @@ -80,17 +80,12 @@ def test_lambda_calls_require_exact_argument_count(call): def test_text_image_size_expression_is_evaluated_once(monkeypatch): visitor_module = importlib.import_module("more_math.Parser.UnifiedMathVisitor") default_font = ImageFont.load_default() - original_isfile = os.path.isfile monkeypatch.setattr( visitor_module.ImageFont, "truetype", lambda *args, **kwargs: default_font, ) - monkeypatch.setattr( - visitor_module.os.path, - "isfile", - lambda path: path == "mock-font" or original_isfile(path), - ) + monkeypatch.setattr(visitor_module, "_is_allowed_font_file", lambda path: path == "mock-font") values = MathDict() _visit( @@ -101,6 +96,15 @@ def test_text_image_size_expression_is_evaluated_once(monkeypatch): assert len(values) == 1 +def test_text_image_rejects_font_path_outside_font_folders(monkeypatch): + visitor_module = importlib.import_module("more_math.Parser.UnifiedMathVisitor") + monkeypatch.setattr(visitor_module, "_find_font", lambda *args: None) + path = os.path.abspath(__file__).replace("\\", "/") + + with pytest.raises(FileNotFoundError): + _visit(f'text_image("x", "{path}", 12)', {}) + + def test_stack_list_of_tensors_with_negative_dimension(): first = torch.tensor([1.0, 2.0]) second = torch.tensor([3.0, 4.0]) diff --git a/web/inbuilt_symbols.js b/web/inbuilt_symbols.js index ed031ea..5d354e8 100644 --- a/web/inbuilt_symbols.js +++ b/web/inbuilt_symbols.js @@ -267,7 +267,7 @@ export const FUNCTION_META = { "interpolate_area": { minArgs: 2, maxArgs: 2, signature: "interpolate_area(x, scale)", forms: [["x", "scale"]], description: "resizes x [N, C, ...] with area interpolation. A number scales every spatial dimension; a list is the target spatial size", details: "", params: {"x": "tensor shaped [N, C, ...] with 1-3 spatial dimensions", "scale": "a number scales every spatial dimension; a list gives the target spatial size"}, aliases: [] }, "interpolate_nearest": { minArgs: 2, maxArgs: 2, signature: "interpolate_nearest(x, scale)", forms: [["x", "scale"]], description: "resizes x [N, C, ...] with nearest-neighbour interpolation. A number scales every spatial dimension; a list is the target spatial size", details: "", params: {"x": "tensor shaped [N, C, ...] with 1-3 spatial dimensions", "scale": "a number scales every spatial dimension; a list gives the target spatial size"}, aliases: ["interpolate_nearest_exact"] }, "interpolate_nearest_exact": { minArgs: 2, maxArgs: 2, signature: "interpolate_nearest_exact(x, scale)", forms: [["x", "scale"]], description: "resizes x [N, C, ...] with nearest-neighbour interpolation. A number scales every spatial dimension; a list is the target spatial size", details: "", params: {"x": "tensor shaped [N, C, ...] with 1-3 spatial dimensions", "scale": "a number scales every spatial dimension; a list gives the target spatial size"}, aliases: ["interpolate_nearest"] }, - "text_image": { minArgs: 3, maxArgs: 9, signature: "text_image(text, font, size, [max_width], [weight], [rotation_angle], [line_spacing], [italic], [underline])", forms: [["text", "font", "size", "[max_width]", "[weight]", "[rotation_angle]", "[line_spacing]", "[italic]", "[underline]"]], description: "renders text to a 2D tensor with values 0-1", details: "", params: {"text": "text to render; line breaks start new lines", "font": "font name to look up in the system fonts, or a path to a font file", "size": "font size in pixels", "max_width": "wrap lines wider than this many pixels (default: 0, no wrapping)", "weight": "font weight 100-900 as in CSS (default: 400)", "rotation_angle": "rotation in degrees, counter-clockwise (default: 0)", "line_spacing": "line distance as a multiple of the line height (default: 1)", "italic": "truthy for italic text (default: false)", "underline": "truthy to underline the text (default: false)"}, aliases: [] }, + "text_image": { minArgs: 3, maxArgs: 9, signature: "text_image(text, font, size, [max_width], [weight], [rotation_angle], [line_spacing], [italic], [underline])", forms: [["text", "font", "size", "[max_width]", "[weight]", "[rotation_angle]", "[line_spacing]", "[italic]", "[underline]"]], description: "renders text to a 2D tensor with values 0-1", details: "", params: {"text": "text to render; line breaks start new lines", "font": "font name to look up in the system fonts, or a path to a font file in a system font folder or the ComfyUI input folder", "size": "font size in pixels", "max_width": "wrap lines wider than this many pixels (default: 0, no wrapping)", "weight": "font weight 100-900 as in CSS (default: 400)", "rotation_angle": "rotation in degrees, counter-clockwise (default: 0)", "line_spacing": "line distance as a multiple of the line height (default: 1)", "italic": "truthy for italic text (default: false)", "underline": "truthy to underline the text (default: false)"}, aliases: [] }, "as_nested_tensor": { minArgs: 1, maxArgs: 1, signature: "as_nested_tensor(values)", forms: [["values"]], description: "converts a list to a nested tensor (special object containing tensors of different shapes behaving like a tensor)", details: "", params: {"values": "list of tensors; their shapes may differ"}, aliases: ["as_nested"] }, "as_nested": { minArgs: 1, maxArgs: 1, signature: "as_nested(values)", forms: [["values"]], description: "converts a list to a nested tensor (special object containing tensors of different shapes behaving like a tensor)", details: "", params: {"values": "list of tensors; their shapes may differ"}, aliases: ["as_nested_tensor"] }, "svd": { minArgs: 1, maxArgs: 2, signature: "svd(x, [full_matrices])", forms: [["x", "[full_matrices]"]], description: "computes the singular value decomposition of x. Returns a list of 3 tensors U, S, Vh such that x = matmul(matmul(U, diagonal_matrix(S, shape(U))), Vh)", details: "", params: {"x": "matrix, or a batch of matrices in the last two dimensions", "full_matrices": "truthy to return full-size U and Vh (default: false)"}, aliases: ["singular_value_decomposition"] },