vkit.engine.font.type

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

Method generated by attrs for class FontGlyphInfo.

class FontGlyphInfoCollection:
64class FontGlyphInfoCollection:
65    font_glyph_infos: Sequence[FontGlyphInfo]
66
67    _tag_to_font_glyph_info: Mapping[str, FontGlyphInfo] = attrs_lazy_field()
68
69    def lazy_post_init_tag_to_font_glyph_info(self):
70        if self._tag_to_font_glyph_info:
71            return self._tag_to_font_glyph_info
72
73        tag_to_font_glyph_info = {}
74        for font_glyph_info in self.font_glyph_infos:
75            assert font_glyph_info.tags
76            for tag in font_glyph_info.tags:
77                assert tag not in tag_to_font_glyph_info
78                tag_to_font_glyph_info[tag] = font_glyph_info
79
80        self._tag_to_font_glyph_info = cast(Mapping[str, FontGlyphInfo], tag_to_font_glyph_info)
81        return self._tag_to_font_glyph_info
82
83    @property
84    def tag_to_font_glyph_info(self):
85        return self.lazy_post_init_tag_to_font_glyph_info()
FontGlyphInfoCollection(font_glyph_infos: Sequence[vkit.engine.font.type.FontGlyphInfo])
2def __init__(self, font_glyph_infos):
3    self.font_glyph_infos = font_glyph_infos
4    self._tag_to_font_glyph_info = attr_dict['_tag_to_font_glyph_info'].default

Method generated by attrs for class FontGlyphInfoCollection.

def lazy_post_init_tag_to_font_glyph_info(self):
69    def lazy_post_init_tag_to_font_glyph_info(self):
70        if self._tag_to_font_glyph_info:
71            return self._tag_to_font_glyph_info
72
73        tag_to_font_glyph_info = {}
74        for font_glyph_info in self.font_glyph_infos:
75            assert font_glyph_info.tags
76            for tag in font_glyph_info.tags:
77                assert tag not in tag_to_font_glyph_info
78                tag_to_font_glyph_info[tag] = font_glyph_info
79
80        self._tag_to_font_glyph_info = cast(Mapping[str, FontGlyphInfo], tag_to_font_glyph_info)
81        return self._tag_to_font_glyph_info
class FontVariant:
89class FontVariant:
90    char_to_tags: Mapping[str, Sequence[str]]
91    font_file: PathType
92    font_glyph_info_collection: FontGlyphInfoCollection
93    is_ttc: bool = False
94    ttc_font_index: Optional[int] = None
FontVariant( char_to_tags: Mapping[str, Sequence[str]], font_file: Union[str, os.PathLike], font_glyph_info_collection: vkit.engine.font.type.FontGlyphInfoCollection, is_ttc: bool = False, ttc_font_index: Union[int, NoneType] = None)
2def __init__(self, char_to_tags, font_file, font_glyph_info_collection, is_ttc=attr_dict['is_ttc'].default, ttc_font_index=attr_dict['ttc_font_index'].default):
3    self.char_to_tags = char_to_tags
4    self.font_file = font_file
5    self.font_glyph_info_collection = font_glyph_info_collection
6    self.is_ttc = is_ttc
7    self.ttc_font_index = ttc_font_index

Method generated by attrs for class FontVariant.

class FontMode(enum.Enum):
 98class FontMode(Enum):
 99    # ttc file.
100    TTC = 'ttc'
101    # Grouped ttf file(s).
102    VTTC = 'vttc'
103    # Grouped otf file(s).
104    VOTC = 'votc'

An enumeration.

