Skip to content

Commit 3c4b031

Browse files
committed
v1.9.5: 优化并行模式、图片序列号,提高编辑器稳定性
1 parent cc65bfe commit 3c4b031

6 files changed

Lines changed: 141 additions & 68 deletions

File tree

desktop_qt_ui/editor/editor_model.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,4 +191,4 @@ def update_region_data(self, index: int, key: str, value: Any):
191191
def update_region_silent(self, index: int, new_data: dict):
192192
"""静默更新整个区域数据,不发出信号"""
193193
if 0 <= index < len(self._regions):
194-
self._regions[index].update(new_data)
194+
self._regions[index].update(new_data)

desktop_qt_ui/editor/graphics_view.py

Lines changed: 69 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,17 @@ def clear_all_state(self):
208208
self.scene.removeItem(self._preview_item)
209209
self._preview_item = None
210210

211+
# 清空框选矩形
212+
if self._box_select_rect_item:
213+
try:
214+
if self._box_select_rect_item.scene():
215+
self.scene.removeItem(self._box_select_rect_item)
216+
except RuntimeError:
217+
pass # C++ 对象已被删除
218+
self._box_select_rect_item = None
219+
self._is_box_selecting = False
220+
self._box_select_start_pos = None
221+
211222
# 清空缓存
212223
if hasattr(self, '_text_render_cache'):
213224
self._text_render_cache.clear()
@@ -246,6 +257,7 @@ def on_image_changed(self, image):
246257
self._inpainted_image_item = None
247258
self._preview_item = None
248259
self._image_np = None
260+
self._box_select_rect_item = None # scene.clear() 会删除所有 item
249261

250262
if image is None:
251263
return
@@ -1154,8 +1166,17 @@ def mousePressEvent(self, event):
11541166
self._is_box_selecting = True
11551167
self._box_select_start_pos = self.mapToScene(event.pos())
11561168

1157-
# 创建框选矩形
1158-
if self._box_select_rect_item is None:
1169+
# 创建框选矩形(检查是否需要重新创建)
1170+
need_create = self._box_select_rect_item is None
1171+
if not need_create:
1172+
# 检查 C++ 对象是否仍然有效
1173+
try:
1174+
_ = self._box_select_rect_item.scene()
1175+
except RuntimeError:
1176+
need_create = True
1177+
self._box_select_rect_item = None
1178+
1179+
if need_create:
11591180
pen = QPen(QColor(0, 120, 215, 180)) # 蓝色半透明
11601181
pen.setWidth(2)
11611182
pen.setStyle(Qt.PenStyle.DashLine)
@@ -1186,12 +1207,24 @@ def mouseMoveEvent(self, event):
11861207
"""Handle mouse move for drawing."""
11871208
# 处理框选
11881209
if self._is_box_selecting and self._box_select_start_pos:
1189-
current_pos = self.mapToScene(event.pos())
1190-
# 更新框选矩形
1191-
rect = QRectF(self._box_select_start_pos, current_pos).normalized()
1192-
self._box_select_rect_item.setRect(rect)
1193-
event.accept()
1194-
return
1210+
# 检查框选矩形是否仍然有效(可能在切换图片时被删除)
1211+
if self._box_select_rect_item is None:
1212+
self._is_box_selecting = False
1213+
self._box_select_start_pos = None
1214+
return
1215+
try:
1216+
current_pos = self.mapToScene(event.pos())
1217+
# 更新框选矩形
1218+
rect = QRectF(self._box_select_start_pos, current_pos).normalized()
1219+
self._box_select_rect_item.setRect(rect)
1220+
event.accept()
1221+
return
1222+
except RuntimeError:
1223+
# C++ 对象已被删除,重置状态
1224+
self._box_select_rect_item = None
1225+
self._is_box_selecting = False
1226+
self._box_select_start_pos = None
1227+
return
11951228

11961229
if self._is_drawing_textbox:
11971230
self._update_textbox_drawing(event.pos())
@@ -1231,31 +1264,36 @@ def mouseReleaseEvent(self, event):
12311264
# 处理框选完成
12321265
if self._is_box_selecting and event.button() == Qt.MouseButton.LeftButton:
12331266
self._is_box_selecting = False
1267+
self._box_select_start_pos = None
12341268

