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/card_builder.py +627 -627
- decksmith/deck_builder.py +101 -101
- decksmith/export.py +168 -168
- decksmith/main.py +138 -138
- decksmith/templates/deck.csv +5 -5
- decksmith/templates/deck.json +31 -31
- decksmith/utils.py +69 -69
- decksmith/validate.py +132 -132
- decksmith-0.1.15.dist-info/METADATA +102 -0
- decksmith-0.1.15.dist-info/RECORD +13 -0
- decksmith-0.1.12.dist-info/METADATA +0 -54
- decksmith-0.1.12.dist-info/RECORD +0 -13
- {decksmith-0.1.12.dist-info → decksmith-0.1.15.dist-info}/WHEEL +0 -0
- {decksmith-0.1.12.dist-info → decksmith-0.1.15.dist-info}/entry_points.txt +0 -0
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
|