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.
- decksmith/card_builder.py +66 -35
- decksmith/deck_builder.py +95 -85
- decksmith/export.py +4 -1
- decksmith/gui/app.py +25 -20
- decksmith/gui/static/css/style.css +105 -33
- decksmith/gui/static/img/decksmith.ico +0 -0
- decksmith/gui/static/js/main.js +169 -115
- decksmith/gui/templates/index.html +184 -182
- decksmith/image_ops.py +11 -0
- decksmith/main.py +7 -6
- decksmith/project.py +35 -39
- decksmith/renderers/image.py +4 -6
- decksmith/renderers/text.py +153 -127
- decksmith/validate.py +14 -2
- {decksmith-0.9.1.dist-info → decksmith-0.9.3.dist-info}/METADATA +3 -1
- decksmith-0.9.3.dist-info/RECORD +27 -0
- decksmith-0.9.1.dist-info/RECORD +0 -26
- {decksmith-0.9.1.dist-info → decksmith-0.9.3.dist-info}/WHEEL +0 -0
- {decksmith-0.9.1.dist-info → decksmith-0.9.3.dist-info}/entry_points.txt +0 -0
decksmith/card_builder.py
CHANGED
|
@@ -4,6 +4,7 @@ which is used to create card images based on a YAML specification.
|
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
6
|
import operator
|
|
7
|
+
import traceback
|
|
7
8
|
from pathlib import Path
|
|
8
9
|
from typing import Any, Dict, Optional, Tuple
|
|
9
10
|
|
|
@@ -52,14 +53,17 @@ class CardBuilder:
|
|
|
52
53
|
|
|
53
54
|
def _calculate_absolute_position(self, element: Dict[str, Any]) -> Tuple[int, int]:
|
|
54
55
|
"""
|
|
55
|
-
Calculates the absolute position of an element,
|
|
56
|
-
|
|
56
|
+
Calculates the absolute position of an element, resolving relative positioning.
|
|
57
|
+
|
|
57
58
|
Args:
|
|
58
59
|
element (dict): The element dictionary.
|
|
60
|
+
|
|
59
61
|
Returns:
|
|
60
62
|
tuple: The absolute (x, y) position of the element.
|
|
63
|
+
|
|
64
|
+
Raises:
|
|
65
|
+
ValueError: If the referenced element for relative positioning is not found.
|
|
61
66
|
"""
|
|
62
|
-
# If the element has no 'relative_to', return its position directly
|
|
63
67
|
if "relative_to" not in element:
|
|
64
68
|
return tuple(element.get("position", [0, 0]))
|
|
65
69
|
|
|
@@ -85,53 +89,80 @@ class CardBuilder:
|
|
|
85
89
|
def render(self) -> Image.Image:
|
|
86
90
|
"""
|
|
87
91
|
Renders the card image by drawing all elements specified in the YAML.
|
|
92
|
+
|
|
88
93
|
Returns:
|
|
89
94
|
Image.Image: The rendered card image.
|
|
95
|
+
|
|
96
|
+
Raises:
|
|
97
|
+
Exception: If an error occurs during rendering.
|
|
90
98
|
"""
|
|
91
99
|
self.spec = transform_card(self.spec)
|
|
92
100
|
validate_card(self.spec)
|
|
93
101
|
|
|
102
|
+
renderers = {
|
|
103
|
+
"text": self._render_text,
|
|
104
|
+
"image": self._render_image,
|
|
105
|
+
"circle": self._render_shape,
|
|
106
|
+
"ellipse": self._render_shape,
|
|
107
|
+
"polygon": self._render_shape,
|
|
108
|
+
"regular-polygon": self._render_shape,
|
|
109
|
+
"rectangle": self._render_shape,
|
|
110
|
+
}
|
|
111
|
+
|
|
94
112
|
for element in self.spec.get("elements", []):
|
|
95
113
|
element_type = element.get("type")
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
element,
|
|
108
|
-
self._calculate_absolute_position,
|
|
109
|
-
self._store_element_position,
|
|
110
|
-
)
|
|
111
|
-
elif element_type in [
|
|
112
|
-
"circle",
|
|
113
|
-
"ellipse",
|
|
114
|
-
"polygon",
|
|
115
|
-
"regular-polygon",
|
|
116
|
-
"rectangle",
|
|
117
|
-
]:
|
|
118
|
-
self.card = self.shape_renderer.render(
|
|
119
|
-
self.card,
|
|
120
|
-
element,
|
|
121
|
-
self._calculate_absolute_position,
|
|
122
|
-
self._store_element_position,
|
|
114
|
+
handler = renderers.get(element_type)
|
|
115
|
+
|
|
116
|
+
if handler:
|
|
117
|
+
try:
|
|
118
|
+
handler(element)
|
|
119
|
+
except Exception as exc:
|
|
120
|
+
logger.error(
|
|
121
|
+
"Error drawing element %s: %s\n%s",
|
|
122
|
+
element_type,
|
|
123
|
+
exc,
|
|
124
|
+
traceback.format_exc(),
|
|
123
125
|
)
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
logger.error("Error drawing element %s: %s", element_type, e)
|
|
128
|
-
# Continue drawing other elements
|
|
126
|
+
raise
|
|
127
|
+
else:
|
|
128
|
+
logger.warning("Unknown element type: %s", element_type)
|
|
129
129
|
|
|
130
130
|
return self.card
|
|
131
131
|
|
|
132
|
+
def _render_text(self, element: Dict[str, Any]):
|
|
133
|
+
"""Renders a text element."""
|
|
134
|
+
self.card = self.text_renderer.render(
|
|
135
|
+
self.card,
|
|
136
|
+
element,
|
|
137
|
+
self._calculate_absolute_position,
|
|
138
|
+
self._store_element_position,
|
|
139
|
+
)
|
|
140
|
+
self.draw = ImageDraw.Draw(self.card, "RGBA")
|
|
141
|
+
|
|
142
|
+
def _render_image(self, element: Dict[str, Any]):
|
|
143
|
+
"""Renders an image element."""
|
|
144
|
+
self.image_renderer.render(
|
|
145
|
+
self.card,
|
|
146
|
+
element,
|
|
147
|
+
self._calculate_absolute_position,
|
|
148
|
+
self._store_element_position,
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
def _render_shape(self, element: Dict[str, Any]):
|
|
152
|
+
"""Renders a shape element."""
|
|
153
|
+
self.card = self.shape_renderer.render(
|
|
154
|
+
self.card,
|
|
155
|
+
element,
|
|
156
|
+
self._calculate_absolute_position,
|
|
157
|
+
self._store_element_position,
|
|
158
|
+
)
|
|
159
|
+
# Re-create draw object because shape renderer might have composited a new image
|
|
160
|
+
self.draw = ImageDraw.Draw(self.card, "RGBA")
|
|
161
|
+
|
|
132
162
|
def build(self, output_path: Path):
|
|
133
163
|
"""
|
|
134
164
|
Builds the card image and saves it to the specified path.
|
|
165
|
+
|
|
135
166
|
Args:
|
|
136
167
|
output_path (Path): The path where the card image will be saved.
|
|
137
168
|
"""
|
decksmith/deck_builder.py
CHANGED
|
@@ -1,85 +1,95 @@
|
|
|
1
|
-
"""
|
|
2
|
-
This module contains the DeckBuilder class,
|
|
3
|
-
which is used to create a deck of cards based on a YAML specification
|
|
4
|
-
and a CSV file.
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
import concurrent.futures
|
|
8
|
-
|
|
9
|
-
from
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
from
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
from decksmith.
|
|
17
|
-
from decksmith.
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
self.
|
|
38
|
-
self.
|
|
39
|
-
self.
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
""
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
"""
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
1
|
+
"""
|
|
2
|
+
This module contains the DeckBuilder class,
|
|
3
|
+
which is used to create a deck of cards based on a YAML specification
|
|
4
|
+
and a CSV file.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import concurrent.futures
|
|
8
|
+
import traceback
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any, Dict, List, Optional
|
|
11
|
+
|
|
12
|
+
import pandas as pd
|
|
13
|
+
from pandas import Series
|
|
14
|
+
from ruamel.yaml import YAML
|
|
15
|
+
|
|
16
|
+
from decksmith.card_builder import CardBuilder
|
|
17
|
+
from decksmith.logger import logger
|
|
18
|
+
from decksmith.macro import MacroResolver
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class DeckBuilder:
|
|
22
|
+
"""
|
|
23
|
+
A class to build a deck of cards based on a YAML specification and a CSV file.
|
|
24
|
+
Attributes:
|
|
25
|
+
spec_path (Path): Path to the YAML specification file.
|
|
26
|
+
csv_path (Union[Path, None]): Path to the CSV file containing card data.
|
|
27
|
+
cards (list): List of CardBuilder instances for each card in the deck.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(self, spec_path: Path, csv_path: Optional[Path] = None):
|
|
31
|
+
"""
|
|
32
|
+
Initializes the DeckBuilder with a YAML specification file and a CSV file.
|
|
33
|
+
Args:
|
|
34
|
+
spec_path (Path): Path to the YAML specification file.
|
|
35
|
+
csv_path (Union[Path, None]): Path to the CSV file containing card data.
|
|
36
|
+
"""
|
|
37
|
+
self.spec_path = spec_path
|
|
38
|
+
self.csv_path = csv_path
|
|
39
|
+
self.cards: List[CardBuilder] = []
|
|
40
|
+
self._spec_cache: Optional[Dict[str, Any]] = None
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def spec(self) -> Dict[str, Any]:
|
|
44
|
+
"""Loads and caches the spec file."""
|
|
45
|
+
if self._spec_cache is None:
|
|
46
|
+
yaml = YAML()
|
|
47
|
+
with open(self.spec_path, "r", encoding="utf-8") as spec_file:
|
|
48
|
+
self._spec_cache = yaml.load(spec_file)
|
|
49
|
+
return self._spec_cache
|
|
50
|
+
|
|
51
|
+
def build_deck(self, output_path: Path):
|
|
52
|
+
"""
|
|
53
|
+
Builds the deck of cards by reading the CSV file and creating CardBuilder instances.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
output_path (Path): The directory path to save the built cards.
|
|
57
|
+
"""
|
|
58
|
+
base_path = self.spec_path.parent if self.spec_path else None
|
|
59
|
+
|
|
60
|
+
if not self.csv_path or not self.csv_path.exists():
|
|
61
|
+
logger.info("No CSV file found. Building single card from spec.")
|
|
62
|
+
card_builder = CardBuilder(self.spec, base_path=base_path)
|
|
63
|
+
card_builder.build(output_path / "card_1.png")
|
|
64
|
+
return
|
|
65
|
+
|
|
66
|
+
try:
|
|
67
|
+
dataframe = pd.read_csv(self.csv_path, encoding="utf-8", sep=";", header=0)
|
|
68
|
+
except Exception as e:
|
|
69
|
+
logger.error("Error reading CSV file: %s\n%s", e, traceback.format_exc())
|
|
70
|
+
return
|
|
71
|
+
|
|
72
|
+
def build_card(row_tuple: tuple[int, Series]):
|
|
73
|
+
"""
|
|
74
|
+
Builds a single card from a row of the CSV file.
|
|
75
|
+
Args:
|
|
76
|
+
row_tuple (tuple[int, Series]): A tuple containing the row index and the row data.
|
|
77
|
+
"""
|
|
78
|
+
idx, row = row_tuple
|
|
79
|
+
try:
|
|
80
|
+
# We need a deep copy of the spec for each card to avoid side effects
|
|
81
|
+
# But resolve_macros creates a new structure, so it should be fine
|
|
82
|
+
spec = MacroResolver.resolve(self.spec, row.to_dict())
|
|
83
|
+
card_builder = CardBuilder(spec, base_path=base_path)
|
|
84
|
+
card_builder.build(output_path / f"card_{idx + 1}.png")
|
|
85
|
+
except Exception as exc:
|
|
86
|
+
logger.error(
|
|
87
|
+
"Error building card %s: %s\n%s",
|
|
88
|
+
idx + 1,
|
|
89
|
+
exc,
|
|
90
|
+
traceback.format_exc(),
|
|
91
|
+
)
|
|
92
|
+
raise
|
|
93
|
+
|
|
94
|
+
with concurrent.futures.ThreadPoolExecutor() as executor:
|
|
95
|
+
list(executor.map(build_card, dataframe.iterrows()))
|
decksmith/export.py
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
This module provides the functionality to export images from a folder to a PDF file.
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
|
+
import traceback
|
|
5
6
|
from pathlib import Path
|
|
6
7
|
from typing import List, Tuple
|
|
7
8
|
|
|
@@ -166,5 +167,7 @@ class PdfExporter:
|
|
|
166
167
|
self.pdf.save()
|
|
167
168
|
logger.info("Successfully exported PDF to %s", self.output_path)
|
|
168
169
|
except Exception as e:
|
|
169
|
-
logger.error(
|
|
170
|
+
logger.error(
|
|
171
|
+
"An error occurred during PDF export: %s\n%s", e, traceback.format_exc()
|
|
172
|
+
)
|
|
170
173
|
raise
|
decksmith/gui/app.py
CHANGED
|
@@ -8,6 +8,7 @@ import os
|
|
|
8
8
|
import signal
|
|
9
9
|
import threading
|
|
10
10
|
import time
|
|
11
|
+
import traceback
|
|
11
12
|
import webbrowser
|
|
12
13
|
from io import StringIO
|
|
13
14
|
from pathlib import Path
|
|
@@ -96,8 +97,8 @@ def get_default_path():
|
|
|
96
97
|
default_path.mkdir(parents=True, exist_ok=True)
|
|
97
98
|
return jsonify({"path": str(default_path)})
|
|
98
99
|
except Exception as e:
|
|
99
|
-
logger.error("Error creating default path: %s", e)
|
|
100
|
-
return jsonify({"error": str(e)}), 500
|
|
100
|
+
logger.error("Error creating default path: %s\n%s", e, traceback.format_exc())
|
|
101
|
+
return jsonify({"status": "error", "message": str(e)}), 500
|
|
101
102
|
|
|
102
103
|
|
|
103
104
|
@app.route("/api/system/browse", methods=["POST"])
|
|
@@ -115,23 +116,26 @@ def browse_folder():
|
|
|
115
116
|
return jsonify({"path": folder_path})
|
|
116
117
|
return jsonify({"path": None})
|
|
117
118
|
except ImportError as e:
|
|
118
|
-
logger.error("crossfiledialog import failed: %s", e)
|
|
119
|
-
return jsonify(
|
|
119
|
+
logger.error("crossfiledialog import failed: %s\n%s", e, traceback.format_exc())
|
|
120
|
+
return jsonify(
|
|
121
|
+
{"status": "error", "message": f"Browse feature unavailable: {e}"}
|
|
122
|
+
), 501
|
|
120
123
|
except Exception as e:
|
|
121
|
-
logger.error("Error browsing folder: %s", e)
|
|
122
|
-
return jsonify({"error": str(e)}), 500
|
|
124
|
+
logger.error("Error browsing folder: %s\n%s", e, traceback.format_exc())
|
|
125
|
+
return jsonify({"status": "error", "message": str(e)}), 500
|
|
123
126
|
|
|
124
127
|
|
|
125
128
|
@app.route("/api/project/select", methods=["POST"])
|
|
126
129
|
def select_project():
|
|
127
130
|
"""Selects a project directory."""
|
|
128
|
-
|
|
131
|
+
data = request.json or {}
|
|
132
|
+
path_str = data.get("path")
|
|
129
133
|
if not path_str:
|
|
130
|
-
return jsonify({"error": "Path is required"}), 400
|
|
134
|
+
return jsonify({"status": "error", "message": "Path is required"}), 400
|
|
131
135
|
|
|
132
136
|
path = Path(path_str)
|
|
133
137
|
if not path.exists() or not path.is_dir():
|
|
134
|
-
return jsonify({"error": "Directory does not exist"}), 400
|
|
138
|
+
return jsonify({"status": "error", "message": "Directory does not exist"}), 400
|
|
135
139
|
|
|
136
140
|
project_manager.set_working_dir(path)
|
|
137
141
|
return jsonify({"status": "success", "path": str(path)})
|
|
@@ -147,9 +151,10 @@ def close_project():
|
|
|
147
151
|
@app.route("/api/project/create", methods=["POST"])
|
|
148
152
|
def create_project():
|
|
149
153
|
"""Creates a new project."""
|
|
150
|
-
|
|
154
|
+
data = request.json or {}
|
|
155
|
+
path_str = data.get("path")
|
|
151
156
|
if not path_str:
|
|
152
|
-
return jsonify({"error": "Path is required"}), 400
|
|
157
|
+
return jsonify({"status": "error", "message": "Path is required"}), 400
|
|
153
158
|
|
|
154
159
|
path = Path(path_str)
|
|
155
160
|
|
|
@@ -157,8 +162,8 @@ def create_project():
|
|
|
157
162
|
project_manager.create_project(path)
|
|
158
163
|
return jsonify({"status": "success", "path": str(path)})
|
|
159
164
|
except Exception as e:
|
|
160
|
-
logger.error("Error creating project: %s", e)
|
|
161
|
-
return jsonify({"error": str(e)}), 500
|
|
165
|
+
logger.error("Error creating project: %s\n%s", e, traceback.format_exc())
|
|
166
|
+
return jsonify({"status": "error", "message": str(e)}), 500
|
|
162
167
|
|
|
163
168
|
|
|
164
169
|
@app.route("/api/load", methods=["GET"])
|
|
@@ -172,7 +177,7 @@ def load_files():
|
|
|
172
177
|
def save_files():
|
|
173
178
|
"""Saves project files."""
|
|
174
179
|
if project_manager.get_working_dir() is None:
|
|
175
|
-
return jsonify({"error": "No project selected"}), 400
|
|
180
|
+
return jsonify({"status": "error", "message": "No project selected"}), 400
|
|
176
181
|
|
|
177
182
|
data = request.json
|
|
178
183
|
yaml_content = data.get("yaml")
|
|
@@ -182,8 +187,8 @@ def save_files():
|
|
|
182
187
|
project_manager.save_files(yaml_content, csv_content)
|
|
183
188
|
return jsonify({"status": "success"})
|
|
184
189
|
except Exception as e:
|
|
185
|
-
logger.error("Error saving files: %s", e)
|
|
186
|
-
return jsonify({"error": str(e)}), 500
|
|
190
|
+
logger.error("Error saving files: %s\n%s", e, traceback.format_exc())
|
|
191
|
+
return jsonify({"status": "error", "message": str(e)}), 500
|
|
187
192
|
|
|
188
193
|
|
|
189
194
|
@app.route("/api/cards", methods=["GET"])
|
|
@@ -201,7 +206,7 @@ def list_cards():
|
|
|
201
206
|
csv_table = pd.read_csv(csv_path, sep=";")
|
|
202
207
|
return jsonify(csv_table.to_dict(orient="records"))
|
|
203
208
|
except Exception as e:
|
|
204
|
-
logger.error("Error listing cards: %s", e)
|
|
209
|
+
logger.error("Error listing cards: %s\n%s", e, traceback.format_exc())
|
|
205
210
|
return jsonify({"error": str(e)}), 400
|
|
206
211
|
|
|
207
212
|
|
|
@@ -242,7 +247,7 @@ def preview_card(card_index):
|
|
|
242
247
|
return send_file(image_buffer, mimetype="image/png")
|
|
243
248
|
|
|
244
249
|
except Exception as e:
|
|
245
|
-
logger.error("Error previewing card: %s", e)
|
|
250
|
+
logger.error("Error previewing card: %s\n%s", e, traceback.format_exc())
|
|
246
251
|
return jsonify({"error": str(e)}), 500
|
|
247
252
|
|
|
248
253
|
|
|
@@ -271,7 +276,7 @@ def build_deck():
|
|
|
271
276
|
|
|
272
277
|
return jsonify({"status": "success", "message": f"Deck built in {output_path}"})
|
|
273
278
|
except Exception as e:
|
|
274
|
-
logger.error("Error building deck: %s", e)
|
|
279
|
+
logger.error("Error building deck: %s\n%s", e, traceback.format_exc())
|
|
275
280
|
return jsonify({"error": str(e)}), 500
|
|
276
281
|
|
|
277
282
|
|
|
@@ -313,7 +318,7 @@ def export_pdf():
|
|
|
313
318
|
{"status": "success", "message": f"PDF exported to {output_pdf}"}
|
|
314
319
|
)
|
|
315
320
|
except Exception as e:
|
|
316
|
-
logger.error("Error exporting PDF: %s", e)
|
|
321
|
+
logger.error("Error exporting PDF: %s\n%s", e, traceback.format_exc())
|
|
317
322
|
return jsonify({"error": str(e)}), 500
|
|
318
323
|
|
|
319
324
|
|