-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathpls339_barcodes.py
More file actions
executable file
·189 lines (156 loc) · 7.08 KB
/
Copy pathpls339_barcodes.py
File metadata and controls
executable file
·189 lines (156 loc) · 7.08 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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
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
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
#!/usr/bin/env python3
"""
Generate Code 128 barcode label sheets as print-ready PDFs for
Premium Label Supply PLS339 sheets (1.75" x 0.5", 4 columns x 20 rows,
80 labels per US Letter 8.5" x 11" sheet).
Intended for high-throughput screening: labeling 96- and 384-well plates with
machine-readable IDs that can be read by the barcode readers integrated into
microscopes and plate readers, or by a handheld wedge scanner.
Layout notes:
- Horizontal spacing: outer margins = 0.33", inner gaps (between columns) = 0.28"
- Vertical spacing: top and bottom margins = 0.50" exactly; inter-row gap is computed
- Barcodes: Code 128, shortened bar height, inner padding, human-readable text
centered below the bars
Run with no arguments for interactive prompts, or pass flags for scripted use:
python pls339_barcodes.py
python pls339_barcodes.py --prefix JZS- --start 1 --sheets 2 --digits 4
Output filename defaults to "<PREFIX><START zero-padded>.pdf", e.g. "JZS-0001.pdf".
"""
import argparse
import os
import sys
from reportlab.pdfgen import canvas
from reportlab.lib.pagesizes import letter
from reportlab.lib.units import inch
from reportlab.pdfbase.pdfmetrics import stringWidth
from reportlab.graphics.barcode import code128
# ---------------------- Fixed PLS339 geometry ----------------------
PAGE_W, PAGE_H = letter
COLS, ROWS = 4, 20
LABEL_W, LABEL_H = 1.75*inch, 0.5*inch
# Horizontal: outer margins 0.33", inner gaps 0.28"
MARGIN_L = 0.33*inch
MARGIN_R = 0.33*inch
H_GAP = 0.28*inch
# Vertical: top/bottom 0.50" exactly; compute V_GAP
MARGIN_T = 0.5*inch
MARGIN_B = 0.5*inch
available_height = PAGE_H - (MARGIN_T + MARGIN_B) - ROWS*LABEL_H
V_GAP = available_height / (ROWS - 1)
# Fine global offsets (use if your printer consistently shifts).
# Positive OFFSET_X moves labels right; positive OFFSET_Y moves them up.
# Can also be set at runtime with --offset-x / --offset-y (in inches).
OFFSET_X = 0.0
OFFSET_Y = 0.0
# ---------------------- Styling (scanner-friendly) -----------------
INNER_PAD_X = 0.10*inch # left/right padding inside each label
INNER_PAD_Y = 0.04*inch # top/bottom padding inside each label
TEXT_FONT = "Helvetica"
TEXT_SIZE = 6.5
TEXT_GAP = 0.02*inch
TEXT_LINE = TEXT_SIZE * 1.2
HEIGHT_REDUCTION_FACTOR = 0.85 # slightly shorter bars for vertical clearance
WIDTH_FILL_FRACTION = 0.90 # do not fill full width; leave side whitespace
BAR_AREA_W = (LABEL_W - 2*INNER_PAD_X) * WIDTH_FILL_FRACTION
BAR_AREA_H = (LABEL_H - 2*INNER_PAD_Y - TEXT_LINE - TEXT_GAP) * HEIGHT_REDUCTION_FACTOR
LABELS_PER_SHEET = ROWS * COLS
# ---------------------- Helpers -----------------------------------
def label_origin(r, c):
"""Lower-left corner of label at row r, column c (r=0 top)."""
x = MARGIN_L + c*(LABEL_W + H_GAP) + OFFSET_X
y_top = PAGE_H - (MARGIN_T + r*(LABEL_H + V_GAP)) + OFFSET_Y
y = y_top - LABEL_H
return x, y
def draw_code128_label(c, r, col, text):
lx, ly = label_origin(r, col)
cx = lx + LABEL_W/2.0
bc = code128.Code128(text, barHeight=BAR_AREA_H, humanReadable=False)
scale_x = BAR_AREA_W / bc.width if bc.width > 0 else 1.0
scale = min(scale_x, 1.0) # never upscale beyond the natural module width
# Place barcode
bar_bottom = ly + INNER_PAD_Y + TEXT_LINE + TEXT_GAP
bar_left = cx - (bc.width*scale)/2.0
c.saveState()
c.translate(bar_left, bar_bottom)
c.scale(scale, scale)
bc.drawOn(c, 0, 0)
c.restoreState()
# Human-readable text
text_baseline = ly + INNER_PAD_Y
c.setFont(TEXT_FONT, TEXT_SIZE)
w = stringWidth(text, TEXT_FONT, TEXT_SIZE)
c.drawString(cx - w/2.0, text_baseline, text)
def build_sequence(prefix: str, start: int, digits: int, sheets: int):
total = LABELS_PER_SHEET * sheets
return [f"{prefix}{str(i).zfill(digits)}" for i in range(start, start + total)]
def create_pdf(output_file: str, codes):
out_dir = os.path.dirname(os.path.abspath(output_file))
if out_dir:
os.makedirs(out_dir, exist_ok=True)
c = canvas.Canvas(output_file, pagesize=letter)
idx = 0
sheets = (len(codes) + LABELS_PER_SHEET - 1) // LABELS_PER_SHEET
for _ in range(sheets):
for r in range(ROWS):
for col in range(COLS):
if idx >= len(codes):
break
draw_code128_label(c, r, col, codes[idx])
idx += 1
c.showPage()
c.save()
print(f"Saved: {output_file}")
# ---------------------- Interactive entry -------------------------
def ask_prefix() -> str:
while True:
p = input("Prefix (e.g., JZS-): ").strip()
if p:
return p
print("Please enter a non-empty prefix.")
def ask_int(prompt: str, default=None, min_value=1):
while True:
s = input(f"{prompt}{' ['+str(default)+']' if default is not None else ''}: ").strip()
if not s and default is not None:
return default
try:
val = int(s)
if val < min_value:
raise ValueError
return val
except ValueError:
print(f"Please enter an integer >= {min_value}.")
def parse_args(argv):
p = argparse.ArgumentParser(
description="Generate Code 128 barcode sheets for PLS339 label stock "
"(1.75\" x 0.5\", 4 x 20, 80 labels per US Letter sheet).",
epilog="Run with no arguments to be prompted interactively.",
)
p.add_argument("--prefix", help="Text placed before the number, e.g. JZS-")
p.add_argument("--start", type=int, help="First number in the sequence (default 1)")
p.add_argument("--sheets", type=int, help="Number of sheets to generate (default 1)")
p.add_argument("--digits", type=int, help="Zero-padding width for the number (default 4)")
p.add_argument("--output", help="Output PDF path (default <first code>.pdf in the current directory)")
p.add_argument("--offset-x", type=float, default=0.0,
help="Shift every label right by this many inches (negative = left)")
p.add_argument("--offset-y", type=float, default=0.0,
help="Shift every label up by this many inches (negative = down)")
return p.parse_args(argv)
def main(argv=None):
global OFFSET_X, OFFSET_Y
args = parse_args(sys.argv[1:] if argv is None else argv)
OFFSET_X = args.offset_x * inch
OFFSET_Y = args.offset_y * inch
prefix = args.prefix if args.prefix is not None else ask_prefix()
start = args.start if args.start is not None else ask_int("Start number", default=1, min_value=0)
sheets = args.sheets if args.sheets is not None else ask_int("Number of sheets", default=1, min_value=1)
digits = args.digits if args.digits is not None else ask_int("Zero-padding digits", default=4, min_value=1)
if not prefix:
sys.exit("Prefix must not be empty.")
if start < 0 or sheets < 1 or digits < 1:
sys.exit("Start must be >= 0, sheets >= 1, and digits >= 1.")
codes = build_sequence(prefix, start, digits, sheets)
first_code = f"{prefix}{str(start).zfill(digits)}"
output_file = args.output or f"{first_code}.pdf"
create_pdf(output_file, codes)
if __name__ == "__main__":
main()