decksmith 0.9.1__py3-none-any.whl → 0.9.3__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.
@@ -1,127 +1,153 @@
1
- """
2
- This module contains the TextRenderer class for drawing text on cards.
3
- """
4
-
5
- import operator
6
- from pathlib import Path
7
- from typing import Any, Dict, Optional
8
-
9
- import pandas as pd
10
- from PIL import ImageDraw, ImageFont
11
-
12
- from decksmith.logger import logger
13
- from decksmith.utils import apply_anchor, get_wrapped_text
14
-
15
-
16
- class TextRenderer:
17
- """
18
- A class to render text elements on a card.
19
- """
20
-
21
- def __init__(self, base_path: Optional[Path] = None):
22
- self.base_path = base_path
23
-
24
- def render(
25
- self,
26
- draw: ImageDraw.ImageDraw,
27
- element: Dict[str, Any],
28
- calculate_pos_func,
29
- store_pos_func,
30
- ):
31
- """
32
- Draws text on the card.
33
- Args:
34
- draw (ImageDraw.ImageDraw): The PIL ImageDraw object.
35
- element (Dict[str, Any]): The text element specification.
36
- calculate_pos_func (callable): Function to calculate absolute position.
37
- store_pos_func (callable): Function to store element position.
38
- """
39
- assert element.pop("type") == "text", "Element type must be 'text'"
40
-
41
- element = self._prepare_text_element(element)
42
-
43
- original_pos = calculate_pos_func(element)
44
- element["position"] = original_pos
45
-
46
- # Calculate anchor offset if needed
47
- if "anchor" in element:
48
- bbox = draw.textbbox(
49
- xy=(0, 0),
50
- text=element.get("text"),
51
- font=element["font"],
52
- spacing=element.get("line_spacing", 4),
53
- align=element.get("align", "left"),
54
- )
55
- anchor_point = apply_anchor(bbox, element.pop("anchor"))
56
- element["position"] = tuple(map(operator.sub, original_pos, anchor_point))
57
-
58
- # Draw text
59
- draw.text(
60
- xy=element.get("position"),
61
- text=element.get("text"),
62
- fill=element.get("color", None),
63
- font=element["font"],
64
- spacing=element.get("line_spacing", 4),
65
- align=element.get("align", "left"),
66
- stroke_width=element.get("stroke_width", 0),
67
- stroke_fill=element.get("stroke_color", None),
68
- )
69
-
70
- # Store position
71
- if "id" in element:
72
- bbox = draw.textbbox(
73
- xy=element.get("position"),
74
- text=element.get("text"),
75
- font=element["font"],
76
- spacing=element.get("line_spacing", 4),
77
- align=element.get("align", "left"),
78
- )
79
- store_pos_func(element["id"], bbox)
80
-
81
- def _prepare_text_element(self, element: Dict[str, Any]) -> Dict[str, Any]:
82
- """Prepares text element properties."""
83
- if pd.isna(element["text"]):
84
- element["text"] = " "
85
-
86
- # Font setup
87
- font_size = element.pop("font_size", 10)
88
- if font_path := element.pop("font_path", False):
89
- # Resolve font path relative to base_path if provided
90
- if self.base_path and not Path(font_path).is_absolute():
91
- potential_path = self.base_path / font_path
92
- if potential_path.exists():
93
- font_path = str(potential_path)
94
-
95
- try:
96
- element["font"] = ImageFont.truetype(
97
- font_path, font_size, encoding="unic"
98
- )
99
- except OSError:
100
- logger.error("Could not load font: %s. Using default.", font_path)
101
- element["font"] = ImageFont.load_default(font_size)
102
- else:
103
- element["font"] = ImageFont.load_default(font_size)
104
-
105
- if font_variant := element.pop("font_variant", None):
106
- try:
107
- element["font"].set_variation_by_name(font_variant)
108
- except AttributeError:
109
- logger.warning(
110
- "Font variant '%s' not supported for this font.", font_variant
111
- )
112
-
113
- # Text wrapping
114
- if line_length := element.pop("width", False):
115
- element["text"] = get_wrapped_text(
116
- element["text"], element["font"], line_length
117
- )
118
-
119
- # Colors and position
120
- if position := element.pop("position", [0, 0]):
121
- element["position"] = tuple(position)
122
- if color := element.pop("color", [0, 0, 0]):
123
- element["color"] = tuple(color)
124
- if stroke_color := element.pop("stroke_color", None):
125
- element["stroke_color"] = tuple(stroke_color)
126
-
127
- return element
1
+ """
2
+ This module contains the TextRenderer class for drawing text on cards.
3
+ """
4
+
5
+ import operator
6
+ from pathlib import Path
7
+ from typing import Any, Dict, Optional
8
+
9
+ import pandas as pd
10
+ from PIL import Image, ImageDraw, ImageFont
11
+
12
+ from decksmith.utils import apply_anchor, get_wrapped_text
13
+
14
+
15
+ class TextRenderer:
16
+ """
17
+ A class to render text elements on a card.
18
+ """
19
+
20
+ def __init__(self, base_path: Optional[Path] = None):
21
+ self.base_path = base_path
22
+
23
+ def render(
24
+ self,
25
+ card: Image.Image,
26
+ element: Dict[str, Any],
27
+ calculate_pos_func,
28
+ store_pos_func,
29
+ ) -> Image.Image:
30
+ """
31
+ Draws text on the card.
32
+ Args:
33
+ card (Image.Image): The card image object.
34
+ element (Dict[str, Any]): The text element specification.
35
+ calculate_pos_func (callable): Function to calculate absolute position.
36
+ store_pos_func (callable): Function to store element position.
37
+ Returns:
38
+ Image.Image: The updated card image.
39
+ """
40
+ assert element.pop("type") == "text", "Element type must be 'text'"
41
+
42
+ element = self._prepare_text_element(element)
43
+
44
+ original_pos = calculate_pos_func(element)
45
+ element["position"] = original_pos
46
+
47
+ layer = Image.new("RGBA", card.size, (0, 0, 0, 0))
48
+ draw = ImageDraw.Draw(layer, "RGBA")
49
+
50
+ # Calculate anchor offset if needed
51
+ if "anchor" in element:
52
+ bbox = draw.textbbox(
53
+ xy=(0, 0),
54
+ text=element.get("text"),
55
+ font=element["font"],
56
+ spacing=element.get("line_spacing", 4),
57
+ align=element.get("align", "left"),
58
+ )
59
+ anchor_point = apply_anchor(bbox, element.pop("anchor"))
60
+ element["position"] = tuple(map(operator.sub, original_pos, anchor_point))
61
+
62
+ # Draw text
63
+ draw.text(
64
+ xy=element.get("position"),
65
+ text=element.get("text"),
66
+ fill=element.get("color", None),
67
+ font=element["font"],
68
+ spacing=element.get("line_spacing", 4),
69
+ align=element.get("align", "left"),
70
+ stroke_width=element.get("stroke_width", 0),
71
+ stroke_fill=element.get("stroke_color", None),
72
+ )
73
+
74
+ card = Image.alpha_composite(card, layer)
75
+
76
+ # Store position
77
+ if "id" in element:
78
+ bbox = draw.textbbox(
79
+ xy=element.get("position"),
80
+ text=element.get("text"),
81
+ font=element["font"],
82
+ spacing=element.get("line_spacing", 4),
83
+ align=element.get("align", "left"),
84
+ )
85
+ store_pos_func(element["id"], bbox)
86
+
87
+ return card
88
+
89
+ def _prepare_text_element(self, element: Dict[str, Any]) -> Dict[str, Any]:
90
+ """Prepares text element properties."""
91
+ if pd.isna(element["text"]):
92
+ element["text"] = " "
93
+
94
+ # Font setup
95
+ font_size = element.pop("font_size", 10)
96
+ if font_path := element.pop("font_path", None):
97
+ # Resolve font path relative to base_path if provided
98
+ if self.base_path and not Path(font_path).is_absolute():
99
+ potential_path = self.base_path / font_path
100
+ if potential_path.exists():
101
+ font_path = str(potential_path)
102
+
103
+ try:
104
+ element["font"] = ImageFont.truetype(
105
+ font_path, font_size, encoding="unic"
106
+ )
107
+ except OSError as exc:
108
+ raise OSError(f"Could not load font: {font_path}") from exc
109
+ else:
110
+ element["font"] = ImageFont.load_default(font_size)
111
+
112
+ if font_variant := element.pop("font_variant", None):
113
+ try:
114
+ names = element["font"].get_variation_names()
115
+ except (AttributeError, OSError):
116
+ names = []
117
+
118
+ # Normalize names to strings (some fonts return bytes)
119
+ names = [
120
+ name.decode("utf-8") if isinstance(name, bytes) else name
121
+ for name in names
122
+ ]
123
+
124
+ if names:
125
+ if font_variant not in names:
126
+ raise ValueError(
127
+ f"Font variant '{font_variant}' not found. "
128
+ f"Available variants: {', '.join(names)}"
129
+ )
130
+ element["font"].set_variation_by_name(font_variant)
131
+ else:
132
+ try:
133
+ element["font"].set_variation_by_name(font_variant)
134
+ except (AttributeError, OSError) as exc:
135
+ raise ValueError(
136
+ f"Font variant '{font_variant}' not supported for this font."
137
+ ) from exc
138
+
139
+ # Text wrapping
140
+ if line_length := element.pop("width", False):
141
+ element["text"] = get_wrapped_text(
142
+ element["text"], element["font"], line_length
143
+ )
144
+
145
+ # Colors and position
146
+ if position := element.pop("position", [0, 0]):
147
+ element["position"] = tuple(position)
148
+ if color := element.pop("color", [0, 0, 0]):
149
+ element["color"] = tuple(color)
150
+ if stroke_color := element.pop("stroke_color", None):
151
+ element["stroke_color"] = tuple(stroke_color)
152
+
153
+ return element
decksmith/validate.py CHANGED
@@ -39,6 +39,7 @@ SPECS_FOR_TYPE: Dict[str, Dict[str, Any]] = {
39
39
  "?*rotate": "<?int>",
40
40
  "?*flip": "<?str>",
41
41
  "?*resize": ["<?int>"],
42
+ "?*opacity": "<?int>",
42
43
  },
