Ethscriptions commited on
Commit
4df41bd
·
verified ·
1 Parent(s): 9a4c156

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +44 -59
app.py CHANGED
@@ -12,17 +12,12 @@ 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
- # Prioritize the .ttc file if it exists
16
  font_path = "simHei.ttc"
17
  if not os.path.exists(font_path):
18
- # Fallback to the .ttf file
19
  font_path = "SimHei.ttf"
20
- # If neither exists, matplotlib will use its default font.
21
- # For best results, ensure one of these fonts is in the same directory as the script.
22
  if os.path.exists(font_path):
23
  return font_manager.FontProperties(fname=font_path, size=size)
24
  else:
25
- # Fallback to a generic font if SimHei is not found
26
  st.warning("SimHei font not found. Display may not be correct. Please add simHei.ttc or SimHei.ttf.")
27
  return font_manager.FontProperties(family='sans-serif', size=size)
28
 
@@ -30,26 +25,34 @@ def get_pinyin_abbr(text):
30
  """Gets the first letter of the Pinyin for the first two Chinese characters of a text."""
31
  if not text:
32
  return ""
33
- # Extract the first two Chinese characters
34
  chars = [c for c in text if '\u4e00' <= c <= '\u9fff'][:2]
35
  if not chars:
36
  return ""
37
- # Get the first letter of the pinyin for each character
38
  pinyin_list = lazy_pinyin(chars, style=Style.FIRST_LETTER)
39
  return ''.join(pinyin_list).upper()
40
 
 
 
 
 
 
 
 
 
 
 
 
 
41
  def process_schedule(file):
42
  """
43
  Processes the uploaded Excel file to extract and clean the movie schedule data.
44
  Adds a sequence number for movies within each hall.
45
  """
46
  try:
47
- # Try to read the date from the specified cell
48
  date_df = pd.read_excel(file, header=None, skiprows=7, nrows=1, usecols=[3])
49
  date_str = pd.to_datetime(date_df.iloc[0, 0]).strftime('%Y-%m-%d')
50
  base_date = pd.to_datetime(date_str).date()
51
  except Exception:
52
- # Fallback to today's date if reading fails
53
  date_str = datetime.today().strftime('%Y-%m-%d')
54
  base_date = datetime.today().date()
55
 
@@ -57,23 +60,19 @@ def process_schedule(file):
57
  df = pd.read_excel(file, header=9, usecols=[1, 2, 4, 5])
58
  df.columns = ['Hall', 'StartTime', 'EndTime', 'Movie']
59
 
60
- # Clean and format the data
61
  df['Hall'] = df['Hall'].ffill()
62
  df.dropna(subset=['StartTime', 'EndTime', 'Movie'], inplace=True)
63
  df['Hall'] = df['Hall'].astype(str).str.extract(r'(\d+号)')
64
 
65
- # Convert times to datetime objects
66
  df['StartTime_dt'] = pd.to_datetime(df['StartTime'], format='%H:%M', errors='coerce').apply(
67
  lambda t: t.replace(year=base_date.year, month=base_date.month, day=base_date.day) if pd.notnull(t) else t
68
  )
