Ethscriptions commited on
Commit
28f28d0
·
verified ·
1 Parent(s): 95f254d

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +156 -170
app.py CHANGED
@@ -6,101 +6,64 @@ import io
6
  import base64
7
  import math
8
  from matplotlib.backends.backend_pdf import PdfPages
9
- from matplotlib.patches import Rectangle # Replaced FancyBboxPatch
10
 
11
  # --- Constants ---
12
  SPLIT_TIME = "17:30"
13
  BUSINESS_START = "09:30"
14
  BUSINESS_END = "01:30"
15
- BORDER_COLOR = '#CCCCCC' # Light gray for dotted border
16
  DATE_COLOR = '#A9A9A9'
17
- A5_WIDTH_INCH = 5.83
18
- A5_HEIGHT_INCH = 8.27
19
-
20
 
21
  def process_schedule(file):
22
- """处理上传的 Excel 文件,生成排序和分组后的打印内容"""
 
 
23
  try:
24
- # 读取 Excel,跳过前 8
25
  df = pd.read_excel(file, skiprows=8)
26
 
27
- # 提取所需列 (G9, H9, J9)
28
- df = df.iloc[:, [6, 7, 9]] # G, H, J 列
29
  df.columns = ['Hall', 'StartTime', 'EndTime']
30
 
31
- # 清理数据
32
  df = df.dropna(subset=['Hall', 'StartTime', 'EndTime'])
33
 
34
- # 转换影厅格式为 "#号" 格式
35
- df['Hall'] = df['Hall'].str.extract(r'(\d+)').astype(str) + ' '
36
-
37
- # 保存原始时间字符串用于诊断
38
- df['original_end'] = df['EndTime']
39
 
40
- # 转换时间为 datetime 对象
41
  base_date = datetime.today().date()
42
  df['StartTime'] = pd.to_datetime(df['StartTime'])
43
  df['EndTime'] = pd.to_datetime(df['EndTime'])
44
 
45
- # 设置基准时间
46
- business_start = datetime.strptime(f"{base_date} {BUSINESS_START}", "%Y-%m-%d %H:%M")
47
- business_end = datetime.strptime(f"{base_date} {BUSINESS_END}", "%Y-%m-%d %H:%M")
48
-
49
- # 处理跨天情况
50
- if business_end < business_start:
51
- business_end += timedelta(days=1)
52
-
53
- # 标准化所有时间到同一天
54
  for idx, row in df.iterrows():
55
- end_time = row['EndTime']
56
- if end_time.hour < 9:
57
- df.at[idx, 'EndTime'] = end_time + timedelta(days=1)
58
-
59
- if row['StartTime'].hour >= 21 and end_time.hour < 9:
60
- df.at[idx, 'EndTime'] = end_time + timedelta(days=1)
61
-
62
- # 筛选营业时间内的场次
63
- df['time_for_comparison'] = df['EndTime'].apply(
64
- lambda x: datetime.combine(base_date, x.time())
65
- )
66
-
67
- df.loc[df['time_for_comparison'].dt.hour < 9, 'time_for_comparison'] += timedelta(days=1)
68
-
69
- valid_times = (
70
- ((df['time_for_comparison'] >= datetime.combine(base_date, business_start.time())) &
71
- (df['time_for_comparison'] <= datetime.combine(base_date + timedelta(days=1), business_end.time())))
72
- )
73
 
74
- df = df[valid_times]
 
75
 
76
- # 按散场时间排序
77
  df = df.sort_values('EndTime')
78
 
79
- # 分割数据
80
- split_time = datetime.strptime(f"{base_date} {SPLIT_TIME}", "%Y-%m-%d %H:%M")
81
- split_time_for_comparison = df['time_for_comparison'].apply(
82
- lambda x: datetime.combine(base_date, split_time.time())
83
- )
84
 
85
- part1 = df[df['time_for_comparison'] <= split_time_for_comparison].copy()
86
- part2 = df[df['time_for_comparison'] > split_time_for_comparison].copy()
87
-
88
- # 格式化时间显示
89
  for part in [part1, part2]:
90
  part['EndTime'] = part['EndTime'].dt.strftime('%-I:%M')
