decksmith 0.1.14__tar.gz → 0.9.1__tar.gz
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-0.1.14 → decksmith-0.9.1}/PKG-INFO +53 -19
- decksmith-0.9.1/decksmith/card_builder.py +140 -0
- decksmith-0.9.1/decksmith/deck_builder.py +85 -0
- {decksmith-0.1.14 → decksmith-0.9.1}/decksmith/export.py +29 -27
- decksmith-0.9.1/decksmith/gui/__init__.py +0 -0
- decksmith-0.9.1/decksmith/gui/app.py +341 -0
- decksmith-0.9.1/decksmith/gui/static/css/style.css +656 -0
- decksmith-0.9.1/decksmith/gui/static/js/main.js +583 -0
- decksmith-0.9.1/decksmith/gui/templates/index.html +182 -0
- decksmith-0.9.1/decksmith/image_ops.py +121 -0
- decksmith-0.9.1/decksmith/logger.py +39 -0
- decksmith-0.9.1/decksmith/macro.py +46 -0
- {decksmith-0.1.14 → decksmith-0.9.1}/decksmith/main.py +146 -138
- decksmith-0.9.1/decksmith/project.py +111 -0
- decksmith-0.9.1/decksmith/renderers/__init__.py +3 -0
- decksmith-0.9.1/decksmith/renderers/image.py +76 -0
- decksmith-0.9.1/decksmith/renderers/shapes.py +237 -0
- decksmith-0.9.1/decksmith/renderers/text.py +127 -0
- decksmith-0.9.1/decksmith/templates/deck.csv +4 -0
- decksmith-0.9.1/decksmith/templates/deck.yaml +46 -0
- {decksmith-0.1.14 → decksmith-0.9.1}/decksmith/utils.py +19 -13
- {decksmith-0.1.14 → decksmith-0.9.1}/decksmith/validate.py +1 -1
- {decksmith-0.1.14 → decksmith-0.9.1}/docs/README.md +79 -49
- {decksmith-0.1.14 → decksmith-0.9.1}/pyproject.toml +19 -26
- decksmith-0.1.14/decksmith/card_builder.py +0 -627
- decksmith-0.1.14/decksmith/deck_builder.py +0 -101
- decksmith-0.1.14/decksmith/templates/deck.csv +0 -5
- decksmith-0.1.14/decksmith/templates/deck.json +0 -31
- {decksmith-0.1.14 → decksmith-0.9.1}/decksmith/__init__.py +0 -0
|
@@ -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,140 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module contains the CardBuilder class,
|
|
3
|
+
which is used to create card images based on a YAML specification.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import operator
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any, Dict, Optional, Tuple
|
|
9
|
+
|
|
10
|
+
from PIL import Image, ImageDraw
|
|
11
|
+
|
|
12
|
+
from decksmith.logger import logger
|
|
13
|
+
from decksmith.renderers.image import ImageRenderer
|
|
14
|
+
from decksmith.renderers.shapes import ShapeRenderer
|
|
15
|
+
from decksmith.renderers.text import TextRenderer
|
|
16
|
+
from decksmith.utils import apply_anchor
|
|
17
|
+
from decksmith.validate import transform_card, validate_card
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class CardBuilder:
|
|
21
|
+
"""
|
|
22
|
+
A class to build a card image based on a YAML specification.
|
|
23
|
+
Attributes:
|
|
24
|
+
spec (Dict[str, Any]): The YAML specification for the card.
|
|
25
|
+
card (Image.Image): The PIL Image object representing the card.
|
|
26
|
+
draw (ImageDraw.ImageDraw): The PIL ImageDraw object for drawing on the card.
|
|
27
|
+
element_positions (Dict[str, Tuple[int, int, int, int]]):
|
|
28
|
+
A dictionary mapping element IDs to their bounding boxes.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(self, spec: Dict[str, Any], base_path: Optional[Path] = None):
|
|
32
|
+
"""
|
|
33
|
+
Initializes the CardBuilder with a YAML specification.
|
|
34
|
+
Args:
|
|
35
|
+
spec (Dict[str, Any]): The YAML specification for the card.
|
|
36
|
+
base_path (Optional[Path]): The base path for resolving relative file paths.
|
|
37
|
+
"""
|
|
38
|
+
self.spec = spec
|
|
39
|
+
self.base_path = base_path
|
|
40
|
+
width = self.spec.get("width", 250)
|
|
41
|
+
height = self.spec.get("height", 350)
|
|
42
|
+
background_color = tuple(self.spec.get("background_color", (255, 255, 255, 0)))
|
|
43
|
+
self.card: Image.Image = Image.new("RGBA", (width, height), background_color)
|
|
44
|
+
self.draw: ImageDraw.ImageDraw = ImageDraw.Draw(self.card, "RGBA")
|
|
45
|
+
self.element_positions: Dict[str, Tuple[int, int, int, int]] = {}
|
|
46
|
+
if "id" in spec:
|
|
47
|
+
self.element_positions[self.spec["id"]] = (0, 0, width, height)
|
|
48
|
+
|
|
49
|
+
self.text_renderer = TextRenderer(base_path)
|
|
50
|
+
self.image_renderer = ImageRenderer(base_path)
|
|
51
|
+
self.shape_renderer = ShapeRenderer()
|
|
52
|
+
|
|
53
|
+
def _calculate_absolute_position(self, element: Dict[str, Any]) -> Tuple[int, int]:
|
|
54
|
+
"""
|
|
55
|
+
Calculates the absolute position of an element,
|
|
56
|
+
resolving relative positioning.
|
|
57
|
+
Args:
|
|
58
|
+
element (dict): The element dictionary.
|
|
59
|
+
Returns:
|
|
60
|
+
tuple: The absolute (x, y) position of the element.
|
|
61
|
+
"""
|
|
62
|
+
# If the element has no 'relative_to', return its position directly
|
|
63
|
+
if "relative_to" not in element:
|
|
64
|
+
return tuple(element.get("position", [0, 0]))
|
|
65
|
+
|
|
66
|
+
# If the element has 'relative_to', resolve based on the reference element and anchor
|
|
67
|
+
relative_identifier, anchor = element["relative_to"]
|
|
68
|
+
if relative_identifier not in self.element_positions:
|
|
69
|
+
raise ValueError(
|
|
70
|
+
f"Element with id '{relative_identifier}' not found for relative positioning."
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
parent_bbox = self.element_positions[relative_identifier]
|
|
74
|
+
anchor_point = apply_anchor(parent_bbox, anchor)
|
|
75
|
+
|
|
76
|
+
offset = tuple(element.get("position", [0, 0]))
|
|
77
|
+
return tuple(map(operator.add, anchor_point, offset))
|
|
78
|
+
|
|
79
|
+
def _store_element_position(
|
|
80
|
+
self, element_identifier: str, bbox: Tuple[int, int, int, int]
|
|
81
|
+
):
|
|
82
|
+
"""Stores the bounding box of an element."""
|
|
83
|
+
self.element_positions[element_identifier] = bbox
|
|
84
|
+
|
|
85
|
+
def render(self) -> Image.Image:
|
|
86
|
+
"""
|
|
87
|
+
Renders the card image by drawing all elements specified in the YAML.
|
|
88
|
+
Returns:
|
|
89
|
+
Image.Image: The rendered card image.
|
|
90
|
+
"""
|
|
91
|
+
self.spec = transform_card(self.spec)
|
|
92
|
+
validate_card(self.spec)
|
|
93
|
+
|
|
94
|
+
for element in self.spec.get("elements", []):
|
|
95
|
+
element_type = element.get("type")
|
|
96
|
+
try:
|
|
97
|
+
if element_type == "text":
|
|
98
|
+
self.text_renderer.render(
|
|
99
|
+
self.draw,
|
|
100
|
+
element,
|
|
101
|
+
self._calculate_absolute_position,
|
|
102
|
+
self._store_element_position,
|
|
103
|
+
)
|
|
104
|
+
elif element_type == "image":
|
|
105
|
+
self.image_renderer.render(
|
|
106
|
+
self.card,
|
|
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,
|
|
123
|
+
)
|
|
124
|
+
# Re-create draw object because shape renderer might have composited a new image
|
|
125
|
+
self.draw = ImageDraw.Draw(self.card, "RGBA")
|
|
126
|
+
except Exception as e:
|
|
127
|
+
logger.error("Error drawing element %s: %s", element_type, e)
|
|
128
|
+
# Continue drawing other elements
|
|
129
|
+
|
|
130
|
+
return self.card
|
|
131
|
+
|
|
132
|
+
def build(self, output_path: Path):
|
|
133
|
+
"""
|
|
134
|
+
Builds the card image and saves it to the specified path.
|
|
135
|
+
Args:
|
|
136
|
+
output_path (Path): The path where the card image will be saved.
|
|
137
|
+
"""
|
|
138
|
+
card = self.render()
|
|
139
|
+
card.save(output_path)
|
|
140
|
+
logger.info("(✔) Card saved to %s", output_path)
|
|
@@ -0,0 +1,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
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any, Dict, List, Optional
|
|
10
|
+
|
|
11
|
+
import pandas as pd
|
|
12
|
+
from pandas import Series
|
|
13
|
+
from ruamel.yaml import YAML
|
|
14
|
+
|
|
15
|
+
from decksmith.card_builder import CardBuilder
|
|
16
|
+
from decksmith.logger import logger
|
|
17
|
+
from decksmith.macro import MacroResolver
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class DeckBuilder:
|
|
21
|
+
"""
|
|
22
|
+
A class to build a deck of cards based on a YAML specification and a CSV file.
|
|
23
|
+
Attributes:
|
|
24
|
+
spec_path (Path): Path to the YAML specification file.
|
|
25
|
+
csv_path (Union[Path, None]): Path to the CSV file containing card data.
|
|
26
|
+
cards (list): List of CardBuilder instances for each card in the deck.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(self, spec_path: Path, csv_path: Optional[Path] = None):
|
|
30
|
+
"""
|
|
31
|
+
Initializes the DeckBuilder with a YAML specification file and a CSV file.
|
|
32
|
+
Args:
|
|
33
|
+
spec_path (Path): Path to the YAML specification file.
|
|
34
|
+
csv_path (Union[Path, None]): Path to the CSV file containing card data.
|
|
35
|
+
"""
|
|
36
|
+
self.spec_path = spec_path
|
|
37
|
+
self.csv_path = csv_path
|
|
38
|
+
self.cards: List[CardBuilder] = []
|
|
39
|
+
self._spec_cache: Optional[Dict[str, Any]] = None
|
|
40
|
+
|
|
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
|
|
49
|
+
|
|
50
|
+
def build_deck(self, output_path: Path):
|
|
51
|
+
"""
|
|
52
|
+
Builds the deck of cards by reading the CSV file and creating CardBuilder instances.
|
|
53
|
+
"""
|
|
54
|
+
base_path = self.spec_path.parent if self.spec_path else None
|
|
55
|
+
|
|
56
|
+
if not self.csv_path or not self.csv_path.exists():
|
|
57
|
+
logger.info("No CSV file found. Building single card from spec.")
|
|
58
|
+
card_builder = CardBuilder(self.spec, base_path=base_path)
|
|
59
|
+
card_builder.build(output_path / "card_1.png")
|
|
60
|
+
return
|
|
61
|
+
|
|
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
|
|
67
|
+
|
|
68
|
+
def build_card(row_tuple: tuple[int, Series]):
|
|
69
|
+
"""
|
|
70
|
+
Builds a single card from a row of the CSV file.
|
|
71
|
+
Args:
|
|
72
|
+
row_tuple (tuple[int, Series]): A tuple containing the row index and the row data.
|
|
73
|
+
"""
|
|
74
|
+
idx, row = row_tuple
|
|
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)
|
|
83
|
+
|
|
84
|
+
with concurrent.futures.ThreadPoolExecutor() as executor:
|
|
85
|
+
list(executor.map(build_card, dataframe.iterrows()))
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
This module provides the functionality to export images from a folder to a PDF file.
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
|
-
import logging
|
|
6
5
|
from pathlib import Path
|
|
7
6
|
from typing import List, Tuple
|
|
8
7
|
|
|
@@ -10,6 +9,8 @@ from reportlab.lib.pagesizes import A4
|
|
|
10
9
|
from reportlab.lib.units import mm
|
|
11
10
|
from reportlab.pdfgen import canvas
|
|
12
11
|
|
|
12
|
+
from decksmith.logger import logger
|
|
13
|
+
|
|
13
14
|
|
|
14
15
|
class PdfExporter:
|
|
15
16
|
"""
|
|
@@ -46,9 +47,9 @@ class PdfExporter:
|
|
|
46
47
|
image_extensions = {".png", ".jpg", ".jpeg", ".webp"}
|
|
47
48
|
return sorted(
|
|
48
49
|
[
|
|
49
|
-
|
|
50
|
-
for
|
|
51
|
-
if
|
|
50
|
+
image_path
|
|
51
|
+
for image_path in self.image_folder.iterdir()
|
|
52
|
+
if image_path.suffix.lower() in image_extensions
|
|
52
53
|
]
|
|
53
54
|
)
|
|
54
55
|
|
|
@@ -82,17 +83,18 @@ class PdfExporter:
|
|
|
82
83
|
best_layout = (0, 0, False)
|
|
83
84
|
|
|
84
85
|
for rotated in [False, True]:
|
|
85
|
-
|
|
86
|
+
image_width, image_height = (
|
|
86
87
|
(self.image_width, self.image_height)
|
|
87
88
|
if not rotated
|
|
88
89
|
else (self.image_height, self.image_width)
|
|
89
90
|
)
|
|
90
91
|
|
|
91
92
|
cols = int(
|
|
92
|
-
(page_width - 2 * self.margins[0] + self.gap) / (
|
|
93
|
+
(page_width - 2 * self.margins[0] + self.gap) / (image_width + self.gap)
|
|
93
94
|
)
|
|
94
95
|
rows = int(
|
|
95
|
-
(page_height - 2 * self.margins[1] + self.gap)
|
|
96
|
+
(page_height - 2 * self.margins[1] + self.gap)
|
|
97
|
+
/ (image_height + self.gap)
|
|
96
98
|
)
|
|
97
99
|
|
|
98
100
|
if cols * rows > best_fit:
|
|
@@ -112,16 +114,16 @@ class PdfExporter:
|
|
|
112
114
|
if cols == 0 or rows == 0:
|
|
113
115
|
raise ValueError("The images are too large to fit on the page.")
|
|
114
116
|
|
|
115
|
-
|
|
117
|
+
image_width, image_height = (
|
|
116
118
|
(self.image_width, self.image_height)
|
|
117
119
|
if not rotated
|
|
118
120
|
else (self.image_height, self.image_width)
|
|
119
121
|
)
|
|
120
122
|
|
|
121
|
-
total_width = cols *
|
|
122
|
-
total_height = rows *
|
|
123
|
-
|
|
124
|
-
|
|
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
|
|
125
127
|
|
|
126
128
|
images_on_page = 0
|
|
127
129
|
for image_path in self.image_paths:
|
|
@@ -132,37 +134,37 @@ class PdfExporter:
|
|
|
132
134
|
row = images_on_page // cols
|
|
133
135
|
col = images_on_page % cols
|
|
134
136
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
+
position_horizontal = start_horizontal + col * (image_width + self.gap)
|
|
138
|
+
position_vertical = start_vertical + row * (image_height + self.gap)
|
|
137
139
|
|
|
138
140
|
if not rotated:
|
|
139
141
|
self.pdf.drawImage(
|
|
140
142
|
str(image_path),
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
width=
|
|
144
|
-
height=
|
|
143
|
+
position_horizontal,
|
|
144
|
+
position_vertical,
|
|
145
|
+
width=image_width,
|
|
146
|
+
height=image_height,
|
|
145
147
|
preserveAspectRatio=True,
|
|
146
148
|
)
|
|
147
149
|
else:
|
|
148
150
|
self.pdf.saveState()
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
self.pdf.translate(
|
|
151
|
+
center_horizontal = position_horizontal + image_width / 2
|
|
152
|
+
center_vertical = position_vertical + image_height / 2
|
|
153
|
+
self.pdf.translate(center_horizontal, center_vertical)
|
|
152
154
|
self.pdf.rotate(90)
|
|
153
155
|
self.pdf.drawImage(
|
|
154
156
|
str(image_path),
|
|
155
|
-
-
|
|
156
|
-
-
|
|
157
|
-
width=
|
|
158
|
-
height=
|
|
157
|
+
-image_height / 2,
|
|
158
|
+
-image_width / 2,
|
|
159
|
+
width=image_height,
|
|
160
|
+
height=image_width,
|
|
159
161
|
preserveAspectRatio=True,
|
|
160
162
|
)
|
|
161
163
|
self.pdf.restoreState()
|
|
162
164
|
images_on_page += 1
|
|
163
165
|
|
|
164
166
|
self.pdf.save()
|
|
165
|
-
|
|
167
|
+
logger.info("Successfully exported PDF to %s", self.output_path)
|
|
166
168
|
except Exception as e:
|
|
167
|
-
|
|
169
|
+
logger.error("An error occurred during PDF export: %s", e)
|
|
168
170
|
raise
|
|
File without changes
|