网友回复
可以通过http的方式对外提供本地的打印服务,比如局域网中,通过python暴露一个网页,打开网页上传文档,点击打印,python调用win32print或pycups(linux)实现文档打印。
由于Windows自带的打印功能对PDF、Word等复杂文档支持不佳。最稳妥的方法是使用一个支持命令行打印的程序。SumatraPDF 是一个极佳的选择,它轻量、免费、启动快,且有强大的命令行接口。
下载并安装 SumatraPDF: https://www.sumatrapdfreader.org/free-pdf-reader
记下它的安装路径,例如 C:\Program Files\SumatraPDF\SumatraPDF.exe。我们将在代码中使用这个路径。
python后端代码
# app.py import os import sqlite3 import random import string import threading import time import subprocess from flask import Flask, request, jsonify, render_template from werkzeug.utils import secure_filename # --- 配置 --- # 警告:在生产环境中,请使用更安全的密钥 APP_SECRET_KEY = 'your-super-secret-key' UPLOAD_FOLDER = 'uploads' ALLOWED_EXTENSIONS = {'txt', 'pdf', 'png', 'jpg', 'jpeg', 'docx', 'xlsx'} DATABASE = 'printer.db' # --- 关键:配置你的打印机和打印程序 --- # 1. 在Windows "打印机和扫描仪" 设置中找到你的打印机确切名称 PRINTER_NAME = "Microsoft Print to PDF" # <-- 替换成你的真实打印机名称, 如 "HP LaserJet Pro M102w" # 2. (推荐) SumatraPDF 的路径,用于打印PDF等复杂文件 SUMATRA_PDF_PATH = "C:\\Program Files\\SumatraPDF\\SumatraPDF.exe" # <-- 替换成你的SumatraPDF.exe路径 # --- Flask应用初始化 --- app = Flask(__name__, template_folder='.') app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER app.config['SECRET_KEY'] = APP_SECRET_KEY # --- 数据库辅助函数 --- def get_db(): db = sqlite3.connect(DATABASE) db.row_factory = sqlite3.Row return db def init_db(): """初始化数据库,创建表结构""" if os.path.exists(DATABASE): return print("Creating database...") db = get_db() with db: db.execute(''' CREATE TABLE print_jobs ( id INTEGER PRIMARY KEY AUTOINCREMENT, original_filename TEXT NOT NULL, stored_filepath TEXT NOT NULL, pickup_code TEXT UNIQUE NOT NULL, status TEXT NOT NULL, -- 'pending', 'printing', 'printed', 'completed', 'error' created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ''') print("Database created.") def allowed_file(filename): return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS def generate_pickup_code(length=6): """生成一个唯一的、纯数字的提取码""" while True: code = ''.join(random.choices(string.digits, k=length)) db = get_db() exists = db.execute('SELECT id FROM print_jobs WHERE pickup_code = ?', (code,)).fetchone() db.close() if not exists: return code def print_document_task(job_id, filepath, printer_name): """在后台线程中执行的打印任务""" db = get_db() try: print(f"Job {job_id}: Starting to print file {filepath} on printer {printer_name}") db.execute('UPDATE print_jobs SET status = ? WHERE id = ?', ('printing', job_id)) db.commit() # 使用 SumatraPDF 命令行进行打印 (推荐,支持多种格式) # -print-to "printer_name": 指定打印机 # -silent: 静默模式,不显示任何UI # -exit-on-print: 打印后自动退出 if os.path.exists(SUMATRA_PDF_PATH): command = [ SUMATRA_PDF_PATH, '-print-to', printer_name, '-silent', '-exit-on-print', filepath ] subprocess.run(command, check=True, timeout=120) # 120秒超时 else: # 备用方案: 使用Windows shell命令 (仅对某些文件类型有效) # import win32api # win32api.ShellExecute(0, "print", filepath, f'"{printer_name}"', ".", 0) # 这行代码更简单,但兼容性较差,推荐使用SumatraPDF raise FileNotFoundError(f"SumatraPDF not found at {SUMATRA_PDF_PATH}. Printing failed.") time.sleep(5) # 留出一点时间让文件进入打印机队列 print(f"Job {job_id}: Print command sent successfully.") db.execute('UPDATE print_jobs SET status = ? WHERE id = ?', ('printed', job_id)) db.commit() except Exception as e: print(f"Error printing job {job_id}: {e}") db.execute('UPDATE print_jobs SET status = ? WHERE id = ?', ('error', job_id)) db.commit() finally: db.close() # 打印完成后,无论成功与否,都删除临时文件 try: os.remove(filepath) print(f"Job {job_id}: Temporary file {filepath} deleted.") except OSError as e: print(f"Error deleting file {filepath}: {e}") # --- API 路由 --- @app.route('/') def index(): """主页,渲染HTML""" return render_template('index.html') @app.route('/api/upload', methods=['POST']) def upload_file(): """处理文件上传和创建打印任务""" if 'file' not in request.files: return jsonify({'error': 'No file part'}), 400 file = request.files['file'] if file.filename == '': return jsonify({'error': 'No selected file'}), 400 if file and allowed_file(file.filename): filename = secure_filename(file.filename) # 使用时间戳确保文件名唯一,避免覆盖 unique_filename = f"{int(time.time())}_{filename}" filepath = os.path.join(app.config['UPLOAD_FOLDER'], unique_filename) file.save(filepath) pickup_code = generate_pickup_code() db = get_db() cursor = db.cursor() cursor.execute( 'INSERT INTO print_jobs (original_filename, stored_filepath, pickup_code, status) VALUES (?, ?, ?, ?)', (filename, filepath, pickup_code, 'pending') ) job_id = cursor.lastrowid db.commit() db.close() # 创建并启动一个新线程来处理打印,防止阻塞Web请求 print_thread = threading.Thread(target=print_document_task, args=(job_id, filepath, PRINTER_NAME)) print_thread.start() return jsonify({'message': 'File uploaded successfully', 'pickup_code': pickup_code}) return jsonify({'error': 'File type not allowed'}), 400 @app.route('/api/pickup', methods=['POST']) def pickup_file(): """核销提取码""" data = request.get_json() code = data.get('code') if not code: return jsonify({'error': 'Pickup code is required'}), 400 db = get_db() job = db.execute('SELECT id, status, original_filename FROM print_jobs WHERE pickup_code = ?', (code,)).fetchone() if not job: db.close() return jsonify({'error': 'Invalid pickup code'}), 404 if job['status'] == 'printed': db.execute('UPDATE print_jobs SET status = ? WHERE id = ?', ('completed', job['id'])) db.commit() db.close() return jsonify({'success': True, 'message': f"Verification successful for file: {job['original_filename']}"}) elif job['status'] in ['pending', 'printing']: db.close() return jsonify({'error': 'Document is still printing. Please wait.'}), 425 # Too Early elif job['status'] == 'completed': db.close() return jsonify({'error': 'This code has already been used.'}), 410 # Gone else: # error db.close() return jsonify({'error': 'An error occurred while printing this document.'}), 500 # --- 主程序入口 --- if __name__ == '__main__': # 确保上传目录存在 if not os.path.exists(UPLOAD_FOLDER): os.makedirs(UPLOAD_FOLDER) # 初始化数据库 init_db() # 启动Flask应用 # host='0.0.0.0' 使其在局域网内可访问 app.run(host='0.0.0.0', port=5000, debug=True)前端h5
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>自助共享打印</title> <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script> <style> :root { --bg-color: #f0f2f5; --card-bg: #ffffff; --text-color: #333; --primary-color: #007bff; --primary-hover: #0056b3; --border-color: #e0e0e0; --success-color: #28a745; --error-color: #dc3545; --gray-text: #6c757d; } /* 暗黑模式 */ @media (prefers-color-scheme: dark) { :root { --bg-color: #1a1a1a; --card-bg: #2c2c2c; --text-color: #e0e0e0; --primary-color: #3b82f6; --primary-hover: #2563eb; --border-color: #444; } ...
点击查看剩余70%