Ethscriptions commited on
Commit
22f8c53
·
verified ·
1 Parent(s): 4df41bd

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +229 -113
app.py CHANGED
@@ -9,20 +9,41 @@ import os
9
  from datetime import datetime, timedelta
10
  from pypinyin import lazy_pinyin, Style
11
  from matplotlib.backends.backend_pdf import PdfPages
12
-
13
- def get_font(size=14):
14
- """Loads a specific TrueType font, defaulting to a common Chinese font."""
15
- font_path = "simHei.ttc"
16
- if not os.path.exists(font_path):
17
- font_path = "SimHei.ttf"
 
 
 
 
 
 
 
 
 
 
 
 
18
  if os.path.exists(font_path):
19
  return font_manager.FontProperties(fname=font_path, size=size)
20
  else:
21
- st.warning("SimHei font not found. Display may not be correct. Please add simHei.ttc or SimHei.ttf.")
22
  return font_manager.FontProperties(family='sans-serif', 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 text:
27
  return ""
28
  chars = [c for c in text if '\u4e00' <= c <= '\u9fff'][:2]
@@ -32,22 +53,19 @@ def get_pinyin_abbr(text):
32
  return ''.join(pinyin_list).upper()
33
 
34
  def format_seq(n):
35
- """Converts an integer to a circled number string (e.g., 1 -> ①)."""
36
  if not isinstance(n, int) or n <= 0:
37
  return str(n)
38
- # Unicode characters for circled numbers 1-50
39
  circled_chars = "①②③④⑤⑥⑦⑧⑨⑩⑪⑫⑬⑭⑮⑯⑰⑱⑲⑳" \
40
  "㉑㉒㉓㉔㉕㉖㉗㉘㉙㉚㉛㉜㉝㉞㉟" \
41
  "㊱㊲㊳㊴㊵㊶㊷㊸㊹㊺㊻㊼㊽㊾㊿"
42
  if 1 <= n <= 50:
43
- return circled_chars[n-1]
44
- return f'({n})' # Fallback for numbers greater than 50
45
 
46
- def process_schedule(file):
47
- """
48
- Processes the uploaded Excel file to extract and clean the movie schedule data.
49
- Adds a sequence number for movies within each hall.
50
- """
51
  try:
52
  date_df = pd.read_excel(file, header=None, skiprows=7, nrows=1, usecols=[3])
53
  date_str = pd.to_datetime(date_df.iloc[0, 0]).strftime('%Y-%m-%d')
@@ -59,20 +77,15 @@ def process_schedule(file):
59
  try:
60
  df = pd.read_excel(file, header=9, usecols=[1, 2, 4, 5])
61
  df.columns = ['Hall', 'StartTime', 'EndTime', 'Movie']
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
-
67
  df['StartTime_dt'] = pd.to_datetime(df['StartTime'], format='%H:%M', errors='coerce').apply(
68
- lambda t: t.replace(year=base_date.year, month=base_date.month, day=base_date.day) if pd.notnull(t) else t
69
- )
70
  df['EndTime_dt'] = pd.to_datetime(df['EndTime'], format='%H:%M', errors='coerce').apply(
71
- lambda t: t.replace(year=base_date.year, month=base_date.month, day=base_date.day) if pd.notnull(t) else t
72
- )
73
  df.loc[df['EndTime_dt'] < df['StartTime_dt'], 'EndTime_dt'] += timedelta(days=1)
74
  df = df.sort_values(['Hall', 'StartTime_dt'])
75
-
76
  merged_rows = []
77
  for _, group in df.groupby('Hall'):
78
  current = None
@@ -86,134 +99,90 @@ def process_schedule(file):
86
  current = row.copy()
87
  if current is not None:
88
  merged_rows.append(current)
89
-
90
  merged_df = pd.DataFrame(merged_rows)
91
-
92
  merged_df['StartTime_dt'] -= timedelta(minutes=10)
93
  merged_df['EndTime_dt'] -= timedelta(minutes=5)
94
-
95
  merged_df['Seq'] = merged_df.groupby('Hall').cumcount() + 1
96
-
97
  merged_df['StartTime_str'] = merged_df['StartTime_dt'].dt.strftime('%H:%M')
98
  merged_df['EndTime_str'] = merged_df['EndTime_dt'].dt.strftime('%H:%M')
99
-
100
  return merged_df[['Hall', 'Seq', 'Movie', 'StartTime_str', 'EndTime_str']], date_str
101
  except Exception as e:
102
- st.error(f"Error processing schedule data: {e}. Please check the file format.")
103
  return None, date_str
104
 
105
-
106
- def create_print_layout(data, date_str):
107
- """
108
- Generates PNG and PDF print layouts for the movie schedule based on a dynamic grid.
109
- """
110
  if data is None or data.empty:
111
  return None
112
-
113
- # --- 1. Layout and Column Calculation ---
114
  A4_width_in, A4_height_in = 8.27, 11.69
115
  dpi = 300
116
-
117
  total_content_rows = len(data)
118
  totalA = total_content_rows + 2
119
  row_height = A4_height_in / totalA
120
-
121
- # Prepare data strings for width calculation
122
  data = data.reset_index(drop=True)
123
- # Format Hall number with LaTeX for superscript
124
  data['hall_str'] = '$' + data['Hall'].str.replace('号', '') + '^{\#}$'
125
- # Format sequence number as circled character
126
  data['seq_str'] = data['Seq'].apply(format_seq)
127
  data['pinyin_abbr'] = data['Movie'].apply(get_pinyin_abbr)
128
  data['time_str'] = data['StartTime_str'] + ' - ' + data['EndTime_str']
129
-
130
  temp_fig = plt.figure(figsize=(A4_width_in, A4_height_in), dpi=dpi)
131
  renderer = temp_fig.canvas.get_renderer()
132
-
133
  base_font_size_pt = (row_height * 0.9) * 72
134
- # Specific font size for circled numbers (50% of row height)
135
  seq_font_size_pt = (row_height * 0.5) * 72
136
-
137
  def get_col_width_in(series, font_size_pt, is_math=False):
138
- """Calculates the required width for a column in inches."""
139
- if series.empty:
140
- return 0
141
- font_prop = get_font(font_size_pt)
142
  longest_str_idx = series.astype(str).str.len().idxmax()
143
  max_content = str(series.loc[longest_str_idx])
144
  text_width_px, _, _ = renderer.get_text_width_height_descent(max_content, font_prop, ismath=is_math)
145
  return (text_width_px / dpi) * 1.1
146
 
147
- # Calculate widths for all columns
148
  margin_col_width = row_height
149
- # Pass is_math=True for hall number width calculation
150
  hall_col_width = get_col_width_in(data['hall_str'], base_font_size_pt, is_math=True)
151
- # Use the smaller font size for sequence number width calculation
152
  seq_col_width = get_col_width_in(data['seq_str'], seq_font_size_pt)
153
  pinyin_col_width = get_col_width_in(data['pinyin_abbr'], base_font_size_pt)
154
  time_col_width = get_col_width_in(data['time_str'], base_font_size_pt)
155
-
156
- movie_col_width = A4_width_in - (margin_col_width * 2 + hall_col_width + seq_col_width + pinyin_col_width + time_col_width)
157
-
158
  plt.close(temp_fig)
159
-
160
- col_widths = {'hall': hall_col_width, 'seq': seq_col_width, 'movie': movie_col_width, 'pinyin': pinyin_col_width, 'time': time_col_width}
161
  col_x_starts = {}
162
  current_x = margin_col_width
163
  for col_name in ['hall', 'seq', 'movie', 'pinyin', 'time']:
164
  col_x_starts[col_name] = current_x
165
  current_x += col_widths[col_name]
166
 
167
- # --- 2. Drawing ---
168
  def draw_figure(fig, ax):
169
  renderer = fig.canvas.get_renderer()
170
-
171
  for col_name in ['hall', 'seq', 'movie', 'pinyin']:
172
  x_line = col_x_starts[col_name] + col_widths[col_name]
173
  line_top_y, line_bottom_y = A4_height_in - row_height, row_height
174
  ax.add_line(Line2D([x_line, x_line], [line_bottom_y, line_top_y], color='gray', linestyle=':', linewidth=0.5))
175
-
176
  last_hall_drawn = None
177
  for i, row in data.iterrows():
178
  y_bottom = A4_height_in - (i + 2) * row_height
179
  y_center = y_bottom + row_height / 2
180
-
181
- # --- Draw Cell Content ---
182
- # Hall Number (renders superscript)
183
  if row['Hall'] != last_hall_drawn:
184
  ax.text(col_x_starts['hall'] + col_widths['hall'] / 2, y_center, row['hall_str'],
185
- fontproperties=get_font(base_font_size_pt), ha='center', va='center')
186
  last_hall_drawn = row['Hall']
187
-
188
- # Sequence Number (with smaller, specific font size)
189
  ax.text(col_x_starts['seq'] + col_widths['seq'] / 2, y_center, row['seq_str'],
190
- fontproperties=get_font(seq_font_size_pt), ha='center', va='center')
191
-
192
- # Pinyin Abbreviation
193
  ax.text(col_x_starts['pinyin'] + col_widths['pinyin'] / 2, y_center, row['pinyin_abbr'],
194
- fontproperties=get_font(base_font_size_pt), ha='center', va='center')
195
-
196
- # Time String
197
  ax.text(col_x_starts['time'] + col_widths['time'] / 2, y_center, row['time_str'],
198
- fontproperties=get_font(base_font_size_pt), ha='center', va='center')
199
-
200
- # Movie Title (with font scaling)
201
  movie_font_size = base_font_size_pt
202
- movie_font_prop = get_font(movie_font_size)
203
  text_w_px, _, _ = renderer.get_text_width_height_descent(row['Movie'], movie_font_prop, ismath=False)
204
  text_w_in = text_w_px / dpi
205
-
206
  max_width_in = col_widths['movie'] * 0.9
207
  if text_w_in > max_width_in:
208
  movie_font_size *= (max_width_in / text_w_in)
209
- movie_font_prop = get_font(movie_font_size)
210
-
211
- ax.text(col_x_starts['movie'] + 0.05, y_center, row['Movie'],
212
- fontproperties=movie_font_prop, ha='left', va='center')
213
-
214
- # --- Draw Horizontal Lines ---
215
  is_last_in_hall = (i == len(data) - 1) or (row['Hall'] != data.loc[i + 1, 'Hall'])
216
-
217
  line_start_x = margin_col_width
218
  line_end_x = A4_width_in - margin_col_width
219
  if is_last_in_hall:
@@ -221,7 +190,6 @@ def create_print_layout(data, date_str):
221
  else:
222
  ax.add_line(Line2D([line_start_x, line_end_x], [y_bottom, y_bottom], color='gray', linestyle=':', linewidth=0.5))
223
 
224
- # --- 3. Setup Figures and Generate Output ---
225
  outputs = {}
226
  for format_type in ['png', 'pdf']:
227
  fig = plt.figure(figsize=(A4_width_in, A4_height_in), dpi=dpi)
@@ -229,47 +197,195 @@ def create_print_layout(data, date_str):
229
  ax.set_axis_off()
230
  ax.set_xlim(0, A4_width_in)
231
  ax.set_ylim(0, A4_height_in)
232
-
233
- ax.text(margin_col_width, A4_height_in - (row_height/2), date_str,
234
- fontproperties=get_font(10), color='#A9A9A9', ha='left', va='center')
235
-
236
  draw_figure(fig, ax)
237
-
238
  buf = io.BytesIO()
239
  fig.savefig(buf, format=format_type, dpi=dpi, bbox_inches='tight', pad_inches=0)
240
  buf.seek(0)
241
-
242
  data_uri = base64.b64encode(buf.getvalue()).decode()
243
  mime_type = 'image/png' if format_type == 'png' else 'application/pdf'
244
  outputs[format_type] = f"data:{mime_type};base64,{data_uri}"
245
-
246
  plt.close(fig)
247
-
248
  return outputs
249
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
250
 
251
  def display_pdf(base64_pdf):
252
- """Generates the HTML to embed a PDF in Streamlit."""
253
  return f'<iframe src="{base64_pdf}" width="100%" height="800" type="application/pdf"></iframe>'
254
 
255
- # --- Streamlit App ---
256
- st.set_page_config(page_title="LED Screen Schedule Printer", layout="wide")
257
- st.title("LED Screen Schedule Printer")
258
 
259
- uploaded_file = st.file_uploader("Select the '放映时间核对表.xls' file", accept_multiple_files=False, type=["xls", "xlsx"])
 
260
 
261
  if uploaded_file:
262
- with st.spinner("Processing file, please wait..."):
263
- schedule, date_str = process_schedule(uploaded_file)
264
- if schedule is not None and not schedule.empty:
265
- output = create_print_layout(schedule, date_str)
266
-
267
- tab1, tab2 = st.tabs(["PDF Preview", "PNG Preview"])
268
-
269
- with tab1:
270
- st.markdown(display_pdf(output['pdf']), unsafe_allow_html=True)
271
-
272
- with tab2:
273
- st.image(output['png'], use_container_width=True)
274
- else:
275
- st.error("Could not process the file. Please check if the file format and content are correct.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
  from datetime import datetime, timedelta
10
  from pypinyin import lazy_pinyin, Style
11
  from matplotlib.backends.backend_pdf import PdfPages
12
+ import matplotlib.gridspec as gridspec
13
+ import math
14
+
15
+ # --- 常量定义 ---
16
+ SPLIT_TIME = "17:30"
17
+ BUSINESS_START = "09:30"
18
+ BUSINESS_END = "01:30"
19
+ BORDER_COLOR = 'grey'
20
+ DATE_COLOR = '#A9A9A9'
21
+ A5_WIDTH_IN = 5.83
22
+ A5_HEIGHT_IN = 8.27
23
+ NUM_COLS = 3
24
+
25
+ # --- 字体加载与文本处理函数 ---
26
+
27
+ def get_font_regular(size=14):
28
+ """加载思源黑体-常规 (SimHei.ttf)"""
29
+ font_path = "SimHei.ttf"
30
  if os.path.exists(font_path):
31
  return font_manager.FontProperties(fname=font_path, size=size)
32
  else:
33
+ st.warning("警告:未找到字体文件 'SimHei.ttf',LED屏排片表显示可能不正确。")
34
  return font_manager.FontProperties(family='sans-serif', size=size)
35
 
36
+ def get_font_bold(size=14):
37
+ """加载思源黑体-粗体 (SourceHanSansOLD-Heavy-2.otf)"""
38
+ font_path = "SourceHanSansOLD-Heavy-2.otf"
39
+ if os.path.exists(font_path):
40
+ return font_manager.FontProperties(fname=font_path, size=size)
41
+ else:
42
+ st.warning("警告:未找到字体文件 'SourceHanSansOLD-Heavy-2.otf',散场时间表显示可能不正确。")
43
+ return font_manager.FontProperties(family='sans-serif', size=size, weight='bold')
44
+
45
  def get_pinyin_abbr(text):
46
+ """获取中文文本前两个字的拼音首字母"""
47
  if not text:
48
  return ""
49
  chars = [c for c in text if '\u4e00' <= c <= '\u9fff'][:2]
 
53
  return ''.join(pinyin_list).upper()
54
 
55
  def format_seq(n):
56
+ """将数字转换为带圈序号 (①, ②, ③...)"""
57
  if not isinstance(n, int) or n <= 0:
58
  return str(n)
 
59
  circled_chars = "①②③④⑤⑥⑦⑧⑨⑩⑪⑫⑬⑭⑮⑯⑰⑱⑲⑳" \
60
  "㉑㉒㉓㉔㉕㉖㉗㉘㉙㉚㉛㉜㉝㉞㉟" \
61
  "㊱㊲㊳㊴㊵㊶㊷㊸㊹㊺㊻㊼㊽㊾㊿"
62
  if 1 <= n <= 50:
63
+ return circled_chars[n - 1]
64
+ return f'({n})'
65
 
66
+ # --- '放映时间核对表' 处理函数 ---
67
+ def process_schedule_led(file):
68
+ """处理 '放映时间核对表.xls' 文件"""
 
 
69
  try:
70
  date_df = pd.read_excel(file, header=None, skiprows=7, nrows=1, usecols=[3])
71
  date_str = pd.to_datetime(date_df.iloc[0, 0]).strftime('%Y-%m-%d')
 
77
  try:
78
  df = pd.read_excel(file, header=9, usecols=[1, 2, 4, 5])
79
  df.columns = ['Hall', 'StartTime', 'EndTime', 'Movie']
 
80
  df['Hall'] = df['Hall'].ffill()
81
  df.dropna(subset=['StartTime', 'EndTime', 'Movie'], inplace=True)
82
  df['Hall'] = df['Hall'].astype(str).str.extract(r'(\d+号)')
 
83
  df['StartTime_dt'] = pd.to_datetime(df['StartTime'], format='%H:%M', errors='coerce').apply(
84
+ lambda t: t.replace(year=base_date.year, month=base_date.month, day=base_date.day) if pd.notnull(t) else t)
 
85
  df['EndTime_dt'] = pd.to_datetime(df['EndTime'], format='%H:%M', errors='coerce').apply(
86
+ lambda t: t.replace(year=base_date.year, month=base_date.month, day=base_date.day) if pd.notnull(t) else t)
 
87
  df.loc[df['EndTime_dt'] < df['StartTime_dt'], 'EndTime_dt'] += timedelta(days=1)
88
  df = df.sort_values(['Hall', 'StartTime_dt'])
 
89
  merged_rows = []
90
  for _, group in df.groupby('Hall'):
91
  current = None
 
99
  current = row.copy()
100
  if current is not None:
101
  merged_rows.append(current)
 
102
  merged_df = pd.DataFrame(merged_rows)
 
103
  merged_df['StartTime_dt'] -= timedelta(minutes=10)
104
  merged_df['EndTime_dt'] -= timedelta(minutes=5)
 
105
  merged_df['Seq'] = merged_df.groupby('Hall').cumcount() + 1
 
106
  merged_df['StartTime_str'] = merged_df['StartTime_dt'].dt.strftime('%H:%M')
107
  merged_df['EndTime_str'] = merged_df['EndTime_dt'].dt.strftime('%H:%M')
 
108
  return merged_df[['Hall', 'Seq', 'Movie', 'StartTime_str', 'EndTime_str']], date_str
109
  except Exception as e:
110
+ st.error(f"处理数据出错: {e}。请检查文件格式是否正确。")
111
  return None, date_str
112
 
113
+ def create_print_layout_led(data, date_str):
114
+ """为 '放映时间核对表' 生成打印布局 (使用 Regular 字体)"""
 
 
 
115
  if data is None or data.empty:
116
  return None
 
 
117
  A4_width_in, A4_height_in = 8.27, 11.69
118
  dpi = 300
 
119
  total_content_rows = len(data)
120
  totalA = total_content_rows + 2
121
  row_height = A4_height_in / totalA
 
 
122
  data = data.reset_index(drop=True)
 
123
  data['hall_str'] = '$' + data['Hall'].str.replace('号', '') + '^{\#}$'
 
124
  data['seq_str'] = data['Seq'].apply(format_seq)
125
  data['pinyin_abbr'] = data['Movie'].apply(get_pinyin_abbr)
126
  data['time_str'] = data['StartTime_str'] + ' - ' + data['EndTime_str']
 
127
  temp_fig = plt.figure(figsize=(A4_width_in, A4_height_in), dpi=dpi)
128
  renderer = temp_fig.canvas.get_renderer()
 
129
  base_font_size_pt = (row_height * 0.9) * 72
 
130
  seq_font_size_pt = (row_height * 0.5) * 72
131
+
132
  def get_col_width_in(series, font_size_pt, is_math=False):
133
+ if series.empty: return 0
134
+ font_prop = get_font_regular(font_size_pt) # 使用 Regular 字体
 
 
135
  longest_str_idx = series.astype(str).str.len().idxmax()
136
  max_content = str(series.loc[longest_str_idx])
137
  text_width_px, _, _ = renderer.get_text_width_height_descent(max_content, font_prop, ismath=is_math)
138
  return (text_width_px / dpi) * 1.1
139
 
 
140
  margin_col_width = row_height
 
141
  hall_col_width = get_col_width_in(data['hall_str'], base_font_size_pt, is_math=True)
 
142
  seq_col_width = get_col_width_in(data['seq_str'], seq_font_size_pt)
143
  pinyin_col_width = get_col_width_in(data['pinyin_abbr'], base_font_size_pt)
144
  time_col_width = get_col_width_in(data['time_str'], base_font_size_pt)
145
+ movie_col_width = A4_width_in - (
146
+ margin_col_width * 2 + hall_col_width + seq_col_width + pinyin_col_width + time_col_width)
 
147
  plt.close(temp_fig)
148
+ col_widths = {'hall': hall_col_width, 'seq': seq_col_width, 'movie': movie_col_width, 'pinyin': pinyin_col_width,
149
+ 'time': time_col_width}
150
  col_x_starts = {}
151
  current_x = margin_col_width
152
  for col_name in ['hall', 'seq', 'movie', 'pinyin', 'time']:
153
  col_x_starts[col_name] = current_x
154
  current_x += col_widths[col_name]
155
 
 
156
  def draw_figure(fig, ax):
157
  renderer = fig.canvas.get_renderer()
 
158
  for col_name in ['hall', 'seq', 'movie', 'pinyin']:
159
  x_line = col_x_starts[col_name] + col_widths[col_name]
160
  line_top_y, line_bottom_y = A4_height_in - row_height, row_height
161
  ax.add_line(Line2D([x_line, x_line], [line_bottom_y, line_top_y], color='gray', linestyle=':', linewidth=0.5))
 
162
  last_hall_drawn = None
163
  for i, row in data.iterrows():
164
  y_bottom = A4_height_in - (i + 2) * row_height
165
  y_center = y_bottom + row_height / 2
 
 
 
166
  if row['Hall'] != last_hall_drawn:
167
  ax.text(col_x_starts['hall'] + col_widths['hall'] / 2, y_center, row['hall_str'],
168
+ fontproperties=get_font_regular(base_font_size_pt), ha='center', va='center')
169
  last_hall_drawn = row['Hall']
 
 
170
  ax.text(col_x_starts['seq'] + col_widths['seq'] / 2, y_center, row['seq_str'],
171
+ fontproperties=get_font_regular(seq_font_size_pt), ha='center', va='center')
 
 
172
  ax.text(col_x_starts['pinyin'] + col_widths['pinyin'] / 2, y_center, row['pinyin_abbr'],
173
+ fontproperties=get_font_regular(base_font_size_pt), ha='center', va='center')
 
 
174
  ax.text(col_x_starts['time'] + col_widths['time'] / 2, y_center, row['time_str'],
175
+ fontproperties=get_font_regular(base_font_size_pt), ha='center', va='center')
 
 
176
  movie_font_size = base_font_size_pt
177
+ movie_font_prop = get_font_regular(movie_font_size)
178
  text_w_px, _, _ = renderer.get_text_width_height_descent(row['Movie'], movie_font_prop, ismath=False)
179
  text_w_in = text_w_px / dpi
 
180
  max_width_in = col_widths['movie'] * 0.9
181
  if text_w_in > max_width_in:
182
  movie_font_size *= (max_width_in / text_w_in)
183
+ movie_font_prop = get_font_regular(movie_font_size)
184
+ ax.text(col_x_starts['movie'] + 0.05, y_center, row['Movie'], fontproperties=movie_font_prop, ha='left', va='center')
 
 
 
 
185
  is_last_in_hall = (i == len(data) - 1) or (row['Hall'] != data.loc[i + 1, 'Hall'])
 
186
  line_start_x = margin_col_width
187
  line_end_x = A4_width_in - margin_col_width
188
  if is_last_in_hall:
 
190
  else:
191
  ax.add_line(Line2D([line_start_x, line_end_x], [y_bottom, y_bottom], color='gray', linestyle=':', linewidth=0.5))
192
 
 
193
  outputs = {}
194
  for format_type in ['png', 'pdf']:
195
  fig = plt.figure(figsize=(A4_width_in, A4_height_in), dpi=dpi)
 
197
  ax.set_axis_off()
198
  ax.set_xlim(0, A4_width_in)
199
  ax.set_ylim(0, A4_height_in)
200
+ ax.text(margin_col_width, A4_height_in - (row_height / 2), date_str,
201
+ fontproperties=get_font_regular(10), color='#A9A9A9', ha='left', va='center')
 
 
202
  draw_figure(fig, ax)
 
203
  buf = io.BytesIO()
204
  fig.savefig(buf, format=format_type, dpi=dpi, bbox_inches='tight', pad_inches=0)
205
  buf.seek(0)
 
206
  data_uri = base64.b64encode(buf.getvalue()).decode()
207
  mime_type = 'image/png' if format_type == 'png' else 'application/pdf'
208
  outputs[format_type] = f"data:{mime_type};base64,{data_uri}"
 
209
  plt.close(fig)
 
210
  return outputs
211
 
212
+ # --- '放映场次核对表' 处理函数 ---
213
+ def process_schedule_times(file):
214
+ """处理 '放映场次核对表.xls' 文件"""
215
+ try:
216
+ df = pd.read_excel(file, skiprows=8)
217
+ df = df.iloc[:, [6, 7, 9]]
218
+ df.columns = ['Hall', 'StartTime', 'EndTime']
219
+ df = df.dropna(subset=['Hall', 'StartTime', 'EndTime'])
220
+ df['Hall'] = df['Hall'].str.extract(r'(\d+)号').astype(str) + ' '
221
+ base_date = datetime.today().date()
222
+ df['StartTime'] = pd.to_datetime(df['StartTime'])
223
+ df['EndTime'] = pd.to_datetime(df['EndTime'])
224
+ business_start = datetime.strptime(f"{base_date} {BUSINESS_START}", "%Y-%m-%d %H:%M")
225
+ business_end = datetime.strptime(f"{base_date} {BUSINESS_END}", "%Y-%m-%d %H:%M")
226
+ if business_end < business_start:
227
+ business_end += timedelta(days=1)
228
+ for idx, row in df.iterrows():
229
+ end_time = row['EndTime']
230
+ if end_time.hour < 9:
231
+ df.at[idx, 'EndTime'] = end_time + timedelta(days=1)
232
+ if row['StartTime'].hour >= 21 and end_time.hour < 9:
233
+ df.at[idx, 'EndTime'] = end_time + timedelta(days=1)
234
+ df['time_for_comparison'] = df['EndTime'].apply(lambda x: datetime.combine(base_date, x.time()))
235
+ df.loc[df['time_for_comparison'].dt.hour < 9, 'time_for_comparison'] += timedelta(days=1)
236
+ valid_times = (((df['time_for_comparison'] >= datetime.combine(base_date, business_start.time())) &
237
+ (df['time_for_comparison'] <= datetime.combine(base_date + timedelta(days=1), business_end.time()))))
238
+ df = df[valid_times]
239
+ df = df.sort_values('EndTime')
240
+ split_time = datetime.strptime(f"{base_date} {SPLIT_TIME}", "%Y-%m-%d %H:%M")
241
+ split_time_for_comparison = df['time_for_comparison'].apply(lambda x: datetime.combine(base_date, split_time.time()))
242
+ part1 = df[df['time_for_comparison'] <= split_time_for_comparison].copy()
243
+ part2 = df[df['time_for_comparison'] > split_time_for_comparison].copy()
244
+ for part in [part1, part2]:
245
+ part['EndTime'] = part['EndTime'].dt.strftime('%-H:%M')
246
+ date_df = pd.read_excel(file, skiprows=5, nrows=1, usecols=[2], header=None)
247
+ date_cell = date_df.iloc[0, 0]
248
+ try:
249
+ if isinstance(date_cell, str):
250
+ date_str = datetime.strptime(date_cell, '%Y-%m-%d').strftime('%Y-%m-%d')
251
+ else:
252
+ date_str = pd.to_datetime(date_cell).strftime('%Y-%m-%d')
253
+ except:
254
+ date_str = datetime.today().strftime('%Y-%m-%d')
255
+ return part1[['Hall', 'EndTime']], part2[['Hall', 'EndTime']], date_str
256
+ except Exception as e:
257
+ st.error(f"处理文件出错: {str(e)}")
258
+ return None, None, None
259
+
260
+ def create_print_layout_times(data, title, date_str):
261
+ """为 '放映场次核对表' 生成打印布局 (使用 Bold 字体)"""
262
+ if data.empty:
263
+ return None
264
+
265
+ def generate_figure():
266
+ total_items = len(data)
267
+ num_rows = math.ceil(total_items / NUM_COLS) if total_items > 0 else 1
268
+ date_header_height_in = 0.3
269
+ data_area_height_in = A5_HEIGHT_IN - date_header_height_in
270
+ cell_width_in = A5_WIDTH_IN / NUM_COLS
271
+ cell_height_in = data_area_height_in / num_rows
272
+ cell_width_pt = cell_width_in * 72
273
+ cell_height_pt = cell_height_in * 72
274
+ target_text_width_pt = cell_width_pt * 0.9
275
+ fontsize_from_width = target_text_width_pt / (8 * 0.6)
276
+ fontsize_from_height = cell_height_pt * 0.8
277
+ base_fontsize = min(fontsize_from_width, fontsize_from_height)
278
+ fig = plt.figure(figsize=(A5_WIDTH_IN, A5_HEIGHT_IN), dpi=300)
279
+ fig.subplots_adjust(left=0, right=1, top=1, bottom=0)
280
+
281
+ gs = gridspec.GridSpec(
282
+ num_rows + 1, NUM_COLS,
283
+ hspace=0, wspace=0,
284
+ height_ratios=[date_header_height_in] + [cell_height_in] * num_rows,
285
+ figure=fig)
286
+ data_values = data.values.tolist()
287
+ while len(data_values) % NUM_COLS != 0:
288
+ data_values.append(['', ''])
289
+ rows_per_col_layout = math.ceil(len(data_values) / NUM_COLS)
290
+ sorted_data = [['', '']] * len(data_values)
291
+ for i, item in enumerate(data_values):
292
+ if item[0] and item[1]:
293
+ row_in_col = i % rows_per_col_layout
294
+ col_idx = i // rows_per_col_layout
295
+ new_index = row_in_col * NUM_COLS + col_idx
296
+ if new_index < len(sorted_data):
297
+ sorted_data[new_index] = item
298
+ for idx, (hall, end_time) in enumerate(sorted_data):
299
+ if hall and end_time:
300
+ row_grid = idx // NUM_COLS + 1
301
+ col_grid = idx % NUM_COLS
302
+ ax = fig.add_subplot(gs[row_grid, col_grid])
303
+ for spine in ax.spines.values():
304
+ spine.set_visible(True)
305
+ spine.set_linestyle((0, (1, 2)))
306
+ spine.set_color(BORDER_COLOR)
307
+ spine.set_linewidth(0.75)
308
+ display_text = f"{hall}{end_time}"
309
+ ax.text(0.5, 0.5, display_text,
310
+ fontproperties=get_font_bold(base_fontsize), # 使用 Bold 字体
311
+ ha='center', va='center',
312
+ transform=ax.transAxes)
313
+ ax.set_xticks([])
314
+ ax.set_yticks([])
315
+ ax.set_facecolor('none')
316
+ ax_date = fig.add_subplot(gs[0, :])
317
+ ax_date.text(0.01, 0.5, f"{date_str} {title}",
318
+ fontproperties=get_font_bold(base_fontsize * 0.5), # 使用 Bold 字体
319
+ color=DATE_COLOR,
320
+ ha='left', va='center',
321
+ transform=ax_date.transAxes)
322
+ ax_date.set_axis_off()
323
+ ax_date.set_facecolor('none')
324
+ return fig
325
+
326
+ fig_for_output = generate_figure()
327
+ png_buffer = io.BytesIO()
328
+ fig_for_output.savefig(png_buffer, format='png')
329
+ png_buffer.seek(0)
330
+ png_base64 = base64.b64encode(png_buffer.getvalue()).decode()
331
+ pdf_buffer = io.BytesIO()
332
+ with PdfPages(pdf_buffer) as pdf:
333
+ pdf.savefig(fig_for_output)
334
+ pdf_buffer.seek(0)
335
+ pdf_base64 = base64.b64encode(pdf_buffer.getvalue()).decode()
336
+ plt.close(fig_for_output)
337
+ return {'png': f'data:image/png;base64,{png_base64}', 'pdf': f'data:application/pdf;base64,{pdf_base64}'}
338
 
339
  def display_pdf(base64_pdf):
340
+ """在Streamlit中嵌入显示PDF"""
341
  return f'<iframe src="{base64_pdf}" width="100%" height="800" type="application/pdf"></iframe>'
342
 
343
+ # --- Streamlit 主程��� ---
344
+ st.set_page_config(page_title="影院排期打印工具", layout="wide")
345
+ st.title("影院排期打印工具")
346
 
347
+ uploaded_file = st.file_uploader("请上传 '放映时间核对表.xls' '放映场次核对表.xls'",
348
+ type=["xls", "xlsx"])
349
 
350
  if uploaded_file:
351
+ # 根据文件名中的关键字判断使用哪个处理流程
352
+ if "时间" in uploaded_file.name:
353
+ st.header("LED屏排片表")
354
+ with st.spinner("正在处理文件,请稍候..."):
355
+ schedule, date_str = process_schedule_led(uploaded_file)
356
+ if schedule is not None and not schedule.empty:
357
+ output = create_print_layout_led(schedule, date_str)
358
+ tab1, tab2 = st.tabs(["PDF 预览", "图片预览 (PNG)"])
359
+ with tab1:
360
+ st.markdown(display_pdf(output['pdf']), unsafe_allow_html=True)
361
+ with tab2:
362
+ st.image(output['png'], use_container_width=True)
363
+ else:
364
+ st.error("无法处理文件。请检查文件内容和格式是否正确。")
365
+ elif "场次" in uploaded_file.name:
366
+ st.header("散场时间快捷打印")
367
+ part1, part2, date_str = process_schedule_times(uploaded_file)
368
+ if part1 is not None and part2 is not None:
369
+ part1_output = create_print_layout_times(part1, "A", date_str)
370
+ part2_output = create_print_layout_times(part2, "C", date_str)
371
+ col1, col2 = st.columns(2)
372
+ with col1:
373
+ st.subheader("白班 (散场时间 ≤ 17:30)")
374
+ if part1_output:
375
+ tab1_1, tab1_2 = st.tabs(["PDF 预览 ", "图片预览 (PNG) "])
376
+ with tab1_1:
377
+ st.markdown(display_pdf(part1_output['pdf']), unsafe_allow_html=True)
378
+ with tab1_2:
379
+ st.image(part1_output['png'])
380
+ else:
381
+ st.info("白班没有排期数据。")
382
+ with col2:
383
+ st.subheader("晚班 (散场时间 > 17:30)")
384
+ if part2_output:
385
+ tab2_1, tab2_2 = st.tabs(["PDF 预览 ", "图片预览 (PNG) "])
386
+ with tab2_1:
387
+ st.markdown(display_pdf(part2_output['pdf']), unsafe_allow_html=True)
388
+ with tab2_2:
389
+ st.image(part2_output['png'])
390
+ else:
391
+ st.info("晚班没有排期数据。")