diff --git a/main.py b/main.py index bf10e71..ae7395c 100644 --- a/main.py +++ b/main.py @@ -1,5 +1,35 @@ -def main(): - print("Hello from custombingo!") +import click +import os +from custom_bingo.card_generator import generate_bingo_cards +from custom_bingo.spreadsheet_reader import read_spreadsheet +from custom_bingo.pdf_generator import export_cards_to_pdf + + +@click.command() +@click.option('--input-file', '-i', required=True, type=click.Path(exists=True), + help='Input spreadsheet file (Excel/CSV) with B, I, N, G, O columns') +@click.option('--output-file', '-o', required=True, type=click.Path(), + help='Output PDF file for the BINGO cards') +@click.option('--number-of-cards', '-n', default=1, type=int, + help='Number of BINGO cards to generate (default: 1)') +def main(input_file, output_file, number_of_cards): + """Generate custom BINGO cards from a spreadsheet.""" + try: + # Read the spreadsheet data + click.echo(f"Reading data from {input_file}...") + data = read_spreadsheet(input_file) + + # Generate the bingo cards + click.echo(f"Generating {number_of_cards} BINGO card(s)...") + cards = generate_bingo_cards(data, number_of_cards) + + # Export to PDF + click.echo(f"Exporting cards to {output_file}...") + export_cards_to_pdf(cards, output_file) + + click.echo(f"Successfully generated {number_of_cards} BINGO card(s) in {output_file}") + except Exception as e: + click.echo(f"Error: {str(e)}", err=True) if __name__ == "__main__": diff --git a/pyproject.toml b/pyproject.toml index 91e95c8..5ae933e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,3 +10,13 @@ dependencies = [ "openpyxl>=3.1.0", "click>=8.0.0" ] + +[project.scripts] +custombingo = "main:main" + +[tool.setuptools.packages.find] +where = ["src"] + +[build-system] +requires = ["setuptools>=45", "wheel"] +build-backend = "setuptools.build_meta" diff --git a/src/custom_bingo/__init__.py b/src/custom_bingo/__init__.py new file mode 100644 index 0000000..f745ded --- /dev/null +++ b/src/custom_bingo/__init__.py @@ -0,0 +1 @@ +# CustomBingo package \ No newline at end of file diff --git a/src/custom_bingo/card_generator.py b/src/custom_bingo/card_generator.py new file mode 100644 index 0000000..bece7bb --- /dev/null +++ b/src/custom_bingo/card_generator.py @@ -0,0 +1,63 @@ +import random +from typing import Dict, List, Tuple + + +def generate_single_card(data: Dict[str, List[str]]) -> List[List[str]]: + """ + Generate a single BINGO card from the provided data. + + Args: + data: Dictionary with keys 'B', 'I', 'N', 'G', 'O' and values as lists of strings + + Returns: + 5x5 matrix representing a BINGO card (list of lists) + """ + card = [] + + # Column B: numbers 1-15 + b_values = random.sample(data['B'], min(len(data['B']), 5)) + card.append(b_values + [''] * max(0, 5 - len(b_values))) + + # Column I: numbers 16-30 + i_values = random.sample(data['I'], min(len(data['I']), 5)) + card.append(i_values + [''] * max(0, 5 - len(i_values))) + + # Column N: numbers 31-45 (with free space in middle) + n_values = random.sample(data['N'], min(len(data['N']), 5)) + # Insert empty string (free space) at position 2 (middle of column) + if len(n_values) >= 3: + n_values.insert(2, "FREE") + else: + n_values.append("FREE") + card.append(n_values + [''] * max(0, 5 - len(n_values))) + + # Column G: numbers 46-60 + g_values = random.sample(data['G'], min(len(data['G']), 5)) + card.append(g_values + [''] * max(0, 5 - len(g_values))) + + # Column O: numbers 61-75 + o_values = random.sample(data['O'], min(len(data['O']), 5)) + card.append(o_values + [''] * max(0, 5 - len(o_values))) + + # Transpose to get rows instead of columns + transposed_card = [[card[col][row] for col in range(5)] for row in range(5)] + + return transposed_card + + +def generate_bingo_cards(data: Dict[str, List[str]], number_of_cards: int) -> List[List[List[str]]]: + """ + Generate multiple BINGO cards from the provided data. + + Args: + data: Dictionary with keys 'B', 'I', 'N', 'G', 'O' and values as lists of strings + number_of_cards: Number of cards to generate + + Returns: + List of 5x5 matrices representing BINGO cards + """ + cards = [] + for _ in range(number_of_cards): + card = generate_single_card(data) + cards.append(card) + return cards \ No newline at end of file diff --git a/src/custom_bingo/pdf_generator.py b/src/custom_bingo/pdf_generator.py new file mode 100644 index 0000000..7476cd6 --- /dev/null +++ b/src/custom_bingo/pdf_generator.py @@ -0,0 +1,131 @@ +from reportlab.lib.pagesizes import letter, A4 +from reportlab.pdfgen import canvas +from reportlab.lib.units import inch +from reportlab.pdfbase import pdfmetrics +from reportlab.pdfbase.ttfonts import TTFont +from typing import List +import math + + +def calculate_font_size(text: str, available_width: float, available_height: float, max_font_size: int = 24) -> int: + """ + Calculate the largest font size that allows the text to fit in the given space. + + Args: + text: Text to fit + available_width: Available width in points + available_height: Available height in points + max_font_size: Maximum font size to consider + + Returns: + Font size that allows the text to fit in the space + """ + if not text: + return max_font_size + + # Start with the maximum font size and decrease until text fits + for font_size in range(max_font_size, 0, -1): + # Estimate text dimensions (this is a rough approximation) + # For more accuracy, we'd need to use the actual font metrics + text_width = len(text) * font_size * 0.6 # rough character width estimate + text_height = font_size + + if text_width <= available_width and text_height <= available_height: + return font_size + + return 1 # smallest possible font size + + +def draw_centered_text(c: canvas.Canvas, text: str, x: float, y: float, width: float, height: float, max_font_size: int = 24): + """ + Draw text centered in a given rectangle with the largest possible font size. + + Args: + c: ReportLab canvas + text: Text to draw + x: X coordinate of bottom-left of rectangle + y: Y coordinate of bottom-left of rectangle + width: Width of rectangle + height: Height of rectangle + max_font_size: Maximum font size to use + """ + if not text: + return + + font_size = calculate_font_size(text, width, height, max_font_size) + c.setFont("Helvetica", font_size) + + # Calculate the text dimensions + text_width = c.stringWidth(text, "Helvetica", font_size) + text_height = font_size + + # Calculate centered position + centered_x = x + (width - text_width) / 2 + # Vertically center the text + # Using font_size * 0.8 as an approximation of text height + centered_y = y + (height - text_height * 0.8) / 2 + text_height * 0.2 + + c.drawString(centered_x, centered_y, text) + + +def export_cards_to_pdf(cards: List[List[List[str]]], output_file: str): + """ + Export the generated BINGO cards to a PDF file. + + Args: + cards: List of 5x5 matrices representing BINGO cards + output_file: Path for the output PDF file + """ + # Create a new PDF document + c = canvas.Canvas(output_file, pagesize=letter) + width, height = letter + + # Define card dimensions + margin = 1 * inch + card_width = (width - 2 * margin) * 0.8 # Make card 80% of page width + card_height = card_width # Keep card square + cell_width = card_width / 5 + cell_height = card_height / 5 + + # Starting position for the first card (centered horizontally) + start_x = (width - card_width) / 2 + start_y = height - margin - card_height + + for idx, card in enumerate(cards): + # Add a new page for each card after the first + if idx > 0: + c.showPage() + start_y = height - margin - card_height + + # Draw title + c.setFont("Helvetica", 24) + title_width = c.stringWidth("BINGO", "Helvetica", 24) + c.drawString((width - title_width) / 2, height - margin + 0.5 * inch, "BINGO") + + # Draw the card grid + for row in range(5): + for col in range(5): + # Calculate the position of this cell + cell_x = start_x + col * cell_width + cell_y = start_y + (4 - row) * cell_height # Flip Y-axis so row 0 is at bottom + + # Draw rectangle for cell + c.rect(cell_x, cell_y, cell_width, cell_height) + + # Get the text for this cell + cell_text = card[row][col] + + # Draw the text centered in the cell + draw_centered_text(c, cell_text, cell_x, cell_y, cell_width, cell_height) + + # Draw column headers (B, I, N, G, O) + headers = ['B', 'I', 'N', 'G', 'O'] + for col, header in enumerate(headers): + cell_x = start_x + col * cell_width + cell_y = start_y + 5 * cell_height # Above the grid + + # Draw header cell (no border, just text) + draw_centered_text(c, header, cell_x, cell_y, cell_width, cell_height, max_font_size=18) + + # Save the PDF + c.save() \ No newline at end of file diff --git a/src/custom_bingo/spreadsheet_reader.py b/src/custom_bingo/spreadsheet_reader.py new file mode 100644 index 0000000..9fbb07c --- /dev/null +++ b/src/custom_bingo/spreadsheet_reader.py @@ -0,0 +1,44 @@ +import pandas as pd +from typing import Dict, List + + +def read_spreadsheet(file_path: str) -> Dict[str, List[str]]: + """ + Read a spreadsheet file and return the data organized by BINGO columns. + + Args: + file_path: Path to the Excel or CSV file + + Returns: + Dictionary with keys 'B', 'I', 'N', 'G', 'O' and values as lists of strings + """ + # Determine file type and read accordingly + if file_path.lower().endswith('.csv'): + df = pd.read_csv(file_path) + elif file_path.lower().endswith(('.xlsx', '.xls')): + df = pd.read_excel(file_path) + else: + raise ValueError("Unsupported file format. Please use CSV or Excel files.") + + # Validate that the dataframe has exactly 5 columns + if len(df.columns) != 5: + raise ValueError(f"Spreadsheet must have exactly 5 columns, but found {len(df.columns)}") + + # Assign column names if they're not already named B, I, N, G, O + if not all(col in ['B', 'I', 'N', 'G', 'O'] for col in df.columns): + df.columns = ['B', 'I', 'N', 'G', 'O'] + else: + # Ensure the order is B, I, N, G, O + df = df[['B', 'I', 'N', 'G', 'O']] + + # Convert to dictionary of lists + data = {} + for col in ['B', 'I', 'N', 'G', 'O']: + # Drop NaN values and convert to list of strings + data[col] = df[col].dropna().astype(str).tolist() + + # Check for empty columns + if len(data[col]) == 0: + raise ValueError(f"Column {col} is empty. Each column must contain at least one value.") + + return data \ No newline at end of file