Files
eink-push/html_shot.py
YANG JIANKUAN 85b5113ba4 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>
2026-09-10 18:55:47 +08:00

98 lines
4.3 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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)))