Ethscriptions commited on
Commit
2b5d755
·
verified ·
1 Parent(s): 41c541d

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +206 -174
app.py CHANGED
@@ -6,72 +6,79 @@ import io
6
  import base64
7
  import os
8
  from datetime import datetime, timedelta
9
- import numpy as np
10
- from matplotlib.backends.backend_agg import FigureCanvasAgg
11
  from pypinyin import lazy_pinyin, Style
12
  from matplotlib.backends.backend_pdf import PdfPages
13
 
 
14
  def get_font(size=14):
15
- """Loads the specified font, with a fallback."""
16
- font_path = "simHei.ttc"
17
  if not os.path.exists(font_path):
18
- font_path = "SimHei.ttf" # Fallback font
 
19
  if not os.path.exists(font_path):
20
- st.warning("Font file (simHei.ttc or SimHei.ttf) not found. Display may be incorrect.")
21
- return font_manager.FontProperties(size=size)
 
 
 
 
22
  return font_manager.FontProperties(fname=font_path, size=size)
23
 
 
24
  def get_pinyin_abbr(text):
25
- """Gets the first letter of the Pinyin for the first two Chinese characters of a text."""
26
- if not isinstance(text, str):
27
  return ""
28
- # Extract the first two Chinese characters
29
- chars = [c for c in text if '\u4e00' <= c <= '\u9fff'][:2]
30
- if not chars:
 
 
31
  return ""
32
- pinyin_list = lazy_pinyin(chars, style=Style.FIRST_LETTER)
 
33
  return ''.join(pinyin_list).upper()
34
 
 
35
  def process_schedule(file):
36
- """
37
- Processes the uploaded Excel file to extract and clean the movie schedule.
38
- This version also prepares all data fields needed for the new layout.
39
- """
40
  try:
41
- # Try to read the date from the specified cell
42
  date_df = pd.read_excel(file, header=None, skiprows=7, nrows=1, usecols=[3])
43
  date_str = pd.to_datetime(date_df.iloc[0, 0]).strftime('%Y-%m-%d')
44
  base_date = pd.to_datetime(date_str).date()
45
  except Exception:
46
- # Fallback to today's date if reading fails
47
  date_str = datetime.today().strftime('%Y-%m-%d')
48
  base_date = datetime.today().date()
49
-
50
  try:
 
51
  df = pd.read_excel(file, header=9, usecols=[1, 2, 4, 5])
52
  df.columns = ['Hall', 'StartTime', 'EndTime', 'Movie']
53
 
54
- # Data Cleaning
55
  df['Hall'] = df['Hall'].ffill()
56
  df.dropna(subset=['StartTime', 'EndTime', 'Movie'], inplace=True)
57
  df['Hall'] = df['Hall'].astype(str).str.extract(r'(\d+号)')
58
- df.dropna(subset=['Hall'], inplace=True) # Ensure rows without a hall number are dropped
59
 
60
- # Convert times to datetime objects
61
  df['StartTime_dt'] = pd.to_datetime(df['StartTime'], format='%H:%M', errors='coerce').apply(
62
  lambda t: t.replace(year=base_date.year, month=base_date.month, day=base_date.day) if pd.notnull(t) else t
63
  )
64
  df['EndTime_dt'] = pd.to_datetime(df['EndTime'], format='%H:%M', errors='coerce').apply(
65
  lambda t: t.replace(year=base_date.year, month=base_date.month, day=base_date.day) if pd.notnull(t) else t
66
  )
67
-
68
  df.dropna(subset=['StartTime_dt', 'EndTime_dt'], inplace=True)
69
 
70
- # Handle screenings that cross midnight
71
  df.loc[df['EndTime_dt'] < df['StartTime_dt'], 'EndTime_dt'] += timedelta(days=1)
72
  df = df.sort_values(['Hall', 'StartTime_dt'])
73
 
74
- # Merge consecutive screenings of the same movie
75
  merged_rows = []
76
  for _, group in df.groupby('Hall'):
77
  group = group.sort_values('StartTime_dt')
@@ -81,7 +88,7 @@ def process_schedule(file):
81
  current = row.copy()
82
  else:
83
  if row['Movie'] == current['Movie']:
84
- current['EndTime_dt'] = row['EndTime_dt'] # Extend the end time
85
  else:
86
  merged_rows.append(current)
