File size: 13,032 Bytes
70694c9
 
 
 
 
 
 
 
2b5d755
2471374
7c5a0e0
70694c9
2b5d755
70694c9
2b5d755
 
1878cfc
2b5d755
 
41c541d
2b5d755
 
 
 
 
 
ee0147b
70694c9
2b5d755
70694c9
2b5d755
 
1878cfc
2b5d755
 
 
 
 
41c541d
2b5d755
 
1878cfc
70694c9
2b5d755
ee0147b
2b5d755
1878cfc
2b5d755
1878cfc
 
 
1b1b918
2b5d755
1878cfc
 
2b5d755
1878cfc
2b5d755
1878cfc
 
ee0147b
2b5d755
1878cfc
 
41c541d
2b5d755
a215102
2b5d755
1878cfc
 
 
 
 
 
41c541d
 
2b5d755
1878cfc
41c541d
ee0147b
2b5d755
1878cfc
41c541d
1878cfc
 
 
 
 
 
928edc3
2b5d755
928edc3
 
 
1878cfc
 
ee0147b
 
2b5d755
 
 
ee0147b
2b5d755
41c541d
 
ee0147b
2b5d755
 
ee0147b
41c541d
 
2b5d755
41c541d
1b1b918
2b5d755
1878cfc
70694c9
41c541d
2b5d755
ee0147b
2b5d755
1878cfc
 
1b1b918
2b5d755
 
 
 
 
 
 
 
 
 
 
 
 
 
41c541d
2b5d755
 
 
 
 
 
 
 
 
 
 
41c541d
2b5d755
41c541d
2b5d755
 
 
 
 
 
 
41c541d
2b5d755
 
 
 
 
 
 
 
 
41c541d
2b5d755
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41c541d
2b5d755
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1878cfc
2b5d755
1878cfc
 
2b5d755
41c541d
1878cfc
2b5d755
1878cfc
 
2b5d755
41c541d
1878cfc
 
 
 
70694c9
 
2b5d755
ee0147b
 
2b5d755
70694c9
2b5d755
70694c9
 
2b5d755
41c541d
ee0147b
 
2b5d755
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
import pandas as pd
import streamlit as st
import matplotlib.pyplot as plt
import matplotlib.font_manager as font_manager
import io
import base64
import os
from datetime import datetime, timedelta
from matplotlib.patches import Rectangle
from pypinyin import lazy_pinyin, Style
from matplotlib.backends.backend_pdf import PdfPages

# --- 字体设置 ---
def get_font(size=14):
    """根据操作系统环境查找并返回中文字体属性"""
    font_path = "simHei.ttc" # 优先使用 simHei.ttc
    if not os.path.exists(font_path):
        font_path = "SimHei.ttf" # SimHei.ttf 作为备选
    # 如果两者都不存在,可以添加更多备选字体路径
    if not os.path.exists(font_path):
        # for Windows
        font_path = "C:/Windows/Fonts/simhei.ttf"
    if not os.path.exists(font_path):
        # for MacOS
         font_path = "/System/Library/Fonts/STHeiti Medium.ttc"
    # 如果仍然找不到,matplotlib会回退到默认字体
    return font_manager.FontProperties(fname=font_path, size=size)

# --- 拼音处理 ---
def get_pinyin_abbr(text):
    """获取文本前两个汉字的拼音首字母"""
    if not text or not isinstance(text, str):
        return ""
    # 提取中文字符
    chars = [c for c in text if '\u4e00' <= c <= '\u9fff']
    # 取前两个汉字
    chars_to_process = chars[:2]
    if not chars_to_process:
        return ""
    # 获取拼音首字母并转为大写
    pinyin_list = lazy_pinyin(chars_to_process, style=Style.FIRST_LETTER)
    return ''.join(pinyin_list).upper()

