Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
151 changes: 147 additions & 4 deletions ui/canvas.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import numpy as np
from typing import List, Union
import os
import copy
import math

from qtpy.QtWidgets import QApplication, QSlider, QMenu, QGraphicsScene, QGraphicsSceneDragDropEvent , QGraphicsView, QGraphicsSceneDragDropEvent, QGraphicsRectItem, QGraphicsItem, QScrollBar, QGraphicsPixmapItem, QGraphicsSceneMouseEvent, QGraphicsSceneContextMenuEvent, QRubberBand
from qtpy.QtWidgets import QApplication, QSlider, QMenu, QGraphicsScene, QGraphicsSceneDragDropEvent , QGraphicsView, QGraphicsSceneDragDropEvent, QGraphicsRectItem, QGraphicsItem, QScrollBar, QGraphicsPixmapItem, QGraphicsSceneMouseEvent, QGraphicsSceneContextMenuEvent, QRubberBand, QAction
from qtpy.QtCore import Qt, QDateTime, QRectF, QPointF, QPoint, Signal, QSizeF, QEvent
from qtpy.QtGui import QKeySequence, QPixmap, QImage, QHideEvent, QKeyEvent, QWheelEvent, QResizeEvent, QPainter, QPen, QPainterPath, QCursor, QNativeGestureEvent

Expand All @@ -20,6 +22,7 @@
from utils import shared as C
from utils.config import pcfg
from utils.proj_imgtrans import ProjImgTrans
from .textedit_commands import ClearTextMaskCommand, WarpItemCommand

CANVAS_SCALE_MAX = 10.0
CANVAS_SCALE_MIN = 0.01
Expand Down Expand Up @@ -189,6 +192,8 @@ class Canvas(QGraphicsScene):
drop_open_folder = Signal(str)
context_menu_requested = Signal(QPoint, bool)
incanvas_selection_changed = Signal()
warp_edit_mode_changed = Signal(bool)
text_mask_edit_mode_changed = Signal(bool)
switch_text_item = Signal(int, QKeyEvent)

def __init__(self, parent=None):
Expand All @@ -199,6 +204,9 @@ def __init__(self, parent=None):
self.creating_textblock = False
self.create_block_origin: QPointF = None
self.editing_textblkitem: TextBlkItem = None
self.warp_edit_mode = False
self.text_mask_edit_mode = False
self.text_mask_brush_px = 18.0

self.gv = CustomGV(self)
self.gv.scale_down_signal.connect(self.scaleDown)
Expand Down Expand Up @@ -428,6 +436,12 @@ def setTextLayerTransparency(self, transparency: float):
self.textLayer.setOpacity(transparency)
self.text_transparency = transparency

def invalidateTextItemRenderCache(self):
for item in self.textLayer.childItems():
item.update()
self.textLayer.update()
self.update()

def adjustScrollBar(self, scrollBar: QScrollBar, factor: float):
scrollBar.setValue(int(factor * scrollBar.value() + ((factor - 1) * scrollBar.pageStep() / 2)))

Expand Down Expand Up @@ -472,6 +486,7 @@ def on_selection_changed(self):
blk_item = self.txtblkShapeControl.blk_item
if blk_item is not None and blk_item.isEditing():
blk_item.endEdit()
self.txtblkShapeControl.setWarpEditing(self.warp_edit_mode)
if self.hasFocus() and not self.block_selection_signal:
self.incanvas_selection_changed.emit()

Expand Down Expand Up @@ -554,6 +569,11 @@ def endCreateTextblock(self, btn=0):
return textblk_created

def mouseMoveEvent(self, event: QGraphicsSceneMouseEvent) -> None:
if self.textEditMode() and self.text_mask_edit_mode:
sel = self.selected_text_items(sort=False)
for it in sel:
if getattr(it, '_text_mask_drawing', False):
return super().mouseMoveEvent(event)
if self.mid_btn_pressed:
new_pos = event.screenPos()
delta_pos = new_pos - self.pan_initial_pos
Expand Down Expand Up @@ -632,6 +652,8 @@ def mousePressEvent(self, event: QGraphicsSceneMouseEvent) -> None:
self.mid_btn_pressed = True
self.pan_initial_pos = event.screenPos()
return
if btn == Qt.MouseButton.RightButton and self.textEditMode() and self.text_mask_edit_mode:
return super().mousePressEvent(event)

