""" 바코드 스캔 시 간단한 라벨 자동 출력 모듈 Brother QL-810W 프린터용 """ from PIL import Image, ImageDraw, ImageFont from brother_ql.raster import BrotherQLRaster from brother_ql.conversion import convert from brother_ql.backends.helpers import send import os import logging import re from datetime import datetime import glob # 프린터 설정 PRINTER_IP = "192.168.0.168" PRINTER_PORT = 9100 PRINTER_MODEL = "QL-810W" LABEL_TYPE = "29" # 로깅 설정 logging.basicConfig(level=logging.INFO, format='[BARCODE_PRINT] %(levelname)s: %(message)s') def normalize_medication_name(med_name): """ 약품명 정제 (print_label.py의 함수 복사) - 괄호 제거 - 밀리그램 → mg 변환 - 대괄호 제거 Args: med_name: 약품명 Returns: str: 정제된 약품명 """ if not med_name: return med_name # 대괄호 및 내용 제거 med_name = re.sub(r'\[.*?\]', '', med_name) med_name = re.sub(r'\[.*$', '', med_name) # 소괄호 및 내용 제거 med_name = re.sub(r'\(.*?\)', '', med_name) med_name = re.sub(r'\(.*$', '', med_name) # 언더스코어 뒤 내용 제거 med_name = re.sub(r'_.*$', '', med_name) # 밀리그램 변환 med_name = re.sub(r'밀리그램|밀리그람|미리그램|미리그람', 'mg', med_name) # 마이크로그램 변환 med_name = re.sub(r'마이크로그램|마이크로그람', 'μg', med_name) # 그램 변환 (단, mg/μg로 이미 변환된 것은 제외) med_name = re.sub(r'(? 20: # 최소 20pt까지 축소 font_drugname_dynamic = ImageFont.truetype(font_path, drugname_font_size) drugname_bbox = draw.textbbox((0, 0), drugname_text, font=font_drugname_dynamic) drugname_width = drugname_bbox[2] - drugname_bbox[0] if drugname_width <= max_drugname_width: break drugname_font_size -= 2 # 2pt씩 축소 # 효능과 같은 Y 위치 (중앙 정렬) drugname_height = drugname_bbox[3] - drugname_bbox[1] drugname_y = 20 + (72 - drugname_height) // 2 draw.text((effect_end_x, drugname_y), drugname_text, font=font_drugname_dynamic, fill=0) # 용법 - 왼쪽 하단에 크게 표시 (동적 폰트 크기 조정) y = 120 # 효능 아래부터 시작 if dosage_instruction: # 대괄호로 묶인 부분을 별도 줄로 분리 dosage_text = re.sub(r'\s*(\[.*?\])\s*', r'\n\1\n', dosage_instruction) # 여러 줄 처리 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) # 일반 텍스트는 그대로 추가 (폰트 크기로 조정) else: dosage_lines.append(part) # 동적 폰트 크기 조정 (박스 안에 들어오도록) max_dosage_width = width - x_margin - 50 # 좌우 여백 dosage_font_size = 50 # 초기 폰트 크기 # 가장 긴 줄을 기준으로 폰트 크기 조정 longest_line = max(dosage_lines, key=len) if dosage_lines else "" test_line = f"□ {longest_line}" while dosage_font_size > 30: # 최소 30pt까지 축소 font_dosage_dynamic = ImageFont.truetype(font_path, dosage_font_size) test_bbox = draw.textbbox((0, 0), test_line, font=font_dosage_dynamic) test_width = test_bbox[2] - test_bbox[0] if test_width <= max_dosage_width: break dosage_font_size -= 2 # 2pt씩 축소 # 첫 줄에 체크박스 추가 if dosage_lines: first_line = f"□ {dosage_lines[0]}" draw.text((x_margin, y), first_line, font=font_dosage_dynamic, fill=0) # 줄 간격 조정 (폰트 크기에 비례) line_spacing = int(dosage_font_size * 1.2) 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_dynamic, 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) # 테두리 (가위선 스타일) for i in range(3): draw.rectangle([5 + i, 5 + i, width - 5 - i, height - 5 - i], outline=0) logging.info(f"가로형 와이드 라벨 이미지 생성 성공: {goods_name}") return img except Exception as e: logging.error(f"가로형 와이드 라벨 이미지 생성 실패: {e}") raise def cleanup_old_preview_files(max_files=10): """ 임시 미리보기 파일 정리 (최대 개수 초과 시 오래된 파일 삭제) Args: max_files: 유지할 최대 파일 개수 """ try: temp_dir = os.path.join(os.path.dirname(__file__), "temp") if not os.path.exists(temp_dir): return # label_preview_*.png 파일 목록 가져오기 preview_files = glob.glob(os.path.join(temp_dir, "label_preview_*.png")) # 파일 개수가 max_files를 초과하면 오래된 파일 삭제 if len(preview_files) > max_files: # 생성 시간 기준으로 정렬 (오래된 순) preview_files.sort(key=os.path.getmtime) # 초과된 파일 삭제 files_to_delete = preview_files[:len(preview_files) - max_files] for file_path in files_to_delete: try: os.remove(file_path) logging.info(f"오래된 미리보기 파일 삭제: {file_path}") except Exception as e: logging.warning(f"파일 삭제 실패: {file_path} - {e}") except Exception as e: logging.warning(f"미리보기 파일 정리 실패: {e}") def print_barcode_label(goods_name, sale_price, preview_mode=False): """ 바코드 스캔 시 가로형 와이드 라벨 출력 또는 미리보기 Args: goods_name: 약품명 sale_price: 판매가 (호환성 유지용, 내부 미사용) preview_mode: True = 이미지 파일 경로 반환, False = 프린터 전송 Returns: preview_mode=True: (성공 여부, 이미지 파일 경로) preview_mode=False: 성공 여부 (bool) """ try: logging.info(f"가로형 와이드 라벨 {'미리보기' if preview_mode else '출력'} 시작: {goods_name}") # 1. 가로형 라벨 이미지 생성 label_image = create_wide_label(goods_name, sale_price) # 2. 미리보기 모드: PNG 파일로 저장 if preview_mode: # temp 디렉터리 생성 temp_dir = os.path.join(os.path.dirname(__file__), "temp") os.makedirs(temp_dir, exist_ok=True) # 파일명 생성 (타임스탬프 포함) timestamp = datetime.now().strftime("%Y%m%d_%H%M%S_%f") filename = f"label_preview_{timestamp}.png" file_path = os.path.join(temp_dir, filename) # PNG로 저장 (회전하지 않은 가로형 이미지) label_image.save(file_path, "PNG") logging.info(f"미리보기 이미지 저장 완료: {file_path}") # 오래된 파일 정리 cleanup_old_preview_files(max_files=10) return True, file_path # 3. 실제 인쇄 모드: 프린터로 전송 else: # 이미지 90도 회전 (시계 반대방향) # Brother QL은 세로 방향 기준이므로 가로형 이미지를 회전 label_image_rotated = label_image.rotate(90, expand=True) logging.info(f"이미지 회전 완료: {label_image_rotated.size}") # Brother QL Raster 객체 생성 qlr = BrotherQLRaster(PRINTER_MODEL) # PIL 이미지를 Brother QL 형식으로 변환 instructions = convert( qlr=qlr, images=[label_image_rotated], label=LABEL_TYPE, rotate="0", # 이미 회전했으므로 0 threshold=70.0, # 흑백 변환 임계값 dither=False, # 디더링 비활성화 (선명한 텍스트) compress=False, # 압축 비활성화 red=False, # 흑백 전용 dpi_600=False, hq=True, # 고품질 모드 cut=True # 자동 절단 ) # 프린터로 전송 printer_identifier = f"tcp://{PRINTER_IP}:{PRINTER_PORT}" send(instructions, printer_identifier=printer_identifier) logging.info(f"가로형 와이드 라벨 출력 성공: {goods_name}") return True except Exception as e: logging.error(f"가로형 와이드 라벨 {'미리보기' if preview_mode else '출력'} 실패: {e}") if preview_mode: return False, None else: return False if __name__ == "__main__": # 테스트 코드 test_result = print_barcode_label("타이레놀정500mg", 3000.0) print(f"테스트 결과: {'성공' if test_result else '실패'}")