TTC = <FontMode.TTC: 'ttc'>
VTTC = <FontMode.VTTC: 'vttc'>
VOTC = <FontMode.VOTC: 'votc'>
Inherited Members
enum.Enum
name
value
class FontMeta:
108class FontMeta:
109    name: str
110    mode: FontMode
111    char_to_tags: Mapping[str, Sequence[str]]
112    font_files: Sequence[str]
113    font_glyph_info_collection: FontGlyphInfoCollection
114    # NOTE: ttc_font_index_max is inclusive.
115    ttc_font_index_max: Optional[int] = None
116
117    _chars: Sequence[str] = attrs_lazy_field()
118
119    def lazy_post_init_chars(self):
120        if self._chars:
121            return self._chars
122
123        self._chars = cast(Sequence[str], sorted(self.char_to_tags))
124        return self._chars
125
126    @property
127    def chars(self):
128        return self.lazy_post_init_chars()
129
130    def __repr__(self):
131        return (
132            'FontMeta('
133            f'name="{self.name}", '
134            f'mode={self.mode}, '
135            f'num_chars={len(self.char_to_tags)}), '
136            f'font_files={self.font_files}, '
137            f'ttc_font_index_max={self.ttc_font_index_max})'
138        )
139
140    @classmethod
141    def from_file(
142        cls,
143        path: PathType,
144        font_file_prefix: Optional[PathType] = None,
145    ):
146        font = dyn_structure(path, FontMeta, force_path_type=True)
147
148        if font_file_prefix:
149            font_file_prefix_fd = io.folder(font_file_prefix, exists=True)
150            font_files = []
151            for font_file in font.font_files:
152                font_file = str(io.file(font_file_prefix_fd / io.file(font_file), exists=True))
153                font_files.append(font_file)
154            font = attrs.evolve(font, font_files=font_files)
155
156        return font
157
158    def to_file(
159        self,
160        path: PathType,
161        font_file_prefix: Optional[PathType] = None,
162    ):
163        font = self
164
165        if font_file_prefix:
166            font_file_prefix_fd = io.folder(font_file_prefix)
167            font_files = []
168            for font_file in self.font_files:
169                font_files.append(str(io.file(font_file).relative_to(font_file_prefix_fd)))
170            font = attrs.evolve(self, font_files=font_files)
171
172        converter = get_cattrs_converter_ignoring_init_equals_false()
173        io.write_json(path, converter.unstructure(font), indent=2, ensure_ascii=False)
174
175    @property
176    def num_font_variants(self):
177        if self.mode in (FontMode.VOTC, FontMode.VTTC):
178            return len(self.font_files)
179
180        elif self.mode == FontMode.TTC:
181            assert self.ttc_font_index_max is not None
182            return self.ttc_font_index_max + 1
183
184        else:
185            raise NotImplementedError()
186
187    def get_font_variant(self, variant_idx: int):
188        if self.mode in (FontMode.VOTC, FontMode.VTTC):
189            assert variant_idx < len(self.font_files)
190            return FontVariant(
191                char_to_tags=self.char_to_tags,
192                font_file=io.file(self.font_files[variant_idx]),
193                font_glyph_info_collection=self.font_glyph_info_collection,
194            )
195
196        elif self.mode == FontMode.TTC:
197            assert self.ttc_font_index_max is not None
198            assert variant_idx <= self.ttc_font_index_max
199            return FontVariant(
200                char_to_tags=self.char_to_tags,
201                font_file=io.file(self.font_files[0]),
202                font_glyph_info_collection=self.font_glyph_info_collection,
203                is_ttc=True,
204                ttc_font_index=variant_idx,
205            )
206
207        else:
208            raise NotImplementedError()
FontMeta( name: str, mode: vkit.engine.font.type.FontMode, char_to_tags: Mapping[str, Sequence[str]], font_files: Sequence[str], font_glyph_info_collection: vkit.engine.font.type.FontGlyphInfoCollection, ttc_font_index_max: Union[int, NoneType] = None)
2def __init__(self, name, mode, char_to_tags, font_files, font_glyph_info_collection, ttc_font_index_max=attr_dict['ttc_font_index_max'].default):
3    self.name = name
4    self.mode = mode
5    self.char_to_tags = char_to_tags
6    self.font_files = font_files
7    self.font_glyph_info_collection = font_glyph_info_collection
8    self.ttc_font_index_max = ttc_font_index_max
9    self._chars = attr_dict['_chars'].default

Method generated by attrs for class FontMeta.

