@liustack/pagepress 1.0.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,14 +1,16 @@
1
1
  # PagePress
2
2
 
3
- A CLI toolkit for AI agents to render local HTML files into high-quality PDF documents.
3
+ Generate one-page visual reports, dashboards, and infographics from HTML powered by Playwright + Chromium.
4
4
 
5
5
  中文说明请见:[README.zh-CN.md](README.zh-CN.md)
6
6
 
7
7
  ## Features
8
8
 
9
- - PDF-only command surface (`pagepress`)
9
+ - HTML PNG screenshot pipeline
10
+ - Custom viewport dimensions (`--width`, `--height`)
11
+ - High-DPI output via `--scale` (up to 4x)
10
12
  - Local HTML file input only (`.html`)
11
- - Deterministic output via Playwright + Chromium print engine
13
+ - Deterministic output via Playwright + Chromium
12
14
 
13
15
  ## Installation
14
16
 
@@ -26,14 +28,35 @@ npx @liustack/pagepress [options]
26
28
  ## Usage
27
29
 
28
30
  ```bash
29
- # Local HTML to PDF
30
- pagepress -i page.html -o output.pdf
31
+ # Generate infographic (default 1200×630 @2x)
32
+ pagepress -i report.html -o report.png
33
+
34
+ # Custom dimensions for a tall infographic
35
+ pagepress -i report.html -o report.png -w 1080 -h 1920
36
+
37
+ # High-DPI poster
38
+ pagepress -i poster.html -o poster.png -w 1200 -h 1500 --scale 3
39
+ ```
40
+
41
+ ## HTML Requirements
42
+
43
+ Your HTML must include a `<div id="container">` — PagePress clips the screenshot to this element's bounding box.
44
+
45
+ ```html
46
+ <body>
47
+ <div id="container">
48
+ <!-- your content here -->
49
+ </div>
50
+ </body>
31
51
  ```
32
52
 
33
53
  ## Options
34
54
 
35
55
  - `-i, --input <path>` input HTML file path
36
- - `-o, --output <path>` output PDF file path
56
+ - `-o, --output <path>` output PNG file path
57
+ - `-w, --width <pixels>` viewport width (default: 1200)
58
+ - `-h, --height <pixels>` viewport height (default: 630)
59
+ - `--scale <number>` device scale factor, 1-4 (default: 2)
37
60
  - `--wait-until <state>` `load | domcontentloaded | networkidle`
38
61
  - `--timeout <ms>` timeout in milliseconds
39
62
  - `--safe` disable external network requests and JavaScript execution
package/README.zh-CN.md CHANGED
@@ -1,12 +1,14 @@
1
1
  # PagePress
2
2
 
3
- 面向 AI Agent PDF 渲染 CLI,可将本地 HTML 文件输出为高质量 PDF
3
+ HTML 生成单页可视化报告、仪表盘和信息图 基于 Playwright + Chromium
4
4
 
5
5
  ## 特性
6
6
 
7
- - 专注 PDF 输出(仅 `pagepress`)
7
+ - HTML PNG 截图管线
8
+ - 自定义视口尺寸(`--width`、`--height`)
9
+ - 高 DPI 输出,支持最高 4 倍缩放(`--scale`)
8
10
  - 仅支持本地 HTML 文件输入(`.html`)
9
- - 基于 Playwright + Chromium 的稳定打印引擎
11
+ - 基于 Playwright + Chromium 的稳定渲染引擎
10
12
 
11
13
  ## 安装
12
14
 
@@ -24,14 +26,35 @@ npx @liustack/pagepress [options]
24
26
  ## 用法
25
27
 
26
28
  ```bash
27
- # 本地 HTML 转 PDF
28
- pagepress -i page.html -o output.pdf
29
+ # 生成信息图(默认 1200×630 @2x)
30
+ pagepress -i report.html -o report.png
31
+
32
+ # 自定义尺寸的长信息图
33
+ pagepress -i report.html -o report.png -w 1080 -h 1920
34
+
35
+ # 高 DPI 海报
36
+ pagepress -i poster.html -o poster.png -w 1200 -h 1500 --scale 3
37
+ ```
38
+
39
+ ## HTML 要求
40
+
41
+ HTML 中必须包含 `<div id="container">` — PagePress 会根据该元素的边界框裁剪截图。
42
+
43
+ ```html
44
+ <body>
45
+ <div id="container">
46
+ <!-- 你的内容 -->
47
+ </div>
48
+ </body>
29
49
  ```
30
50
 
31
51
  ## 参数
32
52
 
