Ethscriptions commited on
Commit
ee0147b
·
verified ·
1 Parent(s): a215102

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +152 -350
app.py CHANGED
@@ -6,78 +6,71 @@ import io
6
  import base64
7
  import os
8
  from datetime import datetime, timedelta
9
- import math
10
  from pypinyin import lazy_pinyin, Style
11
  from matplotlib.backends.backend_pdf import PdfPages
12
- import matplotlib.gridspec as gridspec
13
- from matplotlib.patches import FancyBboxPatch
14
 
15
- # --- Constants for "Quick Print" (放映场次核对表) ---
16
- SPLIT_TIME = "17:30"
17
- BUSINESS_START = "09:30"
18
- BUSINESS_END = "01:30"
19
- BORDER_COLOR = '#A9A9A9'
20
- DATE_COLOR = '#A9A9A9'
21
-
22
- # --- Helper functions for "LED Screen" (放映时间核对表) ---
23
  def get_font(size=14):
24
- """Loads a specific font file, falling back to a default if not found."""
 
 
 
25
  font_path = "simHei.ttc"
26
  if not os.path.exists(font_path):
27
- font_path = "SimHei.ttf" # Fallback font
28
- # Add a final fallback for systems without Chinese fonts
29
- try:
30
- return font_manager.FontProperties(fname=font_path, size=size)
31
- except RuntimeError:
32
- # If the font file is not found, use a default font that should exist.
33
- # This will likely not render Chinese characters correctly but prevents crashing.
34
- return font_manager.FontProperties(family='sans-serif', size=size)
35
-
36
 
37
  def get_pinyin_abbr(text):
38
- """Gets the first letter of the Pinyin for the first two Chinese characters of a text."""
 
 
39
  if not text:
40
  return ""
41
  # Extract the first two Chinese characters
42
  chars = [c for c in text if '\u4e00' <= c <= '\u9fff']
43
  chars = chars[:2]
44
- # Get the first letter of the pinyin for each character
45
  pinyin_list = lazy_pinyin(chars, style=Style.FIRST_LETTER)
46
  return ''.join(pinyin_list).upper()
47
 
48
- # --- Processing logic for "LED Screen" (放映时间核对表) ---
49
- def process_schedule_led(file):
50
- """Processes the '放映时间核对表.xls' file."""
 
51
  try:
52
- # Attempt to read the date from a specific cell
53
  date_df = pd.read_excel(file, header=None, skiprows=7, nrows=1, usecols=[3])
54
  date_str = pd.to_datetime(date_df.iloc[0, 0]).strftime('%Y-%m-%d')
55
  base_date = pd.to_datetime(date_str).date()
56
  except Exception:
57
- # Fallback to the current date if reading fails
58
  date_str = datetime.today().strftime('%Y-%m-%d')
59
  base_date = datetime.today().date()
 
60
 
61
  try:
62
  df = pd.read_excel(file, header=9, usecols=[1, 2, 4, 5])
63
  df.columns = ['Hall', 'StartTime', 'EndTime', 'Movie']
 
64
  df['Hall'] = df['Hall'].ffill()
65
  df.dropna(subset=['StartTime', 'EndTime', 'Movie'], inplace=True)
66
- df['Hall'] = df['Hall'].astype(str).str.extract(r'(\d+号)')
 
 
67
 
68
- # Convert times to datetime objects, handling overnight screenings
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.loc[df['EndTime_dt'] < df['StartTime_dt'], 'EndTime_dt'] += timedelta(days=1)
76
- df = df.sort_values(['Hall', 'StartTime_dt'])
77
-
78
  # Merge consecutive screenings of the same movie
79
  merged_rows = []
80
- for hall, group in df.groupby('Hall'):
81
  group = group.sort_values('StartTime_dt')
82
  current = None
83
  for _, row in group.iterrows():
@@ -91,108 +84,135 @@ def process_schedule_led(file):
91
  current = row.copy()
92
  if current is not None:
93
  merged_rows.append(current)
 
 
 
94
 
95
  merged_df = pd.DataFrame(merged_rows)
96
-
97
  # Adjust start and end times
98
  merged_df['StartTime_dt'] = merged_df['StartTime_dt'] - timedelta(minutes=10)
