# 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: print(f"[ESC/POS] QR 인쇄 시작: {qr_url}") # 1. QR 코드 이미지 생성 (작게: 100x100px) qr = qrcode.QRCode(version=1, box_size=3, 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((100, 100)) print(f"[ESC/POS] QR 이미지 생성 완료: 100x100px") # 2. ESC/POS 명령어 조립 ESC = b'\x1b' GS = b'\x1d' commands = [] # 프린터 초기화 (강화) commands.append(ESC + b'@') # 중앙 정렬 (더 호환성 높은 방식) commands.append(ESC + b'a' + bytes([1])) # 헤더 (폰트 크기 명령어 제거 - 더 안전) commands.append("\n".encode('euc-kr')) commands.append("================================\n".encode('euc-kr')) commands.append(" 청춘약국\n".encode('euc-kr')) commands.append("================================\n".encode('euc-kr')) 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(f"결제금액: {total_amount:,.0f}원\n".encode('euc-kr')) commands.append(f"적립예정: {claimable_points:,}P\n".encode('euc-kr')) commands.append("\n".encode('euc-kr')) commands.append("================================\n".encode('euc-kr')) commands.append("\n".encode('euc-kr')) # 3. QR 코드 인쇄 (비트맵 시도) try: print(f"[ESC/POS] QR 비트맵 변환 시작...") qr_bitmap = image_to_raster_esc_star(qr_image) commands.append(qr_bitmap) commands.append(b"\n") print(f"[ESC/POS] QR 비트맵 변환 완료 (ESC *)") except Exception as e: print(f"[ESC/POS] QR 비트맵 변환 실패: {e}") import traceback traceback.print_exc() # QR 실패 시 URL 텍스트로 대체 commands.append("QR 코드:\n".encode('euc-kr')) commands.append(f"{qr_url}\n".encode('euc-kr')) commands.append("\n".encode('euc-kr')) # 안내 문구 (폰트 명령어 제거) commands.append("QR 촬영하고 포인트 받으세요!\n".encode('euc-kr')) commands.append("\n".encode('euc-kr')) commands.append("================================\n".encode('euc-kr')) # 여백 및 커트 commands.append("\n\n\n".encode('euc-kr')) # 용지 커트 (더 호환성 높은 명령어) commands.append(GS + b'V' + bytes([1])) # Partial cut # 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_esc_star(image): """ PIL 이미지를 ESC/POS 비트맵으로 변환 (ESC * 방식 - 호환성 높음) ESC * m nL nH d1...dk 명령어 사용 m=33 (24-dot double-density) Args: image (PIL.Image): PIL 이미지 객체 Returns: bytes: ESC/POS 비트맵 명령어 """ # 이미지를 흑백으로 변환 image = image.convert('1') # 1-bit 흑백 width, height = image.size # 픽셀 데이터 가져오기 pixels = image.load() # ESC * 명령어로 라인별 인쇄 (24-dot 방식) ESC = b'\x1b' commands = [] # 24픽셀(3바이트) 단위로 인쇄 for y in range(0, height, 24): # 현재 라인의 높이 (최대 24픽셀) line_height = min(24, height - y) # 너비 바이트 수 width_bytes = (width + 7) // 8 # ESC * m nL nH: m=33 (24-dot double-density) nL = width & 0xFF nH = (width >> 8) & 0xFF commands.append(ESC + b'*' + bytes([33, nL, nH])) # 라인 데이터 생성 for x in range(width): # 세로 24픽셀을 3바이트로 변환 byte1, byte2, byte3 = 0, 0, 0 for bit in range(line_height): pixel_y = y + bit if pixel_y < height: if pixels[x, pixel_y] == 0: # 검은색 if bit < 8: byte1 |= (1 << (7 - bit)) elif bit < 16: byte2 |= (1 << (15 - bit)) else: byte3 |= (1 << (23 - bit)) commands.append(bytes([byte1, byte2, byte3])) # 라인 피드 commands.append(b'\n') return b''.join(commands) 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