91
 
92
- # 精确读取C6单元格
93
- date_df = pd.read_excel(
94
- file,
95
- skiprows=5, # 跳过前5行(0-4)
96
- nrows=1, # 只读1行
97
- usecols=[2], # 第三列(C列)
98
- header=None # 无表头
99
- )
100
  date_cell = date_df.iloc[0, 0]
101
 
102
  try:
103
- # ��理不同日期格式
104
  if isinstance(date_cell, str):
105
  date_str = datetime.strptime(date_cell, '%Y-%m-%d').strftime('%Y-%m-%d')
106
  else:
@@ -111,47 +74,44 @@ def process_schedule(file):
111
  return part1[['Hall', 'EndTime']], part2[['Hall', 'EndTime']], date_str
112
 
113
  except Exception as e:
114
- st.error(f"处理文件时出错: {str(e)}")
115
  return None, None, None
116
 
117
-
118
- def create_print_layout(data, title, date_str):
119
- """创建符合新设计要求的打印布局 (PNG PDF)"""
120
- if data.empty:
121
- return None
122
-
123
- # --- 1. 初始化 Figure ---
124
- fig = plt.figure(figsize=(A5_WIDTH_INCH, A5_HEIGHT_INCH), dpi=300)
125
- # 使用整个figure区域,手动控制所有边距
126
- fig.subplots_adjust(left=0, right=1, top=1, bottom=0)
127
-
128
  plt.rcParams['font.family'] = 'sans-serif'
129
- plt.rcParams['font.sans-serif'] = ['Arial Unicode MS']
130
 
131
- # --- 2. 数据准备和布局计算 ---
132
  total_items = len(data)
 
 
 
133
  num_cols = 3
134
- data_rows = math.ceil(total_items / num_cols)
135
- if data_rows == 0: data_rows = 1 # 避免除以零
136
-
137
- # 为header分配固定高度 (占总高度的8%), 剩余为数据区
138
- header_height_norm = 0.08
139
- data_area_height_norm = 1.0 - header_height_norm
140
-
141
- # 每个数据单元格的尺寸 (标准化坐标, 0-1)
142
- cell_height_norm = data_area_height_norm / data_rows
143
- cell_width_norm = 1.0 / num_cols
144
-
145
- # 根据单元格高度计算留白距离 (高度的 1/4 作为边距)
146
- # 这将被用作单元格内部的padding
147
- spacing_norm_y = cell_height_norm / 4
148
- spacing_norm_x = cell_width_norm / 4 # 对称的边距
149
-
150
- # 准备数据 (Z字形排序)
 
 
 
151
  data_values = data.values.tolist()
152
  while len(data_values) % num_cols != 0:
153
- data_values.append(['', ''])
154
-
155
  rows_per_col_layout = math.ceil(len(data_values) / num_cols)
156
  sorted_data = [['', '']] * len(data_values)
157
  for i, item in enumerate(data_values):
@@ -162,80 +122,102 @@ def create_print_layout(data, title, date_str):
162
  if new_index < len(sorted_data):
163
  sorted_data[new_index] = item
164
 
165
- # --- 3. 绘制元素 ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
166
 
167
- # 绘制页眉 (日期和标题)
168
- ax_date = fig.add_axes([0, data_area_height_norm, 1, header_height_norm])
169
- ax_date.text(0.02, 0.5, f"{date_str} {title}",
170
- fontsize=12, color=DATE_COLOR, fontweight='bold',
171
- ha='left', va='center', transform=ax_date.transAxes)
172
- ax_date.set_axis_off()
173
 
174
- # 动态计算主字体大小,使其适应单元格高度
175
- # 1英寸 = 72点。字体大小占单元格高度的40%左右
176
- base_fontsize = (cell_height_norm * A5_HEIGHT_INCH) * 72 * 0.4
177
 