99
  merged_df['EndTime_dt'] = merged_df['EndTime_dt'] - timedelta(minutes=5)
 
 
 
 
 
 
 
 
100
 
101
- merged_df['StartTime_str'] = merged_df['StartTime_dt'].dt.strftime('%H:%M')
102
- merged_df['EndTime_str'] = merged_df['EndTime_dt'].dt.strftime('%H:%M')
103
-
104
- return merged_df[['Hall', 'Movie', 'StartTime_str', 'EndTime_str']], date_str
105
  except Exception as e:
106
- st.error(f"An error occurred during file processing: {e}")
107
  return None, date_str
108
 
109
- # --- Layout generation for "LED Screen" (放映时间核对表) ---
110
- def create_print_layout_led(data, date_str):
111
- """Generates PNG and PDF layouts for the 'LED Screen' schedule."""
 
112
  if data is None or data.empty:
113
  return None
114
 
115
- # Create figures for PNG and PDF output with A4 dimensions
116
- png_fig = plt.figure(figsize=(8.27, 11.69), dpi=300)
117
- png_ax = png_fig.add_subplot(111)
118
- png_ax.set_axis_off()
119
- png_fig.subplots_adjust(left=0.02, right=0.98, top=0.98, bottom=0.02)
120
-
121
- pdf_fig = plt.figure(figsize=(8.27, 11.69), dpi=300)
122
- pdf_ax = pdf_fig.add_subplot(111)
123
- pdf_ax.set_axis_off()
124
- pdf_fig.subplots_adjust(left=0.02, right=0.98, top=0.98, bottom=0.02)
125
-
126
- def process_figure(fig, ax):
127
- halls = sorted(data['Hall'].unique(), key=lambda h: int(h.replace('号','')) if h else 0)
128
-
129
- num_separators = len(halls) - 1
130
- total_layout_rows = len(data) + num_separators + 2
131
 
132
- available_height = 0.96
133
- row_height = available_height / total_layout_rows
134
-
135
- fig_height_inches = fig.get_figheight()
136
- row_height_points = row_height * fig_height_inches * 72
137
- font_size = row_height_points * 0.9
138
-
139
- date_font = get_font(font_size * 0.8)
140
- hall_font = get_font(font_size)
141
- movie_font = get_font(font_size)
142
-
143
- col_hall_left = 0.0
144
- col_movie_right = 0.50
145
- col_seq_left = 0.52
146
- col_pinyin_left = 0.62
147
- col_time_left = 0.75
148
-
149
- ax.text(col_hall_left, 0.99, date_str, color='#A9A9A9',
150
- ha='left', va='top', fontproperties=date_font, transform=ax.transAxes)
 
 
 
 
151
 
152
- y_position = 0.98 - row_height
153
 
154
- for i, hall in enumerate(halls):
155
- hall_data = data[data['Hall'] == hall]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
156
 
157
- if i > 0:
158
- ax.axhline(y=y_position + row_height / 2, xmin=col_hall_left, xmax=0.97, color='black', linewidth=0.7)
159
- y_position -= row_height
160
-
161
- movie_count = 1
162
- for _, row in hall_data.iterrows():
163
- if movie_count == 1:
164
- ax.text(col_hall_left, y_position, f"{hall.replace('', '')}#",
165
- ha='left', va='center', fontweight='bold',
166
- fontproperties=hall_font, transform=ax.transAxes)
167
-
168
- ax.text(col_movie_right, y_position, row['Movie'],
169
- ha='right', va='center', fontproperties=movie_font, transform=ax.transAxes)
170
-
171
- ax.text(col_seq_left, y_position, f"{movie_count}.",
172
- ha='left', va='center', fontproperties=movie_font, transform=ax.transAxes)
173
-
174
- pinyin_abbr = get_pinyin_abbr(row['Movie'])
175
- ax.text(col_pinyin_left, y_position, pinyin_abbr,
176
- ha='left', va='center', fontproperties=movie_font, transform=ax.transAxes)
177
-
178
- ax.text(col_time_left, y_position, f"{row['StartTime_str']}-{row['EndTime_str']}",
179
- ha='left', va='center', fontproperties=movie_font, transform=ax.transAxes)
180
-
181
- y_position -= row_height
182
- movie_count += 1
183
 
