Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ jobs:
matrix:
just-trigger:
- "lint"
# - "tests"
- "tests"
python-version:
- "3.10"
- "3.11"
Expand Down
117 changes: 87 additions & 30 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,58 +6,115 @@ Oversimplified Github-like userpic (avatar) generator.
[![PyPI](https://img.shields.io/pypi/v/tiny-userpic.svg)](https://pypi.python.org/pypi/tiny-userpic)
[![PyPI](https://img.shields.io/pypi/dm/tiny-userpic.svg)](https://pypi.python.org/pypi/tiny-userpic)

## Installation
## 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)

Get started by installing the library via pip:
## Installation

```bash
pip install tiny-userpic
```

## Create a PIL Image
## Usage

```python
from PIL.Image import Image
The library provides several ways to generate avatars:

### 1. Random Generation (Non-deterministic)
Generate a unique random avatar each time.

from userpic import make_userpic_image
```python
from tiny_userpic import make_userpic_image

# Generate a PIL Image object
image: Image = make_userpic_image(
# Generate random avatar
random_image = make_userpic_image(
size=(7, 5),
padding=(20, 10),
mode='RGB',
image_size=(300, 300),
background='white',
foreground='black',
background="white",
foreground="black"
)

# save as JPEG file
with open('output.jpeg', 'wb') as fp:
image.save(fp)
random_image.save("random_avatar.png")
```

## Create SVG Data
### 2. With Custom Seed (Deterministic)
Generate an avatar with a specific seed for reproducible results.

```python
from tiny_userpic import make_userpic_image

from userpic import make_userpic_svg

# Generate SVG string data
image: str = make_userpic_svg(
# Generate avatar with specific seed
seeded_image = make_userpic_image(
size=(7, 5),
padding=(20, 10),
image_size=(300, 300),
background='white',
foreground='black',
background="white",
foreground="black",
seed=42 # Any integer value will work as seed
)
seeded_image.save("seeded_avatar.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.

```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")

# save as SVG file
with open('output.svg', 'w') as fp:
fp.write(image)
# 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)
```

## Example Output
### 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)

## Examples

### Basic (from string)
![Basic example](examples/basic.png)

### Colored
![Colored example](examples/colored.png)

### Transparent
![Transparent example](examples/transparent.png)

### Small
![Small example](examples/small.png)

### Large
![Large example](examples/large.png)

Check out the awesome userpic you can generate:
### Random (non-deterministic)
![Random example](examples/random.png)

![Awesome generated userpic!](example.png)
### Seeded (deterministic)
![Seeded example](examples/seeded.png)
Binary file removed example.png
Binary file not shown.
Binary file added examples/basic.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added examples/colored.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added examples/large.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added examples/random.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added examples/seeded.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added examples/small.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added examples/transparent.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
84 changes: 83 additions & 1 deletion justfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
SOURCE_PATH := "userpic.py"
TEST_PATH := "test_userpic.py"

upgrade:
uv lock --upgrade
Expand All @@ -8,5 +9,86 @@ lint:
uv run python -m mypy --pretty {{ SOURCE_PATH }}

fix:
uv run ruff format {{ SOURCE_PATH }}
uv run ruff format {{ SOURCE_PATH }} {{ TEST_PATH }}
uv run ruff check --fix --unsafe-fixes {{ SOURCE_PATH }}

tests:
uv run pytest test_userpic.py


# Generate images with different parameters
examples:
#!/usr/bin/env bash
.venv/bin/python -c '
from userpic import make_userpic_image
# Basic black and white image
make_userpic_image(
size=(7, 5),
mode="RGB",
image_size=(300, 300),
padding=(20, 20),
background="white",
foreground="black"
).save("examples/basic.png")

# Colored image
make_userpic_image(
size=(7, 5),
mode="RGB",
image_size=(300, 300),
padding=(20, 20),
background="#f0f0f0",
foreground="#2ecc71"
).save("examples/colored.png")

# Image with transparent background
make_userpic_image(
size=(7, 5),
mode="RGBA",
image_size=(300, 300),
padding=(20, 20),
background=(255, 255, 255, 0),
foreground=(0, 0, 128, 255)
).save("examples/transparent.png")

# Image with large pattern size
make_userpic_image(
size=(12, 12),
mode="RGB",
image_size=(300, 300),
padding=(20, 20),
background="white",
foreground="#e74c3c"
).save("examples/large.png")

# Image with small pattern size
make_userpic_image(
size=(5, 5),
mode="RGB",
image_size=(300, 300),
padding=(20, 20),
background="white",
foreground="#f1c40f"
).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),
mode="RGB",
image_size=(300, 300),
padding=(20, 20),
background="white",
foreground="#1abc9c",
seed=42
).save("examples/seeded.png")
'
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ build-backend = "hatchling.build"
[dependency-groups]
dev = [
"mypy>=1.13.0",
"pytest>=8.3.5",
"ruff>=0.8.2",
]

Expand All @@ -47,6 +48,7 @@ exclude = [
]
lint.ignore = [
"PLR0913", # Too many arguments to function call
"S311", # Standard pseudo-random generators are not suitable for cryptographic purposes
]
lint.flake8-tidy-imports.ban-relative-imports = "all"
lint.mccabe.max-complexity = 20
Expand Down
111 changes: 111 additions & 0 deletions test_userpic.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
from typing import Any

import pytest
from PIL.Image import Image

from userpic import (
make_userpic_image,
make_userpic_image_from_string,
make_userpic_svg,
make_userpic_svg_from_string,
)


@pytest.fixture
def default_params() -> dict[str, Any]:
return {
'size': (7, 5),
'image_size': (300, 300),
'padding': (20, 20),
'mode': 'RGB',
'background': 'white',
'foreground': 'black',
}


def test_make_userpic_image(default_params: dict[str, Any]) -> None:
image = make_userpic_image(**default_params)
assert isinstance(image, Image)
assert image.size == default_params['image_size']
assert image.mode == default_params['mode']


def test_make_userpic_svg(default_params: dict[str, Any]) -> None:
params = {k: v for k, v in default_params.items() if k != 'mode'}
svg = make_userpic_svg(**params)
assert isinstance(svg, str)
assert '<svg' in svg
assert 'rect' in svg


def test_seed_reproducibility(default_params: dict[str, Any]) -> None:
seed = 42
image1 = make_userpic_image(**default_params, seed=seed)
image2 = make_userpic_image(**default_params, seed=seed)
assert image1.tobytes() == image2.tobytes()


def test_different_seeds(default_params: dict[str, Any]) -> None:
image1 = make_userpic_image(**default_params, seed=1)
image2 = make_userpic_image(**default_params, seed=2)
assert image1.tobytes() != image2.tobytes()


def test_string_based_image_consistency(default_params: dict[str, Any]) -> None:
text = 'test@example.com'
params = {k: v for k, v in default_params.items() if k not in ['seed']}
image1 = make_userpic_image_from_string(text=text, **params)
image2 = make_userpic_image_from_string(text=text, **params)
assert image1.tobytes() == image2.tobytes()


def test_string_based_svg_consistency(default_params: dict[str, Any]) -> None:
text = 'test@example.com'
params = {k: v for k, v in default_params.items() if k not in ['seed', 'mode']}
svg1 = make_userpic_svg_from_string(text=text, **params)
svg2 = make_userpic_svg_from_string(text=text, **params)
assert svg1 == svg2


def test_different_strings_different_results(default_params: dict[str, Any]) -> None:
params = {k: v for k, v in default_params.items() if k not in ['seed']}
image1 = make_userpic_image_from_string(text='user1@example.com', **params)
image2 = make_userpic_image_from_string(text='user2@example.com', **params)
assert image1.tobytes() != image2.tobytes()


@pytest.mark.parametrize(
'size',
[
(5, 5),
(7, 7),
(9, 9),
],
)
def test_different_sizes(default_params: dict[str, Any], size: tuple[int, int]) -> None:
params = default_params.copy()
params['size'] = size
image = make_userpic_image(**params)
assert isinstance(image, Image)


@pytest.mark.parametrize('mode', ['RGB', 'RGBA', 'L'])
def test_different_modes(default_params: dict[str, Any], mode: str) -> None:
params = default_params.copy()
params['mode'] = mode
image = make_userpic_image(**params)
assert image.mode == mode


def test_svg_structure(default_params: dict[str, Any]) -> None:
params = {k: v for k, v in default_params.items() if k != 'mode'}
svg = make_userpic_svg(**params)
assert '<svg' in svg
assert 'xmlns="http://www.w3.org/2000/svg"' in svg
assert '</svg>' in svg
assert 'rect' in svg


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)
Loading