87
  current = row.copy()
@@ -89,194 +96,219 @@ def process_schedule(file):
89
  merged_rows.append(current)
90
 
91
  if not merged_rows:
92
- return None, date_str
93
-
94
- merged_df = pd.DataFrame(merged_rows).reset_index(drop=True)
95
 
96
- # Adjust times as per original logic
97
  merged_df['StartTime_dt'] -= timedelta(minutes=10)
98
  merged_df['EndTime_dt'] -= timedelta(minutes=5)
99
 
100
- # --- New Data Preparation for Layout ---
101
- # 1. Create Index (序号)
102
- merged_df['Index'] = merged_df.groupby('Hall').cumcount() + 1
103
- # 2. Create Pinyin Abbreviation (拼音缩写)
104
  merged_df['Pinyin'] = merged_df['Movie'].apply(get_pinyin_abbr)
105
- # 3. Create Time String (时间)
106
- merged_df['TimeStr'] = merged_df['StartTime_dt'].dt.strftime('%H:%M') + ' - ' + merged_df['EndTime_dt'].dt.strftime('%H:%M')
107
- # 4. Clean Hall Number for display
108
  merged_df['Hall'] = merged_df['Hall'].str.replace('号', '')
109
-
110
- # Select and reorder columns as per requirement
111
- final_df = merged_df[['Hall', 'Index', 'Movie', 'Pinyin', 'TimeStr']]
112
 
113
- return final_df, date_str
114
 
115
  except Exception as e:
116
- st.error(f"An error occurred during file processing: {e}")
117
  return None, date_str
118
 
119
 
 
120
  def create_print_layout(data, date_str):
121
- """
122
- Creates the print layout on an A4 page based on a dynamic grid system.
123
- """
124
  if data is None or data.empty:
125
  return None
126
 
127
- # --- 1. Layout Constants ---
128
- A4_WIDTH_IN, A4_HEIGHT_IN = 8.27, 11.69
129
- MARGIN_IN = 0.4
130
- USABLE_WIDTH_IN = A4_WIDTH_IN - (2 * MARGIN_IN)
131
- USABLE_HEIGHT_IN = A4_HEIGHT_IN - (2 * MARGIN_IN)
132
-
133
- # --- 2. Row and Font Calculation ---
134
- num_content_rows = len(data)
135
- total_grid_rows = num_content_rows + 2 # Add 2 for top/bottom padding rows
136
- row_height_in = USABLE_HEIGHT_IN / total_grid_rows
137
- # Calculate font size in points (1 inch = 72 points) to be 90% of row height
138
- font_size_pt = (row_height_in * 72) * 0.9
139
- content_font = get_font(font_size_pt)
140
- date_font = get_font(12)
141
-
142
- # --- 3. Column Width Calculation ---
143
- # Create a temporary figure to calculate text widths accurately
144
- temp_fig = plt.figure(figsize=(A4_WIDTH_IN, A4_HEIGHT_IN))
145
- canvas = FigureCanvasAgg(temp_fig)
146
 
147
- cols_to_measure = ['Hall', 'Index', 'Movie', 'Pinyin', 'TimeStr']
148
- col_widths_in = []
149
-
150
- for col in cols_to_measure:
151
- # Find the longest string in the column for measurement
152
- longest_item = max(data[col].astype(str).tolist(), key=len, default="")
153
- # Create a temporary text object to measure its width
154
- t = plt.text(0, 0, longest_item, fontproperties=content_font)
155
- # Get the bounding box of the text in display units and convert to inches
156
- bbox = t.get_window_extent(renderer=canvas.get_renderer())
157
- width_in = bbox.width / temp_fig.dpi
158
- col_widths_in.append(width_in * 1.1) # Add 10% padding
159
  t.remove()
 
160
 
161
- plt.close(temp_fig) # Close the temporary figure
 
 
 
 
 
 
162
 
163
- # Scale column widths to fit the usable page width
164
- total_calculated_width = sum(col_widths_in)
165
- scale_factor = USABLE_WIDTH_IN / total_calculated_width if total_calculated_width > 0 else 1
166
- final_col_widths_in = [w * scale_factor for w in col_widths_in]
 
 
 
 
 
167
 
