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/main.py CHANGED
@@ -1,138 +1,138 @@
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
+ 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,5 +1,5 @@
1
- title;description
2
- Welcome to DeckSmith!;To get started, edit "deck.csv" to add new cards, or "deck.json" to change their structure.
3
- Building the deck;Run "decksmith build" when you are ready to save the cards as images.
4
- Exporting to PDF;You can create a printable PDF with "decksmith export --width 63.5 --height 88.9 output" after building.
5
- Check out the guide;Visit "github.com/julynx/decksmith" to learn all the things you can do using DeckSmith. Enjoy!
1
+ title;description
2
+ Welcome to DeckSmith!;To get started, edit "deck.csv" to add new cards, or "deck.json" to change their structure.
3
+ Building the deck;Run "decksmith build" when you are ready to save the cards as images.
4
+ Exporting to PDF;You can create a printable PDF with "decksmith export --width 63.5 --height 88.9 output" after building.
5
+ Check out the guide;Visit "github.com/julynx/decksmith" to learn all the things you can do using DeckSmith. Enjoy!
@@ -1,31 +1,31 @@
1
- {
2
- "width": 250,
3
- "height": 350,
4
- "background_color": [200, 200, 200],
5
- "elements": [
6
- {
7
- "id": "title",
8
- "type": "text",
9
- "position": [125, 25],
10
- "anchor": "top-center",
11
- "text": "%title%",
12
- "color": [0, 0, 0],
13
- "font_path": "arial.ttf",
14
- "font_size": 20,
15
- "align": "center"
16
- },
17
- {
18
- "id": "description",
19
- "type": "text",
20
- "position": [0, 25],
21
- "relative_to": ["title", "bottom-center"],
22
- "anchor": "top-center",
23
- "text": "%description%",
24
- "color": [0, 0, 0],
25
- "font_path": "arial.ttf",
26
- "font_size": 16,
27
- "width": 200,
28
- "align": "center"
29
- }
30
- ]
31
- }
1
+ {
2
+ "width": 250,
3
+ "height": 350,
4
+ "background_color": [200, 200, 200],
5
+ "elements": [
6
+ {
7
+ "id": "title",
8
+ "type": "text",
9
+ "position": [125, 25],
10
+ "anchor": "top-center",
11
+ "text": "%title%",
12
+ "color": [0, 0, 0],
13
+ "font_path": "arial.ttf",
14
+ "font_size": 20,
15
+ "align": "center"
16
+ },
17
+ {
18
+ "id": "description",
19
+ "type": "text",
20
+ "position": [0, 25],
21
+ "relative_to": ["title", "bottom-center"],
22
+ "anchor": "top-center",
23
+ "text": "%description%",
24
+ "color": [0, 0, 0],
25
+ "font_path": "arial.ttf",
26
+ "font_size": 16,
27
+ "width": 200,
28
+ "align": "center"
29
+ }
30
+ ]
31
+ }
decksmith/utils.py CHANGED
@@ -1,69 +1,69 @@
1
- """
2
- This module provides utility functions for text wrapping and positioning.
3
- """
4
-
5
- from typing import Tuple
6
-
7
- from PIL import ImageFont
8
-
9
-
10
- def get_wrapped_text(text: str, font: ImageFont.ImageFont, line_length: int) -> str:
11
- """
12
- Wraps text to fit within a specified line length using the given font,
13
- preserving existing newlines.
14
- Args:
15
- text (str): The text to wrap.
16
- font (ImageFont.ImageFont): The font to use for measuring text length.
17
- line_length (int): The maximum length of each line in pixels.
18
-
19
- Returns:
20
- str: The wrapped text with newlines inserted where necessary.
21
- """
22
- wrapped_lines = []
23
- for line in text.split("\n"):
24
- lines = [""]
25
- for word in line.split():
26
- line_to_check = f"{lines[-1]} {word}".strip()
27
- if font.getlength(line_to_check) <= line_length:
28
- lines[-1] = line_to_check
29
- else:
30
- lines.append(word)
31
- wrapped_lines.extend(lines)
32
- return "\n".join(wrapped_lines)
33
-
34
-
35
- def apply_anchor(size: Tuple[int, ...], anchor: str) -> Tuple[int, int]:
36
- """
37
- Applies an anchor to a size tuple to determine the position of an element.
38
- Args:
39
- size (Tuple[int, ...]): A tuple representing the size (width, height)
40
- or a bounding box (x1, y1, x2, y2).
41
- anchor (str): The anchor position, e.g., "center", "top-left".
42
- Returns:
43
- Tuple[int, int]: A tuple representing the position (x, y) based on the anchor.
44
- """
45
- if len(size) == 2:
46
- w, h = size
47
- x, y = 0, 0
48
- elif len(size) == 4:
49
- x, y, x2, y2 = size
50
- w, h = x2 - x, y2 - y
51
- else:
52
- raise ValueError("Size must be a tuple of 2 or 4 integers.")
53
-
54
- anchor_points = {
55
- "top-left": (x, y),
56
- "top-center": (x + w // 2, y),
57
- "top-right": (x + w, y),
58
- "middle-left": (x, y + h // 2),
59
- "center": (x + w // 2, y + h // 2),
60
- "middle-right": (x + w, y + h // 2),
61
- "bottom-left": (x, y + h),
62
- "bottom-center": (x + w // 2, y + h),
63
- "bottom-right": (x + w, y + h),
64
- }
65
-
66
- if anchor not in anchor_points:
67
- raise ValueError(f"Unknown anchor: {anchor}")
68
-
69
- return anchor_points[anchor]
1
+ """
2
+ This module provides utility functions for text wrapping and positioning.
3
+ """
4
+
5
+ from typing import Tuple
6
+
7
+ from PIL import ImageFont
8
+
9
+
10
+ def get_wrapped_text(text: str, font: ImageFont.ImageFont, line_length: int) -> str:
11
+ """
12
+ Wraps text to fit within a specified line length using the given font,
13
+ preserving existing newlines.
14
+ Args:
15
+ text (str): The text to wrap.
16
+ font (ImageFont.ImageFont): The font to use for measuring text length.
17
+ line_length (int): The maximum length of each line in pixels.
18
+
19
+ Returns:
20
+ str: The wrapped text with newlines inserted where necessary.
21
+ """
22
+ wrapped_lines = []
23
+ for line in text.split("\n"):
24
+ lines = [""]
25
+ for word in line.split():
26
+ line_to_check = f"{lines[-1]} {word}".strip()
27
+ if font.getlength(line_to_check) <= line_length:
28
+ lines[-1] = line_to_check
29
+ else:
30
+ lines.append(word)
31
+ wrapped_lines.extend(lines)
32
+ return "\n".join(wrapped_lines)
33
+
34
+
35
+ def apply_anchor(size: Tuple[int, ...], anchor: str) -> Tuple[int, int]:
36
+ """
37
+ Applies an anchor to a size tuple to determine the position of an element.
38
+ Args:
39
+ size (Tuple[int, ...]): A tuple representing the size (width, height)
40
+ or a bounding box (x1, y1, x2, y2).
41
+ anchor (str): The anchor position, e.g., "center", "top-left".
42
+ Returns:
43
+ Tuple[int, int]: A tuple representing the position (x, y) based on the anchor.
44
+ """
45
+ if len(size) == 2:
46
+ w, h = size
47
+ x, y = 0, 0
48
+ elif len(size) == 4:
49
+ x, y, x2, y2 = size
50
+ w, h = x2 - x, y2 - y
51
+ else:
52
+ raise ValueError("Size must be a tuple of 2 or 4 integers.")
53
+
54
+ anchor_points = {
55
+ "top-left": (x, y),
56
+ "top-center": (x + w // 2, y),
57
+ "top-right": (x + w, y),
58
+ "middle-left": (x, y + h // 2),
59
+ "center": (x + w // 2, y + h // 2),
60
+ "middle-right": (x + w, y + h // 2),
61
+ "bottom-left": (x, y + h),
62
+ "bottom-center": (x + w // 2, y + h),
63
+ "bottom-right": (x + w, y + h),
64
+ }
65
+
66
+ if anchor not in anchor_points:
67
+ raise ValueError(f"Unknown anchor: {anchor}")
68
+
69
+ return anchor_points[anchor]