def lazy_post_init_chars(self):
119    def lazy_post_init_chars(self):
120        if self._chars:
121            return self._chars
122
123        self._chars = cast(Sequence[str], sorted(self.char_to_tags))
124        return self._chars
@classmethod
def from_file( cls, path: Union[str, os.PathLike], font_file_prefix: Union[str, os.PathLike, NoneType] = None):
140    @classmethod
141    def from_file(
142        cls,
143        path: PathType,
144        font_file_prefix: Optional[PathType] = None,
145    ):
146        font = dyn_structure(path, FontMeta, force_path_type=True)
147
148        if font_file_prefix:
149            font_file_prefix_fd = io.folder(font_file_prefix, exists=True)
150            font_files = []
151            for font_file in font.font_files:
152                font_file = str(io.file(font_file_prefix_fd / io.file(font_file), exists=True))
153                font_files.append(font_file)
154            font = attrs.evolve(font, font_files=font_files)
155
156        return font
def to_file( self, path: Union[str, os.PathLike], font_file_prefix: Union[str, os.PathLike, NoneType] = None):
158    def to_file(
159        self,
160        path: PathType,
161        font_file_prefix: Optional[PathType] = None,
162    ):
163        font = self
164
165        if font_file_prefix:
166            font_file_prefix_fd = io.folder(font_file_prefix)
167            font_files = []
168            for font_file in self.font_files:
169                font_files.append(str(io.file(font_file).relative_to(font_file_prefix_fd)))
170            font = attrs.evolve(self, font_files=font_files)
171
172        converter = get_cattrs_converter_ignoring_init_equals_false()
173        io.write_json(path, converter.unstructure(font), indent=2, ensure_ascii=False)
def get_font_variant(self, variant_idx: int):
187    def get_font_variant(self, variant_idx: int):
188        if self.mode in (FontMode.VOTC, FontMode.VTTC):
189            assert variant_idx < len(self.font_files)
190            return FontVariant(
191                char_to_tags=self.char_to_tags,
192                font_file=io.file(self.font_files[variant_idx]),
193                font_glyph_info_collection=self.font_glyph_info_collection,
194            )
195
196        elif self.mode == FontMode.TTC:
197            assert self.ttc_font_index_max is not None
198            assert variant_idx <= self.ttc_font_index_max
199            return FontVariant(
200                char_to_tags=self.char_to_tags,
201                font_file=io.file(self.font_files[0]),
202                font_glyph_info_collection=self.font_glyph_info_collection,
203                is_ttc=True,
204                ttc_font_index=variant_idx,
205            )
206
207        else:
208            raise NotImplementedError()
class FontCollectionFolderTree:
211class FontCollectionFolderTree:
212    FONT = 'font'
213    FONT_META = 'font_meta'
class FontCollection:
217class FontCollection:
218    font_metas: Sequence[FontMeta]
219
220    _name_to_font_meta: Optional[Mapping[str, FontMeta]] = attrs_lazy_field()
221    _char_to_font_meta_names: Optional[Mapping[str, Set[str]]] = attrs_lazy_field()
222
223    def lazy_post_init(self):
224        initialized = (self._name_to_font_meta is not None)
225        if initialized:
226            return
227
228        name_to_font_meta: Dict[str, FontMeta] = {}
229        char_to_font_meta_names: DefaultDict[str, Set[str]] = defaultdict(set)
230        for font_meta in self.font_metas:
231            assert font_meta.name not in name_to_font_meta
232            name_to_font_meta[font_meta.name] = font_meta
233            for char in font_meta.chars:
234                char_to_font_meta_names[char].add(font_meta.name)
235        self._name_to_font_meta = name_to_font_meta
236        self._char_to_font_meta_names = dict(char_to_font_meta_names)
237
238    @property
239    def name_to_font_meta(self):
240        self.lazy_post_init()
241        return unwrap_optional_field(self._name_to_font_meta)
242
243    @property
244    def char_to_font_meta_names(self):
245        self.lazy_post_init()
246        return unwrap_optional_field(self._char_to_font_meta_names)
247
248    def filter_font_metas(self, chars: Iterable[str]):
249        font_meta_names = set.intersection(
250            *[self.char_to_font_meta_names[char] for char in chars if not char.isspace()]
251        )
252        font_meta_names = sorted(font_meta_names)
253        return [self.name_to_font_meta[font_meta_name] for font_meta_name in font_meta_names]
254
255    @classmethod
256    def from_folder(cls, folder: PathType):
257        in_fd = io.folder(folder, expandvars=True, exists=True)
258        font_fd = io.folder(in_fd / FontCollectionFolderTree.FONT, exists=True)
259        font_meta_fd = io.folder(in_fd / FontCollectionFolderTree.FONT_META, exists=True)
260
261        font_metas: List[FontMeta] = []
262        for font_meta_json in font_meta_fd.glob('*.json'):
263            font_metas.append(FontMeta.from_file(font_meta_json, font_fd))
264
265        return cls(font_metas=font_metas)
FontCollection(font_metas: Sequence[vkit.engine.font.type.FontMeta])
2def __init__(self, font_metas):
3    self.font_metas = font_metas
4    self._name_to_font_meta = attr_dict['_name_to_font_meta'].default
5    self._char_to_font_meta_names = attr_dict['_char_to_font_meta_names'].default

