LED-print / app.py
Ethscriptions's picture
Update app.py
1b1b918 verified
raw
history blame
10.2 kB
import pandas as pd
import streamlit as st
import matplotlib.pyplot as plt
import matplotlib.font_manager as font_manager
import io
import base64
import os
from datetime import datetime, timedelta
import math
from pypinyin import lazy_pinyin, Style
from matplotlib.backends.backend_pdf import PdfPages
def get_font(size=14):
"""Loads a specific font file, falling back to a default if not found."""
font_path = "simHei.ttc"
if not os.path.exists(font_path):
font_path = "SimHei.ttf" # Fallback font
return font_manager.FontProperties(fname=font_path, size=size)
def get_pinyin_abbr(text):
"""Gets the first letter of the Pinyin for the first two Chinese characters of a text."""
if not text:
return ""
# Extract the first two Chinese characters
chars = [c for c in text if '\u4e00' <= c <= '\u9fff']
chars = chars[:2]
# Get the first letter of the Pinyin
pinyin_list = lazy_pinyin(chars, style=Style.FIRST_LETTER)
return ''.join(pinyin_list).upper()
def process_schedule(file):
"""Processes the uploaded Excel file to extract and clean movie schedule data."""
try:
# Try to read the date from a specific cell
date_df = pd.read_excel(file, header=None, skiprows=7, nrows=1, usecols=[3])
date_str = pd.to_datetime(date_df.iloc[0, 0]).strftime('%Y-%m-%d')
base_date = pd.to_datetime(date_str).date()
except Exception:
# Fallback to today's date if reading fails
date_str = datetime.today().strftime('%Y-%m-%d')
base_date = datetime.today().date()
try:
# Read the main schedule data
df = pd.read_excel(file, header=9, usecols=[1, 2, 4, 5])
df.columns = ['Hall', 'StartTime', 'EndTime', 'Movie']
# Clean and process the data
df['Hall'] = df['Hall'].ffill()
df.dropna(subset=['StartTime', 'EndTime', 'Movie'], inplace=True)
df['Hall'] = df['Hall'].astype(str).str.extract(r'(\d+号)')
# Convert times to datetime objects
df['StartTime_dt'] = pd.to_datetime(df['StartTime'], format='%H:%M', errors='coerce').apply(
lambda t: t.replace(year=base_date.year, month=base_date.month, day=base_date.day) if pd.notnull(t) else t
)
df['EndTime_dt'] = pd.to_datetime(df['EndTime'], format='%H:%M', errors='coerce').apply(
lambda t: t.replace(year=base_date.year, month=base_date.month, day=base_date.day) if pd.notnull(t) else t
)
# Handle overnight screenings
df.loc[df['EndTime_dt'] < df['StartTime_dt'], 'EndTime_dt'] += timedelta(days=1)
df = df.sort_values(['Hall', 'StartTime_dt'])
# Merge consecutive screenings of the same movie
merged_rows = []
for hall, group in df.groupby('Hall'):
group = group.sort_values('StartTime_dt')
current = None
for _, row in group.iterrows():
if current is None:
current = row.copy()
else:
if row['Movie'] == current['Movie']:
current['EndTime_dt'] = row['EndTime_dt'] # Extend the end time
else:
merged_rows.append(current)
current = row.copy()
if current is not None:
merged_rows.append(current)
merged_df = pd.DataFrame(merged_rows)
# Adjust start and end times
merged_df['StartTime_dt'] = merged_df['StartTime_dt'] - timedelta(minutes=10)
merged_df['EndTime_dt'] = merged_df['EndTime_dt'] - timedelta(minutes=5)
merged_df['StartTime_str'] = merged_df['StartTime_dt'].dt.strftime('%H:%M')
merged_df['EndTime_str'] = merged_df['EndTime_dt'].dt.strftime('%H:%M')
return merged_df[['Hall', 'Movie', 'StartTime_str', 'EndTime_str']], date_str
except Exception as e:
st.error(f"An error occurred during data processing: {e}")
return None, date_str
def create_print_layout(data, date_str):
"""Creates PNG and PDF layouts for the schedule, designed to fill an A4 page."""
if data is None or data.empty:
return None
# --- Figure setup for both PNG and PDF ---
# We create two figures to handle potential differences in rendering pipelines
png_fig = plt.figure(figsize=(8.27, 11.69), dpi=300)
png_ax = png_fig.add_subplot(111)
png_ax.set_axis_off()
png_fig.subplots_adjust(left=0.05, right=0.95, top=0.95, bottom=0.05)
pdf_fig = plt.figure(figsize=(8.27, 11.69), dpi=300)
pdf_ax = pdf_fig.add_subplot(111)
pdf_ax.set_axis_off()
pdf_fig.subplots_adjust(left=0.05, right=0.95, top=0.95, bottom=0.05)
def process_figure(fig, ax):
"""The core logic to draw the schedule on a given matplotlib axis."""
# --- Dynamic Calculation for Layout ---
halls = sorted(data['Hall'].unique(), key=lambda h: int(h.replace('号','')))
num_movies = len(data)
num_halls = len(halls)
# Total slots = one for each movie + one for each separator + 2 for padding
totalA = num_movies + (num_halls - 1) + 2
available_height = 0.95 # Usable portion of the page
line_height = available_height / totalA
# --- Dynamic Font Size Calculation ---
# Base font size is proportional to the line height to ensure it fits
# The factor (e.g., 60) is an empirical value that provides a good look
dynamic_font_size = line_height * 60
main_font = get_font(dynamic_font_size)
hall_font = get_font(dynamic_font_size * 1.2) # Make hall font slightly larger
date_font = get_font(12)
# Draw the date at the top
ax.text(0.01, 0.98, date_str, ha='left', va='top', fontproperties=date_font, color='#A9A9A9', transform=ax.transAxes)
y_position = 0.96 # Starting y-position from the top
for i, hall in enumerate(halls):
hall_data = data[data['Hall'] == hall]
hall_num_text = f"{hall.replace('号', '')}"
movie_count = 1
# Draw Hall Number (only once per hall block)
ax.text(0.03, y_position - (line_height / 2), hall_num_text,
fontsize=dynamic_font_size * 1.5, fontweight='bold', ha='center', va='center',
fontproperties=hall_font, transform=ax.transAxes)
for _, row in hall_data.iterrows():
pinyin_abbr = get_pinyin_abbr(row['Movie'])
# --- Content Layout per line ---
# Column positions are defined as fractions of the page width
x_movie_name = 0.48
x_movie_num = 0.1
x_pinyin = 0.18
x_time = 0.82
# 1. Movie Name (Right-aligned in its zone)
ax.text(x_movie_name, y_position, row['Movie'],
ha='right', va='center', fontproperties=main_font, transform=ax.transAxes)
# 2. Sequence Number (Left-aligned)
ax.text(x_movie_num, y_position, f"{movie_count}.",
ha='left', va='center', fontproperties=main_font, transform=ax.transAxes)
# 3. Pinyin Abbreviation (Left-aligned)
ax.text(x_pinyin, y_position, pinyin_abbr,
ha='left', va='center', fontproperties=main_font, transform=ax.transAxes)
# 4. Time (Left-aligned)
time_str = f"{row['StartTime_str']} - {row['EndTime_str']}"
ax.text(x_time, y_position, time_str,
ha='left', va='center', fontproperties=main_font, transform=ax.transAxes)
y_position -= line_height
movie_count += 1
# Add a separator line after each hall block, except for the last one
if i < num_halls - 1:
y_position -= (line_height / 2) # small gap for the line
ax.axhline(y=y_position, xmin=0.02, xmax=0.98, color='black', linewidth=0.8)
y_position -= (line_height / 2) # gap after the line
# --- Generate Outputs ---
# Process both figures with the same logic
process_figure(png_fig, png_ax)
process_figure(pdf_fig, pdf_ax)
# Save PNG to a buffer
png_buffer = io.BytesIO()
png_fig.savefig(png_buffer, format='png', bbox_inches='tight', pad_inches=0.05)
png_buffer.seek(0)
image_base64 = base64.b64encode(png_buffer.getvalue()).decode()
plt.close(png_fig)
# Save PDF to a buffer
pdf_buffer = io.BytesIO()
pdf_fig.savefig(pdf_buffer, format='pdf', bbox_inches='tight', pad_inches=0.05)
pdf_buffer.seek(0)
pdf_base64 = base64.b64encode(pdf_buffer.getvalue()).decode()
plt.close(pdf_fig)
return {
'png': f"data:image/png;base64,{image_base64}",
'pdf': f"data:application/pdf;base64,{pdf_base64}"
}
def display_pdf(base64_pdf):
"""Generates the HTML to embed a PDF in Streamlit."""
return f'<iframe src="{base64_pdf}" width="100%" height="800" type="application/pdf"></iframe>'
# --- Streamlit App Main ---
st.set_page_config(page_title="LED Screen Schedule Printer", layout="wide")
st.title("LED Screen Schedule Printer")
uploaded_file = st.file_uploader("Select '放映时间核对表.xls' file", type=["xls"])
if uploaded_file:
with st.spinner("Processing file, please wait..."):
schedule, date_str = process_schedule(uploaded_file)
if schedule is not None:
output = create_print_layout(schedule, date_str)
# Use tabs for PDF and PNG previews
tab1, tab2 = st.tabs(["PDF Preview", "PNG Preview"])
with tab1:
st.markdown(display_pdf(output['pdf']), unsafe_allow_html=True)
with tab2:
st.image(output['png'], use_container_width=True)
else:
st.error("Could not process the file. Please check if the file format and content are correct.")