recard 0.1.2__py3-none-any.whl

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.
Files changed (87) hide show
  1. recard/__init__.py +29 -0
  2. recard/assets/backgrounds/ANEMO.png +0 -0
  3. recard/assets/backgrounds/CRYO.png +0 -0
  4. recard/assets/backgrounds/DENDRO.png +0 -0
  5. recard/assets/backgrounds/ELECTRO.png +0 -0
  6. recard/assets/backgrounds/ERROR.png +0 -0
  7. recard/assets/backgrounds/GEO.png +0 -0
  8. recard/assets/backgrounds/PYRO.png +0 -0
  9. recard/assets/backgrounds/hydro.png +0 -0
  10. recard/assets/constant/closed/ADAPT.png +0 -0
  11. recard/assets/constant/closed/CLOSED.png +0 -0
  12. recard/assets/constant/closed/CLOSE_CONST_ANEMO.png +0 -0
  13. recard/assets/constant/closed/CLOSE_CONST_CRYO.png +0 -0
  14. recard/assets/constant/closed/CLOSE_CONST_DENDRO.png +0 -0
  15. recard/assets/constant/closed/CLOSE_CONST_ELECTRO.png +0 -0
  16. recard/assets/constant/closed/CLOSE_CONST_ERROR.png +0 -0
  17. recard/assets/constant/closed/CLOSE_CONST_GEO.png +0 -0
  18. recard/assets/constant/closed/CLOSE_CONST_GYDRO.png +0 -0
  19. recard/assets/constant/closed/CLOSE_CONST_PYRO.png +0 -0
  20. recard/assets/constant/const_adapt.png +0 -0
  21. recard/assets/constant/maska_constant.png +0 -0
  22. recard/assets/constant/open/ADAPT.png +0 -0
  23. recard/assets/constant/open/OPEN_CONST_ANEMO.png +0 -0
  24. recard/assets/constant/open/OPEN_CONST_CRYO.png +0 -0
  25. recard/assets/constant/open/OPEN_CONST_DENDRO.png +0 -0
  26. recard/assets/constant/open/OPEN_CONST_ELECTRO.png +0 -0
  27. recard/assets/constant/open/OPEN_CONST_ERROR.png +0 -0
  28. recard/assets/constant/open/OPEN_CONST_GEO.png +0 -0
  29. recard/assets/constant/open/OPEN_CONST_GYDRO.png +0 -0
  30. recard/assets/constant/open/OPEN_CONST_PYRO.png +0 -0
  31. recard/assets/fonts/Genshin_Impact.ttf +0 -0
  32. recard/assets/icons/FRIENDS.png +0 -0
  33. recard/assets/icons/anemo.png +0 -0
  34. recard/assets/icons/atk.png +0 -0
  35. recard/assets/icons/cd.png +0 -0
  36. recard/assets/icons/cr.png +0 -0
  37. recard/assets/icons/cryo.png +0 -0
  38. recard/assets/icons/def.png +0 -0
  39. recard/assets/icons/dendro.png +0 -0
  40. recard/assets/icons/electro.png +0 -0
  41. recard/assets/icons/em.png +0 -0
  42. recard/assets/icons/er.png +0 -0
  43. recard/assets/icons/geo.png +0 -0
  44. recard/assets/icons/hp.png +0 -0
  45. recard/assets/icons/hydro.png +0 -0
  46. recard/assets/icons/no_data.png +0 -0
  47. recard/assets/icons/phys.png +0 -0
  48. recard/assets/icons/pyro.png +0 -0
  49. recard/assets/icons/radar_bg.png +0 -0
  50. recard/assets/icons/stars/Star1.png +0 -0
  51. recard/assets/icons/stars/Star2.png +0 -0
  52. recard/assets/icons/stars/Star3.png +0 -0
  53. recard/assets/icons/stars/Star4.png +0 -0
  54. recard/assets/icons/stars/Star5.png +0 -0
  55. recard/assets/icons/stars/bg.png +0 -0
  56. recard/assets/icons/stars/c_stars_4.png +0 -0
  57. recard/assets/icons/stars/c_stars_5.png +0 -0
  58. recard/assets/images/AVATAR.png +0 -0
  59. recard/assets/images/AVATAR_MASK.png +0 -0
  60. recard/assets/images/CHARTER_4.png +0 -0
  61. recard/assets/images/CHARTER_5.png +0 -0
  62. recard/assets/images/CHARTER_MASK.png +0 -0
  63. recard/assets/images/test.jpg +0 -0
  64. recard/assets/images/test1.jpg +0 -0
  65. recard/assets/logo.png +0 -0
  66. recard/assets/talents/10.png +0 -0
  67. recard/assets/talents/ADAPT.png +0 -0
  68. recard/assets/talents/bg.png +0 -0
  69. recard/cards/__init__.py +0 -0
  70. recard/cards/artifacts.py +170 -0
  71. recard/cards/character_card.py +618 -0
  72. recard/cards/hoyolab_character_detail.py +469 -0
  73. recard/cards/watermark.py +64 -0
  74. recard/client.py +186 -0
  75. recard/data/__init__.py +0 -0
  76. recard/data/avatars.json +4036 -0
  77. recard/data/char.json +4036 -0
  78. recard/data/data.json +869 -0
  79. recard/data/new.json +6183 -0
  80. recard/data/update_data.py +199 -0
  81. recard/services/__init__.py +0 -0
  82. recard/services/net.py +99 -0
  83. recard-0.1.2.dist-info/METADATA +105 -0
  84. recard-0.1.2.dist-info/RECORD +87 -0
  85. recard-0.1.2.dist-info/WHEEL +5 -0
  86. recard-0.1.2.dist-info/licenses/LICENSE +21 -0
  87. recard-0.1.2.dist-info/top_level.txt +1 -0
