2025-07-01
This commit is contained in:
@@ -0,0 +1,536 @@
|
||||
#! python3
|
||||
|
||||
# The MIT License (MIT)
|
||||
|
||||
# Copyright (c) 2016 eight04
|
||||
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
|
||||
"""This is an APNG module, which can create apng file from pngs
|
||||
|
||||
Reference:
|
||||
http://littlesvr.ca/apng/
|
||||
http://wiki.mozilla.org/APNG_Specification
|
||||
https://www.w3.org/TR/PNG/
|
||||
"""
|
||||
|
||||
import struct
|
||||
import binascii
|
||||
import io
|
||||
import zlib
|
||||
from collections import namedtuple
|
||||
|
||||
__version__ = "0.3.4"
|
||||
|
||||
PNG_SIGN = b"\x89\x50\x4E\x47\x0D\x0A\x1A\x0A"
|
||||
|
||||
# http://www.libpng.org/pub/png/spec/1.2/PNG-Chunks.html#C.Summary-of-standard-chunks
|
||||
CHUNK_BEFORE_IDAT = {
|
||||
"cHRM", "gAMA", "iCCP", "sBIT", "sRGB", "bKGD", "hIST", "tRNS", "pHYs",
|
||||
"sPLT", "tIME", "PLTE"
|
||||
}
|
||||
|
||||
def parse_chunks(b):
|
||||
"""Parse PNG bytes into multiple chunks.
|
||||
|
||||
:arg bytes b: The raw bytes of the PNG file.
|
||||
:return: A generator yielding :class:`Chunk`.
|
||||
:rtype: Iterator[Chunk]
|
||||
"""
|
||||
# skip signature
|
||||
i = 8
|
||||
# yield chunks
|
||||
while i < len(b):
|
||||
data_len, = struct.unpack("!I", b[i:i+4])
|
||||
type_ = b[i+4:i+8].decode("latin-1")
|
||||
yield Chunk(type_, b[i:i+data_len+12])
|
||||
i += data_len + 12
|
||||
|
||||
def make_chunk(chunk_type, chunk_data):
|
||||
"""Create a raw chunk by composing chunk type and data. It
|
||||
calculates chunk length and CRC for you.
|
||||
|
||||
:arg str chunk_type: PNG chunk type.
|
||||
:arg bytes chunk_data: PNG chunk data, **excluding chunk length, type, and CRC**.
|
||||
:rtype: bytes
|
||||
"""
|
||||
out = struct.pack("!I", len(chunk_data))
|
||||
chunk_data = chunk_type.encode("latin-1") + chunk_data
|
||||
out += chunk_data + struct.pack("!I", binascii.crc32(chunk_data) & 0xffffffff)
|
||||
return out
|
||||
|
||||
def make_text_chunk(
|
||||
type="tEXt", key="Comment", value="",
|
||||
compression_flag=0, compression_method=0, lang="", translated_key=""):
|
||||
"""Create a text chunk with a key value pair.
|
||||
See https://www.w3.org/TR/PNG/#11textinfo for text chunk information.
|
||||
|
||||
Usage:
|
||||
|
||||
.. code:: python
|
||||
|
||||
from apng import APNG, make_text_chunk
|
||||
|
||||
im = APNG.open("file.png")
|
||||
png, control = im.frames[0]
|
||||
png.chunks.append(make_text_chunk("tEXt", "Comment", "some text"))
|
||||
im.save("file.png")
|
||||
|
||||
:arg str type: Text chunk type: "tEXt", "zTXt", or "iTXt":
|
||||
|
||||
tEXt uses Latin-1 characters.
|
||||
zTXt uses Latin-1 characters, compressed with zlib.
|
||||
iTXt uses UTF-8 characters.
|
||||
|
||||
:arg str key: The key string, 1-79 characters.
|
||||
|
||||
:arg str value: The text value. It would be encoded into
|
||||
:class:`bytes` and compressed if needed.
|
||||
|
||||
:arg int compression_flag: The compression flag for iTXt.
|
||||
:arg int compression_method: The compression method for zTXt and iTXt.
|
||||
:arg str lang: The language tag for iTXt.
|
||||
:arg str translated_key: The translated keyword for iTXt.
|
||||
:rtype: Chunk
|
||||
"""
|
||||
# pylint: disable=redefined-builtin
|
||||
if type == "tEXt":
|
||||
data = key.encode("latin-1") + b"\0" + value.encode("latin-1")
|
||||
elif type == "zTXt":
|
||||
data = (
|
||||
key.encode("latin-1") + struct.pack("!xb", compression_method) +
|
||||
zlib.compress(value.encode("latin-1"))
|
||||
)
|
||||
elif type == "iTXt":
|
||||
data = (
|
||||
key.encode("latin-1") +
|
||||
struct.pack("!xbb", compression_flag, compression_method) +
|
||||
lang.encode("latin-1") + b"\0" +
|
||||
translated_key.encode("utf-8") + b"\0"
|
||||
)
|
||||
if compression_flag:
|
||||
data += zlib.compress(value.encode("utf-8"))
|
||||
else:
|
||||
data += value.encode("utf-8")
|
||||
else:
|
||||
raise TypeError("unknown type {!r}".format(type))
|
||||
return Chunk(type, make_chunk(type, data))
|
||||
|
||||
def read_file(file):
|
||||
"""Read ``file`` into ``bytes``.
|
||||
|
||||
:arg file type: path-like or file-like
|
||||
:rtype: bytes
|
||||
"""
|
||||
if hasattr(file, "read"):
|
||||
return file.read()
|
||||
if hasattr(file, "read_bytes"):
|
||||
return file.read_bytes()
|
||||
with open(file, "rb") as f:
|
||||
return f.read()
|
||||
|
||||
def write_file(file, b):
|
||||
"""Write ``b`` to file ``file``.
|
||||
|
||||
:arg file type: path-like or file-like object.
|
||||
:arg bytes b: The content.
|
||||
"""
|
||||
if hasattr(file, "write_bytes"):
|
||||
file.write_bytes(b)
|
||||
elif hasattr(file, "write"):
|
||||
file.write(b)
|
||||
else:
|
||||
with open(file, "wb") as f:
|
||||
f.write(b)
|
||||
|
||||
def open_file(file, mode):
|
||||
"""Open a file.
|
||||
|
||||
:arg file: file-like or path-like object.
|
||||
:arg str mode: ``mode`` argument for :func:`open`.
|
||||
"""
|
||||
if hasattr(file, "read"):
|
||||
return file
|
||||
if hasattr(file, "open"):
|
||||
return file.open(mode)
|
||||
return open(file, mode)
|
||||
|
||||
def file_to_png(fp):
|
||||
"""Convert an image to PNG format with Pillow.
|
||||
|
||||
:arg file-like fp: The image file.
|
||||
:rtype: bytes
|
||||
"""
|
||||
import PIL.Image # pylint: disable=import-error
|
||||
with io.BytesIO() as dest:
|
||||
PIL.Image.open(fp).save(dest, "PNG", optimize=True)
|
||||
return dest.getvalue()
|
||||
|
||||
class Chunk(namedtuple("Chunk", ["type", "data"])):
|
||||
"""A namedtuple to represent the PNG chunk.
|
||||
|
||||
:arg str type: The chunk type.
|
||||
:arg bytes data: The raw bytes of the chunk, including chunk length, type,
|
||||
data, and CRC.
|
||||
"""
|
||||
pass
|
||||
|
||||
class PNG:
|
||||
"""Represent a PNG image.
|
||||
"""
|
||||
def __init__(self):
|
||||
self.hdr = None
|
||||
self.end = None
|
||||
self.width = None
|
||||
self.height = None
|
||||
self.chunks = []
|
||||
"""A list of :class:`Chunk`. After reading a PNG file, the bytes
|
||||
are parsed into multiple chunks. You can remove/add chunks into
|
||||
this array before calling :func:`to_bytes`."""
|
||||
|
||||
def init(self):
|
||||
"""Extract some info from chunks"""
|
||||
for type_, data in self.chunks:
|
||||
if type_ == "IHDR":
|
||||
self.hdr = data
|
||||
elif type_ == "IEND":
|
||||
self.end = data
|
||||
|
||||
if self.hdr:
|
||||
# grab w, h info
|
||||
self.width, self.height = struct.unpack("!II", self.hdr[8:16])
|
||||
|
||||
@classmethod
|
||||
def open(cls, file):
|
||||
"""Open a PNG file.
|
||||
|
||||
:arg file: Input file.
|
||||
:type file: path-like or file-like
|
||||
:rtype: :class:`PNG`
|
||||
"""
|
||||
return cls.from_bytes(read_file(file))
|
||||
|
||||
@classmethod
|
||||
def open_any(cls, file):
|
||||
"""Open an image file. If the image is not PNG format, it would convert
|
||||
the image into PNG with Pillow module. If the module is not
|
||||
installed, :class:`ImportError` would be raised.
|
||||
|
||||
:arg file: Input file.
|
||||
:type file: path-like or file-like
|
||||
:rtype: :class:`PNG`
|
||||
"""
|
||||
with open_file(file, "rb") as f:
|
||||
header = f.read(8)
|
||||
f.seek(0)
|
||||
if header != PNG_SIGN:
|
||||
b = file_to_png(f)
|
||||
else:
|
||||
b = f.read()
|
||||
return cls.from_bytes(b)
|
||||
|
||||
@classmethod
|
||||
def from_bytes(cls, b):
|
||||
"""Create :class:`PNG` from raw bytes.
|
||||
|
||||
:arg bytes b: The raw bytes of the PNG file.
|
||||
:rtype: :class:`PNG`
|
||||
"""
|
||||
im = cls()
|
||||
im.chunks = list(parse_chunks(b))
|
||||
im.init()
|
||||
return im
|
||||
|
||||
@classmethod
|
||||
def from_chunks(cls, chunks):
|
||||
"""Construct PNG from raw chunks.
|
||||
|
||||
:arg chunks: A list of ``(chunk_type, chunk_raw_data)``. Also see
|
||||
:func:`chunks`.
|
||||
:type chunks: list[tuple(str, bytes)]
|
||||
"""
|
||||
im = cls()
|
||||
im.chunks = chunks
|
||||
im.init()
|
||||
return im
|
||||
|
||||
|
||||
def to_bytes(self):
|
||||
"""Convert the entire image to bytes.
|
||||
|
||||
:rtype: bytes
|
||||
"""
|
||||
chunks = [PNG_SIGN]
|
||||
chunks.extend(c[1] for c in self.chunks)
|
||||
return b"".join(chunks)
|
||||
|
||||
def save(self, file):
|
||||
"""Save the entire image to a file.
|
||||
|
||||
:arg file: Output file.
|
||||
:type file: path-like or file-like
|
||||
"""
|
||||
write_file(file, self.to_bytes())
|
||||
|
||||
class FrameControl:
|
||||
"""A data class holding fcTL info."""
|
||||
def __init__(self, width=None, height=None, x_offset=0, y_offset=0,
|
||||
delay=100, delay_den=1000, depose_op=1, blend_op=0):
|
||||
"""Parameters are assigned as object members. See
|
||||
`https://wiki.mozilla.org/APNG_Specification
|
||||
<https://wiki.mozilla.org/APNG_Specification#.60fcTL.60:_The_Frame_Control_Chunk>`_
|
||||
for the detail of fcTL.
|
||||
"""
|
||||
self.width = width
|
||||
self.height = height
|
||||
self.x_offset = x_offset
|
||||
self.y_offset = y_offset
|
||||
self.delay = delay
|
||||
self.delay_den = delay_den
|
||||
self.depose_op = depose_op
|
||||
self.blend_op = blend_op
|
||||
|
||||
def to_bytes(self):
|
||||
"""Convert to bytes.
|
||||
|
||||
:rtype: bytes
|
||||
"""
|
||||
return struct.pack(
|
||||
"!IIIIHHbb", self.width, self.height, self.x_offset, self.y_offset,
|
||||
self.delay, self.delay_den, self.depose_op, self.blend_op
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_bytes(cls, b):
|
||||
"""Contruct fcTL info from bytes.
|
||||
|
||||
:arg bytes b: The length of ``b`` must be *28*, excluding sequence
|
||||
number and CRC.
|
||||
"""
|
||||
return cls(*struct.unpack("!IIIIHHbb", b))
|
||||
|
||||
class APNG:
|
||||
"""Represent an APNG image."""
|
||||
def __init__(self, num_plays=0):
|
||||
"""An :class:`APNG` is composed by multiple :class:`PNG` s and
|
||||
:class:`FrameControl`, which can be inserted with :meth:`append`.
|
||||
|
||||
:arg int num_plays: Number of times to loop. 0 = infinite.
|
||||
|
||||
:var frames: The frames of APNG.
|
||||
:vartype frames: list[tuple(PNG, FrameControl)]
|
||||
:var int num_plays: same as ``num_plays``.
|
||||
"""
|
||||
self.frames = []
|
||||
self.num_plays = num_plays
|
||||
|
||||
def append(self, png, **options):
|
||||
"""Append one frame.
|
||||
|
||||
:arg PNG png: Append a :class:`PNG` as a frame.
|
||||
:arg dict options: The options for :class:`FrameControl`.
|
||||
"""
|
||||
if not isinstance(png, PNG):
|
||||
raise TypeError("Expect an instance of `PNG` but got `{}`".format(png))
|
||||
control = FrameControl(**options)
|
||||
if control.width is None:
|
||||
control.width = png.width
|
||||
if control.height is None:
|
||||
control.height = png.height
|
||||
self.frames.append((png, control))
|
||||
|
||||
def append_file(self, file, **options):
|
||||
"""Create a PNG from file and append the PNG as a frame.
|
||||
|
||||
:arg file: Input file.
|
||||
:type file: path-like or file-like.
|
||||
:arg dict options: The options for :class:`FrameControl`.
|
||||
"""
|
||||
self.append(PNG.open_any(file), **options)
|
||||
|
||||
def to_bytes(self):
|
||||
"""Convert the entire image to bytes.
|
||||
|
||||
:rtype: bytes
|
||||
"""
|
||||
|
||||
# grab the chunks we needs
|
||||
out = [PNG_SIGN]
|
||||
# FIXME: it's tricky to define "other_chunks". HoneyView stop the
|
||||
# animation if it sees chunks other than fctl or idat, so we put other
|
||||
# chunks to the end of the file
|
||||
other_chunks = []
|
||||
seq = 0
|
||||
|
||||
# for first frame
|
||||
png, control = self.frames[0]
|
||||
|
||||
# header
|
||||
out.append(png.hdr)
|
||||
|
||||
# acTL
|
||||
out.append(make_chunk("acTL", struct.pack("!II", len(self.frames), self.num_plays)))
|
||||
|
||||
# fcTL
|
||||
if control:
|
||||
out.append(make_chunk("fcTL", struct.pack("!I", seq) + control.to_bytes()))
|
||||
seq += 1
|
||||
|
||||
# and others...
|
||||
idat_chunks = []
|
||||
for type_, data in png.chunks:
|
||||
if type_ in ("IHDR", "IEND"):
|
||||
continue
|
||||
if type_ == "IDAT":
|
||||
# put at last
|
||||
idat_chunks.append(data)
|
||||
continue
|
||||
out.append(data)
|
||||
out.extend(idat_chunks)
|
||||
|
||||
# FIXME: we should do some optimization to frames...
|
||||
# for other frames
|
||||
for png, control in self.frames[1:]:
|
||||
# fcTL
|
||||
out.append(
|
||||
make_chunk("fcTL", struct.pack("!I", seq) + control.to_bytes())
|
||||
)
|
||||
seq += 1
|
||||
|
||||
# and others...
|
||||
for type_, data in png.chunks:
|
||||
if type_ in ("IHDR", "IEND") or type_ in CHUNK_BEFORE_IDAT:
|
||||
continue
|
||||
|
||||
if type_ == "IDAT":
|
||||
# convert IDAT to fdAT
|
||||
out.append(
|
||||
make_chunk("fdAT", struct.pack("!I", seq) + data[8:-4])
|
||||
)
|
||||
seq += 1
|
||||
else:
|
||||
other_chunks.append(data)
|
||||
|
||||
# end
|
||||
out.extend(other_chunks)
|
||||
out.append(png.end)
|
||||
|
||||
return b"".join(out)
|
||||
|
||||
@classmethod
|
||||
def from_files(cls, files, **options):
|
||||
"""Create an APNG from multiple files.
|
||||
|
||||
This is a shortcut of::
|
||||
|
||||
im = APNG()
|
||||
for file in files:
|
||||
im.append_file(file, **options)
|
||||
|
||||
:arg list files: A list of filename. See :meth:`PNG.open`.
|
||||
:arg dict options: Options for :class:`FrameControl`.
|
||||
:rtype: APNG
|
||||
"""
|
||||
im = cls()
|
||||
for file in files:
|
||||
im.append_file(file, **options)
|
||||
return im
|
||||
|
||||
@classmethod
|
||||
def from_bytes(cls, b):
|
||||
"""Create an APNG from raw bytes.
|
||||
|
||||
:arg bytes b: The raw bytes of the APNG file.
|
||||
:rtype: APNG
|
||||
"""
|
||||
hdr = None
|
||||
head_chunks = []
|
||||
end = ("IEND", make_chunk("IEND", b""))
|
||||
|
||||
frame_chunks = []
|
||||
frames = []
|
||||
num_plays = 0
|
||||
frame_has_head_chunks = False
|
||||
|
||||
control = None
|
||||
|
||||
for type_, data in parse_chunks(b):
|
||||
if type_ == "IHDR":
|
||||
hdr = data
|
||||
frame_chunks.append((type_, data))
|
||||
elif type_ == "acTL":
|
||||
_num_frames, num_plays = struct.unpack("!II", data[8:-4])
|
||||
continue
|
||||
elif type_ == "fcTL":
|
||||
if any(type_ == "IDAT" for type_, data in frame_chunks):
|
||||
# IDAT inside chunk, go to next frame
|
||||
frame_chunks.append(end)
|
||||
frames.append((PNG.from_chunks(frame_chunks), control))
|
||||
frame_has_head_chunks = False
|
||||
control = FrameControl.from_bytes(data[12:-4])
|
||||
# https://github.com/PyCQA/pylint/issues/2072
|
||||
# pylint: disable=typecheck
|
||||
hdr = make_chunk("IHDR", struct.pack("!II", control.width, control.height) + hdr[16:-4])
|
||||
frame_chunks = [("IHDR", hdr)]
|
||||
else:
|
||||
control = FrameControl.from_bytes(data[12:-4])
|
||||
elif type_ == "IDAT":
|
||||
if not frame_has_head_chunks:
|
||||
frame_chunks.extend(head_chunks)
|
||||
frame_has_head_chunks = True
|
||||
frame_chunks.append((type_, data))
|
||||
elif type_ == "fdAT":
|
||||
# convert to IDAT
|
||||
if not frame_has_head_chunks:
|
||||
frame_chunks.extend(head_chunks)
|
||||
frame_has_head_chunks = True
|
||||
frame_chunks.append(("IDAT", make_chunk("IDAT", data[12:-4])))
|
||||
elif type_ == "IEND":
|
||||
# end
|
||||
frame_chunks.append(end)
|
||||
frames.append((PNG.from_chunks(frame_chunks), control))
|
||||
break
|
||||
elif type_ in CHUNK_BEFORE_IDAT:
|
||||
head_chunks.append((type_, data))
|
||||
else:
|
||||
frame_chunks.append((type_, data))
|
||||
|
||||
o = cls()
|
||||
o.frames = frames
|
||||
o.num_plays = num_plays
|
||||
return o
|
||||
|
||||
@classmethod
|
||||
def open(cls, file):
|
||||
"""Open an APNG file.
|
||||
|
||||
:arg file: Input file.
|
||||
:type file: path-like or file-like.
|
||||
:rtype: APNG
|
||||
"""
|
||||
return cls.from_bytes(read_file(file))
|
||||
|
||||
def save(self, file):
|
||||
"""Save the entire image to a file.
|
||||
|
||||
:arg file: Output file.
|
||||
:type file: path-like or file-like
|
||||
"""
|
||||
write_file(file, self.to_bytes())
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,201 @@
|
||||
# Copyright (c) 2008-2011 Volvox Development Team
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
# Author: Konstantin Lepa <konstantin.lepa@gmail.com>
|
||||
|
||||
"""ANSI color formatting for output in terminal."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
import warnings
|
||||
from typing import Any, Iterable
|
||||
|
||||
|
||||
def __getattr__(name: str) -> list[str]:
|
||||
if name == "__ALL__":
|
||||
warnings.warn(
|
||||
"__ALL__ is deprecated and will be removed in termcolor 3. "
|
||||
"Use __all__ instead.",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
return ["colored", "cprint"]
|
||||
msg = f"module '{__name__}' has no attribute '{name}'"
|
||||
raise AttributeError(msg)
|
||||
|
||||
|
||||
ATTRIBUTES = {
|
||||
"bold": 1,
|
||||
"dark": 2,
|
||||
"underline": 4,
|
||||
"blink": 5,
|
||||
"reverse": 7,
|
||||
"concealed": 8,
|
||||
}
|
||||
|
||||
|
||||
HIGHLIGHTS = {
|
||||
"on_black": 40,
|
||||
"on_grey": 40, # Actually black but kept for backwards compatibility
|
||||
"on_red": 41,
|
||||
"on_green": 42,
|
||||
"on_yellow": 43,
|
||||
"on_blue": 44,
|
||||
"on_magenta": 45,
|
||||
"on_cyan": 46,
|
||||
"on_light_grey": 47,
|
||||
"on_dark_grey": 100,
|
||||
"on_light_red": 101,
|
||||
"on_light_green": 102,
|
||||
"on_light_yellow": 103,
|
||||
"on_light_blue": 104,
|
||||
"on_light_magenta": 105,
|
||||
"on_light_cyan": 106,
|
||||
"on_white": 107,
|
||||
}
|
||||
|
||||
COLORS = {
|
||||
"black": 30,
|
||||
"grey": 30, # Actually black but kept for backwards compatibility
|
||||
"red": 31,
|
||||
"green": 32,
|
||||
"yellow": 33,
|
||||
"blue": 34,
|
||||
"magenta": 35,
|
||||
"cyan": 36,
|
||||
"light_grey": 37,
|
||||
"dark_grey": 90,
|
||||
"light_red": 91,
|
||||
"light_green": 92,
|
||||
"light_yellow": 93,
|
||||
"light_blue": 94,
|
||||
"light_magenta": 95,
|
||||
"light_cyan": 96,
|
||||
"white": 97,
|
||||
}
|
||||
|
||||
|
||||
RESET = "\033[0m"
|
||||
|
||||
|
||||
def _can_do_colour(
|
||||
*, no_color: bool | None = None, force_color: bool | None = None
|
||||
) -> bool:
|
||||
"""Check env vars and for tty/dumb terminal"""
|
||||
# First check overrides:
|
||||
# "User-level configuration files and per-instance command-line arguments should
|
||||
# override $NO_COLOR. A user should be able to export $NO_COLOR in their shell
|
||||
# configuration file as a default, but configure a specific program in its
|
||||
# configuration file to specifically enable color."
|
||||
# https://no-color.org
|
||||
if no_color is not None and no_color:
|
||||
return False
|
||||
if force_color is not None and force_color:
|
||||
return True
|
||||
|
||||
# Then check env vars:
|
||||
if "ANSI_COLORS_DISABLED" in os.environ:
|
||||
return False
|
||||
if "NO_COLOR" in os.environ:
|
||||
return False
|
||||
if "FORCE_COLOR" in os.environ:
|
||||
return True
|
||||
return (
|
||||
hasattr(sys.stdout, "isatty")
|
||||
and sys.stdout.isatty()
|
||||
and os.environ.get("TERM") != "dumb"
|
||||
)
|
||||
|
||||
|
||||
def colored(
|
||||
text: str,
|
||||
color: str | None = None,
|
||||
on_color: str | None = None,
|
||||
attrs: Iterable[str] | None = None,
|
||||
*,
|
||||
no_color: bool | None = None,
|
||||
force_color: bool | None = None,
|
||||
) -> str:
|
||||
"""Colorize text.
|
||||
|
||||
Available text colors:
|
||||
black, red, green, yellow, blue, magenta, cyan, white,
|
||||
light_grey, dark_grey, light_red, light_green, light_yellow, light_blue,
|
||||
light_magenta, light_cyan.
|
||||
|
||||
Available text highlights:
|
||||
on_black, on_red, on_green, on_yellow, on_blue, on_magenta, on_cyan, on_white,
|
||||
on_light_grey, on_dark_grey, on_light_red, on_light_green, on_light_yellow,
|
||||
on_light_blue, on_light_magenta, on_light_cyan.
|
||||
|
||||
Available attributes:
|
||||
bold, dark, underline, blink, reverse, concealed.
|
||||
|
||||
Example:
|
||||
colored('Hello, World!', 'red', 'on_black', ['bold', 'blink'])
|
||||
colored('Hello, World!', 'green')
|
||||
"""
|
||||
if not _can_do_colour(no_color=no_color, force_color=force_color):
|
||||
return text
|
||||
|
||||
fmt_str = "\033[%dm%s"
|
||||
if color is not None:
|
||||
text = fmt_str % (COLORS[color], text)
|
||||
|
||||
if on_color is not None:
|
||||
text = fmt_str % (HIGHLIGHTS[on_color], text)
|
||||
|
||||
if attrs is not None:
|
||||
for attr in attrs:
|
||||
text = fmt_str % (ATTRIBUTES[attr], text)
|
||||
|
||||
return text + RESET
|
||||
|
||||
|
||||
def cprint(
|
||||
text: str,
|
||||
color: str | None = None,
|
||||
on_color: str | None = None,
|
||||
attrs: Iterable[str] | None = None,
|
||||
*,
|
||||
no_color: bool | None = None,
|
||||
force_color: bool | None = None,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Print colorized text.
|
||||
|
||||
It accepts arguments of print function.
|
||||
"""
|
||||
|
||||
print(
|
||||
(
|
||||
colored(
|
||||
text,
|
||||
color,
|
||||
on_color,
|
||||
attrs,
|
||||
no_color=no_color,
|
||||
force_color=force_color,
|
||||
)
|
||||
),
|
||||
**kwargs,
|
||||
)
|
||||
Reference in New Issue
Block a user