网友回复
可以通过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%