recard/__init__.py ADDED
@@ -0,0 +1,29 @@
1
+ """
2
+ recard - Genshin Impact character card generator.
3
+
4
+ Same usage pattern as zenka (the ZZZ card library):
5
+
6
+ import asyncio
7
+ import recard
8
+
9
+ async def main():
10
+ async with recard.Client() as client:
11
+ result = await client.card(700000000, character="Hu Tao")
12
+ for card in result.cards:
13
+ card.card.save(f"{card.name}.png")
14
+
15
+ asyncio.run(main())
16
+ """
17
+
18
+ from .client import Card, CardResult, CharacterNotFound, Client, ShowcaseCharacter
19
+
20
+ __version__ = "0.1.2"
21
+
22
+ __all__ = [
23
+ "Client",
24
+ "ShowcaseCharacter",
25
+ "Card",
26
+ "CardResult",
27
+ "CharacterNotFound",
28
+ "__version__",
29
+ ]
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
recard/assets/logo.png ADDED
Binary file
Binary file
Binary file
Binary file
File without changes
@@ -0,0 +1,170 @@
1
+ import asyncio
2
+ import logging
3
+ from io import BytesIO
4
+ from PIL import Image, ImageDraw, ImageFont
5
+
6
+ from pathlib import Path as _Path
7
+ _PKG_ROOT = _Path(__file__).resolve().parent.parent
8
+
9
+ logger = logging.getLogger("recard")
10
+
11
+ # Stat Mapping for icons (Local files)
12
+ STAT_MAP = {
13
+ "FIGHT_PROP_HP": "hp", "FIGHT_PROP_HP_PERCENT": "hp",
14
+ "FIGHT_PROP_ATTACK": "atk", "FIGHT_PROP_ATTACK_PERCENT": "atk",
15
+ "FIGHT_PROP_DEFENSE": "def", "FIGHT_PROP_DEFENSE_PERCENT": "def",
16
+ "FIGHT_PROP_ELEMENT_MASTERY": "em", "FIGHT_PROP_CHARGE_EFFICIENCY": "er",
17
+ "FIGHT_PROP_CRITICAL": "cr", "FIGHT_PROP_CRITICAL_HURT": "cd",
18
+ "FIGHT_PROP_FIRE_ADD_HURT": "pyro", "FIGHT_PROP_WATER_ADD_HURT": "hydro",
19
+ "FIGHT_PROP_GRASS_ADD_HURT": "dendro", "FIGHT_PROP_ELEC_ADD_HURT": "electro",
20
+ "FIGHT_PROP_ICE_ADD_HURT": "cryo", "FIGHT_PROP_WIND_ADD_HURT": "anemo",
21
+ "FIGHT_PROP_ROCK_ADD_HURT": "geo", "FIGHT_PROP_PHYSICAL_ADD_HURT": "phys"
22
+ }
23
+
24
+ # Flower, Feather, Sands, Goblet, Circlet
25
+ EQUIP_ORDER = ["EQUIP_BRACER", "EQUIP_NECKLACE", "EQUIP_SHOES", "EQUIP_RING", "EQUIP_DRESS"]
26
+
27
+
28
+ def _compute_cv(main_data, sub_stats):
29
+ """CV (crit value) = CR% * 2 + CD%, summed across the main stat (only
30
+ relevant for circlets, whose main stat can itself be CR or CD) and all
31
+ substats. Both crit stats stack additively across an artifact, so this
32
+ is just a running total, not an average."""
33
+ cr = 0.0
34
+ cd = 0.0
35
+
36
+ main_prop = main_data.get("mainPropId")
37
+ if main_prop == "FIGHT_PROP_CRITICAL":
38
+ cr += main_data.get("statValue", 0) or 0
39
+ elif main_prop == "FIGHT_PROP_CRITICAL_HURT":
40
+ cd += main_data.get("statValue", 0) or 0
41
+
42
+ for stat in sub_stats:
43
+ prop_id = stat.get("appendPropId")
44
+ if prop_id == "FIGHT_PROP_CRITICAL":
45
+ cr += stat.get("statValue", 0) or 0
46
+ elif prop_id == "FIGHT_PROP_CRITICAL_HURT":
47
+ cd += stat.get("statValue", 0) or 0
48
+
49
+ return cr * 2 + cd
50
+
51
+
52
+ async def draw_artifact_card(session, base_image, x, y, art_data, font):
53
+ draw = ImageDraw.Draw(base_image)
54
+ CARD_W, CARD_H = 330, 200
55
+ ICONS_PATH = f"{_PKG_ROOT}/assets/icons/"
56
+ STARS_PATH = f"{_PKG_ROOT}/assets/icons/stars/"
57
+
58
+ # 1. Background Card - Using a slightly more transparent fill for that glassmorphism look
59
+ draw.rounded_rectangle([x, y, x + CARD_W, y + CARD_H], radius=12, fill=(0, 0, 0, 80), outline=(255, 255, 255, 40))
60
+
61
+ flat = art_data.get("flat", {})
62
+ relic_core = art_data.get("reliquary", {})
63
+ main_data = flat.get("reliquaryMainstat", {})
64
+ sub_stats = flat.get("reliquarySubstats", [])
65
+
66
+ # 2. Artifact Piece Icon (The Gear piece)
67
+ icon_name = flat.get("icon")
68
+ # Live HoYoLAB data (character/detail fallback) gives a full,
69
+ # directly-fetchable CDN URL - it's hash-named, so it can't be
70
+ # reconstructed as an enka.network/ui/... path the way Enka's own
71
+ # semantic icon names can.
72
+ icon_url = flat.get("icon_url") or (f"https://enka.network/ui/{icon_name}.png" if icon_name else None)
73
+ if icon_url:
74
+ url = icon_url
75
+ async with session.get(url) as response:
76
+ if response.status == 200:
77
+ img_data = await response.read()
78
+ art_img = Image.open(BytesIO(img_data)).convert("RGBA").resize((140, 140), Image.Resampling.LANCZOS)
79
+ # Icon background
80
+ base_image.paste(art_img, (x + 15, y + 30), art_img)
81
+
82
+ # 4. Rarity Stars (NEW)
83
+ rarity = flat.get("rankLevel", 5)
84
+ try:
85
+ # 1. Open and convert
86
+ star_img = Image.open(f"{STARS_PATH}Star{rarity}.png").convert("RGBA")
87
+
88
+ # 2. Resize directly to the target size (Re-assigning the variable!)
89
+ star_img = star_img.resize((98, 28), Image.Resampling.LANCZOS)
90
+
91
+ # 3. Paste
92
+ base_image.paste(star_img, (x + 30, y + 140), star_img)
93
+ except Exception as e:
94
+ logger.warning("Error loading star image Star%s.png: %s", rarity, e)
95
+ # 3. Main Stat Icon - PASTE THIS LAST so it's on top
96
+ main_prop = main_data.get("mainPropId")
97
+ if main_prop:
98
+ icon_key = STAT_MAP.get(main_prop, "atk").lower()
99
+ try:
100
+ # We move the main stat icon to the top right of the gear box
101
+ m_icon = Image.open(f"{ICONS_PATH}{icon_key}.png").convert("RGBA").resize((28, 28))
102
+ base_image.paste(m_icon, (x + 10, y + 10), m_icon)
103
+ except Exception:
104
+ pass
105
+
106
+ # 4. Sub-Stat Grid
107
+ grid_x = x + 150
108
+ for i, stat in enumerate(sub_stats[:4]):
109
+ col, row = i % 1, i // 1
110
+ bx = grid_x + (col * 135)
111
+ by = y + 15 + (row * 45)
112
+
113
+ # Substat box
114
+ draw.rounded_rectangle([bx, by, bx + 170, by + 38], radius=10, fill=(20, 20, 30, 150))
115
+
116
+ prop_id = stat.get("appendPropId")
117
+ icon_file = STAT_MAP.get(prop_id, "atk").lower()
118
+ try:
119
+ s_icon = Image.open(f"{ICONS_PATH}{icon_file}.png").convert("RGBA").resize((22, 22))
120
+ base_image.paste(s_icon, (bx + 8, by + 8), s_icon)
121
+ except Exception:
122
+ pass
123
+
124
+ val = stat.get("statValue")
125
+ is_percent = any(k in prop_id for k in ["PERCENT", "CRITICAL", "EFFICIENCY", "HURT"])
126
+ val_str = f"+{val}%" if is_percent else f"+{val}"
127
+ draw.text((bx + 55, by + 19), val_str, font=font, fill=(255, 255, 255), anchor="lm")
128
+
129
+ # 5. Main Stat Value - bottom left of the card (where the level used to sit).
130
+ if main_prop:
131
+ main_val = main_data.get("statValue")
132
+ is_percent = any(k in main_prop for k in ["PERCENT", "CRITICAL", "EFFICIENCY", "HURT"])
133
+ main_val_str = f"{main_val}%" if is_percent else f"{int(main_val)}"
134
+ draw.text((x + 5, y + CARD_H - 15), main_val_str, font=font, fill=(255, 255, 255), anchor="lm", stroke_width=1, stroke_fill=(0, 0, 0))
135
+
136
+ # 6. Enhancement level - bottom right of the card.
137
+ raw_level = relic_core.get("level", 1)
138
+ display_level = f"+{raw_level - 1}"
139
+ draw.text((x + CARD_W - 185, y + CARD_H - 15), display_level, font=font, fill=(255, 204, 0), stroke_width=1, stroke_fill=(0, 0, 0), anchor="rm")
140
+
141
+ # 7. CV (crit value) score - top right of the card. Drawn last, on top
142
+ # of everything else (icon/stars/grid), so it's never painted over.
143
+ cv_score = _compute_cv(main_data, sub_stats)
144
+ cv_text = f"CV{cv_score:.1f}"
145
+ draw.text((x + CARD_W - 185, y + 25), cv_text, font=font, fill=(255, 255, 255), stroke_width=1, stroke_fill=(0, 0, 0), anchor="rm")
146
+
147
+ async def draw_horizontal_artifacts(session, background, char_data, start_x, start_y, font):
148
+ """
149
+ Finds all artifacts for a single character and draws them in a
150
+ horizontal row (Flower -> Feather -> Sands -> Goblet -> Circlet).
151
+ """
152
+ # Filter only relics (artifacts)
153
+ relics = [e for e in char_data.get("equipList", []) if "reliquary" in e]
154
+
155
+ # Sort by EQUIP_ORDER
156
+ sorted_relics = sorted(
157
+ relics,
158
+ key=lambda x: EQUIP_ORDER.index(x.get("flat", {}).get("equipType", ""))
159
+ if x.get("flat", {}).get("equipType") in EQUIP_ORDER else 99
160
+ )
161
+
162
+ tasks = []
163
+ spacing_x = 345
164
+
165
+ for i, art in enumerate(sorted_relics[:5]):
166
+ curr_x = start_x + (i * spacing_x)
167
+ tasks.append(draw_artifact_card(session, background, curr_x, start_y, art, font))
168
+
169
+ if tasks:
170
+ await asyncio.gather(*tasks)