184
- process_figure(png_fig, png_ax)
185
- process_figure(pdf_fig, pdf_ax)
 
 
 
 
 
 
 
 
186
 
 
187
  png_buffer = io.BytesIO()
188
  png_fig.savefig(png_buffer, format='png', bbox_inches='tight', pad_inches=0.05)
189
  png_buffer.seek(0)
190
  image_base64 = base64.b64encode(png_buffer.getvalue()).decode()
191
  plt.close(png_fig)
192
 
 
193
  pdf_buffer = io.BytesIO()
194
- with PdfPages(pdf_buffer) as pdf:
195
- pdf.savefig(pdf_fig, bbox_inches='tight', pad_inches=0.05)
196
  pdf_buffer.seek(0)
197
  pdf_base64 = base64.b64encode(pdf_buffer.getvalue()).decode()
198
  plt.close(pdf_fig)
@@ -202,252 +222,34 @@ def create_print_layout_led(data, date_str):
202
  'pdf': f"data:application/pdf;base64,{pdf_base64}"
203
  }
204
 
205
- # --- Processing logic for "Quick Print" (放映场次核对表) ---
206
- def process_schedule_quick(file):
207
- """Processes the '放映场次核对表.xls' file."""
208
- try:
209
- df = pd.read_excel(file, skiprows=8)
210
- df = df.iloc[:, [6, 7, 9]]
211
- df.columns = ['Hall', 'StartTime', 'EndTime']
212
- df = df.dropna(subset=['Hall', 'StartTime', 'EndTime'])
213
- df['Hall'] = df['Hall'].str.extract(r'(\d+)号').astype(str) + ' '
214
-
215
- base_date = datetime.today().date()
216
- df['StartTime'] = pd.to_datetime(df['StartTime'])
217
- df['EndTime'] = pd.to_datetime(df['EndTime'])
218
-
219
- business_start = datetime.strptime(f"{base_date} {BUSINESS_START}", "%Y-%m-%d %H:%M")
220
- business_end = datetime.strptime(f"{base_date} {BUSINESS_END}", "%Y-%m-%d %H:%M")
221
- if business_end < business_start:
222
- business_end += timedelta(days=1)
223
-
224
- for idx, row in df.iterrows():
225
- end_time = row['EndTime']
226
- if end_time.hour < 9:
227
- df.at[idx, 'EndTime'] = end_time + timedelta(days=1)
228
- if row['StartTime'].hour >= 21 and end_time.hour < 9:
229
- df.at[idx, 'EndTime'] = end_time + timedelta(days=1)
230
-
231
- df['time_for_comparison'] = df['EndTime'].apply(lambda x: datetime.combine(base_date, x.time()))
232
- df.loc[df['time_for_comparison'].dt.hour < 9, 'time_for_comparison'] += timedelta(days=1)
233
-
234
- valid_times = (
235
- (df['time_for_comparison'] >= datetime.combine(base_date, business_start.time())) &
236
- (df['time_for_comparison'] <= datetime.combine(base_date + timedelta(days=1), business_end.time()))
237
- )
238
- df = df[valid_times]
239
- df = df.sort_values('EndTime')
240
-
241
- split_time_dt = datetime.strptime(f"{base_date} {SPLIT_TIME}", "%Y-%m-%d %H:%M")
242
-
243
- part1 = df[df['time_for_comparison'] <= split_time_dt].copy()
244
- part2 = df[df['time_for_comparison'] > split_time_dt].copy()
245
-
246
- for part in [part1, part2]:
247
- part['EndTime'] = part['EndTime'].dt.strftime('%-H:%M')
248
-
249
- date_df = pd.read_excel(file, skiprows=5, nrows=1, usecols=[2], header=None)
250
- date_cell = date_df.iloc[0, 0]
251
-
252
- try:
253
- if isinstance(date_cell, str):
254
- date_str = datetime.strptime(date_cell, '%Y-%m-%d').strftime('%Y-%m-%d')
255
- else:
256
- date_str = pd.to_datetime(date_cell).strftime('%Y-%m-%d')
257
- except:
258
- date_str = datetime.today().strftime('%Y-%m-%d')
259
-
260
- return part1[['Hall', 'EndTime']], part2[['Hall', 'EndTime']], date_str
261
-
262
- except Exception as e:
263
- st.error(f"处理文件时出错: {str(e)}")
264
- return None, None, None
265
-
266
- # --- Layout generation for "Quick Print" (放映场次核对表) ---
267
- def create_print_layout_quick(data, title, date_str):
268
- """Creates print layout for the 'Quick Print' schedule."""
269
- if data.empty:
270
- return None
271
-
272
- png_fig = plt.figure(figsize=(5.83, 8.27), dpi=300) # A5
273
- png_fig.subplots_adjust(left=0.02, right=0.98, top=0.98, bottom=0.02)
274
-
275
- pdf_fig = plt.figure(figsize=(5.83, 8.27), dpi=300) # A5
276
- pdf_fig.subplots_adjust(left=0.02, right=0.98, top=0.98, bottom=0.02)
277
-
278
- def process_figure(fig, is_pdf=False):
279
- plt.rcParams['font.family'] = 'sans-serif'
280
- plt.rcParams['font.sans-serif'] = ['Arial Unicode MS', 'Heiti TC', 'SimHei']
281
-
282
- total_items = len(data)
283
- num_cols = 3
284
- num_rows = math.ceil(total_items / num_cols)
285
-
286
- gs = gridspec.GridSpec(num_rows + 1, num_cols, hspace=0.05, wspace=0.05, height_ratios=[0.1] + [1] * num_rows, figure=fig)
287
-
288
- target_width_px = 1
289
- if total_items > 0:
290
- ax_temp = fig.add_subplot(gs[1, 0])
291
- fig.canvas.draw()
292
- target_width_px = ax_temp.get_window_extent().width * 0.90
293
- ax_temp.remove()
294
-
295
- available_height_per_row = (8.27 * 0.9 * (1 / 1.2)) / num_rows if num_rows > 0 else 1
296
- date_fontsize = min(40, max(10, available_height_per_row * 72 * 0.5))
297
-
298
- data_values = data.values.tolist()
299
- while len(data_values) % num_cols != 0:
300
- data_values.append(['', ''])
301
- rows_per_col_layout = math.ceil(len(data_values) / num_cols)
302
-
303
- sorted_data = [['', '']] * len(data_values)
304
- for i, item in enumerate(data_values):
305
- if item[0] and item[1]:
306
- row_in_col = i % rows_per_col_layout
307
- col_idx = i // rows_per_col_layout
308
- new_index = row_in_col * num_cols + col_idx
309
- if new_index < len(sorted_data):
310
- sorted_data[new_index] = item
311
-
312
- for idx, (hall, end_time) in enumerate(sorted_data):
313
- if hall and end_time:
314
- row_grid = idx // num_cols + 1
315
- col_grid = idx % num_cols
316
-
317
- if row_grid < num_rows + 1:
318
- ax = fig.add_subplot(gs[row_grid, col_grid])
319
- for spine in ax.spines.values():
320
- spine.set_visible(False)
321
-
322
- bbox = FancyBboxPatch(
323
- (0.01, 0.01), 0.98, 0.98,
324
- boxstyle="round,pad=0,rounding_size=0.02",
325
- edgecolor=BORDER_COLOR, facecolor='none',
326
- linewidth=0.5, transform=ax.transAxes, clip_on=False
327
- )
328
- ax.add_patch(bbox)
329
-
330
- display_text = f"{hall}{end_time}"
331
- t = ax.text(0.5, 0.5, display_text,
332
- fontweight='bold', ha='center', va='center',
333
- transform=ax.transAxes)
334
-
335
- current_size = 120
336
- while current_size > 1:
337
- t.set_fontsize(current_size)
338
- text_bbox = t.get_window_extent(renderer=fig.canvas.get_renderer())
339
- if text_bbox.width <= target_width_px:
340
- break
341
- current_size -= 2
342
-
343
- ax.set_xticks([])
344
- ax.set_yticks([])
345
-
346
- ax_date = fig.add_subplot(gs[0, :])
347
- ax_date.text(0.01, 0.5, f"{date_str} {title}",
348
- fontsize=date_fontsize * 0.5,
349
- color=DATE_COLOR, fontweight='bold',
350
- ha='left', va='center', transform=ax_date.transAxes)
351
- for spine in ax_date.spines.values():
352
- spine.set_visible(False)
353
- ax_date.set_xticks([])
354
- ax_date.set_yticks([])
355
- ax_date.set_facecolor('none')
356
-
357
- process_figure(png_fig)
358
- process_figure(pdf_fig, is_pdf=True)
359
-
360
- png_buffer = io.BytesIO()
361
- png_fig.savefig(png_buffer, format='png', bbox_inches='tight', pad_inches=0.02)
362
- png_buffer.seek(0)
363
- png_base64 = base64.b64encode(png_buffer.getvalue()).decode()
364
- plt.close(png_fig)
365
-
366
- pdf_buffer = io.BytesIO()
367
- with PdfPages(pdf_buffer) as pdf:
368
- pdf.savefig(pdf_fig, bbox_inches='tight', pad_inches=0.02)
369
- pdf_buffer.seek(0)
370
- pdf_base64 = base64.b64encode(pdf_buffer.getvalue()).decode()
371
- plt.close(pdf_fig)
372
-
373
- return {
374
- 'png': f'data:image/png;base64,{png_base64}',
375
- 'pdf': f'data:application/pdf;base64,{pdf_base64}'
376
- }
377
-
378
- # --- Generic Helper to Display PDF ---
379
  def display_pdf(base64_pdf):