1235-
# 隐藏框选矩形
1269+
# 隐藏框选矩形(检查对象是否仍然有效)
12361270
if self._box_select_rect_item:
1237-
select_rect = self._box_select_rect_item.rect()
1238-
self._box_select_rect_item.setVisible(False)
1239-
1240-
# 查找框内的所有 RegionTextItem
1241-
selected_indices = []
1242-
for i, item in enumerate(self._region_items):
1243-
if isinstance(item, RegionTextItem):
1244-
# 检查 item 的边界是否与选择框相交
1245-
item_rect = item.sceneBoundingRect()
1246-
if select_rect.intersects(item_rect):
1247-
selected_indices.append(i)
1248-
1249-
# 更新选择
1250-
ctrl_pressed = bool(event.modifiers() & Qt.KeyboardModifier.ControlModifier)
1251-
if ctrl_pressed:
1252-
# Ctrl+框选:添加到现有选择
1253-
current_selection = self.model.get_selection()
1254-
new_selection = list(set(current_selection + selected_indices))
1255-
self.model.set_selection(new_selection)
1256-
else:
1257-
# 普通框选:替换选择
1258-
self.model.set_selection(selected_indices)
1271+
try:
1272+
select_rect = self._box_select_rect_item.rect()
1273+
self._box_select_rect_item.setVisible(False)
1274+
1275+
# 查找框内的所有 RegionTextItem
1276+
selected_indices = []
1277+
for i, item in enumerate(self._region_items):
1278+
if isinstance(item, RegionTextItem):
1279+
# 检查 item 的边界是否与选择框相交
1280+
item_rect = item.sceneBoundingRect()
1281+
if select_rect.intersects(item_rect):
1282+
selected_indices.append(i)
1283+
1284+
# 更新选择
1285+
ctrl_pressed = bool(event.modifiers() & Qt.KeyboardModifier.ControlModifier)
1286+
if ctrl_pressed:
1287+
# Ctrl+框选:添加到现有选择
1288+
current_selection = self.model.get_selection()
1289+
new_selection = list(set(current_selection + selected_indices))
1290+
self.model.set_selection(new_selection)
1291+
else:
1292+
# 普通框选:替换选择
1293+
self.model.set_selection(selected_indices)
1294+
except RuntimeError:
1295+
# C++ 对象已被删除,重置引用
1296+
self._box_select_rect_item = None
12591297

12601298
event.accept()
12611299
return

desktop_qt_ui/services/export_service.py

