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 get_cattrs_converter_ignoring_init_equals_false, 38 dyn_structure, 39 PathType, 40) 41from vkit.element import ( 42 Shapable, 43 Point, 44 PointList, 45 Box, 46 Polygon, 47 Mask, 48 ScoreMap, 49 Image, 50) 51 52 53@attrs.define(frozen=True) 54class FontGlyphInfo: 55 tags: Sequence[str] 56 ascent_plus_pad_up_min_to_font_size_ratio: float 57 height_min_to_font_size_ratio: float 58 width_min_to_font_size_ratio: float 59 60 61@attrs.define 62class FontGlyphInfoCollection: 63 font_glyph_infos: Sequence[FontGlyphInfo] 64 65 _tag_to_font_glyph_info: Mapping[str, FontGlyphInfo] = attrs_lazy_field() 66 67 def lazy_post_init_tag_to_font_glyph_info(self): 68 if self._tag_to_font_glyph_info: 69 return self._tag_to_font_glyph_info 70 71 tag_to_font_glyph_info = {} 72 for font_glyph_info in self.font_glyph_infos: 73 assert font_glyph_info.tags 74 for tag in font_glyph_info.tags: 75 assert tag not in tag_to_font_glyph_info 76 tag_to_font_glyph_info[tag] = font_glyph_info 77 78 self._tag_to_font_glyph_info = cast(Mapping[str, FontGlyphInfo], tag_to_font_glyph_info) 79 return self._tag_to_font_glyph_info 80 81 @property 82 def tag_to_font_glyph_info(self): 83 return self.lazy_post_init_tag_to_font_glyph_info() 84 85 86@attrs.define 87class FontVariant: 88 char_to_tags: Mapping[str, Sequence[str]] 89 font_file: PathType 90 font_glyph_info_collection: FontGlyphInfoCollection 91 is_ttc: bool = False 92 ttc_font_index: Optional[int] = None 93 94 95@unique 96class FontMode(Enum): 97 # ttc file. 98 TTC = 'ttc' 99 # Grouped ttf file(s). 100 VTTC = 'vttc' 101 # Grouped otf file(s). 102 VOTC = 'votc' 103 104 105@attrs.define 106class FontMeta: 107 name: str 108 mode: FontMode 109 char_to_tags: Mapping[str, Sequence[str]] 110 font_files: Sequence[str] 111 font_glyph_info_collection: FontGlyphInfoCollection 112 # NOTE: ttc_font_index_max is inclusive. 113 ttc_font_index_max: Optional[int] = None 114 115 _chars: Sequence[str] = attrs_lazy_field() 116 117 def lazy_post_init_chars(self): 118 if self._chars: 119 return self._chars 120 121 self._chars = cast(Sequence[str], sorted(self.char_to_tags)) 122 return self._chars 123 124 @property 125 def chars(self): 126 return self.lazy_post_init_chars() 127 128 def __repr__(self): 129 return ( 130 'FontMeta(' 131 f'name="{self.name}", ' 132 f'mode={self.mode}, ' 133 f'num_chars={len(self.char_to_tags)}), ' 134 f'font_files={self.font_files}, ' 135 f'ttc_font_index_max={self.ttc_font_index_max})' 136 ) 137 138 @classmethod 139 def from_file( 140 cls, 141 path: PathType, 142 font_file_prefix: Optional[PathType] = None, 143 ): 144 font = dyn_structure(path, FontMeta, force_path_type=True) 145 146 if font_file_prefix: 147 font_file_prefix_fd = io.folder(font_file_prefix, exists=True) 148 font_files = [] 149 for font_file in font.font_files: 150 font_file = str(io.file(font_file_prefix_fd / io.file(font_file), exists=True)) 151 font_files.append(font_file) 152 font = attrs.evolve(font, font_files=font_files) 153 154 return font 155 156 def to_file( 157 self, 158 path: PathType, 159 font_file_prefix: Optional[PathType] = None, 160 ): 161 font = self 162 163 if font_file_prefix: 164 font_file_prefix_fd = io.folder(font_file_prefix) 165 font_files = [] 166 for font_file in self.font_files: 167 font_files.append(str(io.file(font_file).relative_to(font_file_prefix_fd))) 168 font = attrs.evolve(self, font_files=font_files) 169 170 converter = get_cattrs_converter_ignoring_init_equals_false() 171 io.write_json(path, converter.unstructure(font), indent=2, ensure_ascii=False) 172 173 @property 174 def num_font_variants(self): 175 if self.mode in (FontMode.VOTC, FontMode.VTTC): 176 return len(self.font_files) 177 178 elif self.mode == FontMode.TTC: 179 assert self.ttc_font_index_max is not None 180 return self.ttc_font_index_max + 1 181 182 else: 183 raise NotImplementedError() 184 185 def get_font_variant(self, variant_idx: int): 186 if self.mode in (FontMode.VOTC, FontMode.VTTC): 187 assert variant_idx < len(self.font_files) 188 return FontVariant( 189 char_to_tags=self.char_to_tags, 190 font_file=io.file(self.font_files[variant_idx]), 191 font_glyph_info_collection=self.font_glyph_info_collection, 192 ) 193 194 elif self.mode == FontMode.TTC: 195 assert self.ttc_font_index_max is not None 196 assert variant_idx <= self.ttc_font_index_max 197 return FontVariant( 198 char_to_tags=self.char_to_tags, 199 font_file=io.file(self.font_files[0]), 200 font_glyph_info_collection=self.font_glyph_info_collection, 201 is_ttc=True, 202 ttc_font_index=variant_idx, 203 ) 204 205 else: 206 raise NotImplementedError() 207 208 209class FontCollectionFolderTree: 210 FONT = 'font' 211 FONT_META = 'font_meta' 212 213 214@attrs.define 215class FontCollection: 216 font_metas: Sequence[FontMeta] 217 218 _name_to_font_meta: Optional[Mapping[str, FontMeta]] = attrs_lazy_field() 219 _char_to_font_meta_names: Optional[Mapping[str, Set[str]]] = attrs_lazy_field() 220 221 def lazy_post_init(self): 222 initialized = (self._name_to_font_meta is not None) 223 if initialized: 224 return 225 226 name_to_font_meta: Dict[str, FontMeta] = {} 227 char_to_font_meta_names: DefaultDict[str, Set[str]] = defaultdict(set) 228 for font_meta in self.font_metas: 229 assert font_meta.name not in name_to_font_meta 230 name_to_font_meta[font_meta.name] = font_meta 231 for char in font_meta.chars: 232 char_to_font_meta_names[char].add(font_meta.name) 233 self._name_to_font_meta = name_to_font_meta 234 self._char_to_font_meta_names = dict(char_to_font_meta_names) 235 236 @property 237 def name_to_font_meta(self): 238 self.lazy_post_init() 239 assert self._name_to_font_meta is not None 240 return self._name_to_font_meta 241 242 @property 243 def char_to_font_meta_names(self): 244 self.lazy_post_init() 245 assert self._char_to_font_meta_names is not None 246 return 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) 266 267 268@attrs.define 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 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(self, page_height: int, page_width: int): 631 assert len(self.char_boxes) == len(self.char_glyphs) 632 633 if self.is_hori: 634 polygons: List[Polygon] = [] 635 for char_box, char_glyph in zip(self.char_boxes, self.char_glyphs): 636 ref_char_height = char_glyph.ref_char_height 637 ref_char_width = char_glyph.ref_char_width 638 box = char_box.box 639 640 up = box.up 641 down = box.down 642 if box.height < ref_char_height: 643 inc = ref_char_height - box.height 644 half_inc = inc / 2 645 up = max(0, up - half_inc) 646 down = min(page_height - 1, down + half_inc) 647 648 left = box.left 649 right = box.right 650 if box.width < ref_char_width: 651 inc = ref_char_width - box.width 652 half_inc = inc / 2 653 left = max(0, left - half_inc) 654 right = min(page_width - 1, right + half_inc) 655 656 polygons.append(self.build_char_polygon( 657 up=up, 658 down=down, 659 left=left, 660 right=right, 661 )) 662 return polygons 663 664 else: 665 polygons: List[Polygon] = [] 666 for char_box, char_glyph in zip(self.char_boxes, self.char_glyphs): 667 ref_char_height = char_glyph.ref_char_height 668 ref_char_width = char_glyph.ref_char_width 669 box = char_box.box 670 671 left = box.left 672 right = box.right 673 if box.width < ref_char_height: 674 inc = ref_char_height - box.width 675 half_inc = inc / 2 676 left = max(0, left - half_inc) 677 right = min(page_width - 1, right + half_inc) 678 679 up = box.up 680 down = box.down 681 if box.height < ref_char_width: 682 inc = ref_char_width - box.height 683 half_inc = inc / 2 684 up = max(self.box.up, up - half_inc) 685 down = min(page_height - 1, down + half_inc) 686 687 polygons.append(self.build_char_polygon( 688 up=up, 689 down=down, 690 left=left, 691 right=right, 692 )) 693 return polygons 694 695 def get_height_points(self, num_points: int, is_up: bool): 696 if self.is_hori: 697 step = max(1, self.box.width // num_points) 698 xs = list(range(0, self.box.right + 1, step)) 699 if len(xs) >= num_points: 700 xs = xs[:num_points - 1] 701 xs.append(self.box.right) 702 703 points = PointList() 704 for x in xs: 705 if is_up: 706 y = self.box.up 707 else: 708 y = self.box.down 709 points.append(Point.create(y=y, x=x)) 710 return points 711 712 else: 713 step = max(1, self.box.height // num_points) 714 ys = list(range(self.box.up, self.box.down + 1, step)) 715 if len(ys) >= num_points: 716 ys = ys[:num_points - 1] 717 ys.append(self.box.down) 718 719 points = PointList() 720 for y in ys: 721 if is_up: 722 x = self.box.right 723 else: 724 x = self.box.left 725 points.append(Point.create(y=y, x=x)) 726 return points 727 728 def get_char_level_height_points(self, is_up: bool): 729 if self.is_hori: 730 points = PointList() 731 for char_box in self.char_boxes: 732 x = (char_box.left + char_box.right) / 2 733 if is_up: 734 y = self.box.up 735 else: 736 y = self.box.down 737 points.append(Point.create(y=y, x=x)) 738 return points 739 740 else: 741 points = PointList() 742 for char_box in self.char_boxes: 743 y = (char_box.up + char_box.down) / 2 744 if is_up: 745 x = self.box.right 746 else: 747 x = self.box.left 748 points.append(Point.create(y=y, x=x)) 749 return points
class
FontGlyphInfo:
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
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(self, 'tags', tags) 4 _setattr(self, 'ascent_plus_pad_up_min_to_font_size_ratio', ascent_plus_pad_up_min_to_font_size_ratio) 5 _setattr(self, 'height_min_to_font_size_ratio', height_min_to_font_size_ratio) 6 _setattr(self, 'width_min_to_font_size_ratio', width_min_to_font_size_ratio)
Method generated by attrs for class FontGlyphInfo.
class
FontGlyphInfoCollection:
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()
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):
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
class
FontVariant:
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
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):
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'
An enumeration.
Inherited Members
- enum.Enum
- name
- value
class
FontMeta:
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()
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):
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
def
to_file( self, path: Union[str, os.PathLike], font_file_prefix: Union[str, os.PathLike, NoneType] = None):
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)
def
get_font_variant(self, variant_idx: int):
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()
class
FontCollectionFolderTree:
class
FontCollection:
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 assert self._name_to_font_meta is not None 241 return self._name_to_font_meta 242 243 @property 244 def char_to_font_meta_names(self): 245 self.lazy_post_init() 246 assert self._char_to_font_meta_names is not None 247 return self._char_to_font_meta_names 248 249 def filter_font_metas(self, chars: Iterable[str]): 250 font_meta_names = set.intersection( 251 *[self.char_to_font_meta_names[char] for char in chars if not char.isspace()] 252 ) 253 font_meta_names = sorted(font_meta_names) 254 return [self.name_to_font_meta[font_meta_name] for font_meta_name in font_meta_names] 255 256 @classmethod 257 def from_folder(cls, folder: PathType): 258 in_fd = io.folder(folder, expandvars=True, exists=True) 259 font_fd = io.folder(in_fd / FontCollectionFolderTree.FONT, exists=True) 260 font_meta_fd = io.folder(in_fd / FontCollectionFolderTree.FONT_META, exists=True) 261 262 font_metas: List[FontMeta] = [] 263 for font_meta_json in font_meta_fd.glob('*.json'): 264 font_metas.append(FontMeta.from_file(font_meta_json, font_fd)) 265 266 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):
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)
def
filter_font_metas(self, chars: Iterable[str]):
249 def filter_font_metas(self, chars: Iterable[str]): 250 font_meta_names = set.intersection( 251 *[self.char_to_font_meta_names[char] for char in chars if not char.isspace()] 252 ) 253 font_meta_names = sorted(font_meta_names) 254 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]):
256 @classmethod 257 def from_folder(cls, folder: PathType): 258 in_fd = io.folder(folder, expandvars=True, exists=True) 259 font_fd = io.folder(in_fd / FontCollectionFolderTree.FONT, exists=True) 260 font_meta_fd = io.folder(in_fd / FontCollectionFolderTree.FONT_META, exists=True) 261 262 font_metas: List[FontMeta] = [] 263 for font_meta_json in font_meta_fd.glob('*.json'): 264 font_metas.append(FontMeta.from_file(font_meta_json, font_fd)) 265 266 return cls(font_metas=font_metas)
class
FontEngineRunConfigStyle:
270class FontEngineRunConfigStyle: 271 # Font size. 272 font_size_ratio: float = 1.0 273 font_size_min: int = 12 274 font_size_max: int = 96 275 276 # Space between chars. 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, 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, 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.char_space_min = char_space_min 7 self.char_space_max = char_space_max 8 self.char_space_mean = char_space_mean 9 self.char_space_std = char_space_std 10 self.word_space_min = word_space_min 11 self.word_space_max = word_space_max 12 self.word_space_mean = word_space_mean 13 self.word_space_std = word_space_std 14 self.glyph_color = glyph_color 15 self.glyph_color_gamma = glyph_color_gamma 16 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.
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(self, 'char', char) 4 _setattr(self, 'box', box) 5 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(self, page_height: int, page_width: int): 632 assert len(self.char_boxes) == len(self.char_glyphs) 633 634 if self.is_hori: 635 polygons: List[Polygon] = [] 636 for char_box, char_glyph in zip(self.char_boxes, self.char_glyphs): 637 ref_char_height = char_glyph.ref_char_height 638 ref_char_width = char_glyph.ref_char_width 639 box = char_box.box 640 641 up = box.up 642 down = box.down 643 if box.height < ref_char_height: 644 inc = ref_char_height - box.height 645 half_inc = inc / 2 646 up = max(0, up - half_inc) 647 down = min(page_height - 1, down + half_inc) 648 649 left = box.left 650 right = box.right 651 if box.width < ref_char_width: 652 inc = ref_char_width - box.width 653 half_inc = inc / 2 654 left = max(0, left - half_inc) 655 right = min(page_width - 1, right + half_inc) 656 657 polygons.append(self.build_char_polygon( 658 up=up, 659 down=down, 660 left=left, 661 right=right, 662 )) 663 return polygons 664 665 else: 666 polygons: List[Polygon] = [] 667 for char_box, char_glyph in zip(self.char_boxes, self.char_glyphs): 668 ref_char_height = char_glyph.ref_char_height 669 ref_char_width = char_glyph.ref_char_width 670 box = char_box.box 671 672 left = box.left 673 right = box.right 674 if box.width < ref_char_height: 675 inc = ref_char_height - box.width 676 half_inc = inc / 2 677 left = max(0, left - half_inc) 678 right = min(page_width - 1, right + half_inc) 679 680 up = box.up 681 down = box.down 682 if box.height < ref_char_width: 683 inc = ref_char_width - box.height 684 half_inc = inc / 2 685 up = max(self.box.up, up - half_inc) 686 down = min(page_height - 1, down + half_inc) 687 688 polygons.append(self.build_char_polygon( 689 up=up, 690 down=down, 691 left=left, 692 right=right, 693 )) 694 return polygons 695 696 def get_height_points(self, num_points: int, is_up: bool): 697 if self.is_hori: 698 step = max(1, self.box.width // num_points) 699 xs = list(range(0, self.box.right + 1, step)) 700 if len(xs) >= num_points: 701 xs = xs[:num_points - 1] 702 xs.append(self.box.right) 703 704 points = PointList() 705 for x in xs: 706 if is_up: 707 y = self.box.up 708 else: 709 y = self.box.down 710 points.append(Point.create(y=y, x=x)) 711 return points 712 713 else: 714 step = max(1, self.box.height // num_points) 715 ys = list(range(self.box.up, self.box.down + 1, step)) 716 if len(ys) >= num_points: 717 ys = ys[:num_points - 1] 718 ys.append(self.box.down) 719 720 points = PointList() 721 for y in ys: 722 if is_up: 723 x = self.box.right 724 else: 725 x = self.box.left 726 points.append(Point.create(y=y, x=x)) 727 return points 728 729 def get_char_level_height_points(self, is_up: bool): 730 if self.is_hori: 731 points = PointList() 732 for char_box in self.char_boxes: 733 x = (char_box.left + char_box.right) / 2 734 if is_up: 735 y = self.box.up 736 else: 737 y = self.box.down 738 points.append(Point.create(y=y, x=x)) 739 return points 740 741 else: 742 points = PointList() 743 for char_box in self.char_boxes: 744 y = (char_box.up + char_box.down) / 2 745 if is_up: 746 x = self.box.right 747 else: 748 x = self.box.left 749 points.append(Point.create(y=y, x=x)) 750 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):
631 def to_char_polygons(self, page_height: int, page_width: int): 632 assert len(self.char_boxes) == len(self.char_glyphs) 633 634 if self.is_hori: 635 polygons: List[Polygon] = [] 636 for char_box, char_glyph in zip(self.char_boxes, self.char_glyphs): 637 ref_char_height = char_glyph.ref_char_height 638 ref_char_width = char_glyph.ref_char_width 639 box = char_box.box 640 641 up = box.up 642 down = box.down 643 if box.height < ref_char_height: 644 inc = ref_char_height - box.height 645 half_inc = inc / 2 646 up = max(0, up - half_inc) 647 down = min(page_height - 1, down + half_inc) 648 649 left = box.left 650 right = box.right 651 if box.width < ref_char_width: 652 inc = ref_char_width - box.width 653 half_inc = inc / 2 654 left = max(0, left - half_inc) 655 right = min(page_width - 1, right + half_inc) 656 657 polygons.append(self.build_char_polygon( 658 up=up, 659 down=down, 660 left=left, 661 right=right, 662 )) 663 return polygons 664 665 else: 666 polygons: List[Polygon] = [] 667 for char_box, char_glyph in zip(self.char_boxes, self.char_glyphs): 668 ref_char_height = char_glyph.ref_char_height 669 ref_char_width = char_glyph.ref_char_width 670 box = char_box.box 671 672 left = box.left 673 right = box.right 674 if box.width < ref_char_height: 675 inc = ref_char_height - box.width 676 half_inc = inc / 2 677 left = max(0, left - half_inc) 678 right = min(page_width - 1, right + half_inc) 679 680 up = box.up 681 down = box.down 682 if box.height < ref_char_width: 683 inc = ref_char_width - box.height 684 half_inc = inc / 2 685 up = max(self.box.up, up - half_inc) 686 down = min(page_height - 1, down + half_inc) 687 688 polygons.append(self.build_char_polygon( 689 up=up, 690 down=down, 691 left=left, 692 right=right, 693 )) 694 return polygons
def
get_height_points(self, num_points: int, is_up: bool):
696 def get_height_points(self, num_points: int, is_up: bool): 697 if self.is_hori: 698 step = max(1, self.box.width // num_points) 699 xs = list(range(0, self.box.right + 1, step)) 700 if len(xs) >= num_points: 701 xs = xs[:num_points - 1] 702 xs.append(self.box.right) 703 704 points = PointList() 705 for x in xs: 706 if is_up: 707 y = self.box.up 708 else: 709 y = self.box.down 710 points.append(Point.create(y=y, x=x)) 711 return points 712 713 else: 714 step = max(1, self.box.height // num_points) 715 ys = list(range(self.box.up, self.box.down + 1, step)) 716 if len(ys) >= num_points: 717 ys = ys[:num_points - 1] 718 ys.append(self.box.down) 719 720 points = PointList() 721 for y in ys: 722 if is_up: 723 x = self.box.right 724 else: 725 x = self.box.left 726 points.append(Point.create(y=y, x=x)) 727 return points
def
get_char_level_height_points(self, is_up: bool):
729 def get_char_level_height_points(self, is_up: bool): 730 if self.is_hori: 731 points = PointList() 732 for char_box in self.char_boxes: 733 x = (char_box.left + char_box.right) / 2 734 if is_up: 735 y = self.box.up 736 else: 737 y = self.box.down 738 points.append(Point.create(y=y, x=x)) 739 return points 740 741 else: 742 points = PointList() 743 for char_box in self.char_boxes: 744 y = (char_box.up + char_box.down) / 2 745 if is_up: 746 x = self.box.right 747 else: 748 x = self.box.left 749 points.append(Point.create(y=y, x=x)) 750 return points