69
  df['EndTime_dt'] = pd.to_datetime(df['EndTime'], 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
- # Handle overnight screenings
73
  df.loc[df['EndTime_dt'] < df['StartTime_dt'], 'EndTime_dt'] += timedelta(days=1)
74
  df = df.sort_values(['Hall', 'StartTime_dt'])
75
 
76
- # Merge consecutive screenings of the same movie
77
  merged_rows = []
78
  for _, group in df.groupby('Hall'):
79
  current = None
@@ -81,7 +80,7 @@ def process_schedule(file):
81
  if current is None:
82
  current = row.copy()
83
  elif row['Movie'] == current['Movie']:
84
- current['EndTime_dt'] = row['EndTime_dt'] # Extend the end time
85
  else:
86
  merged_rows.append(current)
87
  current = row.copy()
@@ -90,11 +89,9 @@ def process_schedule(file):
90
 
91
  merged_df = pd.DataFrame(merged_rows)
92
 
93
- # Adjust times as per original logic
94
  merged_df['StartTime_dt'] -= timedelta(minutes=10)
95
  merged_df['EndTime_dt'] -= timedelta(minutes=5)
96
 
97
- # Add a sequence number for each movie within its hall
98
  merged_df['Seq'] = merged_df.groupby('Hall').cumcount() + 1
99
 
100
  merged_df['StartTime_str'] = merged_df['StartTime_dt'].dt.strftime('%H:%M')
@@ -114,54 +111,52 @@ def create_print_layout(data, date_str):
114
  return None
115
 
116
  # --- 1. Layout and Column Calculation ---
117
- A4_width_in, A4_height_in = 8.27, 11.69 # A4 size in inches
118
  dpi = 300
119
 
120
- # Calculate total rows and row height based on A4 size
121
  total_content_rows = len(data)
122
- totalA = total_content_rows + 2 # Add 2 for top/bottom margin rows
123
  row_height = A4_height_in / totalA
124
 
125
  # Prepare data strings for width calculation
126
  data = data.reset_index(drop=True)
127
- data['hall_str'] = data['Hall'].str.replace('号', '') + '#'
128
- data['seq_str'] = data['Seq'].astype(str) + '.'
 
 
129
  data['pinyin_abbr'] = data['Movie'].apply(get_pinyin_abbr)
130
  data['time_str'] = data['StartTime_str'] + ' - ' + data['EndTime_str']
131
 
132
- # Create a temporary figure to access the renderer for text width calculation
133
  temp_fig = plt.figure(figsize=(A4_width_in, A4_height_in), dpi=dpi)
134
  renderer = temp_fig.canvas.get_renderer()
135
 
136
- # Base font size is 90% of the row height (converted from inches to points)
137
  base_font_size_pt = (row_height * 0.9) * 72
 
 
138
 
139
- def get_col_width_in(series, font_size_pt):
140
  """Calculates the required width for a column in inches."""
141
  if series.empty:
142
  return 0
143
  font_prop = get_font(font_size_pt)
144
- # Find the string with the maximum visual length in the series
145
  longest_str_idx = series.astype(str).str.len().idxmax()
146
  max_content = str(series.loc[longest_str_idx])
147
- # Get width in pixels from the renderer
148
- text_width_px, _, _ = renderer.get_text_width_height_descent(max_content, font_prop, ismath=False)
149
- # Convert to inches and add a 10% buffer
150
  return (text_width_px / dpi) * 1.1
151
 
152
- # Calculate widths for fixed-content columns
153
- margin_col_width = row_height # Left/right margins are as wide as the rows are high
154
- hall_col_width = get_col_width_in(data['hall_str'], base_font_size_pt)
155
- seq_col_width = get_col_width_in(data['seq_str'], base_font_size_pt)
 
 
156
  pinyin_col_width = get_col_width_in(data['pinyin_abbr'], base_font_size_pt)
157
  time_col_width = get_col_width_in(data['time_str'], base_font_size_pt)
158
 
159
- # The movie title column takes the remaining width
160
  movie_col_width = A4_width_in - (margin_col_width * 2 + hall_col_width + seq_col_width + pinyin_col_width + time_col_width)
161
 
162
- plt.close(temp_fig) # Close the temporary figure
163
 
164
- # Store column widths and their horizontal starting positions
165
  col_widths = {'hall': hall_col_width, 'seq': seq_col_width, 'movie': movie_col_width, 'pinyin': pinyin_col_width, 'time': time_col_width}
166
  col_x_starts = {}
167
  current_x = margin_col_width
@@ -171,33 +166,28 @@ def create_print_layout(data, date_str):
171
 
172
  # --- 2. Drawing ---
173
  def draw_figure(fig, ax):
174
- """The common drawing function to be applied to both PNG and PDF figures."""
175
- # Get a renderer from the actual figure we are drawing on
176
  renderer = fig.canvas.get_renderer()
177
 
178
- # Draw vertical dotted grid lines between columns
179
  for col_name in ['hall', 'seq', 'movie', 'pinyin']:
180
  x_line = col_x_starts[col_name] + col_widths[col_name]
181
- line_top_y = A4_height_in - row_height
182
- line_bottom_y = row_height
183
  ax.add_line(Line2D([x_line, x_line], [line_bottom_y, line_top_y], color='gray', linestyle=':', linewidth=0.5))
184
 
185
- # --- Draw Content and Horizontal Lines for each row ---
186
  last_hall_drawn = None
187
  for i, row in data.iterrows():
188
- y_bottom = A4_height_in - (i + 2) * row_height # Y-coordinate of the row's bottom line
189
- y_center = y_bottom + row_height / 2 # Y-coordinate of the row's vertical center
190
 
191
  # --- Draw Cell Content ---
192
- # Hall Number (only for the first row of each hall)
193
  if row['Hall'] != last_hall_drawn:
194
  ax.text(col_x_starts['hall'] + col_widths['hall'] / 2, y_center, row['hall_str'],
195
  fontproperties=get_font(base_font_size_pt), ha='center', va='center')
196
  last_hall_drawn = row['Hall']
197
 
198
- # Sequence Number
199
  ax.text(col_x_starts['seq'] + col_widths['seq'] / 2, y_center, row['seq_str'],
200
- fontproperties=get_font(base_font_size_pt), ha='center', va='center')
201
 
202
  # Pinyin Abbreviation
203
  ax.text(col_x_starts['pinyin'] + col_widths['pinyin'] / 2, y_center, row['pinyin_abbr'],
@@ -207,43 +197,39 @@ def create_print_layout(data, date_str):
207
  ax.text(col_x_starts['time'] + col_widths['time'] / 2, y_center, row['time_str'],
208
  fontproperties=get_font(base_font_size_pt), ha='center', va='center')
209
 
210
- # Movie Title (with special font scaling to fit)
211
  movie_font_size = base_font_size_pt
212
  movie_font_prop = get_font(movie_font_size)
213
  text_w_px, _, _ = renderer.get_text_width_height_descent(row['Movie'], movie_font_prop, ismath=False)
214
  text_w_in = text_w_px / dpi
215
 
216
- max_width_in = col_widths['movie'] * 0.9 # Target 90% of cell width
217
  if text_w_in > max_width_in:
218
- # If text is too wide, reduce font size proportionally
219
  movie_font_size *= (max_width_in / text_w_in)
220
  movie_font_prop = get_font(movie_font_size)
221
 
222
- ax.text(col_x_starts['movie'] + 0.05, y_center, row['Movie'], # Left-aligned with padding
223
  fontproperties=movie_font_prop, ha='left', va='center')
224
 
225
  # --- Draw Horizontal Lines ---
226
  is_last_in_hall = (i == len(data) - 1) or (row['Hall'] != data.loc[i + 1, 'Hall'])
227
 
 
 
228
  if is_last_in_hall:
229
- # Draw a solid black line to separate halls
230
- line_start_x = margin_col_width
231
- line_end_x = A4_width_in - margin_col_width
232
  ax.add_line(Line2D([line_start_x, line_end_x], [y_bottom, y_bottom], color='black', linestyle='-', linewidth=0.8))
233
  else:
234
- # Draw a dotted gray line for rows within a hall
235
- ax.add_line(Line2D([margin_col_width, A4_width_in - margin_col_width], [y_bottom, y_bottom], color='gray', linestyle=':', linewidth=0.5))
236
 
237
  # --- 3. Setup Figures and Generate Output ---
238
  outputs = {}
239
  for format_type in ['png', 'pdf']:
240
  fig = plt.figure(figsize=(A4_width_in, A4_height_in), dpi=dpi)
241
- ax = fig.add_axes([0, 0, 1, 1]) # Use the whole figure area for drawing
242
  ax.set_axis_off()
243
  ax.set_xlim(0, A4_width_in)
244
  ax.set_ylim(0, A4_height_in)
245
 
246
- # Add date string to the top margin area
247
  ax.text(margin_col_width, A4_height_in - (row_height/2), date_str,
248
  fontproperties=get_font(10), color='#A9A9A9', ha='left', va='center')
249
 
@@ -264,8 +250,7 @@ def create_print_layout(data, date_str):
264
 
265
  def display_pdf(base64_pdf):
266
  """Generates the HTML to embed a PDF in Streamlit."""
267
- pdf_display = f'<iframe src="{base64_pdf}" width="100%" height="800" type="application/pdf"></iframe>'
268
- return pdf_display
269
 
270
  # --- Streamlit App ---
271
  st.set_page_config(page_title="LED Screen Schedule Printer", layout="wide")
 
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
 
 
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]
29
  if not chars:
30
  return ""
 
31
  pinyin_list = lazy_pinyin(chars, style=Style.FIRST_LETTER)
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')
54
  base_date = pd.to_datetime(date_str).date()
55
  except Exception:
 
56
  date_str = datetime.today().strftime('%Y-%m-%d')
57
  base_date = datetime.today().date()
58
 
 
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
 
80
  if current is None:
81
  current = row.copy()
82
  elif row['Movie'] == current['Movie']:
83
+ current['EndTime_dt'] = row['EndTime_dt']
84
  else:
85
  merged_rows.append(current)
86
  current = row.copy()
 
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')
 
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
 
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'],
 
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:
 
 
 
220
  ax.add_line(Line2D([line_start_x, line_end_x], [y_bottom, y_bottom], color='black', linestyle='-', linewidth=0.8))
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)
228
+ ax = fig.add_axes([0, 0, 1, 1])
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
 
 
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")