BillGenerator / app.py
deepak88's picture
Update app.py
e80f9dc verified
import gradio as gr
from PIL import Image, ImageDraw, ImageFont, ImageFilter
import random
import datetime
import io
import os
import tempfile # Needed for creating temporary files
from gradio_calendar import Calendar
# --- Image Generation Function (Adapted for Gradio) ---
def generate_fuel_bill_image(
company_name: str,
company_address: str,
attendant_id: str,
vehicle_number: str,
mobile_number: str,
fuel_point_id: str,
nozzle_number: str,
fuel_type: str,
product: str,
density: str,
preset_type: str,
rate: float,
amount_option: str, # "random" or "manual"
manual_sale_amount: float,
min_rand_amount: float,
max_rand_amount: float,
min_bill_date: datetime.date, # New parameter for min date
max_bill_date: datetime.date, # New parameter for max date
image_width: int,
image_height: int,
font_size_input: int,
line_spacing_input: int,
margin_left_input: int,
logo_y_offset_input: int,
texture_opacity_input: float,
blur_radius_input: float,
selected_crumple_file_path: str, # path from gr.Dropdown or None
custom_crumple_file_obj, # tempfile object from gr.File
selected_logo_file_path: str, # path from gr.Dropdown or None
custom_logo_file_obj # tempfile object from gr.File
) -> tuple[Image.Image, gr.File]: # Now returns a tuple: (PIL Image, Gradio File)
# Determine sale_amount based on selected option
sale_amount = manual_sale_amount
if amount_option == "Random":
sale_amount = round(random.uniform(min_rand_amount, max_rand_amount), 2)
# --- Helper Functions (from your original code) ---
def generate_bill_number():
return f"{random.randint(100000, 999999)}-ORGNL"
def generate_transaction_id():
return f"{random.randint(1000000000000000, 9999999999999999):016d}"
def generate_random_datetime_between_dates(start_date: datetime.date, end_date: datetime.date):
# Ensure start_date is not after end_date
if start_date > end_date:
start_date, end_date = end_date, start_date # Swap if invalid
time_between_dates = end_date - start_date
days_between_dates = time_between_dates.days
if days_between_dates < 0: # Should not happen with swap, but for robustness
random_number_of_days = 0
else:
random_number_of_days = random.randrange(days_between_dates + 1)
random_date = start_date + datetime.timedelta(days=random_number_of_days)
random_hour = random.randint(8, 23)
random_minute = random.randint(0, 59)
random_second = random.randint(0, 59)
random_time = datetime.time(random_hour, random_minute, random_second)
return random_date.strftime("%d/%m/%Y"), random_time.strftime("%H:%M:%S")
def calculate_volume(rate_val, sale_amount_val):
try:
return round(float(sale_amount_val) / float(rate_val), 2)
except ZeroDivisionError:
return 0.0
# --- Core Generation Logic ---
bill_number = generate_bill_number()
transaction_id = generate_transaction_id()
date, time = generate_random_datetime_between_dates(min_bill_date, max_bill_date)
volume = calculate_volume(rate, sale_amount)
# Use the passed input values for image dimensions and aesthetics
width = image_width
height = image_height
font_size = font_size_input
line_spacing = line_spacing_input
margin_left = margin_left_input
logo_y_offset = logo_y_offset_input
texture_opacity = texture_opacity_input
blur_radius = blur_radius_input
bg_color = (245, 245, 235)
text_color = (0, 0, 0)
# Define paths for assets (assuming they are in the same directory as app.py)
# Gradio will handle making these accessible
script_dir = os.path.dirname(__file__)
font_file = os.path.join(script_dir, "cour.ttf")
data = [
f"B111 No:{bill_number}",
f"Trns. ID:{transaction_id}",
f"Atnd. ID:{attendant_id if attendant_id else ''}",
f"Vehicle No:{vehicle_number}",
f"Mobile No: {mobile_number}",
f"Date :{date}",
f"Time :{time}",
f"FIP. ID :{fuel_point_id}",
f"Nozzle No:{nozzle_number}",
f"Fuel : {fuel_type}",
f"Prouct : {product}",
f"Density: {density}",
f"Preset Type : {preset_type}",
f"Rate :Rs. {rate:.2f}",
f"Sale :Rs.{sale_amount:.2f}",
f"Volume :{volume:.2f}",
"THANK YOU!",
"PLEASE VISIT AGAIN"
]
# --- Crumple Texture ---
crumple = None
if custom_crumple_file_obj: # Custom uploaded file takes precedence
try:
crumple = Image.open(custom_crumple_file_obj.name).convert("RGB")
crumple = crumple.resize((width, height))
except Exception as e:
gr.Warning(f"Could not load custom background texture. Error: {e}. Using plain background.")
elif selected_crumple_file_path and selected_crumple_file_path != "None": # Selected default texture
try:
crumple = Image.open(selected_crumple_file_path).convert("RGB")
crumple = crumple.resize((width, height))
except FileNotFoundError:
gr.Warning(f"Selected default texture '{selected_crumple_file_path}' not found. Using plain background.")
except Exception as e:
gr.Warning(f"Error loading selected default texture: {e}. Using plain background.")
# else: crumple remains None, leading to a plain background
# --- Create base image ---
base_image = Image.new("RGB", (width, height), bg_color)
if crumple:
base_image = Image.blend(base_image, crumple, texture_opacity)
# --- Create a layer for the text and logo (transparent background) ---
text_layer = Image.new("RGBA", (width, height), (0, 0, 0, 0))
draw = ImageDraw.Draw(text_layer)
# --- Load font ---
try:
font = ImageFont.truetype(font_file, size=font_size)
except IOError:
gr.Warning(f"Font file '{font_file}' not found. Using default font.")
font = ImageFont.load_default()
# --- Load Logo ---
logo_height = 0
logo_max_width = width - (margin_left * 2) # Ensure logo fits within margins
if custom_logo_file_obj: # Custom uploaded logo takes precedence
try:
logo = Image.open(custom_logo_file_obj.name)
if logo.mode != 'RGBA':
logo = logo.convert("RGBA")
if logo.width > logo_max_width:
ratio = logo_max_width / logo.width
logo = logo.resize((int(logo.width * ratio), int(logo.height * ratio)))
logo_x = (width - logo.width) // 2
text_layer.paste(logo, (logo_x, logo_y_offset), logo)
logo_height = logo.height
except Exception as e:
gr.Warning(f"Could not load custom logo. Error: {e}. Proceeding without logo.")
elif selected_logo_file_path and selected_logo_file_path != "None": # Selected default logo
try:
logo = Image.open(selected_logo_file_path)
if logo.mode != 'RGBA':
logo = logo.convert("RGBA")
if logo.width > logo_max_width:
ratio = logo_max_width / logo.width
logo = logo.resize((int(logo.width * ratio), int(logo.height * ratio)))
logo_x = (width - logo.width) // 2
text_layer.paste(logo, (logo_x, logo_y_offset), logo)
logo_height = logo.height
except FileNotFoundError:
gr.Warning(f"Selected default logo '{selected_logo_file_path}' not found. Generating without logo.")
except Exception as e:
gr.Warning(f"Error loading selected default logo: {e}. Proceeding without logo.")
# --- Company Name and Address (Centered) ---
y_offset = logo_y_offset + logo_height + 20
company_address_lines = company_address.split('\n')
for line in company_address_lines:
company_name_width = draw.textlength(line, font=font)
draw.text(((width - company_name_width) // 2, y_offset), line, font=font, fill=text_color)
y_offset += line_spacing
y_offset += 20
# --- Transaction Data (Left-Aligned) ---
for line in data:
x_offset = margin_left
y_offset += line_spacing
draw.text((x_offset, y_offset), line, font=font, fill=text_color)
final_image = Image.alpha_composite(base_image.convert("RGBA"), text_layer)
final_image = final_image.filter(ImageFilter.GaussianBlur(radius=blur_radius)).convert("RGB")
# --- Save image to a temporary file for download ---
# Create a temporary file and save the image to it
temp_dir = tempfile.mkdtemp()
bill_filename = f"fuel_bill_scan_{random.randint(1000, 9999)}.jpg"
temp_filepath = os.path.join(temp_dir, bill_filename)
final_image.save(temp_filepath, format='JPEG', quality=85)
# Return the PIL image for display and the temporary file path for download
return final_image, gr.File(temp_filepath, type="filepath", label="Download Generated Bill")
# --- Gradio Interface Layout ---
# Helper to get available default assets for Gradio dropdowns
def get_default_assets(extensions):
current_dir = os.path.dirname(__file__)
files = []
if os.path.exists(current_dir):
for f in os.listdir(current_dir):
if any(f.lower().endswith(ext) for ext in extensions):
files.append(os.path.join(current_dir, f)) # Return full path
return ["None"] + files
# Get paths to default textures and logos
default_texture_paths = get_default_assets([".jpg", ".jpeg", ".png"])
default_logo_paths = get_default_assets([".png", ".jpg", ".jpeg"])
with gr.Blocks(title="Fuel Bill Image Generator") as demo:
gr.Markdown(
"""
# ⛽ Custom Fuel Bill Image Generator
Create realistic-looking fuel bill images with customizable details,
background textures, and your own logo!
"""
)
with gr.Row():
with gr.Column(scale=1):
gr.Markdown("## ⚙️ Bill Details Customization")
gr.Markdown("### Company Information")
company_name_input = gr.Textbox(label="Company Name:", value="IndianOil")
company_address_input = gr.Textbox(
label="Company Address:",
value="Welcomes You\nM/S AAKASH BROTHERS\nVILL WAZIRABAD SEC 52\nGURUGRAM HR 122003\nTel . No.: 9311559777",
lines=5
)
gr.Markdown("### Transaction Information")
with gr.Row():
attendant_id_input = gr.Textbox(label="Attendant ID:", value="")
vehicle_number_input = gr.Textbox(label="Vehicle Number:", value="Not Entered")
with gr.Row():
mobile_number_input = gr.Textbox(label="Mobile Number:", value="Not Entered")
fuel_point_id_input = gr.Textbox(label="Fuel Point ID:", value="2")
with gr.Row():
nozzle_number_input = gr.Textbox(label="Nozzle Number:", value="2")
fuel_type_input = gr.Textbox(label="Fuel Type:", value="PETROL")
with gr.Row():
product_input = gr.Textbox(label="Product:", value="XP95")
density_input = gr.Textbox(label="Density:", value="778.1kg/m3")
preset_type_input = gr.Textbox(label="Preset Type:", value="Volume")
gr.Markdown("### Amount Details")
rate_input = gr.Number(label="Rate (Rs.):", value=102.07, step=0.01)
amount_option = gr.Radio(
choices=["Random", "Manual"],
value="Random",
label="Sale Amount Option:",
interactive=True
)
with gr.Column(visible=True) as random_amount_col:
min_rand_amount = gr.Number(label="Min Random Amount", value=2000.0, step=100.0)
max_rand_amount = gr.Number(label="Max Random Amount", value=5000.0, step=100.0)
with gr.Column(visible=True) as manual_amount_col:
manual_sale_amount = gr.Number(label="Manual Sale Amount (Rs.):", value=2500.00, step=0.01)
# Link visibility of columns to radio button
amount_option.change(
lambda value: (gr.Column.update(visible=value == "Random"), gr.Column.update(visible=value == "Manual")),
inputs=amount_option,
outputs=[random_amount_col, manual_amount_col]
)
gr.Markdown("### Bill Date Range")
with gr.Row():
min_bill_date_input = Calendar(label="Min Bill Date", value="2024-01-01")
max_bill_date_input = Calendar(label="Max Bill Date", value=datetime.date.today().strftime("%Y-%m-%d"))
gr.Markdown("## 🖼️ Background & Logo Selection")
gr.Markdown("### Background Texture")
selected_texture = gr.Dropdown(
label="Select a default texture:",
choices=default_texture_paths,
value=next((path for path in default_texture_paths if "scan_texture.jpg" in path), "None") # Pre-select if default exists
)
custom_crumple_file = gr.File(label="Or upload a custom background texture (PNG, JPG, JPEG)", type="filepath")
gr.Markdown("### Logo")
selected_logo = gr.Dropdown(
label="Select a default logo:",
choices=default_logo_paths,
value=next((path for path in default_logo_paths if "indian_oil_logo.png" in path), "None") # Pre-select if default exists
)
custom_logo_file = gr.File(label="Or upload a custom logo (PNG, JPG, JPEG)", type="filepath")
with gr.Column(scale=1):
gr.Markdown("## Image Aesthetics")
image_width = gr.Slider(label="Image Width", minimum=200, maximum=400, value=250, step=10)
image_height = gr.Slider(label="Image Height", minimum=500, maximum=900, value=700, step=10)
font_size_input = gr.Slider(label="Font Size", minimum=10, maximum=20, value=14, step=1)
line_spacing_input = gr.Slider(label="Line Spacing", minimum=15, maximum=25, value=18, step=1)
margin_left_input = gr.Slider(label="Left Margin", minimum=10, maximum=50, value=20, step=1)
logo_y_offset_input = gr.Slider(label="Logo Y-offset", minimum=0, maximum=100, value=20, step=5)
texture_opacity_input = gr.Slider(label="Texture Opacity", minimum=0.0, maximum=1.0, value=1.0, step=0.05)
blur_radius_input = gr.Slider(label="Blur Radius", minimum=0.0, maximum=1.0, value=0.2, step=0.05)
generate_button = gr.Button("Generate Fuel Bill Image")
gr.Markdown("## Generated Image:")
output_image = gr.Image(label="Your Custom Fuel Bill Image", type="pil")
download_file_output = gr.File(label="Download Bill Image", interactive=False) # New output for download
generate_button.click(
fn=generate_fuel_bill_image,
inputs=[
company_name_input,
company_address_input,
attendant_id_input,
vehicle_number_input,
mobile_number_input,
fuel_point_id_input,
nozzle_number_input,
fuel_type_input,
product_input,
density_input,
preset_type_input,
rate_input,
amount_option,
manual_sale_amount,
min_rand_amount,
max_rand_amount,
min_bill_date_input,
max_bill_date_input,
image_width,
image_height,
font_size_input,
line_spacing_input,
margin_left_input,
logo_y_offset_input,
texture_opacity_input,
blur_radius_input,
selected_texture,
custom_crumple_file,
selected_logo,
custom_logo_file
],
outputs=[output_image, download_file_output] # Now outputs both the image and the downloadable file
)
demo.launch()