Method generated by attrs for class FontCollection.

def lazy_post_init(self):
223    def lazy_post_init(self):
224        initialized = (self._name_to_font_meta is not None)
225        if initialized:
226            return
227
228        name_to_font_meta: Dict[str, FontMeta] = {}
229        char_to_font_meta_names: DefaultDict[str, Set[str]] = defaultdict(set)
230        for font_meta in self.font_metas:
231            assert font_meta.name not in name_to_font_meta
232            name_to_font_meta[font_meta.name] = font_meta
233            for char in font_meta.chars:
234                char_to_font_meta_names[char].add(font_meta.name)
235        self._name_to_font_meta = name_to_font_meta
236        self._char_to_font_meta_names = dict(char_to_font_meta_names)
def filter_font_metas(self, chars: Iterable[str]):
248    def filter_font_metas(self, chars: Iterable[str]):
249        font_meta_names = set.intersection(
250            *[self.char_to_font_meta_names[char] for char in chars if not char.isspace()]
251        )
252        font_meta_names = sorted(font_meta_names)
253        return [self.name_to_font_meta[font_meta_name] for font_meta_name in font_meta_names]
@classmethod
def from_folder(cls, folder: Union[str, os.PathLike]):
255    @classmethod
256    def from_folder(cls, folder: PathType):
257        in_fd = io.folder(folder, expandvars=True, exists=True)
258        font_fd = io.folder(in_fd / FontCollectionFolderTree.FONT, exists=True)
259        font_meta_fd = io.folder(in_fd / FontCollectionFolderTree.FONT_META, exists=True)
260
261        font_metas: List[FontMeta] = []
262        for font_meta_json in font_meta_fd.glob('*.json'):
263            font_metas.append(FontMeta.from_file(font_meta_json, font_fd))
264
265        return cls(font_metas=font_metas)
class FontEngineRunConfigStyle:
269class FontEngineRunConfigStyle:
270    # Font size.
271    font_size_ratio: float = 1.0
272    font_size_min: int = 12
273    font_size_max: int = 96
274
275    # Space between chars.
276    prob_set_char_space_min: float = 0.5
277    char_space_min: float = 0.0
278    char_space_max: float = 0.2
279    char_space_mean: float = 0.1
280    char_space_std: float = 0.03
281
282    # Space between words.
283    word_space_min: float = 0.3
284    word_space_max: float = 1.0
285    word_space_mean: float = 0.6
286    word_space_std: float = 0.1
287
288    # Effect.
289    glyph_color: Tuple[int, int, int] = (0, 0, 0)
290    # https://en.wikipedia.org/wiki/Gamma_correction
291    glyph_color_gamma: float = 1.0
292
293    # Implementation related options.
294    freetype_force_autohint: bool = False
FontEngineRunConfigStyle( font_size_ratio: float = 1.0, font_size_min: int = 12, font_size_max: int = 96, prob_set_char_space_min: float = 0.5, char_space_min: float = 0.0, char_space_max: float = 0.2, char_space_mean: float = 0.1, char_space_std: float = 0.03, word_space_min: float = 0.3, word_space_max: float = 1.0, word_space_mean: float = 0.6, word_space_std: float = 0.1, glyph_color: Tuple[int, int, int] = (0, 0, 0), glyph_color_gamma: float = 1.0, freetype_force_autohint: bool = False)
 2def __init__(self, font_size_ratio=attr_dict['font_size_ratio'].default, font_size_min=attr_dict['font_size_min'].default, font_size_max=attr_dict['font_size_max'].default, prob_set_char_space_min=attr_dict['prob_set_char_space_min'].default, char_space_min=attr_dict['char_space_min'].default, char_space_max=attr_dict['char_space_max'].default, char_space_mean=attr_dict['char_space_mean'].default, char_space_std=attr_dict['char_space_std'].default, word_space_min=attr_dict['word_space_min'].default, word_space_max=attr_dict['word_space_max'].default, word_space_mean=attr_dict['word_space_mean'].default, word_space_std=attr_dict['word_space_std'].default, glyph_color=attr_dict['glyph_color'].default, glyph_color_gamma=attr_dict['glyph_color_gamma'].default, freetype_force_autohint=attr_dict['freetype_force_autohint'].default):
 3    self.font_size_ratio = font_size_ratio
 4    self.font_size_min = font_size_min
 5    self.font_size_max = font_size_max
 6    self.prob_set_char_space_min = prob_set_char_space_min
 7    self.char_space_min = char_space_min
 8    self.char_space_max = char_space_max
 9    self.char_space_mean = char_space_mean
