СПЕЦИФИКАЦИЯ ВИЗИТКИ¶

Размер визитки¶

  • 50 × 90 мм при 600 dpi → 1181 × 2125 px

Отступы и полосы¶

  • top = H_PX × 13% → 153 px
  • row = H_PX × 13% → 153 px
  • Центр полосы i: top + i × row + row // 2
  • strip0_center = 229, strip1_center = 382, strip2_center = 535, strip3_center = 688, strip4_center = 841, strip5_center = 994

Текст¶

  • Шрифт ФИО: int(row × 0.70) = 107
  • Шрифт ссылок: int(row × 0.40) = 61
  • Шрифт: Manrope-Bold для ФИО, Manrope-Regular для ссылок
  • Выравнивание: центр буквы 'е'/'e' в надписи = центр полосы
  • ФИО выравнивается по strip0, strip1, strip2
  • Ссылки выравниваются по strip3, strip4, strip5

Фото¶

  • Квадратное, маска — круг
  • Размер: img_w = 2 × (strip1_center − top) — по вертикали остаётся на центре strip1
  • Горизонтально: фото центрировано посередине левой области
  • Отступ слева = отступ сверху = top
  • Позиция: photo_x = top, photo_y = strip1_center − img_w//2

ФИО¶

  • Сдвинуто влево: расстояние между фото и левым краем (= top) равно расстоянию между фото и ФИО
  • Позиция: col_x = photo_x + img_w + top = 2 × top + img_w

QR-код¶

  • Размер: 300 px
  • Центр QR совпадает с центром полосы 4 (strip4_center = 1475)
  • Позиция: qr_x центрирован по горизонтали в правой области, qr_y = strip4_center − qr_w//2
  • Стилизация: круглые точки, цвет = avg non-white
    • avg считается ДО отрисовки QR — фактически это средний цвет текста (ФИО + ссылки), поэтому QR получает тот же оттенок, что и надписи

Рамка¶

  • 1 px, серая (200,200,200), справа и снизу

Обязательный отладочный вывод¶

Каждый запуск кода должен выводить:

  1. РАЗМЕР: W_PX x H_PX
  2. top, row
  3. Все strip центры
  4. КОЛОНКА: x, w
  5. ЛЕВАЯ и ПРАВАЯ ширины
  6. Координаты всех ФИО и ссылок
  7. avg color
  8. ФОТО координаты и центр
  9. QR координаты и центр

Чеклист сохранения¶

  1. Изменил код → перезаписал файл через write
  2. Запустил → ядро перезапустил
  3. Проверил вывод → ошибок нет
  4. Проверил файл на диске → ls -la
  5. Обновилась ли картинка → размер и время
In [1]:
!pip install pillow qrcode numpy font_manrope -q
In [2]:
import os; os.chdir('/home/jovyan/work')

from PIL import Image, ImageDraw, ImageFont
import numpy as np
import qrcode
import os
import font_manrope


W_MM, H_MM = 90, 50
DPI = 600
W_PX = int(W_MM / 25.4 * DPI)
H_PX = int(H_MM / 25.4 * DPI)

print(f'=== РАЗМЕР: {W_PX}x{H_PX} ===')

top = int(H_PX * 0.13)
row = int(H_PX * 0.13)

