0 0

AI图片重命名工具:提高效率_优化管理_必备神器

Windows 电脑软件 17 0 发表于 昨天 15:09
  1. import sys
  2. import os
  3. import time
  4. import tempfile
  5. import shutil
  6. from PyQt5.QtWidgets import (
  7.     QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
  8.     QPushButton, QListWidget, QListWidgetItem, QLabel, QTextEdit,
  9.     QComboBox, QFileDialog, QLineEdit, QMessageBox, QAbstractItemView,
  10.     QStatusBar, QAction, QMenu, QFrame
  11. )
  12. from PyQt5.QtGui import QPixmap, QIcon, QKeySequence, QFont
  13. from PyQt5.QtCore import Qt, QSize, QSettings, QUrl

  14. from PIL import Image

  15. APP_ICON_PATH = 'icon.png'

  16. class ImageBatchProcessor(QMainWindow):
  17.     DEFAULT_FONT_SIZE = 10
  18.     SUPPORTED_EXTENSIONS = ('.png', '.jpg', '.jpeg', '.bmp', '.gif', '.tiff')

  19.     def __init__(self):
  20.         super().__init__()
  21.          
  22.         self.settings = self.init_settings()
  23.         self.temp_dir = None
  24.         self.item_to_move = None # 用于“点击-移动”功能的状态变量
  25.          
  26.         self.initUI()
  27.         self.load_settings()
  28.          
  29.         self.setAcceptDrops(True)

  30.     def init_settings(self):
  31.         config_file_name = 'config.ini'
  32.         if getattr(sys, 'frozen', False):
  33.             application_path = os.path.dirname(sys.executable)
  34.         else:
  35.             application_path = os.path.dirname(os.path.abspath(__file__))
  36.          
  37.         self.config_path = os.path.join(application_path, config_file_name)
  38.         return QSettings(self.config_path, QSettings.IniFormat)

  39.     def initUI(self):
  40.         self.setWindowTitle('批量图片重命名与格式转换工具')
  41.         self.setGeometry(100, 100, 1000, 650)
  42.         if os.path.exists(APP_ICON_PATH): self.setWindowIcon(QIcon(APP_ICON_PATH))
  43.         self.create_menus()
  44.         central_widget = QWidget()
  45.         self.setCentralWidget(central_widget)
  46.         main_layout = QHBoxLayout(central_widget)
  47.         self.statusBar = QStatusBar()
  48.         self.setStatusBar(self.statusBar)
  49.         self.statusBar.showMessage('准备就绪。单击选择,按Delete删除,单击移动。')
  50.          
  51.         left_layout = QVBoxLayout()
  52.         left_label = QLabel('<h3>1. 添加并排序图片</h3>')
  53.         self.image_list_widget = QListWidget()
  54.         self.image_list_widget.setDragDropMode(QAbstractItemView.NoDragDrop)
  55.         self.image_list_widget.setSelectionMode(QAbstractItemView.ExtendedSelection)
  56.         self.image_list_widget.itemClicked.connect(self.handle_item_click)
  57.         self.image_list_widget.currentItemChanged.connect(self.update_preview)
  58.         self.image_list_widget.setIconSize(QSize(64, 64))
  59.          
  60.         top_buttons_layout = QHBoxLayout()
  61.         self.add_button = QPushButton('添加图片'); self.add_button.clicked.connect(self.add_images)
  62.         self.paste_button = QPushButton('粘贴 (Ctrl+V)'); self.paste_button.clicked.connect(self.paste_from_clipboard)
  63.         self.clear_button = QPushButton('清空列表'); self.clear_button.clicked.connect(self.clear_all)
  64.         top_buttons_layout.addWidget(self.add_button); top_buttons_layout.addWidget(self.paste_button); top_buttons_layout.addWidget(self.clear_button)
  65.          
  66.         sort_group_label = QLabel("<b>预排序:</b>")
  67.         sort_buttons_layout = QHBoxLayout()
  68.         self.sort_name_asc_btn = QPushButton("名称 ↑"); self.sort_name_asc_btn.clicked.connect(lambda: self.sort_items(by='name'))
  69.         self.sort_name_desc_btn = QPushButton("名称 ↓"); self.sort_name_desc_btn.clicked.connect(lambda: self.sort_items(by='name', reverse=True))
  70.         self.sort_date_asc_btn = QPushButton("时间 ↑"); self.sort_date_asc_btn.clicked.connect(lambda: self.sort_items(by='mtime'))
  71.         self.sort_date_desc_btn = QPushButton("时间 ↓"); self.sort_date_desc_btn.clicked.connect(lambda: self.sort_items(by='mtime', reverse=True))
  72.         self.sort_name_asc_btn.setToolTip("按文件名升序排列"); self.sort_name_desc_btn.setToolTip("按文件名降序排列")
  73.         self.sort_date_asc_btn.setToolTip("按修改时间升序排列 (旧->新)"); self.sort_date_desc_btn.setToolTip("按修改时间降序排列 (新->旧)")
  74.         sort_buttons_layout.addWidget(sort_group_label); sort_buttons_layout.addWidget(self.sort_name_asc_btn); sort_buttons_layout.addWidget(self.sort_name_desc_btn); sort_buttons_layout.addWidget(self.sort_date_asc_btn); sort_buttons_layout.addWidget(self.sort_date_desc_btn)
  75.          
  76.         left_layout.addWidget(left_label); left_layout.addWidget(self.image_list_widget); left_layout.addLayout(top_buttons_layout)
  77.         line = QFrame(); line.setFrameShape(QFrame.HLine); line.setFrameShadow(QFrame.Sunken)
  78.         left_layout.addWidget(line); left_layout.addLayout(sort_buttons_layout)
  79.          
  80.         center_layout = QVBoxLayout(); preview_label_title = QLabel('<h3>图片预览</h3>'); self.preview_label = QLabel('请先在左侧选择一张图片'); self.preview_label.setAlignment(Qt.AlignCenter); self.preview_label.setFixedSize(350, 350); self.preview_label.setStyleSheet("border: 1px solid #ccc; background-color: #f0f0f0;"); center_layout.addWidget(preview_label_title); center_layout.addWidget(self.preview_label); center_layout.addStretch()
  81.         right_layout = QVBoxLayout(); name_label = QLabel('<h3>2. 输入新名称 (每行一个)</h3>'); self.name_input = QTextEdit(); self.name_input.setPlaceholderText('新名字1\n新名字2\n新名字3\n...'); settings_label = QLabel('<h3>3. 输出设置</h3>'); format_layout = QHBoxLayout(); format_label = QLabel('输出格式:'); self.format_combo = QComboBox(); self.format_combo.addItems(['.jpg', '.png', '.bmp', '.gif', '.tiff']); format_layout.addWidget(format_label); format_layout.addWidget(self.format_combo); folder_layout = QHBoxLayout(); folder_label = QLabel('输出文件夹:'); self.output_path_line = QLineEdit(); self.output_path_line.setReadOnly(True); self.output_folder_button = QPushButton('浏览...'); self.output_folder_button.clicked.connect(self.select_output_folder); folder_layout.addWidget(folder_label); folder_layout.addWidget(self.output_path_line); folder_layout.addWidget(self.output_folder_button); self.start_button = QPushButton('开始处理'); self.start_button.setMinimumHeight(40); self.start_button.setStyleSheet("background-color: #4CAF50; color: white; font-weight: bold;"); self.start_button.clicked.connect(self.start_processing); right_layout.addWidget(name_label); right_layout.addWidget(self.name_input); right_layout.addSpacing(20); right_layout.addWidget(settings_label); right_layout.addLayout(format_layout); right_layout.addLayout(folder_layout); right_layout.addStretch(); right_layout.addWidget(self.start_button)
  82.         main_layout.addLayout(left_layout, 2); main_layout.addLayout(center_layout, 2); main_layout.addLayout(right_layout, 2)

  83.     def keyPressEvent(self, event):
  84.         if event.key() == Qt.Key_Delete: self.delete_selected_items()
  85.         elif event.key() == Qt.Key_Escape: self.cancel_move()
  86.         elif event.matches(QKeySequence.Paste): self.paste_from_clipboard()
  87.         else: super().keyPressEvent(event)

  88.     def delete_selected_items(self):
  89.         selected_items = self.image_list_widget.selectedItems()
  90.         if not selected_items: return
  91.         reply = QMessageBox.question(self, '确认删除', f'确定要从列表中删除这 {len(selected_items)} 个项目吗?\n(此操作不可撤销)', QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
  92.         if reply == QMessageBox.Yes:
  93.             for item in selected_items:
  94.                 file_path = item.data(Qt.UserRole)
  95.                 if self.temp_dir and self.temp_dir in file_path:
  96.                     try: os.remove(file_path)
  97.                     except OSError as e: print(f"删除临时文件失败: {e}")
  98.                 row = self.image_list_widget.row(item); self.image_list_widget.takeItem(row)
  99.             self.statusBar.showMessage(f"成功删除 {len(selected_items)} 个项目。")
  100.             if self.item_to_move in selected_items: self.item_to_move = None

  101.     def handle_item_click(self, clicked_item):
  102.         if not self.item_to_move:
  103.             self.item_to_move = clicked_item; font = self.item_to_move.font(); font.setBold(True); self.item_to_move.setFont(font)
  104.             self.statusBar.showMessage(f"已选择 '{os.path.basename(self.item_to_move.data(Qt.UserRole))}'。请点击目标位置以移动。")
  105.         else:
  106.             font = self.item_to_move.font(); font.setBold(False); self.item_to_move.setFont(font)
  107.             if self.item_to_move is not clicked_item:
  108.                 from_row = self.image_list_widget.row(self.item_to_move); to_row = self.image_list_widget.row(clicked_item)
  109.                 item = self.image_list_widget.takeItem(from_row); self.image_list_widget.insertItem(to_row, item)
  110.                 self.image_list_widget.setCurrentItem(item)
  111.             self.item_to_move = None; self.statusBar.showMessage("移动操作完成。")

  112.     def cancel_move(self):
  113.         if self.item_to_move:
  114.             font = self.item_to_move.font(); font.setBold(False); self.item_to_move.setFont(font)
  115.             self.item_to_move = None; self.statusBar.showMessage("移动操作已取消。")

  116.     def sort_items(self, by='name', reverse=False):
  117.         self.cancel_move()
  118.         item_data_list = []
  119.         for i in range(self.image_list_widget.count()):
  120.             item = self.image_list_widget.item(i)
  121.             item_data_list.append((item.data(Qt.UserRole), item.text()))
  122.         if not item_data_list: return

  123.         if by == 'name': key_func = lambda data: data[1].lower()
  124.         elif by == 'mtime': key_func = lambda data: os.path.getmtime(data[0])
  125.         else: return

  126.         item_data_list.sort(key=key_func, reverse=reverse)
  127.         self.image_list_widget.clear()

  128.         for file_path, display_text in item_data_list:
  129.             new_item = QListWidgetItem(display_text)
  130.             new_item.setData(Qt.UserRole, file_path)
  131.             new_item.setIcon(QIcon(file_path))
  132.             self.image_list_widget.addItem(new_item)
  133.         self.statusBar.showMessage(f"列表已按 {'降序' if reverse else '升序'} 排列。")

  134.     def dragEnterEvent(self, event):
  135.         if event.mimeData().hasUrls(): event.acceptProposedAction()
  136.         else: event.ignore()

  137.     def dropEvent(self, event):
  138.         if event.mimeData().hasUrls():
  139.             urls = event.mimeData().urls(); self._process_file_urls(urls, "拖拽"); event.acceptProposedAction()

  140.     def paste_from_clipboard(self):
  141.         clipboard = QApplication.clipboard(); mime_data = clipboard.mimeData()
  142.         if mime_data.hasImage(): self._paste_image_data(clipboard.image())
  143.         elif mime_data.hasUrls(): self._process_file_urls(mime_data.urls(), "粘贴")
  144.         else: self.statusBar.showMessage('剪贴板中没有可识别的图片或图片文件。')

  145.     def _paste_image_data(self, q_image):
  146.         if q_image.isNull(): self.statusBar.showMessage('无法获取剪贴板中的图片数据。'); return
  147.         if self.temp_dir is None: self.temp_dir = tempfile.mkdtemp(prefix="ImageTool_")
  148.         timestamp = int(time.time() * 1000); temp_filename = f"pasted_image_{timestamp}.png"
  149.         temp_filepath = os.path.join(self.temp_dir, temp_filename); q_image.save(temp_filepath, 'PNG')
  150.         self._add_image_item(temp_filepath); self.statusBar.showMessage(f'已从剪贴板粘贴图片: {temp_filename}')

  151.     def _process_file_urls(self, urls, source_action="添加"):
  152.         added_count = 0
  153.         for url in urls:
  154.             if url.isLocalFile():
  155.                 file_path = url.toLocalFile()
  156.                 if os.path.isfile(file_path) and file_path.lower().endswith(self.SUPPORTED_EXTENSIONS):
  157.                     self._add_image_item(file_path); added_count += 1
  158.         if added_count > 0: self.statusBar.showMessage(f'通过{source_action}添加了 {added_count} 张图片。')
  159.         else: self.statusBar.showMessage(f'没有通过{source_action}添加有效的图片文件。')

  160.     def _add_image_item(self, file_path):
  161.         item = QListWidgetItem(os.path.basename(file_path)); item.setData(Qt.UserRole, file_path)
  162.         item.setIcon(QIcon(file_path)); self.image_list_widget.addItem(item)
  163.         self.image_list_widget.scrollToItem(item)

  164.     def add_images(self):
  165.         files, _ = QFileDialog.getOpenFileNames(self, "选择图片文件", "", f"Image Files (*{' *'.join(self.SUPPORTED_EXTENSIONS)})")
  166.         if files: self._process_file_urls([QUrl.fromLocalFile(f) for f in files], "选择文件")

  167.     def closeEvent(self, event):
  168.         self.save_settings()
  169.         if self.temp_dir and os.path.exists(self.temp_dir):
  170.             try: shutil.rmtree(self.temp_dir)
  171.             except Exception as e: print(f"清理临时文件夹失败: {e}")
  172.         super(ImageBatchProcessor, self).closeEvent(event)

  173.     def create_menus(self):
  174.         menu_bar = self.menuBar(); settings_menu = menu_bar.addMenu('设置 (&S)'); font_menu = QMenu('字体大小', self); increase_font_action = QAction('增大字体 (+)', self); increase_font_action.triggered.connect(lambda: self.adjust_font_size(1)); increase_font_action.setShortcut('Ctrl++'); decrease_font_action = QAction('减小字体 (-)', self); decrease_font_action.triggered.connect(lambda: self.adjust_font_size(-1)); decrease_font_action.setShortcut('Ctrl+-'); reset_font_action = QAction('重置为默认', self); reset_font_action.triggered.connect(self.reset_font_size); reset_font_action.setShortcut('Ctrl+0'); font_menu.addAction(increase_font_action); font_menu.addAction(decrease_font_action); font_menu.addSeparator(); font_menu.addAction(reset_font_action); settings_menu.addMenu(font_menu)

  175.     def adjust_font_size(self, delta):
  176.         font = QApplication.font(); current_size = font.pointSize(); new_size = current_size + delta;
  177.         if 5 < new_size < 30: font.setPointSize(new_size); QApplication.setFont(font); self.statusBar.showMessage(f"字体大小已设置为 {new_size}pt")

  178.     def reset_font_size(self):
  179.         font = QApplication.font(); font.setPointSize(self.DEFAULT_FONT_SIZE); QApplication.setFont(font); self.statusBar.showMessage(f"字体大小已重置为 {self.DEFAULT_FONT_SIZE}pt")

  180.     def load_settings(self):
  181.         self.statusBar.showMessage(f"从 {os.path.basename(self.config_path)} 加载配置..."); geometry = self.settings.value("geometry", self.saveGeometry()); self.restoreGeometry(geometry); state = self.settings.value("windowState", self.saveState()); self.restoreState(state); font_size = self.settings.value("fontSize", self.DEFAULT_FONT_SIZE, type=int); font = QApplication.font(); font.setPointSize(font_size); QApplication.setFont(font); self.statusBar.showMessage(f"配置已加载,当前字体: {font_size}pt")

  182.     def save_settings(self):
  183.         self.settings.setValue("geometry", self.saveGeometry()); self.settings.setValue("windowState", self.saveState()); self.settings.setValue("fontSize", QApplication.font().pointSize()); self.statusBar.showMessage(f"配置已保存到 {os.path.basename(self.config_path)}")

  184.     def update_preview(self, current_item, previous_item):
  185.         if not current_item: self.preview_label.clear(); self.preview_label.setText('请先在左侧选择一张图片'); return
  186.         path = current_item.data(Qt.UserRole); pixmap = QPixmap(path); scaled_pixmap = pixmap.scaled(self.preview_label.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation); self.preview_label.setPixmap(scaled_pixmap)

  187.     def select_output_folder(self):
  188.         folder = QFileDialog.getExistingDirectory(self, "选择输出文件夹");
  189.         if folder: self.output_path_line.setText(folder); self.statusBar.showMessage(f'输出文件夹已选择: {folder}')

  190.     def clear_all(self):
  191.         self.cancel_move()
  192.         if self.temp_dir and os.path.exists(self.temp_dir): shutil.rmtree(self.temp_dir); self.temp_dir = None
  193.         self.image_list_widget.clear(); self.name_input.clear(); self.output_path_line.clear(); self.preview_label.clear(); self.preview_label.setText('请先在左侧选择一张图片'); self.statusBar.showMessage('已清空所有内容。')

  194.     def start_processing(self):
  195.         self.cancel_move()
  196.         image_count = self.image_list_widget.count()
  197.         if image_count == 0: QMessageBox.warning(self, '错误', '请先添加图片!'); return
  198.         new_names = [line for line in self.name_input.toPlainText().splitlines() if line.strip()]
  199.         if len(new_names) != image_count: QMessageBox.warning(self, '错误', f'图片数量 ({image_count}) 与新名称数量 ({len(new_names)}) 不匹配!'); return
  200.         output_dir = self.output_path_line.text()
  201.         if not output_dir or not os.path.isdir(output_dir): QMessageBox.warning(self, '错误', '请选择一个有效的输出文件夹!'); return
  202.         output_format = self.format_combo.currentText(); processed_count = 0
  203.         for i in range(image_count):
  204.             try:
  205.                 item = self.image_list_widget.item(i); original_path = item.data(Qt.UserRole); new_name_base = new_names[i]; new_filename = f"{new_name_base}{output_format}"; new_filepath = os.path.join(output_dir, new_filename); self.statusBar.showMessage(f'正在处理: {os.path.basename(original_path)} -> {new_filename}')
  206.                 with Image.open(original_path) as img:
  207.                     if img.mode in ('RGBA', 'P') and output_format.lower() in ['.jpg', '.jpeg']: img = img.convert('RGB')
  208.                     img.save(new_filepath)
  209.             except Exception as e: QMessageBox.critical(self, '处理失败', f'处理文件 {os.path.basename(original_path)} 时发生错误:\n{str(e)}'); self.statusBar.showMessage('处理中断。'); return
  210.             processed_count += 1
  211.         QMessageBox.information(self, '成功', f'成功处理了 {processed_count} 张图片!\n文件已保存到: {output_dir}')


  212. if __name__ == '__main__':
  213.     app = QApplication(sys.argv)
  214.     ex = ImageBatchProcessor()
  215.     ex.show()
  216.     sys.exit(app.exec_())
复制代码


G8怀旧社区分享AI图片重命名工具:提高效率_优化管理_必备神器 第1张截图
回复

使用道具 举报

怀旧资源论坛回复图
B Color Link Quote Code Smilies
您需要登录后才可以回帖 登录 | 注册
高级模式
返回版块列表主题页