├── start_printer_script.bat ├── setup_script.bat ├── README.md ├── example.js ├── printer_windows.py └── printer_mac.py /start_printer_script.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | cd C:\ 3 | python printer.py -------------------------------------------------------------------------------- /setup_script.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | REM 3 | python -m ensurepip --upgrade 4 | 5 | REM 6 | python -m pip install flask flask-cors pillow pywin32 7 | 8 | REM 9 | echo @echo off > C:\start_printer_script.bat 10 | echo cd C:\ >> C:\start_printer_script.bat 11 | echo python printer.py > C:\output.log 2>&1 >> C:\start_printer_script.bat 12 | 13 | REM دانانی فایلەکە لە شێڵ ستارت بۆ ڕەنبوونی لەکاتی هەڵکردنی لاپتۆپەکە یان کۆمپیوتەرەکە 14 | copy "C:\start_printer_script.bat" "%APPDATA%\Microsoft\Windows\Start Menu\Programs\Startup\" 15 | 16 | echo Setup complete. The script will run automatically at startup. 17 | echo The script has been added to: %APPDATA%\Microsoft\Windows\Start Menu\Programs\Startup\ 18 | pause -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Thermal Printer System / سیستەمی پرێنتەر 2 | 3 | ## English 4 | 5 | ### Description 6 | A thermal printer system that supports both Windows and Mac operating systems. The system can print invoices and stickers using specific thermal printers. 7 | 8 | ### Features 9 | - Multi-platform support (Windows & Mac) 10 | - Invoice printing with: 11 | - Logo 12 | - Order details 13 | - Item list with quantities and prices 14 | - Total calculations 15 | - Date and time 16 | - Sticker printing with: 17 | - Custom text 18 | - Logo 19 | - Multiple lines support 20 | 21 | 22 | ## کوردی 23 | ### پێناسە 24 | سیستەمێکی پڕێنتەرەکە پشتگیری هەردوو سیستەمی ویندۆز و ماک دەکات. سیستەمەکە دەتوانێت پسوڵە و ستیکەر چاپ بکات بە بەکارهێنانی پڕێنتەری حەراری. 25 | 26 | 27 | 28 | ### Setup | دامەزراندن 29 | 1. Install Python dependencies: 30 | python -m pip install flask flask-cors pillow 31 | 32 | 2. For Windows: Install pywin32 33 | 3. For Mac: Install pycups 34 | 4. Run the appropriate printer script: 35 | - Windows: printer_windows.py 36 | - Mac: printer_mac.py 37 | 38 | #### Windows Users (Recommended) 39 | 1. Download and unzip the package 40 | 2. Double-click `setup_windows.bat` to: 41 | - Install Python dependencies 42 | - Configure printer settings 43 | - Start the print server 44 | 45 | -------------------------------------------------------------------------------- /example.js: -------------------------------------------------------------------------------- 1 | let printInvoice = () => { 2 | const now = new Date(); 3 | 4 | const invoiceItems = { 5 | address: 'for test', 6 | date: now.toLocaleString(), 7 | order_type: 'dine-in', 8 | items: [ 9 | { 10 | name: "Burger", 11 | quantity: 2, 12 | price: "1000 IQD" 13 | }, 14 | { 15 | name: "Fries", 16 | quantity: 1, 17 | price: "500 IQD" 18 | }, 19 | { 20 | name: "Soda", 21 | quantity: 2, 22 | price: "500 IQD" 23 | } 24 | ], 25 | subtotal: '2000 IQD', 26 | discount: '250 IQD', 27 | coupon: '0 IQD', 28 | total: '1750 IQD', 29 | }; 30 | 31 | // ناردنی داتاکان بۆ لۆکاڵ هۆستی کۆمپیوتەرەکە بۆ پرێنت کردنی داتاکان لە ڕێگای فلاسکەوە 32 | fetch('http://127.0.0.1:5000/print', { 33 | method: 'POST', 34 | headers: { 35 | 'Content-Type': 'application/json', 36 | }, 37 | body: JSON.stringify({ 38 | type: "invoice", // بۆ جیاکردنەوەی پرێنتەرەکە کە دەتوانرێت بۆ چەند پرێنتەرێک دابنرێ 39 | content: invoiceItems, 40 | }), 41 | }) 42 | .then(response => response.json()) 43 | .then(data => { 44 | if (data.error) { 45 | console.error('Printing error:', data.error); 46 | } else { 47 | console.log('Print job sent successfully'); 48 | } 49 | }) 50 | .catch(error => { 51 | console.error('Error:', error); 52 | }); 53 | }; -------------------------------------------------------------------------------- /printer_windows.py: -------------------------------------------------------------------------------- 1 | import win32print 2 | import win32ui 3 | import win32con 4 | from PIL import Image, ImageWin, ImageDraw, ImageFont 5 | from flask import Flask, request, jsonify 6 | from flask_cors import CORS 7 | from datetime import datetime 8 | import os 9 | 10 | app = Flask(__name__) 11 | CORS(app, resources={r"/*": {"origins": "*"}}) 12 | 13 | # Printer Constants 14 | PRINTERS = { 15 | "invoice": "XP-80", 16 | "sticker": "Xprinter XP-365B", 17 | } 18 | 19 | # Printer paper settings 20 | WIDTH_MM = 41 21 | HEIGHT_MM = 60 22 | DPI = 203 23 | 24 | WIDTH_PX = int(WIDTH_MM / 25.4 * DPI) 25 | HEIGHT_PX = int(HEIGHT_MM / 25.4 * DPI) 26 | 27 | FONT_SIZE = 25 28 | 29 | 30 | 31 | def create_label_image(content): 32 | IMAGE_SCALE = 0.55 33 | IMG_WIDTH_PX = int(WIDTH_PX * IMAGE_SCALE) 34 | IMG_HEIGHT_PX = int(HEIGHT_PX * IMAGE_SCALE) 35 | 36 | SCALE_FACTOR = 2 37 | high_res_canvas = Image.new("RGB", 38 | (WIDTH_PX * SCALE_FACTOR, HEIGHT_PX * SCALE_FACTOR), 39 | (255, 255, 255)) 40 | 41 | try: 42 | font_path = os.path.join(os.path.dirname(__file__), "fonts", "arialbd.ttf") 43 | font = ImageFont.truetype(font_path, FONT_SIZE * SCALE_FACTOR) 44 | except: 45 | try: 46 | font = ImageFont.truetype("arialbd.ttf", FONT_SIZE * SCALE_FACTOR) 47 | except: 48 | try: 49 | font = ImageFont.truetype("arial.ttf", FONT_SIZE * SCALE_FACTOR) 50 | except: 51 | font = ImageFont.load_default() 52 | 53 | logo_path = "logo_79.bmp" 54 | try: 55 | img = Image.open(logo_path) 56 | 57 | if img.mode in ("RGBA", "LA"): 58 | background = Image.new("RGB", img.size, (255, 255, 255)) 59 | background.paste(img, mask=img.split()[3]) 60 | img = background 61 | 62 | img.thumbnail((IMG_WIDTH_PX * SCALE_FACTOR, IMG_HEIGHT_PX * SCALE_FACTOR), 63 | Image.Resampling.LANCZOS) 64 | 65 | img_x = ((WIDTH_PX * SCALE_FACTOR) - img.width) // 2 66 | img_y = 30 67 | high_res_canvas.paste(img, (img_x, img_y)) 68 | except Exception as e: 69 | print(f"Error loading logo: {e}") 70 | 71 | draw = ImageDraw.Draw(high_res_canvas) 72 | 73 | y_offset = 300 74 | 75 | text_groups = [ 76 | [ 77 | ("first_name", content.get("name1", "")), 78 | ("second_name", content.get("name2", "")), 79 | ], 80 | [ 81 | ("food_name", content.get("order_name", "")), 82 | ("category", content.get("category_name", "")), 83 | ], 84 | [ 85 | ("datetime", datetime.now().strftime("%Y-%m-%d %H:%M")), 86 | ] 87 | ] 88 | 89 | for group_idx, group in enumerate(text_groups): 90 | for item_idx, (_, text) in enumerate(group): 91 | if text: 92 | text_width = draw.textlength(str(text), font=font) 93 | text_x = ((WIDTH_PX * SCALE_FACTOR) - text_width) // 2 94 | draw.text((text_x, y_offset), str(text), fill="black", font=font) 95 | 96 | if item_idx < len(group) - 1: 97 | y_offset += FONT_SIZE * SCALE_FACTOR + (5 * SCALE_FACTOR) 98 | 99 | if group_idx < len(text_groups) - 1: 100 | y_offset += FONT_SIZE * SCALE_FACTOR + (40 * SCALE_FACTOR) 101 | 102 | return high_res_canvas.resize((WIDTH_PX, HEIGHT_PX), Image.Resampling.LANCZOS) 103 | def print_sticker(content, printer_name="Xprinter XP-365B"): 104 | try: 105 | img_with_text = create_label_image(content) 106 | 107 | hprinter = win32print.OpenPrinter(printer_name) 108 | 109 | printer_dc = win32ui.CreateDC() 110 | printer_dc.CreatePrinterDC(printer_name) 111 | 112 | printer_dc.StartDoc("Sticker Print") 113 | printer_dc.StartPage() 114 | 115 | dib = ImageWin.Dib(img_with_text) 116 | dib.draw(printer_dc.GetHandleOutput(), (0, 0, WIDTH_PX, HEIGHT_PX)) 117 | 118 | printer_dc.EndPage() 119 | printer_dc.EndDoc() 120 | printer_dc.DeleteDC() 121 | 122 | win32print.ClosePrinter(hprinter) 123 | return True 124 | 125 | except Exception as e: 126 | print(f"Error printing sticker: {e}") 127 | return False 128 | 129 | def format_receipt_line(left, right, width=48): 130 | return f"{left}{right:>{width-len(left)}}" 131 | 132 | def print_receipt(items, printer_name="XP-80", content=None): 133 | try: 134 | data = [] 135 | data.append(b'\x1B@') 136 | data.append(b'\x1Ba1') 137 | data.append(b'\x1BE1') 138 | data.append(b'\x1B!\x30') 139 | data.append(b'SHAR COFFEE\n') 140 | data.append(b'\x1B!\x00') 141 | data.append(b'Quality Coffee Shop\n\n') 142 | data.append(b'\x1BE0') 143 | 144 | data.append(b'\x1Ba0') 145 | date_str = content.get('date', datetime.now().strftime('%Y-%m-%d %H:%M')) 146 | data.append(format_receipt_line("Date:", date_str).encode() + b'\n') 147 | data.append(format_receipt_line("Order:", content.get('order_type', '')).encode() + b'\n') 148 | 149 | data.append(b'=' * 48 + b'\n') 150 | header = f"{'Item':<24}{'Qty':^8}{'Price':>12}" 151 | data.append(header.encode() + b'\n') 152 | data.append(b'-' * 48 + b'\n') 153 | 154 | for item in items: 155 | name = item.get('name', '')[:24] 156 | qty = str(item.get('quantity', 0)) 157 | price = float(item.get('price', '0').replace(',', '').replace(' IQD', '')) 158 | amount = price 159 | line = f"{name:<24}{qty:^8}{amount:>12,.0f}" 160 | data.append(line.encode() + b'\n') 161 | 162 | data.append(b'-' * 48 + b'\n') 163 | data.append(format_receipt_line("Subtotal:", content.get('subtotal', '0 IQD')).encode() + b'\n') 164 | data.append(format_receipt_line("Discount:", content.get('discount', '0 IQD')).encode() + b'\n') 165 | data.append(format_receipt_line("Coupon:", content.get('coupon', '0 IQD')).encode() + b'\n') 166 | data.append(b'=' * 48 + b'\n') 167 | 168 | data.append(b'\x1Ba1') # Center align 169 | data.append(b'\x1B!\x30') # Double height and width 170 | data.append(f"TOTAL: {content.get('total', '0 IQD')}\n".encode()) 171 | data.append(b'\x1B!\x00') 172 | data.append(b'-' * 24 + b' * ' + b'-' * 24 + b'\n') 173 | 174 | data.append(b'Thank you for choosing\n') 175 | data.append(b'\x1BE1') # Bold on 176 | data.append(b'SHAR COFFEE\n') 177 | data.append(b'\x1BE0') # Bold off 178 | data.append(b'Your coffee made with love!\n\n') 179 | data.append(b'Follow us on Instagram\n@shar.coffee\n') 180 | data.append(datetime.now().strftime('%Y-%m-%d %H:%M').encode() + b'\n') 181 | data.append(b'-' * 24 + b' \x03 ' + b'-' * 24 + b'\n') 182 | data.append(b'developed by bernamechi.com\n') 183 | data.append(b'\x1DVA0') # Cut paper 184 | 185 | printer = win32print.OpenPrinter(printer_name) 186 | try: 187 | job = win32print.StartDocPrinter(printer, 1, ("Receipt", None, "RAW")) 188 | win32print.StartPagePrinter(printer) 189 | win32print.WritePrinter(printer, b''.join(data)) 190 | win32print.EndPagePrinter(printer) 191 | win32print.EndDocPrinter(printer) 192 | finally: 193 | win32print.ClosePrinter(printer) 194 | return True 195 | except Exception as e: 196 | print(f"Error printing receipt: {e}") 197 | return False 198 | 199 | @app.route('/print', methods=['POST']) 200 | def print_handler(): 201 | try: 202 | data = request.json 203 | print_type = data.get('type') 204 | content = data.get('content', {}) 205 | items = content.get('items', []) 206 | printer_name = PRINTERS.get(print_type) 207 | 208 | if not printer_name: 209 | return jsonify({"error": "Invalid print type"}), 400 210 | 211 | if print_type == "sticker": 212 | success = print_sticker(content, printer_name) 213 | elif print_type == "invoice": 214 | success = print_receipt(items, printer_name, content) 215 | else: 216 | return jsonify({"error": "Unsupported print type"}), 400 217 | 218 | if success: 219 | return jsonify({"message": "Printed successfully!"}) 220 | else: 221 | return jsonify({"error": "Failed to print"}), 500 222 | except Exception as e: 223 | return jsonify({"error": f"An error occurred: {e}"}), 500 224 | 225 | if __name__ == '__main__': 226 | app.run(debug=True) -------------------------------------------------------------------------------- /printer_mac.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import cups 4 | from flask import Flask, request, jsonify 5 | from flask_cors import CORS 6 | from PIL import Image, ImageDraw, ImageFont 7 | from datetime import datetime 8 | 9 | app = Flask(__name__) 10 | CORS(app, resources={r"/*": {"origins": "*"}}) 11 | 12 | # Constants 13 | PRINTERS = { 14 | "invoice": "Printer_POS_80", 15 | "sticker": "Xprinter_XP_365B", 16 | } 17 | 18 | def print_receipt(items, printer_name="Printer_POS_80", image_path=None, content=None): 19 | try: 20 | conn = cups.Connection() 21 | 22 | printers = conn.getPrinters() 23 | if printer_name not in printers: 24 | print(f"Available printers: {list(printers.keys())}") 25 | raise Exception(f"Printer {printer_name} not found") 26 | 27 | img_width = 800 28 | img_height = 1600 29 | img = Image.new('RGB', (img_width, img_height), 'white') 30 | draw = ImageDraw.Draw(img) 31 | 32 | y = 10 33 | if image_path and os.path.exists(image_path): 34 | logo = Image.open(image_path).convert("RGBA") 35 | max_logo_width = 300 36 | ratio = max_logo_width / logo.width 37 | new_size = (max_logo_width, int(logo.height * ratio)) 38 | logo = logo.resize(new_size, Image.LANCZOS) 39 | logo_width, logo_height = logo.size 40 | img.paste(logo, (int((img_width - logo_width) / 2), y), logo) 41 | y += logo_height + 40 42 | 43 | # Header 44 | font_name = "/Library/Fonts/Arial.ttf" 45 | font_size = 30 46 | font = ImageFont.truetype(font_name, font_size) 47 | draw.text((0, y), f"Date: {content.get('date', datetime.now().strftime('%Y-%m-%d %H:%M'))}", font=font, fill='black') 48 | y += 30 49 | draw.text((0, y), f"Order Type: {content.get('order_type', '')}", font=font, fill='black') 50 | 51 | font_size = 20 52 | font = ImageFont.truetype(font_name, font_size) 53 | y += 25 54 | draw.text((0, y), "--------------------------------------------------------------------------------------------------", font=font, fill='black') 55 | 56 | font_size = 30 57 | font = ImageFont.truetype(font_name, font_size) 58 | y += 25 59 | draw.text((0, y), "Item", font=font, fill='black') 60 | draw.text((250, y), "Qty", font=font, fill='black') 61 | draw.text((470, y), "Total", font=font, fill='black') 62 | y += 35 63 | 64 | font_size = 20 65 | font = ImageFont.truetype(font_name, font_size) 66 | draw.text((0, y), "--------------------------------------------------------------------------------------------------", font=font, fill='black') 67 | 68 | font_size = 30 69 | font = ImageFont.truetype(font_name, font_size) 70 | y += 25 71 | total = 0.0 72 | for item in items: 73 | try: 74 | name = item.get('name', '') 75 | price = float(item.get('price', '0').replace(',', '').replace(' IQD', '')) 76 | qty = int(item.get('quantity', 0)) 77 | item_total = price * qty 78 | draw.text((0, y), name[:15], font=font, fill='black') 79 | draw.text((250, y), str(qty), font=font, fill='black') 80 | draw.text((470, y), "{:>10}".format(f"{item_total:.2f} IQD"), font=font, fill='black') 81 | y += 35 82 | total += item_total 83 | except ValueError as e: 84 | print(f"Error processing item {item}: {e}") 85 | continue 86 | 87 | font_size = 20 88 | font = ImageFont.truetype(font_name, font_size) 89 | draw.text((0, y), "--------------------------------------------------------------------------------------------------", font=font, fill='black') 90 | 91 | font_size = 30 92 | font = ImageFont.truetype(font_name, font_size) 93 | y += 25 94 | draw.text((0, y), f"Subtotal: {content.get('subtotal', '0 IQD')}", font=font, fill='black') 95 | y += 30 96 | draw.text((0, y), f"Discount: {content.get('discount', '0 IQD')}", font=font, fill='black') 97 | y += 30 98 | draw.text((0, y), f"Coupon: {content.get('coupon', '0 IQD')}", font=font, fill='black') 99 | 100 | font_size = 50 101 | font = ImageFont.truetype(font_name, font_size) 102 | y += 40 103 | draw.text((0, y), "Total:", font=font, fill='black') 104 | draw.text((300, y), f"{content.get('total', '0 IQD')}", font=font, fill='black') 105 | 106 | font_size = 20 107 | font = ImageFont.truetype(font_name, font_size) 108 | y += 55 109 | draw.text((0, y), "--------------------------------------------------------------------------------------------------", font=font, fill='black') 110 | 111 | font_size = 30 112 | font = ImageFont.truetype(font_name, font_size) 113 | y += 25 114 | draw.text((0, y), "Thank you for your purchase!", font=font, fill='black') 115 | y += 35 116 | draw.text((0, y), "Please come again!", font=font, fill='black') 117 | 118 | font_size = 35 119 | font = ImageFont.truetype(font_name, font_size) 120 | y += 55 121 | draw.text((0, y), "--------------------------------------------------------------------------------------------------", font=font, fill='black') 122 | y += 25 123 | draw.text((0, y), "DEVELOPED BY bernamechi.com", font=font, fill='black', align='center') 124 | 125 | temp_path = "/tmp/combined_print.png" 126 | img.save(temp_path, dpi=(300, 300)) 127 | 128 | conn.printFile(printer_name, temp_path, "Print Job", {}) 129 | 130 | os.remove(temp_path) 131 | 132 | return True 133 | 134 | except Exception as e: 135 | print(f"Error printing: {str(e)}") 136 | return False 137 | 138 | def print_sticker(content, printer_name="Xprinter_XP_365B"): 139 | try: 140 | conn = cups.Connection() 141 | 142 | LABEL_WIDTH_MM = 38 143 | LABEL_HEIGHT_MM = 25 144 | DPI = 203 145 | 146 | img_width = int(LABEL_WIDTH_MM * DPI / 25.4) 147 | img_height = int(LABEL_HEIGHT_MM * DPI / 25.4) 148 | 149 | img = Image.new('RGB', (img_width, img_height), 'white') 150 | draw = ImageDraw.Draw(img) 151 | 152 | 153 | y = 2 154 | image_path = content.get('image_path', 'logo.png') 155 | if image_path and os.path.exists(image_path): 156 | logo = Image.open(image_path).convert("RGBA") 157 | max_logo_width = 100 158 | ratio = max_logo_width / logo.width 159 | new_size = (max_logo_width, int(logo.height * ratio)) 160 | logo = logo.resize(new_size, Image.LANCZOS) 161 | logo_width, logo_height = logo.size 162 | img.paste(logo, (int((img_width - logo_width) / 2), y), logo) 163 | y += logo_height + 3 164 | 165 | 166 | font_sizes = { 167 | 'order': 25, 168 | 'category': 24, 169 | 'name': 22, 170 | } 171 | 172 | font_name = "/Library/Fonts/Arial.ttf" 173 | fonts = {k: ImageFont.truetype(font_name, size) for k, size in font_sizes.items()} 174 | 175 | x_margin = 5 176 | spacing = 3 177 | 178 | def draw_left_aligned_text(draw, text, y, font): 179 | if not text: 180 | return y 181 | text_bbox = draw.textbbox((x_margin, y), text, font=font) 182 | text_width = text_bbox[2] - text_bbox[0] 183 | 184 | while text and text_width > (img_width - 2 * x_margin): 185 | text = text[:-1] 186 | text_bbox = draw.textbbox((x_margin, y), text, font=font) 187 | text_width = text_bbox[2] - text_bbox[0] 188 | 189 | draw.text((x_margin, y), text, font=font, fill='black') 190 | return y + (text_bbox[3] - text_bbox[1]) + spacing 191 | 192 | 193 | y = draw_left_aligned_text(draw, content.get('order_name', ''), y, fonts['order']) 194 | y += 6 195 | y = draw_left_aligned_text(draw, content.get('category_name', ''), y, fonts['category']) 196 | y += 6 197 | y = draw_left_aligned_text(draw, content.get('name1', ''), y, fonts['name']) 198 | y = draw_left_aligned_text(draw, content.get('name2', ''), y, fonts['name']) 199 | 200 | temp_path = "/tmp/sticker_print.png" 201 | img.save(temp_path, dpi=(DPI, DPI)) 202 | 203 | 204 | options = { 205 | 'media': 'Custom.38x25mm', 206 | 'MediaType': 'Labels', 207 | 'PageSize': 'Custom.38x25mm', 208 | 'orientation-requested': '3', 209 | 'fit-to-page': 'True', 210 | 'scaling': '100', 211 | 'CutOptions': '0', 212 | 'Resolution': f'{DPI}dpi', 213 | 'PrintSpeed': '2', 214 | 'PrintDensity': '7', 215 | 'PrintQuality': 'Normal', 216 | 'ColorModel': 'Gray', 217 | 'blackness': '100', 218 | 'label-mode': '1', 219 | 'page-top': '0', 220 | 'page-bottom': '0', 221 | 'page-left': '0', 222 | 'page-right': '0' 223 | } 224 | 225 | conn.printFile(printer_name, temp_path, "Sticker Print Job", options) 226 | os.remove(temp_path) 227 | return True 228 | 229 | except Exception as e: 230 | print(f"Error printing: {str(e)}") 231 | return False 232 | 233 | 234 | 235 | @app.route('/print', methods=['POST']) 236 | def print_handler(): 237 | try: 238 | data = request.json 239 | print_type = data.get('type') 240 | content = data.get('content', {}) 241 | items = content.get('items', []) 242 | printer_name = PRINTERS.get(print_type) 243 | image_path = content.get('image_path', 'logo.png') 244 | 245 | if not printer_name: 246 | return jsonify({"error": "Invalid print type"}), 400 247 | 248 | if print_type == "sticker": 249 | success = print_sticker(content, printer_name) 250 | else: 251 | success = print_receipt(items, printer_name, image_path, content) 252 | 253 | if success: 254 | return jsonify({"message": "Printed successfully!"}) 255 | else: 256 | return jsonify({"error": "Failed to print"}), 500 257 | except Exception as e: 258 | return jsonify({"error": f"An error occurred: {str(e)}"}), 500 259 | 260 | if __name__ == '__main__': 261 | app.run(debug=True) --------------------------------------------------------------------------------