# --- 数据处理 ---
def process_schedule(file):
    """读取并处理 Excel 文件,返回格式化的 DataFrame 和日期"""
    try:
        # 尝试读取日期
        date_df = pd.read_excel(file, header=None, skiprows=7, nrows=1, usecols=[3])
        date_str = pd.to_datetime(date_df.iloc[0, 0]).strftime('%Y-%m-%d')
        base_date = pd.to_datetime(date_str).date()
    except Exception:
        # 读取失败则使用当天日期
        date_str = datetime.today().strftime('%Y-%m-%d')
        base_date = datetime.today().date()

    try:
        # 读取排片数据
        df = pd.read_excel(file, header=9, usecols=[1, 2, 4, 5])
        df.columns = ['Hall', 'StartTime', 'EndTime', 'Movie']
        
        # 数据清洗
        df['Hall'] = df['Hall'].ffill()
        df.dropna(subset=['StartTime', 'EndTime', 'Movie'], inplace=True)
        df['Hall'] = df['Hall'].astype(str).str.extract(r'(\d+号)')
        df.dropna(subset=['Hall'], inplace=True)

        # 时间转换
        df['StartTime_dt'] = pd.to_datetime(df['StartTime'], format='%H:%M', errors='coerce').apply(
            lambda t: t.replace(year=base_date.year, month=base_date.month, day=base_date.day) if pd.notnull(t) else t
        )
        df['EndTime_dt'] = pd.to_datetime(df['EndTime'], format='%H:%M', errors='coerce').apply(
            lambda t: t.replace(year=base_date.year, month=base_date.month, day=base_date.day) if pd.notnull(t) else t
        )
        df.dropna(subset=['StartTime_dt', 'EndTime_dt'], inplace=True)
        
        # 处理跨天结束时间
        df.loc[df['EndTime_dt'] < df['StartTime_dt'], 'EndTime_dt'] += timedelta(days=1)
        df = df.sort_values(['Hall', 'StartTime_dt'])
        
        # 合并同一影厅的连续相同影片
        merged_rows = []
        for _, group in df.groupby('Hall'):
            group = group.sort_values('StartTime_dt')
            current = None
            for _, row in group.iterrows():
                if current is None:
                    current = row.copy()
                else:
                    if row['Movie'] == current['Movie']:
                        current['EndTime_dt'] = row['EndTime_dt']
                    else:
                        merged_rows.append(current)
                        current = row.copy()
            if current is not None:
                merged_rows.append(current)
        
        if not merged_rows:
            return None, date_str
            
        merged_df = pd.DataFrame(merged_rows)
        
        # 统一调整时间
        merged_df['StartTime_dt'] -= timedelta(minutes=10)
        merged_df['EndTime_dt'] -= timedelta(minutes=5)
        
        # 格式化最终输出的列
        merged_df['Time'] = merged_df['StartTime_dt'].dt.strftime('%H:%M') + ' - ' + merged_df['EndTime_dt'].dt.strftime('%H:%M')
        merged_df['Pinyin'] = merged_df['Movie'].apply(get_pinyin_abbr)
        merged_df['Hall'] = merged_df['Hall'].str.replace('号', '')
        
        return merged_df[['Hall', 'Movie', 'Pinyin', 'Time']], date_str

    except Exception as e:
        st.error(f"处理 Excel 数据时发生错误: {e}")
        return None, date_str