10    self.char_space_std = char_space_std
11    self.word_space_min = word_space_min
12    self.word_space_max = word_space_max
13    self.word_space_mean = word_space_mean
14    self.word_space_std = word_space_std
15    self.glyph_color = glyph_color
16    self.glyph_color_gamma = glyph_color_gamma
17    self.freetype_force_autohint = freetype_force_autohint

Method generated by attrs for class FontEngineRunConfigStyle.

class FontEngineRunConfigGlyphSequence(enum.Enum):
298class FontEngineRunConfigGlyphSequence(Enum):
299    HORI_DEFAULT = 'hori_default'
300    VERT_DEFAULT = 'vert_default'

An enumeration.

HORI_DEFAULT = <FontEngineRunConfigGlyphSequence.HORI_DEFAULT: 'hori_default'>
VERT_DEFAULT = <FontEngineRunConfigGlyphSequence.VERT_DEFAULT: 'vert_default'>
Inherited Members
enum.Enum
name
value
class FontEngineRunConfig:
304class FontEngineRunConfig:
305    height: int
306    width: int
307    chars: Sequence[str]
308    font_variant: FontVariant
309
310    # Sequence mode.
311    glyph_sequence: FontEngineRunConfigGlyphSequence = \
312        FontEngineRunConfigGlyphSequence.HORI_DEFAULT
313
314    style: FontEngineRunConfigStyle = attrs.field(factory=FontEngineRunConfigStyle)
315
316    # For debugging.
317    return_font_variant: bool = False
FontEngineRunConfig( height: int, width: int, chars: Sequence[str], font_variant: vkit.engine.font.type.FontVariant, glyph_sequence: vkit.engine.font.type.FontEngineRunConfigGlyphSequence = <FontEngineRunConfigGlyphSequence.HORI_DEFAULT: 'hori_default'>, style: vkit.engine.font.type.FontEngineRunConfigStyle = NOTHING, return_font_variant: bool = False)
 2def __init__(self, height, width, chars, font_variant, glyph_sequence=attr_dict['glyph_sequence'].default, style=NOTHING, return_font_variant=attr_dict['return_font_variant'].default):
 3    self.height = height
 4    self.width = width
 5    self.chars = chars
 6    self.font_variant = font_variant
 7    self.glyph_sequence = glyph_sequence
 8    if style is not NOTHING:
 9        self.style = style
10    else:
11        self.style = __attr_factory_style()
12    self.return_font_variant = return_font_variant

Method generated by attrs for class FontEngineRunConfig.

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 = _cached_setattr_get(self)
4    _setattr('char', char)
5    _setattr('box', box)
6    self.__attrs_post_init__()

Method generated by attrs for class CharBox.

