284 lines
9.9 KiB
Python
284 lines
9.9 KiB
Python
"""
|
|
OTC 용법 라벨 출력 모듈
|
|
Brother QL-810W 프린터용 가로형 와이드 라벨 생성 및 출력
|
|
|
|
기반: person-lookup-web-local/print_label.py
|
|
"""
|
|
|
|
from PIL import Image, ImageDraw, ImageFont
|
|
import logging
|
|
import re
|
|
from pathlib import Path
|
|
|
|
# 프린터 설정 (QL-810W)
|
|
PRINTER_IP = "192.168.0.168" # QR 라벨과 동일한 Brother QL-810W
|
|
PRINTER_MODEL = "QL-810W"
|
|
LABEL_TYPE = "29" # 29mm 연속 출력 용지
|
|
|
|
# 폰트 경로 (Windows/Linux 크로스 플랫폼)
|
|
FONT_PATHS = [
|
|
"C:/Windows/Fonts/malgunbd.ttf", # Windows
|
|
"/srv/person-lookup-web-local/pop_maker/fonts/malgunbd.ttf", # Linux
|
|
"/usr/share/fonts/truetype/nanum/NanumGothicBold.ttf", # Linux 대체
|
|
]
|
|
|
|
def get_font_path():
|
|
"""사용 가능한 폰트 경로 반환"""
|
|
for path in FONT_PATHS:
|
|
if Path(path).exists():
|
|
return path
|
|
return None
|
|
|
|
|
|
def create_otc_label_image(drug_name, effect="", dosage_instruction="", usage_tip=""):
|
|
"""
|
|
OTC 용법 라벨 이미지 생성 (800 x 306px)
|
|
|
|
레이아웃:
|
|
- 효능: 중앙 상단에 크게 강조 (72pt)
|
|
- 약품명: 오른쪽 중간 (36pt)
|
|
- 용법: 왼쪽 하단 체크박스 (40pt)
|
|
- 약국명: 오른쪽 하단 테두리 박스 (32pt)
|
|
|
|
Args:
|
|
drug_name (str): 약품명
|
|
effect (str): 효능
|
|
dosage_instruction (str): 복용 방법
|
|
usage_tip (str): 사용 팁
|
|
|
|
Returns:
|
|
PIL.Image: 가로형 와이드 라벨 이미지 (800 x 306px, mode='1')
|
|
"""
|
|
try:
|
|
# 1. 캔버스 생성 (가로로 긴 형태)
|
|
width = 800
|
|
height = 306 # Brother QL 29mm 용지 폭
|
|
|
|
img = Image.new('1', (width, height), 1) # 흰색 배경
|
|
draw = ImageDraw.Draw(img)
|
|
|
|
# 2. 폰트 로드
|
|
font_path = get_font_path()
|
|
try:
|
|
font_effect = ImageFont.truetype(font_path, 72) # 효능 (매우 크게!)
|
|
font_drugname = ImageFont.truetype(font_path, 36) # 약품명 (중간)
|
|
font_dosage = ImageFont.truetype(font_path, 40) # 용법 (크게)
|
|
font_pharmacy = ImageFont.truetype(font_path, 32) # 약국명 (크게)
|
|
font_small = ImageFont.truetype(font_path, 26) # 사용팁
|
|
except (IOError, TypeError):
|
|
font_effect = ImageFont.load_default()
|
|
font_drugname = ImageFont.load_default()
|
|
font_dosage = ImageFont.load_default()
|
|
font_pharmacy = ImageFont.load_default()
|
|
font_small = ImageFont.load_default()
|
|
logging.warning("폰트 로드 실패. 기본 폰트 사용.")
|
|
|
|
# 3. 레이아웃
|
|
x_margin = 25
|
|
|
|
# 효능 - 중앙 상단에 크게 (매우 강조!)
|
|
if effect:
|
|
effect_bbox = draw.textbbox((0, 0), effect, font=font_effect)
|
|
effect_width = effect_bbox[2] - effect_bbox[0]
|
|
effect_x = (width - effect_width) // 2
|
|
# 굵게 표시 (offset)
|
|
for offset in [(0, 0), (1, 0), (2, 0), (0, 1), (1, 1), (2, 1)]:
|
|
draw.text((effect_x + offset[0], 20 + offset[1]), effect, font=font_effect, fill=0)
|
|
|
|
# 약품명 - 오른쪽 중간 여백에 배치
|
|
drugname_bbox = draw.textbbox((0, 0), drug_name, font=font_drugname)
|
|
drugname_width = drugname_bbox[2] - drugname_bbox[0]
|
|
drugname_x = width - drugname_width - 30 # 오른쪽에서 30px 여백
|
|
drugname_y = 195
|
|
draw.text((drugname_x, drugname_y), drug_name, font=font_drugname, fill=0)
|
|
|
|
# 용법 - 왼쪽 하단에 크게 표시
|
|
y = 120 # 효능 아래부터 시작
|
|
|
|
# 사용팁이 없으면 복용방법을 더 크게
|
|
if not usage_tip:
|
|
try:
|
|
font_dosage_adjusted = ImageFont.truetype(font_path, 50)
|
|
except:
|
|
font_dosage_adjusted = font_dosage
|
|
else:
|
|
font_dosage_adjusted = font_dosage
|
|
|
|
if dosage_instruction:
|
|
# 대괄호로 묶인 부분을 별도 줄로 분리
|
|
dosage_text = re.sub(r'\s*(\[.*?\])\s*', r'\n\1\n', dosage_instruction)
|
|
|
|
# 여러 줄 처리
|
|
max_chars_per_line = 32
|
|
dosage_lines = []
|
|
|
|
text_parts = dosage_text.split('\n')
|
|
for part in text_parts:
|
|
part = part.strip()
|
|
if not part:
|
|
continue
|
|
|
|
if part.startswith('[') and part.endswith(']'):
|
|
dosage_lines.append(part)
|
|
elif len(part) > max_chars_per_line:
|
|
words = part.split()
|
|
current_line = ""
|
|
for word in words:
|
|
if len(current_line + word) <= max_chars_per_line:
|
|
current_line += word + " "
|
|
else:
|
|
if current_line:
|
|
dosage_lines.append(current_line.strip())
|
|
current_line = word + " "
|
|
if current_line:
|
|
dosage_lines.append(current_line.strip())
|
|
else:
|
|
dosage_lines.append(part)
|
|
|
|
# 첫 줄에 체크박스 추가
|
|
if dosage_lines:
|
|
first_line = f"□ {dosage_lines[0]}"
|
|
draw.text((x_margin, y), first_line, font=font_dosage_adjusted, fill=0)
|
|
|
|
line_spacing = 60 if not usage_tip else 50
|
|
y += line_spacing
|
|
|
|
for line in dosage_lines[1:]:
|
|
indent = 0 if (line.startswith('[') and line.endswith(']')) else 30
|
|
draw.text((x_margin + indent, y), line, font=font_dosage_adjusted, fill=0)
|
|
y += line_spacing + 2
|
|
|
|
# 사용팁 (체크박스 + 텍스트)
|
|
if usage_tip and y < height - 60:
|
|
tip_text = f"□ {usage_tip}"
|
|
if len(tip_text) > 55:
|
|
tip_text = tip_text[:52] + "..."
|
|
draw.text((x_margin, y), tip_text, font=font_small, fill=0)
|
|
|
|
# 약국명 - 오른쪽 하단에 크게 (테두리 박스)
|
|
sign_text = "청춘약국"
|
|
sign_bbox = draw.textbbox((0, 0), sign_text, font=font_pharmacy)
|
|
sign_width = sign_bbox[2] - sign_bbox[0]
|
|
sign_height = sign_bbox[3] - sign_bbox[1]
|
|
|
|
sign_padding_lr = 10
|
|
sign_padding_top = 5
|
|
sign_padding_bottom = 10
|
|
|
|
sign_x = width - sign_width - x_margin - 10 - sign_padding_lr
|
|
sign_y = height - 55
|
|
|
|
# 테두리 박스 그리기
|
|
box_x1 = sign_x - sign_padding_lr
|
|
box_y1 = sign_y - sign_padding_top
|
|
box_x2 = sign_x + sign_width + sign_padding_lr
|
|
box_y2 = sign_y + sign_height + sign_padding_bottom
|
|
draw.rectangle([box_x1, box_y1, box_x2, box_y2], outline=0, width=2)
|
|
|
|
# 약국명 텍스트 (굵게)
|
|
for offset in [(0, 0), (1, 0), (0, 1), (1, 1)]:
|
|
draw.text((sign_x + offset[0], sign_y + offset[1]), sign_text, font=font_pharmacy, fill=0)
|
|
|
|
# 5. 테두리 (가위선 스타일)
|
|
for i in range(3):
|
|
draw.rectangle([5 + i, 5 + i, width - 5 - i, height - 5 - i], outline=0)
|
|
|
|
logging.info(f"OTC 라벨 이미지 생성 성공: {drug_name}")
|
|
return img
|
|
|
|
except Exception as e:
|
|
logging.error(f"OTC 라벨 이미지 생성 실패: {e}")
|
|
raise
|
|
|
|
|
|
def print_otc_label(drug_name, effect="", dosage_instruction="", usage_tip=""):
|
|
"""
|
|
OTC 용법 라벨을 Brother QL-810W 프린터로 출력
|
|
|
|
Args:
|
|
drug_name (str): 약품명
|
|
effect (str): 효능
|
|
dosage_instruction (str): 복용 방법
|
|
usage_tip (str): 사용 팁
|
|
|
|
Returns:
|
|
bool: 성공 여부
|
|
"""
|
|
try:
|
|
from brother_ql.raster import BrotherQLRaster
|
|
from brother_ql.conversion import convert
|
|
from brother_ql.backends.helpers import send
|
|
|
|
# 1. 라벨 이미지 생성
|
|
label_img = create_otc_label_image(drug_name, effect, dosage_instruction, usage_tip)
|
|
|
|
# 2. 이미지 90도 회전 (Brother QL이 세로 방향 기준이므로)
|
|
label_img_rotated = label_img.rotate(90, expand=True)
|
|
|
|
logging.info(f"이미지 회전 완료: {label_img_rotated.size}")
|
|
|
|
# 3. Brother QL 프린터로 전송
|
|
qlr = BrotherQLRaster(PRINTER_MODEL)
|
|
instructions = convert(
|
|
qlr=qlr,
|
|
images=[label_img_rotated],
|
|
label='29',
|
|
rotate='0',
|
|
threshold=70.0,
|
|
dither=False,
|
|
compress=False,
|
|
red=False,
|
|
dpi_600=False,
|
|
hq=True,
|
|
cut=True
|
|
)
|
|
send(instructions, printer_identifier=f"tcp://{PRINTER_IP}:9100")
|
|
|
|
logging.info(f"[SUCCESS] OTC 용법 라벨 인쇄 성공: {drug_name}")
|
|
return True
|
|
|
|
except ImportError:
|
|
logging.error("brother_ql 라이브러리가 설치되지 않았습니다.")
|
|
return False
|
|
except Exception as e:
|
|
logging.error(f"[ERROR] OTC 용법 라벨 인쇄 실패: {e}")
|
|
return False
|
|
|
|
|
|
def generate_preview_image(drug_name, effect="", dosage_instruction="", usage_tip=""):
|
|
"""
|
|
미리보기용 PNG 이미지 생성 (Base64 인코딩)
|
|
|
|
Args:
|
|
drug_name (str): 약품명
|
|
effect (str): 효능
|
|
dosage_instruction (str): 복용 방법
|
|
usage_tip (str): 사용 팁
|
|
|
|
Returns:
|
|
str: Base64 인코딩된 PNG 이미지 (data:image/png;base64,... 형태)
|
|
"""
|
|
import base64
|
|
from io import BytesIO
|
|
|
|
try:
|
|
# 라벨 이미지 생성
|
|
label_img = create_otc_label_image(drug_name, effect, dosage_instruction, usage_tip)
|
|
|
|
# RGB로 변환 (1-bit → RGB)
|
|
label_img_rgb = label_img.convert('RGB')
|
|
|
|
# PNG로 인코딩
|
|
buffer = BytesIO()
|
|
label_img_rgb.save(buffer, format='PNG')
|
|
buffer.seek(0)
|
|
|
|
# Base64 인코딩
|
|
img_base64 = base64.b64encode(buffer.getvalue()).decode('utf-8')
|
|
|
|
return f"data:image/png;base64,{img_base64}"
|
|
|
|
except Exception as e:
|
|
logging.error(f"미리보기 이미지 생성 실패: {e}")
|
|
return None
|