vkit.engine.font.type
1# Copyright 2022 vkit-x Administrator. All Rights Reserved. 2# 3# This project (vkit-x/vkit) is dual-licensed under commercial and SSPL licenses. 4# 5# The commercial license gives you the full rights to create and distribute software 6# on your own terms without any SSPL license obligations. For more information, 7# please see the "LICENSE_COMMERCIAL.txt" file. 8# 9# This project is also available under Server Side Public License (SSPL). 10# The SSPL licensing is ideal for use cases such as open source projects with 11# SSPL distribution, student/academic purposes, hobby projects, internal research 12# projects without external distribution, or other projects where all SSPL 13# obligations can be met. For more information, please see the "LICENSE_SSPL.txt" file. 14from typing import ( 15 cast, 16 Sequence, 17 List, 18 Optional, 19 Mapping, 20 Dict, 21 DefaultDict, 22 Iterable, 23 Set, 24 Tuple, 25 Union, 26) 27from enum import Enum, unique 28from collections import defaultdict 29 30import attrs 31import iolite as io 32import numpy as np 33import cv2 as cv 34 35from vkit.utility import ( 36 attrs_lazy_field, 37 unwrap_optional_field, 38 get_cattrs_converter_ignoring_init_equals_false, 39 dyn_structure, 40 PathType, 41) 42from vkit.element import ( 43 Shapable, 44 Point, 45 PointList, 46 Box, 47 Polygon, 48 Mask, 49 ScoreMap, 50 Image, 51) 52 53 54@attrs.define(frozen=True) 55class FontGlyphInfo: 56 tags: Sequence[str] 57 ascent_plus_pad_up_min_to_font_size_ratio: float 58 height_min_to_font_size_ratio: float 59 width_min_to_font_size_ratio: float 60 61 62@attrs.define 63class FontGlyphInfoCollection: 64 font_glyph_infos: Sequence[FontGlyphInfo] 65 66 _tag_to_font_glyph_info: Mapping[str, FontGlyphInfo] = attrs_lazy_field() 67 68 def lazy_post_init_tag_to_font_glyph_info(self): 69 if self._tag_to_font_glyph_info: 70 return self._tag_to_font_glyph_info 71 72 tag_to_font_glyph_info = {} 73 for font_glyph_info in self.font_glyph_infos: 74 assert font_glyph_info.tags 75 for tag in font_glyph_info.tags: 76 assert tag not in tag_to_font_glyph_info 77 tag_to_font_glyph_info[tag] = font_glyph_info 78 79 self._tag_to_font_glyph_info = cast(Mapping[str, FontGlyphInfo], tag_to_font_glyph_info) 80 return self._tag_to_font_glyph_info 81 82 @property 83 def tag_to_font_glyph_info(self): 84 return self.lazy_post_init_tag_to_font_glyph_info() 85 86 87@attrs.define 88class FontVariant: 89 char_to_tags: Mapping[str, Sequence[str]] 90 font_file: PathType 91 font_glyph_info_collection: FontGlyphInfoCollection 92 is_ttc: bool = False 93 ttc_font_index: Optional[int] = None 94 95 96@unique 97class FontMode(Enum): 98 # ttc file. 99 TTC = 'ttc' 100 # Grouped ttf file(s). 101 VTTC = 'vttc' 102 # Grouped otf file(s). 103 VOTC = 'votc' 104 105 106@attrs.define 107class FontMeta: 108 name: str 109 mode: FontMode 110 char_to_tags: Mapping[str, Sequence[str]] 111 font_files: Sequence[str] 112 font_glyph_info_collection: FontGlyphInfoCollection 113 # NOTE: ttc_font_index_max is inclusive. 114 ttc_font_index_max: Optional[int] = None 115 116 _chars: Sequence[str] = attrs_lazy_field() 117 118 def lazy_post_init_chars(self): 119 if self._chars: 120 return self._chars 121 122 self._chars = cast(Sequence[str], sorted(self.char_to_tags)) 123 return self._chars 124 125 @property 126 def chars(self): 127 return self.lazy_post_init_chars() 128 129 def __repr__(self): 130 return ( 131 'FontMeta(' 132 f'name="{self.name}", ' 133 f'mode={self.mode}, ' 134 f'num_chars={len(self.char_to_tags)}), ' 135 f'font_files={self.font_files}, ' 136 f'ttc_font_index_max={self.ttc_font_index_max})' 137 ) 138 139 @classmethod 140 def from_file( 141 cls, 142 path: PathType, 143 font_file_prefix: Optional[PathType] = None, 144 ): 145 font = dyn_structure(path, FontMeta, force_path_type=True) 146 147 if font_file_prefix: 148 font_file_prefix_fd = io.folder(font_file_prefix, exists=True) 149 font_files = [] 150 for font_file in font.font_files: 151 font_file = str(io.file(font_file_prefix_fd / io.file(font_file), exists=True)) 152 font_files.append(font_file) 153 font = attrs.evolve(font, font_files=font_files) 154 155 return font 156 157 def to_file( 158 self, 159 path: PathType, 160 font_file_prefix: Optional[PathType] = None, 161 ): 162 font = self 163 164 if font_file_prefix: 165 font_file_prefix_fd = io.folder(font_file_prefix) 166 font_files = [] 167 for font_file in self.font_files: 168 font_files.append(str(io.file(font_file).relative_to(font_file_prefix_fd))) 169 font = attrs.evolve(self, font_files=font_files) 170 171 converter = get_cattrs_converter_ignoring_init_equals_false() 172 io.write_json(path, converter.unstructure(font), indent=2, ensure_ascii=False) 173 174 @property 175 def num_font_variants(self): 176 if self.mode in (FontMode.VOTC, FontMode.VTTC): 177 return len(self.font_files) 178 179 elif self.mode == FontMode.TTC: 180 assert self.ttc_font_index_max is not None 181 return self.ttc_font_index_max + 1 182 183 else: 184 raise NotImplementedError() 185 186 def get_font_variant(self, variant_idx: int): 187 if self.mode in (FontMode.VOTC, FontMode.VTTC): 188 assert variant_idx < len(self.font_files) 189 return FontVariant( 190 char_to_tags=self.char_to_tags, 191 font_file=io.file(self.font_files[variant_idx]), 192 font_glyph_info_collection=self.font_glyph_info_collection, 193 ) 194 195 elif self.mode == FontMode.TTC: 196 assert self.ttc_font_index_max is not None 197 assert variant_idx <= self.ttc_font_index_max 198 return FontVariant( 199 char_to_tags=self.char_to_tags, 200 font_file=io.file(self.font_files[0]), 201 font_glyph_info_collection=self.font_glyph_info_collection, 202 is_ttc=True, 203 ttc_font_index=variant_idx, 204 ) 205 206 else: 207 raise NotImplementedError() 208 209 210class FontCollectionFolderTree: 211 FONT = 'font' 212 FONT_META = 'font_meta' 213 214 215@attrs.define 216class FontCollection: 217 font_metas: Sequence[FontMeta] 218 219 _name_to_font_meta: Optional[Mapping[str, FontMeta]] = attrs_lazy_field() 220 _char_to_font_meta_names: Optional[Mapping[str, Set[str]]] = attrs_lazy_field() 221 222 def lazy_post_init(self): 223 initialized = (self._name_to_font_meta is not None) 224 if initialized: 225 return 226 227 name_to_font_meta: Dict[str, FontMeta] = {} 228 char_to_font_meta_names: DefaultDict[str, Set[str]] = defaultdict(set) 229 for font_meta in self.font_metas: 230 assert font_meta.name not in name_to_font_meta 231 name_to_font_meta[font_meta.name] = font_meta 232 for char in font_meta.chars: 233 char_to_font_meta_names[char].add(font_meta.name) 234 self._name_to_font_meta = name_to_font_meta 235 self._char_to_font_meta_names = dict(char_to_font_meta_names) 236 237 @property 238 def name_to_font_meta(self): 239 self.lazy_post_init() 240 return unwrap_optional_field(self._name_to_font_meta) 241 242 @property 243 def char_to_font_meta_names(self): 244 self.lazy_post_init() 245 return unwrap_optional_field(self._char_to_font_meta_names) 246 247 def filter_font_metas(self, chars: Iterable[str]): 248 font_meta_names = set.intersection( 249 *[self.char_to_font_meta_names[char] for char in chars if not char.isspace()] 250 ) 251 font_meta_names = sorted(font_meta_names) 252 return [self.name_to_font_meta[font_meta_name] for font_meta_name in font_meta_names] 253 254 @classmethod 255 def from_folder(cls, folder: PathType): 256 in_fd = io.folder(folder, expandvars=True, exists=True) 257 font_fd = io.folder(in_fd / FontCollectionFolderTree.FONT, exists=True) 258 font_meta_fd = io.folder(in_fd / FontCollectionFolderTree.FONT_META, exists=True) 259 260 font_metas: List[FontMeta] = [] 261 for font_meta_json in font_meta_fd.glob('*.json'): 262 font_metas.append(FontMeta.from_file(font_meta_json, font_fd)) 263 264 return cls(font_metas=font_metas) 265 266 267@attrs.define 268class FontEngineRunConfigStyle: 269 # Font size. 270 font_size_ratio: float = 1.0 271 font_size_min: int = 12 272 font_size_max: int = 96 273 274 # Space between chars. 275 prob_set_char_space_min: float = 0.5 276 char_space_min: float = 0.0 277 char_space_max: float = 0.2 278 char_space_mean: float = 0.1 279 char_space_std: float = 0.03 280 281 # Space between words. 282 word_space_min: float = 0.3 283 word_space_max: float = 1.0 284 word_space_mean: float = 0.6 285 word_space_std: float = 0.1 286 287 # Effect. 288 glyph_color: Tuple[int, int, int] = (0, 0, 0) 289 # https://en.wikipedia.org/wiki/Gamma_correction 290 glyph_color_gamma: float = 1.0 291 292 # Implementation related options. 293 freetype_force_autohint: bool = False 294 295 296@unique 297class FontEngineRunConfigGlyphSequence(Enum): 298 HORI_DEFAULT = 'hori_default' 299 VERT_DEFAULT = 'vert_default' 300 301 302@attrs.define 303class FontEngineRunConfig: 304 height: int 305 width: int 306 chars: Sequence[str] 307 font_variant: FontVariant 308 309 # Sequence mode. 310 glyph_sequence: FontEngineRunConfigGlyphSequence = \ 311 FontEngineRunConfigGlyphSequence.HORI_DEFAULT 312 313 style: FontEngineRunConfigStyle = attrs.field(factory=FontEngineRunConfigStyle) 314 315 # For debugging. 316 return_font_variant: bool = False 317 318 319@attrs.define(frozen=True) 320class CharBox(Shapable): 321 char: str 322 box: Box 323 324 def __attrs_post_init__(self): 325 assert len(self.char) == 1 and not self.char.isspace() 326 327 ############ 328 # Property # 329 ############ 330 @property 331 def up(self): 332 return self.box.up 333 334 @property 335 def down(self): 336 return self.box.down 337 338 @property 339 def left(self): 340 return self.box.left 341 342 @property 343 def right(self): 344 return self.box.right 345 346 @property 347 def height(self): 348 return self.box.height 349 350 @property 351 def width(self): 352 return self.box.width 353 354 ############ 355 # Operator # 356 ############ 357 def to_conducted_resized_char_box( 358 self, 359 shapable_or_shape: Union[Shapable, Tuple[int, int]], 360 resized_height: Optional[int] = None, 361 resized_width: Optional[int] = None, 362 ): 363 return attrs.evolve( 364 self, 365 box=self.box.to_conducted_resized_box( 366 shapable_or_shape=shapable_or_shape, 367 resized_height=resized_height, 368 resized_width=resized_width, 369 ), 370 ) 371 372 def to_resized_char_box( 373 self, 374 resized_height: Optional[int] = None, 375 resized_width: Optional[int] = None, 376 ): 377 return attrs.evolve( 378 self, 379 box=self.box.to_resized_box( 380 resized_height=resized_height, 381 resized_width=resized_width, 382 ), 383 ) 384 385 def to_shifted_char_box(self, offset_y: int = 0, offset_x: int = 0): 386 return attrs.evolve( 387 self, 388 box=self.box.to_shifted_box(offset_y=offset_y, offset_x=offset_x), 389 ) 390 391 392@attrs.define 393class CharGlyph: 394 char: str 395 image: Image 396 score_map: Optional[ScoreMap] 397 # Load from font face. See build_char_glyph. 398 ascent: int 399 pad_up: int 400 pad_down: int 401 pad_left: int 402 pad_right: int 403 # For rendering text line and generating char-level polygon, based on the reference char. 404 ref_ascent_plus_pad_up: int 405 ref_char_height: int 406 ref_char_width: int 407 408 def __attrs_post_init__(self): 409 # NOTE: ascent could be negative for char like '_'. 410 assert self.pad_up >= 0 411 assert self.pad_down >= 0 412 assert self.pad_left >= 0 413 assert self.pad_right >= 0 414 415 @property 416 def height(self): 417 return self.image.height 418 419 @property 420 def width(self): 421 return self.image.width 422 423 def get_glyph_mask( 424 self, 425 box: Optional[Box] = None, 426 enable_resize: bool = False, 427 cv_resize_interpolation: int = cv.INTER_CUBIC, 428 ): 429 if self.image.mat.ndim == 2: 430 # Default or monochrome. 431 np_mask = (self.image.mat > 0) 432 433 elif self.image.mat.ndim == 3: 434 # LCD. 435 np_mask = np.any((self.image.mat > 0), axis=2) 436 437 else: 438 raise NotImplementedError() 439 440 mask = Mask(mat=np_mask.astype(np.uint8)) 441 if box: 442 if mask.shape != box.shape: 443 assert enable_resize 444 mask = mask.to_resized_mask( 445 resized_height=box.height, 446 resized_width=box.width, 447 cv_resize_interpolation=cv_resize_interpolation, 448 ) 449 mask = mask.to_box_attached(box) 450 451 return mask 452 453 454@attrs.define 455class TextLine: 456 image: Image 457 mask: Mask 458 score_map: Optional[ScoreMap] 459 char_boxes: Sequence[CharBox] 460 # NOTE: char_glyphs might not have the same shapes as char_boxes. 461 char_glyphs: Sequence[CharGlyph] 462 cv_resize_interpolation: int 463 style: FontEngineRunConfigStyle 464 font_size: int 465 text: str 466 is_hori: bool 467 468 # Shifted text line is bound to a page. 469 shifted: bool = False 470 471 # For debugging. 472 font_variant: Optional[FontVariant] = None 473 474 @property 475 def box(self): 476 assert self.mask.box 477 return self.mask.box 478 479 @property 480 def glyph_color(self): 481 return self.style.glyph_color 482 483 def to_shifted_text_line(self, offset_y: int = 0, offset_x: int = 0): 484 self.shifted = True 485 486 shifted_image = self.image.to_shifted_image(offset_y=offset_y, offset_x=offset_x) 487 shifted_mask = self.mask.to_shifted_mask(offset_y=offset_y, offset_x=offset_x) 488 489 shifted_score_map = None 490 if self.score_map: 491 shifted_score_map = self.score_map.to_shifted_score_map( 492 offset_y=offset_y, 493 offset_x=offset_x, 494 ) 495 496 shifted_char_boxes = [ 497 char_box.to_shifted_char_box( 498 offset_y=offset_y, 499 offset_x=offset_x, 500 ) for char_box in self.char_boxes 501 ] 502 503 return attrs.evolve( 504 self, 505 image=shifted_image, 506 mask=shifted_mask, 507 score_map=shifted_score_map, 508 char_boxes=shifted_char_boxes, 509 ) 510 511 def split(self): 512 texts = self.text.split() 513 if len(texts) == 1: 514 # No need to split. 515 return [self] 516 assert len(texts) > 1 517 518 # Seperated by space(s). 519 text_lines: List[TextLine] = [] 520 521 begin = 0 522 for text in texts: 523 end = begin + len(text) - 1 524 char_boxes = self.char_boxes[begin:end + 1] 525 char_glyphs = self.char_glyphs[begin:end + 1] 526 527 if self.is_hori: 528 left = char_boxes[0].left 529 right = char_boxes[-1].right 530 up = min(char_box.up for char_box in char_boxes) 531 down = max(char_box.down for char_box in char_boxes) 532 else: 533 up = char_boxes[0].up 534 down = char_boxes[-1].down 535 left = min(char_box.left for char_box in char_boxes) 536 right = max(char_box.right for char_box in char_boxes) 537 box = Box(up=up, down=down, left=left, right=right) 538 539 image = box.extract_image(self.image) 540 mask = box.extract_mask(self.mask) 541 score_map = None 542 if self.score_map: 543 score_map = box.extract_score_map(self.score_map) 544 545 text_lines.append( 546 attrs.evolve( 547 self, 548 image=image, 549 mask=mask, 550 score_map=score_map, 551 char_boxes=char_boxes, 552 char_glyphs=char_glyphs, 553 text=text, 554 ) 555 ) 556 begin = end + 1 557 558 return text_lines 559 560 def to_polygon(self): 561 if self.is_hori: 562 xs = [self.box.left] 563 for char_box in self.char_boxes: 564 if xs[-1] < char_box.left: 565 xs.append(char_box.left) 566 if char_box.left < char_box.right: 567 xs.append(char_box.right) 568 if xs[-1] < self.box.right: 569 xs.append(self.box.right) 570 571 points = PointList() 572 573 for x in xs: 574 points.append(Point.create(y=self.box.up, x=x)) 575 576 y_mid = (self.box.up + self.box.down) // 2 577 if self.box.up < y_mid < self.box.down: 578 points.append(Point.create(y=y_mid, x=xs[-1])) 579 580 for x in reversed(xs): 581 points.append(Point.create(y=self.box.down, x=x)) 582 583 if self.box.up < y_mid < self.box.down: 584 points.append(Point.create(y=y_mid, x=xs[0])) 585 586 return Polygon.create(points=points) 587 588 else: 589 ys = [self.box.up] 590 for char_box in self.char_boxes: 591 if ys[-1] < char_box.up: 592 ys.append(char_box.up) 593 if char_box.up < char_box.down: 594 ys.append(char_box.down) 595 if ys[-1] < self.box.down: 596 ys.append(self.box.down) 597 598 points = PointList() 599 600 for y in ys: 601 points.append(Point.create(y=y, x=self.box.right)) 602 603 x_mid = (self.box.left + self.box.right) // 2 604 if self.box.left < x_mid < self.box.right: 605 points.append(Point.create(y=ys[-1], x=x_mid)) 606 607 for y in reversed(ys): 608 points.append(Point.create(y=y, x=self.box.left)) 609 610 if self.box.left < x_mid < self.box.right: 611 points.append(Point.create(y=ys[0], x=x_mid)) 612 613 return Polygon.create(points=points) 614 615 @classmethod 616 def build_char_polygon( 617 cls, 618 up: float, 619 down: float, 620 left: float, 621 right: float, 622 ): 623 return Polygon.from_xy_pairs([ 624 (left, up), 625 (right, up), 626 (right, down), 627 (left, down), 628 ]) 629 630 def to_char_polygons( 631 self, 632 page_height: int, 633 page_width: int, 634 ref_char_height_ratio: float = 1.0, 635 ref_char_width_ratio: float = 1.0, 636 ): 637 assert len(self.char_boxes) == len(self.char_glyphs) 638 639 if self.is_hori: 640 polygons: List[Polygon] = [] 641 for char_box, char_glyph in zip(self.char_boxes, self.char_glyphs): 642 ref_char_height = char_glyph.ref_char_height * ref_char_height_ratio 643 ref_char_width = char_glyph.ref_char_width * ref_char_width_ratio 644 box = char_box.box 645 646 up = box.up 647 down = box.down 648 if box.height < ref_char_height: 649 inc = ref_char_height - box.height 650 half_inc = inc / 2 651 up = max(0, up - half_inc) 652 down = min(page_height - 1, down + half_inc) 653 654 left = box.left 655 right = box.right 656 if box.width < ref_char_width: 657 inc = ref_char_width - box.width 658 half_inc = inc / 2 659 left = max(0, left - half_inc) 660 right = min(page_width - 1, right + half_inc) 661 662 polygons.append(self.build_char_polygon( 663 up=up, 664 down=down, 665 left=left, 666 right=right, 667 )) 668 return polygons 669 670 else: 671 polygons: List[Polygon] = [] 672 for char_box, char_glyph in zip(self.char_boxes, self.char_glyphs): 673 ref_char_height = char_glyph.ref_char_height * ref_char_height_ratio 674 ref_char_width = char_glyph.ref_char_width * ref_char_width_ratio 675 box = char_box.box 676 677 left = box.left 678 right = box.right 679 if box.width < ref_char_height: 680 inc = ref_char_height - box.width 681 half_inc = inc / 2 682 left = max(0, left - half_inc) 683 right = min(page_width - 1, right + half_inc) 684 685 up = box.up 686 down = box.down 687 if box.height < ref_char_width: 688 inc = ref_char_width - box.height 689 half_inc = inc / 2 690 up = max(self.box.up, up - half_inc) 691 down = min(page_height - 1, down + half_inc) 692 693 polygons.append(self.build_char_polygon( 694 up=up, 695 down=down, 696 left=left, 697 right=right, 698 )) 699 return polygons 700 701 def get_height_points(self, num_points: int, is_up: bool): 702 if self.is_hori: 703 step = max(1, self.box.width // num_points) 704 xs = list(range(0, self.box.right + 1, step)) 705 if len(xs) >= num_points: 706 xs = xs[:num_points - 1] 707 xs.append(self.box.right) 708 709 points = PointList() 710 for x in xs: 711 if is_up: 712 y = self.box.up 713 else: 714 y = self.box.down 715 points.append(Point.create(y=y, x=x)) 716 return points 717 718 else: 719 step = max(1, self.box.height // num_points) 720 ys = list(range(self.box.up, self.box.down + 1, step)) 721 if len(ys) >= num_points: 722 ys = ys[:num_points - 1] 723 ys.append(self.box.down) 724 725 points = PointList() 726 for y in ys: 727 if is_up: 728 x = self.box.right 729 else: 730 x = self.box.left 731 points.append(Point.create(y=y, x=x)) 732 return points 733 734 def get_char_level_height_points(self, is_up: bool): 735 if self.is_hori: 736 points = PointList() 737 for char_box in self.char_boxes: 738 x = (char_box.left + char_box.right) / 2 739 if is_up: 740 y = self.box.up 741 else: 742 y = self.box.down 743 points.append(Point.create(y=y, x=x)) 744 return points 745 746 else: 747 points = PointList() 748 for char_box in self.char_boxes: 749 y = (char_box.up + char_box.down) / 2 750 if is_up: 751 x = self.box.right 752 else: 753 x = self.box.left 754 points.append(Point.create(y=y, x=x)) 755 return points
class
FontGlyphInfo:
56class FontGlyphInfo: 57 tags: Sequence[str] 58 ascent_plus_pad_up_min_to_font_size_ratio: float 59 height_min_to_font_size_ratio: float 60 width_min_to_font_size_ratio: float
FontGlyphInfo( tags: Sequence[str], ascent_plus_pad_up_min_to_font_size_ratio: float, height_min_to_font_size_ratio: float, width_min_to_font_size_ratio: float)
2def __init__(self, tags, ascent_plus_pad_up_min_to_font_size_ratio, height_min_to_font_size_ratio, width_min_to_font_size_ratio): 3 _setattr = _cached_setattr_get(self) 4 _setattr('tags', tags) 5 _setattr('ascent_plus_pad_up_min_to_font_size_ratio', ascent_plus_pad_up_min_to_font_size_ratio) 6 _setattr('height_min_to_font_size_ratio', height_min_to_font_size_ratio) 7 _setattr('width_min_to_font_size_ratio', width_min_to_font_size_ratio)
Method generated by attrs for class FontGlyphInfo.
class
FontGlyphInfoCollection:
64class FontGlyphInfoCollection: 65 font_glyph_infos: Sequence[FontGlyphInfo] 66 67 _tag_to_font_glyph_info: Mapping[str, FontGlyphInfo] = attrs_lazy_field() 68 69 def lazy_post_init_tag_to_font_glyph_info(self): 70 if self._tag_to_font_glyph_info: 71 return self._tag_to_font_glyph_info 72 73 tag_to_font_glyph_info = {} 74 for font_glyph_info in self.font_glyph_infos: 75 assert font_glyph_info.tags 76 for tag in font_glyph_info.tags: 77 assert tag not in tag_to_font_glyph_info 78 tag_to_font_glyph_info[tag] = font_glyph_info 79 80 self._tag_to_font_glyph_info = cast(Mapping[str, FontGlyphInfo], tag_to_font_glyph_info) 81 return self._tag_to_font_glyph_info 82 83 @property 84 def tag_to_font_glyph_info(self): 85 return self.lazy_post_init_tag_to_font_glyph_info()
FontGlyphInfoCollection(font_glyph_infos: Sequence[vkit.engine.font.type.FontGlyphInfo])
2def __init__(self, font_glyph_infos): 3 self.font_glyph_infos = font_glyph_infos 4 self._tag_to_font_glyph_info = attr_dict['_tag_to_font_glyph_info'].default
Method generated by attrs for class FontGlyphInfoCollection.
def
lazy_post_init_tag_to_font_glyph_info(self):
69 def lazy_post_init_tag_to_font_glyph_info(self): 70 if self._tag_to_font_glyph_info: 71 return self._tag_to_font_glyph_info 72 73 tag_to_font_glyph_info = {} 74 for font_glyph_info in self.font_glyph_infos: 75 assert font_glyph_info.tags 76 for tag in font_glyph_info.tags: 77 assert tag not in tag_to_font_glyph_info 78 tag_to_font_glyph_info[tag] = font_glyph_info 79 80 self._tag_to_font_glyph_info = cast(Mapping[str, FontGlyphInfo], tag_to_font_glyph_info) 81 return self._tag_to_font_glyph_info
class
FontVariant:
89class FontVariant: 90 char_to_tags: Mapping[str, Sequence[str]] 91 font_file: PathType 92 font_glyph_info_collection: FontGlyphInfoCollection 93 is_ttc: bool = False 94 ttc_font_index: Optional[int] = None
FontVariant( char_to_tags: Mapping[str, Sequence[str]], font_file: Union[str, os.PathLike], font_glyph_info_collection: vkit.engine.font.type.FontGlyphInfoCollection, is_ttc: bool = False, ttc_font_index: Union[int, NoneType] = None)
2def __init__(self, char_to_tags, font_file, font_glyph_info_collection, is_ttc=attr_dict['is_ttc'].default, ttc_font_index=attr_dict['ttc_font_index'].default): 3 self.char_to_tags = char_to_tags 4 self.font_file = font_file 5 self.font_glyph_info_collection = font_glyph_info_collection 6 self.is_ttc = is_ttc 7 self.ttc_font_index = ttc_font_index
Method generated by attrs for class FontVariant.
class
FontMode(enum.Enum):
98class FontMode(Enum): 99 # ttc file. 100 TTC = 'ttc' 101 # Grouped ttf file(s). 102 VTTC = 'vttc' 103 # Grouped otf file(s). 104 VOTC = 'votc'
An enumeration.
TTC =
<FontMode.TTC: 'ttc'>
VTTC =
<FontMode.VTTC: 'vttc'>
VOTC =
<FontMode.VOTC: 'votc'>
Inherited Members
- enum.Enum
- name
- value
class
FontMeta:
108class FontMeta: 109 name: str 110 mode: FontMode 111 char_to_tags: Mapping[str, Sequence[str]] 112 font_files: Sequence[str] 113 font_glyph_info_collection: FontGlyphInfoCollection 114 # NOTE: ttc_font_index_max is inclusive. 115 ttc_font_index_max: Optional[int] = None 116 117 _chars: Sequence[str] = attrs_lazy_field() 118 119 def lazy_post_init_chars(self): 120 if self._chars: 121 return self._chars 122 123 self._chars = cast(Sequence[str], sorted(self.char_to_tags)) 124 return self._chars 125 126 @property 127 def chars(self): 128 return self.lazy_post_init_chars() 129 130 def __repr__(self): 131 return ( 132 'FontMeta(' 133 f'name="{self.name}", ' 134 f'mode={self.mode}, ' 135 f'num_chars={len(self.char_to_tags)}), ' 136 f'font_files={self.font_files}, ' 137 f'ttc_font_index_max={self.ttc_font_index_max})' 138 ) 139 140 @classmethod 141 def from_file( 142 cls, 143 path: PathType, 144 font_file_prefix: Optional[PathType] = None, 145 ): 146 font = dyn_structure(path, FontMeta, force_path_type=True) 147 148 if font_file_prefix: 149 font_file_prefix_fd = io.folder(font_file_prefix, exists=True) 150 font_files = [] 151 for font_file in font.font_files: 152 font_file = str(io.file(font_file_prefix_fd / io.file(font_file), exists=True)) 153 font_files.append(font_file) 154 font = attrs.evolve(font, font_files=font_files) 155 156 return font 157 158 def to_file( 159 self, 160 path: PathType, 161 font_file_prefix: Optional[PathType] = None, 162 ): 163 font = self 164 165 if font_file_prefix: 166 font_file_prefix_fd = io.folder(font_file_prefix) 167 font_files = [] 168 for font_file in self.font_files: 169 font_files.append(str(io.file(font_file).relative_to(font_file_prefix_fd))) 170 font = attrs.evolve(self, font_files=font_files) 171 172 converter = get_cattrs_converter_ignoring_init_equals_false() 173 io.write_json(path, converter.unstructure(font), indent=2, ensure_ascii=False) 174 175 @property 176 def num_font_variants(self): 177 if self.mode in (FontMode.VOTC, FontMode.VTTC): 178 return len(self.font_files) 179 180 elif self.mode == FontMode.TTC: 181 assert self.ttc_font_index_max is not None 182 return self.ttc_font_index_max + 1 183 184 else: 185 raise NotImplementedError() 186 187 def get_font_variant(self, variant_idx: int): 188 if self.mode in (FontMode.VOTC, FontMode.VTTC): 189 assert variant_idx < len(self.font_files) 190 return FontVariant( 191 char_to_tags=self.char_to_tags, 192 font_file=io.file(self.font_files[variant_idx]), 193 font_glyph_info_collection=self.font_glyph_info_collection, 194 ) 195 196 elif self.mode == FontMode.TTC: 197 assert self.ttc_font_index_max is not None 198 assert variant_idx <= self.ttc_font_index_max 199 return FontVariant( 200 char_to_tags=self.char_to_tags, 201 font_file=io.file(self.font_files[0]), 202 font_glyph_info_collection=self.font_glyph_info_collection, 203 is_ttc=True, 204 ttc_font_index=variant_idx, 205 ) 206 207 else: 208 raise NotImplementedError()
FontMeta( name: str, mode: vkit.engine.font.type.FontMode, char_to_tags: Mapping[str, Sequence[str]], font_files: Sequence[str], font_glyph_info_collection: vkit.engine.font.type.FontGlyphInfoCollection, ttc_font_index_max: Union[int, NoneType] = None)
2def __init__(self, name, mode, char_to_tags, font_files, font_glyph_info_collection, ttc_font_index_max=attr_dict['ttc_font_index_max'].default): 3 self.name = name 4 self.mode = mode 5 self.char_to_tags = char_to_tags 6 self.font_files = font_files 7 self.font_glyph_info_collection = font_glyph_info_collection 8 self.ttc_font_index_max = ttc_font_index_max 9 self._chars = attr_dict['_chars'].default
Method generated by attrs for class FontMeta.
@classmethod
def
from_file( cls, path: Union[str, os.PathLike], font_file_prefix: Union[str, os.PathLike, NoneType] = None):
140 @classmethod 141 def from_file( 142 cls, 143 path: PathType, 144 font_file_prefix: Optional[PathType] = None, 145 ): 146 font = dyn_structure(path, FontMeta, force_path_type=True) 147 148 if font_file_prefix: 149 font_file_prefix_fd = io.folder(font_file_prefix, exists=True) 150 font_files = [] 151 for font_file in font.font_files: 152 font_file = str(io.file(font_file_prefix_fd / io.file(font_file), exists=True)) 153 font_files.append(font_file) 154 font = attrs.evolve(font, font_files=font_files) 155 156 return font
def
to_file( self, path: Union[str, os.PathLike], font_file_prefix: Union[str, os.PathLike, NoneType] = None):
158 def to_file( 159 self, 160 path: PathType, 161 font_file_prefix: Optional[PathType] = None, 162 ): 163 font = self 164 165 if font_file_prefix: 166 font_file_prefix_fd = io.folder(font_file_prefix) 167 font_files = [] 168 for font_file in self.font_files: 169 font_files.append(str(io.file(font_file).relative_to(font_file_prefix_fd))) 170 font = attrs.evolve(self, font_files=font_files) 171 172 converter = get_cattrs_converter_ignoring_init_equals_false() 173 io.write_json(path, converter.unstructure(font), indent=2, ensure_ascii=False)
def
get_font_variant(self, variant_idx: int):
187 def get_font_variant(self, variant_idx: int): 188 if self.mode in (FontMode.VOTC, FontMode.VTTC): 189 assert variant_idx < len(self.font_files) 190 return FontVariant( 191 char_to_tags=self.char_to_tags, 192 font_file=io.file(self.font_files[variant_idx]), 193 font_glyph_info_collection=self.font_glyph_info_collection, 194 ) 195 196 elif self.mode == FontMode.TTC: 197 assert self.ttc_font_index_max is not None 198 assert variant_idx <= self.ttc_font_index_max 199 return FontVariant( 200 char_to_tags=self.char_to_tags, 201 font_file=io.file(self.font_files[0]), 202 font_glyph_info_collection=self.font_glyph_info_collection, 203 is_ttc=True, 204 ttc_font_index=variant_idx, 205 ) 206 207 else: 208 raise NotImplementedError()
class
FontCollectionFolderTree:
class
FontCollection:
217class FontCollection: 218 font_metas: Sequence[FontMeta] 219 220 _name_to_font_meta: Optional[Mapping[str, FontMeta]] = attrs_lazy_field() 221 _char_to_font_meta_names: Optional[Mapping[str, Set[str]]] = attrs_lazy_field() 222 223 def lazy_post_init(self): 224 initialized = (self._name_to_font_meta is not None) 225 if initialized: 226 return 227 228 name_to_font_meta: Dict[str, FontMeta] = {} 229 char_to_font_meta_names: DefaultDict[str, Set[str]] = defaultdict(set) 230 for font_meta in self.font_metas: 231 assert font_meta.name not in name_to_font_meta 232 name_to_font_meta[font_meta.name] = font_meta 233 for char in font_meta.chars: 234 char_to_font_meta_names[char].add(font_meta.name) 235 self._name_to_font_meta = name_to_font_meta 236 self._char_to_font_meta_names = dict(char_to_font_meta_names) 237 238 @property 239 def name_to_font_meta(self): 240 self.lazy_post_init() 241 return unwrap_optional_field(self._name_to_font_meta) 242 243 @property 244 def char_to_font_meta_names(self): 245 self.lazy_post_init() 246 return unwrap_optional_field(self._char_to_font_meta_names) 247 248 def filter_font_metas(self, chars: Iterable[str]): 249 font_meta_names = set.intersection( 250 *[self.char_to_font_meta_names[char] for char in chars if not char.isspace()] 251 ) 252 font_meta_names = sorted(font_meta_names) 253 return [self.name_to_font_meta[font_meta_name] for font_meta_name in font_meta_names] 254 255 @classmethod 256 def from_folder(cls, folder: PathType): 257 in_fd = io.folder(folder, expandvars=True, exists=True) 258 font_fd = io.folder(in_fd / FontCollectionFolderTree.FONT, exists=True) 259 font_meta_fd = io.folder(in_fd / FontCollectionFolderTree.FONT_META, exists=True) 260 261 font_metas: List[FontMeta] = [] 262 for font_meta_json in font_meta_fd.glob('*.json'): 263 font_metas.append(FontMeta.from_file(font_meta_json, font_fd)) 264 265 return cls(font_metas=font_metas)
FontCollection(font_metas: Sequence[vkit.engine.font.type.FontMeta])
2def __init__(self, font_metas): 3 self.font_metas = font_metas 4 self._name_to_font_meta = attr_dict['_name_to_font_meta'].default 5 self._char_to_font_meta_names = attr_dict['_char_to_font_meta_names'].default
Method generated by attrs for class FontCollection.
def
lazy_post_init(self):
223 def lazy_post_init(self): 224 initialized = (self._name_to_font_meta is not None) 225 if initialized: 226 return 227 228 name_to_font_meta: Dict[str, FontMeta] = {} 229 char_to_font_meta_names: DefaultDict[str, Set[str]] = defaultdict(set) 230 for font_meta in self.font_metas: 231 assert font_meta.name not in name_to_font_meta 232 name_to_font_meta[font_meta.name] = font_meta 233 for char in font_meta.chars: 234 char_to_font_meta_names[char].add(font_meta.name) 235 self._name_to_font_meta = name_to_font_meta 236 self._char_to_font_meta_names = dict(char_to_font_meta_names)
def
filter_font_metas(self, chars: Iterable[str]):
248 def filter_font_metas(self, chars: Iterable[str]): 249 font_meta_names = set.intersection( 250 *[self.char_to_font_meta_names[char] for char in chars if not char.isspace()] 251 ) 252 font_meta_names = sorted(font_meta_names) 253 return [self.name_to_font_meta[font_meta_name] for font_meta_name in font_meta_names]
@classmethod
def
from_folder(cls, folder: Union[str, os.PathLike]):
255 @classmethod 256 def from_folder(cls, folder: PathType): 257 in_fd = io.folder(folder, expandvars=True, exists=True) 258 font_fd = io.folder(in_fd / FontCollectionFolderTree.FONT, exists=True) 259 font_meta_fd = io.folder(in_fd / FontCollectionFolderTree.FONT_META, exists=True) 260 261 font_metas: List[FontMeta] = [] 262 for font_meta_json in font_meta_fd.glob('*.json'): 263 font_metas.append(FontMeta.from_file(font_meta_json, font_fd)) 264 265 return cls(font_metas=font_metas)
class
FontEngineRunConfigStyle:
269class FontEngineRunConfigStyle: 270 # Font size. 271 font_size_ratio: float = 1.0 272 font_size_min: int = 12 273 font_size_max: int = 96 274 275 # Space between chars. 276 prob_set_char_space_min: float = 0.5 277 char_space_min: float = 0.0 278 char_space_max: float = 0.2 279 char_space_mean: float = 0.1 280 char_space_std: float = 0.03 281 282 # Space between words. 283 word_space_min: float = 0.3 284 word_space_max: float = 1.0 285 word_space_mean: float = 0.6 286 word_space_std: float = 0.1 287 288 # Effect. 289 glyph_color: Tuple[int, int, int] = (0, 0, 0) 290 # https://en.wikipedia.org/wiki/Gamma_correction 291 glyph_color_gamma: float = 1.0 292 293 # Implementation related options. 294 freetype_force_autohint: bool = False
FontEngineRunConfigStyle( font_size_ratio: float = 1.0, font_size_min: int = 12, font_size_max: int = 96, prob_set_char_space_min: float = 0.5, char_space_min: float = 0.0, char_space_max: float = 0.2, char_space_mean: float = 0.1, char_space_std: float = 0.03, word_space_min: float = 0.3, word_space_max: float = 1.0, word_space_mean: float = 0.6, word_space_std: float = 0.1, glyph_color: Tuple[int, int, int] = (0, 0, 0), glyph_color_gamma: float = 1.0, freetype_force_autohint: bool = False)
2def __init__(self, font_size_ratio=attr_dict['font_size_ratio'].default, font_size_min=attr_dict['font_size_min'].default, font_size_max=attr_dict['font_size_max'].default, prob_set_char_space_min=attr_dict['prob_set_char_space_min'].default, char_space_min=attr_dict['char_space_min'].default, char_space_max=attr_dict['char_space_max'].default, char_space_mean=attr_dict['char_space_mean'].default, char_space_std=attr_dict['char_space_std'].default, word_space_min=attr_dict['word_space_min'].default, word_space_max=attr_dict['word_space_max'].default, word_space_mean=attr_dict['word_space_mean'].default, word_space_std=attr_dict['word_space_std'].default, glyph_color=attr_dict['glyph_color'].default, glyph_color_gamma=attr_dict['glyph_color_gamma'].default, freetype_force_autohint=attr_dict['freetype_force_autohint'].default): 3 self.font_size_ratio = font_size_ratio 4 self.font_size_min = font_size_min 5 self.font_size_max = font_size_max 6 self.prob_set_char_space_min = prob_set_char_space_min 7 self.char_space_min = char_space_min 8 self.char_space_max = char_space_max 9 self.char_space_mean = char_space_mean 10 self.char_space_std = char_space_std 11 self.word_space_min = word_space_min 12 self.word_space_max = word_space_max 13 self.word_space_mean = word_space_mean 14 self.word_space_std = word_space_std 15 self.glyph_color = glyph_color 16 self.glyph_color_gamma = glyph_color_gamma 17 self.freetype_force_autohint = freetype_force_autohint
Method generated by attrs for class FontEngineRunConfigStyle.
class
FontEngineRunConfigGlyphSequence(enum.Enum):
298class FontEngineRunConfigGlyphSequence(Enum): 299 HORI_DEFAULT = 'hori_default' 300 VERT_DEFAULT = 'vert_default'
An enumeration.
HORI_DEFAULT =
<FontEngineRunConfigGlyphSequence.HORI_DEFAULT: 'hori_default'>
VERT_DEFAULT =
<FontEngineRunConfigGlyphSequence.VERT_DEFAULT: 'vert_default'>
Inherited Members
- enum.Enum
- name
- value
class
FontEngineRunConfig:
304class FontEngineRunConfig: 305 height: int 306 width: int 307 chars: Sequence[str] 308 font_variant: FontVariant 309 310 # Sequence mode. 311 glyph_sequence: FontEngineRunConfigGlyphSequence = \ 312 FontEngineRunConfigGlyphSequence.HORI_DEFAULT 313 314 style: FontEngineRunConfigStyle = attrs.field(factory=FontEngineRunConfigStyle) 315 316 # For debugging. 317 return_font_variant: bool = False
FontEngineRunConfig( height: int, width: int, chars: Sequence[str], font_variant: vkit.engine.font.type.FontVariant, glyph_sequence: vkit.engine.font.type.FontEngineRunConfigGlyphSequence = <FontEngineRunConfigGlyphSequence.HORI_DEFAULT: 'hori_default'>, style: vkit.engine.font.type.FontEngineRunConfigStyle = NOTHING, return_font_variant: bool = False)
2def __init__(self, height, width, chars, font_variant, glyph_sequence=attr_dict['glyph_sequence'].default, style=NOTHING, return_font_variant=attr_dict['return_font_variant'].default): 3 self.height = height 4 self.width = width 5 self.chars = chars 6 self.font_variant = font_variant 7 self.glyph_sequence = glyph_sequence 8 if style is not NOTHING: 9 self.style = style 10 else: 11 self.style = __attr_factory_style() 12 self.return_font_variant = return_font_variant
Method generated by attrs for class FontEngineRunConfig.
321class CharBox(Shapable): 322 char: str 323 box: Box 324 325 def __attrs_post_init__(self): 326 assert len(self.char) == 1 and not self.char.isspace() 327 328 ############ 329 # Property # 330 ############ 331 @property 332 def up(self): 333 return self.box.up 334 335 @property 336 def down(self): 337 return self.box.down 338 339 @property 340 def left(self): 341 return self.box.left 342 343 @property 344 def right(self): 345 return self.box.right 346 347 @property 348 def height(self): 349 return self.box.height 350 351 @property 352 def width(self): 353 return self.box.width 354 355 ############ 356 # Operator # 357 ############ 358 def to_conducted_resized_char_box( 359 self, 360 shapable_or_shape: Union[Shapable, Tuple[int, int]], 361 resized_height: Optional[int] = None, 362 resized_width: Optional[int] = None, 363 ): 364 return attrs.evolve( 365 self, 366 box=self.box.to_conducted_resized_box( 367 shapable_or_shape=shapable_or_shape, 368 resized_height=resized_height, 369 resized_width=resized_width, 370 ), 371 ) 372 373 def to_resized_char_box( 374 self, 375 resized_height: Optional[int] = None, 376 resized_width: Optional[int] = None, 377 ): 378 return attrs.evolve( 379 self, 380 box=self.box.to_resized_box( 381 resized_height=resized_height, 382 resized_width=resized_width, 383 ), 384 ) 385 386 def to_shifted_char_box(self, offset_y: int = 0, offset_x: int = 0): 387 return attrs.evolve( 388 self, 389 box=self.box.to_shifted_box(offset_y=offset_y, offset_x=offset_x), 390 )
CharBox(char: str, box: vkit.element.box.Box)
2def __init__(self, char, box): 3 _setattr = _cached_setattr_get(self) 4 _setattr('char', char) 5 _setattr('box', box) 6 self.__attrs_post_init__()
Method generated by attrs for class CharBox.
def
to_conducted_resized_char_box( self, shapable_or_shape: Union[vkit.element.type.Shapable, Tuple[int, int]], resized_height: Union[int, NoneType] = None, resized_width: Union[int, NoneType] = None):
358 def to_conducted_resized_char_box( 359 self, 360 shapable_or_shape: Union[Shapable, Tuple[int, int]], 361 resized_height: Optional[int] = None, 362 resized_width: Optional[int] = None, 363 ): 364 return attrs.evolve( 365 self, 366 box=self.box.to_conducted_resized_box( 367 shapable_or_shape=shapable_or_shape, 368 resized_height=resized_height, 369 resized_width=resized_width, 370 ), 371 )
class
CharGlyph:
394class CharGlyph: 395 char: str 396 image: Image 397 score_map: Optional[ScoreMap] 398 # Load from font face. See build_char_glyph. 399 ascent: int 400 pad_up: int 401 pad_down: int 402 pad_left: int 403 pad_right: int 404 # For rendering text line and generating char-level polygon, based on the reference char. 405 ref_ascent_plus_pad_up: int 406 ref_char_height: int 407 ref_char_width: int 408 409 def __attrs_post_init__(self): 410 # NOTE: ascent could be negative for char like '_'. 411 assert self.pad_up >= 0 412 assert self.pad_down >= 0 413 assert self.pad_left >= 0 414 assert self.pad_right >= 0 415 416 @property 417 def height(self): 418 return self.image.height 419 420 @property 421 def width(self): 422 return self.image.width 423 424 def get_glyph_mask( 425 self, 426 box: Optional[Box] = None, 427 enable_resize: bool = False, 428 cv_resize_interpolation: int = cv.INTER_CUBIC, 429 ): 430 if self.image.mat.ndim == 2: 431 # Default or monochrome. 432 np_mask = (self.image.mat > 0) 433 434 elif self.image.mat.ndim == 3: 435 # LCD. 436 np_mask = np.any((self.image.mat > 0), axis=2) 437 438 else: 439 raise NotImplementedError() 440 441 mask = Mask(mat=np_mask.astype(np.uint8)) 442 if box: 443 if mask.shape != box.shape: 444 assert enable_resize 445 mask = mask.to_resized_mask( 446 resized_height=box.height, 447 resized_width=box.width, 448 cv_resize_interpolation=cv_resize_interpolation, 449 ) 450 mask = mask.to_box_attached(box) 451 452 return mask
CharGlyph( char: str, image: vkit.element.image.Image, score_map: Union[vkit.element.score_map.ScoreMap, NoneType], ascent: int, pad_up: int, pad_down: int, pad_left: int, pad_right: int, ref_ascent_plus_pad_up: int, ref_char_height: int, ref_char_width: int)
2def __init__(self, char, image, score_map, ascent, pad_up, pad_down, pad_left, pad_right, ref_ascent_plus_pad_up, ref_char_height, ref_char_width): 3 self.char = char 4 self.image = image 5 self.score_map = score_map 6 self.ascent = ascent 7 self.pad_up = pad_up 8 self.pad_down = pad_down 9 self.pad_left = pad_left 10 self.pad_right = pad_right 11 self.ref_ascent_plus_pad_up = ref_ascent_plus_pad_up 12 self.ref_char_height = ref_char_height 13 self.ref_char_width = ref_char_width 14 self.__attrs_post_init__()
Method generated by attrs for class CharGlyph.
def
get_glyph_mask( self, box: Union[vkit.element.box.Box, NoneType] = None, enable_resize: bool = False, cv_resize_interpolation: int = 2):
424 def get_glyph_mask( 425 self, 426 box: Optional[Box] = None, 427 enable_resize: bool = False, 428 cv_resize_interpolation: int = cv.INTER_CUBIC, 429 ): 430 if self.image.mat.ndim == 2: 431 # Default or monochrome. 432 np_mask = (self.image.mat > 0) 433 434 elif self.image.mat.ndim == 3: 435 # LCD. 436 np_mask = np.any((self.image.mat > 0), axis=2) 437 438 else: 439 raise NotImplementedError() 440 441 mask = Mask(mat=np_mask.astype(np.uint8)) 442 if box: 443 if mask.shape != box.shape: 444 assert enable_resize 445 mask = mask.to_resized_mask( 446 resized_height=box.height, 447 resized_width=box.width, 448 cv_resize_interpolation=cv_resize_interpolation, 449 ) 450 mask = mask.to_box_attached(box) 451 452 return mask
class
TextLine:
456class TextLine: 457 image: Image 458 mask: Mask 459 score_map: Optional[ScoreMap] 460 char_boxes: Sequence[CharBox] 461 # NOTE: char_glyphs might not have the same shapes as char_boxes. 462 char_glyphs: Sequence[CharGlyph] 463 cv_resize_interpolation: int 464 style: FontEngineRunConfigStyle 465 font_size: int 466 text: str 467 is_hori: bool 468 469 # Shifted text line is bound to a page. 470 shifted: bool = False 471 472 # For debugging. 473 font_variant: Optional[FontVariant] = None 474 475 @property 476 def box(self): 477 assert self.mask.box 478 return self.mask.box 479 480 @property 481 def glyph_color(self): 482 return self.style.glyph_color 483 484 def to_shifted_text_line(self, offset_y: int = 0, offset_x: int = 0): 485 self.shifted = True 486 487 shifted_image = self.image.to_shifted_image(offset_y=offset_y, offset_x=offset_x) 488 shifted_mask = self.mask.to_shifted_mask(offset_y=offset_y, offset_x=offset_x) 489 490 shifted_score_map = None 491 if self.score_map: 492 shifted_score_map = self.score_map.to_shifted_score_map( 493 offset_y=offset_y, 494 offset_x=offset_x, 495 ) 496 497 shifted_char_boxes = [ 498 char_box.to_shifted_char_box( 499 offset_y=offset_y, 500 offset_x=offset_x, 501 ) for char_box in self.char_boxes 502 ] 503 504 return attrs.evolve( 505 self, 506 image=shifted_image, 507 mask=shifted_mask, 508 score_map=shifted_score_map, 509 char_boxes=shifted_char_boxes, 510 ) 511 512 def split(self): 513 texts = self.text.split() 514 if len(texts) == 1: 515 # No need to split. 516 return [self] 517 assert len(texts) > 1 518 519 # Seperated by space(s). 520 text_lines: List[TextLine] = [] 521 522 begin = 0 523 for text in texts: 524 end = begin + len(text) - 1 525 char_boxes = self.char_boxes[begin:end + 1] 526 char_glyphs = self.char_glyphs[begin:end + 1] 527 528 if self.is_hori: 529 left = char_boxes[0].left 530 right = char_boxes[-1].right 531 up = min(char_box.up for char_box in char_boxes) 532 down = max(char_box.down for char_box in char_boxes) 533 else: 534 up = char_boxes[0].up 535 down = char_boxes[-1].down 536 left = min(char_box.left for char_box in char_boxes) 537 right = max(char_box.right for char_box in char_boxes) 538 box = Box(up=up, down=down, left=left, right=right) 539 540 image = box.extract_image(self.image) 541 mask = box.extract_mask(self.mask) 542 score_map = None 543 if self.score_map: 544 score_map = box.extract_score_map(self.score_map) 545 546 text_lines.append( 547 attrs.evolve( 548 self, 549 image=image, 550 mask=mask, 551 score_map=score_map, 552 char_boxes=char_boxes, 553 char_glyphs=char_glyphs, 554 text=text, 555 ) 556 ) 557 begin = end + 1 558 559 return text_lines 560 561 def to_polygon(self): 562 if self.is_hori: 563 xs = [self.box.left] 564 for char_box in self.char_boxes: 565 if xs[-1] < char_box.left: 566 xs.append(char_box.left) 567 if char_box.left < char_box.right: 568 xs.append(char_box.right) 569 if xs[-1] < self.box.right: 570 xs.append(self.box.right) 571 572 points = PointList() 573 574 for x in xs: 575 points.append(Point.create(y=self.box.up, x=x)) 576 577 y_mid = (self.box.up + self.box.down) // 2 578 if self.box.up < y_mid < self.box.down: 579 points.append(Point.create(y=y_mid, x=xs[-1])) 580 581 for x in reversed(xs): 582 points.append(Point.create(y=self.box.down, x=x)) 583 584 if self.box.up < y_mid < self.box.down: 585 points.append(Point.create(y=y_mid, x=xs[0])) 586 587 return Polygon.create(points=points) 588 589 else: 590 ys = [self.box.up] 591 for char_box in self.char_boxes: 592 if ys[-1] < char_box.up: 593 ys.append(char_box.up) 594 if char_box.up < char_box.down: 595 ys.append(char_box.down) 596 if ys[-1] < self.box.down: 597 ys.append(self.box.down) 598 599 points = PointList() 600 601 for y in ys: 602 points.append(Point.create(y=y, x=self.box.right)) 603 604 x_mid = (self.box.left + self.box.right) // 2 605 if self.box.left < x_mid < self.box.right: 606 points.append(Point.create(y=ys[-1], x=x_mid)) 607 608 for y in reversed(ys): 609 points.append(Point.create(y=y, x=self.box.left)) 610 611 if self.box.left < x_mid < self.box.right: 612 points.append(Point.create(y=ys[0], x=x_mid)) 613 614 return Polygon.create(points=points) 615 616 @classmethod 617 def build_char_polygon( 618 cls, 619 up: float, 620 down: float, 621 left: float, 622 right: float, 623 ): 624 return Polygon.from_xy_pairs([ 625 (left, up), 626 (right, up), 627 (right, down), 628 (left, down), 629 ]) 630 631 def to_char_polygons( 632 self, 633 page_height: int, 634 page_width: int, 635 ref_char_height_ratio: float = 1.0, 636 ref_char_width_ratio: float = 1.0, 637 ): 638 assert len(self.char_boxes) == len(self.char_glyphs) 639 640 if self.is_hori: 641 polygons: List[Polygon] = [] 642 for char_box, char_glyph in zip(self.char_boxes, self.char_glyphs): 643 ref_char_height = char_glyph.ref_char_height * ref_char_height_ratio 644 ref_char_width = char_glyph.ref_char_width * ref_char_width_ratio 645 box = char_box.box 646 647 up = box.up 648 down = box.down 649 if box.height < ref_char_height: 650 inc = ref_char_height - box.height 651 half_inc = inc / 2 652 up = max(0, up - half_inc) 653 down = min(page_height - 1, down + half_inc) 654 655 left = box.left 656 right = box.right 657 if box.width < ref_char_width: 658 inc = ref_char_width - box.width 659 half_inc = inc / 2 660 left = max(0, left - half_inc) 661 right = min(page_width - 1, right + half_inc) 662 663 polygons.append(self.build_char_polygon( 664 up=up, 665 down=down, 666 left=left, 667 right=right, 668 )) 669 return polygons 670 671 else: 672 polygons: List[Polygon] = [] 673 for char_box, char_glyph in zip(self.char_boxes, self.char_glyphs): 674 ref_char_height = char_glyph.ref_char_height * ref_char_height_ratio 675 ref_char_width = char_glyph.ref_char_width * ref_char_width_ratio 676 box = char_box.box 677 678 left = box.left 679 right = box.right 680 if box.width < ref_char_height: 681 inc = ref_char_height - box.width 682 half_inc = inc / 2 683 left = max(0, left - half_inc) 684 right = min(page_width - 1, right + half_inc) 685 686 up = box.up 687 down = box.down 688 if box.height < ref_char_width: 689 inc = ref_char_width - box.height 690 half_inc = inc / 2 691 up = max(self.box.up, up - half_inc) 692 down = min(page_height - 1, down + half_inc) 693 694 polygons.append(self.build_char_polygon( 695 up=up, 696 down=down, 697 left=left, 698 right=right, 699 )) 700 return polygons 701 702 def get_height_points(self, num_points: int, is_up: bool): 703 if self.is_hori: 704 step = max(1, self.box.width // num_points) 705 xs = list(range(0, self.box.right + 1, step)) 706 if len(xs) >= num_points: 707 xs = xs[:num_points - 1] 708 xs.append(self.box.right) 709 710 points = PointList() 711 for x in xs: 712 if is_up: 713 y = self.box.up 714 else: 715 y = self.box.down 716 points.append(Point.create(y=y, x=x)) 717 return points 718 719 else: 720 step = max(1, self.box.height // num_points) 721 ys = list(range(self.box.up, self.box.down + 1, step)) 722 if len(ys) >= num_points: 723 ys = ys[:num_points - 1] 724 ys.append(self.box.down) 725 726 points = PointList() 727 for y in ys: 728 if is_up: 729 x = self.box.right 730 else: 731 x = self.box.left 732 points.append(Point.create(y=y, x=x)) 733 return points 734 735 def get_char_level_height_points(self, is_up: bool): 736 if self.is_hori: 737 points = PointList() 738 for char_box in self.char_boxes: 739 x = (char_box.left + char_box.right) / 2 740 if is_up: 741 y = self.box.up 742 else: 743 y = self.box.down 744 points.append(Point.create(y=y, x=x)) 745 return points 746 747 else: 748 points = PointList() 749 for char_box in self.char_boxes: 750 y = (char_box.up + char_box.down) / 2 751 if is_up: 752 x = self.box.right 753 else: 754 x = self.box.left 755 points.append(Point.create(y=y, x=x)) 756 return points
TextLine( image: vkit.element.image.Image, mask: vkit.element.mask.Mask, score_map: Union[vkit.element.score_map.ScoreMap, NoneType], char_boxes: Sequence[vkit.engine.font.type.CharBox], char_glyphs: Sequence[vkit.engine.font.type.CharGlyph], cv_resize_interpolation: int, style: vkit.engine.font.type.FontEngineRunConfigStyle, font_size: int, text: str, is_hori: bool, shifted: bool = False, font_variant: Union[vkit.engine.font.type.FontVariant, NoneType] = None)
2def __init__(self, image, mask, score_map, char_boxes, char_glyphs, cv_resize_interpolation, style, font_size, text, is_hori, shifted=attr_dict['shifted'].default, font_variant=attr_dict['font_variant'].default): 3 self.image = image 4 self.mask = mask 5 self.score_map = score_map 6 self.char_boxes = char_boxes 7 self.char_glyphs = char_glyphs 8 self.cv_resize_interpolation = cv_resize_interpolation 9 self.style = style 10 self.font_size = font_size 11 self.text = text 12 self.is_hori = is_hori 13 self.shifted = shifted 14 self.font_variant = font_variant
Method generated by attrs for class TextLine.
def
to_shifted_text_line(self, offset_y: int = 0, offset_x: int = 0):
484 def to_shifted_text_line(self, offset_y: int = 0, offset_x: int = 0): 485 self.shifted = True 486 487 shifted_image = self.image.to_shifted_image(offset_y=offset_y, offset_x=offset_x) 488 shifted_mask = self.mask.to_shifted_mask(offset_y=offset_y, offset_x=offset_x) 489 490 shifted_score_map = None 491 if self.score_map: 492 shifted_score_map = self.score_map.to_shifted_score_map( 493 offset_y=offset_y, 494 offset_x=offset_x, 495 ) 496 497 shifted_char_boxes = [ 498 char_box.to_shifted_char_box( 499 offset_y=offset_y, 500 offset_x=offset_x, 501 ) for char_box in self.char_boxes 502 ] 503 504 return attrs.evolve( 505 self, 506 image=shifted_image, 507 mask=shifted_mask, 508 score_map=shifted_score_map, 509 char_boxes=shifted_char_boxes, 510 )
def
split(self):
512 def split(self): 513 texts = self.text.split() 514 if len(texts) == 1: 515 # No need to split. 516 return [self] 517 assert len(texts) > 1 518 519 # Seperated by space(s). 520 text_lines: List[TextLine] = [] 521 522 begin = 0 523 for text in texts: 524 end = begin + len(text) - 1 525 char_boxes = self.char_boxes[begin:end + 1] 526 char_glyphs = self.char_glyphs[begin:end + 1] 527 528 if self.is_hori: 529 left = char_boxes[0].left 530 right = char_boxes[-1].right 531 up = min(char_box.up for char_box in char_boxes) 532 down = max(char_box.down for char_box in char_boxes) 533 else: 534 up = char_boxes[0].up 535 down = char_boxes[-1].down 536 left = min(char_box.left for char_box in char_boxes) 537 right = max(char_box.right for char_box in char_boxes) 538 box = Box(up=up, down=down, left=left, right=right) 539 540 image = box.extract_image(self.image) 541 mask = box.extract_mask(self.mask) 542 score_map = None 543 if self.score_map: 544 score_map = box.extract_score_map(self.score_map) 545 546 text_lines.append( 547 attrs.evolve( 548 self, 549 image=image, 550 mask=mask, 551 score_map=score_map, 552 char_boxes=char_boxes, 553 char_glyphs=char_glyphs, 554 text=text, 555 ) 556 ) 557 begin = end + 1 558 559 return text_lines
def
to_polygon(self):
561 def to_polygon(self): 562 if self.is_hori: 563 xs = [self.box.left] 564 for char_box in self.char_boxes: 565 if xs[-1] < char_box.left: 566 xs.append(char_box.left) 567 if char_box.left < char_box.right: 568 xs.append(char_box.right) 569 if xs[-1] < self.box.right: 570 xs.append(self.box.right) 571 572 points = PointList() 573 574 for x in xs: 575 points.append(Point.create(y=self.box.up, x=x)) 576 577 y_mid = (self.box.up + self.box.down) // 2 578 if self.box.up < y_mid < self.box.down: 579 points.append(Point.create(y=y_mid, x=xs[-1])) 580 581 for x in reversed(xs): 582 points.append(Point.create(y=self.box.down, x=x)) 583 584 if self.box.up < y_mid < self.box.down: 585 points.append(Point.create(y=y_mid, x=xs[0])) 586 587 return Polygon.create(points=points) 588 589 else: 590 ys = [self.box.up] 591 for char_box in self.char_boxes: 592 if ys[-1] < char_box.up: 593 ys.append(char_box.up) 594 if char_box.up < char_box.down: 595 ys.append(char_box.down) 596 if ys[-1] < self.box.down: 597 ys.append(self.box.down) 598 599 points = PointList() 600 601 for y in ys: 602 points.append(Point.create(y=y, x=self.box.right)) 603 604 x_mid = (self.box.left + self.box.right) // 2 605 if self.box.left < x_mid < self.box.right: 606 points.append(Point.create(y=ys[-1], x=x_mid)) 607 608 for y in reversed(ys): 609 points.append(Point.create(y=y, x=self.box.left)) 610 611 if self.box.left < x_mid < self.box.right: 612 points.append(Point.create(y=ys[0], x=x_mid)) 613 614 return Polygon.create(points=points)
def
to_char_polygons( self, page_height: int, page_width: int, ref_char_height_ratio: float = 1.0, ref_char_width_ratio: float = 1.0):
631 def to_char_polygons( 632 self, 633 page_height: int, 634 page_width: int, 635 ref_char_height_ratio: float = 1.0, 636 ref_char_width_ratio: float = 1.0, 637 ): 638 assert len(self.char_boxes) == len(self.char_glyphs) 639 640 if self.is_hori: 641 polygons: List[Polygon] = [] 642 for char_box, char_glyph in zip(self.char_boxes, self.char_glyphs): 643 ref_char_height = char_glyph.ref_char_height * ref_char_height_ratio 644 ref_char_width = char_glyph.ref_char_width * ref_char_width_ratio 645 box = char_box.box 646 647 up = box.up 648 down = box.down 649 if box.height < ref_char_height: 650 inc = ref_char_height - box.height 651 half_inc = inc / 2 652 up = max(0, up - half_inc) 653 down = min(page_height - 1, down + half_inc) 654 655 left = box.left 656 right = box.right 657 if box.width < ref_char_width: 658 inc = ref_char_width - box.width 659 half_inc = inc / 2 660 left = max(0, left - half_inc) 661 right = min(page_width - 1, right + half_inc) 662 663 polygons.append(self.build_char_polygon( 664 up=up, 665 down=down, 666 left=left, 667 right=right, 668 )) 669 return polygons 670 671 else: 672 polygons: List[Polygon] = [] 673 for char_box, char_glyph in zip(self.char_boxes, self.char_glyphs): 674 ref_char_height = char_glyph.ref_char_height * ref_char_height_ratio 675 ref_char_width = char_glyph.ref_char_width * ref_char_width_ratio 676 box = char_box.box 677 678 left = box.left 679 right = box.right 680 if box.width < ref_char_height: 681 inc = ref_char_height - box.width 682 half_inc = inc / 2 683 left = max(0, left - half_inc) 684 right = min(page_width - 1, right + half_inc) 685 686 up = box.up 687 down = box.down 688 if box.height < ref_char_width: 689 inc = ref_char_width - box.height 690 half_inc = inc / 2 691 up = max(self.box.up, up - half_inc) 692 down = min(page_height - 1, down + half_inc) 693 694 polygons.append(self.build_char_polygon( 695 up=up, 696 down=down, 697 left=left, 698 right=right, 699 )) 700 return polygons
def
get_height_points(self, num_points: int, is_up: bool):
702 def get_height_points(self, num_points: int, is_up: bool): 703 if self.is_hori: 704 step = max(1, self.box.width // num_points) 705 xs = list(range(0, self.box.right + 1, step)) 706 if len(xs) >= num_points: 707 xs = xs[:num_points - 1] 708 xs.append(self.box.right) 709 710 points = PointList() 711 for x in xs: 712 if is_up: 713 y = self.box.up 714 else: 715 y = self.box.down 716 points.append(Point.create(y=y, x=x)) 717 return points 718 719 else: 720 step = max(1, self.box.height // num_points) 721 ys = list(range(self.box.up, self.box.down + 1, step)) 722 if len(ys) >= num_points: 723 ys = ys[:num_points - 1] 724 ys.append(self.box.down) 725 726 points = PointList() 727 for y in ys: 728 if is_up: 729 x = self.box.right 730 else: 731 x = self.box.left 732 points.append(Point.create(y=y, x=x)) 733 return points
def
get_char_level_height_points(self, is_up: bool):
735 def get_char_level_height_points(self, is_up: bool): 736 if self.is_hori: 737 points = PointList() 738 for char_box in self.char_boxes: 739 x = (char_box.left + char_box.right) / 2 740 if is_up: 741 y = self.box.up 742 else: 743 y = self.box.down 744 points.append(Point.create(y=y, x=x)) 745 return points 746 747 else: 748 points = PointList() 749 for char_box in self.char_boxes: 750 y = (char_box.up + char_box.down) / 2 751 if is_up: 752 x = self.box.right 753 else: 754 x = self.box.left 755 points.append(Point.create(y=y, x=x)) 756 return points