178
- # 绘制数据单元格
179
- for idx, (hall, end_time) in enumerate(sorted_data):
180
- if hall and end_time:
181
- row_grid = idx // num_cols
182
- col_grid = idx % num_cols
183
-
184
- # 计算单元格(Axes)的位置和尺寸
185
- ax_left = col_grid * cell_width_norm
186
- # Y坐标从下往上,所以要从数据区顶部开始往下减
187
- ax_bottom = data_area_height_norm - (row_grid + 1) * cell_height_norm
188
-
189
- ax = fig.add_axes([ax_left, ax_bottom, cell_width_norm, cell_height_norm])
190
- ax.set_axis_off()
191
-
192
- # 要求 2 & 3: 创建点状虚线边框和呼吸感
193
- # 通过在Axes内部绘制一个稍小的矩形来实现
194
- border_padding_x = spacing_norm_x / 2
195
- border_padding_y = spacing_norm_y / 2
196
-
197
- border_rect = Rectangle(
198
- (border_padding_x, border_padding_y), # 左下角坐标 (x,y)
199
- 1 - 2 * border_padding_x, # 宽度
200
- 1 - 2 * border_padding_y, # 高度
201
- transform=ax.transAxes,
202
- edgecolor=BORDER_COLOR,
203
- facecolor='none',
204
- linestyle=':', # 点状虚线
205
- linewidth=2 # 让点更明显
206
- )
207
- ax.add_patch(border_rect)
208
-
209
- # 要求 4: 在左上角添加序号
210
- ax.text(border_padding_x + 0.02, 1 - border_padding_y - 0.02, str(idx + 1),
211
- fontsize=base_fontsize * 0.3, # 序号字体较小
212
- color='grey',
213
- ha='left', va='top',
214
- transform=ax.transAxes)
215
-
216
- # 要求 3: 居中显示主要文本
217
- display_text = f"{hall}{end_time}"
218
- ax.text(0.5, 0.5, display_text,
219
- fontsize=base_fontsize,
220
- fontweight='bold',
221
- ha='center', va='center',
222
- transform=ax.transAxes)
223
-
224
- # --- 4. 保存到内存 ---
225
- # 保存为 PNG
226
  png_buffer = io.BytesIO()
227
- fig.savefig(png_buffer, format='png', bbox_inches='tight', pad_inches=0.02)
228
  png_buffer.seek(0)
229
  png_base64 = base64.b64encode(png_buffer.getvalue()).decode()
 
230
 
231
- # 保存为 PDF
232
  pdf_buffer = io.BytesIO()
233
  with PdfPages(pdf_buffer) as pdf:
234
- pdf.savefig(fig, bbox_inches='tight', pad_inches=0.02)
235
  pdf_buffer.seek(0)
236
  pdf_base64 = base64.b64encode(pdf_buffer.getvalue()).decode()
237
-
238
- plt.close(fig)
239
 
240
  return {
241
  'png': f'data:image/png;base64,{png_base64}',
@@ -243,29 +225,32 @@ def create_print_layout(data, title, date_str):
243
  }
244
 
245
  def display_pdf(base64_pdf):
246
- """在Streamlit中嵌入显示PDF"""
247
- pdf_display = f'<iframe src="{base64_pdf}" width="100%" height="800" type="application/pdf"></iframe>'
 
 
248
  return pdf_display
249
 
250
- # --- Streamlit 界面 (无变化) ---
251
  st.set_page_config(page_title="散厅时间快捷打印", layout="wide")
252
  st.title("散厅时间快捷打印")
253
 
254
- uploaded_file = st.file_uploader("上传【放映场次核对表.xls】文件", type=["xls"])
255
 
256
  if uploaded_file:
257
- part1, part2, date_str = process_schedule(uploaded_file)
258
 
259
- if part1 is not None and part2 is not None:
260
- # 生成包含 PNG PDF 的字典
261
- part1_output = create_print_layout(part1, "A", date_str)
262
- part2_output = create_print_layout(part2, "C", date_str)
263
 
264
  col1, col2 = st.columns(2)
265
 
266
  with col1:
267
- st.subheader("白班散场预览(时间 ≤ 17:30")
268
  if part1_output:
 
269
  tab1_1, tab1_2 = st.tabs(["PDF 预览", "PNG 预览"])
270
  with tab1_1:
271
  st.markdown(display_pdf(part1_output['pdf']), unsafe_allow_html=True)
@@ -275,8 +260,9 @@ if uploaded_file:
275
  st.info("白班部分没有数据")
