feat: E1002 新增公网图片服务(恒定 URL 现渲染)+底部改为 BTC 时K
- server.py:三条恒定 URL 并行——/e1002.png 内存现渲染 PNG(主力)、 /e1002.html 现代网页、/e1002-web.png 网页经无头 Chrome 截图后固化六色; 天气/橘喵/BTC 按 WEATHER_TTL/MAO_TTL/BTC_TTL 走内存缓存,过期才实拉、 失败沿用旧值;用量不读本机文件,改由 POST /push/usage(_gpt) 推送暂存 - push_client.py:本机把 Claude 用量快照推送到服务(X-Push-Token 鉴权) - btc_api.py:OKX/Coinbase/Huobi 免费接口逐源兜底,时K 近 72 小时,涨绿跌红 - data.py 拆出 weather_fetch/mao_fetch/btc_fetch 与 usage_from_raw, 统一按 TZ_NAME 时区;宽版只用点阵字体整数倍字号 12/24/36/48 - probe_server.py:验证平台对同一 URL 是否周期重抓(结论:Image 控件 不重抓,HTML 控件重抓) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
97
html_shot.py
Normal file
97
html_shot.py
Normal file
@@ -0,0 +1,97 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
方案三:把 html_renderer 生成的网页用**服务端无头 Chrome** 截成 800x480,再**固化为六色 PNG**(最近色、不抖动)。
|
||||
|
||||
为什么需要它:SenseCraft 抓 HTML 时是平台自己截图再量化,文字抗锯齿的灰边会被它抖动成噪点、细笔画被打断(真机实拍已确认)。
|
||||
在我们这一侧先截图、先量化,交给平台的就是纯六色图,平台无从抖动——效果对齐 PNG 方案,同时保留现代字体与网页排版。
|
||||
|
||||
量化策略(不抖动):
|
||||
1. 每通道二值化:v < HTML_PNG_THRESHOLD → 0,否则 → 255。阈值默认 160(偏向保墨迹):抗锯齿的中灰边缘归黑,
|
||||
细笔画不会因为「不够黑」而消失;纯红/绿/黄/蓝/黑/白本身各通道就是 0/255,不受影响。
|
||||
2. 二值化后像素落在 {0,255}^3 的 8 个角,其中青/品/其它非六色角极少见(只可能出现在彩色抗锯齿边缘),
|
||||
再用 Pillow 的调色板量化(dither=NONE)映射到最近的六色。
|
||||
→ 输出保证只含 SPECTRA6 六色。
|
||||
|
||||
无头 Chrome 只能把截图写到文件,这里用临时文件并立刻删除(Linux 下 /tmp 通常是 tmpfs,不落磁盘);
|
||||
若想彻底零文件需改用 Playwright/CDP,本项目不引第三方依赖,先不做。
|
||||
"""
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
import threading
|
||||
from PIL import Image
|
||||
|
||||
import config
|
||||
from renderer_e1002 import SPECTRA6
|
||||
|
||||
_CANDIDATES = (
|
||||
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
||||
"/Applications/Chromium.app/Contents/MacOS/Chromium",
|
||||
"google-chrome", "google-chrome-stable", "chromium", "chromium-browser", "chrome",
|
||||
)
|
||||
|
||||
|
||||
def find_chrome():
|
||||
if config.CHROME_BIN:
|
||||
return config.CHROME_BIN
|
||||
for c in _CANDIDATES:
|
||||
if os.path.isabs(c):
|
||||
if os.path.exists(c):
|
||||
return c
|
||||
elif shutil.which(c):
|
||||
return shutil.which(c)
|
||||
raise RuntimeError("未找到 Chrome/Chromium,请配置 CHROME_BIN")
|
||||
|
||||
|
||||
_SHOT_LOCK = threading.Lock() # 无头 Chrome 共用默认 profile,截图串行化避免互相干扰
|
||||
|
||||
|
||||
def screenshot_html(html: str, size=(800, 480), timeout=40) -> Image.Image:
|
||||
"""HTML 字符串 → 无头 Chrome 截图(RGB Image)。临时文件用后即删。
|
||||
注意不要传 --user-data-dir 指向全新目录:新版 --headless=new 对空 profile 的首启流程会卡死到超时(实测)。"""
|
||||
chrome = find_chrome()
|
||||
tmpdir = tempfile.mkdtemp(prefix="eink-shot-")
|
||||
html_path = os.path.join(tmpdir, "page.html")
|
||||
png_path = os.path.join(tmpdir, "shot.png")
|
||||
try:
|
||||
with open(html_path, "w", encoding="utf-8") as f:
|
||||
f.write(html)
|
||||
cmd = [chrome, "--headless=new", "--disable-gpu", "--hide-scrollbars", "--force-device-scale-factor=1",
|
||||
f"--window-size={size[0]},{size[1]}", "--virtual-time-budget=6000", f"--screenshot={png_path}"]
|
||||
if config.CHROME_EXTRA_ARGS:
|
||||
cmd += config.CHROME_EXTRA_ARGS.split()
|
||||
cmd.append("file://" + html_path)
|
||||
with _SHOT_LOCK:
|
||||
subprocess.run(cmd, check=True, timeout=timeout, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
with Image.open(png_path) as im:
|
||||
return im.convert("RGB").copy()
|
||||
finally:
|
||||
shutil.rmtree(tmpdir, ignore_errors=True)
|
||||
|
||||
|
||||
def quantize6(img: Image.Image, threshold=None) -> Image.Image:
|
||||
"""RGB → 六色纯色(最近色、不抖动)。见模块文档。"""
|
||||
t = config.HTML_PNG_THRESHOLD if threshold is None else threshold
|
||||
lut = [0 if v < t else 255 for v in range(256)] * 3
|
||||
bin_img = img.convert("RGB").point(lut)
|
||||
pal = Image.new("P", (1, 1))
|
||||
flat = [c for rgb in SPECTRA6 for c in rgb]
|
||||
pal.putpalette(flat + [0] * (768 - len(flat)))
|
||||
q = bin_img.quantize(palette=pal, dither=Image.Dither.NONE)
|
||||
return q.convert("RGB")
|
||||
|
||||
|
||||
def html_to_png6(html: str) -> Image.Image:
|
||||
return quantize6(screenshot_html(html))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys, data, html_renderer
|
||||
d = data.get_dashboard_data()
|
||||
im = html_to_png6(html_renderer.render_html(d))
|
||||
out = sys.argv[1] if len(sys.argv) > 1 else "output/e1002/web6.png"
|
||||
os.makedirs(os.path.dirname(out), exist_ok=True)
|
||||
im.save(out)
|
||||
print(out, im.size, sorted(c for _, c in im.getcolors(64)))
|
||||
Reference in New Issue
Block a user