# --- 打印布局生成 ---
def create_print_layout(data, date_str):
    """根据处理好的数据,生成用于打印的 PNG 和 PDF 布局"""
    if data is None or data.empty:
        return None

    A4_SIZE_INCHES = (8.27, 11.69)
    DPI = 300
    
    # 准备一个临时的 figure 用于计算文本渲染尺寸
    temp_fig = plt.figure(figsize=A4_SIZE_INCHES, dpi=DPI)
    renderer = temp_fig.canvas.get_renderer()

    # 1. 计算行高
    num_movies = len(data)
    num_halls = len(data['Hall'].unique())
    # 总行数 = 电影条目数 + 厅间分隔数 + 上下留白(2)
    # total_layout_rows = num_movies + (num_halls - 1) + 2
    total_layout_rows = num_movies + 2 # 简化为条目数+2,使行高更宽松
    row_height_inch = A4_SIZE_INCHES[1] / total_layout_rows
    
    # 2. 计算基准字体大小 (点)
    # 1 point = 1/72 inch.
    # 字体高度为行高的 90%
    font_size_pt = row_height_inch * 0.9 * 72 
    base_font = get_font(font_size_pt)

    # 3. 计算各列宽度 (除电影名外)
    def get_text_width_inch(text, font):
        t = plt.text(0, 0, text, fontproperties=font)
        bbox = t.get_window_extent(renderer=renderer)
        width_pixels = bbox.width
        t.remove()
        return width_pixels / DPI

    # 找到每列最长的内容
    data['Index'] = data.groupby('Hall').cumcount() + 1
    max_hall_str = data['Hall'].max() + "#" # e.g. "10#"
    max_index_str = str(data['Index'].max()) + "." # e.g. "5."
    max_pinyin_str = data['Pinyin'].apply(len).max() * "A" # e.g. "PY" -> "AA"
    max_time_str = data['Time'].apply(len).idxmax()
    max_time_str = data.loc[max_time_str, 'Time']

    col_widths = {}
    col_widths['Hall'] = get_text_width_inch(max_hall_str, base_font) * 1.1
    col_widths['Index'] = get_text_width_inch(max_index_str, base_font) * 1.1
    col_widths['Pinyin'] = get_text_width_inch(max_pinyin_str, base_font) * 1.1
    col_widths['Time'] = get_text_width_inch(max_time_str, base_font) * 1.1
    
    # 电影名列的宽度为剩余宽度
    non_movie_width = sum(col_widths.values())
    col_widths['Movie'] = A4_SIZE_INCHES[0] - non_movie_width

    plt.close(temp_fig) # 关闭临时figure

    # --- 开始正式绘图 ---
    figs = {}
    for fmt in ['png', 'pdf']:
        fig = plt.figure(figsize=A4_SIZE_INCHES, dpi=DPI)
        ax = fig.add_subplot(111)
        ax.set_axis_off()
        fig.subplots_adjust(left=0, right=1, top=1, bottom=0)
        ax.set_xlim(0, A4_SIZE_INCHES[0])
        ax.set_ylim(0, A4_SIZE_INCHES[1])

        # 计算列的 X 轴起始位置
        x_pos = {}
        current_x = 0
        # 新顺序: Hall, Index, Movie, Pinyin, Time
        col_order = ['Hall', 'Index', 'Movie', 'Pinyin', 'Time']
        for col in col_order:
            x_pos[col] = current_x
            current_x += col_widths[col]

        # 从顶部开始绘制 (顶部留出一行空白)
        current_y = A4_SIZE_INCHES[1] - row_height_inch 
        
        halls = sorted(data['Hall'].unique(), key=lambda h: int(h))

        for hall in halls:
            hall_data = data[data['Hall'] == hall].sort_values('Index')
            
            for _, row in hall_data.iterrows():
                y_bottom = current_y - row_height_inch
                
                # 绘制单元格
                for col_name in col_order:
                    cell_x = x_pos[col_name]
                    cell_width = col_widths[col_name]
                    
                    # 绘制灰色虚线边框
                    rect = Rectangle((cell_x, y_bottom), cell_width, row_height_inch,
                                     edgecolor='lightgray', facecolor='none',
                                     linestyle=(0, (1, 2)), linewidth=0.8, zorder=1)
                    ax.add_patch(rect)
                    
                    # 准备文本内容
                    text_content = {
                        'Hall': f"{row['Hall']}#",
                        'Index': f"{row['Index']}.",
                        'Movie': row['Movie'],
                        'Pinyin': row['Pinyin'],
                        'Time': row['Time']
                    }[col_name]

                    # 文本垂直居中
                    text_y = y_bottom + row_height_inch / 2
                    
                    # 电影名列特殊处理
                    if col_name == 'Movie':
                        font_to_use = base_font.copy()
                        # 检查宽度并调整字体
                        text_w_inch = get_text_width_inch(text_content, font_to_use)
                        max_w_inch = cell_width * 0.9 # 目标宽度为单元格宽度的90%
                        if text_w_inch > max_w_inch:
                            scale_factor = max_w_inch / text_w_inch
                            font_to_use.set_size(font_size_pt * scale_factor)

                        ax.text(cell_x + cell_width * 0.05, text_y, text_content, # 左对齐
                                fontproperties=font_to_use, ha='left', va='center', clip_on=True)
                    else: # 其他列
                        ax.text(cell_x + cell_width / 2, text_y, text_content, # 居中对齐
                                fontproperties=base_font, ha='center', va='center', clip_on=True)

                current_y -= row_height_inch
            
            # 在每个影厅块结束后绘制黑色分隔线
            ax.plot([0, A4_SIZE_INCHES[0]], [current_y, current_y], color='black', linewidth=1.5, zorder=2)

        # 在左上角添加日期
        ax.text(0.1, A4_SIZE_INCHES[1] - 0.3, date_str, 
                fontproperties=get_font(12), color='gray', ha='left', va='top')

        figs[fmt] = fig

    # 保存到内存
    png_buffer = io.BytesIO()
    figs['png'].savefig(png_buffer, format='png', dpi=DPI)
    png_buffer.seek(0)
    image_base64 = base64.b64encode(png_buffer.getvalue()).decode()
    plt.close(figs['png'])

    pdf_buffer = io.BytesIO()
    figs['pdf'].savefig(pdf_buffer, format='pdf', dpi=DPI)
    pdf_buffer.seek(0)
    pdf_base64 = base64.b64encode(pdf_buffer.getvalue()).decode()
    plt.close(figs['pdf'])

    return {
        'png': f"data:image/png;base64,{image_base64}",
        'pdf': f"data:application/pdf;base64,{pdf_base64}"
    }


