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/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
- from importlib import resources
7
- from pathlib import Path
8
- import traceback
9
-
10
- import click
11
- from decksmith.deck_builder import DeckBuilder
12
- from decksmith.export import PdfExporter
13
-
14
-
15
- @click.group()
16
- def cli():
17
- """A command-line tool for building decks of cards."""
18
-
19
-
20
- @cli.command()
21
- def init():
22
- """Initializes a new project by creating deck.json and deck.csv."""
23
- if Path("deck.json").exists() or Path("deck.csv").exists():
24
- click.echo("(!) Project already initialized.")
25
- return
26
-
27
- with resources.path("decksmith.templates", "deck.json") as template_path:
28
- shutil.copy(template_path, "deck.json")
29
- with resources.path("decksmith.templates", "deck.csv") as template_path:
30
- shutil.copy(template_path, "deck.csv")
31
-
32
- click.echo("() Initialized new project from templates.")
33
-
34
-
35
- @cli.command(context_settings={"show_default": True})
36
- @click.option("--output", default="output", help="The output directory for the deck.")
37
- @click.option(
38
- "--spec", default="deck.json", help="The path to the deck specification file."
39
- )
40
- @click.option("--data", default="deck.csv", help="The path to the data file.")
41
- @click.pass_context
42
- def build(ctx, output, spec, data):
43
- """Builds the deck of cards."""
44
- output_path = Path(output)
45
- output_path.mkdir(exist_ok=True)
46
-
47
- click.echo(f"(i) Building deck in {output_path}...")
48
-
49
- try:
50
- spec_path = Path(spec)
51
- if not spec_path.exists():
52
- raise FileNotFoundError(f"Spec file not found: {spec_path}")
53
-
54
- csv_path = Path(data)
55
- if not csv_path.exists():
56
- source = ctx.get_parameter_source("data")
57
- if source.name == "DEFAULT":
58
- click.echo(
59
- f"(i) Building a single card deck because '{csv_path}' was not found"
60
- )
61
- csv_path = None
62
- else:
63
- raise FileNotFoundError(f"Data file not found: {csv_path}")
64
-
65
- builder = DeckBuilder(spec_path, csv_path)
66
- builder.build_deck(output_path)
67
- except FileNotFoundError as exc:
68
- click.echo(f"(x) {exc}")
69
- ctx.exit(1)
70
- # pylint: disable=W0718
71
- except Exception as exc:
72
- with open("log.txt", "a", encoding="utf-8") as log:
73
- log.write(traceback.format_exc())
74
- # print(f"{traceback.format_exc()}", end="\n")
75
- print(f"(x) Error building deck '{data}' from spec '{spec}':")
76
- print(" " * 4 + f"{exc}")
77
- ctx.exit(1)
78
-
79
- click.echo("(✔) Deck built successfully.")
80
-
81
-
82
- @cli.command(context_settings={"show_default": True})
83
- @click.argument("image_folder")
84
- @click.option(
85
- "--output", default="output.pdf", help="The path for the output PDF file."
86
- )
87
- @click.option(
88
- "--page-size", default="A4", help="The page size for the PDF (e.g., A4, Letter)."
89
- )
90
- @click.option(
91
- "--width", type=float, default=63.5, help="The width for each image in millimeters."
92
- )
93
- @click.option(
94
- "--height",
95
- type=float,
96
- default=88.9,
97
- help="The height for each image in millimeters.",
98
- )
99
- @click.option(
100
- "--gap", type=float, default=0, help="The gap between images in millimeters."
101
- )
102
- @click.option(
103
- "--margins",
104
- type=float,
105
- nargs=2,
106
- default=[2, 2],
107
- help="The horizontal and vertical page margins in millimeters.",
108
- )
109
- def export(image_folder, output, page_size, width, height, gap, margins):
110
- """Exports images from a folder to a PDF file."""
111
- try:
112
- image_folder_path = Path(image_folder)
113
- if not image_folder_path.exists():
114
- raise FileNotFoundError(f"Image folder not found: {image_folder_path}")
115
-
116
- exporter = PdfExporter(
117
- image_folder=image_folder_path,
118
- output_path=Path(output),
119
- page_size_str=page_size,
120
- image_width=width,
121
- image_height=height,
122
- gap=gap,
123
- margins=margins,
124
- )
125
- exporter.export()
126
- click.echo(f"(✔) Successfully exported PDF to {output}")
127
- except FileNotFoundError as exc:
128
- click.echo(f"(x) {exc}")
129
- # pylint: disable=W0718
130
- except Exception as exc:
131
- with open("log.txt", "a", encoding="utf-8") as log:
132
- log.write(traceback.format_exc())
133
- print(f"(x) Error exporting images to '{output}':")
134
- print(" " * 4 + f"{exc}")
135
-
136
-
137
- if __name__ == "__main__":
138
- cli()
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,3 @@
1
+ """
2
+ This package contains renderer classes for different card elements.
3
+ """
@@ -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
+ )