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