33
53
  - `-i, --input <path>` 输入 HTML 路径
34
- - `-o, --output <path>` 输出 PDF 路径
54
+ - `-o, --output <path>` 输出 PNG 路径
55
+ - `-w, --width <pixels>` 视口宽度(默认: 1200)
56
+ - `-h, --height <pixels>` 视口高度(默认: 630)
57
+ - `--scale <number>` 设备缩放因子,1-4(默认: 2)
35
58
  - `--wait-until <state>` `load | domcontentloaded | networkidle`
36
59
  - `--timeout <ms>` 超时时间(毫秒)
37
60
  - `--safe` 禁用外部网络请求和 JavaScript 执行
package/dist/main.js CHANGED
@@ -3,10 +3,12 @@ import { Command } from "commander";
3
3
  import { chromium } from "playwright";
4
4
  import * as fs from "fs";
5
5
  import * as path from "path";
6
- import { fileURLToPath, pathToFileURL } from "url";
7
- const __filename$1 = fileURLToPath(import.meta.url);
8
- path.dirname(__filename$1);
6
+ import { pathToFileURL } from "url";
9
7
  const WAIT_UNTIL_STATES = /* @__PURE__ */ new Set(["load", "domcontentloaded", "networkidle"]);
8
+ const MIN_SCALE = 1;
9
+ const MAX_SCALE = 4;
10
+ const DEFAULT_WIDTH = 1200;
11
+ const DEFAULT_HEIGHT = 630;
10
12
  function normalizeWaitUntil(value) {
11
13
  if (!value) return "networkidle";
12
14
  if (WAIT_UNTIL_STATES.has(value)) {
@@ -21,10 +23,24 @@ function normalizeTimeout(value) {
21
23
  }
22
24
  return value;
23
25
  }
26
+ function validateRange(name, value, min, max) {
27
+ if (!Number.isFinite(value)) {
28
+ throw new Error(`${name} must be a finite number.`);
29
+ }
30
+ if (value < min || value > max) {
31
+ throw new Error(`${name} must be between ${min} and ${max}. Received: ${value}.`);
32
+ }
33
+ }
24
34
  async function render(options) {
35
+ const width = options.width ?? DEFAULT_WIDTH;
36
+ const height = options.height ?? DEFAULT_HEIGHT;
37
+ const scale = options.scale ?? 2;
25
38
  const safeMode = options.safe ?? false;
26
39
  const waitUntil = normalizeWaitUntil(options.waitUntil);
27
40
  const timeout = normalizeTimeout(options.timeout);
41
+ validateRange("Width", width, 1, 1e4);
42
+ validateRange("Height", height, 1, 1e4);
43
+ validateRange("Scale", scale, MIN_SCALE, MAX_SCALE);
28
44
  if (options.input.startsWith("http://") || options.input.startsWith("https://")) {
29
45
  throw new Error("Remote URL inputs are not supported. Please provide a local HTML file path.");
30
46
  }
@@ -36,7 +52,11 @@ async function render(options) {
36
52
  let browser = null;
37
53
  try {
38
54
  browser = await chromium.launch({ headless: true });
39
- const context = await browser.newContext({ javaScriptEnabled: !safeMode });
55
+ const context = await browser.newContext({
56
+ viewport: { width, height },
57
+ deviceScaleFactor: scale,
58
+ javaScriptEnabled: !safeMode
59
+ });
40
60
  if (safeMode) {
41
61
  await context.route("**/*", (route) => {
42
62
  const url = route.request().url();
@@ -49,17 +69,38 @@ async function render(options) {
49
69
  const page = await context.newPage();
50
70
  await page.goto(pathToFileURL(inputPath).href, { waitUntil: "domcontentloaded", timeout });
51
71
  await page.setContent(htmlContent, { waitUntil, timeout });
52
- await page.waitForTimeout(200);
72
+ await page.addStyleTag({
73
+ content: `
74
+ html, body {
75
+ width: ${width}px;
76
+ height: ${height}px;
77
+ margin: 0;
78
+ padding: 0;
79
+ overflow: hidden;
80
+ }
81
+ `
82
+ });
83
+ await page.waitForTimeout(500);
84
+ const cardContainer = await page.$("#container");
85
+ if (!cardContainer) {
86
+ throw new Error('Missing #container element. PNG output requires a <div id="container"> wrapper.');
87
+ }
88
+ const box = await cardContainer.boundingBox();
89
+ if (!box || box.width <= 0 || box.height <= 0) {
90
+ throw new Error("Invalid #container size. Ensure it has a positive width and height.");
91
+ }
53
92
  const outputPath = path.resolve(options.output);
54
- await page.pdf({
93
+ await page.screenshot({
55
94
  path: outputPath,
56
- format: "A4",
57
- margin: { top: "0", right: "0", bottom: "0", left: "0" },
58
- printBackground: true
95
+ type: "png",
96
+ clip: { x: box.x, y: box.y, width: box.width, height: box.height }
59
97
  });
60
98
  return {
61
- pdfPath: outputPath,
99
+ pngPath: outputPath,
62
100
  meta: {
101
+ width,
102
+ height,
103
+ deviceScaleFactor: scale,
63
104
  generatedAt: (/* @__PURE__ */ new Date()).toISOString()
64
105
  }
65
106
  };
@@ -70,7 +111,7 @@ async function render(options) {
70
111
  }
71
112
  }
72
113
  const program = new Command();
73
- program.name("pagepress").description("Render local HTML files to PDF").version("1.0.0").requiredOption("-i, --input <path>", "Input HTML file path").requiredOption("-o, --output <path>", "Output PDF file path").option("--wait-until <state>", "Navigation waitUntil (load, domcontentloaded, networkidle)", "networkidle").option("--timeout <ms>", "Navigation timeout in milliseconds", "30000").option("--safe", "Disable external network requests and JavaScript execution").action(async (options) => {
114
+ program.name("pagepress").description("Generate one-page visual reports, dashboards, and infographics from HTML").version("2.0.0").requiredOption("-i, --input <path>", "Input HTML file path").requiredOption("-o, --output <path>", "Output PNG file path").option("-w, --width <pixels>", "Viewport width in pixels", "1200").option("-h, --height <pixels>", "Viewport height in pixels", "630").option("--scale <number>", "Device scale factor (1-4)", "2").option("--wait-until <state>", "Navigation waitUntil (load, domcontentloaded, networkidle)", "networkidle").option("--timeout <ms>", "Navigation timeout in milliseconds", "30000").option("--safe", "Disable external network requests and JavaScript execution").action(async (options) => {
74
115
  try {
75
116
  const timeout = Number.parseInt(options.timeout, 10);
76
117
  if (!Number.isFinite(timeout) || timeout < 0) {
@@ -79,6 +120,9 @@ program.name("pagepress").description("Render local HTML files to PDF").version(
79
120
  const result = await render({
80
121
  input: options.input,
81
122
  output: options.output,
123
+ width: Number.parseInt(options.width, 10),
124
+ height: Number.parseInt(options.height, 10),
125
+ scale: Number.parseFloat(options.scale),
82
126
  waitUntil: options.waitUntil,
83
127
  timeout,
84
128
  safe: options.safe
package/package.json CHANGED
@@ -1,18 +1,30 @@
1
1
  {
2
2
  "name": "@liustack/pagepress",
3
- "version": "1.0.0",
4
- "description": "CLI tool to render local HTML files into PDF",
3
+ "version": "2.0.0",
4
+ "description": "CLI tool to generate one-page visual reports, dashboards, and infographics from HTML",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "pagepress": "./dist/main.js"
8
8
  },
9
+ "scripts": {
10
+ "dev": "vite build --watch",
11
+ "build": "vite build",
12
+ "typecheck": "tsc --noEmit",
13
+ "test": "vitest run",
14
+ "test:watch": "vitest",
15
+ "test:coverage": "vitest run --coverage",
16
+ "prepublishOnly": "pnpm build",
17
+ "docs:list": "node scripts/docs-list.js"
18
+ },
9
19
  "files": [
10
20
  "dist"
11
21
  ],
12
22
  "keywords": [
13
- "pdf",
14
- "html-to-pdf",
15
- "pdf-report",
23
+ "infographic",
24
+ "html-to-png",
25
+ "visual-report",
26
+ "dashboard",
27
+ "screenshot",
16
28
  "cli",
17
29
  "playwright"
18
30
  ],
@@ -40,14 +52,5 @@
40
52
  "vite": "^6.4.1",
41
53
  "vite-plugin-node": "^5.0.1",
42
54
  "vitest": "^4.0.18"
43
- },
44
- "scripts": {
45
- "dev": "vite build --watch",
46
- "build": "vite build",
47
- "typecheck": "tsc --noEmit",
48
- "test": "vitest run",
49
- "test:watch": "vitest",
50
- "test:coverage": "vitest run --coverage",
51
- "docs:list": "node scripts/docs-list.js"
52
55
  }
53
- }
56
+ }