From a73b502cac243d26094813803732d37f7a54444e Mon Sep 17 00:00:00 2001 From: kevin9327 <5299031+kevin9327@users.noreply.github.com> Date: Thu, 12 Nov 2026 14:50:24 +0900 Subject: [PATCH 1/2] fix: handle incomplete and non-base64 image data URLs _load_image used split(',')[1], so a data URL without a comma raised IndexError, and non-base64 payloads such as data:image/svg+xml, were passed to b64decode. Decode base64 when the header says so, unquote plain payloads, and raise ValueError when the comma is missing. --- CHANGELOG.md | 1 + llama_cpp/_utils.py | 24 +++++++++++++++++ llama_cpp/llama_chat_format.py | 26 +++--------------- tests/test_image_url.py | 49 ++++++++++++++++++++++++++++++++++ 4 files changed, 77 insertions(+), 23 deletions(-) create mode 100644 tests/test_image_url.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ef9dbc67..9ee4aa254 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +- fix: raise a clear error for data URLs missing a comma and decode non-base64 payloads by @kevin9327 - fix(ci): use supported Python versions on Ubuntu by @abetlen in #2407 - feat: update llama.cpp to ggml-org/llama.cpp@7d7f02875 diff --git a/llama_cpp/_utils.py b/llama_cpp/_utils.py index 08ede6420..d0d8636d8 100644 --- a/llama_cpp/_utils.py +++ b/llama_cpp/_utils.py @@ -97,3 +97,27 @@ class Singleton(object, metaclass=MetaSingleton): def __init__(self): super(Singleton, self).__init__() + + +def _bytes_from_image_url(image_url: str) -> bytes: + """Load image bytes from a data URL or an http(s) URL.""" + if image_url.startswith("data:"): + import base64 + + if "," not in image_url: + raise ValueError( + "Invalid data URL: expected 'data:[;base64],'" + ) + header, data = image_url.split(",", 1) + if ";base64" in header.lower(): + return base64.b64decode(data) + # Non-base64 data URLs (e.g. data:image/svg+xml,...) carry the payload as text. + from urllib.parse import unquote_to_bytes + + return unquote_to_bytes(data) + + import urllib.request + + with urllib.request.urlopen(image_url) as f: + return f.read() + diff --git a/llama_cpp/llama_chat_format.py b/llama_cpp/llama_chat_format.py index 62e236415..73c83efde 100644 --- a/llama_cpp/llama_chat_format.py +++ b/llama_cpp/llama_chat_format.py @@ -35,7 +35,7 @@ import llama_cpp.llama_grammar as llama_grammar from ._logger import logger -from ._utils import suppress_stdout_stderr, Singleton +from ._utils import suppress_stdout_stderr, Singleton, _bytes_from_image_url ### Common Chat Templates and Special Tokens ### @@ -3124,17 +3124,7 @@ def __call__( @staticmethod def _load_image(image_url: str) -> bytes: # TODO: Add Pillow support for other image formats beyond (jpg, png) - if image_url.startswith("data:"): - import base64 - - image_bytes = base64.b64decode(image_url.split(",")[1]) - return image_bytes - else: - import urllib.request - - with urllib.request.urlopen(image_url) as f: - image_bytes = f.read() - return image_bytes + return _bytes_from_image_url(image_url) @staticmethod def get_image_urls(messages: List[llama_types.ChatCompletionRequestMessage]): @@ -3658,17 +3648,7 @@ def raise_exception(message: str): @staticmethod def _load_image(image_url: str) -> bytes: - if image_url.startswith("data:"): - import base64 - - image_bytes = base64.b64decode(image_url.split(",")[1]) - return image_bytes - else: - import urllib.request - - with urllib.request.urlopen(image_url) as f: - image_bytes = f.read() - return image_bytes + return _bytes_from_image_url(image_url) @staticmethod def get_image_urls(messages: List[llama_types.ChatCompletionRequestMessage]): diff --git a/tests/test_image_url.py b/tests/test_image_url.py new file mode 100644 index 000000000..21e53ac8b --- /dev/null +++ b/tests/test_image_url.py @@ -0,0 +1,49 @@ +import base64 +import importlib.util +from pathlib import Path + +import pytest + +_UTILS = Path(__file__).resolve().parents[1] / "llama_cpp" / "_utils.py" +_spec = importlib.util.spec_from_file_location("llama_cpp_utils_under_test", _UTILS) +_utils = importlib.util.module_from_spec(_spec) +_spec.loader.exec_module(_utils) +_bytes_from_image_url = _utils._bytes_from_image_url + + +def test_base64_data_url_round_trip(): + payload = b"\x89PNG\r\n" + url = "data:image/png;base64," + base64.b64encode(payload).decode("ascii") + assert _bytes_from_image_url(url) == payload + + +def test_base64_data_url_is_case_insensitive(): + payload = b"hello" + url = "data:image/png;BASE64," + base64.b64encode(payload).decode("ascii") + assert _bytes_from_image_url(url) == payload + + +def test_data_url_without_comma_raises(): + with pytest.raises(ValueError, match="Invalid data URL"): + _bytes_from_image_url("data:image/png;base64") + + +def test_plain_data_url_is_unquoted(): + assert _bytes_from_image_url("data:image/svg+xml,%3Csvg%3E") == b"" + + +def test_http_url(monkeypatch: pytest.MonkeyPatch): + import urllib.request + + class FakeResp: + def read(self): + return b"img" + + def __enter__(self): + return self + + def __exit__(self, *args): + return False + + monkeypatch.setattr(urllib.request, "urlopen", lambda url: FakeResp()) + assert _bytes_from_image_url("https://example.com/a.png") == b"img" From dd5cb9459eff92bfaba67951c4991552e625e1ba Mon Sep 17 00:00:00 2001 From: kevin9327 <5299031+kevin9327@users.noreply.github.com> Date: Thu, 12 Nov 2026 14:50:45 +0900 Subject: [PATCH 2/2] chore: add PR number to changelog entry --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ee4aa254..09d422908 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -- fix: raise a clear error for data URLs missing a comma and decode non-base64 payloads by @kevin9327 +- fix: raise a clear error for data URLs missing a comma and decode non-base64 payloads by @kevin9327 in #2413 - fix(ci): use supported Python versions on Ubuntu by @abetlen in #2407 - feat: update llama.cpp to ggml-org/llama.cpp@7d7f02875