Канал от гиков для гиков. Новости технологий и ссылки на новые каналы ИТ и научной тематики.
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg xmlns="<http://www.w3.org/2000/svg>" viewBox="0, 0, 64, 64">
<rect x="0" y="0" width="64" height="64" style="fill:#933" />
<rect x="32" y="0" width="32" height="64" style="fill:#393" />
</svg>
Кажется, что комментировать тут особо и нечего - сначала рисуем один прямоугольник во всю величину аватарки. А затем на ней начинаем рисовать прямоугольники второго цвета.
Итак, даже тщательно не вглядываясь в XML можно приметить, что условно данные можно разделить на две части:
- Основной блок в котором мы задаем размер области видимости. То есть главный прямоугольник в который и будет вписана аватарка. И в этот блок мы вписываем сгенерированные прямоугольники. Зададим значение вот такой чудесной строкой:
SVG_WRAPPER = """<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg xmlns="<http://www.w3.org/2000/svg>" viewBox="0, 0, {width}, {height}">
{rects}
</svg>
"""
- И множество блоков прямоугольников, которые будут генерироваться из вот такой строки:
SVG_RECT_TAG = '<rect x="{x}" y="{y}" width="{width}" height="{height}" style="fill:{color}" />
Для получения готовых к использованию фрагментов XML будем использовать format для подмены значений в строках. И теперь мы можем написать функцию для создания svg-тега прямоугольника:
def make_svg_cell(
x: int,
y: int,
offset: int,
size: int,
color: Optional[str],
) -> str:
return SVG_RECT_TAG.format(
x=x*size+offset,
y=y*size+offset,
size=size,
color=color,
)
И пробежав по списку со списками сырых значений мы можем сгенерировать все прямоугольники и подставить их в строку SVG_WRAPPER. Готовая функция примет следующий вид:
def make_svg_data(
data: List[List[bool]],
cell_size: int,
offset: int,
first_color: Optional[str],
second_color: Optional[str],
) -> str:
box_size = len(data) * cell_size + 2 * offset
box = make_svg_cell(
x=0,
y=0,
offset=0,
size=box_size,
color=first_color,
)
rects: List[str] = [box]
for y, row in enumerate(data):
for x, cell in enumerate(row):
if not cell:
continue
rect = make_svg_cell(
x=x,
y=y,
offset=offset,
size=cell_size,
color=second_color,
)
rects.append(rect)
svg_xml = SVG_WRAPPER.format(
rects="\\n".join(rects),
size=box_size,
color=first_color,
)
return svg_xml
И завершающий шаг, на котором можно проверить результат работы скрипта:
data = generate_data()
svg = make_svg_data(data, cell_size=16, offset=8, first_color="#fff", second_color="#f6f")
with open("test.svg", "w") as file:
file.write(data)
Теперь можно открыть файл и полюбоваться на сгенеренное. Если хочется растровое изображение, то самый простой способ его получить - конвертнув его из SVG (на pypi можно найти пакет CairoSVG, но в системе должна стоять либа cairo).from random import random
def random_bool() -> bool:
return random() < 0.5
Пожалуй, на этой славной ноте и закончим подготовительные работы. Булево значение для нас будет индикатором того, закрашена ли точка на изображении или все-таки нет.
Генерация необходимых данных
Вторым шагом мы непременно должны создать список из булевых значений заданного размера, который у нас будет ответственен за ряд точек в изображении:
def generate_row(
cells_count: int,
) -> List[bool]:
return [random_bool() for _ in range(cells_count)]
Не то чтоб я каждую строку кода хочу выносить в отдельную функцию, но пока мне так больше нравится. Итак, шажок номер три, чтоб немного приблизиться к величию GitHub, нам необходимо генерировать три блока с данными, а затем склеивать их. Очевидно, что мы можем написать функцию, которая будет принимать целочисленное количество рядов в сетке значений и количество точек в этих рядах. Будем ожидать, что данная функция нам будет отдавать список из списков булевых значений:
def generate_block(
width: int,
height: int,
) -> List[List[bool]]:
return [generate_row(width) for _ in range(height)]
Теперь мы можем создать левый и центральный блок. Для правого блока нам ничего нового не нужно, а достаточно просто отзеркалить левый блок.
И вот, свет увидела еще одна функция в одну строчку:
def reverse_block(
block: List[List[bool]],
) -> List[List[bool]]:
return [list(reversed(i)) for i in block]
Теперь мы можем сгенерировать все три части изображения. Вот приблизительно как-то так:
left = generate_block(width=cells_count // 2, height=cells_count)
spacer = generate_block(width=cells_count % 2, height=cells_count)
right = reverse_block(left)
Очевидно, что нам надо как-то их соединить, и для этого мы напишем вот такую функцию:
def concatenate_blocks(
left: List[List[bool]],
spacer: List[List[bool]],
right: List[List[bool]],
) -> List[List[bool]]:
result = list()
for i, left_row in enumerate(left):
right_row = right[i]
spacer_row = spacer[i]
line = [*left_row, *spacer_row, *right_row]
result.append(line)
return result
И вуаля!
def generate_data(
cells_count: int = 5,
) -> List[List[bool]]:
left = generate_block(width=cells_count // 2, height=cells_count)
spacer = generate_block(width=cells_count % 2, height=cells_count)
right = reverse_block(left)
return concatenate_blocks(left, spacer, right)
Можно запустить данную функцию и вывести результат каким-то таким образом:
>>> from pprint import pprint
>>> data = generate_data()
>>> pprint(data)
[[False, True, True, True, False],
[False, True, False, True, False],
[False, True, True, True, False],
[False, True, True, True, False],
[True, False, False, False, True]]
Запускаем и любуемся на полученный результат из пяти списков. Во время просмотра результата работы скрипта задумываемся над тем, что нам нужна была картинка, а не вот это вот все, что есть на экране, и продолжаем писать код.
Пока, в сугубо отладочных целях, напишем страшенную функцию для быстрой визуализации такого списка, один раз её прогоним и забудем её как страшный сон:
>>> def data_to_str(data: List[List[bool]]) -> str:
>>> return "\\n".join(list("".join(list(map(lambda x: "#" if x else " ", row))) for row in data))
>>> print(data)
###
# #
###
###
# #