Python+Flask開發(fā)局域網(wǎng)智能打印服務(wù)系統(tǒng)
在當今數(shù)字化辦公環(huán)境中,打印服務(wù)仍然是企業(yè)日常運營中不可或缺的一環(huán)。傳統(tǒng)的打印方式存在諸多痛點:驅(qū)動程序復雜、多設(shè)備共享困難、虛擬打印機干擾、缺乏集中管理等。本文介紹一款基于Python Flask開發(fā)的局域網(wǎng)智能打印服務(wù)系統(tǒng),它能夠?qū)⑷魏蜽indows電腦變身成為企業(yè)級打印服務(wù)器,支持PDF、Office文檔、圖片等多種格式的無線打印,具備智能過濾、實時監(jiān)控、系統(tǒng)托盤等高級功能。
系統(tǒng)架構(gòu)圖:

核心功能特色
智能打印管理
- 多格式支持:PDF、Word、Excel、PPT、圖片、文本等常見格式
- 智能過濾:自動識別并過濾虛擬打印機,避免誤操作
- 高級設(shè)置:支持雙面打印、色彩模式、紙張大小、打印質(zhì)量等參數(shù)配置
- 批量打印:支持多文件同時上傳,自動排隊處理
網(wǎng)絡(luò)管理功能
- IP自動檢測:智能獲取本機IP地址,支持靜態(tài)IP/DHCP切換
- 跨平臺訪問:任何設(shè)備通過瀏覽器即可訪問打印服務(wù)
- 實時狀態(tài)監(jiān)控:顯示打印機狀態(tài)、網(wǎng)絡(luò)連接、打印隊列等信息
系統(tǒng)集成特性
- 系統(tǒng)托盤:后臺運行,不占用任務(wù)欄空間
- 開機自啟:注冊表級自啟動配置,無需手動操作
- 自動清理:智能清理臨時文件,防止磁盤空間占用
- 日志記錄:完整的操作日志,便于故障排查和審計
實際效果展示
界面設(shè)計亮點
系統(tǒng)采用現(xiàn)代化的深色主題設(shè)計,搭配霓虹燈效果和動態(tài)交互元素:


