design: 多线站点改竖向胶囊(端头半圆,沿线束方向);构建改本地渲染器+TMPDIR 重定向规避 /tmp 配额崩溃
This commit is contained in:
1 parent
8eca17586e
commit
ed1432cecd
4 files changed
+309
-8
No files matched your search
@@ -8,6 +8,13 @@ cd "$(dirname "$0")"
|
|||||||
PDF_SKILL_DIR="${PDF_SKILL_DIR:-$HOME/.zcode/cli/plugins/cache/zcode-plugins-official/pdf/0.1.7/skills/pdf}"
|
PDF_SKILL_DIR="${PDF_SKILL_DIR:-$HOME/.zcode/cli/plugins/cache/zcode-plugins-official/pdf/0.1.7/skills/pdf}"
|
||||||
export PDF_SKILL_DIR
|
export PDF_SKILL_DIR
|
||||||
|
|
||||||
|
# 本地渲染器(固定 headless-shell 规避系统 Chrome 占用;TMPDIR 重定向规避 /tmp 磁盘配额)
|
||||||
|
HTML2POSTER="$PWD/tools/html2poster.js"
|
||||||
|
H2P_CHROMIUM="${H2P_CHROMIUM:-$HOME/.cache/ms-playwright/chromium_headless_shell-1234/chrome-headless-shell-linux64/chrome-headless-shell}"
|
||||||
|
export HTML2POSTER H2P_CHROMIUM
|
||||||
|
mkdir -p "$HOME/.cache/h2p-tmp"
|
||||||
|
export TMPDIR="$HOME/.cache/h2p-tmp"
|
||||||
|
|
||||||
STEMS=("$@")
|
STEMS=("$@")
|
||||||
if [ ${#STEMS[@]} -eq 0 ]; then
|
if [ ${#STEMS[@]} -eq 0 ]; then
|
||||||
STEMS=(course-shelf components)
|
STEMS=(course-shelf components)
|
||||||
@@ -45,7 +52,7 @@ out_dir = sys.argv[1]
|
|||||||
pages = sorted(glob.glob(f"{out_dir}/page-*.html"), key=lambda p: int(re.search(r"(\d+)", os.path.basename(p)).group(1)))
|
pages = sorted(glob.glob(f"{out_dir}/page-*.html"), key=lambda p: int(re.search(r"(\d+)", os.path.basename(p)).group(1)))
|
||||||
for p in pages:
|
for p in pages:
|
||||||
out = p.replace(".html", ".pdf")
|
out = p.replace(".html", ".pdf")
|
||||||
subprocess.run(["node", f"{os.environ['PDF_SKILL_DIR']}/scripts/html2poster.js", p, "--output", out, "--width", "1280px"], check=True, capture_output=True)
|
subprocess.run(["node", os.environ['HTML2POSTER'], p, "--output", out, "--width", "1280px"], check=True, capture_output=True)
|
||||||
print(f"[{out_dir}] rendered {len(pages)} pages")
|
print(f"[{out_dir}] rendered {len(pages)} pages")
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
|
|||||||
@@ -661,7 +661,7 @@
|
|||||||
<div class="story-head">
|
<div class="story-head">
|
||||||
<span class="tag"><CourseGraph /></span>
|
<span class="tag"><CourseGraph /></span>
|
||||||
<h2>课程路线图 · 线条是构件</h2>
|
<h2>课程路线图 · 线条是构件</h2>
|
||||||
<div class="desc">线条构件:直段 / 转角 / 步进 / 分流 / 并线 / 并行 / 站点,横平竖直。并行线逐条不合并;单线圆点、多线扁点。CourseCard 由路线串联成网。</div>
|
<div class="desc">线条构件:直段 / 转角 / 步进 / 分流 / 并线 / 并行 / 站点,横平竖直。并行线逐条不合并;单线圆点、多线胶囊。CourseCard 由路线串联成网。</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 构件:RouteLine 八原语(横平竖直 · 小圆角 · 并行不合并) -->
|
<!-- 构件:RouteLine 八原语(横平竖直 · 小圆角 · 并行不合并) -->
|
||||||
@@ -697,8 +697,8 @@
|
|||||||
<div class="vlabel"><b>dot</b> · 单线圆点(半嵌卡边)</div>
|
<div class="vlabel"><b>dot</b> · 单线圆点(半嵌卡边)</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="linecard">
|
<div class="linecard">
|
||||||
<svg width="176" height="44" viewBox="0 0 176 44"><rect x="6" y="8" width="26" height="28" rx="6" fill="#f4f4f2" stroke="#e2e2e0"/><rect x="144" y="8" width="26" height="28" rx="6" fill="#f4f4f2" stroke="#e2e2e0"/><path class="edge" d="M 32 18 H 144"/><path class="edge" d="M 32 22 H 144"/><path class="edge" d="M 32 26 H 144"/><ellipse class="station" cx="144" cy="22" rx="7" ry="4"/></svg>
|
<svg width="176" height="44" viewBox="0 0 176 44"><rect x="6" y="8" width="26" height="28" rx="6" fill="#f4f4f2" stroke="#e2e2e0"/><rect x="144" y="8" width="26" height="28" rx="6" fill="#f4f4f2" stroke="#e2e2e0"/><path class="edge" d="M 32 18 H 144"/><path class="edge" d="M 32 22 H 144"/><path class="edge" d="M 32 26 H 144"/><rect class="station" x="141" y="15" width="6" height="14" rx="3"/></svg>
|
||||||
<div class="vlabel"><b>flat</b> · 多线扁点(半嵌卡边)</div>
|
<div class="vlabel"><b>flat</b> · 多线胶囊(半嵌卡边)</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -747,11 +747,11 @@
|
|||||||
<circle class="station" cx="440" cy="252" r="2.8"/>
|
<circle class="station" cx="440" cy="252" r="2.8"/>
|
||||||
<circle class="station" cx="440" cy="265" r="2.8"/>
|
<circle class="station" cx="440" cy="265" r="2.8"/>
|
||||||
<circle class="station" cx="508" cy="322" r="2.8"/>
|
<circle class="station" cx="508" cy="322" r="2.8"/>
|
||||||
<ellipse class="station" cx="724" cy="194" rx="8" ry="4"/>
|
<rect class="station" x="721" y="187" width="6" height="14" rx="3"/>
|
||||||
<circle class="station" cx="860" cy="194" r="2.8"/>
|
<circle class="station" cx="860" cy="194" r="2.8"/>
|
||||||
</svg>
|
</svg>
|
||||||
<div style="position:absolute; right:14px; top:262px; font-size:11px; color:#6b6b6b;">→ 下一程:③ 高级教程</div>
|
<div style="position:absolute; right:14px; top:262px; font-size:11px; color:#6b6b6b;">→ 下一程:③ 高级教程</div>
|
||||||
<div style="position:absolute; right:12px; top:10px; font-size:11px; color:#6b6b6b;">分流 ×1 · 三线并行 ×1 · 圆点 ×10 · 扁点 ×1</div>
|
<div style="position:absolute; right:12px; top:10px; font-size:11px; color:#6b6b6b;">分流 ×1 · 三线并行 ×1 · 圆点 ×10 · 胶囊 ×1</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="position:absolute; top: 138px; left: 948px; width: 292px;" class="panel">
|
<div style="position:absolute; top: 138px; left: 948px; width: 292px;" class="panel">
|
||||||
@@ -761,7 +761,7 @@
|
|||||||
<tr><td class="k"><code>path</code></td><td>Segment[]</td><td>直段/转角/步进序列</td></tr>
|
<tr><td class="k"><code>path</code></td><td>Segment[]</td><td>直段/转角/步进序列</td></tr>
|
||||||
<tr><td class="k"><code>forks</code></td><td>Fork[]</td><td>分流岔口(分出车道)</td></tr>
|
<tr><td class="k"><code>forks</code></td><td>Fork[]</td><td>分流岔口(分出车道)</td></tr>
|
||||||
<tr><td class="k"><code>merges</code></td><td>Merge[]</td><td>直角并线点</td></tr>
|
<tr><td class="k"><code>merges</code></td><td>Merge[]</td><td>直角并线点</td></tr>
|
||||||
<tr><td class="k"><code>stations</code></td><td>Node[]</td><td>单线圆点 / 多线扁点,半嵌卡边</td></tr>
|
<tr><td class="k"><code>stations</code></td><td>Node[]</td><td>单线圆点 / 多线胶囊,半嵌卡边</td></tr>
|
||||||
<tr><td class="k"><code>status</code></td><td>无 prop</td><td>不在图上编码</td></tr>
|
<tr><td class="k"><code>status</code></td><td>无 prop</td><td>不在图上编码</td></tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
@@ -771,7 +771,7 @@
|
|||||||
<div class="note" style="margin-bottom:4px;"><span class="idx">2</span><span class="txt"><b>背景级线条</b>:鼠尾草绿 <code>#A2B49B</code> · 1.4px · 划隙 4:2;站点 <code>#758C72</code> 半嵌卡边。</span></div>
|
<div class="note" style="margin-bottom:4px;"><span class="idx">2</span><span class="txt"><b>背景级线条</b>:鼠尾草绿 <code>#A2B49B</code> · 1.4px · 划隙 4:2;站点 <code>#758C72</code> 半嵌卡边。</span></div>
|
||||||
<div class="note" style="margin-bottom:4px;"><span class="idx">3</span><span class="txt"><b>横平竖直</b>:走向只有水平与垂直,转折用小圆角 R8。</span></div>
|
<div class="note" style="margin-bottom:4px;"><span class="idx">3</span><span class="txt"><b>横平竖直</b>:走向只有水平与垂直,转折用小圆角 R8。</span></div>
|
||||||
<div class="note" style="margin-bottom:4px;"><span class="idx">4</span><span class="txt"><b>并行不合并</b>:多线并行逐条渲染,间距 4px,不合成一条。</span></div>
|
<div class="note" style="margin-bottom:4px;"><span class="idx">4</span><span class="txt"><b>并行不合并</b>:多线并行逐条渲染,间距 4px,不合成一条。</span></div>
|
||||||
<div class="note" style="margin-bottom:4px;"><span class="idx">5</span><span class="txt"><b>站点随线变形</b>:单线 → 圆点 r2.8;多线入同一卡口 → 扁点 rx8×ry4。</span></div>
|
<div class="note" style="margin-bottom:4px;"><span class="idx">5</span><span class="txt"><b>站点随线变形</b>:单线 → 圆点;多线 → 沿线束方向的胶囊(6×14,端头半圆)。</span></div>
|
||||||
<div class="note" style="margin-bottom:0;"><span class="idx">6</span><span class="txt">顺序为<b>示例占位</b>,填 courses.ts 自动更新。</span></div>
|
<div class="note" style="margin-bottom:0;"><span class="idx">6</span><span class="txt">顺序为<b>示例占位</b>,填 courses.ts 自动更新。</span></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
@@ -0,0 +1,294 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* html2poster.js — Single-page poster/long-image HTML → PDF converter
|
||||||
|
*
|
||||||
|
* Purpose: Convert a fixed-width, dynamic-height HTML poster into a single-page
|
||||||
|
* vector PDF with zero margins. This script is PURPOSE-BUILT for posters and
|
||||||
|
* infographics — it does NOT handle multi-page documents, A4 pagination, or
|
||||||
|
* document-style margins. For those, use html2pdf-next.js.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* node html2poster.js poster.html
|
||||||
|
* node html2poster.js poster.html --output out.pdf
|
||||||
|
* node html2poster.js poster.html --width 720px
|
||||||
|
* node html2poster.js poster.html --width 720px --max-height 8000
|
||||||
|
*
|
||||||
|
* What it does (in order):
|
||||||
|
* 1. Load HTML in Playwright
|
||||||
|
* 2. Force overflow:hidden on .poster/.page containers (clip decorative overflow)
|
||||||
|
* 3. Inject @page { margin: 0 } (override any existing margin)
|
||||||
|
* 4. Ensure html/body have margin:0, padding:0, matching background
|
||||||
|
* 5. Measure .poster scrollHeight (actual content height)
|
||||||
|
* 6. Generate single-page PDF with exact dimensions
|
||||||
|
*
|
||||||
|
* What it does NOT do:
|
||||||
|
* - No pagination / page breaks
|
||||||
|
* - No A4 fallback
|
||||||
|
* - No margin injection (always zero)
|
||||||
|
* - No cover adaptation
|
||||||
|
* - No pdf-lib post-processing
|
||||||
|
* - No continuous-canvas detection
|
||||||
|
* - No vertical overflow expansion (posters WANT overflow:hidden)
|
||||||
|
*/
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const { execSync, spawnSync } = require('child_process');
|
||||||
|
|
||||||
|
// ── Playwright resolution ──
|
||||||
|
|
||||||
|
function loadPlaywright() {
|
||||||
|
for (const name of ['playwright', 'playwright-core']) {
|
||||||
|
try { return require(name); } catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Node never searches npm's global dir, and when node itself was installed
|
||||||
|
// into a private prefix, playwright is usually installed right next to it.
|
||||||
|
// Resolve both by hand instead of reporting "not installed".
|
||||||
|
const Module = require('module');
|
||||||
|
const roots = new Set();
|
||||||
|
if (process.env.PLAYWRIGHT_PATH) roots.add(process.env.PLAYWRIGHT_PATH);
|
||||||
|
if (process.env.NODE_PATH) {
|
||||||
|
process.env.NODE_PATH.split(path.delimiter).filter(Boolean).forEach(p => roots.add(p));
|
||||||
|
}
|
||||||
|
// <prefix>/bin/node -> <prefix>/node_modules
|
||||||
|
roots.add(path.join(path.dirname(process.execPath), '..', 'node_modules'));
|
||||||
|
try {
|
||||||
|
const g = execSync('npm root -g', { stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim();
|
||||||
|
if (g) roots.add(g);
|
||||||
|
} catch (_) {}
|
||||||
|
|
||||||
|
for (const base of roots) {
|
||||||
|
for (const name of ['playwright', 'playwright-core']) {
|
||||||
|
const dir = path.join(base, name);
|
||||||
|
if (!fs.existsSync(path.join(dir, 'package.json'))) continue;
|
||||||
|
// Require by absolute path — createRequire() would still walk the
|
||||||
|
// node_modules chain and miss roots not named node_modules.
|
||||||
|
try { return Module.createRequire(__filename)(dir); } catch (_) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Chromium resolution (shared logic with html2pdf-next.js) ──
|
||||||
|
|
||||||
|
function resolveChromium(chromiumObj) {
|
||||||
|
let exe;
|
||||||
|
try { exe = chromiumObj.executablePath(); } catch (_) { exe = null; }
|
||||||
|
if (exe && fs.existsSync(exe)) return { status: 'ok', executablePath: exe };
|
||||||
|
|
||||||
|
const candidates = [
|
||||||
|
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
||||||
|
'/Applications/Chromium.app/Contents/MacOS/Chromium',
|
||||||
|
'/usr/bin/chromium-browser', '/usr/bin/chromium', '/usr/bin/google-chrome',
|
||||||
|
];
|
||||||
|
if (process.env.PLAYWRIGHT_CHROMIUM_PATH) candidates.unshift(process.env.PLAYWRIGHT_CHROMIUM_PATH);
|
||||||
|
|
||||||
|
for (const c of candidates) {
|
||||||
|
if (fs.existsSync(c)) return { status: 'fallback', executablePath: c };
|
||||||
|
}
|
||||||
|
return { status: 'missing', executablePath: exe || '' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── CLI parsing ──
|
||||||
|
|
||||||
|
function parseArgs(argv) {
|
||||||
|
const tokens = argv.slice(2);
|
||||||
|
let input = null, output = null, width = '720px', maxHeight = 16000;
|
||||||
|
|
||||||
|
for (let i = 0; i < tokens.length; i++) {
|
||||||
|
const t = tokens[i];
|
||||||
|
if (t === '--output' || t === '-o') output = tokens[++i];
|
||||||
|
else if (t === '--width') width = tokens[++i];
|
||||||
|
else if (t === '--max-height') maxHeight = parseInt(tokens[++i], 10);
|
||||||
|
else if (t === '--help' || t === '-h') {
|
||||||
|
console.log(`
|
||||||
|
Usage: node html2poster.js <input.html> [options]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--output, -o Output PDF path (default: input with .pdf extension)
|
||||||
|
--width Poster width (default: 720px)
|
||||||
|
--max-height Maximum allowed height in px (default: 16000, safety limit)
|
||||||
|
-h, --help Show this help
|
||||||
|
`);
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
else if (!input) input = t;
|
||||||
|
else if (!output) output = t;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!input) {
|
||||||
|
console.error('Error: No input HTML file specified.');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!output) {
|
||||||
|
output = input.replace(/\.html?$/i, '.pdf');
|
||||||
|
if (output === input) output = input + '.pdf';
|
||||||
|
}
|
||||||
|
|
||||||
|
return { input, output, width, maxHeight };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Main ──
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const { input, output, width, maxHeight } = parseArgs(process.argv);
|
||||||
|
const absIn = path.resolve(input);
|
||||||
|
const absOut = path.resolve(output);
|
||||||
|
|
||||||
|
if (!fs.existsSync(absIn)) {
|
||||||
|
console.error(`Error: File not found: ${absIn}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`\n🖼 html2poster — Single-page poster PDF generator`);
|
||||||
|
console.log(` Input: ${absIn}`);
|
||||||
|
console.log(` Output: ${absOut}`);
|
||||||
|
console.log(` Width: ${width}`);
|
||||||
|
|
||||||
|
// Load Playwright
|
||||||
|
const playwright = loadPlaywright();
|
||||||
|
if (!playwright) {
|
||||||
|
console.error('Error: playwright or playwright-core not installed.');
|
||||||
|
console.error(` Searched: require paths, PLAYWRIGHT_PATH, NODE_PATH,`);
|
||||||
|
console.error(` ${path.join(path.dirname(process.execPath), '..', 'node_modules')}, npm root -g`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { chromium } = playwright;
|
||||||
|
const bInfo = resolveChromium(chromium);
|
||||||
|
|
||||||
|
if (bInfo.status === 'missing') {
|
||||||
|
console.error('Error: No Chromium found. Run: npx playwright install chromium');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
if (bInfo.status === 'fallback') {
|
||||||
|
console.log(` ⚠ Using fallback Chromium: ${bInfo.executablePath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Launch browser
|
||||||
|
// 本地补丁:H2P_CHROMIUM 强制指定浏览器可执行文件,避免与用户正在使用的系统 Chrome 冲突;
|
||||||
|
// TMPDIR 由 build.sh 重定向到非配额磁盘(/tmp 配额超限会导致渲染子进程崩溃)。
|
||||||
|
const launchOpts = { headless: true };
|
||||||
|
if (process.env.H2P_CHROMIUM && fs.existsSync(process.env.H2P_CHROMIUM)) {
|
||||||
|
launchOpts.executablePath = process.env.H2P_CHROMIUM;
|
||||||
|
} else if (bInfo.status === 'fallback') {
|
||||||
|
launchOpts.executablePath = bInfo.executablePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
const browser = await chromium.launch(launchOpts);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Use a wide viewport so content doesn't wrap unexpectedly
|
||||||
|
const widthPx = parseInt(width, 10) || 720;
|
||||||
|
const page = await browser.newPage({ viewport: { width: widthPx, height: 1200 } });
|
||||||
|
|
||||||
|
await page.goto('file://' + absIn, { waitUntil: 'networkidle' });
|
||||||
|
console.log(`\n ✓ HTML loaded`);
|
||||||
|
|
||||||
|
// ── Step 1: Force overflow:hidden on page containers ──
|
||||||
|
// Decorative elements with negative offsets or width>100% inflate scrollWidth,
|
||||||
|
// causing Playwright to shrink content to fit. overflow:hidden clips them.
|
||||||
|
const overflowFixed = await page.evaluate(() => {
|
||||||
|
const selectors = ['.poster', '.page', '#poster', '#page'];
|
||||||
|
let fixed = 0;
|
||||||
|
for (const sel of selectors) {
|
||||||
|
const el = document.querySelector(sel);
|
||||||
|
if (!el) continue;
|
||||||
|
const computed = getComputedStyle(el);
|
||||||
|
if (computed.overflow !== 'hidden') {
|
||||||
|
el.style.overflow = 'hidden';
|
||||||
|
fixed++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fixed;
|
||||||
|
});
|
||||||
|
if (overflowFixed > 0) {
|
||||||
|
console.log(` ✓ Added overflow:hidden to ${overflowFixed} container(s)`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Step 2: Inject @page { margin: 0 } — override any existing @page rule ──
|
||||||
|
await page.evaluate(() => {
|
||||||
|
const s = document.createElement('style');
|
||||||
|
// Use !important-equivalent: place at end so it wins cascade
|
||||||
|
s.textContent = `@page { margin: 0 !important; size: auto; }`;
|
||||||
|
document.head.appendChild(s);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Step 3: Ensure html/body have zero margin/padding ──
|
||||||
|
const bgSync = await page.evaluate(() => {
|
||||||
|
const html = document.documentElement;
|
||||||
|
const body = document.body;
|
||||||
|
html.style.margin = '0';
|
||||||
|
html.style.padding = '0';
|
||||||
|
body.style.margin = '0';
|
||||||
|
body.style.padding = '0';
|
||||||
|
|
||||||
|
// Sync body background with poster background to avoid color gaps
|
||||||
|
const poster = document.querySelector('.poster') || document.querySelector('.page');
|
||||||
|
if (poster) {
|
||||||
|
const posterBg = getComputedStyle(poster).backgroundColor;
|
||||||
|
if (posterBg && posterBg !== 'rgba(0, 0, 0, 0)' && posterBg !== 'transparent') {
|
||||||
|
body.style.backgroundColor = posterBg;
|
||||||
|
html.style.backgroundColor = posterBg;
|
||||||
|
return posterBg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
if (bgSync) {
|
||||||
|
console.log(` ✓ Synced body background: ${bgSync}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Step 4: Measure actual content height ──
|
||||||
|
const measurement = await page.evaluate(() => {
|
||||||
|
const poster = document.querySelector('.poster') || document.querySelector('.page') || document.body;
|
||||||
|
return {
|
||||||
|
scrollHeight: poster.scrollHeight,
|
||||||
|
scrollWidth: poster.scrollWidth,
|
||||||
|
offsetWidth: poster.offsetWidth,
|
||||||
|
selector: poster.className ? '.' + poster.className.split(' ')[0] : poster.tagName,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(` ✓ Measured: ${measurement.selector} = ${measurement.scrollWidth}×${measurement.scrollHeight}px`);
|
||||||
|
|
||||||
|
if (measurement.scrollWidth > widthPx + 2) {
|
||||||
|
console.log(` ⚠ WARNING: scrollWidth (${measurement.scrollWidth}px) > width (${widthPx}px)`);
|
||||||
|
console.log(` Decorative elements may still overflow. Check for position:absolute elements with negative offsets.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
let contentHeight = measurement.scrollHeight;
|
||||||
|
if (contentHeight > maxHeight) {
|
||||||
|
console.log(` ⚠ Content height ${contentHeight}px exceeds max ${maxHeight}px, clamping.`);
|
||||||
|
contentHeight = maxHeight;
|
||||||
|
}
|
||||||
|
if (contentHeight < 100) {
|
||||||
|
console.log(` ⚠ Content height ${contentHeight}px seems too small, using 960px fallback.`);
|
||||||
|
contentHeight = 960;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Step 5: Generate PDF ──
|
||||||
|
console.log(`\n 📄 Generating PDF: ${width} × ${contentHeight}px`);
|
||||||
|
await page.pdf({
|
||||||
|
path: absOut,
|
||||||
|
width: width,
|
||||||
|
height: contentHeight + 'px',
|
||||||
|
printBackground: true,
|
||||||
|
margin: { top: '0', right: '0', bottom: '0', left: '0' },
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`\n ✅ Done: ${absOut}`);
|
||||||
|
console.log(` Size: ${(fs.statSync(absOut).size / 1024).toFixed(1)} KB`);
|
||||||
|
|
||||||
|
} finally {
|
||||||
|
await browser.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(err => {
|
||||||
|
console.error(`\n✗ Fatal: ${err.message}`);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
Reference in new issue
Block a user