13
13
14
14
@IROperationDataclassWithValue (UIAssetElementType )
15
15
class UIAssetTextElementOp (UIAssetElementNodeOp ):
16
- # 文字元素
16
+ # 文字元素,锚点在左上角(可能文字顶部离锚点还有段距离)
17
+ # 具体锚点位置是 https://pillow.readthedocs.io/en/stable/handbook/text-anchors.html#text-anchors 中的 la
17
18
# 如果要对同一张图做不同语言的版本,应该有不同的 UIAssetTextElementOp
18
19
fontpath : OpOperand [StringLiteral ] # 如果为空就用默认字体
19
20
fontindex : OpOperand [IntLiteral ]
20
21
fontsize : OpOperand [IntLiteral ]
21
22
fontcolor : OpOperand [ColorLiteral ]
22
23
text : OpOperand [StringLiteral ]
23
24
25
+ @staticmethod
26
+ def create (context : Context , text : StringLiteral | str , fontsize : IntLiteral | int , fontcolor : ColorLiteral | Color , fontpath : StringLiteral | str | None = None , fontindex : IntLiteral | int | None = None ):
27
+ text_value = StringLiteral .get (text , context = context ) if isinstance (text , str ) else text
28
+ fontsize_value = IntLiteral .get (fontsize , context = context ) if isinstance (fontsize , int ) else fontsize
29
+ fontcolor_value = ColorLiteral .get (fontcolor , context = context ) if isinstance (fontcolor , Color ) else fontcolor
30
+ fontpath_value = StringLiteral .get (fontpath , context = context ) if isinstance (fontpath , str ) else fontpath
31
+ fontindex_value = IntLiteral .get (fontindex , context = context ) if isinstance (fontindex , int ) else fontindex
32
+ return UIAssetTextElementOp (init_mode = IRObjectInitMode .CONSTRUCT , context = context , fontpath = fontpath_value , fontindex = fontindex_value , fontsize = fontsize_value , fontcolor = fontcolor_value , text = text_value )
33
+
24
34
def get_font (self ) -> PIL .ImageFont .FreeTypeFont | PIL .ImageFont .ImageFont :
25
35
size = self .fontsize .get ().value
26
36
index = 0
27
- if index_l := self .fontindex .get ():
37
+ if index_l := self .fontindex .try_get_value ():
28
38
index = index_l .value
29
- if path := self .fontpath .get (). value :
30
- return PIL .ImageFont .truetype (path , index = index , size = size )
39
+ if path := self .fontpath .try_get_value () :
40
+ return PIL .ImageFont .truetype (path . value , index = index , size = size )
31
41
return AssetManager .get_font (fontsize = size )
32
42
33
- def get_bbox (self ) -> tuple [int , int , int , int ]:
43
+ def get_bbox_impl (self , font : PIL . ImageFont . FreeTypeFont | PIL . ImageFont . ImageFont ) -> tuple [int , int , int , int ]:
34
44
font = self .get_font ()
35
45
s = self .text .get ().value
36
- left , top , right , bottom = font .getbbox (s )
37
- return (math .floor (left ), math .floor (top ), math .ceil (right ), math .ceil (bottom ))
46
+ if isinstance (font , PIL .ImageFont .FreeTypeFont ):
47
+ left , top , right , bottom = font .getbbox (s , anchor = 'la' )
48
+ return (math .floor (left ), math .floor (top ), math .ceil (right ), math .ceil (bottom ))
49
+ elif isinstance (font , PIL .ImageFont .ImageFont ):
50
+ return font .getbbox (s )
51
+ else :
52
+ raise PPInternalError (f'Unexpected type of font: { type (font )} ' )
53
+
54
+ def get_bbox (self ) -> tuple [int , int , int , int ]:
55
+ return self .get_bbox_impl (self .get_font ())
38
56
39
57
def draw (self , drawctx : UIAssetDrawingContext ) -> UIAssetElementDrawingData :
40
58
font = self .get_font ()
41
59
s = self .text .get ().value
42
- left , top , right , bottom = font .getbbox (s )
43
- left = math .floor (left )
44
- top = math .floor (top )
45
- right = math .ceil (right )
46
- bottom = math .ceil (bottom )
60
+ left , top , right , bottom = self .get_bbox_impl (font )
47
61
width = right - left
48
62
height = bottom - top
49
63
color = self .fontcolor .get ().value
50
64
image = PIL .Image .new ('RGBA' , (width , height ), (0 , 0 , 0 , 0 ))
51
65
draw = PIL .ImageDraw .Draw (image )
52
- draw .text ((- left , - top ), s , font = font , fill = color .to_tuple ())
66
+ draw .text ((- left , - top ), s , font = font , fill = color .to_tuple (), anchor = 'la' )
67
+ print (f"Original bbox: { left } , { top } , { right } , { bottom } ; actual bbox: { str (image .getbbox ())} " )
53
68
description = "<default_font>"
54
- if path := self .fontpath .get (). value :
55
- description = path
56
- if index_l := self .fontindex .get ():
69
+ if path := self .fontpath .try_get_value () :
70
+ description = path . value
71
+ if index_l := self .fontindex .try_get_value ():
57
72
index = index_l .value
58
73
if index != 0 :
59
74
description += f"#{ index } "
60
75
description += ", size=" + str (self .fontsize .get ().value )
61
- return UIAssetElementDrawingData (image = image , description = description , anchor = (left , top ))
76
+ return UIAssetElementDrawingData (image = image , description = description , anchor = (- left , - top ))
77
+
78
+ @dataclasses .dataclass
79
+ class UIAssetLineLoopDrawingData (UIAssetElementDrawingData ):
80
+ # 除了基类的属性外,我们在这还存储描述边框的信息
81
+ mask_interior : PIL .Image .Image | None = None # L (灰度)模式的图片,非零的部分表示内部(用于区域填充等),可作为 alpha 通道
82
+
83
+ def sanity_check (self ):
84
+ sized_image_list = []
85
+ if self .image is not None :
86
+ sized_image_list .append (self .image )
87
+ if self .mask_interior is not None :
88
+ if not isinstance (self .mask_interior , PIL .Image .Image ):
89
+ raise PPInternalError (f'Unexpected type of mask_interior: { type (self .mask_interior )} ' )
90
+ if self .mask_interior .mode != 'L' :
91
+ raise PPInternalError (f'Unexpected mode of mask_interior: { self .mask_interior .mode } ' )
92
+ if len (sized_image_list ) > 1 :
93
+ size = sized_image_list [0 ].size
94
+ for image in sized_image_list [1 :]:
95
+ if image .size != size :
96
+ raise PPInternalError (f'Inconsistent image size: { image .size } and { size } ' )
97
+
62
98
63
99
@IROperationDataclassWithValue (UIAssetElementType )
64
100
class UIAssetLineLoopOp (UIAssetElementNodeOp ):
65
- pass
101
+ # 用于描述一条或多条线段组成的闭合区域
102
+ # 由于大部分情况下我们有更简单的表示(比如就一个矩形),该类只作为基类,不应该直接使用
103
+ def draw (self , drawctx : UIAssetDrawingContext ) -> UIAssetLineLoopDrawingData :
104
+ return UIAssetLineLoopDrawingData ()
105
+
106
+ @IROperationDataclassWithValue (UIAssetElementType )
107
+ class UIAssetRectangleOp (UIAssetLineLoopOp ):
108
+ width : OpOperand [IntLiteral ]
109
+ height : OpOperand [IntLiteral ]
110
+
111
+ def get_bbox (self ) -> tuple [int , int , int , int ]:
112
+ return (0 , 0 , self .width .get ().value , self .height .get ().value )
113
+
114
+ def draw (self , drawctx : UIAssetDrawingContext ) -> UIAssetLineLoopDrawingData :
115
+ result = UIAssetLineLoopDrawingData ()
116
+ result .mask_interior = PIL .Image .new ('L' , (self .width .get ().value , self .height .get ().value ), 255 )
117
+ return result
118
+
119
+ @staticmethod
120
+ def create (context : Context , width : IntLiteral | int , height : IntLiteral | int ):
121
+ width_value = IntLiteral .get (width , context = context ) if isinstance (width , int ) else width
122
+ height_value = IntLiteral .get (height , context = context ) if isinstance (height , int ) else height
123
+ return UIAssetRectangleOp (init_mode = IRObjectInitMode .CONSTRUCT , context = context , width = width_value , height = height_value )
66
124
67
125
class UIAssetAreaFillMode (enum .Enum ):
68
126
COLOR_FILL = enum .auto () # 单色填充
@@ -72,6 +130,36 @@ class UIAssetAreaFillElementOp(UIAssetElementNodeOp):
72
130
# 区域填充
73
131
# TODO 添加渐变填充等功能
74
132
133
+ boundary : OpOperand [UIAssetLineLoopOp ]
75
134
mode : OpOperand [EnumLiteral [UIAssetAreaFillMode ]]
76
135
color1 : OpOperand [ColorLiteral ]
77
136
color2 : OpOperand [ColorLiteral ] # 目前不用
137
+
138
+ @staticmethod
139
+ def create (context : Context , boundary : UIAssetLineLoopOp , mode : UIAssetAreaFillMode , color1 : ColorLiteral | Color , color2 : ColorLiteral | Color | None = None ):
140
+ mode_value = EnumLiteral .get (value = mode , context = context )
141
+ color1_value = ColorLiteral .get (color1 , context = context ) if isinstance (color1 , Color ) else color1
142
+ color2_value = ColorLiteral .get (color2 , context = context ) if isinstance (color2 , Color ) else color2
143
+ return UIAssetAreaFillElementOp (init_mode = IRObjectInitMode .CONSTRUCT , context = context , boundary = boundary , mode = mode_value , color1 = color1_value , color2 = color2_value )
144
+
145
+ def get_bbox (self ) -> tuple [int , int , int , int ] | None :
146
+ return self .boundary .get ().get_bbox ()
147
+
148
+ def draw (self , drawctx : UIAssetDrawingContext ) -> UIAssetElementDrawingData :
149
+ boundary = self .boundary .get ()
150
+ boundary_data , _ = drawctx .get_drawing_data (boundary )
151
+ if not isinstance (boundary_data , UIAssetLineLoopDrawingData ):
152
+ raise PPInternalError (f'Unexpected type of boundary_data: { type (boundary_data )} ' )
153
+ if boundary_data .mask_interior is None :
154
+ raise PPInternalError ('Boundary mask_interior is None' )
155
+ match self .mode .get ().value :
156
+ case UIAssetAreaFillMode .COLOR_FILL :
157
+ color = self .color1 .get ().value
158
+ result = UIAssetLineLoopDrawingData ()
159
+ result .image = PIL .Image .new ('RGBA' , boundary_data .mask_interior .size , (0 , 0 , 0 , 0 ))
160
+ draw = PIL .ImageDraw .Draw (result .image )
161
+ draw .bitmap ((0 , 0 ), boundary_data .mask_interior , fill = color .to_tuple ())
162
+ result .description = f'ColorFill({ color .get_string ()} )'
163
+ return result
164
+ case _:
165
+ raise PPNotImplementedError ()
0 commit comments