if self.imgtrans_proj.img_valid:
if self.textblock_mode and len(self.selectedItems()) == 0 and self.textEditMode():
Expand Down Expand Up @@ -680,7 +702,7 @@ def mouseReleaseEvent(self, event: QGraphicsSceneMouseEvent) -> None:
if btn == Qt.MouseButton.RightButton:
if self.stroke_img_item is not None:
self.finish_erasing.emit(self.stroke_img_item)
if self.textEditMode() and not textblk_created:
if self.textEditMode() and not textblk_created and not self.text_mask_edit_mode:
self.context_menu_requested.emit(event.screenPos(), False)
if btn == Qt.MouseButton.LeftButton:
if self.stroke_img_item is not None:
Expand Down Expand Up @@ -755,8 +777,89 @@ def setTextLayerTransparencyBySlider(self, slider_value: int):
def setTextBlockMode(self, mode: bool):
self.textblock_mode = mode

def setWarpEditMode(self, enabled: bool):
enabled = bool(enabled)
if self.warp_edit_mode == enabled:
return
self.warp_edit_mode = enabled
if self.txtblkShapeControl is not None:
self.txtblkShapeControl.setWarpEditing(self.warp_edit_mode)
self.warp_edit_mode_changed.emit(self.warp_edit_mode)

def setTextMaskEditMode(self, enabled: bool):
enabled = bool(enabled)
if self.text_mask_edit_mode == enabled:
return
self.text_mask_edit_mode = enabled
self.text_mask_edit_mode_changed.emit(self.text_mask_edit_mode)

def _selected_or_controlled_text_items(self):
sel = self.selected_text_items(sort=False)
if len(sel) == 0 and self.txtblkShapeControl.blk_item is not None:
sel = [self.txtblkShapeControl.blk_item]
return sel

def clearTextMask(self):
sel = self._selected_or_controlled_text_items()
if len(sel) == 1:
self.push_undo_command(ClearTextMaskCommand(sel[0]))

def applyWarpPreset(self, preset: str):
preset = str(preset)
sel = self._selected_or_controlled_text_items()
if len(sel) == 0:
return
for item in sel:
blk = item.blk
before = (getattr(blk, 'warp_mode', 'none'),
copy.deepcopy(getattr(blk, 'warp_quad', None)),
copy.deepcopy(getattr(blk, 'warp_mesh_size', None)),
copy.deepcopy(getattr(blk, 'warp_mesh', None)),
float(getattr(blk, 'warp_rise_fall_u', 0.5) or 0.5),
float(getattr(blk, 'warp_rise_fall_amp', 0.0) or 0.0))
if preset == 'reset':
after = ('none', None, None, None, 0.5, 0.0)
else:
amp = 0.18
br = item.boundingRect()
w = float(br.width())
h = float(br.height())
target_cell_px = 64.0
nx = int(max(7, min(25, round(w / target_cell_px) + 1)))
ny = int(max(5, min(15, round(h / target_cell_px) + 1)))
mesh = []
for j in range(ny):
y = j / (ny - 1)
for i in range(nx):
x = i / (nx - 1)
if preset == 'arc_up':
dy = -amp * math.sin(math.pi * x)
elif preset == 'arc_down':
dy = amp * math.sin(math.pi * x)
elif preset == 'arch':
dy = -amp * math.sin(math.pi * x) * (1.0 - y)
else:
dy = amp * math.sin(2 * math.pi * x)
yy = max(0.0, min(1.0, y + dy))
mesh.append([x, yy])
if preset == 'arc_up':
ru, ra = 0.5, float(amp)
elif preset == 'arc_down':
ru, ra = 0.5, float(-amp)
else:
ru, ra = 0.5, 0.0
after = ('mesh', None, [nx, ny], mesh, ru, ra)
self.push_undo_command(WarpItemCommand(item, before, after, self.txtblkShapeControl))

def triggerResetAngle(self):
self.reset_angle.emit()

def triggerSqueeze(self):
self.squeeze_blk.emit()

