File size: 12,108 Bytes
70694c9
 
 
 
9a4c156
70694c9
 
 
 
2471374
7c5a0e0
70694c9
 
9a4c156
ce8fae8
1878cfc
ce8fae8
9a4c156
 
 
 
 
70694c9
 
9a4c156
ce8fae8
1878cfc
ce8fae8
 
41c541d
9a4c156
 
70694c9
4df41bd
 
 
 
 
 
 
 
 
 
 
 
ee0147b
9a4c156
 
 
 
1878cfc
 
 
 
1b1b918
9a4c156
1878cfc
2b5d755
1878cfc
 
 
9a4c156
1878cfc
 
41c541d
9a4c156
1878cfc
 
 
 
 
 
 
41c541d
ce8fae8
1878cfc
9a4c156
1878cfc
ce8fae8
1878cfc
 
ce8fae8
4df41bd
1878cfc
ce8fae8
 
1878cfc
 
9a4c156
2b5d755
ee0147b
41c541d
 
ee0147b
9a4c156
 
ce8fae8
 
41c541d
9a4c156
1b1b918
9a4c156
1878cfc
70694c9
41c541d
ee0147b
9a4c156
 
 
1878cfc
 
1b1b918
9a4c156
4df41bd
9a4c156
ce8fae8
9a4c156
4df41bd
9a4c156
ce8fae8
9a4c156
 
4df41bd
 
 
 
9a4c156
 
ce8fae8
9a4c156
 
41c541d
9a4c156
4df41bd
 
9a4c156
4df41bd
9a4c156
 
 
 
 
 
4df41bd
9a4c156
2b5d755
4df41bd
 
 
 
 
 
9a4c156
 
ce8fae8
9a4c156
 
4df41bd
ce8fae8
9a4c156
 
 
 
 
 
ce8fae8
9a4c156
 
 
41c541d
9a4c156
 
4df41bd
9a4c156
 
 
 
4df41bd
 
9a4c156
 
4df41bd
9a4c156
 
 
 
ce8fae8
4df41bd
9a4c156
4df41bd
2b5d755
9a4c156
 
 
2b5d755
9a4c156
 
 
2b5d755
4df41bd
9a4c156
 
 
 
 
4df41bd
9a4c156
 
 
 
4df41bd
9a4c156
2b5d755
9a4c156
 
2b5d755
4df41bd
 
9a4c156
 
 
4df41bd
9a4c156
 
 
 
 
4df41bd
9a4c156
 
 
ce8fae8
9a4c156
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2b5d755
70694c9
ce8fae8
 
4df41bd
70694c9
ce8fae8
9a4c156
 
70694c9
9a4c156
70694c9
 
ce8fae8
41c541d
ee0147b
 
9a4c156
 
 
 
 
 
 
 
2b5d755
9a4c156
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
import pandas as pd
import streamlit as st
import matplotlib.pyplot as plt
import matplotlib.font_manager as font_manager
from matplotlib.lines import Line2D
import io
import base64
import os
from datetime import datetime, timedelta
from pypinyin import lazy_pinyin, Style
from matplotlib.backends.backend_pdf import PdfPages

def get_font(size=14):
    """Loads a specific TrueType font, defaulting to a common Chinese font."""
    font_path = "simHei.ttc"
    if not os.path.exists(font_path):
        font_path = "SimHei.ttf"
    if os.path.exists(font_path):
        return font_manager.FontProperties(fname=font_path, size=size)
    else:
        st.warning("SimHei font not found. Display may not be correct. Please add simHei.ttc or SimHei.ttf.")
        return font_manager.FontProperties(family='sans-serif', size=size)

def get_pinyin_abbr(text):
    """Gets the first letter of the Pinyin for the first two Chinese characters of a text."""
    if not text:
        return ""
    chars = [c for c in text if '\u4e00' <= c <= '\u9fff'][:2]
    if not chars:
        return ""
    pinyin_list = lazy_pinyin(chars, style=Style.FIRST_LETTER)
    return ''.join(pinyin_list).upper()

def format_seq(n):
    """Converts an integer to a circled number string (e.g., 1 -> ①)."""
    if not isinstance(n, int) or n <= 0:
        return str(n)
    # Unicode characters for circled numbers 1-50
    circled_chars = "①②③④⑤⑥⑦⑧⑨⑩⑪⑫⑬⑭⑮⑯⑰⑱⑲⑳" \
                    "㉑㉒㉓㉔㉕㉖㉗㉘㉙㉚㉛㉜㉝㉞㉟" \
                    "㊱㊲㊳㊴㊵㊶㊷㊸㊹㊺㊻㊼㊽㊾㊿"
    if 1 <= n <= 50:
        return circled_chars[n-1]
    return f'({n})'  # Fallback for numbers greater than 50

