Skip to content

Commit 168126c

Browse files
committed
[Fixed][BoM Labels] Missing plugin
1 parent 8e1f8d4 commit 168126c

3 files changed

Lines changed: 404 additions & 0 deletions

File tree

kibot/out_bom_labels.py

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
# -*- coding: utf-8 -*-
2+
# Copyright (c) 2025 Salvador E. Tropea
3+
# Copyright (c) 2025 Instituto Nacional de Tecnología Industrial
4+
# License: AGPL-3.0
5+
# Project: KiBot (formerly KiPlot)
6+
# Original code by Stefan Schüller
7+
# https://sschueller.github.io/posts/ci-cd-with-kicad-2025/
8+
"""
9+
Dependencies:
10+
- name: ReportLab
11+
role: Create a PDF with BoM labels
12+
python_module: true
13+
debian: python3-reportlab
14+
arch: python-reportlab
15+
downloader: python
16+
"""
17+
import csv
18+
from reportlab.lib.pagesizes import mm
19+
from reportlab.pdfgen import canvas
20+
from .error import KiPlotConfigurationError
21+
from .gs import GS
22+
from .kiplot import config_output, run_output, look_for_output, get_output_targets, get_columns
23+
from .optionable import Optionable
24+
from .out_base import VariantOptions
25+
from .macros import macros, document, output_class # noqa: F401
26+
from . import log
27+
28+
logger = log.get_logger()
29+
30+
31+
class BoMLabelsOptions(VariantOptions):
32+
def __init__(self):
33+
super().__init__()
34+
with document:
35+
self.output = GS.def_global_output
36+
""" *Name for the generated PDF (%i=bom_labels %x=pdf) """
37+
self.bom = ''
38+
""" *BoM output used for the labels """
39+
self.width = 20
40+
""" Label width in mm """
41+
self.height = 10
42+
""" Label height in mm """
43+
self.margin_x = 2
44+
""" X margin in mm """
45+
self.margin_top = 3
46+
""" Top margin in mm """
47+
self.header_sep = 3
48+
""" Distance from header to first line in mm """
49+
self.line_height = 1.5
50+
""" Regular line height in mm """
51+
self.font = "Helvetica-Bold"
52+
""" Font used for the labels """
53+
self.font_size_header = 6
54+
""" Default size of the header font, will be reduced to fit the text """
55+
self.font_size_rest = 4
56+
""" Default size of the normal font, will be reduced to fit the text """
57+
self.rows = 3
58+
""" How many rows we print, including the header """
59+
super().__init__()
60+
self._expand_ext = 'pdf'
61+
self._expand_id = 'bom_labels'
62+
self._bom_example = 'bom_labels'
63+
64+
def run(self, dir_name):
65+
if not self.bom:
66+
raise KiPlotConfigurationError('You must specify the name of the output that'
67+
' generates the BoM for the labels')
68+
out = look_for_output(self.bom, 'bom', self._parent, {'bom'})
69+
targets, _, _ = get_output_targets(self.bom, self._parent)
70+
config_output(out)
71+
run_output(out)
72+
self.gen_labels(targets[0], dir_name)
73+
74+
def gen_labels(self, ori, dest):
75+
self.ensure_tool('ReportLab')
76+
page_w = self.width * mm
77+
page_h = self.height * mm
78+
margin_x = self.margin_x * mm
79+
margin_top = self.margin_top * mm
80+
first_line = (self.margin_top + self.header_sep) * mm
81+
# Available width for text
82+
max_text_width = page_w - (2 * margin_x)
83+
84+
c = canvas.Canvas(dest, pagesize=(page_w, page_h))
85+
86+
# Read CSV data
87+
with open(ori, 'r') as f:
88+
reader = csv.reader(f)
89+
rows = list(reader)
90+
91+
if len(rows) < 3:
92+
raise KiPlotConfigurationError(f'CSV file has only {len(rows)} rows, but we need at least 3 to skip first 2')
93+
94+
# Skip first 1 row: row 0 (empty) and row 1 (header line)
95+
data_rows = rows[1:]
96+
97+
logger.debug(f"Processing {len(data_rows)} data rows (skipped first 2 rows)")
98+
99+
# Create one page per row
100+
for row_index, row in enumerate(data_rows):
101+
# Start new page (don't need c.showPage() for first page, but will add for consistency)
102+
if row_index > 0:
103+
c.showPage()
104+
105+
# Draw first column as header at top in larger font
106+
if row: # Check if row has at least one column
107+
text = str(row[0])
108+
# Larger font for first column as header
109+
f_size = self.font_size_header
110+
111+
# Dynamic Font Scaling for Header
112+
try:
113+
text_w = c.stringWidth(text, self.font, f_size)
114+
except KeyError:
115+
raise KiPlotConfigurationError(f'Unknown font `{self.font}`')
116+
if text_w > max_text_width:
117+
f_size = f_size * (max_text_width / text_w)
118+
119+
c.setFont(self.font, f_size)
120+
c.drawString(margin_x, page_h - margin_top, text)
121+
122+
# Draw separator line
123+
c.line(margin_x, page_h - margin_top - mm, page_w - margin_x, page_h - margin_top - mm)
124+
125+
# Draw remaining columns below in smaller font
126+
c.setFont(self.font, self.font_size_rest) # Smaller font for other columns
127+
y_position = page_h - first_line # Start below separator
128+
129+
# Start from column 1 (second column)
130+
for col_index in range(1, min(self.rows, len(row))): # Limit to self.rows-1 more columns
131+
text = str(row[col_index])
132+
f_size = self.font_size_rest
133+
134+
# Dynamic Font Scaling for Body
135+
text_w = c.stringWidth(text, self.font, f_size)
136+
if text_w > max_text_width:
137+
f_size = f_size * (max_text_width / text_w)
138+
139+
c.setFont(self.font, f_size)
140+
c.drawString(margin_x, y_position, text)
141+
y_position -= self.line_height * mm
142+
143+
if y_position < mm:
144+
break
145+
146+
c.save()
147+
148+
def get_targets(self, out_dir):
149+
return [self._parent.expand_filename(out_dir, self.output)]
150+
151+
def __str__(self):
152+
txt = f'{self.width}x{self.height} mm, {self.rows} rows, {self.bom}'
153+
return txt
154+
155+
156+
@output_class
157+
class BoM_Labels(BaseOutput): # noqa: F821
158+
""" BoM Labels Printer
159+
Generates a PDF to print labels for the BoM items.
160+
You can find an explanation [here](https://sschueller.github.io/posts/ci-cd-with-kicad-2025/)
161+
You need to create a BoM in CSV format containing the fields to be used.
162+
The first field will be the header, the rest are extra data. """
163+
def __init__(self):
164+
super().__init__()
165+
with document:
166+
self.options = BoMLabelsOptions
167+
""" *[dict={}] Options for the `bom_labels` output """
168+
self._category = ['PCB/docs', 'Schematic/docs']
169+
self._any_related = True
170+
171+
def get_dependencies(self):
172+
files = BaseOutput.get_dependencies(self) # noqa: F821
173+
files.append(self.options.bom)
174+
return files
175+
176+
@staticmethod
177+
def get_conf_examples(name, layers):
178+
if not GS.sch:
179+
return []
180+
field = Optionable.solve_field_name('_field_lcsc_part', empty_when_none=True)
181+
if not field:
182+
(valid_columns, extra_columns) = get_columns()
183+
field = 'digikey#'
184+
if field not in valid_columns:
185+
return []
186+
res = BaseOutput.simple_conf_examples(name, 'BoM labels', 'BoM') # noqa: F821
187+
res[0]['options'] = {'bom': 'bom_labels'}
188+
189+
gb = {}
190+
gb['name'] = 'bom_labels'
191+
gb['comment'] = 'BoM to Print Labels'
192+
gb['type'] = 'bom'
193+
gb['run_by_default'] = False
194+
gb['dir'] = 'BoM'
195+
gb['options'] = {'format': 'CSV', 'output': 'bom_labels.%x', 'group_fields': [field], 'sort_style': 'ref',
196+
'columns': [field, 'Value', 'Footprint'], 'csv': {'hide_pcb_info': True, 'hide_stats_info': True}}
197+
res.append(gb)
198+
199+
return res

tests/GUI/cfg_out/0003.kibot.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,11 @@ outputs:
9191
separator: ','
9292
aggregate:
9393
- file: output.bom.options.dict.aggregate.dict
94+
- name: output.bom_labels
95+
type: bom_labels
96+
comment: Generates a PDF to print labels for the BoM items
97+
options:
98+
output: output.bom_labels.options.dict
9499
- name: output.compress
95100
type: compress
96101
comment: Generates a compressed file containing output files

0 commit comments

Comments
 (0)