276
 
277
  with col2:
278
- st.subheader("夜班散场预览(时间 > 17:30")
279
  if part2_output:
 
280
  tab2_1, tab2_2 = st.tabs(["PDF 预览", "PNG 预览"])
281
  with tab2_1:
282
  st.markdown(display_pdf(part2_output['pdf']), unsafe_allow_html=True)
 
6
  import base64
7
  import math
8
  from matplotlib.backends.backend_pdf import PdfPages
 
9
 
10
  # --- Constants ---
11
  SPLIT_TIME = "17:30"
12
  BUSINESS_START = "09:30"
13
  BUSINESS_END = "01:30"
14
+ BORDER_COLOR = 'grey' # Changed to grey for the new design
15
  DATE_COLOR = '#A9A9A9'
 
 
 
16
 
17
  def process_schedule(file):
18
+ """
19
+ Processes the uploaded Excel file to extract, clean, and sort screening times.
20
+ """
21
  try:
22
+ # Read Excel, skipping header rows
23
  df = pd.read_excel(file, skiprows=8)
24
 
25
+ # Extract required columns (G, H, J)
26
+ df = df.iloc[:, [6, 7, 9]]
27
  df.columns = ['Hall', 'StartTime', 'EndTime']
28
 
29
+ # Clean data: drop rows with missing values
30
  df = df.dropna(subset=['Hall', 'StartTime', 'EndTime'])
31
 
32
+ # Format Hall to "# " format
33
+ df['Hall'] = df['Hall'].str.extract(r'(\d+)').astype(str) + ' '
 
 
 
34
 
35
+ # Convert time columns to datetime objects
36
  base_date = datetime.today().date()
37
  df['StartTime'] = pd.to_datetime(df['StartTime'])
38
  df['EndTime'] = pd.to_datetime(df['EndTime'])
39
 
40
+ # --- Handle overnight screenings ---
41
+ # If a show ends after midnight (e.g., 1:30 AM), it belongs to the previous day's schedule.
42
+ # We handle this by adding a day to its datetime object.
 
 
 
 
 
 
43
  for idx, row in df.iterrows():
44
+ if row['EndTime'].hour < 9: # Assuming any end time before 9 AM is part of the previous night
45
+ df.at[idx, 'EndTime'] = row['EndTime'] + timedelta(days=1)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
 
47
+ # Create a comparable time column that correctly handles the business day logic
48
+ df['time_for_comparison'] = df['EndTime']
49
 
50
+ # Sort screenings by their end time
51
  df = df.sort_values('EndTime')
52
 
53
+ # Split data into day and night shifts
54
+ split_datetime = datetime.combine(base_date, datetime.strptime(SPLIT_TIME, "%H:%M").time())
55
+ part1 = df[df['time_for_comparison'] <= split_datetime].copy()
56
+ part2 = df[df['time_for_comparison'] > split_datetime].copy()
 
57
 
58
+ # Format the time display string (e.g., "5:30")
 
 
 
59
  for part in [part1, part2]:
60
  part['EndTime'] = part['EndTime'].dt.strftime('%-I:%M')
61
 
62
+ # Precisely read the date from cell C6
63
+ date_df = pd.read_excel(file, skiprows=5, nrows=1, usecols=[2], header=None)
 
 
 
 
 
 
64
  date_cell = date_df.iloc[0, 0]
65
 
66
  try:
 
67
  if isinstance(date_cell, str):
68
  date_str = datetime.strptime(date_cell, '%Y-%m-%d').strftime('%Y-%m-%d')
69
  else:
 
74
  return part1[['Hall', 'EndTime']], part2[['Hall', 'EndTime']], date_str
75
 
76
  except Exception as e:
77
+ st.error(f"Error processing file: {str(e)}")
78
  return None, None, None
79
 
80
+ def _draw_grid_on_figure(fig, data, title, date_str):
81
+ """
82
+ Internal helper function to draw the new grid layout onto a Matplotlib figure.
83
+ """
 
 
 
 
 
 
 
84
  plt.rcParams['font.family'] = 'sans-serif'
