python实现PDF处理多合一工具箱
基于python,实现高频使用的PDF文件处理功能,用于替代Adobe Acrobat DC
以下代码,仅限于个人研究学习使用,可以实现PDF文件解密、指定页面导出提取为单独的PDF、多个PDF文件合并、PDF转图片、图片合成PDF文件、PDF书签目录制作功能(仅仅满足可用,有和没有的区别),仅限于个人拥有全部所有权的文件。请勿用于非法用途,若有非法使用,本人概不负责。
个人产生这个需求的情景:出于尊重版权的考虑,我想开发一个程序,可以满足个人在日常办公一些高频使用情景,处理各类PDF文件的需求,比如实现PDF文件解密、指定页面导出提取为单独的PDF、多个PDF文件合并、PDF转图片、图片合成PDF文件、PDF书签目录制作功能。经过一番查找答案,使用AI,询问AI,最终得到了一个基本可用的如下代码,可以满足基本使用需求。仅限于个人研究学习使用,请勿用作非法用途。
python安装:
去python官网,下载并安装python可执行程序。
依赖安装:
pip install PyQt5 PyMuPDF PyPDF2 pikepdf Pillow
基本可用代码如下:
import sys
import os
import re
import fitz
from concurrent.futures import ThreadPoolExecutor
from PIL import Image, ImageOps
from PyQt5.QtWidgets import (QApplication, QMainWindow, QFileDialog, QMessageBox,
QLabel, QLineEdit, QComboBox, QRadioButton, QPushButton,
QHBoxLayout, QVBoxLayout, QGroupBox, QWidget, QTabWidget,
QListWidget, QListWidgetItem, QAbstractItemView, QCheckBox,
QSlider, QProgressDialog, QSplitter, QTreeWidget, QTreeWidgetItem,
QSpinBox)
from PyQt5.QtCore import QThread, pyqtSignal, Qt, QDir, QSize, QMimeData
from PyQt5.QtGui import QDragEnterEvent, QDropEvent, QPixmap, QImage
from PyPDF2 import PdfReader, PdfWriter, PdfMerger
from pikepdf import Pdf, Encryption, Permissions
# PDF转图片功能的工作线程
class WorkerThread(QThread):
finished = pyqtSignal(bool, str)
error = pyqtSignal(str)
def __init__(self, parent=None):
super().__init__(parent)
self.params = {
'dpi': 300,
'alpha': False,
'colorspace': fitz.csRGB,
'output_dir': ''
}
def set_params(self, file_path, output_dir, pages, export_pdf, img_format, dpi):
self.params.update({
'file_path': file_path,
'output_dir': output_dir,
'pages': self._parse_pages(pages),
'export_pdf': export_pdf,
'img_format': img_format.lower(),
'dpi': dpi,
'alpha': (img_format.lower() == 'png')
})
def _parse_pages(self, pages_str):
pages = []
for part in pages_str.replace(',', ',').split(','):
part = part.strip()
if not part:
continue
if '-' in part:
start_end = part.split('-')
if len(start_end) != 2:
raise ValueError("无效的页码范围格式")
start = int(start_end[0])
end = int(start_end[1])
pages.extend(range(start, end+1))
else:
pages.append(int(part))
return sorted(set(pages))
def run(self):
try:
doc = fitz.open(self.params['file_path'])
total_pages = doc.page_count
valid_pages = [p-1 for p in self.params['pages'] if 0 < p <= total_pages]
if not valid_pages:
raise ValueError("所有页码均超出文档范围")
if self.params['export_pdf']:
self._export_pdf(doc, valid_pages)
else:
self._export_images(doc, valid_pages)
doc.close()
self.finished.emit(True, self.params['output_dir'])
except Exception as e:
self.error.emit(str(e))
def _export_pdf(self, doc, pages):
new_doc = fitz.open()
for p in pages:
new_doc.insert_pdf(doc, from_page=p, to_page=p)
output_path = os.path.join(self.params['output_dir'], 'extracted.pdf')
new_doc.save(output_path, garbage=3, deflate=True)
new_doc.close()
def _export_images(self, doc, pages):
for idx, p in enumerate(pages):
page = doc[p]
pix = page.get_pixmap(
dpi=self.params['dpi'],
alpha=self.params['alpha'],
colorspace=self.params['colorspace'],
annots=True,
)
output_path = os.path.join(
self.params['output_dir'],
f'page_{p+1}_dpi{self.params["dpi"]}.{self.params["img_format"]}'
)
if self.params['img_format'] == 'jpg':
pix.save(output_path, jpg_quality=95)
else:
pix.save(output_path)
# 支持拖放的自定义输入框
class DragDropLineEdit(QLineEdit):
def __init__(self, parent=None):
super().__init__(parent)
self.setAcceptDrops(True)
def dragEnterEvent(self, event):
if event.mimeData().hasUrls():
for url in event.mimeData().urls():
if url.isLocalFile() and url.toLocalFile().lower().endswith('.pdf'):
event.acceptProposedAction()
return
event.ignore()
def dropEvent(self, event):
for url in event.mimeData().urls():
file_path = url.toLocalFile()
if file_path.lower().endswith('.pdf'):
self.setText(file_path)
break
# PDF转图片功能模块
class PdfToolApp(QWidget):
def __init__(self):
super().__init__()
self.init_ui()
self.worker = WorkerThread()
self._connect_signals()
self.setAcceptDrops(True)
def init_ui(self):
# 文件选择组
file_group = QGroupBox("文件操作")
self.txt_file = QLineEdit()
self.txt_file.setPlaceholderText("拖拽PDF文件到此或点击浏览...")
btn_browse = QPushButton("浏览")
btn_browse.clicked.connect(self._browse_file)
# 输出设置组
output_group = QGroupBox("输出设置")
self.txt_output = QLineEdit()
btn_output = QPushButton("选择目录")
btn_output.clicked.connect(self._browse_dir)
self.rad_pdf = QRadioButton("导出为PDF")
self.rad_img = QRadioButton("导出为图片")
self.rad_pdf.setChecked(True)
self.cmb_format = QComboBox()
self.cmb_format.addItems(['PNG', 'JPG'])
self.cmb_dpi = QComboBox()
self.cmb_dpi.addItems(['150 (网页)', '300 (印刷)', '600 (高清)'])
self.txt_pages = QLineEdit()
self.txt_pages.setPlaceholderText("输入页码范围(如:1-3 或 1,3,5)")
# 布局
file_layout = QHBoxLayout()
file_layout.addWidget(QLabel("PDF文件:"))
file_layout.addWidget(self.txt_file)
file_layout.addWidget(btn_browse)
file_group.setLayout(file_layout)
output_layout = QVBoxLayout()
output_layout.addWidget(QLabel("输出目录:"))
output_layout.addWidget(self.txt_output)
output_layout.addWidget(btn_output)
pages_layout = QHBoxLayout()
pages_layout.addWidget(QLabel("页码范围:"))
pages_layout.addWidget(self.txt_pages)
format_layout = QHBoxLayout()
format_layout.addWidget(self.rad_pdf)
format_layout.addWidget(self.rad_img)
format_layout.addWidget(self.cmb_format)
format_layout.addWidget(QLabel("DPI:"))
format_layout.addWidget(self.cmb_dpi)
output_group.setLayout(output_layout)
output_layout.addLayout(pages_layout)
output_layout.addLayout(format_layout)
# 主布局
main_layout = QVBoxLayout()
main_layout.addWidget(file_group)
main_layout.addWidget(output_group)
self.setLayout(main_layout)
def _connect_signals(self):
# 确保初始状态下,如果选择了PDF导出,则禁用格式选择
self.cmb_format.setEnabled(self.rad_img.isChecked())
self.rad_pdf.toggled.connect(lambda: self.cmb_format.setEnabled(False))
self.rad_img.toggled.connect(lambda: self.cmb_format.setEnabled(True))
btn_execute = QPushButton("开始转换", self)
btn_execute.clicked.connect(self._start_conversion)
self.layout().addWidget(btn_execute)
self.worker.finished.connect(self._on_finished)
self.worker.error.connect(self._show_error)
def dragEnterEvent(self, event: QDragEnterEvent):
if event.mimeData().hasUrls():
event.acceptProposedAction()
def dropEvent(self, event: QDropEvent):
for url in event.mimeData().urls():
if url.isLocalFile() and url.fileName().lower().endswith('.pdf'):
self.txt_file.setText(os.path.normpath(url.toLocalFile()))
break
def _browse_file(self):
path, _ = QFileDialog.getOpenFileName(self, "选择PDF文件", "", "PDF Files (*.pdf)")
if path:
self.txt_file.setText(path)
def _browse_dir(self):
path = QFileDialog.getExistingDirectory(self, "选择输出目录")
if path:
self.txt_output.setText(path)
def _start_conversion(self):
params = {
'file_path': self.txt_file.text(),
'output_dir': self.txt_output.text(),
'pages': self.txt_pages.text() or "1-1",
'export_pdf': self.rad_pdf.isChecked(),
'img_format': self.cmb_format.currentText(),
'dpi': int(self.cmb_dpi.currentText().split()[0])
}
if not os.path.exists(params['file_path']):
QMessageBox.warning(self, "错误", "PDF文件路径无效")
return
if not os.path.exists(params['output_dir']):
os.makedirs(params['output_dir'], exist_ok=True)
try:
_ = self.worker._parse_pages(params['pages'])
except ValueError as e:
QMessageBox.critical(self, "输入错误", f"页码格式错误:{str(e)}")
return
self.worker.set_params(**params)
self.worker.start()
def _on_finished(self, success, output_dir):
QMessageBox.information(self, "完成", f"文件已保存至:\n{output_dir}")
def _show_error(self, msg):
QMessageBox.critical(self, "错误", f"处理失败:{msg}")
# PDF密码解除与加密功能模块
class PDFToolsUI(QWidget):
def __init__(self):
super().__init__()
self.init_ui()
def init_ui(self):
"""界面初始化"""
tabs = QTabWidget()
layout = QVBoxLayout(self)
layout.addWidget(tabs)
# 解密模块
decrypt_tab = QWidget()
self.decrypt_ui(decrypt_tab)
tabs.addTab(decrypt_tab, "密码解除")
# 加密模块
encrypt_tab = QWidget()
self.encrypt_ui(encrypt_tab)
tabs.addTab(encrypt_tab, "文件加密")
# 合并模块
merge_tab = QWidget()
self.merge_ui(merge_tab)
tabs.addTab(merge_tab, "文件合并")
def decrypt_ui(self, tab):
"""密码解除界面"""
layout = QVBoxLayout()
# 文件选择(支持拖放)
file_layout = QHBoxLayout()
self.decrypt_input = DragDropLineEdit()
btn_choose = QPushButton("选择文件")
btn_choose.clicked.connect(lambda: self.choose_file(self.decrypt_input))
file_layout.addWidget(QLabel("输入文件:"))
file_layout.addWidget(self.decrypt_input)
file_layout.addWidget(btn_choose)
# 密码输入
self.decrypt_pass = QLineEdit()
self.decrypt_pass.setEchoMode(QLineEdit.Password)
# 操作按钮
btn_decrypt = QPushButton("开始解密")
btn_decrypt.clicked.connect(self.handle_decrypt)
layout.addLayout(file_layout)
layout.addWidget(QLabel("输入密码:"))
layout.addWidget(self.decrypt_pass)
layout.addWidget(btn_decrypt)
tab.setLayout(layout)
def encrypt_ui(self, tab):
"""文件加密界面"""
layout = QVBoxLayout()
# 输入文件选择(支持拖放)
input_layout = QHBoxLayout()
self.encrypt_input = DragDropLineEdit()
btn_input = QPushButton("选择文件")
btn_input.clicked.connect(lambda: self.choose_file(self.encrypt_input))
input_layout.addWidget(QLabel("输入文件:"))
input_layout.addWidget(self.encrypt_input)
input_layout.addWidget(btn_input)
# 输出路径选择
output_layout = QHBoxLayout()
self.encrypt_output = QLineEdit()
btn_output = QPushButton("另存为")
btn_output.clicked.connect(self.choose_output)
output_layout.addWidget(QLabel("输出路径:"))
output_layout.addWidget(self.encrypt_output)
output_layout.addWidget(btn_output)
# 加密参数
param_layout = QHBoxLayout()
self.encrypt_pass = QLineEdit()
self.encrypt_pass.setEchoMode(QLineEdit.Password)
self.encrypt_algo = QComboBox()
self.encrypt_algo.addItems(["AES-128", "AES-256"])
param_layout.addWidget(QLabel("加密密码:"))
param_layout.addWidget(self.encrypt_pass)
param_layout.addWidget(QLabel("算法:"))
param_layout.addWidget(self.encrypt_algo)
# 操作按钮
btn_encrypt = QPushButton("开始加密")
btn_encrypt.clicked.connect(self.handle_encrypt)
layout.addLayout(input_layout)
layout.addLayout(output_layout)
layout.addLayout(param_layout)
layout.addWidget(btn_encrypt)
tab.setLayout(layout)
def merge_ui(self, tab):
"""文件合并界面"""
layout = QVBoxLayout()
# 文件列表(支持拖放)
self.merge_list = QListWidget()
self.merge_list.setDragDropMode(QAbstractItemView.InternalMove)
self.merge_list.setSelectionMode(QAbstractItemView.ExtendedSelection)
self.merge_list.setAcceptDrops(True)
self.merge_list.dragEnterEvent = self.merge_drag_enter
self.merge_list.dropEvent = self.merge_drop
# 操作按钮
btn_layout = QHBoxLayout()
btn_add = QPushButton("添加文件")
btn_add.clicked.connect(self.add_merge_files)
btn_remove = QPushButton("移除选中")
btn_remove.clicked.connect(lambda: self.remove_items(self.merge_list))
btn_up = QPushButton("上移")
btn_up.clicked.connect(lambda: self.move_item(self.merge_list, -1))
btn_down = QPushButton("下移")
btn_down.clicked.connect(lambda: self.move_item(self.merge_list, 1))
btn_layout.addWidget(btn_add)
btn_layout.addWidget(btn_remove)
btn_layout.addWidget(btn_up)
btn_layout.addWidget(btn_down)
# 输出路径
output_layout = QHBoxLayout()
self.merge_output = QLineEdit()
btn_output = QPushButton("选择路径")
btn_output.clicked.connect(self.choose_merge_output)
output_layout.addWidget(QLabel("输出文件:"))
output_layout.addWidget(self.merge_output)
output_layout.addWidget(btn_output)
# 合并按钮
btn_merge = QPushButton("开始合并")
btn_merge.clicked.connect(self.handle_merge)
layout.addWidget(self.merge_list)
layout.addLayout(btn_layout)
layout.addLayout(output_layout)
layout.addWidget(btn_merge)
tab.setLayout(layout)
def handle_decrypt(self):
"""解密处理"""
input_path = self.decrypt_input.text()
password = self.decrypt_pass.text()
if not os.path.exists(input_path):
QMessageBox.critical(self, "错误", "文件不存在!")
return
output_path = f"{os.path.splitext(input_path)[0]}_unlocked.pdf"
try:
with Pdf.open(input_path, password=password) as pdf:
pdf.save(output_path)
QMessageBox.information(self, "成功", f"文件已保存至:\n{output_path}")
except Exception as e:
QMessageBox.critical(self, "错误", f"解密失败:\n{str(e)}")
def handle_encrypt(self):
"""加密处理"""
input_path = self.encrypt_input.text()
output_path = self.encrypt_output.text()
password = self.encrypt_pass.text()
# 检查输入文件是否存在
if not os.path.exists(input_path):
QMessageBox.critical(self, "错误", f"输入文件不存在: {input_path}")
return
if not all([input_path, output_path, password]):
QMessageBox.critical(self, "错误", "请填写所有字段!")
return
try:
# 使用PyPDF2进行加密
reader = PdfReader(input_path)
writer = PdfWriter()
# 复制所有页面
for page in reader.pages:
writer.add_page(page)
# 设置加密
writer.encrypt(password)
# 确保输出目录存在
output_dir = os.path.dirname(output_path)
if output_dir and not os.path.exists(output_dir):
os.makedirs(output_dir)
# 保存加密后的文件
with open(output_path, "wb") as f:
writer.write(f)
QMessageBox.information(self, "成功", "文件加密完成!")
except Exception as e:
QMessageBox.critical(self, "错误", f"加密失败:\n{str(e)}")
def handle_merge(self):
"""合并处理"""
output_path = self.merge_output.text()
if not output_path:
QMessageBox.critical(self, "错误", "请选择输出路径!")
return
if self.merge_list.count() == 0:
QMessageBox.critical(self, "错误", "请添加至少一个PDF文件!")
return
merger = PdfMerger()
try:
for i in range(self.merge_list.count()):
file_path = self.merge_list.item(i).text()
merger.append(file_path)
merger.write(output_path)
QMessageBox.information(self, "成功", "文件合并完成!")
except Exception as e:
QMessageBox.critical(self, "错误", f"合并失败:\n{str(e)}")
finally:
merger.close()
def choose_file(self, target_field):
"""文件选择对话框"""
path, _ = QFileDialog.getOpenFileName(
self, "选择PDF文件", "", "PDF文件 (*.pdf)")
if path:
target_field.setText(path)
def choose_output(self):
"""加密输出路径选择"""
path, _ = QFileDialog.getSaveFileName(
self, "保存文件", "", "PDF文件 (*.pdf)")
if path:
self.encrypt_output.setText(path)
def choose_merge_output(self):
"""合并输出路径选择"""
path, _ = QFileDialog.getSaveFileName(
self, "保存合并文件", "", "PDF文件 (*.pdf)")
if path:
self.merge_output.setText(path)
def add_merge_files(self):
"""添加合并文件"""
files, _ = QFileDialog.getOpenFileNames(
self, "选择PDF文件", "", "PDF文件 (*.pdf)")
if files:
for f in files:
item = QListWidgetItem(f)
self.merge_list.addItem(item)
def remove_items(self, list_widget):
"""移除选中项"""
for item in list_widget.selectedItems():
list_widget.takeItem(list_widget.row(item))
def move_item(self, list_widget, direction):
"""调整顺序"""
current_row = list_widget.currentRow()
if current_row >= 0:
new_row = current_row + direction
if 0 <= new_row < list_widget.count():
item = list_widget.takeItem(current_row)
list_widget.insertItem(new_row, item)
list_widget.setCurrentRow(new_row)
# 合并列表拖放事件
def merge_drag_enter(self, event):
if event.mimeData().hasUrls():
for url in event.mimeData().urls():
if url.isLocalFile() and url.toLocalFile().lower().endswith('.pdf'):
event.acceptProposedAction()
return
event.ignore()
def merge_drop(self, event):
for url in event.mimeData().urls():
file_path = url.toLocalFile()
if file_path.lower().endswith('.pdf'):
self.merge_list.addItem(QListWidgetItem(file_path))
# 图片转PDF功能模块
class ImageToPDFConverter(QWidget):
def __init__(self):
super().__init__()
self.preview_size = QSize(300, 400)
self.image_files = []
self.current_dir = ""
self.settings = {
'resolution': 300,
'compression': 75,
'page_size': '原始尺寸',
'auto_rotate': True
}
self.init_ui()
def init_ui(self):
main_layout = QVBoxLayout(self)
splitter = QSplitter(Qt.Horizontal)
# 左侧面板
left_panel = QWidget()
left_layout = QVBoxLayout(left_panel)
# 文件管理区域
dir_group = QGroupBox("文件管理")
dir_layout = QVBoxLayout(dir_group)
self.btn_choose = QPushButton("选择目录")
self.btn_choose.clicked.connect(self.choose_directory)
self.lbl_dir = QLabel("未选择目录")
btn_group = QHBoxLayout()
self.btn_add_files = QPushButton("添加文件")
self.btn_remove_selected = QPushButton("移除选中")
self.btn_clear_list = QPushButton("清空列表")
btn_group.addWidget(self.btn_add_files)
btn_group.addWidget(self.btn_remove_selected)
btn_group.addWidget(self.btn_clear_list)
dir_layout.addWidget(self.btn_choose)
dir_layout.addWidget(self.lbl_dir)
dir_layout.addLayout(btn_group)
# 文件列表
self.list_widget = QListWidget()
self.list_widget.setDragDropMode(QListWidget.InternalMove)
self.list_widget.itemSelectionChanged.connect(self.show_preview)
# 设置区域
settings_group = QGroupBox("转换设置")
settings_layout = QVBoxLayout(settings_group)
self.page_size_combo = QComboBox()
self.page_size_combo.addItems(["原始尺寸", "A4 (210x297mm)", "Letter (216x279mm)"])
# 添加压缩相关设置
compression_group = QGroupBox("压缩设置")
compression_layout = QVBoxLayout(compression_group)
self.compression_check = QCheckBox("启用压缩")
self.compression_slider = QSlider(Qt.Horizontal)
self.compression_slider.setRange(1, 100)
self.compression_slider.setValue(75)
self.compression_slider.setEnabled(False)
self.compression_check.toggled.connect(self.compression_slider.setEnabled)
# 添加图像预处理选项
self.grayscale_check = QCheckBox("转换为灰度")
self.downsample_check = QCheckBox("降低分辨率")
self.downsample_combo = QComboBox()
self.downsample_combo.addItems(["150 DPI", "200 DPI", "300 DPI"])
self.downsample_combo.setEnabled(False)
self.downsample_check.toggled.connect(self.downsample_combo.setEnabled)
compression_layout.addWidget(self.compression_check)
compression_layout.addWidget(self.compression_slider)
compression_layout.addWidget(self.grayscale_check)
compression_layout.addWidget(self.downsample_check)
compression_layout.addWidget(self.downsample_combo)
compression_group.setLayout(compression_layout)
# 添加高级压缩选项
advanced_group = QGroupBox("高级设置")
advanced_layout = QVBoxLayout(advanced_group)
self.optimize_check = QCheckBox("优化图像")
self.jpeg_quality_label = QLabel("JPEG质量: 75")
self.jpeg_quality_slider = QSlider(Qt.Horizontal)
self.jpeg_quality_slider.setRange(10, 100)
self.jpeg_quality_slider.setValue(75)
self.jpeg_quality_slider.valueChanged.connect(
lambda v: self.jpeg_quality_label.setText(f"JPEG质量: {v}")
)
advanced_layout.addWidget(self.optimize_check)
advanced_layout.addWidget(self.jpeg_quality_label)
advanced_layout.addWidget(self.jpeg_quality_slider)
advanced_group.setLayout(advanced_layout)
# 将高级设置添加到主设置布局
settings_layout.addWidget(compression_group)
settings_layout.addWidget(advanced_group)
# 添加图像格式转换选项
self.convert_format_check = QCheckBox("转换图像格式")
self.convert_format_combo = QComboBox()
self.convert_format_combo.addItems(["JPEG", "PNG", "WEBP"])
self.convert_format_combo.setEnabled(False)
self.convert_format_check.toggled.connect(self.convert_format_combo.setEnabled)
advanced_layout.addWidget(self.convert_format_check)
advanced_layout.addWidget(self.convert_format_combo)
# 添加自动旋转选项
self.rotate_check = QCheckBox("自动旋转")
self.rotate_check.setChecked(True)
settings_layout.addWidget(QLabel("页面尺寸:"))
settings_layout.addWidget(self.page_size_combo)
settings_layout.addWidget(self.compression_check)
settings_layout.addWidget(self.compression_slider)
settings_layout.addWidget(self.rotate_check)
# 组装左侧布局
left_layout.addWidget(dir_group)
left_layout.addWidget(QLabel("文件列表:"))
left_layout.addWidget(self.list_widget)
left_layout.addWidget(settings_group)
# 右侧预览区域
right_panel = QWidget()
right_layout = QVBoxLayout(right_panel)
self.preview_label = QLabel("预览区域")
self.preview_label.setAlignment(Qt.AlignCenter)
self.preview_label.setFixedSize(self.preview_size)
right_layout.addWidget(self.preview_label)
# 转换按钮
self.btn_convert = QPushButton("开始转换")
self.btn_convert.clicked.connect(self.convert_to_pdf)
left_layout.addWidget(self.btn_convert)
# 连接按钮事件
self.btn_add_files.clicked.connect(self.add_single_file)
self.btn_remove_selected.clicked.connect(self.remove_selected_files)
self.btn_clear_list.clicked.connect(self.clear_file_list)
splitter.addWidget(left_panel)
splitter.addWidget(right_panel)
main_layout.addWidget(splitter)
def show_preview(self):
"""显示预览图像"""
if not self.list_widget.currentItem():
self.preview_label.clear()
return
try:
# 获取完整路径
filename = self.list_widget.currentItem().text()
full_path = os.path.join(self.current_dir, filename) if self.current_dir else filename
with Image.open(full_path) as img:
# 自动旋转
if self.settings['auto_rotate']:
img = ImageOps.exif_transpose(img)
# 转换QSize为元组
thumbnail_size = (
self.preview_size.width(),
self.preview_size.height()
)
img.thumbnail(thumbnail_size)
# 处理透明通道
if img.mode == 'RGBA':
background = Image.new('RGB', img.size, (255, 255, 255))
background.paste(img, mask=img.split()[3])
img = background
elif img.mode not in ['RGB', 'L']:
img = img.convert('RGB')
# 转换为QImage
if img.mode == 'RGB':
format = QImage.Format_RGB888
bytes_per_line = img.width * 3
elif img.mode == 'L':
format = QImage.Format_Grayscale8
bytes_per_line = img.width
else:
format = QImage.Format_RGBA8888
bytes_per_line = img.width * 4
qimg = QImage(img.tobytes(), img.width, img.height,
bytes_per_line, format)
# 保持宽高比缩放
pixmap = QPixmap.fromImage(qimg).scaled(
self.preview_size.width(),
self.preview_size.height(),
Qt.KeepAspectRatio,
Qt.SmoothTransformation
)
self.preview_label.setPixmap(pixmap)
except Exception as e:
QMessageBox.warning(self, "预览错误", f"{str(e)}")
self.preview_label.setText("预览不可用")
def choose_directory(self):
directory = QFileDialog.getExistingDirectory(self, "选择目录", QDir.homePath())
if directory:
self.current_dir = directory
self.lbl_dir.setText(directory)
self.scan_image_files(directory)
def scan_image_files(self, directory):
valid_ext = ['.jpg', '.jpeg', '.png', '.bmp', '.tiff', '.webp']
def natural_sort(s):
return [int(c) if c.isdigit() else c.lower() for c in re.split(r'(\d+)', s)]
files = sorted(
[f for f in os.listdir(directory) if os.path.splitext(f)[1].lower() in valid_ext],
key=natural_sort
)
self.image_files = files
self.update_file_list()
def update_file_list(self):
self.list_widget.clear()
for f in self.image_files:
item = QListWidgetItem(f)
item.setFlags(item.flags() | Qt.ItemIsUserCheckable)
item.setCheckState(Qt.Checked)
self.list_widget.addItem(item)
def add_single_file(self):
files, _ = QFileDialog.getOpenFileNames(self, "选择文件",
self.current_dir or QDir.homePath(),
"图片文件 (*.jpg *.jpeg *.png *.bmp *.tiff *.webp)")
if files:
if self.current_dir:
new_files = [os.path.relpath(f, self.current_dir) for f in files]
else:
new_files = files
self.current_dir = os.path.dirname(files[0])
self.lbl_dir.setText(self.current_dir)
self.image_files.extend(new_files)
self.update_file_list()
def remove_selected_files(self):
selected = sorted([self.list_widget.row(item) for item in self.list_widget.selectedItems()], reverse=True)
for row in selected:
del self.image_files[row]
self.update_file_list()
def clear_file_list(self):
self.image_files.clear()
self.list_widget.clear()
def _optimize_pdf(self, pdf_path):
"""对生成的PDF进行后处理优化"""
try:
# 创建临时文件路径
temp_path = pdf_path + ".temp"
# 使用PyPDF2进行优化
reader = PdfReader(pdf_path)
writer = PdfWriter()
# 复制所有页面并应用压缩
for page in reader.pages:
writer.add_page(page)
# 设置压缩选项
writer.add_metadata(reader.metadata)
# 保存优化后的文件
with open(temp_path, "wb") as f:
writer.write(f)
# 替换原文件
os.replace(temp_path, pdf_path)
except Exception as e:
print(f"PDF优化失败: {str(e)}")
def convert_to_pdf(self):
if not self.image_files:
QMessageBox.warning(self, "错误", "请先添加图片文件")
return
save_path, _ = QFileDialog.getSaveFileName(self, "保存PDF",
QDir.homePath(),
"PDF文件 (*.pdf)")
if not save_path:
return
files = [os.path.join(self.current_dir, f) for f in self.get_checked_files()]
# 获取压缩设置
compression_quality = self.compression_slider.value() if self.compression_check.isChecked() else 95
self.settings['compression'] = compression_quality
progress = QProgressDialog("转换中...", "取消", 0, len(files), self)
progress.setWindowModality(Qt.WindowModal)
try:
with ThreadPoolExecutor() as executor:
futures = []
images = []
for file_path in files:
futures.append(executor.submit(self.process_image, file_path))
for i, future in enumerate(futures):
if progress.wasCanceled():
break
progress.setValue(i)
img = future.result()
if img:
images.append(img)
if images:
# 获取压缩设置
compression_quality = self.compression_slider.value() if self.compression_check.isChecked() else 95
jpeg_quality = self.jpeg_quality_slider.value()
# 创建保存选项
save_options = {
"resolution": self.settings['resolution'],
"save_all": True,
"append_images": images[1:],
"quality": compression_quality,
}
# 如果启用了优化选项
if self.optimize_check.isChecked():
save_options["optimize"] = True
# 保存PDF
images[0].save(save_path, "PDF", **save_options)
# 如果需要进一步压缩,可以使用PyPDF2进行后处理
if self.optimize_check.isChecked():
self._optimize_pdf(save_path)
QMessageBox.information(self, "完成", "转换成功!")
except Exception as e:
QMessageBox.critical(self, "错误", f"转换失败: {str(e)}")
finally:
progress.close()
def process_image(self, file_path):
try:
img = Image.open(file_path)
# 自动旋转
if self.settings['auto_rotate']:
img = ImageOps.exif_transpose(img)
# 调整大小
img = self.resize_image(img)
# 转换图像格式(如果选中)
if self.convert_format_check.isChecked():
format_name = self.convert_format_combo.currentText().lower()
if format_name != img.format.lower():
# 创建新图像
new_img = Image.new('RGB', img.size, (255, 255, 255))
if img.mode == 'RGBA':
new_img.paste(img, mask=img.split()[3])
else:
new_img.paste(img)
# 使用内存中转换而不是保存到磁盘
img = new_img
# 转换为灰度(如果选中)
if self.grayscale_check.isChecked():
img = img.convert('L')
# 转回RGB模式以兼容PDF保存
img = img.convert('RGB')
elif img.mode != 'RGB':
img = img.convert('RGB')
# 降低分辨率(如果选中)
if self.downsample_check.isChecked():
dpi_text = self.downsample_combo.currentText()
target_dpi = int(dpi_text.split()[0])
# 计算新尺寸
if hasattr(img, 'info') and 'dpi' in img.info:
original_dpi = img.info['dpi'][0]
if original_dpi > target_dpi:
scale_factor = target_dpi / original_dpi
new_size = (int(img.width * scale_factor), int(img.height * scale_factor))
img = img.resize(new_size, Image.LANCZOS)
return img
except Exception as e:
QMessageBox.warning(self, "错误", f"处理失败: {os.path.basename(file_path)}\n{str(e)}")
return None
def resize_image(self, img):
size_map = {
'A4 (210x297mm)': (2480, 3508),
'Letter (216x279mm)': (2550, 3300)
}
target = self.settings['page_size']
if target == '原始尺寸':
return img
if target in size_map:
img.thumbnail(size_map[target])
return img
def get_checked_files(self):
return [self.list_widget.item(i).text()
for i in range(self.list_widget.count())
if self.list_widget.item(i).checkState() == Qt.Checked]
# PDF书签和目录管理功能模块
class PDFBookmarkManager(QWidget):
def __init__(self):
super().__init__()
self.pdf_document = None
self.bookmarks = []
self.init_ui()
def init_ui(self):
main_layout = QVBoxLayout(self)
# 文件选择区域
file_group = QGroupBox("PDF文件")
file_layout = QHBoxLayout()
self.pdf_path_edit = DragDropLineEdit()
self.pdf_path_edit.setPlaceholderText("拖拽PDF文件到此或点击浏览...")
browse_btn = QPushButton("浏览")
browse_btn.clicked.connect(self.browse_pdf)
load_btn = QPushButton("加载")
load_btn.clicked.connect(self.load_pdf)
file_layout.addWidget(QLabel("PDF文件:"))
file_layout.addWidget(self.pdf_path_edit)
file_layout.addWidget(browse_btn)
file_layout.addWidget(load_btn)
file_group.setLayout(file_layout)
# 创建分割器,左侧是书签树,右侧是PDF预览和书签编辑
splitter = QSplitter(Qt.Horizontal)
# 左侧 - 书签树
left_panel = QWidget()
left_layout = QVBoxLayout(left_panel)
self.bookmark_tree = QTreeWidget()
self.bookmark_tree.setHeaderLabels(["书签标题", "页码"])
self.bookmark_tree.itemSelectionChanged.connect(self.on_bookmark_selected)
bookmark_controls = QHBoxLayout()
add_btn = QPushButton("添加书签")
add_btn.clicked.connect(self.add_bookmark)
edit_btn = QPushButton("编辑书签")
edit_btn.clicked.connect(self.edit_bookmark)
remove_btn = QPushButton("删除书签")
remove_btn.clicked.connect(self.remove_bookmark)
bookmark_controls.addWidget(add_btn)
bookmark_controls.addWidget(edit_btn)
bookmark_controls.addWidget(remove_btn)
left_layout.addWidget(QLabel("书签列表:"))
left_layout.addWidget(self.bookmark_tree)
left_layout.addLayout(bookmark_controls)
# 右侧 - 书签编辑和PDF预览
right_panel = QWidget()
right_layout = QVBoxLayout(right_panel)
# 书签编辑区域
edit_group = QGroupBox("书签编辑")
edit_layout = QVBoxLayout()
title_layout = QHBoxLayout()
title_layout.addWidget(QLabel("标题:"))
self.title_edit = QLineEdit()
title_layout.addWidget(self.title_edit)
page_layout = QHBoxLayout()
page_layout.addWidget(QLabel("页码:"))
self.page_spin = QComboBox()
page_layout.addWidget(self.page_spin)
level_layout = QHBoxLayout()
level_layout.addWidget(QLabel("层级:"))
self.level_spin = QComboBox()
self.level_spin.addItems(["1", "2", "3", "4", "5"])
level_layout.addWidget(self.level_spin)
save_btn = QPushButton("保存书签")
save_btn.clicked.connect(self.save_bookmark)
edit_layout.addLayout(title_layout)
edit_layout.addLayout(page_layout)
edit_layout.addLayout(level_layout)
edit_layout.addWidget(save_btn)
edit_group.setLayout(edit_layout)
# 自动生成目录区域
toc_group = QGroupBox("自动生成目录")
toc_layout = QVBoxLayout()
self.detect_headings_btn = QPushButton("检测标题文本")
self.detect_headings_btn.clicked.connect(self.detect_headings)
self.auto_toc_btn = QPushButton("生成目录")
self.auto_toc_btn.clicked.connect(self.generate_toc)
toc_layout.addWidget(self.detect_headings_btn)
toc_layout.addWidget(self.auto_toc_btn)
toc_group.setLayout(toc_layout)
# 保存PDF区域
save_group = QGroupBox("保存PDF")
save_layout = QVBoxLayout()
self.save_pdf_btn = QPushButton("保存带书签的PDF")
self.save_pdf_btn.clicked.connect(self.save_pdf_with_bookmarks)
self.merge_toc_btn = QPushButton("合并目录到PDF")
self.merge_toc_btn.clicked.connect(self.merge_toc_with_pdf)
save_layout.addWidget(self.save_pdf_btn)
save_layout.addWidget(self.merge_toc_btn)
save_group.setLayout(save_layout)
# 组装右侧面板
right_layout.addWidget(edit_group)
right_layout.addWidget(toc_group)
right_layout.addWidget(save_group)
right_layout.addStretch()
# 添加到分割器
splitter.addWidget(left_panel)
splitter.addWidget(right_panel)
# 设置分割器比例
splitter.setSizes([300, 300])
# 添加到主布局
main_layout.addWidget(file_group)
main_layout.addWidget(splitter)
def browse_pdf(self):
"""浏览并选择PDF文件"""
path, _ = QFileDialog.getOpenFileName(self, "选择PDF文件", "", "PDF文件 (*.pdf)")
if path:
self.pdf_path_edit.setText(path)
def load_pdf(self):
"""加载PDF文件并提取书签"""
pdf_path = self.pdf_path_edit.text()
if not pdf_path or not os.path.exists(pdf_path):
QMessageBox.warning(self, "错误", "请选择有效的PDF文件")
return
try:
# 使用PyMuPDF加载PDF
self.pdf_document = fitz.open(pdf_path)
# 更新页码下拉框
self.page_spin.clear()
for i in range(1, self.pdf_document.page_count + 1):
self.page_spin.addItem(str(i))
# 提取现有书签
self.bookmarks = self._extract_bookmarks()
self._update_bookmark_tree()
QMessageBox.information(self, "成功", f"PDF文件已加载,共{self.pdf_document.page_count}页")
except Exception as e:
QMessageBox.critical(self, "错误", f"加载PDF失败: {str(e)}")
def _extract_bookmarks(self):
"""从PDF中提取书签"""
bookmarks = []
if self.pdf_document:
try:
toc = self.pdf_document.get_toc()
for item in toc:
level, title, page = item
# 确保标题是Unicode字符串
if isinstance(title, bytes):
title = title.decode('utf-8', errors='replace')
bookmarks.append({
'level': level,
'title': title,
'page': page
})
except Exception as e:
QMessageBox.warning(self, "警告", f"提取书签时出错: {str(e)}")
return bookmarks
def _update_bookmark_tree(self):
"""更新书签树显示"""
self.bookmark_tree.clear()
# 创建根节点字典,用于构建树结构
root_items = {}
for bookmark in self.bookmarks:
level = bookmark['level']
title = bookmark['title']
page = bookmark['page']
item = QTreeWidgetItem([title, str(page)])
# 根据层级确定父节点
if level == 1:
self.bookmark_tree.addTopLevelItem(item)
root_items[title] = item
else:
# 查找父节点
parent_level = level - 1
parent_found = False
# 从当前书签向前查找最近的上一级书签
for i in range(self.bookmarks.index(bookmark) - 1, -1, -1):
if self.bookmarks[i]['level'] == parent_level:
parent_title = self.bookmarks[i]['title']
if parent_title in root_items:
root_items[parent_title].addChild(item)
parent_found = True
break
# 如果没有找到父节点,添加到根节点
if not parent_found:
self.bookmark_tree.addTopLevelItem(item)
# 将当前节点添加到根节点字典
root_items[title] = item
# 展开所有节点
self.bookmark_tree.expandAll()
def on_bookmark_selected(self):
"""当书签被选中时更新编辑区域"""
selected_items = self.bookmark_tree.selectedItems()
if selected_items:
item = selected_items[0]
title = item.text(0)
page = item.text(1)
# 查找对应的书签
for bookmark in self.bookmarks:
if bookmark['title'] == title and str(bookmark['page']) == page:
self.title_edit.setText(title)
self.page_spin.setCurrentText(str(page))
self.level_spin.setCurrentText(str(bookmark['level']))
break
def add_bookmark(self):
"""添加新书签"""
if not self.pdf_document:
QMessageBox.warning(self, "错误", "请先加载PDF文件")
return
# 创建新书签
new_bookmark = {
'level': 1,
'title': "新书签",
'page': 1
}
self.bookmarks.append(new_bookmark)
self._update_bookmark_tree()
# 选中新添加的书签
last_item = self.bookmark_tree.topLevelItem(self.bookmark_tree.topLevelItemCount() - 1)
self.bookmark_tree.setCurrentItem(last_item)
def edit_bookmark(self):
"""编辑选中的书签"""
selected_items = self.bookmark_tree.selectedItems()
if not selected_items:
QMessageBox.warning(self, "错误", "请先选择一个书签")
return
item = selected_items[0]
title = item.text(0)
page = item.text(1)
# 查找对应的书签
for bookmark in self.bookmarks:
if bookmark['title'] == title and str(bookmark['page']) == page:
# 更新编辑区域
self.title_edit.setText(title)
self.page_spin.setCurrentText(str(page))
self.level_spin.setCurrentText(str(bookmark['level']))
break
def remove_bookmark(self):
"""删除选中的书签"""
selected_items = self.bookmark_tree.selectedItems()
if not selected_items:
QMessageBox.warning(self, "错误", "请先选择一个书签")
return
item = selected_items[0]
title = item.text(0)
page = item.text(1)
# 查找并删除对应的书签
for i, bookmark in enumerate(self.bookmarks):
if bookmark['title'] == title and str(bookmark['page']) == page:
del self.bookmarks[i]
break
self._update_bookmark_tree()
def save_bookmark(self):
"""保存编辑后的书签"""
selected_items = self.bookmark_tree.selectedItems()
if not selected_items:
QMessageBox.warning(self, "错误", "请先选择一个书签")
return
item = selected_items[0]
old_title = item.text(0)
old_page = item.text(1)
# 获取新的书签信息
new_title = self.title_edit.text()
# 确保标题不为空
if not new_title.strip():
QMessageBox.warning(self, "错误", "书签标题不能为空")
return
new_page = int(self.page_spin.currentText())
new_level = int(self.level_spin.currentText())
# 更新书签
for bookmark in self.bookmarks:
if bookmark['title'] == old_title and str(bookmark['page']) == old_page:
bookmark['title'] = new_title
bookmark['page'] = new_page
bookmark['level'] = new_level
break
self._update_bookmark_tree()
QMessageBox.information(self, "成功", "书签已更新")
def detect_headings(self):
"""检测PDF中的标题文本"""
if not self.pdf_document:
QMessageBox.warning(self, "错误", "请先加载PDF文件")
return
try:
# 创建进度对话框
progress = QProgressDialog("正在检测标题...", "取消", 0, self.pdf_document.page_count, self)
progress.setWindowModality(Qt.WindowModal)
detected_headings = []
# 遍历每一页
for page_num in range(self.pdf_document.page_count):
if progress.wasCanceled():
break
progress.setValue(page_num)
# 获取页面
page = self.pdf_document[page_num]
# 提取文本块
blocks = page.get_text("dict")["blocks"]
for block in blocks:
if "lines" in block:
for line in block["lines"]:
if "spans" in line:
for span in line["spans"]:
# 检查是否可能是标题(字体大小较大或者是粗体)
font_size = span["size"]
font_flags = span["flags"]
text = span["text"].strip()
# 如果字体大于12或者是粗体,且文本不为空且长度合适
if (font_size > 12 or (font_flags & 2)) and text and 1 < len(text) < 100:
# 确定标题级别
level = 1
if font_size < 14:
level = 3
elif font_size < 16:
level = 2
# 添加到检测到的标题列表
detected_headings.append({
'level': level,
'title': text,
'page': page_num + 1 # 页码从1开始
})
progress.setValue(self.pdf_document.page_count)
# 更新书签列表
if detected_headings:
# 询问用户是否替换现有书签
if self.bookmarks:
reply = QMessageBox.question(self, "确认",
"是否用检测到的标题替换现有书签?",
QMessageBox.Yes | QMessageBox.No)
if reply == QMessageBox.Yes:
self.bookmarks = detected_headings
else:
# 合并书签
self.bookmarks.extend(detected_headings)
else:
self.bookmarks = detected_headings
self._update_bookmark_tree()
QMessageBox.information(self, "成功", f"检测到{len(detected_headings)}个标题")
else:
QMessageBox.information(self, "结果", "未检测到标题")
except Exception as e:
QMessageBox.critical(self, "错误", f"检测标题失败: {str(e)}")
def generate_toc(self):
"""生成目录页"""
if not self.pdf_document or not self.bookmarks:
QMessageBox.warning(self, "错误", "请先加载PDF文件并添加书签")
return
try:
# 创建新的PDF文档作为目录页
toc_doc = fitz.open()
toc_page = toc_doc.new_page()
# 设置支持中文的字体
# 使用系统内置的中文字体,如果没有则使用通用字体
font = "china-s" # 中文简体字体
fallback_font = "fireflysung" # 备用中文字体
# 尝试加载中文字体,如果失败则使用内置字体
try:
toc_page.insert_font(fontname=font)
except:
try:
toc_page.insert_font(fontname=fallback_font)
font = fallback_font
except:
font = "helv" # 回退到默认字体
QMessageBox.warning(self, "警告", "未找到中文字体,可能导致中文显示不正确")
font_size = 12
title_font_size = 18
# 添加目录标题
toc_page.insert_text((50, 50), "目录", fontname=font, fontsize=title_font_size, color=(0, 0, 0))
# 添加书签条目
y = 80
for bookmark in self.bookmarks:
level = bookmark['level']
title = bookmark['title']
page = bookmark['page']
# 根据层级缩进
x = 50 + (level - 1) * 20
# 插入文本,确保使用支持中文的字体
toc_page.insert_text((x, y), f"{title} ................... {page}",
fontname=font, fontsize=font_size, color=(0, 0, 0))
y += 20
# 如果超出页面,创建新页
if y > 800:
toc_page = toc_doc.new_page()
y = 50
# 保存目录页
toc_path = os.path.splitext(self.pdf_path_edit.text())[0] + "_toc.pdf"
toc_doc.save(toc_path)
toc_doc.close()
QMessageBox.information(self, "成功", f"目录已生成并保存至:\n{toc_path}")
except Exception as e:
QMessageBox.critical(self, "错误", f"生成目录失败: {str(e)}")
def save_pdf_with_bookmarks(self):
"""保存带书签的PDF"""
if not self.pdf_document or not self.bookmarks:
QMessageBox.warning(self, "错误", "请先加载PDF文件并添加书签")
return
# 选择保存路径
save_path, _ = QFileDialog.getSaveFileName(
self, "保存带书签的PDF",
os.path.splitext(self.pdf_path_edit.text())[0] + "_bookmarked.pdf",
"PDF文件 (*.pdf)")
if not save_path:
return
try:
# 准备目录结构
toc = []
for bookmark in self.bookmarks:
toc.append([bookmark['level'], bookmark['title'], bookmark['page']])
# 设置新的目录
self.pdf_document.set_toc(toc)
# 保存PDF
self.pdf_document.save(save_path)
QMessageBox.information(self, "成功", f"带书签的PDF已保存至:\n{save_path}")
except Exception as e:
QMessageBox.critical(self, "错误", f"保存PDF失败: {str(e)}")
def merge_toc_with_pdf(self):
"""将生成的目录页合并到原PDF中"""
if not self.pdf_document:
QMessageBox.warning(self, "错误", "请先加载PDF文件")
return
# 检查目录文件是否存在
toc_path = os.path.splitext(self.pdf_path_edit.text())[0] + "_toc.pdf"
if not os.path.exists(toc_path):
QMessageBox.warning(self, "错误", "请先生成目录")
return
# 选择保存路径
save_path, _ = QFileDialog.getSaveFileName(
self, "保存带目录的PDF",
os.path.splitext(self.pdf_path_edit.text())[0] + "_with_toc.pdf",
"PDF文件 (*.pdf)")
if not save_path:
return
try:
# 打开目录文件
toc_doc = fitz.open(toc_path)
# 创建新文档
new_doc = fitz.open()
# 首先添加目录页
for page_num in range(toc_doc.page_count):
new_doc.insert_pdf(toc_doc, from_page=page_num, to_page=page_num)
# 然后添加原PDF的所有页面
for page_num in range(self.pdf_document.page_count):
new_doc.insert_pdf(self.pdf_document, from_page=page_num, to_page=page_num)
# 更新书签页码(加上目录页数)
toc = []
for bookmark in self.bookmarks:
level = bookmark['level']
title = bookmark['title']
# 确保标题是Unicode字符串
if isinstance(title, bytes):
title = title.decode('utf-8', errors='replace')
page = bookmark['page'] + toc_doc.page_count
toc.append([level, title, page])
# 设置新的目录
new_doc.set_toc(toc)
# 保存PDF
new_doc.save(save_path)
# 关闭文档
toc_doc.close()
new_doc.close()
QMessageBox.information(self, "成功", f"带目录的PDF已保存至:\n{save_path}")
except Exception as e:
QMessageBox.critical(self, "错误", f"合并目录失败: {str(e)}")
# 主窗口类,整合所有功能模块
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("PDF工具箱 version-20250515")
self.setGeometry(100, 100, 900, 600)
# 创建标签页控件
self.tabs = QTabWidget()
self.setCentralWidget(self.tabs)
# 添加PDF密码解除与加密功能
pdf_tools_tab = PDFToolsUI()
self.tabs.addTab(pdf_tools_tab, "PDF密码工具")
# 添加PDF转图片功能
pdf_to_image_tab = PdfToolApp()
self.tabs.addTab(pdf_to_image_tab, "PDF转图片")
# 添加图片转PDF功能
image_to_pdf_tab = ImageToPDFConverter()
self.tabs.addTab(image_to_pdf_tab, "图片转PDF")
# 添加PDF书签和目录功能
pdf_bookmark_tab = PDFBookmarkManager()
self.tabs.addTab(pdf_bookmark_tab, "PDF书签管理")
# 主函数
if __name__ == '__main__':
# 高DPI设置
if hasattr(Qt, 'AA_EnableHighDpiScaling'):
QApplication.setAttribute(Qt.AA_EnableHighDpiScaling, True)
if hasattr(Qt, 'AA_UseHighDpiPixmaps'):
QApplication.setAttribute(Qt.AA_UseHighDpiPixmaps, True)
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec_())
执行测试:
将上述代码,保存为pdf-toolkit.pyw
文件。
将如下命令写入start.bat
脚本文件,双击即可自动调用这个文件:
@echo off
cd /d "%~dp0"
start "" pythonw pdf-toolkit.pyw