def to_conducted_resized_char_box( self, shapable_or_shape: Union[vkit.element.type.Shapable, Tuple[int, int]], resized_height: Union[int, NoneType] = None, resized_width: Union[int, NoneType] = None):
358    def to_conducted_resized_char_box(
359        self,
360        shapable_or_shape: Union[Shapable, Tuple[int, int]],
361        resized_height: Optional[int] = None,
362        resized_width: Optional[int] = None,
363    ):
364        return attrs.evolve(
365            self,
366            box=self.box.to_conducted_resized_box(
367                shapable_or_shape=shapable_or_shape,
368                resized_height=resized_height,
369                resized_width=resized_width,
370            ),
371        )
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(
632        self,
633        page_height: int,
634        page_width: int,
635        ref_char_height_ratio: float = 1.0,
636        ref_char_width_ratio: float = 1.0,
637    ):
638        assert len(self.char_boxes) == len(self.char_glyphs)
639
640        if self.is_hori:
641            polygons: List[Polygon] = []
642            for char_box, char_glyph in zip(self.char_boxes, self.char_glyphs):
643                ref_char_height = char_glyph.ref_char_height * ref_char_height_ratio
644                ref_char_width = char_glyph.ref_char_width * ref_char_width_ratio
645                box = char_box.box
646
647                up = box.up
648                down = box.down
649                if box.height < ref_char_height:
650                    inc = ref_char_height - box.height
651                    half_inc = inc / 2
652                    up = max(0, up - half_inc)
653                    down = min(page_height - 1, down + half_inc)
654
655                left = box.left
656                right = box.right
657                if box.width < ref_char_width:
658                    inc = ref_char_width - box.width
659                    half_inc = inc / 2
660                    left = max(0, left - half_inc)
661                    right = min(page_width - 1, right + half_inc)
662
663                polygons.append(self.build_char_polygon(
664                    up=up,
665                    down=down,
666                    left=left,
667                    right=right,
668                ))
669            return polygons
670
671        else:
672            polygons: List[Polygon] = []
673            for char_box, char_glyph in zip(self.char_boxes, self.char_glyphs):
674                ref_char_height = char_glyph.ref_char_height * ref_char_height_ratio
675                ref_char_width = char_glyph.ref_char_width * ref_char_width_ratio
676                box = char_box.box
677
678                left = box.left
679                right = box.right
680                if box.width < ref_char_height:
681                    inc = ref_char_height - box.width
682                    half_inc = inc / 2
683                    left = max(0, left - half_inc)
684                    right = min(page_width - 1, right + half_inc)
685
686                up = box.up
687                down = box.down
688                if box.height < ref_char_width:
689                    inc = ref_char_width - box.height
690                    half_inc = inc / 2
691                    up = max(self.box.up, up - half_inc)
692                    down = min(page_height - 1, down + half_inc)
693
694                polygons.append(self.build_char_polygon(
695                    up=up,
696                    down=down,
697                    left=left,
698                    right=right,
699                ))
700            return polygons
701
702    def get_height_points(self, num_points: int, is_up: bool):
703        if self.is_hori:
704            step = max(1, self.box.width // num_points)
705            xs = list(range(0, self.box.right + 1, step))
706            if len(xs) >= num_points:
707                xs = xs[:num_points - 1]
708                xs.append(self.box.right)
709
710            points = PointList()
711            for x in xs:
712                if is_up:
713                    y = self.box.up
714                else:
715                    y = self.box.down
716                points.append(Point.create(y=y, x=x))
717            return points
718
719        else:
720            step = max(1, self.box.height // num_points)
721            ys = list(range(self.box.up, self.box.down + 1, step))
722            if len(ys) >= num_points:
723                ys = ys[:num_points - 1]
724                ys.append(self.box.down)
725
726            points = PointList()
727            for y in ys:
728                if is_up:
729                    x = self.box.right
730                else:
731                    x = self.box.left
732                points.append(Point.create(y=y, x=x))
733            return points
734
735    def get_char_level_height_points(self, is_up: bool):
736        if self.is_hori:
737            points = PointList()
738            for char_box in self.char_boxes:
739                x = (char_box.left + char_box.right) / 2
740                if is_up:
741                    y = self.box.up
742                else:
743                    y = self.box.down
744                points.append(Point.create(y=y, x=x))
745            return points
746
747        else:
748            points = PointList()
749            for char_box in self.char_boxes:
750                y = (char_box.up + char_box.down) / 2
751                if is_up:
752                    x = self.box.right
753                else:
754                    x = self.box.left
755                points.append(Point.create(y=y, x=x))
756            return points
TextLine( image: vkit.element.image.Image, mask: vkit.element.mask.Mask, score_map: Union[vkit.element.score_map.ScoreMap, NoneType], char_boxes: Sequence[vkit.engine.font.type.CharBox], char_glyphs: Sequence[vkit.engine.font.type.CharGlyph], cv_resize_interpolation: int, style: vkit.engine.font.type.FontEngineRunConfigStyle, font_size: int, text: str, is_hori: bool, shifted: bool = False, font_variant: Union[vkit.engine.font.type.FontVariant, NoneType] = None)
 2def __init__(self, image, mask, score_map, char_boxes, char_glyphs, cv_resize_interpolation, style, font_size, text, is_hori, shifted=attr_dict['shifted'].default, font_variant=attr_dict['font_variant'].default):
 3    self.image = image
 4    self.mask = mask
 5    self.score_map = score_map
 6    self.char_boxes = char_boxes
 7    self.char_glyphs = char_glyphs
 8    self.cv_resize_interpolation = cv_resize_interpolation
 9    self.style = style
10    self.font_size = font_size
11    self.text = text
12    self.is_hori = is_hori
13    self.shifted = shifted
14    self.font_variant = font_variant

Method generated by attrs for class TextLine.

def to_shifted_text_line(self, offset_y: int = 0, offset_x: int = 0):
484    def to_shifted_text_line(self, offset_y: int = 0, offset_x: int = 0):
485        self.shifted = True
486
487        shifted_image = self.image.to_shifted_image(offset_y=offset_y, offset_x=offset_x)
488        shifted_mask = self.mask.to_shifted_mask(offset_y=offset_y, offset_x=offset_x)
489
490        shifted_score_map = None
491        if self.score_map:
492            shifted_score_map = self.score_map.to_shifted_score_map(
493                offset_y=offset_y,
494                offset_x=offset_x,
495            )
496
497        shifted_char_boxes = [
498            char_box.to_shifted_char_box(
499                offset_y=offset_y,
500                offset_x=offset_x,
501            ) for char_box in self.char_boxes
502        ]
503
504        return attrs.evolve(
505            self,
506            image=shifted_image,
507            mask=shifted_mask,
508            score_map=shifted_score_map,
509            char_boxes=shifted_char_boxes,
510        )
def split(self):
512    def split(self):
513        texts = self.text.split()
514        if len(texts) == 1:
515            # No need to split.
516            return [self]
517        assert len(texts) > 1
518
519        # Seperated by space(s).
520        text_lines: List[TextLine] = []
521
522        begin = 0
523        for text in texts:
524            end = begin + len(text) - 1
525            char_boxes = self.char_boxes[begin:end + 1]
526            char_glyphs = self.char_glyphs[begin:end + 1]
527
528            if self.is_hori:
529                left = char_boxes[0].left
530                right = char_boxes[-1].right
531                up = min(char_box.up for char_box in char_boxes)
532                down = max(char_box.down for char_box in char_boxes)
533            else:
534                up = char_boxes[0].up
535                down = char_boxes[-1].down
536                left = min(char_box.left for char_box in char_boxes)
537                right = max(char_box.right for char_box in char_boxes)
538            box = Box(up=up, down=down, left=left, right=right)
539
540            image = box.extract_image(self.image)
541            mask = box.extract_mask(self.mask)
542            score_map = None
543            if self.score_map:
544                score_map = box.extract_score_map(self.score_map)
545
546            text_lines.append(
547                attrs.evolve(
548                    self,
549                    image=image,
550                    mask=mask,
551                    score_map=score_map,
552                    char_boxes=char_boxes,
553                    char_glyphs=char_glyphs,
554                    text=text,
555                )
556            )
557            begin = end + 1
558
559        return text_lines
def to_polygon(self):
561    def to_polygon(self):
562        if self.is_hori:
563            xs = [self.box.left]
564            for char_box in self.char_boxes:
565                if xs[-1] < char_box.left:
566                    xs.append(char_box.left)
567                if char_box.left < char_box.right:
568                    xs.append(char_box.right)
569            if xs[-1] < self.box.right:
570                xs.append(self.box.right)
571
572            points = PointList()
573
574            for x in xs:
575                points.append(Point.create(y=self.box.up, x=x))
576
577            y_mid = (self.box.up + self.box.down) // 2
578            if self.box.up < y_mid < self.box.down:
579                points.append(Point.create(y=y_mid, x=xs[-1]))
580
581            for x in reversed(xs):
582                points.append(Point.create(y=self.box.down, x=x))
583
584            if self.box.up < y_mid < self.box.down:
585                points.append(Point.create(y=y_mid, x=xs[0]))
586
587            return Polygon.create(points=points)
588
589        else:
590            ys = [self.box.up]
591            for char_box in self.char_boxes:
592                if ys[-1] < char_box.up:
593                    ys.append(char_box.up)
594                if char_box.up < char_box.down:
595                    ys.append(char_box.down)
596            if ys[-1] < self.box.down:
597                ys.append(self.box.down)
598
599            points = PointList()
600
601            for y in ys:
602                points.append(Point.create(y=y, x=self.box.right))
603
604            x_mid = (self.box.left + self.box.right) // 2
605            if self.box.left < x_mid < self.box.right:
606                points.append(Point.create(y=ys[-1], x=x_mid))
607
608            for y in reversed(ys):
609                points.append(Point.create(y=y, x=self.box.left))
610
611            if self.box.left < x_mid < self.box.right:
612                points.append(Point.create(y=ys[0], x=x_mid))
613
614            return Polygon.create(points=points)
@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, ref_char_height_ratio: float = 1.0, ref_char_width_ratio: float = 1.0):
631    def to_char_polygons(
632        self,
633        page_height: int,
634        page_width: int,
635        ref_char_height_ratio: float = 1.0,
636        ref_char_width_ratio: float = 1.0,
637    ):
638        assert len(self.char_boxes) == len(self.char_glyphs)
639
640        if self.is_hori:
641            polygons: List[Polygon] = []
642            for char_box, char_glyph in zip(self.char_boxes, self.char_glyphs):
643                ref_char_height = char_glyph.ref_char_height * ref_char_height_ratio
644                ref_char_width = char_glyph.ref_char_width * ref_char_width_ratio
645                box = char_box.box
646
647                up = box.up
648                down = box.down
649                if box.height < ref_char_height:
650                    inc = ref_char_height - box.height
651                    half_inc = inc / 2
652                    up = max(0, up - half_inc)
653                    down = min(page_height - 1, down + half_inc)
654
655                left = box.left
656                right = box.right
657                if box.width < ref_char_width:
658                    inc = ref_char_width - box.width
659                    half_inc = inc / 2
660                    left = max(0, left - half_inc)
661                    right = min(page_width - 1, right + half_inc)
662
663                polygons.append(self.build_char_polygon(
664                    up=up,
665                    down=down,
666                    left=left,
667                    right=right,
668                ))
669            return polygons
670
671        else:
672            polygons: List[Polygon] = []
673            for char_box, char_glyph in zip(self.char_boxes, self.char_glyphs):
674                ref_char_height = char_glyph.ref_char_height * ref_char_height_ratio
675                ref_char_width = char_glyph.ref_char_width * ref_char_width_ratio
676                box = char_box.box
677
678                left = box.left
679                right = box.right
680                if box.width < ref_char_height:
681                    inc = ref_char_height - box.width
682                    half_inc = inc / 2
683                    left = max(0, left - half_inc)
684                    right = min(page_width - 1, right + half_inc)
685
686                up = box.up
687                down = box.down
688                if box.height < ref_char_width:
689                    inc = ref_char_width - box.height
690                    half_inc = inc / 2
691                    up = max(self.box.up, up - half_inc)
692                    down = min(page_height - 1, down + half_inc)
693
694                polygons.append(self.build_char_polygon(
695                    up=up,
696                    down=down,
697                    left=left,
698                    right=right,
699                ))
700            return polygons
def get_height_points(self, num_points: int, is_up: bool):
702    def get_height_points(self, num_points: int, is_up: bool):
703        if self.is_hori:
704            step = max(1, self.box.width // num_points)
705            xs = list(range(0, self.box.right + 1, step))
706            if len(xs) >= num_points:
707                xs = xs[:num_points - 1]
708                xs.append(self.box.right)
709
710            points = PointList()
711            for x in xs:
712                if is_up:
713                    y = self.box.up
714                else:
715                    y = self.box.down
716                points.append(Point.create(y=y, x=x))
717            return points
718
719        else:
720            step = max(1, self.box.height // num_points)
721            ys = list(range(self.box.up, self.box.down + 1, step))
722            if len(ys) >= num_points:
723                ys = ys[:num_points - 1]
724                ys.append(self.box.down)
725
726            points = PointList()
727            for y in ys:
728                if is_up:
729                    x = self.box.right
730                else:
731                    x = self.box.left
732                points.append(Point.create(y=y, x=x))
733            return points
def get_char_level_height_points(self, is_up: bool):
735    def get_char_level_height_points(self, is_up: bool):
736        if self.is_hori:
737            points = PointList()
738            for char_box in self.char_boxes:
739                x = (char_box.left + char_box.right) / 2
740                if is_up:
741                    y = self.box.up
742                else:
743                    y = self.box.down
744                points.append(Point.create(y=y, x=x))
745            return points
746
747        else:
748            points = PointList()
749            for char_box in self.char_boxes:
750                y = (char_box.up + char_box.down) / 2
751                if is_up:
752                    x = self.box.right
753                else:
754                    x = self.box.left
755                points.append(Point.create(y=y, x=x))
756            return points