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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +186 -211
app.py CHANGED
@@ -6,309 +6,284 @@ import io
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')
85
  current = None
86
- for _, row in group.iterrows():
87
  if current is None:
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()
95
  if current is not None:
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("无法处理文件。请检查文件内容是否为空或格式是否正确。确保文件中包含有效的排片数据。")
 
6
  import base64
7
  import os
8
  from datetime import datetime, timedelta
9
+ import math
10
+ from matplotlib.patches import FancyBboxPatch, Rectangle
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 file, falling back to a default."""
16
+ font_path = "simHei.ttc"
17
  if not os.path.exists(font_path):
18
+ font_path = "SimHei.ttf"
 
19
  if not os.path.exists(font_path):
20
+ # As a last resort, return default font if SimHei is not found
21
+ st.warning("Font file 'simHei.ttc' or 'SimHei.ttf' not found. Text may not render correctly.")
22
+ return font_manager.FontProperties(size=size)
 
 
 
23
  return font_manager.FontProperties(fname=font_path, size=size)
24
 
 
25
  def get_pinyin_abbr(text):
26
+ """Gets the first letter of the Pinyin for the first two Chinese characters."""
27
+ if not text:
28
  return ""
29
+ chars = [c for c in text if '\u4e00' <= c <= '\u9fff'][:2]
30
+ if not chars:
 
 
 
31
  return ""
32
+ return ''.join(lazy_pinyin(chars, style=Style.FIRST_LETTER)).upper()
 
 
33
 
 
34
  def process_schedule(file):
35
+ """Reads and processes the movie schedule from the uploaded Excel file."""
36
  try:
37
+ # Try to read the date from a specific cell
38
  date_df = pd.read_excel(file, header=None, skiprows=7, nrows=1, usecols=[3])
39
  date_str = pd.to_datetime(date_df.iloc[0, 0]).strftime('%Y-%m-%d')
40
  base_date = pd.to_datetime(date_str).date()
41
  except Exception:
42
+ # Fallback to today's date
 
43
  base_date = datetime.today().date()
44
+ date_str = base_date.strftime('%Y-%m-%d')
45
 
46
  try:
 
47
  df = pd.read_excel(file, header=9, usecols=[1, 2, 4, 5])
48
  df.columns = ['Hall', 'StartTime', 'EndTime', 'Movie']
 
 
49
  df['Hall'] = df['Hall'].ffill()
50
  df.dropna(subset=['StartTime', 'EndTime', 'Movie'], inplace=True)
51
  df['Hall'] = df['Hall'].astype(str).str.extract(r'(\d+号)')
52
  df.dropna(subset=['Hall'], inplace=True)
53
 
54
+ # Convert times to datetime objects
55
  df['StartTime_dt'] = pd.to_datetime(df['StartTime'], format='%H:%M', errors='coerce').apply(
56
  lambda t: t.replace(year=base_date.year, month=base_date.month, day=base_date.day) if pd.notnull(t) else t
57
  )
58
  df['EndTime_dt'] = pd.to_datetime(df['EndTime'], format='%H:%M', errors='coerce').apply(
59
  lambda t: t.replace(year=base_date.year, month=base_date.month, day=base_date.day) if pd.notnull(t) else t
60
  )
61
+ # Handle overnight screenings
 
 
62
  df.loc[df['EndTime_dt'] < df['StartTime_dt'], 'EndTime_dt'] += timedelta(days=1)
63
  df = df.sort_values(['Hall', 'StartTime_dt'])
64
+
65
+ # Merge consecutive shows of the same movie
66
  merged_rows = []
67
+ for hall, group in df.groupby('Hall'):
 
68
  current = None
69
+ for _, row in group.sort_values('StartTime_dt').iterrows():
70
  if current is None:
71
  current = row.copy()
72
+ elif row['Movie'] == current['Movie']:
73
+ current['EndTime_dt'] = row['EndTime_dt']
74
  else:
75
+ merged_rows.append(current)
76
+ current = row.copy()
 
 
 
77
  if current is not None:
78
  merged_rows.append(current)
79
+
80
  if not merged_rows:
81
  return None, date_str
82
 
83
  merged_df = pd.DataFrame(merged_rows)
84
 
85
+ # Adjust times as per original logic
86
  merged_df['StartTime_dt'] -= timedelta(minutes=10)
87
  merged_df['EndTime_dt'] -= timedelta(minutes=5)
88
 
89
+ merged_df['StartTime_str'] = merged_df['StartTime_dt'].dt.strftime('%H:%M')
90
+ merged_df['EndTime_str'] = merged_df['EndTime_dt'].dt.strftime('%H:%M')
 
 
91
 
92
+ return merged_df[['Hall', 'Movie', 'StartTime_str', 'EndTime_str']], date_str
 
93
  except Exception as e:
94
+ st.error(f"Error processing the file: {e}")
95
  return None, date_str
96
 
97
+ def get_text_dimensions(text, font_props, fig):
98
+ """Helper function to calculate the width and height of a text string in inches."""
99
+ renderer = fig.canvas.get_renderer()
100
+ t = fig.text(0, 0, text, fontproperties=font_props, visible=False)
101
+ bbox = t.get_window_extent(renderer=renderer)
102
+ t.remove()
103
+ return bbox.width / fig.dpi, bbox.height / fig.dpi
104
+
105
+ def find_font_size(target_height_inches, font_path, fig, text_to_measure="Xg"):
106
+ """Finds the font size (in points) that results in a given text height (in inches)."""
107
+ low, high = 0, 100 # Binary search range for font size
108
+ best_size = 10
109
+
110
+ for _ in range(10): # 10 iterations for precision
111
+ mid = (low + high) / 2
112
+ if mid <= 0: break
113
+ props = font_manager.FontProperties(fname=font_path, size=mid)
114
+ _, h = get_text_dimensions(text_to_measure, props, fig)
115
+ if h > target_height_inches:
116
+ high = mid
117
+ else:
118
+ best_size = mid
119
+ low = mid
120
+ return best_size
121
 
 
122
  def create_print_layout(data, date_str):
123
+ """Creates the PNG and PDF print layouts based on the new grid requirements."""
124
  if data is None or data.empty:
125
  return None
126
 
127
+ A4_WIDTH_IN, A4_HEIGHT_IN = 8.27, 11.69
 
128
 
129
+ # 1. Layout Calculation
130
+ total_A = len(data) + 2 # Total rows + top/bottom margin
131
+ row_height = A4_HEIGHT_IN / total_A
132
+ side_margin_width = row_height # Left/right blank columns width
133
+ content_width = A4_WIDTH_IN - 2 * side_margin_width
134
+ target_font_height = row_height * 0.9
135
+
136
+ # --- Calculate font sizes and column widths ---
137
+ dummy_fig = plt.figure()
138
+ font_path = "simHei.ttc" if os.path.exists("simHei.ttc") else "SimHei.ttf"
139
+ master_font_size = find_font_size(target_font_height, font_path, dummy_fig)
140
+ master_font = get_font(size=master_font_size)
141
+
142
+ # Prepare content to measure for max width
143
+ data['Pinyin'] = data['Movie'].apply(get_pinyin_abbr)
144
+ data['TimeStr'] = data['StartTime_str'] + ' - ' + data['EndTime_str']
145
 
146
+ max_hall_content = max(data['Hall'].str.replace('号', ''), key=len) + "#"
147
+ max_seq_content = f"{data.groupby('Hall').size().max()}."
148
+ max_pinyin_content = max(data['Pinyin'], key=len) if not data['Pinyin'].empty else "PY"
149
+ max_time_content = max(data['TimeStr'], key=len)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
150
 
151
+ w_hall, _ = get_text_dimensions(max_hall_content, master_font, dummy_fig)
152
+ w_seq, _ = get_text_dimensions(max_seq_content, master_font, dummy_fig)
153
+ w_pinyin, _ = get_text_dimensions(max_pinyin_content, master_font, dummy_fig)
154
+ w_time, _ = get_text_dimensions(max_time_content, master_font, dummy_fig)
155
+ plt.close(dummy_fig)
156
+
157
+ col_width_hall = w_hall * 1.1
158
+ col_width_seq = w_seq * 1.1
159
+ col_width_pinyin = w_pinyin * 1.1
160
+ col_width_time = w_time * 1.1
161
+
162
+ col_width_movie = content_width - (col_width_hall + col_width_seq + col_width_pinyin + col_width_time)
163
 
164
+ if col_width_movie <= 0:
165
+ st.error("Content is too wide for the page. The movie title column has no space.")
166
+ return None
167
 
168
+ # --- Create figures for drawing ---
169
+ output_figs = {}
170
  for fmt in ['png', 'pdf']:
171
+ fig = plt.figure(figsize=(A4_WIDTH_IN, A4_HEIGHT_IN), dpi=300)
172
  ax = fig.add_subplot(111)
173
  ax.set_axis_off()
174
  fig.subplots_adjust(left=0, right=1, top=1, bottom=0)
175
+ ax.set_xlim(0, A4_WIDTH_IN)
176
+ ax.set_ylim(0, A4_HEIGHT_IN)
177
+
178
+ # Define column x-positions (from left to right)
179
+ x_pos = [0] * 7
180
+ x_pos[0] = 0
181
+ x_pos[1] = side_margin_width
182
+ x_pos[2] = x_pos[1] + col_width_hall
183
+ x_pos[3] = x_pos[2] + col_width_seq
184
+ x_pos[4] = x_pos[3] + col_width_movie
185
+ x_pos[5] = x_pos[4] + col_width_pinyin
186
+ x_pos[6] = x_pos[5] + col_width_time
187
+
188
+ # Add date string to the top margin area
189
+ ax.text(A4_WIDTH_IN - side_margin_width, A4_HEIGHT_IN - (row_height / 2), date_str,
190
+ ha='right', va='center', color='#A9A9A9', fontproperties=get_font(12))
191
+
192
+ # --- Drawing Loop ---
193
+ y_cursor = A4_HEIGHT_IN - row_height # Start below top margin
194
+ drawn_halls = set()
195
 
196
+ for hall_name, group in data.groupby('Hall', sort=False):
197
+ for i, (_, row) in enumerate(group.iterrows()):
198
+ y_bottom = y_cursor - row_height
199
+ y_center = y_cursor - (row_height / 2)
200
+
201
+ # Draw dotted cell borders
202
+ for c in range(5):
203
+ ax.add_patch(Rectangle((x_pos[c+1], y_bottom), x_pos[c+2] - x_pos[c+1], row_height,
204
+ facecolor='none', edgecolor='gray', linestyle=':', linewidth=0.5))
205
+
206
+ # Column 1: Hall Number (only on first row of the group)
207
+ if hall_name not in drawn_halls:
208
+ hall_num_text = f"${row['Hall'].replace('号', '')}^{{\\#}}$"
209
+ ax.text(x_pos[1] + col_width_hall / 2, y_center, hall_num_text,
210
+ ha='center', va='center', fontproperties=master_font)
211
+ drawn_halls.add(hall_name)
212
+
213
+ # Column 2: Sequence Number
214
+ ax.text(x_pos[2] + col_width_seq / 2, y_center, f"{i+1}.",
215
+ ha='center', va='center', fontproperties=master_font)
216
 
217
+ # Column 3: Movie Title (with font scaling)
218
+ font_movie = master_font.copy()
219
+ movie_cell_inner_width = col_width_movie * 0.9
220
+
221
+ temp_fig = plt.figure()
222
+ w_movie, _ = get_text_dimensions(row['Movie'], font_movie, temp_fig)
223
+ plt.close(temp_fig)
224
 
225
+ if w_movie > movie_cell_inner_width:
226
+ scale_factor = movie_cell_inner_width / w_movie
227
+ font_movie.set_size(font_movie.get_size() * scale_factor)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
228
 
229
+ ax.text(x_pos[3] + col_width_movie * 0.05, y_center, row['Movie'],
230
+ ha='left', va='center', fontproperties=font_movie)
 
 
 
 
 
 
 
 
 
 
231
 
232
+ # Column 4: Pinyin Abbreviation
233
+ ax.text(x_pos[4] + col_width_pinyin / 2, y_center, row['Pinyin'],
234
+ ha='center', va='center', fontproperties=master_font)
235
+
236
+ # Column 5: Time
237
+ ax.text(x_pos[5] + col_width_time / 2, y_center, row['TimeStr'],
238
+ ha='center', va='center', fontproperties=master_font)
239
 
240
+ y_cursor -= row_height
241
 
242
+ # Draw black separator line below each hall group
243
+ ax.plot([x_pos[1], x_pos[6]], [y_cursor, y_cursor], color='black', linewidth=1.0)
244
+
245
+ output_figs[fmt] = fig
 
 
 
 
246
 
247
+ # --- Save figures to in-memory buffers ---
248
  png_buffer = io.BytesIO()
249
+ output_figs['png'].savefig(png_buffer, format='png', bbox_inches=None, pad_inches=0)
250
+ plt.close(output_figs['png'])
251
+
252
+ pdf_buffer = io.BytesIO()
253
+ output_figs['pdf'].savefig(pdf_buffer, format='pdf', bbox_inches=None, pad_inches=0)
254
+ plt.close(output_figs['pdf'])
255
+
256
+ # Encode for display
257
  png_buffer.seek(0)
258
  image_base64 = base64.b64encode(png_buffer.getvalue()).decode()
 
 
 
 
259
  pdf_buffer.seek(0)
260
  pdf_base64 = base64.b64encode(pdf_buffer.getvalue()).decode()
261
+
 
262
  return {
263
  'png': f"data:image/png;base64,{image_base64}",
264
  'pdf': f"data:application/pdf;base64,{pdf_base64}"
265
  }
266
 
267
+ def display_pdf(base64_pdf):
268
+ """Generates the HTML to embed a PDF in Streamlit."""
269
+ return f'<iframe src="{base64_pdf}" width="100%" height="800" type="application/pdf"></iframe>'
270
 
271
+ # --- Streamlit App ---
272
+ st.set_page_config(page_title="LED Screen Schedule Printing", layout="wide")
273
+ st.title("LED Screen Schedule Printing")
 
274
 
275
+ uploaded_file = st.file_uploader("Select the 'Screening Time Checklist.xls' file", type=["xls"])
276
 
277
  if uploaded_file:
278
+ with st.spinner("Processing file, please wait..."):
279
  schedule, date_str = process_schedule(uploaded_file)
280
  if schedule is not None and not schedule.empty:
281
  output = create_print_layout(schedule, date_str)
282
+ if output:
283
+ tab1, tab2 = st.tabs(["PDF Preview", "PNG Preview"])
284
+ with tab1:
285
+ st.markdown(display_pdf(output['pdf']), unsafe_allow_html=True)
286
+ with tab2:
287
+ st.image(output['png'], use_container_width=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
288
  else:
289
+ st.error("Could not process the file. Please check that the file format and content are correct.")