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.

def lazy_post_init_chars(self):
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
@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:
210class FontCollectionFolderTree:
211    FONT = 'font'
212    FONT_META = 'font_meta'
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.

class CharBox(vkit.element.type.Shapable):
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        )
def to_resized_char_box( self, resized_height: Union[int, NoneType] = None, resized_width: Union[int, NoneType] = None):
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        )
def to_shifted_char_box(self, offset_y: int = 0, offset_x: int = 0):
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        )
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)
@classmethod
def build_char_polygon(cls, up: float, down: float, left: float, right: float):
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        ])
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