168
- # --- 4. Figure and PDF/PNG Generation ---
169
- def process_figure(fig, ax):
170
- # Calculate grid coordinates in Axes units (0 to 1)
171
- col_widths_ax = [w / USABLE_WIDTH_IN for w in final_col_widths_in]
172
- row_height_ax = 1.0 / total_grid_rows
173
-
174
- x_coords_ax = [0] + np.cumsum(col_widths_ax).tolist()
175
- y_coords_ax = [1 - i * row_height_ax for i in range(total_grid_rows + 1)]
176
-
177
- # Add date string at the top-left of the usable area
178
- ax.text(0, 1, date_str, transform=ax.transAxes, fontproperties=date_font,
179
- ha='left', va='bottom', color='#A9A9A9')
180
-
181
- # --- Draw Grid and Content ---
182
- for i, row in data.iterrows():
183
- grid_row_index = i + 1 # Offset by 1 for the top padding row
184
- y_bottom = y_coords_ax[grid_row_index + 1]
185
- y_center = y_bottom + row_height_ax / 2
186
-
187
- # Draw bottom dotted line for the current row's cells
188
- ax.plot([0, 1], [y_bottom, y_bottom], transform=ax.transAxes,
189
- linestyle=':', color='gray', linewidth=0.7)
190
-
191
- # Draw content for each cell in the row
192
- content_list = [row['Hall'], row['Index'], row['Movie'], row['Pinyin'], row['TimeStr']]
193
- for j, content in enumerate(content_list):
194
- x_left = x_coords_ax[j]
195
- x_center = x_left + col_widths_ax[j] / 2
196
- ax.text(x_center, y_center, content, transform=ax.transAxes,
197
- fontproperties=content_font, ha='center', va='center')
198
 
199
- # --- Draw Vertical Grid Lines ---
200
- content_area_top_y = y_coords_ax[1]
201
- content_area_bottom_y = y_coords_ax[-2]
202
- for x in x_coords_ax[1:-1]:
203
- ax.plot([x, x], [content_area_bottom_y, content_area_top_y], transform=ax.transAxes,
204
- linestyle=':', color='gray', linewidth=0.7)
205
-
206
- # --- Draw Black Separator Lines Between Halls ---
207
- hall_change_indices = data.index[data['Hall'] != data['Hall'].shift(-1)]
208
- for idx in hall_change_indices:
209
- # The line is at the bottom of the current row
210
- y_line = y_coords_ax[idx + 2] # +1 for top margin, +1 to get bottom of current row
211
- ax.plot([0, 1], [y_line, y_line], transform=ax.transAxes,
212
- linestyle='-', color='black', linewidth=1.2)
213
-
214
-
215
- # Create figures for PNG and PDF
216
- png_fig = plt.figure(figsize=(A4_WIDTH_IN, A4_HEIGHT_IN), dpi=300)
217
- pdf_fig = plt.figure(figsize=(A4_WIDTH_IN, A4_HEIGHT_IN), dpi=300)
218
-
219
- # Configure axes to fill the usable area defined by margins
220
- ax_rect = [
221
- MARGIN_IN / A4_WIDTH_IN, MARGIN_IN / A4_HEIGHT_IN,
222
- USABLE_WIDTH_IN / A4_WIDTH_IN, USABLE_HEIGHT_IN / A4_HEIGHT_IN
223
- ]
224
- png_ax = png_fig.add_axes(ax_rect)
225
- pdf_ax = pdf_fig.add_axes(ax_rect)
226
- png_ax.axis('off')
227
- pdf_ax.axis('off')
228
-
229
- # Process both figures
230
- process_figure(png_fig, png_ax)
231
- process_figure(pdf_fig, pdf_ax)
232
-
233
- # Save PNG to buffer
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
234
  png_buffer = io.BytesIO()
235
- png_fig.savefig(png_buffer, format='png', pad_inches=0)
236
  png_buffer.seek(0)
237
  image_base64 = base64.b64encode(png_buffer.getvalue()).decode()
238
- plt.close(png_fig)
239
 
240
- # Save PDF to buffer
241
  pdf_buffer = io.BytesIO()
242
- pdf_fig.savefig(pdf_buffer, format='pdf', pad_inches=0)
243
  pdf_buffer.seek(0)
244
  pdf_base64 = base64.b64encode(pdf_buffer.getvalue()).decode()
245
- plt.close(pdf_fig)
246
 