Lines changed: 39 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -184,26 +184,27 @@ def _perform_backend_render_export(self, image: Image.Image, regions_data: List[
184184
)
185185

186186
if rendered_text_layer:
187-
# Composite the text layer onto the original image
188-
final_image = image.copy()
189-
if final_image.mode != 'RGBA':
190-
temp_img = final_image.convert('RGBA')
191-
final_image.close() # 释放旧图像
192-
final_image = temp_img
193-
if rendered_text_layer.mode != 'RGBA':
194-
temp_layer = rendered_text_layer.convert('RGBA')
195-
rendered_text_layer.close() # 释放旧图像
196-
rendered_text_layer = temp_layer
197-
198-
# Ensure sizes match before pasting, resizing if necessary
199-
if final_image.size != rendered_text_layer.size:
200-
self.logger.warning(f"Size mismatch: Original {final_image.size}, Rendered {rendered_text_layer.size}. Resizing text layer.")
201-
temp_layer = rendered_text_layer.resize(final_image.size, Image.LANCZOS)
202-
rendered_text_layer.close() # 释放旧图像
203-
rendered_text_layer = temp_layer
204-
205-
final_image.paste(rendered_text_layer, (0, 0), rendered_text_layer)
206-
rendered_text_layer.close() # 释放渲染层
187+
# 检查是否开启了超分(通过尺寸判断)
188+
# 如果后端返回的图片尺寸与原图不同,说明进行了超分处理
189+
# 此时后端返回的是完整的渲染结果(超分背景+文字),直接使用即可
190+
if image.size != rendered_text_layer.size:
191+
self.logger.info(f"检测到超分: 原图 {image.size}, 渲染结果 {rendered_text_layer.size}. 直接使用超分后的完整结果。")
192+
final_image = rendered_text_layer
193+
rendered_text_layer = None # 避免后续释放
194+
else:
195+
# 尺寸相同,按原逻辑合成文字层到原图
196+
final_image = image.copy()
197+
if final_image.mode != 'RGBA':
198+
temp_img = final_image.convert('RGBA')
199+
final_image.close()
200+
final_image = temp_img
201+
if rendered_text_layer.mode != 'RGBA':
202+
temp_layer = rendered_text_layer.convert('RGBA')
203+
rendered_text_layer.close()
204+
rendered_text_layer = temp_layer
205+
206+
final_image.paste(rendered_text_layer, (0, 0), rendered_text_layer)
207+
rendered_text_layer.close()
207208

208209
# --- Safer saving logic ---
209210
temp_output_path = output_path + ".tmp"
@@ -277,6 +278,12 @@ def _perform_backend_render_export(self, image: Image.Image, regions_data: List[
277278

278279
def _save_regions_data(self, regions_data: List[Dict[str, Any]], json_path: str, mask: Optional[np.ndarray] = None, config: Optional[Dict[str, Any]] = None):
279280
"""保存区域数据到JSON文件,确保格式与TextBlock兼容"""
281+
# 获取超分倍率,用于放大坐标
282+
upscale_ratio = 1
283+
if config:
284+
upscale_config = config.get('upscale', {})
285+
upscale_ratio = upscale_config.get('upscale_ratio', 0) or 1
286+
280287
# 准备保存数据,确保数据格式正确
281288
save_data = []
282289
for region in regions_data:
@@ -441,19 +448,19 @@ def _prepare_translator_params(self, config: Dict[str, Any]) -> Dict[str, Any]:
441448
"""准备翻译器参数"""
442449
translator_params = {}
443450

444-
# 提取字体路径
451+
# 字体路径处理:让后端自己处理,不在这里转换
452+
# 后端的 set_font 会在路径不存在时自动回退到 DEFAULT_FONT
453+
# 只有当配置中是绝对路径且存在时才传递
445454
render_config = config.get('render', {})
446-
font_filename = render_config.get('font_path')
447-
if font_filename:
448-
font_full_path = os.path.join(
449-
os.path.dirname(os.path.dirname(__file__)), 'fonts', font_filename
450-
)
451-
font_full_path = os.path.abspath(font_full_path)
452-
if os.path.exists(font_full_path):
453-
translator_params['font_path'] = font_full_path
454-
# 同时更新 config 中的 font_path
455-
config['render']['font_path'] = font_full_path
456-
self.logger.info(f"设置字体路径: {font_full_path}")
455+
font_path_value = render_config.get('font_path')
456+
if font_path_value and os.path.isabs(font_path_value) and os.path.exists(font_path_value):
457+
translator_params['font_path'] = font_path_value
458+
self.logger.info(f"设置字体路径: {font_path_value}")
459+
else:
460+
# 清除相对路径,让后端使用默认字体
461+
if 'render' in config and 'font_path' in config['render']:
462+
config['render']['font_path'] = None
463+
self.logger.info("使用后端默认字体")
457464

458465
# 提取输出格式
459466
output_format = self.get_output_format_from_config(config)

doc/CHANGELOG_v1.9.5.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# v1.9.5 更新日志
2+
3+
发布日期:2025-12-13
4+
5+
## ⚡ 优化
6+
7+
### 并行翻译模式
8+
- **并行模式优化**:改进了并行翻译的处理逻辑
9+
- **图片序列号优化**:优化了发送的图片序列号管理,提高批量处理效率
10+
11+
### 编辑器稳定性
12+
- **提高编辑器稳定性**:修复了框选操作时切换图片可能导致的崩溃问题
13+
- **超分导出修复**:修复了开启超分时导出图片文字丢失的问题
14+
- **字体路径修复**:修复了打包环境下字体路径解析问题

manga_translator/rendering/text_render.py

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -274,7 +274,21 @@ def update_font_selection():
274274

275275
def set_font(path: str):
276276
global FONT
277-
if not path or not os.path.exists(path):
277+
278+
# 处理相对路径:尝试在 BASE_PATH 下查找
279+
resolved_path = path
280+
if path and not os.path.isabs(path) and not os.path.exists(path):
281+
# 尝试 BASE_PATH/fonts/filename 或 BASE_PATH/path
282+
possible_paths = [
283+
os.path.join(BASE_PATH, 'fonts', os.path.basename(path)),
284+
os.path.join(BASE_PATH, path),
285+
]
286+
for p in possible_paths:
287+
if os.path.exists(p):
288+
resolved_path = p
289+
break
290+
291+
if not resolved_path or not os.path.exists(resolved_path):
278292
if path:
279293
logger.error(f'Could not load font: {path}')
280294
try:
@@ -287,9 +301,9 @@ def set_font(path: str):
287301
return
288302

289303
try:
290-
FONT = freetype.Face(Path(path).open('rb'))
304+
FONT = freetype.Face(Path(resolved_path).open('rb'))
291305
except (freetype.ft_errors.FT_Exception, FileNotFoundError):
292-
logger.error(f'Could not load font: {path}')
306+
logger.error(f'Could not load font: {resolved_path}')
293307
try:
294308
FONT = freetype.Face(Path(DEFAULT_FONT).open('rb'))
295309
except (freetype.ft_errors.FT_Exception, FileNotFoundError):

packaging/VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
v1.9.4
1+
v1.9.5

0 commit comments

Comments
 (0)