vkit.engine.seal_impression.text_line_slot_filler

  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 Sequence, List, Optional
 15import logging
 16
 17import numpy as np
 18import attrs
 19
 20from vkit.element import Point, Box, Polygon, ScoreMap
 21from vkit.engine.font import TextLine
 22from vkit.mechanism.distortion import rotate
 23from .type import SealImpression
 24
 25logger = logging.getLogger(__name__)
 26
 27
 28def fill_text_line_to_seal_impression(
 29    seal_impression: SealImpression,
 30    text_line_slot_indices: Sequence[int],
 31    text_lines: Sequence[TextLine],
 32    internal_text_line: Optional[TextLine],
 33):
 34    score_map = ScoreMap.from_shape(seal_impression.shape)
 35    char_polygons: List[Polygon] = []
 36
 37    assert len(text_line_slot_indices) == len(text_lines)
 38
 39    for text_line_slot_idx, text_line in zip(text_line_slot_indices, text_lines):
 40        if text_line_slot_idx >= len(seal_impression.text_line_slots):
 41            logger.error('fill_text_line_to_seal_impression: something wrong.')
 42            break
 43
 44        assert text_line.is_hori
 45        assert not text_line.shifted
 46
 47        # Get the text line slot to be filled.
 48        text_line_slot = seal_impression.text_line_slots[text_line_slot_idx]
 49
 50        # Get the reference char height of text line, for adjusting aspect ratio.
 51        text_line_ref_char_height = 0
 52        text_line_ref_char_width = 0
 53        for char_glyph in text_line.char_glyphs:
 54            if char_glyph.ref_char_height > text_line_ref_char_height:
 55                text_line_ref_char_height = char_glyph.ref_char_height
 56                text_line_ref_char_width = char_glyph.ref_char_width
 57        assert text_line_ref_char_height > 0 and text_line_ref_char_width > 0
 58        text_line_aspect_ratio = text_line_ref_char_width / text_line_ref_char_height
 59
 60        # For resizing.
 61        resized_char_width_factor = text_line_slot.char_aspect_ratio / text_line_aspect_ratio
 62
 63        # Fill each char to the char slot.
 64        for char_slot_idx, (char_box, char_glyph) \
 65                in enumerate(zip(text_line.char_boxes, text_line.char_glyphs)):
 66            # Get the char slot to be filled.
 67            if char_slot_idx >= len(text_line_slot.char_slots):
 68                logger.error('fill_text_line_to_seal_impression: something wrong.')
 69                break
 70
 71            char_slot = text_line_slot.char_slots[char_slot_idx]
 72
 73            # Convert char glyph to score map and adjust aspect ratio.
 74            # NOTE: Only the width of char could be resized.
 75            resized_width = max(1, round(resized_char_width_factor * char_glyph.width))
 76            resized_box = attrs.evolve(char_box.box, left=0, right=resized_width - 1)
 77            # NOTE: Since the height of char is fixed, we simply preserve the text line height.
 78            char_score_map = ScoreMap.from_shape((text_line.box.height, resized_width))
 79
 80            if char_glyph.score_map:
 81                char_glyph_score_map = char_glyph.score_map
 82                if char_glyph_score_map.shape != resized_box.shape:
 83                    char_glyph_score_map = char_glyph_score_map.to_resized_score_map(
 84                        resized_height=resized_box.height,
 85                        resized_width=resized_box.width,
 86                        cv_resize_interpolation=text_line.cv_resize_interpolation,
 87                    )
 88                resized_box.fill_score_map(char_score_map, char_glyph_score_map)
 89
 90            else:
 91                # LCD, fallback to mask.
 92                char_glyph_mask = char_glyph.get_glyph_mask(
 93                    box=char_box.box,
 94                    cv_resize_interpolation=text_line.cv_resize_interpolation,
 95                )
 96                if char_glyph_mask.shape != resized_box.shape:
 97                    char_glyph_mask = char_glyph_mask.to_resized_mask(
 98                        resized_height=resized_box.height,
 99                        resized_width=resized_box.width,
100                        cv_resize_interpolation=text_line.cv_resize_interpolation,
101                    )
102                resized_box.fill_score_map(char_score_map, char_glyph_mask.mat.astype(np.float32))
103
104            # To match char_slot.point_up.
105            point_up = Point.create(y=0, x=char_score_map.width / 2)
106
107            # Generate char polygon.
108            up = resized_box.up
109            down = resized_box.down
110            ref_char_height = char_glyph.ref_char_height
111            if resized_box.height < ref_char_height:
112                inc = ref_char_height - resized_box.height
113                half_inc = inc / 2
114                up = up - half_inc
115                down = down + half_inc
116
117            left = resized_box.left
118            right = resized_box.right
119            # NOTE: Resize factor is applied.
120            ref_char_width = resized_char_width_factor * char_glyph.ref_char_width
121            if resized_box.width < ref_char_width:
122                inc = ref_char_width - resized_box.width
123                half_inc = inc / 2
124                left = left - half_inc
125                right = right + half_inc
126
127            char_polygon = Polygon.from_xy_pairs([
128                (left, up),
129                (right, up),
130                (right, down),
131                (left, down),
132            ])
133
134            # Rotate.
135            rotated_result = rotate.distort(
136                # Horizontal text line has angle 270.
137                {'angle': char_slot.angle - 270},
138                score_map=char_score_map,
139                point=point_up,
140                polygon=char_polygon,
141                # The char polygon could be out-of-bound and should not be clipped.
142                disable_clip_result_elements=True,
143            )
144            rotated_char_score_map = rotated_result.score_map
145            assert rotated_char_score_map
146            rotated_point_up = rotated_result.point
147            assert rotated_point_up
148            rotated_char_polygon = rotated_result.polygon
149            assert rotated_char_polygon
150
151            # Calculate the bounding box based on point_up.
152            # NOTE: rotated_point_up.y represents the vertical offset here.
153            dst_up = char_slot.point_up.y - rotated_point_up.y
154            dst_down = dst_up + rotated_char_score_map.height - 1
155            # NOTE: rotated_point_up.x represents the horizontal offset here.
156            dst_left = char_slot.point_up.x - rotated_point_up.x
157            dst_right = dst_left + rotated_char_score_map.width - 1
158
159            if dst_up < 0 \
160                    or dst_down >= score_map.height \
161                    or dst_left < 0 \
162                    or dst_right >= score_map.width:
163                logger.error('fill_text_line_to_seal_impression: out-of-bound.')
164                continue
165
166            # Fill.
167            dst_box = Box(up=dst_up, down=dst_down, left=dst_left, right=dst_right)
168            dst_box.fill_score_map(
169                score_map,
170                rotated_char_score_map,
171                keep_max_value=True,
172            )
173
174            # Shift and keep rotated char polygon.
175            rotated_char_polygon = rotated_char_polygon.to_shifted_polygon(
176                offset_y=dst_up,
177                offset_x=dst_left,
178            )
179            char_polygons.append(rotated_char_polygon)
180
181    if internal_text_line:
182        internal_text_line_box = seal_impression.internal_text_line_box
183        assert internal_text_line_box
184
185        internal_text_line = internal_text_line.to_shifted_text_line(
186            offset_y=internal_text_line_box.up,
187            offset_x=internal_text_line_box.left,
188        )
189        if internal_text_line.score_map:
190            internal_text_line.box.fill_score_map(score_map, internal_text_line.score_map)
191        else:
192            internal_text_line.box.fill_score_map(score_map, internal_text_line.mask.mat)
193
194        char_polygons.extend(
195            internal_text_line.to_char_polygons(
196                page_height=score_map.height,
197                page_width=score_map.width,
198            )
199        )
200
201    # Adjust alpha.
202    score_map_max = score_map.mat.max()
203    score_map.assign_mat(score_map.mat * seal_impression.alpha / score_map_max)
204
205    return score_map, char_polygons
def fill_text_line_to_seal_impression( seal_impression: vkit.engine.seal_impression.type.SealImpression, text_line_slot_indices: Sequence[int], text_lines: Sequence[vkit.engine.font.type.TextLine], internal_text_line: Union[vkit.engine.font.type.TextLine, NoneType]):
 29def fill_text_line_to_seal_impression(
 30    seal_impression: SealImpression,
 31    text_line_slot_indices: Sequence[int],
 32    text_lines: Sequence[TextLine],
 33    internal_text_line: Optional[TextLine],
 34):
 35    score_map = ScoreMap.from_shape(seal_impression.shape)
 36    char_polygons: List[Polygon] = []
 37
 38    assert len(text_line_slot_indices) == len(text_lines)
 39
 40    for text_line_slot_idx, text_line in zip(text_line_slot_indices, text_lines):
 41        if text_line_slot_idx >= len(seal_impression.text_line_slots):
 42            logger.error('fill_text_line_to_seal_impression: something wrong.')
 43            break
 44
 45        assert text_line.is_hori
 46        assert not text_line.shifted
 47
 48        # Get the text line slot to be filled.
 49        text_line_slot = seal_impression.text_line_slots[text_line_slot_idx]
 50
 51        # Get the reference char height of text line, for adjusting aspect ratio.
 52        text_line_ref_char_height = 0
 53        text_line_ref_char_width = 0
 54        for char_glyph in text_line.char_glyphs:
 55            if char_glyph.ref_char_height > text_line_ref_char_height:
 56                text_line_ref_char_height = char_glyph.ref_char_height
 57                text_line_ref_char_width = char_glyph.ref_char_width
 58        assert text_line_ref_char_height > 0 and text_line_ref_char_width > 0
 59        text_line_aspect_ratio = text_line_ref_char_width / text_line_ref_char_height
 60
 61        # For resizing.
 62        resized_char_width_factor = text_line_slot.char_aspect_ratio / text_line_aspect_ratio
 63
 64        # Fill each char to the char slot.
 65        for char_slot_idx, (char_box, char_glyph) \
 66                in enumerate(zip(text_line.char_boxes, text_line.char_glyphs)):
 67            # Get the char slot to be filled.
 68            if char_slot_idx >= len(text_line_slot.char_slots):
 69                logger.error('fill_text_line_to_seal_impression: something wrong.')
 70                break
 71
 72            char_slot = text_line_slot.char_slots[char_slot_idx]
 73
 74            # Convert char glyph to score map and adjust aspect ratio.
 75            # NOTE: Only the width of char could be resized.
 76            resized_width = max(1, round(resized_char_width_factor * char_glyph.width))
 77            resized_box = attrs.evolve(char_box.box, left=0, right=resized_width - 1)
 78            # NOTE: Since the height of char is fixed, we simply preserve the text line height.
 79            char_score_map = ScoreMap.from_shape((text_line.box.height, resized_width))
 80
 81            if char_glyph.score_map:
 82                char_glyph_score_map = char_glyph.score_map
 83                if char_glyph_score_map.shape != resized_box.shape:
 84                    char_glyph_score_map = char_glyph_score_map.to_resized_score_map(
 85                        resized_height=resized_box.height,
 86                        resized_width=resized_box.width,
 87                        cv_resize_interpolation=text_line.cv_resize_interpolation,
 88                    )
 89                resized_box.fill_score_map(char_score_map, char_glyph_score_map)
 90
 91            else:
 92                # LCD, fallback to mask.
 93                char_glyph_mask = char_glyph.get_glyph_mask(
 94                    box=char_box.box,
 95                    cv_resize_interpolation=text_line.cv_resize_interpolation,
 96                )
 97                if char_glyph_mask.shape != resized_box.shape:
 98                    char_glyph_mask = char_glyph_mask.to_resized_mask(
 99                        resized_height=resized_box.height,
100                        resized_width=resized_box.width,
101                        cv_resize_interpolation=text_line.cv_resize_interpolation,
102                    )
103                resized_box.fill_score_map(char_score_map, char_glyph_mask.mat.astype(np.float32))
104
105            # To match char_slot.point_up.
106            point_up = Point.create(y=0, x=char_score_map.width / 2)
107
108            # Generate char polygon.
109            up = resized_box.up
110            down = resized_box.down
111            ref_char_height = char_glyph.ref_char_height
112            if resized_box.height < ref_char_height:
113                inc = ref_char_height - resized_box.height
114                half_inc = inc / 2
115                up = up - half_inc
116                down = down + half_inc
117
118            left = resized_box.left
119            right = resized_box.right
120            # NOTE: Resize factor is applied.
121            ref_char_width = resized_char_width_factor * char_glyph.ref_char_width
122            if resized_box.width < ref_char_width:
123                inc = ref_char_width - resized_box.width
124                half_inc = inc / 2
125                left = left - half_inc
126                right = right + half_inc
127
128            char_polygon = Polygon.from_xy_pairs([
129                (left, up),
130                (right, up),
131                (right, down),
132                (left, down),
133            ])
134
135            # Rotate.
136            rotated_result = rotate.distort(
137                # Horizontal text line has angle 270.
138                {'angle': char_slot.angle - 270},
139                score_map=char_score_map,
140                point=point_up,
141                polygon=char_polygon,
142                # The char polygon could be out-of-bound and should not be clipped.
143                disable_clip_result_elements=True,
144            )
145            rotated_char_score_map = rotated_result.score_map
146            assert rotated_char_score_map
147            rotated_point_up = rotated_result.point
148            assert rotated_point_up
149            rotated_char_polygon = rotated_result.polygon
150            assert rotated_char_polygon
151
152            # Calculate the bounding box based on point_up.
153            # NOTE: rotated_point_up.y represents the vertical offset here.
154            dst_up = char_slot.point_up.y - rotated_point_up.y
155            dst_down = dst_up + rotated_char_score_map.height - 1
156            # NOTE: rotated_point_up.x represents the horizontal offset here.
157            dst_left = char_slot.point_up.x - rotated_point_up.x
158            dst_right = dst_left + rotated_char_score_map.width - 1
159
160            if dst_up < 0 \
161                    or dst_down >= score_map.height \
162                    or dst_left < 0 \
163                    or dst_right >= score_map.width:
164                logger.error('fill_text_line_to_seal_impression: out-of-bound.')
165                continue
166
167            # Fill.
168            dst_box = Box(up=dst_up, down=dst_down, left=dst_left, right=dst_right)
169            dst_box.fill_score_map(
170                score_map,
171                rotated_char_score_map,
172                keep_max_value=True,
173            )
174
175            # Shift and keep rotated char polygon.
176            rotated_char_polygon = rotated_char_polygon.to_shifted_polygon(
177                offset_y=dst_up,
178                offset_x=dst_left,
179            )
180            char_polygons.append(rotated_char_polygon)
181
182    if internal_text_line:
183        internal_text_line_box = seal_impression.internal_text_line_box
184        assert internal_text_line_box
185
186        internal_text_line = internal_text_line.to_shifted_text_line(
187            offset_y=internal_text_line_box.up,
188            offset_x=internal_text_line_box.left,
189        )
190        if internal_text_line.score_map:
191            internal_text_line.box.fill_score_map(score_map, internal_text_line.score_map)
192        else:
193            internal_text_line.box.fill_score_map(score_map, internal_text_line.mask.mat)
194
195        char_polygons.extend(
196            internal_text_line.to_char_polygons(
197                page_height=score_map.height,
198                page_width=score_map.width,
199            )
200        )
201
202    # Adjust alpha.
203    score_map_max = score_map.mat.max()
204    score_map.assign_mat(score_map.mat * seal_impression.alpha / score_map_max)
205
206    return score_map, char_polygons