import pandas as pd import streamlit as st from datetime import datetime, timedelta import matplotlib.pyplot as plt import io import base64 import matplotlib.gridspec as gridspec import matplotlib.font_manager as fm import math # --- CONSTANTS --- SPLIT_TIME = "17:30" BUSINESS_START = "09:30" BUSINESS_END = "01:30" BORDER_COLOR = '#A9A9A9' DATE_COLOR = '#A9A9A9' A5_WIDTH_IN = 5.83 A5_HEIGHT_IN = 8.27 DPI = 300 NUM_COLS = 3 # --- FONT SETUP --- # Attempt to load the specified font, with a safe fallback. try: # IMPORTANT: Place 'SimHei.ttf' in the same directory as the script. FONT_PROP = fm.FontProperties(fname='SimHei.ttf') except FileNotFoundError: st.warning("SimHei.ttf not found. Using a default sans-serif font. Chinese characters may not display correctly.") FONT_PROP = fm.FontProperties(family='sans-serif') def process_schedule(file): """处理上传的 Excel 文件,生成排序和分组后的打印内容""" try: # 读取 Excel,跳过前 8 行 df = pd.read_excel(file, skiprows=8) # 提取所需列 (G9, H9, J9) df = df.iloc[:, [6, 7, 9]] # G, H, J 列 df.columns = ['Hall', 'StartTime', 'EndTime'] # 清理数据 df = df.dropna(subset=['Hall', 'StartTime', 'EndTime']) df = df[df['Hall'].astype(str).str.contains(r'\d', na=False)] # Ensure Hall has a digit # 转换影厅格式为 "#号" 格式 df['Hall'] = df['Hall'].astype(str).str.extract(r'(\d+)').astype(str) + ' ' # 转换时间为 datetime 对象 base_date = datetime.today().date() df['StartTime'] = pd.to_datetime(df['StartTime'], errors='coerce').dt.time df['EndTime'] = pd.to_datetime(df['EndTime'], errors='coerce').dt.time df = df.dropna(subset=['StartTime', 'EndTime']) def combine_date_time(t): dt = datetime.combine(base_date, t) # Handle overnight shows (times past midnight like 1:30 AM are for the next day) if t.hour < int(BUSINESS_START.split(':')[0]) - 1: return dt + timedelta(days=1) return dt df['StartDateTime'] = df['StartTime'].apply(combine_date_time) df['EndDateTime'] = df['EndTime'].apply(combine_date_time) # 按散场时间排序 df = df.sort_values('EndDateTime') # 分割数据 split_dt = datetime.combine(base_date, datetime.strptime(SPLIT_TIME, "%H:%M").time()) part1 = df[df['EndDateTime'] <= split_dt].copy() part2 = df[df['EndDateTime'] > split_dt].copy() # 格式化时间显示 for part in [part1, part2]: part['EndTimeStr'] = part['EndDateTime'].dt.strftime('%-I:%M') # 关键修改:精确读取C6单元格 date_df = pd.read_excel( file, skiprows=5, # 跳过前5行(0-4) nrows=1, # 只读1行 usecols=[2], # 第三列(C列) header=None # 无表头 ) date_cell = date_df.iloc[0, 0] try: # 处理不同日期格式 if isinstance(date_cell, str): date_str = datetime.strptime(date_cell, '%Y-%m-%d').strftime('%Y-%m-%d') else: date_str = pd.to_datetime(date_cell).strftime('%Y-%m-%d') except: date_str = datetime.today().strftime('%Y-%m-%d') return part1[['Hall', 'EndTimeStr']], part2[['Hall', 'EndTimeStr']], date_str except Exception as e: st.error(f"处理文件时出错: {str(e)}") return None, None, None def create_print_layout(data, title, date_str): """创建符合新要求的打印布局 (PNG 和 PDF)""" if data.empty: return None # --- 1. 动态字体大小计算 --- # 目标:让最长的文本占单元格宽度的90% longest_text = "" if not data.empty: data['combined_text'] = data['Hall'] + data['EndTimeStr'] if not data['combined_text'].empty: longest_text = data.loc[data['combined_text'].str.len().idxmax(), 'combined_text'] # 估算单元格宽度 (A5纸张,减去边距和间距) fig_width_in = A5_WIDTH_IN margin_frac = 0.04 # 4% margin on each side wspace_frac = 0.05 # 5% space between columns # 可用绘图宽度 drawable_width_in = fig_width_in * (1 - 2 * margin_frac) # 宽度被3个子图和2个间隙瓜分 # 3*cell_w + 2*(cell_w*wspace) = drawable_width cell_width_in = drawable_width_in / (NUM_COLS + (NUM_COLS - 1) * wspace_frac) main_fontsize = 30 # Default start size if longest_text: # 估算文本宽度 (这是一个经验法则,0.7是中英混合字体的估算因子) # 文本宽度(点) ≈ 字符数 * 字体大小(点) * 因子 estimated_text_width_pt = len(longest_text) * main_fontsize * 0.7 target_width_pt = (cell_width_in * 0.9) * 72 # 目标宽度 (英寸*0.9 -> 点) if estimated_text_width_pt > 0: ratio = target_width_pt / estimated_text_width_pt main_fontsize *= ratio main_fontsize = max(10, min(main_fontsize, 50)) # 限制字体大小在合理范围 index_fontsize = main_fontsize * 0.45 # 序号字体大小 # --- 2. 准备绘图 --- def generate_figure(): fig = plt.figure(figsize=(A5_WIDTH_IN, A5_HEIGHT_IN), dpi=DPI) fig.subplots_adjust(left=margin_frac, right=1-margin_frac, top=0.95, bottom=margin_frac, hspace=0.2, wspace=wspace_frac) total_items = len(data) num_rows = math.ceil(total_items / NUM_COLS) # 创建网格 gs = gridspec.GridSpec(num_rows + 1, NUM_COLS, height_ratios=[0.2] + [1] * num_rows, figure=fig) # 绘制日期标题 ax_date = fig.add_subplot(gs[0, :]) ax_date.text(0, 0.5, f"{date_str} {title}", fontproperties=FONT_PROP, fontsize=main_fontsize * 0.5, color=DATE_COLOR, fontweight='bold', ha='left', va='center', transform=ax_date.transAxes) ax_date.set_axis_off() # 准备Z字形排列的数据 data_values = data[['Hall', 'EndTimeStr']].values.tolist() while len(data_values) % NUM_COLS != 0: data_values.append(['', '']) rows_per_col_layout = math.ceil(len(data_values) / NUM_COLS) sorted_data = [['', '']] * len(data_values) for i, item in enumerate(data_values): if item[0] and item[1]: row_in_col = i % rows_per_col_layout col_idx = i // rows_per_col_layout new_index = row_in_col * NUM_COLS + col_idx if new_index < len(sorted_data): sorted_data[new_index] = item item_counter = 0 for idx, (hall, end_time) in enumerate(sorted_data): if hall and end_time: item_counter += 1 row_grid = idx // NUM_COLS + 1 col_grid = idx % NUM_COLS if row_grid < num_rows + 1: ax = fig.add_subplot(gs[row_grid, col_grid]) # --- 3. 绘制单元格 --- # a. 设置点状虚线边框 for spine in ax.spines.values(): spine.set_linestyle((0, (1, 2))) # (offset, (on, off)) tuple for dotted spine.set_edgecolor(BORDER_COLOR) spine.set_linewidth(1) # b. 居中绘制主要文本 display_text = f"{hall}{end_time}" ax.text(0.5, 0.5, display_text, fontproperties=FONT_PROP, fontsize=main_fontsize, fontweight='bold', ha='center', va='center', transform=ax.transAxes) # c. 在左上角添加序号 ax.text(0.05, 0.95, str(item_counter), fontproperties=FONT_PROP, fontsize=index_fontsize, color='grey', ha='left', va='top', transform=ax.transAxes) ax.set_xticks([]) ax.set_yticks([]) return fig # --- 4. 保存为 PNG 和 PDF --- fig_for_output = generate_figure() # 保存 PNG png_buffer = io.BytesIO() fig_for_output.savefig(png_buffer, format='png', dpi=DPI) png_buffer.seek(0) png_base64 = base64.b64encode(png_buffer.getvalue()).decode() # 保存 PDF pdf_buffer = io.BytesIO() fig_for_output.savefig(pdf_buffer, format='pdf', dpi=DPI) pdf_buffer.seek(0) pdf_base64 = base64.b64encode(pdf_buffer.getvalue()).decode() plt.close(fig_for_output) return { 'png': f'data:image/png;base64,{png_base64}', 'pdf': f'data:application/pdf;base64,{pdf_base64}' } def display_pdf(base64_pdf): """在Streamlit中嵌入显示PDF""" pdf_display = f'' return pdf_display # --- Streamlit UI --- st.set_page_config(page_title="散厅时间快捷打印", layout="wide") st.title("散厅时间快捷打印") uploaded_file = st.file_uploader("上传【放映场次核对表.xls】文件", type=["xls", "xlsx"]) if uploaded_file: # Rename columns in process_schedule call to match new names part1_data, part2_data, date_str = process_schedule(uploaded_file) if part1_data is not None and part2_data is not None: # Pass the dataframes with renamed 'EndTimeStr' column part1_output = create_print_layout(part1_data, "A", date_str) part2_output = create_print_layout(part2_data, "C", date_str) col1, col2 = st.columns(2) with col1: st.subheader("白班散场预览(结束时间 ≤ 17:30)") if part1_output: tab1_1, tab1_2 = st.tabs(["PDF 预览", "PNG 预览"]) with tab1_1: st.markdown(display_pdf(part1_output['pdf']), unsafe_allow_html=True) with tab1_2: st.image(part1_output['png']) else: st.info("白班部分没有数据") with col2: st.subheader("夜班散场预览(结束时间 > 17:30)") if part2_output: tab2_1, tab2_2 = st.tabs(["PDF 预览", "PNG 预览"]) with tab2_1: st.markdown(display_pdf(part2_output['pdf']), unsafe_allow_html=True) with tab2_2: st.image(part2_output['png']) else: st.info("夜班部分没有数据")