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'' # --- 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.")