#
# Copyright (c) 2016 - 2024 -- Lars Heuer
# All rights reserved.
#
# License: BSD License
#
# type: ignore
import base64
import codecs
import functools
import gzip
import io
import re
import time
import zlib
from collections import defaultdict
from contextlib import contextmanager
from functools import partial, reduce
from itertools import chain, repeat, zip_longest
from operator import itemgetter
from struct import pack
from urllib.parse import quote
from xml.sax.saxutils import escape, quoteattr
from . import consts
from .utils import (
check_valid_border,
check_valid_scale,
get_border,
get_symbol_size,
matrix_iter,
matrix_iter_verbose,
matrix_to_lines,
)
__all__ = (
"writable",
"write_eps",
"write_pam",
"write_pbm",
"write_pdf",
"write_png",
"write_ppm",
"write_svg",
"write_terminal",
"write_tex",
"write_txt",
"write_xbm",
"write_xpm",
)
# Standard creator name
CREATOR = "Segno <https://pypi.org/project/segno/>"
@contextmanager
def writable(file_or_path, mode, encoding=None):
f = file_or_path
must_close = False
try:
file_or_path.write
if encoding is not None:
f = codecs.getwriter(encoding)(file_or_path)
except AttributeError:
f = open(file_or_path, mode, encoding=encoding)
must_close = True
try:
yield f
finally:
if must_close:
f.close()
def colorful(dark, light):
def decorate(f):
@functools.wraps(f)
def wrapper(
matrix,
matrix_size,
out,
dark=dark,
light=light,
finder_dark=False,
finder_light=False,
data_dark=False,
data_light=False,
version_dark=False,
version_light=False,
format_dark=False,
format_light=False,
alignment_dark=False,
alignment_light=False,
timing_dark=False,
timing_light=False,
separator=False,
dark_module=False,
quiet_zone=False,
**kw,
):
cm = _make_colormap(
*matrix_size,
dark=dark,
light=light,
finder_dark=finder_dark,
finder_light=finder_light,
data_dark=data_dark,
data_light=data_light,
version_dark=version_dark,
version_light=version_light,
format_dark=format_dark,
format_light=format_light,
alignment_dark=alignment_dark,
alignment_light=alignment_light,
timing_dark=timing_dark,
timing_light=timing_light,
separator=separator,
dark_module=dark_module,
quiet_zone=quiet_zone,
)
return f(matrix, matrix_size, out, cm, **kw)
return wrapper
return decorate
def _valid_width_height_and_border(matrix_size, scale, border):
check_valid_scale(scale)
check_valid_border(border)
border = get_border(matrix_size, border)
width, height = get_symbol_size(matrix_size, scale, border)
return width, height, border
@colorful(dark="#000", light=None)
def write_svg(
matrix,
matrix_size,
out,
colormap,
scale=1,
border=None,
xmldecl=True,
svgns=True,
title=None,
desc=None,
svgid=None,
svgclass="segno",
lineclass="qrline",
omitsize=False,
unit=None,
encoding="utf-8",
svgversion=None,
nl=True,
draw_transparent=False,
):
def svg_color(clr):
return (
_color_to_webcolor(clr, allow_css3_colors=allow_css3_colors)
if clr is not None
else None
)
def matrix_to_lines_verbose():
j = -0.5 # stroke width / 2
invalid_color = -1
for row in matrix_iter_verbose(matrix, matrix_size, scale=1, border=border):
last_color = invalid_color
x1, x2 = 0, 0
j += 1
for c in (colormap[mt] for mt in row):
if last_color != invalid_color and last_color != c:
yield last_color, (x1, x2, j)
x1 = x2
x2 += 1
last_color = c
yield last_color, (x1, x2, j)
width, height, border = _valid_width_height_and_border(matrix_size, scale, border)
unit = unit or ""
if unit and omitsize:
raise ValueError(
f'The unit "{unit}" has no effect if the size (width and height) is omitted.'
)
omit_encoding = encoding is None
if omit_encoding:
encoding = "utf-8"
allow_css3_colors = svgversion is not None and svgversion >= 2.0
is_multicolor = len(set(colormap.values())) > 2
need_background = (
not is_multicolor and colormap[consts.TYPE_QUIET_ZONE] is not None and not draw_transparent
)
need_svg_group = scale != 1 and (need_background or is_multicolor)
if is_multicolor:
miter = matrix_to_lines_verbose()
else:
x, y = border, border + 0.5
dark = colormap[consts.TYPE_DATA_DARK]
miter = ((dark, (x1, x2, y1)) for (x1, y1), (x2, y2) in matrix_to_lines(matrix, x, y))
xy = defaultdict(lambda: (0, 0))
coordinates = defaultdict(list)
for clr, (x1, x2, y1) in miter:
x, y = xy[clr]
coordinates[clr].append((x1 - x, y1 - y, x2 - x1))
xy[clr] = x2, y1
if need_background:
coordinates[colormap[consts.TYPE_QUIET_ZONE]] = [(0, 0, width // scale)]
if not draw_transparent:
try:
del coordinates[None]
except KeyError:
pass
paths = {}
scale_info = f' transform="scale({scale})"' if scale != 1 else ""
p = "<path{}{}".format(
scale_info if not need_svg_group else "",
"" if not lineclass else f" class={quoteattr(lineclass)}",
)
for color, coord in coordinates.items():
path = p
clr = svg_color(color)
if clr is not None:
opacity = None
if isinstance(clr, tuple):
clr, opacity = clr
path += f" stroke={quoteattr(clr)}"
if opacity is not None:
path += f" stroke-opacity={quoteattr(str(opacity))}"
path += ' d="'
path += "".join(
"{moveto}{x} {y}h{l}".format(
moveto=("m" if i > 0 else "M"),
x=x,
l=length,
y=(int(y) if int(y) == y else y),
)
for i, (x, y, length) in enumerate(coord)
)
path += '"/>'
paths[color] = path
if need_background:
k = colormap[consts.TYPE_QUIET_ZONE]
paths[k] = re.sub(
r'\sclass="[^"]+"',
"",
paths[k]
.replace("stroke", "fill")
.replace('"/>', f'v{height // scale}h-{width // scale}z"/>'),
)
svg = ""
if xmldecl:
svg += '<?xml version="1.0"'
if not omit_encoding:
svg += f" encoding={quoteattr(encoding)}"
svg += "?>\n"
svg += "<svg"
if svgns:
svg += ' xmlns="http://www.w3.org/2000/svg"'
if svgversion is not None and svgversion < 2.0:
svg += f" version={quoteattr(str(svgversion))}"
if not omitsize:
svg += f' width="{width}{unit}" height="{height}{unit}"'
if omitsize or unit:
svg += f' viewBox="0 0 {width} {height}"'
if svgid:
svg += f" id={quoteattr(svgid)}"
if svgclass:
svg += f" class={quoteattr(svgclass)}"
svg += ">"
if title is not None:
svg += f"<title>{escape(title)}</title>"
if desc is not None:
svg += f"<desc>{escape(desc)}</desc>"
if need_svg_group:
svg += f"<g{scale_info}>"
svg += "".join(sorted(paths.values(), key=len))
if need_svg_group:
svg += "</g>"
svg += "</svg>"
if nl:
svg += "\n"
with writable(out, "wt", encoding=encoding) as f:
f.write(svg)
_replace_quotes = partial(re.compile(rb'(=)"([^"]+)"').sub, rb"\1'\2'")
def as_svg_data_uri(
matrix,
matrix_size,
scale=1,
border=None,
xmldecl=False,
svgns=True,
title=None,
desc=None,
svgid=None,
svgclass="segno",
lineclass="qrline",
omitsize=False,
unit="",
encoding="utf-8",
svgversion=None,
nl=False,
encode_minimal=False,
omit_charset=False,
**kw,
):
encode = partial(quote, safe=b"") if not encode_minimal else partial(quote, safe=b" :/='")
buff = io.BytesIO()
write_svg(
matrix,
matrix_size,
buff,
scale=scale,
border=border,
xmldecl=xmldecl,
svgns=svgns,
title=title,
desc=desc,
svgclass=svgclass,
lineclass=lineclass,
omitsize=omitsize,
encoding=encoding,
svgid=svgid,
unit=unit,
svgversion=svgversion,
nl=nl,
**kw,
)
return f"data:image/svg+xml{(';charset=' + encoding if not omit_charset else '')}," + encode(
_replace_quotes(buff.getvalue())
)
def write_svg_debug(
matrix,
matrix_size,
out,
scale=15,
border=None,
fallback_color="fuchsia",
colormap=None,
add_legend=True,
): # pragma: no cover
clr_mapping = {
0x0: "#fff",
0x1: "#000",
0x2: "red",
0x3: "orange",
0x4: "gold",
0x5: "green",
}
if colormap is not None:
clr_mapping.update(colormap)
width, height, border = _valid_width_height_and_border(matrix_size, scale, border)
matrix_width, matrix_height = matrix_size
with writable(out, "wt", encoding="utf-8") as f:
legend = []
write = f.write
write('<?xml version="1.0" encoding="utf-8"?>\n')
write(f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {width} {height}">')
write(
'<style type="text/css"><![CDATA[ text { font-size: 1px; '
"font-family: Helvetica, Arial, sans; } ]]></style>"
)
write(f'<g transform="scale({scale})">')
for i in range(matrix_height):
y = i + border
for j in range(matrix_width):
x = j + border
bit = matrix[i][j]
if add_legend and bit not in (0x0, 0x1):
legend.append((x, y, bit))
fill = clr_mapping.get(bit, fallback_color)
write(f'<rect x="{x}" y="{y}" width="1" height="1" fill="{fill}"/>')
for x, y, val in legend:
write(f'<text x="{x + 0.2}" y="{y + 0.9}">{val}</text>')
write("</g></svg>\n")
def write_eps(matrix, matrix_size, out, scale=1, border=None, dark="#000", light=None):
import textwrap
def write_line(writemeth, content):
for line in textwrap.wrap(content, 254):
writemeth(line)
writemeth("\n")
def rgb_to_floats(clr):
def to_float(c):
if isinstance(c, float):
if not 0.0 <= c <= 1.0:
raise ValueError(f'Invalid color "{c}". Not in range 0 .. 1')
return c
return 1 / 255.0 * c if c != 1 else c
return tuple([to_float(i) for i in _color_to_rgb(clr)])
width, height, border = _valid_width_height_and_border(matrix_size, scale, border)
stroke_color_is_black = _color_is_black(dark)
stroke_color = dark if stroke_color_is_black else rgb_to_floats(dark)
with writable(out, "wt") as f:
writeline = partial(write_line, f.write)
writeline("%!PS-Adobe-3.0 EPSF-3.0")
writeline(f"%%Creator: {CREATOR}")
writeline(f"%%CreationDate: {time.strftime('%Y-%m-%d %H:%M:%S')}")
writeline("%%DocumentData: Clean7Bit")
writeline(f"%%BoundingBox: 0 0 {width} {height}")
writeline("/m { rmoveto } bind def")
writeline("/l { rlineto } bind def")
if light is not None:
writeline("{0:f} {1:f} {2:f} setrgbcolor clippath fill".format(*rgb_to_floats(light)))
if stroke_color_is_black:
writeline("0 0 0 setrgbcolor")
if not stroke_color_is_black:
writeline("{0:f} {1:f} {2:f} setrgbcolor".format(*stroke_color)) # noqa UP030
if scale != 1:
writeline(f"{scale} {scale} scale")
writeline("newpath")
y = get_symbol_size(matrix_size, scale=1, border=0)[1] + border - 0.5 # .5 = linewidth / 2
line_iter = matrix_to_lines(matrix, border, y, incby=-1)
(x1, y1), (x2, y2) = next(line_iter)
coord = [f"{x1} {y1} moveto {x2 - x1} 0 l"]
append_coord = coord.append
x = x2
for (x1, y1), (x2, y2) in line_iter:
append_coord(f" {x1 - x} {int(y1 - y)} m {x2 - x1} 0 l")
x, y = x2, y2
writeline("".join(coord))
writeline("stroke")
writeline("%%EOF")
def as_png_data_uri(matrix, matrix_size, scale=1, border=None, compresslevel=9, **kw):
buff = io.BytesIO()
write_png(
matrix,
matrix_size,
buff,
scale=scale,
border=border,
compresslevel=compresslevel,
**kw,
)
return f"data:image/png;base64,{base64.b64encode(buff.getvalue()).decode('ascii')}"
@colorful(dark="#000", light="#fff")
def write_png(matrix, matrix_size, out, colormap, scale=1, border=None, compresslevel=9, dpi=None):
def png_color(clr):
return _color_to_rgb_or_rgba(clr, alpha_float=False) if clr is not None else transparent
def chunk(name, data):
chunk_head = name + data
return pack(b">I", len(data)) + chunk_head + pack(b">I", zlib.crc32(chunk_head))
def scanline(row, filter_type=b"\0"):
return bytearray(
chain(
filter_type,
(
reduce(lambda x, y: (x << png_bit_depth) + y, e)
for e in zip_longest(*[iter(row)] * (8 // png_bit_depth), fillvalue=0x0)
),
)
)
scale = int(scale)
width, height, border = _valid_width_height_and_border(matrix_size, scale, border)
if dpi:
dpi = int(dpi)
if dpi < 0:
raise ValueError("DPI value must not be negative")
dpi = int(dpi // 0.0254)
black = (0, 0, 0)
white = (255, 255, 255)
transparent = (-1, -1, -1, -1) # Invalid placeholder for transparent color
dark_idx = consts.TYPE_FINDER_PATTERN_DARK
qz_idx = consts.TYPE_QUIET_ZONE
clr_map = {k: png_color(colormap[k]) for k in colormap}
# Creating a palette here regardless of the image type (greyscale vs. index-colors)
palette = sorted(set(clr_map.values()), key=itemgetter(0, 1, 2))
is_transparent = transparent in palette
number_of_colors = len(palette)
# Check if greyscale mode is applicable
is_greyscale = number_of_colors == 2 and all(
clr in (transparent, black, white) for clr in palette
)
png_color_type = 0 if is_greyscale else 3
png_bit_depth = 1 # Assume a bit depth of 1 (may change if PLTE is used)
png_trans_idx = None
if not is_greyscale: # PLTE
if number_of_colors > 2:
png_bit_depth = 2 if number_of_colors < 5 else 4
palette.sort(key=len, reverse=True) # RGBA colors first
if is_transparent:
png_trans_idx = 0
rgb_values = (
_NAME2RGB.values()
if len(palette[1]) == 3
else ((*clr, 0) for clr in _NAME2RGB.values())
)
# Choose a random color which becomes transparent.
transparent_color = next(clr for clr in rgb_values if clr not in palette)
palette[0] = transparent_color
# Replace the placeholder "transparent" with the actual RGB(A) value
clr_map.update(
{
module_type: transparent_color
for module_type, clr in clr_map.items()
if clr == transparent
}
)
elif is_transparent: # Greyscale and transparent
if black in palette:
# Since black is zero, it should be the first entry
palette = [black, transparent]
png_trans_idx = palette.index(transparent)
if number_of_colors > 2:
# Need the more expensive matrix iterator
miter = matrix_iter_verbose(matrix, matrix_size, scale=1, border=0)
color_index = {module_type: palette.index(clr) for module_type, clr in clr_map.items()}
else:
# Just two colors, use the cheap iterator which returns 0x0 or 0x1
miter = iter(matrix)
# The code to create the image requires that TYPE_QUIET_ZONE is available
color_index = {qz_idx: palette.index(clr_map[qz_idx])}
color_index.update({0: color_index[qz_idx], 1: palette.index(clr_map[dark_idx])})
miter = ((color_index[b] for b in r) for r in miter)
horizontal_border = b""
vertical_border = b""
if border > 0:
# Calculate horizontal and vertical border
qz_value = color_index[qz_idx]
horizontal_border = scanline(repeat(qz_value, width)) * border * scale
vertical_border = [qz_value] * border * scale
same_as_above = b""
if scale > 1:
# 2 == PNG Filter "Up" <https://www.w3.org/TR/PNG/#9-table91>
same_as_above = scanline(repeat(0x0, width), filter_type=b"\2") * (scale - 1)
miter = (chain(*(repeat(b, scale) for b in row)) for row in miter)
idat = bytearray(horizontal_border)
for row in miter:
# Chain precalculated left border with row and right border
idat += scanline(chain(vertical_border, row, vertical_border))
idat += same_as_above # This is b'' if no scaling factor was provided
idat += horizontal_border
with writable(out, "wb") as f:
write = f.write
write(b"\211PNG\r\n\032\n") # Magic number
# Header:
# width, height, bitdepth, colortype, compression meth., filter, interlance
write(
chunk(
b"IHDR",
pack(b">2I5B", width, height, png_bit_depth, png_color_type, 0, 0, 0),
)
)
if dpi:
write(chunk(b"pHYs", pack(b">LLB", dpi, dpi, 1)))
if not is_greyscale:
write(chunk(b"PLTE", b"".join(pack(b">3B", *clr[:3]) for clr in palette)))
# <https://www.w3.org/TR/PNG/#11tRNS>
if len(palette[0]) > 3: # Color with alpha channel is the first entry in the palette
write(
chunk(
b"tRNS",
b"".join(pack(b">B", clr[3]) for clr in palette if len(clr) > 3),
)
)
elif is_transparent:
write(chunk(b"tRNS", pack(b">B", png_trans_idx)))
elif is_transparent:
write(chunk(b"tRNS", pack(b">1H", png_trans_idx)))
write(chunk(b"IDAT", zlib.compress(idat, compresslevel)))
write(chunk(b"IEND", b""))
def write_pdf(
matrix,
matrix_size,
out,
scale=1,
border=None,
dark="#000",
light=None,
compresslevel=9,
):
def write_string(writemeth, s):
writemeth(s.encode("ascii"))
def to_pdf_color(clr):
def to_float(c):
if isinstance(c, float):
if not 0.0 <= c <= 1.0:
raise ValueError(f'Invalid color "{c}". Not in range 0 .. 1')
return c
return 1 / 255.0 * c if c != 1 else c
return tuple([to_float(i) for i in _color_to_rgb(clr)])
width, height, border = _valid_width_height_and_border(matrix_size, scale, border)
creation_date = f"{time.strftime('%Y%m%d%H%M%S')}{(time.timezone // 3600):+03d}'{(abs(time.timezone) % 60):02d}'"
cmds = []
append_cmd = cmds.append
if light is not None:
# If the background color is defined, a rect is drawn in the background
append_cmd("{} {} {} rg".format(*to_pdf_color(light)))
append_cmd(f"0 0 {width} {height} re")
append_cmd("f q")
# Set the stroke color only iff it is not black (default)
if not _color_is_black(dark):
append_cmd("{} {} {} RG".format(*to_pdf_color(dark)))
if scale > 1:
append_cmd(f"{scale} 0 0 {scale} 0 0 cm")
y = get_symbol_size(matrix_size, scale=1, border=0)[1] + border - 0.5
# Set the origin in the upper left corner
append_cmd(f"1 0 0 1 {border} {y} cm")
miter = matrix_to_lines(matrix, 0, 0, incby=-1)
# PDF supports absolute coordinates, only
cmds.extend(f"{x1} {y1} m {x2} {y1} l" for (x1, y1), (x2, y2) in miter)
append_cmd("S")
graphic = zlib.compress((" ".join(cmds)).encode("ascii"), compresslevel)
with writable(out, "wb") as f:
write = f.write
writestr = partial(write_string, write)
object_pos = []
write(b"%PDF-1.4\r%\xe2\xe3\xcf\xd3\r\n")
for obj in (
"obj <</Type /Catalog /Pages 2 0 R>>\r\nendobj\r\n",
"obj <</Type /Pages /Kids [3 0 R] /Count 1>>\r\nendobj\r\n",
f"obj <</Type /Page /Parent 2 0 R /MediaBox [0 0 {width} {height}] /Contents 4 0 R>>\r\nendobj\r\n",
f"obj <</Length {len(graphic)} /Filter /FlateDecode>>\r\nstream\r\n",
):
object_pos.append(f.tell())
writestr(f"{len(object_pos)} 0 {obj}")
write(graphic)
write(b"\r\nendstream\r\nendobj\r\n")
object_pos.append(f.tell())
writestr(
f"{len(object_pos)} 0 obj <</CreationDate(D:{creation_date})"
f"/Producer({CREATOR})/Creator({CREATOR})\r\n>>\r\nendobj\r\n"
)
object_pos.append(f.tell())
writestr(f"xref\r\n0 {len(object_pos)}\r\n0000000000 65535 f\r\n")
for pos in object_pos[:-1]:
writestr(f"{pos:010d} {0:05d} n\r\n")
writestr(f"trailer <</Size {len(object_pos)}/Root 1 0 R/Info 5 0 R>>\r\n")
xref_location = object_pos[-1]
writestr(f"startxref\r\n{xref_location}\r\n%%EOF\r\n")
def write_txt(matrix, matrix_size, out, border=None, dark="1", light="0"):
row_iter = matrix_iter(matrix, matrix_size, scale=1, border=border)
colours = (str(light), str(dark))
with writable(out, "wt") as f:
write = f.write
for row in row_iter:
write("".join(colours[i] for i in row))
write("\n")
def write_pbm(matrix, matrix_size, out, scale=1, border=None, plain=False):
def pack_row(iterable):
return (
reduce(lambda x, y: (x << 1) + y, e)
for e in zip_longest(*[iter(iterable)] * 8, fillvalue=0x0)
)
width, height, border = _valid_width_height_and_border(matrix_size, scale, border)
row_iter = matrix_iter(matrix, matrix_size, scale, border)
with writable(out, "wb") as f:
write = f.write
write(
f"{('P4' if not plain else 'P1')}\n# Created by {CREATOR}\n{width} {height}\n".encode(
"ascii"
)
)
if not plain:
for row in row_iter:
write(bytearray(pack_row(row)))
else:
for row in row_iter:
write(b"".join(str(i).encode("ascii") for i in row))
write(b"\n")
def write_pam(matrix, matrix_size, out, scale=1, border=None, dark="#000", light="#fff"):
def invert_row_bits(row):
return bytearray([b ^ 0x1 for b in row])
def row_to_color_values(row, colours):
return b"".join(colours[b] for b in row)
if not dark:
raise ValueError(f'Invalid stroke color "{dark}"')
width, height, border = _valid_width_height_and_border(matrix_size, scale, border)
row_iter = matrix_iter(matrix, matrix_size, scale, border)
depth, maxval, tuple_type = 1, 1, "BLACKANDWHITE"
transparency = False
stroke_color = _color_to_rgb_or_rgba(dark, alpha_float=False)
bg_color = _color_to_rgb_or_rgba(light, alpha_float=False) if light is not None else None
colored_stroke = not (_color_is_black(stroke_color) or _color_is_white(stroke_color))
if bg_color is None:
tuple_type = "GRAYSCALE_ALPHA" if not colored_stroke else "RGB_ALPHA"
transparency = True
bg_color = _invert_color(stroke_color[:3])
bg_color += (0,)
if len(stroke_color) != 4:
stroke_color += (255,)
elif colored_stroke or not (_color_is_black(bg_color) or _color_is_white(bg_color)):
tuple_type = "RGB"
is_rgb = tuple_type.startswith("RGB")
colours = None
if not is_rgb and transparency:
depth = 2
colours = (b"\x01\x00", b"\x00\x01")
elif is_rgb:
maxval = max(chain(stroke_color, bg_color))
depth = 3 if not transparency else 4
fmt = f">{depth}B".encode("ascii")
colours = (pack(fmt, *bg_color), pack(fmt, *stroke_color))
row_filter = (
invert_row_bits if colours is None else partial(row_to_color_values, colours=colours)
)
with writable(out, "wb") as f:
write = f.write
write(
"P7\n"
f"# Created by {CREATOR}\n"
f"WIDTH {width}\n"
f"HEIGHT {height}\n"
f"DEPTH {depth}\n"
f"MAXVAL {maxval}\n"
f"TUPLTYPE {tuple_type}\n"
"ENDHDR\n".encode("ascii")
)
for row in row_iter:
write(row_filter(row))
@colorful(dark="#000", light="#fff")
def write_ppm(matrix, matrix_size, out, colormap, scale=1, border=None):
scale = int(scale)
width, height, border = _valid_width_height_and_border(matrix_size, scale, border)
if None in colormap.values():
raise ValueError("Transparency is not supported")
for mt, clr in colormap.items():
colormap[mt] = _color_to_rgb(clr)
row_iter = matrix_iter_verbose(matrix, matrix_size, scale, border)
with writable(out, "wb") as f:
write = f.write
write(f"P6 # Created by {CREATOR}\n{width} {height} 255\n".encode("ascii"))
for row in row_iter:
write(b"".join(pack(b">3B", *colormap[mt]) for mt in row))
def write_xpm(
matrix,
matrix_size,
out,
scale=1,
border=None,
dark="#000",
light="#fff",
name="img",
):
width, height, border = _valid_width_height_and_border(matrix_size, scale, border)
row_iter = matrix_iter(matrix, matrix_size, scale, border)
stroke_color = color_to_rgb_hex(dark) if dark is not None else "None"
bg_color = color_to_rgb_hex(light) if light is not None else "None"
with writable(out, "wt") as f:
write = f.write
write(
"/* XPM */\n"
f"static char *{name}[] = {{\n"
f'"{width} {height} 2 1",\n'
f'" c {bg_color}",\n'
f'"X c {stroke_color}",\n'
)
for i, row in enumerate(row_iter):
write(
"".join(
chain(
['"'],
(" " if not b else "X" for b in row),
[f'"{("," if i < height - 1 else "")}\n'],
)
)
)
write("};\n")
def write_xbm(matrix, matrix_size, out, scale=1, border=None, name="img"):
width, height, border = _valid_width_height_and_border(matrix_size, scale, border)
row_iter = matrix_iter(matrix, matrix_size, scale, border)
with writable(out, "wt") as f:
write = f.write
write(
f"#define {name}_width {width}\n"
f"#define {name}_height {height}\n"
f"static unsigned char {name}_bits[] = {{\n"
)
for i, row in enumerate(row_iter, start=1):
iter_ = zip_longest(*[iter(row)] * 8, fillvalue=0x0)
# Reverse bits since XBM uses little endian
bits = [f"0x{reduce(lambda x, y: (x << 1) + y, bits[::-1]):02x}" for bits in iter_]
write(" ")
write(", ".join(bits))
write(",\n" if i < height else "\n")
write("};\n")
def write_tex(matrix, matrix_size, out, scale=1, border=None, dark="black", unit="pt", url=None):
def point(x, y):
return f"\\pgfqpoint{{{x}{unit}}}{{{y}{unit}}}"
check_valid_scale(scale)
check_valid_border(border)
border = get_border(matrix_size, border)
end_marker = ""
with writable(out, "wt") as f:
write = f.write
write(f"% Creator: {CREATOR}\n")
write(f"% Date: {time.strftime('%Y-%m-%dT%H:%M:%S')}\n")
if url:
write(f"\\href{{{url}}}{{")
end_marker = "}"
write("\\begin{pgfpicture}\n")
write(f" \\pgfsetlinewidth{{{scale}{unit}}}\n")
if dark and dark != "black":
write(f" \\color{{{dark}}}\n")
x, y = border, -border
for (x1, y1), (x2, y2) in matrix_to_lines(matrix, x, y, incby=-1):
write(f" \\pgfpathmoveto{{{point(x1 * scale, y1 * scale)}}}\n")
write(f" \\pgfpathlineto{{{point(x2 * scale, y2 * scale)}}}\n")
write(" \\pgfusepath{stroke}\n")
write(f"\\end{{pgfpicture}}{end_marker}\n")
def write_terminal(matrix, matrix_size, out, border=None):
with writable(out, "wt") as f:
write = f.write
colours = [f"\033[{i}m" for i in (7, 49)]
for row in matrix_iter(matrix, matrix_size, scale=1, border=border):
prev_bit = -1
cnt = 0
for bit in row:
if bit == prev_bit:
cnt += 1
else:
if cnt:
write(colours[prev_bit])
write(" " * cnt)
write("\033[0m") # reset color
prev_bit = bit
cnt = 1
if cnt:
write(colours[prev_bit])
write(" " * cnt)
write("\033[0m") # reset color
write("\n")
def write_terminal_win(matrix, matrix_size, border=None): # pragma: no cover
import ctypes
import struct
import sys
write = sys.stdout.write
std_out = ctypes.windll.kernel32.GetStdHandle(-11)
csbi = ctypes.create_string_buffer(22)
res = ctypes.windll.kernel32.GetConsoleScreenBufferInfo(std_out, csbi)
if not res:
raise OSError("Cannot find information about the console. Not running on the command line?")
default_color = struct.unpack(b"hhhhHhhhhhh", csbi.raw)[4]
set_color = partial(ctypes.windll.kernel32.SetConsoleTextAttribute, std_out)
colours = (240, default_color)
for row in matrix_iter(matrix, matrix_size, scale=1, border=border):
prev_bit = -1
cnt = 0
for bit in row:
if bit == prev_bit:
cnt += 1
else:
if cnt:
set_color(colours[prev_bit])
write(" " * cnt)
prev_bit = bit
cnt = 1
if cnt:
set_color(colours[prev_bit])
write(" " * cnt)
set_color(default_color) # reset color
write("\n")
def write_terminal_compact(matrix, matrix_size, out, border=None):
blocks = {
(1, 1): " ",
(0, 1): "\u2580", # Upper half block
(1, 0): "\u2584", # Lower half block
(0, 0): "\u2588", # Full block
}
it = [matrix_iter(matrix, matrix_size, scale=1, border=border)] * 2
with writable(out, "wt") as f:
write = f.write
for top_row, bottom_row in zip_longest(*it, fillvalue=repeat(1)):
write("".join(blocks[pair] for pair in zip(top_row, bottom_row)))
write("\n")
def _color_to_rgb_or_rgba(color, alpha_float=True):
rgba = _color_to_rgba(color, alpha_float=alpha_float)
if rgba[3] in (1.0, 255):
return rgba[:3]
return rgba
def _color_to_webcolor(color, allow_css3_colors=True, optimize=True):
if _color_is_black(color):
return "#000"
elif _color_is_white(color):
return "#fff"
clr = _color_to_rgb_or_rgba(color)
alpha_channel = None
if len(clr) == 4:
if allow_css3_colors:
return "rgba({0},{1},{2},{3})".format(*clr) # noqa UP030
alpha_channel = clr[3]
clr = clr[:3]
hx = "#{0:02x}{1:02x}{2:02x}".format(*clr) # noqa UP030
if optimize:
if hx == "#d2b48c":
hx = "tan" # shorter
elif hx == "#ff0000":
hx = "red" # shorter
elif hx[1] == hx[2] and hx[3] == hx[4] and hx[5] == hx[6]:
hx = f"#{hx[1]}{hx[3]}{hx[5]}"
return hx if alpha_channel is None else (hx, alpha_channel)
def color_to_rgb_hex(color):
return "#{0:02x}{1:02x}{2:02x}".format(*_color_to_rgb(color)) # noqa UP030
def _color_is_black(color):
try:
color = color.lower()
except AttributeError:
pass
return color in (
"#000",
"#000000",
"black",
(0, 0, 0),
(0, 0, 0, 255),
(0, 0, 0, 1.0),
)
def _color_is_white(color):
try:
color = color.lower()
except AttributeError:
pass
return color in (
"#fff",
"#ffffff",
"white",
(255, 255, 255),
(255, 255, 255, 255),
(255, 255, 255, 1.0),
)
def _color_to_rgb(color):
rgb = _color_to_rgb_or_rgba(color)
if len(rgb) != 3:
raise ValueError(
f'The alpha channel {rgb[3]} in color "{color}" cannot be converted to RGB'
)
return rgb
def _color_to_rgba(color, alpha_float=True):
res = []
alpha_channel = (1.0,) if alpha_float else (255,)
if isinstance(color, tuple):
col_length = len(color)
is_valid = False
if 3 <= col_length <= 4:
for i, part in enumerate(color[:3]):
is_valid = 0 <= part <= 255
res.append(part)
if not is_valid or i == 2:
break
if is_valid:
if col_length == 4:
res.append(_alpha_value(color[3], alpha_float))
else:
res.append(alpha_channel[0])
if is_valid:
return tuple(res)
raise ValueError(f'Unsupported color "{color}"')
try:
return _NAME2RGB[color.lower()] + alpha_channel
except KeyError:
try:
clr = _hex_to_rgb_or_rgba(color, alpha_float=alpha_float)
if len(clr) == 4:
return clr
else:
return clr + alpha_channel
except ValueError:
raise ValueError(
f'Unsupported color "{color}". Neither a known web '
"color name nor a color in hexadecimal format."
)
def _hex_to_rgb_or_rgba(color, alpha_float=True):
if color[0] == "#":
color = color[1:]
if 2 < len(color) < 5:
# Expand RGB -> RRGGBB and RGBA -> RRGGBBAA
color = "".join([color[i] * 2 for i in range(len(color))])
color_len = len(color)
if color_len not in (6, 8):
raise ValueError(f"Input #{color} is not in #RRGGBB nor in #RRGGBBAA format")
res = tuple([int(color[i : i + 2], 16) for i in range(0, color_len, 2)])
if alpha_float and color_len == 8:
res = res[:3] + (_alpha_value(res[3], alpha_float),)
return res
_ALPHA_COMMONS = {255: 1.0, 128: 0.5, 64: 0.25, 32: 0.125, 16: 0.625, 0: 0.0}
def _alpha_value(color, alpha_float):
if alpha_float:
if not isinstance(color, float):
if 0 <= color <= 255:
return _ALPHA_COMMONS.get(color, float("%.02f" % (color / 255.0)))
else:
if 0 <= color <= 1.0:
return color
else:
if not isinstance(color, float):
if 0 <= color <= 255:
return color
else:
if 0 <= color <= 1.0:
return color * 255.0
raise ValueError(f"Invalid alpha channel value: {color}")
def _invert_color(rgb_or_rgba):
return tuple([255 - c for c in rgb_or_rgba])
# <https://www.w3.org/TR/css-color-3/#svg-color>
_NAME2RGB = {
"aliceblue": (240, 248, 255),
"antiquewhite": (250, 235, 215),
"aqua": (0, 255, 255),
"aquamarine": (127, 255, 212),
"azure": (240, 255, 255),
"beige": (245, 245, 220),
"bisque": (255, 228, 196),
"black": (0, 0, 0),
"blanchedalmond": (255, 235, 205),
"blue": (0, 0, 255),
"blueviolet": (138, 43, 226),
"brown": (165, 42, 42),
"burlywood": (222, 184, 135),
"cadetblue": (95, 158, 160),
"chartreuse": (127, 255, 0),
"chocolate": (210, 105, 30),
"coral": (255, 127, 80),
"cornflowerblue": (100, 149, 237),
"cornsilk": (255, 248, 220),
"crimson": (220, 20, 60),
"cyan": (0, 255, 255),
"darkblue": (0, 0, 139),
"darkcyan": (0, 139, 139),
"darkgoldenrod": (184, 134, 11),
"darkgray": (169, 169, 169),
"darkgreen": (0, 100, 0),
"darkgrey": (169, 169, 169),
"darkkhaki": (189, 183, 107),
"darkmagenta": (139, 0, 139),
"darkolivegreen": (85, 107, 47),
"darkorange": (255, 140, 0),
"darkorchid": (153, 50, 204),
"darkred": (139, 0, 0),
"darksalmon": (233, 150, 122),
"darkseagreen": (143, 188, 143),
"darkslateblue": (72, 61, 139),
"darkslategray": (47, 79, 79),
"darkslategrey": (47, 79, 79),
"darkturquoise": (0, 206, 209),
"darkviolet": (148, 0, 211),
"deeppink": (255, 20, 147),
"deepskyblue": (0, 191, 255),
"dimgray": (105, 105, 105),
"dimgrey": (105, 105, 105),
"dodgerblue": (30, 144, 255),
"firebrick": (178, 34, 34),
"floralwhite": (255, 250, 240),
"forestgreen": (34, 139, 34),
"fuchsia": (255, 0, 255),
"gainsboro": (220, 220, 220),
"ghostwhite": (248, 248, 255),
"gold": (255, 215, 0),
"goldenrod": (218, 165, 32),
"gray": (128, 128, 128),
"green": (0, 128, 0),
"greenyellow": (173, 255, 47),
"grey": (128, 128, 128),
"honeydew": (240, 255, 240),
"hotpink": (255, 105, 180),
"indianred": (205, 92, 92),
"indigo": (75, 0, 130),
"ivory": (255, 255, 240),
"khaki": (240, 230, 140),
"lavender": (230, 230, 250),
"lavenderblush": (255, 240, 245),
"lawngreen": (124, 252, 0),
"lemonchiffon": (255, 250, 205),
"lightblue": (173, 216, 230),
"lightcoral": (240, 128, 128),
"lightcyan": (224, 255, 255),
"lightgoldenrodyellow": (250, 250, 210),
"lightgray": (211, 211, 211),
"lightgreen": (144, 238, 144),
"lightgrey": (211, 211, 211),
"lightpink": (255, 182, 193),
"lightsalmon": (255, 160, 122),
"lightseagreen": (32, 178, 170),
"lightskyblue": (135, 206, 250),
"lightslategray": (119, 136, 153),
"lightslategrey": (119, 136, 153),
"lightsteelblue": (176, 196, 222),
"lightyellow": (255, 255, 224),
"lime": (0, 255, 0),
"limegreen": (50, 205, 50),
"linen": (250, 240, 230),
"magenta": (255, 0, 255),
"maroon": (128, 0, 0),
"mediumaquamarine": (102, 205, 170),
"mediumblue": (0, 0, 205),
"mediumorchid": (186, 85, 211),
"mediumpurple": (147, 112, 219),
"mediumseagreen": (60, 179, 113),
"mediumslateblue": (123, 104, 238),
"mediumspringgreen": (0, 250, 154),
"mediumturquoise": (72, 209, 204),
"mediumvioletred": (199, 21, 133),
"midnightblue": (25, 25, 112),
"mintcream": (245, 255, 250),
"mistyrose": (255, 228, 225),
"moccasin": (255, 228, 181),
"navajowhite": (255, 222, 173),
"navy": (0, 0, 128),
"oldlace": (253, 245, 230),
"olive": (128, 128, 0),
"olivedrab": (107, 142, 35),
"orange": (255, 165, 0),
"orangered": (255, 69, 0),
"orchid": (218, 112, 214),
"palegoldenrod": (238, 232, 170),
"palegreen": (152, 251, 152),
"paleturquoise": (175, 238, 238),
"palevioletred": (219, 112, 147),
"papayawhip": (255, 239, 213),
"peachpuff": (255, 218, 185),
"peru": (205, 133, 63),
"pink": (255, 192, 203),
"plum": (221, 160, 221),
"powderblue": (176, 224, 230),
"purple": (128, 0, 128),
"red": (255, 0, 0),
"rosybrown": (188, 143, 143),
"royalblue": (65, 105, 225),
"saddlebrown": (139, 69, 19),
"salmon": (250, 128, 114),
"sandybrown": (244, 164, 96),
"seagreen": (46, 139, 87),
"seashell": (255, 245, 238),
"sienna": (160, 82, 45),
"silver": (192, 192, 192),
"skyblue": (135, 206, 235),
"slateblue": (106, 90, 205),
"slategray": (112, 128, 144),
"slategrey": (112, 128, 144),
"snow": (255, 250, 250),
"springgreen": (0, 255, 127),
"steelblue": (70, 130, 180),
"tan": (210, 180, 140),
"teal": (0, 128, 128),
"thistle": (216, 191, 216),
"tomato": (255, 99, 71),
"turquoise": (64, 224, 208),
"violet": (238, 130, 238),
"wheat": (245, 222, 179),
"white": (255, 255, 255),
"whitesmoke": (245, 245, 245),
"yellow": (255, 255, 0),
"yellowgreen": (154, 205, 50),
}
def _make_colormap(
matrix_width,
matrix_height,
dark,
light,
finder_dark=False,
finder_light=False,
data_dark=False,
data_light=False,
version_dark=False,
version_light=False,
format_dark=False,
format_light=False,
alignment_dark=False,
alignment_light=False,
timing_dark=False,
timing_light=False,
separator=False,
dark_module=False,
quiet_zone=False,
):
unsupported = ()
is_square = matrix_width == matrix_height
if not is_square: # rMQR
unsupported = [
consts.TYPE_DARKMODULE,
consts.TYPE_VERSION_DARK,
consts.TYPE_VERSION_LIGHT,
]
if matrix_width < 43: # rMQR R11x27, R13x27, …
unsupported.extend(
(
consts.TYPE_ALIGNMENT_PATTERN_DARK,
consts.TYPE_ALIGNMENT_PATTERN_LIGHT,
)
)
elif matrix_width < 45: # QR Code version 7
unsupported = [consts.TYPE_VERSION_DARK, consts.TYPE_VERSION_LIGHT]
if matrix_width < 21: # Lesser than QR Code version 1 => Micro QR code
unsupported.extend(
[
consts.TYPE_DARKMODULE,
consts.TYPE_ALIGNMENT_PATTERN_DARK,
consts.TYPE_ALIGNMENT_PATTERN_LIGHT,
]
)
mt2color = {
consts.TYPE_FINDER_PATTERN_DARK: finder_dark if finder_dark is not False else dark,
consts.TYPE_FINDER_PATTERN_LIGHT: finder_light if finder_light is not False else light,
consts.TYPE_DATA_DARK: data_dark if data_dark is not False else dark,
consts.TYPE_DATA_LIGHT: data_light if data_light is not False else light,
consts.TYPE_VERSION_DARK: version_dark if version_dark is not False else dark,
consts.TYPE_VERSION_LIGHT: version_light if version_light is not False else light,
consts.TYPE_ALIGNMENT_PATTERN_DARK: alignment_dark if alignment_dark is not False else dark,
consts.TYPE_ALIGNMENT_PATTERN_LIGHT: alignment_light
if alignment_light is not False
else light,
consts.TYPE_TIMING_DARK: timing_dark if timing_dark is not False else dark,
consts.TYPE_TIMING_LIGHT: timing_light if timing_light is not False else light,
consts.TYPE_FORMAT_DARK: format_dark if format_dark is not False else dark,
consts.TYPE_FORMAT_LIGHT: format_light if format_light is not False else light,
consts.TYPE_SEPARATOR: separator if separator is not False else light,
consts.TYPE_DARKMODULE: dark_module if dark_module is not False else dark,
consts.TYPE_QUIET_ZONE: quiet_zone if quiet_zone is not False else light,
}
return {mt: val for mt, val in mt2color.items() if mt not in unsupported}
_VALID_SERIALIZERS = {
"svg": write_svg,
"png": write_png,
"eps": write_eps,
"txt": write_txt,
"pdf": write_pdf,
"ans": write_terminal,
"pbm": write_pbm,
"pam": write_pam,
"ppm": write_ppm,
"tex": write_tex,
"xbm": write_xbm,
"xpm": write_xpm,
}
def save(matrix, matrix_size, out, kind=None, **kw):
is_stream = False
if kind is None:
try:
fname = out.name
is_stream = True
except AttributeError:
fname = out
ext = fname[fname.rfind(".") + 1 :].lower()
else:
ext = kind.lower()
is_svgz = not is_stream and ext == "svgz"
try:
serializer = _VALID_SERIALIZERS[ext if not is_svgz else "svg"]
except KeyError:
raise ValueError(f'Unknown file extension ".{ext}"')
if is_svgz:
with gzip.open(out, "wb", compresslevel=kw.pop("compresslevel", 9)) as f:
serializer(matrix, matrix_size, f, **kw)
else:
serializer(matrix, matrix_size, out, **kw)