import io import os import pandas as pd import gradio as gr from reportlab.pdfgen import canvas from reportlab.lib.units import mm from reportlab.pdfbase import pdfmetrics from reportlab.pdfbase.ttfonts import TTFont # ── Font setup ──────────────────────────────────────────────────────────────── FONT_BOLD = "Helvetica-Bold" _bold_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "Montserrat-Bold.ttf") if os.path.exists(_bold_path): try: pdfmetrics.registerFont(TTFont("Montserrat-Bold", _bold_path)) FONT_BOLD = "Montserrat-Bold" except Exception: pass # ── PDF ─────────────────────────────────────────────────────────────────────── def build_cfg(pw, ph, lw, lh, ml, gh, vo, fb, fp, fbr, fr): return dict( PAGE_WIDTH=pw*mm, PAGE_HEIGHT=ph*mm, LABEL_WIDTH=lw*mm, LABEL_HEIGHT=lh*mm, MARGIN_LEFT=ml*mm, GAP_HORIZ=gh*mm, VERTICAL_OFFSET=vo*mm, FONT_SIZE_BARCODE=fb, FONT_SIZE_PRICE=fp, FONT_SIZE_BRAND=fbr, FONT_SIZE_REF=fr, ) def generate_pdf_bytes(products: list, cfg: dict) -> io.BytesIO: buf = io.BytesIO() c = canvas.Canvas(buf, pagesize=(cfg["PAGE_WIDTH"], cfg["PAGE_HEIGHT"])) for i, item in enumerate(products): if i > 0 and i % 3 == 0: c.showPage() slot = i % 3 x = cfg["MARGIN_LEFT"] + slot * (cfg["LABEL_WIDTH"] + cfg["GAP_HORIZ"]) cx = x + cfg["LABEL_WIDTH"] / 2 y = cfg["VERTICAL_OFFSET"] lh = cfg["LABEL_HEIGHT"] c.setFont(FONT_BOLD, cfg["FONT_SIZE_BARCODE"]) c.drawCentredString(cx, y + lh - 4.5*mm, str(item.get("barcode", ""))) c.setFont(FONT_BOLD, cfg["FONT_SIZE_PRICE"]) c.drawCentredString(cx, y + lh - 13*mm, str(item.get("price", ""))) c.setFont(FONT_BOLD, cfg["FONT_SIZE_BRAND"]) c.drawCentredString(cx, y + 10*mm, str(item.get("brand", "")).upper()) c.setFont(FONT_BOLD, cfg["FONT_SIZE_REF"]) c.drawCentredString(cx, y + 6*mm, str(item.get("ref", ""))) c.save() buf.seek(0) return buf # ── Camera barcode scanner ──────────────────────────────────────────────────── SCANNER_HTML = """

Point camera at barcode

Starting camera…
""" # ── State helpers ───────────────────────────────────────────────────────────── COLS = ["barcode", "price", "brand", "ref"] EMPTY_DF = pd.DataFrame(columns=COLS) def df_from(products): return pd.DataFrame(products, columns=COLS) if products else EMPTY_DF.copy() def add_product(products, barcode, price, brand, ref): if not (barcode or "").strip(): raise gr.Error("Barcode is required.") products = list(products) + [{ "barcode": barcode.strip(), "price": (price or "").strip(), "brand": (brand or "").strip(), "ref": (ref or "").strip(), }] return products, df_from(products), "", "", "", "" def remove_last(products): products = list(products)[:-1] return products, df_from(products) def clear_all(_): return [], EMPTY_DF.copy() def sync_df(df): if df is None or df.empty: return [] return df.fillna("").to_dict("records") def generate_pdf(products, *cfg_vals): if not products: raise gr.Error("Add at least one product first.") cfg = build_cfg(*cfg_vals) buf = generate_pdf_bytes(products, cfg) path = "/tmp/labels.pdf" with open(path, "wb") as f: f.write(buf.read()) return path # ── UI ──────────────────────────────────────────────────────────────────────── with gr.Blocks(title="Label Printer", theme=gr.themes.Soft()) as demo: state = gr.State([]) gr.Markdown("# 🏷️ Label Printer") gr.Markdown("Add products using the form or camera scanner, then export a print-ready PDF (3-up labels, 110 × 30 mm).") with gr.Row(equal_height=False): with gr.Column(scale=1, min_width=280): gr.Markdown("### ➕ Add product") gr.HTML(SCANNER_HTML) barcode_in = gr.Textbox(label="Barcode", placeholder="Scan or type…", elem_id="barcode_field") price_in = gr.Textbox(label="Price", placeholder="29.99") brand_in = gr.Textbox(label="Brand", placeholder="ACME") ref_in = gr.Textbox(label="Reference", placeholder="REF-001") with gr.Row(): add_btn = gr.Button("➕ Add", variant="primary") undo_btn = gr.Button("↩ Undo last", variant="secondary") clr_btn = gr.Button("🗑 Clear", variant="stop") with gr.Column(scale=2): gr.Markdown("### 📋 Product list") tbl = gr.Dataframe( value=EMPTY_DF.copy(), headers=COLS, datatype=["str"] * 4, col_count=(4, "fixed"), interactive=True, wrap=True, label=None, ) gr.Markdown("Tip: you can edit or delete rows directly in the table.") with gr.Accordion("⚙️ Label layout", open=False): with gr.Row(): pw = gr.Number(value=110.6, label="Page width (mm)", precision=1) ph = gr.Number(value=30, label="Page height (mm)", precision=1) lw = gr.Number(value=27, label="Label width (mm)", precision=1) lh = gr.Number(value=28, label="Label height (mm)", precision=1) with gr.Row(): ml = gr.Number(value=6.0, label="Left margin (mm)", precision=1) gh = gr.Number(value=8.0, label="Horiz gap (mm)", precision=1) vo = gr.Number(value=1.5, label="Vert offset (mm)", precision=1) with gr.Row(): fb = gr.Number(value=8, label="Font: barcode", precision=0) fp = gr.Number(value=16, label="Font: price", precision=0) fbr = gr.Number(value=8, label="Font: brand", precision=0) fr = gr.Number(value=11, label="Font: ref", precision=0) cfg_inputs = [pw, ph, lw, lh, ml, gh, vo, fb, fp, fbr, fr] with gr.Row(): gen_btn = gr.Button("🖨️ Generate PDF", variant="primary", scale=1) pdf_out = gr.File(label="Download PDF", scale=2) # ── Wiring ──────────────────────────────────────────────────────────────── shared = dict( inputs=[state, barcode_in, price_in, brand_in, ref_in], outputs=[state, tbl, barcode_in, price_in, brand_in, ref_in], ) add_btn.click(fn=add_product, **shared) ref_in.submit(fn=add_product, **shared) # Enter on last field adds row undo_btn.click(fn=remove_last, inputs=[state], outputs=[state, tbl]) clr_btn.click( fn=clear_all, inputs=[state], outputs=[state, tbl]) tbl.change( fn=sync_df, inputs=[tbl], outputs=[state]) gen_btn.click(fn=generate_pdf, inputs=[state] + cfg_inputs, outputs=[pdf_out]) if __name__ == "__main__": demo.launch()