Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
@@ -6,309 +6,284 @@ import io
|
|
6 |
import base64
|
7 |
import os
|
8 |
from datetime import datetime, timedelta
|
9 |
-
|
|
|
10 |
from pypinyin import lazy_pinyin, Style
|
11 |
from matplotlib.backends.backend_pdf import PdfPages
|
12 |
|
13 |
-
# --- 字体设置 ---
|
14 |
def get_font(size=14):
|
15 |
-
"""
|
16 |
-
font_path = "simHei.ttc"
|
17 |
if not os.path.exists(font_path):
|
18 |
-
font_path = "SimHei.ttf"
|
19 |
-
# 如果两者都不存在,可以添加更多备选字体路径
|
20 |
if not os.path.exists(font_path):
|
21 |
-
#
|
22 |
-
|
23 |
-
|
24 |
-
# for MacOS
|
25 |
-
font_path = "/System/Library/Fonts/STHeiti Medium.ttc"
|
26 |
-
# 如果仍然找不到,matplotlib会回退到默认字体
|
27 |
return font_manager.FontProperties(fname=font_path, size=size)
|
28 |
|
29 |
-
# --- 拼音处理 ---
|
30 |
def get_pinyin_abbr(text):
|
31 |
-
"""
|
32 |
-
if not text
|
33 |
return ""
|
34 |
-
|
35 |
-
|
36 |
-
# 取前两个汉字
|
37 |
-
chars_to_process = chars[:2]
|
38 |
-
if not chars_to_process:
|
39 |
return ""
|
40 |
-
|
41 |
-
pinyin_list = lazy_pinyin(chars_to_process, style=Style.FIRST_LETTER)
|
42 |
-
return ''.join(pinyin_list).upper()
|
43 |
|
44 |
-
# --- 数据处理 ---
|
45 |
def process_schedule(file):
|
46 |
-
"""
|
47 |
try:
|
48 |
-
#
|
49 |
date_df = pd.read_excel(file, header=None, skiprows=7, nrows=1, usecols=[3])
|
50 |
date_str = pd.to_datetime(date_df.iloc[0, 0]).strftime('%Y-%m-%d')
|
51 |
base_date = pd.to_datetime(date_str).date()
|
52 |
except Exception:
|
53 |
-
#
|
54 |
-
date_str = datetime.today().strftime('%Y-%m-%d')
|
55 |
base_date = datetime.today().date()
|
|
|
56 |
|
57 |
try:
|
58 |
-
# 读取排片数据
|
59 |
df = pd.read_excel(file, header=9, usecols=[1, 2, 4, 5])
|
60 |
df.columns = ['Hall', 'StartTime', 'EndTime', 'Movie']
|
61 |
-
|
62 |
-
# 数据清洗
|
63 |
df['Hall'] = df['Hall'].ffill()
|
64 |
df.dropna(subset=['StartTime', 'EndTime', 'Movie'], inplace=True)
|
65 |
df['Hall'] = df['Hall'].astype(str).str.extract(r'(\d+号)')
|
66 |
df.dropna(subset=['Hall'], inplace=True)
|
67 |
|
68 |
-
#
|
69 |
df['StartTime_dt'] = pd.to_datetime(df['StartTime'], format='%H:%M', errors='coerce').apply(
|
70 |
lambda t: t.replace(year=base_date.year, month=base_date.month, day=base_date.day) if pd.notnull(t) else t
|
71 |
)
|
72 |
df['EndTime_dt'] = pd.to_datetime(df['EndTime'], format='%H:%M', errors='coerce').apply(
|
73 |
lambda t: t.replace(year=base_date.year, month=base_date.month, day=base_date.day) if pd.notnull(t) else t
|
74 |
)
|
75 |
-
|
76 |
-
|
77 |
-
# 处理跨天结束时间
|
78 |
df.loc[df['EndTime_dt'] < df['StartTime_dt'], 'EndTime_dt'] += timedelta(days=1)
|
79 |
df = df.sort_values(['Hall', 'StartTime_dt'])
|
80 |
-
|
81 |
-
#
|
82 |
merged_rows = []
|
83 |
-
for
|
84 |
-
group = group.sort_values('StartTime_dt')
|
85 |
current = None
|
86 |
-
for _, row in group.iterrows():
|
87 |
if current is None:
|
88 |
current = row.copy()
|
|
|
|
|
89 |
else:
|
90 |
-
|
91 |
-
|
92 |
-
else:
|
93 |
-
merged_rows.append(current)
|
94 |
-
current = row.copy()
|
95 |
if current is not None:
|
96 |
merged_rows.append(current)
|
97 |
-
|
98 |
if not merged_rows:
|
99 |
return None, date_str
|
100 |
|
101 |
merged_df = pd.DataFrame(merged_rows)
|
102 |
|
103 |
-
#
|
104 |
merged_df['StartTime_dt'] -= timedelta(minutes=10)
|
105 |
merged_df['EndTime_dt'] -= timedelta(minutes=5)
|
106 |
|
107 |
-
|
108 |
-
merged_df['
|
109 |
-
merged_df['Pinyin'] = merged_df['Movie'].apply(get_pinyin_abbr)
|
110 |
-
merged_df['Hall'] = merged_df['Hall'].str.replace('号', '')
|
111 |
|
112 |
-
return merged_df[['Hall', 'Movie', '
|
113 |
-
|
114 |
except Exception as e:
|
115 |
-
st.error(f"
|
116 |
return None, date_str
|
117 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
118 |
|
119 |
-
# --- 打印布局生成 ---
|
120 |
def create_print_layout(data, date_str):
|
121 |
-
"""
|
122 |
if data is None or data.empty:
|
123 |
return None
|
124 |
|
125 |
-
|
126 |
-
DPI = 300
|
127 |
|
128 |
-
#
|
129 |
-
|
130 |
-
|
131 |
-
|
132 |
-
|
133 |
-
|
134 |
-
|
135 |
-
#
|
136 |
-
|
137 |
-
|
138 |
-
|
|
|
|
|
|
|
|
|
|
|
139 |
|
140 |
-
|
141 |
-
|
142 |
-
|
143 |
-
|
144 |
-
base_font = get_font(font_size_pt)
|
145 |
-
|
146 |
-
# 3. 计算各列宽度 (除电影名外)
|
147 |
-
def get_text_width_inch(text, font):
|
148 |
-
t = plt.text(0, 0, text, fontproperties=font)
|
149 |
-
bbox = t.get_window_extent(renderer=renderer)
|
150 |
-
width_pixels = bbox.width
|
151 |
-
t.remove()
|
152 |
-
return width_pixels / DPI
|
153 |
-
|
154 |
-
# 找到每列最长的内容
|
155 |
-
data['Index'] = data.groupby('Hall').cumcount() + 1
|
156 |
-
max_hall_str = data['Hall'].max() + "#" # e.g. "10#"
|
157 |
-
max_index_str = str(data['Index'].max()) + "." # e.g. "5."
|
158 |
-
max_pinyin_str = data['Pinyin'].apply(len).max() * "A" # e.g. "PY" -> "AA"
|
159 |
-
max_time_str = data['Time'].apply(len).idxmax()
|
160 |
-
max_time_str = data.loc[max_time_str, 'Time']
|
161 |
-
|
162 |
-
col_widths = {}
|
163 |
-
col_widths['Hall'] = get_text_width_inch(max_hall_str, base_font) * 1.1
|
164 |
-
col_widths['Index'] = get_text_width_inch(max_index_str, base_font) * 1.1
|
165 |
-
col_widths['Pinyin'] = get_text_width_inch(max_pinyin_str, base_font) * 1.1
|
166 |
-
col_widths['Time'] = get_text_width_inch(max_time_str, base_font) * 1.1
|
167 |
|
168 |
-
|
169 |
-
|
170 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
171 |
|
172 |
-
|
|
|
|
|
173 |
|
174 |
-
# ---
|
175 |
-
|
176 |
for fmt in ['png', 'pdf']:
|
177 |
-
fig = plt.figure(figsize=
|
178 |
ax = fig.add_subplot(111)
|
179 |
ax.set_axis_off()
|
180 |
fig.subplots_adjust(left=0, right=1, top=1, bottom=0)
|
181 |
-
ax.set_xlim(0,
|
182 |
-
ax.set_ylim(0,
|
183 |
-
|
184 |
-
#
|
185 |
-
x_pos =
|
186 |
-
|
187 |
-
|
188 |
-
|
189 |
-
|
190 |
-
|
191 |
-
|
192 |
-
|
193 |
-
|
194 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
195 |
|
196 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
197 |
|
198 |
-
|
199 |
-
|
200 |
-
|
201 |
-
|
202 |
-
|
|
|
|
|
203 |
|
204 |
-
|
205 |
-
|
206 |
-
|
207 |
-
cell_width = col_widths[col_name]
|
208 |
-
|
209 |
-
# 绘制灰色虚线边框
|
210 |
-
rect = Rectangle((cell_x, y_bottom), cell_width, row_height_inch,
|
211 |
-
edgecolor='lightgray', facecolor='none',
|
212 |
-
linestyle=(0, (1, 2)), linewidth=0.8, zorder=1)
|
213 |
-
ax.add_patch(rect)
|
214 |
-
|
215 |
-
# 准备文本内容
|
216 |
-
text_content = {
|
217 |
-
'Hall': f"{row['Hall']}#",
|
218 |
-
'Index': f"{row['Index']}.",
|
219 |
-
'Movie': row['Movie'],
|
220 |
-
'Pinyin': row['Pinyin'],
|
221 |
-
'Time': row['Time']
|
222 |
-
}[col_name]
|
223 |
|
224 |
-
|
225 |
-
|
226 |
-
|
227 |
-
# 电影名列特殊处理
|
228 |
-
if col_name == 'Movie':
|
229 |
-
font_to_use = base_font.copy()
|
230 |
-
# 检查宽度并调整字体
|
231 |
-
text_w_inch = get_text_width_inch(text_content, font_to_use)
|
232 |
-
max_w_inch = cell_width * 0.9 # 目标宽度为单元格宽度的90%
|
233 |
-
if text_w_inch > max_w_inch:
|
234 |
-
scale_factor = max_w_inch / text_w_inch
|
235 |
-
font_to_use.set_size(font_size_pt * scale_factor)
|
236 |
|
237 |
-
|
238 |
-
|
239 |
-
|
240 |
-
|
241 |
-
|
|
|
|
|
242 |
|
243 |
-
|
244 |
|
245 |
-
#
|
246 |
-
ax.plot([
|
247 |
-
|
248 |
-
|
249 |
-
ax.text(0.1, A4_SIZE_INCHES[1] - 0.3, date_str,
|
250 |
-
fontproperties=get_font(12), color='gray', ha='left', va='top')
|
251 |
-
|
252 |
-
figs[fmt] = fig
|
253 |
|
254 |
-
#
|
255 |
png_buffer = io.BytesIO()
|
256 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
257 |
png_buffer.seek(0)
|
258 |
image_base64 = base64.b64encode(png_buffer.getvalue()).decode()
|
259 |
-
plt.close(figs['png'])
|
260 |
-
|
261 |
-
pdf_buffer = io.BytesIO()
|
262 |
-
figs['pdf'].savefig(pdf_buffer, format='pdf', dpi=DPI)
|
263 |
pdf_buffer.seek(0)
|
264 |
pdf_base64 = base64.b64encode(pdf_buffer.getvalue()).decode()
|
265 |
-
|
266 |
-
|
267 |
return {
|
268 |
'png': f"data:image/png;base64,{image_base64}",
|
269 |
'pdf': f"data:application/pdf;base64,{pdf_base64}"
|
270 |
}
|
271 |
|
|
|
|
|
|
|
272 |
|
273 |
-
# --- Streamlit
|
274 |
-
st.set_page_config(page_title="LED
|
275 |
-
st.title("LED
|
276 |
-
st.markdown("请上传影院系统导出的 `放映时间核对表.xls` 文件。系统将自动处理数据并生成专业、美观的A4打印布局。")
|
277 |
|
278 |
-
uploaded_file = st.file_uploader("
|
279 |
|
280 |
if uploaded_file:
|
281 |
-
with st.spinner("
|
282 |
schedule, date_str = process_schedule(uploaded_file)
|
283 |
if schedule is not None and not schedule.empty:
|
284 |
output = create_print_layout(schedule, date_str)
|
285 |
-
|
286 |
-
|
287 |
-
|
288 |
-
|
289 |
-
|
290 |
-
|
291 |
-
st.download_button(
|
292 |
-
label="📥 下载 PNG 图像",
|
293 |
-
data=base64.b64decode(output['png'].split(',')[1]),
|
294 |
-
file_name=f"排片表_{date_str}.png",
|
295 |
-
mime="image/png"
|
296 |
-
)
|
297 |
-
with col2:
|
298 |
-
st.download_button(
|
299 |
-
label="📄 下载 PDF 文档",
|
300 |
-
data=base64.b64decode(output['pdf'].split(',')[1]),
|
301 |
-
file_name=f"排片表_{date_str}.pdf",
|
302 |
-
mime="application/pdf"
|
303 |
-
)
|
304 |
-
|
305 |
-
# 创建选项卡进行预览
|
306 |
-
tab1, tab2 = st.tabs(["📄 PDF 预览", "🖼️ PNG 预览"])
|
307 |
-
|
308 |
-
with tab1:
|
309 |
-
st.markdown(f'<iframe src="{output["pdf"]}" width="100%" height="800" type="application/pdf"></iframe>', unsafe_allow_html=True)
|
310 |
-
|
311 |
-
with tab2:
|
312 |
-
st.image(output['png'], use_container_width=True)
|
313 |
else:
|
314 |
-
st.error("
|
|
|
6 |
import base64
|
7 |
import os
|
8 |
from datetime import datetime, timedelta
|
9 |
+
import math
|
10 |
+
from matplotlib.patches import FancyBboxPatch, Rectangle
|
11 |
from pypinyin import lazy_pinyin, Style
|
12 |
from matplotlib.backends.backend_pdf import PdfPages
|
13 |
|
|
|
14 |
def get_font(size=14):
|
15 |
+
"""Loads the specified font file, falling back to a default."""
|
16 |
+
font_path = "simHei.ttc"
|
17 |
if not os.path.exists(font_path):
|
18 |
+
font_path = "SimHei.ttf"
|
|
|
19 |
if not os.path.exists(font_path):
|
20 |
+
# As a last resort, return default font if SimHei is not found
|
21 |
+
st.warning("Font file 'simHei.ttc' or 'SimHei.ttf' not found. Text may not render correctly.")
|
22 |
+
return font_manager.FontProperties(size=size)
|
|
|
|
|
|
|
23 |
return font_manager.FontProperties(fname=font_path, size=size)
|
24 |
|
|
|
25 |
def get_pinyin_abbr(text):
|
26 |
+
"""Gets the first letter of the Pinyin for the first two Chinese characters."""
|
27 |
+
if not text:
|
28 |
return ""
|
29 |
+
chars = [c for c in text if '\u4e00' <= c <= '\u9fff'][:2]
|
30 |
+
if not chars:
|
|
|
|
|
|
|
31 |
return ""
|
32 |
+
return ''.join(lazy_pinyin(chars, style=Style.FIRST_LETTER)).upper()
|
|
|
|
|
33 |
|
|
|
34 |
def process_schedule(file):
|
35 |
+
"""Reads and processes the movie schedule from the uploaded Excel file."""
|
36 |
try:
|
37 |
+
# Try to read the date from a specific cell
|
38 |
date_df = pd.read_excel(file, header=None, skiprows=7, nrows=1, usecols=[3])
|
39 |
date_str = pd.to_datetime(date_df.iloc[0, 0]).strftime('%Y-%m-%d')
|
40 |
base_date = pd.to_datetime(date_str).date()
|
41 |
except Exception:
|
42 |
+
# Fallback to today's date
|
|
|
43 |
base_date = datetime.today().date()
|
44 |
+
date_str = base_date.strftime('%Y-%m-%d')
|
45 |
|
46 |
try:
|
|
|
47 |
df = pd.read_excel(file, header=9, usecols=[1, 2, 4, 5])
|
48 |
df.columns = ['Hall', 'StartTime', 'EndTime', 'Movie']
|
|
|
|
|
49 |
df['Hall'] = df['Hall'].ffill()
|
50 |
df.dropna(subset=['StartTime', 'EndTime', 'Movie'], inplace=True)
|
51 |
df['Hall'] = df['Hall'].astype(str).str.extract(r'(\d+号)')
|
52 |
df.dropna(subset=['Hall'], inplace=True)
|
53 |
|
54 |
+
# Convert times to datetime objects
|
55 |
df['StartTime_dt'] = pd.to_datetime(df['StartTime'], format='%H:%M', errors='coerce').apply(
|
56 |
lambda t: t.replace(year=base_date.year, month=base_date.month, day=base_date.day) if pd.notnull(t) else t
|
57 |
)
|
58 |
df['EndTime_dt'] = pd.to_datetime(df['EndTime'], format='%H:%M', errors='coerce').apply(
|
59 |
lambda t: t.replace(year=base_date.year, month=base_date.month, day=base_date.day) if pd.notnull(t) else t
|
60 |
)
|
61 |
+
# Handle overnight screenings
|
|
|
|
|
62 |
df.loc[df['EndTime_dt'] < df['StartTime_dt'], 'EndTime_dt'] += timedelta(days=1)
|
63 |
df = df.sort_values(['Hall', 'StartTime_dt'])
|
64 |
+
|
65 |
+
# Merge consecutive shows of the same movie
|
66 |
merged_rows = []
|
67 |
+
for hall, group in df.groupby('Hall'):
|
|
|
68 |
current = None
|
69 |
+
for _, row in group.sort_values('StartTime_dt').iterrows():
|
70 |
if current is None:
|
71 |
current = row.copy()
|
72 |
+
elif row['Movie'] == current['Movie']:
|
73 |
+
current['EndTime_dt'] = row['EndTime_dt']
|
74 |
else:
|
75 |
+
merged_rows.append(current)
|
76 |
+
current = row.copy()
|
|
|
|
|
|
|
77 |
if current is not None:
|
78 |
merged_rows.append(current)
|
79 |
+
|
80 |
if not merged_rows:
|
81 |
return None, date_str
|
82 |
|
83 |
merged_df = pd.DataFrame(merged_rows)
|
84 |
|
85 |
+
# Adjust times as per original logic
|
86 |
merged_df['StartTime_dt'] -= timedelta(minutes=10)
|
87 |
merged_df['EndTime_dt'] -= timedelta(minutes=5)
|
88 |
|
89 |
+
merged_df['StartTime_str'] = merged_df['StartTime_dt'].dt.strftime('%H:%M')
|
90 |
+
merged_df['EndTime_str'] = merged_df['EndTime_dt'].dt.strftime('%H:%M')
|
|
|
|
|
91 |
|
92 |
+
return merged_df[['Hall', 'Movie', 'StartTime_str', 'EndTime_str']], date_str
|
|
|
93 |
except Exception as e:
|
94 |
+
st.error(f"Error processing the file: {e}")
|
95 |
return None, date_str
|
96 |
|
97 |
+
def get_text_dimensions(text, font_props, fig):
|
98 |
+
"""Helper function to calculate the width and height of a text string in inches."""
|
99 |
+
renderer = fig.canvas.get_renderer()
|
100 |
+
t = fig.text(0, 0, text, fontproperties=font_props, visible=False)
|
101 |
+
bbox = t.get_window_extent(renderer=renderer)
|
102 |
+
t.remove()
|
103 |
+
return bbox.width / fig.dpi, bbox.height / fig.dpi
|
104 |
+
|
105 |
+
def find_font_size(target_height_inches, font_path, fig, text_to_measure="Xg"):
|
106 |
+
"""Finds the font size (in points) that results in a given text height (in inches)."""
|
107 |
+
low, high = 0, 100 # Binary search range for font size
|
108 |
+
best_size = 10
|
109 |
+
|
110 |
+
for _ in range(10): # 10 iterations for precision
|
111 |
+
mid = (low + high) / 2
|
112 |
+
if mid <= 0: break
|
113 |
+
props = font_manager.FontProperties(fname=font_path, size=mid)
|
114 |
+
_, h = get_text_dimensions(text_to_measure, props, fig)
|
115 |
+
if h > target_height_inches:
|
116 |
+
high = mid
|
117 |
+
else:
|
118 |
+
best_size = mid
|
119 |
+
low = mid
|
120 |
+
return best_size
|
121 |
|
|
|
122 |
def create_print_layout(data, date_str):
|
123 |
+
"""Creates the PNG and PDF print layouts based on the new grid requirements."""
|
124 |
if data is None or data.empty:
|
125 |
return None
|
126 |
|
127 |
+
A4_WIDTH_IN, A4_HEIGHT_IN = 8.27, 11.69
|
|
|
128 |
|
129 |
+
# 1. Layout Calculation
|
130 |
+
total_A = len(data) + 2 # Total rows + top/bottom margin
|
131 |
+
row_height = A4_HEIGHT_IN / total_A
|
132 |
+
side_margin_width = row_height # Left/right blank columns width
|
133 |
+
content_width = A4_WIDTH_IN - 2 * side_margin_width
|
134 |
+
target_font_height = row_height * 0.9
|
135 |
+
|
136 |
+
# --- Calculate font sizes and column widths ---
|
137 |
+
dummy_fig = plt.figure()
|
138 |
+
font_path = "simHei.ttc" if os.path.exists("simHei.ttc") else "SimHei.ttf"
|
139 |
+
master_font_size = find_font_size(target_font_height, font_path, dummy_fig)
|
140 |
+
master_font = get_font(size=master_font_size)
|
141 |
+
|
142 |
+
# Prepare content to measure for max width
|
143 |
+
data['Pinyin'] = data['Movie'].apply(get_pinyin_abbr)
|
144 |
+
data['TimeStr'] = data['StartTime_str'] + ' - ' + data['EndTime_str']
|
145 |
|
146 |
+
max_hall_content = max(data['Hall'].str.replace('号', ''), key=len) + "#"
|
147 |
+
max_seq_content = f"{data.groupby('Hall').size().max()}."
|
148 |
+
max_pinyin_content = max(data['Pinyin'], key=len) if not data['Pinyin'].empty else "PY"
|
149 |
+
max_time_content = max(data['TimeStr'], key=len)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
150 |
|
151 |
+
w_hall, _ = get_text_dimensions(max_hall_content, master_font, dummy_fig)
|
152 |
+
w_seq, _ = get_text_dimensions(max_seq_content, master_font, dummy_fig)
|
153 |
+
w_pinyin, _ = get_text_dimensions(max_pinyin_content, master_font, dummy_fig)
|
154 |
+
w_time, _ = get_text_dimensions(max_time_content, master_font, dummy_fig)
|
155 |
+
plt.close(dummy_fig)
|
156 |
+
|
157 |
+
col_width_hall = w_hall * 1.1
|
158 |
+
col_width_seq = w_seq * 1.1
|
159 |
+
col_width_pinyin = w_pinyin * 1.1
|
160 |
+
col_width_time = w_time * 1.1
|
161 |
+
|
162 |
+
col_width_movie = content_width - (col_width_hall + col_width_seq + col_width_pinyin + col_width_time)
|
163 |
|
164 |
+
if col_width_movie <= 0:
|
165 |
+
st.error("Content is too wide for the page. The movie title column has no space.")
|
166 |
+
return None
|
167 |
|
168 |
+
# --- Create figures for drawing ---
|
169 |
+
output_figs = {}
|
170 |
for fmt in ['png', 'pdf']:
|
171 |
+
fig = plt.figure(figsize=(A4_WIDTH_IN, A4_HEIGHT_IN), dpi=300)
|
172 |
ax = fig.add_subplot(111)
|
173 |
ax.set_axis_off()
|
174 |
fig.subplots_adjust(left=0, right=1, top=1, bottom=0)
|
175 |
+
ax.set_xlim(0, A4_WIDTH_IN)
|
176 |
+
ax.set_ylim(0, A4_HEIGHT_IN)
|
177 |
+
|
178 |
+
# Define column x-positions (from left to right)
|
179 |
+
x_pos = [0] * 7
|
180 |
+
x_pos[0] = 0
|
181 |
+
x_pos[1] = side_margin_width
|
182 |
+
x_pos[2] = x_pos[1] + col_width_hall
|
183 |
+
x_pos[3] = x_pos[2] + col_width_seq
|
184 |
+
x_pos[4] = x_pos[3] + col_width_movie
|
185 |
+
x_pos[5] = x_pos[4] + col_width_pinyin
|
186 |
+
x_pos[6] = x_pos[5] + col_width_time
|
187 |
+
|
188 |
+
# Add date string to the top margin area
|
189 |
+
ax.text(A4_WIDTH_IN - side_margin_width, A4_HEIGHT_IN - (row_height / 2), date_str,
|
190 |
+
ha='right', va='center', color='#A9A9A9', fontproperties=get_font(12))
|
191 |
+
|
192 |
+
# --- Drawing Loop ---
|
193 |
+
y_cursor = A4_HEIGHT_IN - row_height # Start below top margin
|
194 |
+
drawn_halls = set()
|
195 |
|
196 |
+
for hall_name, group in data.groupby('Hall', sort=False):
|
197 |
+
for i, (_, row) in enumerate(group.iterrows()):
|
198 |
+
y_bottom = y_cursor - row_height
|
199 |
+
y_center = y_cursor - (row_height / 2)
|
200 |
+
|
201 |
+
# Draw dotted cell borders
|
202 |
+
for c in range(5):
|
203 |
+
ax.add_patch(Rectangle((x_pos[c+1], y_bottom), x_pos[c+2] - x_pos[c+1], row_height,
|
204 |
+
facecolor='none', edgecolor='gray', linestyle=':', linewidth=0.5))
|
205 |
+
|
206 |
+
# Column 1: Hall Number (only on first row of the group)
|
207 |
+
if hall_name not in drawn_halls:
|
208 |
+
hall_num_text = f"${row['Hall'].replace('号', '')}^{{\\#}}$"
|
209 |
+
ax.text(x_pos[1] + col_width_hall / 2, y_center, hall_num_text,
|
210 |
+
ha='center', va='center', fontproperties=master_font)
|
211 |
+
drawn_halls.add(hall_name)
|
212 |
+
|
213 |
+
# Column 2: Sequence Number
|
214 |
+
ax.text(x_pos[2] + col_width_seq / 2, y_center, f"{i+1}.",
|
215 |
+
ha='center', va='center', fontproperties=master_font)
|
216 |
|
217 |
+
# Column 3: Movie Title (with font scaling)
|
218 |
+
font_movie = master_font.copy()
|
219 |
+
movie_cell_inner_width = col_width_movie * 0.9
|
220 |
+
|
221 |
+
temp_fig = plt.figure()
|
222 |
+
w_movie, _ = get_text_dimensions(row['Movie'], font_movie, temp_fig)
|
223 |
+
plt.close(temp_fig)
|
224 |
|
225 |
+
if w_movie > movie_cell_inner_width:
|
226 |
+
scale_factor = movie_cell_inner_width / w_movie
|
227 |
+
font_movie.set_size(font_movie.get_size() * scale_factor)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
228 |
|
229 |
+
ax.text(x_pos[3] + col_width_movie * 0.05, y_center, row['Movie'],
|
230 |
+
ha='left', va='center', fontproperties=font_movie)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
231 |
|
232 |
+
# Column 4: Pinyin Abbreviation
|
233 |
+
ax.text(x_pos[4] + col_width_pinyin / 2, y_center, row['Pinyin'],
|
234 |
+
ha='center', va='center', fontproperties=master_font)
|
235 |
+
|
236 |
+
# Column 5: Time
|
237 |
+
ax.text(x_pos[5] + col_width_time / 2, y_center, row['TimeStr'],
|
238 |
+
ha='center', va='center', fontproperties=master_font)
|
239 |
|
240 |
+
y_cursor -= row_height
|
241 |
|
242 |
+
# Draw black separator line below each hall group
|
243 |
+
ax.plot([x_pos[1], x_pos[6]], [y_cursor, y_cursor], color='black', linewidth=1.0)
|
244 |
+
|
245 |
+
output_figs[fmt] = fig
|
|
|
|
|
|
|
|
|
246 |
|
247 |
+
# --- Save figures to in-memory buffers ---
|
248 |
png_buffer = io.BytesIO()
|
249 |
+
output_figs['png'].savefig(png_buffer, format='png', bbox_inches=None, pad_inches=0)
|
250 |
+
plt.close(output_figs['png'])
|
251 |
+
|
252 |
+
pdf_buffer = io.BytesIO()
|
253 |
+
output_figs['pdf'].savefig(pdf_buffer, format='pdf', bbox_inches=None, pad_inches=0)
|
254 |
+
plt.close(output_figs['pdf'])
|
255 |
+
|
256 |
+
# Encode for display
|
257 |
png_buffer.seek(0)
|
258 |
image_base64 = base64.b64encode(png_buffer.getvalue()).decode()
|
|
|
|
|
|
|
|
|
259 |
pdf_buffer.seek(0)
|
260 |
pdf_base64 = base64.b64encode(pdf_buffer.getvalue()).decode()
|
261 |
+
|
|
|
262 |
return {
|
263 |
'png': f"data:image/png;base64,{image_base64}",
|
264 |
'pdf': f"data:application/pdf;base64,{pdf_base64}"
|
265 |
}
|
266 |
|
267 |
+
def display_pdf(base64_pdf):
|
268 |
+
"""Generates the HTML to embed a PDF in Streamlit."""
|
269 |
+
return f'<iframe src="{base64_pdf}" width="100%" height="800" type="application/pdf"></iframe>'
|
270 |
|
271 |
+
# --- Streamlit App ---
|
272 |
+
st.set_page_config(page_title="LED Screen Schedule Printing", layout="wide")
|
273 |
+
st.title("LED Screen Schedule Printing")
|
|
|
274 |
|
275 |
+
uploaded_file = st.file_uploader("Select the 'Screening Time Checklist.xls' file", type=["xls"])
|
276 |
|
277 |
if uploaded_file:
|
278 |
+
with st.spinner("Processing file, please wait..."):
|
279 |
schedule, date_str = process_schedule(uploaded_file)
|
280 |
if schedule is not None and not schedule.empty:
|
281 |
output = create_print_layout(schedule, date_str)
|
282 |
+
if output:
|
283 |
+
tab1, tab2 = st.tabs(["PDF Preview", "PNG Preview"])
|
284 |
+
with tab1:
|
285 |
+
st.markdown(display_pdf(output['pdf']), unsafe_allow_html=True)
|
286 |
+
with tab2:
|
287 |
+
st.image(output['png'], use_container_width=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
288 |
else:
|
289 |
+
st.error("Could not process the file. Please check that the file format and content are correct.")
|