СПЕЦИФИКАЦИЯ ВИЗИТКИ¶
Размер визитки¶
- 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), справа и снизу
Обязательный отладочный вывод¶
Каждый запуск кода должен выводить:
- РАЗМЕР: W_PX x H_PX
- top, row
- Все strip центры
- КОЛОНКА: x, w
- ЛЕВАЯ и ПРАВАЯ ширины
- Координаты всех ФИО и ссылок
- avg color
- ФОТО координаты и центр
- QR координаты и центр
Чеклист сохранения¶
- Изменил код → перезаписал файл через
write - Запустил → ядро перезапустил
- Проверил вывод → ошибок нет
- Проверил файл на диске →
ls -la - Обновилась ли картинка → размер и время
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]: