Metadata-Version: 2.2
Name: pylitehtml
Version: 0.2.1
Summary: HTML+CSS to image renderer based on litehtml
Keywords: html,css,render,image,png,litehtml,screenshot,markdown
Author: tyql688
License: MIT License
         
         Copyright (c) 2025 tyql688
         
         Permission is hereby granted, free of charge, to any person obtaining a copy
         of this software and associated documentation files (the "Software"), to deal
         in the Software without restriction, including without limitation the rights
         to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
         copies of the Software, and to permit persons to whom the Software is
         furnished to do so, subject to the following conditions:
         
         The above copyright notice and this permission notice shall be included in all
         copies or substantial portions of the Software.
         
         THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
         IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
         FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
         AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
         LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
         OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
         SOFTWARE.
         
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Topic :: Multimedia :: Graphics
Classifier: Typing :: Typed
Project-URL: Homepage, https://github.com/tyql688/pylitehtml
Project-URL: Repository, https://github.com/tyql688/pylitehtml
Project-URL: Issues, https://github.com/tyql688/pylitehtml/issues
Requires-Python: >=3.10
Requires-Dist: jinja2>=3.0
Provides-Extra: dev
Requires-Dist: pytest; extra == "dev"
Requires-Dist: pillow; extra == "dev"
Requires-Dist: numpy; extra == "dev"
Requires-Dist: pytest-asyncio; extra == "dev"
Requires-Dist: ruff; extra == "dev"
Description-Content-Type: text/markdown

# pylitehtml

