Spaces:
Running
Running
File size: 12,066 Bytes
70694c9 41c541d 2471374 7c5a0e0 70694c9 41c541d 1878cfc 41c541d ee0147b 70694c9 41c541d 1878cfc 2471374 41c541d 1878cfc 70694c9 ee0147b 41c541d ee0147b 1878cfc 41c541d 1878cfc 1b1b918 ee0147b 1878cfc 41c541d 1878cfc ee0147b 41c541d 1878cfc 41c541d a215102 ee0147b 1878cfc ee0147b 41c541d 1878cfc 41c541d ee0147b 2471374 1878cfc 41c541d 1878cfc 928edc3 41c541d 928edc3 1878cfc ee0147b 41c541d a215102 41c541d ee0147b 41c541d ee0147b 41c541d ee0147b 41c541d a215102 41c541d ee0147b 41c541d 1b1b918 41c541d 1878cfc 70694c9 41c541d ee0147b 41c541d ee0147b 1878cfc 1b1b918 41c541d 7c5a0e0 41c541d ee0147b 41c541d 928edc3 41c541d 1878cfc 41c541d 1878cfc 41c541d 1878cfc 41c541d 1878cfc 41c541d 1878cfc 70694c9 ee0147b 093d429 70694c9 41c541d ee0147b 70694c9 41c541d 70694c9 093d429 41c541d ee0147b a215102 41c541d |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 |
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 numpy as np
from matplotlib.backends.backend_agg import FigureCanvasAgg
from pypinyin import lazy_pinyin, Style
from matplotlib.backends.backend_pdf import PdfPages
def get_font(size=14):
"""Loads the specified font, with a fallback."""
font_path = "simHei.ttc"
if not os.path.exists(font_path):
font_path = "SimHei.ttf" # Fallback font
if not os.path.exists(font_path):
st.warning("Font file (simHei.ttc or SimHei.ttf) not found. Display may be incorrect.")
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 of a text."""
if not isinstance(text, str):
return ""
# Extract the first two Chinese characters
chars = [c for c in text if '\u4e00' <= c <= '\u9fff'][:2]
if not chars:
return ""
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 the movie schedule.
This version also prepares all data fields needed for the new layout.
"""
try:
# Try to read the date from the specified 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:
df = pd.read_excel(file, header=9, usecols=[1, 2, 4, 5])
df.columns = ['Hall', 'StartTime', 'EndTime', 'Movie']
# Data Cleaning
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) # Ensure rows without a hall number are dropped
# 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
)
df.dropna(subset=['StartTime_dt', 'EndTime_dt'], inplace=True)
# Handle screenings that cross midnight
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 _, 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)
if not merged_rows:
return None, date_str
merged_df = pd.DataFrame(merged_rows).reset_index(drop=True)
# Adjust times as per original logic
merged_df['StartTime_dt'] -= timedelta(minutes=10)
merged_df['EndTime_dt'] -= timedelta(minutes=5)
# --- New Data Preparation for Layout ---
# 1. Create Index (序号)
merged_df['Index'] = merged_df.groupby('Hall').cumcount() + 1
# 2. Create Pinyin Abbreviation (拼音缩写)
merged_df['Pinyin'] = merged_df['Movie'].apply(get_pinyin_abbr)
# 3. Create Time String (时间)
merged_df['TimeStr'] = merged_df['StartTime_dt'].dt.strftime('%H:%M') + ' - ' + merged_df['EndTime_dt'].dt.strftime('%H:%M')
# 4. Clean Hall Number for display
merged_df['Hall'] = merged_df['Hall'].str.replace('号', '')
# Select and reorder columns as per requirement
final_df = merged_df[['Hall', 'Index', 'Movie', 'Pinyin', 'TimeStr']]
return final_df, date_str
except Exception as e:
st.error(f"An error occurred during file processing: {e}")
return None, date_str
def create_print_layout(data, date_str):
"""
Creates the print layout on an A4 page based on a dynamic grid system.
"""
if data is None or data.empty:
return None
# --- 1. Layout Constants ---
A4_WIDTH_IN, A4_HEIGHT_IN = 8.27, 11.69
MARGIN_IN = 0.4
USABLE_WIDTH_IN = A4_WIDTH_IN - (2 * MARGIN_IN)
USABLE_HEIGHT_IN = A4_HEIGHT_IN - (2 * MARGIN_IN)
# --- 2. Row and Font Calculation ---
num_content_rows = len(data)
total_grid_rows = num_content_rows + 2 # Add 2 for top/bottom padding rows
row_height_in = USABLE_HEIGHT_IN / total_grid_rows
# Calculate font size in points (1 inch = 72 points) to be 90% of row height
font_size_pt = (row_height_in * 72) * 0.9
content_font = get_font(font_size_pt)
date_font = get_font(12)
# --- 3. Column Width Calculation ---
# Create a temporary figure to calculate text widths accurately
temp_fig = plt.figure(figsize=(A4_WIDTH_IN, A4_HEIGHT_IN))
canvas = FigureCanvasAgg(temp_fig)
cols_to_measure = ['Hall', 'Index', 'Movie', 'Pinyin', 'TimeStr']
col_widths_in = []
for col in cols_to_measure:
# Find the longest string in the column for measurement
longest_item = max(data[col].astype(str).tolist(), key=len, default="")
# Create a temporary text object to measure its width
t = plt.text(0, 0, longest_item, fontproperties=content_font)
# Get the bounding box of the text in display units and convert to inches
bbox = t.get_window_extent(renderer=canvas.get_renderer())
width_in = bbox.width / temp_fig.dpi
col_widths_in.append(width_in * 1.1) # Add 10% padding
t.remove()
plt.close(temp_fig) # Close the temporary figure
# Scale column widths to fit the usable page width
total_calculated_width = sum(col_widths_in)
scale_factor = USABLE_WIDTH_IN / total_calculated_width if total_calculated_width > 0 else 1
final_col_widths_in = [w * scale_factor for w in col_widths_in]
# --- 4. Figure and PDF/PNG Generation ---
def process_figure(fig, ax):
# Calculate grid coordinates in Axes units (0 to 1)
col_widths_ax = [w / USABLE_WIDTH_IN for w in final_col_widths_in]
row_height_ax = 1.0 / total_grid_rows
x_coords_ax = [0] + np.cumsum(col_widths_ax).tolist()
y_coords_ax = [1 - i * row_height_ax for i in range(total_grid_rows + 1)]
# Add date string at the top-left of the usable area
ax.text(0, 1, date_str, transform=ax.transAxes, fontproperties=date_font,
ha='left', va='bottom', color='#A9A9A9')
# --- Draw Grid and Content ---
for i, row in data.iterrows():
grid_row_index = i + 1 # Offset by 1 for the top padding row
y_bottom = y_coords_ax[grid_row_index + 1]
y_center = y_bottom + row_height_ax / 2
# Draw bottom dotted line for the current row's cells
ax.plot([0, 1], [y_bottom, y_bottom], transform=ax.transAxes,
linestyle=':', color='gray', linewidth=0.7)
# Draw content for each cell in the row
content_list = [row['Hall'], row['Index'], row['Movie'], row['Pinyin'], row['TimeStr']]
for j, content in enumerate(content_list):
x_left = x_coords_ax[j]
x_center = x_left + col_widths_ax[j] / 2
ax.text(x_center, y_center, content, transform=ax.transAxes,
fontproperties=content_font, ha='center', va='center')
# --- Draw Vertical Grid Lines ---
content_area_top_y = y_coords_ax[1]
content_area_bottom_y = y_coords_ax[-2]
for x in x_coords_ax[1:-1]:
ax.plot([x, x], [content_area_bottom_y, content_area_top_y], transform=ax.transAxes,
linestyle=':', color='gray', linewidth=0.7)
# --- Draw Black Separator Lines Between Halls ---
hall_change_indices = data.index[data['Hall'] != data['Hall'].shift(-1)]
for idx in hall_change_indices:
# The line is at the bottom of the current row
y_line = y_coords_ax[idx + 2] # +1 for top margin, +1 to get bottom of current row
ax.plot([0, 1], [y_line, y_line], transform=ax.transAxes,
linestyle='-', color='black', linewidth=1.2)
# Create figures for PNG and PDF
png_fig = plt.figure(figsize=(A4_WIDTH_IN, A4_HEIGHT_IN), dpi=300)
pdf_fig = plt.figure(figsize=(A4_WIDTH_IN, A4_HEIGHT_IN), dpi=300)
# Configure axes to fill the usable area defined by margins
ax_rect = [
MARGIN_IN / A4_WIDTH_IN, MARGIN_IN / A4_HEIGHT_IN,
USABLE_WIDTH_IN / A4_WIDTH_IN, USABLE_HEIGHT_IN / A4_HEIGHT_IN
]
png_ax = png_fig.add_axes(ax_rect)
pdf_ax = pdf_fig.add_axes(ax_rect)
png_ax.axis('off')
pdf_ax.axis('off')
# Process both figures
process_figure(png_fig, png_ax)
process_figure(pdf_fig, pdf_ax)
# Save PNG to buffer
png_buffer = io.BytesIO()
png_fig.savefig(png_buffer, format='png', pad_inches=0)
png_buffer.seek(0)
image_base64 = base64.b64encode(png_buffer.getvalue()).decode()
plt.close(png_fig)
# Save PDF to buffer
pdf_buffer = io.BytesIO()
pdf_fig.savefig(pdf_buffer, format='pdf', pad_inches=0)
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):
"""Embeds the PDF in the Streamlit app for display."""
pdf_display = f'<iframe src="{base64_pdf}" width="100%" height="800" type="application/pdf"></iframe>'
return pdf_display
# --- Streamlit App Main Body ---
st.set_page_config(page_title="LED 屏幕时间表打印", layout="wide")
st.title("LED 屏幕时间表打印")
uploaded_file = st.file_uploader("选择打开【放映时间核对表.xls】文件", type=["xls"])
if uploaded_file:
with st.spinner("文件正在处理中,请稍候..."):
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:
# Create tabs to switch between PDF and PNG previews
tab1, tab2 = st.tabs(["PDF 预览", "PNG 预览"])
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("生成打印布局失败。")
elif schedule is None:
st.error("无法处理文件,请检查文件格式或内容是否正确。")
else: # schedule is empty
st.warning("处理完成,但文件中没有找到有效的排片数据。") |