decksmith 0.1.14__py3-none-any.whl → 0.9.1__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 +140 -627
- decksmith/deck_builder.py +85 -101
- decksmith/export.py +29 -27
- decksmith/gui/__init__.py +0 -0
- decksmith/gui/app.py +341 -0
- decksmith/gui/static/css/style.css +656 -0
- decksmith/gui/static/js/main.js +583 -0
- decksmith/gui/templates/index.html +182 -0
- decksmith/image_ops.py +121 -0
- decksmith/logger.py +39 -0
- decksmith/macro.py +46 -0
- decksmith/main.py +146 -138
- 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 +19 -13
- decksmith/validate.py +1 -1
- {decksmith-0.1.14.dist-info → decksmith-0.9.1.dist-info}/METADATA +53 -19
- decksmith-0.9.1.dist-info/RECORD +26 -0
- {decksmith-0.1.14.dist-info → decksmith-0.9.1.dist-info}/WHEEL +1 -1
- decksmith/templates/deck.json +0 -31
- decksmith-0.1.14.dist-info/RECORD +0 -13
- {decksmith-0.1.14.dist-info → decksmith-0.9.1.dist-info}/entry_points.txt +0 -0
decksmith/main.py
CHANGED
|
@@ -1,138 +1,146 @@
|
|
|
1
|
-
"""
|
|
2
|
-
This module provides a command-line tool for building decks of cards.
|
|
3
|
-
"""
|
|
4
|
-
|
|
5
|
-
import shutil
|
|
6
|
-
|
|
7
|
-
from
|
|
8
|
-
import
|
|
9
|
-
|
|
10
|
-
import click
|
|
11
|
-
|
|
12
|
-
from decksmith.
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
@
|
|
21
|
-
def
|
|
22
|
-
"""
|
|
23
|
-
if
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
default=
|
|
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
|
-
|
|
1
|
+
"""
|
|
2
|
+
This module provides a command-line tool for building decks of cards.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import shutil
|
|
6
|
+
import traceback
|
|
7
|
+
from importlib import resources
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
import click
|
|
11
|
+
|
|
12
|
+
from decksmith.deck_builder import DeckBuilder
|
|
13
|
+
from decksmith.export import PdfExporter
|
|
14
|
+
from decksmith.gui.app import main as gui_main
|
|
15
|
+
from decksmith.logger import logger
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@click.group(invoke_without_command=True)
|
|
19
|
+
@click.option("--gui", is_flag=True, help="Launch the graphical user interface.")
|
|
20
|
+
@click.pass_context
|
|
21
|
+
def cli(ctx, gui):
|
|
22
|
+
"""A command-line tool for building decks of cards."""
|
|
23
|
+
if gui:
|
|
24
|
+
gui_main()
|
|
25
|
+
elif ctx.invoked_subcommand is None:
|
|
26
|
+
click.echo(ctx.get_help())
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@cli.command()
|
|
30
|
+
def init():
|
|
31
|
+
"""Initializes a new project by creating deck.yaml and deck.csv."""
|
|
32
|
+
if Path("deck.yaml").exists() or Path("deck.csv").exists():
|
|
33
|
+
click.echo("(!) Project already initialized.")
|
|
34
|
+
return
|
|
35
|
+
|
|
36
|
+
with resources.path("decksmith.templates", "deck.yaml") as template_path:
|
|
37
|
+
shutil.copy(template_path, "deck.yaml")
|
|
38
|
+
with resources.path("decksmith.templates", "deck.csv") as template_path:
|
|
39
|
+
shutil.copy(template_path, "deck.csv")
|
|
40
|
+
|
|
41
|
+
click.echo("(✔) Initialized new project from templates.")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@cli.command(context_settings={"show_default": True})
|
|
45
|
+
@click.option("--output", default="output", help="The output directory for the deck.")
|
|
46
|
+
@click.option(
|
|
47
|
+
"--spec", default="deck.yaml", help="The path to the deck specification file."
|
|
48
|
+
)
|
|
49
|
+
@click.option("--data", default="deck.csv", help="The path to the data file.")
|
|
50
|
+
@click.pass_context
|
|
51
|
+
def build(ctx, output, spec, data):
|
|
52
|
+
"""Builds the deck of cards."""
|
|
53
|
+
output_path = Path(output)
|
|
54
|
+
output_path.mkdir(exist_ok=True)
|
|
55
|
+
|
|
56
|
+
logger.info("(i) Building deck in %s...", output_path)
|
|
57
|
+
|
|
58
|
+
try:
|
|
59
|
+
spec_path = Path(spec)
|
|
60
|
+
if not spec_path.exists():
|
|
61
|
+
raise FileNotFoundError(f"Spec file not found: {spec_path}")
|
|
62
|
+
|
|
63
|
+
csv_path = Path(data)
|
|
64
|
+
if not csv_path.exists():
|
|
65
|
+
source = ctx.get_parameter_source("data")
|
|
66
|
+
if source.name == "DEFAULT":
|
|
67
|
+
logger.info(
|
|
68
|
+
"(i) Building a single card deck because '%s' was not found",
|
|
69
|
+
csv_path,
|
|
70
|
+
)
|
|
71
|
+
csv_path = None
|
|
72
|
+
else:
|
|
73
|
+
raise FileNotFoundError(f"Data file not found: {csv_path}")
|
|
74
|
+
|
|
75
|
+
builder = DeckBuilder(spec_path, csv_path)
|
|
76
|
+
builder.build_deck(output_path)
|
|
77
|
+
except FileNotFoundError as exc:
|
|
78
|
+
click.echo(f"(x) {exc}")
|
|
79
|
+
ctx.exit(1)
|
|
80
|
+
# pylint: disable=W0718
|
|
81
|
+
except Exception as exc:
|
|
82
|
+
logger.error("(x) Error building deck '%s' from spec '%s':", data, spec)
|
|
83
|
+
logger.error(" %s", exc)
|
|
84
|
+
logger.debug(traceback.format_exc())
|
|
85
|
+
ctx.exit(1)
|
|
86
|
+
|
|
87
|
+
logger.info("(✔) Deck built successfully.")
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@cli.command(context_settings={"show_default": True})
|
|
91
|
+
@click.argument("image_folder")
|
|
92
|
+
@click.option(
|
|
93
|
+
"--output", default="output.pdf", help="The path for the output PDF file."
|
|
94
|
+
)
|
|
95
|
+
@click.option(
|
|
96
|
+
"--page-size", default="A4", help="The page size for the PDF (e.g., A4, Letter)."
|
|
97
|
+
)
|
|
98
|
+
@click.option(
|
|
99
|
+
"--width", type=float, default=63.5, help="The width for each image in millimeters."
|
|
100
|
+
)
|
|
101
|
+
@click.option(
|
|
102
|
+
"--height",
|
|
103
|
+
type=float,
|
|
104
|
+
default=88.9,
|
|
105
|
+
help="The height for each image in millimeters.",
|
|
106
|
+
)
|
|
107
|
+
@click.option(
|
|
108
|
+
"--gap", type=float, default=0, help="The gap between images in millimeters."
|
|
109
|
+
)
|
|
110
|
+
@click.option(
|
|
111
|
+
"--margins",
|
|
112
|
+
type=float,
|
|
113
|
+
nargs=2,
|
|
114
|
+
default=[2, 2],
|
|
115
|
+
help="The horizontal and vertical page margins in millimeters.",
|
|
116
|
+
)
|
|
117
|
+
def export(image_folder, output, page_size, width, height, gap, margins):
|
|
118
|
+
"""Exports images from a folder to a PDF file."""
|
|
119
|
+
try:
|
|
120
|
+
image_folder_path = Path(image_folder)
|
|
121
|
+
if not image_folder_path.exists():
|
|
122
|
+
raise FileNotFoundError(f"Image folder not found: {image_folder_path}")
|
|
123
|
+
|
|
124
|
+
exporter = PdfExporter(
|
|
125
|
+
image_folder=image_folder_path,
|
|
126
|
+
output_path=Path(output),
|
|
127
|
+
page_size_str=page_size,
|
|
128
|
+
image_width=width,
|
|
129
|
+
image_height=height,
|
|
130
|
+
gap=gap,
|
|
131
|
+
margins=margins,
|
|
132
|
+
)
|
|
133
|
+
exporter.export()
|
|
134
|
+
logger.info("(✔) Successfully exported PDF to %s", output)
|
|
135
|
+
except FileNotFoundError as exc:
|
|
136
|
+
logger.error("(x) %s", exc)
|
|
137
|
+
# pylint: disable=W0718
|
|
138
|
+
except Exception as exc:
|
|
139
|
+
logger.error("(x) Error exporting images to '%s':", output)
|
|
140
|
+
logger.error(" %s", exc)
|
|
141
|
+
logger.debug(traceback.format_exc())
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
if __name__ == "__main__":
|
|
145
|
+
# pylint: disable=no-value-for-parameter
|
|
146
|
+
cli()
|
decksmith/project.py
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module contains the ProjectManager class for managing decksmith projects.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import shutil
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Dict, Optional
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ProjectManager:
|
|
11
|
+
"""
|
|
12
|
+
A class to manage decksmith projects.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
def __init__(self):
|
|
16
|
+
self.working_dir: Optional[Path] = None
|
|
17
|
+
|
|
18
|
+
def set_working_dir(self, path: Path):
|
|
19
|
+
"""Sets the working directory."""
|
|
20
|
+
self.working_dir = path
|
|
21
|
+
|
|
22
|
+
def get_working_dir(self) -> Optional[Path]:
|
|
23
|
+
"""Returns the current working directory."""
|
|
24
|
+
return self.working_dir
|
|
25
|
+
|
|
26
|
+
def close_project(self):
|
|
27
|
+
"""Closes the current project."""
|
|
28
|
+
self.working_dir = None
|
|
29
|
+
|
|
30
|
+
def create_project(self, path: Path):
|
|
31
|
+
"""
|
|
32
|
+
Creates a new project at the specified path.
|
|
33
|
+
Args:
|
|
34
|
+
path (Path): The path to create the project in.
|
|
35
|
+
"""
|
|
36
|
+
path.mkdir(parents=True, exist_ok=True)
|
|
37
|
+
|
|
38
|
+
# Copy templates
|
|
39
|
+
# Assuming templates are in decksmith/templates relative to this file
|
|
40
|
+
# But actually they are in decksmith/templates relative to the package root
|
|
41
|
+
# Let's use importlib.resources or relative path from __file__
|
|
42
|
+
# Since we are in decksmith/project.py, templates are in ../templates
|
|
43
|
+
template_dir = Path(__file__).parent / "templates"
|
|
44
|
+
|
|
45
|
+
if not (path / "deck.yaml").exists():
|
|
46
|
+
shutil.copy(template_dir / "deck.yaml", path / "deck.yaml")
|
|
47
|
+
|
|
48
|
+
if not (path / "deck.csv").exists():
|
|
49
|
+
shutil.copy(template_dir / "deck.csv", path / "deck.csv")
|
|
50
|
+
|
|
51
|
+
self.working_dir = path
|
|
52
|
+
|
|
53
|
+
def load_files(self) -> Dict[str, str]:
|
|
54
|
+
"""
|
|
55
|
+
Loads the deck.yaml and deck.csv files from the current project.
|
|
56
|
+
Returns:
|
|
57
|
+
Dict[str, str]: A dictionary containing the content of the files.
|
|
58
|
+
"""
|
|
59
|
+
if self.working_dir is None:
|
|
60
|
+
return {"yaml": "", "csv": ""}
|
|
61
|
+
|
|
62
|
+
yaml_path = self.working_dir / "deck.yaml"
|
|
63
|
+
csv_path = self.working_dir / "deck.csv"
|
|
64
|
+
|
|
65
|
+
template_dir = Path(__file__).parent / "templates"
|
|
66
|
+
yaml_template = template_dir / "deck.yaml"
|
|
67
|
+
csv_template = template_dir / "deck.csv"
|
|
68
|
+
|
|
69
|
+
data = {}
|
|
70
|
+
|
|
71
|
+
# Load YAML
|
|
72
|
+
if yaml_path.exists() and yaml_path.stat().st_size > 0:
|
|
73
|
+
with open(yaml_path, "r", encoding="utf-8") as yaml_file:
|
|
74
|
+
data["yaml"] = yaml_file.read()
|
|
75
|
+
elif yaml_template.exists():
|
|
76
|
+
with open(yaml_template, "r", encoding="utf-8") as yaml_template_file:
|
|
77
|
+
data["yaml"] = yaml_template_file.read()
|
|
78
|
+
else:
|
|
79
|
+
data["yaml"] = ""
|
|
80
|
+
|
|
81
|
+
# Load CSV
|
|
82
|
+
if csv_path.exists() and csv_path.stat().st_size > 0:
|
|
83
|
+
with open(csv_path, "r", encoding="utf-8") as csv_file:
|
|
84
|
+
data["csv"] = csv_file.read()
|
|
85
|
+
elif csv_template.exists():
|
|
86
|
+
with open(csv_template, "r", encoding="utf-8") as csv_template_file:
|
|
87
|
+
data["csv"] = csv_template_file.read()
|
|
88
|
+
else:
|
|
89
|
+
data["csv"] = ""
|
|
90
|
+
|
|
91
|
+
return data
|
|
92
|
+
|
|
93
|
+
def save_files(self, yaml_content: Optional[str], csv_content: Optional[str]):
|
|
94
|
+
"""
|
|
95
|
+
Saves the deck.yaml and deck.csv files to the current project.
|
|
96
|
+
Args:
|
|
97
|
+
yaml_content (Optional[str]): The content of the deck.yaml file.
|
|
98
|
+
csv_content (Optional[str]): The content of the deck.csv file.
|
|
99
|
+
"""
|
|
100
|
+
if self.working_dir is None:
|
|
101
|
+
raise ValueError("No project selected")
|
|
102
|
+
|
|
103
|
+
if yaml_content is not None:
|
|
104
|
+
with open(
|
|
105
|
+
self.working_dir / "deck.yaml", "w", encoding="utf-8"
|
|
106
|
+
) as yaml_file:
|
|
107
|
+
yaml_file.write(yaml_content.replace("\r\n", "\n"))
|
|
108
|
+
|
|
109
|
+
if csv_content is not None:
|
|
110
|
+
with open(self.working_dir / "deck.csv", "w", encoding="utf-8") as csv_file:
|
|
111
|
+
csv_file.write(csv_content.replace("\r\n", "\n"))
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module contains the ImageRenderer class for drawing images on cards.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import operator
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any, Dict, Optional
|
|
8
|
+
|
|
9
|
+
from PIL import Image
|
|
10
|
+
|
|
11
|
+
from decksmith.image_ops import ImageOps
|
|
12
|
+
from decksmith.logger import logger
|
|
13
|
+
from decksmith.utils import apply_anchor
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ImageRenderer:
|
|
17
|
+
"""
|
|
18
|
+
A class to render image elements on a card.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(self, base_path: Optional[Path] = None):
|
|
22
|
+
self.base_path = base_path
|
|
23
|
+
|
|
24
|
+
def render(
|
|
25
|
+
self,
|
|
26
|
+
card: Image.Image,
|
|
27
|
+
element: Dict[str, Any],
|
|
28
|
+
calculate_pos_func,
|
|
29
|
+
store_pos_func,
|
|
30
|
+
):
|
|
31
|
+
"""
|
|
32
|
+
Draws an image on the card.
|
|
33
|
+
Args:
|
|
34
|
+
card (Image.Image): The card image object.
|
|
35
|
+
element (Dict[str, Any]): The image element specification.
|
|
36
|
+
calculate_pos_func (callable): Function to calculate absolute position.
|
|
37
|
+
store_pos_func (callable): Function to store element position.
|
|
38
|
+
"""
|
|
39
|
+
assert element.pop("type") == "image", "Element type must be 'image'"
|
|
40
|
+
|
|
41
|
+
path_str = element["path"]
|
|
42
|
+
path = Path(path_str)
|
|
43
|
+
|
|
44
|
+
if not path.is_absolute() and self.base_path:
|
|
45
|
+
potential_path = self.base_path / path
|
|
46
|
+
if potential_path.exists():
|
|
47
|
+
path = potential_path
|
|
48
|
+
|
|
49
|
+
try:
|
|
50
|
+
img = Image.open(path)
|
|
51
|
+
except FileNotFoundError:
|
|
52
|
+
logger.error("Image not found: %s", path)
|
|
53
|
+
return
|
|
54
|
+
|
|
55
|
+
img = ImageOps.apply_filters(img, element.get("filters", {}))
|
|
56
|
+
|
|
57
|
+
position = calculate_pos_func(element)
|
|
58
|
+
if "anchor" in element:
|
|
59
|
+
anchor_point = apply_anchor((img.width, img.height), element.pop("anchor"))
|
|
60
|
+
position = tuple(map(operator.sub, position, anchor_point))
|
|
61
|
+
|
|
62
|
+
if img.mode == "RGBA":
|
|
63
|
+
card.paste(img, position, mask=img)
|
|
64
|
+
else:
|
|
65
|
+
card.paste(img, position)
|
|
66
|
+
|
|
67
|
+
if "id" in element:
|
|
68
|
+
store_pos_func(
|
|
69
|
+
element["id"],
|
|
70
|
+
(
|
|
71
|
+
position[0],
|
|
72
|
+
position[1],
|
|
73
|
+
position[0] + img.width,
|
|
74
|
+
position[1] + img.height,
|
|
75
|
+
),
|
|
76
|
+
)
|