PDF Scripts Analysis
/app/.kimi/skills/pdf/scripts/ — PDF Generation and Processing Tools
Executive Summary
The scripts/ directory contains 13 files implementing the PDF skill's multi-route architecture. These scripts handle HTML-to-PDF conversion (via Playwright + Paged.js), LaTeX compilation (via Tectonic), and existing PDF manipulation (via pikepdf/pdfplumber).
1. File Inventory
| File |
Type |
Size |
Purpose |
html_to_pdf.js |
Node.js |
~600 lines |
Main HTML→PDF conversion engine |
paged.polyfill.js |
Library |
922KB |
Paged.js pagination library (bundled) |
browser_helper.js |
Node.js |
~100 lines |
Playwright browser detection |
compile_latex.py |
Python |
~350 lines |
LaTeX compilation wrapper |
pdf.py |
Python |
~260 lines |
Unified CLI entry point |
pdf.sh |
Shell |
~80 lines |
Shell wrapper |
setup.sh |
Shell |
~50 lines |
Dependency checker |
package.json |
JSON |
~30 lines |
Node.js dependencies |
cmd_convert.py |
Python |
~100 lines |
Office→PDF conversion |
cmd_extract.py |
Python |
~150 lines |
Text/table/image extraction |
cmd_form.py |
Python |
~120 lines |
Form filling |
cmd_meta.py |
Python |
~80 lines |
Metadata operations |
cmd_pages.py |
Python |
~150 lines |
Merge/split/rotate/crop |
2. html_to_pdf.js — HTML to PDF Conversion Engine
2.1 Architecture
Input HTML
│
▼
┌─────────────────┐
│ Load & Inject │──→ Custom CSS (optional)
│ Playwright │
└─────────────────┘
│
▼
┌─────────────────┐
│ Detect Counters │──→ Warn if CSS counters used
│ Detect Mermaid │──→ Wait for SVG rendering
│ Detect KaTeX │──→ Trigger math rendering
└─────────────────┘
│
▼
┌─────────────────┐
│ Fix Counters │──→ Burn in counter values
│ Fix Tall Elt │──→ Remove page-break-inside
└─────────────────┘
│
▼
┌─────────────────┐
│ Inject Paged.js │──→ Local bundle or CDN
└─────────────────┘
│
▼
┌─────────────────┐
│ Wait for Pages │──→ Poll until count stable
└─────────────────┘
│
▼
┌─────────────────┐
│ Detect Overflow │──→ Warn if elements overflow
│ Get Statistics │──→ Word count, figures, tables
│ Detect Anomalies│──→ Blank/low-content pages
└─────────────────┘
│
▼
Export PDF (scale=1.5 workaround)
2.2 Key Features
CSS Counter Detection
// Detect counter-reset, counter-increment, content: counter()
const counterWarnings = await page.evaluate(() => {
const warnings = [];
const styles = document.querySelectorAll('style');
styles.forEach((style, idx) => {
const css = style.textContent || '';
// Check for counter-reset (excluding @page)
const counterResetMatches = css.match(/[^@]counter-reset\s*:\s*([^;]+)/gi);
// Check for counter-increment
const counterIncMatches = css.match(/counter-increment\s*:\s*([^;]+)/gi);
// Check for content: counter()
if (/content\s*:\s*[^;]*counter\s*\(/i.test(css)) {
// ... warn
}
});
return warnings;
});
Why This Matters: Paged.js reorders DOM during pagination, breaking CSS counters. The script warns users to use data-* attributes instead.
Counter Fix (Burn-in)
// "Burn in" counter values as data attributes before Paged.js runs
const counterFixCount = await page.evaluate(() => {
let fixedCount = 0;
const olLists = document.querySelectorAll('ol');
olLists.forEach(ol => {
const liItems = ol.querySelectorAll(':scope > li');
// Check if using custom numbering
const hasCustomNumbering = computedStyle.listStyleType === 'none';
if (hasCustomNumbering) {
ol.setAttribute('data-counter-fixed', 'true');
liItems.forEach((li, index) => {
li.setAttribute('data-counter', index + 1);
fixedCount++;
});
}
});
// Inject CSS to use data-counter
const style = document.createElement('style');
style.textContent = `
ol[data-counter-fixed] { counter-reset: none !important; }
ol[data-counter-fixed] > li::before {
content: "[" attr(data-counter) "] " !important;
}
`;
document.head.appendChild(style);
return fixedCount;
});
Mermaid Rendering Wait
const hasMermaid = await page.evaluate(() => {
return document.querySelectorAll('.mermaid').length > 0;
});
if (hasMermaid) {
console.log('Waiting for Mermaid diagrams to render...');
await page.waitForFunction(() => {
const mermaids = document.querySelectorAll('.mermaid');
for (const m of mermaids) {
if (!m.querySelector('svg') && !m.getAttribute('data-processed')) {
return false;
}
}
return true;
}, { timeout: 30000 });
await delay(2000); // Stabilization time
}
KaTeX Rendering
const katexStatus = await page.evaluate(() => {
const hasKatexLib = typeof renderMathInElement === 'function' || typeof katex !== 'undefined';
const hasRenderedKatex = document.querySelectorAll('.katex').length > 0;
const hasUnrenderedMath = /\$[^$]+\$/.test(document.body.innerText);
return { hasKatexLib, hasRenderedKatex, hasUnrenderedMath };
});
if (katexStatus.hasKatexLib && !katexStatus.hasRenderedKatex && katexStatus.hasUnrenderedMath) {
await page.evaluate(() => {
renderMathInElement(document.body, {
delimiters: [
{left: "$$", right: "$$", display: true},
{left: "$", right: "$", display: false}
],
throwOnError: false
});
});
await delay(1000);
}
Paged.js Integration
// Prefer local bundle, fallback to CDN
const pagedJsLocal = path.join(__dirname, 'paged.polyfill.js');
if (fs.existsSync(pagedJsLocal)) {
const pagedJsContent = fs.readFileSync(pagedJsLocal, 'utf-8');
await page.addScriptTag({ content: pagedJsContent });
} else {
await page.addScriptTag({
url: 'https://unpkg.com/pagedjs@0.4.3/dist/paged.polyfill.js'
});
}
Pagination Stability Detection
// Wait for page count to stabilize (no change for 3 consecutive checks)
let lastPageCount = 0;
let stableCount = 0;
const maxWaitTime = 120000; // 2 minutes max
while (stableCount < 3) {
if (Date.now() - startTime > maxWaitTime) {
console.log('Warning: Pagination timeout');
break;
}
await delay(1000);
const currentPageCount = await page.evaluate(() =>
document.querySelectorAll('.pagedjs_page').length
);
if (currentPageCount === lastPageCount) {
stableCount++;
} else {
stableCount = 0;
lastPageCount = currentPageCount;
console.log(` Pagination in progress: ${currentPageCount} pages...`);
}
}
Overflow Detection
const overflows = await page.evaluate(() => {
const results = [];
const selectors = 'pre, table, figure, img, svg, .mermaid, blockquote, .equation';
document.querySelectorAll(selectors).forEach(el => {
const scrollW = el.scrollWidth;
const clientW = el.clientWidth;
if (scrollW > clientW + 2) { // +2 tolerance
results.push({
tag: el.tagName.toLowerCase(),
class: el.className || '',
overflow: scrollW - clientW,
preview: (el.textContent || '').slice(0, 60)
});
}
});
return results;
});
PDF Export (Scale Workaround)
// WORKAROUND: scale=1.5 fixes page filling issue
// Without this, content renders at ~67% size
// Root cause unclear, appears related to Paged.js internal scaling
await page.pdf({
path: outputPath,
format: 'A4',
printBackground: true,
preferCSSPageSize: true,
tagged: true,
scale: 1.5 // Critical workaround
});
2.3 Output Statistics
========================
PDF Information
========================
File: document.pdf
Pages: 12
Size: 245.6 KB
Words: ~3,456
Figures/Tables: 5 figures / 3 tables
Path: /mnt/okcomputer/output/document.pdf
========================
Anomaly Detection
========================
P8: [Low content] 45 words (avg 320, only 14%)
3. compile_latex.py — LaTeX Compilation Wrapper
3.1 Purpose
Wraps the Tectonic LaTeX compiler with:
- Log filtering (removes noise)
- Multiple compilation passes
- PDF statistics extraction
- Error/warning categorization
3.2 Log Filtering
FILTER_PATTERNS = [
r'^note: "version 2" Tectonic command-line interface activated',
r'^note: Running TeX',
r'^note: Rerunning TeX because',
r'^note: Running xdvipdfmx',
r'^note: downloading ',
r'^note: Skipped writing .* intermediate files',
]
3.3 Issue Categorization
| Category |
Pattern |
Severity |
| Errors |
^error: |
Critical |
| Warnings |
^warning: |
Medium |
| Layout |
Overfull/Underfull \\[hv]box |
Medium |
| Fonts |
`Font shape |
Missing character` |
3.4 PDF Statistics
def extract_pdf_info(pdf_path):
from pypdf import PdfReader
reader = PdfReader(pdf_path)
num_pages = len(reader.pages)
text = ''.join(page.extract_text() for page in reader.pages)
word_count = len([w for w in text.split() if w.strip()])
# Count images
image_count = 0
for page in reader.pages:
if '/XObject' in page['/Resources']:
xobjects = page['/Resources']['/XObject'].get_object()
for obj in xobjects:
if xobjects[obj]['/Subtype'] == '/Image':
image_count += 1
return num_pages, word_count, image_count
4. pdf.py — Unified CLI Entry
4.1 Command Structure
pdf.py <command> <subcommand> [options]
| Command |
Subcommands |
Description |
form |
info, fill |
Form field inspection and filling |
extract |
text, table, image |
Content extraction |
pages |
merge, split, rotate, crop |
Page operations |
meta |
get, set |
Metadata operations |
convert |
— |
Format conversion |
4.2 Output Format
// Success
{"status": "success", "data": {...}}
// Error
{"status": "error", "error": "ErrorType", "message": "...", "hint": "..."}
4.3 Exit Codes
| Code |
Meaning |
| 0 |
Success |
| 1 |
Argument error |
| 2 |
File not found |
| 3 |
PDF parse error |
| 4 |
Operation failed |
5. paged.polyfill.js — Paged.js Library
5.1 Identity
- Size: 922KB (bundled)
- Version: 0.4.3
- License: MIT
- Purpose: CSS Paged Media polyfill for browser-based pagination
5.2 Why Bundled?
- Offline Operation: No CDN dependency
- Version Stability: Fixed, tested version
- Performance: Local file load faster than network
- Reliability: Avoids CDN availability issues
5.3 Key Features
@page rule processing
- Page break control (
page-break-before, page-break-after, page-break-inside)
- Running headers/footers (
@top-center, @bottom-center)
- Named pages (
page: cover)
- Page number counters (
counter(page))
6. Inter-Script Relationships
html_to_pdf.js
├── Playwright (browser automation)
│ └── Chromium
├── paged.polyfill.js (pagination)
└── browser_helper.js (browser detection)
compile_latex.py
├── Tectonic (LaTeX compiler)
└── pypdf (PDF analysis)
pdf.py
├── cmd_form.py (pikepdf)
├── cmd_extract.py (pdfplumber)
├── cmd_pages.py (pikepdf)
├── cmd_meta.py (pikepdf)
└── cmd_convert.py (LibreOffice)
7. Code Metrics
| Script |
Lines |
Language |
Purpose |
| html_to_pdf.js |
~600 |
Node.js |
HTML→PDF conversion |
| compile_latex.py |
~350 |
Python |
LaTeX compilation |
| pdf.py |
~260 |
Python |
CLI entry |
| cmd_extract.py |
~150 |
Python |
Content extraction |
| cmd_pages.py |
~150 |
Python |
Page operations |
| cmd_form.py |
~120 |
Python |
Form filling |
| cmd_meta.py |
~80 |
Python |
Metadata |
| cmd_convert.py |
~100 |
Python |
Format conversion |
| browser_helper.js |
~100 |
Node.js |
Browser detection |
| pdf.sh |
~80 |
Shell |
Wrapper |
| setup.sh |
~50 |
Shell |
Setup |
Document Version: 1.0
Analysis Date: 2026-02-02