380
- """Generates the HTML to embed and display a PDF in Streamlit."""
381
- pdf_display = f"""
382
- <iframe src="{base64_pdf}" width="100%" height="800" type="application/pdf"></iframe>
383
- """
384
  return pdf_display
385
 
386
- # --- Main Streamlit App ---
387
- st.set_page_config(page_title="影院排期打印工具", layout="wide")
388
- st.title("影院排期打印工具")
389
 
390
- uploaded_file = st.file_uploader(
391
- "选择【放映时间核对表.xls】或【放映场次核对表.xls】文件",
392
- accept_multiple_files=False,
393
- type=["xls"]
394
- )
395
 
396
  if uploaded_file:
397
  with st.spinner("文件正在处理中,请稍候..."):
398
- # --- Route to the correct processor based on filename ---
399
-
400
- # 1. Logic for "LED 屏幕时间表打印"
401
- if "放映时间核对表" in uploaded_file.name:
402
- st.subheader("LED 屏幕时间表")
403
- schedule, date_str = process_schedule_led(uploaded_file)
404
- if schedule is not None:
405
- output = create_print_layout_led(schedule, date_str)
406
- if output:
407
- tab1, tab2 = st.tabs(["PDF 预览", "PNG 预览"])
408
- with tab1:
409
- st.markdown(display_pdf(output['pdf']), unsafe_allow_html=True)
410
- with tab2:
411
- st.image(output['png'], use_container_width=True)
412
- else:
413
- st.info("没有可显示的数据。")
414
- else:
415
- st.error("无法处理文件,请检查文件格式或内容是否正确。")
416
-
417
- # 2. Logic for "散厅时间快捷打印"
418
- elif "放映场次核对表" in uploaded_file.name:
419
- part1_data, part2_data, date_str = process_schedule_quick(uploaded_file)
420
-
421
- if part1_data is not None and part2_data is not None:
422
- part1_output = create_print_layout_quick(part1_data, "A", date_str)
423
- part2_output = create_print_layout_quick(part2_data, "C", date_str)
424
-
425
- col1, col2 = st.columns(2)
426
-
427
- with col1:
428
- st.subheader("白班散场预览(时间 ≤ 17:30)")
429
- if part1_output:
430
- tab1_1, tab1_2 = st.tabs(["PDF 预览 ", "PNG 预览 "]) # Added space to make keys unique
431
- with tab1_1:
432
- st.markdown(display_pdf(part1_output['pdf']), unsafe_allow_html=True)
433
- with tab1_2:
434
- st.image(part1_output['png'])
435
- else:
436
- st.info("白班部分没有数据")
437
-
438
- with col2:
439
- st.subheader("夜班散场预览(时间 > 17:30)")
440
- if part2_output:
441
- tab2_1, tab2_2 = st.tabs(["PDF 预览 ", "PNG 预览 "]) # Added spaces to make keys unique
442
- with tab2_1:
443
- st.markdown(display_pdf(part2_output['pdf']), unsafe_allow_html=True)
444
- with tab2_2:
445
- st.image(part2_output['png'])
446
- else:
447
- st.info("夜班部分没有数据")
448
  else:
