linecard 0.2.0.post1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 KarisAya
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,32 @@
1
+ Metadata-Version: 2.3
2
+ Name: linecard
3
+ Version: 0.2.0.post1
4
+ Summary:
5
+ Author: KarisAya
6
+ Author-email: karisaya@foxmail.com
7
+ Requires-Python: >=3.12
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.12
10
+ Classifier: Programming Language :: Python :: 3.13
11
+ Requires-Dist: fonttools (>=4.58.0,<5.0.0)
12
+ Requires-Dist: pillow (>=11.1.0,<12.0.0)
13
+ Description-Content-Type: text/markdown
14
+
15
+ <div align="center">
16
+
17
+ # linecard
18
+
19
+ _✨ linecard 是一个基于 pillow 的一个方便文字转图片的函数。 ✨_
20
+
21
+ [![Python](https://img.shields.io/badge/Python-3.12+-blue.svg)](https://www.python.org/)
22
+ [![pypi](https://img.shields.io/pypi/v/linecard.svg)](https://pypi.python.org/pypi/linecard)
23
+ [![pypi download](https://img.shields.io/pypi/dm/linecard)](https://pypi.python.org/pypi/linecard)
24
+ [![Poetry](https://img.shields.io/endpoint?url=https://python-poetry.org/badge/v0.json)](https://python-poetry.org/)
25
+ [![LICENSE](https://img.shields.io/github/license/karisaya/linecard.svg)](./LICENSE)
26
+
27
+ </div>
28
+
29
+ [使用方法见示例文件](/example/main.py)
30
+
31
+ ![效果](/example/example.png)
32
+
@@ -0,0 +1,17 @@
1
+ <div align="center">
2
+
3
+ # linecard
4
+
5
+ _✨ linecard 是一个基于 pillow 的一个方便文字转图片的函数。 ✨_
6
+
7
+ [![Python](https://img.shields.io/badge/Python-3.12+-blue.svg)](https://www.python.org/)
8
+ [![pypi](https://img.shields.io/pypi/v/linecard.svg)](https://pypi.python.org/pypi/linecard)
9
+ [![pypi download](https://img.shields.io/pypi/dm/linecard)](https://pypi.python.org/pypi/linecard)
10
+ [![Poetry](https://img.shields.io/endpoint?url=https://python-poetry.org/badge/v0.json)](https://python-poetry.org/)
11
+ [![LICENSE](https://img.shields.io/github/license/karisaya/linecard.svg)](./LICENSE)
12
+
13
+ </div>
14
+
15
+ [使用方法见示例文件](/example/main.py)
16
+
17
+ ![效果](/example/example.png)
@@ -0,0 +1,582 @@
1
+ import os
2
+ import sys
3
+ from pathlib import Path
4
+ from collections import deque
5
+ from collections.abc import Iterable
6
+ from fontTools.ttLib import TTFont
7
+ from PIL import Image, ImageDraw, ImageFont, ImageFilter
8
+ from PIL.ImageFont import FreeTypeFont
9
+ from PIL.Image import Image as IMG
10
+ from typing import cast, TypedDict, Literal, Protocol, TypeGuard
11
+ from .linecard_parsing import parse_str
12
+
13
+ match sys.platform:
14
+ case "win32":
15
+ FONT_PATHS = [
16
+ "C:/Windows/Fonts",
17
+ f"{os.environ["LOCALAPPDATA"]}/Microsoft/Windows/Fonts",
18
+ ]
19
+ case "darwin":
20
+ FONT_PATHS = ["/Library/Fonts"]
21
+ case "linux":
22
+ FONT_PATHS = ["/usr/share/fonts"]
23
+ case _:
24
+ FONT_PATHS = os.environ["LINECARD_FONT_PATH"].split(";")
25
+
26
+
27
+ def find_font(font_name: str, search_paths: Iterable[Path] = [Path(FONT_PATH) for FONT_PATH in FONT_PATHS]):
28
+ def check_font(font_file: Path, font_name: str):
29
+ suffix = font_file.suffix.lower()
30
+ if not suffix.endswith((".ttf", ".otf", ".ttc")):
31
+ return False
32
+ if not font_name.lower() == font_file.stem.lower():
33
+ return False
34
+ try:
35
+ TTFont(font_file, recalcBBoxes=False, recalcTimestamp=False, fontNumber=0)
36
+ except:
37
+ return False
38
+ return True
39
+
40
+ try:
41
+ TTFont(font_name, recalcBBoxes=False, recalcTimestamp=False, fontNumber=0)
42
+ return Path(font_name).absolute().as_posix()
43
+ except:
44
+ pass
45
+ for search_path in search_paths:
46
+ if not search_path.exists():
47
+ continue
48
+ for file in search_path.iterdir():
49
+ if check_font(file, font_name):
50
+ return file.absolute().as_posix()
51
+ return None
52
+
53
+
54
+ def to_int(n: str | None):
55
+ try:
56
+ return int(n) if n else None
57
+ except ValueError:
58
+ pass
59
+
60
+
61
+ def line_wrap(line: str, width: int, font: FreeTypeFont, start: float = 0.0) -> str:
62
+ """调整文字排版到指定宽度,并添加换行符
63
+
64
+ Args:
65
+ line (str): 待排版的文本,一般是
66
+ width: 文字换行宽度
67
+ font (FreeTypeFont): 字体
68
+ start: 首行起始位置 default to 0.0
69
+
70
+ Returns:
71
+ str: 排版后的文本
72
+ """
73
+
74
+ text_x = start
75
+ new_str = ""
76
+ for char in line:
77
+ if char == "\n":
78
+ new_str += "\n"
79
+ text_x = 0
80
+ else:
81
+ char_lenth = font.getlength(char)
82
+ text_x += char_lenth
83
+ if text_x > width:
84
+ new_str += "\n" + char
85
+ text_x = char_lenth
86
+ else:
87
+ new_str += char
88
+ return new_str
89
+
90
+
91
+ def CropResize(img: IMG, size: tuple[int, int]) -> IMG:
92
+ """
93
+ 修改图像尺寸
94
+ """
95
+
96
+ test_x = img.size[0] / size[0]
97
+ test_y = img.size[1] / size[1]
98
+
99
+ if test_x < test_y:
100
+ width = img.size[0]
101
+ height = size[1] * test_x
102
+ else:
103
+ width = size[0] * test_y
104
+ height = img.size[1]
105
+
106
+ center = (img.size[0] / 2, img.size[1] / 2)
107
+ output = img.crop(
108
+ (
109
+ int(center[0] - width / 2),
110
+ int(center[1] - height / 2),
111
+ int(center[0] + width / 2),
112
+ int(center[1] + height / 2),
113
+ )
114
+ )
115
+ output = output.resize(size)
116
+ return output
117
+
118
+
119
+ type ImageList = list[IMG]
120
+
121
+
122
+ class CanvasEffectHandler(Protocol):
123
+ """图片蒙版效果处理器
124
+
125
+ Args:
126
+ canvas (IMG): 被处理的背景图片
127
+ image (IMG): 被处理的前景图片
128
+ padding (int): 图片边距
129
+ x (int): image 起始像素 x 轴坐标
130
+ y (int): image 起始像素 y 轴坐标
131
+
132
+ Returns:
133
+ None
134
+ """
135
+
136
+ def __call__(self, canvas: IMG, image: IMG, padding: int, x: int, y: int) -> None: ...
137
+
138
+
139
+ def info_splicing(
140
+ info: ImageList,
141
+ BG_path: str | Path | None = None,
142
+ width: int = 880,
143
+ padding: int = 20,
144
+ spacing: int = 20,
145
+ BG_type: str | CanvasEffectHandler = "GAUSS",
146
+ ) -> IMG:
147
+ """
148
+ 信息拼接
149
+ info:信息图片列表
150
+ bg_path:背景地址
151
+ """
152
+
153
+ height = padding
154
+ for image in info:
155
+ # x = image.size[0] if x < image.size[0] else x
156
+ height += image.size[1]
157
+ height += spacing * 2
158
+ else:
159
+ height = height - spacing + padding
160
+
161
+ size = (width + padding * 2, height)
162
+ if BG_path is not None and ((BG_path := Path(BG_path)) if isinstance(BG_path, str) else BG_path).exists():
163
+ bg = Image.open(BG_path).convert("RGB")
164
+ canvas = CropResize(bg, size)
165
+ else:
166
+ canvas = Image.new("RGB", size, "white")
167
+ BG_type = "NONE"
168
+
169
+ CanvasEffect: CanvasEffectHandler
170
+
171
+ if isinstance(BG_type, str):
172
+ if BG_type == "NONE":
173
+
174
+ def BG(canvas: IMG, image: IMG, padding: int, x: int, y: int):
175
+ canvas.paste(image, (padding, y), mask=image)
176
+
177
+ elif BG_type.startswith("GAUSS"):
178
+ arg = BG_type.split(":")
179
+ if len(arg) > 1:
180
+ try:
181
+ radius = int(arg[1])
182
+ except ValueError:
183
+ radius = 4
184
+ else:
185
+ radius = 4
186
+
187
+ def BG(canvas: IMG, image: IMG, padding: int, x: int, y: int):
188
+ box = (padding, y, x + padding, y + image.size[1])
189
+ region = canvas.crop(box)
190
+ blurred_region = region.filter(ImageFilter.GaussianBlur(radius=radius))
191
+ canvas.paste(blurred_region, box)
192
+ canvas.paste(image, (padding, y), mask=image)
193
+
194
+ else:
195
+
196
+ def BG(canvas: IMG, image: IMG, padding: int, x: int, y: int):
197
+ colorBG = Image.new("RGBA", (x, image.size[1]), BG_type)
198
+ canvas.paste(colorBG, (padding, y), mask=colorBG)
199
+ canvas.paste(image, (padding, y), mask=image)
200
+
201
+ CanvasEffect = BG
202
+ else:
203
+ CanvasEffect = BG_type
204
+
205
+ height = padding
206
+
207
+ for image in info:
208
+ CanvasEffect(canvas, image, padding, width, height)
209
+ height += image.size[1] + spacing * 2
210
+
211
+ return canvas
212
+
213
+
214
+ class Linecard:
215
+ """
216
+ 文本标记
217
+ ----:横线
218
+ [left]靠左
219
+ [right]靠右
220
+ [center]居中
221
+ [pixel 400]指定像素
222
+ [font size = 50,name = simsun,color = red,highlight = yellow]指定文本格式
223
+ [style **kwargs] 控制参数
224
+ height: 行高
225
+ width: 行宽
226
+ color: 本行颜色
227
+ [nowrap]不换行
228
+ [passport]保持标记
229
+ [autowrap]自动换行
230
+ [noautowrap]不自动换行
231
+ """
232
+
233
+ def __init__(self, font_name: str, fallback: list[str], sizes: Iterable[int] | None = None) -> None:
234
+ path = find_font(font_name)
235
+ if not path:
236
+ raise ValueError(f"Font:{font_name} not found")
237
+ self.font_path = path
238
+ self.font_cache: dict[str, dict[int, ImageFont.FreeTypeFont]] = {}
239
+ self.font_cache = {}
240
+ fallback_paths: list[str] = [path for font in fallback if (path := find_font(font))]
241
+ self.cmaps = {fallback_path: TTFont(fallback_path, fontNumber=0).getBestCmap() for fallback_path in fallback_paths}
242
+ self.cmaps = {k: v for k, v in self.cmaps.items() if v is not None}
243
+ self.fallback_paths = list(self.cmaps.keys())
244
+ if sizes:
245
+ for size in sizes:
246
+ self.get_font(self.font_path, size)
247
+
248
+ def get_font(self, name: str, size: int):
249
+ try:
250
+ font_cache = self.font_cache.get(name)
251
+ if font_cache:
252
+ if size in font_cache:
253
+ font = font_cache[size]
254
+ else:
255
+ font = font_cache[size] = ImageFont.truetype(font=name, size=size, encoding="utf-8")
256
+ else:
257
+ font = ImageFont.truetype(font=name, size=size, encoding="utf-8")
258
+ name = Path(cast(str, font.path)).absolute().as_posix()
259
+ self.font_cache.setdefault(name, {})[size] = font
260
+ if name in self.cmaps:
261
+ cmap = self.cmaps[name]
262
+ else:
263
+ cmap = self.cmaps[name] = TTFont(name, fontNumber=font.index).getBestCmap()
264
+ return font, cast(dict[int, int], cmap)
265
+ except OSError:
266
+ return
267
+
268
+ class CharSingle(TypedDict):
269
+ char: str
270
+ color: str
271
+ y: int
272
+ x: int
273
+ end_x: int
274
+ font: FreeTypeFont
275
+ align: str
276
+ highlight: str | None
277
+
278
+ class CharLine(TypedDict):
279
+ char: Literal["----"]
280
+ color: str
281
+ y: int
282
+ size: int
283
+ color: str
284
+
285
+ type CharStyle = CharSingle | CharLine
286
+ type CharStyleList = list[CharStyle]
287
+
288
+ @staticmethod
289
+ def is_charsingle(chaestyle: CharStyle) -> TypeGuard[CharSingle]:
290
+ return chaestyle["char"] != "----"
291
+
292
+ def __call__(
293
+ self,
294
+ text: str,
295
+ font_size: int,
296
+ width: int | None = None,
297
+ height: int | None = None,
298
+ padding: tuple[int, int] = (20, 20),
299
+ spacing: float = 1.2,
300
+ color: str = "black",
301
+ bg_color: str | None = None,
302
+ autowrap: bool = False,
303
+ canvas: IMG | None = None,
304
+ ) -> IMG:
305
+ font_cmap = self.get_font(self.font_path, font_size)
306
+ assert font_cmap is not None, "字体文件不存在"
307
+ text, tags = parse_str(text)
308
+ tags = deque(tags)
309
+ padding_x, padding_y = padding
310
+
311
+ charlist: Linecard.CharStyleList = []
312
+
313
+ wrap_width: int = width - padding_x if width else 0
314
+ absolute_spacing: int = int(font_size * (spacing - 1.0) + 0.5)
315
+
316
+ line_height: int = 0
317
+ line_width: int = 0
318
+
319
+ line_align: str = "left"
320
+ line_font, line_cmap = font_cmap
321
+
322
+ line_passport: bool = False
323
+ line_autowrap: bool = False
324
+ line_nowrap: bool = False
325
+
326
+ line_color: str = color
327
+ line_highlight: str | None = None
328
+
329
+ inline_height: int = 0
330
+
331
+ def line_init() -> None:
332
+ nonlocal line_height, line_width, line_align, line_font, line_cmap, line_passport, line_autowrap, line_nowrap, line_color, line_highlight, inline_height
333
+ line_height = font_size
334
+ line_width = wrap_width
335
+
336
+ line_align = line_align if line_nowrap else "left"
337
+
338
+ line_font, line_cmap = font_cmap
339
+
340
+ line_passport = False
341
+ line_autowrap = autowrap
342
+ line_nowrap = False
343
+
344
+ line_color = color
345
+ line_highlight = None
346
+
347
+ inline_height = 0
348
+
349
+ x: int = 0
350
+ max_x: int = 0
351
+ y: int = 0
352
+
353
+ for line in text.split("\n"):
354
+ # 检查继承格式
355
+ if line_passport:
356
+ line_passport = False
357
+ else:
358
+ line_init()
359
+ tmp_height: int = 0
360
+ for unit_rawline in line.split("{"):
361
+ # 渲染行单位存在格式标签
362
+ if unit_rawline.startswith("}"):
363
+ unit_line = unit_rawline = unit_rawline[1:]
364
+ tag, param = tags.popleft()
365
+ match tag:
366
+ # 原样输出字符串
367
+ case b"r":
368
+ unit_line = param + unit_rawline
369
+ # 对齐标签
370
+ case b"a":
371
+ unit_align = param
372
+ if line_align != unit_align:
373
+ x = 0
374
+ line_align = unit_align
375
+ # 字体标签
376
+ case b"f":
377
+ fontkwargs = {k: v for k, v in [x.split("=", 1) for x in param.replace(" ", "").split(",")]}
378
+ if unit_font_name := fontkwargs.get("name"):
379
+ unit_font_name = find_font(unit_font_name)
380
+ if unit_font_size := fontkwargs.get("size"):
381
+ unit_font_size = to_int(unit_font_size)
382
+ if unit_font_size:
383
+ if not unit_font_name:
384
+ unit_font_name = cast(str, line_font.path)
385
+ line_font_cmap = self.get_font(unit_font_name, unit_font_size)
386
+ if line_font_cmap:
387
+ line_font, line_cmap = line_font_cmap
388
+ elif unit_font_name:
389
+ unit_font_size = int(line_font.size)
390
+ line_font_cmap = self.get_font(unit_font_name, unit_font_size)
391
+ if line_font_cmap:
392
+ line_font, line_cmap = line_font_cmap
393
+ line_color = fontkwargs.get("color", line_color)
394
+ line_highlight = fontkwargs.get("highlight")
395
+ # 样式标签
396
+ case b"s":
397
+ stylekwargs = {k: v for k, v in [x.split("=", 1) for x in param.replace(" ", "").split(",")]}
398
+ if unit_height := stylekwargs.get("height"):
399
+ line_height = to_int(unit_height) or line_height
400
+ if unit_width := stylekwargs.get("width"):
401
+ line_width = to_int(unit_width) or line_height
402
+ line_color = stylekwargs.get("color", color)
403
+ case b"t":
404
+ match param:
405
+ case "nowrap":
406
+ line_nowrap = True
407
+ case "autowrap":
408
+ line_autowrap = True
409
+ case "noautowrap":
410
+ line_autowrap = False
411
+ case "passport":
412
+ line_passport = True
413
+ else:
414
+ unit_line = unit_rawline
415
+ # 渲染行单位格式标签外的文本
416
+ if not unit_line:
417
+ continue
418
+ elif unit_rawline == "----":
419
+ line_height = line_height or font_size
420
+ charlist.append({"char": "----", "color": line_color, "y": y, "size": line_height})
421
+ x = 0
422
+ else:
423
+ inline_height = int(line_font.size)
424
+ if line_width and line_autowrap:
425
+ if line_align in ("left", "right", "center"):
426
+ start_x = x
427
+ char_align = 0
428
+ else:
429
+ char_align = to_int(line_align) or 0
430
+ start_x = char_align + x
431
+ if line_font.getlength(unit_line) > line_width - start_x:
432
+ if (inner_wrap_width := line_width - char_align) > inline_height:
433
+ unit_line = line_wrap(unit_line, inner_wrap_width, line_font, x)
434
+ line_seg = unit_line.split("\n")
435
+ line_seg_l = len(line_seg)
436
+ for i, seg in enumerate(line_seg, 1):
437
+ for char in seg:
438
+ charcode = ord(char)
439
+ if charcode in line_cmap:
440
+ inner_font = line_font
441
+ else:
442
+ for fallback_path in self.fallback_paths:
443
+ if charcode in self.cmaps[fallback_path]:
444
+ inner_font = self.get_font(fallback_path, inline_height)
445
+ assert inner_font is not None
446
+ inner_font = inner_font[0]
447
+ break
448
+ else:
449
+ inner_font = line_font
450
+ char = "□"
451
+ temp_x = x
452
+ x += int(inner_font.getlength(char))
453
+ charlist.append(
454
+ {
455
+ "char": char,
456
+ "color": line_color,
457
+ "y": y + tmp_height,
458
+ "x": temp_x,
459
+ "end_x": x,
460
+ "font": inner_font,
461
+ "align": line_align,
462
+ "highlight": line_highlight,
463
+ }
464
+ )
465
+ max_x = max(max_x, x)
466
+ if i < line_seg_l:
467
+ x = 0
468
+ tmp_height += absolute_spacing + inline_height
469
+ inline_height = tmp_height + absolute_spacing + inline_height
470
+ line_height = max(line_height, inline_height)
471
+ if not line_nowrap:
472
+ x = 0
473
+ y += absolute_spacing + line_height
474
+ line_height = 0
475
+
476
+ width = width if width else int(max_x + padding_x * 2)
477
+ height = height if height else int(y + padding_y * 2)
478
+ canvas = canvas if canvas else Image.new("RGBA", (width, height), bg_color)
479
+ draw = ImageDraw.Draw(canvas)
480
+ i = 0
481
+ loop = len(charlist)
482
+ while i < loop:
483
+ charstyle = charlist[i]
484
+ if self.is_charsingle(charstyle):
485
+ charstyle = cast(Linecard.CharSingle, charstyle)
486
+ align = charstyle["align"]
487
+ y = charstyle["y"]
488
+ start_y = y + padding_y
489
+ if align == "left":
490
+ start_x = padding_x
491
+ elif align == "right":
492
+ last_charstyle_index = i
493
+ # 获取下次换行前最后一个的文字样式
494
+ for inner_charstyle in charlist[i:]:
495
+ if self.is_charsingle(inner_charstyle) and inner_charstyle["y"] == y and inner_charstyle["align"] == align:
496
+ last_charstyle_index += 1
497
+ else:
498
+ break
499
+ last_charstyle = cast(Linecard.CharSingle, charlist[last_charstyle_index - 1])
500
+ start_x = width - padding_x - last_charstyle["end_x"]
501
+ for inner_charstyle in cast(list[Linecard.CharSingle], charlist[i:last_charstyle_index]):
502
+ inner_x = inner_charstyle["x"]
503
+ inner_font = inner_charstyle["font"]
504
+ inner_highlight = inner_charstyle["highlight"]
505
+ if inner_highlight:
506
+ draw.rectangle(
507
+ (
508
+ start_x + inner_x,
509
+ start_y,
510
+ start_x + inner_charstyle["end_x"],
511
+ start_y + inner_font.size,
512
+ ),
513
+ fill=inner_highlight,
514
+ )
515
+ draw.text(
516
+ (start_x + inner_x, start_y),
517
+ inner_charstyle["char"],
518
+ fill=inner_charstyle["color"],
519
+ font=inner_font,
520
+ )
521
+ i = last_charstyle_index
522
+ continue
523
+ elif align == "center":
524
+ last_charstyle_index = i
525
+ # 获取下次换行前最后一个的文字样式
526
+ for inner_charstyle in charlist[i:]:
527
+ if self.is_charsingle(inner_charstyle) and inner_charstyle["y"] == y and inner_charstyle["align"] == align:
528
+ last_charstyle_index += 1
529
+ else:
530
+ break
531
+ last_charstyle = cast(Linecard.CharSingle, charlist[last_charstyle_index - 1])
532
+ start_x = (width - last_charstyle["end_x"]) // 2
533
+ for inner_charstyle in cast(list[Linecard.CharSingle], charlist[i:last_charstyle_index]):
534
+ inner_x = inner_charstyle["x"]
535
+ inner_font = inner_charstyle["font"]
536
+ inner_highlight = inner_charstyle["highlight"]
537
+ if inner_highlight:
538
+ draw.rectangle(
539
+ (
540
+ start_x + inner_x,
541
+ start_y,
542
+ start_x + inner_charstyle["end_x"],
543
+ start_y + inner_font.size,
544
+ ),
545
+ fill=inner_highlight,
546
+ )
547
+ draw.text(
548
+ (start_x + inner_x, start_y),
549
+ inner_charstyle["char"],
550
+ fill=inner_charstyle["color"],
551
+ font=inner_font,
552
+ )
553
+ i = last_charstyle_index
554
+ continue
555
+ else:
556
+ start_x = to_int(align) or 0
557
+ x = charstyle["x"]
558
+ font = charstyle["font"]
559
+ highlight = charstyle["highlight"]
560
+ if highlight:
561
+ draw.rectangle(
562
+ (
563
+ start_x + x,
564
+ start_y,
565
+ start_x + charstyle["end_x"],
566
+ start_y + font.size,
567
+ ),
568
+ fill=highlight,
569
+ )
570
+ draw.text(
571
+ (start_x + x, start_y),
572
+ charstyle["char"],
573
+ fill=charstyle["color"],
574
+ font=font,
575
+ )
576
+ i += 1
577
+ else:
578
+ charstyle = cast(Linecard.CharLine, charstyle)
579
+ inner_y = charstyle["y"] + padding_y + (charstyle["size"] + 0.5) // 2 + 4
580
+ draw.line(((0, inner_y), (width, inner_y)), fill=charstyle["color"], width=4)
581
+ i += 1
582
+ return canvas
@@ -0,0 +1 @@
1
+ def parse_str(text: str) -> tuple[str, list[tuple[bytes, str]]]: ...
@@ -0,0 +1,27 @@
1
+ [project]
2
+ name = "linecard"
3
+ version = "0.2.0r1"
4
+ description = ""
5
+ authors = [{ name = "KarisAya", email = "karisaya@foxmail.com" }]
6
+ readme = "README.md"
7
+ requires-python = ">=3.12"
8
+ dependencies = ["pillow (>=11.1.0,<12.0.0)", "fonttools (>=4.58.0,<5.0.0)"]
9
+ # [build-system]
10
+ # requires = ["poetry-core>=2.0.0,<3.0.0"]
11
+ # build-backend = "poetry.core.masonry.api"
12
+
13
+ [build-system]
14
+ requires = ["setuptools"]
15
+ build-backend = "setuptools.build_meta"
16
+
17
+ [tool.poetry.group.build.dependencies]
18
+ setuptools = "^75.8.0"
19
+ cibuildwheel = "^2.22.0"
20
+
21
+
22
+ [tool.setuptools]
23
+ packages = ["linecard"]
24
+
25
+ # python setup.py build_ext --inplace
26
+ # cibuildwheel --platform linux --output-dir ./dist
27
+ # cibuildwheel --platform windows --output-dir ./dist