247
  return {
248
  'png': f"data:image/png;base64,{image_base64}",
249
  'pdf': f"data:application/pdf;base64,{pdf_base64}"
250
  }
251
 
252
- def display_pdf(base64_pdf):
253
- """Embeds the PDF in the Streamlit app for display."""
254
- pdf_display = f'<iframe src="{base64_pdf}" width="100%" height="800" type="application/pdf"></iframe>'
255
- return pdf_display
256
 
257
- # --- Streamlit App Main Body ---
258
  st.set_page_config(page_title="LED 屏幕时间表打印", layout="wide")
259
  st.title("LED 屏幕时间表打印")
 
260
 
261
- uploaded_file = st.file_uploader("选择打开【放映时间核对表.xls】文件", type=["xls"])
262
 
263
  if uploaded_file:
264
- with st.spinner("文件正在处理中,请稍候..."):
265
  schedule, date_str = process_schedule(uploaded_file)
266
  if schedule is not None and not schedule.empty:
267
  output = create_print_layout(schedule, date_str)
268
- if output:
269
- # Create tabs to switch between PDF and PNG previews
270
- tab1, tab2 = st.tabs(["PDF 预览", "PNG 预览"])
271
-
272
- with tab1:
273
- st.markdown(display_pdf(output['pdf']), unsafe_allow_html=True)
274
-
275
- with tab2:
276
- st.image(output['png'], use_container_width=True)
277
- else:
278
- st.error("生成打印布局失败。")
279
- elif schedule is None:
280
- st.error("无法处理文件,请检查文件格式或内容是否正确。")
281
- else: # schedule is empty
282
- st.warning("处理完成,但文件中没有找到有效的排片数据。")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
  import base64
7
  import os
8
  from datetime import datetime, timedelta
9
+ from matplotlib.patches import Rectangle
 
10
  from pypinyin import lazy_pinyin, Style
11
  from matplotlib.backends.backend_pdf import PdfPages
12
 
13
+ # --- 字体设置 ---
14
  def get_font(size=14):
15
+ """根据操作系统环境查找并返回中文字体属性"""
16
+ font_path = "simHei.ttc" # 优先使用 simHei.ttc
17
  if not os.path.exists(font_path):
18
+ font_path = "SimHei.ttf" # SimHei.ttf 作为备选
19
+ # 如果两者都不存在,可以添加更多备选字体路径
20
  if not os.path.exists(font_path):
21
+ # for Windows
22
+ font_path = "C:/Windows/Fonts/simhei.ttf"
23
+ if not os.path.exists(font_path):
24
+ # for MacOS
25
+ font_path = "/System/Library/Fonts/STHeiti Medium.ttc"
26
+ # 如果仍然找不到,matplotlib会回退到默认字体
27
  return font_manager.FontProperties(fname=font_path, size=size)
28
 
29
+ # --- 拼音处理 ---
30
  def get_pinyin_abbr(text):
31
+ """获取文本前两个汉字的拼音首字母"""
32
+ if not text or not isinstance(text, str):
33
  return ""
34
+ # 提取中文字符
35
+ chars = [c for c in text if '\u4e00' <= c <= '\u9fff']
36
+ # 取前两个汉字
37
+ chars_to_process = chars[:2]
38
+ if not chars_to_process:
39
  return ""
40
+ # 获取拼音首字母并转为大写
41
+ pinyin_list = lazy_pinyin(chars_to_process, style=Style.FIRST_LETTER)
42
  return ''.join(pinyin_list).upper()
43
 
44
+ # --- 数据处理 ---
45
  def process_schedule(file):
46
+ """读取并处理 Excel 文件,返回格式化的 DataFrame 和日期"""
 
 
 
47
  try:
48
+ # 尝试读取日期
49
  date_df = pd.read_excel(file, header=None, skiprows=7, nrows=1, usecols=[3])
50
  date_str = pd.to_datetime(date_df.iloc[0, 0]).strftime('%Y-%m-%d')
51
  base_date = pd.to_datetime(date_str).date()
52
  except Exception:
53
+ # 读取失败则使用当天日期
54
  date_str = datetime.today().strftime('%Y-%m-%d')
55
  base_date = datetime.today().date()
56
+
57
  try:
58
+ # 读取排片数据
59
  df = pd.read_excel(file, header=9, usecols=[1, 2, 4, 5])