strip_centers = []
for i in range(6):
    strip_centers.append(top + i * row + row // 2)
for i in range(6):
    print(f'strip{i}_center = {strip_centers[i]}')

base = Image.new('RGBA', (W_PX, H_PX), (255, 255, 255, 255))
draw = ImageDraw.Draw(base)

txt_fio = ['Цветков','Алексей','Игоревич']
txt_link = ['aitsvet.ru','aitsvet@ya.ru','t.me/aitsvet']

fs_fio = int(row * 0.70)
fs_link = int(row * 0.40)

fm_path = os.path.join(os.path.dirname(font_manrope.__file__), 'files')
fb = ImageFont.truetype(os.path.join(fm_path, 'Manrope-Bold.ttf'), fs_fio)
fm = ImageFont.truetype(os.path.join(fm_path, 'Manrope-Regular.ttf'), fs_link)

max_w_fio = max(draw.textbbox((0, 0), t, font=fb)[2] for t in txt_fio)
max_w_link = max(draw.textbbox((0, 0), t, font=fm)[2] for t in txt_link)

temp_col_w = max(max_w_fio, max_w_link)
temp_col_x = W_PX // 2 - temp_col_w // 2
left_part_w = temp_col_x

img_w = left_part_w - 2 * top
img_h = img_w
photo_x = (left_part_w - img_w) // 2
photo_y = strip_centers[1] - img_h // 2

col_x = photo_x + img_w + top

col_w = max(max_w_fio, max_w_link)
right_part_w = W_PX - (col_x + col_w)
links_right_x = col_x + col_w

print(f'КОЛОНКА: x={col_x}, w={col_w}')
print(f'ЛЕВАЯ: w={left_part_w}, ПРАВАЯ: w={right_part_w}')

_bbox_probe = ImageDraw.Draw(Image.new('RGBA', (1, 1)))

def find_y_for_e_center(text, target_center, font):
    for i, ch in enumerate(text):
        if ch in 'еeЕE':
            bb = _bbox_probe.textbbox((0, 0), text[:i+1], font=font)
            return target_center - (bb[1] + bb[3]) // 2
    return 0

for i, t in enumerate(txt_fio):
    strip_center = strip_centers[i]
    y = find_y_for_e_center(t, strip_center, fb)
    draw.text((col_x, y), t, fill='black', font=fb)
    bbox = draw.textbbox((col_x, y), t, font=fb)
    print(f'ФИО {i+1}: ({bbox[0]},{bbox[1]}) - ({bbox[2]},{bbox[3]})')
    for j, ch in enumerate(t):
        if ch in 'еeЕE':
            e_bb = draw.textbbox((col_x, y), t[:j+1], font=fb)
            print(f"  e_center = {(e_bb[1]+e_bb[3])//2}, target = {strip_center}")
            break

for i, t in enumerate(txt_link):
    strip_center = strip_centers[3 + i]
    y = find_y_for_e_center(t, strip_center, fm)
    w = draw.textbbox((0, 0), t, font=fm)[2]
    x = links_right_x - w
    draw.text((x, y), t, fill='#0000FF', font=fm)
    bbox = draw.textbbox((x, y), t, font=fm)
    print(f'ССЫЛКА {i+1}: ({bbox[0]},{bbox[1]}) - ({bbox[2]},{bbox[3]})')
    for j, ch in enumerate(t):
        if ch in 'еeЕE':
            e_bb = draw.textbbox((x, y), t[:j+1], font=fm)
            print(f"  e_center = {(e_bb[1]+e_bb[3])//2}, target = {strip_center}")
            break

arr = np.array(base)
non_white = ~((arr[:,:,0] > 240) & (arr[:,:,1] > 240) & (arr[:,:,2] > 240))
non_white_pixels = arr[:,:,:3][non_white]
avg = non_white_pixels.mean(axis=0).astype(int)
avg_hex = '#{:02x}{:02x}{:02x}'.format(int(avg[0]), int(avg[1]), int(avg[2]))
print(f'avg color: {avg_hex}')

photo = Image.open('aitsvet.png').convert('RGBA')
ph_w, ph_h = photo.size
crop_side = min(ph_w, ph_h)
photo = photo.crop(((ph_w-crop_side)//2, (ph_h-crop_side)//2, (ph_w+crop_side)//2, (ph_h+crop_side)//2))

photo_resized = photo.resize((img_w, img_h), Image.LANCZOS)
mask = Image.new('L', (img_w, img_h), 0)
ImageDraw.Draw(mask).ellipse((0, 0, img_w, img_h), fill=255)

photo_center = photo_y + img_h // 2

margin_left = photo_x
margin_top = top
margin_to_fio = col_x - (photo_x + img_w)

print(f'ФОТО: ({photo_x},{photo_y}) - ({photo_x+img_w},{photo_y+img_h})')
print(f'  photo_center = {photo_center}, strip1_center = {strip_centers[1]}')
print(f'  margin_left = {margin_left}, margin_top = {margin_top}, margin_to_fio = {margin_to_fio}')

qi = qrcode.QRCode(version=None, error_correction=qrcode.constants.ERROR_CORRECT_M, box_size=10, border=1)
qi.add_data('https://aitsvet.ru')
qi.make(fit=True)
qi_origin = qi.make_image(fill_color='black', back_color='white').convert('RGBA')

qr_w = int(right_part_w * 0.60)
qi_resized = qi_origin.resize((qr_w, qr_w), Image.NEAREST)
qr_arr = np.array(qi_resized)

module_size = qr_w // 27
radius = int(module_size * 0.45)

qr_x = links_right_x + (right_part_w - qr_w) // 2
qr_y = strip_centers[4] - qr_w // 2
qr_center = qr_y + qr_w // 2

print(f'QR: {qr_w}x{qr_w}, module={module_size}, radius={radius}')
print(f'QR: ({qr_x},{qr_y}) - ({qr_x+qr_w},{qr_y+qr_w})')
print(f'  qr_center = {qr_center}, strip4_center = {strip_centers[4]}')

scale = 2
qr_canvas = Image.new('RGBA', (qr_w*scale, qr_w*scale), (255, 255, 255, 255))
d = ImageDraw.Draw(qr_canvas)
module_s = module_size * scale
radius_s = radius * scale

for my in range(27):
    for mx in range(27):
        py = my * module_s + module_s // 2
        px = mx * module_s + module_s // 2
        qy = my * module_size + module_size // 2
        qx = mx * module_size + module_size // 2
        if qr_arr[qy, qx, 0] < 128:
            d.ellipse((px-radius_s, py-radius_s, px+radius_s, py+radius_s), fill=avg_hex)

qr_final = qr_canvas.resize((qr_w, qr_w), Image.LANCZOS)

base.paste(photo_resized, (photo_x, photo_y), mask)
base.paste(qr_final, (qr_x, qr_y))

draw.line([(0, H_PX-1), (W_PX-1, H_PX-1)], fill=(200, 200, 200))
draw.line([(W_PX-1, 0), (W_PX-1, H_PX-1)], fill=(200, 200, 200))

base.save('vcard.png')
print('ok')
=== РАЗМЕР: 2125x1181 ===
strip0_center = 229
strip1_center = 382
strip2_center = 535
strip3_center = 688
strip4_center = 841
strip5_center = 994
КОЛОНКА: x=807, w=511
ЛЕВАЯ: w=807, ПРАВАЯ: w=807
ФИО 1: (807,184) - (1252,275)
  e_center = 229, target = 229
ФИО 2: (807,339) - (1257,422)
  e_center = 382, target = 382
ФИО 3: (807,483) - (1318,587)
  e_center = 535, target = 535
ССЫЛКА 1: (1050,665) - (1318,711)
  e_center = 688, target = 688
ССЫЛКА 2: (929,818) - (1318,878)
  e_center = 841, target = 841
ССЫЛКА 3: (972,970) - (1318,1016)
  e_center = 994, target = 994
avg color: #171747
ФОТО: (153,132) - (654,633)
  photo_center = 382, strip1_center = 382
  margin_left = 153, margin_top = 153, margin_to_fio = 153
QR: 484x484, module=17, radius=7
QR: (1479,599) - (1963,1083)
  qr_center = 841, strip4_center = 841
ok
In [3]:
from IPython.display import Image as IPImage
IPImage(filename='vcard.png')
Out[3]:
vcard