decksmith 0.1.12__py3-none-any.whl → 0.1.15__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/deck_builder.py CHANGED
@@ -1,101 +1,101 @@
1
- """
2
- This module contains the DeckBuilder class,
3
- which is used to create a deck of cards based on a JSON specification
4
- and a CSV file.
5
- """
6
-
7
- import concurrent.futures
8
- import json
9
- from pathlib import Path
10
- from typing import Union, List, Dict, Any
11
-
12
- import pandas as pd
13
- from pandas import Series
14
-
15
- from .card_builder import CardBuilder
16
-
17
-
18
- class DeckBuilder:
19
- """
20
- A class to build a deck of cards based on a JSON specification and a CSV file.
21
- Attributes:
22
- spec_path (Path): Path to the JSON specification file.
23
- csv_path (Union[Path, None]): Path to the CSV file containing card data.
24
- cards (list): List of CardBuilder instances for each card in the deck.
25
- """
26
-
27
- def __init__(self, spec_path: Path, csv_path: Union[Path, None] = None):
28
- """
29
- Initializes the DeckBuilder with a JSON specification file and a CSV file.
30
- Args:
31
- spec_path (Path): Path to the JSON specification file.
32
- csv_path (Union[Path, None]): Path to the CSV file containing card data.
33
- """
34
- self.spec_path = spec_path
35
- self.csv_path = csv_path
36
- self.cards: List[CardBuilder] = []
37
-
38
- def _replace_macros(self, row: Dict[str, Any]) -> Dict[str, Any]:
39
- """
40
- Replaces %colname% macros in the card specification with values from the row.
41
- Works recursively for nested structures.
42
- Args:
43
- row (dict): A dictionary representing a row from the CSV file.
44
- Returns:
45
- dict: The updated card specification with macros replaced.
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)
72
-
73
- def build_deck(self, output_path: Path):
74
- """
75
- Builds the deck of cards by reading the CSV file and creating CardBuilder instances.
76
- """
77
- if not self.csv_path or not self.csv_path.exists():
78
- with open(self.spec_path, "r", encoding="utf-8") as f:
79
- spec = json.load(f)
80
- card_builder = CardBuilder(spec)
81
- card_builder.build(output_path / "card_1.png")
82
- return
83
-
84
- df = pd.read_csv(self.csv_path, encoding="utf-8", sep=";", header=0)
85
-
86
- def build_card(row_tuple: tuple[int, Series]):
87
- """
88
- Builds a single card from a row of the CSV file.
89
- Args:
90
- row_tuple (tuple[int, Series]): A tuple containing the row index and the row data.
91
- """
92
- idx, row = row_tuple
93
- spec = self._replace_macros(row.to_dict())
94
- card_builder = CardBuilder(spec)
95
- card_builder.build(output_path / f"card_{idx + 1}.png")
96
-
97
- with concurrent.futures.ThreadPoolExecutor() as executor:
98
- list(executor.map(build_card, df.iterrows()))
99
-
100
- # for row_tuple in df.iterrows():
101
- # build_card(row_tuple)
1
+ """
2
+ This module contains the DeckBuilder class,
3
+ which is used to create a deck of cards based on a JSON specification
4
+ and a CSV file.
5
+ """
6
+
7
+ import concurrent.futures
8
+ import json
9
+ from pathlib import Path
10
+ from typing import Union, List, Dict, Any
11
+
12
+ import pandas as pd
13
+ from pandas import Series
14
+
15
+ from .card_builder import CardBuilder
16
+
17
+
18
+ class DeckBuilder:
19
+ """
20
+ A class to build a deck of cards based on a JSON specification and a CSV file.
21
+ Attributes:
22
+ spec_path (Path): Path to the JSON specification file.
23
+ csv_path (Union[Path, None]): Path to the CSV file containing card data.
24
+ cards (list): List of CardBuilder instances for each card in the deck.
25
+ """
26
+
27
+ def __init__(self, spec_path: Path, csv_path: Union[Path, None] = None):
28
+ """
29
+ Initializes the DeckBuilder with a JSON specification file and a CSV file.
30
+ Args:
31
+ spec_path (Path): Path to the JSON specification file.
32
+ csv_path (Union[Path, None]): Path to the CSV file containing card data.
33
+ """
34
+ self.spec_path = spec_path
35
+ self.csv_path = csv_path
36
+ self.cards: List[CardBuilder] = []
37
+
38
+ def _replace_macros(self, row: Dict[str, Any]) -> Dict[str, Any]:
39
+ """
40
+ Replaces %colname% macros in the card specification with values from the row.
41
+ Works recursively for nested structures.
42
+ Args:
43
+ row (dict): A dictionary representing a row from the CSV file.
44
+ Returns:
45
+ dict: The updated card specification with macros replaced.
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)
72
+
73
+ def build_deck(self, output_path: Path):
74
+ """
75
+ Builds the deck of cards by reading the CSV file and creating CardBuilder instances.
76
+ """
77
+ if not self.csv_path or not self.csv_path.exists():
78
+ with open(self.spec_path, "r", encoding="utf-8") as f:
79
+ spec = json.load(f)
80
+ card_builder = CardBuilder(spec)
81
+ card_builder.build(output_path / "card_1.png")
82
+ return
83
+
84
+ df = pd.read_csv(self.csv_path, encoding="utf-8", sep=";", header=0)
85
+
86
+ def build_card(row_tuple: tuple[int, Series]):
87
+ """
88
+ Builds a single card from a row of the CSV file.
89
+ Args:
90
+ row_tuple (tuple[int, Series]): A tuple containing the row index and the row data.
91
+ """
92
+ idx, row = row_tuple
93
+ spec = self._replace_macros(row.to_dict())
94
+ card_builder = CardBuilder(spec)
95
+ card_builder.build(output_path / f"card_{idx + 1}.png")
96
+
97
+ with concurrent.futures.ThreadPoolExecutor() as executor:
98
+ list(executor.map(build_card, df.iterrows()))
99
+
100
+ # for row_tuple in df.iterrows():
101
+ # build_card(row_tuple)
decksmith/export.py CHANGED
@@ -1,168 +1,168 @@
1
- """
2
- This module provides the functionality to export images from a folder to a PDF file.
3
- """
4
-
5
- import logging
6
- from pathlib import Path
7
- from typing import List, Tuple
8
-
9
- from reportlab.lib.pagesizes import A4
10
- from reportlab.lib.units import mm
11
- from reportlab.pdfgen import canvas
12
-
13
-
14
- class PdfExporter:
15
- """
16
- A class to export images from a folder to a PDF file.
17
- """
18
-
19
- def __init__(
20
- self,
21
- image_folder: Path,
22
- output_path: Path,
23
- page_size_str: str = "A4",
24
- image_width: float = 63,
25
- image_height: float = 88,
26
- gap: float = 0,
27
- margins: Tuple[float, float] = (2, 2),
28
- ):
29
- self.image_folder = image_folder
30
- self.image_paths = self._get_image_paths()
31
- self.output_path = output_path
32
- self.page_size = self._get_page_size(page_size_str)
33
- self.image_width = image_width * mm
34
- self.image_height = image_height * mm
35
- self.gap = gap * mm
36
- self.margins = (margins[0] * mm, margins[1] * mm)
37
- self.pdf = canvas.Canvas(str(self.output_path), pagesize=self.page_size)
38
-
39
- def _get_image_paths(self) -> List[Path]:
40
- """
41
- Scans the image folder and returns a list of image paths.
42
-
43
- Returns:
44
- List[str]: A sorted list of image paths.
45
- """
46
- image_extensions = {".png", ".jpg", ".jpeg", ".webp"}
47
- return sorted(
48
- [
49
- p
50
- for p in self.image_folder.iterdir()
51
- if p.suffix.lower() in image_extensions
52
- ]
53
- )
54
-
55
- def _get_page_size(self, page_size_str: str) -> Tuple[float, float]:
56
- """
57
- Returns the page size from a string.
58
-
59
- Args:
60
- page_size_str (str): The string representing the page size.
61
-
62
- Returns:
63
- Tuple[float, float]: The page size in points.
64
- """
65
- if page_size_str.lower() == "a4":
66
- return A4
67
- # Add other page sizes here if needed
68
- return A4
69
-
70
- def _calculate_layout(self, page_width: float, page_height: float):
71
- """
72
- Calculates the optimal layout for the images on the page.
73
-
74
- Args:
75
- page_width (float): The width of the page.
76
- page_height (float): The height of the page.
77
-
78
- Returns:
79
- Tuple[int, int, bool]: The number of columns, rows, and if the layout is rotated.
80
- """
81
- best_fit = 0
82
- best_layout = (0, 0, False)
83
-
84
- for rotated in [False, True]:
85
- img_w, img_h = (
86
- (self.image_width, self.image_height)
87
- if not rotated
88
- else (self.image_height, self.image_width)
89
- )
90
-
91
- cols = int(
92
- (page_width - 2 * self.margins[0] + self.gap) / (img_w + self.gap)
93
- )
94
- rows = int(
95
- (page_height - 2 * self.margins[1] + self.gap) / (img_h + self.gap)
96
- )
97
-
98
- if cols * rows > best_fit:
99
- best_fit = cols * rows
100
- best_layout = (cols, rows, rotated)
101
-
102
- return best_layout
103
-
104
- def export(self):
105
- """
106
- Exports the images to a PDF file.
107
- """
108
- try:
109
- page_width, page_height = self.page_size
110
- cols, rows, rotated = self._calculate_layout(page_width, page_height)
111
-
112
- if cols == 0 or rows == 0:
113
- raise ValueError("The images are too large to fit on the page.")
114
-
115
- img_w, img_h = (
116
- (self.image_width, self.image_height)
117
- if not rotated
118
- else (self.image_height, self.image_width)
119
- )
120
-
121
- total_width = cols * img_w + (cols - 1) * self.gap
122
- total_height = rows * img_h + (rows - 1) * self.gap
123
- start_x = (page_width - total_width) / 2
124
- start_y = (page_height - total_height) / 2
125
-
126
- images_on_page = 0
127
- for image_path in self.image_paths:
128
- if images_on_page > 0 and images_on_page % (cols * rows) == 0:
129
- self.pdf.showPage()
130
- images_on_page = 0
131
-
132
- row = images_on_page // cols
133
- col = images_on_page % cols
134
-
135
- x = start_x + col * (img_w + self.gap)
136
- y = start_y + row * (img_h + self.gap)
137
-
138
- if not rotated:
139
- self.pdf.drawImage(
140
- str(image_path),
141
- x,
142
- y,
143
- width=img_w,
144
- height=img_h,
145
- preserveAspectRatio=True,
146
- )
147
- else:
148
- self.pdf.saveState()
149
- center_x = x + img_w / 2
150
- center_y = y + img_h / 2
151
- self.pdf.translate(center_x, center_y)
152
- self.pdf.rotate(90)
153
- self.pdf.drawImage(
154
- str(image_path),
155
- -img_h / 2,
156
- -img_w / 2,
157
- width=img_h,
158
- height=img_w,
159
- preserveAspectRatio=True,
160
- )
161
- self.pdf.restoreState()
162
- images_on_page += 1
163
-
164
- self.pdf.save()
165
- logging.info("Successfully exported PDF to %s", self.output_path)
166
- except Exception as e:
167
- logging.error("An error occurred during PDF export: %s", e)
168
- raise
1
+ """
2
+ This module provides the functionality to export images from a folder to a PDF file.
3
+ """
4
+
5
+ import logging
6
+ from pathlib import Path
7
+ from typing import List, Tuple
8
+
9
+ from reportlab.lib.pagesizes import A4
10
+ from reportlab.lib.units import mm
11
+ from reportlab.pdfgen import canvas
12
+
13
+
14
+ class PdfExporter:
15
+ """
16
+ A class to export images from a folder to a PDF file.
17
+ """
18
+
19
+ def __init__(
20
+ self,
21
+ image_folder: Path,
22
+ output_path: Path,
23
+ page_size_str: str = "A4",
24
+ image_width: float = 63,
25
+ image_height: float = 88,
26
+ gap: float = 0,
27
+ margins: Tuple[float, float] = (2, 2),
28
+ ):
29
+ self.image_folder = image_folder
30
+ self.image_paths = self._get_image_paths()
31
+ self.output_path = output_path
32
+ self.page_size = self._get_page_size(page_size_str)
33
+ self.image_width = image_width * mm
34
+ self.image_height = image_height * mm
35
+ self.gap = gap * mm
36
+ self.margins = (margins[0] * mm, margins[1] * mm)
37
+ self.pdf = canvas.Canvas(str(self.output_path), pagesize=self.page_size)
38
+
39
+ def _get_image_paths(self) -> List[Path]:
40
+ """
41
+ Scans the image folder and returns a list of image paths.
42
+
43
+ Returns:
44
+ List[str]: A sorted list of image paths.
45
+ """
46
+ image_extensions = {".png", ".jpg", ".jpeg", ".webp"}
47
+ return sorted(
48
+ [
49
+ p
50
+ for p in self.image_folder.iterdir()
51
+ if p.suffix.lower() in image_extensions
52
+ ]
53
+ )
54
+
55
+ def _get_page_size(self, page_size_str: str) -> Tuple[float, float]:
56
+ """
57
+ Returns the page size from a string.
58
+
59
+ Args:
60
+ page_size_str (str): The string representing the page size.
61
+
62
+ Returns:
63
+ Tuple[float, float]: The page size in points.
64
+ """
65
+ if page_size_str.lower() == "a4":
66
+ return A4
67
+ # Add other page sizes here if needed
68
+ return A4
69
+
70
+ def _calculate_layout(self, page_width: float, page_height: float):
71
+ """
72
+ Calculates the optimal layout for the images on the page.
73
+
74
+ Args:
75
+ page_width (float): The width of the page.
76
+ page_height (float): The height of the page.
77
+
78
+ Returns:
79
+ Tuple[int, int, bool]: The number of columns, rows, and if the layout is rotated.
80
+ """
81
+ best_fit = 0
82
+ best_layout = (0, 0, False)
83
+
84
+ for rotated in [False, True]:
85
+ img_w, img_h = (
86
+ (self.image_width, self.image_height)
87
+ if not rotated
88
+ else (self.image_height, self.image_width)
89
+ )
90
+
91
+ cols = int(
92
+ (page_width - 2 * self.margins[0] + self.gap) / (img_w + self.gap)
93
+ )
94
+ rows = int(
95
+ (page_height - 2 * self.margins[1] + self.gap) / (img_h + self.gap)
96
+ )
97
+
98
+ if cols * rows > best_fit:
99
+ best_fit = cols * rows
100
+ best_layout = (cols, rows, rotated)
101
+
102
+ return best_layout
103
+
104
+ def export(self):
105
+ """
106
+ Exports the images to a PDF file.
107
+ """
108
+ try:
109
+ page_width, page_height = self.page_size
110
+ cols, rows, rotated = self._calculate_layout(page_width, page_height)
111
+
112
+ if cols == 0 or rows == 0:
113
+ raise ValueError("The images are too large to fit on the page.")
114
+
115
+ img_w, img_h = (
116
+ (self.image_width, self.image_height)
117
+ if not rotated
118
+ else (self.image_height, self.image_width)
119
+ )
120
+
121
+ total_width = cols * img_w + (cols - 1) * self.gap
122
+ total_height = rows * img_h + (rows - 1) * self.gap
123
+ start_x = (page_width - total_width) / 2
124
+ start_y = (page_height - total_height) / 2
125
+
126
+ images_on_page = 0
127
+ for image_path in self.image_paths:
128
+ if images_on_page > 0 and images_on_page % (cols * rows) == 0:
129
+ self.pdf.showPage()
130
+ images_on_page = 0
131
+
132
+ row = images_on_page // cols
133
+ col = images_on_page % cols
134
+
135
+ x = start_x + col * (img_w + self.gap)
136
+ y = start_y + row * (img_h + self.gap)
137
+
138
+ if not rotated:
139
+ self.pdf.drawImage(
140
+ str(image_path),
141
+ x,
142
+ y,
143
+ width=img_w,
144
+ height=img_h,
145
+ preserveAspectRatio=True,
146
+ )
147
+ else:
148
+ self.pdf.saveState()
149
+ center_x = x + img_w / 2
150
+ center_y = y + img_h / 2
151
+ self.pdf.translate(center_x, center_y)
152
+ self.pdf.rotate(90)
153
+ self.pdf.drawImage(
154
+ str(image_path),
155
+ -img_h / 2,
156
+ -img_w / 2,
157
+ width=img_h,
158
+ height=img_w,
159
+ preserveAspectRatio=True,
160
+ )
161
+ self.pdf.restoreState()
162
+ images_on_page += 1
163
+
164
+ self.pdf.save()
165
+ logging.info("Successfully exported PDF to %s", self.output_path)
166
+ except Exception as e:
167
+ logging.error("An error occurred during PDF export: %s", e)
168
+ raise