1
1
import random
2
+ import re
2
3
import sys
3
4
from collections import Counter , deque
4
5
from dataclasses import dataclass
@@ -241,21 +242,31 @@ def is_image_in_scope(self, scope: Scope | str, image_index: int,
241
242
return self .image_list_selection_model .isSelected (proxy_index )
242
243
243
244
def get_text_match_count (self , text : str , scope : Scope | str ,
244
- whole_tags_only : bool ) -> int :
245
+ whole_tags_only : bool , use_regex : bool ) -> int :
245
246
"""Get the number of instances of a text in all captions."""
246
247
match_count = 0
247
248
for image_index , image in enumerate (self .images ):
248
249
if not self .is_image_in_scope (scope , image_index , image ):
249
250
continue
250
251
if whole_tags_only :
251
- match_count += image .tags .count (text )
252
+ if use_regex :
253
+ match_count += len ([
254
+ tag for tag in image .tags
255
+ if re .fullmatch (pattern = text , string = tag )
256
+ ])
257
+ else :
258
+ match_count += image .tags .count (text )
252
259
else :
253
260
caption = self .tag_separator .join (image .tags )
254
- match_count += caption .count (text )
261
+ if use_regex :
262
+ match_count += len (re .findall (pattern = text ,
263
+ string = caption ))
264
+ else :
265
+ match_count += caption .count (text )
255
266
return match_count
256
267
257
268
def find_and_replace (self , find_text : str , replace_text : str ,
258
- scope : Scope | str ):
269
+ scope : Scope | str , use_regex : bool ):
259
270
"""
260
271
Find and replace arbitrary text in captions, within and across tag
261
272
boundaries.
@@ -269,10 +280,16 @@ def find_and_replace(self, find_text: str, replace_text: str,
269
280
if not self .is_image_in_scope (scope , image_index , image ):
270
281
continue
271
282
caption = self .tag_separator .join (image .tags )
272
- if find_text not in caption :
273
- continue
283
+ if use_regex :
284
+ if not re .search (pattern = find_text , string = caption ):
285
+ continue
286
+ caption = re .sub (pattern = find_text , repl = replace_text ,
287
+ string = caption )
288
+ else :
289
+ if find_text not in caption :
290
+ continue
291
+ caption = caption .replace (find_text , replace_text )
274
292
changed_image_indices .append (image_index )
275
- caption = caption .replace (find_text , replace_text )
276
293
image .tags = caption .split (self .tag_separator )
277
294
self .write_image_tags_to_disk (image )
278
295
if changed_image_indices :
@@ -465,39 +482,59 @@ def add_tags(self, tags: list[str], image_indices: list[QModelIndex]):
465
482
466
483
@Slot (list , str )
467
484
def rename_tags (self , old_tags : list [str ], new_tag : str ,
468
- scope : Scope | str = Scope .ALL_IMAGES ):
485
+ scope : Scope | str = Scope .ALL_IMAGES ,
486
+ use_regex : bool = False ):
469
487
self .add_to_undo_stack (
470
488
action_name = f'Rename { pluralize ("Tag" , len (old_tags ))} ' ,
471
489
should_ask_for_confirmation = True )
472
490
changed_image_indices = []
473
491
for image_index , image in enumerate (self .images ):
474
492
if not self .is_image_in_scope (scope , image_index , image ):
475
493
continue
476
- if not any (old_tag in image .tags for old_tag in old_tags ):
477
- continue
494
+ if use_regex :
495
+ pattern = old_tags [0 ]
496
+ if not any (re .fullmatch (pattern = pattern , string = image_tag )
497
+ for image_tag in image .tags ):
498
+ continue
499
+ image .tags = [new_tag if re .fullmatch (pattern = pattern ,
500
+ string = image_tag )
501
+ else image_tag for image_tag in image .tags ]
502
+ else :
503
+ if not any (old_tag in image .tags for old_tag in old_tags ):
504
+ continue
505
+ image .tags = [new_tag if image_tag in old_tags else image_tag
506
+ for image_tag in image .tags ]
478
507
changed_image_indices .append (image_index )
479
- image .tags = [new_tag if image_tag in old_tags else image_tag
480
- for image_tag in image .tags ]
481
508
self .write_image_tags_to_disk (image )
482
509
if changed_image_indices :
483
510
self .dataChanged .emit (self .index (changed_image_indices [0 ]),
484
511
self .index (changed_image_indices [- 1 ]))
485
512
486
513
@Slot (list )
487
514
def delete_tags (self , tags : list [str ],
488
- scope : Scope | str = Scope .ALL_IMAGES ):
515
+ scope : Scope | str = Scope .ALL_IMAGES ,
516
+ use_regex : bool = False ):
489
517
self .add_to_undo_stack (
490
518
action_name = f'Delete { pluralize ("Tag" , len (tags ))} ' ,
491
519
should_ask_for_confirmation = True )
492
520
changed_image_indices = []
493
521
for image_index , image in enumerate (self .images ):
494
522
if not self .is_image_in_scope (scope , image_index , image ):
495
523
continue
496
- if not any (tag in image .tags for tag in tags ):
497
- continue
524
+ if use_regex :
525
+ pattern = tags [0 ]
526
+ if not any (re .fullmatch (pattern = pattern , string = image_tag )
527
+ for image_tag in image .tags ):
528
+ continue
529
+ image .tags = [image_tag for image_tag in image .tags
530
+ if not re .fullmatch (pattern = pattern ,
531
+ string = image_tag )]
532
+ else :
533
+ if not any (tag in image .tags for tag in tags ):
534
+ continue
535
+ image .tags = [image_tag for image_tag in image .tags
536
+ if image_tag not in tags ]
498
537
changed_image_indices .append (image_index )
499
- image .tags = [image_tag for image_tag in image .tags
500
- if image_tag not in tags ]
501
538
self .write_image_tags_to_disk (image )
502
539
if changed_image_indices :
503
540
self .dataChanged .emit (self .index (changed_image_indices [0 ]),
0 commit comments