Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 115 additions & 3 deletions Tests/test_file_xbm.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,111 @@ def test_pil151() -> None:
assert im.size == (32, 32)


@pytest.mark.parametrize("name", [b"basic_bits", b"basic_xbm", b"bitmap", b"_bitmap2"])
@pytest.mark.parametrize("hotspot", [False, True])
def test_array_name(name: bytes, hotspot: bool) -> None:
data = PIL151.replace(b"basic_bits", name)
if hotspot:
data = data.replace(
b"static char", b"#define basic_x_hot 3\n#define basic_y_hot 7\nstatic char"
)

with Image.open(BytesIO(PIL151)) as expected, Image.open(BytesIO(data)) as im:
im.load()
assert im.mode == "1"
assert im.size == expected.size
assert im.tobytes() == expected.tobytes()
if hotspot:
assert im.info["hotspot"] == (3, 7)
else:
assert "hotspot" not in im.info


@pytest.mark.parametrize("name", [b"basic_bits", b"bitmap"])
@pytest.mark.parametrize(
"suffix", [b"/* sample[] */", b"static char sample[] = {0xff};"]
)
def test_array_name_with_trailing_array(name: bytes, suffix: bytes) -> None:
data = (
b"#define basic_width 8\n#define basic_height 1\nstatic char "
+ name
+ b"[] = {0x55};\n"
+ suffix
)
with Image.open(BytesIO(data)) as im:
assert im.tobytes() == b"\xaa"


@pytest.mark.parametrize("name", [b"basic_bits", b"bitmap"])
@pytest.mark.parametrize(
"comment",
[b"/* sample[] = {0xff}; */", b"/* sample[] example */", b"// sample[] = {0xff};"],
)
def test_array_name_with_header_comment(name: bytes, comment: bytes) -> None:
data = (
b"#define basic_width 8\n#define basic_height 1\n"
+ comment
+ b"\nstatic char "
+ name
+ b"[] = {0x55};\n"
)
with Image.open(BytesIO(data)) as im:
assert im.tobytes() == b"\xaa"


@pytest.mark.parametrize(
"prefix",
[
b"static char metadata[] = {0xff};",
b"extern char external[]; /* example 0xff */",
b'static const char label[] = "example";',
b'char *label = "/*";',
],
)
def test_conventional_array_with_preceding_declaration(prefix: bytes) -> None:
data = (
b"#define basic_width 8\n#define basic_height 1\n"
+ prefix
+ b"\nstatic char basic_bits[] = {0x55};\n"
)
with Image.open(BytesIO(data)) as im:
assert im.tobytes() == b"\xaa"


@pytest.mark.parametrize(
"prefix",
[
b"extern char external[];",
b'static char label[] = "example";',
b'static char label[] = "/*";',
b'static char label[] = "sample[] = {0xff}";',
b'static char label[] = "\\"sample[] = {0xff}";',
b"static char label = '\\'';",
],
)
def test_nonconventional_array_with_preceding_declaration(prefix: bytes) -> None:
data = (
b"#define basic_width 8\n#define basic_height 1\n"
+ prefix
+ b"\nstatic unsigned char bitmap[] = {0x55};\n"
)
with Image.open(BytesIO(data)) as im:
assert im.tobytes() == b"\xaa"


@pytest.mark.timeout(3)
@pytest.mark.parametrize("declaration", [b"", b"static char bitmap[] = {0x55};"])
@pytest.mark.parametrize("comment", [b"/**/", b"//"])
def test_repeated_header_comments(declaration: bytes, comment: bytes) -> None:
data = b"#define basic_width 8\n#define basic_height 1\n" + comment * 60 + b"\n"
if declaration:
with Image.open(BytesIO(data + declaration)) as im:
assert im.tobytes() == b"\xaa"
else:
with pytest.raises(SyntaxError):
XbmImagePlugin.XbmImageFile(BytesIO(data))


def test_open() -> None:
# Arrange
# Created with `convert hopper.png hopper.xbm`
Expand Down Expand Up @@ -74,6 +179,13 @@ def test_invalid_file() -> None:
XbmImagePlugin.XbmImageFile(invalid_file)


@pytest.mark.parametrize("declaration", [b"static char bitmap", b"static char []"])
def test_missing_array_name_or_brackets(declaration: bytes) -> None:
data = b"#define basic_width 8\n#define basic_height 1\n" + declaration
with pytest.raises(SyntaxError):
XbmImagePlugin.XbmImageFile(BytesIO(data))


def test_save_wrong_mode(tmp_path: Path) -> None:
im = hopper()
out = tmp_path / "temp.xbm"
Expand All @@ -82,12 +194,12 @@ def test_save_wrong_mode(tmp_path: Path) -> None:
im.save(out)


def test_hotspot(tmp_path: Path) -> None:
@pytest.mark.parametrize("hotspot", [None, (0, 7)])
def test_hotspot(tmp_path: Path, hotspot: tuple[int, int] | None) -> None:
im = hopper("1")
out = tmp_path / "temp.xbm"

hotspot = (0, 7)
im.save(out, hotspot=hotspot)

with Image.open(out) as reloaded:
assert reloaded.info["hotspot"] == hotspot
assert reloaded.info.get("hotspot") == hotspot
5 changes: 5 additions & 0 deletions docs/releasenotes/13.0.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,11 @@ escape sequences.
Other changes
=============

XBM array names
^^^^^^^^^^^^^^^

XBM images can now be opened when their data array name does not end in ``_bits``.

Python 3.15
^^^^^^^^^^^

Expand Down
6 changes: 5 additions & 1 deletion src/PIL/XbmImagePlugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,11 @@
b"#define[ \t]+[^_]*_x_hot[ \t]+(?P<xhot>[0-9]+)[\r\n]+"
b"#define[ \t]+[^_]*_y_hot[ \t]+(?P<yhot>[0-9]+)[\r\n]+"
b")?"
rb"[\000-\377]*_bits\[]"
rb"(?:[\000-\377]*_bits\[]|"
rb"(?:/\*(?:[^*]|\*(?!/))*\*/|//[^\r\n]*(?:[\r\n]|$)|"
rb'"(?:\\[\000-\377]|[^"\\])*"|'
rb"'(?:\\[\000-\377]|[^'\\])*'|/(?![/*])|[^/\"'])*?"
rb"[a-zA-Z_][a-zA-Z0-9_]*\[](?=\s*=\s*\{))"
)


Expand Down
Loading