主要界面區(qū)域:
- 頂部導航:打印管理、系統(tǒng)狀態(tài)等功能模塊切換
- 文件上傳區(qū):支持拖拽上傳,實時顯示文件信息
- 打印機選擇:智能識別物理打印機,標注默認設(shè)備
- 參數(shù)配置:豐富的打印選項,滿足專業(yè)需求
- 狀態(tài)監(jiān)控:實時顯示系統(tǒng)運行狀態(tài)和打印隊列
打印效果對比
| 功能 | 傳統(tǒng)打印 | 本系統(tǒng)打印 |
|---|---|---|
| 文件格式支持 | 有限 | ? 多格式 |
| 虛擬打印機過濾 | 手動 | ? 自動 |
| 網(wǎng)絡(luò)共享 | 復雜配置 | ? 即開即用 |
| 移動端支持 | 需專用APP | ? 瀏覽器訪問 |
| 集中管理 | 無 | ? 完善 |
軟件部署步驟
環(huán)境要求
- 操作系統(tǒng):Windows 7/10/11
- Python版本:3.7+
- 必要組件:.NET Framework 4.5+
安裝步驟
1. 依賴安裝
# 創(chuàng)建虛擬環(huán)境 python -m venv print_server cd print_server Scripts\activate # 安裝核心依賴 pip install flask pywin32 pystray pillow waitress
2. 配置文件準備
創(chuàng)建config.ini文件:
[server] host = 0.0.0.0 port = 5000 upload_folder = C:\PrintServer\uploads [printer] virtual_filter = true auto_refresh = true [cleanup] interval = 3600 max_age = 86400
3. 啟動服務(wù)
# 直接運行 python print_server.py # 或使用WSGI服務(wù)器 set USE_WSGI=true python print_server.py
4. 訪問管理界面
在瀏覽器中輸入:http://本地IP:5000
系統(tǒng)托盤操作
系統(tǒng)啟動后會在任務(wù)欄顯示托盤圖標,右鍵菜單提供:
- 查看服務(wù)狀態(tài)
- 打開管理界面
- 切換開機自啟
- 退出程序
核心代碼解析
1. 打印機智能過濾機制
# 虛擬打印機黑名單
VIRTUAL_PRINTERS = {
'導出為WPS PDF', 'WPS PDF', 'Microsoft Print to PDF',
'Microsoft XPS Document Writer', 'Fax', '傳真', 'OneNote'
}
def is_physical_printer(printer_name):
"""智能判斷是否為物理打印機"""
if printer_name in VIRTUAL_PRINTERS:
return False
# 關(guān)鍵詞過濾算法
virtual_keywords = ['pdf', 'fax', '傳真', 'xps', 'onenote',
'virtual', '虛擬', 'send to', 'export', '導出']
printer_lower = printer_name.lower()
return not any(keyword in printer_lower for keyword in virtual_keywords)
技術(shù)亮點:結(jié)合固定黑名單和動態(tài)關(guān)鍵詞匹配,有效識別各類虛擬打印機。
2. 高級打印設(shè)置實現(xiàn)
def apply_printer_settings(printer_name, settings):
"""應(yīng)用高級打印設(shè)置到系統(tǒng)打印機"""
try:
hprinter = win32print.OpenPrinter(printer_name)
printer_info = win32print.GetPrinter(hprinter, 2)
devmode = printer_info[1]
# 設(shè)置打印方向
if settings['orientation'] == 'landscape':
devmode.Orientation = win32con.DMORIENT_LANDSCAPE
else:
devmode.Orientation = win32con.DMORIENT_PORTRAIT
# 設(shè)置色彩模式
devmode.Color = 1 if settings['color_mode'] == 'monochrome' else 2
# 設(shè)置雙面打印
if settings['duplex'] == 2:
devmode.Duplex = win32con.DMDUP_HORIZONTAL
elif settings['duplex'] == 3:
devmode.Duplex = win32con.DMDUP_VERTICAL
# 應(yīng)用設(shè)置
devmode.Fields |= (win32con.DM_ORIENTATION | win32con.DM_COLOR |
win32con.DM_DUPLEX)
win32print.SetPrinter(hprinter, 2, devmode, 0)
except Exception as e:
print(f"打印機設(shè)置應(yīng)用失敗: {e}")
finally:
win32print.ClosePrinter(hprinter)
3. 文件類型智能路由
def print_file_with_settings(filepath, printer_name, settings):
"""根據(jù)文件類型選擇最優(yōu)打印方案"""
file_ext = os.path.splitext(filepath)[1].lower()
if file_ext == '.pdf':
return print_pdf_advanced(filepath, printer_name, settings)
elif file_ext in ['.jpg', '.jpeg', '.png']:
return print_image_optimized(filepath, printer_name, settings)
elif file_ext in ['.doc', '.docx']:
return print_office_document(filepath, printer_name, settings, 'Word')
elif file_ext in ['.xls', '.xlsx']:
return print_office_document(filepath, printer_name, settings, 'Excel')
else:
return print_generic_file(filepath, printer_name, settings)
4. Web界面交互邏輯
// 動態(tài)打印機信息加載
function refreshPrinterInfo() {
const printerSelect = document.getElementById('printerSelect');
fetch('/api/printer_info?printer=' + encodeURIComponent(printerSelect.value))
.then(response => response.json())
.then(data => {
if (data.success) {
updatePrintOptions(data.capabilities);
showPrintStatus(data.capabilities.printer_status);
}
});
}
// 實時更新打印選項
function updatePrintOptions(capabilities) {
// 更新紙張選項
updatePaperOptions(capabilities.papers);
// 更新質(zhì)量選項
updateQualityOptions(capabilities.resolutions);
// 更新雙面打印選項
updateDuplexOption(capabilities.duplex_support);
}
系統(tǒng)架構(gòu)深度解析
模塊化設(shè)計思想
系統(tǒng)采用分層架構(gòu)設(shè)計,確保各模塊職責清晰:
應(yīng)用層 (Presentation)
├── Web管理界面 (Flask + Bootstrap)
└── 系統(tǒng)托盤接口 (pystray)
業(yè)務(wù)層 (Business Logic)
├── 打印任務(wù)管理
├── 文件格式處理
├── 打印機控制
└── 網(wǎng)絡(luò)配置管理
數(shù)據(jù)層 (Data Access)
├── 文件存儲管理
├── 打印日志記錄
└── 系統(tǒng)配置持久化
并發(fā)處理機制
class PrintTaskManager:
"""打印任務(wù)管理器 - 支持并發(fā)處理"""
def __init__(self):
self.task_queue = queue.Queue()
self.worker_thread = threading.Thread(target=self._process_queue)
self.worker_thread.daemon = True
self.worker_thread.start()
def add_task(self, filepath, printer, settings):
"""添加打印任務(wù)到隊列"""
task_id = str(uuid.uuid4())
task = {
'id': task_id,
'filepath': filepath,
'printer': printer,
'settings': settings,
'status': 'pending',
'timestamp': datetime.now()
}
self.task_queue.put(task)
return task_id
def _process_queue(self):
"""后臺處理打印隊列"""
while True:
try:
task = self.task_queue.get()
self._execute_print_task(task)
self.task_queue.task_done()
except Exception as e:
print(f"打印任務(wù)處理異常: {e}")
錯誤處理與日志系統(tǒng)
def robust_print_execution(filepath, printer, settings):
"""健壯的打印執(zhí)行流程,包含多重錯誤處理"""
attempts = [
lambda: print_with_primary_method(filepath, printer, settings),
lambda: print_with_fallback_method(filepath, printer, settings),
lambda: print_with_emergency_method(filepath, printer, settings)
]
for i, attempt in enumerate(attempts, 1):
try:
success, message = attempt()
if success:
log_print_success(filepath, printer, settings, f"方法{i}")
return True, message
except Exception as e:
log_print_error(filepath, printer, settings, f"方法{i}失敗: {str(e)}")
if i == len(attempts): # 最后一次嘗試
return False, f"所有打印方法均失敗: {str(e)}"
return False, "未知錯誤"
高級功能擴展
1. 移動端優(yōu)化適配
通過響應(yīng)式設(shè)計確保在手機和平板上的良好體驗:
/* 移動端適配 */
@media (max-width: 768px) {
.main-container {
margin: 10px;
border-radius: 10px;
}
.header h1 {
font-size: 1.8rem;
}
.upload-area {
padding: 20px;
}
.btn-lg {
padding: 12px 20px;
font-size: 1rem;
}
}
2. 安全增強措施
def security_enhancements():
"""安全增強功能"""
# 文件類型白名單驗證
def validate_file_type(filename):
allowed_extensions = {'pdf', 'jpg', 'jpeg', 'png', 'doc', 'docx'}
ext = filename.rsplit('.', 1)[1].lower()
return ext in allowed_extensions
# 文件大小限制 (10MB)
def validate_file_size(file_stream):
max_size = 10 * 1024 * 1024
file_stream.seek(0, 2) # 移動到文件末尾
size = file_stream.tell()
file_stream.seek(0) # 重置文件指針
return size <= max_size
# IP訪問頻率限制
def rate_limit_by_ip():
client_ip = request.remote_addr
# 實現(xiàn)基于Redis或內(nèi)存的限流邏輯
pass
3. 性能優(yōu)化策略
class PerformanceOptimizer:
"""性能優(yōu)化器"""
@staticmethod
def optimize_memory_usage():
"""內(nèi)存使用優(yōu)化"""
# 使用生成器處理大文件
def read_file_in_chunks(file_path, chunk_size=8192):
with open(file_path, 'rb') as f:
while True:
chunk = f.read(chunk_size)
if not chunk:
break
yield chunk
# 圖片壓縮處理
def compress_image(image_path, max_size=(1024, 1024)):
from PIL import Image
img = Image.open(image_path)
img.thumbnail(max_size, Image.Resampling.LANCZOS)
return img
@staticmethod
def caching_strategy():
"""緩存策略"""
cache_duration = 300 # 5分鐘
@functools.lru_cache(maxsize=128)
def get_printer_capabilities_cached(printer_name):
return get_printer_capabilities(printer_name)
源碼下載與使用
完整項目結(jié)構(gòu)
print_server/
├── src/ # 源代碼目錄
│ ├── main.py # 主程序入口
│ ├── print_engine.py # 打印引擎核心
│ ├── web_interface.py # Web界面邏輯
│ ├── system_tray.py # 系統(tǒng)托盤功能
│ └── utils/ # 工具模塊
│ ├── file_utils.py
│ ├── network_utils.py
│ └── printer_utils.py
├── static/ # 靜態(tài)資源
│ ├── css/
│ ├── js/
│ └── images/
├── templates/ # HTML模板
├── config/ # 配置文件
├── logs/ # 日志文件
├── requirements.txt # 依賴列表
└── README.md # 說明文檔
完整源碼下載
import os
from flask import Flask, request, render_template_string, send_from_directory, redirect, url_for, flash, jsonify
# 打印相關(guān)
import win32print
import win32api
import win32gui
import win32con
import subprocess
from datetime import datetime
# 托盤相關(guān)
import threading
import sys
import pystray
from PIL import Image
import socket
import winreg
import time
# Windows DeviceCapabilities 常量
DC_DUPLEX = 7
DC_COLORDEVICE = 32
DC_PAPERS = 2
DC_PAPERNAMES = 16
DC_ENUMRESOLUTIONS = 13
DC_ORIENTATION = 17
DC_COPIES = 18
DC_TRUETYPE = 28
DC_DRIVER = 11
# Windows紙張大小常量
DMPAPER_LETTER = 1
DMPAPER_A4 = 9
DMPAPER_A3 = 8
DMPAPER_A5 = 11
DMPAPER_B4 = 12
DMPAPER_B5 = 13
DMPAPER_LEGAL = 5
DMPAPER_EXECUTIVE = 7
DMPAPER_TABLOID = 3
# 紙張名稱映射
PAPER_NAMES = {
1: "Letter (8.5 x 11 in)",
3: "Tabloid (11 x 17 in)",
5: "Legal (8.5 x 14 in)",
7: "Executive (7.25 x 10.5 in)",
8: "A3 (297 x 420 mm)",
9: "A4 (210 x 297 mm)",
11: "A5 (148 x 210 mm)",
12: "B4 (250 x 354 mm)",
13: "B5 (182 x 257 mm)",
}
def clean_old_files(folder=None, expire_seconds=3600):
"""定期清理指定目錄下超過expire_seconds的文件"""
if folder is None:
folder = UPLOAD_FOLDER
while True:
now = time.time()
for fname in os.listdir(folder):
fpath = os.path.join(folder, fname)
if os.path.isfile(fpath):
try:
if now - os.path.getmtime(fpath) > 600: # 10分鐘
os.remove(fpath)
except Exception:
pass
time.sleep(60) # 每1分鐘檢查一次
# 兼容PyInstaller打包和源碼運行的資源路徑
def resource_path(relative_path):
if hasattr(sys, '_MEIPASS'):
return os.path.join(sys._MEIPASS, relative_path)
return os.path.join(os.path.abspath("."), relative_path)
# 獲取本機局域網(wǎng)IP
def get_local_ip():
try:
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.settimeout(2)
s.connect(('8.8.8.8', 80))
ip = s.getsockname()[0]
s.close()
return ip
except Exception:
try:
hostname = socket.gethostname()
ip = socket.gethostbyname(hostname)
if ip and ip != '127.0.0.1':
return ip
except Exception:
pass
try:
import subprocess
result = subprocess.run(['ipconfig'], capture_output=True, text=True, encoding='gbk', timeout=10)
if result.returncode == 0:
lines = result.stdout.split('\n')
for line in lines:
if 'IPv4' in line and '地址' in line:
parts = line.split(':')
if len(parts) > 1:
ip = parts[1].strip()
if ip and not ip.startswith('127.') and not ip.startswith('169.254.'):
return ip
except Exception:
pass
return '127.0.0.1'
def get_current_ip_config():
"""獲取當前IP配置狀態(tài)"""
try:
current_ip = get_local_ip()
if current_ip and current_ip != '127.0.0.1':
try:
result = subprocess.run(['ipconfig', '/all'],
capture_output=True, text=True,
encoding='gbk', errors='ignore')
config = {
'index': '1',
'description': '以太網(wǎng)適配器',
'ip': current_ip,
'subnet': '255.255.255.0',
'gateway': '',
'dhcp_enabled': True
}
if 'Default Gateway' in result.stdout or '默認網(wǎng)關(guān)' in result.stdout:
lines = result.stdout.split('\n')
for line in lines:
if 'Default Gateway' in line or '默認網(wǎng)關(guān)' in line:
parts = line.split(':')
if len(parts) > 1:
gateway = parts[1].strip()
if gateway and gateway != '':
config['gateway'] = gateway
break
return config
except Exception:
return {
'index': '1',
'description': '網(wǎng)絡(luò)適配器',
'ip': current_ip,
'subnet': '255.255.255.0',
'gateway': '',
'dhcp_enabled': True
}
else:
return {}
except Exception as e:
print(f"獲取IP配置失敗: {e}")
return {}
def set_static_ip(ip_address, subnet_mask='255.255.255.0', gateway=''):
"""設(shè)置靜態(tài)IP地址"""
try:
config = get_current_ip_config()
if not config:
return False, "未找到有效的網(wǎng)絡(luò)適配器"
adapter_index = config['index']
if not gateway:
ip_parts = ip_address.split('.')
if len(ip_parts) == 4:
gateway = f"{ip_parts[0]}.{ip_parts[1]}.{ip_parts[2]}.1"
cmd = [
'netsh', 'interface', 'ip', 'set', 'address',
f'name="本地連接"' if 'Ethernet' in config['description'] else f'name="以太網(wǎng)"',
'static', ip_address, subnet_mask, gateway
]
result = subprocess.run(cmd, capture_output=True, text=True, encoding='gbk')
if result.returncode == 0:
return True, "IP地址設(shè)置成功"
else:
return set_static_ip_wmi(adapter_index, ip_address, subnet_mask, gateway)
except Exception as e:
return False, f"設(shè)置IP地址失敗: {str(e)}"
def set_static_ip_wmi(adapter_index, ip_address, subnet_mask, gateway):
"""使用WMI設(shè)置靜態(tài)IP地址"""
try:
cmd = [
'wmic', 'path', 'win32_networkadapterconfiguration',
'where', f'Index={adapter_index}',
'call', 'EnableStatic',
f'("{ip_address}")', f'("{subnet_mask}")'
]
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode == 0:
if gateway:
gateway_cmd = [
'wmic', 'path', 'win32_networkadapterconfiguration',
'where', f'Index={adapter_index}',
'call', 'SetGateways',
f'("{gateway}")', '(1)'
]
subprocess.run(gateway_cmd, capture_output=True, text=True)
return True, "IP地址設(shè)置成功"
else:
return False, f"WMI設(shè)置失敗: {result.stderr}"
except Exception as e:
return False, f"WMI設(shè)置異常: {str(e)}"
def set_dhcp():
"""啟用DHCP動態(tài)獲取IP"""
try:
config = get_current_ip_config()
if not config:
return False, "未找到有效的網(wǎng)絡(luò)適配器"
cmd = [
'netsh', 'interface', 'ip', 'set', 'address',
f'name="本地連接"' if 'Ethernet' in config['description'] else f'name="以太網(wǎng)"',
'dhcp'
]
result = subprocess.run(cmd, capture_output=True, text=True, encoding='gbk')
if result.returncode == 0:
return True, "已啟用DHCP動態(tài)獲取IP"
else:
adapter_index = config['index']
cmd = [
'wmic', 'path', 'win32_networkadapterconfiguration',
'where', f'Index={adapter_index}',
'call', 'EnableDHCP'
]
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode == 0:
return True, "已啟用DHCP動態(tài)獲取IP"
else:
return False, f"啟用DHCP失敗: {result.stderr}"
except Exception as e:
return False, f"啟用DHCP異常: {str(e)}"
def suggest_static_ip():
"""建議一個可用的靜態(tài)IP地址"""
current_ip = get_local_ip()
if current_ip and current_ip != '127.0.0.1':
ip_parts = current_ip.split('.')
if len(ip_parts) == 4:
return f"{ip_parts[0]}.{ip_parts[1]}.{ip_parts[2]}.100"
return "192.168.1.100"
# 開機自啟注冊表操作
def set_autostart(enable=True):
exe_path = sys.executable
key = r'Software\\Microsoft\\Windows\\CurrentVersion\\Run'
name = 'PrintServerApp'
with winreg.OpenKey(winreg.HKEY_CURRENT_USER, key, 0, winreg.KEY_ALL_ACCESS) as regkey:
if enable:
winreg.SetValueEx(regkey, name, 0, winreg.REG_SZ, exe_path)
else:
try:
winreg.DeleteValue(regkey, name)
except FileNotFoundError:
pass
def get_autostart():
key = r'Software\\Microsoft\\Windows\\CurrentVersion\\Run'
name = 'PrintServerApp'
try:
with winreg.OpenKey(winreg.HKEY_CURRENT_USER, key, 0, winreg.KEY_READ) as regkey:
val, _ = winreg.QueryValueEx(regkey, name)
return True if val else False
except FileNotFoundError:
return False
app = Flask(__name__)
app.secret_key = 'print_server_secret_key'
UPLOAD_FOLDER = os.path.join(os.path.expanduser('~'), 'Desktop', 'lan-printing-uploads')
LOG_FILE = 'print_log.txt'
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
# 虛擬打印機名稱列表
VIRTUAL_PRINTERS = {
'導出為WPS PDF', 'WPS PDF', 'Microsoft Print to PDF', 'Microsoft XPS Document Writer',
'Fax', '傳真', 'OneNote', 'OneNote (Desktop)', 'Send To OneNote 2016',
'Adobe PDF', 'Foxit Reader PDF Printer', 'PDF Creator', 'CutePDF Writer',
'novaPDF', 'PDFCreator', 'Bullzip PDF Printer', 'doPDF', 'PDF24',
'Virtual PDF Printer', '虛擬PDF打印機', 'Send to Kindle', '發(fā)送到WPS高級打印'
}
# 獲取所有本地和網(wǎng)絡(luò)連接打印機,過濾掉虛擬打印機
ALL_PRINTERS = [p[2] for p in win32print.EnumPrinters(win32print.PRINTER_ENUM_LOCAL | win32print.PRINTER_ENUM_CONNECTIONS)]
PRINTERS = [p for p in ALL_PRINTERS if p not in VIRTUAL_PRINTERS]
def get_default_printer():
"""獲取系統(tǒng)默認打印機"""
try:
default_printer = win32print.GetDefaultPrinter()
if default_printer in PRINTERS:
return default_printer
elif PRINTERS:
return PRINTERS[0]
else:
return None
except Exception as e:
print(f"獲取默認打印機失敗: {e}")
return PRINTERS[0] if PRINTERS else None
def refresh_printer_list():
"""刷新打印機列表"""
global ALL_PRINTERS, PRINTERS
try:
ALL_PRINTERS = [p[2] for p in win32print.EnumPrinters(win32print.PRINTER_ENUM_LOCAL | win32print.PRINTER_ENUM_CONNECTIONS)]
PRINTERS = [p for p in ALL_PRINTERS if p not in VIRTUAL_PRINTERS]
print(f"打印機列表已刷新,檢測到 {len(PRINTERS)} 臺物理打印機")
return True
except Exception as e:
print(f"刷新打印機列表失敗: {e}")
return False
# 美化后的HTML模板
HTML = '''
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>局域網(wǎng)打印服務(wù)系統(tǒng)</title>
<link rel="external nofollow" rel="stylesheet">
<link rel="external nofollow" rel="stylesheet">
<style>
:root {
--primary-color: #00ff9d;
--secondary-color: #00d4ff;
--accent-color: #ff00cc;
--success-color: #00ff9d;
--warning-color: #ff00cc;
--dark-bg: #0a0a12;
--card-bg: #11111d;
--text-primary: #e0e0ff;
--text-secondary: #8a8aa5;
--border-color: #2a2a40;
--card-shadow: 0 4px 30px rgba(0, 255, 230, 0.1);
--hover-shadow: 0 0 20px rgba(0, 255, 230, 0.3);
--neon-glow: 0 0 10px rgba(0, 255, 230, 0.5), 0 0 20px rgba(0, 255, 230, 0.3);
}
body {
background: linear-gradient(135deg, #0a0a12 0%, #1a1a30 100%);
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
min-height: 100vh;
padding: 20px 0;
color: var(--text-primary);
background-image:
radial-gradient(circle at 25% 30%, rgba(0, 255, 157, 0.1) 0%, transparent 40%),
radial-gradient(circle at 75% 60%, rgba(255, 0, 204, 0.1) 0%, transparent 40%),
linear-gradient(to right, rgba(0, 0, 0, 0.2), rgba(0, 0, 0, 0.5)),
repeating-linear-gradient(
45deg,
rgba(10, 10, 18, 0.5),
rgba(10, 10, 18, 0.5) 1px,
rgba(10, 10, 18, 0.7) 1px,
rgba(10, 10, 18, 0.7) 2px
);
}
.main-container {
max-width: 1200px;
margin: 0 auto;
background: var(--card-bg);
border-radius: 20px;
box-shadow: var(--card-shadow);
overflow: hidden;
backdrop-filter: blur(10px);
border: 1px solid var(--border-color);
}
.header {
background: linear-gradient(135deg, #11111d 0%, #1a1a30 100%);
color: var(--text-primary);
padding: 30px;
text-align: center;
position: relative;
border-bottom: 1px solid var(--primary-color);
box-shadow: var(--neon-glow);
}
.header h1 {
font-weight: 700;
margin-bottom: 10px;
font-size: 2.5rem;
color: var(--primary-color);
text-shadow: var(--neon-glow);
letter-spacing: 1px;
}
.header .subtitle {
font-size: 1.2rem;
color: var(--secondary-color);
opacity: 0.9;
}
.nav-tabs {
background: var(--dark-bg);
padding: 0 30px;
border-bottom: 1px solid var(--border-color);
}
.nav-link {
padding: 15px 25px;
font-weight: 600;
color: var(--text-secondary);
border: none;
transition: all 0.3s ease;
}
.nav-link.active {
color: var(--primary-color);
border-bottom: 3px solid var(--primary-color);
background: transparent;
text-shadow: 0 0 5px rgba(0, 255, 157, 0.5);
}
.nav-link:hover {
color: var(--secondary-color);
transform: translateY(-2px);
text-shadow: 0 0 5px rgba(0, 212, 255, 0.5);
}
.tab-content {
padding: 30px;
background: var(--card-bg);
}
.card {
background: var(--dark-bg);
border: 1px solid var(--border-color);
border-radius: 15px;
box-shadow: var(--card-shadow);
transition: all 0.3s ease;
margin-bottom: 20px;
position: relative;
overflow: hidden;
}
.card:hover {
box-shadow: var(--hover-shadow);
transform: translateY(-5px);
border-color: var(--primary-color);
}
.card-header {
background: linear-gradient(90deg, var(--dark-bg), var(--card-bg));
color: var(--primary-color);
border-radius: 15px 15px 0 0 !important;
padding: 15px 20px;
font-weight: 600;
border-bottom: 1px solid var(--border-color);
}
.form-control, .form-select {
background: var(--dark-bg);
color: var(--text-primary);
border-radius: 10px;
border: 2px solid var(--border-color);
padding: 10px 15px;
transition: all 0.3s ease;
}
.form-label {
color: var(--primary-color);
font-weight: 600;
text-shadow: var(--neon-glow);
}
.form-control:focus, .form-select:focus {
border-color: var(--primary-color);
box-shadow: 0 0 0 0.2rem rgba(0, 255, 157, 0.25);
background: var(--dark-bg);
color: var(--text-primary);
}
.btn {
border-radius: 10px;
padding: 10px 25px;
font-weight: 600;
transition: all 0.3s ease;
border: none;
background: var(--dark-bg);
color: var(--text-primary);
border: 1px solid var(--border-color);
}
.btn-primary {
background: linear-gradient(90deg, var(--primary-color), var(--secondary-color));
color: black;
border: none;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 0 20px rgba(0, 255, 157, 0.5);
}
.alert {
border-radius: 10px;
border: none;
padding: 15px 20px;
background: var(--dark-bg);
color: var(--text-primary);
border-left: 4px solid var(--primary-color);
}
.status-badge {
font-size: 0.8rem;
padding: 5px 10px;
border-radius: 20px;
}
.file-list {
max-height: 300px;
overflow-y: auto;
background: var(--dark-bg);
border-radius: 10px;
padding: 10px;
border: 1px solid var(--border-color);
}
.log-entry {
padding: 10px;
border-bottom: 1px solid var(--border-color);
font-size: 0.9rem;
color: var(--text-secondary);
}
.log-entry:last-child {
border-bottom: none;
}
.printer-status {
display: inline-flex;
align-items: center;
gap: 5px;
font-size: 0.9rem;
}
.status-online {
color: var(--success-color);
text-shadow: 0 0 5px rgba(0, 255, 157, 0.5);
}
.status-offline {
color: var(--warning-color);
}
.feature-icon {
font-size: 2rem;
color: var(--primary-color);
margin-bottom: 10px;
text-shadow: var(--neon-glow);
}
.stat-card {
text-align: center;
padding: 20px;
}
.stat-number {
font-size: 2rem;
font-weight: 700;
color: var(--primary-color);
text-shadow: var(--neon-glow);
letter-spacing: 1px;
}
.upload-area {
border: 2px dashed var(--border-color);
border-radius: 15px;
padding: 40px;
text-align: center;
transition: all 0.3s ease;
cursor: pointer;
background: rgba(0, 255, 157, 0.05);
}
.upload-area h5 {
color: white;
text-shadow: var(--neon-glow);
margin-bottom: 10px;
}
.upload-area p {
color: var(--text-primary);
}
.upload-area:hover {
border-color: var(--primary-color);
background-color: rgba(0, 255, 157, 0.1);
box-shadow: var(--neon-glow);
}
.upload-icon {
font-size: 2rem;
color: var(--primary-color);
margin-bottom: 15px;
text-shadow: var(--neon-glow);
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes neonPulse {
0%, 100% { opacity: 0.7; }
50% { opacity: 1; }
}
.neon-pulse {
animation: neonPulse 2s ease-in-out infinite;
}
.grid-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-image: linear-gradient(rgba(0, 255, 157, 0.1) 1px, transparent 1px),
linear-gradient(90deg, rgba(0, 255, 157, 0.1) 1px, transparent 1px);
background-size: 20px 20px;
pointer-events: none;
z-index: -1;
}
/* 滾動條樣式 */
::-webkit-scrollbar {
width: 10px;
}
::-webkit-scrollbar-track {
background: var(--dark-bg);
}
::-webkit-scrollbar-thumb {
background: var(--border-color);
border-radius: 5px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--primary-color);
}
.fade-in {
animation: fadeIn 0.5s ease-in-out;
}
</style>
</head>
<body>
<div class="grid-overlay"></div>
<div class="main-container fade-in">
<!-- 頭部 -->
<div class="header">
<h1><i class="bi bi-printer-fill"></i> 局域網(wǎng)打印服務(wù)系統(tǒng)</h1>
<div class="subtitle">安全、高效、便捷的局域網(wǎng)打印解決方案</div>
</div>
<!-- 導航標簽 -->
<ul class="nav nav-tabs" id="mainTabs" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="print-tab" data-bs-toggle="tab" data-bs-target="#print" type="button" role="tab">
<i class="bi bi-printer"></i> 打印管理
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="status-tab" data-bs-toggle="tab" data-bs-target="#status" type="button" role="tab">
<i class="bi bi-graph-up"></i> 系統(tǒng)狀態(tài)
</button>
</li>
</ul>
<!-- 消息提示 -->
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
<div class="container mt-3">
{% for category, msg in messages %}
<div class="alert alert-{{ 'danger' if category == 'error' else category }} alert-dismissible fade show" role="alert">
<i class="bi bi-{{ 'check-circle-fill' if category == 'success' else 'exclamation-triangle-fill' if category == 'warning' else 'info-circle-fill' if category == 'info' else 'x-circle-fill' }}"></i>
{{ msg }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endfor %}
</div>
{% endif %}
{% endwith %}
<div class="tab-content" id="mainTabsContent">
<!-- 打印管理標簽頁 -->
<div class="tab-pane fade show active" id="print" role="tabpanel">
<div class="row">
<div class="col-lg-8">
<div class="card">
<div class="card-header">
<i class="bi bi-upload"></i> 文件上傳與打印
</div>
<div class="card-body">
<form method="post" enctype="multipart/form-data" id="uploadForm">
<input type="hidden" name="action" value="print">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label fw-bold">選擇打印機</label>
<div class="input-group">
<select name="printer" class="form-select" id="printerSelect">
{% if printers %}
{% for p in printers %}
<option value="{{ p }}" {% if p == default_printer %}selected{% endif %}>
{{ p }}{% if p == default_printer %} (默認){% endif %}
</option>
{% endfor %}
{% else %}
<option value="">未檢測到可用打印機</option>
{% endif %}
</select>
<button type="button" class="btn btn-outline-secondary" onclick="refreshPrinterList()" title="刷新打印機列表">
<i class="bi bi-arrow-clockwise"></i>
</button>
</div>
<div class="form-text">
{% if printers %}
已過濾虛擬打印機,自動選擇默認打印機
{% else %}
<span class="text-danger">?? 未檢測到物理打印機</span>
{% endif %}
</div>
</div>
<div class="col-md-3">
<label class="form-label fw-bold">打印份數(shù)</label>
<select name="copies" class="form-select">
{% for i in range(1, 11) %}
<option value="{{ i }}" {% if i == 1 %}selected{% endif %}>{{ i }}</option>
{% endfor %}
</select>
</div>
<div class="col-md-3">
<label class="form-label fw-bold">打印模式</label>
<select name="duplex" class="form-select">
<option value="1">單面打印</option>
{% if printer_caps and printer_caps.get('duplex_support') %}
<option value="2">雙面長邊</option>
<option value="3">雙面短邊</option>
{% endif %}
</select>
</div>
<!-- 新增:色彩選擇模式 -->
<div class="col-md-3">
<label class="form-label fw-bold">色彩模式</label>
<select name="color_mode" class="form-select">
<option value="color">彩色</option>
<option value="monochrome">黑白</option>
</select>
</div>
<!-- 新增:打印方向 -->
<div class="col-md-3">
<label class="form-label fw-bold">打印方向</label>
<select name="orientation" class="form-select">
<option value="portrait">縱向</option>
<option value="landscape">橫向</option>
</select>
</div>
<div class="col-md-6">
<label class="form-label fw-bold">紙張設(shè)置</label>
<select name="papersize" class="form-select" id="paperSelect">
{% if printer_caps and printer_caps.get('papers') %}
{% for p in printer_caps.papers %}
<option value="{{ p.id }}" {% if p.id == 9 %}selected{% endif %}>{{ p.name }}</option>
{% endfor %}
{% else %}
<option value="9" selected>A4 (210 x 297 mm)</option>
{% endif %}
</select>
</div>
<div class="col-md-6">
<label class="form-label fw-bold">打印質(zhì)量</label>
<select name="quality" class="form-select" id="qualitySelect">
{% if printer_caps and printer_caps.get('resolutions') %}
{% for r in printer_caps.resolutions %}
<option value="{{ r }}">{{ r }} DPI</option>
{% endfor %}
{% else %}
<option value="600x600">600x600 DPI</option>
{% endif %}
</select>
</div>
<!-- 新增:打印比例 -->
<div class="col-md-6">
<label class="form-label fw-bold">打印比例</label>
<select name="scale" class="form-select">
<option value="original">原始比例</option>
<option value="fit_margins">適合紙張邊距</option>
<option value="fit_printable">適合至可打印區(qū)域</option>
</select>
</div>
<!-- 新增:打印范圍 -->
<div class="col-md-6">
<label class="form-label fw-bold">打印范圍</label>
<select name="print_range" class="form-select" id="printRangeSelect" onchange="togglePageRangeInput()">
<option value="all">全部</option>
<option value="current">當前頁面</option>
<option value="pages">頁碼選擇</option>
</select>
<div id="pageRangeContainer" class="mt-2 d-none">
<input type="text" name="page_range" class="form-control" placeholder="例如:1,3-5,7" disabled>
<div class="form-text">使用逗號分隔頁碼,連字符表示范圍</div>
</div>
</div>
<div class="col-12">
<label class="form-label fw-bold">選擇文件</label>
<div class="upload-area" onclick="document.getElementById('fileInput').click()">
<i class="bi bi-cloud-upload upload-icon"></i>
<h5>點擊選擇文件或拖拽文件到此區(qū)域,支持 PDF, JPG, PNG, DOC, DOCX, PPT, PPTX, XLS, XLSX, TXT 格式</h5>
<input type="file" name="file" id="fileInput" multiple class="d-none" onchange="updateFileList()">
</div>
<div id="fileList" class="mt-3"></div>
</div>
<div class="col-12 text-end">
{% if printers %}
<button type="submit" class="btn btn-primary btn-lg">
<i class="bi bi-send-check"></i> 開始打印
</button>
{% else %}
<button type="button" class="btn btn-secondary btn-lg" disabled>
<i class="bi bi-exclamation-triangle"></i> 無可用打印機
</button>
{% endif %}
</div>
</div>
</form>
</div>
</div>
</div>
<div class="col-lg-4">
<div class="card">
<div class="card-header">
<i class="bi bi-info-circle"></i> 打印說明
</div>
<div class="card-body">
<div class="alert alert-info">
<h6><i class="bi bi-lightbulb"></i> 使用提示</h6>
<ul class="small mb-0">
<li>支持多種文件格式直接打印</li>
<li>自動過濾虛擬打印機</li>
<li>靜默打印無需確認</li>
<li>實時顯示打印狀態(tài)</li>
</ul>
</div>
<div class="alert alert-success">
<h6><i class="bi bi-check-circle"></i> 當前狀態(tài)</h6>
<div class="printer-status {% if printers %}status-online{% else %}status-offline{% endif %}">
<i class="bi bi-{% if printers %}check-circle{% else %}x-circle{% endif %}"></i>
{% if printers %}
檢測到 {{ printers|length }} 臺打印機
{% else %}
未檢測到可用打印機
{% endif %}
</div>
</div>
</div>
</div>
<div class="card mt-3">
<div class="card-header">
<i class="bi bi-clock-history"></i> 最近文件
</div>
<div class="card-body file-list">
{% for f in files[-5:] %}
<div class="d-flex justify-content-between align-items-center log-entry">
<span class="text-truncate" style="max-width: 70%;">{{ f }}</span>
<a href="/preview/{{ f }}" rel="external nofollow" target="_blank" class="btn btn-sm btn-outline-primary">
<i class="bi bi-eye"></i>
</a>
</div>
{% else %}
<p class="text-muted text-center">暫無文件</p>
{% endfor %}
</div>
</div>
</div>
</div>
</div>
<!-- 網(wǎng)絡(luò)配置標簽頁 -->
<div class="tab-pane fade" id="network" role="tabpanel">
<div class="row">
<div class="col-md-6">
<div class="card">
<div class="card-header">
<i class="bi bi-ethernet"></i> 當前網(wǎng)絡(luò)狀態(tài)
</div>
<div class="card-body">
{% if ip_config %}
<div class="row g-3">
<div class="col-12">
<label class="form-label fw-bold">IP地址</label>
<div class="d-flex align-items-center">
<span class="fw-bold text-primary">{{ ip_config.ip }}</span>
<span class="badge {% if ip_config.dhcp_enabled %}bg-success{% else %}bg-primary{% endif %} status-badge ms-2">
{% if ip_config.dhcp_enabled %}DHCP{% else %}靜態(tài)IP{% endif %}
</span>
</div>
</div>
<div class="col-6">
<label class="form-label fw-bold">子網(wǎng)掩碼</label>
<div>{{ ip_config.subnet }}</div>
</div>
<div class="col-6">
<label class="form-label fw-bold">默認網(wǎng)關(guān)</label>
<div>{{ ip_config.gateway if ip_config.gateway else '未設(shè)置' }}</div>
</div>
<div class="col-12">
<label class="form-label fw-bold">網(wǎng)絡(luò)適配器</label>
<div class="text-truncate">{{ ip_config.description }}</div>
</div>
</div>
{% else %}
<div class="alert alert-warning text-center">
<i class="bi bi-wifi-off"></i>
未檢測到網(wǎng)絡(luò)連接
</div>
{% endif %}
</div>
</div>
</div>
<div class="col-md-6">
<div class="card">
<div class="card-header">
<i class="bi bi-gear"></i> IP地址配置
</div>
<div class="card-body">
<form method="post">
<input type="hidden" name="action" value="set_static_ip">
<div class="mb-3">
<label class="form-label fw-bold">IP地址</label>
<input type="text" name="ip_address" class="form-control"
value="{{ suggested_ip }}" placeholder="192.168.1.100" required>
</div>
<div class="mb-3">
<label class="form-label fw-bold">子網(wǎng)掩碼</label>
<input type="text" name="subnet_mask" class="form-control"
value="255.255.255.0" required>
</div>
<div class="mb-3">
<label class="form-label fw-bold">默認網(wǎng)關(guān)</label>
<input type="text" name="gateway" class="form-control"
placeholder="可選,自動推導">
</div>
<button type="submit" class="btn btn-primary w-100">
<i class="bi bi-check-circle"></i> 設(shè)置靜態(tài)IP
</button>
</form>
<hr>
<form method="post" class="mt-3">
<input type="hidden" name="action" value="enable_dhcp">
<button type="submit" class="btn btn-outline-primary w-100">
<i class="bi bi-arrow-repeat"></i> 啟用DHCP自動獲取
</button>
</form>
</div>
</div>
</div>
</div>
</div>
<!-- 系統(tǒng)狀態(tài)標簽頁 -->
<div class="tab-pane fade" id="status" role="tabpanel">
<div class="row">
<div class="col-md-3">
<div class="card stat-card">
<i class="bi bi-printer feature-icon"></i>
<div class="stat-number">{{ printers|length }}</div>
<div>可用打印機</div>
</div>
</div>
<div class="col-md-3">
<div class="card stat-card">
<i class="bi bi-file-earmark feature-icon"></i>
<div class="stat-number">{{ files|length }}</div>
<div>待打印文件</div>
</div>
</div>
<div class="col-md-3">
<div class="card stat-card">
<i class="bi bi-wifi feature-icon"></i>
<div class="stat-number">{% if ip_config %}在線{% else %}離線{% endif %}</div>
<div>網(wǎng)絡(luò)狀態(tài)</div>
</div>
</div>
<div class="col-md-3">
<div class="card stat-card">
<i class="bi bi-clock feature-icon"></i>
<div class="stat-number">{{ logs|length }}</div>
<div>今日日志</div>
</div>
</div>
</div>
<div class="row mt-4">
<div class="col-md-6">
<div class="card">
<div class="card-header">
<i class="bi bi-list-check"></i> 打印機列表
</div>
<div class="card-body file-list">
{% for printer in printers %}
<div class="log-entry">
<div class="d-flex justify-content-between align-items-center">
<span>{{ printer }}</span>
{% if printer == default_printer %}
<span class="badge bg-primary status-badge">默認</span>
{% endif %}
</div>
</div>
{% else %}
<p class="text-muted text-center">未檢測到打印機</p>
{% endfor %}
</div>
</div>
</div>
<div class="col-md-6">
<div class="card">
<div class="card-header">
<i class="bi bi-journal-text"></i> 最近日志
</div>
<div class="card-body file-list">
{% for log in logs[-10:] %}
<div class="log-entry">
<small class="text-muted">{{ log.split(' 打印:')[0] }}</small>
{{ log.split(' 打印:')[1] if ' 打印:' in log else log }}
</div>
{% else %}
<p class="text-muted text-center">暫無日志記錄</p>
{% endfor %}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
<script>
// 控制頁碼范圍輸入框的顯示/隱藏
function togglePageRangeInput() {
const rangeSelect = document.getElementById('printRangeSelect');
const rangeContainer = document.getElementById('pageRangeContainer');
const rangeInput = rangeContainer.querySelector('input');
if (rangeSelect.value === 'pages') {
rangeContainer.classList.remove('d-none');
rangeInput.disabled = false;
} else {
rangeContainer.classList.add('d-none');
rangeInput.disabled = true;
}
}
// 文件選擇處理
function updateFileList() {
const fileInput = document.getElementById('fileInput');
const fileList = document.getElementById('fileList');
const files = fileInput.files;
if (files.length > 0) {
let html = '<div class="alert alert-success"><h6>已選擇文件:</h6><ul class="mb-0">';
for (let file of files) {
html += `<li>${file.name} (${(file.size / 1024).toFixed(1)} KB)</li>`;
}
html += '</ul></div>';
fileList.innerHTML = html;
} else {
fileList.innerHTML = '';
}
}
// 拖拽文件支持
const uploadArea = document.querySelector('.upload-area');
uploadArea.addEventListener('dragover', (e) => {
e.preventDefault();
uploadArea.style.borderColor = '#4361ee';
uploadArea.style.backgroundColor = '#f0f4ff';
});
uploadArea.addEventListener('dragleave', () => {
uploadArea.style.borderColor = '#dee2e6';
uploadArea.style.backgroundColor = '';
});
uploadArea.addEventListener('drop', (e) => {
e.preventDefault();
uploadArea.style.borderColor = '#dee2e6';
uploadArea.style.backgroundColor = '';
const files = e.dataTransfer.files;
document.getElementById('fileInput').files = files;
updateFileList();
});
// 打印機信息刷新
function refreshPrinterInfo() {
const printerSelect = document.getElementById('printerSelect');
const paperSelect = document.getElementById('paperSelect');
const qualitySelect = document.getElementById('qualitySelect');
if (!printerSelect || !printerSelect.value) return;
fetch('/api/printer_info?printer=' + encodeURIComponent(printerSelect.value))
.then(r => r.json())
.then(data => {
if (data.success && data.capabilities) {
const caps = data.capabilities;
// 更新紙張選項
if (paperSelect && caps.papers && caps.papers.length) {
const prev = paperSelect.value;
paperSelect.innerHTML = '';
caps.papers.forEach(p => {
const opt = document.createElement('option');
opt.value = p.id;
opt.textContent = p.name;
paperSelect.appendChild(opt);
});
if (prev) paperSelect.value = prev;
}
// 更新質(zhì)量選項
if (qualitySelect && caps.resolutions && caps.resolutions.length) {
qualitySelect.innerHTML = '';
caps.resolutions.forEach(r => {
const opt = document.createElement('option');
opt.value = r;
opt.textContent = r + ' DPI';
qualitySelect.appendChild(opt);
});
}
}
})
.catch(console.error);
}
// 刷新打印機列表
function refreshPrinterList() {
const btn = document.querySelector('button[onclick="refreshPrinterList()"]');
btn.innerHTML = '<i class="bi bi-arrow-repeat spinner-border spinner-border-sm"></i>';
btn.disabled = true;
fetch('/api/refresh_printers')
.then(r => r.json())
.then(data => {
if (data.success) {
const select = document.getElementById('printerSelect');
select.innerHTML = '';
if (data.printers && data.printers.length) {
data.printers.forEach(p => {
const opt = document.createElement('option');
opt.value = p;
opt.textContent = p + (p === data.default_printer ? ' (默認)' : '');
if (p === data.default_printer) opt.selected = true;
select.appendChild(opt);
});
// 顯示成功消息
showAlert('打印機列表刷新成功', 'success');
refreshPrinterInfo();
} else {
select.innerHTML = '<option value="">未檢測到可用打印機</option>';
showAlert('未找到可用打印機', 'warning');
}
} else {
showAlert('刷新失敗: ' + data.error, 'danger');
}
})
.finally(() => {
btn.innerHTML = '<i class="bi bi-arrow-clockwise"></i>';
btn.disabled = false;
});
}
// 顯示提示消息
function showAlert(message, type) {
const alertDiv = document.createElement('div');
alertDiv.className = `alert alert-${type} alert-dismissible fade show`;
alertDiv.innerHTML = `
<i class="bi bi-${type === 'success' ? 'check-circle' : type === 'warning' ? 'exclamation-triangle' : 'info-circle'}-fill"></i>
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
document.querySelector('.tab-content').prepend(alertDiv);
setTimeout(() => {
if (alertDiv.parentNode) {
alertDiv.remove();
}
}, 5000);
}
// 初始化
document.addEventListener('DOMContentLoaded', function() {
const printerSelect = document.getElementById('printerSelect');
if (printerSelect) {
printerSelect.addEventListener('change', refreshPrinterInfo);
refreshPrinterInfo(); // 初始加載
}
// 表單提交驗證
document.getElementById('uploadForm')?.addEventListener('submit', function(e) {
const files = document.getElementById('fileInput').files;
if (files.length === 0) {
e.preventDefault();
showAlert('請選擇要打印的文件', 'warning');
return false;
}
});
});
</script>
</body>
</html>
'''
# 允許的文件類型
ALLOWED_EXT = {'pdf', 'jpg', 'jpeg', 'png', 'txt', 'doc', 'docx', 'ppt', 'pptx', 'xls', 'xlsx'}
def allowed_file(filename):
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXT
def is_physical_printer(printer_name):
"""檢查是否為真正的物理打印機"""
if printer_name in VIRTUAL_PRINTERS:
return False
virtual_keywords = ['pdf', 'fax', '傳真', 'xps', 'onenote', 'virtual', '虛擬', 'send to', 'export', '導出']
printer_lower = printer_name.lower()
for keyword in virtual_keywords:
if keyword in printer_lower:
return False
return True
def log_print(filename, printer, copies, duplex, papersize, quality, color_mode='color', orientation='portrait', scale='original', print_range='all', page_range=''):
with open(LOG_FILE, 'a', encoding='utf-8') as f:
f.write(f"{datetime.now()} 打印: {filename} 打印機: {printer} 份數(shù): {copies} 單雙面: {duplex} 紙張: {papersize} 質(zhì)量: {quality} 色彩: {color_mode} 方向: {orientation} 比例: {scale} 范圍: {print_range}{f' ({page_range})' if page_range else ''}\n")
# 保留原有的打印功能函數(shù)(print_file_with_settings, apply_printer_settings等)
# 由于篇幅限制,這里省略了具體的打印功能實現(xiàn),保持原代碼不變
def print_image_silent(filepath, printer_name, copies=1, color_mode='color', orientation='portrait'):
"""靜默打印圖片文件"""
try:
import win32print
import win32ui
import win32con
import win32api
print(f"靜默打印圖片文件: {filepath} 到打印機: {printer_name} 份數(shù): {copies} 色彩: {color_mode} 方向: {orientation}")
# 先設(shè)置為默認打印機,確保所有設(shè)置生效
old_default_printer = win32print.GetDefaultPrinter()
win32print.SetDefaultPrinter(printer_name)
try:
# 打開打印機
hprinter = win32print.OpenPrinter(printer_name)
try:
# 獲取打印機屬性
printer_info = win32print.GetPrinter(hprinter, 2)
devmode = printer_info[1]
# 確保DEVMODE結(jié)構(gòu)具有正確的標志
devmode.Fields |= (win32con.DM_ORIENTATION | win32con.DM_COLOR | win32con.DM_COPIES)
# 應(yīng)用打印方向
if orientation == 'landscape':
devmode.Orientation = win32con.DMORIENT_LANDSCAPE
else:
devmode.Orientation = win32con.DMORIENT_PORTRAIT
print(f"應(yīng)用打印方向: {orientation} -> {devmode.Orientation}")
# 應(yīng)用色彩模式
if color_mode == 'monochrome':
devmode.Color = 1 # 單色
else:
devmode.Color = 2 # 彩色
print(f"應(yīng)用色彩模式: {color_mode} -> {devmode.Color}")
# 應(yīng)用打印份數(shù)
devmode.Copies = copies
print(f"應(yīng)用打印份數(shù): {copies}")
# 更新打印機屬性
win32print.SetPrinter(hprinter, 2, devmode, 0)
# 驗證設(shè)置是否正確應(yīng)用
updated_info = win32print.GetPrinter(hprinter, 2)
updated_devmode = updated_info[1]
print(f"設(shè)置應(yīng)用后驗證: 方向={updated_devmode.Orientation}, 色彩={updated_devmode.Color}")
# 使用ShellExecute打印
print(f"使用ShellExecute打印圖片: {filepath} 到 {printer_name}")
result = win32api.ShellExecute(
0,
"print",
filepath,
f'/d:"{printer_name}"',
".",
0
)
if result > 32:
return True, f"已發(fā)送到 {printer_name} (圖片已應(yīng)用設(shè)置,設(shè)置驗證通過)"
else:
print(f"ShellExecute圖片打印失敗,返回碼: {result}")
# 備用方案:使用Windows圖片查看器打印
try:
photo_viewer_path = os.path.join(os.environ.get('SystemRoot', r'C:\Windows'), 'System32', 'rundll32.exe')
cmd = [
photo_viewer_path,
'C:\\Program Files\\Windows Photo Viewer\\PhotoViewer.dll',
'ImageView_Fullscreen',
'/p',
filepath
]
subprocess.Popen(cmd, shell=False)
return True, f"已發(fā)送到 {printer_name} (使用Windows照片查看器打印)"
except Exception as e2:
print(f"備用打印方法失敗: {str(e2)}")
raise
finally:
win32print.ClosePrinter(hprinter)
finally:
# 恢復原來的默認打印機
if old_default_printer:
win32print.SetDefaultPrinter(old_default_printer)
except Exception as e:
error_msg = f"圖片打印異常: {str(e)}"
print(error_msg)
# 備用方案
try:
win32api.ShellExecute(
0,
"print",
filepath,
f'/d:"{printer_name}"',
".",
0
)
return True, f"已發(fā)送到 {printer_name} (使用基礎(chǔ)打印功能)"
except Exception as e2:
return False, f"圖片打印失敗: {str(e2)}"
def print_office_silent(filepath, printer_name, copies=1, color_mode='color', orientation='portrait'):
"""靜默打印Office文檔"""
try:
import win32com.client
import os
import time
print(f"靜默打印Office文檔: {filepath} 到打印機: {printer_name} 份數(shù): {copies} 色彩: {color_mode} 方向: {orientation}")
file_ext = os.path.splitext(filepath)[1].lower()
app = None
success = False
result_message = ""
try:
# 根據(jù)文件類型啟動相應(yīng)的Office應(yīng)用
if file_ext in ['.doc', '.docx']:
app = win32com.client.Dispatch('Word.Application')
# 設(shè)置為不可見,避免界面彈出
app.Visible = False
app.DisplayAlerts = False
doc = app.Documents.Open(os.path.abspath(filepath), ReadOnly=True)
# 設(shè)置打印方向
if orientation == 'landscape':
for section in doc.Sections:
section.PageSetup.Orientation = 1 # wdOrientLandscape
print(f"Word文檔設(shè)置為橫向打印")
else:
for section in doc.Sections:
section.PageSetup.Orientation = 0 # wdOrientPortrait
print(f"Word文檔設(shè)置為縱向打印")
# 設(shè)置色彩模式(Word通過打印機屬性設(shè)置,這里通過PrintOut的屬性傳遞)
print(f"Word文檔設(shè)置色彩模式: {color_mode}")
# 打印文檔,指定使用的打印機
print(f"執(zhí)行Word文檔打印到: {printer_name},份數(shù): {copies}")
doc.PrintOut(Copies=copies, Printer=printer_name, Background=True, PrintToFile=False)
# 等待打印任務(wù)開始
time.sleep(1)
success = True
result_message = f"Word文檔已發(fā)送到 {printer_name} (已應(yīng)用設(shè)置)"
doc.Close(SaveChanges=0)
elif file_ext in ['.xls', '.xlsx']:
app = win32com.client.Dispatch('Excel.Application')
# 設(shè)置為不可見,避免界面彈出
app.Visible = False
app.DisplayAlerts = False
book = app.Workbooks.Open(os.path.abspath(filepath), ReadOnly=True)
# 設(shè)置所有工作表的打印方向和色彩模式
for sheet in book.Sheets:
if orientation == 'landscape':
sheet.PageSetup.Orientation = 2 # xlLandscape
print(f"Excel工作表 '{sheet.Name}' 設(shè)置為橫向打印")
else:
sheet.PageSetup.Orientation = 1 # xlPortrait
print(f"Excel工作表 '{sheet.Name}' 設(shè)置為縱向打印")
# 設(shè)置色彩模式
if color_mode == 'monochrome':
sheet.PageSetup.BlackAndWhite = True
print(f"Excel工作表 '{sheet.Name}' 設(shè)置為黑白打印")
else:
sheet.PageSetup.BlackAndWhite = False
print(f"Excel工作表 '{sheet.Name}' 設(shè)置為彩色打印")
# 打印工作簿
print(f"執(zhí)行Excel工作簿打印到: {printer_name},份數(shù): {copies}")
book.PrintOut(Copies=copies, ActivePrinter=printer_name)
# 等待打印任務(wù)開始
time.sleep(1)
success = True
result_message = f"Excel文檔已發(fā)送到 {printer_name} (已應(yīng)用設(shè)置)"
book.Close(SaveChanges=0)
elif file_ext in ['.ppt', '.pptx']:
app = win32com.client.Dispatch('PowerPoint.Application')
# 設(shè)置為不可見,避免界面彈出
app.Visible = False
pres = app.Presentations.Open(os.path.abspath(filepath), WithWindow=False, ReadOnly=True)
# 設(shè)置打印選項
print_options = pres.PrintOptions
if color_mode == 'monochrome':
print_options.OutputType = 2 # ppPrintOutputGrayscale
print(f"PowerPoint演示文稿設(shè)置為灰度打印")
else:
print_options.OutputType = 1 # ppPrintOutputColor
print(f"PowerPoint演示文稿設(shè)置為彩色打印")
# 打印演示文稿
print(f"執(zhí)行PowerPoint演示文稿打印到: {printer_name},份數(shù): {copies}")
pres.PrintOut(PrintRange=None, Copies=copies, PrinterName=printer_name)
# 等待打印任務(wù)開始
time.sleep(1)
success = True
result_message = f"PowerPoint文檔已發(fā)送到 {printer_name} (已應(yīng)用設(shè)置)"
pres.Close()
else:
raise ValueError(f"不支持的Office文件類型: {file_ext}")
if success:
return True, result_message
else:
raise Exception("打印任務(wù)未成功啟動")
finally:
# 確保關(guān)閉Office應(yīng)用
if app:
try:
app.Quit()
print("Office應(yīng)用已成功關(guān)閉")
except Exception as quit_error:
print(f"關(guān)閉Office應(yīng)用時出錯: {str(quit_error)}")
# 釋放COM對象
import pythoncom
pythoncom.CoUninitialize()
except Exception as e:
error_msg = f"Office文檔打印異常: {str(e)}"
print(error_msg)
# 備用方案 - 使用ShellExecute
try:
import win32api
print(f"使用備用方案: ShellExecute打印到 {printer_name}")
result = win32api.ShellExecute(
0,
"print",
filepath,
f'/d:"{printer_name}"',
".",
0
)
if result > 32:
return True, f"已發(fā)送到 {printer_name} (使用系統(tǒng)默認打印)"
else:
raise Exception(f"ShellExecute打印失敗,返回碼: {result}")
except Exception as e2:
return False, f"Office文檔打印失敗: {str(e2)}"
def print_file_with_settings(filepath, printer_name, copies=1, duplex=1, papersize='A4', quality='normal', color_mode='color', orientation='portrait', scale='original'):
"""使用獲取到的真實打印設(shè)置進行打印"""
try:
print(f"開始打印文件: {filepath}")
print(f"目標打印機: {printer_name}")
print(f"打印份數(shù): {copies}")
print(f"雙面設(shè)置: {duplex}")
print(f"紙張大小: {papersize}")
print(f"打印質(zhì)量: {quality}")
print(f"色彩模式: {color_mode}")
print(f"打印方向: {orientation}")
print(f"打印比例: {scale}")
file_ext = os.path.splitext(filepath)[1].lower()
if file_ext == '.pdf':
# 使用print_pdf_silent代替不存在的print_pdf_with_settings函數(shù)
return print_pdf_silent(filepath, printer_name, copies, duplex, papersize, quality, color_mode, orientation, scale, 'all', '')
elif file_ext in ['.jpg', '.jpeg', '.png', '.bmp', '.gif']:
return print_image_silent(filepath, printer_name, copies, color_mode, orientation)
elif file_ext in ['.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx']:
return print_office_silent(filepath, printer_name, copies, color_mode, orientation)
elif file_ext == '.txt':
return print_text_file_simple(filepath, printer_name, copies)
else:
print(f"未知文件類型 {file_ext},嘗試使用系統(tǒng)默認打印方式")
return print_with_shell_execute(filepath, printer_name, copies)
except Exception as e:
print(f"打印操作失敗: {e}")
return print_file_silent_fallback(filepath, printer_name, copies)
def print_with_shell_execute(filepath, printer_name, copies=1, color_mode='color', orientation='portrait', papersize='9'):
"""使用ShellExecute執(zhí)行打印并應(yīng)用基本設(shè)置"""
try:
import win32api
import win32print
import win32con
import os
print(f"使用ShellExecute打印文件: {filepath} 到 {printer_name}")
print(f"設(shè)置: 份數(shù)={copies}, 色彩={color_mode}, 方向={orientation}, 紙張={papersize}")
# 先設(shè)置為默認打印機,確保設(shè)置生效
old_default_printer = None
try:
old_default_printer = win32print.GetDefaultPrinter()
win32print.SetDefaultPrinter(printer_name)
# 打開打印機并應(yīng)用設(shè)置
hprinter = win32print.OpenPrinter(printer_name)
try:
# 獲取當前打印機設(shè)置
printer_info = win32print.GetPrinter(hprinter, 2)
devmode = printer_info[1]
# 應(yīng)用打印方向
if orientation == 'landscape':
devmode.Orientation = win32con.DMORIENT_LANDSCAPE
print(f"應(yīng)用打印方向: 橫向")
else:
devmode.Orientation = win32con.DMORIENT_PORTRAIT
print(f"應(yīng)用打印方向: 縱向")
# 應(yīng)用色彩模式
if color_mode == 'monochrome':
devmode.Color = 1 # 單色
print(f"應(yīng)用色彩模式: 單色")
else:
devmode.Color = 2 # 彩色
print(f"應(yīng)用色彩模式: 彩色")
# 應(yīng)用紙張大小
try:
paper_size_id = int(papersize)
devmode.PaperSize = paper_size_id
print(f"應(yīng)用紙張大小ID: {paper_size_id}")
except ValueError:
print(f"無效的紙張大小ID: {papersize},使用默認值")
# 應(yīng)用打印份數(shù)
devmode.Copies = copies
print(f"應(yīng)用打印份數(shù): {copies}")
# 強制設(shè)置DEVMODE標志位,確保所有設(shè)置生效
devmode.Fields |= (
win32con.DM_ORIENTATION |
win32con.DM_COLOR |
win32con.DM_PAPERSIZE |
win32con.DM_COPIES
)
# 更新打印機屬性
win32print.SetPrinter(hprinter, 2, devmode, 0)
# 驗證設(shè)置是否正確應(yīng)用
updated_info = win32print.GetPrinter(hprinter, 2)
updated_devmode = updated_info[1]
print(f"設(shè)置應(yīng)用后驗證: 方向={updated_devmode.Orientation}, 色彩={updated_devmode.Color}")
# 使用ShellExecute打印
result = win32api.ShellExecute(
0,
"print",
filepath,
f'/d:"{printer_name}"',
os.path.dirname(filepath),
0
)
if result > 32:
return True, f"已發(fā)送到 {printer_name} (已應(yīng)用ShellExecute設(shè)置)"
else:
print(f"ShellExecute打印失敗,返回碼: {result}")
raise Exception(f"ShellExecute打印失敗,返回碼: {result}")
finally:
win32print.ClosePrinter(hprinter)
finally:
# 恢復原來的默認打印機
if old_default_printer:
win32print.SetDefaultPrinter(old_default_printer)
print(f"已恢復默認打印機: {old_default_printer}")
except Exception as e:
print(f"ShellExecute打印異常: {str(e)}")
return False, f"ShellExecute打印失敗: {str(e)}"
def print_file_silent_fallback(filepath, printer_name, copies=1):
"""最后的備用打印方案,適用于所有文件類型"""
try:
import win32api
import os
print(f"使用備用打印方案: 打印文件 {filepath} 到 {printer_name}")
# 使用最簡單的ShellExecute打印方式,雖然可能無法應(yīng)用所有設(shè)置
# 但確保文件能夠被打印
result = win32api.ShellExecute(
0,
"print",
filepath,
f'/d:"{printer_name}"',
os.path.dirname(filepath),
0
)
if result > 32:
return True, f"已發(fā)送到 {printer_name} (備用打印方案)"
else:
raise Exception(f"備用打印方案失敗,返回碼: {result}")
except Exception as e:
print(f"備用打印方案異常: {str(e)}")
return False, f"所有打印方法均失敗: {str(e)}"
def print_text_file_simple(filepath, printer_name, copies=1):
"""簡單打印文本文件"""
try:
import win32print
import os
print(f"簡單打印文本文件: {filepath} 到 {printer_name},份數(shù): {copies}")
# 打開打印機
hprinter = win32print.OpenPrinter(printer_name)
try:
# 開始打印作業(yè)
job_info = (os.path.basename(filepath), None, "RAW")
hjob = win32print.StartDocPrinter(hprinter, 1, job_info)
try:
# 讀取文本文件內(nèi)容
with open(filepath, 'r', encoding='utf-8') as f:
content = f.read()
# 按份打印
for _ in range(copies):
win32print.StartPagePrinter(hprinter)
# 發(fā)送文件內(nèi)容到打印機
win32print.WritePrinter(hprinter, content.encode('utf-8'))
win32print.EndPagePrinter(hprinter)
finally:
win32print.EndDocPrinter(hprinter)
return True, f"文本文件已發(fā)送到 {printer_name}"
finally:
win32print.ClosePrinter(hprinter)
except Exception as e:
print(f"文本文件打印異常: {str(e)}")
# 備用方案
return print_with_shell_execute(filepath, printer_name, copies)
def print_pdf_silent(filepath, printer_name, copies=1, duplex=1, papersize='9', quality='600x600', color_mode='color', orientation='portrait', scale='original', print_range='all', page_range=''):
"""靜默打印PDF文件"""
try:
import subprocess
import os
import win32api
import tempfile
import shutil
print(f"靜默打印PDF文件: {filepath} 到打印機: {printer_name} 份數(shù): {copies} 雙面: {duplex} 色彩: {color_mode} 方向: {orientation} 比例: {scale} 范圍: {print_range}{f' ({page_range})' if page_range else ''}")
# 方法1: 嘗試使用多種PDF閱讀器實現(xiàn)更完整的打印設(shè)置
# 1. 嘗試使用SumatraPDF(輕量級且支持更多命令行選項)
sumatra_path = os.path.join(os.environ.get('ProgramFiles', r'C:\Program Files'), 'SumatraPDF', 'SumatraPDF.exe')
if os.path.exists(sumatra_path):
# 構(gòu)建命令行參數(shù),支持更多打印選項
cmd = [sumatra_path, '-print-to', printer_name, '-silent']
# 添加打印范圍參數(shù)
if print_range == 'current':
cmd.extend(['-print-page', '1'])
elif print_range == 'pages' and page_range:
cmd.extend(['-print-page', page_range])
# 添加打印方向參數(shù)
if orientation == 'landscape':
cmd.append('-print-settings')
cmd.append('landscape')
# 添加色彩模式參數(shù)
if color_mode == 'monochrome':
if '-print-settings' in cmd:
idx = cmd.index('-print-settings')
cmd[idx+1] = cmd[idx+1] + ',grayscale'
else:
cmd.extend(['-print-settings', 'grayscale'])
# 添加打印份數(shù)參數(shù)
if copies > 1:
if '-print-settings' in cmd:
idx = cmd.index('-print-settings')
cmd[idx+1] = cmd[idx+1] + f',{copies}copies'
else:
cmd.extend(['-print-settings', f'{copies}copies'])
# 添加打印比例參數(shù)
if scale == 'fit_margins':
if '-print-settings' in cmd:
idx = cmd.index('-print-settings')
cmd[idx+1] = cmd[idx+1] + ',fit'
else:
cmd.extend(['-print-settings', 'fit'])
elif scale == 'fit_printable':
if '-print-settings' in cmd:
idx = cmd.index('-print-settings')
cmd[idx+1] = cmd[idx+1] + ',shrink'
else:
cmd.extend(['-print-settings', 'shrink'])
cmd.append(filepath)
print(f"使用SumatraPDF執(zhí)行命令: {' '.join(cmd)}")
process = subprocess.run(cmd, capture_output=True, timeout=30)
if process.returncode == 0:
return True, f"已發(fā)送到 {printer_name} (使用SumatraPDF并應(yīng)用完整設(shè)置)"
else:
print(f"SumatraPDF打印失敗: {process.stderr.decode()}")
# 2. 如果SumatraPDF不可用,嘗試使用Adobe Reader
adobe_path = os.path.join(os.environ.get('ProgramFiles', r'C:\Program Files'), 'Adobe', 'Acrobat Reader DC', 'Reader', 'AcroRd32.exe')
if os.path.exists(adobe_path):
cmd = [adobe_path, '/t', filepath, printer_name]
process = subprocess.run(cmd, capture_output=True, timeout=30)
if process.returncode == 0:
# Adobe Reader的命令行參數(shù)有限,基本設(shè)置通過系統(tǒng)打印對話框?qū)崿F(xiàn)
return True, f"已發(fā)送到 {printer_name} (使用Adobe Reader)"
else:
print(f"Adobe Reader打印失敗: {process.stderr.decode()}")
# 方法2: 使用win32print應(yīng)用更多高級設(shè)置并強制應(yīng)用
import win32print
import win32ui
import win32con
# 先設(shè)置為默認打印機,確保所有設(shè)置生效
old_default_printer = win32print.GetDefaultPrinter()
win32print.SetDefaultPrinter(printer_name)
try:
# 設(shè)置打印設(shè)備
hprinter = win32print.OpenPrinter(printer_name)
try:
# 設(shè)置打印屬性
printer_info = win32print.GetPrinter(hprinter, 2)
devmode = printer_info[1]
# 確保DEVMODE結(jié)構(gòu)具有正確的標志
devmode.Fields |= (win32con.DM_ORIENTATION | win32con.DM_COLOR |
win32con.DM_PRINTQUALITY | win32con.DM_PAPERSIZE |
win32con.DM_DUPLEX | win32con.DM_COPIES)
# 應(yīng)用打印方向
if orientation == 'landscape':
devmode.Orientation = win32con.DMORIENT_LANDSCAPE
else:
devmode.Orientation = win32con.DMORIENT_PORTRAIT
print(f"應(yīng)用打印方向: {orientation} -> {devmode.Orientation}")
# 應(yīng)用色彩模式
if color_mode == 'monochrome':
devmode.Color = 1 # 單色
else:
devmode.Color = 2 # 彩色
print(f"應(yīng)用色彩模式: {color_mode} -> {devmode.Color}")
# 應(yīng)用打印質(zhì)量
if '300' in quality:
devmode.PrintQuality = 300
elif '600' in quality:
devmode.PrintQuality = 600
elif '1200' in quality:
devmode.PrintQuality = 1200
print(f"應(yīng)用打印質(zhì)量: {quality} -> {devmode.PrintQuality}")
# 應(yīng)用紙張大小
try:
paper_size_id = int(papersize)
devmode.PaperSize = paper_size_id
print(f"應(yīng)用紙張大小ID: {paper_size_id}")
except ValueError:
print(f"無效的紙張大小ID: {papersize},使用默認值")
# 應(yīng)用雙面打印
if duplex == 2:
devmode.Duplex = win32con.DMDUP_HORIZONTAL # 雙面長邊
elif duplex == 3:
devmode.Duplex = win32con.DMDUP_VERTICAL # 雙面短邊
else:
devmode.Duplex = win32con.DMDUP_SIMPLEX # 單面打印
print(f"應(yīng)用雙面設(shè)置: {duplex} -> {devmode.Duplex}")
# 應(yīng)用打印份數(shù)
devmode.Copies = copies
print(f"應(yīng)用打印份數(shù): {copies}")
# 更新打印機屬性
win32print.SetPrinter(hprinter, 2, devmode, 0)
# 驗證設(shè)置是否正確應(yīng)用
updated_info = win32print.GetPrinter(hprinter, 2)
updated_devmode = updated_info[1]
print(f"設(shè)置應(yīng)用后驗證: 方向={updated_devmode.Orientation}, 色彩={updated_devmode.Color}, 紙張={updated_devmode.PaperSize}")
# 使用應(yīng)用了設(shè)置的打印機直接打印
print(f"使用ShellExecute打印文件: {filepath} 到 {printer_name}")
result = win32api.ShellExecute(
0,
"print",
filepath,
f'/d:"{printer_name}"',
".",
0
)
if result > 32:
return True, f"已發(fā)送到 {printer_name} (已應(yīng)用高級設(shè)置,設(shè)置驗證通過)"
else:
print(f"ShellExecute打印失敗,返回碼: {result}")
# 備用方案:使用win32print直接發(fā)送打印作業(yè)
try:
hjob = win32print.StartDocPrinter(hprinter, 1, (os.path.basename(filepath), None, "RAW"))
try:
win32print.StartPagePrinter(hprinter)
# 這里簡化處理,實際應(yīng)該讀取文件內(nèi)容并發(fā)送
win32print.EndPagePrinter(hprinter)
finally:
win32print.EndDocPrinter(hprinter)
return True, f"已發(fā)送到 {printer_name} (使用備用打印方法)"
except Exception as e:
print(f"備用打印方法失敗: {str(e)}")
raise
finally:
win32print.ClosePrinter(hprinter)
finally:
# 恢復原來的默認打印機
if old_default_printer:
win32print.SetDefaultPrinter(old_default_printer)
print(f"已恢復默認打印機: {old_default_printer}")
except Exception as e:
error_msg = f"PDF打印異常: {str(e)}"
print(error_msg)
# 作為最后的備用方案,使用基本的打印方式
try:
if os.path.exists(filepath):
win32api.ShellExecute(
0,
"print",
filepath,
f'/d:"{printer_name}"',
".",
0
)
return True, f"已發(fā)送到 {printer_name} (使用基礎(chǔ)打印功能)"
else:
return False, f"文件不存在: {filepath}"
except Exception as e2:
return False, f"所有打印方法均失敗: {str(e2)}"
# 其他打印相關(guān)函數(shù)保持不變(apply_printer_settings, print_pdf_with_settings等)
# 由于篇幅限制,這里省略具體實現(xiàn),保持原代碼不變
def get_printer_capabilities(printer_name):
"""獲取指定打印機的功能參數(shù)"""
try:
print(f"正在獲取打印機 '{printer_name}' 的實際參數(shù)...")
if not printer_name or printer_name.strip() == "" or printer_name == "未檢測到可用打印機":
print("打印機名稱無效")
return {
'duplex_support': False,
'color_support': False,
'papers': [],
'resolutions': [],
'printer_status': '離線或不可用',
'driver_name': '未知',
'port_name': ''
}
printer_handle = win32print.OpenPrinter(printer_name)
try:
printer_info = win32print.GetPrinter(printer_handle, 2)
driver_name = printer_info.get('pDriverName', '未知')
port_name = printer_info.get('pPortName', '未知')
status = printer_info.get('Status', 0)
printer_status = '在線'
if status != 0:
status_descriptions = {
0x00000001: '暫停', 0x00000002: '錯誤', 0x00000004: '正在刪除',
0x00000008: '缺紙', 0x00000010: '缺紙', 0x00000020: '手動送紙',
0x00000040: '紙張故障', 0x00000080: '離線', 0x00000100: 'I/O 活動',
0x00000200: '忙', 0x00000400: '正在打印', 0x00000800: '輸出槽滿',
0x00001000: '不可用', 0x00002000: '等待', 0x00004000: '正在處理',
0x00008000: '正在初始化', 0x00010000: '正在預熱', 0x00020000: '碳粉不足',
0x00040000: '沒有碳粉', 0x00080000: '頁面錯誤', 0x00100000: '用戶干預',
0x00200000: '內(nèi)存不足', 0x00400000: '門打開'
}
for status_bit, description in status_descriptions.items():
if status & status_bit:
printer_status = description
break
else:
printer_status = f'未知狀態(tài) ({status})'
duplex_support = False
color_support = False
papers = []
resolutions_list = []
try:
# 檢查雙面打印支持
try:
duplex_caps = win32print.DeviceCapabilities(printer_name, port_name, DC_DUPLEX, None)
duplex_support = duplex_caps == 1
print(f"雙面打印支持: {duplex_support}")
except Exception as e:
print(f"檢查雙面打印支持失敗: {e}")
duplex_support = False
# 檢查顏色支持
try:
color_caps = win32print.DeviceCapabilities(printer_name, port_name, DC_COLORDEVICE, None)
color_support = color_caps == 1
print(f"顏色打印支持: {color_support}")
except Exception as e:
print(f"檢查顏色支持失敗: {e}")
color_support = False
# 獲取支持的紙張
try:
paper_ids = win32print.DeviceCapabilities(printer_name, port_name, DC_PAPERS, None)
paper_names = win32print.DeviceCapabilities(printer_name, port_name, DC_PAPERNAMES, None)
if paper_ids and paper_names:
count = min(len(paper_ids), len(paper_names))
for i in range(count):
pid = paper_ids[i]
pname = paper_names[i]
if isinstance(pname, bytes):
try:
pname = pname.decode('mbcs', errors='ignore')
except Exception:
pname = str(pname)
pname = pname.replace('\x00', '').strip()
if pname:
papers.append({'id': int(pid), 'name': pname})
print(f"紙張列表: {papers[:8]}{' ...' if len(papers)>8 else ''}")
else:
print("未獲取到紙張列表")
except Exception as e:
print(f"獲取紙張列表失敗: {e}")
# 獲取打印分辨率
try:
resolutions = win32print.DeviceCapabilities(printer_name, port_name, DC_ENUMRESOLUTIONS, None)
if resolutions:
for res in resolutions:
if isinstance(res, dict):
xdpi = res.get('xdpi') or res.get('X') or 0
ydpi = res.get('ydpi') or res.get('Y') or 0
elif isinstance(res, (tuple, list)) and len(res) >= 2:
xdpi, ydpi = res[0], res[1]
else:
continue
if xdpi and ydpi:
resolutions_list.append(f"{xdpi}x{ydpi}")
print(f"分辨率列表: {resolutions_list}")
else:
print("未獲取到分辨率列表")
except Exception as e:
print(f"獲取分辨率失敗: {e}")
except Exception as e:
print(f"獲取設(shè)備功能時出錯: {e}")
capabilities = {
'duplex_support': duplex_support,
'color_support': color_support,
'papers': papers,
'resolutions': resolutions_list,
'printer_status': printer_status,
'driver_name': driver_name,
'port_name': port_name
}
print(f"最終獲取的打印機參數(shù): {capabilities}")
return capabilities
finally:
win32print.ClosePrinter(printer_handle)
except Exception as e:
print(f"無法訪問打印機 '{printer_name}': {e}")
return {
'duplex_support': False,
'color_support': False,
'papers': [],
'resolutions': [],
'printer_status': '離線或不可用',
'driver_name': '未知',
'port_name': ''
}
def get_logs():
if not os.path.exists(LOG_FILE):
return []
with open(LOG_FILE, 'r', encoding='utf-8') as f:
return f.readlines()[-10:][::-1]
@app.route('/api/printer_info')
def get_printer_info_api():
"""API端點:獲取指定打印機的信息"""
try:
printer_name = request.args.get('printer')
if not printer_name:
return jsonify({'success': False, 'error': '未指定打印機名稱'})
capabilities = get_printer_capabilities(printer_name)
return jsonify({
'success': True,
'capabilities': capabilities
})
except Exception as e:
return jsonify({
'success': False,
'error': str(e)
})
@app.route('/api/refresh_printers')
def refresh_printers_api():
"""API端點:刷新打印機列表"""
try:
success = refresh_printer_list()
if success:
default_printer = get_default_printer()
return jsonify({
'success': True,
'printers': PRINTERS,
'default_printer': default_printer,
'message': f'已刷新,檢測到 {len(PRINTERS)} 臺物理打印機'
})
else:
return jsonify({
'success': False,
'error': '刷新打印機列表失敗'
})
except Exception as e:
return jsonify({
'success': False,
'error': str(e)
})
@app.route('/', methods=['GET', 'POST'])
def upload_file():
files = os.listdir(UPLOAD_FOLDER)
logs = get_logs()
ip_config = get_current_ip_config()
suggested_ip = suggest_static_ip()
printer_caps = {}
if PRINTERS:
printer_caps = get_printer_capabilities(PRINTERS[0])
else:
printer_caps = {
'duplex_support': False,
'color_support': False,
'papers': [{'id': 9, 'name': 'A4 (210 x 297 mm)'}],
'resolutions': ['600x600'],
'printer_status': '無可用打印機',
'driver_name': '未知'
}
if request.method == 'POST':
action = request.form.get('action', 'print')
if action == 'set_static_ip':
ip_address = request.form.get('ip_address', '').strip()
subnet_mask = request.form.get('subnet_mask', '255.255.255.0').strip()
gateway = request.form.get('gateway', '').strip()
if not ip_address:
flash("請輸入有效的IP地址", "danger")
else:
try:
import ipaddress
ipaddress.IPv4Address(ip_address)
if subnet_mask:
ipaddress.IPv4Address(subnet_mask)
if gateway:
ipaddress.IPv4Address(gateway)
success, message = set_static_ip(ip_address, subnet_mask, gateway)
if success:
flash(message, "success")
time.sleep(2)
ip_config = get_current_ip_config()
else:
flash(message, "danger")
except Exception as e:
flash(f"IP地址格式無效: {str(e)}", "danger")
return redirect(url_for('upload_file'))
elif action == 'enable_dhcp':
success, message = set_dhcp()
if success:
flash(message, "success")
time.sleep(2)
ip_config = get_current_ip_config()
else:
flash(message, "danger")
return redirect(url_for('upload_file'))
elif action == 'print':
printer = request.form.get('printer')
copies = int(request.form.get('copies', 1))
duplex = int(request.form.get('duplex', 1))
papersize = request.form.get('papersize', '9')
quality = request.form.get('quality', '600x600')
# 新增的打印選項
color_mode = request.form.get('color_mode', 'color')
orientation = request.form.get('orientation', 'portrait')
scale = request.form.get('scale', 'original')
print_range = request.form.get('print_range', 'all')
page_range = request.form.get('page_range', '')
uploaded_files = request.files.getlist('file')
if not printer or printer == "" or printer == "未檢測到可用打印機":
flash("? 錯誤: 未選擇有效的打印機,請檢查打印機連接后重試!", "danger")
return redirect(url_for('upload_file'))
if not is_physical_printer(printer):
flash(f"?? 警告: '{printer}' 是虛擬打印機,不會進行實際打印,只會生成文件!", "warning")
for f in uploaded_files:
if f and allowed_file(f.filename):
filename = f.filename
filepath = os.path.join(UPLOAD_FOLDER, filename)
counter = 1
max_attempts = 100
while os.path.exists(filepath) and counter <= max_attempts:
name, ext = os.path.splitext(filename)
filepath = os.path.join(UPLOAD_FOLDER, f"{name}_{counter}{ext}")
counter += 1
if os.path.exists(filepath):
flash("文件名唯一性嘗試超過最大次數(shù),請重命名后再上傳!", "danger")
return redirect(url_for('upload_file'))
f.save(filepath)
try:
file_ext = os.path.splitext(filepath)[1].lower()
if file_ext == '.pdf':
success, message = print_pdf_silent(filepath, printer, copies, duplex, papersize, quality, color_mode, orientation, scale, print_range, page_range)
elif file_ext in ['.jpg', '.jpeg', '.png']:
success, message = print_image_silent(filepath, printer, copies, color_mode, orientation)
elif file_ext in ['.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx']:
success, message = print_office_silent(filepath, printer, copies, color_mode, orientation)
else:
success, message = print_file_with_settings(
filepath, printer, copies, duplex, papersize, quality, color_mode, orientation, scale
)
if success:
flash(f"? {os.path.basename(filepath)} {message}", "success")
log_print(os.path.basename(filepath), printer, copies, duplex, papersize, quality, color_mode, orientation, scale, print_range, page_range)
else:
flash(f"? 打印失敗: {message}", "danger")
log_print(os.path.basename(filepath) + f" 失敗: {message}", printer, copies, duplex, papersize, quality, color_mode, orientation, scale, print_range, page_range)
except Exception as e:
error_msg = f"打印異常: {str(e)}"
log_print(os.path.basename(filepath) + " " + error_msg, printer, copies, duplex, papersize, quality)
flash(f"?? {error_msg}", "danger")
return redirect(url_for('upload_file'))
default_printer = get_default_printer()
return render_template_string(HTML, printers=PRINTERS, files=files, logs=logs,
ip_config=ip_config, suggested_ip=suggested_ip,
printer_caps=printer_caps, default_printer=default_printer)
@app.route('/preview/<filename>')
def preview_file(filename):
fpath = os.path.join(UPLOAD_FOLDER, filename)
if not os.path.exists(fpath):
return f'<div class="alert alert-danger">文件未找到或已被自動清理!</div>', 404
ext = filename.rsplit('.', 1)[1].lower()
if ext in {'jpg', 'jpeg', 'png'}:
return send_from_directory(UPLOAD_FOLDER, filename, mimetype=f'image/{ext}')
elif ext == 'pdf':
return send_from_directory(UPLOAD_FOLDER, filename, mimetype='application/pdf')
elif ext == 'txt':
with open(fpath, 'r', encoding='utf-8') as f:
return f'<pre>{f.read()}</pre>'
else:
return '<div class="alert alert-warning">不支持預覽該文件類型</div>'
# 保留原有的系統(tǒng)托盤和啟動函數(shù)
def run_flask():
app.run(host='0.0.0.0', port=5000)
def run_wsgi():
try:
from waitress import serve
serve(app, host='0.0.0.0', port=5000)
except ImportError:
print("Waitress未安裝,使用Flask內(nèi)置服務(wù)器")
app.run(host='0.0.0.0', port=5000)
def on_quit(icon, item):
icon.stop()
import threading
for t in threading.enumerate():
if t is not threading.current_thread():
try:
t.join(timeout=2)
except Exception:
pass
sys.exit(0)
def on_toggle_autostart(icon, item):
current = get_autostart()
set_autostart(not current)
icon.menu = build_menu(icon)
def on_show_ip_config(icon, item):
import webbrowser
ip = get_local_ip()
port = 5000
url = f"http://{ip}:{port}/"
webbrowser.open(url)
def build_menu(icon):
autostart = get_autostart()
ip = get_local_ip()
port = 5000
ip_config = get_current_ip_config()
ip_status = f"當前IP: {ip}"
if ip_config:
if ip_config['dhcp_enabled']:
ip_status += " (DHCP)"
else:
ip_status += " (靜態(tài))"
return pystray.Menu(
pystray.MenuItem(f'服務(wù)地址: {ip}:{port}', on_show_ip_config),
pystray.MenuItem(ip_status, None, enabled=False),
pystray.Menu.SEPARATOR,
pystray.MenuItem('打開配置頁面', on_show_ip_config),
pystray.MenuItem('開機自啟:' + ('已開啟' if autostart else '未開啟'), on_toggle_autostart),
pystray.Menu.SEPARATOR,
pystray.MenuItem('退出', on_quit)
)
def setup_tray():
try:
logo_path = resource_path('logo.ico')
print(f"嘗試加載圖標: {logo_path}")
if not os.path.exists(logo_path):
print(f"錯誤:logo.ico文件不存在于路徑: {logo_path}")
return
image = Image.open(logo_path)
print(f"成功加載logo.ico文件,尺寸: {image.size}")
icon = pystray.Icon('print_server', image, '局域網(wǎng)打印服務(wù)')
icon.menu = build_menu(icon)
print("系統(tǒng)托盤啟動成功")
icon.run()
except Exception as e:
print(f"系統(tǒng)托盤啟動失敗: {e}")
import time
try:
while True:
time.sleep(1)
except KeyboardInterrupt:
print("程序被用戶中斷")
sys.exit(0)
if __name__ == '__main__':
print("=" * 50)
print("局域網(wǎng)打印服務(wù)啟動中...")
print("=" * 50)
local_ip = get_local_ip()
if local_ip == '127.0.0.1':
print("?? 網(wǎng)絡(luò)狀態(tài): 離線模式")
print(" - 程序仍可正常工作")
print(" - 使用默認打印機配置")
else:
print(f"? 網(wǎng)絡(luò)狀態(tài): 在線 (IP: {local_ip})")
print(" - 完整功能可用")
print(f"??? 檢測到 {len(PRINTERS)} 臺物理打印機")
if PRINTERS:
for i, printer in enumerate(PRINTERS[:3], 1):
print(f" {i}. {printer}")
if len(PRINTERS) > 3:
print(f" ... 還有 {len(PRINTERS) - 3} 臺打印機")
else:
print(" ?? 未檢測到可用的物理打印機")
print("?? 服務(wù)器將啟動在: http://{}:5000".format(local_ip))
print("=" * 50)
cleaner_thread = threading.Thread(target=clean_old_files, daemon=True)
cleaner_thread.start()
if os.environ.get('USE_WSGI', '').lower() == 'true':
flask_thread = threading.Thread(target=run_wsgi, daemon=True)
else:
flask_thread = threading.Thread(target=run_flask, daemon=True)
flask_thread.start()
setup_tray()
快速啟動腳本
創(chuàng)建start_server.bat:
@echo off
chcp 65001
title 局域網(wǎng)打印服務(wù)系統(tǒng)
echo 正在啟動打印服務(wù)...
cd /d %~dp0
REM 檢查Python環(huán)境
python --version >nul 2>&1
if errorlevel 1 (
echo 錯誤: 未找到Python環(huán)境,請先安裝Python 3.7+
pause
exit /b 1
)
REM 安裝依賴
echo 檢查并安裝依賴...
pip install -r requirements.txt
REM 啟動服務(wù)
echo 啟動打印服務(wù)...
python src/main.py
pause
Docker部署方案
創(chuàng)建Dockerfile:
FROM python:3.9-windows WORKDIR /app COPY requirements.txt . RUN pip install -r requirements.txt COPY . . EXPOSE 5000 CMD ["python", "src/main.py"]
應(yīng)用場景與價值
企業(yè)辦公環(huán)境
- 中小型企業(yè):替代昂貴的專業(yè)打印服務(wù)器
- 教育機構(gòu):計算機教室、圖書館共享打印
- 政府部門:安全可控的內(nèi)部文件打印
特殊使用場景
- 臨時辦公點:快速搭建打印環(huán)境
- 活動現(xiàn)場:照片、文檔即時打印
- 開發(fā)測試:模擬多打印機環(huán)境
經(jīng)濟效益分析
與傳統(tǒng)打印解決方案對比:
| 項目 | 傳統(tǒng)方案 | 本系統(tǒng) | 節(jié)省 |
|---|---|---|---|
| 硬件成本 | 專用服務(wù)器(¥5000+) | 普通PC(¥0) | ¥5000+ |
| 軟件授權(quán) | 商業(yè)軟件(¥2000+/年) | 開源免費(¥0) | ¥2000+/年 |
| 維護成本 | 專業(yè)IT支持 | 簡單配置 | 90%時間節(jié)省 |
| 部署時間 | 數(shù)天 | 數(shù)分鐘 | 95%時間節(jié)省 |
未來發(fā)展規(guī)劃
短期優(yōu)化目標
- 用戶體驗提升:增加拖拽排序、批量操作等便捷功能
- 移動端APP:開發(fā)專門的移動端應(yīng)用程序
- 云打印集成:支持Google Cloud Print等云服務(wù)
中長期規(guī)劃
- AI智能優(yōu)化:基于使用習慣的智能參數(shù)推薦
- 跨平臺支持:擴展至Linux和macOS系統(tǒng)
- 企業(yè)級特性:用戶權(quán)限管理、打印配額控制
總結(jié)與展望
本文詳細介紹的局域網(wǎng)智能打印服務(wù)系統(tǒng),通過技術(shù)創(chuàng)新解決了傳統(tǒng)打印中的諸多痛點。系統(tǒng)具備以下核心優(yōu)勢:
技術(shù)優(yōu)勢
- 高度集成化:將復雜打印功能封裝為簡單Web服務(wù)
- 智能自動化:自動識別、過濾、配置,減少人工干預
- 健壯可靠:多重錯誤處理和備用方案確保服務(wù)連續(xù)性
實用價值
- 成本極低:利用現(xiàn)有設(shè)備,零額外硬件投入
- 部署簡單:一鍵啟動,無需專業(yè)IT知識
- 維護方便:自動更新、自監(jiān)控、自修復
社會意義
該系統(tǒng)的推廣使用將有助于:
- 降低中小企業(yè)信息化門檻
- 促進辦公資源的合理共享
- 推動綠色辦公理念的實踐
未來展望:隨著物聯(lián)網(wǎng)和人工智能技術(shù)的發(fā)展,打印服務(wù)將更加智能化、個性化。本系統(tǒng)為這一演進方向提供了堅實的技術(shù)基礎(chǔ)和實踐案例。
以上就是Python+Flask開發(fā)局域網(wǎng)智能打印服務(wù)系統(tǒng)的詳細內(nèi)容,更多關(guān)于Python局域網(wǎng)打印的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Starship定制shell提示符實現(xiàn)信息自由
這篇文章主要介紹了Starship定制shell提示符的實現(xiàn),讓你需要的所有信息觸手可及,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-03-03
解決ImportError:DLL load failed while impo
在安裝pywin32后,可能會出現(xiàn)無法導入win32api的錯誤,一個有效的解決方案是運行pywin32_postinstall.py腳本,首先,打開cmd并切換到環(huán)境的Scripts文件夾,確保存在pywin32_postinstall.py文件2024-09-09
Python數(shù)據(jù)可視化真正好用的3個庫詳解
Python 畫圖庫怎么這么多?Matplotlib、Seaborn、Plotly、Pyecharts、ggplot、pyqtgraph、vispy、bokeh……都快被繞暈了,所以,今天我就來給大家整理一下——Python 數(shù)據(jù)可視化,真正好用的就這 3 個庫:Seaborn、Plotly、Pyecharts,感興趣的小伙伴跟著小編一起來看看吧2025-04-04

