-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathprint_server.py
More file actions
443 lines (362 loc) · 17.6 KB
/
print_server.py
File metadata and controls
443 lines (362 loc) · 17.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
import os
import sys
import json
import time
import win32print
import win32ui
import win32con
import win32api
import win32gui
import tkinter as tk
from tkinter import ttk, messagebox, Canvas, Scrollbar
from PIL import Image, ImageTk, ImageWin
import fitz
from flask import Flask, request, jsonify
from flask_cors import CORS
from threading import Thread
app = Flask(__name__)
CORS(app)
# 确保上传文件目录存在
UPLOAD_FOLDER = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'uploads')
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
class PrintPreviewWindow:
def __init__(self, filename):
self.filename = filename
self.root = tk.Tk()
self.root.title("打印预览")
self.root.geometry("800x600")
self.current_page = 0
self.total_pages = 0
self.zoom_level = 1.0
# 创建主框架
self.main_frame = ttk.Frame(self.root)
self.main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
# 左侧预览区域
self.preview_frame = ttk.Frame(self.main_frame)
self.preview_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
# 创建标尺和预览区域
self.ruler_size = 20 # 标尺宽度
# 水平标尺
self.h_ruler = Canvas(self.preview_frame, height=self.ruler_size, bg='white')
self.h_ruler.pack(side=tk.TOP, fill=tk.X)
# 垂直标尺
self.v_ruler = Canvas(self.preview_frame, width=self.ruler_size, bg='white')
self.v_ruler.pack(side=tk.LEFT, fill=tk.Y)
# 创建预览画布和滚动条
self.canvas = Canvas(self.preview_frame, bg='white')
self.scrollbar = Scrollbar(self.preview_frame, orient=tk.VERTICAL, command=self.canvas.yview)
self.canvas.configure(yscrollcommand=self.scrollbar.set)
self.scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
self.canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
# 绑定画布滚动事件
self.canvas.bind('<Configure>', self.update_rulers)
self.canvas.bind('<Motion>', self.update_ruler_indicators)
# 右侧打印设置区域
self.settings_frame = ttk.LabelFrame(self.main_frame, text="打印设置", padding=10)
self.settings_frame.pack(side=tk.RIGHT, fill=tk.Y, padx=(10, 0))
# 打印机选择
ttk.Label(self.settings_frame, text="选择打印机:").pack(anchor=tk.W)
self.printer_var = tk.StringVar()
self.printer_combo = ttk.Combobox(self.settings_frame, textvariable=self.printer_var)
self.printer_combo.pack(fill=tk.X, pady=(0, 10))
# 纸张大小设置
ttk.Label(self.settings_frame, text="纸张大小:").pack(anchor=tk.W)
self.paper_size_var = tk.StringVar()
self.paper_sizes = {
'A4': (210, 297), # 毫米
'A3': (297, 420),
'A5': (148, 210),
'B4': (250, 353),
'B5': (176, 250)
}
self.paper_size_combo = ttk.Combobox(self.settings_frame, textvariable=self.paper_size_var, values=list(self.paper_sizes.keys()))
self.paper_size_combo.pack(fill=tk.X, pady=(0, 10))
self.paper_size_var.set('A4') # 默认A4
self.paper_size_combo.bind('<<ComboboxSelected>>', lambda e: self.update_page_display())
# 份数设置
ttk.Label(self.settings_frame, text="打印份数:").pack(anchor=tk.W)
self.copies_var = tk.StringVar(value="1")
copies_spinbox = ttk.Spinbox(self.settings_frame, from_=1, to=99, textvariable=self.copies_var)
copies_spinbox.pack(fill=tk.X, pady=(0, 10))
# 页面导航区域
self.nav_frame = ttk.Frame(self.settings_frame)
self.nav_frame.pack(fill=tk.X, pady=(0, 10))
ttk.Button(self.nav_frame, text="上一页", command=self.prev_page).pack(side=tk.LEFT, padx=2)
self.page_label = ttk.Label(self.nav_frame, text="0/0")
self.page_label.pack(side=tk.LEFT, padx=2)
ttk.Button(self.nav_frame, text="下一页", command=self.next_page).pack(side=tk.LEFT, padx=2)
# 缩放控制
self.zoom_frame = ttk.Frame(self.settings_frame)
self.zoom_frame.pack(fill=tk.X, pady=(0, 10))
ttk.Button(self.zoom_frame, text="放大", command=lambda: self.zoom(1.2)).pack(side=tk.LEFT, padx=2)
ttk.Button(self.zoom_frame, text="缩小", command=lambda: self.zoom(0.8)).pack(side=tk.LEFT, padx=2)
# 按钮区域
self.button_frame = ttk.Frame(self.settings_frame)
self.button_frame.pack(fill=tk.X, pady=(10, 0))
ttk.Button(self.button_frame, text="打印", command=self.print_document).pack(fill=tk.X, pady=2)
ttk.Button(self.button_frame, text="取消", command=self.close_window).pack(fill=tk.X, pady=2)
# 加载打印机列表
self.load_printers()
# 加载预览
self.load_preview()
# 等待窗口完全加载
self.root.update()
# 如果是PDF文件,确保立即显示第一页
if hasattr(self, 'doc'):
self.update_page_display()
self.root.protocol("WM_DELETE_WINDOW", self.close_window)
self.root.mainloop()
def load_printers(self):
printers = [printer[2] for printer in win32print.EnumPrinters(win32print.PRINTER_ENUM_LOCAL)]
default_printer = win32print.GetDefaultPrinter()
self.printer_combo['values'] = printers
self.printer_var.set(default_printer)
def load_preview(self):
try:
ext = os.path.splitext(self.filename)[1].lower()
print(f"[INFO] 加载预览文件:{self.filename}")
if ext in ['.pdf']:
self.load_pdf_preview()
elif ext in ['.png', '.jpg', '.jpeg', '.bmp']:
self.load_image_preview()
else:
raise ValueError(f"不支持的文件类型:{ext}")
except Exception as e:
error_msg = f"无法加载预览:{str(e)}"
print(f"[ERROR] {error_msg}")
import traceback
print(f"[ERROR] 异常堆栈:\n{traceback.format_exc()}")
messagebox.showerror("错误", error_msg)
def load_pdf_preview(self):
try:
print(f"[INFO] 加载PDF文件:{self.filename}")
self.doc = fitz.open(self.filename)
self.total_pages = len(self.doc)
print(f"[INFO] PDF总页数:{self.total_pages}")
self.update_page_display()
except Exception as e:
error_msg = f"无法加载PDF文件:{str(e)}"
print(f"[ERROR] {error_msg}")
import traceback
print(f"[ERROR] 异常堆栈:\n{traceback.format_exc()}")
messagebox.showerror("错误", error_msg)
def update_rulers(self, event=None):
# 更新标尺刻度
self.h_ruler.delete('all')
self.v_ruler.delete('all')
# 获取当前纸张尺寸(毫米)
paper_width, paper_height = self.paper_sizes[self.paper_size_var.get()]
# 计算像素与毫米的比例
canvas_width = self.canvas.winfo_width()
canvas_height = self.canvas.winfo_height()
scale_x = canvas_width / paper_width
scale_y = canvas_height / paper_height
# 绘制水平标尺
for x in range(0, paper_width + 1, 10): # 每10毫米一个刻度
x_pos = x * scale_x
height = 5 if x % 50 == 0 else (3 if x % 10 == 0 else 2)
self.h_ruler.create_line(x_pos, self.ruler_size, x_pos, self.ruler_size - height)
if x % 50 == 0: # 每50毫米显示数字
self.h_ruler.create_text(x_pos, self.ruler_size - 10, text=str(x))
# 绘制垂直标尺
for y in range(0, paper_height + 1, 10):
y_pos = y * scale_y
width = 5 if y % 50 == 0 else (3 if y % 10 == 0 else 2)
self.v_ruler.create_line(self.ruler_size, y_pos, self.ruler_size - width, y_pos)
if y % 50 == 0:
self.v_ruler.create_text(self.ruler_size - 10, y_pos, text=str(y))
def update_ruler_indicators(self, event):
# 更新标尺指示器
self.h_ruler.delete('indicator')
self.v_ruler.delete('indicator')
# 创建十字线指示器
self.h_ruler.create_line(event.x, 0, event.x, self.ruler_size, fill='red', tags='indicator')
self.v_ruler.create_line(0, event.y, self.ruler_size, event.y, fill='red', tags='indicator')
def update_page_display(self):
try:
# 确保窗口已完全加载
self.root.update()
# 清除画布
self.canvas.delete("all")
# 更新页码显示
self.page_label.config(text=f"{self.current_page + 1}/{self.total_pages}")
# 获取当前页面
page = self.doc[self.current_page]
# 计算缩放比例
canvas_width = self.canvas.winfo_width() or 600
canvas_height = self.canvas.winfo_height() or 800
# 获取页面大小
rect = page.rect
page_width = rect.width
page_height = rect.height
# 计算适合画布的缩放比例
scale_x = canvas_width / page_width
scale_y = canvas_height / page_height
base_scale = min(scale_x, scale_y) * 0.9 # 留出一些边距
# 应用缩放级别
zoom_scale = base_scale * self.zoom_level
# 渲染页面
mat = fitz.Matrix(zoom_scale, zoom_scale)
pix = page.get_pixmap(matrix=mat)
# 转换为PIL图像
img = Image.frombytes("RGB", [pix.width, pix.height], pix.samples)
# 转换为PhotoImage
self.photo = ImageTk.PhotoImage(img)
# 在画布中央显示图像
canvas_center_x = canvas_width / 2
canvas_center_y = canvas_height / 2
self.canvas.create_image(canvas_center_x, canvas_center_y, image=self.photo, anchor=tk.CENTER)
# 更新滚动区域
self.canvas.configure(scrollregion=self.canvas.bbox(tk.ALL))
except Exception as e:
messagebox.showerror("错误", f"无法显示PDF页面:{str(e)}")
def prev_page(self):
if self.current_page > 0:
self.current_page -= 1
self.update_page_display()
def next_page(self):
if self.current_page < self.total_pages - 1:
self.current_page += 1
self.update_page_display()
def zoom(self, factor):
self.zoom_level *= factor
self.update_page_display()
def load_image_preview(self):
image = Image.open(self.filename)
# 调整图片大小以适应预览区域
canvas_width = self.canvas.winfo_width() or 600
ratio = canvas_width / image.width
new_size = (int(image.width * ratio), int(image.height * ratio))
image = image.resize(new_size, Image.Resampling.LANCZOS)
self.photo = ImageTk.PhotoImage(image)
self.canvas.create_image(0, 0, anchor=tk.NW, image=self.photo)
self.canvas.configure(scrollregion=self.canvas.bbox(tk.ALL))
def print_document(self):
try:
printer_name = self.printer_var.get()
copies = int(self.copies_var.get())
paper_size = self.paper_size_var.get()
print(f"[INFO] 开始打印文档:打印机={printer_name}, 份数={copies}, 纸张大小={paper_size}")
# 获取打印机句柄
hprinter = win32print.OpenPrinter(printer_name)
devmode = win32print.GetPrinter(hprinter, 2)["pDevMode"]
# 设置纸张大小
paper_sizes = {
'A4': win32con.DMPAPER_A4,
'A3': win32con.DMPAPER_A3,
'A5': win32con.DMPAPER_A5,
'B4': win32con.DMPAPER_B4,
'B5': win32con.DMPAPER_B5
}
devmode.PaperSize = paper_sizes.get(paper_size, win32con.DMPAPER_A4)
# 如果是PDF文件,逐页打印
if hasattr(self, 'doc'):
for _ in range(copies):
# 创建打印DC
hdc = win32gui.CreateDC("WINSPOOL", printer_name, devmode)
dc = win32ui.CreateDCFromHandle(hdc)
try:
# 开始打印作业
dc.StartDoc('PDF Print Job')
for page_num in range(self.total_pages):
page = self.doc[page_num]
pix = page.get_pixmap()
img = Image.frombytes("RGB", [pix.width, pix.height], pix.samples)
# 开始新页面
dc.StartPage()
# 获取打印机分辨率
printer_dpi_x = dc.GetDeviceCaps(win32con.LOGPIXELSX)
printer_dpi_y = dc.GetDeviceCaps(win32con.LOGPIXELSY)
# 计算打印区域
page_width = int(pix.width * printer_dpi_x / 72)
page_height = int(pix.height * printer_dpi_y / 72)
# 将图像转换为Windows位图
dib = ImageWin.Dib(img)
dib.draw(hdc, (0, 0, page_width, page_height))
dc.EndPage()
# 结束打印作业
dc.EndDoc()
finally:
dc.DeleteDC()
# win32gui.DeleteDC(hdc)
else: # 如果是图片文件
for _ in range(copies):
img = Image.open(self.filename)
# 创建打印DC
hdc = win32gui.CreateDC("WINSPOOL", printer_name, devmode)
dc = win32ui.CreateDCFromHandle(hdc)
try:
# 开始打印
dc.StartDoc('Image Print Job')
dc.StartPage()
# 获取打印机分辨率
printer_dpi_x = dc.GetDeviceCaps(win32con.LOGPIXELSX)
printer_dpi_y = dc.GetDeviceCaps(win32con.LOGPIXELSY)
# 计算打印区域
page_width = int(img.width * printer_dpi_x / 72)
page_height = int(img.height * printer_dpi_y / 72)
# 将图像转换为Windows位图
dib = ImageWin.Dib(img)
dib.draw(hdc, (0, 0, page_width, page_height))
dc.EndPage()
dc.EndDoc()
finally:
dc.DeleteDC()
win32gui.DeleteDC(hdc)
win32print.ClosePrinter(hprinter)
messagebox.showinfo("打印", f"文档已成功发送到打印机 {printer_name}")
self.close_window()
except Exception as e:
error_msg = f"打印失败:{str(e)}"
print(f"[ERROR] {error_msg}")
import traceback
print(f"[ERROR] 异常堆栈:\n{traceback.format_exc()}")
messagebox.showerror("错误", error_msg)
def close_window(self):
try:
if os.path.exists(self.filename):
os.remove(self.filename)
except Exception:
pass
self.root.destroy()
def show_print_preview(filename):
try:
PrintPreviewWindow(filename)
except Exception as e:
messagebox.showerror("错误", str(e))
if os.path.exists(filename):
os.remove(filename)
@app.route('/api/printers', methods=['GET'])
def get_printers():
printers = []
for printer in win32print.EnumPrinters(win32print.PRINTER_ENUM_LOCAL):
printers.append({
'name': printer[2],
'isDefault': printer[2] == win32print.GetDefaultPrinter()
})
return jsonify(printers)
@app.route('/api/print', methods=['POST'])
def print_file():
try:
print("[INFO] 接收到打印请求")
if 'file' not in request.files:
error_msg = "未找到上传文件"
print(f"[ERROR] {error_msg}")
return jsonify({'error': error_msg}), 400
file = request.files['file']
if not file:
error_msg = "文件无效"
print(f"[ERROR] {error_msg}")
return jsonify({'error': error_msg}), 400
# 保存上传的文件
filename = os.path.join(UPLOAD_FOLDER, file.filename)
file.save(filename)
# 在新线程中打开自定义打印预览
Thread(target=lambda: show_print_preview(filename)).start()
return jsonify({'message': '正在打开打印预览'})
except Exception as e:
if 'filename' in locals() and os.path.exists(filename):
os.remove(filename)
return jsonify({'error': str(e)}), 500
if __name__ == '__main__':
app.run(host='127.0.0.1', port=5000)