def process_schedule(file):
    """
    Processes the uploaded Excel file to extract and clean the movie schedule data.
    Adds a sequence number for movies within each hall.
    """
    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['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.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'):
            current = None
            for _, row in group.sort_values('StartTime_dt').iterrows():
                if current is None:
                    current = row.copy()
                elif 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)
        
        merged_df = pd.DataFrame(merged_rows)
        
        merged_df['StartTime_dt'] -= timedelta(minutes=10)
        merged_df['EndTime_dt'] -= timedelta(minutes=5)
        
        merged_df['Seq'] = merged_df.groupby('Hall').cumcount() + 1
        
        merged_df['StartTime_str'] = merged_df['StartTime_dt'].dt.strftime('%H:%M')
        merged_df['EndTime_str'] = merged_df['EndTime_dt'].dt.strftime('%H:%M')
        
        return merged_df[['Hall', 'Seq', 'Movie', 'StartTime_str', 'EndTime_str']], date_str
    except Exception as e:
        st.error(f"Error processing schedule data: {e}. Please check the file format.")
        return None, date_str


def create_print_layout(data, date_str):
    """
    Generates PNG and PDF print layouts for the movie schedule based on a dynamic grid.
    """
    if data is None or data.empty:
        return None

    # --- 1. Layout and Column Calculation ---
    A4_width_in, A4_height_in = 8.27, 11.69
    dpi = 300

    total_content_rows = len(data)
    totalA = total_content_rows + 2
    row_height = A4_height_in / totalA

    # Prepare data strings for width calculation
    data = data.reset_index(drop=True)
    # Format Hall number with LaTeX for superscript
    data['hall_str'] = '$' + data['Hall'].str.replace('号', '') + '^{\#}$'
    # Format sequence number as circled character
    data['seq_str'] = data['Seq'].apply(format_seq)
    data['pinyin_abbr'] = data['Movie'].apply(get_pinyin_abbr)
    data['time_str'] = data['StartTime_str'] + ' - ' + data['EndTime_str']

    temp_fig = plt.figure(figsize=(A4_width_in, A4_height_in), dpi=dpi)
    renderer = temp_fig.canvas.get_renderer()

    base_font_size_pt = (row_height * 0.9) * 72
    # Specific font size for circled numbers (50% of row height)
    seq_font_size_pt = (row_height * 0.5) * 72
    
    def get_col_width_in(series, font_size_pt, is_math=False):
        """Calculates the required width for a column in inches."""
        if series.empty:
            return 0
        font_prop = get_font(font_size_pt)
        longest_str_idx = series.astype(str).str.len().idxmax()
        max_content = str(series.loc[longest_str_idx])
        text_width_px, _, _ = renderer.get_text_width_height_descent(max_content, font_prop, ismath=is_math)
        return (text_width_px / dpi) * 1.1

    # Calculate widths for all columns
    margin_col_width = row_height
    # Pass is_math=True for hall number width calculation
    hall_col_width = get_col_width_in(data['hall_str'], base_font_size_pt, is_math=True)
    # Use the smaller font size for sequence number width calculation
    seq_col_width = get_col_width_in(data['seq_str'], seq_font_size_pt)
    pinyin_col_width = get_col_width_in(data['pinyin_abbr'], base_font_size_pt)
    time_col_width = get_col_width_in(data['time_str'], base_font_size_pt)

    movie_col_width = A4_width_in - (margin_col_width * 2 + hall_col_width + seq_col_width + pinyin_col_width + time_col_width)
    
    plt.close(temp_fig)

    col_widths = {'hall': hall_col_width, 'seq': seq_col_width, 'movie': movie_col_width, 'pinyin': pinyin_col_width, 'time': time_col_width}
    col_x_starts = {}
    current_x = margin_col_width
    for col_name in ['hall', 'seq', 'movie', 'pinyin', 'time']:
        col_x_starts[col_name] = current_x
        current_x += col_widths[col_name]

    # --- 2. Drawing ---
    def draw_figure(fig, ax):
        renderer = fig.canvas.get_renderer()
        
        for col_name in ['hall', 'seq', 'movie', 'pinyin']:
            x_line = col_x_starts[col_name] + col_widths[col_name]
            line_top_y, line_bottom_y = A4_height_in - row_height, row_height
            ax.add_line(Line2D([x_line, x_line], [line_bottom_y, line_top_y], color='gray', linestyle=':', linewidth=0.5))

        last_hall_drawn = None
        for i, row in data.iterrows():
            y_bottom = A4_height_in - (i + 2) * row_height
            y_center = y_bottom + row_height / 2

            # --- Draw Cell Content ---
            # Hall Number (renders superscript)
            if row['Hall'] != last_hall_drawn:
                ax.text(col_x_starts['hall'] + col_widths['hall'] / 2, y_center, row['hall_str'],
                        fontproperties=get_font(base_font_size_pt), ha='center', va='center')
                last_hall_drawn = row['Hall']

            # Sequence Number (with smaller, specific font size)
            ax.text(col_x_starts['seq'] + col_widths['seq'] / 2, y_center, row['seq_str'],
                    fontproperties=get_font(seq_font_size_pt), ha='center', va='center')

            # Pinyin Abbreviation
            ax.text(col_x_starts['pinyin'] + col_widths['pinyin'] / 2, y_center, row['pinyin_abbr'],
                    fontproperties=get_font(base_font_size_pt), ha='center', va='center')

            # Time String
            ax.text(col_x_starts['time'] + col_widths['time'] / 2, y_center, row['time_str'],
                    fontproperties=get_font(base_font_size_pt), ha='center', va='center')

            # Movie Title (with font scaling)
            movie_font_size = base_font_size_pt
            movie_font_prop = get_font(movie_font_size)
            text_w_px, _, _ = renderer.get_text_width_height_descent(row['Movie'], movie_font_prop, ismath=False)
            text_w_in = text_w_px / dpi
            
            max_width_in = col_widths['movie'] * 0.9
            if text_w_in > max_width_in:
                movie_font_size *= (max_width_in / text_w_in)
                movie_font_prop = get_font(movie_font_size)
            
            ax.text(col_x_starts['movie'] + 0.05, y_center, row['Movie'],
                    fontproperties=movie_font_prop, ha='left', va='center')

            # --- Draw Horizontal Lines ---
            is_last_in_hall = (i == len(data) - 1) or (row['Hall'] != data.loc[i + 1, 'Hall'])
            
            line_start_x = margin_col_width
            line_end_x = A4_width_in - margin_col_width
            if is_last_in_hall:
                ax.add_line(Line2D([line_start_x, line_end_x], [y_bottom, y_bottom], color='black', linestyle='-', linewidth=0.8))
            else:
                ax.add_line(Line2D([line_start_x, line_end_x], [y_bottom, y_bottom], color='gray', linestyle=':', linewidth=0.5))

    # --- 3. Setup Figures and Generate Output ---
    outputs = {}
    for format_type in ['png', 'pdf']:
        fig = plt.figure(figsize=(A4_width_in, A4_height_in), dpi=dpi)
        ax = fig.add_axes([0, 0, 1, 1])
        ax.set_axis_off()
        ax.set_xlim(0, A4_width_in)
        ax.set_ylim(0, A4_height_in)
        
        ax.text(margin_col_width, A4_height_in - (row_height/2), date_str, 
                fontproperties=get_font(10), color='#A9A9A9', ha='left', va='center')

        draw_figure(fig, ax)
        
        buf = io.BytesIO()
        fig.savefig(buf, format=format_type, dpi=dpi, bbox_inches='tight', pad_inches=0)
        buf.seek(0)
        
        data_uri = base64.b64encode(buf.getvalue()).decode()
        mime_type = 'image/png' if format_type == 'png' else 'application/pdf'
        outputs[format_type] = f"data:{mime_type};base64,{data_uri}"
        
        plt.close(fig)

    return outputs


def display_pdf(base64_pdf):
    """Generates the HTML to embed a PDF in Streamlit."""
    return f'<iframe src="{base64_pdf}" width="100%" height="800" type="application/pdf"></iframe>'

# --- Streamlit App ---
st.set_page_config(page_title="LED Screen Schedule Printer", layout="wide")
st.title("LED Screen Schedule Printer")

uploaded_file = st.file_uploader("Select the '放映时间核对表.xls' file", accept_multiple_files=False, type=["xls", "xlsx"])

if uploaded_file:
    with st.spinner("Processing file, please wait..."):
        schedule, date_str = process_schedule(uploaded_file)
        if schedule is not None and not schedule.empty:
            output = create_print_layout(schedule, date_str)
            
            tab1, tab2 = st.tabs(["PDF Preview", "PNG Preview"])
            
            with tab1:
                st.markdown(display_pdf(output['pdf']), unsafe_allow_html=True)
            
            with tab2:
                st.image(output['png'], use_container_width=True)
        else:
            st.error("Could not process the file. Please check if the file format and content are correct.")