diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..5b6ebaf --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use python autokanban diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..24ee5b1 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.13 diff --git a/app/main.py b/app/main.py index c2f416d..d306e7c 100644 --- a/app/main.py +++ b/app/main.py @@ -21,7 +21,7 @@ app.mount("/static", StaticFiles(directory="app/static"), name="static") app.mount("/out", StaticFiles(directory="out"), name="out") TASKS_FILE = Path("data/tasks.json") -DEBUG_PRINT_TO_IMAGE = True # Set to True to enable card image generation instead of real printing +ENABLE_PHYSICAL_PRINTER = True # Set to False to skip physical printing (only generate preview images) OUT_DIR = Path("out") OUT_DIR.mkdir(exist_ok=True) CARD_WIDTH_PX = 354 # 58mm * 300dpi / 25.4 @@ -140,109 +140,110 @@ def print_task(request: Request, task_id: str): if task.id == task_id: if (task.status == "approved") or (task.status == "printed" and request.session.get("admin")): try: - if DEBUG_PRINT_TO_IMAGE: - # Generate styled card image with icon - img = Image.new("L", (CARD_WIDTH_PX, CARD_HEIGHT_PX), 255) - draw = ImageDraw.Draw(img) - draw.rectangle([(0,0),(CARD_WIDTH_PX-1,CARD_HEIGHT_PX-1)], outline=0, width=4) + # Always generate preview image + img = Image.new("L", (CARD_WIDTH_PX, CARD_HEIGHT_PX), 255) + draw = ImageDraw.Draw(img) + draw.rectangle([(0,0),(CARD_WIDTH_PX-1,CARD_HEIGHT_PX-1)], outline=0, width=4) - # Robust font loading - font_title = load_font(FONT_BOLD, 36, font_label="title") - font_label_f = load_font(FONT_BOLD, 18, font_label="label") - font_text = load_font(FONT_REGULAR, 22, font_label="text") - font_icon = load_font(FA_FONT, 48, font_label="icon") + # Robust font loading + font_title = load_font(FONT_BOLD, 36, font_label="title") + font_label_f = load_font(FONT_BOLD, 18, font_label="label") + font_text = load_font(FONT_REGULAR, 22, font_label="text") + font_icon = load_font(FA_FONT, 48, font_label="icon") - # Prepare content for line wrapping - import textwrap - y = 24 - max_text_width = CARD_WIDTH_PX - 40 - content_lines = [] - if font_title: - wrapper = textwrap.TextWrapper(width=24) - lines = wrapper.wrap(task.content) - for line in lines: - # Check pixel width, wrap further if needed - while font_title.getlength(line) > max_text_width: - # Reduce by one word at a time - split = line.rsplit(' ', 1) - if len(split) == 2: - content_lines.append(split[0]) - line = split[1] - else: - # Single long word - content_lines.append(line[:20]) - line = line[20:] - content_lines.append(line) - else: - content_lines = textwrap.wrap(task.content, width=24) - - # Center lines vertically - - # Calculate text heights using getbbox - def get_text_height(font, text): - try: - bbox = font.getbbox(text) - return bbox[3] - bbox[1] - except Exception: - return 24 - - line_heights = [get_text_height(font_title, line) for line in content_lines] - total_text_height = sum(line_heights) + (len(content_lines)-1)*2 - y = (CARD_HEIGHT_PX - total_text_height) // 2 - 10 - for idx, line in enumerate(content_lines): - try: - w = font_title.getlength(line) - except Exception: - w = len(line) * 18 - draw.text(((CARD_WIDTH_PX-w)//2, y), line, font=font_title, fill=0) - y += line_heights[idx] + 2 - - # User (centered below text) - user_label = f"Von: {task.user}" - try: - w = font_label_f.getlength(user_label) - user_h = get_text_height(font_label_f, user_label) - except Exception: - w = len(user_label) * 10 - user_h = 18 - draw.text(((CARD_WIDTH_PX-w)//2, y+8), user_label, font=font_label_f, fill=0) - y += user_h + 12 - - # Priority (centered below user) - prio_label = f"Priorität: {task.priority}" - try: - w = font_label_f.getlength(prio_label) - except Exception: - w = len(prio_label) * 10 - draw.text(((CARD_WIDTH_PX-w)//2, y), prio_label, font=font_label_f, fill=0) - - # Icon in lower right corner - icon = get_icon_for_task(task.content) - if font_icon: - icon_bbox = font_icon.getbbox(icon) - icon_w = icon_bbox[2] - icon_bbox[0] - icon_h = icon_bbox[3] - icon_bbox[1] - icon_x = CARD_WIDTH_PX - icon_w - 16 - icon_y = CARD_HEIGHT_PX - icon_h - 12 - draw.text((icon_x, icon_y), icon, font=font_icon, fill=0) - else: - font_error_msgs.append("[!] Icon font missing") - - # If any font errors, show a warning on the card - if font_error_msgs: - draw.text((10, CARD_HEIGHT_PX-24), ", ".join(font_error_msgs), font=ImageFont.load_default(), fill=128) - - img_path = OUT_DIR / f"task_{task.id}.png" - img.save(img_path) - preview_img = str(img_path) - msg = "success" - task.status = "printed" + # Prepare content for line wrapping + import textwrap + y = 24 + max_text_width = CARD_WIDTH_PX - 40 + content_lines = [] + if font_title: + wrapper = textwrap.TextWrapper(width=24) + lines = wrapper.wrap(task.content) + for line in lines: + # Check pixel width, wrap further if needed + while font_title.getlength(line) > max_text_width: + # Reduce by one word at a time + split = line.rsplit(' ', 1) + if len(split) == 2: + content_lines.append(split[0]) + line = split[1] + else: + # Single long word + content_lines.append(line[:20]) + line = line[20:] + content_lines.append(line) else: - printer = escpos.printer.Serial(devfile="/dev/ttyUSB0", baudrate=19200, timeout=1) + content_lines = textwrap.wrap(task.content, width=24) + + # Center lines vertically + + # Calculate text heights using getbbox + def get_text_height(font, text): + try: + bbox = font.getbbox(text) + return bbox[3] - bbox[1] + except Exception: + return 24 + + line_heights = [get_text_height(font_title, line) for line in content_lines] + total_text_height = sum(line_heights) + (len(content_lines)-1)*2 + y = (CARD_HEIGHT_PX - total_text_height) // 2 - 10 + for idx, line in enumerate(content_lines): + try: + w = font_title.getlength(line) + except Exception: + w = len(line) * 18 + draw.text(((CARD_WIDTH_PX-w)//2, y), line, font=font_title, fill=0) + y += line_heights[idx] + 2 + + # User (centered below text) + user_label = f"Von: {task.user}" + try: + w = font_label_f.getlength(user_label) + user_h = get_text_height(font_label_f, user_label) + except Exception: + w = len(user_label) * 10 + user_h = 18 + draw.text(((CARD_WIDTH_PX-w)//2, y+8), user_label, font=font_label_f, fill=0) + y += user_h + 12 + + # Priority (centered below user) + prio_label = f"Priorität: {task.priority}" + try: + w = font_label_f.getlength(prio_label) + except Exception: + w = len(prio_label) * 10 + draw.text(((CARD_WIDTH_PX-w)//2, y), prio_label, font=font_label_f, fill=0) + + # Icon in lower right corner + icon = get_icon_for_task(task.content) + if font_icon: + icon_bbox = font_icon.getbbox(icon) + icon_w = icon_bbox[2] - icon_bbox[0] + icon_h = icon_bbox[3] - icon_bbox[1] + icon_x = CARD_WIDTH_PX - icon_w - 16 + icon_y = CARD_HEIGHT_PX - icon_h - 12 + draw.text((icon_x, icon_y), icon, font=font_icon, fill=0) + else: + font_error_msgs.append("[!] Icon font missing") + + # If any font errors, show a warning on the card + if font_error_msgs: + draw.text((10, CARD_HEIGHT_PX-24), ", ".join(font_error_msgs), font=ImageFont.load_default(), fill=128) + + # Save preview image + img_path = OUT_DIR / f"task_{task.id}.png" + img.save(img_path) + preview_img = str(img_path) + + # Print to physical printer if enabled + if ENABLE_PHYSICAL_PRINTER: + printer = escpos.printer.Serial(devfile="/dev/serial0", baudrate=19200, timeout=1) printer.text(f"Task: {task.content}\nVon: {task.user}\nPriorität: {task.priority}\n") printer.cut() - task.status = "printed" - msg = "success" + + task.status = "printed" + msg = "success" except Exception as e: print(f"Printer error: {e}") msg = f"error:{e}"