43
44
  },
44
45
  "circle": {
@@ -90,10 +91,17 @@ def validate_element(element: Dict[str, Any], element_type: str):
90
91
  """
91
92
  Validates an element of a card against a spec, raising an exception
92
93
  if it does not meet the spec.
94
+
93
95
  Args:
94
96
  element (dict): The card element.
95
97
  element_type (str): The type of the element
98
+
99
+ Raises:
100
+ ValueError: If the element type is unknown.
101
+ jval.exceptions.ValidationException: If the element does not match the spec.
96
102
  """
103
+ if element_type not in SPECS_FOR_TYPE:
104
+ raise ValueError(f"Unknown element type: {element_type}")
97
105
  spec = ELEMENT_SPEC | SPECS_FOR_TYPE[element_type]
98
106
  validate(element, spec)
99
107
 
@@ -102,13 +110,15 @@ def validate_card(card: Dict[str, Any]):
102
110
  """
103
111
  Validates a card against a spec, raising an exception
104
112
  if it does not meet the spec.
113
+
105
114
  Args:
106
115
  card (Dict[str, Any]): The card.
116
+
117
+ Raises:
118
+ jval.exceptions.ValidationException: If the card does not match the spec.
107
119
  """
108
- # print(f"DEBUG:\n{card=}")
109
120
  validate(card, CARD_SPEC)
110
121
  for element in card["elements"]:
111
- # print(f"DEBUG: {element['type']}")
112
122
  validate_element(element, element["type"])
113
123
 
114
124
 
@@ -123,6 +133,8 @@ def transform_card(card: Dict[str, Any]) -> Dict[str, Any]:
123
133
  Dict[str, Any]: The transformed card with all automatic casts applied.
124
134
  """
125
135
  for element in card.get("elements", []):
136
+ if isinstance(element, str):
137
+ raise ValueError(f"Element '{element}' cannot be empty")
126
138
  if element.get("type") == "text" and "text" in element:
127
139
  if pd.isna(element["text"]):
128
140
  element["text"] = None
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: decksmith
3
- Version: 0.9.1
3
+ Version: 0.9.3
4
4
  Summary: A command-line application to dynamically generate decks of cards from a YAML specification and a CSV data file, inspired by nandeck.
5
5
  License-Expression: GPL-2.0-only
6
6
  Author: Julio Cabria
@@ -28,6 +28,8 @@ Description-Content-Type: text/markdown
28
28
 
29
29
  # DeckSmith
30
30
 
31
+ [julynx.github.io/decksmith](https://julynx.github.io/decksmith/)
32
+
31
33
  *A powerful application to dynamically generate decks of cards from a YAML specification and a CSV data file.*
32
34
 
33
35
  <br>
@@ -0,0 +1,27 @@
1
+ decksmith/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ decksmith/card_builder.py,sha256=vkQDY5hXLrfCxqkjwF6Q87jLxQe4tLmi-ZybFzt_DGw,6213
3
+ decksmith/deck_builder.py,sha256=73p_IxDgH2bbkfow1bTg_eDXhjcALQbVFuudORxZ-E8,3689
4
+ decksmith/export.py,sha256=2dOS59440qwziLG3tvvUzpapAqqHF9TSPqHQHYa_mh0,6057
5
+ decksmith/gui/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ decksmith/gui/app.py,sha256=dDJwMOm6HHTAIewYHcc0zqghxr-k_tXRZmI_44QJ3EM,10707
7
+ decksmith/gui/static/css/style.css,sha256=FMRF5VSGeRGM2CIn2hOe3bnnwNhFq6JRAHTq9wcW0Ak,14172
8
+ decksmith/gui/static/img/decksmith.ico,sha256=bkz7MTDpQELzP2GSvR42Jn7OgIw_j0uBhN4L2i-m1uo,28366
9
+ decksmith/gui/static/js/main.js,sha256=aOwnTIzWvwqBcO9otoB25B43Tbcv_H-JnZ1j8ZvY32s,24125
10
+ decksmith/gui/templates/index.html,sha256=-rZ-eO7O35gh5bJrQrb-5M-tnYEqSVqGWJh5sCe2e5c,9551
11
+ decksmith/image_ops.py,sha256=SU2h_DC2t06adIwFrJdYp9skHSSj8834x8LV9l6z3qE,5073
12
+ decksmith/logger.py,sha256=uSMzIpv7xT7ICMz4a88x_Z9-Izz0WU9MdZ9JEwWnOC4,1020
13
+ decksmith/macro.py,sha256=prHs0fTIqxE5kjT1usqnWg8CNZEoWUVNAezD-qJslRY,1584
14
+ decksmith/main.py,sha256=W5ve6W7D2sFgLRgskuo54H_PDIDMJTcO4JPNgfRCgZI,4681
15
+ decksmith/project.py,sha256=3tp-uWmlVjA1NB-ZElmCK93M3eL-LknbDuZCUqwPVGE,3479
16
+ decksmith/renderers/__init__.py,sha256=xF0SsNJSQEbY-NoSN0N4admLTGc_4JcZumDMpQahWBM,76
17
+ decksmith/renderers/image.py,sha256=5HzKzELhpwN3WMq2Ni7NT-qmTYHuJaBA8jYVAcxioZw,2153
18
+ decksmith/renderers/shapes.py,sha256=iSCCr4hn2lj4EGchq324_eHtuuYAhemYP9PSEUFBkxM,7855
19
+ decksmith/renderers/text.py,sha256=b7NZn5mb8oCg5BzsupH-1-KzveAKCl6YJKcANbzCeRQ,5525
20
+ decksmith/templates/deck.csv,sha256=8P3XknSg6kXulNyAmqFHgJDIzGjMnvdMQ8ctTtUoIzE,782
21
+ decksmith/templates/deck.yaml,sha256=3BMpTUH1It25wrmQXZSrERoPo4-8TBKXKxfaRtYowt8,1034
22
+ decksmith/utils.py,sha256=-QHYbNKQeB1SXB5Ovswn6Yql9sfSb5CmSNVpsw3LiVM,2909
23
+ decksmith/validate.py,sha256=-HNgV1fPhnNUIJK9xI2CBGJwc2HLhChjLEik9cihER4,4245
24
+ decksmith-0.9.3.dist-info/entry_points.txt,sha256=-usRztjj2gnfmPubb8nFYHD22drzThAmSfM6geWI98Y,48
25
+ decksmith-0.9.3.dist-info/METADATA,sha256=g0VD563NLJ0URE68wENn6PSZDWcpy9uPSSWrpgsJL2g,4316
26
+ decksmith-0.9.3.dist-info/WHEEL,sha256=kJCRJT_g0adfAJzTx2GUMmS80rTJIVHRCfG0DQgLq3o,88
27
+ decksmith-0.9.3.dist-info/RECORD,,
@@ -1,26 +0,0 @@
1
- decksmith/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- decksmith/card_builder.py,sha256=hxDVS7-dngCiT392pP2aiWgasfSp1ou3v3lzWmLXxL4,5618
3
- decksmith/deck_builder.py,sha256=J_Zdj4m-tXPSZh67mEyOCQ_CTphRCsPiyHSh1HcNM0M,3305
4
- decksmith/export.py,sha256=rzRGdyfK1YXu_k9cpkXvNFZZBkpfXW5zPr_iotlJxcY,5979
5
- decksmith/gui/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
- decksmith/gui/app.py,sha256=NQPR_hfCVvrr0QnEUmJXQv_A4lFXXoF8_OostVLwZkU,10178
7
- decksmith/gui/static/css/style.css,sha256=djBsKvc7tm7VaK5PWm1vFtan_PyEWESWjckgNSevr2s,13582
8
- decksmith/gui/static/js/main.js,sha256=A1Vz4kxlokqTadESYT7PlZf4XkVlaT0AXmSNNSrmixw,21680
9
- decksmith/gui/templates/index.html,sha256=xQhMFPzkCo1F6qR48NHjLkk12wzgn0Hh0LXTA4ShueE,8949
10
- decksmith/image_ops.py,sha256=8UzibqfoED_Jch-lt--l2LIAoIwdBrXH7so5UqY5pqw,4737
11
- decksmith/logger.py,sha256=uSMzIpv7xT7ICMz4a88x_Z9-Izz0WU9MdZ9JEwWnOC4,1020
12
- decksmith/macro.py,sha256=prHs0fTIqxE5kjT1usqnWg8CNZEoWUVNAezD-qJslRY,1584
13
- decksmith/main.py,sha256=Jm98JBo3a2H_S7mwHEWg8NzyUBXdbFewFxzlpPxjDLI,4653
14
- decksmith/project.py,sha256=QbhSOAveCuQ05P2Dp-uIc7hkC_o0-NAq3LIWQ4g-FnU,3885
15
- decksmith/renderers/__init__.py,sha256=xF0SsNJSQEbY-NoSN0N4admLTGc_4JcZumDMpQahWBM,76
16
- decksmith/renderers/image.py,sha256=SKj_21wlAjHGEG6M0GGu4OkwmJrxf0Nnk9TCu_2BP38,2218
17
- decksmith/renderers/shapes.py,sha256=iSCCr4hn2lj4EGchq324_eHtuuYAhemYP9PSEUFBkxM,7855
18
- decksmith/renderers/text.py,sha256=rN3vYUPWy_OpbxiGI5nydOu2V4O1CnoVKaqRIv64waU,4465
19
- decksmith/templates/deck.csv,sha256=8P3XknSg6kXulNyAmqFHgJDIzGjMnvdMQ8ctTtUoIzE,782
20
- decksmith/templates/deck.yaml,sha256=3BMpTUH1It25wrmQXZSrERoPo4-8TBKXKxfaRtYowt8,1034
21
- decksmith/utils.py,sha256=-QHYbNKQeB1SXB5Ovswn6Yql9sfSb5CmSNVpsw3LiVM,2909
22
- decksmith/validate.py,sha256=uNkBg0ruZp63gOu687Ed2hrtepV9Ez6NwnikKfvexzs,3813
23
- decksmith-0.9.1.dist-info/entry_points.txt,sha256=-usRztjj2gnfmPubb8nFYHD22drzThAmSfM6geWI98Y,48
24
- decksmith-0.9.1.dist-info/METADATA,sha256=u3WtryzgPYzisJIQ-gjvL4NaxmeTmM6uUKMeqUx7HZA,4249
25
- decksmith-0.9.1.dist-info/WHEEL,sha256=kJCRJT_g0adfAJzTx2GUMmS80rTJIVHRCfG0DQgLq3o,88
26
- decksmith-0.9.1.dist-info/RECORD,,