def on_create_contextmenu(self, pos: QPoint, is_textpanel: bool):
if self.textEditMode() and not self.creating_textblock:
move_tools_to_sidebar = bool(getattr(pcfg, 'move_text_tools_to_sidebar', False))
menu = QMenu(self.gv)
copy_act = menu.addAction(self.tr("Copy"))
copy_act.setShortcut(QKeySequence.StandardKey.Copy)
Expand All @@ -777,6 +880,38 @@ def on_create_contextmenu(self, pos: QPoint, is_textpanel: bool):
layout_act = menu.addAction(self.tr("Auto layout"))
angle_act = menu.addAction(self.tr("Reset Angle"))
squeeze_act = menu.addAction(self.tr("Squeeze"))
free_transform_act = None
text_eraser_act = None
clear_text_eraser_act = None
warp_actions = {}
if not move_tools_to_sidebar:
menu.addSeparator()

free_transform_act = QAction(self.tr("Free Transform"), menu)
free_transform_act.setCheckable(True)
free_transform_act.setChecked(self.warp_edit_mode)
menu.addAction(free_transform_act)

text_eraser_act = QAction(self.tr("Text Eraser"), menu)
text_eraser_act.setCheckable(True)
text_eraser_act.setChecked(self.text_mask_edit_mode)
menu.addAction(text_eraser_act)

clear_text_eraser_act = menu.addAction(self.tr("Clear Text Mask"))

warp_menu = menu.addMenu(self.tr("Warp Preset"))
warp_reset = warp_menu.addAction(self.tr("Reset Warp"))
warp_arc_up = warp_menu.addAction(self.tr("Arc Up"))
warp_arc_down = warp_menu.addAction(self.tr("Arc Down"))
warp_arch = warp_menu.addAction(self.tr("Arch"))
warp_flag = warp_menu.addAction(self.tr("Flag"))
warp_actions = {
warp_reset: 'reset',
warp_arc_up: 'arc_up',
warp_arc_down: 'arc_down',
warp_arch: 'arch',
warp_flag: 'flag',
}
menu.addSeparator()
translate_act = menu.addAction(self.tr("translate"))
ocr_act = menu.addAction(self.tr("OCR"))
Expand All @@ -803,9 +938,17 @@ def on_create_contextmenu(self, pos: QPoint, is_textpanel: bool):
elif rst == layout_act:
self.layout_textblks.emit()
elif rst == angle_act:
self.reset_angle.emit()
self.triggerResetAngle()
elif rst == squeeze_act:
self.squeeze_blk.emit()
self.triggerSqueeze()
elif free_transform_act is not None and rst == free_transform_act:
self.setWarpEditMode(not self.warp_edit_mode)
elif text_eraser_act is not None and rst == text_eraser_act:
self.setTextMaskEditMode(not self.text_mask_edit_mode)
elif clear_text_eraser_act is not None and rst == clear_text_eraser_act:
self.clearTextMask()
elif rst in warp_actions:
self.applyWarpPreset(warp_actions[rst])
elif rst == translate_act:
self.run_blktrans.emit(-1)
elif rst == ocr_act:
Expand Down
22 changes: 21 additions & 1 deletion ui/configpanel.py
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,7 @@ class ConfigPanel(Widget):
unload_models = Signal()
reload_textstyle = Signal(bool)
show_only_custom_font = Signal(bool)
text_tools_placement_changed = Signal(bool)

def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
Expand Down Expand Up @@ -459,9 +460,17 @@ def __init__(self, *args, **kwargs) -> None:
self.let_textstyle_indep_checker, _ = generalConfigPanel.addCheckBox(self.tr('Independent text styles for each projects'))
self.let_textstyle_indep_checker.stateChanged.connect(self.on_textstyle_indep_changed)

self.high_quality_warp_preview_checker, _ = generalConfigPanel.addCheckBox(self.tr('High Quality Warp Preview'))
self.high_quality_warp_preview_checker.stateChanged.connect(self.on_high_quality_warp_preview_changed)

self.let_show_only_custom_fonts, sublock = generalConfigPanel.addCheckBox(self.tr("Show only custom fonts"))
self.let_show_only_custom_fonts.stateChanged.connect(self.on_show_only_custom_fonts)

self.move_text_tools_to_sidebar_checker, _ = generalConfigPanel.addCheckBox(
self.tr('Move warp/transform/eraser to advanced text sidebar')
)
self.move_text_tools_to_sidebar_checker.stateChanged.connect(self.on_move_text_tools_to_sidebar_changed)

