diff --git a/beeref/fileio/export.py b/beeref/fileio/export.py index 69267b3..23b8857 100644 --- a/beeref/fileio/export.py +++ b/beeref/fileio/export.py @@ -13,269 +13,307 @@ # You should have received a copy of the GNU General Public License # along with BeeRef. If not, see . -import base64 -import logging -import pathlib -from xml.etree import ElementTree as ET +# 导入必要的模块和类 +import base64 # 用于编码图片数据 +import logging # 用于记录日志 +import pathlib # 用于处理文件路径 +from xml.etree import ElementTree as ET # 用于创建XML文档 -from PyQt6 import QtCore, QtGui +from PyQt6 import QtCore, QtGui # 用于处理Qt的核心功能和图形用户界面 -from .errors import BeeFileIOError -from beeref import constants, widgets -from beeref.items import BeePixmapItem +from .errors import BeeFileIOError # 自定义异常类,用于导出时的错误处理 +from beeref import constants, widgets # 导入 BeeRef 常量和小部件 +from beeref.items import BeePixmapItem # 导入 BeeRef 图片项类 -logger = logging.getLogger(__name__) +# 创建日志记录器 +logger = logging.getLogger(__name__) -class ExporterRegistry(dict): +# 导出器注册类,继承自字典类 +class ExporterRegistry(dict): - DEFAULT_TYPE = 0 + DEFAULT_TYPE = 0 # 默认导出器类型 - def __getitem__(self, key): - key = key.removeprefix('.') - exp = self.get(key, super().__getitem__(self.DEFAULT_TYPE)) - logger.debug(f'Exporter for type {key}: {exp}') - return exp + def __getitem__(self, key): # 获取指定类型的导出器 + key = key.removeprefix('.') # 移除类型前缀的点号 + exp = self.get(key, super().__getitem__(self.DEFAULT_TYPE)) # 获取指定类型的导出器,若不存在则返回默认导出器 + logger.debug(f'Exporter for type {key}: {exp}') # 记录获取的导出器 + return exp # 返回获取的导出器 -exporter_registry = ExporterRegistry() +# 创建导出器注册表实例 +exporter_registry = ExporterRegistry() -def register_exporter(cls): - exporter_registry[cls.TYPE] = cls - return cls +# 导出器注册装饰器,用于注册导出器类 +def register_exporter(cls): + exporter_registry[cls.TYPE] = cls # 将导出器类注册到导出器注册实例中 + return cls # 返回导出器类注册装饰器,用于注册导出器类 +# 导出器基类,用于定义导出器的通用方法 class ExporterBase: + # 发送开始处理信号 def emit_begin_processing(self, worker, start): - if worker: - worker.begin_processing.emit(start) - + if worker: # 如果有工作线程 + worker.begin_processing.emit(start) # 触发工作线程的开始处理信号 + else: # 若没有工作线程 + logger.debug(f'No worker, emit begin processing signal: {start}') # 若没有工作线程,则记录日志 + # 发送进度更新信号 def emit_progress(self, worker, progress): - if worker: - worker.progress.emit(progress) + if worker: # 若有工作线程 + worker.progress.emit(progress) # 若有工作线程,则触发工作线程的进度更新信号 + # 发送完成信号 def emit_finished(self, worker, filename, errors): - filename = str(filename) - if worker: - worker.finished.emit(filename, errors) + filename = str(filename) # 确保文件名是字符串类型 + if worker: # 若有工作线程 + worker.finished.emit(filename, errors) # 若有工作线程,则触发工作线程的完成信号 + # 发送需要用户输入信号 def emit_user_input_required(self, worker, msg): - if worker: - worker.user_input_required.emit(msg) + if worker: # 若有工作线程 + worker.user_input_required.emit(msg) # 若有工作线程,则触发工作线程的用户输入要求信号 + # 处理导出错误 def handle_export_error(self, filename, error, worker): - filename = str(filename) - logger.debug(f'Export failed: {error}') - if worker: - worker.finished.emit(filename, [str(error)]) - return - else: - e = error if isinstance(error, Exception) else None - raise BeeFileIOError(msg=str(error), filename=filename) from e + filename = str(filename) # 确保文件名是字符串类型 + logger.debug(f'Export failed: {error}') # 记录导出失败的日志 + if worker: # 若有工作线程 + worker.finished.emit(filename, [str(error)]) # 若有工作线程,则触发工作线程的完成信号,包含错误信息 + return # 若有工作线程,则返回错误信息 + else: # 若没有工作线程 + e = error if isinstance(error, Exception) else None # 若错误不是异常类型,则设为None + raise BeeFileIOError(msg=str(error), filename=filename) from e # 抛出自定义异常,包含错误信息和文件名 +# 场景导出器基类,用于定义场景导出器为单个图像 class SceneExporterBase(ExporterBase): - """For exporting the scene to a single image.""" + """For exporting the scene to a single image.""" # 场景导出器基类,用于定义场景导出器的通用方法 + # 场景导出器基类的初始化方法 def __init__(self, scene): - self.scene = scene - self.scene.cancel_active_modes() - self.scene.deselect_all_items() + self.scene = scene # 场景导出器基类的初始化方法,接收场景作为参数 + self.scene.cancel_active_modes() # 取消场景中所有活动模式 + self.scene.deselect_all_items() # 取消场景中所有项的选择 # Selection outlines/handles will be rendered to the exported # image, so deselect first. (Alternatively, pass an attribute # to paint functions to not paint them?) - rect = self.scene.itemsBoundingRect() - logger.trace(f'Items bounding rect: {rect}') - size = QtCore.QSize(int(rect.width()), int(rect.height())) - logger.trace(f'Export size without margins: {size}') - self.margin = max(size.width(), size.height()) * 0.03 - self.default_size = size.grownBy( - QtCore.QMargins(*([int(self.margin)] * 4))) - logger.debug(f'Default export margin: {self.margin}') - logger.debug(f'Default export size with margins: {self.default_size}') + rect = self.scene.itemsBoundingRect() # 获取场景中所有项的边界矩形 + logger.trace(f'Items bounding rect: {rect}') # 记录场景中所有项的边界矩形 + size = QtCore.QSize(int(rect.width()), int(rect.height())) # 计算导出图像的大小,取项边界矩形的宽度和高度 + logger.trace(f'Export size without margins: {size}') # 记录导出图像的大小,不包含边距 + self.margin = max(size.width(), size.height()) * 0.03 # 计算导出图像的边距,取宽度和高度的较大值乘以0.03 + self.default_size = size.grownBy( # 计算导出图像的大小,包含边距 + QtCore.QMargins(*([int(self.margin)] * 4))) # 计算导出图像的大小,包含边距 + logger.debug(f'Default export margin: {self.margin}') # 记录导出图像的边距 + logger.debug(f'Default export size with margins: {self.default_size}')# 记录导出图像的大小,包含边距 +# 注册场景到像素图导出器 @register_exporter -class SceneToPixmapExporter(SceneExporterBase): +class SceneToPixmapExporter(SceneExporterBase): # 场景导出器基类,用于定义场景导出器的通用方法 - TYPE = ExporterRegistry.DEFAULT_TYPE + TYPE = ExporterRegistry.DEFAULT_TYPE # 设置为默认导出类型 + # 场景导出器基类的用户输入方法,用于获取用户输入的导出大小 def get_user_input(self, parent): - """Ask user for final export size.""" + """Ask user for final export size.""" # 场景导出器基类的用户输入方法,用于获取用户输入的导出大小 - dialog = widgets.SceneToPixmapExporterDialog( - parent=parent, - default_size=self.default_size, + # 创建导出对话框,让用户输入最终导出尺寸 + dialog = widgets.SceneToPixmapExporterDialog( # 创建场景导出器对话框实例 + parent=parent, # 场景导出器对话框的父窗口 + default_size=self.default_size, # 场景导出器对话框的默认导出大小 ) - if dialog.exec(): - size = dialog.value() - logger.debug(f'Got export size {size}') - self.size = size - return True - else: - return False + if dialog.exec(): # 若场景导出器对话框执行成功 + size = dialog.value() # 获取场景导出器对话框中用户输入的导出大小 + logger.debug(f'Got export size {size}') # 记录用户输入的导出大小 + self.size = size # 将用户输入的导出大小赋值给场景导出器基类的属性 + return True # 返回True表示用户输入成功 + else: # 若场景导出器对话框执行失败 + return False # 返回False表示用户输入失败 + # 场景导出器基类的渲染方法,用于将场景渲染为图像 def render_to_image(self): - logger.debug(f'Final export size: {self.size}') - margin = self.margin * self.size.width() / self.default_size.width() - logger.debug(f'Final export margin: {margin}') + logger.debug(f'Final export size: {self.size}') # 记录最终导出图像的大小 + # 按比例调整边距 + margin = self.margin * self.size.width() / self.default_size.width() # 计算最终导出图像的边距,取宽度和高度的较大值乘以0.03 + logger.debug(f'Final export margin: {margin}') # 记录最终导出图像的边距 - image = QtGui.QImage(self.size, QtGui.QImage.Format.Format_RGB32) - image.fill(QtGui.QColor(*constants.COLORS['Scene:Canvas'])) - painter = QtGui.QPainter(image) + image = QtGui.QImage(self.size, QtGui.QImage.Format.Format_RGB32) # 创建导出图像,大小为最终导出大小,格式为RGB32 + image.fill(QtGui.QColor(*constants.COLORS['Scene:Canvas'])) # 用画布颜色作为导出图像的背景颜色 + painter = QtGui.QPainter(image) # 创建画家对象 + # 定义目标矩形 target_rect = QtCore.QRectF( - margin, - margin, - self.size.width() - 2 * margin, - self.size.height() - 2 * margin) - logger.trace(f'Final export target_rect: {target_rect}') - self.scene.render(painter, - source=self.scene.itemsBoundingRect(), - target=target_rect) - painter.end() - return image + margin, # 取导出图像的边距作为偏移量 + margin, # 取导出图像的边距作为偏移量 + self.size.width() - 2 * margin, # 取导出图像的宽度减去2倍边距作为矩形宽度 + self.size.height() - 2 * margin) # 取导出图像的高度减去2倍边距作为矩形高度 + logger.trace(f'Final export target_rect: {target_rect}') # 记录最终导出图像的目标矩形 + self.scene.render(painter, # 渲染场景到导出图像的绘图器 + source=self.scene.itemsBoundingRect(), # 获取场景中所有项的边界矩形 + target=target_rect) # 将场景渲染到最终导出图像的目标矩形 + painter.end() # 结束导出图像的绘图器 + return image # 返回导出图像 + + # 场景导出器基类的导出方法,用于将场景导出为图像文件 def export(self, filename, worker=None): - logger.debug(f'Exporting scene to {filename}') - self.emit_begin_processing(worker, 1) - image = self.render_to_image() + logger.debug(f'Exporting scene to {filename}') # 记录导出场景的文件名 + self.emit_begin_processing(worker, 1) # 发送导出开始信号,参数为导出器实例和导出任务数量 + image = self.render_to_image() # 调用场景导出器基类的渲染方法,渲染场景到图像 + # 若导出器实例存在且取消导出标志为True if worker and worker.canceled: - logger.debug('Export canceled') - self.emit_finished(worker, filename, []) - return + logger.debug('Export canceled') # 记录导出被取消的信息 + self.emit_finished(worker, filename, []) # 发送导出完成信号,参数为导出器实例、导出文件名和空列表 + return # 若导出器实例存在且取消导出标志为True,则直接返回,不导出图像 + # 若导出图像保存失败 if not image.save(filename, quality=90): - self.handle_export_error(filename, 'Error writing file', worker) - return - - logger.debug('Export finished') - self.emit_progress(worker, 1) - self.emit_finished(worker, filename, []) + self.handle_export_error(filename, 'Error writing file', worker) # 处理导出错误,参数为导出文件名、错误信息和导出器实例 + return # 返回,不继续导出 + logger.debug('Export finished') # 记录导出完成的信息 + self.emit_progress(worker, 1) # 发送导出进度信号,参数为导出器实例和导出进度值1 + self.emit_finished(worker, filename, []) # 发送导出完成信号,参数为导出器实例、导出文件名和空列表 +# 注册场景到SVG, @register_exporter class SceneToSVGExporter(SceneExporterBase): - TYPE = 'svg' + TYPE = 'svg' # 场景导出器基类的导出类型属性,值为'svg',表示导出为SVG格式 + # 场景导出器基类的用户输入方法,用于获取用户输入的导出大小 def get_user_input(self, parent): - self.size = self.default_size - return True + self.size = self.default_size # 将场景导出器基类的默认导出大小赋值给场景导出器基类的属性 + return True # 返回True表示用户输入成功 + # 场景导出器基类的文本样式方法,用于获取项的字体样式 def _get_textstyles(self, item): + # 字体样式映射字典,将QFont的字体样式枚举值映射为SVG的字体样式字符串 fontstylemap = { - QtGui.QFont.Style.StyleNormal: 'normal', - QtGui.QFont.Style.StyleItalic: 'italic', - QtGui.QFont.Style.StyleOblique: 'oblique', + QtGui.QFont.Style.StyleNormal: 'normal', # 将QFont的字体样式枚举值StyleNormal映射为SVG的字体样式字符串normal + QtGui.QFont.Style.StyleItalic: 'italic', # 将QFont的字体样式枚举值StyleItalic映射为SVG的字体样式字符串italic + QtGui.QFont.Style.StyleOblique: 'oblique', # 将QFont的字体样式枚举值StyleOblique映射为SVG的字体样式字符串oblique } - font = item.font() - fontsize = font.pointSize() * item.scale() - families = ', '.join(font.families()) - fontstyle = fontstylemap[font.style()] + font = item.font() # 获取项的字体 + fontsize = font.pointSize() * item.scale() # 计算项的字体大小,取字体点大小乘以项的缩放比例 + families = ', '.join(font.families()) # 取字体的所有字体家族,用逗号分隔 + fontstyle = fontstylemap[font.style()] # 取字体的字体样式,根据字体样式映射表映射为SVG的字体样式字符串 + # 返回CSS样式元组 return ('white-space:pre', - f'font-size:{fontsize}pt', - f'font-family:{families}', - f'font-weight:{font.weight()}', - f'font-stretch:{font.stretch()}', - f'font-style:{fontstyle}') + f'font-size:{fontsize}pt', # 取项的字体大小,取字体点大小乘以项的缩放比例 + f'font-family:{families}', # 取项的字体家族,用逗号分隔 + f'font-weight:{font.weight()}', # 取项的字体重量 + f'font-stretch:{font.stretch()}', # 取项的字体拉伸 + f'font-style:{fontstyle}') # 取项的字体样式,根据字体样式映射表映射为SVG的字体样式字符串 + # 场景导出器基类的导出方法,用于将场景导出为SVG文件 def render_to_svg(self, worker=None): - svg = ET.Element( + svg = ET.Element( # 创建SVG元素,参数为元素标签名svg 'svg', - attrib={'width': str(self.size.width()), - 'height': str(self.size.height()), - 'xmlns': 'http://www.w3.org/2000/svg', - 'xmlns:xlink': 'http://www.w3.org/1999/xlink', + attrib={'width': str(self.size.width()), # 设置SVG元素的宽度属性,值为场景导出器基类的属性size的宽度 + 'height': str(self.size.height()), # 设置SVG元素的高度属性,值为场景导出器基类的属性size的高度 + 'xmlns': 'http://www.w3.org/2000/svg', # 设置SVG元素的XML命名空间属性,值为http://www.w3.org/2000/svg + 'xmlns:xlink': 'http://www.w3.org/1999/xlink', # 设置SVG元素的XML链接命名空间属性,值为http://www.w3.org/1999/xlink }) - rect = self.scene.itemsBoundingRect() - offset = rect.topLeft() - QtCore.QPointF(self.margin, self.margin) + rect = self.scene.itemsBoundingRect() # 获取场景中所有项的边界矩形 + offset = rect.topLeft() - QtCore.QPointF(self.margin, self.margin)# 计算项的位置偏移量,取场景中所有项的边界矩形的左上角坐标减去导出器基类的属性margin + # 按z值排序项目并遍历 for i, item in enumerate(sorted(self.scene.items(), key=lambda x: x.zValue())): # z order in SVG specified via the order of elements in the tree - pos = item.pos() - offset - anchor = pos + pos = item.pos() - offset # 计算项的位置,取项的位置减去项的位置偏移量 + anchor = pos # 设置项的变换锚点,取项的位置 - if item.TYPE == 'text': - styles = self._get_textstyles(item) - element = ET.Element( - 'text', - attrib={'style': ';'.join(styles), - 'dominant-baseline': 'hanging'}) - element.text = item.toPlainText() - if item.TYPE == 'pixmap': - width = item.width * item.scale() - height = item.height * item.scale() - pixmap, imgformat = item.pixmap_to_bytes( - apply_grayscale=True, - apply_crop=True) - pixmap = base64.b64encode(pixmap).decode('ascii') - element = ET.Element( - 'image', - attrib={ - 'xlink:href': - f'data:image/{imgformat};base64,{pixmap}', - 'width': str(width), - 'height': str(height), - 'image-rendering': ('crisp-edges' if item.scale() > 2 + # 处理文本项 + if item.TYPE == 'text': # 若项的类型为文本项 + styles = self._get_textstyles(item) # 获取项的字体样式 + element = ET.Element( # 创建文本元素,参数为元素标签名text + 'text', # 设置文本元素的标签名,值为text + attrib={'style': ';'.join(styles), # 将样式列表用分号连接成CSS样式字符串,设置为元素的style属性 + 'dominant-baseline': 'hanging'}) # 设置文本元素的属性,值为hanging,指定文本的基线位置为悬挂基线 + element.text = item.toPlainText() # 设置文本元素的文本内容,值为项的纯文本内容 + # 处理图片项 + if item.TYPE == 'pixmap': # 若项的类型为图片项 + width = item.width * item.scale() # 计算项的宽度,取项的宽度乘以项的缩放比例 + height = item.height * item.scale() # 计算项的高度,取项的高度乘以项的缩放比例 + pixmap, imgformat = item.pixmap_to_bytes( # 将项的像素图转换为字节数组,参数为项的像素图,是否应用灰度化,是否应用裁剪 + apply_grayscale=True, # 是否应用灰度化,值为True + apply_crop=True) # 是否应用裁剪,值为True + pixmap = base64.b64encode(pixmap).decode('ascii')# 将项的像素图字节数组转换为Base64编码的字符串 + element = ET.Element( # 创建图片元素,参数为元素标签名image + 'image', # 设置图片元素的标签名,值为image + attrib={ # 设置图片元素的属性,值为项的像素图Base64编码的字符串 + 'xlink:href': # 设置图片元素的XML链接属性,值为项的像素图Base64编码的字符串 + f'data:image/{imgformat};base64,{pixmap}',# 设置图片元素的XML链接属性,值为项的像素图Base64编码的字符串 + 'width': str(width), # 设置图片元素的宽度属性,值为项的宽度乘以项的缩放比例 + 'height': str(height), # 设置图片元素的高度属性,值为项的高度乘以项的缩放比例 + 'image-rendering': ('crisp-edges' if item.scale() > 2# 设置图片元素的渲染属性,值为crisp-edges或optimizeQuality,根据项的缩放比例判断 else 'optimizeQuality')}) - pos = pos + item.crop.topLeft() + pos = pos + item.crop.topLeft() # 计算项的位置,取项的位置加上项的裁剪区域的左上角坐标 - transforms = [] - if item.flip() == -1: + #设置变换属性 + transforms = [] # 创建变换列表,用于存储项的变换操作 + if item.flip() == -1: # 若项的翻转方向为-1 # The following is not recognised by Inkscape and not an # official standard: # element.set('transform-origin', f'{anchor.x()} {anchor.y()}') # Thus we need to fix the origin manually - transforms.append(f'translate({anchor.x()} {anchor.y()})') - transforms.append(f'scale({item.flip()} 1)') - transforms.append(f'translate(-{anchor.x()} -{anchor.y()})') + transforms.append(f'translate({anchor.x()} {anchor.y()})') # 若项的翻转方向为-1,添加变换操作,将项的锚点平移到项的位置 + transforms.append(f'scale({item.flip()} 1)') # 添加变换操作,将项的像素图水平翻转 + transforms.append(f'translate(-{anchor.x()} -{anchor.y()})') # 添加变换操作,将项的锚点平移回项的位置 + + #添加变换操作,将项的像素图旋转项的旋转角度 transforms.append( - f'rotate({item.rotation()} {anchor.x()} {anchor.y()})') + f'rotate({item.rotation()} {anchor.x()} {anchor.y()})') #围绕项的锚点旋转项的旋转角度 - element.set('transform', ' '.join(transforms)) - element.set('x', str(pos.x())) - element.set('y', str(pos.y())) - element.set('opacity', str(item.opacity())) + #应用变换 + element.set('transform', ' '.join(transforms)) # 设置元素的变换属性,值为变换列表用空格连接成的字符串 + element.set('x', str(pos.x())) # 设置元素的x属性,值为项的位置的x坐标 + element.set('y', str(pos.y())) # 设置元素的y属性,值为项的位置的y坐标 + element.set('opacity', str(item.opacity())) # 设置元素的透明度属性,值为项的透明度 - svg.append(element) - self.emit_progress(worker, i) - if worker and worker.canceled: - return + svg.append(element) # 将元素添加到SVG根元素中 + self.emit_progress(worker, i) # 发送进度信号,参数为项的索引i + if worker and worker.canceled: # 若导出任务被取消 + return # 若导出任务被取消,直接返回 - return svg + return svg # 返回SVG根元素 + #导出场景到SVG文件 def export(self, filename, worker=None): - logger.debug(f'Exporting scene to {filename}') - self.emit_begin_processing(worker, len(self.scene.items())) - svg = self.render_to_svg(worker) + logger.debug(f'Exporting scene to {filename}') # 调试日志,输出导出场景到SVG文件的文件名 + self.emit_begin_processing(worker, len(self.scene.items())) # 发送开始处理信号,设置总进度为项的数量 + svg = self.render_to_svg(worker) # 渲染场景到SVG元素,参数为导出任务的工作线程 - if worker and worker.canceled: - logger.debug('Export canceled') - worker.finished.emit(filename, []) - return + if worker and worker.canceled: # 若导出任务被取消 + logger.debug('Export canceled') # 调试日志,输出导出任务被取消 + worker.finished.emit(filename, []) # 发送导出任务完成信号,参数为文件名和空列表 + return # 若导出任务被取消,直接返回 - tree = ET.ElementTree(svg) - ET.indent(tree, space=' ') + tree = ET.ElementTree(svg) # 创建XML元素树,参数为SVG根元素 + ET.indent(tree, space=' ') # 缩进XML元素树,参数为空格字符串 try: - with open(filename, 'w') as f: - tree.write(f, encoding='unicode', xml_declaration=True) - except OSError as e: - self.handle_export_error(filename, e, worker) - return + with open(filename, 'w') as f: # 以UTF-8编码打开文件,参数为文件名和写入模式 + tree.write(f, encoding='unicode', xml_declaration=True) # 将XML元素树写入文件,参数为文件对象,编码为unicode,XML声明为True + except OSError as e: # 若导出任务发生OS错误 + self.handle_export_error(filename, e, worker) # 处理导出任务错误,参数为文件名、错误对象和导出任务的工作线程 + return # 若导出任务发生OS错误,直接返回 - logger.debug('Export finished') - self.emit_finished(worker, filename, []) + logger.debug('Export finished') # 调试日志,输出导出任务完成 + self.emit_finished(worker, filename, []) # 发送导出任务完成信号,参数为导出任务的工作线程、文件名和空列表 +# 图像到目录导出器 class ImagesToDirectoryExporter(ExporterBase): """Export all images to a folder. @@ -283,75 +321,82 @@ class ImagesToDirectoryExporter(ExporterBase): not auto-detected by file extension. """ + #初始化图像到目录导出器 def __init__(self, scene, dirname): - self.scene = scene - self.dirname = dirname - self.items = list(self.scene.items_by_type(BeePixmapItem.TYPE)) - self.max_save_id = 0 - for item in self.items: - if item.save_id: - self.max_save_id = max(self.max_save_id, item.save_id) - self.num_total = len(self.items) - self.start_from = 0 - self.handle_existing = None + self.scene = scene # 场景对象 + self.dirname = dirname # 目录名 + self.items = list(self.scene.items_by_type(BeePixmapItem.TYPE)) # 获取所有像素图项的列表 + self.max_save_id = 0 # 最大保存ID + #查找当前最大保存ID + for item in self.items: # 遍历所有像素图项 + if item.save_id: # 若项有保存ID + self.max_save_id = max(self.max_save_id, item.save_id) # 更新最大保存ID为项的保存ID和当前最大保存ID中的较大值 + self.num_total = len(self.items) # 总项数 + self.start_from = 0 # 起始索引 + self.handle_existing = None # 处理已存在文件的方式,初始值为None + #导出场景中的图像到目录 def export(self, worker=None): - logger.debug(f'Exporting images to {self.dirname}') - logger.debug(f'Starting at {self.start_from}') - - self.emit_begin_processing(worker, self.num_total) - self.emit_progress(worker, self.start_from) + logger.debug(f'Exporting images to {self.dirname}') # 调试日志,输出导出图像到目录的目录名 + logger.debug(f'Starting at {self.start_from}') # 调试日志,输出导出任务的起始索引 + self.emit_begin_processing(worker, self.num_total) # 发送开始处理信号,参数为项的数量 + self.emit_progress(worker, self.start_from) # 发送当前进度信号,参数为起始索引 + + #遍历项目 for i, item in enumerate( - self.items[self.start_from:], start=self.start_from): - if worker and worker.canceled: - logger.debug('Export canceled') - worker.finished.emit(self.dirname, []) - return + self.items[self.start_from:], start=self.start_from): # 遍历从起始索引开始的项 + self.emit_progress(worker, i) # 发送当前进度信号,参数为项的索引i + if worker and worker.canceled: # 若导出任务被取消 + logger.debug('Export canceled') # 调试日志,输出导出任务被取消 + worker.finished.emit(self.dirname, []) # 发送导出任务完成信号,参数为目录名和空列表 + return # 若导出任务被取消,直接返回 - pixmap, imgformat = item.pixmap_to_bytes() + pixmap, imgformat = item.pixmap_to_bytes() # 获取项的像素图和图像格式 - if item.save_id: - filename = item.get_filename_for_export(imgformat) - else: - self.max_save_id += 1 - save_id = self.max_save_id - filename = item.get_filename_for_export(imgformat, save_id) + #确定文件名 + if item.save_id: # 若项有保存ID + filename = item.get_filename_for_export(imgformat) # 获取项的导出文件名,参数为图像格式 + else: # 若项没有保存ID + self.max_save_id += 1 # 最大保存ID加1 + save_id = self.max_save_id # 保存ID为最大保存ID + filename = item.get_filename_for_export(imgformat, save_id) # 获取项的导出文件名,参数为图像格式和保存ID try: - path = pathlib.Path(self.dirname) / filename - path_exists = path.exists() - except OSError as e: - self.handle_export_error(self.dirname, e, worker) - return + path = pathlib.Path(self.dirname) / filename # 构建文件路径,参数为目录名和文件名 + path_exists = path.exists() # 检查文件路径是否存在 + except OSError as e: # 若导出任务发生OS错误 + self.handle_export_error(self.dirname, e, worker) # 处理导出任务错误,参数为目录名、错误对象和导出任务的工作线程 + return # 直接返回 - if path_exists: - logger.debug(f'File already exists: {path}') - if self.handle_existing is None: - self.start_from = i - self.emit_user_input_required(worker, str(path)) - return - else: - if self.handle_existing == 'skip': - self.handle_existing = None - logger.debug('Skipping file') - continue - elif self.handle_existing == 'skip_all': - logger.debug('Skipping file') - continue - elif self.handle_existing == 'overwrite': - self.handle_existing = None - logger.debug('Overwrite file') - elif self.handle_existing == 'overwrite_all': - logger.debug('Overwrite file') + #处理文件已存在的情况 + if path_exists: # 若文件路径已存在 + logger.debug(f'File already exists: {path}') # 调试日志,输出文件已存在的路径 + if self.handle_existing is None: # 若处理已存在文件的方式为None + self.start_from = i # 更新起始索引为当前项的索引,记录当前位置 + self.emit_user_input_required(worker, str(path)) # 发送用户输入要求信号,请求用户输入 + return # 若导出任务发生OS错误,直接返回 + else: # 若处理已存在文件的方式不为None + if self.handle_existing == 'skip': # 若处理已存在文件的方式为跳过 + self.handle_existing = None # 处理已存在文件的方式设为None,记录当前位置 + logger.debug('Skipping file') # 调试日志,输出跳过文件 + continue # 若处理已存在文件的方式为跳过,继续下一项 + elif self.handle_existing == 'skip_all': # 若处理已存在文件的方式为跳过所有 + logger.debug('Skipping file') # 调试日志,输出跳过文件 + continue # 若处理已存在文件的方式为跳过所有,继续下一项 + elif self.handle_existing == 'overwrite': # 若处理已存在文件的方式为覆盖 + self.handle_existing = None # 处理已存在文件的方式设为None,记录当前位置 + logger.debug('Overwrite file') # 调试日志,输出覆盖文件 + elif self.handle_existing == 'overwrite_all':# 若处理已存在文件的方式为覆盖所有 + logger.debug('Overwrite file') # 调试日志,输出覆盖文件 - logger.debug(f'Writing file: {path}') + logger.debug(f'Writing file: {path}') # 调试日志,输出写入文件的路径 try: - path.write_bytes(pixmap) - except OSError as e: - self.handle_export_error(path, e, worker) - return + path.write_bytes(pixmap) # 写入像素图到文件路径,参数为像素图 + except OSError as e: # 若写入像素图到文件路径发生OS错误 + self.handle_export_error(path, e, worker) # 处理导出任务错误,参数为文件路径、错误对象和导出任务的工作线程 + return # 若导出任务发生OS错误,直接返回 - self.emit_progress(worker, i) + self.emit_progress(worker, i) # 发送进度信号,参数为当前项的索引,记录当前位置 - self.emit_finished(worker, self.dirname, []) + self.emit_finished(worker, self.dirname, []) # 发送导出任务完成信号,参数为目录名和空列表 diff --git a/beeref/widgets/settings.py b/beeref/widgets/settings.py index 26bdcf2..679dd98 100644 --- a/beeref/widgets/settings.py +++ b/beeref/widgets/settings.py @@ -13,10 +13,10 @@ # You should have received a copy of the GNU General Public License # along with BeeRef. If not, see . -from functools import partial +from functools import partial import logging -from PyQt6 import QtWidgets +from PyQt6 import QtWidgets from PyQt6.QtCore import Qt from beeref import constants