# --- Streamlit UI ---
st.set_page_config(page_title="LED 屏幕时间表打印", layout="wide")
st.title("LED 屏幕时间表打印")
st.markdown("请上传影院系统导出的 `放映时间核对表.xls` 文件。系统将自动处理数据并生成专业、美观的A4打印布局。")

uploaded_file = st.file_uploader("选择文件", accept_multiple_files=False, type=["xls", "xlsx"])

if uploaded_file:
    with st.spinner("文件处理与布局生成中,请稍候..."):
        schedule, date_str = process_schedule(uploaded_file)
        if schedule is not None and not schedule.empty:
            output = create_print_layout(schedule, date_str)
            
            st.success(f"成功生成 **{date_str}** 的排片表!")

            # 创建下载按钮
            col1, col2 = st.columns(2)
            with col1:
                 st.download_button(
                    label="📥 下载 PNG 图像",
                    data=base64.b64decode(output['png'].split(',')[1]),
                    file_name=f"排片表_{date_str}.png",
                    mime="image/png"
                )
            with col2:
                st.download_button(
                    label="📄 下载 PDF 文档",
                    data=base64.b64decode(output['pdf'].split(',')[1]),
                    file_name=f"排片表_{date_str}.pdf",
                    mime="application/pdf"
                )

            # 创建选项卡进行预览
            tab1, tab2 = st.tabs(["📄 PDF 预览", "🖼️ PNG 预览"])
            
            with tab1:
                st.markdown(f'<iframe src="{output["pdf"]}" width="100%" height="800" type="application/pdf"></iframe>', unsafe_allow_html=True)
            
            with tab2:
                st.image(output['png'], use_container_width=True)
        else:
            st.error("无法处理文件。请检查文件内容是否为空或格式是否正确。确保文件中包含有效的排片数据。")