Spaces:
Running
Running
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.") |