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.
Files changed (29) hide show
  1. {decksmith-0.1.14 → decksmith-0.9.1}/PKG-INFO +53 -19
  2. decksmith-0.9.1/decksmith/card_builder.py +140 -0
  3. decksmith-0.9.1/decksmith/deck_builder.py +85 -0
  4. {decksmith-0.1.14 → decksmith-0.9.1}/decksmith/export.py +29 -27
  5. decksmith-0.9.1/decksmith/gui/__init__.py +0 -0
  6. decksmith-0.9.1/decksmith/gui/app.py +341 -0
  7. decksmith-0.9.1/decksmith/gui/static/css/style.css +656 -0
  8. decksmith-0.9.1/decksmith/gui/static/js/main.js +583 -0
  9. decksmith-0.9.1/decksmith/gui/templates/index.html +182 -0
  10. decksmith-0.9.1/decksmith/image_ops.py +121 -0
  11. decksmith-0.9.1/decksmith/logger.py +39 -0
  12. decksmith-0.9.1/decksmith/macro.py +46 -0
  13. {decksmith-0.1.14 → decksmith-0.9.1}/decksmith/main.py +146 -138
  14. decksmith-0.9.1/decksmith/project.py +111 -0
  15. decksmith-0.9.1/decksmith/renderers/__init__.py +3 -0
  16. decksmith-0.9.1/decksmith/renderers/image.py +76 -0
  17. decksmith-0.9.1/decksmith/renderers/shapes.py +237 -0
  18. decksmith-0.9.1/decksmith/renderers/text.py +127 -0
  19. decksmith-0.9.1/decksmith/templates/deck.csv +4 -0
  20. decksmith-0.9.1/decksmith/templates/deck.yaml +46 -0
  21. {decksmith-0.1.14 → decksmith-0.9.1}/decksmith/utils.py +19 -13
  22. {decksmith-0.1.14 → decksmith-0.9.1}/decksmith/validate.py +1 -1
  23. {decksmith-0.1.14 → decksmith-0.9.1}/docs/README.md +79 -49
  24. {decksmith-0.1.14 → decksmith-0.9.1}/pyproject.toml +19 -26
  25. decksmith-0.1.14/decksmith/card_builder.py +0 -627
  26. decksmith-0.1.14/decksmith/deck_builder.py +0 -101
  27. decksmith-0.1.14/decksmith/templates/deck.csv +0 -5
  28. decksmith-0.1.14/decksmith/templates/deck.json +0 -31
  29. {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.14
4
- Summary: A command-line application to dynamically generate decks of cards from a JSON specification and a CSV data file, inspired by nandeck.
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: poetry ; extra == "dev"
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
- Project-URL: Homepage, https://github.com/Julynx/decksmith
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 command-line application to dynamically generate decks of cards from a JSON specification and a CSV data file, inspired by nandeck.*
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 width="600" src="https://raw.githubusercontent.com/Julynx/decksmith/refs/heads/main/docs/assets/decksmith.png">
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 quickly.
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#basic-example-with-deckcsv)
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
- - First, install DeckSmith by running `pip install decksmith`.
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
- - Then, run `decksmith init` to start from sample `deck.json` and `deck.csv` files.
91
+ ### Building the deck
62
92
 
63
- - The `deck.json` file defines the layout for the cards in the deck, while the `deck.csv` file holds the data for each card.
93
+ - When you are ready to generate the deck images, run:
64
94
 
65
- - You can find a complete list of all the available elements you can use in the [documentation](https://github.com/Julynx/decksmith/blob/main/docs/DOCS.md).
95
+ ```text
96
+ decksmith build
97
+ ```
66
98
 
67
- - Any column from the CSV can be referenced anywhere in the JSON as `%column_name%`.
99
+ - After building a deck, you can export it to PDF by running:
68
100
 
69
- - Finally, run `decksmith build` when you are ready to generate the deck images, and export them to PDF using the `decksmith export` command.
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
- p
50
- for p in self.image_folder.iterdir()
51
- if p.suffix.lower() in image_extensions
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
- img_w, img_h = (
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) / (img_w + 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) / (img_h + 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
- img_w, img_h = (
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 * 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
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
- x = start_x + col * (img_w + self.gap)
136
- y = start_y + row * (img_h + self.gap)
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
- x,
142
- y,
143
- width=img_w,
144
- height=img_h,
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
- center_x = x + img_w / 2
150
- center_y = y + img_h / 2
151
- self.pdf.translate(center_x, center_y)
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
- -img_h / 2,
156
- -img_w / 2,
157
- width=img_h,
158
- height=img_w,
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
- logging.info("Successfully exported PDF to %s", self.output_path)
167
+ logger.info("Successfully exported PDF to %s", self.output_path)
166
168
  except Exception as e:
167
- logging.error("An error occurred during PDF export: %s", e)
169
+ logger.error("An error occurred during PDF export: %s", e)
168
170
  raise
File without changes