60
  df.columns = ['Hall', 'StartTime', 'EndTime', 'Movie']
61
 
62
+ # 数据清洗
63
  df['Hall'] = df['Hall'].ffill()
64
  df.dropna(subset=['StartTime', 'EndTime', 'Movie'], inplace=True)
65
  df['Hall'] = df['Hall'].astype(str).str.extract(r'(\d+号)')
66
+ df.dropna(subset=['Hall'], inplace=True)
67
 
68
+ # 时间转换
69
  df['StartTime_dt'] = pd.to_datetime(df['StartTime'], format='%H:%M', errors='coerce').apply(
70
  lambda t: t.replace(year=base_date.year, month=base_date.month, day=base_date.day) if pd.notnull(t) else t
71
  )
72
  df['EndTime_dt'] = pd.to_datetime(df['EndTime'], format='%H:%M', errors='coerce').apply(
73
  lambda t: t.replace(year=base_date.year, month=base_date.month, day=base_date.day) if pd.notnull(t) else t
74
  )
 
75
  df.dropna(subset=['StartTime_dt', 'EndTime_dt'], inplace=True)
76
 
77
+ # 处理跨天结束时间
78
  df.loc[df['EndTime_dt'] < df['StartTime_dt'], 'EndTime_dt'] += timedelta(days=1)
79
  df = df.sort_values(['Hall', 'StartTime_dt'])
80
 
81
+ # 合并同一影厅的连续相同影片
82
  merged_rows = []
83
  for _, group in df.groupby('Hall'):
84
  group = group.sort_values('StartTime_dt')
 
88
  current = row.copy()
89
  else:
90
  if row['Movie'] == current['Movie']:
91
+ current['EndTime_dt'] = row['EndTime_dt']
92
  else:
93
  merged_rows.append(current)
94
  current = row.copy()
 
96
  merged_rows.append(current)
97
 
98
  if not merged_rows:
99
+ return None, date_str
100
+
101
+ merged_df = pd.DataFrame(merged_rows)
102
 
103
+ # 统一调整时间
104
  merged_df['StartTime_dt'] -= timedelta(minutes=10)
105
  merged_df['EndTime_dt'] -= timedelta(minutes=5)
106
 
107
+ # 格式化最终输出的列
108
+ merged_df['Time'] = merged_df['StartTime_dt'].dt.strftime('%H:%M') + ' - ' + merged_df['EndTime_dt'].dt.strftime('%H:%M')
 
 
109
  merged_df['Pinyin'] = merged_df['Movie'].apply(get_pinyin_abbr)
 
 
 
110
  merged_df['Hall'] = merged_df['Hall'].str.replace('号', '')
 
 
 
111
 
112
+ return merged_df[['Hall', 'Movie', 'Pinyin', 'Time']], date_str
113
 
114
  except Exception as e:
115
+ st.error(f"处理 Excel 数据时发生错误: {e}")
116
  return None, date_str
117
 
118
 
119
+ # --- 打印布局生成 ---
120
  def create_print_layout(data, date_str):
121
+ """根据处理好的数据,生成用于打印的 PNG 和 PDF 布局"""
 
 
122
  if data is None or data.empty:
123
  return None
124
 
125
+ A4_SIZE_INCHES = (8.27, 11.69)
126
+ DPI = 300
127
+
128
+ # 准备一个临时的 figure 用于计算文本渲染尺寸
129
+ temp_fig = plt.figure(figsize=A4_SIZE_INCHES, dpi=DPI)
130
+ renderer = temp_fig.canvas.get_renderer()
131
+
132
+ # 1. 计算行高
133
+ num_movies = len(data)
134
+ num_halls = len(data['Hall'].unique())
135
+ # 总行数 = 电影条目数 + 厅间分隔数 + 上下留白(2)
136
+ # total_layout_rows = num_movies + (num_halls - 1) + 2
137
+ total_layout_rows = num_movies + 2 # 简化为条目数+2,使行高更宽松
138
+ row_height_inch = A4_SIZE_INCHES[1] / total_layout_rows
 
 
 
 
 
139
 
