# print_label.py from PIL import Image, ImageDraw, ImageFont import io import os import logging from brother_ql.raster import BrotherQLRaster from brother_ql.conversion import convert from brother_ql.backends.helpers import send import datetime # 날짜 처리를 위해 추가 import pytz import re import qrcode import json # 프린터 기본 설정 (프린터 정보가 전달되지 않을 때 사용) DEFAULT_PRINTER_IP = "192.168.0.121" # QL-710W 프린터의 IP 주소 DEFAULT_PRINTER_MODEL = "QL-710W" DEFAULT_LABEL_TYPE = "29" # 29mm 연속 출력 용지 # 로깅 설정 logging.basicConfig(level=logging.DEBUG, format='%(levelname)s:%(message)s') # KOR 시간대 설정 KOR_TZ = pytz.timezone('Asia/Seoul') def format_total_amount(dosage, frequency, duration): """ 1회 복용량, 복용 횟수, 총 복용 일수를 기반으로 총량을 계산하고, 1/4 단위로 반올림하거나 그대로 반환합니다. Parameters: dosage (float): 1회 복용량 (소수 넷째 자리까지 포함 가능) frequency (int): 1일 복용 횟수 duration (int): 복용 일수 Returns: str: 포맷팅된 총량 문자열 """ if frequency > 0 and duration > 0: # 1일 복용량 = 1회 복용량 * 1일 복용 횟수 daily_dosage = dosage * frequency # 총량 = 1일 복용량 * 총 복용 일수 total_amount = daily_dosage * duration # 1회 복용량이 소수 넷째 자리까지 있는 경우 1/4 단위로 반올림 if round(dosage, 4) != round(dosage, 3): # 소수 넷째 자리 여부 확인 total_amount = round(total_amount * 4) / 4 # 정수인 경우 소수점 없이 표시, 소수가 있는 경우 둘째 자리까지 표시 return str(int(total_amount)) if total_amount.is_integer() else f"{total_amount:.2f}".rstrip('0').rstrip('.') return "0" # 복용 횟수나 복용 일수가 0인 경우 def format_dosage(dosage): """ 1회 복용량을 포맷팅합니다. Parameters: dosage (float): 1회 복용량 Returns: str: 포맷팅된 복용량 문자열 """ if dosage.is_integer(): return str(int(dosage)) else: # 최대 4자리 소수까지 표시, 불필요한 0 제거 return f"{dosage:.4f}".rstrip('0').rstrip('.') def format_converted_total(dosage, frequency, duration, conversion_factor): """ 총량을 계산하고 환산계수를 곱한 변환된 총량을 포맷팅합니다. Parameters: dosage (float): 1회 복용량 (소수 넷째 자리까지 포함 가능) frequency (int): 1일 복용 횟수 duration (int): 복용 일수 conversion_factor (float): 환산계수 Returns: str: 변환된 총량을 포함한 포맷팅된 문자열 """ if frequency > 0 and duration > 0 and conversion_factor is not None: total_amount = dosage * frequency * duration if round(dosage, 4) != round(dosage, 3): total_amount = round(total_amount * 4) / 4 converted_total = total_amount * conversion_factor if converted_total.is_integer(): return str(int(converted_total)) else: return f"{converted_total:.2f}".rstrip('0').rstrip('.') return None def draw_scissor_border(draw, width, height, edge_size=5, steps=230): """ 라벨 이미지의 테두리에 톱니 모양의 절취선을 그립니다. Parameters: draw (ImageDraw.Draw): 이미지에 그리기 위한 Draw 객체 width (int): 라벨 너비 height (int): 라벨 높이 edge_size (int): 톱니 크기 steps (int): 톱니 반복 횟수 """ top_points = [] step_x = width / (steps * 2) for i in range(steps * 2 + 1): x = i * step_x y = 0 if i % 2 == 0 else edge_size top_points.append((int(x), int(y))) draw.line(top_points, fill="black", width=2) bottom_points = [] for i in range(steps * 2 + 1): x = i * step_x y = height if i % 2 == 0 else height - edge_size bottom_points.append((int(x), int(y))) draw.line(bottom_points, fill="black", width=2) left_points = [] step_y = height / (steps * 2) for i in range(steps * 2 + 1): y = i * step_y x = 0 if i % 2 == 0 else edge_size left_points.append((int(x), int(y))) draw.line(left_points, fill="black", width=2) right_points = [] for i in range(steps * 2 + 1): y = i * step_y x = width if i % 2 == 0 else width - edge_size right_points.append((int(x), int(y))) draw.line(right_points, fill="black", width=2) def split_med_name(med_name): """ 약품 이름을 표시용 이름과 시그니처 정보로 분리합니다. Parameters: med_name (str): 약품 이름 Returns: tuple: (표시용 약품 이름, 시그니처 정보, 분리 여부) """ units = ['mg', 'g', 'ml', '%'] pattern = r'(\d+(?:\.\d+)?(?:/\d+(?:\.\d+)?)*)\s*(' + '|'.join(units) + r')(?:/(' + '|'.join(units) + r'))?$' korean_only = re.fullmatch(r'[가-힣]+', med_name) is not None korean_and_num_eng = re.fullmatch(r'[가-힣a-zA-Z0-9/\.]+', med_name) is not None and not korean_only med_name_display = med_name signature_info = "청 춘 약 국" split_occurred = False if korean_only: if len(med_name) >= 10: match = re.search(pattern, med_name) if match and match.start() >= 10: med_name_display = med_name[:match.start()].strip() signature_info = match.group(1) + match.group(2) + (f"/{match.group(3)}" if match.group(3) else "") split_occurred = True else: med_name_display = med_name[:10] # else 그대로 사용 elif korean_and_num_eng: if len(med_name) >= 13: match = re.search(pattern, med_name) if match: med_name_display = med_name[:match.start()].strip() signature_info = match.group(1) + match.group(2) + (f"/{match.group(3)}" if match.group(3) else "") split_occurred = True else: med_name_display = med_name[:12] return med_name_display, signature_info, split_occurred def should_left_align(med_name_display): """ 약품 이름의 길이와 구성을 기반으로 좌측 정렬 여부를 결정합니다. Parameters: med_name_display (str): 분리된 약품 이름 표시 부분 Returns: bool: 좌측 정렬 여부 """ korean_only = re.fullmatch(r'[가-힣]+', med_name_display) is not None korean_and_num_eng = re.fullmatch(r'[가-힣a-zA-Z0-9/\.]+', med_name_display) is not None and not korean_only if korean_only and len(med_name_display) >= 10: # 10글자부터 좌측정렬 (한글단독) return True if korean_and_num_eng and len(med_name_display) >= 13: # 13글자부터 좌측정렬 (한글+숫자+영문) return True return False def normalize_medication_name(med_name): """ 약품 이름을 정제하여 라벨에 표시할 형태로 변환 - 괄호 안 내용 제거: "디오탄정80밀리그램(발사르탄)" → "디오탄정80mg" - 밀리그램 계열: "밀리그램", "밀리그람", "미리그램" → "mg" - 마이크로그램 계열: "마이크로그램", "마이크로그람" → "μg" - 그램 계열: "그램", "그람" → "g" - 대괄호 제거: "[애엽이소프]" → "" """ if not med_name: return med_name # 1. 대괄호 및 내용 제거 (예: "오티렌F정[애엽이소프]" → "오티렌F정") med_name = re.sub(r'\[.*?\]', '', med_name) # 완전한 대괄호 쌍 제거 med_name = re.sub(r'\[.*$', '', med_name) # 여는 괄호부터 끝까지 제거 # 2. 소괄호 및 내용 제거 (예: "디오탄정80밀리그램(발사르탄)" → "디오탄정80밀리그램") med_name = re.sub(r'\(.*?\)', '', med_name) # 완전한 소괄호 쌍 제거 med_name = re.sub(r'\(.*$', '', med_name) # 여는 괄호부터 끝까지 제거 # 2-1. 언더스코어 뒤 내용 제거 (예: "리피토정10mg_(10.85mg/1정)" → "리피토정10mg") med_name = re.sub(r'_.*$', '', med_name) # 언더스코어부터 끝까지 제거 # 3. 밀리그램 변환 (숫자와 함께 있는 경우 포함) # "80밀리그램" → "80mg", "밀리그램" → "mg" med_name = re.sub(r'밀리그램|밀리그람|미리그램|미리그람', 'mg', med_name) # 4. 마이크로그램 변환 med_name = re.sub(r'마이크로그램|마이크로그람', 'μg', med_name) # 5. 그램 변환 (단, mg/μg로 이미 변환된 것은 제외) med_name = re.sub(r'(? 3: ellipsized = text + "..." bbox = draw.textbbox((0, 0), ellipsized, font=final_font) w, h = bbox[2] - bbox[0], bbox[3] - bbox[1] if w <= max_width: draw.text(((label_width - w) / 2, y), ellipsized, font=final_font, fill="black") return y + h + 5 text = text[:-1] # 최악의 경우: "..." 만 표시 bbox = draw.textbbox((0, 0), "...", font=final_font) w, h = bbox[2] - bbox[0], bbox[3] - bbox[1] draw.text(((label_width - w) / 2, y), "...", font=final_font, fill="black") return y + h + 5 # 환자 이름 처리: 한글은 띄워쓰기, 영문은 동적 크기 조정 is_korean_name = all(ord('가') <= ord(char) <= ord('힣') or char.isspace() for char in patient_name if char.strip()) if is_korean_name: # 한글 이름: 한 글자씩 띄우기 (기존 방식) y_position = 10 formatted_patient_name = " ".join(patient_name) y_position = draw_centered_text(draw, formatted_patient_name, y_position, patient_name_font, max_width=label_width - 40) else: # 영문 이름: 1줄에 맞추기 위해 폰트 크기 동적 조정 (44px → 최소 28px) max_width_for_name = label_width - 40 min_font_size_1line = 28 # 1줄일 때 최소 폰트 min_font_size_2line = 20 # 2줄일 때 최소 폰트 (18px → 20px) original_font_size = 44 # 폰트 크기를 줄여가며 1줄에 맞는 크기 찾기 fitted_font = patient_name_font for font_size in range(original_font_size, min_font_size_1line - 1, -1): try: test_font = ImageFont.truetype(font_path, font_size) except IOError: test_font = ImageFont.load_default() bbox = draw.textbbox((0, 0), patient_name, font=test_font) w = bbox[2] - bbox[0] if w <= max_width_for_name: fitted_font = test_font break # 28px에도 안 맞으면 띄어쓰기 기준으로 2줄 처리 (22px 최소 폰트로) bbox = draw.textbbox((0, 0), patient_name, font=fitted_font) w = bbox[2] - bbox[0] if w > max_width_for_name and ' ' in patient_name: # 2줄 처리: 29px ~ 20px 범위에서 동적 조정 y_position = 5 # 2줄일 때는 위에서 시작 max_font_size_2line = 29 # 2줄일 때 최대 폰트 fitted_font_2line = patient_name_font for font_size in range(max_font_size_2line, min_font_size_2line - 1, -1): try: test_font = ImageFont.truetype(font_path, font_size) except IOError: test_font = ImageFont.load_default() # 각 줄이 라벨 너비에 맞는지 확인 words = patient_name.split(' ') line1 = words[0] line2 = ' '.join(words[1:]) if len(words) > 1 else '' bbox1 = draw.textbbox((0, 0), line1, font=test_font) w1 = bbox1[2] - bbox1[0] bbox2 = draw.textbbox((0, 0), line2, font=test_font) w2 = bbox2[2] - bbox2[0] if w1 <= max_width_for_name and w2 <= max_width_for_name: fitted_font_2line = test_font break # 띄어쓰기로 분리하여 2줄로 처리 words = patient_name.split(' ') line1 = words[0] line2 = ' '.join(words[1:]) if len(words) > 1 else '' # 첫 번째 줄 bbox1 = draw.textbbox((0, 0), line1, font=fitted_font_2line) w1, h1 = bbox1[2] - bbox1[0], bbox1[3] - bbox1[1] draw.text(((label_width - w1) / 2, y_position), line1, font=fitted_font_2line, fill="black") y_position += h1 + 1 # 줄 간격 축소 (2 → 1) # 두 번째 줄 if line2: bbox2 = draw.textbbox((0, 0), line2, font=fitted_font_2line) w2, h2 = bbox2[2] - bbox2[0], bbox2[3] - bbox2[1] draw.text(((label_width - w2) / 2, y_position), line2, font=fitted_font_2line, fill="black") y_position += h2 + 5 else: # 1줄로 표시: 한글과 동일한 위치에서 시작 y_position = 10 h = bbox[3] - bbox[1] draw.text(((label_width - w) / 2, y_position), patient_name, font=fitted_font, fill="black") y_position += h + 5 # 약품명 시작 위치 고정 (이름 길이와 관계없이 일정한 위치 보장) DRUG_NAME_START_Y = 60 # 약품명 시작 위치를 y=60으로 고정 if y_position < DRUG_NAME_START_Y: y_position = DRUG_NAME_START_Y # 약품명 정제 (괄호 제거, 단위 변환 등) med_name = normalize_medication_name(med_name) med_name_display, signature_info, split_occurred = split_med_name(med_name) if should_left_align(med_name_display): y_position = draw_left_aligned_text(draw, med_name_display, y_position, drug_name_font, max_width=label_width - 40) y_position = draw_fitted_single_line(draw, f"({add_info})", y_position, info_font, max_width=label_width - 40) else: y_position = draw_centered_text(draw, med_name_display, y_position, drug_name_font, max_width=label_width - 40) y_position = draw_fitted_single_line(draw, f"({add_info})", y_position, info_font, max_width=label_width - 40) if dosage and frequency and duration and unit: formatted_dosage = format_dosage(dosage) daily_dosage = dosage * frequency total_amount = daily_dosage * duration if round(dosage, 4) != round(dosage, 3): total_amount = round(total_amount * 4) / 4 formatted_total_amount = str(int(total_amount)) if total_amount.is_integer() else f"{total_amount:.2f}".rstrip('0').rstrip('.') converted_total = format_converted_total(dosage, frequency, duration, conversion_factor) if converted_total is not None: total_label = f"총{formatted_total_amount}{unit}/{duration}일분({converted_total})" else: total_label = f"총{formatted_total_amount}{unit}/{duration}일분" y_position = draw_centered_text(draw, total_label, y_position, additional_info_font, max_width=label_width - 40) box_height = 75 # 70 → 75로 증가 (하단 여백 확보) box_margin = 10 box_width = label_width - 40 box_x1 = (label_width - box_width) // 2 box_x2 = box_x1 + box_width box_y1 = y_position + box_margin box_y2 = box_y1 + box_height draw.rectangle([box_x1, box_y1, box_x2, box_y2], outline="black", width=2) box_padding = 10 line_spacing = 5 box_text1 = f"{formatted_dosage}{unit}" text1_size = info_font.getbbox(box_text1) text1_height = text1_size[3] - text1_size[1] frequency_text = custom_dosage_instruction.strip() if not frequency_text: if frequency == 1: frequency_text = "아침" elif frequency == 2: frequency_text = "아침, 저녁" elif frequency == 3: frequency_text = "아침, 점심, 저녁" elif frequency == 4: frequency_text = "아침, 점심, 저녁, 취침" # 4회 복용일 때는 작은 폰트 사용 (30px -> 24px) frequency_font = info_font if frequency == 4 and not custom_dosage_instruction.strip(): try: frequency_font = ImageFont.truetype(font_path, 24) except IOError: frequency_font = info_font text2_height = 0 if frequency_text: text2_size = frequency_font.getbbox(frequency_text) text2_height = text2_size[3] - text2_size[1] total_text_height = text1_height + line_spacing + text2_height center_y = (box_y1 + box_y2) // 2 adjustment = 7 start_y = center_y - (total_text_height // 2) - adjustment y_temp = draw_centered_text(draw, box_text1, start_y, info_font, max_width=box_width) if frequency_text: text2_y = y_temp + line_spacing draw_centered_text(draw, frequency_text, text2_y, frequency_font, max_width=box_width) y_position = box_y2 + box_margin if storage_condition: storage_condition_text = f"{storage_condition}" y_position = draw_centered_text(draw, storage_condition_text, y_position, storage_condition_font, max_width=label_width - 40) # === 동적 레이아웃 시스템 === # 상단 컨텐츠의 최종 y_position과 하단 고정 영역의 공간을 계산하여 겹침 방지 # 1. 시그니처 텍스트 및 크기 계산 signature_text = signature_info if signature_info else "청 춘 약 국" margin_val = int(0.1 * label_width) box_width_sig = label_width - 2 * margin_val try: bbox = draw.textbbox((0, 0), signature_text, font=signature_font) w_sig, h_sig = bbox[2] - bbox[0], bbox[3] - bbox[1] scale_factor = box_width_sig / w_sig if w_sig != 0 else 1 scaled_font_size = max(1, int(22 * scale_factor)) scaled_font = ImageFont.truetype(font_path, scaled_font_size) except IOError: scaled_font = ImageFont.load_default() logging.warning("시그니처 폰트 로드 실패. 기본 폰트 사용.") bbox = draw.textbbox((0, 0), signature_text, font=scaled_font) w_sig, h_sig = bbox[2] - bbox[0], bbox[3] - bbox[1] # 2. 조제일 텍스트 크기 계산 print_date_text = f"조제일 : {datetime.datetime.now(KOR_TZ).strftime('%Y-%m-%d')}" bbox = draw.textbbox((0, 0), print_date_text, font=print_date_font) date_w, date_h = bbox[2] - bbox[0], bbox[3] - bbox[1] # 3. 하단 고정 영역 필요 공간 계산 # 기본 패딩 설정 padding_top = int(h_sig * 0.1) padding_bottom = int(h_sig * 0.5) padding_sides = int(h_sig * 0.2) # 시그니처 박스 높이 signature_box_height = h_sig + padding_top + padding_bottom # 조제일과 시그니처 사이 간격 date_signature_gap = 5 # 하단 고정 영역 전체 높이 (조제일 + 간격 + 시그니처 + 하단 여백) bottom_fixed_height = date_h + date_signature_gap + signature_box_height + 10 # 4. 충돌 감지 및 조정 # 상단 컨텐츠 하단 (y_position) + 최소 여백(2px) content_bottom = y_position + 2 # 하단 고정 영역 시작점 bottom_fixed_start = label_height - bottom_fixed_height # 겹침 여부 확인 (10px 이상 겹칠 때만 조정) overlap = content_bottom - bottom_fixed_start if overlap > 10: # 겹침 발생! 조제일 제거로 공간 확보 logging.info(f"레이아웃 충돌 감지: {overlap}px 겹침. 조제일 제거로 조정.") # 조제일 없이 하단 고정 영역 재계산 bottom_fixed_height = signature_box_height + 10 bottom_fixed_start = label_height - bottom_fixed_height # 조제일 표시 안 함 show_date = False # 여전히 겹치면 시그니처 패딩 축소 overlap = content_bottom - bottom_fixed_start if overlap > 0: logging.info(f"추가 충돌 감지: {overlap}px 겹침. 시그니처 패딩 축소.") padding_top = max(2, int(h_sig * 0.05)) padding_bottom = max(2, int(h_sig * 0.2)) signature_box_height = h_sig + padding_top + padding_bottom bottom_fixed_height = signature_box_height + 5 bottom_fixed_start = label_height - bottom_fixed_height else: # 여유 공간 충분 show_date = True # 5. 조제일 그리기 (공간이 충분한 경우에만) if show_date: print_date_x = (label_width - date_w) / 2 print_date_y = label_height - bottom_fixed_height draw.text((print_date_x, print_date_y), print_date_text, font=print_date_font, fill="black") # 시그니처는 조제일 아래에 배치 signature_y_start = print_date_y + date_h + date_signature_gap else: # 시그니처는 하단 고정 시작점에 배치 signature_y_start = bottom_fixed_start # 6. 시그니처 박스 그리기 box_x = (label_width - w_sig) / 2 - padding_sides box_y = signature_y_start box_x2 = box_x + w_sig + 2 * padding_sides box_y2 = box_y + h_sig + padding_top + padding_bottom draw.rectangle([(box_x, box_y), (box_x2, box_y2)], outline="black") draw.text(((label_width - w_sig) / 2, box_y + padding_top), signature_text, font=scaled_font, fill="black") draw_scissor_border(draw, label_width, label_height, edge_size=10, steps=20) return image def print_label(patient_name, med_name, add_info, frequency, dosage, duration, formulation_type, main_ingredient_code, dosage_form, administration_route, label_name, unit=None, conversion_factor=None, storage_condition="실온보관", custom_dosage_instruction="", printer_ip=None, printer_model=None, label_type=None): """ 라벨 이미지를 생성하여 프린터로 인쇄합니다. Parameters: patient_name (str): 환자 이름 med_name (str): 약품 이름 add_info (str): 약품 효능 정보 frequency (int): 복용 횟수 dosage (float): 복용량 duration (int): 복용 일수 formulation_type (str): 제형 타입 main_ingredient_code (str): 주성분 코드 dosage_form (str): 복용 형태 administration_route (str): 투여 경로 label_name (str): 라벨 명칭 unit (str, optional): 복용 단위 conversion_factor (float, optional): 환산계수 storage_condition (str, optional): 보관 조건 custom_dosage_instruction (str, optional): 커스텀 용법 텍스트 printer_ip (str, optional): 프린터 IP 주소 (기본값: DEFAULT_PRINTER_IP) printer_model (str, optional): 프린터 모델 (기본값: DEFAULT_PRINTER_MODEL) label_type (str, optional): 라벨 타입 (기본값: DEFAULT_LABEL_TYPE) """ try: # 프린터 설정 적용 (전달되지 않으면 기본값 사용) printer_ip = printer_ip or DEFAULT_PRINTER_IP printer_model = printer_model or DEFAULT_PRINTER_MODEL label_type = label_type or DEFAULT_LABEL_TYPE if not unit: if "캡슐" in med_name: unit = "캡슐" elif "정" in med_name: unit = "정" elif "시럽" in med_name: unit = "ml" elif "과립" in med_name or "시럽" in med_name: unit = "g" else: unit = "개" label_image = create_label_image( patient_name=patient_name, med_name=med_name, add_info=add_info, frequency=frequency, dosage=dosage, duration=duration, formulation_type=formulation_type, main_ingredient_code=main_ingredient_code, dosage_form=dosage_form, administration_route=administration_route, label_name=label_name, unit=unit, conversion_factor=conversion_factor, storage_condition=storage_condition, custom_dosage_instruction=custom_dosage_instruction ) image_stream = io.BytesIO() label_image.save(image_stream, format="PNG") image_stream.seek(0) from brother_ql.raster import BrotherQLRaster from brother_ql.conversion import convert from brother_ql.backends.helpers import send qlr = BrotherQLRaster(printer_model) instructions = convert( qlr=qlr, images=[Image.open(image_stream)], label=label_type, rotate="0", threshold=70.0, dither=False, compress=False, lq=True, red=False ) send(instructions, printer_identifier=f"tcp://{printer_ip}:9100") logging.info(f"라벨 인쇄 성공: 환자={patient_name}, 약품={med_name}, 커스텀 용법={custom_dosage_instruction}") print(f"[SUCCESS] 라벨 인쇄 성공: 환자={patient_name}, 약품={med_name}, 커스텀 용법={custom_dosage_instruction}") except Exception as e: logging.error(f"라벨 인쇄 실패: {e}") print(f"[ERROR] 라벨 인쇄 실패: {e}") def print_custom_image(pil_image, printer_ip=None, printer_model=None, label_type=None): """ PIL 이미지를 받아 Brother QL 프린터로 인쇄합니다. Parameters: pil_image (PIL.Image): 인쇄할 이미지 printer_ip (str, optional): 프린터 IP 주소 (기본값: DEFAULT_PRINTER_IP) printer_model (str, optional): 프린터 모델 (기본값: DEFAULT_PRINTER_MODEL) label_type (str, optional): 라벨 타입 (기본값: DEFAULT_LABEL_TYPE) """ try: # 프린터 설정 적용 (전달되지 않으면 기본값 사용) printer_ip = printer_ip or DEFAULT_PRINTER_IP printer_model = printer_model or DEFAULT_PRINTER_MODEL label_type = label_type or DEFAULT_LABEL_TYPE logging.info(f"이미지 모드: {pil_image.mode}") if pil_image.mode in ('RGBA', 'LA'): logging.info("알파 채널 있음 (RGBA 또는 LA 모드)") elif pil_image.mode == 'P' and 'transparency' in pil_image.info: logging.info("알파 채널 있음 (팔레트 모드, transparency 키 확인됨)") else: logging.info("알파 채널 없음") pil_image = pil_image.rotate(90, expand=True) width, height = pil_image.size new_height = int((306 / width) * height) pil_image = pil_image.resize((306, new_height), Image.LANCZOS) if pil_image.mode in ('RGBA', 'LA') or (pil_image.mode == 'P' and 'transparency' in pil_image.info): background = Image.new("RGB", pil_image.size, "white") background.paste(pil_image, mask=pil_image.split()[-1]) pil_image = background image_stream = io.BytesIO() pil_image.convert('1').save(image_stream, format="PNG") image_stream.seek(0) from brother_ql.raster import BrotherQLRaster from brother_ql.conversion import convert from brother_ql.backends.helpers import send # Brother QL 프린터로 전송 qlr = BrotherQLRaster(printer_model) instructions = convert( qlr=qlr, images=[Image.open(image_stream)], label=label_type, rotate="0", # 라벨 회전 없음 threshold=70.0, # 흑백 변환 임계값 dither=False, compress=False, lq=True, # 저화질 인쇄 옵션 red=False ) send(instructions, printer_identifier=f"tcp://{printer_ip}:9100") logging.info("커스텀 이미지 인쇄 성공") print("[SUCCESS] 커스텀 이미지 인쇄 성공") except Exception as e: logging.error(f"커스텀 이미지 인쇄 실패: {e}") print(f"[ERROR] 커스텀 이미지 인쇄 실패: {e}") if __name__ == "__main__": # 인터랙티브 메뉴를 통해 샘플 인쇄 선택 samples = { "1": { "patient_name": "이영희", "med_name": "아모크라정375mg", "add_info": "고혈압", "frequency": 1, "dosage": 375.0, "duration": 30, "formulation_type": "정제", "main_ingredient_code": "AMO375", "dosage_form": "경구", "administration_route": "경구", "label_name": "고혈압용", "unit": None, "conversion_factor": 1.0, "storage_condition": "실온보관", "custom_dosage_instruction": "" }, "2": { "patient_name": "박지성", "med_name": "삼남아세트아미노펜정500mg", "add_info": "통증 완화", "frequency": 2, "dosage": 500.0, "duration": 5, "formulation_type": "정제", "main_ingredient_code": "MED001", # 예시용 "dosage_form": "경구", "administration_route": "경구", "label_name": "통증용", "unit": None, "conversion_factor": 1.0, "storage_condition": "서늘한 곳에 보관", "custom_dosage_instruction": "" }, "3": { "patient_name": "최민수", "med_name": "세레타이드125에보할러", "add_info": "알레르기 치료", "frequency": 3, "dosage": 125.0, "duration": 10, "formulation_type": "정제", "main_ingredient_code": "SER125", "dosage_form": "흡입", "administration_route": "흡입", "label_name": "알레르기용", "unit": None, "conversion_factor": 1.0, "storage_condition": "냉장보관", "custom_dosage_instruction": "" }, "4": { "patient_name": "최민수", "med_name": "트윈스타정40/5mg", "add_info": "혈압 조절", "frequency": 2, "dosage": 40.0, "duration": 10, "formulation_type": "정제", "main_ingredient_code": "TW40", "dosage_form": "경구", "administration_route": "경구", "label_name": "고혈압용", "unit": None, "conversion_factor": 1.0, "storage_condition": "실온보관", "custom_dosage_instruction": "" }, "5": { "patient_name": "최우주", "med_name": "오셀타원현탁용분말6mg/mL", "add_info": "오셀타미", "frequency": 2, "dosage": 4.0, "duration": 5, "formulation_type": "현탁용분말", "main_ingredient_code": "358907ASS", "dosage_form": "SS", "administration_route": "A", "label_name": "오셀타원현탁용분말6mg/mL", "unit": "ml", "conversion_factor": 0.126, "storage_condition": "실온및(냉장)", "custom_dosage_instruction": "" }, "6": { "patient_name": "최우주", "med_name": "어린이타이레놀현탁액", "add_info": "해열,진통제", "frequency": 3, "dosage": 3.0, "duration": 3, "formulation_type": "현탁액", "main_ingredient_code": "101330ASS", "dosage_form": "SS", "administration_route": "A", "label_name": "어린이타이레놀현탁액", "unit": "ml", "conversion_factor": None, "storage_condition": "실온보관", "custom_dosage_instruction": "" } } print("=======================================") print(" 라벨 인쇄 샘플 선택 ") print("=======================================") for key, sample in samples.items(): print(f"{key}: {sample['patient_name']} / {sample['med_name']} / {sample['add_info']}") print("q: 종료") choice = input("인쇄할 샘플 번호를 선택하세요: ").strip() while choice.lower() != 'q': if choice in samples: sample = samples[choice] print(f"선택한 샘플: {sample['patient_name']} / {sample['med_name']}") print_label( patient_name=sample["patient_name"], med_name=sample["med_name"], add_info=sample["add_info"], frequency=sample["frequency"], dosage=sample["dosage"], duration=sample["duration"], formulation_type=sample["formulation_type"], main_ingredient_code=sample["main_ingredient_code"], dosage_form=sample["dosage_form"], administration_route=sample["administration_route"], label_name=sample["label_name"], unit=sample["unit"], conversion_factor=sample["conversion_factor"], storage_condition=sample["storage_condition"], custom_dosage_instruction=sample["custom_dosage_instruction"] ) else: print("올바른 번호를 입력하세요.") choice = input("인쇄할 샴플 번호를 선택하세요 (종료하려면 q 입력): ").strip()