![CI](https://github.com/tyql688/pylitehtml/actions/workflows/test.yml/badge.svg)

HTML+CSS → PNG/JPEG 图像渲染器。轻量级，无需无头浏览器，线程安全。需要 Python ≥ 3.10。

## 效果展示

[`examples/`](https://github.com/tyql688/pylitehtml/blob/main/examples/) 下有若干**独立可运行**的示例，各渲染一张图（无浏览器、纯 pylitehtml，与 `tests/` 分开）。

| 示例 | 说明 | 渲染结果 |
| --- | --- | --- |
| [`showcase_html.py`](https://github.com/tyql688/pylitehtml/blob/main/examples/showcase_html.py) | HTML/CSS 能力参考：排版、颜色/渐变、表格、代码、列表、进度条 + **真实 HTTPS 图片** + `data:` URI 图标 | ![html](https://raw.githubusercontent.com/tyql688/pylitehtml/main/assets/showcase_html.png) |
| [`showcase_css.py`](https://github.com/tyql688/pylitehtml/blob/main/examples/showcase_css.py) | More CSS：conic/radial 渐变、边框样式、`position`、`::before`、`text-overflow:ellipsis`、`white-space:pre`、`vertical-align` | ![css](https://raw.githubusercontent.com/tyql688/pylitehtml/main/assets/showcase_css.png) |
| [`showcase_markdown.py`](https://github.com/tyql688/pylitehtml/blob/main/examples/showcase_markdown.py) | 内置**零依赖** Markdown 转换器的端到端输出 | ![markdown](https://raw.githubusercontent.com/tyql688/pylitehtml/main/assets/showcase_markdown.png) |
| [`showcase_jinja2.py`](https://github.com/tyql688/pylitehtml/blob/main/examples/showcase_jinja2.py) | Jinja2 数据驱动模板（循环 / 条件 / 过滤器 / **autoescape**，一张订单发票） | ![jinja2](https://raw.githubusercontent.com/tyql688/pylitehtml/main/assets/showcase_jinja2.png) |
| [`markdown_full_converter.py`](https://github.com/tyql688/pylitehtml/blob/main/examples/markdown_full_converter.py) | 用 markdown-it-py 接入**完整 GFM**（脚注 / 定义列表 / Pygments 语法高亮；需 `pip install markdown-it-py mdit-py-plugins pygments`） | ![gfm](https://raw.githubusercontent.com/tyql688/pylitehtml/main/assets/showcase_markdown_full.png) |
| [`render_markdown_doc.py`](https://github.com/tyql688/pylitehtml/blob/main/examples/render_markdown_doc.py) | 渲染一篇**真实复杂文档**（[`markdown.md`](https://github.com/tyql688/pylitehtml/blob/main/examples/markdown.md)：多表格 / 嵌套列表 / 多语言代码 / Mermaid 源码 / 数学 TeX / 行号引用代码块） | ![doc](https://raw.githubusercontent.com/tyql688/pylitehtml/main/assets/markdown_doc.png) |

> 中英文混排共享同一基线（不会出现中文“飞起来”的错位）；文本测量为子像素精度；
> 模板变量经 autoescape 安全转义；抓取失败的远程图片自动跳过，不影响整图渲染。

```bash
python examples/showcase_html.py      # 生成 assets/showcase_html.png（需联网加载真实图片）
python examples/showcase_css.py
python examples/showcase_markdown.py
python examples/showcase_jinja2.py
```

## 性能（对比 Playwright）

同一篇文档各渲染 30 次（macOS arm64，width=720；基准脚本
[`bench/bench_vs_playwright.py`](https://github.com/tyql688/pylitehtml/blob/main/bench/bench_vs_playwright.py)）：

| 指标 | pylitehtml | Playwright（无头 Chromium） |
| --- | --- | --- |
| 单次渲染（warm，均值） | **≈ 15.5 ms** | ≈ 21 ms |
| 首屏 / 冷启动 | ≈ 0.2 s | ≈ 0.25 s（OS 已缓存）/ **首次可达 ~3.4 s** |
| 额外依赖 | 无（仅系统 Cairo/Pango 等） | **需下载 Chromium ~190 MB+**，每次渲染有浏览器进程内存开销 |
| JavaScript | ❌ 不执行 | ✅ 可执行（KaTeX/Mermaid 等） |

要点：稳态吞吐 pylitehtml **约快 1.4×**；真正的差距在**部署体积与冷启动** ——
无需 ~190MB 的 Chromium、无浏览器进程，特别适合容器 / Serverless / 边缩缩容场景。
需要执行 JS（数学排版、Mermaid 绘图）时仍应选 Playwright。

```bash
pip install playwright && playwright install chromium   # 或: uv pip install --group bench
python bench/bench_vs_playwright.py 30
```

## 安装

```bash
pip install pylitehtml
```

## 快速上手

```python
import pylitehtml

# 最简单：HTML 片段 → PNG（自动套用一套干净的默认样式）
png = pylitehtml.html_to_png("<h1>Hello</h1><p>世界</p>")
open("out.png", "wb").write(png)

# Markdown → PNG（零依赖内置转换器）
from pylitehtml.markdown import markdown_to_png
png = markdown_to_png("# 标题\n\n- 列表\n- [x] 任务")

# 需要精细控制时用底层接口：render / Renderer（不注入默认样式）
png = pylitehtml.render("<h1>Hello</h1>", width=800)
r = pylitehtml.Renderer(width=800)                       # 复用，字体只加载一次
jpg = r.render("<h1>Hello</h1>", fmt="jpeg", quality=90)

# 本地 HTML 文件（自动解析相对路径的 CSS/图片）+ Jinja2 模板
png = r.render_file("project/index.html")
png = r.render_file("project/order.html", title="订单", items=[...])

# 异步（不阻塞事件循环）
import asyncio
png = await asyncio.to_thread(r.render, "<h1>Hello</h1>")
```

---

## 高级封装：html_to_png / markdown_to_png

`render()` 是最底层接口（HTML 字符串 → 图片）。在它之上提供了两个高层便捷函数。

### `html_to_png` — 带默认样式的 HTML 渲染（核心、零额外依赖）

```python
from pylitehtml import html_to_png, html_to_image, wrap_html, DEFAULT_CSS

# 传入“片段”时，自动套用一套干净的 GitHub 风默认样式
png = html_to_png("<h1>标题</h1><p>正文 <code>code</code></p>", width=720)

# 传入完整文档（含 <html>/<body>）时，按原样渲染，不再注入默认样式
png = html_to_png("<html><body style='background:#000'>…</body></html>")

# HiDPI：scale 同时放大画布宽度与（默认样式的）根字号，em 布局等比放大
png = html_to_png(fragment, width=720, scale=2)

# 追加/覆盖样式；或拿原始 RGBA 像素
png = html_to_png(fragment, extra_css="body{background:#0d1117;color:#e6edf3}")
raw = html_to_image(fragment, fmt="raw")          # → RawResult(.data/.width/.height)
doc = wrap_html("<p>x</p>", css=DEFAULT_CSS)       # 仅生成完整 HTML 文档字符串
```

主要参数：`width`、`scale`（HiDPI 倍数，默认 1）、`wrap`（`None` 自动判断片段/整文档；`True/False` 强制）、
`css` / `extra_css`、`fmt`（`"png"`/`"jpeg"`）、`quality`、`height`、`shrink_to_fit`、`locale`、`fonts`、`images`。

### `markdown_to_png` — Markdown → HTML → 图片（可选、默认零依赖）

“用 HTML 渲染 Markdown”：内置一个**纯标准库**的轻量 Markdown→HTML 转换器，再交给 `html_to_png`。
**不引入** `markdown-it-py`、`pygments` 等任何第三方依赖。

```python
from pylitehtml.markdown import markdown_to_png, markdown_to_html

png = markdown_to_png("# 标题\n\n- 列表\n- [x] 任务\n\n> 引用", width=720)  # scale 默认 2
html = markdown_to_html("**bold**")   # → '<p><strong>bold</strong></p>'
```

内置转换器覆盖：标题、粗/斜/`***粗斜***`/删除线/行内代码、反斜杠转义、链接/图片/裸 URL、
行内 raw HTML、硬换行、围栏代码块（不高亮）、引用、有序（含 `start`）/无序/嵌套/任务列表、
带对齐的 GFM 表格、分隔线。**不支持**：setext 标题、引用式链接、脚注、定义列表、缩进代码块。

需要完整 CommonMark/GFM（脚注、定义列表、语法高亮）时传入 `converter=` 接入 markdown-it-py，
见 [`examples/markdown_full_converter.py`](https://github.com/tyql688/pylitehtml/blob/main/examples/markdown_full_converter.py)：

```python
from markdown_it import MarkdownIt
from mdit_py_plugins.footnote import footnote_plugin
from mdit_py_plugins.deflist import deflist_plugin
from pylitehtml.markdown import markdown_to_png

md = MarkdownIt("gfm-like").use(footnote_plugin).use(deflist_plugin)
png = markdown_to_png(text, converter=md.render)
```

> **不执行 JavaScript**：数学公式（KaTeX）与 Mermaid 无法排版/绘制，需调用前自行预渲染或接入完整引擎。

---

## 支持 / 不支持

litehtml = CSS 2.1 + 部分 CSS3。下表为**实测**结论：

| 能力 | 状态 |
| --- | --- |
| PNG / JPEG / RAW 输出 | ✅ |
| 盒模型、边框（solid/dashed/dotted/double）、`border-radius`、背景色 | ✅ |
| `linear-gradient` / `conic-gradient`（`radial-gradient` ⚠️ 仅颜色对） | ✅ |
| 字体、`color`、`text-align`、`line-height`、`text-shadow`、`text-transform` | ✅ |
| `text-decoration`、`<mark>`/`<sub>`/`<sup>`/`<kbd>`、`white-space:pre/nowrap` | ✅ |
| 列表（嵌套 / `list-style-type` / 任务列表）、定义列表、表格（对齐 / `:nth-child`） | ✅ |
| `float`、`inline-block`、`position:relative/absolute`、`text-overflow:ellipsis`、`::before/::after` | ✅ |
| 图片 `<img>`：`data:`(PNG/JPEG/WebP/SVG) / 本地 / HTTP·HTTPS、`@import` CSS | ✅ |
| 中文 / 多语言（内置 Noto Sans + SC，中英共享基线、子像素测量） | ✅ |
| 多线程并发、`asyncio.to_thread` | ✅ |
| Flexbox（简单行 / 列） | ⚠️ 部分 |
| `background-image:url()`、`box-shadow`、`transform`、`opacity`、`letter-spacing` | ❌ |
| CSS Grid、`var()`、`@font-face`、动画 / 过渡、`filter`、JavaScript | ❌ |

> 图片抓取失败会自动跳过（不报错）；官方 wheel（Linux manylinux_2_34 / macOS / Windows）均含 OpenSSL ≥ 3.0，支持 https。

---

## API

```python
# 高层（推荐）：自动默认样式、片段/整文档自动判别、HiDPI scale
html_to_png(html, *, width=720, scale=1, wrap=None, css=DEFAULT_CSS, extra_css="",
            fmt="png", quality=85, height=0, shrink_to_fit=True,
            locale="en-US", fonts=None, images=None) -> bytes
html_to_image(...)   # 同参数，fmt 可取 "raw" → RawResult(.data/.width/.height，RGBA 行主序)

# 底层：不注入默认样式
Renderer(width, *, locale="en-US", dpi=96.0, device_height=600, fonts=None, images=None)
Renderer.render(html, *, fmt="png", quality=85, height=0, shrink_to_fit=True)
Renderer.render_file(path, *, fmt="png", quality=85, height=0, shrink_to_fit=True, **template_data)
render(html, width, ...) / render_file(path, width, ...)   # 一次性便捷函数

FontConfig(default="Noto Sans", size=16, extra=[])          # 已内置 Noto Sans/SC + DejaVu Sans
ImageConfig(cache_mb=64.0, timeout_ms=5000, max_mb=10.0, allow_http=True)
```

- `fmt="raw"` 返回 RGBA：`Image.frombytes("RGBA",(raw.width,raw.height),raw.data)` 或 `np.frombuffer(...)`。
- `scale` 等比放大画布与（默认样式的）根字号；`shrink_to_fit` 把画布收窄到内容宽度。
- 内置字体使 `sans-serif`/`serif`/`monospace` 与中文在无系统字体时也能渲染；`dpi` 影响 `pt` 换算。

---

## render_file 与 Jinja2

按标准网页组织目录，自动解析相对路径的 CSS/图片；传入关键字参数即作为
[Jinja2](https://jinja.palletsprojects.com/) 模板渲染（变量 / 循环 / 条件 / 过滤器 / `include` / `extends`）：

```python
png = r.render_file("project/index.html")                                       # 纯文件
png = r.render_file("order.html", title="订单", items=[{"name": "苹果", "price": 5}])   # 模板
```

> **autoescape 默认开启**：`.html` 模板里的 `{{ 变量 }}` 会转义 `<`/`&`/`"`，不可信数据也不会注入。
> **本地文件安全**：`file://` 与根路径资源仅在 `render_file()` 下加载；直接 `render()` 字符串时忽略。

---

## 线程 / 异步

先在单线程构造 `Renderer`，之后可多线程并发 `render()`（渲染期间释放 GIL）；
异步用 `await asyncio.to_thread(r.render, html)`。带 `extra_fonts` 的构造会改写进程级字体配置，
勿在渲染进行中于其它线程构造新 `Renderer`。

---

## 从源码构建

**Ubuntu / Debian**

```bash
sudo apt-get install -y libcairo2-dev libpango1.0-dev libfontconfig1-dev \
  libwebp-dev libjpeg-turbo8-dev libssl-dev cmake ninja-build pkg-config
pip install -e ".[dev]" --no-build-isolation
```

**macOS**

```bash
brew install cairo pango fontconfig webp jpeg-turbo openssl@3 cmake ninja
CC=/usr/bin/clang CXX=/usr/bin/clang++ pip install -e ".[dev]" --no-build-isolation
```

**运行测试 / 代码检查**

```bash
pytest tests/ -v
ruff check .        # lint
ruff format .       # 格式化（CI 用 `ruff format --check` 校验）
```

---

## 基于

[litehtml](https://github.com/litehtml/litehtml) · [Cairo](https://www.cairographics.org/) · [Pango](https://pango.gnome.org/) · [FontConfig](https://www.freedesktop.org/wiki/Software/fontconfig/) · [pybind11](https://github.com/pybind/pybind11)

MIT License
