- QR 비트맵 인쇄 재활성화 (image_to_raster_esc_star) - 에러 처리 강화: traceback 출력으로 디버깅 정보 제공 - QR 실패 시 URL 텍스트로 자동 폴백 - 텍스트 인쇄 검증 완료 (청춘약국, 거래정보, 금액 등) ESC * 방식 (24-dot double-density) 사용 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
226 lines
7.2 KiB
Python
226 lines
7.2 KiB
Python
# 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
|