85
+ plt.rcParams['font.sans-serif'] = ['Arial Unicode MS'] # Font that supports Chinese characters
86
 
 
87
  total_items = len(data)
88
+ if total_items == 0:
89
+ return
90
+
91
  num_cols = 3
92
+ num_rows = math.ceil(total_items / num_cols)
93
+
94
+ A5_WIDTH_IN = 5.83
95
+ A5_HEIGHT_IN = 8.27
96
+
97
+ # 1. Redesign layout based on precise grid calculations
98
+ margin_y = (A5_HEIGHT_IN / num_rows) / 4
99
+ margin_x = margin_y # Use symmetric margins for a cleaner look
100
+
101
+ # Prevent margins from becoming too large on pages with few items
102
+ if A5_WIDTH_IN < 2 * margin_x or A5_HEIGHT_IN < 2 * margin_y:
103
+ margin_x = A5_WIDTH_IN / 10
104
+ margin_y = A5_HEIGHT_IN / 10
105
+
106
+ printable_width = A5_WIDTH_IN - 2 * margin_x
107
+ printable_height = A5_HEIGHT_IN - 2 * margin_y
108
+ cell_width = printable_width / num_cols
109
+ cell_height = printable_height / num_rows
110
+
111
+ # Prepare data: Sort into column-first (Z-style) order for layout
112
  data_values = data.values.tolist()
113
  while len(data_values) % num_cols != 0:
114
+ data_values.append(['', '']) # Pad data for a full grid
 
115
  rows_per_col_layout = math.ceil(len(data_values) / num_cols)
116
  sorted_data = [['', '']] * len(data_values)
117
  for i, item in enumerate(data_values):
 
122
  if new_index < len(sorted_data):
123
  sorted_data[new_index] = item
124
 
125
+ # --- Draw each cell onto the figure ---
126
+ item_counter = 0
127
+ for idx, (hall, end_time) in enumerate(sorted_data):
128
+ if not (hall and end_time):
129
+ continue
130
+
131
+ item_counter += 1
132
+ row_grid = idx // num_cols
133
+ col_grid = idx % num_cols
134
+
135
+ # Calculate position for each cell's axes in Figure coordinates [left, bottom, width, height]
136
+ ax_left_in = margin_x + col_grid * cell_width
137
+ ax_bottom_in = margin_y + (num_rows - 1 - row_grid) * cell_height # Y-axis from bottom
138
+ ax_pos = [
139
+ ax_left_in / A5_WIDTH_IN,
140
+ ax_bottom_in / A5_HEIGHT_IN,
141
+ cell_width / A5_WIDTH_IN,
142
+ cell_height / A5_HEIGHT_IN,
143
+ ]
144
+ ax = fig.add_axes(ax_pos)
145
+
146
+ # 2. Change Cell Border to a gray, dotted line
147
+ for spine in ax.spines.values():
148
+ spine.set_visible(True)
149
+ spine.set_linestyle(':') # Dotted line style
150
+ spine.set_edgecolor(BORDER_COLOR)
151
+ spine.set_linewidth(1)
152
+
153
+ # 4. Add Cell Index Number
154
+ ax.text(0.07, 0.93, str(item_counter),
155
+ transform=ax.transAxes,
156
+ fontsize=9,
157
+ color='grey',
158
+ ha='left',
159
+ va='top')
160
+
161
+ # 3. Adjust Cell Content
162
+ display_text = f"{hall}{end_time}"
163
+
164
+ # Dynamically estimate font size to fill 90% of the cell width
165
+ # This is a heuristic that provides a good balance of size and spacing.
166
+ font_scale_factor = 1.7
167
+ estimated_fontsize = (cell_width * 72 / len(display_text)) * font_scale_factor * 0.9
168
+ max_fontsize = cell_height * 72 * 0.6 # Cap font size to 60% of cell height
169
+ final_fontsize = min(estimated_fontsize, max_fontsize)
170
+
171
+ ax.text(0.5, 0.5, display_text,
172
+ fontsize=final_fontsize,
173
+ fontweight='bold',
174
+ ha='center',
175
+ va='center',
176
+ transform=ax.transAxes)
177
+
178
+ ax.set_xticks([])
179
+ ax.set_yticks([])
180
+
181
+ # Add date and title to the top margin of the figure
182
+ title_fontsize = margin_y * 72 * 0.3 # Scale title font size with margin
183
+ fig.text(margin_x / A5_WIDTH_IN, 1 - (margin_y * 0.5 / A5_HEIGHT_IN),
184
+ f"{date_str} {title}",
185
+ fontsize=title_fontsize,
186
+ color=DATE_COLOR,
187
+ fontweight='bold',
188
+ ha='left',
189
+ va='center')
190
+
191
+
192
+ def create_print_layout(data, title, date_str):
193
+ """
194
+ Creates the final print-ready output in both PNG and PDF formats using the new grid layout.
195
+ """
196
+ if data.empty:
197
+ return None
198
 
