From b9ac86c69c78104865c5f4b9da22ed7b05702e27 Mon Sep 17 00:00:00 2001
From: zhaoyulin1117 <3166922571@qq.com>
Date: Wed, 15 Oct 2025 23:04:50 +0800
Subject: [PATCH 1/8] Update schema.py
---
beeref/fileio/schema.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/beeref/fileio/schema.py b/beeref/fileio/schema.py
index 6ca7f1b..eca1433 100644
--- a/beeref/fileio/schema.py
+++ b/beeref/fileio/schema.py
@@ -1,4 +1,4 @@
-USER_VERSION = 2
+USER_VERSION = 2 # 数据库版本号,用于控制数据库迁移(当结构变更时递增版本号)
APPLICATION_ID = 2060242126
From dd177204ae6390acd200c1c6f7a0d04242466060 Mon Sep 17 00:00:00 2001
From: zhaoyulin1117 <3166922571@qq.com>
Date: Wed, 15 Oct 2025 23:08:40 +0800
Subject: [PATCH 2/8] Update schema.py
---
beeref/fileio/schema.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/beeref/fileio/schema.py b/beeref/fileio/schema.py
index eca1433..8595ab0 100644
--- a/beeref/fileio/schema.py
+++ b/beeref/fileio/schema.py
@@ -1,5 +1,5 @@
USER_VERSION = 2 # 数据库版本号,用于控制数据库迁移(当结构变更时递增版本号)
-APPLICATION_ID = 2060242126
+APPLICATION_ID = 2060242126 # 应用标识ID,通常用于区分不同应用的数据库(避免冲突)
SCHEMA = [
From 6712da907d544829f39cf01a495e192260dee517 Mon Sep 17 00:00:00 2001
From: zhaoyulin1117 <3166922571@qq.com>
Date: Thu, 16 Oct 2025 16:19:03 +0800
Subject: [PATCH 3/8] Update schema.py
---
beeref/fileio/schema.py | 51 +++++++++++++++++++++++------------------
1 file changed, 29 insertions(+), 22 deletions(-)
diff --git a/beeref/fileio/schema.py b/beeref/fileio/schema.py
index 8595ab0..99c001e 100644
--- a/beeref/fileio/schema.py
+++ b/beeref/fileio/schema.py
@@ -1,41 +1,48 @@
-USER_VERSION = 2 # 数据库版本号,用于控制数据库迁移(当结构变更时递增版本号)
-APPLICATION_ID = 2060242126 # 应用标识ID,通常用于区分不同应用的数据库(避免冲突)
+# 数据库版本号,用于跟踪数据库结构的更改
+USER_VERSION = 2
+# SQLite数据库的应用程序ID,用于标识特定应用程序的数据库文件
+APPLICATION_ID = 2060242126
+# 数据库架构定义,包含创建表的SQL语句列表
SCHEMA = [
+ # 创建items表,存储应用中的所有项目信息
"""
CREATE TABLE items (
- id INTEGER PRIMARY KEY,
- type TEXT NOT NULL,
- x REAL DEFAULT 0,
- y REAL DEFAULT 0,
- z REAL DEFAULT 0,
- scale REAL DEFAULT 1,
- rotation REAL DEFAULT 0,
- flip INTEGER DEFAULT 1,
- data JSON
+ id INTEGER PRIMARY KEY, # 主键,唯一标识每个项目
+ type TEXT NOT NULL, # 项目类型,如图片、文本等
+ x REAL DEFAULT 0, # X坐标位置
+ y REAL DEFAULT 0, # Y坐标位置
+ z REAL DEFAULT 0, # Z轴位置,用于确定项目的显示顺序
+ scale REAL DEFAULT 1, # 缩放比例
+ rotation REAL DEFAULT 0, # 旋转角度(以度为单位)
+ flip INTEGER DEFAULT 1, # 翻转状态,用于水平或垂直翻转项目
+ data JSON # JSON格式的数据,存储项目的其他属性
)
""",
+ # 创建sqlar表,用于存储与项目关联的二进制数据
"""
CREATE TABLE sqlar (
- name TEXT PRIMARY KEY,
- item_id INTEGER NOT NULL UNIQUE,
- mode INT,
- mtime INT default current_timestamp,
- sz INT,
- data BLOB,
- FOREIGN KEY (item_id)
+ name TEXT PRIMARY KEY, # 文件名作为主键
+ item_id INTEGER NOT NULL UNIQUE, # 关联的items表中的项目ID
+ mode INT, # 文件模式
+ mtime INT default current_timestamp, # 修改时间戳
+ sz INT, # 文件大小
+ data BLOB, # 二进制数据内容
+ FOREIGN KEY (item_id) # 外键约束,关联items表
REFERENCES items (id)
- ON DELETE CASCADE
- ON UPDATE NO ACTION
+ ON DELETE CASCADE # 级联删除,当items表中的记录被删除时,相关的sqlar记录也会被删除
+ ON UPDATE NO ACTION # 禁止更新关联字段
)
""",
]
+# 数据库迁移定义,键为目标版本号,值为实现该版本升级所需的SQL语句列表
MIGRATIONS = {
+ # 从版本1迁移到版本2的SQL语句
2: [
- "ALTER TABLE items ADD COLUMN data JSON",
- "UPDATE items SET data = json_object('filename', filename)",
+ "ALTER TABLE items ADD COLUMN data JSON", # 添加JSON类型的data列
+ "UPDATE items SET data = json_object('filename', filename)", # 将原有的filename数据迁移到新的JSON格式中
],
}
From 9ca67215b116a1e647d44ce4df31ca16fcf9de93 Mon Sep 17 00:00:00 2001
From: zhaoyulin1117 <3166922571@qq.com>
Date: Thu, 16 Oct 2025 17:07:16 +0800
Subject: [PATCH 4/8] Update errors.py
---
beeref/fileio/errors.py | 31 ++++++++++++++++++-------------
1 file changed, 18 insertions(+), 13 deletions(-)
diff --git a/beeref/fileio/errors.py b/beeref/fileio/errors.py
index 55e7dff..2437d23 100644
--- a/beeref/fileio/errors.py
+++ b/beeref/fileio/errors.py
@@ -1,25 +1,30 @@
-# This file is part of BeeRef.
+# This file is part of BeeRef. # 此文件是 BeeRef 项目的一部分
+#
+# BeeRef is free software: you can redistribute it and/or modify # BeeRef 是自由软件:您可以重新分发和/或修改它
+# it under the terms of the GNU General Public License as published by # 根据 GNU 通用公共许可证的条款,由 Free Software Foundation 发布,
+# the Free Software Foundation, either version 3 of the License, or # 无论是版本 3 的许可证,还是(在您选择的情况下)任何更高版本。
#
-# BeeRef is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
-# BeeRef is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# BeeRef is distributed in the hope that it will be useful, # BeeRef 是分发在希望它会有用的基础上
+# but WITHOUT ANY WARRANTY; without even the implied warranty of # 但没有任何保修;甚至没有对适销性或特定用途适用性的暗示保证。
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # 有关更多详细信息,请参阅 GNU 通用公共许可证
# GNU General Public License for more details.
#
-# You should have received a copy of the GNU General Public License
-# along with BeeRef. If not, see .
+# You should have received a copy of the GNU General Public License # 您应该已经收到了 GNU 通用公共许可证的副本
+# along with BeeRef. If not, see . # 如果没有,请参阅
+# 图片加载错误的提示消息,当图片格式未知或过大时显示
IMG_LOADING_ERROR_MSG = (
- 'Unknown format or too big?\n'
- 'Check Settings -> Images & Items -> Maximum Image Size')
+ 'Unknown format or too big?\n' # 未知格式或文件太大?
+ 'Check Settings -> Images & Items -> Maximum Image Size') # 请检查设置 -> 图像和项目 -> 最大图像大小
+# 自定义的文件IO错误类,继承自Python内置的Exception类
class BeeFileIOError(Exception):
+ # 构造函数,初始化错误消息和文件名
def __init__(self, msg, filename):
- self.msg = msg
- self.filename = filename
+ self.msg = msg # 保存错误消息
+ self.filename = filename # 保存发生错误的文件名
+
From 53ffc3fe91ff49b329970d2b40a7859ea65079ca Mon Sep 17 00:00:00 2001
From: zhaoyulin1117 <3166922571@qq.com>
Date: Fri, 17 Oct 2025 15:11:07 +0800
Subject: [PATCH 5/8] Update export.py
---
beeref/fileio/export.py | 501 ++++++++++++++++++++-----------------
beeref/widgets/settings.py | 4 +-
2 files changed, 275 insertions(+), 230 deletions(-)
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
From 244118d20f5764c8dc4362fd188c11755cf9413f Mon Sep 17 00:00:00 2001
From: zhaoyulin1117 <3166922571@qq.com>
Date: Sun, 19 Oct 2025 14:46:30 +0800
Subject: [PATCH 6/8] update _init_.py
---
beeref/fileio/__init__.py | 142 ++++++++++++++++++++++++--------------
1 file changed, 90 insertions(+), 52 deletions(-)
diff --git a/beeref/fileio/__init__.py b/beeref/fileio/__init__.py
index 5834d97..a43ae9c 100644
--- a/beeref/fileio/__init__.py
+++ b/beeref/fileio/__init__.py
@@ -13,92 +13,130 @@
# You should have received a copy of the GNU General Public License
# along with BeeRef. If not, see .
-import logging
+# 导入必要的模块
+import logging # 用于日志记录
-from PyQt6 import QtCore
+from PyQt6 import QtCore # 导入PyQt6的QtCore模块,用于处理Qt的核心功能
-from beeref import commands
-from beeref.fileio.errors import BeeFileIOError
-from beeref.fileio.image import load_image
-from beeref.fileio.sql import SQLiteIO, is_bee_file
-from beeref.items import BeePixmapItem
+from beeref import commands # 导入commands命令模块,用于实现撤销和重做功能
+from beeref.fileio.errors import BeeFileIOError # 导入自定义的文件IO错误类
+from beeref.fileio.image import load_image # 导入加载图片的函数
+from beeref.fileio.sql import SQLiteIO, is_bee_file # 导入SQLite数据库相关的类和函数
+from beeref.items import BeePixmapItem # 导入自定义的图片项类
+# 定义公共API,指定从该模块导入时可访问的名称
__all__ = [
- 'is_bee_file',
- 'load_bee',
- 'save_bee',
- 'load_images',
- 'ThreadedLoader',
- 'BeeFileIOError',
+ 'is_bee_file', # 判断文件是否为BeeRef格式的函数
+ 'load_bee', # 加载BeeRef原生文件的函数
+ 'save_bee', # 保存BeeRef原生文件的函数
+ 'load_images', # 加载图像函数
+ 'ThreadedLoader', # 线程加载器类
+ 'BeeFileIOError', # 自定义的文件IO错误类
]
+# 创建日志记录器
logger = logging.getLogger(__name__)
+# 加载BeeRef原生文件函数
def load_bee(filename, scene, worker=None):
"""Load BeeRef native file."""
- logger.info(f'Loading from file {filename}...')
- io = SQLiteIO(filename, scene, readonly=True, worker=worker)
- return io.read()
+ """
+ 参数:
+ filename: 要加载的文件路径
+ scene: 目标场景对象
+ worker: 可选的工作线程对象,用于报告进度
+ """
+ logger.info(f'Loading from file {filename}...') # 记录加载文件的信息日志
+ io = SQLiteIO(filename, scene, readonly=True, worker=worker) # 创建SQLiteIO对象,用于读取文件,设置为只读模式
+ return io.read() # 调用read方法读取文件内容并返回
+# 保存BeeRef原生文件函数
def save_bee(filename, scene, create_new=False, worker=None):
"""Save BeeRef native file."""
- logger.info(f'Saving to file {filename}...')
- logger.debug(f'Create new: {create_new}')
- io = SQLiteIO(filename, scene, create_new, worker=worker)
- io.write()
- logger.info('End save')
+ """
+ 参数:
+ filename: 要保存的文件路径
+ scene: 要保存的场景对象
+ create_new: 是否创建新文件
+ worker: 可选的工作线程对象,用于报告进度
+ """
+ logger.info(f'Saving to file {filename}...') # 记录保存文件的信息日志
+ logger.debug(f'Create new: {create_new}') # 记录创建新文件的调试日志
+ io = SQLiteIO(filename, scene, create_new, worker=worker) # 创建SQLiteIO对象,用于写入文件,根据create_new参数确定是否创建新文件
+ io.write() # 调用write方法将场景数据写入文件
+ logger.info('End save') # 记录保存结束的信息日志
+
+# 加载图像到现有场景函数
def load_images(filenames, pos, scene, worker):
"""Add images to existing scene."""
-
- errors = []
- items = []
- worker.begin_processing.emit(len(filenames))
+ """
+ 参数:
+ filenames: 图像文件路径列表
+ pos: 图像在场景中的位置
+ scene: 目标场景对象
+ worker: 工作线程对象,用于报告进度和处理取消操作
+ """
+ errors = [] # 存储加载失败的图像文件路径
+ items = [] # 存储成功加载的图片项对象
+ worker.begin_processing.emit(len(filenames)) # 发送开始处理信号,参数为图像文件数量
+ # 遍历图像文件路径列表
for i, filename in enumerate(filenames):
- logger.info(f'Loading image from file {filename}')
- img, filename = load_image(filename)
- worker.progress.emit(i)
- if img.isNull():
- logger.info(f'Could not load file {filename}')
- errors.append(filename)
- continue
+ logger.info(f'Loading image from file {filename}') # 记录加载图像文件的信息日志
+ img, filename = load_image(filename) # 调用load_image函数加载图像文件,返回图像对象和文件名
+ worker.progress.emit(i) # 发送进度更新信号
+ if img.isNull(): # 检查图像是否加载失败
+ logger.info(f'Could not load file {filename}') # 记录加载失败的图像文件路径的信息日志
+ errors.append(filename) # 将加载失败的图像文件路径添加到errors列表中
+ continue # 继续处理下一个图像文件
- item = BeePixmapItem(img, filename)
- item.set_pos_center(pos)
- scene.add_item_later({'item': item, 'type': 'pixmap'}, selected=True)
- items.append(item)
+ item = BeePixmapItem(img, filename) # 创建BeePixmapItem对象,参数为加载的图像对象和文件名
+ item.set_pos_center(pos) # 设置图片项的位置为指定的中心位置
+ scene.add_item_later({'item': item, 'type': 'pixmap'}, selected=True) # 将图片项添加到场景中,参数为字典,包含项对象和类型
+ items.append(item) # 将图片项对象添加到items列表中
if worker.canceled:
- break
+ break # 如果工作线程被取消,跳出循环
# Give main thread time to process items:
- worker.msleep(10)
+ worker.msleep(10) # 线程休眠10毫秒,允许主线程处理其他任务
+ # 将添加项的操作添加到撤销栈
scene.undo_stack.push(
commands.InsertItems(scene, items, ignore_first_redo=True))
- worker.finished.emit('', errors)
+ worker.finished.emit('', errors) # 发送完成信号,传递错误列表
+# 用于加载和保存的专用线程类
class ThreadedIO(QtCore.QThread):
"""Dedicated thread for loading and saving."""
- progress = QtCore.pyqtSignal(int)
- finished = QtCore.pyqtSignal(str, list)
- begin_processing = QtCore.pyqtSignal(int)
- user_input_required = QtCore.pyqtSignal(str)
+ # 定义信号
+ progress = QtCore.pyqtSignal(int) # 进度更新信号
+ finished = QtCore.pyqtSignal(str, list) # 完成信号,传递文件名和和错误列表
+ begin_processing = QtCore.pyqtSignal(int) # 开始处理信号,传递总处理数量
+ user_input_required = QtCore.pyqtSignal(str) # 用户输入required信号,参数为提示信息
+ # 初始化线程,设置要执行的函数和参数
def __init__(self, func, *args, **kwargs):
- super().__init__()
- self.func = func
- self.args = args
- self.kwargs = kwargs
- self.kwargs['worker'] = self
- self.canceled = False
-
+ """
+ 参数:
+ func: 要在线程中执行的函数
+ *args: 传递给func的位置参数
+ **kwargs: 传递给func的关键字参数
+ """
+ super().__init__() # 调用父类QThread构造函数
+ self.func = func # 存储要执行的函数
+ self.args = args # 存储要传递给函数的位置参数
+ self.kwargs = kwargs # 存储要传递给函数的关键字参数
+ self.kwargs['worker'] = self # 将自身作为worker参数传递给函数
+ self.canceled = False # 取消标志,初始化为False
+ # 线程执行函数
def run(self):
- self.func(*self.args, **self.kwargs)
+ self.func(*self.args, **self.kwargs) # 调用指定的函数执行IO操作
+ # 处理取消操作的方法
def on_canceled(self):
- self.canceled = True
+ self.canceled = True # 设置取消标志为True
From 31d664f8e1ab02633ca53f325e2ebd9ae0250ae9 Mon Sep 17 00:00:00 2001
From: zhaoyulin1117 <3166922571@qq.com>
Date: Sun, 19 Oct 2025 16:10:14 +0800
Subject: [PATCH 7/8] update image.py
---
beeref/fileio/image.py | 125 +++++++++++++++++++++++------------------
1 file changed, 71 insertions(+), 54 deletions(-)
diff --git a/beeref/fileio/image.py b/beeref/fileio/image.py
index 9e6449c..7b8fec9 100644
--- a/beeref/fileio/image.py
+++ b/beeref/fileio/image.py
@@ -13,101 +13,118 @@
# You should have received a copy of the GNU General Public License
# along with BeeRef. If not, see .
-import logging
-import os.path
-import tempfile
-from urllib.error import URLError
-from urllib import parse, request
+# 导入必要的模块
+import logging # 导入日志模块,用于记录运行时信息和错误
+import os.path # 导入os.path模块,用于处理文件路径
+import tempfile # 导入tempfile模块,用于创建临时文件
+from urllib.error import URLError # 导入URLError类,用于处理URL错误
+from urllib import parse, request # 导入urllib模块中的parse和request子模块,用于处理URL解析和请求
-from PyQt6 import QtGui
-
-import exif
-from lxml import etree
-import plum
+from PyQt6 import QtGui # 导入QtGui模块,用于处理图像和绘图
+import exif # 导入exif模块,用于处理图像的EXIF数据
+from lxml import etree # 导入lxml模块中的etree子模块,用于解析HTML
+import plum # 导入plum模块,用于处理图像的二进制数据
+# 创建日志记录器实例
logger = logging.getLogger(__name__)
+# 定义函数exif_rotated_image,用于根据图像的EXIF数据进行旋转和镜像变换
def exif_rotated_image(path=None):
"""Returns a QImage that is transformed according to the source's
orientation EXIF data.
"""
+ # 创建QImage对象
img = QtGui.QImage(path)
+ # 检查图像是否为空(无法加载)
if img.isNull():
- return img
+ return img # 如果图像为空,直接返回
+ # 尝试读取EXIF数据
with open(path, 'rb') as f:
try:
- exifimg = exif.Image(f)
- except (plum.exceptions.UnpackError, NotImplementedError):
- logger.exception(f'Exif parser failed on image: {path}')
- return img
+ exifimg = exif.Image(f) # 从文件对象创建EXIF图像对象
+ except (plum.exceptions.UnpackError, NotImplementedError): # 处理EXIF解析错误
+ logger.exception(f'Exif parser failed on image: {path}') # 记录EXIF解析错误日志
+ return img # 如果EXIF解析错误,直接返回原始图像
try:
- if 'orientation' in exifimg.list_all():
- orientation = exifimg.orientation
+ if 'orientation' in exifimg.list_all(): # 检查是否存在方向EXIF信息
+ orientation = exifimg.orientation # 获取方向EXIF信息
else:
- return img
- except (NotImplementedError, ValueError):
- logger.exception(f'Exif failed reading orientation of image: {path}')
- return img
+ return img # 如果不存在方向EXIF信息,直接返回原始图像
+ except (NotImplementedError, ValueError): # 处理其他EXIF读取错误
+ logger.exception(f'Exif failed reading orientation of image: {path}') # 记录EXIF读取错误日志
+ return img # 如果EXIF读取错误,直接返回原始图像
+ # 创建变换对象
transform = QtGui.QTransform()
- if orientation == exif.Orientation.TOP_RIGHT:
+ # 根据EXIF方向值进行相应的变换
+ if orientation == exif.Orientation.TOP_RIGHT: # 方向值2:方向为顶部右侧,需要水平镜像
return img.mirrored(horizontal=True, vertical=False)
- if orientation == exif.Orientation.BOTTOM_RIGHT:
+ if orientation == exif.Orientation.BOTTOM_RIGHT: # 方向值3:方向为底部右侧,需要旋转180度
transform.rotate(180)
return img.transformed(transform)
- if orientation == exif.Orientation.BOTTOM_LEFT:
+ if orientation == exif.Orientation.BOTTOM_LEFT: # 方向值4:方向为底部左侧,需要垂直镜像
return img.mirrored(horizontal=False, vertical=True)
- if orientation == exif.Orientation.LEFT_TOP:
+ if orientation == exif.Orientation.LEFT_TOP: # 方向值5:方向为左侧顶部,需要顺时针旋转90度
transform.rotate(90)
return img.transformed(transform).mirrored(
horizontal=True, vertical=False)
- if orientation == exif.Orientation.RIGHT_TOP:
+ if orientation == exif.Orientation.RIGHT_TOP: # 方向值6:方向为右侧顶部,需要逆时针旋转90度
transform.rotate(90)
return img.transformed(transform)
- if orientation == exif.Orientation.RIGHT_BOTTOM:
+ if orientation == exif.Orientation.RIGHT_BOTTOM: # 方向值7:方向为右侧底部,需要顺时针旋转270度
transform.rotate(270)
return img.transformed(transform).mirrored(
horizontal=True, vertical=False)
- if orientation == exif.Orientation.LEFT_BOTTOM:
+ if orientation == exif.Orientation.LEFT_BOTTOM: # 方向值8:方向为左侧底部,需要逆时针旋转270度
transform.rotate(270)
return img.transformed(transform)
- return img
+ return img # 方向值1或其他未知值,返回原始图像
+# 加载图像的主要函数
def load_image(path):
- if isinstance(path, str):
- path = os.path.normpath(path)
- return (exif_rotated_image(path), path)
- if path.isLocalFile():
- path = os.path.normpath(path.toLocalFile())
- return (exif_rotated_image(path), path)
+
+ # 处理本地文件路径(字符串形式)
+ if isinstance(path, str): # 检查路径是否为字符串类型
+ path = os.path.normpath(path) # 规范路径格式,处理路径中的特殊字符和点号
+ return (exif_rotated_image(path), path) # 加载图像并处理EXIF方向
+
+ # 处理QUrl对象指向的本地文件
+ if path.isLocalFile(): # 检查路径是否为本地文件路径
+ path = os.path.normpath(path.toLocalFile()) # 转化为本地路径并规范本地文件路径格式
+ return (exif_rotated_image(path), path) # 加载图像并处理EXIF方向
- url = bytes(path.toEncoded()).decode()
- domain = '.'.join(parse.urlparse(url).netloc.split(".")[-2:])
- img = exif_rotated_image()
+ # 处理远程URL
+ url = bytes(path.toEncoded()).decode() # 将QUrl对象转化为URL字符串并解码
+ domain = '.'.join(parse.urlparse(url).netloc.split(".")[-2:]) # 从URL中提取域名(例如:'pinterest.com')
+ img = exif_rotated_image() # 创建一个空的QImage对象
+
+ # 对Pinterest特定处理
if domain == 'pinterest.com':
- try:
- page_data = request.urlopen(url).read()
- root = etree.HTML(page_data)
- url = root.xpath("//img")[0].get('src')
- except Exception as e:
- logger.debug(f'Pinterest image download failed: {e}')
+ try: # 尝试下载Pinterest页面HTML内容
+ page_data = request.urlopen(url).read() # 下载Pinterest页面HTML内容
+ root = etree.HTML(page_data) # 解析HTML内容为ElementTree对象
+ url = root.xpath("//img")[0].get('src') # 从HTML中提取第一个img标签的src属性值(图像URL)
+ except Exception as e: # 处理下载Pinterest页面HTML内容时的异常
+ logger.debug(f'Pinterest image download failed: {e}') # 如果Pinterest图片下载失败,记录Pinterest图像下载失败日志
+
+ # 尝试下载图像数据
try:
- imgdata = request.urlopen(url).read()
- except URLError as e:
- logger.debug(f'Downloading image failed: {e.reason}')
+ imgdata = request.urlopen(url).read() # 下载图像数据
+ except URLError as e: # 处理下载图像数据时的异常
+ logger.debug(f'Downloading image failed: {e.reason}') # 如果图像下载失败,记录图像下载失败日志
else:
- with tempfile.TemporaryDirectory() as tmp:
- fname = os.path.join(tmp, 'img')
- with open(fname, 'wb') as f:
- f.write(imgdata)
- logger.debug(f'Temporarily saved in: {fname}')
- img = exif_rotated_image(fname)
- return (img, url)
+ with tempfile.TemporaryDirectory() as tmp: # 创建一个临时文件用于存储下载成功的图像数据
+ fname = os.path.join(tmp, 'img') # 构建临时文件路径(例如:'/tmp/img')
+ with open(fname, 'wb') as f: # 以二进制写入模式打开临时文件
+ f.write(imgdata) # 将图像数据写入临时文件
+ logger.debug(f'Temporarily saved in: {fname}') # 记录临时保存的图像路径
+ img = exif_rotated_image(fname) # 加载临时图像文件并处理EXIF方向
+ return (img, url) # 返回处理后的图像对象和原始URL
From 8b4f36bdbc12d45cfc44e7c63fd8c81963c8dcc5 Mon Sep 17 00:00:00 2001
From: zhaoyulin1117 <3166922571@qq.com>
Date: Tue, 28 Oct 2025 10:50:33 +0800
Subject: [PATCH 8/8] update sql.py
---
beeref/fileio/sql.py | 404 ++++++++++++++++++++++++-------------------
1 file changed, 225 insertions(+), 179 deletions(-)
diff --git a/beeref/fileio/sql.py b/beeref/fileio/sql.py
index 9e2a2b5..59b3381 100644
--- a/beeref/fileio/sql.py
+++ b/beeref/fileio/sql.py
@@ -23,308 +23,354 @@ https://www.sqlite.org/appfileformat.html
https://www.sqlite.org/sqlar.html
"""
-import json
-import logging
-import os
-import pathlib
-import shutil
-import sqlite3
-import tempfile
+# 导入必要的模块
+import json # 导入 JSON 模块,用于处理 JSON 数据
+import logging # 导入日志模块,用于记录程序运行时的信息
+import os # 导入 os 模块,用于处理文件路径
+import pathlib # 导入 pathlib 模块,用于处理文件路径
+import shutil # 导入 shutil 模块,用于文件操作
+import sqlite3 # 导入 sqlite3 模块,用于操作 SQLite 数据库
+import tempfile # 导入 tempfile 模块,用于创建临时文件
-from PyQt6 import QtGui
-
-from beeref import constants
-from beeref.items import BeePixmapItem, BeeErrorItem
-from .errors import BeeFileIOError, IMG_LOADING_ERROR_MSG
-from .schema import SCHEMA, USER_VERSION, MIGRATIONS, APPLICATION_ID
+from PyQt6 import QtGui # 导入 PyQt6 模块,用于 GUI 编程
+from beeref import constants # 导入 BeeRef 模块,用于定义常量
+from beeref.items import BeePixmapItem, BeeErrorItem # 导入 BeeRef 模块,用于定义项
+from .errors import BeeFileIOError, IMG_LOADING_ERROR_MSG # 导入 BeeRef 模块,用于定义错误
+from .schema import SCHEMA, USER_VERSION, MIGRATIONS, APPLICATION_ID # 导入 BeeRef 模块,用于定义数据库模式
+# 配置日志记录器
logger = logging.getLogger(__name__)
+# 检查文件是否为bee文件
def is_bee_file(path):
"""Check whether the file at the given path is a bee file."""
-
- return os.path.splitext(path)[1] == '.bee'
+
+ return os.path.splitext(path)[1] == '.bee' # 使用os.path.splitext分割文件名和扩展名
+# 处理SQLite错误
def handle_sqlite_errors(func):
def wrapper(self, *args, **kwargs):
try:
- func(self, *args, **kwargs)
- except Exception as e:
- logger.exception(f'Error while reading/writing {self.filename}')
- try:
+ # 尝试执行原始函数
+ func(self, *args, **kwargs) # 执行函数
+ except Exception as e: # 捕获所有异常
+ logger.exception(f'Error while reading/writing {self.filename}')# 记录异常日志,包含文件名信息
+ try: # 尝试回滚事务
# Try to roll back transaction if there is any
- if (hasattr(self, '_connection')
- and self._connection.in_transaction):
- self.ex('ROLLBACK')
- logger.debug('Transaction rolled back')
- except sqlite3.Error:
- pass
- self._close_connection()
- if self.worker:
- self.worker.finished.emit(self.filename, [str(e)])
- else:
- raise BeeFileIOError(msg=str(e), filename=self.filename) from e
-
- return wrapper
+ if (hasattr(self, '_connection') # 检查是否有数据库连接属性
+ and self._connection.in_transaction): # 检查是否有未提交的事务
+ self.ex('ROLLBACK') # 执行SQL回滚命令
+ logger.debug('Transaction rolled back') # 记录回滚信息
+ except sqlite3.Error as e: # 捕获SQLite错误
+ pass # 如果回滚失败,忽略SQLite错误
+ self._close_connection() # 关闭数据库连接
+ if self.worker: # 如果有工作线程,则向其发送完成信号并附带错误信息
+ self.worker.finished.emit(self.filename, [str(e)]) # 发射工作线程完成信号
+ else: # 如果没有工作线程
+ raise BeeFileIOError(msg=str(e), filename=self.filename) from e# 抛出 BeeFileIOError 异常
+ return wrapper # 返回包装后的函数
+# 定义SQLiteIO类,用于操作SQLite数据库文件
class SQLiteIO:
+ # 初始化SQLiteIO类
def __init__(self, filename, scene, create_new=False, readonly=False,
worker=None):
- self.scene = scene
- self.create_new = create_new
- self.filename = filename
- self.readonly = readonly
- self.worker = worker
- self.retry = False
+ self.scene = scene # 保存场景引用,用于添加或获取项目
+ self.create_new = create_new # 设置是否创建新文件的标志
+ self.filename = filename # 保存数据库文件名
+ self.readonly = readonly # 设置是否为只读模式
+ self.worker = worker # 保存工作线程引用,用于进度更新
+ self.retry = False # 初始化重试标志,用于错误恢复
+ # 析构函数
def __del__(self):
- self._close_connection()
+ self._close_connection() # 在对象被销毁时调用,确保资源被正确释放
+ # 关闭数据库连接
def _close_connection(self):
- if hasattr(self, '_connection'):
- self._connection.close()
- delattr(self, '_connection')
- if hasattr(self, '_cursor'):
- delattr(self, '_cursor')
- if hasattr(self, '_tmpdir'):
- self._tmpdir.cleanup()
- delattr(self, '_tmpdir')
+ # 检查并关闭数据库连接
+ if hasattr(self, '_connection'): # 检查是否有数据库连接属性
+ self._connection.close() # 关闭数据库连接
+ delattr(self, '_connection') # 删除数据库连接属性
+ # 检查并删除游标引用
+ if hasattr(self, '_cursor'): # 检查是否有游标属性
+ delattr(self, '_cursor') # 删除游标属性
+ # 检查并清理临时目录
+ if hasattr(self, '_tmpdir'): # 检查是否有临时目录属性
+ self._tmpdir.cleanup() # 清理临时目录
+ delattr(self, '_tmpdir') # 删除临时目录属性
+ # 建立数据库连接
def _establish_connection(self):
- if (self.create_new
- and not self.readonly
- and os.path.exists(self.filename)):
- os.remove(self.filename)
+ # 如果需要创建新文件且文件已存在,则删除现有文件
+ if (self.create_new # 检查是否创建新文件
+ and not self.readonly # 检查是否为只读模式
+ and os.path.exists(self.filename)): # 检查文件是否已存在
+ os.remove(self.filename) # 删除已存在的文件
- if self.create_new:
- self.scene.clear_save_ids()
+ # 如果创建新文件,清除场景中所有项目的保存I
+ if self.create_new: # 检查是否创建新文件
+ self.scene.clear_save_ids() # 清除场景中所有项目的保存ID
- uri = pathlib.Path(self.filename).resolve().as_uri()
- if self.readonly:
- uri = f'{uri}?mode=rw'
- self._connection = sqlite3.connect(uri, uri=True)
- self._cursor = self.connection.cursor()
- if not self.create_new:
+ uri = pathlib.Path(self.filename).resolve().as_uri()# 将文件路径转换为URI格式,确保跨平台兼容性
+ if self.readonly: # 检查是否为只读模式
+ uri = f'{uri}?mode=rw' # 如果是只读模式,添加查询参数以指定为读写模式
+ self._connection = sqlite3.connect(uri, uri=True) # 建立数据库连接,使用URI格式
+ self._cursor = self._connection.cursor() # 创建游标对象,用于执行SQL语句
+ # 如果不是创建新文件,则尝试迁移数据库
+ if not self.create_new: # 检查是否不是创建新文件
try:
- self._migrate()
- except Exception:
+ self._migrate() # 尝试迁移数据库
+ except Exception as e: # 捕获所有异常
# Updating a file failed; try creating it from scratch instead
- logger.exception('Error migrating bee file')
- self.create_new = True
- self._establish_connection()
+ logger.exception('Error migrating bee file: %s', e) # 记录异常日志,包含异常信息
+ self.create_new = True # 标记为创建新文件
+ self._establish_connection() # 重新建立数据库连接
+ # 迁移数据库
def _migrate(self):
"""Migrate database if necessary."""
- version = self.fetchone('PRAGMA user_version')[0]
- logger.debug(f'Found bee file version: {version}')
- if version >= USER_VERSION:
- logger.debug('Version ok; no migrations necessary')
- return
-
+ version = self.fetchone('PRAGMA user_version')[0] # 获取数据库用户版本号
+ logger.debug(f'Found bee file version: {version}') # 记录数据库版本号
+ if version >= USER_VERSION: # 检查数据库版本号是否大于等于用户版本号
+ logger.debug('Version ok; no migrations necessary') # 如果版本号大于等于用户版本号,无需迁移
+ return # 如果版本号大于等于用户版本号,无需迁移
+ # 如果版本已经是最新的,不需要迁移
if self.readonly:
try:
# See whether file is writable so we can migrate it directly
- self.ex('PRAGMA application_id=%s' % APPLICATION_ID)
- except sqlite3.Error:
- logger.debug('File not writable; use temporary copy instead')
- self._connection.close()
- self._tmpdir = tempfile.TemporaryDirectory(
- prefix=constants.APPNAME)
- tmpname = os.path.join(self._tmpdir.name, 'mig.bee')
- shutil.copyfile(self.filename, tmpname)
- self._connection = sqlite3.connect(tmpname)
- self._cursor = self.connection.cursor()
+ self.ex('PRAGMA application_id=%s' % APPLICATION_ID) # 设置应用程序ID,确保数据库与应用程序兼容
+ except sqlite3.Error as e: # 捕获 SQLite 错误异常
+ logger.debug('File not writable; use temporary copy instead')# 记录文件不可写日志,建议使用临时副本
+ self._connection.close() # 关闭数据库连接
+ self._tmpdir = tempfile.TemporaryDirectory( # 创建临时目录,用于存储迁移后的数据库文件
+ prefix=constants.APPNAME) # 设置临时目录前缀,确保与应用程序名称一致
+ tmpname = os.path.join(self._tmpdir.name, 'mig.bee') # 构建临时数据库文件名
+ shutil.copyfile(self.filename, tmpname) # 复制原始数据库文件到临时目录
+ self._connection = sqlite3.connect(tmpname) # 连接到临时数据库文件
+ self._cursor = self.connection.cursor() # 创建游标对象,用于执行SQL语句
- self.ex('BEGIN TRANSACTION')
- for i in range(version, USER_VERSION):
- logger.debug(f'Migrating from version {i} to {i + 1}...')
- for migration in MIGRATIONS[i + 1]:
- self.ex(migration)
- self.write_meta()
- self.connection.commit()
- logger.debug('Migration finished')
+ self.ex('BEGIN TRANSACTION') # 开始事务,确保迁移的原子性
+ for i in range(version, USER_VERSION): # 遍历需要迁移的版本号范围
+ logger.debug(f'Migrating from version {i} to {i + 1}...') # 记录迁移信息,包含当前版本号和目标版本号
+ for migration in MIGRATIONS[i + 1]: # 遍历当前版本号对应的迁移SQL语句列表
+ self.ex(migration) # 执行迁移SQL语句
+ self.write_meta() # 写入数据库元数据,包括应用程序ID和用户版本号
+ self.connection.commit() # 提交事务,确保所有迁移操作都被永久保存
+ logger.debug('Migration finished') # 记录迁移完成日志
@property
+ # 获取数据库连接属性
def connection(self):
- if not hasattr(self, '_connection'):
- self._establish_connection()
- return self._connection
+ if not hasattr(self, '_connection'): # 检查是否没有数据库连接属性
+ self._establish_connection() # 如果没有数据库连接属性,建立数据库连接
+ return self._connection # 返回数据库连接属性
@property
+ # 获取数据库游标属性
def cursor(self):
- if not hasattr(self, '_cursor'):
- self._establish_connection()
- return self._cursor
+ if not hasattr(self, '_cursor'): # 检查是否没有数据库游标属性
+ self._establish_connection() # 如果没有数据库游标属性,建立数据库连接
+ return self._cursor # 返回数据库游标属性
+ # 执行SQL语句,返回执行结果
def ex(self, *args, **kwargs):
return self.cursor.execute(*args, **kwargs)
+ # 执行批量SQL语句,返回执行结果
def exmany(self, *args, **kwargs):
return self.cursor.executemany(*args, **kwargs)
+ # 从数据库游标中获取一条记录,返回记录元组
def fetchone(self, *args, **kwargs):
- self.ex(*args, **kwargs)
- return self.cursor.fetchone()
+ self.ex(*args, **kwargs) # 执行SQL语句,传入参数和关键字参数
+ return self.cursor.fetchone() # 返回查询结果的第一行
+ # 从数据库游标中获取所有记录,返回记录列表
def fetchall(self, *args, **kwargs):
- self.ex(*args, **kwargs)
- return self.cursor.fetchall()
+ self.ex(*args, **kwargs) # 执行SQL语句,传入参数和关键字参数
+ return self.cursor.fetchall() # 从游标中获取所有记录,返回记录列表
+ # 写入数据库元数据,包括应用程序ID和用户版本号
def write_meta(self):
- self.ex('PRAGMA application_id=%s' % APPLICATION_ID)
- self.ex('PRAGMA user_version=%s' % USER_VERSION)
- self.ex('PRAGMA foreign_keys=ON')
+ self.ex('PRAGMA application_id=%s' % APPLICATION_ID) # 设置应用程序ID,确保数据库与应用程序兼容
+ self.ex('PRAGMA user_version=%s' % USER_VERSION) # 设置用户版本,用于版本控制
+ self.ex('PRAGMA foreign_keys=ON') # 启用外键约束,确保数据完整性
+ # 创建新文件时创建数据库架构
def create_schema_on_new(self):
- if self.create_new:
- self.write_meta()
- for schema in SCHEMA:
- self.ex(schema)
+ if self.create_new: # 仅在创建新文件时执行
+ self.write_meta() # 写入数据库元数据
+ for schema in SCHEMA: # 执行所有架构创建SQL语句
+ self.ex(schema) # 执行架构创建SQL语句
+ # 从数据库中读取所有项,包括图片和文本项
@handle_sqlite_errors
def read(self):
+ # 读取所有图像项目及其关联的二进制数据
rows = self.fetchall(
'SELECT items.id, type, x, y, z, scale, rotation, flip, '
'items.data, sqlar.data '
'FROM sqlar JOIN items on sqlar.item_id = items.id')
# Avoid OUTER JOIN for performance reasons; fetch text items
# separately instead
+ # 读取所有文本项目
rows.extend(self.fetchall(
'SELECT items.id, type, x, y, z, scale, rotation, flip, '
' items.data, null as data '
'FROM items '
'WHERE items.type = "text"'))
- if self.worker:
- self.worker.begin_processing.emit(len(rows))
+ if self.worker: # 如果有工作线程
+ self.worker.begin_processing.emit(len(rows)) # 发送开始处理信号,包含项数
+ # 遍历所有读取的项目数据
for i, row in enumerate(rows):
+ # 构建项目数据字典
data = {
- 'save_id': row[0],
- 'type': row[1],
- 'x': row[2],
- 'y': row[3],
- 'z': row[4],
- 'scale': row[5],
- 'rotation': row[6],
- 'flip': row[7],
- 'data': json.loads(row[8]),
+ 'save_id': row[0], # 项目ID
+ 'type': row[1], # 项目类型
+ 'x': row[2], # X坐标
+ 'y': row[3], # Y坐标
+ 'z': row[4], # Z坐标
+ 'scale': row[5], # 缩放比例
+ 'rotation': row[6], # 旋转角度
+ 'flip': row[7], # 是否翻转
+ 'data': json.loads(row[8]), # 项目数据
}
- if data['type'] == 'pixmap':
- item = BeePixmapItem(QtGui.QImage())
- item.pixmap_from_bytes(row[9])
+ # 处理像素图项目
+ if data['type'] == 'pixmap': # 如果项目类型为像素图
+ item = BeePixmapItem(QtGui.QImage()) # 创建空的像素图项目
+ item.pixmap_from_bytes(row[9]) # 从二进制数据创建像素图
+ # 检查图像是否加载成功
if item.pixmap().isNull():
- item = data['data']['text'] = (
+ # 如果图像加载失败,创建错误项目
+ item = data['data']['text'] = (
f'Image could not be loaded: {item.filename}\n'
+ IMG_LOADING_ERROR_MSG)
- data['type'] = BeeErrorItem.TYPE
- data['item'] = item
-
- self.scene.add_item_later(data)
+ data['type'] = BeeErrorItem.TYPE # 将项目类型设置为错误项类型
+ data['item'] = item # 将项目添加到数据字典中
+ self.scene.add_item_later(data) # 稍后将项目添加到场景中
+ # 如果有工作线程,更新进度并检查是否取消操作
if self.worker:
- logger.trace(f'Emit progress: {i}')
- self.worker.progress.emit(i)
- if self.worker.canceled:
- self.worker.finished.emit('', [])
- return
+ logger.trace(f'Emit progress: {i}') # 记录进度信息
+ self.worker.progress.emit(i) # 发送进度信号,包含当前项索引
+ if self.worker.canceled: # 如果工作线程已取消
+ self.worker.finished.emit('', []) # 发送完成信号,包含空字符串和空列表
+ return # 退出循环,停止处理
# Give main thread time to process items:
- self.worker.msleep(10)
- if self.worker:
- self.worker.finished.emit(self.filename, [])
+ self.worker.msleep(10) # 暂停10毫秒,允许主线程处理事件
+ if self.worker: # 如果有工作线程
+ self.worker.finished.emit(self.filename, [])# 发送完成信号,包含文件名和空列表
@handle_sqlite_errors
+ # 写入数据库中的所有项,包括图片和文本项
def write(self):
- if self.readonly:
- raise sqlite3.OperationalError(
+ # 检查是否为只读模式
+ if self.readonly: # 如果数据库为只读模式
+ raise sqlite3.OperationalError( # 抛出操作错误异常
'Attempt to write to a readonly database')
- try:
- self.create_schema_on_new()
- self.write_data()
- except Exception:
- if self.retry:
+ try: # 尝试执行写入操作
+ self.create_schema_on_new() # 创建新文件时创建数据库架构
+ self.write_data() # 写入所有数据
+ except Exception as e: # 捕获所有异常
+ #处理写入错误
+ if self.retry: # 如果允许重试
# Trying to recover failed
- raise
+ raise # 如果已经重试过一次,则放弃并抛出异常
else:
- self.retry = True
+ self.retry = True # 第一次失败后,标记为需要重试
# Try creating file from scratch and save again
- logger.exception(
- f'Updating to existing file {self.filename} failed')
- self.create_new = True
- self._close_connection()
- self.write()
+ logger.exception( # 尝试从头创建文件并再次保存
+ f'Updating to existing file {self.filename} failed') # 记录更新失败的异常信息
+ self.create_new = True # 标记为创建新文件
+ self._close_connection() # 关闭现有连接,以便重新建立连接
+ self.write() # 递归调用写入方法,尝试重新创建文件并保存
+ # 写入场景数据到数据库
def write_data(self):
- to_delete = {row[0] for row in self.fetchall('SELECT id from ITEMS')}
+ to_delete = {row[0] for row in self.fetchall('SELECT id from ITEMS')} # 获取所有需要删除的项目ID
# We don't want to touch existing items that are displayed as errors:
keep = {item.original_save_id
- for item in self.scene.items_by_type(BeeErrorItem.TYPE)}
- logger.debug(f'Not saving error items: {keep}')
- to_delete = to_delete - keep
+ for item in self.scene.items_by_type(BeeErrorItem.TYPE)} # 获取所有错误项的原始保存ID
+ logger.debug(f'Not saving error items: {keep}') # 记录不保存错误项的ID
+ to_delete = to_delete - keep # 从待删除列表中移除需要保留的项目
+ # 获取需要保存的项目列表
to_save = list(self.scene.items_for_save())
+ # 如果有工作线程,发送开始处理信号
if self.worker:
self.worker.begin_processing.emit(len(to_save))
+ # 遍历所有需要保存的项目
for i, item in enumerate(to_save):
- logger.debug(f'Saving {item} with id {item.save_id}')
- if item.save_id:
- self.update_item(item)
- to_delete.remove(item.save_id)
- else:
- self.insert_item(item)
+ logger.debug(f'Saving {item} with id {item.save_id}') # 记录正在保存的项目信息
+ if item.save_id: # 如果项目已存在数据库中
+ self.update_item(item) # 更新项目数据
+ to_delete.remove(item.save_id) # 从待删除列表中移除已更新的项目ID
+ else: # 如果项目不在数据库中
+ self.insert_item(item) # 插入新项目到数据库中
+ # 如果有工作线程,发送进度信号,包含当前项索引
if self.worker:
self.worker.progress.emit(i)
+ # 如果工作线程已取消,跳出循环,停止处理
if self.worker.canceled:
break
- self.delete_items(to_delete)
- self.ex('VACUUM')
- self.connection.commit()
- if self.worker:
- self.worker.finished.emit(self.filename, [])
+ self.delete_items(to_delete) # 删除不再需要的项目
+ self.ex('VACUUM') # 执行VACUUM命令,优化数据库空间
+ self.connection.commit() # 提交事务,确保所有更改生效
+ if self.worker: # 如果有工作线程
+ self.worker.finished.emit(self.filename, []) # 发送完成信号,包含文件名和空列表
+ # 从数据库中删除指定的项目
def delete_items(self, to_delete):
- to_delete = [(pk,) for pk in to_delete]
- self.exmany('DELETE FROM items WHERE id=?', to_delete)
- self.exmany('DELETE FROM sqlar WHERE item_id=?', to_delete)
- self.connection.commit()
+ to_delete = [(pk,) for pk in to_delete] # 将待删除项目ID转换为元组列表
+ self.exmany('DELETE FROM items WHERE id=?', to_delete) # 执行批量删除项目的SQL语句
+ self.exmany('DELETE FROM sqlar WHERE item_id=?', to_delete) # 执行批量删除关联文件的SQL语句
+ self.connection.commit() # 提交事务,确保所有更改生效
+ # 向数据库中插入新项目
def insert_item(self, item):
+ # 插入项目基本信息到items表
self.ex(
'INSERT INTO items (type, x, y, z, scale, rotation, flip, '
'data) '
'VALUES (?, ?, ?, ?, ?, ?, ?, ?)',
- (item.TYPE, item.pos().x(), item.pos().y(), item.zValue(),
- item.scale(), item.rotation(), item.flip(),
- json.dumps(item.get_extra_save_data())))
- item.save_id = self.cursor.lastrowid
-
+ (item.TYPE, item.pos().x(), item.pos().y(), item.zValue(), # 插入项目的类型、位置、Z值、缩放比例、旋转角度和翻转状态
+ item.scale(), item.rotation(), item.flip(), # 插入项目的缩放比例、旋转角度和翻转状态
+ json.dumps(item.get_extra_save_data()))) # 插入项目的额外保存数据
+ item.save_id = self.cursor.lastrowid # 获取新插入项目的ID
+ # 如果项目有像素图数据,也保存它
if hasattr(item, 'pixmap_to_bytes'):
- pixmap, imgformat = item.pixmap_to_bytes()
- name = item.get_filename_for_export(imgformat)
+ pixmap, imgformat = item.pixmap_to_bytes() # 获取像素图的二进制数据和图像格式
+ name = item.get_filename_for_export(imgformat) # 获取导出文件名
+ # 将二进制数据插入到sqlar表
self.ex(
'INSERT INTO sqlar (item_id, name, mode, sz, data) '
'VALUES (?, ?, ?, ?, ?)',
- (item.save_id, name, 0o644, len(pixmap), pixmap))
- self.connection.commit()
+ (item.save_id, name, 0o644, len(pixmap), pixmap)) # 插入像素图数据到sqlar表
+ self.connection.commit() # 提交事务,确保所有更改生效
+ # 更新数据库中的项目数据
def update_item(self, item):
"""Update item data.
We only update the item data, not the pixmap data, as pixmap
data never changes and is also time-consuming to save.
"""
+ # 更新items表中的项目数据
self.ex(
'UPDATE items SET x=?, y=?, z=?, scale=?, rotation=?, flip=?, '
'data=? '
'WHERE id=?',
- (item.pos().x(), item.pos().y(), item.zValue(), item.scale(),
- item.rotation(), item.flip(),
- json.dumps(item.get_extra_save_data()),
- item.save_id))
- self.connection.commit()
+ (item.pos().x(), item.pos().y(), item.zValue(), item.scale(), # 更新项目的位置、Z值、缩放比例
+ item.rotation(), item.flip(), # 更新项目的旋转角度和翻转状态
+ json.dumps(item.get_extra_save_data()), # 更新项目的额外保存数据
+ item.save_id)) # 更新项目的ID
+ self.connection.commit() # 提交事务,确保所有更改生效