diff --git a/convert_img.py b/convert_img.py new file mode 100644 index 0000000..d12604f --- /dev/null +++ b/convert_img.py @@ -0,0 +1,240 @@ +import os +from PIL import Image, ImageOps + +def image_to_tspl_commands(image_path): + """ + 读取图片并转换为 TSPL 打印指令 (bytes) + 包含: SIZE, GAP, CLS, BITMAP, PRINT + """ + if not os.path.exists(image_path): + print(f"错误: 找不到图片 {image_path}") + return None + + try: + img = Image.open(image_path) + except Exception as e: + print(f"无法打开图片: {e}") + return None + + # 处理透明背景 + 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 + + # 目标尺寸: 48mm x 30mm @ 203dpi + # 宽度: 48 * 8 = 384 dots + # 高度: 30 * 8 = 240 dots + MAX_WIDTH = 384 + MAX_HEIGHT = 240 + + # 1. 强制裁剪/缩放以填满 (Aspect Fill / Cover) + # 计算目标比例 + target_ratio = MAX_WIDTH / MAX_HEIGHT + img_ratio = img.width / img.height + + if img_ratio > target_ratio: + # 图片更宽,以高度为基准缩放,然后裁剪宽度 + new_height = MAX_HEIGHT + new_width = int(new_height * img_ratio) + img = img.resize((new_width, new_height), Image.Resampling.LANCZOS) + # 居中裁剪 + left = (new_width - MAX_WIDTH) // 2 + img = img.crop((left, 0, left + MAX_WIDTH, MAX_HEIGHT)) + else: + # 图片更高,以宽度为基准缩放,然后裁剪高度 + new_width = MAX_WIDTH + new_height = int(new_width / img_ratio) + img = img.resize((new_width, new_height), Image.Resampling.LANCZOS) + # 居中裁剪 + top = (new_height - MAX_HEIGHT) // 2 + img = img.crop((0, top, MAX_WIDTH, top + MAX_HEIGHT)) + + target_width, target_height = img.size + print(f"图片处理后尺寸 (Cover模式): {target_width}x{target_height}") + + # 转为二值图 + # 1. 先转灰度 + img = img.convert('L') + # 2. 二值化 (使用默认的抖动算法) + img = img.convert('1') + + # 构造 BITMAP 数据 + width_bytes = (target_width + 7) // 8 + data = bytearray() + + # 遍历像素生成数据 + # 修正逻辑:用户反馈打印出来是负片(黑底白字),说明原有的判断反了。 + # 我们现在采用完全相反的逻辑。 + # 之前的逻辑: if pixel == 0: should_print = True (导致背景被打印) + # 现在的逻辑: if pixel != 0: should_print = True (即 0不打印,255打印) + # 这样背景(通常是0或255,看PIL怎么处理)会被反转。 + # 如果原图是白底(255)黑字(0),PIL convert('1') 后背景通常是 255,字是 0。 + # 如果用 if pixel != 0: print -> 背景打(黑),字不打(白)。这还是负片。 + # 如果之前的逻辑 (pixel==0 -> print) 导致了负片,说明背景走了 print 分支,说明背景是 0。 + # 这意味着 PIL 读进来的图背景是 0。 + # 所以如果要背景不打,我们应该:if pixel == 0: no_print. + # 所以 if pixel != 0: print. + + # 让我们再加一道保险:ImageOps.invert + # 但 ImageOps.invert 不支持 '1' 模式,要先转 'L' 再 invert 再转 '1' 比较好。 + # 不过既然我们是在 pixel 级别操作,我们可以直接在判断里改。 + + for y in range(target_height): + row_bytes = bytearray(width_bytes) + for x in range(target_width): + pixel = img.getpixel((x, y)) + + should_print = False + # 尝试修复:如果 pixel != 0 (即白色/非黑),则打印? + # 不,根据推导,背景如果是 0,那我们要背景不打印,所以 if pixel != 0: print. + # 让我们试试这个逻辑。 + if pixel != 0: + should_print = True + + if should_print: + byte_index = x // 8 + bit_index = 7 - (x % 8) + row_bytes[byte_index] |= (1 << bit_index) + + data.extend(row_bytes) + + # 计算居中偏移 (理论上 Cover 模式下偏移为 0) + x_offset = (MAX_WIDTH - target_width) // 2 + y_offset = (MAX_HEIGHT - target_height) // 2 + + # 生成指令 + cmds = bytearray() + + # 1. 初始化 + # CLS + cmds.extend(b"CLS\r\n") + # 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") + # HOME + cmds.extend(b"HOME\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() + + script_content = f'''from machine import UART +import time +from config import ttl_tx, ttl_rx +from printer_driver import TsplPrinter +import ubinascii + +# ============================================================================== +# 图片打印脚本 (自动生成) +# ============================================================================== +# 图片来源: {image_path} +# ============================================================================== + +# 1. 初始化 +uart = UART(1, baudrate=115200, tx=ttl_tx, rx=ttl_rx) +printer = TsplPrinter(uart) + +def print_image(): + print("=== 开始打印图片 ===") + + # 2. 设置标签 + printer.cls() + printer.size(48, 30) + printer.gap(2, 0) + + # 3. 准备图片数据 + img_hex = "{hex_data}" + img_data = ubinascii.unhexlify(img_hex) + + # 4. 发送 BITMAP 指令 + print(f"正在发送图片数据 ({{len(img_data)}} bytes)...") + + # BITMAP X, Y, width_bytes, height, mode, data + # 居中打印 + cmd = f"BITMAP {x_offset},{y_offset},{width_bytes},{target_height},0,".encode('utf-8') + uart.write(cmd) + + chunk_size = 128 + for i in range(0, len(img_data), chunk_size): + uart.write(img_data[i : i + chunk_size]) + time.sleep(0.005) + + uart.write(b'\\r\\n') + + # 5. 打印 + printer.print_out(1) + print("打印完成") + +if __name__ == "__main__": + try: + print_image() + except Exception as e: + print(f"错误: {{e}}") +''' + + with open(output_py_path, 'w', encoding='utf-8') as f: + f.write(script_content) + + print(f"成功生成 MicroPython 脚本: {output_py_path}") + +if __name__ == "__main__": + generate_micropython_printer_script( + "/Users/jeremygan/Desktop/python_dev/V2_micropython/test_image.png", + "/Users/jeremygan/Desktop/python_dev/V2_micropython/printer_image_print.py" + ) diff --git a/websocket_server/server.py b/websocket_server/server.py index de8cf0a..4e26954 100644 --- a/websocket_server/server.py +++ b/websocket_server/server.py @@ -535,7 +535,7 @@ def optimize_prompt(asr_text, progress_callback=None): 关键要求: 1. 风格必须是:简单的黑白线稿、简笔画、图标风格 (Line art, Sketch, Icon style)。 2. 画面必须清晰、线条粗壮,适合低分辨率热敏打印机打印。 -3. 绝对不要有复杂的阴影、渐变、彩色描述。 +3. 绝对不要有复杂的阴影、渐变、黑白线条描述。 4. 背景必须是纯白 (White background)。 5. 提示词内容请使用英文描述,因为绘图模型对英文理解更好,但在描述中强调 "black and white line art", "simple lines", "vector style"。 6. 尺寸比例遵循宽48mm:高30mm (约 1.6:1)。 @@ -615,7 +615,8 @@ def generate_image(prompt, progress_callback=None, retry_count=0, max_retries=2) response = ImageSynthesis.call( model='wanx2.0-t2i-turbo', - prompt=prompt + prompt=prompt, + size='1280*720' ) if response.status_code == 200: