decksmith 0.1.14__py3-none-any.whl → 0.9.1__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.
- decksmith/card_builder.py +140 -627
- decksmith/deck_builder.py +85 -101
- decksmith/export.py +29 -27
- decksmith/gui/__init__.py +0 -0
- decksmith/gui/app.py +341 -0
- decksmith/gui/static/css/style.css +656 -0
- decksmith/gui/static/js/main.js +583 -0
- decksmith/gui/templates/index.html +182 -0
- decksmith/image_ops.py +121 -0
- decksmith/logger.py +39 -0
- decksmith/macro.py +46 -0
- decksmith/main.py +146 -138
- decksmith/project.py +111 -0
- decksmith/renderers/__init__.py +3 -0
- decksmith/renderers/image.py +76 -0
- decksmith/renderers/shapes.py +237 -0
- decksmith/renderers/text.py +127 -0
- decksmith/templates/deck.csv +4 -5
- decksmith/templates/deck.yaml +46 -0
- decksmith/utils.py +19 -13
- decksmith/validate.py +1 -1
- {decksmith-0.1.14.dist-info → decksmith-0.9.1.dist-info}/METADATA +53 -19
- decksmith-0.9.1.dist-info/RECORD +26 -0
- {decksmith-0.1.14.dist-info → decksmith-0.9.1.dist-info}/WHEEL +1 -1
- decksmith/templates/deck.json +0 -31
- decksmith-0.1.14.dist-info/RECORD +0 -13
- {decksmith-0.1.14.dist-info → decksmith-0.9.1.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module contains the ShapeRenderer class for drawing shapes on cards.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import operator
|
|
6
|
+
from typing import Any, Dict
|
|
7
|
+
|
|
8
|
+
from PIL import Image, ImageDraw
|
|
9
|
+
|
|
10
|
+
from decksmith.utils import apply_anchor
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ShapeRenderer:
|
|
14
|
+
"""
|
|
15
|
+
A class to render shape elements on a card.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def render(
|
|
19
|
+
self,
|
|
20
|
+
card: Image.Image,
|
|
21
|
+
element: Dict[str, Any],
|
|
22
|
+
calculate_pos_func,
|
|
23
|
+
store_pos_func,
|
|
24
|
+
) -> Image.Image:
|
|
25
|
+
"""
|
|
26
|
+
Draws a shape on the card.
|
|
27
|
+
Args:
|
|
28
|
+
card (Image.Image): The card image object.
|
|
29
|
+
element (Dict[str, Any]): The shape element specification.
|
|
30
|
+
calculate_pos_func (callable): Function to calculate absolute position.
|
|
31
|
+
store_pos_func (callable): Function to store element position.
|
|
32
|
+
Returns:
|
|
33
|
+
Image.Image: The updated card image.
|
|
34
|
+
"""
|
|
35
|
+
element_type = element.get("type")
|
|
36
|
+
draw_methods = {
|
|
37
|
+
"circle": self._draw_shape_circle,
|
|
38
|
+
"ellipse": self._draw_shape_ellipse,
|
|
39
|
+
"polygon": self._draw_shape_polygon,
|
|
40
|
+
"regular-polygon": self._draw_shape_regular_polygon,
|
|
41
|
+
"rectangle": self._draw_shape_rectangle,
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if draw_method := draw_methods.get(element_type):
|
|
45
|
+
return draw_method(card, element, calculate_pos_func, store_pos_func)
|
|
46
|
+
return card
|
|
47
|
+
|
|
48
|
+
def _draw_shape_generic(
|
|
49
|
+
self,
|
|
50
|
+
card: Image.Image,
|
|
51
|
+
element: Dict[str, Any],
|
|
52
|
+
draw_func,
|
|
53
|
+
size_func,
|
|
54
|
+
calculate_pos_func,
|
|
55
|
+
store_pos_func,
|
|
56
|
+
) -> Image.Image:
|
|
57
|
+
"""Generic method to draw shapes."""
|
|
58
|
+
size = size_func(element)
|
|
59
|
+
absolute_pos = calculate_pos_func(element)
|
|
60
|
+
|
|
61
|
+
if "color" in element:
|
|
62
|
+
element["fill"] = tuple(element["color"])
|
|
63
|
+
if "outline_color" in element:
|
|
64
|
+
element["outline_color"] = tuple(element["outline_color"])
|
|
65
|
+
|
|
66
|
+
if "anchor" in element:
|
|
67
|
+
anchor_offset = apply_anchor(size, element.pop("anchor"))
|
|
68
|
+
absolute_pos = tuple(map(operator.sub, absolute_pos, anchor_offset))
|
|
69
|
+
|
|
70
|
+
layer = Image.new("RGBA", card.size, (0, 0, 0, 0))
|
|
71
|
+
layer_draw = ImageDraw.Draw(layer, "RGBA")
|
|
72
|
+
|
|
73
|
+
draw_func(layer_draw, absolute_pos, element)
|
|
74
|
+
|
|
75
|
+
card = Image.alpha_composite(card, layer)
|
|
76
|
+
|
|
77
|
+
if "id" in element:
|
|
78
|
+
store_pos_func(
|
|
79
|
+
element["id"],
|
|
80
|
+
(
|
|
81
|
+
absolute_pos[0],
|
|
82
|
+
absolute_pos[1],
|
|
83
|
+
absolute_pos[0] + size[0],
|
|
84
|
+
absolute_pos[1] + size[1],
|
|
85
|
+
),
|
|
86
|
+
)
|
|
87
|
+
return card
|
|
88
|
+
|
|
89
|
+
def _draw_shape_circle(
|
|
90
|
+
self,
|
|
91
|
+
card: Image.Image,
|
|
92
|
+
element: Dict[str, Any],
|
|
93
|
+
calculate_pos_func,
|
|
94
|
+
store_pos_func,
|
|
95
|
+
) -> Image.Image:
|
|
96
|
+
assert element.pop("type") == "circle", "Element type must be 'circle'"
|
|
97
|
+
radius = element["radius"]
|
|
98
|
+
|
|
99
|
+
def draw(layer_draw, pos, element):
|
|
100
|
+
center_pos = (pos[0] + radius, pos[1] + radius)
|
|
101
|
+
layer_draw.circle(
|
|
102
|
+
center_pos,
|
|
103
|
+
radius,
|
|
104
|
+
fill=element.get("fill", None),
|
|
105
|
+
outline=element.get("outline_color", None),
|
|
106
|
+
width=element.get("outline_width", 1),
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
return self._draw_shape_generic(
|
|
110
|
+
card,
|
|
111
|
+
element,
|
|
112
|
+
draw,
|
|
113
|
+
lambda _: (radius * 2, radius * 2),
|
|
114
|
+
calculate_pos_func,
|
|
115
|
+
store_pos_func,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
def _draw_shape_ellipse(
|
|
119
|
+
self,
|
|
120
|
+
card: Image.Image,
|
|
121
|
+
element: Dict[str, Any],
|
|
122
|
+
calculate_pos_func,
|
|
123
|
+
store_pos_func,
|
|
124
|
+
) -> Image.Image:
|
|
125
|
+
assert element.pop("type") == "ellipse", "Element type must be 'ellipse'"
|
|
126
|
+
size = element["size"]
|
|
127
|
+
|
|
128
|
+
def draw(layer_draw, pos, element):
|
|
129
|
+
bbox = (pos[0], pos[1], pos[0] + size[0], pos[1] + size[1])
|
|
130
|
+
layer_draw.ellipse(
|
|
131
|
+
bbox,
|
|
132
|
+
fill=element.get("fill", None),
|
|
133
|
+
outline=element.get("outline_color", None),
|
|
134
|
+
width=element.get("outline_width", 1),
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
return self._draw_shape_generic(
|
|
138
|
+
card, element, draw, lambda _: size, calculate_pos_func, store_pos_func
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
def _draw_shape_polygon(
|
|
142
|
+
self,
|
|
143
|
+
card: Image.Image,
|
|
144
|
+
element: Dict[str, Any],
|
|
145
|
+
calculate_pos_func,
|
|
146
|
+
store_pos_func,
|
|
147
|
+
) -> Image.Image:
|
|
148
|
+
assert element.pop("type") == "polygon", "Element type must be 'polygon'"
|
|
149
|
+
points = [tuple(point) for point in element.get("points", [])]
|
|
150
|
+
if not points:
|
|
151
|
+
return card
|
|
152
|
+
|
|
153
|
+
min_horizontal = min(point[0] for point in points)
|
|
154
|
+
max_horizontal = max(point[0] for point in points)
|
|
155
|
+
min_vertical = min(point[1] for point in points)
|
|
156
|
+
max_vertical = max(point[1] for point in points)
|
|
157
|
+
bbox_size = (max_horizontal - min_horizontal, max_vertical - min_vertical)
|
|
158
|
+
|
|
159
|
+
def draw(layer_draw, pos, element):
|
|
160
|
+
# pos is the top-left of the bounding box
|
|
161
|
+
# We need to shift points so that (min_horizontal, min_vertical) aligns with pos
|
|
162
|
+
offset_horizontal = pos[0] - min_horizontal
|
|
163
|
+
offset_vertical = pos[1] - min_vertical
|
|
164
|
+
final_points = [
|
|
165
|
+
(point[0] + offset_horizontal, point[1] + offset_vertical)
|
|
166
|
+
for point in points
|
|
167
|
+
]
|
|
168
|
+
|
|
169
|
+
layer_draw.polygon(
|
|
170
|
+
final_points,
|
|
171
|
+
fill=element.get("fill", None),
|
|
172
|
+
outline=element.get("outline_color", None),
|
|
173
|
+
width=element.get("outline_width", 1),
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
return self._draw_shape_generic(
|
|
177
|
+
card, element, draw, lambda _: bbox_size, calculate_pos_func, store_pos_func
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
def _draw_shape_regular_polygon(
|
|
181
|
+
self,
|
|
182
|
+
card: Image.Image,
|
|
183
|
+
element: Dict[str, Any],
|
|
184
|
+
calculate_pos_func,
|
|
185
|
+
store_pos_func,
|
|
186
|
+
) -> Image.Image:
|
|
187
|
+
assert element.pop("type") == "regular-polygon", (
|
|
188
|
+
"Element type must be 'regular-polygon'"
|
|
189
|
+
)
|
|
190
|
+
radius = element["radius"]
|
|
191
|
+
|
|
192
|
+
def draw(layer_draw, pos, element):
|
|
193
|
+
center_pos = (pos[0] + radius, pos[1] + radius)
|
|
194
|
+
layer_draw.regular_polygon(
|
|
195
|
+
(center_pos[0], center_pos[1], radius),
|
|
196
|
+
n_sides=element["sides"],
|
|
197
|
+
rotation=element.get("rotation", 0),
|
|
198
|
+
fill=element.get("fill", None),
|
|
199
|
+
outline=element.get("outline_color", None),
|
|
200
|
+
width=element.get("outline_width", 1),
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
return self._draw_shape_generic(
|
|
204
|
+
card,
|
|
205
|
+
element,
|
|
206
|
+
draw,
|
|
207
|
+
lambda _: (radius * 2, radius * 2),
|
|
208
|
+
calculate_pos_func,
|
|
209
|
+
store_pos_func,
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
def _draw_shape_rectangle(
|
|
213
|
+
self,
|
|
214
|
+
card: Image.Image,
|
|
215
|
+
element: Dict[str, Any],
|
|
216
|
+
calculate_pos_func,
|
|
217
|
+
store_pos_func,
|
|
218
|
+
) -> Image.Image:
|
|
219
|
+
assert element.pop("type") == "rectangle", "Element type must be 'rectangle'"
|
|
220
|
+
size = element["size"]
|
|
221
|
+
if "corners" in element:
|
|
222
|
+
element["corners"] = tuple(element["corners"])
|
|
223
|
+
|
|
224
|
+
def draw(layer_draw, pos, element):
|
|
225
|
+
bbox = (pos[0], pos[1], pos[0] + size[0], pos[1] + size[1])
|
|
226
|
+
layer_draw.rounded_rectangle(
|
|
227
|
+
bbox,
|
|
228
|
+
radius=element.get("corner_radius", 0),
|
|
229
|
+
fill=element.get("fill", None),
|
|
230
|
+
outline=element.get("outline_color", None),
|
|
231
|
+
width=element.get("outline_width", 1),
|
|
232
|
+
corners=element.get("corners", None),
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
return self._draw_shape_generic(
|
|
236
|
+
card, element, draw, lambda _: size, calculate_pos_func, store_pos_func
|
|
237
|
+
)
|
|
@@ -0,0 +1,127 @@
|
|
|
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
|
decksmith/templates/deck.csv
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
title;
|
|
2
|
-
Welcome to DeckSmith!;
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
Check out the guide;Visit "github.com/julynx/decksmith" to learn all the things you can do using DeckSmith. Enjoy!
|
|
1
|
+
title;first-paragraph;second-paragraph
|
|
2
|
+
Welcome to DeckSmith!;This is a card. You can add as many cards as you need to your deck by adding a new line to the data file (CSV).;The layout file is a YAML file that holds the structure for all the cards in the deck.
|
|
3
|
+
Connecting the dots;Any column in the data file (CSV) can be referenced from the layout file as "%colname%".;Cards can have all kinds of elements, such as images or shapes, that are layered in the order they appear in the layout file.
|
|
4
|
+
Check out the docs;Shapes can have transparency, images can be transformed using filters, and elements can be placed relative to each other. You can also use custom fonts and much, much more!;Visit julynx.github.io/decksmith/docs.html to discover all you can do with DeckSmith. Enjoy!
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
id: canvas
|
|
2
|
+
width: 1000
|
|
3
|
+
height: 1400
|
|
4
|
+
background_color: [220, 220, 220]
|
|
5
|
+
elements:
|
|
6
|
+
- id: title
|
|
7
|
+
type: text
|
|
8
|
+
position: [0, 120]
|
|
9
|
+
relative_to: [canvas, top-center]
|
|
10
|
+
anchor: top-center
|
|
11
|
+
text: "%title%"
|
|
12
|
+
color: [50, 50, 100]
|
|
13
|
+
font_path: arial.ttf
|
|
14
|
+
font_size: 64
|
|
15
|
+
align: center
|
|
16
|
+
- id: line
|
|
17
|
+
type: rectangle
|
|
18
|
+
size: [600, 2]
|
|
19
|
+
position: [0, 100]
|
|
20
|
+
relative_to: [title, bottom-center]
|
|
21
|
+
anchor: top-center
|
|
22
|
+
color: [100, 100, 100]
|
|
23
|
+
- id: first-paragraph
|
|
24
|
+
type: text
|
|
25
|
+
position: [0, 200]
|
|
26
|
+
relative_to: [title, bottom-center]
|
|
27
|
+
anchor: top-center
|
|
28
|
+
text: "%first-paragraph%"
|
|
29
|
+
color: [50, 100, 100]
|
|
30
|
+
font_path: arial.ttf
|
|
31
|
+
font_size: 48
|
|
32
|
+
line_spacing: 20
|
|
33
|
+
width: 800
|
|
34
|
+
align: justify
|
|
35
|
+
- id: second-paragraph
|
|
36
|
+
type: text
|
|
37
|
+
position: [0, 100]
|
|
38
|
+
relative_to: [first-paragraph, bottom-center]
|
|
39
|
+
anchor: top-center
|
|
40
|
+
text: "%second-paragraph%"
|
|
41
|
+
color: [50, 100, 100]
|
|
42
|
+
font_path: arial.ttf
|
|
43
|
+
font_size: 48
|
|
44
|
+
line_spacing: 20
|
|
45
|
+
width: 800
|
|
46
|
+
align: justify
|
decksmith/utils.py
CHANGED
|
@@ -43,24 +43,30 @@ def apply_anchor(size: Tuple[int, ...], anchor: str) -> Tuple[int, int]:
|
|
|
43
43
|
Tuple[int, int]: A tuple representing the position (x, y) based on the anchor.
|
|
44
44
|
"""
|
|
45
45
|
if len(size) == 2:
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
width, height = size
|
|
47
|
+
position_horizontal, position_vertical = 0, 0
|
|
48
48
|
elif len(size) == 4:
|
|
49
|
-
|
|
50
|
-
|
|
49
|
+
position_horizontal, position_vertical, position_right, position_bottom = size
|
|
50
|
+
width, height = (
|
|
51
|
+
position_right - position_horizontal,
|
|
52
|
+
position_bottom - position_vertical,
|
|
53
|
+
)
|
|
51
54
|
else:
|
|
52
55
|
raise ValueError("Size must be a tuple of 2 or 4 integers.")
|
|
53
56
|
|
|
54
57
|
anchor_points = {
|
|
55
|
-
"top-left": (
|
|
56
|
-
"top-center": (
|
|
57
|
-
"top-right": (
|
|
58
|
-
"middle-left": (
|
|
59
|
-
"center": (
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
"
|
|
58
|
+
"top-left": (position_horizontal, position_vertical),
|
|
59
|
+
"top-center": (position_horizontal + width // 2, position_vertical),
|
|
60
|
+
"top-right": (position_horizontal + width, position_vertical),
|
|
61
|
+
"middle-left": (position_horizontal, position_vertical + height // 2),
|
|
62
|
+
"center": (
|
|
63
|
+
position_horizontal + width // 2,
|
|
64
|
+
position_vertical + height // 2,
|
|
65
|
+
),
|
|
66
|
+
"middle-right": (position_horizontal + width, position_vertical + height // 2),
|
|
67
|
+
"bottom-left": (position_horizontal, position_vertical + height),
|
|
68
|
+
"bottom-center": (position_horizontal + width // 2, position_vertical + height),
|
|
69
|
+
"bottom-right": (position_horizontal + width, position_vertical + height),
|
|
64
70
|
}
|
|
65
71
|
|
|
66
72
|
if anchor not in anchor_points:
|
decksmith/validate.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: decksmith
|
|
3
|
-
Version: 0.1
|
|
4
|
-
Summary: A command-line application to dynamically generate decks of cards from a
|
|
3
|
+
Version: 0.9.1
|
|
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
|
|
7
7
|
Author-email: juliocabria@tutanota.com
|
|
@@ -13,27 +13,26 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
13
13
|
Classifier: Programming Language :: Python :: 3.14
|
|
14
14
|
Provides-Extra: dev
|
|
15
15
|
Requires-Dist: click
|
|
16
|
+
Requires-Dist: crossfiledialog (>=1.1.0)
|
|
17
|
+
Requires-Dist: flask (>=3.1.2)
|
|
16
18
|
Requires-Dist: jval (==1.0.6)
|
|
17
19
|
Requires-Dist: pandas
|
|
18
20
|
Requires-Dist: pillow (>=11.3.0)
|
|
19
|
-
Requires-Dist:
|
|
21
|
+
Requires-Dist: platformdirs (>=4.5.1)
|
|
20
22
|
Requires-Dist: pytest ; extra == "dev"
|
|
23
|
+
Requires-Dist: pywin32 (>=311) ; sys_platform == "win32"
|
|
21
24
|
Requires-Dist: reportlab (>=4.4.3)
|
|
22
|
-
|
|
25
|
+
Requires-Dist: ruamel-yaml (>=0.19.1)
|
|
26
|
+
Requires-Dist: waitress (>=3.0.2)
|
|
23
27
|
Description-Content-Type: text/markdown
|
|
24
28
|
|
|
25
29
|
# DeckSmith
|
|
26
30
|
|
|
27
|
-
*A
|
|
31
|
+
*A powerful application to dynamically generate decks of cards from a YAML specification and a CSV data file.*
|
|
28
32
|
|
|
29
33
|
<br>
|
|
30
34
|
<p align="center">
|
|
31
|
-
<img
|
|
32
|
-
</p>
|
|
33
|
-
|
|
34
|
-
<br>
|
|
35
|
-
<p align="center">
|
|
36
|
-
<img width="600" src="https://raw.githubusercontent.com/Julynx/decksmith/refs/heads/main/docs/assets/banner.png">
|
|
35
|
+
<img src="https://raw.githubusercontent.com/Julynx/decksmith/refs/heads/main/docs/assets/screenshot.png" width='100%'>
|
|
37
36
|
</p>
|
|
38
37
|
|
|
39
38
|
<br>
|
|
@@ -44,29 +43,64 @@ DeckSmith is ideal for automating the creation of all kinds of decks, including
|
|
|
44
43
|
|
|
45
44
|
- ✨ Consistent layout and formatting across all cards. Define once, edit anytime, generate as many cards as you need.
|
|
46
45
|
- 🍳 Pure python, with easy installation via pip.
|
|
46
|
+
- 🖥️ User-friendly GUI for easy project management and generation.
|
|
47
47
|
- ⚡ Highly performant card generation using parallel processing.
|
|
48
|
-
- 📖 Intuitive syntax and extensive [documentation](https://github.com/Julynx/decksmith/blob/main/docs/DOCS.md) with examples to help you get started
|
|
48
|
+
- 📖 Intuitive syntax and extensive [documentation](https://github.com/Julynx/decksmith/blob/main/docs/DOCS.md) with examples to help you get started.
|
|
49
49
|
- 🧰 Tons of powerful features such as:
|
|
50
50
|
- [Start from a sample project and edit it instead of starting from scratch](https://github.com/Julynx/decksmith/blob/main/docs/DOCS.md#creating-a-project)
|
|
51
51
|
- [Extensive support for images](https://github.com/Julynx/decksmith/blob/main/docs/DOCS.md#images), [text](https://github.com/Julynx/decksmith/blob/main/docs/DOCS.md#text), [and all kinds of different shapes](https://github.com/Julynx/decksmith/blob/main/docs/DOCS.md#shapes)
|
|
52
|
-
- [Link any field to a column in the CSV file](https://github.com/Julynx/decksmith/blob/main/docs/DOCS.md#
|
|
52
|
+
- [Link any field to a column in the CSV file](https://github.com/Julynx/decksmith/blob/main/docs/DOCS.md#example-with-deckcsv)
|
|
53
53
|
- [Position elements absolutely or relative to other elements, using anchors to simplify placement](https://github.com/Julynx/decksmith/blob/main/docs/DOCS.md#positioning)
|
|
54
54
|
- [Powerful image transformations using filters like crop, resize, rotate, or flip](https://github.com/Julynx/decksmith/blob/main/docs/DOCS.md#images)
|
|
55
55
|
- [Export your deck as images or as a PDF for printing](https://github.com/Julynx/decksmith/blob/main/docs/DOCS.md#building-the-deck)
|
|
56
56
|
|
|
57
57
|
## Getting started
|
|
58
58
|
|
|
59
|
-
|
|
59
|
+
### Installation
|
|
60
|
+
|
|
61
|
+
- To begin, install DeckSmith by running:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
pip install decksmith
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
- To launch the GUI, run:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
decksmith --gui
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Creating a project
|
|
74
|
+
|
|
75
|
+
- Run the following command to start from sample `deck.yaml` and `deck.csv` files:
|
|
76
|
+
|
|
77
|
+
```text
|
|
78
|
+
decksmith init
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
- `deck.yaml` defines the layout for the cards in the deck.
|
|
82
|
+
- `deck.csv` holds the data for each card, like the content of the text fields and the image paths.
|
|
83
|
+
|
|
84
|
+
### Defining the layout
|
|
85
|
+
|
|
86
|
+
- Edit `deck.yaml` to include all the elements you want on your cards.
|
|
87
|
+
You can find a complete list of all the available elements and their properties in the [documentation](https://github.com/Julynx/decksmith/blob/main/docs/DOCS.md).
|
|
88
|
+
|
|
89
|
+
- You can reference any column from `deck.csv` in the `deck.yaml` file as `%column_name%`.
|
|
60
90
|
|
|
61
|
-
|
|
91
|
+
### Building the deck
|
|
62
92
|
|
|
63
|
-
-
|
|
93
|
+
- When you are ready to generate the deck images, run:
|
|
64
94
|
|
|
65
|
-
|
|
95
|
+
```text
|
|
96
|
+
decksmith build
|
|
97
|
+
```
|
|
66
98
|
|
|
67
|
-
|
|
99
|
+
- After building a deck, you can export it to PDF by running:
|
|
68
100
|
|
|
69
|
-
|
|
101
|
+
```text
|
|
102
|
+
decksmith export
|
|
103
|
+
```
|
|
70
104
|
|
|
71
105
|
## Documentation
|
|
72
106
|
|
|
@@ -0,0 +1,26 @@
|
|
|
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,,
|
decksmith/templates/deck.json
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"width": 250,
|
|
3
|
-
"height": 350,
|
|
4
|
-
"background_color": [200, 200, 200],
|
|
5
|
-
"elements": [
|
|
6
|
-
{
|
|
7
|
-
"id": "title",
|
|
8
|
-
"type": "text",
|
|
9
|
-
"position": [125, 25],
|
|
10
|
-
"anchor": "top-center",
|
|
11
|
-
"text": "%title%",
|
|
12
|
-
"color": [0, 0, 0],
|
|
13
|
-
"font_path": "arial.ttf",
|
|
14
|
-
"font_size": 20,
|
|
15
|
-
"align": "center"
|
|
16
|
-
},
|
|
17
|
-
{
|
|
18
|
-
"id": "description",
|
|
19
|
-
"type": "text",
|
|
20
|
-
"position": [0, 25],
|
|
21
|
-
"relative_to": ["title", "bottom-center"],
|
|
22
|
-
"anchor": "top-center",
|
|
23
|
-
"text": "%description%",
|
|
24
|
-
"color": [0, 0, 0],
|
|
25
|
-
"font_path": "arial.ttf",
|
|
26
|
-
"font_size": 16,
|
|
27
|
-
"width": 200,
|
|
28
|
-
"align": "center"
|
|
29
|
-
}
|
|
30
|
-
]
|
|
31
|
-
}
|