449
- st.error("无法处理文件,请检查文件格式或内容是否正确。")
450
-
451
- # 3. Fallback for incorrect file
452
  else:
453
- st.warning("文件名不匹配。请上传名为【放映时间核对表.xls】或【放映场次核对表.xls】的文件。")
 
 
6
  import base64
7
  import os
8
  from datetime import datetime, timedelta
 
9
  from pypinyin import lazy_pinyin, Style
10
  from matplotlib.backends.backend_pdf import PdfPages
 
 
11
 
 
 
 
 
 
 
 
 
12
  def get_font(size=14):
13
+ """
14
+ Retrieves the specified font properties. Looks for 'simHei.ttc' first,
15
+ then falls back to 'SimHei.ttf'.
16
+ """
17
  font_path = "simHei.ttc"
18
  if not os.path.exists(font_path):
19
+ font_path = "SimHei.ttf"
20
+ return font_manager.FontProperties(fname=font_path, size=size)
 
 
 
 
 
 
 
21
 
22
  def get_pinyin_abbr(text):
23
+ """
24
+ Gets the first letter of the Pinyin for the first two Chinese characters of a text.
25
+ """
26
  if not text:
27
  return ""
28
  # Extract the first two Chinese characters
29
  chars = [c for c in text if '\u4e00' <= c <= '\u9fff']
