设为首页收藏本站

 找回密码
 立即注册

只需一步,快速开始

搜索
查看: 192|回复: 22

[实用软件] ColorPdfSpliter 将 PDF 分为黑白和彩色页面,支持单双面打印

 火... [复制链接]
累计签到:31 天
连续签到:2 天
灌水成绩
14
53
4404
主题
帖子
积分

等级头衔

ID : 636

助理工程师

积分成就 测量币 : 4404
在线时间 : 0 小时
注册时间 : 2025-10-8
最后登录 : 2026-6-12

勋章
UID勋章测量学徒测量员
发表于 2026-4-15 13:10:20 | 显示全部楼层 |阅读模式 IP:广东东莞
打印店黑白打印要0.15一面,彩色1块一页,价格差的很多,课程作业和论文里只有一部分有图需要彩色打印,直接全按彩色打印的话会贵上不少。


原作者开发了 “ColorPdfSpliter” (https://github.com/huuhghhgyg/ColorPdfSpliter),支持单面和双面打印模式,双面打印会把本属于同一张的页码分在一起,方便拿去打印店打印。原作者描述:


可使用duplex参数实现双面打印时的彩色与黑白分割。这个参数保证生成的彩色与黑白文件均为连续的两页(即一张纸上的双面)。当一张纸上正反打印的两个页面中有一个为彩色,则这两个页面都会被划分入彩色文件。


提供的本地部署版需要安装python环境,对一些小白用户有难度。


因此,在原作者的基础上使用 Qt+PySide6 做了个界面,解压后直接使用,支持

  • 输出到PDF同目录 / 指定同一目录;
  • 单面打印/双面打印添加多个文件同时处理,PDF不必放在一起


单面打印模式:

双面打印模式:


使用 Nuitka 打包的程序,蓝奏云 https://wwauf.lanzouw.com/iU5Jk3mwmv4f  密码:a8sf
源码如下
[Python] 纯文本查看 复制代码import ioimport hashlibimport osimport sysimport tracebackfrom contextlib import redirect_stderr, redirect_stdoutfrom pathlib import Pathtry:    from PySide6.QtCore import QObject, Qt, QThread, Signal, Slot    from PySide6.QtWidgets import (        QApplication,        QCheckBox,        QFileDialog,        QHBoxLayout,        QLabel,        QListWidget,        QListWidgetItem,        QMainWindow,        QMessageBox,        QPlainTextEdit,        QPushButton,        QProgressBar,        QVBoxLayout,        QWidget,        QLineEdit,    )except ImportError as exc:  # pragma: no cover - runtime dependency guard    raise SystemExit(        "缺少 PySide6 依赖,请先运行 initialize.bat 或执行 pip install PySide6。"    ) from excfrom ColorPdfSpliter import splitPDFclass SignalWriter(io.TextIOBase):    def __init__(self, signal):        self.signal = signal        self._buffer = ""    def write(self, text):        if not text:            return        self._buffer += text        while "\n" in self._buffer:            line, self._buffer = self._buffer.split("\n", 1)            line = line.strip()            if line:                self.signal.emit(line)    def flush(self):        line = self._buffer.strip()        if line:            self.signal.emit(line)        self._buffer = ""class SplitWorker(QObject):    log = Signal(str)    fileStarted = Signal(str, int, int)    progressChanged = Signal(int, int)    fileFinished = Signal(str, bool, str)    finished = Signal()    error = Signal(str)    def __init__(self, files, duplex=False, use_custom_output=False, output_dir=""):        super().__init__()        self._files = list(files)        self._duplex = duplex        self._use_custom_output = use_custom_output        self._output_dir = output_dir    @Slot()    def run(self):        stdout_writer = SignalWriter(self.log)        stderr_writer = SignalWriter(self.log)        try:            total_files = len(self._files)            for index, file_path in enumerate(self._files, start=1):                source_path = Path(file_path)                export_dir = self._output_dir if self._use_custom_output else str(source_path.parent)                export_dir = str(Path(export_dir)) if export_dir else ""                name_prefix = ""                if self._use_custom_output:                    name_prefix = hashlib.sha1(str(source_path).encode("utf-8")).hexdigest()[:8]                if export_dir:                    os.makedirs(export_dir, exist_ok=True)                self.fileStarted.emit(str(source_path), index, total_files)                self.log.emit(f"▶ 开始处理:{source_path}")                with redirect_stdout(stdout_writer), redirect_stderr(stderr_writer):                    splitPDF(                        str(source_path),                        self._on_progress,                        exportdir=export_dir,                        duplex=self._duplex,                        name_prefix=name_prefix,                    )                self.fileFinished.emit(str(source_path), True, export_dir)            self.log.emit("✅ 所有文件处理完成")        except Exception:            self.error.emit(traceback.format_exc())        finally:            self.finished.emit()    def _on_progress(self, current, total, _title=""):        self.progressChanged.emit(int(current), int(total))class MainWindow(QMainWindow):    def __init__(self, duplex_default=False):        super().__init__()        self.setWindowTitle("ColorPdfSpliter - PySide")        self.resize(920, 640)        self.worker_thread = None        self.worker = None        central = QWidget(self)        self.setCentralWidget(central)        root_layout = QVBoxLayout(central)        title_label = QLabel("PDF 列表")        title_label.setStyleSheet("font-weight: bold;")        root_layout.addWidget(title_label)        self.file_list = QListWidget()        self.file_list.setSelectionMode(QListWidget.SelectionMode.ExtendedSelection)        root_layout.addWidget(self.file_list, 3)        file_button_row = QHBoxLayout()        self.add_button = QPushButton("添加 PDF")        self.remove_button = QPushButton("移除选中")        self.clear_button = QPushButton("清空列表")        file_button_row.addWidget(self.add_button)        file_button_row.addWidget(self.remove_button)        file_button_row.addWidget(self.clear_button)        file_button_row.addStretch(1)        root_layout.addLayout(file_button_row)        output_title = QLabel("输出设置")        output_title.setStyleSheet("font-weight: bold;")        root_layout.addWidget(output_title)        self.custom_output_checkbox = QCheckBox("输出到指定目录")        self.output_dir_edit = QLineEdit()        self.output_dir_edit.setReadOnly(True)        self.output_dir_edit.setPlaceholderText("与 PDF 同目录")        self.output_dir_edit.setEnabled(False)        self.output_browse_button = QPushButton("选择目录")        self.output_browse_button.setEnabled(False)        output_row = QHBoxLayout()        output_row.addWidget(self.custom_output_checkbox)        output_row.addWidget(self.output_dir_edit, 1)        output_row.addWidget(self.output_browse_button)        root_layout.addLayout(output_row)        options_row = QHBoxLayout()        self.duplex_checkbox = QCheckBox("双面打印")        self.duplex_checkbox.setChecked(duplex_default)        options_row.addWidget(self.duplex_checkbox)        options_row.addStretch(1)        root_layout.addLayout(options_row)        action_row = QHBoxLayout()        self.start_button = QPushButton("开始处理")        self.start_button.setEnabled(False)        self.progress_bar = QProgressBar()        self.progress_bar.setRange(0, 100)        self.progress_bar.setValue(0)        self.progress_bar.setFormat("%p%")        action_row.addWidget(self.start_button)        action_row.addWidget(self.progress_bar, 1)        root_layout.addLayout(action_row)        self.status_label = QLabel("就绪")        root_layout.addWidget(self.status_label)        log_title = QLabel("日志")        log_title.setStyleSheet("font-weight: bold;")        root_layout.addWidget(log_title)        self.log_view = QPlainTextEdit()        self.log_view.setReadOnly(True)        root_layout.addWidget(self.log_view, 2)        self.add_button.clicked.connect(self.add_files)        self.remove_button.clicked.connect(self.remove_selected_files)        self.clear_button.clicked.connect(self.clear_files)        self.custom_output_checkbox.toggled.connect(self.on_output_mode_changed)        self.output_browse_button.clicked.connect(self.choose_output_dir)        self.start_button.clicked.connect(self.start_processing)        self.file_list.itemSelectionChanged.connect(self.update_buttons_state)    def append_log(self, text):        self.log_view.appendPlainText(text)    def update_buttons_state(self):        has_files = self.file_list.count() > 0        has_selection = bool(self.file_list.selectedItems())        self.remove_button.setEnabled(has_selection)        self.clear_button.setEnabled(has_files)        output_ready = (not self.custom_output_checkbox.isChecked()) or bool(self.output_dir_edit.text().strip())        self.start_button.setEnabled(has_files and self.worker_thread is None and output_ready)    def on_output_mode_changed(self, checked):        self.output_browse_button.setEnabled(checked)        self.output_dir_edit.setEnabled(checked)        if checked and not self.output_dir_edit.text().strip():            self.output_dir_edit.setPlaceholderText("请选择输出目录")        if not checked:            self.output_dir_edit.setPlaceholderText("与 PDF 同目录")        self.update_buttons_state()    def choose_output_dir(self):        directory = QFileDialog.getExistingDirectory(self, "选择输出文件夹")        if directory:            self.output_dir_edit.setText(directory)        self.update_buttons_state()    def add_files(self):        files, _ = QFileDialog.getOpenFileNames(            self,            "选择一个或多个 PDF 文件",            "",            "PDF Files (*.pdf);;All Files (*)",        )        if not files:            return        existing = {self.file_list.item(i).data(Qt.ItemDataRole.UserRole) for i in range(self.file_list.count())}        added = 0        for file_path in files:            normalized = str(Path(file_path).resolve())            if normalized in existing:                continue            item = QListWidgetItem(normalized)            item.setData(Qt.ItemDataRole.UserRole, normalized)            self.file_list.addItem(item)            existing.add(normalized)            added += 1        self.append_log(f"已添加 {added} 个 PDF 文件")        self.update_buttons_state()    def remove_selected_files(self):        for item in self.file_list.selectedItems():            row = self.file_list.row(item)            self.file_list.takeItem(row)        self.append_log("已移除选中的文件")        self.update_buttons_state()    def clear_files(self):        self.file_list.clear()        self.append_log("已清空文件列表")        self.update_buttons_state()    def _selected_files(self):        return [self.file_list.item(i).data(Qt.ItemDataRole.UserRole) for i in range(self.file_list.count())]    def start_processing(self):        files = self._selected_files()        if not files:            QMessageBox.warning(self, "提示", "请先添加一个或多个 PDF 文件。")            return        use_custom_output = self.custom_output_checkbox.isChecked()        output_dir = self.output_dir_edit.text().strip() if use_custom_output else ""        if use_custom_output and not output_dir:            QMessageBox.warning(self, "提示", "请先选择输出文件夹。")            return        self.log_view.clear()        self.append_log(f"待处理文件:{len(files)} 个")        self.append_log(f"双面打印:{'是' if self.duplex_checkbox.isChecked() else '否'}")        self.append_log(f"输出模式:{'指定目录 ' + output_dir if use_custom_output else '与源文件同目录'}")        self._set_processing_state(True)        self.worker_thread = QThread(self)        self.worker = SplitWorker(            files,            duplex=self.duplex_checkbox.isChecked(),            use_custom_output=use_custom_output,            output_dir=output_dir or "",        )        self.worker.moveToThread(self.worker_thread)        self.worker_thread.started.connect(self.worker.run)        self.worker.log.connect(self.append_log)        self.worker.fileStarted.connect(self.on_file_started)        self.worker.progressChanged.connect(self.on_progress_changed)        self.worker.fileFinished.connect(self.on_file_finished)        self.worker.error.connect(self.on_worker_error)        self.worker.finished.connect(self.worker_thread.quit)        self.worker.finished.connect(self.worker.deleteLater)        self.worker_thread.finished.connect(self.on_worker_finished)        self.worker_thread.finished.connect(self.worker_thread.deleteLater)        self.worker_thread.start()    def _set_processing_state(self, is_processing):        self.add_button.setEnabled(not is_processing)        self.remove_button.setEnabled(False)        self.clear_button.setEnabled(not is_processing and self.file_list.count() > 0)        self.start_button.setEnabled(False)        self.custom_output_checkbox.setEnabled(not is_processing)        self.output_browse_button.setEnabled(not is_processing and self.custom_output_checkbox.isChecked())        self.duplex_checkbox.setEnabled(not is_processing)        self.file_list.setEnabled(not is_processing)        if is_processing:            self.status_label.setText("处理中...")        else:            self.status_label.setText("就绪")    def on_file_started(self, file_path, index, total):        self.status_label.setText(f"正在处理第 {index}/{total} 个文件:{Path(file_path).name}")        self.progress_bar.setValue(0)        self.progress_bar.setRange(0, 100)        self.append_log(f"文件 {index}/{total}:{file_path}")    def on_progress_changed(self, current, total):        total = max(int(total), 1)        current = max(0, min(int(current), total))        self.progress_bar.setRange(0, total)        self.progress_bar.setValue(current)        self.status_label.setText(f"当前页面进度:{current}/{total}")    def on_file_finished(self, file_path, ok, export_dir):        if ok:            if export_dir:                self.append_log(f"✅ 完成:{file_path} -> {export_dir}")            else:                self.append_log(f"✅ 完成:{file_path}")        else:            self.append_log(f"❌ 失败:{file_path}")    def on_worker_error(self, error_text):        self.append_log("❌ 处理过程中发生错误:")        self.append_log(error_text)        QMessageBox.critical(self, "处理失败", error_text)    def on_worker_finished(self):        self.worker_thread = None        self.worker = None        self._set_processing_state(False)        self.update_buttons_state()        self.progress_bar.setValue(self.progress_bar.maximum())        self.status_label.setText("全部处理完成")def main():    app = QApplication(sys.argv)    duplex_default = "--duplex" in sys.argv[1:]    window = MainWindow(duplex_default=duplex_default)    window.show()    sys.exit(app.exec())if __name__ == "__main__":    main()


累计签到:58 天
连续签到:47 天
灌水成绩
2
207
13184
主题
帖子
积分

等级头衔

ID : 544

中级工程师

积分成就 测量币 : 13184
在线时间 : 526 小时
注册时间 : 2026-4-17
最后登录 : 2026-7-5

勋章
UID勋章测量学徒测量员
发表于 2026-4-26 07:40:22 | 显示全部楼层 IP:
博主经验丰富,写得很靠谱。
回复

使用道具 举报

累计签到:62 天
连续签到:45 天
灌水成绩
3
228
14079
主题
帖子
积分

等级头衔

ID : 512

中级工程师

积分成就 测量币 : 14079
在线时间 : 522 小时
注册时间 : 2025-9-22
最后登录 : 2026-7-5

勋章
UID勋章测量学徒测量员
发表于 2026-4-30 02:09:19 | 显示全部楼层 IP:广东东莞
解决了我一直困惑的问题。
回复

使用道具 举报

累计签到:61 天
连续签到:46 天
灌水成绩
2
231
15086
主题
帖子
积分

等级头衔

ID : 524

中级工程师

积分成就 测量币 : 15086
在线时间 : 518 小时
注册时间 : 2026-3-21
最后登录 : 2026-7-5

勋章
UID勋章测量学徒测量员
发表于 2026-5-1 01:10:33 | 显示全部楼层 IP:广东东莞
感谢分享这么优质的内容。
回复

使用道具 举报

累计签到:62 天
连续签到:47 天
灌水成绩
3
254
16210
主题
帖子
积分

等级头衔

ID : 571

中级工程师

积分成就 测量币 : 16210
在线时间 : 532 小时
注册时间 : 2026-4-9
最后登录 : 2026-7-5

勋章
UID勋章测量学徒测量员
发表于 2026-5-1 01:10:33 | 显示全部楼层 IP:广东东莞
很有启发性,打开新视角。
回复

使用道具 举报

累计签到:56 天
连续签到:43 天
灌水成绩
2
238
14828
主题
帖子
积分

等级头衔

ID : 573

中级工程师

积分成就 测量币 : 14828
在线时间 : 530 小时
注册时间 : 2026-3-8
最后登录 : 2026-7-5

勋章
UID勋章测量学徒测量员
发表于 2026-5-1 01:41:59 | 显示全部楼层 IP:广东东莞
对比了好几篇,这篇最靠谱。
回复

使用道具 举报

累计签到:57 天
连续签到:47 天
灌水成绩
2
224
14478
主题
帖子
积分

等级头衔

ID : 553

中级工程师

积分成就 测量币 : 14478
在线时间 : 530 小时
注册时间 : 2025-10-4
最后登录 : 2026-7-5

勋章
UID勋章测量学徒测量员
发表于 2026-5-1 01:43:51 | 显示全部楼层 IP:广东东莞
文章结构合理,由浅入深。
回复

使用道具 举报

累计签到:41 天
连续签到:1 天
灌水成绩
0
67
3833
主题
帖子
积分

等级头衔

ID : 999

助理工程师

积分成就 测量币 : 3833
在线时间 : 0 小时
注册时间 : 2025-11-15
最后登录 : 2026-6-26

勋章
UID勋章测量学徒测量员
发表于 2026-5-12 20:15:05 | 显示全部楼层 IP:沙特阿拉伯
相当不错,感谢无私分享精神!
回复

使用道具 举报

累计签到:10 天
连续签到:2 天
灌水成绩
1
46
2478
主题
帖子
积分

等级头衔

ID : 1303

高级技术员

积分成就 测量币 : 2478
在线时间 : 0 小时
注册时间 : 2026-4-10
最后登录 : 2026-6-14

勋章
测量员UID勋章测量学徒
发表于 2026-5-13 02:30:05 | 显示全部楼层 IP:北京
示例代码规范,值得学习。
回复

使用道具 举报

累计签到:32 天
连续签到:1 天
灌水成绩
11
56
4528
主题
帖子
积分

等级头衔

ID : 606

助理工程师

积分成就 测量币 : 4528
在线时间 : 0 小时
注册时间 : 2026-3-16
最后登录 : 2026-6-21

勋章
UID勋章测量学徒测量员
发表于 2026-5-17 07:31:51 | 显示全部楼层 IP:美国
看完收获很大,感谢无私分享。
回复

使用道具 举报

快速回复换一批
路过留名
前排围观! 搬好小板凳,坐看大佬们在线battle技术。 🪑🍿
自古二楼出人才? 我来占个前排,楼主继续,不要停! 🏃‍♂️💨
马克一下(MK)。字字珠玑,容我回去消化一下再来交作业。 🧠🧐
学到了! 请问楼主,这个方法在实际操作中有什么需要特别注意的坑吗? 🧐❓
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

Archiver|手机版|小黑屋|精密测量技术论坛 ( 桂ICP备2026007449号-1 )

GMT+8, 2026-7-5 15:03 , Processed in 0.776065 second(s), 52 queries .

Powered by 精密测量技术论坛

© 2025-2026 联系站长

快速回复 返回顶部 返回列表