140
+ # 2. 计算基准字体大小 (点)
141
+ # 1 point = 1/72 inch.
142
+ # 字体高度为行高的 90%
143
+ font_size_pt = row_height_inch * 0.9 * 72
144
+ base_font = get_font(font_size_pt)
145
+
146
+ # 3. 计算各列宽度 (除电影名外)
147
+ def get_text_width_inch(text, font):
148
+ t = plt.text(0, 0, text, fontproperties=font)
149
+ bbox = t.get_window_extent(renderer=renderer)
150
+ width_pixels = bbox.width
 
151
  t.remove()
152
+ return width_pixels / DPI
153
 
154
+ # 找到每列最长的内容
155
+ data['Index'] = data.groupby('Hall').cumcount() + 1
156
+ max_hall_str = data['Hall'].max() + "#" # e.g. "10#"
157
+ max_index_str = str(data['Index'].max()) + "." # e.g. "5."
158
+ max_pinyin_str = data['Pinyin'].apply(len).max() * "A" # e.g. "PY" -> "AA"
159
+ max_time_str = data['Time'].apply(len).idxmax()
160
+ max_time_str = data.loc[max_time_str, 'Time']
161
 
162
+ col_widths = {}
163
+ col_widths['Hall'] = get_text_width_inch(max_hall_str, base_font) * 1.1
164
+ col_widths['Index'] = get_text_width_inch(max_index_str, base_font) * 1.1
165
+ col_widths['Pinyin'] = get_text_width_inch(max_pinyin_str, base_font) * 1.1
166
+ col_widths['Time'] = get_text_width_inch(max_time_str, base_font) * 1.1
167
+
168
+ # 电影名列的宽度为剩余宽度
169
+ non_movie_width = sum(col_widths.values())
170
+ col_widths['Movie'] = A4_SIZE_INCHES[0] - non_movie_width
171
 
172
+ plt.close(temp_fig) # 关闭临时figure
173
+
174
+ # --- 开始正式绘图 ---
175
+ figs = {}
176
+ for fmt in ['png', 'pdf']:
177
+ fig = plt.figure(figsize=A4_SIZE_INCHES, dpi=DPI)
178
+ ax = fig.add_subplot(111)
179
+ ax.set_axis_off()
180
+ fig.subplots_adjust(left=0, right=1, top=1, bottom=0)
181
+ ax.set_xlim(0, A4_SIZE_INCHES[0])
182
+ ax.set_ylim(0, A4_SIZE_INCHES[1])
183
+
184
+ # 计算列的 X 轴起始位置
185
+ x_pos = {}
186
+ current_x = 0
187
+ # 新顺序: Hall, Index, Movie, Pinyin, Time
188
+ col_order = ['Hall', 'Index', 'Movie', 'Pinyin', 'Time']
189
+ for col in col_order:
190
+ x_pos[col] = current_x
191
+ current_x += col_widths[col]
192
+
193
+ # 从顶部开始绘制 (顶部留出一行空白)
194
+ current_y = A4_SIZE_INCHES[1] - row_height_inch
 
 
 
 
 
 
 
195
 
196
+ halls = sorted(data['Hall'].unique(), key=lambda h: int(h))
197
+
198
+ for hall in halls:
199
+ hall_data = data[data['Hall'] == hall].sort_values('Index')
200
+
201
+ for _, row in hall_data.iterrows():
202
+ y_bottom = current_y - row_height_inch
203
+
204
+ # 绘制单元格
205
+ for col_name in col_order:
206
+ cell_x = x_pos[col_name]
207
+ cell_width = col_widths[col_name]
208
+
209
+ # 绘制灰色虚线边框
210
+ rect = Rectangle((cell_x, y_bottom), cell_width, row_height_inch,
211
+ edgecolor='lightgray', facecolor='none',
212
+ linestyle=(0, (1, 2)), linewidth=0.8, zorder=1)
213
+ ax.add_patch(rect)
214
+
215
+ # 准备文本内容
216
+ text_content = {
217
+ 'Hall': f"{row['Hall']}#",
218
+ 'Index': f"{row['Index']}.",
219
+ 'Movie': row['Movie'],
220
+ 'Pinyin': row['Pinyin'],
221
+ 'Time': row['Time']
222
+ }[col_name]
223
+
224
+ # 文本垂直居中
225
+ text_y = y_bottom + row_height_inch / 2
226
+
227
+ # 电影名列特殊处理
228
+ if col_name == 'Movie':
229
+ font_to_use = base_font.copy()
230
+ # 检查宽度并调整字体
231
+ text_w_inch = get_text_width_inch(text_content, font_to_use)
232
+ max_w_inch = cell_width * 0.9 # 目标宽度为单元格宽度的90%
233
+ if text_w_inch > max_w_inch:
234
+ scale_factor = max_w_inch / text_w_inch
235
+ font_to_use.set_size(font_size_pt * scale_factor)
236
+
237
+ ax.text(cell_x + cell_width * 0.05, text_y, text_content, # 左对齐
238
+ fontproperties=font_to_use, ha='left', va='center', clip_on=True)
239
+ else: # 其他列
240
+ ax.text(cell_x + cell_width / 2, text_y, text_content, # 居中对齐
241
+ fontproperties=base_font, ha='center', va='center', clip_on=True)
242
+
243
+ current_y -= row_height_inch
244
+
245
+ # 在每个影厅块结束后绘制黑色分隔线
246
+ ax.plot([0, A4_SIZE_INCHES[0]], [current_y, current_y], color='black', linewidth=1.5, zorder=2)
247
+
248
+ # 在左上角添加日期
249
+ ax.text(0.1, A4_SIZE_INCHES[1] - 0.3, date_str,
250
+ fontproperties=get_font(12), color='gray', ha='left', va='top')
251
+
252
+ figs[fmt] = fig
253
+
254
+ # 保存到内存
255
  png_buffer = io.BytesIO()