generalConfigPanel.addTextLabel(label_save)
self.rst_imgformat_combobox, imsave_sublock = generalConfigPanel.addCombobox(['PNG', 'JPG', 'WEBP', 'JXL'], self.tr('Result image format'))
self.rst_imgformat_combobox.activated.connect(self.on_rst_imgformat_changed)
Expand Down Expand Up @@ -553,6 +562,11 @@ def on_textstyle_indep_changed(self):
pcfg.let_textstyle_indep_flag = self.let_textstyle_indep_checker.isChecked()
self.reload_textstyle.emit(pcfg.let_textstyle_indep_flag)

def on_high_quality_warp_preview_changed(self):
pcfg.high_quality_warp_preview = self.high_quality_warp_preview_checker.isChecked()
if hasattr(self.parent(), 'canvas'):
self.parent().canvas.invalidateTextItemRenderCache()

def on_rst_imgformat_changed(self):
pcfg.imgsave_ext = '.' + self.rst_imgformat_combobox.currentText().lower()

Expand Down Expand Up @@ -596,6 +610,10 @@ def on_show_only_custom_fonts(self):
pcfg.let_show_only_custom_fonts_flag = self.let_show_only_custom_fonts.isChecked()
self.show_only_custom_font.emit(pcfg.let_show_only_custom_fonts_flag)

def on_move_text_tools_to_sidebar_changed(self):
pcfg.move_text_tools_to_sidebar = self.move_text_tools_to_sidebar_checker.isChecked()
self.text_tools_placement_changed.emit(pcfg.move_text_tools_to_sidebar)

def focusOnTranslator(self):
idx0, idx1 = self.trans_sub_block.idx0, self.trans_sub_block.idx1
self.configTable.setCurrentItem(idx0, idx1)
Expand Down Expand Up @@ -639,6 +657,7 @@ def setupConfig(self):
self.selectext_minimenu_checker.setChecked(pcfg.textselect_mini_menu)
self.let_uppercase_checker.setChecked(pcfg.let_uppercase_flag)
self.let_textstyle_indep_checker.setChecked(pcfg.let_textstyle_indep_flag)
self.high_quality_warp_preview_checker.setChecked(pcfg.high_quality_warp_preview)
self.saladict_shortcut.setKeySequence(pcfg.saladict_shortcut)
self.searchurl_combobox.setCurrentText(pcfg.search_url)
self.ocr_config_panel.restoreEmptyOCRChecker.setChecked(pcfg.restore_ocr_empty)
Expand All @@ -648,5 +667,6 @@ def setupConfig(self):
self.load_model_checker.setChecked(pcfg.module.load_model_on_demand)
self.empty_runcache_checker.setChecked(pcfg.module.empty_runcache)
self.let_show_only_custom_fonts.setChecked(pcfg.let_show_only_custom_fonts_flag)
self.move_text_tools_to_sidebar_checker.setChecked(pcfg.move_text_tools_to_sidebar)

