LED-print / app.py
Ethscriptions's picture
Update app.py
ce8fae8 verified
raw
history blame
12.6 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 matplotlib.patches import FancyBboxPatch, Rectangle
from pypinyin import lazy_pinyin, Style
from matplotlib.backends.backend_pdf import PdfPages
def get_font(size=14):
"""Loads the specified font file, falling back to a default."""
font_path = "simHei.ttc"
if not os.path.exists(font_path):
font_path = "SimHei.ttf"
if not os.path.exists(font_path):
# As a last resort, return default font if SimHei is not found
st.warning("Font file 'simHei.ttc' or 'SimHei.ttf' not found. Text may not render correctly.")
return font_manager.FontProperties(size=size)
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."""
if not text:
return ""
chars = [c for c in text if '\u4e00' <= c <= '\u9fff'][:2]
if not chars:
return ""
return ''.join(lazy_pinyin(chars, style=Style.FIRST_LETTER)).upper()
def process_schedule(file):
"""Reads and processes the movie schedule from the uploaded Excel file."""
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
base_date = datetime.today().date()
date_str = base_date.strftime('%Y-%m-%d')
try:
df = pd.read_excel(file, header=9, usecols=[1, 2, 4, 5])
df.columns = ['Hall', 'StartTime', 'EndTime', 'Movie']
df['Hall'] = df['Hall'].ffill()
df.dropna(subset=['StartTime', 'EndTime', 'Movie'], inplace=True)
df['Hall'] = df['Hall'].astype(str).str.extract(r'(\d+号)')
df.dropna(subset=['Hall'], inplace=True)
# 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 shows of the same movie
merged_rows = []
for hall, group in df.groupby('Hall'):
current = None
for _, row in group.sort_values('StartTime_dt').iterrows():
if current is None:
current = row.copy()
elif row['Movie'] == current['Movie']:
current['EndTime_dt'] = row['EndTime_dt']
else:
merged_rows.append(current)
current = row.copy()
if current is not None:
merged_rows.append(current)
if not merged_rows:
return None, date_str
merged_df = pd.DataFrame(merged_rows)
# Adjust times as per original logic
merged_df['StartTime_dt'] -= timedelta(minutes=10)
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"Error processing the file: {e}")
return None, date_str
def get_text_dimensions(text, font_props, fig):
"""Helper function to calculate the width and height of a text string in inches."""
renderer = fig.canvas.get_renderer()
t = fig.text(0, 0, text, fontproperties=font_props, visible=False)
bbox = t.get_window_extent(renderer=renderer)
t.remove()
return bbox.width / fig.dpi, bbox.height / fig.dpi
def find_font_size(target_height_inches, font_path, fig, text_to_measure="Xg"):
"""Finds the font size (in points) that results in a given text height (in inches)."""
low, high = 0, 100 # Binary search range for font size
best_size = 10
for _ in range(10): # 10 iterations for precision
mid = (low + high) / 2
if mid <= 0: break
props = font_manager.FontProperties(fname=font_path, size=mid)
_, h = get_text_dimensions(text_to_measure, props, fig)
if h > target_height_inches:
high = mid
else:
best_size = mid
low = mid
return best_size
def create_print_layout(data, date_str):
"""Creates the PNG and PDF print layouts based on the new grid requirements."""
if data is None or data.empty:
return None
A4_WIDTH_IN, A4_HEIGHT_IN = 8.27, 11.69
# 1. Layout Calculation
total_A = len(data) + 2 # Total rows + top/bottom margin
row_height = A4_HEIGHT_IN / total_A
side_margin_width = row_height # Left/right blank columns width
content_width = A4_WIDTH_IN - 2 * side_margin_width
target_font_height = row_height * 0.9
# --- Calculate font sizes and column widths ---
dummy_fig = plt.figure()
font_path = "simHei.ttc" if os.path.exists("simHei.ttc") else "SimHei.ttf"
master_font_size = find_font_size(target_font_height, font_path, dummy_fig)
master_font = get_font(size=master_font_size)
# Prepare content to measure for max width
data['Pinyin'] = data['Movie'].apply(get_pinyin_abbr)
data['TimeStr'] = data['StartTime_str'] + ' - ' + data['EndTime_str']
max_hall_content = max(data['Hall'].str.replace('号', ''), key=len) + "#"
max_seq_content = f"{data.groupby('Hall').size().max()}."
max_pinyin_content = max(data['Pinyin'], key=len) if not data['Pinyin'].empty else "PY"
max_time_content = max(data['TimeStr'], key=len)
w_hall, _ = get_text_dimensions(max_hall_content, master_font, dummy_fig)
w_seq, _ = get_text_dimensions(max_seq_content, master_font, dummy_fig)
w_pinyin, _ = get_text_dimensions(max_pinyin_content, master_font, dummy_fig)
w_time, _ = get_text_dimensions(max_time_content, master_font, dummy_fig)
plt.close(dummy_fig)
col_width_hall = w_hall * 1.1
col_width_seq = w_seq * 1.1
col_width_pinyin = w_pinyin * 1.1
col_width_time = w_time * 1.1
col_width_movie = content_width - (col_width_hall + col_width_seq + col_width_pinyin + col_width_time)
if col_width_movie <= 0:
st.error("Content is too wide for the page. The movie title column has no space.")
return None
# --- Create figures for drawing ---
output_figs = {}
for fmt in ['png', 'pdf']:
fig = plt.figure(figsize=(A4_WIDTH_IN, A4_HEIGHT_IN), dpi=300)
ax = fig.add_subplot(111)
ax.set_axis_off()
fig.subplots_adjust(left=0, right=1, top=1, bottom=0)
ax.set_xlim(0, A4_WIDTH_IN)
ax.set_ylim(0, A4_HEIGHT_IN)
# Define column x-positions (from left to right)
x_pos = [0] * 7
x_pos[0] = 0
x_pos[1] = side_margin_width
x_pos[2] = x_pos[1] + col_width_hall
x_pos[3] = x_pos[2] + col_width_seq
x_pos[4] = x_pos[3] + col_width_movie
x_pos[5] = x_pos[4] + col_width_pinyin
x_pos[6] = x_pos[5] + col_width_time
# Add date string to the top margin area
ax.text(A4_WIDTH_IN - side_margin_width, A4_HEIGHT_IN - (row_height / 2), date_str,
ha='right', va='center', color='#A9A9A9', fontproperties=get_font(12))
# --- Drawing Loop ---
y_cursor = A4_HEIGHT_IN - row_height # Start below top margin
drawn_halls = set()
for hall_name, group in data.groupby('Hall', sort=False):
for i, (_, row) in enumerate(group.iterrows()):
y_bottom = y_cursor - row_height
y_center = y_cursor - (row_height / 2)
# Draw dotted cell borders
for c in range(5):
ax.add_patch(Rectangle((x_pos[c+1], y_bottom), x_pos[c+2] - x_pos[c+1], row_height,
facecolor='none', edgecolor='gray', linestyle=':', linewidth=0.5))
# Column 1: Hall Number (only on first row of the group)
if hall_name not in drawn_halls:
hall_num_text = f"${row['Hall'].replace('号', '')}^{{\\#}}$"
ax.text(x_pos[1] + col_width_hall / 2, y_center, hall_num_text,
ha='center', va='center', fontproperties=master_font)
drawn_halls.add(hall_name)
# Column 2: Sequence Number
ax.text(x_pos[2] + col_width_seq / 2, y_center, f"{i+1}.",
ha='center', va='center', fontproperties=master_font)
# Column 3: Movie Title (with font scaling)
font_movie = master_font.copy()
movie_cell_inner_width = col_width_movie * 0.9
temp_fig = plt.figure()
w_movie, _ = get_text_dimensions(row['Movie'], font_movie, temp_fig)
plt.close(temp_fig)
if w_movie > movie_cell_inner_width:
scale_factor = movie_cell_inner_width / w_movie
font_movie.set_size(font_movie.get_size() * scale_factor)
ax.text(x_pos[3] + col_width_movie * 0.05, y_center, row['Movie'],
ha='left', va='center', fontproperties=font_movie)
# Column 4: Pinyin Abbreviation
ax.text(x_pos[4] + col_width_pinyin / 2, y_center, row['Pinyin'],
ha='center', va='center', fontproperties=master_font)
# Column 5: Time
ax.text(x_pos[5] + col_width_time / 2, y_center, row['TimeStr'],
ha='center', va='center', fontproperties=master_font)
y_cursor -= row_height
# Draw black separator line below each hall group
ax.plot([x_pos[1], x_pos[6]], [y_cursor, y_cursor], color='black', linewidth=1.0)
output_figs[fmt] = fig
# --- Save figures to in-memory buffers ---
png_buffer = io.BytesIO()
output_figs['png'].savefig(png_buffer, format='png', bbox_inches=None, pad_inches=0)
plt.close(output_figs['png'])
pdf_buffer = io.BytesIO()
output_figs['pdf'].savefig(pdf_buffer, format='pdf', bbox_inches=None, pad_inches=0)
plt.close(output_figs['pdf'])
# Encode for display
png_buffer.seek(0)
image_base64 = base64.b64encode(png_buffer.getvalue()).decode()
pdf_buffer.seek(0)
pdf_base64 = base64.b64encode(pdf_buffer.getvalue()).decode()
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 ---
st.set_page_config(page_title="LED Screen Schedule Printing", layout="wide")
st.title("LED Screen Schedule Printing")
uploaded_file = st.file_uploader("Select the 'Screening Time Checklist.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 and not schedule.empty:
output = create_print_layout(schedule, date_str)
if output:
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 that the file format and content are correct.")