256
+ figs['png'].savefig(png_buffer, format='png', dpi=DPI)
257
  png_buffer.seek(0)
258
  image_base64 = base64.b64encode(png_buffer.getvalue()).decode()
259
+ plt.close(figs['png'])
260
 
 
261
  pdf_buffer = io.BytesIO()
262
+ figs['pdf'].savefig(pdf_buffer, format='pdf', dpi=DPI)
263
  pdf_buffer.seek(0)
264
  pdf_base64 = base64.b64encode(pdf_buffer.getvalue()).decode()
265
+ plt.close(figs['pdf'])
266
 
267
  return {
268
  'png': f"data:image/png;base64,{image_base64}",
269
  'pdf': f"data:application/pdf;base64,{pdf_base64}"
270
  }
271
 
 
 
 
 
272
 
273
+ # --- Streamlit UI ---
274
  st.set_page_config(page_title="LED 屏幕时间表打印", layout="wide")
275
  st.title("LED 屏幕时间表打印")
276
+ st.markdown("请上传影院系统导出的 `放映时间核对表.xls` 文件。系统将自动处理数据并生成专业、美观的A4打印布局。")
277
 
278
+ uploaded_file = st.file_uploader("选择文件", accept_multiple_files=False, type=["xls", "xlsx"])
279
 
280
  if uploaded_file:
281
+ with st.spinner("文件处理与布局生成中,请稍候..."):
282
  schedule, date_str = process_schedule(uploaded_file)
283
  if schedule is not None and not schedule.empty:
284
  output = create_print_layout(schedule, date_str)
285
+
286
+ st.success(f"成功生成 **{date_str}** 的排片表��")
287
+
288
+ # 创建下载按钮
289
+ col1, col2 = st.columns(2)
290
+ with col1:
291
+ st.download_button(
292
+ label="📥 下载 PNG 图像",
293
+ data=base64.b64decode(output['png'].split(',')[1]),
294
+ file_name=f"排片表_{date_str}.png",
295
+ mime="image/png"
296
+ )
297
+ with col2:
298
+ st.download_button(
299
+ label="📄 下载 PDF 文档",
300
+ data=base64.b64decode(output['pdf'].split(',')[1]),
301
+ file_name=f"排片表_{date_str}.pdf",
302
+ mime="application/pdf"
303
+ )
304
+
305
+ # 创建选项卡进行预览
306
+ tab1, tab2 = st.tabs(["📄 PDF 预览", "🖼️ PNG 预览"])
307
+
308
+ with tab1:
309
+ st.markdown(f'<iframe src="{output["pdf"]}" width="100%" height="800" type="application/pdf"></iframe>', unsafe_allow_html=True)
310
+
311
+ with tab2:
312
+ st.image(output['png'], use_container_width=True)
313
+ else:
314
+ st.error("无法处理文件。请检查文件内容是否为空或格式是否正确。确保文件中包含有效的排片数据。")