ansi-pixel 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ansi_pixel/__init__.py +49 -0
- ansi_pixel/cli.py +299 -0
- ansi_pixel/converter.py +374 -0
- ansi_pixel/exporters/__init__.py +100 -0
- ansi_pixel/exporters/ansi.py +19 -0
- ansi_pixel/exporters/code.py +44 -0
- ansi_pixel/exporters/html.py +145 -0
- ansi_pixel/exporters/markdown.py +18 -0
- ansi_pixel/optimizer.py +153 -0
- ansi_pixel/py.typed +1 -0
- ansi_pixel/render.py +57 -0
- ansi_pixel-0.2.0.dist-info/METADATA +304 -0
- ansi_pixel-0.2.0.dist-info/RECORD +16 -0
- ansi_pixel-0.2.0.dist-info/WHEEL +4 -0
- ansi_pixel-0.2.0.dist-info/entry_points.txt +2 -0
- ansi_pixel-0.2.0.dist-info/licenses/LICENSE +21 -0
ansi_pixel/__init__.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""ansi-pixel: Convert images into true-color ANSI terminal art."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from ansi_pixel.converter import (
|
|
6
|
+
FilterInput,
|
|
7
|
+
ImageSource,
|
|
8
|
+
ResamplingFilter,
|
|
9
|
+
get_terminal_width,
|
|
10
|
+
image_to_ansi,
|
|
11
|
+
parse_hex_color,
|
|
12
|
+
parse_resampling_filter,
|
|
13
|
+
)
|
|
14
|
+
from ansi_pixel.exporters import (
|
|
15
|
+
OutputFormat,
|
|
16
|
+
export_ansi,
|
|
17
|
+
export_format,
|
|
18
|
+
export_html,
|
|
19
|
+
export_javascript,
|
|
20
|
+
export_markdown,
|
|
21
|
+
export_python,
|
|
22
|
+
infer_format_from_path,
|
|
23
|
+
)
|
|
24
|
+
from ansi_pixel.optimizer import AnsiOptimizer, RGBColor, strip_ansi
|
|
25
|
+
from ansi_pixel.render import render_image
|
|
26
|
+
|
|
27
|
+
__version__ = "0.2.0"
|
|
28
|
+
|
|
29
|
+
__all__ = [
|
|
30
|
+
"AnsiOptimizer",
|
|
31
|
+
"FilterInput",
|
|
32
|
+
"ImageSource",
|
|
33
|
+
"OutputFormat",
|
|
34
|
+
"RGBColor",
|
|
35
|
+
"ResamplingFilter",
|
|
36
|
+
"export_ansi",
|
|
37
|
+
"export_format",
|
|
38
|
+
"export_html",
|
|
39
|
+
"export_javascript",
|
|
40
|
+
"export_markdown",
|
|
41
|
+
"export_python",
|
|
42
|
+
"get_terminal_width",
|
|
43
|
+
"image_to_ansi",
|
|
44
|
+
"infer_format_from_path",
|
|
45
|
+
"parse_hex_color",
|
|
46
|
+
"parse_resampling_filter",
|
|
47
|
+
"render_image",
|
|
48
|
+
"strip_ansi",
|
|
49
|
+
]
|
ansi_pixel/cli.py
ADDED
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
"""Command-line interface entry point for ansi-pixel."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import os
|
|
7
|
+
import sys
|
|
8
|
+
import urllib.error
|
|
9
|
+
from collections.abc import Sequence
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import TextIO
|
|
12
|
+
|
|
13
|
+
from PIL import UnidentifiedImageError
|
|
14
|
+
|
|
15
|
+
from ansi_pixel import __version__
|
|
16
|
+
from ansi_pixel.converter import ResamplingFilter, get_terminal_width, image_to_ansi
|
|
17
|
+
from ansi_pixel.exporters import (
|
|
18
|
+
OutputFormat,
|
|
19
|
+
export_format,
|
|
20
|
+
infer_format_from_path,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def format_lines(lines: list[str], output_format: OutputFormat | str) -> str:
|
|
25
|
+
"""Serialize rendered ANSI lines into the target representation format.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
lines: Rendered ANSI art lines.
|
|
29
|
+
output_format: Desired OutputFormat or format name.
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
Formatted string suitable for writing to stdout or a file.
|
|
33
|
+
"""
|
|
34
|
+
return export_format(lines, output_format)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def write_output(
|
|
38
|
+
lines: list[str],
|
|
39
|
+
output_format: OutputFormat | str = OutputFormat.ANSI,
|
|
40
|
+
file_path: str | Path | None = None,
|
|
41
|
+
) -> None:
|
|
42
|
+
"""Save rendered ANSI art lines to a file according to the requested format.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
lines: Rendered ANSI art lines.
|
|
46
|
+
output_format: Target format (ansi, md, py, js, html, txt).
|
|
47
|
+
file_path: Destination path. If None, defaults to 'output.<ext>'.
|
|
48
|
+
"""
|
|
49
|
+
fmt = (
|
|
50
|
+
output_format
|
|
51
|
+
if isinstance(output_format, OutputFormat)
|
|
52
|
+
else OutputFormat(str(output_format).lower())
|
|
53
|
+
)
|
|
54
|
+
if file_path is None:
|
|
55
|
+
file_path = f"output.{fmt.value}"
|
|
56
|
+
content = format_lines(lines, fmt)
|
|
57
|
+
Path(file_path).write_text(content, encoding="utf-8")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def is_color_enabled(color_mode: str = "auto", stream: TextIO | None = None) -> bool:
|
|
61
|
+
"""Determine whether ANSI color escape codes should be emitted.
|
|
62
|
+
|
|
63
|
+
Adheres strictly to the NO_COLOR standard (https://no-color.org) and non-TTY pipe detection.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
color_mode: Color choice ('auto', 'always', or 'never').
|
|
67
|
+
stream: Output stream to inspect for interactive TTY (defaults to sys.stdout).
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
True if ANSI color codes should be included, False if suppressed.
|
|
71
|
+
"""
|
|
72
|
+
if color_mode == "never":
|
|
73
|
+
return False
|
|
74
|
+
if color_mode == "always":
|
|
75
|
+
return True
|
|
76
|
+
|
|
77
|
+
# color_mode == "auto":
|
|
78
|
+
# 1. NO_COLOR standard: if NO_COLOR environment variable is present and non-empty, disable color
|
|
79
|
+
if os.environ.get("NO_COLOR", "") != "":
|
|
80
|
+
return False
|
|
81
|
+
|
|
82
|
+
target_stream = stream if stream is not None else sys.stdout
|
|
83
|
+
return bool(getattr(target_stream, "isatty", lambda: False)())
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
87
|
+
"""Construct and configure the command-line argument parser.
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
Configured ArgumentParser instance.
|
|
91
|
+
"""
|
|
92
|
+
parser = argparse.ArgumentParser(
|
|
93
|
+
prog="ansi-pixel",
|
|
94
|
+
description=(
|
|
95
|
+
"Convert an image into true-color ANSI art for terminal banners, "
|
|
96
|
+
"SSH previews, and Markdown."
|
|
97
|
+
),
|
|
98
|
+
)
|
|
99
|
+
parser.add_argument(
|
|
100
|
+
"-V",
|
|
101
|
+
"--version",
|
|
102
|
+
action="version",
|
|
103
|
+
version=f"%(prog)s {__version__}",
|
|
104
|
+
help="Show program's version number and exit",
|
|
105
|
+
)
|
|
106
|
+
parser.add_argument(
|
|
107
|
+
"image",
|
|
108
|
+
nargs="?",
|
|
109
|
+
default=None,
|
|
110
|
+
help="Path to image file, image URL, or '-' to read from standard input",
|
|
111
|
+
)
|
|
112
|
+
parser.add_argument(
|
|
113
|
+
"-w",
|
|
114
|
+
"--width",
|
|
115
|
+
type=int,
|
|
116
|
+
default=None,
|
|
117
|
+
help="Output width in terminal characters (default: auto-detected terminal width)",
|
|
118
|
+
)
|
|
119
|
+
parser.add_argument(
|
|
120
|
+
"-o",
|
|
121
|
+
"--output",
|
|
122
|
+
type=str,
|
|
123
|
+
default=None,
|
|
124
|
+
metavar="FILE",
|
|
125
|
+
help="Path to save output file instead of writing solely to stdout",
|
|
126
|
+
)
|
|
127
|
+
parser.add_argument(
|
|
128
|
+
"-f",
|
|
129
|
+
"--format",
|
|
130
|
+
type=str,
|
|
131
|
+
choices=[f.value for f in OutputFormat],
|
|
132
|
+
default=None,
|
|
133
|
+
help="Output format: ansi, md, py, js, html (default: ansi or inferred from --output)",
|
|
134
|
+
)
|
|
135
|
+
parser.add_argument(
|
|
136
|
+
"--filter",
|
|
137
|
+
type=str,
|
|
138
|
+
choices=[f.value for f in ResamplingFilter],
|
|
139
|
+
default=ResamplingFilter.NEAREST.value,
|
|
140
|
+
help="Resampling filter: nearest, lanczos, or bilinear (default: nearest)",
|
|
141
|
+
)
|
|
142
|
+
parser.add_argument(
|
|
143
|
+
"--trim-bg",
|
|
144
|
+
action="store_true",
|
|
145
|
+
help="Strip white/solid background (treat near-white pixels as transparent)",
|
|
146
|
+
)
|
|
147
|
+
parser.add_argument(
|
|
148
|
+
"--chroma-key",
|
|
149
|
+
type=str,
|
|
150
|
+
default=None,
|
|
151
|
+
help="Hex color to treat as transparent (e.g. #FFFFFF or 00FF00)",
|
|
152
|
+
)
|
|
153
|
+
parser.add_argument(
|
|
154
|
+
"--color",
|
|
155
|
+
dest="color",
|
|
156
|
+
choices=["auto", "always", "never"],
|
|
157
|
+
default="auto",
|
|
158
|
+
nargs="?",
|
|
159
|
+
const="always",
|
|
160
|
+
help=(
|
|
161
|
+
"When to output ANSI colors: auto, always, never (default: auto). "
|
|
162
|
+
"Follows NO_COLOR standard."
|
|
163
|
+
),
|
|
164
|
+
)
|
|
165
|
+
parser.add_argument(
|
|
166
|
+
"--no-color",
|
|
167
|
+
dest="color",
|
|
168
|
+
action="store_const",
|
|
169
|
+
const="never",
|
|
170
|
+
help="Disable ANSI color codes (alias for --color=never)",
|
|
171
|
+
)
|
|
172
|
+
parser.add_argument(
|
|
173
|
+
"--print",
|
|
174
|
+
dest="print_output",
|
|
175
|
+
action=argparse.BooleanOptionalAction,
|
|
176
|
+
default=None,
|
|
177
|
+
help="Control printing to stdout when -o/--output is provided; use --no-print to silence",
|
|
178
|
+
)
|
|
179
|
+
return parser
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace:
|
|
183
|
+
"""Parse command line arguments.
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
argv: Optional sequence of argument strings. Defaults to sys.argv[1:].
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
Parsed arguments Namespace.
|
|
190
|
+
"""
|
|
191
|
+
parser = build_parser()
|
|
192
|
+
return parser.parse_args(argv)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
196
|
+
"""Main CLI execution entry point.
|
|
197
|
+
|
|
198
|
+
Args:
|
|
199
|
+
argv: Optional sequence of CLI arguments.
|
|
200
|
+
|
|
201
|
+
Returns:
|
|
202
|
+
POSIX exit code: 0 on success, 1 on input/runtime error, 2 on usage error.
|
|
203
|
+
"""
|
|
204
|
+
try:
|
|
205
|
+
args = parse_args(argv)
|
|
206
|
+
except SystemExit as exc:
|
|
207
|
+
return int(exc.code) if isinstance(exc.code, int) else 2
|
|
208
|
+
|
|
209
|
+
# Handle image argument: if omitted, check whether stdin is piped
|
|
210
|
+
image_source: str | None = args.image
|
|
211
|
+
if image_source is None:
|
|
212
|
+
if not sys.stdin.isatty():
|
|
213
|
+
image_source = "-"
|
|
214
|
+
else:
|
|
215
|
+
sys.stderr.write("ansi-pixel: error: the following arguments are required: image\n")
|
|
216
|
+
return 2
|
|
217
|
+
|
|
218
|
+
# Validate width if explicitly provided
|
|
219
|
+
if args.width is not None and args.width < 1:
|
|
220
|
+
sys.stderr.write("Error: target_width must be at least 1.\n")
|
|
221
|
+
return 2
|
|
222
|
+
|
|
223
|
+
target_width: int = args.width if args.width is not None else get_terminal_width()
|
|
224
|
+
|
|
225
|
+
# Determine whether color codes should be generated
|
|
226
|
+
if args.output is not None:
|
|
227
|
+
# Saving to file: honor explicit --color flag and NO_COLOR env var
|
|
228
|
+
color_enabled = (args.color != "never") and (
|
|
229
|
+
os.environ.get("NO_COLOR", "") == "" or args.color == "always"
|
|
230
|
+
)
|
|
231
|
+
else:
|
|
232
|
+
# Outputting to stdout: check non-TTY pipe detection and NO_COLOR standard
|
|
233
|
+
color_enabled = is_color_enabled(args.color, sys.stdout)
|
|
234
|
+
|
|
235
|
+
try:
|
|
236
|
+
lines = image_to_ansi(
|
|
237
|
+
image_source,
|
|
238
|
+
target_width=target_width,
|
|
239
|
+
filter=args.filter,
|
|
240
|
+
trim_bg=args.trim_bg,
|
|
241
|
+
chroma_key=args.chroma_key,
|
|
242
|
+
color=color_enabled,
|
|
243
|
+
)
|
|
244
|
+
except FileNotFoundError:
|
|
245
|
+
sys.stderr.write(f"Error: image not found: {image_source}\n")
|
|
246
|
+
return 1
|
|
247
|
+
except UnidentifiedImageError:
|
|
248
|
+
desc = "standard input" if image_source == "-" else repr(image_source)
|
|
249
|
+
sys.stderr.write(f"Error: unsupported or invalid image: {desc}\n")
|
|
250
|
+
return 1
|
|
251
|
+
except urllib.error.HTTPError as exc:
|
|
252
|
+
sys.stderr.write(f"Error: HTTP {exc.code} {exc.reason}: {image_source}\n")
|
|
253
|
+
return 1
|
|
254
|
+
except urllib.error.URLError as exc:
|
|
255
|
+
sys.stderr.write(f"Error: failed to fetch URL '{image_source}': {exc.reason}\n")
|
|
256
|
+
return 1
|
|
257
|
+
except TimeoutError:
|
|
258
|
+
sys.stderr.write(f"Error: request timed out fetching URL: {image_source}\n")
|
|
259
|
+
return 1
|
|
260
|
+
except ValueError as exc:
|
|
261
|
+
sys.stderr.write(f"Error: {exc}\n")
|
|
262
|
+
err_msg = str(exc).lower()
|
|
263
|
+
if any(keyword in err_msg for keyword in ("target_width", "filter", "hex", "choice")):
|
|
264
|
+
return 2
|
|
265
|
+
return 1
|
|
266
|
+
except OSError as exc:
|
|
267
|
+
sys.stderr.write(f"Error: could not open image: {exc}\n")
|
|
268
|
+
return 1
|
|
269
|
+
|
|
270
|
+
# Determine output serialization format
|
|
271
|
+
if args.format is not None:
|
|
272
|
+
out_format = OutputFormat(args.format.lower())
|
|
273
|
+
elif args.output is not None:
|
|
274
|
+
out_format = infer_format_from_path(args.output)
|
|
275
|
+
else:
|
|
276
|
+
out_format = OutputFormat.ANSI
|
|
277
|
+
|
|
278
|
+
formatted_output = format_lines(lines, out_format)
|
|
279
|
+
|
|
280
|
+
# Write to destination file if -o/--output was specified
|
|
281
|
+
if args.output is not None:
|
|
282
|
+
try:
|
|
283
|
+
target_path = Path(args.output)
|
|
284
|
+
target_path.parent.mkdir(parents=True, exist_ok=True)
|
|
285
|
+
target_path.write_text(formatted_output, encoding="utf-8")
|
|
286
|
+
except OSError as exc:
|
|
287
|
+
sys.stderr.write(f"Error: failed to write output file '{args.output}': {exc}\n")
|
|
288
|
+
return 1
|
|
289
|
+
|
|
290
|
+
# Output to stdout by default (when -o is omitted) or when --print is explicitly enabled
|
|
291
|
+
should_print = args.print_output if args.print_output is not None else (args.output is None)
|
|
292
|
+
if should_print:
|
|
293
|
+
sys.stdout.write(formatted_output)
|
|
294
|
+
|
|
295
|
+
return 0
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
if __name__ == "__main__":
|
|
299
|
+
sys.exit(main())
|