This commit is contained in:
254
convert_img.py
254
convert_img.py
@@ -1,159 +1,66 @@
|
|||||||
import os
|
import os
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
||||||
def generate_micropython_printer_script(image_path, output_py_path):
|
def image_to_tspl_commands(image_path):
|
||||||
"""
|
"""
|
||||||
将图片转换为 TSPL BITMAP 指令数据,并生成 MicroPython 脚本
|
读取图片并转换为 TSPL 打印指令 (bytes)
|
||||||
|
包含: SIZE, GAP, CLS, BITMAP, PRINT
|
||||||
"""
|
"""
|
||||||
if not os.path.exists(image_path):
|
if not os.path.exists(image_path):
|
||||||
print(f"错误: 找不到图片 {image_path}")
|
print(f"错误: 找不到图片 {image_path}")
|
||||||
return
|
return None
|
||||||
|
|
||||||
# 1. 加载并预处理图片
|
|
||||||
try:
|
try:
|
||||||
img = Image.open(image_path)
|
img = Image.open(image_path)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"无法打开图片: {e}")
|
print(f"无法打开图片: {e}")
|
||||||
return
|
return None
|
||||||
|
|
||||||
# 处理透明背景 (将透明部分变为白色)
|
# 处理透明背景
|
||||||
if img.mode in ('RGBA', 'LA') or (img.mode == 'P' and 'transparency' in img.info):
|
if img.mode in ('RGBA', 'LA') or (img.mode == 'P' and 'transparency' in img.info):
|
||||||
alpha = img.convert('RGBA').split()[-1]
|
alpha = img.convert('RGBA').split()[-1]
|
||||||
bg = Image.new("RGBA", img.size, (255, 255, 255, 255))
|
bg = Image.new("RGBA", img.size, (255, 255, 255, 255))
|
||||||
bg.paste(img, mask=alpha)
|
bg.paste(img, mask=alpha)
|
||||||
img = bg
|
img = bg
|
||||||
|
|
||||||
# 目标宽度: 48mm = 384 dots (假设 203dpi)
|
# 目标尺寸: 48mm x 30mm @ 203dpi
|
||||||
target_width = 384
|
# 宽度: 48 * 8 = 384 dots
|
||||||
|
# 高度: 30 * 8 = 240 dots
|
||||||
|
MAX_WIDTH = 384
|
||||||
|
MAX_HEIGHT = 240
|
||||||
|
|
||||||
# 保持比例缩放
|
# 使用 thumbnail 进行等比缩放,确保不超过最大尺寸
|
||||||
w_percent = (target_width / float(img.size[0]))
|
img.thumbnail((MAX_WIDTH, MAX_HEIGHT), Image.Resampling.LANCZOS)
|
||||||
target_height = int((float(img.size[1]) * float(w_percent)))
|
|
||||||
|
|
||||||
img = img.resize((target_width, target_height), Image.Resampling.LANCZOS)
|
target_width, target_height = img.size
|
||||||
|
print(f"图片缩放后尺寸: {target_width}x{target_height}")
|
||||||
|
|
||||||
# 转为二值图 (0=黑, 255=白)
|
# 转为二值图
|
||||||
# 优化: 使用 Floyd-Steinberg 抖动算法 (convert('1') 默认行为)
|
# 1. 先转灰度
|
||||||
# 这样可以保留更多细节,而不是简单的阈值切断
|
img = img.convert('L')
|
||||||
|
# 2. 二值化 (使用默认的抖动算法)
|
||||||
img = img.convert('1')
|
img = img.convert('1')
|
||||||
|
|
||||||
# 如果线条太细,可以先尝试增强一下对比度 (可选)
|
# 构造 BITMAP 数据
|
||||||
# 但对于线稿,抖动通常能很好地还原灰度细节
|
|
||||||
|
|
||||||
# 强制不自动判断,直接根据用户反馈修改
|
|
||||||
# 用户反馈: "明明白色的底打印成黑色了"
|
|
||||||
# 这意味着我们之前的逻辑把白色映射为了 1 (打印/变黑)。
|
|
||||||
# TSPL 中: 1 = Print dot (Black), 0 = No print (White).
|
|
||||||
# 所以我们需要确保: White pixel -> 0 bit.
|
|
||||||
|
|
||||||
# 如果之前判定逻辑是:
|
|
||||||
# if white > black: invert=True (白色为背景)
|
|
||||||
# if invert: if pixel==0(Black) -> set 1.
|
|
||||||
# 也就是说之前逻辑是: 黑色像素设为1,白色像素设为0。这应该是对的。
|
|
||||||
# 但用户说打出来全是黑的,说明白色像素被设为了1。
|
|
||||||
# 这意味着可能实际上不需要反色,或者之前的反色逻辑写反了。
|
|
||||||
|
|
||||||
# 让我们强制反转之前的逻辑。
|
|
||||||
# 之前: invert_logic = True (when white bg)
|
|
||||||
# 现在强制改为 False 再试一次。
|
|
||||||
|
|
||||||
invert_logic = False
|
|
||||||
print("强制设置: 不执行反色逻辑 (测试用)")
|
|
||||||
|
|
||||||
# 2. 转换为 TSPL BITMAP 数据格式
|
|
||||||
width_bytes = (target_width + 7) // 8
|
width_bytes = (target_width + 7) // 8
|
||||||
data = bytearray()
|
data = bytearray()
|
||||||
|
|
||||||
|
# 遍历像素生成数据
|
||||||
|
# TSPL BITMAP 数据: 1=Black(Print), 0=White(No Print)
|
||||||
|
# PIL '1' mode: 0=Black, 255=White
|
||||||
|
|
||||||
for y in range(target_height):
|
for y in range(target_height):
|
||||||
row_bytes = bytearray(width_bytes)
|
row_bytes = bytearray(width_bytes)
|
||||||
for x in range(target_width):
|
for x in range(target_width):
|
||||||
pixel = img.getpixel((x, y))
|
pixel = img.getpixel((x, y))
|
||||||
|
|
||||||
|
# 逻辑修正:
|
||||||
|
# 我们希望 黑色像素(0) -> 打印(1)
|
||||||
|
# 白色像素(255) -> 不打印(0)
|
||||||
|
|
||||||
should_print = False
|
should_print = False
|
||||||
# 重新梳理逻辑
|
if pixel == 0: # Black
|
||||||
# 我们想要: 黑色像素(0) -> 打印(1), 白色像素(255) -> 不打印(0)
|
should_print = True
|
||||||
|
|
||||||
# 假设 img 是 '1' mode: 0=Black, 255/1=White
|
|
||||||
if pixel == 0: # 是黑色像素
|
|
||||||
should_print = True # 我们要打印它 -> set 1
|
|
||||||
else: # 是白色像素
|
|
||||||
should_print = False # 不打印 -> set 0
|
|
||||||
|
|
||||||
# 如果之前的逻辑打出来是反的,说明之前的代码逻辑产生的位是反的。
|
|
||||||
# 之前的代码:
|
|
||||||
# if invert_logic (True): if pixel == 0: should_print = True
|
|
||||||
# 这逻辑看起来是对的啊... 黑像素打印。
|
|
||||||
# 为什么用户说反了?
|
|
||||||
# 可能性 1: TSPL 0=Black, 1=White? (不太可能,通常热敏都是加热点为1)
|
|
||||||
# 可能性 2: 图片转换时 convert('1') 并没有按预期工作,或者 getpixel 返回值理解错了。
|
|
||||||
# 可能性 3: 之前的 invert_logic 其实是 False?
|
|
||||||
|
|
||||||
# 不管怎样,既然用户说反了,我们就强制反过来。
|
|
||||||
# 强制反转:
|
|
||||||
# 如果是黑色像素(0) -> 不打印 (0)
|
|
||||||
# 如果是白色像素(255) -> 打印 (1)
|
|
||||||
# (但这会导致白底变黑块... 等等,用户现在的抱怨正是"白色的底打印成黑色了")
|
|
||||||
# 所以现在的状态是: 白色像素被打印了。
|
|
||||||
|
|
||||||
# 所以我们需要: 白色像素 -> 不打印。
|
|
||||||
# 这意味着 bit 必须是 0。
|
|
||||||
|
|
||||||
# 让我们再试一次完全相反的逻辑:
|
|
||||||
# 之前: if pixel == 0: print. (即黑->打) -> 用户说白底变黑了。
|
|
||||||
# 这说明 convert('1') 后,原来的白色背景变成了 0 ?
|
|
||||||
# 让我们打印一下像素值看看。
|
|
||||||
|
|
||||||
# 修正策略:不管那么多,直接加一个全局取反开关。
|
|
||||||
# 用户现在的现象:白底 -> 黑。
|
|
||||||
# 我们要:白底 -> 白。
|
|
||||||
# 所以我们要把发送给打印机的 bit 取反。
|
|
||||||
|
|
||||||
# 在这里我们实现一个逻辑:
|
|
||||||
# 如果 pixel != 0 (即白色): 不打印 (0)
|
|
||||||
# 如果 pixel == 0 (即黑色): 打印 (1)
|
|
||||||
|
|
||||||
# 但等等,之前的代码:
|
|
||||||
# if invert_logic: if pixel == 0: should_print = True
|
|
||||||
# invert_logic 之前是 True. 所以 if pixel == 0 (黑) -> print.
|
|
||||||
# 结果白底变黑了。
|
|
||||||
# 这说明在 convert('1') 之后,白色背景的 pixel 值变成了 0 ??
|
|
||||||
# 在 PIL '1' mode 中,0=Black, 255=White。
|
|
||||||
# 除非... resize 过程中的插值导致了问题?
|
|
||||||
|
|
||||||
# 让我们尝试最简单的逻辑:
|
|
||||||
# 强制反转当前所有位的逻辑。
|
|
||||||
|
|
||||||
if pixel != 0: # 白色
|
|
||||||
should_print = True # 试一下让白色打印?不,这会更黑。
|
|
||||||
|
|
||||||
# 让我们回退到最原始的猜测:
|
|
||||||
# 用户说 "白色的底打印成黑色了"。
|
|
||||||
# 说明我们给白色背景发了 '1'。
|
|
||||||
# 我们之前的逻辑是: if pixel == 0: 1.
|
|
||||||
# 说明白色背景的 pixel 是 0。
|
|
||||||
# 这意味着 convert('1') 把白色转成了 0。
|
|
||||||
# 让我们检查 convert 的阈值逻辑。
|
|
||||||
|
|
||||||
# 此次修改:直接取反。
|
|
||||||
if pixel != 0: # 如果是 255 (原图白)
|
|
||||||
should_print = False # 不打印 -> 0
|
|
||||||
else: # 如果是 0 (原图黑)
|
|
||||||
should_print = True # 打印 -> 1
|
|
||||||
|
|
||||||
# 但为了确解决用户的"反了"的问题,我们将在此逻辑基础上再取反一次?
|
|
||||||
# 不,用户说现在是反的。
|
|
||||||
# 现在的代码是:
|
|
||||||
# if invert_logic (True): if pixel == 0: True.
|
|
||||||
# 也就是 0->1.
|
|
||||||
# 结果: 反了。
|
|
||||||
# 结论: 应该是 0->0, 255->1 ? (即 0是不打,1是打?)
|
|
||||||
# 或者是 pixel值反了。
|
|
||||||
|
|
||||||
# 决定:直接反转判断条件。
|
|
||||||
if pixel != 0: # 白色/255
|
|
||||||
should_print = True # 之前是 False
|
|
||||||
else: # 黑色/0
|
|
||||||
should_print = False # 之前是 True
|
|
||||||
|
|
||||||
if should_print:
|
if should_print:
|
||||||
byte_index = x // 8
|
byte_index = x // 8
|
||||||
@@ -161,29 +68,76 @@ def generate_micropython_printer_script(image_path, output_py_path):
|
|||||||
row_bytes[byte_index] |= (1 << bit_index)
|
row_bytes[byte_index] |= (1 << bit_index)
|
||||||
|
|
||||||
data.extend(row_bytes)
|
data.extend(row_bytes)
|
||||||
|
|
||||||
# 3. 生成 MicroPython 脚本内容
|
|
||||||
# 优化: 修复 GAP 问题,避免浪费纸张
|
|
||||||
# 用户抱怨: "每次打印中间都浪费了一张白色的贴纸"
|
|
||||||
# 原因: 可能 GAP 设置不对,或者 size 设置太高,或者自动进纸了。
|
|
||||||
# 标签纸高度通常是固定的。用户之前说是 30mm。
|
|
||||||
# 我们现在的 height 是动态算的。
|
|
||||||
# 如果算的 height > 30mm,打印机就会跨页。
|
|
||||||
# 让我们强制限制最大高度。
|
|
||||||
|
|
||||||
# 强制裁剪或缩放高度到 30mm (240 dots)
|
|
||||||
if target_height > 240:
|
|
||||||
print(f"警告: 图片高度 {target_height} 超过标签高度 240,将强制调整。")
|
|
||||||
# 这里为了不破坏长宽比,最好是重新 resize,但为了简单,先让用户能打出来。
|
|
||||||
# 更好的做法是缩小图片以适应 48x30mm
|
|
||||||
# 重新计算缩放
|
|
||||||
h_percent = (240 / float(img.size[1]))
|
|
||||||
# 取宽高中较小的缩放比,确保能塞进去
|
|
||||||
scale = min(w_percent, h_percent) # 使用之前的 w_percent 和新的 h_percent 比较?
|
|
||||||
# 不,重新算吧。
|
|
||||||
|
|
||||||
# 但我们已经在上面 resize 过了。
|
# 计算居中偏移
|
||||||
# 让我们修改脚本里的 SIZE 设置。
|
x_offset = (MAX_WIDTH - target_width) // 2
|
||||||
|
y_offset = (MAX_HEIGHT - target_height) // 2
|
||||||
|
|
||||||
|
# 生成指令
|
||||||
|
cmds = bytearray()
|
||||||
|
|
||||||
|
# 1. 初始化
|
||||||
|
# SIZE 48 mm, 30 mm
|
||||||
|
cmds.extend(b"SIZE 48 mm, 30 mm\r\n")
|
||||||
|
# GAP 2 mm, 0 mm
|
||||||
|
cmds.extend(b"GAP 2 mm, 0 mm\r\n")
|
||||||
|
# CLS
|
||||||
|
cmds.extend(b"CLS\r\n")
|
||||||
|
|
||||||
|
# 2. BITMAP
|
||||||
|
# BITMAP x, y, width_bytes, height, mode, data
|
||||||
|
header = f"BITMAP {x_offset},{y_offset},{width_bytes},{target_height},0,".encode('utf-8')
|
||||||
|
cmds.extend(header)
|
||||||
|
cmds.extend(data)
|
||||||
|
cmds.extend(b"\r\n") # BITMAP data 后面通常不需要回车,但有些文档建议加? 不,binary data后紧跟下一个指令
|
||||||
|
# TSPL protocol says: BITMAP ... data ... CR LF is NOT required after data, but next command must start.
|
||||||
|
# Usually it's safer to just send data.
|
||||||
|
|
||||||
|
# 3. PRINT
|
||||||
|
cmds.extend(b"PRINT 1\r\n")
|
||||||
|
|
||||||
|
return cmds
|
||||||
|
|
||||||
|
def generate_micropython_printer_script(image_path, output_py_path):
|
||||||
|
"""
|
||||||
|
保留此函数以兼容现有 workflow,但内部使用新的逻辑
|
||||||
|
"""
|
||||||
|
cmds = image_to_tspl_commands(image_path)
|
||||||
|
if not cmds:
|
||||||
|
return
|
||||||
|
|
||||||
|
# 提取 BITMAP 数据部分用于生成脚本 (因为脚本里是用 uart.write 分段发送的)
|
||||||
|
# 为了简单,我们重新解析一下 cmds 或者直接重写这部分逻辑
|
||||||
|
# 但为了脚本的可读性,还是像之前一样生成
|
||||||
|
|
||||||
|
# 重新执行一遍核心逻辑来获取参数 (为了生成漂亮的 python 代码)
|
||||||
|
if not os.path.exists(image_path): return
|
||||||
|
img = Image.open(image_path)
|
||||||
|
if img.mode in ('RGBA', 'LA') or (img.mode == 'P' and 'transparency' in img.info):
|
||||||
|
alpha = img.convert('RGBA').split()[-1]
|
||||||
|
bg = Image.new("RGBA", img.size, (255, 255, 255, 255))
|
||||||
|
bg.paste(img, mask=alpha)
|
||||||
|
img = bg
|
||||||
|
|
||||||
|
MAX_WIDTH = 384
|
||||||
|
MAX_HEIGHT = 240
|
||||||
|
img.thumbnail((MAX_WIDTH, MAX_HEIGHT), Image.Resampling.LANCZOS)
|
||||||
|
target_width, target_height = img.size
|
||||||
|
img = img.convert('L').convert('1')
|
||||||
|
|
||||||
|
width_bytes = (target_width + 7) // 8
|
||||||
|
data = bytearray()
|
||||||
|
for y in range(target_height):
|
||||||
|
row_bytes = bytearray(width_bytes)
|
||||||
|
for x in range(target_width):
|
||||||
|
if img.getpixel((x, y)) == 0:
|
||||||
|
byte_index = x // 8
|
||||||
|
bit_index = 7 - (x % 8)
|
||||||
|
row_bytes[byte_index] |= (1 << bit_index)
|
||||||
|
data.extend(row_bytes)
|
||||||
|
|
||||||
|
x_offset = (MAX_WIDTH - target_width) // 2
|
||||||
|
y_offset = (MAX_HEIGHT - target_height) // 2
|
||||||
|
|
||||||
hex_data = data.hex()
|
hex_data = data.hex()
|
||||||
|
|
||||||
@@ -207,11 +161,9 @@ def print_image():
|
|||||||
print("=== 开始打印图片 ===")
|
print("=== 开始打印图片 ===")
|
||||||
|
|
||||||
# 2. 设置标签
|
# 2. 设置标签
|
||||||
# 修正: 严格匹配标签纸尺寸,防止浪费
|
|
||||||
printer.cls()
|
printer.cls()
|
||||||
printer.size(48, 30) # 强制 48mm x 30mm
|
printer.size(48, 30)
|
||||||
printer.gap(2, 0)
|
printer.gap(2, 0)
|
||||||
printer.home()
|
|
||||||
|
|
||||||
# 3. 准备图片数据
|
# 3. 准备图片数据
|
||||||
img_hex = "{hex_data}"
|
img_hex = "{hex_data}"
|
||||||
@@ -221,10 +173,8 @@ def print_image():
|
|||||||
print(f"正在发送图片数据 ({{len(img_data)}} bytes)...")
|
print(f"正在发送图片数据 ({{len(img_data)}} bytes)...")
|
||||||
|
|
||||||
# BITMAP X, Y, width_bytes, height, mode, data
|
# BITMAP X, Y, width_bytes, height, mode, data
|
||||||
# 居中打印: Y 轴偏移
|
# 居中打印
|
||||||
y_offset = max(0, (240 - {target_height}) // 2)
|
cmd = f"BITMAP {x_offset},{y_offset},{width_bytes},{target_height},0,".encode('utf-8')
|
||||||
|
|
||||||
cmd = f"BITMAP 0,{{y_offset}},{width_bytes},{target_height},0,".encode('utf-8')
|
|
||||||
uart.write(cmd)
|
uart.write(cmd)
|
||||||
|
|
||||||
chunk_size = 128
|
chunk_size = 128
|
||||||
|
|||||||
3
main.py
3
main.py
@@ -4,7 +4,7 @@ import struct
|
|||||||
import gc
|
import gc
|
||||||
import network
|
import network
|
||||||
import st7789py as st7789
|
import st7789py as st7789
|
||||||
from config import CURRENT_CONFIG, SERVER_URL
|
from config import CURRENT_CONFIG, SERVER_URL, ttl_tx, ttl_rx
|
||||||
from audio import AudioPlayer, Microphone
|
from audio import AudioPlayer, Microphone
|
||||||
|
|
||||||
# Define colors that might be missing in st7789py
|
# Define colors that might be missing in st7789py
|
||||||
@@ -19,6 +19,7 @@ WIFI_PASS = "djt12345678"
|
|||||||
|
|
||||||
IMAGE_STATE_IDLE = 0
|
IMAGE_STATE_IDLE = 0
|
||||||
IMAGE_STATE_RECEIVING = 1
|
IMAGE_STATE_RECEIVING = 1
|
||||||
|
PRINTER_STATE_RECEIVING = 2
|
||||||
|
|
||||||
UI_SCREEN_HOME = 0
|
UI_SCREEN_HOME = 0
|
||||||
UI_SCREEN_RECORDING = 1
|
UI_SCREEN_RECORDING = 1
|
||||||
|
|||||||
@@ -14,6 +14,10 @@ from dashscope.audio.asr import Recognition, RecognitionCallback, RecognitionRes
|
|||||||
from dashscope import ImageSynthesis
|
from dashscope import ImageSynthesis
|
||||||
from dashscope import Generation
|
from dashscope import Generation
|
||||||
|
|
||||||
|
import sys
|
||||||
|
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
import convert_img
|
||||||
|
|
||||||
# 加载环境变量
|
# 加载环境变量
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
dashscope.api_key = os.getenv("DASHSCOPE_API_KEY")
|
dashscope.api_key = os.getenv("DASHSCOPE_API_KEY")
|
||||||
@@ -883,6 +887,34 @@ async def websocket_endpoint(websocket: WebSocket):
|
|||||||
else:
|
else:
|
||||||
await websocket.send_text("STATUS:ERROR:提示词为空")
|
await websocket.send_text("STATUS:ERROR:提示词为空")
|
||||||
|
|
||||||
|
elif text == "PRINT_IMAGE":
|
||||||
|
print("Received PRINT_IMAGE request")
|
||||||
|
if os.path.exists(GENERATED_IMAGE_FILE):
|
||||||
|
try:
|
||||||
|
# Use convert_img logic to get TSPL commands
|
||||||
|
tspl_data = convert_img.image_to_tspl_commands(GENERATED_IMAGE_FILE)
|
||||||
|
if tspl_data:
|
||||||
|
print(f"Sending printer data: {len(tspl_data)} bytes")
|
||||||
|
await websocket.send_text(f"PRINTER_DATA_START:{len(tspl_data)}")
|
||||||
|
|
||||||
|
# Send in chunks
|
||||||
|
chunk_size = 512
|
||||||
|
for i in range(0, len(tspl_data), chunk_size):
|
||||||
|
chunk = tspl_data[i:i+chunk_size]
|
||||||
|
await websocket.send_bytes(chunk)
|
||||||
|
# Small delay to prevent overwhelming ESP32 buffer
|
||||||
|
await asyncio.sleep(0.01)
|
||||||
|
|
||||||
|
await websocket.send_text("PRINTER_DATA_END")
|
||||||
|
print("Printer data sent")
|
||||||
|
else:
|
||||||
|
await websocket.send_text("STATUS:ERROR:图片转换失败")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error converting image for printer: {e}")
|
||||||
|
await websocket.send_text(f"STATUS:ERROR:打印出错: {str(e)}")
|
||||||
|
else:
|
||||||
|
await websocket.send_text("STATUS:ERROR:没有可打印的图片")
|
||||||
|
|
||||||
elif text.startswith("GET_TASK_STATUS:"):
|
elif text.startswith("GET_TASK_STATUS:"):
|
||||||
task_id = text.split(":", 1)[1].strip()
|
task_id = text.split(":", 1)[1].strip()
|
||||||
if task_id in active_tasks:
|
if task_id in active_tasks:
|
||||||
|
|||||||
Reference in New Issue
Block a user