#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
朋友圈「首图版」促销海报生成器 —— 针对缩略图尺度重排（不点开也能读到重点）
================================================================
问题：原长版 1080×3441（1:3.19），在朋友圈缩略图里整张缩得很小，标题/促销/卖点读不出。
对策：砍掉图表卡（图表移到长版详情），只留 3 个重点 + 核心实测数字；
      画布压到 1080×~1700，字号抬到 84/48/30（仍守「全图只用 3 种字号」铁律）。

重点顺序：① 标题（蓝横幅）→ ③ 限时促销（暖橙）→ 产品图 → 核心数据 → ② 产品特点（青卡）→ 扫码 → 落款
跑法：env -u PYTHONPATH /usr/bin/python3 脚本/gen_promo_poster_short.py
"""
import base64, io, os, re
from PIL import Image, ImageChops
from playwright.sync_api import sync_playwright

DIR = os.environ.get('POSTER_DIR', '/Users/liuxinyuan/Desktop/Hermes输出-工作类/对外物料/科研朋友圈_20261009')
IMG = os.environ.get('POSTER_IMG_DIR', os.path.join(DIR, 'images'))
BG_DIR = os.environ.get('POSTER_BG_DIR', os.path.join(DIR, 'bg'))
QRDIR = os.environ.get('POSTER_QR_DIR',
                       '/Users/liuxinyuan/Desktop/Hermes输出-工作类/科研2026秋季促销/华南农大海报_20260920/qr')
LOGO = '/Users/liuxinyuan/work/公司资料/logo_transparent.png'
SLOGAN, BRAND = '守护生命健康，缔造美好生活', '宝锐生物 BIORI BIOTECH'
W = 1080
FS_ALLOWED = {'84px', '48px', '30px'}          # 首图版字号铁律（仍为 3 种）


def b64(p):
    return 'data:image/png;base64,' + base64.b64encode(io.open(p, 'rb').read()).decode()


def fit(p, maxw=900, maxh=560):
    im = Image.open(p).convert('RGB')
    bg = Image.new('RGB', im.size, (255, 255, 255))
    bb = ImageChops.difference(im, bg).getbbox()
    if bb:
        q = 6
        im = im.crop((max(0, bb[0]-q), max(0, bb[1]-q), min(im.width, bb[2]+q), min(im.height, bb[3]+q)))
    im.thumbnail((maxw, maxh), Image.LANCZOS)
    f = '/tmp/_sp_%s.jpg' % os.path.basename(p)
    im.save(f, quality=92)
    return 'data:image/jpeg;base64,' + base64.b64encode(io.open(f, 'rb').read()).decode()


def qr_img(name, px=176):
    """二维码：裁静默区 + 最近邻缩放（LANCZOS 会糊细模块导致扫不出）"""
    im = Image.open(os.path.join(QRDIR, name)).convert('L')
    bb = ImageChops.difference(im, Image.new('L', im.size, 255)).getbbox()
    if bb:
        q = 6
        im = im.crop((max(0, bb[0]-q), max(0, bb[1]-q), min(im.width, bb[2]+q), min(im.height, bb[3]+q)))
    im = im.resize((px, px), Image.NEAREST)
    f = '/tmp/_sqr_%s.png' % name[:-4]
    im.convert('RGB').save(f)
    return 'data:image/png;base64,' + base64.b64encode(io.open(f, 'rb').read()).decode()


LOGO_U = b64(LOGO)
P = {k: fit(os.path.join(IMG, k + '.png')) for k in
     ['p1_i1_x19', 'p1_i2_x20', 'p3_i2_x35']}
QR_WX, QR_XHS, QR_SPH = qr_img('公众号.png'), qr_img('小红书.png'), qr_img('视频号.png')
QR_TRIAL = b64(os.path.expanduser('~/work/公司资料/试用装申请.png'))
BG = b64(os.path.join(BG_DIR, '海报底图_竖版拼接.png'))

ICON = {
    'bolt': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M13 2 3 14h7l-1 8 10-12h-7l1-8z"/></svg>',
    'tube': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 3h6v4l3 4v9a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1v-9l3-4V3z"/><path d="M6 14h12"/></svg>',
    'sliders': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 6h16M4 12h16M4 18h16"/><circle cx="9" cy="6" r="2"/><circle cx="15" cy="12" r="2"/><circle cx="8" cy="18" r="2"/></svg>',
    'thermo': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 14V5a2 2 0 1 0-4 0v9a4 4 0 1 0 4 0z"/><path d="M12 9v6"/></svg>',
    'insert': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="6" cy="12" r="3"/><circle cx="18" cy="12" r="3"/><path d="M9 12h6"/></svg>',
    'sparkle': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3v4M12 17v4M3 12h4M17 12h4M6 6l3 3M15 15l3 3M18 6l-3 3M9 15l-3 3"/></svg>',
    'clock': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/></svg>',
}


def stats_block(items):
    cells = ''.join('<div class="st"><div class="sv">%s<small>%s</small></div><div class="sk">%s</div></div>'
                    % (v, u, k) for v, u, k in items)
    return '<div class="stats">%s</div>' % cells


def feats_block(items):
    rows = ''.join('<div class="fr"><div class="fi">%s</div><div><div class="ft"><em>%02d</em>%s</div>'
                   '<div class="fd">%s</div></div></div>' % (ICON[ic], i, t, d)
                   for i, (ic, t, d) in enumerate(items, 1))
    return '<div class="feats"><div class="fh"><i></i>产品特点</div>%s</div>' % rows


def fill(tpl, vals):
    """顺序替换 %s 占位符 —— 不用 % 格式化，避免 CSS 里的 width:100% 撞车"""
    for v in vals:
        tpl = tpl.replace('%s', v, 1)
    return tpl


def poster(kicker, h1, sub, imgs, pname, pen, promo_date, stats_html, feats_html, cta_title, cta_sub):
    stage = ''.join('<img src="%s">' % i for i in imgs)
    return fill('''<!DOCTYPE html><html><head><meta charset="utf-8"><style>
*{margin:0;padding:0;box-sizing:border-box}
body{width:1080px;font-family:"PingFang SC","Hiragino Sans GB","Microsoft YaHei",sans-serif;
     -webkit-font-smoothing:antialiased}
.poster{position:relative;width:1080px;overflow:hidden;background:#EEF3FA}
.bg{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;z-index:0}
.veil{position:absolute;inset:0;z-index:1;background:linear-gradient(180deg,
  rgba(255,255,255,.80) 0%,rgba(255,255,255,.62) 42%,rgba(255,255,255,.78) 100%)}
.inner{position:relative;z-index:2;padding:18px 30px 20px}
/* 顶栏：圆角磨砂胶囊 */
.topbar{height:72px;border-radius:20px;background:rgba(255,255,255,.84);
  backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px);
  display:flex;align-items:center;justify-content:space-between;padding:0 22px;
  box-shadow:0 10px 26px rgba(16,40,90,.10)}
.topbar img{height:40px;width:auto;display:block}
.ttag{font-size:30px;font-weight:800;color:#fff;letter-spacing:.5px;padding:8px 20px;border-radius:999px;
  background:linear-gradient(115deg,#1852B6,#2E7BE0);box-shadow:0 6px 16px rgba(24,82,182,.28)}
/* ① 标题横幅（品牌蓝，白字） */
.hero{margin-top:14px;border-radius:26px;padding:24px 28px;position:relative;overflow:hidden;
  background:linear-gradient(125deg,#0F3A8A 0%,#1852B6 52%,#2E7BE0 100%);
  box-shadow:0 18px 46px rgba(15,58,138,.28)}
.hero .wm{position:absolute;right:-90px;top:-120px;width:400px;height:400px;border-radius:50%;
  background:radial-gradient(circle at 40% 40%,rgba(255,255,255,.22),rgba(255,255,255,0) 62%)}
.kicker{font-size:48px;font-weight:800;letter-spacing:1px;margin-bottom:6px;color:rgba(255,255,255,.92);position:relative;z-index:2}
h1{font-size:84px;line-height:1.14;font-weight:900;color:#fff;letter-spacing:-2px;position:relative;z-index:2;
  -webkit-text-stroke:5px #0A2A66;paint-order:stroke fill;
  text-shadow:6px 6px 0 rgba(9,26,58,.42)}
.sub{font-size:48px;font-weight:700;color:#fff;line-height:1.32;margin-top:10px;position:relative;z-index:2}
/* ③ 限时促销（暖橙红） */
.promo{margin-top:14px;border-radius:24px;padding:20px 26px;display:flex;align-items:center;gap:18px;
  background:linear-gradient(115deg,#F2691E,#E8443C 52%,#D6336C);
  box-shadow:0 16px 40px rgba(214,51,108,.30)}
.pi{flex:0 0 54px;height:54px;color:#fff;display:flex;align-items:center;justify-content:center}
.pi svg{width:54px;height:54px}
.pt{font-size:48px;font-weight:900;color:#fff;letter-spacing:2px}
.pd{font-size:84px;font-weight:900;color:#fff;line-height:1.1;letter-spacing:-1px;text-shadow:5px 5px 0 rgba(120,20,60,.32)}
.ps{font-size:30px;color:rgba(255,255,255,.94);margin-top:6px}
/* 产品图卡 */
.stage-card{margin-top:14px;border-radius:24px;padding:16px 18px 14px;background:rgba(255,255,255,.94);
  border:1px solid rgba(224,232,244,.9);box-shadow:0 8px 24px rgba(16,40,90,.06);
  backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px)}
.stage{border-radius:18px;padding:14px 12px;display:flex;align-items:center;justify-content:center;gap:22px;
  background:linear-gradient(150deg,rgba(238,244,255,.92),rgba(243,251,249,.88) 52%,rgba(255,244,238,.88))}
.stage img{display:block;max-height:190px;max-width:100%;object-fit:contain}
.pname{margin-top:10px;text-align:center;font-size:48px;font-weight:900;color:#0F2A5C}
.pen{font-size:30px;color:#7A8598;text-align:center;margin-top:4px}
/* 核心实测数字 */
.stats{margin-top:14px;display:flex;border-radius:26px;overflow:hidden;background:rgba(255,255,255,.94);
  border:1px solid rgba(224,232,244,.9);box-shadow:0 8px 24px rgba(16,40,90,.06);
  backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px)}
.st{flex:1;padding:15px 10px;text-align:center;border-left:1px solid rgba(224,232,244,.9)}
.st:first-child{border-left:none}
.sv{font-size:84px;font-weight:900;color:#1852B6;line-height:1.05;letter-spacing:-2px}
.sv small{font-size:48px;font-weight:800;margin-left:2px}
.sk{font-size:30px;color:#5A6475;margin-top:6px}
/* ② 产品特点（青卡） */
.feats{margin-top:14px;border-radius:24px;padding:4px 24px;
  background:linear-gradient(150deg,rgba(224,248,246,.97),rgba(244,253,252,.97));
  border:1px solid rgba(0,169,158,.24);box-shadow:0 10px 30px rgba(0,169,158,.12);
  backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px)}
.fh{display:inline-flex;align-items:center;gap:12px;margin:16px 0 4px;padding:8px 22px;border-radius:999px;
  font-size:48px;font-weight:900;color:#fff;background:linear-gradient(115deg,#00A99E,#0C8E85);
  box-shadow:0 8px 20px rgba(0,169,158,.28)}
.fh i{width:12px;height:12px;border-radius:3px;background:rgba(255,255,255,.92);display:block;flex:0 0 12px;transform:rotate(45deg)}
.fr{display:flex;gap:16px;align-items:flex-start;padding:14px 0;border-bottom:1px solid rgba(0,169,158,.16)}
.fr:last-child{border-bottom:none}
.fi{flex:0 0 60px;height:60px;border-radius:16px;background:#D9F4F1;color:#00A99E;display:flex;
  align-items:center;justify-content:center}
.fi svg{width:34px;height:34px}
.ft{font-size:48px;font-weight:900;color:#0B3B38;line-height:1.2}
.ft em{font-style:normal;color:#00A99E;margin-right:12px}
.fd{font-size:30px;color:#4A5568;line-height:1.5;margin-top:6px}
/* 扫码 CTA */
.cta{margin-top:14px;border-radius:24px;overflow:hidden;position:relative;padding:18px 26px;
  display:flex;align-items:center;gap:26px;box-shadow:0 14px 36px rgba(16,40,90,.18)}
.cta .cbg{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;z-index:0}
.cta .csc{position:absolute;inset:0;z-index:1;
  background:linear-gradient(90deg,rgba(9,26,58,.86) 0%,rgba(9,26,58,.70) 55%,rgba(9,26,58,.46) 100%)}
.cta .qi{position:relative;z-index:2;flex:0 0 auto;background:#fff;border-radius:14px;padding:8px}
.cta .qi img{display:block;width:176px;height:176px}
.cta .ct{position:relative;z-index:2;color:#fff}
.cta h3{font-size:48px;font-weight:900;line-height:1.2;color:#fff}
.cta p{font-size:30px;color:rgba(255,255,255,.9);margin-top:8px}
/* 落款 */
.foot{margin-top:14px;border-radius:24px;padding:18px 24px;background:rgba(255,255,255,.88);
  backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px);
  box-shadow:0 10px 30px rgba(16,40,90,.10);text-align:center}
.slogan{font-size:48px;font-weight:900;color:#0F3A8A;letter-spacing:1px}
.qrs{display:flex;justify-content:center;gap:18px;margin-top:12px}
.qi2{text-align:center}
.qi2 img{display:block;width:176px;height:176px;border-radius:12px;background:#fff;padding:5px}
.qi2 span{display:block;font-size:30px;color:#5A6475;margin-top:6px}
.fcon{font-size:30px;color:#7A8598;margin-top:10px;line-height:1.5}
</style></head><body><div class="poster">
<img class="bg" src="%s"><div class="veil"></div>
<div class="inner">
  <div class="topbar"><img src="%s"><div class="ttag">实验加速季 · 科研产品</div></div>

  <div class="hero"><div class="wm"></div>
    <div class="kicker">%s</div><h1>%s</h1><div class="sub">%s</div>
  </div>

  <div class="promo"><div class="pi">%s</div><div>
    <div class="pt">限时促销</div><div class="pd">%s</div>
    <div class="ps">活动期内下单，享受专属优惠，更可参加朋友圈点赞好礼活动，详询销售人员</div>
  </div></div>

  <div class="stage-card"><div class="stage">%s</div>
    <div class="pname">%s</div><div class="pen">%s</div></div>

  %s

  %s

  <div class="cta"><img class="cbg" src="%s"><div class="csc"></div>
    <div class="qi"><img src="%s"></div>
    <div class="ct"><h3>%s</h3><p>%s</p></div>
  </div>

  <div class="foot">
    <div class="slogan">%s</div>
    <div class="qrs">
      <div class="qi2"><img src="%s"><span>公众号</span></div>
      <div class="qi2"><img src="%s"><span>小红书</span></div>
      <div class="qi2"><img src="%s"><span>视频号</span></div>
    </div>
    <div class="fcon">%s ｜ 电话 0756-8699969 ｜ www.biori.com ｜ marketing@biori.com<br>珠海市香洲区水岸一路88号</div>
  </div>
</div></div></body></html>''', [
        BG, LOGO_U, kicker, h1, sub, ICON['clock'], promo_date, stage, pname, pen,
        stats_html, feats_html, BG, QR_TRIAL, cta_title, cta_sub, SLOGAN,
        QR_WX, QR_XHS, QR_SPH, BRAND])


# ═══════════ 内容配置（两个子活动）═══════════
RTQ = poster(
    'RNA 定量难题，交给 RTQ',
    '高效逆转搭配稳定 qPCR',
    '低丰度样本也能稳定出 Ct —— 5 min 逆转录 + 30 min 跑完 40 循环',
    [P['p1_i1_x19'], P['p1_i2_x20']],
    'BR1D303 ＋ BR1E102',
    'RT-qPCR 一站式解决方案 · 逆转录预混液 + 快速 qPCR 预混液',
    '10.12 – 10.18',
    stats_block([('5', ' min', '完成逆转录'), ('100.795', '%', '标准曲线扩增效率'), ('1.000', '', '线性度')]),
    feats_block([('bolt', '极速上手', '5 min 完成逆转录，快速程序 30 min 跑完 40 循环'),
                 ('tube', '一管到底', '单管完成 gDNA 去除与逆转录，告别两步操作与反复开盖'),
                 ('sliders', '平台通用', '通用型 ROX 免调浓度，标准程序与快速程序皆可定量')]),
    '扫码申请试用装', 'BR1D303 ｜ BR1E102 ｜ BR1C103 三款均开放试用')

CLONE = poster(
    '载体构建，不用再守冰盒',
    'VellixClon™ 室温 5 min 无缝克隆',
    '同源重组一步到位 —— 打破温度限制，克隆随时随地',
    [P['p3_i2_x35']],
    'BR1C103',
    'VellixClon™ Ambient Seamless Cloning Mix · 室温同源重组预混液',
    '10.19 – 10.25',
    stats_block([('25', '℃', '通用反应温度'), ('5', ' min', '室温反应时长'), ('未纯化', '', 'PCR 产物可直接上样')]),
    feats_block([('thermo', '室温 25℃ 即可反应', '无需冰盒守温，桌面配完直接上机，实验节奏不再被温度绑住'),
                 ('insert', '单片段一步到位', '同源重组无缝克隆，单片段载体构建适用，模板兼容性广'),
                 ('sparkle', '未纯化也能用', '对未经纯化回收的 PCR 产物兼容良好，省掉胶回收那一步')]),
    '扫码申请试用装', 'BR1C103 ｜ BR1D303 ｜ BR1E102 三款均开放试用')

POSTERS = [('海报_2026年10月RTQ主推_首图版.png', 'RTQ 首图版', RTQ),
           ('海报_2026年10月1C103主推_首图版.png', '1C103 首图版', CLONE)]


def check_fs(html, label):
    sizes = set(re.findall(r'font-size:\s*([\d.]+px)', html))
    bad = sizes - FS_ALLOWED
    flat = html.replace(' ', '')
    errs = []
    if bad:
        errs.append('字号越界 %s' % sorted(bad))
    if 'background-clip:text' in flat or '-webkit-text-fill-color:transparent' in flat:
        errs.append('渐变填充文字')
    if re.search(r'(?<!backdrop-)filter\s*:\s*blur', html):
        errs.append('CSS blur 滤镜')
    print('  %s 门禁 [%s]：字号 %s' % ('✅' if not errs else '❌ ' + '／'.join(errs), label,
                                     '／'.join(sorted(sizes, key=lambda s: -float(s[:-2])))))
    return not errs


os.makedirs(DIR, exist_ok=True)
ok = True
with sync_playwright() as pw:
    br = pw.chromium.launch(channel='chrome')
    pg = br.new_page(viewport={'width': W, 'height': 900}, device_scale_factor=1)
    for fn, label, html in POSTERS:
        pg.set_content(html, wait_until='load')
        pg.wait_for_timeout(500)
        h = pg.evaluate("document.querySelector('.poster').getBoundingClientRect().height")
        pg.set_viewport_size({'width': W, 'height': int(h) + 4})
        pg.wait_for_timeout(250)
        out = os.path.join(DIR, fn)
        pg.screenshot(path=out, clip={'x': 0, 'y': 0, 'width': W, 'height': int(h)})
        im = Image.open(out)
        print('  → %s  %dx%d（1:%.2f）' % (fn, im.width, im.height, im.height / float(im.width)))
        ok = check_fs(html, label) and ok
        # 底栏三码反解（从成品 PNG 裁）
        for i, sel in enumerate(pg.query_selector_all('.qrs .qi2 img')):
            b = sel.bounding_box()
            Image.open(out).crop((int(b['x']), int(b['y']),
                                  int(b['x'] + b['width']), int(b['y'] + b['height']))).save('/tmp/sq_%d.png' % i)
        print('    三码已裁出 /tmp/sq_0~2.png')
    br.close()
print('✅ 全部通过' if ok else '❌ 有失败项')
