decksmith 0.1.15__py3-none-any.whl → 0.9.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.
- decksmith/card_builder.py +78 -565
- decksmith/deck_builder.py +38 -54
- decksmith/export.py +170 -168
- decksmith/gui/__init__.py +0 -0
- decksmith/gui/app.py +341 -0
- decksmith/gui/static/css/style.css +707 -0
- decksmith/gui/static/img/decksmith.ico +0 -0
- decksmith/gui/static/js/main.js +583 -0
- decksmith/gui/templates/index.html +184 -0
- decksmith/image_ops.py +121 -0
- decksmith/logger.py +39 -0
- decksmith/macro.py +46 -0
- decksmith/main.py +31 -23
- 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 +75 -69
- decksmith/validate.py +132 -132
- {decksmith-0.1.15.dist-info → decksmith-0.9.2.dist-info}/METADATA +23 -15
- decksmith-0.9.2.dist-info/RECORD +27 -0
- {decksmith-0.1.15.dist-info → decksmith-0.9.2.dist-info}/WHEEL +1 -1
- decksmith/templates/deck.json +0 -31
- decksmith-0.1.15.dist-info/RECORD +0 -13
- {decksmith-0.1.15.dist-info → decksmith-0.9.2.dist-info}/entry_points.txt +0 -0
decksmith/deck_builder.py
CHANGED
|
@@ -1,87 +1,69 @@
|
|
|
1
1
|
"""
|
|
2
2
|
This module contains the DeckBuilder class,
|
|
3
|
-
which is used to create a deck of cards based on a
|
|
3
|
+
which is used to create a deck of cards based on a YAML specification
|
|
4
4
|
and a CSV file.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
import concurrent.futures
|
|
8
|
-
import json
|
|
9
8
|
from pathlib import Path
|
|
10
|
-
from typing import
|
|
9
|
+
from typing import Any, Dict, List, Optional
|
|
11
10
|
|
|
12
11
|
import pandas as pd
|
|
13
12
|
from pandas import Series
|
|
13
|
+
from ruamel.yaml import YAML
|
|
14
14
|
|
|
15
|
-
from .card_builder import CardBuilder
|
|
15
|
+
from decksmith.card_builder import CardBuilder
|
|
16
|
+
from decksmith.logger import logger
|
|
17
|
+
from decksmith.macro import MacroResolver
|
|
16
18
|
|
|
17
19
|
|
|
18
20
|
class DeckBuilder:
|
|
19
21
|
"""
|
|
20
|
-
A class to build a deck of cards based on a
|
|
22
|
+
A class to build a deck of cards based on a YAML specification and a CSV file.
|
|
21
23
|
Attributes:
|
|
22
|
-
spec_path (Path): Path to the
|
|
24
|
+
spec_path (Path): Path to the YAML specification file.
|
|
23
25
|
csv_path (Union[Path, None]): Path to the CSV file containing card data.
|
|
24
26
|
cards (list): List of CardBuilder instances for each card in the deck.
|
|
25
27
|
"""
|
|
26
28
|
|
|
27
|
-
def __init__(self, spec_path: Path, csv_path:
|
|
29
|
+
def __init__(self, spec_path: Path, csv_path: Optional[Path] = None):
|
|
28
30
|
"""
|
|
29
|
-
Initializes the DeckBuilder with a
|
|
31
|
+
Initializes the DeckBuilder with a YAML specification file and a CSV file.
|
|
30
32
|
Args:
|
|
31
|
-
spec_path (Path): Path to the
|
|
33
|
+
spec_path (Path): Path to the YAML specification file.
|
|
32
34
|
csv_path (Union[Path, None]): Path to the CSV file containing card data.
|
|
33
35
|
"""
|
|
34
36
|
self.spec_path = spec_path
|
|
35
37
|
self.csv_path = csv_path
|
|
36
38
|
self.cards: List[CardBuilder] = []
|
|
39
|
+
self._spec_cache: Optional[Dict[str, Any]] = None
|
|
37
40
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
"""
|
|
47
|
-
|
|
48
|
-
def replace_in_value(value: Any) -> Any:
|
|
49
|
-
if isinstance(value, str):
|
|
50
|
-
stripped_value = value.strip()
|
|
51
|
-
# First, check for an exact macro match to preserve type
|
|
52
|
-
for key in row:
|
|
53
|
-
if stripped_value == f"%{key}%":
|
|
54
|
-
return row[key] # Return the raw value, preserving type
|
|
55
|
-
|
|
56
|
-
# If no exact match, perform standard string replacement for all macros
|
|
57
|
-
for key, val in row.items():
|
|
58
|
-
value = value.replace(f"%{key}%", str(val))
|
|
59
|
-
return value
|
|
60
|
-
|
|
61
|
-
if isinstance(value, list):
|
|
62
|
-
return [replace_in_value(v) for v in value]
|
|
63
|
-
|
|
64
|
-
if isinstance(value, dict):
|
|
65
|
-
return {k: replace_in_value(v) for k, v in value.items()}
|
|
66
|
-
|
|
67
|
-
return value
|
|
68
|
-
|
|
69
|
-
with open(self.spec_path, "r", encoding="utf-8") as f:
|
|
70
|
-
spec = json.load(f)
|
|
71
|
-
return replace_in_value(spec)
|
|
41
|
+
@property
|
|
42
|
+
def spec(self) -> Dict[str, Any]:
|
|
43
|
+
"""Loads and caches the spec file."""
|
|
44
|
+
if self._spec_cache is None:
|
|
45
|
+
yaml = YAML()
|
|
46
|
+
with open(self.spec_path, "r", encoding="utf-8") as spec_file:
|
|
47
|
+
self._spec_cache = yaml.load(spec_file)
|
|
48
|
+
return self._spec_cache
|
|
72
49
|
|
|
73
50
|
def build_deck(self, output_path: Path):
|
|
74
51
|
"""
|
|
75
52
|
Builds the deck of cards by reading the CSV file and creating CardBuilder instances.
|
|
76
53
|
"""
|
|
54
|
+
base_path = self.spec_path.parent if self.spec_path else None
|
|
55
|
+
|
|
77
56
|
if not self.csv_path or not self.csv_path.exists():
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
card_builder = CardBuilder(spec)
|
|
57
|
+
logger.info("No CSV file found. Building single card from spec.")
|
|
58
|
+
card_builder = CardBuilder(self.spec, base_path=base_path)
|
|
81
59
|
card_builder.build(output_path / "card_1.png")
|
|
82
60
|
return
|
|
83
61
|
|
|
84
|
-
|
|
62
|
+
try:
|
|
63
|
+
dataframe = pd.read_csv(self.csv_path, encoding="utf-8", sep=";", header=0)
|
|
64
|
+
except Exception as e:
|
|
65
|
+
logger.error("Error reading CSV file: %s", e)
|
|
66
|
+
return
|
|
85
67
|
|
|
86
68
|
def build_card(row_tuple: tuple[int, Series]):
|
|
87
69
|
"""
|
|
@@ -90,12 +72,14 @@ class DeckBuilder:
|
|
|
90
72
|
row_tuple (tuple[int, Series]): A tuple containing the row index and the row data.
|
|
91
73
|
"""
|
|
92
74
|
idx, row = row_tuple
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
75
|
+
try:
|
|
76
|
+
# We need a deep copy of the spec for each card to avoid side effects
|
|
77
|
+
# But resolve_macros creates a new structure, so it should be fine
|
|
78
|
+
spec = MacroResolver.resolve(self.spec, row.to_dict())
|
|
79
|
+
card_builder = CardBuilder(spec, base_path=base_path)
|
|
80
|
+
card_builder.build(output_path / f"card_{idx + 1}.png")
|
|
81
|
+
except Exception as e:
|
|
82
|
+
logger.error("Error building card %s: %s", idx + 1, e)
|
|
96
83
|
|
|
97
84
|
with concurrent.futures.ThreadPoolExecutor() as executor:
|
|
98
|
-
list(executor.map(build_card,
|
|
99
|
-
|
|
100
|
-
# for row_tuple in df.iterrows():
|
|
101
|
-
# build_card(row_tuple)
|
|
85
|
+
list(executor.map(build_card, dataframe.iterrows()))
|
decksmith/export.py
CHANGED
|
@@ -1,168 +1,170 @@
|
|
|
1
|
-
"""
|
|
2
|
-
This module provides the functionality to export images from a folder to a PDF file.
|
|
3
|
-
"""
|
|
4
|
-
|
|
5
|
-
import
|
|
6
|
-
from
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
from reportlab.lib.
|
|
10
|
-
from reportlab.
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
self.
|
|
31
|
-
self.
|
|
32
|
-
self.
|
|
33
|
-
self.
|
|
34
|
-
self.
|
|
35
|
-
self.
|
|
36
|
-
self.
|
|
37
|
-
self.
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
"""
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
self.pdf.
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
1
|
+
"""
|
|
2
|
+
This module provides the functionality to export images from a folder to a PDF file.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import List, Tuple
|
|
7
|
+
|
|
8
|
+
from reportlab.lib.pagesizes import A4
|
|
9
|
+
from reportlab.lib.units import mm
|
|
10
|
+
from reportlab.pdfgen import canvas
|
|
11
|
+
|
|
12
|
+
from decksmith.logger import logger
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class PdfExporter:
|
|
16
|
+
"""
|
|
17
|
+
A class to export images from a folder to a PDF file.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(
|
|
21
|
+
self,
|
|
22
|
+
image_folder: Path,
|
|
23
|
+
output_path: Path,
|
|
24
|
+
page_size_str: str = "A4",
|
|
25
|
+
image_width: float = 63,
|
|
26
|
+
image_height: float = 88,
|
|
27
|
+
gap: float = 0,
|
|
28
|
+
margins: Tuple[float, float] = (2, 2),
|
|
29
|
+
):
|
|
30
|
+
self.image_folder = image_folder
|
|
31
|
+
self.image_paths = self._get_image_paths()
|
|
32
|
+
self.output_path = output_path
|
|
33
|
+
self.page_size = self._get_page_size(page_size_str)
|
|
34
|
+
self.image_width = image_width * mm
|
|
35
|
+
self.image_height = image_height * mm
|
|
36
|
+
self.gap = gap * mm
|
|
37
|
+
self.margins = (margins[0] * mm, margins[1] * mm)
|
|
38
|
+
self.pdf = canvas.Canvas(str(self.output_path), pagesize=self.page_size)
|
|
39
|
+
|
|
40
|
+
def _get_image_paths(self) -> List[Path]:
|
|
41
|
+
"""
|
|
42
|
+
Scans the image folder and returns a list of image paths.
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
List[str]: A sorted list of image paths.
|
|
46
|
+
"""
|
|
47
|
+
image_extensions = {".png", ".jpg", ".jpeg", ".webp"}
|
|
48
|
+
return sorted(
|
|
49
|
+
[
|
|
50
|
+
image_path
|
|
51
|
+
for image_path in self.image_folder.iterdir()
|
|
52
|
+
if image_path.suffix.lower() in image_extensions
|
|
53
|
+
]
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
def _get_page_size(self, page_size_str: str) -> Tuple[float, float]:
|
|
57
|
+
"""
|
|
58
|
+
Returns the page size from a string.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
page_size_str (str): The string representing the page size.
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
Tuple[float, float]: The page size in points.
|
|
65
|
+
"""
|
|
66
|
+
if page_size_str.lower() == "a4":
|
|
67
|
+
return A4
|
|
68
|
+
# Add other page sizes here if needed
|
|
69
|
+
return A4
|
|
70
|
+
|
|
71
|
+
def _calculate_layout(self, page_width: float, page_height: float):
|
|
72
|
+
"""
|
|
73
|
+
Calculates the optimal layout for the images on the page.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
page_width (float): The width of the page.
|
|
77
|
+
page_height (float): The height of the page.
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
Tuple[int, int, bool]: The number of columns, rows, and if the layout is rotated.
|
|
81
|
+
"""
|
|
82
|
+
best_fit = 0
|
|
83
|
+
best_layout = (0, 0, False)
|
|
84
|
+
|
|
85
|
+
for rotated in [False, True]:
|
|
86
|
+
image_width, image_height = (
|
|
87
|
+
(self.image_width, self.image_height)
|
|
88
|
+
if not rotated
|
|
89
|
+
else (self.image_height, self.image_width)
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
cols = int(
|
|
93
|
+
(page_width - 2 * self.margins[0] + self.gap) / (image_width + self.gap)
|
|
94
|
+
)
|
|
95
|
+
rows = int(
|
|
96
|
+
(page_height - 2 * self.margins[1] + self.gap)
|
|
97
|
+
/ (image_height + self.gap)
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
if cols * rows > best_fit:
|
|
101
|
+
best_fit = cols * rows
|
|
102
|
+
best_layout = (cols, rows, rotated)
|
|
103
|
+
|
|
104
|
+
return best_layout
|
|
105
|
+
|
|
106
|
+
def export(self):
|
|
107
|
+
"""
|
|
108
|
+
Exports the images to a PDF file.
|
|
109
|
+
"""
|
|
110
|
+
try:
|
|
111
|
+
page_width, page_height = self.page_size
|
|
112
|
+
cols, rows, rotated = self._calculate_layout(page_width, page_height)
|
|
113
|
+
|
|
114
|
+
if cols == 0 or rows == 0:
|
|
115
|
+
raise ValueError("The images are too large to fit on the page.")
|
|
116
|
+
|
|
117
|
+
image_width, image_height = (
|
|
118
|
+
(self.image_width, self.image_height)
|
|
119
|
+
if not rotated
|
|
120
|
+
else (self.image_height, self.image_width)
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
total_width = cols * image_width + (cols - 1) * self.gap
|
|
124
|
+
total_height = rows * image_height + (rows - 1) * self.gap
|
|
125
|
+
start_horizontal = (page_width - total_width) / 2
|
|
126
|
+
start_vertical = (page_height - total_height) / 2
|
|
127
|
+
|
|
128
|
+
images_on_page = 0
|
|
129
|
+
for image_path in self.image_paths:
|
|
130
|
+
if images_on_page > 0 and images_on_page % (cols * rows) == 0:
|
|
131
|
+
self.pdf.showPage()
|
|
132
|
+
images_on_page = 0
|
|
133
|
+
|
|
134
|
+
row = images_on_page // cols
|
|
135
|
+
col = images_on_page % cols
|
|
136
|
+
|
|
137
|
+
position_horizontal = start_horizontal + col * (image_width + self.gap)
|
|
138
|
+
position_vertical = start_vertical + row * (image_height + self.gap)
|
|
139
|
+
|
|
140
|
+
if not rotated:
|
|
141
|
+
self.pdf.drawImage(
|
|
142
|
+
str(image_path),
|
|
143
|
+
position_horizontal,
|
|
144
|
+
position_vertical,
|
|
145
|
+
width=image_width,
|
|
146
|
+
height=image_height,
|
|
147
|
+
preserveAspectRatio=True,
|
|
148
|
+
)
|
|
149
|
+
else:
|
|
150
|
+
self.pdf.saveState()
|
|
151
|
+
center_horizontal = position_horizontal + image_width / 2
|
|
152
|
+
center_vertical = position_vertical + image_height / 2
|
|
153
|
+
self.pdf.translate(center_horizontal, center_vertical)
|
|
154
|
+
self.pdf.rotate(90)
|
|
155
|
+
self.pdf.drawImage(
|
|
156
|
+
str(image_path),
|
|
157
|
+
-image_height / 2,
|
|
158
|
+
-image_width / 2,
|
|
159
|
+
width=image_height,
|
|
160
|
+
height=image_width,
|
|
161
|
+
preserveAspectRatio=True,
|
|
162
|
+
)
|
|
163
|
+
self.pdf.restoreState()
|
|
164
|
+
images_on_page += 1
|
|
165
|
+
|
|
166
|
+
self.pdf.save()
|
|
167
|
+
logger.info("Successfully exported PDF to %s", self.output_path)
|
|
168
|
+
except Exception as e:
|
|
169
|
+
logger.error("An error occurred during PDF export: %s", e)
|
|
170
|
+
raise
|
|
File without changes
|