199
+ # --- Create separate figures for PNG and PDF to ensure no cross-contamination ---
200
+ png_fig = plt.figure(figsize=(5.83, 8.27), dpi=300)
201
+ pdf_fig = plt.figure(figsize=(5.83, 8.27), dpi=300)
 
 
 
202
 
203
+ # --- Draw the layout on both figures ---
204
+ _draw_grid_on_figure(png_fig, data, title, date_str)
205
+ _draw_grid_on_figure(pdf_fig, data, title, date_str)
206
 
207
+ # --- Save PNG to a memory buffer ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
208
  png_buffer = io.BytesIO()
209
+ png_fig.savefig(png_buffer, format='png', bbox_inches='tight', pad_inches=0.02)
210
  png_buffer.seek(0)
211
  png_base64 = base64.b64encode(png_buffer.getvalue()).decode()
212
+ plt.close(png_fig)
213
 
214
+ # --- Save PDF to a memory buffer ---
215
  pdf_buffer = io.BytesIO()
216
  with PdfPages(pdf_buffer) as pdf:
217
+ pdf.savefig(pdf_fig, bbox_inches='tight', pad_inches=0.02)
218
  pdf_buffer.seek(0)
219
  pdf_base64 = base64.b64encode(pdf_buffer.getvalue()).decode()
220
+ plt.close(pdf_fig)
 
221
 
222
  return {
223
  'png': f'data:image/png;base64,{png_base64}',
 
225
  }
226
 
227
  def display_pdf(base64_pdf):
228
+ """
229
+ Generates the HTML to embed and display a PDF in Streamlit.
230
+ """
231
+ pdf_display = f'<iframe src="data:application/pdf;base64,{base64_pdf}" width="100%" height="800" type="application/pdf"></iframe>'
232
  return pdf_display
233
 
234
+ # --- Streamlit User Interface ---
235
  st.set_page_config(page_title="散厅时间快捷打印", layout="wide")
236
  st.title("散厅时间快捷打印")
237
 
238
+ uploaded_file = st.file_uploader("上传【放映场次核对表.xls】文件", type=["xls", "xlsx"])
239
 
240
  if uploaded_file:
241
+ part1_data, part2_data, date_string = process_schedule(uploaded_file)
242
 
243
+ if part1_data is not None and part2_data is not None:
244
+ # Generate layouts for both day and night shifts
245
+ part1_output = create_print_layout(part1_data, "A", date_string)
246
+ part2_output = create_print_layout(part2_data, "C", date_string)
247
 
248
  col1, col2 = st.columns(2)
249
 
250
  with col1:
251
+ st.subheader("白班散场预览 (时间 ≤ 17:30)")
252
  if part1_output:
253
+ # Use tabs for PDF and PNG previews
254
  tab1_1, tab1_2 = st.tabs(["PDF 预览", "PNG 预览"])
255
  with tab1_1:
256
  st.markdown(display_pdf(part1_output['pdf']), unsafe_allow_html=True)
 
260
  st.info("白班部分没有数据")
261
 
262
  with col2:
263
+ st.subheader("夜班散场预览 (时间 > 17:30)")
264
  if part2_output:
265
+ # Use tabs for PDF and PNG previews
266
  tab2_1, tab2_2 = st.tabs(["PDF 预览", "PNG 预览"])
267
  with tab2_1:
268
  st.markdown(display_pdf(part2_output['pdf']), unsafe_allow_html=True)