self.blockSignals(False)
self.blockSignals(False)
5 changes: 4 additions & 1 deletion ui/mainwindow.py
Original file line number Diff line number Diff line change
Expand Up @@ -1023,6 +1023,9 @@ def on_search_result_item_clicked(self, pagename: str, blk_idx: int, is_src: boo
edit.setTextCursor(cursor)

def shortcutEscape(self):
if getattr(self.canvas, 'text_mask_edit_mode', False):
self.canvas.setTextMaskEditMode(False)
return
if self.canvas.search_widget.isVisible():
self.canvas.search_widget.hide()
elif self.canvas.editing_textblkitem is not None and self.canvas.editing_textblkitem.isEditing():
Expand Down Expand Up @@ -1833,4 +1836,4 @@ def on_hide_view_widget(self, cfg_name: str):
widget.setVisible(False)
action: QAction = d['action']
action.setChecked(False)
setattr(pcfg, cfg_name, False)
setattr(pcfg, cfg_name, False)
23 changes: 22 additions & 1 deletion ui/scenetext_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from .canvas import Canvas
from .textedit_area import TransTextEdit, SourceTextEdit, TransPairWidget, SelectTextMiniMenu, TextEditListScrollArea, QVBoxLayout, Widget
from utils.fontformat import FontFormat
from .textedit_commands import propagate_user_edit, TextEditCommand, ReshapeItemCommand, MoveBlkItemsCommand, AutoLayoutCommand, ApplyFontformatCommand, RotateItemCommand, TextItemEditCommand, TextEditCommand, PageReplaceOneCommand, PageReplaceAllCommand, MultiPasteCommand, ResetAngleCommand, SqueezeCommand
from .textedit_commands import propagate_user_edit, TextEditCommand, ReshapeItemCommand, MoveBlkItemsCommand, AutoLayoutCommand, ApplyFontformatCommand, RotateItemCommand, TextItemEditCommand, TextEditCommand, PageReplaceOneCommand, PageReplaceAllCommand, MultiPasteCommand, ResetAngleCommand, SqueezeCommand, WarpItemCommand, TextMaskStrokeCommand, ClearTextMaskCommand
from .text_panel import FontFormatPanel
from utils.config import pcfg
from utils import shared
Expand Down Expand Up @@ -346,6 +346,7 @@ def __init__(self,
self.textEditList.rearrange_blks.connect(self.on_rearrange_blks)
self.formatpanel = textpanel.formatpanel
self.formatpanel.textstyle_panel.apply_fontfmt.connect(self.onFormatTextblks)
self.formatpanel.bind_canvas(canvas)

self.imgtrans_proj = self.canvas.imgtrans_proj
self.textblk_item_list: List[TextBlkItem] = []
Expand All @@ -356,6 +357,14 @@ def __init__(self,

self.prev_blkitem: TextBlkItem = None

if hasattr(self.mainwindow, 'configPanel') and hasattr(self.mainwindow.configPanel, 'text_tools_placement_changed'):
self.mainwindow.configPanel.text_tools_placement_changed.connect(self.on_text_tools_placement_changed)
self.on_text_tools_placement_changed(pcfg.move_text_tools_to_sidebar)

def on_text_tools_placement_changed(self, enabled: bool):
if hasattr(self.formatpanel, 'set_text_tools_visible'):
self.formatpanel.set_text_tools_visible(bool(enabled))

def on_switch_textitem(self, switch_delta: int, key_event: QKeyEvent = None, current_editing_widget: Union[SourceTextEdit, TransTextEdit] = None):
n_blk = len(self.textblk_item_list)
if n_blk < 1:
Expand Down Expand Up @@ -505,12 +514,14 @@ def addTextBlkItem(self, textblk_item: TextBlkItem) -> TextBlkItem:
textblk_item.moved.connect(self.onTextBlkItemMoved)
textblk_item.reshaped.connect(self.onTextBlkItemReshaped)
textblk_item.rotated.connect(self.onTextBlkItemRotated)
textblk_item.warped.connect(self.onTextBlkItemWarped)
textblk_item.push_undo_stack.connect(self.on_push_textitem_undostack)
textblk_item.undo_signal.connect(self.on_textedit_undo)
textblk_item.redo_signal.connect(self.on_textedit_redo)
textblk_item.propagate_user_edited.connect(self.on_propagate_textitem_edit)
textblk_item.doc_size_changed.connect(self.onTextBlkItemSizeChanged)
textblk_item.pasted.connect(self.onBlkitemPaste)
textblk_item.text_mask_stroke_finished.connect(self.onTextMaskStrokeFinished)
return textblk_item

def deleteTextblkItemList(self, blkitem_list: List[TextBlkItem], p_widget_list: List[TransPairWidget]):
Expand Down Expand Up @@ -629,6 +640,16 @@ def onTextBlkItemRotated(self, new_angle: float):
if blk_item:
self.canvas.push_undo_command(RotateItemCommand(blk_item, new_angle, self.txtblkShapeControl))

def onTextBlkItemWarped(self, before: object, after: object):
item = self.sender()
if isinstance(item, TextBlkItem):
self.canvas.push_undo_command(WarpItemCommand(item, before, after, self.txtblkShapeControl))

def onTextMaskStrokeFinished(self, stroke: object):
item = self.sender()
if isinstance(item, TextBlkItem):
self.canvas.push_undo_command(TextMaskStrokeCommand(item, stroke))

def onDeleteBlkItems(self, mode: int):
selected_blks = self.canvas.selected_text_items()
if len(selected_blks) == 0 and self.txtblkShapeControl.blk_item is not None:
Expand Down
Loading