30
  chars = chars[:2]
31
+ # Get the first letter of the Pinyin for each character
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 data.
38
+ """
39
  try:
40
+ # Try to read the date from the Excel file
41
  date_df = pd.read_excel(file, header=None, skiprows=7, nrows=1, usecols=[3])
42
  date_str = pd.to_datetime(date_df.iloc[0, 0]).strftime('%Y-%m-%d')
43
  base_date = pd.to_datetime(date_str).date()
44
  except Exception:
45
+ # Fallback to today's date if reading fails
46
  date_str = datetime.today().strftime('%Y-%m-%d')
47
  base_date = datetime.today().date()
48
+ file.seek(0) # Reset file pointer after failed read attempt
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
  df['Hall'] = df['Hall'].ffill()
55
  df.dropna(subset=['StartTime', 'EndTime', 'Movie'], inplace=True)
56
+
57
+ df['Hall_Num'] = df['Hall'].astype(str).str.extract(r'(\d+)').astype(int)
58
+ df['Hall'] = df['Hall_Num'].astype(str) + '号'
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.loc[df['EndTime_dt'] < df['StartTime_dt'], 'EndTime_dt'] += timedelta(days=1)
69
+ df = df.sort_values(['Hall_Num', 'StartTime_dt'])
70
+
71
  # Merge consecutive screenings of the same movie
72
  merged_rows = []
73
+ for _, group in df.groupby('Hall_Num'):
74
  group = group.sort_values('StartTime_dt')
75
  current = None
76
  for _, row in group.iterrows():
 
84
  current = row.copy()
85
  if current is not None:
86
  merged_rows.append(current)
87
+
88
+ if not merged_rows:
89
+ return None, date_str
90
 
91
  merged_df = pd.DataFrame(merged_rows)
92
+
93
  # Adjust start and end times
94
  merged_df['StartTime_dt'] = merged_df['StartTime_dt'] - timedelta(minutes=10)
95
  merged_df['EndTime_dt'] = merged_df['EndTime_dt'] - timedelta(minutes=5)
96
+
97
+ # Create final data columns for the table
98
+ merged_df['Sequence'] = merged_df.groupby('Hall_Num').cumcount() + 1
99
+ merged_df['Pinyin'] = merged_df['Movie'].apply(get_pinyin_abbr)
100
+ merged_df['Time'] = merged_df['StartTime_dt'].dt.strftime('%H:%M') + ' - ' + merged_df['EndTime_dt'].dt.strftime('%H:%M')
101
+
102
+ final_df = merged_df[['Hall', 'Hall_Num', 'Sequence', 'Movie', 'Pinyin', 'Time']].copy()
103
+ final_df = final_df.sort_values(['Hall_Num', 'Sequence']).reset_index(drop=True)
104
 
105
+ return final_df, date_str
 
 
 
106
  except Exception as e:
107
+ st.error(f"An error occurred during data processing: {e}")
108
  return None, date_str
109
 
110
+ def create_print_layout(data, date_str):
111
+ """
112
+ Generates the print layout as PNG and PDF based on the processed data.
113
+ """
114
  if data is None or data.empty:
115
  return None
116
 
117
+ def draw_table_on_ax(fig, ax):
118
+ """Helper function to draw the table on a given Matplotlib axis."""
119
+ ax.set_axis_off()
 
 
 
 
 
 
 
 
 
 
 
 
 
120
 
121
+ num_rows = len(data)
122
+ if num_rows == 0:
123
+ return
124
+
125
+ # --- Layout & Sizing Calculations ---
126
+ # Define printable area using relative coordinates (fractions of the page)
127
+ TOP_MARGIN, BOTTOM_MARGIN = 0.96, 0.04
128
+ LEFT_MARGIN, RIGHT_MARGIN = 0.04, 0.96
129
+ TABLE_HEIGHT = TOP_MARGIN - BOTTOM_MARGIN
130
+ TABLE_WIDTH = RIGHT_MARGIN - LEFT_MARGIN
131
+
132
+ # Total rows for calculation: content rows + 2 for top/bottom buffer
133
+ total_layout_rows = num_rows + 2
134
+ row_height = TABLE_HEIGHT / total_layout_rows
135
+
136
+ # Font size is 90% of the calculated row height
137
+ # (row_height is a fraction of figure height, convert to points)
138
+ font_size_pt = (row_height * fig.get_figheight() * 0.9) * 72
139
+ font = get_font(size=font_size_pt)
140
+
141
+ # Relative column widths
142
+ col_relative_widths = {'hall': 1.2, 'seq': 0.8, 'movie': 5.0, 'pinyin': 1.5, 'time': 2.5}
143
+ total_rel_width = sum(col_relative_widths.values())
144
 
145
+ col_widths = {k: (v / total_rel_width) * TABLE_WIDTH for k, v in col_relative_widths.items()}
146
 
147
+ # Calculate the absolute x-position for the left edge of each column
148
+ x_pos = {}
149
+ current_x = LEFT_MARGIN
150
+ col_order = ['hall', 'seq', 'movie', 'pinyin', 'time']
151
+ for col_name in col_order:
152
+ x_pos[col_name] = current_x
153
+ current_x += col_widths[col_name]
154
+ x_pos['end'] = current_x # The right edge of the last column
155
+
156
+ # Add date header
157
+ ax.text(LEFT_MARGIN, TOP_MARGIN, date_str, fontsize=12, color='#A9A9A9',
158
+ ha='left', va='top', fontproperties=get_font(12), transform=ax.transAxes)
159
+
160
+ # --- Drawing Loop ---
161
+ for i, row in data.iterrows():
162
+ # Calculate y-position for the center of the current row
163
+ y_center = TOP_MARGIN - (i + 1.5) * row_height # +1.5 to account for header space
164
+ y_top = y_center + row_height / 2
165
+ y_bottom = y_center - row_height / 2
166
+
167
+ # --- 1. Draw Cell Content ---
168
+ # Column content is ordered as per requirement
169
+ ax.text(x_pos['hall'] + col_widths['hall']/2, y_center, f"{row['Hall']}", va='center', ha='center', fontproperties=font, transform=ax.transAxes)
170
+ ax.text(x_pos['seq'] + col_widths['seq']/2, y_center, f"{row['Sequence']}", va='center', ha='center', fontproperties=font, transform=ax.transAxes)
171
+ ax.text(x_pos['movie'] + 0.01, y_center, f"{row['Movie']}", va='center', ha='left', fontproperties=font, transform=ax.transAxes, clip_on=True)
172
+ ax.text(x_pos['pinyin'] + col_widths['pinyin']/2, y_center, f"{row['Pinyin']}", va='center', ha='center', fontproperties=font, transform=ax.transAxes)
173
+ ax.text(x_pos['time'] + col_widths['time']/2, y_center, f"{row['Time']}", va='center', ha='center', fontproperties=font, transform=ax.transAxes)
174
 
175
+ # --- 2. Draw Cell Borders ---
176
+ # Draw all vertical cell lines with gray dots
177
+ for col_name in col_order:
178
+ ax.plot([x_pos[col_name], x_pos[col_name]], [y_bottom, y_top], color='gray', linestyle=':', linewidth=0.7)
179
+ ax.plot([x_pos['end'], x_pos['end']], [y_bottom, y_top], color='gray', linestyle=':', linewidth=0.7)
180
+
181
+ # Draw top border of the cell
182
+ ax.plot([LEFT_MARGIN, x_pos['end']], [y_top, y_top], color='gray', linestyle=':', linewidth=0.7)
183
+
184
+ # --- 3. Draw Hall Separator or Bottom Border ---
185
+ # Check if this is the last movie for the current hall
186
+ is_last_in_hall = (i == num_rows - 1) or (row['Hall_Num'] != data.iloc[i + 1]['Hall_Num'])
187
+
188
+ if is_last_in_hall:
189
+ # If it's the last entry for a hall, draw a solid black line below it
190
+ ax.plot([LEFT_MARGIN, x_pos['end']], [y_bottom, y_bottom], color='black', linestyle='-', linewidth=1.2, zorder=3)
191
+ else:
192
+ # Otherwise, draw a standard gray dotted line
193
+ ax.plot([LEFT_MARGIN, x_pos['end']], [y_bottom, y_bottom], color='gray', linestyle=':', linewidth=0.7)
 
 
 
 
 
 
 
194
 
195
+ # --- Generate PNG and PDF Outputs ---
196
+ # Create a figure for PNG output
197
+ png_fig = plt.figure(figsize=(8.27, 11.69), dpi=300)
198
+ png_ax = png_fig.add_subplot(111)
199
+ draw_table_on_ax(png_fig, png_ax)
200
+
201
+ # Create a separate figure for PDF output
202
+ pdf_fig = plt.figure(figsize=(8.27, 11.69), dpi=300)
203
+ pdf_ax = pdf_fig.add_subplot(111)
204
+ draw_table_on_ax(pdf_fig, pdf_ax)
205
 
206
+ # Save PNG to a buffer
207
  png_buffer = io.BytesIO()
208
  png_fig.savefig(png_buffer, format='png', bbox_inches='tight', pad_inches=0.05)
209
  png_buffer.seek(0)
210
  image_base64 = base64.b64encode(png_buffer.getvalue()).decode()
211
  plt.close(png_fig)
212
 
213
+ # Save PDF to a buffer
214
  pdf_buffer = io.BytesIO()
215
+ pdf_fig.savefig(pdf_buffer, format='pdf', bbox_inches='tight', pad_inches=0.05)
 
216
  pdf_buffer.seek(0)
217
  pdf_base64 = base64.b64encode(pdf_buffer.getvalue()).decode()
218
  plt.close(pdf_fig)
 
222
  'pdf': f"data:application/pdf;base64,{pdf_base64}"
223
  }
224
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
225
  def display_pdf(base64_pdf):
226
+ """Embeds the PDF in the Streamlit app for display."""
227
+ pdf_display = f'<iframe src="{base64_pdf}" width="100%" height="800" type="application/pdf"></iframe>'
 
 
228
  return pdf_display
229
 
230
+ # --- Streamlit App UI ---
231
+ st.set_page_config(page_title="LED 屏幕时间表打印", layout="wide")
232
+ st.title("LED 屏幕时间表打印")
233
 
234
+ uploaded_file = st.file_uploader("选择打开【放映时间核对表.xls】文件", accept_multiple_files=False, type=["xls", "xlsx"])
 
 
 
 
235
 
236
  if uploaded_file:
237
  with st.spinner("文件正在处理中,请稍候..."):
238
+ schedule, date_str = process_schedule(io.BytesIO(uploaded_file.getvalue()))
239
+ if schedule is not None and not schedule.empty:
240
+ output = create_print_layout(schedule, date_str)
241
+
242
+ if output:
243
+ # Create tabs to switch between PDF and PNG previews
244
+ tab1, tab2 = st.tabs(["PDF 预览", "PNG 预览"])
245
+
246
+ with tab1:
247
+ st.markdown(display_pdf(output['pdf']), unsafe_allow_html=True)
248
+
249
+ with tab2:
250
+ st.image(output['png'], use_container_width=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
251
  else:
252
+ st.error("生成打印布局时出错。")
 
 
253
  else:
254
+ st.error("无法处理文件,或文件中没有找到有效排片数据。请检查文件格式或内容是否正确。")
255
+