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
30 changes: 30 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: Publish to PyPI

on:
push:
tags: ["v*"]

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v5
- run: uv sync --all-extras
- run: uv run --no-sync pytest -q

publish:
needs: test
runs-on: ubuntu-latest
environment: pypi
permissions:
id-token: write # PyPI trusted publishing — no API token secrets
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v5
- name: Build sdist and wheel
run: uv build
- name: Check metadata renders
run: uvx twine check dist/*
- name: Publish
uses: pypa/gh-action-pypi-publish@release/v1
52 changes: 31 additions & 21 deletions README.MD
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,16 @@ Convert images into colorized ASCII art. This project provides two main tools fo
Requires Python 3.10+.

```bash
pip install -e . # CLI tools only
pip install -e ".[web]" # CLI tools + web GUI
pipx install ascii-magic-tools # from PyPI (CLI tools)
pip install "ascii-magic-tools[web]" # + web GUI
pip install "ascii-magic-tools[video]" # + video support

# from a checkout:
pip install -e ".[web,video]"
```

The import package is `asciimagic` (the PyPI project `ascii-magic` is an unrelated package that owns the `ascii_magic` import name).

## Quick Start (Unified CLI)

The `ascii-magic` command is the recommended entry point. It provides all tools under one command:
Expand Down Expand Up @@ -75,6 +81,10 @@ python3 -m venv .venv

Always create the virtualenv as `.venv/` inside the repo — never at the repo root — so editors and tools auto-detect it and git ignores it.

### Releasing

Bump `version` in `pyproject.toml` and `__version__` in `src/asciimagic/__init__.py`, merge, then tag: `git tag v0.3.0 && git push origin v0.3.0`. The publish workflow runs the tests, builds sdist+wheel, and uploads via PyPI trusted publishing (configure once on PyPI: project `ascii-magic-tools` → publisher = this repo, workflow `publish.yml`, environment `pypi`).

## Usage

### Image to ASCII (`image_to_ascii.py`)
Expand All @@ -84,7 +94,7 @@ Convert an image directly to ASCII art (preferred: installed console script):
```bash
image-to-ascii input.png -o output.txt
# or, during development:
python -m ascii_magic.image_to_ascii input.png -o output.txt
python -m asciimagic.image_to_ascii input.png -o output.txt
```

**Options:**
Expand Down Expand Up @@ -130,14 +140,14 @@ Add colors to existing ASCII art (preferred: installed console script):
```bash
colorize-ascii image.png ascii.txt output.ans
# or, during development:
python -m ascii_magic.colorize_ascii image.png ascii.txt output.ans
python -m asciimagic.colorize_ascii image.png ascii.txt output.ans
```

If you omit the output file, the tool defaults to ANSI and prints to stdout (convenient for piping). To print HTML to stdout, pass `--format html` and redirect, for example:

```bash
python -m ascii_magic.colorize_ascii image.png ascii.txt
python -m ascii_magic.colorize_ascii image.png ascii.txt --format html > out.html
python -m asciimagic.colorize_ascii image.png ascii.txt
python -m asciimagic.colorize_ascii image.png ascii.txt --format html > out.html
```

**Output formats:** `.ans` (ANSI), `.html` (HTML)
Expand Down Expand Up @@ -206,7 +216,7 @@ colorize-ascii photo.png art.txt --animate --loops 3 # play in
- `--loops N` : Terminal playback repeats; `0` plays until Ctrl-C (default: 3)
- `--reveal` : The rain uncovers the colorized image, which persists beneath it — the loop ends (and holds) on the fully revealed picture

All `--matrix-*` knobs (seed, gamma, intensity ranges, mask biasing, `--matrix-color` themes) apply to the rain too, and `--caption*` works in every animation sink — the caption stays static (matching the rain tint by default, or any `--caption-color` including `image`) while the rain falls. The web GUI exposes the same controls under *Matrix mode → Animate*, previews the animation live, and adds a `.gif` download. Programmatic use: `ascii_magic.pipeline.animate(ctx, ...)` returns an object with `frames_ansi()`, `play()`, `to_gif_bytes()`, and `to_html()`.
All `--matrix-*` knobs (seed, gamma, intensity ranges, mask biasing, `--matrix-color` themes) apply to the rain too, and `--caption*` works in every animation sink — the caption stays static (matching the rain tint by default, or any `--caption-color` including `image`) while the rain falls. The web GUI exposes the same controls under *Matrix mode → Animate*, previews the animation live, and adds a `.gif` download. Programmatic use: `asciimagic.pipeline.animate(ctx, ...)` returns an object with `frames_ansi()`, `play()`, `to_gif_bytes()`, and `to_html()`.

### Text to ASCII (`text_to_ascii.py`)

Expand Down Expand Up @@ -240,14 +250,14 @@ Options:
```bash
image-to-ascii photo.png -o art.txt
# or during development:
python -m ascii_magic.image_to_ascii photo.png -o art.txt
python -m asciimagic.image_to_ascii photo.png -o art.txt
```

2. Colorize the output (preferred: console script):
```bash
colorize-ascii photo.png art.txt output.ans --max-rows 30
# or during development:
python -m ascii_magic.colorize_ascii photo.png art.txt output.ans --max-rows 30
python -m asciimagic.colorize_ascii photo.png art.txt output.ans --max-rows 30
```

3. View in terminal or browser
Expand All @@ -258,8 +268,8 @@ You can run image -> ASCII -> colorized output entirely in memory:

```python
from PIL import Image
from ascii_magic.pipeline import AsciiPipelineContext, image_to_ascii, colorize
from ascii_magic.colorize_ascii import Options
from asciimagic.pipeline import AsciiPipelineContext, image_to_ascii, colorize
from asciimagic.colorize_ascii import Options

img = Image.open("photo.png").convert("RGB")
ctx = AsciiPipelineContext(source_image=img)
Expand All @@ -270,9 +280,9 @@ html_output = colorize(ctx, opt=Options(out_format="html"))
```

There are also lower-level in-memory helpers if you do not want the context object:
- `ascii_magic.image_to_ascii.image_to_text_glyph_from_image(...)`
- `ascii_magic.image_to_ascii.image_to_braille_from_image(...)`
- `ascii_magic.colorize_ascii.colorize_ascii_text(...)`
- `asciimagic.image_to_ascii.image_to_text_glyph_from_image(...)`
- `asciimagic.image_to_ascii.image_to_braille_from_image(...)`
- `asciimagic.colorize_ascii.colorize_ascii_text(...)`

## Video to ASCII (`ascii-magic video`)

Expand Down Expand Up @@ -365,11 +375,11 @@ ascii-magic web

The standalone scripts remain and behave identically:

- `ascii-magic` -> `ascii_magic.unified_cli:main` (subcommands: image, text, colorize, greet, web)
- `image-to-ascii` -> `ascii_magic.image_to_ascii:main`
- `colorize-ascii` -> `ascii_magic.colorize_ascii:main`
- `text-to-ascii` -> `ascii_magic.text_to_ascii:main`
- `ascii-magic-greet` -> `ascii_magic.greet:main`
- `ascii-magic-web` -> `ascii_magic.webapp:main` (requires the `[web]` extra)
- `ascii-magic` -> `asciimagic.unified_cli:main` (subcommands: image, text, colorize, greet, web)
- `image-to-ascii` -> `asciimagic.image_to_ascii:main`
- `colorize-ascii` -> `asciimagic.colorize_ascii:main`
- `text-to-ascii` -> `asciimagic.text_to_ascii:main`
- `ascii-magic-greet` -> `asciimagic.greet:main`
- `ascii-magic-web` -> `asciimagic.webapp:main` (requires the `[web]` extra)

Prefer calling `ascii-magic` or the individual console scripts after `pip install -e .`. You can also invoke modules directly with `python -m ascii_magic.<module>` when developing.
Prefer calling `ascii-magic` or the individual console scripts after `pip install -e .`. You can also invoke modules directly with `python -m asciimagic.<module>` when developing.
31 changes: 22 additions & 9 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "ascii-magic-tools"
version = "0.2.0"
version = "0.3.0"
description = "Convert images and text to colorized ASCII art - CLI tools, an in-memory pipeline, and a web GUI"
requires-python = ">=3.10"
dependencies = [
Expand All @@ -17,12 +17,25 @@ authors = [
]
readme = "README.MD"
license = {text = "Apache-2.0"}
keywords = ["ascii", "ascii-art", "ansi", "terminal", "braille", "matrix", "figlet", "video"]
classifiers = [
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"License :: OSI Approved :: Apache Software License",
"Operating System :: OS Independent",
"Environment :: Console",
"Topic :: Multimedia :: Graphics",
"Topic :: Text Processing :: Fonts",
]

[project.urls]
Homepage = "https://github.com/irobinson010/ASCII-Magic"
Repository = "https://github.com/irobinson010/ASCII-Magic"
Issues = "https://github.com/irobinson010/ASCII-Magic/issues"

[project.optional-dependencies]
web = [
"fastapi>=0.110",
Expand All @@ -41,13 +54,13 @@ dev = [
]

[project.scripts]
ascii-magic = "ascii_magic.unified_cli:main"
image-to-ascii = "ascii_magic.image_to_ascii:main"
colorize-ascii = "ascii_magic.colorize_ascii:main"
text-to-ascii = "ascii_magic.text_to_ascii:main"
ascii-magic-web = "ascii_magic.webapp:main"
ascii-magic-greet = "ascii_magic.greet:main"
ascii-magic-video = "ascii_magic.video:main"
ascii-magic = "asciimagic.unified_cli:main"
image-to-ascii = "asciimagic.image_to_ascii:main"
colorize-ascii = "asciimagic.colorize_ascii:main"
text-to-ascii = "asciimagic.text_to_ascii:main"
ascii-magic-web = "asciimagic.webapp:main"
ascii-magic-greet = "asciimagic.greet:main"
ascii-magic-video = "asciimagic.video:main"

[tool.setuptools]
package-dir = {"" = "src"}
Expand All @@ -56,7 +69,7 @@ package-dir = {"" = "src"}
where = ["src"]

[tool.setuptools.package-data]
ascii_magic = ["static/*"]
asciimagic = ["static/*"]


[tool.pytest.ini_options]
Expand Down
2 changes: 1 addition & 1 deletion src/ascii_magic/__init__.py → src/asciimagic/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""ASCII Magic - Convert images to ASCII art."""

__version__ = "0.2.0"
__version__ = "0.3.0"
__author__ = "Ian Robinson"

"""
Expand Down
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ def bash_script() -> str:

lines = [
"# ascii-magic shell completion - generated by `ascii-magic completion bash`",
"_ascii_magic() {",
"_asciimagic() {",
" local cur prev cmd base",
' cur="${COMP_WORDS[COMP_CWORD]}"',
' prev="${COMP_WORDS[COMP_CWORD-1]}"',
Expand Down Expand Up @@ -153,7 +153,7 @@ def bash_script() -> str:
" esac",
" return 0",
"}",
f"complete -o default -F _ascii_magic {all_names}",
f"complete -o default -F _asciimagic {all_names}",
"",
]
return "\n".join(lines)
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
16 changes: 8 additions & 8 deletions src/ascii_magic/unified_cli.py → src/asciimagic/unified_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@
from typing import Sequence, List, Optional

COMMANDS = {
"colorize": "ascii_magic.colorize_ascii",
"image": "ascii_magic.image_to_ascii",
"text": "ascii_magic.text_to_ascii",
"greet": "ascii_magic.greet",
"web": "ascii_magic.webapp",
"video": "ascii_magic.video",
"completion": "ascii_magic.completion",
"colorize": "asciimagic.colorize_ascii",
"image": "asciimagic.image_to_ascii",
"text": "asciimagic.text_to_ascii",
"greet": "asciimagic.greet",
"web": "asciimagic.webapp",
"video": "asciimagic.video",
"completion": "asciimagic.completion",
}


Expand Down Expand Up @@ -60,7 +60,7 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
return 0

if argv[0] in ("-V", "--version"):
from ascii_magic import __version__
from asciimagic import __version__

print(f"ascii-magic {__version__}")
return 0
Expand Down
File renamed without changes.
2 changes: 1 addition & 1 deletion src/ascii_magic/webapp.py → src/asciimagic/webapp.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
and HTML renders in one response. The single-page GUI in ``static/`` is
served from the same app.

Run locally: ascii-magic-web (or: uvicorn ascii_magic.webapp:app)
Run locally: ascii-magic-web (or: uvicorn asciimagic.webapp:app)
"""

from __future__ import annotations
Expand Down
10 changes: 5 additions & 5 deletions tests/test_animate.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
import pytest
from PIL import Image

from ascii_magic.animate import AnimationOptions, generate
from ascii_magic.colorize_ascii import MatrixOptions
from ascii_magic.pipeline import AsciiPipelineContext, animate
from asciimagic.animate import AnimationOptions, generate
from asciimagic.colorize_ascii import MatrixOptions
from asciimagic.pipeline import AsciiPipelineContext, animate


def _image():
Expand Down Expand Up @@ -114,7 +114,7 @@ def test_pipeline_animate():


def test_caption_in_all_animation_sinks():
from ascii_magic.colorize_ascii import CaptionOptions
from asciimagic.colorize_ascii import CaptionOptions

cap = CaptionOptions(text="Cat", style="box", position="bottom")
anim = generate(
Expand Down Expand Up @@ -147,7 +147,7 @@ def test_caption_in_all_animation_sinks():


def test_caption_image_colors_in_animation():
from ascii_magic.colorize_ascii import CaptionOptions
from asciimagic.colorize_ascii import CaptionOptions

cap = CaptionOptions(text="Cat", style="box", color="image")
anim = generate(
Expand Down
6 changes: 3 additions & 3 deletions tests/test_caption.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

from PIL import Image

from ascii_magic.colorize_ascii import CaptionOptions, Options, colorize_ascii_text
from ascii_magic.text_to_ascii import caption_lines, compose_caption
from asciimagic.colorize_ascii import CaptionOptions, Options, colorize_ascii_text
from asciimagic.text_to_ascii import caption_lines, compose_caption

ART = "\n".join("#" * 40 for _ in range(10))
STRIP = lambda s: re.sub(r"\x1b\[[0-9;]*m", "", s)
Expand Down Expand Up @@ -133,7 +133,7 @@ def test_caption_figlet_downscales_to_fit():


def test_wrap_html_default_text_contrasts_background():
from ascii_magic.colorize_ascii import wrap_html
from asciimagic.colorize_ascii import wrap_html

doc = wrap_html(["hello"])
assert "background: #000" in doc
Expand Down
4 changes: 2 additions & 2 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
import pytest
from PIL import Image

from ascii_magic.unified_cli import COMMANDS, main as cli_main
from ascii_magic.colorize_ascii import parse_args
from asciimagic.unified_cli import COMMANDS, main as cli_main
from asciimagic.colorize_ascii import parse_args


def test_new_subcommands_registered():
Expand Down
10 changes: 5 additions & 5 deletions tests/test_completion.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

import pytest

from ascii_magic.completion import bash_script, main as completion_main, zsh_script
from asciimagic.completion import bash_script, main as completion_main, zsh_script


def test_bash_script_contents():
Expand All @@ -26,7 +26,7 @@ def test_bash_script_contents():
def test_zsh_script_wraps_bash():
z = zsh_script()
assert "bashcompinit" in z
assert "_ascii_magic()" in z
assert "_asciimagic()" in z


def test_completion_command(capsys):
Expand All @@ -36,10 +36,10 @@ def test_completion_command(capsys):


def test_completion_dispatch_via_unified_cli(capsys):
from ascii_magic.unified_cli import main as cli_main
from asciimagic.unified_cli import main as cli_main

assert cli_main(["completion", "bash"]) == 0
assert "complete -o default -F _ascii_magic" in capsys.readouterr().out
assert "complete -o default -F _asciimagic" in capsys.readouterr().out


BASH = shutil.which("bash")
Expand Down Expand Up @@ -68,7 +68,7 @@ def test_bash_completion_behaves(tmp_path, words, expected, not_expected):
source "{script_file}"
COMP_WORDS=({comp_words})
COMP_CWORD={len(words) - 1}
_ascii_magic
_asciimagic
printf '%s\\n' "${{COMPREPLY[@]}}"
"""
r = subprocess.run([BASH, "-c", harness], capture_output=True, text=True, timeout=30)
Expand Down
2 changes: 1 addition & 1 deletion tests/test_greet.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import pytest

from ascii_magic import greet
from asciimagic import greet

# greet writes POSIX shell rc blocks; install is gated off on Windows.
pytestmark = pytest.mark.skipif(os.name == "nt", reason="greet targets POSIX shells")
Expand Down
Loading
Loading