File size: 4,071 Bytes
80c4080
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""
将甘特图完整 HTML 文档渲染为 PNG(headless Chrome),再转 JPG,并写入本地缓存目录供下载。
依赖:html2image(需本机已安装 Chrome/Chromium)、Pillow。
"""
import io
import os
from datetime import datetime
from typing import Optional, Tuple


def _build_export_html(html_document: str) -> str:
    """注入导出专用样式:关闭滚动裁切,避免截图只保留可视区域。"""
    export_css = """
<style>
html, body {
    margin: 0 !important;
    padding: 0 !important;
    background: #fff !important;
    width: max-content !important;
    display: inline-block !important;
}
.scroll-wrapper { overflow: visible !important; width: max-content !important; }
.schedule-container { width: max-content !important; }
</style>
"""
    if "</head>" in html_document:
        return html_document.replace("</head>", f"{export_css}\n</head>", 1)
    return f"{export_css}\n{html_document}"


def _trim_png_whitespace(png_bytes: bytes, threshold: int = 250, padding: int = 8) -> bytes:
    """裁切四周近白空白区域,让图像内容尽量铺满画面。"""
    from PIL import Image

    im = Image.open(io.BytesIO(png_bytes)).convert("RGB")
    gray = im.convert("L")
    mask = gray.point(lambda x: 255 if x < threshold else 0)
    bbox = mask.getbbox()
    if not bbox:
        return png_bytes

    left, top, right, bottom = bbox
    left = max(0, left - padding)
    top = max(0, top - padding)
    right = min(im.width, right + padding)
    bottom = min(im.height, bottom + padding)

    cropped = im.crop((left, top, right, bottom))
    out = io.BytesIO()
    cropped.save(out, format="PNG", optimize=True)
    return out.getvalue()


def html_document_to_png_bytes(html_document: str, width: int = 1680, height: int = 5000) -> bytes:
    """使用 html2image 将完整 HTML 页面渲染为高分辨率 PNG 字节。"""
    import tempfile

    from html2image import Html2Image

    prepared_html = _build_export_html(html_document)

    with tempfile.TemporaryDirectory() as td:
        hti = Html2Image(
            output_path=td,
            size=(width, height),
            custom_flags=[
                "--hide-scrollbars",
                "--force-device-scale-factor=3",
                "--default-background-color=ffffff",
            ],
        )
        hti.screenshot(html_str=prepared_html, save_as="gantt.png")
        path = os.path.join(td, "gantt.png")
        with open(path, "rb") as f:
            raw = f.read()
    return _trim_png_whitespace(raw)


def png_bytes_to_jpeg_bytes(png_bytes: bytes, quality: int = 95) -> bytes:
    from PIL import Image

    im = Image.open(io.BytesIO(png_bytes)).convert("RGB")
    buf = io.BytesIO()
    im.save(buf, format="JPEG", quality=quality, optimize=True, subsampling=0)
    return buf.getvalue()


def save_png_jpeg_to_cache(
    png_bytes: bytes,
    jpg_bytes: bytes,
    base_name: str,
    cache_dir: Optional[str] = None,
) -> Tuple[str, str]:
    """后台保存 PNG/JPG 到 cinema_cache/gantt_exports/,返回文件路径。"""
    if cache_dir is None:
        cache_dir = os.path.join("cinema_cache", "gantt_exports")
    os.makedirs(cache_dir, exist_ok=True)
    ts = datetime.now().strftime("%Y%m%d_%H%M%S")
    safe = "".join(c if c.isalnum() or c in "-_" else "_" for c in base_name)[:80]
    png_path = os.path.join(cache_dir, f"{safe}_{ts}.png")
    jpg_path = os.path.join(cache_dir, f"{safe}_{ts}.jpg")
    with open(png_path, "wb") as f:
        f.write(png_bytes)
    with open(jpg_path, "wb") as f:
        f.write(jpg_bytes)
    return png_path, jpg_path


def build_png_and_jpeg_for_gantt(html_document: str, num_hall_rows: int) -> Tuple[bytes, bytes]:
    """
    根据影厅行数估算截图尺寸,生成更清晰且内容占满画面的 PNG/JPG 字节。
    """
    nrows = max(1, num_hall_rows)
    width = 1680
    height = min(22000, max(2200, 360 + nrows * 120))
    png = html_document_to_png_bytes(html_document, width=width, height=height)
    jpg = png_bytes_to_jpeg_bytes(png)
    return png, jpg