pkgprint 0.1.0__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.
- pkgprint/__init__.py +42 -0
- pkgprint/color.py +200 -0
- pkgprint/paper.py +174 -0
- pkgprint/print_specs.py +181 -0
- pkgprint/units.py +163 -0
- pkgprint-0.1.0.dist-info/LICENSE +21 -0
- pkgprint-0.1.0.dist-info/METADATA +199 -0
- pkgprint-0.1.0.dist-info/RECORD +10 -0
- pkgprint-0.1.0.dist-info/WHEEL +5 -0
- pkgprint-0.1.0.dist-info/top_level.txt +1 -0
pkgprint/__init__.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""
|
|
2
|
+
pkgprint — Print & packaging math utilities for developers.
|
|
3
|
+
|
|
4
|
+
Provides unit conversions, color model conversions, standard paper/box sizes,
|
|
5
|
+
and bleed/margin calculations useful in print, packaging, and design workflows.
|
|
6
|
+
|
|
7
|
+
Example:
|
|
8
|
+
>>> import pkgprint
|
|
9
|
+
>>> pkgprint.mm_to_inches(210)
|
|
10
|
+
8.267716535433071
|
|
11
|
+
>>> pkgprint.paper_size("A4")
|
|
12
|
+
(210, 297)
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from .units import mm_to_inches, inches_to_mm, mm_to_points, points_to_mm, dpi_to_ppi, ppi_to_dpi
|
|
16
|
+
from .color import cmyk_to_rgb, rgb_to_cmyk, hex_to_rgb, rgb_to_hex
|
|
17
|
+
from .paper import paper_size, list_paper_sizes, box_size
|
|
18
|
+
from .print_specs import add_bleed, safe_margin, trim_size
|
|
19
|
+
|
|
20
|
+
__version__ = "0.1.0"
|
|
21
|
+
__all__ = [
|
|
22
|
+
# units
|
|
23
|
+
"mm_to_inches",
|
|
24
|
+
"inches_to_mm",
|
|
25
|
+
"mm_to_points",
|
|
26
|
+
"points_to_mm",
|
|
27
|
+
"dpi_to_ppi",
|
|
28
|
+
"ppi_to_dpi",
|
|
29
|
+
# color
|
|
30
|
+
"cmyk_to_rgb",
|
|
31
|
+
"rgb_to_cmyk",
|
|
32
|
+
"hex_to_rgb",
|
|
33
|
+
"rgb_to_hex",
|
|
34
|
+
# paper
|
|
35
|
+
"paper_size",
|
|
36
|
+
"list_paper_sizes",
|
|
37
|
+
"box_size",
|
|
38
|
+
# print specs
|
|
39
|
+
"add_bleed",
|
|
40
|
+
"safe_margin",
|
|
41
|
+
"trim_size",
|
|
42
|
+
]
|
pkgprint/color.py
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"""
|
|
2
|
+
color.py — Color model conversions for print and screen workflows.
|
|
3
|
+
|
|
4
|
+
Conversion references:
|
|
5
|
+
- CMYK → RGB: standard ICC-style formula using key (K) channel.
|
|
6
|
+
- RGB → CMYK: inverse of the above (approximate, device-independent).
|
|
7
|
+
- Hex ↔ RGB: CSS hex notation, 6-digit only (with optional '#' prefix).
|
|
8
|
+
|
|
9
|
+
CMYK inputs are accepted in **both** 0–100 and 0–1 ranges and are
|
|
10
|
+
auto-detected: values > 1 are treated as percentage (0–100).
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _normalise_cmyk(c: float, m: float, y: float, k: float) -> tuple[float, float, float, float]:
|
|
17
|
+
"""Return CMYK values normalised to the 0–1 range."""
|
|
18
|
+
values = (c, m, y, k)
|
|
19
|
+
if any(v > 1 for v in values):
|
|
20
|
+
# Assume 0–100 percentage input
|
|
21
|
+
c, m, y, k = c / 100, m / 100, y / 100, k / 100
|
|
22
|
+
return c, m, y, k
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def cmyk_to_rgb(c: float, m: float, y: float, k: float) -> tuple[int, int, int]:
|
|
26
|
+
"""Convert CMYK colour values to RGB.
|
|
27
|
+
|
|
28
|
+
Inputs are accepted in either 0–100 (percentage) or 0–1 range.
|
|
29
|
+
If any value exceeds 1, all four are treated as percentages.
|
|
30
|
+
|
|
31
|
+
Formula (ICC standard):
|
|
32
|
+
R = 255 × (1 − C) × (1 − K)
|
|
33
|
+
G = 255 × (1 − M) × (1 − K)
|
|
34
|
+
B = 255 × (1 − Y) × (1 − K)
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
c: Cyan component (0–100 or 0–1).
|
|
38
|
+
m: Magenta component (0–100 or 0–1).
|
|
39
|
+
y: Yellow component (0–100 or 0–1).
|
|
40
|
+
k: Key (Black) component (0–100 or 0–1).
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
Tuple of (R, G, B) integers in the range 0–255.
|
|
44
|
+
|
|
45
|
+
Raises:
|
|
46
|
+
ValueError: If any normalised value falls outside 0–1.
|
|
47
|
+
|
|
48
|
+
Example:
|
|
49
|
+
>>> cmyk_to_rgb(0, 100, 100, 0) # pure red in CMYK
|
|
50
|
+
(255, 0, 0)
|
|
51
|
+
>>> cmyk_to_rgb(0, 0, 0, 0) # white
|
|
52
|
+
(255, 255, 255)
|
|
53
|
+
>>> cmyk_to_rgb(0, 0, 0, 100) # black
|
|
54
|
+
(0, 0, 0)
|
|
55
|
+
"""
|
|
56
|
+
for name, val in zip(("c", "m", "y", "k"), (c, m, y, k)):
|
|
57
|
+
if not isinstance(val, (int, float)):
|
|
58
|
+
raise TypeError(f"{name!r} must be a number, got {type(val).__name__!r}")
|
|
59
|
+
|
|
60
|
+
c, m, y, k = _normalise_cmyk(c, m, y, k)
|
|
61
|
+
|
|
62
|
+
for name, val in zip(("c", "m", "y", "k"), (c, m, y, k)):
|
|
63
|
+
if not (0.0 <= val <= 1.0):
|
|
64
|
+
raise ValueError(f"{name!r} must be in range 0–1 (or 0–100), got {val}")
|
|
65
|
+
|
|
66
|
+
r = round(255 * (1 - c) * (1 - k))
|
|
67
|
+
g = round(255 * (1 - m) * (1 - k))
|
|
68
|
+
b = round(255 * (1 - y) * (1 - k))
|
|
69
|
+
return (r, g, b)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def rgb_to_cmyk(r: int, g: int, b: int) -> tuple[float, float, float, float]:
|
|
73
|
+
"""Convert RGB colour values to approximate CMYK.
|
|
74
|
+
|
|
75
|
+
This is a device-independent approximation (no ICC profile).
|
|
76
|
+
Output values are in the 0–100 percentage range, rounded to 2 dp.
|
|
77
|
+
|
|
78
|
+
Formula:
|
|
79
|
+
K = 1 − max(R, G, B) / 255
|
|
80
|
+
C = (1 − R/255 − K) / (1 − K) [0 when K=1]
|
|
81
|
+
M = (1 − G/255 − K) / (1 − K)
|
|
82
|
+
Y = (1 − B/255 − K) / (1 − K)
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
r: Red channel (0–255).
|
|
86
|
+
g: Green channel (0–255).
|
|
87
|
+
b: Blue channel (0–255).
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
Tuple of (C, M, Y, K) floats in the range 0.0–100.0.
|
|
91
|
+
|
|
92
|
+
Raises:
|
|
93
|
+
ValueError: If any channel is outside 0–255.
|
|
94
|
+
|
|
95
|
+
Example:
|
|
96
|
+
>>> rgb_to_cmyk(255, 0, 0) # red
|
|
97
|
+
(0.0, 100.0, 100.0, 0.0)
|
|
98
|
+
>>> rgb_to_cmyk(0, 0, 0) # black
|
|
99
|
+
(0.0, 0.0, 0.0, 100.0)
|
|
100
|
+
>>> rgb_to_cmyk(255, 255, 255) # white
|
|
101
|
+
(0.0, 0.0, 0.0, 0.0)
|
|
102
|
+
"""
|
|
103
|
+
for name, val in zip(("r", "g", "b"), (r, g, b)):
|
|
104
|
+
if not isinstance(val, (int, float)):
|
|
105
|
+
raise TypeError(f"{name!r} must be a number, got {type(val).__name__!r}")
|
|
106
|
+
if not (0 <= val <= 255):
|
|
107
|
+
raise ValueError(f"{name!r} must be in range 0–255, got {val}")
|
|
108
|
+
|
|
109
|
+
r_, g_, b_ = r / 255, g / 255, b / 255
|
|
110
|
+
k = 1 - max(r_, g_, b_)
|
|
111
|
+
|
|
112
|
+
if k == 1.0:
|
|
113
|
+
# Pure black — avoid division by zero
|
|
114
|
+
return (0.0, 0.0, 0.0, 100.0)
|
|
115
|
+
|
|
116
|
+
c = (1 - r_ - k) / (1 - k)
|
|
117
|
+
m = (1 - g_ - k) / (1 - k)
|
|
118
|
+
y = (1 - b_ - k) / (1 - k)
|
|
119
|
+
|
|
120
|
+
return (
|
|
121
|
+
round(c * 100, 2),
|
|
122
|
+
round(m * 100, 2),
|
|
123
|
+
round(y * 100, 2),
|
|
124
|
+
round(k * 100, 2),
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def hex_to_rgb(hex_code: str) -> tuple[int, int, int]:
|
|
129
|
+
"""Convert a CSS hex colour string to an RGB tuple.
|
|
130
|
+
|
|
131
|
+
Accepts 6-digit hex strings with or without a leading '#'.
|
|
132
|
+
3-digit shorthand (e.g. ``#fff``) is also supported and expanded.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
hex_code: Hex colour string, e.g. ``"#FF5733"`` or ``"ff5733"``.
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
Tuple of (R, G, B) integers in the range 0–255.
|
|
139
|
+
|
|
140
|
+
Raises:
|
|
141
|
+
ValueError: If *hex_code* is not a valid 3- or 6-digit hex colour.
|
|
142
|
+
|
|
143
|
+
Example:
|
|
144
|
+
>>> hex_to_rgb("#FF5733")
|
|
145
|
+
(255, 87, 51)
|
|
146
|
+
>>> hex_to_rgb("ffffff")
|
|
147
|
+
(255, 255, 255)
|
|
148
|
+
>>> hex_to_rgb("#000")
|
|
149
|
+
(0, 0, 0)
|
|
150
|
+
"""
|
|
151
|
+
if not isinstance(hex_code, str):
|
|
152
|
+
raise TypeError(f"hex_code must be a string, got {type(hex_code).__name__!r}")
|
|
153
|
+
|
|
154
|
+
code = hex_code.strip().lstrip("#")
|
|
155
|
+
|
|
156
|
+
if len(code) == 3:
|
|
157
|
+
code = "".join(ch * 2 for ch in code)
|
|
158
|
+
|
|
159
|
+
if len(code) != 6:
|
|
160
|
+
raise ValueError(
|
|
161
|
+
f"Invalid hex colour {hex_code!r}: expected 3 or 6 hex digits (with optional '#')."
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
try:
|
|
165
|
+
r = int(code[0:2], 16)
|
|
166
|
+
g = int(code[2:4], 16)
|
|
167
|
+
b = int(code[4:6], 16)
|
|
168
|
+
except ValueError:
|
|
169
|
+
raise ValueError(f"Invalid hex colour {hex_code!r}: contains non-hex characters.")
|
|
170
|
+
|
|
171
|
+
return (r, g, b)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def rgb_to_hex(r: int, g: int, b: int) -> str:
|
|
175
|
+
"""Convert RGB integers to a CSS hex colour string (uppercase, with '#').
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
r: Red channel (0–255).
|
|
179
|
+
g: Green channel (0–255).
|
|
180
|
+
b: Blue channel (0–255).
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
Uppercase hex string, e.g. ``"#FF5733"``.
|
|
184
|
+
|
|
185
|
+
Raises:
|
|
186
|
+
ValueError: If any channel is outside 0–255.
|
|
187
|
+
|
|
188
|
+
Example:
|
|
189
|
+
>>> rgb_to_hex(255, 87, 51)
|
|
190
|
+
'#FF5733'
|
|
191
|
+
>>> rgb_to_hex(0, 0, 0)
|
|
192
|
+
'#000000'
|
|
193
|
+
"""
|
|
194
|
+
for name, val in zip(("r", "g", "b"), (r, g, b)):
|
|
195
|
+
if not isinstance(val, (int, float)):
|
|
196
|
+
raise TypeError(f"{name!r} must be a number, got {type(val).__name__!r}")
|
|
197
|
+
if not (0 <= val <= 255):
|
|
198
|
+
raise ValueError(f"{name!r} must be in range 0–255, got {val}")
|
|
199
|
+
|
|
200
|
+
return "#{:02X}{:02X}{:02X}".format(int(r), int(g), int(b))
|
pkgprint/paper.py
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"""
|
|
2
|
+
paper.py — Standard paper and packaging box sizes.
|
|
3
|
+
|
|
4
|
+
All dimensions are in millimetres (mm).
|
|
5
|
+
|
|
6
|
+
Paper size references:
|
|
7
|
+
- ISO 216 A-series: https://www.iso.org/standard/36631.html
|
|
8
|
+
- ISO 216 B-series: same standard.
|
|
9
|
+
- ANSI paper sizes: ANSI/ASME Y14.1
|
|
10
|
+
- US common sizes (Letter, Legal, Tabloid): industry convention.
|
|
11
|
+
|
|
12
|
+
Box size references:
|
|
13
|
+
- RSC (Regular Slotted Container): TAPPI T 802
|
|
14
|
+
- Mailer / literature mailer: common postal sizing conventions.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
# ---------------------------------------------------------------------------
|
|
20
|
+
# Paper sizes (width × height) in mm — portrait orientation
|
|
21
|
+
# ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
_PAPER_SIZES: dict[str, tuple[int, int]] = {
|
|
24
|
+
# ISO A-series
|
|
25
|
+
"A0": (841, 1189),
|
|
26
|
+
"A1": (594, 841),
|
|
27
|
+
"A2": (420, 594),
|
|
28
|
+
"A3": (297, 420),
|
|
29
|
+
"A4": (210, 297),
|
|
30
|
+
"A5": (148, 210),
|
|
31
|
+
"A6": (105, 148),
|
|
32
|
+
"A7": (74, 105),
|
|
33
|
+
"A8": (52, 74),
|
|
34
|
+
# ISO B-series
|
|
35
|
+
"B0": (1000, 1414),
|
|
36
|
+
"B1": (707, 1000),
|
|
37
|
+
"B2": (500, 707),
|
|
38
|
+
"B3": (353, 500),
|
|
39
|
+
"B4": (250, 353),
|
|
40
|
+
"B5": (176, 250),
|
|
41
|
+
"B6": (125, 176),
|
|
42
|
+
# ISO C-series (envelope)
|
|
43
|
+
"C4": (229, 324),
|
|
44
|
+
"C5": (162, 229),
|
|
45
|
+
"C6": (114, 162),
|
|
46
|
+
# US / ANSI sizes
|
|
47
|
+
"Letter": (216, 279),
|
|
48
|
+
"Legal": (216, 356),
|
|
49
|
+
"Tabloid": (279, 432),
|
|
50
|
+
"Executive": (184, 267),
|
|
51
|
+
"ANSI A": (216, 279),
|
|
52
|
+
"ANSI B": (279, 432),
|
|
53
|
+
"ANSI C": (432, 559),
|
|
54
|
+
"ANSI D": (559, 864),
|
|
55
|
+
"ANSI E": (864, 1118),
|
|
56
|
+
# Business card sizes — two regional standards exist:
|
|
57
|
+
"Business Card ISO": (85, 55), # ISO 7810 ID-1 / European standard (85×55mm)
|
|
58
|
+
"Business Card US": (89, 51), # North American standard (3.5"×2" = 88.9×50.8mm, rounded)
|
|
59
|
+
# Postcard
|
|
60
|
+
"Postcard": (148, 105),
|
|
61
|
+
# Square social-media-friendly
|
|
62
|
+
"Square Small": (130, 130),
|
|
63
|
+
"Square Large": (210, 210),
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
# ---------------------------------------------------------------------------
|
|
67
|
+
# Packaging / box sizes (length × width × height) in mm
|
|
68
|
+
# ---------------------------------------------------------------------------
|
|
69
|
+
|
|
70
|
+
_BOX_SIZES: dict[str, tuple[int, int, int]] = {
|
|
71
|
+
# RSC (Regular Slotted Container) — common e-commerce shipping boxes
|
|
72
|
+
"RSC Small": (200, 150, 100), # ~6L mailer
|
|
73
|
+
"RSC Medium": (305, 229, 152), # ~10L standard shipper
|
|
74
|
+
"RSC Large": (406, 305, 203), # ~25L large shipper
|
|
75
|
+
# Literature / document mailers
|
|
76
|
+
"Mailer A4": (320, 230, 30), # fits A4 flat
|
|
77
|
+
"Mailer A5": (230, 160, 20), # fits A5 flat
|
|
78
|
+
# Gift / retail boxes
|
|
79
|
+
"Gift Small": (150, 100, 50),
|
|
80
|
+
"Gift Medium": (220, 170, 80),
|
|
81
|
+
"Gift Large": (300, 220, 110),
|
|
82
|
+
# Pizza-style tuck-top (common for cosmetics / pharma)
|
|
83
|
+
"Tuck Small": (80, 50, 20),
|
|
84
|
+
"Tuck Medium": (120, 80, 35),
|
|
85
|
+
"Tuck Large": (170, 110, 50),
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def paper_size(name: str) -> tuple[int, int]:
|
|
90
|
+
"""Return the (width, height) in mm for a named standard paper size.
|
|
91
|
+
|
|
92
|
+
Lookup is **case-insensitive** and ignores leading/trailing whitespace.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
name: Paper size name, e.g. ``"A4"``, ``"letter"``, ``"Legal"``.
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
Tuple of (width, height) in millimetres (portrait orientation).
|
|
99
|
+
|
|
100
|
+
Raises:
|
|
101
|
+
KeyError: If *name* is not a recognised paper size.
|
|
102
|
+
|
|
103
|
+
Example:
|
|
104
|
+
>>> paper_size("A4")
|
|
105
|
+
(210, 297)
|
|
106
|
+
>>> paper_size("letter")
|
|
107
|
+
(216, 279)
|
|
108
|
+
"""
|
|
109
|
+
if not isinstance(name, str):
|
|
110
|
+
raise TypeError(f"name must be a string, got {type(name).__name__!r}")
|
|
111
|
+
|
|
112
|
+
# Case-insensitive lookup
|
|
113
|
+
normalised = name.strip()
|
|
114
|
+
for key, dims in _PAPER_SIZES.items():
|
|
115
|
+
if key.lower() == normalised.lower():
|
|
116
|
+
return dims
|
|
117
|
+
|
|
118
|
+
available = ", ".join(sorted(_PAPER_SIZES))
|
|
119
|
+
raise KeyError(
|
|
120
|
+
f"Unknown paper size {name!r}. "
|
|
121
|
+
f"Call list_paper_sizes() to see all options.\nAvailable: {available}"
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def list_paper_sizes() -> list[str]:
|
|
126
|
+
"""Return a sorted list of all supported paper size names.
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
Alphabetically sorted list of size name strings.
|
|
130
|
+
|
|
131
|
+
Example:
|
|
132
|
+
>>> names = list_paper_sizes()
|
|
133
|
+
>>> "A4" in names
|
|
134
|
+
True
|
|
135
|
+
>>> "Letter" in names
|
|
136
|
+
True
|
|
137
|
+
"""
|
|
138
|
+
return sorted(_PAPER_SIZES.keys())
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def box_size(style: str) -> tuple[int, int, int]:
|
|
142
|
+
"""Return standard packaging box dimensions (L × W × H) in mm.
|
|
143
|
+
|
|
144
|
+
Lookup is **case-insensitive** and ignores leading/trailing whitespace.
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
style: Box style name, e.g. ``"RSC Medium"``, ``"Mailer A4"``,
|
|
148
|
+
``"Gift Large"``.
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
Tuple of (length, width, height) in millimetres.
|
|
152
|
+
|
|
153
|
+
Raises:
|
|
154
|
+
KeyError: If *style* is not a recognised box style.
|
|
155
|
+
|
|
156
|
+
Example:
|
|
157
|
+
>>> box_size("RSC Medium")
|
|
158
|
+
(305, 229, 152)
|
|
159
|
+
>>> box_size("mailer a4")
|
|
160
|
+
(320, 230, 30)
|
|
161
|
+
"""
|
|
162
|
+
if not isinstance(style, str):
|
|
163
|
+
raise TypeError(f"style must be a string, got {type(style).__name__!r}")
|
|
164
|
+
|
|
165
|
+
normalised = style.strip()
|
|
166
|
+
for key, dims in _BOX_SIZES.items():
|
|
167
|
+
if key.lower() == normalised.lower():
|
|
168
|
+
return dims
|
|
169
|
+
|
|
170
|
+
available = ", ".join(sorted(_BOX_SIZES))
|
|
171
|
+
raise KeyError(
|
|
172
|
+
f"Unknown box style {style!r}. "
|
|
173
|
+
f"Available styles: {available}"
|
|
174
|
+
)
|
pkgprint/print_specs.py
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"""
|
|
2
|
+
print_specs.py — Print production math: bleed, safe margin, and trim size.
|
|
3
|
+
|
|
4
|
+
All dimensions are in millimetres (mm).
|
|
5
|
+
|
|
6
|
+
Terminology:
|
|
7
|
+
- Bleed: Extra artwork beyond the trim edge, cut away during finishing.
|
|
8
|
+
Ensures colour/image reaches the physical edge with no white gaps.
|
|
9
|
+
Industry default: 3 mm on each side (ISO / offset litho).
|
|
10
|
+
- Trim size: The finished dimensions after cutting.
|
|
11
|
+
- Safe margin: Inner padding inside the trim where critical content should stay,
|
|
12
|
+
to avoid it being accidentally cut or visually too close to the edge.
|
|
13
|
+
Typical recommendation: 3–5 mm inside the trim line.
|
|
14
|
+
|
|
15
|
+
Reference: FOGRA / ISO 12647-2 for offset print bleed conventions.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _validate_dimension(value: float, name: str) -> None:
|
|
22
|
+
"""Raise TypeError/ValueError for invalid dimension values."""
|
|
23
|
+
if not isinstance(value, (int, float)):
|
|
24
|
+
raise TypeError(f"{name!r} must be a number, got {type(value).__name__!r}")
|
|
25
|
+
if value <= 0:
|
|
26
|
+
raise ValueError(f"{name!r} must be positive, got {value}")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _validate_offset(value: float, name: str) -> None:
|
|
30
|
+
"""Raise TypeError/ValueError for invalid offset/margin values."""
|
|
31
|
+
if not isinstance(value, (int, float)):
|
|
32
|
+
raise TypeError(f"{name!r} must be a number, got {type(value).__name__!r}")
|
|
33
|
+
if value < 0:
|
|
34
|
+
raise ValueError(f"{name!r} cannot be negative, got {value}")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def add_bleed(
|
|
38
|
+
width: float,
|
|
39
|
+
height: float,
|
|
40
|
+
bleed_mm: float = 3.0,
|
|
41
|
+
) -> tuple[float, float]:
|
|
42
|
+
"""Return artwork dimensions with bleed added to all four sides.
|
|
43
|
+
|
|
44
|
+
Bleed is added symmetrically: ``bleed_mm`` is added to **each** side,
|
|
45
|
+
so the total increase per axis is ``2 × bleed_mm``.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
width: Trim width in mm (must be positive).
|
|
49
|
+
height: Trim height in mm (must be positive).
|
|
50
|
+
bleed_mm: Bleed amount per side in mm. Default is 3 mm (industry standard).
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
Tuple of (bleed_width, bleed_height) in mm.
|
|
54
|
+
|
|
55
|
+
Raises:
|
|
56
|
+
TypeError: If any argument is not numeric.
|
|
57
|
+
ValueError: If *width* or *height* is not positive, or *bleed_mm* is negative.
|
|
58
|
+
|
|
59
|
+
Example:
|
|
60
|
+
>>> add_bleed(89, 51) # business card with 3 mm bleed
|
|
61
|
+
(95, 57)
|
|
62
|
+
>>> add_bleed(210, 297, bleed_mm=5) # A4 with 5 mm bleed
|
|
63
|
+
(220, 307)
|
|
64
|
+
>>> add_bleed(210, 297, bleed_mm=0) # no bleed
|
|
65
|
+
(210, 297)
|
|
66
|
+
"""
|
|
67
|
+
_validate_dimension(width, "width")
|
|
68
|
+
_validate_dimension(height, "height")
|
|
69
|
+
_validate_offset(bleed_mm, "bleed_mm")
|
|
70
|
+
|
|
71
|
+
bleed_width = width + 2 * bleed_mm
|
|
72
|
+
bleed_height = height + 2 * bleed_mm
|
|
73
|
+
# Return int if values are whole numbers, otherwise float
|
|
74
|
+
bleed_width = int(bleed_width) if bleed_width == int(bleed_width) else bleed_width
|
|
75
|
+
bleed_height = int(bleed_height) if bleed_height == int(bleed_height) else bleed_height
|
|
76
|
+
return (bleed_width, bleed_height)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def safe_margin(
|
|
80
|
+
width: float,
|
|
81
|
+
height: float,
|
|
82
|
+
margin_mm: float = 5.0,
|
|
83
|
+
) -> tuple[float, float]:
|
|
84
|
+
"""Return the safe content area after subtracting margins from all sides.
|
|
85
|
+
|
|
86
|
+
The safe area is the inner region where critical content (text, logos)
|
|
87
|
+
should be placed. ``margin_mm`` is subtracted from **each** side, so the
|
|
88
|
+
total reduction per axis is ``2 × margin_mm``.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
width: Trim width in mm (must be positive).
|
|
92
|
+
height: Trim height in mm (must be positive).
|
|
93
|
+
margin_mm: Margin per side in mm. Default is 5 mm.
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
Tuple of (safe_width, safe_height) in mm.
|
|
97
|
+
|
|
98
|
+
Raises:
|
|
99
|
+
TypeError: If any argument is not numeric.
|
|
100
|
+
ValueError: If *width* or *height* is not positive, *margin_mm* is
|
|
101
|
+
negative, or the margin exceeds half the dimension.
|
|
102
|
+
|
|
103
|
+
Example:
|
|
104
|
+
>>> safe_margin(210, 297) # A4 with 5 mm margins
|
|
105
|
+
(200, 287)
|
|
106
|
+
>>> safe_margin(89, 51, margin_mm=3) # business card with 3 mm margins
|
|
107
|
+
(83, 45)
|
|
108
|
+
"""
|
|
109
|
+
_validate_dimension(width, "width")
|
|
110
|
+
_validate_dimension(height, "height")
|
|
111
|
+
_validate_offset(margin_mm, "margin_mm")
|
|
112
|
+
|
|
113
|
+
safe_w = width - 2 * margin_mm
|
|
114
|
+
safe_h = height - 2 * margin_mm
|
|
115
|
+
|
|
116
|
+
if safe_w <= 0:
|
|
117
|
+
raise ValueError(
|
|
118
|
+
f"margin_mm={margin_mm} is too large for width={width}: "
|
|
119
|
+
f"safe width would be {safe_w} mm."
|
|
120
|
+
)
|
|
121
|
+
if safe_h <= 0:
|
|
122
|
+
raise ValueError(
|
|
123
|
+
f"margin_mm={margin_mm} is too large for height={height}: "
|
|
124
|
+
f"safe height would be {safe_h} mm."
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
safe_w = int(safe_w) if safe_w == int(safe_w) else safe_w
|
|
128
|
+
safe_h = int(safe_h) if safe_h == int(safe_h) else safe_h
|
|
129
|
+
return (safe_w, safe_h)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def trim_size(
|
|
133
|
+
width: float,
|
|
134
|
+
height: float,
|
|
135
|
+
bleed_mm: float = 3.0,
|
|
136
|
+
) -> tuple[float, float]:
|
|
137
|
+
"""Reverse-calculate trim size from artwork dimensions that include bleed.
|
|
138
|
+
|
|
139
|
+
The inverse of :func:`add_bleed`. Given a canvas/file size that already
|
|
140
|
+
has bleed, return the final trim (finished) dimensions.
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
width: Artwork width including bleed, in mm (must be positive).
|
|
144
|
+
height: Artwork height including bleed, in mm (must be positive).
|
|
145
|
+
bleed_mm: Bleed amount per side in mm. Default is 3 mm.
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
Tuple of (trim_width, trim_height) in mm.
|
|
149
|
+
|
|
150
|
+
Raises:
|
|
151
|
+
TypeError: If any argument is not numeric.
|
|
152
|
+
ValueError: If *width* or *height* is not positive, *bleed_mm* is
|
|
153
|
+
negative, or the bleed exceeds the available dimension.
|
|
154
|
+
|
|
155
|
+
Example:
|
|
156
|
+
>>> trim_size(95, 57) # back-calc from business card with bleed
|
|
157
|
+
(89, 51)
|
|
158
|
+
>>> trim_size(220, 307, bleed_mm=5)
|
|
159
|
+
(210, 297)
|
|
160
|
+
"""
|
|
161
|
+
_validate_dimension(width, "width")
|
|
162
|
+
_validate_dimension(height, "height")
|
|
163
|
+
_validate_offset(bleed_mm, "bleed_mm")
|
|
164
|
+
|
|
165
|
+
trim_w = width - 2 * bleed_mm
|
|
166
|
+
trim_h = height - 2 * bleed_mm
|
|
167
|
+
|
|
168
|
+
if trim_w <= 0:
|
|
169
|
+
raise ValueError(
|
|
170
|
+
f"bleed_mm={bleed_mm} is too large for width={width}: "
|
|
171
|
+
f"trim width would be {trim_w} mm."
|
|
172
|
+
)
|
|
173
|
+
if trim_h <= 0:
|
|
174
|
+
raise ValueError(
|
|
175
|
+
f"bleed_mm={bleed_mm} is too large for height={height}: "
|
|
176
|
+
f"trim height would be {trim_h} mm."
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
trim_w = int(trim_w) if trim_w == int(trim_w) else trim_w
|
|
180
|
+
trim_h = int(trim_h) if trim_h == int(trim_h) else trim_h
|
|
181
|
+
return (trim_w, trim_h)
|
pkgprint/units.py
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"""
|
|
2
|
+
units.py — Measurement unit conversions for print and packaging.
|
|
3
|
+
|
|
4
|
+
Conversion references:
|
|
5
|
+
- 1 inch = 25.4 mm (ISO 1)
|
|
6
|
+
- 1 point = 1/72 inch (PostScript / CSS pt)
|
|
7
|
+
- DPI/PPI are numerically equivalent for these conversions;
|
|
8
|
+
the functions are provided as semantic aliases for clarity.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def mm_to_inches(mm: float) -> float:
|
|
13
|
+
"""Convert millimetres to inches.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
mm: Length in millimetres.
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
Equivalent length in inches.
|
|
20
|
+
|
|
21
|
+
Raises:
|
|
22
|
+
TypeError: If *mm* is not a numeric type.
|
|
23
|
+
|
|
24
|
+
Example:
|
|
25
|
+
>>> mm_to_inches(25.4)
|
|
26
|
+
1.0
|
|
27
|
+
>>> mm_to_inches(210) # A4 width
|
|
28
|
+
8.267716535433071
|
|
29
|
+
"""
|
|
30
|
+
if not isinstance(mm, (int, float)):
|
|
31
|
+
raise TypeError(f"mm must be a number, got {type(mm).__name__!r}")
|
|
32
|
+
return mm / 25.4
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def inches_to_mm(inches: float) -> float:
|
|
36
|
+
"""Convert inches to millimetres.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
inches: Length in inches.
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
Equivalent length in millimetres.
|
|
43
|
+
|
|
44
|
+
Raises:
|
|
45
|
+
TypeError: If *inches* is not a numeric type.
|
|
46
|
+
|
|
47
|
+
Example:
|
|
48
|
+
>>> inches_to_mm(1.0)
|
|
49
|
+
25.4
|
|
50
|
+
>>> inches_to_mm(8.5) # US Letter width
|
|
51
|
+
215.9
|
|
52
|
+
"""
|
|
53
|
+
if not isinstance(inches, (int, float)):
|
|
54
|
+
raise TypeError(f"inches must be a number, got {type(inches).__name__!r}")
|
|
55
|
+
return inches * 25.4
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def mm_to_points(mm: float) -> float:
|
|
59
|
+
"""Convert millimetres to typographic points (PostScript / CSS pt).
|
|
60
|
+
|
|
61
|
+
1 pt = 1/72 inch = 25.4/72 mm ≈ 0.35278 mm
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
mm: Length in millimetres.
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
Equivalent length in points.
|
|
68
|
+
|
|
69
|
+
Raises:
|
|
70
|
+
TypeError: If *mm* is not a numeric type.
|
|
71
|
+
|
|
72
|
+
Example:
|
|
73
|
+
>>> round(mm_to_points(25.4), 4)
|
|
74
|
+
72.0
|
|
75
|
+
>>> round(mm_to_points(210), 2) # A4 width
|
|
76
|
+
595.28
|
|
77
|
+
"""
|
|
78
|
+
if not isinstance(mm, (int, float)):
|
|
79
|
+
raise TypeError(f"mm must be a number, got {type(mm).__name__!r}")
|
|
80
|
+
return mm * (72 / 25.4)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def points_to_mm(points: float) -> float:
|
|
84
|
+
"""Convert typographic points to millimetres.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
points: Length in points (PostScript / CSS pt, 1 pt = 1/72 inch).
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
Equivalent length in millimetres.
|
|
91
|
+
|
|
92
|
+
Raises:
|
|
93
|
+
TypeError: If *points* is not a numeric type.
|
|
94
|
+
|
|
95
|
+
Example:
|
|
96
|
+
>>> round(points_to_mm(72), 4)
|
|
97
|
+
25.4
|
|
98
|
+
>>> round(points_to_mm(595.28), 2)
|
|
99
|
+
210.0
|
|
100
|
+
"""
|
|
101
|
+
if not isinstance(points, (int, float)):
|
|
102
|
+
raise TypeError(f"points must be a number, got {type(points).__name__!r}")
|
|
103
|
+
return points * (25.4 / 72)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def dpi_to_ppi(dpi: float) -> float:
|
|
107
|
+
"""Convert print DPI (dots per inch) to screen PPI (pixels per inch).
|
|
108
|
+
|
|
109
|
+
DPI and PPI share the same unit (measurements per linear inch), so this
|
|
110
|
+
function is a semantic alias that returns the value unchanged. It exists
|
|
111
|
+
to make intent explicit when bridging print and screen workflows.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
dpi: Resolution in dots per inch.
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
Equivalent resolution in pixels per inch.
|
|
118
|
+
|
|
119
|
+
Raises:
|
|
120
|
+
TypeError: If *dpi* is not a numeric type.
|
|
121
|
+
ValueError: If *dpi* is negative.
|
|
122
|
+
|
|
123
|
+
Example:
|
|
124
|
+
>>> dpi_to_ppi(300)
|
|
125
|
+
300
|
|
126
|
+
>>> dpi_to_ppi(72.0)
|
|
127
|
+
72.0
|
|
128
|
+
"""
|
|
129
|
+
if not isinstance(dpi, (int, float)):
|
|
130
|
+
raise TypeError(f"dpi must be a number, got {type(dpi).__name__!r}")
|
|
131
|
+
if dpi < 0:
|
|
132
|
+
raise ValueError(f"dpi cannot be negative, got {dpi}")
|
|
133
|
+
return dpi
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def ppi_to_dpi(ppi: float) -> float:
|
|
137
|
+
"""Convert screen PPI (pixels per inch) to print DPI (dots per inch).
|
|
138
|
+
|
|
139
|
+
PPI and DPI share the same unit, so this function is a semantic alias
|
|
140
|
+
that returns the value unchanged. It exists to make intent explicit when
|
|
141
|
+
bridging screen and print workflows.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
ppi: Resolution in pixels per inch.
|
|
145
|
+
|
|
146
|
+
Returns:
|
|
147
|
+
Equivalent resolution in dots per inch.
|
|
148
|
+
|
|
149
|
+
Raises:
|
|
150
|
+
TypeError: If *ppi* is not a numeric type.
|
|
151
|
+
ValueError: If *ppi* is negative.
|
|
152
|
+
|
|
153
|
+
Example:
|
|
154
|
+
>>> ppi_to_dpi(96)
|
|
155
|
+
96
|
|
156
|
+
>>> ppi_to_dpi(144.0)
|
|
157
|
+
144.0
|
|
158
|
+
"""
|
|
159
|
+
if not isinstance(ppi, (int, float)):
|
|
160
|
+
raise TypeError(f"ppi must be a number, got {type(ppi).__name__!r}")
|
|
161
|
+
if ppi < 0:
|
|
162
|
+
raise ValueError(f"ppi cannot be negative, got {ppi}")
|
|
163
|
+
return ppi
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Rishabh
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
|
+
Name: pkgprint
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Print and packaging math utilities for developers
|
|
5
|
+
Author: Rishabh
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/Rishabh55122/pkgprint
|
|
8
|
+
Project-URL: Bug Tracker, https://github.com/Rishabh55122/pkgprint/issues
|
|
9
|
+
Keywords: print,packaging,design,cmyk,dpi,bleed,typography
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Topic :: Multimedia :: Graphics
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
20
|
+
Classifier: Intended Audience :: Developers
|
|
21
|
+
Requires-Python: >=3.8
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
26
|
+
Requires-Dist: build; extra == "dev"
|
|
27
|
+
Requires-Dist: twine; extra == "dev"
|
|
28
|
+
|
|
29
|
+
# pkgprint
|
|
30
|
+
|
|
31
|
+
**Print and packaging math utilities for Python developers.**
|
|
32
|
+
|
|
33
|
+
[](https://pypi.org/project/pkgprint/)
|
|
34
|
+
[](https://www.python.org/)
|
|
35
|
+
[](LICENSE)
|
|
36
|
+
|
|
37
|
+
`pkgprint` is a zero-dependency Python library that handles the fiddly maths behind print and packaging production: unit conversions, colour model translations, standard paper and box sizes, and bleed/margin arithmetic.
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## Installation
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
pip install pkgprint
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## Quick Start
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
import pkgprint
|
|
53
|
+
|
|
54
|
+
# Get A4 dimensions and convert to inches
|
|
55
|
+
w, h = pkgprint.paper_size("A4")
|
|
56
|
+
print(pkgprint.mm_to_inches(w), pkgprint.mm_to_inches(h))
|
|
57
|
+
# → 8.267716535433071 11.692913385826772
|
|
58
|
+
|
|
59
|
+
# Add 3 mm bleed to a business card
|
|
60
|
+
bleed_w, bleed_h = pkgprint.add_bleed(89, 51, bleed_mm=3)
|
|
61
|
+
print(bleed_w, bleed_h)
|
|
62
|
+
# → 95 57
|
|
63
|
+
|
|
64
|
+
# Convert a brand colour from CMYK to RGB
|
|
65
|
+
rgb = pkgprint.cmyk_to_rgb(0, 100, 100, 0) # Pantone-ish red
|
|
66
|
+
print(rgb)
|
|
67
|
+
# → (255, 0, 0)
|
|
68
|
+
|
|
69
|
+
# Convert back from hex to RGB
|
|
70
|
+
print(pkgprint.hex_to_rgb("#FF5733"))
|
|
71
|
+
# → (255, 87, 51)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## API Reference
|
|
77
|
+
|
|
78
|
+
### `units` — Measurement Conversions
|
|
79
|
+
|
|
80
|
+
| Function | Description |
|
|
81
|
+
|---|---|
|
|
82
|
+
| `mm_to_inches(mm)` | Millimetres → inches |
|
|
83
|
+
| `inches_to_mm(inches)` | Inches → millimetres |
|
|
84
|
+
| `mm_to_points(mm)` | Millimetres → typographic points (1 pt = 1/72 in) |
|
|
85
|
+
| `points_to_mm(points)` | Points → millimetres |
|
|
86
|
+
| `dpi_to_ppi(dpi)` | DPI → PPI (semantic alias, same unit) |
|
|
87
|
+
| `ppi_to_dpi(ppi)` | PPI → DPI (semantic alias, same unit) |
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
import pkgprint
|
|
91
|
+
|
|
92
|
+
pkgprint.mm_to_inches(25.4) # → 1.0
|
|
93
|
+
pkgprint.inches_to_mm(8.5) # → 215.9 (US Letter width)
|
|
94
|
+
pkgprint.mm_to_points(210) # → 595.28 (A4 width in points)
|
|
95
|
+
pkgprint.points_to_mm(72) # → 25.4
|
|
96
|
+
pkgprint.dpi_to_ppi(300) # → 300
|
|
97
|
+
pkgprint.ppi_to_dpi(96) # → 96
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
### `color` — Colour Model Conversions
|
|
103
|
+
|
|
104
|
+
| Function | Description |
|
|
105
|
+
|---|---|
|
|
106
|
+
| `cmyk_to_rgb(c, m, y, k)` | CMYK → RGB. Accepts 0–100 **or** 0–1 inputs. |
|
|
107
|
+
| `rgb_to_cmyk(r, g, b)` | RGB → approximate CMYK (device-independent). |
|
|
108
|
+
| `hex_to_rgb(hex_code)` | CSS hex string → RGB tuple. Supports `#fff` shorthand. |
|
|
109
|
+
| `rgb_to_hex(r, g, b)` | RGB tuple → uppercase CSS hex string. |
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
import pkgprint
|
|
113
|
+
|
|
114
|
+
pkgprint.cmyk_to_rgb(0, 100, 100, 0) # → (255, 0, 0) red
|
|
115
|
+
pkgprint.cmyk_to_rgb(0, 1, 1, 0) # → (255, 0, 0) same, 0–1 range
|
|
116
|
+
pkgprint.rgb_to_cmyk(0, 0, 255) # → (100.0, 100.0, 0.0, 0.0) blue
|
|
117
|
+
pkgprint.hex_to_rgb("#FF5733") # → (255, 87, 51)
|
|
118
|
+
pkgprint.hex_to_rgb("#fff") # → (255, 255, 255) shorthand ok
|
|
119
|
+
pkgprint.rgb_to_hex(255, 87, 51) # → '#FF5733'
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
### `paper` — Standard Sizes
|
|
125
|
+
|
|
126
|
+
| Function | Description |
|
|
127
|
+
|---|---|
|
|
128
|
+
| `paper_size(name)` | `(width, height)` in mm for a named paper size. Case-insensitive. |
|
|
129
|
+
| `list_paper_sizes()` | Sorted list of all supported paper size names. |
|
|
130
|
+
| `box_size(style)` | `(length, width, height)` in mm for a named packaging box. Case-insensitive. |
|
|
131
|
+
|
|
132
|
+
**Supported paper sizes:** ISO A0–A8, B0–B6, C4–C6 (envelopes), Letter, Legal, Tabloid, Executive, ANSI A–E, Business Card, Postcard, and more.
|
|
133
|
+
|
|
134
|
+
**Supported box styles:** RSC Small/Medium/Large, Mailer A4/A5, Gift Small/Medium/Large, Tuck Small/Medium/Large.
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
import pkgprint
|
|
138
|
+
|
|
139
|
+
pkgprint.paper_size("A4") # → (210, 297)
|
|
140
|
+
pkgprint.paper_size("letter") # → (216, 279) case-insensitive
|
|
141
|
+
pkgprint.box_size("RSC Medium") # → (305, 229, 152)
|
|
142
|
+
pkgprint.list_paper_sizes() # → ['A0', 'A1', ..., 'Tabloid', ...]
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
### `print_specs` — Print Production Math
|
|
148
|
+
|
|
149
|
+
| Function | Description |
|
|
150
|
+
|---|---|
|
|
151
|
+
| `add_bleed(width, height, bleed_mm=3)` | Trim size → artwork size with bleed on all four sides. |
|
|
152
|
+
| `safe_margin(width, height, margin_mm=5)` | Trim size → safe content area (removes margins). |
|
|
153
|
+
| `trim_size(width, height, bleed_mm=3)` | Artwork size (with bleed) → trim size. |
|
|
154
|
+
|
|
155
|
+
```python
|
|
156
|
+
import pkgprint
|
|
157
|
+
|
|
158
|
+
# Business card: 89 × 51 mm → with 3 mm bleed → 95 × 57 mm
|
|
159
|
+
pkgprint.add_bleed(89, 51) # → (95, 57)
|
|
160
|
+
|
|
161
|
+
# A4 safe content area with 5 mm margins
|
|
162
|
+
pkgprint.safe_margin(210, 297, margin_mm=5) # → (200, 287)
|
|
163
|
+
|
|
164
|
+
# Reverse: what is the trim size of a 220 × 307 mm file with 5 mm bleed?
|
|
165
|
+
pkgprint.trim_size(220, 307, bleed_mm=5) # → (210, 297)
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
## Build & Publish
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
# Install build tools
|
|
174
|
+
pip install build twine
|
|
175
|
+
|
|
176
|
+
# Build source + wheel
|
|
177
|
+
python -m build
|
|
178
|
+
|
|
179
|
+
# Test publish (TestPyPI)
|
|
180
|
+
twine upload --repository testpypi dist/*
|
|
181
|
+
|
|
182
|
+
# Real publish
|
|
183
|
+
twine upload dist/*
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
## Running Tests
|
|
189
|
+
|
|
190
|
+
```bash
|
|
191
|
+
pip install pytest
|
|
192
|
+
pytest
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
---
|
|
196
|
+
|
|
197
|
+
## License
|
|
198
|
+
|
|
199
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
pkgprint/__init__.py,sha256=ulgXTBBGuKYIajYYCaDUVWnv4GJ7GD6Jwr9azFUk8rI,1045
|
|
2
|
+
pkgprint/color.py,sha256=BZ6Xl_Kr7cXOAZJ2hPzFxTVYn2wNmJL5qm0M0mao95c,6278
|
|
3
|
+
pkgprint/paper.py,sha256=PqWh6aUiven9_qES6AyPQd9dK5JmybZNGkgVltpAO8Q,5252
|
|
4
|
+
pkgprint/print_specs.py,sha256=p_7RxHNFlsvfQ5d09oZvey_Vqucvr8Xeul5Jo4uHUuo,6434
|
|
5
|
+
pkgprint/units.py,sha256=wdeRkdypAw2-gbnj21bqHcFSlX1h0SMsk_zS6OyUus0,4292
|
|
6
|
+
pkgprint-0.1.0.dist-info/LICENSE,sha256=NKgQ3-TWgHdRTQX1pYHv12yIXKOQ5_AUxjm_MMGI4LM,1064
|
|
7
|
+
pkgprint-0.1.0.dist-info/METADATA,sha256=6usdQcQ0gu0FiUsOFnIBRVIPJvWOMuFQ-7blt1UzI5o,6156
|
|
8
|
+
pkgprint-0.1.0.dist-info/WHEEL,sha256=beeZ86-EfXScwlR_HKu4SllMC9wUEj_8Z_4FJ3egI2w,91
|
|
9
|
+
pkgprint-0.1.0.dist-info/top_level.txt,sha256=BauSMV774J8U8YJnExtUUcw_yjTTe4ngEsrznqEDqzM,9
|
|
10
|
+
pkgprint-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pkgprint
|