robot_face_rec/preview_qrcode.py
2025-12-05 12:01:20 +08:00

334 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import math
import os
import cv2
import yaml
import numpy as np
from PIL import Image, ImageDraw, ImageFont, ImageFilter
# Canvas Configuration
CANVAS_SIZE = (1920, 1080)
BG_COLOR_START = (240, 242, 245)
BG_COLOR_END = (200, 210, 230)
# Colors
PRIMARY_COLOR = (23, 43, 77) # Dark Blue for headings
SECONDARY_COLOR = (94, 108, 132) # Grey for secondary text
ACCENT_COLOR = (0, 82, 204) # Bright Blue for highlights/numbers
CARD_BG_COLOR = (255, 255, 255)
CARD_SHADOW_COLOR = (9, 30, 66, 40) # Shadow color
INSTRUCTION_TITLE = "访客预约流程"
INSTRUCTION_SUBTITLE = "Visitor Registration Process"
INSTRUCTION_STEPS = [
"第一步:扫码关注“康达新材”公众号。",
"第二步:点击【关于我们】→【我是访客】进入“访客注册”界面,填写并上传相应信息并点击“提交”。",
"第三步:将第二步信息提交完毕后在“访客预约”界面选择右下角的“+”号按钮,填写预约信息。",
"第四步:请仔细阅读安全告知书,点击我知道了。",
"第五步:填写被访人信息及来访事由等内容并提交。",
"第六步:显示提交成功,并且手机、微信会收到预约相关短信通知。",
]
def load_config(path="config.yaml"):
try:
with open(path, "r", encoding="utf-8") as f:
return yaml.safe_load(f)
except FileNotFoundError:
return {}
def resolve_font(size: int, preferred: str | None = None) -> ImageFont.FreeTypeFont:
candidates = [preferred] if preferred else []
candidates += [
"C:/Windows/Fonts/msyhbd.ttc", # Microsoft YaHei Bold
"C:/Windows/Fonts/msyh.ttc", # Microsoft YaHei
"C:/Windows/Fonts/simhei.ttf",
"/usr/share/fonts/truetype/noto/NotoSansCJK-Bold.ttc",
"/usr/share/fonts/truetype/noto/NotoSansCJK-Regular.ttc",
"/usr/share/fonts/truetype/noto/NotoMono-Regular.ttf",
]
for path in candidates:
if path and os.path.exists(path):
try:
return ImageFont.truetype(path, size)
except OSError:
continue
return ImageFont.load_default()
def wrap_text(text: str, font: ImageFont.FreeTypeFont, max_width: int, draw: ImageDraw.ImageDraw):
lines = []
current = ""
for ch in text:
tentative = current + ch
# Pillow 10+ uses textlength, older uses textsize
try:
width = draw.textlength(tentative, font=font)
except AttributeError:
width, _ = draw.textsize(tentative, font=font)
if width <= max_width:
current = tentative
else:
if current:
lines.append(current)
current = ch
if current:
lines.append(current)
return lines
def create_gradient_background(size: tuple[int, int], start_color: tuple[int, int, int], end_color: tuple[int, int, int]) -> Image.Image:
width, height = size
# Vertical gradient
base = Image.new('RGB', size, start_color)
top = Image.new('RGB', size, end_color)
mask = Image.new('L', size)
mask_data = np.tile(np.linspace(0, 255, height, dtype=np.uint8), (width, 1)).T
mask = Image.fromarray(mask_data, 'L')
return Image.composite(top, base, mask)
def draw_rounded_rect(draw, box, radius, fill, outline=None, width=0):
draw.rounded_rectangle(box, radius=radius, fill=fill, outline=outline, width=width)
def draw_shadow(image, box, radius, offset=(0, 4), blur=10, shadow_color=(0,0,0,50)):
# Create a separate shadow layer
shadow_layer = Image.new('RGBA', image.size, (0,0,0,0))
shadow_draw = ImageDraw.Draw(shadow_layer)
sx0, sy0, sx1, sy1 = box
dx, dy = offset
shadow_box = (sx0 + dx, sy0 + dy, sx1 + dx, sy1 + dy)
shadow_draw.rounded_rectangle(shadow_box, radius=radius, fill=shadow_color)
# Blur the shadow
shadow_layer = shadow_layer.filter(ImageFilter.GaussianBlur(blur))
# Composite
image.alpha_composite(shadow_layer)
def build_canvas(qr_image: np.ndarray, title_font, subtitle_font, body_font, step_title_font, badge_font) -> np.ndarray:
canvas_width, canvas_height = CANVAS_SIZE
# 1. Background
bg = create_gradient_background(CANVAS_SIZE, (245, 247, 250), (223, 225, 230)).convert("RGBA")
draw = ImageDraw.Draw(bg, "RGBA")
# Layout Constants
MARGIN = 50
GUTTER = 40
# Left Panel (QR Code) - 35% width approx
left_width = int((canvas_width - 2 * MARGIN - GUTTER) * 0.35)
right_width = canvas_width - 2 * MARGIN - GUTTER - left_width
left_box = (MARGIN, MARGIN, MARGIN + left_width, canvas_height - MARGIN)
right_box = (MARGIN + left_width + GUTTER, MARGIN, canvas_width - MARGIN, canvas_height - MARGIN)
# --- Draw Left Panel ---
# Shadow
draw_shadow(bg, left_box, radius=30, offset=(0, 10), blur=20, shadow_color=(0,0,0,30))
# Card
draw_rounded_rect(draw, left_box, radius=30, fill=(255, 255, 255, 255))
# Left Content
cx = (left_box[0] + left_box[2]) // 2
cy_top = left_box[1] + 120
# Title
text = "访客登记"
try:
w = draw.textlength(text, font=title_font)
except:
w, _ = draw.textsize(text, font=title_font)
draw.text((cx - w/2, cy_top), text, font=title_font, fill=PRIMARY_COLOR)
# Subtitle
text = "Visitor Registration"
try:
w = draw.textlength(text, font=subtitle_font)
except:
w, _ = draw.textsize(text, font=subtitle_font)
draw.text((cx - w/2, cy_top + 70), text, font=subtitle_font, fill=SECONDARY_COLOR)
# QR Code
qr_size = min(left_width - 100, 500)
qr_y = cy_top + 180
# Resize QR
qr_pil = Image.fromarray(cv2.cvtColor(qr_image, cv2.COLOR_BGR2RGB))
qr_pil = qr_pil.resize((qr_size, qr_size), Image.LANCZOS)
bg.paste(qr_pil, (cx - qr_size//2, qr_y))
# Scan Hint
hint_y = qr_y + qr_size + 50
hint_text = "请使用微信扫码登记"
try:
w = draw.textlength(hint_text, font=step_title_font)
except:
w, _ = draw.textsize(hint_text, font=step_title_font)
# Draw a pill background for hint
pill_padding = 20
pill_box = (cx - w/2 - pill_padding, hint_y - pill_padding, cx + w/2 + pill_padding, hint_y + 40 + pill_padding)
draw.rounded_rectangle(pill_box, radius=30, fill=(240, 242, 245), outline=None)
draw.text((cx - w/2, hint_y), hint_text, font=step_title_font, fill=ACCENT_COLOR)
# --- Draw Right Panel ---
# Right Title Area - Compacted
rt_y = MARGIN + 20
draw.text((right_box[0], rt_y), INSTRUCTION_TITLE, font=title_font, fill=PRIMARY_COLOR)
draw.text((right_box[0], rt_y + 60), INSTRUCTION_SUBTITLE, font=subtitle_font, fill=SECONDARY_COLOR)
# Separator Line
sep_y = rt_y + 110
draw.line((right_box[0], sep_y, right_box[2], sep_y), fill=(200, 200, 200), width=2)
# Grid Configuration
grid_y_start = sep_y + 40
grid_width = right_width
cols = 2
col_gap = 30
row_gap = 30
col_width = (grid_width - (cols - 1) * col_gap) // cols
# Pre-calculate text layout to find uniform height
max_lines = 0
processed_steps = []
padding = 30
badge_size = 50
text_left_margin = badge_size + 20
# Calculate available width for text inside a card
text_max_width = col_width - padding * 2 - text_left_margin
for i, step in enumerate(INSTRUCTION_STEPS):
# Remove "第一步:" etc prefix if present to make it cleaner, we have badges
clean_step = step.split("", 1)[-1] if "" in step else step
lines = wrap_text(clean_step, body_font, text_max_width, draw)
processed_steps.append(lines)
max_lines = max(max_lines, len(lines))
# Calculate Card Height
# Padding top + max_lines * line_height + Padding bottom
line_height = body_font.size + 10
card_content_height = max(badge_size, max_lines * line_height)
uniform_card_height = int(padding * 2 + card_content_height)
# Check if we overflow canvas height
total_grid_height = 3 * uniform_card_height + 2 * row_gap
if grid_y_start + total_grid_height > canvas_height - MARGIN:
print(f"Warning: Content might overflow vertically. Required: {grid_y_start + total_grid_height}, Available: {canvas_height - MARGIN}")
# Dynamic adjustment if needed (e.g. reduce gaps) but for now we rely on the font resizing done in main()
# Draw Grid
for idx, lines in enumerate(processed_steps):
row = idx // cols
col = idx % cols
x = right_box[0] + col * (col_width + col_gap)
y = grid_y_start + row * (uniform_card_height + row_gap)
card_box = (x, y, x + col_width, y + uniform_card_height)
# Card Shadow
draw_shadow(bg, card_box, radius=20, offset=(0, 4), blur=12, shadow_color=(0,0,0,15))
# Card Body
draw_rounded_rect(draw, card_box, radius=20, fill=(255, 255, 255))
# Badge (Step Number)
bx = x + padding
by = y + padding
draw.ellipse((bx, by, bx + badge_size, by + badge_size), fill=ACCENT_COLOR)
num_text = str(idx + 1)
try:
nw = draw.textlength(num_text, font=badge_font)
except:
nw, _ = draw.textsize(num_text, font=badge_font)
# Center number
draw.text((bx + (badge_size - nw)/2, by + (badge_size - badge_font.size)/2 - 2),
num_text, font=badge_font, fill=(255, 255, 255))
# Text
tx = x + padding + text_left_margin
ty = y + padding
for line in lines:
draw.text((tx, ty), line, font=body_font, fill=PRIMARY_COLOR)
ty += line_height
return cv2.cvtColor(np.array(bg.convert("RGB")), cv2.COLOR_RGB2BGR)
def create_mock_qrcode(size: int = 640) -> np.ndarray:
img = np.full((size, size, 3), 255, dtype=np.uint8) # White background
# Draw some patterns
block = size // 15
for y in range(0, size, block):
for x in range(0, size, block):
if (x // block + y // block) % 2 == 0:
color = (0, 0, 0)
# Corner markers
if (x < 3*block and y < 3*block) or (x > size-4*block and y < 3*block) or (x < 3*block and y > size-4*block):
color = (0, 0, 0)
elif np.random.rand() > 0.3:
color = (0, 0, 0)
else:
color = (255, 255, 255)
cv2.rectangle(img, (x, y), (x + block, y + block), color, -1)
# Borders for markers
marker_len = 3 * block
thickness = block
# Top Left
cv2.rectangle(img, (0,0), (marker_len, marker_len), (0,0,0), thickness)
# Top Right
cv2.rectangle(img, (size-marker_len,0), (size, marker_len), (0,0,0), thickness)
# Bottom Left
cv2.rectangle(img, (0,size-marker_len), (marker_len, size), (0,0,0), thickness)
return img
def main():
config = load_config()
preferred_font = config.get("display", {}).get("font_path") if config else None
# Define Fonts - Adjusted sizes for better fit
# Title for left/right headers
title_font = resolve_font(56, preferred_font)
# Subtitles
subtitle_font = resolve_font(30, preferred_font)
# Step text - Reduced to ensure fit
body_font = resolve_font(24, preferred_font)
# Step titles or emphasis
step_title_font = resolve_font(28, preferred_font)
# Badge numbers
badge_font = resolve_font(30, preferred_font)
# Mock data
mock_qr = create_mock_qrcode(500)
# Build
canvas = build_canvas(mock_qr, title_font, subtitle_font, body_font, step_title_font, badge_font)
# Display
window_name = "Visitor Registration Preview"
cv2.namedWindow(window_name, cv2.WINDOW_NORMAL)
cv2.resizeWindow(window_name, 1280, 720)
cv2.imshow(window_name, canvas)
print("Displaying preview. Press any key to exit.")
cv2.waitKey(0)
cv2.destroyAllWindows()
# Save for verification
cv2.imwrite("preview_qrcode_sota.jpg", canvas)
print("Saved preview to preview_qrcode_sota.jpg")
if __name__ == "__main__":
main()