diff --git a/.github/workflows/pypi.yml b/.github/workflows/pypi.yml
deleted file mode 100644
index 73e74fb..0000000
--- a/.github/workflows/pypi.yml
+++ /dev/null
@@ -1,25 +0,0 @@
----
-name: 👷🏼 pypi
-
-on:
- release:
- types: [ created ]
-
-jobs:
- build:
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v4
- - name: Extract branch name
- shell: bash
- run: |
- echo "##[set-output name=ver;]$(echo ${GITHUB_REF#refs/*/})"
- id: extract_name_and_version
- - run: sed -i 's/0.0.0/'"${{ steps.extract_name_and_version.outputs.ver }}"'/' pyproject.toml
- - run: head -n 10 pyproject.toml
- - run: sed -i 's/0.0.0/'"${{ steps.extract_name_and_version.outputs.ver }}"'/' userpic.py
- - uses: astral-sh/setup-uv@v4
- - run: uv python install 3.12
- - run: uv sync --all-extras --dev
- - run: uv build
- - run: uv publish --username __token__ --password ${{ secrets.PYPI_TOKEN }}
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 0000000..d782ee6
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,91 @@
+---
+name: 🚀 release
+
+on:
+ push:
+ branches: [ main ]
+
+concurrency:
+ group: release
+ cancel-in-progress: false
+
+permissions:
+ contents: read
+
+jobs:
+ check:
+ runs-on: ubuntu-latest
+ env:
+ UV_PYTHON: "3.13"
+ steps:
+ - uses: actions/checkout@v4
+ - uses: extractions/setup-just@v2
+ - uses: astral-sh/setup-uv@v4
+ - run: uv python install "${UV_PYTHON}"
+ - run: uv sync --all-extras --dev
+ - run: just lint
+ - run: just tests
+ - name: Check that the version files agree
+ run: |
+ version="$(uv run cz version --project)"
+ grep -qx "__version__ = '${version}'" userpic.py
+
+ tag:
+ needs: check
+ runs-on: ubuntu-latest
+ permissions:
+ contents: write
+ outputs:
+ version: ${{ steps.tag.outputs.version }}
+ env:
+ UV_PYTHON: "3.13"
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+ - uses: astral-sh/setup-uv@v4
+ - run: uv python install "${UV_PYTHON}"
+ - run: uv sync --all-extras --dev
+ - name: Tag the release
+ id: tag
+ run: |
+ version="$(uv run cz version --project)"
+ if git rev-parse -q --verify "refs/tags/${version}" >/dev/null; then
+ echo "${version} is already tagged, nothing to release"
+ exit 0
+ fi
+ git config user.name "github-actions[bot]"
+ git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
+ git tag -a "${version}" -m "${version}"
+ git push origin "${version}"
+ echo "version=${version}" >> "${GITHUB_OUTPUT}"
+
+ publish:
+ needs: tag
+ if: needs.tag.outputs.version != ''
+ runs-on: ubuntu-latest
+ permissions:
+ contents: write
+ env:
+ UV_PYTHON: "3.13"
+ VERSION: ${{ needs.tag.outputs.version }}
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ ref: ${{ needs.tag.outputs.version }}
+ fetch-depth: 0
+ - uses: astral-sh/setup-uv@v4
+ - run: uv python install "${UV_PYTHON}"
+ - run: uv sync --all-extras --dev
+ - name: Build and check that the artifacts match the tag
+ run: |
+ uv build
+ test -f "dist/tiny_userpic-${VERSION}-py3-none-any.whl"
+ test -f "dist/tiny_userpic-${VERSION}.tar.gz"
+ - run: uv publish --username __token__ --password ${{ secrets.PYPI_TOKEN }}
+ - name: Publish the github release
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ run: |
+ uv run cz changelog "${VERSION}" --dry-run > notes.md
+ gh release create "${VERSION}" --title "${VERSION}" --notes-file notes.md dist/*
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 0698e4e..3ca9ad5 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -2,26 +2,45 @@
name: 🧐 test
on:
+ push:
+ branches: [ main ]
pull_request:
branches: [ main ]
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
jobs:
+ lint:
+ runs-on: ubuntu-latest
+ env:
+ UV_PYTHON: "3.13"
+ steps:
+ - uses: actions/checkout@v4
+ - uses: extractions/setup-just@v2
+ - uses: astral-sh/setup-uv@v4
+ - run: uv python install "${UV_PYTHON}"
+ - run: uv sync --all-extras --dev
+ - run: just lint
+
tests:
runs-on: ubuntu-latest
strategy:
+ fail-fast: false
matrix:
- just-trigger:
- - "lint"
- - "tests"
python-version:
- "3.10"
- "3.11"
- "3.12"
- "3.13"
+ - "3.14"
+ env:
+ UV_PYTHON: ${{ matrix.python-version }}
steps:
- uses: actions/checkout@v4
- uses: extractions/setup-just@v2
- uses: astral-sh/setup-uv@v4
- - run: uv python install ${{ matrix.python-version }}
+ - run: uv python install "${UV_PYTHON}"
- run: uv sync --all-extras --dev
- - run: just ${{ matrix.just-trigger }}
+ - run: just tests
diff --git a/.gitignore b/.gitignore
index 5a1bb6a..402885d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,5 @@
- .png
-.svg
+/*.png
+/*.svg
.idea/
# Byte-compiled / optimized / DLL files
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 184b7be..6731c65 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -11,7 +11,7 @@ repos:
language: system
'types_or': [ python, pyi ]
- repo: https://github.com/astral-sh/ruff-pre-commit
- rev: "v0.8.1"
+ rev: "v0.8.2"
hooks:
- id: ruff
args: [ --fix, --unsafe-fixes ]
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..d277074
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,58 @@
+# Changelog
+
+All notable changes to this project are documented in this file.
+
+Entries from 1.0.0 onwards are generated from [conventional commits](https://www.conventionalcommits.org/)
+by [commitizen](https://commitizen-tools.github.io/commitizen/): run `just bump` in the pull request
+to update the version and this file, and the release workflow tags and publishes it once the pull
+request lands on `main`. Earlier releases predate that convention and are summarised by hand.
+
+## 1.0.0 (2026-08-22)
+
+### BREAKING CHANGE
+
+- the padding default changed from (0, 0) to (20, 20) in
+make_userpic_svg and in both *_from_string functions, make_userpic was removed,
+and degenerate size/image_size/padding values now raise ValueError instead of
+hanging or returning a broken image. The rendered pixels and the SVG markup
+differ from 0.4.0, although the pattern for a given seed or string is unchanged.
+
+### Feat
+
+- declare the api stable and automate releases
+
+### Fix
+
+- pattern rendering, input validation and svg output
+
+## 0.4.0 (2025-03-24)
+
+### Feat
+
+- add `make_userpic_image_from_string` and `make_userpic_svg_from_string`, deriving the pattern from
+ a SHA-256 digest of the input text
+- add the `seed` parameter to the image and SVG generators for reproducible output
+- add a test suite and the example images used by the readme
+
+## 0.3.0 (2024-12-12)
+
+### Refactor
+
+- rewrite the library as a single `userpic.py` module
+- drop the command line interface, leaving a library-only package
+- make Pillow the only runtime dependency
+- move the tooling to uv and just
+
+Patch releases 0.3.1, 0.3.2 and 0.3.3 only touched the readme and the packaging metadata.
+
+## 0.2.0 (2022-07-22)
+
+### Refactor
+
+- make CairoSVG an optional dependency instead of a required one
+- replace typer with click in the command line interface
+
+## 0.1.1 (2021-07-03)
+
+Initial release: a `userpic` package with an SVG generator, a Cairo-backed PNG renderer and a
+typer-based command line interface.
diff --git a/README.md b/README.md
index ac077d0..0be2f3e 100644
--- a/README.md
+++ b/README.md
@@ -6,12 +6,9 @@ Oversimplified Github-like userpic (avatar) generator.
[](https://pypi.python.org/pypi/tiny-userpic)
[](https://pypi.python.org/pypi/tiny-userpic)
-## Features
-
-- Generate unique avatars from text input (email, username, etc.)
-- Create both PIL Image and SVG outputs
-- Customizable size, colors, and padding
-- Deterministic output (same input always produces the same avatar)
+Turns any text — an email, a username, a UUID — into a symmetric identicon-style
+avatar, rendered as a PIL image or an SVG string. The same input always produces
+the same picture. The only dependency is Pillow.
## Installation
@@ -19,102 +16,92 @@ Oversimplified Github-like userpic (avatar) generator.
pip install tiny-userpic
```
-## Usage
-
-The library provides several ways to generate avatars:
+Requires Python 3.10 or newer. The package installs a single top-level module
+named `userpic`.
-### 1. Random Generation (Non-deterministic)
-Generate a unique random avatar each time.
+## Quickstart
```python
-from tiny_userpic import make_userpic_image
+from userpic import make_userpic_image_from_string
-# Generate random avatar
-random_image = make_userpic_image(
- size=(7, 5),
- image_size=(300, 300),
- background="white",
- foreground="black"
-)
-random_image.save("random_avatar.png")
+make_userpic_image_from_string(text="user@example.com").save("avatar.png")
```
-### 2. With Custom Seed (Deterministic)
-Generate an avatar with a specific seed for reproducible results.
+
+
+## API
+
+Four functions share the same parameters and differ only in the output format
+and in where the pattern comes from:
+
+| | returns | pattern comes from |
+|----------------------------------|-------------|--------------------------|
+| `make_userpic_image` | `PIL.Image` | `seed` or system entropy |
+| `make_userpic_svg` | `str` (SVG) | `seed` or system entropy |
+| `make_userpic_image_from_string` | `PIL.Image` | `text` |
+| `make_userpic_svg_from_string` | `str` (SVG) | `text` |
+
+### Images
```python
-from tiny_userpic import make_userpic_image
+from userpic import make_userpic_image
-# Generate avatar with specific seed
-seeded_image = make_userpic_image(
+# a new random avatar on every call
+make_userpic_image(size=(7, 5)).save("random.png")
+
+# the same integer seed always gives the same avatar
+make_userpic_image(size=(7, 5), seed=42).save("seeded.png")
+
+# transparency: RGBA mode plus a fully transparent background
+make_userpic_image(
size=(7, 5),
- image_size=(300, 300),
- background="white",
- foreground="black",
- seed=42 # Any integer value will work as seed
-)
-seeded_image.save("seeded_avatar.png")
+ mode="RGBA",
+ background=(255, 255, 255, 0),
+ foreground=(0, 0, 128, 255),
+).save("transparent.png")
```
-### 3. From Text Input (Deterministic)
-Generate an avatar from any text input (email, username, etc.). The same input will always produce the same avatar.
+### SVG
```python
-from tiny_userpic import make_userpic_image_from_string, make_userpic_svg_from_string
-
-# Generate avatar from email
-email = "user@example.com"
-
-# As PNG image
-image = make_userpic_image_from_string(
- text=email, # Input text to generate avatar from
- size=(7, 5), # Pattern size (width, height)
- image_size=(300, 300), # Output image size in pixels
- background="white", # Background color (can be color name, hex or RGB tuple)
- foreground="black" # Foreground color (can be color name, hex or RGB tuple)
-)
-image.save("avatar.png")
-
-# As SVG
-svg = make_userpic_svg_from_string(
- text=email,
- size=(7, 5),
- image_size=(300, 300),
- background="white",
- foreground="black"
-)
-with open("avatar.svg", "w") as f:
- f.write(svg)
+from userpic import make_userpic_svg, make_userpic_svg_from_string
+
+svg = make_userpic_svg_from_string(text="user@example.com", size=(7, 5))
+with open("avatar.svg", "w") as file:
+ file.write(svg)
+
+# seeded, without the background rectangle
+svg = make_userpic_svg(size=(7, 5), background=None, seed=42)
```
-### Common Parameters
-All generation methods share these parameters:
-- `size`: Tuple of (width, height) for the pattern size
-- `image_size`: Tuple of (width, height) for the output image size in pixels
-- `background`: Background color (can be color name, hex or RGB tuple)
-- `foreground`: Foreground color (can be color name, hex or RGB tuple)
-- `padding`: Optional padding around the pattern (default: (20, 20))
-- `mode`: Image mode for PNG output (default: 'RGB', can be 'RGBA' for transparency)
+### Parameters
-## Examples
+| name | default | description |
+|--------------|--------------|-------------------------------------------------------------------------------------------|
+| `text` | — | input string for the `*_from_string` functions |
+| `size` | `(5, 5)` | pattern size in cells as (width, height); the width must be at least 2 |
+| `image_size` | `(300, 300)` | output size in pixels |
+| `padding` | `(20, 20)` | blank border around the pattern, in pixels |
+| `background` | `"white"` | color name, hex string or RGB/RGBA tuple; `None` omits the background rectangle in SVG |
+| `foreground` | `"black"` | same formats as `background` |
+| `mode` | `"RGB"` | PIL image mode, image functions only; use `"RGBA"` for transparency |
+| `seed` | `None` | any integer for reproducible output; omit for a random avatar |
-### Basic (from string)
-
+The pattern has to fit into the image: `image_size` minus twice the `padding`
+must leave at least one pixel per cell, otherwise a `ValueError` is raised.
-### Colored
-
+## Examples
-### Transparent
-
+| Basic | Colored | Transparent |
+|--------------------------------|------------------------------------|--------------------------------------------|
+|  |  |  |
-### Small
-
+| Small | Large | Seeded |
+|--------------------------------|------------------------------------|--------------------------------------------|
+|  |  |  |
-### Large
-
+The images are generated by `just examples`.
-### Random (non-deterministic)
-
+## License
-### Seeded (deterministic)
-
+[MIT](LICENSE)
diff --git a/examples/basic.png b/examples/basic.png
index 104ae48..2bb5a07 100644
Binary files a/examples/basic.png and b/examples/basic.png differ
diff --git a/examples/colored.png b/examples/colored.png
index f7ab0ee..da45c72 100644
Binary files a/examples/colored.png and b/examples/colored.png differ
diff --git a/examples/large.png b/examples/large.png
index adcea51..c883d62 100644
Binary files a/examples/large.png and b/examples/large.png differ
diff --git a/examples/quickstart.png b/examples/quickstart.png
new file mode 100644
index 0000000..87ca4a7
Binary files /dev/null and b/examples/quickstart.png differ
diff --git a/examples/random.png b/examples/random.png
deleted file mode 100644
index 79e1bb7..0000000
Binary files a/examples/random.png and /dev/null differ
diff --git a/examples/seeded.png b/examples/seeded.png
index 0aa2a52..7fa0d7d 100644
Binary files a/examples/seeded.png and b/examples/seeded.png differ
diff --git a/examples/small.png b/examples/small.png
index 3961db3..655fc93 100644
Binary files a/examples/small.png and b/examples/small.png differ
diff --git a/examples/transparent.png b/examples/transparent.png
index f7dda33..a7ac8c4 100644
Binary files a/examples/transparent.png and b/examples/transparent.png differ
diff --git a/justfile b/justfile
index d412c4d..94e4ca8 100644
--- a/justfile
+++ b/justfile
@@ -5,24 +5,41 @@ upgrade:
uv lock --upgrade
lint:
- uv run ruff check {{ SOURCE_PATH }}
- uv run python -m mypy --pretty {{ SOURCE_PATH }}
+ uv run ruff check {{ SOURCE_PATH }} {{ TEST_PATH }}
+ uv run python -m mypy --pretty {{ SOURCE_PATH }} {{ TEST_PATH }}
fix:
uv run ruff format {{ SOURCE_PATH }} {{ TEST_PATH }}
- uv run ruff check --fix --unsafe-fixes {{ SOURCE_PATH }}
+ uv run ruff check --fix --unsafe-fixes {{ SOURCE_PATH }} {{ TEST_PATH }}
tests:
- uv run pytest test_userpic.py
+ uv run pytest
+# Bump the version and the changelog from the conventional commits since the last tag
+bump:
+ #!/usr/bin/env bash
+ set -euo pipefail
+ previous="$(uv run cz version --project)"
+ uv run cz bump --changelog --files-only --yes
+ version="$(uv run cz version --project)"
+ git add CHANGELOG.md pyproject.toml userpic.py
+ git commit -m "bump: version ${previous} → ${version}"
-# Generate images with different parameters
+# Generate the images used in the readme
examples:
#!/usr/bin/env bash
- .venv/bin/python -c '
- from userpic import make_userpic_image
- # Basic black and white image
- make_userpic_image(
+ set -euo pipefail
+ uv run python -c '
+ from userpic import make_userpic_image, make_userpic_image_from_string
+
+ # The quickstart snippet from the readme, defaults only
+ make_userpic_image_from_string(
+ text="user@example.com"
+ ).save("examples/quickstart.png")
+
+ # Basic black and white image, generated from a string
+ make_userpic_image_from_string(
+ text="user@example.com",
size=(7, 5),
mode="RGB",
image_size=(300, 300),
@@ -38,7 +55,8 @@ examples:
image_size=(300, 300),
padding=(20, 20),
background="#f0f0f0",
- foreground="#2ecc71"
+ foreground="#2ecc71",
+ seed=1
).save("examples/colored.png")
# Image with transparent background
@@ -48,7 +66,8 @@ examples:
image_size=(300, 300),
padding=(20, 20),
background=(255, 255, 255, 0),
- foreground=(0, 0, 128, 255)
+ foreground=(0, 0, 128, 255),
+ seed=2
).save("examples/transparent.png")
# Image with large pattern size
@@ -58,7 +77,8 @@ examples:
image_size=(300, 300),
padding=(20, 20),
background="white",
- foreground="#e74c3c"
+ foreground="#e74c3c",
+ seed=3
).save("examples/large.png")
# Image with small pattern size
@@ -68,19 +88,10 @@ examples:
image_size=(300, 300),
padding=(20, 20),
background="white",
- foreground="#f1c40f"
+ foreground="#f1c40f",
+ seed=4
).save("examples/small.png")
- # Random image
- make_userpic_image(
- size=(7, 5),
- mode="RGB",
- image_size=(300, 300),
- padding=(20, 20),
- background="white",
- foreground="#9b59b6"
- ).save("examples/random.png")
-
# Image with fixed seed
make_userpic_image(
size=(7, 5),
diff --git a/pyproject.toml b/pyproject.toml
index 543e7c7..5fed1b8 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "tiny-userpic"
-version = "0.0.0"
+version = "1.0.0"
description = "Oversimplified Github-like userpic (avatar) generator"
readme = "README.md"
requires-python = ">=3.10"
@@ -12,13 +12,17 @@ authors = [
{ name = "Aleksandr Shpak", email = "shpaker@gmail.com" },
]
classifiers = [
- "Topic :: Software Development :: Libraries :: Python Modules",
- "Development Status :: 4 - Beta",
+ "Development Status :: 5 - Production/Stable",
+ "Intended Audience :: Developers",
+ "Operating System :: OS Independent",
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
+ "Programming Language :: Python :: 3.14",
+ "Topic :: Multimedia :: Graphics",
+ "Topic :: Software Development :: Libraries :: Python Modules",
]
keywords = ["avatar", "userpic", "avatar-generator"]
@@ -26,6 +30,16 @@ keywords = ["avatar", "userpic", "avatar-generator"]
Homepage = "https://github.com/shpaker/tiny-userpic/"
Repository = "https://github.com/shpaker/tiny-userpic/"
+[tool.commitizen]
+name = "cz_conventional_commits"
+version_provider = "pep621"
+version_files = [
+ "userpic.py:^__version__",
+]
+tag_format = "$version"
+annotated_tag = true
+update_changelog_on_bump = true
+
[tool.hatch.build]
include = ["userpic.py"]
@@ -35,6 +49,7 @@ build-backend = "hatchling.build"
[dependency-groups]
dev = [
+ "commitizen>=4.18.0",
"mypy>=1.13.0",
"pytest>=8.3.5",
"ruff>=0.8.2",
@@ -110,6 +125,19 @@ lint.select = [
"RUF", # Ruff-specific rules
]
+lint.per-file-ignores."test_userpic.py" = [
+ "S101", # Use of `assert` detected
+ "S318", # the parsed XML is produced by the library itself, not by an untrusted source
+]
+
[tool.ruff.format]
quote-style = "single"
docstring-code-format = true
+
+[tool.mypy]
+python_version = "3.10"
+strict = true
+
+[tool.pytest.ini_options]
+testpaths = ["test_userpic.py"]
+addopts = "-q"
diff --git a/test_userpic.py b/test_userpic.py
index a629e64..6e76dc8 100644
--- a/test_userpic.py
+++ b/test_userpic.py
@@ -1,9 +1,16 @@
+import hashlib
+import re
+from collections.abc import Callable
from typing import Any
+from xml.dom.minidom import parseString
import pytest
+from PIL import ImageOps
from PIL.Image import Image
from userpic import (
+ _cell_edges,
+ _iter_bit_lines,
make_userpic_image,
make_userpic_image_from_string,
make_userpic_svg,
@@ -23,6 +30,33 @@ def default_params() -> dict[str, Any]:
}
+def _svg_cells(svg: str) -> set[tuple[int, int]]:
+ body = svg.split('', body):
+ cells.update((int(x) + offset, int(y)) for offset in range(int(span)))
+ return cells
+
+
+def _image_cells(
+ image: Image,
+ size: tuple[int, int],
+ image_size: tuple[int, int],
+ padding: tuple[int, int],
+) -> set[tuple[int, int]]:
+ cell_width = (image_size[0] - 2 * padding[0]) / size[0]
+ cell_height = (image_size[1] - 2 * padding[1]) / size[1]
+ return {
+ (x, y)
+ for y in range(size[1])
+ for x in range(size[0])
+ if image.getpixel(
+ (int(padding[0] + (x + 0.5) * cell_width), int(padding[1] + (y + 0.5) * cell_height)),
+ )
+ == (0, 0, 0)
+ }
+
+
def test_make_userpic_image(default_params: dict[str, Any]) -> None:
image = make_userpic_image(**default_params)
assert isinstance(image, Image)
@@ -109,3 +143,192 @@ def test_svg_structure(default_params: dict[str, Any]) -> None:
def test_empty_string() -> None:
image = make_userpic_image_from_string(text='', size=(7, 5), mode='RGB', image_size=(300, 300))
assert isinstance(image, Image)
+
+
+@pytest.mark.parametrize(
+ 'size',
+ [
+ (7, 5),
+ (8, 6),
+ (5, 5),
+ (2, 1),
+ ],
+)
+@pytest.mark.parametrize('image_size', [(300, 300), (280, 300), (301, 299), (256, 256)])
+def test_image_is_mirror_symmetric(size: tuple[int, int], image_size: tuple[int, int]) -> None:
+ for seed in range(10):
+ image = make_userpic_image(size=size, image_size=image_size, padding=(0, 0), seed=seed)
+ assert image.tobytes() == ImageOps.mirror(image).tobytes()
+
+
+@pytest.mark.parametrize('size', [(7, 5), (8, 6), (12, 12)])
+def test_svg_matches_image(size: tuple[int, int]) -> None:
+ image_size, padding = (300, 300), (20, 20)
+ for seed in range(10):
+ svg = make_userpic_svg(size=size, image_size=image_size, padding=padding, seed=seed)
+ image = make_userpic_image(size=size, image_size=image_size, padding=padding, seed=seed)
+ assert _svg_cells(svg) == _image_cells(image, size, image_size, padding)
+
+
+@pytest.mark.parametrize(
+ 'color',
+ [
+ 'red"/>",
+ 'red&<>',
+ ],
+)
+def test_svg_escapes_colors(color: str) -> None:
+ svg = make_userpic_svg(size=(5, 5), background=color, foreground=color, seed=1)
+ assert '