# pos_qr_printer.py # ESC/POS 프린터로 QR 영수증 인쇄 import socket import qrcode from PIL import Image from datetime import datetime def print_qr_receipt_escpos(qr_url, transaction_id, total_amount, claimable_points, transaction_time, printer_ip, printer_port=9100): """ ESC/POS 프린터로 QR 영수증 인쇄 Args: qr_url (str): QR 코드 URL transaction_id (str): 거래번호 total_amount (float): 판매 금액 claimable_points (int): 적립 예정 포인트 transaction_time (datetime): 거래 시간 printer_ip (str): 프린터 IP 주소 printer_port (int): 프린터 포트 (기본 9100) Returns: bool: 성공 여부 """ try: # 1. QR 코드 이미지 생성 (150x150px) qr = qrcode.QRCode(version=1, box_size=5, border=2) qr.add_data(qr_url) qr.make(fit=True) qr_image = qr.make_image(fill_color="black", back_color="white") qr_image = qr_image.resize((150, 150)) # 2. QR 이미지를 ESC/POS 비트맵으로 변환 qr_bitmap = image_to_raster(qr_image) # 3. ESC/POS 명령어 조립 ESC = b'\x1b' GS = b'\x1d' commands = [] commands.append(ESC + b'@') # 프린터 초기화 commands.append(ESC + b'a\x01') # 중앙 정렬 # 헤더 commands.append(ESC + b'!\x10') # 크게 commands.append("청춘약국\n".encode('euc-kr')) commands.append(ESC + b'!\x00') # 보통 commands.append("================================\n".encode('euc-kr')) # 거래 정보 date_str = transaction_time.strftime('%Y-%m-%d %H:%M') commands.append(f"거래일시: {date_str}\n".encode('euc-kr')) commands.append(f"거래번호: {transaction_id}\n".encode('euc-kr')) commands.append("\n".encode('euc-kr')) # 금액 정보 commands.append(ESC + b'!\x10') # 크게 commands.append(f"결제금액: {total_amount:,.0f}원\n".encode('euc-kr')) commands.append(f"적립예정: {claimable_points:,}P\n".encode('euc-kr')) commands.append(ESC + b'!\x00') # 보통 commands.append("================================\n".encode('euc-kr')) commands.append("\n".encode('euc-kr')) # QR 코드 인쇄 commands.append(qr_bitmap) # QR 비트맵 데이터 commands.append("\n".encode('euc-kr')) # 안내 문구 commands.append(ESC + b'!\x08') # 작게 commands.append("QR 촬영하고 포인트 받으세요!\n".encode('euc-kr')) commands.append(ESC + b'!\x00') # 보통 commands.append("\n\n\n".encode('euc-kr')) commands.append(GS + b'V\x00') # 용지 커트 # 4. TCP 소켓으로 전송 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.settimeout(5) sock.connect((printer_ip, printer_port)) sock.sendall(b''.join(commands)) sock.close() print(f"[ESC/POS] 인쇄 완료: {printer_ip}:{printer_port}") return True except socket.timeout: print(f"[ESC/POS] 연결 시간 초과: {printer_ip}:{printer_port}") return False except ConnectionRefusedError: print(f"[ESC/POS] 연결 거부됨: {printer_ip}:{printer_port}") return False except Exception as e: print(f"[ESC/POS] 인쇄 오류: {e}") return False def image_to_raster(image): """ PIL 이미지를 ESC/POS 비트맵 래스터 데이터로 변환 ESC/POS GS v 0 명령어 사용: GS v 0 m xL xH yL yH d1...dk Args: image (PIL.Image): PIL 이미지 객체 Returns: bytes: ESC/POS 래스터 비트맵 명령어 """ # 이미지를 흑백으로 변환 image = image.convert('1') # 1-bit 흑백 width, height = image.size # 바이트 정렬 (8픽셀 = 1바이트) width_bytes = (width + 7) // 8 # 헤더 생성 GS = b'\x1d' cmd = GS + b'v0' # 래스터 비트맵 모드 cmd += b'\x00' # 보통 모드 cmd += bytes([width_bytes & 0xFF, (width_bytes >> 8) & 0xFF]) # xL, xH cmd += bytes([height & 0xFF, (height >> 8) & 0xFF]) # yL, yH # 이미지 데이터 변환 pixels = image.load() data = [] for y in range(height): line = [] for x in range(0, width, 8): byte = 0 for bit in range(8): if x + bit < width: if pixels[x + bit, y] == 0: # 검은색 byte |= (1 << (7 - bit)) line.append(byte) data.extend(line) cmd += bytes(data) return cmd